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/.claude/skills/sillyspec-archive/SKILL.md +4 -0
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +4 -0
- package/.claude/skills/sillyspec-doctor/SKILL.md +4 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +4 -0
- package/.claude/skills/sillyspec-explore/SKILL.md +4 -0
- package/.claude/skills/sillyspec-plan/SKILL.md +4 -0
- package/.claude/skills/sillyspec-propose/SKILL.md +4 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +4 -0
- package/.claude/skills/sillyspec-scan/SKILL.md +4 -0
- package/.claude/skills/sillyspec-status/SKILL.md +4 -0
- package/.claude/skills/sillyspec-verify/SKILL.md +4 -0
- package/package.json +1 -1
- package/src/index.js +16 -13
- package/src/init.js +5 -8
- package/src/progress.js +337 -90
- package/src/run.js +92 -74
- package/src/stages/brainstorm.js +1 -1
- package/src/stages/doctor.js +12 -6
- package/src/stages/scan.js +1 -1
package/src/progress.js
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SillySpec ProgressManager — 进度恢复管理
|
|
3
3
|
*
|
|
4
|
-
* 纯 Node.js
|
|
4
|
+
* 纯 Node.js,无外部依赖。支持多变更并行。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
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
|
|
20
|
+
const BACKUP_SUFFIX = '.bak';
|
|
15
21
|
|
|
16
|
-
const CURRENT_VERSION =
|
|
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
|
-
|
|
56
|
+
_runtimePath(cwd, ...parts) {
|
|
47
57
|
return join(cwd, RUNTIME_DIR, ...parts);
|
|
48
58
|
}
|
|
49
59
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
77
|
-
const
|
|
78
|
-
if (!
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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.
|
|
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
|
|
278
|
+
return this.readGlobal(cwd);
|
|
115
279
|
}
|
|
116
280
|
|
|
117
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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);
|