@lbruton/specflow 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (414) hide show
  1. package/CHANGELOG.md +955 -0
  2. package/LICENSE +674 -0
  3. package/README.ar.md +314 -0
  4. package/README.de.md +314 -0
  5. package/README.es.md +314 -0
  6. package/README.fr.md +314 -0
  7. package/README.it.md +314 -0
  8. package/README.ja.md +316 -0
  9. package/README.ko.md +314 -0
  10. package/README.md +239 -0
  11. package/README.pt.md +314 -0
  12. package/README.ru.md +314 -0
  13. package/README.zh.md +314 -0
  14. package/dist/__tests__/config.test.d.ts +2 -0
  15. package/dist/__tests__/config.test.d.ts.map +1 -0
  16. package/dist/__tests__/config.test.js +264 -0
  17. package/dist/__tests__/config.test.js.map +1 -0
  18. package/dist/__tests__/index-args.test.d.ts +2 -0
  19. package/dist/__tests__/index-args.test.d.ts.map +1 -0
  20. package/dist/__tests__/index-args.test.js +43 -0
  21. package/dist/__tests__/index-args.test.js.map +1 -0
  22. package/dist/__tests__/index-entrypoint.test.d.ts +2 -0
  23. package/dist/__tests__/index-entrypoint.test.d.ts.map +1 -0
  24. package/dist/__tests__/index-entrypoint.test.js +23 -0
  25. package/dist/__tests__/index-entrypoint.test.js.map +1 -0
  26. package/dist/config.d.ts +26 -0
  27. package/dist/config.d.ts.map +1 -0
  28. package/dist/config.js +188 -0
  29. package/dist/config.js.map +1 -0
  30. package/dist/core/__tests__/git-utils.test.d.ts +2 -0
  31. package/dist/core/__tests__/git-utils.test.d.ts.map +1 -0
  32. package/dist/core/__tests__/git-utils.test.js +179 -0
  33. package/dist/core/__tests__/git-utils.test.js.map +1 -0
  34. package/dist/core/__tests__/mdx-validator.test.d.ts +2 -0
  35. package/dist/core/__tests__/mdx-validator.test.d.ts.map +1 -0
  36. package/dist/core/__tests__/mdx-validator.test.js +42 -0
  37. package/dist/core/__tests__/mdx-validator.test.js.map +1 -0
  38. package/dist/core/__tests__/path-utils.test.d.ts +2 -0
  39. package/dist/core/__tests__/path-utils.test.d.ts.map +1 -0
  40. package/dist/core/__tests__/path-utils.test.js +344 -0
  41. package/dist/core/__tests__/path-utils.test.js.map +1 -0
  42. package/dist/core/__tests__/project-registry.test.d.ts +2 -0
  43. package/dist/core/__tests__/project-registry.test.d.ts.map +1 -0
  44. package/dist/core/__tests__/project-registry.test.js +62 -0
  45. package/dist/core/__tests__/project-registry.test.js.map +1 -0
  46. package/dist/core/__tests__/security-utils.test.d.ts +2 -0
  47. package/dist/core/__tests__/security-utils.test.d.ts.map +1 -0
  48. package/dist/core/__tests__/security-utils.test.js +643 -0
  49. package/dist/core/__tests__/security-utils.test.js.map +1 -0
  50. package/dist/core/__tests__/task-validator.test.d.ts +2 -0
  51. package/dist/core/__tests__/task-validator.test.d.ts.map +1 -0
  52. package/dist/core/__tests__/task-validator.test.js +237 -0
  53. package/dist/core/__tests__/task-validator.test.js.map +1 -0
  54. package/dist/core/archive-service.d.ts +10 -0
  55. package/dist/core/archive-service.d.ts.map +1 -0
  56. package/dist/core/archive-service.js +99 -0
  57. package/dist/core/archive-service.js.map +1 -0
  58. package/dist/core/dashboard-session.d.ts +49 -0
  59. package/dist/core/dashboard-session.d.ts.map +1 -0
  60. package/dist/core/dashboard-session.js +132 -0
  61. package/dist/core/dashboard-session.js.map +1 -0
  62. package/dist/core/git-utils.d.ts +25 -0
  63. package/dist/core/git-utils.d.ts.map +1 -0
  64. package/dist/core/git-utils.js +87 -0
  65. package/dist/core/git-utils.js.map +1 -0
  66. package/dist/core/global-dir.d.ts +44 -0
  67. package/dist/core/global-dir.d.ts.map +1 -0
  68. package/dist/core/global-dir.js +74 -0
  69. package/dist/core/global-dir.js.map +1 -0
  70. package/dist/core/implementation-log-migrator.d.ts +41 -0
  71. package/dist/core/implementation-log-migrator.d.ts.map +1 -0
  72. package/dist/core/implementation-log-migrator.js +258 -0
  73. package/dist/core/implementation-log-migrator.js.map +1 -0
  74. package/dist/core/mdx-validator.d.ts +14 -0
  75. package/dist/core/mdx-validator.d.ts.map +1 -0
  76. package/dist/core/mdx-validator.js +34 -0
  77. package/dist/core/mdx-validator.js.map +1 -0
  78. package/dist/core/parser.d.ts +12 -0
  79. package/dist/core/parser.d.ts.map +1 -0
  80. package/dist/core/parser.js +144 -0
  81. package/dist/core/parser.js.map +1 -0
  82. package/dist/core/path-utils.d.ts +63 -0
  83. package/dist/core/path-utils.d.ts.map +1 -0
  84. package/dist/core/path-utils.js +288 -0
  85. package/dist/core/path-utils.js.map +1 -0
  86. package/dist/core/project-registry.d.ts +94 -0
  87. package/dist/core/project-registry.d.ts.map +1 -0
  88. package/dist/core/project-registry.js +293 -0
  89. package/dist/core/project-registry.js.map +1 -0
  90. package/dist/core/security-utils.d.ts +97 -0
  91. package/dist/core/security-utils.d.ts.map +1 -0
  92. package/dist/core/security-utils.js +264 -0
  93. package/dist/core/security-utils.js.map +1 -0
  94. package/dist/core/task-parser.d.ts +63 -0
  95. package/dist/core/task-parser.d.ts.map +1 -0
  96. package/dist/core/task-parser.js +332 -0
  97. package/dist/core/task-parser.js.map +1 -0
  98. package/dist/core/task-validator.d.ts +35 -0
  99. package/dist/core/task-validator.d.ts.map +1 -0
  100. package/dist/core/task-validator.js +236 -0
  101. package/dist/core/task-validator.js.map +1 -0
  102. package/dist/core/workspace-initializer.d.ts +16 -0
  103. package/dist/core/workspace-initializer.d.ts.map +1 -0
  104. package/dist/core/workspace-initializer.js +168 -0
  105. package/dist/core/workspace-initializer.js.map +1 -0
  106. package/dist/dashboard/__tests__/approval-storage-path-resolution.test.d.ts +2 -0
  107. package/dist/dashboard/__tests__/approval-storage-path-resolution.test.d.ts.map +1 -0
  108. package/dist/dashboard/__tests__/approval-storage-path-resolution.test.js +69 -0
  109. package/dist/dashboard/__tests__/approval-storage-path-resolution.test.js.map +1 -0
  110. package/dist/dashboard/__tests__/multi-server-approvals-content.test.d.ts +2 -0
  111. package/dist/dashboard/__tests__/multi-server-approvals-content.test.d.ts.map +1 -0
  112. package/dist/dashboard/__tests__/multi-server-approvals-content.test.js +116 -0
  113. package/dist/dashboard/__tests__/multi-server-approvals-content.test.js.map +1 -0
  114. package/dist/dashboard/__tests__/watcher-error-handling.test.d.ts +2 -0
  115. package/dist/dashboard/__tests__/watcher-error-handling.test.d.ts.map +1 -0
  116. package/dist/dashboard/__tests__/watcher-error-handling.test.js +118 -0
  117. package/dist/dashboard/__tests__/watcher-error-handling.test.js.map +1 -0
  118. package/dist/dashboard/approval-storage.d.ts +139 -0
  119. package/dist/dashboard/approval-storage.d.ts.map +1 -0
  120. package/dist/dashboard/approval-storage.js +586 -0
  121. package/dist/dashboard/approval-storage.js.map +1 -0
  122. package/dist/dashboard/execution-history-manager.d.ts +52 -0
  123. package/dist/dashboard/execution-history-manager.d.ts.map +1 -0
  124. package/dist/dashboard/execution-history-manager.js +161 -0
  125. package/dist/dashboard/execution-history-manager.js.map +1 -0
  126. package/dist/dashboard/implementation-log-manager.d.ts +97 -0
  127. package/dist/dashboard/implementation-log-manager.d.ts.map +1 -0
  128. package/dist/dashboard/implementation-log-manager.js +625 -0
  129. package/dist/dashboard/implementation-log-manager.js.map +1 -0
  130. package/dist/dashboard/job-scheduler.d.ts +91 -0
  131. package/dist/dashboard/job-scheduler.d.ts.map +1 -0
  132. package/dist/dashboard/job-scheduler.js +321 -0
  133. package/dist/dashboard/job-scheduler.js.map +1 -0
  134. package/dist/dashboard/multi-server.d.ts +42 -0
  135. package/dist/dashboard/multi-server.d.ts.map +1 -0
  136. package/dist/dashboard/multi-server.js +1304 -0
  137. package/dist/dashboard/multi-server.js.map +1 -0
  138. package/dist/dashboard/parser.d.ts +19 -0
  139. package/dist/dashboard/parser.d.ts.map +1 -0
  140. package/dist/dashboard/parser.js +290 -0
  141. package/dist/dashboard/parser.js.map +1 -0
  142. package/dist/dashboard/project-manager.d.ts +82 -0
  143. package/dist/dashboard/project-manager.d.ts.map +1 -0
  144. package/dist/dashboard/project-manager.js +257 -0
  145. package/dist/dashboard/project-manager.js.map +1 -0
  146. package/dist/dashboard/public/assets/Inter-Bold-CD3Pr7BX.woff2 +0 -0
  147. package/dist/dashboard/public/assets/Inter-Medium-B_8v_WHh.woff2 +0 -0
  148. package/dist/dashboard/public/assets/Inter-Regular-DRVdRqcI.woff2 +0 -0
  149. package/dist/dashboard/public/assets/Inter-SemiBold-CtskMddL.woff2 +0 -0
  150. package/dist/dashboard/public/assets/JetBrainsMono-Bold-D4WEaHbo.woff2 +0 -0
  151. package/dist/dashboard/public/assets/JetBrainsMono-Medium-3S3k2nMz.woff2 +0 -0
  152. package/dist/dashboard/public/assets/JetBrainsMono-Regular-BQaDgvhP.woff2 +0 -0
  153. package/dist/dashboard/public/assets/Tableau10-B-NsZVaP.js +1 -0
  154. package/dist/dashboard/public/assets/apl-B4CMkyY2.js +1 -0
  155. package/dist/dashboard/public/assets/arc-CtYZ6WTd.js +1 -0
  156. package/dist/dashboard/public/assets/array-BKyUJesY.js +1 -0
  157. package/dist/dashboard/public/assets/asciiarmor-Df11BRmG.js +1 -0
  158. package/dist/dashboard/public/assets/asn1-EdZsLKOL.js +1 -0
  159. package/dist/dashboard/public/assets/asterisk-B-8jnY81.js +1 -0
  160. package/dist/dashboard/public/assets/blockDiagram-c4efeb88-CzEvDZI5.js +118 -0
  161. package/dist/dashboard/public/assets/brainfuck-C4LP7Hcl.js +1 -0
  162. package/dist/dashboard/public/assets/c4Diagram-c83219d4-CuPd1dkK.js +10 -0
  163. package/dist/dashboard/public/assets/channel-kGGkJUti.js +1 -0
  164. package/dist/dashboard/public/assets/classDiagram-beda092f-CDTLUu9K.js +2 -0
  165. package/dist/dashboard/public/assets/classDiagram-v2-2358418a-BlHqrcgN.js +2 -0
  166. package/dist/dashboard/public/assets/clike-B9uivgTg.js +1 -0
  167. package/dist/dashboard/public/assets/clojure-BMjYHr_A.js +1 -0
  168. package/dist/dashboard/public/assets/clone-CHdfA33T.js +1 -0
  169. package/dist/dashboard/public/assets/cmake-BQqOBYOt.js +1 -0
  170. package/dist/dashboard/public/assets/cobol-CWcv1MsR.js +1 -0
  171. package/dist/dashboard/public/assets/coffeescript-S37ZYGWr.js +1 -0
  172. package/dist/dashboard/public/assets/commonlisp-DBKNyK5s.js +1 -0
  173. package/dist/dashboard/public/assets/createText-1719965b-B3cTooQ1.js +7 -0
  174. package/dist/dashboard/public/assets/crystal-SjHAIU92.js +1 -0
  175. package/dist/dashboard/public/assets/css-BnMrqG3P.js +1 -0
  176. package/dist/dashboard/public/assets/cypher-C_CwsFkJ.js +1 -0
  177. package/dist/dashboard/public/assets/d-pRatUO7H.js +1 -0
  178. package/dist/dashboard/public/assets/diff-DbItnlRl.js +1 -0
  179. package/dist/dashboard/public/assets/dockerfile-BKs6k2Af.js +1 -0
  180. package/dist/dashboard/public/assets/dtd-DF_7sFjM.js +1 -0
  181. package/dist/dashboard/public/assets/dylan-DwRh75JA.js +1 -0
  182. package/dist/dashboard/public/assets/ebnf-CDyGwa7X.js +1 -0
  183. package/dist/dashboard/public/assets/ecl-Cabwm37j.js +1 -0
  184. package/dist/dashboard/public/assets/edges-96097737-CbHatH6o.js +4 -0
  185. package/dist/dashboard/public/assets/eiffel-CnydiIhH.js +1 -0
  186. package/dist/dashboard/public/assets/elm-vLlmbW-K.js +1 -0
  187. package/dist/dashboard/public/assets/erDiagram-0228fc6a-MYCicRH6.js +51 -0
  188. package/dist/dashboard/public/assets/erlang-BNw1qcRV.js +1 -0
  189. package/dist/dashboard/public/assets/factor-kuTfRLto.js +1 -0
  190. package/dist/dashboard/public/assets/fcl-Kvtd6kyn.js +1 -0
  191. package/dist/dashboard/public/assets/flowDb-c6c81e3f-DDm9aBGx.js +10 -0
  192. package/dist/dashboard/public/assets/flowDiagram-50d868cf-zb-JtJG0.js +4 -0
  193. package/dist/dashboard/public/assets/flowDiagram-v2-4f6560a1-DRD7GAtr.js +1 -0
  194. package/dist/dashboard/public/assets/flowchart-elk-definition-6af322e1-D5TGPnG-.js +139 -0
  195. package/dist/dashboard/public/assets/forth-Ffai-XNe.js +1 -0
  196. package/dist/dashboard/public/assets/fortran-DYz_wnZ1.js +1 -0
  197. package/dist/dashboard/public/assets/ganttDiagram-a2739b55-Cqst4HhR.js +257 -0
  198. package/dist/dashboard/public/assets/gas-Bneqetm1.js +1 -0
  199. package/dist/dashboard/public/assets/gherkin-heZmZLOM.js +1 -0
  200. package/dist/dashboard/public/assets/gitGraphDiagram-82fe8481-C8JWqtTx.js +70 -0
  201. package/dist/dashboard/public/assets/graph-Lpyd8dqZ.js +1 -0
  202. package/dist/dashboard/public/assets/groovy-D9Dt4D0W.js +1 -0
  203. package/dist/dashboard/public/assets/haskell-Cw1EW3IL.js +1 -0
  204. package/dist/dashboard/public/assets/haxe-H-WmDvRZ.js +1 -0
  205. package/dist/dashboard/public/assets/http-DBlCnlav.js +1 -0
  206. package/dist/dashboard/public/assets/idl-BEugSyMb.js +1 -0
  207. package/dist/dashboard/public/assets/index--_-9KDkB.js +1 -0
  208. package/dist/dashboard/public/assets/index-5325376f-Ngqm3jj1.js +1 -0
  209. package/dist/dashboard/public/assets/index-7POr4IQt.js +1 -0
  210. package/dist/dashboard/public/assets/index-B2wloU4M.js +7 -0
  211. package/dist/dashboard/public/assets/index-BB9i8dPx.js +329 -0
  212. package/dist/dashboard/public/assets/index-BHCmvfMN.js +1 -0
  213. package/dist/dashboard/public/assets/index-BWvyG2sq.js +1 -0
  214. package/dist/dashboard/public/assets/index-BhwV70vC.js +2 -0
  215. package/dist/dashboard/public/assets/index-Bqk8CAAy.js +1 -0
  216. package/dist/dashboard/public/assets/index-CUP35nar.js +1 -0
  217. package/dist/dashboard/public/assets/index-CYy3mVaI.js +1 -0
  218. package/dist/dashboard/public/assets/index-Cqbzr5qD.js +3 -0
  219. package/dist/dashboard/public/assets/index-Cv_FT1Rp.css +1 -0
  220. package/dist/dashboard/public/assets/index-D4J257wx.js +1 -0
  221. package/dist/dashboard/public/assets/index-D9fDAa7f.js +1 -0
  222. package/dist/dashboard/public/assets/index-DFN4bEYo.js +1 -0
  223. package/dist/dashboard/public/assets/index-DQ2Aap_e.js +1 -0
  224. package/dist/dashboard/public/assets/index-Djo38KZb.js +1 -0
  225. package/dist/dashboard/public/assets/index-RZXsuDxo.js +1 -0
  226. package/dist/dashboard/public/assets/index-m5SRqxVX.js +1 -0
  227. package/dist/dashboard/public/assets/infoDiagram-8eee0895-DYMb32US.js +7 -0
  228. package/dist/dashboard/public/assets/init-Gi6I4Gst.js +1 -0
  229. package/dist/dashboard/public/assets/javascript-iXu5QeM3.js +1 -0
  230. package/dist/dashboard/public/assets/journeyDiagram-c64418c1-BgL0JuAb.js +139 -0
  231. package/dist/dashboard/public/assets/julia-DuME0IfC.js +1 -0
  232. package/dist/dashboard/public/assets/katex-XbL3y5x-.js +261 -0
  233. package/dist/dashboard/public/assets/layout-DP9fJAz2.js +1 -0
  234. package/dist/dashboard/public/assets/line-N_NGiB5u.js +1 -0
  235. package/dist/dashboard/public/assets/linear-B1Bl-FQE.js +1 -0
  236. package/dist/dashboard/public/assets/livescript-BwQOo05w.js +1 -0
  237. package/dist/dashboard/public/assets/lua-BgMRiT3U.js +1 -0
  238. package/dist/dashboard/public/assets/mathematica-DTrFuWx2.js +1 -0
  239. package/dist/dashboard/public/assets/mbox-CNhZ1qSd.js +1 -0
  240. package/dist/dashboard/public/assets/mindmap-definition-8da855dc-DuejFf1j.js +415 -0
  241. package/dist/dashboard/public/assets/mirc-CjQqDB4T.js +1 -0
  242. package/dist/dashboard/public/assets/mllike-CXdrOF99.js +1 -0
  243. package/dist/dashboard/public/assets/modelica-Dc1JOy9r.js +1 -0
  244. package/dist/dashboard/public/assets/mscgen-BA5vi2Kp.js +1 -0
  245. package/dist/dashboard/public/assets/mumps-BT43cFF4.js +1 -0
  246. package/dist/dashboard/public/assets/nginx-DdIZxoE0.js +1 -0
  247. package/dist/dashboard/public/assets/nsis-LdVXkNf5.js +1 -0
  248. package/dist/dashboard/public/assets/ntriples-BfvgReVJ.js +1 -0
  249. package/dist/dashboard/public/assets/octave-Ck1zUtKM.js +1 -0
  250. package/dist/dashboard/public/assets/ordinal-Cboi1Yqb.js +1 -0
  251. package/dist/dashboard/public/assets/oz-BzwKVEFT.js +1 -0
  252. package/dist/dashboard/public/assets/pascal--L3eBynH.js +1 -0
  253. package/dist/dashboard/public/assets/path-CbwjOpE9.js +1 -0
  254. package/dist/dashboard/public/assets/perl-CdXCOZ3F.js +1 -0
  255. package/dist/dashboard/public/assets/pieDiagram-a8764435-fJ2uKW1i.js +35 -0
  256. package/dist/dashboard/public/assets/pig-CevX1Tat.js +1 -0
  257. package/dist/dashboard/public/assets/powershell-CFHJl5sT.js +1 -0
  258. package/dist/dashboard/public/assets/properties-C78fOPTZ.js +1 -0
  259. package/dist/dashboard/public/assets/protobuf-ChK-085T.js +1 -0
  260. package/dist/dashboard/public/assets/pug-DeIclll2.js +1 -0
  261. package/dist/dashboard/public/assets/puppet-DMA9R1ak.js +1 -0
  262. package/dist/dashboard/public/assets/python-BuPzkPfP.js +1 -0
  263. package/dist/dashboard/public/assets/q-pXgVlZs6.js +1 -0
  264. package/dist/dashboard/public/assets/quadrantDiagram-1e28029f-Dowa1iq5.js +7 -0
  265. package/dist/dashboard/public/assets/r-B6wPVr8A.js +1 -0
  266. package/dist/dashboard/public/assets/requirementDiagram-08caed73-1PKeJnUu.js +52 -0
  267. package/dist/dashboard/public/assets/rpm-CTu-6PCP.js +1 -0
  268. package/dist/dashboard/public/assets/ruby-B2Rjki9n.js +1 -0
  269. package/dist/dashboard/public/assets/sankeyDiagram-a04cb91d-C3Nu91E8.js +8 -0
  270. package/dist/dashboard/public/assets/sas-B4kiWyti.js +1 -0
  271. package/dist/dashboard/public/assets/scheme-C41bIUwD.js +1 -0
  272. package/dist/dashboard/public/assets/sequenceDiagram-c5b8d532-BbyYeaFb.js +122 -0
  273. package/dist/dashboard/public/assets/shell-CjFT_Tl9.js +1 -0
  274. package/dist/dashboard/public/assets/sieve-C3Gn_uJK.js +1 -0
  275. package/dist/dashboard/public/assets/simple-mode-GW_nhZxv.js +1 -0
  276. package/dist/dashboard/public/assets/smalltalk-CnHTOXQT.js +1 -0
  277. package/dist/dashboard/public/assets/solr-DehyRSwq.js +1 -0
  278. package/dist/dashboard/public/assets/sparql-DkYu6x3z.js +1 -0
  279. package/dist/dashboard/public/assets/spreadsheet-BCZA_wO0.js +1 -0
  280. package/dist/dashboard/public/assets/sql-D0XecflT.js +1 -0
  281. package/dist/dashboard/public/assets/stateDiagram-1ecb1508-BVkb4gIE.js +1 -0
  282. package/dist/dashboard/public/assets/stateDiagram-v2-c2b004d7-Bm7qug6Q.js +1 -0
  283. package/dist/dashboard/public/assets/stex-C3f8Ysf7.js +1 -0
  284. package/dist/dashboard/public/assets/styles-b4e223ce-BuqydlKk.js +160 -0
  285. package/dist/dashboard/public/assets/styles-ca3715f6-C8L7peEM.js +207 -0
  286. package/dist/dashboard/public/assets/styles-d45a18b0-BbrCCPkQ.js +116 -0
  287. package/dist/dashboard/public/assets/stylus-B533Al4x.js +1 -0
  288. package/dist/dashboard/public/assets/svgDrawCommon-b86b1483-DQZAHvSt.js +1 -0
  289. package/dist/dashboard/public/assets/swift-BzpIVaGY.js +1 -0
  290. package/dist/dashboard/public/assets/tcl-DVfN8rqt.js +1 -0
  291. package/dist/dashboard/public/assets/textile-CnDTJFAw.js +1 -0
  292. package/dist/dashboard/public/assets/tiddlywiki-DO-Gjzrf.js +1 -0
  293. package/dist/dashboard/public/assets/tiki-DGYXhP31.js +1 -0
  294. package/dist/dashboard/public/assets/timeline-definition-faaaa080-YPoIuE--.js +61 -0
  295. package/dist/dashboard/public/assets/toml-Bm5Em-hy.js +1 -0
  296. package/dist/dashboard/public/assets/troff-wAsdV37c.js +1 -0
  297. package/dist/dashboard/public/assets/ttcn-CfJYG6tj.js +1 -0
  298. package/dist/dashboard/public/assets/ttcn-cfg-B9xdYoR4.js +1 -0
  299. package/dist/dashboard/public/assets/turtle-B1tBg_DP.js +1 -0
  300. package/dist/dashboard/public/assets/vb-CmGdzxic.js +1 -0
  301. package/dist/dashboard/public/assets/vbscript-BuJXcnF6.js +1 -0
  302. package/dist/dashboard/public/assets/velocity-D8B20fx6.js +1 -0
  303. package/dist/dashboard/public/assets/verilog-C6RDOZhf.js +1 -0
  304. package/dist/dashboard/public/assets/vhdl-lSbBsy5d.js +1 -0
  305. package/dist/dashboard/public/assets/webidl-ZXfAyPTL.js +1 -0
  306. package/dist/dashboard/public/assets/xquery-DzFWVndE.js +1 -0
  307. package/dist/dashboard/public/assets/xychartDiagram-f5964ef8-BnI4pVWV.js +7 -0
  308. package/dist/dashboard/public/assets/yacas-BJ4BC0dw.js +1 -0
  309. package/dist/dashboard/public/assets/z80-Hz9HOZM7.js +1 -0
  310. package/dist/dashboard/public/claude-icon-dark.svg +1 -0
  311. package/dist/dashboard/public/claude-icon.svg +1 -0
  312. package/dist/dashboard/public/index.html +16 -0
  313. package/dist/dashboard/settings-manager.d.ts +47 -0
  314. package/dist/dashboard/settings-manager.d.ts.map +1 -0
  315. package/dist/dashboard/settings-manager.js +180 -0
  316. package/dist/dashboard/settings-manager.js.map +1 -0
  317. package/dist/dashboard/utils.d.ts +31 -0
  318. package/dist/dashboard/utils.d.ts.map +1 -0
  319. package/dist/dashboard/utils.js +102 -0
  320. package/dist/dashboard/utils.js.map +1 -0
  321. package/dist/dashboard/watcher.d.ts +32 -0
  322. package/dist/dashboard/watcher.d.ts.map +1 -0
  323. package/dist/dashboard/watcher.js +173 -0
  324. package/dist/dashboard/watcher.js.map +1 -0
  325. package/dist/index.d.ts +13 -0
  326. package/dist/index.d.ts.map +1 -0
  327. package/dist/index.js +380 -0
  328. package/dist/index.js.map +1 -0
  329. package/dist/markdown/templates/code-quality-reviewer-template.md +76 -0
  330. package/dist/markdown/templates/design-template.md +141 -0
  331. package/dist/markdown/templates/implementer-prompt-template.md +122 -0
  332. package/dist/markdown/templates/product-template.md +51 -0
  333. package/dist/markdown/templates/requirements-template.md +73 -0
  334. package/dist/markdown/templates/spec-reviewer-template.md +58 -0
  335. package/dist/markdown/templates/structure-template.md +145 -0
  336. package/dist/markdown/templates/tasks-template.md +182 -0
  337. package/dist/markdown/templates/tech-template.md +99 -0
  338. package/dist/prompts/create-spec.d.ts +3 -0
  339. package/dist/prompts/create-spec.d.ts.map +1 -0
  340. package/dist/prompts/create-spec.js +93 -0
  341. package/dist/prompts/create-spec.js.map +1 -0
  342. package/dist/prompts/create-steering-doc.d.ts +3 -0
  343. package/dist/prompts/create-steering-doc.d.ts.map +1 -0
  344. package/dist/prompts/create-steering-doc.js +73 -0
  345. package/dist/prompts/create-steering-doc.js.map +1 -0
  346. package/dist/prompts/implement-task.d.ts +3 -0
  347. package/dist/prompts/implement-task.d.ts.map +1 -0
  348. package/dist/prompts/implement-task.js +189 -0
  349. package/dist/prompts/implement-task.js.map +1 -0
  350. package/dist/prompts/index.d.ts +15 -0
  351. package/dist/prompts/index.d.ts.map +1 -0
  352. package/dist/prompts/index.js +49 -0
  353. package/dist/prompts/index.js.map +1 -0
  354. package/dist/prompts/inject-spec-workflow-guide.d.ts +3 -0
  355. package/dist/prompts/inject-spec-workflow-guide.d.ts.map +1 -0
  356. package/dist/prompts/inject-spec-workflow-guide.js +47 -0
  357. package/dist/prompts/inject-spec-workflow-guide.js.map +1 -0
  358. package/dist/prompts/inject-steering-guide.d.ts +3 -0
  359. package/dist/prompts/inject-steering-guide.d.ts.map +1 -0
  360. package/dist/prompts/inject-steering-guide.js +51 -0
  361. package/dist/prompts/inject-steering-guide.js.map +1 -0
  362. package/dist/prompts/refresh-tasks.d.ts +3 -0
  363. package/dist/prompts/refresh-tasks.d.ts.map +1 -0
  364. package/dist/prompts/refresh-tasks.js +224 -0
  365. package/dist/prompts/refresh-tasks.js.map +1 -0
  366. package/dist/prompts/spec-status.d.ts +3 -0
  367. package/dist/prompts/spec-status.d.ts.map +1 -0
  368. package/dist/prompts/spec-status.js +75 -0
  369. package/dist/prompts/spec-status.js.map +1 -0
  370. package/dist/prompts/types.d.ts +13 -0
  371. package/dist/prompts/types.d.ts.map +1 -0
  372. package/dist/prompts/types.js +2 -0
  373. package/dist/prompts/types.js.map +1 -0
  374. package/dist/server.d.ts +17 -0
  375. package/dist/server.d.ts.map +1 -0
  376. package/dist/server.js +175 -0
  377. package/dist/server.js.map +1 -0
  378. package/dist/tools/__tests__/projectPath.test.d.ts +2 -0
  379. package/dist/tools/__tests__/projectPath.test.d.ts.map +1 -0
  380. package/dist/tools/__tests__/projectPath.test.js +187 -0
  381. package/dist/tools/__tests__/projectPath.test.js.map +1 -0
  382. package/dist/tools/approvals.d.ts +14 -0
  383. package/dist/tools/approvals.d.ts.map +1 -0
  384. package/dist/tools/approvals.js +501 -0
  385. package/dist/tools/approvals.js.map +1 -0
  386. package/dist/tools/index.d.ts +5 -0
  387. package/dist/tools/index.d.ts.map +1 -0
  388. package/dist/tools/index.js +57 -0
  389. package/dist/tools/index.js.map +1 -0
  390. package/dist/tools/log-implementation.d.ts +5 -0
  391. package/dist/tools/log-implementation.d.ts.map +1 -0
  392. package/dist/tools/log-implementation.js +446 -0
  393. package/dist/tools/log-implementation.js.map +1 -0
  394. package/dist/tools/spec-list.d.ts +5 -0
  395. package/dist/tools/spec-list.d.ts.map +1 -0
  396. package/dist/tools/spec-list.js +144 -0
  397. package/dist/tools/spec-list.js.map +1 -0
  398. package/dist/tools/spec-status.d.ts +5 -0
  399. package/dist/tools/spec-status.d.ts.map +1 -0
  400. package/dist/tools/spec-status.js +238 -0
  401. package/dist/tools/spec-status.js.map +1 -0
  402. package/dist/tools/spec-workflow-guide.d.ts +5 -0
  403. package/dist/tools/spec-workflow-guide.d.ts.map +1 -0
  404. package/dist/tools/spec-workflow-guide.js +580 -0
  405. package/dist/tools/spec-workflow-guide.js.map +1 -0
  406. package/dist/tools/steering-guide.d.ts +5 -0
  407. package/dist/tools/steering-guide.d.ts.map +1 -0
  408. package/dist/tools/steering-guide.js +192 -0
  409. package/dist/tools/steering-guide.js.map +1 -0
  410. package/dist/types.d.ts +186 -0
  411. package/dist/types.d.ts.map +1 -0
  412. package/dist/types.js +13 -0
  413. package/dist/types.js.map +1 -0
  414. package/package.json +105 -0
@@ -0,0 +1,1304 @@
1
+ import fastify from 'fastify';
2
+ import fastifyStatic from '@fastify/static';
3
+ import fastifyWebsocket from '@fastify/websocket';
4
+ import fastifyCors from '@fastify/cors';
5
+ import { join, dirname } from 'path';
6
+ import { readFile } from 'fs/promises';
7
+ import { promises as fs } from 'fs';
8
+ import { fileURLToPath } from 'url';
9
+ import open from 'open';
10
+ import { WebSocket } from 'ws';
11
+ import { validateAndCheckPort, DASHBOARD_TEST_MESSAGE } from './utils.js';
12
+ import { parseTasksFromMarkdown } from '../core/task-parser.js';
13
+ import { ProjectManager } from './project-manager.js';
14
+ import { JobScheduler } from './job-scheduler.js';
15
+ import { ImplementationLogManager } from './implementation-log-manager.js';
16
+ import { DashboardSessionManager } from '../core/dashboard-session.js';
17
+ import { getSecurityConfig, RateLimiter, AuditLogger, createSecurityHeadersMiddleware, getCorsConfig, isLocalhostAddress } from '../core/security-utils.js';
18
+ const __filename = fileURLToPath(import.meta.url);
19
+ const __dirname = dirname(__filename);
20
+ export class MultiProjectDashboardServer {
21
+ app;
22
+ projectManager;
23
+ jobScheduler;
24
+ sessionManager;
25
+ options;
26
+ bindAddress;
27
+ allowExternalAccess;
28
+ securityConfig;
29
+ rateLimiter;
30
+ auditLogger;
31
+ actualPort = 0;
32
+ clients = new Set();
33
+ packageVersion = 'unknown';
34
+ heartbeatInterval;
35
+ HEARTBEAT_INTERVAL_MS = 30000;
36
+ HEARTBEAT_TIMEOUT_MS = 10000;
37
+ // Debounce spec broadcasts to coalesce rapid updates
38
+ pendingSpecBroadcasts = new Map();
39
+ SPEC_BROADCAST_DEBOUNCE_MS = 300;
40
+ constructor(options = {}) {
41
+ this.options = options;
42
+ this.projectManager = new ProjectManager();
43
+ this.jobScheduler = new JobScheduler(this.projectManager);
44
+ this.sessionManager = new DashboardSessionManager();
45
+ // Initialize network binding configuration
46
+ this.bindAddress = options.bindAddress || '127.0.0.1';
47
+ this.allowExternalAccess = options.allowExternalAccess || false;
48
+ // Validate network binding security
49
+ if (!isLocalhostAddress(this.bindAddress) && !this.allowExternalAccess) {
50
+ throw new Error(`SECURITY ERROR: Binding to '${this.bindAddress}' (non-localhost) requires explicit allowExternalAccess=true. ` +
51
+ 'This exposes your dashboard to network access. Use 127.0.0.1 for localhost-only access.');
52
+ }
53
+ // Initialize security features configuration with the actual port
54
+ // This ensures CORS allowedOrigins and CSP are port-aware
55
+ this.securityConfig = getSecurityConfig(options.security, options.port);
56
+ this.app = fastify({ logger: false });
57
+ }
58
+ async start() {
59
+ // Security warning if binding to non-localhost address
60
+ if (!isLocalhostAddress(this.bindAddress)) {
61
+ console.error('');
62
+ console.error('⚠️ ═══════════════════════════════════════════════════════════');
63
+ console.error(`⚠️ SECURITY WARNING: Dashboard binding to ${this.bindAddress}`);
64
+ console.error('⚠️ This exposes your dashboard to network-based attacks!');
65
+ console.error('⚠️ Recommendation: Use 127.0.0.1 for localhost-only access');
66
+ console.error('⚠️ ═══════════════════════════════════════════════════════════');
67
+ console.error('');
68
+ }
69
+ // Display security status
70
+ console.error('🔒 Security Configuration:');
71
+ console.error(` - Bind Address: ${this.bindAddress}`);
72
+ console.error(` - Rate Limiting: ${this.securityConfig.rateLimitEnabled ? 'ENABLED ✓' : 'DISABLED ⚠️'}`);
73
+ console.error(` - Audit Logging: ${this.securityConfig.auditLogEnabled ? 'ENABLED ✓' : 'DISABLED ⚠️'}`);
74
+ console.error(` - CORS: ${this.securityConfig.corsEnabled ? 'ENABLED ✓' : 'DISABLED ⚠️'}`);
75
+ console.error(` - Allowed Origins: ${this.securityConfig.allowedOrigins.join(', ')}`);
76
+ console.error('');
77
+ // Read version from local package.json
78
+ try {
79
+ const packageJsonPath = join(__dirname, '..', '..', 'package.json');
80
+ const packageJsonContent = await readFile(packageJsonPath, 'utf-8');
81
+ const packageJson = JSON.parse(packageJsonContent);
82
+ this.packageVersion = packageJson.version || 'unknown';
83
+ }
84
+ catch {
85
+ // Keep default 'unknown' if package.json is unreadable
86
+ }
87
+ // Initialize security components
88
+ if (this.securityConfig.rateLimitEnabled) {
89
+ this.rateLimiter = new RateLimiter(this.securityConfig);
90
+ }
91
+ if (this.securityConfig.auditLogEnabled) {
92
+ this.auditLogger = new AuditLogger(this.securityConfig);
93
+ await this.auditLogger.initialize();
94
+ }
95
+ // Initialize project manager
96
+ await this.projectManager.initialize();
97
+ // Initialize job scheduler
98
+ await this.jobScheduler.initialize();
99
+ // Register CORS plugin if enabled
100
+ const corsConfig = getCorsConfig(this.securityConfig);
101
+ if (corsConfig !== false) {
102
+ await this.app.register(fastifyCors, corsConfig);
103
+ }
104
+ // Register security middleware (apply to all routes)
105
+ // Pass the actual port for CSP connect-src WebSocket configuration
106
+ this.app.addHook('onRequest', createSecurityHeadersMiddleware(this.options.port));
107
+ if (this.rateLimiter) {
108
+ this.app.addHook('onRequest', this.rateLimiter.middleware());
109
+ }
110
+ if (this.auditLogger) {
111
+ this.app.addHook('onRequest', this.auditLogger.middleware());
112
+ }
113
+ // Register plugins
114
+ await this.app.register(fastifyStatic, {
115
+ root: join(__dirname, 'public'),
116
+ prefix: '/',
117
+ });
118
+ await this.app.register(fastifyWebsocket);
119
+ // WebSocket endpoint for real-time updates
120
+ const self = this;
121
+ await this.app.register(async function (fastify) {
122
+ fastify.get('/ws', { websocket: true }, (connection, req) => {
123
+ const socket = connection.socket;
124
+ // Get projectId from query parameter
125
+ const url = new URL(req.url || '', `http://${req.headers.host}`);
126
+ const projectId = url.searchParams.get('projectId') || undefined;
127
+ connection.projectId = projectId;
128
+ connection.isAlive = true;
129
+ self.clients.add(connection);
130
+ // Handle pong for heartbeat
131
+ socket.on('pong', () => {
132
+ connection.isAlive = true;
133
+ });
134
+ // Send initial state for the requested project
135
+ if (projectId) {
136
+ const project = self.projectManager.getProject(projectId);
137
+ if (project) {
138
+ Promise.all([
139
+ project.parser.getAllSpecs(),
140
+ project.approvalStorage.getAllPendingApprovals()
141
+ ])
142
+ .then(([specs, approvals]) => {
143
+ socket.send(JSON.stringify({
144
+ type: 'initial',
145
+ projectId,
146
+ data: { specs, approvals },
147
+ }));
148
+ })
149
+ .catch((error) => {
150
+ console.error('Error getting initial data:', error);
151
+ });
152
+ }
153
+ }
154
+ // Send projects list
155
+ socket.send(JSON.stringify({
156
+ type: 'projects-update',
157
+ data: { projects: self.projectManager.getProjectsList() }
158
+ }));
159
+ // Handle client disconnect
160
+ const cleanup = () => {
161
+ self.clients.delete(connection);
162
+ socket.removeAllListeners();
163
+ };
164
+ socket.on('close', cleanup);
165
+ socket.on('error', cleanup);
166
+ socket.on('disconnect', cleanup);
167
+ socket.on('end', cleanup);
168
+ // Handle subscription messages
169
+ socket.on('message', (data) => {
170
+ try {
171
+ const msg = JSON.parse(data.toString());
172
+ if (msg.type === 'subscribe' && msg.projectId) {
173
+ connection.projectId = msg.projectId;
174
+ // Send initial data for new subscription
175
+ const project = self.projectManager.getProject(msg.projectId);
176
+ if (project) {
177
+ Promise.all([
178
+ project.parser.getAllSpecs(),
179
+ project.approvalStorage.getAllPendingApprovals()
180
+ ])
181
+ .then(([specs, approvals]) => {
182
+ socket.send(JSON.stringify({
183
+ type: 'initial',
184
+ projectId: msg.projectId,
185
+ data: { specs, approvals },
186
+ }));
187
+ })
188
+ .catch((error) => {
189
+ console.error('Error getting initial data:', error);
190
+ });
191
+ }
192
+ }
193
+ }
194
+ catch (error) {
195
+ // Ignore invalid messages
196
+ }
197
+ });
198
+ });
199
+ });
200
+ // Serve Claude icon as favicon
201
+ this.app.get('/favicon.ico', async (request, reply) => {
202
+ return reply.sendFile('claude-icon.svg');
203
+ });
204
+ // Setup project manager event handlers
205
+ this.setupProjectManagerEvents();
206
+ // Register API routes
207
+ this.registerApiRoutes();
208
+ // Validate and set port (always provided by caller)
209
+ if (!this.options.port) {
210
+ throw new Error('Dashboard port must be specified');
211
+ }
212
+ await validateAndCheckPort(this.options.port, this.bindAddress);
213
+ this.actualPort = this.options.port;
214
+ // Start server with configured network binding
215
+ await this.app.listen({
216
+ port: this.actualPort,
217
+ host: this.bindAddress
218
+ });
219
+ // Start WebSocket heartbeat monitoring
220
+ this.startHeartbeat();
221
+ // Register dashboard in the session manager
222
+ const dashboardUrl = `http://localhost:${this.actualPort}`;
223
+ await this.sessionManager.registerDashboard(dashboardUrl, this.actualPort, process.pid);
224
+ // Open browser if requested
225
+ if (this.options.autoOpen) {
226
+ await open(dashboardUrl);
227
+ }
228
+ return dashboardUrl;
229
+ }
230
+ setupProjectManagerEvents() {
231
+ // Broadcast projects update when projects change
232
+ this.projectManager.on('projects-update', (projects) => {
233
+ this.broadcastToAll({
234
+ type: 'projects-update',
235
+ data: { projects }
236
+ });
237
+ });
238
+ // Broadcast spec changes (debounced per project to coalesce rapid updates)
239
+ this.projectManager.on('spec-change', (event) => {
240
+ const { projectId } = event;
241
+ // Clear existing pending broadcast for this project
242
+ const existingTimeout = this.pendingSpecBroadcasts.get(projectId);
243
+ if (existingTimeout) {
244
+ clearTimeout(existingTimeout);
245
+ }
246
+ // Schedule debounced broadcast
247
+ const timeout = setTimeout(async () => {
248
+ this.pendingSpecBroadcasts.delete(projectId);
249
+ try {
250
+ const project = this.projectManager.getProject(projectId);
251
+ if (project) {
252
+ const specs = await project.parser.getAllSpecs();
253
+ const archivedSpecs = await project.parser.getAllArchivedSpecs();
254
+ this.broadcastToProject(projectId, {
255
+ type: 'spec-update',
256
+ projectId,
257
+ data: { specs, archivedSpecs }
258
+ });
259
+ }
260
+ }
261
+ catch (error) {
262
+ console.error('Error broadcasting spec changes:', error);
263
+ // Don't propagate error to prevent event system crash
264
+ }
265
+ }, this.SPEC_BROADCAST_DEBOUNCE_MS);
266
+ this.pendingSpecBroadcasts.set(projectId, timeout);
267
+ });
268
+ // Broadcast task updates
269
+ this.projectManager.on('task-update', (event) => {
270
+ const { projectId, specName } = event;
271
+ this.broadcastTaskUpdate(projectId, specName);
272
+ });
273
+ // Broadcast steering changes
274
+ this.projectManager.on('steering-change', async (event) => {
275
+ try {
276
+ const { projectId, steeringStatus } = event;
277
+ this.broadcastToProject(projectId, {
278
+ type: 'steering-update',
279
+ projectId,
280
+ data: steeringStatus
281
+ });
282
+ }
283
+ catch (error) {
284
+ console.error('Error broadcasting steering changes:', error);
285
+ // Don't propagate error to prevent event system crash
286
+ }
287
+ });
288
+ // Broadcast approval changes
289
+ this.projectManager.on('approval-change', async (event) => {
290
+ try {
291
+ const { projectId } = event;
292
+ const project = this.projectManager.getProject(projectId);
293
+ if (project) {
294
+ const approvals = await project.approvalStorage.getAllPendingApprovals();
295
+ this.broadcastToProject(projectId, {
296
+ type: 'approval-update',
297
+ projectId,
298
+ data: approvals
299
+ });
300
+ }
301
+ }
302
+ catch (error) {
303
+ console.error('Error broadcasting approval changes:', error);
304
+ // Don't propagate error to prevent event system crash
305
+ }
306
+ });
307
+ }
308
+ registerApiRoutes() {
309
+ // Health check / test endpoint (used by utils.ts to detect running dashboard)
310
+ this.app.get('/api/test', async () => {
311
+ return { message: DASHBOARD_TEST_MESSAGE };
312
+ });
313
+ // Projects list
314
+ this.app.get('/api/projects/list', async () => {
315
+ return this.projectManager.getProjectsList();
316
+ });
317
+ // Add project manually
318
+ this.app.post('/api/projects/add', async (request, reply) => {
319
+ const { projectPath } = request.body;
320
+ if (!projectPath) {
321
+ return reply.code(400).send({ error: 'projectPath is required' });
322
+ }
323
+ try {
324
+ const projectId = await this.projectManager.addProjectByPath(projectPath);
325
+ return { projectId, success: true };
326
+ }
327
+ catch (error) {
328
+ return reply.code(500).send({ error: error.message });
329
+ }
330
+ });
331
+ // Remove project
332
+ this.app.delete('/api/projects/:projectId', async (request, reply) => {
333
+ const { projectId } = request.params;
334
+ try {
335
+ await this.projectManager.removeProjectById(projectId);
336
+ return { success: true };
337
+ }
338
+ catch (error) {
339
+ return reply.code(500).send({ error: error.message });
340
+ }
341
+ });
342
+ // Project info
343
+ this.app.get('/api/projects/:projectId/info', async (request, reply) => {
344
+ const { projectId } = request.params;
345
+ const project = this.projectManager.getProject(projectId);
346
+ if (!project) {
347
+ return reply.code(404).send({ error: 'Project not found' });
348
+ }
349
+ const steeringStatus = await project.parser.getProjectSteeringStatus();
350
+ return {
351
+ projectId,
352
+ projectName: project.projectName,
353
+ projectPath: project.originalProjectPath, // Return original path for display
354
+ steering: steeringStatus,
355
+ version: this.packageVersion
356
+ };
357
+ });
358
+ // Specs list
359
+ this.app.get('/api/projects/:projectId/specs', async (request, reply) => {
360
+ const { projectId } = request.params;
361
+ const project = this.projectManager.getProject(projectId);
362
+ if (!project) {
363
+ return reply.code(404).send({ error: 'Project not found' });
364
+ }
365
+ return await project.parser.getAllSpecs();
366
+ });
367
+ // Archived specs list
368
+ this.app.get('/api/projects/:projectId/specs/archived', async (request, reply) => {
369
+ const { projectId } = request.params;
370
+ const project = this.projectManager.getProject(projectId);
371
+ if (!project) {
372
+ return reply.code(404).send({ error: 'Project not found' });
373
+ }
374
+ return await project.parser.getAllArchivedSpecs();
375
+ });
376
+ // Get spec details
377
+ this.app.get('/api/projects/:projectId/specs/:name', async (request, reply) => {
378
+ const { projectId, name } = request.params;
379
+ const project = this.projectManager.getProject(projectId);
380
+ if (!project) {
381
+ return reply.code(404).send({ error: 'Project not found' });
382
+ }
383
+ const spec = await project.parser.getSpec(name);
384
+ if (!spec) {
385
+ return reply.code(404).send({ error: 'Spec not found' });
386
+ }
387
+ return spec;
388
+ });
389
+ // Get all spec documents
390
+ this.app.get('/api/projects/:projectId/specs/:name/all', async (request, reply) => {
391
+ const { projectId, name } = request.params;
392
+ const project = this.projectManager.getProject(projectId);
393
+ if (!project) {
394
+ return reply.code(404).send({ error: 'Project not found' });
395
+ }
396
+ const specDir = join(project.projectPath, '.spec-workflow', 'specs', name);
397
+ const documents = ['requirements', 'design', 'tasks', 'readiness-report'];
398
+ const result = {};
399
+ for (const doc of documents) {
400
+ const docPath = join(specDir, `${doc}.md`);
401
+ try {
402
+ const content = await readFile(docPath, 'utf-8');
403
+ const stats = await fs.stat(docPath);
404
+ result[doc] = {
405
+ content,
406
+ lastModified: stats.mtime.toISOString()
407
+ };
408
+ }
409
+ catch {
410
+ result[doc] = null;
411
+ }
412
+ }
413
+ return result;
414
+ });
415
+ // Get all archived spec documents
416
+ this.app.get('/api/projects/:projectId/specs/:name/all/archived', async (request, reply) => {
417
+ const { projectId, name } = request.params;
418
+ const project = this.projectManager.getProject(projectId);
419
+ if (!project) {
420
+ return reply.code(404).send({ error: 'Project not found' });
421
+ }
422
+ // Use archive path instead of active specs path
423
+ const specDir = join(project.projectPath, '.spec-workflow', 'archive', 'specs', name);
424
+ const documents = ['requirements', 'design', 'tasks', 'readiness-report'];
425
+ const result = {};
426
+ for (const doc of documents) {
427
+ const docPath = join(specDir, `${doc}.md`);
428
+ try {
429
+ const content = await readFile(docPath, 'utf-8');
430
+ const stats = await fs.stat(docPath);
431
+ result[doc] = {
432
+ content,
433
+ lastModified: stats.mtime.toISOString()
434
+ };
435
+ }
436
+ catch {
437
+ result[doc] = null;
438
+ }
439
+ }
440
+ return result;
441
+ });
442
+ // Save spec document
443
+ this.app.put('/api/projects/:projectId/specs/:name/:document', async (request, reply) => {
444
+ const { projectId, name, document } = request.params;
445
+ const { content } = request.body;
446
+ const project = this.projectManager.getProject(projectId);
447
+ if (!project) {
448
+ return reply.code(404).send({ error: 'Project not found' });
449
+ }
450
+ const allowedDocs = ['requirements', 'design', 'tasks'];
451
+ if (!allowedDocs.includes(document)) {
452
+ return reply.code(400).send({ error: 'Invalid document type' });
453
+ }
454
+ if (typeof content !== 'string') {
455
+ return reply.code(400).send({ error: 'Content must be a string' });
456
+ }
457
+ const docPath = join(project.projectPath, '.spec-workflow', 'specs', name, `${document}.md`);
458
+ try {
459
+ const specDir = join(project.projectPath, '.spec-workflow', 'specs', name);
460
+ await fs.mkdir(specDir, { recursive: true });
461
+ await fs.writeFile(docPath, content, 'utf-8');
462
+ return { success: true, message: 'Document saved successfully' };
463
+ }
464
+ catch (error) {
465
+ return reply.code(500).send({ error: `Failed to save document: ${error.message}` });
466
+ }
467
+ });
468
+ // Archive spec
469
+ this.app.post('/api/projects/:projectId/specs/:name/archive', async (request, reply) => {
470
+ const { projectId, name } = request.params;
471
+ const project = this.projectManager.getProject(projectId);
472
+ if (!project) {
473
+ return reply.code(404).send({ error: 'Project not found' });
474
+ }
475
+ try {
476
+ await project.archiveService.archiveSpec(name);
477
+ return { success: true, message: `Spec '${name}' archived successfully` };
478
+ }
479
+ catch (error) {
480
+ return reply.code(400).send({ error: error.message });
481
+ }
482
+ });
483
+ // Unarchive spec
484
+ this.app.post('/api/projects/:projectId/specs/:name/unarchive', async (request, reply) => {
485
+ const { projectId, name } = request.params;
486
+ const project = this.projectManager.getProject(projectId);
487
+ if (!project) {
488
+ return reply.code(404).send({ error: 'Project not found' });
489
+ }
490
+ try {
491
+ await project.archiveService.unarchiveSpec(name);
492
+ return { success: true, message: `Spec '${name}' unarchived successfully` };
493
+ }
494
+ catch (error) {
495
+ return reply.code(400).send({ error: error.message });
496
+ }
497
+ });
498
+ // Get approvals
499
+ this.app.get('/api/projects/:projectId/approvals', async (request, reply) => {
500
+ const { projectId } = request.params;
501
+ const project = this.projectManager.getProject(projectId);
502
+ if (!project) {
503
+ return reply.code(404).send({ error: 'Project not found' });
504
+ }
505
+ return await project.approvalStorage.getAllPendingApprovals();
506
+ });
507
+ // Get approval content
508
+ this.app.get('/api/projects/:projectId/approvals/:id/content', async (request, reply) => {
509
+ const { projectId, id } = request.params;
510
+ const project = this.projectManager.getProject(projectId);
511
+ if (!project) {
512
+ return reply.code(404).send({ error: 'Project not found' });
513
+ }
514
+ try {
515
+ const approval = await project.approvalStorage.getApproval(id);
516
+ if (!approval || !approval.filePath) {
517
+ return reply.code(404).send({ error: 'Approval not found or no file path' });
518
+ }
519
+ const candidateSet = new Set();
520
+ const p = approval.filePath;
521
+ const isAbsolutePath = p.startsWith('/') || p.match(/^[A-Za-z]:[\\\/]/);
522
+ if (!isAbsolutePath) {
523
+ // 1) Resolve against workspace/worktree first
524
+ candidateSet.add(join(project.workspacePath, p));
525
+ }
526
+ // 2) Absolute path as-is
527
+ if (isAbsolutePath) {
528
+ candidateSet.add(p);
529
+ }
530
+ if (!isAbsolutePath) {
531
+ // 3) Resolve against workflow root
532
+ candidateSet.add(join(project.projectPath, p));
533
+ // 4) Legacy fallback for historical paths
534
+ if (!p.includes('.spec-workflow')) {
535
+ candidateSet.add(join(project.projectPath, '.spec-workflow', p));
536
+ }
537
+ }
538
+ const candidates = Array.from(candidateSet);
539
+ let content = null;
540
+ let resolvedPath = null;
541
+ for (const candidate of candidates) {
542
+ try {
543
+ const data = await fs.readFile(candidate, 'utf-8');
544
+ content = data;
545
+ resolvedPath = candidate;
546
+ break;
547
+ }
548
+ catch {
549
+ // try next candidate
550
+ }
551
+ }
552
+ if (content == null) {
553
+ return reply.code(500).send({ error: `Failed to read file at any known location for ${approval.filePath}` });
554
+ }
555
+ return { content, filePath: resolvedPath || approval.filePath };
556
+ }
557
+ catch (error) {
558
+ return reply.code(500).send({ error: `Failed to read file: ${error.message}` });
559
+ }
560
+ });
561
+ // Approval actions (approve, reject, needs-revision)
562
+ this.app.post('/api/projects/:projectId/approvals/:id/:action', async (request, reply) => {
563
+ try {
564
+ const { projectId, id, action } = request.params;
565
+ const { response, annotations, comments } = (request.body || {});
566
+ const project = this.projectManager.getProject(projectId);
567
+ if (!project) {
568
+ return reply.code(404).send({ error: 'Project not found' });
569
+ }
570
+ const validActions = ['approve', 'reject', 'needs-revision', 'concerns'];
571
+ if (!validActions.includes(action)) {
572
+ return reply.code(400).send({ error: 'Invalid action' });
573
+ }
574
+ // Convert action name to status value
575
+ const actionToStatus = {
576
+ 'approve': 'approved',
577
+ 'reject': 'rejected',
578
+ 'needs-revision': 'needs-revision',
579
+ 'concerns': 'concerns'
580
+ };
581
+ const status = actionToStatus[action];
582
+ await project.approvalStorage.updateApproval(id, status, response, annotations, comments);
583
+ return { success: true };
584
+ }
585
+ catch (error) {
586
+ return reply.code(500).send({ error: error.message || 'Internal server error' });
587
+ }
588
+ });
589
+ // Undo batch operations - revert items back to pending
590
+ // IMPORTANT: This route MUST be defined BEFORE the /batch/:action route
591
+ // because Fastify matches routes in order of registration
592
+ this.app.post('/api/projects/:projectId/approvals/batch/undo', async (request, reply) => {
593
+ const { projectId } = request.params;
594
+ const { ids } = request.body;
595
+ const project = this.projectManager.getProject(projectId);
596
+ if (!project) {
597
+ return reply.code(404).send({ error: 'Project not found' });
598
+ }
599
+ // Validate ids array
600
+ if (!Array.isArray(ids) || ids.length === 0) {
601
+ return reply.code(400).send({ error: 'ids must be a non-empty array' });
602
+ }
603
+ // Batch size limit
604
+ const BATCH_SIZE_LIMIT = 100;
605
+ if (ids.length > BATCH_SIZE_LIMIT) {
606
+ return reply.code(400).send({
607
+ error: `Batch size exceeds limit. Maximum ${BATCH_SIZE_LIMIT} items allowed.`
608
+ });
609
+ }
610
+ // Validate ID format
611
+ const idPattern = /^[a-zA-Z0-9_-]+$/;
612
+ const invalidIds = ids.filter(id => !idPattern.test(id));
613
+ if (invalidIds.length > 0) {
614
+ return reply.code(400).send({
615
+ error: `Invalid ID format: ${invalidIds.slice(0, 3).join(', ')}${invalidIds.length > 3 ? '...' : ''}`
616
+ });
617
+ }
618
+ // Process all undo operations with continue-on-error
619
+ const results = {
620
+ succeeded: [],
621
+ failed: []
622
+ };
623
+ for (const id of ids) {
624
+ try {
625
+ // Revert to pending status, clear response and respondedAt
626
+ await project.approvalStorage.revertToPending(id);
627
+ results.succeeded.push(id);
628
+ }
629
+ catch (error) {
630
+ results.failed.push({ id, error: error.message });
631
+ }
632
+ }
633
+ // Broadcast WebSocket update for successful undos
634
+ if (results.succeeded.length > 0) {
635
+ this.broadcastToProject(projectId, {
636
+ type: 'batch-approval-undo',
637
+ ids: results.succeeded,
638
+ count: results.succeeded.length
639
+ });
640
+ }
641
+ return {
642
+ success: results.failed.length === 0,
643
+ total: ids.length,
644
+ succeeded: results.succeeded,
645
+ failed: results.failed
646
+ };
647
+ });
648
+ // Batch approval actions (approve, reject only - no batch needs-revision)
649
+ this.app.post('/api/projects/:projectId/approvals/batch/:action', async (request, reply) => {
650
+ const { projectId, action } = request.params;
651
+ const { ids, response } = request.body;
652
+ const project = this.projectManager.getProject(projectId);
653
+ if (!project) {
654
+ return reply.code(404).send({ error: 'Project not found' });
655
+ }
656
+ // Only allow approve and reject for batch operations (UX recommendation)
657
+ const validBatchActions = ['approve', 'reject'];
658
+ if (!validBatchActions.includes(action)) {
659
+ return reply.code(400).send({
660
+ error: 'Invalid batch action. Only "approve" and "reject" are allowed for batch operations.'
661
+ });
662
+ }
663
+ // Validate ids array
664
+ if (!Array.isArray(ids) || ids.length === 0) {
665
+ return reply.code(400).send({ error: 'ids must be a non-empty array' });
666
+ }
667
+ // Batch size limit (PE recommendation)
668
+ const BATCH_SIZE_LIMIT = 100;
669
+ if (ids.length > BATCH_SIZE_LIMIT) {
670
+ return reply.code(400).send({
671
+ error: `Batch size exceeds limit. Maximum ${BATCH_SIZE_LIMIT} items allowed.`
672
+ });
673
+ }
674
+ // Validate ID format (PE recommendation - alphanumeric with hyphens/underscores)
675
+ const idPattern = /^[a-zA-Z0-9_-]+$/;
676
+ const invalidIds = ids.filter(id => !idPattern.test(id));
677
+ if (invalidIds.length > 0) {
678
+ return reply.code(400).send({
679
+ error: `Invalid ID format: ${invalidIds.slice(0, 3).join(', ')}${invalidIds.length > 3 ? '...' : ''}`
680
+ });
681
+ }
682
+ const actionToStatus = {
683
+ 'approve': 'approved',
684
+ 'reject': 'rejected'
685
+ };
686
+ const status = actionToStatus[action];
687
+ const batchResponse = response || `Batch ${action}d`;
688
+ // Process all approvals with continue-on-error (PE recommendation)
689
+ const results = {
690
+ succeeded: [],
691
+ failed: []
692
+ };
693
+ // Debounce WebSocket broadcasts - collect all updates first (use results.succeeded)
694
+ for (const id of ids) {
695
+ try {
696
+ await project.approvalStorage.updateApproval(id, status, batchResponse);
697
+ results.succeeded.push(id);
698
+ }
699
+ catch (error) {
700
+ results.failed.push({ id, error: error.message });
701
+ }
702
+ }
703
+ // Single consolidated WebSocket broadcast for all successful updates
704
+ if (results.succeeded.length > 0) {
705
+ this.broadcastToProject(projectId, {
706
+ type: 'batch-approval-update',
707
+ action: action,
708
+ ids: results.succeeded,
709
+ count: results.succeeded.length
710
+ });
711
+ }
712
+ return {
713
+ success: results.failed.length === 0,
714
+ total: ids.length,
715
+ succeeded: results.succeeded,
716
+ failed: results.failed
717
+ };
718
+ });
719
+ // Get all snapshots for an approval
720
+ this.app.get('/api/projects/:projectId/approvals/:id/snapshots', async (request, reply) => {
721
+ const { projectId, id } = request.params;
722
+ const project = this.projectManager.getProject(projectId);
723
+ if (!project) {
724
+ return reply.code(404).send({ error: 'Project not found' });
725
+ }
726
+ try {
727
+ const snapshots = await project.approvalStorage.getSnapshots(id);
728
+ return snapshots;
729
+ }
730
+ catch (error) {
731
+ return reply.code(500).send({ error: `Failed to get snapshots: ${error.message}` });
732
+ }
733
+ });
734
+ // Get specific snapshot version for an approval
735
+ this.app.get('/api/projects/:projectId/approvals/:id/snapshots/:version', async (request, reply) => {
736
+ const { projectId, id, version } = request.params;
737
+ const project = this.projectManager.getProject(projectId);
738
+ if (!project) {
739
+ return reply.code(404).send({ error: 'Project not found' });
740
+ }
741
+ try {
742
+ const versionNum = parseInt(version, 10);
743
+ if (isNaN(versionNum)) {
744
+ return reply.code(400).send({ error: 'Invalid version number' });
745
+ }
746
+ const snapshot = await project.approvalStorage.getSnapshot(id, versionNum);
747
+ if (!snapshot) {
748
+ return reply.code(404).send({ error: `Snapshot version ${version} not found` });
749
+ }
750
+ return snapshot;
751
+ }
752
+ catch (error) {
753
+ return reply.code(500).send({ error: `Failed to get snapshot: ${error.message}` });
754
+ }
755
+ });
756
+ // Get diff between two versions or between version and current
757
+ this.app.get('/api/projects/:projectId/approvals/:id/diff', async (request, reply) => {
758
+ const { projectId, id } = request.params;
759
+ const { from, to } = request.query;
760
+ const project = this.projectManager.getProject(projectId);
761
+ if (!project) {
762
+ return reply.code(404).send({ error: 'Project not found' });
763
+ }
764
+ if (!from) {
765
+ return reply.code(400).send({ error: 'from parameter is required' });
766
+ }
767
+ try {
768
+ const fromVersion = parseInt(from, 10);
769
+ if (isNaN(fromVersion)) {
770
+ return reply.code(400).send({ error: 'Invalid from version number' });
771
+ }
772
+ let toVersion;
773
+ if (to === 'current' || to === undefined) {
774
+ toVersion = 'current';
775
+ }
776
+ else {
777
+ const toVersionNum = parseInt(to, 10);
778
+ if (isNaN(toVersionNum)) {
779
+ return reply.code(400).send({ error: 'Invalid to version number' });
780
+ }
781
+ toVersion = toVersionNum;
782
+ }
783
+ const diff = await project.approvalStorage.compareSnapshots(id, fromVersion, toVersion);
784
+ return diff;
785
+ }
786
+ catch (error) {
787
+ return reply.code(500).send({ error: `Failed to compute diff: ${error.message}` });
788
+ }
789
+ });
790
+ // Manual snapshot capture
791
+ this.app.post('/api/projects/:projectId/approvals/:id/snapshot', async (request, reply) => {
792
+ const { projectId, id } = request.params;
793
+ const project = this.projectManager.getProject(projectId);
794
+ if (!project) {
795
+ return reply.code(404).send({ error: 'Project not found' });
796
+ }
797
+ try {
798
+ await project.approvalStorage.captureSnapshot(id, 'manual');
799
+ return { success: true, message: 'Snapshot captured successfully' };
800
+ }
801
+ catch (error) {
802
+ return reply.code(500).send({ error: `Failed to capture snapshot: ${error.message}` });
803
+ }
804
+ });
805
+ // Get steering document
806
+ this.app.get('/api/projects/:projectId/steering/:name', async (request, reply) => {
807
+ const { projectId, name } = request.params;
808
+ const project = this.projectManager.getProject(projectId);
809
+ if (!project) {
810
+ return reply.code(404).send({ error: 'Project not found' });
811
+ }
812
+ const allowedDocs = ['product', 'tech', 'structure'];
813
+ if (!allowedDocs.includes(name)) {
814
+ return reply.code(400).send({ error: 'Invalid steering document name' });
815
+ }
816
+ const docPath = join(project.projectPath, '.spec-workflow', 'steering', `${name}.md`);
817
+ try {
818
+ const content = await readFile(docPath, 'utf-8');
819
+ const stats = await fs.stat(docPath);
820
+ return {
821
+ content,
822
+ lastModified: stats.mtime.toISOString()
823
+ };
824
+ }
825
+ catch {
826
+ return {
827
+ content: '',
828
+ lastModified: new Date().toISOString()
829
+ };
830
+ }
831
+ });
832
+ // Save steering document
833
+ this.app.put('/api/projects/:projectId/steering/:name', async (request, reply) => {
834
+ const { projectId, name } = request.params;
835
+ const { content } = request.body;
836
+ const project = this.projectManager.getProject(projectId);
837
+ if (!project) {
838
+ return reply.code(404).send({ error: 'Project not found' });
839
+ }
840
+ const allowedDocs = ['product', 'tech', 'structure'];
841
+ if (!allowedDocs.includes(name)) {
842
+ return reply.code(400).send({ error: 'Invalid steering document name' });
843
+ }
844
+ if (typeof content !== 'string') {
845
+ return reply.code(400).send({ error: 'Content must be a string' });
846
+ }
847
+ const steeringDir = join(project.projectPath, '.spec-workflow', 'steering');
848
+ const docPath = join(steeringDir, `${name}.md`);
849
+ try {
850
+ await fs.mkdir(steeringDir, { recursive: true });
851
+ await fs.writeFile(docPath, content, 'utf-8');
852
+ return { success: true, message: 'Steering document saved successfully' };
853
+ }
854
+ catch (error) {
855
+ return reply.code(500).send({ error: `Failed to save steering document: ${error.message}` });
856
+ }
857
+ });
858
+ // Get task progress
859
+ this.app.get('/api/projects/:projectId/specs/:name/tasks/progress', async (request, reply) => {
860
+ const { projectId, name } = request.params;
861
+ const project = this.projectManager.getProject(projectId);
862
+ if (!project) {
863
+ return reply.code(404).send({ error: 'Project not found' });
864
+ }
865
+ try {
866
+ const spec = await project.parser.getSpec(name);
867
+ if (!spec || !spec.phases.tasks.exists) {
868
+ return reply.code(404).send({ error: 'Spec or tasks not found' });
869
+ }
870
+ const tasksPath = join(project.projectPath, '.spec-workflow', 'specs', name, 'tasks.md');
871
+ const tasksContent = await readFile(tasksPath, 'utf-8');
872
+ const parseResult = parseTasksFromMarkdown(tasksContent);
873
+ const totalTasks = parseResult.summary.total;
874
+ const completedTasks = parseResult.summary.completed;
875
+ const progress = totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0;
876
+ return {
877
+ total: totalTasks,
878
+ completed: completedTasks,
879
+ inProgress: parseResult.inProgressTask,
880
+ progress: progress,
881
+ taskList: parseResult.tasks,
882
+ lastModified: spec.phases.tasks.lastModified || spec.lastModified
883
+ };
884
+ }
885
+ catch (error) {
886
+ return reply.code(500).send({ error: `Failed to get task progress: ${error.message}` });
887
+ }
888
+ });
889
+ // Update task status
890
+ this.app.put('/api/projects/:projectId/specs/:name/tasks/:taskId/status', async (request, reply) => {
891
+ const { projectId, name, taskId } = request.params;
892
+ const { status } = request.body;
893
+ const project = this.projectManager.getProject(projectId);
894
+ if (!project) {
895
+ return reply.code(404).send({ error: 'Project not found' });
896
+ }
897
+ if (!status || !['pending', 'in-progress', 'completed'].includes(status)) {
898
+ return reply.code(400).send({ error: 'Invalid status. Must be pending, in-progress, or completed' });
899
+ }
900
+ try {
901
+ const tasksPath = join(project.projectPath, '.spec-workflow', 'specs', name, 'tasks.md');
902
+ let tasksContent;
903
+ try {
904
+ tasksContent = await readFile(tasksPath, 'utf-8');
905
+ }
906
+ catch (error) {
907
+ if (error.code === 'ENOENT') {
908
+ return reply.code(404).send({ error: 'Tasks file not found' });
909
+ }
910
+ throw error;
911
+ }
912
+ const parseResult = parseTasksFromMarkdown(tasksContent);
913
+ const task = parseResult.tasks.find(t => t.id === taskId);
914
+ if (!task) {
915
+ return reply.code(404).send({ error: `Task ${taskId} not found` });
916
+ }
917
+ if (task.status === status) {
918
+ return {
919
+ success: true,
920
+ message: `Task ${taskId} already has status ${status}`,
921
+ task: { ...task, status }
922
+ };
923
+ }
924
+ const { updateTaskStatus } = await import('../core/task-parser.js');
925
+ const updatedContent = updateTaskStatus(tasksContent, taskId, status);
926
+ if (updatedContent === tasksContent) {
927
+ return reply.code(500).send({ error: `Failed to update task ${taskId} in markdown content` });
928
+ }
929
+ await fs.writeFile(tasksPath, updatedContent, 'utf-8');
930
+ this.broadcastTaskUpdate(projectId, name);
931
+ return {
932
+ success: true,
933
+ message: `Task ${taskId} status updated to ${status}`,
934
+ task: { ...task, status }
935
+ };
936
+ }
937
+ catch (error) {
938
+ return reply.code(500).send({ error: `Failed to update task status: ${error.message}` });
939
+ }
940
+ });
941
+ // Add implementation log entry
942
+ this.app.post('/api/projects/:projectId/specs/:name/implementation-log', async (request, reply) => {
943
+ const { projectId, name } = request.params;
944
+ const project = this.projectManager.getProject(projectId);
945
+ if (!project) {
946
+ return reply.code(404).send({ error: 'Project not found' });
947
+ }
948
+ try {
949
+ const logData = request.body;
950
+ // Validate artifacts are provided
951
+ if (!logData.artifacts) {
952
+ return reply.code(400).send({ error: 'artifacts field is REQUIRED. Include apiEndpoints, components, functions, classes, or integrations in the artifacts object.' });
953
+ }
954
+ const specPath = join(project.projectPath, '.spec-workflow', 'specs', name);
955
+ const logManager = new ImplementationLogManager(specPath);
956
+ const entry = await logManager.addLogEntry(logData);
957
+ await this.broadcastImplementationLogUpdate(projectId, name);
958
+ return entry;
959
+ }
960
+ catch (error) {
961
+ return reply.code(500).send({ error: `Failed to add implementation log: ${error.message}` });
962
+ }
963
+ });
964
+ // Get implementation logs
965
+ this.app.get('/api/projects/:projectId/specs/:name/implementation-log', async (request, reply) => {
966
+ const { projectId, name } = request.params;
967
+ const query = request.query;
968
+ const project = this.projectManager.getProject(projectId);
969
+ if (!project) {
970
+ return reply.code(404).send({ error: 'Project not found' });
971
+ }
972
+ try {
973
+ const specPath = join(project.projectPath, '.spec-workflow', 'specs', name);
974
+ const logManager = new ImplementationLogManager(specPath);
975
+ let logs = await logManager.getAllLogs();
976
+ if (query.taskId) {
977
+ logs = logs.filter(log => log.taskId === query.taskId);
978
+ }
979
+ if (query.search) {
980
+ logs = await logManager.searchLogs(query.search);
981
+ }
982
+ return { entries: logs };
983
+ }
984
+ catch (error) {
985
+ return reply.code(500).send({ error: `Failed to get implementation logs: ${error.message}` });
986
+ }
987
+ });
988
+ // Get implementation log task stats
989
+ this.app.get('/api/projects/:projectId/specs/:name/implementation-log/task/:taskId/stats', async (request, reply) => {
990
+ const { projectId, name, taskId } = request.params;
991
+ const project = this.projectManager.getProject(projectId);
992
+ if (!project) {
993
+ return reply.code(404).send({ error: 'Project not found' });
994
+ }
995
+ try {
996
+ const specPath = join(project.projectPath, '.spec-workflow', 'specs', name);
997
+ const logManager = new ImplementationLogManager(specPath);
998
+ const stats = await logManager.getTaskStats(taskId);
999
+ return stats;
1000
+ }
1001
+ catch (error) {
1002
+ return reply.code(500).send({ error: `Failed to get implementation log stats: ${error.message}` });
1003
+ }
1004
+ });
1005
+ // Project-specific changelog endpoint
1006
+ this.app.get('/api/projects/:projectId/changelog/:version', async (request, reply) => {
1007
+ const { version } = request.params;
1008
+ try {
1009
+ const changelogPath = join(__dirname, '..', '..', 'CHANGELOG.md');
1010
+ const content = await readFile(changelogPath, 'utf-8');
1011
+ // Extract the section for the requested version
1012
+ const versionRegex = new RegExp(`## \\[${version}\\][^]*?(?=## \\[|$)`, 'i');
1013
+ const match = content.match(versionRegex);
1014
+ if (!match) {
1015
+ return reply.code(404).send({ error: `Changelog for version ${version} not found` });
1016
+ }
1017
+ return { content: match[0].trim() };
1018
+ }
1019
+ catch (error) {
1020
+ if (error.code === 'ENOENT') {
1021
+ return reply.code(404).send({ error: 'Changelog file not found' });
1022
+ }
1023
+ return reply.code(500).send({ error: `Failed to fetch changelog: ${error.message}` });
1024
+ }
1025
+ });
1026
+ // Global changelog endpoint
1027
+ this.app.get('/api/changelog/:version', async (request, reply) => {
1028
+ const { version } = request.params;
1029
+ try {
1030
+ const changelogPath = join(__dirname, '..', '..', 'CHANGELOG.md');
1031
+ const content = await readFile(changelogPath, 'utf-8');
1032
+ // Extract the section for the requested version
1033
+ const versionRegex = new RegExp(`## \\[${version}\\][^]*?(?=## \\[|$)`, 'i');
1034
+ const match = content.match(versionRegex);
1035
+ if (!match) {
1036
+ return reply.code(404).send({ error: `Changelog for version ${version} not found` });
1037
+ }
1038
+ return { content: match[0].trim() };
1039
+ }
1040
+ catch (error) {
1041
+ if (error.code === 'ENOENT') {
1042
+ return reply.code(404).send({ error: 'Changelog file not found' });
1043
+ }
1044
+ return reply.code(500).send({ error: `Failed to fetch changelog: ${error.message}` });
1045
+ }
1046
+ });
1047
+ // Global settings endpoints
1048
+ // Get all automation jobs
1049
+ this.app.get('/api/jobs', async () => {
1050
+ return await this.jobScheduler.getAllJobs();
1051
+ });
1052
+ // Create a new automation job
1053
+ this.app.post('/api/jobs', async (request, reply) => {
1054
+ const job = request.body;
1055
+ if (!job.id || !job.name || !job.type || job.config === undefined || !job.schedule) {
1056
+ return reply.code(400).send({ error: 'Missing required fields: id, name, type, config, schedule' });
1057
+ }
1058
+ try {
1059
+ await this.jobScheduler.addJob({
1060
+ id: job.id,
1061
+ name: job.name,
1062
+ type: job.type,
1063
+ enabled: job.enabled !== false,
1064
+ config: job.config,
1065
+ schedule: job.schedule,
1066
+ createdAt: new Date().toISOString()
1067
+ });
1068
+ return { success: true, message: 'Job created successfully' };
1069
+ }
1070
+ catch (error) {
1071
+ return reply.code(400).send({ error: error.message });
1072
+ }
1073
+ });
1074
+ // Get a specific automation job
1075
+ this.app.get('/api/jobs/:jobId', async (request, reply) => {
1076
+ const { jobId } = request.params;
1077
+ const settingsManager = new (await import('./settings-manager.js')).SettingsManager();
1078
+ try {
1079
+ const job = await settingsManager.getJob(jobId);
1080
+ if (!job) {
1081
+ return reply.code(404).send({ error: 'Job not found' });
1082
+ }
1083
+ return job;
1084
+ }
1085
+ catch (error) {
1086
+ return reply.code(500).send({ error: error.message });
1087
+ }
1088
+ });
1089
+ // Update an automation job
1090
+ this.app.put('/api/jobs/:jobId', async (request, reply) => {
1091
+ const { jobId } = request.params;
1092
+ const updates = request.body;
1093
+ try {
1094
+ await this.jobScheduler.updateJob(jobId, updates);
1095
+ return { success: true, message: 'Job updated successfully' };
1096
+ }
1097
+ catch (error) {
1098
+ return reply.code(400).send({ error: error.message });
1099
+ }
1100
+ });
1101
+ // Delete an automation job
1102
+ this.app.delete('/api/jobs/:jobId', async (request, reply) => {
1103
+ const { jobId } = request.params;
1104
+ try {
1105
+ await this.jobScheduler.deleteJob(jobId);
1106
+ return { success: true, message: 'Job deleted successfully' };
1107
+ }
1108
+ catch (error) {
1109
+ return reply.code(400).send({ error: error.message });
1110
+ }
1111
+ });
1112
+ // Manually run a job
1113
+ this.app.post('/api/jobs/:jobId/run', async (request, reply) => {
1114
+ const { jobId } = request.params;
1115
+ try {
1116
+ const result = await this.jobScheduler.runJobManually(jobId);
1117
+ return result;
1118
+ }
1119
+ catch (error) {
1120
+ return reply.code(400).send({ error: error.message });
1121
+ }
1122
+ });
1123
+ // Get job execution history
1124
+ this.app.get('/api/jobs/:jobId/history', async (request, reply) => {
1125
+ const { jobId } = request.params;
1126
+ const { limit } = request.query;
1127
+ try {
1128
+ const history = await this.jobScheduler.getJobExecutionHistory(jobId, parseInt(limit || '50'));
1129
+ return history;
1130
+ }
1131
+ catch (error) {
1132
+ return reply.code(500).send({ error: error.message });
1133
+ }
1134
+ });
1135
+ // Get job statistics
1136
+ this.app.get('/api/jobs/:jobId/stats', async (request, reply) => {
1137
+ const { jobId } = request.params;
1138
+ try {
1139
+ const stats = await this.jobScheduler.getJobStats(jobId);
1140
+ return stats;
1141
+ }
1142
+ catch (error) {
1143
+ return reply.code(500).send({ error: error.message });
1144
+ }
1145
+ });
1146
+ }
1147
+ broadcastToAll(message) {
1148
+ const messageStr = JSON.stringify(message);
1149
+ this.clients.forEach((connection) => {
1150
+ try {
1151
+ if (connection.socket.readyState === WebSocket.OPEN) {
1152
+ connection.socket.send(messageStr);
1153
+ }
1154
+ }
1155
+ catch (error) {
1156
+ console.error('Error broadcasting to client:', error);
1157
+ this.scheduleConnectionCleanup(connection);
1158
+ }
1159
+ });
1160
+ }
1161
+ broadcastToProject(projectId, message) {
1162
+ const messageStr = JSON.stringify(message);
1163
+ this.clients.forEach((connection) => {
1164
+ try {
1165
+ if (connection.socket.readyState === WebSocket.OPEN && connection.projectId === projectId) {
1166
+ connection.socket.send(messageStr);
1167
+ }
1168
+ }
1169
+ catch (error) {
1170
+ console.error('Error broadcasting to project client:', error);
1171
+ this.scheduleConnectionCleanup(connection);
1172
+ }
1173
+ });
1174
+ }
1175
+ scheduleConnectionCleanup(connection) {
1176
+ // Use setImmediate to avoid modifying Set during iteration
1177
+ setImmediate(() => {
1178
+ try {
1179
+ this.clients.delete(connection);
1180
+ connection.socket.removeAllListeners();
1181
+ if (connection.socket.readyState === WebSocket.OPEN) {
1182
+ connection.socket.close();
1183
+ }
1184
+ }
1185
+ catch {
1186
+ // Ignore cleanup errors
1187
+ }
1188
+ });
1189
+ }
1190
+ startHeartbeat() {
1191
+ this.heartbeatInterval = setInterval(() => {
1192
+ this.clients.forEach((connection) => {
1193
+ if (connection.socket.readyState === WebSocket.OPEN) {
1194
+ try {
1195
+ // Mark as waiting for pong
1196
+ connection.isAlive = false;
1197
+ connection.socket.ping();
1198
+ }
1199
+ catch {
1200
+ this.scheduleConnectionCleanup(connection);
1201
+ }
1202
+ }
1203
+ });
1204
+ // Check for dead connections after timeout
1205
+ setTimeout(() => {
1206
+ this.clients.forEach((connection) => {
1207
+ if (connection.isAlive === false) {
1208
+ console.error('Connection did not respond to heartbeat, cleaning up');
1209
+ this.scheduleConnectionCleanup(connection);
1210
+ }
1211
+ });
1212
+ }, this.HEARTBEAT_TIMEOUT_MS);
1213
+ }, this.HEARTBEAT_INTERVAL_MS);
1214
+ }
1215
+ stopHeartbeat() {
1216
+ if (this.heartbeatInterval) {
1217
+ clearInterval(this.heartbeatInterval);
1218
+ this.heartbeatInterval = undefined;
1219
+ }
1220
+ }
1221
+ async broadcastTaskUpdate(projectId, specName) {
1222
+ try {
1223
+ const project = this.projectManager.getProject(projectId);
1224
+ if (!project)
1225
+ return;
1226
+ const tasksPath = join(project.projectPath, '.spec-workflow', 'specs', specName, 'tasks.md');
1227
+ const tasksContent = await readFile(tasksPath, 'utf-8');
1228
+ const parseResult = parseTasksFromMarkdown(tasksContent);
1229
+ this.broadcastToProject(projectId, {
1230
+ type: 'task-status-update',
1231
+ projectId,
1232
+ data: {
1233
+ specName,
1234
+ taskList: parseResult.tasks,
1235
+ summary: parseResult.summary,
1236
+ inProgress: parseResult.inProgressTask
1237
+ }
1238
+ });
1239
+ }
1240
+ catch (error) {
1241
+ console.error('Error broadcasting task update:', error);
1242
+ }
1243
+ }
1244
+ async broadcastImplementationLogUpdate(projectId, specName) {
1245
+ try {
1246
+ const project = this.projectManager.getProject(projectId);
1247
+ if (!project)
1248
+ return;
1249
+ const specPath = join(project.projectPath, '.spec-workflow', 'specs', specName);
1250
+ const logManager = new ImplementationLogManager(specPath);
1251
+ const logs = await logManager.getAllLogs();
1252
+ this.broadcastToProject(projectId, {
1253
+ type: 'implementation-log-update',
1254
+ projectId,
1255
+ data: {
1256
+ specName,
1257
+ entries: logs
1258
+ }
1259
+ });
1260
+ }
1261
+ catch (error) {
1262
+ console.error('Error broadcasting implementation log update:', error);
1263
+ }
1264
+ }
1265
+ async stop() {
1266
+ // Stop heartbeat monitoring
1267
+ this.stopHeartbeat();
1268
+ // Clear pending spec broadcasts
1269
+ for (const timeout of this.pendingSpecBroadcasts.values()) {
1270
+ clearTimeout(timeout);
1271
+ }
1272
+ this.pendingSpecBroadcasts.clear();
1273
+ // Close all WebSocket connections
1274
+ this.clients.forEach((connection) => {
1275
+ try {
1276
+ connection.socket.removeAllListeners();
1277
+ if (connection.socket.readyState === WebSocket.OPEN) {
1278
+ connection.socket.close();
1279
+ }
1280
+ }
1281
+ catch (error) {
1282
+ // Ignore cleanup errors
1283
+ }
1284
+ });
1285
+ this.clients.clear();
1286
+ // Stop job scheduler
1287
+ await this.jobScheduler.shutdown();
1288
+ // Stop project manager
1289
+ await this.projectManager.stop();
1290
+ // Close the Fastify server
1291
+ await this.app.close();
1292
+ // Unregister from the session manager
1293
+ try {
1294
+ await this.sessionManager.unregisterDashboard();
1295
+ }
1296
+ catch (error) {
1297
+ // Ignore cleanup errors
1298
+ }
1299
+ }
1300
+ getUrl() {
1301
+ return `http://localhost:${this.actualPort}`;
1302
+ }
1303
+ }
1304
+ //# sourceMappingURL=multi-server.js.map