@jonit-dev/night-watch-cli 1.7.29 → 1.7.31
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/night-watch.mjs +1 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/{src/cli.js → cli.js} +1 -0
- package/dist/cli.js.map +1 -0
- package/dist/{src/commands → commands}/audit.d.ts +2 -2
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +105 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/{src/commands → commands}/board.d.ts +1 -1
- package/dist/commands/board.d.ts.map +1 -0
- package/dist/commands/board.js +664 -0
- package/dist/commands/board.js.map +1 -0
- package/dist/{src/commands → commands}/cancel.d.ts +3 -3
- package/dist/commands/cancel.d.ts.map +1 -0
- package/dist/{src/commands → commands}/cancel.js +18 -20
- package/dist/commands/cancel.js.map +1 -0
- package/dist/commands/dashboard/tab-actions.d.ts.map +1 -0
- package/dist/commands/dashboard/tab-actions.js.map +1 -0
- package/dist/{src/commands → commands}/dashboard/tab-config.d.ts +3 -3
- package/dist/commands/dashboard/tab-config.d.ts.map +1 -0
- package/dist/{src/commands → commands}/dashboard/tab-config.js +250 -187
- package/dist/commands/dashboard/tab-config.js.map +1 -0
- package/dist/{src/commands → commands}/dashboard/tab-logs.d.ts +1 -1
- package/dist/commands/dashboard/tab-logs.d.ts.map +1 -0
- package/dist/{src/commands → commands}/dashboard/tab-logs.js +62 -38
- package/dist/commands/dashboard/tab-logs.js.map +1 -0
- package/dist/{src/commands → commands}/dashboard/tab-schedules.d.ts +1 -1
- package/dist/commands/dashboard/tab-schedules.d.ts.map +1 -0
- package/dist/{src/commands → commands}/dashboard/tab-schedules.js +85 -76
- package/dist/commands/dashboard/tab-schedules.js.map +1 -0
- package/dist/{src/commands → commands}/dashboard/tab-status.d.ts +7 -7
- package/dist/commands/dashboard/tab-status.d.ts.map +1 -0
- package/dist/{src/commands → commands}/dashboard/tab-status.js +98 -95
- package/dist/commands/dashboard/tab-status.js.map +1 -0
- package/dist/{src/commands → commands}/dashboard/types.d.ts +3 -4
- package/dist/commands/dashboard/types.d.ts.map +1 -0
- package/dist/commands/dashboard/types.js.map +1 -0
- package/dist/{src/commands → commands}/dashboard.d.ts +2 -2
- package/dist/commands/dashboard.d.ts.map +1 -0
- package/dist/{src/commands → commands}/dashboard.js +32 -33
- package/dist/commands/dashboard.js.map +1 -0
- package/dist/{src/commands → commands}/doctor.d.ts +2 -2
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/{src/commands → commands}/doctor.js +40 -43
- package/dist/commands/doctor.js.map +1 -0
- package/dist/{src/commands → commands}/history.d.ts +1 -1
- package/dist/commands/history.d.ts.map +1 -0
- package/dist/{src/commands → commands}/history.js +11 -18
- package/dist/commands/history.js.map +1 -0
- package/dist/{src/commands → commands}/init.d.ts +1 -1
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/{src/commands → commands}/init.js +62 -36
- package/dist/commands/init.js.map +1 -0
- package/dist/{src/commands → commands}/install.d.ts +2 -2
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/{src/commands → commands}/install.js +48 -50
- package/dist/commands/install.js.map +1 -0
- package/dist/{src/commands → commands}/logs.d.ts +1 -1
- package/dist/commands/logs.d.ts.map +1 -0
- package/dist/{src/commands → commands}/logs.js +29 -30
- package/dist/commands/logs.js.map +1 -0
- package/dist/{src/commands → commands}/prd-state.d.ts +1 -1
- package/dist/commands/prd-state.d.ts.map +1 -0
- package/dist/{src/commands → commands}/prd-state.js +14 -14
- package/dist/commands/prd-state.js.map +1 -0
- package/dist/{src/commands → commands}/prd.d.ts +1 -1
- package/dist/commands/prd.d.ts.map +1 -0
- package/dist/{src/commands → commands}/prd.js +57 -66
- package/dist/commands/prd.js.map +1 -0
- package/dist/{src/commands → commands}/prds.d.ts +1 -1
- package/dist/commands/prds.d.ts.map +1 -0
- package/dist/{src/commands → commands}/prds.js +51 -53
- package/dist/commands/prds.js.map +1 -0
- package/dist/{src/commands → commands}/prs.d.ts +1 -1
- package/dist/commands/prs.d.ts.map +1 -0
- package/dist/{src/commands → commands}/prs.js +22 -24
- package/dist/commands/prs.js.map +1 -0
- package/dist/{src/commands → commands}/qa.d.ts +2 -2
- package/dist/commands/qa.d.ts.map +1 -0
- package/dist/{src/commands → commands}/qa.js +50 -51
- package/dist/commands/qa.js.map +1 -0
- package/dist/{src/commands → commands}/retry.d.ts +1 -1
- package/dist/commands/retry.d.ts.map +1 -0
- package/dist/{src/commands → commands}/retry.js +9 -10
- package/dist/commands/retry.js.map +1 -0
- package/dist/{src/commands → commands}/review.d.ts +2 -2
- package/dist/commands/review.d.ts.map +1 -0
- package/dist/{src/commands → commands}/review.js +68 -59
- package/dist/commands/review.js.map +1 -0
- package/dist/{src/commands → commands}/run.d.ts +2 -2
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/{src/commands → commands}/run.js +87 -83
- package/dist/commands/run.js.map +1 -0
- package/dist/{src/commands → commands}/serve.d.ts +2 -2
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/{src/commands → commands}/serve.js +18 -18
- package/dist/commands/serve.js.map +1 -0
- package/dist/{src/commands → commands}/slice.d.ts +2 -2
- package/dist/commands/slice.d.ts.map +1 -0
- package/dist/{src/commands → commands}/slice.js +50 -46
- package/dist/commands/slice.js.map +1 -0
- package/dist/{src/commands → commands}/state.d.ts +1 -1
- package/dist/commands/state.d.ts.map +1 -0
- package/dist/{src/commands → commands}/state.js +20 -22
- package/dist/commands/state.js.map +1 -0
- package/dist/{src/commands → commands}/status.d.ts +1 -1
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/{src/commands → commands}/status.js +75 -54
- package/dist/commands/status.js.map +1 -0
- package/dist/{src/commands → commands}/uninstall.d.ts +1 -1
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/{src/commands → commands}/uninstall.js +12 -14
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/{src/commands → commands}/update.d.ts +1 -1
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/{src/commands → commands}/update.js +23 -23
- package/dist/commands/update.js.map +1 -0
- package/package.json +18 -42
- package/LICENSE +0 -21
- package/README.md +0 -132
- package/dist/shared/types.d.ts +0 -226
- package/dist/shared/types.d.ts.map +0 -1
- package/dist/shared/types.js +0 -7
- package/dist/shared/types.js.map +0 -1
- package/dist/src/agents/soul-compiler.d.ts +0 -11
- package/dist/src/agents/soul-compiler.d.ts.map +0 -1
- package/dist/src/agents/soul-compiler.js +0 -157
- package/dist/src/agents/soul-compiler.js.map +0 -1
- package/dist/src/board/factory.d.ts +0 -3
- package/dist/src/board/factory.d.ts.map +0 -1
- package/dist/src/board/factory.js +0 -10
- package/dist/src/board/factory.js.map +0 -1
- package/dist/src/board/providers/github-graphql.d.ts +0 -16
- package/dist/src/board/providers/github-graphql.d.ts.map +0 -1
- package/dist/src/board/providers/github-graphql.js +0 -43
- package/dist/src/board/providers/github-graphql.js.map +0 -1
- package/dist/src/board/providers/github-projects.d.ts +0 -51
- package/dist/src/board/providers/github-projects.d.ts.map +0 -1
- package/dist/src/board/providers/github-projects.js +0 -672
- package/dist/src/board/providers/github-projects.js.map +0 -1
- package/dist/src/board/types.d.ts +0 -60
- package/dist/src/board/types.d.ts.map +0 -1
- package/dist/src/board/types.js +0 -4
- package/dist/src/board/types.js.map +0 -1
- package/dist/src/cli.d.ts +0 -3
- package/dist/src/cli.d.ts.map +0 -1
- package/dist/src/cli.js.map +0 -1
- package/dist/src/commands/audit.d.ts.map +0 -1
- package/dist/src/commands/audit.js +0 -98
- package/dist/src/commands/audit.js.map +0 -1
- package/dist/src/commands/board.d.ts.map +0 -1
- package/dist/src/commands/board.js +0 -294
- package/dist/src/commands/board.js.map +0 -1
- package/dist/src/commands/cancel.d.ts.map +0 -1
- package/dist/src/commands/cancel.js.map +0 -1
- package/dist/src/commands/dashboard/tab-actions.d.ts.map +0 -1
- package/dist/src/commands/dashboard/tab-actions.js.map +0 -1
- package/dist/src/commands/dashboard/tab-config.d.ts.map +0 -1
- package/dist/src/commands/dashboard/tab-config.js.map +0 -1
- package/dist/src/commands/dashboard/tab-logs.d.ts.map +0 -1
- package/dist/src/commands/dashboard/tab-logs.js.map +0 -1
- package/dist/src/commands/dashboard/tab-schedules.d.ts.map +0 -1
- package/dist/src/commands/dashboard/tab-schedules.js.map +0 -1
- package/dist/src/commands/dashboard/tab-status.d.ts.map +0 -1
- package/dist/src/commands/dashboard/tab-status.js.map +0 -1
- package/dist/src/commands/dashboard/types.d.ts.map +0 -1
- package/dist/src/commands/dashboard/types.js.map +0 -1
- package/dist/src/commands/dashboard.d.ts.map +0 -1
- package/dist/src/commands/dashboard.js.map +0 -1
- package/dist/src/commands/doctor.d.ts.map +0 -1
- package/dist/src/commands/doctor.js.map +0 -1
- package/dist/src/commands/history.d.ts.map +0 -1
- package/dist/src/commands/history.js.map +0 -1
- package/dist/src/commands/init.d.ts.map +0 -1
- package/dist/src/commands/init.js.map +0 -1
- package/dist/src/commands/install.d.ts.map +0 -1
- package/dist/src/commands/install.js.map +0 -1
- package/dist/src/commands/logs.d.ts.map +0 -1
- package/dist/src/commands/logs.js.map +0 -1
- package/dist/src/commands/prd-state.d.ts.map +0 -1
- package/dist/src/commands/prd-state.js.map +0 -1
- package/dist/src/commands/prd.d.ts.map +0 -1
- package/dist/src/commands/prd.js.map +0 -1
- package/dist/src/commands/prds.d.ts.map +0 -1
- package/dist/src/commands/prds.js.map +0 -1
- package/dist/src/commands/prs.d.ts.map +0 -1
- package/dist/src/commands/prs.js.map +0 -1
- package/dist/src/commands/qa.d.ts.map +0 -1
- package/dist/src/commands/qa.js.map +0 -1
- package/dist/src/commands/retry.d.ts.map +0 -1
- package/dist/src/commands/retry.js.map +0 -1
- package/dist/src/commands/review.d.ts.map +0 -1
- package/dist/src/commands/review.js.map +0 -1
- package/dist/src/commands/run.d.ts.map +0 -1
- package/dist/src/commands/run.js.map +0 -1
- package/dist/src/commands/serve.d.ts.map +0 -1
- package/dist/src/commands/serve.js.map +0 -1
- package/dist/src/commands/slice.d.ts.map +0 -1
- package/dist/src/commands/slice.js.map +0 -1
- package/dist/src/commands/state.d.ts.map +0 -1
- package/dist/src/commands/state.js.map +0 -1
- package/dist/src/commands/status.d.ts.map +0 -1
- package/dist/src/commands/status.js.map +0 -1
- package/dist/src/commands/uninstall.d.ts.map +0 -1
- package/dist/src/commands/uninstall.js.map +0 -1
- package/dist/src/commands/update.d.ts.map +0 -1
- package/dist/src/commands/update.js.map +0 -1
- package/dist/src/config.d.ts +0 -23
- package/dist/src/config.d.ts.map +0 -1
- package/dist/src/config.js +0 -671
- package/dist/src/config.js.map +0 -1
- package/dist/src/constants.d.ts +0 -67
- package/dist/src/constants.d.ts.map +0 -1
- package/dist/src/constants.js +0 -131
- package/dist/src/constants.js.map +0 -1
- package/dist/src/server/index.d.ts +0 -23
- package/dist/src/server/index.d.ts.map +0 -1
- package/dist/src/server/index.js +0 -1704
- package/dist/src/server/index.js.map +0 -1
- package/dist/src/slack/channel-manager.d.ts +0 -32
- package/dist/src/slack/channel-manager.d.ts.map +0 -1
- package/dist/src/slack/channel-manager.js +0 -128
- package/dist/src/slack/channel-manager.js.map +0 -1
- package/dist/src/slack/client.d.ts +0 -76
- package/dist/src/slack/client.d.ts.map +0 -1
- package/dist/src/slack/client.js +0 -193
- package/dist/src/slack/client.js.map +0 -1
- package/dist/src/slack/deliberation.d.ts +0 -87
- package/dist/src/slack/deliberation.d.ts.map +0 -1
- package/dist/src/slack/deliberation.js +0 -1354
- package/dist/src/slack/deliberation.js.map +0 -1
- package/dist/src/slack/index.d.ts +0 -6
- package/dist/src/slack/index.d.ts.map +0 -1
- package/dist/src/slack/index.js +0 -5
- package/dist/src/slack/index.js.map +0 -1
- package/dist/src/slack/interaction-listener.d.ts +0 -130
- package/dist/src/slack/interaction-listener.d.ts.map +0 -1
- package/dist/src/slack/interaction-listener.js +0 -1329
- package/dist/src/slack/interaction-listener.js.map +0 -1
- package/dist/src/storage/json-state-migrator.d.ts +0 -24
- package/dist/src/storage/json-state-migrator.d.ts.map +0 -1
- package/dist/src/storage/json-state-migrator.js +0 -197
- package/dist/src/storage/json-state-migrator.js.map +0 -1
- package/dist/src/storage/repositories/index.d.ts +0 -25
- package/dist/src/storage/repositories/index.d.ts.map +0 -1
- package/dist/src/storage/repositories/index.js +0 -45
- package/dist/src/storage/repositories/index.js.map +0 -1
- package/dist/src/storage/repositories/interfaces.d.ts +0 -60
- package/dist/src/storage/repositories/interfaces.d.ts.map +0 -1
- package/dist/src/storage/repositories/interfaces.js +0 -6
- package/dist/src/storage/repositories/interfaces.js.map +0 -1
- package/dist/src/storage/repositories/sqlite/agent-persona-repository.d.ts +0 -33
- package/dist/src/storage/repositories/sqlite/agent-persona-repository.d.ts.map +0 -1
- package/dist/src/storage/repositories/sqlite/agent-persona-repository.js +0 -715
- package/dist/src/storage/repositories/sqlite/agent-persona-repository.js.map +0 -1
- package/dist/src/storage/repositories/sqlite/execution-history-repository.d.ts +0 -21
- package/dist/src/storage/repositories/sqlite/execution-history-repository.d.ts.map +0 -1
- package/dist/src/storage/repositories/sqlite/execution-history-repository.js +0 -94
- package/dist/src/storage/repositories/sqlite/execution-history-repository.js.map +0 -1
- package/dist/src/storage/repositories/sqlite/prd-state-repository.d.ts +0 -17
- package/dist/src/storage/repositories/sqlite/prd-state-repository.d.ts.map +0 -1
- package/dist/src/storage/repositories/sqlite/prd-state-repository.js +0 -74
- package/dist/src/storage/repositories/sqlite/prd-state-repository.js.map +0 -1
- package/dist/src/storage/repositories/sqlite/project-registry-repository.d.ts +0 -17
- package/dist/src/storage/repositories/sqlite/project-registry-repository.d.ts.map +0 -1
- package/dist/src/storage/repositories/sqlite/project-registry-repository.js +0 -43
- package/dist/src/storage/repositories/sqlite/project-registry-repository.js.map +0 -1
- package/dist/src/storage/repositories/sqlite/roadmap-state-repository.d.ts +0 -14
- package/dist/src/storage/repositories/sqlite/roadmap-state-repository.d.ts.map +0 -1
- package/dist/src/storage/repositories/sqlite/roadmap-state-repository.js +0 -47
- package/dist/src/storage/repositories/sqlite/roadmap-state-repository.js.map +0 -1
- package/dist/src/storage/repositories/sqlite/slack-discussion-repository.d.ts +0 -20
- package/dist/src/storage/repositories/sqlite/slack-discussion-repository.d.ts.map +0 -1
- package/dist/src/storage/repositories/sqlite/slack-discussion-repository.js +0 -88
- package/dist/src/storage/repositories/sqlite/slack-discussion-repository.js.map +0 -1
- package/dist/src/storage/sqlite/client.d.ts +0 -23
- package/dist/src/storage/sqlite/client.d.ts.map +0 -1
- package/dist/src/storage/sqlite/client.js +0 -47
- package/dist/src/storage/sqlite/client.js.map +0 -1
- package/dist/src/storage/sqlite/migrations.d.ts +0 -11
- package/dist/src/storage/sqlite/migrations.d.ts.map +0 -1
- package/dist/src/storage/sqlite/migrations.js +0 -94
- package/dist/src/storage/sqlite/migrations.js.map +0 -1
- package/dist/src/templates/prd-template.d.ts +0 -11
- package/dist/src/templates/prd-template.d.ts.map +0 -1
- package/dist/src/templates/prd-template.js +0 -166
- package/dist/src/templates/prd-template.js.map +0 -1
- package/dist/src/templates/slicer-prompt.d.ts +0 -54
- package/dist/src/templates/slicer-prompt.d.ts.map +0 -1
- package/dist/src/templates/slicer-prompt.js +0 -163
- package/dist/src/templates/slicer-prompt.js.map +0 -1
- package/dist/src/types.d.ts +0 -140
- package/dist/src/types.d.ts.map +0 -1
- package/dist/src/types.js +0 -5
- package/dist/src/types.js.map +0 -1
- package/dist/src/utils/avatar-generator.d.ts +0 -6
- package/dist/src/utils/avatar-generator.d.ts.map +0 -1
- package/dist/src/utils/avatar-generator.js +0 -133
- package/dist/src/utils/avatar-generator.js.map +0 -1
- package/dist/src/utils/checks.d.ts +0 -55
- package/dist/src/utils/checks.d.ts.map +0 -1
- package/dist/src/utils/checks.js +0 -246
- package/dist/src/utils/checks.js.map +0 -1
- package/dist/src/utils/config-writer.d.ts +0 -16
- package/dist/src/utils/config-writer.d.ts.map +0 -1
- package/dist/src/utils/config-writer.js +0 -45
- package/dist/src/utils/config-writer.js.map +0 -1
- package/dist/src/utils/crontab.d.ts +0 -62
- package/dist/src/utils/crontab.d.ts.map +0 -1
- package/dist/src/utils/crontab.js +0 -168
- package/dist/src/utils/crontab.js.map +0 -1
- package/dist/src/utils/execution-history.d.ts +0 -54
- package/dist/src/utils/execution-history.d.ts.map +0 -1
- package/dist/src/utils/execution-history.js +0 -80
- package/dist/src/utils/execution-history.js.map +0 -1
- package/dist/src/utils/github.d.ts +0 -40
- package/dist/src/utils/github.d.ts.map +0 -1
- package/dist/src/utils/github.js +0 -126
- package/dist/src/utils/github.js.map +0 -1
- package/dist/src/utils/notify.d.ts +0 -64
- package/dist/src/utils/notify.d.ts.map +0 -1
- package/dist/src/utils/notify.js +0 -405
- package/dist/src/utils/notify.js.map +0 -1
- package/dist/src/utils/prd-states.d.ts +0 -16
- package/dist/src/utils/prd-states.d.ts.map +0 -1
- package/dist/src/utils/prd-states.js +0 -28
- package/dist/src/utils/prd-states.js.map +0 -1
- package/dist/src/utils/registry.d.ts +0 -45
- package/dist/src/utils/registry.d.ts.map +0 -1
- package/dist/src/utils/registry.js +0 -86
- package/dist/src/utils/registry.js.map +0 -1
- package/dist/src/utils/roadmap-parser.d.ts +0 -45
- package/dist/src/utils/roadmap-parser.d.ts.map +0 -1
- package/dist/src/utils/roadmap-parser.js +0 -136
- package/dist/src/utils/roadmap-parser.js.map +0 -1
- package/dist/src/utils/roadmap-scanner.d.ts +0 -92
- package/dist/src/utils/roadmap-scanner.d.ts.map +0 -1
- package/dist/src/utils/roadmap-scanner.js +0 -349
- package/dist/src/utils/roadmap-scanner.js.map +0 -1
- package/dist/src/utils/roadmap-state.d.ts +0 -90
- package/dist/src/utils/roadmap-state.d.ts.map +0 -1
- package/dist/src/utils/roadmap-state.js +0 -154
- package/dist/src/utils/roadmap-state.js.map +0 -1
- package/dist/src/utils/script-result.d.ts +0 -12
- package/dist/src/utils/script-result.d.ts.map +0 -1
- package/dist/src/utils/script-result.js +0 -46
- package/dist/src/utils/script-result.js.map +0 -1
- package/dist/src/utils/shell.d.ts +0 -27
- package/dist/src/utils/shell.d.ts.map +0 -1
- package/dist/src/utils/shell.js +0 -64
- package/dist/src/utils/shell.js.map +0 -1
- package/dist/src/utils/status-data.d.ts +0 -148
- package/dist/src/utils/status-data.d.ts.map +0 -1
- package/dist/src/utils/status-data.js +0 -548
- package/dist/src/utils/status-data.js.map +0 -1
- package/dist/src/utils/ui.d.ts +0 -55
- package/dist/src/utils/ui.d.ts.map +0 -1
- package/dist/src/utils/ui.js +0 -121
- package/dist/src/utils/ui.js.map +0 -1
- package/scripts/night-watch-audit-cron.sh +0 -149
- package/scripts/night-watch-cron.sh +0 -484
- package/scripts/night-watch-helpers.sh +0 -499
- package/scripts/night-watch-pr-reviewer-cron.sh +0 -528
- package/scripts/night-watch-qa-cron.sh +0 -281
- package/scripts/night-watch-slicer-cron.sh +0 -90
- package/scripts/test-helpers.bats +0 -77
- package/templates/night-watch-pr-reviewer.md +0 -174
- package/templates/night-watch-qa.md +0 -157
- package/templates/night-watch-slicer.md +0 -219
- package/templates/night-watch.config.json +0 -30
- package/templates/night-watch.md +0 -94
- package/templates/prd-executor.md +0 -235
- package/templates/prd.md +0 -26
- package/web/dist/assets/index-BiJf9LFT.js +0 -458
- package/web/dist/assets/index-OpSgvsYu.css +0 -1
- package/web/dist/avatars/carlos.webp +0 -0
- package/web/dist/avatars/dev.webp +0 -0
- package/web/dist/avatars/maya.webp +0 -0
- package/web/dist/avatars/priya.webp +0 -0
- package/web/dist/index.html +0 -82
- /package/dist/{src/commands → commands}/dashboard/tab-actions.d.ts +0 -0
- /package/dist/{src/commands → commands}/dashboard/tab-actions.js +0 -0
- /package/dist/{src/commands → commands}/dashboard/types.js +0 -0
package/dist/src/server/index.js
DELETED
|
@@ -1,1704 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HTTP API Server for Night Watch CLI
|
|
3
|
-
* Provides REST API endpoints for the Web UI
|
|
4
|
-
* Supports both single-project and global (multi-project) modes
|
|
5
|
-
*/
|
|
6
|
-
import { execSync, spawn } from 'child_process';
|
|
7
|
-
import cors from 'cors';
|
|
8
|
-
import express, { Router, } from 'express';
|
|
9
|
-
import * as fs from 'fs';
|
|
10
|
-
import * as path from 'path';
|
|
11
|
-
import { dirname } from 'path';
|
|
12
|
-
import { fileURLToPath } from 'url';
|
|
13
|
-
import { CronExpressionParser } from 'cron-parser';
|
|
14
|
-
import { createBoardProvider } from '../board/factory.js';
|
|
15
|
-
import { BOARD_COLUMNS } from '../board/types.js';
|
|
16
|
-
import { performCancel } from '../commands/cancel.js';
|
|
17
|
-
import { validateWebhook } from '../commands/doctor.js';
|
|
18
|
-
import { loadConfig } from '../config.js';
|
|
19
|
-
import { CLAIM_FILE_EXTENSION, CONFIG_FILE_NAME, LOG_DIR, LOG_FILE_NAMES, } from '../constants.js';
|
|
20
|
-
import { getRepositories } from '../storage/repositories/index.js';
|
|
21
|
-
import { saveConfig } from '../utils/config-writer.js';
|
|
22
|
-
import { generateMarker, getEntries, getProjectEntries, } from '../utils/crontab.js';
|
|
23
|
-
import { sendNotifications } from '../utils/notify.js';
|
|
24
|
-
import { loadRegistry, validateRegistry } from '../utils/registry.js';
|
|
25
|
-
import { getRoadmapStatus, scanRoadmap } from '../utils/roadmap-scanner.js';
|
|
26
|
-
import { loadRoadmapState } from '../utils/roadmap-state.js';
|
|
27
|
-
import { checkLockFile, collectPrInfo, collectPrdInfo, executorLockPath, fetchStatusSnapshot, getLastLogLines, reviewerLockPath, } from '../utils/status-data.js';
|
|
28
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
29
|
-
const __dirname = dirname(__filename);
|
|
30
|
-
// Find the package root (works from both src/ in dev and dist/src/ in production)
|
|
31
|
-
function findPackageRoot(dir) {
|
|
32
|
-
let d = dir;
|
|
33
|
-
for (let i = 0; i < 5; i++) {
|
|
34
|
-
if (fs.existsSync(path.join(d, 'package.json')))
|
|
35
|
-
return d;
|
|
36
|
-
d = dirname(d);
|
|
37
|
-
}
|
|
38
|
-
return dir;
|
|
39
|
-
}
|
|
40
|
-
const __packageRoot = findPackageRoot(__dirname);
|
|
41
|
-
// Track spawned processes
|
|
42
|
-
const spawnedProcesses = new Map();
|
|
43
|
-
/**
|
|
44
|
-
* Broadcast an SSE event to all connected clients
|
|
45
|
-
*/
|
|
46
|
-
function broadcastSSE(clients, event, data) {
|
|
47
|
-
const msg = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
48
|
-
for (const client of clients) {
|
|
49
|
-
try {
|
|
50
|
-
client.write(msg);
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
clients.delete(client);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Start the SSE status change watcher that broadcasts when snapshot changes
|
|
59
|
-
*/
|
|
60
|
-
function startSseStatusWatcher(clients, projectDir, getConfig) {
|
|
61
|
-
let lastSnapshotHash = '';
|
|
62
|
-
return setInterval(() => {
|
|
63
|
-
if (clients.size === 0)
|
|
64
|
-
return;
|
|
65
|
-
try {
|
|
66
|
-
const snapshot = fetchStatusSnapshot(projectDir, getConfig());
|
|
67
|
-
const hash = JSON.stringify({
|
|
68
|
-
processes: snapshot.processes,
|
|
69
|
-
prds: snapshot.prds.map((p) => ({ n: p.name, s: p.status })),
|
|
70
|
-
});
|
|
71
|
-
if (hash !== lastSnapshotHash) {
|
|
72
|
-
lastSnapshotHash = hash;
|
|
73
|
-
broadcastSSE(clients, 'status_changed', snapshot);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
// Silently ignore errors during status polling
|
|
78
|
-
}
|
|
79
|
-
}, 2000);
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Error handler middleware
|
|
83
|
-
*/
|
|
84
|
-
function errorHandler(err, _req, res, _next) {
|
|
85
|
-
console.error('API Error:', err);
|
|
86
|
-
res.status(500).json({ error: err.message });
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Validate PRD name to prevent path traversal
|
|
90
|
-
*/
|
|
91
|
-
function validatePrdName(name) {
|
|
92
|
-
return /^[a-zA-Z0-9_-]+(\.md)?$/.test(name) && !name.includes('..');
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Mask persona model env var values before returning API payloads.
|
|
96
|
-
*/
|
|
97
|
-
function maskPersonaSecrets(persona) {
|
|
98
|
-
const modelConfig = persona.modelConfig;
|
|
99
|
-
const envVars = modelConfig?.envVars;
|
|
100
|
-
if (!modelConfig || !envVars)
|
|
101
|
-
return persona;
|
|
102
|
-
return {
|
|
103
|
-
...persona,
|
|
104
|
-
modelConfig: {
|
|
105
|
-
...modelConfig,
|
|
106
|
-
envVars: Object.fromEntries(Object.keys(envVars).map((key) => [key, '***'])),
|
|
107
|
-
},
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
// ==================== Extracted Route Handlers ====================
|
|
111
|
-
function handleGetStatus(projectDir, config, _req, res) {
|
|
112
|
-
try {
|
|
113
|
-
const snapshot = fetchStatusSnapshot(projectDir, config);
|
|
114
|
-
res.json(snapshot);
|
|
115
|
-
}
|
|
116
|
-
catch (error) {
|
|
117
|
-
res
|
|
118
|
-
.status(500)
|
|
119
|
-
.json({ error: error instanceof Error ? error.message : String(error) });
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
function handleGetPrds(projectDir, config, _req, res) {
|
|
123
|
-
try {
|
|
124
|
-
const prds = collectPrdInfo(projectDir, config.prdDir, config.maxRuntime);
|
|
125
|
-
const prdsWithContent = prds.map((prd) => {
|
|
126
|
-
const prdPath = path.join(projectDir, config.prdDir, `${prd.name}.md`);
|
|
127
|
-
let content = '';
|
|
128
|
-
if (fs.existsSync(prdPath)) {
|
|
129
|
-
try {
|
|
130
|
-
content = fs.readFileSync(prdPath, 'utf-8');
|
|
131
|
-
}
|
|
132
|
-
catch {
|
|
133
|
-
content = '';
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return { ...prd, content };
|
|
137
|
-
});
|
|
138
|
-
res.json(prdsWithContent);
|
|
139
|
-
}
|
|
140
|
-
catch (error) {
|
|
141
|
-
res
|
|
142
|
-
.status(500)
|
|
143
|
-
.json({ error: error instanceof Error ? error.message : String(error) });
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
function handleGetPrdByName(projectDir, config, req, res) {
|
|
147
|
-
try {
|
|
148
|
-
const { name } = req.params;
|
|
149
|
-
if (!validatePrdName(name)) {
|
|
150
|
-
res.status(400).json({ error: 'Invalid PRD name' });
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
const nameStr = name;
|
|
154
|
-
const filename = nameStr.endsWith('.md') ? nameStr : `${nameStr}.md`;
|
|
155
|
-
const prdPath = path.join(projectDir, config.prdDir, filename);
|
|
156
|
-
if (!fs.existsSync(prdPath)) {
|
|
157
|
-
res.status(404).json({ error: 'PRD not found' });
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
const content = fs.readFileSync(prdPath, 'utf-8');
|
|
161
|
-
res.json({ name: filename.replace(/\.md$/, ''), content });
|
|
162
|
-
}
|
|
163
|
-
catch (error) {
|
|
164
|
-
res
|
|
165
|
-
.status(500)
|
|
166
|
-
.json({ error: error instanceof Error ? error.message : String(error) });
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
function handleGetPrs(projectDir, config, _req, res) {
|
|
170
|
-
try {
|
|
171
|
-
const prs = collectPrInfo(projectDir, config.branchPatterns);
|
|
172
|
-
res.json(prs);
|
|
173
|
-
}
|
|
174
|
-
catch (error) {
|
|
175
|
-
res
|
|
176
|
-
.status(500)
|
|
177
|
-
.json({ error: error instanceof Error ? error.message : String(error) });
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
function handleGetLogs(projectDir, _config, req, res) {
|
|
181
|
-
try {
|
|
182
|
-
const { name } = req.params;
|
|
183
|
-
const validNames = ['executor', 'reviewer', 'qa'];
|
|
184
|
-
if (!validNames.includes(name)) {
|
|
185
|
-
res.status(400).json({
|
|
186
|
-
error: `Invalid log name. Must be one of: ${validNames.join(', ')}`,
|
|
187
|
-
});
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
const linesParam = req.query.lines;
|
|
191
|
-
const lines = typeof linesParam === 'string' ? parseInt(linesParam, 10) : 200;
|
|
192
|
-
const linesToRead = isNaN(lines) || lines < 1 ? 200 : Math.min(lines, 10000);
|
|
193
|
-
// Map logical name (executor/reviewer) to actual file name (night-watch/night-watch-pr-reviewer)
|
|
194
|
-
const fileName = LOG_FILE_NAMES[name] || name;
|
|
195
|
-
const logPath = path.join(projectDir, LOG_DIR, `${fileName}.log`);
|
|
196
|
-
const logLines = getLastLogLines(logPath, linesToRead);
|
|
197
|
-
res.json({ name, lines: logLines });
|
|
198
|
-
}
|
|
199
|
-
catch (error) {
|
|
200
|
-
res
|
|
201
|
-
.status(500)
|
|
202
|
-
.json({ error: error instanceof Error ? error.message : String(error) });
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
function handleGetConfig(config, _req, res) {
|
|
206
|
-
try {
|
|
207
|
-
res.json(config);
|
|
208
|
-
}
|
|
209
|
-
catch (error) {
|
|
210
|
-
res
|
|
211
|
-
.status(500)
|
|
212
|
-
.json({ error: error instanceof Error ? error.message : String(error) });
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
function handlePutConfig(projectDir, getConfig, reloadConfig, req, res) {
|
|
216
|
-
try {
|
|
217
|
-
let changes = req.body;
|
|
218
|
-
if (typeof changes !== 'object' || changes === null) {
|
|
219
|
-
res.status(400).json({ error: 'Invalid request body' });
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
if (changes.provider !== undefined) {
|
|
223
|
-
const validProviders = ['claude', 'codex'];
|
|
224
|
-
if (!validProviders.includes(changes.provider)) {
|
|
225
|
-
res.status(400).json({
|
|
226
|
-
error: `Invalid provider. Must be one of: ${validProviders.join(', ')}`,
|
|
227
|
-
});
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
if (changes.reviewerEnabled !== undefined) {
|
|
232
|
-
if (typeof changes.reviewerEnabled !== 'boolean') {
|
|
233
|
-
res.status(400).json({ error: 'reviewerEnabled must be a boolean' });
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
if (changes.maxRuntime !== undefined) {
|
|
238
|
-
if (typeof changes.maxRuntime !== 'number' || changes.maxRuntime < 60) {
|
|
239
|
-
res.status(400).json({ error: 'maxRuntime must be a number >= 60' });
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
if (changes.reviewerMaxRuntime !== undefined) {
|
|
244
|
-
if (typeof changes.reviewerMaxRuntime !== 'number' ||
|
|
245
|
-
changes.reviewerMaxRuntime < 60) {
|
|
246
|
-
res
|
|
247
|
-
.status(400)
|
|
248
|
-
.json({ error: 'reviewerMaxRuntime must be a number >= 60' });
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
if (changes.minReviewScore !== undefined) {
|
|
253
|
-
if (typeof changes.minReviewScore !== 'number' ||
|
|
254
|
-
changes.minReviewScore < 0 ||
|
|
255
|
-
changes.minReviewScore > 100) {
|
|
256
|
-
res
|
|
257
|
-
.status(400)
|
|
258
|
-
.json({ error: 'minReviewScore must be a number between 0 and 100' });
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
if (changes.maxLogSize !== undefined) {
|
|
263
|
-
if (typeof changes.maxLogSize !== 'number' || changes.maxLogSize < 0) {
|
|
264
|
-
res.status(400).json({ error: 'maxLogSize must be a positive number' });
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
if (changes.branchPatterns !== undefined) {
|
|
269
|
-
if (!Array.isArray(changes.branchPatterns) ||
|
|
270
|
-
!changes.branchPatterns.every((p) => typeof p === 'string')) {
|
|
271
|
-
res
|
|
272
|
-
.status(400)
|
|
273
|
-
.json({ error: 'branchPatterns must be an array of strings' });
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
if (changes.prdPriority !== undefined) {
|
|
278
|
-
if (!Array.isArray(changes.prdPriority) ||
|
|
279
|
-
!changes.prdPriority.every((p) => typeof p === 'string')) {
|
|
280
|
-
res
|
|
281
|
-
.status(400)
|
|
282
|
-
.json({ error: 'prdPriority must be an array of strings' });
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
if (changes.cronSchedule !== undefined) {
|
|
287
|
-
if (typeof changes.cronSchedule !== 'string' ||
|
|
288
|
-
changes.cronSchedule.trim().length === 0) {
|
|
289
|
-
res
|
|
290
|
-
.status(400)
|
|
291
|
-
.json({ error: 'cronSchedule must be a non-empty string' });
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
if (changes.reviewerSchedule !== undefined) {
|
|
296
|
-
if (typeof changes.reviewerSchedule !== 'string' ||
|
|
297
|
-
changes.reviewerSchedule.trim().length === 0) {
|
|
298
|
-
res
|
|
299
|
-
.status(400)
|
|
300
|
-
.json({ error: 'reviewerSchedule must be a non-empty string' });
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
if (changes.notifications?.webhooks !== undefined) {
|
|
305
|
-
if (!Array.isArray(changes.notifications.webhooks)) {
|
|
306
|
-
res
|
|
307
|
-
.status(400)
|
|
308
|
-
.json({ error: 'notifications.webhooks must be an array' });
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
for (const webhook of changes.notifications.webhooks) {
|
|
312
|
-
const issues = validateWebhook(webhook);
|
|
313
|
-
if (issues.length > 0) {
|
|
314
|
-
res
|
|
315
|
-
.status(400)
|
|
316
|
-
.json({ error: `Invalid webhook: ${issues.join(', ')}` });
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
if (changes.roadmapScanner !== undefined) {
|
|
322
|
-
const rs = changes.roadmapScanner;
|
|
323
|
-
if (typeof rs !== 'object' || rs === null) {
|
|
324
|
-
res.status(400).json({ error: 'roadmapScanner must be an object' });
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
if (rs.enabled !== undefined && typeof rs.enabled !== 'boolean') {
|
|
328
|
-
res
|
|
329
|
-
.status(400)
|
|
330
|
-
.json({ error: 'roadmapScanner.enabled must be a boolean' });
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
if (rs.roadmapPath !== undefined) {
|
|
334
|
-
if (typeof rs.roadmapPath !== 'string' ||
|
|
335
|
-
rs.roadmapPath.trim().length === 0) {
|
|
336
|
-
res.status(400).json({
|
|
337
|
-
error: 'roadmapScanner.roadmapPath must be a non-empty string',
|
|
338
|
-
});
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
if (rs.autoScanInterval !== undefined) {
|
|
343
|
-
if (typeof rs.autoScanInterval !== 'number' ||
|
|
344
|
-
rs.autoScanInterval < 30) {
|
|
345
|
-
res.status(400).json({
|
|
346
|
-
error: 'roadmapScanner.autoScanInterval must be a number >= 30',
|
|
347
|
-
});
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
const result = saveConfig(projectDir, changes);
|
|
353
|
-
if (!result.success) {
|
|
354
|
-
res.status(500).json({ error: result.error });
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
reloadConfig();
|
|
358
|
-
res.json(getConfig());
|
|
359
|
-
}
|
|
360
|
-
catch (error) {
|
|
361
|
-
res
|
|
362
|
-
.status(500)
|
|
363
|
-
.json({ error: error instanceof Error ? error.message : String(error) });
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
function handleGetDoctor(projectDir, config, _req, res) {
|
|
367
|
-
try {
|
|
368
|
-
const checks = [];
|
|
369
|
-
try {
|
|
370
|
-
execSync('git rev-parse --is-inside-work-tree', {
|
|
371
|
-
cwd: projectDir,
|
|
372
|
-
stdio: 'pipe',
|
|
373
|
-
});
|
|
374
|
-
checks.push({
|
|
375
|
-
name: 'git',
|
|
376
|
-
status: 'pass',
|
|
377
|
-
detail: 'Git repository detected',
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
catch {
|
|
381
|
-
checks.push({
|
|
382
|
-
name: 'git',
|
|
383
|
-
status: 'fail',
|
|
384
|
-
detail: 'Not a git repository',
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
try {
|
|
388
|
-
execSync(`which ${config.provider}`, { stdio: 'pipe' });
|
|
389
|
-
checks.push({
|
|
390
|
-
name: 'provider',
|
|
391
|
-
status: 'pass',
|
|
392
|
-
detail: `Provider CLI found: ${config.provider}`,
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
catch {
|
|
396
|
-
checks.push({
|
|
397
|
-
name: 'provider',
|
|
398
|
-
status: 'fail',
|
|
399
|
-
detail: `Provider CLI not found: ${config.provider}`,
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
try {
|
|
403
|
-
const projectName = path.basename(projectDir);
|
|
404
|
-
const marker = generateMarker(projectName);
|
|
405
|
-
const crontabEntries = [
|
|
406
|
-
...getEntries(marker),
|
|
407
|
-
...getProjectEntries(projectDir),
|
|
408
|
-
];
|
|
409
|
-
if (crontabEntries.length > 0) {
|
|
410
|
-
checks.push({
|
|
411
|
-
name: 'crontab',
|
|
412
|
-
status: 'pass',
|
|
413
|
-
detail: `${crontabEntries.length} crontab entr(y/ies) installed`,
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
else {
|
|
417
|
-
checks.push({
|
|
418
|
-
name: 'crontab',
|
|
419
|
-
status: 'warn',
|
|
420
|
-
detail: 'No crontab entries installed',
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
catch (_error) {
|
|
425
|
-
checks.push({
|
|
426
|
-
name: 'crontab',
|
|
427
|
-
status: 'fail',
|
|
428
|
-
detail: 'Failed to check crontab',
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
const configPath = path.join(projectDir, CONFIG_FILE_NAME);
|
|
432
|
-
if (fs.existsSync(configPath)) {
|
|
433
|
-
checks.push({
|
|
434
|
-
name: 'config',
|
|
435
|
-
status: 'pass',
|
|
436
|
-
detail: 'Config file exists',
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
else {
|
|
440
|
-
checks.push({
|
|
441
|
-
name: 'config',
|
|
442
|
-
status: 'warn',
|
|
443
|
-
detail: 'Config file not found (using defaults)',
|
|
444
|
-
});
|
|
445
|
-
}
|
|
446
|
-
const prdDir = path.join(projectDir, config.prdDir);
|
|
447
|
-
if (fs.existsSync(prdDir)) {
|
|
448
|
-
const prds = fs
|
|
449
|
-
.readdirSync(prdDir)
|
|
450
|
-
.filter((f) => f.endsWith('.md') && f !== 'NIGHT-WATCH-SUMMARY.md');
|
|
451
|
-
checks.push({
|
|
452
|
-
name: 'prdDir',
|
|
453
|
-
status: 'pass',
|
|
454
|
-
detail: `PRD directory exists (${prds.length} PRDs)`,
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
else {
|
|
458
|
-
checks.push({
|
|
459
|
-
name: 'prdDir',
|
|
460
|
-
status: 'warn',
|
|
461
|
-
detail: `PRD directory not found: ${config.prdDir}`,
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
res.json(checks);
|
|
465
|
-
}
|
|
466
|
-
catch (error) {
|
|
467
|
-
res
|
|
468
|
-
.status(500)
|
|
469
|
-
.json({ error: error instanceof Error ? error.message : String(error) });
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
function handleSpawnAction(projectDir, command, req, res, onSpawned) {
|
|
473
|
-
try {
|
|
474
|
-
// Prevent duplicate execution: check the lock file before spawning
|
|
475
|
-
const lockPath = command[0] === 'run'
|
|
476
|
-
? executorLockPath(projectDir)
|
|
477
|
-
: command[0] === 'review'
|
|
478
|
-
? reviewerLockPath(projectDir)
|
|
479
|
-
: null;
|
|
480
|
-
if (lockPath) {
|
|
481
|
-
const lock = checkLockFile(lockPath);
|
|
482
|
-
if (lock.running) {
|
|
483
|
-
const processType = command[0] === 'run' ? 'Executor' : 'Reviewer';
|
|
484
|
-
res.status(409).json({
|
|
485
|
-
error: `${processType} is already running (PID ${lock.pid})`,
|
|
486
|
-
pid: lock.pid,
|
|
487
|
-
});
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
// Extract optional prdName for priority execution (only for "run" command)
|
|
492
|
-
const prdName = command[0] === 'run'
|
|
493
|
-
? req.body?.prdName
|
|
494
|
-
: undefined;
|
|
495
|
-
// Build extra env vars for priority hint
|
|
496
|
-
const extraEnv = {};
|
|
497
|
-
if (prdName) {
|
|
498
|
-
extraEnv.NW_PRD_PRIORITY = prdName; // bash script respects NW_PRD_PRIORITY
|
|
499
|
-
}
|
|
500
|
-
const child = spawn('night-watch', command, {
|
|
501
|
-
detached: true,
|
|
502
|
-
stdio: 'ignore',
|
|
503
|
-
cwd: projectDir,
|
|
504
|
-
env: { ...process.env, ...extraEnv },
|
|
505
|
-
});
|
|
506
|
-
child.unref();
|
|
507
|
-
if (child.pid !== undefined) {
|
|
508
|
-
spawnedProcesses.set(child.pid, child);
|
|
509
|
-
// Fire notification for executor start (non-blocking)
|
|
510
|
-
if (command[0] === 'run') {
|
|
511
|
-
const config = loadConfig(projectDir);
|
|
512
|
-
sendNotifications(config, {
|
|
513
|
-
event: 'run_started',
|
|
514
|
-
projectName: path.basename(projectDir),
|
|
515
|
-
exitCode: 0,
|
|
516
|
-
provider: config.provider,
|
|
517
|
-
}).catch(() => {
|
|
518
|
-
/* silently ignore notification errors */
|
|
519
|
-
});
|
|
520
|
-
}
|
|
521
|
-
// Notify SSE clients about executor start
|
|
522
|
-
if (onSpawned) {
|
|
523
|
-
onSpawned(child.pid);
|
|
524
|
-
}
|
|
525
|
-
res.json({ started: true, pid: child.pid });
|
|
526
|
-
}
|
|
527
|
-
else {
|
|
528
|
-
res
|
|
529
|
-
.status(500)
|
|
530
|
-
.json({ error: 'Failed to spawn process: no PID assigned' });
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
catch (error) {
|
|
534
|
-
res
|
|
535
|
-
.status(500)
|
|
536
|
-
.json({ error: error instanceof Error ? error.message : String(error) });
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
function handleGetScheduleInfo(projectDir, config, _req, res) {
|
|
540
|
-
try {
|
|
541
|
-
const snapshot = fetchStatusSnapshot(projectDir, config);
|
|
542
|
-
const installed = snapshot.crontab.installed;
|
|
543
|
-
const entries = snapshot.crontab.entries;
|
|
544
|
-
const computeNextRun = (cronExpr) => {
|
|
545
|
-
try {
|
|
546
|
-
const interval = CronExpressionParser.parse(cronExpr);
|
|
547
|
-
return interval.next().toISOString();
|
|
548
|
-
}
|
|
549
|
-
catch {
|
|
550
|
-
return null;
|
|
551
|
-
}
|
|
552
|
-
};
|
|
553
|
-
res.json({
|
|
554
|
-
executor: {
|
|
555
|
-
schedule: config.cronSchedule,
|
|
556
|
-
installed,
|
|
557
|
-
nextRun: installed ? computeNextRun(config.cronSchedule) : null,
|
|
558
|
-
},
|
|
559
|
-
reviewer: {
|
|
560
|
-
schedule: config.reviewerSchedule,
|
|
561
|
-
installed: installed && config.reviewerEnabled,
|
|
562
|
-
nextRun: installed && config.reviewerEnabled
|
|
563
|
-
? computeNextRun(config.reviewerSchedule)
|
|
564
|
-
: null,
|
|
565
|
-
},
|
|
566
|
-
qa: {
|
|
567
|
-
schedule: config.qa.schedule,
|
|
568
|
-
installed: installed && config.qa.enabled,
|
|
569
|
-
nextRun: installed && config.qa.enabled
|
|
570
|
-
? computeNextRun(config.qa.schedule)
|
|
571
|
-
: null,
|
|
572
|
-
},
|
|
573
|
-
paused: !installed,
|
|
574
|
-
entries,
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
catch (error) {
|
|
578
|
-
res
|
|
579
|
-
.status(500)
|
|
580
|
-
.json({ error: error instanceof Error ? error.message : String(error) });
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
function handleGetRoadmap(projectDir, config, _req, res) {
|
|
584
|
-
try {
|
|
585
|
-
const status = getRoadmapStatus(projectDir, config);
|
|
586
|
-
const prdDir = path.join(projectDir, config.prdDir);
|
|
587
|
-
const state = loadRoadmapState(prdDir);
|
|
588
|
-
res.json({
|
|
589
|
-
...status,
|
|
590
|
-
lastScan: state.lastScan || null,
|
|
591
|
-
autoScanInterval: config.roadmapScanner.autoScanInterval,
|
|
592
|
-
});
|
|
593
|
-
}
|
|
594
|
-
catch (error) {
|
|
595
|
-
res
|
|
596
|
-
.status(500)
|
|
597
|
-
.json({ error: error instanceof Error ? error.message : String(error) });
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
async function handlePostRoadmapScan(projectDir, config, _req, res) {
|
|
601
|
-
try {
|
|
602
|
-
if (!config.roadmapScanner.enabled) {
|
|
603
|
-
res.status(409).json({ error: 'Roadmap scanner is disabled' });
|
|
604
|
-
return;
|
|
605
|
-
}
|
|
606
|
-
const result = await scanRoadmap(projectDir, config);
|
|
607
|
-
res.json(result);
|
|
608
|
-
}
|
|
609
|
-
catch (error) {
|
|
610
|
-
res
|
|
611
|
-
.status(500)
|
|
612
|
-
.json({ error: error instanceof Error ? error.message : String(error) });
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
function handlePutRoadmapToggle(projectDir, getConfig, reloadConfig, req, res) {
|
|
616
|
-
try {
|
|
617
|
-
const { enabled } = req.body;
|
|
618
|
-
if (typeof enabled !== 'boolean') {
|
|
619
|
-
res.status(400).json({ error: 'enabled must be a boolean' });
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
const currentConfig = getConfig();
|
|
623
|
-
const result = saveConfig(projectDir, {
|
|
624
|
-
roadmapScanner: {
|
|
625
|
-
...currentConfig.roadmapScanner,
|
|
626
|
-
enabled,
|
|
627
|
-
},
|
|
628
|
-
});
|
|
629
|
-
if (!result.success) {
|
|
630
|
-
res.status(500).json({ error: result.error });
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
reloadConfig();
|
|
634
|
-
res.json(getConfig());
|
|
635
|
-
}
|
|
636
|
-
catch (error) {
|
|
637
|
-
res
|
|
638
|
-
.status(500)
|
|
639
|
-
.json({ error: error instanceof Error ? error.message : String(error) });
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
// ==================== Slack Integration ====================
|
|
643
|
-
import { SlackClient } from '../slack/client.js';
|
|
644
|
-
import { SlackInteractionListener } from '../slack/interaction-listener.js';
|
|
645
|
-
async function handlePostSlackChannels(req, res) {
|
|
646
|
-
try {
|
|
647
|
-
const { botToken } = (req.body ?? {});
|
|
648
|
-
if (!botToken || typeof botToken !== 'string') {
|
|
649
|
-
res.status(400).json({ error: 'botToken is required' });
|
|
650
|
-
return;
|
|
651
|
-
}
|
|
652
|
-
const slack = new SlackClient(botToken);
|
|
653
|
-
const channels = await slack.listChannels();
|
|
654
|
-
res.json(channels);
|
|
655
|
-
}
|
|
656
|
-
catch (error) {
|
|
657
|
-
res
|
|
658
|
-
.status(500)
|
|
659
|
-
.json({ error: error instanceof Error ? error.message : String(error) });
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
async function handlePostSlackChannelCreate(req, res) {
|
|
663
|
-
try {
|
|
664
|
-
const { botToken, name } = (req.body ?? {});
|
|
665
|
-
if (!botToken || typeof botToken !== 'string') {
|
|
666
|
-
res.status(400).json({ error: 'botToken is required' });
|
|
667
|
-
return;
|
|
668
|
-
}
|
|
669
|
-
if (!name || typeof name !== 'string') {
|
|
670
|
-
res.status(400).json({ error: 'name is required' });
|
|
671
|
-
return;
|
|
672
|
-
}
|
|
673
|
-
const slack = new SlackClient(botToken);
|
|
674
|
-
const channelId = await slack.createChannel(name);
|
|
675
|
-
let invitedCount = 0;
|
|
676
|
-
let inviteWarning = null;
|
|
677
|
-
let welcomeMessagePosted = false;
|
|
678
|
-
// Auto-invite everyone in the workspace
|
|
679
|
-
try {
|
|
680
|
-
const users = await slack.listUsers();
|
|
681
|
-
const userIds = users.map((u) => u.id);
|
|
682
|
-
if (userIds.length > 0) {
|
|
683
|
-
// inviteUsers can take up to 1000 IDs
|
|
684
|
-
invitedCount = await slack.inviteUsers(channelId, userIds);
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
catch (inviteErr) {
|
|
688
|
-
console.warn('Failed to auto-invite users to new channel:', inviteErr);
|
|
689
|
-
inviteWarning =
|
|
690
|
-
inviteErr instanceof Error ? inviteErr.message : String(inviteErr);
|
|
691
|
-
}
|
|
692
|
-
// Post a first message so the channel pops up in the user's Slack
|
|
693
|
-
try {
|
|
694
|
-
await slack.postMessage(channelId, `👋 *Night Watch AI* has linked this channel for integration. Ready to work!`);
|
|
695
|
-
welcomeMessagePosted = true;
|
|
696
|
-
}
|
|
697
|
-
catch (msgErr) {
|
|
698
|
-
console.warn('Failed to post welcome message to new channel:', msgErr);
|
|
699
|
-
}
|
|
700
|
-
res.json({
|
|
701
|
-
channelId,
|
|
702
|
-
invitedCount,
|
|
703
|
-
inviteWarning,
|
|
704
|
-
welcomeMessagePosted,
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
catch (error) {
|
|
708
|
-
res
|
|
709
|
-
.status(500)
|
|
710
|
-
.json({ error: error instanceof Error ? error.message : String(error) });
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
const BOARD_CACHE_TTL_MS = 60_000; // 60 seconds
|
|
714
|
-
const boardCacheMap = new Map();
|
|
715
|
-
function getCachedBoardData(projectDir) {
|
|
716
|
-
const entry = boardCacheMap.get(projectDir);
|
|
717
|
-
if (!entry)
|
|
718
|
-
return null;
|
|
719
|
-
if (Date.now() - entry.timestamp > BOARD_CACHE_TTL_MS) {
|
|
720
|
-
boardCacheMap.delete(projectDir);
|
|
721
|
-
return null;
|
|
722
|
-
}
|
|
723
|
-
return entry.data;
|
|
724
|
-
}
|
|
725
|
-
function setCachedBoardData(projectDir, data) {
|
|
726
|
-
boardCacheMap.set(projectDir, { data, timestamp: Date.now() });
|
|
727
|
-
}
|
|
728
|
-
function invalidateBoardCache(projectDir) {
|
|
729
|
-
boardCacheMap.delete(projectDir);
|
|
730
|
-
}
|
|
731
|
-
// ==================== Board Handlers ====================
|
|
732
|
-
function getBoardProvider(config, projectDir) {
|
|
733
|
-
if (!config.boardProvider?.enabled || !config.boardProvider?.projectNumber) {
|
|
734
|
-
return null;
|
|
735
|
-
}
|
|
736
|
-
return createBoardProvider(config.boardProvider, projectDir);
|
|
737
|
-
}
|
|
738
|
-
async function handleGetBoardStatus(projectDir, config, _req, res) {
|
|
739
|
-
try {
|
|
740
|
-
const provider = getBoardProvider(config, projectDir);
|
|
741
|
-
if (!provider) {
|
|
742
|
-
res.status(404).json({ error: 'Board not configured' });
|
|
743
|
-
return;
|
|
744
|
-
}
|
|
745
|
-
const cached = getCachedBoardData(projectDir);
|
|
746
|
-
if (cached) {
|
|
747
|
-
res.json(cached);
|
|
748
|
-
return;
|
|
749
|
-
}
|
|
750
|
-
const issues = await provider.getAllIssues();
|
|
751
|
-
const columns = {
|
|
752
|
-
Draft: [],
|
|
753
|
-
Ready: [],
|
|
754
|
-
'In Progress': [],
|
|
755
|
-
Review: [],
|
|
756
|
-
Done: [],
|
|
757
|
-
};
|
|
758
|
-
for (const issue of issues) {
|
|
759
|
-
const col = issue.column ?? 'Draft';
|
|
760
|
-
columns[col].push(issue);
|
|
761
|
-
}
|
|
762
|
-
const result = { enabled: true, columns };
|
|
763
|
-
setCachedBoardData(projectDir, result);
|
|
764
|
-
res.json(result);
|
|
765
|
-
}
|
|
766
|
-
catch (error) {
|
|
767
|
-
res
|
|
768
|
-
.status(500)
|
|
769
|
-
.json({ error: error instanceof Error ? error.message : String(error) });
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
async function handleGetBoardIssues(projectDir, config, _req, res) {
|
|
773
|
-
try {
|
|
774
|
-
const provider = getBoardProvider(config, projectDir);
|
|
775
|
-
if (!provider) {
|
|
776
|
-
res.status(404).json({ error: 'Board not configured' });
|
|
777
|
-
return;
|
|
778
|
-
}
|
|
779
|
-
const issues = await provider.getAllIssues();
|
|
780
|
-
res.json(issues);
|
|
781
|
-
}
|
|
782
|
-
catch (error) {
|
|
783
|
-
res
|
|
784
|
-
.status(500)
|
|
785
|
-
.json({ error: error instanceof Error ? error.message : String(error) });
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
async function handlePostBoardIssue(projectDir, config, req, res) {
|
|
789
|
-
try {
|
|
790
|
-
const provider = getBoardProvider(config, projectDir);
|
|
791
|
-
if (!provider) {
|
|
792
|
-
res.status(404).json({ error: 'Board not configured' });
|
|
793
|
-
return;
|
|
794
|
-
}
|
|
795
|
-
const { title, body, column } = req.body;
|
|
796
|
-
if (!title || typeof title !== 'string' || title.trim().length === 0) {
|
|
797
|
-
res.status(400).json({ error: 'title is required' });
|
|
798
|
-
return;
|
|
799
|
-
}
|
|
800
|
-
if (column && !BOARD_COLUMNS.includes(column)) {
|
|
801
|
-
res.status(400).json({
|
|
802
|
-
error: `Invalid column. Must be one of: ${BOARD_COLUMNS.join(', ')}`,
|
|
803
|
-
});
|
|
804
|
-
return;
|
|
805
|
-
}
|
|
806
|
-
const issue = await provider.createIssue({
|
|
807
|
-
title: title.trim(),
|
|
808
|
-
body: body ?? '',
|
|
809
|
-
column,
|
|
810
|
-
});
|
|
811
|
-
invalidateBoardCache(projectDir);
|
|
812
|
-
res.status(201).json(issue);
|
|
813
|
-
}
|
|
814
|
-
catch (error) {
|
|
815
|
-
res
|
|
816
|
-
.status(500)
|
|
817
|
-
.json({ error: error instanceof Error ? error.message : String(error) });
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
async function handlePatchBoardIssueMove(projectDir, config, req, res) {
|
|
821
|
-
try {
|
|
822
|
-
const provider = getBoardProvider(config, projectDir);
|
|
823
|
-
if (!provider) {
|
|
824
|
-
res.status(404).json({ error: 'Board not configured' });
|
|
825
|
-
return;
|
|
826
|
-
}
|
|
827
|
-
const issueNumber = parseInt(req.params.number, 10);
|
|
828
|
-
if (isNaN(issueNumber)) {
|
|
829
|
-
res.status(400).json({ error: 'Invalid issue number' });
|
|
830
|
-
return;
|
|
831
|
-
}
|
|
832
|
-
const { column } = req.body;
|
|
833
|
-
if (!column || !BOARD_COLUMNS.includes(column)) {
|
|
834
|
-
res.status(400).json({
|
|
835
|
-
error: `Invalid column. Must be one of: ${BOARD_COLUMNS.join(', ')}`,
|
|
836
|
-
});
|
|
837
|
-
return;
|
|
838
|
-
}
|
|
839
|
-
await provider.moveIssue(issueNumber, column);
|
|
840
|
-
invalidateBoardCache(projectDir);
|
|
841
|
-
res.json({ moved: true });
|
|
842
|
-
}
|
|
843
|
-
catch (error) {
|
|
844
|
-
res
|
|
845
|
-
.status(500)
|
|
846
|
-
.json({ error: error instanceof Error ? error.message : String(error) });
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
async function handlePostBoardIssueComment(projectDir, config, req, res) {
|
|
850
|
-
try {
|
|
851
|
-
const provider = getBoardProvider(config, projectDir);
|
|
852
|
-
if (!provider) {
|
|
853
|
-
res.status(404).json({ error: 'Board not configured' });
|
|
854
|
-
return;
|
|
855
|
-
}
|
|
856
|
-
const issueNumber = parseInt(req.params.number, 10);
|
|
857
|
-
if (isNaN(issueNumber)) {
|
|
858
|
-
res.status(400).json({ error: 'Invalid issue number' });
|
|
859
|
-
return;
|
|
860
|
-
}
|
|
861
|
-
const { body } = req.body;
|
|
862
|
-
if (!body || typeof body !== 'string' || body.trim().length === 0) {
|
|
863
|
-
res.status(400).json({ error: 'body is required' });
|
|
864
|
-
return;
|
|
865
|
-
}
|
|
866
|
-
await provider.commentOnIssue(issueNumber, body);
|
|
867
|
-
invalidateBoardCache(projectDir);
|
|
868
|
-
res.json({ commented: true });
|
|
869
|
-
}
|
|
870
|
-
catch (error) {
|
|
871
|
-
res
|
|
872
|
-
.status(500)
|
|
873
|
-
.json({ error: error instanceof Error ? error.message : String(error) });
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
async function handleDeleteBoardIssue(projectDir, config, req, res) {
|
|
877
|
-
try {
|
|
878
|
-
const provider = getBoardProvider(config, projectDir);
|
|
879
|
-
if (!provider) {
|
|
880
|
-
res.status(404).json({ error: 'Board not configured' });
|
|
881
|
-
return;
|
|
882
|
-
}
|
|
883
|
-
const issueNumber = parseInt(req.params.number, 10);
|
|
884
|
-
if (isNaN(issueNumber)) {
|
|
885
|
-
res.status(400).json({ error: 'Invalid issue number' });
|
|
886
|
-
return;
|
|
887
|
-
}
|
|
888
|
-
await provider.closeIssue(issueNumber);
|
|
889
|
-
invalidateBoardCache(projectDir);
|
|
890
|
-
res.json({ closed: true });
|
|
891
|
-
}
|
|
892
|
-
catch (error) {
|
|
893
|
-
res
|
|
894
|
-
.status(500)
|
|
895
|
-
.json({ error: error instanceof Error ? error.message : String(error) });
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
async function handleCancelAction(projectDir, req, res) {
|
|
899
|
-
try {
|
|
900
|
-
const { type = 'all' } = req.body;
|
|
901
|
-
const validTypes = ['run', 'review', 'all'];
|
|
902
|
-
if (!validTypes.includes(type)) {
|
|
903
|
-
res.status(400).json({
|
|
904
|
-
error: `Invalid type. Must be one of: ${validTypes.join(', ')}`,
|
|
905
|
-
});
|
|
906
|
-
return;
|
|
907
|
-
}
|
|
908
|
-
const results = await performCancel(projectDir, {
|
|
909
|
-
type: type,
|
|
910
|
-
force: true,
|
|
911
|
-
});
|
|
912
|
-
const hasFailure = results.some((r) => !r.success);
|
|
913
|
-
res.status(hasFailure ? 500 : 200).json({ results });
|
|
914
|
-
}
|
|
915
|
-
catch (error) {
|
|
916
|
-
res
|
|
917
|
-
.status(500)
|
|
918
|
-
.json({ error: error instanceof Error ? error.message : String(error) });
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
function handleRetryAction(projectDir, config, req, res) {
|
|
922
|
-
try {
|
|
923
|
-
const { prdName } = req.body;
|
|
924
|
-
if (!prdName || typeof prdName !== 'string') {
|
|
925
|
-
res.status(400).json({ error: 'prdName is required' });
|
|
926
|
-
return;
|
|
927
|
-
}
|
|
928
|
-
if (!validatePrdName(prdName)) {
|
|
929
|
-
res.status(400).json({ error: 'Invalid PRD name' });
|
|
930
|
-
return;
|
|
931
|
-
}
|
|
932
|
-
const prdDir = path.join(projectDir, config.prdDir);
|
|
933
|
-
const normalized = prdName.endsWith('.md') ? prdName : `${prdName}.md`;
|
|
934
|
-
const pendingPath = path.join(prdDir, normalized);
|
|
935
|
-
const donePath = path.join(prdDir, 'done', normalized);
|
|
936
|
-
if (fs.existsSync(pendingPath)) {
|
|
937
|
-
res.json({ message: `"${normalized}" is already pending` });
|
|
938
|
-
return;
|
|
939
|
-
}
|
|
940
|
-
if (!fs.existsSync(donePath)) {
|
|
941
|
-
res.status(404).json({ error: `PRD "${normalized}" not found in done/` });
|
|
942
|
-
return;
|
|
943
|
-
}
|
|
944
|
-
fs.renameSync(donePath, pendingPath);
|
|
945
|
-
res.json({ message: `Moved "${normalized}" back to pending` });
|
|
946
|
-
}
|
|
947
|
-
catch (error) {
|
|
948
|
-
res
|
|
949
|
-
.status(500)
|
|
950
|
-
.json({ error: error instanceof Error ? error.message : String(error) });
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
/**
|
|
954
|
-
* Handle clearing stale executor lock and orphaned claim files.
|
|
955
|
-
* Returns 409 if executor is actively running (should use Stop instead).
|
|
956
|
-
*/
|
|
957
|
-
function handleClearLockAction(projectDir, config, sseClients, _req, res) {
|
|
958
|
-
try {
|
|
959
|
-
const lockPath = executorLockPath(projectDir);
|
|
960
|
-
const lock = checkLockFile(lockPath);
|
|
961
|
-
if (lock.running) {
|
|
962
|
-
res
|
|
963
|
-
.status(409)
|
|
964
|
-
.json({ error: 'Executor is actively running — use Stop instead' });
|
|
965
|
-
return;
|
|
966
|
-
}
|
|
967
|
-
// Remove the stale lock file if it exists
|
|
968
|
-
if (fs.existsSync(lockPath)) {
|
|
969
|
-
fs.unlinkSync(lockPath);
|
|
970
|
-
}
|
|
971
|
-
// Clean up any orphaned claim files
|
|
972
|
-
const prdDir = path.join(projectDir, config.prdDir);
|
|
973
|
-
if (fs.existsSync(prdDir)) {
|
|
974
|
-
cleanOrphanedClaims(prdDir);
|
|
975
|
-
}
|
|
976
|
-
// Broadcast updated status via SSE
|
|
977
|
-
broadcastSSE(sseClients, 'status_changed', fetchStatusSnapshot(projectDir, config));
|
|
978
|
-
res.json({ cleared: true });
|
|
979
|
-
}
|
|
980
|
-
catch (error) {
|
|
981
|
-
res
|
|
982
|
-
.status(500)
|
|
983
|
-
.json({ error: error instanceof Error ? error.message : String(error) });
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
/**
|
|
987
|
-
* Recursively clean up orphaned claim files in the PRD directory.
|
|
988
|
-
* A claim is orphaned if the executor is not running.
|
|
989
|
-
*/
|
|
990
|
-
function cleanOrphanedClaims(dir) {
|
|
991
|
-
let entries;
|
|
992
|
-
try {
|
|
993
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
994
|
-
}
|
|
995
|
-
catch {
|
|
996
|
-
return;
|
|
997
|
-
}
|
|
998
|
-
for (const entry of entries) {
|
|
999
|
-
const fullPath = path.join(dir, entry.name);
|
|
1000
|
-
if (entry.isDirectory() && entry.name !== 'done') {
|
|
1001
|
-
cleanOrphanedClaims(fullPath);
|
|
1002
|
-
}
|
|
1003
|
-
else if (entry.name.endsWith(CLAIM_FILE_EXTENSION)) {
|
|
1004
|
-
// This is a claim file - remove it since executor is not running
|
|
1005
|
-
try {
|
|
1006
|
-
fs.unlinkSync(fullPath);
|
|
1007
|
-
}
|
|
1008
|
-
catch {
|
|
1009
|
-
// Ignore errors during cleanup
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
// ==================== Static Files + SPA Fallback ====================
|
|
1015
|
-
function setupStaticFiles(app) {
|
|
1016
|
-
const webDistPath = path.join(__packageRoot, 'web/dist');
|
|
1017
|
-
if (fs.existsSync(webDistPath)) {
|
|
1018
|
-
app.use(express.static(webDistPath));
|
|
1019
|
-
}
|
|
1020
|
-
app.use((req, res, next) => {
|
|
1021
|
-
if (req.path.startsWith('/api/')) {
|
|
1022
|
-
next();
|
|
1023
|
-
return;
|
|
1024
|
-
}
|
|
1025
|
-
const indexPath = path.resolve(webDistPath, 'index.html');
|
|
1026
|
-
if (fs.existsSync(indexPath)) {
|
|
1027
|
-
res.sendFile(indexPath, (err) => {
|
|
1028
|
-
if (err)
|
|
1029
|
-
next();
|
|
1030
|
-
});
|
|
1031
|
-
}
|
|
1032
|
-
else {
|
|
1033
|
-
next();
|
|
1034
|
-
}
|
|
1035
|
-
});
|
|
1036
|
-
}
|
|
1037
|
-
// ==================== Single-Project Mode ====================
|
|
1038
|
-
/**
|
|
1039
|
-
* Create and configure the Express application (single-project mode)
|
|
1040
|
-
*/
|
|
1041
|
-
export function createApp(projectDir) {
|
|
1042
|
-
const app = express();
|
|
1043
|
-
app.use(cors());
|
|
1044
|
-
app.use(express.json());
|
|
1045
|
-
let config = loadConfig(projectDir);
|
|
1046
|
-
const reloadConfig = () => {
|
|
1047
|
-
config = loadConfig(projectDir);
|
|
1048
|
-
};
|
|
1049
|
-
// SSE client registry for real-time push
|
|
1050
|
-
const sseClients = new Set();
|
|
1051
|
-
// SSE endpoint for real-time status updates
|
|
1052
|
-
app.get('/api/status/events', (req, res) => {
|
|
1053
|
-
res.setHeader('Content-Type', 'text/event-stream');
|
|
1054
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
1055
|
-
res.setHeader('Connection', 'keep-alive');
|
|
1056
|
-
res.flushHeaders();
|
|
1057
|
-
sseClients.add(res);
|
|
1058
|
-
// Send current snapshot immediately on connect
|
|
1059
|
-
try {
|
|
1060
|
-
const snapshot = fetchStatusSnapshot(projectDir, config);
|
|
1061
|
-
res.write(`event: status_changed\ndata: ${JSON.stringify(snapshot)}\n\n`);
|
|
1062
|
-
}
|
|
1063
|
-
catch {
|
|
1064
|
-
// Ignore errors during initial snapshot
|
|
1065
|
-
}
|
|
1066
|
-
req.on('close', () => {
|
|
1067
|
-
sseClients.delete(res);
|
|
1068
|
-
});
|
|
1069
|
-
});
|
|
1070
|
-
// Start the SSE status watcher (runs until process exits)
|
|
1071
|
-
startSseStatusWatcher(sseClients, projectDir, () => config);
|
|
1072
|
-
// API Routes
|
|
1073
|
-
app.get('/api/status', (req, res) => handleGetStatus(projectDir, config, req, res));
|
|
1074
|
-
app.get('/api/schedule-info', (req, res) => handleGetScheduleInfo(projectDir, config, req, res));
|
|
1075
|
-
app.get('/api/prds', (req, res) => handleGetPrds(projectDir, config, req, res));
|
|
1076
|
-
app.get('/api/prds/:name', (req, res) => handleGetPrdByName(projectDir, config, req, res));
|
|
1077
|
-
app.get('/api/prs', (req, res) => handleGetPrs(projectDir, config, req, res));
|
|
1078
|
-
app.get('/api/logs/:name', (req, res) => handleGetLogs(projectDir, config, req, res));
|
|
1079
|
-
app.get('/api/config', (req, res) => handleGetConfig(config, req, res));
|
|
1080
|
-
app.put('/api/config', (req, res) => handlePutConfig(projectDir, () => config, reloadConfig, req, res));
|
|
1081
|
-
app.get('/api/doctor', (req, res) => handleGetDoctor(projectDir, config, req, res));
|
|
1082
|
-
app.post('/api/actions/run', (req, res) => handleSpawnAction(projectDir, ['run'], req, res, (pid) => {
|
|
1083
|
-
broadcastSSE(sseClients, 'executor_started', { pid });
|
|
1084
|
-
}));
|
|
1085
|
-
app.post('/api/actions/review', (req, res) => handleSpawnAction(projectDir, ['review'], req, res));
|
|
1086
|
-
app.post('/api/actions/install-cron', (req, res) => handleSpawnAction(projectDir, ['install'], req, res));
|
|
1087
|
-
app.post('/api/actions/uninstall-cron', (req, res) => handleSpawnAction(projectDir, ['uninstall'], req, res));
|
|
1088
|
-
app.post('/api/actions/cancel', (req, res) => handleCancelAction(projectDir, req, res));
|
|
1089
|
-
app.post('/api/actions/retry', (req, res) => handleRetryAction(projectDir, config, req, res));
|
|
1090
|
-
app.post('/api/actions/clear-lock', (req, res) => handleClearLockAction(projectDir, config, sseClients, req, res));
|
|
1091
|
-
app.get('/api/roadmap', (req, res) => handleGetRoadmap(projectDir, config, req, res));
|
|
1092
|
-
app.post('/api/roadmap/scan', (req, res) => handlePostRoadmapScan(projectDir, config, req, res));
|
|
1093
|
-
app.put('/api/roadmap/toggle', (req, res) => handlePutRoadmapToggle(projectDir, () => config, reloadConfig, req, res));
|
|
1094
|
-
app.post('/api/slack/channels/create', (req, res) => handlePostSlackChannelCreate(req, res));
|
|
1095
|
-
app.post('/api/slack/channels', (req, res) => handlePostSlackChannels(req, res));
|
|
1096
|
-
// Board routes
|
|
1097
|
-
app.get('/api/board/status', (req, res) => handleGetBoardStatus(projectDir, config, req, res));
|
|
1098
|
-
app.get('/api/board/issues', (req, res) => handleGetBoardIssues(projectDir, config, req, res));
|
|
1099
|
-
app.post('/api/board/issues', (req, res) => handlePostBoardIssue(projectDir, config, req, res));
|
|
1100
|
-
app.patch('/api/board/issues/:number/move', (req, res) => handlePatchBoardIssueMove(projectDir, config, req, res));
|
|
1101
|
-
app.post('/api/board/issues/:number/comment', (req, res) => handlePostBoardIssueComment(projectDir, config, req, res));
|
|
1102
|
-
app.delete('/api/board/issues/:number', (req, res) => handleDeleteBoardIssue(projectDir, config, req, res));
|
|
1103
|
-
// ==================== Agent Personas ====================
|
|
1104
|
-
app.post('/api/agents/seed-defaults', (_req, res) => {
|
|
1105
|
-
try {
|
|
1106
|
-
const repos = getRepositories();
|
|
1107
|
-
repos.agentPersona.seedDefaults();
|
|
1108
|
-
res.json({ message: 'Default personas seeded successfully' });
|
|
1109
|
-
}
|
|
1110
|
-
catch (err) {
|
|
1111
|
-
res.status(500).json({ error: err.message });
|
|
1112
|
-
}
|
|
1113
|
-
});
|
|
1114
|
-
app.get('/api/agents', (_req, res) => {
|
|
1115
|
-
try {
|
|
1116
|
-
const repos = getRepositories();
|
|
1117
|
-
const personas = repos.agentPersona.getAll();
|
|
1118
|
-
const masked = personas.map(maskPersonaSecrets);
|
|
1119
|
-
res.json(masked);
|
|
1120
|
-
}
|
|
1121
|
-
catch (err) {
|
|
1122
|
-
res.status(500).json({ error: err.message });
|
|
1123
|
-
}
|
|
1124
|
-
});
|
|
1125
|
-
app.get('/api/agents/:id', (req, res) => {
|
|
1126
|
-
try {
|
|
1127
|
-
const repos = getRepositories();
|
|
1128
|
-
const persona = repos.agentPersona.getById(req.params.id);
|
|
1129
|
-
if (!persona)
|
|
1130
|
-
return res.status(404).json({ error: 'Agent not found' });
|
|
1131
|
-
const masked = maskPersonaSecrets(persona);
|
|
1132
|
-
return res.json(masked);
|
|
1133
|
-
}
|
|
1134
|
-
catch (err) {
|
|
1135
|
-
return res.status(500).json({ error: err.message });
|
|
1136
|
-
}
|
|
1137
|
-
});
|
|
1138
|
-
app.get('/api/agents/:id/prompt', async (req, res) => {
|
|
1139
|
-
try {
|
|
1140
|
-
const repos = getRepositories();
|
|
1141
|
-
const persona = repos.agentPersona.getById(req.params.id);
|
|
1142
|
-
if (!persona)
|
|
1143
|
-
return res.status(404).json({ error: 'Agent not found' });
|
|
1144
|
-
const { compileSoul } = await import('../agents/soul-compiler.js');
|
|
1145
|
-
const prompt = compileSoul(persona);
|
|
1146
|
-
return res.json({ prompt });
|
|
1147
|
-
}
|
|
1148
|
-
catch (err) {
|
|
1149
|
-
return res.status(500).json({ error: err.message });
|
|
1150
|
-
}
|
|
1151
|
-
});
|
|
1152
|
-
app.post('/api/agents', (req, res) => {
|
|
1153
|
-
try {
|
|
1154
|
-
const repos = getRepositories();
|
|
1155
|
-
const input = req.body;
|
|
1156
|
-
if (!input.name || !input.role) {
|
|
1157
|
-
return res.status(400).json({ error: 'name and role are required' });
|
|
1158
|
-
}
|
|
1159
|
-
const persona = repos.agentPersona.create(input);
|
|
1160
|
-
return res.status(201).json(maskPersonaSecrets(persona));
|
|
1161
|
-
}
|
|
1162
|
-
catch (err) {
|
|
1163
|
-
return res.status(500).json({ error: err.message });
|
|
1164
|
-
}
|
|
1165
|
-
});
|
|
1166
|
-
app.put('/api/agents/:id', (req, res) => {
|
|
1167
|
-
try {
|
|
1168
|
-
const repos = getRepositories();
|
|
1169
|
-
const persona = repos.agentPersona.update(req.params.id, req.body);
|
|
1170
|
-
res.json(maskPersonaSecrets(persona));
|
|
1171
|
-
}
|
|
1172
|
-
catch (err) {
|
|
1173
|
-
const msg = err.message;
|
|
1174
|
-
if (msg.includes('not found'))
|
|
1175
|
-
return res.status(404).json({ error: msg });
|
|
1176
|
-
return res.status(500).json({ error: msg });
|
|
1177
|
-
}
|
|
1178
|
-
});
|
|
1179
|
-
app.delete('/api/agents/:id', (req, res) => {
|
|
1180
|
-
try {
|
|
1181
|
-
const repos = getRepositories();
|
|
1182
|
-
repos.agentPersona.delete(req.params.id);
|
|
1183
|
-
res.status(204).send();
|
|
1184
|
-
}
|
|
1185
|
-
catch (err) {
|
|
1186
|
-
res.status(500).json({ error: err.message });
|
|
1187
|
-
}
|
|
1188
|
-
});
|
|
1189
|
-
app.post('/api/agents/:id/avatar', (req, res) => {
|
|
1190
|
-
try {
|
|
1191
|
-
const repos = getRepositories();
|
|
1192
|
-
const { avatarUrl } = req.body;
|
|
1193
|
-
if (!avatarUrl)
|
|
1194
|
-
return res.status(400).json({ error: 'avatarUrl is required' });
|
|
1195
|
-
const persona = repos.agentPersona.update(req.params.id, {
|
|
1196
|
-
avatarUrl,
|
|
1197
|
-
});
|
|
1198
|
-
return res.json(maskPersonaSecrets(persona));
|
|
1199
|
-
}
|
|
1200
|
-
catch (err) {
|
|
1201
|
-
const msg = err.message;
|
|
1202
|
-
if (msg.includes('not found'))
|
|
1203
|
-
return res.status(404).json({ error: msg });
|
|
1204
|
-
return res.status(500).json({ error: msg });
|
|
1205
|
-
}
|
|
1206
|
-
});
|
|
1207
|
-
// ==================== Slack Discussions ====================
|
|
1208
|
-
app.get('/api/discussions', (_req, res) => {
|
|
1209
|
-
try {
|
|
1210
|
-
const repos = getRepositories();
|
|
1211
|
-
const discussions = repos.slackDiscussion.getActive(projectDir);
|
|
1212
|
-
res.json(discussions);
|
|
1213
|
-
}
|
|
1214
|
-
catch (err) {
|
|
1215
|
-
res.status(500).json({ error: err.message });
|
|
1216
|
-
}
|
|
1217
|
-
});
|
|
1218
|
-
app.get('/api/discussions/:id', (req, res) => {
|
|
1219
|
-
try {
|
|
1220
|
-
const repos = getRepositories();
|
|
1221
|
-
const discussion = repos.slackDiscussion.getById(req.params.id);
|
|
1222
|
-
if (!discussion)
|
|
1223
|
-
return res.status(404).json({ error: 'Discussion not found' });
|
|
1224
|
-
return res.json(discussion);
|
|
1225
|
-
}
|
|
1226
|
-
catch (err) {
|
|
1227
|
-
return res.status(500).json({ error: err.message });
|
|
1228
|
-
}
|
|
1229
|
-
});
|
|
1230
|
-
// Auto-scan timer
|
|
1231
|
-
let autoScanTimer = null;
|
|
1232
|
-
function startAutoScan() {
|
|
1233
|
-
stopAutoScan();
|
|
1234
|
-
const currentConfig = loadConfig(projectDir);
|
|
1235
|
-
if (!currentConfig.roadmapScanner.enabled)
|
|
1236
|
-
return;
|
|
1237
|
-
const intervalMs = currentConfig.roadmapScanner.autoScanInterval * 1000;
|
|
1238
|
-
autoScanTimer = setInterval(() => {
|
|
1239
|
-
const cfg = loadConfig(projectDir);
|
|
1240
|
-
if (!cfg.roadmapScanner.enabled)
|
|
1241
|
-
return;
|
|
1242
|
-
const status = getRoadmapStatus(projectDir, cfg);
|
|
1243
|
-
if (status.status === 'complete' || status.status === 'no-roadmap')
|
|
1244
|
-
return;
|
|
1245
|
-
// Fire and forget - async scan
|
|
1246
|
-
scanRoadmap(projectDir, cfg).catch(() => {
|
|
1247
|
-
// Silently ignore auto-scan errors
|
|
1248
|
-
});
|
|
1249
|
-
}, intervalMs);
|
|
1250
|
-
}
|
|
1251
|
-
function stopAutoScan() {
|
|
1252
|
-
if (autoScanTimer) {
|
|
1253
|
-
clearInterval(autoScanTimer);
|
|
1254
|
-
autoScanTimer = null;
|
|
1255
|
-
}
|
|
1256
|
-
}
|
|
1257
|
-
if (config.roadmapScanner.enabled) {
|
|
1258
|
-
startAutoScan();
|
|
1259
|
-
}
|
|
1260
|
-
setupStaticFiles(app);
|
|
1261
|
-
app.use(errorHandler);
|
|
1262
|
-
return app;
|
|
1263
|
-
}
|
|
1264
|
-
// ==================== Global (Multi-Project) Mode ====================
|
|
1265
|
-
/**
|
|
1266
|
-
* Middleware that resolves a project from the registry by :projectId param
|
|
1267
|
-
*/
|
|
1268
|
-
function resolveProject(req, res, next) {
|
|
1269
|
-
const projectId = req.params.projectId;
|
|
1270
|
-
// Decode ~ back to / (frontend encodes / as ~ to avoid Express 5 %2F routing issues)
|
|
1271
|
-
const decodedId = decodeURIComponent(projectId).replace(/~/g, '/');
|
|
1272
|
-
const entries = loadRegistry();
|
|
1273
|
-
const entry = entries.find((e) => e.name === decodedId);
|
|
1274
|
-
if (!entry) {
|
|
1275
|
-
res.status(404).json({ error: `Project not found: ${decodedId}` });
|
|
1276
|
-
return;
|
|
1277
|
-
}
|
|
1278
|
-
if (!fs.existsSync(entry.path) ||
|
|
1279
|
-
!fs.existsSync(path.join(entry.path, CONFIG_FILE_NAME))) {
|
|
1280
|
-
res
|
|
1281
|
-
.status(404)
|
|
1282
|
-
.json({ error: `Project path invalid or missing config: ${entry.path}` });
|
|
1283
|
-
return;
|
|
1284
|
-
}
|
|
1285
|
-
req.projectDir = entry.path;
|
|
1286
|
-
req.projectConfig = loadConfig(entry.path);
|
|
1287
|
-
next();
|
|
1288
|
-
}
|
|
1289
|
-
/**
|
|
1290
|
-
* Create a router with all project-scoped endpoints
|
|
1291
|
-
*/
|
|
1292
|
-
function createProjectRouter() {
|
|
1293
|
-
const router = Router({ mergeParams: true });
|
|
1294
|
-
// Per-project SSE client registry and watchers
|
|
1295
|
-
const projectSseClients = new Map();
|
|
1296
|
-
const projectSseWatchers = new Map();
|
|
1297
|
-
const dir = (req) => req.projectDir;
|
|
1298
|
-
const cfg = (req) => req.projectConfig;
|
|
1299
|
-
// SSE endpoint for project-scoped status updates
|
|
1300
|
-
router.get('/status/events', (req, res) => {
|
|
1301
|
-
const projectDir = dir(req);
|
|
1302
|
-
const config = cfg(req);
|
|
1303
|
-
// Initialize client set for this project if not exists
|
|
1304
|
-
if (!projectSseClients.has(projectDir)) {
|
|
1305
|
-
projectSseClients.set(projectDir, new Set());
|
|
1306
|
-
}
|
|
1307
|
-
const clients = projectSseClients.get(projectDir);
|
|
1308
|
-
// Start watcher for this project if not already running
|
|
1309
|
-
if (!projectSseWatchers.has(projectDir)) {
|
|
1310
|
-
const watcher = startSseStatusWatcher(clients, projectDir, () => loadConfig(projectDir));
|
|
1311
|
-
projectSseWatchers.set(projectDir, watcher);
|
|
1312
|
-
}
|
|
1313
|
-
res.setHeader('Content-Type', 'text/event-stream');
|
|
1314
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
1315
|
-
res.setHeader('Connection', 'keep-alive');
|
|
1316
|
-
res.flushHeaders();
|
|
1317
|
-
clients.add(res);
|
|
1318
|
-
// Send current snapshot immediately on connect
|
|
1319
|
-
try {
|
|
1320
|
-
const snapshot = fetchStatusSnapshot(projectDir, config);
|
|
1321
|
-
res.write(`event: status_changed\ndata: ${JSON.stringify(snapshot)}\n\n`);
|
|
1322
|
-
}
|
|
1323
|
-
catch {
|
|
1324
|
-
// Ignore errors during initial snapshot
|
|
1325
|
-
}
|
|
1326
|
-
req.on('close', () => {
|
|
1327
|
-
clients.delete(res);
|
|
1328
|
-
});
|
|
1329
|
-
});
|
|
1330
|
-
router.get('/status', (req, res) => handleGetStatus(dir(req), cfg(req), req, res));
|
|
1331
|
-
router.get('/schedule-info', (req, res) => handleGetScheduleInfo(dir(req), cfg(req), req, res));
|
|
1332
|
-
router.get('/prds', (req, res) => handleGetPrds(dir(req), cfg(req), req, res));
|
|
1333
|
-
router.get('/prds/:name', (req, res) => handleGetPrdByName(dir(req), cfg(req), req, res));
|
|
1334
|
-
router.get('/prs', (req, res) => handleGetPrs(dir(req), cfg(req), req, res));
|
|
1335
|
-
router.get('/logs/:name', (req, res) => handleGetLogs(dir(req), cfg(req), req, res));
|
|
1336
|
-
router.get('/config', (req, res) => handleGetConfig(cfg(req), req, res));
|
|
1337
|
-
router.put('/config', (req, res) => {
|
|
1338
|
-
const projectDir = dir(req);
|
|
1339
|
-
let config = cfg(req);
|
|
1340
|
-
handlePutConfig(projectDir, () => config, () => {
|
|
1341
|
-
config = loadConfig(projectDir);
|
|
1342
|
-
}, req, res);
|
|
1343
|
-
});
|
|
1344
|
-
router.get('/doctor', (req, res) => handleGetDoctor(dir(req), cfg(req), req, res));
|
|
1345
|
-
router.post('/actions/run', (req, res) => {
|
|
1346
|
-
const projectDir = dir(req);
|
|
1347
|
-
handleSpawnAction(projectDir, ['run'], req, res, (pid) => {
|
|
1348
|
-
const clients = projectSseClients.get(projectDir);
|
|
1349
|
-
if (clients) {
|
|
1350
|
-
broadcastSSE(clients, 'executor_started', { pid });
|
|
1351
|
-
}
|
|
1352
|
-
});
|
|
1353
|
-
});
|
|
1354
|
-
router.post('/actions/review', (req, res) => handleSpawnAction(dir(req), ['review'], req, res));
|
|
1355
|
-
router.post('/actions/install-cron', (req, res) => handleSpawnAction(dir(req), ['install'], req, res));
|
|
1356
|
-
router.post('/actions/uninstall-cron', (req, res) => handleSpawnAction(dir(req), ['uninstall'], req, res));
|
|
1357
|
-
router.post('/actions/cancel', (req, res) => handleCancelAction(dir(req), req, res));
|
|
1358
|
-
router.post('/actions/retry', (req, res) => handleRetryAction(dir(req), cfg(req), req, res));
|
|
1359
|
-
router.post('/actions/clear-lock', (req, res) => {
|
|
1360
|
-
const projectDir = dir(req);
|
|
1361
|
-
const config = cfg(req);
|
|
1362
|
-
const clients = projectSseClients.get(projectDir);
|
|
1363
|
-
handleClearLockAction(projectDir, config, clients ?? new Set(), req, res);
|
|
1364
|
-
});
|
|
1365
|
-
router.get('/roadmap', (req, res) => handleGetRoadmap(dir(req), cfg(req), req, res));
|
|
1366
|
-
router.post('/roadmap/scan', (req, res) => handlePostRoadmapScan(dir(req), cfg(req), req, res));
|
|
1367
|
-
router.put('/roadmap/toggle', (req, res) => {
|
|
1368
|
-
const projectDir = dir(req);
|
|
1369
|
-
let config = cfg(req);
|
|
1370
|
-
handlePutRoadmapToggle(projectDir, () => config, () => {
|
|
1371
|
-
config = loadConfig(projectDir);
|
|
1372
|
-
}, req, res);
|
|
1373
|
-
});
|
|
1374
|
-
// Board routes
|
|
1375
|
-
router.get('/board/status', (req, res) => handleGetBoardStatus(dir(req), cfg(req), req, res));
|
|
1376
|
-
router.get('/board/issues', (req, res) => handleGetBoardIssues(dir(req), cfg(req), req, res));
|
|
1377
|
-
router.post('/board/issues', (req, res) => handlePostBoardIssue(dir(req), cfg(req), req, res));
|
|
1378
|
-
router.patch('/board/issues/:number/move', (req, res) => handlePatchBoardIssueMove(dir(req), cfg(req), req, res));
|
|
1379
|
-
router.post('/board/issues/:number/comment', (req, res) => handlePostBoardIssueComment(dir(req), cfg(req), req, res));
|
|
1380
|
-
router.delete('/board/issues/:number', (req, res) => handleDeleteBoardIssue(dir(req), cfg(req), req, res));
|
|
1381
|
-
// ==================== Agent Personas ====================
|
|
1382
|
-
router.post('/agents/seed-defaults', (_req, res) => {
|
|
1383
|
-
try {
|
|
1384
|
-
const repos = getRepositories();
|
|
1385
|
-
repos.agentPersona.seedDefaults();
|
|
1386
|
-
res.json({ message: 'Default personas seeded successfully' });
|
|
1387
|
-
}
|
|
1388
|
-
catch (err) {
|
|
1389
|
-
res.status(500).json({ error: err.message });
|
|
1390
|
-
}
|
|
1391
|
-
});
|
|
1392
|
-
router.get('/agents', (_req, res) => {
|
|
1393
|
-
try {
|
|
1394
|
-
const repos = getRepositories();
|
|
1395
|
-
const personas = repos.agentPersona.getAll();
|
|
1396
|
-
const masked = personas.map(maskPersonaSecrets);
|
|
1397
|
-
res.json(masked);
|
|
1398
|
-
}
|
|
1399
|
-
catch (err) {
|
|
1400
|
-
res.status(500).json({ error: err.message });
|
|
1401
|
-
}
|
|
1402
|
-
});
|
|
1403
|
-
router.get('/agents/:id', (req, res) => {
|
|
1404
|
-
try {
|
|
1405
|
-
const repos = getRepositories();
|
|
1406
|
-
const persona = repos.agentPersona.getById(req.params.id);
|
|
1407
|
-
if (!persona)
|
|
1408
|
-
return res.status(404).json({ error: 'Agent not found' });
|
|
1409
|
-
const masked = maskPersonaSecrets(persona);
|
|
1410
|
-
return res.json(masked);
|
|
1411
|
-
}
|
|
1412
|
-
catch (err) {
|
|
1413
|
-
return res.status(500).json({ error: err.message });
|
|
1414
|
-
}
|
|
1415
|
-
});
|
|
1416
|
-
router.get('/agents/:id/prompt', async (req, res) => {
|
|
1417
|
-
try {
|
|
1418
|
-
const repos = getRepositories();
|
|
1419
|
-
const persona = repos.agentPersona.getById(req.params.id);
|
|
1420
|
-
if (!persona)
|
|
1421
|
-
return res.status(404).json({ error: 'Agent not found' });
|
|
1422
|
-
const { compileSoul } = await import('../agents/soul-compiler.js');
|
|
1423
|
-
const prompt = compileSoul(persona);
|
|
1424
|
-
return res.json({ prompt });
|
|
1425
|
-
}
|
|
1426
|
-
catch (err) {
|
|
1427
|
-
return res.status(500).json({ error: err.message });
|
|
1428
|
-
}
|
|
1429
|
-
});
|
|
1430
|
-
router.post('/agents', (req, res) => {
|
|
1431
|
-
try {
|
|
1432
|
-
const repos = getRepositories();
|
|
1433
|
-
const input = req.body;
|
|
1434
|
-
if (!input.name || !input.role) {
|
|
1435
|
-
return res.status(400).json({ error: 'name and role are required' });
|
|
1436
|
-
}
|
|
1437
|
-
const persona = repos.agentPersona.create(input);
|
|
1438
|
-
return res.status(201).json(maskPersonaSecrets(persona));
|
|
1439
|
-
}
|
|
1440
|
-
catch (err) {
|
|
1441
|
-
return res.status(500).json({ error: err.message });
|
|
1442
|
-
}
|
|
1443
|
-
});
|
|
1444
|
-
router.put('/agents/:id', (req, res) => {
|
|
1445
|
-
try {
|
|
1446
|
-
const repos = getRepositories();
|
|
1447
|
-
const persona = repos.agentPersona.update(req.params.id, req.body);
|
|
1448
|
-
res.json(maskPersonaSecrets(persona));
|
|
1449
|
-
}
|
|
1450
|
-
catch (err) {
|
|
1451
|
-
const msg = err.message;
|
|
1452
|
-
if (msg.includes('not found'))
|
|
1453
|
-
return res.status(404).json({ error: msg });
|
|
1454
|
-
return res.status(500).json({ error: msg });
|
|
1455
|
-
}
|
|
1456
|
-
});
|
|
1457
|
-
router.delete('/agents/:id', (req, res) => {
|
|
1458
|
-
try {
|
|
1459
|
-
const repos = getRepositories();
|
|
1460
|
-
repos.agentPersona.delete(req.params.id);
|
|
1461
|
-
res.status(204).send();
|
|
1462
|
-
}
|
|
1463
|
-
catch (err) {
|
|
1464
|
-
res.status(500).json({ error: err.message });
|
|
1465
|
-
}
|
|
1466
|
-
});
|
|
1467
|
-
router.post('/agents/:id/avatar', (req, res) => {
|
|
1468
|
-
try {
|
|
1469
|
-
const repos = getRepositories();
|
|
1470
|
-
const { avatarUrl } = req.body;
|
|
1471
|
-
if (!avatarUrl)
|
|
1472
|
-
return res.status(400).json({ error: 'avatarUrl is required' });
|
|
1473
|
-
const persona = repos.agentPersona.update(req.params.id, {
|
|
1474
|
-
avatarUrl,
|
|
1475
|
-
});
|
|
1476
|
-
return res.json(maskPersonaSecrets(persona));
|
|
1477
|
-
}
|
|
1478
|
-
catch (err) {
|
|
1479
|
-
const msg = err.message;
|
|
1480
|
-
if (msg.includes('not found'))
|
|
1481
|
-
return res.status(404).json({ error: msg });
|
|
1482
|
-
return res.status(500).json({ error: msg });
|
|
1483
|
-
}
|
|
1484
|
-
});
|
|
1485
|
-
// ==================== Slack Channels ====================
|
|
1486
|
-
router.post('/slack/channels', (req, res) => handlePostSlackChannels(req, res));
|
|
1487
|
-
router.post('/slack/channels/create', (req, res) => handlePostSlackChannelCreate(req, res));
|
|
1488
|
-
// ==================== Slack Discussions ====================
|
|
1489
|
-
router.get('/discussions', (req, res) => {
|
|
1490
|
-
try {
|
|
1491
|
-
const repos = getRepositories();
|
|
1492
|
-
const discussions = repos.slackDiscussion.getActive(dir(req));
|
|
1493
|
-
res.json(discussions);
|
|
1494
|
-
}
|
|
1495
|
-
catch (err) {
|
|
1496
|
-
res.status(500).json({ error: err.message });
|
|
1497
|
-
}
|
|
1498
|
-
});
|
|
1499
|
-
router.get('/discussions/:id', (req, res) => {
|
|
1500
|
-
try {
|
|
1501
|
-
const repos = getRepositories();
|
|
1502
|
-
const discussion = repos.slackDiscussion.getById(req.params.id);
|
|
1503
|
-
if (!discussion)
|
|
1504
|
-
return res.status(404).json({ error: 'Discussion not found' });
|
|
1505
|
-
return res.json(discussion);
|
|
1506
|
-
}
|
|
1507
|
-
catch (err) {
|
|
1508
|
-
return res.status(500).json({ error: err.message });
|
|
1509
|
-
}
|
|
1510
|
-
});
|
|
1511
|
-
return router;
|
|
1512
|
-
}
|
|
1513
|
-
/**
|
|
1514
|
-
* Create the Express application for global (multi-project) mode
|
|
1515
|
-
*/
|
|
1516
|
-
export function createGlobalApp() {
|
|
1517
|
-
const app = express();
|
|
1518
|
-
app.use(cors());
|
|
1519
|
-
app.use(express.json());
|
|
1520
|
-
// List all registered projects
|
|
1521
|
-
app.get('/api/projects', (_req, res) => {
|
|
1522
|
-
try {
|
|
1523
|
-
const entries = loadRegistry();
|
|
1524
|
-
const { invalid } = validateRegistry();
|
|
1525
|
-
const invalidPaths = new Set(invalid.map((e) => e.path));
|
|
1526
|
-
res.json(entries.map((e) => ({
|
|
1527
|
-
name: e.name,
|
|
1528
|
-
path: e.path,
|
|
1529
|
-
valid: !invalidPaths.has(e.path),
|
|
1530
|
-
})));
|
|
1531
|
-
}
|
|
1532
|
-
catch (error) {
|
|
1533
|
-
res.status(500).json({
|
|
1534
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1535
|
-
});
|
|
1536
|
-
}
|
|
1537
|
-
});
|
|
1538
|
-
// Project-scoped routes
|
|
1539
|
-
app.use('/api/projects/:projectId', resolveProject, createProjectRouter());
|
|
1540
|
-
setupStaticFiles(app);
|
|
1541
|
-
app.use(errorHandler);
|
|
1542
|
-
return app;
|
|
1543
|
-
}
|
|
1544
|
-
// ==================== Server Startup ====================
|
|
1545
|
-
const PRE_SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
1546
|
-
const GRACEFUL_SHUTDOWN_TIMEOUT_MS = 12_000;
|
|
1547
|
-
function withTimeout(promise, timeoutMs, label) {
|
|
1548
|
-
let timeoutId = null;
|
|
1549
|
-
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
1550
|
-
timeoutId = setTimeout(() => {
|
|
1551
|
-
reject(new Error(`${label} timed out after ${timeoutMs}ms`));
|
|
1552
|
-
}, timeoutMs);
|
|
1553
|
-
timeoutId.unref?.();
|
|
1554
|
-
});
|
|
1555
|
-
return Promise.race([promise, timeoutPromise]).finally(() => {
|
|
1556
|
-
if (timeoutId)
|
|
1557
|
-
clearTimeout(timeoutId);
|
|
1558
|
-
});
|
|
1559
|
-
}
|
|
1560
|
-
/**
|
|
1561
|
-
* Graceful shutdown handler
|
|
1562
|
-
*/
|
|
1563
|
-
function setupGracefulShutdown(server, beforeClose) {
|
|
1564
|
-
let shuttingDown = false;
|
|
1565
|
-
const sockets = new Set();
|
|
1566
|
-
server.on('connection', (socket) => {
|
|
1567
|
-
sockets.add(socket);
|
|
1568
|
-
socket.on('close', () => sockets.delete(socket));
|
|
1569
|
-
});
|
|
1570
|
-
const closeOpenConnections = () => {
|
|
1571
|
-
server.closeIdleConnections?.();
|
|
1572
|
-
server.closeAllConnections?.();
|
|
1573
|
-
for (const socket of sockets) {
|
|
1574
|
-
socket.destroy();
|
|
1575
|
-
}
|
|
1576
|
-
};
|
|
1577
|
-
const shutdown = (signal) => {
|
|
1578
|
-
if (shuttingDown) {
|
|
1579
|
-
console.warn(`${signal} received again, forcing shutdown...`);
|
|
1580
|
-
closeOpenConnections();
|
|
1581
|
-
process.exit(signal === 'SIGINT' ? 130 : 143);
|
|
1582
|
-
return;
|
|
1583
|
-
}
|
|
1584
|
-
shuttingDown = true;
|
|
1585
|
-
if (signal === 'SIGINT') {
|
|
1586
|
-
console.log('\nSIGINT received, shutting down server...');
|
|
1587
|
-
}
|
|
1588
|
-
else {
|
|
1589
|
-
console.log('SIGTERM received, shutting down server...');
|
|
1590
|
-
}
|
|
1591
|
-
const forceExitTimer = setTimeout(() => {
|
|
1592
|
-
console.warn(`Graceful shutdown timed out after ${GRACEFUL_SHUTDOWN_TIMEOUT_MS}ms; forcing exit`);
|
|
1593
|
-
closeOpenConnections();
|
|
1594
|
-
process.exit(1);
|
|
1595
|
-
}, GRACEFUL_SHUTDOWN_TIMEOUT_MS);
|
|
1596
|
-
forceExitTimer.unref?.();
|
|
1597
|
-
const runPreShutdown = beforeClose
|
|
1598
|
-
? withTimeout(Promise.resolve(beforeClose()), PRE_SHUTDOWN_TIMEOUT_MS, 'Pre-shutdown cleanup')
|
|
1599
|
-
: Promise.resolve();
|
|
1600
|
-
runPreShutdown
|
|
1601
|
-
.catch((err) => {
|
|
1602
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1603
|
-
console.warn(`Pre-shutdown cleanup failed: ${message}`);
|
|
1604
|
-
})
|
|
1605
|
-
.finally(() => {
|
|
1606
|
-
server.close((err) => {
|
|
1607
|
-
clearTimeout(forceExitTimer);
|
|
1608
|
-
if (err) {
|
|
1609
|
-
console.warn(`Server close failed: ${err.message}`);
|
|
1610
|
-
process.exit(1);
|
|
1611
|
-
return;
|
|
1612
|
-
}
|
|
1613
|
-
console.log('Server closed');
|
|
1614
|
-
process.exit(0);
|
|
1615
|
-
});
|
|
1616
|
-
closeOpenConnections();
|
|
1617
|
-
});
|
|
1618
|
-
};
|
|
1619
|
-
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
1620
|
-
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
1621
|
-
}
|
|
1622
|
-
/**
|
|
1623
|
-
* Start the HTTP server (single-project mode)
|
|
1624
|
-
*/
|
|
1625
|
-
export function startServer(projectDir, port) {
|
|
1626
|
-
const config = loadConfig(projectDir);
|
|
1627
|
-
const app = createApp(projectDir);
|
|
1628
|
-
const listener = new SlackInteractionListener(config);
|
|
1629
|
-
const server = app.listen(port, () => {
|
|
1630
|
-
console.log(`\nNight Watch UI http://localhost:${port}`);
|
|
1631
|
-
console.log(`Project ${projectDir}`);
|
|
1632
|
-
console.log(`Provider ${config.provider}`);
|
|
1633
|
-
const slack = config.slack;
|
|
1634
|
-
if (slack?.enabled && slack.botToken) {
|
|
1635
|
-
console.log(`Slack enabled — channels: ${Object.entries(slack.channels ?? {}).map(([k, v]) => `#${k}=${v}`).join(', ')}`);
|
|
1636
|
-
if (slack.replicateApiToken) {
|
|
1637
|
-
console.log(`Avatar gen Replicate Flux enabled`);
|
|
1638
|
-
}
|
|
1639
|
-
}
|
|
1640
|
-
else {
|
|
1641
|
-
console.log(`Slack not configured`);
|
|
1642
|
-
}
|
|
1643
|
-
console.log('');
|
|
1644
|
-
});
|
|
1645
|
-
void listener.start().catch((err) => {
|
|
1646
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1647
|
-
console.warn(`Slack interaction listener failed to start: ${message}`);
|
|
1648
|
-
});
|
|
1649
|
-
setupGracefulShutdown(server, async () => {
|
|
1650
|
-
await listener.stop();
|
|
1651
|
-
});
|
|
1652
|
-
}
|
|
1653
|
-
/**
|
|
1654
|
-
* Start the HTTP server (global multi-project mode)
|
|
1655
|
-
*/
|
|
1656
|
-
export function startGlobalServer(port) {
|
|
1657
|
-
const entries = loadRegistry();
|
|
1658
|
-
if (entries.length === 0) {
|
|
1659
|
-
console.error("No projects registered. Run 'night-watch init' in a project first.");
|
|
1660
|
-
process.exit(1);
|
|
1661
|
-
}
|
|
1662
|
-
const { valid, invalid } = validateRegistry();
|
|
1663
|
-
if (invalid.length > 0) {
|
|
1664
|
-
console.warn(`Warning: ${invalid.length} registered project(s) have invalid paths and will be skipped.`);
|
|
1665
|
-
}
|
|
1666
|
-
console.log(`\nNight Watch Global UI`);
|
|
1667
|
-
console.log(`Managing ${valid.length} project(s):`);
|
|
1668
|
-
for (const p of valid) {
|
|
1669
|
-
const cfg = loadConfig(p.path);
|
|
1670
|
-
const slackStatus = cfg.slack?.enabled && cfg.slack.botToken ? 'slack:on' : 'slack:off';
|
|
1671
|
-
const avatarStatus = cfg.slack?.replicateApiToken ? ' avatar-gen:on' : '';
|
|
1672
|
-
console.log(` - ${p.name} (${p.path}) [${slackStatus}${avatarStatus}]`);
|
|
1673
|
-
}
|
|
1674
|
-
const app = createGlobalApp();
|
|
1675
|
-
const listenersBySlackToken = new Map();
|
|
1676
|
-
for (const project of valid) {
|
|
1677
|
-
const config = loadConfig(project.path);
|
|
1678
|
-
const slack = config.slack;
|
|
1679
|
-
if (!slack?.enabled ||
|
|
1680
|
-
!slack.discussionEnabled ||
|
|
1681
|
-
!slack.botToken ||
|
|
1682
|
-
!slack.appToken) {
|
|
1683
|
-
continue;
|
|
1684
|
-
}
|
|
1685
|
-
const key = `${slack.botToken}:${slack.appToken}`;
|
|
1686
|
-
if (!listenersBySlackToken.has(key)) {
|
|
1687
|
-
listenersBySlackToken.set(key, new SlackInteractionListener(config));
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
1690
|
-
const listeners = Array.from(listenersBySlackToken.values());
|
|
1691
|
-
const server = app.listen(port, () => {
|
|
1692
|
-
console.log(`Night Watch Global UI running at http://localhost:${port}`);
|
|
1693
|
-
});
|
|
1694
|
-
for (const listener of listeners) {
|
|
1695
|
-
void listener.start().catch((err) => {
|
|
1696
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1697
|
-
console.warn(`Slack interaction listener failed to start: ${message}`);
|
|
1698
|
-
});
|
|
1699
|
-
}
|
|
1700
|
-
setupGracefulShutdown(server, async () => {
|
|
1701
|
-
await Promise.allSettled(listeners.map((listener) => listener.stop()));
|
|
1702
|
-
});
|
|
1703
|
-
}
|
|
1704
|
-
//# sourceMappingURL=index.js.map
|