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
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
"openpgp": "6.3.2",
"react": "19.3.0",
"react-dom": "19.3.0",
"typebox": "1.3.6",
"web-ui-primitives": "1.1.4",
"wire-mesh-core": "4.0.6",
"ws": "8.21.1",
Expand Down Expand Up @@ -70,6 +69,7 @@
"semantic-release": "25.0.7",
"tsx": "^4.23.1",
"turbo": "2.11.5",
"typebox": "1.3.6",
"typescript": "^6.0.3",
"typescript-eslint": "8.70.0",
"vite": "8.3.2",
Expand Down Expand Up @@ -113,7 +113,8 @@
"packageManager": "pnpm@12.4.1+sha512.2e81e399d73fe8390dab25e06aa788ab7a5908248d2f5a370f82b481147a6a7a367bf8048f9a6fdb6460f21a66f0542dedb8b94ca2c8723596741920b1656d4c",
"peerDependencies": {
"@earendil-works/pi-ai": ">=0.79.0",
"@earendil-works/pi-coding-agent": ">=0.79.0"
"@earendil-works/pi-coding-agent": ">=0.79.0",
"typebox": "*"
},
"pi": {
"extensions": [
Expand Down
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions src/test/pi-manifest-packed.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
* Every extension the `pi` manifest in package.json names must ship in the published package. `pi install npm:agent-comms` reported success and then could not load the extension when the manifest pointed at `src/`, which `files` did not publish (ExaDev/agent-comms#412, #440); nothing in the suite noticed, because the manifest and the tarball were never compared.
*
* The tarball contents come from `npm pack --dry-run`, the same file selection a publish uses, so a `files` entry or an ignore rule that drops the manifest's path fails here and not in a user's install.
*
* The same manifest declares `typebox`, which the pi bridge imports directly. Pi supplies that package to extensions and maps the specifier to its own copy, so declaring it in `dependencies` installs a second physical copy that shadows the mapping and makes Pi warn on every start (ExaDev/agent-comms#455).
*/
import { spawnSync } from "node:child_process";
import * as fs from "node:fs";
Expand All @@ -10,6 +12,16 @@ import { describe, expect, it } from "vitest";

const packageRoot = path.resolve(import.meta.dirname, "../..");

// Packages Pi supplies to extensions, from the host-provided list in Pi's package docs.
const HOST_PROVIDED_PACKAGES = [
"@earendil-works/pi-agent-core",
"@earendil-works/pi-ai",
"@earendil-works/pi-coding-agent",
"@earendil-works/pi-tui",
"@sinclair/typebox",
"typebox",
];

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
Expand Down Expand Up @@ -81,4 +93,24 @@ describe("the pi manifest", () => {
expect(packed.has(path.posix.normalize(extension)), extension).toBe(true);
}
});

it("declares host-provided packages as wildcard peers, never dependencies", () => {
const manifest = readJson(path.join(packageRoot, "package.json"));
if (!isRecord(manifest)) {
throw new Error("package.json is not an object");
}
const dependencies = isRecord(manifest.dependencies)
? Object.keys(manifest.dependencies)
: [];
const peers = isRecord(manifest.peerDependencies)
? manifest.peerDependencies
: {};
for (const name of HOST_PROVIDED_PACKAGES) {
expect(dependencies, `${name} must not be a dependency`).not.toContain(
name,
);
}
// The pi bridge imports `typebox`, so it must be declared as a peer, and `"*"` lets Pi's own copy satisfy it.
expect(peers.typebox).toBe("*");
});
});