From 9676db39458a06a54313d9df8462910ca2342b71 Mon Sep 17 00:00:00 2001 From: Renan Fagundes Date: Wed, 21 Oct 2026 10:09:33 +0100 Subject: [PATCH 1/4] chore: fill in the missing manifest fields and keep shared values in one place - Add repository to the Claude Code and Codex manifests, and category to the Antigravity manifest and both marketplace entries. - Add displayName and the directory listing fields (icon, documentation, support, privacy policy and terms of service links) to the Claude Code manifest. - Drop the empty Codex screenshots list. - scripts/plugin-metadata.json now holds the values every manifest shares. scripts/sync-manifests.mjs writes them into each manifest and the README sample output, and its --check mode runs in CI so the manifests can't drift. Co-Authored-By: Claude Opus 5.5 --- .antigravity-plugin/plugin.json | 1 + .claude-plugin/marketplace.json | 3 +- .claude-plugin/plugin.json | 9 +- .codex-plugin/plugin.json | 4 +- .github/workflows/validate-manifests.yml | 32 +++++++ scripts/plugin-metadata.json | 25 ++++++ scripts/sync-manifests.mjs | 106 +++++++++++++++++++++++ 7 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/validate-manifests.yml create mode 100644 scripts/plugin-metadata.json create mode 100644 scripts/sync-manifests.mjs diff --git a/.antigravity-plugin/plugin.json b/.antigravity-plugin/plugin.json index fcacf0f..3edc6fd 100644 --- a/.antigravity-plugin/plugin.json +++ b/.antigravity-plugin/plugin.json @@ -9,6 +9,7 @@ "Help me implement in-app purchases and set up ad mediation with LevelPlay.", "Set up multiplayer lobbies and networking for my Unity game." ], + "category": "Developer Tools", "author": { "name": "Unity Technologies", "url": "https://unity.com" diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index c9cf3de..4a0fbe1 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,8 @@ { "name": "unity", "source": "./", - "description": "Unity's official game development plugin. Build, monetize, and operate Unity games with guidance grounded in Unity's documented practices." + "description": "Unity's official game development plugin. Build, monetize, and operate Unity games with guidance grounded in Unity's documented practices.", + "category": "Developer Tools" } ] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index a7df8ed..5097208 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,5 +1,6 @@ { "name": "unity", + "displayName": "Unity", "version": "0.1.8-beta", "description": "Unity's official plugin for coding agents, with curated skills for game development, monetization, and performance optimization.", "author": { @@ -7,6 +8,7 @@ "url": "https://unity.com" }, "homepage": "https://unity.com", + "repository": "https://github.com/Unity-Technologies/unity-agent-plugin", "license": "LicenseRef-Unity-Companion-License", "keywords": [ "unity", @@ -21,5 +23,10 @@ "localization", "multiplayer", "shader-graph" - ] + ], + "icon": "./assets/unity-tile.svg", + "documentationUrl": "https://github.com/Unity-Technologies/unity-agent-plugin#readme", + "supportUrl": "https://github.com/Unity-Technologies/unity-agent-plugin/issues", + "privacyPolicyUrl": "https://unity.com/legal/privacy-policy", + "termsOfServiceUrl": "https://unity.com/legal/terms-of-service" } diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index da8c81a..1bc5e8d 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -7,6 +7,7 @@ "url": "https://unity.com" }, "homepage": "https://unity.com", + "repository": "https://github.com/Unity-Technologies/unity-agent-plugin", "license": "LicenseRef-Unity-Companion-License", "keywords": [ "unity", @@ -45,7 +46,6 @@ "Set up a tilemap level with rule tiles for my 2D Unity game.", "Help me implement in-app purchases and set up ad mediation with LevelPlay.", "Set up multiplayer lobbies and networking for my Unity game." - ], - "screenshots": [] + ] } } diff --git a/.github/workflows/validate-manifests.yml b/.github/workflows/validate-manifests.yml new file mode 100644 index 0000000..62fdebf --- /dev/null +++ b/.github/workflows/validate-manifests.yml @@ -0,0 +1,32 @@ +name: Validate manifests + +on: + pull_request: + paths: + - '.claude-plugin/**' + - '.codex-plugin/**' + - '.antigravity-plugin/**' + - '.agents/**' + - '.github/README.md' + - 'scripts/plugin-metadata.json' + - 'scripts/sync-manifests.mjs' + - '.github/workflows/validate-manifests.yml' + push: + branches: [main] + paths: + - '.claude-plugin/**' + - '.codex-plugin/**' + - '.antigravity-plugin/**' + - '.agents/**' + - 'scripts/plugin-metadata.json' + +jobs: + shared-values: + name: Manifests share one set of values + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - run: node scripts/sync-manifests.mjs --check diff --git a/scripts/plugin-metadata.json b/scripts/plugin-metadata.json new file mode 100644 index 0000000..0d3576c --- /dev/null +++ b/scripts/plugin-metadata.json @@ -0,0 +1,25 @@ +{ + "version": "0.1.8-beta", + "author": { + "name": "Unity Technologies", + "url": "https://unity.com" + }, + "homepage": "https://unity.com", + "repository": "https://github.com/Unity-Technologies/unity-agent-plugin", + "license": "LicenseRef-Unity-Companion-License", + "category": "Developer Tools", + "keywords": [ + "unity", + "gamedev", + "game-development", + "ui-toolkit", + "ugui", + "2d", + "tilemap", + "in-app-purchases", + "monetization", + "localization", + "multiplayer", + "shader-graph" + ] +} diff --git a/scripts/sync-manifests.mjs b/scripts/sync-manifests.mjs new file mode 100644 index 0000000..8470301 --- /dev/null +++ b/scripts/sync-manifests.mjs @@ -0,0 +1,106 @@ +#!/usr/bin/env node +// Keeps the values every platform manifest shares (version, author, homepage, repository, +// license, keywords, category) in one place: scripts/plugin-metadata.json. +// +// node scripts/sync-manifests.mjs write the shared values into every manifest +// node scripts/sync-manifests.mjs --check fail if any manifest has drifted (CI runs this) +// +// To release, change "version" in plugin-metadata.json and run the script without --check. +// Platform-only fields (descriptions, the Codex interface block, Antigravity prompts) stay in +// each manifest and are not touched. +import { readFileSync, writeFileSync } from 'node:fs'; + +const check = process.argv.includes('--check'); +const meta = JSON.parse(readFileSync('scripts/plugin-metadata.json', 'utf8')); +const shared = ['version', 'author', 'homepage', 'repository', 'license', 'keywords']; + +// Each target names a manifest and where each shared key lives in it. +const targets = [ + { file: '.claude-plugin/plugin.json', set: (m) => assign(m, shared) }, + { + file: '.codex-plugin/plugin.json', + set: (m) => { + assign(m, shared); + m.interface.category = meta.category; + }, + }, + { + file: '.antigravity-plugin/plugin.json', + set: (m) => assign(m, [...shared, 'category']), + }, + { + // The entry must not carry a version: plugin.json wins at install, and validate warns. + file: '.claude-plugin/marketplace.json', + set: (m) => { + const entry = pluginEntry(m); + delete entry.version; + entry.category = meta.category; + }, + }, + { + file: '.agents/plugins/marketplace.json', + set: (m) => { + pluginEntry(m).category = meta.category; + }, + }, +]; + +// Where a key goes when a manifest doesn't have it yet, so new keys sit beside related ones. +const anchors = { repository: 'homepage', license: 'repository', category: 'suggestedPrompts' }; + +function assign(manifest, keys) { + for (const key of keys) setKey(manifest, key, structuredClone(meta[key])); +} + +function setKey(obj, key, value) { + const anchor = anchors[key]; + if (key in obj || !anchor || !(anchor in obj)) { + obj[key] = value; + return; + } + const entries = Object.entries(obj); + const at = entries.findIndex(([k]) => k === anchor) + 1; + entries.splice(at, 0, [key, value]); + for (const k of Object.keys(obj)) delete obj[k]; + Object.assign(obj, Object.fromEntries(entries)); +} + +function pluginEntry(marketplace) { + const entry = marketplace.plugins?.find((p) => p.name === 'unity'); + if (!entry) throw new Error('no "unity" entry in plugins[]'); + return entry; +} + +const drifted = []; +for (const { file, set } of targets) { + const before = readFileSync(file, 'utf8').replace(/\r\n/g, '\n'); + const manifest = JSON.parse(before); + set(manifest); + const after = `${JSON.stringify(manifest, null, 2)}\n`; + if (after === before) continue; + if (check) drifted.push(file); + else writeFileSync(file, after); +} + +// The sample `agy plugin list` and `codex plugin list` output in the README shows the version. +const readme = '.github/README.md'; +const readmeText = readFileSync(readme, 'utf8'); +const stale = [...readmeText.matchAll(/\b0\.\d+\.\d+(?:-[a-z0-9.]+)?\b/g)] + .map((m) => m[0]) + .filter((v) => v !== meta.version); +if (stale.length > 0) { + if (check) drifted.push(`${readme} (shows ${[...new Set(stale)].join(', ')})`); + else writeFileSync(readme, readmeText.replaceAll(new RegExp(stale.map(escape).join('|'), 'g'), meta.version)); +} + +function escape(s) { + return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +if (drifted.length > 0) { + console.error('These files differ from scripts/plugin-metadata.json:'); + for (const file of drifted) console.error(` ${file}`); + console.error('Run `node scripts/sync-manifests.mjs` and commit the result.'); + process.exit(1); +} +console.log(check ? 'Manifests match scripts/plugin-metadata.json' : 'Manifests updated'); From 542febb9225bd9aa1637503c7219e63e01993090 Mon Sep 17 00:00:00 2001 From: Renan Fagundes Date: Wed, 21 Oct 2026 11:29:34 +0100 Subject: [PATCH 2/4] fix: drop category from the Antigravity manifest agy 1.4 reports category as an unknown field, which fails validate --strict. Co-Authored-By: Claude Opus 5.5 --- .antigravity-plugin/plugin.json | 1 - scripts/sync-manifests.mjs | 8 ++++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.antigravity-plugin/plugin.json b/.antigravity-plugin/plugin.json index 3edc6fd..fcacf0f 100644 --- a/.antigravity-plugin/plugin.json +++ b/.antigravity-plugin/plugin.json @@ -9,7 +9,6 @@ "Help me implement in-app purchases and set up ad mediation with LevelPlay.", "Set up multiplayer lobbies and networking for my Unity game." ], - "category": "Developer Tools", "author": { "name": "Unity Technologies", "url": "https://unity.com" diff --git a/scripts/sync-manifests.mjs b/scripts/sync-manifests.mjs index 8470301..743e641 100644 --- a/scripts/sync-manifests.mjs +++ b/scripts/sync-manifests.mjs @@ -25,8 +25,12 @@ const targets = [ }, }, { + // No category: agy 1.4 reports it as an unknown field, which fails `validate --strict`. file: '.antigravity-plugin/plugin.json', - set: (m) => assign(m, [...shared, 'category']), + set: (m) => { + assign(m, shared); + delete m.category; + }, }, { // The entry must not carry a version: plugin.json wins at install, and validate warns. @@ -46,7 +50,7 @@ const targets = [ ]; // Where a key goes when a manifest doesn't have it yet, so new keys sit beside related ones. -const anchors = { repository: 'homepage', license: 'repository', category: 'suggestedPrompts' }; +const anchors = { repository: 'homepage', license: 'repository' }; function assign(manifest, keys) { for (const key of keys) setKey(manifest, key, structuredClone(meta[key])); From 3e5469f94fc7f0199e4edb840a67ae907188f0ea Mon Sep 17 00:00:00 2001 From: Renan Fagundes Date: Wed, 21 Oct 2026 12:17:40 +0100 Subject: [PATCH 3/4] feat: add a script that builds the Codex release zip scripts/build-codex-zip.mjs checks the manifests match plugin-metadata.json, stages a unity/ folder with the skills (no evals/ or tests/), both manifests, the README, the license and the logos, then writes dist/unity-codex-.zip and prints the version and skill count. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 1 + scripts/build-codex-zip.mjs | 84 +++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 .gitignore create mode 100644 scripts/build-codex-zip.mjs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..849ddff --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/scripts/build-codex-zip.mjs b/scripts/build-codex-zip.mjs new file mode 100644 index 0000000..acf2306 --- /dev/null +++ b/scripts/build-codex-zip.mjs @@ -0,0 +1,84 @@ +#!/usr/bin/env node +// Builds the zip uploaded to OpenAI's plugin portal for a Codex release. +// +// node scripts/build-codex-zip.mjs writes dist/unity-codex-.zip +// +// Run it on a clean checkout of merged main, after the version bump. The zip holds one folder, +// `unity`, with the skills (no evals/ or tests/), both manifests, the README, the license and +// the logos the manifests reference. It refuses to build if the manifests disagree on +// the version or drift from scripts/plugin-metadata.json. +import { execFileSync } from 'node:child_process'; +import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { basename, join, resolve } from 'node:path'; + +const root = resolve(import.meta.dirname, '..'); +const meta = JSON.parse(readFileSync(join(root, 'scripts/plugin-metadata.json'), 'utf8')); + +// 1. The manifests must already carry the release version. +execFileSync(process.execPath, [join(root, 'scripts/sync-manifests.mjs'), '--check'], { + cwd: root, + stdio: 'inherit', +}); +const manifests = ['.codex-plugin/plugin.json', '.claude-plugin/plugin.json']; +for (const file of manifests) { + const { version } = JSON.parse(readFileSync(join(root, file), 'utf8')); + if (version !== meta.version) fail(`${file} is at ${version}, expected ${meta.version}`); +} + +// 2. Stage the package as /unity. +const excluded = new Set(['evals', 'tests', '.DS_Store', 'Thumbs.db']); +const contents = [ + 'skills', + '.codex-plugin', + '.claude-plugin', + 'README.md', + 'LICENSE.md', + 'assets/unity.svg', + 'assets/unity-small.svg', + 'assets/unity-tile.svg', // the icon .claude-plugin/plugin.json points to +]; +const stage = mkdtempSync(join(tmpdir(), 'unity-codex-')); +const pkg = join(stage, 'unity'); +try { + for (const entry of contents) { + const from = join(root, entry); + if (!existsSync(from)) fail(`missing ${entry}`); + cpSync(from, join(pkg, entry), { + recursive: true, + filter: (src) => !excluded.has(basename(src)), + }); + } + + // 3. Check what was staged before zipping it. + const skills = readdirSync(join(pkg, 'skills')).filter((d) => + existsSync(join(pkg, 'skills', d, 'SKILL.md')), + ); + const codex = JSON.parse(readFileSync(join(pkg, '.codex-plugin/plugin.json'), 'utf8')); + for (const asset of [codex.interface?.logo, codex.interface?.composerIcon].filter(Boolean)) { + if (!existsSync(join(pkg, asset))) fail(`the Codex manifest references ${asset}, which is not in the package`); + } + + // 4. Zip it. Windows and macOS ship bsdtar, which writes zip archives; Linux uses zip. + const dist = join(root, 'dist'); + mkdirSync(dist, { recursive: true }); + const out = join(dist, `unity-codex-${meta.version}.zip`); + rmSync(out, { force: true }); + if (process.platform === 'linux') { + execFileSync('zip', ['-r', '-X', '-q', out, 'unity'], { cwd: stage, stdio: 'inherit' }); + } else { + const tar = process.platform === 'win32' ? join(process.env.SystemRoot, 'System32', 'tar.exe') : 'tar'; + execFileSync(tar, ['--format', 'zip', '-cf', out, 'unity'], { cwd: stage, stdio: 'inherit' }); + } + + console.log(`Built ${out}`); + console.log(` version ${meta.version}, ${skills.length} skills, ${(statSync(out).size / 1024).toFixed(0)} KB`); + console.log('Next: upload it at https://platform.openai.com/plugins (Upload plugin to make changes).'); +} finally { + rmSync(stage, { recursive: true, force: true }); +} + +function fail(message) { + console.error(`build-codex-zip: ${message}`); + process.exit(1); +} From 6cad373682a5041e22ef21caf0efab8967820ac0 Mon Sep 17 00:00:00 2001 From: Renan Fagundes Date: Wed, 21 Oct 2026 14:36:56 +0100 Subject: [PATCH 4/4] chore: convert the release scripts to TypeScript and typecheck them in CI Node 24 runs the .mts files directly. The workflow now runs tsc against scripts/tsconfig.json (strict, erasable syntax only) before the drift check. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/validate-manifests.yml | 11 +++-- ...uild-codex-zip.mjs => build-codex-zip.mts} | 22 +++++---- ...{sync-manifests.mjs => sync-manifests.mts} | 47 +++++++++++++------ scripts/tsconfig.json | 14 ++++++ 4 files changed, 68 insertions(+), 26 deletions(-) rename scripts/{build-codex-zip.mjs => build-codex-zip.mts} (82%) rename scripts/{sync-manifests.mjs => sync-manifests.mts} (71%) create mode 100644 scripts/tsconfig.json diff --git a/.github/workflows/validate-manifests.yml b/.github/workflows/validate-manifests.yml index 62fdebf..ada8e15 100644 --- a/.github/workflows/validate-manifests.yml +++ b/.github/workflows/validate-manifests.yml @@ -9,7 +9,8 @@ on: - '.agents/**' - '.github/README.md' - 'scripts/plugin-metadata.json' - - 'scripts/sync-manifests.mjs' + - 'scripts/*.mts' + - 'scripts/tsconfig.json' - '.github/workflows/validate-manifests.yml' push: branches: [main] @@ -28,5 +29,9 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 22 - - run: node scripts/sync-manifests.mjs --check + node-version: 24 + - name: Typecheck the scripts + run: | + npm install --no-save --no-audit --no-fund typescript@5 @types/node@24 + npx tsc -p scripts/tsconfig.json + - run: node scripts/sync-manifests.mts --check diff --git a/scripts/build-codex-zip.mjs b/scripts/build-codex-zip.mts similarity index 82% rename from scripts/build-codex-zip.mjs rename to scripts/build-codex-zip.mts index acf2306..2204a8c 100644 --- a/scripts/build-codex-zip.mjs +++ b/scripts/build-codex-zip.mts @@ -1,7 +1,7 @@ #!/usr/bin/env node // Builds the zip uploaded to OpenAI's plugin portal for a Codex release. // -// node scripts/build-codex-zip.mjs writes dist/unity-codex-.zip +// node scripts/build-codex-zip.mts writes dist/unity-codex-.zip // // Run it on a clean checkout of merged main, after the version bump. The zip holds one folder, // `unity`, with the skills (no evals/ or tests/), both manifests, the README, the license and @@ -13,16 +13,18 @@ import { tmpdir } from 'node:os'; import { basename, join, resolve } from 'node:path'; const root = resolve(import.meta.dirname, '..'); -const meta = JSON.parse(readFileSync(join(root, 'scripts/plugin-metadata.json'), 'utf8')); +const meta: { version: string } = JSON.parse( + readFileSync(join(root, 'scripts/plugin-metadata.json'), 'utf8'), +); // 1. The manifests must already carry the release version. -execFileSync(process.execPath, [join(root, 'scripts/sync-manifests.mjs'), '--check'], { +execFileSync(process.execPath, [join(root, 'scripts/sync-manifests.mts'), '--check'], { cwd: root, stdio: 'inherit', }); const manifests = ['.codex-plugin/plugin.json', '.claude-plugin/plugin.json']; for (const file of manifests) { - const { version } = JSON.parse(readFileSync(join(root, file), 'utf8')); + const { version } = JSON.parse(readFileSync(join(root, file), 'utf8')) as { version: string }; if (version !== meta.version) fail(`${file} is at ${version}, expected ${meta.version}`); } @@ -54,8 +56,11 @@ try { const skills = readdirSync(join(pkg, 'skills')).filter((d) => existsSync(join(pkg, 'skills', d, 'SKILL.md')), ); - const codex = JSON.parse(readFileSync(join(pkg, '.codex-plugin/plugin.json'), 'utf8')); - for (const asset of [codex.interface?.logo, codex.interface?.composerIcon].filter(Boolean)) { + const codex = JSON.parse(readFileSync(join(pkg, '.codex-plugin/plugin.json'), 'utf8')) as { + interface?: { logo?: string; composerIcon?: string }; + }; + const assets = [codex.interface?.logo, codex.interface?.composerIcon]; + for (const asset of assets.filter((a): a is string => typeof a === 'string')) { if (!existsSync(join(pkg, asset))) fail(`the Codex manifest references ${asset}, which is not in the package`); } @@ -67,7 +72,8 @@ try { if (process.platform === 'linux') { execFileSync('zip', ['-r', '-X', '-q', out, 'unity'], { cwd: stage, stdio: 'inherit' }); } else { - const tar = process.platform === 'win32' ? join(process.env.SystemRoot, 'System32', 'tar.exe') : 'tar'; + const tar = + process.platform === 'win32' ? join(process.env.SystemRoot ?? 'C:\Windows', 'System32', 'tar.exe') : 'tar'; execFileSync(tar, ['--format', 'zip', '-cf', out, 'unity'], { cwd: stage, stdio: 'inherit' }); } @@ -78,7 +84,7 @@ try { rmSync(stage, { recursive: true, force: true }); } -function fail(message) { +function fail(message: string): never { console.error(`build-codex-zip: ${message}`); process.exit(1); } diff --git a/scripts/sync-manifests.mjs b/scripts/sync-manifests.mts similarity index 71% rename from scripts/sync-manifests.mjs rename to scripts/sync-manifests.mts index 743e641..78167ce 100644 --- a/scripts/sync-manifests.mjs +++ b/scripts/sync-manifests.mts @@ -2,26 +2,43 @@ // Keeps the values every platform manifest shares (version, author, homepage, repository, // license, keywords, category) in one place: scripts/plugin-metadata.json. // -// node scripts/sync-manifests.mjs write the shared values into every manifest -// node scripts/sync-manifests.mjs --check fail if any manifest has drifted (CI runs this) +// node scripts/sync-manifests.mts write the shared values into every manifest +// node scripts/sync-manifests.mts --check fail if any manifest has drifted (CI runs this) // // To release, change "version" in plugin-metadata.json and run the script without --check. // Platform-only fields (descriptions, the Codex interface block, Antigravity prompts) stay in // each manifest and are not touched. import { readFileSync, writeFileSync } from 'node:fs'; +type Json = string | number | boolean | null | Json[] | { [key: string]: Json }; +type JsonObject = { [key: string]: Json }; + +interface Metadata { + version: string; + author: { name: string; url?: string; email?: string }; + homepage: string; + repository: string; + license: string; + category: string; + keywords: string[]; +} + +interface Marketplace extends JsonObject { + plugins: JsonObject[]; +} + const check = process.argv.includes('--check'); -const meta = JSON.parse(readFileSync('scripts/plugin-metadata.json', 'utf8')); -const shared = ['version', 'author', 'homepage', 'repository', 'license', 'keywords']; +const meta: Metadata = JSON.parse(readFileSync('scripts/plugin-metadata.json', 'utf8')); +const shared = ['version', 'author', 'homepage', 'repository', 'license', 'keywords'] as const; // Each target names a manifest and where each shared key lives in it. -const targets = [ +const targets: { file: string; set: (manifest: JsonObject) => void }[] = [ { file: '.claude-plugin/plugin.json', set: (m) => assign(m, shared) }, { file: '.codex-plugin/plugin.json', set: (m) => { assign(m, shared); - m.interface.category = meta.category; + (m.interface as JsonObject).category = meta.category; }, }, { @@ -36,7 +53,7 @@ const targets = [ // The entry must not carry a version: plugin.json wins at install, and validate warns. file: '.claude-plugin/marketplace.json', set: (m) => { - const entry = pluginEntry(m); + const entry = pluginEntry(m as Marketplace); delete entry.version; entry.category = meta.category; }, @@ -44,19 +61,19 @@ const targets = [ { file: '.agents/plugins/marketplace.json', set: (m) => { - pluginEntry(m).category = meta.category; + pluginEntry(m as Marketplace).category = meta.category; }, }, ]; // Where a key goes when a manifest doesn't have it yet, so new keys sit beside related ones. -const anchors = { repository: 'homepage', license: 'repository' }; +const anchors: Record = { repository: 'homepage', license: 'repository' }; -function assign(manifest, keys) { +function assign(manifest: JsonObject, keys: readonly (keyof Metadata)[]): void { for (const key of keys) setKey(manifest, key, structuredClone(meta[key])); } -function setKey(obj, key, value) { +function setKey(obj: JsonObject, key: string, value: Json): void { const anchor = anchors[key]; if (key in obj || !anchor || !(anchor in obj)) { obj[key] = value; @@ -69,13 +86,13 @@ function setKey(obj, key, value) { Object.assign(obj, Object.fromEntries(entries)); } -function pluginEntry(marketplace) { +function pluginEntry(marketplace: Marketplace): JsonObject { const entry = marketplace.plugins?.find((p) => p.name === 'unity'); if (!entry) throw new Error('no "unity" entry in plugins[]'); return entry; } -const drifted = []; +const drifted: string[] = []; for (const { file, set } of targets) { const before = readFileSync(file, 'utf8').replace(/\r\n/g, '\n'); const manifest = JSON.parse(before); @@ -97,14 +114,14 @@ if (stale.length > 0) { else writeFileSync(readme, readmeText.replaceAll(new RegExp(stale.map(escape).join('|'), 'g'), meta.version)); } -function escape(s) { +function escape(s: string): string { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } if (drifted.length > 0) { console.error('These files differ from scripts/plugin-metadata.json:'); for (const file of drifted) console.error(` ${file}`); - console.error('Run `node scripts/sync-manifests.mjs` and commit the result.'); + console.error('Run `node scripts/sync-manifests.mts` and commit the result.'); process.exit(1); } console.log(check ? 'Manifests match scripts/plugin-metadata.json' : 'Manifests updated'); diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json new file mode 100644 index 0000000..3b6b4b4 --- /dev/null +++ b/scripts/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es2023", + "module": "nodenext", + "moduleResolution": "nodenext", + "strict": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "erasableSyntaxOnly": true, + "types": ["node"], + "skipLibCheck": true + }, + "include": ["*.mts"] +}