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.
@@ -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 deps + start Expo tunnel
159
+ // POST expo/start — smart start: attach if already running, otherwise install + start
135
160
  app.post("/api/expo/start", async (req, res) => {
136
- if (expoProcess) return res.json({ ok: false, error: "Expo already running" });
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: npm install
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"], { cwd: projectDir, stdio: "pipe" });
163
- install.stdout.on("data", (d) => broadcast("expo_log", { message: d.toString().trim() }));
164
- install.stderr.on("data", (d) => broadcast("expo_log", { message: d.toString().trim() }));
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 installed. Starting Expo..." });
222
+ broadcast("expo_log", { message: "Dependencies ready. Starting Expo tunnel..." });
169
223
 
170
- // Step 2: expo start with tunnel
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
- broadcast("expo_log", { message: text.trim() });
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 tunnel URL
195
- const tunnel = text.match(/https:\/\/[a-z0-9-]+\.exp\.direct[^\s]*/);
196
- if (tunnel) {
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
- // Detect QR data from expo output
203
- if (text.includes("QR")) {
204
- broadcast("expo_log", { message: "📱 QR code ready — scan with Expo Go" });
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
- expoStatus = code === 0 ? "stopped" : "error";
216
- expoQR = null;
266
+ if (code !== 0 && expoStatus !== "running") expoStatus = "error";
217
267
  broadcastExpoStatus();
218
- broadcast("expo_log", { message: `Expo process exited (code ${code})` });
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, () => console.log(`READY on port ${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
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeboard",
3
- "version": "2.3.0",
3
+ "version": "2.8.0",
4
4
  "description": "AI engineering team — from PRD to working mobile app, autonomously",
5
5
  "type": "module",
6
6
  "bin": {