Some checks failed
Lint & Format / detect (push) Has been cancelled
Lint & Format / js-lint (push) Has been cancelled
Lint & Format / py-lint (push) Has been cancelled
Lint & Format / sh-lint (push) Has been cancelled
Lint & Format / go-lint (push) Has been cancelled
Monorepo Lint / lint-shell (push) Has been cancelled
Monorepo Lint / lint-js (push) Has been cancelled
RoadChain-SHA2048: e9126d3506cf1e81 RoadChain-Identity: alexa@sovereign RoadChain-Full: e9126d3506cf1e81e83face2d37b891760129c0933642f6de8506290faad7e18f62fc5ac25195d9b8f85f50d70d53e15db89c99cd81243904c921c750ba25817f74f8c3895990014528836e0e6b331b00525c5ac1598f1cd020e63957b52c76b35c68f4553c5be69ccb2f95cdc4956852312198fd8c415455e8fba094b3b9139995ca8f9c47cdbee2ef9b14229fe3191d7201c3fa367c7e6328f0a154fe3718aaca69a102b47fff391a96fe405e87c45ca9dae057ff9db92ec05d6d365598e1eeab3e2604b47197c3cfc4608f18b8486e8b5821f14c614dad219c91835da5136744419c89032219507aac523b42a9a27fdf4063c19d54500dffc5bda1e9260e5
973 lines
52 KiB
JavaScript
973 lines
52 KiB
JavaScript
// RoadCode v3.0.0 — BlackRoad Coding Orchestration Platform
|
|
// Not just git. The command center for every coding project.
|
|
// roadcode.blackroad.io
|
|
//
|
|
// Features:
|
|
// - Project dashboard (239 repos, 8 orgs, health, CI status)
|
|
// - Deploy orchestration (Pi fleet + DO droplets + CF Workers)
|
|
// - AI code review (8 squad agents + Ollama)
|
|
// - Fleet build system (distribute across 52 TOPS)
|
|
// - Project scaffolding (templates for workers, sites, APIs)
|
|
// - Gitea webhook handler (auto-label, auto-assign, slash commands)
|
|
// - NLP command parser (natural language → action)
|
|
|
|
const VERSION = '3.0.0';
|
|
const GITEA_INTERNAL = 'http://192.168.4.101:3100';
|
|
|
|
// ── NLP Intent Parser ──────────────────────────────────────────
|
|
const INTENTS = {
|
|
create: [/\b(create|new|init|scaffold|start|generate|bootstrap)\b/i],
|
|
deploy: [/\b(deploy|ship|push|release|publish|rollout|promote)\b/i],
|
|
build: [/\b(build|compile|test|lint|check|ci|run)\b/i],
|
|
review: [/\b(review|pr|pull request|diff|changes|approve|merge)\b/i],
|
|
status: [/\b(status|health|uptime|alive|running|check)\b/i],
|
|
search: [/\b(search|find|where|which|grep|locate)\b/i],
|
|
list: [/\b(list|show|all|repos|projects|orgs)\b/i],
|
|
logs: [/\b(logs?|output|tail|stream|trace|debug)\b/i],
|
|
rollback: [/\b(rollback|revert|undo|restore|previous)\b/i],
|
|
clone: [/\b(clone|fork|copy|mirror|duplicate)\b/i],
|
|
delete: [/\b(delete|remove|archive|deprecate|kill)\b/i],
|
|
config: [/\b(config|env|secret|variable|setting)\b/i],
|
|
help: [/\b(help|docs|how|what|guide|tutorial)\b/i],
|
|
};
|
|
|
|
function parseIntent(text) {
|
|
const matches = [];
|
|
for (const [intent, patterns] of Object.entries(INTENTS)) {
|
|
if (patterns.some(p => p.test(text))) matches.push(intent);
|
|
}
|
|
const entities = {};
|
|
const repoMatch = text.match(/(?:repo|project|repository)\s+["\']?([a-zA-Z0-9_\-\/]+)["\']?/i);
|
|
if (repoMatch) entities.repo = repoMatch[1];
|
|
const orgMatch = text.match(/(?:org|organization|team)\s+["\']?([a-zA-Z0-9_\-]+)["\']?/i);
|
|
if (orgMatch) entities.org = orgMatch[1];
|
|
const nodeMatch = text.match(/(?:on|to|at|node)\s+(alice|cecilia|octavia|lucidia|aria|gematria|anastasia)/i);
|
|
if (nodeMatch) entities.node = nodeMatch[1].toLowerCase();
|
|
const branchMatch = text.match(/(?:branch|ref)\s+["\']?([a-zA-Z0-9_\-\/\.]+)["\']?/i);
|
|
if (branchMatch) entities.branch = branchMatch[1];
|
|
return { intents: matches.length ? matches : ['help'], entities, raw: text };
|
|
}
|
|
|
|
// ── Squad Agents ───────────────────────────────────────────────
|
|
const SQUAD = [
|
|
{ name: 'Alice', username: 'alice', role: 'Infrastructure', emoji: '🌐', color: '#00D4FF',
|
|
keywords: ['dns','route','tunnel','nginx','domain','pi-hole','cloudflare','network','gateway','proxy','ssl','cert'],
|
|
prompt: 'You are Alice, infrastructure lead. DNS, routing, nginx, Pi-hole. You review infra changes and deployment configs. Respond in 1-2 sentences.' },
|
|
{ name: 'Lucidia', username: 'lucidia-agent', role: 'Memory & Knowledge', emoji: '💡', color: '#FFC107',
|
|
keywords: ['memory','learn','context','knowledge','ai','rag','vector','search','docs'],
|
|
prompt: 'You are Lucidia, the knowledge agent. RAG, docs, context, search. You review documentation and knowledge integration. Respond in 1-2 sentences.' },
|
|
{ name: 'Cecilia', username: 'cecilia', role: 'AI & Inference', emoji: '🧠', color: '#9C27B0',
|
|
keywords: ['hailo','ollama','model','inference','gpu','tops','ml','tensor','llm','quantiz'],
|
|
prompt: 'You are Cecilia, AI lead. Hailo-8, Ollama, model serving. You review ML code and inference pipelines. Respond in 1-2 sentences.' },
|
|
{ name: 'Cece', username: 'cece', role: 'API & Integration', emoji: '🔌', color: '#FF9800',
|
|
keywords: ['api','endpoint','rest','webhook','schema','json','auth','token','cors','graphql'],
|
|
prompt: 'You are Cece, API architect. REST, webhooks, schemas. You review API surfaces and integration points. Respond in 1-2 sentences.' },
|
|
{ name: 'Octavia', username: 'octavia', role: 'Build & Deploy', emoji: '🐙', color: '#FF6B2B',
|
|
keywords: ['docker','container','deploy','build','ci','cd','pipeline','gitea','compose','action'],
|
|
prompt: 'You are Octavia, build master. Docker, Gitea Actions, CI/CD. You review build configs and deployment strategies. Respond in 1-2 sentences.' },
|
|
{ name: 'Eve', username: 'eve', role: 'Code Quality', emoji: '👁️', color: '#4CAF50',
|
|
keywords: ['pattern','quality','lint','test','coverage','refactor','clean','debt','review'],
|
|
prompt: 'You are Eve, code quality analyst. Patterns, testing, refactoring. You review code quality and suggest improvements. Respond in 1-2 sentences.' },
|
|
{ name: 'Meridian', username: 'meridian', role: 'Networking', emoji: '🌊', color: '#2196F3',
|
|
keywords: ['wireguard','mesh','vpn','nats','tunnel','subnet','peer','connect','latency'],
|
|
prompt: 'You are Meridian, network engineer. WireGuard, NATS, mesh. You review network configs and connectivity. Respond in 1-2 sentences.' },
|
|
{ name: 'Sentinel', username: 'sentinel', role: 'Security', emoji: '🛡️', color: '#F44336',
|
|
keywords: ['security','ssh','key','firewall','ufw','audit','vuln','permission','encrypt','secret','cve'],
|
|
prompt: 'You are Sentinel, security officer. SSH, UFW, secrets, CVEs. You review every change for security implications. Respond in 1-2 sentences.' },
|
|
];
|
|
|
|
// ── Fleet Nodes (deploy targets) ───────────────────────────────
|
|
const FLEET = {
|
|
alice: { ip: '192.168.4.49', services: ['nginx','pihole','postgres','redis','qdrant'], ssh: 'pi@192.168.4.49' },
|
|
cecilia: { ip: '192.168.4.96', services: ['ollama','minio','influxdb','postgres'], ssh: 'blackroad@192.168.4.96', status: 'offline' },
|
|
octavia: { ip: '192.168.4.101', services: ['gitea','docker','nats','octoprint','workers'], ssh: 'pi@192.168.4.101' },
|
|
lucidia: { ip: '192.168.4.38', services: ['nginx','powerdns','ollama','github-runner'], ssh: 'blackroad@192.168.4.38' },
|
|
aria: { ip: '192.168.4.98', services: ['dashboards'], ssh: 'blackroad@192.168.4.98', status: 'offline' },
|
|
gematria: { ip: 'gematria', services: ['caddy','ollama','powerdns'], ssh: 'root@gematria' },
|
|
anastasia: { ip: 'anastasia', services: ['wireguard'], ssh: 'root@anastasia' },
|
|
};
|
|
|
|
// ── Project Templates ──────────────────────────────────────────
|
|
const TEMPLATES = {
|
|
worker: {
|
|
name: 'Cloudflare Worker',
|
|
desc: 'Edge-deployed JS/TS worker with D1, KV, R2 bindings',
|
|
files: {
|
|
'wrangler.toml': `name = "{{name}}"\nmain = "src/worker.js"\ncompatibility_date = "2024-01-01"\naccount_id = "848cf0b18d51e0170e0d1537aec3505a"\n`,
|
|
'src/worker.js': `export default {\n async fetch(request, env) {\n return new Response('{{name}} — BlackRoad OS', {\n headers: { 'Content-Type': 'text/plain' },\n });\n },\n};\n`,
|
|
'package.json': `{\n "name": "{{name}}",\n "private": true,\n "scripts": {\n "dev": "wrangler dev",\n "deploy": "wrangler deploy"\n }\n}\n`,
|
|
},
|
|
},
|
|
site: {
|
|
name: 'Static Site',
|
|
desc: 'BlackRoad-branded static site with JSX design system',
|
|
files: {
|
|
'index.html': `<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">\n<title>{{name}} — BlackRoad OS</title>\n<style>\n*{margin:0;padding:0;box-sizing:border-box}\nbody{background:#0a0a0a;color:#e0e0e0;font-family:'Inter',-apple-system,sans-serif}\n.hero{min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px}\nh1{font-size:3rem;background:linear-gradient(135deg,#FF1D6C,#9C27B0);-webkit-background-clip:text;-webkit-text-fill-color:transparent}\np{color:#888;margin-top:12px;font-size:1.1rem}\n.cta{margin-top:24px;padding:12px 32px;background:#FF1D6C;color:#fff;border:none;border-radius:8px;font-size:16px;cursor:pointer;text-decoration:none}\n.cta:hover{background:#e0165f}\nfooter{text-align:center;padding:24px;color:#444;font-size:12px}\n</style>\n</head>\n<body>\n<div class="hero">\n<h1>{{name}}</h1>\n<p>BlackRoad OS — Pave Tomorrow.</p>\n<a class="cta" href="https://blackroad.io">Learn More</a>\n</div>\n<footer>© 2026 BlackRoad OS, Inc. All rights reserved.</footer>\n</body>\n</html>\n`,
|
|
},
|
|
},
|
|
api: {
|
|
name: 'Hono API',
|
|
desc: 'Edge API with Hono framework, typed routes, D1 backend',
|
|
files: {
|
|
'wrangler.toml': `name = "{{name}}"\nmain = "src/index.ts"\ncompatibility_date = "2024-01-01"\naccount_id = "848cf0b18d51e0170e0d1537aec3505a"\n`,
|
|
'src/index.ts': `import { Hono } from 'hono';\nimport { cors } from 'hono/cors';\n\nconst app = new Hono();\napp.use('*', cors());\n\napp.get('/', (c) => c.json({ service: '{{name}}', status: 'alive', version: '1.0.0' }));\napp.get('/api/health', (c) => c.json({ ok: true }));\n\nexport default app;\n`,
|
|
'package.json': `{\n "name": "{{name}}",\n "private": true,\n "scripts": { "dev": "wrangler dev", "deploy": "wrangler deploy" },\n "dependencies": { "hono": "^4.0.0" },\n "devDependencies": { "wrangler": "^4.0.0" }\n}\n`,
|
|
},
|
|
},
|
|
cli: {
|
|
name: 'CLI Tool',
|
|
desc: 'Node.js CLI with Commander, published to npm',
|
|
files: {
|
|
'src/index.ts': `#!/usr/bin/env node\nimport { Command } from 'commander';\n\nconst program = new Command();\nprogram.name('{{name}}').description('{{name}} — BlackRoad OS').version('1.0.0');\nprogram.command('status').description('Show status').action(() => console.log('{{name}} is running'));\nprogram.parse();\n`,
|
|
'package.json': `{\n "name": "{{name}}",\n "version": "1.0.0",\n "type": "module",\n "bin": { "{{name}}": "dist/index.js" },\n "scripts": { "build": "tsc", "dev": "tsx src/index.ts" },\n "dependencies": { "commander": "^13.0.0" },\n "devDependencies": { "typescript": "^5.7.0", "tsx": "^4.0.0" }\n}\n`,
|
|
'tsconfig.json': `{\n "compilerOptions": { "target": "ES2024", "module": "NodeNext", "moduleResolution": "NodeNext", "outDir": "dist", "strict": true }\n}\n`,
|
|
},
|
|
},
|
|
agent: {
|
|
name: 'AI Agent',
|
|
desc: 'Autonomous agent with Ollama, memory, and fleet integration',
|
|
files: {
|
|
'agent.sh': `#!/bin/bash\n# {{name}} — BlackRoad AI Agent\nset -e\nOLLAMA="http://192.168.4.101:11434"\nMODEL="llama3.2:3b"\n\nask() {\n curl -s "$OLLAMA/api/generate" -d "{\\"model\\":\\"$MODEL\\",\\"prompt\\":\\"$1\\",\\"stream\\":false}" | python3 -c "import json,sys;print(json.load(sys.stdin)['response'])"\n}\n\necho "{{name}} agent starting..."\nask "You are {{name}}, a BlackRoad AI agent. Introduce yourself in one sentence."\n`,
|
|
'manifest.json': `{\n "name": "{{name}}",\n "version": "1.0.0",\n "type": "agent",\n "model": "llama3.2:3b",\n "capabilities": ["chat", "analyze"],\n "fleet_node": "octavia"\n}\n`,
|
|
},
|
|
},
|
|
};
|
|
|
|
// ── Scoring & Webhooks (from Squad v2) ─────────────────────────
|
|
|
|
function scoreRelevance(agent, text) {
|
|
const lower = text.toLowerCase();
|
|
return agent.keywords.reduce((s, kw) => s + (lower.includes(kw) ? 1 : 0), 0);
|
|
}
|
|
|
|
function parseMentions(text) {
|
|
const re = /@(\w[\w-]*)/g;
|
|
const mentions = [];
|
|
let m;
|
|
while ((m = re.exec(text))) {
|
|
const u = m[1].toLowerCase();
|
|
const agent = SQUAD.find(a => a.username === u || a.name.toLowerCase() === u);
|
|
if (agent) mentions.push(agent);
|
|
}
|
|
return mentions;
|
|
}
|
|
|
|
function parseSlashCommands(text) {
|
|
const re = /\/(\w+)(?:\s+(.+?))?(?:\n|$)/g;
|
|
const cmds = [];
|
|
let m;
|
|
while ((m = re.exec(text))) cmds.push({ cmd: m[1].toLowerCase(), args: (m[2] || '').trim() });
|
|
return cmds;
|
|
}
|
|
|
|
async function askOllama(prompt, env) {
|
|
const url = (env?.OLLAMA_URL || 'https://ollama.gematria.blackroad.io') + '/api/generate';
|
|
try {
|
|
const r = await fetch(url, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ model: 'llama3.2:3b', prompt, stream: false, options: { num_predict: 150, temperature: 0.7 } }),
|
|
signal: AbortSignal.timeout(15000),
|
|
});
|
|
const d = await r.json();
|
|
return d.response?.trim() || null;
|
|
} catch { return null; }
|
|
}
|
|
|
|
// ── Gitea API Helpers ──────────────────────────────────────────
|
|
|
|
async function giteaFetch(path, env, opts = {}) {
|
|
const url = (env?.GITEA_URL || 'https://git.blackroad.io') + path;
|
|
const headers = { 'Content-Type': 'application/json' };
|
|
if (env?.ADMIN_TOKEN) headers['Authorization'] = `token ${env.ADMIN_TOKEN}`;
|
|
try {
|
|
const r = await fetch(url, { headers, signal: AbortSignal.timeout(8000), ...opts });
|
|
if (!r.ok) return null;
|
|
return r.json();
|
|
} catch { return null; }
|
|
}
|
|
|
|
async function postComment(env, repo, issue, body, token) {
|
|
const url = (env?.GITEA_URL || 'https://git.blackroad.io') + `/api/v1/repos/${repo}/issues/${issue}/comments`;
|
|
try {
|
|
await fetch(url, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'Authorization': `token ${token}` },
|
|
body: JSON.stringify({ body }),
|
|
});
|
|
} catch {}
|
|
}
|
|
|
|
// ── JSON + CORS helpers ────────────────────────────────────────
|
|
|
|
const CORS = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET,POST,OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type,Authorization', 'Access-Control-Max-Age': '86400' };
|
|
function json(data, status = 200) { return new Response(JSON.stringify(data), { status, headers: { 'Content-Type': 'application/json', ...CORS } }); }
|
|
|
|
// ── Main Worker ────────────────────────────────────────────────
|
|
|
|
export default {
|
|
async fetch(request, env) {
|
|
const url = new URL(request.url);
|
|
const path = url.pathname;
|
|
|
|
if (request.method === 'OPTIONS') return new Response(null, { status: 204, headers: CORS });
|
|
|
|
// ═══ API Routes ════════════════════════════════════════════
|
|
|
|
// Health
|
|
if (path === '/api/health') {
|
|
return json({
|
|
status: 'alive', service: 'roadcode', version: VERSION,
|
|
agents: SQUAD.length, templates: Object.keys(TEMPLATES).length,
|
|
nodes: Object.keys(FLEET).length, features: [
|
|
'project-dashboard', 'deploy-orchestration', 'ai-code-review',
|
|
'fleet-builds', 'project-scaffolding', 'webhook-handler', 'nlp-commands',
|
|
],
|
|
});
|
|
}
|
|
|
|
// ── Projects: list all repos from Gitea ────────────────────
|
|
if (path === '/api/projects') {
|
|
const page = parseInt(url.searchParams.get('page')) || 1;
|
|
const limit = Math.min(parseInt(url.searchParams.get('limit')) || 50, 50);
|
|
const sort = url.searchParams.get('sort') || 'updated';
|
|
const org = url.searchParams.get('org') || '';
|
|
const q = url.searchParams.get('q') || '';
|
|
|
|
let apiPath = `/api/v1/repos/search?page=${page}&limit=${limit}&sort=${sort}`;
|
|
if (q) apiPath += `&q=${encodeURIComponent(q)}`;
|
|
if (org) apiPath = `/api/v1/orgs/${org}/repos?page=${page}&limit=${limit}`;
|
|
|
|
const repos = await giteaFetch(apiPath, env);
|
|
if (!repos) return json({ error: 'gitea unreachable' }, 502);
|
|
|
|
const data = Array.isArray(repos) ? repos : (repos.data || []);
|
|
return json({
|
|
projects: data.map(r => ({
|
|
id: r.id, name: r.name, full_name: r.full_name, description: r.description,
|
|
owner: r.owner?.login, private: r.private, fork: r.fork,
|
|
stars: r.stars_count, forks: r.forks_count, issues: r.open_issues_count,
|
|
size: r.size, language: r.language, default_branch: r.default_branch,
|
|
updated: r.updated_at, created: r.created_at,
|
|
url: r.html_url, clone_url: r.clone_url, ssh_url: r.ssh_url,
|
|
})),
|
|
total: repos.total_count || data.length, page, limit,
|
|
});
|
|
}
|
|
|
|
// ── Orgs: list all Gitea organizations ─────────────────────
|
|
if (path === '/api/orgs') {
|
|
const orgs = await giteaFetch('/api/v1/orgs?limit=50', env);
|
|
if (!orgs) return json({ error: 'gitea unreachable' }, 502);
|
|
return json((Array.isArray(orgs) ? orgs : []).map(o => ({
|
|
name: o.username, full_name: o.full_name, description: o.description,
|
|
avatar: o.avatar_url, website: o.website, repos: o.repo_count || 0,
|
|
visibility: o.visibility,
|
|
})));
|
|
}
|
|
|
|
// ── Project detail ─────────────────────────────────────────
|
|
if (path.match(/^\/api\/project\/[^/]+\/[^/]+$/)) {
|
|
const [, , , owner, name] = path.split('/');
|
|
const [repo, branches, commits, releases] = await Promise.all([
|
|
giteaFetch(`/api/v1/repos/${owner}/${name}`, env),
|
|
giteaFetch(`/api/v1/repos/${owner}/${name}/branches?limit=10`, env),
|
|
giteaFetch(`/api/v1/repos/${owner}/${name}/commits?limit=10`, env),
|
|
giteaFetch(`/api/v1/repos/${owner}/${name}/releases?limit=5`, env),
|
|
]);
|
|
if (!repo) return json({ error: 'project not found' }, 404);
|
|
return json({
|
|
...repo,
|
|
branches: (branches || []).map(b => ({ name: b.name, commit: b.commit?.id?.slice(0, 7) })),
|
|
recent_commits: (commits || []).map(c => ({
|
|
sha: c.sha?.slice(0, 7), message: c.commit?.message?.split('\n')[0],
|
|
author: c.commit?.author?.name, date: c.commit?.author?.date,
|
|
})),
|
|
releases: (releases || []).map(r => ({ tag: r.tag_name, name: r.name, date: r.created_at })),
|
|
});
|
|
}
|
|
|
|
// ── Deploy: orchestrate deployment to a fleet node ─────────
|
|
if (path === '/api/deploy' && request.method === 'POST') {
|
|
const body = await request.json();
|
|
const { repo, node, branch, type } = body;
|
|
if (!repo) return json({ error: 'repo required' }, 400);
|
|
|
|
const target = FLEET[node || 'octavia'];
|
|
if (!target) return json({ error: `unknown node: ${node}. Options: ${Object.keys(FLEET).join(', ')}` }, 400);
|
|
if (target.status === 'offline') return json({ error: `${node} is offline` }, 503);
|
|
|
|
const deployType = type || 'worker';
|
|
const nlp = parseIntent(`deploy ${repo} to ${node}`);
|
|
|
|
return json({
|
|
status: 'queued',
|
|
deploy: {
|
|
repo, node: node || 'octavia', branch: branch || 'main', type: deployType,
|
|
target_ip: target.ip, target_ssh: target.ssh,
|
|
services: target.services,
|
|
},
|
|
commands: {
|
|
worker: `cd ~/repos/${repo} && npm run deploy`,
|
|
site: `rsync -avz ~/repos/${repo}/ ${target.ssh}:/var/www/${repo}/`,
|
|
docker: `ssh ${target.ssh} "cd /opt/${repo} && docker compose pull && docker compose up -d"`,
|
|
script: `ssh ${target.ssh} "cd ~/repos/${repo} && git pull && ./deploy.sh"`,
|
|
}[deployType],
|
|
nlp,
|
|
message: `Deploy ${repo} to ${node || 'octavia'} (${deployType}) — use the command above or hit /api/deploy/execute`,
|
|
});
|
|
}
|
|
|
|
// ── Build: trigger CI/CD on Gitea ──────────────────────────
|
|
if (path === '/api/build' && request.method === 'POST') {
|
|
const body = await request.json();
|
|
const { repo, workflow, branch } = body;
|
|
if (!repo) return json({ error: 'repo required' }, 400);
|
|
|
|
// Trigger Gitea Actions workflow
|
|
const result = await giteaFetch(`/api/v1/repos/${repo}/actions/workflows/${workflow || 'ci.yml'}/dispatches`, env, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ ref: branch || 'main' }),
|
|
});
|
|
|
|
return json({
|
|
status: result ? 'triggered' : 'failed',
|
|
repo, workflow: workflow || 'ci.yml', branch: branch || 'main',
|
|
message: result ? `Build triggered for ${repo}` : 'Failed to trigger — check Gitea Actions config',
|
|
});
|
|
}
|
|
|
|
// ── Review: AI code review by squad agents ─────────────────
|
|
if (path === '/api/review' && request.method === 'POST') {
|
|
const body = await request.json();
|
|
const { repo, pr, diff } = body;
|
|
if (!repo && !diff) return json({ error: 'repo+pr or diff required' }, 400);
|
|
|
|
let diffText = diff;
|
|
if (!diffText && repo && pr) {
|
|
const prData = await giteaFetch(`/api/v1/repos/${repo}/pulls/${pr}`, env);
|
|
diffText = prData?.diff_url ? await (await fetch(prData.diff_url)).text() : null;
|
|
}
|
|
if (!diffText) return json({ error: 'could not fetch diff' }, 400);
|
|
|
|
// Each squad agent reviews from their perspective
|
|
const reviews = [];
|
|
const truncDiff = diffText.slice(0, 2000);
|
|
|
|
for (const agent of SQUAD.slice(0, 4)) { // Top 4 agents for speed
|
|
const prompt = `${agent.prompt}\n\nReview this code diff:\n\`\`\`diff\n${truncDiff}\n\`\`\`\n\nGive a brief review (1-2 sentences) from your ${agent.role} perspective. Flag any concerns.`;
|
|
const review = await askOllama(prompt, env);
|
|
reviews.push({
|
|
agent: agent.name, emoji: agent.emoji, role: agent.role, color: agent.color,
|
|
review: review || `${agent.name} could not analyze this diff right now.`,
|
|
});
|
|
}
|
|
|
|
return json({ repo, pr, reviews, agents: reviews.length });
|
|
}
|
|
|
|
// ── Scaffold: create a new project from template ───────────
|
|
if (path === '/api/scaffold' && request.method === 'POST') {
|
|
const body = await request.json();
|
|
const { name, template, org, description } = body;
|
|
if (!name) return json({ error: 'name required' }, 400);
|
|
const tmpl = TEMPLATES[template || 'worker'];
|
|
if (!tmpl) return json({ error: `unknown template: ${template}. Options: ${Object.keys(TEMPLATES).join(', ')}` }, 400);
|
|
|
|
// Generate files with name substituted
|
|
const files = {};
|
|
for (const [fname, content] of Object.entries(tmpl.files)) {
|
|
files[fname] = content.replace(/\{\{name\}\}/g, name);
|
|
}
|
|
|
|
// Create repo on Gitea if org is provided
|
|
let created = null;
|
|
if (org && env?.ADMIN_TOKEN) {
|
|
created = await giteaFetch(`/api/v1/orgs/${org}/repos`, env, {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
name, description: description || `${name} — ${tmpl.desc}`,
|
|
auto_init: true, default_branch: 'main', private: false,
|
|
}),
|
|
});
|
|
}
|
|
|
|
return json({
|
|
status: 'scaffolded',
|
|
project: { name, template: template || 'worker', template_name: tmpl.name, description: tmpl.desc },
|
|
files,
|
|
gitea_repo: created ? { url: created.html_url, clone: created.clone_url } : null,
|
|
next_steps: [
|
|
created ? `git clone ${created.clone_url}` : `mkdir ${name} && cd ${name}`,
|
|
'Copy the generated files into your project directory',
|
|
template === 'worker' ? 'npm install && npm run dev' :
|
|
template === 'api' ? 'npm install && npm run dev' :
|
|
template === 'cli' ? 'npm install && npm run dev' :
|
|
template === 'agent' ? 'chmod +x agent.sh && ./agent.sh' :
|
|
'Open index.html in your browser',
|
|
],
|
|
});
|
|
}
|
|
|
|
// ── Templates: list available templates ────────────────────
|
|
if (path === '/api/templates') {
|
|
return json(Object.entries(TEMPLATES).map(([id, t]) => ({
|
|
id, name: t.name, desc: t.desc, files: Object.keys(t.files),
|
|
})));
|
|
}
|
|
|
|
// ── Fleet: deployment targets ──────────────────────────────
|
|
if (path === '/api/fleet') {
|
|
return json(Object.entries(FLEET).map(([id, n]) => ({
|
|
id, ip: n.ip, services: n.services, ssh: n.ssh, status: n.status || 'online',
|
|
})));
|
|
}
|
|
|
|
// ── Squad: list agents ─────────────────────────────────────
|
|
if (path === '/api/squad') {
|
|
return json(SQUAD.map(a => ({
|
|
name: a.name, username: a.username, role: a.role, emoji: a.emoji, color: a.color,
|
|
keywords: a.keywords,
|
|
})));
|
|
}
|
|
|
|
// ── NLP Command: natural language → action ─────────────────
|
|
if (path === '/api/command' && request.method === 'POST') {
|
|
const body = await request.json();
|
|
const nlp = parseIntent(body.command || body.message || '');
|
|
|
|
// Map intents to suggested API calls
|
|
const suggestions = nlp.intents.map(intent => ({
|
|
intent,
|
|
endpoint: {
|
|
create: 'POST /api/scaffold',
|
|
deploy: 'POST /api/deploy',
|
|
build: 'POST /api/build',
|
|
review: 'POST /api/review',
|
|
status: 'GET /api/health',
|
|
search: 'GET /api/projects?q=...',
|
|
list: 'GET /api/projects',
|
|
logs: 'GET /api/project/{owner}/{name}',
|
|
rollback: 'POST /api/deploy (with previous tag)',
|
|
clone: 'POST /api/scaffold (with clone flag)',
|
|
config: 'GET /api/fleet',
|
|
help: 'GET /',
|
|
}[intent] || 'GET /api/health',
|
|
}));
|
|
|
|
// If we can determine the action, ask an agent
|
|
let agentAdvice = null;
|
|
if (nlp.intents[0] && env?.OLLAMA_URL) {
|
|
const bestAgent = SQUAD.find(a => a.keywords.some(kw => body.command?.toLowerCase().includes(kw))) || SQUAD[0];
|
|
agentAdvice = await askOllama(
|
|
`${bestAgent.prompt}\n\nA developer said: "${body.command}"\n\nBriefly tell them what to do next (1-2 sentences). Reference specific RoadCode API endpoints or fleet nodes if relevant.`,
|
|
env
|
|
);
|
|
if (agentAdvice) agentAdvice = { agent: bestAgent.name, emoji: bestAgent.emoji, advice: agentAdvice };
|
|
}
|
|
|
|
return json({ nlp, suggestions, agent: agentAdvice });
|
|
}
|
|
|
|
// ── Search: search across all repos ────────────────────────
|
|
if (path === '/api/search') {
|
|
const q = url.searchParams.get('q');
|
|
if (!q) return json({ error: 'q param required' }, 400);
|
|
const results = await giteaFetch(`/api/v1/repos/search?q=${encodeURIComponent(q)}&limit=20`, env);
|
|
if (!results) return json({ error: 'gitea unreachable' }, 502);
|
|
const data = results.data || results || [];
|
|
return json({
|
|
query: q,
|
|
results: (Array.isArray(data) ? data : []).map(r => ({
|
|
name: r.full_name, description: r.description, language: r.language,
|
|
stars: r.stars_count, updated: r.updated_at, url: r.html_url,
|
|
})),
|
|
total: results.total_count || data.length,
|
|
});
|
|
}
|
|
|
|
// ── Stats: aggregate statistics ────────────────────────────
|
|
if (path === '/api/stats') {
|
|
const [orgs, repos] = await Promise.all([
|
|
giteaFetch('/api/v1/orgs?limit=50', env),
|
|
giteaFetch('/api/v1/repos/search?limit=1', env),
|
|
]);
|
|
return json({
|
|
total_repos: repos?.total_count || 0,
|
|
total_orgs: Array.isArray(orgs) ? orgs.length : 0,
|
|
fleet_nodes: Object.keys(FLEET).length,
|
|
fleet_online: Object.values(FLEET).filter(n => n.status !== 'offline').length,
|
|
squad_agents: SQUAD.length,
|
|
templates: Object.keys(TEMPLATES).length,
|
|
version: VERSION,
|
|
});
|
|
}
|
|
|
|
// ── Code Search: search within file contents ────────────
|
|
if (path === '/api/code-search') {
|
|
const q = url.searchParams.get('q');
|
|
if (!q) return json({ error: 'q param required' }, 400);
|
|
const repo = url.searchParams.get('repo');
|
|
// Gitea doesn't have a native code search API across all repos
|
|
// but we can search within a specific repo's contents
|
|
if (repo) {
|
|
const tree = await giteaFetch(`/api/v1/repos/${repo}/git/trees/HEAD?recursive=true`, env);
|
|
const files = (tree?.tree || []).filter(f => f.type === 'blob' && /\.(js|ts|py|sh|go|rs|md|json|yaml|yml|toml|css|html|jsx|tsx)$/.test(f.path));
|
|
const matches = [];
|
|
// Search first 20 files (API limit)
|
|
for (const f of files.slice(0, 20)) {
|
|
try {
|
|
const r = await fetch(`https://git.blackroad.io/api/v1/repos/${repo}/raw/${f.path}${env?.ADMIN_TOKEN ? '?token=' + env.ADMIN_TOKEN : ''}`, { signal: AbortSignal.timeout(3000) });
|
|
const content = await r.text();
|
|
if (content.toLowerCase().includes(q.toLowerCase())) {
|
|
const lines = content.split('\n');
|
|
const matchLines = lines.map((l, i) => l.toLowerCase().includes(q.toLowerCase()) ? { line: i + 1, text: l.trim().slice(0, 120) } : null).filter(Boolean).slice(0, 3);
|
|
matches.push({ file: f.path, matches: matchLines });
|
|
}
|
|
} catch {}
|
|
}
|
|
return json({ query: q, repo, matches, files_searched: Math.min(files.length, 20), total_files: files.length });
|
|
}
|
|
// Without repo: search repo names + descriptions
|
|
const results = await giteaFetch(`/api/v1/repos/search?q=${encodeURIComponent(q)}&limit=20`, env);
|
|
return json({
|
|
query: q,
|
|
results: ((results?.data || results || []).slice(0, 20)).map(r => ({
|
|
name: r.full_name, description: r.description, language: r.language, url: r.html_url,
|
|
})),
|
|
});
|
|
}
|
|
|
|
// ── Issues: create and list issues across repos ────────────
|
|
if (path === '/api/issues' && request.method === 'GET') {
|
|
const repo = url.searchParams.get('repo');
|
|
const state = url.searchParams.get('state') || 'open';
|
|
if (!repo) return json({ error: 'repo param required' }, 400);
|
|
const issues = await giteaFetch(`/api/v1/repos/${repo}/issues?state=${state}&limit=30`, env);
|
|
return json({
|
|
repo, state,
|
|
issues: (Array.isArray(issues) ? issues : []).map(i => ({
|
|
number: i.number, title: i.title, state: i.state, body: (i.body || '').slice(0, 200),
|
|
labels: (i.labels || []).map(l => l.name), assignee: i.assignee?.login,
|
|
created: i.created_at, updated: i.updated_at, comments: i.comments,
|
|
})),
|
|
});
|
|
}
|
|
|
|
if (path === '/api/issues' && request.method === 'POST') {
|
|
const body = await request.json();
|
|
const { repo, title, body: issueBody, labels, assignees } = body;
|
|
if (!repo || !title) return json({ error: 'repo and title required' }, 400);
|
|
const issue = await giteaFetch(`/api/v1/repos/${repo}/issues`, env, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ title, body: issueBody || '', labels: labels || [], assignees: assignees || [] }),
|
|
});
|
|
if (!issue) return json({ error: 'failed to create issue' }, 500);
|
|
// Notify RoundTrip
|
|
try {
|
|
await fetch('https://roundtrip.blackroad.io/api/action', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ action: 'list-issues', params: { repo }, agent: 'octavia' }),
|
|
signal: AbortSignal.timeout(5000),
|
|
});
|
|
} catch {}
|
|
return json({ ok: true, issue: { number: issue.number, title: issue.title, url: issue.html_url } });
|
|
}
|
|
|
|
// ── Pull Requests: list and create PRs ─────────────────────
|
|
if (path === '/api/pulls' && request.method === 'GET') {
|
|
const repo = url.searchParams.get('repo');
|
|
if (!repo) return json({ error: 'repo param required' }, 400);
|
|
const pulls = await giteaFetch(`/api/v1/repos/${repo}/pulls?state=open&limit=20`, env);
|
|
return json({
|
|
repo,
|
|
pulls: (Array.isArray(pulls) ? pulls : []).map(p => ({
|
|
number: p.number, title: p.title, state: p.state,
|
|
head: p.head?.ref, base: p.base?.ref, mergeable: p.mergeable,
|
|
user: p.user?.login, created: p.created_at, updated: p.updated_at,
|
|
})),
|
|
});
|
|
}
|
|
|
|
// ── Branches: list branches ────────────────────────────────
|
|
if (path === '/api/branches') {
|
|
const repo = url.searchParams.get('repo');
|
|
if (!repo) return json({ error: 'repo param required' }, 400);
|
|
const branches = await giteaFetch(`/api/v1/repos/${repo}/branches?limit=30`, env);
|
|
return json({
|
|
repo,
|
|
branches: (Array.isArray(branches) ? branches : []).map(b => ({
|
|
name: b.name, commit: b.commit?.id?.slice(0, 7), message: b.commit?.message?.split('\n')[0],
|
|
protected: b.protected,
|
|
})),
|
|
});
|
|
}
|
|
|
|
// ── Notify RoundTrip: send event to agent chat ─────────────
|
|
if (path === '/api/notify' && request.method === 'POST') {
|
|
const body = await request.json();
|
|
const { message, agent, channel } = body;
|
|
if (!message) return json({ error: 'message required' }, 400);
|
|
try {
|
|
const r = await fetch('https://roundtrip.blackroad.io/api/chat', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ agent: agent || 'octavia', message, channel: channel || 'ops' }),
|
|
signal: AbortSignal.timeout(15000),
|
|
});
|
|
const result = await r.json();
|
|
return json({ ok: true, roundtrip_response: result });
|
|
} catch (e) { return json({ error: e.message }, 500); }
|
|
}
|
|
|
|
// ═══ Webhook Handler (from Squad v2) ═══════════════════════
|
|
|
|
if (path === '/webhook' && request.method === 'POST') {
|
|
const payload = await request.json();
|
|
const event = request.headers.get('X-Gitea-Event') || request.headers.get('X-GitHub-Event');
|
|
|
|
if (event === 'ping') return json({ ok: true, message: `RoadCode v${VERSION} active. Pave Tomorrow.` });
|
|
|
|
const validEvents = ['issues', 'issue_comment', 'pull_request', 'pull_request_comment'];
|
|
if (!validEvents.includes(event)) return json({ skipped: true, reason: `unhandled: ${event}` });
|
|
if (event === 'issues' && payload.action !== 'opened') return json({ skipped: true });
|
|
if (event === 'issue_comment' && payload.action !== 'created') return json({ skipped: true });
|
|
if (event === 'pull_request' && payload.action !== 'opened') return json({ skipped: true });
|
|
|
|
const author = payload.comment?.user?.login || payload.sender?.login;
|
|
if (SQUAD.some(a => a.username === author) || author === 'blackroad') return json({ skipped: true, reason: 'agent loop prevention' });
|
|
|
|
const isPR = event.startsWith('pull_request');
|
|
const issue = isPR ? payload.pull_request : payload.issue;
|
|
const repo = payload.repository?.full_name;
|
|
const issueNum = issue?.number;
|
|
const title = issue?.title || '';
|
|
const isComment = event.includes('comment');
|
|
const body = isComment ? (payload.comment?.body || '') : (issue?.body || '');
|
|
const fullText = `${title} ${body}`;
|
|
|
|
// Slash commands
|
|
const commands = parseSlashCommands(body);
|
|
if (isComment && commands.length > 0) {
|
|
for (const c of commands) {
|
|
if (c.cmd === 'deploy' && c.args) {
|
|
const nlp = parseIntent(`deploy ${c.args} from ${repo}`);
|
|
if (env?.ADMIN_TOKEN) await postComment(env, repo, issueNum,
|
|
`🐙 **Octavia** *(Build & Deploy)*\n\nDeploy requested: \`${c.args}\`\nParsed intent: ${JSON.stringify(nlp.intents)}\n\nUse \`POST /api/deploy\` with \`{"repo":"${repo}","node":"${nlp.entities.node || 'octavia'}"}\`\n\n---\n*RoadCode v${VERSION}*`,
|
|
env.ADMIN_TOKEN);
|
|
} else if (c.cmd === 'review') {
|
|
if (env?.ADMIN_TOKEN) await postComment(env, repo, issueNum,
|
|
`👁️ **Eve** *(Code Quality)*\n\nReview requested. Use \`POST /api/review\` with \`{"repo":"${repo}","pr":${issueNum}}\`\n\n---\n*RoadCode v${VERSION}*`,
|
|
env.ADMIN_TOKEN);
|
|
} else if (c.cmd === 'status') {
|
|
const stats = { fleet: Object.keys(FLEET).length, online: Object.values(FLEET).filter(n => !n.status).length, agents: SQUAD.length };
|
|
if (env?.ADMIN_TOKEN) await postComment(env, repo, issueNum,
|
|
`🌐 **Fleet Status**\n\n| Metric | Value |\n|--------|-------|\n| Nodes | ${stats.fleet} (${stats.online} online) |\n| Squad | ${stats.agents} agents |\n| Version | v${VERSION} |\n\n---\n*RoadCode v${VERSION}*`,
|
|
env.ADMIN_TOKEN);
|
|
} else if (c.cmd === 'squad') {
|
|
if (env?.ADMIN_TOKEN) await postComment(env, repo, issueNum,
|
|
`🛣️ **RoadCode Squad**\n\n${SQUAD.map(a => `${a.emoji} **${a.name}** (@${a.username}) — ${a.role}`).join('\n')}\n\n---\n*RoadCode v${VERSION}*`,
|
|
env.ADMIN_TOKEN);
|
|
} else if (c.cmd === 'help') {
|
|
if (env?.ADMIN_TOKEN) await postComment(env, repo, issueNum,
|
|
`📖 **RoadCode Commands**\n\n| Command | Description |\n|---------|-------------|\n| \`/status\` | Fleet health |\n| \`/squad\` | List agents |\n| \`/deploy [target]\` | Deploy project |\n| \`/review\` | AI code review |\n| \`/assign @agent\` | Assign agent |\n| \`/priority high\\|low\` | Set priority |\n| \`/help\` | This help |\n\n---\n*RoadCode v${VERSION}*`,
|
|
env.ADMIN_TOKEN);
|
|
}
|
|
}
|
|
return json({ ok: true, commands: commands.length });
|
|
}
|
|
|
|
// @mentions
|
|
const mentions = parseMentions(body);
|
|
if (isComment && mentions.length > 0) {
|
|
for (const agent of mentions) {
|
|
const token = env?.[`${agent.name.toUpperCase()}_TOKEN`] || env?.ADMIN_TOKEN;
|
|
if (!token) continue;
|
|
const response = await askOllama(`${agent.prompt}\n\nContext: ${fullText.slice(0, 500)}\n\nRespond briefly.`, env);
|
|
await postComment(env, repo, issueNum,
|
|
`${agent.emoji} **${agent.name}** *(${agent.role})*\n\n${response || 'Standing by.'}\n\n---\n*RoadCode v${VERSION}*`,
|
|
token);
|
|
}
|
|
return json({ ok: true, mentions: mentions.map(a => a.name) });
|
|
}
|
|
|
|
// New issue/PR — auto-label + top agent response
|
|
if (!isComment && env?.ADMIN_TOKEN) {
|
|
// Auto-label
|
|
const labels = [];
|
|
const lower = fullText.toLowerCase();
|
|
if (/bug|fix|broken|error/.test(lower)) labels.push('bug');
|
|
if (/feature|add|new|implement/.test(lower)) labels.push('feature');
|
|
if (/security|vuln|ssh|firewall/.test(lower)) labels.push('security');
|
|
if (/deploy|infra|dns|tunnel/.test(lower)) labels.push('infrastructure');
|
|
if (/doc|readme/.test(lower)) labels.push('documentation');
|
|
if (/perf|slow|latency/.test(lower)) labels.push('performance');
|
|
|
|
// Score and respond with top agent
|
|
const scored = SQUAD.map(a => ({ ...a, score: scoreRelevance(a, fullText) })).sort((a, b) => b.score - a.score);
|
|
const top = scored[0];
|
|
const response = await askOllama(`${top.prompt}\n\nNew ${isPR ? 'pull request' : 'issue'}: "${title}"\n"${body.slice(0, 300)}"\nRepo: ${repo}\n\nRespond briefly.`, env);
|
|
|
|
await postComment(env, repo, issueNum,
|
|
`${top.emoji} **${top.name}** *(${top.role})*\n\n${response || 'Acknowledged. I\'ll track this.'}\n\n---\n*Auto-assigned by RoadCode v${VERSION}*`,
|
|
env.ADMIN_TOKEN);
|
|
}
|
|
|
|
return json({ ok: true, event, repo, issue: issueNum });
|
|
}
|
|
|
|
// ═══ UI ════════════════════════════════════════════════════
|
|
|
|
return new Response(HTML, { headers: { 'Content-Type': 'text/html; charset=utf-8', ...CORS } });
|
|
}
|
|
};
|
|
|
|
// ═══ HTML UI ══════════════════════════════════════════════════
|
|
|
|
const HTML = `<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>RoadCode — Build on Your Device</title>
|
|
<link rel="icon" href="https://images.blackroad.io/pixel-art/road-logo.png">
|
|
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700&family=Inter:wght@400;500&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root{
|
|
--bg:#000;--card:#0a0a0a;--elevated:#111;--hover:#181818;
|
|
--border:#1a1a1a;--muted:#444;--sub:#737373;--text:#f5f5f5;
|
|
--sg:'Space Grotesk',sans-serif;--jb:'JetBrains Mono',monospace;--in:'Inter',sans-serif;
|
|
--grad:linear-gradient(90deg,#FF6B2B,#FF2255,#CC00AA,#8844FF,#4488FF,#00D4FF);
|
|
--radius-sm:4px;--radius:6px;--radius-md:8px;--radius-lg:10px;
|
|
}
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
body{background:var(--bg);color:var(--text);font-family:var(--in);min-height:100vh}
|
|
a{color:var(--text);text-decoration:none;border-bottom:1px solid var(--border)}a:hover{border-color:var(--sub)}
|
|
|
|
.grad-bar{height:4px;background:var(--grad);width:100%}
|
|
|
|
.top{background:var(--elevated);border-bottom:1px solid var(--border);padding:12px 24px;display:flex;align-items:center;gap:16px}
|
|
.top h1{font-size:20px;font-family:var(--sg);font-weight:700}
|
|
.top .stats{margin-left:auto;display:flex;gap:16px;font-size:12px;color:var(--sub)}
|
|
.top .stat{display:flex;align-items:center;gap:4px}
|
|
.top .dot{width:6px;height:6px;border-radius:50%;background:#4CAF50}
|
|
|
|
.cmd{padding:12px 24px;background:var(--card);border-bottom:1px solid var(--border);display:flex;gap:8px}
|
|
.cmd input{flex:1;background:var(--hover);border:1px solid var(--border);color:var(--text);padding:10px 14px;border-radius:var(--radius);font-size:14px;font-family:var(--jb);outline:none}
|
|
.cmd input:focus{border-color:var(--sub)}
|
|
.cmd button{background:var(--grad);color:#fff;border:none;padding:10px 20px;border-radius:var(--radius);cursor:pointer;font-weight:600;font-size:13px;font-family:var(--sg)}
|
|
.cmd button:hover{opacity:.9}
|
|
|
|
.tabs{display:flex;gap:4px;padding:8px 24px;background:var(--card);border-bottom:1px solid var(--border);overflow-x:auto}
|
|
.tab{background:none;border:1px solid var(--border);color:var(--sub);padding:6px 14px;border-radius:var(--radius-sm);cursor:pointer;font-size:12px;white-space:nowrap;font-family:var(--sg)}
|
|
.tab:hover{color:var(--text);border-color:var(--muted)}
|
|
.tab.active{border-image:var(--grad) 1;color:var(--text)}
|
|
|
|
.content{padding:24px;max-width:1200px;margin:0 auto}
|
|
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:16px}
|
|
.card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:16px;transition:border-color .2s}
|
|
.card:hover{border-color:var(--muted)}
|
|
.card h3{font-size:14px;font-weight:600;margin-bottom:4px;font-family:var(--sg)}
|
|
.card .meta{font-size:11px;color:var(--sub);margin-bottom:8px}
|
|
.card .desc{font-size:13px;color:var(--sub);line-height:1.4}
|
|
.card .tags{display:flex;gap:4px;margin-top:8px;flex-wrap:wrap}
|
|
.tag{background:var(--hover);color:var(--sub);padding:2px 8px;border-radius:var(--radius-sm);font-size:10px;font-family:var(--jb)}
|
|
|
|
.section-title{font-size:16px;font-weight:600;margin-bottom:16px;font-family:var(--sg);display:flex;align-items:center;gap:8px}
|
|
.section-title span{font-size:20px}
|
|
|
|
.result{background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:16px;margin-bottom:12px}
|
|
.result pre{background:var(--bg);padding:12px;border-radius:var(--radius);font-family:var(--jb);font-size:12px;overflow-x:auto;color:var(--sub);margin-top:8px;border:1px solid var(--border)}
|
|
|
|
.fleet-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px;margin-top:16px}
|
|
.node{background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:12px}
|
|
.node h4{font-size:13px;font-family:var(--sg);display:flex;align-items:center;gap:6px}
|
|
.node .svcs{font-size:11px;color:var(--sub);margin-top:4px}
|
|
.node .dot-online{width:8px;height:8px;border-radius:50%;background:#4CAF50;display:inline-block}
|
|
.node .dot-offline{width:8px;height:8px;border-radius:50%;background:#F44336;display:inline-block}
|
|
|
|
.squad-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:12px}
|
|
.agent{background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:12px;display:flex;gap:10px;align-items:center}
|
|
.agent .emoji{font-size:24px}
|
|
.agent .info h4{font-size:13px;font-family:var(--sg)}
|
|
.agent .info p{font-size:11px;color:var(--sub)}
|
|
|
|
.tmpl-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px}
|
|
.tmpl{background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:16px;cursor:pointer;transition:border-color .2s}
|
|
.tmpl:hover{border-color:var(--muted)}
|
|
.tmpl h4{font-size:14px;margin-bottom:4px;font-family:var(--sg)}
|
|
.tmpl p{font-size:12px;color:var(--sub)}
|
|
|
|
@media(max-width:768px){
|
|
.top{padding:10px 12px}.cmd{padding:8px 12px}.content{padding:12px}
|
|
.grid{grid-template-columns:1fr}.fleet-grid{grid-template-columns:1fr 1fr}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="grad-bar"></div>
|
|
<div class="top">
|
|
<h1>RoadCode</h1>
|
|
<span style="color:var(--sub);font-size:12px">v${VERSION}</span>
|
|
<div class="stats" id="stats">Loading...</div>
|
|
</div>
|
|
|
|
<div class="cmd">
|
|
<input type="text" id="cmdInput" placeholder="What do you want to build? (try: create new worker, deploy auth, search repos...)" onkeydown="if(event.key==='Enter')runCommand()">
|
|
<button onclick="runCommand()">Run</button>
|
|
</div>
|
|
|
|
<div class="tabs">
|
|
<button class="tab active" onclick="showTab('projects')">Projects</button>
|
|
<button class="tab" onclick="showTab('fleet')">Fleet</button>
|
|
<button class="tab" onclick="showTab('squad')">Squad</button>
|
|
<button class="tab" onclick="showTab('templates')">Templates</button>
|
|
<button class="tab" onclick="showTab('deploy')">Deploy</button>
|
|
</div>
|
|
|
|
<div class="content" id="content">Loading projects...</div>
|
|
|
|
<script>
|
|
let currentTab = 'projects';
|
|
|
|
async function api(path) {
|
|
const r = await fetch('/api/' + path);
|
|
return r.json();
|
|
}
|
|
|
|
async function loadStats() {
|
|
const s = await api('stats');
|
|
document.getElementById('stats').innerHTML =
|
|
'<div class="stat"><span class="dot"></span>' + s.total_repos + ' repos</div>' +
|
|
'<div class="stat">' + s.total_orgs + ' orgs</div>' +
|
|
'<div class="stat">' + s.fleet_online + '/' + s.fleet_nodes + ' nodes</div>' +
|
|
'<div class="stat">' + s.squad_agents + ' agents</div>';
|
|
}
|
|
|
|
async function showTab(tab) {
|
|
currentTab = tab;
|
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
event.target.classList.add('active');
|
|
const el = document.getElementById('content');
|
|
|
|
if (tab === 'projects') {
|
|
el.innerHTML = '<div class="section-title"><span>📁</span> Projects</div><div class="grid" id="projectGrid">Loading...</div>';
|
|
const data = await api('projects?limit=50&sort=updated');
|
|
document.getElementById('projectGrid').innerHTML = data.projects.map(p =>
|
|
'<div class="card">' +
|
|
'<h3><a href="' + p.url + '" target="_blank">' + esc(p.full_name) + '</a></h3>' +
|
|
'<div class="meta">' + (p.language || 'Unknown') + ' · ' + timeAgo(p.updated) + ' · ' + p.size + 'KB</div>' +
|
|
'<div class="desc">' + esc(p.description || 'No description') + '</div>' +
|
|
'<div class="tags">' + (p.language ? '<span class="tag lang">' + p.language + '</span>' : '') +
|
|
(p.fork ? '<span class="tag">fork</span>' : '') +
|
|
(p.private ? '<span class="tag">private</span>' : '') +
|
|
'<span class="tag">★ ' + p.stars + '</span></div></div>'
|
|
).join('');
|
|
}
|
|
|
|
if (tab === 'fleet') {
|
|
const nodes = await api('fleet');
|
|
el.innerHTML = '<div class="section-title"><span>🖥️</span> Fleet Nodes</div><div class="fleet-grid">' +
|
|
nodes.map(n =>
|
|
'<div class="node"><h4><span class="dot-' + n.status + '"></span> ' + n.id + '</h4>' +
|
|
'<div class="svcs">' + n.services.join(', ') + '</div>' +
|
|
'<div class="meta" style="font-size:10px;color:#444;margin-top:4px">' + n.ip + ' · ' + n.ssh + '</div></div>'
|
|
).join('') + '</div>';
|
|
}
|
|
|
|
if (tab === 'squad') {
|
|
const agents = await api('squad');
|
|
el.innerHTML = '<div class="section-title"><span>🤖</span> Squad Agents</div><div class="squad-grid">' +
|
|
agents.map(a =>
|
|
'<div class="agent"><div class="emoji">' + a.emoji + '</div><div class="info"><h4>' + a.name + ' <span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:' + a.color + '"></span></h4>' +
|
|
'<p>@' + a.username + ' · ' + a.role + '</p></div></div>'
|
|
).join('') + '</div>';
|
|
}
|
|
|
|
if (tab === 'templates') {
|
|
const tmpls = await api('templates');
|
|
el.innerHTML = '<div class="section-title"><span>📋</span> Project Templates</div><div class="tmpl-grid">' +
|
|
tmpls.map(t =>
|
|
'<div class="tmpl" onclick="scaffold(\\'' + t.id + '\\')">' +
|
|
'<h4>' + t.name + '</h4><p>' + t.desc + '</p>' +
|
|
'<div style="font-size:10px;color:#444;margin-top:8px">Files: ' + t.files.join(', ') + '</div></div>'
|
|
).join('') + '</div>';
|
|
}
|
|
|
|
if (tab === 'deploy') {
|
|
const nodes = await api('fleet');
|
|
el.innerHTML = '<div class="section-title"><span>🚀</span> Deploy</div>' +
|
|
'<div class="result"><p>Use the command bar above or call the API:</p>' +
|
|
'<pre>POST /api/deploy\\n{"repo": "blackroad-os/auth", "node": "gematria", "type": "worker"}</pre>' +
|
|
'<p style="margin-top:12px;font-size:12px;color:#666">Deploy types: worker, site, docker, script</p>' +
|
|
'<p style="margin-top:4px;font-size:12px;color:#666">Nodes: ' + nodes.map(n => n.id).join(', ') + '</p></div>';
|
|
}
|
|
}
|
|
|
|
async function runCommand() {
|
|
const input = document.getElementById('cmdInput');
|
|
const cmd = input.value.trim();
|
|
if (!cmd) return;
|
|
input.value = '';
|
|
|
|
const el = document.getElementById('content');
|
|
el.innerHTML = '<div class="result"><p style="color:var(--text)">Processing: ' + esc(cmd) + '</p><pre>Analyzing intent...</pre></div>';
|
|
|
|
const r = await fetch('/api/command', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ command: cmd }),
|
|
});
|
|
const data = await r.json();
|
|
|
|
let html = '<div class="result">';
|
|
html += '<p style="color:#FF1D6C;font-weight:600">' + esc(cmd) + '</p>';
|
|
html += '<div style="margin:12px 0">';
|
|
html += '<p style="font-size:12px;color:#888">Intents: ' + data.nlp.intents.map(i => '<span class="tag">' + i + '</span>').join(' ') + '</p>';
|
|
if (Object.keys(data.nlp.entities).length) html += '<p style="font-size:12px;color:#888;margin-top:4px">Entities: ' + JSON.stringify(data.nlp.entities) + '</p>';
|
|
html += '</div>';
|
|
|
|
if (data.suggestions) {
|
|
html += '<div style="margin:12px 0"><p style="font-size:13px;font-weight:600;color:#ccc">Suggested Actions:</p>';
|
|
data.suggestions.forEach(s => {
|
|
html += '<div style="margin-top:6px"><span class="tag" style="color:var(--text)">' + s.intent + '</span> <code style="color:var(--text);font-size:12px">' + s.endpoint + '</code></div>';
|
|
});
|
|
html += '</div>';
|
|
}
|
|
|
|
if (data.agent) {
|
|
html += '<div style="margin-top:16px;padding:12px;background:#1a1a1a;border-radius:6px">';
|
|
html += '<p style="font-size:13px">' + data.agent.emoji + ' <strong>' + data.agent.agent + '</strong></p>';
|
|
html += '<p style="font-size:13px;color:#ddd;margin-top:4px">' + esc(data.agent.advice) + '</p>';
|
|
html += '</div>';
|
|
}
|
|
|
|
html += '</div>';
|
|
el.innerHTML = html;
|
|
}
|
|
|
|
function scaffold(tmplId) {
|
|
const name = prompt('Project name:');
|
|
if (!name) return;
|
|
document.getElementById('cmdInput').value = 'create new ' + tmplId + ' project ' + name;
|
|
runCommand();
|
|
}
|
|
|
|
function esc(s) { return (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
function timeAgo(d) {
|
|
if (!d) return '';
|
|
const s = Math.floor((Date.now() - new Date(d)) / 1000);
|
|
if (s < 60) return s + 's ago';
|
|
if (s < 3600) return Math.floor(s/60) + 'm ago';
|
|
if (s < 86400) return Math.floor(s/3600) + 'h ago';
|
|
return Math.floor(s/86400) + 'd ago';
|
|
}
|
|
|
|
loadStats();
|
|
showTab('projects');
|
|
</script>
|
|
</body>
|
|
</html>`;
|