claudeboard 2.4.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.
@@ -0,0 +1,240 @@
1
+ import { callClaude } from "./claude-api.js";
2
+ import { runCommand } from "../tools/terminal.js";
3
+ import { startProcess, waitForPort } from "../tools/terminal.js";
4
+ import chalk from "chalk";
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import { createRequire } from "module";
8
+
9
+ const require = createRequire(import.meta.url);
10
+
11
+ const MAX_FIX_ATTEMPTS = 3;
12
+
13
+ /**
14
+ * Expo Health Check Agent
15
+ * Runs before development loop starts.
16
+ * 1. Install deps
17
+ * 2. Try to start Expo
18
+ * 3. If it crashes, read the error and fix it (up to 3 attempts)
19
+ * 4. Return { ready: true/false, process, port }
20
+ */
21
+ export async function runExpoHealthCheck(projectPath, port = 8081) {
22
+ console.log(chalk.bold.cyan("\n[ EXPO HEALTH CHECK ]\n"));
23
+
24
+ if (!fs.existsSync(path.join(projectPath, "package.json"))) {
25
+ console.log(chalk.dim(" No package.json found — skipping Expo health check"));
26
+ return { ready: false, process: null };
27
+ }
28
+
29
+ // ── Step 1: Install dependencies ──────────────────────────────────────────
30
+ console.log(chalk.dim(" Installing dependencies (--legacy-peer-deps)..."));
31
+ const installResult = await runCommand(
32
+ "npm install --legacy-peer-deps 2>&1",
33
+ projectPath,
34
+ 120000
35
+ );
36
+ const installErrors = extractNpmErrors(installResult.stdout);
37
+ if (installErrors) {
38
+ console.log(chalk.yellow(` ⚠ npm install issues detected:\n${installErrors}`));
39
+ // Try to fix install errors before continuing
40
+ const fixed = await fixInstallErrors(projectPath, installErrors);
41
+ if (!fixed) {
42
+ console.log(chalk.red(" ✗ Could not fix install errors — continuing without Expo"));
43
+ return { ready: false, process: null };
44
+ }
45
+ }
46
+ console.log(chalk.dim(" ✓ Dependencies installed"));
47
+
48
+ // ── Step 2: Attempt Expo start with error detection ───────────────────────
49
+ for (let attempt = 1; attempt <= MAX_FIX_ATTEMPTS; attempt++) {
50
+ console.log(chalk.dim(` Starting Expo (attempt ${attempt}/${MAX_FIX_ATTEMPTS})...`));
51
+
52
+ const result = await tryStartExpo(projectPath, port);
53
+
54
+ if (result.ready) {
55
+ console.log(chalk.green(` ✓ Expo running on port ${port}\n`));
56
+ return { ready: true, process: result.process };
57
+ }
58
+
59
+ const error = result.error;
60
+ console.log(chalk.yellow(` ✗ Expo failed to start:\n ${error?.slice(0, 200)}`));
61
+
62
+ if (attempt === MAX_FIX_ATTEMPTS) break;
63
+
64
+ // ── Step 3: Ask Claude to fix the error ─────────────────────────────────
65
+ console.log(chalk.dim(` Asking Claude to fix the error...`));
66
+ const fixed = await fixExpoError(projectPath, error);
67
+
68
+ if (!fixed) {
69
+ console.log(chalk.red(" Could not determine fix — skipping Expo"));
70
+ break;
71
+ }
72
+ console.log(chalk.dim(` Applied fix — retrying...`));
73
+ }
74
+
75
+ console.log(chalk.yellow(" ⚠ Expo not available — continuing without visual QA\n"));
76
+ return { ready: false, process: null };
77
+ }
78
+
79
+ // ── Try to start Expo and detect if it crashes or succeeds ─────────────────
80
+ async function tryStartExpo(projectPath, port) {
81
+ return new Promise((resolve) => {
82
+ let output = "";
83
+ let resolved = false;
84
+ let proc = null;
85
+
86
+ const done = (ready, error) => {
87
+ if (resolved) return;
88
+ resolved = true;
89
+ if (!ready && proc) {
90
+ try { proc.kill(); } catch {}
91
+ proc = null;
92
+ }
93
+ resolve({ ready, process: proc, error });
94
+ };
95
+
96
+ // Collect logs for 20 seconds, then give up if no success
97
+ const timeout = setTimeout(() => done(false, `Expo did not start within 20s. Last output:\n${output.slice(-500)}`), 20000);
98
+
99
+ try {
100
+ const { spawn } = require("child_process");
101
+ proc = spawn("npx", ["expo", "start", "--web", "--port", String(port)], {
102
+ cwd: projectPath,
103
+ env: {
104
+ ...process.env,
105
+ CI: "1",
106
+ EXPO_NO_INTERACTIVE: "1",
107
+ EXPO_NO_DOTENV: "0",
108
+ },
109
+ stdio: "pipe",
110
+ });
111
+
112
+ proc.stdout.on("data", (d) => {
113
+ const text = d.toString();
114
+ output += text;
115
+ // Success signals
116
+ if (
117
+ text.includes("Metro waiting on") ||
118
+ text.includes(`localhost:${port}`) ||
119
+ text.includes("Web is waiting") ||
120
+ text.includes("Bundling complete")
121
+ ) {
122
+ clearTimeout(timeout);
123
+ done(true, null);
124
+ }
125
+ });
126
+
127
+ proc.stderr.on("data", (d) => {
128
+ const text = d.toString();
129
+ output += text;
130
+ // Fatal crash signals
131
+ if (
132
+ text.includes("Unable to resolve module") ||
133
+ text.includes("Cannot find module") ||
134
+ text.includes("Error: Cannot") ||
135
+ text.includes("ENOENT") ||
136
+ text.includes("SyntaxError")
137
+ ) {
138
+ clearTimeout(timeout);
139
+ // Small delay to collect full error
140
+ setTimeout(() => done(false, output.slice(-1500)), 1000);
141
+ }
142
+ });
143
+
144
+ proc.on("close", (code) => {
145
+ clearTimeout(timeout);
146
+ if (code !== 0) done(false, `Process exited with code ${code}.\n${output.slice(-1000)}`);
147
+ });
148
+
149
+ } catch (e) {
150
+ clearTimeout(timeout);
151
+ done(false, e.message);
152
+ }
153
+ });
154
+ }
155
+
156
+ // ── Ask Claude to diagnose and apply the fix ──────────────────────────────
157
+ async function fixExpoError(projectPath, errorText) {
158
+ const pkgRaw = fs.readFileSync(path.join(projectPath, "package.json"), "utf8");
159
+ const pkg = JSON.parse(pkgRaw);
160
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
161
+
162
+ const prompt = `You are a React Native / Expo dependency expert.
163
+
164
+ The app failed to start with this error:
165
+ \`\`\`
166
+ ${errorText}
167
+ \`\`\`
168
+
169
+ Current package.json dependencies:
170
+ \`\`\`json
171
+ ${JSON.stringify(deps, null, 2)}
172
+ \`\`\`
173
+
174
+ Project path: ${projectPath}
175
+
176
+ Diagnose the root cause and provide the EXACT shell commands to fix it.
177
+ Common fixes:
178
+ - Version conflicts: upgrade/downgrade specific packages
179
+ - Missing modules: npm install <package>
180
+ - Peer dep issues: npm install <pkg>@<compatible-version> --legacy-peer-deps
181
+ - Wrong import paths: patch the source file
182
+
183
+ Respond with JSON:
184
+ {
185
+ "diagnosis": "One sentence explanation of the root cause",
186
+ "commands": ["npm install ...", "..."],
187
+ "confidence": 0-100
188
+ }`;
189
+
190
+ let verdict;
191
+ try {
192
+ const { callClaudeJSON } = await import("./claude-api.js");
193
+ verdict = await callClaudeJSON(
194
+ "You are a React Native dependency expert. Respond only with valid JSON.",
195
+ prompt
196
+ );
197
+ } catch {
198
+ return false;
199
+ }
200
+
201
+ if (!verdict?.commands?.length || verdict.confidence < 40) {
202
+ console.log(chalk.dim(` Diagnosis: ${verdict?.diagnosis || "unknown"} (confidence: ${verdict?.confidence}%)`));
203
+ return false;
204
+ }
205
+
206
+ console.log(chalk.dim(` Diagnosis: ${verdict.diagnosis}`));
207
+ console.log(chalk.dim(` Applying ${verdict.commands.length} fix command(s)...`));
208
+
209
+ for (const cmd of verdict.commands) {
210
+ console.log(chalk.dim(` → ${cmd}`));
211
+ const result = await runCommand(cmd + " 2>&1", projectPath, 120000);
212
+ if (result.returncode !== 0) {
213
+ const errLine = result.stdout.split("\n").find(l => l.includes("error")) || result.stdout.slice(-200);
214
+ console.log(chalk.yellow(` ⚠ Command failed: ${errLine}`));
215
+ }
216
+ }
217
+
218
+ return true;
219
+ }
220
+
221
+ // ── Fix npm install errors ──────────────────────────────────────────────────
222
+ async function fixInstallErrors(projectPath, errorText) {
223
+ // Try --force as a last resort for install errors
224
+ if (errorText.includes("ERESOLVE") || errorText.includes("peer")) {
225
+ console.log(chalk.dim(" Retrying with --force..."));
226
+ const result = await runCommand("npm install --force 2>&1", projectPath, 120000);
227
+ const stillBroken = extractNpmErrors(result.stdout);
228
+ return !stillBroken;
229
+ }
230
+ return false;
231
+ }
232
+
233
+ // ── Extract real npm errors (ignore peer dep warnings) ─────────────────────
234
+ function extractNpmErrors(output) {
235
+ const lines = output.split("\n");
236
+ const errors = lines.filter(l =>
237
+ l.includes("npm error") && !l.includes("ERESOLVE") && !l.includes("peer")
238
+ );
239
+ return errors.length ? errors.join("\n") : null;
240
+ }
@@ -5,6 +5,18 @@ 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 } from "./expo-health.js";
9
+ import { createConnection } from "net";
10
+
11
+ function isPortOpen(port) {
12
+ return new Promise(resolve => {
13
+ const sock = createConnection({ port, host: "127.0.0.1" });
14
+ sock.setTimeout(600);
15
+ sock.once("connect", () => { sock.destroy(); resolve(true); });
16
+ sock.once("error", () => resolve(false));
17
+ sock.once("timeout", () => resolve(false));
18
+ });
19
+ }
8
20
  import {
9
21
  initBoard,
10
22
  getNextTask,
@@ -84,28 +96,11 @@ export async function runOrchestrator(config) {
84
96
  const logFile = path.join(projectPath, ".claudeboard-logs.txt");
85
97
  const logStream = fs.createWriteStream(logFile, { flags: "a" });
86
98
 
87
- // Check if expo project exists
88
- const hasPackageJson = fs.existsSync(path.join(projectPath, "package.json"));
89
-
90
- if (hasPackageJson) {
91
- console.log(chalk.dim(" Starting Expo Web for QA screenshots...\n"));
92
- expoProcess = startProcess(
93
- "npx",
94
- ["expo", "start", "--web", "--port", String(expoPort)],
95
- projectPath,
96
- (log) => {
97
- logStream.write(log);
98
- if (log.includes("Metro waiting") || log.includes("localhost:" + expoPort)) {
99
- expoReady = true;
100
- }
101
- }
102
- );
103
-
104
- // Wait up to 30s for expo to start
105
- await waitForPort(expoPort, 30000);
106
- expoReady = true;
107
- console.log(chalk.dim(` Expo Web running at http://localhost:${expoPort}\n`));
108
- }
99
+ // ── EXPO HEALTH CHECK: install deps + start + auto-fix errors ─────────────
100
+ // Runs before development loop so the app is visible from task #1
101
+ const expoHealth = await runExpoHealthCheck(projectPath, expoPort);
102
+ expoReady = expoHealth.ready;
103
+ expoProcess = expoHealth.process;
109
104
 
110
105
  // ── PHASE 2: DEVELOPMENT LOOP ─────────────────────────────────────────────
111
106
  console.log(chalk.bold.cyan("[ PHASE 2: DEVELOPMENT ]\n"));
@@ -145,6 +140,17 @@ export async function runOrchestrator(config) {
145
140
 
146
141
  consecutiveFailures = 0;
147
142
 
143
+ // ── Re-check Expo health if it's supposed to be running but isn't ────────
144
+ if (expoReady) {
145
+ const portOpen = await isPortOpen(expoPort);
146
+ if (!portOpen) {
147
+ console.log(chalk.yellow(" ⚠ Expo seems to have crashed — running health check..."));
148
+ const recheck = await runExpoHealthCheck(projectPath, expoPort);
149
+ expoReady = recheck.ready;
150
+ expoProcess = recheck.process;
151
+ }
152
+ }
153
+
148
154
  // Run QA agent (only if expo is running and this is a UI/feature task)
149
155
  const shouldRunQA = expoReady && ["feature", "bug"].includes(task.type);
150
156
 
@@ -801,6 +801,31 @@
801
801
  /* adjust board to not overlap toolbar */
802
802
  .board-wrap { padding-bottom: 40px; }
803
803
 
804
+ /* ── LANGUAGE SELECTOR ── */
805
+ .lang-selector {
806
+ display: flex;
807
+ gap: 2px;
808
+ background: rgba(255,255,255,0.05);
809
+ border: 1px solid var(--border);
810
+ border-radius: 6px;
811
+ padding: 2px;
812
+ }
813
+ .lang-btn {
814
+ background: none;
815
+ border: none;
816
+ color: var(--muted);
817
+ font-family: var(--mono);
818
+ font-size: 10px;
819
+ font-weight: 700;
820
+ padding: 3px 8px;
821
+ border-radius: 4px;
822
+ cursor: pointer;
823
+ letter-spacing: 0.05em;
824
+ transition: all 0.15s;
825
+ }
826
+ .lang-btn:hover { color: var(--text); }
827
+ .lang-btn.active { background: var(--accent); color: #fff; }
828
+
804
829
  /* ── RETRY BUTTON on failed cards ── */
805
830
  .card-retry-btn {
806
831
  margin-top: 10px;
@@ -848,10 +873,10 @@
848
873
  <div class="project-badge" id="projectName">—</div>
849
874
 
850
875
  <div class="header-stats">
851
- <div class="hstat todo"><div class="hstat-dot"></div><span id="statTodo">0</span> todo</div>
852
- <div class="hstat prog"><div class="hstat-dot"></div><span id="statProg">0</span> running</div>
853
- <div class="hstat done"><div class="hstat-dot"></div><span id="statDone">0</span> done</div>
854
- <div class="hstat err"><div class="hstat-dot"></div><span id="statErr">0</span> failed</div>
876
+ <div class="hstat todo"><div class="hstat-dot"></div><span id="statTodo">0</span> <span data-i18n="todo">todo</span></div>
877
+ <div class="hstat prog"><div class="hstat-dot"></div><span id="statProg">0</span> <span data-i18n="running">running</span></div>
878
+ <div class="hstat done"><div class="hstat-dot"></div><span id="statDone">0</span> <span data-i18n="done">done</span></div>
879
+ <div class="hstat err"><div class="hstat-dot"></div><span id="statErr">0</span> <span data-i18n="failed">failed</span></div>
855
880
  </div>
856
881
 
857
882
  <div class="header-right">
@@ -861,16 +886,21 @@
861
886
  <div class="progress-pct" id="progressPct">0%</div>
862
887
  <div class="ws-badge">
863
888
  <div class="ws-dot" id="wsDot"></div>
864
- <span id="wsLabel">connecting</span>
889
+ <span id="wsLabel" data-i18n="connecting">connecting</span>
865
890
  </div>
866
- <button class="btn btn-primary" onclick="openModal()">+ Add Task</button>
891
+ <!-- Language selector -->
892
+ <div class="lang-selector">
893
+ <button class="lang-btn active" id="lang-en" onclick="setLang('en')">EN</button>
894
+ <button class="lang-btn" id="lang-es" onclick="setLang('es')">ES</button>
895
+ </div>
896
+ <button class="btn btn-primary" onclick="openModal()" data-i18n="addTask">+ Add Task</button>
867
897
  </div>
868
898
  </div>
869
899
 
870
900
  <!-- RUNNING BAR -->
871
901
  <div class="running-bar" id="runningBar">
872
902
  <div class="running-spinner"></div>
873
- <span class="running-label">Agent working →</span>
903
+ <span class="running-label" data-i18n="agentWorking">Agent working →</span>
874
904
  <span class="running-title" id="runningTitle">—</span>
875
905
  </div>
876
906
 
@@ -884,7 +914,7 @@
884
914
  <div class="column col-todo" id="col-todo">
885
915
  <div class="column-header">
886
916
  <div class="column-dot"></div>
887
- <span class="column-title">To Do</span>
917
+ <span class="column-title" data-i18n="colTodo">To Do</span>
888
918
  <span class="column-count" id="cnt-todo">0</span>
889
919
  </div>
890
920
  <div class="column-body" id="body-todo" ondragover="onDragOver(event,'todo')" ondrop="onDrop(event,'todo')" ondragleave="onDragLeave(event)"></div>
@@ -894,7 +924,7 @@
894
924
  <div class="column col-prog" id="col-prog">
895
925
  <div class="column-header">
896
926
  <div class="column-dot"></div>
897
- <span class="column-title">In Progress</span>
927
+ <span class="column-title" data-i18n="colProg">In Progress</span>
898
928
  <span class="column-count" id="cnt-prog">0</span>
899
929
  </div>
900
930
  <div class="column-body" id="body-prog" ondragover="onDragOver(event,'in_progress')" ondrop="onDrop(event,'in_progress')" ondragleave="onDragLeave(event)"></div>
@@ -904,7 +934,7 @@
904
934
  <div class="column col-done" id="col-done">
905
935
  <div class="column-header">
906
936
  <div class="column-dot"></div>
907
- <span class="column-title">Done</span>
937
+ <span class="column-title" data-i18n="colDone">Done</span>
908
938
  <span class="column-count" id="cnt-done">0</span>
909
939
  </div>
910
940
  <div class="column-body" id="body-done" ondragover="onDragOver(event,'done')" ondrop="onDrop(event,'done')" ondragleave="onDragLeave(event)"></div>
@@ -914,7 +944,7 @@
914
944
  <div class="column col-err" id="col-err">
915
945
  <div class="column-header">
916
946
  <div class="column-dot"></div>
917
- <span class="column-title">Failed</span>
947
+ <span class="column-title" data-i18n="colFailed">Failed</span>
918
948
  <span class="column-count" id="cnt-err">0</span>
919
949
  </div>
920
950
  <div class="column-body" id="body-err" ondragover="onDragOver(event,'error')" ondrop="onDrop(event,'error')" ondragleave="onDragLeave(event)"></div>
@@ -925,17 +955,17 @@
925
955
  <!-- SIDEBAR -->
926
956
  <div class="sidebar">
927
957
  <div class="sidebar-tabs">
928
- <button class="stab active" id="tab-activity" onclick="switchTab('activity')">Activity</button>
929
- <button class="stab" id="tab-detail" onclick="switchTab('detail')">Detail</button>
958
+ <button class="stab active" id="tab-activity" onclick="switchTab('activity')" data-i18n="tabActivity">Activity</button>
959
+ <button class="stab" id="tab-detail" onclick="switchTab('detail')" data-i18n="tabDetail">Detail</button>
930
960
  </div>
931
961
  <div class="sidebar-body" id="sidebarBody">
932
962
  <!-- Activity pane — always in DOM, shown/hidden -->
933
963
  <div id="activityPane">
934
- <div class="detail-empty">Waiting for activity...<br><br>Agents will log<br>their work here.</div>
964
+ <div class="detail-empty" data-i18n="activityEmpty">Waiting for activity...<br><br>Agents will log<br>their work here.</div>
935
965
  </div>
936
966
  <!-- Detail pane — always in DOM, shown/hidden -->
937
967
  <div id="detailPane" style="display:none">
938
- <div class="detail-empty">Click any task card<br>to see its details.</div>
968
+ <div class="detail-empty" data-i18n="detailEmpty">Click any task card<br>to see its details.</div>
939
969
  </div>
940
970
  </div>
941
971
  </div>
@@ -945,38 +975,38 @@
945
975
  <!-- ADD TASK MODAL -->
946
976
  <div class="overlay" id="modal" onclick="if(event.target===this)closeModal()">
947
977
  <div class="modal">
948
- <div class="modal-title">Add Task</div>
978
+ <div class="modal-title" data-i18n="addTaskTitle">Add Task</div>
949
979
  <div class="field">
950
- <label>Title</label>
951
- <input type="text" id="f-title" placeholder="Implement login screen...">
980
+ <label data-i18n="fieldTitle">Title</label>
981
+ <input type="text" id="f-title" data-i18n-placeholder="placeholderTitle">
952
982
  </div>
953
983
  <div class="field">
954
- <label>Description</label>
955
- <textarea id="f-desc" rows="3" placeholder="Detailed description of what needs to be done..."></textarea>
984
+ <label data-i18n="fieldDesc">Description</label>
985
+ <textarea id="f-desc" rows="3" data-i18n-placeholder="placeholderDesc"></textarea>
956
986
  </div>
957
987
  <div class="modal-grid">
958
988
  <div class="field">
959
- <label>Priority</label>
989
+ <label data-i18n="fieldPriority">Priority</label>
960
990
  <select id="f-priority">
961
- <option value="high">High</option>
962
- <option value="medium" selected>Medium</option>
963
- <option value="low">Low</option>
991
+ <option value="high" data-i18n="prioHigh">High</option>
992
+ <option value="medium" selected data-i18n="prioMed">Medium</option>
993
+ <option value="low" data-i18n="prioLow">Low</option>
964
994
  </select>
965
995
  </div>
966
996
  <div class="field">
967
- <label>Type</label>
997
+ <label data-i18n="fieldType">Type</label>
968
998
  <select id="f-type">
969
- <option value="feature" selected>Feature</option>
970
- <option value="bug">Bug</option>
971
- <option value="config">Config</option>
972
- <option value="refactor">Refactor</option>
973
- <option value="test">Test</option>
999
+ <option value="feature" selected data-i18n="typeFeature">Feature</option>
1000
+ <option value="bug" data-i18n="typeBug">Bug</option>
1001
+ <option value="config" data-i18n="typeConfig">Config</option>
1002
+ <option value="refactor" data-i18n="typeRefactor">Refactor</option>
1003
+ <option value="test" data-i18n="typeTest">Test</option>
974
1004
  </select>
975
1005
  </div>
976
1006
  </div>
977
1007
  <div class="modal-actions">
978
- <button class="btn-cancel" onclick="closeModal()">Cancel</button>
979
- <button class="btn-create" onclick="submitTask()">Create Task</button>
1008
+ <button class="btn-cancel" onclick="closeModal()" data-i18n="cancel">Cancel</button>
1009
+ <button class="btn-create" onclick="submitTask()" data-i18n="createTask">Create Task</button>
980
1010
  </div>
981
1011
  </div>
982
1012
  </div>
@@ -986,48 +1016,46 @@
986
1016
  <div class="modal">
987
1017
  <div class="modal-title" style="display:flex;align-items:center;gap:10px">
988
1018
  <span style="color:var(--red)">✕</span>
989
- <span>Edit & Retry Failed Task</span>
1019
+ <span data-i18n="retryTitle">Edit & Retry Failed Task</span>
990
1020
  </div>
991
-
992
- <div style="background:rgba(248,113,113,0.06);border:1px solid rgba(248,113,113,0.2);border-radius:8px;padding:10px 12px;margin-bottom:16px;font-family:var(--mono);font-size:11px;color:var(--red)" id="retryErrorLog">
1021
+ <div style="background:rgba(248,113,113,0.06);border:1px solid rgba(248,113,113,0.2);border-radius:8px;padding:10px 12px;margin-bottom:16px;font-family:var(--mono);font-size:11px;color:var(--red)" id="retryErrorLog" data-i18n="noErrorLog">
993
1022
  No error log found.
994
1023
  </div>
995
-
996
1024
  <div class="field">
997
- <label>Title</label>
1025
+ <label data-i18n="fieldTitle">Title</label>
998
1026
  <input type="text" id="r-title">
999
1027
  </div>
1000
1028
  <div class="field">
1001
- <label>Description</label>
1029
+ <label data-i18n="fieldDesc">Description</label>
1002
1030
  <textarea id="r-desc" rows="4"></textarea>
1003
1031
  </div>
1004
1032
  <div class="field">
1005
- <label style="color:var(--accent)">💬 Note for the agent (hint to fix the issue)</label>
1006
- <textarea id="r-note" rows="3" placeholder="e.g. Use tailwind v3 not v4. The error is about missing module X. Try a simpler approach without..."></textarea>
1033
+ <label style="color:var(--accent)" data-i18n="agentNoteLabel">💬 Note for the agent (hint to fix the issue)</label>
1034
+ <textarea id="r-note" rows="3" data-i18n-placeholder="agentNotePlaceholder"></textarea>
1007
1035
  </div>
1008
1036
  <div class="modal-grid">
1009
1037
  <div class="field">
1010
- <label>Priority</label>
1038
+ <label data-i18n="fieldPriority">Priority</label>
1011
1039
  <select id="r-priority">
1012
- <option value="high">High</option>
1013
- <option value="medium">Medium</option>
1014
- <option value="low">Low</option>
1040
+ <option value="high" data-i18n="prioHigh">High</option>
1041
+ <option value="medium" data-i18n="prioMed">Medium</option>
1042
+ <option value="low" data-i18n="prioLow">Low</option>
1015
1043
  </select>
1016
1044
  </div>
1017
1045
  <div class="field">
1018
- <label>Type</label>
1046
+ <label data-i18n="fieldType">Type</label>
1019
1047
  <select id="r-type">
1020
- <option value="feature">Feature</option>
1021
- <option value="bug">Bug</option>
1022
- <option value="config">Config</option>
1023
- <option value="refactor">Refactor</option>
1024
- <option value="test">Test</option>
1048
+ <option value="feature" data-i18n="typeFeature">Feature</option>
1049
+ <option value="bug" data-i18n="typeBug">Bug</option>
1050
+ <option value="config" data-i18n="typeConfig">Config</option>
1051
+ <option value="refactor" data-i18n="typeRefactor">Refactor</option>
1052
+ <option value="test" data-i18n="typeTest">Test</option>
1025
1053
  </select>
1026
1054
  </div>
1027
1055
  </div>
1028
1056
  <div class="modal-actions">
1029
- <button class="btn-cancel" onclick="closeRetry()">Cancel</button>
1030
- <button class="btn-create" style="background:var(--red)" onclick="submitRetry()">↩ Retry Task</button>
1057
+ <button class="btn-cancel" onclick="closeRetry()" data-i18n="cancel">Cancel</button>
1058
+ <button class="btn-create" style="background:var(--red)" onclick="submitRetry()" data-i18n="retryBtn">↩ Retry Task</button>
1031
1059
  </div>
1032
1060
  </div>
1033
1061
  </div>
@@ -1036,9 +1064,9 @@
1036
1064
  <div class="bottom-toolbar">
1037
1065
  <button class="toolbar-btn" id="expoBtn" onclick="toggleExpoPanel()">
1038
1066
  📱 Expo
1039
- <span class="expo-status-badge stopped" id="expoBadge">stopped</span>
1067
+ <span class="expo-status-badge stopped" id="expoBadge" data-i18n="statusStopped">stopped</span>
1040
1068
  </button>
1041
- <button class="toolbar-btn" id="termBtn" onclick="toggleTerminal()">
1069
+ <button class="toolbar-btn" id="termBtn" onclick="toggleTerminal()" data-i18n="terminal">
1042
1070
  ⌨️ Terminal
1043
1071
  </button>
1044
1072
  </div>
@@ -1047,18 +1075,18 @@
1047
1075
  <div class="expo-panel" id="expoPanel">
1048
1076
  <div class="expo-panel-header" onclick="toggleExpoPanel()">
1049
1077
  <span class="expo-panel-title">📱 Expo Go</span>
1050
- <span class="expo-status-badge stopped" id="expoPanelBadge">stopped</span>
1078
+ <span class="expo-status-badge stopped" id="expoPanelBadge" data-i18n="statusStopped">stopped</span>
1051
1079
  <div style="margin-left:auto;display:flex;gap:8px">
1052
- <button class="btn btn-primary" id="expoStartBtn" onclick="event.stopPropagation();startExpo()" style="font-size:11px;padding:4px 12px">Start Expo</button>
1053
- <button class="btn btn-ghost" id="expoStopBtn" onclick="event.stopPropagation();stopExpo()" style="font-size:11px;padding:4px 12px;display:none">Stop</button>
1080
+ <button class="btn btn-primary" id="expoStartBtn" onclick="event.stopPropagation();startExpo()" style="font-size:11px;padding:4px 12px" data-i18n="startExpo">Start Expo</button>
1081
+ <button class="btn btn-ghost" id="expoStopBtn" onclick="event.stopPropagation();stopExpo()" style="font-size:11px;padding:4px 12px;display:none" data-i18n="stopExpo">Stop</button>
1054
1082
  </div>
1055
1083
  </div>
1056
1084
  <div style="display:flex;gap:0">
1057
1085
  <div style="flex:1">
1058
- <div class="expo-logs" id="expoLogs">Expo not started. Click "Start Expo" to install dependencies and launch with tunnel.</div>
1086
+ <div class="expo-logs" id="expoLogs" data-i18n="expoIdle">Expo not started. Click "Start Expo" to install dependencies and launch with tunnel.</div>
1059
1087
  <div class="expo-qr-wrap" id="expoUrlWrap" style="display:none">
1060
1088
  <div>
1061
- <div style="font-family:var(--mono);font-size:10px;color:var(--muted);margin-bottom:4px">SCAN WITH EXPO GO</div>
1089
+ <div style="font-family:var(--mono);font-size:10px;color:var(--muted);margin-bottom:4px" data-i18n="scanWith">SCAN WITH EXPO GO</div>
1062
1090
  <div class="expo-url" id="expoUrl">—</div>
1063
1091
  </div>
1064
1092
  </div>
@@ -1122,7 +1150,7 @@ function connectWS() {
1122
1150
 
1123
1151
  function setWS(on) {
1124
1152
  document.getElementById('wsDot').className = 'ws-dot' + (on ? ' on' : '');
1125
- document.getElementById('wsLabel').textContent = on ? 'live' : 'reconnecting';
1153
+ document.getElementById('wsLabel').textContent = on ? t('live') : t('reconnecting');
1126
1154
  }
1127
1155
 
1128
1156
  // ── DATA ─────────────────────────────────────────────────────────────────────
@@ -1218,7 +1246,7 @@ function cardHTML(task) {
1218
1246
  <span class="tag ${task.type}">${task.type}</span>
1219
1247
  ${shortEpic ? `<span class="card-epic">${esc(shortEpic)}</span>` : ''}
1220
1248
  </div>
1221
- ${isError ? `<button class="card-retry-btn" onclick="event.stopPropagation();openRetry('${task.id}')">↩ Edit & Retry</button>` : ''}
1249
+ ${isError ? `<button class="card-retry-btn" onclick="event.stopPropagation();openRetry('${task.id}')">${t('retryCard')} Edit</button>` : ''}
1222
1250
  </div>`;
1223
1251
  }
1224
1252
 
@@ -1483,8 +1511,8 @@ function appendExpoLog(msg) {
1483
1511
  }
1484
1512
 
1485
1513
  function setExpoStatus(status, url) {
1486
- const labels = { stopped:'stopped', installing:'installing...', starting:'starting...', running:'running', error:'error' };
1487
- const label = labels[status] || status;
1514
+ const statusKey = 'status' + (status || 'stopped').charAt(0).toUpperCase() + (status || 'stopped').slice(1);
1515
+ const label = t(statusKey);
1488
1516
 
1489
1517
  ['expoBadge','expoPanelBadge'].forEach(id => {
1490
1518
  const el = document.getElementById(id);
@@ -1602,6 +1630,100 @@ document.addEventListener('keydown', e => {
1602
1630
  if ((e.metaKey || e.ctrlKey) && e.key === 'n') { e.preventDefault(); openModal(); }
1603
1631
  });
1604
1632
 
1633
+ // ── I18N ──────────────────────────────────────────────────────────────────────
1634
+ const STRINGS = {
1635
+ en: {
1636
+ todo:'todo', running:'running', done:'done', failed:'failed',
1637
+ connecting:'connecting', live:'live', reconnecting:'reconnecting',
1638
+ addTask:'+ Add Task',
1639
+ agentWorking:'Agent working →',
1640
+ colTodo:'To Do', colProg:'In Progress', colDone:'Done', colFailed:'Failed',
1641
+ tabActivity:'Activity', tabDetail:'Detail',
1642
+ activityEmpty:'Waiting for activity...<br><br>Agents will log<br>their work here.',
1643
+ detailEmpty:'Click any task card<br>to see its details.',
1644
+ addTaskTitle:'Add Task',
1645
+ fieldTitle:'Title', fieldDesc:'Description', fieldPriority:'Priority', fieldType:'Type',
1646
+ placeholderTitle:'Implement login screen...',
1647
+ placeholderDesc:'Detailed description of what needs to be done...',
1648
+ prioHigh:'High', prioMed:'Medium', prioLow:'Low',
1649
+ typeFeature:'Feature', typeBug:'Bug', typeConfig:'Config', typeRefactor:'Refactor', typeTest:'Test',
1650
+ cancel:'Cancel', createTask:'Create Task',
1651
+ retryTitle:'Edit & Retry Failed Task',
1652
+ noErrorLog:'No error log found.',
1653
+ agentNoteLabel:'💬 Note for the agent (hint to fix the issue)',
1654
+ agentNotePlaceholder:'e.g. Use tailwind v3 not v4. The error is about missing module X...',
1655
+ retryBtn:'↩ Retry Task',
1656
+ terminal:'⌨️ Terminal',
1657
+ statusStopped:'stopped', statusInstalling:'installing', statusStarting:'starting',
1658
+ statusRunning:'running', statusError:'error',
1659
+ startExpo:'Start Expo', stopExpo:'Stop',
1660
+ expoIdle:'Expo not started. Click "Start Expo" to install dependencies and launch with tunnel.',
1661
+ scanWith:'SCAN WITH EXPO GO',
1662
+ retryCard:'↩ Retry',
1663
+ },
1664
+ es: {
1665
+ todo:'pendiente', running:'en curso', done:'listo', failed:'fallido',
1666
+ connecting:'conectando', live:'en vivo', reconnecting:'reconectando',
1667
+ addTask:'+ Nueva tarea',
1668
+ agentWorking:'Agente trabajando →',
1669
+ colTodo:'Por hacer', colProg:'En progreso', colDone:'Hecho', colFailed:'Fallido',
1670
+ tabActivity:'Actividad', tabDetail:'Detalle',
1671
+ activityEmpty:'Esperando actividad...<br><br>Los agentes registrarán<br>su trabajo aquí.',
1672
+ detailEmpty:'Hacé clic en una tarea<br>para ver sus detalles.',
1673
+ addTaskTitle:'Nueva tarea',
1674
+ fieldTitle:'Título', fieldDesc:'Descripción', fieldPriority:'Prioridad', fieldType:'Tipo',
1675
+ placeholderTitle:'Implementar pantalla de login...',
1676
+ placeholderDesc:'Descripción detallada de lo que hay que hacer...',
1677
+ prioHigh:'Alta', prioMed:'Media', prioLow:'Baja',
1678
+ typeFeature:'Feature', typeBug:'Bug', typeConfig:'Config', typeRefactor:'Refactor', typeTest:'Test',
1679
+ cancel:'Cancelar', createTask:'Crear tarea',
1680
+ retryTitle:'Editar y reintentar tarea fallida',
1681
+ noErrorLog:'No se encontró log de error.',
1682
+ agentNoteLabel:'💬 Nota para el agente (pista para corregir el problema)',
1683
+ agentNotePlaceholder:'Ej: Usá tailwind v3 no v4. El error es por el módulo X...',
1684
+ retryBtn:'↩ Reintentar',
1685
+ terminal:'⌨️ Terminal',
1686
+ statusStopped:'detenido', statusInstalling:'instalando', statusStarting:'iniciando',
1687
+ statusRunning:'activo', statusError:'error',
1688
+ startExpo:'Iniciar Expo', stopExpo:'Detener',
1689
+ expoIdle:'Expo no iniciado. Hacé clic en "Iniciar Expo" para instalar dependencias y lanzar con tunnel.',
1690
+ scanWith:'ESCANEAR CON EXPO GO',
1691
+ retryCard:'↩ Reintentar',
1692
+ }
1693
+ };
1694
+
1695
+ let currentLang = localStorage.getItem('cb-lang') || 'en';
1696
+
1697
+ function t(key) {
1698
+ return STRINGS[currentLang][key] ?? STRINGS.en[key] ?? key;
1699
+ }
1700
+
1701
+ function setLang(lang) {
1702
+ currentLang = lang;
1703
+ localStorage.setItem('cb-lang', lang);
1704
+ document.getElementById('lang-en').classList.toggle('active', lang === 'en');
1705
+ document.getElementById('lang-es').classList.toggle('active', lang === 'es');
1706
+ applyTranslations();
1707
+ }
1708
+
1709
+ function applyTranslations() {
1710
+ document.querySelectorAll('[data-i18n]').forEach(el => {
1711
+ el.innerHTML = t(el.getAttribute('data-i18n'));
1712
+ });
1713
+ document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
1714
+ el.placeholder = t(el.getAttribute('data-i18n-placeholder'));
1715
+ });
1716
+ // Re-sync expo badge text based on current status class
1717
+ ['expoBadge','expoPanelBadge'].forEach(id => {
1718
+ const el = document.getElementById(id);
1719
+ if (!el) return;
1720
+ const status = [...el.classList].find(c =>
1721
+ ['stopped','installing','starting','running','error'].includes(c)
1722
+ ) || 'stopped';
1723
+ el.textContent = t('status' + status.charAt(0).toUpperCase() + status.slice(1));
1724
+ });
1725
+ }
1726
+
1605
1727
  // ── INIT ──────────────────────────────────────────────────────────────────────
1606
1728
  loadBoard();
1607
1729
  setInterval(loadBoard, 8000);
@@ -1609,6 +1731,13 @@ connectWS();
1609
1731
 
1610
1732
  // Load expo status
1611
1733
  fetch('/api/expo/status').then(r => r.json()).then(d => setExpoStatus(d.status, d.url));
1734
+
1735
+ // Apply saved language on load
1736
+ (function() {
1737
+ document.getElementById('lang-en').classList.toggle('active', currentLang === 'en');
1738
+ document.getElementById('lang-es').classList.toggle('active', currentLang === 'es');
1739
+ applyTranslations();
1740
+ })();
1612
1741
  </script>
1613
1742
  </body>
1614
1743
  </html>
@@ -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.4.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": {