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
package/lib/init.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* 5. 更新 .gitignore
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
12
14
|
const fs = require('fs');
|
|
13
15
|
const path = require('path');
|
|
14
16
|
const { execSync } = require('child_process');
|
|
@@ -16,7 +18,6 @@ const readline = require('readline');
|
|
|
16
18
|
const os = require('os');
|
|
17
19
|
const {
|
|
18
20
|
SKILLS_BUNDLE_NAME,
|
|
19
|
-
OPSX_SKILL_DIRS,
|
|
20
21
|
OPSX_SKILL_METADATA,
|
|
21
22
|
renderTemplate,
|
|
22
23
|
validateRenderedContent,
|
|
@@ -28,8 +29,19 @@ const {
|
|
|
28
29
|
BRIDGE_MARKER,
|
|
29
30
|
} = require('./command-bridge');
|
|
30
31
|
const { deployCodebuddyHookPack } = require('./deploy-codebuddy-hooks');
|
|
32
|
+
const { PROVIDERS, computeMergedSettings } = require('./settings-merge');
|
|
31
33
|
const workspaceLayout = require('./workspace-layout');
|
|
32
34
|
const sddConfig = require('../skywalk-sdd/ontology/sdd-config.cjs');
|
|
35
|
+
// spec 判据与候选枚举的唯一入口(tier 分级 + 排序)
|
|
36
|
+
const specDetect = require('../skywalk-sdd/ontology/spec-detect.cjs');
|
|
37
|
+
// spec 候选单选交互(init / uninstall / sync-repos 共用)
|
|
38
|
+
const { selectSpecRoot } = require('./spec-choice');
|
|
39
|
+
const {
|
|
40
|
+
STRATEGY,
|
|
41
|
+
ACTION,
|
|
42
|
+
deployFile,
|
|
43
|
+
deployDir,
|
|
44
|
+
} = require('./deploy-strategy');
|
|
33
45
|
|
|
34
46
|
let rl = null;
|
|
35
47
|
|
|
@@ -63,14 +75,19 @@ function ask(question) {
|
|
|
63
75
|
}
|
|
64
76
|
|
|
65
77
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
78
|
+
* 输出详细度开关。
|
|
79
|
+
*
|
|
80
|
+
* 判定(--verbose / KLD_SDD_VERBOSE)属 CLI 输入层职责,由 bin/kld-sdd-init.js 解析后
|
|
81
|
+
* 通过 setVerbose 注入;lib 不读取 process.argv。
|
|
69
82
|
*/
|
|
83
|
+
let verboseEnabled = false;
|
|
84
|
+
|
|
85
|
+
function setVerbose(value) {
|
|
86
|
+
verboseEnabled = Boolean(value);
|
|
87
|
+
}
|
|
88
|
+
|
|
70
89
|
function isVerbose() {
|
|
71
|
-
|
|
72
|
-
const env = process.env.KLD_SDD_VERBOSE;
|
|
73
|
-
return typeof env === 'string' && /^(1|true)$/i.test(env);
|
|
90
|
+
return verboseEnabled;
|
|
74
91
|
}
|
|
75
92
|
|
|
76
93
|
/**
|
|
@@ -82,57 +99,6 @@ function vlog(...args) {
|
|
|
82
99
|
}
|
|
83
100
|
}
|
|
84
101
|
|
|
85
|
-
/**
|
|
86
|
-
* 从命令行参数读取指定编辑器
|
|
87
|
-
* 支持:--tool codex、--tool=codex、--tool cursor,codex、--tool all
|
|
88
|
-
*/
|
|
89
|
-
function parseSelectedTools(args) {
|
|
90
|
-
const toolArgIndex = args.findIndex(arg => arg === '--tool' || arg.startsWith('--tool='));
|
|
91
|
-
if (toolArgIndex === -1) return null;
|
|
92
|
-
|
|
93
|
-
const rawValue = args[toolArgIndex] === '--tool'
|
|
94
|
-
? args[toolArgIndex + 1]
|
|
95
|
-
: args[toolArgIndex].slice('--tool='.length);
|
|
96
|
-
|
|
97
|
-
if (!rawValue) {
|
|
98
|
-
throw new Error('缺少 --tool 参数值,请指定编辑器名称');
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const normalized = rawValue.trim().toLowerCase();
|
|
102
|
-
if (normalized === 'all' || normalized === '全部') {
|
|
103
|
-
return Object.keys(TOOL_CONFIGS);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const selected = normalized.split(',').map(item => item.trim()).filter(Boolean);
|
|
107
|
-
const invalid = selected.filter(tool => !TOOL_CONFIGS[tool]);
|
|
108
|
-
if (invalid.length > 0) {
|
|
109
|
-
throw new Error(`不支持的编辑器: ${invalid.join(', ')}。可用值: ${Object.keys(TOOL_CONFIGS).join(', ')}, all`);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return selected;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* 从命令行参数读取自定义 spec 目录
|
|
117
|
-
* 支持:--spec-path erp-sdd-specs、--spec-path=./specs、--spec-path=/abs/path/specs
|
|
118
|
-
* 返回相对/绝对路径字符串,未指定时返回 null。
|
|
119
|
-
*/
|
|
120
|
-
function parseSpecPathArg(args) {
|
|
121
|
-
const index = args.findIndex(
|
|
122
|
-
(arg) => arg === '--spec-path' || arg.startsWith('--spec-path='),
|
|
123
|
-
);
|
|
124
|
-
if (index === -1) return null;
|
|
125
|
-
|
|
126
|
-
const rawValue = args[index] === '--spec-path'
|
|
127
|
-
? args[index + 1]
|
|
128
|
-
: args[index].slice('--spec-path='.length);
|
|
129
|
-
|
|
130
|
-
if (!rawValue || rawValue.startsWith('--')) {
|
|
131
|
-
throw new Error('缺少 --spec-path 参数值,请指定 spec 仓库目录(相对或绝对路径)');
|
|
132
|
-
}
|
|
133
|
-
return rawValue.trim();
|
|
134
|
-
}
|
|
135
|
-
|
|
136
102
|
/**
|
|
137
103
|
* 将 --spec-path 解析为 { name, abs }。
|
|
138
104
|
* - 绝对路径:直接使用
|
|
@@ -156,82 +122,76 @@ function resolveSpecPathOption(specPathArg, workspaceRoot) {
|
|
|
156
122
|
}
|
|
157
123
|
|
|
158
124
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
125
|
+
* 枚举交互提示用的 spec 目录候选(与卸载共用同一套识别)。
|
|
126
|
+
*
|
|
127
|
+
* 委托 `spec-detect.detectSpecCandidatesWithGit` —— 判据与「读 git config」
|
|
128
|
+
* 均在全仓唯一入口内完成,本函数只是 init 侧的命名别名。
|
|
129
|
+
* 历史上 init 与 uninstall 各写一份相同实现,是「一处改、一处漏」的来源。
|
|
130
|
+
*
|
|
131
|
+
* @returns {Array<{abs: string, name: string, source: string, tier: number, evidence: string[]}>}
|
|
164
132
|
*/
|
|
165
|
-
function
|
|
166
|
-
|
|
167
|
-
const hintPath = path.join(workspaceRoot, '.sdd-spec-root');
|
|
168
|
-
if (fs.existsSync(hintPath)) {
|
|
169
|
-
const rel = String(fs.readFileSync(hintPath, 'utf8') || '')
|
|
170
|
-
.trim()
|
|
171
|
-
.split(/\r?\n/)[0]
|
|
172
|
-
.trim();
|
|
173
|
-
if (rel) {
|
|
174
|
-
const abs = path.resolve(workspaceRoot, rel);
|
|
175
|
-
if (fs.existsSync(abs) && workspaceLayout.looksLikeSpecRepo(abs)) {
|
|
176
|
-
return { name: path.basename(abs), abs };
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
} catch { /* 指针读取异常时继续兜底 */ }
|
|
181
|
-
|
|
182
|
-
const nested = workspaceLayout.findNestedSpecPackage(workspaceRoot);
|
|
183
|
-
return nested ? { name: nested.name, abs: nested.abs } : null;
|
|
133
|
+
function listSpecCandidatesForInit(workspaceRoot) {
|
|
134
|
+
return specDetect.detectSpecCandidatesWithGit(workspaceRoot);
|
|
184
135
|
}
|
|
185
136
|
|
|
186
137
|
/**
|
|
187
138
|
* 交互式确定 spec 包裹包目录(--spec-path 未显式指定时):
|
|
188
|
-
* 1.
|
|
189
|
-
* 2.
|
|
139
|
+
* 1. 检测到候选 → 单选确认(0 个 / 1 个 / N 个 的行为见 lib/spec-choice.js)
|
|
140
|
+
* 2. 未检测到、或用户拒绝候选 → 要求输入相对路径
|
|
141
|
+
* (回车则用默认推导名 `<当前目录名>-sdd-specs`)
|
|
190
142
|
*
|
|
191
143
|
* 非交互环境(CI / 管道 / --tool 自动化)不询问,保持原自动推导行为。
|
|
192
144
|
*
|
|
193
|
-
*
|
|
145
|
+
* 单选交互委托 `lib/spec-choice.js#selectSpecRoot` —— 与 uninstall /
|
|
146
|
+
* sync-repos 共用同一实现。历史上这里另写了一套「确认检测目录」逻辑,
|
|
147
|
+
* 导致 init 只能确认单个候选(多候选时静默取第一个),与另两个场景不一致。
|
|
148
|
+
*
|
|
149
|
+
* @param {Array<{abs: string, name: string, source: string, tier?: number}>} candidates
|
|
150
|
+
* spec 候选集合(由 spec-detect 枚举,已按 tier 排序);空数组表示未检测到
|
|
151
|
+
* @param {object} [options] - 测试/调用方注入:{ askFn, interactive, hasToolArg, isTTY }
|
|
152
|
+
* - hasToolArg:命令行是否显式指定了编辑器(自动化场景,不询问)。由 bin 层解析后注入,
|
|
153
|
+
* 本函数不读取 process.argv。
|
|
154
|
+
* - isTTY:覆盖 TTY 判定,便于测试
|
|
194
155
|
* @returns {{ name: string, abs: string, rel: string, display: string } | null}
|
|
195
156
|
* null 表示未选择自定义目录,走默认推导逻辑
|
|
196
157
|
*/
|
|
197
|
-
async function promptSpecPath(workspaceRoot,
|
|
158
|
+
async function promptSpecPath(workspaceRoot, candidates, options = {}) {
|
|
198
159
|
const askUser = options.askFn || ask;
|
|
199
160
|
let interactive = options.interactive;
|
|
200
161
|
if (typeof interactive !== 'boolean') {
|
|
201
162
|
// 非交互环境:无 TTY 或显式给了 --tool(自动化场景),不询问
|
|
202
|
-
const isTTY =
|
|
203
|
-
|
|
204
|
-
|
|
163
|
+
const isTTY = options.isTTY !== undefined
|
|
164
|
+
? options.isTTY
|
|
165
|
+
: Boolean(process.stdin && process.stdin.isTTY);
|
|
166
|
+
interactive = Boolean(isTTY) && !options.hasToolArg;
|
|
205
167
|
}
|
|
206
168
|
if (!interactive) return null;
|
|
207
169
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
if (normalized === 'n' || normalized === 'no') break;
|
|
219
|
-
console.log(`❌ 无效输入 "${answer}",请输入 Y 或 n`);
|
|
170
|
+
const list = Array.isArray(candidates) ? candidates : (candidates ? [candidates] : []);
|
|
171
|
+
const hasCandidate = list.length > 0;
|
|
172
|
+
|
|
173
|
+
if (hasCandidate) {
|
|
174
|
+
const chosen = await selectSpecRoot({ ask: askUser }, list, { actionLabel: '使用' });
|
|
175
|
+
if (chosen) {
|
|
176
|
+
const name = path.basename(chosen);
|
|
177
|
+
const relPosix = path.relative(workspaceRoot, chosen).replace(/\\/g, '/') || name;
|
|
178
|
+
return { name: relPosix, abs: chosen, rel: relPosix, display: name };
|
|
220
179
|
}
|
|
180
|
+
// 取消(多候选选 0 / 唯一候选拒绝)→ 转入手动输入路径
|
|
221
181
|
} else {
|
|
222
182
|
console.log('\n🔍 未检测到已有 spec 目录(目录名 *-sdd-specs,或 modules.yaml + sdd.config.yaml + openspec/changes + openspec/specs/overview.md 齐全)');
|
|
223
183
|
}
|
|
224
184
|
|
|
225
|
-
//
|
|
185
|
+
// 未确认使用候选(或未检测到):要求输入相对路径
|
|
226
186
|
const defaultName = workspaceLayout.deriveSpecPackageName(workspaceRoot);
|
|
227
187
|
while (true) {
|
|
228
188
|
const input = await askUser(`请输入 spec 目录的相对路径(直接回车使用默认 ${defaultName}): `);
|
|
229
189
|
const trimmed = (input || '').trim();
|
|
230
190
|
if (!trimmed) {
|
|
231
191
|
// 回车 = 使用默认推导名。
|
|
232
|
-
//
|
|
192
|
+
// 若此前拒绝了检测到的候选,必须显式返回默认目录:
|
|
233
193
|
// 返回 null 会让主流程走自动探测,重新命中刚被拒绝的目录。
|
|
234
|
-
if (
|
|
194
|
+
if (hasCandidate) {
|
|
235
195
|
return resolveSpecPathOption(defaultName, workspaceRoot);
|
|
236
196
|
}
|
|
237
197
|
return null; // 未检测到场景:走主流程默认创建
|
|
@@ -245,49 +205,6 @@ async function promptSpecPath(workspaceRoot, detectedSpec, options = {}) {
|
|
|
245
205
|
}
|
|
246
206
|
}
|
|
247
207
|
|
|
248
|
-
/**
|
|
249
|
-
* 让用户选择编辑器
|
|
250
|
-
* 无效输入时重新询问,不允许默认
|
|
251
|
-
*/
|
|
252
|
-
async function selectEditor() {
|
|
253
|
-
const mapping = {
|
|
254
|
-
'1': ['cursor'],
|
|
255
|
-
'2': ['claude'],
|
|
256
|
-
'3': ['codebuddy'],
|
|
257
|
-
'4': ['qoder'],
|
|
258
|
-
'5': ['opencode'],
|
|
259
|
-
'6': ['kunlunzhima'],
|
|
260
|
-
'7': ['workbuddy'],
|
|
261
|
-
'8': ['codex'],
|
|
262
|
-
'9': ['cursor', 'claude', 'codebuddy', 'qoder', 'opencode', 'kunlunzhima', 'workbuddy', 'codex']
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
while (true) {
|
|
266
|
-
console.log('\n请选择您使用的 AI 编辑器:');
|
|
267
|
-
console.log(' 1. Cursor');
|
|
268
|
-
console.log(' 2. Claude Code');
|
|
269
|
-
console.log(' 3. CodeBuddy');
|
|
270
|
-
console.log(' 4. Qoder');
|
|
271
|
-
console.log(' 5. OpenCode');
|
|
272
|
-
console.log(' 6. KunlunZhima');
|
|
273
|
-
console.log(' 7. WorkBuddy');
|
|
274
|
-
console.log(' 8. Codex');
|
|
275
|
-
console.log(' 9. 全部(为所有编辑器生成 skills)');
|
|
276
|
-
|
|
277
|
-
const answer = await ask('请输入选项 (1-9): ');
|
|
278
|
-
const selected = mapping[answer];
|
|
279
|
-
|
|
280
|
-
if (!selected) {
|
|
281
|
-
console.log(`❌ 无效选项 "${answer}",请输入 1-9 之间的数字\n`);
|
|
282
|
-
continue; // 重新循环询问
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const names = selected.map(k => TOOL_CONFIGS[k].name).join(', ');
|
|
286
|
-
console.log(`✓ 已选择: ${names}\n`);
|
|
287
|
-
return selected;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
208
|
/**
|
|
292
209
|
* 检查命令是否可用
|
|
293
210
|
*/
|
|
@@ -319,14 +236,6 @@ function getPackagePath() {
|
|
|
319
236
|
}
|
|
320
237
|
}
|
|
321
238
|
|
|
322
|
-
/**
|
|
323
|
-
* 获取模版路径
|
|
324
|
-
*/
|
|
325
|
-
function getTemplatePath() {
|
|
326
|
-
const pkgPath = getPackagePath();
|
|
327
|
-
return path.join(pkgPath, 'templates', 'openspec');
|
|
328
|
-
}
|
|
329
|
-
|
|
330
239
|
/**
|
|
331
240
|
* 自动安装 openspec
|
|
332
241
|
*/
|
|
@@ -420,48 +329,18 @@ async function runOpenspecInit(selectedTools = []) {
|
|
|
420
329
|
}
|
|
421
330
|
|
|
422
331
|
/**
|
|
423
|
-
*
|
|
332
|
+
* 递归复制并渲染模板目录(工具产物 → OVERWRITE)
|
|
424
333
|
*/
|
|
425
|
-
function copyDir(source, target) {
|
|
426
|
-
if (!fs.existsSync(target)) {
|
|
427
|
-
fs.mkdirSync(target, { recursive: true });
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const files = fs.readdirSync(source);
|
|
431
|
-
for (const file of files) {
|
|
432
|
-
const srcPath = path.join(source, file);
|
|
433
|
-
const tgtPath = path.join(target, file);
|
|
434
|
-
|
|
435
|
-
const stat = fs.statSync(srcPath);
|
|
436
|
-
if (stat.isDirectory()) {
|
|
437
|
-
copyDir(srcPath, tgtPath);
|
|
438
|
-
} else {
|
|
439
|
-
fs.copyFileSync(srcPath, tgtPath);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
334
|
function copyDirRendered(source, target, config, toolKey) {
|
|
445
335
|
const profile = getToolProfile(toolKey);
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
const files = fs.readdirSync(source);
|
|
452
|
-
for (const file of files) {
|
|
453
|
-
const srcPath = path.join(source, file);
|
|
454
|
-
const tgtPath = path.join(target, file);
|
|
455
|
-
|
|
456
|
-
const stat = fs.statSync(srcPath);
|
|
457
|
-
if (stat.isDirectory()) {
|
|
458
|
-
copyDirRendered(srcPath, tgtPath, config, toolKey);
|
|
459
|
-
} else {
|
|
336
|
+
return deployDir(source, target, {
|
|
337
|
+
strategy: STRATEGY.OVERWRITE,
|
|
338
|
+
render: (srcPath, tgtPath) => {
|
|
460
339
|
const rendered = renderTemplate(fs.readFileSync(srcPath, 'utf8'), config, toolKey, profile);
|
|
461
340
|
validateRenderedContent(rendered, profile, tgtPath);
|
|
462
|
-
|
|
463
|
-
}
|
|
464
|
-
}
|
|
341
|
+
return rendered;
|
|
342
|
+
},
|
|
343
|
+
});
|
|
465
344
|
}
|
|
466
345
|
|
|
467
346
|
/** OpenSpec init 生成的 4 个官方 slash command(KLD-SDD 使用 skills,不保留 command) */
|
|
@@ -715,37 +594,10 @@ function deployProfileArtifacts(selectedTools = Object.keys(TOOL_CONFIGS), cwd =
|
|
|
715
594
|
/**
|
|
716
595
|
* 部署 Claude Code Hook Pack(可选增强)
|
|
717
596
|
* Hook 只调用 skywalk-sdd/log.cjs,不写私有日志,不替代 OPSX 模板采集。
|
|
597
|
+
*
|
|
598
|
+
* SDD 受管 hook 的识别/合并/摘除统一由 lib/settings-merge.js 提供,
|
|
599
|
+
* 保证安装与卸载共用同一套归属判定(装得进去的一定摘得掉)。
|
|
718
600
|
*/
|
|
719
|
-
function hookCommandText(hook) {
|
|
720
|
-
const args = Array.isArray(hook.args) ? hook.args : [];
|
|
721
|
-
return [hook.command, ...args].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim();
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
function isSddClaudeHook(hook) {
|
|
725
|
-
const text = hookCommandText(hook);
|
|
726
|
-
return /(?:^|\s|["'])\.claude[\\/]+hooks[\\/]+sdd-(?:prompt|pre-tool|post-tool|stop|apply-gate|apply-test-gate|skill-apply-gate)\.(?:cjs|js)(?=$|\s|["'])/i.test(text) ||
|
|
727
|
-
/(?:^|\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.test(text);
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
function sameHookCommand(left, right) {
|
|
731
|
-
return hookCommandText(left) === hookCommandText(right) &&
|
|
732
|
-
String(left.type || '') === String(right.type || '');
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
function removeManagedSddHooks(entries) {
|
|
736
|
-
return entries
|
|
737
|
-
.map(entry => {
|
|
738
|
-
if (!Array.isArray(entry.hooks)) {
|
|
739
|
-
return entry;
|
|
740
|
-
}
|
|
741
|
-
return {
|
|
742
|
-
...entry,
|
|
743
|
-
hooks: entry.hooks.filter(hook => !isSddClaudeHook(hook)),
|
|
744
|
-
};
|
|
745
|
-
})
|
|
746
|
-
.filter(entry => !Array.isArray(entry.hooks) || entry.hooks.length > 0);
|
|
747
|
-
}
|
|
748
|
-
|
|
749
601
|
function deployClaudeHookPack(selectedTools = Object.keys(TOOL_CONFIGS)) {
|
|
750
602
|
if (!selectedTools.includes('claude')) {
|
|
751
603
|
return true;
|
|
@@ -770,41 +622,45 @@ function deployClaudeHookPack(selectedTools = Object.keys(TOOL_CONFIGS)) {
|
|
|
770
622
|
fs.mkdirSync(targetHookDir, { recursive: true });
|
|
771
623
|
}
|
|
772
624
|
|
|
773
|
-
|
|
625
|
+
deployDir(hookSourceDir, targetHookDir, { strategy: STRATEGY.OVERWRITE });
|
|
774
626
|
console.log(' ✓ 部署 .claude/hooks/sdd-*.cjs');
|
|
775
627
|
|
|
776
628
|
const targetSettingsPath = path.join(claudeDir, 'settings.json');
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
629
|
+
// 结构化配置 → MERGE:保留用户已有字段,只剔除并重装 SDD 受管 hook,幂等
|
|
630
|
+
const mergeResult = deployFile(settingsTemplatePath, targetSettingsPath, {
|
|
631
|
+
strategy: STRATEGY.MERGE,
|
|
632
|
+
label: 'settings.json',
|
|
633
|
+
mergeFn: ({ readSource, readTarget }) => {
|
|
634
|
+
const templateSettings = JSON.parse(readSource());
|
|
635
|
+
const rawExisting = readTarget();
|
|
636
|
+
// 非法 JSON:备份后按空配置重建,避免覆盖用户其它配置时无迹可循
|
|
637
|
+
if (rawExisting !== null) {
|
|
638
|
+
try {
|
|
639
|
+
JSON.parse(rawExisting);
|
|
640
|
+
} catch {
|
|
641
|
+
const backupPath = `${targetSettingsPath}.backup`;
|
|
642
|
+
fs.copyFileSync(targetSettingsPath, backupPath);
|
|
643
|
+
console.log(` ℹ️ settings.json 不是合法 JSON,已备份到 ${backupPath} 后重建`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
789
646
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
for (const entry of entries) {
|
|
794
|
-
const templateHooks = entry.hooks || [];
|
|
795
|
-
const exists = existingSettings.hooks[eventName].some(existingEntry => {
|
|
796
|
-
const existingHooks = existingEntry.hooks || [];
|
|
797
|
-
return templateHooks.every(templateHook => {
|
|
798
|
-
return existingHooks.some(existingHook => sameHookCommand(existingHook, templateHook));
|
|
799
|
-
});
|
|
800
|
-
});
|
|
801
|
-
if (!exists) {
|
|
802
|
-
existingSettings.hooks[eventName].push(entry);
|
|
647
|
+
const computed = computeMergedSettings(rawExisting, templateSettings, PROVIDERS.CLAUDE);
|
|
648
|
+
if (!computed.ok) {
|
|
649
|
+
return { ok: false, error: `${computed.error}: ${targetSettingsPath}` };
|
|
803
650
|
}
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
651
|
|
|
807
|
-
|
|
652
|
+
fs.writeFileSync(targetSettingsPath, JSON.stringify(computed.merged, null, 2) + '\n', 'utf8');
|
|
653
|
+
return { ok: true, action: ACTION.MERGED };
|
|
654
|
+
},
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
if (!mergeResult.ok) {
|
|
658
|
+
console.log(` ⚠️ 合并 .claude/settings.json 失败: ${mergeResult.message}`);
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
661
|
+
if (mergeResult.message) {
|
|
662
|
+
console.log(` ℹ️ ${mergeResult.message}`);
|
|
663
|
+
}
|
|
808
664
|
console.log(' ✓ 合并 .claude/settings.json hooks 配置');
|
|
809
665
|
console.log('✅ Claude Code SDD Hook Pack 已就绪(可选增强,核心采集仍由 OPSX skills 完成)');
|
|
810
666
|
return true;
|
|
@@ -855,27 +711,32 @@ function cleanupNativeOpenspecSkills(selectedTools = Object.keys(TOOL_CONFIGS))
|
|
|
855
711
|
|
|
856
712
|
/**
|
|
857
713
|
* 复制标准文档模版到项目(openspec-templates/)
|
|
714
|
+
*
|
|
715
|
+
* 定位:**只读模板**。skills 强制以它作为文档骨架(opsx-propose §4「不得使用
|
|
716
|
+
* openspec instructions 返回的简化 template」),因此每次 init 全量覆盖更新。
|
|
717
|
+
* 项目若需自定义文档结构,应改 skill 或另建文件,不要就地修改本目录。
|
|
858
718
|
*/
|
|
859
719
|
function copyTemplatesToProject(cwd = process.cwd()) {
|
|
860
720
|
console.log('📄 正在复制标准文档模版到项目...');
|
|
861
|
-
|
|
721
|
+
|
|
862
722
|
const pkgPath = getPackagePath();
|
|
863
723
|
const templatePath = path.join(pkgPath, 'templates', 'openspec');
|
|
864
724
|
const targetDir = path.join(cwd, 'openspec-templates');
|
|
865
|
-
|
|
725
|
+
|
|
866
726
|
if (!fs.existsSync(templatePath)) {
|
|
867
727
|
console.log(`⚠️ 模版源目录不存在: ${templatePath}`);
|
|
868
728
|
return false;
|
|
869
729
|
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
730
|
+
|
|
731
|
+
// 只读模板 → OVERWRITE:每次覆盖更新,保证文档骨架与当前 skills 版本一致
|
|
732
|
+
deployDir(templatePath, targetDir, {
|
|
733
|
+
strategy: STRATEGY.OVERWRITE,
|
|
734
|
+
label: 'openspec-templates',
|
|
735
|
+
});
|
|
876
736
|
|
|
877
737
|
const files = fs.readdirSync(targetDir);
|
|
878
738
|
console.log(`✅ 标准文档模版已复制到: ${targetDir}(${files.length} 个文件)`);
|
|
739
|
+
vlog(' ℹ️ 只读模板:每次 init 全量覆盖更新,请勿就地修改本目录');
|
|
879
740
|
vlog(' 包含文件:');
|
|
880
741
|
files.forEach(file => {
|
|
881
742
|
vlog(` - ${file}`);
|
|
@@ -886,45 +747,38 @@ function copyTemplatesToProject(cwd = process.cwd()) {
|
|
|
886
747
|
|
|
887
748
|
/**
|
|
888
749
|
* 初始化全局 overview.md 到 openspec/specs/ 目录
|
|
889
|
-
*
|
|
750
|
+
*
|
|
751
|
+
* overview.md 是全局架构约束,也是**用户产物**:由项目团队按 opsx-propose §3.5
|
|
752
|
+
* 的引导填充(技术栈 / 数据字典 / 接口规范 / 错误码),AI 在 design / task 阶段读取。
|
|
753
|
+
* 因此首次创建模板,之后**绝不覆盖**——否则一次重跑就会清空已填充的全局契约。
|
|
890
754
|
*/
|
|
891
755
|
function initGlobalOverview(cwd = process.cwd()) {
|
|
892
756
|
console.log('🌐 正在初始化全局架构约束 (overview.md)...');
|
|
893
|
-
|
|
757
|
+
|
|
894
758
|
const pkgPath = getPackagePath();
|
|
895
759
|
const overviewSource = path.join(pkgPath, 'templates', 'openspec', 'overview.md');
|
|
896
760
|
const targetDir = path.join(cwd, 'openspec', 'specs');
|
|
897
761
|
const targetFile = path.join(targetDir, 'overview.md');
|
|
898
|
-
|
|
762
|
+
|
|
899
763
|
if (!fs.existsSync(overviewSource)) {
|
|
900
764
|
console.log(`⚠️ overview.md 源文件不存在: ${overviewSource}`);
|
|
901
765
|
return false;
|
|
902
766
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
if (existingContent === newContent) {
|
|
913
|
-
console.log(' ✓ overview.md 已是最新版本,跳过');
|
|
914
|
-
return true;
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
const backupFile = path.join(targetDir, 'overview.md.backup');
|
|
918
|
-
fs.copyFileSync(targetFile, backupFile);
|
|
919
|
-
console.log(` ℹ️ 已备份现有 overview.md 到 ${backupFile}`);
|
|
767
|
+
|
|
768
|
+
const result = deployFile(overviewSource, targetFile, {
|
|
769
|
+
strategy: STRATEGY.SKIP_IF_EXISTS,
|
|
770
|
+
label: 'overview.md',
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
if (result.action === ACTION.SKIPPED) {
|
|
774
|
+
console.log(' ✓ overview.md 已存在(用户产物),保留不覆盖');
|
|
775
|
+
return true;
|
|
920
776
|
}
|
|
921
|
-
|
|
922
|
-
fs.copyFileSync(overviewSource, targetFile);
|
|
923
|
-
|
|
777
|
+
|
|
924
778
|
console.log(`✅ 全局架构约束已初始化: ${targetFile}`);
|
|
925
779
|
console.log(' ℹ️ 此文件定义了全局数据字典、接口规范、共享实体等约束');
|
|
926
780
|
console.log(' ℹ️ 执行 opsx-design 或 opsx-task 时会自动读取此文件');
|
|
927
|
-
|
|
781
|
+
|
|
928
782
|
return true;
|
|
929
783
|
}
|
|
930
784
|
|
|
@@ -943,15 +797,43 @@ function deploySddGuideManual(cwd = process.cwd(), pkgPath = getPackagePath()) {
|
|
|
943
797
|
return false;
|
|
944
798
|
}
|
|
945
799
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
fs.copyFileSync(source, targetFile);
|
|
800
|
+
deployFile(source, targetFile, {
|
|
801
|
+
strategy: STRATEGY.OVERWRITE,
|
|
802
|
+
label: 'kld-sdd操作手册.html',
|
|
803
|
+
});
|
|
951
804
|
console.log(`✅ SDD 操作手册已部署: openspec/kld-sdd操作手册.html`);
|
|
952
805
|
return true;
|
|
953
806
|
}
|
|
954
807
|
|
|
808
|
+
/**
|
|
809
|
+
* 部署 skywalk-sdd/ 下的单个运行时模块(工具产物 → OVERWRITE)。
|
|
810
|
+
*
|
|
811
|
+
* 统一「源存在则覆盖部署并计数、缺失则告警」这一重复模式,
|
|
812
|
+
* 避免每个模块各写一遍 existsSync + copyFileSync。
|
|
813
|
+
*
|
|
814
|
+
* @param {string} src 源文件绝对路径
|
|
815
|
+
* @param {string} dst 目标文件绝对路径
|
|
816
|
+
* @param {string} desc 日志用描述(verbose 下输出)
|
|
817
|
+
* @param {string} [missingHint] 源缺失时的告警文案
|
|
818
|
+
* @returns {boolean} 是否成功部署
|
|
819
|
+
*/
|
|
820
|
+
function deployRuntimeModule(src, dst, desc, missingHint) {
|
|
821
|
+
if (!fs.existsSync(src)) {
|
|
822
|
+
console.log(` ⚠️ ${missingHint || `${desc} 源文件缺失`}: ${src}`);
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
825
|
+
const result = deployFile(src, dst, {
|
|
826
|
+
strategy: STRATEGY.OVERWRITE,
|
|
827
|
+
label: path.basename(dst),
|
|
828
|
+
});
|
|
829
|
+
if (!result.ok) {
|
|
830
|
+
console.log(` ⚠️ 部署 ${desc} 失败: ${result.message}`);
|
|
831
|
+
return false;
|
|
832
|
+
}
|
|
833
|
+
vlog(` ✓ 部署 ${desc}`);
|
|
834
|
+
return true;
|
|
835
|
+
}
|
|
836
|
+
|
|
955
837
|
/**
|
|
956
838
|
* 部署 SDD Telemetry 数据目录
|
|
957
839
|
* 创建本地数据目录用于存储事件和活跃状态(最终报告落 openspec/changes/<change>/reports/,不再写 skywalk-sdd/reports/)
|
|
@@ -984,33 +866,21 @@ function deployTelemetryDataDir(targetCwd = process.cwd()) {
|
|
|
984
866
|
// 部署项目内 Telemetry CLI(不依赖 npm 发布)
|
|
985
867
|
const telemetrySrc = path.join(pkgPath, 'skywalk-sdd', 'index.cjs');
|
|
986
868
|
const telemetryDst = path.join(dataDir, 'log.cjs');
|
|
987
|
-
if (
|
|
988
|
-
fs.copyFileSync(telemetrySrc, telemetryDst);
|
|
869
|
+
if (deployRuntimeModule(telemetrySrc, telemetryDst, 'skywalk-sdd/log.cjs(本地 Telemetry CLI)', 'Telemetry CLI 源文件缺失')) {
|
|
989
870
|
deployedModules += 1;
|
|
990
|
-
vlog(' ✓ 部署 skywalk-sdd/log.cjs(本地 Telemetry CLI)');
|
|
991
|
-
} else {
|
|
992
|
-
console.log(` ⚠️ Telemetry CLI 源文件缺失: ${telemetrySrc}`);
|
|
993
871
|
}
|
|
994
872
|
|
|
995
873
|
const metricsV3Src = path.join(pkgPath, 'skywalk-sdd', 'metrics-v3.cjs');
|
|
996
874
|
const metricsV3Dst = path.join(dataDir, 'metrics-v3.cjs');
|
|
997
|
-
if (
|
|
998
|
-
fs.copyFileSync(metricsV3Src, metricsV3Dst);
|
|
875
|
+
if (deployRuntimeModule(metricsV3Src, metricsV3Dst, 'skywalk-sdd/metrics-v3.cjs(V3 指标运行时)', 'V3 指标模块源文件缺失(report/metrics 可能无法加载 metrics-v3)')) {
|
|
999
876
|
deployedModules += 1;
|
|
1000
|
-
vlog(' ✓ 部署 skywalk-sdd/metrics-v3.cjs(V3 指标运行时)');
|
|
1001
|
-
} else {
|
|
1002
|
-
console.log(` ⚠️ V3 指标模块源文件缺失: ${metricsV3Src}(report/metrics 可能无法加载 metrics-v3)`);
|
|
1003
877
|
}
|
|
1004
878
|
|
|
1005
879
|
// 部署 spec-root.cjs(替代脆弱的 $(cat .sdd-spec-root),CWD 无关)
|
|
1006
880
|
const specRootSrc = path.join(pkgPath, 'skywalk-sdd', 'spec-root.cjs');
|
|
1007
881
|
const specRootDst = path.join(dataDir, 'spec-root.cjs');
|
|
1008
|
-
if (
|
|
1009
|
-
fs.copyFileSync(specRootSrc, specRootDst);
|
|
882
|
+
if (deployRuntimeModule(specRootSrc, specRootDst, 'skywalk-sdd/spec-root.cjs(CWD 无关的 spec 根路径解析器)', 'spec-root.cjs 源文件缺失,$(cat .sdd-spec-root) 模式可能受影响')) {
|
|
1010
883
|
deployedModules += 1;
|
|
1011
|
-
vlog(' ✓ 部署 skywalk-sdd/spec-root.cjs(CWD 无关的 spec 根路径解析器)');
|
|
1012
|
-
} else {
|
|
1013
|
-
console.log(' ⚠️ spec-root.cjs 源文件缺失,$(cat .sdd-spec-root) 模式可能受影响');
|
|
1014
884
|
}
|
|
1015
885
|
|
|
1016
886
|
// 部署 lib/shared.cjs(共用工具模块:parseArgs/readJson/atomicWrite/HTTP/specRoot/KB状态)
|
|
@@ -1027,12 +897,8 @@ function deployTelemetryDataDir(targetCwd = process.cwd()) {
|
|
|
1027
897
|
}
|
|
1028
898
|
const sharedSrc = path.join(pkgPath, 'skywalk-sdd', 'lib', 'shared.cjs');
|
|
1029
899
|
const sharedDst = path.join(sharedLibDir, 'shared.cjs');
|
|
1030
|
-
if (
|
|
1031
|
-
fs.copyFileSync(sharedSrc, sharedDst);
|
|
900
|
+
if (deployRuntimeModule(sharedSrc, sharedDst, 'skywalk-sdd/lib/shared.cjs(共用工具:parseArgs/JSON/BOM/HTTP/specRoot/KB)', 'lib/shared.cjs 源文件缺失,多个脚本将缺少共用工具依赖')) {
|
|
1032
901
|
deployedModules += 1;
|
|
1033
|
-
vlog(' ✓ 部署 skywalk-sdd/lib/shared.cjs(共用工具:parseArgs/JSON/BOM/HTTP/specRoot/KB)');
|
|
1034
|
-
} else {
|
|
1035
|
-
console.log(' ⚠️ lib/shared.cjs 源文件缺失,多个脚本将缺少共用工具依赖');
|
|
1036
902
|
}
|
|
1037
903
|
|
|
1038
904
|
// kld-fixback: 部署使用统计上报模块(usage-contract/usage-reporter/user-config/git-identity)
|
|
@@ -1049,40 +915,33 @@ function deployTelemetryDataDir(targetCwd = process.cwd()) {
|
|
|
1049
915
|
for (const mod of usageModules) {
|
|
1050
916
|
const src = path.join(pkgPath, 'skywalk-sdd', 'lib', mod.src);
|
|
1051
917
|
const dst = path.join(sharedLibDir, mod.src);
|
|
1052
|
-
if (
|
|
1053
|
-
fs.copyFileSync(src, dst);
|
|
918
|
+
if (deployRuntimeModule(src, dst, `skywalk-sdd/lib/${mod.src}(${mod.desc})`, `${mod.src} 源文件缺失,使用统计上报将不可用`)) {
|
|
1054
919
|
deployedModules += 1;
|
|
1055
|
-
vlog(` ✓ 部署 skywalk-sdd/lib/${mod.src}(${mod.desc})`);
|
|
1056
|
-
} else {
|
|
1057
|
-
console.log(` ⚠️ ${mod.src} 源文件缺失,使用统计上报将不可用`);
|
|
1058
920
|
}
|
|
1059
921
|
}
|
|
1060
922
|
|
|
1061
923
|
// 部署 scale-thresholds.json(Simple/Full 模式判定阈值,opsx-propose §6.8 自动规模判定依赖)
|
|
1062
924
|
const thresholdsSrc = path.join(pkgPath, 'lib', 'scale-thresholds.json');
|
|
1063
925
|
const thresholdsDst = path.join(sharedLibDir, 'scale-thresholds.json');
|
|
1064
|
-
if (
|
|
1065
|
-
fs.copyFileSync(thresholdsSrc, thresholdsDst);
|
|
926
|
+
if (deployRuntimeModule(thresholdsSrc, thresholdsDst, 'skywalk-sdd/lib/scale-thresholds.json(Simple/Full 判定阈值)', 'scale-thresholds.json 源文件缺失,propose 自动规模判定将使用内置默认值')) {
|
|
1066
927
|
deployedModules += 1;
|
|
1067
|
-
vlog(' ✓ 部署 skywalk-sdd/lib/scale-thresholds.json(Simple/Full 判定阈值)');
|
|
1068
|
-
} else {
|
|
1069
|
-
console.log(' ⚠️ scale-thresholds.json 源文件缺失,propose 自动规模判定将使用内置默认值');
|
|
1070
928
|
}
|
|
1071
929
|
|
|
930
|
+
// 运行时身份:内容由生成器产出(版本号写死),升级时必须重写以反映当前 kld-sdd 版本
|
|
1072
931
|
const packageVersion = require(path.join(pkgPath, 'package.json')).version;
|
|
1073
932
|
const runtimeMetadataDst = path.join(dataDir, 'runtime-metadata.cjs');
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
'
|
|
1078
|
-
);
|
|
933
|
+
deployFile(null, runtimeMetadataDst, {
|
|
934
|
+
strategy: STRATEGY.GENERATE,
|
|
935
|
+
label: 'runtime-metadata.cjs',
|
|
936
|
+
generate: () => `'use strict';\nmodule.exports = { resolveRuntimeMetadata() { return { version: '${packageVersion}', producer: 'kld-sdd@${packageVersion}' }; } };\n`,
|
|
937
|
+
});
|
|
1079
938
|
deployedModules += 1;
|
|
1080
939
|
vlog(` ✓ 部署 kld-sdd 运行时身份(${packageVersion})`);
|
|
1081
940
|
|
|
1082
941
|
const reportingSrc = path.join(pkgPath, 'skywalk-sdd', 'reporting');
|
|
1083
942
|
const reportingDst = path.join(dataDir, 'reporting');
|
|
1084
943
|
if (fs.existsSync(reportingSrc)) {
|
|
1085
|
-
|
|
944
|
+
deployDir(reportingSrc, reportingDst, { strategy: STRATEGY.OVERWRITE });
|
|
1086
945
|
deployedModules += 1;
|
|
1087
946
|
vlog(' ✓ 部署 skywalk-sdd/reporting/(V3 单次变更报告)');
|
|
1088
947
|
} else {
|
|
@@ -1091,18 +950,14 @@ function deployTelemetryDataDir(targetCwd = process.cwd()) {
|
|
|
1091
950
|
|
|
1092
951
|
const contextClientSrc = path.join(pkgPath, 'skywalk-sdd', 'context-client.cjs');
|
|
1093
952
|
const contextClientDst = path.join(dataDir, 'context-client.cjs');
|
|
1094
|
-
if (
|
|
1095
|
-
fs.copyFileSync(contextClientSrc, contextClientDst);
|
|
953
|
+
if (deployRuntimeModule(contextClientSrc, contextClientDst, 'skywalk-sdd/context-client.cjs(工程 Spec 上下文客户端)', '工程 Spec 上下文客户端缺失')) {
|
|
1096
954
|
deployedModules += 1;
|
|
1097
|
-
vlog(' ✓ 部署 skywalk-sdd/context-client.cjs(工程 Spec 上下文客户端)');
|
|
1098
|
-
} else {
|
|
1099
|
-
console.log(` ⚠️ 工程 Spec 上下文客户端缺失: ${contextClientSrc}`);
|
|
1100
955
|
}
|
|
1101
956
|
|
|
1102
957
|
const ontologySrc = path.join(pkgPath, 'skywalk-sdd', 'ontology');
|
|
1103
958
|
const ontologyDst = path.join(dataDir, 'ontology');
|
|
1104
959
|
if (fs.existsSync(ontologySrc)) {
|
|
1105
|
-
|
|
960
|
+
deployDir(ontologySrc, ontologyDst, { strategy: STRATEGY.OVERWRITE });
|
|
1106
961
|
deployedModules += 1;
|
|
1107
962
|
vlog(' ✓ 部署 skywalk-sdd/ontology/(本地本体语义运行时)');
|
|
1108
963
|
} else {
|
|
@@ -1111,46 +966,32 @@ function deployTelemetryDataDir(targetCwd = process.cwd()) {
|
|
|
1111
966
|
|
|
1112
967
|
const worktreeFinishSrc = path.join(pkgPath, 'skywalk-sdd', 'apply-worktree-finish.cjs');
|
|
1113
968
|
const worktreeFinishDst = path.join(dataDir, 'apply-worktree-finish.cjs');
|
|
1114
|
-
if (
|
|
1115
|
-
fs.copyFileSync(worktreeFinishSrc, worktreeFinishDst);
|
|
969
|
+
if (deployRuntimeModule(worktreeFinishSrc, worktreeFinishDst, 'skywalk-sdd/apply-worktree-finish.cjs(Apply 收尾脚本)', 'apply-worktree-finish.cjs 源文件缺失,Apply 阶段收尾需要手动执行')) {
|
|
1116
970
|
deployedModules += 1;
|
|
1117
|
-
vlog(' ✓ 部署 skywalk-sdd/apply-worktree-finish.cjs(Apply 收尾脚本)');
|
|
1118
|
-
} else {
|
|
1119
|
-
console.log(' ⚠️ apply-worktree-finish.cjs 源文件缺失,Apply 阶段收尾需要手动执行');
|
|
1120
971
|
}
|
|
1121
972
|
|
|
1122
973
|
const openspecShimSrc = path.join(pkgPath, 'skywalk-sdd', 'openspec-shim.cjs');
|
|
1123
974
|
const openspecShimDst = path.join(dataDir, 'openspec-shim.cjs');
|
|
1124
|
-
if (
|
|
1125
|
-
fs.copyFileSync(openspecShimSrc, openspecShimDst);
|
|
975
|
+
if (deployRuntimeModule(openspecShimSrc, openspecShimDst, 'skywalk-sdd/openspec-shim.cjs(openspec 包裹包路径 shim)', 'openspec-shim.cjs 源文件缺失,openspec 命令需要手动 cd 到 spec 包执行')) {
|
|
1126
976
|
deployedModules += 1;
|
|
1127
|
-
vlog(' ✓ 部署 skywalk-sdd/openspec-shim.cjs(openspec 包裹包路径 shim)');
|
|
1128
|
-
} else {
|
|
1129
|
-
console.log(' ⚠️ openspec-shim.cjs 源文件缺失,openspec 命令需要手动 cd 到 spec 包执行');
|
|
1130
977
|
}
|
|
1131
978
|
|
|
1132
979
|
// 部署 kb-upload.cjs(知识库上传脚本,支持文件夹/zip 双模式随时入库)
|
|
1133
980
|
const kbUploadSrc = path.join(pkgPath, 'skywalk-sdd', 'kb-upload.cjs');
|
|
1134
981
|
const kbUploadDst = path.join(dataDir, 'kb-upload.cjs');
|
|
1135
|
-
if (
|
|
1136
|
-
fs.copyFileSync(kbUploadSrc, kbUploadDst);
|
|
982
|
+
if (deployRuntimeModule(kbUploadSrc, kbUploadDst, 'skywalk-sdd/kb-upload.cjs(知识库上传:文件夹/zip 双模式)', 'kb-upload.cjs 源文件缺失,随时入库功能不可用')) {
|
|
1137
983
|
deployedModules += 1;
|
|
1138
|
-
vlog(' ✓ 部署 skywalk-sdd/kb-upload.cjs(知识库上传:文件夹/zip 双模式)');
|
|
1139
|
-
} else {
|
|
1140
|
-
console.log(' ⚠️ kb-upload.cjs 源文件缺失,随时入库功能不可用');
|
|
1141
984
|
}
|
|
1142
985
|
|
|
1143
986
|
// 部署 kb-sync-identity.cjs(KB 身份同步脚本,重新入库前同步 version-id)
|
|
1144
987
|
const kbSyncIdentitySrc = path.join(pkgPath, 'skywalk-sdd', 'kb-sync-identity.cjs');
|
|
1145
988
|
const kbSyncIdentityDst = path.join(dataDir, 'kb-sync-identity.cjs');
|
|
1146
|
-
if (
|
|
1147
|
-
fs.copyFileSync(kbSyncIdentitySrc, kbSyncIdentityDst);
|
|
989
|
+
if (deployRuntimeModule(kbSyncIdentitySrc, kbSyncIdentityDst, 'skywalk-sdd/kb-sync-identity.cjs(KB 身份同步:重新入库前同步 version-id)', 'kb-sync-identity.cjs 源文件缺失,重新入库身份同步不可用')) {
|
|
1148
990
|
deployedModules += 1;
|
|
1149
|
-
vlog(' ✓ 部署 skywalk-sdd/kb-sync-identity.cjs(KB 身份同步:重新入库前同步 version-id)');
|
|
1150
|
-
} else {
|
|
1151
|
-
console.log(' ⚠️ kb-sync-identity.cjs 源文件缺失,重新入库身份同步不可用');
|
|
1152
991
|
}
|
|
1153
992
|
|
|
993
|
+
require('./hook-install.js').deployRuntime(cwd, pkgPath);
|
|
994
|
+
|
|
1154
995
|
console.log(` ✓ 部署 skywalk-sdd/ 运行时(events/state 数据目录 + ${deployedModules} 个模块)`);
|
|
1155
996
|
vlog(' ✓ 调用方式: node skywalk-sdd/log.cjs start|end|metrics|semantic-identity|semantic-reconcile|semantic-check');
|
|
1156
997
|
vlog(' ✓ openspec: node skywalk-sdd/openspec-shim.cjs list(自动 cd 到 *-sdd-specs)');
|
|
@@ -1161,76 +1002,12 @@ function deployTelemetryDataDir(targetCwd = process.cwd()) {
|
|
|
1161
1002
|
return true;
|
|
1162
1003
|
}
|
|
1163
1004
|
|
|
1164
|
-
/**
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
*
|
|
1168
|
-
* shell hook 运行时通过 git config sdd.specPath / .sdd-spec-root / 向上搜索
|
|
1169
|
-
* 定位 spec 仓的 skywalk-sdd/git-hooks/ 目录,兼容单仓/多仓/mono 布局。
|
|
1170
|
-
*
|
|
1171
|
-
* @param {string} cwd - Git 仓库根目录
|
|
1172
|
-
* @param {string} hookName - hook 名称(如 'pre-commit'、'pre-push')
|
|
1173
|
-
* @param {string} pkgPath - kld-sdd 包路径(用于读取模板)
|
|
1174
|
-
*/
|
|
1175
|
-
function installSddShellHook(cwd, hookName, pkgPath = getPackagePath()) {
|
|
1176
|
-
const hooksDir = path.join(cwd, '.git', 'hooks');
|
|
1177
|
-
if (!fs.existsSync(hooksDir)) {
|
|
1178
|
-
return;
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
const templatePath = path.join(pkgPath, 'templates', 'git-hooks', hookName);
|
|
1182
|
-
if (!fs.existsSync(templatePath)) {
|
|
1183
|
-
console.log(` ⚠️ Hook 模板不存在: ${templatePath}`);
|
|
1184
|
-
return;
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
const hookPath = path.join(hooksDir, hookName);
|
|
1188
|
-
const marker = 'KLD SDD quality gate';
|
|
1189
|
-
const content = fs.readFileSync(templatePath, 'utf8');
|
|
1190
|
-
|
|
1191
|
-
if (fs.existsSync(hookPath)) {
|
|
1192
|
-
const existing = fs.readFileSync(hookPath, 'utf8');
|
|
1193
|
-
if (existing.includes(marker)) {
|
|
1194
|
-
// 已是 SDD hook:允许覆盖升级
|
|
1195
|
-
fs.writeFileSync(hookPath, content, 'utf8');
|
|
1196
|
-
try {
|
|
1197
|
-
// 0o700:仅属主可执行(git 以属主身份执行 hook),避免 S2612 对组/其他用户放权的告警
|
|
1198
|
-
fs.chmodSync(hookPath, 0o700);
|
|
1199
|
-
} catch {
|
|
1200
|
-
// ignore
|
|
1201
|
-
}
|
|
1202
|
-
console.log(` ✓ 更新 .git/hooks/${hookName} SDD 质量门禁(多脚本)`);
|
|
1203
|
-
return;
|
|
1204
|
-
}
|
|
1205
|
-
console.log(` ℹ️ .git/hooks/${hookName} 已存在且非 SDD 受管 hook,已保留不覆盖`);
|
|
1206
|
-
return;
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
fs.writeFileSync(hookPath, content, 'utf8');
|
|
1210
|
-
try {
|
|
1211
|
-
fs.chmodSync(hookPath, 0o755);
|
|
1212
|
-
} catch {
|
|
1213
|
-
// Windows 上 chmod 可能没有实际效果
|
|
1214
|
-
}
|
|
1215
|
-
console.log(` ✓ 安装 .git/hooks/${hookName} SDD 质量门禁(多脚本)`);
|
|
1005
|
+
/** 安全升级 commit-msg、备份并退役受管旧门禁,保留用户 Hook 与管理器。 */
|
|
1006
|
+
function installSddShellHook(cwd, hookName, pkgPath = getPackagePath(), configFile) {
|
|
1007
|
+
return require('./hook-install.js').updateHook(cwd, hookName, pkgPath, configFile);
|
|
1216
1008
|
}
|
|
1217
1009
|
|
|
1218
|
-
/**
|
|
1219
|
-
* 部署代码仓全部 Git hooks(commit-msg + pre-commit + pre-push)。
|
|
1220
|
-
*
|
|
1221
|
-
* 统一入口,封装三种 hook 的部署逻辑。代码仓**不落 skywalk-sdd/**,
|
|
1222
|
-
* 只在 .git/hooks/ 安装 shell hook 模板,远程引用 spec 仓的 .cjs 脚本:
|
|
1223
|
-
*
|
|
1224
|
-
* - **spec 仓**:确保 skywalk-sdd/git-hooks/ 下有全部 .cjs 脚本
|
|
1225
|
-
* (commit-msg-sdd-trailer + pre-commit-* + pre-push-* + ontology)。
|
|
1226
|
-
* - **代码仓 .git/hooks/**:仅安装 3 个 shell hook 模板,运行时通过
|
|
1227
|
-
* git config sdd.specPath / .sdd-spec-root / 向上搜索定位 spec 仓的脚本。
|
|
1228
|
-
*
|
|
1229
|
-
* @param {string} codeRepoRoot - 代码仓根目录
|
|
1230
|
-
* @param {string} specRepoRoot - spec 包裹包根目录
|
|
1231
|
-
* @param {object} options - { pkgPath }
|
|
1232
|
-
* @returns {{ ok: boolean, message?: string }}
|
|
1233
|
-
*/
|
|
1010
|
+
/** 代码仓只安装 commit-msg;验证脚本集中部署在 Spec 包。 */
|
|
1234
1011
|
function deployCodeRepoHooks(codeRepoRoot, specRepoRoot, options = {}) {
|
|
1235
1012
|
const cwd = path.resolve(codeRepoRoot);
|
|
1236
1013
|
const specRoot = path.resolve(specRepoRoot);
|
|
@@ -1240,32 +1017,11 @@ function deployCodeRepoHooks(codeRepoRoot, specRepoRoot, options = {}) {
|
|
|
1240
1017
|
return { ok: false, message: '不是 Git 仓库' };
|
|
1241
1018
|
}
|
|
1242
1019
|
|
|
1243
|
-
// ── 1. 确保 spec 仓有全部 hook 脚本 + ontology ──
|
|
1244
|
-
// commit-msg .cjs + ontology(ensureSpecCommitMsgScript 已处理)
|
|
1245
1020
|
ensureSpecCommitMsgScript(specRoot, pkgPath);
|
|
1246
|
-
|
|
1247
|
-
const
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
fs.mkdirSync(specHooksDir, { recursive: true });
|
|
1251
|
-
}
|
|
1252
|
-
// 复制全部 .cjs 脚本 + hooks.config 配置文件
|
|
1253
|
-
const hookScripts = fs.existsSync(gitHooksTemplateDir)
|
|
1254
|
-
? fs.readdirSync(gitHooksTemplateDir).filter(f => f.endsWith('.cjs') || f === 'hooks.config')
|
|
1255
|
-
: [];
|
|
1256
|
-
for (const script of hookScripts) {
|
|
1257
|
-
const src = path.join(gitHooksTemplateDir, script);
|
|
1258
|
-
const dst = path.join(specHooksDir, script);
|
|
1259
|
-
// 始终覆盖,与 shell hook 的 marker 覆盖策略保持一致
|
|
1260
|
-
fs.copyFileSync(src, dst);
|
|
1261
|
-
}
|
|
1262
|
-
console.log(` ✓ 确保 spec 仓 skywalk-sdd/git-hooks/ 脚本齐全(${hookScripts.length} 个文件)`);
|
|
1263
|
-
|
|
1264
|
-
// ── 2. 安装三个 shell hooks 到代码仓 .git/hooks/ ──
|
|
1265
|
-
// 模板 hook 运行时自动定位 spec 仓的 .cjs 脚本
|
|
1266
|
-
installSddShellHook(cwd, 'commit-msg', pkgPath);
|
|
1267
|
-
installSddShellHook(cwd, 'pre-commit', pkgPath);
|
|
1268
|
-
installSddShellHook(cwd, 'pre-push', pkgPath);
|
|
1021
|
+
const configFile = require('./hook-install.js').deployRuntime(specRoot, pkgPath);
|
|
1022
|
+
for (const name of ['commit-msg', 'pre-commit', 'pre-push']) {
|
|
1023
|
+
installSddShellHook(cwd, name, pkgPath, configFile);
|
|
1024
|
+
}
|
|
1269
1025
|
|
|
1270
1026
|
return { ok: true };
|
|
1271
1027
|
}
|
|
@@ -1285,71 +1041,28 @@ function ensureSpecCommitMsgScript(specRepoRoot, pkgPath = getPackagePath()) {
|
|
|
1285
1041
|
const ontologySrc = path.join(pkgPath, 'skywalk-sdd', 'ontology');
|
|
1286
1042
|
const ontologyDst = path.join(specRoot, 'skywalk-sdd', 'ontology');
|
|
1287
1043
|
if (fs.existsSync(ontologySrc) && !fs.existsSync(path.join(ontologyDst, 'change-key.cjs'))) {
|
|
1288
|
-
|
|
1044
|
+
deployDir(ontologySrc, ontologyDst, { strategy: STRATEGY.OVERWRITE });
|
|
1289
1045
|
}
|
|
1290
1046
|
fs.mkdirSync(path.dirname(scriptPath), { recursive: true });
|
|
1291
1047
|
fs.copyFileSync(src, scriptPath);
|
|
1292
1048
|
return { ok: true, scriptPath, deployed: true };
|
|
1293
1049
|
}
|
|
1294
1050
|
|
|
1295
|
-
/**
|
|
1296
|
-
* 部署 Git hooks / CI 兜底模板
|
|
1297
|
-
* 这些脚本只做质量门禁和 CI 补充记录,不替代 OPSX 主采集链路。
|
|
1298
|
-
*
|
|
1299
|
-
* @param {string} [targetCwd]
|
|
1300
|
-
* @param {{ mode?: 'full'|'commit-msg-only' }} [options]
|
|
1301
|
-
* full: spec 包裹包(pre-commit/pre-push/commit-msg + CI)
|
|
1302
|
-
* commit-msg-only: 仅安装 commit-msg hook(跳过 pre-commit/pre-push)
|
|
1303
|
-
*/
|
|
1051
|
+
/** 部署验证运行时与轻量 Hook;不自动启用 CI。options 为旧调用兼容保留。 */
|
|
1304
1052
|
function deployQualityGateTemplates(targetCwd = process.cwd(), options = {}) {
|
|
1305
1053
|
const pkgPath = getPackagePath();
|
|
1306
1054
|
const cwd = path.resolve(targetCwd);
|
|
1307
|
-
const
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
return false;
|
|
1312
|
-
}
|
|
1313
|
-
console.log(`🧰 正在部署 SDD Git hooks(${mode}): ${cwd}`);
|
|
1314
|
-
const dataDir = path.join(cwd, 'skywalk-sdd');
|
|
1315
|
-
const gitHooksTemplateDir = path.join(pkgPath, 'templates', 'git-hooks');
|
|
1316
|
-
const ciTemplateDir = path.join(pkgPath, 'templates', 'ci');
|
|
1317
|
-
const targetGitHooksDir = path.join(dataDir, 'git-hooks');
|
|
1318
|
-
const targetCiDir = path.join(dataDir, 'ci');
|
|
1319
|
-
|
|
1320
|
-
if (fs.existsSync(gitHooksTemplateDir)) {
|
|
1321
|
-
// 复制 .cjs 脚本 + hooks.config 到 skywalk-sdd/git-hooks/
|
|
1322
|
-
// shell hook 模板(commit-msg/pre-commit/pre-push)不落此目录
|
|
1323
|
-
if (!fs.existsSync(targetGitHooksDir)) {
|
|
1324
|
-
fs.mkdirSync(targetGitHooksDir, { recursive: true });
|
|
1325
|
-
}
|
|
1326
|
-
for (const file of fs.readdirSync(gitHooksTemplateDir)) {
|
|
1327
|
-
if (file.endsWith('.cjs') || file === 'hooks.config') {
|
|
1328
|
-
fs.copyFileSync(path.join(gitHooksTemplateDir, file), path.join(targetGitHooksDir, file));
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
console.log(' ✓ 部署 skywalk-sdd/git-hooks/(.cjs 脚本 + hooks.config)');
|
|
1332
|
-
} else {
|
|
1333
|
-
console.log(` ⚠️ Git hooks 模板缺失: ${gitHooksTemplateDir}`);
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
if (mode === 'full' && fs.existsSync(ciTemplateDir)) {
|
|
1337
|
-
copyDir(ciTemplateDir, targetCiDir);
|
|
1338
|
-
console.log(' ✓ 部署 skywalk-sdd/ci/');
|
|
1055
|
+
const configFile = require('./hook-install.js').deployRuntime(cwd, pkgPath);
|
|
1056
|
+
if (!workspaceLayout.isGitRepo(cwd)) return false;
|
|
1057
|
+
for (const name of ['commit-msg', 'pre-commit', 'pre-push']) {
|
|
1058
|
+
installSddShellHook(cwd, name, pkgPath, configFile);
|
|
1339
1059
|
}
|
|
1340
|
-
|
|
1341
|
-
if (mode === 'full') {
|
|
1342
|
-
installSddShellHook(cwd, 'pre-commit', pkgPath);
|
|
1343
|
-
installSddShellHook(cwd, 'pre-push', pkgPath);
|
|
1344
|
-
}
|
|
1345
|
-
installSddShellHook(cwd, 'commit-msg', pkgPath);
|
|
1346
|
-
|
|
1347
|
-
console.log('✅ Git hooks 已就绪');
|
|
1060
|
+
// CI is an explicit team integration; installing SDD does not enable a merge gate.
|
|
1348
1061
|
return true;
|
|
1349
1062
|
}
|
|
1350
1063
|
|
|
1351
1064
|
/**
|
|
1352
|
-
* 代码仓轻量接入:写 .sdd.yaml + sdd.specPath +
|
|
1065
|
+
* 代码仓轻量接入:写 .sdd.yaml + sdd.specPath + 轻量 commit-msg。
|
|
1353
1066
|
* 代码仓不落 skywalk-sdd/,shell hook 远程引用 spec 仓 skywalk-sdd/git-hooks/ 下的 .cjs 脚本。
|
|
1354
1067
|
*/
|
|
1355
1068
|
function attachCodeRepoLite(codeRepoRoot, specRepoRoot, options = {}) {
|
|
@@ -1364,21 +1077,22 @@ function attachCodeRepoLite(codeRepoRoot, specRepoRoot, options = {}) {
|
|
|
1364
1077
|
return { ok: false, repo: label, message: `spec 路径不是 Git 仓库: ${specRoot}` };
|
|
1365
1078
|
}
|
|
1366
1079
|
|
|
1367
|
-
console.log(`🔗
|
|
1080
|
+
console.log(`🔗 轻量接入代码仓(本地关联 Hook): ${label}`);
|
|
1368
1081
|
|
|
1369
1082
|
const remote = sddConfig.gitRemoteUrl(specRoot) || 'git@gitlab.example.com:biz/xxx-sdd-specs.git';
|
|
1370
1083
|
const sddYamlPath = path.join(cwd, '.sdd.yaml');
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1084
|
+
// 用户产物 → SKIP_IF_EXISTS:代码仓的 spec_repository 由团队填写,升级不覆盖
|
|
1085
|
+
const yamlResult = deployFile(null, sddYamlPath, {
|
|
1086
|
+
strategy: STRATEGY.SKIP_IF_EXISTS,
|
|
1087
|
+
label: '.sdd.yaml',
|
|
1088
|
+
generate: () => [
|
|
1089
|
+
'# 代码仓库关联团队 spec Git 仓库(个人绝对路径勿写入本文件)',
|
|
1090
|
+
'version: 1',
|
|
1091
|
+
`spec_repository: ${remote}`,
|
|
1092
|
+
'',
|
|
1093
|
+
].join('\n'),
|
|
1094
|
+
});
|
|
1095
|
+
if (yamlResult.action === ACTION.CREATED) {
|
|
1382
1096
|
console.log(` ✓ 写入 .sdd.yaml → ${remote}`);
|
|
1383
1097
|
} else {
|
|
1384
1098
|
console.log(' ✓ .sdd.yaml 已存在,跳过覆盖');
|
|
@@ -1390,14 +1104,15 @@ function attachCodeRepoLite(codeRepoRoot, specRepoRoot, options = {}) {
|
|
|
1390
1104
|
}
|
|
1391
1105
|
console.log(` ✓ sdd.specPath = ${linked.path}`);
|
|
1392
1106
|
|
|
1393
|
-
//
|
|
1107
|
+
// 部署轻量关联 Hook,并迁移可识别的旧门禁
|
|
1394
1108
|
deployCodeRepoHooks(cwd, specRoot, options);
|
|
1395
1109
|
|
|
1396
1110
|
return { ok: true, repo: label, path: cwd, specPath: linked.path };
|
|
1397
1111
|
}
|
|
1398
1112
|
|
|
1399
1113
|
/**
|
|
1400
|
-
* 部署 modules.yaml 到 spec
|
|
1114
|
+
* 部署 modules.yaml 到 spec 仓库根目录(用户产物 → SKIP_IF_EXISTS)
|
|
1115
|
+
* 模块代号由项目团队维护,升级不得覆盖。
|
|
1401
1116
|
*/
|
|
1402
1117
|
function deployModulesYaml(cwd = process.cwd(), pkgPath = getPackagePath()) {
|
|
1403
1118
|
console.log('📦 正在检查 modules.yaml...');
|
|
@@ -1407,18 +1122,22 @@ function deployModulesYaml(cwd = process.cwd(), pkgPath = getPackagePath()) {
|
|
|
1407
1122
|
console.log(`⚠️ modules.yaml 模板缺失: ${source}`);
|
|
1408
1123
|
return false;
|
|
1409
1124
|
}
|
|
1410
|
-
|
|
1411
|
-
|
|
1125
|
+
const result = deployFile(source, target, {
|
|
1126
|
+
strategy: STRATEGY.SKIP_IF_EXISTS,
|
|
1127
|
+
label: 'modules.yaml',
|
|
1128
|
+
});
|
|
1129
|
+
if (result.action === ACTION.SKIPPED) {
|
|
1130
|
+
console.log(' ✓ modules.yaml 已存在(用户产物),跳过');
|
|
1412
1131
|
return true;
|
|
1413
1132
|
}
|
|
1414
|
-
fs.copyFileSync(source, target);
|
|
1415
1133
|
console.log(`✅ 已创建 modules.yaml: ${target}`);
|
|
1416
1134
|
return true;
|
|
1417
1135
|
}
|
|
1418
1136
|
|
|
1419
1137
|
/**
|
|
1420
|
-
* 部署 sdd.config.yaml(活动变更登记表)到 spec
|
|
1421
|
-
* propose 隐式写入、archive 隐式移除;代码仓 AI 与 commit-msg Hook
|
|
1138
|
+
* 部署 sdd.config.yaml(活动变更登记表)到 spec 仓库根目录(用户产物 → SKIP_IF_EXISTS)
|
|
1139
|
+
* propose 隐式写入、archive 隐式移除;代码仓 AI 与 commit-msg Hook 读取。
|
|
1140
|
+
* 存在即跳过:避免清空 active_changes 导致活跃变更登记静默丢失。
|
|
1422
1141
|
*/
|
|
1423
1142
|
function deploySddConfigYaml(cwd = process.cwd(), pkgPath = getPackagePath()) {
|
|
1424
1143
|
console.log('📦 正在检查 sdd.config.yaml...');
|
|
@@ -1428,40 +1147,25 @@ function deploySddConfigYaml(cwd = process.cwd(), pkgPath = getPackagePath()) {
|
|
|
1428
1147
|
console.log(`⚠️ sdd.config.yaml 模板缺失: ${source}`);
|
|
1429
1148
|
return false;
|
|
1430
1149
|
}
|
|
1431
|
-
|
|
1432
|
-
|
|
1150
|
+
const result = deployFile(source, target, {
|
|
1151
|
+
strategy: STRATEGY.SKIP_IF_EXISTS,
|
|
1152
|
+
label: 'sdd.config.yaml',
|
|
1153
|
+
});
|
|
1154
|
+
if (result.action === ACTION.SKIPPED) {
|
|
1155
|
+
console.log(' ✓ sdd.config.yaml 已存在(含活跃变更登记),跳过');
|
|
1433
1156
|
return true;
|
|
1434
1157
|
}
|
|
1435
|
-
fs.copyFileSync(source, target);
|
|
1436
1158
|
console.log(`✅ 已创建 sdd.config.yaml: ${target}`);
|
|
1437
1159
|
return true;
|
|
1438
1160
|
}
|
|
1439
1161
|
|
|
1440
|
-
/**
|
|
1441
|
-
* 部署 .sdd.yaml 示例到代码仓库(若不存在且不像 spec 仓)
|
|
1442
|
-
* spec 仓特征:存在 openspec/changes 或 modules.yaml
|
|
1443
|
-
*/
|
|
1444
|
-
function deployDotSddYaml(cwd = process.cwd(), pkgPath = getPackagePath()) {
|
|
1445
|
-
const hasSpecLayout =
|
|
1446
|
-
fs.existsSync(path.join(cwd, 'openspec', 'changes')) ||
|
|
1447
|
-
fs.existsSync(path.join(cwd, 'modules.yaml'));
|
|
1448
|
-
if (hasSpecLayout) {
|
|
1449
|
-
return false;
|
|
1450
|
-
}
|
|
1451
|
-
const source = path.join(pkgPath, 'templates', 'dot-sdd.yaml');
|
|
1452
|
-
const target = path.join(cwd, '.sdd.yaml');
|
|
1453
|
-
if (!fs.existsSync(source) || fs.existsSync(target)) {
|
|
1454
|
-
return false;
|
|
1455
|
-
}
|
|
1456
|
-
console.log('🔗 检测到代码仓库布局,部署 .sdd.yaml 示例...');
|
|
1457
|
-
fs.copyFileSync(source, target);
|
|
1458
|
-
console.log(`✅ 已创建 .sdd.yaml(请修改 spec_repository): ${target}`);
|
|
1459
|
-
return true;
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
1162
|
/**
|
|
1463
1163
|
* 单仓声明 layout: mono,并记录仓内统一包裹目录相对路径。
|
|
1464
1164
|
* commit-msg Hook 靠它跳过「spec 工作区干净」检查并只写 Spec-Change。
|
|
1165
|
+
*
|
|
1166
|
+
* 策略为 MERGE:`.sdd.yaml` 是用户产物,只在缺失时创建;
|
|
1167
|
+
* 已存在时仅当「已声明 mono 但 spec_path 缺失/不符」才定向补写该字段,
|
|
1168
|
+
* 其余内容(用户注释、自定义键)原样保留。
|
|
1465
1169
|
*/
|
|
1466
1170
|
function deployMonoSddYaml(cwd = process.cwd(), options = {}) {
|
|
1467
1171
|
const target = path.join(cwd, '.sdd.yaml');
|
|
@@ -1469,27 +1173,6 @@ function deployMonoSddYaml(cwd = process.cwd(), options = {}) {
|
|
|
1469
1173
|
? String(options.specPathRelative).replace(/\\/g, '/')
|
|
1470
1174
|
: '';
|
|
1471
1175
|
|
|
1472
|
-
if (fs.existsSync(target)) {
|
|
1473
|
-
const existing = fs.readFileSync(target, 'utf8');
|
|
1474
|
-
if (/^layout:\s*mono\s*$/m.test(existing)) {
|
|
1475
|
-
if (specRel && !new RegExp(`^spec_path:\\s*${specRel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`, 'm').test(existing)) {
|
|
1476
|
-
// 已有 mono 但缺/错 spec_path:补写相对路径
|
|
1477
|
-
let next = existing.replace(/\s*$/, '');
|
|
1478
|
-
if (/^spec_path:\s*.+$/m.test(next)) {
|
|
1479
|
-
next = next.replace(/^spec_path:\s*.+$/m, `spec_path: ${specRel}`);
|
|
1480
|
-
} else {
|
|
1481
|
-
next = `${next}\nspec_path: ${specRel}`;
|
|
1482
|
-
}
|
|
1483
|
-
fs.writeFileSync(target, `${next}\n`, 'utf8');
|
|
1484
|
-
console.log(` ✓ 已更新 .sdd.yaml spec_path → ${specRel}`);
|
|
1485
|
-
return true;
|
|
1486
|
-
}
|
|
1487
|
-
console.log(' ✓ .sdd.yaml 已声明 layout: mono,跳过');
|
|
1488
|
-
return true;
|
|
1489
|
-
}
|
|
1490
|
-
console.log('⚠️ .sdd.yaml 已存在但未声明 layout: mono;单仓请手动补 `layout: mono`');
|
|
1491
|
-
return false;
|
|
1492
|
-
}
|
|
1493
1176
|
const lines = [
|
|
1494
1177
|
'# 单仓布局:业务代码与 SDD 包裹包在同一个 Git 仓库',
|
|
1495
1178
|
'# SDD 内容统一在 spec_path 目录内(openspec / modules / sdd.config / 模版)',
|
|
@@ -1499,7 +1182,67 @@ function deployMonoSddYaml(cwd = process.cwd(), options = {}) {
|
|
|
1499
1182
|
];
|
|
1500
1183
|
if (specRel) lines.push(`spec_path: ${specRel}`);
|
|
1501
1184
|
lines.push('');
|
|
1502
|
-
|
|
1185
|
+
|
|
1186
|
+
const result = deployFile(null, target, {
|
|
1187
|
+
strategy: STRATEGY.MERGE,
|
|
1188
|
+
label: '.sdd.yaml',
|
|
1189
|
+
mergeFn: ({ readTarget }) => {
|
|
1190
|
+
let existing = readTarget();
|
|
1191
|
+
if (existing === null) {
|
|
1192
|
+
// 不存在 → 首次创建
|
|
1193
|
+
fs.writeFileSync(target, lines.join('\n'), 'utf8');
|
|
1194
|
+
return { ok: true, action: ACTION.CREATED };
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// 仍是模板占位(代码仓示例)→ 备份后转为单仓声明。
|
|
1198
|
+
// 必须先于 mono 判定:占位内容不含 layout,否则会被下面的
|
|
1199
|
+
// 「未声明 mono」分支挡掉,单仓初始化直接失败。
|
|
1200
|
+
const placeholder = fs.readFileSync(path.join(getPackagePath(), 'templates/dot-sdd.yaml'), 'utf8');
|
|
1201
|
+
if (existing === placeholder && specRel) {
|
|
1202
|
+
fs.copyFileSync(target, `${target}.pre-mono.bak`);
|
|
1203
|
+
existing = 'version: 1\nlayout: mono\n';
|
|
1204
|
+
fs.writeFileSync(target, existing, 'utf8');
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
if (!/^layout:\s*mono\s*$/m.test(existing)) {
|
|
1208
|
+
return {
|
|
1209
|
+
ok: false,
|
|
1210
|
+
error: '.sdd.yaml 已存在但未声明 layout: mono;单仓请手动补 `layout: mono`',
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
const specPathPattern = new RegExp(
|
|
1215
|
+
`^spec_path:\\s*${specRel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`,
|
|
1216
|
+
'm',
|
|
1217
|
+
);
|
|
1218
|
+
if (!specRel || specPathPattern.test(existing)) {
|
|
1219
|
+
return { ok: true, action: ACTION.SKIPPED };
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// 已有 mono 但缺/错 spec_path:只定向补写该字段,保留其余内容
|
|
1223
|
+
let next = existing.replace(/\s*$/, '');
|
|
1224
|
+
if (/^spec_path:\s*.+$/m.test(next)) {
|
|
1225
|
+
next = next.replace(/^spec_path:\s*.+$/m, `spec_path: ${specRel}`);
|
|
1226
|
+
} else {
|
|
1227
|
+
next = `${next}\nspec_path: ${specRel}`;
|
|
1228
|
+
}
|
|
1229
|
+
fs.writeFileSync(target, `${next}\n`, 'utf8');
|
|
1230
|
+
return { ok: true, action: ACTION.MERGED, message: `已更新 .sdd.yaml spec_path → ${specRel}` };
|
|
1231
|
+
},
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
if (!result.ok) {
|
|
1235
|
+
console.log(`⚠️ ${result.message}`);
|
|
1236
|
+
return false;
|
|
1237
|
+
}
|
|
1238
|
+
if (result.action === ACTION.MERGED) {
|
|
1239
|
+
console.log(` ✓ ${result.message}`);
|
|
1240
|
+
return true;
|
|
1241
|
+
}
|
|
1242
|
+
if (result.action === ACTION.SKIPPED) {
|
|
1243
|
+
console.log(' ✓ .sdd.yaml 已声明 layout: mono,跳过');
|
|
1244
|
+
return true;
|
|
1245
|
+
}
|
|
1503
1246
|
console.log(`✅ 已创建 .sdd.yaml(layout: mono${specRel ? `, spec_path: ${specRel}` : ''}): ${target}`);
|
|
1504
1247
|
return true;
|
|
1505
1248
|
}
|
|
@@ -1540,6 +1283,7 @@ async function populateSpecPackage(specRoot, options = {}) {
|
|
|
1540
1283
|
|
|
1541
1284
|
/**
|
|
1542
1285
|
* 在工作区 / Git 根写入 .sdd-spec-root,供 shell 与 skills 引用相对包裹包路径。
|
|
1286
|
+
* 内容完全由目录关系推导(派生状态)→ GENERATE,总是重写。
|
|
1543
1287
|
*/
|
|
1544
1288
|
function writeSddSpecRootHint(anchorDir, specPackageAbs) {
|
|
1545
1289
|
const anchor = path.resolve(anchorDir);
|
|
@@ -1547,7 +1291,11 @@ function writeSddSpecRootHint(anchorDir, specPackageAbs) {
|
|
|
1547
1291
|
const rel = path.relative(anchor, specAbs).replace(/\\/g, '/');
|
|
1548
1292
|
if (!rel || rel.startsWith('..')) return null;
|
|
1549
1293
|
const filePath = path.join(anchor, '.sdd-spec-root');
|
|
1550
|
-
|
|
1294
|
+
deployFile(null, filePath, {
|
|
1295
|
+
strategy: STRATEGY.GENERATE,
|
|
1296
|
+
label: '.sdd-spec-root',
|
|
1297
|
+
generate: () => `${rel}\n`,
|
|
1298
|
+
});
|
|
1551
1299
|
console.log(` ✓ 已写入 .sdd-spec-root → ${rel}`);
|
|
1552
1300
|
return filePath;
|
|
1553
1301
|
}
|
|
@@ -1612,20 +1360,6 @@ function removeOuterSkywalk(anchorDir) {
|
|
|
1612
1360
|
}
|
|
1613
1361
|
}
|
|
1614
1362
|
|
|
1615
|
-
/**
|
|
1616
|
-
* 检测当前项目已初始化的 AI 编辑器
|
|
1617
|
-
*/
|
|
1618
|
-
function detectTools() {
|
|
1619
|
-
const detected = [];
|
|
1620
|
-
const cwd = process.cwd();
|
|
1621
|
-
for (const [key, config] of Object.entries(TOOL_CONFIGS)) {
|
|
1622
|
-
if (fs.existsSync(path.join(cwd, config.configDir))) {
|
|
1623
|
-
detected.push(key);
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
return detected;
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
1363
|
/**
|
|
1630
1364
|
* 创建 / 更新 .gitignore
|
|
1631
1365
|
* @param {string} [cwd=process.cwd()]
|
|
@@ -1643,8 +1377,6 @@ function updateGitignore(cwd = process.cwd()) {
|
|
|
1643
1377
|
'.agents/commands/personal-*',
|
|
1644
1378
|
'kb-state.json',
|
|
1645
1379
|
'**/kb-state.json',
|
|
1646
|
-
'skywalk-sdd/kb-state.json',
|
|
1647
|
-
'**/skywalk-sdd/kb-state.json',
|
|
1648
1380
|
'skywalk-sdd/events/',
|
|
1649
1381
|
'skywalk-sdd/state/',
|
|
1650
1382
|
'**/skywalk-sdd/events/',
|
|
@@ -1662,11 +1394,9 @@ function updateGitignore(cwd = process.cwd()) {
|
|
|
1662
1394
|
.workbuddy/commands/personal-*
|
|
1663
1395
|
.agents/commands/personal-*
|
|
1664
1396
|
|
|
1665
|
-
# KB 绑定状态(含 API Key
|
|
1397
|
+
# KB 绑定状态(含 API Key,勿提交;唯一权威位置 = spec 包裹包根目录)
|
|
1666
1398
|
kb-state.json
|
|
1667
1399
|
**/kb-state.json
|
|
1668
|
-
skywalk-sdd/kb-state.json
|
|
1669
|
-
**/skywalk-sdd/kb-state.json
|
|
1670
1400
|
|
|
1671
1401
|
# SDD Telemetry 数据(实体在 *-sdd-specs/skywalk-sdd/;工作区可能是 symlink)
|
|
1672
1402
|
skywalk-sdd/events/
|
|
@@ -1678,138 +1408,161 @@ skywalk-sdd/state/
|
|
|
1678
1408
|
.worktrees/
|
|
1679
1409
|
`;
|
|
1680
1410
|
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1411
|
+
const result = deployFile(null, gitignorePath, {
|
|
1412
|
+
strategy: STRATEGY.APPEND_LINES,
|
|
1413
|
+
label: '.gitignore',
|
|
1414
|
+
lines: requiredLines,
|
|
1415
|
+
block: sddConfig,
|
|
1416
|
+
requiresMarker: 'KLD SDD',
|
|
1417
|
+
supplementHeader: '# KLD SDD 补充忽略规则',
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
if (!result.ok) {
|
|
1421
|
+
console.log(`⚠️ 更新 .gitignore 失败: ${result.message}`);
|
|
1422
|
+
return result;
|
|
1423
|
+
}
|
|
1424
|
+
if (result.action === ACTION.CREATED) {
|
|
1695
1425
|
console.log('✅ 已创建 .gitignore');
|
|
1426
|
+
} else if (result.action === ACTION.APPENDED) {
|
|
1427
|
+
console.log(`✅ 已${result.message && result.message.includes('完整') ? '更新' : '补充'} .gitignore`);
|
|
1696
1428
|
}
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
/**
|
|
1700
|
-
* 显示帮助信息
|
|
1701
|
-
*/
|
|
1702
|
-
function showHelp() {
|
|
1703
|
-
console.log(`
|
|
1704
|
-
KLD SDD 项目初始化工具
|
|
1705
|
-
|
|
1706
|
-
用法:
|
|
1707
|
-
kld-sdd-init [选项]
|
|
1708
|
-
npx kld-sdd [选项]
|
|
1709
|
-
kld-sdd sync-repos # 工作目录下发现新增代码仓并轻量接入
|
|
1710
|
-
|
|
1711
|
-
选项:
|
|
1712
|
-
-h, --help 显示帮助信息
|
|
1713
|
-
-v, --version 显示版本号
|
|
1714
|
-
--verbose 输出每个安装文件/目录的明细日志(默认仅摘要,亦可用 KLD_SDD_VERBOSE=1)
|
|
1715
|
-
--skip-openspec 跳过 openspec init 步骤
|
|
1716
|
-
--skip-template 跳过复制内置模版
|
|
1717
|
-
--tool <name> 指定编辑器,可用值: cursor, claude, codebuddy, qoder, opencode, kunlunzhima, workbuddy, codex, all
|
|
1718
|
-
--spec-path <dir> 指定 spec 包裹包目录(必须是当前目录的子目录)。
|
|
1719
|
-
不指定时(交互终端):自动检测已有 spec 目录(目录名 *-sdd-specs,
|
|
1720
|
-
或 modules.yaml + sdd.config.yaml + openspec/changes + openspec/specs/overview.md 齐全),
|
|
1721
|
-
检测到则询问是否使用;未检测到则要求输入相对路径(回车用默认 <当前目录名>-sdd-specs)。
|
|
1722
|
-
例: 在 erp-klny-all 下 --spec-path erp-sdd-specs
|
|
1723
|
-
|
|
1724
|
-
工作区模式(在个人工作目录执行):
|
|
1725
|
-
- skills / 编辑器配置只部署一次到当前工作目录
|
|
1726
|
-
- 自动发现子目录 Git 仓库
|
|
1727
|
-
- spec 仓:modules.yaml / openspec 模板 / 文档门禁
|
|
1728
|
-
- 代码仓:仅 commit-msg Hook + .sdd.yaml + link-spec(不装 skills)
|
|
1729
|
-
- Trailer 数据仍在代码 commit 时由 Hook 写入
|
|
1730
|
-
|
|
1731
|
-
示例:
|
|
1732
|
-
kld-sdd-init # 完整初始化流程
|
|
1733
|
-
kld-sdd-init --skip-openspec # 跳过 openspec,直接部署 skills
|
|
1734
|
-
kld-sdd-init --tool codex # 仅部署 Codex 配置
|
|
1735
|
-
kld-sdd-init --spec-path erp-sdd-specs # 指定 spec 目录(当前目录的子目录)
|
|
1736
|
-
kld-sdd sync-repos # 新代码仓加入后同步接入
|
|
1737
|
-
`);
|
|
1429
|
+
return result;
|
|
1738
1430
|
}
|
|
1739
1431
|
|
|
1740
1432
|
/**
|
|
1741
1433
|
* 工作区:为已发现的代码仓做轻量接入,并刷新 .sdd-workspace.yaml
|
|
1434
|
+
*
|
|
1435
|
+
* spec 定位遵循与 init / uninstall 相同的「候选集合 → 用户单选」契约:
|
|
1436
|
+
* - `options.specRoot` 已由 CLI 层确认 → 直接采用(不重新探测)
|
|
1437
|
+
* - 未提供 → 枚举候选;**唯一候选**自动采用,**多候选**返回错误交由调用方
|
|
1438
|
+
* 裁决(不静默挑一个)
|
|
1439
|
+
*
|
|
1440
|
+
* 为什么多候选必须停下:本函数会把 spec 根路径**永久写进每个代码仓**的
|
|
1441
|
+
* `git config sdd.specPath`(见 attachCodeRepoLite),挑错等于污染所有代码仓
|
|
1442
|
+
* 的关联关系。历史上这里直接取 `layout.specRepo`(多候选时静默挑第一个),
|
|
1443
|
+
* 与 init / uninstall 的交互契约不一致。
|
|
1444
|
+
*
|
|
1445
|
+
* @param {string} [workspaceRoot]
|
|
1446
|
+
* @param {{onlyNew?: boolean, specRoot?: string}} [options]
|
|
1447
|
+
* @returns {{ok: boolean, attached: Array, failed?: Array, layout: object,
|
|
1448
|
+
* specRoot?: string|null, filePath?: string, message?: string}}
|
|
1742
1449
|
*/
|
|
1743
1450
|
function syncWorkspaceRepos(workspaceRoot = process.cwd(), options = {}) {
|
|
1744
1451
|
const root = path.resolve(workspaceRoot);
|
|
1745
1452
|
const { layout, newcomers, saved } = workspaceLayout.findNewCodeRepos(root);
|
|
1746
1453
|
const onlyNew = options.onlyNew !== false;
|
|
1747
1454
|
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1455
|
+
// ── spec 定位:显式指定 > 唯一候选自动采用 > 多候选交调用方裁决 ──
|
|
1456
|
+
let specRootAbs = options.specRoot ? path.resolve(options.specRoot) : null;
|
|
1457
|
+
if (!specRootAbs) {
|
|
1458
|
+
const candidates = specDetect.detectSpecCandidatesWithGit(root);
|
|
1459
|
+
if (candidates.length === 0) {
|
|
1460
|
+
console.log('❌ 未发现 spec 仓库(需子目录含 modules.yaml / openspec/changes / *-sdd-specs)');
|
|
1461
|
+
return { ok: false, attached: [], layout, specRoot: null };
|
|
1462
|
+
}
|
|
1463
|
+
if (candidates.length > 1) {
|
|
1464
|
+
// 交由 CLI 层让用户单选后带 specRoot 重新调用;此处不猜。
|
|
1465
|
+
return {
|
|
1466
|
+
ok: false,
|
|
1467
|
+
attached: [],
|
|
1468
|
+
layout,
|
|
1469
|
+
specRoot: null,
|
|
1470
|
+
candidates,
|
|
1471
|
+
message: `检测到 ${candidates.length} 个 spec 仓库候选,需用户单选`,
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
specRootAbs = candidates[0].abs;
|
|
1751
1475
|
}
|
|
1752
1476
|
|
|
1477
|
+
const specName = path.basename(specRootAbs);
|
|
1478
|
+
|
|
1479
|
+
// 代码仓清单必须排除**全部** spec 候选,而不只是被选中的那个。
|
|
1480
|
+
//
|
|
1481
|
+
// 原因:`detectWorkspaceLayout` 的 codeRepos 由 spec 反推
|
|
1482
|
+
//(`children.filter(c => c.abs !== spec.abs)`),只排除它自己挑中的那一个。
|
|
1483
|
+
// 其余候选(如用户复制的 `erp-sdd-specs copy`、未被选中的另一个 spec 包)
|
|
1484
|
+
// 会落进 codeRepos,进而被 attachCodeRepoLite 写入 `.sdd.yaml` 并挂代码仓
|
|
1485
|
+
// hook —— 那不是代码仓,是 spec 仓库/备份。uninstall 侧已有同样的排除逻辑
|
|
1486
|
+
//(见 lib/uninstall.js 的 specDirs 集合)。
|
|
1487
|
+
const specDirs = new Set(
|
|
1488
|
+
specDetect.detectSpecCandidatesWithGit(root).map((c) => path.resolve(c.abs)),
|
|
1489
|
+
);
|
|
1490
|
+
specDirs.add(specRootAbs);
|
|
1491
|
+
const codeRepos = layout.codeRepos.filter(
|
|
1492
|
+
(c) => !specDirs.has(path.resolve(c.abs)),
|
|
1493
|
+
);
|
|
1494
|
+
|
|
1753
1495
|
console.log(`📂 工作目录: ${root}`);
|
|
1754
|
-
console.log(`📄 Spec 仓: ${
|
|
1755
|
-
console.log(`🔎 已发现代码仓: ${
|
|
1496
|
+
console.log(`📄 Spec 仓: ${specName}`);
|
|
1497
|
+
console.log(`🔎 已发现代码仓: ${codeRepos.map((c) => c.name).join(', ') || '(无)'}`);
|
|
1756
1498
|
|
|
1757
|
-
const targets = onlyNew && saved.ok
|
|
1758
|
-
|
|
1759
|
-
|
|
1499
|
+
const targets = onlyNew && saved.ok ? newcomers : codeRepos;
|
|
1500
|
+
const filteredTargets = targets.filter(
|
|
1501
|
+
(c) => !specDirs.has(path.resolve(c.abs)),
|
|
1502
|
+
);
|
|
1760
1503
|
|
|
1761
1504
|
if (onlyNew && saved.ok) {
|
|
1762
|
-
console.log(`🆕 待接入新仓: ${
|
|
1505
|
+
console.log(`🆕 待接入新仓: ${filteredTargets.map((c) => c.name).join(', ') || '(无)'}`);
|
|
1763
1506
|
}
|
|
1764
1507
|
|
|
1765
1508
|
const attached = [];
|
|
1766
1509
|
const failed = [];
|
|
1767
|
-
for (const repo of
|
|
1768
|
-
const result = attachCodeRepoLite(repo.abs,
|
|
1510
|
+
for (const repo of filteredTargets) {
|
|
1511
|
+
const result = attachCodeRepoLite(repo.abs, specRootAbs);
|
|
1769
1512
|
if (result.ok) attached.push(result);
|
|
1770
1513
|
else failed.push(result);
|
|
1771
1514
|
}
|
|
1772
1515
|
|
|
1773
|
-
//
|
|
1774
|
-
const
|
|
1516
|
+
// 刷新清单(spec 指向用户确认的那个;代码仓排除 spec 包自身)
|
|
1517
|
+
const layoutForFile = {
|
|
1518
|
+
...layout,
|
|
1519
|
+
specRepo: { name: specName, abs: specRootAbs },
|
|
1520
|
+
codeRepos,
|
|
1521
|
+
};
|
|
1522
|
+
const filePath = workspaceLayout.writeWorkspaceFile(root, layoutForFile);
|
|
1775
1523
|
console.log(`✅ 已更新 ${path.basename(filePath)}`);
|
|
1776
1524
|
|
|
1777
|
-
return {
|
|
1525
|
+
return {
|
|
1526
|
+
ok: failed.length === 0,
|
|
1527
|
+
attached,
|
|
1528
|
+
failed,
|
|
1529
|
+
layout: layoutForFile,
|
|
1530
|
+
specRoot: specRootAbs,
|
|
1531
|
+
filePath,
|
|
1532
|
+
};
|
|
1778
1533
|
}
|
|
1779
1534
|
|
|
1780
1535
|
/**
|
|
1781
1536
|
* 主函数
|
|
1537
|
+
*
|
|
1538
|
+
* CLI 参数解析(argv / 帮助 / 版本 / 编辑器交互选择)全部在 bin/ 层完成,
|
|
1539
|
+
* 本函数只接收已解析的选项对象,不读取 process.argv。
|
|
1540
|
+
* 参数非法时抛错,由 bin 层统一转成 stderr + 非零退出码。
|
|
1541
|
+
*
|
|
1542
|
+
* @param {object} [options]
|
|
1543
|
+
* @param {string[]} [options.tools] 已选编辑器 id 列表;缺省时调用 selectTools 交互选择
|
|
1544
|
+
* @param {Function} [options.selectTools] 交互式选择编辑器(由 bin 注入)
|
|
1545
|
+
* @param {boolean} [options.hasToolArg] 命令行是否显式指定了编辑器(抑制 spec 路径交互询问)
|
|
1546
|
+
* @param {string} [options.specPath] spec 包裹包目录原始值(相对/绝对),在此做语义校验
|
|
1547
|
+
* @param {boolean} [options.skipOpenspec] 跳过 openspec init 步骤
|
|
1548
|
+
* @param {boolean} [options.skipTemplate] 跳过复制内置模版
|
|
1549
|
+
* @param {string} [options.cwd] 工作目录,缺省 process.cwd()
|
|
1782
1550
|
*/
|
|
1783
|
-
async function main() {
|
|
1784
|
-
const
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
return;
|
|
1791
|
-
}
|
|
1792
|
-
|
|
1793
|
-
if (args.includes('-v') || args.includes('--version')) {
|
|
1794
|
-
const pkg = require('../package.json');
|
|
1795
|
-
console.log(pkg.version);
|
|
1796
|
-
closeRl();
|
|
1797
|
-
return;
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
const skipOpenspec = args.includes('--skip-openspec');
|
|
1801
|
-
const skipTemplate = args.includes('--skip-template');
|
|
1551
|
+
async function main(options = {}) {
|
|
1552
|
+
const workspaceRoot = path.resolve(options.cwd || process.cwd());
|
|
1553
|
+
const skipOpenspec = Boolean(options.skipOpenspec);
|
|
1554
|
+
const skipTemplate = Boolean(options.skipTemplate);
|
|
1555
|
+
const hasToolArg = options.hasToolArg !== undefined
|
|
1556
|
+
? Boolean(options.hasToolArg)
|
|
1557
|
+
: Boolean(options.tools && options.tools.length);
|
|
1802
1558
|
|
|
1803
1559
|
// 自定义 spec 目录(--spec-path):未指定时交互询问(自动检测确认 / 输入相对路径)
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
closeRl();
|
|
1811
|
-
process.exit(1);
|
|
1812
|
-
}
|
|
1560
|
+
// 路径语义校验(必须落在工作目录内)属业务规则,在此完成;非法时抛错交由 bin 出口处理。
|
|
1561
|
+
// 注意:下面交互分支会回填 customSpec,故必须用 let(const 会抛
|
|
1562
|
+
// "Assignment to constant variable",导致交互输入路径时初始化直接失败)。
|
|
1563
|
+
let customSpec = options.specPath
|
|
1564
|
+
? resolveSpecPathOption(options.specPath, workspaceRoot)
|
|
1565
|
+
: null;
|
|
1813
1566
|
|
|
1814
1567
|
console.log('╔════════════════════════════════════════╗');
|
|
1815
1568
|
console.log('║ KLD SDD 项目初始化工具 ║');
|
|
@@ -1817,17 +1570,12 @@ async function main() {
|
|
|
1817
1570
|
console.log('╚════════════════════════════════════════╝');
|
|
1818
1571
|
console.log();
|
|
1819
1572
|
|
|
1820
|
-
// 0.
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
closeRl();
|
|
1827
|
-
process.exit(1);
|
|
1828
|
-
}
|
|
1829
|
-
|
|
1830
|
-
const workspaceRoot = process.cwd();
|
|
1573
|
+
// 0. 选择编辑器(--tool 已指定时直接用;否则由 bin 注入的 selectTools 交互选择)
|
|
1574
|
+
const selectedTools = options.tools && options.tools.length
|
|
1575
|
+
? options.tools
|
|
1576
|
+
: await (typeof options.selectTools === 'function'
|
|
1577
|
+
? options.selectTools()
|
|
1578
|
+
: Promise.reject(new Error('未指定编辑器且未注入 selectTools,CLI 入口应传入 bin/kld-sdd-init.js 的 selectEditor')));
|
|
1831
1579
|
let layout = workspaceLayout.detectWorkspaceLayout(workspaceRoot);
|
|
1832
1580
|
let specPackage = layout.specRepo
|
|
1833
1581
|
? { name: layout.specRepo.name, abs: layout.specRepo.abs, created: false, isGit: true }
|
|
@@ -1848,11 +1596,11 @@ async function main() {
|
|
|
1848
1596
|
console.log(`📌 使用自定义 spec 目录: ${customSpec.rel}`);
|
|
1849
1597
|
console.log();
|
|
1850
1598
|
} else {
|
|
1851
|
-
//
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
const interactiveSpec = await promptSpecPath(workspaceRoot,
|
|
1599
|
+
// 交互提示:传入**完整候选集合**(多候选让用户单选),而不是只传一个。
|
|
1600
|
+
// 集合由 spec-detect 枚举(含 hint/git/mono 权威来源与结构/名称扫描),
|
|
1601
|
+
// 与 uninstall / sync-repos 同源;Git 仓探测漏掉的非 Git spec 产物也在其中。
|
|
1602
|
+
const candidates = listSpecCandidatesForInit(workspaceRoot);
|
|
1603
|
+
const interactiveSpec = await promptSpecPath(workspaceRoot, candidates, { hasToolArg });
|
|
1856
1604
|
if (interactiveSpec) {
|
|
1857
1605
|
customSpec = interactiveSpec;
|
|
1858
1606
|
specPackage = workspaceLayout.ensureSpecPackage(workspaceRoot, {
|
|
@@ -1927,7 +1675,7 @@ async function main() {
|
|
|
1927
1675
|
removeOuterSkywalk(workspaceRoot);
|
|
1928
1676
|
|
|
1929
1677
|
deployMonoSddYaml(workspaceRoot, { specPathRelative: specPackage.name });
|
|
1930
|
-
//
|
|
1678
|
+
// 部署轻量关联 Hook,并迁移可识别的旧门禁
|
|
1931
1679
|
// 单仓 mono:hook 通过 .sdd-spec-root 定位包裹包的 skywalk-sdd/git-hooks/
|
|
1932
1680
|
deployCodeRepoHooks(workspaceRoot, specPackage.abs);
|
|
1933
1681
|
const linked = sddConfig.setSpecPath(workspaceRoot, specPackage.abs, {
|
|
@@ -1947,7 +1695,7 @@ async function main() {
|
|
|
1947
1695
|
});
|
|
1948
1696
|
writeSddSpecRootHint(workspaceRoot, specPackage.abs);
|
|
1949
1697
|
removeOuterSkywalk(workspaceRoot);
|
|
1950
|
-
|
|
1698
|
+
deployMonoSddYaml(workspaceRoot, { specPathRelative: specPackage.name });
|
|
1951
1699
|
}
|
|
1952
1700
|
}
|
|
1953
1701
|
} else if (!skipOpenspec) {
|
|
@@ -2075,35 +1823,26 @@ async function main() {
|
|
|
2075
1823
|
// kld-T06: Progress Runtime 已删除,用户级 workspace 注册函数一并移除。
|
|
2076
1824
|
// 使用统计按本机 git 身份(user.name/user.email)上报,无需任何账号绑定。
|
|
2077
1825
|
|
|
1826
|
+
/**
|
|
1827
|
+
* 对外导出面(仅 CLI 入口与测试实际消费的符号)。
|
|
1828
|
+
* 内部实现函数不导出,避免被外部当作稳定契约而阻碍重构。
|
|
1829
|
+
*/
|
|
2078
1830
|
module.exports = {
|
|
2079
1831
|
main,
|
|
2080
|
-
|
|
2081
|
-
getTemplatePath,
|
|
2082
|
-
getPackagePath,
|
|
2083
|
-
selectEditor,
|
|
1832
|
+
setVerbose,
|
|
2084
1833
|
isVerbose,
|
|
2085
|
-
vlog,
|
|
2086
1834
|
deployOpsxSkills,
|
|
1835
|
+
copyDirRendered,
|
|
2087
1836
|
copyTemplatesToProject,
|
|
2088
|
-
deployProfileArtifacts,
|
|
2089
|
-
deploySddGuideManual,
|
|
2090
|
-
deployModulesYaml,
|
|
2091
|
-
deploySddConfigYaml,
|
|
2092
|
-
deployDotSddYaml,
|
|
2093
1837
|
deployMonoSddYaml,
|
|
2094
1838
|
populateSpecPackage,
|
|
2095
1839
|
writeSddSpecRootHint,
|
|
2096
1840
|
removeOuterSkywalk,
|
|
2097
1841
|
deployQualityGateTemplates,
|
|
2098
1842
|
deployTelemetryDataDir,
|
|
2099
|
-
updateGitignore,
|
|
2100
1843
|
attachCodeRepoLite,
|
|
2101
1844
|
syncWorkspaceRepos,
|
|
2102
|
-
copyDirRendered,
|
|
2103
|
-
parseSelectedTools,
|
|
2104
1845
|
promptSpecPath,
|
|
2105
|
-
|
|
2106
|
-
installSddShellHook,
|
|
1846
|
+
listSpecCandidatesForInit,
|
|
2107
1847
|
deployCodeRepoHooks,
|
|
2108
|
-
TOOL_CONFIGS,
|
|
2109
1848
|
};
|