Announcing aubeshim v1.0.0

Published:
Keywords: webdev, rust

Contents

Introduction

I’m happy to announce aubeshim v1.0.0, the first stable release of a small Rust tool I’ve been using on my own machines for a while. This is the first time I’ve written about it here, so I’ll start with what it is, why I built it, and what it does.

What Is aubeshim?

aube is a fast JavaScript package manager from jdx (the author of mise). It offers quick installs, a strict node_modules layout, and run-time checks that auto-install missing dependencies when you run a script.

aubeshim is a separate project (not affiliated with jdx or aube) that installs PATH shims named bun, bunx, npm, npx, pnpm, pnpx, pnx, and yarn. When you run one of those commands in a directory where aubeshim is active, it routes the invocation to aube if the command shape is compatible. Otherwise it falls back to the real package manager binary.

The point is to get aube’s benefits without editing every project’s package.json scripts or retraining your muscle memory. npm install, pnpm add, bun run build, and npx eslint can all go through aube when the shim recognizes the command.

Why Use It?

If you work across many JavaScript repositories, you probably have a mix of package-lock.json, pnpm-lock.yaml, bun.lock, and yarn.lock files. Each package manager keeps its own copy of dependencies under node_modules, and those trees add up fast. On a machine with dozens of checkouts, it’s easy to end up with hundreds of gigabytes of duplicate packages.

aubeshim doesn’t magically unify every lockfile format, but it does let you standardize on aube for day-to-day installs and script runs in the directories you choose, while still falling back to the real npm, pnpm, bun, or yarn binary when a command needs exact package-manager behavior. Registry queries, publish commands, and tool-specific flags that aube doesn’t model all pass through transparently.

I also wanted something that cooperates with mise. aubeshim discovers real package-manager binaries through mise which first, auto-detects whether mise or aube owns each global npm CLI, and activates cleanly after mise activate. You keep your existing tool versions; aubeshim just intercepts compatible invocations on the way to them.

What It Can Do

Here’s a quick tour of the routing behavior:

  • Local installs and scripts run through aube. That covers npm install, npm ci, npm run build, pnpm install, bun add, and similar commands.
  • Package edits are normalized to aube’s vocabulary. npm install lodash becomes aube add lodash, and npm uninstall lodash becomes aube remove lodash.
  • For npm compatibility, shimmed npm commands set AUBE_NODE_LINKER=hoisted unless you already set a node-linker env var, so the resulting node_modules tree looks like what npm users expect.
  • One-off runners such as npx, bunx, pnpx, and pnpm dlx route to aube dlx when the flags are compatible. No-install modes map to aube exec --no-install.
  • Since aube already speaks a pnpm-compatible command surface, pnpm mostly passes through directly.
  • Common Yarn package-manager and script commands route to aube; Yarn-only management commands fall back to real Yarn.
  • bun install, bun add, and bun run go through aube, while other Bun runtime commands and unknown subcommands fall back to the real binary.
  • Global npm CLI commands use global_packages = "auto" by default. aubeshim checks whether mise or aube owns the package and routes add/install/remove/outdated accordingly. Registry metadata commands like npm view ... --json still hit real npm so tools like mise can parse the response.

You control where shimming happens with a TOML config file. Globs match the current working directory or any ancestor, so a rule on ~/devel/projects/** covers nested packages inside a monorepo:

enabled = true
default = true
global_packages = "auto"

ignore = [
  "~/devel/work/broken-expo",
]

shim = [
  "~/devel/projects/**",
]

Commands run from ignored directories pass straight through to the real package manager. Everything else uses aube when default = true and no more specific shim glob applies.

Getting Started

Install with Cargo and create the shims:

cargo install aubeshim
aubeshim install --force

Activate aubeshim after mise (mise installs its own package-manager shims, so aubeshim needs to come last):

eval "$(mise activate zsh --shims)"
eval "$(aubeshim activate zsh)"

If you use full mise activate without --shims, add --persistent to the aubeshim line:

eval "$(mise activate zsh)"
eval "$(aubeshim activate zsh --persistent)"

From a source checkout, ./install.sh builds the binary and replaces shims in ~/.local/share/aubeshim/shims.

Run npm --version (or bun --version, etc.) inside a shimmed directory and you should see the real package manager version plus a parenthesized hint showing the aubeshim and aube versions.

Compatibility Notes

aubeshim is opinionated about when to shim and when to fall back, but migrating existing projects to aube can still surface dependency layout differences. aube’s strict layout rejects imports of packages that aren’t declared in your own package.json, even if npm-style hoisting made them work before. Adding direct dependencies is usually the right fix; a hoisted linker profile is available as a fallback for messier trees.

Lockfile compatibility is a separate question. A hoisted aube install might produce a package-lock.json that npm coworkers need to validate with npm ci. If your team isn’t ready to switch lockfiles, keep npm responsible for writing the lockfile, or import to a native aube-lock.yaml when you’re ready to commit fully.

I’ve been tracking upstream interop findings in aube-issues as I run into them.