sillyspec 3.9.0 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +17 -0
  2. package/.claude/skills/sillyspec-auto/SKILL.md +78 -0
  3. package/.claude/skills/sillyspec-brainstorm/SKILL.md +17 -0
  4. package/{templates/commit.md → .claude/skills/sillyspec-commit/SKILL.md} +32 -47
  5. package/.claude/skills/sillyspec-continue/SKILL.md +45 -0
  6. package/.claude/skills/sillyspec-doctor/SKILL.md +27 -0
  7. package/.claude/skills/sillyspec-execute/SKILL.md +17 -0
  8. package/.claude/skills/sillyspec-explore/SKILL.md +105 -0
  9. package/.claude/skills/sillyspec-export/SKILL.md +53 -0
  10. package/.claude/skills/sillyspec-init/SKILL.md +170 -0
  11. package/.claude/skills/sillyspec-plan/SKILL.md +17 -0
  12. package/.claude/skills/sillyspec-propose/SKILL.md +17 -0
  13. package/.claude/skills/sillyspec-quick/SKILL.md +17 -0
  14. package/.claude/skills/sillyspec-resume/SKILL.md +111 -0
  15. package/.claude/skills/sillyspec-scan/SKILL.md +17 -0
  16. package/.claude/skills/sillyspec-state/SKILL.md +54 -0
  17. package/.claude/skills/sillyspec-status/SKILL.md +17 -0
  18. package/.claude/skills/sillyspec-verify/SKILL.md +17 -0
  19. package/.claude/skills/sillyspec-workspace/SKILL.md +149 -0
  20. package/README.md +19 -11
  21. package/SKILL.md +15 -10
  22. package/package.json +7 -9
  23. package/packages/dashboard/dist/assets/index-BcM2J-hv.css +1 -0
  24. package/packages/dashboard/dist/assets/index-DpLHK4jv.js +7446 -0
  25. package/packages/dashboard/dist/index.html +16 -16
  26. package/packages/dashboard/dist/prototype-dashboard.html +836 -0
  27. package/packages/dashboard/dist/prototype-overview.html +256 -0
  28. package/packages/dashboard/package-lock.json +226 -6
  29. package/packages/dashboard/package.json +8 -5
  30. package/packages/dashboard/public/logo.jpg +0 -0
  31. package/packages/dashboard/public/prototype-dashboard.html +836 -0
  32. package/packages/dashboard/public/prototype-overview.html +256 -0
  33. package/packages/dashboard/server/executor.js +1 -1
  34. package/packages/dashboard/server/index.js +341 -113
  35. package/packages/dashboard/server/parser.js +442 -30
  36. package/packages/dashboard/server/watcher.js +214 -134
  37. package/packages/dashboard/src/App.vue +475 -71
  38. package/packages/dashboard/src/components/ActionBar.vue +36 -43
  39. package/packages/dashboard/src/components/CommandPalette.vue +45 -66
  40. package/packages/dashboard/src/components/DetailPanel.vue +68 -53
  41. package/packages/dashboard/src/components/DocPreview.vue +257 -0
  42. package/packages/dashboard/src/components/DocTree.vue +114 -0
  43. package/packages/dashboard/src/components/HResizeHandle.vue +48 -0
  44. package/packages/dashboard/src/components/LogStream.vue +13 -33
  45. package/packages/dashboard/src/components/PipelineStage.vue +8 -8
  46. package/packages/dashboard/src/components/PipelineView.vue +99 -45
  47. package/packages/dashboard/src/components/ProjectCard.vue +187 -0
  48. package/packages/dashboard/src/components/ProjectList.vue +103 -45
  49. package/packages/dashboard/src/components/ProjectOverview.vue +152 -0
  50. package/packages/dashboard/src/components/StageBadge.vue +13 -13
  51. package/packages/dashboard/src/components/StepCard.vue +15 -15
  52. package/packages/dashboard/src/components/VResizeHandle.vue +61 -0
  53. package/packages/dashboard/src/components/detail/DocsDetail.vue +48 -0
  54. package/packages/dashboard/src/components/detail/GitDetail.vue +61 -0
  55. package/packages/dashboard/src/components/detail/TechDetail.vue +43 -0
  56. package/packages/dashboard/src/composables/useDashboard.js +48 -6
  57. package/packages/dashboard/src/composables/useKeyboard.js +6 -4
  58. package/packages/dashboard/src/composables/useLayout.js +131 -0
  59. package/packages/dashboard/src/main.js +4 -1
  60. package/packages/dashboard/src/style.css +17 -17
  61. package/src/index.js +141 -22
  62. package/src/init.js +93 -231
  63. package/src/migrate.js +117 -0
  64. package/src/progress.js +460 -0
  65. package/src/run.js +635 -0
  66. package/src/setup.js +2 -72
  67. package/src/stages/archive.js +54 -0
  68. package/src/stages/brainstorm.js +264 -0
  69. package/src/stages/doctor.js +303 -0
  70. package/src/stages/execute.js +287 -0
  71. package/src/stages/explore.js +34 -0
  72. package/src/stages/index.js +28 -0
  73. package/src/stages/plan.js +354 -0
  74. package/src/stages/propose.js +115 -0
  75. package/src/stages/quick.js +64 -0
  76. package/src/stages/scan.js +141 -0
  77. package/src/stages/status.js +65 -0
  78. package/src/stages/verify.js +135 -0
  79. package/.sillyspec/changes/dashboard/design.md +0 -219
  80. package/.sillyspec/plans/2026-04-05-dashboard.md +0 -737
  81. package/.sillyspec/specs/2026-04-05-dashboard-design.md +0 -206
  82. package/dist/steps/brainstorm/01-load-context.md +0 -30
  83. package/dist/steps/brainstorm/02-reuse-check.md +0 -6
  84. package/dist/steps/brainstorm/03-prototype-analysis.md +0 -11
  85. package/dist/steps/brainstorm/04-module-split.md +0 -23
  86. package/dist/steps/brainstorm/05-dialog-explore.md +0 -8
  87. package/dist/steps/brainstorm/06-propose-approaches.md +0 -3
  88. package/dist/steps/brainstorm/07-present-design.md +0 -3
  89. package/dist/steps/brainstorm/08-write-design.md +0 -21
  90. package/dist/steps/brainstorm/09-self-review.md +0 -15
  91. package/dist/steps/brainstorm/10-user-confirm.md +0 -3
  92. package/dist/steps/brainstorm/11-output-spec.md +0 -7
  93. package/dist/steps/brainstorm/manifest.yaml +0 -26
  94. package/dist/steps/execute/01-load-context.md +0 -41
  95. package/dist/steps/execute/02-scan-conventions.md +0 -47
  96. package/dist/steps/execute/03-skill-mcp.md +0 -19
  97. package/dist/steps/execute/04-assign-task.md +0 -22
  98. package/dist/steps/execute/04b-prompt-template.md +0 -54
  99. package/dist/steps/execute/05-write-test.md +0 -7
  100. package/dist/steps/execute/06-write-code.md +0 -8
  101. package/dist/steps/execute/07-run-test.md +0 -26
  102. package/dist/steps/execute/08-fix-issues.md +0 -28
  103. package/dist/steps/execute/09-next-task.md +0 -33
  104. package/dist/steps/execute/manifest.yaml +0 -28
  105. package/dist/steps/plan/01-load-context.md +0 -22
  106. package/dist/steps/plan/02-anchor-confirm.md +0 -1
  107. package/dist/steps/plan/03-expand-tasks.md +0 -33
  108. package/dist/steps/plan/04-mark-order.md +0 -15
  109. package/dist/steps/plan/05-e2e-planning.md +0 -17
  110. package/dist/steps/plan/06-self-check.md +0 -16
  111. package/dist/steps/plan/07-save.md +0 -1
  112. package/dist/steps/plan/manifest.yaml +0 -18
  113. package/dist/steps/scan/01-env-detect.md +0 -51
  114. package/dist/steps/scan/02-tech-stack.md +0 -16
  115. package/dist/steps/scan/03-conventions.md +0 -16
  116. package/dist/steps/scan/04-structure.md +0 -19
  117. package/dist/steps/scan/05-quality.md +0 -18
  118. package/dist/steps/scan/06-complete.md +0 -49
  119. package/dist/steps/scan/manifest.yaml +0 -16
  120. package/dist/steps/verify/01-load-specs.md +0 -28
  121. package/dist/steps/verify/02-check-tasks.md +0 -1
  122. package/dist/steps/verify/03-check-design.md +0 -6
  123. package/dist/steps/verify/04-run-tests.md +0 -7
  124. package/dist/steps/verify/05-e2e-tests.md +0 -27
  125. package/dist/steps/verify/05b-e2e-fix.md +0 -33
  126. package/dist/steps/verify/06-code-quality.md +0 -25
  127. package/dist/steps/verify/07-lint-check.md +0 -27
  128. package/dist/steps/verify/08-output-report.md +0 -14
  129. package/dist/steps/verify/manifest.yaml +0 -22
  130. package/docs/.vitepress/config.mts +0 -45
  131. package/docs/.vitepress/dist/404.html +0 -25
  132. package/docs/.vitepress/dist/assets/app.YytxICdd.js +0 -1
  133. package/docs/.vitepress/dist/assets/chunks/framework.Czhw_PXq.js +0 -19
  134. package/docs/.vitepress/dist/assets/chunks/theme.DusTRZQk.js +0 -1
  135. package/docs/.vitepress/dist/assets/index.md.C3VCvtQA.js +0 -1
  136. package/docs/.vitepress/dist/assets/index.md.C3VCvtQA.lean.js +0 -1
  137. package/docs/.vitepress/dist/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
  138. package/docs/.vitepress/dist/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
  139. package/docs/.vitepress/dist/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
  140. package/docs/.vitepress/dist/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
  141. package/docs/.vitepress/dist/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
  142. package/docs/.vitepress/dist/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
  143. package/docs/.vitepress/dist/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
  144. package/docs/.vitepress/dist/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
  145. package/docs/.vitepress/dist/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
  146. package/docs/.vitepress/dist/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
  147. package/docs/.vitepress/dist/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
  148. package/docs/.vitepress/dist/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
  149. package/docs/.vitepress/dist/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
  150. package/docs/.vitepress/dist/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
  151. package/docs/.vitepress/dist/assets/sillyspec_commands.md.CXFFsj08.js +0 -15
  152. package/docs/.vitepress/dist/assets/sillyspec_commands.md.CXFFsj08.lean.js +0 -1
  153. package/docs/.vitepress/dist/assets/sillyspec_dashboard.md.BuPXHqjX.js +0 -4
  154. package/docs/.vitepress/dist/assets/sillyspec_dashboard.md.BuPXHqjX.lean.js +0 -1
  155. package/docs/.vitepress/dist/assets/sillyspec_file-io.md.Cz3x7llx.js +0 -1
  156. package/docs/.vitepress/dist/assets/sillyspec_file-io.md.Cz3x7llx.lean.js +0 -1
  157. package/docs/.vitepress/dist/assets/sillyspec_getting-started.md.ClcvV8k3.js +0 -4
  158. package/docs/.vitepress/dist/assets/sillyspec_getting-started.md.ClcvV8k3.lean.js +0 -1
  159. package/docs/.vitepress/dist/assets/sillyspec_install.md.CKuR2tiT.js +0 -5
  160. package/docs/.vitepress/dist/assets/sillyspec_install.md.CKuR2tiT.lean.js +0 -1
  161. package/docs/.vitepress/dist/assets/sillyspec_lifecycle.md.DY293cR1.js +0 -28
  162. package/docs/.vitepress/dist/assets/sillyspec_lifecycle.md.DY293cR1.lean.js +0 -1
  163. package/docs/.vitepress/dist/assets/sillyspec_structure.md.sVYS4zPs.js +0 -30
  164. package/docs/.vitepress/dist/assets/sillyspec_structure.md.sVYS4zPs.lean.js +0 -1
  165. package/docs/.vitepress/dist/assets/style.DFTx90Kk.css +0 -1
  166. package/docs/.vitepress/dist/hashmap.json +0 -1
  167. package/docs/.vitepress/dist/index.html +0 -28
  168. package/docs/.vitepress/dist/sillyspec/commands.html +0 -42
  169. package/docs/.vitepress/dist/sillyspec/dashboard.html +0 -31
  170. package/docs/.vitepress/dist/sillyspec/file-io.html +0 -28
  171. package/docs/.vitepress/dist/sillyspec/getting-started.html +0 -31
  172. package/docs/.vitepress/dist/sillyspec/install.html +0 -32
  173. package/docs/.vitepress/dist/sillyspec/lifecycle.html +0 -55
  174. package/docs/.vitepress/dist/sillyspec/structure.html +0 -57
  175. package/docs/.vitepress/dist/vp-icons.css +0 -1
  176. package/docs/index.md +0 -34
  177. package/docs/sillyspec/commands.md +0 -218
  178. package/docs/sillyspec/dashboard.md +0 -51
  179. package/docs/sillyspec/file-io.md +0 -34
  180. package/docs/sillyspec/getting-started.md +0 -61
  181. package/docs/sillyspec/install.md +0 -51
  182. package/docs/sillyspec/lifecycle.md +0 -146
  183. package/docs/sillyspec/structure.md +0 -62
  184. package/packages/dashboard/dist/assets/index-Bh-GPjKY.css +0 -1
  185. package/packages/dashboard/dist/assets/index-CrCn5Gg6.js +0 -17
  186. package/src/step.js +0 -543
  187. package/templates/archive.md +0 -120
  188. package/templates/brainstorm.md +0 -170
  189. package/templates/continue.md +0 -32
  190. package/templates/execute.md +0 -304
  191. package/templates/explore.md +0 -59
  192. package/templates/export.md +0 -21
  193. package/templates/init.md +0 -61
  194. package/templates/plan.md +0 -146
  195. package/templates/quick.md +0 -135
  196. package/templates/scan-quick.md +0 -49
  197. package/templates/scan.md +0 -156
  198. package/templates/skills/playwright-e2e/SKILL.md +0 -340
  199. package/templates/status.md +0 -75
  200. package/templates/verify.md +0 -236
  201. package/templates/workspace-sync.md +0 -99
  202. package/templates/workspace.md +0 -70
  203. /package/{docs/.vitepress/dist/logo.jpg → logo.jpg} +0 -0
  204. /package/{docs/.vitepress → packages/dashboard}/dist/favicon.jpg +0 -0
  205. /package/{docs/public → packages/dashboard/dist}/logo.jpg +0 -0
  206. /package/{docs → packages/dashboard}/public/favicon.jpg +0 -0
@@ -0,0 +1,460 @@
1
+ /**
2
+ * SillySpec ProgressManager — 进度恢复管理
3
+ *
4
+ * 纯 Node.js,无外部依赖。管理 .sillyspec/.runtime/progress.json。
5
+ *
6
+ * Schema v2: { project, currentStage, stages: { [name]: { status, steps, startedAt, completedAt } }, lastActive }
7
+ */
8
+
9
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, copyFileSync, unlinkSync } from 'fs';
10
+ import { join, basename } from 'path';
11
+
12
+ const RUNTIME_DIR = '.sillyspec/.runtime';
13
+ const PROGRESS_FILE = 'progress.json';
14
+ const BACKUP_FILE = 'progress.json.bak';
15
+
16
+ const CURRENT_VERSION = 2;
17
+ const VALID_STAGES = ['scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore'];
18
+ const VALID_STATUSES = ['pending', 'in-progress', 'completed', 'failed', 'blocked'];
19
+
20
+ const STAGE_LABELS = {
21
+ brainstorm: '🧠 需求探索',
22
+ plan: '📐 实现计划',
23
+ execute: '⚡ 波次执行',
24
+ verify: '🔍 验证确认',
25
+ scan: '🔍 代码扫描',
26
+ quick: '⚡ 快速任务',
27
+ explore: '🧭 自由探索',
28
+ archive: '📦 归档变更',
29
+ };
30
+
31
+ function emptyStage() {
32
+ return { status: 'pending', steps: [], startedAt: null, completedAt: null };
33
+ }
34
+
35
+ function makeInitialProgress(project) {
36
+ const stages = {};
37
+ for (const s of VALID_STAGES) stages[s] = emptyStage();
38
+ return { _version: CURRENT_VERSION, project: project || '', currentStage: '', currentChange: null, stages, lastActive: null };
39
+ }
40
+
41
+ // ── ProgressManager ──
42
+
43
+ export class ProgressManager {
44
+ // ── 核心读写 ──
45
+
46
+ _path(cwd, ...parts) {
47
+ return join(cwd, RUNTIME_DIR, ...parts);
48
+ }
49
+
50
+ read(cwd) {
51
+ const progressPath = this._path(cwd, PROGRESS_FILE);
52
+ const backupPath = this._path(cwd, BACKUP_FILE);
53
+
54
+ for (const p of [progressPath, backupPath]) {
55
+ if (!existsSync(p)) continue;
56
+ const parsed = this._parseWithRecovery(readFileSync(p, 'utf8'));
57
+ if (parsed) {
58
+ if (p === backupPath) {
59
+ console.log('⚠️ progress.json 损坏,已从备份恢复');
60
+ writeFileSync(progressPath, JSON.stringify(parsed, null, 2) + '\n');
61
+ }
62
+ return parsed;
63
+ }
64
+ }
65
+ return null;
66
+ }
67
+
68
+ _write(cwd, data) {
69
+ const progressPath = this._path(cwd, PROGRESS_FILE);
70
+ const tmpPath = progressPath + '.tmp';
71
+ this._ensureDir(cwd);
72
+ writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n');
73
+ renameSync(tmpPath, progressPath);
74
+ }
75
+
76
+ _ensureDir(cwd) {
77
+ const runtimeDir = this._path(cwd);
78
+ if (!existsSync(runtimeDir)) {
79
+ mkdirSync(runtimeDir, { recursive: true });
80
+ for (const d of ['artifacts', 'history', 'logs', 'templates']) {
81
+ mkdirSync(join(runtimeDir, d), { recursive: true });
82
+ }
83
+ }
84
+ }
85
+
86
+ _backup(cwd) {
87
+ const p = this._path(cwd, PROGRESS_FILE);
88
+ if (existsSync(p)) copyFileSync(p, this._path(cwd, BACKUP_FILE));
89
+ }
90
+
91
+ // ── CLI 命令 ──
92
+
93
+ init(cwd) {
94
+ this._ensureDir(cwd);
95
+ const progressPath = this._path(cwd, PROGRESS_FILE);
96
+
97
+ if (existsSync(progressPath)) {
98
+ console.log(`ℹ️ progress.json 已存在,跳过`);
99
+ return this.read(cwd);
100
+ }
101
+
102
+ const project = basename(cwd);
103
+ const data = makeInitialProgress(project);
104
+ this._write(cwd, data);
105
+ console.log(`✅ 已创建 ${join(RUNTIME_DIR, PROGRESS_FILE)}`);
106
+
107
+ // 创建 user-inputs.md
108
+ const inputsPath = this._path(cwd, 'user-inputs.md');
109
+ if (!existsSync(inputsPath)) {
110
+ writeFileSync(inputsPath, '# 用户输入记录\n\n> 每步完成时由 AI 自动追加,记录用户所有原话。\n\n');
111
+ }
112
+
113
+ this._ensureGitignore(cwd);
114
+ return data;
115
+ }
116
+
117
+ setStage(cwd, stage) {
118
+ if (!VALID_STAGES.includes(stage)) {
119
+ console.log(`❌ 未知阶段: ${stage},可选: ${VALID_STAGES.join(', ')}`);
120
+ return;
121
+ }
122
+
123
+ const data = this._readOrInit(cwd);
124
+ if (!data) return;
125
+
126
+ if (!data.stages[stage]) data.stages[stage] = emptyStage();
127
+ const stageData = data.stages[stage];
128
+
129
+ data.currentStage = stage;
130
+ if (stageData.status === 'pending') {
131
+ stageData.status = 'in-progress';
132
+ stageData.startedAt = new Date().toLocaleString('zh-CN',{hour12:false});
133
+ }
134
+ data.lastActive = new Date().toLocaleString('zh-CN',{hour12:false});
135
+
136
+ this._backup(cwd);
137
+ this._write(cwd, data);
138
+ console.log(`✅ 当前阶段已设为: ${STAGE_LABELS[stage] || stage} (${stageData.status})`);
139
+ }
140
+
141
+ addStep(cwd, stage, stepName) {
142
+ if (!stepName) { console.log('❌ 请指定步骤名称'); return; }
143
+ const data = this._requireStage(cwd, stage);
144
+ if (!data) return;
145
+
146
+ const stageData = data.stages[stage];
147
+ if (stageData.steps.some(s => s.name === stepName)) {
148
+ console.log(`ℹ️ 步骤 "${stepName}" 已存在于 ${stage}`);
149
+ return;
150
+ }
151
+
152
+ stageData.steps.push({ name: stepName, status: 'pending' });
153
+ data.lastActive = new Date().toLocaleString('zh-CN',{hour12:false});
154
+
155
+ this._backup(cwd);
156
+ this._write(cwd, data);
157
+ console.log(`✅ 已添加步骤: ${stage}/${stepName}`);
158
+ }
159
+
160
+ updateStep(cwd, stage, stepName, options = {}) {
161
+ const { status, output } = options;
162
+ if (!stepName) { console.log('❌ 请指定步骤名称'); return; }
163
+ const data = this._requireStage(cwd, stage);
164
+ if (!data) return;
165
+
166
+ const stageData = data.stages[stage];
167
+ const step = stageData.steps.find(s => s.name === stepName);
168
+ if (!step) { console.log(`❌ 步骤不存在: ${stage}/${stepName}`); return; }
169
+
170
+ if (status) {
171
+ if (!VALID_STATUSES.includes(status)) {
172
+ console.log(`❌ 无效状态: ${status},可选: ${VALID_STATUSES.join(', ')}`);
173
+ return;
174
+ }
175
+ step.status = status;
176
+ }
177
+ if (output !== undefined) step.output = output;
178
+
179
+ // 检查是否所有步骤都 completed
180
+ if (stageData.steps.length > 0 && stageData.steps.every(s => s.status === 'completed')) {
181
+ stageData.status = 'completed';
182
+ stageData.completedAt = new Date().toLocaleString('zh-CN',{hour12:false});
183
+ console.log(`✅ 阶段 ${stage} 所有步骤已完成,阶段已标记为 completed`);
184
+ }
185
+
186
+ data.lastActive = new Date().toLocaleString('zh-CN',{hour12:false});
187
+ this._backup(cwd);
188
+ this._write(cwd, data);
189
+ console.log(`✅ 步骤已更新: ${stage}/${stepName} → ${status || step.status}`);
190
+ }
191
+
192
+ completeStage(cwd, stage) {
193
+ if (!VALID_STAGES.includes(stage)) {
194
+ console.log(`❌ 未知阶段: ${stage}`);
195
+ return;
196
+ }
197
+
198
+ const data = this._readOrInit(cwd);
199
+ if (!data) return;
200
+
201
+ if (!data.stages[stage]) data.stages[stage] = emptyStage();
202
+ const stageData = data.stages[stage];
203
+ stageData.status = 'completed';
204
+ stageData.completedAt = new Date().toLocaleString('zh-CN',{hour12:false});
205
+
206
+ // 标记所有未完成步骤为 completed
207
+ for (const step of stageData.steps) {
208
+ if (step.status === 'pending') step.status = 'completed';
209
+ }
210
+
211
+ data.lastActive = new Date().toLocaleString('zh-CN',{hour12:false});
212
+
213
+ // 归档到 history/(ISO 时间戳,去掉所有非法路径字符)
214
+ const historyDir = this._path(cwd, 'history');
215
+ mkdirSync(historyDir, { recursive: true });
216
+ const ts = new Date().toISOString().replace(/[:.TZ-]/g, '');
217
+ writeFileSync(join(historyDir, `${stage}-${ts}.json`), JSON.stringify({ stage, data: stageData, completedAt: stageData.completedAt }, null, 2) + '\n');
218
+
219
+ this._backup(cwd);
220
+ this._write(cwd, data);
221
+
222
+ console.log(`✅ 阶段 ${stage} 已标记为完成(不自动推进,下一步由你决定)`);
223
+ }
224
+
225
+ show(cwd) {
226
+ const data = this.read(cwd);
227
+ if (!data) {
228
+ console.log('❌ 未找到 progress.json,请先运行 sillyspec progress init');
229
+ return;
230
+ }
231
+
232
+ console.log('');
233
+ console.log(' ═══════════════════════════════════════');
234
+ console.log(` 项目: ${data.project || '(未命名)'}`);
235
+ console.log(` 当前阶段: ${STAGE_LABELS[data.currentStage] || data.currentStage || '(无)'}`);
236
+ console.log(` 最近活跃: ${data.lastActive ? this._timeAgo(data.lastActive) : '未知'}`);
237
+ console.log(' ═══════════════════════════════════════');
238
+ console.log('');
239
+
240
+ const statusIcons = { pending: '⬜', 'in-progress': '🔵', completed: '✅', failed: '❌', blocked: '🚫' };
241
+
242
+ for (const stage of VALID_STAGES) {
243
+ const stageData = data.stages[stage] || emptyStage();
244
+ const label = STAGE_LABELS[stage] || stage;
245
+ const icon = statusIcons[stageData.status] || '⬜';
246
+ const isCurrent = data.currentStage === stage ? ' ◀' : '';
247
+
248
+ console.log(` ${icon} ${label}${isCurrent}`);
249
+
250
+ if (stageData.steps && stageData.steps.length > 0) {
251
+ for (const step of stageData.steps) {
252
+ const si = statusIcons[step.status] || '○';
253
+ const out = step.output ? ` — ${step.output.slice(0, 60)}` : '';
254
+ console.log(` ${si} ${step.name}${out}`);
255
+ }
256
+ }
257
+
258
+ if (stageData.startedAt) {
259
+ console.log(` 开始: ${new Date(stageData.startedAt).toLocaleString('zh-CN')}`);
260
+ }
261
+ if (stageData.completedAt) {
262
+ console.log(` 完成: ${new Date(stageData.completedAt).toLocaleString('zh-CN')}`);
263
+ }
264
+ }
265
+
266
+ // 批量进度
267
+ if (data.batchProgress) {
268
+ const batchLine = this._renderBatchProgress(data.batchProgress);
269
+ if (batchLine) {
270
+ console.log('');
271
+ console.log(` ${batchLine}`);
272
+ }
273
+ }
274
+
275
+ console.log('');
276
+ }
277
+
278
+ status(cwd) {
279
+ this.show(cwd);
280
+ }
281
+
282
+ async validate(cwd) {
283
+ const data = this.read(cwd);
284
+ if (!data) { console.log('❌ 无法读取 progress.json'); return false; }
285
+
286
+ const errors = [];
287
+ if (!data._version || !Number.isInteger(data._version) || data._version < 1) {
288
+ errors.push(`_version 缺失或无效(期望正整数,实际为 ${JSON.stringify(data._version)})`);
289
+ }
290
+ if (!data.stages || typeof data.stages !== 'object') errors.push('缺少 stages');
291
+ if (!VALID_STAGES.every(s => data.stages[s])) errors.push('缺少阶段定义');
292
+
293
+ if (errors.length === 0) { console.log('✅ progress.json 格式正确'); return true; }
294
+
295
+ console.log(`⚠️ 发现问题,尝试修复...`);
296
+ let fixed = { ...data, stages: { ...data.stages } };
297
+ let changed = false;
298
+ if (!fixed.project) {
299
+ fixed.project = basename(cwd);
300
+ changed = true;
301
+ }
302
+ if (!fixed._version || !Number.isInteger(fixed._version) || fixed._version < 1) {
303
+ fixed._version = CURRENT_VERSION;
304
+ changed = true;
305
+ }
306
+ for (const s of VALID_STAGES) {
307
+ if (!fixed.stages[s]) { fixed.stages[s] = emptyStage(); changed = true; }
308
+ }
309
+ if (changed) {
310
+ this._backup(cwd);
311
+ this._write(cwd, fixed);
312
+ console.log('✅ 已修复并备份');
313
+ }
314
+
315
+ return true;
316
+ }
317
+
318
+ reset(cwd, stage) {
319
+ this._ensureDir(cwd);
320
+
321
+ if (stage) {
322
+ this._backup(cwd);
323
+ const data = this.read(cwd);
324
+ if (!data) { console.log('❌ 无法读取 progress.json'); return; }
325
+ if (!data.stages[stage]) { console.log(`❌ 未知阶段: ${stage}`); return; }
326
+ data.stages[stage] = emptyStage();
327
+ data.lastActive = new Date().toLocaleString('zh-CN',{hour12:false});
328
+ this._write(cwd, data);
329
+ console.log(`✅ 已重置阶段: ${stage}`);
330
+ } else {
331
+ const p = this._path(cwd, PROGRESS_FILE);
332
+ const backup = this._path(cwd, BACKUP_FILE);
333
+ let didReset = false;
334
+ if (existsSync(p)) { unlinkSync(p); didReset = true; }
335
+ if (existsSync(backup)) { unlinkSync(backup); didReset = true; }
336
+ if (didReset) {
337
+ console.log('✅ 已重置所有进度');
338
+ } else {
339
+ console.log('ℹ️ 无进度文件可重置');
340
+ }
341
+ }
342
+ }
343
+
344
+ // ── 内部辅助 ──
345
+
346
+ _readOrInit(cwd) {
347
+ let data = this.read(cwd);
348
+ if (!data) {
349
+ this._ensureDir(cwd);
350
+ const progressPath = this._path(cwd, PROGRESS_FILE);
351
+ if (!existsSync(progressPath)) {
352
+ data = makeInitialProgress(basename(cwd));
353
+ this._write(cwd, data);
354
+ } else {
355
+ console.log('❌ progress.json 损坏,请运行 sillyspec progress validate');
356
+ return null;
357
+ }
358
+ }
359
+ return data;
360
+ }
361
+
362
+ _requireStage(cwd, stage) {
363
+ if (!VALID_STAGES.includes(stage)) {
364
+ console.log(`❌ 未知阶段: ${stage},可选: ${VALID_STAGES.join(', ')}`);
365
+ return null;
366
+ }
367
+ const data = this._readOrInit(cwd);
368
+ if (!data) return null;
369
+ if (!data.stages[stage]) data.stages[stage] = emptyStage();
370
+ return data;
371
+ }
372
+
373
+ _parseWithRecovery(jsonString) {
374
+ try { return JSON.parse(jsonString); } catch {}
375
+
376
+ let fixed = jsonString.replace(/,\s*([}\]])/g, '$1');
377
+ fixed = fixed.replace(/([{,]\s*)'([^']+)'(\s*:)/g, '$1"$2"$3');
378
+ fixed = fixed.replace(/:\s*'([^']*)'([,}\]])/g, ':"$1"$2');
379
+ try { return JSON.parse(fixed); } catch {}
380
+
381
+ const lastBrace = fixed.lastIndexOf('}');
382
+ if (lastBrace > 0) {
383
+ let open = 0;
384
+ for (const ch of fixed.substring(0, lastBrace + 1)) {
385
+ if (ch === '{') open++;
386
+ if (ch === '}') open--;
387
+ }
388
+ try { return JSON.parse(fixed.substring(0, lastBrace + 1) + '}'.repeat(Math.max(0, open))); } catch {}
389
+ }
390
+ return null;
391
+ }
392
+
393
+ _timeAgo(dateStr) {
394
+ if (!dateStr) return '未知';
395
+ let ts = Date.parse(dateStr);
396
+ // toLocaleString('zh-CN') 格式(如 2026/5/12 21:09:00)可能解析失败,尝试手动解析
397
+ if (isNaN(ts)) {
398
+ const m = dateStr.match(/(\d{4})[\/-](\d{1,2})[\/-](\d{1,2})[\s,]+(\d{1,2}):(\d{2})(?::(\d{2}))?/);
399
+ if (m) ts = new Date(+m[1], +m[2]-1, +m[3], +m[4], +m[5], +(m[6]||0)).getTime();
400
+ }
401
+ if (isNaN(ts)) return dateStr;
402
+ const diff = Date.now() - ts;
403
+ const minutes = Math.floor(diff / 60000);
404
+ if (minutes < 1) return '刚刚';
405
+ if (minutes < 60) return `${minutes} 分钟前`;
406
+ const hours = Math.floor(minutes / 60);
407
+ if (hours < 24) return `${hours} 小时前`;
408
+ return `${Math.floor(hours / 24)} 天前`;
409
+ }
410
+
411
+ // ── 批量进度 ──
412
+
413
+ updateBatchProgress(cwd, batchData) {
414
+ const data = this._readOrInit(cwd);
415
+ if (!data) return;
416
+
417
+ if (!data.batchProgress) {
418
+ data.batchProgress = { total: 0, completed: 0, failed: 0, skipped: 0 };
419
+ }
420
+ if (batchData.total !== undefined) data.batchProgress.total = batchData.total;
421
+ if (batchData.completed !== undefined) data.batchProgress.completed = batchData.completed;
422
+ if (batchData.failed !== undefined) data.batchProgress.failed = batchData.failed;
423
+ if (batchData.skipped !== undefined) data.batchProgress.skipped = batchData.skipped;
424
+
425
+ data.lastActive = new Date().toLocaleString('zh-CN', { hour12: false });
426
+ this._backup(cwd);
427
+ this._write(cwd, data);
428
+ }
429
+
430
+ readBatchProgress(cwd) {
431
+ const data = this.read(cwd);
432
+ return data?.batchProgress || null;
433
+ }
434
+
435
+ _renderBatchProgress(batchProgress) {
436
+ if (!batchProgress || !batchProgress.total) return null;
437
+ const { total, completed = 0, failed = 0, skipped = 0 } = batchProgress;
438
+ const done = Math.min(completed + failed + skipped, total);
439
+ const barLen = 20;
440
+ const filled = Math.round((completed / total) * barLen);
441
+ const bar = '█'.repeat(filled) + '░'.repeat(barLen - filled);
442
+ const parts = [];
443
+ if (failed > 0) parts.push(`${failed} 失败`);
444
+ if (skipped > 0) parts.push(`${skipped} 跳过`);
445
+ const suffix = parts.length ? ` (${parts.join(', ')})` : '';
446
+ return `📊 批量进度: ${bar} ${completed}/${total}${suffix}`;
447
+ }
448
+
449
+ _ensureGitignore(cwd) {
450
+ const gitignorePath = join(cwd, '.gitignore');
451
+ const rule = '.sillyspec/.runtime/';
452
+ if (existsSync(gitignorePath)) {
453
+ const content = readFileSync(gitignorePath, 'utf8');
454
+ if (content.includes(rule)) return;
455
+ writeFileSync(gitignorePath, content.trimEnd() + '\n' + rule + '\n');
456
+ } else {
457
+ writeFileSync(gitignorePath, rule + '\n');
458
+ }
459
+ }
460
+ }