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