Skip to content

fix: restore npm install -g comisai by keeping bundled copies inert#350

Merged
anconina merged 2 commits into
mainfrom
fix/global-install-bundled-metadeps
Jul 26, 2026
Merged

fix: restore npm install -g comisai by keeping bundled copies inert#350
anconina merged 2 commits into
mainfrom
fix/global-install-bundled-metadeps

Conversation

@anconina

@anconina anconina commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Description

Every global install of v1.0.55 dies before building a single package, so curl https://comis.ai/install.sh | bash fails on a clean box:

npm error code ENOENT
npm error syscall spawn sh
npm error path .../comisai/node_modules/@google/genai

spawn sh ENOENT is npm's report for a missing working directory, not a PATH problem.

Root cause. npm counts a dependency of a bundled package as part of that bundle: arborist's Node#getBundler() walks edgesIn and claims the dep as soon as any dependent is bundled and it resolves into the bundling package's own node_modules/. It then skips unpacking it — expecting the tarball to carry it — while still scheduling its lifecycle scripts. @google/genai ships a preinstall, so it ran in a directory that was never created.

This is global-only: a global install nests every comisai dependency under comisai/node_modules/ (the bundler's own directory); a local install hoists them to the project root, outside it. npm install comisai succeeds, which is why nothing caught it.

prepack already stripped dependencies from the @comis/* copies for exactly this reason. When @earendil-works/pi-ai was added to bundledDependencies, its registry manifest was copied verbatim — and 7 of its 11 dependencies were not umbrella dependencies at all, so nothing else installed them.

Second regression. @hono/node-server was bumped 1.19.14 → 2.0.11 while @hono/node-ws@1.3.1 peers on ^1.19.11 (upstream has published no release accepting 2.x). pnpm only warns; npm fails with ERESOLVE — which broke repair_comisai_bundled_deps, the pass that repairs npm's separate, long-standing pruning of a non-bundled direct dep's transitive deps (the @earendil-works/pi-tui class). Reverted to 1.19.15.

Changes

  • bundled-manifest.js (new) — carries the invariant: a bundled copy declares no dependencies, and everything it imports is a top-level umbrella dependency. prepack Step 0 fails the pack when that's violated.
  • prepack.js — every bundled copy's manifest now goes through serializeBundledManifest, including the third-party ones.
  • packages/comis/package.json — hoisted the 7 unprovided pi-ai dependencies; hono revert.
  • install.sh — stop capturing npm with --silent. All six diagnostics fields are parsed from that log, so silencing it turned a failed install into a bare "install failed" with the cause reachable only from npm's own debug log — a path the installer could no longer extract. Adds a fallback naming $(npm config get cache)/_logs.

Related Issue

Fixes #351

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • Refactor

Checklist

  • Targeted checks for the changed area pass
  • Full repository validation passes (pnpm validate)
  • New or changed behavior has a test that demonstrated the RED state before the production change
  • Documentation updated (if applicable) — CLAUDE.md supply-chain invariants
  • No secrets or credentials committed
  • Security implications considered
  • The change is focused on one concern and links the related issue

RED Test Proof

All five assertions fail on the test-first commit 04f8d1063, before the production patch in 8e42dddd3:

 × hoists every runtime dependency of a bundled package to the umbrella 4ms
 × routes every bundled copy in prepack through the shared strip helper 1ms
 × pins versions that satisfy every declared peer we also pin 24ms
 × does not silence the npm output it later parses for diagnostics 13ms
 × names a place to look when npm wrote no output at all 24ms

AssertionError: A bundled package depends on packages the umbrella does not provide.
Found 1 violation(s):
    bundled @earendil-works/pi-ai needs 7 package(s) the umbrella does not provide:
    @anthropic-ai/sdk, @aws-sdk/client-bedrock-runtime, @mistralai/mistralai,
    @smithy/node-http-handler, http-proxy-agent, https-proxy-agent, partial-json

AssertionError: prepack.js can leave a bundled copy's dependency list intact.
Found 2 violation(s):
    does not import ./bundled-manifest.js — a hand-rolled copy re-opens the defect
    calls serializeBundledManifest 0 time(s); both the workspace copies and the
    third-party copies must be stripped

AssertionError: A pinned version violates a dependency's peer range; npm refuses to install it.
Found 2 violation(s):
    @hono/node-ws@1.3.1 requires peer @hono/node-server@"^1.19.11", but this
    manifest pins @hono/node-server@2.0.11 — npm install fails with ERESOLVE

AssertionError: an empty installer log must still point at npm's own debug log
directory — otherwise the failure leaves no evidence anywhere:
expected 'WARN: npm install failed for comisai@…' to match /_logs/

 Test Files  3 failed (3)
      Tests  5 failed | 2 passed (7)

All 5 pass on 8e42dddd3.

Evidence and Residual Risk

  • Contribution path: workload — first-install path for every new deployment.
  • Evidence level: live integration run. Packed the tarball, installed it globally as the dedicated service user on a clean Ubuntu prefix through the real install.sh, and confirmed: install completes (previously died at @google/genai); all 11 pi-ai dependencies resolve from the bundled copy via createRequire; repair_comisai_bundled_deps succeeds (previously ERESOLVE); comis --version returns; the daemon boots with 0 errors and the gateway listens. Also confirmed v1.0.54 exhibits the identical pi-tui prune, establishing that part as pre-existing rather than a regression here.
  • Tested profile: Ubuntu 24.04 (aws), Node 22.23.1, npm 10.9.8, dedicated comis system user, systemd service, fresh global prefix (rm -rf between runs); pnpm validate on macOS.
  • Residual risk: pnpm validate:full and a Docker build have not been run on Linux. No version bump — v1.0.55 stays broken on npm until a v1.0.56 ships. The new guards are static; the real gap that let both regressions through is that no gate anywhere runs npm install -g <tarball> and loads the CLI, and that tier still does not exist. The hono revert pins a dependency backwards, so a future @hono/node-server 2.x adoption is blocked until upstream publishes a @hono/node-ws that accepts it.

comis-agent added 2 commits July 26, 2026 09:25
Adds the failing guards first; all five assertions fail on this commit.

1. umbrella-bundled-metadeps — npm counts a dependency of a bundled package
   as part of that bundle whenever it resolves into the bundling package's
   own node_modules/ (arborist Node#getBundler follows edgesIn). A GLOBAL
   install nests every comisai dependency exactly there, so npm skips
   unpacking those packages while still scheduling their lifecycle scripts;
   a script whose package was never unpacked runs with a missing cwd, which
   npm reports as the opaque `spawn sh ENOENT`. A local install hoists the
   same packages to the project root, outside the bundler, and succeeds --
   so neither `pnpm validate`, nor `npm install comisai`, nor the tarball
   smoke (which packs but never installs) can see this class.

2. npm-peer-consistency — pnpm only warns on an unsatisfied peer, so the
   workspace stays green while `npm install` fails the same conflict with
   ERESOLVE. That breaks the installer's repair pass, which is a plain
   `npm install` inside the installed package.

3. installer-npm-failure-evidence — the installer captured npm with
   --silent, emptying the very log its diagnostics parse for the error
   code, syscall, errno, npm debug-log path and the "showing last log
   lines" tail. The operator got a bare failure with no evidence, and the
   temp log is deleted at exit.

bundled-manifest.js carries the invariant the first two guards assert
against: a bundled copy declares no dependencies, and everything it
imports is a top-level umbrella dependency.
Every global install of the umbrella package died before building a
single package:

  npm error code ENOENT
  npm error syscall spawn sh
  npm error path .../comisai/node_modules/@google/genai

`@earendil-works/pi-ai` is bundled, and its manifest was copied into the
tarball verbatim. npm therefore claimed all eleven of its dependencies
for that bundle, skipped unpacking them, and still scheduled their
lifecycle scripts -- and `@google/genai` ships a preinstall. The script
ran in a directory that was never created, which npm reports as
`spawn sh ENOENT`. Seven of those eleven were not umbrella dependencies
at all, so they had nothing else installing them.

prepack already stripped dependencies from the @comis/* copies for
exactly this reason; the third-party path never got the same treatment.
Route every bundled copy through serializeBundledManifest, hoist all of
pi-ai's runtime dependencies to the umbrella, and fail the pack (Step 0)
when a bundled package needs something the umbrella does not provide.

Also revert @hono/node-server 2.0.11 -> 1.19.15. @hono/node-ws@1.3.1
peers on ^1.19.11 and upstream has published no release accepting 2.x.
pnpm only warned, so the workspace stayed green while npm refused with
ERESOLVE -- which broke repair_comisai_bundled_deps, the pass that
repairs npm's separate, long-standing pruning of a non-bundled direct
dep's transitive deps (the @earendil-works/pi-tui class).

Finally, stop capturing npm with --silent. All six installer diagnostics
fields are parsed out of that log, so silencing it turned a failed
install into a bare "install failed" with the cause reachable only from
npm's own debug log -- a path the installer could no longer extract. Add
a fallback that names $(npm config get cache)/_logs when the log is empty.

Verified end-to-end, not by mock: packed the tarball, installed it
globally as the dedicated service user on a clean Ubuntu prefix through
the real install.sh, and confirmed the CLI loads, all eleven pi-ai
dependencies resolve from the bundled copy, the repair pass now
succeeds, and the daemon boots with no errors. pnpm validate passes.
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​types/​semver@​7.7.11001007481100

View full report

@anconina
anconina merged commit 7fb64c7 into main Jul 26, 2026
15 of 17 checks passed
@anconina
anconina deleted the fix/global-install-bundled-metadeps branch July 26, 2026 06:53
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.

npm install -g comisai fails: spawn sh ENOENT on every global install of v1.0.55

1 participant