@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.
Files changed (384) hide show
  1. package/bin/night-watch.mjs +1 -1
  2. package/dist/cli.d.ts +3 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/{src/cli.js → cli.js} +1 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/{src/commands → commands}/audit.d.ts +2 -2
  7. package/dist/commands/audit.d.ts.map +1 -0
  8. package/dist/commands/audit.js +105 -0
  9. package/dist/commands/audit.js.map +1 -0
  10. package/dist/{src/commands → commands}/board.d.ts +1 -1
  11. package/dist/commands/board.d.ts.map +1 -0
  12. package/dist/commands/board.js +664 -0
  13. package/dist/commands/board.js.map +1 -0
  14. package/dist/{src/commands → commands}/cancel.d.ts +3 -3
  15. package/dist/commands/cancel.d.ts.map +1 -0
  16. package/dist/{src/commands → commands}/cancel.js +18 -20
  17. package/dist/commands/cancel.js.map +1 -0
  18. package/dist/commands/dashboard/tab-actions.d.ts.map +1 -0
  19. package/dist/commands/dashboard/tab-actions.js.map +1 -0
  20. package/dist/{src/commands → commands}/dashboard/tab-config.d.ts +3 -3
  21. package/dist/commands/dashboard/tab-config.d.ts.map +1 -0
  22. package/dist/{src/commands → commands}/dashboard/tab-config.js +250 -187
  23. package/dist/commands/dashboard/tab-config.js.map +1 -0
  24. package/dist/{src/commands → commands}/dashboard/tab-logs.d.ts +1 -1
  25. package/dist/commands/dashboard/tab-logs.d.ts.map +1 -0
  26. package/dist/{src/commands → commands}/dashboard/tab-logs.js +62 -38
  27. package/dist/commands/dashboard/tab-logs.js.map +1 -0
  28. package/dist/{src/commands → commands}/dashboard/tab-schedules.d.ts +1 -1
  29. package/dist/commands/dashboard/tab-schedules.d.ts.map +1 -0
  30. package/dist/{src/commands → commands}/dashboard/tab-schedules.js +85 -76
  31. package/dist/commands/dashboard/tab-schedules.js.map +1 -0
  32. package/dist/{src/commands → commands}/dashboard/tab-status.d.ts +7 -7
  33. package/dist/commands/dashboard/tab-status.d.ts.map +1 -0
  34. package/dist/{src/commands → commands}/dashboard/tab-status.js +98 -95
  35. package/dist/commands/dashboard/tab-status.js.map +1 -0
  36. package/dist/{src/commands → commands}/dashboard/types.d.ts +3 -4
  37. package/dist/commands/dashboard/types.d.ts.map +1 -0
  38. package/dist/commands/dashboard/types.js.map +1 -0
  39. package/dist/{src/commands → commands}/dashboard.d.ts +2 -2
  40. package/dist/commands/dashboard.d.ts.map +1 -0
  41. package/dist/{src/commands → commands}/dashboard.js +32 -33
  42. package/dist/commands/dashboard.js.map +1 -0
  43. package/dist/{src/commands → commands}/doctor.d.ts +2 -2
  44. package/dist/commands/doctor.d.ts.map +1 -0
  45. package/dist/{src/commands → commands}/doctor.js +40 -43
  46. package/dist/commands/doctor.js.map +1 -0
  47. package/dist/{src/commands → commands}/history.d.ts +1 -1
  48. package/dist/commands/history.d.ts.map +1 -0
  49. package/dist/{src/commands → commands}/history.js +11 -18
  50. package/dist/commands/history.js.map +1 -0
  51. package/dist/{src/commands → commands}/init.d.ts +1 -1
  52. package/dist/commands/init.d.ts.map +1 -0
  53. package/dist/{src/commands → commands}/init.js +62 -36
  54. package/dist/commands/init.js.map +1 -0
  55. package/dist/{src/commands → commands}/install.d.ts +2 -2
  56. package/dist/commands/install.d.ts.map +1 -0
  57. package/dist/{src/commands → commands}/install.js +48 -50
  58. package/dist/commands/install.js.map +1 -0
  59. package/dist/{src/commands → commands}/logs.d.ts +1 -1
  60. package/dist/commands/logs.d.ts.map +1 -0
  61. package/dist/{src/commands → commands}/logs.js +29 -30
  62. package/dist/commands/logs.js.map +1 -0
  63. package/dist/{src/commands → commands}/prd-state.d.ts +1 -1
  64. package/dist/commands/prd-state.d.ts.map +1 -0
  65. package/dist/{src/commands → commands}/prd-state.js +14 -14
  66. package/dist/commands/prd-state.js.map +1 -0
  67. package/dist/{src/commands → commands}/prd.d.ts +1 -1
  68. package/dist/commands/prd.d.ts.map +1 -0
  69. package/dist/{src/commands → commands}/prd.js +57 -66
  70. package/dist/commands/prd.js.map +1 -0
  71. package/dist/{src/commands → commands}/prds.d.ts +1 -1
  72. package/dist/commands/prds.d.ts.map +1 -0
  73. package/dist/{src/commands → commands}/prds.js +51 -53
  74. package/dist/commands/prds.js.map +1 -0
  75. package/dist/{src/commands → commands}/prs.d.ts +1 -1
  76. package/dist/commands/prs.d.ts.map +1 -0
  77. package/dist/{src/commands → commands}/prs.js +22 -24
  78. package/dist/commands/prs.js.map +1 -0
  79. package/dist/{src/commands → commands}/qa.d.ts +2 -2
  80. package/dist/commands/qa.d.ts.map +1 -0
  81. package/dist/{src/commands → commands}/qa.js +50 -51
  82. package/dist/commands/qa.js.map +1 -0
  83. package/dist/{src/commands → commands}/retry.d.ts +1 -1
  84. package/dist/commands/retry.d.ts.map +1 -0
  85. package/dist/{src/commands → commands}/retry.js +9 -10
  86. package/dist/commands/retry.js.map +1 -0
  87. package/dist/{src/commands → commands}/review.d.ts +2 -2
  88. package/dist/commands/review.d.ts.map +1 -0
  89. package/dist/{src/commands → commands}/review.js +68 -59
  90. package/dist/commands/review.js.map +1 -0
  91. package/dist/{src/commands → commands}/run.d.ts +2 -2
  92. package/dist/commands/run.d.ts.map +1 -0
  93. package/dist/{src/commands → commands}/run.js +87 -83
  94. package/dist/commands/run.js.map +1 -0
  95. package/dist/{src/commands → commands}/serve.d.ts +2 -2
  96. package/dist/commands/serve.d.ts.map +1 -0
  97. package/dist/{src/commands → commands}/serve.js +18 -18
  98. package/dist/commands/serve.js.map +1 -0
  99. package/dist/{src/commands → commands}/slice.d.ts +2 -2
  100. package/dist/commands/slice.d.ts.map +1 -0
  101. package/dist/{src/commands → commands}/slice.js +50 -46
  102. package/dist/commands/slice.js.map +1 -0
  103. package/dist/{src/commands → commands}/state.d.ts +1 -1
  104. package/dist/commands/state.d.ts.map +1 -0
  105. package/dist/{src/commands → commands}/state.js +20 -22
  106. package/dist/commands/state.js.map +1 -0
  107. package/dist/{src/commands → commands}/status.d.ts +1 -1
  108. package/dist/commands/status.d.ts.map +1 -0
  109. package/dist/{src/commands → commands}/status.js +75 -54
  110. package/dist/commands/status.js.map +1 -0
  111. package/dist/{src/commands → commands}/uninstall.d.ts +1 -1
  112. package/dist/commands/uninstall.d.ts.map +1 -0
  113. package/dist/{src/commands → commands}/uninstall.js +12 -14
  114. package/dist/commands/uninstall.js.map +1 -0
  115. package/dist/{src/commands → commands}/update.d.ts +1 -1
  116. package/dist/commands/update.d.ts.map +1 -0
  117. package/dist/{src/commands → commands}/update.js +23 -23
  118. package/dist/commands/update.js.map +1 -0
  119. package/package.json +18 -42
  120. package/LICENSE +0 -21
  121. package/README.md +0 -132
  122. package/dist/shared/types.d.ts +0 -226
  123. package/dist/shared/types.d.ts.map +0 -1
  124. package/dist/shared/types.js +0 -7
  125. package/dist/shared/types.js.map +0 -1
  126. package/dist/src/agents/soul-compiler.d.ts +0 -11
  127. package/dist/src/agents/soul-compiler.d.ts.map +0 -1
  128. package/dist/src/agents/soul-compiler.js +0 -157
  129. package/dist/src/agents/soul-compiler.js.map +0 -1
  130. package/dist/src/board/factory.d.ts +0 -3
  131. package/dist/src/board/factory.d.ts.map +0 -1
  132. package/dist/src/board/factory.js +0 -10
  133. package/dist/src/board/factory.js.map +0 -1
  134. package/dist/src/board/providers/github-graphql.d.ts +0 -16
  135. package/dist/src/board/providers/github-graphql.d.ts.map +0 -1
  136. package/dist/src/board/providers/github-graphql.js +0 -43
  137. package/dist/src/board/providers/github-graphql.js.map +0 -1
  138. package/dist/src/board/providers/github-projects.d.ts +0 -51
  139. package/dist/src/board/providers/github-projects.d.ts.map +0 -1
  140. package/dist/src/board/providers/github-projects.js +0 -672
  141. package/dist/src/board/providers/github-projects.js.map +0 -1
  142. package/dist/src/board/types.d.ts +0 -60
  143. package/dist/src/board/types.d.ts.map +0 -1
  144. package/dist/src/board/types.js +0 -4
  145. package/dist/src/board/types.js.map +0 -1
  146. package/dist/src/cli.d.ts +0 -3
  147. package/dist/src/cli.d.ts.map +0 -1
  148. package/dist/src/cli.js.map +0 -1
  149. package/dist/src/commands/audit.d.ts.map +0 -1
  150. package/dist/src/commands/audit.js +0 -98
  151. package/dist/src/commands/audit.js.map +0 -1
  152. package/dist/src/commands/board.d.ts.map +0 -1
  153. package/dist/src/commands/board.js +0 -294
  154. package/dist/src/commands/board.js.map +0 -1
  155. package/dist/src/commands/cancel.d.ts.map +0 -1
  156. package/dist/src/commands/cancel.js.map +0 -1
  157. package/dist/src/commands/dashboard/tab-actions.d.ts.map +0 -1
  158. package/dist/src/commands/dashboard/tab-actions.js.map +0 -1
  159. package/dist/src/commands/dashboard/tab-config.d.ts.map +0 -1
  160. package/dist/src/commands/dashboard/tab-config.js.map +0 -1
  161. package/dist/src/commands/dashboard/tab-logs.d.ts.map +0 -1
  162. package/dist/src/commands/dashboard/tab-logs.js.map +0 -1
  163. package/dist/src/commands/dashboard/tab-schedules.d.ts.map +0 -1
  164. package/dist/src/commands/dashboard/tab-schedules.js.map +0 -1
  165. package/dist/src/commands/dashboard/tab-status.d.ts.map +0 -1
  166. package/dist/src/commands/dashboard/tab-status.js.map +0 -1
  167. package/dist/src/commands/dashboard/types.d.ts.map +0 -1
  168. package/dist/src/commands/dashboard/types.js.map +0 -1
  169. package/dist/src/commands/dashboard.d.ts.map +0 -1
  170. package/dist/src/commands/dashboard.js.map +0 -1
  171. package/dist/src/commands/doctor.d.ts.map +0 -1
  172. package/dist/src/commands/doctor.js.map +0 -1
  173. package/dist/src/commands/history.d.ts.map +0 -1
  174. package/dist/src/commands/history.js.map +0 -1
  175. package/dist/src/commands/init.d.ts.map +0 -1
  176. package/dist/src/commands/init.js.map +0 -1
  177. package/dist/src/commands/install.d.ts.map +0 -1
  178. package/dist/src/commands/install.js.map +0 -1
  179. package/dist/src/commands/logs.d.ts.map +0 -1
  180. package/dist/src/commands/logs.js.map +0 -1
  181. package/dist/src/commands/prd-state.d.ts.map +0 -1
  182. package/dist/src/commands/prd-state.js.map +0 -1
  183. package/dist/src/commands/prd.d.ts.map +0 -1
  184. package/dist/src/commands/prd.js.map +0 -1
  185. package/dist/src/commands/prds.d.ts.map +0 -1
  186. package/dist/src/commands/prds.js.map +0 -1
  187. package/dist/src/commands/prs.d.ts.map +0 -1
  188. package/dist/src/commands/prs.js.map +0 -1
  189. package/dist/src/commands/qa.d.ts.map +0 -1
  190. package/dist/src/commands/qa.js.map +0 -1
  191. package/dist/src/commands/retry.d.ts.map +0 -1
  192. package/dist/src/commands/retry.js.map +0 -1
  193. package/dist/src/commands/review.d.ts.map +0 -1
  194. package/dist/src/commands/review.js.map +0 -1
  195. package/dist/src/commands/run.d.ts.map +0 -1
  196. package/dist/src/commands/run.js.map +0 -1
  197. package/dist/src/commands/serve.d.ts.map +0 -1
  198. package/dist/src/commands/serve.js.map +0 -1
  199. package/dist/src/commands/slice.d.ts.map +0 -1
  200. package/dist/src/commands/slice.js.map +0 -1
  201. package/dist/src/commands/state.d.ts.map +0 -1
  202. package/dist/src/commands/state.js.map +0 -1
  203. package/dist/src/commands/status.d.ts.map +0 -1
  204. package/dist/src/commands/status.js.map +0 -1
  205. package/dist/src/commands/uninstall.d.ts.map +0 -1
  206. package/dist/src/commands/uninstall.js.map +0 -1
  207. package/dist/src/commands/update.d.ts.map +0 -1
  208. package/dist/src/commands/update.js.map +0 -1
  209. package/dist/src/config.d.ts +0 -23
  210. package/dist/src/config.d.ts.map +0 -1
  211. package/dist/src/config.js +0 -671
  212. package/dist/src/config.js.map +0 -1
  213. package/dist/src/constants.d.ts +0 -67
  214. package/dist/src/constants.d.ts.map +0 -1
  215. package/dist/src/constants.js +0 -131
  216. package/dist/src/constants.js.map +0 -1
  217. package/dist/src/server/index.d.ts +0 -23
  218. package/dist/src/server/index.d.ts.map +0 -1
  219. package/dist/src/server/index.js +0 -1704
  220. package/dist/src/server/index.js.map +0 -1
  221. package/dist/src/slack/channel-manager.d.ts +0 -32
  222. package/dist/src/slack/channel-manager.d.ts.map +0 -1
  223. package/dist/src/slack/channel-manager.js +0 -128
  224. package/dist/src/slack/channel-manager.js.map +0 -1
  225. package/dist/src/slack/client.d.ts +0 -76
  226. package/dist/src/slack/client.d.ts.map +0 -1
  227. package/dist/src/slack/client.js +0 -193
  228. package/dist/src/slack/client.js.map +0 -1
  229. package/dist/src/slack/deliberation.d.ts +0 -87
  230. package/dist/src/slack/deliberation.d.ts.map +0 -1
  231. package/dist/src/slack/deliberation.js +0 -1354
  232. package/dist/src/slack/deliberation.js.map +0 -1
  233. package/dist/src/slack/index.d.ts +0 -6
  234. package/dist/src/slack/index.d.ts.map +0 -1
  235. package/dist/src/slack/index.js +0 -5
  236. package/dist/src/slack/index.js.map +0 -1
  237. package/dist/src/slack/interaction-listener.d.ts +0 -130
  238. package/dist/src/slack/interaction-listener.d.ts.map +0 -1
  239. package/dist/src/slack/interaction-listener.js +0 -1329
  240. package/dist/src/slack/interaction-listener.js.map +0 -1
  241. package/dist/src/storage/json-state-migrator.d.ts +0 -24
  242. package/dist/src/storage/json-state-migrator.d.ts.map +0 -1
  243. package/dist/src/storage/json-state-migrator.js +0 -197
  244. package/dist/src/storage/json-state-migrator.js.map +0 -1
  245. package/dist/src/storage/repositories/index.d.ts +0 -25
  246. package/dist/src/storage/repositories/index.d.ts.map +0 -1
  247. package/dist/src/storage/repositories/index.js +0 -45
  248. package/dist/src/storage/repositories/index.js.map +0 -1
  249. package/dist/src/storage/repositories/interfaces.d.ts +0 -60
  250. package/dist/src/storage/repositories/interfaces.d.ts.map +0 -1
  251. package/dist/src/storage/repositories/interfaces.js +0 -6
  252. package/dist/src/storage/repositories/interfaces.js.map +0 -1
  253. package/dist/src/storage/repositories/sqlite/agent-persona-repository.d.ts +0 -33
  254. package/dist/src/storage/repositories/sqlite/agent-persona-repository.d.ts.map +0 -1
  255. package/dist/src/storage/repositories/sqlite/agent-persona-repository.js +0 -715
  256. package/dist/src/storage/repositories/sqlite/agent-persona-repository.js.map +0 -1
  257. package/dist/src/storage/repositories/sqlite/execution-history-repository.d.ts +0 -21
  258. package/dist/src/storage/repositories/sqlite/execution-history-repository.d.ts.map +0 -1
  259. package/dist/src/storage/repositories/sqlite/execution-history-repository.js +0 -94
  260. package/dist/src/storage/repositories/sqlite/execution-history-repository.js.map +0 -1
  261. package/dist/src/storage/repositories/sqlite/prd-state-repository.d.ts +0 -17
  262. package/dist/src/storage/repositories/sqlite/prd-state-repository.d.ts.map +0 -1
  263. package/dist/src/storage/repositories/sqlite/prd-state-repository.js +0 -74
  264. package/dist/src/storage/repositories/sqlite/prd-state-repository.js.map +0 -1
  265. package/dist/src/storage/repositories/sqlite/project-registry-repository.d.ts +0 -17
  266. package/dist/src/storage/repositories/sqlite/project-registry-repository.d.ts.map +0 -1
  267. package/dist/src/storage/repositories/sqlite/project-registry-repository.js +0 -43
  268. package/dist/src/storage/repositories/sqlite/project-registry-repository.js.map +0 -1
  269. package/dist/src/storage/repositories/sqlite/roadmap-state-repository.d.ts +0 -14
  270. package/dist/src/storage/repositories/sqlite/roadmap-state-repository.d.ts.map +0 -1
  271. package/dist/src/storage/repositories/sqlite/roadmap-state-repository.js +0 -47
  272. package/dist/src/storage/repositories/sqlite/roadmap-state-repository.js.map +0 -1
  273. package/dist/src/storage/repositories/sqlite/slack-discussion-repository.d.ts +0 -20
  274. package/dist/src/storage/repositories/sqlite/slack-discussion-repository.d.ts.map +0 -1
  275. package/dist/src/storage/repositories/sqlite/slack-discussion-repository.js +0 -88
  276. package/dist/src/storage/repositories/sqlite/slack-discussion-repository.js.map +0 -1
  277. package/dist/src/storage/sqlite/client.d.ts +0 -23
  278. package/dist/src/storage/sqlite/client.d.ts.map +0 -1
  279. package/dist/src/storage/sqlite/client.js +0 -47
  280. package/dist/src/storage/sqlite/client.js.map +0 -1
  281. package/dist/src/storage/sqlite/migrations.d.ts +0 -11
  282. package/dist/src/storage/sqlite/migrations.d.ts.map +0 -1
  283. package/dist/src/storage/sqlite/migrations.js +0 -94
  284. package/dist/src/storage/sqlite/migrations.js.map +0 -1
  285. package/dist/src/templates/prd-template.d.ts +0 -11
  286. package/dist/src/templates/prd-template.d.ts.map +0 -1
  287. package/dist/src/templates/prd-template.js +0 -166
  288. package/dist/src/templates/prd-template.js.map +0 -1
  289. package/dist/src/templates/slicer-prompt.d.ts +0 -54
  290. package/dist/src/templates/slicer-prompt.d.ts.map +0 -1
  291. package/dist/src/templates/slicer-prompt.js +0 -163
  292. package/dist/src/templates/slicer-prompt.js.map +0 -1
  293. package/dist/src/types.d.ts +0 -140
  294. package/dist/src/types.d.ts.map +0 -1
  295. package/dist/src/types.js +0 -5
  296. package/dist/src/types.js.map +0 -1
  297. package/dist/src/utils/avatar-generator.d.ts +0 -6
  298. package/dist/src/utils/avatar-generator.d.ts.map +0 -1
  299. package/dist/src/utils/avatar-generator.js +0 -133
  300. package/dist/src/utils/avatar-generator.js.map +0 -1
  301. package/dist/src/utils/checks.d.ts +0 -55
  302. package/dist/src/utils/checks.d.ts.map +0 -1
  303. package/dist/src/utils/checks.js +0 -246
  304. package/dist/src/utils/checks.js.map +0 -1
  305. package/dist/src/utils/config-writer.d.ts +0 -16
  306. package/dist/src/utils/config-writer.d.ts.map +0 -1
  307. package/dist/src/utils/config-writer.js +0 -45
  308. package/dist/src/utils/config-writer.js.map +0 -1
  309. package/dist/src/utils/crontab.d.ts +0 -62
  310. package/dist/src/utils/crontab.d.ts.map +0 -1
  311. package/dist/src/utils/crontab.js +0 -168
  312. package/dist/src/utils/crontab.js.map +0 -1
  313. package/dist/src/utils/execution-history.d.ts +0 -54
  314. package/dist/src/utils/execution-history.d.ts.map +0 -1
  315. package/dist/src/utils/execution-history.js +0 -80
  316. package/dist/src/utils/execution-history.js.map +0 -1
  317. package/dist/src/utils/github.d.ts +0 -40
  318. package/dist/src/utils/github.d.ts.map +0 -1
  319. package/dist/src/utils/github.js +0 -126
  320. package/dist/src/utils/github.js.map +0 -1
  321. package/dist/src/utils/notify.d.ts +0 -64
  322. package/dist/src/utils/notify.d.ts.map +0 -1
  323. package/dist/src/utils/notify.js +0 -405
  324. package/dist/src/utils/notify.js.map +0 -1
  325. package/dist/src/utils/prd-states.d.ts +0 -16
  326. package/dist/src/utils/prd-states.d.ts.map +0 -1
  327. package/dist/src/utils/prd-states.js +0 -28
  328. package/dist/src/utils/prd-states.js.map +0 -1
  329. package/dist/src/utils/registry.d.ts +0 -45
  330. package/dist/src/utils/registry.d.ts.map +0 -1
  331. package/dist/src/utils/registry.js +0 -86
  332. package/dist/src/utils/registry.js.map +0 -1
  333. package/dist/src/utils/roadmap-parser.d.ts +0 -45
  334. package/dist/src/utils/roadmap-parser.d.ts.map +0 -1
  335. package/dist/src/utils/roadmap-parser.js +0 -136
  336. package/dist/src/utils/roadmap-parser.js.map +0 -1
  337. package/dist/src/utils/roadmap-scanner.d.ts +0 -92
  338. package/dist/src/utils/roadmap-scanner.d.ts.map +0 -1
  339. package/dist/src/utils/roadmap-scanner.js +0 -349
  340. package/dist/src/utils/roadmap-scanner.js.map +0 -1
  341. package/dist/src/utils/roadmap-state.d.ts +0 -90
  342. package/dist/src/utils/roadmap-state.d.ts.map +0 -1
  343. package/dist/src/utils/roadmap-state.js +0 -154
  344. package/dist/src/utils/roadmap-state.js.map +0 -1
  345. package/dist/src/utils/script-result.d.ts +0 -12
  346. package/dist/src/utils/script-result.d.ts.map +0 -1
  347. package/dist/src/utils/script-result.js +0 -46
  348. package/dist/src/utils/script-result.js.map +0 -1
  349. package/dist/src/utils/shell.d.ts +0 -27
  350. package/dist/src/utils/shell.d.ts.map +0 -1
  351. package/dist/src/utils/shell.js +0 -64
  352. package/dist/src/utils/shell.js.map +0 -1
  353. package/dist/src/utils/status-data.d.ts +0 -148
  354. package/dist/src/utils/status-data.d.ts.map +0 -1
  355. package/dist/src/utils/status-data.js +0 -548
  356. package/dist/src/utils/status-data.js.map +0 -1
  357. package/dist/src/utils/ui.d.ts +0 -55
  358. package/dist/src/utils/ui.d.ts.map +0 -1
  359. package/dist/src/utils/ui.js +0 -121
  360. package/dist/src/utils/ui.js.map +0 -1
  361. package/scripts/night-watch-audit-cron.sh +0 -149
  362. package/scripts/night-watch-cron.sh +0 -484
  363. package/scripts/night-watch-helpers.sh +0 -499
  364. package/scripts/night-watch-pr-reviewer-cron.sh +0 -528
  365. package/scripts/night-watch-qa-cron.sh +0 -281
  366. package/scripts/night-watch-slicer-cron.sh +0 -90
  367. package/scripts/test-helpers.bats +0 -77
  368. package/templates/night-watch-pr-reviewer.md +0 -174
  369. package/templates/night-watch-qa.md +0 -157
  370. package/templates/night-watch-slicer.md +0 -219
  371. package/templates/night-watch.config.json +0 -30
  372. package/templates/night-watch.md +0 -94
  373. package/templates/prd-executor.md +0 -235
  374. package/templates/prd.md +0 -26
  375. package/web/dist/assets/index-BiJf9LFT.js +0 -458
  376. package/web/dist/assets/index-OpSgvsYu.css +0 -1
  377. package/web/dist/avatars/carlos.webp +0 -0
  378. package/web/dist/avatars/dev.webp +0 -0
  379. package/web/dist/avatars/maya.webp +0 -0
  380. package/web/dist/avatars/priya.webp +0 -0
  381. package/web/dist/index.html +0 -82
  382. /package/dist/{src/commands → commands}/dashboard/tab-actions.d.ts +0 -0
  383. /package/dist/{src/commands → commands}/dashboard/tab-actions.js +0 -0
  384. /package/dist/{src/commands → commands}/dashboard/types.js +0 -0
@@ -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