This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
pnpm dcd <args>— run the CLI from source viatsx.pnpm build— clean, compile TypeScript todist/, andchmod +x dist/index.jsso the published binary is directly executable.pnpm lint— ESLint oversrc/andtest/.pnpm typecheck—tsc --noEmitoversrc/andtest/(strict mode;pnpm buildonly compilessrc/).pnpm test— runsscripts/test-runner.mjs: builds the CLI, boots a mock API, then runs alltest/**/*.test.tsvia mocha + ts-node. The mock API lives in the siblingdcd/repo (../dcd/mock-api). Override its location withMOCK_API_DIR=/path/to/mock-api.- Run a single test:
pnpm mocha test/integration/cloud.integration.test.ts --timeout 60000(picks up.mocharc.jsonwhich wires tsx; requires the mock API already running on its expected port).
The CLI is citty-native: src/index.ts has a #!/usr/bin/env node shebang (preserved by tsc), and package.json points bin.dcd directly at dist/index.js. There is no bin/ wrapper directory — don't add one.
Top-level defineCommand in src/index.ts wires ten subcommands (cloud, upload, list, status, artifacts, live, plus the auth-related login, logout, whoami, switch-org). cloud is the primary command and replicates maestro cloud.
Flag composition. Flag definitions are split by domain in src/config/flags/*.flags.ts (api, binary, device, environment, execution, github, output) and re-exported as a single flags object from src/constants.ts. Commands that need the full cloud surface spread ...flags into their citty args; subset commands import individual flag groups.
Layered call stack.
src/commands/*— thin citty command definitions; orchestrate services, no I/O logic.src/services/*.service.ts— domain workflows (execution planning, device validation, test submission, results polling, report download, version check, metadata extraction). Services call gateways and each other.src/gateways/api-gateway.ts,src/gateways/supabase-gateway.ts— HTTP/Supabase boundaries.ApiGatewayalso owns network-error enhancement and standardized response handling.src/methods.ts— shared binary/archive helpers (zip verification, SHA-based upload dedup, JSON file writes).src/utils/*—cli(logger, CliError, version, enum/int parsing),styling(colors, section headers),progress(ux/spinners),compatibility(device/OS matrix),expo(Expo URL download + tarball extraction),connectivity.
Types. src/types/generated/schema.types.ts is auto-generated by openapi-typescript — do not hand-edit. Domain types live in src/types/domain/.
Cloud command workflow (the canonical shape other commands mirror): upload binary with SHA dedup → extract metadata / validate devices → build execution plan → submit tests → poll results every 10s → download artifacts (reports/videos/logs). RunFailedError from the polling service is the signal for a failed test run (distinct from infra errors).
Auth. Every command calls resolveAuth({ apiKeyFlag }) (src/utils/auth.ts) once and threads the returned AuthContext into gateways/services. ApiGateway and fetchCompatibilityData spread auth.headers into fetch headers — they no longer accept a raw api key. Precedence: --api-key flag > DEVICE_CLOUD_API_KEY env > stored session from dcd login. resolveAuth refreshes expiring Supabase sessions via CliAuthGateway.refresh and rewrites the config atomically.
Config store. dcd login writes $XDG_CONFIG_HOME/dcd/config.json (fallback ~/.dcd/config.json, 0600). Shape: { version, env, api_url, supabase_url, session: { access_token, refresh_token, expires_at, user_email, user_id }, current_org_id, current_org_name }. DCD_CONFIG_DIR overrides the directory (used by tests). The login command itself (src/commands/login.ts) uses PKCE (S256) with a server rendezvous — no loopback server: it mints state, code_verifier, and code_challenge, opens <frontend>/cli-login?state=...&code_challenge=..., then polls the dcd API's POST /cli-login/claim with {state, code_verifier} while the frontend POSTs the session to POST /cli-login/handoff; the API verifies sha256(verifier) === challenge and returns the session. After claiming, the CLI fetches /me/orgs and prompts for an org (the same picker dcd switch-org uses). The frontend lives in ../dcd/frontend/app/features/cli-login/CliLoginScreen.tsx.
Cross-repo auth surface. The dcd API's ApiKeyGuard accepts either x-app-api-key (existing) or Authorization: Bearer <jwt> + x-dcd-org: <id>. For Bearer it verifies the JWT, checks user_org_profile membership, and injects the org's api_key back into the request headers so existing @Headers(APP_API_KEY_HEADER) controller code keeps working unchanged. dcd switch-org calls GET /me/orgs, a JWT-only endpoint at ../dcd/api/src/apps/me/me.controller.ts.
Telemetry. src/services/telemetry.service.ts ships lifecycle (command started / command completed / command failed) and error events to the dcd API's /cli/logs proxy → Axiom cli-dev / cli-prod. Wired in at three points: src/index.ts replicates citty's runMain (which would otherwise swallow errors and exit 1) to record start/success/failure and honor CliError.exitCode; src/utils/auth.ts calls telemetry.configure({ auth }) from resolveAuth so the token never has to be re-derived; src/utils/cli.ts logger.error calls telemetry.flushSync() (which shells out to curl because process.exit bypasses beforeExit) before exiting. Unauthenticated invocations (--help, --version, dcd login pre-success) buffer in memory and drop on exit — by design, since there's no identity to attach. Opt out per-invocation with DCD_TELEMETRY_DISABLED=1.