claudeboard 2.15.3 → 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.
- package/agents/architect.js +35 -10
- package/agents/expo-health.js +148 -2
- package/agents/orchestrator.js +16 -13
- package/bin/cli.js +8 -0
- package/package.json +1 -1
- package/tools/terminal.js +10 -5
package/agents/architect.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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)",
|
package/agents/expo-health.js
CHANGED
|
@@ -10,6 +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, 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 } : {};
|
|
13
17
|
const MAX_FIX_ATTEMPTS = 5;
|
|
14
18
|
const BUILD_HASH_FILE = ".claudeboard-build-hash.txt";
|
|
15
19
|
const BUILD_TIMEOUT_MS = 10 * 60 * 1000; // 10 min max for expo run:ios
|
|
@@ -244,7 +248,8 @@ export async function ensureDevBuild(projectPath) {
|
|
|
244
248
|
}, BUILD_TIMEOUT_MS);
|
|
245
249
|
|
|
246
250
|
try {
|
|
247
|
-
const proc = _spawn(
|
|
251
|
+
const proc = _spawn(NPX, ["expo", "run:ios", "--simulator"], {
|
|
252
|
+
...SPAWN_OPTS_BASE,
|
|
248
253
|
cwd: projectPath,
|
|
249
254
|
env: { ...process.env, CI: "1", EXPO_NO_INTERACTIVE: "1" },
|
|
250
255
|
stdio: "pipe",
|
|
@@ -386,7 +391,8 @@ async function tryStartExpo(projectPath, port) {
|
|
|
386
391
|
? ["expo", "start", "--ios", "--port", String(port)]
|
|
387
392
|
: ["expo", "start", "--web", "--port", String(port)];
|
|
388
393
|
|
|
389
|
-
proc = _spawn(
|
|
394
|
+
proc = _spawn(NPX, expoArgs, {
|
|
395
|
+
...SPAWN_OPTS_BASE,
|
|
390
396
|
cwd: projectPath,
|
|
391
397
|
env: (() => { const e = { ...process.env, CI: "1", EXPO_NO_INTERACTIVE: "1", EXPO_NO_DOTENV: "0" }; delete e.ANTHROPIC_API_KEY; return e; })(),
|
|
392
398
|
stdio: "pipe",
|
|
@@ -579,3 +585,143 @@ export async function tapSimulator(x, y) {
|
|
|
579
585
|
return true;
|
|
580
586
|
} catch { return false; }
|
|
581
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
|
+
}
|
package/agents/orchestrator.js
CHANGED
|
@@ -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 {
|
|
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
|
-
// ──
|
|
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
|
|
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("
|
|
189
|
-
const recheck = await
|
|
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(
|
|
222
|
+
const portOpen = await isPortOpen(devPort);
|
|
220
223
|
if (!portOpen) {
|
|
221
|
-
console.log(chalk.yellow(" ⚠
|
|
222
|
-
const recheck = await
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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
package/tools/terminal.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { exec, spawn } from "child_process";
|
|
2
2
|
import { promisify } from "util";
|
|
3
|
+
import { createConnection } from "net";
|
|
3
4
|
|
|
4
5
|
const execAsync = promisify(exec);
|
|
6
|
+
const isWin = process.platform === "win32";
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Run a shell command in a given directory, return { stdout, stderr, exitCode }
|
|
@@ -12,6 +14,7 @@ export async function runCommand(cmd, cwd, timeoutMs = 60000) {
|
|
|
12
14
|
cwd,
|
|
13
15
|
timeout: timeoutMs,
|
|
14
16
|
env: { ...process.env, CI: "true", FORCE_COLOR: "0" },
|
|
17
|
+
shell: isWin ? "cmd.exe" : "/bin/sh",
|
|
15
18
|
});
|
|
16
19
|
return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode: 0 };
|
|
17
20
|
} catch (err) {
|
|
@@ -43,15 +46,17 @@ export function startProcess(cmd, args, cwd, onLog) {
|
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
/**
|
|
46
|
-
* Check if a port is in use
|
|
49
|
+
* Check if a port is in use — uses a TCP connection probe (cross-platform, no curl needed)
|
|
47
50
|
*/
|
|
48
51
|
export async function waitForPort(port, timeoutMs = 30000) {
|
|
49
52
|
const start = Date.now();
|
|
50
53
|
while (Date.now() - start < timeoutMs) {
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
const open = await new Promise((resolve) => {
|
|
55
|
+
const sock = createConnection({ port, host: "127.0.0.1" });
|
|
56
|
+
sock.once("connect", () => { sock.destroy(); resolve(true); });
|
|
57
|
+
sock.once("error", () => { sock.destroy(); resolve(false); });
|
|
58
|
+
});
|
|
59
|
+
if (open) return true;
|
|
55
60
|
await new Promise((r) => setTimeout(r, 1000));
|
|
56
61
|
}
|
|
57
62
|
return false;
|