claudeboard 1.1.0 → 1.5.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.
@@ -6,192 +6,310 @@ import { createClient } from "@supabase/supabase-js";
6
6
  import path from "path";
7
7
  import { fileURLToPath } from "url";
8
8
  import fs from "fs";
9
+ import { spawn } from "child_process";
10
+ import { createRequire } from "module";
9
11
 
10
12
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+ const require = createRequire(import.meta.url);
11
14
 
12
15
  const app = express();
13
16
  const server = createServer(app);
14
- const wss = new WebSocketServer({ server });
17
+
18
+ // Two WS servers: one for board events, one for terminal
19
+ const boardWss = new WebSocketServer({ noServer: true });
20
+ const termWss = new WebSocketServer({ noServer: true });
21
+
22
+ // Route upgrade requests
23
+ server.on("upgrade", (req, socket, head) => {
24
+ if (req.url === "/terminal") {
25
+ termWss.handleUpgrade(req, socket, head, (ws) => termWss.emit("connection", ws, req));
26
+ } else {
27
+ boardWss.handleUpgrade(req, socket, head, (ws) => boardWss.emit("connection", ws, req));
28
+ }
29
+ });
15
30
 
16
31
  app.use(cors());
17
32
  app.use(express.json());
18
33
 
19
- const PORT = process.env.PORT || 3131;
20
- const PROJECT = process.env.PROJECT_NAME || "default";
34
+ const PORT = process.env.PORT || 3131;
35
+ const PROJECT = process.env.PROJECT_NAME || "default";
36
+ const PROJECT_DIR = process.env.PROJECT_DIR || process.cwd();
37
+ const SUPABASE_URL = process.env.SUPABASE_URL;
38
+ const SUPABASE_KEY = process.env.SUPABASE_KEY;
21
39
 
22
- const supabase = createClient(
23
- process.env.SUPABASE_URL,
24
- process.env.SUPABASE_KEY
25
- );
40
+ const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
26
41
 
27
- // Broadcast to all WS clients
42
+ // ── STATE ─────────────────────────────────────────────────────────────────────
43
+ let expoProcess = null;
44
+ let expoStatus = "stopped"; // stopped | installing | starting | running | error
45
+ let expoQR = null;
46
+ let expoUrl = null;
47
+
48
+ // ── BOARD BROADCAST ───────────────────────────────────────────────────────────
28
49
  function broadcast(event, data) {
29
50
  const msg = JSON.stringify({ event, data, ts: Date.now() });
30
- wss.clients.forEach((client) => {
31
- if (client.readyState === 1) client.send(msg);
32
- });
51
+ boardWss.clients.forEach((c) => { if (c.readyState === 1) c.send(msg); });
33
52
  }
34
53
 
35
- // Subscribe to Supabase realtime
54
+ function broadcastExpoStatus() {
55
+ broadcast("expo_status", { status: expoStatus, qr: expoQR, url: expoUrl });
56
+ }
57
+
58
+ // ── SUPABASE REALTIME ─────────────────────────────────────────────────────────
36
59
  supabase
37
60
  .channel("cb_changes")
38
- .on("postgres_changes", { event: "*", schema: "public", table: "cb_tasks" }, (payload) => {
39
- broadcast("task_update", payload);
40
- })
41
- .on("postgres_changes", { event: "*", schema: "public", table: "cb_logs" }, (payload) => {
42
- broadcast("log", payload.new);
43
- })
61
+ .on("postgres_changes", { event: "*", schema: "public", table: "cb_tasks" }, (p) => broadcast("task_update", p))
62
+ .on("postgres_changes", { event: "*", schema: "public", table: "cb_logs" }, (p) => broadcast("log", p.new))
44
63
  .subscribe();
45
64
 
46
- // ─── API ROUTES ───────────────────────────────────────────────────────────────
65
+ // ── TERMINAL (xterm.js via WebSocket + node-pty) ──────────────────────────────
66
+ termWss.on("connection", (ws) => {
67
+ let pty = null;
68
+
69
+ try {
70
+ // Try node-pty for full PTY support
71
+ const nodePty = require("node-pty");
72
+ pty = nodePty.spawn(process.env.SHELL || "bash", [], {
73
+ name: "xterm-256color",
74
+ cols: 120,
75
+ rows: 40,
76
+ cwd: PROJECT_DIR,
77
+ env: {
78
+ ...process.env,
79
+ SUPABASE_URL,
80
+ SUPABASE_ACCESS_TOKEN: process.env.SUPABASE_ACCESS_TOKEN || "",
81
+ TERM: "xterm-256color",
82
+ },
83
+ });
84
+
85
+ pty.onData((data) => { if (ws.readyState === 1) ws.send(JSON.stringify({ type: "output", data })); });
86
+ pty.onExit(() => { if (ws.readyState === 1) ws.send(JSON.stringify({ type: "exit" })); });
87
+
88
+ ws.on("message", (raw) => {
89
+ try {
90
+ const msg = JSON.parse(raw);
91
+ if (msg.type === "input") pty.write(msg.data);
92
+ if (msg.type === "resize") pty.resize(msg.cols, msg.rows);
93
+ } catch {}
94
+ });
95
+
96
+ ws.on("close", () => { try { pty.kill(); } catch {} });
97
+
98
+ } catch {
99
+ // Fallback: simple shell without PTY (no colors but functional)
100
+ const shell = spawn(process.env.SHELL || "bash", [], {
101
+ cwd: PROJECT_DIR,
102
+ env: { ...process.env, SUPABASE_URL, TERM: "dumb" },
103
+ });
104
+
105
+ shell.stdout.on("data", (d) => ws.send(JSON.stringify({ type: "output", data: d.toString() })));
106
+ shell.stderr.on("data", (d) => ws.send(JSON.stringify({ type: "output", data: d.toString() })));
107
+ shell.on("close", () => ws.send(JSON.stringify({ type: "exit" })));
108
+
109
+ ws.on("message", (raw) => {
110
+ try {
111
+ const msg = JSON.parse(raw);
112
+ if (msg.type === "input") shell.stdin.write(msg.data);
113
+ } catch {}
114
+ });
115
+
116
+ ws.on("close", () => shell.kill());
117
+
118
+ // Send welcome message
119
+ ws.send(JSON.stringify({
120
+ type: "output",
121
+ data: `\r\n\x1b[33m[ClaudeBoard Terminal]\x1b[0m — Project: ${PROJECT_DIR}\r\n` +
122
+ `\x1b[2mTip: Run 'npx supabase ...' for Supabase CLI commands\x1b[0m\r\n\r\n`,
123
+ }));
124
+ }
125
+ });
126
+
127
+ // ── EXPO MANAGEMENT ───────────────────────────────────────────────────────────
128
+
129
+ // GET expo status
130
+ app.get("/api/expo/status", (req, res) => {
131
+ res.json({ status: expoStatus, qr: expoQR, url: expoUrl });
132
+ });
133
+
134
+ // POST expo/start — install deps + start Expo tunnel
135
+ app.post("/api/expo/start", async (req, res) => {
136
+ if (expoProcess) return res.json({ ok: false, error: "Expo already running" });
137
+
138
+ res.json({ ok: true, message: "Starting Expo..." });
139
+ _startExpo(PROJECT_DIR);
140
+ });
141
+
142
+ // POST expo/stop
143
+ app.post("/api/expo/stop", (req, res) => {
144
+ if (expoProcess) {
145
+ try { expoProcess.kill("SIGTERM"); } catch {}
146
+ expoProcess = null;
147
+ }
148
+ expoStatus = "stopped";
149
+ expoQR = null;
150
+ expoUrl = null;
151
+ broadcastExpoStatus();
152
+ res.json({ ok: true });
153
+ });
154
+
155
+ async function _startExpo(projectDir) {
156
+ // Step 1: npm install
157
+ expoStatus = "installing";
158
+ broadcastExpoStatus();
159
+ broadcast("expo_log", { message: "Installing dependencies..." });
160
+
161
+ 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() }));
165
+ install.on("close", resolve);
166
+ });
167
+
168
+ broadcast("expo_log", { message: "Dependencies installed. Starting Expo..." });
169
+
170
+ // Step 2: expo start with tunnel
171
+ expoStatus = "starting";
172
+ broadcastExpoStatus();
173
+
174
+ const expo = spawn("npx", ["expo", "start", "--tunnel"], {
175
+ cwd: projectDir,
176
+ env: { ...process.env, CI: "false", EXPO_NO_DOTENV: "0" },
177
+ stdio: "pipe",
178
+ });
179
+
180
+ expoProcess = expo;
181
+
182
+ expo.stdout.on("data", (d) => {
183
+ 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
+ }
193
+
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
+ }
201
+
202
+ // Detect QR data from expo output
203
+ if (text.includes("QR")) {
204
+ broadcast("expo_log", { message: "📱 QR code ready — scan with Expo Go" });
205
+ }
206
+ });
207
+
208
+ expo.stderr.on("data", (d) => {
209
+ const text = d.toString().trim();
210
+ if (text) broadcast("expo_log", { message: text });
211
+ });
212
+
213
+ expo.on("close", (code) => {
214
+ expoProcess = null;
215
+ expoStatus = code === 0 ? "stopped" : "error";
216
+ expoQR = null;
217
+ broadcastExpoStatus();
218
+ broadcast("expo_log", { message: `Expo process exited (code ${code})` });
219
+ });
220
+ }
221
+
222
+ // ── SUPABASE QUERY API ────────────────────────────────────────────────────────
223
+ app.post("/api/supabase/query", async (req, res) => {
224
+ const { sql } = req.body;
225
+ if (!sql) return res.status(400).json({ error: "No SQL provided" });
226
+
227
+ try {
228
+ const { data, error } = await supabase.rpc("execute_sql", { query: sql });
229
+ if (error) return res.status(400).json({ error: error.message });
230
+ res.json({ data });
231
+ } catch (err) {
232
+ res.status(500).json({ error: err.message });
233
+ }
234
+ });
235
+
236
+ // ── BOARD API ROUTES (unchanged) ──────────────────────────────────────────────
47
237
 
48
- // GET all tasks grouped by epic
49
238
  app.get("/api/board", async (req, res) => {
50
239
  const { data: epics } = await supabase
51
- .from("cb_epics")
52
- .select("*, cb_tasks(*)")
53
- .eq("project", PROJECT)
54
- .order("created_at");
55
-
240
+ .from("cb_epics").select("*, cb_tasks(*)").eq("project", PROJECT).order("created_at");
56
241
  const { data: logs } = await supabase
57
- .from("cb_logs")
58
- .select("*")
59
- .eq("project", PROJECT)
60
- .order("created_at", { ascending: false })
61
- .limit(50);
62
-
242
+ .from("cb_logs").select("*").eq("project", PROJECT)
243
+ .order("created_at", { ascending: false }).limit(50);
63
244
  res.json({ epics: epics || [], logs: logs || [], project: PROJECT });
64
245
  });
65
246
 
66
- // GET next pending task
67
247
  app.get("/api/tasks/next", async (req, res) => {
68
- const { data } = await supabase
69
- .from("cb_tasks")
70
- .select("*")
71
- .eq("project", PROJECT)
72
- .eq("status", "todo")
73
- .order("priority_order", { ascending: true })
74
- .limit(1)
75
- .single();
76
-
248
+ const { data } = await supabase.from("cb_tasks").select("*")
249
+ .eq("project", PROJECT).eq("status", "todo")
250
+ .order("priority_order", { ascending: true }).limit(1).single();
77
251
  if (!data) return res.json({ task: null, message: "All tasks complete! 🎉" });
78
252
  res.json({ task: data });
79
253
  });
80
254
 
81
- // POST start task
82
255
  app.post("/api/tasks/:id/start", async (req, res) => {
83
256
  const { id } = req.params;
84
- const { log } = req.body;
85
-
86
- await supabase
87
- .from("cb_tasks")
88
- .update({ status: "in_progress", started_at: new Date().toISOString() })
89
- .eq("id", id);
90
-
91
- if (log) await addLog(id, log, "start");
257
+ await supabase.from("cb_tasks").update({ status: "in_progress", started_at: new Date().toISOString() }).eq("id", id);
258
+ if (req.body.log) await addLog(id, req.body.log, "start");
92
259
  broadcast("task_started", { id });
93
260
  res.json({ ok: true });
94
261
  });
95
262
 
96
- // POST log progress
97
263
  app.post("/api/tasks/:id/log", async (req, res) => {
98
- const { id } = req.params;
99
- const { message } = req.body;
100
- await addLog(id, message, "progress");
264
+ await addLog(req.params.id, req.body.message, "progress");
101
265
  res.json({ ok: true });
102
266
  });
103
267
 
104
- // POST complete task
105
268
  app.post("/api/tasks/:id/complete", async (req, res) => {
106
269
  const { id } = req.params;
107
- const { log } = req.body;
108
-
109
- await supabase
110
- .from("cb_tasks")
111
- .update({ status: "done", completed_at: new Date().toISOString() })
112
- .eq("id", id);
113
-
114
- if (log) await addLog(id, log, "complete");
270
+ await supabase.from("cb_tasks").update({ status: "done", completed_at: new Date().toISOString() }).eq("id", id);
271
+ if (req.body.log) await addLog(id, req.body.log, "complete");
115
272
  broadcast("task_complete", { id });
116
273
  res.json({ ok: true });
117
274
  });
118
275
 
119
- // POST fail task
120
276
  app.post("/api/tasks/:id/fail", async (req, res) => {
121
277
  const { id } = req.params;
122
- const { log } = req.body;
123
-
124
- await supabase
125
- .from("cb_tasks")
126
- .update({ status: "error" })
127
- .eq("id", id);
128
-
129
- if (log) await addLog(id, log, "error");
278
+ await supabase.from("cb_tasks").update({ status: "error" }).eq("id", id);
279
+ if (req.body.log) await addLog(id, req.body.log, "error");
130
280
  broadcast("task_failed", { id });
131
281
  res.json({ ok: true });
132
282
  });
133
283
 
134
- // POST add new task manually (from dashboard or agent)
135
284
  app.post("/api/tasks", async (req, res) => {
136
285
  const { title, description, priority, type, epic_id } = req.body;
137
-
138
286
  const priorityOrder = { high: 1, medium: 2, low: 3 };
139
-
140
- const { data, error } = await supabase
141
- .from("cb_tasks")
142
- .insert({
143
- project: PROJECT,
144
- epic_id: epic_id || null,
145
- title,
146
- description,
147
- priority: priority || "medium",
148
- priority_order: priorityOrder[priority] || 2,
149
- type: type || "feature",
150
- status: "todo",
151
- })
152
- .select()
153
- .single();
154
-
287
+ const { data, error } = await supabase.from("cb_tasks").insert({
288
+ project: PROJECT, epic_id: epic_id || null, title, description,
289
+ priority: priority || "medium", priority_order: priorityOrder[priority] || 2,
290
+ type: type || "feature", status: "todo",
291
+ }).select().single();
155
292
  if (error) return res.status(400).json({ error: error.message });
156
293
  broadcast("task_added", data);
157
294
  res.json({ task: data });
158
295
  });
159
296
 
160
- // PATCH update task status manually
161
297
  app.patch("/api/tasks/:id", async (req, res) => {
162
- const { id } = req.params;
163
- const updates = req.body;
164
-
165
- await supabase.from("cb_tasks").update(updates).eq("id", id);
166
- broadcast("task_update", { id, ...updates });
298
+ await supabase.from("cb_tasks").update(req.body).eq("id", req.params.id);
299
+ broadcast("task_update", { id: req.params.id, ...req.body });
167
300
  res.json({ ok: true });
168
301
  });
169
302
 
170
- // GET logs for a specific task
171
303
  app.get("/api/tasks/:id/logs", async (req, res) => {
172
- const { data } = await supabase
173
- .from("cb_logs")
174
- .select("*")
175
- .eq("task_id", req.params.id)
176
- .order("created_at");
304
+ const { data } = await supabase.from("cb_logs").select("*")
305
+ .eq("task_id", req.params.id).order("created_at");
177
306
  res.json({ logs: data || [] });
178
307
  });
179
308
 
180
- // Serve dashboard HTML
181
- app.get("*", (req, res) => {
182
- res.sendFile(path.join(__dirname, "index.html"));
183
- });
309
+ app.get("*", (req, res) => res.sendFile(path.join(__dirname, "index.html")));
184
310
 
185
- // ─── HELPERS ─────────────────────────────────────────────────────────────────
186
311
  async function addLog(taskId, message, type = "info") {
187
- await supabase.from("cb_logs").insert({
188
- project: PROJECT,
189
- task_id: taskId,
190
- message,
191
- type,
192
- });
312
+ await supabase.from("cb_logs").insert({ project: PROJECT, task_id: taskId, message, type });
193
313
  }
194
314
 
195
- server.listen(PORT, () => {
196
- console.log(`READY on port ${PORT}`);
197
- });
315
+ server.listen(PORT, () => console.log(`READY on port ${PORT}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeboard",
3
- "version": "1.1.0",
3
+ "version": "1.5.0",
4
4
  "description": "AI engineering team — from PRD to working mobile app, autonomously",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,6 +22,7 @@
22
22
  "dotenv": "^16.4.5",
23
23
  "enquirer": "^2.4.1",
24
24
  "express": "^4.18.3",
25
+ "node-pty": "^1.0.0",
25
26
  "open": "^10.1.0",
26
27
  "ora": "^8.0.1",
27
28
  "puppeteer": "^22.8.0",