Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@ node_modules/
dist/
website/.astro/
website/dist/
website/src/generated/
website/public/og-image-*.png
*.log
.DS_Store
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ All notable changes to `binpatch` are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Documentation

- Reposition homepage to lead with "any binary" framing (Electron apps, CLIs,
agents, game updaters) instead of CLI-only. Hero now features a measured
download comparison chart for getsentry/cli 0.29.0 → 0.39.0 (8 adjacent
release pairs, sentry-linux-x64). The typical (median) patch is 4.0%
the size of the full gzipped binary — 1.32 MB vs 31.38 MB, 96% saved per
update. Range across the 8 pairs: 0.9% (small fixes) to 8.1% (big
features).
- Add "View as Markdown" link in the page footer. Each page now exposes its
raw markdown source at `/<slug>.md` — implemented via a Starlight
component override and an Astro API endpoint, both base-path aware so
PR previews keep working.

## [0.3.1] - 2026-07-27

- Guard chain discovery against malformed/incomparable version tags (no longer
Expand Down
23 changes: 13 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
Reusable binary delta-update engine. Apply a **TRDIFF10 / bsdiff+zstd** patch
chain to a binary, discover chains from a pluggable source (OCI/GHCR tags or
GitHub Release assets), and generate + publish patches via a composite GitHub
Action. Pure Node, zero product coupling.
Action. Pure Node, zero product coupling — works for Electron apps, CLIs,
agents, and any single-file binary artifact.

```sh
npm install binpatch
Expand All @@ -17,11 +18,13 @@ npm install binpatch

## Why

Every time you `mycli update`, you pull the **entire binary again** — even when
the new release changed a few hundred kilobytes of a 100&nbsp;MB file. That's
bandwidth and patience burned on bytes that didn't move. A binary delta (bsdiff)
between consecutive builds is typically **0.05–0.1%** of the full size, so that
100&nbsp;MB download becomes a ~190&nbsp;KB patch.
Every time your binary updates itself, your users pull the **entire file
again** — even when the new release changed a few hundred kilobytes of a
100&nbsp;MB Electron app, a 50&nbsp;MB CLI, or a 200&nbsp;MB game updater.
That's bandwidth and patience burned on bytes that didn't move. A binary
delta (bsdiff) between consecutive builds is typically a few percent of
the full size — see the [home page graph](https://binpatch.p.byk.im/)
for real measurements on `getsentry/cli`.

The hard part isn't making the patch — it's the **two halves** that most
projects hand-roll separately (and get wrong):
Expand All @@ -31,10 +34,10 @@ projects hand-roll separately (and get wrong):
safely (integrity check, size cap, progress).

`binpatch` gives you **both** as one MIT-licensed TypeScript library plus a
drop-in GitHub Action. It's the apply/discovery core extracted from
Powers self-updates in production for shipped CLI binaries you may
already be using. Battle-tested reliability — minus the years of accumulated
fixes you'd otherwise have to write yourself.
drop-in GitHub Action. Powers self-updates in production for shipped
binaries you may already be using (including [getsentry/cli](https://github.com/getsentry/cli)).
Battle-tested reliability — minus the years of accumulated fixes you'd
otherwise have to write yourself.

## Scope

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
],
"sideEffects": false,
"engines": {
"node": ">=22.5"
"node": ">=22.15.0"
},
"scripts": {
"build": "tsup",
Expand Down
117 changes: 117 additions & 0 deletions website/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -1,15 +1,34 @@
import { defineConfig } from "astro/config";
import starlight from "@astrojs/starlight";
import { fileURLToPath } from "node:url";
import mermaidRenderer from "./integrations/mermaid-renderer.mjs";
import {
socialAssets,
generateAssetsEagerly,
} from "./integrations/social-assets.ts";

// Production serves from the root of the custom domain binpatch.p.byk.im.
// PR previews are built under `/_preview/pr-<n>/` (pr-preview-action's
// umbrella dir) — same root, so DOCS_BASE_PATH points there with no /binpatch/.
const base = process.env.DOCS_BASE_PATH || "/";

// Run the OG image generation synchronously at config-load time so the
// Starlight head array can reference the (content-hashed) OG image
// filename. Without this, the static head array would hardcode a stale
// URL and social platforms could cache the wrong image indefinitely.
const projectRoot = fileURLToPath(new URL(".", import.meta.url));
const { ogFilename } = await generateAssetsEagerly(projectRoot);

export default defineConfig({
site: "https://binpatch.p.byk.im",
base,
integrations: [
// Bundles the Mermaid renderer into every page so fenced ```mermaid
// blocks render as SVG. See ./integrations/mermaid-renderer.mjs.
mermaidRenderer(),
// Generates the 1200x630 OG image at build time with a content-hashed
// filename. See ./integrations/social-assets.ts.
socialAssets(),
starlight({
title: "binpatch",
description:
Expand Down Expand Up @@ -66,6 +85,101 @@ export default defineConfig({
sizes: "180x180",
},
},
// Open Graph + Twitter Card. The OG image is content-hashed at
// build time so the URL changes whenever the image does — see
// integrations/social-assets.ts for the generation logic.
{
tag: "meta",
attrs: {
property: "og:title",
content: "binpatch",
},
},
{
tag: "meta",
attrs: {
property: "og:description",
content:
"Ship binary updates that download a patch instead of the whole file. binpatch generates and applies small binary delta patches — the same engine getsentry/cli uses to self-update.",
},
},
{
tag: "meta",
attrs: {
property: "og:image",
content: `https://binpatch.p.byk.im${base}${ogFilename}`,
},
},
{
tag: "meta",
attrs: {
property: "og:image:width",
content: "1200",
},
},
{
tag: "meta",
attrs: {
property: "og:image:height",
content: "630",
},
},
{
tag: "meta",
attrs: {
property: "og:image:alt",
content: "binpatch — Patch only what moved. Up to 96% smaller updates for any binary.",
},
},
{
tag: "meta",
attrs: {
property: "og:url",
content: "https://binpatch.p.byk.im",
},
},
{
tag: "meta",
attrs: {
property: "og:type",
content: "website",
},
},
{
tag: "meta",
attrs: {
property: "og:site_name",
content: "binpatch",
},
},
{
tag: "meta",
attrs: {
name: "twitter:card",
content: "summary_large_image",
},
},
{
tag: "meta",
attrs: {
name: "twitter:title",
content: "binpatch",
},
},
{
tag: "meta",
attrs: {
name: "twitter:description",
content: "Patch only what moved. Up to 96% smaller updates for any binary.",
},
},
{
tag: "meta",
attrs: {
name: "twitter:image",
content: `https://binpatch.p.byk.im${base}${ogFilename}`,
},
},
],
social: [
{
Expand Down Expand Up @@ -118,6 +232,9 @@ export default defineConfig({
},
],
customCss: ["./src/custom.css"],
components: {
Footer: "./src/components/Footer.astro",
},
}),
],
});
43 changes: 43 additions & 0 deletions website/integrations/mermaid-renderer.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Mermaid renderer integration.
//
// Bundles the Mermaid renderer into every page so fenced ```mermaid
// blocks render as SVG. The render target is any
// `pre[data-language="mermaid"]` block (Expressive Code's wrapping of
// a Mermaid fenced code block) — replaced with the rendered SVG.
export default function mermaidRenderer() {
return {
name: "mermaid-renderer",
hooks: {
"astro:config:setup": ({ injectScript }) => {
injectScript(
"page",
`
import mermaid from "mermaid";
mermaid.initialize({
startOnLoad: false,
theme: "neutral",
securityLevel: "loose",
fontFamily: "var(--sl-font)"
});
const blocks = document.querySelectorAll('pre[data-language="mermaid"]');
for (const pre of blocks) {
const source = Array.from(pre.querySelectorAll('.ec-line')).map((l) => l.textContent).join('\\n');
if (!source.trim()) continue;
const id = 'mermaid-' + Math.random().toString(36).slice(2, 9);
try {
const { svg } = await mermaid.render(id, source);
const wrap = document.createElement('div');
wrap.className = 'mermaid';
wrap.innerHTML = svg;
pre.replaceWith(wrap);
} catch (err) {
console.error('Mermaid render failed:', err);
pre.textContent = source;
}
}
`,
);
},
},
};
}
Loading
Loading