@hegemonart/get-design-done 1.28.7 → 1.30.0
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +116 -0
- package/README.de.md +37 -0
- package/README.fr.md +37 -0
- package/README.it.md +37 -0
- package/README.ja.md +37 -0
- package/README.ko.md +37 -0
- package/README.md +44 -0
- package/README.zh-CN.md +37 -0
- package/SKILL.md +12 -10
- package/agents/design-reflector.md +50 -0
- package/package.json +3 -1
- package/reference/capability-gap-stage-gate.md +261 -0
- package/reference/known-failure-modes.md +185 -0
- package/reference/pseudonymization-rules.md +189 -0
- package/reference/registry.json +22 -1
- package/reference/schemas/events.schema.json +97 -3
- package/reference/schemas/generated.d.ts +319 -4
- package/scripts/build-distribution-bundles.cjs +549 -0
- package/scripts/cli/gdd-events.mjs +35 -2
- package/scripts/gsd-cleanup-incubator.cjs +367 -0
- package/scripts/install.cjs +61 -0
- package/scripts/lib/apply-reflections/incubator-proposals.cjs +448 -0
- package/scripts/lib/bandit-router.cjs +92 -9
- package/scripts/lib/gsd-health-mirror/index.cjs +37 -1
- package/scripts/lib/incubator-author.cjs +845 -0
- package/scripts/lib/install/config-dir.cjs +26 -0
- package/scripts/lib/install/converters/codex-plugin.cjs +407 -0
- package/scripts/lib/install/converters/cursor-marketplace.cjs +309 -0
- package/scripts/lib/install/doctor-codex-plugin.cjs +388 -0
- package/scripts/lib/install/doctor-cursor-marketplace.cjs +366 -0
- package/scripts/lib/install/doctor-tier2.cjs +586 -0
- package/scripts/lib/install/runtimes.cjs +48 -0
- package/scripts/lib/issue-reporter/cli-flag-report.cjs +153 -0
- package/scripts/lib/issue-reporter/consent-prompt.cjs +231 -0
- package/scripts/lib/issue-reporter/dedup.cjs +458 -0
- package/scripts/lib/issue-reporter/destination.cjs +37 -0
- package/scripts/lib/issue-reporter/draft-writer.cjs +157 -0
- package/scripts/lib/issue-reporter/gh-absent-fallback.cjs +220 -0
- package/scripts/lib/issue-reporter/gh-submit.cjs +114 -0
- package/scripts/lib/issue-reporter/kill-switch.cjs +122 -0
- package/scripts/lib/issue-reporter/payload-assembly.cjs +367 -0
- package/scripts/lib/issue-reporter/privacy-diff.cjs +385 -0
- package/scripts/lib/issue-reporter/report-flow.cjs +269 -0
- package/scripts/lib/issue-reporter/triage-matcher.cjs +270 -0
- package/scripts/lib/pseudonymize.cjs +444 -0
- package/scripts/lib/reflections-cycle-writer.cjs +172 -0
- package/scripts/lib/reflector/capability-gap-scan.cjs +751 -0
- package/scripts/lib/reflector-capability-gap-aggregator.cjs +320 -0
- package/scripts/lint-agentskills-spec.cjs +457 -0
- package/scripts/release-smoke-test.cjs +33 -2
- package/scripts/validate-incubator-scope.cjs +133 -0
- package/skills/apply-reflections/SKILL.md +16 -1
- package/skills/apply-reflections/apply-reflections-procedure.md +71 -3
- package/skills/compare/SKILL.md +2 -2
- package/skills/compare/compare-rubric.md +1 -1
- package/skills/darkmode/SKILL.md +2 -2
- package/skills/darkmode/darkmode-audit-procedure.md +1 -1
- package/skills/fast/SKILL.md +46 -0
- package/skills/figma-write/SKILL.md +2 -2
- package/skills/graphify/SKILL.md +2 -2
- package/skills/reflect/SKILL.md +9 -0
- package/skills/reflect/procedures/capability-gap-scan.md +120 -0
- package/skills/report-issue/SKILL.md +53 -0
- package/skills/report-issue/report-issue-procedure.md +120 -0
- package/skills/router/SKILL.md +5 -0
- package/skills/router/capability-gap-emitter.md +65 -0
- package/skills/style/SKILL.md +2 -2
- package/skills/style/style-doc-procedure.md +1 -1
- package/skills/update/SKILL.md +3 -2
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* scripts/build-distribution-bundles.cjs — Phase 28.8 (Plan 28-8-X1).
|
|
4
|
+
*
|
|
5
|
+
* Shared-source / multi-channel distribution bundler.
|
|
6
|
+
*
|
|
7
|
+
* Per CONTEXT D-06: skills are shared source / per-channel converters at
|
|
8
|
+
* distribution-build time. This script fans out canonical `skills/` into
|
|
9
|
+
* three channel-specific bundles under `dist/`:
|
|
10
|
+
*
|
|
11
|
+
* - dist/cursor-marketplace/ (via scripts/lib/install/converters/cursor-marketplace.cjs)
|
|
12
|
+
* - dist/codex-plugin/ (via scripts/lib/install/converters/codex-plugin.cjs)
|
|
13
|
+
* - dist/agentskills-io/ (passthrough per D-13 lint-only)
|
|
14
|
+
*
|
|
15
|
+
* Tier-2 channels are discovered by inspecting `runtimes.cjs` entries with
|
|
16
|
+
* `kind: 'cursor-marketplace'` or `kind: 'codex-plugin'`. `agentskills-io`
|
|
17
|
+
* is hardcoded as a passthrough (it's a spec, not a runtime — D-02/D-13).
|
|
18
|
+
*
|
|
19
|
+
* Determinism: two consecutive runs produce byte-identical output.
|
|
20
|
+
* Tier-1 unaffected: only writes under `dist/`.
|
|
21
|
+
*
|
|
22
|
+
* --- ADAPTER NOTE (Plan 28-8-X1 implementation): the Wave-B converters
|
|
23
|
+
* (B1: cursor-marketplace.cjs, C1: codex-plugin.cjs) actually export
|
|
24
|
+
* `{ buildManifest, convert, CURATED_KEYWORDS }` — NOT the
|
|
25
|
+
* `{ convertSkill, buildManifest, MANIFEST_PATH }` shape that the plan's
|
|
26
|
+
* `<interfaces>` block hypothesized. Per the plan's "Adapter divergence
|
|
27
|
+
* handling" clause, this bundler adapts to the actual converter shape
|
|
28
|
+
* rather than modifying the converters (which are already shipped and
|
|
29
|
+
* out of scope per D-05).
|
|
30
|
+
*
|
|
31
|
+
* Actual converter contract used here:
|
|
32
|
+
* - converter.buildManifest({ packageJson, claudePlugin, claudePluginJson,
|
|
33
|
+
* marketplaceJson, readmeFirstPara })
|
|
34
|
+
* → returns a manifest OBJECT (not a string).
|
|
35
|
+
* (cursor-marketplace looks for `claudePluginJson`; codex-plugin
|
|
36
|
+
* looks for `claudePlugin`. We pass BOTH keys to be compatible
|
|
37
|
+
* with either accessor.)
|
|
38
|
+
* - converter.convert({ skillsDir, outDir, manifest })
|
|
39
|
+
* → writes manifest + copies skills/ tree under outDir. Owns its
|
|
40
|
+
* own manifest path (.cursor-plugin/plugin.json or .codex-plugin/
|
|
41
|
+
* plugin.json) — the bundler doesn't need to know.
|
|
42
|
+
*
|
|
43
|
+
* CLI:
|
|
44
|
+
* node scripts/build-distribution-bundles.cjs # all channels
|
|
45
|
+
* node scripts/build-distribution-bundles.cjs --channel cursor-marketplace
|
|
46
|
+
* node scripts/build-distribution-bundles.cjs --help
|
|
47
|
+
*
|
|
48
|
+
* Exit codes: 0 ok / 1 converter error / 2 missing dependency.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
const fs = require('fs');
|
|
52
|
+
const path = require('path');
|
|
53
|
+
|
|
54
|
+
const EXIT_CODES = Object.freeze({
|
|
55
|
+
OK: 0,
|
|
56
|
+
CONVERTER_ERROR: 1,
|
|
57
|
+
MISSING_DEPENDENCY: 2,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// agentskills-io is hardcoded — it is a spec, not a runtime entry
|
|
61
|
+
// (per CONTEXT D-02 / D-13). No converter file, no manifest file.
|
|
62
|
+
const PASSTHROUGH_CHANNEL = Object.freeze({
|
|
63
|
+
id: 'agentskills-io',
|
|
64
|
+
kind: 'passthrough',
|
|
65
|
+
converterPath: null,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Set of runtime `kind` values that the bundler dispatches to Wave-B
|
|
69
|
+
// converters. Hardcoded to two kinds — adding a third Tier-2 channel in
|
|
70
|
+
// a future phase requires (a) adding the runtime entry with a new kind,
|
|
71
|
+
// (b) shipping a converter at scripts/lib/install/converters/<kind>.cjs,
|
|
72
|
+
// (c) extending this set. The channel-ID discovery itself is data-driven.
|
|
73
|
+
const TIER2_KINDS = Object.freeze(new Set(['cursor-marketplace', 'codex-plugin']));
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------
|
|
76
|
+
// Channel discovery
|
|
77
|
+
// ---------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Discover Tier-2 channels from the runtimes registry + add the hardcoded
|
|
81
|
+
* passthrough channel. Returns Array<{id, kind, converterPath}>.
|
|
82
|
+
*
|
|
83
|
+
* `runtimesModule` is dependency-injected so tests can supply a fixture.
|
|
84
|
+
* Production callers pass `require('./lib/install/runtimes.cjs')`.
|
|
85
|
+
*
|
|
86
|
+
* Determinism: runtime list sorted lexicographically by id before iteration.
|
|
87
|
+
* The hardcoded PASSTHROUGH_CHANNEL is appended last; callers that want a
|
|
88
|
+
* fully lexicographic ordering should re-sort the returned array.
|
|
89
|
+
*/
|
|
90
|
+
function discoverTier2Channels(runtimesModule) {
|
|
91
|
+
const channels = [];
|
|
92
|
+
const runtimes = (runtimesModule && typeof runtimesModule.listRuntimes === 'function')
|
|
93
|
+
? runtimesModule.listRuntimes()
|
|
94
|
+
: [];
|
|
95
|
+
const sorted = runtimes.slice().sort((a, b) => a.id.localeCompare(b.id));
|
|
96
|
+
for (const rt of sorted) {
|
|
97
|
+
if (!TIER2_KINDS.has(rt.kind)) continue;
|
|
98
|
+
channels.push({
|
|
99
|
+
id: rt.id,
|
|
100
|
+
kind: rt.kind,
|
|
101
|
+
// Converter file lives at scripts/lib/install/converters/<kind>.cjs.
|
|
102
|
+
// T-28.8-X1-01 (Tampering / require()): `kind` originates in the
|
|
103
|
+
// version-controlled runtimes.cjs file — an attacker would already
|
|
104
|
+
// need write access to introduce a malicious value. Acceptable.
|
|
105
|
+
converterPath: path.join(
|
|
106
|
+
__dirname,
|
|
107
|
+
'lib', 'install', 'converters',
|
|
108
|
+
rt.kind + '.cjs',
|
|
109
|
+
),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
channels.push(PASSTHROUGH_CHANNEL);
|
|
113
|
+
return channels;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------
|
|
117
|
+
// Skill enumeration (canonical source)
|
|
118
|
+
// ---------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Enumerate child directories of `<sourceRoot>/skills/` that contain a
|
|
122
|
+
* `SKILL.md`. Returns { skillsRoot, skillNames } where skillNames is
|
|
123
|
+
* sorted lexicographically (determinism).
|
|
124
|
+
*
|
|
125
|
+
* Throws Error with code MISSING_SKILLS_ROOT if skills/ is absent.
|
|
126
|
+
*/
|
|
127
|
+
function enumerateSkills(sourceRoot) {
|
|
128
|
+
const skillsRoot = path.join(sourceRoot, 'skills');
|
|
129
|
+
if (!fs.existsSync(skillsRoot)) {
|
|
130
|
+
const err = new Error('Canonical skills/ tree not found at ' + skillsRoot);
|
|
131
|
+
err.code = 'MISSING_SKILLS_ROOT';
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
const names = fs.readdirSync(skillsRoot)
|
|
135
|
+
.filter((name) => {
|
|
136
|
+
const skillDir = path.join(skillsRoot, name);
|
|
137
|
+
try {
|
|
138
|
+
return fs.statSync(skillDir).isDirectory()
|
|
139
|
+
&& fs.existsSync(path.join(skillDir, 'SKILL.md'));
|
|
140
|
+
} catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
.sort();
|
|
145
|
+
return { skillsRoot, skillNames: names };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------
|
|
149
|
+
// Filesystem helpers
|
|
150
|
+
// ---------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
function ensureCleanDir(dirPath) {
|
|
153
|
+
if (fs.existsSync(dirPath)) {
|
|
154
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
155
|
+
}
|
|
156
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Deterministic file write: 0o644, no timestamp metadata leaked into content.
|
|
161
|
+
*/
|
|
162
|
+
function writeFile(dest, content) {
|
|
163
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
164
|
+
fs.writeFileSync(dest, content, { mode: 0o644 });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Recursive byte-for-byte copy of `srcDir` into `destDir`. Used by the
|
|
169
|
+
* passthrough channel. Deterministic: lexicographic readdir + 0o644.
|
|
170
|
+
*
|
|
171
|
+
* T-28.8-X1-03 (Tampering / symlinks): only entry.isFile() and
|
|
172
|
+
* entry.isDirectory() are propagated. Symlinks and other types are
|
|
173
|
+
* silently skipped — the canonical skills/ tree is expected to be
|
|
174
|
+
* regular files only.
|
|
175
|
+
*/
|
|
176
|
+
function copyDirRecursive(srcDir, destDir) {
|
|
177
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
178
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true })
|
|
179
|
+
.slice()
|
|
180
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
181
|
+
for (const entry of entries) {
|
|
182
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
183
|
+
const destPath = path.join(destDir, entry.name);
|
|
184
|
+
if (entry.isDirectory()) {
|
|
185
|
+
copyDirRecursive(srcPath, destPath);
|
|
186
|
+
} else if (entry.isFile()) {
|
|
187
|
+
const content = fs.readFileSync(srcPath);
|
|
188
|
+
writeFile(destPath, content);
|
|
189
|
+
}
|
|
190
|
+
// Symlinks / other entry types: skip.
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function countFiles(dir) {
|
|
195
|
+
let count = 0;
|
|
196
|
+
if (!fs.existsSync(dir)) return 0;
|
|
197
|
+
const stack = [dir];
|
|
198
|
+
while (stack.length) {
|
|
199
|
+
const d = stack.pop();
|
|
200
|
+
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
201
|
+
const p = path.join(d, entry.name);
|
|
202
|
+
if (entry.isDirectory()) stack.push(p);
|
|
203
|
+
else if (entry.isFile()) count++;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return count;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ---------------------------------------------------------------
|
|
210
|
+
// Optional ancillary sources (loaded best-effort from repo root)
|
|
211
|
+
// ---------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Best-effort loader for ancillary inputs the converters may consult:
|
|
215
|
+
* - .claude-plugin/plugin.json → claudePlugin / claudePluginJson
|
|
216
|
+
* - .claude-plugin/marketplace.json → marketplaceJson
|
|
217
|
+
* - README.md (first paragraph) → readmeFirstPara
|
|
218
|
+
*
|
|
219
|
+
* Returns an object with each key present only if the corresponding source
|
|
220
|
+
* exists and parses cleanly. Never throws — converters are tolerant of
|
|
221
|
+
* absent optional sources, and tmpdir test fixtures typically omit them.
|
|
222
|
+
*/
|
|
223
|
+
function loadAncillarySources(sourceRoot) {
|
|
224
|
+
const sources = {};
|
|
225
|
+
const pluginJsonPath = path.join(sourceRoot, '.claude-plugin', 'plugin.json');
|
|
226
|
+
if (fs.existsSync(pluginJsonPath)) {
|
|
227
|
+
try {
|
|
228
|
+
const parsed = JSON.parse(fs.readFileSync(pluginJsonPath, 'utf8'));
|
|
229
|
+
sources.claudePlugin = parsed;
|
|
230
|
+
sources.claudePluginJson = parsed;
|
|
231
|
+
} catch {
|
|
232
|
+
// Best-effort: malformed plugin.json is the converter's problem,
|
|
233
|
+
// not the bundler's. Continue without it.
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const marketplaceJsonPath = path.join(sourceRoot, '.claude-plugin', 'marketplace.json');
|
|
237
|
+
if (fs.existsSync(marketplaceJsonPath)) {
|
|
238
|
+
try {
|
|
239
|
+
sources.marketplaceJson = JSON.parse(fs.readFileSync(marketplaceJsonPath, 'utf8'));
|
|
240
|
+
} catch {
|
|
241
|
+
// skip
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const readmePath = path.join(sourceRoot, 'README.md');
|
|
245
|
+
if (fs.existsSync(readmePath)) {
|
|
246
|
+
try {
|
|
247
|
+
const raw = fs.readFileSync(readmePath, 'utf8');
|
|
248
|
+
// First non-empty, non-heading paragraph.
|
|
249
|
+
const paragraphs = raw.split(/\n\s*\n/);
|
|
250
|
+
for (const p of paragraphs) {
|
|
251
|
+
const trimmed = p.trim();
|
|
252
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
253
|
+
sources.readmeFirstPara = trimmed.replace(/\s+/g, ' ');
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
} catch {
|
|
257
|
+
// skip
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return sources;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ---------------------------------------------------------------
|
|
264
|
+
// Channel build dispatch
|
|
265
|
+
// ---------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Build a single channel into `outRoot/<channelId>/`.
|
|
269
|
+
* Returns { channel, fileCount }.
|
|
270
|
+
*
|
|
271
|
+
* Throws on converter error or missing dependency — caller maps to exit code.
|
|
272
|
+
*
|
|
273
|
+
* Errors set `err.code` to one of:
|
|
274
|
+
* - 'MISSING_CONVERTER' → exit 2
|
|
275
|
+
* - 'CONVERTER_LOAD_FAILED' → exit 2 (require() failed — broken module)
|
|
276
|
+
* - 'CONVERTER_EXEC_FAILED' → exit 1 (converter ran and threw)
|
|
277
|
+
* - 'MANIFEST_BUILD_FAILED' → exit 1 (buildManifest threw)
|
|
278
|
+
* - 'MISSING_SKILLS_ROOT' → exit 2 (no skills/ dir to read)
|
|
279
|
+
*/
|
|
280
|
+
function buildChannel(channel, opts) {
|
|
281
|
+
const { sourceRoot, outRoot, packageJson } = opts || {};
|
|
282
|
+
if (!channel || typeof channel !== 'object') {
|
|
283
|
+
throw new Error('buildChannel: channel is required');
|
|
284
|
+
}
|
|
285
|
+
if (typeof sourceRoot !== 'string' || sourceRoot.length === 0) {
|
|
286
|
+
throw new Error('buildChannel: opts.sourceRoot is required');
|
|
287
|
+
}
|
|
288
|
+
if (typeof outRoot !== 'string' || outRoot.length === 0) {
|
|
289
|
+
throw new Error('buildChannel: opts.outRoot is required');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const bundleRoot = path.join(outRoot, channel.id);
|
|
293
|
+
ensureCleanDir(bundleRoot);
|
|
294
|
+
|
|
295
|
+
if (channel.kind === 'passthrough') {
|
|
296
|
+
// agentskills-io: passthrough copy of skills/ (D-13).
|
|
297
|
+
const skillsSrc = path.join(sourceRoot, 'skills');
|
|
298
|
+
if (!fs.existsSync(skillsSrc)) {
|
|
299
|
+
const err = new Error('Canonical skills/ tree not found at ' + skillsSrc);
|
|
300
|
+
err.code = 'MISSING_SKILLS_ROOT';
|
|
301
|
+
err.channelId = channel.id;
|
|
302
|
+
throw err;
|
|
303
|
+
}
|
|
304
|
+
copyDirRecursive(skillsSrc, path.join(bundleRoot, 'skills'));
|
|
305
|
+
return { channel: channel.id, fileCount: countFiles(bundleRoot) };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Tier-2 converter-backed channels (cursor-marketplace, codex-plugin).
|
|
309
|
+
if (!fs.existsSync(channel.converterPath)) {
|
|
310
|
+
const err = new Error(
|
|
311
|
+
'Missing converter for channel "' + channel.id + '": expected at ' + channel.converterPath
|
|
312
|
+
);
|
|
313
|
+
err.code = 'MISSING_CONVERTER';
|
|
314
|
+
err.channelId = channel.id;
|
|
315
|
+
throw err;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
let converter;
|
|
319
|
+
try {
|
|
320
|
+
converter = require(channel.converterPath);
|
|
321
|
+
} catch (e) {
|
|
322
|
+
const err = new Error(
|
|
323
|
+
'Failed to load converter for channel "' + channel.id + '": ' + e.message
|
|
324
|
+
);
|
|
325
|
+
err.code = 'CONVERTER_LOAD_FAILED';
|
|
326
|
+
err.channelId = channel.id;
|
|
327
|
+
err.cause = e;
|
|
328
|
+
throw err;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (typeof converter.buildManifest !== 'function' || typeof converter.convert !== 'function') {
|
|
332
|
+
const err = new Error(
|
|
333
|
+
'Converter for channel "' + channel.id + '" missing required exports: ' +
|
|
334
|
+
'expected { buildManifest, convert }, got ' + Object.keys(converter).join(', ')
|
|
335
|
+
);
|
|
336
|
+
err.code = 'CONVERTER_LOAD_FAILED';
|
|
337
|
+
err.channelId = channel.id;
|
|
338
|
+
throw err;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Enumerate skills BEFORE invoking convert(). This both validates that
|
|
342
|
+
// skills/ exists and surfaces a deterministic name list for error
|
|
343
|
+
// messages — convert() will re-walk the directory itself per its own
|
|
344
|
+
// semantics, which is fine (idempotent) but we want a stable list for
|
|
345
|
+
// the CONVERTER_EXEC_FAILED error message.
|
|
346
|
+
const { skillsRoot, skillNames } = enumerateSkills(sourceRoot);
|
|
347
|
+
|
|
348
|
+
// Build manifest via the converter.
|
|
349
|
+
// Pass BOTH `claudePlugin` and `claudePluginJson` accessor keys so the
|
|
350
|
+
// adapter works regardless of which key the specific converter consults.
|
|
351
|
+
const ancillary = loadAncillarySources(sourceRoot);
|
|
352
|
+
const sources = Object.assign({}, ancillary, { packageJson });
|
|
353
|
+
|
|
354
|
+
let manifest;
|
|
355
|
+
try {
|
|
356
|
+
manifest = converter.buildManifest(sources);
|
|
357
|
+
} catch (e) {
|
|
358
|
+
const err = new Error(
|
|
359
|
+
'Converter "' + channel.id + '" failed building manifest: ' + e.message
|
|
360
|
+
);
|
|
361
|
+
err.code = 'MANIFEST_BUILD_FAILED';
|
|
362
|
+
err.channelId = channel.id;
|
|
363
|
+
err.cause = e;
|
|
364
|
+
throw err;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Invoke convert() — converter writes manifest + copies skills/ under outDir.
|
|
368
|
+
try {
|
|
369
|
+
converter.convert({
|
|
370
|
+
skillsDir: skillsRoot,
|
|
371
|
+
outDir: bundleRoot,
|
|
372
|
+
manifest,
|
|
373
|
+
});
|
|
374
|
+
} catch (e) {
|
|
375
|
+
// The converter walked skills/ internally so we don't know which
|
|
376
|
+
// individual skill triggered the throw — surface the full list to
|
|
377
|
+
// aid debugging.
|
|
378
|
+
const skillsHint = skillNames.length > 0
|
|
379
|
+
? ' (skills: ' + skillNames.join(', ') + ')'
|
|
380
|
+
: '';
|
|
381
|
+
const err = new Error(
|
|
382
|
+
'Converter "' + channel.id + '" failed during convert()' + skillsHint + ': ' + e.message
|
|
383
|
+
);
|
|
384
|
+
err.code = 'CONVERTER_EXEC_FAILED';
|
|
385
|
+
err.channelId = channel.id;
|
|
386
|
+
err.skillName = skillNames[0] || null;
|
|
387
|
+
err.cause = e;
|
|
388
|
+
throw err;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return { channel: channel.id, fileCount: countFiles(bundleRoot) };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Build all (or one filtered) channel(s) into `outRoot`.
|
|
396
|
+
*
|
|
397
|
+
* Options:
|
|
398
|
+
* sourceRoot — repo root containing skills/ + ancillary sources
|
|
399
|
+
* outRoot — destination root (e.g., repo/dist)
|
|
400
|
+
* runtimesModule — dependency-injected runtimes registry (test seam)
|
|
401
|
+
* packageJson — parsed package.json object passed to converters
|
|
402
|
+
* channelFilter — optional channel id to scope the build to one channel
|
|
403
|
+
*
|
|
404
|
+
* Returns Array<{ channel, fileCount }> in lexicographic channel order.
|
|
405
|
+
*/
|
|
406
|
+
function buildAllChannels(opts) {
|
|
407
|
+
const { sourceRoot, outRoot, runtimesModule, packageJson, channelFilter } = opts || {};
|
|
408
|
+
const channels = discoverTier2Channels(runtimesModule);
|
|
409
|
+
const targets = channelFilter
|
|
410
|
+
? channels.filter((c) => c.id === channelFilter)
|
|
411
|
+
: channels;
|
|
412
|
+
if (channelFilter && targets.length === 0) {
|
|
413
|
+
const err = new Error(
|
|
414
|
+
'Unknown channel: "' + channelFilter + '". Available: ' +
|
|
415
|
+
channels.map((c) => c.id).join(', ')
|
|
416
|
+
);
|
|
417
|
+
err.code = 'UNKNOWN_CHANNEL';
|
|
418
|
+
throw err;
|
|
419
|
+
}
|
|
420
|
+
// Lexicographic order for deterministic stdout + filesystem traversal.
|
|
421
|
+
targets.sort((a, b) => a.id.localeCompare(b.id));
|
|
422
|
+
const results = [];
|
|
423
|
+
for (const channel of targets) {
|
|
424
|
+
results.push(buildChannel(channel, { sourceRoot, outRoot, packageJson }));
|
|
425
|
+
}
|
|
426
|
+
return results;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ---------------------------------------------------------------
|
|
430
|
+
// CLI entrypoint
|
|
431
|
+
// ---------------------------------------------------------------
|
|
432
|
+
|
|
433
|
+
function parseArgs(argv) {
|
|
434
|
+
const args = { help: false, channel: null };
|
|
435
|
+
for (let i = 0; i < argv.length; i++) {
|
|
436
|
+
const a = argv[i];
|
|
437
|
+
if (a === '--help' || a === '-h') {
|
|
438
|
+
args.help = true;
|
|
439
|
+
} else if (a === '--channel') {
|
|
440
|
+
if (i + 1 >= argv.length) {
|
|
441
|
+
throw new Error('--channel requires a value');
|
|
442
|
+
}
|
|
443
|
+
args.channel = argv[++i];
|
|
444
|
+
} else {
|
|
445
|
+
throw new Error('Unknown argument: ' + a);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return args;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function printUsage(out) {
|
|
452
|
+
out.write([
|
|
453
|
+
'Usage: node scripts/build-distribution-bundles.cjs [--channel <id>]',
|
|
454
|
+
'',
|
|
455
|
+
'Builds Tier-2 distribution bundles from canonical skills/ into dist/.',
|
|
456
|
+
'',
|
|
457
|
+
'Options:',
|
|
458
|
+
' --channel <id> Build only the named channel (e.g., cursor-marketplace,',
|
|
459
|
+
' codex-plugin, agentskills-io). Default: all channels.',
|
|
460
|
+
' --help, -h Print this message.',
|
|
461
|
+
'',
|
|
462
|
+
'Exit codes:',
|
|
463
|
+
' 0 success',
|
|
464
|
+
' 1 converter error (converter ran and threw)',
|
|
465
|
+
' 2 missing dependency (converter file, runtimes.cjs entry, skills/, or bad arg)',
|
|
466
|
+
'',
|
|
467
|
+
].join('\n'));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function main(argv, ioOpts) {
|
|
471
|
+
const stdout = (ioOpts && ioOpts.stdout) || process.stdout;
|
|
472
|
+
const stderr = (ioOpts && ioOpts.stderr) || process.stderr;
|
|
473
|
+
|
|
474
|
+
let args;
|
|
475
|
+
try {
|
|
476
|
+
args = parseArgs(argv || []);
|
|
477
|
+
} catch (e) {
|
|
478
|
+
stderr.write('Error: ' + e.message + '\n');
|
|
479
|
+
printUsage(stderr);
|
|
480
|
+
return EXIT_CODES.MISSING_DEPENDENCY;
|
|
481
|
+
}
|
|
482
|
+
if (args.help) {
|
|
483
|
+
printUsage(stdout);
|
|
484
|
+
return EXIT_CODES.OK;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const repoRoot = path.resolve(__dirname, '..');
|
|
488
|
+
const sourceRoot = repoRoot;
|
|
489
|
+
const outRoot = path.join(repoRoot, 'dist');
|
|
490
|
+
|
|
491
|
+
let runtimesModule;
|
|
492
|
+
try {
|
|
493
|
+
runtimesModule = require('./lib/install/runtimes.cjs');
|
|
494
|
+
} catch (e) {
|
|
495
|
+
stderr.write('Error: failed to load runtimes.cjs: ' + e.message + '\n');
|
|
496
|
+
return EXIT_CODES.MISSING_DEPENDENCY;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
let packageJson;
|
|
500
|
+
try {
|
|
501
|
+
packageJson = require(path.join(repoRoot, 'package.json'));
|
|
502
|
+
} catch (e) {
|
|
503
|
+
stderr.write('Error: failed to load package.json: ' + e.message + '\n');
|
|
504
|
+
return EXIT_CODES.MISSING_DEPENDENCY;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
try {
|
|
508
|
+
const results = buildAllChannels({
|
|
509
|
+
sourceRoot,
|
|
510
|
+
outRoot,
|
|
511
|
+
runtimesModule,
|
|
512
|
+
packageJson,
|
|
513
|
+
channelFilter: args.channel,
|
|
514
|
+
});
|
|
515
|
+
for (const r of results) {
|
|
516
|
+
stdout.write('[bundles] ' + r.channel + ': ' + r.fileCount + ' file(s)\n');
|
|
517
|
+
}
|
|
518
|
+
return EXIT_CODES.OK;
|
|
519
|
+
} catch (e) {
|
|
520
|
+
stderr.write('Error: ' + e.message + '\n');
|
|
521
|
+
if (
|
|
522
|
+
e.code === 'MISSING_CONVERTER' ||
|
|
523
|
+
e.code === 'MISSING_SKILLS_ROOT' ||
|
|
524
|
+
e.code === 'UNKNOWN_CHANNEL' ||
|
|
525
|
+
e.code === 'CONVERTER_LOAD_FAILED'
|
|
526
|
+
) {
|
|
527
|
+
return EXIT_CODES.MISSING_DEPENDENCY;
|
|
528
|
+
}
|
|
529
|
+
// CONVERTER_EXEC_FAILED, MANIFEST_BUILD_FAILED, or anything else.
|
|
530
|
+
return EXIT_CODES.CONVERTER_ERROR;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
module.exports = {
|
|
535
|
+
buildAllChannels,
|
|
536
|
+
buildChannel,
|
|
537
|
+
discoverTier2Channels,
|
|
538
|
+
enumerateSkills,
|
|
539
|
+
loadAncillarySources,
|
|
540
|
+
main,
|
|
541
|
+
parseArgs,
|
|
542
|
+
EXIT_CODES,
|
|
543
|
+
PASSTHROUGH_CHANNEL,
|
|
544
|
+
TIER2_KINDS,
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
if (require.main === module) {
|
|
548
|
+
process.exitCode = main(process.argv.slice(2));
|
|
549
|
+
}
|
|
@@ -50,6 +50,7 @@ function usage() {
|
|
|
50
50
|
' gdd-events cat [--path=<p>]',
|
|
51
51
|
' gdd-events list-types',
|
|
52
52
|
' gdd-events serve [--port=<n>] [--token=<t>] [--tail=<file>]',
|
|
53
|
+
' gdd-events --type <typename> [--path=<p>] (Plan 29-03 ergonomic alias for `grep type=<typename>`)',
|
|
53
54
|
'',
|
|
54
55
|
'Filter language (grep): type=<s> payload.<dotted.path>=<s> !type=<s>',
|
|
55
56
|
'',
|
|
@@ -59,11 +60,22 @@ function usage() {
|
|
|
59
60
|
|
|
60
61
|
function parseArgs(args) {
|
|
61
62
|
const out = { _: [], flags: {} };
|
|
62
|
-
|
|
63
|
+
// Flags that take a value via the space-separated form `--flag <value>`.
|
|
64
|
+
// Without this set, `--type capability_gap` would be parsed as
|
|
65
|
+
// { flags: { type: true } } and the value would land in positional args.
|
|
66
|
+
const VALUE_FLAGS = new Set(['type', 'path', 'port', 'token', 'tail']);
|
|
67
|
+
for (let i = 0; i < args.length; i++) {
|
|
68
|
+
const a = args[i];
|
|
63
69
|
if (a.startsWith('--')) {
|
|
64
70
|
const eq = a.indexOf('=');
|
|
65
71
|
if (eq === -1) {
|
|
66
|
-
|
|
72
|
+
const name = a.slice(2);
|
|
73
|
+
if (VALUE_FLAGS.has(name) && i + 1 < args.length && !args[i + 1].startsWith('--')) {
|
|
74
|
+
out.flags[name] = args[i + 1];
|
|
75
|
+
i += 1;
|
|
76
|
+
} else {
|
|
77
|
+
out.flags[name] = true;
|
|
78
|
+
}
|
|
67
79
|
} else {
|
|
68
80
|
out.flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
69
81
|
}
|
|
@@ -242,6 +254,27 @@ async function cmdServe(parsed) {
|
|
|
242
254
|
|
|
243
255
|
async function main() {
|
|
244
256
|
const parsed = parseArgs(argv.slice(2));
|
|
257
|
+
|
|
258
|
+
// Plan 29-03: `--type <typename>` (and `--type=<typename>`) is an ergonomic
|
|
259
|
+
// alias for `grep type=<typename>`. Desugar BEFORE shifting the subcommand
|
|
260
|
+
// so the flag can be used without a subcommand prefix:
|
|
261
|
+
// gdd-events --type capability_gap --path=…
|
|
262
|
+
// Equivalent to:
|
|
263
|
+
// gdd-events grep type=capability_gap --path=…
|
|
264
|
+
if (typeof parsed.flags.type === 'string' && parsed.flags.type.length > 0) {
|
|
265
|
+
const typeValue = parsed.flags.type;
|
|
266
|
+
delete parsed.flags.type;
|
|
267
|
+
// Inject the filter term and force the grep code path. Preserve any
|
|
268
|
+
// additional positional terms the user already supplied.
|
|
269
|
+
parsed._ = [`type=${typeValue}`, ...parsed._];
|
|
270
|
+
try {
|
|
271
|
+
return await cmdGrep(parsed);
|
|
272
|
+
} catch (err) {
|
|
273
|
+
stderr.write(`gdd-events: ${err && err.message ? err.message : String(err)}\n`);
|
|
274
|
+
return 1;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
245
278
|
const sub = parsed._.shift();
|
|
246
279
|
try {
|
|
247
280
|
switch (sub) {
|