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