sillyspec 3.10.5 → 3.10.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/progress.js CHANGED
@@ -1,19 +1,25 @@
1
1
  /**
2
2
  * SillySpec ProgressManager — 进度恢复管理
3
3
  *
4
- * 纯 Node.js,无外部依赖。管理 .sillyspec/.runtime/progress.json。
4
+ * 纯 Node.js,无外部依赖。支持多变更并行。
5
5
  *
6
- * Schema v2: { project, currentStage, stages: { [name]: { status, steps, startedAt, completedAt } }, lastActive }
6
+ * 存储结构(v3):
7
+ * .sillyspec/.runtime/global.json — 全局状态(项目名、活跃变更列表)
8
+ * .sillyspec/changes/<name>/progress.json — 每个变更独立的阶段/步骤状态
9
+ *
10
+ * 向后兼容:如果存在旧的 .sillyspec/.runtime/progress.json,自动迁移。
7
11
  */
8
12
 
9
- import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, copyFileSync, unlinkSync } from 'fs';
13
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, copyFileSync, unlinkSync, readdirSync } from 'fs';
10
14
  import { join, basename } from 'path';
11
15
 
12
16
  const RUNTIME_DIR = '.sillyspec/.runtime';
17
+ const CHANGES_DIR = '.sillyspec/changes';
18
+ const GLOBAL_FILE = 'global.json';
13
19
  const PROGRESS_FILE = 'progress.json';
14
- const BACKUP_FILE = 'progress.json.bak';
20
+ const BACKUP_SUFFIX = '.bak';
15
21
 
16
- const CURRENT_VERSION = 2;
22
+ const CURRENT_VERSION = 3;
17
23
  const VALID_STAGES = ['scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore'];
18
24
  const VALID_STATUSES = ['pending', 'in-progress', 'completed', 'failed', 'blocked'];
19
25
 
@@ -38,18 +44,131 @@ function makeInitialProgress(project) {
38
44
  return { _version: CURRENT_VERSION, project: project || '', currentStage: '', currentChange: null, stages, lastActive: null };
39
45
  }
40
46
 
47
+ function makeInitialGlobal(project) {
48
+ return { _version: CURRENT_VERSION, project: project || '', activeChanges: [] };
49
+ }
50
+
41
51
  // ── ProgressManager ──
42
52
 
43
53
  export class ProgressManager {
44
- // ── 核心读写 ──
54
+ // ── 路径工具 ──
45
55
 
46
- _path(cwd, ...parts) {
56
+ _runtimePath(cwd, ...parts) {
47
57
  return join(cwd, RUNTIME_DIR, ...parts);
48
58
  }
49
59
 
50
- read(cwd) {
51
- const progressPath = this._path(cwd, PROGRESS_FILE);
52
- const backupPath = this._path(cwd, BACKUP_FILE);
60
+ _changePath(cwd, changeName, ...parts) {
61
+ return join(cwd, CHANGES_DIR, changeName, ...parts);
62
+ }
63
+
64
+ _ensureRuntimeDir(cwd) {
65
+ const runtimeDir = this._runtimePath(cwd);
66
+ if (!existsSync(runtimeDir)) {
67
+ mkdirSync(runtimeDir, { recursive: true });
68
+ for (const d of ['artifacts', 'history', 'logs', 'templates']) {
69
+ mkdirSync(join(runtimeDir, d), { recursive: true });
70
+ }
71
+ }
72
+ }
73
+
74
+ _ensureChangeDir(cwd, changeName) {
75
+ const dir = this._changePath(cwd, changeName);
76
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
77
+ return dir;
78
+ }
79
+
80
+ // ── 向后兼容:检测并迁移旧版 progress.json ──
81
+
82
+ _migrateIfNeeded(cwd) {
83
+ const oldPath = this._runtimePath(cwd, PROGRESS_FILE);
84
+ const globalPath = this._runtimePath(cwd, GLOBAL_FILE);
85
+
86
+ // 新版已存在,不迁移
87
+ if (existsSync(globalPath)) return;
88
+
89
+ // 旧版不存在,不迁移
90
+ if (!existsSync(oldPath)) return;
91
+
92
+ const oldData = this._parseWithRecovery(readFileSync(oldPath, 'utf8'));
93
+ if (!oldData) return;
94
+
95
+ console.log('🔄 检测到旧版 progress.json,正在迁移到按变更隔离存储...');
96
+
97
+ // 提取变更名
98
+ const changeName = oldData.currentChange || 'default';
99
+
100
+ // 迁移:将旧 progress.json 复制到变更目录
101
+ this._ensureChangeDir(cwd, changeName);
102
+ const newChangePath = this._changePath(cwd, changeName, PROGRESS_FILE);
103
+ if (!existsSync(newChangePath)) {
104
+ writeFileSync(newChangePath, JSON.stringify(oldData, null, 2) + '\n');
105
+ }
106
+
107
+ // 创建全局文件
108
+ const globalData = makeInitialGlobal(oldData.project);
109
+ globalData.activeChanges = [changeName];
110
+ if (!existsSync(globalPath)) {
111
+ writeFileSync(globalPath, JSON.stringify(globalData, null, 2) + '\n');
112
+ }
113
+
114
+ // 备份旧文件
115
+ const backupPath = oldPath + BACKUP_SUFFIX;
116
+ if (!existsSync(backupPath)) copyFileSync(oldPath, backupPath);
117
+ unlinkSync(oldPath);
118
+
119
+ console.log(` ✅ 已迁移到 .sillyspec/changes/${changeName}/progress.json`);
120
+ console.log(` 📦 旧文件已备份到 .sillyspec/.runtime/progress.json.bak`);
121
+ }
122
+
123
+ // ── 全局状态 ──
124
+
125
+ readGlobal(cwd) {
126
+ this._migrateIfNeeded(cwd);
127
+ const globalPath = this._runtimePath(cwd, GLOBAL_FILE);
128
+ if (!existsSync(globalPath)) return null;
129
+ return this._parseWithRecovery(readFileSync(globalPath, 'utf8'));
130
+ }
131
+
132
+ writeGlobal(cwd, data) {
133
+ this._ensureRuntimeDir(cwd);
134
+ const globalPath = this._runtimePath(cwd, GLOBAL_FILE);
135
+ const tmpPath = globalPath + '.tmp';
136
+ writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n');
137
+ renameSync(tmpPath, globalPath);
138
+ }
139
+
140
+ // ── 变更级别状态 ──
141
+
142
+ /**
143
+ * 读取指定变更的 progress
144
+ * @param {string} cwd
145
+ * @param {string|null} changeName - 变更名,null 时尝试自动检测
146
+ */
147
+ read(cwd, changeName = null) {
148
+ // 向后兼容:如果没有 changeName,尝试读旧版路径
149
+ if (!changeName) {
150
+ // 先看新版全局文件
151
+ const global = this.readGlobal(cwd);
152
+ if (global && global.activeChanges && global.activeChanges.length === 1) {
153
+ changeName = global.activeChanges[0];
154
+ } else {
155
+ // fallback:扫描 changes 目录
156
+ const changes = this.listChanges(cwd);
157
+ if (changes.length === 1) {
158
+ changeName = changes[0];
159
+ } else {
160
+ // 最后尝试旧版路径
161
+ const oldPath = this._runtimePath(cwd, PROGRESS_FILE);
162
+ if (existsSync(oldPath)) {
163
+ return this._parseWithRecovery(readFileSync(oldPath, 'utf8'));
164
+ }
165
+ return null;
166
+ }
167
+ }
168
+ }
169
+
170
+ const progressPath = this._changePath(cwd, changeName, PROGRESS_FILE);
171
+ const backupPath = progressPath + BACKUP_SUFFIX;
53
172
 
54
173
  for (const p of [progressPath, backupPath]) {
55
174
  if (!existsSync(p)) continue;
@@ -65,62 +184,125 @@ export class ProgressManager {
65
184
  return null;
66
185
  }
67
186
 
68
- _write(cwd, data) {
69
- const progressPath = this._path(cwd, PROGRESS_FILE);
187
+ /**
188
+ * 写入指定变更的 progress
189
+ * @param {string} cwd
190
+ * @param {object} data
191
+ * @param {string|null} changeName - 从 data.currentChange 推导,或显式传入
192
+ */
193
+ _write(cwd, data, changeName = null) {
194
+ const cn = changeName || data.currentChange;
195
+ if (!cn) {
196
+ // 无变更名时 fallback 到旧路径(不应该发生,但保底)
197
+ const progressPath = this._runtimePath(cwd, PROGRESS_FILE);
198
+ this._ensureRuntimeDir(cwd);
199
+ const tmpPath = progressPath + '.tmp';
200
+ writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n');
201
+ renameSync(tmpPath, progressPath);
202
+ return;
203
+ }
204
+
205
+ this._ensureChangeDir(cwd, cn);
206
+ const progressPath = this._changePath(cwd, cn, PROGRESS_FILE);
70
207
  const tmpPath = progressPath + '.tmp';
71
- this._ensureDir(cwd);
72
208
  writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n');
73
209
  renameSync(tmpPath, progressPath);
74
210
  }
75
211
 
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
- }
212
+ _backup(cwd, data) {
213
+ const cn = data?.currentChange;
214
+ if (!cn) return;
215
+ const p = this._changePath(cwd, cn, PROGRESS_FILE);
216
+ if (existsSync(p)) copyFileSync(p, this._changePath(cwd, cn, PROGRESS_FILE + BACKUP_SUFFIX));
217
+ }
218
+
219
+ // ── 变更管理 ──
220
+
221
+ /**
222
+ * 列出所有变更名(不含 archive 子目录)
223
+ */
224
+ listChanges(cwd) {
225
+ const changesDir = join(cwd, CHANGES_DIR);
226
+ if (!existsSync(changesDir)) return [];
227
+ return readdirSync(changesDir, { withFileTypes: true })
228
+ .filter(e => e.isDirectory() && e.name !== 'archive')
229
+ .map(e => e.name);
230
+ }
231
+
232
+ /**
233
+ * 注册变更到全局活跃列表
234
+ */
235
+ registerChange(cwd, changeName) {
236
+ let global = this.readGlobal(cwd);
237
+ if (!global) {
238
+ global = makeInitialGlobal(basename(cwd));
239
+ }
240
+ if (!global.activeChanges.includes(changeName)) {
241
+ global.activeChanges.push(changeName);
242
+ this.writeGlobal(cwd, global);
83
243
  }
84
244
  }
85
245
 
86
- _backup(cwd) {
87
- const p = this._path(cwd, PROGRESS_FILE);
88
- if (existsSync(p)) copyFileSync(p, this._path(cwd, BACKUP_FILE));
246
+ /**
247
+ * 从全局活跃列表移除变更(归档时调用)
248
+ */
249
+ unregisterChange(cwd, changeName) {
250
+ const global = this.readGlobal(cwd);
251
+ if (!global) return;
252
+ global.activeChanges = global.activeChanges.filter(c => c !== changeName);
253
+ this.writeGlobal(cwd, global);
89
254
  }
90
255
 
91
256
  // ── CLI 命令 ──
92
257
 
93
258
  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);
259
+ this._migrateIfNeeded(cwd);
260
+ this._ensureRuntimeDir(cwd);
261
+
262
+ const globalPath = this._runtimePath(cwd, GLOBAL_FILE);
263
+ if (!existsSync(globalPath)) {
264
+ const data = makeInitialGlobal(basename(cwd));
265
+ this.writeGlobal(cwd, data);
266
+ console.log(`✅ 已创建全局状态文件`);
267
+ } else {
268
+ console.log(`ℹ️ 全局状态文件已存在,跳过`);
100
269
  }
101
270
 
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
271
  // 创建 user-inputs.md
108
- const inputsPath = this._path(cwd, 'user-inputs.md');
272
+ const inputsPath = this._runtimePath(cwd, 'user-inputs.md');
109
273
  if (!existsSync(inputsPath)) {
110
274
  writeFileSync(inputsPath, '# 用户输入记录\n\n> 每步完成时由 AI 自动追加,记录用户所有原话。\n\n');
111
275
  }
112
276
 
113
277
  this._ensureGitignore(cwd);
114
- return data;
278
+ return this.readGlobal(cwd);
115
279
  }
116
280
 
117
- setStage(cwd, stage) {
281
+ /**
282
+ * 初始化指定变更的 progress
283
+ */
284
+ initChange(cwd, changeName) {
285
+ this._ensureChangeDir(cwd, changeName);
286
+ this.registerChange(cwd, changeName);
287
+
288
+ const progressPath = this._changePath(cwd, changeName, PROGRESS_FILE);
289
+ if (!existsSync(progressPath)) {
290
+ const data = makeInitialProgress(basename(cwd));
291
+ data.currentChange = changeName;
292
+ this._write(cwd, data, changeName);
293
+ console.log(`✅ 已创建变更 ${changeName} 的 progress.json`);
294
+ }
295
+
296
+ return this.read(cwd, changeName);
297
+ }
298
+
299
+ setStage(cwd, stage, changeName = null) {
118
300
  if (!VALID_STAGES.includes(stage)) {
119
301
  console.log(`❌ 未知阶段: ${stage},可选: ${VALID_STAGES.join(', ')}`);
120
302
  return;
121
303
  }
122
304
 
123
- const data = this._readOrInit(cwd);
305
+ const data = this._readOrInit(cwd, changeName);
124
306
  if (!data) return;
125
307
 
126
308
  if (!data.stages[stage]) data.stages[stage] = emptyStage();
@@ -133,14 +315,14 @@ export class ProgressManager {
133
315
  }
134
316
  data.lastActive = new Date().toLocaleString('zh-CN',{hour12:false});
135
317
 
136
- this._backup(cwd);
318
+ this._backup(cwd, data);
137
319
  this._write(cwd, data);
138
320
  console.log(`✅ 当前阶段已设为: ${STAGE_LABELS[stage] || stage} (${stageData.status})`);
139
321
  }
140
322
 
141
- addStep(cwd, stage, stepName) {
323
+ addStep(cwd, stage, stepName, changeName = null) {
142
324
  if (!stepName) { console.log('❌ 请指定步骤名称'); return; }
143
- const data = this._requireStage(cwd, stage);
325
+ const data = this._requireStage(cwd, stage, changeName);
144
326
  if (!data) return;
145
327
 
146
328
  const stageData = data.stages[stage];
@@ -152,15 +334,15 @@ export class ProgressManager {
152
334
  stageData.steps.push({ name: stepName, status: 'pending' });
153
335
  data.lastActive = new Date().toLocaleString('zh-CN',{hour12:false});
154
336
 
155
- this._backup(cwd);
337
+ this._backup(cwd, data);
156
338
  this._write(cwd, data);
157
339
  console.log(`✅ 已添加步骤: ${stage}/${stepName}`);
158
340
  }
159
341
 
160
- updateStep(cwd, stage, stepName, options = {}) {
342
+ updateStep(cwd, stage, stepName, options = {}, changeName = null) {
161
343
  const { status, output } = options;
162
344
  if (!stepName) { console.log('❌ 请指定步骤名称'); return; }
163
- const data = this._requireStage(cwd, stage);
345
+ const data = this._requireStage(cwd, stage, changeName);
164
346
  if (!data) return;
165
347
 
166
348
  const stageData = data.stages[stage];
@@ -184,18 +366,18 @@ export class ProgressManager {
184
366
  }
185
367
 
186
368
  data.lastActive = new Date().toLocaleString('zh-CN',{hour12:false});
187
- this._backup(cwd);
369
+ this._backup(cwd, data);
188
370
  this._write(cwd, data);
189
371
  console.log(`✅ 步骤已更新: ${stage}/${stepName} → ${status || step.status}`);
190
372
  }
191
373
 
192
- completeStage(cwd, stage) {
374
+ completeStage(cwd, stage, changeName = null) {
193
375
  if (!VALID_STAGES.includes(stage)) {
194
376
  console.log(`❌ 未知阶段: ${stage}`);
195
377
  return;
196
378
  }
197
379
 
198
- const data = this._readOrInit(cwd);
380
+ const data = this._readOrInit(cwd, changeName);
199
381
  if (!data) return;
200
382
 
201
383
  if (!data.stages[stage]) data.stages[stage] = emptyStage();
@@ -210,27 +392,74 @@ export class ProgressManager {
210
392
 
211
393
  data.lastActive = new Date().toLocaleString('zh-CN',{hour12:false});
212
394
 
213
- // 归档到 history/(ISO 时间戳,去掉所有非法路径字符)
214
- const historyDir = this._path(cwd, 'history');
395
+ // 归档到 history/(ISO 时间戳)
396
+ const historyDir = this._runtimePath(cwd, 'history');
215
397
  mkdirSync(historyDir, { recursive: true });
216
398
  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');
399
+ const cn = data.currentChange || 'unknown';
400
+ writeFileSync(join(historyDir, `${cn}-${stage}-${ts}.json`), JSON.stringify({ change: cn, stage, data: stageData, completedAt: stageData.completedAt }, null, 2) + '\n');
218
401
 
219
- this._backup(cwd);
402
+ this._backup(cwd, data);
220
403
  this._write(cwd, data);
221
404
 
222
405
  console.log(`✅ 阶段 ${stage} 已标记为完成(不自动推进,下一步由你决定)`);
223
406
  }
224
407
 
225
- show(cwd) {
226
- const data = this.read(cwd);
408
+ show(cwd, changeName = null) {
409
+ // 如果指定了变更名,只显示该变更
410
+ if (changeName) {
411
+ return this._showChange(cwd, changeName);
412
+ }
413
+
414
+ // 否则显示所有变更
415
+ const changes = this.listChanges(cwd);
416
+ if (changes.length === 0) {
417
+ console.log('ℹ️ 没有活跃的变更');
418
+ return;
419
+ }
420
+
421
+ if (changes.length === 1) {
422
+ return this._showChange(cwd, changes[0]);
423
+ }
424
+
425
+ // 多个变更:汇总显示
426
+ const global = this.readGlobal(cwd);
427
+ console.log('');
428
+ console.log(' ═══════════════════════════════════════');
429
+ console.log(` 项目: ${(global?.project) || basename(cwd) || '(未命名)'}`);
430
+ console.log(` 活跃变更: ${changes.length} 个`);
431
+ console.log(' ═══════════════════════════════════════');
432
+ console.log('');
433
+
434
+ for (const cn of changes) {
435
+ const data = this.read(cwd, cn);
436
+ if (!data) {
437
+ console.log(` 📂 ${cn} — (无法读取)`);
438
+ continue;
439
+ }
440
+ const currentStage = data.currentStage || '(无)';
441
+ const stageLabel = STAGE_LABELS[data.currentStage] || currentStage;
442
+ const lastActive = data.lastActive ? this._timeAgo(data.lastActive) : '未知';
443
+
444
+ console.log(` 📂 ${cn}`);
445
+ console.log(` 当前阶段: ${stageLabel} 最近活跃: ${lastActive}`);
446
+ console.log('');
447
+ }
448
+
449
+ console.log(` 💡 查看详情:sillyspec progress show --change <name>`);
450
+ console.log('');
451
+ }
452
+
453
+ _showChange(cwd, changeName) {
454
+ const data = this.read(cwd, changeName);
227
455
  if (!data) {
228
- console.log('❌ 未找到 progress.json,请先运行 sillyspec progress init');
456
+ console.log(`❌ 未找到变更 ${changeName} 的 progress.json`);
229
457
  return;
230
458
  }
231
459
 
232
460
  console.log('');
233
461
  console.log(' ═══════════════════════════════════════');
462
+ console.log(` 变更: ${changeName}`);
234
463
  console.log(` 项目: ${data.project || '(未命名)'}`);
235
464
  console.log(` 当前阶段: ${STAGE_LABELS[data.currentStage] || data.currentStage || '(无)'}`);
236
465
  console.log(` 最近活跃: ${data.lastActive ? this._timeAgo(data.lastActive) : '未知'}`);
@@ -275,12 +504,12 @@ export class ProgressManager {
275
504
  console.log('');
276
505
  }
277
506
 
278
- status(cwd) {
279
- this.show(cwd);
507
+ status(cwd, changeName = null) {
508
+ this.show(cwd, changeName);
280
509
  }
281
510
 
282
- async validate(cwd) {
283
- const data = this.read(cwd);
511
+ async validate(cwd, changeName = null) {
512
+ const data = this.read(cwd, changeName);
284
513
  if (!data) { console.log('❌ 无法读取 progress.json'); return false; }
285
514
 
286
515
  const errors = [];
@@ -307,7 +536,7 @@ export class ProgressManager {
307
536
  if (!fixed.stages[s]) { fixed.stages[s] = emptyStage(); changed = true; }
308
537
  }
309
538
  if (changed) {
310
- this._backup(cwd);
539
+ this._backup(cwd, fixed);
311
540
  this._write(cwd, fixed);
312
541
  console.log('✅ 已修复并备份');
313
542
  }
@@ -315,56 +544,76 @@ export class ProgressManager {
315
544
  return true;
316
545
  }
317
546
 
318
- reset(cwd, stage) {
319
- this._ensureDir(cwd);
320
-
547
+ reset(cwd, stage, changeName = null) {
321
548
  if (stage) {
322
- this._backup(cwd);
323
- const data = this.read(cwd);
549
+ const data = this.read(cwd, changeName);
324
550
  if (!data) { console.log('❌ 无法读取 progress.json'); return; }
551
+ this._backup(cwd, data);
325
552
  if (!data.stages[stage]) { console.log(`❌ 未知阶段: ${stage}`); return; }
326
553
  data.stages[stage] = emptyStage();
327
554
  data.lastActive = new Date().toLocaleString('zh-CN',{hour12:false});
328
555
  this._write(cwd, data);
329
556
  console.log(`✅ 已重置阶段: ${stage}`);
330
557
  } 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('✅ 已重置所有进度');
558
+ // 重置所有变更或指定变更
559
+ if (changeName) {
560
+ const p = this._changePath(cwd, changeName, PROGRESS_FILE);
561
+ const backup = this._changePath(cwd, changeName, PROGRESS_FILE + BACKUP_SUFFIX);
562
+ let didReset = false;
563
+ if (existsSync(p)) { unlinkSync(p); didReset = true; }
564
+ if (existsSync(backup)) { unlinkSync(backup); didReset = true; }
565
+ if (didReset) console.log(`✅ 已重置变更 ${changeName} 的进度`);
566
+ else console.log('ℹ️ 无进度文件可重置');
338
567
  } else {
339
- console.log('ℹ️ 无进度文件可重置');
568
+ const changes = this.listChanges(cwd);
569
+ for (const cn of changes) {
570
+ const p = this._changePath(cwd, cn, PROGRESS_FILE);
571
+ const backup = this._changePath(cwd, cn, PROGRESS_FILE + BACKUP_SUFFIX);
572
+ if (existsSync(p)) unlinkSync(p);
573
+ if (existsSync(backup)) unlinkSync(backup);
574
+ }
575
+ console.log('✅ 已重置所有变更的进度');
340
576
  }
341
577
  }
342
578
  }
343
579
 
344
580
  // ── 内部辅助 ──
345
581
 
346
- _readOrInit(cwd) {
347
- let data = this.read(cwd);
582
+ _readOrInit(cwd, changeName = null) {
583
+ let data = this.read(cwd, changeName);
348
584
  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');
585
+ // 尝试自动检测变更名
586
+ if (!changeName) {
587
+ const changes = this.listChanges(cwd);
588
+ if (changes.length === 1) changeName = changes[0];
589
+ }
590
+ if (changeName) {
591
+ this._ensureChangeDir(cwd, changeName);
592
+ const progressPath = this._changePath(cwd, changeName, PROGRESS_FILE);
593
+ if (!existsSync(progressPath)) {
594
+ data = makeInitialProgress(basename(cwd));
595
+ data.currentChange = changeName;
596
+ this._write(cwd, data, changeName);
597
+ this.registerChange(cwd, changeName);
598
+ }
599
+ }
600
+ if (!data) {
601
+ data = this.read(cwd, changeName);
602
+ }
603
+ if (!data) {
604
+ console.log('❌ 无法确定当前变更,请指定 --change <name>');
356
605
  return null;
357
606
  }
358
607
  }
359
608
  return data;
360
609
  }
361
610
 
362
- _requireStage(cwd, stage) {
611
+ _requireStage(cwd, stage, changeName = null) {
363
612
  if (!VALID_STAGES.includes(stage)) {
364
613
  console.log(`❌ 未知阶段: ${stage},可选: ${VALID_STAGES.join(', ')}`);
365
614
  return null;
366
615
  }
367
- const data = this._readOrInit(cwd);
616
+ const data = this._readOrInit(cwd, changeName);
368
617
  if (!data) return null;
369
618
  if (!data.stages[stage]) data.stages[stage] = emptyStage();
370
619
  return data;
@@ -393,7 +642,6 @@ export class ProgressManager {
393
642
  _timeAgo(dateStr) {
394
643
  if (!dateStr) return '未知';
395
644
  let ts = Date.parse(dateStr);
396
- // toLocaleString('zh-CN') 格式(如 2026/5/12 21:09:00)可能解析失败,尝试手动解析
397
645
  if (isNaN(ts)) {
398
646
  const m = dateStr.match(/(\d{4})[\/-](\d{1,2})[\/-](\d{1,2})[\s,]+(\d{1,2}):(\d{2})(?::(\d{2}))?/);
399
647
  if (m) ts = new Date(+m[1], +m[2]-1, +m[3], +m[4], +m[5], +(m[6]||0)).getTime();
@@ -410,8 +658,8 @@ export class ProgressManager {
410
658
 
411
659
  // ── 批量进度 ──
412
660
 
413
- updateBatchProgress(cwd, batchData) {
414
- const data = this._readOrInit(cwd);
661
+ updateBatchProgress(cwd, batchData, changeName = null) {
662
+ const data = this._readOrInit(cwd, changeName);
415
663
  if (!data) return;
416
664
 
417
665
  if (!data.batchProgress) {
@@ -423,19 +671,18 @@ export class ProgressManager {
423
671
  if (batchData.skipped !== undefined) data.batchProgress.skipped = batchData.skipped;
424
672
 
425
673
  data.lastActive = new Date().toLocaleString('zh-CN', { hour12: false });
426
- this._backup(cwd);
674
+ this._backup(cwd, data);
427
675
  this._write(cwd, data);
428
676
  }
429
677
 
430
- readBatchProgress(cwd) {
431
- const data = this.read(cwd);
678
+ readBatchProgress(cwd, changeName = null) {
679
+ const data = this.read(cwd, changeName);
432
680
  return data?.batchProgress || null;
433
681
  }
434
682
 
435
683
  _renderBatchProgress(batchProgress) {
436
684
  if (!batchProgress || !batchProgress.total) return null;
437
685
  const { total, completed = 0, failed = 0, skipped = 0 } = batchProgress;
438
- const done = Math.min(completed + failed + skipped, total);
439
686
  const barLen = 20;
440
687
  const filled = Math.round((completed / total) * barLen);
441
688
  const bar = '█'.repeat(filled) + '░'.repeat(barLen - filled);