Crossdeck Docs
Dashboard

@cross-deck/cli — CLI reference

Reference Current version: 1.1.1 · ~9 min read · Updated May 18, 2026

@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

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

The npm scope is @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.

SourceResolution orderNotes
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:

PrefixEnvironment
cd_sk_test_…sandbox
cd_sk_live_…production
Publishable keys can't upload.

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

ArgumentDescription
<dist-dir> Path to your build's output directory (e.g. ./dist, ./build, ./.next/static). Required.

Flags

FlagRequiredDescription
-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.

BehaviourWhy
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/originhttps://app.example.com/static/js/
Server-side Node, Lambda, Cloud Functionsapp:///
Webpack frames inside Web Workerswebpack://
Capacitor / Ionic nativecapacitor://localhost/
React Nativereact-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

CodeMeaning
0All 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).
1Upload error or partial failure (one or more files failed). Auth resolution failure also exits 1.
2Argument 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

FlagDescription
-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

  1. Auth token resolves. Reads --auth-token, $CROSSDECK_SECRET_KEY, or $CROSSDECK_AUTH_TOKEN in that order.
  2. Token shape. Must match cd_sk_test_… or cd_sk_live_…. Publishable keys (cd_pub_*) are rejected with a specific error.
  3. Environment derivation. Reports the inferred environment from the token prefix (test → sandbox, live → production).
  4. API reachability. HEAD request 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

FailureLikely 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 reachableAbortError The 5-second timeout fired. Check outbound network access from the runner; corporate proxies often block CI runners from arbitrary HTTPS hosts.
✗ API reachableFetchError: 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.

SettingFlagEnv varDefault
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
Match --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 deploypredeploy 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.

Sentry deletes source maps after it uploads them.

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:

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:

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.

VersionHighlights
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.


Last updated when @cross-deck/[email protected] shipped (May 15, 2026). Future versions are documented in the table above as they publish to npm.