diff --git a/cli/README.md b/cli/README.md index a33f4c7..573705f 100644 --- a/cli/README.md +++ b/cli/README.md @@ -16,6 +16,7 @@ when signed in — favorite stations, add your own, and post comments to your PD ## Contents - [Install](#install) + - [npm](#npm) - [Build (from a checkout)](#build-from-a-checkout) - [Usage](#usage) - [Signing in](#signing-in) @@ -38,6 +39,19 @@ Prebuilt release tarballs, `.deb`, and `.rpm` packages are attached to every LICENSE. The BSD builds run in emulated VMs and are attached to the release shortly after it's published (aarch64 ones can take hours). +### npm + +```bash +npm install -g @atradio/cli +atradio +``` + +`@atradio/cli` pulls in the prebuilt binary for your platform as an optional +dependency (`@atradio/cli--`), so npm downloads only the one that +matches your `os`/`cpu` — no build step, no postinstall. Covers macOS +(arm64/x64) and Linux glibc (arm64/x64). On musl (e.g. Alpine), use another +install method below or build from source. + ### macOS / Linux — Homebrew ```bash diff --git a/cli/npm/.gitignore b/cli/npm/.gitignore new file mode 100644 index 0000000..c7b9146 --- /dev/null +++ b/cli/npm/.gitignore @@ -0,0 +1,3 @@ +# Prebuilt binaries are dropped into each platform package at release time by +# CI (from the GitHub Release tarballs) — never committed. +platforms/*/bin/ diff --git a/cli/npm/README.md b/cli/npm/README.md new file mode 100644 index 0000000..b3d1832 --- /dev/null +++ b/cli/npm/README.md @@ -0,0 +1,35 @@ +# @atradio/cli + +**atradio.fm in your terminal** — a TUI radio player on the AT Protocol. + +```bash +npm install -g @atradio/cli +atradio +``` + +## What this is + +`@atradio/cli` ships the prebuilt `atradio` binary. Installing it pulls in a +single per-platform package — `@atradio/cli--` — matching your +`os`/`cpu` via npm's optional-dependency resolution. There is **no download step +and no postinstall**; the tiny launcher in this package resolves the matching +binary and execs it. + +## Supported platforms + +| OS | Architecture | Package | +| ----- | ------------------- | --------------------------- | +| macOS | Apple Silicon (arm64) | `@atradio/cli-darwin-arm64` | +| macOS | Intel (x64) | `@atradio/cli-darwin-x64` | +| Linux | arm64 (glibc) | `@atradio/cli-linux-arm64` | +| Linux | x64 (glibc) | `@atradio/cli-linux-x64` | + +Linux binaries are built against glibc. On a musl host (e.g. Alpine), install a +glibc compatibility layer or [build from source][repo]. + +## Also available as + +Homebrew, `.deb`/`.rpm`, a Nix flake, and `cargo install`. See the +[project README][repo]. + +[repo]: https://github.com/tsirysndr/atradio.fm diff --git a/cli/npm/bin/atradio.js b/cli/npm/bin/atradio.js new file mode 100644 index 0000000..578d3a3 --- /dev/null +++ b/cli/npm/bin/atradio.js @@ -0,0 +1,101 @@ +#!/usr/bin/env node +"use strict"; + +// Thin launcher for the `atradio` CLI. +// +// The actual binary is a prebuilt Rust executable shipped in a per-platform +// optional dependency (@atradio/cli--). npm installs only the +// package whose `os`/`cpu` match the host, so at runtime we just resolve that +// package's binary and hand off to it — no download, no postinstall. + +const { spawnSync } = require("node:child_process"); +const fs = require("node:fs"); + +// Node platform+arch -> the optional dependency that carries the binary. +const PACKAGES = { + "darwin arm64": "@atradio/cli-darwin-arm64", + "darwin x64": "@atradio/cli-darwin-x64", + "linux arm64": "@atradio/cli-linux-arm64", + "linux x64": "@atradio/cli-linux-x64", +}; + +// The Linux packages are built against glibc (…-unknown-linux-gnu). Detect a +// musl host up front so we can fail with a clear message instead of a cryptic +// dynamic-linker error. +function isLinuxMusl() { + if (process.platform !== "linux") return false; + try { + const report = + typeof process.report?.getReport === "function" + ? process.report.getReport() + : null; + // glibc runtimes expose this field; musl does not. + return !(report && report.header && report.header.glibcVersionRuntime); + } catch { + return false; + } +} + +function resolveBinary() { + const key = `${process.platform} ${process.arch}`; + + if (process.platform === "linux" && isLinuxMusl()) { + throw new Error( + `atradio: prebuilt binaries are built against glibc and won't run on a ` + + `musl-based system (e.g. Alpine).\n` + + `Install glibc compatibility, use a glibc-based image, or build from ` + + `source: https://github.com/tsirysndr/atradio.fm`, + ); + } + + const pkg = PACKAGES[key]; + if (!pkg) { + throw new Error( + `atradio: no prebuilt binary for your platform (${key}).\n` + + `Supported: ${Object.keys(PACKAGES).join(", ")}.\n` + + `Build from source: https://github.com/tsirysndr/atradio.fm`, + ); + } + + try { + return require.resolve(`${pkg}/bin/atradio`); + } catch (err) { + throw new Error( + `atradio: the platform package "${pkg}" is not installed.\n` + + `This usually means npm skipped optional dependencies. Reinstall ` + + `without --no-optional / --ignore-optional:\n` + + ` npm install -g @atradio/cli\n` + + `(original error: ${err && err.message})`, + ); + } +} + +let binary; +try { + binary = resolveBinary(); +} catch (err) { + console.error(err.message); + process.exit(1); +} + +// Belt-and-suspenders: make sure the binary is executable. npm preserves the +// mode from the published tarball, but some installers (or restrictive umasks) +// can drop the bit. Ignore failures — a read-only install may still be runnable. +try { + fs.chmodSync(binary, 0o755); +} catch {} + +const result = spawnSync(binary, process.argv.slice(2), { stdio: "inherit" }); + +if (result.error) { + console.error(`atradio: failed to launch binary: ${result.error.message}`); + process.exit(1); +} + +// Re-raise a terminating signal so callers (and Ctrl-C) behave as if they'd run +// the binary directly; otherwise propagate the exit code. +if (result.signal) { + process.kill(process.pid, result.signal); +} else { + process.exit(result.status === null ? 1 : result.status); +} diff --git a/cli/npm/package.json b/cli/npm/package.json new file mode 100644 index 0000000..d83e07e --- /dev/null +++ b/cli/npm/package.json @@ -0,0 +1,44 @@ +{ + "name": "@atradio/cli", + "version": "0.5.2", + "description": "atradio.fm in your terminal — a TUI radio player on the AT Protocol", + "bin": { + "atradio": "bin/atradio.js" + }, + "files": [ + "bin/atradio.js", + "README.md" + ], + "keywords": [ + "atradio", + "radio", + "tui", + "terminal", + "atproto", + "music", + "cli" + ], + "license": "GPL-2.0-or-later", + "author": "Tsiry Sandratraina ", + "homepage": "https://github.com/tsirysndr/atradio.fm#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/tsirysndr/atradio.fm.git", + "directory": "cli/npm" + }, + "bugs": { + "url": "https://github.com/tsirysndr/atradio.fm/issues" + }, + "engines": { + "node": ">=16" + }, + "publishConfig": { + "access": "public" + }, + "optionalDependencies": { + "@atradio/cli-darwin-arm64": "0.5.2", + "@atradio/cli-darwin-x64": "0.5.2", + "@atradio/cli-linux-arm64": "0.5.2", + "@atradio/cli-linux-x64": "0.5.2" + } +} diff --git a/cli/npm/platforms/darwin-arm64/package.json b/cli/npm/platforms/darwin-arm64/package.json new file mode 100644 index 0000000..273bcaa --- /dev/null +++ b/cli/npm/platforms/darwin-arm64/package.json @@ -0,0 +1,25 @@ +{ + "name": "@atradio/cli-darwin-arm64", + "version": "0.5.2", + "description": "Prebuilt atradio CLI binary for macOS (Apple Silicon / arm64).", + "os": [ + "darwin" + ], + "cpu": [ + "arm64" + ], + "files": [ + "bin/atradio" + ], + "license": "GPL-2.0-or-later", + "author": "Tsiry Sandratraina ", + "homepage": "https://github.com/tsirysndr/atradio.fm#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/tsirysndr/atradio.fm.git", + "directory": "cli/npm/platforms/darwin-arm64" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/cli/npm/platforms/darwin-x64/package.json b/cli/npm/platforms/darwin-x64/package.json new file mode 100644 index 0000000..df32a26 --- /dev/null +++ b/cli/npm/platforms/darwin-x64/package.json @@ -0,0 +1,25 @@ +{ + "name": "@atradio/cli-darwin-x64", + "version": "0.5.2", + "description": "Prebuilt atradio CLI binary for macOS (Intel / x86_64).", + "os": [ + "darwin" + ], + "cpu": [ + "x64" + ], + "files": [ + "bin/atradio" + ], + "license": "GPL-2.0-or-later", + "author": "Tsiry Sandratraina ", + "homepage": "https://github.com/tsirysndr/atradio.fm#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/tsirysndr/atradio.fm.git", + "directory": "cli/npm/platforms/darwin-x64" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/cli/npm/platforms/linux-arm64/package.json b/cli/npm/platforms/linux-arm64/package.json new file mode 100644 index 0000000..1324117 --- /dev/null +++ b/cli/npm/platforms/linux-arm64/package.json @@ -0,0 +1,25 @@ +{ + "name": "@atradio/cli-linux-arm64", + "version": "0.5.2", + "description": "Prebuilt atradio CLI binary for Linux (glibc, aarch64 / arm64).", + "os": [ + "linux" + ], + "cpu": [ + "arm64" + ], + "files": [ + "bin/atradio" + ], + "license": "GPL-2.0-or-later", + "author": "Tsiry Sandratraina ", + "homepage": "https://github.com/tsirysndr/atradio.fm#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/tsirysndr/atradio.fm.git", + "directory": "cli/npm/platforms/linux-arm64" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/cli/npm/platforms/linux-x64/package.json b/cli/npm/platforms/linux-x64/package.json new file mode 100644 index 0000000..a4cc0a3 --- /dev/null +++ b/cli/npm/platforms/linux-x64/package.json @@ -0,0 +1,25 @@ +{ + "name": "@atradio/cli-linux-x64", + "version": "0.5.2", + "description": "Prebuilt atradio CLI binary for Linux (glibc, x86_64 / x64).", + "os": [ + "linux" + ], + "cpu": [ + "x64" + ], + "files": [ + "bin/atradio" + ], + "license": "GPL-2.0-or-later", + "author": "Tsiry Sandratraina ", + "homepage": "https://github.com/tsirysndr/atradio.fm#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/tsirysndr/atradio.fm.git", + "directory": "cli/npm/platforms/linux-x64" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/cli/npm/scripts/stamp-version.mjs b/cli/npm/scripts/stamp-version.mjs new file mode 100644 index 0000000..873c3bf --- /dev/null +++ b/cli/npm/scripts/stamp-version.mjs @@ -0,0 +1,53 @@ +#!/usr/bin/env node +// Stamp a release version across the npm packages under cli/npm: +// - the main package (@atradio/cli) `version` +// - every platform package `version` +// - the main package's optionalDependencies, pinned to the exact version +// +// Usage: node scripts/stamp-version.mjs (leading "v" is stripped) + +import { readFileSync, writeFileSync, readdirSync, existsSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const raw = process.argv[2]; +if (!raw) { + console.error("usage: node scripts/stamp-version.mjs "); + process.exit(1); +} +const version = raw.replace(/^v/, ""); + +const npmDir = join(dirname(fileURLToPath(import.meta.url)), ".."); +const platformsDir = join(npmDir, "platforms"); + +function patch(pkgDir, mutate) { + const file = join(pkgDir, "package.json"); + const pkg = JSON.parse(readFileSync(file, "utf8")); + mutate(pkg); + writeFileSync(file, JSON.stringify(pkg, null, 2) + "\n"); + return pkg.name; +} + +// Platform packages first, so we know their names for the optionalDependencies. +const platformNames = []; +for (const entry of readdirSync(platformsDir, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const dir = join(platformsDir, entry.name); + if (!existsSync(join(dir, "package.json"))) continue; + const name = patch(dir, (pkg) => { + pkg.version = version; + }); + platformNames.push(name); +} + +// Main package: version + pin every optional dependency to this version. +patch(npmDir, (pkg) => { + pkg.version = version; + pkg.optionalDependencies = Object.fromEntries( + platformNames.sort().map((name) => [name, version]), + ); +}); + +console.log( + `Stamped ${version} across @atradio/cli and ${platformNames.length} platform packages.`, +);