claudeboard 2.15.4 → 2.16.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.
@@ -3,7 +3,7 @@ import { createEpic, createTask } from "./board-client.js";
3
3
  import fs from "fs";
4
4
  import path from "path";
5
5
 
6
- const SYSTEM = `You are a senior mobile app architect specializing in React Native / Expo apps.
6
+ const SYSTEM_MOBILE = `You are a senior mobile app architect specializing in React Native / Expo apps.
7
7
  Your job is to read a PRD and produce a complete, ordered task breakdown.
8
8
 
9
9
  Rules:
@@ -15,6 +15,19 @@ Rules:
15
15
  - Types: config (setup/deps), feature (new screen or functionality), bug (fix), refactor, test (QA task)
16
16
  - When building on an existing project: do NOT recreate files that already exist — create tasks that extend or integrate with them`;
17
17
 
18
+ const SYSTEM_WEB = `You are a senior web app architect specializing in React + Vite apps.
19
+ Your job is to read a PRD and produce a complete, ordered task breakdown.
20
+
21
+ Rules:
22
+ - Think like a real engineering team: setup first, core features, then polish, then QA
23
+ - Each task must be self-contained and implementable by a single developer agent
24
+ - Be specific — tasks like "implement auth" are too vague. Break into: "create login page component", "implement Supabase auth hook", "add protected route with React Router"
25
+ - Always include: project setup (Vite + React), routing, data layer, each page/view, error handling, loading states, and final QA tasks
26
+ - Use React Router v6 for routing, TailwindCSS or CSS Modules for styles, Vite as build tool
27
+ - Priority: high = blocking/core, medium = main features, low = polish/nice-to-have
28
+ - Types: config (setup/deps), feature (new page or functionality), bug (fix), refactor, test (QA task)
29
+ - When building on an existing project: do NOT recreate files that already exist — create tasks that extend or integrate with them`;
30
+
18
31
  /**
19
32
  * Build a concise snapshot of the existing project:
20
33
  * - Top-level file/folder structure
@@ -67,7 +80,8 @@ function getProjectSnapshot(projectPath) {
67
80
  }
68
81
 
69
82
  export async function runArchitectAgent(prdContent, projectName, options = {}) {
70
- const { buildOnExisting = false, projectPath = null } = options;
83
+ const { buildOnExisting = false, projectPath = null, appType = "mobile" } = options;
84
+ const SYSTEM = appType === "web" ? SYSTEM_WEB : SYSTEM_MOBILE;
71
85
 
72
86
  console.log(" Architect analyzing PRD...");
73
87
 
@@ -79,6 +93,24 @@ export async function runArchitectAgent(prdContent, projectName, options = {}) {
79
93
  ? `\n\nEXISTING PROJECT SNAPSHOT:\n${projectSnapshot}\n\nIMPORTANT: Create tasks that BUILD ON top of what already exists. Do NOT recreate files or setup that is already in place. Analyze the snapshot carefully and only create tasks for what is missing or needs to be extended.`
80
94
  : "";
81
95
 
96
+ const techStackShape = appType === "web"
97
+ ? `"techStack": {
98
+ "framework": "vite+react",
99
+ "routing": "react-router-dom",
100
+ "stateManagement": "...",
101
+ "backend": "supabase / firebase / none",
102
+ "ui": "tailwindcss / css-modules / shadcn-ui",
103
+ "otherDeps": ["list of npm packages needed"]
104
+ }`
105
+ : `"techStack": {
106
+ "framework": "expo",
107
+ "navigation": "expo-router or react-navigation",
108
+ "stateManagement": "...",
109
+ "backend": "supabase / firebase / none",
110
+ "ui": "nativewind / tamagui / stylesheet",
111
+ "otherDeps": ["list of npm packages needed"]
112
+ }`;
113
+
82
114
  const result = await callClaudeJSON(SYSTEM, `
83
115
  Project: ${projectName}
84
116
 
@@ -88,14 +120,7 @@ ${existingProjectSection}
88
120
 
89
121
  Return this JSON structure:
90
122
  {
91
- "techStack": {
92
- "framework": "expo",
93
- "navigation": "expo-router or react-navigation",
94
- "stateManagement": "...",
95
- "backend": "supabase / firebase / none",
96
- "ui": "nativewind / tamagui / stylesheet",
97
- "otherDeps": ["list of npm packages needed"]
98
- },
123
+ ${techStackShape},
99
124
  "epics": [
100
125
  {
101
126
  "name": "Epic name (e.g. Project Setup, Authentication, Home Screen)",
@@ -10,9 +10,10 @@ import { createConnection } from "net";
10
10
  import { spawn as _spawn, execSync } from "child_process";
11
11
 
12
12
  const require = createRequire(import.meta.url);
13
- // On Windows, npm-installed binaries have a .cmd wrapper use it for spawn()
13
+ // On Windows, use shell:true to avoid spawn EINVAL with .cmd wrappers
14
14
  const isWin = process.platform === "win32";
15
- const NPX = isWin ? "npx.cmd" : "npx";
15
+ const NPX = "npx";
16
+ const SPAWN_OPTS_BASE = isWin ? { shell: true } : {};
16
17
  const MAX_FIX_ATTEMPTS = 5;
17
18
  const BUILD_HASH_FILE = ".claudeboard-build-hash.txt";
18
19
  const BUILD_TIMEOUT_MS = 10 * 60 * 1000; // 10 min max for expo run:ios
@@ -248,6 +249,7 @@ export async function ensureDevBuild(projectPath) {
248
249
 
249
250
  try {
250
251
  const proc = _spawn(NPX, ["expo", "run:ios", "--simulator"], {
252
+ ...SPAWN_OPTS_BASE,
251
253
  cwd: projectPath,
252
254
  env: { ...process.env, CI: "1", EXPO_NO_INTERACTIVE: "1" },
253
255
  stdio: "pipe",
@@ -390,6 +392,7 @@ async function tryStartExpo(projectPath, port) {
390
392
  : ["expo", "start", "--web", "--port", String(port)];
391
393
 
392
394
  proc = _spawn(NPX, expoArgs, {
395
+ ...SPAWN_OPTS_BASE,
393
396
  cwd: projectPath,
394
397
  env: (() => { const e = { ...process.env, CI: "1", EXPO_NO_INTERACTIVE: "1", EXPO_NO_DOTENV: "0" }; delete e.ANTHROPIC_API_KEY; return e; })(),
395
398
  stdio: "pipe",
@@ -582,3 +585,143 @@ export async function tapSimulator(x, y) {
582
585
  return true;
583
586
  } catch { return false; }
584
587
  }
588
+
589
+ // ── Vite Health Check (web apps) ─────────────────────────────────────────────
590
+
591
+ /**
592
+ * Ensure Vite is installed in the project, then start the dev server.
593
+ * Returns { ready: boolean, process: ChildProcess|null }
594
+ */
595
+ export async function runViteHealthCheck(projectPath, port = 5173) {
596
+ console.log(chalk.bold.cyan("\n[ VITE HEALTH CHECK ]\n"));
597
+
598
+ if (!fs.existsSync(path.join(projectPath, "package.json"))) {
599
+ console.log(chalk.dim(" No package.json — skipping"));
600
+ return { ready: false, process: null };
601
+ }
602
+
603
+ // Install deps
604
+ const installOk = await installDeps(projectPath);
605
+ if (!installOk) {
606
+ await injectFixTask(projectPath, "npm install fails — cannot start Vite",
607
+ "npm install fails with version conflicts. Fix all version constraints in package.json, then run npm install --legacy-peer-deps."
608
+ );
609
+ console.log(chalk.yellow(" ✗ Install failed — injected fix task into board\n"));
610
+ return { ready: false, process: null };
611
+ }
612
+
613
+ // Ensure vite is available
614
+ const pkgRaw = fs.readFileSync(path.join(projectPath, "package.json"), "utf8");
615
+ const pkg = JSON.parse(pkgRaw);
616
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
617
+ if (!allDeps["vite"]) {
618
+ console.log(chalk.dim(" Vite not found — installing vite + @vitejs/plugin-react..."));
619
+ await runCommand("npm install --save-dev vite @vitejs/plugin-react 2>&1", projectPath, 120000);
620
+ }
621
+
622
+ // Start Vite dev server
623
+ for (let attempt = 1; attempt <= MAX_FIX_ATTEMPTS; attempt++) {
624
+ console.log(chalk.dim(` Starting Vite (attempt ${attempt}/${MAX_FIX_ATTEMPTS})...`));
625
+ const result = await tryStartVite(projectPath, port);
626
+
627
+ if (result.ready) {
628
+ console.log(chalk.green(` ✓ Vite running on http://localhost:${port}\n`));
629
+ return { ready: true, process: result.process };
630
+ }
631
+
632
+ const error = result.error;
633
+ console.log(chalk.yellow(` ✗ Vite error: ${error?.split("\n")[0]?.slice(0, 120)}`));
634
+
635
+ if (attempt === MAX_FIX_ATTEMPTS) break;
636
+
637
+ const fixed = await applyExpoFix(projectPath, error);
638
+ if (!fixed) break;
639
+ console.log(chalk.dim(" Fix applied — retrying..."));
640
+ }
641
+
642
+ const lastError = await getLastExpoError(projectPath);
643
+ await injectFixTask(projectPath,
644
+ "FIX: Vite fails to start — resolve dependency errors",
645
+ `Vite dev server cannot start. Fix all issues so the app boots without errors.\n\nLast error:\n${lastError}\n\nSteps:\n1. Read package.json and identify incompatible versions\n2. Run: npm install --legacy-peer-deps\n3. Verify with: npx vite --port ${port}`
646
+ );
647
+
648
+ console.log(chalk.yellow(" ✗ Vite broken — injected fix task\n"));
649
+ return { ready: false, process: null };
650
+ }
651
+
652
+ async function tryStartVite(projectPath, port) {
653
+ return new Promise((resolve) => {
654
+ let output = "";
655
+ let resolved = false;
656
+ let proc = null;
657
+
658
+ const done = (ready, error) => {
659
+ if (resolved) return;
660
+ resolved = true;
661
+ if (!ready && proc) { try { proc.kill(); } catch {} proc = null; }
662
+ if (!ready && error) saveLastError(projectPath, error);
663
+ resolve({ ready, process: proc, error });
664
+ };
665
+
666
+ const timeout = setTimeout(() =>
667
+ done(false, `Vite did not become ready within 60s.\n${output.slice(-800)}`), 60000);
668
+
669
+ try {
670
+ proc = _spawn(NPX, ["vite", "--port", String(port), "--host", "localhost"], {
671
+ ...SPAWN_OPTS_BASE,
672
+ cwd: projectPath,
673
+ env: (() => { const e = { ...process.env }; delete e.ANTHROPIC_API_KEY; return e; })(),
674
+ stdio: "pipe",
675
+ });
676
+
677
+ const checkError = (text) => {
678
+ if (
679
+ text.includes("Cannot find module") ||
680
+ text.includes("error during build") ||
681
+ text.includes("Failed to resolve")
682
+ ) {
683
+ clearTimeout(timeout);
684
+ setTimeout(() => done(false, output.slice(-2000)), 1500);
685
+ }
686
+ };
687
+
688
+ proc.stdout.on("data", (d) => {
689
+ const text = d.toString();
690
+ output += text;
691
+ if (
692
+ text.includes("Local:") ||
693
+ text.includes(`localhost:${port}`) ||
694
+ text.includes("ready in")
695
+ ) {
696
+ setTimeout(() => {
697
+ if (!resolved) { clearTimeout(timeout); done(true, null); }
698
+ }, 2000);
699
+ }
700
+ checkError(text);
701
+ });
702
+
703
+ proc.stderr.on("data", (d) => {
704
+ const text = d.toString();
705
+ output += text;
706
+ checkError(text);
707
+ });
708
+
709
+ proc.on("close", (code) => {
710
+ clearTimeout(timeout);
711
+ if (!resolved) done(false, `Vite exited (code ${code})\n${output.slice(-1000)}`);
712
+ });
713
+
714
+ } catch (e) {
715
+ clearTimeout(timeout);
716
+ done(false, e.message);
717
+ }
718
+ });
719
+ }
720
+
721
+ // ── Generic app health check — delegates to Expo or Vite ─────────────────────
722
+ export async function runAppHealthCheck(projectPath, port, appType = "mobile") {
723
+ if (appType === "web") {
724
+ return runViteHealthCheck(projectPath, port || 5173);
725
+ }
726
+ return runExpoHealthCheck(projectPath, port || 8081);
727
+ }
@@ -5,7 +5,7 @@ import path from "path";
5
5
  import { runArchitectAgent } from "./architect.js";
6
6
  import { runDeveloperAgent } from "./developer.js";
7
7
  import { runQAAgent, runFullAppQA } from "./qa.js";
8
- import { runExpoHealthCheck, ensureDevBuild, packageJsonHash } from "./expo-health.js";
8
+ import { runAppHealthCheck, ensureDevBuild, packageJsonHash } from "./expo-health.js";
9
9
  import { createConnection } from "net";
10
10
 
11
11
  function isPortOpen(port) {
@@ -45,6 +45,7 @@ export async function runOrchestrator(config) {
45
45
  supabaseUrl,
46
46
  supabaseKey,
47
47
  projectName,
48
+ appType = "mobile",
48
49
  expoPort = 8081,
49
50
  skipArchitect = false,
50
51
  useIOS = false,
@@ -121,6 +122,7 @@ export async function runOrchestrator(config) {
121
122
  const archResult = await runArchitectAgent(prdContent, projectName, {
122
123
  buildOnExisting,
123
124
  projectPath,
125
+ appType,
124
126
  });
125
127
  techStack = archResult.techStack;
126
128
  console.log(chalk.green(`\n ✓ ${archResult.totalTasks} tasks created across ${archResult.epics.length} epics\n`));
@@ -132,9 +134,10 @@ export async function runOrchestrator(config) {
132
134
  const logFile = path.join(projectPath, ".claudeboard-logs.txt");
133
135
  const logStream = fs.createWriteStream(logFile, { flags: "a" });
134
136
 
135
- // ── EXPO HEALTH CHECK: install deps + start + auto-fix errors ─────────────
137
+ // ── APP HEALTH CHECK: install deps + start dev server + auto-fix errors ───
136
138
  // Runs before development loop so the app is visible from task #1
137
- const expoHealth = await runExpoHealthCheck(projectPath, expoPort);
139
+ const devPort = appType === "web" ? (expoPort !== 8081 ? expoPort : 5173) : expoPort;
140
+ const expoHealth = await runAppHealthCheck(projectPath, devPort, appType);
138
141
  expoReady = expoHealth.ready;
139
142
  expoProcess = expoHealth.process;
140
143
 
@@ -183,10 +186,10 @@ export async function runOrchestrator(config) {
183
186
  consecutiveFailures = 0;
184
187
 
185
188
  // ── If this was an Expo fix task, clear the error and re-health-check ────
186
- if (task.title?.toLowerCase().includes("expo") && task.type === "bug") {
189
+ if ((task.title?.toLowerCase().includes("expo") || task.title?.toLowerCase().includes("vite")) && task.type === "bug") {
187
190
  try { fs.unlinkSync(path.join(projectPath, ".claudeboard-expo-error.txt")); } catch {}
188
- console.log(chalk.dim(" Expo fix completed — re-running health check..."));
189
- const recheck = await runExpoHealthCheck(projectPath, expoPort);
191
+ console.log(chalk.dim(" Dev server fix completed — re-running health check..."));
192
+ const recheck = await runAppHealthCheck(projectPath, devPort, appType);
190
193
  expoReady = recheck.ready;
191
194
  expoProcess = recheck.process;
192
195
  }
@@ -216,10 +219,10 @@ export async function runOrchestrator(config) {
216
219
 
217
220
  // ── Re-check Expo health if it's supposed to be running but isn't ────────
218
221
  if (expoReady) {
219
- const portOpen = await isPortOpen(expoPort);
222
+ const portOpen = await isPortOpen(devPort);
220
223
  if (!portOpen) {
221
- console.log(chalk.yellow(" ⚠ Expo seems to have crashed — running health check..."));
222
- const recheck = await runExpoHealthCheck(projectPath, expoPort);
224
+ console.log(chalk.yellow(" ⚠ Dev server seems to have crashed — running health check..."));
225
+ const recheck = await runAppHealthCheck(projectPath, devPort, appType);
223
226
  expoReady = recheck.ready;
224
227
  expoProcess = recheck.process;
225
228
  }
@@ -229,10 +232,10 @@ export async function runOrchestrator(config) {
229
232
  const shouldRunQA = expoReady && ["feature", "bug"].includes(task.type);
230
233
 
231
234
  if (shouldRunQA) {
232
- // Give Expo a moment to hot-reload
235
+ // Give dev server a moment to hot-reload
233
236
  await new Promise((r) => setTimeout(r, 3000));
234
237
 
235
- const qaResult = await runQAAgent(task, devResult, projectPath, prdContent, expoPort);
238
+ const qaResult = await runQAAgent(task, devResult, projectPath, prdContent, devPort);
236
239
 
237
240
  if (!qaResult.passed && qaResult.fixInstructions) {
238
241
  console.log(chalk.yellow(" ↩️ QA failed — sending back to developer..."));
@@ -249,7 +252,7 @@ export async function runOrchestrator(config) {
249
252
 
250
253
  if (fixedByDev.success) {
251
254
  await new Promise((r) => setTimeout(r, 3000));
252
- await runQAAgent(task, fixedByDev, projectPath, prdContent, expoPort);
255
+ await runQAAgent(task, fixedByDev, projectPath, prdContent, devPort);
253
256
  }
254
257
  }
255
258
  }
@@ -261,7 +264,7 @@ export async function runOrchestrator(config) {
261
264
  // ── PHASE 3: FINAL QA ─────────────────────────────────────────────────────
262
265
  if (expoReady) {
263
266
  console.log(chalk.bold.cyan("\n[ PHASE 3: FINAL QA ]\n"));
264
- const fullQAReport = await runFullAppQA(projectPath, prdContent, expoPort);
267
+ const fullQAReport = await runFullAppQA(projectPath, prdContent, devPort);
265
268
 
266
269
  if (fullQAReport.issues.length > 0) {
267
270
  console.log(chalk.yellow(` ⚠️ Final QA found ${fullQAReport.issues.length} issues:`));
package/bin/cli.js CHANGED
@@ -52,6 +52,12 @@ program
52
52
 
53
53
  const answers = await enquirer.prompt([
54
54
  { type: "input", name: "projectName", message: "Project name:", initial: path.basename(process.cwd()) },
55
+ {
56
+ type: "select",
57
+ name: "appType",
58
+ message: "App type:",
59
+ choices: ["mobile (Expo / React Native)", "web (Vite + React)"],
60
+ },
55
61
  { type: "input", name: "supabaseUrl", message: "Supabase URL:", hint: "https://xxxx.supabase.co" },
56
62
  { type: "input", name: "supabaseKey", message: "Supabase anon key:" },
57
63
  { type: "password", name: "anthropicKey", message: "Anthropic API key:", hint: "sk-ant-..." },
@@ -62,6 +68,7 @@ program
62
68
 
63
69
  const config = {
64
70
  projectName: answers.projectName,
71
+ appType: answers.appType.startsWith("mobile") ? "mobile" : "web",
65
72
  port: parseInt(answers.port),
66
73
  supabaseUrl: answers.supabaseUrl,
67
74
  supabaseKey: answers.supabaseKey,
@@ -193,6 +200,7 @@ program
193
200
  supabaseUrl: config.supabaseUrl,
194
201
  supabaseKey: config.supabaseKey,
195
202
  projectName: config.projectName,
203
+ appType: config.appType || "mobile",
196
204
  expoPort: parseInt(opts.expoPort),
197
205
  forceRestart: !!opts.restart,
198
206
  useIOS: !!opts.ios,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeboard",
3
- "version": "2.15.4",
3
+ "version": "2.16.0",
4
4
  "description": "AI engineering team — from PRD to working mobile app, autonomously",
5
5
  "type": "module",
6
6
  "bin": {