From e6b56757dbaaa284e2a1ce87da0fe0965f859647 Mon Sep 17 00:00:00 2001 From: Pierre Carrier Date: Fri, 12 Jun 2026 01:38:20 +0000 Subject: [PATCH 1/2] Publish blit binary via npm (blit-bin) Add a blit-bin npm launcher plus per-platform blit-bin--[-musl] data packages generated from the existing release tarballs/zip, so the host installs exactly one prebuilt binary through optionalDependencies. The default export is the absolute path to the blit executable (ffmpeg-static convention), so downstreams can: import blit from "blit-bin"; spawn(blit, ["open"], { stdio: "inherit" }); Ships dual ESM/CJS entries, a blit-bin/resolve subpath, and .d.ts types. A new publish-bin-npm release job builds and publishes these from the artifacts the release already produces, using OIDC provenance. --- .github/workflows/release.yml | 30 ++++ bin/build-npm-bin-packages | 255 ++++++++++++++++++++++++++++++++++ bin/publish-npm-bin-packages | 28 ++++ npm/blit-bin/README.md | 63 +++++++++ npm/blit-bin/bin/blit.js | 30 ++++ npm/blit-bin/index.d.ts | 21 +++ npm/blit-bin/index.js | 11 ++ npm/blit-bin/index.mjs | 15 ++ npm/blit-bin/package.json | 55 ++++++++ npm/blit-bin/resolve.d.ts | 8 ++ npm/blit-bin/resolve.js | 73 ++++++++++ 11 files changed, 589 insertions(+) create mode 100755 bin/build-npm-bin-packages create mode 100755 bin/publish-npm-bin-packages create mode 100644 npm/blit-bin/README.md create mode 100644 npm/blit-bin/bin/blit.js create mode 100644 npm/blit-bin/index.d.ts create mode 100644 npm/blit-bin/index.js create mode 100644 npm/blit-bin/index.mjs create mode 100644 npm/blit-bin/package.json create mode 100644 npm/blit-bin/resolve.d.ts create mode 100644 npm/blit-bin/resolve.js diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0afdd84c..22241b5e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -94,6 +94,36 @@ jobs: - uses: DeterminateSystems/magic-nix-cache-action@main - run: ./bin/publish-npm-packages --provenance --access public + publish-bin-npm: + needs: [release] + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v4 + with: + node-version: "22" + registry-url: "https://registry.npmjs.org" + + - uses: actions/download-artifact@v4 + with: + pattern: tarballs-* + merge-multiple: true + path: artifacts + + - uses: actions/download-artifact@v4 + with: + name: windows-x86_64 + path: artifacts + + - name: Build blit-bin npm packages + run: ./bin/build-npm-bin-packages artifacts dist/npm-bin "${GITHUB_REF_NAME#v}" + + - name: Publish blit-bin npm packages + run: ./bin/publish-npm-bin-packages dist/npm-bin --provenance --access public + update-homebrew: needs: [release] runs-on: blacksmith-4vcpu-ubuntu-2404 diff --git a/bin/build-npm-bin-packages b/bin/build-npm-bin-packages new file mode 100755 index 00000000..3b3d279f --- /dev/null +++ b/bin/build-npm-bin-packages @@ -0,0 +1,255 @@ +#!/usr/bin/env bash +# Repackage release artifacts into npm packages: one data-only package per +# platform (blit-bin--[-musl]) plus the blit-bin launcher that +# depends on them as optionalDependencies. +# +# Usage: build-npm-bin-packages [version] +# +# artifacts-dir directory scanned (recursively) for blit_*.tar.gz and +# blit_*_windows_*.zip release artifacts +# out-dir where generated packages are written +# version optional override; otherwise inferred from filenames +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/.." && pwd)" + +in_dir="${1:?usage: build-npm-bin-packages [version]}" +out_dir="${2:?usage: build-npm-bin-packages [version]}" +version_override="${3:-}" + +in_dir="$(cd "$in_dir" && pwd)" +mkdir -p "$out_dir" +out_dir="$(cd "$out_dir" && pwd)" + +HOMEPAGE="https://blit.sh" +AUTHOR="Indent (https://indent.com)" +REPO_URL="git+https://github.com/indent-com/blit.git" +BUGS_URL="https://github.com/indent-com/blit/issues" +LICENSE="MIT" + +extract_zip() { + # extract_zip + local zip="$1" dest="$2" + if command -v unzip >/dev/null 2>&1; then + unzip -q -o "$zip" -d "$dest" + elif command -v bsdtar >/dev/null 2>&1; then + bsdtar -xf "$zip" -C "$dest" + else + echo "error: need 'unzip' or 'bsdtar' to extract $zip" >&2 + exit 1 + fi +} + +version="" +set_version() { + local v="$1" + if [ -z "$version" ]; then + version="$v" + elif [ "$version" != "$v" ]; then + echo "error: conflicting versions in artifacts: $version vs $v" >&2 + exit 1 + fi +} +[ -n "$version_override" ] && version="$version_override" + +# Track generated platform package names for the launcher manifest. +pkg_names=() + +# write_platform_pkg +write_platform_pkg() { + local name="$1" os="$2" cpu="$3" libc="$4" binfile="$5" src="$6" desc="$7" + local dir="$out_dir/$name" + rm -rf "$dir" + mkdir -p "$dir/bin" + cp "$src" "$dir/bin/$binfile" + [ "$binfile" = "blit.exe" ] || chmod 0755 "$dir/bin/$binfile" + + local libc_field="" + if [ -n "$libc" ]; then + libc_field="\n \"libc\": [\"$libc\"]," + fi + + cat > "$dir/package.json" <=18" } +} +JSON + + cat > "$dir/README.md" <. +MD + + pkg_names+=("$name") + echo " generated $name ($desc)" +} + +# Split "_" into os + arch, where arch is a known token that may +# itself contain an underscore (x86_64). +split_os_arch() { + local s="$1" + case "$s" in + *_x86_64) split_os="${s%_x86_64}"; split_arch="x86_64" ;; + *_aarch64) split_os="${s%_aarch64}"; split_arch="aarch64" ;; + *) echo "error: cannot parse os/arch from '$s'" >&2; exit 1 ;; + esac +} + +process_tarball() { + local f="$1" base os arch v rest + base="$(basename "$f" .tar.gz)" # blit___ + rest="${base#blit_}" # __ + v="${rest%%_*}"; rest="${rest#*_}" # ver, then _ + split_os_arch "$rest"; os="$split_os"; arch="$split_arch" + set_version "$v" + + local tmp; tmp="$(mktemp -d)" + tar -xzf "$f" -C "$tmp" + local bin="$tmp/bin/blit" + [ -f "$bin" ] || { echo "error: $f has no bin/blit" >&2; exit 1; } + + local cpu + case "$arch" in + x86_64) cpu="x64" ;; + aarch64) cpu="arm64" ;; + *) echo "error: unknown arch '$arch' in $f" >&2; exit 1 ;; + esac + + case "$os" in + linux) + write_platform_pkg "blit-bin-linux-$cpu" linux "$cpu" glibc blit "$bin" "Linux $arch (glibc)" ;; + linux-musl) + write_platform_pkg "blit-bin-linux-$cpu-musl" linux "$cpu" musl blit "$bin" "Linux $arch (musl)" ;; + darwin) + write_platform_pkg "blit-bin-darwin-$cpu" darwin "$cpu" "" blit "$bin" "macOS $arch" ;; + *) echo "error: unknown os '$os' in $f" >&2; exit 1 ;; + esac + rm -rf "$tmp" +} + +process_zip() { + local f="$1" base rest v arch + base="$(basename "$f" .zip)" # blit__windows_ + rest="${base#blit_}" # _windows_ + v="${rest%%_*}"; rest="${rest#*_}" # ver, then windows_ + split_os_arch "$rest"; arch="$split_arch" # split_os == "windows" + set_version "$v" + + local tmp; tmp="$(mktemp -d)" + extract_zip "$f" "$tmp" + local bin="$tmp/blit.exe" + [ -f "$bin" ] || bin="$(find "$tmp" -name blit.exe -print -quit)" + [ -n "$bin" ] && [ -f "$bin" ] || { echo "error: $f has no blit.exe" >&2; exit 1; } + + local cpu + case "$arch" in + x86_64) cpu="x64" ;; + aarch64) cpu="arm64" ;; + *) echo "error: unknown arch '$arch' in $f" >&2; exit 1 ;; + esac + write_platform_pkg "blit-bin-win32-$cpu" win32 "$cpu" "" blit.exe "$bin" "Windows $arch" + rm -rf "$tmp" +} + +echo "scanning $in_dir for release artifacts..." +shopt -s nullglob globstar +for f in "$in_dir"/**/*.tar.gz; do + [ -f "$f" ] || continue + process_tarball "$f" +done +for f in "$in_dir"/**/*_windows_*.zip; do + [ -f "$f" ] || continue + process_zip "$f" +done + +if [ ${#pkg_names[@]} -eq 0 ]; then + echo "error: no blit_*.tar.gz / *_windows_*.zip artifacts found in $in_dir" >&2 + exit 1 +fi +if [ -z "$version" ]; then + echo "error: could not determine version" >&2 + exit 1 +fi + +# De-duplicate package names (glob fallbacks may match twice). +mapfile -t pkg_names < <(printf '%s\n' "${pkg_names[@]}" | sort -u) + +# --- Launcher package (blit-bin) --- +launcher_src="$repo_root/npm/blit-bin" +launcher_out="$out_dir/blit-bin" +rm -rf "$launcher_out" +mkdir -p "$launcher_out/bin" +for f in index.js index.mjs index.d.ts resolve.js resolve.d.ts README.md; do + cp "$launcher_src/$f" "$launcher_out/$f" +done +cp "$launcher_src/bin/blit.js" "$launcher_out/bin/blit.js" + +# Build optionalDependencies JSON object pinned to this version. +opt_deps="" +for name in "${pkg_names[@]}"; do + [ -n "$opt_deps" ] && opt_deps+="," + opt_deps+="\n \"$name\": \"$version\"" +done + +cat > "$launcher_out/package.json" <=18" } +} +JSON +echo " generated blit-bin launcher (optionalDependencies: ${pkg_names[*]})" + +# Emit a publish order manifest: platform packages first, launcher last. +{ + for name in "${pkg_names[@]}"; do echo "$name"; done + echo "blit-bin" +} > "$out_dir/.publish-order" + +echo "" +echo "version: $version" +echo "packages written to: $out_dir" +ls -1 "$out_dir" diff --git a/bin/publish-npm-bin-packages b/bin/publish-npm-bin-packages new file mode 100755 index 00000000..c44f6c2e --- /dev/null +++ b/bin/publish-npm-bin-packages @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Publish the generated blit-bin npm packages (platform packages first, then +# the blit-bin launcher last so optionalDependencies resolve on install). +# +# Usage: publish-npm-bin-packages [extra npm publish args...] +# e.g. publish-npm-bin-packages dist/npm-bin --provenance --access public +set -euo pipefail + +pkgs_dir="${1:?usage: publish-npm-bin-packages [npm publish args...]}" +shift || true +pkgs_dir="$(cd "$pkgs_dir" && pwd)" + +order_file="$pkgs_dir/.publish-order" +if [ ! -f "$order_file" ]; then + echo "error: $order_file not found (run build-npm-bin-packages first)" >&2 + exit 1 +fi + +while IFS= read -r name; do + [ -n "$name" ] || continue + dir="$pkgs_dir/$name" + [ -d "$dir" ] || { echo "error: missing package dir $dir" >&2; exit 1; } + echo "=== Publishing $name ===" + npm publish "$dir" "$@" + echo "" +done < "$order_file" + +echo "done." diff --git a/npm/blit-bin/README.md b/npm/blit-bin/README.md new file mode 100644 index 00000000..6ebc607b --- /dev/null +++ b/npm/blit-bin/README.md @@ -0,0 +1,63 @@ +# blit-bin + +The [blit](https://blit.sh) binary, distributed via npm. Installing `blit-bin` +pulls in exactly one prebuilt package for your platform +(`blit-bin--[-musl]`) through optional dependencies — nothing else. + +## CLI + +```sh +npm i -g blit-bin +blit open +``` + +## Bundle the binary in your own tool + +The default export is the absolute filesystem path to the `blit` executable, so +you can spawn it directly. Resolution happens on import and throws with an +actionable message if the matching prebuilt package was not installed. + +### ESM + +```js +import blit from "blit-bin"; +import { spawn } from "node:child_process"; + +spawn(blit, ["open"], { stdio: "inherit" }); +``` + +### CommonJS + +```js +const blit = require("blit-bin"); +const { spawn } = require("node:child_process"); + +spawn(blit, ["open"], { stdio: "inherit" }); +``` + +### Helpers + +Lower-level resolution helpers are available on the `blit-bin/resolve` subpath +(and as named exports of the main entry): + +```js +import { binaryPath, binaryName, candidatePackages, isMusl } from "blit-bin"; +// or: import { binaryPath } from "blit-bin/resolve"; +``` + +| export | description | +| --- | --- | +| `default` | absolute path to the `blit` binary (resolved at import) | +| `binaryPath()` | same path, computed lazily; throws if unavailable | +| `binaryName()` | `"blit"` or `"blit.exe"` | +| `candidatePackages()` | platform package names, in resolution order | +| `isMusl()` | `true` on musl-libc Linux | + +## Platforms + +Linux x64/arm64 (glibc & musl), macOS arm64, Windows x64 — matching the +binaries the blit release pipeline builds. + +## License + +MIT diff --git a/npm/blit-bin/bin/blit.js b/npm/blit-bin/bin/blit.js new file mode 100644 index 00000000..44c705cd --- /dev/null +++ b/npm/blit-bin/bin/blit.js @@ -0,0 +1,30 @@ +#!/usr/bin/env node +"use strict"; + +const { spawnSync } = require("child_process"); +const { binaryPath } = require("../resolve.js"); + +let bin; +try { + bin = binaryPath(); +} catch (err) { + process.stderr.write(`${err.message}\n`); + process.exit(1); +} + +const result = spawnSync(bin, process.argv.slice(2), { stdio: "inherit" }); + +if (result.error) { + process.stderr.write( + `blit-bin: failed to launch ${bin}: ${result.error.message}\n`, + ); + process.exit(1); +} + +// Re-raise the child's terminating signal, otherwise forward its exit code. +if (result.signal) { + process.kill(process.pid, result.signal); + process.exit(1); +} + +process.exit(result.status === null ? 1 : result.status); diff --git a/npm/blit-bin/index.d.ts b/npm/blit-bin/index.d.ts new file mode 100644 index 00000000..c1fca808 --- /dev/null +++ b/npm/blit-bin/index.d.ts @@ -0,0 +1,21 @@ +/** + * Absolute filesystem path to the prebuilt `blit` executable for the current + * platform. Resolved eagerly on import; throws if no matching prebuilt package + * is installed. + * + * @example + * import blit from "blit-bin"; + * import { spawn } from "node:child_process"; + * spawn(blit, ["open"], { stdio: "inherit" }); + */ +declare const blitPath: string; +export default blitPath; + +/** Resolve the absolute path to the platform `blit` binary (throws if none installed). */ +export declare function binaryPath(): string; +/** Executable filename for this platform (`blit` or `blit.exe`). */ +export declare function binaryName(): string; +/** Candidate npm package names for this platform, in resolution order. */ +export declare function candidatePackages(): string[]; +/** Whether the current Linux runtime uses musl libc. */ +export declare function isMusl(): boolean; diff --git a/npm/blit-bin/index.js b/npm/blit-bin/index.js new file mode 100644 index 00000000..5fd08ff0 --- /dev/null +++ b/npm/blit-bin/index.js @@ -0,0 +1,11 @@ +"use strict"; + +// Default (CommonJS) export: the absolute filesystem path to the prebuilt +// `blit` executable for the current platform. +// +// const blit = require("blit-bin"); +// require("child_process").spawn(blit, ["open"], { stdio: "inherit" }); +// +// Throws at require time with an actionable message if no matching prebuilt +// package is installed. Named helpers live on `blit-bin/resolve`. +module.exports = require("./resolve.js").binaryPath(); diff --git a/npm/blit-bin/index.mjs b/npm/blit-bin/index.mjs new file mode 100644 index 00000000..0f7ac6fd --- /dev/null +++ b/npm/blit-bin/index.mjs @@ -0,0 +1,15 @@ +// ESM entry point. Default export is the absolute path to the platform `blit` +// binary; named exports expose the resolution helpers. +// +// import blit from "blit-bin"; +// import { spawn } from "node:child_process"; +// spawn(blit, ["open"], { stdio: "inherit" }); +import resolve from "./resolve.js"; + +const blitPath = resolve.binaryPath(); + +export default blitPath; +export const binaryPath = resolve.binaryPath; +export const binaryName = resolve.binaryName; +export const candidatePackages = resolve.candidatePackages; +export const isMusl = resolve.isMusl; diff --git a/npm/blit-bin/package.json b/npm/blit-bin/package.json new file mode 100644 index 00000000..0523060d --- /dev/null +++ b/npm/blit-bin/package.json @@ -0,0 +1,55 @@ +{ + "name": "blit-bin", + "version": "0.0.0", + "description": "Installs the prebuilt blit binary for your platform; default export is the binary path.", + "keywords": [ + "blit", + "terminal", + "multiplexer", + "wayland", + "cli" + ], + "homepage": "https://blit.sh", + "license": "MIT", + "author": "Indent (https://indent.com)", + "repository": { + "type": "git", + "url": "git+https://github.com/indent-com/blit.git", + "directory": "npm/blit-bin" + }, + "bugs": { + "url": "https://github.com/indent-com/blit/issues" + }, + "bin": { + "blit": "bin/blit.js" + }, + "type": "commonjs", + "main": "index.js", + "module": "index.mjs", + "types": "index.d.ts", + "exports": { + ".": { + "types": "./index.d.ts", + "import": "./index.mjs", + "require": "./index.js" + }, + "./resolve": { + "types": "./resolve.d.ts", + "default": "./resolve.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "bin/blit.js", + "index.js", + "index.mjs", + "index.d.ts", + "resolve.js", + "resolve.d.ts", + "README.md" + ], + "optionalDependencies": {}, + "engines": { + "node": ">=18" + } +} diff --git a/npm/blit-bin/resolve.d.ts b/npm/blit-bin/resolve.d.ts new file mode 100644 index 00000000..69a393c0 --- /dev/null +++ b/npm/blit-bin/resolve.d.ts @@ -0,0 +1,8 @@ +/** Resolve the absolute path to the platform `blit` binary (throws if none installed). */ +export declare function binaryPath(): string; +/** Executable filename for this platform (`blit` or `blit.exe`). */ +export declare function binaryName(): string; +/** Candidate npm package names for this platform, in resolution order. */ +export declare function candidatePackages(): string[]; +/** Whether the current Linux runtime uses musl libc. */ +export declare function isMusl(): boolean; diff --git a/npm/blit-bin/resolve.js b/npm/blit-bin/resolve.js new file mode 100644 index 00000000..f5b7a9ee --- /dev/null +++ b/npm/blit-bin/resolve.js @@ -0,0 +1,73 @@ +"use strict"; + +// Platform binary resolution for blit-bin. Mirrors the release artifact matrix +// (see bin/build-npm-bin-packages). Kept separate from the public entry points +// (index.js / index.mjs) so the CLI and both module systems share one copy. + +const fs = require("fs"); + +// Map process.platform/process.arch to the npm package that ships the binary. +function candidatePackages() { + const platform = process.platform; + const arch = process.arch; + if (platform === "win32") return [`blit-bin-win32-${arch}`]; + if (platform === "darwin") return [`blit-bin-darwin-${arch}`]; + if (platform === "linux") { + const base = `blit-bin-linux-${arch}`; + // Prefer the libc variant we detect, but fall back to the other. + return isMusl() ? [`${base}-musl`, base] : [base, `${base}-musl`]; + } + return []; +} + +// Detect musl libc on Linux. Uses the same signal as detect-libc: glibc +// runtimes expose glibcVersionRuntime in the Node process report header. +function isMusl() { + if (process.platform !== "linux") return false; + try { + const header = process.report.getReport().header; + return !header.glibcVersionRuntime; + } catch { + return false; + } +} + +function binaryName() { + return process.platform === "win32" ? "blit.exe" : "blit"; +} + +// Resolve the absolute path to the platform binary, or throw a helpful error. +function binaryPath() { + const exe = binaryName(); + const tried = []; + for (const pkg of candidatePackages()) { + tried.push(pkg); + let resolved; + try { + resolved = require.resolve(`${pkg}/bin/${exe}`); + } catch { + continue; + } + if (process.platform !== "win32") { + try { + fs.chmodSync(resolved, 0o755); + } catch { + // read-only install; ignore. + } + } + return resolved; + } + throw new Error( + [ + `blit-bin: no prebuilt binary found for ${process.platform} ${process.arch}.`, + tried.length ? `Tried optional packages: ${tried.join(", ")}.` : "", + "Supported: linux x64/arm64 (glibc & musl), darwin arm64, win32 x64.", + "If your platform is supported, ensure optional dependencies were not", + "skipped (e.g. npm install without --no-optional / --omit=optional).", + ] + .filter(Boolean) + .join("\n"), + ); +} + +module.exports = { binaryPath, binaryName, candidatePackages, isMusl }; From 6272e723864e3fe963e39c054373bd04ba6ddb11 Mon Sep 17 00:00:00 2001 From: Pierre Carrier Date: Fri, 12 Jun 2026 01:48:27 +0000 Subject: [PATCH 2/2] Format blit-bin README with prettier --- npm/blit-bin/README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/npm/blit-bin/README.md b/npm/blit-bin/README.md index 6ebc607b..af44be2b 100644 --- a/npm/blit-bin/README.md +++ b/npm/blit-bin/README.md @@ -45,13 +45,13 @@ import { binaryPath, binaryName, candidatePackages, isMusl } from "blit-bin"; // or: import { binaryPath } from "blit-bin/resolve"; ``` -| export | description | -| --- | --- | -| `default` | absolute path to the `blit` binary (resolved at import) | -| `binaryPath()` | same path, computed lazily; throws if unavailable | -| `binaryName()` | `"blit"` or `"blit.exe"` | -| `candidatePackages()` | platform package names, in resolution order | -| `isMusl()` | `true` on musl-libc Linux | +| export | description | +| --------------------- | ------------------------------------------------------- | +| `default` | absolute path to the `blit` binary (resolved at import) | +| `binaryPath()` | same path, computed lazily; throws if unavailable | +| `binaryName()` | `"blit"` or `"blit.exe"` | +| `candidatePackages()` | platform package names, in resolution order | +| `isMusl()` | `true` on musl-libc Linux | ## Platforms