@cluesmith/codev 2.0.0-rc.9 → 2.0.1
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/af.js +2 -2
- package/bin/consult.js +1 -1
- package/dashboard/dist/assets/index-4n9zpWLY.css +32 -0
- package/dashboard/dist/assets/index-b38SaXk5.js +136 -0
- package/dashboard/dist/assets/index-b38SaXk5.js.map +1 -0
- package/dashboard/dist/index.html +14 -0
- package/dist/agent-farm/cli.d.ts.map +1 -1
- package/dist/agent-farm/cli.js +179 -104
- package/dist/agent-farm/cli.js.map +1 -1
- package/dist/agent-farm/commands/architect.d.ts +3 -3
- package/dist/agent-farm/commands/architect.d.ts.map +1 -1
- package/dist/agent-farm/commands/architect.js +20 -147
- 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 +144 -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 +35 -19
- package/dist/agent-farm/commands/cleanup.js.map +1 -1
- package/dist/agent-farm/commands/consult.d.ts +3 -4
- package/dist/agent-farm/commands/consult.d.ts.map +1 -1
- package/dist/agent-farm/commands/consult.js +27 -37
- 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/open.d.ts +4 -2
- package/dist/agent-farm/commands/open.d.ts.map +1 -1
- package/dist/agent-farm/commands/open.js +33 -83
- package/dist/agent-farm/commands/open.js.map +1 -1
- package/dist/agent-farm/commands/send.d.ts +1 -1
- package/dist/agent-farm/commands/send.d.ts.map +1 -1
- package/dist/agent-farm/commands/send.js +70 -79
- package/dist/agent-farm/commands/send.js.map +1 -1
- package/dist/agent-farm/commands/shell.d.ts +15 -0
- package/dist/agent-farm/commands/shell.d.ts.map +1 -0
- package/dist/agent-farm/commands/shell.js +50 -0
- package/dist/agent-farm/commands/shell.js.map +1 -0
- package/dist/agent-farm/commands/spawn-roles.d.ts +80 -0
- package/dist/agent-farm/commands/spawn-roles.d.ts.map +1 -0
- package/dist/agent-farm/commands/spawn-roles.js +278 -0
- package/dist/agent-farm/commands/spawn-roles.js.map +1 -0
- package/dist/agent-farm/commands/spawn-worktree.d.ts +96 -0
- package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -0
- package/dist/agent-farm/commands/spawn-worktree.js +305 -0
- package/dist/agent-farm/commands/spawn-worktree.js.map +1 -0
- package/dist/agent-farm/commands/spawn.d.ts +5 -1
- package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn.js +241 -561
- package/dist/agent-farm/commands/spawn.js.map +1 -1
- package/dist/agent-farm/commands/start.d.ts +10 -20
- package/dist/agent-farm/commands/start.d.ts.map +1 -1
- package/dist/agent-farm/commands/start.js +45 -449
- package/dist/agent-farm/commands/start.js.map +1 -1
- package/dist/agent-farm/commands/status.d.ts +2 -0
- package/dist/agent-farm/commands/status.d.ts.map +1 -1
- package/dist/agent-farm/commands/status.js +75 -24
- package/dist/agent-farm/commands/status.js.map +1 -1
- package/dist/agent-farm/commands/stop.d.ts +6 -0
- package/dist/agent-farm/commands/stop.d.ts.map +1 -1
- package/dist/agent-farm/commands/stop.js +49 -109
- package/dist/agent-farm/commands/stop.js.map +1 -1
- package/dist/agent-farm/commands/tower-cloud.d.ts +48 -0
- package/dist/agent-farm/commands/tower-cloud.d.ts.map +1 -0
- package/dist/agent-farm/commands/tower-cloud.js +293 -0
- package/dist/agent-farm/commands/tower-cloud.js.map +1 -0
- 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 +59 -19
- package/dist/agent-farm/commands/tower.js.map +1 -1
- package/dist/agent-farm/db/index.d.ts +6 -2
- package/dist/agent-farm/db/index.d.ts.map +1 -1
- package/dist/agent-farm/db/index.js +301 -19
- package/dist/agent-farm/db/index.js.map +1 -1
- package/dist/agent-farm/db/migrate.d.ts +0 -4
- package/dist/agent-farm/db/migrate.d.ts.map +1 -1
- package/dist/agent-farm/db/migrate.js +6 -55
- package/dist/agent-farm/db/migrate.js.map +1 -1
- package/dist/agent-farm/db/schema.d.ts +3 -3
- package/dist/agent-farm/db/schema.d.ts.map +1 -1
- package/dist/agent-farm/db/schema.js +25 -19
- package/dist/agent-farm/db/schema.js.map +1 -1
- package/dist/agent-farm/db/types.d.ts +3 -13
- package/dist/agent-farm/db/types.d.ts.map +1 -1
- package/dist/agent-farm/db/types.js +3 -11
- package/dist/agent-farm/db/types.js.map +1 -1
- package/dist/agent-farm/hq-connector.d.ts +2 -6
- package/dist/agent-farm/hq-connector.d.ts.map +1 -1
- package/dist/agent-farm/hq-connector.js +2 -17
- package/dist/agent-farm/hq-connector.js.map +1 -1
- package/dist/agent-farm/lib/cloud-config.d.ts +59 -0
- package/dist/agent-farm/lib/cloud-config.d.ts.map +1 -0
- package/dist/agent-farm/lib/cloud-config.js +143 -0
- package/dist/agent-farm/lib/cloud-config.js.map +1 -0
- package/dist/agent-farm/lib/device-name.d.ts +25 -0
- package/dist/agent-farm/lib/device-name.d.ts.map +1 -0
- package/dist/agent-farm/lib/device-name.js +46 -0
- package/dist/agent-farm/lib/device-name.js.map +1 -0
- package/dist/agent-farm/lib/nonce-store.d.ts +28 -0
- package/dist/agent-farm/lib/nonce-store.d.ts.map +1 -0
- package/dist/agent-farm/lib/nonce-store.js +60 -0
- package/dist/agent-farm/lib/nonce-store.js.map +1 -0
- package/dist/agent-farm/lib/token-exchange.d.ts +18 -0
- package/dist/agent-farm/lib/token-exchange.d.ts.map +1 -0
- package/dist/agent-farm/lib/token-exchange.js +48 -0
- package/dist/agent-farm/lib/token-exchange.js.map +1 -0
- package/dist/agent-farm/lib/tower-client.d.ts +163 -0
- package/dist/agent-farm/lib/tower-client.d.ts.map +1 -0
- package/dist/agent-farm/lib/tower-client.js +233 -0
- package/dist/agent-farm/lib/tower-client.js.map +1 -0
- package/dist/agent-farm/lib/tunnel-client.d.ts +117 -0
- package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -0
- package/dist/agent-farm/lib/tunnel-client.js +504 -0
- package/dist/agent-farm/lib/tunnel-client.js.map +1 -0
- package/dist/agent-farm/servers/tower-instances.d.ts +82 -0
- package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -0
- package/dist/agent-farm/servers/tower-instances.js +454 -0
- package/dist/agent-farm/servers/tower-instances.js.map +1 -0
- package/dist/agent-farm/servers/tower-routes.d.ts +34 -0
- package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -0
- package/dist/agent-farm/servers/tower-routes.js +1445 -0
- package/dist/agent-farm/servers/tower-routes.js.map +1 -0
- package/dist/agent-farm/servers/tower-server.d.ts +5 -2
- package/dist/agent-farm/servers/tower-server.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-server.js +157 -475
- package/dist/agent-farm/servers/tower-server.js.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.d.ts +119 -0
- package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -0
- package/dist/agent-farm/servers/tower-terminals.js +629 -0
- package/dist/agent-farm/servers/tower-terminals.js.map +1 -0
- package/dist/agent-farm/servers/tower-tunnel.d.ts +34 -0
- package/dist/agent-farm/servers/tower-tunnel.d.ts.map +1 -0
- package/dist/agent-farm/servers/tower-tunnel.js +480 -0
- package/dist/agent-farm/servers/tower-tunnel.js.map +1 -0
- package/dist/agent-farm/servers/tower-types.d.ts +86 -0
- package/dist/agent-farm/servers/tower-types.d.ts.map +1 -0
- package/dist/agent-farm/servers/tower-types.js +6 -0
- package/dist/agent-farm/servers/tower-types.js.map +1 -0
- package/dist/agent-farm/servers/tower-utils.d.ts +58 -0
- package/dist/agent-farm/servers/tower-utils.d.ts.map +1 -0
- package/dist/agent-farm/servers/tower-utils.js +182 -0
- package/dist/agent-farm/servers/tower-utils.js.map +1 -0
- package/dist/agent-farm/servers/tower-websocket.d.ts +25 -0
- package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -0
- package/dist/agent-farm/servers/tower-websocket.js +171 -0
- package/dist/agent-farm/servers/tower-websocket.js.map +1 -0
- package/dist/agent-farm/state.d.ts +6 -2
- package/dist/agent-farm/state.d.ts.map +1 -1
- package/dist/agent-farm/state.js +34 -25
- package/dist/agent-farm/state.js.map +1 -1
- package/dist/agent-farm/types.d.ts +49 -26
- package/dist/agent-farm/types.d.ts.map +1 -1
- package/dist/agent-farm/utils/config.d.ts +0 -5
- package/dist/agent-farm/utils/config.d.ts.map +1 -1
- package/dist/agent-farm/utils/config.js +12 -44
- 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 -32
- package/dist/agent-farm/utils/deps.js.map +1 -1
- package/dist/agent-farm/utils/file-tabs.d.ts +27 -0
- package/dist/agent-farm/utils/file-tabs.d.ts.map +1 -0
- package/dist/agent-farm/utils/file-tabs.js +46 -0
- package/dist/agent-farm/utils/file-tabs.js.map +1 -0
- package/dist/agent-farm/utils/gate-status.d.ts +16 -0
- package/dist/agent-farm/utils/gate-status.d.ts.map +1 -0
- package/dist/agent-farm/utils/gate-status.js +79 -0
- package/dist/agent-farm/utils/gate-status.js.map +1 -0
- package/dist/agent-farm/utils/gate-watcher.d.ts +38 -0
- package/dist/agent-farm/utils/gate-watcher.d.ts.map +1 -0
- package/dist/agent-farm/utils/gate-watcher.js +122 -0
- package/dist/agent-farm/utils/gate-watcher.js.map +1 -0
- package/dist/agent-farm/utils/index.d.ts +0 -1
- package/dist/agent-farm/utils/index.d.ts.map +1 -1
- package/dist/agent-farm/utils/index.js +0 -1
- package/dist/agent-farm/utils/index.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 +5 -5
- package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
- package/dist/agent-farm/utils/server-utils.js +5 -16
- package/dist/agent-farm/utils/server-utils.js.map +1 -1
- package/dist/agent-farm/utils/session.d.ts +32 -0
- package/dist/agent-farm/utils/session.d.ts.map +1 -0
- package/dist/agent-farm/utils/session.js +57 -0
- package/dist/agent-farm/utils/session.js.map +1 -0
- 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/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -37
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt.d.ts.map +1 -1
- package/dist/commands/adopt.js +33 -4
- package/dist/commands/adopt.js.map +1 -1
- package/dist/commands/consult/index.d.ts +13 -2
- package/dist/commands/consult/index.d.ts.map +1 -1
- package/dist/commands/consult/index.js +244 -29
- package/dist/commands/consult/index.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +96 -79
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +36 -3
- 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 +3 -2
- package/dist/commands/porch/checks.d.ts.map +1 -1
- package/dist/commands/porch/checks.js +8 -2
- package/dist/commands/porch/checks.js.map +1 -1
- package/dist/commands/porch/index.d.ts +4 -0
- package/dist/commands/porch/index.d.ts.map +1 -1
- package/dist/commands/porch/index.js +109 -70
- package/dist/commands/porch/index.js.map +1 -1
- package/dist/commands/porch/next.d.ts +22 -0
- package/dist/commands/porch/next.d.ts.map +1 -0
- package/dist/commands/porch/next.js +571 -0
- package/dist/commands/porch/next.js.map +1 -0
- package/dist/commands/porch/plan.d.ts +11 -1
- package/dist/commands/porch/plan.d.ts.map +1 -1
- package/dist/commands/porch/plan.js +33 -5
- package/dist/commands/porch/plan.js.map +1 -1
- package/dist/commands/porch/prompts.d.ts.map +1 -1
- package/dist/commands/porch/prompts.js +44 -26
- package/dist/commands/porch/prompts.js.map +1 -1
- package/dist/commands/porch/protocol.d.ts +6 -4
- package/dist/commands/porch/protocol.d.ts.map +1 -1
- package/dist/commands/porch/protocol.js +59 -15
- package/dist/commands/porch/protocol.js.map +1 -1
- package/dist/commands/porch/state.d.ts +29 -2
- package/dist/commands/porch/state.d.ts.map +1 -1
- package/dist/commands/porch/state.js +71 -3
- package/dist/commands/porch/state.js.map +1 -1
- package/dist/commands/porch/types.d.ts +45 -2
- package/dist/commands/porch/types.d.ts.map +1 -1
- package/dist/commands/porch/verdict.d.ts +31 -0
- package/dist/commands/porch/verdict.d.ts.map +1 -0
- package/dist/commands/porch/verdict.js +59 -0
- package/dist/commands/porch/verdict.js.map +1 -0
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +18 -6
- package/dist/commands/update.js.map +1 -1
- package/dist/lib/scaffold.d.ts +13 -0
- package/dist/lib/scaffold.d.ts.map +1 -1
- package/dist/lib/scaffold.js +36 -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 +69 -0
- package/dist/terminal/pty-manager.d.ts.map +1 -0
- package/dist/terminal/pty-manager.js +377 -0
- package/dist/terminal/pty-manager.js.map +1 -0
- package/dist/terminal/pty-session.d.ts +104 -0
- package/dist/terminal/pty-session.d.ts.map +1 -0
- package/dist/terminal/pty-session.js +327 -0
- package/dist/terminal/pty-session.js.map +1 -0
- package/dist/terminal/ring-buffer.d.ts +34 -0
- package/dist/terminal/ring-buffer.d.ts.map +1 -0
- package/dist/terminal/ring-buffer.js +94 -0
- package/dist/terminal/ring-buffer.js.map +1 -0
- package/dist/terminal/session-manager.d.ts +115 -0
- package/dist/terminal/session-manager.d.ts.map +1 -0
- package/dist/terminal/session-manager.js +582 -0
- package/dist/terminal/session-manager.js.map +1 -0
- package/dist/terminal/shellper-client.d.ts +66 -0
- package/dist/terminal/shellper-client.d.ts.map +1 -0
- package/dist/terminal/shellper-client.js +234 -0
- package/dist/terminal/shellper-client.js.map +1 -0
- package/dist/terminal/shellper-main.d.ts +19 -0
- package/dist/terminal/shellper-main.d.ts.map +1 -0
- package/dist/terminal/shellper-main.js +153 -0
- package/dist/terminal/shellper-main.js.map +1 -0
- package/dist/terminal/shellper-process.d.ts +75 -0
- package/dist/terminal/shellper-process.d.ts.map +1 -0
- package/dist/terminal/shellper-process.js +279 -0
- package/dist/terminal/shellper-process.js.map +1 -0
- package/dist/terminal/shellper-protocol.d.ts +115 -0
- package/dist/terminal/shellper-protocol.d.ts.map +1 -0
- package/dist/terminal/shellper-protocol.js +214 -0
- package/dist/terminal/shellper-protocol.js.map +1 -0
- package/dist/terminal/shellper-replay-buffer.d.ts +38 -0
- package/dist/terminal/shellper-replay-buffer.d.ts.map +1 -0
- package/dist/terminal/shellper-replay-buffer.js +94 -0
- package/dist/terminal/shellper-replay-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 +17 -5
- package/skeleton/.claude/skills/af/SKILL.md +89 -0
- package/skeleton/.claude/skills/codev/SKILL.md +41 -0
- package/skeleton/.claude/skills/consult/SKILL.md +81 -0
- package/skeleton/.claude/skills/generate-image/SKILL.md +56 -0
- package/skeleton/DEPENDENCIES.md +4 -62
- package/skeleton/builders.md +1 -1
- package/skeleton/consult-types/impl-review.md +18 -9
- package/skeleton/consult-types/integration-review.md +1 -1
- package/skeleton/consult-types/plan-review.md +1 -1
- package/skeleton/consult-types/pr-ready.md +1 -1
- package/skeleton/consult-types/spec-review.md +1 -1
- package/skeleton/porch/prompts/defend.md +1 -1
- package/skeleton/porch/prompts/evaluate.md +2 -2
- package/skeleton/porch/prompts/implement.md +1 -1
- package/skeleton/porch/prompts/plan.md +1 -1
- package/skeleton/porch/prompts/review.md +4 -4
- package/skeleton/porch/prompts/specify.md +1 -1
- package/skeleton/porch/prompts/understand.md +2 -2
- package/skeleton/protocol-schema.json +282 -0
- package/skeleton/protocols/bugfix/builder-prompt.md +60 -0
- package/skeleton/protocols/bugfix/prompts/fix.md +77 -0
- package/skeleton/protocols/bugfix/prompts/investigate.md +77 -0
- package/skeleton/protocols/bugfix/prompts/pr.md +84 -0
- package/skeleton/protocols/bugfix/protocol.json +20 -33
- package/skeleton/protocols/experiment/builder-prompt.md +52 -0
- package/skeleton/protocols/experiment/protocol.json +101 -0
- package/skeleton/protocols/experiment/protocol.md +3 -3
- package/skeleton/protocols/experiment/templates/notes.md +1 -1
- package/skeleton/protocols/maintain/builder-prompt.md +46 -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 +17 -11
- package/skeleton/protocols/protocol-schema.json +54 -1
- package/skeleton/protocols/spir/builder-prompt.md +66 -0
- package/skeleton/protocols/spir/prompts/implement.md +208 -0
- package/skeleton/protocols/{spider → spir}/prompts/plan.md +6 -70
- package/skeleton/protocols/{spider → spir}/prompts/review.md +20 -39
- package/skeleton/protocols/{spider → spir}/prompts/specify.md +24 -59
- package/skeleton/protocols/{spider → spir}/protocol.json +30 -10
- package/skeleton/protocols/{spider → spir}/protocol.md +35 -21
- package/skeleton/protocols/spir/templates/review.md +89 -0
- package/skeleton/protocols/tick/builder-prompt.md +56 -0
- package/skeleton/protocols/tick/protocol.json +7 -2
- package/skeleton/protocols/tick/protocol.md +18 -18
- package/skeleton/protocols/tick/templates/review.md +1 -1
- package/skeleton/resources/commands/agent-farm.md +63 -46
- package/skeleton/resources/commands/codev.md +0 -2
- package/skeleton/resources/commands/overview.md +7 -17
- package/skeleton/resources/workflow-reference.md +4 -4
- package/skeleton/roles/architect.md +151 -306
- package/skeleton/roles/builder.md +115 -332
- package/skeleton/roles/consultant.md +6 -6
- package/skeleton/templates/AGENTS.md +2 -2
- package/skeleton/templates/CLAUDE.md +2 -2
- package/skeleton/templates/cheatsheet.md +7 -5
- package/skeleton/templates/projectlist.md +1 -1
- package/templates/dashboard/index.html +17 -16
- package/templates/dashboard/js/dialogs.js +7 -7
- package/templates/dashboard/js/files.js +2 -2
- package/templates/dashboard/js/main.js +4 -4
- package/templates/dashboard/js/projects.js +3 -3
- package/templates/dashboard/js/tabs.js +1 -1
- package/templates/dashboard/js/utils.js +22 -1
- package/templates/open.html +26 -0
- package/templates/tower.html +731 -91
- package/dist/agent-farm/commands/kickoff.d.ts +0 -20
- package/dist/agent-farm/commands/kickoff.d.ts.map +0 -1
- package/dist/agent-farm/commands/kickoff.js +0 -273
- 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 +0 -15
- package/dist/agent-farm/commands/util.d.ts.map +0 -1
- package/dist/agent-farm/commands/util.js +0 -108
- package/dist/agent-farm/commands/util.js.map +0 -1
- package/dist/agent-farm/servers/dashboard-server.d.ts +0 -7
- package/dist/agent-farm/servers/dashboard-server.d.ts.map +0 -1
- package/dist/agent-farm/servers/dashboard-server.js +0 -1858
- package/dist/agent-farm/servers/dashboard-server.js.map +0 -1
- package/dist/agent-farm/servers/open-server.d.ts +0 -7
- package/dist/agent-farm/servers/open-server.d.ts.map +0 -1
- package/dist/agent-farm/servers/open-server.js +0 -315
- package/dist/agent-farm/servers/open-server.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/agent-farm/utils/orphan-handler.d.ts +0 -27
- package/dist/agent-farm/utils/orphan-handler.d.ts.map +0 -1
- package/dist/agent-farm/utils/orphan-handler.js +0 -149
- package/dist/agent-farm/utils/orphan-handler.js.map +0 -1
- package/dist/agent-farm/utils/port-registry.d.ts +0 -58
- package/dist/agent-farm/utils/port-registry.d.ts.map +0 -1
- package/dist/agent-farm/utils/port-registry.js +0 -166
- package/dist/agent-farm/utils/port-registry.js.map +0 -1
- package/dist/agent-farm/utils/terminal-ports.d.ts +0 -18
- package/dist/agent-farm/utils/terminal-ports.d.ts.map +0 -1
- package/dist/agent-farm/utils/terminal-ports.js +0 -35
- package/dist/agent-farm/utils/terminal-ports.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/claude.d.ts +0 -29
- package/dist/commands/porch/claude.d.ts.map +0 -1
- package/dist/commands/porch/claude.js +0 -79
- package/dist/commands/porch/claude.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 -262
- package/dist/commands/porch/protocol-loader.js.map +0 -1
- package/dist/commands/porch/repl.d.ts +0 -33
- package/dist/commands/porch/repl.d.ts.map +0 -1
- package/dist/commands/porch/repl.js +0 -206
- package/dist/commands/porch/repl.js.map +0 -1
- package/dist/commands/porch/run.d.ts +0 -15
- package/dist/commands/porch/run.d.ts.map +0 -1
- package/dist/commands/porch/run.js +0 -551
- package/dist/commands/porch/run.js.map +0 -1
- package/dist/commands/porch/signal-parser.d.ts +0 -102
- package/dist/commands/porch/signal-parser.d.ts.map +0 -1
- package/dist/commands/porch/signal-parser.js +0 -199
- package/dist/commands/porch/signal-parser.js.map +0 -1
- package/dist/commands/porch/signals.d.ts +0 -35
- package/dist/commands/porch/signals.d.ts.map +0 -1
- package/dist/commands/porch/signals.js +0 -76
- package/dist/commands/porch/signals.js.map +0 -1
- package/dist/commands/porch2/checks.d.ts +0 -29
- package/dist/commands/porch2/checks.d.ts.map +0 -1
- package/dist/commands/porch2/checks.js +0 -141
- package/dist/commands/porch2/checks.js.map +0 -1
- package/dist/commands/porch2/index.d.ts +0 -38
- package/dist/commands/porch2/index.d.ts.map +0 -1
- package/dist/commands/porch2/index.js +0 -483
- package/dist/commands/porch2/index.js.map +0 -1
- package/dist/commands/porch2/plan.d.ts +0 -70
- package/dist/commands/porch2/plan.d.ts.map +0 -1
- package/dist/commands/porch2/plan.js +0 -227
- package/dist/commands/porch2/plan.js.map +0 -1
- package/dist/commands/porch2/protocol.d.ts +0 -37
- package/dist/commands/porch2/protocol.d.ts.map +0 -1
- package/dist/commands/porch2/protocol.js +0 -183
- package/dist/commands/porch2/protocol.js.map +0 -1
- package/dist/commands/porch2/state.d.ts +0 -35
- package/dist/commands/porch2/state.d.ts.map +0 -1
- package/dist/commands/porch2/state.js +0 -124
- package/dist/commands/porch2/state.js.map +0 -1
- package/dist/commands/porch2/types.d.ts +0 -79
- package/dist/commands/porch2/types.d.ts.map +0 -1
- package/dist/commands/porch2/types.js +0 -8
- package/dist/commands/porch2/types.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/protocols/spider/prompts/defend.md +0 -215
- package/skeleton/protocols/spider/prompts/evaluate.md +0 -241
- package/skeleton/protocols/spider/prompts/implement.md +0 -149
- package/skeleton/protocols/spider/templates/review.md +0 -207
- /package/skeleton/protocols/{spider → spir}/templates/plan.md +0 -0
- /package/skeleton/protocols/{spider → spir}/templates/spec.md +0 -0
|
@@ -0,0 +1,1445 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP route handlers for tower server.
|
|
3
|
+
* Spec 0105: Tower Server Decomposition — Phase 6
|
|
4
|
+
*
|
|
5
|
+
* Contains all HTTP request routing and response logic.
|
|
6
|
+
* The orchestrator (tower-server.ts) creates the HTTP server and
|
|
7
|
+
* delegates to handleRequest() for all HTTP requests.
|
|
8
|
+
*
|
|
9
|
+
* NOTE: This file exceeds the 900-line guideline because it contains
|
|
10
|
+
* all HTTP route handlers (~30 routes) which share a single responsibility
|
|
11
|
+
* (HTTP request handling). Splitting would create arbitrary boundaries
|
|
12
|
+
* without improving cohesion. See spec: "cohesion trumps arbitrary ceilings."
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import crypto from 'node:crypto';
|
|
17
|
+
import { execSync } from 'node:child_process';
|
|
18
|
+
import { homedir, tmpdir } from 'node:os';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { parseJsonBody, isRequestAllowed } from '../utils/server-utils.js';
|
|
21
|
+
import { isRateLimited, normalizeProjectPath, getLanguageForExt, getMimeTypeForFile, serveStaticFile, } from './tower-utils.js';
|
|
22
|
+
import { handleTunnelEndpoint } from './tower-tunnel.js';
|
|
23
|
+
import { getKnownProjectPaths, getInstances, getDirectorySuggestions, launchInstance, killTerminalWithShellper, stopInstance, } from './tower-instances.js';
|
|
24
|
+
import { getProjectTerminals, getTerminalManager, getProjectTerminalsEntry, getNextShellId, saveTerminalSession, isSessionPersistent, deleteTerminalSession, deleteProjectTerminalSessions, saveFileTab, deleteFileTab, getTerminalsForProject, } from './tower-terminals.js';
|
|
25
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
26
|
+
const __dirname = path.dirname(__filename);
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Helper: read raw request body
|
|
29
|
+
// ============================================================================
|
|
30
|
+
async function readBody(req) {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
let data = '';
|
|
33
|
+
req.on('data', (chunk) => data += chunk.toString());
|
|
34
|
+
req.on('end', () => resolve(data));
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
const ROUTES = {
|
|
38
|
+
'GET /health': (_req, res) => handleHealthCheck(res),
|
|
39
|
+
'GET /api/projects': (_req, res) => handleListProjects(res),
|
|
40
|
+
'POST /api/terminals': (req, res, _url, ctx) => handleTerminalCreate(req, res, ctx),
|
|
41
|
+
'GET /api/terminals': (_req, res) => handleTerminalList(res),
|
|
42
|
+
'GET /api/status': (_req, res) => handleStatus(res),
|
|
43
|
+
'GET /api/events': (req, res, _url, ctx) => handleSSEEvents(req, res, ctx),
|
|
44
|
+
'POST /api/notify': (req, res, _url, ctx) => handleNotify(req, res, ctx),
|
|
45
|
+
'GET /api/browse': (_req, res, url) => handleBrowse(res, url),
|
|
46
|
+
'POST /api/create': (req, res, _url, ctx) => handleCreateProject(req, res, ctx),
|
|
47
|
+
'POST /api/launch': (req, res) => handleLaunchInstance(req, res),
|
|
48
|
+
'POST /api/stop': (req, res) => handleStopInstance(req, res),
|
|
49
|
+
'GET /': (_req, res, _url, ctx) => handleDashboard(res, ctx),
|
|
50
|
+
'GET /index.html': (_req, res, _url, ctx) => handleDashboard(res, ctx),
|
|
51
|
+
};
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// Main request handler
|
|
54
|
+
// ============================================================================
|
|
55
|
+
export async function handleRequest(req, res, ctx) {
|
|
56
|
+
// Security: Validate Host and Origin headers
|
|
57
|
+
if (!isRequestAllowed(req)) {
|
|
58
|
+
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
59
|
+
res.end('Forbidden');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// CORS headers — allow localhost and tunnel proxy origins
|
|
63
|
+
const origin = req.headers.origin;
|
|
64
|
+
if (origin && (origin.startsWith('http://localhost:') ||
|
|
65
|
+
origin.startsWith('http://127.0.0.1:') ||
|
|
66
|
+
origin.startsWith('https://'))) {
|
|
67
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
68
|
+
}
|
|
69
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
70
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
71
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
72
|
+
if (req.method === 'OPTIONS') {
|
|
73
|
+
res.writeHead(200);
|
|
74
|
+
res.end();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const url = new URL(req.url || '/', `http://localhost:${ctx.port}`);
|
|
78
|
+
try {
|
|
79
|
+
// Exact-match route dispatch (O(1) lookup)
|
|
80
|
+
const routeKey = `${req.method} ${url.pathname}`;
|
|
81
|
+
const handler = ROUTES[routeKey];
|
|
82
|
+
if (handler) {
|
|
83
|
+
return await handler(req, res, url, ctx);
|
|
84
|
+
}
|
|
85
|
+
// Pattern-based routes (require regex or prefix matching)
|
|
86
|
+
// Tunnel endpoints: /api/tunnel/* (Spec 0097 Phase 4)
|
|
87
|
+
if (url.pathname.startsWith('/api/tunnel/')) {
|
|
88
|
+
const tunnelSub = url.pathname.slice('/api/tunnel/'.length);
|
|
89
|
+
await handleTunnelEndpoint(req, res, tunnelSub);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// Project API: /api/projects/:encodedPath/activate|deactivate|status (Spec 0090 Phase 1)
|
|
93
|
+
const projectApiMatch = url.pathname.match(/^\/api\/projects\/([^/]+)\/(activate|deactivate|status)$/);
|
|
94
|
+
if (projectApiMatch) {
|
|
95
|
+
return await handleProjectAction(req, res, ctx, projectApiMatch);
|
|
96
|
+
}
|
|
97
|
+
// Terminal-specific routes: /api/terminals/:id/* (Spec 0090 Phase 2)
|
|
98
|
+
const terminalRouteMatch = url.pathname.match(/^\/api\/terminals\/([^/]+)(\/.*)?$/);
|
|
99
|
+
if (terminalRouteMatch) {
|
|
100
|
+
return await handleTerminalRoutes(req, res, url, terminalRouteMatch);
|
|
101
|
+
}
|
|
102
|
+
// Project routes: /project/:base64urlPath/* (Spec 0090 Phase 4)
|
|
103
|
+
if (url.pathname.startsWith('/project/')) {
|
|
104
|
+
return await handleProjectRoutes(req, res, ctx, url);
|
|
105
|
+
}
|
|
106
|
+
// 404 for everything else
|
|
107
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
108
|
+
res.end('Not found');
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
ctx.log('ERROR', `Request error: ${err.message}`);
|
|
112
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
113
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// ============================================================================
|
|
117
|
+
// Global route handlers
|
|
118
|
+
// ============================================================================
|
|
119
|
+
async function handleHealthCheck(res) {
|
|
120
|
+
const instances = await getInstances();
|
|
121
|
+
const activeCount = instances.filter((i) => i.running).length;
|
|
122
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
123
|
+
res.end(JSON.stringify({
|
|
124
|
+
status: 'healthy',
|
|
125
|
+
uptime: process.uptime(),
|
|
126
|
+
activeProjects: activeCount,
|
|
127
|
+
totalProjects: instances.length,
|
|
128
|
+
memoryUsage: process.memoryUsage().heapUsed,
|
|
129
|
+
timestamp: new Date().toISOString(),
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
async function handleListProjects(res) {
|
|
133
|
+
const instances = await getInstances();
|
|
134
|
+
const projects = instances.map((i) => ({
|
|
135
|
+
path: i.projectPath,
|
|
136
|
+
name: i.projectName,
|
|
137
|
+
active: i.running,
|
|
138
|
+
proxyUrl: i.proxyUrl,
|
|
139
|
+
terminals: i.terminals.length,
|
|
140
|
+
}));
|
|
141
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
142
|
+
res.end(JSON.stringify({ projects }));
|
|
143
|
+
}
|
|
144
|
+
async function handleProjectAction(req, res, ctx, match) {
|
|
145
|
+
const [, encodedPath, action] = match;
|
|
146
|
+
let projectPath;
|
|
147
|
+
try {
|
|
148
|
+
projectPath = Buffer.from(encodedPath, 'base64url').toString('utf-8');
|
|
149
|
+
if (!projectPath || (!projectPath.startsWith('/') && !/^[A-Za-z]:[\\/]/.test(projectPath))) {
|
|
150
|
+
throw new Error('Invalid path');
|
|
151
|
+
}
|
|
152
|
+
projectPath = normalizeProjectPath(projectPath);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
156
|
+
res.end(JSON.stringify({ error: 'Invalid project path encoding' }));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// GET /api/projects/:path/status
|
|
160
|
+
if (req.method === 'GET' && action === 'status') {
|
|
161
|
+
const instances = await getInstances();
|
|
162
|
+
const instance = instances.find((i) => i.projectPath === projectPath);
|
|
163
|
+
if (!instance) {
|
|
164
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
165
|
+
res.end(JSON.stringify({ error: 'Project not found' }));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
169
|
+
res.end(JSON.stringify({
|
|
170
|
+
path: instance.projectPath,
|
|
171
|
+
name: instance.projectName,
|
|
172
|
+
active: instance.running,
|
|
173
|
+
terminals: instance.terminals,
|
|
174
|
+
gateStatus: instance.gateStatus,
|
|
175
|
+
}));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// POST /api/projects/:path/activate
|
|
179
|
+
if (req.method === 'POST' && action === 'activate') {
|
|
180
|
+
// Rate limiting: 10 activations per minute per client
|
|
181
|
+
const clientIp = req.socket.remoteAddress || '127.0.0.1';
|
|
182
|
+
if (isRateLimited(clientIp)) {
|
|
183
|
+
res.writeHead(429, { 'Content-Type': 'application/json' });
|
|
184
|
+
res.end(JSON.stringify({ error: 'Too many activations, try again later' }));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const result = await launchInstance(projectPath);
|
|
188
|
+
if (result.success) {
|
|
189
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
190
|
+
res.end(JSON.stringify({ success: true, adopted: result.adopted }));
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
194
|
+
res.end(JSON.stringify({ success: false, error: result.error }));
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// POST /api/projects/:path/deactivate
|
|
199
|
+
if (req.method === 'POST' && action === 'deactivate') {
|
|
200
|
+
const knownPaths = getKnownProjectPaths();
|
|
201
|
+
const resolvedPath = fs.existsSync(projectPath) ? fs.realpathSync(projectPath) : projectPath;
|
|
202
|
+
const isKnown = knownPaths.some((p) => p === projectPath || p === resolvedPath);
|
|
203
|
+
if (!isKnown) {
|
|
204
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
205
|
+
res.end(JSON.stringify({ ok: false, error: 'Project not found' }));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const result = await stopInstance(projectPath);
|
|
209
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
210
|
+
res.end(JSON.stringify(result));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async function handleTerminalCreate(req, res, ctx) {
|
|
215
|
+
try {
|
|
216
|
+
const body = await parseJsonBody(req);
|
|
217
|
+
const manager = getTerminalManager();
|
|
218
|
+
// Parse request fields
|
|
219
|
+
const command = typeof body.command === 'string' ? body.command : undefined;
|
|
220
|
+
const args = Array.isArray(body.args) ? body.args : undefined;
|
|
221
|
+
const cols = typeof body.cols === 'number' ? body.cols : undefined;
|
|
222
|
+
const rows = typeof body.rows === 'number' ? body.rows : undefined;
|
|
223
|
+
const cwd = typeof body.cwd === 'string' ? body.cwd : undefined;
|
|
224
|
+
const env = typeof body.env === 'object' && body.env !== null ? body.env : undefined;
|
|
225
|
+
const label = typeof body.label === 'string' ? body.label : undefined;
|
|
226
|
+
// Optional session persistence via shellper
|
|
227
|
+
const projectPath = typeof body.projectPath === 'string' ? body.projectPath : null;
|
|
228
|
+
const termType = typeof body.type === 'string' && ['builder', 'shell'].includes(body.type) ? body.type : null;
|
|
229
|
+
const roleId = typeof body.roleId === 'string' ? body.roleId : null;
|
|
230
|
+
const requestPersistence = body.persistent === true;
|
|
231
|
+
let info;
|
|
232
|
+
let persistent = false;
|
|
233
|
+
// Try shellper if persistence was requested
|
|
234
|
+
const shellperManager = ctx.getShellperManager();
|
|
235
|
+
if (requestPersistence && shellperManager && command && cwd) {
|
|
236
|
+
try {
|
|
237
|
+
const sessionId = crypto.randomUUID();
|
|
238
|
+
// Strip CLAUDECODE so spawned Claude processes don't detect nesting
|
|
239
|
+
const sessionEnv = { ...(env || process.env) };
|
|
240
|
+
delete sessionEnv['CLAUDECODE'];
|
|
241
|
+
const client = await shellperManager.createSession({
|
|
242
|
+
sessionId,
|
|
243
|
+
command,
|
|
244
|
+
args: args || [],
|
|
245
|
+
cwd,
|
|
246
|
+
env: sessionEnv,
|
|
247
|
+
cols: cols || 200,
|
|
248
|
+
rows: 50,
|
|
249
|
+
restartOnExit: false,
|
|
250
|
+
});
|
|
251
|
+
const replayData = client.getReplayData() ?? Buffer.alloc(0);
|
|
252
|
+
const shellperInfo = shellperManager.getSessionInfo(sessionId);
|
|
253
|
+
const session = manager.createSessionRaw({
|
|
254
|
+
label: label || `terminal-${sessionId.slice(0, 8)}`,
|
|
255
|
+
cwd,
|
|
256
|
+
});
|
|
257
|
+
const ptySession = manager.getSession(session.id);
|
|
258
|
+
if (ptySession) {
|
|
259
|
+
ptySession.attachShellper(client, replayData, shellperInfo.pid, sessionId);
|
|
260
|
+
}
|
|
261
|
+
info = session;
|
|
262
|
+
persistent = true;
|
|
263
|
+
if (projectPath && termType && roleId) {
|
|
264
|
+
const entry = getProjectTerminalsEntry(normalizeProjectPath(projectPath));
|
|
265
|
+
if (termType === 'builder') {
|
|
266
|
+
entry.builders.set(roleId, session.id);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
entry.shells.set(roleId, session.id);
|
|
270
|
+
}
|
|
271
|
+
saveTerminalSession(session.id, projectPath, termType, roleId, shellperInfo.pid, shellperInfo.socketPath, shellperInfo.pid, shellperInfo.startTime);
|
|
272
|
+
ctx.log('INFO', `Registered shellper terminal ${session.id} as ${termType} "${roleId}" for project ${projectPath}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch (shellperErr) {
|
|
276
|
+
ctx.log('WARN', `Shellper creation failed for terminal, falling back: ${shellperErr.message}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// Fallback: non-persistent session (graceful degradation per plan)
|
|
280
|
+
// Shellper is the only persistence backend for new sessions.
|
|
281
|
+
if (!info) {
|
|
282
|
+
info = await manager.createSession({ command, args, cols, rows, cwd, env, label });
|
|
283
|
+
persistent = false;
|
|
284
|
+
if (projectPath && termType && roleId) {
|
|
285
|
+
const entry = getProjectTerminalsEntry(normalizeProjectPath(projectPath));
|
|
286
|
+
if (termType === 'builder') {
|
|
287
|
+
entry.builders.set(roleId, info.id);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
entry.shells.set(roleId, info.id);
|
|
291
|
+
}
|
|
292
|
+
saveTerminalSession(info.id, projectPath, termType, roleId, info.pid);
|
|
293
|
+
ctx.log('WARN', `Terminal ${info.id} for ${projectPath} is non-persistent (shellper unavailable)`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
297
|
+
res.end(JSON.stringify({ ...info, wsPath: `/ws/terminal/${info.id}`, persistent }));
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
301
|
+
ctx.log('ERROR', `Failed to create terminal: ${message}`);
|
|
302
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
303
|
+
res.end(JSON.stringify({ error: 'INTERNAL_ERROR', message }));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function handleTerminalList(res) {
|
|
307
|
+
const manager = getTerminalManager();
|
|
308
|
+
const terminals = manager.listSessions();
|
|
309
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
310
|
+
res.end(JSON.stringify({ terminals }));
|
|
311
|
+
}
|
|
312
|
+
async function handleTerminalRoutes(req, res, url, match) {
|
|
313
|
+
const [, terminalId, subpath] = match;
|
|
314
|
+
const manager = getTerminalManager();
|
|
315
|
+
// GET /api/terminals/:id - Get terminal info
|
|
316
|
+
if (req.method === 'GET' && (!subpath || subpath === '')) {
|
|
317
|
+
const session = manager.getSession(terminalId);
|
|
318
|
+
if (!session) {
|
|
319
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
320
|
+
res.end(JSON.stringify({ error: 'NOT_FOUND', message: `Session ${terminalId} not found` }));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
324
|
+
res.end(JSON.stringify(session.info));
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
// DELETE /api/terminals/:id - Kill terminal (disable shellper auto-restart if applicable)
|
|
328
|
+
if (req.method === 'DELETE' && (!subpath || subpath === '')) {
|
|
329
|
+
if (!(await killTerminalWithShellper(manager, terminalId))) {
|
|
330
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
331
|
+
res.end(JSON.stringify({ error: 'NOT_FOUND', message: `Session ${terminalId} not found` }));
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
// TICK-001: Delete from SQLite
|
|
335
|
+
deleteTerminalSession(terminalId);
|
|
336
|
+
res.writeHead(204);
|
|
337
|
+
res.end();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
// POST /api/terminals/:id/write - Write data to terminal (Spec 0104)
|
|
341
|
+
if (req.method === 'POST' && subpath === '/write') {
|
|
342
|
+
try {
|
|
343
|
+
const body = await parseJsonBody(req);
|
|
344
|
+
if (typeof body.data !== 'string') {
|
|
345
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
346
|
+
res.end(JSON.stringify({ error: 'INVALID_PARAMS', message: 'data must be a string' }));
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const session = manager.getSession(terminalId);
|
|
350
|
+
if (!session) {
|
|
351
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
352
|
+
res.end(JSON.stringify({ error: 'NOT_FOUND', message: `Session ${terminalId} not found` }));
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
session.write(body.data);
|
|
356
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
357
|
+
res.end(JSON.stringify({ ok: true }));
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
361
|
+
res.end(JSON.stringify({ error: 'INVALID_PARAMS', message: 'Invalid JSON body' }));
|
|
362
|
+
}
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
// POST /api/terminals/:id/resize - Resize terminal
|
|
366
|
+
if (req.method === 'POST' && subpath === '/resize') {
|
|
367
|
+
try {
|
|
368
|
+
const body = await parseJsonBody(req);
|
|
369
|
+
if (typeof body.cols !== 'number' || typeof body.rows !== 'number') {
|
|
370
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
371
|
+
res.end(JSON.stringify({ error: 'INVALID_PARAMS', message: 'cols and rows must be numbers' }));
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const info = manager.resizeSession(terminalId, body.cols, body.rows);
|
|
375
|
+
if (!info) {
|
|
376
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
377
|
+
res.end(JSON.stringify({ error: 'NOT_FOUND', message: `Session ${terminalId} not found` }));
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
381
|
+
res.end(JSON.stringify(info));
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
385
|
+
res.end(JSON.stringify({ error: 'INVALID_PARAMS', message: 'Invalid JSON body' }));
|
|
386
|
+
}
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
// GET /api/terminals/:id/output - Get terminal output
|
|
390
|
+
if (req.method === 'GET' && subpath === '/output') {
|
|
391
|
+
const lines = parseInt(url.searchParams.get('lines') ?? '100', 10);
|
|
392
|
+
const offset = parseInt(url.searchParams.get('offset') ?? '0', 10);
|
|
393
|
+
const output = manager.getOutput(terminalId, lines, offset);
|
|
394
|
+
if (!output) {
|
|
395
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
396
|
+
res.end(JSON.stringify({ error: 'NOT_FOUND', message: `Session ${terminalId} not found` }));
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
400
|
+
res.end(JSON.stringify(output));
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
async function handleStatus(res) {
|
|
405
|
+
const instances = await getInstances();
|
|
406
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
407
|
+
res.end(JSON.stringify({ instances }));
|
|
408
|
+
}
|
|
409
|
+
function handleSSEEvents(req, res, ctx) {
|
|
410
|
+
const clientId = crypto.randomBytes(8).toString('hex');
|
|
411
|
+
res.writeHead(200, {
|
|
412
|
+
'Content-Type': 'text/event-stream',
|
|
413
|
+
'Cache-Control': 'no-cache',
|
|
414
|
+
Connection: 'keep-alive',
|
|
415
|
+
});
|
|
416
|
+
// Send initial connection event
|
|
417
|
+
res.write(`data: ${JSON.stringify({ type: 'connected', id: clientId })}\n\n`);
|
|
418
|
+
const client = { res, id: clientId };
|
|
419
|
+
ctx.addSseClient(client);
|
|
420
|
+
ctx.log('INFO', `SSE client connected: ${clientId}`);
|
|
421
|
+
// Clean up on disconnect
|
|
422
|
+
req.on('close', () => {
|
|
423
|
+
ctx.removeSseClient(clientId);
|
|
424
|
+
ctx.log('INFO', `SSE client disconnected: ${clientId}`);
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
async function handleNotify(req, res, ctx) {
|
|
428
|
+
const body = await parseJsonBody(req);
|
|
429
|
+
const type = typeof body.type === 'string' ? body.type : 'info';
|
|
430
|
+
const title = typeof body.title === 'string' ? body.title : '';
|
|
431
|
+
const messageBody = typeof body.body === 'string' ? body.body : '';
|
|
432
|
+
const project = typeof body.project === 'string' ? body.project : undefined;
|
|
433
|
+
if (!title || !messageBody) {
|
|
434
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
435
|
+
res.end(JSON.stringify({ success: false, error: 'Missing title or body' }));
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
// Broadcast to all connected SSE clients
|
|
439
|
+
ctx.broadcastNotification({
|
|
440
|
+
type,
|
|
441
|
+
title,
|
|
442
|
+
body: messageBody,
|
|
443
|
+
project,
|
|
444
|
+
});
|
|
445
|
+
ctx.log('INFO', `Notification broadcast: ${title}`);
|
|
446
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
447
|
+
res.end(JSON.stringify({ success: true }));
|
|
448
|
+
}
|
|
449
|
+
async function handleBrowse(res, url) {
|
|
450
|
+
const inputPath = url.searchParams.get('path') || '';
|
|
451
|
+
try {
|
|
452
|
+
const suggestions = await getDirectorySuggestions(inputPath);
|
|
453
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
454
|
+
res.end(JSON.stringify({ suggestions }));
|
|
455
|
+
}
|
|
456
|
+
catch (err) {
|
|
457
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
458
|
+
res.end(JSON.stringify({ suggestions: [], error: err.message }));
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
async function handleCreateProject(req, res, ctx) {
|
|
462
|
+
const body = await parseJsonBody(req);
|
|
463
|
+
const parentPath = body.parent;
|
|
464
|
+
const projectName = body.name;
|
|
465
|
+
if (!parentPath || !projectName) {
|
|
466
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
467
|
+
res.end(JSON.stringify({ success: false, error: 'Missing parent or name' }));
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
// Validate project name
|
|
471
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(projectName)) {
|
|
472
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
473
|
+
res.end(JSON.stringify({ success: false, error: 'Invalid project name' }));
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
// Expand ~ to home directory
|
|
477
|
+
let expandedParent = parentPath;
|
|
478
|
+
if (expandedParent.startsWith('~')) {
|
|
479
|
+
expandedParent = expandedParent.replace('~', homedir());
|
|
480
|
+
}
|
|
481
|
+
// Validate parent exists
|
|
482
|
+
if (!fs.existsSync(expandedParent)) {
|
|
483
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
484
|
+
res.end(JSON.stringify({ success: false, error: `Parent directory does not exist: ${parentPath}` }));
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const projectPath = path.join(expandedParent, projectName);
|
|
488
|
+
// Check if project already exists
|
|
489
|
+
if (fs.existsSync(projectPath)) {
|
|
490
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
491
|
+
res.end(JSON.stringify({ success: false, error: `Directory already exists: ${projectPath}` }));
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
// Run codev init (it creates the directory)
|
|
496
|
+
execSync(`codev init --yes "${projectName}"`, {
|
|
497
|
+
cwd: expandedParent,
|
|
498
|
+
stdio: 'pipe',
|
|
499
|
+
timeout: 60000,
|
|
500
|
+
});
|
|
501
|
+
// Launch the instance
|
|
502
|
+
const launchResult = await launchInstance(projectPath);
|
|
503
|
+
if (!launchResult.success) {
|
|
504
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
505
|
+
res.end(JSON.stringify({ success: false, error: launchResult.error }));
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
509
|
+
res.end(JSON.stringify({ success: true, projectPath }));
|
|
510
|
+
}
|
|
511
|
+
catch (err) {
|
|
512
|
+
// Clean up on failure
|
|
513
|
+
try {
|
|
514
|
+
if (fs.existsSync(projectPath)) {
|
|
515
|
+
fs.rmSync(projectPath, { recursive: true });
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
// Ignore cleanup errors
|
|
520
|
+
}
|
|
521
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
522
|
+
res.end(JSON.stringify({ success: false, error: `Failed to create project: ${err.message}` }));
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
async function handleLaunchInstance(req, res) {
|
|
526
|
+
const body = await parseJsonBody(req);
|
|
527
|
+
let projectPath = body.projectPath;
|
|
528
|
+
if (!projectPath) {
|
|
529
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
530
|
+
res.end(JSON.stringify({ success: false, error: 'Missing projectPath' }));
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
// Expand ~ to home directory
|
|
534
|
+
if (projectPath.startsWith('~')) {
|
|
535
|
+
projectPath = projectPath.replace('~', homedir());
|
|
536
|
+
}
|
|
537
|
+
// Reject relative paths — tower daemon CWD is unpredictable
|
|
538
|
+
if (!path.isAbsolute(projectPath)) {
|
|
539
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
540
|
+
res.end(JSON.stringify({
|
|
541
|
+
success: false,
|
|
542
|
+
error: `Relative paths are not supported. Use an absolute path (e.g., /Users/.../project or ~/Development/project).`,
|
|
543
|
+
}));
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
// Normalize path (resolve .. segments, trailing slashes)
|
|
547
|
+
projectPath = path.resolve(projectPath);
|
|
548
|
+
const result = await launchInstance(projectPath);
|
|
549
|
+
res.writeHead(result.success ? 200 : 400, { 'Content-Type': 'application/json' });
|
|
550
|
+
res.end(JSON.stringify(result));
|
|
551
|
+
}
|
|
552
|
+
async function handleStopInstance(req, res) {
|
|
553
|
+
const body = await parseJsonBody(req);
|
|
554
|
+
const targetPath = body.projectPath;
|
|
555
|
+
if (!targetPath) {
|
|
556
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
557
|
+
res.end(JSON.stringify({ success: false, error: 'Missing projectPath' }));
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const result = await stopInstance(targetPath);
|
|
561
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
562
|
+
res.end(JSON.stringify(result));
|
|
563
|
+
}
|
|
564
|
+
function handleDashboard(res, ctx) {
|
|
565
|
+
if (!ctx.templatePath) {
|
|
566
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
567
|
+
res.end('Template not found. Make sure tower.html exists in agent-farm/templates/');
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
try {
|
|
571
|
+
const template = fs.readFileSync(ctx.templatePath, 'utf-8');
|
|
572
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
573
|
+
res.end(template);
|
|
574
|
+
}
|
|
575
|
+
catch (err) {
|
|
576
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
577
|
+
res.end('Error loading template: ' + err.message);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
// ============================================================================
|
|
581
|
+
// Project-scoped route handler
|
|
582
|
+
// ============================================================================
|
|
583
|
+
async function handleProjectRoutes(req, res, ctx, url) {
|
|
584
|
+
const pathParts = url.pathname.split('/');
|
|
585
|
+
// ['', 'project', base64urlPath, ...rest]
|
|
586
|
+
const encodedPath = pathParts[2];
|
|
587
|
+
const subPath = pathParts.slice(3).join('/');
|
|
588
|
+
if (!encodedPath) {
|
|
589
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
590
|
+
res.end(JSON.stringify({ error: 'Missing project path' }));
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
// Decode Base64URL (RFC 4648)
|
|
594
|
+
let projectPath;
|
|
595
|
+
try {
|
|
596
|
+
projectPath = Buffer.from(encodedPath, 'base64url').toString('utf-8');
|
|
597
|
+
// Support both POSIX (/) and Windows (C:\) paths
|
|
598
|
+
if (!projectPath || (!projectPath.startsWith('/') && !/^[A-Za-z]:[\\/]/.test(projectPath))) {
|
|
599
|
+
throw new Error('Invalid project path');
|
|
600
|
+
}
|
|
601
|
+
// Normalize to resolve symlinks (e.g. /var/folders → /private/var/folders on macOS)
|
|
602
|
+
projectPath = normalizeProjectPath(projectPath);
|
|
603
|
+
}
|
|
604
|
+
catch {
|
|
605
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
606
|
+
res.end(JSON.stringify({ error: 'Invalid project path encoding' }));
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
// Phase 4 (Spec 0090): Tower handles everything directly
|
|
610
|
+
const isApiCall = subPath.startsWith('api/') || subPath === 'api';
|
|
611
|
+
const isWsPath = subPath.startsWith('ws/') || subPath === 'ws';
|
|
612
|
+
// Tunnel endpoints are tower-level, not project-scoped, but the React
|
|
613
|
+
// dashboard uses relative paths (./api/tunnel/...) which resolve to
|
|
614
|
+
// /project/<encoded>/api/tunnel/... in project context. Handle here by
|
|
615
|
+
// extracting the tunnel sub-path and dispatching to handleTunnelEndpoint().
|
|
616
|
+
if (subPath.startsWith('api/tunnel/')) {
|
|
617
|
+
const tunnelSub = subPath.slice('api/tunnel/'.length); // e.g. "status", "connect", "disconnect"
|
|
618
|
+
await handleTunnelEndpoint(req, res, tunnelSub);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
// GET /file?path=<relative-path> — Read project file by path (for StatusPanel project list)
|
|
622
|
+
if (req.method === 'GET' && subPath === 'file' && url.searchParams.has('path')) {
|
|
623
|
+
const relPath = url.searchParams.get('path');
|
|
624
|
+
const fullPath = path.resolve(projectPath, relPath);
|
|
625
|
+
// Security: ensure resolved path stays within project directory
|
|
626
|
+
if (!fullPath.startsWith(projectPath + path.sep) && fullPath !== projectPath) {
|
|
627
|
+
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
628
|
+
res.end('Forbidden');
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
try {
|
|
632
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
633
|
+
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
634
|
+
res.end(content);
|
|
635
|
+
}
|
|
636
|
+
catch {
|
|
637
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
638
|
+
res.end('Not found');
|
|
639
|
+
}
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
// Serve React dashboard static files directly if:
|
|
643
|
+
// 1. Not an API call
|
|
644
|
+
// 2. Not a WebSocket path
|
|
645
|
+
// 3. React dashboard is available
|
|
646
|
+
// 4. Project doesn't need to be running for static files
|
|
647
|
+
if (!isApiCall && !isWsPath && ctx.hasReactDashboard) {
|
|
648
|
+
// Determine which static file to serve
|
|
649
|
+
let staticPath;
|
|
650
|
+
if (!subPath || subPath === '' || subPath === 'index.html') {
|
|
651
|
+
staticPath = path.join(ctx.reactDashboardPath, 'index.html');
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
// Check if it's a static asset
|
|
655
|
+
staticPath = path.join(ctx.reactDashboardPath, subPath);
|
|
656
|
+
}
|
|
657
|
+
// Try to serve the static file
|
|
658
|
+
if (serveStaticFile(staticPath, res)) {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
// SPA fallback: serve index.html for client-side routing
|
|
662
|
+
const indexPath = path.join(ctx.reactDashboardPath, 'index.html');
|
|
663
|
+
if (serveStaticFile(indexPath, res)) {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
// Phase 4 (Spec 0090): Handle project APIs directly instead of proxying to dashboard-server
|
|
668
|
+
if (isApiCall) {
|
|
669
|
+
const apiPath = subPath.replace(/^api\/?/, '');
|
|
670
|
+
// GET /api/state - Return project state (architect, builders, shells)
|
|
671
|
+
if (req.method === 'GET' && (apiPath === 'state' || apiPath === '')) {
|
|
672
|
+
return handleProjectState(res, projectPath);
|
|
673
|
+
}
|
|
674
|
+
// POST /api/tabs/shell - Create a new shell terminal
|
|
675
|
+
if (req.method === 'POST' && apiPath === 'tabs/shell') {
|
|
676
|
+
return handleProjectShellCreate(res, ctx, projectPath);
|
|
677
|
+
}
|
|
678
|
+
// POST /api/tabs/file - Create a file tab (Spec 0092)
|
|
679
|
+
if (req.method === 'POST' && apiPath === 'tabs/file') {
|
|
680
|
+
return handleProjectFileTabCreate(req, res, ctx, projectPath);
|
|
681
|
+
}
|
|
682
|
+
// GET /api/file/:id - Get file content as JSON (Spec 0092)
|
|
683
|
+
const fileGetMatch = apiPath.match(/^file\/([^/]+)$/);
|
|
684
|
+
if (req.method === 'GET' && fileGetMatch) {
|
|
685
|
+
return handleProjectFileGet(res, ctx, projectPath, fileGetMatch[1]);
|
|
686
|
+
}
|
|
687
|
+
// GET /api/file/:id/raw - Get raw file content (for images/video) (Spec 0092)
|
|
688
|
+
const fileRawMatch = apiPath.match(/^file\/([^/]+)\/raw$/);
|
|
689
|
+
if (req.method === 'GET' && fileRawMatch) {
|
|
690
|
+
return handleProjectFileRaw(res, ctx, projectPath, fileRawMatch[1]);
|
|
691
|
+
}
|
|
692
|
+
// POST /api/file/:id/save - Save file content (Spec 0092)
|
|
693
|
+
const fileSaveMatch = apiPath.match(/^file\/([^/]+)\/save$/);
|
|
694
|
+
if (req.method === 'POST' && fileSaveMatch) {
|
|
695
|
+
return handleProjectFileSave(req, res, ctx, projectPath, fileSaveMatch[1]);
|
|
696
|
+
}
|
|
697
|
+
// DELETE /api/tabs/:id - Delete a terminal or file tab
|
|
698
|
+
const deleteMatch = apiPath.match(/^tabs\/(.+)$/);
|
|
699
|
+
if (req.method === 'DELETE' && deleteMatch) {
|
|
700
|
+
return handleProjectTabDelete(res, ctx, projectPath, deleteMatch[1]);
|
|
701
|
+
}
|
|
702
|
+
// POST /api/stop - Stop all terminals for project
|
|
703
|
+
if (req.method === 'POST' && apiPath === 'stop') {
|
|
704
|
+
return handleProjectStopAll(res, projectPath);
|
|
705
|
+
}
|
|
706
|
+
// GET /api/files - Return project directory tree for file browser (Spec 0092)
|
|
707
|
+
if (req.method === 'GET' && apiPath === 'files') {
|
|
708
|
+
return handleProjectFiles(res, url, projectPath);
|
|
709
|
+
}
|
|
710
|
+
// GET /api/git/status - Return git status for file browser (Spec 0092)
|
|
711
|
+
if (req.method === 'GET' && apiPath === 'git/status') {
|
|
712
|
+
return handleProjectGitStatus(res, ctx, projectPath);
|
|
713
|
+
}
|
|
714
|
+
// GET /api/files/recent - Return recently opened file tabs (Spec 0092)
|
|
715
|
+
if (req.method === 'GET' && apiPath === 'files/recent') {
|
|
716
|
+
return handleProjectRecentFiles(res, projectPath);
|
|
717
|
+
}
|
|
718
|
+
// GET /api/annotate/:tabId/* — Serve rich annotator template and sub-APIs
|
|
719
|
+
const annotateMatch = apiPath.match(/^annotate\/([^/]+)(\/(.*))?$/);
|
|
720
|
+
if (annotateMatch) {
|
|
721
|
+
return handleProjectAnnotate(req, res, ctx, url, projectPath, annotateMatch);
|
|
722
|
+
}
|
|
723
|
+
// POST /api/paste-image - Upload pasted image to temp file (Issue #252)
|
|
724
|
+
if (req.method === 'POST' && apiPath === 'paste-image') {
|
|
725
|
+
const MAX_IMAGE_SIZE = 10 * 1024 * 1024; // 10 MB
|
|
726
|
+
let size = 0;
|
|
727
|
+
const chunks = [];
|
|
728
|
+
let aborted = false;
|
|
729
|
+
req.on('data', (chunk) => {
|
|
730
|
+
size += chunk.length;
|
|
731
|
+
if (size > MAX_IMAGE_SIZE) {
|
|
732
|
+
aborted = true;
|
|
733
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
734
|
+
res.end(JSON.stringify({ error: 'Image too large (max 10 MB)' }));
|
|
735
|
+
req.destroy();
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
chunks.push(chunk);
|
|
739
|
+
});
|
|
740
|
+
req.on('end', () => {
|
|
741
|
+
if (aborted)
|
|
742
|
+
return;
|
|
743
|
+
try {
|
|
744
|
+
const buffer = Buffer.concat(chunks);
|
|
745
|
+
const contentType = req.headers['content-type'] || 'image/png';
|
|
746
|
+
const ext = contentType.includes('jpeg') || contentType.includes('jpg') ? '.jpg'
|
|
747
|
+
: contentType.includes('gif') ? '.gif'
|
|
748
|
+
: contentType.includes('webp') ? '.webp'
|
|
749
|
+
: '.png';
|
|
750
|
+
const filename = `paste-${crypto.randomUUID()}${ext}`;
|
|
751
|
+
const pasteDir = path.join(tmpdir(), 'codev-paste');
|
|
752
|
+
fs.mkdirSync(pasteDir, { recursive: true });
|
|
753
|
+
const filePath = path.join(pasteDir, filename);
|
|
754
|
+
fs.writeFileSync(filePath, buffer);
|
|
755
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
756
|
+
res.end(JSON.stringify({ path: filePath }));
|
|
757
|
+
}
|
|
758
|
+
catch (err) {
|
|
759
|
+
if (!res.headersSent) {
|
|
760
|
+
const status = err.message.includes('too large') ? 413 : 500;
|
|
761
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
762
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
req.on('error', (err) => {
|
|
767
|
+
if (!res.headersSent) {
|
|
768
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
769
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
// Unhandled API route
|
|
775
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
776
|
+
res.end(JSON.stringify({ error: 'API endpoint not found', path: apiPath }));
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
// For WebSocket paths, let the upgrade handler deal with it
|
|
780
|
+
if (isWsPath) {
|
|
781
|
+
// WebSocket paths are handled by the upgrade handler
|
|
782
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
783
|
+
res.end('WebSocket connections should use ws:// protocol');
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
// If we get here for non-API, non-WS paths and React dashboard is not available
|
|
787
|
+
if (!ctx.hasReactDashboard) {
|
|
788
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
789
|
+
res.end('Dashboard not available');
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
// Fallback for unmatched paths
|
|
793
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
794
|
+
res.end('Not found');
|
|
795
|
+
}
|
|
796
|
+
// ============================================================================
|
|
797
|
+
// Project API sub-handlers
|
|
798
|
+
// ============================================================================
|
|
799
|
+
async function handleProjectState(res, projectPath) {
|
|
800
|
+
// Refresh cache via getTerminalsForProject (handles SQLite sync
|
|
801
|
+
// and shellper reconnection in one place)
|
|
802
|
+
const encodedPath = Buffer.from(projectPath).toString('base64url');
|
|
803
|
+
const proxyUrl = `/project/${encodedPath}/`;
|
|
804
|
+
const { gateStatus } = await getTerminalsForProject(projectPath, proxyUrl);
|
|
805
|
+
// Now read from the refreshed cache
|
|
806
|
+
const entry = getProjectTerminalsEntry(projectPath);
|
|
807
|
+
const manager = getTerminalManager();
|
|
808
|
+
const state = {
|
|
809
|
+
architect: null,
|
|
810
|
+
builders: [],
|
|
811
|
+
utils: [],
|
|
812
|
+
annotations: [],
|
|
813
|
+
projectName: path.basename(projectPath),
|
|
814
|
+
gateStatus,
|
|
815
|
+
};
|
|
816
|
+
// Add architect if exists
|
|
817
|
+
if (entry.architect) {
|
|
818
|
+
const session = manager.getSession(entry.architect);
|
|
819
|
+
if (session) {
|
|
820
|
+
state.architect = {
|
|
821
|
+
port: 0,
|
|
822
|
+
pid: session.pid || 0,
|
|
823
|
+
terminalId: entry.architect,
|
|
824
|
+
persistent: isSessionPersistent(entry.architect, session),
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
// Add shells from refreshed cache
|
|
829
|
+
for (const [shellId, terminalId] of entry.shells) {
|
|
830
|
+
const session = manager.getSession(terminalId);
|
|
831
|
+
if (session) {
|
|
832
|
+
state.utils.push({
|
|
833
|
+
id: shellId,
|
|
834
|
+
name: `Shell ${shellId.replace('shell-', '')}`,
|
|
835
|
+
port: 0,
|
|
836
|
+
pid: session.pid || 0,
|
|
837
|
+
terminalId,
|
|
838
|
+
persistent: isSessionPersistent(terminalId, session),
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
// Add builders from refreshed cache
|
|
843
|
+
for (const [builderId, terminalId] of entry.builders) {
|
|
844
|
+
const session = manager.getSession(terminalId);
|
|
845
|
+
if (session) {
|
|
846
|
+
state.builders.push({
|
|
847
|
+
id: builderId,
|
|
848
|
+
name: `Builder ${builderId}`,
|
|
849
|
+
port: 0,
|
|
850
|
+
pid: session.pid || 0,
|
|
851
|
+
status: 'running',
|
|
852
|
+
phase: '',
|
|
853
|
+
worktree: '',
|
|
854
|
+
branch: '',
|
|
855
|
+
type: 'spec',
|
|
856
|
+
terminalId,
|
|
857
|
+
persistent: isSessionPersistent(terminalId, session),
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
// Add file tabs (Spec 0092 - served through Tower, no separate ports)
|
|
862
|
+
for (const [tabId, tab] of entry.fileTabs) {
|
|
863
|
+
state.annotations.push({
|
|
864
|
+
id: tabId,
|
|
865
|
+
file: tab.path,
|
|
866
|
+
port: 0, // No separate port - served through Tower
|
|
867
|
+
pid: 0, // No separate process
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
871
|
+
res.end(JSON.stringify(state));
|
|
872
|
+
}
|
|
873
|
+
async function handleProjectShellCreate(res, ctx, projectPath) {
|
|
874
|
+
try {
|
|
875
|
+
const manager = getTerminalManager();
|
|
876
|
+
const shellId = getNextShellId(projectPath);
|
|
877
|
+
const shellCmd = process.env.SHELL || '/bin/bash';
|
|
878
|
+
const shellArgs = [];
|
|
879
|
+
let shellCreated = false;
|
|
880
|
+
// Try shellper first for persistent shell session
|
|
881
|
+
const shellperManager = ctx.getShellperManager();
|
|
882
|
+
if (shellperManager) {
|
|
883
|
+
try {
|
|
884
|
+
const sessionId = crypto.randomUUID();
|
|
885
|
+
// Strip CLAUDECODE so spawned Claude processes don't detect nesting
|
|
886
|
+
const shellEnv = { ...process.env };
|
|
887
|
+
delete shellEnv['CLAUDECODE'];
|
|
888
|
+
const client = await shellperManager.createSession({
|
|
889
|
+
sessionId,
|
|
890
|
+
command: shellCmd,
|
|
891
|
+
args: shellArgs,
|
|
892
|
+
cwd: projectPath,
|
|
893
|
+
env: shellEnv,
|
|
894
|
+
cols: 200,
|
|
895
|
+
rows: 50,
|
|
896
|
+
restartOnExit: false,
|
|
897
|
+
});
|
|
898
|
+
const replayData = client.getReplayData() ?? Buffer.alloc(0);
|
|
899
|
+
const shellperInfo = shellperManager.getSessionInfo(sessionId);
|
|
900
|
+
const session = manager.createSessionRaw({
|
|
901
|
+
label: `Shell ${shellId.replace('shell-', '')}`,
|
|
902
|
+
cwd: projectPath,
|
|
903
|
+
});
|
|
904
|
+
const ptySession = manager.getSession(session.id);
|
|
905
|
+
if (ptySession) {
|
|
906
|
+
ptySession.attachShellper(client, replayData, shellperInfo.pid, sessionId);
|
|
907
|
+
}
|
|
908
|
+
const entry = getProjectTerminalsEntry(projectPath);
|
|
909
|
+
entry.shells.set(shellId, session.id);
|
|
910
|
+
saveTerminalSession(session.id, projectPath, 'shell', shellId, shellperInfo.pid, shellperInfo.socketPath, shellperInfo.pid, shellperInfo.startTime);
|
|
911
|
+
shellCreated = true;
|
|
912
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
913
|
+
res.end(JSON.stringify({
|
|
914
|
+
id: shellId,
|
|
915
|
+
port: 0,
|
|
916
|
+
name: `Shell ${shellId.replace('shell-', '')}`,
|
|
917
|
+
terminalId: session.id,
|
|
918
|
+
persistent: true,
|
|
919
|
+
}));
|
|
920
|
+
}
|
|
921
|
+
catch (shellperErr) {
|
|
922
|
+
ctx.log('WARN', `Shellper creation failed for shell, falling back: ${shellperErr.message}`);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
// Fallback: non-persistent session (graceful degradation per plan)
|
|
926
|
+
// Shellper is the only persistence backend for new sessions.
|
|
927
|
+
if (!shellCreated) {
|
|
928
|
+
const session = await manager.createSession({
|
|
929
|
+
command: shellCmd,
|
|
930
|
+
args: shellArgs,
|
|
931
|
+
cwd: projectPath,
|
|
932
|
+
label: `Shell ${shellId.replace('shell-', '')}`,
|
|
933
|
+
env: process.env,
|
|
934
|
+
});
|
|
935
|
+
const entry = getProjectTerminalsEntry(projectPath);
|
|
936
|
+
entry.shells.set(shellId, session.id);
|
|
937
|
+
saveTerminalSession(session.id, projectPath, 'shell', shellId, session.pid);
|
|
938
|
+
ctx.log('WARN', `Shell ${shellId} for ${projectPath} is non-persistent (shellper unavailable)`);
|
|
939
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
940
|
+
res.end(JSON.stringify({
|
|
941
|
+
id: shellId,
|
|
942
|
+
port: 0,
|
|
943
|
+
name: `Shell ${shellId.replace('shell-', '')}`,
|
|
944
|
+
terminalId: session.id,
|
|
945
|
+
persistent: false,
|
|
946
|
+
}));
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
catch (err) {
|
|
950
|
+
ctx.log('ERROR', `Failed to create shell: ${err.message}`);
|
|
951
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
952
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
async function handleProjectFileTabCreate(req, res, ctx, projectPath) {
|
|
956
|
+
try {
|
|
957
|
+
const body = await readBody(req);
|
|
958
|
+
const { path: filePath, line, terminalId } = JSON.parse(body || '{}');
|
|
959
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
960
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
961
|
+
res.end(JSON.stringify({ error: 'Missing path parameter' }));
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
// Resolve path: use terminal's cwd for relative paths when terminalId is provided
|
|
965
|
+
let fullPath;
|
|
966
|
+
if (path.isAbsolute(filePath)) {
|
|
967
|
+
fullPath = filePath;
|
|
968
|
+
}
|
|
969
|
+
else if (terminalId) {
|
|
970
|
+
const manager = getTerminalManager();
|
|
971
|
+
const session = manager.getSession(terminalId);
|
|
972
|
+
if (session) {
|
|
973
|
+
fullPath = path.join(session.cwd, filePath);
|
|
974
|
+
}
|
|
975
|
+
else {
|
|
976
|
+
ctx.log('WARN', `Terminal session ${terminalId} not found, falling back to project root`);
|
|
977
|
+
fullPath = path.join(projectPath, filePath);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
else {
|
|
981
|
+
fullPath = path.join(projectPath, filePath);
|
|
982
|
+
}
|
|
983
|
+
// Security: symlink-aware containment check
|
|
984
|
+
// For non-existent files, resolve the parent directory to handle
|
|
985
|
+
// intermediate symlinks (e.g., /tmp -> /private/tmp on macOS).
|
|
986
|
+
let resolvedPath;
|
|
987
|
+
try {
|
|
988
|
+
resolvedPath = fs.realpathSync(fullPath);
|
|
989
|
+
}
|
|
990
|
+
catch {
|
|
991
|
+
try {
|
|
992
|
+
resolvedPath = path.join(fs.realpathSync(path.dirname(fullPath)), path.basename(fullPath));
|
|
993
|
+
}
|
|
994
|
+
catch {
|
|
995
|
+
resolvedPath = path.resolve(fullPath);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
let normalizedProject;
|
|
999
|
+
try {
|
|
1000
|
+
normalizedProject = fs.realpathSync(projectPath);
|
|
1001
|
+
}
|
|
1002
|
+
catch {
|
|
1003
|
+
normalizedProject = path.resolve(projectPath);
|
|
1004
|
+
}
|
|
1005
|
+
const isWithinProject = resolvedPath.startsWith(normalizedProject + path.sep)
|
|
1006
|
+
|| resolvedPath === normalizedProject;
|
|
1007
|
+
if (!isWithinProject) {
|
|
1008
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1009
|
+
res.end(JSON.stringify({ error: 'Path outside project' }));
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
// Non-existent files still create a tab (spec 0101: file viewer shows "File not found")
|
|
1013
|
+
const fileExists = fs.existsSync(fullPath);
|
|
1014
|
+
const entry = getProjectTerminalsEntry(projectPath);
|
|
1015
|
+
// Check if already open
|
|
1016
|
+
for (const [id, tab] of entry.fileTabs) {
|
|
1017
|
+
if (tab.path === fullPath) {
|
|
1018
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1019
|
+
res.end(JSON.stringify({ id, existing: true, line, notFound: !fileExists }));
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
// Create new file tab (write-through: in-memory + SQLite)
|
|
1024
|
+
const id = `file-${crypto.randomUUID()}`;
|
|
1025
|
+
const createdAt = Date.now();
|
|
1026
|
+
entry.fileTabs.set(id, { id, path: fullPath, createdAt });
|
|
1027
|
+
saveFileTab(id, projectPath, fullPath, createdAt);
|
|
1028
|
+
ctx.log('INFO', `Created file tab: ${id} for ${path.basename(fullPath)}`);
|
|
1029
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1030
|
+
res.end(JSON.stringify({ id, existing: false, line, notFound: !fileExists }));
|
|
1031
|
+
}
|
|
1032
|
+
catch (err) {
|
|
1033
|
+
ctx.log('ERROR', `Failed to create file tab: ${err.message}`);
|
|
1034
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1035
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
function handleProjectFileGet(res, ctx, projectPath, tabId) {
|
|
1039
|
+
const entry = getProjectTerminalsEntry(projectPath);
|
|
1040
|
+
const tab = entry.fileTabs.get(tabId);
|
|
1041
|
+
if (!tab) {
|
|
1042
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1043
|
+
res.end(JSON.stringify({ error: 'File tab not found' }));
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
try {
|
|
1047
|
+
const ext = path.extname(tab.path).slice(1).toLowerCase();
|
|
1048
|
+
const isText = !['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'mp4', 'webm', 'mov', 'pdf'].includes(ext);
|
|
1049
|
+
if (isText) {
|
|
1050
|
+
const content = fs.readFileSync(tab.path, 'utf-8');
|
|
1051
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1052
|
+
res.end(JSON.stringify({
|
|
1053
|
+
path: tab.path,
|
|
1054
|
+
name: path.basename(tab.path),
|
|
1055
|
+
content,
|
|
1056
|
+
language: getLanguageForExt(ext),
|
|
1057
|
+
isMarkdown: ext === 'md',
|
|
1058
|
+
isImage: false,
|
|
1059
|
+
isVideo: false,
|
|
1060
|
+
}));
|
|
1061
|
+
}
|
|
1062
|
+
else {
|
|
1063
|
+
// For binary files, just return metadata
|
|
1064
|
+
const stat = fs.statSync(tab.path);
|
|
1065
|
+
const isImage = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(ext);
|
|
1066
|
+
const isVideo = ['mp4', 'webm', 'mov'].includes(ext);
|
|
1067
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1068
|
+
res.end(JSON.stringify({
|
|
1069
|
+
path: tab.path,
|
|
1070
|
+
name: path.basename(tab.path),
|
|
1071
|
+
content: null,
|
|
1072
|
+
language: ext,
|
|
1073
|
+
isMarkdown: false,
|
|
1074
|
+
isImage,
|
|
1075
|
+
isVideo,
|
|
1076
|
+
size: stat.size,
|
|
1077
|
+
}));
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
catch (err) {
|
|
1081
|
+
ctx.log('ERROR', `GET /api/file/:id failed: ${err.message}`);
|
|
1082
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1083
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
function handleProjectFileRaw(res, ctx, projectPath, tabId) {
|
|
1087
|
+
const entry = getProjectTerminalsEntry(projectPath);
|
|
1088
|
+
const tab = entry.fileTabs.get(tabId);
|
|
1089
|
+
if (!tab) {
|
|
1090
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1091
|
+
res.end(JSON.stringify({ error: 'File tab not found' }));
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
try {
|
|
1095
|
+
const data = fs.readFileSync(tab.path);
|
|
1096
|
+
const mimeType = getMimeTypeForFile(tab.path);
|
|
1097
|
+
res.writeHead(200, {
|
|
1098
|
+
'Content-Type': mimeType,
|
|
1099
|
+
'Content-Length': data.length,
|
|
1100
|
+
'Cache-Control': 'no-cache',
|
|
1101
|
+
});
|
|
1102
|
+
res.end(data);
|
|
1103
|
+
}
|
|
1104
|
+
catch (err) {
|
|
1105
|
+
ctx.log('ERROR', `GET /api/file/:id/raw failed: ${err.message}`);
|
|
1106
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1107
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
async function handleProjectFileSave(req, res, ctx, projectPath, tabId) {
|
|
1111
|
+
const entry = getProjectTerminalsEntry(projectPath);
|
|
1112
|
+
const tab = entry.fileTabs.get(tabId);
|
|
1113
|
+
if (!tab) {
|
|
1114
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1115
|
+
res.end(JSON.stringify({ error: 'File tab not found' }));
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
try {
|
|
1119
|
+
const body = await readBody(req);
|
|
1120
|
+
const { content } = JSON.parse(body || '{}');
|
|
1121
|
+
if (typeof content !== 'string') {
|
|
1122
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1123
|
+
res.end(JSON.stringify({ error: 'Missing content parameter' }));
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
fs.writeFileSync(tab.path, content, 'utf-8');
|
|
1127
|
+
ctx.log('INFO', `Saved file: ${tab.path}`);
|
|
1128
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1129
|
+
res.end(JSON.stringify({ success: true }));
|
|
1130
|
+
}
|
|
1131
|
+
catch (err) {
|
|
1132
|
+
ctx.log('ERROR', `POST /api/file/:id/save failed: ${err.message}`);
|
|
1133
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1134
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
async function handleProjectTabDelete(res, ctx, projectPath, tabId) {
|
|
1138
|
+
const entry = getProjectTerminalsEntry(projectPath);
|
|
1139
|
+
const manager = getTerminalManager();
|
|
1140
|
+
// Check if it's a file tab first (Spec 0092, write-through: in-memory + SQLite)
|
|
1141
|
+
if (tabId.startsWith('file-')) {
|
|
1142
|
+
if (entry.fileTabs.has(tabId)) {
|
|
1143
|
+
entry.fileTabs.delete(tabId);
|
|
1144
|
+
deleteFileTab(tabId);
|
|
1145
|
+
ctx.log('INFO', `Deleted file tab: ${tabId}`);
|
|
1146
|
+
res.writeHead(204);
|
|
1147
|
+
res.end();
|
|
1148
|
+
}
|
|
1149
|
+
else {
|
|
1150
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1151
|
+
res.end(JSON.stringify({ error: 'File tab not found' }));
|
|
1152
|
+
}
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
// Find and delete the terminal
|
|
1156
|
+
let terminalId;
|
|
1157
|
+
if (tabId.startsWith('shell-')) {
|
|
1158
|
+
terminalId = entry.shells.get(tabId);
|
|
1159
|
+
if (terminalId) {
|
|
1160
|
+
entry.shells.delete(tabId);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
else if (tabId.startsWith('builder-')) {
|
|
1164
|
+
terminalId = entry.builders.get(tabId);
|
|
1165
|
+
if (terminalId) {
|
|
1166
|
+
entry.builders.delete(tabId);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
else if (tabId === 'architect') {
|
|
1170
|
+
terminalId = entry.architect;
|
|
1171
|
+
if (terminalId) {
|
|
1172
|
+
entry.architect = undefined;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
if (terminalId) {
|
|
1176
|
+
// Disable shellper auto-restart if applicable, then kill the PtySession
|
|
1177
|
+
await killTerminalWithShellper(manager, terminalId);
|
|
1178
|
+
// TICK-001: Delete from SQLite
|
|
1179
|
+
deleteTerminalSession(terminalId);
|
|
1180
|
+
res.writeHead(204);
|
|
1181
|
+
res.end();
|
|
1182
|
+
}
|
|
1183
|
+
else {
|
|
1184
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1185
|
+
res.end(JSON.stringify({ error: 'Tab not found' }));
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
async function handleProjectStopAll(res, projectPath) {
|
|
1189
|
+
const entry = getProjectTerminalsEntry(projectPath);
|
|
1190
|
+
const manager = getTerminalManager();
|
|
1191
|
+
// Kill all terminals (disable shellper auto-restart if applicable)
|
|
1192
|
+
if (entry.architect) {
|
|
1193
|
+
await killTerminalWithShellper(manager, entry.architect);
|
|
1194
|
+
}
|
|
1195
|
+
for (const terminalId of entry.shells.values()) {
|
|
1196
|
+
await killTerminalWithShellper(manager, terminalId);
|
|
1197
|
+
}
|
|
1198
|
+
for (const terminalId of entry.builders.values()) {
|
|
1199
|
+
await killTerminalWithShellper(manager, terminalId);
|
|
1200
|
+
}
|
|
1201
|
+
// Clear registry
|
|
1202
|
+
getProjectTerminals().delete(projectPath);
|
|
1203
|
+
// TICK-001: Delete all terminal sessions from SQLite
|
|
1204
|
+
deleteProjectTerminalSessions(projectPath);
|
|
1205
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1206
|
+
res.end(JSON.stringify({ ok: true }));
|
|
1207
|
+
}
|
|
1208
|
+
function handleProjectFiles(res, url, projectPath) {
|
|
1209
|
+
const maxDepth = parseInt(url.searchParams.get('depth') || '3', 10);
|
|
1210
|
+
const ignore = new Set(['.git', 'node_modules', '.builders', 'dist', '.agent-farm', '.next', '.cache', '__pycache__']);
|
|
1211
|
+
function readTree(dir, depth) {
|
|
1212
|
+
if (depth <= 0)
|
|
1213
|
+
return [];
|
|
1214
|
+
try {
|
|
1215
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1216
|
+
return entries
|
|
1217
|
+
.filter(e => !e.name.startsWith('.') || e.name === '.env.example')
|
|
1218
|
+
.filter(e => !ignore.has(e.name))
|
|
1219
|
+
.sort((a, b) => {
|
|
1220
|
+
// Directories first, then alphabetical
|
|
1221
|
+
if (a.isDirectory() && !b.isDirectory())
|
|
1222
|
+
return -1;
|
|
1223
|
+
if (!a.isDirectory() && b.isDirectory())
|
|
1224
|
+
return 1;
|
|
1225
|
+
return a.name.localeCompare(b.name);
|
|
1226
|
+
})
|
|
1227
|
+
.map(e => {
|
|
1228
|
+
const fullPath = path.join(dir, e.name);
|
|
1229
|
+
const relativePath = path.relative(projectPath, fullPath);
|
|
1230
|
+
if (e.isDirectory()) {
|
|
1231
|
+
return { name: e.name, path: relativePath, type: 'directory', children: readTree(fullPath, depth - 1) };
|
|
1232
|
+
}
|
|
1233
|
+
return { name: e.name, path: relativePath, type: 'file' };
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
catch {
|
|
1237
|
+
return [];
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
const tree = readTree(projectPath, maxDepth);
|
|
1241
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1242
|
+
res.end(JSON.stringify(tree));
|
|
1243
|
+
}
|
|
1244
|
+
function handleProjectGitStatus(res, ctx, projectPath) {
|
|
1245
|
+
try {
|
|
1246
|
+
// Get git status in porcelain format for parsing
|
|
1247
|
+
const result = execSync('git status --porcelain', {
|
|
1248
|
+
cwd: projectPath,
|
|
1249
|
+
encoding: 'utf-8',
|
|
1250
|
+
timeout: 5000,
|
|
1251
|
+
});
|
|
1252
|
+
// Parse porcelain output: XY filename
|
|
1253
|
+
// X = staging area status, Y = working tree status
|
|
1254
|
+
const modified = [];
|
|
1255
|
+
const staged = [];
|
|
1256
|
+
const untracked = [];
|
|
1257
|
+
for (const line of result.split('\n')) {
|
|
1258
|
+
if (!line)
|
|
1259
|
+
continue;
|
|
1260
|
+
const x = line[0]; // staging area
|
|
1261
|
+
const y = line[1]; // working tree
|
|
1262
|
+
const filepath = line.slice(3);
|
|
1263
|
+
if (x === '?' && y === '?') {
|
|
1264
|
+
untracked.push(filepath);
|
|
1265
|
+
}
|
|
1266
|
+
else {
|
|
1267
|
+
if (x !== ' ' && x !== '?') {
|
|
1268
|
+
staged.push(filepath);
|
|
1269
|
+
}
|
|
1270
|
+
if (y !== ' ' && y !== '?') {
|
|
1271
|
+
modified.push(filepath);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1276
|
+
res.end(JSON.stringify({ modified, staged, untracked }));
|
|
1277
|
+
}
|
|
1278
|
+
catch (err) {
|
|
1279
|
+
// Not a git repo or git command failed — return graceful degradation with error field
|
|
1280
|
+
ctx.log('WARN', `GET /api/git/status failed: ${err.message}`);
|
|
1281
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1282
|
+
res.end(JSON.stringify({ modified: [], staged: [], untracked: [], error: err.message }));
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
function handleProjectRecentFiles(res, projectPath) {
|
|
1286
|
+
const entry = getProjectTerminalsEntry(projectPath);
|
|
1287
|
+
// Get all file tabs sorted by creation time (most recent first)
|
|
1288
|
+
const recentFiles = Array.from(entry.fileTabs.values())
|
|
1289
|
+
.sort((a, b) => b.createdAt - a.createdAt)
|
|
1290
|
+
.slice(0, 10) // Limit to 10 most recent
|
|
1291
|
+
.map(tab => ({
|
|
1292
|
+
id: tab.id,
|
|
1293
|
+
path: tab.path,
|
|
1294
|
+
name: path.basename(tab.path),
|
|
1295
|
+
relativePath: path.relative(projectPath, tab.path),
|
|
1296
|
+
}));
|
|
1297
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1298
|
+
res.end(JSON.stringify(recentFiles));
|
|
1299
|
+
}
|
|
1300
|
+
function handleProjectAnnotate(req, res, ctx, url, projectPath, annotateMatch) {
|
|
1301
|
+
const tabId = annotateMatch[1];
|
|
1302
|
+
const subRoute = annotateMatch[3] || '';
|
|
1303
|
+
const entry = getProjectTerminalsEntry(projectPath);
|
|
1304
|
+
const tab = entry.fileTabs.get(tabId);
|
|
1305
|
+
if (!tab) {
|
|
1306
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1307
|
+
res.end(JSON.stringify({ error: 'File tab not found' }));
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
const filePath = tab.path;
|
|
1311
|
+
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
1312
|
+
const isImage = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(ext);
|
|
1313
|
+
const isVideo = ['mp4', 'webm', 'mov'].includes(ext);
|
|
1314
|
+
const is3D = ['stl', '3mf'].includes(ext);
|
|
1315
|
+
const isPdf = ext === 'pdf';
|
|
1316
|
+
const isMarkdown = ext === 'md';
|
|
1317
|
+
// Sub-route: GET /file — re-read file content from disk
|
|
1318
|
+
if (req.method === 'GET' && subRoute === 'file') {
|
|
1319
|
+
try {
|
|
1320
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1321
|
+
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
1322
|
+
res.end(content);
|
|
1323
|
+
}
|
|
1324
|
+
catch (err) {
|
|
1325
|
+
ctx.log('ERROR', `GET /api/annotate/:id/file failed: ${err.message}`);
|
|
1326
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1327
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1328
|
+
}
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
// Sub-route: POST /save — save file content
|
|
1332
|
+
if (req.method === 'POST' && subRoute === 'save') {
|
|
1333
|
+
// Note: async body reading handled via callback pattern since this function is sync
|
|
1334
|
+
let data = '';
|
|
1335
|
+
req.on('data', (chunk) => data += chunk.toString());
|
|
1336
|
+
req.on('end', () => {
|
|
1337
|
+
try {
|
|
1338
|
+
const parsed = JSON.parse(data || '{}');
|
|
1339
|
+
const fileContent = parsed.content;
|
|
1340
|
+
if (typeof fileContent !== 'string') {
|
|
1341
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
1342
|
+
res.end('Missing content');
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
fs.writeFileSync(filePath, fileContent, 'utf-8');
|
|
1346
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1347
|
+
res.end(JSON.stringify({ ok: true }));
|
|
1348
|
+
}
|
|
1349
|
+
catch (err) {
|
|
1350
|
+
ctx.log('ERROR', `POST /api/annotate/:id/save failed: ${err.message}`);
|
|
1351
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1352
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
// Sub-route: GET /api/mtime — file modification time
|
|
1358
|
+
if (req.method === 'GET' && subRoute === 'api/mtime') {
|
|
1359
|
+
try {
|
|
1360
|
+
const stat = fs.statSync(filePath);
|
|
1361
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1362
|
+
res.end(JSON.stringify({ mtime: stat.mtimeMs }));
|
|
1363
|
+
}
|
|
1364
|
+
catch (err) {
|
|
1365
|
+
ctx.log('ERROR', `GET /api/annotate/:id/api/mtime failed: ${err.message}`);
|
|
1366
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1367
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1368
|
+
}
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
// Sub-route: GET /api/image, /api/video, /api/model, /api/pdf — raw binary content
|
|
1372
|
+
if (req.method === 'GET' && (subRoute === 'api/image' || subRoute === 'api/video' || subRoute === 'api/model' || subRoute === 'api/pdf')) {
|
|
1373
|
+
try {
|
|
1374
|
+
const data = fs.readFileSync(filePath);
|
|
1375
|
+
const mimeType = getMimeTypeForFile(filePath);
|
|
1376
|
+
res.writeHead(200, {
|
|
1377
|
+
'Content-Type': mimeType,
|
|
1378
|
+
'Content-Length': data.length,
|
|
1379
|
+
'Cache-Control': 'no-cache',
|
|
1380
|
+
});
|
|
1381
|
+
res.end(data);
|
|
1382
|
+
}
|
|
1383
|
+
catch (err) {
|
|
1384
|
+
ctx.log('ERROR', `GET /api/annotate/:id/${subRoute} failed: ${err.message}`);
|
|
1385
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1386
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1387
|
+
}
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
// Default: serve the annotator HTML template
|
|
1391
|
+
if (req.method === 'GET' && (subRoute === '' || subRoute === undefined)) {
|
|
1392
|
+
try {
|
|
1393
|
+
const templateFile = is3D ? '3d-viewer.html' : 'open.html';
|
|
1394
|
+
const tplPath = path.resolve(__dirname, `../../../templates/${templateFile}`);
|
|
1395
|
+
let html = fs.readFileSync(tplPath, 'utf-8');
|
|
1396
|
+
const fileName = path.basename(filePath);
|
|
1397
|
+
const fileSize = fs.statSync(filePath).size;
|
|
1398
|
+
if (is3D) {
|
|
1399
|
+
html = html.replace(/\{\{FILE\}\}/g, fileName);
|
|
1400
|
+
html = html.replace(/\{\{FILE_PATH_JSON\}\}/g, JSON.stringify(filePath));
|
|
1401
|
+
html = html.replace(/\{\{FORMAT\}\}/g, ext);
|
|
1402
|
+
}
|
|
1403
|
+
else {
|
|
1404
|
+
html = html.replace(/\{\{FILE\}\}/g, fileName);
|
|
1405
|
+
html = html.replace(/\{\{FILE_PATH\}\}/g, filePath);
|
|
1406
|
+
html = html.replace(/\{\{BUILDER_ID\}\}/g, '');
|
|
1407
|
+
html = html.replace(/\{\{LANG\}\}/g, getLanguageForExt(ext));
|
|
1408
|
+
html = html.replace(/\{\{IS_MARKDOWN\}\}/g, String(isMarkdown));
|
|
1409
|
+
html = html.replace(/\{\{IS_IMAGE\}\}/g, String(isImage));
|
|
1410
|
+
html = html.replace(/\{\{IS_VIDEO\}\}/g, String(isVideo));
|
|
1411
|
+
html = html.replace(/\{\{IS_PDF\}\}/g, String(isPdf));
|
|
1412
|
+
html = html.replace(/\{\{FILE_SIZE\}\}/g, String(fileSize));
|
|
1413
|
+
// Inject initialization script (template loads content via fetch)
|
|
1414
|
+
let initScript;
|
|
1415
|
+
if (isImage) {
|
|
1416
|
+
initScript = `initImage(${fileSize});`;
|
|
1417
|
+
}
|
|
1418
|
+
else if (isVideo) {
|
|
1419
|
+
initScript = `initVideo(${fileSize});`;
|
|
1420
|
+
}
|
|
1421
|
+
else if (isPdf) {
|
|
1422
|
+
initScript = `initPdf(${fileSize});`;
|
|
1423
|
+
}
|
|
1424
|
+
else {
|
|
1425
|
+
initScript = `fetch('file').then(r=>r.text()).then(init);`;
|
|
1426
|
+
}
|
|
1427
|
+
html = html.replace('// FILE_CONTENT will be injected by the server', initScript);
|
|
1428
|
+
}
|
|
1429
|
+
// Handle ?line= query param for scroll-to-line
|
|
1430
|
+
const lineParam = url.searchParams.get('line');
|
|
1431
|
+
if (lineParam) {
|
|
1432
|
+
const scrollScript = `<script>window.addEventListener('load',()=>{setTimeout(()=>{const el=document.querySelector('[data-line="${lineParam}"]');if(el){el.scrollIntoView({block:'center'});el.classList.add('highlighted-line');}},200);})</script>`;
|
|
1433
|
+
html = html.replace('</body>', `${scrollScript}</body>`);
|
|
1434
|
+
}
|
|
1435
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
1436
|
+
res.end(html);
|
|
1437
|
+
}
|
|
1438
|
+
catch (err) {
|
|
1439
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
1440
|
+
res.end(`Failed to serve annotator: ${err.message}`);
|
|
1441
|
+
}
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
//# sourceMappingURL=tower-routes.js.map
|