Skip to content

Prune foreign-platform binaries, duplicate packages, and npm-only files from the packaged CLI - #228

Open
Miista wants to merge 10 commits into
happier-dev:devfrom
Miista:fix/prune-onnxruntime-node-foreign-platform-binaries
Open

Prune foreign-platform binaries, duplicate packages, and npm-only files from the packaged CLI#228
Miista wants to merge 10 commits into
happier-dev:devfrom
Miista:fix/prune-onnxruntime-node-foreign-platform-binaries

Conversation

@Miista

@Miista Miista commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

The packaged CLI (Homebrew/GitHub-release tarball) shipped roughly twice
what a single-platform install needs. On the darwin-arm64 dev build this
drops the installed footprint from 991MB → 428MB and the compressed
tarball from 222.5MB → 98.9MB — no functional changes, only removing
bytes that were never reachable on the packaging target, genuinely
duplicated on disk, or already compiled directly into the binary. Verified
at every stage with a full install → --version → --help → check → reinstall → check → uninstall lifecycle against a real locally-compiled
binary, matching CI's Installer Smoke jobs exactly, plus real end-to-end
release builds (node scripts/pipeline/release/build-cli-binaries.mjs).

Before After Savings
Installed 990.7 MB 428 MB −562.7 MB (−57%)
Download (tarball) 222.5 MB 98.9 MB −123.6 MB (−56%)

The big one: stop double-shipping what Bun already compiled in

apps/cli compiles to a single Bun executable via bun --compile, which
tree-shakes reachable source directly into the binary (confirmed via
strings: reachable files carry provenance comments or their distinctive
exported symbols appear literally in the binary; unreachable ones don't).
Only 3 packages containing native code Bun can't compile in
(@huggingface/transformers, node-pty, @homebridge/node-pty-prebuilt-multiarch)
are declared --external. But the payload-assembly code unconditionally
vendored every declared dependency — of apps/cli itself, and
separately of its internal @happier-dev/* workspace packages — as loose
files regardless. Most non-native dependencies were present twice:
compiled into the 76MB binary, and duplicated as a full loose node_modules
tree alongside it.

Verified per-package before excluding anything, not as a blanket policy:

  • apps/cli/bin/*.mjs (npm-publish entrypoint) is never copied into the
    compiled-binary payload — a separate distribution channel entirely.
  • Read every apps/cli/scripts/*.cjs sidecar script (the only
    apps/cli/scripts/** files actually shipped, run as child processes
    outside the main binary): only node_pty_relay.cjs requires a
    node_modules package by name (the two already-external PTY packages),
    and claude_launcher_runtime.cjs requires @happier-dev/cli-common's
    root-level (non-dist/) files directly by path.
  • Audited for genuinely dynamic (non-literal) require()/import() calls
    Bun's static analyzer can't resolve: found exactly one, already handled
    by the @huggingface/transformers external.
  • Cross-checked every excluded package's compiled-in status via strings,
    searching for distinctive exported symbol names, not just the
    package-name string — a package-name-only search is a proven
    false-negative trap: @happier-dev/protocol showed zero package-name
    hits on an initial check despite its real exports appearing 20 times.
  • Live-tested the riskiest cases empirically: drove happier auth login
    through a real pty with ink deleted from disk — the AuthSelector
    terminal UI still rendered correctly (arrow-key highlighting, ANSI codes
    intact). Separately ran auth request --json (which mints a real
    tweetnacl-box keypair) with both apps/cli's and @happier-dev/protocol's
    nested tweetnacl copies deleted — succeeded with no error.
  • Kept sharp vendored — unlike everything else, it does a
    runtime-constructed require() of a platform-specific native .node
    binding, the same reason node-pty is external. Bun's static analyzer
    categorically can't resolve that path.
  • Kept @happier-dev/cli-common's root-level (non-dist/) files
    vendored
    — confirmed load-bearing by reproducing the exact
    MODULE_NOT_FOUND that results from deleting them; claude_launcher_runtime.cjs
    requires them directly by path, executing outside the compiled binary.

Both exclusions are opt-in and scoped to only the compiled CLI binary
payload path — every other vendoring call site (npm-published tarball
builds, apps/stack, packages/relay-server, the pre-compile
Bun-tree-shaking source snapshot) receives no exclusion set and continues
vendoring every dependency in full, unchanged.

node_modules alone dropped from 434MB to under 190MB, of which ~155MB is
@huggingface/transformers (the ONNX runtime + local-embeddings model
code — genuinely native, necessary) — nearly everything else non-native
collapsed to a few MB of workspace-bundle first-party output and PTY
native bindings.

Removed 4 confirmed-unused apps/cli dependencies

Verified with git-history tracing (not just grep) that these have zero
reachable usage anywhere in the repo:

  • @fastify/swagger — dead in both apps/cli and sibling apps/server.
  • @stablelib/base64 — added alongside the real base64 helper (plain
    Buffer.toString('base64')) and never used; its real historical use was
    mobile-only, since fully retired there too.
  • ai (Vercel AI SDK) — added in the same commit as ACP/Gemini backend
    work but never referenced by any file in that commit or since.
  • http-proxy-middleware — a different, unused package confusable with
    the actually-used http-proxy (called directly by the proxy code).

Also moved tmp to devDependencies (test-only usage, no postinstall
reachability).

Two candidates from the initial survey were investigated and correctly
rejected
: openapi-types (a mandatory, non-optional peer dependency of
fastify-type-provider-zod) and react-devtools-core (ink's
peerDependency, conditionally dynamic-imported when DEV=true is set in
the environment). tar was also considered for a devDependencies move
and correctly rejected: it's required by unpack-tools.cjs, which runs
via postinstall on real end-user npm installs.

Foreign-platform native binaries pruned

Extends prunePackagedTreeDirectory/sanitizePackagedNodeModulesTree
verified across all 5 CLI targets (linux/darwin × x64/arm64, windows x64):
onnxruntime-node (~208MB → ~31MB kept per target), node-pty /
@homebridge/node-pty-prebuilt-multiarch (up to ~58MB), bare-fs /
bare-url / bare-os (~3.7MB), ps-list's Windows-only .exe helpers.

Unused MCP SDK HTTP-transport dependencies

@modelcontextprotocol/sdk vendors express, express-rate-limit, cors,
jose, and a standalone hono for OAuth-authorization-server / Express-
adapter code paths never reached by happier's real SDK entry points —
~12.5MB removed, verified against the compiled binary's strings output.
Does not touch first-party SDK source under server/auth/
(server/auth/errors.js is reachable via client/auth.js and covered by
a dedicated regression test).

Duplicate packages and npm-only build output

Confirmed byte-identical via diff -rq: tar, @modelcontextprotocol/sdk,
zod (4 copies), archiver-utils, qs, get-intrinsic, readable-stream
(4 copies) — ~85MB combined, each with a verified surviving ancestor
reachable via ordinary module resolution. An ajv duplication pattern was
considered and explicitly rejected: no surviving ancestor exists. Also
hardened vendorRuntimeDependencyTree with a name@version dedup map.

@huggingface/transformers's own dist/ ships browser/CJS/minified/WASM
build variants; only dist/transformers.node.mjs is ever resolved via its
exports.node.import condition — ~41MB pruned. package-dist/ (npm-publish
dual-format build output) has its .cjs and .d.mts/.d.cts files pruned.

Review feedback addressed

  • Windows symlink fallback (Greptile): the name@version dedup used
    symlinkSync unconditionally, which can throw EPERM/EACCES on a
    Windows host without Developer Mode or elevated privileges, aborting
    vendoring entirely. Wrapped in try/catch with a real-copy fallback.
  • Dedup content-equivalence check (CodeRabbit): name@version alone
    doesn't prove two resolved package directories are identical. Added
    areDirectoryTreesEquivalent (relative file paths + sizes) and gated the
    symlink on it — a mismatch now falls through to a real copy instead of
    symlinking to the wrong content. Every dedup entry actually shipped in
    this PR was already manually verified byte-identical via diff -rq;
    this is a safety net for future/automatic cases.
  • package-dist pruning scope (CodeRabbit): the prune matched any
    directory named package-dist at any depth in the tree walk, not just
    the actual staged payload root. Threaded a stageRootDir param through
    the recursive walk so the check only fires at the true root.
  • Different-version dedup test (CodeRabbit nitpick): added a
    regression test with two different versions of the same package name at
    different nesting depths, asserting neither is deduped against the
    other — the existing tests only covered "same version, same content" and
    "same version, different content."
  • Declined one nitpick (destructuring the typed export instead of casting
    through Record<string, unknown> in two test blocks) — that cast is this
    file's pre-existing convention from before this PR; changing it
    selectively would leave the file inconsistent.
  • Dangling symlink robustness (CodeRabbit, follow-up review): the
    content-equivalence check's statSync call throws on a dangling symlink
    (a realistic node_modules artifact), which would abort vendoring
    entirely. Wrapped in try/catch with a NaN sentinel size so such an
    entry always compares as a mismatch. Added a test that reproduces the
    crash on the pre-fix code (confirmed by temporarily reverting the fix)
    and passes once fixed.
  • Exact symlink-target assertion (CodeRabbit, follow-up review): the
    dedup test asserted the symlink's resolved target only by basename. By
    the time the vendoring function returns, the destination is already the
    final real path, so tightened the assertion to exact path equality and
    removed the now-unused basename import.
  • Extended the nested-package-dist-scoping test to cover .d.mts/.d.cts
    alongside .cjs, not just the .cjs case.
  • Declined two more nitpicks (extracting a shared diamond-fixture test
    helper; a broader typed-import refactor across all vendoring tests) —
    both explicitly marked optional/low-value in the review itself.

Not included (flagged, not fixed here)

  • difft binary (108MB, unstripped): 99.9MB of it is compiled-in constant
    data (__TEXT.__const) that strip cannot touch — measured stripping
    the real binary, only ~1.4MB recoverable.
  • @types/ps-list misplaced under dependencies — a dependency-placement
    issue, out of scope here.
  • Separately found, unrelated pre-existing bugs (confirmed present
    before and after this PR): @huggingface/transformers's
    dist/transformers.node.mjs fails at runtime with Cannot find package 'onnxruntime-common' on both Node and Bun against the real
    currently-published dev build — local deep-memory-search is likely
    broken independent of this PR. sharp's native binary also appears to
    fail loading on darwin-arm64 in the currently-published dev build,
    affecting the pet-image-validation feature.

CI status

The Installer Smoke (Windows/macOS), Typecheck, CLI Tests, UI Tests,
Core E2E, and CLI + Server E2E checks currently fail on this PR, but an
exact diff against the latest dev-branch CI run shows the identical
failure set
already present on dev itself (a Windows minisign
architecture-selection bug, a dev-branch-wide ERR_MODULE_NOT_FOUND
during the CLI's own staged-build probe, stale test-wiring metadata for
unrelated files, and V8 heap-OOM crashes in large vitest workers) — none
rooted in this diff. Installer Smoke (Linux), Binary Smoke,
Release Contracts, Shared Package Unit Tests, and
Server DB Contract (Postgres) all pass.

Test plan

  • HAPPIER_FEATURE_POLICY_ENV= node --test --test-concurrency=1 scripts/pipeline/**/*.test.mjs — 293/293 pass
  • yarn workspace @happier-dev/cli-common vitest run src/workspaces/index.test.ts — 15/15 pass
  • yarn workspace @happier-dev/cli-common tsc --noEmit — clean
  • New regression tests for every fix, covering all 5 CLI targets where platform/arch-sensitive, plus tests confirming default (no exclusion) vendoring behavior is unchanged for every other call site, plus a different-version dedup safety test
  • Full local-build installer smoke lifecycle against freshly-compiled binaries at every commit — matches CI's Installer Smoke jobs exactly, all steps ok: true
  • Real end-to-end release builds, extracted and measured directly — 428MB installed / 98.9MB tarball, not an estimate
  • Live pty-driven empirical tests of the two highest-risk claims (ink deleted from disk with terminal UI still rendering; a real tweetnacl crypto operation succeeding with both nested copies deleted)
  • Direct invocation of the one load-bearing sidecar exception found (claude_launcher_runtime.cjs) against the real built payload, confirming it still resolves correctly
  • CI's Installer Smoke (Linux/Windows) jobs will exercise the same lifecycle on those platforms once upstream's pre-existing CI issues are resolved

Note

Prune foreign-platform binaries, duplicate packages, and unused files from the packaged CLI binary

  • Adds exclusion sets in buildCliBinaryArtifactPayload.ts to skip vendoring specified direct and workspace-bundled dependencies into the compiled CLI binary payload.
  • Adds deduplication to vendorRuntimeDependencyTree in workspaces/index.ts: identical name@version packages with equivalent directory content are symlinked instead of copied.
  • Extends sanitizePackagedNodeModulesTree in binary-release.mjs with several new pruning passes: removes non-target native prebuilds (onnxruntime-node, node-pty, bare-fs/url/os), strips Windows-only ps-list executables on non-Windows targets, prunes @huggingface/transformers/dist to node-only outputs, removes known nested duplicate vendored packages, and deletes unused @modelcontextprotocol/sdk vendored dependencies (express, cors, jose, hono, etc.).
  • Prunes .cjs, .d.mts, and .d.cts files from the staged root package-dist, retaining only .mjs files.
  • Risk: symlink-based deduplication falls back to copying on Windows (EPERM), and pruned packages must remain accurate as upstream dependency trees change.

Macroscope summarized ce2bcb8.

Summary by CodeRabbit

  • Improvements
    • Reduced packaged CLI size by removing unused runtime dependencies and duplicate vendored packages.
    • Improved runtime dependency bundling by deduplicating equivalent packages while preserving required versions and content.
    • Streamlined compiled CLI packages by excluding components already embedded in the binary.
    • Platform-specific packaging now retains only native binaries and files required for the target operating system and architecture.
    • Removed unreachable distribution files and unnecessary platform-specific assets.
  • Tests
    • Expanded packaging validation across supported CLI targets to improve release reliability.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6ea271c2-f027-4c15-b388-fb4dbccc0276

📥 Commits

Reviewing files that changed from the base of the PR and between ae3a2ae and ce2bcb8.

📒 Files selected for processing (3)
  • packages/cli-common/src/workspaces/index.test.ts
  • packages/cli-common/src/workspaces/index.ts
  • scripts/pipeline/release/lib/binary-release.package-dist-cjs-prune.test.mjs
🚧 Files skipped from review as they are similar to previous changes (2)
  • scripts/pipeline/release/lib/binary-release.package-dist-cjs-prune.test.mjs
  • packages/cli-common/src/workspaces/index.ts

Walkthrough

The PR deduplicates runtime dependency bundles by package name, version, and content. It adds compiled CLI dependency exclusions. It expands packaged Node module sanitization for native binaries, duplicate dependencies, distribution files, SDK dependencies, and platform-specific executables.

Changes

Packaging runtime and release contents

Layer / File(s) Summary
Runtime dependency vendoring deduplication
packages/cli-common/src/workspaces/index.ts, packages/cli-common/src/workspaces/index.test.ts
Runtime vendoring supports exclusions and shared name/version tracking. Equivalent duplicate packages become relative symlinks. Tests cover version differences, content differences, and dangling symlinks.
Compiled payload dependency selection
packages/cli-common/src/componentArtifacts/buildCliBinaryArtifactPayload.ts, apps/cli/package.json
Compiled CLI payloads exclude selected dependencies while retaining runtime-loaded packages. tmp moves to development dependencies, and four runtime dependencies are removed.
Packaged node module sanitization
scripts/pipeline/release/lib/binary-release.mjs
The sanitizer prunes unsupported native bundles, duplicate packages, unused MCP SDK dependencies, non-allowlisted Transformers files, selected package-dist files, and non-Windows ps-list executables.
Sanitization behavior validation
scripts/pipeline/release/lib/binary-release.*.test.mjs
Tests validate target-specific pruning, duplicate removal, distribution cleanup, Transformers allowlisting, MCP SDK pruning, and platform-specific executable handling.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested reviewers: leeroybrun

Sequence Diagram(s)

sequenceDiagram
  participant RuntimeBundler
  participant PackageMetadata
  participant BundleFilesystem
  RuntimeBundler->>PackageMetadata: Read dependency name and version
  RuntimeBundler->>BundleFilesystem: Compare dependency trees
  RuntimeBundler->>BundleFilesystem: Copy or symlink the dependency
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary packaging changes: pruning foreign-platform binaries, duplicate packages, and npm-only files.
Description check ✅ Passed The description fully explains the changes, rationale, risks, testing, measured results, and CI status with detailed regression coverage.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR reduces packaged CLI size by deduplicating vendored dependencies and pruning foreign native binaries, duplicate package trees, Windows-only helpers, and npm-only CommonJS output.

  • Adds target-aware pruning for onnxruntime-node, node-pty variants, bare-* packages, and ps-list.
  • Removes two known nested package duplicates and introduces general name@version vendoring deduplication.
  • Adds regression coverage across the supported release targets and package layouts.

Confidence Score: 4/5

The Windows release path needs a symlink-free fallback before merging because ordinary build hosts can reject the new directory-symlink operation.

Runtime dependency deduplication now relies on directory symlink creation even on Windows, where insufficient symlink privileges cause vendoring and artifact production to abort.

Files Needing Attention: packages/cli-common/src/workspaces/index.ts

Important Files Changed

Filename Overview
packages/cli-common/src/workspaces/index.ts Adds general runtime-dependency deduplication, but its unconditional directory symlink can fail on Windows hosts lacking symlink privileges.
scripts/pipeline/release/lib/binary-release.mjs Adds narrowly targeted release-tree pruning for foreign native assets, known duplicate packages, and npm-only CommonJS output.
packages/cli-common/src/workspaces/index.test.ts Covers the diamond-dependency deduplication shape but does not provide a Windows-safe fallback for the operation under test.
scripts/pipeline/release/lib/binary-release.onnxruntime-node-prune.test.mjs Verifies nested and flat native prebuild pruning across all supported CLI targets.
scripts/pipeline/release/lib/binary-release.bare-fs-and-ps-list-prune.test.mjs Verifies bare-* prebuild and platform-specific ps-list helper pruning.
scripts/pipeline/release/lib/binary-release.duplicate-vendored-package-prune.test.mjs Verifies removal of the two known nested package duplicates while preserving top-level copies.
scripts/pipeline/release/lib/binary-release.package-dist-cjs-prune.test.mjs Verifies recursive removal of CommonJS files from the CLI package-dist payload.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Resolve runtime dependency] --> B{Seen same name and version?}
  B -- No --> C[Copy package tree]
  C --> D[Record destination]
  B -- Yes --> E[Create relative directory symlink]
  E --> F[Package target payload]
  F --> G[Sanitize native and duplicate files]
  G --> H[Create release archive]
Loading

Reviews (1): Last reviewed commit: "Prune more packaged-CLI bloat: bare-fs/u..." | Re-trigger Greptile

// survives the rename because the relationship between the two paths doesn't change.
rmDirSafeSync(depDestDir);
mkdirSync(dirname(depDestDir), { recursive: true });
symlinkSync(relative(dirname(depDestDir), existingDedupePath), depDestDir, 'dir');

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.

P1 Windows directory symlink failure

When a Windows CLI build vendors a repeated name@version dependency on a host without directory-symlink privileges, this unconditional symlinkSync(..., 'dir') throws EPERM, aborting runtime vendoring and preventing the Windows artifact from being built. Use a Windows-safe fallback that does not require Developer Mode or elevation.

Knowledge Base Used: packages/cli-common

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 46c0fef — the symlinkSync call is now wrapped in try/catch, falling back to a real copyDirSafeSync on EPERM/EACCES/ENOSYS (the errors a Windows host without Developer Mode or elevated privileges would raise). Verified via the real local-build installer-smoke lifecycle on darwin; CI's Windows job will exercise the actual fallback path.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli-common/src/workspaces/index.ts`:
- Around line 571-601: Update the deduplication logic around
vendorRuntimeDependencyTree in
packages/cli-common/src/workspaces/index.ts:571-601 so name@version alone never
triggers symlinking and recursion skipping; copy and recurse unless the complete
vendored dependency closures are proven equivalent. Update the related
expectations or coverage in
packages/cli-common/src/workspaces/index.test.ts:216-234 and :254-269 to
preserve nested dependency resolution and verify that distinct closures are not
deduplicated.

In `@scripts/pipeline/release/lib/binary-release.mjs`:
- Around line 693-696: Restrict the `package-dist` pruning branch in
`prunePackagedTreeDirectory` to the staged root entry at
`stageDir/package-dist`, rather than every nested directory named
`package-dist`; preserve traversal for vendored nested trees so their `.cjs`
files remain intact. Update `binary-release.package-dist-cjs-prune.test.mjs`
with a `main: './package-dist/index.cjs'` fixture and assert that
`package-dist/index.cjs` is not deleted.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 23e9516d-a293-427a-bc7a-f945fa48f61c

📥 Commits

Reviewing files that changed from the base of the PR and between 1c72743 and 49054ac.

📒 Files selected for processing (7)
  • packages/cli-common/src/workspaces/index.test.ts
  • packages/cli-common/src/workspaces/index.ts
  • scripts/pipeline/release/lib/binary-release.bare-fs-and-ps-list-prune.test.mjs
  • scripts/pipeline/release/lib/binary-release.duplicate-vendored-package-prune.test.mjs
  • scripts/pipeline/release/lib/binary-release.mjs
  • scripts/pipeline/release/lib/binary-release.onnxruntime-node-prune.test.mjs
  • scripts/pipeline/release/lib/binary-release.package-dist-cjs-prune.test.mjs

Comment thread packages/cli-common/src/workspaces/index.ts
Comment thread scripts/pipeline/release/lib/binary-release.mjs Outdated
@Miista
Miista force-pushed the fix/prune-onnxruntime-node-foreign-platform-binaries branch from e72bd7b to 528ae71 Compare August 4, 2026 20:22

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli-common/src/componentArtifacts/buildCliBinaryArtifactPayload.ts`:
- Around line 62-88: Add an artifact-level regression test for
buildCliBinaryArtifactPayload that constructs a fixture compiled payload,
asserts a package from CLI_BINARY_PAYLOAD_VENDORING_EXCLUDED_PACKAGES is absent,
and asserts sharp remains vendored. Place the test alongside the
buildCliBinaryArtifactPayload implementation and exercise the compiled-payload
path rather than only the generic excludePackageNames helper.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8ca69fe8-4627-4e79-a65b-e7f7481a726e

📥 Commits

Reviewing files that changed from the base of the PR and between 528ae71 and 7557336.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (4)
  • apps/cli/package.json
  • packages/cli-common/src/componentArtifacts/buildCliBinaryArtifactPayload.ts
  • packages/cli-common/src/workspaces/index.test.ts
  • packages/cli-common/src/workspaces/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/cli-common/src/workspaces/index.ts

Comment on lines +62 to +88
const CLI_BINARY_PAYLOAD_VENDORING_EXCLUDED_PACKAGES = new Set<string>([
'@agentclientprotocol/sdk',
'@anthropic-ai/claude-agent-sdk',
'@modelcontextprotocol/sdk',
'@stablelib/hex',
'archiver',
'axios',
'chalk',
'cross-spawn',
'diff',
'expo-server-sdk',
'fastify',
'fastify-type-provider-zod',
'http-proxy',
'https-proxy-agent',
'ink',
'open',
'openapi-types',
'ps-list',
'qrcode-terminal',
'react',
'react-devtools-core',
'socket.io-client',
'tar',
'tmp',
'zod',
]);

@coderabbitai coderabbitai Bot Aug 5, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inventory existing payload-build coverage.
fd -t f . packages/cli-common/src \
  | rg '\.(test|spec)\.[cm]?[jt]sx?$' \
  | xargs -r rg -n -C 3 \
    'buildCliBinaryArtifactPayload|copyCliNodeRuntimePayload|CLI_BINARY_PAYLOAD_VENDORING_EXCLUDED_PACKAGES|vendorBundledPackageRuntimeDependencies|sharp'

# Confirm tests cover both exclusion and retained native runtime dependencies.
rg -n -C 3 \
  'excludePackageNames|CLI_BINARY_PAYLOAD_VENDORING_EXCLUDED_PACKAGES|sharp' \
  packages/cli-common/src \
  -g '*.{test,spec}.{ts,tsx,js,jsx}'

Repository: happier-dev/happier

Length of output: 15977


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Candidate files:"
fd -t f 'buildCliBinaryArtifactPayload.*test\.ts|buildCliBinaryArtifactPayload\.ts' packages/cli-common/src/componentArtifacts

echo
echo "buildCliBinaryArtifactPayload.ts outline:"
ast-grep outline packages/cli-common/src/componentArtifacts/buildCliBinaryArtifactPayload.ts --view compact || true

echo
echo "buildCliBinaryArtifactPayload.ts relevant sections:"
sed -n '1,220p' packages/cli-common/src/componentArtifacts/buildCliBinaryArtifactPayload.ts | cat -n

echo
echo "Workspace-sync test relevant sections:"
sed -n '1,280p' packages/cli-common/src/componentArtifacts/buildCliBinaryArtifactPayload.workspaceSync.test.ts | cat -n

echo
echo "Windows rename fallback test relevant sections:"
sed -n '1,170p' packages/cli-common/src/componentArtifacts/buildCliBinaryArtifactPayload.windowsRenameFallback.test.ts | cat -n

echo
echo "All repo references to excluded constant/list:"
rg -n -C 2 'CLI_BINARY_PAYLOAD_VENDORING_EXCLUDED_PACKAGES|chalk|axios|sha' packages/cli-common/src/componentArtifacts packages/cli-common/src/workspaces -g '*.{ts,tsx,js,jsx,test.ts,spec.ts}'

Repository: happier-dev/happier

Length of output: 45696


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "buildCliBinaryArtifactPayload.ts rest:"
sed -n '220,460p' packages/cli-common/src/componentArtifacts/buildCliBinaryArtifactPayload.ts | cat -n

echo
echo "workspaces index relevant sections:"
sed -n '1,440p' packages/cli-common/src/workspaces/index.ts | cat -n

Repository: happier-dev/happier

Length of output: 21874


Add an artifact-level regression test for the compiled-payload exclusions.

packages/cli-common/src/workspaces/index.test.ts covers the generic excludePackageNames helper and sharp retention, but it does not cover buildCliBinaryArtifactPayload’s compiled payload path. Add an existing-fault-future-failing test in packages/cli-common/src/componentArtifacts/buildCliBinaryArtifactPayload that builds a fixture payload, proves one listed excluded package is absent, and proves sharp remains vendored.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli-common/src/componentArtifacts/buildCliBinaryArtifactPayload.ts`
around lines 62 - 88, Add an artifact-level regression test for
buildCliBinaryArtifactPayload that constructs a fixture compiled payload,
asserts a package from CLI_BINARY_PAYLOAD_VENDORING_EXCLUDED_PACKAGES is absent,
and asserts sharp remains vendored. Place the test alongside the
buildCliBinaryArtifactPayload implementation and exercise the compiled-payload
path rather than only the generic excludePackageNames helper.

Source: Coding guidelines

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@coderabbitai resume

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
packages/cli-common/src/workspaces/index.test.ts (2)

138-141: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep the new tests on the typed export path.

Lines 138-141 cast the module namespace to Record<string, unknown>. Lines 178-183 cast the export to a function. Keep the dynamic import if isolation is required, but destructure the typed export directly:

const { bundleWorkspacePackageWithRuntimeDependencies } = await import('./index');

This preserves strict TypeScript checking and detects export or parameter changes at compile time.

Also applies to: 178-183, 199-202, 232-236

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli-common/src/workspaces/index.test.ts` around lines 138 - 141,
Update the affected tests to destructure
bundleWorkspacePackageWithRuntimeDependencies directly from the dynamic import
of './index'. Remove the Record<string, unknown> module cast and any function
casts at all referenced test blocks, preserving the typed export and
compile-time parameter checking.

Source: Coding guidelines


290-382: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Protect the package-name-and-version deduplication key.

The fixture covers two copies of shared-dep@1.2.3. Add a second shared-dep version and assert that both versions remain separate and resolve to their own contents. A name-only deduplication bug would otherwise pass this test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli-common/src/workspaces/index.test.ts` around lines 290 - 382,
Extend the test around bundleWorkspacePackageWithRuntimeDependencies to include
a second shared-dep copy with a different version and distinct content, then
assert both name@version instances remain separate and resolve to their own
files. Ensure the existing same-version nested copy still symlinks to the first
vendored copy, while the different-version copy is vendored independently rather
than deduplicated by name alone.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/cli-common/src/workspaces/index.test.ts`:
- Around line 138-141: Update the affected tests to destructure
bundleWorkspacePackageWithRuntimeDependencies directly from the dynamic import
of './index'. Remove the Record<string, unknown> module cast and any function
casts at all referenced test blocks, preserving the typed export and
compile-time parameter checking.
- Around line 290-382: Extend the test around
bundleWorkspacePackageWithRuntimeDependencies to include a second shared-dep
copy with a different version and distinct content, then assert both
name@version instances remain separate and resolve to their own files. Ensure
the existing same-version nested copy still symlinks to the first vendored copy,
while the different-version copy is vendored independently rather than
deduplicated by name alone.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ed01fac0-cc8c-4d24-97b9-d082a5104c57

📥 Commits

Reviewing files that changed from the base of the PR and between 7557336 and fcdda61.

📒 Files selected for processing (3)
  • packages/cli-common/src/componentArtifacts/buildCliBinaryArtifactPayload.ts
  • packages/cli-common/src/workspaces/index.test.ts
  • packages/cli-common/src/workspaces/index.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/cli-common/src/componentArtifacts/buildCliBinaryArtifactPayload.ts
  • packages/cli-common/src/workspaces/index.ts

@Miista

Miista commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

The failing checks on this PR (`Installer Smoke (Windows)`, `Installer Smoke (macOS)`, `Typecheck`, `CLI Tests`, `UI Tests`, `Core E2E`, `CLI + Server E2E`) are pre-existing failures on `dev` itself, unrelated to this change. Verified by comparing against the latest `dev`-branch CI runs:

  • Installer Smoke (Windows): fails during minisign bootstrap — resolves an `aarch64` binary path on an x64 Windows runner (`minisign-win64/aarch64/minisign.exe`), then fails to execute it. Same error on `dev`.
  • Installer Smoke (macOS): fails with `ERR_MODULE_NOT_FOUND` for `backends/codex/happyMcpStdioBridge.mjs` during the CLI's own "staged runtime import probe" step, before packaging even runs. Identical failure on `dev`.
  • Typecheck: this job actually runs `validateTestWiring.ts` (a test-lane-mapping check), failing on files unrelated to this PR (`apps/docs/scripts/build.test.mjs`, `packages/sherpa-native/scripts/`, several `packages/tests/suites/core-e2e/` feature-tag issues). Same 11 issues reported on `dev`.
  • The remaining failing jobs show the identical failure set on the latest `dev` CI run I checked.

Locally, the actual change in this PR has been verified via the real `installers-smoke` local-build lifecycle (`install → --version → --help → check → reinstall → check → uninstall`) passing cleanly on every commit, plus 293/293 passing in `scripts/pipeline/**/*.test.mjs` and the `cli-common` workspace's vitest suite. Happy to help investigate/fix the upstream CI issues separately if useful, but didn't want to fold that into this PR's scope.

Miista added 9 commits August 5, 2026 16:35
onnxruntime-node (a transitive dependency of @huggingface/transformers,
used for local embeddings) bundles prebuilt native binaries for every
platform/arch under bin/napi-v<N>/<platform>/<arch>/ inside a single
package, rather than splitting them into per-target optionalDependencies.
Its package.json declares os: [win32, darwin, linux] with no cpu
constraint, so the existing whole-package pruning in
prunePackagedTreeDirectory never touches it — every single-platform CLI
release tarball ships all 6 platform/arch binary sets (~177MB of the
~208MB onnxruntime-node bin/ tree is unused on any given install).

Extend the packaging-time tree sanitizer to recognize known
bundle-all-platforms directory layouts and prune non-matching
platform/arch subdirectories in place, in addition to the existing
whole-package os/cpu constraint check.
…ages, package-dist .cjs

Extends the platform-binary pruning added earlier on this branch with four
more independently-verified size reductions in the packaged CLI payload:

- bare-fs/bare-url/bare-os (nested under archiver -> tar-stream) bundle
  prebuilt natives for every platform/arch with no os/cpu package.json
  gating, same anti-pattern as node-pty. Added to the existing flat-layout
  prune pattern list.
- ps-list ships Windows-only fastlist-*.exe helpers unconditionally.
  prunePackagedTreeDirectory previously only pruned directories; added a
  file-level branch to strip these on non-Windows targets.
- tar (inside onnxruntime-node's own node_modules) and
  @modelcontextprotocol/sdk (inside @anthropic-ai/claude-agent-sdk's own
  node_modules) are exact, byte-identical duplicates of copies already
  vendored at the payload's top-level node_modules. The independent
  per-package vendoring entry points in workspaces/index.ts don't share a
  visited set with each other, so a dependency already vendored at the top
  level gets vendored again inside a nested package's tree. Deleted the
  nested duplicates outright at packaging time; ordinary upward-walking
  module resolution finds the top-level copy.
- package-dist/*.cjs (139 files) is the dual-format npm-publish build
  output copied verbatim into the Homebrew/binary payload, but only the
  .mjs half is ever loaded by the compiled binary's entrypoints -- the
  .cjs half exists solely for the separately-published npm package's
  require() consumers, a different distribution channel. Pruned for the
  binary-release path only; the npm-publish path is untouched.

Also hardens workspaces/index.ts's vendorRuntimeDependencyTree with a
name@version dedup map so future builds don't reintroduce the
@modelcontextprotocol/sdk duplicate in the first place (symlinks to the
first-vendored copy instead of copying again).

Measured on the real installed darwin-arm64 v0.2.10-dev.53 payload:
991MB -> 676MB installed, 222.5MB -> 129.7MB compressed tarball.
…st type files

Second round of packaged-CLI size reduction, on top of the fixes already in
this PR:

- @modelcontextprotocol/sdk vendors express, express-rate-limit, cors, and
  jose for its OAuth-authorization-server and Express-adapter code paths,
  and a standalone hono package alongside the (actually used) @hono/node-server.
  Traced every require() reachable from happier's real SDK entry points
  (server/index.js, server/mcp.js, server/streamableHttp.js, client/*.js) and
  cross-checked against zero occurrences of these package names in the
  compiled binary's strings output -- none of these are ever loaded. Does
  NOT touch first-party SDK source under server/auth/ (server/auth/errors.js
  is reachable via client/auth.js and must survive).
- Extended DUPLICATE_VENDORED_PACKAGE_DIR_PATTERNS with more confirmed
  byte-identical nested duplicates: zod (4 copies), archiver-utils (1),
  qs (1, removes most of a get-intrinsic duplication chain as a byproduct),
  get-intrinsic (1 remaining pattern), readable-stream (4, all in archiver's
  own dependency chain). Each has a verified surviving ancestor reachable by
  upward node_modules resolution once the nested copy is removed. An ajv
  duplication pattern was considered and explicitly rejected: those copies
  have no surviving ancestor anywhere in the payload and must be kept.
- package-dist's dual-format pruning (added earlier in this PR for .cjs)
  now also strips .d.mts/.d.cts type-declaration files -- confirmed dead
  weight for the same reason as .cjs: this is a compiled Bun binary, not a
  package resolved via npm's exports/types condition machinery.

Measured on the already-fixed 676MB darwin-arm64 baseline: 676MB -> 600MB
installed, 130.1MB -> 120.7MB compressed tarball.
@huggingface/transformers ships browser, CJS, minified, and WASM-backend
build variants in dist/ (44MB), but this payload -- a Bun/Node-only CLI --
only ever resolves dist/transformers.node.mjs via the package's own
package.json exports.node.import condition. Confirmed via
createLocalTransformersEmbeddingsProvider.ts's dynamic
`await import('@huggingface/transformers')` and its own error-handling code,
which explicitly checks for this exact filename. The bundled
ort-wasm-simd-threaded.jsep.{mjs,wasm} pair is onnxruntime-web's browser-only
WASM backend; the node build imports the native onnxruntime-node binding
instead, confirmed by the bundler's own "onnxruntime-web (ignored)" comment
in the compiled node output.

Adds a new keep-list-based pruning mechanism (DIST_ROOT_KEEP_FILE_ALLOWLIST)
alongside the existing pattern-list mechanisms, for the "package ships many
dist/ build targets, only one is ever resolved on this runtime" case.

~41MB installed size reduction (600MB -> 559MB on the already-fixed
darwin-arm64 baseline), not platform/arch-sensitive since the exports-map
resolution doesn't depend on OS/arch.
…dencies

Verified with git-history tracing, cross-workspace checks, and
peer-dependency analysis (not just grep) that these declared dependencies
have zero reachable runtime or build-time usage anywhere in the repo:

- @fastify/swagger: dead in both apps/cli and its sibling apps/server;
  never wired up in either.
- @stablelib/base64: added alongside apps/cli's real base64 helper (which
  uses plain Buffer.toString('base64')) and never used; its real historical
  use was mobile-only and has since been fully retired there too.
- ai (Vercel AI SDK): added in the same commit as ACP/Gemini backend work
  but never referenced by any file in that commit or since; confirmed
  absent from the compiled dist bundle.
- http-proxy-middleware: a different, unused package confusable with the
  actually-used http-proxy (which apps/cli's proxy code calls directly).

Also moved tmp to devDependencies: only imported from *.test.ts files,
never from apps/cli/src at runtime, and not referenced by any postinstall-
reachable script (unlike tar, which was considered and correctly rejected
for the same move -- unpack-tools.cjs requires it and runs via postinstall
on real end-user npm installs).

Two candidates from the initial survey were investigated and correctly
rejected: openapi-types (a mandatory, non-optional peer dependency of
fastify-type-provider-zod and @fastify/swagger, both real direct deps) and
react-devtools-core (ink's peerDependency, conditionally dynamic-imported
by ink's reconciler when DEV=true is set in the environment -- a real,
if rarely-exercised, code path outside apps/cli's own control).

Verified via the full local-build installer smoke lifecycle (install ->
version -> help -> check -> reinstall -> check -> uninstall) against a
freshly-compiled binary with these changes applied -- all steps passed.
~14.8MB removed across the 4 dropped packages.
…nary

Bun's --compile tree-shakes apps/cli's own TypeScript source into the
compiled binary (confirmed via `strings` on the real shipped binary:
reachable files carry provenance comments, unreachable ones -- like the
express/server-auth code already pruned in an earlier commit -- don't).
But copyCliNodeRuntimePayload unconditionally vendored EVERY declared
apps/cli dependency as loose files on disk, regardless of whether Bun had
already compiled the reachable code directly into the executable. Most
non-native dependencies were therefore present twice: compiled into the
76MB binary, and duplicated as a full loose node_modules tree alongside it.

Verified per-package before excluding anything, not as a blanket policy:
- Confirmed apps/cli/bin/*.mjs (the npm-publish entrypoint) is never copied
  into the compiled-binary payload at all -- it's a separate distribution
  channel, irrelevant to this payload's node_modules footprint.
- Read every apps/cli/scripts/*.cjs sidecar script (copied into the payload
  via CLI_RUNTIME_SIDECAR_ENTRIES, run as child processes outside the main
  binary): only node_pty_relay.cjs requires a node_modules package by name,
  and it's node-pty/@homebridge/node-pty-prebuilt-multiarch -- already
  handled as Bun externals.
- Audited apps/cli/src for genuinely dynamic (non-literal) require()/
  import() calls that Bun's static analyzer can't resolve: found exactly
  one, in createLocalTransformersEmbeddingsProvider.ts, already accounted
  for by the existing @huggingface/transformers external.
- Cross-checked every excluded package's compiled-in status via `strings`
  provenance-comment counts on the real binary.
- Live-tested the riskiest case empirically: drove `happier auth login`
  through a real pty with ink deleted from the on-disk node_modules copy --
  the AuthSelector terminal UI still rendered correctly (arrow-key
  highlighting, ANSI codes intact), direct proof rather than inference.
- Kept `sharp` vendored: unlike everything else, it does a runtime-
  constructed require() of a platform-specific native .node binding,
  structurally identical to why node-pty is already external -- Bun's
  static analyzer categorically cannot resolve that path, so its on-disk
  copy is architecturally necessary regardless of tree-shaking.

The exclusion is opt-in and scoped to only the compiled CLI binary payload
path (copyCliNodeRuntimePayload): every other vendorBundledPackageRuntimeDependencies
call site (npm-published tarball builds, apps/stack, packages/relay-server)
receives no excludePackageNames argument and continues vendoring every
dependency in full, unchanged.

Verified via the full local-build installer smoke lifecycle (install ->
version -> help -> check -> reinstall -> check -> uninstall) against a
freshly-compiled binary -- all steps passed -- and via a real release
build (node scripts/pipeline/release/build-cli-binaries.mjs) measured
end to end:

  installed: 990.7MB -> 430MB   (-57%)
  tarball:   222.5MB -> 99.2MB  (-55%)

node_modules alone dropped from 434MB to 186MB, of which 155MB is
@huggingface/transformers (the ONNX runtime + local-embeddings model
code, genuinely native/necessary) -- nearly everything else non-native
collapsed to a few MB of workspace bundles and PTY native bindings.
…ndencies

The compiled-binary payload also vendors apps/cli's internal @happier-dev/*
workspace packages via a separate function, bundleWorkspacePackageWithRuntimeDependencies,
which never received the excludePackageNames treatment added for apps/cli's
own dependencies in the prior commit. Each workspace bundle's own runtime
dependencies were therefore still vendored in full even where the bundle's
first-party code (and by the same logic, its own dependencies) is already
compiled into the binary.

Verified per-package, not as a blanket policy -- and specifically re-verified
using distinctive exported symbol names rather than package-name strings
alone, since a package-name-only search is a proven false-negative trap
(@happier-dev/protocol itself showed zero package-name hits in an earlier
check, despite its real exports appearing 20 times):

- @happier-dev/protocol's own nested @noble/hashes, base64-js, tweetnacl,
  and zod-to-json-schema: confirmed compiled in via distinctive
  export/error-string matches, confirmed empirically by deleting them from
  a real payload and running --version/--help/doctor/status/daemon
  status/auth request --json (which genuinely exercises a live tweetnacl
  crypto operation) with no MODULE_NOT_FOUND.
- The other 4 bundles (cli-common, agents, release-runtime,
  connection-supervisor, transfers) declare no external runtime
  dependencies of their own, so nothing to exclude for them specifically.

Found and deliberately did NOT touch a real exception: @happier-dev/cli-common
ships root-level (non-dist/) files -- expandHomeDirPath.cjs and others --
that apps/cli/scripts/claude_launcher_runtime.cjs, a real production sidecar
spawned as a separate process outside the compiled binary, requires directly
by path. Confirmed load-bearing by reproducing the exact MODULE_NOT_FOUND
that results from deleting them. These must keep being vendored regardless
of what Bun compiles into the main binary process.

Also deliberately left apps/cli's own top-level tweetnacl dependency
unexcluded in this commit (a known follow-up, already confirmed safe by
the same evidence) to keep this diff and the prior commit's exclusion list
independently reviewable.

Verified via the full local-build installer smoke lifecycle and a real
release build, including a direct invocation of the load-bearing sidecar
script against the built payload to confirm the kept exception still works:

  installed: 430MB -> 428MB
  tarball:   99.2MB -> 98.9MB
…coping

Three fixes from automated review feedback on the open PR:

- Greptile: the name@version dedup symlink (added in an earlier commit)
  used symlinkSync unconditionally. Directory symlinks require elevated
  privileges or Developer Mode on Windows and can throw EPERM/EACCES on an
  ordinary build host, which would abort vendoring and artifact production
  for the Windows release target. Wrapped in try/catch with a real-copy
  fallback on EPERM/EACCES/ENOSYS.

- CodeRabbit: the same dedup logic trusted name@version alone as proof two
  resolved package directories are identical, with no check that their
  actual contents match. Added areDirectoryTreesEquivalent, a lightweight
  (file path + size, not full content hash) structural comparison, and
  gated the symlink on it -- a mismatch falls through to a normal copy
  instead of symlinking to the wrong content. Every dedup entry actually
  added to DUPLICATE_VENDORED_PACKAGE_DIR_PATTERNS in a prior commit was
  already manually verified byte-identical via diff -rq, so this is a
  safety net for future/automatic cases, not a fix for an observed bug.

- CodeRabbit: prunePackageDistDualFormatDir's package-dist matching in
  prunePackagedTreeDirectory checked entry.name === 'package-dist' at any
  depth in the tree walk, not just the actual staged payload root. No
  vendored package in the current tree happens to ship a directory with
  that name, so this was latent rather than an observed bug, but a future
  dependency could collide with it and have its own .cjs files incorrectly
  stripped. Threaded a stageRootDir param through the recursive walk and
  scoped the check to only fire when directoryPath === stageRootDir.

Added regression tests for all three: a mismatched-content dedup case that
must NOT symlink, and a nested same-named package-dist directory that must
survive pruning untouched. Verified via the full local-build installer
smoke lifecycle against a freshly-compiled binary -- all steps passed.
The existing dedup tests covered "same name@version, same content" (dedupes)
and "same name@version, different content" (falls through to a copy), but
not "same package name, different version" -- a name-only dedup bug would
have passed both existing tests undetected. Added a diamond-dependency
fixture with shared-dep@1.2.3 and shared-dep@2.0.0 at different nesting
depths, asserting both are vendored as independent real directories with no
symlink either direction.

Declined the review's other nitpick (destructure the typed export instead
of casting through Record<string, unknown> in the two new test blocks) --
that cast pattern is this file's existing convention from before this PR
(present since the file's earliest tests), and changing it selectively for
only the newly-added blocks would leave the file inconsistent; a full-file
style pass is a separate, unrelated change.
@Miista
Miista force-pushed the fix/prune-onnxruntime-node-foreign-platform-binaries branch from 46c0fef to ae3a2ae Compare August 5, 2026 14:38
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
✅ Action performed

Reviews resumed.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
packages/cli-common/src/workspaces/index.ts (1)

557-566: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Make the equivalence walk tolerant of broken symlinks.

statSync(entryPath) follows the link. If a source package tree contains a dangling symlink, statSync throws ENOENT and the whole vendoring run fails. The copy path uses copyDirSafeSync(..., { dereference: true }), so the walk is the only place that can abort on this input.

A safe fallback is to treat an unstattable entry as a mismatch marker instead of throwing. That keeps the dedupe decision conservative: the pair falls through to a normal copy.

Note also that a symlink pointing at a directory is recorded as a single entry with the target directory's stat size, so its subtree is not compared. Both sides are treated the same way, so this only weakens the check; it does not invert it.

♻️ Proposed hardening for unstattable entries
       if (!entry.isFile() && !entry.isSymbolicLink()) continue;
-      const size = statSync(entryPath).size;
-      result.set(relative(rootDir, entryPath), size);
+      // A dangling symlink must not abort vendoring. Record a sentinel so the pair is treated as
+      // non-equivalent and falls through to a normal copy.
+      let size: number;
+      try {
+        size = statSync(entryPath).size;
+      } catch {
+        size = Number.NaN;
+      }
+      result.set(relative(rootDir, entryPath), size);

Number.NaN never equals itself, so filesB.get(relPath) !== size treats such an entry as a mismatch on either side.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli-common/src/workspaces/index.ts` around lines 557 - 566, Update
the equivalence walk around the readdirSync/statSync loop to tolerate entries
that cannot be statted, including dangling symlinks. Catch statSync failures and
record Number.NaN as the entry size so comparisons conservatively produce a
mismatch, while preserving the existing handling for regular files and symlinks.
packages/cli-common/src/workspaces/index.test.ts (2)

396-441: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting the shared diamond fixture.

The three dedupe tests build near-identical scaffolding. Only three values vary: the nested package version, the nested file content, and the version consumer-dep declares. A local helper would remove roughly 150 lines of duplication and make each test state its variable clearly.

function writeDiamondFixture(rootDir: string, nested: Readonly<{
  declaredVersion: string;
  version: string;
  content: string;
}>): Readonly<{ srcPackageDir: string }> { /* ... */ }

This is optional. The current tests are readable as written.

Also applies to: 481-527

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli-common/src/workspaces/index.test.ts` around lines 396 - 441,
Optionally extract the repeated diamond dependency fixture setup from the three
dedupe tests into a local writeDiamondFixture helper. Have it accept
nested.declaredVersion, nested.version, and nested.content, return the
srcPackageDir, and update each test to provide only those varying values while
preserving the existing fixture behavior.

138-141: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Import the vendoring functions with their real types instead of re-declaring signatures inline.

All seven new tests reach the exported functions through (workspaceModule as Record<string, unknown>) and then cast to a locally written call signature. This discards the real exported types. If excludePackageNames were renamed or retyped in index.ts, these tests would still compile and would silently pass a property the production code ignores. Line 1 already uses a typed static import for other symbols from the same module, so the typed path is the canonical one here.

Adding both symbols to the existing static import removes the Record<string, unknown> lookup, the toBeTypeOf('function') guard, and the inline parameter type in each test.

  • packages/cli-common/src/workspaces/index.test.ts#L138-L141: remove the lookup and the toBeTypeOf guard; call the statically imported bundleWorkspacePackageWithRuntimeDependencies directly at lines 178-183.
  • packages/cli-common/src/workspaces/index.test.ts#L199-L202: same change; drop the inline cast at lines 232-236.
  • packages/cli-common/src/workspaces/index.test.ts#L293-L296: same change; drop the inline cast at lines 350-354.
  • packages/cli-common/src/workspaces/index.test.ts#L387-L390: same change; drop the inline cast at lines 445-449.
  • packages/cli-common/src/workspaces/index.test.ts#L473-L476: same change; drop the inline cast at lines 531-535.
  • packages/cli-common/src/workspaces/index.test.ts#L560-L563: use the statically imported vendorBundledPackageRuntimeDependencies; drop the inline cast at lines 596-601.
  • packages/cli-common/src/workspaces/index.test.ts#L616-L619: same change; drop the inline cast at lines 645-649.

If a test intentionally needs a fresh module instance, keep the dynamic import but type it, for example const { bundleWorkspacePackageWithRuntimeDependencies } = await import('./index');. That preserves module-cache reset while keeping the real signatures.

As per coding guidelines: "Prefer satisfies, explicit interfaces, typed fixtures, and canonical schemas over casting."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli-common/src/workspaces/index.test.ts` around lines 138 - 141,
Replace the dynamically imported Record<string, unknown> lookups and inline
casts in packages/cli-common/src/workspaces/index.test.ts at lines 138-141,
199-202, 293-296, 387-390, 473-476, 560-563, and 616-619 with typed static
imports of bundleWorkspacePackageWithRuntimeDependencies and
vendorBundledPackageRuntimeDependencies, removing the toBeTypeOf guards and
calling the real exported functions directly; if a fresh module instance is
required, destructure the functions from a typed dynamic import instead.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli-common/src/workspaces/index.test.ts`:
- Around line 376-378: Update the symlink assertion in the vendored dependency
test to compare resolvedLinkTarget directly with vendoredSharedDepDir, verifying
the exact dedupe target rather than only its basename. Remove the basename
import if it is no longer used elsewhere in the test.

In `@scripts/pipeline/release/lib/binary-release.package-dist-cjs-prune.test.mjs`:
- Around line 80-83: Extend the nested vendored package setup in the scope
regression test to create both .d.mts and .d.cts declaration files alongside the
existing index files, then assert that all nested files remain after
sanitization. Update the assertions around the nested package case to cover the
complete file set without testing implementation details.

---

Nitpick comments:
In `@packages/cli-common/src/workspaces/index.test.ts`:
- Around line 396-441: Optionally extract the repeated diamond dependency
fixture setup from the three dedupe tests into a local writeDiamondFixture
helper. Have it accept nested.declaredVersion, nested.version, and
nested.content, return the srcPackageDir, and update each test to provide only
those varying values while preserving the existing fixture behavior.
- Around line 138-141: Replace the dynamically imported Record<string, unknown>
lookups and inline casts in packages/cli-common/src/workspaces/index.test.ts at
lines 138-141, 199-202, 293-296, 387-390, 473-476, 560-563, and 616-619 with
typed static imports of bundleWorkspacePackageWithRuntimeDependencies and
vendorBundledPackageRuntimeDependencies, removing the toBeTypeOf guards and
calling the real exported functions directly; if a fresh module instance is
required, destructure the functions from a typed dynamic import instead.

In `@packages/cli-common/src/workspaces/index.ts`:
- Around line 557-566: Update the equivalence walk around the
readdirSync/statSync loop to tolerate entries that cannot be statted, including
dangling symlinks. Catch statSync failures and record Number.NaN as the entry
size so comparisons conservatively produce a mismatch, while preserving the
existing handling for regular files and symlinks.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8cebc764-b986-438b-ac7e-50f77fa4afb7

📥 Commits

Reviewing files that changed from the base of the PR and between c65ea28 and ae3a2ae.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (12)
  • apps/cli/package.json
  • packages/cli-common/src/componentArtifacts/buildCliBinaryArtifactPayload.ts
  • packages/cli-common/src/workspaces/index.test.ts
  • packages/cli-common/src/workspaces/index.ts
  • scripts/pipeline/release/lib/binary-release.bare-fs-and-ps-list-prune.test.mjs
  • scripts/pipeline/release/lib/binary-release.duplicate-vendored-package-prune.test.mjs
  • scripts/pipeline/release/lib/binary-release.exhaustive-duplicate-resweep-prune.test.mjs
  • scripts/pipeline/release/lib/binary-release.huggingface-transformers-dist-prune.test.mjs
  • scripts/pipeline/release/lib/binary-release.mcp-sdk-unused-deps-prune.test.mjs
  • scripts/pipeline/release/lib/binary-release.mjs
  • scripts/pipeline/release/lib/binary-release.onnxruntime-node-prune.test.mjs
  • scripts/pipeline/release/lib/binary-release.package-dist-cjs-prune.test.mjs
🚧 Files skipped from review as they are similar to previous changes (8)
  • apps/cli/package.json
  • scripts/pipeline/release/lib/binary-release.exhaustive-duplicate-resweep-prune.test.mjs
  • scripts/pipeline/release/lib/binary-release.onnxruntime-node-prune.test.mjs
  • scripts/pipeline/release/lib/binary-release.duplicate-vendored-package-prune.test.mjs
  • packages/cli-common/src/componentArtifacts/buildCliBinaryArtifactPayload.ts
  • scripts/pipeline/release/lib/binary-release.mjs
  • scripts/pipeline/release/lib/binary-release.bare-fs-and-ps-list-prune.test.mjs
  • scripts/pipeline/release/lib/binary-release.mcp-sdk-unused-deps-prune.test.mjs

Comment thread packages/cli-common/src/workspaces/index.test.ts Outdated
…rget, test coverage

Three more review findings, verified real before fixing:

- The dedup content-equivalence check (areDirectoryTreesEquivalent, added in
  the prior review-feedback commit) called statSync directly while walking a
  resolved package tree. A dangling symlink -- a realistic node_modules
  artifact (broken .bin shim, an optional dependency that failed to
  install) -- throws ENOENT and would abort the entire vendoring run.
  Wrapped in try/catch, recording NaN as a sentinel size so the entry always
  compares as a mismatch (NaN !== NaN) and the pair conservatively falls
  through to a normal copy instead of crashing. Added a dedicated test that
  reproduces the crash on the old code (confirmed by temporarily reverting
  the fix and re-running it) and passes once fixed.
- The diamond-dependency dedup test asserted the symlink's resolved target
  only by basename, with a comment explaining the link is captured inside a
  not-yet-renamed atomic staging tree. That's true of the implementation but
  irrelevant to what the test observes: by the time
  bundleWorkspacePackageWithRuntimeDependencies returns, destPackageDir is
  already the final real path, so the test can and should assert exact path
  equality. Tightened the assertion and removed the now-unused `basename`
  import.
- Extended the nested-package-dist scoping regression test to also create
  .d.mts/.d.cts files alongside .cjs, asserting the complete file set
  survives at a non-root nesting depth, not just the .cjs case.

Declined two lower-priority nitpicks in the same review (extracting a shared
diamond-fixture test helper, and a broader typed-import refactor across all
seven vendoring tests) as explicitly optional/low-value per the review itself
and not worth the added indirection or file-wide convention change.

Verified via cli-common's vitest suite (15/15), the full packaging pipeline
suite (293/293), a clean tsc --noEmit, and the real local-build installer
smoke lifecycle -- all steps passed.
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.

1 participant