Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 49 additions & 40 deletions src/adapters/pi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ────────────────────────────────────────────
Expand Down
73 changes: 72 additions & 1 deletion tests/adapters/pi-adapter.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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", () => {
Expand Down Expand Up @@ -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");
});
});
});