How I Published My Rust Bun Version Manager (bum) CLI to NPM Package

Published: (December 6, 2025 at 11:49 PM EST)
2 min read
Source: Dev.to

Source: Dev.to

Background

I built a CLI called bum – a fast Bun version manager written in Rust. It works great locally, but I wanted anyone to be able to run:

npx @owenizedd/bum use 1.3.3

without installing Rust or compiling anything. Turns out this is totally possible! I learned about it from an article by lekoarts.de about publishing Rust CLIs on npm using napi‑rs.

The Setup

Add napi-rs to your Rust project

# Cargo.toml
[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
napi = "2.16"
napi-derive = "2.16"

[build-dependencies]
napi-build = "2.1"

Wrap your CLI in a napi function

// src/lib.rs
use napi_derive::napi;

#[napi]
pub fn run(args: Vec) -> napi::Result {
    let rt = tokio::runtime::Runtime::new()
        .map_err(|e| napi::Error::from_reason(format!("Failed to create runtime: {}", e)))?;

    // Your CLI logic here
    rt.block_on(async {
        // run_commands(args).await
    });

    Ok(())
}

Create a simple bin.js

#!/usr/bin/env node

const { run } = require("./index");
const args = process.argv.slice(2);

try {
  run(args);
} catch (e) {
  console.error(e);
  process.exit(1);
}

Configure package.json

{
  "name": "@owenizedd/bum",
  "bin": "bin.js",
  "napi": {
    "name": "bum",
    "triples": {
      "defaults": true,
      "additional": [
        "aarch64-apple-darwin",
        "x86_64-apple-darwin",
        "x86_64-unknown-linux-musl",
        "aarch64-unknown-linux-gnu"
      ]
    }
  }
}

OpenSSL Cross‑Compilation Hell

reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
zip = { version = "1.1", default-features = false, features = ["deflate"] }

Each platform (e.g., darwin-arm64, linux-x64-gnu) is published as a separate npm package. Initially we forgot to update their package.json versions, so npm couldn’t find them.

Fix: Sync platform package versions in CI

- name: Sync platform package versions
  run: |
    VERSION=$(node -p "require('./package.json').version")
    for dir in npm/*/; do
      node -e "
        const fs = require('fs');
        const pkg = JSON.parse(fs.readFileSync('$dir/package.json'));
        pkg.version = '$VERSION';
        fs.writeFileSync('$dir/package.json', JSON.stringify(pkg, null, 2));
      "
    done

Bundling Pitfalls

We initially tried to compile TypeScript with bun build bin.ts. Bun inlined require('./index') and hard‑coded the .node filename with a hash, producing broken code:

// Broken bundling result
nativeBinding = require("./bum.darwin-arm64-7xvffnqw.node");

The correct (working) import should be:

// Correct import
nativeBinding = require("./bum.darwin-arm64.node");

Solution: Use plain JavaScript for bin.js and avoid bundling.

Result

Now anyone can run:

npx @owenizedd/bum use 1.3.3

and it works on macOS, Linux, and Windows (still being tested on other platforms).

References

Back to Blog

Related posts

Read more »