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