@hegemonart/get-design-done 1.28.7 → 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 +35 -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 +16 -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 +61 -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/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,407 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* scripts/lib/install/converters/codex-plugin.cjs — Phase 28.8 (Plan 28-8-C1).
|
|
5
|
+
*
|
|
6
|
+
* Codex Plugin distribution-channel converter. Emits a Codex-plugin-shape
|
|
7
|
+
* bundle (`.codex-plugin/plugin.json` + verbatim-copied `skills/` tree)
|
|
8
|
+
* from our `skills/` canonical source. Consumed by Plan 28-8-X1's
|
|
9
|
+
* scripts/build-distribution-bundles.cjs (downstream — this module is
|
|
10
|
+
* the contract, X1 wires the build pipeline).
|
|
11
|
+
*
|
|
12
|
+
* Per CONTEXT D-05 (additive): this is a NEW kind, alongside the
|
|
13
|
+
* existing scripts/lib/install/converters/codex.cjs (Phase 28.7
|
|
14
|
+
* file-drop AGENTS.md surface). codex.cjs is UNCHANGED. Tier-1 and
|
|
15
|
+
* Tier-2 surfaces coexist as documented in
|
|
16
|
+
* .planning/research/codex-plugins-2026-05-19.md § vs AGENTS.md.
|
|
17
|
+
*
|
|
18
|
+
* Per CONTEXT D-06 (skills are shared source): skill content is copied
|
|
19
|
+
* verbatim during bundle emission — Codex consumes the same SKILL.md
|
|
20
|
+
* shape we already produce for Phase 28.5 authoring contract. No
|
|
21
|
+
* per-skill content rewriting in this converter.
|
|
22
|
+
*
|
|
23
|
+
* Per CONTEXT D-14 (no new catalog): we do NOT emit a Codex-specific
|
|
24
|
+
* marketplace.json — Codex's legacy-compat path consumes our existing
|
|
25
|
+
* .claude-plugin/marketplace.json directly.
|
|
26
|
+
*
|
|
27
|
+
* GDD-original pattern (no gsd-build/get-shit-done counterpart): Tier-2
|
|
28
|
+
* distribution channels do not exist in the upstream multi-runtime install
|
|
29
|
+
* reference. Mirrors the cursor-marketplace.cjs sibling (Plan 28-8-B1).
|
|
30
|
+
*
|
|
31
|
+
* Pure / side-effect-free for `buildManifest`. `convert` performs
|
|
32
|
+
* filesystem writes (it's a bundle emitter) and is the impure boundary.
|
|
33
|
+
* All test invocations use tmpdir per CONTEXT D-10.
|
|
34
|
+
*
|
|
35
|
+
* Exports:
|
|
36
|
+
* - `buildManifest(sources)` — pure function, returns the Codex manifest
|
|
37
|
+
* object ready to `JSON.stringify(obj, null, 2)`.
|
|
38
|
+
* - `convert({ skillsDir, outDir, manifest })` — file-emission function
|
|
39
|
+
* for `build-distribution-bundles.cjs`. The only side-effect surface;
|
|
40
|
+
* touches only paths under `outDir`.
|
|
41
|
+
* - `MANIFEST_REQUIRED_FIELDS` — frozen 3-tuple of required spec fields.
|
|
42
|
+
* - `CURATED_KEYWORDS` — frozen 10-tag default keyword subset.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
const fs = require('node:fs');
|
|
46
|
+
const path = require('node:path');
|
|
47
|
+
|
|
48
|
+
// Per research § Top-level fields: name, version, description are the only
|
|
49
|
+
// strictly-required spec fields. All other manifest fields are optional.
|
|
50
|
+
const MANIFEST_REQUIRED_FIELDS = Object.freeze(['name', 'version', 'description']);
|
|
51
|
+
|
|
52
|
+
// Curated keyword subset for Codex marketplace card display.
|
|
53
|
+
// Per research § Schema Mapping `keywords` row: keep to ~10 design-relevant
|
|
54
|
+
// terms (our package.json carries 50+ tags). The intersection of these tags
|
|
55
|
+
// with package.json#keywords drives `curateKeywords()` below.
|
|
56
|
+
const CURATED_KEYWORDS = Object.freeze([
|
|
57
|
+
'design',
|
|
58
|
+
'ui',
|
|
59
|
+
'ux',
|
|
60
|
+
'frontend',
|
|
61
|
+
'pipeline',
|
|
62
|
+
'design-system',
|
|
63
|
+
'accessibility',
|
|
64
|
+
'figma',
|
|
65
|
+
'wcag',
|
|
66
|
+
'agent-sdk',
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
// ── Private helpers ────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
function stripNpmScope(name) {
|
|
72
|
+
if (typeof name !== 'string') return name;
|
|
73
|
+
return name.replace(/^@[^/]+\//, '');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function stripGitSuffix(url) {
|
|
77
|
+
if (typeof url !== 'string') return url;
|
|
78
|
+
return url.replace(/\.git$/, '');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function truncate(str, n) {
|
|
82
|
+
if (typeof str !== 'string') return str;
|
|
83
|
+
if (str.length <= n) return str;
|
|
84
|
+
// Prefer ending at sentence boundary, then word boundary, then hard cut.
|
|
85
|
+
const head = str.slice(0, n);
|
|
86
|
+
const sentenceEnd = head.lastIndexOf('. ');
|
|
87
|
+
if (sentenceEnd > n * 0.6) return head.slice(0, sentenceEnd + 1);
|
|
88
|
+
const wordEnd = head.lastIndexOf(' ');
|
|
89
|
+
if (wordEnd > n * 0.6) return head.slice(0, wordEnd);
|
|
90
|
+
return head;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function capitalize(str) {
|
|
94
|
+
if (typeof str !== 'string' || str.length === 0) return str;
|
|
95
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Curate keywords to the ≤10-element CURATED_KEYWORDS intersection with
|
|
100
|
+
* the source array. If the whitelist isn't a subset of source, fall back
|
|
101
|
+
* to `source.slice(0, 10)`. Always returns a fresh array (callers may
|
|
102
|
+
* mutate without polluting the frozen module constant).
|
|
103
|
+
*/
|
|
104
|
+
function curateKeywords(arr) {
|
|
105
|
+
if (!Array.isArray(arr)) return CURATED_KEYWORDS.slice();
|
|
106
|
+
const sourceSet = new Set(arr);
|
|
107
|
+
const intersected = CURATED_KEYWORDS.filter((k) => sourceSet.has(k));
|
|
108
|
+
if (intersected.length > 0) return intersected;
|
|
109
|
+
return arr.slice(0, 10);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Copy a directory tree recursively. Vanilla fs only — no deps. Mirrors
|
|
114
|
+
* the helper used by cursor-marketplace.cjs (Plan 28-8-B1).
|
|
115
|
+
*/
|
|
116
|
+
function copyDirRecursive(src, dest) {
|
|
117
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
118
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
119
|
+
const srcPath = path.join(src, entry.name);
|
|
120
|
+
const destPath = path.join(dest, entry.name);
|
|
121
|
+
if (entry.isDirectory()) {
|
|
122
|
+
copyDirRecursive(srcPath, destPath);
|
|
123
|
+
} else if (entry.isFile()) {
|
|
124
|
+
fs.copyFileSync(srcPath, destPath);
|
|
125
|
+
}
|
|
126
|
+
// symlinks + other: ignored (skills tree is regular files only).
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Public exports ─────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Build the Codex Plugin manifest object from GDD source artifacts.
|
|
134
|
+
* Pure function — no fs, env, or path access.
|
|
135
|
+
*
|
|
136
|
+
* Field-by-field source mapping per research § Schema Mapping:
|
|
137
|
+
*
|
|
138
|
+
* name ← marketplaceJson.plugins[0].name (canonical, kebab-case)
|
|
139
|
+
* → claudePlugin.name → stripNpmScope(packageJson.name)
|
|
140
|
+
* version ← packageJson.version (verbatim, lockstep per D-08)
|
|
141
|
+
* description ← packageJson.description (verbatim)
|
|
142
|
+
* author ← claudePlugin.author (canonical, has url)
|
|
143
|
+
* → marketplaceJson.plugins[0].author → packageJson.author
|
|
144
|
+
* homepage ← packageJson.homepage
|
|
145
|
+
* repository ← stripGitSuffix(packageJson.repository.url)
|
|
146
|
+
* license ← packageJson.license
|
|
147
|
+
* keywords ← curateKeywords(packageJson.keywords) → ≤10 entries
|
|
148
|
+
* skills ← static "./skills/"
|
|
149
|
+
* mcpServers ← inline { gdd-mcp: { command: "npx", args: [...] } }
|
|
150
|
+
* interface ← 9 sub-fields per Schema Mapping table:
|
|
151
|
+
* displayName, shortDescription, longDescription,
|
|
152
|
+
* developerName, category, capabilities, websiteURL,
|
|
153
|
+
* defaultPrompt, brandColor
|
|
154
|
+
*
|
|
155
|
+
* OMITTED (per research § Manifest Format "Omitted fields"):
|
|
156
|
+
* apps, hooks (off-by-default), interface.privacyPolicyURL,
|
|
157
|
+
* interface.termsOfServiceURL, interface.composerIcon, interface.logo,
|
|
158
|
+
* interface.screenshots
|
|
159
|
+
*
|
|
160
|
+
* @param {Object} sources Source metadata.
|
|
161
|
+
* @param {Object} sources.packageJson Parsed package.json.
|
|
162
|
+
* @param {Object} [sources.claudePlugin] Parsed .claude-plugin/plugin.json.
|
|
163
|
+
* @param {Object} [sources.marketplaceJson] Parsed .claude-plugin/marketplace.json.
|
|
164
|
+
* @param {string} [sources.readmeFirstPara] README.md first paragraph
|
|
165
|
+
* for interface.longDescription.
|
|
166
|
+
* @returns {Object} Manifest object ready
|
|
167
|
+
* to JSON.stringify with 2-space indent.
|
|
168
|
+
*/
|
|
169
|
+
function buildManifest(sources) {
|
|
170
|
+
if (!sources || typeof sources !== 'object') {
|
|
171
|
+
throw new Error('codex-plugin.buildManifest: sources is required');
|
|
172
|
+
}
|
|
173
|
+
const { packageJson, claudePlugin, marketplaceJson, readmeFirstPara } = sources;
|
|
174
|
+
|
|
175
|
+
if (!packageJson || typeof packageJson !== 'object') {
|
|
176
|
+
throw new Error('codex-plugin.buildManifest: sources.packageJson is required');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// name — required, kebab-case. Priority: marketplaceJson > claudePlugin >
|
|
180
|
+
// package.json (with scope stripped).
|
|
181
|
+
let name;
|
|
182
|
+
if (
|
|
183
|
+
marketplaceJson
|
|
184
|
+
&& Array.isArray(marketplaceJson.plugins)
|
|
185
|
+
&& marketplaceJson.plugins[0]
|
|
186
|
+
&& typeof marketplaceJson.plugins[0].name === 'string'
|
|
187
|
+
) {
|
|
188
|
+
name = marketplaceJson.plugins[0].name;
|
|
189
|
+
} else if (claudePlugin && typeof claudePlugin.name === 'string') {
|
|
190
|
+
name = claudePlugin.name;
|
|
191
|
+
} else if (typeof packageJson.name === 'string') {
|
|
192
|
+
name = stripNpmScope(packageJson.name);
|
|
193
|
+
} else {
|
|
194
|
+
throw new Error('codex-plugin.buildManifest: name is required (no source)');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// version — required, semver-shaped.
|
|
198
|
+
if (typeof packageJson.version !== 'string' || !/^\d+\.\d+\.\d+/.test(packageJson.version)) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
'codex-plugin.buildManifest: packageJson.version is required and must be semver-shaped'
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
const version = packageJson.version;
|
|
204
|
+
|
|
205
|
+
// description — required, free text.
|
|
206
|
+
if (typeof packageJson.description !== 'string' || packageJson.description.length === 0) {
|
|
207
|
+
throw new Error('codex-plugin.buildManifest: packageJson.description is required');
|
|
208
|
+
}
|
|
209
|
+
const description = packageJson.description;
|
|
210
|
+
|
|
211
|
+
// author — prefer claudePlugin (has url), then marketplace, then package.json.
|
|
212
|
+
let author;
|
|
213
|
+
if (
|
|
214
|
+
claudePlugin
|
|
215
|
+
&& claudePlugin.author
|
|
216
|
+
&& typeof claudePlugin.author === 'object'
|
|
217
|
+
&& typeof claudePlugin.author.name === 'string'
|
|
218
|
+
) {
|
|
219
|
+
author = Object.assign({}, claudePlugin.author);
|
|
220
|
+
} else if (
|
|
221
|
+
marketplaceJson
|
|
222
|
+
&& Array.isArray(marketplaceJson.plugins)
|
|
223
|
+
&& marketplaceJson.plugins[0]
|
|
224
|
+
&& marketplaceJson.plugins[0].author
|
|
225
|
+
&& typeof marketplaceJson.plugins[0].author === 'object'
|
|
226
|
+
) {
|
|
227
|
+
author = Object.assign({}, marketplaceJson.plugins[0].author);
|
|
228
|
+
} else if (typeof packageJson.author === 'string') {
|
|
229
|
+
author = { name: packageJson.author };
|
|
230
|
+
} else if (
|
|
231
|
+
packageJson.author
|
|
232
|
+
&& typeof packageJson.author === 'object'
|
|
233
|
+
&& typeof packageJson.author.name === 'string'
|
|
234
|
+
) {
|
|
235
|
+
author = Object.assign({}, packageJson.author);
|
|
236
|
+
} else {
|
|
237
|
+
author = { name: 'unknown' };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// homepage — verbatim, omit if absent.
|
|
241
|
+
const homepage =
|
|
242
|
+
typeof packageJson.homepage === 'string' && packageJson.homepage.length > 0
|
|
243
|
+
? packageJson.homepage
|
|
244
|
+
: undefined;
|
|
245
|
+
|
|
246
|
+
// repository — string or object form, strip trailing .git for cleaner display.
|
|
247
|
+
let repository;
|
|
248
|
+
if (packageJson.repository) {
|
|
249
|
+
let rawUrl;
|
|
250
|
+
if (typeof packageJson.repository === 'string') {
|
|
251
|
+
rawUrl = packageJson.repository;
|
|
252
|
+
} else if (
|
|
253
|
+
typeof packageJson.repository === 'object'
|
|
254
|
+
&& typeof packageJson.repository.url === 'string'
|
|
255
|
+
) {
|
|
256
|
+
rawUrl = packageJson.repository.url;
|
|
257
|
+
}
|
|
258
|
+
if (rawUrl) {
|
|
259
|
+
repository = stripGitSuffix(rawUrl);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// license — verbatim, omit if absent.
|
|
264
|
+
const license =
|
|
265
|
+
typeof packageJson.license === 'string' && packageJson.license.length > 0
|
|
266
|
+
? packageJson.license
|
|
267
|
+
: undefined;
|
|
268
|
+
|
|
269
|
+
// keywords — curated ≤10-tag subset.
|
|
270
|
+
const keywords = curateKeywords(packageJson.keywords || []);
|
|
271
|
+
|
|
272
|
+
// skills — static path string per build doc complete-manifest example.
|
|
273
|
+
const skills = './skills/';
|
|
274
|
+
|
|
275
|
+
// mcpServers — inline object form (D-14 minimalism: no separate .mcp.json
|
|
276
|
+
// artifact in this plan). The bin name `gdd-mcp` is verified against
|
|
277
|
+
// package.json#bin during integration.
|
|
278
|
+
const mcpServers = {
|
|
279
|
+
'gdd-mcp': {
|
|
280
|
+
command: 'npx',
|
|
281
|
+
args: ['-y', `--package=${packageJson.name}`, 'gdd-mcp'],
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// interface — 9-field install-surface metadata per Schema Mapping table.
|
|
286
|
+
const developerName = (author && typeof author.name === 'string')
|
|
287
|
+
? author.name
|
|
288
|
+
: 'hegemonart';
|
|
289
|
+
|
|
290
|
+
const categoryRaw =
|
|
291
|
+
(marketplaceJson
|
|
292
|
+
&& Array.isArray(marketplaceJson.plugins)
|
|
293
|
+
&& marketplaceJson.plugins[0]
|
|
294
|
+
&& typeof marketplaceJson.plugins[0].category === 'string')
|
|
295
|
+
? marketplaceJson.plugins[0].category
|
|
296
|
+
: 'design';
|
|
297
|
+
const category = capitalize(categoryRaw);
|
|
298
|
+
|
|
299
|
+
const interfaceObj = {
|
|
300
|
+
displayName: 'Get Design Done',
|
|
301
|
+
shortDescription: truncate(description, 120),
|
|
302
|
+
longDescription: (typeof readmeFirstPara === 'string' && readmeFirstPara.length > 0)
|
|
303
|
+
? readmeFirstPara
|
|
304
|
+
: description,
|
|
305
|
+
developerName,
|
|
306
|
+
category,
|
|
307
|
+
capabilities: ['Read', 'Write'],
|
|
308
|
+
websiteURL: homepage || '',
|
|
309
|
+
defaultPrompt: [
|
|
310
|
+
'Run /gdd:brief to start a design cycle.',
|
|
311
|
+
'Use $gdd-explore to audit a screen.',
|
|
312
|
+
],
|
|
313
|
+
brandColor: '#10A37F',
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// Assemble in documented order. Omit undefined fields so JSON.stringify
|
|
317
|
+
// produces a clean diff (matches cursor-marketplace.cjs convention).
|
|
318
|
+
const manifest = {};
|
|
319
|
+
manifest.name = name;
|
|
320
|
+
manifest.version = version;
|
|
321
|
+
manifest.description = description;
|
|
322
|
+
manifest.author = author;
|
|
323
|
+
if (homepage !== undefined) manifest.homepage = homepage;
|
|
324
|
+
if (repository !== undefined) manifest.repository = repository;
|
|
325
|
+
if (license !== undefined) manifest.license = license;
|
|
326
|
+
manifest.keywords = keywords;
|
|
327
|
+
manifest.skills = skills;
|
|
328
|
+
manifest.mcpServers = mcpServers;
|
|
329
|
+
manifest.interface = interfaceObj;
|
|
330
|
+
|
|
331
|
+
return manifest;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Convert/emit the codex-plugin bundle into a destination directory.
|
|
336
|
+
* Called by build-distribution-bundles.cjs (Plan 28-8-X1).
|
|
337
|
+
*
|
|
338
|
+
* Per CONTEXT D-06, `skills/` is the shared source — this converter emits
|
|
339
|
+
* the marketplace bundle as:
|
|
340
|
+
*
|
|
341
|
+
* <outDir>/
|
|
342
|
+
* .codex-plugin/
|
|
343
|
+
* plugin.json ← the manifest object, JSON.stringified
|
|
344
|
+
* skills/
|
|
345
|
+
* <each skill copied verbatim from input.skillsDir>
|
|
346
|
+
*
|
|
347
|
+
* Codex consumes Claude-compatible SKILL.md (Phase 28.5 contract is
|
|
348
|
+
* already mattpocock-shaped, which Codex accepts per research § vs
|
|
349
|
+
* AGENTS.md) so no per-skill content transform is required at the
|
|
350
|
+
* Tier-2 bundle layer. The Tier-1 codex.cjs converter remains
|
|
351
|
+
* responsible for any per-runtime SKILL.md rewrites needed by the
|
|
352
|
+
* file-drop install path; those rewrites are irrelevant to a marketplace
|
|
353
|
+
* bundle.
|
|
354
|
+
*
|
|
355
|
+
* Idempotent: rerunning with the same inputs produces identical files.
|
|
356
|
+
* Touches only paths under `outDir`. The source `skillsDir` is read-only.
|
|
357
|
+
*
|
|
358
|
+
* @param {Object} input
|
|
359
|
+
* @param {string} input.skillsDir Path to source skills/ tree.
|
|
360
|
+
* @param {string} input.outDir Path to destination bundle directory.
|
|
361
|
+
* @param {Object} input.manifest Manifest object from buildManifest().
|
|
362
|
+
* @returns {{ manifestPath: string, outDir: string }}
|
|
363
|
+
*/
|
|
364
|
+
function convert(input) {
|
|
365
|
+
if (!input || typeof input !== 'object') {
|
|
366
|
+
throw new Error('codex-plugin.convert: input is required');
|
|
367
|
+
}
|
|
368
|
+
const { skillsDir, outDir, manifest } = input;
|
|
369
|
+
if (typeof skillsDir !== 'string' || skillsDir.length === 0) {
|
|
370
|
+
throw new Error('codex-plugin.convert: input.skillsDir is required');
|
|
371
|
+
}
|
|
372
|
+
if (typeof outDir !== 'string' || outDir.length === 0) {
|
|
373
|
+
throw new Error('codex-plugin.convert: input.outDir is required');
|
|
374
|
+
}
|
|
375
|
+
if (!manifest || typeof manifest !== 'object') {
|
|
376
|
+
throw new Error('codex-plugin.convert: input.manifest is required');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Validate required fields before writing.
|
|
380
|
+
for (const field of MANIFEST_REQUIRED_FIELDS) {
|
|
381
|
+
if (!manifest[field]) {
|
|
382
|
+
throw new Error(`codex-plugin: manifest missing required field "${field}"`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Ensure output dir exists.
|
|
387
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
388
|
+
|
|
389
|
+
// Write manifest at <outDir>/.codex-plugin/plugin.json.
|
|
390
|
+
const manifestDir = path.join(outDir, '.codex-plugin');
|
|
391
|
+
fs.mkdirSync(manifestDir, { recursive: true });
|
|
392
|
+
const manifestPath = path.join(manifestDir, 'plugin.json');
|
|
393
|
+
fs.writeFileSync(
|
|
394
|
+
manifestPath,
|
|
395
|
+
JSON.stringify(manifest, null, 2) + '\n',
|
|
396
|
+
'utf8'
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
// Copy skills/ tree verbatim (D-06: skills are shared source, no rewriting).
|
|
400
|
+
if (fs.existsSync(skillsDir)) {
|
|
401
|
+
copyDirRecursive(skillsDir, path.join(outDir, 'skills'));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return { manifestPath, outDir };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
module.exports = { buildManifest, convert, MANIFEST_REQUIRED_FIELDS, CURATED_KEYWORDS };
|