Generate from CI
The binpatch/generate GitHub Action shells out to a pinned
bsdiff, produces one patch per platform, and publishes them to GHCR or
GitHub Releases — automatically, on every release.
You ship a 100 MB binary. A bug fix lands. The user runs mycli update
and pulls another 100 MB — even though the fix touched a few hundred
kilobytes. Repeat that across every release and every machine, and you are
burning bandwidth and patience for bytes that haven’t moved since the last
build.
The old binary and the new one are almost identical. A binary delta (compressed bsdiff) captures just the difference: typically 0.05–0.1% of the full size. That 100 MB update becomes a ~190 KB patch.
A delta patch is useless without both:
old → new in CI, and publish it
somewhere your users can find it.And if the user is several versions behind, they don’t get a single patch — they get a chain of patches (1.2.0 → 1.2.1 → 1.2.2 → 1.3.4). binpatch chains them automatically, downloads them in parallel, applies each hop in order, verifies the cumulative SHA-256, and falls back to a full download if any hop is missing or malformed.
Most projects hand-roll one half and skip the other. binpatch gives you
both, as a small MIT-licensed TypeScript library plus a drop-in GitHub
Action.
Generate from CI
The binpatch/generate GitHub Action shells out to a pinned
bsdiff, produces one patch per platform, and publishes them to GHCR or
GitHub Releases — automatically, on every release.
Apply in your binary
resolveAndApply() discovers the patch chain for the user’s
installed version, downloads only what’s needed, and reconstructs the new
binary with SHA-256 verification and an OOM guard built in.
Battle-tested at scale
Powers self-updates in production for shipped CLI binaries you may already be using. Same reliability you’d build into your own tool — minus the years of accumulated fixes.
Pure Node, tiny
No native bindings, no WASM, no shelled-out processes at apply time. One ESM package, zero runtime dependencies beyond Node 22+.
CI user's machine ────── ─────────────── old binary (100 MB) │ ▼ ┌──────────────────────┐ │ binpatch Action (CI) │ new binary (100 MB) │ bsdiff old → new │ ──────────────────────► └──────────────────────┘ │ ▼ patch (70 KB) ────────────────────────► ┌──────────────────────────┐ │ resolveAndApply() │ │ download · verify · │ │ reconstruct new binary │ └──────────────────────────┘ │ ▼ installed binary (100 MB)The user downloads kilobytes instead of megabytes. Your CI does the heavy lifting once.
mycli update story).Do not reach for it when updates are source-level (use git, npm update),
or when your binary is tiny (deltas only amortize at scale).
npm install binpatchThen both: generate patches from CI with the
GitHub Action (so nightly builds push to GHCR and stable
releases push to GitHub Releases), and wire
resolveAndApply into your binary’s update command
to discover and apply them.
import { resolveAndApply } from "binpatch";
const result = await resolveAndApply({ currentVersion: "1.2.0", targetVersion: "1.3.4", source: ghcrSource({ repo: "myorg/mycli" }),});// result.destPath now holds the verified 1.3.4 binary; download was ~70 KB.