kld-sdd 2.7.8-2 → 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/README.md +2 -0
- package/USABILITY.md +52 -0
- 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/hook-install.js +75 -0
- package/lib/init.js +450 -711
- package/lib/managed-hook-hashes.json +24 -0
- 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/index.cjs +1 -1
- package/skywalk-sdd/kb-upload.cjs +64 -49
- package/skywalk-sdd/lib/shared.cjs +62 -41
- package/skywalk-sdd/lib/test-execution.cjs +20 -0
- 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/git-hooks/commit-msg +17 -99
- 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 +61 -240
- 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-consistency-check.cjs +13 -31
- package/templates/git-hooks/pre-push-consistency-check.cjs +36 -83
- package/templates/hooks/codebuddy/hooks/hook-gate-core.cjs +7 -8
- package/templates/openspec/proposal.md +1 -1
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +24 -22
- 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 +157 -11
- package/index.js +0 -13
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pre-commit": [
|
|
3
|
+
"497f012da027df5c26af3c501126e9a1c26276b038ee3b1edaaf5ab4401aa95c",
|
|
4
|
+
"7322d9aecafb497a5f40171a6bd7daeaadd7d2056b4a253cb0ee1a128faea36e",
|
|
5
|
+
"8fcdd74a3d2afaa823f9038c477c9dfd6fe53e2bccbe48cecd01535794862e4a",
|
|
6
|
+
"c8546c6c96adc3ecc32a15a689c59189543c2454f47998c46fbd5a0bf810cf30",
|
|
7
|
+
"e3b6776af51df76ff04b528152d0049a6c45d94d96a19bd2bc5ca2cc9786b22c"
|
|
8
|
+
],
|
|
9
|
+
"pre-push": [
|
|
10
|
+
"405debb5e6f9186e95b727e6013b06ef3e06c598b6d2b96678d34554d3530e8c",
|
|
11
|
+
"461abd16663757c5a092103ac2ecae8453891c0c817d1c763369e0cdd0a8ec7b",
|
|
12
|
+
"80f710ff647ce8b216fb1c081fb3e22f6ee8be25847b6d02921668ba83fb1965",
|
|
13
|
+
"d49eb8794fde4b0d7847a876cc7083d161b290a44990ff8a036c2ebb8a00ec21",
|
|
14
|
+
"e6e125cbf9d7424c01b249ce03197cef9e1923ed2974a53656084488a0de60c4",
|
|
15
|
+
"ebf628481ae93ac0c4c6649b204449f6825291dc6b6550a71563bebec1f3ad92"
|
|
16
|
+
],
|
|
17
|
+
"commit-msg": [
|
|
18
|
+
"15083d5165bff965ab2feba9a55dfedcecd54cb577c06fd11f4d1b9672d978b6",
|
|
19
|
+
"3b0ee15f5a126d159e14c5a3681cb7820cbf9c5e0c134ee6812053bc34330b85",
|
|
20
|
+
"6db7adbf17905bf14915d7e57a3c5ac46030ee3282fea5d5b211c80065406d48",
|
|
21
|
+
"9c5a86761b36d71b655f804a822eff675a244175799736c803e1f7379c638609",
|
|
22
|
+
"fc390776720393f100712fa02d5e143c2927fbb0d605a4a0242824ff7ea84574"
|
|
23
|
+
]
|
|
24
|
+
}
|
package/lib/settings-merge.js
CHANGED
|
@@ -1,19 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 编辑器 settings.json 中 SDD 受管 hook 的识别、合并与摘除。
|
|
3
|
+
*
|
|
4
|
+
* 策略归属:`.claude/settings.json` 与 `.codebuddy/settings.json` 都是**用户配置** → MERGE。
|
|
5
|
+
* - 安装:保留用户未知字段与自定义 hook,只剔除并重装 SDD 受管 hook(幂等)
|
|
6
|
+
* - 卸载:只摘除 SDD 受管 hook,文件其余内容原样保留
|
|
7
|
+
*
|
|
8
|
+
* 两个 provider 的 hook 判定集中在此模块,保证「装得进去的一定摘得掉」。
|
|
3
9
|
*/
|
|
4
10
|
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
5
13
|
const fs = require('fs');
|
|
6
14
|
const path = require('path');
|
|
15
|
+
const { STRATEGY, ACTION, deployFile } = require('./deploy-strategy');
|
|
16
|
+
|
|
17
|
+
/** CodeBuddy 受管 hook:$CODEBUDDY_PROJECT_DIR/.codebuddy/hooks/sdd-*.cjs */
|
|
18
|
+
const SDD_CODEBUDDY_HOOK_COMMAND_RE = /(?:^|\s|["'])\$?CODEBUDDY_PROJECT_DIR[\\/]+\.codebuddy[\\/]+hooks[\\/]+sdd-[\w-]+\.cjs(?=$|\s|["'])/i;
|
|
19
|
+
|
|
20
|
+
/** Claude 受管 hook:.claude/hooks/sdd-*.cjs(含 ${CLAUDE_PROJECT_DIR} 形式) */
|
|
21
|
+
const SDD_CLAUDE_HOOK_COMMAND_RE = /(?:^|\s|["'])(?:\$\{CLAUDE_PROJECT_DIR\}[\\/]+)?\.claude[\\/]+hooks[\\/]+sdd-(?:prompt|pre-tool|post-tool|stop|apply-gate|apply-test-gate|skill-apply-gate)\.(?:cjs|js)(?=$|\s|["'])/i;
|
|
7
22
|
|
|
8
|
-
const
|
|
23
|
+
const PROVIDERS = Object.freeze({
|
|
24
|
+
CLAUDE: 'claude',
|
|
25
|
+
CODEBUDDY: 'codebuddy',
|
|
26
|
+
});
|
|
9
27
|
|
|
10
28
|
function hookCommandText(hook) {
|
|
11
29
|
const args = Array.isArray(hook.args) ? hook.args : [];
|
|
12
30
|
return [hook.command, ...args].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim();
|
|
13
31
|
}
|
|
14
32
|
|
|
15
|
-
|
|
16
|
-
|
|
33
|
+
/** 判断单个 hook 是否 SDD 受管(按 provider 选择判定规则) */
|
|
34
|
+
function isSddHook(hook, provider) {
|
|
35
|
+
const text = hookCommandText(hook);
|
|
36
|
+
if (provider === PROVIDERS.CLAUDE) return SDD_CLAUDE_HOOK_COMMAND_RE.test(text);
|
|
37
|
+
return SDD_CODEBUDDY_HOOK_COMMAND_RE.test(text);
|
|
17
38
|
}
|
|
18
39
|
|
|
19
40
|
function sameHookCommand(left, right) {
|
|
@@ -21,36 +42,56 @@ function sameHookCommand(left, right) {
|
|
|
21
42
|
String(left.type || '') === String(right.type || '');
|
|
22
43
|
}
|
|
23
44
|
|
|
24
|
-
|
|
25
|
-
|
|
45
|
+
/**
|
|
46
|
+
* 从 hook 条目数组中剔除 SDD 受管 hook,保留用户自定义条目。
|
|
47
|
+
* 条目内 hooks 被清空时整条移除。
|
|
48
|
+
*/
|
|
49
|
+
function removeManagedHooks(entries, provider) {
|
|
50
|
+
return (entries || [])
|
|
26
51
|
.map(entry => {
|
|
27
52
|
if (!Array.isArray(entry.hooks)) {
|
|
28
53
|
return entry;
|
|
29
54
|
}
|
|
30
55
|
return {
|
|
31
56
|
...entry,
|
|
32
|
-
hooks: entry.hooks.filter(hook => !
|
|
57
|
+
hooks: entry.hooks.filter(hook => !isSddHook(hook, provider)),
|
|
33
58
|
};
|
|
34
59
|
})
|
|
35
60
|
.filter(entry => !Array.isArray(entry.hooks) || entry.hooks.length > 0);
|
|
36
61
|
}
|
|
37
62
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
63
|
+
/**
|
|
64
|
+
* 纯计算:摘除 settings 对象中全部 SDD 受管 hook(卸载路径)。
|
|
65
|
+
* 返回新对象,不修改入参;hooks 变空时保留空对象结构,避免下游读取报错。
|
|
66
|
+
*/
|
|
67
|
+
function stripManagedHooks(settings, provider) {
|
|
68
|
+
const next = { ...settings };
|
|
69
|
+
if (!next.hooks || typeof next.hooks !== 'object') {
|
|
70
|
+
return next;
|
|
71
|
+
}
|
|
72
|
+
const hooks = {};
|
|
73
|
+
for (const [eventName, entries] of Object.entries(next.hooks)) {
|
|
74
|
+
hooks[eventName] = removeManagedHooks(entries, provider);
|
|
43
75
|
}
|
|
76
|
+
next.hooks = hooks;
|
|
77
|
+
return next;
|
|
78
|
+
}
|
|
44
79
|
|
|
80
|
+
/**
|
|
81
|
+
* 纯计算:把模板 hooks 合并进已有配置,保留用户未知字段与自定义 hook。
|
|
82
|
+
*
|
|
83
|
+
* @param {string|null} existingRaw 现有 settings.json 原文(不存在传 null)
|
|
84
|
+
* @param {object} templateSettings 模板 settings 对象
|
|
85
|
+
* @param {string} [provider] 受管 hook 归属,默认 codebuddy
|
|
86
|
+
* @returns {{ok: true, merged: object} | {ok: false, error: string}}
|
|
87
|
+
*/
|
|
88
|
+
function computeMergedSettings(existingRaw, templateSettings, provider = PROVIDERS.CODEBUDDY) {
|
|
45
89
|
let existingSettings = {};
|
|
46
|
-
if (
|
|
90
|
+
if (existingRaw) {
|
|
47
91
|
try {
|
|
48
|
-
existingSettings = JSON.parse(
|
|
92
|
+
existingSettings = JSON.parse(existingRaw);
|
|
49
93
|
} catch {
|
|
50
|
-
return {
|
|
51
|
-
ok: false,
|
|
52
|
-
error: `${displayPath} 不是合法 JSON,请修复后重试(路径: ${settingsPath})`,
|
|
53
|
-
};
|
|
94
|
+
return { ok: false, error: '不是合法 JSON' };
|
|
54
95
|
}
|
|
55
96
|
}
|
|
56
97
|
|
|
@@ -58,7 +99,7 @@ function mergeCodebuddySettings(settingsPath, templateSettings) {
|
|
|
58
99
|
merged.hooks = merged.hooks || {};
|
|
59
100
|
|
|
60
101
|
for (const [eventName, entries] of Object.entries(templateSettings.hooks || {})) {
|
|
61
|
-
merged.hooks[eventName] =
|
|
102
|
+
merged.hooks[eventName] = removeManagedHooks(merged.hooks[eventName], provider);
|
|
62
103
|
for (const entry of entries) {
|
|
63
104
|
const templateHooks = entry.hooks || [];
|
|
64
105
|
const exists = merged.hooks[eventName].some(existingEntry => {
|
|
@@ -73,13 +114,49 @@ function mergeCodebuddySettings(settingsPath, templateSettings) {
|
|
|
73
114
|
}
|
|
74
115
|
}
|
|
75
116
|
|
|
76
|
-
|
|
77
|
-
|
|
117
|
+
return { ok: true, merged };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 从模板文件部署 settings.json(用户配置 → MERGE)。
|
|
122
|
+
* 所有写盘经 deploy-strategy,非法 JSON 时 fail-closed 并保持原字节不变。
|
|
123
|
+
*
|
|
124
|
+
* @returns {{ok: boolean, action?: string, error?: string}}
|
|
125
|
+
*/
|
|
126
|
+
function mergeCodebuddySettingsFromTemplate(sourcePath, targetPath) {
|
|
127
|
+
const displayPath = path.basename(targetPath);
|
|
128
|
+
let templateSettings;
|
|
129
|
+
try {
|
|
130
|
+
templateSettings = JSON.parse(fs.readFileSync(sourcePath, 'utf8'));
|
|
131
|
+
} catch (error) {
|
|
132
|
+
return { ok: false, error: `模板 ${displayPath} 无法解析: ${error.message}` };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const result = deployFile(sourcePath, targetPath, {
|
|
136
|
+
strategy: STRATEGY.MERGE,
|
|
137
|
+
label: displayPath,
|
|
138
|
+
mergeFn: ({ readTarget }) => {
|
|
139
|
+
const computed = computeMergedSettings(readTarget(), templateSettings, PROVIDERS.CODEBUDDY);
|
|
140
|
+
if (!computed.ok) {
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
error: `${displayPath} 不是合法 JSON,请修复后重试(路径: ${targetPath})`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
fs.writeFileSync(targetPath, JSON.stringify(computed.merged, null, 2) + '\n', 'utf8');
|
|
147
|
+
return { ok: true, action: ACTION.MERGED };
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
if (!result.ok) {
|
|
152
|
+
return { ok: false, error: result.message };
|
|
153
|
+
}
|
|
154
|
+
return { ok: true, action: result.action };
|
|
78
155
|
}
|
|
79
156
|
|
|
80
157
|
module.exports = {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
158
|
+
PROVIDERS,
|
|
159
|
+
mergeCodebuddySettingsFromTemplate,
|
|
160
|
+
computeMergedSettings,
|
|
161
|
+
stripManagedHooks,
|
|
85
162
|
};
|
package/lib/skills-bundle.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* SDD skills bundle 共享配置与模板渲染
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
5
7
|
const fs = require('fs');
|
|
6
8
|
const path = require('path');
|
|
7
9
|
|
|
@@ -186,10 +188,6 @@ module.exports = {
|
|
|
186
188
|
OPSX_SKILL_METADATA,
|
|
187
189
|
buildOpsxSkillMetadata,
|
|
188
190
|
commandNameFromSkillDir,
|
|
189
|
-
displayNameFromCommandName,
|
|
190
|
-
parseSkillFrontmatter,
|
|
191
191
|
renderTemplate,
|
|
192
|
-
replaceProfileTokens,
|
|
193
|
-
transformKunlunSlashCommands,
|
|
194
192
|
validateRenderedContent,
|
|
195
193
|
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* spec 候选的单选交互 —— init / uninstall / sync-repos 三个场景**共用**。
|
|
5
|
+
*
|
|
6
|
+
* 为什么需要用户裁决:判据无法区分「当前项目的 spec 仓」与「用户手动复制的
|
|
7
|
+
* 备份」(如 `erp-sdd-specs copy`)—— 二者都命中。故由用户单选,而不是静默
|
|
8
|
+
* 挑一个:挑错的代价是 init 把内容灌进错误目录、uninstall 删错仓、
|
|
9
|
+
* sync-repos 把错误路径写进每个代码仓的 `git config`。
|
|
10
|
+
*
|
|
11
|
+
* 归属 lib/ 而非 bin/:init 的交互发生在 `lib/init.js#main()` 内部,uninstall
|
|
12
|
+
* 与 sync-repos 的交互在 bin/ 层。放在 lib/ 使两侧都能复用,且依赖方向正确
|
|
13
|
+
*(bin → lib,不反向)。
|
|
14
|
+
*
|
|
15
|
+
* 交互约定(三个场景一致):
|
|
16
|
+
* - 0 个候选 → 返回 undefined(调用方决定「要求输入路径」还是「报错退出」)
|
|
17
|
+
* - 1 个候选 → 确认(回车默认 Y)
|
|
18
|
+
* - N 个候选 → 列出(标注来源)让用户单选
|
|
19
|
+
*
|
|
20
|
+
* 实现约束:候选信息必须**并入 question 字符串**,不能靠提问前的 console.log。
|
|
21
|
+
* readline 重绘提示行时会发 \x1b[0J(清除光标之后内容),会把前置输出抹掉,
|
|
22
|
+
* 导致用户看不到自己在确认哪个目录。
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const { specSourceLabel } = require('../skywalk-sdd/ontology/spec-detect.cjs');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 让用户在 spec 候选集合中单选。
|
|
29
|
+
*
|
|
30
|
+
* @param {{ask: (q: string) => Promise<string>}} prompt readline 会话(只需 ask)
|
|
31
|
+
* @param {Array<{abs: string, name: string, source: string, tier?: number}>} candidates
|
|
32
|
+
* 候选集合(由 spec-detect 枚举,已按 tier 排序)
|
|
33
|
+
* @param {{actionLabel?: string}} [options]
|
|
34
|
+
* actionLabel:提问文案里的动作词(默认「卸载」;init 用「使用」、
|
|
35
|
+
* sync-repos 用「接入」)
|
|
36
|
+
* @returns {Promise<string|null|undefined>}
|
|
37
|
+
* string=选中的绝对路径;null=用户拒绝/取消;undefined=无候选
|
|
38
|
+
*/
|
|
39
|
+
async function selectSpecRoot(prompt, candidates, options = {}) {
|
|
40
|
+
if (!candidates || candidates.length === 0) return undefined;
|
|
41
|
+
const action = options.actionLabel || '卸载';
|
|
42
|
+
|
|
43
|
+
if (candidates.length === 1) {
|
|
44
|
+
const only = candidates[0];
|
|
45
|
+
const question = [
|
|
46
|
+
`\n🔍 检测到 spec 仓库: ${only.name} [${specSourceLabel(only.source)}]`,
|
|
47
|
+
` 位置: ${only.abs}`,
|
|
48
|
+
`是否${action}该 spec 仓库?(Y=是 / n=重新选择,默认 Y): `,
|
|
49
|
+
].join('\n');
|
|
50
|
+
while (true) {
|
|
51
|
+
const answer = await prompt.ask(question);
|
|
52
|
+
const normalized = String(answer || '').trim().toLowerCase();
|
|
53
|
+
if (!normalized || normalized === 'y' || normalized === 'yes') return only.abs;
|
|
54
|
+
if (normalized === 'n' || normalized === 'no') break;
|
|
55
|
+
console.log(`❌ 无效输入 "${answer}",请输入 Y 或 n`);
|
|
56
|
+
}
|
|
57
|
+
// 拒绝唯一候选 → 无其它候选可选,交由调用方决定后续(重新选择 / 输入路径)
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
while (true) {
|
|
62
|
+
const question = [
|
|
63
|
+
`\n检测到多个 spec 仓库,请选择要${action}的一个:`,
|
|
64
|
+
...candidates.map(
|
|
65
|
+
(c, i) => ` ${i + 1}. ${c.name} [${specSourceLabel(c.source)}]\n ${c.abs}`,
|
|
66
|
+
),
|
|
67
|
+
' 0. 取消',
|
|
68
|
+
`\n请输入选项 (0-${candidates.length}): `,
|
|
69
|
+
].join('\n');
|
|
70
|
+
|
|
71
|
+
const answer = await prompt.ask(question);
|
|
72
|
+
const trimmed = String(answer || '').trim();
|
|
73
|
+
if (trimmed === '0' || /^(q|quit|exit)$/i.test(trimmed)) return null;
|
|
74
|
+
|
|
75
|
+
const index = Number(trimmed);
|
|
76
|
+
if (Number.isInteger(index) && index >= 1 && index <= candidates.length) {
|
|
77
|
+
const chosen = candidates[index - 1];
|
|
78
|
+
console.log(`✓ 已选择: ${chosen.name}\n`);
|
|
79
|
+
return chosen.abs;
|
|
80
|
+
}
|
|
81
|
+
console.log(`❌ 无效选项 "${answer}",请输入 0-${candidates.length} 之间的数字\n`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { selectSpecRoot };
|
package/lib/tool-profiles.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Single source of truth for per-agent paths and capabilities.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
6
8
|
const path = require('path');
|
|
7
9
|
|
|
8
10
|
const TOOL_PROFILES = Object.freeze({
|
|
@@ -272,7 +274,5 @@ module.exports = {
|
|
|
272
274
|
getToolProfile,
|
|
273
275
|
listToolProfileIds,
|
|
274
276
|
validateToolProfiles,
|
|
275
|
-
isSafeRelativePath,
|
|
276
277
|
buildToolConfigs,
|
|
277
|
-
DEFAULT_TOOL_DISPLAY_NAMES,
|
|
278
278
|
};
|