diff --git a/src/adapters/pi/index.ts b/src/adapters/pi/index.ts index dd9a273a2..ec3ff88ee 100644 --- a/src/adapters/pi/index.ts +++ b/src/adapters/pi/index.ts @@ -153,61 +153,70 @@ export class PiAdapter extends BaseAdapter implements HookAdapter { check: "Hook support", status: "pass", message: - "Pi hooks are wired via the context-mode Pi extension " + - "(~/.pi/extensions/context-mode/), not via JSON-stdio.", + "Pi hooks are wired via the context-mode Pi extension, " + + "not via JSON-stdio.", }, ]; } + private getExtensionPackagePaths(): string[] { + const agentDir = process.env.PI_CODING_AGENT_DIR + ?? resolve(homedir(), ".pi", "agent"); + + return [ + resolve(agentDir, "npm", "node_modules", "context-mode", "package.json"), + resolve(homedir(), ".pi", "extensions", "context-mode", "package.json"), + ]; + } + checkPluginRegistration(): DiagnosticResult { - // Pi registers extensions by directory presence; the version-sync - // script writes ~/.pi/extensions/context-mode/package.json. We treat - // that file as the registration signal. - const pkgPath = resolve( - homedir(), - ".pi", - "extensions", - "context-mode", - "package.json", - ); - try { - const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); - if (pkg?.name === "context-mode") { - return { - check: "Pi extension registration", - status: "pass", - message: `context-mode extension installed at ${pkgPath}`, - }; + let unexpectedPath: string | undefined; + + for (const pkgPath of this.getExtensionPackagePaths()) { + try { + const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); + if (pkg?.name === "context-mode") { + return { + check: "Pi extension registration", + status: "pass", + message: `context-mode extension installed at ${pkgPath}`, + }; + } + unexpectedPath ??= pkgPath; + } catch { + // Try the next supported Pi package layout. } + } + + if (unexpectedPath) { return { check: "Pi extension registration", status: "warn", - message: `Unexpected package at ${pkgPath}`, - }; - } catch { - return { - check: "Pi extension registration", - status: "fail", - message: `context-mode not found at ${pkgPath}`, - fix: "Run: context-mode upgrade", + message: `Unexpected package at ${unexpectedPath}`, }; } + + return { + check: "Pi extension registration", + status: "fail", + message: "context-mode not found in Pi's managed or legacy extension directories", + fix: "Run: context-mode upgrade", + }; } getInstalledVersion(): string { - try { - const pkgPath = resolve( - homedir(), - ".pi", - "extensions", - "context-mode", - "package.json", - ); - const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); - return pkg.version ?? "unknown"; - } catch { - return "not installed"; + for (const pkgPath of this.getExtensionPackagePaths()) { + try { + const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); + if (pkg?.name === "context-mode") { + return pkg.version ?? "unknown"; + } + } catch { + // Try the next supported Pi package layout. + } } + + return "not installed"; } // ── Upgrade ──────────────────────────────────────────── diff --git a/tests/adapters/pi-adapter.test.ts b/tests/adapters/pi-adapter.test.ts index 3b645c765..0fd93a680 100644 --- a/tests/adapters/pi-adapter.test.ts +++ b/tests/adapters/pi-adapter.test.ts @@ -1,5 +1,6 @@ import "../setup-home"; -import { describe, it, expect, beforeEach } from "vitest"; +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { join, resolve } from "node:path"; import { PiAdapter } from "../../src/adapters/pi/index.js"; @@ -10,10 +11,35 @@ import { hashProjectDirCanonical, resolveSessionDbPath } from "../../src/session describe("PiAdapter — Pi platform adapter", () => { let adapter: PiAdapter; + const managedExtensionDir = () => join( + homedir(), + ".pi", + "agent", + "npm", + "node_modules", + "context-mode", + ); + + const legacyExtensionDir = () => join( + homedir(), + ".pi", + "extensions", + "context-mode", + ); + beforeEach(() => { + delete process.env.PI_CODING_AGENT_DIR; + rmSync(managedExtensionDir(), { recursive: true, force: true }); + rmSync(legacyExtensionDir(), { recursive: true, force: true }); adapter = new PiAdapter(); }); + afterEach(() => { + delete process.env.PI_CODING_AGENT_DIR; + rmSync(managedExtensionDir(), { recursive: true, force: true }); + rmSync(legacyExtensionDir(), { recursive: true, force: true }); + }); + // ── Identity ─────────────────────────────────────────── describe("identity", () => { @@ -140,4 +166,49 @@ describe("PiAdapter — Pi platform adapter", () => { expect(adapter.readSettings()).toEqual({ foo: "bar" }); }); }); + + describe("extension diagnostics", () => { + it("detects a package installed in Pi's managed npm directory", () => { + mkdirSync(managedExtensionDir(), { recursive: true }); + writeFileSync( + join(managedExtensionDir(), "package.json"), + JSON.stringify({ name: "context-mode", version: "1.2.3" }), + ); + + expect(adapter.checkPluginRegistration()).toMatchObject({ status: "pass" }); + expect(adapter.getInstalledVersion()).toBe("1.2.3"); + }); + + it("honors PI_CODING_AGENT_DIR for managed packages", () => { + const customAgentDir = join(homedir(), "custom-pi-agent"); + const extensionDir = join( + customAgentDir, + "npm", + "node_modules", + "context-mode", + ); + process.env.PI_CODING_AGENT_DIR = customAgentDir; + mkdirSync(extensionDir, { recursive: true }); + writeFileSync( + join(extensionDir, "package.json"), + JSON.stringify({ name: "context-mode", version: "2.0.0" }), + ); + + expect(adapter.checkPluginRegistration()).toMatchObject({ status: "pass" }); + expect(adapter.getInstalledVersion()).toBe("2.0.0"); + + rmSync(customAgentDir, { recursive: true, force: true }); + }); + + it("keeps the legacy extension directory as a fallback", () => { + mkdirSync(legacyExtensionDir(), { recursive: true }); + writeFileSync( + join(legacyExtensionDir(), "package.json"), + JSON.stringify({ name: "context-mode", version: "0.9.0" }), + ); + + expect(adapter.checkPluginRegistration()).toMatchObject({ status: "pass" }); + expect(adapter.getInstalledVersion()).toBe("0.9.0"); + }); + }); });