One CLI. Detect the package manager. Translate the intent. Run the right command.
zpm is a fast, polished, cross-platform Rust CLI that abstracts over the modern JavaScript/TypeScript package-manager ecosystem.
zpm install
zpm add react
zpm add typescript --dev
zpm run dev
zpm exec vite --host 0.0.0.0
zpm update
zpm remove react
zpm dedupezpm detects your project's package manager (npm, pnpm, Yarn Classic, Yarn Berry, Bun, Deno, Aube, Nub, Rush/pnpm) and translates the generic command to the correct native invocation:
zpm add react
# β npm install react
# β pnpm add react
# β yarn add react
# β bun add react
# β deno add npm:react (handled as deno add)
# β aube add react- Zero Node.js requirement β native binary, instant startup
- Robust detection β lockfiles,
packageManagerfield,devEngines,deno.json, workspace files - Reliable argument forwarding β correct handling of
--for npm and other managers - Great terminal UX β colors, interactive pickers, dry-run previews, useful errors
- Workspace-aware β nested packages resolve to workspace root,
--roottargets root explicitly - CI-friendly β respects
NO_COLOR, non-interactive terminals, and provides clean output
cargo install zpmgit clone https://github.com/zomeru/zpm
cd zpm
cargo build --release
# binary at target/release/zpmPrebuilt binaries for x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, x86_64-apple-darwin, aarch64-apple-darwin, x86_64-pc-windows-msvc, aarch64-pc-windows-msvc are attached to GitHub Releases.
Future distribution will include:
brew install zpm
scoop install zpm
winget install zpm| Manager | Lockfiles | Global | Frozen | Dedupe | Execute |
|---|---|---|---|---|---|
| npm | package-lock.json, npm-shrinkwrap.json |
β (-g) |
npm ci |
npm dedupe |
npx |
| pnpm | pnpm-lock.yaml, pnpm-workspace.yaml |
β | pnpm i --frozen-lockfile |
pnpm dedupe |
pnpm dlx / pnpm exec |
| pnpm@6 | pnpm-lock.yaml + packageManager: pnpm@6 |
β | same | β | same |
| pnpm via Rush | rush.json, common/config/rush/pnpm-lock.yaml |
β | same via rush-pnpm |
β | same via rush-pnpm |
| Yarn Classic | yarn.lock + packageManager: yarn@1 |
yarn global add |
--frozen-lockfile |
β | npx / yarn exec -- |
| Yarn Berry | yarn.lock + packageManager: yarn@2+ or .yarnrc.yml |
via npm i -g |
--immutable |
yarn dedupe |
yarn dlx / yarn exec |
| Bun | bun.lock, bun.lockb |
β | --frozen-lockfile |
β | bun x |
| Deno | deno.lock, deno.json |
deno install -g |
--frozen |
β | deno x / deno task --eval |
| Aube | aube-lock.yaml, aube-workspace.yaml |
β | --frozen-lockfile |
β | aube dlx / aube exec |
| Nub | nub.lock |
β | --frozen-lockfile |
β | nubx / nub exec |
Adding a new manager is straightforward: implement its capability table in src/package_manager/mod.rs β no scattered if/else blocks.
zpm walks from the current directory up to the filesystem root and checks, in order at each directory:
-
rush.jsonβpnpm-rush -
Lockfiles (
pnpm-lock.yaml,yarn.lock,bun.lockb, etc.) β if apackage.jsonwith apackageManagerfield exists in the same directory, that takes precedence -
packageManager/devEngines.packageManagerinpackage.json:{ "packageManager": "pnpm@9.1.0" } { "packageManager": "yarn@3.2.0" } { "devEngines": { "packageManager": { "name": "pnpm", "version": "9.0.0" } } }pnpm@<7βpnpm@6(legacypnpm i --frozen-lockfilebehavior with different run semantics)yarn@>1βyarn@berry
-
Install metadata (
node_modules/.pnpm,node_modules/.yarn-state.yml,.pnp.cjs, etc.) -
deno.json/deno.jsonc(checked early for the target directory, then again as ancestor fallback)
If detection is ambiguous (multiple lockfiles in the same directory) zpm uses deterministic precedence mirroring package-manager-detector and will surface a prompt when running in an interactive terminal.
Deno shortcut: deno.json in the current directory short-circuits to deno immediately, matching ni.
zpm --pm pnpm add react # force pnpm
zpm --pm bun install
ZPM_PM=pnpm zpm install # env overrideAll commands support --dry-run (show without executing) and --verbose (show detection + command).
zpm --help
zpm add --helpInstall dependencies.
zpm install
zpm install --frozen
zpm install --frozen-if-present
zpm install --production # npm β --omit=dev
zpm i -P # aliasAdd dependencies. Flags mirror common PM conventions:
zpm add react react-dom
zpm add typescript --dev # -D
zpm add typescript -D # bun β -d automatically
zpm add eslint --peer
zpm add eslint --exact -E
zpm add react --global -g # globalzpm remove react
zpm rm react lodash
zpm remove --global eslint
zpm remove # interactive multi-select (TTY)
zpm remove -i # explicit interactivezpm update
zpm update react
zpm update -i --latest # interactive latest
zpm up -iNote: npm has no upgrade-interactive; zpm falls back to npm update.
Run scripts. Handles npm's -- insertion and workspace flags:
zpm run dev
zpm run dev --port 3000 # npm β npm run dev -- --port 3000
zpm run build --watch -o
zpm run dev -- --port 3000 # explicit --
zpm run -w packages/foo test # before script β -w=packages/foo test
zpm run test -w packages/foo # after script β test -- -w=packages/foo
zpm run --if-present test
zpm run # interactive fuzzy picker
zpm run -p dev # strip -p monorepo prefixzpm exec vite
zpm x vite --host 0.0.0.0 # pnpm β pnpm dlx vite ...
zpm exec --local esbuild # pnpm β pnpm exec, yarn classic β yarn exec -- --version
zpm x --local vitestzpm dedupe
zpm dedupe --check -c # npm β --dry-run, pnpm/aube β --checkUnsupported managers (e.g., bun dedupe, deno dedupe) return a clear error: Γ dedupe not supported for bun.
Clean (frozen) install β alias for --frozen:
zpm clean
# npm β npm ci
# pnpm β pnpm i --frozen-lockfile
# yarn classic β yarn install --frozen-lockfile
# yarn berry β yarn install --immutable
# bun β bun install --frozen-lockfile
# deno β deno install --frozenPrint detected agent name (for scripting):
zpm agent
# npmzpm # β install
zpm react # β add react
zpm react lodash # β add react lodash
zpm --frozen # β frozen install
zpm -g eslint # β global addzpm preserves arguments as separate argv entries via std::process::Command β no shell-string concatenation.
zpm run dev -- --port 3000andzpm run dev --port 3000behave correctly per manager (npm requires--before forwarded args, others do not)zpm add react react-domforwards both as packageszpm exec vite --host 0.0.0.0forwards correctly;yarn execuses--when needed
zpm detects workspace roots via:
pnpm-workspace.yaml.yarnrc.ymlrush.jsondeno.json/deno.jsoncpackage.jsonworkspacesfield
From a nested package, commands resolve against the workspace root. --root explicitly targets the root:
zpm --root installPackage script discovery also supports monorepo package selection via interactive prompts when multiple package.json files are found.
zpm includes architecture for modern dependency catalogs:
- pnpm catalogs (
pnpm-workspace.yamlcatalog/catalogs) - Yarn Berry catalogs (
.yarnrc.yml) - Bun catalogs (
package.jsonworkspaces.catalog/ top-levelcatalog/catalogs)
Current implementation provides provider detection and catalog: reference generation; full read-modify-write of workspace catalog files (with proper YAML/JSON serialization and preservation) is scaffolded for the next milestone. The architecture avoids brittle string replacement.
To disable catalog mode: ZPM_CATALOG=false or NI_CATALOG=false.
zpm reads (first existing wins):
~/.config/zpm/config.toml~/.nirc(ini, fornicompatibility)
Example ~/.config/zpm/config.toml:
default_manager = "pnpm"
global_manager = "pnpm"
interactive = true
color = "auto" # auto | always | never
auto_install = false # if true, auto-install missing PM via Corepack/npm
catalog = true| Variable | Description |
|---|---|
ZPM_DEFAULT_MANAGER |
Default manager when none detected (also NI_DEFAULT_AGENT) |
ZPM_GLOBAL_MANAGER |
Manager for -g operations (also NI_GLOBAL_AGENT) |
ZPM_CONFIG |
Custom config file path (also NI_CONFIG_FILE; "false" disables) |
ZPM_PM / zpm --pm |
Per-invocation override |
ZPM_AUTO_INSTALL |
true to auto-install missing PM |
ZPM_NO_INTERACTIVE |
true to disable prompts |
ZPM_CATALOG |
false to disable catalog mode (NI_CATALOG) |
ZPM_DRY_RUN |
true acts like --dry-run |
ZPM_VERBOSE |
true acts like --verbose |
NO_COLOR |
Disable colors (standard) |
CI |
In CI, missing PM causes immediate exit; default agent falls back to npm |
Precedence: CLI flags > environment > config file > defaults.
zpm add react --dry-run
# Detected package manager: pnpm
# Command: pnpm add react
zpm --verbose add react
# Detected: pnpm
# Command: pnpm add react
# (then executes)Both flags are available globally:
zpm --dry-run --pm pnpm add react
zpm --verbose run devWhen zpm detects a manager that is not installed:
- In CI: exits with helpful install hint
- Interactively: prompts to install globally via
npm i -g <manager>(requires confirmation) - With
ZPM_AUTO_INSTALL=true: installs automatically - Prefer Corepack-managed managers where applicable (
corepack enableis recommended for pnpm/Yarn)
Never installs silently unless explicitly enabled.
# Bash
zpm completion --bash >> ~/.bashrc
# Zsh
zpm completion --zsh >> ~/.zshrc
# Fish
zpm completion --fish >> ~/.config/fish/completions/zpm.fishBlock is also emitted for nr-compatible tooling via zpm completion.
zpm recreates ni's core purpose but with a deliberate, idiomatic Rust architecture:
| Aspect | ni |
zpm |
|---|---|---|
| Language | TypeScript / Node.js | Rust (native) |
| Startup | Node.js VM | < 5ms native |
| CLI style | Separate binaries (ni, nr, nlx, β¦) |
Unified zpm with subcommands + ni-compatible bare mode |
| Detection | package-manager-detector (JS) |
Native Rust port with same precedence |
| Config | ~/.nirc (ini) |
~/.config/zpm/config.toml + ~/.nirc compat + env |
| Catalogs | Full pnpm/Yarn/Bun support (recent) | Scaffolded provider model (full YAML/JSON impl next milestone) |
| Interactive | prompts + fzf |
dialoguer fuzzy/select/multi |
| Process | tinyexec (sh -c on some paths) |
std::process::Command argv-preserving, no shell |
| Global state | Module-level config | Explicit passing, no global mut |
| Windows | Supported via Node | First-class Rust PathBuf handling |
Behavioral compatibility: zpm aims for zpm result β ni result for all core translations. Intentional differences are documented:
zpmis a single binary;ni,nr,nlx, etc. map tozpm install/add,zpm run,zpm execyarn@berryglobal delegates tonpm i -g(Berry has no global) β same asni/package-manager-detectorpnpm@6is distinguished viapackageManager: pnpm@6for correctrundash semantics--dry-runis explicit (ni uses?token for debug)zpm cleanis an explicit alias for frozen install
See CONTRIBUTING.md for the full workflow. Quick start:
rustup show # installs toolchain from rust-toolchain.toml (stable + rustfmt + clippy)
# formatting / lint / test / build (same commands CI runs)
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features
cargo test --doc
cargo build --release
./target/release/zpm --help
# or with `just` (cargo install just)
just format-check
just lint
just test-all
just ci # full local CI validation
just dev --help # run zpm with args: cargo run -- --helpsrc/
main.rs
lib.rs
cli/ # Clap definitions + high-level resolvers (install/add/run/execβ¦)
config/ # TOML/ini + env precedence
detection/ # Lockfile/packageManager/devEngines/deno.json walk
package_manager/ # Agent enum + central COMMANDS capability table + resolve_command
process/ # std::process::Command execution (argv-preserving)
ui/ # Colors, prompts, spinners (respects NO_COLOR)
workspace/ # Workspace root + package.json helpers
catalog/ # Provider model for pnpm/Yarn/Bun catalogs
error.rs
MIT β see LICENSE.