@hegemonart/get-design-done 1.28.6 → 1.28.8
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 +81 -0
- package/README.de.md +14 -0
- package/README.fr.md +14 -0
- package/README.it.md +14 -0
- package/README.ja.md +14 -0
- package/README.ko.md +14 -0
- package/README.md +18 -0
- package/README.zh-CN.md +14 -0
- package/SKILL.md +10 -10
- package/package.json +3 -1
- package/scripts/build-distribution-bundles.cjs +549 -0
- package/scripts/install.cjs +68 -0
- package/scripts/lib/install/config-dir.cjs +26 -0
- package/scripts/lib/install/converters/antigravity.cjs +48 -0
- package/scripts/lib/install/converters/augment.cjs +68 -0
- package/scripts/lib/install/converters/cline.cjs +206 -0
- package/scripts/lib/install/converters/codebuddy.cjs +55 -0
- package/scripts/lib/install/converters/codex-plugin.cjs +407 -0
- package/scripts/lib/install/converters/codex.cjs +61 -0
- package/scripts/lib/install/converters/copilot.cjs +47 -0
- package/scripts/lib/install/converters/cursor-marketplace.cjs +309 -0
- package/scripts/lib/install/converters/cursor.cjs +49 -0
- package/scripts/lib/install/converters/gemini.cjs +116 -0
- package/scripts/lib/install/converters/kilo.cjs +62 -0
- package/scripts/lib/install/converters/opencode.cjs +64 -0
- package/scripts/lib/install/converters/qwen.cjs +51 -0
- package/scripts/lib/install/converters/shared.cjs +377 -0
- package/scripts/lib/install/converters/trae.cjs +47 -0
- package/scripts/lib/install/converters/windsurf.cjs +47 -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/installer.cjs +529 -47
- package/scripts/lib/install/merge.cjs +31 -1
- package/scripts/lib/install/runtime-artifact-layout.cjs +431 -0
- package/scripts/lib/install/runtime-homes.cjs +225 -0
- package/scripts/lib/install/runtime-slash.cjs +172 -0
- package/scripts/lib/install/runtimes.cjs +73 -32
- package/scripts/lint-agentskills-spec.cjs +457 -0
- 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/figma-write/SKILL.md +2 -2
- package/skills/graphify/SKILL.md +2 -2
- package/skills/style/SKILL.md +2 -2
- package/skills/style/style-doc-procedure.md +1 -1
|
@@ -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
|
+
}
|
package/scripts/install.cjs
CHANGED
|
@@ -15,6 +15,18 @@
|
|
|
15
15
|
//
|
|
16
16
|
// Modifiers: --global (default) | --local; --uninstall; --dry-run;
|
|
17
17
|
// --config-dir <path>; --help / -h.
|
|
18
|
+
//
|
|
19
|
+
// Read-only modes: --doctor (Phase 28.8 — Tier-2 distribution-channel
|
|
20
|
+
// status; no install side effects). Future Tier-2 channels (Codex
|
|
21
|
+
// plugin via Plan 28-8-C2; aggregated Tier-2 status via Plan 28-8-X2)
|
|
22
|
+
// plug additional sections into the same flag dispatch — see
|
|
23
|
+
// `runDoctor()` below for the section-module pattern.
|
|
24
|
+
//
|
|
25
|
+
// Read-only modes: --doctor (Phase 28.8 — Tier-2 distribution-channel
|
|
26
|
+
// status; no install side effects). Future Tier-2 channels (Codex
|
|
27
|
+
// plugin via Plan 28-8-C2; aggregated Tier-2 status via Plan 28-8-X2)
|
|
28
|
+
// plug additional sections into the same flag dispatch — see
|
|
29
|
+
// `runDoctor()` below for the section-module pattern.
|
|
18
30
|
|
|
19
31
|
const path = require('node:path');
|
|
20
32
|
|
|
@@ -64,6 +76,7 @@ function helpText() {
|
|
|
64
76
|
' --no-peer-prompt Suppress the post-install peer-CLI detection nudge',
|
|
65
77
|
' --register-mcp Register gdd-mcp with detected harnesses (Claude Code, Codex). Opt-in.',
|
|
66
78
|
' --no-register-mcp Skip MCP registration (default behavior; included for symmetry).',
|
|
79
|
+
' --doctor Print Tier-2 distribution-channel status (read-only; no install)',
|
|
67
80
|
' --help, -h Show this message',
|
|
68
81
|
'',
|
|
69
82
|
'Environment overrides (per-runtime):',
|
|
@@ -125,6 +138,47 @@ function summariseResults(results) {
|
|
|
125
138
|
return lines.join('\n');
|
|
126
139
|
}
|
|
127
140
|
|
|
141
|
+
// Phase 28.8 — Tier-2 distribution-channel doctor.
|
|
142
|
+
//
|
|
143
|
+
// Read-only status reporter for distribution channels (Cursor Marketplace,
|
|
144
|
+
// Codex Plugins, agentskills.io lint pass). Phase 28.8-X2 D-13/D-16: the
|
|
145
|
+
// three channels are aggregated via `scripts/lib/install/doctor-tier2.cjs`
|
|
146
|
+
// which composes B2's `reportCursorMarketplace`, C2's `checkCodexPlugin`,
|
|
147
|
+
// and A1's `lintSummary` into a single "## Tier-2 Distribution Channels"
|
|
148
|
+
// section with a one-line summary + per-channel subsections.
|
|
149
|
+
//
|
|
150
|
+
// Phase 28.8-X2: B2's and C2's individual doctor sections used to be
|
|
151
|
+
// rendered as standalone blocks here. With X2 the aggregator now owns
|
|
152
|
+
// the entire Tier-2 section — the B2/C2 modules remain callable internals
|
|
153
|
+
// (the aggregator consumes their pure `report*()` functions), but the
|
|
154
|
+
// individual formatters are no longer invoked directly from install.cjs.
|
|
155
|
+
// Rationale: a single section with a unified summary line is what the
|
|
156
|
+
// maintainer wants (Plan 28-8-X2 §<objective>). The aggregator handles
|
|
157
|
+
// throw safety internally (Cursor's malformed-state-file throw becomes
|
|
158
|
+
// a `not-configured` subsection rather than killing the doctor).
|
|
159
|
+
function runDoctor() {
|
|
160
|
+
const projectRoot = process.cwd();
|
|
161
|
+
try {
|
|
162
|
+
const {
|
|
163
|
+
readTier2Status,
|
|
164
|
+
formatTier2Section,
|
|
165
|
+
} = require('./lib/install/doctor-tier2.cjs');
|
|
166
|
+
const status = readTier2Status({ sourceRoot: projectRoot });
|
|
167
|
+
process.stdout.write(formatTier2Section(status) + '\n');
|
|
168
|
+
} catch (err) {
|
|
169
|
+
// The aggregator is throw-resistant (channel errors surface as
|
|
170
|
+
// `not-configured` with detail). A top-level throw here implies the
|
|
171
|
+
// aggregator module itself failed to load — surface inline so the
|
|
172
|
+
// maintainer sees the breakage without losing the whole CLI exit.
|
|
173
|
+
process.stdout.write(
|
|
174
|
+
'## Tier-2 Distribution Channels\n\n'
|
|
175
|
+
+ ' ERROR: '
|
|
176
|
+
+ (err && err.message ? err.message : String(err))
|
|
177
|
+
+ '\n'
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
128
182
|
async function main() {
|
|
129
183
|
const { flags, configDir } = parseArgs(process.argv);
|
|
130
184
|
|
|
@@ -133,6 +187,13 @@ async function main() {
|
|
|
133
187
|
process.exit(0);
|
|
134
188
|
}
|
|
135
189
|
|
|
190
|
+
// Phase 28.8 D-16 + B2 — read-only Tier-2 doctor. Early dispatch BEFORE
|
|
191
|
+
// any runtime selection so doctor never performs install side effects.
|
|
192
|
+
if (flags.has('--doctor')) {
|
|
193
|
+
runDoctor();
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}
|
|
196
|
+
|
|
136
197
|
const dryRun = flags.has('--dry-run');
|
|
137
198
|
const uninstall = flags.has('--uninstall');
|
|
138
199
|
const local = flags.has('--local');
|
|
@@ -167,6 +228,13 @@ async function main() {
|
|
|
167
228
|
} else if (location === 'local') {
|
|
168
229
|
opts.configDir = resolveLocalConfigDir(runtime);
|
|
169
230
|
}
|
|
231
|
+
// Phase 28.7 D-07 — pass scope through to the installer. The new
|
|
232
|
+
// `multi-artifact` kind branches on `local` vs `global` in
|
|
233
|
+
// `runtime-artifact-layout.cjs` (claude local writes commands/+agents/;
|
|
234
|
+
// claude global writes skills/), so the installer needs to know the
|
|
235
|
+
// user's chosen scope, not just the resolved configDir. Default is
|
|
236
|
+
// `global` per D-07.
|
|
237
|
+
opts.scope = location;
|
|
170
238
|
const result = uninstall
|
|
171
239
|
? uninstallRuntime(id, opts)
|
|
172
240
|
: installRuntime(id, opts);
|
|
@@ -22,6 +22,23 @@ function homeDir() {
|
|
|
22
22
|
|
|
23
23
|
function resolveConfigDir(runtimeId, opts) {
|
|
24
24
|
const runtime = getRuntime(runtimeId);
|
|
25
|
+
|
|
26
|
+
// Phase 28.8 (Plan B1) — Tier-2 distribution-channel runtimes have
|
|
27
|
+
// configDir === null and configDirFallback === null. They are NOT
|
|
28
|
+
// per-user install targets; calling resolveConfigDir on them is a
|
|
29
|
+
// programming error (the regular install flow skips them via
|
|
30
|
+
// detect-runtimes). Throw a clear error rather than crashing on
|
|
31
|
+
// `null.split('/')` further down.
|
|
32
|
+
if (
|
|
33
|
+
runtime.configDirFallback === null
|
|
34
|
+
|| typeof runtime.configDirFallback !== 'string'
|
|
35
|
+
) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Runtime "${runtimeId}" is a Tier-2 distribution channel (kind: ${runtime.kind}); ` +
|
|
38
|
+
'it has no per-user config dir. Filter these out before calling resolveConfigDir.'
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
25
42
|
const overrides = (opts && opts.env) || process.env;
|
|
26
43
|
const explicit = opts && opts.configDir;
|
|
27
44
|
|
|
@@ -44,6 +61,15 @@ function resolveConfigDir(runtimeId, opts) {
|
|
|
44
61
|
function resolveAllConfigDirs(opts) {
|
|
45
62
|
const out = {};
|
|
46
63
|
for (const runtime of listRuntimes()) {
|
|
64
|
+
// Phase 28.8 (Plan B1) — Tier-2 distribution channels have no per-user
|
|
65
|
+
// config dir. Skip them so the returned map covers only the per-user
|
|
66
|
+
// install targets (the 14 multi-artifact + claude-marketplace runtimes).
|
|
67
|
+
if (
|
|
68
|
+
runtime.configDirFallback === null
|
|
69
|
+
|| typeof runtime.configDirFallback !== 'string'
|
|
70
|
+
) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
47
73
|
out[runtime.id] = resolveConfigDir(runtime.id, opts);
|
|
48
74
|
}
|
|
49
75
|
return out;
|