kld-sdd 2.7.8-4 → 2.7.8-6
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 +24 -0
- package/bin/kld-sdd-init.js +115 -97
- package/lib/init.js +51 -16
- package/lib/maintenance.js +221 -0
- package/lib/skills-bundle.js +2 -0
- package/lib/uninstall.js +121 -432
- package/package.json +6 -3
- package/skywalk-sdd/context-client.cjs +38 -7
- package/skywalk-sdd/kb-upload.cjs +73 -28
- package/skywalk-sdd/ontology/archive-package.cjs +145 -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/schema.cjs +6 -0
- package/skywalk-sdd/ontology/spec-ontology.cjs +384 -0
- package/skywalk-sdd/ontology/workspace-layout.cjs +19 -1
- package/templates/openspec/spec.md +8 -0
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +10 -10
- package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +3 -2
- package/templates/skills/kld-sdd/opsx-ontology-ingest/SKILL.md +75 -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-spec/SKILL.md +5 -3
package/README.md
CHANGED
|
@@ -6,6 +6,30 @@ KLD SDD OpenSpec 工程增强工具:一键初始化 AI 编辑器技能、语
|
|
|
6
6
|
|
|
7
7
|
日常提交默认只保留本地关联 Hook;Apply 自动执行代码/Spec 检查、真实测试与交付验证。旧项目升级、备份回退、多仓和 CI 边界见 [Git Hook 调整说明](USABILITY.md#2026-09-10git-hook-收缩与交付验证)。
|
|
8
8
|
|
|
9
|
+
## 工具更新与卸载
|
|
10
|
+
|
|
11
|
+
**更新不需要先卸载。** 在原工作目录重新运行安装器,覆盖运行脚本、Skills 和只读模板;Spec 文档、KB 绑定、项目身份、历史数据、用户配置和自定义 Hook 保留。先确认目标编辑器及 Spec 目录,完成后检查实际部署版本。下载失败不启动安装;执行中断后可以重跑补齐,不承诺整个项目的事务回滚。
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# 选择包含本次修复的新发布版本,在原工作目录更新
|
|
15
|
+
npx kld-sdd@<新版版本号> --tool=cursor --spec-path=team-sdd-specs --skip-openspec
|
|
16
|
+
|
|
17
|
+
# 卸载只有工具一种方式;可以先看计划
|
|
18
|
+
npx kld-sdd@<新版版本号> uninstall --dry-run
|
|
19
|
+
npx kld-sdd@<新版版本号> uninstall
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
卸载不再提供全量删除,旧 `--purge` 参数会报错退出。`openspec/`(含活动变更、归档和规格)、KB 配置、身份、历史数据、`.sdd.yaml`、`.sdd-spec-root`、`.sdd-workspace.yaml`、Git 关联和隐私忽略规则始终保留。工具目录内的未知文件和自建规则也保留;Git Hook 只有完整匹配受管版本时才删除。预览后文件改变需要重新预览。
|
|
23
|
+
|
|
24
|
+
多仓注意:
|
|
25
|
+
|
|
26
|
+
- 升级沿用已有清单,只维护登记的代码仓;多个 Spec 和备份不会互相接入。新代码仓通过 `kld-sdd sync-repos` 明确接入,追加清单时保留注释、自定义字段和原有记录。
|
|
27
|
+
- 本地路径、团队远程声明或工作区清单冲突时,先停止更新并提示核对,不自动换绑。
|
|
28
|
+
- 从单个代码仓卸载时,工作目录外的共享 Spec 工具保留。从 worktree 卸载时,共用的 Git Hook 保留,应从主仓库管理。
|
|
29
|
+
- 旧平铺 `skywalk-sdd/` 只清理已知工具,历史数据原位保留,不自动合并两份项目身份或运行状态。
|
|
30
|
+
|
|
31
|
+
以上行为需要使用包含本次改动的新版本;修改此源码不会改变已经发布的 npm 包。安装包通过 `sddMaintenance.toolOnly: 1` 声明该维护契约,托盘据此避免用旧安装器处理存在已知清理风险的布局。
|
|
32
|
+
|
|
9
33
|
## 这是什么?
|
|
10
34
|
|
|
11
35
|
**SDD(Specification-Driven Development)** 是一种以文档链驱动 AI 编码的研发方法:先写清楚"要做什么",再让 AI 去实现,避免 AI 乱猜、反复返工。
|
package/bin/kld-sdd-init.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* log SDD Telemetry CLI(记录阶段事件、查询指标)
|
|
13
13
|
* link-spec 将代码仓关联到本地 spec clone(git config sdd.specPath)
|
|
14
14
|
* sync-repos 工作目录下发现新增代码仓并轻量接入(不装 skills)
|
|
15
|
-
* uninstall 卸载 SDD
|
|
15
|
+
* uninstall 卸载 SDD 产物(渐进式:先预览工具范围,最后确认执行)
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
'use strict';
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
const path = require('path');
|
|
21
21
|
const { listToolProfileIds, buildToolConfigs } = require('../lib/tool-profiles');
|
|
22
22
|
const { MODE, OP, modeLabel } = require('../lib/uninstall');
|
|
23
|
-
const
|
|
23
|
+
const specDetect = require('../skywalk-sdd/ontology/spec-detect.cjs');
|
|
24
|
+
const { specSourceLabel } = specDetect;
|
|
24
25
|
// spec 候选单选交互(唯一实现,与 lib/init.js 共用)
|
|
25
26
|
const { selectSpecRoot: selectSpecChoice } = require('../lib/spec-choice');
|
|
26
27
|
|
|
@@ -125,6 +126,9 @@ function parseInitArgs(argv = cliArgs(), env = process.env) {
|
|
|
125
126
|
verbose: isVerbose(argv, env),
|
|
126
127
|
skipOpenspec: argv.includes('--skip-openspec'),
|
|
127
128
|
skipTemplate: argv.includes('--skip-template'),
|
|
129
|
+
repairGit: argv[0] === 'repair-git' || argv.includes('--repair-git'),
|
|
130
|
+
applyRepair: argv.includes('--apply'),
|
|
131
|
+
json: argv.includes('--json'),
|
|
128
132
|
tools,
|
|
129
133
|
// 显式给了 --tool 即自动化场景:lib 据此抑制 spec 路径的交互询问
|
|
130
134
|
hasToolArg: tools !== null,
|
|
@@ -136,19 +140,19 @@ function parseInitArgs(argv = cliArgs(), env = process.env) {
|
|
|
136
140
|
* kld-sdd uninstall [选项]
|
|
137
141
|
*
|
|
138
142
|
* 卸载产物与编辑器无关(8 个编辑器的产物全量扫描),工作目录固定为当前目录,
|
|
139
|
-
* 故不提供 --tool / --project
|
|
143
|
+
* 故不提供 --tool / --project:仅卸载当前范围的工具,目录由 cwd 决定。
|
|
140
144
|
*
|
|
141
145
|
* --spec-path 用于无人值守场景:多候选(如用户复制的备份)时无法自动判定,
|
|
142
146
|
* 需显式指定要卸载的 spec 仓库。
|
|
143
147
|
*/
|
|
144
148
|
function parseUninstallArgs(argv = cliArgs()) {
|
|
145
|
-
|
|
149
|
+
if (argv.some(arg => arg === '--purge' || arg.startsWith('--purge=') || arg === '--full' || arg.startsWith('--mode'))) {
|
|
150
|
+
throw new Error('已取消全量删除,仅支持卸载工具;Spec 文档、配置与历史数据始终保留');
|
|
151
|
+
}
|
|
146
152
|
return {
|
|
147
153
|
cwd: process.cwd(),
|
|
148
154
|
specPath: parseSpecPathOption(argv),
|
|
149
|
-
mode:
|
|
150
|
-
// 是否显式指定了方式:未指定且未跳过交互时,进入渐进式方式选择
|
|
151
|
-
modeExplicit: purge,
|
|
155
|
+
mode: MODE.TOOLS_ONLY,
|
|
152
156
|
dryRun: argv.includes('--dry-run'),
|
|
153
157
|
yes: hasFlag(argv, '--yes', '-y'),
|
|
154
158
|
help: hasFlag(argv, '-h', '--help'),
|
|
@@ -325,78 +329,26 @@ async function selectSpecRoot(prompt, candidates, options = {}) {
|
|
|
325
329
|
return selectSpecChoice(prompt, candidates, options);
|
|
326
330
|
}
|
|
327
331
|
|
|
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
332
|
/** 显示卸载帮助信息 */
|
|
359
333
|
function showUninstallHelp() {
|
|
360
334
|
console.log(`
|
|
361
|
-
KLD SDD
|
|
335
|
+
KLD SDD 工具卸载
|
|
362
336
|
|
|
363
337
|
用法:
|
|
364
|
-
kld-sdd uninstall
|
|
365
|
-
kld-sdd uninstall
|
|
338
|
+
kld-sdd uninstall # 选择 Spec → 预览范围 → 确认
|
|
339
|
+
kld-sdd uninstall --dry-run # 只看计划,不修改文件
|
|
340
|
+
kld-sdd uninstall --yes --spec-path <dir> # 明确范围后用于脚本
|
|
366
341
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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)。执行前会打印待删目录与文件数,需二次确认。
|
|
342
|
+
仅删除可识别的 SDD Skills、运行脚本、只读模板与受管 Hook。
|
|
343
|
+
始终保留 openspec/ 全部文档、KB 配置、项目身份、历史数据、多仓关联和 Git 历史。
|
|
344
|
+
自定义 Hook、规则、未知文件与隐私忽略规则保留。不提供全量删除模式。
|
|
394
345
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
346
|
+
-h, --help 显示本帮助
|
|
347
|
+
--spec-path <dir> 指定目标 Spec;多个候选时必须选择,备份不受影响
|
|
348
|
+
--dry-run 只预览
|
|
349
|
+
-y, --yes 确认工具卸载;非交互执行必须提供
|
|
350
|
+
|
|
351
|
+
升级不需要先卸载:重新运行 init 覆盖工具文件,保留已有文档及配置。
|
|
400
352
|
`);
|
|
401
353
|
}
|
|
402
354
|
|
|
@@ -409,8 +361,9 @@ KLD SDD 项目初始化工具
|
|
|
409
361
|
kld-sdd-init [选项]
|
|
410
362
|
npx kld-sdd [选项]
|
|
411
363
|
kld-sdd sync-repos # 工作目录下发现新增代码仓并轻量接入(多 spec 候选时单选)
|
|
412
|
-
kld-sdd
|
|
413
|
-
kld-sdd uninstall
|
|
364
|
+
kld-sdd repair-git # 检查并清理旧 Spec 仓的工具跟踪噪声(默认只预览)
|
|
365
|
+
kld-sdd uninstall [选项] # 卸载 SDD 产物(渐进式:预览工具范围 → 确认)
|
|
366
|
+
kld-sdd uninstall --help # 查看工具卸载与保留范围
|
|
414
367
|
|
|
415
368
|
选项:
|
|
416
369
|
-h, --help 显示帮助信息
|
|
@@ -418,6 +371,9 @@ KLD SDD 项目初始化工具
|
|
|
418
371
|
--verbose 输出每个安装文件/目录的明细日志(默认仅摘要,亦可用 KLD_SDD_VERBOSE=1)
|
|
419
372
|
--skip-openspec 跳过 openspec init 步骤
|
|
420
373
|
--skip-template 跳过复制内置模版
|
|
374
|
+
--repair-git 检查 Spec 仓中被 Git 跟踪的安装器工具(默认只预览)
|
|
375
|
+
--apply 与 --repair-git 同用:保留本地文件,仅从 Git 索引移除工具
|
|
376
|
+
--json --repair-git 输出机器可读 JSON,供托盘健康检查使用
|
|
421
377
|
--tool <name> 指定编辑器,可用值: cursor, claude, codebuddy, qoder, opencode, kunlunzhima, workbuddy, codex, all
|
|
422
378
|
--spec-path <dir> 指定 spec 包裹包目录(必须是当前目录的子目录)。
|
|
423
379
|
不指定时(交互终端):自动检测已有 spec 目录(目录名 *-sdd-specs,
|
|
@@ -437,6 +393,8 @@ KLD SDD 项目初始化工具
|
|
|
437
393
|
kld-sdd-init --skip-openspec # 跳过 openspec,直接部署 skills
|
|
438
394
|
kld-sdd-init --tool codex # 仅部署 Codex 配置
|
|
439
395
|
kld-sdd-init --spec-path erp-sdd-specs # 指定 spec 目录(当前目录的子目录)
|
|
396
|
+
kld-sdd repair-git --spec-path=. # 预览旧仓工具跟踪问题
|
|
397
|
+
kld-sdd repair-git --apply --spec-path=. # 一次迁出工具,文件仍留在磁盘
|
|
440
398
|
kld-sdd sync-repos # 新代码仓加入后同步接入
|
|
441
399
|
`);
|
|
442
400
|
}
|
|
@@ -445,6 +403,72 @@ KLD SDD 项目初始化工具
|
|
|
445
403
|
// 子命令路由:解析参数后调用 lib/ 业务实现
|
|
446
404
|
// ═══════════════════════════════════════════════════════════════
|
|
447
405
|
|
|
406
|
+
function resolveRepairSpecRoot(parsed, cwd = process.cwd()) {
|
|
407
|
+
const root = path.resolve(cwd);
|
|
408
|
+
if (parsed.specPath) {
|
|
409
|
+
const explicit = path.resolve(root, parsed.specPath);
|
|
410
|
+
if (!specDetect.isSpecPackageRoot(explicit)) {
|
|
411
|
+
throw new Error(`指定目录不是有效的 Spec 包裹包: ${explicit}`);
|
|
412
|
+
}
|
|
413
|
+
return explicit;
|
|
414
|
+
}
|
|
415
|
+
if (specDetect.isSpecPackageRoot(root)) return root;
|
|
416
|
+
const candidates = specDetect.detectSpecCandidatesWithGit(root);
|
|
417
|
+
if (candidates.length === 1) return candidates[0].abs;
|
|
418
|
+
if (candidates.length === 0) throw new Error('未发现 Spec 包裹包,请用 --spec-path=<dir> 指定');
|
|
419
|
+
throw new Error(`发现 ${candidates.length} 个 Spec 包裹包,请用 --spec-path=<dir> 指定,不自动猜测`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function runGitRepair(parsed) {
|
|
423
|
+
const init = require('../lib/init');
|
|
424
|
+
const maintenance = require('../lib/maintenance');
|
|
425
|
+
const specRoot = resolveRepairSpecRoot(parsed);
|
|
426
|
+
const plan = maintenance.trackedManagedSpecTools(specRoot);
|
|
427
|
+
if (!plan.ok) throw new Error(plan.error);
|
|
428
|
+
|
|
429
|
+
if (!parsed.applyRepair) {
|
|
430
|
+
const payload = {
|
|
431
|
+
mode: 'preview',
|
|
432
|
+
specRoot,
|
|
433
|
+
trackedToolCount: plan.files.length,
|
|
434
|
+
trackedTools: plan.files.map((file) => file.relative),
|
|
435
|
+
businessFilesAffected: 0,
|
|
436
|
+
};
|
|
437
|
+
if (parsed.json) {
|
|
438
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
console.log(`🧹 Spec Git 清洁度检查: ${path.basename(specRoot)}`);
|
|
442
|
+
console.log(` 被 Git 跟踪的可再生成工具: ${plan.files.length} 个`);
|
|
443
|
+
for (const file of plan.files.slice(0, 8)) console.log(` - ${file.relative}`);
|
|
444
|
+
if (plan.files.length > 8) console.log(` … 其余 ${plan.files.length - 8} 个`);
|
|
445
|
+
console.log(' 业务 Spec、配置、项目身份与 Hook 参数: 不处理');
|
|
446
|
+
if (plan.files.length) console.log(' 执行 --repair-git --apply 可一次迁出工具;不会删除磁盘文件。');
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const existingBefore = plan.files.filter((file) => require('fs').existsSync(path.join(specRoot, file.relative)));
|
|
451
|
+
init.updateGitignore(specRoot, { specPackage: true });
|
|
452
|
+
const result = maintenance.untrackManagedSpecTools(specRoot);
|
|
453
|
+
if (!result.ok) throw new Error(result.error);
|
|
454
|
+
const payload = {
|
|
455
|
+
mode: 'applied',
|
|
456
|
+
specRoot,
|
|
457
|
+
untrackedToolCount: result.files.length,
|
|
458
|
+
filesRemainOnDisk: existingBefore.every((file) => require('fs').existsSync(path.join(specRoot, file.relative))),
|
|
459
|
+
retainedLocalToolCount: existingBefore.length,
|
|
460
|
+
alreadyMissingToolCount: result.files.length - existingBefore.length,
|
|
461
|
+
businessFilesAffected: 0,
|
|
462
|
+
};
|
|
463
|
+
if (parsed.json) {
|
|
464
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
console.log(`✅ 已将 ${result.files.length} 个安装器工具移出 Git 跟踪,现存的 ${existingBefore.length} 个本地文件全部保留`);
|
|
468
|
+
if (payload.alreadyMissingToolCount) console.log(` ℹ️ 其中 ${payload.alreadyMissingToolCount} 个文件在修复前已从磁盘删除`);
|
|
469
|
+
console.log(' 请审阅一次 git status 并提交迁移;以后升级不再污染业务改动列表。');
|
|
470
|
+
}
|
|
471
|
+
|
|
448
472
|
async function runInit() {
|
|
449
473
|
let parsed;
|
|
450
474
|
try {
|
|
@@ -464,6 +488,15 @@ async function runInit() {
|
|
|
464
488
|
return;
|
|
465
489
|
}
|
|
466
490
|
|
|
491
|
+
if (parsed.repairGit) {
|
|
492
|
+
try {
|
|
493
|
+
runGitRepair(parsed);
|
|
494
|
+
} catch (error) {
|
|
495
|
+
fail(`Git 清洁度修复失败: ${error.message}`);
|
|
496
|
+
}
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
467
500
|
const init = require('../lib/init');
|
|
468
501
|
init.setVerbose(parsed.verbose);
|
|
469
502
|
|
|
@@ -484,7 +517,7 @@ async function runInit() {
|
|
|
484
517
|
/**
|
|
485
518
|
* 卸载(渐进式):方式选择 → 计划预览 → 确认执行。
|
|
486
519
|
*
|
|
487
|
-
* 交互编排全部在本函数(CLI
|
|
520
|
+
* 交互编排全部在本函数(CLI 输入层);仅选择 Spec、预览工具范围并确认:
|
|
488
521
|
* - buildUninstallPlan 纯计算计划,不落盘
|
|
489
522
|
* - main 按给定计划执行,无交互
|
|
490
523
|
*
|
|
@@ -524,7 +557,7 @@ async function runUninstall(argv = args, deps = {}) {
|
|
|
524
557
|
// 候选枚举:结构判据无法区分「当前项目的 spec 仓」与「用户复制的备份」
|
|
525
558
|
//(如 `erp-sdd-specs copy` 结构标记齐全),故由用户单选裁决。
|
|
526
559
|
// 显式 --spec-path 时跳过枚举(调用方已指定)。
|
|
527
|
-
let specRoot = opts.specPath ? path.resolve(opts.specPath) : undefined;
|
|
560
|
+
let specRoot = opts.specPath ? path.resolve(cwd, opts.specPath) : undefined;
|
|
528
561
|
const candidates = specRoot ? [] : listSpecCandidatesForUninstall(cwd);
|
|
529
562
|
|
|
530
563
|
if (!specRoot && candidates.length > 1 && !canPrompt) {
|
|
@@ -539,6 +572,7 @@ async function runUninstall(argv = args, deps = {}) {
|
|
|
539
572
|
throw new Error('多个 spec 仓库候选,需 --spec-path 显式指定');
|
|
540
573
|
}
|
|
541
574
|
|
|
575
|
+
if (!specRoot && !canPrompt && candidates.length === 1) specRoot = candidates[0].abs;
|
|
542
576
|
const prompt = canPrompt ? promptFactory() : null;
|
|
543
577
|
|
|
544
578
|
// 步骤 1:spec 仓库选择(多候选 → 用户单选;单候选 → 确认)
|
|
@@ -563,31 +597,15 @@ async function runUninstall(argv = args, deps = {}) {
|
|
|
563
597
|
}
|
|
564
598
|
|
|
565
599
|
if (!prompt) {
|
|
600
|
+
if (!dryRun && !opts.yes) throw new Error('非交互卸载需要 --yes;可先用 --dry-run 预览');
|
|
566
601
|
printUninstallHeader(cwd, planned.specRoot, mode, dryRun);
|
|
567
602
|
if (dryRun) printPlanPreview(planned.plan);
|
|
568
603
|
// 已打印过预览(含警告)时不重复;直接执行时由 lib 打印
|
|
569
|
-
runUninstallPlan({ cwd, mode, dryRun, plan: planned.plan, showWarnings: !dryRun });
|
|
604
|
+
const result = runUninstallPlan({ cwd, mode, dryRun, plan: planned.plan, showWarnings: !dryRun });
|
|
605
|
+
if (!result.ok) throw new Error(result.errors.join('; '));
|
|
570
606
|
return;
|
|
571
607
|
}
|
|
572
608
|
|
|
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
609
|
// 步骤 4:计划预览
|
|
592
610
|
printUninstallHeader(cwd, planned.specRoot, mode, dryRun);
|
|
593
611
|
printPlanPreview(planned.plan);
|
|
@@ -602,7 +620,8 @@ async function runUninstall(argv = args, deps = {}) {
|
|
|
602
620
|
console.log('');
|
|
603
621
|
|
|
604
622
|
// 交互路径:警告已由 printPlanPreview 打印过,不再重复
|
|
605
|
-
runUninstallPlan({ cwd, mode, dryRun, plan: planned.plan, showWarnings: false });
|
|
623
|
+
const result = runUninstallPlan({ cwd, mode, dryRun, plan: planned.plan, showWarnings: false });
|
|
624
|
+
if (!result.ok) throw new Error(result.errors.join('; '));
|
|
606
625
|
} finally {
|
|
607
626
|
if (prompt) prompt.close();
|
|
608
627
|
}
|
|
@@ -724,7 +743,6 @@ module.exports = {
|
|
|
724
743
|
runUninstall,
|
|
725
744
|
parseUninstallArgs,
|
|
726
745
|
printPlanPreview,
|
|
727
|
-
selectUninstallMode,
|
|
728
746
|
selectSpecRoot,
|
|
729
747
|
showUninstallHelp,
|
|
730
748
|
};
|
package/lib/init.js
CHANGED
|
@@ -31,6 +31,7 @@ const {
|
|
|
31
31
|
const { deployCodebuddyHookPack } = require('./deploy-codebuddy-hooks');
|
|
32
32
|
const { PROVIDERS, computeMergedSettings } = require('./settings-merge');
|
|
33
33
|
const workspaceLayout = require('./workspace-layout');
|
|
34
|
+
const maintenance = require('./maintenance');
|
|
34
35
|
const sddConfig = require('../skywalk-sdd/ontology/sdd-config.cjs');
|
|
35
36
|
// spec 判据与候选枚举的唯一入口(tier 分级 + 排序)
|
|
36
37
|
const specDetect = require('../skywalk-sdd/ontology/spec-detect.cjs');
|
|
@@ -336,6 +337,7 @@ function copyDirRendered(source, target, config, toolKey) {
|
|
|
336
337
|
return deployDir(source, target, {
|
|
337
338
|
strategy: STRATEGY.OVERWRITE,
|
|
338
339
|
render: (srcPath, tgtPath) => {
|
|
340
|
+
if (path.basename(srcPath) === 'config.json' && fs.existsSync(tgtPath)) return fs.readFileSync(tgtPath, 'utf8');
|
|
339
341
|
const rendered = renderTemplate(fs.readFileSync(srcPath, 'utf8'), config, toolKey, profile);
|
|
340
342
|
validateRenderedContent(rendered, profile, tgtPath);
|
|
341
343
|
return rendered;
|
|
@@ -1077,6 +1079,9 @@ function attachCodeRepoLite(codeRepoRoot, specRepoRoot, options = {}) {
|
|
|
1077
1079
|
return { ok: false, repo: label, message: `spec 路径不是 Git 仓库: ${specRoot}` };
|
|
1078
1080
|
}
|
|
1079
1081
|
|
|
1082
|
+
const conflict = maintenance.associationConflict(cwd, specRoot);
|
|
1083
|
+
if (conflict) return { ok: false, repo: label, message: conflict };
|
|
1084
|
+
|
|
1080
1085
|
console.log(`🔗 轻量接入代码仓(本地关联 Hook): ${label}`);
|
|
1081
1086
|
|
|
1082
1087
|
const remote = sddConfig.gitRemoteUrl(specRoot) || 'git@gitlab.example.com:biz/xxx-sdd-specs.git';
|
|
@@ -1277,6 +1282,9 @@ async function populateSpecPackage(specRoot, options = {}) {
|
|
|
1277
1282
|
copyTemplatesToProject(root);
|
|
1278
1283
|
initGlobalOverview(root);
|
|
1279
1284
|
deploySddGuideManual(root);
|
|
1285
|
+
// Spec 包可能是独立 Git 仓或单仓内的子目录;忽略规则必须落在它自身,
|
|
1286
|
+
// 不能只写工作区根,否则升级会把整套可再生成运行时显示为业务改动。
|
|
1287
|
+
updateGitignore(root, { specPackage: true });
|
|
1280
1288
|
console.log(`✅ SDD 包裹包已就绪: ${path.basename(root)}`);
|
|
1281
1289
|
return { ok: true, path: root };
|
|
1282
1290
|
}
|
|
@@ -1347,13 +1355,20 @@ function removeOuterSkywalk(anchorDir) {
|
|
|
1347
1355
|
return { ok: true, removed: true, mode: 'symlink' };
|
|
1348
1356
|
}
|
|
1349
1357
|
if (st.isDirectory()) {
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1358
|
+
// 旧目录可能存有 KB 身份、待上报事件和用户文件,不能整目录删除。
|
|
1359
|
+
for (const file of maintenance.runtimeToolFiles(outer)) fs.unlinkSync(file);
|
|
1360
|
+
const prune = dir => {
|
|
1361
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1362
|
+
if (entry.isDirectory() && !['events', 'state'].includes(entry.name)) prune(path.join(dir, entry.name));
|
|
1363
|
+
}
|
|
1364
|
+
if (!fs.readdirSync(dir).length) fs.rmdirSync(dir);
|
|
1365
|
+
};
|
|
1366
|
+
prune(outer);
|
|
1367
|
+
const retained = fs.existsSync(outer);
|
|
1368
|
+
console.log(retained ? ' ℹ️ 旧 skywalk-sdd/ 的配置、历史数据和未知文件已保留;仅清理工具脚本' : ' ✓ 已清理根目录旧工具脚本');
|
|
1369
|
+
return { ok: true, removed: !retained, retained, mode: 'directory' };
|
|
1353
1370
|
}
|
|
1354
|
-
|
|
1355
|
-
console.log(' ✓ 已移除 Git/工作区根多余的 skywalk-sdd');
|
|
1356
|
-
return { ok: true, removed: true, mode: 'file' };
|
|
1371
|
+
return { ok: true, removed: false, skipped: true }; // 同名用户文件不处理
|
|
1357
1372
|
} catch (err) {
|
|
1358
1373
|
console.log(` ⚠️ 未能移除根目录 skywalk-sdd/: ${err.message}`);
|
|
1359
1374
|
return { ok: false, message: err.message };
|
|
@@ -1364,7 +1379,7 @@ function removeOuterSkywalk(anchorDir) {
|
|
|
1364
1379
|
* 创建 / 更新 .gitignore
|
|
1365
1380
|
* @param {string} [cwd=process.cwd()]
|
|
1366
1381
|
*/
|
|
1367
|
-
function updateGitignore(cwd = process.cwd()) {
|
|
1382
|
+
function updateGitignore(cwd = process.cwd(), options = {}) {
|
|
1368
1383
|
const root = path.resolve(cwd);
|
|
1369
1384
|
const gitignorePath = path.join(root, '.gitignore');
|
|
1370
1385
|
const requiredLines = [
|
|
@@ -1383,6 +1398,10 @@ function updateGitignore(cwd = process.cwd()) {
|
|
|
1383
1398
|
'**/skywalk-sdd/state/',
|
|
1384
1399
|
'.worktrees/'
|
|
1385
1400
|
];
|
|
1401
|
+
const specPackageLines = options.specPackage ? maintenance.specPackageIgnoreLines() : [];
|
|
1402
|
+
if (options.specPackage) {
|
|
1403
|
+
requiredLines.push(...specPackageLines);
|
|
1404
|
+
}
|
|
1386
1405
|
|
|
1387
1406
|
const sddConfig = `
|
|
1388
1407
|
# KLD SDD AI 编辑器个人配置(请勿提交)
|
|
@@ -1406,6 +1425,10 @@ skywalk-sdd/state/
|
|
|
1406
1425
|
|
|
1407
1426
|
# SDD Apply 隔离 worktree(本地临时目录)
|
|
1408
1427
|
.worktrees/
|
|
1428
|
+
${options.specPackage ? `
|
|
1429
|
+
# KLD SDD 安装器工具(可由 init/升级重新生成,不属于业务 Spec)
|
|
1430
|
+
${specPackageLines.join('\n')}
|
|
1431
|
+
` : ''}
|
|
1409
1432
|
`;
|
|
1410
1433
|
|
|
1411
1434
|
const result = deployFile(null, gitignorePath, {
|
|
@@ -1505,6 +1528,9 @@ function syncWorkspaceRepos(workspaceRoot = process.cwd(), options = {}) {
|
|
|
1505
1528
|
console.log(`🆕 待接入新仓: ${filteredTargets.map((c) => c.name).join(', ') || '(无)'}`);
|
|
1506
1529
|
}
|
|
1507
1530
|
|
|
1531
|
+
const conflicts = maintenance.maintenanceConflicts(root, specRootAbs, codeRepos);
|
|
1532
|
+
if (conflicts.length) return { ok: false, attached: [], failed: conflicts, layout, specRoot: specRootAbs, message: conflicts.map(c => c.repo + ': ' + c.message).join('; ') };
|
|
1533
|
+
|
|
1508
1534
|
const attached = [];
|
|
1509
1535
|
const failed = [];
|
|
1510
1536
|
for (const repo of filteredTargets) {
|
|
@@ -1513,7 +1539,9 @@ function syncWorkspaceRepos(workspaceRoot = process.cwd(), options = {}) {
|
|
|
1513
1539
|
else failed.push(result);
|
|
1514
1540
|
}
|
|
1515
1541
|
|
|
1516
|
-
|
|
1542
|
+
if (failed.length) return { ok: false, attached, failed, layout, specRoot: specRootAbs };
|
|
1543
|
+
|
|
1544
|
+
// 显式 sync-repos 成功后才刷新清单
|
|
1517
1545
|
const layoutForFile = {
|
|
1518
1546
|
...layout,
|
|
1519
1547
|
specRepo: { name: specName, abs: specRootAbs },
|
|
@@ -1614,6 +1642,12 @@ async function main(options = {}) {
|
|
|
1614
1642
|
}
|
|
1615
1643
|
}
|
|
1616
1644
|
|
|
1645
|
+
if (specPackage) {
|
|
1646
|
+
if (layout.isWorkspace) layout.codeRepos = maintenance.workspaceCodeRepos(workspaceRoot, specPackage.abs, layout, { respectManifest: true });
|
|
1647
|
+
const conflicts = maintenance.maintenanceConflicts(workspaceRoot, specPackage.abs, layout.isWorkspace ? layout.codeRepos : []);
|
|
1648
|
+
if (conflicts.length) throw new Error('未更新工具:' + conflicts.map(c => c.repo + ': ' + c.message).join('; '));
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1617
1651
|
if (layout.isWorkspace) {
|
|
1618
1652
|
// 显示层同步过滤:用户交互选择的 spec 目录可能原被分类为代码仓
|
|
1619
1653
|
const displayCodeRepos = specPackage
|
|
@@ -1650,18 +1684,18 @@ async function main(options = {}) {
|
|
|
1650
1684
|
if (!layout.specRepo || path.resolve(layout.specRepo.abs) !== path.resolve(specPackage.abs)) {
|
|
1651
1685
|
layout.specRepo = { name: specPackage.name, abs: specPackage.abs };
|
|
1652
1686
|
}
|
|
1653
|
-
layout.codeRepos =
|
|
1654
|
-
(c) => path.resolve(c.abs) !== path.resolve(specPackage.abs),
|
|
1655
|
-
);
|
|
1687
|
+
layout.codeRepos = maintenance.workspaceCodeRepos(workspaceRoot, specPackage.abs, layout, { respectManifest: true });
|
|
1656
1688
|
}
|
|
1657
1689
|
|
|
1658
1690
|
console.log();
|
|
1659
1691
|
console.log('🔗 正在为子代码仓做轻量接入(不安装 skills)...');
|
|
1660
1692
|
for (const repo of layout.codeRepos) {
|
|
1661
|
-
attachCodeRepoLite(repo.abs, specPackage.abs);
|
|
1693
|
+
const result = attachCodeRepoLite(repo.abs, specPackage.abs);
|
|
1694
|
+
if (!result.ok) throw new Error(`${repo.name}: ${result.message}`);
|
|
1662
1695
|
}
|
|
1663
|
-
const
|
|
1664
|
-
|
|
1696
|
+
const savedWorkspace = workspaceLayout.loadWorkspaceFile(workspaceRoot);
|
|
1697
|
+
const wsFile = savedWorkspace.ok ? savedWorkspace.path : workspaceLayout.writeWorkspaceFile(workspaceRoot, layout);
|
|
1698
|
+
console.log(`✅ 工作区清单${savedWorkspace.ok ? '已保留(新增仓用 sync-repos 接入)' : ''}: ${wsFile}`);
|
|
1665
1699
|
} else {
|
|
1666
1700
|
// 单仓:Git 根只放代码 + skills + Hook;SDD 内容进统一包裹子目录
|
|
1667
1701
|
const isGit = workspaceLayout.isGitRepo(workspaceRoot);
|
|
@@ -1684,7 +1718,7 @@ async function main(options = {}) {
|
|
|
1684
1718
|
if (linked.ok) {
|
|
1685
1719
|
console.log(` ✓ sdd.specPath = ${linked.path}`);
|
|
1686
1720
|
} else {
|
|
1687
|
-
|
|
1721
|
+
throw new Error(`未能写入 sdd.specPath: ${linked.message}`);
|
|
1688
1722
|
}
|
|
1689
1723
|
} else {
|
|
1690
1724
|
// 非 Git 目录:仍创建包裹包,便于随后 git init
|
|
@@ -1713,7 +1747,7 @@ async function main(options = {}) {
|
|
|
1713
1747
|
// 7. 更新工作目录 .gitignore
|
|
1714
1748
|
updateGitignore(workspaceRoot);
|
|
1715
1749
|
|
|
1716
|
-
// 8.
|
|
1750
|
+
// 8. 收尾:清理可识别的旧工具,保留历史配置和数据
|
|
1717
1751
|
if (specPackage && fs.existsSync(specPackage.abs)) {
|
|
1718
1752
|
writeSddSpecRootHint(workspaceRoot, specPackage.abs);
|
|
1719
1753
|
}
|
|
@@ -1845,4 +1879,5 @@ module.exports = {
|
|
|
1845
1879
|
promptSpecPath,
|
|
1846
1880
|
listSpecCandidatesForInit,
|
|
1847
1881
|
deployCodeRepoHooks,
|
|
1882
|
+
updateGitignore,
|
|
1848
1883
|
};
|