@devicecloud.dev/dcd 4.4.8 → 5.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +40 -2
  2. package/dist/commands/artifacts.d.ts +47 -18
  3. package/dist/commands/artifacts.js +68 -60
  4. package/dist/commands/cloud.d.ts +228 -88
  5. package/dist/commands/cloud.js +389 -282
  6. package/dist/commands/list.d.ts +39 -38
  7. package/dist/commands/list.js +122 -127
  8. package/dist/commands/live.d.ts +2 -0
  9. package/dist/commands/live.js +513 -0
  10. package/dist/commands/login.d.ts +17 -0
  11. package/dist/commands/login.js +250 -0
  12. package/dist/commands/logout.d.ts +2 -0
  13. package/dist/commands/logout.js +32 -0
  14. package/dist/commands/status.d.ts +23 -42
  15. package/dist/commands/status.js +162 -173
  16. package/dist/commands/switch-org.d.ts +12 -0
  17. package/dist/commands/switch-org.js +78 -0
  18. package/dist/commands/upgrade.d.ts +2 -0
  19. package/dist/commands/upgrade.js +122 -0
  20. package/dist/commands/upload.d.ts +33 -18
  21. package/dist/commands/upload.js +62 -67
  22. package/dist/commands/whoami.d.ts +2 -0
  23. package/dist/commands/whoami.js +34 -0
  24. package/dist/config/environments.d.ts +31 -0
  25. package/dist/config/environments.js +58 -0
  26. package/dist/config/flags/api.flags.d.ts +10 -2
  27. package/dist/config/flags/api.flags.js +12 -10
  28. package/dist/config/flags/binary.flags.d.ts +17 -4
  29. package/dist/config/flags/binary.flags.js +13 -14
  30. package/dist/config/flags/device.flags.d.ts +49 -11
  31. package/dist/config/flags/device.flags.js +41 -33
  32. package/dist/config/flags/environment.flags.d.ts +27 -6
  33. package/dist/config/flags/environment.flags.js +23 -25
  34. package/dist/config/flags/execution.flags.d.ts +35 -8
  35. package/dist/config/flags/execution.flags.js +30 -37
  36. package/dist/config/flags/github.flags.d.ts +23 -5
  37. package/dist/config/flags/github.flags.js +18 -11
  38. package/dist/config/flags/output.flags.d.ts +57 -13
  39. package/dist/config/flags/output.flags.js +47 -43
  40. package/dist/constants.d.ts +218 -51
  41. package/dist/constants.js +2 -2
  42. package/dist/gateways/api-gateway.d.ts +43 -12
  43. package/dist/gateways/api-gateway.js +240 -100
  44. package/dist/gateways/cli-auth-gateway.d.ts +13 -0
  45. package/dist/gateways/cli-auth-gateway.js +57 -0
  46. package/dist/gateways/supabase-gateway.d.ts +11 -11
  47. package/dist/gateways/supabase-gateway.js +15 -39
  48. package/dist/index.d.ts +2 -1
  49. package/dist/index.js +93 -2
  50. package/dist/methods.d.ts +3 -5
  51. package/dist/methods.js +170 -178
  52. package/dist/services/device-validation.service.d.ts +8 -0
  53. package/dist/services/device-validation.service.js +55 -35
  54. package/dist/services/execution-plan.service.js +27 -15
  55. package/dist/services/execution-plan.utils.d.ts +3 -0
  56. package/dist/services/execution-plan.utils.js +10 -32
  57. package/dist/services/metadata-extractor.service.d.ts +0 -2
  58. package/dist/services/metadata-extractor.service.js +57 -57
  59. package/dist/services/moropo.service.js +25 -24
  60. package/dist/services/report-download.service.d.ts +12 -1
  61. package/dist/services/report-download.service.js +31 -20
  62. package/dist/services/results-polling.service.d.ts +6 -7
  63. package/dist/services/results-polling.service.js +80 -33
  64. package/dist/services/telemetry.service.d.ts +40 -0
  65. package/dist/services/telemetry.service.js +230 -0
  66. package/dist/services/test-submission.service.js +2 -1
  67. package/dist/services/version.service.d.ts +3 -2
  68. package/dist/services/version.service.js +27 -11
  69. package/dist/types/domain/auth.types.d.ts +12 -0
  70. package/dist/types/{schema.types.js → domain/auth.types.js} +0 -1
  71. package/dist/types/domain/live.types.d.ts +76 -0
  72. package/dist/types/domain/live.types.js +4 -0
  73. package/dist/utils/auth.d.ts +13 -0
  74. package/dist/utils/auth.js +142 -0
  75. package/dist/utils/cli.d.ts +35 -0
  76. package/dist/utils/cli.js +127 -0
  77. package/dist/utils/compatibility.d.ts +2 -1
  78. package/dist/utils/compatibility.js +2 -2
  79. package/dist/utils/config-store.d.ts +35 -0
  80. package/dist/utils/config-store.js +125 -0
  81. package/dist/utils/connectivity.js +7 -3
  82. package/dist/utils/expo.js +14 -3
  83. package/dist/utils/orgs.d.ts +11 -0
  84. package/dist/utils/orgs.js +40 -0
  85. package/dist/utils/paths.d.ts +11 -0
  86. package/dist/utils/paths.js +24 -0
  87. package/dist/utils/progress.d.ts +13 -0
  88. package/dist/utils/progress.js +50 -0
  89. package/dist/utils/styling.d.ts +13 -5
  90. package/dist/utils/styling.js +37 -7
  91. package/package.json +26 -38
  92. package/bin/dev.cmd +0 -3
  93. package/bin/dev.js +0 -6
  94. package/bin/run.cmd +0 -3
  95. package/bin/run.js +0 -7
  96. package/dist/types/schema.types.d.ts +0 -2702
  97. package/oclif.manifest.json +0 -884
@@ -4,8 +4,9 @@ import { CompatibilityData } from '../utils/compatibility';
4
4
  */
5
5
  export declare class VersionService {
6
6
  /**
7
- * Check npm registry for the latest published version of the CLI
8
- * @returns Latest version string or null if check fails
7
+ * Fetch the latest published CLI version from the release manifest.
8
+ * Works for both npm- and binary-installed users (no `npm` shell-out).
9
+ * Silently returns null on any failure — this check is informational only.
9
10
  */
10
11
  checkLatestCliVersion(): Promise<null | string>;
11
12
  /**
@@ -1,27 +1,34 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VersionService = void 0;
4
- const node_child_process_1 = require("node:child_process");
4
+ const DEFAULT_MANIFEST_URL = 'https://get.devicecloud.dev/latest.json';
5
+ const MANIFEST_TIMEOUT_MS = 3000;
5
6
  /**
6
7
  * Service for handling version validation and checking
7
8
  */
8
9
  class VersionService {
9
10
  /**
10
- * Check npm registry for the latest published version of the CLI
11
- * @returns Latest version string or null if check fails
11
+ * Fetch the latest published CLI version from the release manifest.
12
+ * Works for both npm- and binary-installed users (no `npm` shell-out).
13
+ * Silently returns null on any failure — this check is informational only.
12
14
  */
13
15
  async checkLatestCliVersion() {
16
+ const url = process.env.DCD_MANIFEST_URL ?? DEFAULT_MANIFEST_URL;
17
+ const controller = new AbortController();
18
+ const timer = setTimeout(() => controller.abort(), MANIFEST_TIMEOUT_MS);
14
19
  try {
15
- const latestVersion = (0, node_child_process_1.execSync)('npm view @devicecloud.dev/dcd version', {
16
- encoding: 'utf8',
17
- stdio: ['ignore', 'pipe', 'ignore'],
18
- }).trim();
19
- return latestVersion;
20
+ const res = await fetch(url, { signal: controller.signal });
21
+ if (!res.ok)
22
+ return null;
23
+ const data = (await res.json());
24
+ return typeof data.version === 'string' ? data.version : null;
20
25
  }
21
26
  catch {
22
- // Silently fail - version check is informational only
23
27
  return null;
24
28
  }
29
+ finally {
30
+ clearTimeout(timer);
31
+ }
25
32
  }
26
33
  /**
27
34
  * Compare two semantic version strings
@@ -30,8 +37,14 @@ class VersionService {
30
37
  * @returns true if current is older than latest
31
38
  */
32
39
  isOutdated(current, latest) {
33
- const currentParts = current.split('.').map(Number);
34
- const latestParts = latest.split('.').map(Number);
40
+ // Strip any prerelease suffix ("1.2.3-beta.1" -> "1.2.3") and default
41
+ // missing segments to 0 so short/prerelease versions still compare.
42
+ const parts = (version) => {
43
+ const nums = version.split('-')[0].split('.').map(Number);
44
+ return [nums[0] || 0, nums[1] || 0, nums[2] || 0];
45
+ };
46
+ const currentParts = parts(current);
47
+ const latestParts = parts(latest);
35
48
  for (let i = 0; i < 3; i++) {
36
49
  if (currentParts[i] < latestParts[i])
37
50
  return true;
@@ -68,6 +81,9 @@ class VersionService {
68
81
  log(`[DEBUG] Using default Maestro version ${defaultVersion}`);
69
82
  }
70
83
  }
84
+ if (!resolvedVersion) {
85
+ throw new Error('Unable to resolve a Maestro version: compatibility data did not provide a default.');
86
+ }
71
87
  // Validate Maestro version
72
88
  if (!supportedVersions.includes(resolvedVersion)) {
73
89
  throw new Error(`Maestro version ${resolvedVersion} is not supported. Supported versions: ${supportedVersions.join(', ')}`);
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Auth context threaded through gateways and services. Callers build this once
3
+ * (via resolveAuth) and the gateway spreads .headers into every fetch.
4
+ */
5
+ export interface AuthContext {
6
+ headers: Record<string, string>;
7
+ mode: 'apiKey' | 'bearer';
8
+ /** Present when mode === 'bearer'. */
9
+ orgId?: string;
10
+ /** Present when mode === 'bearer'. */
11
+ userEmail?: string;
12
+ }
@@ -1,3 +1,2 @@
1
1
  "use strict";
2
- /* eslint-disable prettier/prettier */
3
2
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,76 @@
1
+ /** Summary returned when a live session is created. */
2
+ export interface LiveSessionSummary {
3
+ id: number;
4
+ platform: string;
5
+ session_name: string;
6
+ status: string;
7
+ }
8
+ /** One node of the device view hierarchy, as the live API exposes it. */
9
+ export interface LiveHierarchyElement {
10
+ id: string;
11
+ text?: string;
12
+ accessibilityText?: string;
13
+ resourceId?: string;
14
+ resourceIdIndex?: number;
15
+ textIndex?: number;
16
+ bounds?: {
17
+ x: number;
18
+ y: number;
19
+ width: number;
20
+ height: number;
21
+ };
22
+ }
23
+ /** The device view hierarchy plus the frame it was captured against. */
24
+ export interface LiveHierarchy {
25
+ elements: LiveHierarchyElement[];
26
+ width: number;
27
+ height: number;
28
+ /** base64 PNG of the frame the hierarchy was captured against. */
29
+ screenshot: string;
30
+ }
31
+ /** Where the device is in its boot/stream lifecycle. */
32
+ export interface LiveDeviceState {
33
+ phase: string;
34
+ /** Server-derived human label for `phase` — prefer this over mapping it. */
35
+ phase_label?: string;
36
+ last_seen_at?: string;
37
+ }
38
+ /**
39
+ * Full live session record returned by `GET /live/:identifier`. The API returns
40
+ * far more than the create summary — including the current screenshot, the view
41
+ * hierarchy, and a `ready` flag — which the screenshot/hierarchy/--wait commands
42
+ * read directly.
43
+ */
44
+ export interface LiveSession extends LiveSessionSummary {
45
+ binary_upload_id: null | string;
46
+ created_at: string;
47
+ device_state?: LiveDeviceState | null;
48
+ /** Current device frame as a `data:image/png;base64,...` URL. */
49
+ screenshot_data_url?: null | string;
50
+ hierarchy?: LiveHierarchy | null;
51
+ /** True when the session can accept exec requests (RUNNING + streaming). */
52
+ ready?: boolean;
53
+ /** Model the simulator actually cloned, e.g. "pixel-7-api-34". */
54
+ device_model?: null | string;
55
+ /** Locale requested for the session, e.g. "de_DE", or null for default. */
56
+ device_locale?: null | string;
57
+ seconds_until_auto_cancel?: null | number;
58
+ }
59
+ /** Result of executing Maestro YAML against a live session. */
60
+ export interface LiveExecResult {
61
+ error?: string;
62
+ output?: string;
63
+ success: boolean;
64
+ /** Async mode only: id of the queued command to poll, and its submit status. */
65
+ commandId?: string;
66
+ status?: string;
67
+ }
68
+ /** Status of a queued live command (async exec poll). */
69
+ export interface LiveCommandStatus {
70
+ status: string;
71
+ /** True once the command reached a terminal state (passed/failed/timeout). */
72
+ done: boolean;
73
+ success: boolean;
74
+ output?: string;
75
+ error?: string;
76
+ }
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ // Hand-defined: the swagger spec has no /live routes, so openapi-typescript
3
+ // cannot generate these in schema.types.ts.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,13 @@
1
+ import type { AuthContext } from '../types/domain/auth.types';
2
+ export interface ResolveAuthOptions {
3
+ apiKeyFlag: string | undefined;
4
+ /** When true, bypass stored session entirely (for `dcd login` itself). */
5
+ skipSession?: boolean;
6
+ /**
7
+ * When true, ignore the --api-key flag / DEVICE_CLOUD_API_KEY env and
8
+ * resolve straight from the stored session (for `dcd switch-org`, which
9
+ * needs a browser login even when an API key is exported).
10
+ */
11
+ sessionOnly?: boolean;
12
+ }
13
+ export declare function resolveAuth(opts: ResolveAuthOptions): Promise<AuthContext>;
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveAuth = resolveAuth;
4
+ /**
5
+ * Resolves which credential a command should use and returns the fetch headers
6
+ * to send. Precedence: --api-key flag > DEVICE_CLOUD_API_KEY env > stored
7
+ * Supabase session (refreshed if near expiry). Throws CliError if none found.
8
+ *
9
+ * The refresh path writes the rotated tokens back to disk atomically. Callers
10
+ * use the returned AuthContext for the duration of the command — one resolve
11
+ * per invocation, not per request.
12
+ */
13
+ const node_fs_1 = require("node:fs");
14
+ const environments_1 = require("../config/environments");
15
+ const cli_auth_gateway_1 = require("../gateways/cli-auth-gateway");
16
+ const telemetry_service_1 = require("../services/telemetry.service");
17
+ const cli_1 = require("./cli");
18
+ const config_store_1 = require("./config-store");
19
+ const REFRESH_SKEW_SECONDS = 60;
20
+ // Refresh lock tuning: a refresh is a single HTTP round-trip, so anything
21
+ // holding the lock longer than this is presumed dead.
22
+ const LOCK_STALE_MS = 10_000;
23
+ const LOCK_WAIT_MS = 10_000;
24
+ const LOCK_POLL_MS = 250;
25
+ async function resolveAuth(opts) {
26
+ if (!opts.sessionOnly) {
27
+ const flag = opts.apiKeyFlag?.trim();
28
+ if (flag) {
29
+ const auth = {
30
+ mode: 'apiKey',
31
+ headers: { 'x-app-api-key': flag },
32
+ };
33
+ telemetry_service_1.telemetry.configure({ auth });
34
+ return auth;
35
+ }
36
+ const env = process.env.DEVICE_CLOUD_API_KEY?.trim();
37
+ if (env) {
38
+ const auth = {
39
+ mode: 'apiKey',
40
+ headers: { 'x-app-api-key': env },
41
+ };
42
+ telemetry_service_1.telemetry.configure({ auth });
43
+ return auth;
44
+ }
45
+ }
46
+ if (opts.skipSession) {
47
+ throw missingCredentialsError();
48
+ }
49
+ let config = (0, config_store_1.readConfig)();
50
+ if (!config?.session) {
51
+ throw missingCredentialsError();
52
+ }
53
+ const now = Math.floor(Date.now() / 1000);
54
+ let session = config.session;
55
+ if (session.expires_at <= now + REFRESH_SKEW_SECONDS) {
56
+ ({ config, session } = await refreshSessionWithLock(config));
57
+ }
58
+ if (!config.current_org_id) {
59
+ throw new cli_1.CliError('No active organization set. Run `dcd switch-org <slug>` to pick one.');
60
+ }
61
+ const auth = {
62
+ mode: 'bearer',
63
+ orgId: config.current_org_id,
64
+ userEmail: session.user_email,
65
+ headers: {
66
+ authorization: `Bearer ${session.access_token}`,
67
+ 'x-dcd-org': config.current_org_id,
68
+ },
69
+ };
70
+ telemetry_service_1.telemetry.configure({ auth, apiUrl: config.api_url });
71
+ return auth;
72
+ }
73
+ /**
74
+ * Refresh the stored session under a config-adjacent lockfile so two
75
+ * concurrent `dcd` invocations (CI matrices) can't both consume the same
76
+ * Supabase refresh token — token rotation would revoke the session family.
77
+ */
78
+ async function refreshSessionWithLock(initial) {
79
+ const lockPath = `${(0, config_store_1.getConfigPath)()}.lock`;
80
+ await acquireRefreshLock(lockPath);
81
+ try {
82
+ // Re-read: another process may have refreshed while we waited.
83
+ const current = (0, config_store_1.readConfig)() ?? initial;
84
+ const session = current.session ?? initial.session;
85
+ const now = Math.floor(Date.now() / 1000);
86
+ if (session.expires_at > now + REFRESH_SKEW_SECONDS) {
87
+ return { config: current, session };
88
+ }
89
+ const { anonKey } = environments_1.ENVIRONMENTS[current.env].supabase;
90
+ const refreshed = await cli_auth_gateway_1.CliAuthGateway.refresh(current.supabase_url, anonKey, session);
91
+ // Re-read again and merge only `session` so a concurrent `switch-org`
92
+ // write (org fields) isn't reverted by our pre-refresh snapshot.
93
+ const merged = { ...((0, config_store_1.readConfig)() ?? current), session: refreshed };
94
+ (0, config_store_1.writeConfig)(merged);
95
+ return { config: merged, session: refreshed };
96
+ }
97
+ finally {
98
+ try {
99
+ (0, node_fs_1.rmSync)(lockPath, { force: true });
100
+ }
101
+ catch { /* best effort */ }
102
+ }
103
+ }
104
+ async function acquireRefreshLock(lockPath) {
105
+ const deadline = Date.now() + LOCK_WAIT_MS;
106
+ for (;;) {
107
+ try {
108
+ (0, node_fs_1.closeSync)((0, node_fs_1.openSync)(lockPath, 'wx'));
109
+ return;
110
+ }
111
+ catch {
112
+ if (Date.now() >= deadline) {
113
+ // Don't hang the command forever: steal the lock if possible and
114
+ // proceed regardless — worst case we race like the pre-lock code did.
115
+ try {
116
+ (0, node_fs_1.rmSync)(lockPath, { force: true });
117
+ }
118
+ catch { /* best effort */ }
119
+ try {
120
+ (0, node_fs_1.closeSync)((0, node_fs_1.openSync)(lockPath, 'wx'));
121
+ }
122
+ catch { /* best effort */ }
123
+ return;
124
+ }
125
+ try {
126
+ if (Date.now() - (0, node_fs_1.statSync)(lockPath).mtimeMs > LOCK_STALE_MS) {
127
+ // Holder presumed dead — take over.
128
+ (0, node_fs_1.rmSync)(lockPath, { force: true });
129
+ continue;
130
+ }
131
+ }
132
+ catch {
133
+ // Lock vanished between open and stat — fall through to a short
134
+ // sleep (not an immediate retry) so persistent fs errors can't spin.
135
+ }
136
+ await new Promise((resolve) => { setTimeout(resolve, LOCK_POLL_MS); });
137
+ }
138
+ }
139
+ }
140
+ function missingCredentialsError() {
141
+ return new cli_1.CliError('Not authenticated. Provide an API key via --api-key or the DEVICE_CLOUD_API_KEY environment variable, or run `dcd login`.');
142
+ }
@@ -0,0 +1,35 @@
1
+ export declare function getCliVersion(): string;
2
+ export type InstallMethod = 'binary' | 'npm';
3
+ export declare function getInstallMethod(): InstallMethod;
4
+ export declare function getUpgradeCommand(): string;
5
+ export declare class CliError extends Error {
6
+ exitCode: number;
7
+ constructor(message: string, exitCode?: number);
8
+ }
9
+ export interface Logger {
10
+ log(message: string): void;
11
+ warn(message: string): void;
12
+ error(message: Error | string, opts?: {
13
+ exit?: number;
14
+ json?: boolean;
15
+ }): never;
16
+ exit(code?: number): never;
17
+ }
18
+ export declare const logger: Logger;
19
+ /**
20
+ * Validate that a string flag value is one of the allowed options.
21
+ * Returns the value untouched on success; throws CliError otherwise.
22
+ */
23
+ export declare function validateEnum<T extends string>(value: string | undefined, allowed: readonly T[], flagName: string): T | undefined;
24
+ /**
25
+ * Coerce a flag value (possibly a single string, array, or undefined) into a
26
+ * flat string array. Comma-separated values inside each entry are split out.
27
+ * Used for repeatable flags like --include-tags, --env, --metadata where citty
28
+ * surfaces a string (single use) or string[] (repeated).
29
+ */
30
+ export declare function coerceArray(value: string | string[] | undefined, split?: boolean): string[];
31
+ /**
32
+ * Parse an integer flag. Returns undefined if the value is undefined/empty.
33
+ * Throws CliError if the value is not a valid integer.
34
+ */
35
+ export declare function parseIntFlag(value: string | undefined, flagName: string): number | undefined;
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.logger = exports.CliError = void 0;
4
+ exports.getCliVersion = getCliVersion;
5
+ exports.getInstallMethod = getInstallMethod;
6
+ exports.getUpgradeCommand = getUpgradeCommand;
7
+ exports.validateEnum = validateEnum;
8
+ exports.coerceArray = coerceArray;
9
+ exports.parseIntFlag = parseIntFlag;
10
+ /**
11
+ * Shared helpers for command files.
12
+ * - Version lookup from package.json
13
+ * - Enum validation for string flags (citty 0.1.6 has no native enum)
14
+ * - Array coercion for repeatable flags with comma-separated values
15
+ * - A minimal Logger mirroring the oclif Command log/warn/error shape so call
16
+ * sites ported from oclif keep working.
17
+ */
18
+ const telemetry_service_1 = require("../services/telemetry.service");
19
+ const styling_1 = require("./styling");
20
+ // Resolve version at runtime — avoids pulling package.json into the tsbuildinfo rootDir.
21
+ function getCliVersion() {
22
+ try {
23
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
24
+ const pkg = require('../../package.json');
25
+ return pkg.version;
26
+ }
27
+ catch {
28
+ return '0.0.0';
29
+ }
30
+ }
31
+ // The Bun runtime sets process.versions.bun; bun-compiled standalone binaries
32
+ // inherit this. Node-run installs (npm/pnpm/npx/tsx) don't expose it.
33
+ function getInstallMethod() {
34
+ return typeof process.versions.bun === 'string'
35
+ ? 'binary'
36
+ : 'npm';
37
+ }
38
+ function getUpgradeCommand() {
39
+ return getInstallMethod() === 'binary'
40
+ ? 'dcd upgrade'
41
+ : 'npm install -g @devicecloud.dev/dcd@latest';
42
+ }
43
+ class CliError extends Error {
44
+ exitCode;
45
+ constructor(message, exitCode = 1) {
46
+ super(message);
47
+ this.exitCode = exitCode;
48
+ this.name = 'CliError';
49
+ }
50
+ }
51
+ exports.CliError = CliError;
52
+ exports.logger = {
53
+ log(message) {
54
+ // eslint-disable-next-line no-console
55
+ console.log(message);
56
+ },
57
+ warn(message) {
58
+ // eslint-disable-next-line no-console
59
+ console.warn(styling_1.symbols.warning + ' ' + message);
60
+ },
61
+ error(message, opts = {}) {
62
+ const text = message instanceof Error ? message.message : message;
63
+ if (opts.json) {
64
+ // When --json is active, emit failures as JSON on stdout so that
65
+ // scripts consuming the output never need to parse stderr too.
66
+ // eslint-disable-next-line no-console
67
+ console.log(JSON.stringify({ status: 'FAILED', error: text }, null, 2));
68
+ }
69
+ else {
70
+ // The literal "Error:" prefix is important for tests and for grep-friendly
71
+ // CI logs — it survives color stripping and matches /error/i assertions.
72
+ // eslint-disable-next-line no-console
73
+ console.error(styling_1.symbols.error + ' Error: ' + text);
74
+ }
75
+ // process.exit bypasses beforeExit, so async fetch in telemetry.flush()
76
+ // would be killed mid-flight. flushSync uses curl to ship synchronously
77
+ // before we exit; it's a no-op if telemetry never reached configure().
78
+ const exitCode = opts.exit ?? 1;
79
+ telemetry_service_1.telemetry.recordCommandFailure({ error: message, exitCode });
80
+ telemetry_service_1.telemetry.flushSync();
81
+ process.exit(exitCode);
82
+ },
83
+ exit(code = 0) {
84
+ process.exit(code);
85
+ },
86
+ };
87
+ /**
88
+ * Validate that a string flag value is one of the allowed options.
89
+ * Returns the value untouched on success; throws CliError otherwise.
90
+ */
91
+ function validateEnum(value, allowed, flagName) {
92
+ if (value === undefined || value === null || value === '')
93
+ return undefined;
94
+ if (!allowed.includes(value)) {
95
+ throw new CliError(`Invalid value for --${flagName}: "${value}". Expected one of: ${allowed.join(', ')}`);
96
+ }
97
+ return value;
98
+ }
99
+ /**
100
+ * Coerce a flag value (possibly a single string, array, or undefined) into a
101
+ * flat string array. Comma-separated values inside each entry are split out.
102
+ * Used for repeatable flags like --include-tags, --env, --metadata where citty
103
+ * surfaces a string (single use) or string[] (repeated).
104
+ */
105
+ function coerceArray(value, split = true) {
106
+ if (value === undefined)
107
+ return [];
108
+ const arr = Array.isArray(value) ? value : [value];
109
+ if (!split)
110
+ return arr;
111
+ return arr.flatMap((v) => v.split(','));
112
+ }
113
+ /**
114
+ * Parse an integer flag. Returns undefined if the value is undefined/empty.
115
+ * Throws CliError if the value is not a valid integer.
116
+ */
117
+ function parseIntFlag(value, flagName) {
118
+ if (value === undefined || value === null || value === '')
119
+ return undefined;
120
+ // All integer flags (limit/offset/retry) are non-negative; also rejects
121
+ // trailing garbage that parseInt would silently accept ("20abc" -> 20).
122
+ const trimmed = String(value).trim();
123
+ if (!/^\d+$/.test(trimmed)) {
124
+ throw new CliError(`Invalid integer value for --${flagName}: "${value}"`);
125
+ }
126
+ return Number.parseInt(trimmed, 10);
127
+ }
@@ -1,3 +1,4 @@
1
+ import type { AuthContext } from '../types/domain/auth.types';
1
2
  export interface CompatibilityData {
2
3
  android: Record<string, string[]>;
3
4
  androidPlay: Record<string, string[]>;
@@ -8,5 +9,5 @@ export interface CompatibilityData {
8
9
  supportedVersions: string[];
9
10
  };
10
11
  }
11
- export declare function fetchCompatibilityData(apiUrl: string, apiKey: string): Promise<CompatibilityData>;
12
+ export declare function fetchCompatibilityData(apiUrl: string, auth: AuthContext): Promise<CompatibilityData>;
12
13
  export declare function clearCompatibilityCache(): void;
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.fetchCompatibilityData = fetchCompatibilityData;
4
4
  exports.clearCompatibilityCache = clearCompatibilityCache;
5
5
  let cachedCompatibilityData = null;
6
- async function fetchCompatibilityData(apiUrl, apiKey) {
6
+ async function fetchCompatibilityData(apiUrl, auth) {
7
7
  if (cachedCompatibilityData) {
8
8
  return cachedCompatibilityData;
9
9
  }
@@ -11,7 +11,7 @@ async function fetchCompatibilityData(apiUrl, apiKey) {
11
11
  const response = await fetch(`${apiUrl}/results/compatibility/data`, {
12
12
  headers: {
13
13
  'Content-Type': 'application/json',
14
- 'x-app-api-key': apiKey,
14
+ ...auth.headers,
15
15
  },
16
16
  method: 'GET',
17
17
  });
@@ -0,0 +1,35 @@
1
+ export declare const CONFIG_SCHEMA_VERSION = 1;
2
+ export interface StoredSession {
3
+ access_token: string;
4
+ refresh_token: string;
5
+ /** Unix epoch seconds. */
6
+ expires_at: number;
7
+ user_email: string;
8
+ user_id: string;
9
+ }
10
+ export interface StoredConfig {
11
+ version: number;
12
+ env: 'dev' | 'prod';
13
+ api_url: string;
14
+ supabase_url: string;
15
+ session?: StoredSession;
16
+ current_org_id?: string;
17
+ current_org_name?: string;
18
+ }
19
+ export declare function getConfigDir(): string;
20
+ export declare function getConfigPath(): string;
21
+ export declare function readConfig(): StoredConfig | null;
22
+ /**
23
+ * Resolve the API URL a command should talk to. Precedence:
24
+ * 1. explicit --api-url flag
25
+ * 2. api_url stored by `dcd login` (honors the env the user logged into)
26
+ * 3. prod default
27
+ *
28
+ * Without this, session commands default to prod and a dev/staging Bearer
29
+ * token is rejected with a misleading "Invalid or expired JWT". `switch-org`
30
+ * has always done this; this helper extends it to every command.
31
+ */
32
+ export declare function resolveApiUrl(flag: string | undefined): string;
33
+ export declare function writeConfig(config: StoredConfig): void;
34
+ export declare function clearConfig(): void;
35
+ export declare function configFileMode(): number | null;