kld-sdd 2.7.8-3 → 2.7.8-4
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/bin/kld-sdd-init.js +700 -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/init.js +429 -570
- package/lib/settings-merge.js +102 -25
- package/lib/skills-bundle.js +2 -4
- package/lib/spec-choice.js +85 -0
- package/lib/tool-profiles.js +2 -2
- package/lib/uninstall.js +764 -0
- package/package.json +3 -3
- package/skywalk-sdd/kb-upload.cjs +43 -32
- package/skywalk-sdd/lib/shared.cjs +62 -41
- package/skywalk-sdd/lib/usage-reporter.cjs +17 -8
- package/skywalk-sdd/ontology/resolve-spec-root.cjs +49 -0
- package/skywalk-sdd/ontology/sdd-config.cjs +33 -5
- package/skywalk-sdd/ontology/spec-detect.cjs +421 -0
- package/skywalk-sdd/ontology/workspace-layout.cjs +46 -16
- package/templates/hooks/codebuddy/hooks/hook-gate-core.cjs +7 -8
- package/templates/skills/kld-sdd/opsx-consistency-check/scripts/scripts.cjs +14 -0
- package/templates/skills/kld-sdd/opsx-kb-config/SKILL.md +157 -11
- package/index.js +0 -13
package/bin/kld-sdd-init.js
CHANGED
|
@@ -1,45 +1,662 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* KLD SDD
|
|
5
|
-
*
|
|
4
|
+
* KLD SDD CLI 入口
|
|
5
|
+
*
|
|
6
|
+
* 职责边界:
|
|
7
|
+
* - bin/(本文件):命令行参数解析、帮助/版本输出、交互式输入(编辑器选择)、子命令路由
|
|
8
|
+
* - lib/:纯业务逻辑,只接收解析后的选项对象,不读取 process.argv
|
|
6
9
|
*
|
|
7
10
|
* 子命令:
|
|
8
11
|
* (无参数) 运行项目初始化
|
|
9
12
|
* log SDD Telemetry CLI(记录阶段事件、查询指标)
|
|
10
13
|
* link-spec 将代码仓关联到本地 spec clone(git config sdd.specPath)
|
|
11
14
|
* sync-repos 工作目录下发现新增代码仓并轻量接入(不装 skills)
|
|
15
|
+
* uninstall 卸载 SDD 产物(渐进式:先选方式,再预览计划,最后确认执行)
|
|
12
16
|
*/
|
|
13
17
|
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
14
20
|
const path = require('path');
|
|
15
|
-
const
|
|
21
|
+
const { listToolProfileIds, buildToolConfigs } = require('../lib/tool-profiles');
|
|
22
|
+
const { MODE, OP, modeLabel } = require('../lib/uninstall');
|
|
23
|
+
const { specSourceLabel } = require('../skywalk-sdd/ontology/spec-detect.cjs');
|
|
24
|
+
// spec 候选单选交互(唯一实现,与 lib/init.js 共用)
|
|
25
|
+
const { selectSpecRoot: selectSpecChoice } = require('../lib/spec-choice');
|
|
16
26
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
const args = cliArgs();
|
|
28
|
+
const command = args[0];
|
|
29
|
+
|
|
30
|
+
/** 统一的失败出口:stderr + 非零退出码 */
|
|
31
|
+
function fail(message) {
|
|
32
|
+
console.error(`❌ ${message}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ═══════════════════════════════════════════════════════════════
|
|
37
|
+
// CLI 输入层:参数解析 / 交互输入 / 帮助输出
|
|
38
|
+
// 只做「字符串 → 选项对象」的转换,不落盘、不执行业务。
|
|
39
|
+
// ═══════════════════════════════════════════════════════════════
|
|
40
|
+
|
|
41
|
+
/** verbose 环境变量名(与 --verbose 等价) */
|
|
42
|
+
const VERBOSE_ENV = 'KLD_SDD_VERBOSE';
|
|
43
|
+
|
|
44
|
+
/** 取子命令及其后的参数(去掉 node 与脚本路径) */
|
|
45
|
+
function cliArgs(argv = process.argv) {
|
|
46
|
+
return argv.slice(2);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function hasFlag(argv, ...names) {
|
|
50
|
+
return names.some((name) => argv.includes(name));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 输出详细度判定
|
|
55
|
+
* 触发条件:argv 含 --verbose,或 KLD_SDD_VERBOSE 为 1/true(大小写不敏感)
|
|
56
|
+
* 注意:-v 已被 --version 占用,不复用
|
|
57
|
+
*/
|
|
58
|
+
function isVerbose(argv, env) {
|
|
59
|
+
const list = Array.isArray(argv) ? argv : cliArgs();
|
|
60
|
+
const envVars = env && typeof env === 'object' ? env : process.env;
|
|
61
|
+
if (list.includes('--verbose')) return true;
|
|
62
|
+
const value = envVars[VERBOSE_ENV];
|
|
63
|
+
return typeof value === 'string' && /^(1|true)$/i.test(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 读取 --tool / --tool= 的原始值;未出现返回 null */
|
|
67
|
+
function readToolArg(argv) {
|
|
68
|
+
const index = argv.findIndex((arg) => arg === '--tool' || arg.startsWith('--tool='));
|
|
69
|
+
if (index === -1) return null;
|
|
70
|
+
return argv[index] === '--tool' ? argv[index + 1] : argv[index].slice('--tool='.length);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 解析 --tool,返回编辑器 id 数组;未指定返回 null
|
|
75
|
+
* 支持:--tool codex、--tool=codex、--tool cursor,codex、--tool all
|
|
76
|
+
*/
|
|
77
|
+
function parseToolOption(argv) {
|
|
78
|
+
const rawValue = readToolArg(argv);
|
|
79
|
+
if (rawValue === null) return null;
|
|
80
|
+
if (!rawValue) {
|
|
81
|
+
throw new Error('缺少 --tool 参数值,请指定编辑器名称');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const allTools = listToolProfileIds();
|
|
85
|
+
const normalized = String(rawValue).trim().toLowerCase();
|
|
86
|
+
if (normalized === 'all' || normalized === '全部') {
|
|
87
|
+
return allTools;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const selected = normalized.split(',').map((item) => item.trim()).filter(Boolean);
|
|
91
|
+
const invalid = selected.filter((tool) => !allTools.includes(tool));
|
|
92
|
+
if (invalid.length > 0) {
|
|
93
|
+
throw new Error(`不支持的编辑器: ${invalid.join(', ')}。可用值: ${allTools.join(', ')}, all`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return selected;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 解析 --spec-path(相对/绝对路径字符串);未指定返回 null。
|
|
101
|
+
* 路径语义校验(必须落在当前目录内)属业务逻辑,由 lib 完成。
|
|
102
|
+
*/
|
|
103
|
+
function parseSpecPathOption(argv) {
|
|
104
|
+
const index = argv.findIndex(
|
|
105
|
+
(arg) => arg === '--spec-path' || arg.startsWith('--spec-path='),
|
|
106
|
+
);
|
|
107
|
+
if (index === -1) return null;
|
|
108
|
+
|
|
109
|
+
const rawValue = argv[index] === '--spec-path'
|
|
110
|
+
? argv[index + 1]
|
|
111
|
+
: argv[index].slice('--spec-path='.length);
|
|
112
|
+
|
|
113
|
+
if (!rawValue || rawValue.startsWith('--')) {
|
|
114
|
+
throw new Error('缺少 --spec-path 参数值,请指定 spec 仓库目录(相对或绝对路径)');
|
|
115
|
+
}
|
|
116
|
+
return rawValue.trim();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** kld-sdd [选项] */
|
|
120
|
+
function parseInitArgs(argv = cliArgs(), env = process.env) {
|
|
121
|
+
const tools = parseToolOption(argv);
|
|
122
|
+
return {
|
|
123
|
+
help: hasFlag(argv, '-h', '--help'),
|
|
124
|
+
version: hasFlag(argv, '-v', '--version'),
|
|
125
|
+
verbose: isVerbose(argv, env),
|
|
126
|
+
skipOpenspec: argv.includes('--skip-openspec'),
|
|
127
|
+
skipTemplate: argv.includes('--skip-template'),
|
|
128
|
+
tools,
|
|
129
|
+
// 显式给了 --tool 即自动化场景:lib 据此抑制 spec 路径的交互询问
|
|
130
|
+
hasToolArg: tools !== null,
|
|
131
|
+
specPath: parseSpecPathOption(argv),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* kld-sdd uninstall [选项]
|
|
137
|
+
*
|
|
138
|
+
* 卸载产物与编辑器无关(8 个编辑器的产物全量扫描),工作目录固定为当前目录,
|
|
139
|
+
* 故不提供 --tool / --project:卸载范围由「方式选择」决定,目录由 cwd 决定。
|
|
140
|
+
*
|
|
141
|
+
* --spec-path 用于无人值守场景:多候选(如用户复制的备份)时无法自动判定,
|
|
142
|
+
* 需显式指定要卸载的 spec 仓库。
|
|
143
|
+
*/
|
|
144
|
+
function parseUninstallArgs(argv = cliArgs()) {
|
|
145
|
+
const purge = argv.includes('--purge');
|
|
146
|
+
return {
|
|
147
|
+
cwd: process.cwd(),
|
|
148
|
+
specPath: parseSpecPathOption(argv),
|
|
149
|
+
mode: purge ? MODE.FULL : MODE.TOOLS_ONLY,
|
|
150
|
+
// 是否显式指定了方式:未指定且未跳过交互时,进入渐进式方式选择
|
|
151
|
+
modeExplicit: purge,
|
|
152
|
+
dryRun: argv.includes('--dry-run'),
|
|
153
|
+
yes: hasFlag(argv, '--yes', '-y'),
|
|
154
|
+
help: hasFlag(argv, '-h', '--help'),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* kld-sdd sync-repos [选项]
|
|
160
|
+
*
|
|
161
|
+
* --spec-path 用于无人值守场景:多候选(如用户复制的备份)时无法自动判定,
|
|
162
|
+
* 需显式指定要接入的 spec 仓库(与 uninstall 同名参数语义一致)。
|
|
163
|
+
*/
|
|
164
|
+
function parseSyncReposArgs(argv = cliArgs()) {
|
|
165
|
+
const projectArg = argv.find((item) => item.startsWith('--project='));
|
|
166
|
+
return {
|
|
167
|
+
workspaceRoot: path.resolve(projectArg ? projectArg.slice('--project='.length) : '.'),
|
|
168
|
+
onlyNew: !argv.includes('--all'),
|
|
169
|
+
specPath: parseSpecPathOption(argv),
|
|
170
|
+
yes: hasFlag(argv, '--yes', '-y'),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** kld-sdd link-spec [选项] */
|
|
175
|
+
function parseLinkSpecArgs(argv = cliArgs()) {
|
|
176
|
+
const pathArg = argv.find((item) => item.startsWith('--path='));
|
|
177
|
+
const projectArg = argv.find((item) => item.startsWith('--project='));
|
|
178
|
+
const positional = argv.slice(1).find((item) => !item.startsWith('--'));
|
|
25
179
|
const specPath = pathArg ? pathArg.slice('--path='.length) : positional;
|
|
26
180
|
const projectRoot = path.resolve(projectArg ? projectArg.slice('--project='.length) : '.');
|
|
27
181
|
if (!specPath) {
|
|
28
|
-
|
|
29
|
-
process.exit(1);
|
|
182
|
+
throw new Error('用法: kld-sdd link-spec --path=<spec-clone> [--project=.]');
|
|
30
183
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
184
|
+
return { specPath, projectRoot };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** 编辑器交互选择菜单映射(1-9) */
|
|
188
|
+
const EDITOR_CHOICES = Object.freeze({
|
|
189
|
+
'1': ['cursor'],
|
|
190
|
+
'2': ['claude'],
|
|
191
|
+
'3': ['codebuddy'],
|
|
192
|
+
'4': ['qoder'],
|
|
193
|
+
'5': ['opencode'],
|
|
194
|
+
'6': ['kunlunzhima'],
|
|
195
|
+
'7': ['workbuddy'],
|
|
196
|
+
'8': ['codex'],
|
|
197
|
+
'9': listToolProfileIds(),
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 让用户选择编辑器(交互式 CLI 输入,非参数解析)
|
|
202
|
+
* 无效输入时重新询问,不允许默认
|
|
203
|
+
*/
|
|
204
|
+
async function selectEditor(options = {}) {
|
|
205
|
+
const toolConfigs = buildToolConfigs();
|
|
206
|
+
const readline = require('readline');
|
|
207
|
+
const rl = readline.createInterface({
|
|
208
|
+
input: options.input || process.stdin,
|
|
209
|
+
output: options.output || process.stdout,
|
|
210
|
+
});
|
|
211
|
+
const ask = (question) => new Promise(
|
|
212
|
+
(resolve) => rl.question(question, (answer) => resolve(String(answer).trim())),
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
while (true) {
|
|
217
|
+
console.log('\n请选择您使用的 AI 编辑器:');
|
|
218
|
+
console.log(' 1. Cursor');
|
|
219
|
+
console.log(' 2. Claude Code');
|
|
220
|
+
console.log(' 3. CodeBuddy');
|
|
221
|
+
console.log(' 4. Qoder');
|
|
222
|
+
console.log(' 5. OpenCode');
|
|
223
|
+
console.log(' 6. KunlunZhima');
|
|
224
|
+
console.log(' 7. WorkBuddy');
|
|
225
|
+
console.log(' 8. Codex');
|
|
226
|
+
console.log(' 9. 全部(为所有编辑器生成 skills)');
|
|
227
|
+
|
|
228
|
+
const answer = await ask('请输入选项 (1-9): ');
|
|
229
|
+
const selected = EDITOR_CHOICES[answer];
|
|
230
|
+
|
|
231
|
+
if (!selected) {
|
|
232
|
+
console.log(`❌ 无效选项 "${answer}",请输入 1-9 之间的数字\n`);
|
|
233
|
+
continue; // 重新循环询问
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const names = selected.map((k) => toolConfigs[k].name).join(', ');
|
|
237
|
+
console.log(`✓ 已选择: ${names}\n`);
|
|
238
|
+
return selected.slice();
|
|
239
|
+
}
|
|
240
|
+
} finally {
|
|
241
|
+
rl.close();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 打开 readline 交互会话,返回 { ask, close }
|
|
247
|
+
* 卸载的「方式选择 / 计划预览 / 确认」全部在本文件完成(CLI 输入层职责)。
|
|
248
|
+
*/
|
|
249
|
+
function openPrompt(options = {}) {
|
|
250
|
+
const readline = require('readline');
|
|
251
|
+
const rl = readline.createInterface({
|
|
252
|
+
input: options.input || process.stdin,
|
|
253
|
+
output: options.output || process.stdout,
|
|
254
|
+
});
|
|
255
|
+
return {
|
|
256
|
+
ask: (question) => new Promise(
|
|
257
|
+
(resolve) => rl.question(question, (answer) => resolve(String(answer).trim())),
|
|
258
|
+
),
|
|
259
|
+
close: () => rl.close(),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** 打印卸载头部信息 */
|
|
264
|
+
function printUninstallHeader(cwd, specRoot, mode, dryRun) {
|
|
265
|
+
console.log('');
|
|
266
|
+
console.log(`🗑 KLD SDD 卸载 · ${modeLabel(mode)}`);
|
|
267
|
+
console.log(` 工作目录: ${cwd}`);
|
|
268
|
+
if (specRoot) console.log(` Spec 包裹包: ${specRoot}`);
|
|
269
|
+
if (dryRun) console.log(' 模式: 预演(不落盘)');
|
|
270
|
+
console.log('');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 打印计划预览:按操作类型分组,避免逐条刷屏。
|
|
275
|
+
* 每类最多列 5 条,其余折叠为计数;KEEP 项只列文件名与原因。
|
|
276
|
+
*/
|
|
277
|
+
function printPlanPreview(plan) {
|
|
278
|
+
const dels = plan.ops.filter((o) => o.op !== OP.KEEP);
|
|
279
|
+
const keeps = plan.ops.filter((o) => o.op === OP.KEEP);
|
|
280
|
+
|
|
281
|
+
const OP_LABELS = {
|
|
282
|
+
[OP.DELETE_DIR]: '删除目录',
|
|
283
|
+
[OP.DELETE_FILE]: '删除文件',
|
|
284
|
+
[OP.STRIP_HOOKS]: '摘除受管 hook(文件其余配置保留)',
|
|
285
|
+
[OP.STRIP_GITIGNORE]: '摘除 .gitignore 的 SDD 块(其余行保留)',
|
|
286
|
+
[OP.UNLINK_GIT_CONFIG]: '清除 git config sdd.specPath',
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const byOp = new Map();
|
|
290
|
+
for (const item of dels) {
|
|
291
|
+
if (!byOp.has(item.op)) byOp.set(item.op, []);
|
|
292
|
+
byOp.get(item.op).push(item);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
console.log(`📋 计划预览:待处理 ${dels.length} 项,保留 ${keeps.length} 项`);
|
|
296
|
+
console.log('');
|
|
297
|
+
for (const [op, items] of byOp) {
|
|
298
|
+
console.log(` ${OP_LABELS[op] || op}(${items.length} 项):`);
|
|
299
|
+
for (const item of items.slice(0, 5)) console.log(` - ${item.target}`);
|
|
300
|
+
if (items.length > 5) console.log(` … 其余 ${items.length - 5} 项`);
|
|
301
|
+
console.log('');
|
|
302
|
+
}
|
|
303
|
+
if (keeps.length) {
|
|
304
|
+
console.log(` ✅ 保留(${keeps.length} 项):`);
|
|
305
|
+
for (const item of keeps) console.log(` - ${path.basename(item.target)}(${item.reason})`);
|
|
306
|
+
console.log('');
|
|
307
|
+
}
|
|
308
|
+
for (const w of plan.warnings) console.log(` ⚠️ ${w}`);
|
|
309
|
+
if (plan.warnings.length) console.log('');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* 选择 spec 包裹包(单选)。
|
|
314
|
+
*
|
|
315
|
+
* 实现已收敛到 `lib/spec-choice.js#selectSpecRoot`(init / uninstall /
|
|
316
|
+
* sync-repos 三个场景共用的唯一交互实现)。此处仅作转发,保持 bin 层的
|
|
317
|
+
* 导出面不变(既有测试与 `runUninstall` 依赖)。
|
|
318
|
+
*
|
|
319
|
+
* @param {{ask: Function}} prompt readline 会话
|
|
320
|
+
* @param {Array<{abs: string, name: string, source: string, tier?: number}>} candidates
|
|
321
|
+
* @param {{actionLabel?: string}} [options] actionLabel 用于提问文案(默认「卸载」)
|
|
322
|
+
* @returns {Promise<string|null|undefined>} 选中的绝对路径;null=用户取消;undefined=无候选
|
|
323
|
+
*/
|
|
324
|
+
async function selectSpecRoot(prompt, candidates, options = {}) {
|
|
325
|
+
return selectSpecChoice(prompt, candidates, options);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* 渐进式选择卸载方式。
|
|
330
|
+
* 默认「1 卸载工具内容」为保守选项,回车即安全;无效输入重新询问。
|
|
331
|
+
*
|
|
332
|
+
* @returns {Promise<string|null>} MODE 之一;null 表示用户取消
|
|
333
|
+
*/
|
|
334
|
+
async function selectUninstallMode(prompt) {
|
|
335
|
+
while (true) {
|
|
336
|
+
// 选项文本并入 question:readline 重绘提示行会发 \x1b[0J(清除光标之后内容),
|
|
337
|
+
// 提问前的 console.log 会被抹掉,导致用户看不到选项。
|
|
338
|
+
const question = [
|
|
339
|
+
'请选择卸载方式:',
|
|
340
|
+
' 1. 卸载工具内容(默认,保留用户配置与运行时数据)',
|
|
341
|
+
' 删除 编辑器 skills / hook 脚本 / skywalk-sdd 工具脚本 / 文档模版',
|
|
342
|
+
' 保留 modules.yaml、sdd.config.yaml、.sdd.yaml、events/、state/',
|
|
343
|
+
' 2. 全量卸载(含用户配置、运行时数据与文档工作区)',
|
|
344
|
+
' 在「1」的基础上,额外删除上述用户配置、历史度量数据与 openspec/ 整目录',
|
|
345
|
+
' ⚠️ openspec/ 内含 changes/ 变更提案与 specs/ 业务规格文档,将一并删除',
|
|
346
|
+
' 0. 取消',
|
|
347
|
+
'\n请输入选项 (0-2,直接回车=1): ',
|
|
348
|
+
].join('\n');
|
|
349
|
+
|
|
350
|
+
const answer = await prompt.ask(question);
|
|
351
|
+
if (answer === '' || answer === '1') return MODE.TOOLS_ONLY;
|
|
352
|
+
if (answer === '2') return MODE.FULL;
|
|
353
|
+
if (answer === '0' || /^(q|quit|exit)$/i.test(answer)) return null;
|
|
354
|
+
console.log(`❌ 无效选项 "${answer}",请输入 0-2 之间的数字\n`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** 显示卸载帮助信息 */
|
|
359
|
+
function showUninstallHelp() {
|
|
360
|
+
console.log(`
|
|
361
|
+
KLD SDD 卸载工具
|
|
362
|
+
|
|
363
|
+
用法:
|
|
364
|
+
kld-sdd uninstall # 渐进式交互(推荐):选方式 → 预览计划 → 确认
|
|
365
|
+
kld-sdd uninstall [选项] # 非交互:显式给出方式与确认
|
|
366
|
+
|
|
367
|
+
选项:
|
|
368
|
+
-h, --help 显示本帮助
|
|
369
|
+
--purge 全量卸载(含用户配置与运行时数据);不指定则只卸载工具内容
|
|
370
|
+
--dry-run 预演,打印计划预览但不落盘(不进入交互)
|
|
371
|
+
--spec-path <dir> 显式指定要卸载的 spec 仓库(无人值守时多候选必须指定)
|
|
372
|
+
-y, --yes 跳过最后的执行确认(CI / 脚本用)
|
|
373
|
+
|
|
374
|
+
说明:
|
|
375
|
+
卸载在「当前目录」执行,产物范围为全部 8 个编辑器(cursor, claude, codebuddy,
|
|
376
|
+
qoder, opencode, kunlunzhima, workbuddy, codex),无需也无法指定编辑器。
|
|
377
|
+
卸载范围由方式选择(工具内容 / 全量)决定。需在目标项目目录下执行本命令。
|
|
378
|
+
|
|
379
|
+
交互流程:spec 仓库选择 → 卸载方式选择 → 计划预览 → 确认执行。
|
|
380
|
+
若检测到多个 spec 仓库(例如您手动复制了一份备份),会列出候选让您单选;
|
|
381
|
+
无人值守(--yes / 无 TTY)时多候选会直接报错,请用 --spec-path 指定。
|
|
382
|
+
|
|
383
|
+
两种方式的区别:
|
|
384
|
+
工具内容卸载(默认):
|
|
385
|
+
删除 编辑器 skills(opsx-* / tdd-* / openspec-*)、sdd-*.cjs hook 脚本、
|
|
386
|
+
hook-gate-core.cjs、skywalk-sdd/ 工具脚本、openspec-templates/、操作手册
|
|
387
|
+
保留 settings.json 文件本身(仅摘除 SDD 受管 hook)、modules.yaml、
|
|
388
|
+
sdd.config.yaml、.sdd.yaml、.sdd-spec-root、skywalk-sdd/events/ 与 state/、
|
|
389
|
+
openspec/ 文档工作区
|
|
390
|
+
全量卸载(--purge):
|
|
391
|
+
在「工具内容卸载」基础上,额外删除上述保留项中的用户配置、历史度量数据,
|
|
392
|
+
以及 openspec/ 整目录(含 changes/ 变更提案、specs/ 业务规格与 overview.md、
|
|
393
|
+
config.yaml)。执行前会打印待删目录与文件数,需二次确认。
|
|
394
|
+
|
|
395
|
+
示例:
|
|
396
|
+
kld-sdd uninstall # 交互式,最常用(在项目目录下执行)
|
|
397
|
+
kld-sdd uninstall --dry-run # 只看计划
|
|
398
|
+
kld-sdd uninstall --purge --dry-run # 全量预演
|
|
399
|
+
kld-sdd uninstall --purge --yes # 全量执行,不询问
|
|
400
|
+
`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** 显示初始化帮助信息 */
|
|
404
|
+
function showInitHelp() {
|
|
405
|
+
console.log(`
|
|
406
|
+
KLD SDD 项目初始化工具
|
|
407
|
+
|
|
408
|
+
用法:
|
|
409
|
+
kld-sdd-init [选项]
|
|
410
|
+
npx kld-sdd [选项]
|
|
411
|
+
kld-sdd sync-repos # 工作目录下发现新增代码仓并轻量接入(多 spec 候选时单选)
|
|
412
|
+
kld-sdd uninstall [选项] # 卸载 SDD 产物(渐进式:选方式 → 预览 → 确认)
|
|
413
|
+
kld-sdd uninstall --help # 查看卸载选项与两种方式的区别
|
|
414
|
+
|
|
415
|
+
选项:
|
|
416
|
+
-h, --help 显示帮助信息
|
|
417
|
+
-v, --version 显示版本号
|
|
418
|
+
--verbose 输出每个安装文件/目录的明细日志(默认仅摘要,亦可用 KLD_SDD_VERBOSE=1)
|
|
419
|
+
--skip-openspec 跳过 openspec init 步骤
|
|
420
|
+
--skip-template 跳过复制内置模版
|
|
421
|
+
--tool <name> 指定编辑器,可用值: cursor, claude, codebuddy, qoder, opencode, kunlunzhima, workbuddy, codex, all
|
|
422
|
+
--spec-path <dir> 指定 spec 包裹包目录(必须是当前目录的子目录)。
|
|
423
|
+
不指定时(交互终端):自动检测已有 spec 目录(目录名 *-sdd-specs,
|
|
424
|
+
或 modules.yaml + sdd.config.yaml + openspec/changes + openspec/specs/overview.md 齐全),
|
|
425
|
+
检测到则询问是否使用;未检测到则要求输入相对路径(回车用默认 <当前目录名>-sdd-specs)。
|
|
426
|
+
例: 在 erp-klny-all 下 --spec-path erp-sdd-specs
|
|
427
|
+
|
|
428
|
+
工作区模式(在个人工作目录执行):
|
|
429
|
+
- skills / 编辑器配置只部署一次到当前工作目录
|
|
430
|
+
- 自动发现子目录 Git 仓库
|
|
431
|
+
- spec 仓:modules.yaml / openspec 模板 / 文档门禁
|
|
432
|
+
- 代码仓:仅 commit-msg Hook + .sdd.yaml + link-spec(不装 skills)
|
|
433
|
+
- Trailer 数据仍在代码 commit 时由 Hook 写入
|
|
434
|
+
|
|
435
|
+
示例:
|
|
436
|
+
kld-sdd-init # 完整初始化流程
|
|
437
|
+
kld-sdd-init --skip-openspec # 跳过 openspec,直接部署 skills
|
|
438
|
+
kld-sdd-init --tool codex # 仅部署 Codex 配置
|
|
439
|
+
kld-sdd-init --spec-path erp-sdd-specs # 指定 spec 目录(当前目录的子目录)
|
|
440
|
+
kld-sdd sync-repos # 新代码仓加入后同步接入
|
|
441
|
+
`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ═══════════════════════════════════════════════════════════════
|
|
445
|
+
// 子命令路由:解析参数后调用 lib/ 业务实现
|
|
446
|
+
// ═══════════════════════════════════════════════════════════════
|
|
447
|
+
|
|
448
|
+
async function runInit() {
|
|
449
|
+
let parsed;
|
|
450
|
+
try {
|
|
451
|
+
parsed = parseInitArgs(args);
|
|
452
|
+
} catch (error) {
|
|
453
|
+
fail(error.message);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (parsed.help) {
|
|
458
|
+
showInitHelp();
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (parsed.version) {
|
|
463
|
+
console.log(require('../package.json').version);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const init = require('../lib/init');
|
|
468
|
+
init.setVerbose(parsed.verbose);
|
|
469
|
+
|
|
470
|
+
try {
|
|
471
|
+
await init.main({
|
|
472
|
+
tools: parsed.tools,
|
|
473
|
+
selectTools: selectEditor,
|
|
474
|
+
hasToolArg: parsed.hasToolArg,
|
|
475
|
+
specPath: parsed.specPath,
|
|
476
|
+
skipOpenspec: parsed.skipOpenspec,
|
|
477
|
+
skipTemplate: parsed.skipTemplate,
|
|
478
|
+
});
|
|
479
|
+
} catch (error) {
|
|
480
|
+
fail(`初始化失败: ${error.message}`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* 卸载(渐进式):方式选择 → 计划预览 → 确认执行。
|
|
486
|
+
*
|
|
487
|
+
* 交互编排全部在本函数(CLI 输入层);lib/uninstall 只提供纯计算与执行:
|
|
488
|
+
* - buildUninstallPlan 纯计算计划,不落盘
|
|
489
|
+
* - main 按给定计划执行,无交互
|
|
490
|
+
*
|
|
491
|
+
* 非交互场景(管道 / CI / --dry-run / --yes)自动退化为直接执行。
|
|
492
|
+
*/
|
|
493
|
+
async function runUninstall(argv = args, deps = {}) {
|
|
494
|
+
let opts;
|
|
495
|
+
try {
|
|
496
|
+
opts = parseUninstallArgs(argv);
|
|
497
|
+
} catch (error) {
|
|
498
|
+
fail(error.message);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (opts.help) {
|
|
503
|
+
showUninstallHelp();
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const uninstall = require('../lib/uninstall');
|
|
508
|
+
const { buildUninstallPlan, main: runUninstallPlan, listSpecCandidatesForUninstall } = uninstall;
|
|
509
|
+
// 工作目录固定为 cwd;deps.cwd 仅供测试注入(避免测试改全局 cwd)
|
|
510
|
+
const cwd = deps.cwd ? path.resolve(deps.cwd) : opts.cwd;
|
|
511
|
+
const { dryRun } = opts;
|
|
512
|
+
let mode = opts.mode;
|
|
513
|
+
|
|
514
|
+
// 交互依赖可注入(测试用):默认走真实 readline 与 process.stdin.isTTY
|
|
515
|
+
const promptFactory = deps.openPrompt || openPrompt;
|
|
516
|
+
const isTTY = deps.isTTY !== undefined ? Boolean(deps.isTTY) : Boolean(process.stdin.isTTY);
|
|
517
|
+
|
|
518
|
+
// 交互能力判定:
|
|
519
|
+
// - --dry-run 语义是「只看不改」,不弹菜单
|
|
520
|
+
// - --yes 表示无人值守,跳过全部询问
|
|
521
|
+
// - 无 TTY(管道 / CI)同样跳过询问
|
|
522
|
+
const canPrompt = isTTY && !dryRun && !opts.yes;
|
|
523
|
+
|
|
524
|
+
// 候选枚举:结构判据无法区分「当前项目的 spec 仓」与「用户复制的备份」
|
|
525
|
+
//(如 `erp-sdd-specs copy` 结构标记齐全),故由用户单选裁决。
|
|
526
|
+
// 显式 --spec-path 时跳过枚举(调用方已指定)。
|
|
527
|
+
let specRoot = opts.specPath ? path.resolve(opts.specPath) : undefined;
|
|
528
|
+
const candidates = specRoot ? [] : listSpecCandidatesForUninstall(cwd);
|
|
529
|
+
|
|
530
|
+
if (!specRoot && candidates.length > 1 && !canPrompt) {
|
|
531
|
+
// 无人值守 + 多候选:不猜,报错要求显式指定(猜错 = 不可逆数据丢失)。
|
|
532
|
+
// 抛错而非 process.exit:由 route() 统一转成非零退出码,测试也可断言。
|
|
533
|
+
const lines = [
|
|
534
|
+
`❌ 检测到 ${candidates.length} 个 spec 仓库,无法自动判定要卸载哪一个:`,
|
|
535
|
+
...candidates.map((c) => ` - ${c.abs}`),
|
|
536
|
+
' 请用 --spec-path=<dir> 显式指定,或在交互终端中运行以手动选择。',
|
|
537
|
+
];
|
|
538
|
+
for (const line of lines) console.error(line);
|
|
539
|
+
throw new Error('多个 spec 仓库候选,需 --spec-path 显式指定');
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const prompt = canPrompt ? promptFactory() : null;
|
|
543
|
+
|
|
544
|
+
// 步骤 1:spec 仓库选择(多候选 → 用户单选;单候选 → 确认)
|
|
545
|
+
// 必须在生成计划之前完成,保证预览列出的是用户真正要卸载的那一个。
|
|
546
|
+
try {
|
|
547
|
+
if (prompt && !specRoot) {
|
|
548
|
+
const chosenSpec = await selectSpecRoot(prompt, candidates);
|
|
549
|
+
if (chosenSpec === null) {
|
|
550
|
+
console.log('已取消');
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (chosenSpec) specRoot = chosenSpec;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// 步骤 2:生成计划(纯计算,用于空产物判定与预览)
|
|
557
|
+
let planned = buildUninstallPlan({ cwd, mode, specRoot });
|
|
558
|
+
|
|
559
|
+
if (planned.plan.ops.length === 0) {
|
|
560
|
+
printUninstallHeader(cwd, planned.specRoot, mode, dryRun);
|
|
561
|
+
console.log('ℹ️ 未发现需要处理的 SDD 产物(可能未初始化或已卸载)');
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (!prompt) {
|
|
566
|
+
printUninstallHeader(cwd, planned.specRoot, mode, dryRun);
|
|
567
|
+
if (dryRun) printPlanPreview(planned.plan);
|
|
568
|
+
// 已打印过预览(含警告)时不重复;直接执行时由 lib 打印
|
|
569
|
+
runUninstallPlan({ cwd, mode, dryRun, plan: planned.plan, showWarnings: !dryRun });
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// 步骤 3:方式选择(已显式 --purge 则跳过)
|
|
574
|
+
if (!opts.modeExplicit) {
|
|
575
|
+
const chosen = await selectUninstallMode(prompt);
|
|
576
|
+
if (chosen === null) {
|
|
577
|
+
console.log('已取消');
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (chosen !== mode) {
|
|
581
|
+
mode = chosen;
|
|
582
|
+
planned = buildUninstallPlan({ cwd, mode, specRoot });
|
|
583
|
+
if (planned.plan.ops.length === 0) {
|
|
584
|
+
printUninstallHeader(cwd, planned.specRoot, mode, dryRun);
|
|
585
|
+
console.log('ℹ️ 未发现需要处理的 SDD 产物(可能未初始化或已卸载)');
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// 步骤 4:计划预览
|
|
592
|
+
printUninstallHeader(cwd, planned.specRoot, mode, dryRun);
|
|
593
|
+
printPlanPreview(planned.plan);
|
|
594
|
+
|
|
595
|
+
// 步骤 5:确认执行
|
|
596
|
+
const dels = planned.plan.ops.filter((o) => o.op !== OP.KEEP).length;
|
|
597
|
+
const answer = await prompt.ask(`将处理 ${dels} 项,此操作不可撤销。确认执行? (y/N): `);
|
|
598
|
+
if (!/^y(es)?$/i.test(answer)) {
|
|
599
|
+
console.log('已取消');
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
console.log('');
|
|
603
|
+
|
|
604
|
+
// 交互路径:警告已由 printPlanPreview 打印过,不再重复
|
|
605
|
+
runUninstallPlan({ cwd, mode, dryRun, plan: planned.plan, showWarnings: false });
|
|
606
|
+
} finally {
|
|
607
|
+
if (prompt) prompt.close();
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* sync-repos:工作区新增代码仓后轻量接入。
|
|
613
|
+
*
|
|
614
|
+
* spec 定位与 init / uninstall 同契约:多候选时让用户单选,**不静默挑一个**。
|
|
615
|
+
* 这里尤其重要 —— 本命令会把 spec 根路径写进每个代码仓的
|
|
616
|
+
* `git config sdd.specPath`,挑错等于污染全部代码仓的关联关系。
|
|
617
|
+
*
|
|
618
|
+
* 交互能力判定与 uninstall 一致:无 TTY / 非交互时不询问;
|
|
619
|
+
* 此时多候选直接报错,要求用 --spec-path 显式指定。
|
|
620
|
+
*/
|
|
621
|
+
async function runSyncRepos() {
|
|
622
|
+
const { syncWorkspaceRepos, listSpecCandidatesForInit } = require('../lib/init');
|
|
623
|
+
const opts = parseSyncReposArgs(args);
|
|
624
|
+
|
|
625
|
+
let specRoot = opts.specPath ? path.resolve(opts.specPath) : undefined;
|
|
626
|
+
const candidates = specRoot ? [] : listSpecCandidatesForInit(opts.workspaceRoot);
|
|
627
|
+
const canPrompt = Boolean(process.stdin.isTTY) && !opts.yes;
|
|
628
|
+
|
|
629
|
+
// 多候选 + 无法询问:不猜(猜错不可逆 —— 写错的是每个代码仓的 git config)
|
|
630
|
+
if (!specRoot && candidates.length > 1 && !canPrompt) {
|
|
631
|
+
const lines = [
|
|
632
|
+
`❌ 检测到 ${candidates.length} 个 spec 仓库,无法自动判定要接入哪一个:`,
|
|
633
|
+
...candidates.map((c) => ` - ${c.abs} [${specSourceLabel(c.source)}]`),
|
|
634
|
+
' 请用 --spec-path=<dir> 显式指定,或在交互终端中运行以手动选择。',
|
|
635
|
+
];
|
|
636
|
+
for (const line of lines) console.error(line);
|
|
34
637
|
process.exit(1);
|
|
35
638
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
639
|
+
|
|
640
|
+
// 单选(0 个候选 → undefined;1 个 → 确认;N 个 → 列出选择)
|
|
641
|
+
if (!specRoot && candidates.length > 0 && canPrompt) {
|
|
642
|
+
const prompt = openPrompt();
|
|
643
|
+
try {
|
|
644
|
+
const chosen = await selectSpecRoot(prompt, candidates, { actionLabel: '接入' });
|
|
645
|
+
if (chosen === null) {
|
|
646
|
+
console.log('已取消');
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (chosen) specRoot = chosen;
|
|
650
|
+
} finally {
|
|
651
|
+
prompt.close();
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const result = syncWorkspaceRepos(opts.workspaceRoot, {
|
|
656
|
+
onlyNew: opts.onlyNew,
|
|
657
|
+
specRoot,
|
|
658
|
+
});
|
|
659
|
+
|
|
43
660
|
if (!result.ok && result.attached.length === 0 && (!result.layout || !result.layout.specRepo)) {
|
|
44
661
|
process.exit(1);
|
|
45
662
|
}
|
|
@@ -50,15 +667,64 @@ if (args[0] === 'log') {
|
|
|
50
667
|
process.exit(1);
|
|
51
668
|
}
|
|
52
669
|
console.log(`✅ sync-repos 完成,接入 ${result.attached.length} 个代码仓`);
|
|
53
|
-
} else if (args[0] === 'auth') {
|
|
54
|
-
// 登录体系已随 2.7.0 移除:仅保留 deprecation 提示,不进入 init 向导
|
|
55
|
-
console.log('ℹ️ auth 子命令已于 2.7.0 移除:使用统计按本机 git 身份(user.name/user.email)上报,无需绑定。');
|
|
56
|
-
process.exit(0);
|
|
57
|
-
} else {
|
|
58
|
-
// 正常初始化模式
|
|
59
|
-
const { main } = require('../lib/init');
|
|
60
|
-
main().catch(error => {
|
|
61
|
-
console.error('❌ 初始化失败:', error.message);
|
|
62
|
-
process.exit(1);
|
|
63
|
-
});
|
|
64
670
|
}
|
|
671
|
+
|
|
672
|
+
function runLinkSpec() {
|
|
673
|
+
let opts;
|
|
674
|
+
try {
|
|
675
|
+
opts = parseLinkSpecArgs(args);
|
|
676
|
+
} catch (error) {
|
|
677
|
+
fail(error.message);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const sddConfig = require('../skywalk-sdd/ontology/sdd-config.cjs');
|
|
682
|
+
const result = sddConfig.setSpecPath(opts.projectRoot, opts.specPath);
|
|
683
|
+
if (!result.ok) {
|
|
684
|
+
fail(result.message);
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
console.log(`✅ 已设置 git config --local sdd.specPath = ${result.path}`);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* 子命令路由(仅在被当作入口脚本执行时触发)。
|
|
692
|
+
*
|
|
693
|
+
* require.main 守卫:使本文件可被测试 require 以复用卸载编排函数,
|
|
694
|
+
* 而不产生「加载即执行」的副作用。
|
|
695
|
+
*/
|
|
696
|
+
function route() {
|
|
697
|
+
if (command === 'uninstall') {
|
|
698
|
+
// 卸载模式:默认只删工具内容,保留用户配置与运行时数据
|
|
699
|
+
runUninstall().catch((error) => fail(error.message));
|
|
700
|
+
} else if (command === 'log') {
|
|
701
|
+
// Telemetry CLI 模式(运行时入口为 index.cjs,部署到用户仓后改名为 log.cjs)
|
|
702
|
+
require('../skywalk-sdd/index.cjs').main();
|
|
703
|
+
} else if (command === 'link-spec') {
|
|
704
|
+
runLinkSpec();
|
|
705
|
+
} else if (command === 'sync-repos') {
|
|
706
|
+
// 与 uninstall 一致:异步失败统一转成 stderr + 非零退出码
|
|
707
|
+
runSyncRepos().catch((error) => fail(error.message));
|
|
708
|
+
} else if (command === 'auth') {
|
|
709
|
+
// 登录体系已随 2.7.0 移除:仅保留 deprecation 提示,不进入 init 向导
|
|
710
|
+
console.log('ℹ️ auth 子命令已于 2.7.0 移除:使用统计按本机 git 身份(user.name/user.email)上报,无需绑定。');
|
|
711
|
+
process.exit(0);
|
|
712
|
+
} else {
|
|
713
|
+
// 正常初始化模式
|
|
714
|
+
runInit();
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (require.main === module) {
|
|
719
|
+
route();
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
module.exports = {
|
|
723
|
+
route,
|
|
724
|
+
runUninstall,
|
|
725
|
+
parseUninstallArgs,
|
|
726
|
+
printPlanPreview,
|
|
727
|
+
selectUninstallMode,
|
|
728
|
+
selectSpecRoot,
|
|
729
|
+
showUninstallHelp,
|
|
730
|
+
};
|