diff --git a/package.json b/package.json index d3ada198..8089d8e5 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", @@ -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": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc169bb5..963af2f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -221,9 +221,6 @@ importers: react-dom: specifier: 19.3.0 version: 19.3.0(react@19.3.0) - typebox: - specifier: 1.3.6 - version: 1.3.6 web-ui-primitives: specifier: 1.1.4 version: 1.1.4(@mantine/core@9.6.1(@mantine/hooks@9.6.1(react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0) @@ -363,6 +360,9 @@ importers: turbo: specifier: 2.11.5 version: 2.11.5 + typebox: + specifier: 1.3.6 + version: 1.3.6 typescript: specifier: ^6.0.3 version: 6.0.3 diff --git a/src/test/pi-manifest-packed.integration.test.ts b/src/test/pi-manifest-packed.integration.test.ts index cf3233fe..832c57a7 100644 --- a/src/test/pi-manifest-packed.integration.test.ts +++ b/src/test/pi-manifest-packed.integration.test.ts @@ -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"; @@ -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 { return typeof value === "object" && value !== null && !Array.isArray(value); } @@ -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("*"); + }); });