← reports

#2548 · bb plugin new discards npm's failure output

Bug Priority: Low Effort: unassigned cli plugins open on GitHub 2026-08-27 · base ad79bbb5ec909524f8f281e62d860c588a86f332

Verdict: REPRODUCED · Root-cause confidence: high

1. TL;DR

bb plugin new creates a plugin even when its automatic npm install fails. The command then shows only a general warning.

The rejected execFile call contains npm's stderr. However, the catch block does not read the error object.

A test with an EPERM failure reproduced the exact output. PR #2550 fixes the simple case, but noisy stdout can still remove stderr.

2. Claims vs findings

Claim from the issueStatusEvidence
Any npm failure can produce only the general warning. Verified A fake npm process wrote EPERM details to stderr and returned status 1. The CLI did not show those details.
The actual output contains only Could not run npm install. Verified The regression test received the exact general warning. It did not receive npm error code EPERM.
The catch block discards the cause. Verified The base code uses catch { and prints a fixed string.
The existing test covers only a missing npm executable. Verified The existing test replaces PATH with an empty directory.
The npm registry warning uses a separate path. Verified The command checks the registry before it starts the install.

3. Environment

4. Minimal reproduction

The patch adds one fake npm mode and one CLI regression test.

  1. Check out the base commit.
  2. Save the repro patch as issue-2548-repro.patch in the repository root.
  3. Check and apply the patch.
  4. Install the private dependency copy.
  5. Run the one CLI test file.
git checkout --detach ad79bbb5ec909524f8f281e62d860c588a86f332
git apply --check issue-2548-repro.patch
git apply issue-2548-repro.patch
pnpm install --frozen-lockfile --prefer-offline --package-import-method=copy
pnpm exec turbo run test --filter=@bb/cli -- --run src/__tests__/plugin-new.test.ts

Expected: The warning includes npm error code EPERM and the root-owned cache message.

Actual:

FAIL |@bb/cli:isolated| src/__tests__/plugin-new.test.ts
AssertionError: expected 'Could not run npm install — run it in…'
to contain 'npm error code EPERM'

Expected: "npm error code EPERM"
Received: "Could not run npm install — run it in the plugin directory before `bb plugin build`."

Test Files  1 failed (1)
Tests  1 failed | 34 passed (35)

Repro files:

5. Root cause

installScaffoldDependencies uses the promise form of execFile. A failed child process rejects the promise with its output data.

The test process wrote the EPERM diagnosis to stderr before it returned status 1. The rejection therefore had the required diagnosis.

Lines 493–504 catch the rejection without a cause variable.

The code then prints a fixed warning and returns false. The caller adds the manual npm command but cannot add the lost reason.

Lines 1335–1341 show that final path.

Commit 4c86eceb8a added the fixed warning on 2026-08-11. No later commit on origin/main changes this path.

6. Proposed fix (first principles)

Catch the rejection cause. Narrow it as an object at the child-process boundary.

Read a string stderr value first. Use stdout only when stderr has no text.

Show a small tail below the current warning. Keep the current result when the process has no output.

Add tests for stderr only, stdout only, both streams, and no streams. The both-stream test must keep stderr after long stdout.

This change affects only CLI text. It does not require a host daemon protocol version change.

7. PR review

PR #2550 — REQUEST CHANGES

The PR catches the cause and shows the last eight lines from joined stderr and stdout. Its normal EPERM test passes.

Major: The global tail can remove stderr when stdout has eight or more later lines.

Lines 496–507 put stderr before stdout. The code then keeps only the final eight joined lines.

The hostile test wrote two stderr lines and nine stdout lines. The warning showed stdout lines 2–9 and omitted npm error code EPERM.

Low: Line 497 uses an unchecked type cast for the caught value. A small object and property check can narrow this boundary safely.

Fix: Give stderr its own line budget. Use stdout only as a fallback, or keep separate labeled tails for both streams.

Tests run on PR head 0ee3c8b:

The revised hostile patch passes git apply --check on PR head.

Artifacts: normal test, hostile patch, and hostile failure.

8. Related issues

9. Appendix

Commands

gh issue view 2548 --comments
pnpm install --frozen-lockfile --prefer-offline --package-import-method=copy
pnpm exec turbo run build
git apply --check issue-2548-repro.patch
git blame -L 488,505 -- apps/cli/src/commands/plugin.ts
git log ad79bbb5ec90..origin/main -- apps/cli/src/commands/plugin.ts apps/cli/src/__tests__/plugin-new.test.ts
gh pr view 2550
gh pr diff 2550
gh pr checkout 2550
git apply --check pr2550-noisy-stdout.patch
pnpm exec turbo run typecheck --filter=@bb/cli --filter=@bb/templates --filter=@bb/server
pnpm exec turbo run test --filter=@bb/cli -- --run src/__tests__/plugin-new.test.ts src/__tests__/plugin-scaffold-dependencies.test.ts
pnpm exec turbo run test --filter=@bb/server -- --run test/services/plugins/plugin-authoring-docs.test.ts test/services/plugins/plugin-authoring-doc-examples.test.ts
pnpm exec turbo run test --filter=@bb/templates

Test setup note

The dev launcher first selected a Node 22 native binary. The first Turbo test stopped before it ran any test.

I stopped the private instance. I then repeated the private-copy install with --force under Node 24.

A direct memory database check then confirmed the ABI 137 binary. All stated tests ran after that check.

Verification

The verifier found that both reproduction patches had invalid unified diff headers. The verifier then made the edits manually and confirmed both failures.

This revision replaces both files with complete git diff output. Each patch now passes git apply --check on its stated commit.

I ran the published base test again. It failed once and passed 34 tests, with the EPERM text absent.

I ran the PR test again without the hostile patch. All 35 tests passed.

I then applied the hostile patch and ran the same test command. It failed once and passed 35 tests.

The failure output contains stdout lines 2 through 9. It does not contain the two stderr diagnosis lines.