GitNexus/.devcontainer/translate-plugin-registries.cjs
Gergo Magyar 1a2c480847 fix(devcontainer): set persist-credentials:false on CI checkouts + prettier
- zizmor `artipacked` (CodeQL/GitHub Advanced Security) flagged both
  actions/checkout steps in ci-devcontainer.yml: checkout defaults to
  persist-credentials:true, leaving GITHUB_TOKEN in .git/config where it
  can leak into uploaded artifacts. Both jobs are read-only (run tests /
  build smoke, never push), so persist-credentials:false is correct —
  matches the repo convention in codeql.yml / ci-tests.yml.
- Ran prettier 3.8.0 over the new .cjs modules + test (single-quote/style
  normalization to match the repo). JSON/YAML were already compliant;
  README is in .prettierignore; .sh has no prettier parser. Behavior
  unchanged — 12/12 transform unit tests still pass.
2026-05-28 21:51:55 +01:00

92 lines
3.3 KiB
JavaScript

// Translates Claude + Cursor plugin-registry JSON files from host absolute
// paths to the container's Linux paths, then writes them into the named
// volume. Both CLIs bake absolute OS-native install paths into their registry
// JSONs — `C:\Users\X\.claude\plugins\...` on Windows, `/Users/X/.cursor/...`
// on macOS — so the host versions can't be bind-mounted into the Linux
// container (the CLI fails with `cache-miss` resolving a Windows path under
// Linux). For each CLI we read the host registry, rewrite every absolute path
// ending in `/.<cli>/plugins/<rest>` to `/home/node/.<cli>/plugins/<rest>`,
// and write the result into the named volume. (Codex needs no translation —
// its enablement registry is config.toml with git URLs, not filesystem paths,
// so its whole plugins/ dir is bind-mounted instead.)
//
// Extracted from a post-create.sh heredoc so the regex + deep rewrite are
// lintable and unit-tested (the regex has had path-handling bugs before).
'use strict';
const fs = require('fs');
const path = require('path');
// Match an absolute path that contains `<sep>.<cli><sep>plugins<sep><rest>`
// where <sep> is `/` or `\`. Anchored at start; the lazy `.*?` consumes the
// home prefix up to the FIRST `.<cli>/plugins` segment.
function buildRe(cliName) {
return new RegExp(`^(?:[A-Za-z]:)?[\\\\/].*?[\\\\/]\\.${cliName}[\\\\/]plugins[\\\\/](.*)$`);
}
// Recursively rewrite every string value in `obj` that matches `re`,
// remapping it under `ctr` (the container plugins dir) and normalizing
// Windows backslashes to forward slashes.
function rewriteDeep(obj, re, ctr) {
if (Array.isArray(obj)) return obj.map((v) => rewriteDeep(v, re, ctr));
if (obj && typeof obj === 'object') {
const out = {};
for (const [k, v] of Object.entries(obj)) out[k] = rewriteDeep(v, re, ctr);
return out;
}
if (typeof obj === 'string') {
return obj.replace(re, (_, rest) => `${ctr}/${rest.replace(/\\/g, '/')}`);
}
return obj;
}
const REGISTRIES = [
{
cli: 'claude',
host: '/host/.claude/plugins',
ctr: '/home/node/.claude/plugins',
files: ['known_marketplaces.json', 'installed_plugins.json', 'plugin-catalog-cache.json'],
},
{
cli: 'cursor',
host: '/host/.cursor/plugins',
ctr: '/home/node/.cursor/plugins',
files: ['installed_plugins.json'],
},
];
function translate(registries) {
for (const reg of registries) {
const re = buildRe(reg.cli);
try {
fs.mkdirSync(reg.ctr, { recursive: true });
} catch (err) {
console.error(`[post-create] ERROR: failed to create ${reg.ctr}: ${err && err.message}`);
process.exit(1);
}
for (const name of reg.files) {
const src = path.join(reg.host, name);
const dst = path.join(reg.ctr, name);
if (!fs.existsSync(src) || fs.statSync(src).size === 0) continue;
let data;
try {
data = JSON.parse(fs.readFileSync(src, 'utf8'));
} catch {
continue; // skip a malformed host registry rather than abort
}
try {
fs.writeFileSync(dst, JSON.stringify(rewriteDeep(data, re, reg.ctr), null, 2));
} catch (err) {
console.error(`[post-create] ERROR: failed to write ${dst}: ${err && err.message}`);
process.exit(1);
}
}
}
}
module.exports = { buildRe, rewriteDeep, REGISTRIES, translate };
if (require.main === module) {
translate(REGISTRIES);
}