From 759d0d3fe105a3d2e5ccbed7553bafeb632559c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Halber?= Date: Tue, 26 May 2026 15:42:43 -0700 Subject: [PATCH 1/4] feat: package bt as npm pkg --- .github/workflows/release.yml | 59 ++++++++++ npm/bt/README.md | 41 +++++++ npm/bt/bin/bt.js | 43 +++++++ npm/bt/lib/platform.js | 46 ++++++++ npm/bt/package.json | 35 ++++++ npm/scripts/build-platform-packages.mjs | 149 ++++++++++++++++++++++++ npm/targets.json | 54 +++++++++ 7 files changed, 427 insertions(+) create mode 100644 npm/bt/README.md create mode 100644 npm/bt/bin/bt.js create mode 100644 npm/bt/lib/platform.js create mode 100644 npm/bt/package.json create mode 100644 npm/scripts/build-platform-packages.mjs create mode 100644 npm/targets.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f32a75f..b7c7bc9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -488,3 +488,62 @@ jobs: & $btExe --version $env:PATH = "$binDir;$env:PATH" & $btExe self update --check --channel stable + + publish-npm: + needs: + - plan + - announce + if: ${{ needs.announce.result == 'success' }} + runs-on: ubuntu-22.04 + environment: release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false + + - name: Download release archives + env: + TAG: ${{ needs.plan.outputs.tag }} + shell: bash + run: | + set -euo pipefail + mkdir -p archives + gh release download "$TAG" \ + --repo "${{ github.repository }}" \ + --pattern 'bt-*.tar.gz' \ + --pattern 'bt-*.zip' \ + --dir archives + ls -la archives + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: "20" + registry-url: "https://registry.npmjs.org" + + - name: Build npm packages + run: | + node npm/scripts/build-platform-packages.mjs \ + --version "${{ needs.plan.outputs.release-version }}" \ + --archives-dir archives \ + --out-dir npm/dist + + - name: Publish platform packages + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + shell: bash + run: | + set -euo pipefail + for dir in npm/dist/bt-*; do + echo "Publishing $dir" + (cd "$dir" && npm publish --access public --provenance) + done + + - name: Publish wrapper + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + shell: bash + run: | + cd npm/dist/bt + npm publish --access public --provenance diff --git a/npm/bt/README.md b/npm/bt/README.md new file mode 100644 index 0000000..3df1c16 --- /dev/null +++ b/npm/bt/README.md @@ -0,0 +1,41 @@ +# @braintrust/bt + +The [Braintrust](https://www.braintrust.dev) command-line interface (`bt`), +distributed as an npm package. + +## Install + +```bash +npm install -g @braintrust/bt +# or +pnpm add -g @braintrust/bt +``` + +Then: + +```bash +bt --help +``` + +## How it works + +This package is a thin Node.js launcher. The actual `bt` binary is shipped via +platform-specific packages declared as `optionalDependencies`: + +- `@braintrust/bt-darwin-arm64` +- `@braintrust/bt-darwin-x64` +- `@braintrust/bt-linux-arm64` +- `@braintrust/bt-linux-x64` +- `@braintrust/bt-linux-x64-musl` +- `@braintrust/bt-win32-arm64` +- `@braintrust/bt-win32-x64` + +`npm/pnpm/yarn` install only the one matching your OS + CPU (+ libc on Linux). + +If your platform isn't supported, see + for alternative installation methods +(shell installer, building with `cargo`). + +## License + +Apache-2.0 diff --git a/npm/bt/bin/bt.js b/npm/bt/bin/bt.js new file mode 100644 index 0000000..d655b65 --- /dev/null +++ b/npm/bt/bin/bt.js @@ -0,0 +1,43 @@ +#!/usr/bin/env node +"use strict"; + +const { spawnSync } = require("node:child_process"); +const { + PLATFORM_PACKAGES, + currentPlatformPackage, + binaryName, +} = require("../lib/platform"); + +function resolveBinary() { + const pkg = currentPlatformPackage(); + if (!pkg) { + throw new Error( + `No prebuilt bt binary for ${process.platform}-${process.arch}. ` + + `Supported: ${Object.keys(PLATFORM_PACKAGES).join(", ")}. ` + + `See https://github.com/braintrustdata/bt for other install methods.`, + ); + } + try { + return require.resolve(`${pkg}/bin/${binaryName()}`); + } catch (err) { + throw new Error( + `Failed to locate ${pkg}. This package is shipped via optionalDependencies; ` + + `if you installed with --ignore-optional / --no-optional, reinstall without ` + + `that flag, or use the shell installer at https://github.com/braintrustdata/bt. ` + + `(${err.message})`, + ); + } +} + +try { + const binary = resolveBinary(); + const result = spawnSync(binary, process.argv.slice(2), { + stdio: "inherit", + windowsHide: true, + }); + if (result.error) throw result.error; + process.exit(result.status ?? 1); +} catch (err) { + console.error(`bt: ${err.message}`); + process.exit(1); +} diff --git a/npm/bt/lib/platform.js b/npm/bt/lib/platform.js new file mode 100644 index 0000000..5889a3c --- /dev/null +++ b/npm/bt/lib/platform.js @@ -0,0 +1,46 @@ +"use strict"; + +const PLATFORM_PACKAGES = { + "darwin-arm64": "@braintrust/bt-darwin-arm64", + "darwin-x64": "@braintrust/bt-darwin-x64", + "linux-arm64": "@braintrust/bt-linux-arm64", + "linux-x64-glibc": "@braintrust/bt-linux-x64", + "linux-x64-musl": "@braintrust/bt-linux-x64-musl", + "win32-arm64": "@braintrust/bt-win32-arm64", + "win32-x64": "@braintrust/bt-win32-x64", +}; + +function detectLibc() { + if (process.platform !== "linux") return null; + try { + const report = process.report && process.report.getReport(); + if (report && report.header && report.header.glibcVersionRuntime) { + return "glibc"; + } + return "musl"; + } catch { + return "glibc"; + } +} + +function platformKey() { + const { platform, arch } = process; + if (platform === "linux" && arch === "x64") { + return `linux-x64-${detectLibc()}`; + } + return `${platform}-${arch}`; +} + +function currentPlatformPackage() { + return PLATFORM_PACKAGES[platformKey()] || null; +} + +function binaryName() { + return process.platform === "win32" ? "bt.exe" : "bt"; +} + +module.exports = { + PLATFORM_PACKAGES, + currentPlatformPackage, + binaryName, +}; diff --git a/npm/bt/package.json b/npm/bt/package.json new file mode 100644 index 0000000..6a8ae54 --- /dev/null +++ b/npm/bt/package.json @@ -0,0 +1,35 @@ +{ + "name": "@braintrust/bt", + "version": "0.0.0", + "description": "The Braintrust command line interface", + "homepage": "https://github.com/braintrustdata/bt", + "repository": { + "type": "git", + "url": "git+https://github.com/braintrustdata/bt.git" + }, + "license": "Apache-2.0", + "author": "Braintrust engineering ", + "bin": { + "bt": "bin/bt.js" + }, + "files": [ + "bin/", + "lib/" + ], + "publishConfig": { + "access": "public", + "provenance": true + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@braintrust/bt-darwin-arm64": "0.0.0", + "@braintrust/bt-darwin-x64": "0.0.0", + "@braintrust/bt-linux-arm64": "0.0.0", + "@braintrust/bt-linux-x64": "0.0.0", + "@braintrust/bt-linux-x64-musl": "0.0.0", + "@braintrust/bt-win32-arm64": "0.0.0", + "@braintrust/bt-win32-x64": "0.0.0" + } +} diff --git a/npm/scripts/build-platform-packages.mjs b/npm/scripts/build-platform-packages.mjs new file mode 100644 index 0000000..cdc34f5 --- /dev/null +++ b/npm/scripts/build-platform-packages.mjs @@ -0,0 +1,149 @@ +#!/usr/bin/env node +// Builds the per-platform npm packages from cargo-dist release archives. +// +// --version version to stamp into every package.json (required) +// --archives-dir directory containing cargo-dist archives +// (bt-.tar.gz / bt-.zip), required +// --out-dir directory to write packages into (default: npm/dist) +// +// Emits /bt/ (wrapper) and /bt-/ (one per target), +// each ready to `npm publish`. + +import { execFileSync } from "node:child_process"; +import { + chmodSync, + cpSync, + existsSync, + mkdirSync, + readFileSync, + readdirSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const NPM_DIR = resolve(__dirname, ".."); + +function parseArgs(argv) { + const args = {}; + for (let i = 0; i < argv.length; i++) { + const k = argv[i]; + if (k.startsWith("--")) args[k.slice(2)] = argv[++i]; + } + return args; +} + +const args = parseArgs(process.argv.slice(2)); +const version = args.version; +const archivesDir = args["archives-dir"] && resolve(args["archives-dir"]); +const outDir = resolve(args["out-dir"] ?? join(NPM_DIR, "dist")); + +if (!version) throw new Error("--version is required"); +if (!archivesDir) throw new Error("--archives-dir is required"); +if (!existsSync(archivesDir)) + throw new Error(`archives-dir not found: ${archivesDir}`); + +const targets = JSON.parse(readFileSync(join(NPM_DIR, "targets.json"), "utf8")); + +if (existsSync(outDir)) rmSync(outDir, { recursive: true, force: true }); +mkdirSync(outDir, { recursive: true }); + +function extract(archive, dest) { + mkdirSync(dest, { recursive: true }); + if (archive.endsWith(".tar.gz")) { + execFileSync("tar", ["-xzf", archive, "-C", dest], { stdio: "inherit" }); + } else if (archive.endsWith(".zip")) { + execFileSync("unzip", ["-o", "-q", archive, "-d", dest], { + stdio: "inherit", + }); + } else { + throw new Error(`Unsupported archive: ${archive}`); + } +} + +function findBinary(rootDir, binName) { + const stack = [rootDir]; + while (stack.length) { + const cur = stack.pop(); + for (const entry of readdirSync(cur)) { + const full = join(cur, entry); + const s = statSync(full); + if (s.isDirectory()) stack.push(full); + else if (entry === binName) return full; + } + } + throw new Error(`Binary ${binName} not found under ${rootDir}`); +} + +// --- Wrapper package --- +const wrapperSrc = join(NPM_DIR, "bt"); +const wrapperOut = join(outDir, "bt"); +cpSync(wrapperSrc, wrapperOut, { recursive: true }); +const wrapperPkgPath = join(wrapperOut, "package.json"); +const wrapperPkg = JSON.parse(readFileSync(wrapperPkgPath, "utf8")); +wrapperPkg.version = version; +for (const key of Object.keys(wrapperPkg.optionalDependencies ?? {})) { + wrapperPkg.optionalDependencies[key] = version; +} +writeFileSync(wrapperPkgPath, JSON.stringify(wrapperPkg, null, 2) + "\n"); +console.log(`Built wrapper -> ${wrapperOut}`); + +// --- Per-platform packages --- +for (const [target, spec] of Object.entries(targets)) { + const archiveName = `bt-${target}.${spec.archiveExt}`; + const archive = join(archivesDir, archiveName); + if (!existsSync(archive)) { + console.warn(`SKIP ${target}: archive not found (${archive})`); + continue; + } + + const stagingDir = join(outDir, ".staging", target); + extract(archive, stagingDir); + const binPath = findBinary(stagingDir, spec.bin); + + const pkgName = `@braintrust/bt-${spec.pkg}`; + const pkgOut = join(outDir, `bt-${spec.pkg}`); + const pkgBin = join(pkgOut, "bin"); + mkdirSync(pkgBin, { recursive: true }); + cpSync(binPath, join(pkgBin, spec.bin)); + if (spec.os !== "win32") chmodSync(join(pkgBin, spec.bin), 0o755); + + const platformPkg = { + name: pkgName, + version, + description: `Prebuilt bt binary for ${spec.os}-${spec.cpu}${spec.libc ? `-${spec.libc}` : ""}`, + homepage: "https://github.com/braintrustdata/bt", + repository: { + type: "git", + url: "git+https://github.com/braintrustdata/bt.git", + }, + license: "Apache-2.0", + author: "Braintrust engineering ", + files: ["bin/"], + os: [spec.os], + cpu: [spec.cpu], + publishConfig: { + access: "public", + provenance: true, + }, + preferUnplugged: true, + }; + if (spec.libc) platformPkg.libc = [spec.libc]; + + writeFileSync( + join(pkgOut, "package.json"), + JSON.stringify(platformPkg, null, 2) + "\n", + ); + writeFileSync( + join(pkgOut, "README.md"), + `# ${pkgName}\n\nPrebuilt \`bt\` binary for ${spec.os}-${spec.cpu}${spec.libc ? ` (${spec.libc})` : ""}.\n\nInstalled automatically as an optional dependency of [\`@braintrust/bt\`](https://www.npmjs.com/package/@braintrust/bt). Install that package instead.\n`, + ); + + console.log(`Built ${pkgName} -> ${pkgOut}`); +} + +rmSync(join(outDir, ".staging"), { recursive: true, force: true }); +console.log(`\nAll packages written to ${outDir}`); diff --git a/npm/targets.json b/npm/targets.json new file mode 100644 index 0000000..21b6cda --- /dev/null +++ b/npm/targets.json @@ -0,0 +1,54 @@ +{ + "aarch64-apple-darwin": { + "pkg": "darwin-arm64", + "os": "darwin", + "cpu": "arm64", + "bin": "bt", + "archiveExt": "tar.gz" + }, + "x86_64-apple-darwin": { + "pkg": "darwin-x64", + "os": "darwin", + "cpu": "x64", + "bin": "bt", + "archiveExt": "tar.gz" + }, + "aarch64-unknown-linux-gnu": { + "pkg": "linux-arm64", + "os": "linux", + "cpu": "arm64", + "libc": "glibc", + "bin": "bt", + "archiveExt": "tar.gz" + }, + "x86_64-unknown-linux-gnu": { + "pkg": "linux-x64", + "os": "linux", + "cpu": "x64", + "libc": "glibc", + "bin": "bt", + "archiveExt": "tar.gz" + }, + "x86_64-unknown-linux-musl": { + "pkg": "linux-x64-musl", + "os": "linux", + "cpu": "x64", + "libc": "musl", + "bin": "bt", + "archiveExt": "tar.gz" + }, + "aarch64-pc-windows-msvc": { + "pkg": "win32-arm64", + "os": "win32", + "cpu": "arm64", + "bin": "bt.exe", + "archiveExt": "zip" + }, + "x86_64-pc-windows-msvc": { + "pkg": "win32-x64", + "os": "win32", + "cpu": "x64", + "bin": "bt.exe", + "archiveExt": "zip" + } +} From aa2507b3c60166eea6137b1a33c05986d24b3f51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Halber?= Date: Thu, 28 May 2026 12:25:35 -0700 Subject: [PATCH 2/4] chore: update name from bt to cli --- npm/bt/README.md | 20 ++++++++++---------- npm/bt/lib/platform.js | 14 +++++++------- npm/bt/package.json | 16 ++++++++-------- npm/scripts/build-platform-packages.mjs | 4 ++-- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/npm/bt/README.md b/npm/bt/README.md index 3df1c16..c25987e 100644 --- a/npm/bt/README.md +++ b/npm/bt/README.md @@ -1,4 +1,4 @@ -# @braintrust/bt +# @braintrust/cli The [Braintrust](https://www.braintrust.dev) command-line interface (`bt`), distributed as an npm package. @@ -6,9 +6,9 @@ distributed as an npm package. ## Install ```bash -npm install -g @braintrust/bt +npm install -g @braintrust/cli # or -pnpm add -g @braintrust/bt +pnpm add -g @braintrust/cli ``` Then: @@ -22,13 +22,13 @@ bt --help This package is a thin Node.js launcher. The actual `bt` binary is shipped via platform-specific packages declared as `optionalDependencies`: -- `@braintrust/bt-darwin-arm64` -- `@braintrust/bt-darwin-x64` -- `@braintrust/bt-linux-arm64` -- `@braintrust/bt-linux-x64` -- `@braintrust/bt-linux-x64-musl` -- `@braintrust/bt-win32-arm64` -- `@braintrust/bt-win32-x64` +- `@braintrust/cli-darwin-arm64` +- `@braintrust/cli-darwin-x64` +- `@braintrust/cli-linux-arm64` +- `@braintrust/cli-linux-x64` +- `@braintrust/cli-linux-x64-musl` +- `@braintrust/cli-win32-arm64` +- `@braintrust/cli-win32-x64` `npm/pnpm/yarn` install only the one matching your OS + CPU (+ libc on Linux). diff --git a/npm/bt/lib/platform.js b/npm/bt/lib/platform.js index 5889a3c..6ba0f94 100644 --- a/npm/bt/lib/platform.js +++ b/npm/bt/lib/platform.js @@ -1,13 +1,13 @@ "use strict"; const PLATFORM_PACKAGES = { - "darwin-arm64": "@braintrust/bt-darwin-arm64", - "darwin-x64": "@braintrust/bt-darwin-x64", - "linux-arm64": "@braintrust/bt-linux-arm64", - "linux-x64-glibc": "@braintrust/bt-linux-x64", - "linux-x64-musl": "@braintrust/bt-linux-x64-musl", - "win32-arm64": "@braintrust/bt-win32-arm64", - "win32-x64": "@braintrust/bt-win32-x64", + "darwin-arm64": "@braintrust/cli-darwin-arm64", + "darwin-x64": "@braintrust/cli-darwin-x64", + "linux-arm64": "@braintrust/cli-linux-arm64", + "linux-x64-glibc": "@braintrust/cli-linux-x64", + "linux-x64-musl": "@braintrust/cli-linux-x64-musl", + "win32-arm64": "@braintrust/cli-win32-arm64", + "win32-x64": "@braintrust/cli-win32-x64", }; function detectLibc() { diff --git a/npm/bt/package.json b/npm/bt/package.json index 6a8ae54..f6e6fe9 100644 --- a/npm/bt/package.json +++ b/npm/bt/package.json @@ -1,5 +1,5 @@ { - "name": "@braintrust/bt", + "name": "@braintrust/cli", "version": "0.0.0", "description": "The Braintrust command line interface", "homepage": "https://github.com/braintrustdata/bt", @@ -24,12 +24,12 @@ "node": ">=18" }, "optionalDependencies": { - "@braintrust/bt-darwin-arm64": "0.0.0", - "@braintrust/bt-darwin-x64": "0.0.0", - "@braintrust/bt-linux-arm64": "0.0.0", - "@braintrust/bt-linux-x64": "0.0.0", - "@braintrust/bt-linux-x64-musl": "0.0.0", - "@braintrust/bt-win32-arm64": "0.0.0", - "@braintrust/bt-win32-x64": "0.0.0" + "@braintrust/cli-darwin-arm64": "0.0.0", + "@braintrust/cli-darwin-x64": "0.0.0", + "@braintrust/cli-linux-arm64": "0.0.0", + "@braintrust/cli-linux-x64": "0.0.0", + "@braintrust/cli-linux-x64-musl": "0.0.0", + "@braintrust/cli-win32-arm64": "0.0.0", + "@braintrust/cli-win32-x64": "0.0.0" } } diff --git a/npm/scripts/build-platform-packages.mjs b/npm/scripts/build-platform-packages.mjs index cdc34f5..2f7bfd8 100644 --- a/npm/scripts/build-platform-packages.mjs +++ b/npm/scripts/build-platform-packages.mjs @@ -104,7 +104,7 @@ for (const [target, spec] of Object.entries(targets)) { extract(archive, stagingDir); const binPath = findBinary(stagingDir, spec.bin); - const pkgName = `@braintrust/bt-${spec.pkg}`; + const pkgName = `@braintrust/cli-${spec.pkg}`; const pkgOut = join(outDir, `bt-${spec.pkg}`); const pkgBin = join(pkgOut, "bin"); mkdirSync(pkgBin, { recursive: true }); @@ -139,7 +139,7 @@ for (const [target, spec] of Object.entries(targets)) { ); writeFileSync( join(pkgOut, "README.md"), - `# ${pkgName}\n\nPrebuilt \`bt\` binary for ${spec.os}-${spec.cpu}${spec.libc ? ` (${spec.libc})` : ""}.\n\nInstalled automatically as an optional dependency of [\`@braintrust/bt\`](https://www.npmjs.com/package/@braintrust/bt). Install that package instead.\n`, + `# ${pkgName}\n\nPrebuilt \`bt\` binary for ${spec.os}-${spec.cpu}${spec.libc ? ` (${spec.libc})` : ""}.\n\nInstalled automatically as an optional dependency of [\`@braintrust/cli\`](https://www.npmjs.com/package/@braintrust/cli). Install that package instead.\n`, ); console.log(`Built ${pkgName} -> ${pkgOut}`); From 2c685797d4478bee81786a9bee5048c6ee66ae1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Halber?= Date: Thu, 28 May 2026 15:15:23 -0700 Subject: [PATCH 3/4] feat: instead of publishing a package, publish platform specific binaries on npm --- .github/workflows/release.yml | 9 +---- npm/bt/README.md | 41 ---------------------- npm/bt/bin/bt.js | 43 ----------------------- npm/bt/lib/platform.js | 46 ------------------------- npm/bt/package.json | 35 ------------------- npm/scripts/build-platform-packages.mjs | 23 ++++--------- 6 files changed, 7 insertions(+), 190 deletions(-) delete mode 100644 npm/bt/README.md delete mode 100644 npm/bt/bin/bt.js delete mode 100644 npm/bt/lib/platform.js delete mode 100644 npm/bt/package.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b7c7bc9..d3cda4f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -535,15 +535,8 @@ jobs: shell: bash run: | set -euo pipefail + shopt -s nullglob for dir in npm/dist/bt-*; do echo "Publishing $dir" (cd "$dir" && npm publish --access public --provenance) done - - - name: Publish wrapper - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - shell: bash - run: | - cd npm/dist/bt - npm publish --access public --provenance diff --git a/npm/bt/README.md b/npm/bt/README.md deleted file mode 100644 index c25987e..0000000 --- a/npm/bt/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# @braintrust/cli - -The [Braintrust](https://www.braintrust.dev) command-line interface (`bt`), -distributed as an npm package. - -## Install - -```bash -npm install -g @braintrust/cli -# or -pnpm add -g @braintrust/cli -``` - -Then: - -```bash -bt --help -``` - -## How it works - -This package is a thin Node.js launcher. The actual `bt` binary is shipped via -platform-specific packages declared as `optionalDependencies`: - -- `@braintrust/cli-darwin-arm64` -- `@braintrust/cli-darwin-x64` -- `@braintrust/cli-linux-arm64` -- `@braintrust/cli-linux-x64` -- `@braintrust/cli-linux-x64-musl` -- `@braintrust/cli-win32-arm64` -- `@braintrust/cli-win32-x64` - -`npm/pnpm/yarn` install only the one matching your OS + CPU (+ libc on Linux). - -If your platform isn't supported, see - for alternative installation methods -(shell installer, building with `cargo`). - -## License - -Apache-2.0 diff --git a/npm/bt/bin/bt.js b/npm/bt/bin/bt.js deleted file mode 100644 index d655b65..0000000 --- a/npm/bt/bin/bt.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -"use strict"; - -const { spawnSync } = require("node:child_process"); -const { - PLATFORM_PACKAGES, - currentPlatformPackage, - binaryName, -} = require("../lib/platform"); - -function resolveBinary() { - const pkg = currentPlatformPackage(); - if (!pkg) { - throw new Error( - `No prebuilt bt binary for ${process.platform}-${process.arch}. ` + - `Supported: ${Object.keys(PLATFORM_PACKAGES).join(", ")}. ` + - `See https://github.com/braintrustdata/bt for other install methods.`, - ); - } - try { - return require.resolve(`${pkg}/bin/${binaryName()}`); - } catch (err) { - throw new Error( - `Failed to locate ${pkg}. This package is shipped via optionalDependencies; ` + - `if you installed with --ignore-optional / --no-optional, reinstall without ` + - `that flag, or use the shell installer at https://github.com/braintrustdata/bt. ` + - `(${err.message})`, - ); - } -} - -try { - const binary = resolveBinary(); - const result = spawnSync(binary, process.argv.slice(2), { - stdio: "inherit", - windowsHide: true, - }); - if (result.error) throw result.error; - process.exit(result.status ?? 1); -} catch (err) { - console.error(`bt: ${err.message}`); - process.exit(1); -} diff --git a/npm/bt/lib/platform.js b/npm/bt/lib/platform.js deleted file mode 100644 index 6ba0f94..0000000 --- a/npm/bt/lib/platform.js +++ /dev/null @@ -1,46 +0,0 @@ -"use strict"; - -const PLATFORM_PACKAGES = { - "darwin-arm64": "@braintrust/cli-darwin-arm64", - "darwin-x64": "@braintrust/cli-darwin-x64", - "linux-arm64": "@braintrust/cli-linux-arm64", - "linux-x64-glibc": "@braintrust/cli-linux-x64", - "linux-x64-musl": "@braintrust/cli-linux-x64-musl", - "win32-arm64": "@braintrust/cli-win32-arm64", - "win32-x64": "@braintrust/cli-win32-x64", -}; - -function detectLibc() { - if (process.platform !== "linux") return null; - try { - const report = process.report && process.report.getReport(); - if (report && report.header && report.header.glibcVersionRuntime) { - return "glibc"; - } - return "musl"; - } catch { - return "glibc"; - } -} - -function platformKey() { - const { platform, arch } = process; - if (platform === "linux" && arch === "x64") { - return `linux-x64-${detectLibc()}`; - } - return `${platform}-${arch}`; -} - -function currentPlatformPackage() { - return PLATFORM_PACKAGES[platformKey()] || null; -} - -function binaryName() { - return process.platform === "win32" ? "bt.exe" : "bt"; -} - -module.exports = { - PLATFORM_PACKAGES, - currentPlatformPackage, - binaryName, -}; diff --git a/npm/bt/package.json b/npm/bt/package.json deleted file mode 100644 index f6e6fe9..0000000 --- a/npm/bt/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "@braintrust/cli", - "version": "0.0.0", - "description": "The Braintrust command line interface", - "homepage": "https://github.com/braintrustdata/bt", - "repository": { - "type": "git", - "url": "git+https://github.com/braintrustdata/bt.git" - }, - "license": "Apache-2.0", - "author": "Braintrust engineering ", - "bin": { - "bt": "bin/bt.js" - }, - "files": [ - "bin/", - "lib/" - ], - "publishConfig": { - "access": "public", - "provenance": true - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@braintrust/cli-darwin-arm64": "0.0.0", - "@braintrust/cli-darwin-x64": "0.0.0", - "@braintrust/cli-linux-arm64": "0.0.0", - "@braintrust/cli-linux-x64": "0.0.0", - "@braintrust/cli-linux-x64-musl": "0.0.0", - "@braintrust/cli-win32-arm64": "0.0.0", - "@braintrust/cli-win32-x64": "0.0.0" - } -} diff --git a/npm/scripts/build-platform-packages.mjs b/npm/scripts/build-platform-packages.mjs index 2f7bfd8..8b28039 100644 --- a/npm/scripts/build-platform-packages.mjs +++ b/npm/scripts/build-platform-packages.mjs @@ -6,8 +6,10 @@ // (bt-.tar.gz / bt-.zip), required // --out-dir directory to write packages into (default: npm/dist) // -// Emits /bt/ (wrapper) and /bt-/ (one per target), -// each ready to `npm publish`. +// Emits /bt-/ (one per target), each ready to `npm publish`. +// The `bt` command is exposed via the `braintrust` SDK, which lists these +// packages as optionalDependencies and ships a launcher that resolves the +// matching binary. import { execFileSync } from "node:child_process"; import { @@ -78,19 +80,6 @@ function findBinary(rootDir, binName) { throw new Error(`Binary ${binName} not found under ${rootDir}`); } -// --- Wrapper package --- -const wrapperSrc = join(NPM_DIR, "bt"); -const wrapperOut = join(outDir, "bt"); -cpSync(wrapperSrc, wrapperOut, { recursive: true }); -const wrapperPkgPath = join(wrapperOut, "package.json"); -const wrapperPkg = JSON.parse(readFileSync(wrapperPkgPath, "utf8")); -wrapperPkg.version = version; -for (const key of Object.keys(wrapperPkg.optionalDependencies ?? {})) { - wrapperPkg.optionalDependencies[key] = version; -} -writeFileSync(wrapperPkgPath, JSON.stringify(wrapperPkg, null, 2) + "\n"); -console.log(`Built wrapper -> ${wrapperOut}`); - // --- Per-platform packages --- for (const [target, spec] of Object.entries(targets)) { const archiveName = `bt-${target}.${spec.archiveExt}`; @@ -104,7 +93,7 @@ for (const [target, spec] of Object.entries(targets)) { extract(archive, stagingDir); const binPath = findBinary(stagingDir, spec.bin); - const pkgName = `@braintrust/cli-${spec.pkg}`; + const pkgName = `@braintrust/bt-${spec.pkg}`; const pkgOut = join(outDir, `bt-${spec.pkg}`); const pkgBin = join(pkgOut, "bin"); mkdirSync(pkgBin, { recursive: true }); @@ -139,7 +128,7 @@ for (const [target, spec] of Object.entries(targets)) { ); writeFileSync( join(pkgOut, "README.md"), - `# ${pkgName}\n\nPrebuilt \`bt\` binary for ${spec.os}-${spec.cpu}${spec.libc ? ` (${spec.libc})` : ""}.\n\nInstalled automatically as an optional dependency of [\`@braintrust/cli\`](https://www.npmjs.com/package/@braintrust/cli). Install that package instead.\n`, + `# ${pkgName}\n\nPrebuilt \`bt\` binary for ${spec.os}-${spec.cpu}${spec.libc ? ` (${spec.libc})` : ""}.\n\nInstalled automatically as an optional dependency of [\`braintrust\`](https://www.npmjs.com/package/braintrust), which exposes the \`bt\` command. Install that package instead.\n`, ); console.log(`Built ${pkgName} -> ${pkgOut}`); From 872f62445c66476693fd5ab341c3f859614b9e47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Halber?= Date: Fri, 29 May 2026 11:58:53 -0700 Subject: [PATCH 4/4] fix: failed npm release on one arch blocking other arch --- .github/workflows/release.yml | 11 ++++++++++- npm/scripts/build-platform-packages.mjs | 14 +++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d3cda4f..0e3c5e9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -537,6 +537,15 @@ jobs: set -euo pipefail shopt -s nullglob for dir in npm/dist/bt-*; do - echo "Publishing $dir" + name="$(node -p "require('./$dir/package.json').name")" + version="$(node -p "require('./$dir/package.json').version")" + # Skip already-published versions so a partial publish can be re-run. + # Test output, not exit code: npm view is empty for a missing version. + published="$(npm view "$name@$version" version 2>/dev/null || true)" + if [ -n "$published" ]; then + echo "Skipping $name@$version (already published)" + continue + fi + echo "Publishing $name@$version" (cd "$dir" && npm publish --access public --provenance) done diff --git a/npm/scripts/build-platform-packages.mjs b/npm/scripts/build-platform-packages.mjs index 8b28039..0dd508c 100644 --- a/npm/scripts/build-platform-packages.mjs +++ b/npm/scripts/build-platform-packages.mjs @@ -81,12 +81,14 @@ function findBinary(rootDir, binName) { } // --- Per-platform packages --- +let built = 0; for (const [target, spec] of Object.entries(targets)) { const archiveName = `bt-${target}.${spec.archiveExt}`; const archive = join(archivesDir, archiveName); if (!existsSync(archive)) { - console.warn(`SKIP ${target}: archive not found (${archive})`); - continue; + // Fail hard, don't skip: the SDK pins each package at an exact version, so a + // missing platform would break installs for that platform at runtime. + throw new Error(`Archive not found for ${target}: ${archive}`); } const stagingDir = join(outDir, ".staging", target); @@ -132,7 +134,13 @@ for (const [target, spec] of Object.entries(targets)) { ); console.log(`Built ${pkgName} -> ${pkgOut}`); + built++; } rmSync(join(outDir, ".staging"), { recursive: true, force: true }); -console.log(`\nAll packages written to ${outDir}`); + +const expected = Object.keys(targets).length; +if (built !== expected) { + throw new Error(`Built ${built} packages but expected ${expected}`); +} +console.log(`\nAll ${built} packages written to ${outDir}`);