learn / 07·7 — under wbx · distribution

one release,EVERYdoor

Distribution is the part where most tools fork into a dozen packaging pipelines that quietly disagree. Here there is exactly one — a GitHub Release tagged wbx-v*, holding five native binaries and one wasm module. curl, npm, self-upgrade, and the runtime image are all just couriers for the same bytes.

distribution12 min read
A single monumental glowing vault of identical golden ingots, with four small couriers each carrying one ingot out through four different doors into the dawn — bright, monumental vault dwarfing the figures, 1970s sci-fi style

three installs, three TOOLS

Every tool ecosystem hands you an install ritual, and the rituals disagree with each other. The Homebrew formula is three versions behind because a volunteer maintains it on weekends. The npm package wraps a native binary in a download step that fails silently — you find out at runtime, weeks later, when the command finally errors. The curl | sh one-liner works but feels like trusting a stranger with sudo. And six months on you have three copies of the same tool fighting over your PATH, and no honest answer to the only question that matters: which one does which wbx actually find?

The deeper problem is that each lane is usually its own build — its own pipeline, its own packaging, its own version cadence. Three pipelines is three things that can drift apart, and they do. The version brew ships, the version npm ships, and the version you'd get from source are three different numbers, and reconciling them is somebody's afternoon.

This lesson opens the hood on how wbx ships instead. The parent lesson gave you the install one-liners; this one is why there's only one set of bytes behind all of them.

the DEFINITION

dis·tri·bu·tion /ˌdɪs·trɪ·ˈbjuː·ʃən/ noun

1. one GitHub Release tagged wbx-v* that every install lane — curl, npm, self-upgrade, and the runtime image — is a courier for. Same bytes, different doors.

The word usually names a sprawl: registries, mirrors, package formats, channels. Here it names a single artifact. A tag triggers one CI workflow; that workflow builds the binaries once and attaches them to a Release; and every door into the tool reads from that Release. There is no second build, no parallel packaging job, nothing that can drift — because there is only one thing to keep honest, and CI keeps it.

anatomy of a RELEASE

A push of a tag like wbx-v0.13.0 is the whole trigger. The workflow strips the prefix to get the version, fans out across a five-target build matrix, runs one more job for the wasm module, and attaches all six files to a Release named for the tag:

flowchart TD
  tag["git tag wbx-v0.13.0"]
  tag --> ci["cli-release.yml
v = 0.13.0"] ci --> matrix["native matrix — 5 targets"] ci --> wasmjob["wasm job — wasm32-wasip1"] matrix --> rel[["Release wbx-v0.13.0
5 binaries + wbx.wasm"]] wasmjob --> rel rel --> curl["curl lane"] rel --> npm["npm lane"] rel --> upgrade["wbx upgrade"] style tag fill:#d9dbd3,stroke:#121316 style rel fill:#13d943,stroke:#121316,stroke-width:2.5px style curl fill:#ffffff,stroke:#121316 style npm fill:#ffffff,stroke:#121316 style upgrade fill:#ffffff,stroke:#121316

That graph is the entire mental model. One tag in; one Release out; the three install lanes fan from that single node. The matrix builds five binaries — Apple Silicon and Intel macOS, x64 and arm64 Linux, and an MSVC Windows .exe. The Intel mac binary is the one worth noting: it's cross-compiled on the Apple Silicon runner, because the Intel macOS runners are being retired and can queue indefinitely. The wasm job builds separately and drops a sixth asset, wbx.wasm.

Here is an actual release, laid bare, with each asset's door marked:

wbx-v0.13.0 — "wbx v0.13.0"
├── wbx-darwin-arm64        ← curl (Apple Silicon) · npm postinstall
├── wbx-darwin-x64          ← cross-compiled ON the arm64 runner
├── wbx-linux-x64
├── wbx-linux-arm64
├── wbx-windows-x64.exe     ← npm lane only (curl says: Windows via WSL)
└── wbx.wasm                ← wasm32-wasip1, for in-sandbox agents

Publishing the Release is deliberately idempotent. The job runs gh release create … || gh release upload … --clobber — create it if it's new, otherwise overwrite the assets in place. Re-running a release never errors and never half-publishes; you can rerun the workflow and get the same end state.

the curl LANE

curl -fsSL https://workbooks.sh/cli.sh | sh is the first door, and it is a plain POSIX script — about sixty lines, served as a static file. (The spec still mentions a Cloudflare worker; that's stale — there's no worker logic, just a file you can read.) It does four things in order, and each is auditable.

First it discovers the latest tag: it asks the GitHub API for the repo's releases and greps the first tag_name that starts with wbx-v. Second it maps your machine to an asset name — uname gives the OS and arch, which become darwin or linux and arm64 or x64, matching the asset names exactly. Third it downloads to a temp file, makes it executable, and moves it into place. Fourth it runs wbx --version as a smoke check, so a broken download fails right there instead of next Tuesday.

The install directory has a fallback built in: it uses /usr/local/bin if that's writable, otherwise ~/.local/bin — and if the chosen directory isn't on your PATH, it warns you. Three environment knobs steer the whole script:

env varwhat it doesdefault
WBX_VERSIONpin a specific release instead of latestlatest wbx-v* tag
WBX_INSTALL_DIRwhere the binary lands/usr/local/bin, else ~/.local/bin
WBX_REPOwhich repo's releases to readworkbooks-sh/workbooks.sh

One honest limit lives in the OS map: it's macOS and Linux only. An unrecognized OS exits with unsupported OS — macOS and Linux today; Windows via WSL. Windows users take the npm lane, or run the script inside WSL.

the npm LANE

The npm package is a thin launcher, not a copy of the binary. It ships exactly three files — bin/wbx.js, install.js, and a README — and a postinstall hook that downloads the right native binary the moment you npm install it. The clever part is how it finds the binary with no metadata service at all:

The CI workflow stamps the package's version straight from the tag. So the package that calls itself 0.13.0 can compute its own download URL — github.com/…/releases/download/wbx-v0.13.0/wbx-darwin-arm64 — from nothing but its own package.json version and your platform. The npm version and the release tag are lockstep by construction. There's no version endpoint, no manifest, no API call to resolve "latest" — the package already knows which Release it belongs to, because the tag is what minted it.

sequenceDiagram
  participant U as npm install
  participant I as install.js (postinstall)
  participant G as GitHub Release
  participant C as storage CDN
  participant L as bin/wbx.js
  U->>I: run postinstall
  I->>I: asset = platform + arch
  I->>G: GET …/wbx-v0.13.0/wbx-darwin-arm64
  G-->>I: 302 redirect
  I->>C: follow redirect (up to 10)
  C-->>I: binary bytes
  I->>I: write + chmod 0755
  I->>L: shadow check on PATH
  

Read that exchange as a story. The install runs the postinstall; the postinstall computes the asset name from your platform and architecture; it requests that asset from the Release. GitHub answers not with bytes but with a 302 redirect to its storage backend — so the installer follows redirects by hand, up to ten hops, with a recognizable user agent. The bytes arrive, it writes them and sets the executable bit, and then it runs one last safety check we'll come back to: the shadow check.

The launcher itself, bin/wbx.js, is thirty-seven lines. It checks that the binary exists, then separately that it's executable — two distinct failure messages, because the postinstall download likely failed and the binary exists but is not executable are different problems with different fixes. If all is well it spawns the native binary, inherits your terminal, and forwards the child's exit code back out. The Node process is a pass-through; the real wbx is the binary it launches.

the shadow CHECK

Depth rung. Here's the scenario the npm lane is built to survive: you curl-installed wbx months ago into /usr/local/bin, and today you run npm i -g @work.books/cli. Now there are two binaries on your PATH, and the one that wins is whichever directory comes first. The npm one you just installed might be invisible.

So findShadow walks every directory on your PATH, in order, looking for a file named wbx. If the first one it finds resolves to a different file than the one it just installed, that earlier path is shadowing the new install — and it says so, by name:

[wb] downloading wbx-darwin-arm64 (v0.13.0)…
[wb] installed.

╔══════════════════════════════════════════════════════════════╗
║  wb PATH SHADOW WARNING                                      ║
╚══════════════════════════════════════════════════════════════╝
  Another wb binary was found earlier on your PATH:
    /usr/local/bin/wbx
  It will shadow the npm-installed wb. To fix, either:
    • Remove or rename the old binary:  rm /usr/local/bin/wbx
    • Or adjust PATH so npm's bin dir comes first.

The predicate is the whole insight: the first wbx on PATH that isn't the one we just wrote is, by definition, the one that shadows it — because shells resolve left to right and stop at the first hit. Walking in PATH order and comparing resolved paths is exactly what the shell will do at call time.

PATH dir (in order)has wbx?verdict
/usr/local/binyes — the old curl oneshadows the new install
~/.npm-global/binyes — just installedtoo late, never reached
(if the first hit IS ours)oursno shadow — returns null

When the first hit is the freshly-installed binary, findShadow returns null and prints nothing. No shadow, no noise.

failing LOUD, exiting zero

Depth rung. The npm lane has a design stance about failure that's worth naming, because it's the opposite of how most postinstalls behave. A postinstall download must never brick npm install — if it exits non-zero, it takes the whole dependency install down with it, which is a terrible thing to do to someone who just wanted a CLI. But it also must never fail silently — the silent postinstall is exactly the failure mode that makes people distrust npm-wrapped binaries.

The resolution: on any download error, print a loud boxed banner, then process.exit(0). The banner is the signal; the zero keeps the install alive. And the banner is careful to tell you which of two situations you're in:

╔══════════════════════════════════════════════════════════════╗
║  wb install FAILED                                           ║
╚══════════════════════════════════════════════════════════════╝
  The wb binary was NOT downloaded. Running `wb` will fail.
  (Or: a previous binary is still present and may work.)

  To recover, either:
    • curl -fsSL https://workbooks.sh/cli.sh | sh
    • npm rebuild @work.books/cli

A previous binary is still present and may work is a very different message than the wb binary was NOT downloaded; running wb will fail — and the installer knows which to say. It hands you both recovery commands right there: the curl lane as an escape hatch, and npm rebuild to retry the postinstall.

And the launcher is the backstop for anything that slips past the installer. If you run wbx and the binary is missing or not executable, bin/wbx.js prints its own fatal banner — with the same two fix options — and exits 1. Loud at install time, loud at run time, never silent at either.

the binary replaces ITSELF

The third lane is wbx upgrade — the tool swapping itself for a newer copy. It locates its own executable on disk, then does the most important thing first: it checks whether it lives inside a node_modules directory. If any component of its path is node_modules, it stops and tells you to use npm instead:

$ wbx upgrade                       # curl-installed at /usr/local/bin/wbx
upgraded: wbx 0.12.0 → 0.13.0       # wrote wbx.new, chmod 755, atomic rename

$ wbx upgrade                       # npm-installed (path has node_modules)
installed via npm — upgrade with: npm update -g @work.books/cli

That refusal is a feature, not a gap. File ownership inside node_modules belongs to the package manager; a binary that rewrites itself there would be fighting npm over which copy is real. So it declines, and points you at the command that respects the package manager's bookkeeping.

For a curl-installed binary, it discovers the latest release the same way cli.sh does — reads the GitHub API, takes the first wbx-v tag — and compares it to its own compiled-in version. If they match, it says already current and stops. Otherwise it picks the asset for its platform (decided at compile time) and performs an atomic swap:

sequenceDiagram
  participant X as wbx (running)
  participant A as api.github.com/releases
  participant R as the Release
  participant F as filesystem
  X->>A: GET releases — first wbx-v* tag
  A-->>X: wbx-v0.13.0
  X->>X: compare to CARGO_PKG_VERSION
  X->>R: download asset for this platform
  R-->>X: bytes
  X->>F: write /usr/local/bin/wbx.new
  X->>F: chmod 0755
  X->>F: rename wbx.new → wbx (atomic)
  

Follow the swap as a story. The running wbx asks the API for the latest tag, gets wbx-v0.13.0, and compares it to the version baked into itself at compile time. They differ, so it downloads the new bytes and writes them to a sibling file — wbx.new, right next to itself. It marks that new file executable. Then the one move that makes this safe: it renames wbx.new over wbx. Rename is atomic on the same filesystem, so there is never a moment where PATH holds a half-written binary. The running process keeps executing the old inode it already loaded; the next invocation is the new version; and a crash mid-download leaves the old binary untouched.

The curl lane's counterpart to all this is pinning — to install an exact version instead of upgrading, you set the version knob: WBX_VERSION=0.12.0 sh -c "$(curl -fsSL https://workbooks.sh/cli.sh)".

the fourth ASSET

Depth rung. The sixth file in every release is wbx.wasm, built for the wasm32-wasip1 target. It ships in the same Release as the native binaries because wbx is one crate compiled to two kinds of target — the reason a wasm asset exists at all is the one-crate-two-targets story. This is the lane for the sandboxed future: a wasm module wbx that can run inside the engine's sandbox alongside the workbooks it operates on.

Two honest notes belong here. First, the wasm build of upgrade (and open) deliberately bails with wbx upgrade needs the native binary — a module running inside a sandbox cannot rewrite its own file, and pretending otherwise would be a lie. Second, the consumption side: the artifact ships, and it's the spec'd path for in-sandbox agents, but the runtime-side wiring that fetches that published wasm isn't visible in the code yet. It's the lane that exists for where this is going, stated plainly as that.

Meanwhile, the runtime image doesn't download wbx from a release at all. Its Dockerfile compiles wbx from source in a build stage and copies the stripped binary to /usr/local/bin/wbx. So the agents working inside the engine get a native wbx pinned to the image's exact commit — not a downloaded one, not a wasm one. Same source, a different door again: built in place, at a known SHA.

what this doesn't SOLVE

Honesty section. The one-artifact design buys a lot, but it has real edges, and naming them is the point.

The npm lane's postinstall dies under npm install --ignore-scripts — no postinstall runs, no binary downloads. The spec actually describes a different shape (esbuild-style per-platform optionalDependencies) that would survive --ignore-scripts, because the platform binary arrives as a real dependency rather than a download step. The shipped implementation chose the single-package postinstall instead: one package to publish and maintain, against five-plus registry packages. That's a deliberate trade, not an oversight — and if you run --ignore-scripts, the curl lane is your way in.

The other edges, briefly. There's no Homebrew, apt, or winget — the lanes are curl, npm, and self-upgrade. The curl lane skips Windows entirely (npm or WSL). The atomic rename in upgrade is only atomic on the same filesystem, and needs write permission to the install directory. And the "static binary, no runtime deps" claim is precise about what it means: wbx is built with rustls instead of OpenSSL, so there's no interpreter and no VM to install — but the Linux builds target -gnu, not musl, so "no runtime deps" means no interpreter, not zero dynamic libraries.

lanewhat breaks itescape hatch
curlWindows; no PATH-writable dirnpm lane, or WSL
npm--ignore-scriptscurl lane, or npm rebuild
upgradecross-filesystem; no write permreinstall via curl
wasmconsumption wiring still landingnative binary today

questions people actually ASK

Is curl | sh safe here?

It's about sixty lines of POSIX shell, served as a static file you can read before you run it — open workbooks.sh/cli.sh in a tab first. It discovers a tag, downloads a release asset over HTTPS, moves it into place, and runs --version. No worker logic, no hidden steps. Auditable beats trusted.

How do I pin a specific version?

On the curl lane, set WBX_VERSION=0.12.0 before running the script. On npm, ask for the version directly: npm i @work.books/[email protected] — and because the package version and the release tag are lockstep, that pins the binary too.

I installed both curl and npm — which one wins?

Whichever directory comes first on your PATH. That's also exactly what the shadow banner told you at npm install time — it named the earlier binary and gave you both fixes: rm the old one, or reorder PATH so npm's bin directory leads.

Why is the npm package @work.books/cli, not wbx?

The bare name wbx was already taken on the registry, so the package is scoped. The parent lesson covers the naming; here it's just a label on the same bytes.

How do agents inside the engine get wbx?

They don't download it. The runtime image compiles wbx from source during its build and bakes the binary into /usr/local/bin/wbx, pinned to the image's commit. Same source as every other lane, built in place.

Why publish to two npm registries?

The release publishes to npmjs as @work.books/cli and mirrors to GitHub Packages as @workbooks-sh/cli — the same launcher, two homes. The publisher sets a skip-download flag on itself so it doesn't fetch a binary onto the build runner while stamping the package.

keep GOING

Distribution is one chapter under the command line — the parent has the rest, and the neighbors explain why the lanes are shaped this way.