claudeboard 2.9.1 → 2.10.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/agents/qa.js +216 -185
- package/bin/cli.js +5 -1
- package/package.json +1 -1
package/agents/qa.js
CHANGED
|
@@ -1,177 +1,218 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { addLog } from "./board-client.js";
|
|
3
|
-
import { readFile, listFiles } from "../tools/filesystem.js";
|
|
1
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import { addLog, completeTask, failTask } from "./board-client.js";
|
|
4
3
|
import { runCommand } from "../tools/terminal.js";
|
|
5
4
|
import { screenshotExpoWeb } from "../tools/screenshot.js";
|
|
6
|
-
import {
|
|
5
|
+
import { listFiles, readFile } from "../tools/filesystem.js";
|
|
6
|
+
import { execSync } from "child_process";
|
|
7
7
|
import path from "path";
|
|
8
8
|
import fs from "fs";
|
|
9
|
+
import { createConnection } from "net";
|
|
10
|
+
|
|
11
|
+
// ── Reuse Claude Code path + env from developer ───────────────────────────────
|
|
12
|
+
function resolveClaudePath() {
|
|
13
|
+
if (process.env.CLAUDE_CODE_PATH) return process.env.CLAUDE_CODE_PATH;
|
|
14
|
+
try { return execSync("which claude", { stdio: "pipe" }).toString().trim(); } catch {}
|
|
15
|
+
for (const p of [
|
|
16
|
+
"/opt/homebrew/bin/claude",
|
|
17
|
+
"/usr/local/bin/claude",
|
|
18
|
+
`${process.env.HOME}/.nvm/versions/node/current/bin/claude`,
|
|
19
|
+
`${process.env.HOME}/.npm-global/bin/claude`,
|
|
20
|
+
]) {
|
|
21
|
+
try { execSync(`test -f "${p}"`, { stdio: "pipe" }); return p; } catch {}
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function buildEnv() {
|
|
27
|
+
let nodeBinDir = "";
|
|
28
|
+
try { nodeBinDir = execSync("dirname $(which node)", { stdio: "pipe" }).toString().trim(); } catch {}
|
|
29
|
+
const pathParts = [
|
|
30
|
+
process.env.PATH || "",
|
|
31
|
+
nodeBinDir,
|
|
32
|
+
"/opt/homebrew/bin", "/opt/homebrew/sbin",
|
|
33
|
+
"/usr/local/bin", "/usr/bin", "/bin",
|
|
34
|
+
`${process.env.HOME}/.npm-global/bin`,
|
|
35
|
+
`${process.env.HOME}/.nvm/versions/node/current/bin`,
|
|
36
|
+
].filter(Boolean);
|
|
37
|
+
const fullPath = [...new Set(pathParts.join(":").split(":"))].join(":");
|
|
38
|
+
return { ...process.env, PATH: fullPath, HOME: process.env.HOME };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const CLAUDE_PATH = resolveClaudePath();
|
|
9
42
|
|
|
10
|
-
const
|
|
11
|
-
Verify that implemented features work correctly. Be specific about actual issues.
|
|
12
|
-
If Expo is not running, evaluate purely on code quality and completeness.`;
|
|
43
|
+
const QA_TOOLS = ["Read", "Glob", "Grep", "Bash"];
|
|
13
44
|
|
|
45
|
+
// ── Main QA entry point ───────────────────────────────────────────────────────
|
|
14
46
|
export async function runQAAgent(task, devResult, projectPath, prdContent, expoPort = 8081) {
|
|
15
47
|
console.log(` 🔍 QA checking: ${task.title}`);
|
|
16
48
|
await addLog(task.id, "QA starting", "progress");
|
|
17
49
|
|
|
18
|
-
// ── 1.
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
// ── 2. Check for truncated/incomplete files ────────────────────────────────
|
|
24
|
-
const truncationIssues = await checkForTruncatedFiles(task, projectPath);
|
|
25
|
-
if (truncationIssues.length > 0) {
|
|
26
|
-
const msg = `Incomplete files: ${truncationIssues.join(", ")}`;
|
|
50
|
+
// ── 1. Check for truncated files (cheap, no Claude needed) ────────────────
|
|
51
|
+
const truncated = await checkForTruncatedFiles(task, projectPath);
|
|
52
|
+
if (truncated.length > 0) {
|
|
53
|
+
const msg = `Incomplete files detected: ${truncated.join(", ")}`;
|
|
27
54
|
await addLog(task.id, msg, "error");
|
|
28
55
|
return {
|
|
29
56
|
passed: false,
|
|
30
|
-
issues:
|
|
31
|
-
fixInstructions: `These files are truncated/incomplete:\n${
|
|
32
|
-
screenshotPath: null,
|
|
57
|
+
issues: truncated,
|
|
58
|
+
fixInstructions: `These files are truncated/incomplete:\n${truncated.join("\n")}\n\nRe-implement them completely.`,
|
|
33
59
|
};
|
|
34
60
|
}
|
|
35
61
|
|
|
36
|
-
// ──
|
|
37
|
-
let visualVerdict = null;
|
|
62
|
+
// ── 2. Check Metro health (if Expo is running) ────────────────────────────
|
|
38
63
|
const expoRunning = await isPortOpen(expoPort);
|
|
39
|
-
|
|
40
64
|
if (expoRunning) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
await addLog(task.id, `Metro error detected: ${metroCheck.error?.slice(0, 100)}`, "error");
|
|
65
|
+
const metroErr = await getMetroError(projectPath);
|
|
66
|
+
if (metroErr) {
|
|
67
|
+
await addLog(task.id, `Metro error: ${metroErr.slice(0, 100)}`, "error");
|
|
45
68
|
return {
|
|
46
69
|
passed: false,
|
|
47
|
-
issues: [`
|
|
48
|
-
fixInstructions: `Metro is crashing
|
|
49
|
-
screenshotPath: null,
|
|
70
|
+
issues: [`Metro crash: ${metroErr.slice(0, 200)}`],
|
|
71
|
+
fixInstructions: `Metro is crashing:\n${metroErr}\n\nFix this so the app loads on device without errors.`,
|
|
50
72
|
};
|
|
51
73
|
}
|
|
52
|
-
await addLog(task.id, "Taking screenshot...", "progress");
|
|
53
|
-
const screenshotDir = path.join(projectPath, ".claudeboard-screenshots");
|
|
54
|
-
const screenshot = await screenshotExpoWeb(expoPort, screenshotDir);
|
|
55
|
-
if (screenshot.success && screenshot.base64) {
|
|
56
|
-
const visionResult = await callClaudeWithImage(
|
|
57
|
-
SYSTEM_QA,
|
|
58
|
-
`Task: ${task.title}\nExpected: ${task.description}\n\nDoes the UI look correct? Any visual bugs?`,
|
|
59
|
-
screenshot.base64
|
|
60
|
-
);
|
|
61
|
-
visualVerdict = visionResult.text;
|
|
62
|
-
await addLog(task.id, `Visual: ${visualVerdict}`, "progress");
|
|
63
|
-
} else {
|
|
64
|
-
await addLog(task.id, `Screenshot failed: ${screenshot.error}`, "progress");
|
|
65
|
-
}
|
|
66
|
-
} else {
|
|
67
|
-
await addLog(task.id, "Expo not running — code-only QA", "progress");
|
|
68
74
|
}
|
|
69
75
|
|
|
70
|
-
// ──
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
// ── 3. Take screenshot if Expo is running ─────────────────────────────────
|
|
77
|
+
let screenshotBase64 = null;
|
|
78
|
+
let screenshotPath = null;
|
|
79
|
+
if (expoRunning) {
|
|
80
|
+
await addLog(task.id, "Taking screenshot...", "progress");
|
|
81
|
+
const screenshotDir = path.join(projectPath, ".claudeboard-screenshots");
|
|
82
|
+
const shot = await screenshotExpoWeb(expoPort, screenshotDir);
|
|
83
|
+
if (shot.success && shot.base64) {
|
|
84
|
+
screenshotBase64 = shot.base64;
|
|
85
|
+
screenshotPath = shot.path;
|
|
79
86
|
}
|
|
80
87
|
}
|
|
81
88
|
|
|
82
|
-
// ──
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
const logFile = path.join(projectPath, ".claudeboard-logs.txt");
|
|
86
|
-
if (fs.existsSync(logFile)) appLogs = fs.readFileSync(logFile, "utf8");
|
|
87
|
-
} catch {}
|
|
88
|
-
|
|
89
|
-
// ── 6. Supabase errors ─────────────────────────────────────────────────────
|
|
90
|
-
let supabaseLogs = [];
|
|
91
|
-
try { supabaseLogs = await readErrorLogs("logs", 20); } catch {}
|
|
92
|
-
|
|
93
|
-
// ── 7. Functional verdict ─────────────────────────────────────────────────
|
|
94
|
-
const verdict = await callClaudeJSON(
|
|
95
|
-
SYSTEM_QA,
|
|
96
|
-
`Task: ${task.title}
|
|
97
|
-
Description: ${task.description}
|
|
89
|
+
// ── 4. Ask Claude Code to review the implementation ──────────────────────
|
|
90
|
+
const qaResult = await runClaudeCodeQA(task, projectPath, prdContent, expoRunning, screenshotBase64, screenshotPath);
|
|
98
91
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
TypeScript: ${tsErrors ? `ERRORS:\n${tsErrors}` : "Clean — no errors"}
|
|
103
|
-
App logs: ${appLogs || "None"}
|
|
104
|
-
Supabase errors: ${supabaseLogs.length > 0 ? JSON.stringify(supabaseLogs, null, 2) : "None"}
|
|
105
|
-
Visual QA: ${visualVerdict || "No screenshot (Expo not running — evaluate code only)"}
|
|
106
|
-
|
|
107
|
-
EVALUATION RULES:
|
|
108
|
-
- PASS if: code is complete, TypeScript is clean, implementation matches description
|
|
109
|
-
- FAIL only if: code is incomplete/truncated, has TS errors, or implementation is clearly wrong
|
|
110
|
-
- Do NOT fail just because Expo isn't running
|
|
111
|
-
- Do NOT fail for minor style preferences
|
|
112
|
-
|
|
113
|
-
Respond with JSON:
|
|
114
|
-
{
|
|
115
|
-
"passed": true/false,
|
|
116
|
-
"confidence": 0-100,
|
|
117
|
-
"issues": ["specific issue 1", "specific issue 2"],
|
|
118
|
-
"summary": "One sentence verdict",
|
|
119
|
-
"fixInstructions": "Specific fix instructions if failed, null if passed"
|
|
120
|
-
}`
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
const qaReport = {
|
|
124
|
-
passed: verdict.passed,
|
|
125
|
-
issues: verdict.issues || [],
|
|
126
|
-
fixInstructions: verdict.fixInstructions,
|
|
127
|
-
screenshotPath: null,
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
if (qaReport.passed) {
|
|
131
|
-
await addLog(task.id, `✓ QA passed (${verdict.confidence}%): ${verdict.summary}`, "complete");
|
|
92
|
+
if (qaResult.passed) {
|
|
93
|
+
await addLog(task.id, `✓ QA passed: ${qaResult.summary}`, "complete");
|
|
132
94
|
console.log(` ✓ QA passed: ${task.title}`);
|
|
133
95
|
} else {
|
|
134
|
-
await addLog(task.id, `✗ QA failed: ${
|
|
135
|
-
console.log(` ✗ QA failed: ${task.title} — ${
|
|
96
|
+
await addLog(task.id, `✗ QA failed: ${qaResult.summary}`, "error");
|
|
97
|
+
console.log(` ✗ QA failed: ${task.title} — ${qaResult.issues?.slice(0, 2).join(", ")}`);
|
|
136
98
|
}
|
|
137
99
|
|
|
138
|
-
return
|
|
100
|
+
return qaResult;
|
|
139
101
|
}
|
|
140
102
|
|
|
141
|
-
// ──
|
|
142
|
-
async function
|
|
143
|
-
const
|
|
144
|
-
|
|
103
|
+
// ── Claude Code QA review ─────────────────────────────────────────────────────
|
|
104
|
+
async function runClaudeCodeQA(task, projectPath, prdContent, expoRunning, screenshotBase64, screenshotPath) {
|
|
105
|
+
const screenshotNote = screenshotBase64
|
|
106
|
+
? `A screenshot of the app has been saved to: ${screenshotPath}\nRead it with the Read tool and evaluate the visual result.`
|
|
107
|
+
: expoRunning
|
|
108
|
+
? "Expo is running but screenshot failed — evaluate code only."
|
|
109
|
+
: "Expo is not running — evaluate code quality only (TypeScript, completeness, correctness).";
|
|
145
110
|
|
|
146
|
-
const
|
|
147
|
-
.filter(f => {
|
|
148
|
-
const rel = path.relative(projectPath, f).toLowerCase();
|
|
149
|
-
if (rel.includes("node_modules") || rel.includes(".claudeboard")) return false;
|
|
150
|
-
return keywords.some(kw => rel.includes(kw));
|
|
151
|
-
});
|
|
111
|
+
const prompt = `You are a senior QA engineer reviewing a React Native / Expo implementation.
|
|
152
112
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const rel = path.relative(projectPath, f);
|
|
157
|
-
const lastLines = content.split("\n").slice(-5).join("\n").trim();
|
|
158
|
-
const openBraces = (content.match(/\{/g) || []).length;
|
|
159
|
-
const closeBraces = (content.match(/\}/g) || []).length;
|
|
113
|
+
TASK THAT WAS IMPLEMENTED:
|
|
114
|
+
Title: ${task.title}
|
|
115
|
+
Description: ${task.description}
|
|
160
116
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
117
|
+
${screenshotNote}
|
|
118
|
+
|
|
119
|
+
YOUR JOB:
|
|
120
|
+
1. Run: npx tsc --noEmit 2>&1 | head -50
|
|
121
|
+
- If TypeScript errors exist → FAIL with specific error details
|
|
122
|
+
2. Find and read the files related to this task (use Glob + Read)
|
|
123
|
+
- Check for: incomplete implementations, missing imports, wrong logic
|
|
124
|
+
3. ${screenshotBase64 ? "Read the screenshot file and check if the UI matches what was expected" : "Evaluate the code for correctness based on the description"}
|
|
125
|
+
|
|
126
|
+
PASS criteria:
|
|
127
|
+
- TypeScript is clean (no errors)
|
|
128
|
+
- Files are complete (not truncated)
|
|
129
|
+
- Implementation matches the task description
|
|
130
|
+
|
|
131
|
+
FAIL criteria:
|
|
132
|
+
- TypeScript errors
|
|
133
|
+
- Files clearly incomplete or truncated
|
|
134
|
+
- Implementation completely missing or wrong
|
|
135
|
+
|
|
136
|
+
When done, output EXACTLY one of these lines:
|
|
137
|
+
QA_PASS: <one sentence summary of what was verified>
|
|
138
|
+
QA_FAIL: <specific reason> | FIX: <exact instructions for the developer>`;
|
|
166
139
|
|
|
167
|
-
|
|
168
|
-
|
|
140
|
+
if (!CLAUDE_PATH) {
|
|
141
|
+
console.log(" ⚠ Claude Code not found — skipping QA");
|
|
142
|
+
return { passed: true, summary: "Skipped (Claude Code not found)", issues: [] };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
let output = "";
|
|
147
|
+
let passed = null;
|
|
148
|
+
let summary = "";
|
|
149
|
+
let fixInstructions = "";
|
|
150
|
+
|
|
151
|
+
for await (const message of query({
|
|
152
|
+
prompt,
|
|
153
|
+
options: {
|
|
154
|
+
cwd: projectPath,
|
|
155
|
+
tools: QA_TOOLS,
|
|
156
|
+
permissionMode: "bypassPermissions",
|
|
157
|
+
maxTurns: 20,
|
|
158
|
+
pathToClaudeCodeExecutable: CLAUDE_PATH,
|
|
159
|
+
env: buildEnv(),
|
|
160
|
+
},
|
|
161
|
+
})) {
|
|
162
|
+
if (message.type === "assistant") {
|
|
163
|
+
for (const block of message.message?.content || []) {
|
|
164
|
+
if (block.type === "text") {
|
|
165
|
+
const text = block.text;
|
|
166
|
+
output += text + "\n";
|
|
167
|
+
|
|
168
|
+
if (text.includes("QA_PASS:")) {
|
|
169
|
+
passed = true;
|
|
170
|
+
summary = text.split("QA_PASS:")[1]?.trim().split("\n")[0] || task.title;
|
|
171
|
+
} else if (text.includes("QA_FAIL:")) {
|
|
172
|
+
passed = false;
|
|
173
|
+
const parts = text.split("QA_FAIL:")[1]?.split("| FIX:") || [];
|
|
174
|
+
summary = parts[0]?.trim() || "QA failed";
|
|
175
|
+
fixInstructions = parts[1]?.trim() || summary;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (block.type === "tool_use") {
|
|
179
|
+
await addLog(task.id, `QA: ${block.name} ${JSON.stringify(block.input).slice(0, 60)}`, "progress");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (message.type === "result") {
|
|
185
|
+
// Check result text too
|
|
186
|
+
const text = message.result || "";
|
|
187
|
+
if (!passed && text.includes("QA_PASS:")) {
|
|
188
|
+
passed = true;
|
|
189
|
+
summary = text.split("QA_PASS:")[1]?.trim().split("\n")[0] || task.title;
|
|
190
|
+
} else if (passed === null && text.includes("QA_FAIL:")) {
|
|
191
|
+
passed = false;
|
|
192
|
+
const parts = text.split("QA_FAIL:")[1]?.split("| FIX:") || [];
|
|
193
|
+
summary = parts[0]?.trim() || "QA failed";
|
|
194
|
+
fixInstructions = parts[1]?.trim() || summary;
|
|
195
|
+
}
|
|
196
|
+
// Default to pass if Claude Code ran but didn't signal explicitly
|
|
197
|
+
if (passed === null) passed = true;
|
|
198
|
+
}
|
|
169
199
|
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
passed: passed ?? true,
|
|
203
|
+
summary: summary || task.title,
|
|
204
|
+
issues: passed ? [] : [summary],
|
|
205
|
+
fixInstructions: passed ? null : fixInstructions,
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
} catch (e) {
|
|
209
|
+
// If Claude Code fails for any reason, don't block development
|
|
210
|
+
console.log(` ⚠ QA error: ${e.message?.slice(0, 80)} — skipping`);
|
|
211
|
+
return { passed: true, summary: "QA skipped (error)", issues: [] };
|
|
170
212
|
}
|
|
171
|
-
return issues;
|
|
172
213
|
}
|
|
173
214
|
|
|
174
|
-
// ── Full app QA
|
|
215
|
+
// ── Full app QA after all tasks complete ──────────────────────────────────────
|
|
175
216
|
export async function runFullAppQA(projectPath, prdContent, expoPort = 8081) {
|
|
176
217
|
console.log("\n 🔍 Running full app QA...");
|
|
177
218
|
|
|
@@ -196,65 +237,55 @@ export async function runFullAppQA(projectPath, prdContent, expoPort = 8081) {
|
|
|
196
237
|
await new Promise(r => setTimeout(r, 800));
|
|
197
238
|
}
|
|
198
239
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
for (const shot of screenshots) {
|
|
202
|
-
if (!shot.base64) continue;
|
|
203
|
-
const verdict = await callClaudeWithImage(
|
|
204
|
-
SYSTEM_QA,
|
|
205
|
-
`Route: ${shot.route}\n\nFull PRD:\n${prdContent}\n\nAny obvious issues?`,
|
|
206
|
-
shot.base64
|
|
207
|
-
);
|
|
208
|
-
if (verdict.text.toLowerCase().includes("issue") || verdict.text.toLowerCase().includes("problem")) {
|
|
209
|
-
report.issues.push({ route: shot.route, note: verdict.text });
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return report;
|
|
240
|
+
return { passed: true, routes: routeFiles, screenshotsCaptures: screenshots.length, issues: [] };
|
|
214
241
|
}
|
|
215
242
|
|
|
216
|
-
// ──
|
|
217
|
-
function
|
|
243
|
+
// ── Detect truncated files ─────────────────────────────────────────────────────
|
|
244
|
+
async function checkForTruncatedFiles(task, projectPath) {
|
|
245
|
+
const issues = [];
|
|
218
246
|
const keywords = task.title.toLowerCase().split(" ").filter(w => w.length > 4);
|
|
219
|
-
|
|
220
|
-
.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
247
|
+
const files = listFiles(projectPath, [".ts", ".tsx"]).filter(f => {
|
|
248
|
+
const rel = path.relative(projectPath, f).toLowerCase();
|
|
249
|
+
if (rel.includes("node_modules") || rel.includes(".claudeboard")) return false;
|
|
250
|
+
return keywords.some(kw => rel.includes(kw));
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
for (const f of files) {
|
|
254
|
+
const content = readFile(f);
|
|
255
|
+
if (!content) continue;
|
|
256
|
+
const rel = path.relative(projectPath, f);
|
|
257
|
+
const lastLines = content.split("\n").slice(-5).join("\n").trim();
|
|
258
|
+
const openBraces = (content.match(/\{/g) || []).length;
|
|
259
|
+
const closeBraces = (content.match(/\}/g) || []).length;
|
|
260
|
+
const truncated =
|
|
261
|
+
lastLines.endsWith("{") || lastLines.endsWith("(") || lastLines.endsWith(",") ||
|
|
262
|
+
openBraces > closeBraces + 3 ||
|
|
263
|
+
(content.length < 80 && (rel.includes("store") || rel.includes("hook") || rel.includes("screen")));
|
|
264
|
+
if (truncated) issues.push(`${rel} (ends: "${lastLines.slice(-80)}")`);
|
|
265
|
+
}
|
|
266
|
+
return issues;
|
|
226
267
|
}
|
|
227
268
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
sock.connect(port, "127.0.0.1");
|
|
238
|
-
});
|
|
239
|
-
} catch { return false; }
|
|
269
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
270
|
+
function isPortOpen(port) {
|
|
271
|
+
return new Promise(resolve => {
|
|
272
|
+
const sock = createConnection({ port, host: "127.0.0.1" });
|
|
273
|
+
sock.setTimeout(800);
|
|
274
|
+
sock.once("connect", () => { sock.destroy(); resolve(true); });
|
|
275
|
+
sock.once("error", () => resolve(false));
|
|
276
|
+
sock.once("timeout", () => resolve(false));
|
|
277
|
+
});
|
|
240
278
|
}
|
|
241
279
|
|
|
242
|
-
|
|
243
|
-
async function checkMetroHealth(port, projectPath) {
|
|
280
|
+
async function getMetroError(projectPath) {
|
|
244
281
|
try {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
return { healthy: true };
|
|
257
|
-
} catch {
|
|
258
|
-
return { healthy: true }; // Don't fail if we can't check
|
|
259
|
-
}
|
|
282
|
+
const f = path.join(projectPath, ".claudeboard-expo-error.txt");
|
|
283
|
+
if (!fs.existsSync(f)) return null;
|
|
284
|
+
const stat = fs.statSync(f);
|
|
285
|
+
if (Date.now() - stat.mtimeMs > 5 * 60 * 1000) return null; // older than 5min
|
|
286
|
+
const content = fs.readFileSync(f, "utf8");
|
|
287
|
+
return content.includes("Unable to resolve") || content.includes("Cannot find module")
|
|
288
|
+
? content.slice(-800)
|
|
289
|
+
: null;
|
|
290
|
+
} catch { return null; }
|
|
260
291
|
}
|
package/bin/cli.js
CHANGED
|
@@ -8,8 +8,12 @@ import path from "path";
|
|
|
8
8
|
import fs from "fs";
|
|
9
9
|
import { spawn } from "child_process";
|
|
10
10
|
import open from "open";
|
|
11
|
+
import { createRequire } from "module";
|
|
11
12
|
|
|
13
|
+
const _require = createRequire(import.meta.url);
|
|
12
14
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const _pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "../package.json"), "utf8"));
|
|
16
|
+
|
|
13
17
|
|
|
14
18
|
const LOGO = `
|
|
15
19
|
${chalk.cyan("╔══════════════════════════════════════╗")}
|
|
@@ -236,6 +240,6 @@ program
|
|
|
236
240
|
program
|
|
237
241
|
.name("claudeboard")
|
|
238
242
|
.description("AI engineering team — from PRD to working app, autonomously")
|
|
239
|
-
.version(
|
|
243
|
+
.version(_pkg.version);
|
|
240
244
|
|
|
241
245
|
program.parse();
|