@cross-deck/cli — CLI reference
@cross-deck/cli is the command-line tool that uploads source maps to Crossdeck so production stack traces resolve back to original file:line:function. It ships exactly two commands today: upload-sourcemaps for the CI hot path, and doctor for one-shot install diagnostics. Same wire shape as the build-tool plugins, designed to be dropped into a GitHub Actions or GitLab CI job in under a minute.
TL;DR
- Two commands.
crossdeck upload-sourcemapswalks a build directory, pairs.jswith.map, and POSTs them in batches of ≤100 to/v1/releases/sourcemaps.crossdeck doctorvalidates auth and API reachability without uploading anything. - Auth is one env var. Set
CROSSDECK_SECRET_KEYto acd_sk_test_…orcd_sk_live_…key. No config file required, no rotation tokens. The environment (sandbox vs production) is inferred from the key prefix. - CI-friendly exit codes.
0on success or "no maps found,"1on upload / auth error,2on bad arguments. A failed upload never silently lets the pipeline advance. - Programmatic API too.
import { uploadSourcemaps, discoverSourcemaps } from "@cross-deck/cli"drives the same flow from a build-tool plugin or test fixture without spawning a subprocess. - Sentry-compatible URL prefixes. Browser bundles use
https://app.example.com/static/js/; server-side Node uses theapp:///sentinel; native runtimes usecapacitor://,react-native://, etc. Anyscheme://URL is accepted.
Install
The CLI is a normal npm package. Install it globally for one-off uploads or use npx directly from a CI step — there's no state to persist, so npx is the cleaner choice for build pipelines.
Global install
npm install -g @cross-deck/cli
Provides a crossdeck binary on your PATH. Verify with crossdeck --version (prints 1.1.1).
One-shot via npx
npx @cross-deck/cli upload-sourcemaps --release v1.2.3 --url-prefix https://app.example.com/static/js/ ./dist
No global install — pulls the latest 1.x on each invocation. Recommended for CI so the runner doesn't depend on a pre-installed binary.
Requirements
- Node.js ≥ 18. Uses the native
fetchimplementation; nonode-fetchdependency. - One npm dependency.
commanderfor argument parsing. That's it.
@cross-deck with a hyphen.
Not @crossdeck. AI assistants frequently guess wrong here — if npm install errors with "package not found," the scope is the cause.
Authentication
The CLI authenticates with a single secret key — the same cd_sk_* credential the server SDKs use. Issue one from the dashboard's API Keys page; pick a test key (cd_sk_test_…) for sandbox uploads and a live key (cd_sk_live_…) for production.
| Source | Resolution order | Notes |
|---|---|---|
| Flag | --auth-token <key> (alias -t) |
Highest priority. Useful for one-off invocations. |
| Env var (canonical) | CROSSDECK_SECRET_KEY |
Same env var name every Crossdeck SDK reads. Set this in CI secrets. |
| Env var (back-compat) | CROSSDECK_AUTH_TOKEN |
Honoured for users who set it during the 1.0.x window. Don't use for new setups. |
The token's prefix determines the environment automatically:
| Prefix | Environment |
|---|---|
cd_sk_test_… | sandbox |
cd_sk_live_… | production |
cd_pub_* keys are client-only and ship in browser bundles. Source-map upload is a privileged operation that requires a cd_sk_* secret key. The CLI rejects publishable keys at config-resolve time with a clear error.
Project ID (optional)
Multi-project workspaces can pass --project <id> (or set CROSSDECK_PROJECT_ID) as a sanity-check that the CLI is hitting the right tenant. The backend infers the project from the secret key automatically, so this flag is never required — it just fails fast if the declared project and the key's project disagree.
No config file
The CLI is stateless. There is no .crossdeckrc, no crossdeck.config.json, no ~/.config/crossdeck/. All configuration comes from flags or environment variables on each invocation. Secret keys are long-lived; rotate them via the dashboard's Rotate button on the API Keys page when you need to.
crossdeck upload-sourcemaps
The single-purpose upload command. Walks a dist directory, pairs every .js / .mjs / .cjs bundle with its referenced .map file, base64-encodes the map JSON, and POSTs batches of ≤100 files per request to {baseUrl}/v1/releases/sourcemaps. Reports per-file outcome at the end.
Synopsis
crossdeck upload-sourcemaps [options] <dist-dir>
Arguments
| Argument | Description |
|---|---|
<dist-dir> |
Path to your build's output directory (e.g. ./dist, ./build, ./.next/static). Required. |
Flags
| Flag | Required | Description |
|---|---|---|
-r, --release <version> |
Yes | Release version this build represents. 1–64 chars: letters, digits, dot, underscore, hyphen. Examples: v1.2.3, commit-abc1234, 2026-05-15-build.42. |
-u, --url-prefix <url> |
Yes | Where the bundles are served from. Must be a URL with an explicit scheme (any scheme:// is accepted — see below). |
-e, --environment <env> |
No | Target environment: production or sandbox. Default production. |
-t, --auth-token <token> |
No | Secret key. Defaults to $CROSSDECK_SECRET_KEY or $CROSSDECK_AUTH_TOKEN. |
-p, --project <id> |
No | Project ID for multi-project sanity-check. Defaults to $CROSSDECK_PROJECT_ID. |
--base-url <url> |
No | API base URL. Defaults to https://api.cross-deck.com. |
-v, --verbose |
No | Print every discovered file and every skipped file's reason. |
Discovery — what gets uploaded
The walker recurses into <dist-dir> looking for .js, .mjs, and .cjs files. For each one it reads the trailing 4 KB and matches the //# sourceMappingURL=… (or older //@ sourceMappingURL=…) comment. The referenced .map path is resolved relative to the bundle and verified to exist on disk.
| Behaviour | Why |
|---|---|
node_modules/ and .git/ are skipped |
Avoids re-discovering library bundles on every run. |
Bundles without a sourceMappingURL comment are skipped silently |
Usually vendor files that ship without source maps. Pass --verbose to see them. |
Bundles with an inline data: URI map are skipped |
Already embedded in the bundle — nothing to upload separately. Reconfigure your bundler to emit an external .map for production builds. |
Bundles referencing a .map that doesn't exist on disk are skipped (with diagnostic) |
Common after a partial build. --verbose prints the expected path. |
?v=… / #… on the map reference is stripped before disk lookup |
Webpack sometimes emits foo.map?v=123; the on-disk file is just foo.map. |
URL prefix — what to set
The URL prefix tells Crossdeck how the stack-trace frame's file URL maps to your discovered bundle. It must be a URL with an explicit scheme — anything matching [a-z][a-z0-9+.-]*:// is accepted, so Sentry-compatible sentinel schemes work too.
| Runtime | --url-prefix |
|---|---|
| Browser bundles served from a CDN/origin | https://app.example.com/static/js/ |
| Server-side Node, Lambda, Cloud Functions | app:/// |
| Webpack frames inside Web Workers | webpack:// |
| Capacitor / Ionic native | capacitor://localhost/ |
| React Native | react-native://0.0.0.0/ |
Trailing slashes are normalised — app:///, app:///nested/, and app:///nested///// all collapse to a stable form. The scheme //host shape is preserved (so app:/// is not corrupted into app:/).
Examples
Vite / Rollup (browser app)
crossdeck upload-sourcemaps \
--release v1.2.3 \
--url-prefix https://app.example.com/assets/ \
./dist
Vite emits external .map files when build.sourcemap: true is set. sourcesContent is inlined by default.
Next.js (App Router, client bundle)
crossdeck upload-sourcemaps \
--release "$VERCEL_GIT_COMMIT_SHA" \
--url-prefix https://app.example.com/_next/static/ \
./.next/static
Requires productionBrowserSourceMaps: true in next.config.js.
Webpack
crossdeck upload-sourcemaps \
--release v1.2.3 \
--url-prefix https://app.example.com/static/js/ \
./build
Set devtool: 'source-map' in webpack.config.js to emit external maps.
Server-side Node / Lambda
crossdeck upload-sourcemaps \
--release v1.2.3 \
--url-prefix app:/// \
./dist
Use the app:/// sentinel for Node runtimes — stack frames don't have an origin URL, so this matches the SDK's normalised frame shape.
TypeScript (tsc output)
crossdeck upload-sourcemaps \
--release v1.2.3 \
--url-prefix app:/// \
./lib
Requires "sourceMap": true and "inlineSources": true in tsconfig.json.
Output
The command prints discovery summary, per-batch progress, and a final tally. Example:
Found 42 sourcemaps under /home/runner/work/app/dist
assets/index-abc123.js → https://app.example.com/assets/index-abc123.js
assets/vendor-def456.js → https://app.example.com/assets/vendor-def456.js
…and 39 more.
Uploading to https://api.cross-deck.com as release v1.2.3 (production)…
Batch 1/1: 42 uploaded, 0 errors
✓ 42 sourcemaps uploaded in 3.4s
Production stack traces for release v1.2.3 will now decode to original source on the next view.
Dashboard → https://cross-deck.com/dashboard/errors/
Exit codes
| Code | Meaning |
|---|---|
0 | All discovered maps uploaded successfully — or no maps were found (treated as success so the CI step doesn't fail on a project that hasn't built yet). |
1 | Upload error or partial failure (one or more files failed). Auth resolution failure also exits 1. |
2 | Argument error (missing <dist-dir>, missing required flag, dist dir doesn't exist, invalid --release or --url-prefix shape). |
crossdeck doctor
One-command install diagnostic. Runs four checks in order and stops at the first failure. Doesn't upload anything — safe to run on a developer laptop without affecting production data.
Synopsis
crossdeck doctor [options]
Flags
| Flag | Description |
|---|---|
-t, --auth-token <token> | Secret key. Defaults to $CROSSDECK_SECRET_KEY or $CROSSDECK_AUTH_TOKEN. |
-p, --project <id> | Project ID. Defaults to $CROSSDECK_PROJECT_ID. |
--base-url <url> | API base URL. Defaults to https://api.cross-deck.com. |
Checks performed
- Auth token resolves. Reads
--auth-token,$CROSSDECK_SECRET_KEY, or$CROSSDECK_AUTH_TOKENin that order. - Token shape. Must match
cd_sk_test_…orcd_sk_live_…. Publishable keys (cd_pub_*) are rejected with a specific error. - Environment derivation. Reports the inferred environment from the token prefix (
test → sandbox,live → production). - API reachability.
HEADrequest to the base URL with a 5-second timeout. Any HTTP response (including 404 / 401) counts as reachable — only network-level errors fail the check. The doctor command does not validate the token against the server — that's the first real upload's job. This check proves connectivity, not authorisation.
Example output
Crossdeck CLI · install diagnostic
──────────────────────────────────
✓ Auth token
Resolved (cd_sk_live_a1…b9c2)
✓ Token shape
cd_sk_live_… → environment: production
✓ Project ID
Not set — backend will infer from the token.
✓ API reachable (https://api.cross-deck.com)
200 OK in 142ms
✓ All checks passed. You're ready to run `crossdeck upload-sourcemaps`.
Common failures
| Failure | Likely cause |
|---|---|
✗ Auth token — "No Crossdeck secret key found" |
Neither --auth-token nor CROSSDECK_SECRET_KEY / CROSSDECK_AUTH_TOKEN is set. |
✗ Token shape — "doesn't match cd_sk_test_… or cd_sk_live_…" |
You passed a publishable key (cd_pub_*) or a malformed string. Issue a secret key from the dashboard. |
✗ API reachable — AbortError |
The 5-second timeout fired. Check outbound network access from the runner; corporate proxies often block CI runners from arbitrary HTTPS hosts. |
✗ API reachable — FetchError: getaddrinfo ENOTFOUND |
DNS can't resolve the base URL. Verify --base-url spelling (or unset CROSSDECK_BASE_URL). |
Configuration reference
Every input to the CLI, in one table. Flags always win over environment variables, which always win over defaults.
| Setting | Flag | Env var | Default |
|---|---|---|---|
| Secret key | -t, --auth-token |
CROSSDECK_SECRET_KEY (canonical)CROSSDECK_AUTH_TOKEN (back-compat) |
— |
| Project ID | -p, --project |
CROSSDECK_PROJECT_ID |
Inferred from key |
| API base URL | --base-url |
CROSSDECK_BASE_URL |
https://api.cross-deck.com |
| Release version | -r, --release |
— | — |
| URL prefix | -u, --url-prefix |
— | — |
| Environment | -e, --environment |
— | production |
| Verbose output | -v, --verbose |
— | false |
Programmatic API
The CLI also exports its core functions for build-tool plugins, custom CI scripts, and tests that need to drive the upload flow without spawning a subprocess.
import {
uploadSourcemaps,
discoverSourcemaps,
resolveConfig,
ApiError,
} from "@cross-deck/cli";
const config = resolveConfig({ authToken: process.env.CROSSDECK_SECRET_KEY });
const { files } = discoverSourcemaps({
distDir: "./dist",
urlPrefix: "https://app.example.com/static/js/",
});
const summary = await uploadSourcemaps({
config,
release: "v1.2.3",
environment: "production",
files,
});
Same library, same wire shape — just no commander front end. See the source on GitHub for the full type definitions.
CI usage
The upload isn't really a build step — it needs only two things: the emitted .map files on disk and a secret key, so it runs wherever both are available. If you own the CI runner, slot it in right after your bundle step; three common runners follow. If a managed platform builds for you — Firebase and the like — see Managed build platforms below.
GitHub Actions
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npm ci
- run: npm run build
- name: Upload source maps to Crossdeck
env:
CROSSDECK_SECRET_KEY: ${{ secrets.CROSSDECK_SECRET_KEY }}
run: |
npx @cross-deck/cli upload-sourcemaps \
--release ${{ github.sha }} \
--url-prefix https://app.example.com/static/js/ \
./dist
GitLab CI
# .gitlab-ci.yml
upload_sourcemaps:
stage: deploy
image: node:20
script:
- npm ci
- npm run build
- npx @cross-deck/cli upload-sourcemaps
--release "$CI_COMMIT_SHORT_SHA"
--url-prefix https://app.example.com/static/js/
./dist
variables:
CROSSDECK_SECRET_KEY: "$CROSSDECK_SECRET_KEY"
Generic shell (CircleCI, Buildkite, Jenkins, etc.)
#!/usr/bin/env bash
set -euo pipefail
npm ci
npm run build
export CROSSDECK_SECRET_KEY="$CROSSDECK_SECRET_KEY" # injected by CI secrets
RELEASE="$(git rev-parse --short HEAD)"
npx @cross-deck/cli upload-sourcemaps \
--release "$RELEASE" \
--url-prefix https://app.example.com/static/js/ \
./dist
--release to the SDK's release option.
The dashboard joins maps to errors by the release string. If your web SDK initialises with release: process.env.NEXT_PUBLIC_RELEASE, pass the same value to --release at upload time. Commit SHAs work well because they're stable across SDK init and CI variables.
Managed build platforms (Firebase)
On a runner you configure — GitHub Actions, GitLab, a shell script — you add the upload as a pipeline step. Some hosts instead run the build for you, on infrastructure you don't configure; Firebase is the common case. There's no pipeline step to add, but the upload still has a clean home, because it was never a build step — it needs only the .map files and a key.
Firebase Hosting doesn't build on its end: firebase deploy ships whatever you built locally. Put the upload in the predeploy hook, which runs on the deploying machine right before the files go up.
// firebase.json
{
"hosting": {
"public": "dist",
"predeploy": [
"npm run build",
"npx @cross-deck/cli upload-sourcemaps --release $(git rev-parse --short HEAD) --url-prefix https://YOURAPP.web.app/ ./dist"
]
}
}
Export CROSSDECK_SECRET_KEY in the shell that runs firebase deploy — predeploy hooks inherit its environment.
Firebase App Hosting builds from your Git repo on Google Cloud Build. You can't add a step to Google's pipeline — but you don't need to, because Google runs your build script. Put the upload there, and expose the key to the build with an App Hosting secret.
// package.json — Google Cloud Build runs this script
"scripts": {
"build": "vite build && npx @cross-deck/cli upload-sourcemaps --release $CD_RELEASE --url-prefix https://YOURAPP.web.app/ ./dist"
}
# apphosting.yaml — make the key readable at build time
env:
- variable: CROSSDECK_SECRET_KEY
secret: crossdeck-secret-key
availability: [BUILD]
Store the secret value with firebase apphosting:secrets:set crossdeck-secret-key. This uploads maps from the exact bundle Google ships, so they always match the deployed code. Set CD_RELEASE to a value your SDK also reports as release — a commit SHA works well.
Other managed hosts — Netlify, Cloudflare Pages, Render — work the same way: add the upload to the build command they run for you, or run it from your own CI against a production build before you hand the code over.
Running alongside Sentry or another error tool
Crossdeck and Sentry both resolve stack traces from source maps, and running both is fine — they upload to separate services and never touch each other's data. The friction is the files: they compete for the same .map outputs, and Sentry's tooling removes them by default.
The Sentry bundler plugins and the sentry-cli wizard set sourcemaps.filesToDeleteAfterUpload so maps never ship to production. If Sentry's upload runs first, crossdeck upload-sourcemaps finds an empty build directory and exits with "No .js + .map pairs found."
Pick one fix:
- Upload to Crossdeck first. Order the steps so
crossdeck upload-sourcemapsruns before Sentry's upload. The maps are still on disk; both tools read them. - Stop Sentry deleting them. Drop
filesToDeleteAfterUploadfrom the Sentry plugin config, let both uploads run, then delete the maps yourself as the final step. Use this when ordering is fixed — e.g. Sentry runs inside the bundler.
The same caution applies to any tool that uploads and then strips source maps — Bugsnag, Rollbar, and Datadog among them. Crossdeck never deletes or rewrites build output; it only reads it, so it is always safe to run last — provided the maps survive that long.
Troubleshooting
"No Crossdeck secret key found"
The CLI couldn't resolve a token. Set CROSSDECK_SECRET_KEY in your environment, or pass --auth-token cd_sk_live_…. Verify with crossdeck doctor before re-running upload.
"Auth token doesn't look like a Crossdeck secret key"
You passed a publishable key (cd_pub_*) or a malformed string. Publishable keys are client-only and can't upload source maps. Issue a secret key from the API Keys page.
"No .js + .map pairs found under ./dist"
Four common causes:
- Wrong directory. Vite/Rollup emit to
./dist, Webpack to./build, Next.js to./.next/static/,tscto./lib(or youroutDir), ESBuild to wherever you wrote it. - Sourcemaps disabled in production. Vite needs
build.sourcemap: true; Webpack needsdevtool: 'source-map'; Next.js needsproductionBrowserSourceMaps: true;tscneeds"sourceMap": trueintsconfig.json. - Inline source maps. Some bundler defaults embed the map as a
data:URI. The CLI skips these (the map is already in the bundle). Configure the bundler to emit external.mapfiles for production. - Another tool deleted them. Sentry's bundler plugin and the
sentry-cliwizard remove.mapfiles after their own upload — see Running alongside Sentry or another error tool.
Pass --verbose to see exactly which files were inspected and why each was skipped.
"Upload failed: HTTP 401"
The secret key was rejected by the server. Three likely causes: the key was rotated and the CI variable is stale, the key belongs to a different project than --project claims, or the key is for sandbox but the dashboard project is in production (or vice versa). Re-issue the key from the dashboard and update the CI secret.
"Upload failed: HTTP 413" / payload too large
Single source-map files cap at the server's per-file limit (declared in the backend's v1-releases.ts). The CLI already batches in groups of ≤100 files per request — if one file alone exceeds the limit, it's almost always an unminified vendor map. Inspect the offending .map and consider regenerating with sourcesContent for the application code only.
Network errors / ENOTFOUND / ECONNREFUSED
Run crossdeck doctor from the same environment. CI runners behind corporate proxies sometimes block outbound HTTPS to non-allowlisted hosts; ask your platform team to allow api.cross-deck.com. Self-hosted runners with strict DNS may need CROSSDECK_BASE_URL set explicitly.
Errors decode for one release but not another
The release string at upload time has to match the release value the SDK initialised with at runtime. A mismatch (e.g. SDK sees "v1.2.3" but CLI uploaded under "1.2.3" without the leading v) means the dashboard can't join maps to frames. Normalise the release string in one place — commit SHAs work well.
Request ID for support
Every failed upload prints Request: req_… from the response's x-request-id header. Include this when emailing support so the backend logs are searchable.
Versioning
The CLI follows semantic versioning. The current published version is 1.1.1. The CLI's wire protocol matches the backend's /v1/releases/sourcemaps endpoint — major-version bumps signal a breaking change to the upload contract, not just a UX tweak.
| Version | Highlights |
|---|---|
1.1.1 |
Current. Two commands (upload-sourcemaps, doctor), CROSSDECK_SECRET_KEY as canonical env var with CROSSDECK_AUTH_TOKEN back-compat alias, Sentry-compatible URL prefix scheme matching, programmatic exports for build-tool plugins. |
Pin the version explicitly in CI (npx @cross-deck/[email protected]) once you've validated a setup, so a future minor release doesn't change behaviour mid-pipeline.
Related
- Web SDK reference — the SDK that captures the errors whose stack traces this CLI decodes.
- Source maps — concept doc explaining how Crossdeck stores and resolves maps server-side.
- API keys & authentication — publishable vs secret keys, rotation, and scope.
- Create a project — produces the project the secret key authenticates against.
- Source on GitHub — the CLI lives under
sdks/cli/.
Last updated when @cross-deck/[email protected] shipped (May 15, 2026). Future versions are documented in the table above as they publish to npm.