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/lib/command-bridge.js
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
* KunlunZhima OPSX Command Bridge 生成与管理
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
5
7
|
const fs = require('fs');
|
|
6
8
|
const path = require('path');
|
|
7
|
-
const {
|
|
9
|
+
const { validateRenderedContent } = require('./skills-bundle');
|
|
10
|
+
const { STRATEGY, CONFLICT_POLICY, deployFile } = require('./deploy-strategy');
|
|
8
11
|
|
|
9
12
|
const BRIDGE_MARKER = '# SDD KunlunZhima Skill Bridge';
|
|
10
13
|
const BRIDGE_MAX_BYTES = 4096;
|
|
@@ -70,18 +73,27 @@ function isManagedBridgeFile(content) {
|
|
|
70
73
|
return typeof content === 'string' && content.includes(BRIDGE_MARKER);
|
|
71
74
|
}
|
|
72
75
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
76
|
+
/**
|
|
77
|
+
* 部署单个 bridge:受管标记文件才覆盖,用户文件 fail-closed。
|
|
78
|
+
*
|
|
79
|
+
* MARKER 策略的 marker 用 BRIDGE_MARKER,冲突时 ERROR(而非静默保留),
|
|
80
|
+
* 保持「命令文件冲突」的既有语义(由 deploy-strategy 统一实现冲突判定)。
|
|
81
|
+
*/
|
|
82
|
+
function deploySingleBridge(commandPath, commandName, content) {
|
|
83
|
+
const result = deployFile(null, commandPath, {
|
|
84
|
+
strategy: STRATEGY.MARKER,
|
|
85
|
+
marker: BRIDGE_MARKER,
|
|
86
|
+
conflictPolicy: CONFLICT_POLICY.ERROR,
|
|
87
|
+
conflictMessage:
|
|
88
|
+
`命令文件冲突: ${commandName}.md 已存在且不含 SDD 受管标记。`
|
|
89
|
+
+ '请手动备份后删除或重命名该文件,再重新初始化。',
|
|
90
|
+
label: `${commandName}.md`,
|
|
91
|
+
generate: () => content,
|
|
92
|
+
});
|
|
93
|
+
if (!result.ok) {
|
|
94
|
+
throw new Error(result.message);
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
85
97
|
}
|
|
86
98
|
|
|
87
99
|
function deployKunlunCommandBridges({ profile, projectRoot, pkgPath, skillMetadata }) {
|
|
@@ -107,10 +119,8 @@ function deployKunlunCommandBridges({ profile, projectRoot, pkgPath, skillMetada
|
|
|
107
119
|
|
|
108
120
|
for (const metadata of skillMetadata) {
|
|
109
121
|
const commandPath = getBridgeCommandPath(profile, projectRoot, metadata.commandName);
|
|
110
|
-
assertNoUserCommandConflict(commandPath, metadata.commandName);
|
|
111
|
-
|
|
112
122
|
const content = generateKunlunBridge(metadata, profile, projectRoot, pkgPath);
|
|
113
|
-
|
|
123
|
+
deploySingleBridge(commandPath, metadata.commandName, content);
|
|
114
124
|
deployed.push({
|
|
115
125
|
commandName: metadata.commandName,
|
|
116
126
|
path: commandPath,
|
|
@@ -152,5 +162,4 @@ module.exports = {
|
|
|
152
162
|
deployKunlunCommandBridges,
|
|
153
163
|
cleanupManagedKunlunBridges,
|
|
154
164
|
isManagedBridgeFile,
|
|
155
|
-
getBridgeCommandPath,
|
|
156
165
|
};
|
|
@@ -1,29 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Deploy CodeBuddy SDD Hook Pack to a target project.
|
|
3
|
+
*
|
|
4
|
+
* 策略:`.codebuddy/hooks/*` 是 SDD 私有目录下的工具产物 → OVERWRITE;
|
|
5
|
+
* `.codebuddy/settings.json` 是用户配置 → MERGE(保留用户 hooks 与未知字段)。
|
|
3
6
|
*/
|
|
4
7
|
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
5
10
|
const fs = require('fs');
|
|
6
11
|
const path = require('path');
|
|
7
|
-
const {
|
|
8
|
-
|
|
9
|
-
function copyFileSync(src, dest) {
|
|
10
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
11
|
-
fs.copyFileSync(src, dest);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function copyDirSync(srcDir, destDir) {
|
|
15
|
-
if (!fs.existsSync(srcDir)) return;
|
|
16
|
-
fs.mkdirSync(destDir, { recursive: true });
|
|
17
|
-
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
18
|
-
const srcPath = path.join(srcDir, entry.name);
|
|
19
|
-
const destPath = path.join(destDir, entry.name);
|
|
20
|
-
if (entry.isDirectory()) {
|
|
21
|
-
copyDirSync(srcPath, destPath);
|
|
22
|
-
} else {
|
|
23
|
-
copyFileSync(srcPath, destPath);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
12
|
+
const { mergeCodebuddySettingsFromTemplate } = require('./settings-merge');
|
|
13
|
+
const { STRATEGY, deployFile } = require('./deploy-strategy');
|
|
27
14
|
|
|
28
15
|
function validateExistingSettings(settingsPath) {
|
|
29
16
|
if (!fs.existsSync(settingsPath)) {
|
|
@@ -69,13 +56,21 @@ function deployCodebuddyHookPack(profile, cwd, pkgPath) {
|
|
|
69
56
|
const targetHookDir = path.join(codebuddyDir, 'hooks');
|
|
70
57
|
fs.mkdirSync(targetHookDir, { recursive: true });
|
|
71
58
|
|
|
59
|
+
// 工具产物 → OVERWRITE,随 init 升级
|
|
72
60
|
for (const file of fs.readdirSync(hookSourceDir)) {
|
|
73
61
|
if (file === 'hook-gate-core.cjs') continue;
|
|
74
|
-
|
|
62
|
+
deployFile(path.join(hookSourceDir, file), path.join(targetHookDir, file), {
|
|
63
|
+
strategy: STRATEGY.OVERWRITE,
|
|
64
|
+
label: file,
|
|
65
|
+
});
|
|
75
66
|
}
|
|
76
67
|
|
|
77
68
|
if (fs.existsSync(coreSourcePath)) {
|
|
78
|
-
|
|
69
|
+
// hook-gate-core.cjs 以 lib/ 为唯一来源,覆盖模板目录里的同名副本
|
|
70
|
+
deployFile(coreSourcePath, path.join(targetHookDir, 'hook-gate-core.cjs'), {
|
|
71
|
+
strategy: STRATEGY.OVERWRITE,
|
|
72
|
+
label: 'hook-gate-core.cjs',
|
|
73
|
+
});
|
|
79
74
|
} else {
|
|
80
75
|
return {
|
|
81
76
|
ok: false,
|
|
@@ -83,8 +78,8 @@ function deployCodebuddyHookPack(profile, cwd, pkgPath) {
|
|
|
83
78
|
};
|
|
84
79
|
}
|
|
85
80
|
|
|
86
|
-
|
|
87
|
-
const mergeResult =
|
|
81
|
+
// 用户配置 → MERGE:保留用户未知字段与自定义 hook
|
|
82
|
+
const mergeResult = mergeCodebuddySettingsFromTemplate(settingsTemplatePath, targetSettingsPath);
|
|
88
83
|
if (!mergeResult.ok) {
|
|
89
84
|
return mergeResult;
|
|
90
85
|
}
|
|
@@ -94,6 +89,4 @@ function deployCodebuddyHookPack(profile, cwd, pkgPath) {
|
|
|
94
89
|
|
|
95
90
|
module.exports = {
|
|
96
91
|
deployCodebuddyHookPack,
|
|
97
|
-
copyDirSync,
|
|
98
|
-
validateExistingSettings,
|
|
99
92
|
};
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* KLD SDD 部署策略抽象层
|
|
5
|
+
*
|
|
6
|
+
* 所有 init 向用户仓库写入文件的操作统一走这里,策略在调用点显式声明。
|
|
7
|
+
* 目的是消除「各部署函数各自手写 fs.copyFileSync / existsSync 判定」导致的行为漂移,
|
|
8
|
+
* 并让「这个文件是工具产物还是用户产物」在代码里可读。
|
|
9
|
+
*
|
|
10
|
+
* 策略一览(与产物归属的对应关系):
|
|
11
|
+
* - OVERWRITE 工具产物 / 只读模板 → 无条件覆盖
|
|
12
|
+
* - SKIP_IF_EXISTS 用户产物 / 用户配置 → 存在即跳过,绝不覆盖
|
|
13
|
+
* - MERGE 结构化配置(settings.json / .sdd.yaml)→ 委托 mergeFn 保守合并
|
|
14
|
+
* - MARKER 与用户共用命名空间(.git/hooks)→ 含 marker 才覆盖,否则保留
|
|
15
|
+
* - APPEND_LINES 追加式清单(.gitignore)→ 只补缺失行
|
|
16
|
+
* - GENERATE 由生成器产出(runtime-metadata.cjs / .sdd-spec-root)→ 总是重写
|
|
17
|
+
*
|
|
18
|
+
* 设计约束:本模块只在 init 期使用,不得被 skywalk-sdd/ 下的运行时脚本 require
|
|
19
|
+
* (运行时脚本会被部署到用户仓库,不应依赖 lib/)。
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
|
|
25
|
+
/** 部署策略 */
|
|
26
|
+
const STRATEGY = Object.freeze({
|
|
27
|
+
/** 无条件覆盖:工具产物、只读模板 */
|
|
28
|
+
OVERWRITE: 'overwrite',
|
|
29
|
+
/** 存在则跳过:用户产物、用户配置 */
|
|
30
|
+
SKIP_IF_EXISTS: 'skip-if-exists',
|
|
31
|
+
/** 保守合并:保留用户已有字段,只注入受管内容 */
|
|
32
|
+
MERGE: 'merge',
|
|
33
|
+
/** marker 判定:含受管标记才覆盖,否则保留 */
|
|
34
|
+
MARKER: 'marker',
|
|
35
|
+
/** 追加缺失行:用于 .gitignore 这类清单文件 */
|
|
36
|
+
APPEND_LINES: 'append-lines',
|
|
37
|
+
/** 生成器产出:内容由代码生成,总是重写 */
|
|
38
|
+
GENERATE: 'generate',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/** 部署动作(用于调用点决定日志与统计) */
|
|
42
|
+
const ACTION = Object.freeze({
|
|
43
|
+
CREATED: 'created',
|
|
44
|
+
OVERWRITTEN: 'overwritten',
|
|
45
|
+
SKIPPED: 'skipped',
|
|
46
|
+
MERGED: 'merged',
|
|
47
|
+
APPENDED: 'appended',
|
|
48
|
+
GENERATED: 'generated',
|
|
49
|
+
PRESERVED: 'preserved',
|
|
50
|
+
FAILED: 'failed',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/** 冲突处理:MARKER 策略遇到非受管文件时,保留还是报错 */
|
|
54
|
+
const CONFLICT_POLICY = Object.freeze({
|
|
55
|
+
PRESERVE: 'preserve',
|
|
56
|
+
ERROR: 'error',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/** 默认 marker:SDD shell hook 的受管标记 */
|
|
60
|
+
const DEFAULT_HOOK_MARKER = 'KLD SDD quality gate';
|
|
61
|
+
|
|
62
|
+
function ensureParentDir(filePath) {
|
|
63
|
+
const dir = path.dirname(filePath);
|
|
64
|
+
if (!fs.existsSync(dir)) {
|
|
65
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
return dir;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readIfExists(filePath) {
|
|
71
|
+
if (!fs.existsSync(filePath)) return null;
|
|
72
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 解析要写入的内容。
|
|
77
|
+
* - 提供 render:调用 render(srcPath, tgtPath) 取内容(模板渲染场景)
|
|
78
|
+
* - 否则从 source 读取原文
|
|
79
|
+
*/
|
|
80
|
+
function resolveContent(source, target, options) {
|
|
81
|
+
if (typeof options.render === 'function') {
|
|
82
|
+
return options.render(source, target);
|
|
83
|
+
}
|
|
84
|
+
if (!source) {
|
|
85
|
+
throw new Error(`部署需要 source 或 render 之一: ${target}`);
|
|
86
|
+
}
|
|
87
|
+
return fs.readFileSync(source, 'utf8');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 部署单个文件。
|
|
92
|
+
*
|
|
93
|
+
* @param {string|null} source 源文件绝对路径(GENERATE / render 场景可为 null)
|
|
94
|
+
* @param {string} target 目标文件绝对路径
|
|
95
|
+
* @param {object} [options]
|
|
96
|
+
* @param {string} [options.strategy] STRATEGY 之一,默认 OVERWRITE
|
|
97
|
+
* @param {string} [options.label] 日志用文件名,默认 target 的 basename
|
|
98
|
+
* @param {function} [options.render] (srcPath, tgtPath) => string,模板渲染
|
|
99
|
+
* @param {function} [options.mergeFn] ({source,target,readSource,readTarget}) => {ok, action?, error?}
|
|
100
|
+
* @param {string} [options.marker] MARKER 策略的受管标记
|
|
101
|
+
* @param {string} [options.conflictPolicy] MARKER 策略冲突处理,默认 PRESERVE
|
|
102
|
+
* @param {string} [options.conflictMessage] MARKER 冲突且 policy=ERROR 时的自定义报错文案
|
|
103
|
+
* @param {string[]} [options.lines] APPEND_LINES 策略的必需行清单
|
|
104
|
+
* @param {string} [options.block] APPEND_LINES 策略在目标不存在时写入的完整块
|
|
105
|
+
* @param {string} [options.requiresMarker] APPEND_LINES 策略的块级标记(无标记则追加整块)
|
|
106
|
+
* @param {string} [options.supplementHeader] APPEND_LINES 策略补行时的段落标题
|
|
107
|
+
* @param {number} [options.mode] 写文件权限(如 0o755),仅新建时尝试
|
|
108
|
+
* @returns {{ok: boolean, action: string, target: string, label: string, message?: string}}
|
|
109
|
+
*/
|
|
110
|
+
function deployFile(source, target, options = {}) {
|
|
111
|
+
const strategy = options.strategy || STRATEGY.OVERWRITE;
|
|
112
|
+
const label = options.label || path.basename(target);
|
|
113
|
+
const done = (action, message) => ({ ok: true, action, target, label, message });
|
|
114
|
+
const failed = (message) => ({ ok: false, action: ACTION.FAILED, target, label, message });
|
|
115
|
+
|
|
116
|
+
switch (strategy) {
|
|
117
|
+
case STRATEGY.OVERWRITE: {
|
|
118
|
+
const existed = fs.existsSync(target);
|
|
119
|
+
ensureParentDir(target);
|
|
120
|
+
if (typeof options.render === 'function') {
|
|
121
|
+
fs.writeFileSync(target, resolveContent(source, target, options), 'utf8');
|
|
122
|
+
} else {
|
|
123
|
+
if (!source) return failed('OVERWRITE 策略需要 source 或 render');
|
|
124
|
+
fs.copyFileSync(source, target);
|
|
125
|
+
}
|
|
126
|
+
applyMode(target, options);
|
|
127
|
+
return done(existed ? ACTION.OVERWRITTEN : ACTION.CREATED);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
case STRATEGY.SKIP_IF_EXISTS: {
|
|
131
|
+
if (fs.existsSync(target)) {
|
|
132
|
+
return done(ACTION.SKIPPED);
|
|
133
|
+
}
|
|
134
|
+
ensureParentDir(target);
|
|
135
|
+
if (typeof options.render === 'function') {
|
|
136
|
+
fs.writeFileSync(target, resolveContent(source, target, options), 'utf8');
|
|
137
|
+
} else if (options.generate != null) {
|
|
138
|
+
fs.writeFileSync(target, options.generate(), 'utf8');
|
|
139
|
+
} else {
|
|
140
|
+
if (!source) return failed('SKIP_IF_EXISTS 策略需要 source 或 render/generate');
|
|
141
|
+
fs.copyFileSync(source, target);
|
|
142
|
+
}
|
|
143
|
+
applyMode(target, options);
|
|
144
|
+
return done(ACTION.CREATED);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
case STRATEGY.MERGE: {
|
|
148
|
+
if (typeof options.mergeFn !== 'function') {
|
|
149
|
+
return failed('MERGE 策略需要 mergeFn');
|
|
150
|
+
}
|
|
151
|
+
ensureParentDir(target);
|
|
152
|
+
const result = options.mergeFn({
|
|
153
|
+
source,
|
|
154
|
+
target,
|
|
155
|
+
readSource: () => {
|
|
156
|
+
if (!source) throw new Error(`MERGE 策略 readSource 需要 source: ${target}`);
|
|
157
|
+
return fs.readFileSync(source, 'utf8');
|
|
158
|
+
},
|
|
159
|
+
readTarget: () => readIfExists(target),
|
|
160
|
+
});
|
|
161
|
+
if (!result || result.ok !== true) {
|
|
162
|
+
return failed((result && result.error) || '合并失败');
|
|
163
|
+
}
|
|
164
|
+
return done(result.action || ACTION.MERGED, result.message);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
case STRATEGY.MARKER: {
|
|
168
|
+
const marker = options.marker || DEFAULT_HOOK_MARKER;
|
|
169
|
+
const existing = readIfExists(target);
|
|
170
|
+
if (existing !== null && !existing.includes(marker)) {
|
|
171
|
+
if (options.conflictPolicy === CONFLICT_POLICY.ERROR) {
|
|
172
|
+
return failed(
|
|
173
|
+
options.conflictMessage
|
|
174
|
+
|| `${label} 已存在且不含 SDD 受管标记。请手动备份后删除或重命名该文件,再重新初始化。`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return done(ACTION.PRESERVED, '已存在且非 SDD 受管,已保留不覆盖');
|
|
178
|
+
}
|
|
179
|
+
ensureParentDir(target);
|
|
180
|
+
const content = options.generate != null
|
|
181
|
+
? options.generate()
|
|
182
|
+
: resolveContent(source, target, options);
|
|
183
|
+
fs.writeFileSync(target, content, 'utf8');
|
|
184
|
+
applyMode(target, options);
|
|
185
|
+
return done(existing === null ? ACTION.CREATED : ACTION.OVERWRITTEN);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
case STRATEGY.APPEND_LINES: {
|
|
189
|
+
const lines = Array.isArray(options.lines) ? options.lines : [];
|
|
190
|
+
const existing = readIfExists(target);
|
|
191
|
+
|
|
192
|
+
if (existing === null) {
|
|
193
|
+
ensureParentDir(target);
|
|
194
|
+
const initial = options.block != null ? `${String(options.block).trim()}\n` : `${lines.join('\n')}\n`;
|
|
195
|
+
fs.writeFileSync(target, initial, 'utf8');
|
|
196
|
+
return done(ACTION.CREATED);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 块级标记缺失 → 视为从未注入,追加整块
|
|
200
|
+
const marker = options.requiresMarker;
|
|
201
|
+
if (marker && !existing.includes(marker)) {
|
|
202
|
+
if (options.block == null) return done(ACTION.SKIPPED);
|
|
203
|
+
fs.appendFileSync(target, String(options.block));
|
|
204
|
+
return done(ACTION.APPENDED, '已追加完整忽略块');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const missing = lines.filter((line) => line && !existing.includes(line));
|
|
208
|
+
if (missing.length === 0) {
|
|
209
|
+
return done(ACTION.SKIPPED);
|
|
210
|
+
}
|
|
211
|
+
const header = options.supplementHeader ? `\n${options.supplementHeader}\n` : '\n';
|
|
212
|
+
fs.appendFileSync(target, `${header}${missing.join('\n')}\n`);
|
|
213
|
+
return done(ACTION.APPENDED, `补充 ${missing.length} 行`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
case STRATEGY.GENERATE: {
|
|
217
|
+
if (typeof options.generate !== 'function') {
|
|
218
|
+
return failed('GENERATE 策略需要 generate');
|
|
219
|
+
}
|
|
220
|
+
ensureParentDir(target);
|
|
221
|
+
fs.writeFileSync(target, options.generate(), 'utf8');
|
|
222
|
+
applyMode(target, options);
|
|
223
|
+
return done(ACTION.GENERATED);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
default:
|
|
227
|
+
return failed(`未知部署策略: ${strategy}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function applyMode(target, options) {
|
|
232
|
+
if (!options.mode) return;
|
|
233
|
+
try {
|
|
234
|
+
fs.chmodSync(target, options.mode);
|
|
235
|
+
} catch {
|
|
236
|
+
// Windows 上 chmod 可能没有实际效果
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* 递归部署目录。文件级策略沿用 options,可额外提供 render 做模板渲染。
|
|
242
|
+
*
|
|
243
|
+
* @param {string} source 源目录
|
|
244
|
+
* @param {string} target 目标目录
|
|
245
|
+
* @param {object} [options] 同 deployFile
|
|
246
|
+
* @returns {Array} 每个文件的部署结果
|
|
247
|
+
*/
|
|
248
|
+
function deployDir(source, target, options = {}) {
|
|
249
|
+
const results = [];
|
|
250
|
+
if (!fs.existsSync(source)) {
|
|
251
|
+
return results;
|
|
252
|
+
}
|
|
253
|
+
if (!fs.existsSync(target)) {
|
|
254
|
+
fs.mkdirSync(target, { recursive: true });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
for (const name of fs.readdirSync(source)) {
|
|
258
|
+
const srcPath = path.join(source, name);
|
|
259
|
+
const tgtPath = path.join(target, name);
|
|
260
|
+
if (fs.statSync(srcPath).isDirectory()) {
|
|
261
|
+
results.push(...deployDir(srcPath, tgtPath, options));
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
results.push(deployFile(srcPath, tgtPath, { ...options, label: options.label || name }));
|
|
265
|
+
}
|
|
266
|
+
return results;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** 统计各动作数量,供调用点输出摘要 */
|
|
270
|
+
function summarize(results) {
|
|
271
|
+
const counts = {};
|
|
272
|
+
for (const item of results) {
|
|
273
|
+
const key = item.action || ACTION.FAILED;
|
|
274
|
+
counts[key] = (counts[key] || 0) + 1;
|
|
275
|
+
}
|
|
276
|
+
return counts;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** 过滤出失败项 */
|
|
280
|
+
function failures(results) {
|
|
281
|
+
return results.filter((item) => !item.ok);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
module.exports = {
|
|
285
|
+
STRATEGY,
|
|
286
|
+
ACTION,
|
|
287
|
+
CONFLICT_POLICY,
|
|
288
|
+
DEFAULT_HOOK_MARKER,
|
|
289
|
+
deployFile,
|
|
290
|
+
deployDir,
|
|
291
|
+
summarize,
|
|
292
|
+
failures,
|
|
293
|
+
};
|
package/lib/hook-gate-core.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared SDD hook gate utilities for Claude and CodeBuddy adapters.
|
|
3
|
+
*
|
|
4
|
+
* 注意:本文件在 templates/hooks/codebuddy/hooks/hook-gate-core.cjs 有一份
|
|
5
|
+
* 逐字节相同的副本(模板 hook 以 ./hook-gate-core.cjs 相对引用,需可就地 require)。
|
|
6
|
+
* 修改本文件后必须同步副本,否则 test/codebuddy-hooks.cjs UT-022 漂移守卫会失败。
|
|
3
7
|
*/
|
|
4
8
|
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
5
11
|
const fs = require('fs');
|
|
6
12
|
const path = require('path');
|
|
7
13
|
|
|
@@ -666,7 +672,6 @@ function detectTddBatchPattern(events, sinceTimestamp, tasksMdContent) {
|
|
|
666
672
|
}
|
|
667
673
|
|
|
668
674
|
module.exports = {
|
|
669
|
-
PROJECT_ENV_KEYS,
|
|
670
675
|
readStdin,
|
|
671
676
|
parseHookInput,
|
|
672
677
|
normalizeHookInput,
|
|
@@ -685,12 +690,6 @@ module.exports = {
|
|
|
685
690
|
evaluateApplyWriteGate,
|
|
686
691
|
evaluateSkillApplyGate,
|
|
687
692
|
evaluatePreToolDangerGate,
|
|
688
|
-
// TDD
|
|
689
|
-
extractTddTestEvents,
|
|
690
|
-
buildPairMap,
|
|
691
|
-
detectConsecutiveReds,
|
|
692
|
-
detectInterleavingViolation,
|
|
693
|
-
detectTimestampClustering,
|
|
694
|
-
detectRapidPairing,
|
|
693
|
+
// TDD 批量执行检测(子检测器仅内部使用,对外只暴露聚合入口)
|
|
695
694
|
detectTddBatchPattern,
|
|
696
695
|
};
|