@cluesmith/codev 2.0.0-rc.3 → 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/bin/porch.js +6 -35
- 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 +74 -64
- 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.d.ts.map +1 -1
- package/dist/agent-farm/commands/cleanup.js +30 -3
- package/dist/agent-farm/commands/cleanup.js.map +1 -1
- package/dist/agent-farm/commands/consult.js +1 -1
- package/dist/agent-farm/commands/consult.js.map +1 -1
- package/dist/agent-farm/commands/index.d.ts +2 -2
- package/dist/agent-farm/commands/index.d.ts.map +1 -1
- package/dist/agent-farm/commands/index.js +2 -2
- package/dist/agent-farm/commands/index.js.map +1 -1
- package/dist/agent-farm/commands/{util.d.ts → shell.d.ts} +5 -5
- package/dist/agent-farm/commands/shell.d.ts.map +1 -0
- package/dist/agent-farm/commands/{util.js → shell.js} +23 -36
- package/dist/agent-farm/commands/shell.js.map +1 -0
- package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn.js +455 -217
- 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 +21 -74
- 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/commands/tower.d.ts.map +1 -1
- package/dist/agent-farm/commands/tower.js +2 -1
- package/dist/agent-farm/commands/tower.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/hq-connector.d.ts +2 -2
- package/dist/agent-farm/hq-connector.js +2 -2
- package/dist/agent-farm/servers/dashboard-server.js +408 -127
- package/dist/agent-farm/servers/dashboard-server.js.map +1 -1
- package/dist/agent-farm/servers/tower-server.js +353 -16
- package/dist/agent-farm/servers/tower-server.js.map +1 -1
- package/dist/agent-farm/state.d.ts +4 -10
- package/dist/agent-farm/state.d.ts.map +1 -1
- package/dist/agent-farm/state.js +30 -31
- package/dist/agent-farm/state.js.map +1 -1
- package/dist/agent-farm/types.d.ts +48 -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/notifications.d.ts +30 -0
- package/dist/agent-farm/utils/notifications.d.ts.map +1 -0
- package/dist/agent-farm/utils/notifications.js +121 -0
- package/dist/agent-farm/utils/notifications.js.map +1 -0
- package/dist/agent-farm/utils/server-utils.d.ts +2 -1
- package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
- package/dist/agent-farm/utils/server-utils.js +11 -1
- package/dist/agent-farm/utils/server-utils.js.map +1 -1
- package/dist/agent-farm/utils/shell.d.ts +9 -22
- package/dist/agent-farm/utils/shell.d.ts.map +1 -1
- package/dist/agent-farm/utils/shell.js +34 -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/cli.d.ts.map +1 -1
- package/dist/cli.js +5 -54
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt.d.ts.map +1 -1
- package/dist/commands/adopt.js +20 -4
- package/dist/commands/adopt.js.map +1 -1
- package/dist/commands/consult/index.d.ts.map +1 -1
- package/dist/commands/consult/index.js +63 -3
- 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/init.d.ts.map +1 -1
- package/dist/commands/init.js +18 -2
- package/dist/commands/init.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/checks.d.ts +16 -29
- package/dist/commands/porch/checks.d.ts.map +1 -1
- package/dist/commands/porch/checks.js +90 -144
- package/dist/commands/porch/checks.js.map +1 -1
- package/dist/commands/porch/claude.d.ts +27 -0
- package/dist/commands/porch/claude.d.ts.map +1 -0
- package/dist/commands/porch/claude.js +107 -0
- package/dist/commands/porch/claude.js.map +1 -0
- package/dist/commands/porch/index.d.ts +21 -43
- package/dist/commands/porch/index.d.ts.map +1 -1
- package/dist/commands/porch/index.js +469 -753
- package/dist/commands/porch/index.js.map +1 -1
- package/dist/commands/porch/plan.d.ts +70 -0
- package/dist/commands/porch/plan.d.ts.map +1 -0
- package/dist/commands/porch/plan.js +190 -0
- package/dist/commands/porch/plan.js.map +1 -0
- package/dist/commands/porch/prompts.d.ts +19 -0
- package/dist/commands/porch/prompts.d.ts.map +1 -0
- package/dist/commands/porch/prompts.js +250 -0
- package/dist/commands/porch/prompts.js.map +1 -0
- package/dist/commands/porch/protocol.d.ts +59 -0
- package/dist/commands/porch/protocol.d.ts.map +1 -0
- package/dist/commands/porch/protocol.js +260 -0
- package/dist/commands/porch/protocol.js.map +1 -0
- package/dist/commands/porch/run.d.ts +40 -0
- package/dist/commands/porch/run.d.ts.map +1 -0
- package/dist/commands/porch/run.js +893 -0
- package/dist/commands/porch/run.js.map +1 -0
- package/dist/commands/porch/state.d.ts +23 -112
- package/dist/commands/porch/state.d.ts.map +1 -1
- package/dist/commands/porch/state.js +81 -685
- package/dist/commands/porch/state.js.map +1 -1
- package/dist/commands/porch/types.d.ts +72 -173
- package/dist/commands/porch/types.d.ts.map +1 -1
- package/dist/commands/porch/types.js +2 -1
- package/dist/commands/porch/types.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +12 -0
- package/dist/commands/update.js.map +1 -1
- package/dist/lib/scaffold.d.ts +24 -0
- package/dist/lib/scaffold.d.ts.map +1 -1
- package/dist/lib/scaffold.js +78 -0
- package/dist/lib/scaffold.js.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 +18 -3
- package/skeleton/DEPENDENCIES.md +3 -29
- package/skeleton/builders.md +1 -1
- package/skeleton/protocol-schema.json +282 -0
- package/skeleton/protocols/bugfix/builder-prompt.md +49 -0
- package/skeleton/protocols/bugfix/protocol.json +14 -2
- package/skeleton/protocols/experiment/builder-prompt.md +47 -0
- package/skeleton/protocols/experiment/protocol.json +101 -0
- package/skeleton/protocols/maintain/builder-prompt.md +41 -0
- package/skeleton/protocols/maintain/protocol.json +114 -0
- package/skeleton/protocols/protocol-schema.json +53 -0
- package/skeleton/protocols/spider/builder-prompt.md +53 -0
- package/skeleton/protocols/spider/prompts/implement.md +208 -0
- package/skeleton/protocols/spider/prompts/plan.md +214 -0
- package/skeleton/protocols/spider/prompts/review.md +217 -0
- package/skeleton/protocols/spider/prompts/specify.md +192 -0
- package/skeleton/protocols/spider/protocol.json +96 -148
- package/skeleton/protocols/spider/protocol.md +26 -16
- package/skeleton/protocols/spider/templates/plan.md +14 -0
- package/skeleton/protocols/tick/builder-prompt.md +51 -0
- package/skeleton/protocols/tick/protocol.json +7 -2
- package/skeleton/resources/commands/agent-farm.md +23 -41
- package/skeleton/resources/commands/overview.md +5 -5
- package/skeleton/resources/workflow-reference.md +2 -2
- package/skeleton/roles/architect.md +152 -315
- package/skeleton/roles/builder.md +109 -218
- package/skeleton/templates/cheatsheet.md +4 -2
- package/templates/dashboard/index.html +17 -43
- package/templates/dashboard/js/dialogs.js +7 -7
- package/templates/dashboard/js/files.js +2 -2
- package/templates/dashboard/js/main.js +3 -3
- package/templates/dashboard/js/projects.js +3 -3
- package/templates/dashboard/js/tabs.js +1 -1
- package/templates/dashboard/js/utils.js +22 -87
- package/templates/tower.html +474 -17
- package/dist/agent-farm/commands/kickoff.d.ts +0 -19
- package/dist/agent-farm/commands/kickoff.d.ts.map +0 -1
- package/dist/agent-farm/commands/kickoff.js +0 -323
- package/dist/agent-farm/commands/kickoff.js.map +0 -1
- package/dist/agent-farm/commands/rename.d.ts +0 -13
- package/dist/agent-farm/commands/rename.d.ts.map +0 -1
- package/dist/agent-farm/commands/rename.js +0 -33
- package/dist/agent-farm/commands/rename.js.map +0 -1
- package/dist/agent-farm/commands/tutorial.d.ts +0 -10
- package/dist/agent-farm/commands/tutorial.d.ts.map +0 -1
- package/dist/agent-farm/commands/tutorial.js +0 -49
- package/dist/agent-farm/commands/tutorial.js.map +0 -1
- package/dist/agent-farm/commands/util.d.ts.map +0 -1
- package/dist/agent-farm/commands/util.js.map +0 -1
- package/dist/agent-farm/tutorial/index.d.ts +0 -8
- package/dist/agent-farm/tutorial/index.d.ts.map +0 -1
- package/dist/agent-farm/tutorial/index.js +0 -8
- package/dist/agent-farm/tutorial/index.js.map +0 -1
- package/dist/agent-farm/tutorial/prompts.d.ts +0 -57
- package/dist/agent-farm/tutorial/prompts.d.ts.map +0 -1
- package/dist/agent-farm/tutorial/prompts.js +0 -147
- package/dist/agent-farm/tutorial/prompts.js.map +0 -1
- package/dist/agent-farm/tutorial/runner.d.ts +0 -52
- package/dist/agent-farm/tutorial/runner.d.ts.map +0 -1
- package/dist/agent-farm/tutorial/runner.js +0 -204
- package/dist/agent-farm/tutorial/runner.js.map +0 -1
- package/dist/agent-farm/tutorial/state.d.ts +0 -26
- package/dist/agent-farm/tutorial/state.d.ts.map +0 -1
- package/dist/agent-farm/tutorial/state.js +0 -89
- package/dist/agent-farm/tutorial/state.js.map +0 -1
- package/dist/agent-farm/tutorial/steps/first-spec.d.ts +0 -7
- package/dist/agent-farm/tutorial/steps/first-spec.d.ts.map +0 -1
- package/dist/agent-farm/tutorial/steps/first-spec.js +0 -136
- package/dist/agent-farm/tutorial/steps/first-spec.js.map +0 -1
- package/dist/agent-farm/tutorial/steps/implementation.d.ts +0 -7
- package/dist/agent-farm/tutorial/steps/implementation.d.ts.map +0 -1
- package/dist/agent-farm/tutorial/steps/implementation.js +0 -76
- package/dist/agent-farm/tutorial/steps/implementation.js.map +0 -1
- package/dist/agent-farm/tutorial/steps/index.d.ts +0 -10
- package/dist/agent-farm/tutorial/steps/index.d.ts.map +0 -1
- package/dist/agent-farm/tutorial/steps/index.js +0 -10
- package/dist/agent-farm/tutorial/steps/index.js.map +0 -1
- package/dist/agent-farm/tutorial/steps/planning.d.ts +0 -7
- package/dist/agent-farm/tutorial/steps/planning.d.ts.map +0 -1
- package/dist/agent-farm/tutorial/steps/planning.js +0 -143
- package/dist/agent-farm/tutorial/steps/planning.js.map +0 -1
- package/dist/agent-farm/tutorial/steps/review.d.ts +0 -7
- package/dist/agent-farm/tutorial/steps/review.d.ts.map +0 -1
- package/dist/agent-farm/tutorial/steps/review.js +0 -78
- package/dist/agent-farm/tutorial/steps/review.js.map +0 -1
- package/dist/agent-farm/tutorial/steps/setup.d.ts +0 -7
- package/dist/agent-farm/tutorial/steps/setup.d.ts.map +0 -1
- package/dist/agent-farm/tutorial/steps/setup.js +0 -126
- package/dist/agent-farm/tutorial/steps/setup.js.map +0 -1
- package/dist/agent-farm/tutorial/steps/welcome.d.ts +0 -7
- package/dist/agent-farm/tutorial/steps/welcome.d.ts.map +0 -1
- package/dist/agent-farm/tutorial/steps/welcome.js +0 -50
- package/dist/agent-farm/tutorial/steps/welcome.js.map +0 -1
- package/dist/commands/pcheck/cache.d.ts +0 -48
- package/dist/commands/pcheck/cache.d.ts.map +0 -1
- package/dist/commands/pcheck/cache.js +0 -170
- package/dist/commands/pcheck/cache.js.map +0 -1
- package/dist/commands/pcheck/evaluator.d.ts +0 -15
- package/dist/commands/pcheck/evaluator.d.ts.map +0 -1
- package/dist/commands/pcheck/evaluator.js +0 -246
- package/dist/commands/pcheck/evaluator.js.map +0 -1
- package/dist/commands/pcheck/index.d.ts +0 -12
- package/dist/commands/pcheck/index.d.ts.map +0 -1
- package/dist/commands/pcheck/index.js +0 -249
- package/dist/commands/pcheck/index.js.map +0 -1
- package/dist/commands/pcheck/parser.d.ts +0 -39
- package/dist/commands/pcheck/parser.d.ts.map +0 -1
- package/dist/commands/pcheck/parser.js +0 -155
- package/dist/commands/pcheck/parser.js.map +0 -1
- package/dist/commands/pcheck/types.d.ts +0 -82
- package/dist/commands/pcheck/types.d.ts.map +0 -1
- package/dist/commands/pcheck/types.js +0 -5
- package/dist/commands/pcheck/types.js.map +0 -1
- package/dist/commands/porch/consultation.d.ts +0 -56
- package/dist/commands/porch/consultation.d.ts.map +0 -1
- package/dist/commands/porch/consultation.js +0 -330
- package/dist/commands/porch/consultation.js.map +0 -1
- package/dist/commands/porch/notifications.d.ts +0 -99
- package/dist/commands/porch/notifications.d.ts.map +0 -1
- package/dist/commands/porch/notifications.js +0 -223
- package/dist/commands/porch/notifications.js.map +0 -1
- package/dist/commands/porch/plan-parser.d.ts +0 -38
- package/dist/commands/porch/plan-parser.d.ts.map +0 -1
- package/dist/commands/porch/plan-parser.js +0 -166
- package/dist/commands/porch/plan-parser.js.map +0 -1
- package/dist/commands/porch/protocol-loader.d.ts +0 -46
- package/dist/commands/porch/protocol-loader.d.ts.map +0 -1
- package/dist/commands/porch/protocol-loader.js +0 -249
- package/dist/commands/porch/protocol-loader.js.map +0 -1
- package/dist/commands/porch/signal-parser.d.ts +0 -88
- package/dist/commands/porch/signal-parser.d.ts.map +0 -1
- package/dist/commands/porch/signal-parser.js +0 -148
- package/dist/commands/porch/signal-parser.js.map +0 -1
- package/dist/commands/tower.d.ts +0 -16
- package/dist/commands/tower.d.ts.map +0 -1
- package/dist/commands/tower.js +0 -21
- package/dist/commands/tower.js.map +0 -1
- package/skeleton/porch/protocols/bugfix.json +0 -85
- package/skeleton/porch/protocols/spider.json +0 -135
- package/skeleton/porch/protocols/tick.json +0 -76
- package/templates/dashboard/css/activity.css +0 -151
- package/templates/dashboard/js/activity.js +0 -112
|
@@ -10,14 +10,14 @@ import net from 'node:net';
|
|
|
10
10
|
import httpProxy from 'http-proxy';
|
|
11
11
|
import { spawn, execSync, exec } from 'node:child_process';
|
|
12
12
|
import { promisify } from 'node:util';
|
|
13
|
-
import { randomUUID } from 'node:crypto';
|
|
13
|
+
import { randomUUID, timingSafeEqual } from 'node:crypto';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
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);
|
|
@@ -888,6 +980,115 @@ function isRequestAllowed(req) {
|
|
|
888
980
|
}
|
|
889
981
|
return isRequestAllowedBase(req);
|
|
890
982
|
}
|
|
983
|
+
/**
|
|
984
|
+
* Timing-safe token comparison to prevent timing attacks
|
|
985
|
+
*/
|
|
986
|
+
function isValidToken(provided, expected) {
|
|
987
|
+
if (!provided)
|
|
988
|
+
return false;
|
|
989
|
+
// Ensure both strings are same length for timing-safe comparison
|
|
990
|
+
const providedBuf = Buffer.from(provided);
|
|
991
|
+
const expectedBuf = Buffer.from(expected);
|
|
992
|
+
if (providedBuf.length !== expectedBuf.length) {
|
|
993
|
+
// Still do a comparison to maintain constant time
|
|
994
|
+
timingSafeEqual(expectedBuf, expectedBuf);
|
|
995
|
+
return false;
|
|
996
|
+
}
|
|
997
|
+
return timingSafeEqual(providedBuf, expectedBuf);
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Generate HTML for login page
|
|
1001
|
+
*/
|
|
1002
|
+
function getLoginPageHtml() {
|
|
1003
|
+
return `<!DOCTYPE html>
|
|
1004
|
+
<html>
|
|
1005
|
+
<head>
|
|
1006
|
+
<title>Dashboard Login</title>
|
|
1007
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1008
|
+
<style>
|
|
1009
|
+
body { font-family: system-ui; background: #1a1a2e; color: #eee;
|
|
1010
|
+
display: flex; justify-content: center; align-items: center;
|
|
1011
|
+
min-height: 100vh; margin: 0; }
|
|
1012
|
+
.login { background: #16213e; padding: 2rem; border-radius: 8px;
|
|
1013
|
+
max-width: 400px; width: 90%; }
|
|
1014
|
+
h1 { margin-top: 0; }
|
|
1015
|
+
input { width: 100%; padding: 0.75rem; margin: 0.5rem 0;
|
|
1016
|
+
border: 1px solid #444; border-radius: 4px;
|
|
1017
|
+
background: #0f0f23; color: #eee; font-size: 1rem;
|
|
1018
|
+
box-sizing: border-box; }
|
|
1019
|
+
button { width: 100%; padding: 0.75rem; margin-top: 1rem;
|
|
1020
|
+
background: #4a7c59; color: white; border: none;
|
|
1021
|
+
border-radius: 4px; font-size: 1rem; cursor: pointer; }
|
|
1022
|
+
button:hover { background: #5a9c69; }
|
|
1023
|
+
.error { color: #ff6b6b; margin-top: 0.5rem; display: none; }
|
|
1024
|
+
</style>
|
|
1025
|
+
</head>
|
|
1026
|
+
<body>
|
|
1027
|
+
<div class="login">
|
|
1028
|
+
<h1>Agent Farm Login</h1>
|
|
1029
|
+
<p>Enter your API key to access the dashboard.</p>
|
|
1030
|
+
<input type="password" id="key" placeholder="API Key" autofocus>
|
|
1031
|
+
<div class="error" id="error">Invalid API key</div>
|
|
1032
|
+
<button onclick="login()">Login</button>
|
|
1033
|
+
</div>
|
|
1034
|
+
<script>
|
|
1035
|
+
// Check for key in URL (from QR code scan) or localStorage
|
|
1036
|
+
(async function() {
|
|
1037
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
1038
|
+
const keyFromUrl = urlParams.get('key');
|
|
1039
|
+
const keyFromStorage = localStorage.getItem('codev_web_key');
|
|
1040
|
+
const key = keyFromUrl || keyFromStorage;
|
|
1041
|
+
|
|
1042
|
+
if (key) {
|
|
1043
|
+
if (keyFromUrl) {
|
|
1044
|
+
localStorage.setItem('codev_web_key', keyFromUrl);
|
|
1045
|
+
}
|
|
1046
|
+
await verifyAndLoadDashboard(key);
|
|
1047
|
+
}
|
|
1048
|
+
})();
|
|
1049
|
+
|
|
1050
|
+
async function verifyAndLoadDashboard(key) {
|
|
1051
|
+
try {
|
|
1052
|
+
// Fetch the actual dashboard with auth header
|
|
1053
|
+
const res = await fetch(window.location.pathname, {
|
|
1054
|
+
headers: {
|
|
1055
|
+
'Authorization': 'Bearer ' + key,
|
|
1056
|
+
'Accept': 'text/html'
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
if (res.ok) {
|
|
1060
|
+
// Replace entire page with dashboard
|
|
1061
|
+
const html = await res.text();
|
|
1062
|
+
document.open();
|
|
1063
|
+
document.write(html);
|
|
1064
|
+
document.close();
|
|
1065
|
+
// Clean URL without reload
|
|
1066
|
+
history.replaceState({}, '', window.location.pathname);
|
|
1067
|
+
} else {
|
|
1068
|
+
// Key invalid
|
|
1069
|
+
localStorage.removeItem('codev_web_key');
|
|
1070
|
+
document.getElementById('error').style.display = 'block';
|
|
1071
|
+
document.getElementById('error').textContent = 'Invalid API key';
|
|
1072
|
+
}
|
|
1073
|
+
} catch (e) {
|
|
1074
|
+
document.getElementById('error').style.display = 'block';
|
|
1075
|
+
document.getElementById('error').textContent = 'Connection error';
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
async function login() {
|
|
1080
|
+
const key = document.getElementById('key').value;
|
|
1081
|
+
if (!key) return;
|
|
1082
|
+
localStorage.setItem('codev_web_key', key);
|
|
1083
|
+
await verifyAndLoadDashboard(key);
|
|
1084
|
+
}
|
|
1085
|
+
document.getElementById('key').addEventListener('keypress', (e) => {
|
|
1086
|
+
if (e.key === 'Enter') login();
|
|
1087
|
+
});
|
|
1088
|
+
</script>
|
|
1089
|
+
</body>
|
|
1090
|
+
</html>`;
|
|
1091
|
+
}
|
|
891
1092
|
// Create server
|
|
892
1093
|
const server = http.createServer(async (req, res) => {
|
|
893
1094
|
// Security: Validate Host and Origin headers
|
|
@@ -896,17 +1097,37 @@ const server = http.createServer(async (req, res) => {
|
|
|
896
1097
|
res.end('Forbidden');
|
|
897
1098
|
return;
|
|
898
1099
|
}
|
|
1100
|
+
// CRITICAL: When CODEV_WEB_KEY is set, ALL requests require auth
|
|
1101
|
+
// NO localhost bypass - tunnel daemons (cloudflared) run locally and proxy
|
|
1102
|
+
// to localhost, so checking remoteAddress would incorrectly trust remote traffic
|
|
1103
|
+
const webKey = process.env.CODEV_WEB_KEY;
|
|
1104
|
+
if (webKey) {
|
|
1105
|
+
const authHeader = req.headers.authorization;
|
|
1106
|
+
const token = authHeader?.replace('Bearer ', '');
|
|
1107
|
+
if (!isValidToken(token, webKey)) {
|
|
1108
|
+
// Return login page for HTML requests, 401 for API
|
|
1109
|
+
if (req.headers.accept?.includes('text/html')) {
|
|
1110
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1111
|
+
res.end(getLoginPageHtml());
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
1115
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
// When CODEV_WEB_KEY is NOT set: no auth required (local dev mode only)
|
|
899
1120
|
// CORS headers
|
|
900
1121
|
const origin = req.headers.origin;
|
|
901
|
-
if (insecureRemoteMode) {
|
|
902
|
-
// Allow any origin in insecure remote mode
|
|
1122
|
+
if (insecureRemoteMode || webKey) {
|
|
1123
|
+
// Allow any origin in insecure remote mode or when using auth (tunnel access)
|
|
903
1124
|
res.setHeader('Access-Control-Allow-Origin', origin || '*');
|
|
904
1125
|
}
|
|
905
1126
|
else if (origin && (origin.startsWith('http://localhost:') || origin.startsWith('http://127.0.0.1:'))) {
|
|
906
1127
|
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
907
1128
|
}
|
|
908
1129
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
909
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
1130
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
910
1131
|
// Prevent caching of API responses
|
|
911
1132
|
res.setHeader('Cache-Control', 'no-store');
|
|
912
1133
|
if (req.method === 'OPTIONS') {
|
|
@@ -916,6 +1137,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
916
1137
|
}
|
|
917
1138
|
const url = new URL(req.url || '/', `http://localhost:${port}`);
|
|
918
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
|
+
}
|
|
919
1146
|
// API: Get state
|
|
920
1147
|
if (req.method === 'GET' && url.pathname === '/api/state') {
|
|
921
1148
|
const state = loadStateWithCleanup();
|
|
@@ -1025,14 +1252,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
1025
1252
|
// Find available port for builder
|
|
1026
1253
|
const builderPort = await findAvailablePort(CONFIG.builderPortStart, builderState);
|
|
1027
1254
|
// Spawn worktree builder
|
|
1028
|
-
const result = spawnWorktreeBuilder(builderPort, builderState);
|
|
1255
|
+
const result = await spawnWorktreeBuilder(builderPort, builderState);
|
|
1029
1256
|
if (!result) {
|
|
1030
1257
|
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
1031
1258
|
res.end('Failed to spawn worktree builder');
|
|
1032
1259
|
return;
|
|
1033
1260
|
}
|
|
1034
|
-
// Wait for ttyd to be ready
|
|
1035
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1036
1261
|
// Save builder to state
|
|
1037
1262
|
upsertBuilder(result.builder);
|
|
1038
1263
|
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
@@ -1165,49 +1390,25 @@ const server = http.createServer(async (req, res) => {
|
|
|
1165
1390
|
const shellCommand = command
|
|
1166
1391
|
? `${shell} -c '${command.replace(/'/g, "'\\''")}; exec ${shell}'`
|
|
1167
1392
|
: shell;
|
|
1168
|
-
//
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1171
|
-
let pid = null;
|
|
1172
|
-
for (let attempt = 0; attempt < MAX_PORT_RETRIES; attempt++) {
|
|
1173
|
-
// Get fresh state on each attempt to see newly allocated ports
|
|
1174
|
-
const currentState = loadState();
|
|
1175
|
-
const candidatePort = await findAvailablePort(CONFIG.utilPortStart, currentState);
|
|
1176
|
-
// Start tmux session with ttyd attached (use cwd which may be worktree)
|
|
1177
|
-
const spawnedPid = spawnTmuxWithTtyd(sessionName, shellCommand, candidatePort, cwd);
|
|
1178
|
-
if (!spawnedPid) {
|
|
1179
|
-
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
1180
|
-
res.end('Failed to start shell');
|
|
1181
|
-
return;
|
|
1182
|
-
}
|
|
1183
|
-
// Wait for ttyd to be ready
|
|
1184
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1185
|
-
// Try to add util record - may fail if port was taken by concurrent request
|
|
1186
|
-
const util = {
|
|
1187
|
-
id,
|
|
1188
|
-
name: utilName,
|
|
1189
|
-
port: candidatePort,
|
|
1190
|
-
pid: spawnedPid,
|
|
1191
|
-
tmuxSession: sessionName,
|
|
1192
|
-
worktreePath: worktreePath, // Track for cleanup on tab close
|
|
1193
|
-
};
|
|
1194
|
-
if (tryAddUtil(util)) {
|
|
1195
|
-
// Success - port reserved
|
|
1196
|
-
utilPort = candidatePort;
|
|
1197
|
-
pid = spawnedPid;
|
|
1198
|
-
break;
|
|
1199
|
-
}
|
|
1200
|
-
// Port conflict - kill the spawned process and retry
|
|
1201
|
-
console.log(`[info] Port ${candidatePort} conflict, retrying (attempt ${attempt + 1}/${MAX_PORT_RETRIES})`);
|
|
1202
|
-
await killProcessGracefully(spawnedPid);
|
|
1203
|
-
}
|
|
1204
|
-
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) {
|
|
1205
1396
|
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
1206
|
-
res.end('Failed to
|
|
1397
|
+
res.end('Failed to create terminal session');
|
|
1207
1398
|
return;
|
|
1208
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);
|
|
1209
1410
|
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
1210
|
-
res.end(JSON.stringify({ success: true, id, port:
|
|
1411
|
+
res.end(JSON.stringify({ success: true, id, port: 0, name: utilName, terminalId }));
|
|
1211
1412
|
return;
|
|
1212
1413
|
}
|
|
1213
1414
|
// API: Check if tab process is running (Bugfix #132)
|
|
@@ -1228,7 +1429,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
1228
1429
|
const util = tabUtils.find((u) => u.id === utilId);
|
|
1229
1430
|
if (util) {
|
|
1230
1431
|
found = true;
|
|
1231
|
-
|
|
1432
|
+
// Check tmux session status (Spec 0076)
|
|
1433
|
+
if (util.tmuxSession) {
|
|
1434
|
+
running = tmuxSessionExists(util.tmuxSession);
|
|
1435
|
+
}
|
|
1436
|
+
else {
|
|
1437
|
+
// Fallback for shells without tmux session (shouldn't happen in practice)
|
|
1438
|
+
running = isProcessRunning(util.pid);
|
|
1439
|
+
}
|
|
1232
1440
|
}
|
|
1233
1441
|
}
|
|
1234
1442
|
// Check if it's a builder tab
|
|
@@ -1237,7 +1445,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
1237
1445
|
const builder = getBuilder(builderId);
|
|
1238
1446
|
if (builder) {
|
|
1239
1447
|
found = true;
|
|
1240
|
-
|
|
1448
|
+
// Check tmux session status (Spec 0076)
|
|
1449
|
+
if (builder.tmuxSession) {
|
|
1450
|
+
running = tmuxSessionExists(builder.tmuxSession);
|
|
1451
|
+
}
|
|
1452
|
+
else {
|
|
1453
|
+
// Fallback for builders without tmux session (shouldn't happen in practice)
|
|
1454
|
+
running = isProcessRunning(builder.pid);
|
|
1455
|
+
}
|
|
1241
1456
|
}
|
|
1242
1457
|
}
|
|
1243
1458
|
if (found) {
|
|
@@ -1281,6 +1496,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
1281
1496
|
const tabUtils = getUtils();
|
|
1282
1497
|
const util = tabUtils.find((u) => u.id === utilId);
|
|
1283
1498
|
if (util) {
|
|
1499
|
+
// Kill PTY session if present
|
|
1500
|
+
if (util.terminalId && terminalManager) {
|
|
1501
|
+
terminalManager.killSession(util.terminalId);
|
|
1502
|
+
}
|
|
1284
1503
|
await killProcessGracefully(util.pid, util.tmuxSession);
|
|
1285
1504
|
// Note: worktrees are NOT cleaned up on tab close - they may contain useful context
|
|
1286
1505
|
// Users can manually clean up with `git worktree list` and `git worktree remove`
|
|
@@ -1660,20 +1879,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
1660
1879
|
}
|
|
1661
1880
|
return;
|
|
1662
1881
|
}
|
|
1663
|
-
// API: Get daily activity summary (Spec 0059)
|
|
1664
|
-
if (req.method === 'GET' && url.pathname === '/api/activity-summary') {
|
|
1665
|
-
try {
|
|
1666
|
-
const activitySummary = await collectActivitySummary(projectRoot);
|
|
1667
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1668
|
-
res.end(JSON.stringify(activitySummary));
|
|
1669
|
-
}
|
|
1670
|
-
catch (err) {
|
|
1671
|
-
console.error('Activity summary error:', err);
|
|
1672
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1673
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
1674
|
-
}
|
|
1675
|
-
return;
|
|
1676
|
-
}
|
|
1677
1882
|
// API: Hot reload check (Spec 0060)
|
|
1678
1883
|
// Returns modification times for all dashboard CSS/JS files
|
|
1679
1884
|
if (req.method === 'GET' && url.pathname === '/api/hot-reload') {
|
|
@@ -1751,7 +1956,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1751
1956
|
return;
|
|
1752
1957
|
}
|
|
1753
1958
|
// Terminal proxy route (Spec 0062 - Secure Remote Access)
|
|
1754
|
-
// Routes /terminal/:id to the appropriate
|
|
1959
|
+
// Routes /terminal/:id to the appropriate terminal instance
|
|
1755
1960
|
const terminalMatch = url.pathname.match(/^\/terminal\/([^/]+)(\/.*)?$/);
|
|
1756
1961
|
if (terminalMatch) {
|
|
1757
1962
|
const terminalId = terminalMatch[1];
|
|
@@ -1784,8 +1989,49 @@ const server = http.createServer(async (req, res) => {
|
|
|
1784
1989
|
terminalProxy.web(req, res, { target: `http://localhost:${annotation.port}` });
|
|
1785
1990
|
return;
|
|
1786
1991
|
}
|
|
1787
|
-
// Serve dashboard
|
|
1788
|
-
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
|
|
1789
2035
|
try {
|
|
1790
2036
|
let template = fs.readFileSync(templatePath, 'utf-8');
|
|
1791
2037
|
const state = loadStateWithCleanup();
|
|
@@ -1814,15 +2060,37 @@ const server = http.createServer(async (req, res) => {
|
|
|
1814
2060
|
res.end('Internal server error: ' + err.message);
|
|
1815
2061
|
}
|
|
1816
2062
|
});
|
|
2063
|
+
// Spec 0085: Attach node-pty WebSocket handler for /ws/terminal/:id routes
|
|
2064
|
+
if (terminalManager) {
|
|
2065
|
+
terminalManager.attachWebSocket(server);
|
|
2066
|
+
}
|
|
1817
2067
|
// WebSocket upgrade handler for terminal proxy (Spec 0062)
|
|
1818
|
-
//
|
|
2068
|
+
// WebSocket for bidirectional terminal communication
|
|
1819
2069
|
server.on('upgrade', (req, socket, head) => {
|
|
1820
|
-
// Security check
|
|
2070
|
+
// Security check for non-auth mode
|
|
1821
2071
|
const host = req.headers.host;
|
|
1822
|
-
if (!insecureRemoteMode && host && !host.startsWith('localhost') && !host.startsWith('127.0.0.1')) {
|
|
2072
|
+
if (!insecureRemoteMode && !process.env.CODEV_WEB_KEY && host && !host.startsWith('localhost') && !host.startsWith('127.0.0.1')) {
|
|
1823
2073
|
socket.destroy();
|
|
1824
2074
|
return;
|
|
1825
2075
|
}
|
|
2076
|
+
// CRITICAL: When CODEV_WEB_KEY is set, ALL WebSocket upgrades require auth
|
|
2077
|
+
// NO localhost bypass - tunnel daemons run locally, so remoteAddress is unreliable
|
|
2078
|
+
const webKey = process.env.CODEV_WEB_KEY;
|
|
2079
|
+
if (webKey && !insecureRemoteMode) {
|
|
2080
|
+
// Check Sec-WebSocket-Protocol for auth token
|
|
2081
|
+
// Format: "auth-<token>, tty" or just "tty"
|
|
2082
|
+
const protocols = req.headers['sec-websocket-protocol']?.split(',').map((p) => p.trim()) || [];
|
|
2083
|
+
const authProtocol = protocols.find((p) => p.startsWith('auth-'));
|
|
2084
|
+
const token = authProtocol?.substring(5); // Remove 'auth-' prefix
|
|
2085
|
+
if (!isValidToken(token, webKey)) {
|
|
2086
|
+
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
2087
|
+
socket.destroy();
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
// Remove auth protocol from the list before forwarding
|
|
2091
|
+
const cleanProtocols = protocols.filter((p) => !p.startsWith('auth-'));
|
|
2092
|
+
req.headers['sec-websocket-protocol'] = cleanProtocols.join(', ') || 'tty';
|
|
2093
|
+
}
|
|
1826
2094
|
const reqUrl = new URL(req.url || '/', `http://localhost:${port}`);
|
|
1827
2095
|
const terminalMatch = reqUrl.pathname.match(/^\/terminal\/([^/]+)(\/.*)?$/);
|
|
1828
2096
|
if (terminalMatch) {
|
|
@@ -1869,4 +2137,17 @@ else {
|
|
|
1869
2137
|
console.log(`Dashboard: http://localhost:${port}`);
|
|
1870
2138
|
});
|
|
1871
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
|
+
});
|
|
1872
2153
|
//# sourceMappingURL=dashboard-server.js.map
|