sillyspec 3.7.13 → 3.7.15

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 (91) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +77 -0
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +591 -0
  3. package/.claude/skills/sillyspec-continue/SKILL.md +44 -0
  4. package/.claude/skills/sillyspec-execute/SKILL.md +233 -0
  5. package/.claude/skills/sillyspec-explore/SKILL.md +96 -0
  6. package/.claude/skills/sillyspec-export/SKILL.md +53 -0
  7. package/.claude/skills/sillyspec-init/SKILL.md +171 -0
  8. package/.claude/skills/sillyspec-plan/SKILL.md +263 -0
  9. package/.claude/skills/sillyspec-propose/SKILL.md +248 -0
  10. package/.claude/skills/sillyspec-quick/SKILL.md +102 -0
  11. package/.claude/skills/sillyspec-resume/SKILL.md +111 -0
  12. package/{templates/scan.md → .claude/skills/sillyspec-scan/SKILL.md} +22 -60
  13. package/.claude/skills/sillyspec-state/SKILL.md +54 -0
  14. package/.claude/skills/sillyspec-status/SKILL.md +131 -0
  15. package/.claude/skills/sillyspec-verify/SKILL.md +146 -0
  16. package/.claude/skills/sillyspec-workspace/SKILL.md +149 -0
  17. package/.sillyspec/changes/run-command-design/design.md +1230 -0
  18. package/.sillyspec/docs/sillyspec/scan/.gitkeep +0 -0
  19. package/.sillyspec/knowledge/INDEX.md +8 -0
  20. package/.sillyspec/knowledge/uncategorized.md +3 -0
  21. package/.sillyspec/projects/sillyspec.yaml +3 -0
  22. package/README.md +4 -0
  23. package/logo.jpg +0 -0
  24. package/package.json +7 -1
  25. package/packages/dashboard/dist/assets/index-Bx0cgoK_.js +7446 -0
  26. package/packages/dashboard/dist/assets/index-DbkUSsNO.css +1 -0
  27. package/packages/dashboard/dist/favicon.jpg +0 -0
  28. package/packages/dashboard/dist/index.html +2 -2
  29. package/packages/dashboard/dist/logo.jpg +0 -0
  30. package/packages/dashboard/package-lock.json +220 -0
  31. package/packages/dashboard/package.json +8 -5
  32. package/packages/dashboard/public/favicon.jpg +0 -0
  33. package/packages/dashboard/public/logo.jpg +0 -0
  34. package/packages/dashboard/server/index.js +92 -4
  35. package/packages/dashboard/server/parser.js +252 -28
  36. package/packages/dashboard/src/App.vue +139 -9
  37. package/packages/dashboard/src/components/ActionBar.vue +23 -39
  38. package/packages/dashboard/src/components/CommandPalette.vue +40 -65
  39. package/packages/dashboard/src/components/DetailPanel.vue +68 -53
  40. package/packages/dashboard/src/components/DocPreview.vue +137 -20
  41. package/packages/dashboard/src/components/DocTree.vue +48 -26
  42. package/packages/dashboard/src/components/LogStream.vue +12 -32
  43. package/packages/dashboard/src/components/PipelineStage.vue +8 -8
  44. package/packages/dashboard/src/components/PipelineView.vue +35 -43
  45. package/packages/dashboard/src/components/ProjectList.vue +52 -77
  46. package/packages/dashboard/src/components/ProjectOverview.vue +178 -0
  47. package/packages/dashboard/src/components/StageBadge.vue +13 -13
  48. package/packages/dashboard/src/components/StepCard.vue +11 -11
  49. package/packages/dashboard/src/components/detail/DocsDetail.vue +48 -0
  50. package/packages/dashboard/src/components/detail/GitDetail.vue +61 -0
  51. package/packages/dashboard/src/components/detail/TechDetail.vue +43 -0
  52. package/packages/dashboard/src/main.js +4 -1
  53. package/packages/dashboard/src/style.css +13 -13
  54. package/src/index.js +55 -8
  55. package/src/init.js +66 -196
  56. package/src/migrate.js +1 -18
  57. package/src/progress.js +279 -281
  58. package/src/run.js +320 -0
  59. package/src/setup.js +1 -9
  60. package/src/stages/brainstorm.js +210 -0
  61. package/src/stages/execute.js +190 -0
  62. package/src/stages/index.js +22 -0
  63. package/src/stages/plan.js +118 -0
  64. package/src/stages/propose.js +115 -0
  65. package/src/stages/verify.js +98 -0
  66. package/packages/dashboard/dist/assets/index-5Rrvs0Rl.css +0 -1
  67. package/packages/dashboard/dist/assets/index-ZNToqi9V.js +0 -17
  68. package/templates/archive.md +0 -121
  69. package/templates/brainstorm.md +0 -246
  70. package/templates/commit.md +0 -123
  71. package/templates/continue.md +0 -32
  72. package/templates/execute.md +0 -314
  73. package/templates/explore.md +0 -60
  74. package/templates/export.md +0 -21
  75. package/templates/init.md +0 -61
  76. package/templates/plan.md +0 -157
  77. package/templates/progress-format.md +0 -90
  78. package/templates/propose.md +0 -73
  79. package/templates/quick.md +0 -135
  80. package/templates/resume-dialog.md +0 -55
  81. package/templates/resume.md +0 -53
  82. package/templates/scan-quick.md +0 -49
  83. package/templates/skills/playwright-e2e/SKILL.md +0 -340
  84. package/templates/status.md +0 -72
  85. package/templates/verify.md +0 -253
  86. package/templates/workspace-sync.md +0 -89
  87. package/templates/workspace.md +0 -67
  88. /package/.sillyspec/{docs/sillyspec/brainstorm → changes/brainstorm-archive}/2026-04-05-dashboard-design.md +0 -0
  89. /package/.sillyspec/{docs/sillyspec/brainstorm → changes/brainstorm-archive}/2026-04-05-unified-docs-design.md +0 -0
  90. /package/.sillyspec/{specs/2026-04-05-dashboard-design.md → changes/dashboard/design.md.braindraft} +0 -0
  91. /package/.sillyspec/{specs/2026-04-05-unified-docs-design.md → changes/unified-docs-design/design.md} +0 -0
package/src/progress.js CHANGED
@@ -2,9 +2,11 @@
2
2
  * SillySpec ProgressManager — 进度恢复管理
3
3
  *
4
4
  * 纯 Node.js,无外部依赖。管理 .sillyspec/.runtime/progress.json。
5
+ *
6
+ * Schema v2: { project, currentStage, stages: { [name]: { status, steps, startedAt, completedAt } }, lastActive }
5
7
  */
6
8
 
7
- import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, readdirSync, unlinkSync, copyFileSync } from 'fs';
9
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, readdirSync, unlinkSync } from 'fs';
8
10
  import { join, resolve, dirname } from 'path';
9
11
  import { fileURLToPath } from 'url';
10
12
 
@@ -14,403 +16,399 @@ const RUNTIME_DIR = '.sillyspec/.runtime';
14
16
  const PROGRESS_FILE = 'progress.json';
15
17
  const BACKUP_FILE = 'progress.json.bak';
16
18
 
17
- // ── 默认步骤定义 ──
18
-
19
- const STEP_DEFINITIONS = {
20
- brainstorm: [
21
- { id: 1, name: '加载项目上下文' },
22
- { id: 2, name: '协作与复用检查' },
23
- { id: 3, name: '原型/设计图分析' },
24
- { id: 4, name: '评估需求范围' },
25
- { id: 5, name: '对话式探索' },
26
- { id: 6, name: '提出方案并推荐' },
27
- { id: 7, name: '分段展示设计' },
28
- { id: 8, name: '写设计文档' },
29
- { id: 9, name: 'AI 自审' },
30
- { id: 10, name: '用户确认设计方案' },
31
- { id: 11, name: '输出技术方案' },
32
- { id: 12, name: '更新 STATE.md' },
33
- { id: 13, name: '保存最终进度' },
34
- ],
35
- propose: [
36
- { id: 1, name: '变更范围' },
37
- { id: 2, name: '方案设计' },
38
- { id: 3, name: '任务拆分' },
39
- { id: 4, name: '优先级排序' },
40
- { id: 5, name: '规范输出' },
41
- ],
42
- plan: [
43
- { id: 1, name: '任务分析' },
44
- { id: 2, name: '依赖梳理' },
45
- { id: 3, name: '实现路径' },
46
- { id: 4, name: '风险评估' },
47
- { id: 5, name: '计划输出' },
48
- ],
49
- execute: [
50
- { id: 1, name: '环境准备' },
51
- { id: 2, name: '编码实现' },
52
- { id: 3, name: '单元测试' },
53
- { id: 4, name: '代码审查' },
54
- { id: 5, name: '集成验证' },
55
- ],
56
- verify: [
57
- { id: 1, name: '规范对照' },
58
- { id: 2, name: '功能测试' },
59
- { id: 3, name: '边界测试' },
60
- { id: 4, name: '回归测试' },
61
- { id: 5, name: '验收报告' },
62
- ],
63
- };
19
+ const CURRENT_VERSION = 2;
20
+ const VALID_STAGES = ['brainstorm', 'propose', 'plan', 'execute', 'verify'];
21
+ const VALID_STATUSES = ['pending', 'in-progress', 'completed', 'failed', 'blocked'];
64
22
 
65
- const EMPTY_PROGRESS = {
66
- _version: 1,
67
- schemaVersion: '1.0.0',
68
- currentStage: 'brainstorm',
69
- lastActiveAt: new Date().toISOString(),
70
- resumeCount: 0,
71
- checkpoint: '',
72
- stages: {},
73
- artifacts: [],
23
+ const STAGE_LABELS = {
24
+ brainstorm: '🧠 需求探索',
25
+ propose: '📋 方案设计',
26
+ plan: '📐 实现计划',
27
+ execute: '⚡ 波次执行',
28
+ verify: '🔍 验证确认',
74
29
  };
75
30
 
76
31
  function emptyStage() {
77
- return {
78
- status: 'not_started',
79
- completedSteps: [],
80
- inProgressStep: null,
81
- summaries: {},
82
- artifacts: [],
83
- stageSummary: null,
84
- };
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: '', stages, lastActive: null };
85
39
  }
86
40
 
87
41
  // ── ProgressManager ──
88
42
 
89
43
  export class ProgressManager {
90
- // ── 公开方法 ──
44
+ // ── 核心读写 ──
91
45
 
92
- init(cwd) {
93
- const runtimeDir = join(cwd, RUNTIME_DIR);
94
- const subdirs = ['artifacts', 'history', 'logs', 'templates'];
95
- mkdirSync(runtimeDir, { recursive: true });
96
- for (const d of subdirs) {
97
- mkdirSync(join(runtimeDir, d), { recursive: true });
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
+ }
98
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
+ }
99
75
 
100
- const progressPath = join(runtimeDir, PROGRESS_FILE);
101
- if (!existsSync(progressPath)) {
102
- // 初始化所有阶段
103
- const data = { ...EMPTY_PROGRESS, stages: {} };
104
- for (const stage of Object.keys(STEP_DEFINITIONS)) {
105
- data.stages[stage] = emptyStage();
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 });
106
82
  }
107
- writeFileSync(progressPath, JSON.stringify(data, null, 2) + '\n');
108
- console.log(`✅ 已创建 ${join(RUNTIME_DIR, PROGRESS_FILE)}`);
109
- } else {
110
- console.log(`ℹ️ ${join(RUNTIME_DIR, PROGRESS_FILE)} 已存在,跳过`);
111
83
  }
84
+ }
85
+
86
+ _backup(cwd) {
87
+ const p = this._path(cwd, PROGRESS_FILE);
88
+ if (existsSync(p)) renameSync(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);
112
96
 
113
- // 复制 resume-dialog.md 模板
114
- const templateDir = resolve(__dirname, '..', 'templates');
115
- const resumeSrc = join(templateDir, 'resume-dialog.md');
116
- const resumeDest = join(runtimeDir, 'templates', 'resume-dialog.md');
117
- if (existsSync(resumeSrc) && !existsSync(resumeDest)) {
118
- copyFileSync(resumeSrc, resumeDest);
97
+ if (existsSync(progressPath)) {
98
+ console.log(`ℹ️ progress.json 已存在,跳过`);
99
+ return this.read(cwd);
119
100
  }
120
101
 
102
+ const project = require('path').basename(cwd);
103
+ const data = makeInitialProgress(project);
104
+ this._write(cwd, data);
105
+ console.log(`✅ 已创建 ${join(RUNTIME_DIR, PROGRESS_FILE)}`);
106
+
121
107
  // 创建 user-inputs.md
122
- const inputsPath = join(runtimeDir, 'user-inputs.md');
108
+ const inputsPath = this._path(cwd, 'user-inputs.md');
123
109
  if (!existsSync(inputsPath)) {
124
110
  writeFileSync(inputsPath, '# 用户输入记录\n\n> 每步完成时由 AI 自动追加,记录用户所有原话。\n\n');
125
111
  }
126
112
 
127
- // .gitignore
128
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];
129
128
 
130
- return this.read(cwd);
129
+ data.currentStage = stage;
130
+ if (stageData.status === 'pending') {
131
+ stageData.status = 'in-progress';
132
+ stageData.startedAt = new Date().toISOString();
133
+ }
134
+ data.lastActive = new Date().toISOString();
135
+
136
+ this._backup(cwd);
137
+ this._write(cwd, data);
138
+ console.log(`✅ 当前阶段已设为: ${STAGE_LABELS[stage] || stage} (${stageData.status})`);
131
139
  }
132
140
 
133
- read(cwd) {
134
- const progressPath = join(cwd, RUNTIME_DIR, PROGRESS_FILE);
135
- const backupPath = join(cwd, RUNTIME_DIR, BACKUP_FILE);
141
+ addStep(cwd, stage, stepName) {
142
+ if (!stepName) { console.log('❌ 请指定步骤名称'); return; }
143
+ const data = this._requireStage(cwd, stage);
144
+ if (!data) return;
136
145
 
137
- // 三层容错:正常解析 修复 → 读 .bak
138
- if (existsSync(progressPath)) {
139
- const raw = readFileSync(progressPath, 'utf8');
140
- const parsed = this._parseWithRecovery(raw);
141
- if (parsed) return parsed;
146
+ const stageData = data.stages[stage];
147
+ if (stageData.steps.some(s => s.name === stepName)) {
148
+ console.log(`ℹ️ 步骤 "${stepName}" 已存在于 ${stage}`);
149
+ return;
142
150
  }
143
151
 
144
- if (existsSync(backupPath)) {
145
- const raw = readFileSync(backupPath, 'utf8');
146
- const parsed = this._parseWithRecovery(raw);
147
- if (parsed) {
148
- console.log('⚠️ progress.json 损坏,已从备份恢复');
149
- writeFileSync(progressPath, JSON.stringify(parsed, null, 2) + '\n');
150
- return parsed;
152
+ stageData.steps.push({ name: stepName, status: 'pending' });
153
+ data.lastActive = new Date().toISOString();
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;
151
174
  }
175
+ step.status = status;
152
176
  }
177
+ if (output !== undefined) step.output = output;
153
178
 
154
- return null;
179
+ // 检查是否所有步骤都 completed
180
+ if (stageData.steps.length > 0 && stageData.steps.every(s => s.status === 'completed')) {
181
+ stageData.status = 'completed';
182
+ stageData.completedAt = new Date().toISOString();
183
+ console.log(`✅ 阶段 ${stage} 所有步骤已完成,阶段已标记为 completed`);
184
+ }
185
+
186
+ data.lastActive = new Date().toISOString();
187
+ this._backup(cwd);
188
+ this._write(cwd, data);
189
+ console.log(`✅ 步骤已更新: ${stage}/${stepName} → ${status || step.status}`);
155
190
  }
156
191
 
157
- validate(cwd) {
158
- const data = this.read(cwd);
159
- if (!data) {
160
- console.log('❌ 无法读取 progress.json');
161
- return false;
192
+ completeStage(cwd, stage) {
193
+ if (!VALID_STAGES.includes(stage)) {
194
+ console.log(`❌ 未知阶段: ${stage}`);
195
+ return;
162
196
  }
163
197
 
164
- const errors = this._validate(data);
165
- if (errors.length === 0) {
166
- console.log('✅ progress.json 格式正确');
167
- return true;
168
- }
198
+ const data = this._readOrInit(cwd);
199
+ if (!data) return;
169
200
 
170
- console.log(`⚠️ 发现 ${errors.length} 个问题,尝试修复...`);
201
+ if (!data.stages[stage]) data.stages[stage] = emptyStage();
202
+ const stageData = data.stages[stage];
203
+ stageData.status = 'completed';
204
+ stageData.completedAt = new Date().toISOString();
171
205
 
172
- // 自动修复:补全缺失的阶段
173
- let fixed = { ...data };
174
- let changed = false;
175
- for (const stage of Object.keys(STEP_DEFINITIONS)) {
176
- if (!fixed.stages[stage]) {
177
- fixed.stages[stage] = emptyStage();
178
- changed = true;
206
+ // 标记所有未完成步骤为 completed
207
+ for (const step of stageData.steps) {
208
+ if (step.status !== 'completed') step.status = 'completed';
209
+ }
210
+
211
+ // 推进到下一个未完成阶段
212
+ const idx = VALID_STAGES.indexOf(stage);
213
+ let nextStage = null;
214
+ for (let i = idx + 1; i < VALID_STAGES.length; i++) {
215
+ const s = data.stages[VALID_STAGES[i]];
216
+ if (!s || s.status !== 'completed') {
217
+ nextStage = VALID_STAGES[i];
218
+ break;
179
219
  }
180
220
  }
181
- if (!fixed._version) { fixed._version = 1; changed = true; }
182
- if (!fixed.schemaVersion) { fixed.schemaVersion = '1.0.0'; changed = true; }
183
- if (!fixed.artifacts) { fixed.artifacts = []; changed = true; }
184
- if (!fixed.lastActiveAt) { fixed.lastActiveAt = new Date().toISOString(); changed = true; }
185
221
 
186
- if (changed) {
187
- this._backup(cwd);
188
- const progressPath = join(cwd, RUNTIME_DIR, PROGRESS_FILE);
189
- writeFileSync(progressPath, JSON.stringify(fixed, null, 2) + '\n');
190
- console.log('✅ 已修复并备份');
222
+ if (nextStage) {
223
+ data.currentStage = nextStage;
224
+ if (!data.stages[nextStage]) data.stages[nextStage] = emptyStage();
225
+ if (data.stages[nextStage].status === 'pending') {
226
+ data.stages[nextStage].status = 'in-progress';
227
+ data.stages[nextStage].startedAt = new Date().toISOString();
228
+ }
229
+ console.log(`✅ 阶段 ${stage} 已完成,推进到: ${STAGE_LABELS[nextStage] || nextStage}`);
191
230
  } else {
192
- console.log('❌ 无法自动修复:');
193
- errors.forEach(e => console.log(` - ${e}`));
231
+ data.currentStage = stage;
232
+ console.log(`✅ 阶段 ${stage} 已完成(已是最后阶段)`);
194
233
  }
195
234
 
196
- return errors.length === 0;
235
+ data.lastActive = new Date().toISOString();
236
+
237
+ // 归档到 history/
238
+ const historyDir = this._path(cwd, 'history');
239
+ mkdirSync(historyDir, { recursive: true });
240
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
241
+ writeFileSync(join(historyDir, `${stage}-${ts}.json`), JSON.stringify({ stage, data: stageData, completedAt: stageData.completedAt }, null, 2) + '\n');
242
+
243
+ this._backup(cwd);
244
+ this._write(cwd, data);
197
245
  }
198
246
 
199
- status(cwd) {
247
+ show(cwd) {
200
248
  const data = this.read(cwd);
201
249
  if (!data) {
202
250
  console.log('❌ 未找到 progress.json,请先运行 sillyspec progress init');
203
251
  return;
204
252
  }
205
253
 
206
- const stageLabels = {
207
- brainstorm: '🧠 需求探索',
208
- propose: '📋 方案设计',
209
- plan: '📐 实现计划',
210
- execute: '⚡ 波次执行',
211
- verify: '🔍 验证确认',
212
- };
213
-
214
254
  console.log('');
215
255
  console.log(' ═══════════════════════════════════════');
216
- console.log(` 当前阶段: ${(stageLabels[data.currentStage] || data.currentStage)}`);
217
- console.log(` 最近活跃: ${data.lastActiveAt ? this._timeAgo(data.lastActiveAt) : '未知'}`);
218
- console.log(` 恢复次数: ${data.resumeCount ?? 0}`);
219
- if (data.checkpoint) {
220
- console.log(` 检查点: ${data.checkpoint}`);
221
- }
256
+ console.log(` 项目: ${data.project || '(未命名)'}`);
257
+ console.log(` 当前阶段: ${STAGE_LABELS[data.currentStage] || data.currentStage || '(无)'}`);
258
+ console.log(` 最近活跃: ${data.lastActive ? this._timeAgo(data.lastActive) : '未知'}`);
222
259
  console.log(' ═══════════════════════════════════════');
223
260
  console.log('');
224
261
 
225
- for (const [stage, def] of Object.entries(STEP_DEFINITIONS)) {
262
+ const statusIcons = { pending: '⬜', 'in-progress': '🔵', completed: '✅', failed: '❌', blocked: '🚫' };
263
+
264
+ for (const stage of VALID_STAGES) {
226
265
  const stageData = data.stages[stage] || emptyStage();
227
- const label = stageLabels[stage] || stage;
228
- const statusIcons = { not_started: '⬜', in_progress: '🔵', completed: '✅' };
266
+ const label = STAGE_LABELS[stage] || stage;
229
267
  const icon = statusIcons[stageData.status] || '⬜';
268
+ const isCurrent = data.currentStage === stage ? ' ◀' : '';
230
269
 
231
- let steps = '';
232
- for (const step of def) {
233
- if (stageData.completedSteps.includes(step.id)) {
234
- steps += '●';
235
- } else if (stageData.inProgressStep && stageData.inProgressStep.id === step.id) {
236
- steps += '';
237
- } else {
238
- steps += '○';
270
+ console.log(` ${icon} ${label}${isCurrent}`);
271
+
272
+ if (stageData.steps && stageData.steps.length > 0) {
273
+ for (const step of stageData.steps) {
274
+ const si = statusIcons[step.status] || '○';
275
+ const out = step.output ? ` — ${step.output.slice(0, 60)}` : '';
276
+ console.log(` ${si} ${step.name}${out}`);
239
277
  }
240
278
  }
241
279
 
242
- const completed = (stageData.completedSteps || []).length;
243
- const total = def.length;
244
- console.log(` ${icon} ${label} [${steps}] ${completed}/${total}`);
280
+ if (stageData.startedAt) {
281
+ console.log(` 开始: ${new Date(stageData.startedAt).toLocaleString('zh-CN')}`);
282
+ }
283
+ if (stageData.completedAt) {
284
+ console.log(` 完成: ${new Date(stageData.completedAt).toLocaleString('zh-CN')}`);
285
+ }
245
286
  }
246
287
 
247
288
  console.log('');
289
+ }
248
290
 
249
- // 产出文件
250
- if (data.artifacts && data.artifacts.length > 0) {
251
- console.log(' 📦 产出文件:');
252
- for (const a of data.artifacts) {
253
- console.log(` - ${a.name} (${a.stage}) → ${a.path}`);
254
- }
255
- console.log('');
291
+ status(cwd) {
292
+ this.show(cwd);
293
+ }
294
+
295
+ validate(cwd) {
296
+ const data = this.read(cwd);
297
+ if (!data) { console.log('❌ 无法读取 progress.json'); return false; }
298
+
299
+ const errors = [];
300
+ if (!data._version || !Number.isInteger(data._version) || data._version < 1) {
301
+ errors.push(`_version 缺失或无效(期望正整数,实际为 ${JSON.stringify(data._version)})`);
302
+ }
303
+ if (!data.stages || typeof data.stages !== 'object') errors.push('缺少 stages');
304
+ if (!VALID_STAGES.every(s => data.stages[s])) errors.push('缺少阶段定义');
305
+
306
+ if (errors.length === 0) { console.log('✅ progress.json 格式正确'); return true; }
307
+
308
+ console.log(`⚠️ 发现问题,尝试修复...`);
309
+ let fixed = { ...data, stages: { ...data.stages } };
310
+ let changed = false;
311
+ if (!fixed._version || !Number.isInteger(fixed._version) || fixed._version < 1) {
312
+ fixed._version = CURRENT_VERSION;
313
+ changed = true;
314
+ }
315
+ for (const s of VALID_STAGES) {
316
+ if (!fixed.stages[s]) { fixed.stages[s] = emptyStage(); changed = true; }
256
317
  }
318
+ if (changed) {
319
+ this._backup(cwd);
320
+ this._write(cwd, fixed);
321
+ console.log('✅ 已修复并备份');
322
+ }
323
+ return true;
257
324
  }
258
325
 
259
326
  reset(cwd, stage) {
260
327
  this._ensureDir(cwd);
261
328
  this._backup(cwd);
262
329
 
263
- const progressPath = join(cwd, RUNTIME_DIR, PROGRESS_FILE);
264
-
265
330
  if (stage) {
266
- // 只重置指定阶段
267
331
  const data = this.read(cwd);
268
332
  if (!data) { console.log('❌ 无法读取 progress.json'); return; }
269
333
  if (!data.stages[stage]) { console.log(`❌ 未知阶段: ${stage}`); return; }
270
-
271
334
  data.stages[stage] = emptyStage();
272
- data.lastActiveAt = new Date().toISOString();
273
- writeFileSync(progressPath, JSON.stringify(data, null, 2) + '\n');
335
+ data.lastActive = new Date().toISOString();
336
+ this._write(cwd, data);
274
337
  console.log(`✅ 已重置阶段: ${stage}`);
275
338
  } else {
276
- // 全部重置:备份后删除
277
- if (existsSync(progressPath)) {
278
- unlinkSync(progressPath);
279
- console.log('✅ 已重置所有进度(备份已保留)');
280
- } else {
281
- console.log('ℹ️ 无进度文件可重置');
282
- }
339
+ const p = this._path(cwd, PROGRESS_FILE);
340
+ if (existsSync(p)) { unlinkSync(p); console.log('✅ 已重置所有进度(备份已保留)'); }
341
+ else console.log('ℹ️ 无进度文件可重置');
283
342
  }
284
343
  }
285
344
 
286
345
  complete(cwd, stage) {
287
- if (!stage) {
288
- console.log('❌ 请指定阶段: --stage <stage>');
289
- return;
290
- }
291
-
292
- const data = this.read(cwd);
293
- if (!data) { console.log('❌ 无法读取 progress.json'); return; }
294
-
295
- const stageData = data.stages[stage];
296
- if (!stageData) { console.log(`❌ 未知阶段: ${stage}`); return; }
297
-
298
- // 归档到 history/
299
- const historyDir = join(cwd, RUNTIME_DIR, 'history');
300
- mkdirSync(historyDir, { recursive: true });
301
- const ts = new Date().toISOString().replace(/[:.]/g, '-');
302
- const archiveName = `${stage}-${ts}.json`;
303
- writeFileSync(join(historyDir, archiveName), JSON.stringify({ stage, data: stageData, completedAt: new Date().toISOString() }, null, 2) + '\n');
304
-
305
- console.log(`✅ 已归档 ${stage} → ${join(RUNTIME_DIR, 'history', archiveName)}`);
346
+ this.completeStage(cwd, stage);
306
347
  }
307
348
 
308
- // ── 内部方法 ──
349
+ // ── 内部辅助 ──
309
350
 
310
- _ensureDir(cwd) {
311
- const runtimeDir = join(cwd, RUNTIME_DIR);
312
- if (!existsSync(runtimeDir)) {
313
- mkdirSync(runtimeDir, { recursive: true });
314
- for (const d of ['artifacts', 'history', 'logs', 'templates']) {
315
- mkdirSync(join(runtimeDir, d), { recursive: true });
351
+ _readOrInit(cwd) {
352
+ let data = this.read(cwd);
353
+ if (!data) {
354
+ this._ensureDir(cwd);
355
+ const progressPath = this._path(cwd, PROGRESS_FILE);
356
+ if (!existsSync(progressPath)) {
357
+ data = makeInitialProgress(require('path').basename(cwd));
358
+ this._write(cwd, data);
359
+ } else {
360
+ console.log('❌ progress.json 损坏,请运行 sillyspec progress validate');
361
+ return null;
316
362
  }
317
363
  }
364
+ return data;
318
365
  }
319
366
 
320
- _backup(cwd) {
321
- const progressPath = join(cwd, RUNTIME_DIR, PROGRESS_FILE);
322
- const backupPath = join(cwd, RUNTIME_DIR, BACKUP_FILE);
323
- if (existsSync(progressPath)) {
324
- renameSync(progressPath, backupPath);
367
+ _requireStage(cwd, stage) {
368
+ if (!VALID_STAGES.includes(stage)) {
369
+ console.log(`❌ 未知阶段: ${stage},可选: ${VALID_STAGES.join(', ')}`);
370
+ return null;
325
371
  }
372
+ const data = this._readOrInit(cwd);
373
+ if (!data) return null;
374
+ if (!data.stages[stage]) data.stages[stage] = emptyStage();
375
+ return data;
326
376
  }
327
377
 
328
378
  _parseWithRecovery(jsonString) {
329
- // 第一层:直接解析
330
- try {
331
- return JSON.parse(jsonString);
332
- } catch {}
333
-
334
- // 第二层:修复常见问题
335
- let fixed = jsonString;
336
-
337
- // 去尾随逗号(对象和数组中的 ,} 和 ,])
338
- fixed = fixed.replace(/,\s*([}\]])/g, '$1');
379
+ try { return JSON.parse(jsonString); } catch {}
339
380
 
340
- // 单引号转双引号(简易处理)
341
- // 只处理 key 和简单字符串值
381
+ let fixed = jsonString.replace(/,\s*([}\]])/g, '$1');
342
382
  fixed = fixed.replace(/([{,]\s*)'([^']+)'(\s*:)/g, '$1"$2"$3');
343
383
  fixed = fixed.replace(/:\s*'([^']*)'([,}\]])/g, ':"$1"$2');
384
+ try { return JSON.parse(fixed); } catch {}
344
385
 
345
- try {
346
- return JSON.parse(fixed);
347
- } catch {}
348
-
349
- // 第三层:尝试截断到最后一个完整对象
350
386
  const lastBrace = fixed.lastIndexOf('}');
351
387
  if (lastBrace > 0) {
352
- const truncated = fixed.substring(0, lastBrace + 1);
353
- // 补全外层括号
354
388
  let open = 0;
355
- for (const ch of truncated) {
389
+ for (const ch of fixed.substring(0, lastBrace + 1)) {
356
390
  if (ch === '{') open++;
357
391
  if (ch === '}') open--;
358
392
  }
359
- const repaired = truncated + '}'.repeat(Math.max(0, open));
360
- try {
361
- return JSON.parse(repaired);
362
- } catch {}
393
+ try { return JSON.parse(fixed.substring(0, lastBrace + 1) + '}'.repeat(Math.max(0, open))); } catch {}
363
394
  }
364
-
365
395
  return null;
366
396
  }
367
397
 
368
- _validate(data) {
369
- const errors = [];
370
- if (!data || typeof data !== 'object') { errors.push('数据不是有效对象'); return errors; }
371
- if (!data.stages || typeof data.stages !== 'object') { errors.push('缺少 stages 字段'); }
372
- if (!data.currentStage || typeof data.currentStage !== 'string') { errors.push('缺少 currentStage 字段'); }
373
- if (!data.schemaVersion) { errors.push('缺少 schemaVersion 字段'); }
374
- if (typeof data._version !== 'number' || data._version < 1) { errors.push('_version 应为正整数'); }
375
-
376
- // 校验阶段数据
377
- if (data.stages) {
378
- for (const [name, stage] of Object.entries(data.stages)) {
379
- if (!stage.status) errors.push(`阶段 ${name} 缺少 status`);
380
- if (stage.completedSteps && !Array.isArray(stage.completedSteps)) {
381
- errors.push(`阶段 ${name} 的 completedSteps 不是数组`);
382
- }
383
- }
384
- }
385
-
386
- return errors;
387
- }
388
-
389
398
  _timeAgo(dateStr) {
390
399
  if (!dateStr) return '未知';
391
- const now = Date.now();
392
- const then = new Date(dateStr).getTime();
393
- const diff = now - then;
394
-
395
- const seconds = Math.floor(diff / 1000);
396
- const minutes = Math.floor(seconds / 60);
397
- const hours = Math.floor(minutes / 60);
398
- const days = Math.floor(hours / 24);
399
-
400
- if (days > 0) return `${days} 天前`;
401
- if (hours > 0) return `${hours} 小时前`;
402
- if (minutes > 0) return `${minutes} 分钟前`;
403
- return '刚刚';
404
- }
405
-
406
- _getStepDefinitions(stage) {
407
- return STEP_DEFINITIONS[stage] || [];
400
+ const diff = Date.now() - new Date(dateStr).getTime();
401
+ const m = Math.floor(diff / 60000);
402
+ if (m < 1) return '刚刚';
403
+ if (m < 60) return `${m} 分钟前`;
404
+ const h = Math.floor(m / 60);
405
+ if (h < 24) return `${h} 小时前`;
406
+ return `${Math.floor(h / 24)} 天前`;
408
407
  }
409
408
 
410
409
  _ensureGitignore(cwd) {
411
410
  const gitignorePath = join(cwd, '.gitignore');
412
411
  const rule = '.sillyspec/.runtime/';
413
-
414
412
  if (existsSync(gitignorePath)) {
415
413
  const content = readFileSync(gitignorePath, 'utf8');
416
414
  if (content.includes(rule)) return;