diff --git a/scripts/plugin-open.js b/scripts/plugin-open.js index 3bf6af93..650efb55 100644 --- a/scripts/plugin-open.js +++ b/scripts/plugin-open.js @@ -76,6 +76,11 @@ function dashboardUrl() { } } +// Exported before the entry block below, not after it: `bootstrap()` requires +// this module back for `buildClient`, and when plugin-open is itself the entry +// that lazy require resolves to whatever is on module.exports at that moment. +module.exports = { buildClient, dashboardUrl }; + if (require.main === module) { // A plugin update applied mid-session (/plugin + /reload-plugins) fires no // SessionStart, so the server keeps executing a cache dir Claude Code has @@ -95,5 +100,3 @@ if (require.main === module) { console.log(dashboardUrl()); } } - -module.exports = { buildClient, dashboardUrl }; diff --git a/server/__tests__/plugin-bootstrap.test.js b/server/__tests__/plugin-bootstrap.test.js index 5c485aaf..aeecdf98 100644 --- a/server/__tests__/plugin-bootstrap.test.js +++ b/server/__tests__/plugin-bootstrap.test.js @@ -271,3 +271,26 @@ describe("ccam CLI launcher", () => { assert.match(fs.readFileSync(target, "utf8"), /my\/checkout/); }); }); + +describe("plugin-open / plugin-bootstrap require cycle", () => { + // bootstrap() lazily requires plugin-open for buildClient, and plugin-open + // requires bootstrap at load. When plugin-open is the entry point (/ccam-open) + // that lazy require resolves to whatever sits on its module.exports at that + // instant — so the assignment must come before the entry block that calls + // bootstrap(), or /ccam-open dies with "buildClient is not a function". + const src = fs.readFileSync( + path.join(__dirname, "..", "..", "scripts", "plugin-open.js"), + "utf8" + ); + + it("assigns module.exports before running the entry block", () => { + const exportsAt = src.indexOf("module.exports ="); + const entryAt = src.indexOf("require.main === module"); + assert.ok(exportsAt !== -1 && entryAt !== -1); + assert.ok(exportsAt < entryAt, "module.exports must be assigned before the entry block"); + }); + + it("exposes buildClient to a bootstrap that requires it back", () => { + assert.equal(typeof require("../../scripts/plugin-open").buildClient, "function"); + }); +});