@zhuan-ai/zhuanspec 2.8.0 → 2.9.5
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/dist/core/archive.js +3 -1
- package/dist/core/hooks/record-progress.js +58 -16
- package/dist/core/init.js +11 -7
- package/package.json +1 -1
package/dist/core/archive.js
CHANGED
|
@@ -531,7 +531,9 @@ export class ArchiveCommand {
|
|
|
531
531
|
await this.copyDirectoryContentsIfExists(path.join(templateRoot, 'specs'), path.join(zhuanspecDir, 'specs'));
|
|
532
532
|
console.log(chalk.gray(' → 同步 changes 目录...'));
|
|
533
533
|
await this.copyDirectoryContentsIfExists(path.join(templateRoot, 'changes'), path.join(zhuanspecDir, 'changes'));
|
|
534
|
-
console.log(chalk.
|
|
534
|
+
console.log(chalk.gray(' → 同步 knowledge 目录...'));
|
|
535
|
+
await this.copyDirectoryContentsIfExists(path.join(templateRoot, 'knowledge'), path.join(zhuanspecDir, 'knowledge'));
|
|
536
|
+
console.log(chalk.green(`✅ 已拉取 ${businessDirection} 业务模板(specs/changes/knowledge)。`));
|
|
535
537
|
}
|
|
536
538
|
finally {
|
|
537
539
|
rmSync(tempRepo, { recursive: true, force: true });
|
|
@@ -211,38 +211,80 @@ function extractMcpTool(toolName) {
|
|
|
211
211
|
return parts.length >= 3 ? parts[2] : undefined;
|
|
212
212
|
}
|
|
213
213
|
/**
|
|
214
|
-
* Detect active change from progress.json files
|
|
215
|
-
*
|
|
216
|
-
*
|
|
214
|
+
* Detect active change from progress.json files.
|
|
215
|
+
*
|
|
216
|
+
* Strategy:
|
|
217
|
+
* 1. Primary — if filePath is under zhuanspec/changes/{changeId}/, extract changeId directly.
|
|
218
|
+
* 2. Fallback — scan all changes and return the most recently updated one
|
|
219
|
+
* (by lastUpdatedAt field; stub files without it fall back to filesystem mtime).
|
|
217
220
|
*/
|
|
218
|
-
async function detectActiveChange(cwd) {
|
|
221
|
+
async function detectActiveChange(cwd, filePath) {
|
|
219
222
|
const changesDir = path.join(cwd, 'zhuanspec', 'changes');
|
|
220
223
|
if (!await FileSystemUtils.directoryExists(changesDir)) {
|
|
221
224
|
return null;
|
|
222
225
|
}
|
|
226
|
+
// Primary: derive changeId from the file being modified
|
|
227
|
+
if (filePath) {
|
|
228
|
+
const absFilePath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
|
|
229
|
+
const changesDirAbs = path.resolve(changesDir);
|
|
230
|
+
if (absFilePath.startsWith(changesDirAbs + path.sep)) {
|
|
231
|
+
const changeId = path.relative(changesDirAbs, absFilePath).split(path.sep)[0];
|
|
232
|
+
if (changeId && changeId !== 'archive') {
|
|
233
|
+
const progressPath = path.join(changesDirAbs, changeId, 'metrics', 'progress.json');
|
|
234
|
+
if (await FileSystemUtils.fileExists(progressPath)) {
|
|
235
|
+
try {
|
|
236
|
+
const progress = JSON.parse(await FileSystemUtils.readFile(progressPath));
|
|
237
|
+
if (progress.phase && progress.phase !== 'idle' && PHASE_ORDER.includes(progress.phase)) {
|
|
238
|
+
return { changeId, phase: progress.phase };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// Fall through to fallback
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// Fallback: most recently updated active change
|
|
223
249
|
const entries = await fs.readdir(changesDir, { withFileTypes: true });
|
|
250
|
+
const candidates = [];
|
|
224
251
|
for (const entry of entries) {
|
|
225
252
|
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'archive')
|
|
226
253
|
continue;
|
|
227
254
|
const progressPath = path.join(changesDir, entry.name, 'metrics', 'progress.json');
|
|
228
|
-
if (await FileSystemUtils.fileExists(progressPath))
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
255
|
+
if (!await FileSystemUtils.fileExists(progressPath))
|
|
256
|
+
continue;
|
|
257
|
+
try {
|
|
258
|
+
const progress = JSON.parse(await FileSystemUtils.readFile(progressPath));
|
|
259
|
+
if (!progress.phase || progress.phase === 'idle' || !PHASE_ORDER.includes(progress.phase))
|
|
260
|
+
continue;
|
|
261
|
+
let sortKey;
|
|
262
|
+
if (progress.lastUpdatedAt) {
|
|
263
|
+
// "2026-04-22 11:39:59" (Beijing time) — replace space with T for Date parsing
|
|
264
|
+
sortKey = new Date(progress.lastUpdatedAt.replace(' ', 'T')).getTime();
|
|
265
|
+
if (isNaN(sortKey))
|
|
266
|
+
sortKey = 0;
|
|
234
267
|
}
|
|
235
|
-
|
|
236
|
-
//
|
|
268
|
+
else {
|
|
269
|
+
// Stub files (e.g. techDesign) lack lastUpdatedAt — use filesystem mtime
|
|
270
|
+
const stat = await fs.stat(progressPath);
|
|
271
|
+
sortKey = stat.mtime.getTime();
|
|
237
272
|
}
|
|
273
|
+
candidates.push({ changeId: entry.name, phase: progress.phase, sortKey });
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
// Ignore parse errors
|
|
238
277
|
}
|
|
239
278
|
}
|
|
240
|
-
|
|
279
|
+
if (candidates.length === 0)
|
|
280
|
+
return null;
|
|
281
|
+
candidates.sort((a, b) => b.sortKey - a.sortKey);
|
|
282
|
+
return { changeId: candidates[0].changeId, phase: candidates[0].phase };
|
|
241
283
|
}
|
|
242
284
|
async function runRecordProgress(filePath, toolName, success, stdinData) {
|
|
243
285
|
const cwd = process.cwd();
|
|
244
|
-
// Priority: progress.json > environment variables
|
|
245
|
-
const activeChange = await detectActiveChange(cwd);
|
|
286
|
+
// Priority: filePath-derived > progress.json scan > environment variables
|
|
287
|
+
const activeChange = await detectActiveChange(cwd, filePath);
|
|
246
288
|
const phase = activeChange?.phase || process.env.ZHUANSPEC_PHASE || 'idle';
|
|
247
289
|
const changeId = activeChange?.changeId || process.env.ZHUANSPEC_CHANGE_ID || '';
|
|
248
290
|
const currentTask = process.env.ZHUANSPEC_CURRENT_TASK || '';
|
|
@@ -641,7 +683,7 @@ export async function initializeProgress(changeId, initialPhase = 'propose') {
|
|
|
641
683
|
*/
|
|
642
684
|
export async function recordHookTrigger(hookName, hookPhase, success, options) {
|
|
643
685
|
const cwd = process.cwd();
|
|
644
|
-
const activeChange = await detectActiveChange(cwd);
|
|
686
|
+
const activeChange = await detectActiveChange(cwd, options?.filePath);
|
|
645
687
|
const phase = activeChange?.phase || process.env.ZHUANSPEC_PHASE || 'idle';
|
|
646
688
|
const changeId = activeChange?.changeId || process.env.ZHUANSPEC_CHANGE_ID || '';
|
|
647
689
|
if (!changeId || phase === 'idle')
|
package/dist/core/init.js
CHANGED
|
@@ -314,7 +314,7 @@ export class InitCommand {
|
|
|
314
314
|
}
|
|
315
315
|
const specTemplateSpinner = this.startSpinner(`正在同步 ${this.businessDirection} 业务规范模板...`);
|
|
316
316
|
const syncResult = this.syncBusinessSpecTemplate(zhuanspecPath);
|
|
317
|
-
if (syncResult.syncedSpecs || syncResult.syncedChanges) {
|
|
317
|
+
if (syncResult.syncedSpecs || syncResult.syncedChanges || syncResult.syncedKnowledge) {
|
|
318
318
|
const syncedParts = [];
|
|
319
319
|
if (syncResult.syncedSpecs) {
|
|
320
320
|
syncedParts.push('specs');
|
|
@@ -322,6 +322,9 @@ export class InitCommand {
|
|
|
322
322
|
if (syncResult.syncedChanges) {
|
|
323
323
|
syncedParts.push('changes');
|
|
324
324
|
}
|
|
325
|
+
if (syncResult.syncedKnowledge) {
|
|
326
|
+
syncedParts.push('knowledge');
|
|
327
|
+
}
|
|
325
328
|
specTemplateSpinner.stopAndPersist({
|
|
326
329
|
symbol: PALETTE.white('▌'),
|
|
327
330
|
text: PALETTE.white(`已从远端模板同步 ${syncedParts.join('、')} 目录`),
|
|
@@ -791,28 +794,29 @@ export class InitCommand {
|
|
|
791
794
|
}
|
|
792
795
|
syncBusinessSpecTemplate(zhuanspecPath) {
|
|
793
796
|
if (!this.businessDirection) {
|
|
794
|
-
return { syncedSpecs: false, syncedChanges: false };
|
|
797
|
+
return { syncedSpecs: false, syncedChanges: false, syncedKnowledge: false };
|
|
795
798
|
}
|
|
796
799
|
const tempDir = this.cloneRemoteArchitectureRepoToTemp();
|
|
797
800
|
if (!tempDir) {
|
|
798
|
-
return { syncedSpecs: false, syncedChanges: false };
|
|
801
|
+
return { syncedSpecs: false, syncedChanges: false, syncedKnowledge: false };
|
|
799
802
|
}
|
|
800
803
|
try {
|
|
801
804
|
const specsRoot = path.join(tempDir, 'specs');
|
|
802
805
|
const businessRoot = this.resolveBusinessDirectionRoot(specsRoot);
|
|
803
806
|
if (!businessRoot) {
|
|
804
|
-
return { syncedSpecs: false, syncedChanges: false };
|
|
807
|
+
return { syncedSpecs: false, syncedChanges: false, syncedKnowledge: false };
|
|
805
808
|
}
|
|
806
809
|
const templateRoot = this.findFirstTemplateSourceRoot(businessRoot);
|
|
807
810
|
if (!templateRoot) {
|
|
808
|
-
return { syncedSpecs: false, syncedChanges: false };
|
|
811
|
+
return { syncedSpecs: false, syncedChanges: false, syncedKnowledge: false };
|
|
809
812
|
}
|
|
810
813
|
const syncedSpecs = this.copyDirectoryContentsIfExists(path.join(templateRoot, 'specs'), path.join(zhuanspecPath, 'specs'));
|
|
811
814
|
const syncedChanges = this.copyDirectoryContentsIfExists(path.join(templateRoot, 'changes'), path.join(zhuanspecPath, 'changes'));
|
|
812
|
-
|
|
815
|
+
const syncedKnowledge = this.copyDirectoryContentsIfExists(path.join(templateRoot, 'knowledge'), path.join(zhuanspecPath, 'knowledge'));
|
|
816
|
+
return { syncedSpecs, syncedChanges, syncedKnowledge };
|
|
813
817
|
}
|
|
814
818
|
catch {
|
|
815
|
-
return { syncedSpecs: false, syncedChanges: false };
|
|
819
|
+
return { syncedSpecs: false, syncedChanges: false, syncedKnowledge: false };
|
|
816
820
|
}
|
|
817
821
|
finally {
|
|
818
822
|
rmSync(tempDir, { recursive: true, force: true });
|