kld-sdd 2.6.8 → 2.6.10
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/lib/init.js +194 -88
- package/package.json +2 -2
- package/skywalk-sdd/context-client.cjs +59 -5
- package/skywalk-sdd/index.cjs +3 -2
- package/skywalk-sdd/ontology/cli.cjs +34 -7
- package/skywalk-sdd/ontology/naming-diagnose.cjs +77 -17
- package/skywalk-sdd/ontology/resolve-spec-root.cjs +102 -0
- package/skywalk-sdd/ontology/sdd-config.cjs +62 -16
- package/skywalk-sdd/ontology/workspace-layout.cjs +81 -0
- package/templates/git-hooks/commit-msg-sdd-trailer.cjs +9 -4
- package/templates/skills/kld-sdd/openspec-sync-specs/SKILL.md +148 -0
- package/templates/skills/kld-sdd/openspec-update-change/SKILL.md +86 -0
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +2 -1
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +4 -2
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +37 -2
- package/templates/skills/kld-sdd/opsx-design/SKILL.md +12 -1
- package/templates/skills/kld-sdd/opsx-explore/SKILL.md +3 -1
- package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +9 -14
- package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +83 -109
- package/templates/skills/kld-sdd/opsx-ontology-query/phase-1-prechange.md +276 -0
- package/templates/skills/kld-sdd/opsx-ontology-query/phase-2-during.md +354 -0
- package/templates/skills/kld-sdd/opsx-ontology-query/phase-3-postchange.md +223 -0
- package/templates/skills/kld-sdd/opsx-ontology-query/phase-4-explore.md +240 -0
- package/templates/skills/kld-sdd/opsx-ontology-query/phase-5-governance.md +232 -0
- package/templates/skills/kld-sdd/opsx-ontology-query/reference.md +92 -4
- package/templates/skills/kld-sdd/opsx-propose/SKILL.md +48 -4
- package/templates/skills/kld-sdd/opsx-propose/checklist.md +1 -0
- package/templates/skills/kld-sdd/opsx-rules/SKILL.md +3 -1
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +36 -4
- package/templates/skills/kld-sdd/opsx-task/SKILL.md +12 -1
- package/templates/skills/kld-sdd/opsx-test/SKILL.md +3 -1
|
@@ -17,6 +17,7 @@ const namingDiagnose = require('./naming-diagnose.cjs');
|
|
|
17
17
|
const listChanges = require('./list-changes.cjs');
|
|
18
18
|
const sddConfig = require('./sdd-config.cjs');
|
|
19
19
|
const activeChanges = require('./active-changes.cjs');
|
|
20
|
+
const { resolveProjectArg } = require('./resolve-spec-root.cjs');
|
|
20
21
|
|
|
21
22
|
function parseArgs(argv) {
|
|
22
23
|
const result = { _: [] };
|
|
@@ -70,6 +71,7 @@ function showHelp() {
|
|
|
70
71
|
node skywalk-sdd/ontology/cli.cjs active-change --register --change=<key> [--title=] [--summary=] [--module=]
|
|
71
72
|
node skywalk-sdd/ontology/cli.cjs active-change --remove --change=<key>
|
|
72
73
|
node skywalk-sdd/ontology/cli.cjs active-change --list [--json]
|
|
74
|
+
node skywalk-sdd/ontology/cli.cjs spec-root [--project=.] [--json]
|
|
73
75
|
node skywalk-sdd/ontology/cli.cjs reconcile --project=. --change=<name> [--profile=...]
|
|
74
76
|
node skywalk-sdd/ontology/cli.cjs check --project=. --change=<name> [--profile=...]
|
|
75
77
|
node skywalk-sdd/ontology/cli.cjs status --project=. --change=<name>
|
|
@@ -79,6 +81,10 @@ function showHelp() {
|
|
|
79
81
|
observe 只负责采集和同步;check/archive 前仍必须执行 reconcile。`);
|
|
80
82
|
}
|
|
81
83
|
|
|
84
|
+
function resolveCliProject(args, options = {}) {
|
|
85
|
+
return resolveProjectArg(args.project || '.', process.cwd(), options);
|
|
86
|
+
}
|
|
87
|
+
|
|
82
88
|
function main(argv = process.argv.slice(2)) {
|
|
83
89
|
const args = parseArgs(argv);
|
|
84
90
|
const command = args._[0];
|
|
@@ -109,7 +115,7 @@ function main(argv = process.argv.slice(2)) {
|
|
|
109
115
|
}
|
|
110
116
|
if (command === 'change-key') {
|
|
111
117
|
if (args.generate || args._[1] === 'generate') {
|
|
112
|
-
const projectRoot =
|
|
118
|
+
const projectRoot = resolveCliProject(args);
|
|
113
119
|
const registry = modules.loadModulesYaml(projectRoot);
|
|
114
120
|
const existing = listChanges.listActiveChanges(projectRoot);
|
|
115
121
|
const built = changeKey.buildChangeKey(args.module, args.slug, {
|
|
@@ -123,7 +129,7 @@ function main(argv = process.argv.slice(2)) {
|
|
|
123
129
|
}
|
|
124
130
|
const key = args.validate || args._[1];
|
|
125
131
|
if (!key) throw new Error('用法: change-key --validate <key> | --generate --module= --slug=');
|
|
126
|
-
const projectRoot =
|
|
132
|
+
const projectRoot = resolveCliProject(args);
|
|
127
133
|
const registry = modules.loadModulesYaml(projectRoot);
|
|
128
134
|
const result = changeKey.validate(key, {
|
|
129
135
|
registeredModules: registry.ok ? registry.codes : null,
|
|
@@ -140,7 +146,7 @@ function main(argv = process.argv.slice(2)) {
|
|
|
140
146
|
return;
|
|
141
147
|
}
|
|
142
148
|
if (command === 'modules') {
|
|
143
|
-
const projectRoot =
|
|
149
|
+
const projectRoot = resolveCliProject(args);
|
|
144
150
|
const result = modules.loadModulesYaml(projectRoot);
|
|
145
151
|
console.log(JSON.stringify({
|
|
146
152
|
ok: result.ok,
|
|
@@ -170,7 +176,7 @@ function main(argv = process.argv.slice(2)) {
|
|
|
170
176
|
return;
|
|
171
177
|
}
|
|
172
178
|
if (command === 'list-changes') {
|
|
173
|
-
const projectRoot =
|
|
179
|
+
const projectRoot = resolveCliProject(args);
|
|
174
180
|
const result = listChanges.groupChanges(projectRoot);
|
|
175
181
|
if (args.json) {
|
|
176
182
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -179,8 +185,29 @@ function main(argv = process.argv.slice(2)) {
|
|
|
179
185
|
}
|
|
180
186
|
return;
|
|
181
187
|
}
|
|
188
|
+
if (command === 'spec-root') {
|
|
189
|
+
const start = path.resolve(args.project || '.');
|
|
190
|
+
const { resolveSpecProjectRoot, isSpecPackageRoot } = require('./resolve-spec-root.cjs');
|
|
191
|
+
const specRoot = resolveSpecProjectRoot(start);
|
|
192
|
+
const payload = {
|
|
193
|
+
start,
|
|
194
|
+
specRoot,
|
|
195
|
+
resolved: specRoot !== start || isSpecPackageRoot(start),
|
|
196
|
+
openspecShim: 'node skywalk-sdd/openspec-shim.cjs <openspec-args>',
|
|
197
|
+
};
|
|
198
|
+
if (args.json) {
|
|
199
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
200
|
+
} else {
|
|
201
|
+
console.log(`start: ${payload.start}`);
|
|
202
|
+
console.log(`specRoot: ${payload.specRoot}`);
|
|
203
|
+
console.log(`resolved: ${payload.resolved}`);
|
|
204
|
+
console.log(`openspec: cd "${payload.specRoot}" && openspec list`);
|
|
205
|
+
console.log(`shim: ${payload.openspecShim}`);
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
182
209
|
if (command === 'link-spec') {
|
|
183
|
-
const projectRoot =
|
|
210
|
+
const projectRoot = resolveCliProject(args, { forCodeRepo: true });
|
|
184
211
|
const specPath = args.path || args._[1];
|
|
185
212
|
if (!specPath) throw new Error('用法: link-spec --path=<spec-clone> [--project=.]');
|
|
186
213
|
const result = sddConfig.setSpecPath(projectRoot, specPath);
|
|
@@ -189,7 +216,7 @@ function main(argv = process.argv.slice(2)) {
|
|
|
189
216
|
return;
|
|
190
217
|
}
|
|
191
218
|
if (command === 'active-change') {
|
|
192
|
-
const specRoot =
|
|
219
|
+
const specRoot = resolveCliProject(args);
|
|
193
220
|
if (args.list || args._[1] === 'list') {
|
|
194
221
|
const config = activeChanges.loadSddConfig(specRoot);
|
|
195
222
|
if (args.json) {
|
|
@@ -227,7 +254,7 @@ function main(argv = process.argv.slice(2)) {
|
|
|
227
254
|
if (!result.ok) process.exitCode = 1;
|
|
228
255
|
return;
|
|
229
256
|
}
|
|
230
|
-
const projectRoot =
|
|
257
|
+
const projectRoot = resolveCliProject(args);
|
|
231
258
|
const changeName = args.change;
|
|
232
259
|
if (!changeName) throw new Error('缺少 --change 参数');
|
|
233
260
|
const profile = args.profile || 'auto';
|
|
@@ -57,6 +57,7 @@ function readProposalFrontmatter(changeDir) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
function detectEntryMode(projectRoot) {
|
|
60
|
+
if (sddConfig.isMonoLayout(projectRoot)) return 'mono-repo';
|
|
60
61
|
const hasModules = fs.existsSync(path.join(projectRoot, 'modules.yaml'));
|
|
61
62
|
const hasOpenspecChanges = fs.existsSync(path.join(projectRoot, 'openspec', 'changes'));
|
|
62
63
|
const hasSddYaml = fs.existsSync(path.join(projectRoot, '.sdd.yaml'));
|
|
@@ -281,40 +282,94 @@ function diagnoseSpecRepo(projectRoot, changeName) {
|
|
|
281
282
|
...diagnoseActiveChanges(projectRoot, changeName),
|
|
282
283
|
});
|
|
283
284
|
|
|
284
|
-
|
|
285
|
-
|
|
285
|
+
checks.push({
|
|
286
|
+
id: 'code-repo-association',
|
|
287
|
+
status: STATUS.SKIP,
|
|
288
|
+
message: '当前位于 spec 仓库:代码仓 .sdd.yaml / Hook 请在各代码仓库或工作目录诊断',
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
mode: 'spec-repo',
|
|
293
|
+
checks,
|
|
294
|
+
summary: summarize(checks),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* 单仓:Git 根有 layout: mono;SDD 内容在仓内统一包裹目录。
|
|
300
|
+
*/
|
|
301
|
+
function diagnoseMonoRepo(gitRoot, changeName) {
|
|
302
|
+
const root = path.resolve(gitRoot);
|
|
303
|
+
const specRoot = sddConfig.resolveMonoSpecRoot(root);
|
|
304
|
+
const checks = [];
|
|
305
|
+
|
|
306
|
+
checks.push({
|
|
307
|
+
id: 'spec-git',
|
|
308
|
+
status: workspaceLayout.isGitRepo(root) ? STATUS.PASS : STATUS.FAIL,
|
|
309
|
+
message: workspaceLayout.isGitRepo(root)
|
|
310
|
+
? '单仓 Git 根可用'
|
|
311
|
+
: '单仓目录不是 Git 仓库',
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const remote = sddConfig.gitRemoteUrl(root);
|
|
315
|
+
checks.push({
|
|
316
|
+
id: 'spec-remote',
|
|
317
|
+
status: remote ? STATUS.PASS : STATUS.NEEDS_INPUT,
|
|
318
|
+
message: remote ? `remote 可识别: ${remote}` : '无法识别 git remote',
|
|
319
|
+
remote,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const registry = modules.loadModulesYaml(specRoot);
|
|
323
|
+
checks.push({
|
|
324
|
+
id: 'modules-yaml',
|
|
325
|
+
status: registry.ok ? STATUS.PASS : (registry.code === modules.CODES.MISSING ? STATUS.NEEDS_INPUT : STATUS.FAIL),
|
|
326
|
+
message: registry.ok
|
|
327
|
+
? `modules.yaml 合法(${registry.codes.size} 个模块,路径 ${path.relative(root, specRoot) || '.'})`
|
|
328
|
+
: registry.message,
|
|
329
|
+
registry,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
if (changeName) {
|
|
333
|
+
const naming = diagnoseChangeNaming(specRoot, changeName, registry.ok ? registry : null);
|
|
286
334
|
checks.push({
|
|
287
|
-
id: '
|
|
288
|
-
|
|
335
|
+
id: 'change-naming',
|
|
336
|
+
status: naming.status,
|
|
337
|
+
message: naming.message,
|
|
338
|
+
detail: naming,
|
|
289
339
|
});
|
|
340
|
+
} else {
|
|
290
341
|
checks.push({
|
|
291
|
-
id: '
|
|
292
|
-
status: STATUS.
|
|
293
|
-
message: '
|
|
342
|
+
id: 'change-naming',
|
|
343
|
+
status: STATUS.SKIP,
|
|
344
|
+
message: '未指定 --change,跳过命名一致性检查',
|
|
294
345
|
});
|
|
295
|
-
return {
|
|
296
|
-
mode: 'mono-repo',
|
|
297
|
-
checks,
|
|
298
|
-
summary: summarize(checks),
|
|
299
|
-
};
|
|
300
346
|
}
|
|
301
347
|
|
|
348
|
+
checks.push({
|
|
349
|
+
id: 'active-changes',
|
|
350
|
+
...diagnoseActiveChanges(specRoot, changeName),
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
checks.push({
|
|
354
|
+
id: 'commit-hook',
|
|
355
|
+
...checkHookInstalled(root),
|
|
356
|
+
});
|
|
302
357
|
checks.push({
|
|
303
358
|
id: 'code-repo-association',
|
|
304
|
-
status: STATUS.
|
|
305
|
-
message:
|
|
359
|
+
status: STATUS.PASS,
|
|
360
|
+
message: `单仓布局(layout: mono,包裹包 ${path.relative(root, specRoot) || '.'}):commit 写 Spec-Change,不写 Spec-Revision`,
|
|
306
361
|
});
|
|
307
362
|
|
|
308
363
|
return {
|
|
309
|
-
mode: '
|
|
364
|
+
mode: 'mono-repo',
|
|
310
365
|
checks,
|
|
311
366
|
summary: summarize(checks),
|
|
367
|
+
specRoot,
|
|
312
368
|
};
|
|
313
369
|
}
|
|
314
370
|
|
|
315
371
|
/**
|
|
316
|
-
*
|
|
317
|
-
* 不用启发式,否则独立的 spec 仓 clone 会被误判成单仓。
|
|
372
|
+
* 单仓:由 .sdd.yaml 的 layout: mono 显式声明。
|
|
318
373
|
*/
|
|
319
374
|
function isMonoRepoLayout(projectRoot) {
|
|
320
375
|
return sddConfig.isMonoLayout(path.resolve(projectRoot));
|
|
@@ -532,6 +587,10 @@ function diagnose(projectRoot, options = {}) {
|
|
|
532
587
|
};
|
|
533
588
|
}
|
|
534
589
|
|
|
590
|
+
if (mode === 'mono-repo' || isMonoRepoLayout(root)) {
|
|
591
|
+
return diagnoseMonoRepo(root, options.change);
|
|
592
|
+
}
|
|
593
|
+
|
|
535
594
|
if (mode === 'code-repo') {
|
|
536
595
|
return diagnoseCodeRepo(root);
|
|
537
596
|
}
|
|
@@ -586,6 +645,7 @@ module.exports = {
|
|
|
586
645
|
diagnoseChangeNaming,
|
|
587
646
|
diagnoseActiveChanges,
|
|
588
647
|
diagnoseWorkspaceRepos,
|
|
648
|
+
diagnoseMonoRepo,
|
|
589
649
|
isMonoRepoLayout,
|
|
590
650
|
diagnose,
|
|
591
651
|
diagnoseTrailer,
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const sddConfig = require('./sdd-config.cjs');
|
|
6
|
+
const workspaceLayout = require('./workspace-layout.cjs');
|
|
7
|
+
|
|
8
|
+
function isSpecPackageRoot(dir) {
|
|
9
|
+
const root = path.resolve(dir);
|
|
10
|
+
return (
|
|
11
|
+
fs.existsSync(path.join(root, 'openspec', 'changes'))
|
|
12
|
+
|| fs.existsSync(path.join(root, 'modules.yaml'))
|
|
13
|
+
|| fs.existsSync(path.join(root, 'sdd.config.yaml'))
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 解析 SDD 文档根(*-sdd-specs 包裹包或旧平铺 spec 根)。
|
|
19
|
+
* openspec CLI 只认 cwd 下的 openspec/;本函数供 log.cjs / ontology cli 自动定位包裹包。
|
|
20
|
+
*
|
|
21
|
+
* 解析顺序:
|
|
22
|
+
* 1. start 本身已是 spec 包根
|
|
23
|
+
* 2. KLD_SDD_SPEC_ROOT(绝对或相对 start)
|
|
24
|
+
* 3. 单仓 layout: mono → spec_path / 子目录 *-sdd-specs
|
|
25
|
+
* 4. 工作区 layout → specRepo
|
|
26
|
+
* 5. start 下嵌套包裹包
|
|
27
|
+
* 6. 代码仓 git config sdd.specPath(含仓内 nested 包裹)
|
|
28
|
+
* 7. 向上最多 3 层找 mono / 嵌套包(Git 根、工作区根常见)
|
|
29
|
+
* 8. 回退 start
|
|
30
|
+
*/
|
|
31
|
+
function resolveSpecProjectRoot(startDir = process.cwd(), options = {}) {
|
|
32
|
+
const start = path.resolve(startDir || process.cwd());
|
|
33
|
+
if (options.skipResolve || options.forCodeRepo) return start;
|
|
34
|
+
if (isSpecPackageRoot(start)) return start;
|
|
35
|
+
|
|
36
|
+
const envRoot = process.env.KLD_SDD_SPEC_ROOT && String(process.env.KLD_SDD_SPEC_ROOT).trim();
|
|
37
|
+
if (envRoot) {
|
|
38
|
+
const candidate = path.isAbsolute(envRoot) ? envRoot : path.resolve(start, envRoot);
|
|
39
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (sddConfig.isMonoLayout(start)) {
|
|
43
|
+
const monoRoot = sddConfig.resolveMonoSpecRoot(start);
|
|
44
|
+
if (fs.existsSync(monoRoot)) return monoRoot;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const layout = workspaceLayout.detectWorkspaceLayout(start);
|
|
48
|
+
if (layout.specRepo && fs.existsSync(layout.specRepo.abs)) {
|
|
49
|
+
return layout.specRepo.abs;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const nested = workspaceLayout.findNestedSpecPackage(start);
|
|
53
|
+
if (nested) return nested.abs;
|
|
54
|
+
|
|
55
|
+
const specPathResult = sddConfig.resolveSpecPath(start);
|
|
56
|
+
if (specPathResult.ok) {
|
|
57
|
+
const valid = sddConfig.validateSpecPath(specPathResult.path, { allowNestedPackage: true });
|
|
58
|
+
if (valid.ok) return valid.path;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let dir = start;
|
|
62
|
+
for (let depth = 0; depth < 3; depth += 1) {
|
|
63
|
+
const parent = path.dirname(dir);
|
|
64
|
+
if (parent === dir) break;
|
|
65
|
+
if (sddConfig.isMonoLayout(parent)) {
|
|
66
|
+
const monoRoot = sddConfig.resolveMonoSpecRoot(parent);
|
|
67
|
+
if (fs.existsSync(monoRoot)) return monoRoot;
|
|
68
|
+
}
|
|
69
|
+
const parentNested = workspaceLayout.findNestedSpecPackage(parent);
|
|
70
|
+
if (parentNested) return parentNested.abs;
|
|
71
|
+
const parentLayout = workspaceLayout.detectWorkspaceLayout(parent);
|
|
72
|
+
if (parentLayout.specRepo && fs.existsSync(parentLayout.specRepo.abs)) {
|
|
73
|
+
return parentLayout.specRepo.abs;
|
|
74
|
+
}
|
|
75
|
+
dir = parent;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return start;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* CLI / skills 用:解析 --project 参数。
|
|
83
|
+
* '.' 或未显式绝对 spec 根时走 resolve;link-spec 等代码仓命令传 forCodeRepo:true。
|
|
84
|
+
*/
|
|
85
|
+
function resolveProjectArg(projectArg, startDir = process.cwd(), options = {}) {
|
|
86
|
+
const raw = projectArg == null || projectArg === '' ? '.' : String(projectArg);
|
|
87
|
+
const resolved = path.resolve(startDir, raw);
|
|
88
|
+
if (options.forCodeRepo) return resolved;
|
|
89
|
+
if (raw !== '.' && raw !== './' && path.isAbsolute(raw)) {
|
|
90
|
+
return isSpecPackageRoot(resolved) ? resolved : resolveSpecProjectRoot(resolved, options);
|
|
91
|
+
}
|
|
92
|
+
if (raw !== '.' && raw !== './') {
|
|
93
|
+
if (isSpecPackageRoot(resolved)) return resolved;
|
|
94
|
+
}
|
|
95
|
+
return resolveSpecProjectRoot(startDir, options);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = {
|
|
99
|
+
isSpecPackageRoot,
|
|
100
|
+
resolveSpecProjectRoot,
|
|
101
|
+
resolveProjectArg,
|
|
102
|
+
};
|
|
@@ -35,15 +35,17 @@ function stripYamlQuotes(value) {
|
|
|
35
35
|
* version: 1
|
|
36
36
|
* spec_repository: git@host:group/repo.git
|
|
37
37
|
*
|
|
38
|
-
*
|
|
38
|
+
* 单仓:同一 Git 仓内用统一包裹目录存放 openspec / 配置
|
|
39
39
|
* version: 1
|
|
40
40
|
* layout: mono
|
|
41
|
+
* spec_path: myapp-sdd-specs
|
|
41
42
|
*/
|
|
42
43
|
function parseSddYaml(text) {
|
|
43
44
|
const lines = String(text || '').split(/\r?\n/);
|
|
44
45
|
let version = null;
|
|
45
46
|
let specRepository = null;
|
|
46
47
|
let layout = null;
|
|
48
|
+
let specPathRel = null;
|
|
47
49
|
|
|
48
50
|
for (let index = 0; index < lines.length; index += 1) {
|
|
49
51
|
const raw = lines[index];
|
|
@@ -76,6 +78,12 @@ function parseSddYaml(text) {
|
|
|
76
78
|
continue;
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
const pathMatch = /^spec_path:\s*(.+?)\s*$/.exec(line);
|
|
82
|
+
if (pathMatch) {
|
|
83
|
+
specPathRel = stripYamlQuotes(pathMatch[1]);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
79
87
|
return {
|
|
80
88
|
ok: false,
|
|
81
89
|
code: CODES.FORMAT_INVALID,
|
|
@@ -108,6 +116,7 @@ function parseSddYaml(text) {
|
|
|
108
116
|
version,
|
|
109
117
|
layout: resolvedLayout,
|
|
110
118
|
spec_repository: specRepository || '',
|
|
119
|
+
spec_path: specPathRel || '',
|
|
111
120
|
};
|
|
112
121
|
}
|
|
113
122
|
|
|
@@ -132,6 +141,29 @@ function isMonoLayout(repoRoot) {
|
|
|
132
141
|
return loaded.ok && loaded.layout === LAYOUTS.MONO;
|
|
133
142
|
}
|
|
134
143
|
|
|
144
|
+
/**
|
|
145
|
+
* 单仓 SDD 包裹根:优先 .sdd.yaml spec_path → 子目录 *-sdd-specs → 兼容旧平铺根目录。
|
|
146
|
+
*/
|
|
147
|
+
function resolveMonoSpecRoot(repoRoot) {
|
|
148
|
+
const root = path.resolve(repoRoot);
|
|
149
|
+
const loaded = loadSddYaml(root);
|
|
150
|
+
if (loaded.ok && loaded.spec_path) {
|
|
151
|
+
return path.resolve(root, loaded.spec_path);
|
|
152
|
+
}
|
|
153
|
+
const workspaceLayout = require('./workspace-layout.cjs');
|
|
154
|
+
const nested = workspaceLayout.findNestedSpecPackage(root);
|
|
155
|
+
if (nested) return nested.abs;
|
|
156
|
+
return root;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function looksLikeSpecPackageContent(dir) {
|
|
160
|
+
return (
|
|
161
|
+
fs.existsSync(path.join(dir, 'modules.yaml'))
|
|
162
|
+
|| fs.existsSync(path.join(dir, 'sdd.config.yaml'))
|
|
163
|
+
|| fs.existsSync(path.join(dir, 'openspec', 'changes'))
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
135
167
|
function gitConfig(cwd, key) {
|
|
136
168
|
try {
|
|
137
169
|
return execFileSync('git', ['config', '--local', '--get', key], {
|
|
@@ -203,7 +235,7 @@ function resolveSpecPath(codeRepoRoot, env = process.env) {
|
|
|
203
235
|
};
|
|
204
236
|
}
|
|
205
237
|
|
|
206
|
-
function validateSpecPath(specPath) {
|
|
238
|
+
function validateSpecPath(specPath, options = {}) {
|
|
207
239
|
if (!specPath || !fs.existsSync(specPath)) {
|
|
208
240
|
return {
|
|
209
241
|
ok: false,
|
|
@@ -211,25 +243,37 @@ function validateSpecPath(specPath) {
|
|
|
211
243
|
message: `spec 路径不存在: ${specPath}`,
|
|
212
244
|
};
|
|
213
245
|
}
|
|
214
|
-
if (
|
|
215
|
-
return {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
};
|
|
246
|
+
if (fs.existsSync(path.join(specPath, '.git'))) {
|
|
247
|
+
return { ok: true, path: specPath, nested: false };
|
|
248
|
+
}
|
|
249
|
+
// 单仓内部包裹包:不是独立 Git,但内容齐全且落在某个 Git 仓内
|
|
250
|
+
if (options.allowNestedPackage && looksLikeSpecPackageContent(specPath)) {
|
|
251
|
+
return { ok: true, path: specPath, nested: true };
|
|
220
252
|
}
|
|
221
|
-
return {
|
|
253
|
+
return {
|
|
254
|
+
ok: false,
|
|
255
|
+
code: CODES.SPEC_PATH_INVALID,
|
|
256
|
+
message: `spec 路径不是 Git 仓库: ${specPath}`,
|
|
257
|
+
};
|
|
222
258
|
}
|
|
223
259
|
|
|
224
|
-
function setSpecPath(codeRepoRoot, specPath) {
|
|
260
|
+
function setSpecPath(codeRepoRoot, specPath, options = {}) {
|
|
225
261
|
const absolute = path.resolve(specPath);
|
|
226
|
-
const valid = validateSpecPath(absolute);
|
|
262
|
+
const valid = validateSpecPath(absolute, options);
|
|
227
263
|
if (!valid.ok) return valid;
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
264
|
+
try {
|
|
265
|
+
execFileSync('git', ['config', '--local', 'sdd.specPath', absolute], {
|
|
266
|
+
cwd: codeRepoRoot,
|
|
267
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
268
|
+
});
|
|
269
|
+
} catch (error) {
|
|
270
|
+
return {
|
|
271
|
+
ok: false,
|
|
272
|
+
code: CODES.SPEC_PATH_INVALID,
|
|
273
|
+
message: `无法写入 git config sdd.specPath(请确认目录是真实 Git 仓库): ${error.message}`,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
return { ok: true, path: absolute, nested: Boolean(valid.nested) };
|
|
233
277
|
}
|
|
234
278
|
|
|
235
279
|
function resolveCodeRepoContext(codeRepoRoot, env = process.env) {
|
|
@@ -320,6 +364,8 @@ module.exports = {
|
|
|
320
364
|
LAYOUTS,
|
|
321
365
|
parseSddYaml,
|
|
322
366
|
isMonoLayout,
|
|
367
|
+
resolveMonoSpecRoot,
|
|
368
|
+
looksLikeSpecPackageContent,
|
|
323
369
|
loadSddYaml,
|
|
324
370
|
gitConfig,
|
|
325
371
|
gitRemoteUrl,
|
|
@@ -181,14 +181,95 @@ function findNewCodeRepos(workspaceRoot) {
|
|
|
181
181
|
};
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
/**
|
|
185
|
+
* 推导统一 SDD 包裹目录名:`<parentBasename>-sdd-specs`
|
|
186
|
+
*/
|
|
187
|
+
function deriveSpecPackageName(parentDir) {
|
|
188
|
+
const base = path.basename(path.resolve(parentDir)).toLowerCase();
|
|
189
|
+
if (!base || base === '.' || base === '/' || base === '\\') return 'project-sdd-specs';
|
|
190
|
+
if (base.endsWith('sdd-specs')) return path.basename(path.resolve(parentDir));
|
|
191
|
+
return `${path.basename(path.resolve(parentDir))}-sdd-specs`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function listChildDirectories(parentDir) {
|
|
195
|
+
const root = path.resolve(parentDir);
|
|
196
|
+
if (!fs.existsSync(root)) return [];
|
|
197
|
+
return fs.readdirSync(root)
|
|
198
|
+
.filter((name) => !name.startsWith('.') && !IGNORE_DIR_NAMES.has(name))
|
|
199
|
+
.map((name) => ({ name, abs: path.join(root, name) }))
|
|
200
|
+
.filter((entry) => {
|
|
201
|
+
try {
|
|
202
|
+
return fs.statSync(entry.abs).isDirectory();
|
|
203
|
+
} catch {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 在父目录下查找已有的 SDD 包裹包(不必是独立 Git)。
|
|
212
|
+
* 优先:modules.yaml → 目录名 *-sdd-specs → openspec/changes。
|
|
213
|
+
*/
|
|
214
|
+
function findNestedSpecPackage(parentDir) {
|
|
215
|
+
const children = listChildDirectories(parentDir);
|
|
216
|
+
const candidates = children.filter((c) => looksLikeSpecRepo(c.abs));
|
|
217
|
+
if (candidates.length === 0) return null;
|
|
218
|
+
return (
|
|
219
|
+
candidates.find((c) => fs.existsSync(path.join(c.abs, 'modules.yaml')))
|
|
220
|
+
|| candidates.find((c) => /-sdd-specs$/i.test(c.name))
|
|
221
|
+
|| candidates[0]
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 确保父目录下存在统一的 SDD 包裹包目录。
|
|
227
|
+
* @param {{ name?: string, initGit?: boolean }} options
|
|
228
|
+
* initGit=true:多仓兄弟目录,包裹包本身是独立 Git 仓
|
|
229
|
+
* initGit=false:单仓内部子目录,共用外层 Git
|
|
230
|
+
*/
|
|
231
|
+
function ensureSpecPackage(parentDir, options = {}) {
|
|
232
|
+
const { execFileSync } = require('child_process');
|
|
233
|
+
const root = path.resolve(parentDir);
|
|
234
|
+
const existing = findNestedSpecPackage(root);
|
|
235
|
+
if (existing) {
|
|
236
|
+
return {
|
|
237
|
+
name: existing.name,
|
|
238
|
+
abs: existing.abs,
|
|
239
|
+
created: false,
|
|
240
|
+
isGit: isGitRepo(existing.abs),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const name = options.name || deriveSpecPackageName(root);
|
|
245
|
+
const abs = path.join(root, name);
|
|
246
|
+
const existed = fs.existsSync(abs);
|
|
247
|
+
fs.mkdirSync(abs, { recursive: true });
|
|
248
|
+
|
|
249
|
+
let isGit = isGitRepo(abs);
|
|
250
|
+
if (options.initGit && !isGit) {
|
|
251
|
+
execFileSync('git', ['init'], {
|
|
252
|
+
cwd: abs,
|
|
253
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
254
|
+
});
|
|
255
|
+
isGit = true;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return { name, abs, created: !existed, isGit };
|
|
259
|
+
}
|
|
260
|
+
|
|
184
261
|
module.exports = {
|
|
185
262
|
WORKSPACE_FILE,
|
|
186
263
|
isGitRepo,
|
|
187
264
|
looksLikeSpecRepo,
|
|
188
265
|
listChildGitRepos,
|
|
266
|
+
listChildDirectories,
|
|
189
267
|
detectWorkspaceLayout,
|
|
190
268
|
parseWorkspaceYaml,
|
|
191
269
|
loadWorkspaceFile,
|
|
192
270
|
writeWorkspaceFile,
|
|
193
271
|
findNewCodeRepos,
|
|
272
|
+
deriveSpecPackageName,
|
|
273
|
+
findNestedSpecPackage,
|
|
274
|
+
ensureSpecPackage,
|
|
194
275
|
};
|
|
@@ -60,7 +60,7 @@ function resolveLayout(repoRoot, env = process.env) {
|
|
|
60
60
|
return { mode: 'unlinked' };
|
|
61
61
|
}
|
|
62
62
|
if (sddConfig.isMonoLayout(repoRoot)) {
|
|
63
|
-
return { mode: 'mono', specPath: repoRoot };
|
|
63
|
+
return { mode: 'mono', specPath: sddConfig.resolveMonoSpecRoot(repoRoot) };
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
const context = sddConfig.resolveCodeRepoContext(repoRoot, env);
|
|
@@ -70,9 +70,14 @@ function resolveLayout(repoRoot, env = process.env) {
|
|
|
70
70
|
if (context.code === sddConfig.CODES.SPEC_PATH_INVALID) {
|
|
71
71
|
fail(context.message);
|
|
72
72
|
}
|
|
73
|
-
// specPath
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
// specPath 指回本仓根:等同单仓(兼容旧平铺);若指向仓内包裹包仍按 mono
|
|
74
|
+
const resolvedSpec = path.resolve(context.specPath);
|
|
75
|
+
const resolvedRoot = path.resolve(repoRoot);
|
|
76
|
+
if (resolvedSpec === resolvedRoot) {
|
|
77
|
+
return { mode: 'mono', specPath: sddConfig.resolveMonoSpecRoot(repoRoot), context };
|
|
78
|
+
}
|
|
79
|
+
if (resolvedSpec.startsWith(`${resolvedRoot}${path.sep}`)) {
|
|
80
|
+
return { mode: 'mono', specPath: resolvedSpec, context };
|
|
76
81
|
}
|
|
77
82
|
return { mode: 'multi', specPath: context.specPath, context };
|
|
78
83
|
}
|