From 14ca72938cc23612ff77a80f6b140db99ecfab64 Mon Sep 17 00:00:00 2001 From: opensource-elearning <159253500+opensource-elearning@users.noreply.github.com> Date: Sat, 5 Sep 2026 20:02:12 +0530 Subject: [PATCH 1/2] feat(release): add bun-pack script for bun-first global installs bun pm pack produces the same universal npm tarball npm pack does, so a bun-packed artifact installs with both 'bun add -g' and 'npm install -g'. Add scripts/release/bun-pack.mjs (--pm bun|npm, --skip-build, --destination) plus the bun:release npm script, and a Bun row in the README install table. The next build and dist assembly stay on Node; Bun handles the build orchestration, packing, and the end-user install. --- README.md | 1 + package.json | 1 + scripts/release/bun-pack.mjs | 151 +++++++++++++++++++++++++++++++++++ 3 files changed, 153 insertions(+) create mode 100644 scripts/release/bun-pack.mjs diff --git a/README.md b/README.md index b64f3dc543f..d5ad39c3b4b 100644 --- a/README.md +++ b/README.md @@ -722,6 +722,7 @@ of your shell history. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md) + diff --git a/package.json b/package.json index f984ef4314f..9a4456331fb 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,7 @@ "build:cli": "node --import tsx scripts/build/prepublish.ts", "omniroute:verify": "node scripts/check/omniroute-verify.mjs", "build:release": "rm -rf .build dist && OMNIROUTE_BUILD_SHA=$(git rev-parse --short HEAD) npm run build && npm run build:cli && node scripts/build/write-build-sha.mjs", + "bun:release": "bun scripts/release/bun-pack.mjs", "build:native:tproxy": "cd src/mitm/tproxy/native && npx --yes node-gyp rebuild", "start": "node scripts/dev/run-next.mjs start", "homolog": "node scripts/homolog/run.mjs", diff --git a/scripts/release/bun-pack.mjs b/scripts/release/bun-pack.mjs new file mode 100644 index 00000000000..70c6c4425fb --- /dev/null +++ b/scripts/release/bun-pack.mjs @@ -0,0 +1,151 @@ +#!/usr/bin/env node + +/** + * bun-pack — produce an npm-registry-compatible tarball, Bun-first. + * + * Bun is the default package manager for the whole flow (build orchestration + * and packing) because it is measurably faster and lighter than npm on the + * same machine. Node still runs the actual `next` build and the dist assembly + * (AGENTS.md: Bun is NOT widened into the build path, npm install, + * check:pack-artifact, or the published runtime) — `bun run` only executes the + * same Node build scripts npm would, without npm's overhead. + * + * The npm publish channel stays the release-captain flow + * (scripts/release/verify-published.mjs). `bun pm pack` and `npm pack` produce + * the SAME universal npm tarball format, so either packer's output installs + * with both `bun add -g ` and `npm install -g `. + * + * Usage: + * node scripts/release/bun-pack.mjs [--pm bun|npm] [--skip-build] [--destination ] + * + * Flags: + * --pm bun|npm Packer + build runner. Default: bun (npm is the fallback). + * --skip-build Reuse an already-staged dist/ (or run + * `npm run build:release` / `bun run build:release` first). + * Default: build first (needs the workspace node_modules). + * --destination Output directory. Default: /_artifacts. + * + * Output: /omniroute-.tgz (universal tarball, gzip) + * Verified: package.json, both bin entries, and dist/ are inside the tarball. + * Cross-platform: node >= 22, bun >= 1.1; no shell string interpolation + * (secrets/env travel as spawn options, never in the script body). + */ + +import { execFileSync, spawnSync } from "node:child_process"; +import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const ROOT = join(__dirname, "..", ".."); +const isWin = process.platform === "win32"; +const npmBin = isWin ? "npm.cmd" : "npm"; +const bunBin = isWin ? "bun.exe" : "bun"; + +function log(line) { + console.log(`[bun-pack] ${line}`); +} + +function fail(message, exitCode = 1) { + console.error(`[bun-pack] ❌ ${message}`); + process.exit(exitCode); +} + +function parseArgs(argv) { + const opts = { pm: "bun", skipBuild: false, destination: join(ROOT, "_artifacts") }; + for (let i = 0; i < argv.length; i += 1) { + if (argv[i] === "--pm") { + const pm = (argv[++i] || "").toLowerCase(); + if (pm !== "bun" && pm !== "npm") fail("--pm must be bun or npm"); + opts.pm = pm; + } else if (argv[i] === "--skip-build") { + opts.skipBuild = true; + } else if (argv[i] === "--destination") { + opts.destination = argv[++i]; + } else { + fail(`unknown argument "${argv[i]}"`); + } + } + if (!opts.destination) fail("--destination requires a directory path"); + return opts; +} + +function readVersion() { + const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")); + if (!/^\d+\.\d+\.\d+(-[A-Za-z0-9.-]+)?$/.test(pkg.version || "")) { + fail(`invalid version in package.json: ${pkg.version}`); + } + return pkg.version; +} + +function run(bin, args, opts = {}) { + return spawnSync(bin, args, { cwd: ROOT, encoding: "utf8", ...opts }); +} + +function verifyTarball(tarball) { + const list = run("tar", ["-tzf", tarball]); + if (list.status !== 0) fail(`could not read tarball listing (${list.error?.message || "tar failed"})`); + const entries = new Set((list.stdout || "").split("\n").map((l) => l.replace(/\/$/, ""))); + + const requiredFiles = [ + "package/package.json", + "package/bin/omniroute.mjs", + "package/bin/reset-password.mjs", + ]; + const missing = requiredFiles.filter((entry) => !entries.has(entry)); + if (missing.length > 0) fail(`tarball missing required entries: ${missing.join(", ")}`); + + const distFiles = [...entries].filter((e) => e.startsWith("package/dist/")); + if (distFiles.length === 0) fail("tarball contains an empty dist/ — run the build first"); + return distFiles.length; +} + +log("OmniRoute release pack (npm-compatible tarball)"); +if (!existsSync(join(ROOT, "package.json"))) fail(`no package.json at ${ROOT}`); + +const opts = parseArgs(process.argv.slice(2)); +const version = readVersion(); +const tarball = join(opts.destination, `omniroute-${version}.tgz`); +const isBun = opts.pm === "bun"; + +if (isBun) { + const bunCheck = run(bunBin, ["--version"]); + if (bunCheck.status !== 0) fail("bun not found — run `npm run bun:release` or install bun (curl -fsSL https://bun.sh/install | bash)"); + log(`packer: bun ${bunCheck.stdout.trim()}`); +} else { + log("packer: npm (fallback)"); +} + +if (opts.skipBuild) { + if (!existsSync(join(ROOT, "dist", "server.js"))) { + fail("dist/ not staged — run `bun run build:release` first, or drop --skip-build"); + } + log("reusing existing dist/ (--skip-build)"); +} else { + const runner = isBun ? bunBin : npmBin; + log(`running ${opts.pm} run build:release (Node runs the actual build)`); + const b = run(runner, ["run", "build:release"], { stdio: "inherit" }); + if (b.status !== 0) fail(`build:release failed (exit ${b.status})`); +} + +if (!existsSync(opts.destination)) mkdirSync(opts.destination, { recursive: true }); +if (existsSync(tarball)) { + log(`removing stale ${tarball}`); + rmSync(tarball, { force: true }); +} + +const packArgs = + opts.pm === "npm" + ? ["pack", "--ignore-scripts", "--pack-destination", opts.destination, "--json"] + : ["pm", "pack", "--destination", opts.destination, "--ignore-scripts", "--quiet"]; +const p = run(isBun ? bunBin : npmBin, packArgs, { stdio: "inherit" }); +if (p.status !== 0) fail(`${opts.pm} pack failed (exit ${p.status})`); + +if (!existsSync(tarball)) fail(`${opts.pm} pack completed but ${tarball} was not created`); + +const distFiles = verifyTarball(tarball); +log(`✅ packed ${tarball} (${distFiles} tracked dist/ entries verified)`); +log(`install (bun): bun add -g ${tarball}`); +log(`install (npm): npm install -g ${tarball}`); +log(`uninstall (bun): bun remove -g omniroute`); +log(`run: omniroute serve`); \ No newline at end of file From 52b2096b4cb43cbac7786bdd7696002420dd1e9f Mon Sep 17 00:00:00 2001 From: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Date: Fri, 18 Sep 2026 13:48:43 -0300 Subject: [PATCH 2/2] test(release): add unit coverage for bun-pack parseArgs/verifyTarball Extract the two pure pieces of logic in scripts/release/bun-pack.mjs so they are importable and testable without running a real build: parseArgs and verifyTarball now throw UsageError on bad input instead of calling fail()/process.exit directly, and the top-level script body is wrapped in main() behind a direct-run guard (import.meta.url check) that still translates a UsageError into the same fail() CLI output as before - verified manually that --pm bogus / --skip-build without dist/ / an unknown flag print identical messages and exit codes to pre-change behavior. Adds tests/unit/bun-pack.test.ts (node:test) covering parseArgs flag parsing, defaults and bad input, and verifyTarball against real tarballs built in a temp dir (good tarball, missing/corrupted tarball, missing required bin entry, empty dist/) - satisfies Hard Rule #8 for this PR's production code. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --- scripts/release/bun-pack.mjs | 141 ++++++++++++++++---------- tests/unit/bun-pack.test.ts | 187 +++++++++++++++++++++++++++++++++++ 2 files changed, 275 insertions(+), 53 deletions(-) create mode 100644 tests/unit/bun-pack.test.ts diff --git a/scripts/release/bun-pack.mjs b/scripts/release/bun-pack.mjs index 70c6c4425fb..d89c6f79406 100644 --- a/scripts/release/bun-pack.mjs +++ b/scripts/release/bun-pack.mjs @@ -29,11 +29,16 @@ * Verified: package.json, both bin entries, and dist/ are inside the tarball. * Cross-platform: node >= 22, bun >= 1.1; no shell string interpolation * (secrets/env travel as spawn options, never in the script body). + * + * `parseArgs` and `verifyTarball` are pure (no `process.exit`) and exported so + * they are unit-testable without a real build — see tests/unit/bun-pack.test.ts. + * They signal a bad-input/bad-tarball condition by throwing `UsageError`; only + * the direct-run `main()` below translates that into the CLI's `fail()` exit. */ -import { execFileSync, spawnSync } from "node:child_process"; +import { spawnSync } from "node:child_process"; import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs"; -import { dirname, join } from "node:path"; +import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -42,6 +47,8 @@ const isWin = process.platform === "win32"; const npmBin = isWin ? "npm.cmd" : "npm"; const bunBin = isWin ? "bun.exe" : "bun"; +export class UsageError extends Error {} + function log(line) { console.log(`[bun-pack] ${line}`); } @@ -51,22 +58,22 @@ function fail(message, exitCode = 1) { process.exit(exitCode); } -function parseArgs(argv) { - const opts = { pm: "bun", skipBuild: false, destination: join(ROOT, "_artifacts") }; +export function parseArgs(argv, defaultDestination = join(ROOT, "_artifacts")) { + const opts = { pm: "bun", skipBuild: false, destination: defaultDestination }; for (let i = 0; i < argv.length; i += 1) { if (argv[i] === "--pm") { const pm = (argv[++i] || "").toLowerCase(); - if (pm !== "bun" && pm !== "npm") fail("--pm must be bun or npm"); + if (pm !== "bun" && pm !== "npm") throw new UsageError("--pm must be bun or npm"); opts.pm = pm; } else if (argv[i] === "--skip-build") { opts.skipBuild = true; } else if (argv[i] === "--destination") { opts.destination = argv[++i]; } else { - fail(`unknown argument "${argv[i]}"`); + throw new UsageError(`unknown argument "${argv[i]}"`); } } - if (!opts.destination) fail("--destination requires a directory path"); + if (!opts.destination) throw new UsageError("--destination requires a directory path"); return opts; } @@ -82,9 +89,11 @@ function run(bin, args, opts = {}) { return spawnSync(bin, args, { cwd: ROOT, encoding: "utf8", ...opts }); } -function verifyTarball(tarball) { +export function verifyTarball(tarball) { const list = run("tar", ["-tzf", tarball]); - if (list.status !== 0) fail(`could not read tarball listing (${list.error?.message || "tar failed"})`); + if (list.status !== 0) { + throw new UsageError(`could not read tarball listing (${list.error?.message || "tar failed"})`); + } const entries = new Set((list.stdout || "").split("\n").map((l) => l.replace(/\/$/, ""))); const requiredFiles = [ @@ -93,59 +102,85 @@ function verifyTarball(tarball) { "package/bin/reset-password.mjs", ]; const missing = requiredFiles.filter((entry) => !entries.has(entry)); - if (missing.length > 0) fail(`tarball missing required entries: ${missing.join(", ")}`); + if (missing.length > 0) + throw new UsageError(`tarball missing required entries: ${missing.join(", ")}`); const distFiles = [...entries].filter((e) => e.startsWith("package/dist/")); - if (distFiles.length === 0) fail("tarball contains an empty dist/ — run the build first"); + if (distFiles.length === 0) + throw new UsageError("tarball contains an empty dist/ — run the build first"); return distFiles.length; } -log("OmniRoute release pack (npm-compatible tarball)"); -if (!existsSync(join(ROOT, "package.json"))) fail(`no package.json at ${ROOT}`); +function main() { + log("OmniRoute release pack (npm-compatible tarball)"); + if (!existsSync(join(ROOT, "package.json"))) fail(`no package.json at ${ROOT}`); -const opts = parseArgs(process.argv.slice(2)); -const version = readVersion(); -const tarball = join(opts.destination, `omniroute-${version}.tgz`); -const isBun = opts.pm === "bun"; + let opts; + try { + opts = parseArgs(process.argv.slice(2)); + } catch (err) { + if (err instanceof UsageError) fail(err.message); + throw err; + } -if (isBun) { - const bunCheck = run(bunBin, ["--version"]); - if (bunCheck.status !== 0) fail("bun not found — run `npm run bun:release` or install bun (curl -fsSL https://bun.sh/install | bash)"); - log(`packer: bun ${bunCheck.stdout.trim()}`); -} else { - log("packer: npm (fallback)"); -} + const version = readVersion(); + const tarball = join(opts.destination, `omniroute-${version}.tgz`); + const isBun = opts.pm === "bun"; -if (opts.skipBuild) { - if (!existsSync(join(ROOT, "dist", "server.js"))) { - fail("dist/ not staged — run `bun run build:release` first, or drop --skip-build"); + if (isBun) { + const bunCheck = run(bunBin, ["--version"]); + if (bunCheck.status !== 0) { + fail( + "bun not found — run `npm run bun:release` or install bun (curl -fsSL https://bun.sh/install | bash)" + ); + } + log(`packer: bun ${bunCheck.stdout.trim()}`); + } else { + log("packer: npm (fallback)"); } - log("reusing existing dist/ (--skip-build)"); -} else { - const runner = isBun ? bunBin : npmBin; - log(`running ${opts.pm} run build:release (Node runs the actual build)`); - const b = run(runner, ["run", "build:release"], { stdio: "inherit" }); - if (b.status !== 0) fail(`build:release failed (exit ${b.status})`); -} -if (!existsSync(opts.destination)) mkdirSync(opts.destination, { recursive: true }); -if (existsSync(tarball)) { - log(`removing stale ${tarball}`); - rmSync(tarball, { force: true }); + if (opts.skipBuild) { + if (!existsSync(join(ROOT, "dist", "server.js"))) { + fail("dist/ not staged — run `bun run build:release` first, or drop --skip-build"); + } + log("reusing existing dist/ (--skip-build)"); + } else { + const runner = isBun ? bunBin : npmBin; + log(`running ${opts.pm} run build:release (Node runs the actual build)`); + const b = run(runner, ["run", "build:release"], { stdio: "inherit" }); + if (b.status !== 0) fail(`build:release failed (exit ${b.status})`); + } + + if (!existsSync(opts.destination)) mkdirSync(opts.destination, { recursive: true }); + if (existsSync(tarball)) { + log(`removing stale ${tarball}`); + rmSync(tarball, { force: true }); + } + + const packArgs = + opts.pm === "npm" + ? ["pack", "--ignore-scripts", "--pack-destination", opts.destination, "--json"] + : ["pm", "pack", "--destination", opts.destination, "--ignore-scripts", "--quiet"]; + const p = run(isBun ? bunBin : npmBin, packArgs, { stdio: "inherit" }); + if (p.status !== 0) fail(`${opts.pm} pack failed (exit ${p.status})`); + + if (!existsSync(tarball)) fail(`${opts.pm} pack completed but ${tarball} was not created`); + + let distFiles; + try { + distFiles = verifyTarball(tarball); + } catch (err) { + if (err instanceof UsageError) fail(err.message); + throw err; + } + log(`✅ packed ${tarball} (${distFiles} tracked dist/ entries verified)`); + log(`install (bun): bun add -g ${tarball}`); + log(`install (npm): npm install -g ${tarball}`); + log(`uninstall (bun): bun remove -g omniroute`); + log(`run: omniroute serve`); } -const packArgs = - opts.pm === "npm" - ? ["pack", "--ignore-scripts", "--pack-destination", opts.destination, "--json"] - : ["pm", "pack", "--destination", opts.destination, "--ignore-scripts", "--quiet"]; -const p = run(isBun ? bunBin : npmBin, packArgs, { stdio: "inherit" }); -if (p.status !== 0) fail(`${opts.pm} pack failed (exit ${p.status})`); - -if (!existsSync(tarball)) fail(`${opts.pm} pack completed but ${tarball} was not created`); - -const distFiles = verifyTarball(tarball); -log(`✅ packed ${tarball} (${distFiles} tracked dist/ entries verified)`); -log(`install (bun): bun add -g ${tarball}`); -log(`install (npm): npm install -g ${tarball}`); -log(`uninstall (bun): bun remove -g omniroute`); -log(`run: omniroute serve`); \ No newline at end of file +// direct-run guard (importable for tests) +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + main(); +} diff --git a/tests/unit/bun-pack.test.ts b/tests/unit/bun-pack.test.ts new file mode 100644 index 00000000000..e92e6f895e2 --- /dev/null +++ b/tests/unit/bun-pack.test.ts @@ -0,0 +1,187 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import { + parseArgs, + verifyTarball, + UsageError, + // @ts-expect-error — .mjs helper has no type declarations; runtime shape is known. +} from "../../scripts/release/bun-pack.mjs"; + +// Everything below runs against os.tmpdir() temp directories only — never the +// real DATA_DIR / repo _artifacts. + +function mkTmpDir(prefix: string): string { + return fs.mkdtempSync(path.join(os.tmpdir(), prefix)); +} + +function buildTarball( + dir: string, + { + withPackageJson = true, + withBinOmniroute = true, + withBinResetPassword = true, + withDistFile = true, + }: { + withPackageJson?: boolean; + withBinOmniroute?: boolean; + withBinResetPassword?: boolean; + withDistFile?: boolean; + } = {} +): string { + const stage = path.join(dir, "stage"); + const pkgDir = path.join(stage, "package"); + fs.mkdirSync(path.join(pkgDir, "bin"), { recursive: true }); + fs.mkdirSync(path.join(pkgDir, "dist"), { recursive: true }); + + if (withPackageJson) { + fs.writeFileSync(path.join(pkgDir, "package.json"), JSON.stringify({ name: "omniroute" })); + } + if (withBinOmniroute) { + fs.writeFileSync(path.join(pkgDir, "bin", "omniroute.mjs"), "// stub\n"); + } + if (withBinResetPassword) { + fs.writeFileSync(path.join(pkgDir, "bin", "reset-password.mjs"), "// stub\n"); + } + if (withDistFile) { + fs.writeFileSync(path.join(pkgDir, "dist", "server.js"), "// stub\n"); + } + + const tarball = path.join(dir, "omniroute-test.tgz"); + const tarResult = spawnSync("tar", ["czf", tarball, "-C", stage, "package"], { + encoding: "utf8", + }); + assert.equal(tarResult.status, 0, `failed to build fixture tarball: ${tarResult.stderr}`); + return tarball; +} + +// --------------------------------------------------------------------------- +// parseArgs +// --------------------------------------------------------------------------- + +test("parseArgs: defaults to bun/no-skip-build/given destination when no flags are passed", () => { + const opts = parseArgs([], "/tmp/fake-artifacts"); + assert.deepEqual(opts, { pm: "bun", skipBuild: false, destination: "/tmp/fake-artifacts" }); +}); + +test("parseArgs: --pm npm is accepted and overrides the default", () => { + const opts = parseArgs(["--pm", "npm"], "/tmp/fake-artifacts"); + assert.equal(opts.pm, "npm"); +}); + +test("parseArgs: --pm bun is accepted explicitly", () => { + const opts = parseArgs(["--pm", "bun"], "/tmp/fake-artifacts"); + assert.equal(opts.pm, "bun"); +}); + +test("parseArgs: --pm is case-insensitive", () => { + const opts = parseArgs(["--pm", "NPM"], "/tmp/fake-artifacts"); + assert.equal(opts.pm, "npm"); +}); + +test("parseArgs: --skip-build flips skipBuild to true", () => { + const opts = parseArgs(["--skip-build"], "/tmp/fake-artifacts"); + assert.equal(opts.skipBuild, true); +}); + +test("parseArgs: --destination overrides the default output directory", () => { + const opts = parseArgs(["--destination", "/tmp/custom-out"], "/tmp/fake-artifacts"); + assert.equal(opts.destination, "/tmp/custom-out"); +}); + +test("parseArgs: combines multiple flags together", () => { + const opts = parseArgs( + ["--pm", "npm", "--skip-build", "--destination", "/tmp/custom-out"], + "/tmp/fake-artifacts" + ); + assert.deepEqual(opts, { pm: "npm", skipBuild: true, destination: "/tmp/custom-out" }); +}); + +test("parseArgs: rejects an invalid --pm value with UsageError (bad input, does not exit)", () => { + assert.throws( + () => parseArgs(["--pm", "bogus"], "/tmp/fake-artifacts"), + (err: unknown) => err instanceof UsageError && err.message === "--pm must be bun or npm" + ); +}); + +test("parseArgs: rejects an unrecognized flag with UsageError (bad input)", () => { + assert.throws( + () => parseArgs(["--unknown-flag"], "/tmp/fake-artifacts"), + (err: unknown) => + err instanceof UsageError && /unknown argument "--unknown-flag"/.test(err.message) + ); +}); + +test("parseArgs: --destination with no value throws UsageError (bad input)", () => { + assert.throws( + () => parseArgs(["--destination"], "/tmp/fake-artifacts"), + (err: unknown) => + err instanceof UsageError && /--destination requires a directory path/.test(err.message) + ); +}); + +// --------------------------------------------------------------------------- +// verifyTarball +// --------------------------------------------------------------------------- + +test("verifyTarball: a well-formed tarball passes and reports its dist/ entry count", (t) => { + const dir = mkTmpDir("bun-pack-good-"); + t.after(() => fs.rmSync(dir, { recursive: true, force: true })); + const tarball = buildTarball(dir); + + const distFiles = verifyTarball(tarball); + assert.equal(distFiles, 1); +}); + +test("verifyTarball: a missing tarball path throws UsageError instead of crashing", (t) => { + const dir = mkTmpDir("bun-pack-missing-"); + t.after(() => fs.rmSync(dir, { recursive: true, force: true })); + const missingPath = path.join(dir, "does-not-exist.tgz"); + + assert.throws( + () => verifyTarball(missingPath), + (err: unknown) => + err instanceof UsageError && /could not read tarball listing/.test(err.message) + ); +}); + +test("verifyTarball: a corrupted (non-gzip) tarball throws UsageError", (t) => { + const dir = mkTmpDir("bun-pack-corrupt-"); + t.after(() => fs.rmSync(dir, { recursive: true, force: true })); + const corrupted = path.join(dir, "corrupted.tgz"); + fs.writeFileSync(corrupted, "this is not a gzip tarball at all"); + + assert.throws( + () => verifyTarball(corrupted), + (err: unknown) => + err instanceof UsageError && /could not read tarball listing/.test(err.message) + ); +}); + +test("verifyTarball: a tarball missing a required bin entry throws UsageError naming it", (t) => { + const dir = mkTmpDir("bun-pack-missing-bin-"); + t.after(() => fs.rmSync(dir, { recursive: true, force: true })); + const tarball = buildTarball(dir, { withBinResetPassword: false }); + + assert.throws( + () => verifyTarball(tarball), + (err: unknown) => + err instanceof UsageError && + /tarball missing required entries: package\/bin\/reset-password\.mjs/.test(err.message) + ); +}); + +test("verifyTarball: a tarball with an empty dist/ throws UsageError", (t) => { + const dir = mkTmpDir("bun-pack-empty-dist-"); + t.after(() => fs.rmSync(dir, { recursive: true, force: true })); + const tarball = buildTarball(dir, { withDistFile: false }); + + assert.throws( + () => verifyTarball(tarball), + (err: unknown) => err instanceof UsageError && /empty dist/.test(err.message) + ); +});
PlatformInstallHighlights
📦 npm (global)npm install -g omnirouteOne command, any OS
⚡ Bun (global)bun add -g omnirouteSame registry tarball, faster installs
🐳 Dockerdocker run … diegosouzapw/omnirouteMulti-arch AMD64 + ARM64
🖥️ Desktop (Electron)npm run electron:buildNative window + system tray — Windows / macOS / Linux
🎩 Menu-bar (OmniRouteTray)brew install --cask zoispag/tap/omniroute-traySupervises & auto-updates the server — macOS