@carverauto/serviceradar-cli
v0.1.4
Published
ServiceRadar developer CLI: dashboard authoring loop, auth, and tooling. Companion to @carverauto/serviceradar-dashboard-sdk.
Readme
@carverauto/serviceradar-cli
The ServiceRadar developer CLI. Lives inside the ServiceRadar monorepo at
~/src/serviceradar/js/cli/ and ships independently to npm as
@carverauto/serviceradar-cli. Companion to @carverauto/serviceradar-dashboard-sdk (the runtime
React/JS surface customer dashboards depend on).
Subcommand groups
serviceradar-cli auth <login|status|logout>
serviceradar-cli dashboard <init|build|dev|validate|manifest|publish|import>Help for either group:
serviceradar-cli help
serviceradar-cli auth help
serviceradar-cli dashboard --help # delegates through to the dashboard subgroupSingle install for developers
@carverauto/serviceradar-dashboard-sdk declares @carverauto/serviceradar-cli in its
dependencies, so a customer building a dashboard runs only:
npm install @carverauto/serviceradar-dashboard-sdk…and the CLI bin lands in ./node_modules/.bin/serviceradar-cli. Project npm
scripts ("dev": "serviceradar-cli dashboard dev") resolve it from the local
.bin/. For ad-hoc invocation: npx serviceradar-cli ....
The legacy serviceradar-dashboard bin name is preserved as a transitional
alias that prints a deprecation notice and delegates to
serviceradar-cli dashboard *. Removal scheduled for the release after.
Authoring loop
npm create @carverauto/dashboard my-dashboard
cd my-dashboard
npm run dev # SDK harness with HMR
npm run validate # static check
npm run build # write dist/ for publish
serviceradar-cli auth login --instance https://serviceradar.example.com
serviceradar-cli dashboard publish --instance https://serviceradar.example.com --route my-dashboardAuth
serviceradar-cli auth login --instance <url> runs the OAuth 2.0 Device
Authorization Grant flow (RFC 8628) against /api/v1/cli/auth/device and
/api/v1/cli/auth/token. Until those endpoints land on the ServiceRadar
side, the CLI falls back to manual token paste. Tokens persist to
~/.config/serviceradar/credentials.json (mode 0600), keyed by instance URL.
auth status prints the resolved identity without leaking the token.
auth logout removes a credential entry.
Publish
serviceradar-cli dashboard publish --instance <url> [--route <slug>] [--enable] [--yes]
posts the built manifest + renderer to /api/v1/dashboard-packages as a
multipart upload. The bearer JWT must carry the dashboard.publish scope
(minted by auth login) and the user must hold the cli.dashboard.publish
RBAC permission.
Behavior worth knowing about:
- Idempotent re-publish. Pushing the same
manifest.id@versionwhose renderer SHA256 matches the persistedcontent_hashis a no-op: the CLI prints✓ Re-published … (already at this content_hash; nothing changed)and the server returnsresult: "idempotent_noop". Safe to retry. - Version overwrite is rejected. Pushing the same
id@versionwith different bytes against an enabled or verified package returns 409version_already_published. Bumpmanifest.version, or rundashboard disablefirst. - Slug ownership. A
--route <slug>belongs to onedashboard_idwhile enabled. Trying to bind a slug that's already enabled for a different dashboard returns 409slug_in_usewith the conflictingowner_dashboard_id. Pick a different--routeor have an admin disable the existing dashboard first. - Slug regex. Slugs must match
^[a-z0-9][a-z0-9-]{1,62}$; anything else returns 400invalid_route. - Per-token rate limit. 10 publishes/minute/JWT (429 +
Retry-After), 30/min for enable/disable. - Disable is symmetric.
serviceradar-clidoes not shipdashboard disabledirectly today; use the API atPOST /api/v1/dashboard-packages/:id/disableor the Settings UI.
The CLI surfaces structured server errors with actionable hints for each
of these cases — the raw HTTP status and error code are also included so
they can be parsed by automation.
Repository structure
js/cli/
├── bin/
│ ├── serviceradar-cli.js # 5-line shim → ../dist/cli.js
│ └── serviceradar-dashboard.js # transitional alias → serviceradar-cli
├── src/ # CLI implementation (TypeScript, 18 modules)
│ ├── cli.ts, args.ts, config.ts, manifest.ts, validation.ts,
│ │ doctor.ts, paths.ts, utils.ts
│ ├── auth/ # auth/{credentials,login,status,logout,index}.ts
│ └── dashboard/ # dashboard/{init,build,manifest,validate,
│ # dev,publish,import,index}.ts
├── dist/ # generated by `npm run build` (compiled JS + sourcemaps)
├── harness/ # browser-side dev harness
│ ├── index.html # legacy form-field harness (preserved at /?advanced)
│ ├── harness.js
│ ├── dev.js # HMR runtime
│ └── dev.css
├── schemas/
│ └── dashboard-config.schema.json # ajv-validated dashboard config schema
├── templates/ # scaffolder templates
│ ├── react-map/
│ ├── react-table/
│ └── react-blank/
├── tests/
├── tsconfig.json # tsc --noEmit (`npm run typecheck`)
├── tsconfig.build.json # tsc emit to dist/ (`npm run build`)
└── package.jsonBuild flow
The CLI source ships in src/ as TypeScript; the published tarball
ships compiled JS + sourcemaps in dist/. The bins under bin/ are
5-line shims that import from dist/. The split is deliberate:
- Source is the only thing kept in git, so diffs and reviews stay readable.
- The published tarball is what consumers actually run; shipping
compiled JS + sourcemaps from
dist/keeps the install path toolchain-free. prepublishOnlyrunsnpm run build, sonpm publishalways emits freshdist/from the currentsrc/— no chance of drift between the two.
npm run typecheck runs tsc --noEmit against src/**/*.ts.
npm run build compiles src/ to dist/ via tsconfig.build.json
(outDir: dist, sourceMap: true). npm test rebuilds first so
tests always exercise the shipped code path. npm run ci runs
typecheck → build → test → pack dry-run, mirroring CI.
Development
The CLI is a leaf package — no @carverauto/* runtime deps. To work on
it, cd js/cli && npm install then npm run ci (typecheck → build →
test → pack dry-run). The bazel target //js/cli:ci runs the same
pipeline opt-in.
Documentation
The canonical Dashboard SDK + CLI reference lives on the developer portal:
developer.serviceradar.cloud/docs/v2/dashboard-sdk.
