diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e287f7..c8b92ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ ### Changed -- Peer dependencies on `@earendil-works/pi-*` are `>=0.87.1` instead of `*`. 0.87.1 is the oldest pi the extension is verified against: typecheck and the full test suite pass with every `@earendil-works` package at 0.87.1. +- Declare the four `@earendil-works/pi-*` host packages and `typebox` as `*` peer dependencies supplied by Pi. 0.87.1 is the oldest previously tested host, not an enforced peer version constraint. +- Mark all host peers optional to prevent npm from automatically installing another copy of Pi. Move `typebox` from runtime dependencies to an exact 1.3.36 development dependency; `diff` is the sole runtime dependency. - CI: the Dependabot `bun.lock` refresh approves the CI run its own push starts, so the required checks attach to the pull request, and falls back to dispatching `ci.yml` if that run cannot be approved. - CI: the publish workflow no longer has an npm publish step; the package is distributed via git. A release now fails when its tag does not match `package.json`. diff --git a/README.md b/README.md index bdd8583..088e3bf 100644 --- a/README.md +++ b/README.md @@ -50,12 +50,17 @@ senpi install git:github.com/code-yeongyu/pi-apply-patch # 2. Manual placement git clone https://github.com/code-yeongyu/pi-apply-patch ~/.pi/agent/extensions/pi-apply-patch -cd ~/.pi/agent/extensions/pi-apply-patch && npm install +cd ~/.pi/agent/extensions/pi-apply-patch +npm install --omit=dev --legacy-peer-deps # 3. Dev / one-shot test pi -e /path/to/pi-apply-patch/src/index.ts ``` +Pi supplies `@earendil-works/pi-agent-core`, `@earendil-works/pi-ai`, `@earendil-works/pi-coding-agent`, `@earendil-works/pi-tui`, and `typebox` at runtime. These host modules are declared as optional peers with `*` ranges so npm does not install another copy of Pi. The extension installs only `diff` as a runtime dependency. + +0.87.1 is the oldest previously tested Pi host, not an enforced peer version constraint. The `*` ranges defer module provision to Pi; they do not guarantee compatibility with future breaking API changes. + After installation, restart pi or run `/reload` inside an interactive session. ## Development @@ -69,7 +74,7 @@ npm pack --dry-run pi -e ./src/index.ts ``` -Development and CI use Bun 1.4.2. `package-lock.json` is kept alongside `bun.lock` so `npm ci && npm test` keeps working for npm consumers. +Development and CI use Bun 1.4.2 and pinned SDK development dependencies: the four `@earendil-works/pi-*` packages at 1.0.4 and `typebox` at 1.3.36. `package-lock.json` is kept alongside `bun.lock` so `npm ci && npm test` keeps working for npm consumers. The test suite uses vitest. TypeScript is strict, Node-only, and uses ESM imports with `.js` suffixes. diff --git a/bun.lock b/bun.lock index adc507f..8e0d2c0 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,6 @@ "name": "pi-apply-patch", "dependencies": { "diff": "^9.0.0", - "typebox": "^1.3.36", }, "devDependencies": { "@biomejs/biome": "2.5.15", @@ -16,15 +15,24 @@ "@earendil-works/pi-tui": "1.0.4", "@types/node": "26.6.4", "@typescript/native-preview": "7.0.0-dev.20260707.2", + "typebox": "1.3.36", "typescript": "7.0.2", "vitest": "5.0.3", }, "peerDependencies": { - "@earendil-works/pi-agent-core": ">=0.87.1", - "@earendil-works/pi-ai": ">=0.87.1", - "@earendil-works/pi-coding-agent": ">=0.87.1", - "@earendil-works/pi-tui": ">=0.87.1", + "@earendil-works/pi-agent-core": "*", + "@earendil-works/pi-ai": "*", + "@earendil-works/pi-coding-agent": "*", + "@earendil-works/pi-tui": "*", + "typebox": "*", }, + "optionalPeers": [ + "@earendil-works/pi-agent-core", + "@earendil-works/pi-ai", + "@earendil-works/pi-coding-agent", + "@earendil-works/pi-tui", + "typebox", + ], }, }, "packages": { diff --git a/package-lock.json b/package-lock.json index 7571e47..1190ab1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,7 @@ "version": "0.1.4", "license": "MIT", "dependencies": { - "diff": "^9.0.0", - "typebox": "^1.3.36" + "diff": "^9.0.0" }, "devDependencies": { "@biomejs/biome": "2.5.15", @@ -20,6 +19,7 @@ "@earendil-works/pi-tui": "1.0.4", "@types/node": "26.6.4", "@typescript/native-preview": "7.0.0-dev.20260707.2", + "typebox": "1.3.36", "typescript": "7.0.2", "vitest": "5.0.3" }, @@ -27,10 +27,28 @@ "node": ">=22.19.0" }, "peerDependencies": { - "@earendil-works/pi-agent-core": ">=0.87.1", - "@earendil-works/pi-ai": ">=0.87.1", - "@earendil-works/pi-coding-agent": ">=0.87.1", - "@earendil-works/pi-tui": ">=0.87.1" + "@earendil-works/pi-agent-core": "*", + "@earendil-works/pi-ai": "*", + "@earendil-works/pi-coding-agent": "*", + "@earendil-works/pi-tui": "*", + "typebox": "*" + }, + "peerDependenciesMeta": { + "@earendil-works/pi-agent-core": { + "optional": true + }, + "@earendil-works/pi-ai": { + "optional": true + }, + "@earendil-works/pi-coding-agent": { + "optional": true + }, + "@earendil-works/pi-tui": { + "optional": true + }, + "typebox": { + "optional": true + } } }, "node_modules/@anthropic-ai/sdk": { @@ -3685,6 +3703,7 @@ "version": "1.3.36", "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.36.tgz", "integrity": "sha512-bu2Ec2Ti9B4IFSFj1FcZbtLNB/W7igQwxocMGJLAG2NTv1nI+ImtIA34YgHW4CVyGVlJqNXR1GJyLmdWgV3DuA==", + "dev": true, "license": "MIT" }, "node_modules/typescript": { diff --git a/package.json b/package.json index 861d9d4..7ca6f52 100644 --- a/package.json +++ b/package.json @@ -43,10 +43,28 @@ "check": "tsgo --noEmit && biome check ." }, "peerDependencies": { - "@earendil-works/pi-agent-core": ">=0.87.1", - "@earendil-works/pi-ai": ">=0.87.1", - "@earendil-works/pi-coding-agent": ">=0.87.1", - "@earendil-works/pi-tui": ">=0.87.1" + "@earendil-works/pi-agent-core": "*", + "@earendil-works/pi-ai": "*", + "@earendil-works/pi-coding-agent": "*", + "@earendil-works/pi-tui": "*", + "typebox": "*" + }, + "peerDependenciesMeta": { + "@earendil-works/pi-agent-core": { + "optional": true + }, + "@earendil-works/pi-ai": { + "optional": true + }, + "@earendil-works/pi-coding-agent": { + "optional": true + }, + "@earendil-works/pi-tui": { + "optional": true + }, + "typebox": { + "optional": true + } }, "devDependencies": { "@biomejs/biome": "2.5.15", @@ -56,6 +74,7 @@ "@earendil-works/pi-tui": "1.0.4", "@types/node": "26.6.4", "@typescript/native-preview": "7.0.0-dev.20260707.2", + "typebox": "1.3.36", "typescript": "7.0.2", "vitest": "5.0.3" }, @@ -63,7 +82,6 @@ "node": ">=22.19.0" }, "dependencies": { - "diff": "^9.0.0", - "typebox": "^1.3.36" + "diff": "^9.0.0" } } diff --git a/test/package-manifest.test.ts b/test/package-manifest.test.ts new file mode 100644 index 0000000..d27a8b9 --- /dev/null +++ b/test/package-manifest.test.ts @@ -0,0 +1,83 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +type DependencyMap = Record; +type BundledDependencies = string[] | boolean; +interface PackageManifest { + dependencies?: DependencyMap; + optionalDependencies?: DependencyMap; + devDependencies?: DependencyMap; + peerDependencies?: DependencyMap; + peerDependenciesMeta?: Record; + bundledDependencies?: BundledDependencies; + bundleDependencies?: BundledDependencies; +} + +const manifest: PackageManifest = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")); +const lockfile: { packages: Record } = JSON.parse( + readFileSync(new URL("../package-lock.json", import.meta.url), "utf8"), +); +const hostPackages = [ + "@earendil-works/pi-agent-core", + "@earendil-works/pi-ai", + "@earendil-works/pi-coding-agent", + "@earendil-works/pi-tui", + "typebox", +]; +const dependencySections = [ + "dependencies", + "optionalDependencies", + "devDependencies", + "peerDependencies", + "peerDependenciesMeta", + "bundledDependencies", + "bundleDependencies", +] as const; + +function bundledPackages(bundle: BundledDependencies | undefined): string[] { + if (bundle === true) { + return Object.keys({ ...manifest.dependencies, ...manifest.optionalDependencies }); + } + return Array.isArray(bundle) ? bundle : []; +} + +describe("package manifest host dependencies", () => { + it.each(hostPackages)("#given host package %s #when declaring peers #then its range is *", (hostPackage) => { + expect(manifest.peerDependencies?.[hostPackage]).toBe("*"); + }); + + it.each(hostPackages)("#given host package %s #when npm resolves peers #then it is optional", (hostPackage) => { + expect(manifest.peerDependenciesMeta?.[hostPackage]?.optional).toBe(true); + }); + + it.each(hostPackages)( + "#given host package %s #when installing production dependencies #then it is absent from runtime sections and bundles", + (hostPackage) => { + expect(manifest.dependencies ?? {}).not.toHaveProperty(hostPackage); + expect(manifest.optionalDependencies ?? {}).not.toHaveProperty(hostPackage); + expect(bundledPackages(manifest.bundledDependencies)).not.toContain(hostPackage); + expect(bundledPackages(manifest.bundleDependencies)).not.toContain(hostPackage); + }, + ); + + it.each(hostPackages)( + "#given host package %s #when developing standalone #then it is a development dependency", + (hostPackage) => { + expect(manifest.devDependencies?.[hostPackage]).toEqual(expect.any(String)); + expect(manifest.devDependencies?.[hostPackage]).not.toBe(""); + }, + ); +}); + +describe("package-lock root dependency declarations", () => { + it("#given the npm lockfile #when reading root metadata #then the package entry exists", () => { + expect(lockfile.packages[""]).toBeDefined(); + }); + + it.each(dependencySections)( + "#given the real manifest #when comparing lockfile root %s #then the declarations match", + (section) => { + expect(lockfile.packages[""]?.[section]).toEqual(manifest[section]); + }, + ); +});