Skip to content

fix(util): hand pacote a real npm so git plugins with a build step install - #62817

Open
JoshRob297 wants to merge 4 commits into
anomalyco:devfrom
JoshRob297:fix/git-dep-npm-bin
Open

JoshRob297 wants to merge 4 commits into
anomalyco:devfrom
JoshRob297:fix/git-dep-npm-bin

Conversation

@JoshRob297

@JoshRob297 JoshRob297 commented Nov 9, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #49704

Type of change

  • Bug fix

What does this PR do?

opencode plugin add github:... fails with git dep preparation failed for any Git dependency that builds itself in prepare / build.

pacote builds a Git dependency by running npm install inside the clone, and takes the binary from npmBin. When that name ends in .js, pacote/lib/util/npm.js spawns process.execPath with the script as its first argument. In the compiled binary npm_execpath is /$bunfs/bin/npm-cli.js, so the child process is opencode itself: it runs its own CLI, never that script, and nothing gets installed.

strace -f -e trace=execve over the command shows 23 git execs, zero npm execs, and one re-exec of the opencode binary with the npm-cli.js argument. No npm debug log is written, which fits npm never running.

The change gives pacote a real npm: resolveNpmBin() ignores an npm_execpath that lives inside the bundle or does not exist, and returns "npm", which pacote resolves through PATH. Arborist forwards the option. Machines without npm on PATH keep the current behaviour.

I did not build the compiled binary to test this end to end. The failure and the fix were reproduced at the pacote/arborist level, which is where the defect lives.

Related: #60201 skips the preparation instead. That removes the error but leaves packages that build in prepare installed without their built entry point — measured here: dist/ absent with ignoreScripts: true, present with a real npm.

How did you verify your code works?

  • bun test test/npm.test.ts in packages/core: 19 pass, including the new Npm.resolveNpmBin case.
  • bun run typecheck from the repository root: 33 tasks, no type errors.
  • Isolated with the pinned [email protected]: npmBin: "/$bunfs/bin/npm-cli.js" → git dep preparation failed; the same call with npmBin: "npm" gets past preparation.
  • Same failure from a clean Debian container on 1.19.5 with four public plugins: numman-ali/opencode-openai-codex-auth, griffinmartin/opencode-claude-auth, tickernelz/opencode-mem, slkiser/opencode-quota.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Added by the opencode bot (AI-generated).

Related: #59504, #60201

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes the reported failure when running from source. On the base, a git+file:// plugin with a prepare script fails with git dep preparation failed. With this PR it installs when npm is on PATH. Without npm on PATH it still fails with the same error. I did not test a compiled binary or Windows. test/npm.test.ts passes.

Public API: unchanged. Npm.resolveNpmBin is a new export of the internal @opencode/util package.

Concerns:

  1. Scripts now run despite ignoreScripts: true. Once a working npm is found, pacote's Git preparation runs npm install --force inside the clone without --ignore-scripts. That runs the plugin's own preinstall/install/postinstall/prepare scripts and its dependencies' install scripts. I confirmed it: a prepare script wrote a file into the installed plugin, even though Npm.reify passes ignoreScripts: true. Upstream pacote 22 goes the other way and skips this step when ignoreScripts is set, which is what #60201 backports. Whether Git plugins may run code during install is a policy decision for maintainers. If they may, it should be deliberate and visible, not a side effect of the binary lookup.
  2. Returning a .js path is unsafe (inline).
  3. The fallback stays silent. With no npm on PATH, or on Windows, where bare npm cannot be spawned without a shell (#59504), users still get the bare git dep preparation failed with no hint that npm is required.

Related: #59504, #60201

Comment thread packages/util/src/npm.ts Outdated
// into Bun's embedded filesystem, so the child was opencode itself — which runs its
// own CLI and never that script — and every Git plugin with a build step died with
// "git dep preparation failed". Only trust a path that exists outside the bundle.
const exec = env.npm_execpath

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pacote runs any npmBin ending in .js as process.execPath <npmBin> .... In the compiled binary, process.execPath is opencode itself. So when opencode is started from an npm run script, npm_execpath is a real, existing npm-cli.js, this branch returns it, and the binary re-executes itself exactly as before. A .js path is only safe when process.execPath is node. Always returning "npm" would avoid this. It would also make the $bunfs check, the existsSync injection and the new unit test unnecessary, shrinking the change to a single npmBin: "npm".

@JoshRob297

Copy link
Copy Markdown
Author

Typecheck, since the PR body only claimed the test run:

  • bun run typecheck from the repo root: 33 tasks successful, no type errors
  • bun check --no-script -b -p tsconfig.json in packages/util: no type errors in 68 files
  • bun test test/npm.test.ts in packages/core: 19 pass / 0 fail

Confirmed that github:numman-ali/opencode-openai-codex-auth, github:griffinmartin/opencode-claude-auth, github:tickernelz/opencode-mem and github:slkiser/opencode-quota all fail the same way on 1.19.5 from a clean container, so the report is not repo-specific.

@github-actions github-actions Bot removed the needs:compliance This means the issue will auto-close after 2 hours. label Nov 9, 2026
@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

@JoshRob297

JoshRob297 commented Nov 9, 2026 •

Copy link
Copy Markdown
Author

Thanks — all three are fair, and two are fixed in the branch.

1. Scripts run despite ignoreScripts: true. Correct, and I should have called it out instead of leaving it implicit. The preparation is the build: skip it and the package installs without the file its main points to — that is the measurement on #60201. Since a plugin is code that runs in this process anyway, its install script is not a new capability, but you are right that it should not arrive as a side effect of fixing the binary lookup. The branch now logs preparing a git plugin runs its install scripts before reify. If the maintainers prefer the other policy — never run install scripts for Git plugins and fail with "this plugin has to be installed prebuilt" — say so and I will switch it; the release tarball covers plugins that need building.

2. Returning a .js path is unsafe. It should not have been trusted unconditionally. resolveNpmBin now only returns one when process.execPath can actually run a script, decided by scriptCapable(): under Bun it checks Bun.embeddedFiles.length === 0, because process.versions.node is also set inside a compiled binary and is not a usable test. Otherwise it returns npm.

3. The fallback stays silent. Fixed the dead end: when the preparation fails and no npm is on PATH — or on Windows, where a bare npm needs a shell — the error now carries Git plugins are built with npm during install, and no npm was found on PATH. Install Node.js and npm, or use a release that ships the plugin already built. It does not fix #59504, but it stops the bare message.

test/npm.test.ts: 19 pass, including both runtime conditions. bun check clean in packages/util.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commits keep the fix working from source. With npm on PATH, a git+file:// plugin with a prepare script installs. Without npm, the install now fails with a clear hint instead of the bare git dep preparation failed. test/npm.test.ts passes and packages/util type-checks.

Public API: unchanged. npmOnPath and scriptCapable are new exports of the internal @opencode/util package.

The new check only partly fixes the self-re-run problem from my earlier comment: it still misses the Node build (inline). The new install-scripts warning only goes to the log file and fires for every Git plugin, so whether Git plugins may run install scripts still needs a maintainer's decision.

Comment thread packages/util/src/npm.ts Outdated
* too, compiled or not.
*/
export function scriptCapable() {
if (typeof Bun !== "undefined") return Bun.embeddedFiles.length === 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still misses the Node build. The release also ships a Node single-executable build (packages/cli/script/build-node.ts, the build-node job in release.yml). There Bun is undefined and process.versions.node is set, so this returns true. But process.execPath is the opencode binary, which runs its own entry point and ignores the script argument. So when npm_execpath is a real npm-cli.js (opencode started from npm run), the Git preparation re-executes opencode exactly as before. For Bun, embeddedFiles.length === 0 only works because the release build happens to embed files. It does not tell you whether this is a compiled binary. Always passing npmBin: "npm" avoids the whole question and makes scriptCapable and the .js branch unnecessary.

@JoshRob297

Copy link
Copy Markdown
Author

You're right about the Node build, and it makes the .js branch pointless rather than just incomplete.

release.yml ships both binaries from the same source, and in both of them process.execPath is opencode: in the Bun build because the bundle re-runs itself, in the Node single-executable build because the SEA entry point is the opencode binary and it ignores the script argument. So scriptCapable() was answering the wrong question — it detects "is this a script runtime", when what pacote needs is "will process.execPath <script> actually run the script". Under npm run on the Node build, npm_execpath is a real npm-cli.js, my check returned true, and the preparation re-executed opencode exactly as before. Your second review is correct.

So I took the suggestion in both of your comments: resolveNpmBin() now returns "npm" and nothing else.

  • scriptCapable() is gone, along with the $bunfs check, the injected existsSync and the unused node:fs import.
  • npmOnPath() stays, and it is what turns a missing npm into a readable error instead of the bare git dep preparation failed — that was point 3 of your first review.
  • The test now asserts the property that matters: resolveNpmBin() returns "npm" regardless of npm_execpath, so a compiled binary can never be handed a script path.

Verified on 0562260:

  • bun test test/npm.test.ts from packages/core: 19 pass, 0 fail
  • bun run typecheck in packages/core: no type errors in 2,014 files across 2 projects
  • bun run typecheck in packages/util: no type errors in 68 files

That leaves the policy question from your first review, which I'm not going to decide in a PR: with a working npm, pacote's Git preparation runs the plugin's install scripts even though Npm.reify passes ignoreScripts: true. The warning is now in the log, but whether Git plugins may run code at install time is the maintainers' call. If they say no, the fix is to stop installing such plugins rather than to keep the warning, and I'd rather that be explicit.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latest commit fixes both earlier points about the .js npm path. pacote now always gets the plain npm name, so neither the Bun binary nor the Node single-executable build can start opencode again in place of npm. The repro from source passes: a git+file:// plugin with a prepare script installs. test/npm.test.ts passes and packages/util type-checks.

Public API: unchanged. resolveNpmBin and npmOnPath are new exports of the internal @opencode/util package.

One small follow-up: the wrapper function and its test can be dropped (inline).

Comment thread packages/util/src/npm.ts
* every Git plugin with a build step. Handing pacote the bare name lets the system npm do
* the work, and `npmOnPath()` turns a missing npm into a readable error.
*/
export function resolveNpmBin() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that this always returns "npm", the exported function and its Npm.resolveNpmBin test add nothing; the test only checks a constant. Setting npmBin: "npm" directly in reify, with this explanation as a comment there, keeps the change minimal. The test can be dropped.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin: git dep with a build/prepare script fails — compiled binary can't run its own bundled npm-cli.js

2 participants