npmDependencies in the manifest, installed once for every plugin

replaces the per-plugin package.json from earlier tonight. the owner's call,
and the reason is one i had not weighed: sharp is ~50 MB of platform binaries
and the obvious dependency for anything touching images, so a second and third
plugin wanting it would have meant a second and third copy with no version they
could converge on.

so every plugin's `npmDependencies` merge into ONE generated plugins/package.json,
installed into plugins/node_modules/. bun resolves node_modules up the tree, so
that directory serves every plugin and none of the platform's own code.

ranges intersect rather than alternate. two plugins asking for ^0.33.5 and
^0.33.9 produce "^0.33.5 ^0.33.9", which npm and bun read as AND — the resolved
version must satisfy both. `||` would have meant "either", which is the silent
version of this failure: one plugin quietly getting a version the other cannot
import. incompatible requirements now fail the install with bun's own message,
and both askers are named in the steps either way.

generated from the plugins ON DISK, like generatePluginSchemas, and never
edited — so a package cannot appear there without a plugin having declared it.

what this gives up, knowingly: a committed lockfile. ranges are the whole point
and a range resolves per machine, so plugin packages are resolved at install
time like osDependencies are, and unlike the platform's own. CLAUDE.md is
emphatic about frozen lockfiles and 2026-08-04, so it is named in the manifest
type rather than left to be discovered.

the generated plugins/package.json and lockfile are gitignored — /plugins/*/
only ignores directories, so files at that level needed naming explicitly.
This commit is contained in:
2026-08-20 18:22:58 +00:00
parent 3450f5eb57
commit 21bedf1418
6 changed files with 242 additions and 166 deletions
+19
View File
@@ -244,6 +244,25 @@ export type PluginManifest = {
* that runs and quietly produces nothing.
*/
osDependencies?: OsDependency[];
/**
* npm packages this plugin imports. Absent means it needs none, which is every plugin but Atelier.
*
* Ranges, not pins, and deliberately: every plugin's declarations are merged into ONE shared install at
* `plugins/node_modules/`, so two plugins asking for `^0.33.5` and `^0.33.9` share one copy rather than
* carrying 50 MB of the same platform binaries twice. Where ranges differ they are intersected — both
* plugins have to work — so incompatible requirements fail the install loudly instead of handing one
* plugin a version the other cannot use.
*
* This is the one place in the codebase where a dependency is resolved at install time rather than from
* a committed lockfile. That is a real cost against the rule `CLAUDE.md` states, taken knowingly: the
* alternative is a lockfile per plugin and no sharing at all. What it buys back is that every package a
* plugin can pull is DECLARED here, visible before install, and the merged file is regenerated from
* these manifests rather than edited — so nothing can appear in it without a plugin having asked.
*
* See `npm-deps.ts`.
*/
npmDependencies?: Record<string, string>;
};
/** What the platform knows about a plugin on disk: its manifest, plus everything the tree said. */