feat(plugins): install imported pi extension dependencies - #277
feat(plugins): install imported pi extension dependencies#277muzimu217 wants to merge 4 commits into
Conversation
Importing a pi CLI extension stripped node_modules and copied only the sources, so any extension with npm dependencies failed to load (jiti reported "Cannot find module ..." and the whole extension was skipped). The generated plugin now keeps the source package.json (plus lockfile) at the plugin root and installs declared dependencies there before the first load: npm install --omit=dev --legacy-peer-deps --no-audit --no-fund --ignore-scripts. Kernel packages keep resolving through virtual modules, and --ignore-scripts means no third-party install script ever runs; a native module that needs one reports its own load error. A failed install never blocks the import — the renderer shows a warning toast with the npm stderr tail. Verified with pi-hermes-memory: the imported plugin loads in the sidecar runtime with its tools, slash commands, and turn hooks registered (previously a load_error).
Pair spec 07-plugins/16 §3.2 with the new import flow (package.json kept at the plugin root, workspaces stripped, pinned npm flags, failure never blocking), add the E2E-PLUGIN-import-extension-* scenarios to the test plan in both locales, spell out the dependency behavior and the Electron-headers rebuild workaround in the plugin development guide, and narrow the README claim to disclose the npm step.
Linux pipes deliver the flood in smaller chunks, so the buffer can sit between the keep size and the 2x trim threshold when the child exits. Assert the actual invariant of the rolling cap (bounded by 2x the keep size after every chunk) instead of the single-chunk macOS result.
vastsa
left a comment
There was a problem hiding this comment.
Request changes: the direction is sound, but I found two correctness issues that should be fixed before merge.
-
Blocking import failure for the exact npm-installed path from issue #242. In apps/desktop/electron/main/agent-extensions.ts:399, the directory copy filters with
p.includes("node_modules"). When the selected source itself is under~/.pi/agent/npm/node_modules/<package>(the path described in #242), the filter rejects the source root, so no extension files are copied. The generated manifest still points atsrc/..., but the entry is missing and the extension does not register. I reproduced this with a temporarynode_modules/pi-maestro-flowsource:copiedEntry: false, while the generated package.json was present. Filter only descendant node_modules segments relative to the selected root, and add a regression test whose source directory is itself nested under node_modules. -
Valid JSON can still abort the import. In apps/desktop/electron/main/agent-extensions.ts:336, JSON.parse() is caught, but a valid
package.jsoncontainingnullmakesmanifest.dependenciesthrow a TypeError. Because IPC awaits this at agent-extensions-ipc.ts:80 before loadDevPlugin(), the documented “failed install never blocks the import” behavior is violated. Validate that the parsed manifest is a non-null object (or keep the access inside the guarded path) and add a regression test.
The current required GitHub checks are green for JS and Rust; the Vercel status is red only because deployment authorization is required. I did not run E2E locally because this review request did not authorize it and the repository checklist requires an explicit request. The PR description’s reported E2E result therefore remains independently unverified here.
There is also a documentation completeness follow-up: the two new E2E scenario IDs are not added to the traceability matrix in docs/spec/06-delivery/04-e2e-test-plan.md.
Two review findings:
- The directory copy filter tested the absolute path with
includes("node_modules"), so a source selected inside the
~/.pi/agent/npm/node_modules/<package> layout (the exact path from
issue vastsa#242) copied nothing and the extension never registered. The
filter now excludes only node_modules segments below the selected
root.
- A package.json containing the valid JSON value null threw on the
dependencies access after parsing succeeded, aborting the import and
violating the never-blocks guarantee; any non-object JSON is now
reported as a failed install instead.
Also adds the two new E2E scenario IDs to the traceability matrix in
both locales.
|
Thanks for the fast, precise review — both findings reproduced locally before fixing, exactly as described. 1. npm-layout source (#242) copied nothing — reproduced ( Regression test added ( End-to-end proof through the full new flow with the exact #242 shape (source selected from inside a 2. Valid-JSON 3. Traceability matrix — both new scenario IDs added to the "Trusted extensions (R7 v1)" row in en and zh (docs:check still verifies 77 pairs). Local validation on af098fe: |
|
Superseded by #288 — @Blue-Berrys covers the node_modules filter fix and extends further into skill import, so keeping one PR is cleaner. Our unique part here (the dependencies-install flow for imported extensions, plus the malformed-manifest handling and its regression tests) has been offered over on #288 for whoever wants to fold it in; the branch stays available at Thanks for the precise review @vastsa — closing this to keep the queue tidy. (=^・ω・^=) |
Closes #242. Related: #183.
Problem
Importing a pi CLI extension (Plugins → Import pi extension) copies the extension sources without
node_modules, and the sidecar loader virtualizes only the kernel packages (pi-ai,pi-agent-core,pi-coding-agentshim,pi-tuistub, typebox). Any bare import of an npm dependency fails at load, the whole extension is reportederror, and none of its tools, commands, or hooks register. Reproduced withpi-hermes-memory:Change
generateImportedExtensionPluginkeeps the sourcepackage.json(+ lockfile) at the generated plugin root, stripping anyworkspacesfield so npm never enters the copied sources.New
installExtensionDependenciesruns, when the manifest declaresdependencies, before the plugin's first load:--legacy-peer-depskeepspi-coding-agent-style peers out of the tree; disk-installed kernel packages lose to the runtime's virtual modules (verified), so installing them is harmless.--ignore-scriptsmeans no third-party install script ever runs. Modernbetter-sqlite3ships prebuilds and works anyway; a native module that needs a build step fails to load with a diagnostic (workaround:npx @electron/rebuild -v <version>in the plugin dir — verified it recovers an ABI-broken case).A failed install never blocks the import: the renderer shows a warning toast with the npm stderr tail, the plugin registers, and the extension reports its own
load_errordiagnostic.The import confirm dialog now discloses the npm step (network egress; scripts disabled).
Security notes
--ignore-scriptsremoves third-party install-script execution from the import path entirely.workspacesstripping prevents npm from treating the copied source tree as a workspace.agent.extensiongating and the diagnostics surface are unchanged.Validation
Gates:
pnpm build:js, desktoptypecheck,pnpm lint,pnpm -r test(1455 passing),pnpm docs:check(77 en/zh pairs) — all green.E2E:
pnpm test:e2e→ 18/18 passed (2 skipped: live-model suites requiringPI_DESKTOP_TEST_API_KEY, unrelated to this surface).New unit tests cover: package.json/lockfile placement,
workspacesstripping, never copyingnode_modules, pinned npm flags and cwd, failure/skip/invalid-manifest paths, stderr cap, and timeout kill.New E2E scenario docs (en/zh paired):
E2E-PLUGIN-import-extension-installs-dependencies,E2E-PLUGIN-import-extension-reports-missing-dependency.End-to-end proof with
pi-hermes-memory(fresh clone, no node_modules) imported through the new flow and loaded through the same esbuild sidecar bundle as E2E-245, under both system Node and the Electron runtime:Spec & docs
07-plugins/16-trusted-extensions.md§3.2 (en/zh): the import flow's dependency semantics.06-delivery/04-e2e-test-plan.md(en/zh): two new scenarios.docs/plugin-development.md§6.11 (en/zh): dependency behavior + native-module workaround.Known limits / follow-ups
ctx.sessionManageris a v1 stub (spec §5, deferred to v2), so hermes-style session indexing stays limited — that part needs the v2 read shim.uncaughtException(sidecar crash) instead of a cleanload_errordiagnostic; happy to follow up with a small hardening PR.