refactor(nix): manage Nix-built JS tooling with bun2nix - #1489
Conversation
`restore-prefixes-first-match` only carried the prefix that embeds the deps hash, so any change to flake.nix, flake.lock or nix/packages.nix sent the job to a completely cold Nix store even when the change merely added developer tooling and left every Cargo artefact untouched. Add a second, hashless prefix after it. The hashed prefix still wins when it matches, and otherwise the newest prior entry warms the deps instead of forcing a full rebuild.
nix/publint.nix carried a full pnpm-lock.yaml as a heredoc inside a Nix
string, fed to fetchPnpmDeps. Bumping publint meant hand-editing that
embedded lockfile and its fetcherVersion/hash pair, which is how the
lockfile drifted from the workspace catalogue in the first place.
Move the dependency set into nix/tools/publint as real files instead:
package.json, the bun.lock that `bun install` resolved from it, and the
bun.nix that `bun2nix` derived from that lockfile. The Nix expression
lives next to them as default.nix, so the tool is self-contained.
nix/bun-node-modules.nix is the shared piece: it turns one of those
manifests into a node_modules tree via bun2nix's setup hook, scoping src
to just package.json and bun.lock so neither the sibling Nix expression
nor a tool's own scripts force the dependency tree to rebuild.
Two wrinkles worth recording:
- bun2nix stages each dependency through its own derivation and patches
shebangs there against bun's `bun-with-fake-node` shim, so publint
arrives bound to Bun's Node emulation. dontPatchShebangs cannot undo
that, since the path is already baked into the cached package, so the
install phase repoints it at real Node. publint is a Node packaging
linter, and the baked-in path would otherwise keep bun in the runtime
closure, which is now just bash and nodejs.
- The hook only honours a wholesale replacement of its default install
flags, so the linker and backend are restated alongside
--frozen-lockfile.
The publint flake check now skips nix/tools/*/package.json: those are
dependency manifests for Nix-built tooling, not publishable packages.
treefmt matches files against a formatter's `includes` and passes them as arguments, so a batch can consist entirely of paths that nix/oxlint-check.json ignores. oxlint exits non-zero with "No files found to lint" in that case, which treefmt reports as a formatting failure for a file that was deliberately excluded. Pass --no-error-on-unmatched-pattern, oxlint's own switch for exactly this situation, mirroring the flag oxfmt is already given here.
The generator's two runtime dependencies were vendored by hand: two pkgs.fetchurl calls with npm integrity strings copied out of the pinned models-dev input's bun.lock, extracted into a flat node_modules with tar. Bumping the input meant transcribing hashes between two lockfiles. Resolve them through bun2nix instead, from a package.json that pins the same remeda 2.33.7 and zod 3.24.2 that input's lockfile records. The generated bun.nix carries hashes identical to the fetchurl calls it replaces, and both snapshots the build emits are unchanged. Also move the tool into nix/tools/models-dev-gen so its Nix expression, the scripts it copies into the workspace and its dependency manifests sit together: models-dev-pricing.nix becomes default.nix, models-dev-gen.ts becomes gen.ts, and models-dev-compact.ts drops its now-redundant prefix. The remaining references follow: the node test invocation in the justfile and in the git hook, the tsconfig exclude and the oxlint ignore pattern.
Each tool under nix/tools now carries a bun.lock and the bun.nix that bun2nix derived from it. Nothing tied the two together: Renovate bumps bun.lock without knowing bun.nix exists, and a stale pair only shows up as a confusing "install wants the network" failure inside the sandbox. Add a bun-nix flake check that re-derives bun.nix from each committed bun.lock and diffs the result. bun2nix only parses the lockfile, so this needs no network access and runs in the sandbox like the other checks. `just gen-bun-nix` is the fix the check points at: it re-resolves every tool and regenerates its bun.nix. Both bun and the bun2nix CLI join the dev shell so that recipe can run.
23b7d24 moved nix/models-dev-gen.ts to nix/tools/models-dev-gen/gen.ts and updated nix/oxlint-check.json, but .oxlintrc.json carries its own copy of the same ignore list and was left pointing at the old path. `just typecheck` therefore started type-checking the generator, which imports `./packages/core/src/generate.ts` from the upstream models.dev worktree it is copied into at build time. That module does not exist in this repository, so the file cannot type-check here by construction.
nix/tools/publint/default.nix held the whole recipe for turning a bun2nix-resolved node_modules tree into a wrapped CLI, including the non-obvious part: bun2nix bakes `bun-with-fake-node` into the shebangs of the dependencies it stages, so a Node CLI has to be repointed at real Node or it runs under Bun's emulation and drags bun into the runtime closure. That is exactly the kind of detail the next tool would silently omit, so move it into nix/bun-cli.nix. The tool's own expression shrinks to the package name and its metadata. `runtimeInputs` is there for CLIs that shell out to other programs; publint does not need it.
changelogithub was a root devDependency purely so the release workflow could reach it through `nix develop --command pnpm changelogithub`: a full dev shell plus a pnpm workspace install, to run one CLI that touches nothing in this repository's source. Move it to nix/tools/changelogithub and run it as `nix run .#changelogithub`. It shells out to git to resolve the release tag, so git is wired into the wrapper instead of being inherited from the runner. This also retires the `changelogithub>c12` pnpm override. That existed only to keep chokidar 4.0.3 out of pnpm-lock.yaml, which pnpm rejected under `trustPolicy: no-downgrade` (see f7f7be5). bun resolves c12 3.3.4 and chokidar 5.0.0 on its own, so the constraint is satisfied by plain resolution rather than by an override. Dropping the dependency removes 751 lines from pnpm-lock.yaml.
zizmor 1.26.1 predates the parallel-step keys GitHub Actions shipped on 2026-06-25, so it failed schema validation on ci.yaml and audited nothing there. That gap was invisible: treefmt hands the matched files to zizmor in batches, and a batch containing any other workflow still exits zero, so the warning about ci.yaml scrolled past while its findings went uncollected. A batch consisting of ci.yaml alone exits non-zero instead, which is how it finally surfaced. zizmor 1.27.0 added experimental support for parallel steps, and nixpkgs now carries it. ci.yaml audits clean with no findings, so this turns an unaudited workflow into an audited one rather than merely quieting an error.
pkg-pr-new was a root devDependency, so every workspace install carried it even though the preview-release CI job is the only thing that runs it and it resolves nothing against the workspace. Fetch it with `pnpm dlx` at the point of use instead. The version is pinned in the workflow because Renovate does not update `dlx` specifiers, which is noted next to the call so the pin does not quietly rot.
The root package.json carried @types/node solely so the root tsconfig could set `types: ["node"]`, and that tsconfig existed solely to cover `nix/**/*.ts`. With the last of those files now living under nix/tools/models-dev-gen, both belonged there rather than at the root: no `*.ts` file sits at the repository root, and apps/ccusage and docs each declare @types/node themselves. So the tool directory gets its own tsconfig, standalone like the other two in this repository, and pins @types/node next to the dependencies it already pins. gen.ts stays excluded for the same reason the oxlint config ignores it. The root package.json is left with no devDependencies at all. `just typecheck` still resolves node types for compact.test.ts, now out of nix/tools/models-dev-gen/node_modules, which `just gen-bun-nix` populates.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
ccusage-guide | 1fdc668 | Commit Preview URL Branch Preview URL |
Jul 25 2026, 09:11 PM |
📝 WalkthroughWalkthroughThe PR relocates models.dev generation into a Bun-backed Nix tool, adds shared Bun dependency and CLI builders, exposes new package outputs, and updates development, validation, release, publishing, formatting, and cache workflows. ChangesNix and Bun tooling migration
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
ℹ️ Minor suggestions only.
Reviewed changes — refactors Nix-built JS tooling (publint, models-dev-pricing generator, changelogithub) from hand-maintained Nix dependency expressions to bun2nix with real bun.lock/bun.nix files.
- Add
nix/bun-node-modules.nixandnix/bun-cli.nix— shared builders that turnnix/tools/*manifests into reproduciblenode_modulestrees and CLI wrappers, with shebang repointing to keep bun out of runtime closures. - Restructure tooling under
nix/tools/—publint,models-dev-gen, andchangelogithubeach get a self-contained directory withpackage.json,bun.lock,bun.nix, anddefault.nix; the oldnix/publint.nixandnix/models-dev-pricing.nixare removed. - Add
bun-nixflake check — fails onbun.lock/bun.nixdrift, paired withjust gen-bun-nixfor regeneration. - Remove root
devDependencies—changelogithub,pkg-pr-new, and@types/nodemove out of the pnpm workspace;pkg-pr-newis now fetched viapnpm dlx. - Clean up
pnpm-lock.yaml— removes thereleasecatalog, thechangelogithub>c12override, and 774 lines of changelogithub/changelogen transitive dependencies. - Move
@types/nodeandtsconfig.jsontonix/tools/models-dev-gen/where the lastnix/**/*.tsfiles live; roottsconfig.jsonno longer includesnix/**/*.ts. - Incidental fixes: zizmor 1.27 (
ci.yamlparallel-step support), oxlint--no-error-on-unmatched-pattern, nix-store cache fallback restore key.
ℹ️ Nitpicks
nix/tools/models-dev-gen/package.json:5— thedescriptionfield references the old file pathnix/models-dev-gen.ts; after the move it should readgen.tsornix/tools/models-dev-gen/gen.ts.
@v0 or keep the SHA fresh with Dependabot | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
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 @.github/workflows/ci.yaml:
- Around line 278-282: Add pkg-pr-new as a root devDependency so it is recorded
in pnpm-lock.yaml, then update the workflow command in the publish step to
invoke it through pnpm exec rather than pnpm dlx, preserving the existing
publish arguments.
In `@nix/tools/models-dev-gen/package.json`:
- Around line 6-12: Move the runtime libraries remeda and zod from dependencies
into devDependencies in the package manifest, keeping their existing versions
and retaining `@types/node` alongside them.
🪄 Autofix (Beta)
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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ba9a02e3-53a7-40a9-b214-5496535a919f
⛔ Files ignored due to path filters (5)
flake.lockis excluded by!**/*.locknix/tools/changelogithub/bun.lockis excluded by!**/*.locknix/tools/models-dev-gen/bun.lockis excluded by!**/*.locknix/tools/publint/bun.lockis excluded by!**/*.lockpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (31)
.github/actions/setup-nix-cache/action.yaml.github/workflows/ci.yaml.github/workflows/release.yaml.oxlintrc.jsonflake.nixjustfilenix/bun-cli.nixnix/bun-node-modules.nixnix/checks.nixnix/dev-shell.nixnix/git-hooks.nixnix/models-dev-pricing.nixnix/oxlint-check.jsonnix/packages.nixnix/publint.nixnix/tools/changelogithub/bun.nixnix/tools/changelogithub/default.nixnix/tools/changelogithub/package.jsonnix/tools/models-dev-gen/bun.nixnix/tools/models-dev-gen/compact.test.tsnix/tools/models-dev-gen/compact.tsnix/tools/models-dev-gen/default.nixnix/tools/models-dev-gen/gen.tsnix/tools/models-dev-gen/package.jsonnix/tools/models-dev-gen/tsconfig.jsonnix/tools/publint/bun.nixnix/tools/publint/default.nixnix/tools/publint/package.jsonnix/treefmt.nixpackage.jsonpnpm-workspace.yaml
💤 Files with no reviewable changes (4)
- nix/publint.nix
- package.json
- pnpm-workspace.yaml
- nix/models-dev-pricing.nix
Both nix/tools manifests described themselves in terms of the file layout that existed before the move: models-dev-gen pointed at `nix/models-dev-gen.ts` and publint at `nix/publint.nix`. Neither path exists any more; each tool's Nix expression and scripts are siblings of the manifest now. Reported on #1489 by pullfrog for the first of the two; the second is the same mistake, found by grepping for the pre-rename paths.
ccusage
@ccusage/ccusage-darwin-arm64
@ccusage/ccusage-darwin-x64
@ccusage/ccusage-linux-arm64
@ccusage/ccusage-linux-x64
@ccusage/ccusage-win32-x64
commit: |
There was a problem hiding this comment.
All reported issues were addressed
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
ccusage performance comparisonPR SHA: This compares the Rust PR release binary against the configured base package on the same CI runner. Package runtime diagnosticsCompares the PR package wrapper, the installed native optional dependency binary, and the workspace release binary on the same large fixture. This identifies whether slow package results come from JavaScript wrapper overhead, the published native binary build, or the Rust core itself. Fixtures: Claude
Committed fixture performanceCommitted small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage. Fixtures: Claude
Large real-world-shaped fixture performanceGenerated fixtures shaped from aggregate local log statistics: thousands of JSONL files, many small sessions, and a long tail of larger sessions. No real prompts, paths, or outputs are stored in the fixtures. Fixtures: Claude
Artifact size
Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees. |
ccusage performance comparisonPR SHA: This compares the PR package against the configured base package on the same CI runner. Package runtime diagnosticsCompares the PR package wrapper, the installed native optional dependency binary, and the workspace release binary on the same large fixture. This identifies whether slow package results come from JavaScript wrapper overhead, the published native binary build, or the Rust core itself. Fixtures: Claude
Committed fixture performanceCommitted small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage. Fixtures: Claude
Large real-world-shaped fixture performanceGenerated fixtures shaped from aggregate local log statistics: thousands of JSONL files, many small sessions, and a long tail of larger sessions. No real prompts, paths, or outputs are stored in the fixtures. Fixtures: Claude
Artifact size
Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees. |
CONTRIBUTING tells contributors to run `just typecheck`, and moving @types/node into nix/tools/models-dev-gen broke that on a fresh checkout: oxlint resolves that directory's types out of its own node_modules, which nothing created. `pnpm install` does not cover it, since the tool directories sit outside the pnpm workspace. Consolidate setup into `just install`: the workspace install plus a `bun install --frozen-lockfile` for every tool directory carrying a tsconfig. Nix builds each tool's dependencies itself, so the ones with no TypeScript in them need nothing locally and are skipped. `git wt`'s hook now calls the recipe instead of running pnpm directly, so new worktrees stay usable without a manual step, and CONTRIBUTING lists it ahead of the checks it is a prerequisite for.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — one new commit adds a just install recipe and integrates it into the contributor workflow.
- Added
just installrecipe — runspnpm install --frozen-lockfileplusbun install --frozen-lockfilefor eachnix/tools/*/tsconfig.jsondirectory, sojust typecheckworks on a fresh checkout. - Updated
CONTRIBUTING.md—just installis now the first step in the pre-submit checklist, with a note that it's only needed once per checkout and thatgit wtruns it automatically. - Expanded
git wthook innix/dev-shell.nix— changed from barepnpm installtojust install, so worktrees created bygit wtare typecheck-ready.
@v0 or keep the SHA fresh with Dependabot | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
ccusage performance comparisonPR SHA: This compares the Rust PR release binary against the configured base package on the same CI runner. Package runtime diagnosticsCompares the PR package wrapper, the installed native optional dependency binary, and the workspace release binary on the same large fixture. This identifies whether slow package results come from JavaScript wrapper overhead, the published native binary build, or the Rust core itself. Fixtures: Claude
Committed fixture performanceCommitted small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage. Fixtures: Claude
Large real-world-shaped fixture performanceGenerated fixtures shaped from aggregate local log statistics: thousands of JSONL files, many small sessions, and a long tail of larger sessions. No real prompts, paths, or outputs are stored in the fixtures. Fixtures: Claude
Artifact size
Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees. |
ccusage performance comparisonPR SHA: This compares the PR package against the configured base package on the same CI runner. Package runtime diagnosticsCompares the PR package wrapper, the installed native optional dependency binary, and the workspace release binary on the same large fixture. This identifies whether slow package results come from JavaScript wrapper overhead, the published native binary build, or the Rust core itself. Fixtures: Claude
Committed fixture performanceCommitted small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage. Fixtures: Claude
Large real-world-shaped fixture performanceGenerated fixtures shaped from aggregate local log statistics: thousands of JSONL files, many small sessions, and a long tail of larger sessions. No real prompts, paths, or outputs are stored in the fixtures. Fixtures: Claude
Artifact size
Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees. |
ccusage performance comparisonPR SHA: This compares the Rust PR release binary against the configured base package on the same CI runner. Package runtime diagnosticsCompares the PR package wrapper, the installed native optional dependency binary, and the workspace release binary on the same large fixture. This identifies whether slow package results come from JavaScript wrapper overhead, the published native binary build, or the Rust core itself. Fixtures: Claude
Committed fixture performanceCommitted small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage. Fixtures: Claude
Large real-world-shaped fixture performanceGenerated fixtures shaped from aggregate local log statistics: thousands of JSONL files, many small sessions, and a long tail of larger sessions. No real prompts, paths, or outputs are stored in the fixtures. Fixtures: Claude
Artifact size
Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees. |
ccusage performance comparisonPR SHA: This compares the PR package against the configured base package on the same CI runner. Package runtime diagnosticsCompares the PR package wrapper, the installed native optional dependency binary, and the workspace release binary on the same large fixture. This identifies whether slow package results come from JavaScript wrapper overhead, the published native binary build, or the Rust core itself. Fixtures: Claude
Committed fixture performanceCommitted small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage. Fixtures: Claude
Large real-world-shaped fixture performanceGenerated fixtures shaped from aggregate local log statistics: thousands of JSONL files, many small sessions, and a long tail of larger sessions. No real prompts, paths, or outputs are stored in the fixtures. Fixtures: Claude
Artifact size
Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees. |

Summary
The Nix side of this repository built JS tooling by hand-maintaining dependency
metadata inside Nix expressions.
nix/publint.nixcarried a fullpnpm-lock.yamlas a heredoc fed tofetchPnpmDeps, andnix/models-dev-pricing.nixvendoredremedaandzodthroughfetchurlcalls whose integrity hashes were transcribed out of the pinned
models-devinput's lockfile. Both are now resolved by bun2nix
from real
bun.lockfiles, and each tool is a self-contained directory undernix/tools/.changelogithubmoves there too, which — together with runningpkg-pr-newvia
pnpm dlxand relocating@types/node— leaves the rootpackage.jsonwith no
devDependenciesat all and removes 774 lines frompnpm-lock.yaml.The pnpm workspace itself, package publishing, and the
pnpm dlx ccusageend-user documentation and E2E matrix are untouched.
What Changed
Each tool directory holds its
package.json, thebun.lockthatbun installresolved from it, the
bun.nixthatbun2nixderived from that lockfile, andits own Nix expression:
nix/tools/publint/— replaces the inlinepnpm-lock.yaml+fetchPnpmDepsnix/tools/models-dev-gen/— replaces the hand-pinnedfetchurltarballs;also absorbs
models-dev-gen.ts(nowgen.ts) andmodels-dev-compact.tsnix/tools/changelogithub/— new;release.yamlnow runsnix run .#changelogithubinstead ofnix develop --command pnpm changelogithubShared pieces:
nix/bun-node-modules.nixturns a tool manifest into anode_modulestreenix/bun-cli.nixwraps a CLI from one, so the non-obvious part is written once:bun2nix bakes
bun-with-fake-nodeinto the shebangs of the dependencies itstages, so a Node CLI must be repointed at real Node or it runs under Bun's
emulation and drags bun into the runtime closure
bun-nixflake check re-derives eachbun.nixfrom itsbun.lockand failson drift, because Renovate bumps
bun.lockwithout knowingbun.nixexists.just gen-bun-nixis the fix it points atbunand thebun2nixCLI join the dev shellDependency cleanup:
pkg-pr-new→pnpm dlx [email protected]at its single call site. Renovatedoes not update
dlxspecifiers, which is noted next to the callchangelogithub>c12pnpm override is retired. It existed only to keepchokidar 4.0.3 out of
pnpm-lock.yamlundertrustPolicy: no-downgrade(fix(deps): avoid c12 chokidar trust downgrade #1159); bun resolves c12 3.3.4 and chokidar 5.0.0 unaided
@types/nodemoves tonix/tools/models-dev-gen, which is where the lastnix/**/*.tsfiles live. The roottsconfig.jsonexisted only to cover them,so it becomes that directory's
tsconfig.jsonjust installconsolidates setup: the workspace install plus abun installfor each tool directory that carries a tsconfig. Thosedirectories are outside the pnpm workspace, so without it
just typecheck—which CONTRIBUTING tells contributors to run — fails on a fresh checkout.
git wt's hook calls the recipeIncidental Fixes
Three pre-existing problems surfaced while getting the above to pass:
ci.yaml. 1.26.1 predates thebackground:/wait-all:parallel-step keys, so it rejected the file at schema validation. That was
invisible because treefmt batches files and a batch containing any other
workflow still exits zero. nixpkgs now carries 1.27.0, which added experimental
parallel-step support;
ci.yamlaudits clean, so an unaudited workflow becomesan audited one.
with its own
--no-error-on-unmatched-pattern.CI to a cold store even when it only added developer tooling.
Testing
nix flake checkpasses, including the newbun-nixchecknix/tools/models-dev-genemits byte-identicalmodels-dev-pricing.jsonandcodex-auto-review-fallbacks.jsonto the committed snapshotspublint's runtime closure is bash + nodejs only, with no bun referencechangelogithub --dryresolves tags, reaches the GitHub API and renders notespnpm dlx [email protected]was checked against this repository's own pnpmsettings, so
strictDepBuilds/blockExoticSubdepsdo not reject itjust typecheckandjust test-nodepass; several were verified negativelytoo — a deliberate type error in
compact.test.tsis caught, a corruptedbun.nixfails the drift check, and removing the tool'snode_modulesreproduces the fresh-checkout failure that
just installfixesevaluates on its own
Summary by CodeRabbit
just install.just install,just fmtguidance, and Bun/Nix generation support).