@browsonic/cli
v1.2.4
Published
CLI for the Browsonic SDK — sourcemap upload, release management. Apache-2.0.
Downloads
266
Maintainers
Readme
@browsonic/cli
Command-line companion for the Browsonic SDK. One working command: upload-sourcemaps — walks a dist tree and POSTs every .map file to the Browsonic service. help and version are the only other subcommands. No further commands exist or are planned; the package.json description still advertises "release management", which has no implementation and no design behind it anywhere in the repos.
Status (2026-07-28)
- npm has
1.2.4, and it matchesmain. Published 2026-07-27; it carries the--versionfix and the part-name fix below. (1.2.2 was version-stamped but never published as a release of its own.)- The upload part-name mismatch is fixed in every supported version. Up to and including
1.2.1the CLI sent the map as a multipart part namedsourcemap, whilePOST /v1/sourcemapsinbrowsonic-servicereadsfile— so a real upload was rejected with400before any of the CLI's logic mattered. Confirmed 2026-07-27 by capturing the CLI's actual multipart body and readingSourceMapController.uploadat service HEAD. From1.2.3the CLI postsfile; upgrade if you are on<= 1.2.1. The service also acceptssourcemapas a deprecated alias, so an already-installed<= 1.2.1works against a service carrying the alias.Everything below describes what the code on
maindoes today, which is what1.2.3shipped.
Pure TypeScript, zero npm runtime dependencies — Node 20+'s built-in fetch / FormData / Blob plus node:util.parseArgs. The pipeline design lives in docs/design/SOURCEMAP_PIPELINE.md; it now matches the shipped service on the multipart contract and the size cap, but one detail remains stale — the retention policy (see What this package does NOT do).
Install
# Per-project (recommended for CI)
npm install --save-dev @browsonic/cli
# Or run ad-hoc with npx
npx @browsonic/cli upload-sourcemaps --dist-path ./dist --release v1.2.3 --dry-runRequires Node ≥ 20. The bin is named browsonic, so npx browsonic … works once the package is a project dependency (npx resolves node_modules/.bin first); without a local install, use npx @browsonic/cli … so npx does not go looking for an unrelated browsonic package on the registry.
Commands
| Command | What it does |
| ----------------------------- | ---------------------------------------------------------------------- |
| browsonic upload-sourcemaps | Walk a dist tree and upload every *.map. Flags below. |
| browsonic help [<topic>] | Print usage. help upload-sourcemaps prints that command's flags. |
| browsonic version | Print the CLI version. Also --version / -v, or no args for help. |
--version / -v are recognised only in the leading position (browsonic -v). Passed after a subcommand they are unknown options — see Exit codes.
Quickstart — upload-sourcemaps
Wire it into your CI's post-build step (or your bundler plugin):
BROWSONIC_API_ENDPOINT=https://api.browsonic.com \
BROWSONIC_APP_KEY=app_xyz \
BROWSONIC_SOURCEMAP_TOKEN=sm_upload_abc123 \
npx browsonic upload-sourcemaps --dist-path ./dist --release "$GITHUB_SHA"The CLI walks ./dist recursively (skipping node_modules and .git, max depth 12), finds every *.map file, and POSTs each one to <BROWSONIC_API_ENDPOINT>/v1/sourcemaps as multipart/form-data with an Authorization: Bearer <BROWSONIC_SOURCEMAP_TOKEN> header. Parts sent: release, filename (path relative to --dist-path, forward-slashed), appKey, file (the map bytes), plus dist when --dist is passed. Uploads are sequential, not parallel.
Server-side constraints worth knowing before you wire this up:
releaseand--app-keymust match[A-Za-z0-9._-]{1,128};filenamemust match[A-Za-z0-9._/-]{1,512}and contain no... A commit SHA is a fine release tag.- A single map above 10 MiB (
sentinel.storage.sourcemap.max-upload-bytes, default10485760) is rejected with413. /v1/sourcemapsis plan-gated behind thesource_mapsfeature. A tenant without it gets402, which the CLI reports as an ordinary per-file failure.- The service ignores
dist—POST /v1/sourcemapshas no such parameter, and the stored row is keyed on(tenantId, appKey, release, filename). Re-uploading the same coordinate overwrites the object and the row and returns200with the same id; it is not a409.
The --release tag has to line up with what the SDK reports, or the dashboard cannot pair frames to maps. The dashboard symbolicates with event.release ?? event.clientVersion, and the SDK emits no release field through 3.21.0 (npm latest, checked 2026-07-29), so in practice the tag must equal your BrowsonicConfig.clientVersion.
Per-file progress lines stream as the upload runs (→ is the service-issued sourcemap id, a UUID):
[browsonic] upload-sourcemaps: 12 file(s) found under ./dist
✓ chunks/main.abc123.js.map (482103 bytes) → e2f1c3d4-5a6b-7c8d-9e0f-112233445566
✓ chunks/about.def456.js.map (102301 bytes) → 8b7a6959-4c3d-2e1f-0a9b-887766554433
...
[browsonic] upload-sourcemaps: 12/12 succeededIf the walk finds no .map files the CLI prints no *.map files found under <path> — nothing to upload. and exits 0. A build that silently stopped emitting sourcemaps will not fail your pipeline.
Dry-run mode
--dry-run reads each discovered file (so the byte counts are real) but makes no HTTP call, and exits 0:
[browsonic] upload-sourcemaps: 2 file(s) found under ./dist (dry-run)
✓ about.def456.js.map (61 bytes) → dry-run:v1.2.3:about.def456.js.map
✓ chunks/main.abc123.js.map (61 bytes) → dry-run:v1.2.3:chunks/main.abc123.js.map
[browsonic] upload-sourcemaps: 2/2 succeeded (dry-run — no HTTP requests made)What it does prove: your build emits .map files where you think it does, their relative filenames are what you expect, their sizes are sane, and every required flag / env var is present.
What it does not prove: that the token, app key or endpoint are valid, or that the request the CLI would build is one the service accepts — dry-run never constructs the multipart body. That gap is exactly how the part-name mismatch in the status note above went unnoticed; the multipart part names are now pinned by unit tests on both sides instead (packages/cli/src/upload.test.ts, SourceMapUploadWireContractTest in browsonic-service).
npx browsonic upload-sourcemaps --dist-path ./dist --release v1.2.3 --dry-runThe command shape is identical with and without --dry-run — drop the flag for the real upload.
Flags (upload-sourcemaps)
| Flag | Required | Env | Description |
| -------------------- | -------- | --------------------------- | ---------------------------------------------------------------------- |
| --dist-path <path> | yes | — | Directory to walk for *.map files (e.g. ./dist). |
| --release <tag> | yes | — | Release tag matching the SDK's BrowsonicConfig.clientVersion. |
| --app-key <key> | yes | BROWSONIC_APP_KEY | App key from the Browsonic dashboard. |
| --token <token> | yes | BROWSONIC_SOURCEMAP_TOKEN | Sourcemap-upload Bearer token (sm_upload_…). |
| --base-url <url> | yes | BROWSONIC_API_ENDPOINT | Ingest service base URL. |
| --dist <name> | no | — | Distribution discriminator. Sent, but the service ignores it. |
| --dry-run | no | — | Skip the HTTP call (see above). |
| --bail-on-error | no | — | Abort batch on first per-file failure (default: continue + aggregate). |
| -h, --help | no | — | Print this command's usage and exit 0. |
Tokens passed via env vars never leak onto CI shell history. Mix-and-match (env var for --token, flag for --release) works fine.
Exit codes
| Code | Meaning |
| ---- | ------------------------------------------------------------------------------------------------------ |
| 0 | Every file uploaded, dry-run, no .map files found, or help / version |
| 1 | At least one file failed; --dist-path does not exist or is not a directory; an unrecognised flag |
| 2 | A required flag (or its env fallback) is missing, or the command is unknown |
| 3 | HTTP auth failure (401 / 403) — token doesn't have sourcemap-upload scope |
| 4 | HTTP payload-too-large (413) — sourcemap exceeded the ingest size limit |
| 5 | HTTP transient failure (5xx) — caller should retry |
Two things CI scripts get wrong here:
3/4/5are only reachable with--bail-on-error. By default per-file HTTP failures are caught inside the run loop and aggregated, so the process exits1no matter what the server said. Verified by running the CLI against a local 401: exit1without the flag, exit3with it.- An unrecognised flag exits
1, not2.node:util.parseArgsthrows aTypeErrorthat is not the CLI's ownArgsError, so the bin entry prints[browsonic] fatal: TypeError [ERR_PARSE_ARGS_UNKNOWN_OPTION] …with a stack trace and exits1. Missing required flags and unknown commands do exit2.
A 402 from the plan gate is not mapped and surfaces as 1.
Programmatic API
The command is exposed as a library function for consumers building bundler plugins (@browsonic/build-tools wraps exactly this):
import { runUploadSourcemaps } from "@browsonic/cli";
await runUploadSourcemaps(
{
command: "upload-sourcemaps",
distPath: "./dist",
release: "v1.2.3",
appKey: process.env.BROWSONIC_APP_KEY!,
token: process.env.BROWSONIC_SOURCEMAP_TOKEN!,
baseUrl: process.env.BROWSONIC_API_ENDPOINT!,
dryRun: false,
bailOnError: false,
},
{
log: (m) => console.log(m),
error: (m) => console.error(m),
},
);It resolves to { discovered, uploaded, failed, results }. Lower-level building blocks (uploadOne, uploadOneDryRun, discoverSourceMaps, relativeFilenameForUpload, UploadError, parseCliArgs, CLI_VERSION) are also exported — see src/index.ts.
Defensive contract
- Per-file failures are aggregated by default — a single bad file doesn't abort the whole batch (
--bail-on-erroroverrides). See the exit-code caveat above for what that costs you. - An HTTP failure raises an
UploadErrorcarrying the status, the filename and the response body; the run loop prints✗ <filename>: <message>, so CI logs name the file. Transport-level failures (DNS, TLS, connection refused) are plainfetcherrors with no status — they still get the✗ <filename>:prefix, but nothing maps them to exit codes 3/4/5. uploadOneaccepts afetchoverride for tests.runUploadSourcemapsdoes not expose one —UploadSourcemapsArgshas nofetchfield — so anything testing the command end-to-end has to stub the global.
What this package does NOT do
- Bundler plugins. Use the companion
@browsonic/build-toolspackage for its webpack / vite / rollup / esbuild plugins. They callrunUploadSourcemapsfrom this package directly, so they inherit this package's wire contract — including the part-name fix described at the top, and the same caveat about published versions. - Inline-sourcemap extraction. Modern bundlers default to external
.mapfiles. If your config emits inline sourcemaps (//# sourceMappingURL=data:…at the end of the bundle), extract them to.mapsiblings before invoking the CLI. Reviewed 2026-07-27 and left closed — not deferred: every bundler this pipeline targets emits external maps by default, and no consumer has asked. Open an issue if your config genuinely cannot. - Symbolication. That happens server-side at dashboard read time via
POST /v1/symbolicate; this CLI only uploads. - Sourcemap retention or pruning — and neither does the service. The design doc describes an N-releases-per-app policy (default 50) with an "archived" flag, but as of 2026-07-27 nothing implements it:
browsonic-servicehas no retention job, nomaxReleasessetting and no archive flag. Old uploads stay until someone deletes them explicitly. - Token CRUD / sourcemap list and delete. Those shipped server-side — token CRUD (
GET/POST/DELETE /v1/sourcemap-tokens, OWNER-gated,SOURCEMAP_UPLOAD-type keys added by Flyway V40) plusDELETE /v1/sourcemaps/{id}and a filtered, paginatedGET /v1/sourcemaps— but this CLI stays upload-only. Mint, revoke and prune from the dashboard or those endpoints directly.
