@cluesmith/codev 2.0.0-rc.9 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +473 -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
|
@@ -1,1858 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Dashboard server for Agent Farm.
|
|
4
|
-
* Serves the split-pane dashboard UI and provides state/tab management APIs.
|
|
5
|
-
*/
|
|
6
|
-
import http from 'node:http';
|
|
7
|
-
import fs from 'node:fs';
|
|
8
|
-
import path from 'node:path';
|
|
9
|
-
import net from 'node:net';
|
|
10
|
-
import httpProxy from 'http-proxy';
|
|
11
|
-
import { spawn, execSync, exec } from 'node:child_process';
|
|
12
|
-
import { promisify } from 'node:util';
|
|
13
|
-
import { randomUUID } from 'node:crypto';
|
|
14
|
-
import { fileURLToPath } from 'node:url';
|
|
15
|
-
const execAsync = promisify(exec);
|
|
16
|
-
import { Command } from 'commander';
|
|
17
|
-
import { getPortForTerminal } from '../utils/terminal-ports.js';
|
|
18
|
-
import { escapeHtml, parseJsonBody, isRequestAllowed as isRequestAllowedBase, } from '../utils/server-utils.js';
|
|
19
|
-
import { loadState, getAnnotations, addAnnotation, removeAnnotation, getUtils, tryAddUtil, removeUtil, getBuilder, getBuilders, removeBuilder, upsertBuilder, clearState, } from '../state.js';
|
|
20
|
-
import { spawnTtyd } from '../utils/shell.js';
|
|
21
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
-
const __dirname = path.dirname(__filename);
|
|
23
|
-
// Default dashboard port
|
|
24
|
-
const DEFAULT_DASHBOARD_PORT = 4200;
|
|
25
|
-
// Parse arguments with Commander for proper --help and validation
|
|
26
|
-
const program = new Command()
|
|
27
|
-
.name('dashboard-server')
|
|
28
|
-
.description('Dashboard server for Agent Farm')
|
|
29
|
-
.argument('[port]', 'Port to listen on', String(DEFAULT_DASHBOARD_PORT))
|
|
30
|
-
.argument('[bindHost]', 'Host to bind to (default: localhost, use 0.0.0.0 for remote)')
|
|
31
|
-
.option('-p, --port <port>', 'Port to listen on (overrides positional argument)')
|
|
32
|
-
.option('-b, --bind <host>', 'Host to bind to (overrides positional argument)')
|
|
33
|
-
.parse(process.argv);
|
|
34
|
-
const opts = program.opts();
|
|
35
|
-
const args = program.args;
|
|
36
|
-
// Support both positional arg and --port flag (flag takes precedence)
|
|
37
|
-
const portArg = opts.port || args[0] || String(DEFAULT_DASHBOARD_PORT);
|
|
38
|
-
const port = parseInt(portArg, 10);
|
|
39
|
-
// Bind host: flag > positional arg > default (undefined = localhost)
|
|
40
|
-
const bindHost = opts.bind || args[1] || undefined;
|
|
41
|
-
if (isNaN(port) || port < 1 || port > 65535) {
|
|
42
|
-
console.error(`Error: Invalid port "${portArg}". Must be a number between 1 and 65535.`);
|
|
43
|
-
process.exit(1);
|
|
44
|
-
}
|
|
45
|
-
// Configuration - ports are relative to the dashboard port
|
|
46
|
-
// This ensures multi-project support (e.g., dashboard on 4300 uses 4350 for annotations)
|
|
47
|
-
const CONFIG = {
|
|
48
|
-
dashboardPort: port,
|
|
49
|
-
architectPort: port + 1,
|
|
50
|
-
builderPortStart: port + 10,
|
|
51
|
-
utilPortStart: port + 30,
|
|
52
|
-
openPortStart: port + 50,
|
|
53
|
-
maxTabs: 20, // DoS protection: max concurrent tabs
|
|
54
|
-
};
|
|
55
|
-
// Find project root by looking for .agent-farm directory
|
|
56
|
-
function findProjectRoot() {
|
|
57
|
-
let dir = process.cwd();
|
|
58
|
-
while (dir !== '/') {
|
|
59
|
-
if (fs.existsSync(path.join(dir, '.agent-farm'))) {
|
|
60
|
-
return dir;
|
|
61
|
-
}
|
|
62
|
-
if (fs.existsSync(path.join(dir, 'codev'))) {
|
|
63
|
-
return dir;
|
|
64
|
-
}
|
|
65
|
-
dir = path.dirname(dir);
|
|
66
|
-
}
|
|
67
|
-
return process.cwd();
|
|
68
|
-
}
|
|
69
|
-
// Get project name from root path, with truncation for long names
|
|
70
|
-
function getProjectName(projectRoot) {
|
|
71
|
-
const baseName = path.basename(projectRoot);
|
|
72
|
-
const maxLength = 30;
|
|
73
|
-
if (baseName.length <= maxLength) {
|
|
74
|
-
return baseName;
|
|
75
|
-
}
|
|
76
|
-
// Truncate with ellipsis for very long names
|
|
77
|
-
return '...' + baseName.slice(-(maxLength - 3));
|
|
78
|
-
}
|
|
79
|
-
function findTemplatePath(filename, required = false) {
|
|
80
|
-
// Templates are at package root: packages/codev/templates/
|
|
81
|
-
// From compiled: dist/agent-farm/servers/ -> ../../../templates/
|
|
82
|
-
// From source: src/agent-farm/servers/ -> ../../../templates/
|
|
83
|
-
const pkgPath = path.resolve(__dirname, '../../../templates/', filename);
|
|
84
|
-
if (fs.existsSync(pkgPath))
|
|
85
|
-
return pkgPath;
|
|
86
|
-
if (required) {
|
|
87
|
-
throw new Error(`Template not found: ${filename}`);
|
|
88
|
-
}
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
const projectRoot = findProjectRoot();
|
|
92
|
-
// Use modular dashboard template (Spec 0060)
|
|
93
|
-
const templatePath = findTemplatePath('dashboard/index.html', true);
|
|
94
|
-
// Clean up dead processes from state (called on state load)
|
|
95
|
-
function cleanupDeadProcesses() {
|
|
96
|
-
// Clean up dead shell processes
|
|
97
|
-
for (const util of getUtils()) {
|
|
98
|
-
if (!isProcessRunning(util.pid)) {
|
|
99
|
-
console.log(`Auto-closing shell tab ${util.name} (process ${util.pid} exited)`);
|
|
100
|
-
if (util.tmuxSession) {
|
|
101
|
-
killTmuxSession(util.tmuxSession);
|
|
102
|
-
}
|
|
103
|
-
removeUtil(util.id);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
// Clean up dead annotation processes
|
|
107
|
-
for (const annotation of getAnnotations()) {
|
|
108
|
-
if (!isProcessRunning(annotation.pid)) {
|
|
109
|
-
console.log(`Auto-closing file tab ${annotation.file} (process ${annotation.pid} exited)`);
|
|
110
|
-
removeAnnotation(annotation.id);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
// Load state with cleanup
|
|
115
|
-
function loadStateWithCleanup() {
|
|
116
|
-
cleanupDeadProcesses();
|
|
117
|
-
return loadState();
|
|
118
|
-
}
|
|
119
|
-
// Generate unique ID using crypto for collision resistance
|
|
120
|
-
function generateId(prefix) {
|
|
121
|
-
const uuid = randomUUID().replace(/-/g, '').substring(0, 8).toUpperCase();
|
|
122
|
-
return `${prefix}${uuid}`;
|
|
123
|
-
}
|
|
124
|
-
// Get all ports currently used in state
|
|
125
|
-
function getUsedPorts(state) {
|
|
126
|
-
const ports = new Set();
|
|
127
|
-
if (state.architect?.port)
|
|
128
|
-
ports.add(state.architect.port);
|
|
129
|
-
for (const builder of state.builders || []) {
|
|
130
|
-
if (builder.port)
|
|
131
|
-
ports.add(builder.port);
|
|
132
|
-
}
|
|
133
|
-
for (const util of state.utils || []) {
|
|
134
|
-
if (util.port)
|
|
135
|
-
ports.add(util.port);
|
|
136
|
-
}
|
|
137
|
-
for (const annotation of state.annotations || []) {
|
|
138
|
-
if (annotation.port)
|
|
139
|
-
ports.add(annotation.port);
|
|
140
|
-
}
|
|
141
|
-
return ports;
|
|
142
|
-
}
|
|
143
|
-
// Find available port in range (checks both state and actual availability)
|
|
144
|
-
async function findAvailablePort(startPort, state) {
|
|
145
|
-
// Get ports already allocated in state
|
|
146
|
-
const usedPorts = state ? getUsedPorts(state) : new Set();
|
|
147
|
-
// Skip ports already in state
|
|
148
|
-
let port = startPort;
|
|
149
|
-
while (usedPorts.has(port)) {
|
|
150
|
-
port++;
|
|
151
|
-
}
|
|
152
|
-
// Then verify the port is actually available for binding
|
|
153
|
-
return new Promise((resolve) => {
|
|
154
|
-
const server = net.createServer();
|
|
155
|
-
server.listen(port, () => {
|
|
156
|
-
const { port: boundPort } = server.address();
|
|
157
|
-
server.close(() => resolve(boundPort));
|
|
158
|
-
});
|
|
159
|
-
server.on('error', () => {
|
|
160
|
-
resolve(findAvailablePort(port + 1, state));
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
// Wait for a port to be accepting connections (server ready)
|
|
165
|
-
async function waitForPortReady(port, timeoutMs = 5000) {
|
|
166
|
-
const startTime = Date.now();
|
|
167
|
-
const pollInterval = 100; // Check every 100ms
|
|
168
|
-
while (Date.now() - startTime < timeoutMs) {
|
|
169
|
-
const isReady = await new Promise((resolve) => {
|
|
170
|
-
const socket = new net.Socket();
|
|
171
|
-
socket.setTimeout(pollInterval);
|
|
172
|
-
socket.on('connect', () => {
|
|
173
|
-
socket.destroy();
|
|
174
|
-
resolve(true);
|
|
175
|
-
});
|
|
176
|
-
socket.on('error', () => {
|
|
177
|
-
socket.destroy();
|
|
178
|
-
resolve(false);
|
|
179
|
-
});
|
|
180
|
-
socket.on('timeout', () => {
|
|
181
|
-
socket.destroy();
|
|
182
|
-
resolve(false);
|
|
183
|
-
});
|
|
184
|
-
socket.connect(port, '127.0.0.1');
|
|
185
|
-
});
|
|
186
|
-
if (isReady) {
|
|
187
|
-
return true;
|
|
188
|
-
}
|
|
189
|
-
// Wait before next poll
|
|
190
|
-
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
191
|
-
}
|
|
192
|
-
return false;
|
|
193
|
-
}
|
|
194
|
-
// Kill tmux session
|
|
195
|
-
function killTmuxSession(sessionName) {
|
|
196
|
-
try {
|
|
197
|
-
execSync(`tmux kill-session -t "${sessionName}" 2>/dev/null`, { stdio: 'ignore' });
|
|
198
|
-
}
|
|
199
|
-
catch {
|
|
200
|
-
// Session may not exist
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
// Check if a process is running
|
|
204
|
-
function isProcessRunning(pid) {
|
|
205
|
-
try {
|
|
206
|
-
// Signal 0 doesn't kill, just checks if process exists
|
|
207
|
-
process.kill(pid, 0);
|
|
208
|
-
return true;
|
|
209
|
-
}
|
|
210
|
-
catch {
|
|
211
|
-
return false;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
// Graceful process termination with two-phase shutdown
|
|
215
|
-
async function killProcessGracefully(pid, tmuxSession) {
|
|
216
|
-
// First kill tmux session if provided
|
|
217
|
-
if (tmuxSession) {
|
|
218
|
-
killTmuxSession(tmuxSession);
|
|
219
|
-
}
|
|
220
|
-
try {
|
|
221
|
-
// First try SIGTERM
|
|
222
|
-
process.kill(pid, 'SIGTERM');
|
|
223
|
-
// Wait up to 500ms for process to exit
|
|
224
|
-
await new Promise((resolve) => {
|
|
225
|
-
let attempts = 0;
|
|
226
|
-
const checkInterval = setInterval(() => {
|
|
227
|
-
attempts++;
|
|
228
|
-
try {
|
|
229
|
-
// Signal 0 checks if process exists
|
|
230
|
-
process.kill(pid, 0);
|
|
231
|
-
if (attempts >= 5) {
|
|
232
|
-
// Process still alive after 500ms, use SIGKILL
|
|
233
|
-
clearInterval(checkInterval);
|
|
234
|
-
try {
|
|
235
|
-
process.kill(pid, 'SIGKILL');
|
|
236
|
-
}
|
|
237
|
-
catch {
|
|
238
|
-
// Already dead
|
|
239
|
-
}
|
|
240
|
-
resolve();
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
catch {
|
|
244
|
-
// Process is dead
|
|
245
|
-
clearInterval(checkInterval);
|
|
246
|
-
resolve();
|
|
247
|
-
}
|
|
248
|
-
}, 100);
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
catch {
|
|
252
|
-
// Process may already be dead
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
// Spawn detached process with error handling
|
|
256
|
-
function spawnDetached(command, args, cwd) {
|
|
257
|
-
try {
|
|
258
|
-
const child = spawn(command, args, {
|
|
259
|
-
cwd,
|
|
260
|
-
detached: true,
|
|
261
|
-
stdio: 'ignore',
|
|
262
|
-
});
|
|
263
|
-
child.on('error', (err) => {
|
|
264
|
-
console.error(`Failed to spawn ${command}:`, err.message);
|
|
265
|
-
});
|
|
266
|
-
child.unref();
|
|
267
|
-
return child.pid || null;
|
|
268
|
-
}
|
|
269
|
-
catch (err) {
|
|
270
|
-
console.error(`Failed to spawn ${command}:`, err.message);
|
|
271
|
-
return null;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
// Check if tmux session exists
|
|
275
|
-
function tmuxSessionExists(sessionName) {
|
|
276
|
-
try {
|
|
277
|
-
execSync(`tmux has-session -t "${sessionName}" 2>/dev/null`, { stdio: 'ignore' });
|
|
278
|
-
return true;
|
|
279
|
-
}
|
|
280
|
-
catch {
|
|
281
|
-
return false;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
// Create a persistent tmux session and attach ttyd to it
|
|
285
|
-
// Idempotent: if session exists, just spawn ttyd to attach to it
|
|
286
|
-
function spawnTmuxWithTtyd(sessionName, shellCommand, ttydPort, cwd) {
|
|
287
|
-
try {
|
|
288
|
-
// Only create session if it doesn't exist (idempotent)
|
|
289
|
-
if (!tmuxSessionExists(sessionName)) {
|
|
290
|
-
// Create tmux session with the shell command
|
|
291
|
-
execSync(`tmux new-session -d -s "${sessionName}" -x 200 -y 50 "${shellCommand}"`, { cwd, stdio: 'ignore' });
|
|
292
|
-
// Hide the tmux status bar (dashboard has its own tabs)
|
|
293
|
-
execSync(`tmux set-option -t "${sessionName}" status off`, { stdio: 'ignore' });
|
|
294
|
-
// Enable mouse support in the session
|
|
295
|
-
execSync(`tmux set-option -t "${sessionName}" -g mouse on`, { stdio: 'ignore' });
|
|
296
|
-
// Enable OSC 52 clipboard (allows copy to browser clipboard via ttyd)
|
|
297
|
-
execSync(`tmux set-option -t "${sessionName}" -g set-clipboard on`, { stdio: 'ignore' });
|
|
298
|
-
// Enable passthrough for hyperlinks and clipboard
|
|
299
|
-
execSync(`tmux set-option -t "${sessionName}" -g allow-passthrough on`, { stdio: 'ignore' });
|
|
300
|
-
// Copy selection to clipboard when mouse is released
|
|
301
|
-
// Use copy-pipe-and-cancel with pbcopy to directly copy to system clipboard
|
|
302
|
-
// (OSC 52 via set-clipboard doesn't work reliably through ttyd/xterm.js)
|
|
303
|
-
execSync(`tmux bind-key -T copy-mode MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"`, { stdio: 'ignore' });
|
|
304
|
-
execSync(`tmux bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"`, { stdio: 'ignore' });
|
|
305
|
-
}
|
|
306
|
-
// Start ttyd to attach to the tmux session
|
|
307
|
-
const customIndexPath = findTemplatePath('ttyd-index.html');
|
|
308
|
-
const ttydProcess = spawnTtyd({
|
|
309
|
-
port: ttydPort,
|
|
310
|
-
sessionName,
|
|
311
|
-
cwd,
|
|
312
|
-
customIndexPath: customIndexPath ?? undefined,
|
|
313
|
-
});
|
|
314
|
-
return ttydProcess?.pid ?? null;
|
|
315
|
-
}
|
|
316
|
-
catch (err) {
|
|
317
|
-
console.error(`Failed to create tmux session ${sessionName}:`, err.message);
|
|
318
|
-
// Cleanup any partial session
|
|
319
|
-
killTmuxSession(sessionName);
|
|
320
|
-
return null;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
/**
|
|
324
|
-
* Generate a short 4-character base64-encoded ID for worktree names
|
|
325
|
-
*/
|
|
326
|
-
function generateShortId() {
|
|
327
|
-
const num = Math.floor(Math.random() * 0xFFFFFF);
|
|
328
|
-
const bytes = new Uint8Array([num >> 16, (num >> 8) & 0xFF, num & 0xFF]);
|
|
329
|
-
return btoa(String.fromCharCode(...bytes))
|
|
330
|
-
.replace(/\+/g, '-')
|
|
331
|
-
.replace(/\//g, '_')
|
|
332
|
-
.replace(/=/g, '')
|
|
333
|
-
.substring(0, 4);
|
|
334
|
-
}
|
|
335
|
-
/**
|
|
336
|
-
* Spawn a worktree builder - creates git worktree and starts builder CLI
|
|
337
|
-
* Similar to shell spawning but with git worktree isolation
|
|
338
|
-
*/
|
|
339
|
-
function spawnWorktreeBuilder(builderPort, state) {
|
|
340
|
-
const shortId = generateShortId();
|
|
341
|
-
const builderId = `worktree-${shortId}`;
|
|
342
|
-
const branchName = `builder/worktree-${shortId}`;
|
|
343
|
-
const worktreePath = path.resolve(projectRoot, '.builders', builderId);
|
|
344
|
-
const sessionName = `builder-${builderId}`;
|
|
345
|
-
try {
|
|
346
|
-
// Ensure .builders directory exists
|
|
347
|
-
const buildersDir = path.resolve(projectRoot, '.builders');
|
|
348
|
-
if (!fs.existsSync(buildersDir)) {
|
|
349
|
-
fs.mkdirSync(buildersDir, { recursive: true });
|
|
350
|
-
}
|
|
351
|
-
// Create git branch and worktree
|
|
352
|
-
execSync(`git branch "${branchName}" HEAD`, { cwd: projectRoot, stdio: 'ignore' });
|
|
353
|
-
execSync(`git worktree add "${worktreePath}" "${branchName}"`, { cwd: projectRoot, stdio: 'ignore' });
|
|
354
|
-
// Get builder command from config or use default shell
|
|
355
|
-
const configPath = path.resolve(projectRoot, 'codev', 'config.json');
|
|
356
|
-
const defaultShell = process.env.SHELL || 'bash';
|
|
357
|
-
let builderCommand = defaultShell;
|
|
358
|
-
if (fs.existsSync(configPath)) {
|
|
359
|
-
try {
|
|
360
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
361
|
-
builderCommand = config?.shell?.builder || defaultShell;
|
|
362
|
-
}
|
|
363
|
-
catch {
|
|
364
|
-
// Use default
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
// Create tmux session with builder command
|
|
368
|
-
execSync(`tmux new-session -d -s "${sessionName}" -x 200 -y 50 -c "${worktreePath}" "${builderCommand}"`, { cwd: worktreePath, stdio: 'ignore' });
|
|
369
|
-
// Hide the tmux status bar (dashboard has its own tabs)
|
|
370
|
-
execSync(`tmux set-option -t "${sessionName}" status off`, { stdio: 'ignore' });
|
|
371
|
-
// Enable mouse support
|
|
372
|
-
execSync(`tmux set-option -t "${sessionName}" -g mouse on`, { stdio: 'ignore' });
|
|
373
|
-
execSync(`tmux set-option -t "${sessionName}" -g set-clipboard on`, { stdio: 'ignore' });
|
|
374
|
-
execSync(`tmux set-option -t "${sessionName}" -g allow-passthrough on`, { stdio: 'ignore' });
|
|
375
|
-
// Copy selection to clipboard when mouse is released (pbcopy for macOS)
|
|
376
|
-
execSync(`tmux bind-key -T copy-mode MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"`, { stdio: 'ignore' });
|
|
377
|
-
execSync(`tmux bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"`, { stdio: 'ignore' });
|
|
378
|
-
// Start ttyd connecting to the tmux session
|
|
379
|
-
const customIndexPath = findTemplatePath('ttyd-index.html');
|
|
380
|
-
const ttydProcess = spawnTtyd({
|
|
381
|
-
port: builderPort,
|
|
382
|
-
sessionName,
|
|
383
|
-
cwd: worktreePath,
|
|
384
|
-
customIndexPath: customIndexPath ?? undefined,
|
|
385
|
-
});
|
|
386
|
-
const pid = ttydProcess?.pid ?? null;
|
|
387
|
-
if (!pid) {
|
|
388
|
-
// Cleanup on failure
|
|
389
|
-
killTmuxSession(sessionName);
|
|
390
|
-
try {
|
|
391
|
-
execSync(`git worktree remove "${worktreePath}" --force`, { cwd: projectRoot, stdio: 'ignore' });
|
|
392
|
-
execSync(`git branch -D "${branchName}"`, { cwd: projectRoot, stdio: 'ignore' });
|
|
393
|
-
}
|
|
394
|
-
catch {
|
|
395
|
-
// Best effort cleanup
|
|
396
|
-
}
|
|
397
|
-
return null;
|
|
398
|
-
}
|
|
399
|
-
const builder = {
|
|
400
|
-
id: builderId,
|
|
401
|
-
name: `Worktree ${shortId}`,
|
|
402
|
-
port: builderPort,
|
|
403
|
-
pid,
|
|
404
|
-
status: 'implementing',
|
|
405
|
-
phase: 'interactive',
|
|
406
|
-
worktree: worktreePath,
|
|
407
|
-
branch: branchName,
|
|
408
|
-
tmuxSession: sessionName,
|
|
409
|
-
type: 'worktree',
|
|
410
|
-
};
|
|
411
|
-
return { builder, pid };
|
|
412
|
-
}
|
|
413
|
-
catch (err) {
|
|
414
|
-
console.error(`Failed to spawn worktree builder:`, err.message);
|
|
415
|
-
// Cleanup any partial state
|
|
416
|
-
killTmuxSession(sessionName);
|
|
417
|
-
try {
|
|
418
|
-
execSync(`git worktree remove "${worktreePath}" --force`, { cwd: projectRoot, stdio: 'ignore' });
|
|
419
|
-
execSync(`git branch -D "${branchName}"`, { cwd: projectRoot, stdio: 'ignore' });
|
|
420
|
-
}
|
|
421
|
-
catch {
|
|
422
|
-
// Best effort cleanup
|
|
423
|
-
}
|
|
424
|
-
return null;
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
// parseJsonBody imported from ../utils/server-utils.js
|
|
428
|
-
// Validate path is within project root (prevent path traversal)
|
|
429
|
-
// Handles URL-encoded dots (%2e), symlinks, and other encodings
|
|
430
|
-
function validatePathWithinProject(filePath) {
|
|
431
|
-
// First decode any URL encoding to catch %2e%2e (encoded ..)
|
|
432
|
-
let decodedPath;
|
|
433
|
-
try {
|
|
434
|
-
decodedPath = decodeURIComponent(filePath);
|
|
435
|
-
}
|
|
436
|
-
catch {
|
|
437
|
-
// Invalid encoding
|
|
438
|
-
return null;
|
|
439
|
-
}
|
|
440
|
-
// Resolve to absolute path
|
|
441
|
-
const resolvedPath = decodedPath.startsWith('/')
|
|
442
|
-
? path.resolve(decodedPath)
|
|
443
|
-
: path.resolve(projectRoot, decodedPath);
|
|
444
|
-
// Normalize to remove any .. or . segments
|
|
445
|
-
const normalizedPath = path.normalize(resolvedPath);
|
|
446
|
-
// First check normalized path (for paths that don't exist yet)
|
|
447
|
-
if (!normalizedPath.startsWith(projectRoot + path.sep) && normalizedPath !== projectRoot) {
|
|
448
|
-
return null; // Path escapes project root
|
|
449
|
-
}
|
|
450
|
-
// If file exists, resolve symlinks to prevent symlink-based path traversal
|
|
451
|
-
// An attacker could create a symlink within the repo pointing outside
|
|
452
|
-
if (fs.existsSync(normalizedPath)) {
|
|
453
|
-
try {
|
|
454
|
-
const realPath = fs.realpathSync(normalizedPath);
|
|
455
|
-
if (!realPath.startsWith(projectRoot + path.sep) && realPath !== projectRoot) {
|
|
456
|
-
return null; // Symlink target escapes project root
|
|
457
|
-
}
|
|
458
|
-
return realPath;
|
|
459
|
-
}
|
|
460
|
-
catch {
|
|
461
|
-
// realpathSync failed (broken symlink, permissions, etc.)
|
|
462
|
-
return null;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
return normalizedPath;
|
|
466
|
-
}
|
|
467
|
-
// Count total tabs for DoS protection
|
|
468
|
-
function countTotalTabs(state) {
|
|
469
|
-
return state.builders.length + state.utils.length + state.annotations.length;
|
|
470
|
-
}
|
|
471
|
-
// Find open server script (prefer .ts for dev, .js for compiled)
|
|
472
|
-
function getOpenServerPath() {
|
|
473
|
-
const tsPath = path.join(__dirname, 'open-server.ts');
|
|
474
|
-
const jsPath = path.join(__dirname, 'open-server.js');
|
|
475
|
-
if (fs.existsSync(tsPath)) {
|
|
476
|
-
return { script: tsPath, useTsx: true };
|
|
477
|
-
}
|
|
478
|
-
return { script: jsPath, useTsx: false };
|
|
479
|
-
}
|
|
480
|
-
/**
|
|
481
|
-
* Escape a string for safe use in shell commands
|
|
482
|
-
* Handles special characters that could cause command injection
|
|
483
|
-
*/
|
|
484
|
-
function escapeShellArg(str) {
|
|
485
|
-
// Single-quote the string and escape any single quotes within it
|
|
486
|
-
return "'" + str.replace(/'/g, "'\\''") + "'";
|
|
487
|
-
}
|
|
488
|
-
/**
|
|
489
|
-
* Get today's git commits from all branches for the current user
|
|
490
|
-
*/
|
|
491
|
-
async function getGitCommits(projectRoot) {
|
|
492
|
-
try {
|
|
493
|
-
const { stdout: authorRaw } = await execAsync('git config user.name', { cwd: projectRoot });
|
|
494
|
-
const author = authorRaw.trim();
|
|
495
|
-
if (!author)
|
|
496
|
-
return [];
|
|
497
|
-
// Escape author name to prevent command injection
|
|
498
|
-
const safeAuthor = escapeShellArg(author);
|
|
499
|
-
// Get commits from all branches since midnight
|
|
500
|
-
const { stdout: output } = await execAsync(`git log --all --since="midnight" --author=${safeAuthor} --format="%H|%s|%aI|%D"`, { cwd: projectRoot, maxBuffer: 10 * 1024 * 1024 });
|
|
501
|
-
if (!output.trim())
|
|
502
|
-
return [];
|
|
503
|
-
return output.trim().split('\n').filter(Boolean).map(line => {
|
|
504
|
-
const parts = line.split('|');
|
|
505
|
-
const hash = parts[0] || '';
|
|
506
|
-
const message = parts[1] || '';
|
|
507
|
-
const time = parts[2] || '';
|
|
508
|
-
const refs = parts.slice(3).join('|'); // refs might contain |
|
|
509
|
-
// Extract branch name from refs
|
|
510
|
-
let branch = 'unknown';
|
|
511
|
-
const headMatch = refs.match(/HEAD -> ([^,]+)/);
|
|
512
|
-
const branchMatch = refs.match(/([^,\s]+)$/);
|
|
513
|
-
if (headMatch) {
|
|
514
|
-
branch = headMatch[1];
|
|
515
|
-
}
|
|
516
|
-
else if (branchMatch && branchMatch[1]) {
|
|
517
|
-
branch = branchMatch[1];
|
|
518
|
-
}
|
|
519
|
-
return {
|
|
520
|
-
hash: hash.slice(0, 7),
|
|
521
|
-
message: message.slice(0, 100), // Truncate long messages
|
|
522
|
-
time,
|
|
523
|
-
branch,
|
|
524
|
-
};
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
catch (err) {
|
|
528
|
-
console.error('Error getting git commits:', err.message);
|
|
529
|
-
return [];
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
/**
|
|
533
|
-
* Get unique files modified today
|
|
534
|
-
*/
|
|
535
|
-
async function getModifiedFiles(projectRoot) {
|
|
536
|
-
try {
|
|
537
|
-
const { stdout: authorRaw } = await execAsync('git config user.name', { cwd: projectRoot });
|
|
538
|
-
const author = authorRaw.trim();
|
|
539
|
-
if (!author)
|
|
540
|
-
return [];
|
|
541
|
-
// Escape author name to prevent command injection
|
|
542
|
-
const safeAuthor = escapeShellArg(author);
|
|
543
|
-
const { stdout: output } = await execAsync(`git log --all --since="midnight" --author=${safeAuthor} --name-only --format=""`, { cwd: projectRoot, maxBuffer: 10 * 1024 * 1024 });
|
|
544
|
-
if (!output.trim())
|
|
545
|
-
return [];
|
|
546
|
-
const files = [...new Set(output.trim().split('\n').filter(Boolean))];
|
|
547
|
-
return files.sort();
|
|
548
|
-
}
|
|
549
|
-
catch (err) {
|
|
550
|
-
console.error('Error getting modified files:', err.message);
|
|
551
|
-
return [];
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
/**
|
|
555
|
-
* Get GitHub PRs created or merged today via gh CLI
|
|
556
|
-
* Combines PRs created today AND PRs merged today (which may have been created earlier)
|
|
557
|
-
*/
|
|
558
|
-
async function getGitHubPRs(projectRoot) {
|
|
559
|
-
try {
|
|
560
|
-
// Use local time for the date (spec says "today" means local machine time)
|
|
561
|
-
const now = new Date();
|
|
562
|
-
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
|
563
|
-
// Fetch PRs created today AND PRs merged today in parallel
|
|
564
|
-
const [createdResult, mergedResult] = await Promise.allSettled([
|
|
565
|
-
execAsync(`gh pr list --author "@me" --state all --search "created:>=${today}" --json number,title,state,url`, { cwd: projectRoot, timeout: 15000 }),
|
|
566
|
-
execAsync(`gh pr list --author "@me" --state merged --search "merged:>=${today}" --json number,title,state,url`, { cwd: projectRoot, timeout: 15000 }),
|
|
567
|
-
]);
|
|
568
|
-
const prsMap = new Map();
|
|
569
|
-
// Process PRs created today
|
|
570
|
-
if (createdResult.status === 'fulfilled' && createdResult.value.stdout.trim()) {
|
|
571
|
-
const prs = JSON.parse(createdResult.value.stdout);
|
|
572
|
-
for (const pr of prs) {
|
|
573
|
-
prsMap.set(pr.number, {
|
|
574
|
-
number: pr.number,
|
|
575
|
-
title: pr.title.slice(0, 100),
|
|
576
|
-
state: pr.state,
|
|
577
|
-
url: pr.url,
|
|
578
|
-
});
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
// Process PRs merged today (may overlap with created, deduped by Map)
|
|
582
|
-
if (mergedResult.status === 'fulfilled' && mergedResult.value.stdout.trim()) {
|
|
583
|
-
const prs = JSON.parse(mergedResult.value.stdout);
|
|
584
|
-
for (const pr of prs) {
|
|
585
|
-
prsMap.set(pr.number, {
|
|
586
|
-
number: pr.number,
|
|
587
|
-
title: pr.title.slice(0, 100),
|
|
588
|
-
state: pr.state,
|
|
589
|
-
url: pr.url,
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
return Array.from(prsMap.values());
|
|
594
|
-
}
|
|
595
|
-
catch (err) {
|
|
596
|
-
// gh CLI might not be available or authenticated
|
|
597
|
-
console.error('Error getting GitHub PRs:', err.message);
|
|
598
|
-
return [];
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
/**
|
|
602
|
-
* Get builder activity from state.db for today
|
|
603
|
-
* Note: state.json doesn't track timestamps, so we can only report current builders
|
|
604
|
-
* without duration. They'll be counted as activity points, not time intervals.
|
|
605
|
-
*/
|
|
606
|
-
function getBuilderActivity() {
|
|
607
|
-
try {
|
|
608
|
-
const builders = getBuilders();
|
|
609
|
-
// Return current builders without time tracking (state.json lacks timestamps)
|
|
610
|
-
// Time tracking will rely primarily on git commits
|
|
611
|
-
return builders.map(b => ({
|
|
612
|
-
id: b.id,
|
|
613
|
-
status: b.status || 'unknown',
|
|
614
|
-
startTime: '', // Unknown - not tracked in state.json
|
|
615
|
-
endTime: undefined,
|
|
616
|
-
}));
|
|
617
|
-
}
|
|
618
|
-
catch (err) {
|
|
619
|
-
console.error('Error getting builder activity:', err.message);
|
|
620
|
-
return [];
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
/**
|
|
624
|
-
* Detect project status changes in projectlist.md today
|
|
625
|
-
* Handles YAML format inside Markdown fenced code blocks
|
|
626
|
-
*/
|
|
627
|
-
async function getProjectChanges(projectRoot) {
|
|
628
|
-
try {
|
|
629
|
-
const projectlistPath = path.join(projectRoot, 'codev/projectlist.md');
|
|
630
|
-
if (!fs.existsSync(projectlistPath))
|
|
631
|
-
return [];
|
|
632
|
-
// Get the first commit hash from today that touched projectlist.md
|
|
633
|
-
const { stdout: firstCommitOutput } = await execAsync(`git log --since="midnight" --format=%H -- codev/projectlist.md | tail -1`, { cwd: projectRoot });
|
|
634
|
-
if (!firstCommitOutput.trim())
|
|
635
|
-
return [];
|
|
636
|
-
// Get diff of projectlist.md from that commit's parent to HEAD
|
|
637
|
-
let diff;
|
|
638
|
-
try {
|
|
639
|
-
const { stdout } = await execAsync(`git diff ${firstCommitOutput.trim()}^..HEAD -- codev/projectlist.md`, { cwd: projectRoot, maxBuffer: 1024 * 1024 });
|
|
640
|
-
diff = stdout;
|
|
641
|
-
}
|
|
642
|
-
catch {
|
|
643
|
-
return [];
|
|
644
|
-
}
|
|
645
|
-
if (!diff.trim())
|
|
646
|
-
return [];
|
|
647
|
-
// Parse status changes from diff
|
|
648
|
-
// Format is YAML inside Markdown code blocks:
|
|
649
|
-
// - id: "0058"
|
|
650
|
-
// title: "File Search Autocomplete"
|
|
651
|
-
// status: implementing
|
|
652
|
-
const changes = [];
|
|
653
|
-
const lines = diff.split('\n');
|
|
654
|
-
let currentId = '';
|
|
655
|
-
let currentTitle = '';
|
|
656
|
-
let oldStatus = '';
|
|
657
|
-
let newStatus = '';
|
|
658
|
-
for (const line of lines) {
|
|
659
|
-
// Track current project context from YAML id field
|
|
660
|
-
// Match lines like: " - id: \"0058\"" or "+ - id: \"0058\""
|
|
661
|
-
const idMatch = line.match(/^[+-]?\s*-\s*id:\s*["']?(\d{4})["']?/);
|
|
662
|
-
if (idMatch) {
|
|
663
|
-
// If we have a pending status change from previous project, emit it
|
|
664
|
-
if (oldStatus && newStatus && currentId) {
|
|
665
|
-
changes.push({
|
|
666
|
-
id: currentId,
|
|
667
|
-
title: currentTitle,
|
|
668
|
-
oldStatus,
|
|
669
|
-
newStatus,
|
|
670
|
-
});
|
|
671
|
-
oldStatus = '';
|
|
672
|
-
newStatus = '';
|
|
673
|
-
}
|
|
674
|
-
currentId = idMatch[1];
|
|
675
|
-
currentTitle = ''; // Will be filled by title line
|
|
676
|
-
}
|
|
677
|
-
// Track title (comes after id in YAML)
|
|
678
|
-
// Match lines like: " title: \"File Search Autocomplete\""
|
|
679
|
-
const titleMatch = line.match(/^[+-]?\s*title:\s*["']?([^"']+)["']?/);
|
|
680
|
-
if (titleMatch && currentId) {
|
|
681
|
-
currentTitle = titleMatch[1].trim();
|
|
682
|
-
}
|
|
683
|
-
// Track status changes
|
|
684
|
-
// Match lines like: "- status: implementing" or "+ status: implemented"
|
|
685
|
-
const statusMatch = line.match(/^([+-])\s*status:\s*(\w+)/);
|
|
686
|
-
if (statusMatch) {
|
|
687
|
-
const [, modifier, status] = statusMatch;
|
|
688
|
-
if (modifier === '-') {
|
|
689
|
-
oldStatus = status;
|
|
690
|
-
}
|
|
691
|
-
else if (modifier === '+') {
|
|
692
|
-
newStatus = status;
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
// Emit final pending change if exists
|
|
697
|
-
if (oldStatus && newStatus && currentId) {
|
|
698
|
-
changes.push({
|
|
699
|
-
id: currentId,
|
|
700
|
-
title: currentTitle,
|
|
701
|
-
oldStatus,
|
|
702
|
-
newStatus,
|
|
703
|
-
});
|
|
704
|
-
}
|
|
705
|
-
return changes;
|
|
706
|
-
}
|
|
707
|
-
catch (err) {
|
|
708
|
-
console.error('Error getting project changes:', err.message);
|
|
709
|
-
return [];
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
/**
|
|
713
|
-
* Merge overlapping time intervals
|
|
714
|
-
*/
|
|
715
|
-
function mergeIntervals(intervals) {
|
|
716
|
-
if (intervals.length === 0)
|
|
717
|
-
return [];
|
|
718
|
-
// Sort by start time
|
|
719
|
-
const sorted = [...intervals].sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
720
|
-
const merged = [{ ...sorted[0] }];
|
|
721
|
-
for (let i = 1; i < sorted.length; i++) {
|
|
722
|
-
const last = merged[merged.length - 1];
|
|
723
|
-
const current = sorted[i];
|
|
724
|
-
// If overlapping or within 2 hours, merge
|
|
725
|
-
const gapMs = current.start.getTime() - last.end.getTime();
|
|
726
|
-
const twoHoursMs = 2 * 60 * 60 * 1000;
|
|
727
|
-
if (gapMs <= twoHoursMs) {
|
|
728
|
-
last.end = new Date(Math.max(last.end.getTime(), current.end.getTime()));
|
|
729
|
-
}
|
|
730
|
-
else {
|
|
731
|
-
merged.push({ ...current });
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
return merged;
|
|
735
|
-
}
|
|
736
|
-
/**
|
|
737
|
-
* Calculate active time from commits and builder activity
|
|
738
|
-
*/
|
|
739
|
-
function calculateTimeTracking(commits, builders) {
|
|
740
|
-
const intervals = [];
|
|
741
|
-
const fiveMinutesMs = 5 * 60 * 1000;
|
|
742
|
-
// Add commit timestamps (treat each as 5-minute interval)
|
|
743
|
-
for (const commit of commits) {
|
|
744
|
-
if (commit.time) {
|
|
745
|
-
const time = new Date(commit.time);
|
|
746
|
-
if (!isNaN(time.getTime())) {
|
|
747
|
-
intervals.push({
|
|
748
|
-
start: time,
|
|
749
|
-
end: new Date(time.getTime() + fiveMinutesMs),
|
|
750
|
-
});
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
// Add builder sessions
|
|
755
|
-
for (const builder of builders) {
|
|
756
|
-
if (builder.startTime) {
|
|
757
|
-
const start = new Date(builder.startTime);
|
|
758
|
-
const end = builder.endTime ? new Date(builder.endTime) : new Date();
|
|
759
|
-
if (!isNaN(start.getTime()) && !isNaN(end.getTime())) {
|
|
760
|
-
intervals.push({ start, end });
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
if (intervals.length === 0) {
|
|
765
|
-
return {
|
|
766
|
-
activeMinutes: 0,
|
|
767
|
-
firstActivity: '',
|
|
768
|
-
lastActivity: '',
|
|
769
|
-
};
|
|
770
|
-
}
|
|
771
|
-
const merged = mergeIntervals(intervals);
|
|
772
|
-
const totalMinutes = merged.reduce((sum, interval) => sum + (interval.end.getTime() - interval.start.getTime()) / (1000 * 60), 0);
|
|
773
|
-
return {
|
|
774
|
-
activeMinutes: Math.round(totalMinutes),
|
|
775
|
-
firstActivity: merged[0].start.toISOString(),
|
|
776
|
-
lastActivity: merged[merged.length - 1].end.toISOString(),
|
|
777
|
-
};
|
|
778
|
-
}
|
|
779
|
-
/**
|
|
780
|
-
* Find the consult CLI path
|
|
781
|
-
* Returns the path to the consult binary, checking multiple locations
|
|
782
|
-
*/
|
|
783
|
-
function findConsultPath() {
|
|
784
|
-
// When running from dist/, check relative paths
|
|
785
|
-
// dist/agent-farm/servers/ -> ../../../bin/consult.js
|
|
786
|
-
const distPath = path.join(__dirname, '../../../bin/consult.js');
|
|
787
|
-
if (fs.existsSync(distPath)) {
|
|
788
|
-
return distPath;
|
|
789
|
-
}
|
|
790
|
-
// When running from src/ with tsx, check src-relative paths
|
|
791
|
-
// src/agent-farm/servers/ -> ../../../bin/consult.js (won't exist, it's .ts in src)
|
|
792
|
-
// But bin/ is at packages/codev/bin/consult.js, so it should still work
|
|
793
|
-
// Fall back to npx consult (works if @cluesmith/codev is installed)
|
|
794
|
-
return 'npx consult';
|
|
795
|
-
}
|
|
796
|
-
/**
|
|
797
|
-
* Generate AI summary via consult CLI
|
|
798
|
-
*/
|
|
799
|
-
async function generateAISummary(data) {
|
|
800
|
-
// Build prompt with commit messages and file names only (security: no full diffs)
|
|
801
|
-
const hours = Math.floor(data.timeTracking.activeMinutes / 60);
|
|
802
|
-
const mins = data.timeTracking.activeMinutes % 60;
|
|
803
|
-
const prompt = `Summarize this developer's activity today for a standup report.
|
|
804
|
-
|
|
805
|
-
Commits (${data.commits.length}):
|
|
806
|
-
${data.commits.slice(0, 20).map(c => `- ${c.message}`).join('\n') || '(none)'}
|
|
807
|
-
${data.commits.length > 20 ? `... and ${data.commits.length - 20} more` : ''}
|
|
808
|
-
|
|
809
|
-
PRs: ${data.prs.map(p => `#${p.number} ${p.title} (${p.state})`).join(', ') || 'None'}
|
|
810
|
-
|
|
811
|
-
Files modified: ${data.files.length} files
|
|
812
|
-
${data.files.slice(0, 10).join(', ')}${data.files.length > 10 ? ` ... and ${data.files.length - 10} more` : ''}
|
|
813
|
-
|
|
814
|
-
Project status changes:
|
|
815
|
-
${data.projectChanges.map(p => `- ${p.id} ${p.title}: ${p.oldStatus} → ${p.newStatus}`).join('\n') || '(none)'}
|
|
816
|
-
|
|
817
|
-
Active time: ~${hours}h ${mins}m
|
|
818
|
-
|
|
819
|
-
Write a brief, professional summary (2-3 sentences) focusing on accomplishments. Be concise and suitable for a standup or status report.`;
|
|
820
|
-
try {
|
|
821
|
-
// Use consult CLI to generate summary
|
|
822
|
-
const consultCmd = findConsultPath();
|
|
823
|
-
const safePrompt = escapeShellArg(prompt);
|
|
824
|
-
// Use async exec with timeout
|
|
825
|
-
const { stdout } = await execAsync(`${consultCmd} --model gemini general ${safePrompt}`, { timeout: 60000, maxBuffer: 1024 * 1024 });
|
|
826
|
-
return stdout.trim();
|
|
827
|
-
}
|
|
828
|
-
catch (err) {
|
|
829
|
-
console.error('AI summary generation failed:', err.message);
|
|
830
|
-
return '';
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
/**
|
|
834
|
-
* Collect all activity data for today
|
|
835
|
-
*/
|
|
836
|
-
async function collectActivitySummary(projectRoot) {
|
|
837
|
-
// Collect data from all sources in parallel - these are now truly async
|
|
838
|
-
const [commits, files, prs, builders, projectChanges] = await Promise.all([
|
|
839
|
-
getGitCommits(projectRoot),
|
|
840
|
-
getModifiedFiles(projectRoot),
|
|
841
|
-
getGitHubPRs(projectRoot),
|
|
842
|
-
Promise.resolve(getBuilderActivity()), // This one is sync (reads from state)
|
|
843
|
-
getProjectChanges(projectRoot),
|
|
844
|
-
]);
|
|
845
|
-
const timeTracking = calculateTimeTracking(commits, builders);
|
|
846
|
-
// Generate AI summary (skip if no activity)
|
|
847
|
-
let aiSummary = '';
|
|
848
|
-
if (commits.length > 0 || prs.length > 0) {
|
|
849
|
-
aiSummary = await generateAISummary({
|
|
850
|
-
commits,
|
|
851
|
-
prs,
|
|
852
|
-
files,
|
|
853
|
-
timeTracking,
|
|
854
|
-
projectChanges,
|
|
855
|
-
});
|
|
856
|
-
}
|
|
857
|
-
return {
|
|
858
|
-
commits,
|
|
859
|
-
prs,
|
|
860
|
-
builders,
|
|
861
|
-
projectChanges,
|
|
862
|
-
files,
|
|
863
|
-
timeTracking,
|
|
864
|
-
aiSummary: aiSummary || undefined,
|
|
865
|
-
};
|
|
866
|
-
}
|
|
867
|
-
// Insecure remote mode - set when bindHost is 0.0.0.0
|
|
868
|
-
const insecureRemoteMode = bindHost === '0.0.0.0';
|
|
869
|
-
// ============================================================
|
|
870
|
-
// Terminal Proxy (Spec 0062 - Secure Remote Access)
|
|
871
|
-
// ============================================================
|
|
872
|
-
// Create http-proxy instance for terminal proxying
|
|
873
|
-
const terminalProxy = httpProxy.createProxyServer({ ws: true });
|
|
874
|
-
// Handle proxy errors gracefully
|
|
875
|
-
terminalProxy.on('error', (err, req, res) => {
|
|
876
|
-
console.error('Terminal proxy error:', err.message);
|
|
877
|
-
if (res && 'writeHead' in res && !res.headersSent) {
|
|
878
|
-
res.writeHead(502, { 'Content-Type': 'application/json' });
|
|
879
|
-
res.end(JSON.stringify({ error: 'Terminal unavailable' }));
|
|
880
|
-
}
|
|
881
|
-
});
|
|
882
|
-
// getPortForTerminal is imported from utils/terminal-ports.ts (Spec 0062)
|
|
883
|
-
// Security: Validate request origin (uses base from server-utils with insecureRemoteMode override)
|
|
884
|
-
function isRequestAllowed(req) {
|
|
885
|
-
// Skip all security checks in insecure remote mode
|
|
886
|
-
if (insecureRemoteMode) {
|
|
887
|
-
return true;
|
|
888
|
-
}
|
|
889
|
-
return isRequestAllowedBase(req);
|
|
890
|
-
}
|
|
891
|
-
// Create server
|
|
892
|
-
const server = http.createServer(async (req, res) => {
|
|
893
|
-
// Security: Validate Host and Origin headers
|
|
894
|
-
if (!isRequestAllowed(req)) {
|
|
895
|
-
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
896
|
-
res.end('Forbidden');
|
|
897
|
-
return;
|
|
898
|
-
}
|
|
899
|
-
// CORS headers
|
|
900
|
-
const origin = req.headers.origin;
|
|
901
|
-
if (insecureRemoteMode) {
|
|
902
|
-
// Allow any origin in insecure remote mode
|
|
903
|
-
res.setHeader('Access-Control-Allow-Origin', origin || '*');
|
|
904
|
-
}
|
|
905
|
-
else if (origin && (origin.startsWith('http://localhost:') || origin.startsWith('http://127.0.0.1:'))) {
|
|
906
|
-
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
907
|
-
}
|
|
908
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
909
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
910
|
-
// Prevent caching of API responses
|
|
911
|
-
res.setHeader('Cache-Control', 'no-store');
|
|
912
|
-
if (req.method === 'OPTIONS') {
|
|
913
|
-
res.writeHead(200);
|
|
914
|
-
res.end();
|
|
915
|
-
return;
|
|
916
|
-
}
|
|
917
|
-
const url = new URL(req.url || '/', `http://localhost:${port}`);
|
|
918
|
-
try {
|
|
919
|
-
// API: Get state
|
|
920
|
-
if (req.method === 'GET' && url.pathname === '/api/state') {
|
|
921
|
-
const state = loadStateWithCleanup();
|
|
922
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
923
|
-
res.end(JSON.stringify(state));
|
|
924
|
-
return;
|
|
925
|
-
}
|
|
926
|
-
// API: Create file tab (annotation)
|
|
927
|
-
if (req.method === 'POST' && url.pathname === '/api/tabs/file') {
|
|
928
|
-
const body = await parseJsonBody(req);
|
|
929
|
-
const filePath = body.path;
|
|
930
|
-
if (!filePath) {
|
|
931
|
-
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
932
|
-
res.end('Missing path');
|
|
933
|
-
return;
|
|
934
|
-
}
|
|
935
|
-
// Validate path is within project root (prevent path traversal)
|
|
936
|
-
const fullPath = validatePathWithinProject(filePath);
|
|
937
|
-
if (!fullPath) {
|
|
938
|
-
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
939
|
-
res.end('Path must be within project directory');
|
|
940
|
-
return;
|
|
941
|
-
}
|
|
942
|
-
// Check file exists
|
|
943
|
-
if (!fs.existsSync(fullPath)) {
|
|
944
|
-
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
945
|
-
res.end(`File not found: ${filePath}`);
|
|
946
|
-
return;
|
|
947
|
-
}
|
|
948
|
-
// Check if already open
|
|
949
|
-
const annotations = getAnnotations();
|
|
950
|
-
const existing = annotations.find((a) => a.file === fullPath);
|
|
951
|
-
if (existing) {
|
|
952
|
-
// Verify the process is still running
|
|
953
|
-
if (isProcessRunning(existing.pid)) {
|
|
954
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
955
|
-
res.end(JSON.stringify({ id: existing.id, port: existing.port, existing: true }));
|
|
956
|
-
return;
|
|
957
|
-
}
|
|
958
|
-
// Process is dead - clean up stale entry and spawn new one
|
|
959
|
-
console.log(`Cleaning up stale annotation for ${fullPath} (pid ${existing.pid} dead)`);
|
|
960
|
-
removeAnnotation(existing.id);
|
|
961
|
-
}
|
|
962
|
-
// DoS protection: check tab limit
|
|
963
|
-
const state = loadState();
|
|
964
|
-
if (countTotalTabs(state) >= CONFIG.maxTabs) {
|
|
965
|
-
res.writeHead(429, { 'Content-Type': 'text/plain' });
|
|
966
|
-
res.end(`Tab limit reached (max ${CONFIG.maxTabs}). Close some tabs first.`);
|
|
967
|
-
return;
|
|
968
|
-
}
|
|
969
|
-
// Find available port (pass state to avoid already-allocated ports)
|
|
970
|
-
const openPort = await findAvailablePort(CONFIG.openPortStart, state);
|
|
971
|
-
// Start open server
|
|
972
|
-
const { script: serverScript, useTsx } = getOpenServerPath();
|
|
973
|
-
if (!fs.existsSync(serverScript)) {
|
|
974
|
-
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
975
|
-
res.end('Open server not found');
|
|
976
|
-
return;
|
|
977
|
-
}
|
|
978
|
-
// Use tsx for TypeScript files, node for compiled JavaScript
|
|
979
|
-
const cmd = useTsx ? 'npx' : 'node';
|
|
980
|
-
const args = useTsx
|
|
981
|
-
? ['tsx', serverScript, String(openPort), fullPath]
|
|
982
|
-
: [serverScript, String(openPort), fullPath];
|
|
983
|
-
const pid = spawnDetached(cmd, args, projectRoot);
|
|
984
|
-
if (!pid) {
|
|
985
|
-
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
986
|
-
res.end('Failed to start open server');
|
|
987
|
-
return;
|
|
988
|
-
}
|
|
989
|
-
// Wait for open server to be ready (accepting connections)
|
|
990
|
-
const serverReady = await waitForPortReady(openPort, 5000);
|
|
991
|
-
if (!serverReady) {
|
|
992
|
-
// Server didn't start in time - kill it and report error
|
|
993
|
-
try {
|
|
994
|
-
process.kill(pid);
|
|
995
|
-
}
|
|
996
|
-
catch {
|
|
997
|
-
// Process may have already died
|
|
998
|
-
}
|
|
999
|
-
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
1000
|
-
res.end('Open server failed to start (timeout)');
|
|
1001
|
-
return;
|
|
1002
|
-
}
|
|
1003
|
-
// Create annotation record
|
|
1004
|
-
const annotation = {
|
|
1005
|
-
id: generateId('A'),
|
|
1006
|
-
file: fullPath,
|
|
1007
|
-
port: openPort,
|
|
1008
|
-
pid,
|
|
1009
|
-
parent: { type: 'architect' },
|
|
1010
|
-
};
|
|
1011
|
-
addAnnotation(annotation);
|
|
1012
|
-
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
1013
|
-
res.end(JSON.stringify({ id: annotation.id, port: openPort }));
|
|
1014
|
-
return;
|
|
1015
|
-
}
|
|
1016
|
-
// API: Create builder tab (spawns worktree builder with random ID)
|
|
1017
|
-
if (req.method === 'POST' && url.pathname === '/api/tabs/builder') {
|
|
1018
|
-
const builderState = loadState();
|
|
1019
|
-
// DoS protection: check tab limit
|
|
1020
|
-
if (countTotalTabs(builderState) >= CONFIG.maxTabs) {
|
|
1021
|
-
res.writeHead(429, { 'Content-Type': 'text/plain' });
|
|
1022
|
-
res.end(`Tab limit reached (max ${CONFIG.maxTabs}). Close some tabs first.`);
|
|
1023
|
-
return;
|
|
1024
|
-
}
|
|
1025
|
-
// Find available port for builder
|
|
1026
|
-
const builderPort = await findAvailablePort(CONFIG.builderPortStart, builderState);
|
|
1027
|
-
// Spawn worktree builder
|
|
1028
|
-
const result = spawnWorktreeBuilder(builderPort, builderState);
|
|
1029
|
-
if (!result) {
|
|
1030
|
-
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
1031
|
-
res.end('Failed to spawn worktree builder');
|
|
1032
|
-
return;
|
|
1033
|
-
}
|
|
1034
|
-
// Wait for ttyd to be ready
|
|
1035
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1036
|
-
// Save builder to state
|
|
1037
|
-
upsertBuilder(result.builder);
|
|
1038
|
-
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
1039
|
-
res.end(JSON.stringify({ id: result.builder.id, port: result.builder.port, name: result.builder.name }));
|
|
1040
|
-
return;
|
|
1041
|
-
}
|
|
1042
|
-
// API: Create shell tab (supports worktree parameter for Spec 0057)
|
|
1043
|
-
if (req.method === 'POST' && url.pathname === '/api/tabs/shell') {
|
|
1044
|
-
const body = await parseJsonBody(req);
|
|
1045
|
-
const name = body.name || undefined;
|
|
1046
|
-
const command = body.command || undefined;
|
|
1047
|
-
const worktree = body.worktree === true;
|
|
1048
|
-
const branch = body.branch || undefined;
|
|
1049
|
-
// Validate name if provided (prevent command injection)
|
|
1050
|
-
if (name && !/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
1051
|
-
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
1052
|
-
res.end('Invalid name format');
|
|
1053
|
-
return;
|
|
1054
|
-
}
|
|
1055
|
-
// Validate branch name if provided (prevent command injection)
|
|
1056
|
-
// Allow: letters, numbers, underscores, hyphens, slashes, dots
|
|
1057
|
-
// Reject: control chars, spaces, .., @{, trailing/leading slashes
|
|
1058
|
-
if (branch) {
|
|
1059
|
-
const invalidPatterns = [
|
|
1060
|
-
/[\x00-\x1f\x7f]/, // Control characters
|
|
1061
|
-
/\s/, // Whitespace
|
|
1062
|
-
/\.\./, // Parent directory traversal
|
|
1063
|
-
/@\{/, // Git reflog syntax
|
|
1064
|
-
/^\//, // Leading slash
|
|
1065
|
-
/\/$/, // Trailing slash
|
|
1066
|
-
/\/\//, // Double slash
|
|
1067
|
-
/^-/, // Leading hyphen (could be flag)
|
|
1068
|
-
];
|
|
1069
|
-
const isInvalid = invalidPatterns.some(p => p.test(branch));
|
|
1070
|
-
if (isInvalid) {
|
|
1071
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1072
|
-
res.end(JSON.stringify({
|
|
1073
|
-
success: false,
|
|
1074
|
-
error: 'Invalid branch name. Avoid spaces, control characters, .., @{, and leading/trailing slashes.'
|
|
1075
|
-
}));
|
|
1076
|
-
return;
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
const shellState = loadState();
|
|
1080
|
-
// DoS protection: check tab limit
|
|
1081
|
-
if (countTotalTabs(shellState) >= CONFIG.maxTabs) {
|
|
1082
|
-
res.writeHead(429, { 'Content-Type': 'text/plain' });
|
|
1083
|
-
res.end(`Tab limit reached (max ${CONFIG.maxTabs}). Close some tabs first.`);
|
|
1084
|
-
return;
|
|
1085
|
-
}
|
|
1086
|
-
// Determine working directory (project root or worktree)
|
|
1087
|
-
let cwd = projectRoot;
|
|
1088
|
-
let worktreePath;
|
|
1089
|
-
if (worktree) {
|
|
1090
|
-
// Create worktree for the shell
|
|
1091
|
-
const worktreesDir = path.join(projectRoot, '.worktrees');
|
|
1092
|
-
if (!fs.existsSync(worktreesDir)) {
|
|
1093
|
-
fs.mkdirSync(worktreesDir, { recursive: true });
|
|
1094
|
-
}
|
|
1095
|
-
// Generate worktree name
|
|
1096
|
-
const worktreeName = branch || `temp-${Date.now()}`;
|
|
1097
|
-
worktreePath = path.join(worktreesDir, worktreeName);
|
|
1098
|
-
// Check if worktree already exists
|
|
1099
|
-
if (fs.existsSync(worktreePath)) {
|
|
1100
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1101
|
-
res.end(JSON.stringify({
|
|
1102
|
-
success: false,
|
|
1103
|
-
error: `Worktree '${worktreeName}' already exists at ${worktreePath}`
|
|
1104
|
-
}));
|
|
1105
|
-
return;
|
|
1106
|
-
}
|
|
1107
|
-
// Create worktree
|
|
1108
|
-
try {
|
|
1109
|
-
let gitCmd;
|
|
1110
|
-
if (branch) {
|
|
1111
|
-
// Check if branch already exists
|
|
1112
|
-
let branchExists = false;
|
|
1113
|
-
try {
|
|
1114
|
-
execSync(`git rev-parse --verify "${branch}"`, { cwd: projectRoot, stdio: 'pipe' });
|
|
1115
|
-
branchExists = true;
|
|
1116
|
-
}
|
|
1117
|
-
catch {
|
|
1118
|
-
// Branch doesn't exist
|
|
1119
|
-
}
|
|
1120
|
-
if (branchExists) {
|
|
1121
|
-
// Checkout existing branch into worktree
|
|
1122
|
-
gitCmd = `git worktree add "${worktreePath}" "${branch}"`;
|
|
1123
|
-
}
|
|
1124
|
-
else {
|
|
1125
|
-
// Create new branch and worktree
|
|
1126
|
-
gitCmd = `git worktree add "${worktreePath}" -b "${branch}"`;
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
else {
|
|
1130
|
-
// Detached HEAD worktree
|
|
1131
|
-
gitCmd = `git worktree add "${worktreePath}" --detach`;
|
|
1132
|
-
}
|
|
1133
|
-
execSync(gitCmd, { cwd: projectRoot, stdio: 'pipe' });
|
|
1134
|
-
// Symlink .env from project root into worktree (if it exists)
|
|
1135
|
-
const rootEnvPath = path.join(projectRoot, '.env');
|
|
1136
|
-
const worktreeEnvPath = path.join(worktreePath, '.env');
|
|
1137
|
-
if (fs.existsSync(rootEnvPath) && !fs.existsSync(worktreeEnvPath)) {
|
|
1138
|
-
try {
|
|
1139
|
-
fs.symlinkSync(rootEnvPath, worktreeEnvPath);
|
|
1140
|
-
}
|
|
1141
|
-
catch {
|
|
1142
|
-
// Non-fatal: continue without .env symlink
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
cwd = worktreePath;
|
|
1146
|
-
}
|
|
1147
|
-
catch (gitError) {
|
|
1148
|
-
const errorMsg = gitError instanceof Error
|
|
1149
|
-
? gitError.stderr?.toString() || gitError.message
|
|
1150
|
-
: 'Unknown error';
|
|
1151
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1152
|
-
res.end(JSON.stringify({
|
|
1153
|
-
success: false,
|
|
1154
|
-
error: `Git worktree creation failed: ${errorMsg}`
|
|
1155
|
-
}));
|
|
1156
|
-
return;
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
// Generate ID and name
|
|
1160
|
-
const id = generateId('U');
|
|
1161
|
-
const utilName = name || (worktree ? `worktree-${shellState.utils.length + 1}` : `shell-${shellState.utils.length + 1}`);
|
|
1162
|
-
const sessionName = `af-shell-${id}`;
|
|
1163
|
-
// Get shell command - if command provided, run it then keep shell open
|
|
1164
|
-
const shell = process.env.SHELL || '/bin/bash';
|
|
1165
|
-
const shellCommand = command
|
|
1166
|
-
? `${shell} -c '${command.replace(/'/g, "'\\''")}; exec ${shell}'`
|
|
1167
|
-
: shell;
|
|
1168
|
-
// Retry loop for concurrent port allocation race conditions
|
|
1169
|
-
const MAX_PORT_RETRIES = 5;
|
|
1170
|
-
let utilPort = null;
|
|
1171
|
-
let pid = null;
|
|
1172
|
-
for (let attempt = 0; attempt < MAX_PORT_RETRIES; attempt++) {
|
|
1173
|
-
// Get fresh state on each attempt to see newly allocated ports
|
|
1174
|
-
const currentState = loadState();
|
|
1175
|
-
const candidatePort = await findAvailablePort(CONFIG.utilPortStart, currentState);
|
|
1176
|
-
// Start tmux session with ttyd attached (use cwd which may be worktree)
|
|
1177
|
-
const spawnedPid = spawnTmuxWithTtyd(sessionName, shellCommand, candidatePort, cwd);
|
|
1178
|
-
if (!spawnedPid) {
|
|
1179
|
-
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
1180
|
-
res.end('Failed to start shell');
|
|
1181
|
-
return;
|
|
1182
|
-
}
|
|
1183
|
-
// Wait for ttyd to be ready
|
|
1184
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1185
|
-
// Try to add util record - may fail if port was taken by concurrent request
|
|
1186
|
-
const util = {
|
|
1187
|
-
id,
|
|
1188
|
-
name: utilName,
|
|
1189
|
-
port: candidatePort,
|
|
1190
|
-
pid: spawnedPid,
|
|
1191
|
-
tmuxSession: sessionName,
|
|
1192
|
-
worktreePath: worktreePath, // Track for cleanup on tab close
|
|
1193
|
-
};
|
|
1194
|
-
if (tryAddUtil(util)) {
|
|
1195
|
-
// Success - port reserved
|
|
1196
|
-
utilPort = candidatePort;
|
|
1197
|
-
pid = spawnedPid;
|
|
1198
|
-
break;
|
|
1199
|
-
}
|
|
1200
|
-
// Port conflict - kill the spawned process and retry
|
|
1201
|
-
console.log(`[info] Port ${candidatePort} conflict, retrying (attempt ${attempt + 1}/${MAX_PORT_RETRIES})`);
|
|
1202
|
-
await killProcessGracefully(spawnedPid);
|
|
1203
|
-
}
|
|
1204
|
-
if (utilPort === null || pid === null) {
|
|
1205
|
-
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
1206
|
-
res.end('Failed to allocate port after multiple retries');
|
|
1207
|
-
return;
|
|
1208
|
-
}
|
|
1209
|
-
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
1210
|
-
res.end(JSON.stringify({ success: true, id, port: utilPort, name: utilName }));
|
|
1211
|
-
return;
|
|
1212
|
-
}
|
|
1213
|
-
// API: Check if tab process is running (Bugfix #132)
|
|
1214
|
-
if (req.method === 'GET' && url.pathname.match(/^\/api\/tabs\/[^/]+\/running$/)) {
|
|
1215
|
-
const match = url.pathname.match(/^\/api\/tabs\/([^/]+)\/running$/);
|
|
1216
|
-
if (!match) {
|
|
1217
|
-
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
1218
|
-
res.end('Invalid tab ID');
|
|
1219
|
-
return;
|
|
1220
|
-
}
|
|
1221
|
-
const tabId = decodeURIComponent(match[1]);
|
|
1222
|
-
let running = false;
|
|
1223
|
-
let found = false;
|
|
1224
|
-
// Check if it's a shell tab
|
|
1225
|
-
if (tabId.startsWith('shell-')) {
|
|
1226
|
-
const utilId = tabId.replace('shell-', '');
|
|
1227
|
-
const tabUtils = getUtils();
|
|
1228
|
-
const util = tabUtils.find((u) => u.id === utilId);
|
|
1229
|
-
if (util) {
|
|
1230
|
-
found = true;
|
|
1231
|
-
running = isProcessRunning(util.pid);
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
// Check if it's a builder tab
|
|
1235
|
-
if (tabId.startsWith('builder-')) {
|
|
1236
|
-
const builderId = tabId.replace('builder-', '');
|
|
1237
|
-
const builder = getBuilder(builderId);
|
|
1238
|
-
if (builder) {
|
|
1239
|
-
found = true;
|
|
1240
|
-
running = isProcessRunning(builder.pid);
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
if (found) {
|
|
1244
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1245
|
-
res.end(JSON.stringify({ running }));
|
|
1246
|
-
}
|
|
1247
|
-
else {
|
|
1248
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1249
|
-
res.end(JSON.stringify({ running: false }));
|
|
1250
|
-
}
|
|
1251
|
-
return;
|
|
1252
|
-
}
|
|
1253
|
-
// API: Close tab
|
|
1254
|
-
if (req.method === 'DELETE' && url.pathname.startsWith('/api/tabs/')) {
|
|
1255
|
-
const tabId = decodeURIComponent(url.pathname.replace('/api/tabs/', ''));
|
|
1256
|
-
let found = false;
|
|
1257
|
-
// Check if it's a file tab
|
|
1258
|
-
if (tabId.startsWith('file-')) {
|
|
1259
|
-
const annotationId = tabId.replace('file-', '');
|
|
1260
|
-
const tabAnnotations = getAnnotations();
|
|
1261
|
-
const annotation = tabAnnotations.find((a) => a.id === annotationId);
|
|
1262
|
-
if (annotation) {
|
|
1263
|
-
await killProcessGracefully(annotation.pid);
|
|
1264
|
-
removeAnnotation(annotationId);
|
|
1265
|
-
found = true;
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
// Check if it's a builder tab
|
|
1269
|
-
if (tabId.startsWith('builder-')) {
|
|
1270
|
-
const builderId = tabId.replace('builder-', '');
|
|
1271
|
-
const builder = getBuilder(builderId);
|
|
1272
|
-
if (builder) {
|
|
1273
|
-
await killProcessGracefully(builder.pid);
|
|
1274
|
-
removeBuilder(builderId);
|
|
1275
|
-
found = true;
|
|
1276
|
-
}
|
|
1277
|
-
}
|
|
1278
|
-
// Check if it's a shell tab
|
|
1279
|
-
if (tabId.startsWith('shell-')) {
|
|
1280
|
-
const utilId = tabId.replace('shell-', '');
|
|
1281
|
-
const tabUtils = getUtils();
|
|
1282
|
-
const util = tabUtils.find((u) => u.id === utilId);
|
|
1283
|
-
if (util) {
|
|
1284
|
-
await killProcessGracefully(util.pid, util.tmuxSession);
|
|
1285
|
-
// Note: worktrees are NOT cleaned up on tab close - they may contain useful context
|
|
1286
|
-
// Users can manually clean up with `git worktree list` and `git worktree remove`
|
|
1287
|
-
removeUtil(utilId);
|
|
1288
|
-
found = true;
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
if (found) {
|
|
1292
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1293
|
-
res.end(JSON.stringify({ success: true }));
|
|
1294
|
-
}
|
|
1295
|
-
else {
|
|
1296
|
-
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
1297
|
-
res.end('Tab not found');
|
|
1298
|
-
}
|
|
1299
|
-
return;
|
|
1300
|
-
}
|
|
1301
|
-
// API: Stop all
|
|
1302
|
-
if (req.method === 'POST' && url.pathname === '/api/stop') {
|
|
1303
|
-
const stopState = loadState();
|
|
1304
|
-
// Kill all tmux sessions first
|
|
1305
|
-
for (const util of stopState.utils) {
|
|
1306
|
-
if (util.tmuxSession) {
|
|
1307
|
-
killTmuxSession(util.tmuxSession);
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
if (stopState.architect?.tmuxSession) {
|
|
1311
|
-
killTmuxSession(stopState.architect.tmuxSession);
|
|
1312
|
-
}
|
|
1313
|
-
// Kill all processes gracefully
|
|
1314
|
-
const pids = [];
|
|
1315
|
-
if (stopState.architect) {
|
|
1316
|
-
pids.push(stopState.architect.pid);
|
|
1317
|
-
}
|
|
1318
|
-
for (const builder of stopState.builders) {
|
|
1319
|
-
pids.push(builder.pid);
|
|
1320
|
-
}
|
|
1321
|
-
for (const util of stopState.utils) {
|
|
1322
|
-
pids.push(util.pid);
|
|
1323
|
-
}
|
|
1324
|
-
for (const annotation of stopState.annotations) {
|
|
1325
|
-
pids.push(annotation.pid);
|
|
1326
|
-
}
|
|
1327
|
-
// Kill all processes in parallel
|
|
1328
|
-
await Promise.all(pids.map((pid) => killProcessGracefully(pid)));
|
|
1329
|
-
// Clear state
|
|
1330
|
-
clearState();
|
|
1331
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1332
|
-
res.end(JSON.stringify({ success: true, killed: pids.length }));
|
|
1333
|
-
// Exit after a short delay
|
|
1334
|
-
setTimeout(() => process.exit(0), 500);
|
|
1335
|
-
return;
|
|
1336
|
-
}
|
|
1337
|
-
// Open file route - handles file clicks from terminal
|
|
1338
|
-
// Returns a small HTML page that messages the dashboard via BroadcastChannel
|
|
1339
|
-
if (req.method === 'GET' && url.pathname === '/open-file') {
|
|
1340
|
-
const filePath = url.searchParams.get('path');
|
|
1341
|
-
const line = url.searchParams.get('line');
|
|
1342
|
-
const sourcePort = url.searchParams.get('sourcePort');
|
|
1343
|
-
if (!filePath) {
|
|
1344
|
-
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
1345
|
-
res.end('Missing path parameter');
|
|
1346
|
-
return;
|
|
1347
|
-
}
|
|
1348
|
-
// Determine base path for relative path resolution
|
|
1349
|
-
// If sourcePort is provided, look up the builder/util to get its worktree
|
|
1350
|
-
let basePath = projectRoot;
|
|
1351
|
-
if (sourcePort) {
|
|
1352
|
-
const portNum = parseInt(sourcePort, 10);
|
|
1353
|
-
const builders = getBuilders();
|
|
1354
|
-
// Check if it's a builder terminal
|
|
1355
|
-
const builder = builders.find((b) => b.port === portNum);
|
|
1356
|
-
if (builder && builder.worktree) {
|
|
1357
|
-
basePath = builder.worktree;
|
|
1358
|
-
}
|
|
1359
|
-
// Check if it's a utility terminal (they run in project root, so no change needed)
|
|
1360
|
-
// Architect terminal also runs in project root
|
|
1361
|
-
}
|
|
1362
|
-
// Validate path is within project (or builder worktree)
|
|
1363
|
-
// For relative paths, resolve against the determined base path
|
|
1364
|
-
let fullPath;
|
|
1365
|
-
if (filePath.startsWith('/')) {
|
|
1366
|
-
// Absolute path - validate against project root
|
|
1367
|
-
fullPath = validatePathWithinProject(filePath);
|
|
1368
|
-
}
|
|
1369
|
-
else {
|
|
1370
|
-
// Relative path - resolve against base path, then validate
|
|
1371
|
-
const resolvedPath = path.resolve(basePath, filePath);
|
|
1372
|
-
// For builder worktrees, the path is within project root (worktrees are under .builders/)
|
|
1373
|
-
fullPath = validatePathWithinProject(resolvedPath);
|
|
1374
|
-
}
|
|
1375
|
-
if (!fullPath) {
|
|
1376
|
-
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
1377
|
-
res.end('Path must be within project directory');
|
|
1378
|
-
return;
|
|
1379
|
-
}
|
|
1380
|
-
// Check file exists
|
|
1381
|
-
if (!fs.existsSync(fullPath)) {
|
|
1382
|
-
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
1383
|
-
res.end(`File not found: ${filePath}`);
|
|
1384
|
-
return;
|
|
1385
|
-
}
|
|
1386
|
-
// HTML-escape the file path for safe display (uses imported escapeHtml from server-utils.js)
|
|
1387
|
-
const safeFilePath = escapeHtml(filePath);
|
|
1388
|
-
const safeLineDisplay = line ? ':' + escapeHtml(line) : '';
|
|
1389
|
-
// Serve a small HTML page that communicates back to dashboard
|
|
1390
|
-
// Note: We only use BroadcastChannel, not API call (dashboard handles tab creation)
|
|
1391
|
-
const html = `<!DOCTYPE html>
|
|
1392
|
-
<html>
|
|
1393
|
-
<head>
|
|
1394
|
-
<title>Opening file...</title>
|
|
1395
|
-
<style>
|
|
1396
|
-
body {
|
|
1397
|
-
font-family: system-ui;
|
|
1398
|
-
background: #1a1a1a;
|
|
1399
|
-
color: #ccc;
|
|
1400
|
-
display: flex;
|
|
1401
|
-
align-items: center;
|
|
1402
|
-
justify-content: center;
|
|
1403
|
-
height: 100vh;
|
|
1404
|
-
margin: 0;
|
|
1405
|
-
}
|
|
1406
|
-
.message { text-align: center; }
|
|
1407
|
-
.path { color: #3b82f6; font-family: monospace; margin: 8px 0; }
|
|
1408
|
-
</style>
|
|
1409
|
-
</head>
|
|
1410
|
-
<body>
|
|
1411
|
-
<div class="message">
|
|
1412
|
-
<p>Opening file...</p>
|
|
1413
|
-
<p class="path">${safeFilePath}${safeLineDisplay}</p>
|
|
1414
|
-
</div>
|
|
1415
|
-
<script>
|
|
1416
|
-
(async function() {
|
|
1417
|
-
const path = ${JSON.stringify(fullPath)};
|
|
1418
|
-
const line = ${line ? parseInt(line, 10) : 'null'};
|
|
1419
|
-
|
|
1420
|
-
// Use BroadcastChannel to message the dashboard
|
|
1421
|
-
// Dashboard will handle opening the file tab
|
|
1422
|
-
const channel = new BroadcastChannel('agent-farm');
|
|
1423
|
-
channel.postMessage({
|
|
1424
|
-
type: 'openFile',
|
|
1425
|
-
path: path,
|
|
1426
|
-
line: line
|
|
1427
|
-
});
|
|
1428
|
-
|
|
1429
|
-
// Close this window/tab after a short delay
|
|
1430
|
-
setTimeout(() => {
|
|
1431
|
-
window.close();
|
|
1432
|
-
// If window.close() doesn't work (wasn't opened by script),
|
|
1433
|
-
// show success message
|
|
1434
|
-
document.body.innerHTML = '<div class="message"><p>File opened in dashboard</p><p class="path">You can close this tab</p></div>';
|
|
1435
|
-
}, 500);
|
|
1436
|
-
})();
|
|
1437
|
-
</script>
|
|
1438
|
-
</body>
|
|
1439
|
-
</html>`;
|
|
1440
|
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
1441
|
-
res.end(html);
|
|
1442
|
-
return;
|
|
1443
|
-
}
|
|
1444
|
-
// API: Check if projectlist.md exists (for starter page polling)
|
|
1445
|
-
if (req.method === 'GET' && url.pathname === '/api/projectlist-exists') {
|
|
1446
|
-
const projectlistPath = path.join(projectRoot, 'codev/projectlist.md');
|
|
1447
|
-
const exists = fs.existsSync(projectlistPath);
|
|
1448
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1449
|
-
res.end(JSON.stringify({ exists }));
|
|
1450
|
-
return;
|
|
1451
|
-
}
|
|
1452
|
-
// Read file contents (for Projects tab to read projectlist.md)
|
|
1453
|
-
if (req.method === 'GET' && url.pathname === '/file') {
|
|
1454
|
-
const filePath = url.searchParams.get('path');
|
|
1455
|
-
if (!filePath) {
|
|
1456
|
-
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
1457
|
-
res.end('Missing path parameter');
|
|
1458
|
-
return;
|
|
1459
|
-
}
|
|
1460
|
-
// Validate path is within project root (prevent path traversal)
|
|
1461
|
-
const fullPath = validatePathWithinProject(filePath);
|
|
1462
|
-
if (!fullPath) {
|
|
1463
|
-
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
1464
|
-
res.end('Path must be within project directory');
|
|
1465
|
-
return;
|
|
1466
|
-
}
|
|
1467
|
-
// Check file exists
|
|
1468
|
-
if (!fs.existsSync(fullPath)) {
|
|
1469
|
-
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
1470
|
-
res.end(`File not found: ${filePath}`);
|
|
1471
|
-
return;
|
|
1472
|
-
}
|
|
1473
|
-
// Read and return file contents
|
|
1474
|
-
try {
|
|
1475
|
-
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
1476
|
-
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
1477
|
-
res.end(content);
|
|
1478
|
-
}
|
|
1479
|
-
catch (err) {
|
|
1480
|
-
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
1481
|
-
res.end('Error reading file: ' + err.message);
|
|
1482
|
-
}
|
|
1483
|
-
return;
|
|
1484
|
-
}
|
|
1485
|
-
// API: Get directory tree for file browser (Spec 0055)
|
|
1486
|
-
if (req.method === 'GET' && url.pathname === '/api/files') {
|
|
1487
|
-
// Directories to exclude from the tree
|
|
1488
|
-
const EXCLUDED_DIRS = new Set([
|
|
1489
|
-
'node_modules',
|
|
1490
|
-
'.git',
|
|
1491
|
-
'dist',
|
|
1492
|
-
'__pycache__',
|
|
1493
|
-
'.next',
|
|
1494
|
-
'.nuxt',
|
|
1495
|
-
'.turbo',
|
|
1496
|
-
'coverage',
|
|
1497
|
-
'.nyc_output',
|
|
1498
|
-
'.cache',
|
|
1499
|
-
'.parcel-cache',
|
|
1500
|
-
'build',
|
|
1501
|
-
'.svelte-kit',
|
|
1502
|
-
'vendor',
|
|
1503
|
-
'.venv',
|
|
1504
|
-
'venv',
|
|
1505
|
-
'env',
|
|
1506
|
-
]);
|
|
1507
|
-
// Recursively build directory tree
|
|
1508
|
-
function buildTree(dirPath, relativePath = '') {
|
|
1509
|
-
const entries = [];
|
|
1510
|
-
try {
|
|
1511
|
-
const items = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
1512
|
-
for (const item of items) {
|
|
1513
|
-
// Skip excluded directories only (allow dotfiles like .github, .eslintrc, etc.)
|
|
1514
|
-
if (EXCLUDED_DIRS.has(item.name))
|
|
1515
|
-
continue;
|
|
1516
|
-
const itemRelPath = relativePath ? `${relativePath}/${item.name}` : item.name;
|
|
1517
|
-
const itemFullPath = path.join(dirPath, item.name);
|
|
1518
|
-
if (item.isDirectory()) {
|
|
1519
|
-
const children = buildTree(itemFullPath, itemRelPath);
|
|
1520
|
-
entries.push({
|
|
1521
|
-
name: item.name,
|
|
1522
|
-
path: itemRelPath,
|
|
1523
|
-
type: 'dir',
|
|
1524
|
-
children,
|
|
1525
|
-
});
|
|
1526
|
-
}
|
|
1527
|
-
else if (item.isFile()) {
|
|
1528
|
-
entries.push({
|
|
1529
|
-
name: item.name,
|
|
1530
|
-
path: itemRelPath,
|
|
1531
|
-
type: 'file',
|
|
1532
|
-
});
|
|
1533
|
-
}
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
1536
|
-
catch (err) {
|
|
1537
|
-
// Ignore permission errors or inaccessible directories
|
|
1538
|
-
console.error(`Error reading directory ${dirPath}:`, err.message);
|
|
1539
|
-
}
|
|
1540
|
-
// Sort: directories first, then files, alphabetically within each group
|
|
1541
|
-
entries.sort((a, b) => {
|
|
1542
|
-
if (a.type === 'dir' && b.type === 'file')
|
|
1543
|
-
return -1;
|
|
1544
|
-
if (a.type === 'file' && b.type === 'dir')
|
|
1545
|
-
return 1;
|
|
1546
|
-
return a.name.localeCompare(b.name);
|
|
1547
|
-
});
|
|
1548
|
-
return entries;
|
|
1549
|
-
}
|
|
1550
|
-
const tree = buildTree(projectRoot);
|
|
1551
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1552
|
-
res.end(JSON.stringify(tree));
|
|
1553
|
-
return;
|
|
1554
|
-
}
|
|
1555
|
-
// API: Get hash of file tree for change detection (auto-refresh)
|
|
1556
|
-
if (req.method === 'GET' && url.pathname === '/api/files/hash') {
|
|
1557
|
-
// Build a lightweight hash based on directory mtimes
|
|
1558
|
-
// This is faster than building the full tree
|
|
1559
|
-
function getTreeHash(dirPath) {
|
|
1560
|
-
const EXCLUDED_DIRS = new Set([
|
|
1561
|
-
'node_modules', '.git', 'dist', '__pycache__', '.next',
|
|
1562
|
-
'.nuxt', '.turbo', 'coverage', '.nyc_output', '.cache',
|
|
1563
|
-
'.parcel-cache', 'build', '.svelte-kit', 'vendor', '.venv', 'venv', 'env',
|
|
1564
|
-
]);
|
|
1565
|
-
let hash = '';
|
|
1566
|
-
function walk(dir) {
|
|
1567
|
-
try {
|
|
1568
|
-
const stat = fs.statSync(dir);
|
|
1569
|
-
hash += `${dir}:${stat.mtimeMs};`;
|
|
1570
|
-
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
1571
|
-
for (const item of items) {
|
|
1572
|
-
if (EXCLUDED_DIRS.has(item.name))
|
|
1573
|
-
continue;
|
|
1574
|
-
if (item.isDirectory()) {
|
|
1575
|
-
walk(path.join(dir, item.name));
|
|
1576
|
-
}
|
|
1577
|
-
else if (item.isFile()) {
|
|
1578
|
-
// Include file mtime for change detection
|
|
1579
|
-
const fileStat = fs.statSync(path.join(dir, item.name));
|
|
1580
|
-
hash += `${item.name}:${fileStat.mtimeMs};`;
|
|
1581
|
-
}
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1584
|
-
catch {
|
|
1585
|
-
// Ignore errors
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1588
|
-
walk(dirPath);
|
|
1589
|
-
// Simple hash: sum of char codes
|
|
1590
|
-
let sum = 0;
|
|
1591
|
-
for (let i = 0; i < hash.length; i++) {
|
|
1592
|
-
sum = ((sum << 5) - sum + hash.charCodeAt(i)) | 0;
|
|
1593
|
-
}
|
|
1594
|
-
return sum.toString(16);
|
|
1595
|
-
}
|
|
1596
|
-
const hash = getTreeHash(projectRoot);
|
|
1597
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1598
|
-
res.end(JSON.stringify({ hash }));
|
|
1599
|
-
return;
|
|
1600
|
-
}
|
|
1601
|
-
// API: Create a new file (Bugfix #131)
|
|
1602
|
-
if (req.method === 'POST' && url.pathname === '/api/files') {
|
|
1603
|
-
const body = await parseJsonBody(req);
|
|
1604
|
-
const filePath = body.path;
|
|
1605
|
-
const content = body.content || '';
|
|
1606
|
-
if (!filePath) {
|
|
1607
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1608
|
-
res.end(JSON.stringify({ error: 'Missing path' }));
|
|
1609
|
-
return;
|
|
1610
|
-
}
|
|
1611
|
-
// Validate path is within project root (prevent path traversal)
|
|
1612
|
-
const fullPath = validatePathWithinProject(filePath);
|
|
1613
|
-
if (!fullPath) {
|
|
1614
|
-
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1615
|
-
res.end(JSON.stringify({ error: 'Path must be within project directory' }));
|
|
1616
|
-
return;
|
|
1617
|
-
}
|
|
1618
|
-
// Check if file already exists
|
|
1619
|
-
if (fs.existsSync(fullPath)) {
|
|
1620
|
-
res.writeHead(409, { 'Content-Type': 'application/json' });
|
|
1621
|
-
res.end(JSON.stringify({ error: 'File already exists' }));
|
|
1622
|
-
return;
|
|
1623
|
-
}
|
|
1624
|
-
// Additional security: validate parent directories don't symlink outside project
|
|
1625
|
-
// Find the deepest existing parent and ensure it's within project
|
|
1626
|
-
let checkDir = path.dirname(fullPath);
|
|
1627
|
-
while (checkDir !== projectRoot && !fs.existsSync(checkDir)) {
|
|
1628
|
-
checkDir = path.dirname(checkDir);
|
|
1629
|
-
}
|
|
1630
|
-
if (fs.existsSync(checkDir) && checkDir !== projectRoot) {
|
|
1631
|
-
try {
|
|
1632
|
-
const realParent = fs.realpathSync(checkDir);
|
|
1633
|
-
if (!realParent.startsWith(projectRoot + path.sep) && realParent !== projectRoot) {
|
|
1634
|
-
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1635
|
-
res.end(JSON.stringify({ error: 'Path must be within project directory' }));
|
|
1636
|
-
return;
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
catch {
|
|
1640
|
-
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1641
|
-
res.end(JSON.stringify({ error: 'Cannot resolve path' }));
|
|
1642
|
-
return;
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
try {
|
|
1646
|
-
// Create parent directories if they don't exist
|
|
1647
|
-
const parentDir = path.dirname(fullPath);
|
|
1648
|
-
if (!fs.existsSync(parentDir)) {
|
|
1649
|
-
fs.mkdirSync(parentDir, { recursive: true });
|
|
1650
|
-
}
|
|
1651
|
-
// Write the file
|
|
1652
|
-
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
1653
|
-
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
1654
|
-
res.end(JSON.stringify({ success: true, path: filePath }));
|
|
1655
|
-
}
|
|
1656
|
-
catch (err) {
|
|
1657
|
-
console.error('Error creating file:', err.message);
|
|
1658
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1659
|
-
res.end(JSON.stringify({ error: 'Failed to create file: ' + err.message }));
|
|
1660
|
-
}
|
|
1661
|
-
return;
|
|
1662
|
-
}
|
|
1663
|
-
// API: Hot reload check (Spec 0060)
|
|
1664
|
-
// Returns modification times for all dashboard CSS/JS files
|
|
1665
|
-
if (req.method === 'GET' && url.pathname === '/api/hot-reload') {
|
|
1666
|
-
try {
|
|
1667
|
-
const dashboardDir = path.join(__dirname, '../../../templates/dashboard');
|
|
1668
|
-
const cssDir = path.join(dashboardDir, 'css');
|
|
1669
|
-
const jsDir = path.join(dashboardDir, 'js');
|
|
1670
|
-
const mtimes = {};
|
|
1671
|
-
// Collect CSS file modification times
|
|
1672
|
-
if (fs.existsSync(cssDir)) {
|
|
1673
|
-
for (const file of fs.readdirSync(cssDir)) {
|
|
1674
|
-
if (file.endsWith('.css')) {
|
|
1675
|
-
const stat = fs.statSync(path.join(cssDir, file));
|
|
1676
|
-
mtimes[`css/${file}`] = stat.mtimeMs;
|
|
1677
|
-
}
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
// Collect JS file modification times
|
|
1681
|
-
if (fs.existsSync(jsDir)) {
|
|
1682
|
-
for (const file of fs.readdirSync(jsDir)) {
|
|
1683
|
-
if (file.endsWith('.js')) {
|
|
1684
|
-
const stat = fs.statSync(path.join(jsDir, file));
|
|
1685
|
-
mtimes[`js/${file}`] = stat.mtimeMs;
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1690
|
-
res.end(JSON.stringify({ mtimes }));
|
|
1691
|
-
}
|
|
1692
|
-
catch (err) {
|
|
1693
|
-
console.error('Hot reload check error:', err);
|
|
1694
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1695
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
1696
|
-
}
|
|
1697
|
-
return;
|
|
1698
|
-
}
|
|
1699
|
-
// Serve dashboard CSS files
|
|
1700
|
-
if (req.method === 'GET' && url.pathname.startsWith('/dashboard/css/')) {
|
|
1701
|
-
const filename = url.pathname.replace('/dashboard/css/', '');
|
|
1702
|
-
// Validate filename to prevent path traversal
|
|
1703
|
-
if (!filename || filename.includes('..') || filename.includes('/') || !filename.endsWith('.css')) {
|
|
1704
|
-
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
1705
|
-
res.end('Invalid filename');
|
|
1706
|
-
return;
|
|
1707
|
-
}
|
|
1708
|
-
const cssPath = path.join(__dirname, '../../../templates/dashboard/css', filename);
|
|
1709
|
-
if (fs.existsSync(cssPath)) {
|
|
1710
|
-
const content = fs.readFileSync(cssPath, 'utf-8');
|
|
1711
|
-
res.writeHead(200, { 'Content-Type': 'text/css; charset=utf-8' });
|
|
1712
|
-
res.end(content);
|
|
1713
|
-
return;
|
|
1714
|
-
}
|
|
1715
|
-
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
1716
|
-
res.end('CSS file not found');
|
|
1717
|
-
return;
|
|
1718
|
-
}
|
|
1719
|
-
// Serve dashboard JS files
|
|
1720
|
-
if (req.method === 'GET' && url.pathname.startsWith('/dashboard/js/')) {
|
|
1721
|
-
const filename = url.pathname.replace('/dashboard/js/', '');
|
|
1722
|
-
// Validate filename to prevent path traversal
|
|
1723
|
-
if (!filename || filename.includes('..') || filename.includes('/') || !filename.endsWith('.js')) {
|
|
1724
|
-
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
1725
|
-
res.end('Invalid filename');
|
|
1726
|
-
return;
|
|
1727
|
-
}
|
|
1728
|
-
const jsPath = path.join(__dirname, '../../../templates/dashboard/js', filename);
|
|
1729
|
-
if (fs.existsSync(jsPath)) {
|
|
1730
|
-
const content = fs.readFileSync(jsPath, 'utf-8');
|
|
1731
|
-
res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8' });
|
|
1732
|
-
res.end(content);
|
|
1733
|
-
return;
|
|
1734
|
-
}
|
|
1735
|
-
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
1736
|
-
res.end('JS file not found');
|
|
1737
|
-
return;
|
|
1738
|
-
}
|
|
1739
|
-
// Terminal proxy route (Spec 0062 - Secure Remote Access)
|
|
1740
|
-
// Routes /terminal/:id to the appropriate ttyd instance
|
|
1741
|
-
const terminalMatch = url.pathname.match(/^\/terminal\/([^/]+)(\/.*)?$/);
|
|
1742
|
-
if (terminalMatch) {
|
|
1743
|
-
const terminalId = terminalMatch[1];
|
|
1744
|
-
const terminalPort = getPortForTerminal(terminalId, loadState());
|
|
1745
|
-
if (!terminalPort) {
|
|
1746
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1747
|
-
res.end(JSON.stringify({ error: `Terminal not found: ${terminalId}` }));
|
|
1748
|
-
return;
|
|
1749
|
-
}
|
|
1750
|
-
// Rewrite the URL to strip the /terminal/:id prefix
|
|
1751
|
-
req.url = terminalMatch[2] || '/';
|
|
1752
|
-
terminalProxy.web(req, res, { target: `http://localhost:${terminalPort}` });
|
|
1753
|
-
return;
|
|
1754
|
-
}
|
|
1755
|
-
// Annotation proxy route (Spec 0062 - Secure Remote Access)
|
|
1756
|
-
// Routes /annotation/:id to the appropriate open-server instance
|
|
1757
|
-
const annotationMatch = url.pathname.match(/^\/annotation\/([^/]+)(\/.*)?$/);
|
|
1758
|
-
if (annotationMatch) {
|
|
1759
|
-
const annotationId = annotationMatch[1];
|
|
1760
|
-
const annotations = getAnnotations();
|
|
1761
|
-
const annotation = annotations.find((a) => a.id === annotationId);
|
|
1762
|
-
if (!annotation) {
|
|
1763
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1764
|
-
res.end(JSON.stringify({ error: `Annotation not found: ${annotationId}` }));
|
|
1765
|
-
return;
|
|
1766
|
-
}
|
|
1767
|
-
// Rewrite the URL to strip the /annotation/:id prefix, preserving query string
|
|
1768
|
-
const remainingPath = annotationMatch[2] || '/';
|
|
1769
|
-
req.url = url.search ? `${remainingPath}${url.search}` : remainingPath;
|
|
1770
|
-
terminalProxy.web(req, res, { target: `http://localhost:${annotation.port}` });
|
|
1771
|
-
return;
|
|
1772
|
-
}
|
|
1773
|
-
// Serve dashboard
|
|
1774
|
-
if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/index.html')) {
|
|
1775
|
-
try {
|
|
1776
|
-
let template = fs.readFileSync(templatePath, 'utf-8');
|
|
1777
|
-
const state = loadStateWithCleanup();
|
|
1778
|
-
// Inject project name into template (HTML-escaped for security)
|
|
1779
|
-
const projectName = escapeHtml(getProjectName(projectRoot));
|
|
1780
|
-
template = template.replace(/\{\{PROJECT_NAME\}\}/g, projectName);
|
|
1781
|
-
// Inject state into template
|
|
1782
|
-
const stateJson = JSON.stringify(state);
|
|
1783
|
-
template = template.replace('// STATE_INJECTION_POINT', `window.INITIAL_STATE = ${stateJson};`);
|
|
1784
|
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
1785
|
-
res.end(template);
|
|
1786
|
-
}
|
|
1787
|
-
catch (err) {
|
|
1788
|
-
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
1789
|
-
res.end('Error loading dashboard: ' + err.message);
|
|
1790
|
-
}
|
|
1791
|
-
return;
|
|
1792
|
-
}
|
|
1793
|
-
// 404 for everything else
|
|
1794
|
-
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
1795
|
-
res.end('Not found');
|
|
1796
|
-
}
|
|
1797
|
-
catch (err) {
|
|
1798
|
-
console.error('Request error:', err);
|
|
1799
|
-
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
1800
|
-
res.end('Internal server error: ' + err.message);
|
|
1801
|
-
}
|
|
1802
|
-
});
|
|
1803
|
-
// WebSocket upgrade handler for terminal proxy (Spec 0062)
|
|
1804
|
-
// ttyd uses WebSocket for bidirectional terminal communication
|
|
1805
|
-
server.on('upgrade', (req, socket, head) => {
|
|
1806
|
-
// Security check
|
|
1807
|
-
const host = req.headers.host;
|
|
1808
|
-
if (!insecureRemoteMode && host && !host.startsWith('localhost') && !host.startsWith('127.0.0.1')) {
|
|
1809
|
-
socket.destroy();
|
|
1810
|
-
return;
|
|
1811
|
-
}
|
|
1812
|
-
const reqUrl = new URL(req.url || '/', `http://localhost:${port}`);
|
|
1813
|
-
const terminalMatch = reqUrl.pathname.match(/^\/terminal\/([^/]+)(\/.*)?$/);
|
|
1814
|
-
if (terminalMatch) {
|
|
1815
|
-
const terminalId = terminalMatch[1];
|
|
1816
|
-
const terminalPort = getPortForTerminal(terminalId, loadState());
|
|
1817
|
-
if (terminalPort) {
|
|
1818
|
-
// Rewrite URL to strip /terminal/:id prefix
|
|
1819
|
-
req.url = terminalMatch[2] || '/';
|
|
1820
|
-
terminalProxy.ws(req, socket, head, { target: `http://localhost:${terminalPort}` });
|
|
1821
|
-
}
|
|
1822
|
-
else {
|
|
1823
|
-
// Terminal not found - close the socket
|
|
1824
|
-
socket.destroy();
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
// Non-terminal WebSocket requests are ignored (socket will time out)
|
|
1828
|
-
});
|
|
1829
|
-
// Handle WebSocket proxy errors separately
|
|
1830
|
-
terminalProxy.on('error', (err, req, socket) => {
|
|
1831
|
-
console.error('WebSocket proxy error:', err.message);
|
|
1832
|
-
if (socket && 'destroy' in socket && typeof socket.destroy === 'function' && !socket.destroyed) {
|
|
1833
|
-
socket.destroy();
|
|
1834
|
-
}
|
|
1835
|
-
});
|
|
1836
|
-
// Handle server errors (e.g., port already in use)
|
|
1837
|
-
server.on('error', (err) => {
|
|
1838
|
-
if (err.code === 'EADDRINUSE') {
|
|
1839
|
-
console.error(`Error: Port ${port} is already in use.`);
|
|
1840
|
-
console.error(`Run 'lsof -i :${port}' to find the process, or use 'af ports cleanup' to clean up orphans.`);
|
|
1841
|
-
process.exit(1);
|
|
1842
|
-
}
|
|
1843
|
-
else {
|
|
1844
|
-
console.error(`Server error: ${err.message}`);
|
|
1845
|
-
process.exit(1);
|
|
1846
|
-
}
|
|
1847
|
-
});
|
|
1848
|
-
if (bindHost) {
|
|
1849
|
-
server.listen(port, bindHost, () => {
|
|
1850
|
-
console.log(`Dashboard: http://${bindHost}:${port}`);
|
|
1851
|
-
});
|
|
1852
|
-
}
|
|
1853
|
-
else {
|
|
1854
|
-
server.listen(port, () => {
|
|
1855
|
-
console.log(`Dashboard: http://localhost:${port}`);
|
|
1856
|
-
});
|
|
1857
|
-
}
|
|
1858
|
-
//# sourceMappingURL=dashboard-server.js.map
|