diff --git a/packages/pi-coding-agent/src/core/extensions/loader.ts b/packages/pi-coding-agent/src/core/extensions/loader.ts index 62e7e08bf..1f50198b1 100644 --- a/packages/pi-coding-agent/src/core/extensions/loader.ts +++ b/packages/pi-coding-agent/src/core/extensions/loader.ts @@ -495,7 +495,13 @@ function resolveExtensionEntries(dir: string): string[] | null { const packageJsonPath = path.join(dir, "package.json"); if (fs.existsSync(packageJsonPath)) { const manifest = readPiManifest(packageJsonPath); - if (manifest?.extensions?.length) { + if (manifest) { + // When a pi manifest exists, it is authoritative โ€” don't fall through + // to index.ts/index.js auto-detection. This allows library directories + // (like cmux) to opt out by declaring "pi": {} with no extensions. + if (!manifest.extensions?.length) { + return null; + } const entries: string[] = []; for (const extPath of manifest.extensions) { const resolvedExtPath = path.resolve(dir, extPath); @@ -503,9 +509,7 @@ function resolveExtensionEntries(dir: string): string[] | null { entries.push(resolvedExtPath); } } - if (entries.length > 0) { - return entries; - } + return entries.length > 0 ? entries : null; } } diff --git a/src/resources/extensions/cmux/package.json b/src/resources/extensions/cmux/package.json new file mode 100644 index 000000000..6eca7fa6a --- /dev/null +++ b/src/resources/extensions/cmux/package.json @@ -0,0 +1,7 @@ +{ + "name": "@gsd/cmux", + "private": true, + "type": "module", + "description": "cmux integration library โ€” used by other extensions, not an extension itself", + "pi": {} +} diff --git a/src/resources/extensions/gsd/tests/cmux.test.ts b/src/resources/extensions/gsd/tests/cmux.test.ts index 2efbed1a8..d174285b1 100644 --- a/src/resources/extensions/gsd/tests/cmux.test.ts +++ b/src/resources/extensions/gsd/tests/cmux.test.ts @@ -1,5 +1,8 @@ -import test from "node:test"; +import test, { describe } from "node:test"; import assert from "node:assert/strict"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; import { buildCmuxProgress, buildCmuxStatusLabel, @@ -96,3 +99,24 @@ test("buildCmuxStatusLabel and progress prefer deepest active unit", () => { assert.equal(buildCmuxStatusLabel(state), "M001 S02/T03 ยท executing"); assert.deepEqual(buildCmuxProgress(state), { value: 0.4, label: "2/5 tasks" }); }); + +describe("cmux extension discovery opt-out", () => { + test("cmux directory has package.json with pi manifest to prevent auto-discovery as extension", () => { + const cmuxDir = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "../../cmux", + ); + const pkgPath = path.join(cmuxDir, "package.json"); + assert.ok(fs.existsSync(pkgPath), `${pkgPath} must exist`); + + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")); + assert.ok( + pkg.pi !== undefined && typeof pkg.pi === "object", + 'package.json must have a "pi" field to opt out of extension auto-discovery', + ); + assert.ok( + !pkg.pi.extensions?.length, + "pi.extensions must be empty or absent โ€” cmux is a library, not an extension", + ); + }); +});