@cluesmith/codev 2.0.0-rc.4 → 2.0.0-rc.40
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-BLqoFC1H.js +120 -0
- package/dashboard/dist/assets/index-BLqoFC1H.js.map +1 -0
- package/dashboard/dist/assets/index-CXwnJkPh.css +32 -0
- package/dashboard/dist/index.html +13 -0
- package/dist/agent-farm/cli.d.ts.map +1 -1
- package/dist/agent-farm/cli.js +93 -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 +13 -6
- 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 +27 -79
- 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 +9 -0
- package/dist/agent-farm/commands/tower.d.ts.map +1 -1
- package/dist/agent-farm/commands/tower.js +54 -18
- 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 +435 -131
- package/dist/agent-farm/servers/dashboard-server.js.map +1 -1
- package/dist/agent-farm/servers/tower-server.js +430 -17
- 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 +12 -11
- 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 +39 -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 +31 -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 +466 -926
- 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 -699
- 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 +22 -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/prompts/audit.md +111 -0
- package/skeleton/protocols/maintain/prompts/clean.md +91 -0
- package/skeleton/protocols/maintain/prompts/sync.md +113 -0
- package/skeleton/protocols/maintain/prompts/verify.md +110 -0
- package/skeleton/protocols/maintain/protocol.json +141 -0
- package/skeleton/protocols/maintain/protocol.md +13 -7
- package/skeleton/protocols/protocol-schema.json +53 -0
- package/skeleton/protocols/spider/builder-prompt.md +53 -0
- package/skeleton/protocols/spider/prompts/implement.md +109 -50
- package/skeleton/protocols/spider/prompts/specify.md +29 -4
- package/skeleton/protocols/spider/protocol.json +96 -154
- 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 +25 -43
- package/skeleton/resources/commands/overview.md +6 -16
- 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/AGENTS.md +1 -1
- package/skeleton/templates/CLAUDE.md +1 -1
- 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 +497 -26
- 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 -331
- 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/config.json +0 -7
- 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/skeleton/protocols/spider/prompts/defend.md +0 -215
- package/skeleton/protocols/spider/prompts/evaluate.md +0 -241
- 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,147 @@ 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, 'af-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
|
+
// Listen for exit and auto-restart
|
|
176
|
+
session.on('exit', (exitCode) => {
|
|
177
|
+
console.log(`Architect terminal exited (code=${exitCode}), will attempt restart...`);
|
|
178
|
+
// Clear the terminalId so we can recreate
|
|
179
|
+
const arch = getArchitect();
|
|
180
|
+
if (arch) {
|
|
181
|
+
setArchitect({ ...arch, terminalId: undefined });
|
|
182
|
+
}
|
|
183
|
+
// Schedule restart after a brief delay
|
|
184
|
+
setTimeout(() => {
|
|
185
|
+
console.log('Attempting to restart architect terminal...');
|
|
186
|
+
initArchitectTerminal().catch((err) => {
|
|
187
|
+
console.error('Failed to restart architect terminal:', err.message);
|
|
188
|
+
});
|
|
189
|
+
}, 2000);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
console.error('Failed to create architect terminal session:', err.message);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Poll for architect state and create PTY session once available
|
|
197
|
+
// start.ts writes architect to DB before spawning this server, but there can be a small delay
|
|
198
|
+
(async function waitForArchitectAndInit() {
|
|
199
|
+
for (let attempt = 0; attempt < 30; attempt++) {
|
|
200
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
201
|
+
try {
|
|
202
|
+
const arch = getArchitect();
|
|
203
|
+
if (!arch)
|
|
204
|
+
continue;
|
|
205
|
+
if (arch.terminalId)
|
|
206
|
+
return; // Already has terminal
|
|
207
|
+
if (!arch.tmuxSession)
|
|
208
|
+
continue; // No tmux session yet
|
|
209
|
+
console.log(`initArchitectTerminal: attempt ${attempt + 1}, tmux=${arch.tmuxSession}`);
|
|
210
|
+
await initArchitectTerminal();
|
|
211
|
+
const updated = getArchitect();
|
|
212
|
+
if (updated?.terminalId) {
|
|
213
|
+
console.log(`initArchitectTerminal: success, terminalId=${updated.terminalId}`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
console.log(`initArchitectTerminal: attempt ${attempt + 1} failed, terminalId still unset`);
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
console.error(`initArchitectTerminal: attempt ${attempt + 1} error:`, err.message);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
console.warn('initArchitectTerminal: gave up after 30 attempts');
|
|
223
|
+
})();
|
|
224
|
+
// Log telemetry
|
|
225
|
+
try {
|
|
226
|
+
const metricsPath = path.join(projectRoot, '.agent-farm', 'metrics.log');
|
|
227
|
+
fs.mkdirSync(path.dirname(metricsPath), { recursive: true });
|
|
228
|
+
fs.appendFileSync(metricsPath, JSON.stringify({
|
|
229
|
+
event: 'backend_selected',
|
|
230
|
+
backend: 'node-pty',
|
|
231
|
+
timestamp: new Date().toISOString(),
|
|
232
|
+
}) + '\n');
|
|
233
|
+
}
|
|
234
|
+
catch { /* ignore */ }
|
|
94
235
|
// Clean up dead processes from state (called on state load)
|
|
95
236
|
function cleanupDeadProcesses() {
|
|
96
237
|
// Clean up dead shell processes
|
|
@@ -217,6 +358,9 @@ async function killProcessGracefully(pid, tmuxSession) {
|
|
|
217
358
|
if (tmuxSession) {
|
|
218
359
|
killTmuxSession(tmuxSession);
|
|
219
360
|
}
|
|
361
|
+
// Guard: PID 0 sends signal to entire process group — never do that
|
|
362
|
+
if (!pid || pid <= 0)
|
|
363
|
+
return;
|
|
220
364
|
try {
|
|
221
365
|
// First try SIGTERM
|
|
222
366
|
process.kill(pid, 'SIGTERM');
|
|
@@ -281,42 +425,24 @@ function tmuxSessionExists(sessionName) {
|
|
|
281
425
|
return false;
|
|
282
426
|
}
|
|
283
427
|
}
|
|
284
|
-
// Create a
|
|
285
|
-
//
|
|
286
|
-
function
|
|
428
|
+
// Create a PTY terminal session via the TerminalManager.
|
|
429
|
+
// Returns the terminal session ID, or null on failure.
|
|
430
|
+
async function createTerminalSession(shellCommand, cwd, label) {
|
|
431
|
+
if (!terminalManager)
|
|
432
|
+
return null;
|
|
287
433
|
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,
|
|
434
|
+
const info = await terminalManager.createSession({
|
|
435
|
+
command: '/bin/bash',
|
|
436
|
+
args: ['-c', shellCommand],
|
|
311
437
|
cwd,
|
|
312
|
-
|
|
438
|
+
cols: 200,
|
|
439
|
+
rows: 50,
|
|
440
|
+
label,
|
|
313
441
|
});
|
|
314
|
-
return
|
|
442
|
+
return info.id;
|
|
315
443
|
}
|
|
316
444
|
catch (err) {
|
|
317
|
-
console.error(`Failed to create
|
|
318
|
-
// Cleanup any partial session
|
|
319
|
-
killTmuxSession(sessionName);
|
|
445
|
+
console.error(`Failed to create terminal session:`, err.message);
|
|
320
446
|
return null;
|
|
321
447
|
}
|
|
322
448
|
}
|
|
@@ -336,7 +462,7 @@ function generateShortId() {
|
|
|
336
462
|
* Spawn a worktree builder - creates git worktree and starts builder CLI
|
|
337
463
|
* Similar to shell spawning but with git worktree isolation
|
|
338
464
|
*/
|
|
339
|
-
function spawnWorktreeBuilder(builderPort, state) {
|
|
465
|
+
async function spawnWorktreeBuilder(builderPort, state) {
|
|
340
466
|
const shortId = generateShortId();
|
|
341
467
|
const builderId = `worktree-${shortId}`;
|
|
342
468
|
const branchName = `builder/worktree-${shortId}`;
|
|
@@ -351,42 +477,23 @@ function spawnWorktreeBuilder(builderPort, state) {
|
|
|
351
477
|
// Create git branch and worktree
|
|
352
478
|
execSync(`git branch "${branchName}" HEAD`, { cwd: projectRoot, stdio: 'ignore' });
|
|
353
479
|
execSync(`git worktree add "${worktreePath}" "${branchName}"`, { cwd: projectRoot, stdio: 'ignore' });
|
|
354
|
-
// Get builder command from config or use default shell
|
|
355
|
-
const
|
|
480
|
+
// Get builder command from af-config.json or use default shell
|
|
481
|
+
const afConfigPath = path.resolve(projectRoot, 'af-config.json');
|
|
356
482
|
const defaultShell = process.env.SHELL || 'bash';
|
|
357
483
|
let builderCommand = defaultShell;
|
|
358
|
-
if (fs.existsSync(
|
|
484
|
+
if (fs.existsSync(afConfigPath)) {
|
|
359
485
|
try {
|
|
360
|
-
const config = JSON.parse(fs.readFileSync(
|
|
486
|
+
const config = JSON.parse(fs.readFileSync(afConfigPath, 'utf-8'));
|
|
361
487
|
builderCommand = config?.shell?.builder || defaultShell;
|
|
362
488
|
}
|
|
363
489
|
catch {
|
|
364
490
|
// Use default
|
|
365
491
|
}
|
|
366
492
|
}
|
|
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) {
|
|
493
|
+
// Create PTY terminal session via node-pty
|
|
494
|
+
const terminalId = await createTerminalSession(builderCommand, worktreePath, `builder-${builderId}`);
|
|
495
|
+
if (!terminalId) {
|
|
388
496
|
// Cleanup on failure
|
|
389
|
-
killTmuxSession(sessionName);
|
|
390
497
|
try {
|
|
391
498
|
execSync(`git worktree remove "${worktreePath}" --force`, { cwd: projectRoot, stdio: 'ignore' });
|
|
392
499
|
execSync(`git branch -D "${branchName}"`, { cwd: projectRoot, stdio: 'ignore' });
|
|
@@ -399,16 +506,17 @@ function spawnWorktreeBuilder(builderPort, state) {
|
|
|
399
506
|
const builder = {
|
|
400
507
|
id: builderId,
|
|
401
508
|
name: `Worktree ${shortId}`,
|
|
402
|
-
port:
|
|
403
|
-
pid,
|
|
509
|
+
port: 0,
|
|
510
|
+
pid: 0,
|
|
404
511
|
status: 'implementing',
|
|
405
512
|
phase: 'interactive',
|
|
406
513
|
worktree: worktreePath,
|
|
407
514
|
branch: branchName,
|
|
408
515
|
tmuxSession: sessionName,
|
|
409
516
|
type: 'worktree',
|
|
517
|
+
terminalId,
|
|
410
518
|
};
|
|
411
|
-
return { builder, pid };
|
|
519
|
+
return { builder, pid: 0 };
|
|
412
520
|
}
|
|
413
521
|
catch (err) {
|
|
414
522
|
console.error(`Failed to spawn worktree builder:`, err.message);
|
|
@@ -888,6 +996,115 @@ function isRequestAllowed(req) {
|
|
|
888
996
|
}
|
|
889
997
|
return isRequestAllowedBase(req);
|
|
890
998
|
}
|
|
999
|
+
/**
|
|
1000
|
+
* Timing-safe token comparison to prevent timing attacks
|
|
1001
|
+
*/
|
|
1002
|
+
function isValidToken(provided, expected) {
|
|
1003
|
+
if (!provided)
|
|
1004
|
+
return false;
|
|
1005
|
+
// Ensure both strings are same length for timing-safe comparison
|
|
1006
|
+
const providedBuf = Buffer.from(provided);
|
|
1007
|
+
const expectedBuf = Buffer.from(expected);
|
|
1008
|
+
if (providedBuf.length !== expectedBuf.length) {
|
|
1009
|
+
// Still do a comparison to maintain constant time
|
|
1010
|
+
timingSafeEqual(expectedBuf, expectedBuf);
|
|
1011
|
+
return false;
|
|
1012
|
+
}
|
|
1013
|
+
return timingSafeEqual(providedBuf, expectedBuf);
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Generate HTML for login page
|
|
1017
|
+
*/
|
|
1018
|
+
function getLoginPageHtml() {
|
|
1019
|
+
return `<!DOCTYPE html>
|
|
1020
|
+
<html>
|
|
1021
|
+
<head>
|
|
1022
|
+
<title>Dashboard Login</title>
|
|
1023
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1024
|
+
<style>
|
|
1025
|
+
body { font-family: system-ui; background: #1a1a2e; color: #eee;
|
|
1026
|
+
display: flex; justify-content: center; align-items: center;
|
|
1027
|
+
min-height: 100vh; margin: 0; }
|
|
1028
|
+
.login { background: #16213e; padding: 2rem; border-radius: 8px;
|
|
1029
|
+
max-width: 400px; width: 90%; }
|
|
1030
|
+
h1 { margin-top: 0; }
|
|
1031
|
+
input { width: 100%; padding: 0.75rem; margin: 0.5rem 0;
|
|
1032
|
+
border: 1px solid #444; border-radius: 4px;
|
|
1033
|
+
background: #0f0f23; color: #eee; font-size: 1rem;
|
|
1034
|
+
box-sizing: border-box; }
|
|
1035
|
+
button { width: 100%; padding: 0.75rem; margin-top: 1rem;
|
|
1036
|
+
background: #4a7c59; color: white; border: none;
|
|
1037
|
+
border-radius: 4px; font-size: 1rem; cursor: pointer; }
|
|
1038
|
+
button:hover { background: #5a9c69; }
|
|
1039
|
+
.error { color: #ff6b6b; margin-top: 0.5rem; display: none; }
|
|
1040
|
+
</style>
|
|
1041
|
+
</head>
|
|
1042
|
+
<body>
|
|
1043
|
+
<div class="login">
|
|
1044
|
+
<h1>Agent Farm Login</h1>
|
|
1045
|
+
<p>Enter your API key to access the dashboard.</p>
|
|
1046
|
+
<input type="password" id="key" placeholder="API Key" autofocus>
|
|
1047
|
+
<div class="error" id="error">Invalid API key</div>
|
|
1048
|
+
<button onclick="login()">Login</button>
|
|
1049
|
+
</div>
|
|
1050
|
+
<script>
|
|
1051
|
+
// Check for key in URL (from QR code scan) or localStorage
|
|
1052
|
+
(async function() {
|
|
1053
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
1054
|
+
const keyFromUrl = urlParams.get('key');
|
|
1055
|
+
const keyFromStorage = localStorage.getItem('codev_web_key');
|
|
1056
|
+
const key = keyFromUrl || keyFromStorage;
|
|
1057
|
+
|
|
1058
|
+
if (key) {
|
|
1059
|
+
if (keyFromUrl) {
|
|
1060
|
+
localStorage.setItem('codev_web_key', keyFromUrl);
|
|
1061
|
+
}
|
|
1062
|
+
await verifyAndLoadDashboard(key);
|
|
1063
|
+
}
|
|
1064
|
+
})();
|
|
1065
|
+
|
|
1066
|
+
async function verifyAndLoadDashboard(key) {
|
|
1067
|
+
try {
|
|
1068
|
+
// Fetch the actual dashboard with auth header
|
|
1069
|
+
const res = await fetch(window.location.pathname, {
|
|
1070
|
+
headers: {
|
|
1071
|
+
'Authorization': 'Bearer ' + key,
|
|
1072
|
+
'Accept': 'text/html'
|
|
1073
|
+
}
|
|
1074
|
+
});
|
|
1075
|
+
if (res.ok) {
|
|
1076
|
+
// Replace entire page with dashboard
|
|
1077
|
+
const html = await res.text();
|
|
1078
|
+
document.open();
|
|
1079
|
+
document.write(html);
|
|
1080
|
+
document.close();
|
|
1081
|
+
// Clean URL without reload
|
|
1082
|
+
history.replaceState({}, '', window.location.pathname);
|
|
1083
|
+
} else {
|
|
1084
|
+
// Key invalid
|
|
1085
|
+
localStorage.removeItem('codev_web_key');
|
|
1086
|
+
document.getElementById('error').style.display = 'block';
|
|
1087
|
+
document.getElementById('error').textContent = 'Invalid API key';
|
|
1088
|
+
}
|
|
1089
|
+
} catch (e) {
|
|
1090
|
+
document.getElementById('error').style.display = 'block';
|
|
1091
|
+
document.getElementById('error').textContent = 'Connection error';
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
async function login() {
|
|
1096
|
+
const key = document.getElementById('key').value;
|
|
1097
|
+
if (!key) return;
|
|
1098
|
+
localStorage.setItem('codev_web_key', key);
|
|
1099
|
+
await verifyAndLoadDashboard(key);
|
|
1100
|
+
}
|
|
1101
|
+
document.getElementById('key').addEventListener('keypress', (e) => {
|
|
1102
|
+
if (e.key === 'Enter') login();
|
|
1103
|
+
});
|
|
1104
|
+
</script>
|
|
1105
|
+
</body>
|
|
1106
|
+
</html>`;
|
|
1107
|
+
}
|
|
891
1108
|
// Create server
|
|
892
1109
|
const server = http.createServer(async (req, res) => {
|
|
893
1110
|
// Security: Validate Host and Origin headers
|
|
@@ -896,17 +1113,37 @@ const server = http.createServer(async (req, res) => {
|
|
|
896
1113
|
res.end('Forbidden');
|
|
897
1114
|
return;
|
|
898
1115
|
}
|
|
1116
|
+
// CRITICAL: When CODEV_WEB_KEY is set, ALL requests require auth
|
|
1117
|
+
// NO localhost bypass - tunnel daemons (cloudflared) run locally and proxy
|
|
1118
|
+
// to localhost, so checking remoteAddress would incorrectly trust remote traffic
|
|
1119
|
+
const webKey = process.env.CODEV_WEB_KEY;
|
|
1120
|
+
if (webKey) {
|
|
1121
|
+
const authHeader = req.headers.authorization;
|
|
1122
|
+
const token = authHeader?.replace('Bearer ', '');
|
|
1123
|
+
if (!isValidToken(token, webKey)) {
|
|
1124
|
+
// Return login page for HTML requests, 401 for API
|
|
1125
|
+
if (req.headers.accept?.includes('text/html')) {
|
|
1126
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1127
|
+
res.end(getLoginPageHtml());
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
1131
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
// When CODEV_WEB_KEY is NOT set: no auth required (local dev mode only)
|
|
899
1136
|
// CORS headers
|
|
900
1137
|
const origin = req.headers.origin;
|
|
901
|
-
if (insecureRemoteMode) {
|
|
902
|
-
// Allow any origin in insecure remote mode
|
|
1138
|
+
if (insecureRemoteMode || webKey) {
|
|
1139
|
+
// Allow any origin in insecure remote mode or when using auth (tunnel access)
|
|
903
1140
|
res.setHeader('Access-Control-Allow-Origin', origin || '*');
|
|
904
1141
|
}
|
|
905
1142
|
else if (origin && (origin.startsWith('http://localhost:') || origin.startsWith('http://127.0.0.1:'))) {
|
|
906
1143
|
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
907
1144
|
}
|
|
908
1145
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
909
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
1146
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
910
1147
|
// Prevent caching of API responses
|
|
911
1148
|
res.setHeader('Cache-Control', 'no-store');
|
|
912
1149
|
if (req.method === 'OPTIONS') {
|
|
@@ -916,6 +1153,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
916
1153
|
}
|
|
917
1154
|
const url = new URL(req.url || '/', `http://localhost:${port}`);
|
|
918
1155
|
try {
|
|
1156
|
+
// Spec 0085: node-pty terminal manager REST API routes
|
|
1157
|
+
if (terminalManager && url.pathname.startsWith('/api/terminals')) {
|
|
1158
|
+
if (terminalManager.handleRequest(req, res)) {
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
919
1162
|
// API: Get state
|
|
920
1163
|
if (req.method === 'GET' && url.pathname === '/api/state') {
|
|
921
1164
|
const state = loadStateWithCleanup();
|
|
@@ -1025,14 +1268,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
1025
1268
|
// Find available port for builder
|
|
1026
1269
|
const builderPort = await findAvailablePort(CONFIG.builderPortStart, builderState);
|
|
1027
1270
|
// Spawn worktree builder
|
|
1028
|
-
const result = spawnWorktreeBuilder(builderPort, builderState);
|
|
1271
|
+
const result = await spawnWorktreeBuilder(builderPort, builderState);
|
|
1029
1272
|
if (!result) {
|
|
1030
1273
|
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
1031
1274
|
res.end('Failed to spawn worktree builder');
|
|
1032
1275
|
return;
|
|
1033
1276
|
}
|
|
1034
|
-
// Wait for ttyd to be ready
|
|
1035
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1036
1277
|
// Save builder to state
|
|
1037
1278
|
upsertBuilder(result.builder);
|
|
1038
1279
|
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
@@ -1165,49 +1406,25 @@ const server = http.createServer(async (req, res) => {
|
|
|
1165
1406
|
const shellCommand = command
|
|
1166
1407
|
? `${shell} -c '${command.replace(/'/g, "'\\''")}; exec ${shell}'`
|
|
1167
1408
|
: 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) {
|
|
1409
|
+
// Create PTY terminal session via node-pty
|
|
1410
|
+
const terminalId = await createTerminalSession(shellCommand, cwd, `shell-${utilName}`);
|
|
1411
|
+
if (!terminalId) {
|
|
1205
1412
|
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
1206
|
-
res.end('Failed to
|
|
1413
|
+
res.end('Failed to create terminal session');
|
|
1207
1414
|
return;
|
|
1208
1415
|
}
|
|
1416
|
+
const util = {
|
|
1417
|
+
id,
|
|
1418
|
+
name: utilName,
|
|
1419
|
+
port: 0,
|
|
1420
|
+
pid: 0,
|
|
1421
|
+
tmuxSession: sessionName,
|
|
1422
|
+
worktreePath: worktreePath,
|
|
1423
|
+
terminalId,
|
|
1424
|
+
};
|
|
1425
|
+
addUtil(util);
|
|
1209
1426
|
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
1210
|
-
res.end(JSON.stringify({ success: true, id, port:
|
|
1427
|
+
res.end(JSON.stringify({ success: true, id, port: 0, name: utilName, terminalId }));
|
|
1211
1428
|
return;
|
|
1212
1429
|
}
|
|
1213
1430
|
// API: Check if tab process is running (Bugfix #132)
|
|
@@ -1228,7 +1445,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
1228
1445
|
const util = tabUtils.find((u) => u.id === utilId);
|
|
1229
1446
|
if (util) {
|
|
1230
1447
|
found = true;
|
|
1231
|
-
|
|
1448
|
+
// Check tmux session status (Spec 0076)
|
|
1449
|
+
if (util.tmuxSession) {
|
|
1450
|
+
running = tmuxSessionExists(util.tmuxSession);
|
|
1451
|
+
}
|
|
1452
|
+
else {
|
|
1453
|
+
// Fallback for shells without tmux session (shouldn't happen in practice)
|
|
1454
|
+
running = isProcessRunning(util.pid);
|
|
1455
|
+
}
|
|
1232
1456
|
}
|
|
1233
1457
|
}
|
|
1234
1458
|
// Check if it's a builder tab
|
|
@@ -1237,7 +1461,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
1237
1461
|
const builder = getBuilder(builderId);
|
|
1238
1462
|
if (builder) {
|
|
1239
1463
|
found = true;
|
|
1240
|
-
|
|
1464
|
+
// Check tmux session status (Spec 0076)
|
|
1465
|
+
if (builder.tmuxSession) {
|
|
1466
|
+
running = tmuxSessionExists(builder.tmuxSession);
|
|
1467
|
+
}
|
|
1468
|
+
else {
|
|
1469
|
+
// Fallback for builders without tmux session (shouldn't happen in practice)
|
|
1470
|
+
running = isProcessRunning(builder.pid);
|
|
1471
|
+
}
|
|
1241
1472
|
}
|
|
1242
1473
|
}
|
|
1243
1474
|
if (found) {
|
|
@@ -1281,6 +1512,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
1281
1512
|
const tabUtils = getUtils();
|
|
1282
1513
|
const util = tabUtils.find((u) => u.id === utilId);
|
|
1283
1514
|
if (util) {
|
|
1515
|
+
// Kill PTY session if present
|
|
1516
|
+
if (util.terminalId && terminalManager) {
|
|
1517
|
+
terminalManager.killSession(util.terminalId);
|
|
1518
|
+
}
|
|
1284
1519
|
await killProcessGracefully(util.pid, util.tmuxSession);
|
|
1285
1520
|
// Note: worktrees are NOT cleaned up on tab close - they may contain useful context
|
|
1286
1521
|
// Users can manually clean up with `git worktree list` and `git worktree remove`
|
|
@@ -1470,6 +1705,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
1470
1705
|
res.end(`File not found: ${filePath}`);
|
|
1471
1706
|
return;
|
|
1472
1707
|
}
|
|
1708
|
+
// Check if it's a directory
|
|
1709
|
+
const stat = fs.statSync(fullPath);
|
|
1710
|
+
if (stat.isDirectory()) {
|
|
1711
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
1712
|
+
res.end(`Cannot read directory as file: ${filePath}`);
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1473
1715
|
// Read and return file contents
|
|
1474
1716
|
try {
|
|
1475
1717
|
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
@@ -1660,20 +1902,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
1660
1902
|
}
|
|
1661
1903
|
return;
|
|
1662
1904
|
}
|
|
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
1905
|
// API: Hot reload check (Spec 0060)
|
|
1678
1906
|
// Returns modification times for all dashboard CSS/JS files
|
|
1679
1907
|
if (req.method === 'GET' && url.pathname === '/api/hot-reload') {
|
|
@@ -1751,7 +1979,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1751
1979
|
return;
|
|
1752
1980
|
}
|
|
1753
1981
|
// Terminal proxy route (Spec 0062 - Secure Remote Access)
|
|
1754
|
-
// Routes /terminal/:id to the appropriate
|
|
1982
|
+
// Routes /terminal/:id to the appropriate terminal instance
|
|
1755
1983
|
const terminalMatch = url.pathname.match(/^\/terminal\/([^/]+)(\/.*)?$/);
|
|
1756
1984
|
if (terminalMatch) {
|
|
1757
1985
|
const terminalId = terminalMatch[1];
|
|
@@ -1784,8 +2012,49 @@ const server = http.createServer(async (req, res) => {
|
|
|
1784
2012
|
terminalProxy.web(req, res, { target: `http://localhost:${annotation.port}` });
|
|
1785
2013
|
return;
|
|
1786
2014
|
}
|
|
1787
|
-
// Serve dashboard
|
|
1788
|
-
if (req.method === 'GET'
|
|
2015
|
+
// Serve dashboard (Spec 0085: React or legacy based on config)
|
|
2016
|
+
if (useReactDashboard && req.method === 'GET') {
|
|
2017
|
+
// Serve React dashboard static files
|
|
2018
|
+
const filePath = url.pathname === '/' || url.pathname === '/index.html'
|
|
2019
|
+
? path.join(reactDashboardPath, 'index.html')
|
|
2020
|
+
: path.join(reactDashboardPath, url.pathname);
|
|
2021
|
+
// Security: Prevent path traversal
|
|
2022
|
+
const resolved = path.resolve(filePath);
|
|
2023
|
+
if (!resolved.startsWith(reactDashboardPath)) {
|
|
2024
|
+
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
2025
|
+
res.end('Forbidden');
|
|
2026
|
+
return;
|
|
2027
|
+
}
|
|
2028
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) {
|
|
2029
|
+
const ext = path.extname(resolved);
|
|
2030
|
+
const mimeTypes = {
|
|
2031
|
+
'.html': 'text/html; charset=utf-8',
|
|
2032
|
+
'.js': 'application/javascript',
|
|
2033
|
+
'.css': 'text/css',
|
|
2034
|
+
'.json': 'application/json',
|
|
2035
|
+
'.svg': 'image/svg+xml',
|
|
2036
|
+
'.png': 'image/png',
|
|
2037
|
+
'.ico': 'image/x-icon',
|
|
2038
|
+
'.map': 'application/json',
|
|
2039
|
+
};
|
|
2040
|
+
const contentType = mimeTypes[ext] ?? 'application/octet-stream';
|
|
2041
|
+
// Cache static assets (hashed filenames) but not index.html
|
|
2042
|
+
if (ext !== '.html') {
|
|
2043
|
+
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
|
2044
|
+
}
|
|
2045
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
2046
|
+
fs.createReadStream(resolved).pipe(res);
|
|
2047
|
+
return;
|
|
2048
|
+
}
|
|
2049
|
+
// SPA fallback: serve index.html for client-side routing
|
|
2050
|
+
if (!url.pathname.startsWith('/api/') && !url.pathname.startsWith('/ws/') && !url.pathname.startsWith('/terminal/') && !url.pathname.startsWith('/annotation/')) {
|
|
2051
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
2052
|
+
fs.createReadStream(path.join(reactDashboardPath, 'index.html')).pipe(res);
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
if (!useReactDashboard && req.method === 'GET' && (url.pathname === '/' || url.pathname === '/index.html')) {
|
|
2057
|
+
// Legacy vanilla JS dashboard
|
|
1789
2058
|
try {
|
|
1790
2059
|
let template = fs.readFileSync(templatePath, 'utf-8');
|
|
1791
2060
|
const state = loadStateWithCleanup();
|
|
@@ -1814,15 +2083,37 @@ const server = http.createServer(async (req, res) => {
|
|
|
1814
2083
|
res.end('Internal server error: ' + err.message);
|
|
1815
2084
|
}
|
|
1816
2085
|
});
|
|
2086
|
+
// Spec 0085: Attach node-pty WebSocket handler for /ws/terminal/:id routes
|
|
2087
|
+
if (terminalManager) {
|
|
2088
|
+
terminalManager.attachWebSocket(server);
|
|
2089
|
+
}
|
|
1817
2090
|
// WebSocket upgrade handler for terminal proxy (Spec 0062)
|
|
1818
|
-
//
|
|
2091
|
+
// WebSocket for bidirectional terminal communication
|
|
1819
2092
|
server.on('upgrade', (req, socket, head) => {
|
|
1820
|
-
// Security check
|
|
2093
|
+
// Security check for non-auth mode
|
|
1821
2094
|
const host = req.headers.host;
|
|
1822
|
-
if (!insecureRemoteMode && host && !host.startsWith('localhost') && !host.startsWith('127.0.0.1')) {
|
|
2095
|
+
if (!insecureRemoteMode && !process.env.CODEV_WEB_KEY && host && !host.startsWith('localhost') && !host.startsWith('127.0.0.1')) {
|
|
1823
2096
|
socket.destroy();
|
|
1824
2097
|
return;
|
|
1825
2098
|
}
|
|
2099
|
+
// CRITICAL: When CODEV_WEB_KEY is set, ALL WebSocket upgrades require auth
|
|
2100
|
+
// NO localhost bypass - tunnel daemons run locally, so remoteAddress is unreliable
|
|
2101
|
+
const webKey = process.env.CODEV_WEB_KEY;
|
|
2102
|
+
if (webKey && !insecureRemoteMode) {
|
|
2103
|
+
// Check Sec-WebSocket-Protocol for auth token
|
|
2104
|
+
// Format: "auth-<token>, tty" or just "tty"
|
|
2105
|
+
const protocols = req.headers['sec-websocket-protocol']?.split(',').map((p) => p.trim()) || [];
|
|
2106
|
+
const authProtocol = protocols.find((p) => p.startsWith('auth-'));
|
|
2107
|
+
const token = authProtocol?.substring(5); // Remove 'auth-' prefix
|
|
2108
|
+
if (!isValidToken(token, webKey)) {
|
|
2109
|
+
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
2110
|
+
socket.destroy();
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
2113
|
+
// Remove auth protocol from the list before forwarding
|
|
2114
|
+
const cleanProtocols = protocols.filter((p) => !p.startsWith('auth-'));
|
|
2115
|
+
req.headers['sec-websocket-protocol'] = cleanProtocols.join(', ') || 'tty';
|
|
2116
|
+
}
|
|
1826
2117
|
const reqUrl = new URL(req.url || '/', `http://localhost:${port}`);
|
|
1827
2118
|
const terminalMatch = reqUrl.pathname.match(/^\/terminal\/([^/]+)(\/.*)?$/);
|
|
1828
2119
|
if (terminalMatch) {
|
|
@@ -1869,4 +2160,17 @@ else {
|
|
|
1869
2160
|
console.log(`Dashboard: http://localhost:${port}`);
|
|
1870
2161
|
});
|
|
1871
2162
|
}
|
|
2163
|
+
// Spec 0085: Graceful shutdown for node-pty terminal manager
|
|
2164
|
+
process.on('SIGTERM', () => {
|
|
2165
|
+
if (terminalManager) {
|
|
2166
|
+
terminalManager.shutdown();
|
|
2167
|
+
}
|
|
2168
|
+
process.exit(0);
|
|
2169
|
+
});
|
|
2170
|
+
process.on('SIGINT', () => {
|
|
2171
|
+
if (terminalManager) {
|
|
2172
|
+
terminalManager.shutdown();
|
|
2173
|
+
}
|
|
2174
|
+
process.exit(0);
|
|
2175
|
+
});
|
|
1872
2176
|
//# sourceMappingURL=dashboard-server.js.map
|