claudeboard 2.3.0 → 2.8.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/claude-api.js +1 -1
- package/agents/expo-health.js +240 -0
- package/agents/orchestrator.js +28 -22
- package/agents/qa.js +160 -139
- package/bin/cli.js +11 -1
- package/dashboard/index.html +213 -70
- package/dashboard/server.js +89 -35
- package/package.json +1 -1
package/dashboard/server.js
CHANGED
|
@@ -8,6 +8,7 @@ import { fileURLToPath } from "url";
|
|
|
8
8
|
import fs from "fs";
|
|
9
9
|
import { spawn } from "child_process";
|
|
10
10
|
import { createRequire } from "module";
|
|
11
|
+
import { createConnection } from "net";
|
|
11
12
|
|
|
12
13
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
14
|
const require = createRequire(import.meta.url);
|
|
@@ -126,14 +127,53 @@ termWss.on("connection", (ws) => {
|
|
|
126
127
|
|
|
127
128
|
// ── EXPO MANAGEMENT ───────────────────────────────────────────────────────────
|
|
128
129
|
|
|
130
|
+
// On server start, check if Expo is already running on the port
|
|
131
|
+
// (e.g. started by claudeboard run) and mark it as running
|
|
132
|
+
async function detectExistingExpo() {
|
|
133
|
+
const port = parseInt(process.env.EXPO_PORT || "8081");
|
|
134
|
+
const running = await isPortOpen(port);
|
|
135
|
+
if (running) {
|
|
136
|
+
expoStatus = "running";
|
|
137
|
+
expoUrl = `exp://localhost:${port}`;
|
|
138
|
+
broadcast("expo_log", { message: `✓ Detected existing Expo on port ${port} — ready to scan` });
|
|
139
|
+
broadcastExpoStatus();
|
|
140
|
+
console.log(` Expo already running on port ${port} — attached`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isPortOpen(port) {
|
|
145
|
+
return new Promise(resolve => {
|
|
146
|
+
const sock = createConnection({ port, host: "127.0.0.1" });
|
|
147
|
+
sock.setTimeout(600);
|
|
148
|
+
sock.once("connect", () => { sock.destroy(); resolve(true); });
|
|
149
|
+
sock.once("error", () => resolve(false));
|
|
150
|
+
sock.once("timeout", () => resolve(false));
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
129
154
|
// GET expo status
|
|
130
155
|
app.get("/api/expo/status", (req, res) => {
|
|
131
156
|
res.json({ status: expoStatus, qr: expoQR, url: expoUrl });
|
|
132
157
|
});
|
|
133
158
|
|
|
134
|
-
// POST expo/start — install
|
|
159
|
+
// POST expo/start — smart start: attach if already running, otherwise install + start
|
|
135
160
|
app.post("/api/expo/start", async (req, res) => {
|
|
136
|
-
|
|
161
|
+
const port = parseInt(process.env.EXPO_PORT || "8081");
|
|
162
|
+
|
|
163
|
+
// Already managed by this server
|
|
164
|
+
if (expoProcess) {
|
|
165
|
+
return res.json({ ok: true, message: "Already running", status: expoStatus, url: expoUrl });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Check if something is already listening on the port (e.g. started by claudeboard run)
|
|
169
|
+
const alreadyRunning = await isPortOpen(port);
|
|
170
|
+
if (alreadyRunning) {
|
|
171
|
+
expoStatus = "running";
|
|
172
|
+
expoUrl = `exp://localhost:${port}`;
|
|
173
|
+
broadcastExpoStatus();
|
|
174
|
+
broadcast("expo_log", { message: `✓ Attached to existing Expo on port ${port}` });
|
|
175
|
+
return res.json({ ok: true, message: "Attached to existing Expo", url: expoUrl });
|
|
176
|
+
}
|
|
137
177
|
|
|
138
178
|
res.json({ ok: true, message: "Starting Expo..." });
|
|
139
179
|
_startExpo(PROJECT_DIR);
|
|
@@ -153,69 +193,79 @@ app.post("/api/expo/stop", (req, res) => {
|
|
|
153
193
|
});
|
|
154
194
|
|
|
155
195
|
async function _startExpo(projectDir) {
|
|
156
|
-
// Step 1:
|
|
196
|
+
// Step 1: install deps with --legacy-peer-deps
|
|
197
|
+
// Expo projects almost always have peer dep conflicts — this is expected
|
|
157
198
|
expoStatus = "installing";
|
|
158
199
|
broadcastExpoStatus();
|
|
159
|
-
broadcast("expo_log", { message: "Installing dependencies..." });
|
|
200
|
+
broadcast("expo_log", { message: "Installing dependencies (--legacy-peer-deps)..." });
|
|
160
201
|
|
|
161
202
|
await new Promise((resolve) => {
|
|
162
|
-
const install = spawn("npm", ["install"
|
|
163
|
-
|
|
164
|
-
|
|
203
|
+
const install = spawn("npm", ["install", "--legacy-peer-deps"], {
|
|
204
|
+
cwd: projectDir,
|
|
205
|
+
stdio: "pipe",
|
|
206
|
+
env: { ...process.env },
|
|
207
|
+
});
|
|
208
|
+
install.stdout.on("data", (d) => {
|
|
209
|
+
const msg = d.toString().trim();
|
|
210
|
+
if (msg && !msg.startsWith("npm warn")) broadcast("expo_log", { message: msg });
|
|
211
|
+
});
|
|
212
|
+
install.stderr.on("data", (d) => {
|
|
213
|
+
const msg = d.toString().trim();
|
|
214
|
+
// Only show real errors, not peer dep warnings
|
|
215
|
+
if (msg && msg.includes("npm error") && !msg.includes("ERESOLVE")) {
|
|
216
|
+
broadcast("expo_log", { message: msg });
|
|
217
|
+
}
|
|
218
|
+
});
|
|
165
219
|
install.on("close", resolve);
|
|
166
220
|
});
|
|
167
221
|
|
|
168
|
-
broadcast("expo_log", { message: "Dependencies
|
|
222
|
+
broadcast("expo_log", { message: "✓ Dependencies ready. Starting Expo tunnel..." });
|
|
169
223
|
|
|
170
|
-
// Step 2: expo start
|
|
224
|
+
// Step 2: expo start --tunnel, fully non-interactive
|
|
171
225
|
expoStatus = "starting";
|
|
172
226
|
broadcastExpoStatus();
|
|
173
227
|
|
|
174
|
-
const expo = spawn("npx", ["expo", "start", "--tunnel"], {
|
|
228
|
+
const expo = spawn("npx", ["expo", "start", "--tunnel", "--non-interactive"], {
|
|
175
229
|
cwd: projectDir,
|
|
176
|
-
env: { ...process.env, CI: "false", EXPO_NO_DOTENV: "0" },
|
|
177
230
|
stdio: "pipe",
|
|
231
|
+
env: {
|
|
232
|
+
...process.env,
|
|
233
|
+
CI: "1", // Prevents "use port X instead?" prompts
|
|
234
|
+
EXPO_NO_DOTENV: "0",
|
|
235
|
+
EXPO_NO_INTERACTIVE: "1", // Belt + suspenders non-interactive
|
|
236
|
+
TERM: "dumb", // No ANSI color codes in output
|
|
237
|
+
},
|
|
178
238
|
});
|
|
179
239
|
|
|
180
240
|
expoProcess = expo;
|
|
181
241
|
|
|
182
242
|
expo.stdout.on("data", (d) => {
|
|
183
243
|
const text = d.toString();
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
// Detect QR code URL (exp:// or https://expo.dev)
|
|
187
|
-
const expUrl = text.match(/exp:\/\/[^\s]+/);
|
|
188
|
-
if (expUrl) {
|
|
189
|
-
expoUrl = expUrl[0];
|
|
190
|
-
expoStatus = "running";
|
|
191
|
-
broadcastExpoStatus();
|
|
192
|
-
}
|
|
244
|
+
const clean = text.replace(/\x1b\[[0-9;]*m/g, "").trim(); // strip ANSI codes
|
|
245
|
+
if (clean) broadcast("expo_log", { message: clean });
|
|
193
246
|
|
|
194
|
-
// Detect
|
|
195
|
-
const
|
|
196
|
-
if (
|
|
197
|
-
expoUrl = tunnel[0];
|
|
198
|
-
expoStatus = "running";
|
|
199
|
-
broadcastExpoStatus();
|
|
200
|
-
}
|
|
247
|
+
// Detect URLs
|
|
248
|
+
const expUrl = text.match(/exp:\/\/[^\s\]]+/);
|
|
249
|
+
if (expUrl) { expoUrl = expUrl[0]; expoStatus = "running"; broadcastExpoStatus(); }
|
|
201
250
|
|
|
202
|
-
|
|
203
|
-
if (
|
|
204
|
-
|
|
251
|
+
const tunnel = text.match(/https:\/\/[a-z0-9-]+\.exp\.direct[^\s\]]*/);
|
|
252
|
+
if (tunnel) { expoUrl = tunnel[0]; expoStatus = "running"; broadcastExpoStatus(); }
|
|
253
|
+
|
|
254
|
+
if (text.includes("scan") || text.includes("QR")) {
|
|
255
|
+
broadcast("expo_log", { message: "📱 QR ready — open Expo Go and scan" });
|
|
205
256
|
}
|
|
206
257
|
});
|
|
207
258
|
|
|
208
259
|
expo.stderr.on("data", (d) => {
|
|
209
|
-
const text = d.toString().trim();
|
|
260
|
+
const text = d.toString().replace(/\x1b\[[0-9;]*m/g, "").trim();
|
|
210
261
|
if (text) broadcast("expo_log", { message: text });
|
|
211
262
|
});
|
|
212
263
|
|
|
213
264
|
expo.on("close", (code) => {
|
|
214
265
|
expoProcess = null;
|
|
215
|
-
|
|
216
|
-
expoQR = null;
|
|
266
|
+
if (code !== 0 && expoStatus !== "running") expoStatus = "error";
|
|
217
267
|
broadcastExpoStatus();
|
|
218
|
-
broadcast("expo_log", { message: `Expo
|
|
268
|
+
broadcast("expo_log", { message: `Expo exited (code ${code})` });
|
|
219
269
|
});
|
|
220
270
|
}
|
|
221
271
|
|
|
@@ -312,4 +362,8 @@ async function addLog(taskId, message, type = "info") {
|
|
|
312
362
|
await supabase.from("cb_logs").insert({ project: PROJECT, task_id: taskId, message, type });
|
|
313
363
|
}
|
|
314
364
|
|
|
315
|
-
server.listen(PORT, () =>
|
|
365
|
+
server.listen(PORT, () => {
|
|
366
|
+
console.log(`READY on port ${PORT}`);
|
|
367
|
+
// Check if Expo is already running (started by claudeboard run)
|
|
368
|
+
setTimeout(detectExistingExpo, 1000);
|
|
369
|
+
});
|