@jspg-ai/coding-bb 0.0.3-beta.6 → 0.0.3-beta.7
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/cbb/lib/install/init.js +60 -23
- package/cbb/lib/openspec/index.js +337 -554
- package/cbb/lib/superpowers/index.js +246 -265
- package/cbb/lib/utils/tar.js +92 -0
- package/cbb/lib/utils/upstream.js +90 -0
- package/config/upstream-mirrors.json +12 -0
- package/openspec/.version +2 -3
- package/openspec/commands/apply.md +189 -175
- package/openspec/commands/archive.md +237 -216
- package/openspec/commands/bulk-archive.md +355 -327
- package/openspec/commands/continue.md +116 -105
- package/openspec/commands/explore.md +230 -199
- package/openspec/commands/ff.md +115 -104
- package/openspec/commands/new.md +74 -63
- package/openspec/commands/onboard.md +557 -548
- package/openspec/commands/propose.md +161 -150
- package/openspec/commands/sync.md +277 -249
- package/openspec/commands/update.md +92 -80
- package/openspec/commands/verify.md +175 -162
- package/openspec/skills/openspec-apply-change/SKILL.md +20 -5
- package/openspec/skills/openspec-archive-change/SKILL.md +30 -8
- package/openspec/skills/openspec-bulk-archive-change/SKILL.md +36 -6
- package/openspec/skills/openspec-continue-change/SKILL.md +14 -2
- package/openspec/skills/openspec-explore/SKILL.md +21 -9
- package/openspec/skills/openspec-ff-change/SKILL.md +14 -2
- package/openspec/skills/openspec-new-change/SKILL.md +13 -1
- package/openspec/skills/openspec-onboard/SKILL.md +49 -39
- package/openspec/skills/openspec-propose/SKILL.md +15 -3
- package/openspec/skills/openspec-sync-specs/SKILL.md +31 -2
- package/openspec/skills/openspec-update-change/SKILL.md +27 -14
- package/openspec/skills/openspec-verify-change/SKILL.md +17 -3
- package/package.json +1 -1
- package/superpowers/.version +4 -4
- package/superpowers/skills/brainstorming/SKILL.md +47 -12
- package/superpowers/skills/brainstorming/scripts/frame-template.html +213 -213
- package/superpowers/skills/brainstorming/scripts/server.cjs +723 -723
- package/superpowers/skills/brainstorming/visual-companion.md +6 -6
- package/superpowers/skills/diagnosing-superpowers/SKILL.md +120 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/analyst-common.md +38 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/cost-and-time.md +28 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/plan-adherence.md +29 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/quality-evidence.md +26 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/repeated-work.md +30 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/request-conflicts.md +20 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/scrub-audit.md +33 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/scrub.md +29 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/similar-session.md +38 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/skill-timeline.md +30 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/stumbles.md +28 -0
- package/superpowers/skills/diagnosing-superpowers/references/context-safety.md +22 -0
- package/superpowers/skills/diagnosing-superpowers/references/github-issues.md +47 -0
- package/superpowers/skills/diagnosing-superpowers/references/redaction-policy.md +34 -0
- package/superpowers/skills/diagnosing-superpowers/references/session-discovery.md +31 -0
- package/superpowers/skills/diagnosing-superpowers/templates/bundle-README.md +77 -0
- package/superpowers/skills/diagnosing-superpowers/templates/case.md +64 -0
- package/superpowers/skills/diagnosing-superpowers/templates/issue.md +51 -0
- package/superpowers/skills/diagnosing-superpowers/templates/report.md +82 -0
- package/superpowers/skills/executing-plans/SKILL.md +350 -41
- package/superpowers/skills/executing-plans/scripts/task-done +52 -0
- package/superpowers/skills/executing-plans/scripts/task-start +28 -0
- package/superpowers/skills/requesting-code-review/SKILL.md +1 -1
- package/superpowers/skills/requesting-code-review/code-reviewer.md +17 -0
- package/superpowers/skills/subagent-driven-development/SKILL.md +18 -18
- package/superpowers/skills/subagent-driven-development/re-review-prompt.md +1 -1
- package/superpowers/skills/subagent-driven-development/scripts/review-package +53 -46
- package/superpowers/skills/subagent-driven-development/scripts/sdd-workspace +82 -40
- package/superpowers/skills/subagent-driven-development/scripts/task-brief +43 -41
- package/superpowers/skills/subagent-driven-development/task-reviewer-prompt.md +2 -2
- package/superpowers/skills/systematic-debugging/root-cause-tracing.md +1 -1
- package/superpowers/skills/test-driven-development/SKILL.md +10 -0
- package/superpowers/skills/using-superpowers/SKILL.md +2 -0
- package/superpowers/skills/using-superpowers/references/claude-code-tools.md +29 -0
- package/superpowers/skills/using-superpowers/references/muse-tools.md +35 -0
- package/superpowers/skills/writing-plans/SKILL.md +30 -9
- package/superpowers/skills/writing-skills/SKILL.md +4 -2
- package/superpowers/skills/writing-skills/graphviz-conventions.dot +171 -171
|
@@ -1,554 +1,337 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* OpenSpec 工作流管理模块
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
{
|
|
55
|
-
{
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
]
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
.map(t => t.trim().replace(/^['"]|['"]$/g, ''))
|
|
339
|
-
.filter(Boolean);
|
|
340
|
-
tags = '[' + tagList.join(', ') + ']';
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// 提取 content (backtick 模板字符串;content 是 CommandTemplate 的最末字段,
|
|
344
|
-
// 所以以字符串结尾 `$` 作为锚点即可,不带 /m 标志)
|
|
345
|
-
let contentMatch = fields.match(/content:\s*`([\s\S]*?)`\s*$/);
|
|
346
|
-
if (!contentMatch) {
|
|
347
|
-
// 兜底: content 可能是函数调用,如 content: getOnboardInstructions(),
|
|
348
|
-
const fnCallMatch = fields.match(/content:\s*(\w+)\(\)/);
|
|
349
|
-
if (fnCallMatch) {
|
|
350
|
-
const helperFnName = fnCallMatch[1];
|
|
351
|
-
const helperFnRegex = new RegExp(
|
|
352
|
-
`function\\s+${helperFnName}\\s*\\([^)]*\\)\\s*:\\s*string\\s*\\{[\\s\\S]*?return\\s*\`([\\s\\S]*?)\`\\s*;\\s*\\}`,
|
|
353
|
-
'm'
|
|
354
|
-
);
|
|
355
|
-
const helperMatch = tsContent.match(helperFnRegex);
|
|
356
|
-
if (helperMatch) {
|
|
357
|
-
contentMatch = [null, helperMatch[1]];
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
if (!contentMatch) return null;
|
|
362
|
-
let content = unescapeTsTemplate(substituteStoreGuidance(contentMatch[1]));
|
|
363
|
-
content = content.replace(/^\n+/, '').replace(/\n+$/, '') + '\n';
|
|
364
|
-
|
|
365
|
-
// 派生 name: 'OPSX: <Title>' 来自 description 的若干词
|
|
366
|
-
// 实际从 CommandTemplate.name 字段读取
|
|
367
|
-
const nameMatch = fields.match(/name:\s*'([^']+)'/);
|
|
368
|
-
const name = nameMatch ? nameMatch[1] : `OPSX: ${description.split(/[.,:!?]/)[0].slice(0, 24)}`;
|
|
369
|
-
|
|
370
|
-
const fm = [
|
|
371
|
-
'---',
|
|
372
|
-
`name: "${name}"`,
|
|
373
|
-
`description: ${description}`,
|
|
374
|
-
`category: ${category}`,
|
|
375
|
-
`tags: ${tags}`,
|
|
376
|
-
'---',
|
|
377
|
-
'',
|
|
378
|
-
].join('\n');
|
|
379
|
-
|
|
380
|
-
return fm + content;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* 把 workflow 名映射到它对应的 TS 函数名。
|
|
385
|
-
* 例如 'propose' → getOpsxProposeCommandTemplate / getOpsxProposeSkillTemplate
|
|
386
|
-
*/
|
|
387
|
-
function functionNamesForWorkflow(name, kind /* 'cmd' | 'skill' */) {
|
|
388
|
-
// 所有官方 cmd 函数都以 getOpsx 开头,skill 以 get 开头,Workflow 名驼峰式接续
|
|
389
|
-
// 我们手动列出(因为没法从文件名可靠推导)
|
|
390
|
-
const map = {
|
|
391
|
-
'propose': { cmd: 'getOpsxProposeCommandTemplate', skill: 'getOpsxProposeSkillTemplate' },
|
|
392
|
-
'apply-change': { cmd: 'getOpsxApplyCommandTemplate', skill: 'getApplyChangeSkillTemplate' },
|
|
393
|
-
'archive-change': { cmd: 'getOpsxArchiveCommandTemplate', skill: 'getArchiveChangeSkillTemplate' },
|
|
394
|
-
'verify-change': { cmd: 'getOpsxVerifyCommandTemplate', skill: 'getVerifyChangeSkillTemplate' },
|
|
395
|
-
'ff-change': { cmd: 'getOpsxFfCommandTemplate', skill: 'getFfChangeSkillTemplate' },
|
|
396
|
-
'continue-change': { cmd: 'getOpsxContinueCommandTemplate', skill: 'getContinueChangeSkillTemplate' },
|
|
397
|
-
'new-change': { cmd: 'getOpsxNewCommandTemplate', skill: 'getNewChangeSkillTemplate' },
|
|
398
|
-
'explore': { cmd: 'getOpsxExploreCommandTemplate', skill: 'getExploreSkillTemplate' },
|
|
399
|
-
'update-change': { cmd: 'getOpsxUpdateCommandTemplate', skill: 'getUpdateChangeSkillTemplate' },
|
|
400
|
-
'sync-specs': { cmd: 'getOpsxSyncCommandTemplate', skill: 'getSyncSpecsSkillTemplate' },
|
|
401
|
-
'bulk-archive-change':{ cmd: 'getOpsxBulkArchiveCommandTemplate', skill: 'getBulkArchiveChangeSkillTemplate' },
|
|
402
|
-
'onboard': { cmd: 'getOpsxOnboardCommandTemplate', skill: 'getOnboardSkillTemplate' },
|
|
403
|
-
};
|
|
404
|
-
return map[name] ? map[name][kind] : null;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// ── 下载与部署 ──────────────────────────────────────────────────
|
|
408
|
-
|
|
409
|
-
async function fetchLatestOpenSpec() {
|
|
410
|
-
log(`正在获取 OpenSpec 版本元数据...`, 'info');
|
|
411
|
-
// 1) 先 HEAD 拿 ETag,避免每次都下载完整 tarball
|
|
412
|
-
const head = await fetchEtag(GITHUB_TARBALL_URL);
|
|
413
|
-
if (head.statusCode !== 200 || !head.etag) {
|
|
414
|
-
throw new Error(`无法获取版本信息: HTTP ${head.statusCode}`);
|
|
415
|
-
}
|
|
416
|
-
const versionToken = head.etag;
|
|
417
|
-
const localToken = getLocalVersion();
|
|
418
|
-
|
|
419
|
-
if (versionToken === localToken) {
|
|
420
|
-
// 即使版本号一致,也要校验全部文件是否已部署(白名单可能新增了条目)
|
|
421
|
-
const { valid } = validateDeployed();
|
|
422
|
-
if (valid) {
|
|
423
|
-
log(`已是最新版本 (${versionToken.slice(0, 12)})`, 'success');
|
|
424
|
-
return { version: versionToken, cmdCount: 0, skillCount: 0, skipped: true };
|
|
425
|
-
}
|
|
426
|
-
log(`版本一致但文件不完整,强制重新部署...`, 'info');
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
log(`检测到上游更新,正在下载 OpenSpec tarball...`, 'info');
|
|
430
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cbb-os-'));
|
|
431
|
-
const tarballPath = path.join(tmpDir, 'openspec.tar.gz');
|
|
432
|
-
|
|
433
|
-
try {
|
|
434
|
-
await downloadToFile(GITHUB_TARBALL_URL, tarballPath);
|
|
435
|
-
log('正在解压...', 'info');
|
|
436
|
-
// 跨平台解压:用 zlib + 自写 tar parser。无需 shell tar,Windows 也能跑。
|
|
437
|
-
extractTarGz(tarballPath, tmpDir);
|
|
438
|
-
|
|
439
|
-
// 通过「含 src/core/templates/workflows」特征定位真仓库根。
|
|
440
|
-
// codeload 的 tarball 顶层可能是 pax_global_header / 杂项,但
|
|
441
|
-
// 真正的源码根目录固定有一个 src/ 子树。这种探测比依赖目录名约定稳。
|
|
442
|
-
let extractedDir = null;
|
|
443
|
-
for (const entry of fs.readdirSync(tmpDir)) {
|
|
444
|
-
const cand = path.join(tmpDir, entry);
|
|
445
|
-
let isDir = false;
|
|
446
|
-
try { isDir = fs.statSync(cand).isDirectory(); } catch (_) { continue; }
|
|
447
|
-
if (!isDir) continue;
|
|
448
|
-
const probe = path.join(cand, 'src', 'core', 'templates', 'workflows');
|
|
449
|
-
if (fs.existsSync(probe)) {
|
|
450
|
-
extractedDir = cand;
|
|
451
|
-
break;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
if (!extractedDir) {
|
|
455
|
-
throw new Error('解压后未在 tarball 中定位到含 src/core/templates/workflows 的 OpenSpec 根目录');
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
const workflowsDir = path.join(extractedDir, 'src', 'core', 'templates', 'workflows');
|
|
459
|
-
if (!fs.existsSync(workflowsDir)) throw new Error('未找到 workflows 目录');
|
|
460
|
-
|
|
461
|
-
let cmdCount = 0;
|
|
462
|
-
let skillCount = 0;
|
|
463
|
-
|
|
464
|
-
for (const wf of WORKFLOWS) {
|
|
465
|
-
const tsFile = path.join(workflowsDir, `${wf.name}.ts`);
|
|
466
|
-
if (!fs.existsSync(tsFile)) {
|
|
467
|
-
log(`跳过 ${wf.name}: 官方源无对应文件`, 'warn');
|
|
468
|
-
continue;
|
|
469
|
-
}
|
|
470
|
-
const tsContent = fs.readFileSync(tsFile, 'utf-8');
|
|
471
|
-
|
|
472
|
-
// 命令
|
|
473
|
-
if (wf.cmd) {
|
|
474
|
-
const fnName = functionNamesForWorkflow(wf.name, 'cmd');
|
|
475
|
-
const rendered = fnName ? renderCommandFromTs(tsContent, fnName) : null;
|
|
476
|
-
if (!rendered) {
|
|
477
|
-
log(`${wf.name}.ts: 未提取到 CommandTemplate,跳过`, 'warn');
|
|
478
|
-
} else {
|
|
479
|
-
const dest = path.join(OPENSPEC_COMMANDS_DIR, `${wf.cmd}.md`);
|
|
480
|
-
assertSafeDestination(dest, `写入命令 ${wf.name}`);
|
|
481
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
482
|
-
fs.writeFileSync(dest, rendered);
|
|
483
|
-
cmdCount++;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// 技能
|
|
488
|
-
if (wf.skill) {
|
|
489
|
-
const fnName = functionNamesForWorkflow(wf.name, 'skill');
|
|
490
|
-
const rendered = fnName ? renderSkillFromTs(tsContent, fnName) : null;
|
|
491
|
-
if (!rendered) {
|
|
492
|
-
log(`${wf.name}.ts: 未提取到 SkillTemplate,跳过`, 'warn');
|
|
493
|
-
} else {
|
|
494
|
-
const dest = path.join(OPENSPEC_SKILLS_DIR, wf.skill, 'SKILL.md');
|
|
495
|
-
assertSafeDestination(dest, `写入技能 ${wf.name}`);
|
|
496
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
497
|
-
fs.writeFileSync(dest, rendered);
|
|
498
|
-
skillCount++;
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
writeVersion(versionToken);
|
|
504
|
-
log(
|
|
505
|
-
`已更新 OpenSpec 工作流: ${localToken ? localToken.slice(0, 12) : '(无)'} → ${versionToken.slice(0, 12)} ` +
|
|
506
|
-
`(${cmdCount} 个命令 + ${skillCount} 个技能)`,
|
|
507
|
-
'success'
|
|
508
|
-
);
|
|
509
|
-
|
|
510
|
-
return { version: versionToken, cmdCount, skillCount, skipped: false };
|
|
511
|
-
} catch (err) {
|
|
512
|
-
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) { /* ignore */ }
|
|
513
|
-
throw err;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// ── 完整性校验 ──────────────────────────────────────────────────
|
|
518
|
-
|
|
519
|
-
/**
|
|
520
|
-
* 校验 Safe-Overwrite 区的所有文件均已部署。
|
|
521
|
-
* @returns {{valid: boolean, missing: string[]}}
|
|
522
|
-
*/
|
|
523
|
-
function validateDeployed() {
|
|
524
|
-
const missing = [];
|
|
525
|
-
for (const wf of WORKFLOWS) {
|
|
526
|
-
if (wf.cmd) {
|
|
527
|
-
const p = path.join(OPENSPEC_COMMANDS_DIR, `${wf.cmd}.md`);
|
|
528
|
-
if (!fs.existsSync(p)) missing.push(`commands/${wf.cmd}.md`);
|
|
529
|
-
}
|
|
530
|
-
if (wf.skill) {
|
|
531
|
-
const p = path.join(OPENSPEC_SKILLS_DIR, wf.skill, 'SKILL.md');
|
|
532
|
-
if (!fs.existsSync(p)) missing.push(`skills/${wf.skill}/SKILL.md`);
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
return { valid: missing.length === 0, missing };
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// ── 导出 ────────────────────────────────────────────────────────
|
|
539
|
-
|
|
540
|
-
module.exports = {
|
|
541
|
-
OPENSPEC_DIR,
|
|
542
|
-
OPENSPEC_COMMANDS_DIR,
|
|
543
|
-
OPENSPEC_SKILLS_DIR,
|
|
544
|
-
VERSION_FILE,
|
|
545
|
-
WORKFLOWS,
|
|
546
|
-
GITHUB_REPO,
|
|
547
|
-
GITHUB_BRANCH,
|
|
548
|
-
|
|
549
|
-
PROTECTED_PATH_PREFIXES,
|
|
550
|
-
assertSafeDestination,
|
|
551
|
-
fetchLatestOpenSpec,
|
|
552
|
-
getLocalVersion,
|
|
553
|
-
validateDeployed,
|
|
554
|
-
};
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OpenSpec 工作流管理模块
|
|
5
|
+
*
|
|
6
|
+
* 内容来源 = 指定版本的 OpenSpec CLI npm 包(默认 npmmirror,可配置镜像),
|
|
7
|
+
* 调用该包自带的渲染器产出 canonical 形态的 commands + skills,再由本模块
|
|
8
|
+
* 套上本仓库的 frontmatter 格式写入 openspec/{commands,skills}/。
|
|
9
|
+
*
|
|
10
|
+
* 为什么不再自研 TS 模板解析:上游 1.13.1 起模板引入 PROJECT_ROOT_GUARD、
|
|
11
|
+
* optionalWorkflow 条件块等新语法,正则解析会持续落后。改用 CLI 自带渲染器后,
|
|
12
|
+
* 内容与 CLI 版本严格一致,且完全不依赖 GitHub(用户/维护者本机可达 npmmirror)。
|
|
13
|
+
*
|
|
14
|
+
* 接缝:用 getSkillTemplates()/getCommandContents()(canonical `/opsx:<id>` 形态),
|
|
15
|
+
* 不用 CLI 的 generateCommand()(那会按 CLI 自己的适配器格式化)——
|
|
16
|
+
* 分发格式的唯一决定者仍是本仓库的适配器改写层。
|
|
17
|
+
*
|
|
18
|
+
* 升级范围严格限定:只有 WORKFLOWS 白名单中的 commands/*.md 和
|
|
19
|
+
* skills/<name>/SKILL.md 会被覆盖写入。config/openspec/(schemas + config.yaml)、
|
|
20
|
+
* cbb/worktrees/ 等项目自有扩展位不会被触碰 —— 由 assertSafeDestination
|
|
21
|
+
* 兜底拦截,任何写入命中受保护前缀都会抛错退出。
|
|
22
|
+
*
|
|
23
|
+
* 升级方式:cbbm update --upstream-openspec(按 .version 的 pin 幂等重渲染)
|
|
24
|
+
* 版本检查:cbbm check-upstream
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
const os = require('os');
|
|
30
|
+
const url = require('url');
|
|
31
|
+
|
|
32
|
+
const { log } = require('../utils/output');
|
|
33
|
+
const { extractTarGz } = require('../utils/tar');
|
|
34
|
+
const { loadMirrors, fetchWithMirrors } = require('../utils/upstream');
|
|
35
|
+
|
|
36
|
+
const SHARED_STANDARDS_DIR = path.join(__dirname, '..', '..', '..');
|
|
37
|
+
|
|
38
|
+
// 部署目标根目录
|
|
39
|
+
const OPENSPEC_DIR = path.join(SHARED_STANDARDS_DIR, 'openspec');
|
|
40
|
+
const OPENSPEC_COMMANDS_DIR = path.join(OPENSPEC_DIR, 'commands');
|
|
41
|
+
const OPENSPEC_SKILLS_DIR = path.join(OPENSPEC_DIR, 'skills');
|
|
42
|
+
|
|
43
|
+
// 版本锁定文件(pin)
|
|
44
|
+
const VERSION_FILE = path.join(OPENSPEC_DIR, '.version');
|
|
45
|
+
|
|
46
|
+
// 上游包信息
|
|
47
|
+
const NPM_PKG = '@fission-ai/openspec';
|
|
48
|
+
const NPM_PKG_BASENAME = 'openspec';
|
|
49
|
+
const GITHUB_REPO = 'Fission-AI/OpenSpec';
|
|
50
|
+
|
|
51
|
+
// 升级白名单:每个工作流的命令文件名 + skill 目录名
|
|
52
|
+
// 用于 validateDeployed(不下载即可校验完整性)与写入目标定位。
|
|
53
|
+
const WORKFLOWS = [
|
|
54
|
+
{ id: 'propose', cmd: 'propose', skill: 'openspec-propose' },
|
|
55
|
+
{ id: 'apply', cmd: 'apply', skill: 'openspec-apply-change' },
|
|
56
|
+
{ id: 'archive', cmd: 'archive', skill: 'openspec-archive-change' },
|
|
57
|
+
{ id: 'verify', cmd: 'verify', skill: 'openspec-verify-change' },
|
|
58
|
+
{ id: 'ff', cmd: 'ff', skill: 'openspec-ff-change' },
|
|
59
|
+
{ id: 'continue', cmd: 'continue', skill: 'openspec-continue-change' },
|
|
60
|
+
{ id: 'new', cmd: 'new', skill: 'openspec-new-change' },
|
|
61
|
+
{ id: 'explore', cmd: 'explore', skill: 'openspec-explore' },
|
|
62
|
+
{ id: 'update', cmd: 'update', skill: 'openspec-update-change' },
|
|
63
|
+
{ id: 'sync', cmd: 'sync', skill: 'openspec-sync-specs' },
|
|
64
|
+
{ id: 'bulk-archive', cmd: 'bulk-archive', skill: 'openspec-bulk-archive-change' },
|
|
65
|
+
{ id: 'onboard', cmd: 'onboard', skill: 'openspec-onboard' },
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
// ── 受保护路径(项目自有扩展位,绝对禁止覆盖写入)─────────────
|
|
69
|
+
//
|
|
70
|
+
// 与 AGENTS.md「原生内容保护与扩展位」一致:
|
|
71
|
+
// - config/openspec/schemas/ 项目自托管 schema 与模板(spec-driven)
|
|
72
|
+
// - cbb/worktrees/ 项目自有扩展(worktree 命令/技能/_shared)
|
|
73
|
+
// - config/openspec/config.yaml 项目自有
|
|
74
|
+
//
|
|
75
|
+
// 任何写入命中下列任一前缀,assertSafeDestination 立即抛错,
|
|
76
|
+
// 避免后续重构意外把 schema.yaml / config.yaml / worktrees/ 冲掉。
|
|
77
|
+
const PROTECTED_PATH_PREFIXES = [
|
|
78
|
+
path.join(SHARED_STANDARDS_DIR, 'config', 'openspec', 'schemas'),
|
|
79
|
+
path.join(SHARED_STANDARDS_DIR, 'cbb', 'worktrees'),
|
|
80
|
+
path.join(SHARED_STANDARDS_DIR, 'config', 'openspec', 'config.yaml'),
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
function assertSafeDestination(destPath, op) {
|
|
84
|
+
const abs = path.resolve(destPath);
|
|
85
|
+
for (const prefix of PROTECTED_PATH_PREFIXES) {
|
|
86
|
+
if (abs === prefix || abs.startsWith(prefix + path.sep)) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`[cbbm update --upstream-openspec] ${op} 试图写入受保护路径: ${abs}\n` +
|
|
89
|
+
`命中前缀: ${prefix}\n` +
|
|
90
|
+
`该路径属于项目自有扩展位,绝对禁止被 cbbm update --upstream-openspec 覆盖。\n` +
|
|
91
|
+
`详见 AGENTS.md「原生内容保护与扩展位」一节。`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── 版本管理(pin)────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 读取本地 pin(.version 的 version 字段)。
|
|
101
|
+
* 兼容旧格式:无 version 时回退 sha(历史 ETag/commit)。
|
|
102
|
+
* @returns {string|null}
|
|
103
|
+
*/
|
|
104
|
+
function getLocalVersion() {
|
|
105
|
+
if (!fs.existsSync(VERSION_FILE)) return null;
|
|
106
|
+
try {
|
|
107
|
+
const content = fs.readFileSync(VERSION_FILE, 'utf-8').trim();
|
|
108
|
+
if (content.startsWith('{')) {
|
|
109
|
+
const data = JSON.parse(content);
|
|
110
|
+
return data.version || data.sha || null;
|
|
111
|
+
}
|
|
112
|
+
return content || null;
|
|
113
|
+
} catch (_) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function writeVersion(version) {
|
|
119
|
+
const data = {
|
|
120
|
+
version,
|
|
121
|
+
repo: GITHUB_REPO,
|
|
122
|
+
renderedAt: new Date().toISOString(),
|
|
123
|
+
};
|
|
124
|
+
fs.writeFileSync(VERSION_FILE, JSON.stringify(data, null, 2) + '\n');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── 上游拉取 ──────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
/** npm tarball 下载地址(按镜像前缀拼接) */
|
|
130
|
+
function tarballUrl(mirror, version) {
|
|
131
|
+
return `${mirror}/${NPM_PKG}/-/${NPM_PKG_BASENAME}-${version}.tgz`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** npm registry 元数据地址(用于 check-upstream) */
|
|
135
|
+
function registryUrl(mirror) {
|
|
136
|
+
return `${mirror}/${NPM_PKG}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 下载指定版本的 OpenSpec CLI npm tarball 并解压,返回包根目录。
|
|
141
|
+
* @param {string} version
|
|
142
|
+
* @param {string} tmpDir
|
|
143
|
+
* @returns {Promise<string>} 解压出的 package/ 绝对路径
|
|
144
|
+
*/
|
|
145
|
+
async function downloadCliPackage(version, tmpDir) {
|
|
146
|
+
const mirrors = loadMirrors('openspec');
|
|
147
|
+
log(`正在获取 OpenSpec CLI ${version} 包(${mirrors.map(m => m || '直连').join(' → ')})...`, 'info');
|
|
148
|
+
const { buffer, mirror } = await fetchWithMirrors(mirrors, m => tarballUrl(m, version));
|
|
149
|
+
log(`已从 ${mirror || '(直连)'} 下载 ${(buffer.length / 1024).toFixed(0)} KB`, 'info');
|
|
150
|
+
|
|
151
|
+
const pkgDir = path.join(tmpDir, 'pkg');
|
|
152
|
+
fs.mkdirSync(pkgDir, { recursive: true });
|
|
153
|
+
const { files } = extractTarGz(buffer, pkgDir);
|
|
154
|
+
|
|
155
|
+
const extracted = path.join(pkgDir, 'package');
|
|
156
|
+
if (!fs.existsSync(extracted)) {
|
|
157
|
+
throw new Error('解压后未找到 package/ 目录');
|
|
158
|
+
}
|
|
159
|
+
log(`已解压 ${files} 个文件`, 'info');
|
|
160
|
+
return extracted;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 加载 CLI 自带渲染器。
|
|
165
|
+
* @param {string} pkgDir - 解压出的 package/ 目录
|
|
166
|
+
* @returns {Promise<{getSkillTemplates: Function, getCommandContents: Function, generateSkillContent: Function, version: string}>}
|
|
167
|
+
*/
|
|
168
|
+
async function loadCliRenderer(pkgDir) {
|
|
169
|
+
const entry = path.join(pkgDir, 'dist', 'core', 'shared', 'skill-generation.js');
|
|
170
|
+
if (!fs.existsSync(entry)) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`未找到 CLI 渲染器: ${path.relative(pkgDir, entry)}\n` +
|
|
173
|
+
`该路径属于 @fission-ai/openspec 的内部结构,上游重构可能导致失效。`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
const mod = await import(url.pathToFileURL(entry).href);
|
|
177
|
+
const pkgJson = JSON.parse(fs.readFileSync(path.join(pkgDir, 'package.json'), 'utf-8'));
|
|
178
|
+
|
|
179
|
+
for (const fn of ['getSkillTemplates', 'getCommandContents', 'generateSkillContent']) {
|
|
180
|
+
if (typeof mod[fn] !== 'function') {
|
|
181
|
+
throw new Error(`CLI 渲染器缺少导出函数 ${fn}(@fission-ai/openspec@${pkgJson.version})`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return { ...mod, version: pkgJson.version };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── 渲染:套本仓库的 frontmatter 格式 ─────────────────────────
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 生成 YAML 标量:需要引号时用双引号包裹,否则原样输出(与仓库既有风格一致)。
|
|
191
|
+
*/
|
|
192
|
+
function yamlScalar(value) {
|
|
193
|
+
const s = String(value == null ? '' : value);
|
|
194
|
+
if (s === '' || /^[\s>|&*!%@`{}[\],#?:-]/.test(s) || /[:#]\s/.test(s) || /["'\\\n\r\t]/.test(s)) {
|
|
195
|
+
return JSON.stringify(s);
|
|
196
|
+
}
|
|
197
|
+
return s;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** 渲染命令 .md(本仓库格式:name / description / category / tags) */
|
|
201
|
+
function renderCommand(commandContent) {
|
|
202
|
+
const tags = Array.isArray(commandContent.tags) ? commandContent.tags : [];
|
|
203
|
+
return [
|
|
204
|
+
'---',
|
|
205
|
+
`name: ${yamlScalar(commandContent.name)}`,
|
|
206
|
+
`description: ${yamlScalar(commandContent.description)}`,
|
|
207
|
+
`category: ${yamlScalar(commandContent.category)}`,
|
|
208
|
+
`tags: [${tags.join(', ')}]`,
|
|
209
|
+
'---',
|
|
210
|
+
commandContent.body.replace(/\n+$/, '') + '\n',
|
|
211
|
+
].join('\n');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── 主流程 ────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* 按 pin 拉取并重新渲染 OpenSpec 工作流到本仓库。
|
|
218
|
+
* 幂等:重复执行结果一致。
|
|
219
|
+
* @returns {Promise<{version: string, cmdCount: number, skillCount: number, skipped: boolean}>}
|
|
220
|
+
*/
|
|
221
|
+
async function fetchLatestOpenSpec() {
|
|
222
|
+
const pin = getLocalVersion();
|
|
223
|
+
if (!pin) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`未找到 OpenSpec 版本 pin(${path.relative(SHARED_STANDARDS_DIR, VERSION_FILE)} 缺失或损坏)。\n` +
|
|
226
|
+
`请先写入 { "version": "<x.y.z>" } 再执行。`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// pin 一致且文件齐全 → 幂等跳过
|
|
231
|
+
if (validateDeployed().valid) {
|
|
232
|
+
log(`OpenSpec 内容已按 pin ${pin} 就位,跳过重渲染`, 'success');
|
|
233
|
+
return { version: pin, cmdCount: 0, skillCount: 0, skipped: true };
|
|
234
|
+
}
|
|
235
|
+
log(`检测到内容不完整,按 pin ${pin} 重新渲染...`, 'info');
|
|
236
|
+
|
|
237
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cbb-os-'));
|
|
238
|
+
try {
|
|
239
|
+
const pkgDir = await downloadCliPackage(pin, tmpDir);
|
|
240
|
+
const cli = await loadCliRenderer(pkgDir);
|
|
241
|
+
if (cli.version !== pin) {
|
|
242
|
+
log(`警告:pin 为 ${pin},但包内版本为 ${cli.version}`, 'warn');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const skills = cli.getSkillTemplates();
|
|
246
|
+
const commands = cli.getCommandContents();
|
|
247
|
+
const cmdById = new Map(commands.map(c => [c.id, c]));
|
|
248
|
+
const skillByName = new Map(skills.map(s => [s.dirName, s]));
|
|
249
|
+
|
|
250
|
+
let cmdCount = 0;
|
|
251
|
+
let skillCount = 0;
|
|
252
|
+
|
|
253
|
+
for (const wf of WORKFLOWS) {
|
|
254
|
+
const cmd = cmdById.get(wf.cmd);
|
|
255
|
+
if (!cmd) {
|
|
256
|
+
log(`跳过命令 ${wf.cmd}: 官方渲染器无此 workflow`, 'warn');
|
|
257
|
+
} else {
|
|
258
|
+
const dest = path.join(OPENSPEC_COMMANDS_DIR, `${wf.cmd}.md`);
|
|
259
|
+
assertSafeDestination(dest, `写入命令 ${wf.cmd}`);
|
|
260
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
261
|
+
fs.writeFileSync(dest, renderCommand(cmd));
|
|
262
|
+
cmdCount += 1;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const skill = skillByName.get(wf.skill);
|
|
266
|
+
if (!skill) {
|
|
267
|
+
log(`跳过技能 ${wf.skill}: 官方渲染器无此 workflow`, 'warn');
|
|
268
|
+
} else {
|
|
269
|
+
const dest = path.join(OPENSPEC_SKILLS_DIR, wf.skill, 'SKILL.md');
|
|
270
|
+
assertSafeDestination(dest, `写入技能 ${wf.skill}`);
|
|
271
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
272
|
+
fs.writeFileSync(dest, cli.generateSkillContent(skill.template, cli.version));
|
|
273
|
+
skillCount += 1;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
writeVersion(pin);
|
|
278
|
+
log(`OpenSpec 工作流已渲染: ${pin}(${cmdCount} 个命令 + ${skillCount} 个技能)`, 'success');
|
|
279
|
+
return { version: pin, cmdCount, skillCount, skipped: false };
|
|
280
|
+
} finally {
|
|
281
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) { /* ignore */ }
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* 检查上游是否有新版本(不改动任何文件)。
|
|
287
|
+
* @returns {Promise<{current: string|null, latest: string|null, outdated: boolean}>}
|
|
288
|
+
*/
|
|
289
|
+
async function checkUpstream() {
|
|
290
|
+
const current = getLocalVersion();
|
|
291
|
+
const mirrors = loadMirrors('openspec');
|
|
292
|
+
const { buffer } = await fetchWithMirrors(mirrors, m => registryUrl(m));
|
|
293
|
+
const data = JSON.parse(buffer.toString('utf-8'));
|
|
294
|
+
const latest = (data['dist-tags'] || {}).latest || null;
|
|
295
|
+
const outdated = Boolean(current && latest && current !== latest);
|
|
296
|
+
return { current, latest, outdated };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ── 完整性校验 ────────────────────────────────────────────────
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 校验 Safe-Overwrite 区的所有文件均已部署(无需下载)。
|
|
303
|
+
* @returns {{valid: boolean, missing: string[]}}
|
|
304
|
+
*/
|
|
305
|
+
function validateDeployed() {
|
|
306
|
+
const missing = [];
|
|
307
|
+
for (const wf of WORKFLOWS) {
|
|
308
|
+
if (wf.cmd) {
|
|
309
|
+
const p = path.join(OPENSPEC_COMMANDS_DIR, `${wf.cmd}.md`);
|
|
310
|
+
if (!fs.existsSync(p)) missing.push(`commands/${wf.cmd}.md`);
|
|
311
|
+
}
|
|
312
|
+
if (wf.skill) {
|
|
313
|
+
const p = path.join(OPENSPEC_SKILLS_DIR, wf.skill, 'SKILL.md');
|
|
314
|
+
if (!fs.existsSync(p)) missing.push(`skills/${wf.skill}/SKILL.md`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return { valid: missing.length === 0, missing };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ── 导出 ──────────────────────────────────────────────────────
|
|
321
|
+
|
|
322
|
+
module.exports = {
|
|
323
|
+
OPENSPEC_DIR,
|
|
324
|
+
OPENSPEC_COMMANDS_DIR,
|
|
325
|
+
OPENSPEC_SKILLS_DIR,
|
|
326
|
+
VERSION_FILE,
|
|
327
|
+
WORKFLOWS,
|
|
328
|
+
GITHUB_REPO,
|
|
329
|
+
NPM_PKG,
|
|
330
|
+
|
|
331
|
+
PROTECTED_PATH_PREFIXES,
|
|
332
|
+
assertSafeDestination,
|
|
333
|
+
fetchLatestOpenSpec,
|
|
334
|
+
checkUpstream,
|
|
335
|
+
getLocalVersion,
|
|
336
|
+
validateDeployed,
|
|
337
|
+
};
|