sillyspec 3.11.10 → 3.12.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.
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +7 -5
- package/.claude/skills/sillyspec-resume/SKILL.md +21 -64
- package/.claude/skills/sillyspec-workspace/SKILL.md +10 -2
- package/docs/sillyspec/file-lifecycle.md +1110 -0
- package/package.json +2 -1
- package/src/db.js +168 -0
- package/src/hooks/worktree-guard.js +110 -26
- package/src/index.js +98 -2
- package/src/init.js +2 -8
- package/src/progress.js +652 -325
- package/src/run.js +123 -24
- package/src/stages/archive.js +7 -11
- package/src/stages/verify.js +6 -6
- package/src/sync.js +497 -0
- package/src/worktree.js +30 -0
- package/.npmrc.tmp +0 -1
package/src/progress.js
CHANGED
|
@@ -10,17 +10,16 @@
|
|
|
10
10
|
* 向后兼容:如果存在旧的 .sillyspec/.runtime/progress.json,自动迁移。
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync,
|
|
14
|
-
import { join, basename
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync } from 'fs';
|
|
14
|
+
import { join, basename } from 'path';
|
|
15
|
+
import { DB } from './db.js';
|
|
15
16
|
|
|
16
17
|
const RUNTIME_DIR = '.sillyspec/.runtime';
|
|
17
18
|
const CHANGES_DIR = '.sillyspec/changes';
|
|
18
19
|
const GLOBAL_FILE = 'global.json';
|
|
19
20
|
const PROGRESS_FILE = 'progress.json';
|
|
20
|
-
const BACKUP_SUFFIX = '.bak';
|
|
21
|
-
|
|
22
21
|
const CURRENT_VERSION = 3;
|
|
23
|
-
const VALID_STAGES = ['scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore'];
|
|
22
|
+
const VALID_STAGES = ['scan', 'brainstorm', 'propose', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore'];
|
|
24
23
|
const VALID_STATUSES = ['pending', 'in-progress', 'completed', 'failed', 'blocked'];
|
|
25
24
|
|
|
26
25
|
const STAGE_LABELS = {
|
|
@@ -71,117 +70,193 @@ export class ProgressManager {
|
|
|
71
70
|
}
|
|
72
71
|
}
|
|
73
72
|
|
|
73
|
+
/** 懒初始化 DB 连接,缓存在实例上 */
|
|
74
|
+
async _ensureDB(cwd) {
|
|
75
|
+
if (!this._db) {
|
|
76
|
+
this._db = new DB(this._runtimePath(cwd, 'sillyspec.db'));
|
|
77
|
+
await this._db.init();
|
|
78
|
+
}
|
|
79
|
+
return this._db;
|
|
80
|
+
}
|
|
81
|
+
|
|
74
82
|
_ensureChangeDir(cwd, changeName) {
|
|
75
83
|
const dir = this._changePath(cwd, changeName);
|
|
76
84
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
77
85
|
return dir;
|
|
78
86
|
}
|
|
79
87
|
|
|
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
88
|
// ── 全局状态 ──
|
|
124
89
|
|
|
125
|
-
readGlobal(cwd) {
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
90
|
+
async readGlobal(cwd) {
|
|
91
|
+
// SQL: SELECT FROM project + changes
|
|
92
|
+
const db = await this._ensureDB(cwd);
|
|
93
|
+
const sqlDb = db.getDb();
|
|
94
|
+
|
|
95
|
+
// 读取 project 行(id=1)
|
|
96
|
+
const rows = sqlDb.exec('SELECT name, schema_version FROM project WHERE id = 1');
|
|
97
|
+
if (!rows || rows.length === 0 || rows[0].values.length === 0) return null;
|
|
98
|
+
const [name, schemaVersion] = rows[0].values[0];
|
|
99
|
+
|
|
100
|
+
// 读取 active 变更列表
|
|
101
|
+
const changeRows = sqlDb.exec("SELECT name FROM changes WHERE status = 'active' ORDER BY name");
|
|
102
|
+
const activeChanges = changeRows && changeRows.length > 0
|
|
103
|
+
? changeRows[0].values.map(r => r[0])
|
|
104
|
+
: [];
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
_version: schemaVersion,
|
|
108
|
+
project: name,
|
|
109
|
+
activeChanges,
|
|
110
|
+
};
|
|
130
111
|
}
|
|
131
112
|
|
|
132
|
-
writeGlobal(cwd, data) {
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
113
|
+
async writeGlobal(cwd, data) {
|
|
114
|
+
// SQL: UPDATE project + UPSERT changes status
|
|
115
|
+
const db = await this._ensureDB(cwd);
|
|
116
|
+
db.transaction((sqlDb) => {
|
|
117
|
+
const now = new Date().toISOString();
|
|
118
|
+
|
|
119
|
+
// UPSERT project 行
|
|
120
|
+
sqlDb.run(`
|
|
121
|
+
INSERT INTO project (id, name, schema_version, created_at, updated_at)
|
|
122
|
+
VALUES (1, ?, ?, ?, ?)
|
|
123
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
124
|
+
name = excluded.name,
|
|
125
|
+
schema_version = excluded.schema_version,
|
|
126
|
+
updated_at = excluded.updated_at
|
|
127
|
+
`, [data.project || '', data._version || CURRENT_VERSION, now, now]);
|
|
128
|
+
|
|
129
|
+
// 同步 changes 表:确保 activeChanges 列表中的变更存在且为 active,
|
|
130
|
+
// 不在列表中的设为 archived
|
|
131
|
+
const activeChanges = data.activeChanges || [];
|
|
132
|
+
for (const cn of activeChanges) {
|
|
133
|
+
sqlDb.run(`
|
|
134
|
+
INSERT INTO changes (name, status, created_at, last_active)
|
|
135
|
+
VALUES (?, 'active', ?, ?)
|
|
136
|
+
ON CONFLICT(name) DO UPDATE SET status = 'active', last_active = excluded.last_active
|
|
137
|
+
`, [cn, now, now]);
|
|
138
|
+
}
|
|
139
|
+
if (activeChanges.length > 0) {
|
|
140
|
+
sqlDb.run(`
|
|
141
|
+
UPDATE changes SET status = 'archived'
|
|
142
|
+
WHERE status = 'active' AND name NOT IN (${activeChanges.map(() => '?').join(',')})
|
|
143
|
+
`, activeChanges);
|
|
144
|
+
} else {
|
|
145
|
+
// 没有活跃变更,将所有 active 归档
|
|
146
|
+
sqlDb.run("UPDATE changes SET status = 'archived' WHERE status = 'active'");
|
|
147
|
+
}
|
|
148
|
+
});
|
|
138
149
|
}
|
|
139
150
|
|
|
140
151
|
// ── 变更级别状态 ──
|
|
141
152
|
|
|
142
153
|
/**
|
|
143
|
-
* 读取指定变更的 progress
|
|
154
|
+
* 读取指定变更的 progress(SQL 版)
|
|
144
155
|
* @param {string} cwd
|
|
145
156
|
* @param {string|null} changeName - 变更名,null 时尝试自动检测
|
|
157
|
+
* @returns {Promise<object|null>} 与旧版 progress.json 结构完全一致的 JS 对象
|
|
146
158
|
*/
|
|
147
|
-
read(cwd, changeName = null) {
|
|
148
|
-
//
|
|
159
|
+
async read(cwd, changeName = null) {
|
|
160
|
+
// 自动检测变更名
|
|
149
161
|
if (!changeName) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
changeName = global.activeChanges[0];
|
|
162
|
+
const changes = await this.listChanges(cwd);
|
|
163
|
+
if (changes.length === 1) {
|
|
164
|
+
changeName = changes[0];
|
|
154
165
|
} else {
|
|
155
|
-
//
|
|
156
|
-
|
|
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
|
-
}
|
|
166
|
+
// 多个或零个活跃变更,无法确定
|
|
167
|
+
return null;
|
|
167
168
|
}
|
|
168
169
|
}
|
|
169
170
|
|
|
170
|
-
const
|
|
171
|
-
const
|
|
171
|
+
const db = await this._ensureDB(cwd);
|
|
172
|
+
const sqlDb = db.getDb();
|
|
173
|
+
|
|
174
|
+
// 1. 从 changes 表获取基本信息
|
|
175
|
+
const changeRows = sqlDb.exec('SELECT id, name, current_stage, no_worktree, last_active FROM changes WHERE name = ?', [changeName]);
|
|
176
|
+
if (!changeRows || changeRows.length === 0 || changeRows[0].values.length === 0) return null;
|
|
177
|
+
const [changeId, cName, currentStage, noWorktree, lastActive] = changeRows[0].values[0];
|
|
178
|
+
|
|
179
|
+
// 2. 从 stages 表获取所有阶段
|
|
180
|
+
const stageRows = sqlDb.exec('SELECT id, stage, status, started_at, completed_at FROM stages WHERE change_id = ? ORDER BY id', [changeId]);
|
|
181
|
+
const stageMap = {};
|
|
182
|
+
const stageIds = [];
|
|
183
|
+
if (stageRows && stageRows.length > 0) {
|
|
184
|
+
for (const [sId, stage, status, startedAt, completedAt] of stageRows[0].values) {
|
|
185
|
+
stageMap[stage] = { _dbId: sId, status, startedAt, completedAt };
|
|
186
|
+
stageIds.push(sId);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
172
189
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
190
|
+
// 3. 从 steps 表获取所有步骤
|
|
191
|
+
let stepRows = null;
|
|
192
|
+
if (stageIds.length > 0) {
|
|
193
|
+
const placeholders = stageIds.map(() => '?').join(',');
|
|
194
|
+
stepRows = sqlDb.exec(
|
|
195
|
+
`SELECT stage_id, name, status, output, completed_at, ordering FROM steps WHERE stage_id IN (${placeholders}) ORDER BY stage_id, ordering`,
|
|
196
|
+
stageIds
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
// 按阶段分组步骤
|
|
200
|
+
const stepsByStage = {};
|
|
201
|
+
if (stepRows && stepRows.length > 0) {
|
|
202
|
+
for (const [stageId, name, status, output, completedAt, ordering] of stepRows[0].values) {
|
|
203
|
+
if (!stepsByStage[stageId]) stepsByStage[stageId] = [];
|
|
204
|
+
stepsByStage[stageId].push({ name, status, output, completedAt });
|
|
182
205
|
}
|
|
183
206
|
}
|
|
184
|
-
|
|
207
|
+
|
|
208
|
+
// 4. 从 batch_progress 表获取批量进度
|
|
209
|
+
const batchRows = sqlDb.exec('SELECT total, completed, failed, skipped FROM batch_progress WHERE change_id = ?', [changeId]);
|
|
210
|
+
let batchProgress = undefined;
|
|
211
|
+
if (batchRows && batchRows.length > 0 && batchRows[0].values.length > 0) {
|
|
212
|
+
const [total, completed, failed, skipped] = batchRows[0].values[0];
|
|
213
|
+
batchProgress = { total, completed, failed, skipped };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 5. 获取项目名
|
|
217
|
+
const projectRows = sqlDb.exec('SELECT name FROM project WHERE id = 1');
|
|
218
|
+
const projectName = (projectRows && projectRows.length > 0 && projectRows[0].values.length > 0)
|
|
219
|
+
? projectRows[0].values[0][0]
|
|
220
|
+
: '';
|
|
221
|
+
|
|
222
|
+
// 6. 组装为兼容对象
|
|
223
|
+
const stages = {};
|
|
224
|
+
// 先填充所有 VALID_STAGES
|
|
225
|
+
for (const s of VALID_STAGES) {
|
|
226
|
+
stages[s] = emptyStage();
|
|
227
|
+
}
|
|
228
|
+
// 用 DB 数据覆盖
|
|
229
|
+
for (const [stage, info] of Object.entries(stageMap)) {
|
|
230
|
+
const steps = (stepsByStage[info._dbId] || []).map(s => ({
|
|
231
|
+
name: s.name,
|
|
232
|
+
status: s.status,
|
|
233
|
+
output: s.output,
|
|
234
|
+
completedAt: s.completedAt,
|
|
235
|
+
}));
|
|
236
|
+
stages[stage] = {
|
|
237
|
+
status: info.status,
|
|
238
|
+
steps,
|
|
239
|
+
startedAt: info.startedAt,
|
|
240
|
+
completedAt: info.completedAt,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const result = {
|
|
245
|
+
_version: 3,
|
|
246
|
+
project: projectName,
|
|
247
|
+
currentChange: cName,
|
|
248
|
+
currentStage: currentStage || '',
|
|
249
|
+
lastActive: lastActive || null,
|
|
250
|
+
stages,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// noWorktree
|
|
254
|
+
if (noWorktree) result.noWorktree = true;
|
|
255
|
+
|
|
256
|
+
// batchProgress(仅在 DB 中有记录时才包含)
|
|
257
|
+
if (batchProgress) result.batchProgress = batchProgress;
|
|
258
|
+
|
|
259
|
+
return result;
|
|
185
260
|
}
|
|
186
261
|
|
|
187
262
|
/**
|
|
@@ -190,85 +265,177 @@ export class ProgressManager {
|
|
|
190
265
|
* @param {object} data
|
|
191
266
|
* @param {string|null} changeName - 从 data.currentChange 推导,或显式传入
|
|
192
267
|
*/
|
|
193
|
-
_write(cwd, data, changeName = null) {
|
|
268
|
+
async _write(cwd, data, changeName = null) {
|
|
194
269
|
const cn = changeName || data.currentChange;
|
|
195
270
|
if (!cn) {
|
|
196
|
-
|
|
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
|
-
this._updateGateStatus(cwd);
|
|
271
|
+
console.warn('⚠️ _write: 无变更名,跳过写入');
|
|
203
272
|
return;
|
|
204
273
|
}
|
|
205
274
|
|
|
206
|
-
this.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
275
|
+
const db = await this._ensureDB(cwd);
|
|
276
|
+
db.transaction((sqlDb) => {
|
|
277
|
+
// 1. 更新 changes 表
|
|
278
|
+
const now = new Date().toISOString();
|
|
279
|
+
const noWorktree = data.noWorktree ? 1 : 0;
|
|
280
|
+
sqlDb.run(
|
|
281
|
+
'UPDATE changes SET current_stage = ?, last_active = ?, no_worktree = ? WHERE name = ?',
|
|
282
|
+
[data.currentStage || '', now, noWorktree, cn]
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// 2. 获取 change_id
|
|
286
|
+
const changeRow = sqlDb.exec('SELECT id FROM changes WHERE name = ?', [cn]);
|
|
287
|
+
if (!changeRow || changeRow.length === 0 || changeRow[0].values.length === 0) return;
|
|
288
|
+
const changeId = changeRow[0].values[0][0];
|
|
289
|
+
|
|
290
|
+
// 3. 遍历 stages,UPSERT stages 表和 steps 表
|
|
291
|
+
if (data.stages && typeof data.stages === 'object') {
|
|
292
|
+
for (const [stageName, stageData] of Object.entries(data.stages)) {
|
|
293
|
+
// UPSERT stages 行
|
|
294
|
+
sqlDb.run(
|
|
295
|
+
`INSERT INTO stages (change_id, stage, status, started_at, completed_at)
|
|
296
|
+
VALUES (?, ?, ?, ?, ?)
|
|
297
|
+
ON CONFLICT(change_id, stage) DO UPDATE SET
|
|
298
|
+
status = excluded.status,
|
|
299
|
+
started_at = excluded.started_at,
|
|
300
|
+
completed_at = excluded.completed_at`,
|
|
301
|
+
[changeId, stageName, stageData.status || 'pending', stageData.startedAt || null, stageData.completedAt || null]
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
// 获取 stage_id
|
|
305
|
+
const stageRow = sqlDb.exec('SELECT id FROM stages WHERE change_id = ? AND stage = ?', [changeId, stageName]);
|
|
306
|
+
if (!stageRow || stageRow.length === 0 || stageRow[0].values.length === 0) continue;
|
|
307
|
+
const stageId = stageRow[0].values[0][0];
|
|
308
|
+
|
|
309
|
+
// 收集 data 中的步骤名
|
|
310
|
+
const stepNames = new Set();
|
|
311
|
+
if (Array.isArray(stageData.steps)) {
|
|
312
|
+
for (let i = 0; i < stageData.steps.length; i++) {
|
|
313
|
+
const step = stageData.steps[i];
|
|
314
|
+
stepNames.add(step.name);
|
|
315
|
+
// UPSERT 步骤(先删再插,steps 表无 UNIQUE 约束)
|
|
316
|
+
sqlDb.run('DELETE FROM steps WHERE stage_id = ? AND name = ?', [stageId, step.name]);
|
|
317
|
+
sqlDb.run(
|
|
318
|
+
'INSERT INTO steps (stage_id, name, status, output, completed_at, ordering) VALUES (?, ?, ?, ?, ?, ?)',
|
|
319
|
+
[stageId, step.name, step.status || 'pending', step.output || null, step.completedAt || null, i]
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// 删除 data 中不存在的多余步骤
|
|
325
|
+
if (stepNames.size > 0) {
|
|
326
|
+
const namePlaceholders = [...stepNames].map(() => '?').join(',');
|
|
327
|
+
sqlDb.run(
|
|
328
|
+
`DELETE FROM steps WHERE stage_id = ? AND name NOT IN (${namePlaceholders})`,
|
|
329
|
+
[stageId, ...stepNames]
|
|
330
|
+
);
|
|
331
|
+
} else {
|
|
332
|
+
// data 中没有步骤,清空该阶段所有步骤
|
|
333
|
+
sqlDb.run('DELETE FROM steps WHERE stage_id = ?', [stageId]);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// 4. UPSERT batch_progress
|
|
339
|
+
if (data.batchProgress && typeof data.batchProgress === 'object') {
|
|
340
|
+
sqlDb.run(
|
|
341
|
+
`INSERT INTO batch_progress (change_id, total, completed, failed, skipped)
|
|
342
|
+
VALUES (?, ?, ?, ?, ?)
|
|
343
|
+
ON CONFLICT(change_id) DO UPDATE SET
|
|
344
|
+
total = excluded.total,
|
|
345
|
+
completed = excluded.completed,
|
|
346
|
+
failed = excluded.failed,
|
|
347
|
+
skipped = excluded.skipped`,
|
|
348
|
+
[changeId, data.batchProgress.total || 0, data.batchProgress.completed || 0, data.batchProgress.failed || 0, data.batchProgress.skipped || 0]
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
213
352
|
|
|
214
|
-
|
|
215
|
-
const cn = data?.currentChange;
|
|
216
|
-
if (!cn) return;
|
|
217
|
-
const p = this._changePath(cwd, cn, PROGRESS_FILE);
|
|
218
|
-
if (existsSync(p)) copyFileSync(p, this._changePath(cwd, cn, PROGRESS_FILE + BACKUP_SUFFIX));
|
|
353
|
+
await this._updateGateStatus(cwd);
|
|
219
354
|
}
|
|
220
355
|
|
|
221
356
|
// ── 变更管理 ──
|
|
222
357
|
|
|
223
358
|
/**
|
|
224
|
-
*
|
|
359
|
+
* 列出所有活跃变更名
|
|
360
|
+
* SQL: SELECT name FROM changes WHERE status = 'active'
|
|
225
361
|
*/
|
|
226
|
-
listChanges(cwd) {
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
362
|
+
async listChanges(cwd) {
|
|
363
|
+
const db = await this._ensureDB(cwd);
|
|
364
|
+
const sqlDb = db.getDb();
|
|
365
|
+
const rows = sqlDb.exec("SELECT name FROM changes WHERE status = 'active' ORDER BY name");
|
|
366
|
+
if (!rows || rows.length === 0) return [];
|
|
367
|
+
return rows[0].values.map(r => r[0]);
|
|
232
368
|
}
|
|
233
369
|
|
|
234
370
|
/**
|
|
235
|
-
*
|
|
371
|
+
* 注册变更到活跃列表
|
|
372
|
+
* SQL: INSERT OR IGNORE → 若已 archived 则 UPDATE status='active'
|
|
236
373
|
*/
|
|
237
|
-
registerChange(cwd, changeName) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
if (!global.activeChanges.includes(changeName)) {
|
|
243
|
-
global.activeChanges.push(changeName);
|
|
244
|
-
this.writeGlobal(cwd, global);
|
|
374
|
+
async registerChange(cwd, changeName) {
|
|
375
|
+
if (!changeName) {
|
|
376
|
+
console.warn('⚠️ registerChange: changeName 为空,跳过');
|
|
377
|
+
return;
|
|
245
378
|
}
|
|
379
|
+
const db = await this._ensureDB(cwd);
|
|
380
|
+
db.transaction((sqlDb) => {
|
|
381
|
+
const now = new Date().toISOString();
|
|
382
|
+
// 尝试插入新行
|
|
383
|
+
sqlDb.run(
|
|
384
|
+
`INSERT OR IGNORE INTO changes (name, created_at, last_active)
|
|
385
|
+
VALUES (?, ?, ?)`,
|
|
386
|
+
[changeName, now, now]
|
|
387
|
+
);
|
|
388
|
+
// 如果已存在但为 archived,恢复为 active
|
|
389
|
+
sqlDb.run(
|
|
390
|
+
`UPDATE changes SET status = 'active', last_active = ? WHERE name = ? AND status = 'archived'`,
|
|
391
|
+
[now, changeName]
|
|
392
|
+
);
|
|
393
|
+
});
|
|
246
394
|
}
|
|
247
395
|
|
|
248
396
|
/**
|
|
249
|
-
*
|
|
397
|
+
* 从活跃列表移除变更(归档时调用,不物理删除)
|
|
398
|
+
* SQL: UPDATE changes SET status = 'archived'
|
|
250
399
|
*/
|
|
251
|
-
unregisterChange(cwd, changeName) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
400
|
+
async unregisterChange(cwd, changeName) {
|
|
401
|
+
if (!changeName) {
|
|
402
|
+
console.warn('⚠️ unregisterChange: changeName 为空,跳过');
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const db = await this._ensureDB(cwd);
|
|
406
|
+
db.transaction((sqlDb) => {
|
|
407
|
+
const now = new Date().toISOString();
|
|
408
|
+
sqlDb.run(
|
|
409
|
+
`UPDATE changes SET status = 'archived', last_active = ? WHERE name = ?`,
|
|
410
|
+
[now, changeName]
|
|
411
|
+
);
|
|
412
|
+
});
|
|
256
413
|
}
|
|
257
414
|
|
|
258
415
|
// ── CLI 命令 ──
|
|
259
416
|
|
|
260
|
-
init(cwd) {
|
|
261
|
-
this._migrateIfNeeded(cwd);
|
|
417
|
+
async init(cwd) {
|
|
262
418
|
this._ensureRuntimeDir(cwd);
|
|
263
419
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
420
|
+
// 初始化 DB(如不存在则创建文件 + 建表)
|
|
421
|
+
const db = await this._ensureDB(cwd);
|
|
422
|
+
db.transaction((sqlDb) => {
|
|
423
|
+
const now = new Date().toISOString();
|
|
424
|
+
const projectName = basename(cwd) || 'project';
|
|
425
|
+
|
|
426
|
+
// 检查 project id=1 是否已存在
|
|
427
|
+
const existing = sqlDb.exec('SELECT id FROM project WHERE id = 1');
|
|
428
|
+
if (!existing || existing.length === 0 || existing[0].values.length === 0) {
|
|
429
|
+
sqlDb.run(
|
|
430
|
+
`INSERT INTO project (id, name, schema_version, created_at, updated_at)
|
|
431
|
+
VALUES (1, ?, ?, ?, ?)`,
|
|
432
|
+
[projectName, CURRENT_VERSION, now, now]
|
|
433
|
+
);
|
|
434
|
+
console.log(`✅ 已创建全局状态文件(SQLite)`);
|
|
435
|
+
} else {
|
|
436
|
+
console.log(`ℹ️ 全局状态文件已存在,跳过`);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
272
439
|
|
|
273
440
|
// 创建 user-inputs.md
|
|
274
441
|
const inputsPath = this._runtimePath(cwd, 'user-inputs.md');
|
|
@@ -277,155 +444,292 @@ export class ProgressManager {
|
|
|
277
444
|
}
|
|
278
445
|
|
|
279
446
|
this._ensureGitignore(cwd);
|
|
280
|
-
return this.readGlobal(cwd);
|
|
447
|
+
return await this.readGlobal(cwd);
|
|
281
448
|
}
|
|
282
449
|
|
|
283
450
|
/**
|
|
284
451
|
* 初始化指定变更的 progress
|
|
452
|
+
* SQL: INSERT changes + 批量 INSERT stages
|
|
285
453
|
*/
|
|
286
|
-
initChange(cwd, changeName) {
|
|
454
|
+
async initChange(cwd, changeName) {
|
|
455
|
+
if (!changeName) {
|
|
456
|
+
console.warn('⚠️ initChange: changeName 为空,跳过');
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
287
459
|
this._ensureChangeDir(cwd, changeName);
|
|
288
|
-
this.registerChange(cwd, changeName);
|
|
289
460
|
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
461
|
+
const db = await this._ensureDB(cwd);
|
|
462
|
+
db.transaction((sqlDb) => {
|
|
463
|
+
const now = new Date().toISOString();
|
|
464
|
+
|
|
465
|
+
// 检查变更是否已存在
|
|
466
|
+
const existing = sqlDb.exec('SELECT id FROM changes WHERE name = ?', [changeName]);
|
|
467
|
+
if (!existing || existing.length === 0 || existing[0].values.length === 0) {
|
|
468
|
+
// 插入 changes 行
|
|
469
|
+
sqlDb.run(
|
|
470
|
+
`INSERT INTO changes (name, current_stage, status, created_at, last_active)
|
|
471
|
+
VALUES (?, 'scan', 'active', ?, ?)`,
|
|
472
|
+
[changeName, now, now]
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// 获取 change_id
|
|
477
|
+
const changeRow = sqlDb.exec('SELECT id FROM changes WHERE name = ?', [changeName]);
|
|
478
|
+
const changeId = changeRow[0].values[0][0];
|
|
479
|
+
|
|
480
|
+
// 批量插入 9 个阶段(INSERT OR IGNORE 跳过已存在的)
|
|
481
|
+
const allStages = ['scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore', 'propose'];
|
|
482
|
+
for (const stage of allStages) {
|
|
483
|
+
sqlDb.run(
|
|
484
|
+
`INSERT OR IGNORE INTO stages (change_id, stage, status)
|
|
485
|
+
VALUES (?, ?, 'pending')`,
|
|
486
|
+
[changeId, stage]
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
297
490
|
|
|
298
|
-
|
|
491
|
+
// 不再需要写文件:read() 已改为 SQL
|
|
492
|
+
return await this.read(cwd, changeName);
|
|
299
493
|
}
|
|
300
494
|
|
|
301
|
-
setStage(cwd, stage, changeName = null) {
|
|
495
|
+
async setStage(cwd, stage, changeName = null) {
|
|
302
496
|
if (!VALID_STAGES.includes(stage)) {
|
|
303
497
|
console.log(`❌ 未知阶段: ${stage},可选: ${VALID_STAGES.join(', ')}`);
|
|
304
498
|
return;
|
|
305
499
|
}
|
|
306
500
|
|
|
307
|
-
const
|
|
308
|
-
|
|
501
|
+
const db = await this._ensureDB(cwd);
|
|
502
|
+
const now = new Date().toISOString();
|
|
309
503
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
stageData.startedAt = new Date().toLocaleString('zh-CN',{hour12:false});
|
|
504
|
+
// 获取变更名
|
|
505
|
+
let cn = changeName;
|
|
506
|
+
if (!cn) {
|
|
507
|
+
const changes = await this.listChanges(cwd);
|
|
508
|
+
if (changes.length === 1) cn = changes[0];
|
|
509
|
+
if (!cn) { console.log('❌ 无法确定当前变更,请指定 --change <name>'); return; }
|
|
317
510
|
}
|
|
318
|
-
data.lastActive = new Date().toLocaleString('zh-CN',{hour12:false});
|
|
319
511
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
512
|
+
db.transaction((sqlDb) => {
|
|
513
|
+
// 确保 change 存在
|
|
514
|
+
const changeRow = sqlDb.exec('SELECT id, current_stage FROM changes WHERE name = ?', [cn]);
|
|
515
|
+
if (!changeRow || changeRow.length === 0 || changeRow[0].values.length === 0) return;
|
|
516
|
+
|
|
517
|
+
const changeId = changeRow[0].values[0][0];
|
|
518
|
+
|
|
519
|
+
// UPDATE changes.current_stage + last_active
|
|
520
|
+
sqlDb.run('UPDATE changes SET current_stage = ?, last_active = ? WHERE name = ?', [stage, now, cn]);
|
|
521
|
+
|
|
522
|
+
// 确保 stages 行存在(INSERT OR IGNORE)
|
|
523
|
+
sqlDb.run(
|
|
524
|
+
'INSERT OR IGNORE INTO stages (change_id, stage, status) VALUES (?, ?, "pending")',
|
|
525
|
+
[changeId, stage]
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
// UPDATE stages.status 为 in-progress(仅当仍为 pending 时)
|
|
529
|
+
sqlDb.run(
|
|
530
|
+
"UPDATE stages SET status = 'in-progress', started_at = ? WHERE change_id = ? AND stage = ? AND status = 'pending'",
|
|
531
|
+
[now, changeId, stage]
|
|
532
|
+
);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// read() 已改为 SQL,直接通过 SQL 查询即可,无需 _write
|
|
536
|
+
console.log(`✅ 当前阶段已设为: ${STAGE_LABELS[stage] || stage}`);
|
|
323
537
|
}
|
|
324
538
|
|
|
325
|
-
addStep(cwd, stage, stepName, changeName = null) {
|
|
539
|
+
async addStep(cwd, stage, stepName, changeName = null) {
|
|
326
540
|
if (!stepName) { console.log('❌ 请指定步骤名称'); return; }
|
|
327
|
-
const data = this._requireStage(cwd, stage, changeName);
|
|
328
|
-
if (!data) return;
|
|
329
541
|
|
|
330
|
-
const
|
|
331
|
-
|
|
542
|
+
const db = await this._ensureDB(cwd);
|
|
543
|
+
|
|
544
|
+
// 获取变更名
|
|
545
|
+
let cn = changeName;
|
|
546
|
+
if (!cn) {
|
|
547
|
+
const changes = await this.listChanges(cwd);
|
|
548
|
+
if (changes.length === 1) cn = changes[0];
|
|
549
|
+
if (!cn) { console.log('❌ 无法确定当前变更,请指定 --change <name>'); return; }
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// 查找 stage_id
|
|
553
|
+
const sqlDb = db.getDb();
|
|
554
|
+
const stageRow = sqlDb.exec(
|
|
555
|
+
'SELECT s.id FROM stages s JOIN changes c ON s.change_id = c.id WHERE c.name = ? AND s.stage = ?',
|
|
556
|
+
[cn, stage]
|
|
557
|
+
);
|
|
558
|
+
if (!stageRow || stageRow.length === 0 || stageRow[0].values.length === 0) {
|
|
559
|
+
// stages 行不存在,静默跳过
|
|
560
|
+
console.log(`ℹ️ 阶段 ${stage} 不存在`);
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const stageId = stageRow[0].values[0][0];
|
|
564
|
+
|
|
565
|
+
// 重复步骤名检查
|
|
566
|
+
const dupRow = sqlDb.exec('SELECT id FROM steps WHERE stage_id = ? AND name = ?', [stageId, stepName]);
|
|
567
|
+
if (dupRow && dupRow.length > 0 && dupRow[0].values.length > 0) {
|
|
332
568
|
console.log(`ℹ️ 步骤 "${stepName}" 已存在于 ${stage}`);
|
|
333
569
|
return;
|
|
334
570
|
}
|
|
335
571
|
|
|
336
|
-
|
|
337
|
-
|
|
572
|
+
// INSERT INTO steps(ordering 递增)
|
|
573
|
+
db.transaction((tDb) => {
|
|
574
|
+
tDb.run(
|
|
575
|
+
`INSERT INTO steps (stage_id, name, ordering, status)
|
|
576
|
+
VALUES (?, ?, (SELECT COALESCE(MAX(ordering), 0) + 1 FROM steps WHERE stage_id = ?), 'pending')`,
|
|
577
|
+
[stageId, stepName, stageId]
|
|
578
|
+
);
|
|
579
|
+
tDb.run('UPDATE changes SET last_active = ? WHERE name = ?', [new Date().toISOString(), cn]);
|
|
580
|
+
});
|
|
338
581
|
|
|
339
|
-
this._backup(cwd, data);
|
|
340
|
-
this._write(cwd, data);
|
|
341
582
|
console.log(`✅ 已添加步骤: ${stage}/${stepName}`);
|
|
342
583
|
}
|
|
343
584
|
|
|
344
|
-
updateStep(cwd, stage, stepName, options = {}, changeName = null) {
|
|
585
|
+
async updateStep(cwd, stage, stepName, options = {}, changeName = null) {
|
|
345
586
|
const { status, output } = options;
|
|
346
587
|
if (!stepName) { console.log('❌ 请指定步骤名称'); return; }
|
|
347
|
-
const data = this._requireStage(cwd, stage, changeName);
|
|
348
|
-
if (!data) return;
|
|
349
588
|
|
|
350
|
-
const
|
|
351
|
-
const step = stageData.steps.find(s => s.name === stepName);
|
|
352
|
-
if (!step) { console.log(`❌ 步骤不存在: ${stage}/${stepName}`); return; }
|
|
589
|
+
const db = await this._ensureDB(cwd);
|
|
353
590
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
591
|
+
// 获取变更名
|
|
592
|
+
let cn = changeName;
|
|
593
|
+
if (!cn) {
|
|
594
|
+
const changes = await this.listChanges(cwd);
|
|
595
|
+
if (changes.length === 1) cn = changes[0];
|
|
596
|
+
if (!cn) { console.log('❌ 无法确定当前变更,请指定 --change <name>'); return; }
|
|
360
597
|
}
|
|
361
|
-
if (output !== undefined) step.output = output;
|
|
362
598
|
|
|
363
|
-
//
|
|
364
|
-
if (
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
console.log(`✅ 阶段 ${stage} 所有步骤已完成,阶段已标记为 completed`);
|
|
599
|
+
// 状态校验
|
|
600
|
+
if (status && !VALID_STATUSES.includes(status)) {
|
|
601
|
+
console.log(`❌ 无效状态: ${status},可选: ${VALID_STATUSES.join(', ')}`);
|
|
602
|
+
return;
|
|
368
603
|
}
|
|
369
604
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
605
|
+
// 查找 step_id:通过 changes → stages → steps JOIN 查询
|
|
606
|
+
const sqlDb = db.getDb();
|
|
607
|
+
const stepRow = sqlDb.exec(
|
|
608
|
+
`SELECT st.id, st.status FROM steps st
|
|
609
|
+
JOIN stages sg ON st.stage_id = sg.id
|
|
610
|
+
JOIN changes c ON sg.change_id = c.id
|
|
611
|
+
WHERE c.name = ? AND sg.stage = ? AND st.name = ?`,
|
|
612
|
+
[cn, stage, stepName]
|
|
613
|
+
);
|
|
614
|
+
if (!stepRow || stepRow.length === 0 || stepRow[0].values.length === 0) {
|
|
615
|
+
console.log(`❌ 步骤不存在: ${stage}/${stepName}`);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const stepId = stepRow[0].values[0][0];
|
|
619
|
+
|
|
620
|
+
// UPDATE steps
|
|
621
|
+
db.transaction((tDb) => {
|
|
622
|
+
const now = new Date().toISOString();
|
|
623
|
+
if (status) {
|
|
624
|
+
tDb.run('UPDATE steps SET status = ?, completed_at = ? WHERE id = ? AND name = ?', [status, now, stepId, stepName]);
|
|
625
|
+
}
|
|
626
|
+
if (output !== undefined) {
|
|
627
|
+
tDb.run('UPDATE steps SET output = ? WHERE id = ? AND name = ?', [output, stepId, stepName]);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// 自动完成检测:同 stage_id 下所有 steps 都 completed 时,标记 stage completed
|
|
631
|
+
if (status === 'completed') {
|
|
632
|
+
// 获取 stage_id
|
|
633
|
+
const stRow = tDb.exec('SELECT stage_id FROM steps WHERE id = ?', [stepId]);
|
|
634
|
+
if (stRow && stRow.length > 0 && stRow[0].values.length > 0) {
|
|
635
|
+
const stId = stRow[0].values[0][0];
|
|
636
|
+
const pendingRows = tDb.exec('SELECT COUNT(*) FROM steps WHERE stage_id = ? AND status != "completed"', [stId]);
|
|
637
|
+
if (pendingRows && pendingRows.length > 0 && pendingRows[0].values[0][0] === 0) {
|
|
638
|
+
tDb.run('UPDATE stages SET status = "completed", completed_at = ? WHERE id = ?', [now, stId]);
|
|
639
|
+
console.log(`✅ 阶段 ${stage} 所有步骤已完成,阶段已标记为 completed`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
tDb.run('UPDATE changes SET last_active = ? WHERE name = ?', [now, cn]);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
console.log(`✅ 步骤已更新: ${stage}/${stepName} → ${status || '(仅更新 output)'}`);
|
|
374
648
|
}
|
|
375
649
|
|
|
376
|
-
completeStage(cwd, stage, changeName = null) {
|
|
650
|
+
async completeStage(cwd, stage, changeName = null) {
|
|
377
651
|
if (!VALID_STAGES.includes(stage)) {
|
|
378
652
|
console.log(`❌ 未知阶段: ${stage}`);
|
|
379
653
|
return;
|
|
380
654
|
}
|
|
381
655
|
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
if (!data.stages[stage]) data.stages[stage] = emptyStage();
|
|
386
|
-
const stageData = data.stages[stage];
|
|
387
|
-
stageData.status = 'completed';
|
|
388
|
-
stageData.completedAt = new Date().toLocaleString('zh-CN',{hour12:false});
|
|
656
|
+
const db = await this._ensureDB(cwd);
|
|
657
|
+
const now = new Date().toISOString();
|
|
389
658
|
|
|
390
|
-
//
|
|
391
|
-
|
|
392
|
-
|
|
659
|
+
// 获取变更名
|
|
660
|
+
let cn = changeName;
|
|
661
|
+
if (!cn) {
|
|
662
|
+
const changes = await this.listChanges(cwd);
|
|
663
|
+
if (changes.length === 1) cn = changes[0];
|
|
664
|
+
if (!cn) { console.log('❌ 无法确定当前变更,请指定 --change <name>'); return; }
|
|
393
665
|
}
|
|
394
666
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
667
|
+
db.transaction((sqlDb) => {
|
|
668
|
+
const changeRow = sqlDb.exec('SELECT id FROM changes WHERE name = ?', [cn]);
|
|
669
|
+
if (!changeRow || changeRow.length === 0 || changeRow[0].values.length === 0) return;
|
|
670
|
+
const changeId = changeRow[0].values[0][0];
|
|
671
|
+
|
|
672
|
+
// 确保 stages 行存在(阶段不存在时自动创建)
|
|
673
|
+
sqlDb.run(
|
|
674
|
+
'INSERT OR IGNORE INTO stages (change_id, stage, status) VALUES (?, ?, "pending")',
|
|
675
|
+
[changeId, stage]
|
|
676
|
+
);
|
|
677
|
+
|
|
678
|
+
// UPDATE stages.status=completed + completed_at
|
|
679
|
+
sqlDb.run(
|
|
680
|
+
'UPDATE stages SET status = "completed", completed_at = ? WHERE change_id = ? AND stage = ?',
|
|
681
|
+
[now, changeId, stage]
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
// 将该阶段所有 pending 步骤标记为 completed
|
|
685
|
+
const stageRow = sqlDb.exec('SELECT id FROM stages WHERE change_id = ? AND stage = ?', [changeId, stage]);
|
|
686
|
+
if (stageRow && stageRow.length > 0 && stageRow[0].values.length > 0) {
|
|
687
|
+
const stageId = stageRow[0].values[0][0];
|
|
688
|
+
sqlDb.run(
|
|
689
|
+
'UPDATE steps SET status = "completed", completed_at = ? WHERE stage_id = ? AND status = "pending"',
|
|
690
|
+
[now, stageId]
|
|
691
|
+
);
|
|
692
|
+
}
|
|
403
693
|
|
|
404
|
-
|
|
405
|
-
|
|
694
|
+
// UPDATE changes.last_active
|
|
695
|
+
sqlDb.run('UPDATE changes SET last_active = ? WHERE name = ?', [now, cn]);
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
// 写 history 文件(保持文件系统,不变)
|
|
699
|
+
const data = await this.read(cwd, cn);
|
|
700
|
+
if (data && data.stages && data.stages[stage]) {
|
|
701
|
+
const historyDir = this._runtimePath(cwd, 'history');
|
|
702
|
+
mkdirSync(historyDir, { recursive: true });
|
|
703
|
+
const ts = now.replace(/[:.TZ-]/g, '');
|
|
704
|
+
const stageData = data.stages[stage];
|
|
705
|
+
writeFileSync(
|
|
706
|
+
join(historyDir, `${cn}-${stage}-${ts}.json`),
|
|
707
|
+
JSON.stringify({ change: cn, stage, data: stageData, completedAt: now }, null, 2) + '\n'
|
|
708
|
+
);
|
|
709
|
+
}
|
|
406
710
|
|
|
407
711
|
console.log(`✅ 阶段 ${stage} 已标记为完成(不自动推进,下一步由你决定)`);
|
|
408
712
|
}
|
|
409
713
|
|
|
410
|
-
show(cwd, changeName = null) {
|
|
714
|
+
async show(cwd, changeName = null) {
|
|
411
715
|
// 如果指定了变更名,只显示该变更
|
|
412
716
|
if (changeName) {
|
|
413
|
-
return this._showChange(cwd, changeName);
|
|
717
|
+
return await this._showChange(cwd, changeName);
|
|
414
718
|
}
|
|
415
719
|
|
|
416
720
|
// 否则显示所有变更
|
|
417
|
-
const changes = this.listChanges(cwd);
|
|
721
|
+
const changes = await this.listChanges(cwd);
|
|
418
722
|
if (changes.length === 0) {
|
|
419
723
|
console.log('ℹ️ 没有活跃的变更');
|
|
420
724
|
return;
|
|
421
725
|
}
|
|
422
726
|
|
|
423
727
|
if (changes.length === 1) {
|
|
424
|
-
return this._showChange(cwd, changes[0]);
|
|
728
|
+
return await this._showChange(cwd, changes[0]);
|
|
425
729
|
}
|
|
426
730
|
|
|
427
731
|
// 多个变更:汇总显示
|
|
428
|
-
const global = this.readGlobal(cwd);
|
|
732
|
+
const global = await this.readGlobal(cwd);
|
|
429
733
|
console.log('');
|
|
430
734
|
console.log(' ═══════════════════════════════════════');
|
|
431
735
|
console.log(` 项目: ${(global?.project) || basename(cwd) || '(未命名)'}`);
|
|
@@ -434,7 +738,7 @@ export class ProgressManager {
|
|
|
434
738
|
console.log('');
|
|
435
739
|
|
|
436
740
|
for (const cn of changes) {
|
|
437
|
-
const data = this.read(cwd, cn);
|
|
741
|
+
const data = await this.read(cwd, cn);
|
|
438
742
|
if (!data) {
|
|
439
743
|
console.log(` 📂 ${cn} — (无法读取)`);
|
|
440
744
|
continue;
|
|
@@ -452,8 +756,8 @@ export class ProgressManager {
|
|
|
452
756
|
console.log('');
|
|
453
757
|
}
|
|
454
758
|
|
|
455
|
-
_showChange(cwd, changeName) {
|
|
456
|
-
const data = this.read(cwd, changeName);
|
|
759
|
+
async _showChange(cwd, changeName) {
|
|
760
|
+
const data = await this.read(cwd, changeName);
|
|
457
761
|
if (!data) {
|
|
458
762
|
console.log(`❌ 未找到变更 ${changeName} 的 progress.json`);
|
|
459
763
|
return;
|
|
@@ -506,12 +810,12 @@ export class ProgressManager {
|
|
|
506
810
|
console.log('');
|
|
507
811
|
}
|
|
508
812
|
|
|
509
|
-
status(cwd, changeName = null) {
|
|
510
|
-
this.show(cwd, changeName);
|
|
813
|
+
async status(cwd, changeName = null) {
|
|
814
|
+
await this.show(cwd, changeName);
|
|
511
815
|
}
|
|
512
816
|
|
|
513
817
|
async validate(cwd, changeName = null) {
|
|
514
|
-
const data = this.read(cwd, changeName);
|
|
818
|
+
const data = await this.read(cwd, changeName);
|
|
515
819
|
if (!data) { console.log('❌ 无法读取 progress.json'); return false; }
|
|
516
820
|
|
|
517
821
|
const errors = [];
|
|
@@ -538,42 +842,54 @@ export class ProgressManager {
|
|
|
538
842
|
if (!fixed.stages[s]) { fixed.stages[s] = emptyStage(); changed = true; }
|
|
539
843
|
}
|
|
540
844
|
if (changed) {
|
|
541
|
-
this.
|
|
542
|
-
|
|
543
|
-
console.log('✅ 已修复并备份');
|
|
845
|
+
await this._write(cwd, fixed);
|
|
846
|
+
console.log('✅ 已修复');
|
|
544
847
|
}
|
|
545
848
|
|
|
546
849
|
return true;
|
|
547
850
|
}
|
|
548
851
|
|
|
549
|
-
reset(cwd, stage, changeName = null) {
|
|
852
|
+
async reset(cwd, stage, changeName = null) {
|
|
550
853
|
if (stage) {
|
|
551
|
-
const data = this.read(cwd, changeName);
|
|
854
|
+
const data = await this.read(cwd, changeName);
|
|
552
855
|
if (!data) { console.log('❌ 无法读取 progress.json'); return; }
|
|
553
|
-
this._backup(cwd, data);
|
|
554
856
|
if (!data.stages[stage]) { console.log(`❌ 未知阶段: ${stage}`); return; }
|
|
555
857
|
data.stages[stage] = emptyStage();
|
|
556
858
|
data.lastActive = new Date().toLocaleString('zh-CN',{hour12:false});
|
|
557
|
-
this._write(cwd, data);
|
|
859
|
+
await this._write(cwd, data);
|
|
558
860
|
console.log(`✅ 已重置阶段: ${stage}`);
|
|
559
861
|
} else {
|
|
560
862
|
// 重置所有变更或指定变更
|
|
561
863
|
if (changeName) {
|
|
562
|
-
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
864
|
+
// SQL: 删除该变更的所有 stages 和 steps 数据
|
|
865
|
+
const db = await this._ensureDB(cwd);
|
|
866
|
+
db.transaction((sqlDb) => {
|
|
867
|
+
const changeRow = sqlDb.exec('SELECT id FROM changes WHERE name = ?', [changeName]);
|
|
868
|
+
if (changeRow && changeRow.length > 0 && changeRow[0].values.length > 0) {
|
|
869
|
+
const changeId = changeRow[0].values[0][0];
|
|
870
|
+
sqlDb.run('DELETE FROM steps WHERE stage_id IN (SELECT id FROM stages WHERE change_id = ?)', [changeId]);
|
|
871
|
+
sqlDb.run('DELETE FROM stages WHERE change_id = ?', [changeId]);
|
|
872
|
+
sqlDb.run('UPDATE stages SET status = "pending", started_at = NULL, completed_at = NULL WHERE change_id = ?', [changeId]);
|
|
873
|
+
// 重新插入所有阶段
|
|
874
|
+
for (const s of VALID_STAGES) {
|
|
875
|
+
sqlDb.run('INSERT OR IGNORE INTO stages (change_id, stage, status) VALUES (?, ?, "pending")', [changeId, s]);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
console.log(`✅ 已重置变更 ${changeName} 的进度`);
|
|
569
880
|
} else {
|
|
570
|
-
const changes = this.listChanges(cwd);
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
881
|
+
const changes = await this.listChanges(cwd);
|
|
882
|
+
const db = await this._ensureDB(cwd);
|
|
883
|
+
db.transaction((sqlDb) => {
|
|
884
|
+
for (const cn of changes) {
|
|
885
|
+
const changeRow = sqlDb.exec('SELECT id FROM changes WHERE name = ?', [cn]);
|
|
886
|
+
if (changeRow && changeRow.length > 0 && changeRow[0].values.length > 0) {
|
|
887
|
+
const changeId = changeRow[0].values[0][0];
|
|
888
|
+
sqlDb.run('DELETE FROM steps WHERE stage_id IN (SELECT id FROM stages WHERE change_id = ?)', [changeId]);
|
|
889
|
+
sqlDb.run('UPDATE stages SET status = "pending", started_at = NULL, completed_at = NULL WHERE change_id = ?', [changeId]);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
});
|
|
577
893
|
console.log('✅ 已重置所有变更的进度');
|
|
578
894
|
}
|
|
579
895
|
}
|
|
@@ -581,26 +897,35 @@ export class ProgressManager {
|
|
|
581
897
|
|
|
582
898
|
// ── 内部辅助 ──
|
|
583
899
|
|
|
584
|
-
_readOrInit(cwd, changeName = null) {
|
|
585
|
-
let data = this.read(cwd, changeName);
|
|
900
|
+
async _readOrInit(cwd, changeName = null) {
|
|
901
|
+
let data = await this.read(cwd, changeName);
|
|
586
902
|
if (!data) {
|
|
587
903
|
// 尝试自动检测变更名
|
|
588
904
|
if (!changeName) {
|
|
589
|
-
const changes = this.listChanges(cwd);
|
|
905
|
+
const changes = await this.listChanges(cwd);
|
|
590
906
|
if (changes.length === 1) changeName = changes[0];
|
|
591
907
|
}
|
|
592
908
|
if (changeName) {
|
|
593
|
-
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
909
|
+
// 确保变更在 DB 中已初始化
|
|
910
|
+
const db = await this._ensureDB(cwd);
|
|
911
|
+
db.transaction((sqlDb) => {
|
|
912
|
+
const now = new Date().toISOString();
|
|
913
|
+
sqlDb.run(
|
|
914
|
+
'INSERT OR IGNORE INTO changes (name, current_stage, status, created_at, last_active) VALUES (?, "scan", "active", ?, ?)',
|
|
915
|
+
[changeName, now, now]
|
|
916
|
+
);
|
|
917
|
+
const changeRow = sqlDb.exec('SELECT id FROM changes WHERE name = ?', [changeName]);
|
|
918
|
+
if (changeRow && changeRow.length > 0 && changeRow[0].values.length > 0) {
|
|
919
|
+
const changeId = changeRow[0].values[0][0];
|
|
920
|
+
for (const s of VALID_STAGES) {
|
|
921
|
+
sqlDb.run('INSERT OR IGNORE INTO stages (change_id, stage, status) VALUES (?, ?, "pending")', [changeId, s]);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
await this.registerChange(cwd, changeName);
|
|
601
926
|
}
|
|
602
927
|
if (!data) {
|
|
603
|
-
data = this.read(cwd, changeName);
|
|
928
|
+
data = await this.read(cwd, changeName);
|
|
604
929
|
}
|
|
605
930
|
if (!data) {
|
|
606
931
|
console.log('❌ 无法确定当前变更,请指定 --change <name>');
|
|
@@ -610,37 +935,17 @@ export class ProgressManager {
|
|
|
610
935
|
return data;
|
|
611
936
|
}
|
|
612
937
|
|
|
613
|
-
_requireStage(cwd, stage, changeName = null) {
|
|
938
|
+
async _requireStage(cwd, stage, changeName = null) {
|
|
614
939
|
if (!VALID_STAGES.includes(stage)) {
|
|
615
940
|
console.log(`❌ 未知阶段: ${stage},可选: ${VALID_STAGES.join(', ')}`);
|
|
616
941
|
return null;
|
|
617
942
|
}
|
|
618
|
-
const data = this._readOrInit(cwd, changeName);
|
|
943
|
+
const data = await this._readOrInit(cwd, changeName);
|
|
619
944
|
if (!data) return null;
|
|
620
945
|
if (!data.stages[stage]) data.stages[stage] = emptyStage();
|
|
621
946
|
return data;
|
|
622
947
|
}
|
|
623
948
|
|
|
624
|
-
_parseWithRecovery(jsonString) {
|
|
625
|
-
try { return JSON.parse(jsonString); } catch {}
|
|
626
|
-
|
|
627
|
-
let fixed = jsonString.replace(/,\s*([}\]])/g, '$1');
|
|
628
|
-
fixed = fixed.replace(/([{,]\s*)'([^']+)'(\s*:)/g, '$1"$2"$3');
|
|
629
|
-
fixed = fixed.replace(/:\s*'([^']*)'([,}\]])/g, ':"$1"$2');
|
|
630
|
-
try { return JSON.parse(fixed); } catch {}
|
|
631
|
-
|
|
632
|
-
const lastBrace = fixed.lastIndexOf('}');
|
|
633
|
-
if (lastBrace > 0) {
|
|
634
|
-
let open = 0;
|
|
635
|
-
for (const ch of fixed.substring(0, lastBrace + 1)) {
|
|
636
|
-
if (ch === '{') open++;
|
|
637
|
-
if (ch === '}') open--;
|
|
638
|
-
}
|
|
639
|
-
try { return JSON.parse(fixed.substring(0, lastBrace + 1) + '}'.repeat(Math.max(0, open))); } catch {}
|
|
640
|
-
}
|
|
641
|
-
return null;
|
|
642
|
-
}
|
|
643
|
-
|
|
644
949
|
_timeAgo(dateStr) {
|
|
645
950
|
if (!dateStr) return '未知';
|
|
646
951
|
let ts = Date.parse(dateStr);
|
|
@@ -660,25 +965,39 @@ export class ProgressManager {
|
|
|
660
965
|
|
|
661
966
|
// ── 批量进度 ──
|
|
662
967
|
|
|
663
|
-
updateBatchProgress(cwd, batchData, changeName = null) {
|
|
664
|
-
const
|
|
665
|
-
if (!data) return;
|
|
666
|
-
|
|
667
|
-
if (!data.batchProgress) {
|
|
668
|
-
data.batchProgress = { total: 0, completed: 0, failed: 0, skipped: 0 };
|
|
669
|
-
}
|
|
670
|
-
if (batchData.total !== undefined) data.batchProgress.total = batchData.total;
|
|
671
|
-
if (batchData.completed !== undefined) data.batchProgress.completed = batchData.completed;
|
|
672
|
-
if (batchData.failed !== undefined) data.batchProgress.failed = batchData.failed;
|
|
673
|
-
if (batchData.skipped !== undefined) data.batchProgress.skipped = batchData.skipped;
|
|
968
|
+
async updateBatchProgress(cwd, batchData, changeName = null) {
|
|
969
|
+
const cn = changeName || null;
|
|
674
970
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
971
|
+
const db = await this._ensureDB(cwd);
|
|
972
|
+
db.transaction((sqlDb) => {
|
|
973
|
+
// 获取 change_id
|
|
974
|
+
let changeId = null;
|
|
975
|
+
if (cn) {
|
|
976
|
+
const row = sqlDb.exec('SELECT id FROM changes WHERE name = ?', [cn]);
|
|
977
|
+
if (row && row.length > 0 && row[0].values.length > 0) changeId = row[0].values[0][0];
|
|
978
|
+
}
|
|
979
|
+
if (!changeId) {
|
|
980
|
+
// 尝试从唯一活跃变更获取
|
|
981
|
+
const rows = sqlDb.exec("SELECT id FROM changes WHERE status = 'active'");
|
|
982
|
+
if (rows && rows.length > 0 && rows[0].values.length === 1) changeId = rows[0].values[0][0];
|
|
983
|
+
}
|
|
984
|
+
if (!changeId) return;
|
|
985
|
+
|
|
986
|
+
sqlDb.run(
|
|
987
|
+
`INSERT INTO batch_progress (change_id, total, completed, failed, skipped)
|
|
988
|
+
VALUES (?, ?, ?, ?, ?)
|
|
989
|
+
ON CONFLICT(change_id) DO UPDATE SET
|
|
990
|
+
total = excluded.total,
|
|
991
|
+
completed = excluded.completed,
|
|
992
|
+
failed = excluded.failed,
|
|
993
|
+
skipped = excluded.skipped`,
|
|
994
|
+
[changeId, batchData.total || 0, batchData.completed || 0, batchData.failed || 0, batchData.skipped || 0]
|
|
995
|
+
);
|
|
996
|
+
});
|
|
678
997
|
}
|
|
679
998
|
|
|
680
|
-
readBatchProgress(cwd, changeName = null) {
|
|
681
|
-
const data = this.read(cwd, changeName);
|
|
999
|
+
async readBatchProgress(cwd, changeName = null) {
|
|
1000
|
+
const data = await this.read(cwd, changeName);
|
|
682
1001
|
return data?.batchProgress || null;
|
|
683
1002
|
}
|
|
684
1003
|
|
|
@@ -697,13 +1016,22 @@ export class ProgressManager {
|
|
|
697
1016
|
|
|
698
1017
|
/**
|
|
699
1018
|
* 更新 gate-status.json,供 worktree-guard hook 读取
|
|
700
|
-
*
|
|
1019
|
+
* 从 SQLite 查询所有处于 execute/quick 阶段的活跃变更,生成或删除 gate-status.json
|
|
701
1020
|
*/
|
|
702
|
-
_updateGateStatus(cwd) {
|
|
703
|
-
const
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
1021
|
+
async _updateGateStatus(cwd) {
|
|
1022
|
+
const db = await this._ensureDB(cwd);
|
|
1023
|
+
const sqlDb = db.getDb();
|
|
1024
|
+
|
|
1025
|
+
// SQL 查询:所有处于 execute/quick 阶段的活跃变更
|
|
1026
|
+
const rows = sqlDb.exec(
|
|
1027
|
+
`SELECT name, current_stage, no_worktree FROM changes
|
|
1028
|
+
WHERE status = 'active' AND current_stage IN ('execute', 'quick')`
|
|
1029
|
+
);
|
|
1030
|
+
|
|
1031
|
+
const gatePath = this._runtimePath(cwd, 'gate-status.json');
|
|
1032
|
+
|
|
1033
|
+
if (!rows || rows.length === 0 || rows[0].values.length === 0) {
|
|
1034
|
+
// 无 execute/quick 阶段的活跃变更,删除 gate-status
|
|
707
1035
|
if (existsSync(gatePath)) {
|
|
708
1036
|
try { unlinkSync(gatePath); } catch {}
|
|
709
1037
|
}
|
|
@@ -714,23 +1042,25 @@ export class ProgressManager {
|
|
|
714
1042
|
let hasNoWorktree = false;
|
|
715
1043
|
const activeChanges = [];
|
|
716
1044
|
|
|
717
|
-
for (const
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
// 优先取 execute,其次 quick
|
|
723
|
-
if (gateStage !== 'execute' || stage === 'execute') {
|
|
724
|
-
gateStage = stage;
|
|
725
|
-
}
|
|
726
|
-
activeChanges.push(cn);
|
|
727
|
-
if (data.noWorktree) hasNoWorktree = true;
|
|
1045
|
+
for (const [name, stage, noWorktree] of rows[0].values) {
|
|
1046
|
+
if (!stage) continue;
|
|
1047
|
+
// 优先取 execute,其次 quick
|
|
1048
|
+
if (gateStage !== 'execute' || stage === 'execute') {
|
|
1049
|
+
gateStage = stage;
|
|
728
1050
|
}
|
|
1051
|
+
activeChanges.push(name);
|
|
1052
|
+
if (noWorktree === 1) hasNoWorktree = true;
|
|
729
1053
|
}
|
|
730
1054
|
|
|
731
|
-
|
|
1055
|
+
if (!gateStage) {
|
|
1056
|
+
// current_stage 为 NULL 的边界情况,等同于无 execute/quick
|
|
1057
|
+
if (existsSync(gatePath)) {
|
|
1058
|
+
try { unlinkSync(gatePath); } catch {}
|
|
1059
|
+
}
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
732
1062
|
|
|
733
|
-
|
|
1063
|
+
try {
|
|
734
1064
|
this._ensureRuntimeDir(cwd);
|
|
735
1065
|
const gateData = {
|
|
736
1066
|
stage: gateStage,
|
|
@@ -741,11 +1071,8 @@ export class ProgressManager {
|
|
|
741
1071
|
const tmpPath = gatePath + '.tmp';
|
|
742
1072
|
writeFileSync(tmpPath, JSON.stringify(gateData, null, 2) + '\n');
|
|
743
1073
|
renameSync(tmpPath, gatePath);
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
if (existsSync(gatePath)) {
|
|
747
|
-
try { unlinkSync(gatePath); } catch {}
|
|
748
|
-
}
|
|
1074
|
+
} catch (err) {
|
|
1075
|
+
console.warn('⚠️ 写入 gate-status.json 失败:', err.message);
|
|
749
1076
|
}
|
|
750
1077
|
}
|
|
751
1078
|
|