diff --git a/.devcontainer/seed-claude-config.cjs b/.devcontainer/seed-claude-config.cjs index 5efe3fe42..d9e488731 100644 --- a/.devcontainer/seed-claude-config.cjs +++ b/.devcontainer/seed-claude-config.cjs @@ -14,17 +14,17 @@ // test harness). DISABLE_AUTOUPDATER=1 (containerEnv) already neutralizes // runtime updates; this purely silences the doctor mismatch + native probe. -"use strict"; +'use strict'; -const fs = require("fs"); +const fs = require('fs'); // Host binary-management / machine-install fields — never valid for an // `npm install -g` container. Stripping lets Claude auto-detect npm-global. const MACHINE_FIELDS = [ - "installMethod", - "autoUpdates", - "autoUpdatesProtectedForNative", - "shiftEnterKeyBindingInstalled", + 'installMethod', + 'autoUpdates', + 'autoUpdatesProtectedForNative', + 'shiftEnterKeyBindingInstalled', ]; // Pure transform: take whatever the host file parsed to and return the @@ -34,7 +34,7 @@ const MACHINE_FIELDS = [ // hasCompletedOnboarding assignment, and re-trigger onboarding every rebuild). function sanitizeClaudeConfig(parsed) { let cfg = parsed; - if (cfg === null || typeof cfg !== "object" || Array.isArray(cfg)) { + if (cfg === null || typeof cfg !== 'object' || Array.isArray(cfg)) { cfg = {}; } for (const k of MACHINE_FIELDS) { @@ -47,7 +47,7 @@ function sanitizeClaudeConfig(parsed) { function readHostConfig(src) { try { if (fs.existsSync(src) && fs.statSync(src).size > 0) { - return JSON.parse(fs.readFileSync(src, "utf8")); + return JSON.parse(fs.readFileSync(src, 'utf8')); } } catch { // Malformed/unreadable host file — fall back to an empty config so the @@ -57,16 +57,14 @@ function readHostConfig(src) { } function main() { - const src = process.argv[2] || "/host/.claude.json"; - const dst = process.argv[3] || "/home/node/.claude.json"; + const src = process.argv[2] || '/host/.claude.json'; + const dst = process.argv[3] || '/home/node/.claude.json'; const cfg = sanitizeClaudeConfig(readHostConfig(src)); try { fs.writeFileSync(dst, JSON.stringify(cfg, null, 2)); fs.chmodSync(dst, 0o644); } catch (err) { - console.error( - `[post-create] ERROR: failed to seed ${dst}: ${err && err.message}`, - ); + console.error(`[post-create] ERROR: failed to seed ${dst}: ${err && err.message}`); process.exit(1); } } diff --git a/.devcontainer/translate-plugin-registries.cjs b/.devcontainer/translate-plugin-registries.cjs index d10a1f0e7..2425635d6 100644 --- a/.devcontainer/translate-plugin-registries.cjs +++ b/.devcontainer/translate-plugin-registries.cjs @@ -13,18 +13,16 @@ // 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"; +'use strict'; -const fs = require("fs"); -const path = require("path"); +const fs = require('fs'); +const path = require('path'); // Match an absolute path that contains `.plugins` // where is `/` or `\`. Anchored at start; the lazy `.*?` consumes the // home prefix up to the FIRST `./plugins` segment. function buildRe(cliName) { - return new RegExp( - `^(?:[A-Za-z]:)?[\\\\/].*?[\\\\/]\\.${cliName}[\\\\/]plugins[\\\\/](.*)$`, - ); + return new RegExp(`^(?:[A-Za-z]:)?[\\\\/].*?[\\\\/]\\.${cliName}[\\\\/]plugins[\\\\/](.*)$`); } // Recursively rewrite every string value in `obj` that matches `re`, @@ -32,33 +30,29 @@ function buildRe(cliName) { // 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") { + 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, "/")}`); + 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: '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"], + cli: 'cursor', + host: '/host/.cursor/plugins', + ctr: '/home/node/.cursor/plugins', + files: ['installed_plugins.json'], }, ]; @@ -68,9 +62,7 @@ function translate(registries) { try { fs.mkdirSync(reg.ctr, { recursive: true }); } catch (err) { - console.error( - `[post-create] ERROR: failed to create ${reg.ctr}: ${err && err.message}`, - ); + console.error(`[post-create] ERROR: failed to create ${reg.ctr}: ${err && err.message}`); process.exit(1); } for (const name of reg.files) { @@ -79,19 +71,14 @@ function translate(registries) { if (!fs.existsSync(src) || fs.statSync(src).size === 0) continue; let data; try { - data = JSON.parse(fs.readFileSync(src, "utf8")); + 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), - ); + 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}`, - ); + console.error(`[post-create] ERROR: failed to write ${dst}: ${err && err.message}`); process.exit(1); } } diff --git a/.devcontainer/translate-plugin-registries.test.cjs b/.devcontainer/translate-plugin-registries.test.cjs index 4e0528d48..ad3322aa1 100644 --- a/.devcontainer/translate-plugin-registries.test.cjs +++ b/.devcontainer/translate-plugin-registries.test.cjs @@ -8,121 +8,118 @@ // Run with the built-in Node test runner (no deps): // node --test .devcontainer/ -"use strict"; +'use strict'; -const test = require("node:test"); -const assert = require("node:assert/strict"); +const test = require('node:test'); +const assert = require('node:assert/strict'); -const { - buildRe, - rewriteDeep, -} = require("./translate-plugin-registries.cjs"); -const { sanitizeClaudeConfig } = require("./seed-claude-config.cjs"); +const { buildRe, rewriteDeep } = require('./translate-plugin-registries.cjs'); +const { sanitizeClaudeConfig } = require('./seed-claude-config.cjs'); -const CLAUDE = "/home/node/.claude/plugins"; -const CURSOR = "/home/node/.cursor/plugins"; +const CLAUDE = '/home/node/.claude/plugins'; +const CURSOR = '/home/node/.cursor/plugins'; function rw(value, cli, ctr) { return rewriteDeep(value, buildRe(cli), ctr); } -test("claude: Windows backslash absolute path -> container path", () => { +test('claude: Windows backslash absolute path -> container path', () => { assert.equal( - rw("C:\\Users\\gergo\\.claude\\plugins\\cache\\x\\1.0", "claude", CLAUDE), - "/home/node/.claude/plugins/cache/x/1.0", + rw('C:\\Users\\gergo\\.claude\\plugins\\cache\\x\\1.0', 'claude', CLAUDE), + '/home/node/.claude/plugins/cache/x/1.0', ); }); -test("claude: Windows forward-slash absolute path -> container path", () => { +test('claude: Windows forward-slash absolute path -> container path', () => { assert.equal( - rw("C:/Users/gergo/.claude/plugins/marketplaces/m", "claude", CLAUDE), - "/home/node/.claude/plugins/marketplaces/m", + rw('C:/Users/gergo/.claude/plugins/marketplaces/m', 'claude', CLAUDE), + '/home/node/.claude/plugins/marketplaces/m', ); }); -test("claude: macOS POSIX path -> container path", () => { +test('claude: macOS POSIX path -> container path', () => { assert.equal( - rw("/Users/alice/.claude/plugins/marketplaces/m", "claude", CLAUDE), - "/home/node/.claude/plugins/marketplaces/m", + rw('/Users/alice/.claude/plugins/marketplaces/m', 'claude', CLAUDE), + '/home/node/.claude/plugins/marketplaces/m', ); }); -test("claude: Linux POSIX path -> container path", () => { +test('claude: Linux POSIX path -> container path', () => { assert.equal( - rw("/home/bob/.claude/plugins/cache/foo", "claude", CLAUDE), - "/home/node/.claude/plugins/cache/foo", + rw('/home/bob/.claude/plugins/cache/foo', 'claude', CLAUDE), + '/home/node/.claude/plugins/cache/foo', ); }); -test("cursor: Windows path -> container cursor path", () => { +test('cursor: Windows path -> container cursor path', () => { assert.equal( - rw("C:\\Users\\gergo\\.cursor\\plugins\\local\\myplug", "cursor", CURSOR), - "/home/node/.cursor/plugins/local/myplug", + rw('C:\\Users\\gergo\\.cursor\\plugins\\local\\myplug', 'cursor', CURSOR), + '/home/node/.cursor/plugins/local/myplug', ); }); -test("cross-CLI isolation: claude regex leaves a .cursor path untouched", () => { - const input = "C:\\Users\\g\\.cursor\\plugins\\x"; - assert.equal(rw(input, "claude", CLAUDE), input); +test('cross-CLI isolation: claude regex leaves a .cursor path untouched', () => { + const input = 'C:\\Users\\g\\.cursor\\plugins\\x'; + assert.equal(rw(input, 'claude', CLAUDE), input); }); -test("non-path strings pass through unchanged", () => { - assert.equal(rw("not-a-path", "claude", CLAUDE), "not-a-path"); - assert.equal(rw("https://github.com/EveryInc/x.git", "claude", CLAUDE), "https://github.com/EveryInc/x.git"); +test('non-path strings pass through unchanged', () => { + assert.equal(rw('not-a-path', 'claude', CLAUDE), 'not-a-path'); + assert.equal( + rw('https://github.com/EveryInc/x.git', 'claude', CLAUDE), + 'https://github.com/EveryInc/x.git', + ); }); -test("non-string scalars pass through unchanged", () => { - assert.equal(rw(42, "claude", CLAUDE), 42); - assert.equal(rw(null, "claude", CLAUDE), null); - assert.equal(rw(true, "claude", CLAUDE), true); +test('non-string scalars pass through unchanged', () => { + assert.equal(rw(42, 'claude', CLAUDE), 42); + assert.equal(rw(null, 'claude', CLAUDE), null); + assert.equal(rw(true, 'claude', CLAUDE), true); }); -test("nested objects/arrays are rewritten deeply", () => { +test('nested objects/arrays are rewritten deeply', () => { const input = { - "compound-engineering@m": [ - { installPath: "C:\\Users\\g\\.claude\\plugins\\cache\\ce\\3.9.2", version: "3.9.2" }, + 'compound-engineering@m': [ + { installPath: 'C:\\Users\\g\\.claude\\plugins\\cache\\ce\\3.9.2', version: '3.9.2' }, ], - nested: { installLocation: "/Users/g/.claude/plugins/marketplaces/m" }, + nested: { installLocation: '/Users/g/.claude/plugins/marketplaces/m' }, }; - const out = rw(input, "claude", CLAUDE); + const out = rw(input, 'claude', CLAUDE); assert.equal( - out["compound-engineering@m"][0].installPath, - "/home/node/.claude/plugins/cache/ce/3.9.2", - ); - assert.equal(out["compound-engineering@m"][0].version, "3.9.2"); - assert.equal( - out.nested.installLocation, - "/home/node/.claude/plugins/marketplaces/m", + out['compound-engineering@m'][0].installPath, + '/home/node/.claude/plugins/cache/ce/3.9.2', ); + assert.equal(out['compound-engineering@m'][0].version, '3.9.2'); + assert.equal(out.nested.installLocation, '/home/node/.claude/plugins/marketplaces/m'); }); -test("sanitizeClaudeConfig: strips machine fields, forces hasCompletedOnboarding", () => { +test('sanitizeClaudeConfig: strips machine fields, forces hasCompletedOnboarding', () => { const out = sanitizeClaudeConfig({ - installMethod: "native", + installMethod: 'native', autoUpdates: false, autoUpdatesProtectedForNative: true, shiftEnterKeyBindingInstalled: true, - userID: "abc", - oauthAccount: { emailAddress: "x@y.z" }, + userID: 'abc', + oauthAccount: { emailAddress: 'x@y.z' }, }); assert.equal(out.installMethod, undefined); assert.equal(out.autoUpdates, undefined); assert.equal(out.autoUpdatesProtectedForNative, undefined); assert.equal(out.shiftEnterKeyBindingInstalled, undefined); - assert.equal(out.userID, "abc"); - assert.equal(out.oauthAccount.emailAddress, "x@y.z"); + assert.equal(out.userID, 'abc'); + assert.equal(out.oauthAccount.emailAddress, 'x@y.z'); assert.equal(out.hasCompletedOnboarding, true); }); -test("sanitizeClaudeConfig: non-object inputs become a valid onboarding-bearing object", () => { - for (const bad of [42, "x", null, ["a"], true]) { +test('sanitizeClaudeConfig: non-object inputs become a valid onboarding-bearing object', () => { + for (const bad of [42, 'x', null, ['a'], true]) { const out = sanitizeClaudeConfig(bad); - assert.equal(typeof out, "object"); + assert.equal(typeof out, 'object'); assert.equal(Array.isArray(out), false); assert.equal(out.hasCompletedOnboarding, true); } }); -test("sanitizeClaudeConfig: empty object still gets hasCompletedOnboarding", () => { +test('sanitizeClaudeConfig: empty object still gets hasCompletedOnboarding', () => { assert.deepEqual(sanitizeClaudeConfig({}), { hasCompletedOnboarding: true }); }); diff --git a/.github/workflows/ci-devcontainer.yml b/.github/workflows/ci-devcontainer.yml index d2f2b12c0..77d309549 100644 --- a/.github/workflows/ci-devcontainer.yml +++ b/.github/workflows/ci-devcontainer.yml @@ -31,7 +31,11 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: + # persist-credentials: false — read-only job (tests + syntax checks), + # never pushes; keeps GITHUB_TOKEN out of .git/config (zizmor artipacked). - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 22 @@ -47,7 +51,11 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: + # persist-credentials: false — read-only build smoke, never pushes; + # keeps GITHUB_TOKEN out of .git/config (zizmor artipacked). - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 22