claudeboard 2.16.0 → 3.1.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.
@@ -1,727 +0,0 @@
1
- import { callClaudeJSON } from "./claude-api.js";
2
- import { runCommand } from "../tools/terminal.js";
3
- import { createTask, createEpic, addLog } from "./board-client.js";
4
- import chalk from "chalk";
5
- import fs from "fs";
6
- import path from "path";
7
- import crypto from "crypto";
8
- import { createRequire } from "module";
9
- import { createConnection } from "net";
10
- import { spawn as _spawn, execSync } from "child_process";
11
-
12
- const require = createRequire(import.meta.url);
13
- // On Windows, use shell:true to avoid spawn EINVAL with .cmd wrappers
14
- const isWin = process.platform === "win32";
15
- const NPX = "npx";
16
- const SPAWN_OPTS_BASE = isWin ? { shell: true } : {};
17
- const MAX_FIX_ATTEMPTS = 5;
18
- const BUILD_HASH_FILE = ".claudeboard-build-hash.txt";
19
- const BUILD_TIMEOUT_MS = 10 * 60 * 1000; // 10 min max for expo run:ios
20
-
21
- /**
22
- * Expo Health Check Agent — runs BEFORE the development loop.
23
- *
24
- * Strategy:
25
- * 1. Try npm install (--legacy-peer-deps, then --force if needed)
26
- * 2. If install fails with ETARGET/missing version → ask Claude for fix → apply → retry
27
- * 3. If iOS mode: ensure a dev build exists in the simulator (builds if needed)
28
- * 4. Start Expo, wait for Metro to be truly ready (not just port open)
29
- * 5. If Metro crashes with module/dep errors → ask Claude → apply → retry
30
- * 6. If still broken after MAX attempts → inject a BLOCKER task into the board
31
- * so the developer agent fixes it before any other task runs
32
- *
33
- * Returns { ready: boolean, process: ChildProcess|null }
34
- */
35
- export async function runExpoHealthCheck(projectPath, port = 8081) {
36
- console.log(chalk.bold.cyan("\n[ EXPO HEALTH CHECK ]\n"));
37
-
38
- if (!fs.existsSync(path.join(projectPath, "package.json"))) {
39
- console.log(chalk.dim(" No package.json — skipping"));
40
- return { ready: false, process: null };
41
- }
42
-
43
- // ── 1. Install ─────────────────────────────────────────────────────────────
44
- const installOk = await installDeps(projectPath);
45
- if (!installOk) {
46
- await injectFixTask(projectPath, "npm install fails — cannot start Expo",
47
- "npm install fails with ETARGET or unresolvable version conflicts. " +
48
- "Audit package.json, fix all version constraints to be compatible with " +
49
- "the installed Expo SDK, then run npm install --legacy-peer-deps."
50
- );
51
- console.log(chalk.yellow(" ✗ Install failed — injected fix task into board\n"));
52
- return { ready: false, process: null };
53
- }
54
-
55
- // ── 2. Dev build (iOS mode only) ───────────────────────────────────────────
56
- const useIOS = process.env.CLAUDEBOARD_IOS === "1" || isSimulatorAvailableSync();
57
- if (useIOS) {
58
- const buildOk = await ensureDevBuild(projectPath);
59
- if (!buildOk) {
60
- await injectFixTask(projectPath,
61
- "FIX: Dev build failed — fix native compilation errors",
62
- "npx expo run:ios failed. Fix any native dependency or Podfile issues so the app compiles.\n" +
63
- "Run: npx expo run:ios --simulator and fix all errors."
64
- );
65
- console.log(chalk.yellow(" ✗ Dev build failed — injected fix task into board\n"));
66
- return { ready: false, process: null };
67
- }
68
- }
69
-
70
- // ── 3. Start Expo + verify Metro is error-free ─────────────────────────────
71
- for (let attempt = 1; attempt <= MAX_FIX_ATTEMPTS; attempt++) {
72
- console.log(chalk.dim(` Starting Expo (attempt ${attempt}/${MAX_FIX_ATTEMPTS})...`));
73
-
74
- const result = await tryStartExpo(projectPath, port);
75
-
76
- if (result.ready) {
77
- console.log(chalk.green(` ✓ Expo running cleanly on port ${port}\n`));
78
- return { ready: true, process: result.process };
79
- }
80
-
81
- const error = result.error;
82
- console.log(chalk.yellow(` ✗ Expo error: ${error?.split("\n")[0]?.slice(0, 120)}`));
83
-
84
- if (attempt === MAX_FIX_ATTEMPTS) break;
85
-
86
- // Ask Claude for a fix and apply it
87
- const fixed = await applyExpoFix(projectPath, error);
88
- if (!fixed) break;
89
- console.log(chalk.dim(" Fix applied — retrying..."));
90
- }
91
-
92
- // All attempts failed — inject task so developer agent fixes it
93
- const lastError = await getLastExpoError(projectPath);
94
- await injectFixTask(projectPath,
95
- "FIX: Expo fails to start — resolve dependency errors",
96
- `Expo cannot start due to dependency errors. Fix ALL issues so the app boots without errors.\n\n` +
97
- `Last error:\n${lastError}\n\n` +
98
- `Steps:\n` +
99
- `1. Read package.json and identify incompatible versions\n` +
100
- `2. Fix react-native-worklets, react-native-reanimated and any other conflicting packages\n` +
101
- `3. Run: npm install --legacy-peer-deps\n` +
102
- `4. Verify with: npx expo start --web --port ${port} (should not crash)\n` +
103
- `5. If imports fail (Unable to resolve module), remove or replace the problematic import`
104
- );
105
-
106
- console.log(chalk.yellow(" ✗ Expo broken — injected fix task (will be worked on first)\n"));
107
- return { ready: false, process: null };
108
- }
109
-
110
- // ── Dev Build management ───────────────────────────────────────────────────
111
-
112
- /**
113
- * Compute a short hash of package.json to detect dependency changes.
114
- */
115
- export function packageJsonHash(projectPath) {
116
- try {
117
- const content = fs.readFileSync(path.join(projectPath, "package.json"), "utf8");
118
- return crypto.createHash("md5").update(content).digest("hex").slice(0, 12);
119
- } catch {
120
- return null;
121
- }
122
- }
123
-
124
- /**
125
- * Returns true if a rebuild is needed:
126
- * - No saved hash (never built), OR
127
- * - package.json changed since last build
128
- */
129
- export function needsRebuild(projectPath) {
130
- const hashFile = path.join(projectPath, BUILD_HASH_FILE);
131
- if (!fs.existsSync(hashFile)) return true;
132
- try {
133
- const saved = fs.readFileSync(hashFile, "utf8").trim();
134
- return saved !== packageJsonHash(projectPath);
135
- } catch {
136
- return true;
137
- }
138
- }
139
-
140
- /**
141
- * Persist the current package.json hash after a successful build.
142
- */
143
- function saveBuildHash(projectPath) {
144
- try {
145
- const hash = packageJsonHash(projectPath);
146
- if (hash) fs.writeFileSync(path.join(projectPath, BUILD_HASH_FILE), hash, "utf8");
147
- } catch {}
148
- }
149
-
150
- /**
151
- * Check if the app's dev build is already installed in the booted simulator.
152
- */
153
- function isDevBuildInstalled(projectPath) {
154
- try {
155
- const bundleId = getBundleId(projectPath);
156
- if (!bundleId) return false;
157
- const output = execSync(`xcrun simctl get_app_container booted "${bundleId}" 2>&1`, { encoding: "utf8" });
158
- return output.trim().length > 0 && !output.includes("No such file");
159
- } catch {
160
- return false;
161
- }
162
- }
163
-
164
- /**
165
- * Read the iOS bundle identifier from app.json / app.config.js.
166
- * Falls back to deriving it from the project slug.
167
- */
168
- function getBundleId(projectPath) {
169
- try {
170
- const appJsonPath = path.join(projectPath, "app.json");
171
- if (fs.existsSync(appJsonPath)) {
172
- const cfg = JSON.parse(fs.readFileSync(appJsonPath, "utf8"));
173
- const bundleId = cfg?.expo?.ios?.bundleIdentifier;
174
- if (bundleId) return bundleId;
175
- // Derive from slug if no explicit bundleIdentifier
176
- const slug = cfg?.expo?.slug;
177
- if (slug) return `com.anonymous.${slug.replace(/[^a-zA-Z0-9]/g, "")}`;
178
- }
179
- } catch {}
180
- return null;
181
- }
182
-
183
- /**
184
- * Boot the first available iOS simulator if none is booted.
185
- */
186
- function ensureSimulatorBooted() {
187
- try {
188
- const booted = execSync("xcrun simctl list devices booted 2>&1", { encoding: "utf8" });
189
- if (booted.includes("Booted")) return true;
190
-
191
- // Find a suitable device to boot
192
- const devices = execSync("xcrun simctl list devices available 2>&1", { encoding: "utf8" });
193
- const match = devices.match(/iPhone \d[^(]*\(([A-F0-9-]{36})\)/i);
194
- if (!match) return false;
195
-
196
- console.log(chalk.dim(` Booting simulator ${match[1]}...`));
197
- execSync(`xcrun simctl boot "${match[1]}" 2>&1`, { encoding: "utf8" });
198
- // Give the simulator a moment to fully boot
199
- execSync("sleep 5");
200
- return true;
201
- } catch {
202
- return false;
203
- }
204
- }
205
-
206
- /**
207
- * Build and install the dev build in the simulator using `expo run:ios`.
208
- * Only rebuilds when package.json changed or the app is not installed.
209
- * Returns true on success, false on failure.
210
- */
211
- export async function ensureDevBuild(projectPath) {
212
- const installed = isDevBuildInstalled(projectPath);
213
- const rebuild = needsRebuild(projectPath);
214
-
215
- if (installed && !rebuild) {
216
- console.log(chalk.dim(" ✓ Dev build up to date — skipping rebuild"));
217
- return true;
218
- }
219
-
220
- const reason = !installed ? "app not installed" : "dependencies changed";
221
- console.log(chalk.cyan(` Building dev build (${reason})...`));
222
- console.log(chalk.dim(" This may take several minutes on first run."));
223
-
224
- // Make sure a simulator is running
225
- const simReady = ensureSimulatorBooted();
226
- if (!simReady) {
227
- console.log(chalk.yellow(" ✗ No iOS simulator available"));
228
- return false;
229
- }
230
-
231
- return new Promise((resolve) => {
232
- let output = "";
233
- let resolved = false;
234
-
235
- const done = (ok) => {
236
- if (resolved) return;
237
- resolved = true;
238
- if (ok) {
239
- saveBuildHash(projectPath);
240
- console.log(chalk.green(" ✓ Dev build installed in simulator"));
241
- }
242
- resolve(ok);
243
- };
244
-
245
- const timeout = setTimeout(() => {
246
- console.log(chalk.yellow(" ✗ Dev build timed out after 10 minutes"));
247
- done(false);
248
- }, BUILD_TIMEOUT_MS);
249
-
250
- try {
251
- const proc = _spawn(NPX, ["expo", "run:ios", "--simulator"], {
252
- ...SPAWN_OPTS_BASE,
253
- cwd: projectPath,
254
- env: { ...process.env, CI: "1", EXPO_NO_INTERACTIVE: "1" },
255
- stdio: "pipe",
256
- });
257
-
258
- const onData = (d) => {
259
- const text = d.toString();
260
- output += text;
261
- // Log progress lines that are meaningful
262
- const lines = text.split("\n").filter(l => l.trim() && !l.includes("\u001b["));
263
- for (const line of lines) {
264
- if (
265
- line.includes("Installing") ||
266
- line.includes("Building") ||
267
- line.includes("Compiling") ||
268
- line.includes("Linking") ||
269
- line.includes("error:") ||
270
- line.includes("warning:")
271
- ) {
272
- console.log(chalk.dim(` ${line.trim().slice(0, 100)}`));
273
- }
274
- }
275
-
276
- if (
277
- text.includes("Installed on") ||
278
- text.includes("Opening on") ||
279
- text.includes("Successfully built") ||
280
- text.includes("BUILD SUCCEEDED")
281
- ) {
282
- clearTimeout(timeout);
283
- done(true);
284
- }
285
-
286
- if (
287
- text.includes("BUILD FAILED") ||
288
- text.includes("error: ") ||
289
- text.includes("Command failed")
290
- ) {
291
- // Wait a bit to collect full error output
292
- setTimeout(() => {
293
- clearTimeout(timeout);
294
- saveLastError(projectPath, output.slice(-3000));
295
- done(false);
296
- }, 2000);
297
- }
298
- };
299
-
300
- proc.stdout.on("data", onData);
301
- proc.stderr.on("data", onData);
302
-
303
- proc.on("close", (code) => {
304
- clearTimeout(timeout);
305
- if (!resolved) {
306
- if (code === 0) {
307
- done(true);
308
- } else {
309
- saveLastError(projectPath, output.slice(-3000));
310
- done(false);
311
- }
312
- }
313
- });
314
-
315
- } catch (e) {
316
- clearTimeout(timeout);
317
- done(false);
318
- }
319
- });
320
- }
321
-
322
- // ── npm install with progressive fallback ─────────────────────────────────
323
- async function installDeps(projectPath) {
324
- const pkg = JSON.parse(fs.readFileSync(path.join(projectPath, "package.json"), "utf8"));
325
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
326
-
327
- // Step 1: legacy-peer-deps
328
- console.log(chalk.dim(" npm install --legacy-peer-deps..."));
329
- let result = await runCommand("npm install --legacy-peer-deps 2>&1", projectPath, 180000);
330
- if (!hasFatalNpmError(result.stdout)) {
331
- console.log(chalk.dim(" ✓ Dependencies installed"));
332
- return true;
333
- }
334
-
335
- const error1 = result.stdout;
336
- console.log(chalk.yellow(` ⚠ Install error: ${getFatalError(error1)}`));
337
-
338
- // Step 2: ask Claude to fix version conflicts
339
- console.log(chalk.dim(" Asking Claude to resolve version conflicts..."));
340
- const fix = await askClaudeForInstallFix(deps, error1);
341
- if (fix?.commands?.length) {
342
- console.log(chalk.dim(` Diagnosis: ${fix.diagnosis}`));
343
- for (const cmd of fix.commands) {
344
- console.log(chalk.dim(` → ${cmd}`));
345
- await runCommand(cmd + " 2>&1", projectPath, 180000);
346
- }
347
- // Retry install
348
- result = await runCommand("npm install --legacy-peer-deps 2>&1", projectPath, 180000);
349
- if (!hasFatalNpmError(result.stdout)) {
350
- console.log(chalk.dim(" ✓ Dependencies installed after fix"));
351
- return true;
352
- }
353
- }
354
-
355
- // Step 3: --force (last resort)
356
- console.log(chalk.dim(" Retrying with --force..."));
357
- result = await runCommand("npm install --force 2>&1", projectPath, 180000);
358
- if (!hasFatalNpmError(result.stdout)) {
359
- console.log(chalk.dim(" ✓ Dependencies installed (--force)"));
360
- return true;
361
- }
362
-
363
- return false;
364
- }
365
-
366
- // ── Start Expo and wait until Metro is TRULY ready (no errors) ─────────────
367
- // We wait up to 60s. Success = "Metro waiting on" AND no error lines.
368
- // Failure = any "Unable to resolve module", "Cannot find module", etc.
369
- async function tryStartExpo(projectPath, port) {
370
- return new Promise((resolve) => {
371
- let output = "";
372
- let resolved = false;
373
- let proc = null;
374
-
375
- const done = (ready, error) => {
376
- if (resolved) return;
377
- resolved = true;
378
- if (!ready && proc) { try { proc.kill(); } catch {} proc = null; }
379
- // Save error for later
380
- if (!ready && error) saveLastError(projectPath, error);
381
- resolve({ ready, process: proc, error });
382
- };
383
-
384
- const timeout = setTimeout(() =>
385
- done(false, `Expo did not become ready within 60s.\n${output.slice(-800)}`), 60000);
386
-
387
- try {
388
- // iOS mode: use dev build (--go is intentionally omitted so the installed dev build opens)
389
- const useIOS = process.env.CLAUDEBOARD_IOS === "1" || isSimulatorAvailableSync();
390
- const expoArgs = useIOS
391
- ? ["expo", "start", "--ios", "--port", String(port)]
392
- : ["expo", "start", "--web", "--port", String(port)];
393
-
394
- proc = _spawn(NPX, expoArgs, {
395
- ...SPAWN_OPTS_BASE,
396
- cwd: projectPath,
397
- env: (() => { const e = { ...process.env, CI: "1", EXPO_NO_INTERACTIVE: "1", EXPO_NO_DOTENV: "0" }; delete e.ANTHROPIC_API_KEY; return e; })(),
398
- stdio: "pipe",
399
- });
400
-
401
- const checkError = (text) => {
402
- if (
403
- text.includes("Unable to resolve module") ||
404
- text.includes("Cannot find module") ||
405
- text.includes("Error: Unable") ||
406
- text.includes("Module not found") ||
407
- text.includes("SyntaxError") ||
408
- text.includes("ENOENT: no such file")
409
- ) {
410
- clearTimeout(timeout);
411
- setTimeout(() => done(false, output.slice(-2000)), 1500); // collect full error
412
- }
413
- };
414
-
415
- proc.stdout.on("data", (d) => {
416
- const text = d.toString();
417
- output += text;
418
- // Metro ready signal — but wait 3s to make sure no errors follow
419
- if (
420
- text.includes("Metro waiting on") ||
421
- text.includes(`http://localhost:${port}`) ||
422
- text.includes("Bundling complete") ||
423
- text.includes("Web is waiting on") ||
424
- text.includes("Opening on iOS") ||
425
- text.includes("Installed on iOS") ||
426
- text.includes("Building JavaScript bundle")
427
- ) {
428
- setTimeout(() => {
429
- // If we haven't already failed, declare success
430
- if (!resolved) { clearTimeout(timeout); done(true, null); }
431
- }, 3000);
432
- }
433
- checkError(text);
434
- });
435
-
436
- proc.stderr.on("data", (d) => {
437
- const text = d.toString();
438
- output += text;
439
- checkError(text);
440
- });
441
-
442
- proc.on("close", (code) => {
443
- clearTimeout(timeout);
444
- if (!resolved) done(false, `Expo exited (code ${code})\n${output.slice(-1000)}`);
445
- });
446
-
447
- } catch (e) {
448
- clearTimeout(timeout);
449
- done(false, e.message);
450
- }
451
- });
452
- }
453
-
454
- // ── Ask Claude how to fix a runtime Expo error ────────────────────────────
455
- async function applyExpoFix(projectPath, errorText) {
456
- const pkg = JSON.parse(fs.readFileSync(path.join(projectPath, "package.json"), "utf8"));
457
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
458
-
459
- let verdict;
460
- try {
461
- verdict = await callClaudeJSON(
462
- "You are a React Native / Expo dependency expert. Respond only with valid JSON.",
463
- `Expo failed to start with this error:
464
- \`\`\`
465
- ${errorText}
466
- \`\`\`
467
-
468
- Current dependencies:
469
- ${JSON.stringify(deps, null, 2)}
470
-
471
- Provide the exact commands to fix this.
472
- For "Unable to resolve module X from file Y": the fix is usually to install the missing package or fix its version.
473
- For react-native-worklets version conflict: install react-native-worklets@latest.
474
-
475
- Respond with JSON:
476
- {
477
- "diagnosis": "one sentence",
478
- "commands": ["npm install ...", "..."],
479
- "confidence": 0-100
480
- }`
481
- );
482
- } catch { return false; }
483
-
484
- if (!verdict?.commands?.length || verdict.confidence < 30) return false;
485
-
486
- console.log(chalk.dim(` Diagnosis: ${verdict.diagnosis}`));
487
- for (const cmd of verdict.commands) {
488
- console.log(chalk.dim(` → ${cmd}`));
489
- await runCommand(cmd + " 2>&1", projectPath, 120000);
490
- }
491
- return true;
492
- }
493
-
494
- // ── Ask Claude how to fix npm install failures ────────────────────────────
495
- async function askClaudeForInstallFix(deps, errorText) {
496
- try {
497
- return await callClaudeJSON(
498
- "You are a React Native dependency expert. Respond only with valid JSON.",
499
- `npm install failed:
500
- ${errorText}
501
-
502
- Dependencies: ${JSON.stringify(deps, null, 2)}
503
-
504
- Provide exact commands to resolve the version conflict.
505
- For ETARGET (version not found): suggest the nearest valid version.
506
- For peer dep conflicts: suggest compatible versions for all conflicting packages.
507
-
508
- JSON: { "diagnosis": "...", "commands": ["..."], "confidence": 0-100 }`
509
- );
510
- } catch { return null; }
511
- }
512
-
513
- // ── Create a high-priority fix task in the board ──────────────────────────
514
- async function injectFixTask(projectPath, title, description) {
515
- try {
516
- // Make sure the epic exists
517
- const epicId = await createEpic("⚠ Expo Fix Required", "expo-fix");
518
- await createTask({
519
- epicId,
520
- title,
521
- description,
522
- priority: "high",
523
- priorityOrder: 0, // Run FIRST
524
- type: "bug",
525
- status: "todo",
526
- });
527
- console.log(chalk.yellow(` ➕ Injected task: "${title}"`));
528
- } catch (e) {
529
- console.log(chalk.dim(` Could not inject task: ${e.message}`));
530
- }
531
- }
532
-
533
- // ── Save/read last error to disk for later reference ─────────────────────
534
- function saveLastError(projectPath, error) {
535
- try {
536
- fs.writeFileSync(path.join(projectPath, ".claudeboard-expo-error.txt"), error, "utf8");
537
- } catch {}
538
- }
539
-
540
- async function getLastExpoError(projectPath) {
541
- try {
542
- const f = path.join(projectPath, ".claudeboard-expo-error.txt");
543
- return fs.existsSync(f) ? fs.readFileSync(f, "utf8").slice(-1500) : "Unknown error";
544
- } catch { return "Unknown error"; }
545
- }
546
-
547
- // ── Helpers ───────────────────────────────────────────────────────────────
548
- function hasFatalNpmError(output) {
549
- return output.includes("npm error code ETARGET") ||
550
- output.includes("notarget No matching version") ||
551
- (output.includes("npm error") && output.includes("ERESOLVE") && output.includes("unable to resolve"));
552
- }
553
-
554
- function getFatalError(output) {
555
- return output.split("\n").find(l => l.includes("npm error") && !l.includes("peer")) || output.slice(-200);
556
- }
557
-
558
- // ── iOS Simulator detection ───────────────────────────────────────────────────
559
- function isSimulatorAvailableSync() {
560
- try {
561
- const output = execSync("xcrun simctl list devices booted 2>&1", { encoding: "utf8" });
562
- if (output.includes("Booted")) return true;
563
- const runtimes = execSync("xcrun simctl list runtimes 2>&1", { encoding: "utf8" });
564
- return runtimes.includes("iOS") && !runtimes.includes("unavailable");
565
- } catch {
566
- return false;
567
- }
568
- }
569
-
570
- // Take a screenshot of the iOS simulator
571
- export async function screenshotSimulator(outputPath) {
572
- try {
573
- execSync(`xcrun simctl io booted screenshot "${outputPath}"`, { stdio: "pipe" });
574
- const data = fs.readFileSync(outputPath);
575
- return { success: true, base64: data.toString("base64"), path: outputPath };
576
- } catch (e) {
577
- return { success: false, error: e.message };
578
- }
579
- }
580
-
581
- // Tap on the iOS simulator at given coordinates
582
- export async function tapSimulator(x, y) {
583
- try {
584
- execSync(`xcrun simctl io booted tap ${x} ${y}`, { stdio: "pipe" });
585
- return true;
586
- } catch { return false; }
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
- }