sillyspec 3.20.3 → 3.20.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +21 -17
  2. package/.claude/skills/sillyspec-auto/SKILL.md +83 -77
  3. package/.claude/skills/sillyspec-brainstorm/SKILL.md +44 -17
  4. package/.claude/skills/sillyspec-commit/SKILL.md +106 -105
  5. package/.claude/skills/sillyspec-continue/SKILL.md +45 -44
  6. package/.claude/skills/sillyspec-doctor/SKILL.md +31 -22
  7. package/.claude/skills/sillyspec-execute/SKILL.md +30 -17
  8. package/.claude/skills/sillyspec-explore/SKILL.md +109 -96
  9. package/.claude/skills/sillyspec-knowledge/SKILL.md +270 -0
  10. package/.claude/skills/sillyspec-plan/SKILL.md +21 -52
  11. package/.claude/skills/sillyspec-propose/SKILL.md +21 -17
  12. package/.claude/skills/sillyspec-quick/SKILL.md +21 -17
  13. package/.claude/skills/sillyspec-resume/SKILL.md +68 -111
  14. package/.claude/skills/sillyspec-scan/SKILL.md +21 -17
  15. package/.claude/skills/sillyspec-state/SKILL.md +54 -54
  16. package/.claude/skills/sillyspec-status/SKILL.md +21 -17
  17. package/.claude/skills/sillyspec-verify/SKILL.md +21 -17
  18. package/.claude/skills/sillyspec-workspace/SKILL.md +157 -149
  19. package/.husky/pre-push +13 -0
  20. package/CLAUDE.md +18 -0
  21. package/README.md +198 -186
  22. package/SKILL.md +90 -87
  23. package/bin/sillyspec.js +2 -2
  24. package/docs/brainstorm-plan-contract.md +64 -0
  25. package/docs/plan-execute-contract.md +123 -0
  26. package/docs/platform-scan-protocol.md +298 -0
  27. package/docs/revision-mode.md +115 -0
  28. package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +99 -0
  29. package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +218 -0
  30. package/docs/sillyspec/file-lifecycle/stage-artifacts.md +167 -0
  31. package/docs/sillyspec/file-lifecycle/storage-and-state.md +148 -0
  32. package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +211 -0
  33. package/docs/sillyspec/file-lifecycle.md +125 -0
  34. package/docs/workflow-contract-regression.md +106 -0
  35. package/docs/worktree-isolation.md +252 -0
  36. package/package.json +40 -34
  37. package/packages/dashboard/dist/assets/{index-D1EVTLmc.js → index-Bq_Z2hne.js} +7446 -7446
  38. package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -0
  39. package/packages/dashboard/dist/index.html +16 -16
  40. package/packages/dashboard/dist/prototype-dashboard.html +836 -0
  41. package/packages/dashboard/dist/prototype-overview.html +256 -0
  42. package/packages/dashboard/index.html +15 -15
  43. package/packages/dashboard/package-lock.json +2384 -2384
  44. package/packages/dashboard/package.json +25 -25
  45. package/packages/dashboard/public/prototype-dashboard.html +836 -0
  46. package/packages/dashboard/public/prototype-overview.html +256 -0
  47. package/packages/dashboard/server/executor.js +86 -86
  48. package/packages/dashboard/server/index.js +588 -508
  49. package/packages/dashboard/server/parser.js +526 -458
  50. package/packages/dashboard/server/watcher.js +350 -342
  51. package/packages/dashboard/src/App.vue +558 -325
  52. package/packages/dashboard/src/components/ActionBar.vue +93 -84
  53. package/packages/dashboard/src/components/CommandPalette.vue +96 -92
  54. package/packages/dashboard/src/components/DetailPanel.vue +137 -137
  55. package/packages/dashboard/src/components/DocPreview.vue +105 -8
  56. package/packages/dashboard/src/components/DocTree.vue +75 -19
  57. package/packages/dashboard/src/components/HResizeHandle.vue +48 -0
  58. package/packages/dashboard/src/components/LogStream.vue +65 -65
  59. package/packages/dashboard/src/components/PipelineStage.vue +95 -75
  60. package/packages/dashboard/src/components/PipelineView.vue +156 -129
  61. package/packages/dashboard/src/components/ProjectCard.vue +187 -0
  62. package/packages/dashboard/src/components/ProjectList.vue +210 -210
  63. package/packages/dashboard/src/components/ProjectOverview.vue +113 -139
  64. package/packages/dashboard/src/components/StageBadge.vue +67 -53
  65. package/packages/dashboard/src/components/StepCard.vue +94 -89
  66. package/packages/dashboard/src/components/VResizeHandle.vue +61 -0
  67. package/packages/dashboard/src/components/detail/DocsDetail.vue +48 -48
  68. package/packages/dashboard/src/components/detail/GitDetail.vue +61 -61
  69. package/packages/dashboard/src/components/detail/TechDetail.vue +43 -43
  70. package/packages/dashboard/src/composables/useDashboard.js +192 -164
  71. package/packages/dashboard/src/composables/useKeyboard.js +119 -119
  72. package/packages/dashboard/src/composables/useLayout.js +131 -0
  73. package/packages/dashboard/src/composables/useWebSocket.js +129 -129
  74. package/packages/dashboard/src/main.js +8 -8
  75. package/packages/dashboard/src/style.css +132 -132
  76. package/packages/dashboard/vite.config.js +18 -18
  77. package/src/brainstorm-postcheck.js +158 -0
  78. package/src/change-list.js +52 -0
  79. package/src/change-risk-profile.js +352 -0
  80. package/src/classify-change.js +73 -0
  81. package/src/constants.js +70 -0
  82. package/src/contract-matrix.js +278 -0
  83. package/src/db.js +201 -0
  84. package/src/endpoint-extractor.js +315 -0
  85. package/src/hooks/claude-pre-tool-use.cjs +125 -0
  86. package/src/hooks/worktree-guard.js +761 -0
  87. package/src/index.js +922 -252
  88. package/src/init.js +431 -375
  89. package/src/knowledge-match.js +130 -0
  90. package/src/migrate.js +117 -117
  91. package/src/modules.js +482 -0
  92. package/src/progress.js +1734 -508
  93. package/src/run.js +3465 -652
  94. package/src/scan-postcheck.js +387 -0
  95. package/src/setup.js +398 -460
  96. package/src/stage-contract.js +700 -0
  97. package/src/stages/archive.js +160 -54
  98. package/src/stages/brainstorm-auto.js +229 -0
  99. package/src/stages/brainstorm.js +645 -239
  100. package/src/stages/doctor.js +365 -312
  101. package/src/stages/execute.js +625 -264
  102. package/src/stages/explore.js +34 -0
  103. package/src/stages/index.js +29 -35
  104. package/src/stages/knowledge.js +498 -0
  105. package/src/stages/plan-postcheck.js +511 -0
  106. package/src/stages/plan.js +582 -279
  107. package/src/stages/propose.js +174 -115
  108. package/src/stages/quick.js +102 -63
  109. package/src/stages/scan.js +558 -141
  110. package/src/stages/status.js +65 -65
  111. package/src/stages/verify.js +322 -135
  112. package/src/sync.js +497 -0
  113. package/src/task-review.js +346 -0
  114. package/src/workflow.js +785 -0
  115. package/src/worktree-apply.js +549 -0
  116. package/src/worktree-deps.js +185 -0
  117. package/src/worktree.js +982 -0
  118. package/templates/workflows/archive-impact.yaml +79 -0
  119. package/templates/workflows/scan-docs.yaml +132 -0
  120. package/test/brainstorm-plan-contract.test.mjs +273 -0
  121. package/test/check-syntax.mjs +26 -0
  122. package/test/cli-top-level-aliases.test.mjs +174 -0
  123. package/test/contract-artifacts.test.mjs +323 -0
  124. package/test/decision-supersede.test.mjs +277 -0
  125. package/test/knowledge-match.test.mjs +231 -0
  126. package/test/plan-execute-contract.test.mjs +330 -0
  127. package/test/plan-optimization.test.mjs +572 -0
  128. package/test/platform-artifacts.test.mjs +190 -0
  129. package/test/platform-failure-samples.test.mjs +199 -0
  130. package/test/platform-recovery-chain.test.mjs +179 -0
  131. package/test/platform-recovery.test.mjs +167 -0
  132. package/test/platform-scan-p0.test.mjs +175 -0
  133. package/test/revision-v1.test.mjs +1145 -0
  134. package/test/run-sanitize-project-name.test.mjs +51 -0
  135. package/test/run-scan-postcheck-fail.test.mjs +64 -0
  136. package/test/run-scan-project-parse.test.mjs +200 -0
  137. package/test/run-tests.mjs +48 -0
  138. package/test/scan-docs-yaml-placeholders.test.mjs +84 -0
  139. package/test/scan-knowledge.test.mjs +175 -0
  140. package/test/scan-paths.test.mjs +68 -0
  141. package/test/scan-postcheck-project-priority.test.mjs +85 -0
  142. package/test/scan-postcheck.test.mjs +197 -0
  143. package/test/scan-workflow-anyfailed-block.test.mjs +52 -0
  144. package/test/spec-dir.test.mjs +206 -0
  145. package/test/stage-contract-failed-post-check.test.mjs +102 -0
  146. package/test/stage-contract.test.mjs +299 -0
  147. package/test/stage-definitions.test.mjs +39 -0
  148. package/test/wait-gates.test.mjs +501 -0
  149. package/test/workflow-spec-base.test.mjs +142 -0
  150. package/test/worktree-deps-provision.test.mjs +148 -0
  151. package/test/worktree-guard.test.mjs +136 -0
  152. package/test/worktree-native-overlay.test.mjs +188 -0
  153. package/.sillyspec/changes/archive/2026-04-08-derive-state/design.md +0 -97
  154. package/.sillyspec/changes/archive/2026-04-08-derive-state/plan.md +0 -51
  155. package/.sillyspec/changes/archive/2026-04-08-derive-state/proposal.md +0 -29
  156. package/.sillyspec/changes/archive/2026-04-08-derive-state/requirements.md +0 -34
  157. package/.sillyspec/changes/archive/2026-04-08-derive-state/tasks.md +0 -13
  158. package/.sillyspec/changes/archive/2026-04-08-derive-state/verify-result.md +0 -43
  159. package/.sillyspec/changes/auto-mode/design.md +0 -50
  160. package/.sillyspec/changes/auto-mode/proposal.md +0 -19
  161. package/.sillyspec/changes/auto-mode/requirements.md +0 -21
  162. package/.sillyspec/changes/auto-mode/tasks.md +0 -7
  163. package/.sillyspec/changes/brainstorm-archive/2026-04-05-dashboard-design.md +0 -206
  164. package/.sillyspec/changes/brainstorm-archive/2026-04-05-unified-docs-design.md +0 -199
  165. package/.sillyspec/changes/dashboard/design.md +0 -219
  166. package/.sillyspec/changes/dashboard/design.md.braindraft +0 -206
  167. package/.sillyspec/changes/run-command-design/design.md +0 -1230
  168. package/.sillyspec/changes/unified-docs-design/design.md +0 -199
  169. package/.sillyspec/docs/sillyspec/scan/.gitkeep +0 -0
  170. package/.sillyspec/knowledge/INDEX.md +0 -8
  171. package/.sillyspec/knowledge/uncategorized.md +0 -3
  172. package/.sillyspec/plans/2026-04-05-dashboard.md +0 -737
  173. package/.sillyspec/projects/sillyspec.yaml +0 -3
  174. package/dist/steps/brainstorm/01-load-context.md +0 -30
  175. package/dist/steps/brainstorm/02-reuse-check.md +0 -6
  176. package/dist/steps/brainstorm/03-prototype-analysis.md +0 -11
  177. package/dist/steps/brainstorm/04-module-split.md +0 -23
  178. package/dist/steps/brainstorm/05-dialog-explore.md +0 -8
  179. package/dist/steps/brainstorm/06-propose-approaches.md +0 -3
  180. package/dist/steps/brainstorm/07-present-design.md +0 -3
  181. package/dist/steps/brainstorm/08-write-design.md +0 -21
  182. package/dist/steps/brainstorm/09-self-review.md +0 -15
  183. package/dist/steps/brainstorm/10-user-confirm.md +0 -3
  184. package/dist/steps/brainstorm/11-output-spec.md +0 -7
  185. package/dist/steps/brainstorm/manifest.yaml +0 -26
  186. package/dist/steps/execute/01-load-context.md +0 -41
  187. package/dist/steps/execute/02-scan-conventions.md +0 -47
  188. package/dist/steps/execute/03-skill-mcp.md +0 -19
  189. package/dist/steps/execute/04-assign-task.md +0 -22
  190. package/dist/steps/execute/04b-prompt-template.md +0 -54
  191. package/dist/steps/execute/05-write-test.md +0 -7
  192. package/dist/steps/execute/06-write-code.md +0 -8
  193. package/dist/steps/execute/07-run-test.md +0 -26
  194. package/dist/steps/execute/08-fix-issues.md +0 -28
  195. package/dist/steps/execute/09-next-task.md +0 -33
  196. package/dist/steps/execute/manifest.yaml +0 -28
  197. package/dist/steps/plan/01-load-context.md +0 -22
  198. package/dist/steps/plan/02-anchor-confirm.md +0 -1
  199. package/dist/steps/plan/03-expand-tasks.md +0 -33
  200. package/dist/steps/plan/04-mark-order.md +0 -15
  201. package/dist/steps/plan/05-e2e-planning.md +0 -17
  202. package/dist/steps/plan/06-self-check.md +0 -16
  203. package/dist/steps/plan/07-save.md +0 -1
  204. package/dist/steps/plan/manifest.yaml +0 -18
  205. package/dist/steps/scan/01-env-detect.md +0 -51
  206. package/dist/steps/scan/02-tech-stack.md +0 -16
  207. package/dist/steps/scan/03-conventions.md +0 -16
  208. package/dist/steps/scan/04-structure.md +0 -19
  209. package/dist/steps/scan/05-quality.md +0 -18
  210. package/dist/steps/scan/06-complete.md +0 -49
  211. package/dist/steps/scan/manifest.yaml +0 -16
  212. package/dist/steps/verify/01-load-specs.md +0 -28
  213. package/dist/steps/verify/02-check-tasks.md +0 -1
  214. package/dist/steps/verify/03-check-design.md +0 -6
  215. package/dist/steps/verify/04-run-tests.md +0 -7
  216. package/dist/steps/verify/05-e2e-tests.md +0 -27
  217. package/dist/steps/verify/05b-e2e-fix.md +0 -33
  218. package/dist/steps/verify/06-code-quality.md +0 -25
  219. package/dist/steps/verify/07-lint-check.md +0 -27
  220. package/dist/steps/verify/08-output-report.md +0 -14
  221. package/dist/steps/verify/manifest.yaml +0 -22
  222. package/packages/dashboard/dist/assets/index-DGe8CqeP.css +0 -1
  223. package/src/derive.js +0 -147
  224. package/src/step.js +0 -543
package/src/index.js CHANGED
@@ -1,252 +1,922 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * SillySpec CLI — 安装工具
5
- *
6
- * 只负责两件事:init(安装命令模板)和 setup(安装 MCP 工具)。
7
- * 状态管理通过 progress.json 完成,使用 `sillyspec progress` 命令。
8
- */
9
- import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
10
- import { join, resolve } from 'path';
11
- import { cmdInit, getVersion } from './init.js';
12
- import { ProgressManager } from './progress.js';
13
-
14
- // ── CLI 入口 ──
15
-
16
- function printUsage() {
17
- console.log(`
18
- SillySpec CLI — 规范驱动开发工具包
19
-
20
- 用法:
21
- sillyspec init 初始化(零交互,自动检测工具)
22
- [--tool <name>] 只安装指定工具
23
- [--interactive] 交互式引导
24
- [--dir <path>] 指定目录
25
- sillyspec setup [--list] 安装推荐 MCP 工具
26
- [--list] 查看已安装状态
27
- sillyspec progress <cmd> 进度管理
28
- init 初始化 progress.json
29
- show 查看当前进度
30
- set-stage <stage> 设置当前阶段
31
- add-step <stage> <name> 添加步骤
32
- update-step <s> <n> --status <st> [--output <t>]
33
- complete-stage <stage> 完成阶段并推进
34
- validate 校验并修复
35
- reset [--stage X] 重置进度
36
- complete --stage X 归档已完成阶段
37
- sillyspec docs migrate 迁移旧文档到统一结构
38
- sillyspec dashboard 启动 Dashboard Web UI
39
- [--port <number>] 指定端口(默认 3456)
40
- [--no-open] 不自动打开浏览器
41
-
42
- 选项:
43
- --json 输出 JSON(给 AI 程序化读取)
44
- --dir <path> 指定项目目录(默认当前目录)
45
-
46
- 示例:
47
- sillyspec init
48
- sillyspec init --tool claude
49
-
50
- sillyspec setup
51
- sillyspec setup --list
52
- sillyspec dashboard
53
- sillyspec dashboard --port 8080 --no-open
54
- `);
55
- }
56
-
57
- async function main() {
58
- const args = process.argv.slice(2);
59
-
60
- if (args[0] === '--version' || args[0] === '-v') {
61
- console.log(getVersion());
62
- process.exit(0);
63
- }
64
-
65
- if (args.length === 0 || args[0] === 'help' || args[0] === '--help' || args[0] === '-h') {
66
- printUsage();
67
- process.exit(0);
68
- }
69
-
70
- // 解析全局选项
71
- let json = false;
72
- let targetDir = process.cwd();
73
- let tool = null;
74
- let interactive = false;
75
- const filteredArgs = [];
76
-
77
- for (let i = 0; i < args.length; i++) {
78
- if (args[i] === '--json') {
79
- json = true;
80
- } else if (args[i] === '--dir' && args[i + 1]) {
81
- targetDir = resolve(args[i + 1]);
82
- i++;
83
- } else if (args[i] === '--tool' && args[i + 1]) {
84
- tool = args[i + 1];
85
- i++;
86
- } else if (args[i] === '--interactive' || args[i] === '-i') {
87
- interactive = true;
88
- } else if (args[i] === '--list' || args[i] === '-l') {
89
- filteredArgs.push('--list');
90
- } else {
91
- filteredArgs.push(args[i]);
92
- }
93
- }
94
-
95
- const command = filteredArgs[0];
96
- // 支持 sillyspec init /path/to/project 语法:如果第二个参数看起来像路径,当作 targetDir
97
- if (command === 'init' && filteredArgs[1] && !filteredArgs[1].startsWith('-')) {
98
- targetDir = resolve(filteredArgs[1]);
99
- filteredArgs.splice(1, 1);
100
- }
101
- const dir = targetDir;
102
-
103
- if (!existsSync(dir)) {
104
- console.error(`❌ 目录不存在: ${dir}`);
105
- process.exit(1);
106
- }
107
-
108
- switch (command) {
109
- case 'init':
110
- await cmdInit(dir, { tool, interactive });
111
- break;
112
- case 'setup':
113
- const setupList = filteredArgs.includes('--list') || filteredArgs.includes('-l');
114
- await (await import('./setup.js')).cmdSetup(dir, { json, list: setupList });
115
- break;
116
- case 'progress': {
117
- const pm = new ProgressManager();
118
- const subCommand = filteredArgs[1];
119
- const stageIdx = filteredArgs.indexOf('--stage');
120
- const stage = stageIdx >= 0 && filteredArgs[stageIdx + 1] ? filteredArgs[stageIdx + 1] : null;
121
-
122
- switch (subCommand) {
123
- case 'init':
124
- pm.init(dir);
125
- break;
126
- case 'status':
127
- case 'show':
128
- pm.show(dir);
129
- break;
130
- case 'validate':
131
- await pm.validate(dir, filteredArgs.includes('--deep'));
132
- break;
133
- case 'reset':
134
- pm.reset(dir, stage);
135
- break;
136
- case 'complete':
137
- pm.complete(dir, stage);
138
- break;
139
- case 'set-stage': {
140
- const setStageName = filteredArgs[2];
141
- if (!setStageName) { console.log('❌ 用法: sillyspec progress set-stage <stage>'); break; }
142
- pm.setStage(dir, setStageName);
143
- break;
144
- }
145
- case 'add-step': {
146
- const addStepStage = filteredArgs[2];
147
- const addStepName = filteredArgs[3];
148
- if (!addStepStage || !addStepName) { console.log('❌ 用法: sillyspec progress add-step <stage> <step-name>'); break; }
149
- pm.addStep(dir, addStepStage, addStepName);
150
- break;
151
- }
152
- case 'update-step': {
153
- const updStepStage = filteredArgs[2];
154
- const updStepName = filteredArgs[3];
155
- if (!updStepStage || !updStepName) { console.log('❌ 用法: sillyspec progress update-step <stage> <step-name> --status <status> [--output <text>]'); break; }
156
- // Parse --status and --output from args
157
- let updStatus = null, updOutput = undefined;
158
- for (let ai = 0; ai < args.length; ai++) {
159
- if (args[ai] === '--status' && args[ai + 1]) { updStatus = args[ai + 1]; ai++; }
160
- if (args[ai] === '--output' && args[ai + 1]) { updOutput = args[ai + 1]; ai++; }
161
- }
162
- pm.updateStep(dir, updStepStage, updStepName, { status: updStatus, output: updOutput });
163
- break;
164
- }
165
- case 'complete-stage': {
166
- const compStageName = filteredArgs[2];
167
- if (!compStageName) { console.log('❌ 用法: sillyspec progress complete-stage <stage>'); break; }
168
- pm.completeStage(dir, compStageName);
169
- break;
170
- }
171
- case 'batch': {
172
- if (filteredArgs.includes('--status')) {
173
- const bp = pm.readBatchProgress(dir);
174
- if (!bp) { console.log('📭 无批量进度数据'); break; }
175
- const line = pm._renderBatchProgress(bp);
176
- console.log(line || '📭 无批量进度数据');
177
- console.log(JSON.stringify(bp, null, 2));
178
- } else {
179
- let batchData = {};
180
- const a = args;
181
- for (let i = 0; i < a.length; i++) {
182
- if (a[i] === '--total' && a[i + 1]) { batchData.total = parseInt(a[i + 1]); i++; }
183
- if (a[i] === '--completed' && a[i + 1]) { batchData.completed = parseInt(a[i + 1]); i++; }
184
- if (a[i] === '--failed' && a[i + 1]) { batchData.failed = parseInt(a[i + 1]); i++; }
185
- if (a[i] === '--skipped' && a[i + 1]) { batchData.skipped = parseInt(a[i + 1]); i++; }
186
- }
187
- if (Object.keys(batchData).length === 0) {
188
- console.log('用法: sillyspec progress batch --total 100 --completed 73');
189
- console.log(' sillyspec progress batch --status');
190
- break;
191
- }
192
- pm.updateBatchProgress(dir, batchData);
193
- console.log('✅ 批量进度已更新');
194
- }
195
- break;
196
- }
197
- default:
198
- console.log('用法: sillyspec progress <init|show|validate|reset|complete|set-stage|add-step|update-step|complete-stage>');
199
- }
200
- break;
201
- }
202
- case 'docs': {
203
- const docsSubCmd = filteredArgs[1];
204
- if (docsSubCmd === 'migrate') {
205
- const { migrateDocs } = await import('./migrate.js');
206
- migrateDocs(dir);
207
- } else {
208
- console.log('用法: sillyspec docs migrate');
209
- }
210
- break;
211
- }
212
- case 'run': {
213
- const { runCommand } = await import('./run.js')
214
- await runCommand(filteredArgs.slice(1), dir)
215
- break
216
- }
217
- case 'step': {
218
- const stepArgs = filteredArgs.slice(1);
219
- if (json) stepArgs.push('--json');
220
- await (await import('./step.js')).cmdStep(dir, stepArgs);
221
- break;
222
- }
223
- case 'dashboard': {
224
- // Parse dashboard options
225
- let port = 3456;
226
- let openBrowser = true;
227
-
228
- for (let i = 1; i < args.length; i++) {
229
- if (args[i] === '--port' && args[i + 1]) {
230
- port = parseInt(args[i + 1], 10);
231
- i++;
232
- } else if (args[i] === '--no-open') {
233
- openBrowser = false;
234
- }
235
- }
236
-
237
- // Import and start dashboard server
238
- const { startServer } = await import('../packages/dashboard/server/index.js');
239
- startServer({ port, open: openBrowser });
240
-
241
- // Keep process alive
242
- console.log(' Ctrl+C 停止服务器');
243
- break;
244
- }
245
- default:
246
- console.error(`❌ 未知命令: ${command}`);
247
- printUsage();
248
- process.exit(1);
249
- }
250
- }
251
-
252
- main();
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * SillySpec CLI — 安装工具
5
+ *
6
+ * 只负责两件事:init(安装命令模板)和 setup(安装 MCP 工具)。
7
+ * 状态管理通过 sillyspec.db(SQLite)完成,使用 `sillyspec progress` 命令。
8
+ */
9
+ import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
10
+ import { join, resolve } from 'path';
11
+ import { cmdInit, getVersion } from './init.js';
12
+ import { ProgressManager } from './progress.js';
13
+
14
+ // ── CLI 入口 ──
15
+
16
+ function printUsage() {
17
+ console.log(`
18
+ SillySpec CLI — 规范驱动开发工具包
19
+
20
+ 用法:
21
+ sillyspec init 初始化(零交互,自动检测工具)
22
+ [--tool <name>] 只安装指定工具
23
+ [--interactive] 交互式引导
24
+ [--dir <path>] 指定目录
25
+ [--spec-dir <path>] 指定规范目录(默认 <项目>/.sillyspec)
26
+
27
+ sillyspec setup [--list] 安装推荐 MCP 工具
28
+ [--list] 查看已安装状态
29
+
30
+ sillyspec run <stage> 执行阶段步骤(核心命令)
31
+ --done --output "..." 完成当前步骤
32
+ --skip 跳过可选步骤
33
+ --status 查看阶段进度
34
+ --reset 重置阶段(从头开始)
35
+ --reopen 重新打开已完成阶段进入修订模式
36
+ --from-step <index|name> 配合 --reopen:从指定步骤开始修订
37
+ --change <name> 设置当前变更名
38
+ --spec-dir <path> 指定规范目录(默认 <项目>/.sillyspec)
39
+ --runtime-root <path> 平台模式:运行时产物根路径
40
+ --workspace-id <id> 平台模式:workspace ID
41
+ --scan-run-id <id> 平台模式:scan run ID
42
+ auto 连续推进 brainstorm→plan→execute→verify
43
+
44
+ 可选阶段:
45
+ scan, brainstorm, plan, execute, verify, archive
46
+ quick, explore, status, doctor
47
+
48
+ Revision mode:
49
+ 已完成阶段不能直接重跑。使用 --reopen --from-step 进入受控修订。
50
+ 重开会使下游阶段自动标记为 stale,但不修改已有产物文件。
51
+
52
+ sillyspec progress <cmd> 进度记录(轻量,不强制顺序)
53
+ init 初始化项目数据库
54
+ show 查看当前进度
55
+ set-stage <stage> 设置当前阶段
56
+ add-step <stage> <name> 添加步骤
57
+ update-step <s> <n> --status <st> [--output <t>]
58
+ complete-stage <stage> 标记阶段完成
59
+ check 状态一致性检查(只报告,不修复)
60
+ repair [--apply] 修复状态元数据(默认 dry-run,--apply 才修改)
61
+ validate 校验并修复
62
+ reset [--stage X] 重置进度
63
+
64
+ sillyspec docs migrate 迁移旧文档到统一结构
65
+
66
+ sillyspec knowledge <cmd> 知识库管理(agent-safe,输出 JSON)
67
+ search --query "<text>" --limit N 搜索知识库
68
+ inspect --id "<id>" 读取知识条目详情
69
+ validate 校验知识库健康度
70
+ refresh 从 scan 文档刷新自动知识(仅写 generated/)
71
+ propose --title "<title>" --category <name> 提议新知识(写入 proposed/)
72
+
73
+ sillyspec platform <cmd> SillyHub 平台同步
74
+ connect <url> [--token <t>] 连接平台
75
+ disconnect 断开连接
76
+ sync [--change <name>] 同步变更状态
77
+ sync-docs [--change <name>] 同步四件套文档
78
+ status 查看同步状态
79
+ approve <change-name> 审批变更
80
+ reject <change-name> [--reason <r>] 拒绝变更
81
+
82
+ sillyspec dashboard 启动 Dashboard Web UI
83
+ [--port <number>] 指定端口(默认 3456)
84
+ [--no-open] 不自动打开浏览器
85
+
86
+ 选项:
87
+ --json 输出 JSON(给 AI 程序化读取)
88
+ --dir <path> 指定项目目录(默认当前目录)
89
+ --spec-dir <path> 指定规范目录(默认 <项目目录>/.sillyspec)
90
+
91
+ 示例:
92
+ sillyspec init
93
+ sillyspec init --spec-dir /data/specs/my-project
94
+ sillyspec run scan
95
+ sillyspec run brainstorm
96
+ sillyspec run quick
97
+ sillyspec run explore
98
+ sillyspec run brainstorm --done --output "需求已澄清"
99
+ sillyspec setup --list
100
+ sillyspec dashboard --port 8080 --no-open
101
+ `);
102
+ }
103
+
104
+ async function main() {
105
+ const args = process.argv.slice(2);
106
+
107
+ if (args[0] === '--version' || args[0] === '-v') {
108
+ console.log(getVersion());
109
+ process.exit(0);
110
+ }
111
+
112
+ if (args.length === 0 || args[0] === 'help' || args[0] === '--help' || args[0] === '-h') {
113
+ printUsage();
114
+ process.exit(0);
115
+ }
116
+
117
+ // 解析全局选项
118
+ let json = false;
119
+ let saveWorkflowRunFlag = false;
120
+ let targetDir = process.cwd();
121
+ let specDir = null;
122
+ let tool = null;
123
+ let interactive = false;
124
+ const filteredArgs = [];
125
+
126
+ for (let i = 0; i < args.length; i++) {
127
+ if (args[i] === '--json') {
128
+ json = true;
129
+ } else if (args[i] === '--save') {
130
+ saveWorkflowRunFlag = true;
131
+ } else if (args[i] === '--dir' && args[i + 1]) {
132
+ targetDir = resolve(args[i + 1]);
133
+ i++;
134
+ } else if (args[i] === '--spec-dir' && args[i + 1]) {
135
+ specDir = resolve(args[i + 1]);
136
+ i++;
137
+ } else if (args[i] === '--tool' && args[i + 1]) {
138
+ tool = args[i + 1];
139
+ i++;
140
+ } else if (args[i] === '--interactive' || args[i] === '-i') {
141
+ interactive = true;
142
+ } else if (args[i] === '--list' || args[i] === '-l') {
143
+ filteredArgs.push('--list');
144
+ } else {
145
+ filteredArgs.push(args[i]);
146
+ }
147
+ }
148
+
149
+ const command = filteredArgs[0];
150
+ // 支持 sillyspec init /path/to/project 语法:如果第二个参数看起来像路径,当作 targetDir
151
+ if (command === 'init' && filteredArgs[1] && !filteredArgs[1].startsWith('-')) {
152
+ targetDir = resolve(filteredArgs[1]);
153
+ filteredArgs.splice(1, 1);
154
+ }
155
+ // ── 自动纠正 cwd ──
156
+ // agent worktree 内跑 pnpm 等工具后 shell cwd 可能被改变,
157
+ // 导致 sillyspec 命令找不到 .sillyspec。此函数尝试从 git root 解析。
158
+ function resolveEffectiveDir(baseDir) {
159
+ if (existsSync(join(baseDir, '.sillyspec'))) return baseDir
160
+ try {
161
+ const { execSync } = require('child_process')
162
+ const gitRoot = execSync('git rev-parse --show-toplevel', {
163
+ cwd: baseDir, encoding: 'utf8', timeout: 5000
164
+ }).trim()
165
+ if (gitRoot && existsSync(join(gitRoot, '.sillyspec'))) return gitRoot
166
+ } catch {}
167
+ return baseDir
168
+ }
169
+
170
+ const dir = targetDir;
171
+
172
+ if (command === 'init' && !existsSync(dir)) {
173
+ const { mkdirSync } = await import('fs');
174
+ mkdirSync(dir, { recursive: true });
175
+ }
176
+
177
+ if (!existsSync(dir)) {
178
+ console.error(`❌ 目录不存在: ${dir}`);
179
+ process.exit(1);
180
+ }
181
+
182
+ switch (command) {
183
+ case 'init':
184
+ await cmdInit(dir, { tool, interactive, specDir });
185
+ break;
186
+ case 'setup':
187
+ const setupList = filteredArgs.includes('--list') || filteredArgs.includes('-l');
188
+ await (await import('./setup.js')).cmdSetup(dir, { json, list: setupList });
189
+ break;
190
+ case 'progress': {
191
+ const pm = new ProgressManager();
192
+ const progDir = specDir ? dir : resolveEffectiveDir(dir);
193
+ const subCommand = filteredArgs[1];
194
+ const stageIdx = filteredArgs.indexOf('--stage');
195
+ const stage = stageIdx >= 0 && filteredArgs[stageIdx + 1] ? filteredArgs[stageIdx + 1] : null;
196
+ // 解析 --change 参数
197
+ const progChangeIdx = args.indexOf('--change');
198
+ const progChangeName = progChangeIdx >= 0 && args[progChangeIdx + 1] ? args[progChangeIdx + 1] : null;
199
+
200
+ switch (subCommand) {
201
+ case 'init':
202
+ pm.init(progDir);
203
+ break;
204
+ case 'status':
205
+ case 'show':
206
+ pm.show(progDir, progChangeName);
207
+ break;
208
+ case 'check':
209
+ await pm.checkConsistency(progDir, progChangeName);
210
+ break;
211
+ case 'repair': {
212
+ const repairApply = filteredArgs.includes('--apply');
213
+ await pm.repairConsistency(progDir, { apply: repairApply, changeName: progChangeName });
214
+ break;
215
+ }
216
+ case 'validate':
217
+ await pm.validate(dir, progChangeName);
218
+ break;
219
+ case 'reset':
220
+ pm.reset(dir, stage, progChangeName);
221
+ break;
222
+ case 'set-stage': {
223
+ const setStageName = filteredArgs[2];
224
+ if (!setStageName) { console.log('❌ 用法: sillyspec progress set-stage <stage> [--change <name>]'); break; }
225
+ pm.setStage(dir, setStageName, progChangeName);
226
+ break;
227
+ }
228
+ case 'add-step': {
229
+ const addStepStage = filteredArgs[2];
230
+ const addStepName = filteredArgs[3];
231
+ if (!addStepStage || !addStepName) { console.log('❌ 用法: sillyspec progress add-step <stage> <step-name> [--change <name>]'); break; }
232
+ pm.addStep(dir, addStepStage, addStepName, progChangeName);
233
+ break;
234
+ }
235
+ case 'update-step': {
236
+ const updStepStage = filteredArgs[2];
237
+ const updStepName = filteredArgs[3];
238
+ if (!updStepStage || !updStepName) { console.log('❌ 用法: sillyspec progress update-step <stage> <step-name> --status <status> [--output <text>] [--change <name>]'); break; }
239
+ let updStatus = null, updOutput = undefined;
240
+ for (let ai = 0; ai < args.length; ai++) {
241
+ if (args[ai] === '--status' && args[ai + 1]) { updStatus = args[ai + 1]; ai++; }
242
+ if (args[ai] === '--output' && args[ai + 1]) { updOutput = args[ai + 1]; ai++; }
243
+ }
244
+ pm.updateStep(dir, updStepStage, updStepName, { status: updStatus, output: updOutput }, progChangeName);
245
+ break;
246
+ }
247
+ case 'complete-stage': {
248
+ const compStageName = filteredArgs[2];
249
+ if (!compStageName) { console.log('❌ 用法: sillyspec progress complete-stage <stage>'); break; }
250
+ pm.completeStage(dir, compStageName, progChangeName);
251
+ break;
252
+ }
253
+ case 'batch': {
254
+ if (filteredArgs.includes('--status')) {
255
+ const bp = pm.readBatchProgress(dir, progChangeName);
256
+ if (!bp) { console.log('📭 无批量进度数据'); break; }
257
+ const line = pm._renderBatchProgress(bp);
258
+ console.log(line || '📭 无批量进度数据');
259
+ console.log(JSON.stringify(bp, null, 2));
260
+ } else {
261
+ let batchData = {};
262
+ const a = args;
263
+ for (let i = 0; i < a.length; i++) {
264
+ if (a[i] === '--total' && a[i + 1]) { batchData.total = parseInt(a[i + 1]); i++; }
265
+ if (a[i] === '--completed' && a[i + 1]) { batchData.completed = parseInt(a[i + 1]); i++; }
266
+ if (a[i] === '--failed' && a[i + 1]) { batchData.failed = parseInt(a[i + 1]); i++; }
267
+ if (a[i] === '--skipped' && a[i + 1]) { batchData.skipped = parseInt(a[i + 1]); i++; }
268
+ }
269
+ if (Object.keys(batchData).length === 0) {
270
+ console.log('用法: sillyspec progress batch --total 100 --completed 73');
271
+ console.log(' sillyspec progress batch --status');
272
+ break;
273
+ }
274
+ pm.updateBatchProgress(dir, batchData, progChangeName);
275
+ console.log('✅ 批量进度已更新');
276
+ }
277
+ break;
278
+ }
279
+ default:
280
+ console.log('用法: sillyspec progress <init|show|validate|reset|set-stage|add-step|update-step|complete-stage>');
281
+ }
282
+ break;
283
+ }
284
+ case 'docs': {
285
+ const docsSubCmd = filteredArgs[1];
286
+ if (docsSubCmd === 'migrate') {
287
+ const { migrateDocs } = await import('./migrate.js');
288
+ migrateDocs(dir);
289
+ } else {
290
+ console.log('用法: sillyspec docs migrate');
291
+ }
292
+ break;
293
+ }
294
+ case 'run': {
295
+ const { runCommand } = await import('./run.js')
296
+ // 平台模式(--spec-dir 已指定)时,--dir 是明确的 source_root,不应被 resolveEffectiveDir 纠正
297
+ const effectiveDir = specDir ? dir : resolveEffectiveDir(dir)
298
+ await runCommand(filteredArgs.slice(1), effectiveDir, specDir)
299
+ break
300
+ }
301
+ // task-10: 顶层命令别名,转发 runCommand,与 case 'run': 路径行为一致
302
+ // help 文本(:44-46)已宣称这些 stage 可直接使用,这里补齐路由避免落 default 分支。
303
+ // 注意:filteredArgs[0] === command,直接透传 filteredArgs 即可让 runCommand
304
+ // 从 args[0] 取到 stage 名(run.js:1036)。与 case 'run': 的 filteredArgs.slice(1)
305
+ // 区别只在于 slice(1) 去掉的是 'run' 字面量,这里 command 本身就是 stage 名不能丢。
306
+ case 'doctor':
307
+ case 'scan':
308
+ case 'status':
309
+ case 'quick':
310
+ case 'explore': {
311
+ const { runCommand } = await import('./run.js')
312
+ const stageArgs = [command, ...filteredArgs.slice(1)]
313
+ const effectiveDir = specDir ? dir : resolveEffectiveDir(dir)
314
+ await runCommand(stageArgs, effectiveDir, specDir)
315
+ break
316
+ }
317
+ case 'knowledge': {
318
+ const { cmdKnowledge } = await import('./stages/knowledge.js')
319
+ await cmdKnowledge(filteredArgs.slice(1), specDir ? dir : resolveEffectiveDir(dir), { specDir })
320
+ break
321
+ }
322
+ case 'dashboard': {
323
+ // Parse dashboard options
324
+ let port = 3456;
325
+ let openBrowser = true;
326
+
327
+ for (let i = 1; i < args.length; i++) {
328
+ if (args[i] === '--port' && args[i + 1]) {
329
+ port = parseInt(args[i + 1], 10);
330
+ i++;
331
+ } else if (args[i] === '--no-open') {
332
+ openBrowser = false;
333
+ }
334
+ }
335
+
336
+ // Import and start dashboard server
337
+ const { startServer } = await import('../packages/dashboard/server/index.js');
338
+ startServer({ port, open: openBrowser });
339
+
340
+ // Keep process alive
341
+ console.log('按 Ctrl+C 停止服务器');
342
+ break;
343
+ }
344
+ case 'worktree': {
345
+ const { WorktreeManager } = await import('./worktree.js');
346
+ const { ProgressManager } = await import('./progress.js');
347
+ const wtSubCmd = filteredArgs[1];
348
+ const wtName = filteredArgs.slice(2).find(a => !a.startsWith('-'));
349
+ const wm = new WorktreeManager({ cwd: dir });
350
+ const pm = new ProgressManager({ specDir });
351
+
352
+ // isolation 写入 DB 的辅助函数
353
+ async function _writeIsolationToDB(cwd, changeName, info) {
354
+ if (info.blocked) {
355
+ await pm.updateChangeIsolation(cwd, changeName, {
356
+ status: 'blocked',
357
+ mode: null,
358
+ reason: info.reason,
359
+ });
360
+ } else {
361
+ const mode = info.mode || 'worktree';
362
+ const statusMap = { 'worktree': 'verified', 'native-worktree': 'verified', 'in-place-fallback': 'degraded' };
363
+ await pm.updateChangeIsolation(cwd, changeName, {
364
+ status: statusMap[mode] || 'verified',
365
+ mode,
366
+ });
367
+ }
368
+ }
369
+
370
+ if (!wtSubCmd || wtSubCmd === 'help' || wtSubCmd === '--help' || wtSubCmd === '-h') {
371
+ console.log(`
372
+ SillySpec worktree — git worktree 隔离管理
373
+
374
+ 用法:
375
+ sillyspec worktree create <change-name> [--base <branch>] 创建隔离 worktree
376
+ sillyspec worktree apply <change-name> [--check-only] 校验并应用变更到主工作区
377
+ sillyspec worktree assess <change-name> 风险审计 + 自动 apply
378
+ sillyspec worktree list 列出所有活跃 worktree
379
+ sillyspec worktree meta <change-name> 读取 worktree meta.json
380
+ sillyspec worktree cleanup <change-name> [--force] 强制清理 worktree
381
+ sillyspec worktree doctor [--fix] [--stale-hours N] 健康检查 + 修复
382
+
383
+ 选项:
384
+ --base <branch> create: 指定基础分支(默认当前 HEAD)
385
+ --check-only apply: 只输出检查结果,不实际 apply
386
+ `);
387
+ break;
388
+ }
389
+
390
+ switch (wtSubCmd) {
391
+ case 'create': {
392
+ if (!wtName) {
393
+ console.error('❌ 用法: sillyspec worktree create <change-name> [--base <branch>]');
394
+ process.exit(1);
395
+ }
396
+ const baseIdx = args.indexOf('--base');
397
+ const base = baseIdx >= 0 && args[baseIdx + 1] ? args[baseIdx + 1] : undefined;
398
+ try {
399
+ const info = wm.create(wtName, { base });
400
+ console.log(`✅ worktree 已创建`);
401
+ console.log(` 分支: ${info.branch}`);
402
+ console.log(` 路径: ${info.worktreePath}`);
403
+ console.log(` 基准: ${info.baseHash.slice(0, 8)}`);
404
+ if (info.mode) {
405
+ console.log(` 模式: ${info.mode}`);
406
+ }
407
+ // 写入 isolation 信息到 gate-status.json
408
+ await _writeIsolationToDB(dir, wtName, info);
409
+ } catch (e) {
410
+ console.error(`❌ ${e.message}`);
411
+ // 写入 blocked 状态到 gate-status.json
412
+ await _writeIsolationToDB(dir, wtName, { blocked: true, reason: e.message });
413
+ process.exit(1);
414
+ }
415
+ break;
416
+ }
417
+ case 'apply': {
418
+ if (!wtName) {
419
+ console.error('❌ 用法: sillyspec worktree apply <change-name> [--check-only]');
420
+ process.exit(1);
421
+ }
422
+ const checkOnly = args.includes('--check-only');
423
+ const { applyWorktree } = await import('./worktree-apply.js');
424
+ const result = applyWorktree(wtName, { cwd: dir, checkOnly });
425
+
426
+ if (result.errors.length > 0) {
427
+ console.error(`❌ 校验失败:`);
428
+ for (const err of result.errors) {
429
+ console.error(` ${err}`);
430
+ }
431
+ process.exit(1);
432
+ }
433
+
434
+ if (result.changedFiles.length === 0) {
435
+ console.log('📭 无变更需要应用');
436
+ break;
437
+ }
438
+
439
+ if (checkOnly) {
440
+ console.log(`✅ 检查通过 (${result.changedFiles.length} 个文件):`);
441
+ for (const f of result.changedFiles) {
442
+ console.log(` ${f}`);
443
+ }
444
+ } else {
445
+ console.log(`✅ 已应用 ${result.changedFiles.length} 个文件变更`);
446
+ }
447
+ if (result.warnings && result.warnings.length > 0) {
448
+ for (const w of result.warnings) {
449
+ console.log(`⚠️ ${w}`);
450
+ }
451
+ }
452
+ break;
453
+ }
454
+ case 'assess': {
455
+ if (!wtName) {
456
+ console.error('❌ 用法: sillyspec worktree assess <change-name>');
457
+ process.exit(1);
458
+ }
459
+ const { assessApplyRisk } = await import('./worktree-apply.js');
460
+ const assessment = assessApplyRisk(wtName, { cwd: dir });
461
+
462
+ const SEPARATOR = '─'.repeat(32);
463
+ console.log('Worktree Apply Decision');
464
+ console.log(SEPARATOR);
465
+ const decisionIcon = assessment.decision === 'SAFE' ? '✅' : assessment.decision === 'WARNING' ? '⚠️ ' : '🚫';
466
+ console.log(`Decision: ${decisionIcon} ${assessment.decision}`);
467
+ console.log(`Changed files: ${assessment.changedFiles.length}`);
468
+ if (assessment.stats.additions > 0 || assessment.stats.deletions > 0) {
469
+ console.log(`Additions: +${assessment.stats.additions} Deletions: -${assessment.stats.deletions}`);
470
+ }
471
+
472
+ if (assessment.reasons.length > 0) {
473
+ console.log('');
474
+ console.log('Blocked reasons:');
475
+ for (const r of assessment.reasons) r.split('\n').forEach(l => console.log(` ${l}`));
476
+ }
477
+ if (assessment.warnings.length > 0) {
478
+ console.log('');
479
+ console.log('Warnings:');
480
+ for (const w of assessment.warnings) console.log(` ⚠️ ${w}`);
481
+ }
482
+
483
+ console.log('');
484
+ if (assessment.decision === 'SAFE' || assessment.decision === 'WARNING') {
485
+ console.log('Action: auto-applying...');
486
+ const { applyWorktree } = await import('./worktree-apply.js');
487
+ const applyResult = applyWorktree(wtName, { cwd: dir });
488
+ if (applyResult.errors.length > 0) {
489
+ console.error('❌ apply 失败:', applyResult.errors.join('; '));
490
+ } else {
491
+ console.log(`✅ 已自动应用 ${applyResult.changedFiles.length} 个文件变更`);
492
+ }
493
+ } else {
494
+ console.log('Action: blocked');
495
+ console.log(' → 检查变更: sillyspec worktree diff ' + wtName);
496
+ console.log(' → 丢弃变更: sillyspec worktree cleanup ' + wtName);
497
+ }
498
+ break;
499
+ }
500
+ case 'meta': {
501
+ if (!wtName) {
502
+ console.error('❌ 用法: sillyspec worktree meta <change-name>');
503
+ process.exit(1);
504
+ }
505
+ const meta = wm.getMeta(wtName);
506
+ if (!meta) {
507
+ console.error(`❌ 未找到 worktree meta: ${wtName}(可能未创建或已被清理)`);
508
+ process.exit(1);
509
+ }
510
+ console.log(`change: ${wtName}`);
511
+ console.log(`worktreePath: ${meta.worktreePath || '(未设置)'}`);
512
+ console.log(`branch: ${meta.branch || '(未设置)'}`);
513
+ console.log(`mode: ${meta.mode || '(未设置)'}`);
514
+ if (meta.baseBranch) console.log(`baseBranch: ${meta.baseBranch}`);
515
+ if (meta.baseHash) console.log(`baseHash: ${String(meta.baseHash).slice(0, 8)}`);
516
+ console.log('');
517
+ console.log('JSON:');
518
+ console.log(JSON.stringify(meta, null, 2));
519
+ break;
520
+ }
521
+ case 'list': {
522
+ const items = wm.list();
523
+ if (items.length === 0) {
524
+ console.log('📭 无活跃 worktree');
525
+ break;
526
+ }
527
+ // 计算列宽
528
+ const maxName = Math.max('Change Name'.length, ...items.map(i => i.changeName.length));
529
+ const maxBranch = Math.max('Branch'.length, ...items.map(i => i.branch.length));
530
+ const header = ` ${'Change Name'.padEnd(maxName)} ${'Branch'.padEnd(maxBranch)} Created`;
531
+ const sep = ` ${'─'.repeat(maxName)} ${'─'.repeat(maxBranch)} ${'─'.repeat(19)}`;
532
+ console.log(header);
533
+ console.log(sep);
534
+ for (const item of items) {
535
+ const created = item.createdAt ? item.createdAt.replace('T', ' ').replace('Z', '').slice(0, 19) : '-';
536
+ console.log(` ${item.changeName.padEnd(maxName)} ${item.branch.padEnd(maxBranch)} ${created}`);
537
+ }
538
+ break;
539
+ }
540
+ case 'cleanup': {
541
+ if (!wtName) {
542
+ console.error('❌ 用法: sillyspec worktree cleanup <change-name>');
543
+ process.exit(1);
544
+ }
545
+ const forceFlag = args.includes('--force');
546
+ try {
547
+ const result = wm.cleanup(wtName, { force: forceFlag });
548
+ if (result.result === 'cleaned' || result.result === 'force-cleaned') {
549
+ console.log(`✅ worktree 已清理: ${wtName} (mode: ${result.mode})`);
550
+ if (result.details?.length > 0) {
551
+ for (const d of result.details) {
552
+ if (d.startsWith('⚠️')) console.log(` ${d}`);
553
+ }
554
+ }
555
+ } else if (result.result === 'skipped') {
556
+ console.log(`⏭️ worktree 跳过清理: ${wtName} (mode: ${result.mode})`);
557
+ console.log(` 原因: in-place 模式没有隔离目录需要清理`);
558
+ } else {
559
+ console.log(`ℹ️ worktree 未找到: ${wtName}`);
560
+ }
561
+ } catch (e) {
562
+ console.error(`❌ ${e.message}`);
563
+ process.exit(1);
564
+ }
565
+ break;
566
+ }
567
+ case 'doctor': {
568
+ const fixFlag = args.includes('--fix');
569
+ const staleIdx = args.indexOf('--stale-hours');
570
+ const staleHours = staleIdx !== -1 && args[staleIdx + 1] ? parseInt(args[staleIdx + 1], 10) : 24;
571
+ const diag = wm.doctor({ fix: fixFlag, staleHours });
572
+ if (diag.issues.length === 0) {
573
+ console.log('✅ worktree 健康检查通过,无异常');
574
+ } else {
575
+ console.log(`🔍 发现 ${diag.issues.length} 个问题:\n`);
576
+ for (const issue of diag.issues) {
577
+ const icon = issue.fixable ? '⚠️' : '❌';
578
+ console.log(` ${icon} [${issue.type}] ${issue.name}: ${issue.detail}`);
579
+ }
580
+ if (fixFlag) {
581
+ console.log(`\n🔧 修复完成:`);
582
+ for (const f of diag.fixed) console.log(` ✅ ${f}`);
583
+ if (diag.unfixable.length > 0) {
584
+ for (const u of diag.unfixable) console.log(` ❌ ${u}`);
585
+ }
586
+ if (diag.fixed.length === 0 && diag.unfixable.length === 0) {
587
+ console.log(' 无需修复');
588
+ }
589
+ } else {
590
+ console.log(`\n💡 运行 sillyspec worktree doctor --fix 自动修复`);
591
+ }
592
+ }
593
+ break;
594
+ }
595
+ default:
596
+ console.error(`❌ 未知子命令: worktree ${wtSubCmd}`);
597
+ console.log(' 运行 sillyspec worktree --help 查看帮助');
598
+ process.exit(1);
599
+ }
600
+ break;
601
+ }
602
+ case 'platform': {
603
+ const platformSub = filteredArgs[1];
604
+ const platformArgs = filteredArgs.slice(2);
605
+
606
+ if (!platformSub || platformSub === 'help' || platformSub === '--help' || platformSub === '-h') {
607
+ console.log(`
608
+ SillySpec platform — SillyHub 平台同步
609
+
610
+ 用法:
611
+ sillyspec platform connect <url> [--token <token>]
612
+ sillyspec platform disconnect
613
+ sillyspec platform sync [--change <name>]
614
+ sillyspec platform sync-docs [--change <name>]
615
+ sillyspec platform status
616
+ sillyspec platform pointer [--cleanup]
617
+ sillyspec platform approve <change-name>
618
+ sillyspec platform reject <change-name> [--reason <reason>]
619
+ `);
620
+ break;
621
+ }
622
+
623
+ let syncModule;
624
+ try {
625
+ syncModule = await import('./sync.js');
626
+ } catch {
627
+ console.error('❌ 平台同步功能不可用(sync.js 未实现)');
628
+ process.exit(1);
629
+ }
630
+
631
+ switch (platformSub) {
632
+ case 'pointer': {
633
+ // 指针状态检查(不依赖 sync 模块)
634
+ const { readFileSync, existsSync } = await import('fs')
635
+ const { join } = await import('path')
636
+ const { POINTER_STATUS, isPointerStale, isPointerCorrupted } = await import('./constants.js')
637
+ const pointerPath = join(dir, '.sillyspec-platform.json')
638
+
639
+ if (!existsSync(pointerPath)) {
640
+ console.log('ℹ️ 无平台指针文件。当前不在平台模式或未进行过平台 scan。')
641
+ break
642
+ }
643
+
644
+ try {
645
+ const pointer = JSON.parse(readFileSync(pointerPath, 'utf8'))
646
+ console.log(`📄 指针文件: ${pointerPath}`)
647
+ console.log(` specRoot: ${pointer.specRoot || '(缺失 ❌)'}`)
648
+ console.log(` runtimeRoot: ${pointer.runtimeRoot || '(未设置)'}`)
649
+ console.log(` workspaceId: ${pointer.workspaceId || '(未设置)'}`)
650
+ console.log(` scanRunId: ${pointer.scanRunId || '(未设置)'}`)
651
+ console.log(` savedAt: ${pointer.savedAt || '(未知)'}`)
652
+
653
+ if (isPointerCorrupted(pointer)) {
654
+ console.log(` 状态: ${POINTER_STATUS.CORRUPTED} ❌`)
655
+ console.log(` ⚠️ 指针损坏(缺少 specRoot),建议删除后重新运行平台 scan。`)
656
+ if (platformArgs.includes('--cleanup')) {
657
+ const { unlinkSync } = await import('fs')
658
+ unlinkSync(pointerPath)
659
+ console.log(` 🗑️ 已清理损坏指针。`)
660
+ }
661
+ } else if (pointer.status === POINTER_STATUS.SCAN_COMPLETED) {
662
+ if (isPointerStale(pointer)) {
663
+ console.log(` 状态: ${POINTER_STATUS.STALE} ⚠️`)
664
+ console.log(` completedAt: ${pointer.completedAt}`)
665
+ console.log(` scanStatus: ${pointer.scanStatus || '(未知)'}`)
666
+ console.log(` ⚠️ 指针已过时(完成超过 24h),可以安全删除。`)
667
+ if (platformArgs.includes('--cleanup')) {
668
+ const { unlinkSync } = await import('fs')
669
+ unlinkSync(pointerPath)
670
+ console.log(` 🗑️ 已清理过时指针。`)
671
+ }
672
+ } else {
673
+ console.log(` 状态: ${pointer.status} ✅`)
674
+ console.log(` completedAt: ${pointer.completedAt}`)
675
+ console.log(` scanStatus: ${pointer.scanStatus || '(未知)'}`)
676
+ }
677
+ } else {
678
+ console.log(` 状态: ${POINTER_STATUS.ACTIVE} 🔄`)
679
+ }
680
+ } catch (e) {
681
+ console.log(` 状态: ${POINTER_STATUS.CORRUPTED} ❌`)
682
+ console.log(` ⚠️ 指针文件损坏: ${e.message}`)
683
+ }
684
+ break;
685
+ }
686
+ case 'connect': {
687
+ const url = platformArgs[0];
688
+ if (!url) {
689
+ console.error('❌ 用法: sillyspec platform connect <url> [--token <token>]');
690
+ process.exit(1);
691
+ }
692
+ const tokenIdx = args.indexOf('--token');
693
+ const token = tokenIdx >= 0 && args[tokenIdx + 1] ? args[tokenIdx + 1] : undefined;
694
+ if (!token) {
695
+ console.error('⚠️ 未提供 --token,将使用交互式输入(TODO: task-11)');
696
+ }
697
+ await syncModule.connect(url, token, dir);
698
+ break;
699
+ }
700
+ case 'disconnect':
701
+ await syncModule.disconnect(dir);
702
+ break;
703
+ case 'sync': {
704
+ const syncChangeIdx = args.indexOf('--change');
705
+ const syncChangeName = syncChangeIdx >= 0 && args[syncChangeIdx + 1] ? args[syncChangeIdx + 1] : null;
706
+ await syncModule.sync(syncChangeName, dir);
707
+ break;
708
+ }
709
+ case 'sync-docs': {
710
+ const syncDocsChangeIdx = args.indexOf('--change');
711
+ const syncDocsChangeName = syncDocsChangeIdx >= 0 && args[syncDocsChangeIdx + 1] ? args[syncDocsChangeIdx + 1] : null;
712
+ await syncModule.syncDocuments(syncDocsChangeName, dir);
713
+ break;
714
+ }
715
+ case 'status':
716
+ await syncModule.status(dir);
717
+ break;
718
+ case 'approve': {
719
+ const approveName = platformArgs[0];
720
+ if (!approveName) {
721
+ console.error('❌ 用法: sillyspec platform approve <change-name>');
722
+ process.exit(1);
723
+ }
724
+ await syncModule.approve(approveName, dir);
725
+ break;
726
+ }
727
+ case 'reject': {
728
+ const rejectName = platformArgs[0];
729
+ if (!rejectName) {
730
+ console.error('❌ 用法: sillyspec platform reject <change-name> [--reason <reason>]');
731
+ process.exit(1);
732
+ }
733
+ const reasonIdx = args.indexOf('--reason');
734
+ const reason = reasonIdx >= 0 && args[reasonIdx + 1] ? args[reasonIdx + 1] : undefined;
735
+ await syncModule.reject(rejectName, reason, dir);
736
+ break;
737
+ }
738
+ default:
739
+ console.error(`❌ 未知子命令: platform ${platformSub}`);
740
+ console.log(' 运行 sillyspec platform --help 查看帮助');
741
+ process.exit(1);
742
+ }
743
+ break;
744
+ }
745
+ case 'change-rename': {
746
+ const oldName = filteredArgs[1];
747
+ const newName = filteredArgs[2];
748
+ if (!oldName || !newName) {
749
+ console.error('❌ 用法: sillyspec change-rename <旧变更名> <新变更名>');
750
+ process.exit(1);
751
+ }
752
+ const pm = new ProgressManager({ specDir });
753
+ await pm.renameChange(dir, oldName, newName);
754
+ break;
755
+ }
756
+ case 'workflow': {
757
+ const wfSub = filteredArgs[1];
758
+ if (!wfSub || wfSub === 'help' || wfSub === '--help') {
759
+ console.log(`
760
+ SillySpec workflow — 工作流管理
761
+
762
+ 用法:
763
+ sillyspec workflow check <name> [--project <project>] [--json]
764
+ sillyspec workflow list
765
+ `);
766
+ break;
767
+ }
768
+ if (wfSub === 'list') {
769
+ const { listWorkflows } = await import('./workflow.js');
770
+ const names = listWorkflows(dir);
771
+ if (names.length === 0) {
772
+ console.log('未找到 workflow 定义(.sillyspec/workflows/*.yaml)');
773
+ } else {
774
+ console.log(`可用 workflow:`);
775
+ for (const name of names) {
776
+ const { loadWorkflow } = await import('./workflow.js');
777
+ const wf = loadWorkflow(dir, name);
778
+ const specVer = wf?.spec_version || wf?.version || '?';
779
+ const mode = wf?.orchestration?.mode || '?';
780
+ const roles = wf?.roles?.length || 0;
781
+ console.log(` ${name} (spec v${specVer}, ${mode}, ${roles} roles)`);
782
+ }
783
+ }
784
+ break;
785
+ }
786
+ if (wfSub === 'check') {
787
+ const { loadWorkflow, runPostCheck, listWorkflows, saveWorkflowRun } = await import('./workflow.js');
788
+ const wfName = filteredArgs[2];
789
+ if (!wfName) {
790
+ console.error('❌ 请指定 workflow 名称,例如:sillyspec workflow check scan-docs --project sillyspec');
791
+ process.exit(2);
792
+ }
793
+ const wf = loadWorkflow(dir, wfName);
794
+ if (!wf) {
795
+ console.error(`❌ 未找到 workflow: ${wfName}`);
796
+ console.error(`可用 workflow:${listWorkflows(dir).join(', ') || '无'}`);
797
+ process.exit(2);
798
+ }
799
+ // depends_on 校验
800
+ if (wf._validationErrors && wf._validationErrors.length > 0) {
801
+ console.error('❌ workflow YAML 校验失败:');
802
+ for (const err of wf._validationErrors) {
803
+ console.error(` ${err}`);
804
+ }
805
+ process.exit(2);
806
+ }
807
+ // spec_version 校验
808
+ const specVer = wf.spec_version || wf.version;
809
+ if (!specVer) {
810
+ console.error('❌ workflow YAML 缺少 spec_version 字段');
811
+ process.exit(2);
812
+ }
813
+ const SUPPORTED_SPECS = [1];
814
+ if (!SUPPORTED_SPECS.includes(specVer)) {
815
+ console.error(`❌ 不支持的 spec_version: ${specVer}(支持: ${SUPPORTED_SPECS.join(', ')})`);
816
+ process.exit(2);
817
+ }
818
+ // 解析 --project
819
+ const projectIdx = filteredArgs.indexOf('--project');
820
+ const project = projectIdx !== -1 && filteredArgs[projectIdx + 1] ? filteredArgs[projectIdx + 1] : null;
821
+ // 解析 --json(已在顶层解析)
822
+ const isJson = json;
823
+ // 解析 --change
824
+ const changeIdx = filteredArgs.indexOf('--change');
825
+ const changeName = changeIdx !== -1 && filteredArgs[changeIdx + 1] ? filteredArgs[changeIdx + 1] : null;
826
+
827
+ if (!project && wfName !== 'archive-impact') {
828
+ console.error('❌ 请指定 --project,例如:--project sillyspec');
829
+ process.exit(2);
830
+ }
831
+
832
+ // 执行检查
833
+ let resolvedWf = wf;
834
+ const placeholders = {};
835
+ if (changeName) placeholders['change-name'] = changeName;
836
+ // 替换占位符
837
+ let jsonStr = JSON.stringify(resolvedWf);
838
+ if (changeName) jsonStr = jsonStr.replace(/<change-name>/g, changeName);
839
+ resolvedWf = JSON.parse(jsonStr);
840
+
841
+ const projectName = project || 'sillyspec';
842
+ const result = runPostCheck(resolvedWf, dir, projectName, placeholders);
843
+
844
+ if (isJson) {
845
+ console.log(JSON.stringify(result, null, 2));
846
+ } else {
847
+ // 带项目维度前缀的输出(从统一结果对象格式化)
848
+ const lines = [`\n📋 Workflow Post-Check: ${result.workflow} (project: ${result.project})\n`];
849
+ for (const r of (result.roles || [])) {
850
+ const icon = r.status === 'pass' ? '✅' : '❌';
851
+ lines.push(`${icon} [${result.project}] ${r.name} (${r.id})`);
852
+ const roleFailures = (result.failures || []).filter(f => f.role_id === r.id);
853
+ for (const f of roleFailures) {
854
+ lines.push(` └─ ${f.message}`);
855
+ }
856
+ }
857
+ const wfFailures = (result.workflow_checks || []).filter(c => c.status === 'fail');
858
+ if (wfFailures.length > 0) {
859
+ lines.push('');
860
+ for (const f of wfFailures) {
861
+ lines.push(`❌ [${result.project}] 全局: ${f.detail}`);
862
+ }
863
+ }
864
+ lines.push('');
865
+ if (result.status === 'pass') {
866
+ lines.push('✅ 全部检查通过');
867
+ } else {
868
+ lines.push('❌ 存在失败项');
869
+ }
870
+ console.log(lines.join('\n'));
871
+ }
872
+
873
+ // exit code: 0=通过, 1=检查失败, 2=参数/YAML错误
874
+ if (saveWorkflowRunFlag) {
875
+ const saved = saveWorkflowRun(result, { cwd: dir, source: 'cli' });
876
+ if (saved) {
877
+ if (!isJson) console.log(`\n📁 结果已归档:${saved}`);
878
+ }
879
+ }
880
+ process.exit(result.status === 'pass' ? 0 : 1);
881
+ } else {
882
+ console.error(`❌ 未知子命令: workflow ${wfSub}`);
883
+ process.exit(1);
884
+ }
885
+ break;
886
+ }
887
+ case 'modules': {
888
+ const modulesSub = filteredArgs[1];
889
+ if (!modulesSub || modulesSub === 'help' || modulesSub === '--help') {
890
+ console.log(`
891
+ SillySpec modules — 模块文档管理
892
+
893
+ 用法:
894
+ sillyspec modules rebuild 从模块卡片 + 源码重建 _module-map.yaml
895
+ sillyspec modules status 显示模块索引状态
896
+ sillyspec modules migrate 旧格式模块文档迁移到新格式
897
+ `);
898
+ break;
899
+ }
900
+ if (modulesSub === 'rebuild') {
901
+ const { rebuildModuleMap } = await import('./modules.js');
902
+ await rebuildModuleMap(dir);
903
+ } else if (modulesSub === 'status') {
904
+ const { showModuleStatus } = await import('./modules.js');
905
+ await showModuleStatus(dir);
906
+ } else if (modulesSub === 'migrate') {
907
+ const { migrateModuleDocs } = await import('./modules.js');
908
+ await migrateModuleDocs(dir);
909
+ } else {
910
+ console.error(`❌ 未知子命令: modules ${modulesSub}`);
911
+ process.exit(1);
912
+ }
913
+ break;
914
+ }
915
+ default:
916
+ console.error(`❌ 未知命令: ${command}`);
917
+ printUsage();
918
+ process.exit(1);
919
+ }
920
+ }
921
+
922
+ main();