@cluesmith/codev 2.0.0-rc.29 → 2.0.0-rc.32
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/dashboard/dist/assets/index-CXwnJkPh.css +32 -0
- package/dashboard/dist/assets/index-D429K6qO.js +120 -0
- package/dashboard/dist/assets/index-D429K6qO.js.map +1 -0
- package/dashboard/dist/index.html +13 -0
- package/dist/agent-farm/cli.d.ts.map +1 -1
- package/dist/agent-farm/cli.js +22 -0
- package/dist/agent-farm/cli.js.map +1 -1
- package/dist/agent-farm/commands/architect.d.ts.map +1 -1
- package/dist/agent-farm/commands/architect.js +0 -2
- package/dist/agent-farm/commands/architect.js.map +1 -1
- package/dist/agent-farm/commands/attach.d.ts +13 -0
- package/dist/agent-farm/commands/attach.d.ts.map +1 -0
- package/dist/agent-farm/commands/attach.js +179 -0
- package/dist/agent-farm/commands/attach.js.map +1 -0
- package/dist/agent-farm/commands/cleanup.js +1 -1
- package/dist/agent-farm/commands/cleanup.js.map +1 -1
- package/dist/agent-farm/commands/index.d.ts +1 -0
- package/dist/agent-farm/commands/index.d.ts.map +1 -1
- package/dist/agent-farm/commands/index.js +1 -0
- package/dist/agent-farm/commands/index.js.map +1 -1
- package/dist/agent-farm/commands/shell.d.ts +1 -1
- package/dist/agent-farm/commands/shell.d.ts.map +1 -1
- package/dist/agent-farm/commands/shell.js +19 -32
- package/dist/agent-farm/commands/shell.js.map +1 -1
- package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn.js +64 -97
- package/dist/agent-farm/commands/spawn.js.map +1 -1
- package/dist/agent-farm/commands/start.d.ts.map +1 -1
- package/dist/agent-farm/commands/start.js +8 -20
- package/dist/agent-farm/commands/start.js.map +1 -1
- package/dist/agent-farm/commands/stop.d.ts.map +1 -1
- package/dist/agent-farm/commands/stop.js +79 -10
- package/dist/agent-farm/commands/stop.js.map +1 -1
- package/dist/agent-farm/db/index.d.ts.map +1 -1
- package/dist/agent-farm/db/index.js +15 -0
- package/dist/agent-farm/db/index.js.map +1 -1
- package/dist/agent-farm/db/schema.d.ts +1 -1
- package/dist/agent-farm/db/schema.d.ts.map +1 -1
- package/dist/agent-farm/db/schema.js +6 -3
- package/dist/agent-farm/db/schema.js.map +1 -1
- package/dist/agent-farm/db/types.d.ts +3 -0
- package/dist/agent-farm/db/types.d.ts.map +1 -1
- package/dist/agent-farm/db/types.js +3 -0
- package/dist/agent-farm/db/types.js.map +1 -1
- package/dist/agent-farm/servers/dashboard-server.js +241 -108
- package/dist/agent-farm/servers/dashboard-server.js.map +1 -1
- package/dist/agent-farm/servers/tower-server.js +15 -52
- package/dist/agent-farm/servers/tower-server.js.map +1 -1
- package/dist/agent-farm/state.d.ts +4 -0
- package/dist/agent-farm/state.d.ts.map +1 -1
- package/dist/agent-farm/state.js +30 -7
- package/dist/agent-farm/state.js.map +1 -1
- package/dist/agent-farm/types.d.ts +10 -0
- package/dist/agent-farm/types.d.ts.map +1 -1
- package/dist/agent-farm/utils/config.d.ts.map +1 -1
- package/dist/agent-farm/utils/config.js +1 -0
- package/dist/agent-farm/utils/config.js.map +1 -1
- package/dist/agent-farm/utils/deps.d.ts.map +1 -1
- package/dist/agent-farm/utils/deps.js +0 -16
- package/dist/agent-farm/utils/deps.js.map +1 -1
- package/dist/agent-farm/utils/shell.d.ts +8 -22
- package/dist/agent-farm/utils/shell.d.ts.map +1 -1
- package/dist/agent-farm/utils/shell.js +32 -34
- package/dist/agent-farm/utils/shell.js.map +1 -1
- package/dist/agent-farm/utils/terminal-ports.d.ts +1 -1
- package/dist/agent-farm/utils/terminal-ports.js +1 -1
- package/dist/commands/consult/index.d.ts.map +1 -1
- package/dist/commands/consult/index.js +6 -27
- package/dist/commands/consult/index.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +0 -15
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/porch/build-counter.d.ts +5 -0
- package/dist/commands/porch/build-counter.d.ts.map +1 -0
- package/dist/commands/porch/build-counter.js +5 -0
- package/dist/commands/porch/build-counter.js.map +1 -0
- package/dist/commands/porch/claude.d.ts +20 -22
- package/dist/commands/porch/claude.d.ts.map +1 -1
- package/dist/commands/porch/claude.js +92 -65
- package/dist/commands/porch/claude.js.map +1 -1
- package/dist/commands/porch/index.d.ts.map +1 -1
- package/dist/commands/porch/index.js +7 -6
- package/dist/commands/porch/index.js.map +1 -1
- package/dist/commands/porch/plan.d.ts +11 -1
- package/dist/commands/porch/plan.d.ts.map +1 -1
- package/dist/commands/porch/plan.js +33 -5
- package/dist/commands/porch/plan.js.map +1 -1
- package/dist/commands/porch/prompts.d.ts.map +1 -1
- package/dist/commands/porch/prompts.js +0 -20
- package/dist/commands/porch/prompts.js.map +1 -1
- package/dist/commands/porch/run.d.ts +20 -3
- package/dist/commands/porch/run.d.ts.map +1 -1
- package/dist/commands/porch/run.js +306 -161
- package/dist/commands/porch/run.js.map +1 -1
- package/dist/commands/porch/state.d.ts +5 -5
- package/dist/commands/porch/state.d.ts.map +1 -1
- package/dist/commands/porch/state.js +6 -49
- package/dist/commands/porch/state.js.map +1 -1
- package/dist/commands/porch/types.d.ts +3 -0
- package/dist/commands/porch/types.d.ts.map +1 -1
- package/dist/terminal/index.d.ts +8 -0
- package/dist/terminal/index.d.ts.map +1 -0
- package/dist/terminal/index.js +5 -0
- package/dist/terminal/index.js.map +1 -0
- package/dist/terminal/pty-manager.d.ts +60 -0
- package/dist/terminal/pty-manager.d.ts.map +1 -0
- package/dist/terminal/pty-manager.js +334 -0
- package/dist/terminal/pty-manager.js.map +1 -0
- package/dist/terminal/pty-session.d.ts +79 -0
- package/dist/terminal/pty-session.d.ts.map +1 -0
- package/dist/terminal/pty-session.js +215 -0
- package/dist/terminal/pty-session.js.map +1 -0
- package/dist/terminal/ring-buffer.d.ts +27 -0
- package/dist/terminal/ring-buffer.d.ts.map +1 -0
- package/dist/terminal/ring-buffer.js +74 -0
- package/dist/terminal/ring-buffer.js.map +1 -0
- package/dist/terminal/ws-protocol.d.ts +27 -0
- package/dist/terminal/ws-protocol.d.ts.map +1 -0
- package/dist/terminal/ws-protocol.js +44 -0
- package/dist/terminal/ws-protocol.js.map +1 -0
- package/package.json +11 -3
- package/skeleton/DEPENDENCIES.md +3 -29
- package/skeleton/builders.md +1 -1
- package/skeleton/protocols/spider/prompts/implement.md +10 -3
- package/templates/dashboard/js/tabs.js +1 -1
- package/templates/tower.html +3 -12
- package/dist/commands/porch/repl.d.ts +0 -33
- package/dist/commands/porch/repl.d.ts.map +0 -1
- package/dist/commands/porch/repl.js +0 -206
- package/dist/commands/porch/repl.js.map +0 -1
- package/dist/commands/porch/signals.d.ts +0 -38
- package/dist/commands/porch/signals.d.ts.map +0 -1
- package/dist/commands/porch/signals.js +0 -81
- package/dist/commands/porch/signals.js.map +0 -1
|
@@ -16,8 +16,8 @@ const execAsync = promisify(exec);
|
|
|
16
16
|
import { Command } from 'commander';
|
|
17
17
|
import { getPortForTerminal } from '../utils/terminal-ports.js';
|
|
18
18
|
import { escapeHtml, parseJsonBody, isRequestAllowed as isRequestAllowedBase, } from '../utils/server-utils.js';
|
|
19
|
-
import { loadState, getAnnotations, addAnnotation, removeAnnotation, getUtils,
|
|
20
|
-
import {
|
|
19
|
+
import { loadState, getAnnotations, addAnnotation, removeAnnotation, getUtils, addUtil, removeUtil, updateUtil, getBuilder, getBuilders, removeBuilder, upsertBuilder, clearState, getArchitect, setArchitect, } from '../state.js';
|
|
20
|
+
import { TerminalManager } from '../../terminal/pty-manager.js';
|
|
21
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
22
22
|
const __dirname = path.dirname(__filename);
|
|
23
23
|
// Default dashboard port
|
|
@@ -91,6 +91,131 @@ function findTemplatePath(filename, required = false) {
|
|
|
91
91
|
const projectRoot = findProjectRoot();
|
|
92
92
|
// Use modular dashboard template (Spec 0060)
|
|
93
93
|
const templatePath = findTemplatePath('dashboard/index.html', true);
|
|
94
|
+
// Terminal backend is always node-pty (Spec 0085)
|
|
95
|
+
const terminalBackend = 'node-pty';
|
|
96
|
+
// Load dashboard frontend preference from config (Spec 0085)
|
|
97
|
+
function loadDashboardFrontend() {
|
|
98
|
+
const configPath = path.resolve(projectRoot, 'codev', 'config.json');
|
|
99
|
+
if (fs.existsSync(configPath)) {
|
|
100
|
+
try {
|
|
101
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
102
|
+
return config?.dashboard?.frontend ?? 'react';
|
|
103
|
+
}
|
|
104
|
+
catch { /* ignore */ }
|
|
105
|
+
}
|
|
106
|
+
return 'react';
|
|
107
|
+
}
|
|
108
|
+
const dashboardFrontend = loadDashboardFrontend();
|
|
109
|
+
// React dashboard dist path (built by Vite)
|
|
110
|
+
const reactDashboardPath = path.resolve(__dirname, '../../../dashboard/dist');
|
|
111
|
+
const useReactDashboard = dashboardFrontend === 'react' && fs.existsSync(reactDashboardPath);
|
|
112
|
+
if (useReactDashboard) {
|
|
113
|
+
console.log('Dashboard frontend: React');
|
|
114
|
+
}
|
|
115
|
+
else if (dashboardFrontend === 'react') {
|
|
116
|
+
console.log('Dashboard frontend: React (dist not found, falling back to legacy)');
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
console.log('Dashboard frontend: legacy');
|
|
120
|
+
}
|
|
121
|
+
const terminalManager = new TerminalManager({ projectRoot });
|
|
122
|
+
console.log('Terminal backend: node-pty');
|
|
123
|
+
// Clear stale terminalIds on startup — TerminalManager starts empty, so any
|
|
124
|
+
// persisted terminalId from a previous run is no longer valid.
|
|
125
|
+
{
|
|
126
|
+
const arch = getArchitect();
|
|
127
|
+
if (arch?.terminalId) {
|
|
128
|
+
setArchitect({ ...arch, terminalId: undefined });
|
|
129
|
+
}
|
|
130
|
+
for (const builder of getBuilders()) {
|
|
131
|
+
if (builder.terminalId) {
|
|
132
|
+
upsertBuilder({ ...builder, terminalId: undefined });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const util of getUtils()) {
|
|
136
|
+
if (util.terminalId) {
|
|
137
|
+
updateUtil(util.id, { terminalId: undefined });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Auto-create architect PTY session if architect exists with a tmux session
|
|
142
|
+
async function initArchitectTerminal() {
|
|
143
|
+
const architect = getArchitect();
|
|
144
|
+
if (!architect || !architect.tmuxSession || architect.terminalId)
|
|
145
|
+
return;
|
|
146
|
+
try {
|
|
147
|
+
// Verify the tmux session actually exists before trying to attach.
|
|
148
|
+
// If it doesn't exist, tmux attach exits immediately, leaving a dead terminalId.
|
|
149
|
+
const { spawnSync } = await import('node:child_process');
|
|
150
|
+
const probe = spawnSync('tmux', ['has-session', '-t', architect.tmuxSession], { stdio: 'ignore' });
|
|
151
|
+
if (probe.status !== 0) {
|
|
152
|
+
console.log(`initArchitectTerminal: tmux session '${architect.tmuxSession}' does not exist yet`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// Use tmux directly (not via bash -c) to avoid DA response chaff.
|
|
156
|
+
// bash -c creates a brief window where readline echoes DA responses as text.
|
|
157
|
+
const info = await terminalManager.createSession({
|
|
158
|
+
command: 'tmux',
|
|
159
|
+
args: ['attach-session', '-t', architect.tmuxSession],
|
|
160
|
+
cwd: projectRoot,
|
|
161
|
+
cols: 200,
|
|
162
|
+
rows: 50,
|
|
163
|
+
label: 'architect',
|
|
164
|
+
});
|
|
165
|
+
// Wait to detect immediate exit (e.g., tmux session disappeared between check and attach)
|
|
166
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
167
|
+
const session = terminalManager.getSession(info.id);
|
|
168
|
+
if (!session || session.info.exitCode !== undefined) {
|
|
169
|
+
console.error(`initArchitectTerminal: PTY exited immediately (exit=${session?.info.exitCode})`);
|
|
170
|
+
terminalManager.killSession(info.id);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
setArchitect({ ...architect, terminalId: info.id });
|
|
174
|
+
console.log(`Architect terminal session created: ${info.id}`);
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
console.error('Failed to create architect terminal session:', err.message);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Poll for architect state and create PTY session once available
|
|
181
|
+
// start.ts writes architect to DB before spawning this server, but there can be a small delay
|
|
182
|
+
(async function waitForArchitectAndInit() {
|
|
183
|
+
for (let attempt = 0; attempt < 30; attempt++) {
|
|
184
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
185
|
+
try {
|
|
186
|
+
const arch = getArchitect();
|
|
187
|
+
if (!arch)
|
|
188
|
+
continue;
|
|
189
|
+
if (arch.terminalId)
|
|
190
|
+
return; // Already has terminal
|
|
191
|
+
if (!arch.tmuxSession)
|
|
192
|
+
continue; // No tmux session yet
|
|
193
|
+
console.log(`initArchitectTerminal: attempt ${attempt + 1}, tmux=${arch.tmuxSession}`);
|
|
194
|
+
await initArchitectTerminal();
|
|
195
|
+
const updated = getArchitect();
|
|
196
|
+
if (updated?.terminalId) {
|
|
197
|
+
console.log(`initArchitectTerminal: success, terminalId=${updated.terminalId}`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
console.log(`initArchitectTerminal: attempt ${attempt + 1} failed, terminalId still unset`);
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
console.error(`initArchitectTerminal: attempt ${attempt + 1} error:`, err.message);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
console.warn('initArchitectTerminal: gave up after 30 attempts');
|
|
207
|
+
})();
|
|
208
|
+
// Log telemetry
|
|
209
|
+
try {
|
|
210
|
+
const metricsPath = path.join(projectRoot, '.agent-farm', 'metrics.log');
|
|
211
|
+
fs.mkdirSync(path.dirname(metricsPath), { recursive: true });
|
|
212
|
+
fs.appendFileSync(metricsPath, JSON.stringify({
|
|
213
|
+
event: 'backend_selected',
|
|
214
|
+
backend: 'node-pty',
|
|
215
|
+
timestamp: new Date().toISOString(),
|
|
216
|
+
}) + '\n');
|
|
217
|
+
}
|
|
218
|
+
catch { /* ignore */ }
|
|
94
219
|
// Clean up dead processes from state (called on state load)
|
|
95
220
|
function cleanupDeadProcesses() {
|
|
96
221
|
// Clean up dead shell processes
|
|
@@ -217,6 +342,9 @@ async function killProcessGracefully(pid, tmuxSession) {
|
|
|
217
342
|
if (tmuxSession) {
|
|
218
343
|
killTmuxSession(tmuxSession);
|
|
219
344
|
}
|
|
345
|
+
// Guard: PID 0 sends signal to entire process group — never do that
|
|
346
|
+
if (!pid || pid <= 0)
|
|
347
|
+
return;
|
|
220
348
|
try {
|
|
221
349
|
// First try SIGTERM
|
|
222
350
|
process.kill(pid, 'SIGTERM');
|
|
@@ -281,42 +409,24 @@ function tmuxSessionExists(sessionName) {
|
|
|
281
409
|
return false;
|
|
282
410
|
}
|
|
283
411
|
}
|
|
284
|
-
// Create a
|
|
285
|
-
//
|
|
286
|
-
function
|
|
412
|
+
// Create a PTY terminal session via the TerminalManager.
|
|
413
|
+
// Returns the terminal session ID, or null on failure.
|
|
414
|
+
async function createTerminalSession(shellCommand, cwd, label) {
|
|
415
|
+
if (!terminalManager)
|
|
416
|
+
return null;
|
|
287
417
|
try {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
execSync(`tmux new-session -d -s "${sessionName}" -x 200 -y 50 "${shellCommand}"`, { cwd, stdio: 'ignore' });
|
|
292
|
-
// Hide the tmux status bar (dashboard has its own tabs)
|
|
293
|
-
execSync(`tmux set-option -t "${sessionName}" status off`, { stdio: 'ignore' });
|
|
294
|
-
// Enable mouse support in the session
|
|
295
|
-
execSync(`tmux set-option -t "${sessionName}" -g mouse on`, { stdio: 'ignore' });
|
|
296
|
-
// Enable OSC 52 clipboard (allows copy to browser clipboard via ttyd)
|
|
297
|
-
execSync(`tmux set-option -t "${sessionName}" -g set-clipboard on`, { stdio: 'ignore' });
|
|
298
|
-
// Enable passthrough for hyperlinks and clipboard
|
|
299
|
-
execSync(`tmux set-option -t "${sessionName}" -g allow-passthrough on`, { stdio: 'ignore' });
|
|
300
|
-
// Copy selection to clipboard when mouse is released
|
|
301
|
-
// Use copy-pipe-and-cancel with pbcopy to directly copy to system clipboard
|
|
302
|
-
// (OSC 52 via set-clipboard doesn't work reliably through ttyd/xterm.js)
|
|
303
|
-
execSync(`tmux bind-key -T copy-mode MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"`, { stdio: 'ignore' });
|
|
304
|
-
execSync(`tmux bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"`, { stdio: 'ignore' });
|
|
305
|
-
}
|
|
306
|
-
// Start ttyd to attach to the tmux session
|
|
307
|
-
const customIndexPath = findTemplatePath('ttyd-index.html');
|
|
308
|
-
const ttydProcess = spawnTtyd({
|
|
309
|
-
port: ttydPort,
|
|
310
|
-
sessionName,
|
|
418
|
+
const info = await terminalManager.createSession({
|
|
419
|
+
command: '/bin/bash',
|
|
420
|
+
args: ['-c', shellCommand],
|
|
311
421
|
cwd,
|
|
312
|
-
|
|
422
|
+
cols: 200,
|
|
423
|
+
rows: 50,
|
|
424
|
+
label,
|
|
313
425
|
});
|
|
314
|
-
return
|
|
426
|
+
return info.id;
|
|
315
427
|
}
|
|
316
428
|
catch (err) {
|
|
317
|
-
console.error(`Failed to create
|
|
318
|
-
// Cleanup any partial session
|
|
319
|
-
killTmuxSession(sessionName);
|
|
429
|
+
console.error(`Failed to create terminal session:`, err.message);
|
|
320
430
|
return null;
|
|
321
431
|
}
|
|
322
432
|
}
|
|
@@ -336,7 +446,7 @@ function generateShortId() {
|
|
|
336
446
|
* Spawn a worktree builder - creates git worktree and starts builder CLI
|
|
337
447
|
* Similar to shell spawning but with git worktree isolation
|
|
338
448
|
*/
|
|
339
|
-
function spawnWorktreeBuilder(builderPort, state) {
|
|
449
|
+
async function spawnWorktreeBuilder(builderPort, state) {
|
|
340
450
|
const shortId = generateShortId();
|
|
341
451
|
const builderId = `worktree-${shortId}`;
|
|
342
452
|
const branchName = `builder/worktree-${shortId}`;
|
|
@@ -364,29 +474,10 @@ function spawnWorktreeBuilder(builderPort, state) {
|
|
|
364
474
|
// Use default
|
|
365
475
|
}
|
|
366
476
|
}
|
|
367
|
-
// Create
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
execSync(`tmux set-option -t "${sessionName}" status off`, { stdio: 'ignore' });
|
|
371
|
-
// Enable mouse support
|
|
372
|
-
execSync(`tmux set-option -t "${sessionName}" -g mouse on`, { stdio: 'ignore' });
|
|
373
|
-
execSync(`tmux set-option -t "${sessionName}" -g set-clipboard on`, { stdio: 'ignore' });
|
|
374
|
-
execSync(`tmux set-option -t "${sessionName}" -g allow-passthrough on`, { stdio: 'ignore' });
|
|
375
|
-
// Copy selection to clipboard when mouse is released (pbcopy for macOS)
|
|
376
|
-
execSync(`tmux bind-key -T copy-mode MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"`, { stdio: 'ignore' });
|
|
377
|
-
execSync(`tmux bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"`, { stdio: 'ignore' });
|
|
378
|
-
// Start ttyd connecting to the tmux session
|
|
379
|
-
const customIndexPath = findTemplatePath('ttyd-index.html');
|
|
380
|
-
const ttydProcess = spawnTtyd({
|
|
381
|
-
port: builderPort,
|
|
382
|
-
sessionName,
|
|
383
|
-
cwd: worktreePath,
|
|
384
|
-
customIndexPath: customIndexPath ?? undefined,
|
|
385
|
-
});
|
|
386
|
-
const pid = ttydProcess?.pid ?? null;
|
|
387
|
-
if (!pid) {
|
|
477
|
+
// Create PTY terminal session via node-pty
|
|
478
|
+
const terminalId = await createTerminalSession(builderCommand, worktreePath, `builder-${builderId}`);
|
|
479
|
+
if (!terminalId) {
|
|
388
480
|
// Cleanup on failure
|
|
389
|
-
killTmuxSession(sessionName);
|
|
390
481
|
try {
|
|
391
482
|
execSync(`git worktree remove "${worktreePath}" --force`, { cwd: projectRoot, stdio: 'ignore' });
|
|
392
483
|
execSync(`git branch -D "${branchName}"`, { cwd: projectRoot, stdio: 'ignore' });
|
|
@@ -399,16 +490,17 @@ function spawnWorktreeBuilder(builderPort, state) {
|
|
|
399
490
|
const builder = {
|
|
400
491
|
id: builderId,
|
|
401
492
|
name: `Worktree ${shortId}`,
|
|
402
|
-
port:
|
|
403
|
-
pid,
|
|
493
|
+
port: 0,
|
|
494
|
+
pid: 0,
|
|
404
495
|
status: 'implementing',
|
|
405
496
|
phase: 'interactive',
|
|
406
497
|
worktree: worktreePath,
|
|
407
498
|
branch: branchName,
|
|
408
499
|
tmuxSession: sessionName,
|
|
409
500
|
type: 'worktree',
|
|
501
|
+
terminalId,
|
|
410
502
|
};
|
|
411
|
-
return { builder, pid };
|
|
503
|
+
return { builder, pid: 0 };
|
|
412
504
|
}
|
|
413
505
|
catch (err) {
|
|
414
506
|
console.error(`Failed to spawn worktree builder:`, err.message);
|
|
@@ -1045,6 +1137,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
1045
1137
|
}
|
|
1046
1138
|
const url = new URL(req.url || '/', `http://localhost:${port}`);
|
|
1047
1139
|
try {
|
|
1140
|
+
// Spec 0085: node-pty terminal manager REST API routes
|
|
1141
|
+
if (terminalManager && url.pathname.startsWith('/api/terminals')) {
|
|
1142
|
+
if (terminalManager.handleRequest(req, res)) {
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1048
1146
|
// API: Get state
|
|
1049
1147
|
if (req.method === 'GET' && url.pathname === '/api/state') {
|
|
1050
1148
|
const state = loadStateWithCleanup();
|
|
@@ -1154,14 +1252,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
1154
1252
|
// Find available port for builder
|
|
1155
1253
|
const builderPort = await findAvailablePort(CONFIG.builderPortStart, builderState);
|
|
1156
1254
|
// Spawn worktree builder
|
|
1157
|
-
const result = spawnWorktreeBuilder(builderPort, builderState);
|
|
1255
|
+
const result = await spawnWorktreeBuilder(builderPort, builderState);
|
|
1158
1256
|
if (!result) {
|
|
1159
1257
|
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
1160
1258
|
res.end('Failed to spawn worktree builder');
|
|
1161
1259
|
return;
|
|
1162
1260
|
}
|
|
1163
|
-
// Wait for ttyd to be ready
|
|
1164
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1165
1261
|
// Save builder to state
|
|
1166
1262
|
upsertBuilder(result.builder);
|
|
1167
1263
|
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
@@ -1294,49 +1390,25 @@ const server = http.createServer(async (req, res) => {
|
|
|
1294
1390
|
const shellCommand = command
|
|
1295
1391
|
? `${shell} -c '${command.replace(/'/g, "'\\''")}; exec ${shell}'`
|
|
1296
1392
|
: shell;
|
|
1297
|
-
//
|
|
1298
|
-
const
|
|
1299
|
-
|
|
1300
|
-
let pid = null;
|
|
1301
|
-
for (let attempt = 0; attempt < MAX_PORT_RETRIES; attempt++) {
|
|
1302
|
-
// Get fresh state on each attempt to see newly allocated ports
|
|
1303
|
-
const currentState = loadState();
|
|
1304
|
-
const candidatePort = await findAvailablePort(CONFIG.utilPortStart, currentState);
|
|
1305
|
-
// Start tmux session with ttyd attached (use cwd which may be worktree)
|
|
1306
|
-
const spawnedPid = spawnTmuxWithTtyd(sessionName, shellCommand, candidatePort, cwd);
|
|
1307
|
-
if (!spawnedPid) {
|
|
1308
|
-
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
1309
|
-
res.end('Failed to start shell');
|
|
1310
|
-
return;
|
|
1311
|
-
}
|
|
1312
|
-
// Wait for ttyd to be ready
|
|
1313
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1314
|
-
// Try to add util record - may fail if port was taken by concurrent request
|
|
1315
|
-
const util = {
|
|
1316
|
-
id,
|
|
1317
|
-
name: utilName,
|
|
1318
|
-
port: candidatePort,
|
|
1319
|
-
pid: spawnedPid,
|
|
1320
|
-
tmuxSession: sessionName,
|
|
1321
|
-
worktreePath: worktreePath, // Track for cleanup on tab close
|
|
1322
|
-
};
|
|
1323
|
-
if (tryAddUtil(util)) {
|
|
1324
|
-
// Success - port reserved
|
|
1325
|
-
utilPort = candidatePort;
|
|
1326
|
-
pid = spawnedPid;
|
|
1327
|
-
break;
|
|
1328
|
-
}
|
|
1329
|
-
// Port conflict - kill the spawned process and retry
|
|
1330
|
-
console.log(`[info] Port ${candidatePort} conflict, retrying (attempt ${attempt + 1}/${MAX_PORT_RETRIES})`);
|
|
1331
|
-
await killProcessGracefully(spawnedPid);
|
|
1332
|
-
}
|
|
1333
|
-
if (utilPort === null || pid === null) {
|
|
1393
|
+
// Create PTY terminal session via node-pty
|
|
1394
|
+
const terminalId = await createTerminalSession(shellCommand, cwd, `shell-${utilName}`);
|
|
1395
|
+
if (!terminalId) {
|
|
1334
1396
|
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
1335
|
-
res.end('Failed to
|
|
1397
|
+
res.end('Failed to create terminal session');
|
|
1336
1398
|
return;
|
|
1337
1399
|
}
|
|
1400
|
+
const util = {
|
|
1401
|
+
id,
|
|
1402
|
+
name: utilName,
|
|
1403
|
+
port: 0,
|
|
1404
|
+
pid: 0,
|
|
1405
|
+
tmuxSession: sessionName,
|
|
1406
|
+
worktreePath: worktreePath,
|
|
1407
|
+
terminalId,
|
|
1408
|
+
};
|
|
1409
|
+
addUtil(util);
|
|
1338
1410
|
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
1339
|
-
res.end(JSON.stringify({ success: true, id, port:
|
|
1411
|
+
res.end(JSON.stringify({ success: true, id, port: 0, name: utilName, terminalId }));
|
|
1340
1412
|
return;
|
|
1341
1413
|
}
|
|
1342
1414
|
// API: Check if tab process is running (Bugfix #132)
|
|
@@ -1357,8 +1429,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1357
1429
|
const util = tabUtils.find((u) => u.id === utilId);
|
|
1358
1430
|
if (util) {
|
|
1359
1431
|
found = true;
|
|
1360
|
-
// Check tmux session status
|
|
1361
|
-
// ttyd stays alive after shell exits, so checking its PID is wrong
|
|
1432
|
+
// Check tmux session status (Spec 0076)
|
|
1362
1433
|
if (util.tmuxSession) {
|
|
1363
1434
|
running = tmuxSessionExists(util.tmuxSession);
|
|
1364
1435
|
}
|
|
@@ -1374,7 +1445,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1374
1445
|
const builder = getBuilder(builderId);
|
|
1375
1446
|
if (builder) {
|
|
1376
1447
|
found = true;
|
|
1377
|
-
// Check tmux session status
|
|
1448
|
+
// Check tmux session status (Spec 0076)
|
|
1378
1449
|
if (builder.tmuxSession) {
|
|
1379
1450
|
running = tmuxSessionExists(builder.tmuxSession);
|
|
1380
1451
|
}
|
|
@@ -1425,6 +1496,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
1425
1496
|
const tabUtils = getUtils();
|
|
1426
1497
|
const util = tabUtils.find((u) => u.id === utilId);
|
|
1427
1498
|
if (util) {
|
|
1499
|
+
// Kill PTY session if present
|
|
1500
|
+
if (util.terminalId && terminalManager) {
|
|
1501
|
+
terminalManager.killSession(util.terminalId);
|
|
1502
|
+
}
|
|
1428
1503
|
await killProcessGracefully(util.pid, util.tmuxSession);
|
|
1429
1504
|
// Note: worktrees are NOT cleaned up on tab close - they may contain useful context
|
|
1430
1505
|
// Users can manually clean up with `git worktree list` and `git worktree remove`
|
|
@@ -1881,7 +1956,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1881
1956
|
return;
|
|
1882
1957
|
}
|
|
1883
1958
|
// Terminal proxy route (Spec 0062 - Secure Remote Access)
|
|
1884
|
-
// Routes /terminal/:id to the appropriate
|
|
1959
|
+
// Routes /terminal/:id to the appropriate terminal instance
|
|
1885
1960
|
const terminalMatch = url.pathname.match(/^\/terminal\/([^/]+)(\/.*)?$/);
|
|
1886
1961
|
if (terminalMatch) {
|
|
1887
1962
|
const terminalId = terminalMatch[1];
|
|
@@ -1914,8 +1989,49 @@ const server = http.createServer(async (req, res) => {
|
|
|
1914
1989
|
terminalProxy.web(req, res, { target: `http://localhost:${annotation.port}` });
|
|
1915
1990
|
return;
|
|
1916
1991
|
}
|
|
1917
|
-
// Serve dashboard
|
|
1918
|
-
if (req.method === 'GET'
|
|
1992
|
+
// Serve dashboard (Spec 0085: React or legacy based on config)
|
|
1993
|
+
if (useReactDashboard && req.method === 'GET') {
|
|
1994
|
+
// Serve React dashboard static files
|
|
1995
|
+
const filePath = url.pathname === '/' || url.pathname === '/index.html'
|
|
1996
|
+
? path.join(reactDashboardPath, 'index.html')
|
|
1997
|
+
: path.join(reactDashboardPath, url.pathname);
|
|
1998
|
+
// Security: Prevent path traversal
|
|
1999
|
+
const resolved = path.resolve(filePath);
|
|
2000
|
+
if (!resolved.startsWith(reactDashboardPath)) {
|
|
2001
|
+
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
2002
|
+
res.end('Forbidden');
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
2005
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) {
|
|
2006
|
+
const ext = path.extname(resolved);
|
|
2007
|
+
const mimeTypes = {
|
|
2008
|
+
'.html': 'text/html; charset=utf-8',
|
|
2009
|
+
'.js': 'application/javascript',
|
|
2010
|
+
'.css': 'text/css',
|
|
2011
|
+
'.json': 'application/json',
|
|
2012
|
+
'.svg': 'image/svg+xml',
|
|
2013
|
+
'.png': 'image/png',
|
|
2014
|
+
'.ico': 'image/x-icon',
|
|
2015
|
+
'.map': 'application/json',
|
|
2016
|
+
};
|
|
2017
|
+
const contentType = mimeTypes[ext] ?? 'application/octet-stream';
|
|
2018
|
+
// Cache static assets (hashed filenames) but not index.html
|
|
2019
|
+
if (ext !== '.html') {
|
|
2020
|
+
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
|
2021
|
+
}
|
|
2022
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
2023
|
+
fs.createReadStream(resolved).pipe(res);
|
|
2024
|
+
return;
|
|
2025
|
+
}
|
|
2026
|
+
// SPA fallback: serve index.html for client-side routing
|
|
2027
|
+
if (!url.pathname.startsWith('/api/') && !url.pathname.startsWith('/ws/') && !url.pathname.startsWith('/terminal/') && !url.pathname.startsWith('/annotation/')) {
|
|
2028
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
2029
|
+
fs.createReadStream(path.join(reactDashboardPath, 'index.html')).pipe(res);
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
if (!useReactDashboard && req.method === 'GET' && (url.pathname === '/' || url.pathname === '/index.html')) {
|
|
2034
|
+
// Legacy vanilla JS dashboard
|
|
1919
2035
|
try {
|
|
1920
2036
|
let template = fs.readFileSync(templatePath, 'utf-8');
|
|
1921
2037
|
const state = loadStateWithCleanup();
|
|
@@ -1944,8 +2060,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
1944
2060
|
res.end('Internal server error: ' + err.message);
|
|
1945
2061
|
}
|
|
1946
2062
|
});
|
|
2063
|
+
// Spec 0085: Attach node-pty WebSocket handler for /ws/terminal/:id routes
|
|
2064
|
+
if (terminalManager) {
|
|
2065
|
+
terminalManager.attachWebSocket(server);
|
|
2066
|
+
}
|
|
1947
2067
|
// WebSocket upgrade handler for terminal proxy (Spec 0062)
|
|
1948
|
-
//
|
|
2068
|
+
// WebSocket for bidirectional terminal communication
|
|
1949
2069
|
server.on('upgrade', (req, socket, head) => {
|
|
1950
2070
|
// Security check for non-auth mode
|
|
1951
2071
|
const host = req.headers.host;
|
|
@@ -2017,4 +2137,17 @@ else {
|
|
|
2017
2137
|
console.log(`Dashboard: http://localhost:${port}`);
|
|
2018
2138
|
});
|
|
2019
2139
|
}
|
|
2140
|
+
// Spec 0085: Graceful shutdown for node-pty terminal manager
|
|
2141
|
+
process.on('SIGTERM', () => {
|
|
2142
|
+
if (terminalManager) {
|
|
2143
|
+
terminalManager.shutdown();
|
|
2144
|
+
}
|
|
2145
|
+
process.exit(0);
|
|
2146
|
+
});
|
|
2147
|
+
process.on('SIGINT', () => {
|
|
2148
|
+
if (terminalManager) {
|
|
2149
|
+
terminalManager.shutdown();
|
|
2150
|
+
}
|
|
2151
|
+
process.exit(0);
|
|
2152
|
+
});
|
|
2020
2153
|
//# sourceMappingURL=dashboard-server.js.map
|