kld-sdd 2.7.8 → 2.8.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/README.md +26 -0
- package/USABILITY.md +72 -0
- package/bin/kld-sdd-init.js +718 -34
- package/lib/command-bridge.js +26 -17
- package/lib/deploy-codebuddy-hooks.js +19 -26
- package/lib/deploy-strategy.js +293 -0
- package/lib/hook-gate-core.js +7 -8
- package/lib/hook-install.js +75 -0
- package/lib/init.js +507 -726
- package/lib/maintenance.js +221 -0
- package/lib/managed-hook-hashes.json +24 -0
- package/lib/settings-merge.js +102 -25
- package/lib/skills-bundle.js +5 -5
- package/lib/spec-choice.js +85 -0
- package/lib/tool-profiles.js +2 -2
- package/lib/uninstall.js +453 -0
- package/package.json +7 -4
- package/skywalk-sdd/context-client.cjs +38 -7
- package/skywalk-sdd/index.cjs +153 -27
- package/skywalk-sdd/kb-upload.cjs +224 -82
- package/skywalk-sdd/lib/check-review.cjs +41 -0
- package/skywalk-sdd/lib/shared.cjs +62 -41
- package/skywalk-sdd/lib/test-execution.cjs +130 -0
- package/skywalk-sdd/lib/usage-reporter.cjs +17 -8
- package/skywalk-sdd/ontology/archive-package.cjs +141 -0
- package/skywalk-sdd/ontology/artifact-parser.cjs +24 -3
- package/skywalk-sdd/ontology/cli.cjs +53 -0
- package/skywalk-sdd/ontology/id.cjs +1 -1
- package/skywalk-sdd/ontology/identity-index.cjs +5 -1
- package/skywalk-sdd/ontology/resolve-spec-root.cjs +49 -0
- package/skywalk-sdd/ontology/schema.cjs +6 -0
- package/skywalk-sdd/ontology/sdd-config.cjs +33 -5
- package/skywalk-sdd/ontology/spec-detect.cjs +421 -0
- package/skywalk-sdd/ontology/spec-ontology.cjs +507 -0
- package/skywalk-sdd/ontology/workspace-layout.cjs +65 -17
- package/templates/git-hooks/commit-msg +17 -85
- package/templates/git-hooks/commit-msg-runner.cjs +44 -0
- package/templates/git-hooks/commit-msg-sdd-trailer.cjs +60 -69
- package/templates/git-hooks/consistency-check-core.cjs +92 -261
- package/templates/git-hooks/delivery-check.cjs +231 -0
- package/templates/git-hooks/delivery-snapshot.cjs +181 -0
- package/templates/git-hooks/hooks.config +8 -6
- package/templates/git-hooks/pre-commit +19 -5
- package/templates/git-hooks/pre-commit-consistency-check.cjs +13 -31
- package/templates/git-hooks/pre-push +19 -5
- package/templates/git-hooks/pre-push-consistency-check.cjs +36 -83
- package/templates/hooks/claude/hooks/sdd-post-tool.cjs +2 -2
- package/templates/hooks/codebuddy/hooks/hook-gate-core.cjs +7 -8
- package/templates/hooks/codebuddy/hooks/sdd-post-tool.cjs +2 -2
- package/templates/openspec/proposal.md +1 -1
- package/templates/openspec/spec.md +8 -0
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +28 -24
- package/templates/skills/kld-sdd/opsx-apply/checklist.md +12 -3
- package/templates/skills/kld-sdd/opsx-apply/reference.md +21 -1
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +20 -18
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +18 -13
- package/templates/skills/kld-sdd/opsx-check/reference.md +3 -1
- package/templates/skills/kld-sdd/opsx-check/review-template.json +22 -0
- package/templates/skills/kld-sdd/opsx-check/reviewer.md +27 -6
- package/templates/skills/kld-sdd/opsx-consistency-check/SKILL.md +34 -112
- package/templates/skills/kld-sdd/opsx-consistency-check/scripts/scripts.cjs +15 -14
- package/templates/skills/kld-sdd/opsx-kb-config/SKILL.md +82 -61
- package/templates/skills/kld-sdd/opsx-kb-config/reference.md +8 -11
- package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +3 -2
- package/templates/skills/kld-sdd/opsx-kb-ingest/reference.md +2 -2
- package/templates/skills/kld-sdd/opsx-ontology-ingest/SKILL.md +77 -0
- package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +5 -4
- package/templates/skills/kld-sdd/opsx-ontology-query/reference.md +14 -1
- package/templates/skills/kld-sdd/opsx-propose/SKILL.md +1 -1
- package/templates/skills/kld-sdd/opsx-propose/interaction-policy.md +9 -0
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +5 -3
- package/templates/skills/kld-sdd/opsx-task/SKILL.md +2 -2
- package/index.js +0 -13
package/lib/uninstall.js
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
/** 只卸载可识别的 SDD 工具;文档、配置、数据和仓库关联始终保留。 */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const { isManagedBridgeFile } = require('./command-bridge');
|
|
8
|
+
const { PROVIDERS, stripManagedHooks } = require('./settings-merge');
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
const { execFileSync } = require('child_process');
|
|
11
|
+
const maintenance = require('./maintenance');
|
|
12
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
13
|
+
const { getToolProfile, listToolProfileIds, buildToolConfigs } = require('./tool-profiles');
|
|
14
|
+
const sddConfig = require('../skywalk-sdd/ontology/sdd-config.cjs');
|
|
15
|
+
const workspaceLayout = require('./workspace-layout');
|
|
16
|
+
// spec 候选枚举的唯一入口(含 tier 分级与排序)
|
|
17
|
+
const specDetect = require('../skywalk-sdd/ontology/spec-detect.cjs');
|
|
18
|
+
|
|
19
|
+
/** 卸载模式 */
|
|
20
|
+
const MODE = Object.freeze({
|
|
21
|
+
/** 只删工具内容,保留用户配置与运行时数据 */
|
|
22
|
+
TOOLS_ONLY: 'tools-only',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/** 操作类型(用于计划与摘要) */
|
|
26
|
+
const OP = Object.freeze({
|
|
27
|
+
DELETE_FILE: 'delete-file',
|
|
28
|
+
STRIP_HOOKS: 'strip-hooks',
|
|
29
|
+
KEEP: 'keep',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/** .gitignore 中 SDD 块的起始标记 */
|
|
33
|
+
const GITIGNORE_MARKER = 'KLD SDD';
|
|
34
|
+
|
|
35
|
+
function createPlan(root = null) {
|
|
36
|
+
return { ops: [], warnings: [], root: root ? path.resolve(root) : null };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function addOp(plan, op, target, reason) {
|
|
40
|
+
plan.ops.push({ op, target, reason });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function warn(plan, message) {
|
|
44
|
+
plan.warnings.push(message);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 列出目录下所有文件(递归),目录不存在返回空 */
|
|
48
|
+
function listFilesRecursive(dir) {
|
|
49
|
+
if (!fs.existsSync(dir)) return [];
|
|
50
|
+
const out = [];
|
|
51
|
+
for (const name of fs.readdirSync(dir)) {
|
|
52
|
+
const p = path.join(dir, name);
|
|
53
|
+
let st;
|
|
54
|
+
try {
|
|
55
|
+
st = fs.lstatSync(p);
|
|
56
|
+
} catch {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (st.isDirectory()) {
|
|
60
|
+
out.push(...listFilesRecursive(p));
|
|
61
|
+
} else {
|
|
62
|
+
out.push(p);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 按随包工具文件清单逐文件清理,未知文件和用户添加的文件不删。 */
|
|
69
|
+
function planOwnedFiles(plan, source, target, reason) {
|
|
70
|
+
for (const rel of maintenance.filesUnder(source)) {
|
|
71
|
+
// Skill 的本地检索配置可能包含连接信息,不作为工具脚本清理。
|
|
72
|
+
if (path.basename(rel) === 'config.json') continue;
|
|
73
|
+
if (maintenance.regularFileWithin(target, rel)) addOp(plan, OP.DELETE_FILE, path.join(target, rel), reason);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function planProfileArtifacts(plan, toolKey, cwd) {
|
|
78
|
+
const profile = getToolProfile(toolKey);
|
|
79
|
+
const config = buildToolConfigs()[toolKey];
|
|
80
|
+
if (!config) return;
|
|
81
|
+
if (profile.skillsDir) {
|
|
82
|
+
planOwnedFiles(plan, path.join(packageRoot, 'templates/skills/kld-sdd'), path.join(cwd, profile.skillsDir), `${config.name} SDD skill 文件`);
|
|
83
|
+
}
|
|
84
|
+
if (profile.requiresCommandBridge && profile.opsxCommandsDir) {
|
|
85
|
+
const root = path.join(cwd, profile.opsxCommandsDir);
|
|
86
|
+
for (const rel of maintenance.filesUnder(root)) {
|
|
87
|
+
if (!rel.endsWith('.md') || !maintenance.regularFileWithin(cwd, path.relative(cwd, path.join(root, rel)))) continue;
|
|
88
|
+
const file = path.join(root, rel);
|
|
89
|
+
if (isManagedBridgeFile(fs.readFileSync(file, 'utf8'))) addOp(plan, OP.DELETE_FILE, file, 'SDD command bridge');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (profile.hookProvider && profile.hookProvider !== 'none') {
|
|
93
|
+
planOwnedFiles(plan, path.join(packageRoot, 'templates/hooks', profile.hookProvider, 'hooks'), path.join(cwd, profile.configDir, 'hooks'), `${config.name} SDD hook`);
|
|
94
|
+
const relative = path.join(profile.configDir, 'settings.json');
|
|
95
|
+
if (maintenance.regularFileWithin(cwd, relative)) addOp(plan, OP.STRIP_HOOKS, path.join(cwd, relative), '仅摘除 SDD 受管 hook');
|
|
96
|
+
}
|
|
97
|
+
// rules 由用户或 Agent 编写,不因名称带 sdd/opsx 就删除。
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function planSpecPackage(plan, specRoot) {
|
|
101
|
+
if (!specRoot || !fs.existsSync(specRoot)) return;
|
|
102
|
+
for (const file of maintenance.runtimeToolFiles(path.join(specRoot, 'skywalk-sdd'))) {
|
|
103
|
+
addOp(plan, OP.DELETE_FILE, file, 'SDD 运行工具');
|
|
104
|
+
}
|
|
105
|
+
planOwnedFiles(plan, path.join(packageRoot, 'templates/openspec'), path.join(specRoot, 'openspec-templates'), 'SDD 只读模板');
|
|
106
|
+
for (const name of ['openspec', 'modules.yaml', 'sdd.config.yaml', 'kb-state.json', 'skywalk-sdd/events', 'skywalk-sdd/state', 'skywalk-sdd/project-identity.json', 'skywalk-sdd/git-hooks/hooks.config']) {
|
|
107
|
+
const target = path.join(specRoot, name);
|
|
108
|
+
if (fs.existsSync(target)) addOp(plan, OP.KEEP, target, '项目文档、配置或历史数据');
|
|
109
|
+
}
|
|
110
|
+
// 即使没有文档,也不删除 Spec 仓根目录及 .git。
|
|
111
|
+
addOp(plan, OP.KEEP, specRoot, 'Spec 仓库目录');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function planCodeRepo(plan, repoRoot) {
|
|
115
|
+
if (!repoRoot || !fs.existsSync(repoRoot)) return;
|
|
116
|
+
for (const name of ['.sdd.yaml', '.sdd-spec-root', '.sdd-workspace.yaml', '.gitignore']) {
|
|
117
|
+
const target = path.join(repoRoot, name);
|
|
118
|
+
if (fs.existsSync(target)) addOp(plan, OP.KEEP, target, '保留关联配置与隐私数据忽略规则');
|
|
119
|
+
}
|
|
120
|
+
// 与安装器一样尊重 Hook 管理器、worktree 和自定义 Hook。
|
|
121
|
+
if (!workspaceLayout.isGitRepo(repoRoot) || sddConfig.gitConfig(repoRoot, 'core.hooksPath')) return;
|
|
122
|
+
const known = require('./managed-hook-hashes.json');
|
|
123
|
+
for (const name of ['commit-msg', 'pre-commit', 'pre-push']) {
|
|
124
|
+
let target;
|
|
125
|
+
try {
|
|
126
|
+
const value = execFileSync('git', ['rev-parse', '--git-path', `hooks/${name}`], { cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
127
|
+
target = path.resolve(repoRoot, value);
|
|
128
|
+
} catch { continue; }
|
|
129
|
+
if (!target.startsWith(path.resolve(repoRoot) + path.sep)) {
|
|
130
|
+
addOp(plan, OP.KEEP, target, '其他 worktree 共用的 Git Hook;请从主仓库管理');
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const stat = fs.lstatSync(target, { throwIfNoEntry: false });
|
|
134
|
+
if (!stat || !stat.isFile() || !fs.lstatSync(path.dirname(target)).isDirectory()) continue;
|
|
135
|
+
const hash = content => crypto.createHash('sha256').update(content).digest('hex');
|
|
136
|
+
const current = hash(fs.readFileSync(target));
|
|
137
|
+
const template = hash(fs.readFileSync(path.join(packageRoot, 'templates/git-hooks', name)));
|
|
138
|
+
if (current === template || (known[name] || []).includes(current)) addOp(plan, OP.DELETE_FILE, target, '完整匹配已发布的 SDD Hook');
|
|
139
|
+
else addOp(plan, OP.KEEP, target, '自定义或未知 Hook,请自行审阅其中的 SDD 调用');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 生成卸载计划(纯计算,不落盘)
|
|
145
|
+
*
|
|
146
|
+
* @param {object} options
|
|
147
|
+
* @param {string} options.cwd 工作目录(代码仓 / 工作区根)
|
|
148
|
+
* @param {string[]} [options.tools] 要卸载的编辑器,缺省=全部
|
|
149
|
+
* @param {string} [options.mode] MODE.TOOLS_ONLY(唯一支持的方式)
|
|
150
|
+
* @param {string} [options.specRoot] 已确认的 spec 包裹包目录(CLI 交互层选定后传入);
|
|
151
|
+
* 缺省时仅按权威来源(.sdd-spec-root → git config)保守定位
|
|
152
|
+
* @returns {{ok: boolean, plan: object, specRoot: string|null, message?: string}}
|
|
153
|
+
*/
|
|
154
|
+
function buildUninstallPlan(options = {}) {
|
|
155
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
156
|
+
if (options.mode && options.mode !== MODE.TOOLS_ONLY) throw new Error('仅支持工具卸载;不允许删除 Spec 文档、配置和历史数据');
|
|
157
|
+
const mode = MODE.TOOLS_ONLY;
|
|
158
|
+
const tools = Array.isArray(options.tools) && options.tools.length
|
|
159
|
+
? options.tools
|
|
160
|
+
: listToolProfileIds();
|
|
161
|
+
|
|
162
|
+
const plan = createPlan(cwd);
|
|
163
|
+
plan.tools = tools;
|
|
164
|
+
|
|
165
|
+
// 编辑器专属产物:工作区根 / 当前目录
|
|
166
|
+
for (const toolKey of tools) {
|
|
167
|
+
planProfileArtifacts(plan, toolKey, cwd);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// spec 仓:优先用调用方(CLI 交互层)已确认的目录;
|
|
171
|
+
// 未指定时才自行定位 —— 此时仅取「权威来源」,不做全目录扫描。
|
|
172
|
+
// 定位职责已上移到 bin 层(listSpecCandidates + 用户单选 + 确认),
|
|
173
|
+
// 见 listSpecCandidatesForUninstall。本函数只接收结果,不猜。
|
|
174
|
+
const specRoot = options.specRoot
|
|
175
|
+
? path.resolve(options.specRoot)
|
|
176
|
+
: resolveSpecRootForUninstall(cwd);
|
|
177
|
+
if (specRoot) {
|
|
178
|
+
plan.specRoot = path.resolve(specRoot);
|
|
179
|
+
const relative = path.relative(cwd, specRoot);
|
|
180
|
+
const sharedExternal = relative === '..' || relative.startsWith('..' + path.sep) || path.isAbsolute(relative);
|
|
181
|
+
if (sharedExternal) {
|
|
182
|
+
addOp(plan, OP.KEEP, specRoot, '工作目录外的共享 Spec,仅卸载当前代码仓工具');
|
|
183
|
+
warn(plan, '共享 Spec 位于当前目录之外,运行工具与文档均保留;如需卸载共享工具,请从所属工作区操作');
|
|
184
|
+
} else {
|
|
185
|
+
planSpecPackage(plan, specRoot);
|
|
186
|
+
planCodeRepo(plan, specRoot);
|
|
187
|
+
}
|
|
188
|
+
// spec 仓自身的 .git/hooks(init 装的 commit-msg / pre-commit / pre-push)
|
|
189
|
+
// 是工具产物,须摘除 —— 与代码仓共用同一套受管标记判据,
|
|
190
|
+
// 非受管 hook 与 .git 其余内容一律保留。
|
|
191
|
+
} else {
|
|
192
|
+
warn(plan, '未定位到 spec 包裹包:spec 侧产物(skywalk-sdd/、openspec/ 等)未纳入计划');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 代码仓 / 工作区根
|
|
196
|
+
planCodeRepo(plan, cwd, { mode });
|
|
197
|
+
|
|
198
|
+
// 工作区模式:子代码仓也要摘 hook。
|
|
199
|
+
//
|
|
200
|
+
// 排除所有 spec 候选目录:detectWorkspaceLayout 只挑一个作为 specRepo,
|
|
201
|
+
// 其余候选(如用户复制的 `erp-sdd-specs copy`)会被误归入 codeRepos,
|
|
202
|
+
// 进而被 planCodeRepo 摘掉 .git/hooks/ —— 那不是代码仓,是 spec 仓库/备份。
|
|
203
|
+
const layout = workspaceLayout.detectWorkspaceLayout(cwd);
|
|
204
|
+
if (layout.isWorkspace) {
|
|
205
|
+
const specDirs = new Set(
|
|
206
|
+
listSpecCandidatesForUninstall(cwd).map((c) => path.resolve(c.abs)),
|
|
207
|
+
);
|
|
208
|
+
if (specRoot) specDirs.add(path.resolve(specRoot));
|
|
209
|
+
for (const repo of layout.codeRepos) {
|
|
210
|
+
if (specDirs.has(path.resolve(repo.abs))) continue;
|
|
211
|
+
if (!specRoot || maintenance.associationConflict(repo.abs, specRoot)) {
|
|
212
|
+
warn(plan, `保留代码仓 ${repo.name}:未确认属于所选 Spec`);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const linked = sddConfig.gitConfig(repo.abs, 'sdd.specPath');
|
|
216
|
+
const yaml = sddConfig.loadSddYaml(repo.abs);
|
|
217
|
+
if (!linked && !yaml.ok) continue;
|
|
218
|
+
planCodeRepo(plan, repo.abs);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 不沿中间符号链接删除文件;同一个文件仅处理一次。
|
|
223
|
+
const seen = new Set();
|
|
224
|
+
plan.ops = plan.ops.filter(item => {
|
|
225
|
+
const key = item.op + ':' + item.target;
|
|
226
|
+
if (seen.has(key)) return false;
|
|
227
|
+
seen.add(key);
|
|
228
|
+
if (item.op === OP.KEEP) return true;
|
|
229
|
+
const root = specRoot && item.target.startsWith(path.resolve(specRoot) + path.sep) ? path.resolve(specRoot) : cwd;
|
|
230
|
+
// Git worktree 的 hooks 位于 common git dir,已单独校验完整内容。
|
|
231
|
+
if (item.reason !== '完整匹配已发布的 SDD Hook' && !maintenance.regularFileWithin(root, path.relative(root, item.target))) return false;
|
|
232
|
+
item.digest = crypto.createHash('sha256').update(fs.readFileSync(item.target)).digest('hex');
|
|
233
|
+
return true;
|
|
234
|
+
});
|
|
235
|
+
return { ok: true, plan, specRoot: specRoot || null, mode };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* 枚举待卸载的 spec 包裹包候选(供 CLI 交互层让用户单选)。
|
|
240
|
+
*
|
|
241
|
+
* 返回全部候选并标注来源,由用户裁决;`erp-sdd-specs copy` 这类用户备份
|
|
242
|
+
* 会作为 'scan' 候选被列出,而不是被静默删除。
|
|
243
|
+
*
|
|
244
|
+
* 委托 `spec-detect.detectSpecCandidatesWithGit` —— 与 init 侧共用唯一入口。
|
|
245
|
+
*
|
|
246
|
+
* @param {string} cwd 工作目录
|
|
247
|
+
* @returns {Array<{abs: string, name: string, source: string, tier: number, evidence: string[]}>}
|
|
248
|
+
*/
|
|
249
|
+
function listSpecCandidatesForUninstall(cwd) {
|
|
250
|
+
return specDetect.detectSpecCandidatesWithGit(cwd || process.cwd());
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* 保守定位 spec 包裹包:仅取**权威来源**(.sdd-spec-root → git config)。
|
|
255
|
+
*
|
|
256
|
+
* 不复用 `findNestedSpecPackage` 的扫描来源 —— 判据命中不等于「当前项目的
|
|
257
|
+
* spec 仓」(用户备份同样命中)。需要扫描裁决时,由 CLI 层走
|
|
258
|
+
* `listSpecCandidatesForUninstall` + 用户单选。
|
|
259
|
+
*
|
|
260
|
+
* 实现上复用 `listSpecCandidatesForUninstall` 并筛出权威来源,而不是另写一遍
|
|
261
|
+
* hint / git config 的读取逻辑 —— 历史上这里是第二份实现,导致「卸载比 init
|
|
262
|
+
* 少认来源」(如 init 认 `.sdd.yaml mono`,卸载不认)的漂移。
|
|
263
|
+
* 候选已按 tier → 来源优先级排序,故 hint 天然优先于 git。
|
|
264
|
+
*
|
|
265
|
+
* @param {string} cwd 工作目录
|
|
266
|
+
* @returns {string|null} 权威来源指向的 spec 根;无权威来源时 null
|
|
267
|
+
*/
|
|
268
|
+
function resolveSpecRootForUninstall(cwd) {
|
|
269
|
+
const authoritative = listSpecCandidatesForUninstall(cwd)
|
|
270
|
+
.find((c) => c.source === 'hint' || c.source === 'git');
|
|
271
|
+
return authoritative ? path.resolve(authoritative.abs) : null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** 从 .gitignore 摘除 SDD 块(保留其余行) */
|
|
275
|
+
function stripGitignoreBlock(filePath) {
|
|
276
|
+
const original = fs.readFileSync(filePath, 'utf8');
|
|
277
|
+
const lines = original.split(/\r?\n/);
|
|
278
|
+
const kept = [];
|
|
279
|
+
let skipping = false;
|
|
280
|
+
let removed = 0;
|
|
281
|
+
|
|
282
|
+
for (const line of lines) {
|
|
283
|
+
const trimmed = line.trim();
|
|
284
|
+
// SDD 块由注释标题开头,遇到下一个非 SDD 注释标题或空行段落结束
|
|
285
|
+
if (/^#\s*KLD SDD/.test(trimmed)) {
|
|
286
|
+
skipping = true;
|
|
287
|
+
removed += 1;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (skipping) {
|
|
291
|
+
// 块内:注释、空行、SDD 相关忽略行
|
|
292
|
+
if (trimmed === '' || trimmed.startsWith('#')) {
|
|
293
|
+
if (trimmed.startsWith('#') && !/SDD|Telemetry|Apply|KB 绑定/.test(trimmed)) {
|
|
294
|
+
// 遇到无关注释 → 块结束
|
|
295
|
+
skipping = false;
|
|
296
|
+
kept.push(line);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
removed += 1;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (/^(\.?[\w.*/-]*)(sdd|skywalk|worktrees|kb-state)/i.test(trimmed)
|
|
303
|
+
|| /sdd/i.test(trimmed) || /skywalk/i.test(trimmed)
|
|
304
|
+
|| /^\.worktrees\//.test(trimmed) || /personal-\*$/.test(trimmed)) {
|
|
305
|
+
removed += 1;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
skipping = false;
|
|
309
|
+
kept.push(line);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
kept.push(line);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const next = kept.join('\n').replace(/\n{3,}/g, '\n\n').replace(/\s+$/, '') + '\n';
|
|
316
|
+
return { changed: removed > 0 && next !== original, next, removed };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** 从 settings.json 摘除受管 hook */
|
|
320
|
+
function stripSettingsHooks(filePath, provider) {
|
|
321
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
322
|
+
let settings;
|
|
323
|
+
try {
|
|
324
|
+
settings = JSON.parse(raw);
|
|
325
|
+
} catch {
|
|
326
|
+
return { ok: false, error: '不是合法 JSON,已跳过(请手工处理)' };
|
|
327
|
+
}
|
|
328
|
+
const stripped = stripManagedHooks(settings, provider);
|
|
329
|
+
const next = JSON.stringify(stripped, null, 2) + '\n';
|
|
330
|
+
if (next === raw) return { ok: true, changed: false };
|
|
331
|
+
fs.writeFileSync(filePath, next, 'utf8');
|
|
332
|
+
return { ok: true, changed: true };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** 判断目录是否为空 */
|
|
336
|
+
function isEmptyDir(dir) {
|
|
337
|
+
try {
|
|
338
|
+
return fs.existsSync(dir) && fs.statSync(dir).isDirectory() && fs.readdirSync(dir).length === 0;
|
|
339
|
+
} catch {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** provider 判定:从 settings.json 路径反推 */
|
|
345
|
+
function providerFromSettingsPath(settingsPath) {
|
|
346
|
+
return /\.claude[\\/]/.test(settingsPath) ? PROVIDERS.CLAUDE : PROVIDERS.CODEBUDDY;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** 执行前重新校验白名单和文件快照,拒绝旧全量计划以及预览后的变更。 */
|
|
350
|
+
function applyUninstallPlan(plan, options = {}) {
|
|
351
|
+
const log = typeof options.log === 'function' ? options.log : console.log;
|
|
352
|
+
const errors = [];
|
|
353
|
+
if (!plan || !plan.root || !Array.isArray(plan.ops)) return { ok: false, applied: 0, skipped: 0, errors: ['卸载计划无效,请重新预览'] };
|
|
354
|
+
const fresh = buildUninstallPlan({ cwd: plan.root, specRoot: plan.specRoot, tools: plan.tools }).plan;
|
|
355
|
+
const allowed = new Map(fresh.ops.filter(item => item.op !== OP.KEEP).map(item => [item.op + ':' + item.target, item]));
|
|
356
|
+
for (const item of plan.ops) {
|
|
357
|
+
if (item.op === OP.KEEP) continue;
|
|
358
|
+
const current = allowed.get(item.op + ':' + item.target);
|
|
359
|
+
if (!current || !item.digest || current.digest !== item.digest) errors.push(`${item.target}: 范围或内容已变化,请重新预览`);
|
|
360
|
+
}
|
|
361
|
+
if (errors.length) return { ok: false, applied: 0, skipped: 0, errors };
|
|
362
|
+
let applied = 0;
|
|
363
|
+
let skipped = 0;
|
|
364
|
+
for (const item of plan.ops) {
|
|
365
|
+
if (item.op === OP.KEEP) { skipped++; continue; }
|
|
366
|
+
try {
|
|
367
|
+
// 执行时再检查一次,不接受被替换为链接的文件。
|
|
368
|
+
const stat = fs.lstatSync(item.target);
|
|
369
|
+
if (!stat.isFile() || crypto.createHash('sha256').update(fs.readFileSync(item.target)).digest('hex') !== item.digest) throw new Error('文件已变化,请重新预览');
|
|
370
|
+
if (options.dryRun) { log(` 将处理 ${item.target}`); applied++; continue; }
|
|
371
|
+
if (item.op === OP.DELETE_FILE) { fs.unlinkSync(item.target); applied++; }
|
|
372
|
+
else if (item.op === OP.STRIP_HOOKS) {
|
|
373
|
+
const result = stripSettingsHooks(item.target, providerFromSettingsPath(item.target));
|
|
374
|
+
if (!result.ok) throw new Error(result.error);
|
|
375
|
+
if (result.changed) applied++; else skipped++;
|
|
376
|
+
}
|
|
377
|
+
} catch (error) { errors.push(`${item.target}: ${error.message}`); }
|
|
378
|
+
}
|
|
379
|
+
// 只收起因工具文件删除而变空的目录;根目录、Spec、数据目录保持原样。
|
|
380
|
+
if (!options.dryRun) {
|
|
381
|
+
const roots = [plan.root, plan.specRoot].filter(Boolean).map(p => path.resolve(p));
|
|
382
|
+
const kept = plan.ops.filter(i => i.op === OP.KEEP).map(i => path.resolve(i.target));
|
|
383
|
+
for (const item of plan.ops.filter(i => i.op === OP.DELETE_FILE && !i.target.includes(`${path.sep}.git${path.sep}`))) {
|
|
384
|
+
let dir = path.dirname(item.target);
|
|
385
|
+
while (roots.some(root => dir.startsWith(root + path.sep)) && !roots.includes(dir)) {
|
|
386
|
+
if (kept.some(p => p === dir || p.startsWith(dir + path.sep))) break;
|
|
387
|
+
if (!isEmptyDir(dir)) break;
|
|
388
|
+
fs.rmdirSync(dir);
|
|
389
|
+
dir = path.dirname(dir);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return { ok: errors.length === 0, applied, skipped, errors };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** 卸载方式的中文标签(供 CLI 层复用,避免文案漂移) */
|
|
397
|
+
function modeLabel() {
|
|
398
|
+
return '卸载工具(保留 Spec 文档、配置与历史数据)';
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* 卸载执行(纯业务,无交互)
|
|
403
|
+
*
|
|
404
|
+
* CLI 参数解析与全部交互(方式选择 / 预览 / 确认)都在 bin/ 层完成;
|
|
405
|
+
* 本函数只接收已解析的选项对象,不读取 process.argv,也不询问用户。
|
|
406
|
+
*
|
|
407
|
+
* @param {object} [opts]
|
|
408
|
+
* @param {string} [opts.cwd] 工作目录(代码仓 / 工作区根),缺省 process.cwd()
|
|
409
|
+
* @param {string[]} [opts.tools] 要卸载的编辑器 id 列表,缺省=全部
|
|
410
|
+
* @param {string} [opts.mode] MODE.TOOLS_ONLY(唯一支持的方式)
|
|
411
|
+
* @param {boolean} [opts.dryRun] 只打印计划,不落盘
|
|
412
|
+
* @param {object} [opts.plan] 复用调用方已算好的计划(避免重复计算)
|
|
413
|
+
* @param {boolean} [opts.showWarnings] 是否打印 plan.warnings,缺省 true
|
|
414
|
+
* @returns {{ok: boolean, applied: number, skipped: number, errors: string[]}}
|
|
415
|
+
*/
|
|
416
|
+
function main(opts = {}) {
|
|
417
|
+
const cwd = opts.cwd || process.cwd();
|
|
418
|
+
if (opts.mode && opts.mode !== MODE.TOOLS_ONLY) throw new Error('仅支持工具卸载');
|
|
419
|
+
const mode = MODE.TOOLS_ONLY;
|
|
420
|
+
|
|
421
|
+
const plan = opts.plan || buildUninstallPlan({ cwd, tools: opts.tools, mode }).plan;
|
|
422
|
+
const result = applyUninstallPlan(plan, { dryRun: Boolean(opts.dryRun) });
|
|
423
|
+
|
|
424
|
+
// 交互路径下 CLI 层已用 printPlanPreview 打过同一份 warnings,
|
|
425
|
+
// 由调用方传 showWarnings:false 避免重复刷屏;非交互路径缺省照常打印。
|
|
426
|
+
if (opts.showWarnings !== false && plan.warnings.length) {
|
|
427
|
+
console.log('');
|
|
428
|
+
for (const w of plan.warnings) console.log(`⚠️ ${w}`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
console.log('');
|
|
432
|
+
console.log(`${result.ok ? '✅ 完成' : '❌ 未完整卸载'}:处理 ${result.applied} 项,保留 ${result.skipped} 项`);
|
|
433
|
+
if (result.errors.length) {
|
|
434
|
+
console.log(`⚠️ ${result.errors.length} 项失败:`);
|
|
435
|
+
for (const e of result.errors) console.log(` - ${e}`);
|
|
436
|
+
}
|
|
437
|
+
if (opts.dryRun) {
|
|
438
|
+
console.log('');
|
|
439
|
+
console.log('ℹ️ 这是预演结果,未修改任何文件。去掉 --dry-run 执行实际卸载。');
|
|
440
|
+
}
|
|
441
|
+
return result;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
module.exports = {
|
|
445
|
+
MODE,
|
|
446
|
+
OP,
|
|
447
|
+
modeLabel,
|
|
448
|
+
main,
|
|
449
|
+
buildUninstallPlan,
|
|
450
|
+
applyUninstallPlan,
|
|
451
|
+
stripGitignoreBlock,
|
|
452
|
+
listSpecCandidatesForUninstall,
|
|
453
|
+
};
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kld-sdd",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "KLD SDD OpenSpec 项目初始化工具 - 一键部署 SDD skills",
|
|
5
|
-
"main": "
|
|
5
|
+
"main": "lib/init.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"kld-sdd": "bin/kld-sdd-init.js",
|
|
8
8
|
"kld-sdd-init": "bin/kld-sdd-init.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"bench:check": "node test/benchmark-check.cjs",
|
|
12
|
-
"test": "node --test test/kb-fact-lifecycle.test.cjs test/workflow-usability.test.cjs test/check-efficiency.test.cjs && node test/external-key.cjs && node test/change-key.cjs && node test/modules-and-sdd-config.cjs && node test/active-changes.cjs && node test/hook-layouts.cjs && node test/spec-package-init.cjs && node test/prompt-spec-path.cjs && node test/workspace-layout.cjs && node test/ontology-release-blockers.cjs && node test/ontology-semantic-core.cjs && node test/ontology-identity-versioning.cjs && node test/ontology-identity-continuity.cjs && node test/ontology-state-transaction.cjs && node test/ontology-process-concurrency.cjs && node test/ontology-observer-convergence.cjs && node test/ontology-working-runtime.cjs && node test/ontology-stage-materialization.cjs && node test/ontology-template-contract.cjs && node test/ontology-cli-archive.cjs && node test/archive-package-producer.cjs && node --test test/evidence-integrity-oracle.cjs test/evidence-integrity-wiring.cjs test/change-report-correctness.cjs test/report-metrics-correctness.cjs test/change-report-model-ui.cjs && node test/validate-skills-bundle.cjs && node test/tool-profiles.cjs && node test/settings-merge.cjs && node test/command-bridge.cjs && node test/codebuddy-hooks.cjs && node test/skill-content-contract.cjs && node test/init-agent-profiles.cjs && node test/init-report-runtime.cjs && node test/init-output-verbosity.cjs && node --test test/usage-contract.test.cjs test/user-config.test.cjs test/usage-runtime-removal.test.cjs
|
|
12
|
+
"test": "node --test test/delivery-hooks.test.cjs test/execution-friction.test.cjs test/kb-fact-lifecycle.test.cjs test/workflow-usability.test.cjs test/check-efficiency.test.cjs && node test/external-key.cjs && node test/change-key.cjs && node test/modules-and-sdd-config.cjs && node test/active-changes.cjs && node test/hook-layouts.cjs && node test/spec-package-init.cjs && node test/prompt-spec-path.cjs && node test/spec-detect.cjs && node test/workspace-layout.cjs && node test/sync-repos-spec-choice.cjs && node test/ontology-release-blockers.cjs && node test/ontology-semantic-core.cjs && node test/ontology-identity-versioning.cjs && node test/ontology-identity-continuity.cjs && node test/ontology-state-transaction.cjs && node test/ontology-process-concurrency.cjs && node test/ontology-observer-convergence.cjs && node test/ontology-working-runtime.cjs && node test/ontology-stage-materialization.cjs && node test/ontology-template-contract.cjs && node test/ontology-cli-archive.cjs && node test/spec-ontology-compatibility.cjs && node test/archive-package-producer.cjs && node --test test/evidence-integrity-oracle.cjs test/evidence-integrity-wiring.cjs test/change-report-correctness.cjs test/report-metrics-correctness.cjs test/change-report-model-ui.cjs && node test/validate-skills-bundle.cjs && node test/tool-profiles.cjs && node test/deploy-strategy.cjs && node test/uninstall.cjs && node --test test/maintenance.test.cjs && node test/settings-merge.cjs && node test/command-bridge.cjs && node test/codebuddy-hooks.cjs && node test/skill-content-contract.cjs && node test/init-agent-profiles.cjs && node test/init-report-runtime.cjs && node test/init-output-verbosity.cjs && node --test test/usage-contract.test.cjs test/user-config.test.cjs test/usage-runtime-removal.test.cjs test/runtime/usage-reporter.test.cjs test/runtime/usage-reporting.test.cjs test/runtime/git-identity.test.cjs test/runtime/kb-state-location.test.cjs && node test/runtime/metrics-v3.test.cjs && node test/runtime/fixback.test.cjs && node test/progress-package-artifact.test.cjs && node --test test/progress-package-install.e2e.cjs && node test/consistency-check-core.cjs"
|
|
13
13
|
},
|
|
14
14
|
"keywords": [
|
|
15
15
|
"kld",
|
|
@@ -46,5 +46,8 @@
|
|
|
46
46
|
"skywalk-sdd/kb-sync-identity.cjs",
|
|
47
47
|
"README.md",
|
|
48
48
|
"USABILITY.md"
|
|
49
|
-
]
|
|
49
|
+
],
|
|
50
|
+
"sddMaintenance": {
|
|
51
|
+
"toolOnly": 1
|
|
52
|
+
}
|
|
50
53
|
}
|
|
@@ -37,6 +37,9 @@ function buildEndpoint(apiBase, spaceId, kbId, mode) {
|
|
|
37
37
|
if (mode === 'resolve') {
|
|
38
38
|
return `${base}/v1/spaces/${space}/knowledge-bases/${kb}/entities/resolve`;
|
|
39
39
|
}
|
|
40
|
+
if (mode === 'spec-generation') {
|
|
41
|
+
return `${base}/v1/spaces/${space}/knowledge-bases/${kb}/context/spec-generation`;
|
|
42
|
+
}
|
|
40
43
|
return `${base}/v1/spaces/${space}/knowledge-bases/${kb}/context/match-requirement`;
|
|
41
44
|
}
|
|
42
45
|
|
|
@@ -61,7 +64,14 @@ function compactContext(data, includeCandidates, mode = 'match') {
|
|
|
61
64
|
warnings: data.warnings,
|
|
62
65
|
clarificationQuestions: data.clarificationQuestions,
|
|
63
66
|
specGenerationContext: data.specGenerationContext,
|
|
67
|
+
matchedCapabilities: data.matchedCapabilities,
|
|
68
|
+
matchedScenarios: data.matchedScenarios,
|
|
69
|
+
businessObjects: data.businessObjects,
|
|
70
|
+
rules: data.rules,
|
|
71
|
+
specPatterns: data.specPatterns,
|
|
72
|
+
compatibilityMode: data.compatibilityMode,
|
|
64
73
|
supportEvidence: data.supportEvidence,
|
|
74
|
+
candidateEvidence: data.candidateEvidence,
|
|
65
75
|
oppositionEvidence: data.oppositionEvidence,
|
|
66
76
|
};
|
|
67
77
|
// Continuity resolve 必须透出 candidates;match 默认隐藏内部候选除非 --include-candidates
|
|
@@ -171,19 +181,40 @@ async function retrieveContext(args = parseArgs(process.argv), env = process.env
|
|
|
171
181
|
return { ...unavailableContext('engineering_kb_not_configured', 'KB 尚未配置,本地工作可以继续'), configured: false };
|
|
172
182
|
}
|
|
173
183
|
|
|
174
|
-
const
|
|
184
|
+
const requestedMode = String(args.mode || 'match').trim().toLowerCase();
|
|
185
|
+
const mode = requestedMode === 'resolve'
|
|
186
|
+
? 'resolve'
|
|
187
|
+
: (['spec-generation', 'spec_generation', 'generation'].includes(requestedMode)
|
|
188
|
+
? 'spec-generation' : 'match');
|
|
175
189
|
const timeoutMs = contextTimeout(args, env, 5000);
|
|
176
190
|
const payload = buildPayload(args, mode);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
191
|
+
let responseMode = mode;
|
|
192
|
+
let data;
|
|
193
|
+
try {
|
|
194
|
+
data = await requestJson(
|
|
195
|
+
buildEndpoint(apiBase, spaceId, kbId, mode),
|
|
196
|
+
payload,
|
|
197
|
+
token,
|
|
198
|
+
timeoutMs,
|
|
199
|
+
);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
const endpointUnavailable = /Not Found|HTTP 404/i.test(error.message);
|
|
202
|
+
if (mode !== 'spec-generation' || !endpointUnavailable) throw error;
|
|
203
|
+
data = await requestJson(
|
|
204
|
+
buildEndpoint(apiBase, spaceId, kbId, 'match'),
|
|
205
|
+
payload,
|
|
206
|
+
token,
|
|
207
|
+
timeoutMs,
|
|
208
|
+
);
|
|
209
|
+
responseMode = 'match';
|
|
210
|
+
}
|
|
183
211
|
return {
|
|
184
212
|
available: true,
|
|
185
213
|
mode,
|
|
214
|
+
responseMode,
|
|
215
|
+
remoteContextVerified: true,
|
|
186
216
|
...compactContext(data, Boolean(args['include-candidates']), mode),
|
|
217
|
+
compatibilityMode: responseMode === mode ? (data.compatibilityMode || 'NATIVE') : 'LEGACY_V2',
|
|
187
218
|
};
|
|
188
219
|
}
|
|
189
220
|
|