claudeboard 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -0
- package/agents/architect.js +76 -0
- package/agents/board-client.js +91 -0
- package/agents/claude-api.js +91 -0
- package/agents/developer.js +161 -0
- package/agents/orchestrator.js +204 -0
- package/agents/qa.js +208 -0
- package/bin/cli.js +199 -0
- package/dashboard/index.html +983 -0
- package/dashboard/server.js +197 -0
- package/package.json +55 -0
- package/sql/setup.sql +57 -0
- package/tools/filesystem.js +95 -0
- package/tools/screenshot.js +74 -0
- package/tools/supabase-reader.js +74 -0
- package/tools/terminal.js +58 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { runArchitectAgent } from "./architect.js";
|
|
6
|
+
import { runDeveloperAgent } from "./developer.js";
|
|
7
|
+
import { runQAAgent, runFullAppQA } from "./qa.js";
|
|
8
|
+
import {
|
|
9
|
+
initBoard,
|
|
10
|
+
getNextTask,
|
|
11
|
+
getAllTasks,
|
|
12
|
+
getStats,
|
|
13
|
+
addLog,
|
|
14
|
+
createTask,
|
|
15
|
+
createEpic,
|
|
16
|
+
} from "./board-client.js";
|
|
17
|
+
import { initSupabaseReader } from "../tools/supabase-reader.js";
|
|
18
|
+
import { runCommand, startProcess, waitForPort } from "../tools/terminal.js";
|
|
19
|
+
|
|
20
|
+
const PHASES = {
|
|
21
|
+
ARCHITECTURE: "architecture",
|
|
22
|
+
DEVELOPMENT: "development",
|
|
23
|
+
QA_FINAL: "qa_final",
|
|
24
|
+
COMPLETE: "complete",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export async function runOrchestrator(config) {
|
|
28
|
+
const {
|
|
29
|
+
prdPath,
|
|
30
|
+
projectPath,
|
|
31
|
+
supabaseUrl,
|
|
32
|
+
supabaseKey,
|
|
33
|
+
projectName,
|
|
34
|
+
expoPort = 8081,
|
|
35
|
+
skipArchitect = false,
|
|
36
|
+
} = config;
|
|
37
|
+
|
|
38
|
+
console.log(chalk.cyan("\n╔═══════════════════════════════════════╗"));
|
|
39
|
+
console.log(chalk.cyan("║") + chalk.bold(" 🤖 CLAUDEBOARD ORCHESTRATOR STARTING ") + chalk.cyan("║"));
|
|
40
|
+
console.log(chalk.cyan("╚═══════════════════════════════════════╝\n"));
|
|
41
|
+
|
|
42
|
+
// Init board connection
|
|
43
|
+
initBoard(supabaseUrl, supabaseKey, projectName);
|
|
44
|
+
initSupabaseReader(supabaseUrl, supabaseKey);
|
|
45
|
+
|
|
46
|
+
// Read PRD
|
|
47
|
+
const prdContent = fs.readFileSync(prdPath, "utf8");
|
|
48
|
+
console.log(chalk.dim(` PRD: ${prdPath} (${prdContent.length} chars)`));
|
|
49
|
+
console.log(chalk.dim(` Project: ${projectPath}\n`));
|
|
50
|
+
|
|
51
|
+
let techStack = {};
|
|
52
|
+
|
|
53
|
+
// ── PHASE 1: ARCHITECTURE ──────────────────────────────────────────────────
|
|
54
|
+
if (!skipArchitect) {
|
|
55
|
+
console.log(chalk.bold.cyan("[ PHASE 1: ARCHITECTURE ]\n"));
|
|
56
|
+
const archResult = await runArchitectAgent(prdContent, projectName);
|
|
57
|
+
techStack = archResult.techStack;
|
|
58
|
+
console.log(chalk.green(`\n ✓ ${archResult.totalTasks} tasks created across ${archResult.epics.length} epics\n`));
|
|
59
|
+
} else {
|
|
60
|
+
console.log(chalk.dim(" Skipping architect (tasks already exist)\n"));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── START EXPO ─────────────────────────────────────────────────────────────
|
|
64
|
+
let expoProcess = null;
|
|
65
|
+
let expoReady = false;
|
|
66
|
+
const logFile = path.join(projectPath, ".claudeboard-logs.txt");
|
|
67
|
+
const logStream = fs.createWriteStream(logFile, { flags: "a" });
|
|
68
|
+
|
|
69
|
+
// Check if expo project exists
|
|
70
|
+
const hasPackageJson = fs.existsSync(path.join(projectPath, "package.json"));
|
|
71
|
+
|
|
72
|
+
if (hasPackageJson) {
|
|
73
|
+
console.log(chalk.dim(" Starting Expo Web for QA screenshots...\n"));
|
|
74
|
+
expoProcess = startProcess(
|
|
75
|
+
"npx",
|
|
76
|
+
["expo", "start", "--web", "--port", String(expoPort)],
|
|
77
|
+
projectPath,
|
|
78
|
+
(log) => {
|
|
79
|
+
logStream.write(log);
|
|
80
|
+
if (log.includes("Metro waiting") || log.includes("localhost:" + expoPort)) {
|
|
81
|
+
expoReady = true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Wait up to 30s for expo to start
|
|
87
|
+
await waitForPort(expoPort, 30000);
|
|
88
|
+
expoReady = true;
|
|
89
|
+
console.log(chalk.dim(` Expo Web running at http://localhost:${expoPort}\n`));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── PHASE 2: DEVELOPMENT LOOP ─────────────────────────────────────────────
|
|
93
|
+
console.log(chalk.bold.cyan("[ PHASE 2: DEVELOPMENT ]\n"));
|
|
94
|
+
|
|
95
|
+
let consecutiveFailures = 0;
|
|
96
|
+
const MAX_CONSECUTIVE_FAILURES = 3;
|
|
97
|
+
|
|
98
|
+
while (true) {
|
|
99
|
+
const task = await getNextTask();
|
|
100
|
+
|
|
101
|
+
if (!task) {
|
|
102
|
+
console.log(chalk.green("\n ✓ All tasks complete!\n"));
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const stats = await getStats();
|
|
107
|
+
const pct = stats.pct;
|
|
108
|
+
const bar = "█".repeat(Math.floor(pct / 5)) + "░".repeat(20 - Math.floor(pct / 5));
|
|
109
|
+
console.log(chalk.dim(`\n [${bar}] ${pct}% — ${stats.done}/${stats.total} tasks done`));
|
|
110
|
+
console.log(chalk.bold(`\n → ${task.title}`));
|
|
111
|
+
console.log(chalk.dim(` Epic: ${task.cb_epics?.name || "—"} | ${task.priority} | ${task.type}`));
|
|
112
|
+
|
|
113
|
+
// Run developer agent
|
|
114
|
+
const devResult = await runDeveloperAgent(task, projectPath, techStack);
|
|
115
|
+
|
|
116
|
+
if (!devResult.success) {
|
|
117
|
+
consecutiveFailures++;
|
|
118
|
+
console.log(chalk.red(` ✗ Dev failed (${consecutiveFailures} consecutive)`));
|
|
119
|
+
|
|
120
|
+
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
|
|
121
|
+
console.log(chalk.yellow("\n ⚠️ Too many consecutive failures — pausing for human review"));
|
|
122
|
+
console.log(chalk.yellow(" Check the dashboard for blocked tasks.\n"));
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
consecutiveFailures = 0;
|
|
129
|
+
|
|
130
|
+
// Run QA agent (only if expo is running and this is a UI/feature task)
|
|
131
|
+
const shouldRunQA = expoReady && ["feature", "bug"].includes(task.type);
|
|
132
|
+
|
|
133
|
+
if (shouldRunQA) {
|
|
134
|
+
// Give Expo a moment to hot-reload
|
|
135
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
136
|
+
|
|
137
|
+
const qaResult = await runQAAgent(task, devResult, projectPath, prdContent, expoPort);
|
|
138
|
+
|
|
139
|
+
if (!qaResult.passed && qaResult.fixInstructions) {
|
|
140
|
+
console.log(chalk.yellow(" ↩️ QA failed — sending back to developer..."));
|
|
141
|
+
|
|
142
|
+
// Create a fix task right after current
|
|
143
|
+
await createEpicIfNeeded();
|
|
144
|
+
const fixedByDev = await runDeveloperAgent(
|
|
145
|
+
{ ...task, title: `FIX: ${task.title}`, description: qaResult.fixInstructions },
|
|
146
|
+
projectPath,
|
|
147
|
+
techStack,
|
|
148
|
+
qaResult.fixInstructions
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
if (fixedByDev.success) {
|
|
152
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
153
|
+
await runQAAgent(task, fixedByDev, projectPath, prdContent, expoPort);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Small delay between tasks
|
|
159
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── PHASE 3: FINAL QA ─────────────────────────────────────────────────────
|
|
163
|
+
if (expoReady) {
|
|
164
|
+
console.log(chalk.bold.cyan("\n[ PHASE 3: FINAL QA ]\n"));
|
|
165
|
+
const fullQAReport = await runFullAppQA(projectPath, prdContent, expoPort);
|
|
166
|
+
|
|
167
|
+
if (fullQAReport.issues.length > 0) {
|
|
168
|
+
console.log(chalk.yellow(` ⚠️ Final QA found ${fullQAReport.issues.length} issues:`));
|
|
169
|
+
fullQAReport.issues.forEach((issue) => {
|
|
170
|
+
console.log(chalk.dim(` ${issue.route}: ${issue.note}`));
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Create fix tasks for final issues
|
|
174
|
+
for (const issue of fullQAReport.issues) {
|
|
175
|
+
await createTask({
|
|
176
|
+
title: `Final QA fix: ${issue.route}`,
|
|
177
|
+
description: issue.note,
|
|
178
|
+
priority: "high",
|
|
179
|
+
type: "bug",
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.log(chalk.yellow("\n Fix tasks added to board. Run claudeboard run --skip-architect to continue.\n"));
|
|
184
|
+
} else {
|
|
185
|
+
console.log(chalk.green(` ✓ Full QA passed — ${fullQAReport.screenshotsCaptures} screens checked\n`));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── COMPLETE ───────────────────────────────────────────────────────────────
|
|
190
|
+
const finalStats = await getStats();
|
|
191
|
+
console.log(chalk.cyan("\n╔═══════════════════════════════════════╗"));
|
|
192
|
+
console.log(chalk.cyan("║") + chalk.bold.green(` ✓ COMPLETE — ${finalStats.done}/${finalStats.total} tasks done (${finalStats.pct}%) `) + chalk.cyan("║"));
|
|
193
|
+
console.log(chalk.cyan("╚═══════════════════════════════════════╝\n"));
|
|
194
|
+
|
|
195
|
+
if (expoProcess) {
|
|
196
|
+
expoProcess.kill();
|
|
197
|
+
logStream.end();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function createEpicIfNeeded() {
|
|
202
|
+
// Dummy — fix tasks go into a "Fixes" epic
|
|
203
|
+
// In practice the board-client handles this
|
|
204
|
+
}
|
package/agents/qa.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { callClaude, callClaudeJSON, callClaudeWithImage } from "./claude-api.js";
|
|
2
|
+
import { addLog, createTask, startTask, completeTask, failTask } from "./board-client.js";
|
|
3
|
+
import { readFile, listFiles, projectTree } from "../tools/filesystem.js";
|
|
4
|
+
import { runCommand, waitForPort } from "../tools/terminal.js";
|
|
5
|
+
import { screenshotExpoWeb } from "../tools/screenshot.js";
|
|
6
|
+
import { readRecentLogs, readErrorLogs } from "../tools/supabase-reader.js";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
|
|
10
|
+
const SYSTEM_QA = `You are a senior QA engineer for mobile apps built with React Native / Expo.
|
|
11
|
+
Your job is to verify that implemented features work correctly and look good.
|
|
12
|
+
|
|
13
|
+
You receive:
|
|
14
|
+
- The original PRD description of a feature
|
|
15
|
+
- Console logs from the app
|
|
16
|
+
- A screenshot of the current state
|
|
17
|
+
- TypeScript/build errors if any
|
|
18
|
+
|
|
19
|
+
You must determine:
|
|
20
|
+
1. Does the feature work as described in the PRD?
|
|
21
|
+
2. Does the UI look polished and correct?
|
|
22
|
+
3. Are there any errors in the logs?
|
|
23
|
+
4. What bugs or issues need to be fixed?
|
|
24
|
+
|
|
25
|
+
Be specific about what's wrong and what needs to change.`;
|
|
26
|
+
|
|
27
|
+
export async function runQAAgent(task, devResult, projectPath, prdContent, expoPort = 8081) {
|
|
28
|
+
console.log(` 🔍 QA checking: ${task.title}`);
|
|
29
|
+
await addLog(task.id, "QA agent starting verification", "progress");
|
|
30
|
+
|
|
31
|
+
const qaReport = {
|
|
32
|
+
passed: false,
|
|
33
|
+
issues: [],
|
|
34
|
+
screenshotPath: null,
|
|
35
|
+
fixInstructions: null,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// 1. Check for TypeScript errors
|
|
39
|
+
const tsResult = await runCommand("npx tsc --noEmit 2>&1 | head -30", projectPath, 30000);
|
|
40
|
+
if (tsResult.stdout.includes("error TS")) {
|
|
41
|
+
qaReport.issues.push(`TypeScript errors found:\n${tsResult.stdout}`);
|
|
42
|
+
await addLog(task.id, `QA: TypeScript errors detected`, "error");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 2. Read console/app logs
|
|
46
|
+
let appLogs = "";
|
|
47
|
+
try {
|
|
48
|
+
// Read from Expo CLI output (saved to temp file by runner)
|
|
49
|
+
const logFile = path.join(projectPath, ".claudeboard-logs.txt");
|
|
50
|
+
if (fs.existsSync(logFile)) {
|
|
51
|
+
appLogs = fs.readFileSync(logFile, "utf8").slice(-3000);
|
|
52
|
+
}
|
|
53
|
+
} catch {}
|
|
54
|
+
|
|
55
|
+
// 3. Read Supabase error logs if project uses Supabase
|
|
56
|
+
let supabaseLogs = [];
|
|
57
|
+
try {
|
|
58
|
+
supabaseLogs = await readErrorLogs("logs", 10);
|
|
59
|
+
} catch {}
|
|
60
|
+
|
|
61
|
+
// 4. Take screenshot
|
|
62
|
+
await addLog(task.id, "Taking screenshot of Expo Web...", "progress");
|
|
63
|
+
const screenshotDir = path.join(projectPath, ".claudeboard-screenshots");
|
|
64
|
+
const screenshot = await screenshotExpoWeb(expoPort, screenshotDir);
|
|
65
|
+
|
|
66
|
+
if (screenshot.success) {
|
|
67
|
+
qaReport.screenshotPath = screenshot.imagePath;
|
|
68
|
+
await addLog(task.id, `Screenshot captured: ${path.basename(screenshot.imagePath)}`, "progress");
|
|
69
|
+
} else {
|
|
70
|
+
await addLog(task.id, `Screenshot failed: ${screenshot.error}`, "progress");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 5. Visual QA via Claude Vision
|
|
74
|
+
let visualVerdict = null;
|
|
75
|
+
if (screenshot.success && screenshot.base64) {
|
|
76
|
+
const visualPrompt = `
|
|
77
|
+
This is a screenshot of a React Native / Expo app screen.
|
|
78
|
+
|
|
79
|
+
Task that was just implemented: ${task.title}
|
|
80
|
+
Expected behavior from PRD: ${task.description}
|
|
81
|
+
|
|
82
|
+
Evaluate:
|
|
83
|
+
1. Does the screen look implemented? (not blank, not error screen)
|
|
84
|
+
2. Does the UI look polished? (proper layout, readable text, no obvious visual bugs)
|
|
85
|
+
3. Does it appear to match what was described in the task?
|
|
86
|
+
4. Any visual issues? (overlapping elements, cut off text, missing components)
|
|
87
|
+
|
|
88
|
+
Be concise and specific.`;
|
|
89
|
+
|
|
90
|
+
const visionResult = await callClaudeWithImage(SYSTEM_QA, visualPrompt, screenshot.base64);
|
|
91
|
+
visualVerdict = visionResult.text;
|
|
92
|
+
await addLog(task.id, `Visual QA: ${visualVerdict.slice(0, 200)}`, "progress");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 6. Functional QA — read code and verify logic
|
|
96
|
+
const relevantFiles = listFiles(projectPath, [".ts", ".tsx"])
|
|
97
|
+
.filter((f) => {
|
|
98
|
+
const rel = path.relative(projectPath, f).toLowerCase();
|
|
99
|
+
return task.title.toLowerCase().split(" ").some((w) => w.length > 4 && rel.includes(w));
|
|
100
|
+
})
|
|
101
|
+
.slice(0, 5);
|
|
102
|
+
|
|
103
|
+
let codeContext = "";
|
|
104
|
+
for (const f of relevantFiles) {
|
|
105
|
+
const content = readFile(f);
|
|
106
|
+
if (content) {
|
|
107
|
+
codeContext += `\n### ${path.relative(projectPath, f)}\n${content.slice(0, 1500)}\n`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const functionalVerdict = await callClaudeJSON(
|
|
112
|
+
SYSTEM_QA,
|
|
113
|
+
`
|
|
114
|
+
Task: ${task.title}
|
|
115
|
+
Description: ${task.description}
|
|
116
|
+
|
|
117
|
+
Code implemented:
|
|
118
|
+
${codeContext || "Could not read relevant files"}
|
|
119
|
+
|
|
120
|
+
Console logs:
|
|
121
|
+
${appLogs || "No logs captured"}
|
|
122
|
+
|
|
123
|
+
Supabase errors:
|
|
124
|
+
${supabaseLogs.length > 0 ? JSON.stringify(supabaseLogs) : "None"}
|
|
125
|
+
|
|
126
|
+
TypeScript status:
|
|
127
|
+
${tsResult.stdout.includes("error TS") ? tsResult.stdout : "No TypeScript errors"}
|
|
128
|
+
|
|
129
|
+
Visual QA notes:
|
|
130
|
+
${visualVerdict || "No screenshot available"}
|
|
131
|
+
|
|
132
|
+
Respond with JSON:
|
|
133
|
+
{
|
|
134
|
+
"passed": true/false,
|
|
135
|
+
"confidence": 0-100,
|
|
136
|
+
"issues": ["specific issue 1", "specific issue 2"],
|
|
137
|
+
"summary": "One sentence verdict",
|
|
138
|
+
"fixInstructions": "If failed: specific instructions for the developer to fix. If passed: null"
|
|
139
|
+
}`,
|
|
140
|
+
{ maxTokens: 2000 }
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
qaReport.passed = functionalVerdict.passed;
|
|
144
|
+
qaReport.issues = functionalVerdict.issues || [];
|
|
145
|
+
qaReport.fixInstructions = functionalVerdict.fixInstructions;
|
|
146
|
+
|
|
147
|
+
if (qaReport.passed) {
|
|
148
|
+
await addLog(task.id, `✓ QA passed (${functionalVerdict.confidence}%): ${functionalVerdict.summary}`, "complete");
|
|
149
|
+
console.log(` ✓ QA passed: ${task.title}`);
|
|
150
|
+
} else {
|
|
151
|
+
await addLog(
|
|
152
|
+
task.id,
|
|
153
|
+
`✗ QA failed: ${functionalVerdict.summary}. Issues: ${qaReport.issues.slice(0, 2).join("; ")}`,
|
|
154
|
+
"error"
|
|
155
|
+
);
|
|
156
|
+
console.log(` ✗ QA failed: ${task.title}`);
|
|
157
|
+
console.log(` Issues: ${qaReport.issues.join(", ")}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return qaReport;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Full app QA — run at end of development phase
|
|
165
|
+
* Goes through every major screen and validates against PRD
|
|
166
|
+
*/
|
|
167
|
+
export async function runFullAppQA(projectPath, prdContent, expoPort = 8081) {
|
|
168
|
+
console.log("\n 🔍 Running full app QA...");
|
|
169
|
+
|
|
170
|
+
// Detect routes from expo-router structure
|
|
171
|
+
const routeFiles = listFiles(path.join(projectPath, "app"), [".tsx", ".ts"])
|
|
172
|
+
.filter((f) => !f.includes("_layout") && !f.includes("_error"))
|
|
173
|
+
.map((f) => {
|
|
174
|
+
const rel = path.relative(path.join(projectPath, "app"), f);
|
|
175
|
+
return "/" + rel.replace(/\.(tsx|ts)$/, "").replace(/index$/, "");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const screenshotDir = path.join(projectPath, ".claudeboard-screenshots", "full-qa");
|
|
179
|
+
const screenshots = [];
|
|
180
|
+
|
|
181
|
+
for (const route of routeFiles.slice(0, 10)) {
|
|
182
|
+
const shot = await screenshotExpoWeb(expoPort, screenshotDir, route);
|
|
183
|
+
if (shot.success) screenshots.push({ route, ...shot });
|
|
184
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Send all screenshots + PRD to Claude for final verdict
|
|
188
|
+
const report = {
|
|
189
|
+
passed: screenshots.length > 0,
|
|
190
|
+
routes: routeFiles,
|
|
191
|
+
screenshotsCaptures: screenshots.length,
|
|
192
|
+
issues: [],
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
for (const shot of screenshots) {
|
|
196
|
+
if (!shot.base64) continue;
|
|
197
|
+
const verdict = await callClaudeWithImage(
|
|
198
|
+
SYSTEM_QA,
|
|
199
|
+
`Route: ${shot.route}\n\nPRD for reference:\n${prdContent.slice(0, 2000)}\n\nDoes this screen look complete and working? Any issues?`,
|
|
200
|
+
shot.base64
|
|
201
|
+
);
|
|
202
|
+
if (verdict.text.toLowerCase().includes("issue") || verdict.text.toLowerCase().includes("problem")) {
|
|
203
|
+
report.issues.push({ route: shot.route, note: verdict.text.slice(0, 200) });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return report;
|
|
208
|
+
}
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { program } from "commander";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import ora from "ora";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import { spawn } from "child_process";
|
|
10
|
+
import open from "open";
|
|
11
|
+
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
|
|
14
|
+
const LOGO = `
|
|
15
|
+
${chalk.cyan("╔══════════════════════════════════════╗")}
|
|
16
|
+
${chalk.cyan("║")} ${chalk.bold.white("●")} ${chalk.bold.cyan("CLAUDEBOARD")} ${chalk.dim("ai engineering team")} ${chalk.cyan("║")}
|
|
17
|
+
${chalk.cyan("╚══════════════════════════════════════╝")}
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
function loadConfig() {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(fs.readFileSync(path.join(process.cwd(), ".claudeboard.json"), "utf8"));
|
|
23
|
+
} catch {
|
|
24
|
+
console.log(chalk.yellow("No .claudeboard.json found. Run: claudeboard init"));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── INIT ────────────────────────────────────────────────────────────────────
|
|
30
|
+
program
|
|
31
|
+
.command("init")
|
|
32
|
+
.description("Initialize claudeboard in a project")
|
|
33
|
+
.action(async () => {
|
|
34
|
+
console.log(LOGO);
|
|
35
|
+
const { default: Enquirer } = await import("enquirer");
|
|
36
|
+
const enquirer = new Enquirer();
|
|
37
|
+
console.log(chalk.bold("Let's set up your AI engineering team.\n"));
|
|
38
|
+
|
|
39
|
+
const answers = await enquirer.prompt([
|
|
40
|
+
{ type: "input", name: "projectName", message: "Project name:", initial: path.basename(process.cwd()) },
|
|
41
|
+
{ type: "input", name: "supabaseUrl", message: "Supabase URL:", hint: "https://xxxx.supabase.co" },
|
|
42
|
+
{ type: "input", name: "supabaseKey", message: "Supabase anon key:" },
|
|
43
|
+
{ type: "input", name: "port", message: "Dashboard port:", initial: "3131" },
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
const spinner = ora("Creating config...").start();
|
|
47
|
+
|
|
48
|
+
const config = {
|
|
49
|
+
projectName: answers.projectName,
|
|
50
|
+
port: parseInt(answers.port),
|
|
51
|
+
supabaseUrl: answers.supabaseUrl,
|
|
52
|
+
supabaseKey: answers.supabaseKey,
|
|
53
|
+
createdAt: new Date().toISOString(),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
fs.writeFileSync(path.join(process.cwd(), ".claudeboard.json"), JSON.stringify(config, null, 2));
|
|
57
|
+
|
|
58
|
+
const sqlSrc = path.join(__dirname, "../sql/setup.sql");
|
|
59
|
+
fs.copyFileSync(sqlSrc, path.join(process.cwd(), "claudeboard-setup.sql"));
|
|
60
|
+
|
|
61
|
+
spinner.succeed(chalk.green("Config created!"));
|
|
62
|
+
|
|
63
|
+
console.log(`
|
|
64
|
+
${chalk.bold("Next steps:")}
|
|
65
|
+
|
|
66
|
+
${chalk.cyan("1.")} Run the SQL in your Supabase SQL Editor:
|
|
67
|
+
${chalk.dim("→ claudeboard-setup.sql")}
|
|
68
|
+
|
|
69
|
+
${chalk.cyan("2.")} Start the dashboard:
|
|
70
|
+
${chalk.bold("claudeboard start")}
|
|
71
|
+
|
|
72
|
+
${chalk.cyan("3.")} Run the AI team with your PRD:
|
|
73
|
+
${chalk.bold("claudeboard run --prd ./PRD.md --project ./your-app")}
|
|
74
|
+
`);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ─── START DASHBOARD ─────────────────────────────────────────────────────────
|
|
78
|
+
program
|
|
79
|
+
.command("start")
|
|
80
|
+
.description("Start the dashboard server")
|
|
81
|
+
.option("-p, --port <port>", "Port number")
|
|
82
|
+
.action(async (opts) => {
|
|
83
|
+
console.log(LOGO);
|
|
84
|
+
const config = loadConfig();
|
|
85
|
+
const port = opts.port || config.port || 3131;
|
|
86
|
+
const spinner = ora("Starting dashboard...").start();
|
|
87
|
+
|
|
88
|
+
const serverPath = path.join(__dirname, "../dashboard/server.js");
|
|
89
|
+
const server = spawn("node", [serverPath], {
|
|
90
|
+
env: {
|
|
91
|
+
...process.env,
|
|
92
|
+
SUPABASE_URL: config.supabaseUrl,
|
|
93
|
+
SUPABASE_KEY: config.supabaseKey,
|
|
94
|
+
PORT: String(port),
|
|
95
|
+
PROJECT_NAME: config.projectName,
|
|
96
|
+
},
|
|
97
|
+
stdio: "pipe",
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
server.stdout.on("data", (data) => {
|
|
101
|
+
if (data.toString().includes("READY")) {
|
|
102
|
+
spinner.succeed(chalk.green("Dashboard running!"));
|
|
103
|
+
console.log(`\n ${chalk.bold("→")} ${chalk.cyan(`http://localhost:${port}`)}\n`);
|
|
104
|
+
console.log(chalk.dim(" Press Ctrl+C to stop\n"));
|
|
105
|
+
open(`http://localhost:${port}`);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
server.stderr.on("data", (d) => console.error(chalk.red(d.toString())));
|
|
110
|
+
process.on("SIGINT", () => { server.kill(); process.exit(0); });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ─── RUN AI TEAM ─────────────────────────────────────────────────────────────
|
|
114
|
+
program
|
|
115
|
+
.command("run")
|
|
116
|
+
.description("Run the AI engineering team on your project")
|
|
117
|
+
.requiredOption("--prd <path>", "Path to PRD markdown file")
|
|
118
|
+
.requiredOption("--project <path>", "Path to your app project directory")
|
|
119
|
+
.option("--skip-architect", "Skip architecture phase (tasks already exist)")
|
|
120
|
+
.option("--expo-port <port>", "Expo Web port for QA screenshots", "8081")
|
|
121
|
+
.action(async (opts) => {
|
|
122
|
+
console.log(LOGO);
|
|
123
|
+
const config = loadConfig();
|
|
124
|
+
|
|
125
|
+
if (!fs.existsSync(opts.prd)) {
|
|
126
|
+
console.log(chalk.red(`PRD not found: ${opts.prd}`)); process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
if (!fs.existsSync(opts.project)) {
|
|
129
|
+
console.log(chalk.red(`Project not found: ${opts.project}`)); process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log(chalk.bold(` AI Team starting for: ${chalk.cyan(config.projectName)}\n`));
|
|
133
|
+
console.log(chalk.dim(` Dashboard → http://localhost:${config.port}`));
|
|
134
|
+
console.log(chalk.dim(` PRD → ${opts.prd}`));
|
|
135
|
+
console.log(chalk.dim(` Project → ${opts.project}\n`));
|
|
136
|
+
|
|
137
|
+
const { runOrchestrator } = await import("../agents/orchestrator.js");
|
|
138
|
+
|
|
139
|
+
await runOrchestrator({
|
|
140
|
+
prdPath: path.resolve(opts.prd),
|
|
141
|
+
projectPath: path.resolve(opts.project),
|
|
142
|
+
supabaseUrl: config.supabaseUrl,
|
|
143
|
+
supabaseKey: config.supabaseKey,
|
|
144
|
+
projectName: config.projectName,
|
|
145
|
+
expoPort: parseInt(opts.expoPort),
|
|
146
|
+
skipArchitect: !!opts.skipArchitect,
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ─── IMPORT PRD ──────────────────────────────────────────────────────────────
|
|
151
|
+
program
|
|
152
|
+
.command("import-prd <file>")
|
|
153
|
+
.description("Parse PRD → create tasks in board (no agents run)")
|
|
154
|
+
.action(async (file) => {
|
|
155
|
+
console.log(LOGO);
|
|
156
|
+
const config = loadConfig();
|
|
157
|
+
|
|
158
|
+
if (!fs.existsSync(file)) {
|
|
159
|
+
console.log(chalk.red(`File not found: ${file}`)); process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const prdContent = fs.readFileSync(file, "utf8");
|
|
163
|
+
const { initBoard } = await import("../agents/board-client.js");
|
|
164
|
+
const { runArchitectAgent } = await import("../agents/architect.js");
|
|
165
|
+
|
|
166
|
+
initBoard(config.supabaseUrl, config.supabaseKey, config.projectName);
|
|
167
|
+
const result = await runArchitectAgent(prdContent, config.projectName);
|
|
168
|
+
|
|
169
|
+
console.log(chalk.green(`\n ✓ Created ${result.totalTasks} tasks across ${result.epics.length} epics`));
|
|
170
|
+
console.log(`\n Run ${chalk.bold("claudeboard start")} to see your board.\n`);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ─── STATUS ──────────────────────────────────────────────────────────────────
|
|
174
|
+
program
|
|
175
|
+
.command("status")
|
|
176
|
+
.description("Show current board status in terminal")
|
|
177
|
+
.action(async () => {
|
|
178
|
+
const config = loadConfig();
|
|
179
|
+
const { initBoard, getStats } = await import("../agents/board-client.js");
|
|
180
|
+
initBoard(config.supabaseUrl, config.supabaseKey, config.projectName);
|
|
181
|
+
|
|
182
|
+
const stats = await getStats();
|
|
183
|
+
const bar = "█".repeat(Math.floor(stats.pct / 5)) + "░".repeat(20 - Math.floor(stats.pct / 5));
|
|
184
|
+
|
|
185
|
+
console.log(LOGO);
|
|
186
|
+
console.log(chalk.bold(` ${config.projectName}\n`));
|
|
187
|
+
console.log(` [${chalk.green(bar)}] ${chalk.bold(stats.pct + "%")}`);
|
|
188
|
+
console.log(`\n ${chalk.green("✓ Done:")} ${stats.done}`);
|
|
189
|
+
console.log(` ${chalk.yellow("◌ Running:")} ${stats.in_progress}`);
|
|
190
|
+
console.log(` ${chalk.dim("○ Todo:")} ${stats.todo}`);
|
|
191
|
+
console.log(` ${chalk.red("✗ Error:")} ${stats.error}\n`);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
program
|
|
195
|
+
.name("claudeboard")
|
|
196
|
+
.description("AI engineering team — from PRD to working app, autonomously")
|
|
197
|
+
.version("1.0.0");
|
|
198
|
+
|
|
199
|
+
program.parse();
|