sillyspec 3.27.11 → 3.27.12
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-execute/SKILL.md +214 -214
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +116 -116
- package/README.md +3 -2
- package/package.json +1 -1
- package/src/change-delete.js +181 -0
- package/src/docs-check.js +126 -0
- package/src/endpoint-extractor.js +7 -4
- package/src/index.js +173 -9
- package/src/init.js +104 -27
- package/src/module-changelog.js +116 -0
- package/src/progress/change-registry.js +28 -0
- package/src/progress/shared.js +6 -0
- package/src/progress/stage-machine.js +85 -8
- package/src/progress.js +3 -0
- package/src/run/command.js +25 -0
- package/src/run/complete-handlers.js +11 -1
- package/src/run/prompt.js +12 -1
- package/src/run/shared.js +32 -0
- package/src/stages/archive.js +5 -0
- package/src/stages/execute.js +1 -0
- package/src/sync.js +453 -17
- package/src/workflow.js +4 -0
- package/src/worktree-apply.js +4 -4
- package/src/worktree.js +6 -4
- package/templates/{claude-instruction.md → agents-instruction.md} +2 -2
- package/.npmrc-temp +0 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* change-delete.js — 变更删除命令(2026-08-30 用户反馈①)
|
|
3
|
+
*
|
|
4
|
+
* 背景:此前删除一个变更要靠「git rm 目录 + 借道 doctor --cleanup-ghosts 清 DB 行」两步,
|
|
5
|
+
* 且幽灵清理把删除行的 status 写成 archived——DB 无法区分「流程正常收尾的归档」与
|
|
6
|
+
* 「中途废弃的删除」,事后审计只能回溯 git。
|
|
7
|
+
*
|
|
8
|
+
* 本命令提供一等删除路径:
|
|
9
|
+
* - DB:status='deleted'(与 archived 语义分离,DB 可直接审计区分,行保留不物理删)
|
|
10
|
+
* - 目录:changes/<name>/ 物理移除(git tracked 文件删除后照常 commit,历史仍可回溯)
|
|
11
|
+
* - worktree:复用 archiveWorktreeCleanup(有未 apply 变更时保留 worktree 只警告——
|
|
12
|
+
* 不因删变更丢用户唯一副本的代码;顺带清 execute/stage-review runId marker)
|
|
13
|
+
* - git:safeGit add -A 暂存目录删除(best-effort,对齐 archive CLI 下沉 git add)
|
|
14
|
+
* - 平台:triggerSync 推终态(sync.js 对 status='deleted' 上行墓碑,平台收敛软删)
|
|
15
|
+
*
|
|
16
|
+
* 两段式安全(对齐 --cleanup-remnant / --cleanup-ghosts / --align-execute-progress 家族):
|
|
17
|
+
* 默认 dry-run 只报告将发生什么,--confirm 才执行。
|
|
18
|
+
*
|
|
19
|
+
* 执行序(DB-first + 失败补偿,对齐 renameChange 的回滚模式):DB 翻 deleted → 删目录,
|
|
20
|
+
* 目录删除失败则把 DB 行翻回原 status,避免「目录还在但行已 deleted」的分裂态。
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync, readdirSync, rmSync, statSync } from 'fs';
|
|
23
|
+
import { join, relative } from 'path';
|
|
24
|
+
import { safeGit } from './git-helper.js';
|
|
25
|
+
import { assertSafeChangeName, triggerSync } from './run/shared.js';
|
|
26
|
+
import { archiveWorktreeCleanup } from './run/complete-handlers.js';
|
|
27
|
+
|
|
28
|
+
function countFiles(dir) {
|
|
29
|
+
let n = 0;
|
|
30
|
+
let entries;
|
|
31
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return 0; }
|
|
32
|
+
for (const e of entries) {
|
|
33
|
+
if (e.isDirectory()) n += countFiles(join(dir, e.name));
|
|
34
|
+
else n++;
|
|
35
|
+
}
|
|
36
|
+
return n;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 探测变更的 worktree 状态(dry-run 也报——「删了会不会丢代码」是关键决策信息)。
|
|
41
|
+
* meta 缺失 / 探测失败 → null(无 worktree),不阻断。
|
|
42
|
+
*/
|
|
43
|
+
async function probeWorktree(cwd, changeName) {
|
|
44
|
+
try {
|
|
45
|
+
const { WorktreeManager } = await import('./worktree.js');
|
|
46
|
+
const wm = new WorktreeManager({ cwd });
|
|
47
|
+
const meta = wm.getMeta(changeName);
|
|
48
|
+
if (!meta) return null;
|
|
49
|
+
const info = { mode: meta.mode || null, worktreePath: meta.worktreePath || null, unappliedChanges: 0 };
|
|
50
|
+
if (meta.mode !== 'in-place-fallback') {
|
|
51
|
+
const check = wm.hasUnappliedChanges(changeName);
|
|
52
|
+
if (check && check.hasChanges) info.unappliedChanges = (check.changedFiles || []).length;
|
|
53
|
+
}
|
|
54
|
+
return info;
|
|
55
|
+
} catch { return null; }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 删除变更(两段式)。
|
|
60
|
+
* @param {{cwd: string, specDir?: string|null, changeName: string, confirm?: boolean}} opts
|
|
61
|
+
* @returns {Promise<{ok: boolean, reason?: string, action?: 'dry_run'|'deleted', change?: string,
|
|
62
|
+
* db_row?: {status: string, current_stage: string|null, last_active: string|null}|null,
|
|
63
|
+
* dir?: {path: string, files: number}|null, worktree?: object|null,
|
|
64
|
+
* db_updated?: boolean, dir_removed?: boolean, files_removed?: number, warnings?: string[]}>}
|
|
65
|
+
*/
|
|
66
|
+
export async function deleteChange({ cwd, specDir = null, changeName, confirm = false }) {
|
|
67
|
+
if (!changeName) return { ok: false, reason: '缺少变更名(sillyspec change-delete <名> 或 --change <名>)' };
|
|
68
|
+
try {
|
|
69
|
+
assertSafeChangeName(changeName, '变更名');
|
|
70
|
+
} catch (e) {
|
|
71
|
+
return { ok: false, reason: e.message };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const { ProgressManager } = await import('./progress.js');
|
|
75
|
+
const pm = new ProgressManager({ specDir: specDir || null });
|
|
76
|
+
const specBase = specDir || join(cwd, '.sillyspec');
|
|
77
|
+
const dirPath = join(specBase, 'changes', changeName);
|
|
78
|
+
|
|
79
|
+
// 只读探测 DB 行(不存在 DB 时 _ensureDB 会建空库——先探目录避免为纯孤儿目录凭空建库)
|
|
80
|
+
const dirExists = existsSync(dirPath);
|
|
81
|
+
let row = null;
|
|
82
|
+
if (dirExists || existsSync(join(specBase, '.runtime', 'sillyspec.db')) || existsSync(join(specBase, 'sillyspec.db'))) {
|
|
83
|
+
try {
|
|
84
|
+
const sqlDb = pm._ensureDB(cwd).getDb();
|
|
85
|
+
const r = sqlDb.prepare('SELECT name, status, current_stage, last_active FROM changes WHERE name = ?').get(changeName);
|
|
86
|
+
if (r !== undefined) row = { status: r.status, current_stage: r.current_stage, last_active: r.last_active };
|
|
87
|
+
} catch (e) {
|
|
88
|
+
return { ok: false, reason: `读取进度库失败:${e.message}` };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!row && !dirExists) {
|
|
93
|
+
return { ok: false, reason: `变更 ${changeName} 不存在(DB 无行且目录缺失)` };
|
|
94
|
+
}
|
|
95
|
+
if (row && row.status === 'archived') {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
reason: `变更 ${changeName} 已是归档态(archived)——change-delete 面向活跃/残留变更;归档历史如需清理请直接处理 changes/archive/ 目录(git 层操作)`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const alreadyDeleted = row && row.status === 'deleted';
|
|
102
|
+
if (alreadyDeleted && !dirExists) {
|
|
103
|
+
return { ok: false, reason: `变更 ${changeName} 已是删除态(deleted)且无残留目录,无需重复删除` };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const dirFiles = dirExists ? countFiles(dirPath) : 0;
|
|
107
|
+
const worktreeInfo = await probeWorktree(cwd, changeName);
|
|
108
|
+
|
|
109
|
+
if (!confirm) {
|
|
110
|
+
return {
|
|
111
|
+
ok: true, action: 'dry_run', change: changeName,
|
|
112
|
+
db_row: row, dir: dirExists ? { path: dirPath, files: dirFiles } : null,
|
|
113
|
+
worktree: worktreeInfo,
|
|
114
|
+
already_deleted: alreadyDeleted,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const warnings = [];
|
|
119
|
+
// 1. DB 翻 deleted(已是 deleted 的残留目录清理跳过——幂等)
|
|
120
|
+
let dbUpdated = false;
|
|
121
|
+
if (row && !alreadyDeleted) {
|
|
122
|
+
try {
|
|
123
|
+
pm.deleteChange(cwd, changeName);
|
|
124
|
+
dbUpdated = true;
|
|
125
|
+
} catch (e) {
|
|
126
|
+
return { ok: false, reason: `更新进度库失败:${e.message}` };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// 2. 删目录(失败回滚 DB,对齐 renameChange 的补偿模式)
|
|
130
|
+
if (dirExists) {
|
|
131
|
+
try {
|
|
132
|
+
rmSync(dirPath, { recursive: true, force: true });
|
|
133
|
+
} catch (e) {
|
|
134
|
+
if (dbUpdated) {
|
|
135
|
+
try {
|
|
136
|
+
const sqlDb = pm._ensureDB(cwd).getDb();
|
|
137
|
+
sqlDb.prepare('UPDATE changes SET status = ? WHERE name = ?').run(row.status, changeName);
|
|
138
|
+
} catch (re) {
|
|
139
|
+
warnings.push(`目录删除失败且 DB 回滚也失败(状态分裂,跑 sillyspec doctor --json 排查):${re.message}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { ok: false, reason: `删除目录失败:${e.message}`, warnings };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// 3. worktree 清理(archiveWorktreeCleanup 内建「未 apply 变更保留」护栏,不丢代码)
|
|
146
|
+
try {
|
|
147
|
+
await archiveWorktreeCleanup(cwd, changeName, specBase, {});
|
|
148
|
+
} catch (e) {
|
|
149
|
+
warnings.push(`worktree 清理失败(不阻断删除):${e.message}`);
|
|
150
|
+
}
|
|
151
|
+
// 4. git 暂存目录删除(best-effort:非 git 仓 / specBase 在仓外时跳过,git status 照常可见)
|
|
152
|
+
try {
|
|
153
|
+
const relDir = relative(cwd, dirPath).split('\\').join('/');
|
|
154
|
+
if (relDir && !relDir.startsWith('../')) {
|
|
155
|
+
safeGit(cwd, ['add', '-A', '--', relDir]);
|
|
156
|
+
}
|
|
157
|
+
} catch { /* 非 git 仓等场景:删除在 git status 可见,暂存非必需 */ }
|
|
158
|
+
// 5. 平台终态/墓碑上行(triggerSync 内建 8s 熔断 + 未连接静默;change-delete 是终态
|
|
159
|
+
// 一次性命令,await 保证墓碑发出后再退出,输出顺序确定)
|
|
160
|
+
try {
|
|
161
|
+
await triggerSync(cwd, changeName, { specRoot: specBase });
|
|
162
|
+
} catch (e) {
|
|
163
|
+
warnings.push(`平台终态同步失败(不阻断删除,下次常规同步重试):${e.message}`);
|
|
164
|
+
}
|
|
165
|
+
// 6. 审计留痕(破坏性操作,对齐 complete-stage --force 的审计口径)
|
|
166
|
+
try {
|
|
167
|
+
pm._appendAuditLog(cwd, {
|
|
168
|
+
action: 'change-delete',
|
|
169
|
+
change: changeName,
|
|
170
|
+
db_status_before: row ? row.status : null,
|
|
171
|
+
dir_removed: dirExists,
|
|
172
|
+
files_removed: dirFiles,
|
|
173
|
+
});
|
|
174
|
+
} catch { /* 审计失败不阻断 */ }
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
ok: true, action: 'deleted', change: changeName,
|
|
178
|
+
db_updated: dbUpdated, dir_removed: dirExists, files_removed: dirFiles,
|
|
179
|
+
worktree: worktreeInfo, warnings,
|
|
180
|
+
};
|
|
181
|
+
}
|
package/src/docs-check.js
CHANGED
|
@@ -833,3 +833,129 @@ export async function runDecisionRules(opts) {
|
|
|
833
833
|
exempted.sort((a, b) => a.key.localeCompare(b.key))
|
|
834
834
|
return { empty: false, domains: domainFiles.length, entries: entriesTotal, implemented: implementedTotal, threshold, findings, exempted }
|
|
835
835
|
}
|
|
836
|
+
|
|
837
|
+
// ---------------------------------------------------------------------------
|
|
838
|
+
// 变更名提名规则族(advisory,2026-08-31 变更关联审计 P2)
|
|
839
|
+
//
|
|
840
|
+
// 校验对象:文档正文里提名的变更名 token(`2026-NN-NN-<slug>`)。背景实测:变更间关联
|
|
841
|
+
// 的真实形态是散文提名(全仓 265 处他者提名,85% 属实质依赖/溯源/叙事),而非路径链接
|
|
842
|
+
// 或声明式字段;提名当前完全不受校验——拼错、归档改名(历史 3 单)后静默烂掉没人发现。
|
|
843
|
+
// 规则单一:提名的名字在「活跃 ∪ 归档」变更名单中存在;不存在 = 悬空。
|
|
844
|
+
// 铁律(与决策规则族同款,docs-consistency 模块卡):advisory 只 warn 不进 runDocsCheck
|
|
845
|
+
// 的 invalid 阻断链——ok/invalid 判定、exit code、docs gate 阻断行为均不受影响;只读零写盘。
|
|
846
|
+
// 豁免走 known_failures `change-name.<名字>` 键(decisions.* 同款机制):prompt 模板示例名、
|
|
847
|
+
// 跨仓变更名(repo:// 语义但散文书写)按名豁免,披露不隐藏。
|
|
848
|
+
// 结构化排除(非豁免,正则层面):
|
|
849
|
+
// - lookbehind [A-Za-z0-9-]:x- 前缀化合物不拆半名(review-2026-08-08.md 文档名、
|
|
850
|
+
// brainstorm-review-2026-08-23-205426 run-id 时间戳、双日期前缀目录名整体不命中);
|
|
851
|
+
// - slug 字母开头:纯数字 slug(HHMMSS 时间戳类)不命中;
|
|
852
|
+
// - ql-* quicklog id 不是变更目录名,天然不命中(quick 关联另有 CLI 悬空守卫)。
|
|
853
|
+
// ---------------------------------------------------------------------------
|
|
854
|
+
|
|
855
|
+
/** 缺省扫描集:主链路 docs 两侧 + 变更文档。强制 skip 追加 archive/(归档文档是冻结
|
|
856
|
+
* 历史事实,当时提名不追改)与 docs/prompt/(agent 指令模板,示例变更名 by-design 虚构) */
|
|
857
|
+
export const CHANGE_NAME_DEFAULT_PATHS = ['docs/**/*.md', '.sillyspec/docs/**/*.md', '.sillyspec/changes/**/*.md']
|
|
858
|
+
const CHANGE_NAME_FORCED_SKIP = ['.sillyspec/changes/archive', 'docs/prompt']
|
|
859
|
+
|
|
860
|
+
/** 变更名 token:日期前缀 + 字母开头 slug(≥3 字符) */
|
|
861
|
+
const CHANGE_NAME_TOKEN_RE = /(?<![A-Za-z0-9-])(\d{4}-\d{2}-\d{2}-[a-z][a-z0-9-]{2,})/g
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* 纯函数:提取文本里的变更名提名 token(matchAll 对共享 /g 正则内部克隆,无 lastIndex 竞态)。
|
|
865
|
+
* 紧随 `.md` 的 token 排除——那是文件名提及(.sillyspec/plans/ 计划快照、他仓 docs 路径),
|
|
866
|
+
* 不是变更目录提名(目录提名后随 `/` 或独立成词)。
|
|
867
|
+
* @param {string} text 行文本或全文
|
|
868
|
+
* @returns {string[]} 名字列表(含重复,调用方自行聚合计数)
|
|
869
|
+
*/
|
|
870
|
+
export function collectChangeNameTokens(text) {
|
|
871
|
+
const md = String(text || '')
|
|
872
|
+
return [...md.matchAll(CHANGE_NAME_TOKEN_RE)]
|
|
873
|
+
.filter(m => md.slice(m.index + m[0].length, m.index + m[0].length + 3).toLowerCase() !== '.md')
|
|
874
|
+
.map(m => m[1])
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* 变更名单收集:活跃目录 ∪ 归档目录(+ 调用方注入额 extraNames,如 DB 名单——DB 与目录
|
|
879
|
+
* 历史上曾分叉,并集口径防假阳性)。目录缺失 → 跳过不抛。
|
|
880
|
+
* @param {string} specBase 规范根(<root>/.sillyspec 或平台 specRoot)
|
|
881
|
+
* @param {string[]} [extraNames]
|
|
882
|
+
* @returns {Set<string>}
|
|
883
|
+
*/
|
|
884
|
+
export function collectKnownChangeNames(specBase, extraNames = []) {
|
|
885
|
+
const names = new Set()
|
|
886
|
+
for (const sub of ['changes', join('changes', 'archive')]) {
|
|
887
|
+
try {
|
|
888
|
+
for (const e of readdirSync(join(specBase, sub), { withFileTypes: true })) {
|
|
889
|
+
if (e.isDirectory()) names.add(e.name)
|
|
890
|
+
}
|
|
891
|
+
} catch { /* 目录缺失 → 跳过 */ }
|
|
892
|
+
}
|
|
893
|
+
for (const n of extraNames) if (n && typeof n === 'string') names.add(n)
|
|
894
|
+
return names
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* IO 入口(advisory):扫文档集报告悬空的变更名提名。不进 invalid 阻断链、不写盘。
|
|
899
|
+
* @param {{ projectRoot: string, specBase?: string|null, paths?: string[]|null, skip?: string[]|null,
|
|
900
|
+
* knownFailures?: string[]|null, extraNames?: string[] }} opts
|
|
901
|
+
* paths 缺省 CHANGE_NAME_DEFAULT_PATHS(与主链路不同:多扫 .sillyspec/changes/——变更
|
|
902
|
+
* design/proposal 是实质依赖提名的主阵地);skip 缺省读 local.yaml 并强制追加
|
|
903
|
+
* .sillyspec/changes/archive;knownFailures 缺省读 local.yaml known_failures
|
|
904
|
+
* @returns {{ empty: boolean, scannedDocs: number, mentions: number, uniqueNames: number,
|
|
905
|
+
* findings: Array<{ doc: string, docLine: number, name: string, message: string }>,
|
|
906
|
+
* exempted: Array<{ key: string, name: string }> }}
|
|
907
|
+
*/
|
|
908
|
+
export function runChangeNameAdvisory(opts) {
|
|
909
|
+
const { projectRoot, specBase = null, paths = null, skip = null, knownFailures = null, extraNames = [] } = opts || {}
|
|
910
|
+
const emptyResult = { empty: true, scannedDocs: 0, mentions: 0, uniqueNames: 0, findings: [], exempted: [] }
|
|
911
|
+
if (!projectRoot) return emptyResult
|
|
912
|
+
|
|
913
|
+
const cfg = readDocsCheckConfig(projectRoot)
|
|
914
|
+
const globs = paths || cfg.paths || CHANGE_NAME_DEFAULT_PATHS
|
|
915
|
+
const skipList = [...(skip || cfg.skip || []), ...CHANGE_NAME_FORCED_SKIP]
|
|
916
|
+
let failures = knownFailures
|
|
917
|
+
if (!Array.isArray(failures)) {
|
|
918
|
+
try {
|
|
919
|
+
failures = extractKnownFailureKeys(readFileSync(join(projectRoot, '.sillyspec', 'local.yaml'), 'utf8'))
|
|
920
|
+
} catch { failures = [] }
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// 名单口径:本仓活跃∪归档 ∪ 调用方注入(如 DB 名)∪ 跨仓根(docs-check.cross_repo_roots
|
|
924
|
+
// 已配置的兄弟仓——散文跨仓提名无 repo:// 标记,出现在任一已配置根的变更名录即有效;
|
|
925
|
+
// 未配置根不参与,与 repo:// 行号校验的「未配映射跳过」同语义)
|
|
926
|
+
const known = collectKnownChangeNames(specBase || join(projectRoot, '.sillyspec'), extraNames)
|
|
927
|
+
for (const root of Object.values(cfg.crossRepoRoots || {})) {
|
|
928
|
+
for (const n of collectKnownChangeNames(join(root, '.sillyspec'))) known.add(n)
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const docs = new Set()
|
|
932
|
+
for (const g of globs) for (const rel of walkGlob(projectRoot, g, skipList)) docs.add(rel)
|
|
933
|
+
|
|
934
|
+
const findings = []
|
|
935
|
+
const exempted = []
|
|
936
|
+
const seenNames = new Set()
|
|
937
|
+
let mentions = 0
|
|
938
|
+
let scanned = 0
|
|
939
|
+
for (const rel of [...docs].sort()) {
|
|
940
|
+
let lines
|
|
941
|
+
try { lines = readFileSync(join(projectRoot, rel), 'utf8').replace(/\r\n?/g, '\n').split('\n') } catch { continue }
|
|
942
|
+
scanned++
|
|
943
|
+
lines.forEach((line, i) => {
|
|
944
|
+
for (const name of collectChangeNameTokens(line)) {
|
|
945
|
+
mentions++
|
|
946
|
+
seenNames.add(name)
|
|
947
|
+
if (known.has(name)) continue
|
|
948
|
+
if (failures.includes(`change-name.${name}`)) {
|
|
949
|
+
exempted.push({ key: `change-name.${name}`, name })
|
|
950
|
+
continue
|
|
951
|
+
}
|
|
952
|
+
findings.push({
|
|
953
|
+
doc: rel, docLine: i + 1, name,
|
|
954
|
+
message: `[${rel}:L${i + 1}] 提名「${name}」在活跃∪归档变更名单中不存在——拼错 / 改名未同步 / 他仓变更?确认后修正,或在 local.yaml known_failures 加 "change-name.${name}" 豁免(advisory 不阻断)`,
|
|
955
|
+
})
|
|
956
|
+
}
|
|
957
|
+
})
|
|
958
|
+
}
|
|
959
|
+
findings.sort((a, b) => a.doc.localeCompare(b.doc) || a.docLine - b.docLine || a.name.localeCompare(b.name))
|
|
960
|
+
return { empty: scanned === 0, scannedDocs: scanned, mentions, uniqueNames: seenNames.size, findings, exempted }
|
|
961
|
+
}
|
|
@@ -47,8 +47,10 @@ export function extractFastApiEndpoints(filePath) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
// 2. 装饰器全文匹配:单行 `@router.get("/path")`、分散式 `@router.get\n("/path")`、
|
|
50
|
-
// 多行参数 `@router.get(\n "/path",\n response_model=…)` 三态合一(\s*
|
|
51
|
-
|
|
50
|
+
// 多行参数 `@router.get(\n "/path",\n response_model=…)` 三态合一(\s* 跨换行)。
|
|
51
|
+
// 路径组 `[^"'`]*` 允许空串(坑 endpoints-extract-worktree-pitfalls ③:`@router.get("")`
|
|
52
|
+
// 空路径装饰器 = 前缀本身即路由,`+` 量词静默漏扫——GET /notifications 实证丢失)。
|
|
53
|
+
const decoratorRe = /@(?:router|api_router)\.(get|post|put|delete|patch)\s*\(\s*["'`]([^"'`]*)["'`]/g
|
|
52
54
|
let m
|
|
53
55
|
while ((m = decoratorRe.exec(content)) !== null) {
|
|
54
56
|
endpoints.push({
|
|
@@ -82,8 +84,9 @@ export function extractExpressEndpoints(filePath) {
|
|
|
82
84
|
if (useMatch) routerPrefix = useMatch[1]
|
|
83
85
|
}
|
|
84
86
|
|
|
85
|
-
// 路由全文匹配(坑 endpoint-multiline-decorator-miss:`router.get(\n "/path",\n handler)`
|
|
86
|
-
|
|
87
|
+
// 路由全文匹配(坑 endpoint-multiline-decorator-miss:`router.get(\n "/path",\n handler)` 同病;
|
|
88
|
+
// 空串路径与 FastAPI 同理允许——`router.get("")` = use 前缀本身)
|
|
89
|
+
const routeRe = /\b(?:app|router)\.(get|post|put|delete|patch)\s*\(\s*["'`]([^"'`]*)["'`]/gi
|
|
87
90
|
let m
|
|
88
91
|
while ((m = routeRe.exec(content)) !== null) {
|
|
89
92
|
endpoints.push({ method: m[1].toUpperCase(), path: routerPrefix + m[2], source: filePath, line: lineOfIndex(content, m.index) })
|
package/src/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'fs';
|
|
10
10
|
import { writeAtomicSync } from './fs-atomic.js';
|
|
11
|
-
import { basename, extname, join, resolve } from 'path';
|
|
11
|
+
import { basename, extname, join, resolve, isAbsolute } from 'path';
|
|
12
12
|
import { safeGit, git } from './git-helper.js';
|
|
13
13
|
import { getVersion } from './version.js';
|
|
14
14
|
|
|
@@ -118,6 +118,8 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
118
118
|
生成 Windows 安全 TaskCard 骨架(LF 行尾 + frontmatter 闭合 +
|
|
119
119
|
硬校验 9 字段齐全,标题自动取自 plan.md,Edit 填占位符即可)
|
|
120
120
|
sillyspec change-rename <旧变更名> <新变更名>
|
|
121
|
+
sillyspec change-delete <变更名> [--confirm] 删除变更(DB status='deleted' 与归档语义分离 +
|
|
122
|
+
移目录 + 清 worktree + 推平台墓碑;默认 dry-run)
|
|
121
123
|
sillyspec knowledge <search --query "..." --limit N
|
|
122
124
|
| inspect --id "..."
|
|
123
125
|
| validate | refresh
|
|
@@ -127,6 +129,8 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
127
129
|
| sync [--change <name>] | sync-docs [--change <name>]
|
|
128
130
|
| status | pointer [--cleanup]
|
|
129
131
|
| approve <change> | reject <change> [--reason <r>]>
|
|
132
|
+
sillyspec pull --spec [--force] 拉取服务器 spec 快照到本地 .sillyspec(打包时刻
|
|
133
|
+
快照非实时、无自动同步;进度六表拉取用 platform pull)
|
|
130
134
|
sillyspec docs migrate
|
|
131
135
|
sillyspec dashboard [--port <N>] [--no-open]
|
|
132
136
|
|
|
@@ -911,7 +915,7 @@ async function main() {
|
|
|
911
915
|
// 与 verifyApiParity 的聚合读侧(contract-artifacts/<change>/*/endpoints.json 全量并集)对齐。
|
|
912
916
|
const epSub = filteredArgs[1];
|
|
913
917
|
if (epSub !== 'extract') {
|
|
914
|
-
console.error('用法: sillyspec endpoints extract --change <name> [--task task-NN | --all-tasks] [--dir <backendRoot> | --files <a.py,b.js...>] [--spec-dir <path>]\n 静态扫描后端路由装饰器生成 endpoints.json(FastAPI/Express/Spring);verify 探针 5 消费。\n --all-tasks:聚合逐 task 卡 allowed_paths 各自提取(对账口径与探针 5
|
|
918
|
+
console.error('用法: sillyspec endpoints extract --change <name> [--task task-NN | --all-tasks] [--dir <backendRoot> | --files <a.py,b.js...>] [--spec-dir <path>]\n 静态扫描后端路由装饰器生成 endpoints.json(FastAPI/Express/Spring);verify 探针 5 消费。\n --all-tasks:聚合逐 task 卡 allowed_paths 各自提取(对账口径与探针 5 聚合读侧一致,推荐)。\n execute worktree 模式:allowed_paths 自动按 worktree 根解析,产物恒落主仓 spec(坑 endpoints-extract-worktree-pitfalls)');
|
|
915
919
|
process.exit(2);
|
|
916
920
|
}
|
|
917
921
|
const epChangeIdx = args.indexOf('--change');
|
|
@@ -934,14 +938,44 @@ async function main() {
|
|
|
934
938
|
assertSafeChangeName(epChange, '--change 变更名');
|
|
935
939
|
const { extractFastApiEndpoints, extractExpressEndpoints, extractSpringEndpoints, scanBackendEndpoints } = await import('./endpoint-extractor.js');
|
|
936
940
|
const endpoints = [];
|
|
937
|
-
|
|
941
|
+
let epSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
|
|
942
|
+
// 坑 endpoints-extract-worktree-pitfalls ②(2026-08-29 实测):CLI 在 execute worktree 内跑时
|
|
943
|
+
// spec 命中副本(worktree 内 checkout 出来的 .sillyspec)→ 产物落 worktree 自己的
|
|
944
|
+
// contract-artifacts/,主仓 verify 探针 5 读不到。复用 detectWorktreeSpecDrift 判据锚回
|
|
945
|
+
// 主仓 spec:产物落点恒主仓,扫描面(--dir/--files 绝对路径)不受影响。
|
|
946
|
+
const epDrift = (await import('./run/shared.js')).detectWorktreeSpecDrift(epSpecBase);
|
|
947
|
+
if (epDrift) {
|
|
948
|
+
console.log(`⚠️ 已自动锚定主仓 spec:${epDrift.mainSpecBase}(endpoints 产物落主仓;当前 cwd 命中 worktree 副本 ${epDrift.changeName},流程继续)`);
|
|
949
|
+
epSpecBase = epDrift.mainSpecBase;
|
|
950
|
+
}
|
|
938
951
|
const epChangeDir = join(epSpecBase, 'changes', epChange);
|
|
939
952
|
if (!existsSync(epChangeDir)) {
|
|
940
953
|
console.error(`❌ 变更目录不存在: ${epChangeDir}`);
|
|
941
954
|
process.exit(1);
|
|
942
955
|
}
|
|
956
|
+
// 坑 endpoints-extract-worktree-pitfalls ①(2026-08-29 实测 13 task 全 0 端点):
|
|
957
|
+
// execute worktree 模式下变更代码只在 worktree 里,allowed_paths 按主仓(cwd)相对
|
|
958
|
+
// 解析 0 命中。活跃 worktree meta 存在时相对路径优先按 worktree 根解析——worktree =
|
|
959
|
+
// baseline 检出 + 本变更改动,扫描结果即交付态;不在 worktree 的文件回落主仓(显式
|
|
960
|
+
// --files 绝对路径原样通过)。WorktreeManager 从 worktree 内跑也会经 git-common-dir
|
|
961
|
+
// 锚定主仓 meta(构造器注释),meta 读取失败按无 worktree 处理不阻断。
|
|
962
|
+
let epWorktreeRoot = null;
|
|
963
|
+
try {
|
|
964
|
+
const { WorktreeManager } = await import('./worktree.js');
|
|
965
|
+
const epWtMeta = new WorktreeManager(dir).getMeta(epChange);
|
|
966
|
+
if (epWtMeta?.worktreePath && existsSync(epWtMeta.worktreePath)) epWorktreeRoot = epWtMeta.worktreePath;
|
|
967
|
+
} catch { /* meta 读取失败按无 worktree 处理 */ }
|
|
968
|
+
const resolveScoped = (p) => {
|
|
969
|
+
if (isAbsolute(p)) return p;
|
|
970
|
+
if (epWorktreeRoot) {
|
|
971
|
+
const inWt = join(epWorktreeRoot, p);
|
|
972
|
+
if (existsSync(inWt)) return inWt;
|
|
973
|
+
}
|
|
974
|
+
return resolve(p);
|
|
975
|
+
};
|
|
976
|
+
if (epWorktreeRoot) console.log(`ℹ️ 变更有活跃 worktree,扫描面优先按其根解析:${epWorktreeRoot}`);
|
|
943
977
|
const collectFile = (f) => {
|
|
944
|
-
const abs =
|
|
978
|
+
const abs = resolveScoped(f)
|
|
945
979
|
if (!existsSync(abs)) return
|
|
946
980
|
const ext = extname(abs).toLowerCase()
|
|
947
981
|
try {
|
|
@@ -984,7 +1018,7 @@ async function main() {
|
|
|
984
1018
|
const taskName = card.replace(/\.md$/, '')
|
|
985
1019
|
const eps = []
|
|
986
1020
|
const perFile = (f) => {
|
|
987
|
-
const abs =
|
|
1021
|
+
const abs = resolveScoped(f)
|
|
988
1022
|
if (!existsSync(abs)) return
|
|
989
1023
|
const ext = extname(abs).toLowerCase()
|
|
990
1024
|
try {
|
|
@@ -1019,10 +1053,10 @@ async function main() {
|
|
|
1019
1053
|
}
|
|
1020
1054
|
let scanned = 0
|
|
1021
1055
|
for (const p of candidates) {
|
|
1022
|
-
const abs =
|
|
1056
|
+
const abs = resolveScoped(p)
|
|
1023
1057
|
if (!existsSync(abs)) continue
|
|
1024
1058
|
scanned++
|
|
1025
|
-
collectFile(
|
|
1059
|
+
collectFile(abs)
|
|
1026
1060
|
}
|
|
1027
1061
|
if (candidates.size === 0) {
|
|
1028
1062
|
console.error('❌ 无扫描面:--task 卡无 allowed_paths 且 design.md 无具体文件清单——显式传 --dir <backendRoot> 或 --files <a.py,b.js>,或用 --all-tasks 聚合逐卡提取');
|
|
@@ -1205,7 +1239,7 @@ async function main() {
|
|
|
1205
1239
|
docsCheckFlags.push(a);
|
|
1206
1240
|
}
|
|
1207
1241
|
}
|
|
1208
|
-
const { runDocsCheck, readDocsCheckConfig, DocsCheckConfigError, applyFixes } = await import('./docs-check.js');
|
|
1242
|
+
const { runDocsCheck, readDocsCheckConfig, DocsCheckConfigError, applyFixes, runChangeNameAdvisory } = await import('./docs-check.js');
|
|
1209
1243
|
try {
|
|
1210
1244
|
// 配置优先级:--paths > local.yaml docs-check.paths > 缺省 docs/**/*.md
|
|
1211
1245
|
const cfg = readDocsCheckConfig(dir);
|
|
@@ -1236,10 +1270,20 @@ async function main() {
|
|
|
1236
1270
|
}
|
|
1237
1271
|
}
|
|
1238
1272
|
const fixResult = fixActive ? applyFixes(dir, fixes, dryRun ? { dryRun: true } : {}) : null;
|
|
1273
|
+
// 变更名提名 advisory(2026-08-31 变更关联审计 P2):铁律同决策规则族——只 warn,
|
|
1274
|
+
// 不进 invalid、不影响下方 exit code 与 docs gate。降级容错:advisory 自身异常
|
|
1275
|
+
// 不拖垮主检查(try-catch 吞并提示)。
|
|
1276
|
+
let changeNameReport = null;
|
|
1277
|
+
try {
|
|
1278
|
+
changeNameReport = runChangeNameAdvisory({ projectRoot: dir });
|
|
1279
|
+
} catch (e) {
|
|
1280
|
+
console.warn(`⚠️ 变更名提名检查执行失败(advisory 降级跳过):${e.message}`);
|
|
1281
|
+
}
|
|
1239
1282
|
if (json) {
|
|
1240
1283
|
// --fix 与 --json 组合保持 stdout 纯 JSON 可解析(constraints):修复统计作 result.fixReport
|
|
1241
1284
|
// 附加后整体 stringify,主体仍是 runDocsCheck 返回(invalid[].fix 是 task-01 设计内增量)
|
|
1242
1285
|
if (fixResult) result.fixReport = { applied: fixResult.applied, skipped: fixResult.skipped.length, dryRun };
|
|
1286
|
+
if (changeNameReport) result.changeNameReport = changeNameReport;
|
|
1243
1287
|
console.log(JSON.stringify(result, null, 2));
|
|
1244
1288
|
} else {
|
|
1245
1289
|
for (const w of result.warnings) console.warn(`⚠️ ${w}`);
|
|
@@ -1278,6 +1322,16 @@ async function main() {
|
|
|
1278
1322
|
console.error(`\n修复指引:行号漂移 → 更新文档行号到当前源码;文件删改名 → 更新引用路径;`);
|
|
1279
1323
|
console.error(`关键词缺失但行号正确 → 确认符号是否改名,改文档 token 或行号。`);
|
|
1280
1324
|
}
|
|
1325
|
+
// 变更名提名 advisory 段(主结果之后、exit 之前;不参与 ok/invalid 与 exit code)。
|
|
1326
|
+
// 零输出原则(docs-check-fix 契约:无新 flag 时 stdout 逐字节一致;决策规则族同款
|
|
1327
|
+
// 「无信号零输出」):仅 findings 非空才输出——悬空即信号,无悬空不打扰。
|
|
1328
|
+
if (changeNameReport && changeNameReport.findings.length > 0) {
|
|
1329
|
+
console.warn(`⚠️ 变更名提名悬空 ${changeNameReport.findings.length}/${changeNameReport.mentions} 处(advisory 不阻断、不进 docs gate):`);
|
|
1330
|
+
for (const f of changeNameReport.findings) console.warn(` - ${f.message}`);
|
|
1331
|
+
if (changeNameReport.exempted.length > 0) {
|
|
1332
|
+
console.log(`ℹ️ 另有 ${changeNameReport.exempted.length} 处经 known_failures change-name.* 键豁免`);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1281
1335
|
}
|
|
1282
1336
|
// exit code(design §5.2 行为矩阵逐字对齐):全绿 → 0(--fix 无操作);--fix 后全部
|
|
1283
1337
|
// fixable 修复完且无 needs-manual 残留 → 0;仍有 needs-manual 或写回 skipped → 1;
|
|
@@ -2142,6 +2196,9 @@ SillySpec platform — SillyHub 平台同步
|
|
|
2142
2196
|
sillyspec platform pointer [--cleanup]
|
|
2143
2197
|
sillyspec platform approve <change-name>
|
|
2144
2198
|
sillyspec platform reject <change-name> [--reason <reason>]
|
|
2199
|
+
|
|
2200
|
+
注: platform pull 拉的是进度六表(多机进度同步);拉取 spec 文件整树快照请用
|
|
2201
|
+
sillyspec pull --spec(X2,打包时刻快照非实时)。
|
|
2145
2202
|
`);
|
|
2146
2203
|
break;
|
|
2147
2204
|
}
|
|
@@ -2504,6 +2561,53 @@ checkbox 行;depends_on 自动反填行内注解 "(depends_on: task-01,02)";
|
|
|
2504
2561
|
pm.renameChange(dir, oldName, newName);
|
|
2505
2562
|
break;
|
|
2506
2563
|
}
|
|
2564
|
+
case 'change-delete': {
|
|
2565
|
+
// 2026-08-30 用户反馈①:一等删除路径——此前删变更靠 git rm + 借道幽灵清理,
|
|
2566
|
+
// 且幽灵清理把删除行写成 archived,DB 无法区分归档与删除。两段式(默认 dry-run)。
|
|
2567
|
+
// 变更名:位置参数(对齐 change-rename)或 --change(对齐 gate/derive)二选一。
|
|
2568
|
+
let delName = filteredArgs[1];
|
|
2569
|
+
if (!delName || delName.startsWith('-')) {
|
|
2570
|
+
const idx = filteredArgs.indexOf('--change');
|
|
2571
|
+
if (idx >= 0 && filteredArgs[idx + 1]) delName = filteredArgs[idx + 1];
|
|
2572
|
+
}
|
|
2573
|
+
if (!delName || delName.startsWith('-')) {
|
|
2574
|
+
console.error('❌ 用法: sillyspec change-delete <变更名> [--confirm](或 --change <变更名>)');
|
|
2575
|
+
process.exitCode = 2;
|
|
2576
|
+
break;
|
|
2577
|
+
}
|
|
2578
|
+
const delConfirm = filteredArgs.includes('--confirm');
|
|
2579
|
+
const { deleteChange } = await import('./change-delete.js');
|
|
2580
|
+
const r = await deleteChange({
|
|
2581
|
+
cwd: dir,
|
|
2582
|
+
specDir: resolvePlatformSpecDir(dir, specDir),
|
|
2583
|
+
changeName: delName,
|
|
2584
|
+
confirm: delConfirm,
|
|
2585
|
+
});
|
|
2586
|
+
if (json) {
|
|
2587
|
+
console.log(JSON.stringify(r, null, 2));
|
|
2588
|
+
} else if (!r.ok) {
|
|
2589
|
+
console.error(`❌ ${r.reason}`);
|
|
2590
|
+
} else if (r.action === 'dry_run') {
|
|
2591
|
+
console.log(`🗑️ 变更 ${delName} 待删除(dry-run,未执行):`);
|
|
2592
|
+
if (r.db_row) console.log(` DB 行:status=${r.db_row.status}${r.already_deleted ? '(已删除,本次仅清残留)' : ` → deleted(与 archived 语义分离,审计可直接区分)`}`);
|
|
2593
|
+
else console.log(' DB 行:无(孤儿目录,仅删目录)');
|
|
2594
|
+
if (r.dir) console.log(` 目录:${r.dir.path}(${r.dir.files} 个文件,git tracked 文件可从历史回溯)`);
|
|
2595
|
+
else console.log(' 目录:不存在(仅 DB 行翻 deleted——取代 git rm 后借道幽灵清理的老路径)');
|
|
2596
|
+
if (r.worktree) {
|
|
2597
|
+
console.log(` worktree:${r.worktree.mode}${r.worktree.unappliedChanges > 0
|
|
2598
|
+
? `(⚠️ ${r.worktree.unappliedChanges} 个未 apply 变更——删除时保留 worktree 不丢代码,确认废弃后手动 cleanup)`
|
|
2599
|
+
: '(将一并清理)'}`);
|
|
2600
|
+
}
|
|
2601
|
+
console.log(`\n加 --confirm 执行删除(DB status='deleted' + 移除目录 + 清理 worktree + 暂存 git 删除 + 推平台墓碑)。`);
|
|
2602
|
+
} else {
|
|
2603
|
+
console.log(`🗑️ 变更 ${delName} 已删除:`);
|
|
2604
|
+
if (r.db_updated) console.log(` ✅ DB status='deleted'(与 archived 语义分离,审计可直接区分)`);
|
|
2605
|
+
if (r.dir_removed) console.log(` ✅ 目录已移除(${r.files_removed} 个文件,git 历史可回溯)`);
|
|
2606
|
+
for (const w of r.warnings || []) console.log(` ⚠️ ${w}`);
|
|
2607
|
+
}
|
|
2608
|
+
process.exitCode = r.ok ? 0 : 1;
|
|
2609
|
+
break;
|
|
2610
|
+
}
|
|
2507
2611
|
case 'workflow': {
|
|
2508
2612
|
const wfSub = filteredArgs[1];
|
|
2509
2613
|
if (!wfSub || wfSub === 'help' || wfSub === '--help') {
|
|
@@ -2936,8 +3040,68 @@ SillySpec config — local.yaml 配置键速查
|
|
|
2936
3040
|
}
|
|
2937
3041
|
break;
|
|
2938
3042
|
}
|
|
3043
|
+
case 'pull': {
|
|
3044
|
+
// X2(task-14 / design §7.1 §7.4 / FR-07 / FR-08):拉取服务器 spec 整树快照。
|
|
3045
|
+
// 注意与 platform pull(进度六表 DB import,多机进度同步)语义正交——本命令操作
|
|
3046
|
+
// .sillyspec 文件树;命名 pull --spec 显式区分,防误用。
|
|
3047
|
+
const pullSpecArgs = filteredArgs.slice(1);
|
|
3048
|
+
const pullWantsHelp = pullSpecArgs.length === 0
|
|
3049
|
+
|| pullSpecArgs.includes('help') || pullSpecArgs.includes('--help') || pullSpecArgs.includes('-h');
|
|
3050
|
+
const pullWantsSpec = pullSpecArgs.includes('--spec');
|
|
3051
|
+
const printPullSpecHelp = () => console.log(`
|
|
3052
|
+
SillySpec pull — 拉取服务器 spec 快照到本地(X2 / FR-07)
|
|
3053
|
+
|
|
3054
|
+
用法:
|
|
3055
|
+
sillyspec pull --spec [--force]
|
|
3056
|
+
|
|
3057
|
+
说明:
|
|
3058
|
+
从 SillyHub 平台拉取当前工作区的 .sillyspec 整树快照(tar 解压到 specDir,
|
|
3059
|
+
含 tar 顶层 PLATFORM-BUNDLE.json 快照元数据,可离线辨认快照新旧)。
|
|
3060
|
+
覆盖语义:specDir 为空 → 直接解压;非空 → 必须加 --force 整树替换
|
|
3061
|
+
(先删除本地整棵 .sillyspec 再解包——.runtime(进度 DB)等本地文件会一并丢失;
|
|
3062
|
+
--force 保留连接凭据 local.yaml(服务端 bundle 恒排除它),覆盖后无需重新 connect;
|
|
3063
|
+
崩溃安全:覆盖前凭据先备份为 .sillyspec.local.yaml.bak,成功后自动清理,
|
|
3064
|
+
中途失败可从该备份文件恢复)。
|
|
3065
|
+
|
|
3066
|
+
⚠️ 快照语义:拉取的是服务器「打包时刻」的快照,非实时镜像——
|
|
3067
|
+
本命令不自动同步、不在会话中自动刷新;拉取后本地的改动仍需正常推送。
|
|
3068
|
+
daemon 模式下任务/会话开始时会按 spec_version 自动取新,无需手动跑本命令。
|
|
3069
|
+
|
|
3070
|
+
拉取进度六表(多机进度同步)请用:sillyspec platform pull [--change <name>]
|
|
3071
|
+
`);
|
|
3072
|
+
if (pullWantsHelp || !pullWantsSpec) {
|
|
3073
|
+
printPullSpecHelp();
|
|
3074
|
+
if (!pullWantsHelp) {
|
|
3075
|
+
console.error('❌ 缺少 --spec(本命令拉取 spec 整树快照;进度六表拉取用 sillyspec platform pull)');
|
|
3076
|
+
process.exit(1);
|
|
3077
|
+
}
|
|
3078
|
+
break;
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
const pullSpecForce = pullSpecArgs.includes('--force');
|
|
3082
|
+
const syncModule = await import('./sync.js');
|
|
3083
|
+
// 未连接明确提示不崩(对齐 platform pull :2289 先例)
|
|
3084
|
+
const smForSpecPull = new syncModule.SyncManager(dir);
|
|
3085
|
+
if (!smForSpecPull._getPlatform()) {
|
|
3086
|
+
console.error('❌ 未连接平台,请先 sillyspec platform connect');
|
|
3087
|
+
process.exit(1);
|
|
3088
|
+
}
|
|
3089
|
+
const specR = await syncModule.pullSpecBundle(dir, { force: pullSpecForce, specDir });
|
|
3090
|
+
if (specR.ok && specR.pulled) {
|
|
3091
|
+
console.log(`✅ 已拉取 spec 快照 → ${specR.specDir}${specR.specVersion ? `(平台 spec_version: ${specR.specVersion})` : ''}`);
|
|
3092
|
+
console.log(' 快照语义:打包时刻快照非实时,无自动同步;本地后续改动仍需正常推送。');
|
|
3093
|
+
} else if (/非空/.test(specR.reason || '')) {
|
|
3094
|
+
console.error(`❌ ${specR.reason}`);
|
|
3095
|
+
console.error(' 确认整树替换请加 --force(先删除本地 .sillyspec 整树再解包,进度 DB 等本地文件一并丢失;连接凭据 local.yaml 保留,无需重新 connect)');
|
|
3096
|
+
process.exit(1);
|
|
3097
|
+
} else {
|
|
3098
|
+
console.error(`❌ 拉取 spec 快照失败: ${specR.reason || '未知'}`);
|
|
3099
|
+
process.exit(1);
|
|
3100
|
+
}
|
|
3101
|
+
break;
|
|
3102
|
+
}
|
|
2939
3103
|
default: {
|
|
2940
|
-
const topCommands = ['init', 'setup', 'run', 'progress', 'worktree', 'dispatch', 'agent-log', 'local', 'workflow', 'gate', 'derive', 'backfill-reviews', 'register-stage-review', 'modules', 'change-rename', 'knowledge', 'platform', 'scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore', 'status', 'doctor', 'auto', 'runtime'];
|
|
3104
|
+
const topCommands = ['init', 'setup', 'run', 'progress', 'worktree', 'dispatch', 'agent-log', 'local', 'workflow', 'gate', 'derive', 'backfill-reviews', 'register-stage-review', 'modules', 'change-rename', 'change-delete', 'knowledge', 'platform', 'pull', 'scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore', 'status', 'doctor', 'auto', 'runtime'];
|
|
2941
3105
|
const suggestion = didYouMean(command, topCommands);
|
|
2942
3106
|
console.error(`❌ 未知命令: ${command}`);
|
|
2943
3107
|
if (command === '--status') {
|