@devicecloud.dev/dcd 4.4.9 → 5.0.0-beta.1

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 (130) hide show
  1. package/README.md +75 -2
  2. package/dist/commands/artifacts.d.ts +47 -18
  3. package/dist/commands/artifacts.js +69 -64
  4. package/dist/commands/cloud.d.ts +228 -88
  5. package/dist/commands/cloud.js +430 -342
  6. package/dist/commands/list.d.ts +39 -38
  7. package/dist/commands/list.js +124 -131
  8. package/dist/commands/live.d.ts +2 -0
  9. package/dist/commands/live.js +520 -0
  10. package/dist/commands/login.d.ts +17 -0
  11. package/dist/commands/login.js +252 -0
  12. package/dist/commands/logout.d.ts +2 -0
  13. package/dist/commands/logout.js +30 -0
  14. package/dist/commands/status.d.ts +23 -42
  15. package/dist/commands/status.js +170 -179
  16. package/dist/commands/switch-org.d.ts +12 -0
  17. package/dist/commands/switch-org.js +76 -0
  18. package/dist/commands/upgrade.d.ts +2 -0
  19. package/dist/commands/upgrade.js +120 -0
  20. package/dist/commands/upload.d.ts +33 -18
  21. package/dist/commands/upload.js +72 -78
  22. package/dist/commands/whoami.d.ts +2 -0
  23. package/dist/commands/whoami.js +31 -0
  24. package/dist/config/environments.d.ts +31 -0
  25. package/dist/config/environments.js +52 -0
  26. package/dist/config/flags/api.flags.d.ts +10 -2
  27. package/dist/config/flags/api.flags.js +13 -14
  28. package/dist/config/flags/binary.flags.d.ts +17 -4
  29. package/dist/config/flags/binary.flags.js +14 -18
  30. package/dist/config/flags/device.flags.d.ts +49 -11
  31. package/dist/config/flags/device.flags.js +43 -38
  32. package/dist/config/flags/environment.flags.d.ts +27 -6
  33. package/dist/config/flags/environment.flags.js +24 -29
  34. package/dist/config/flags/execution.flags.d.ts +35 -8
  35. package/dist/config/flags/execution.flags.js +31 -41
  36. package/dist/config/flags/github.flags.d.ts +23 -5
  37. package/dist/config/flags/github.flags.js +19 -15
  38. package/dist/config/flags/output.flags.d.ts +57 -13
  39. package/dist/config/flags/output.flags.js +48 -47
  40. package/dist/constants.d.ts +218 -51
  41. package/dist/constants.js +17 -20
  42. package/dist/gateways/api-gateway.d.ts +72 -16
  43. package/dist/gateways/api-gateway.js +298 -104
  44. package/dist/gateways/cli-auth-gateway.d.ts +13 -0
  45. package/dist/gateways/cli-auth-gateway.js +54 -0
  46. package/dist/gateways/realtime-gateway.d.ts +32 -0
  47. package/dist/gateways/realtime-gateway.js +103 -0
  48. package/dist/gateways/supabase-gateway.d.ts +11 -11
  49. package/dist/gateways/supabase-gateway.js +20 -48
  50. package/dist/index.d.ts +2 -1
  51. package/dist/index.js +98 -4
  52. package/dist/mcp/context.d.ts +33 -0
  53. package/dist/mcp/context.js +33 -0
  54. package/dist/mcp/helpers.d.ts +16 -0
  55. package/dist/mcp/helpers.js +34 -0
  56. package/dist/mcp/index.d.ts +2 -0
  57. package/dist/mcp/index.js +24 -0
  58. package/dist/mcp/server.d.ts +7 -0
  59. package/dist/mcp/server.js +27 -0
  60. package/dist/mcp/tools/download-artifacts.d.ts +11 -0
  61. package/dist/mcp/tools/download-artifacts.js +84 -0
  62. package/dist/mcp/tools/get-status.d.ts +7 -0
  63. package/dist/mcp/tools/get-status.js +39 -0
  64. package/dist/mcp/tools/list-devices.d.ts +7 -0
  65. package/dist/mcp/tools/list-devices.js +27 -0
  66. package/dist/mcp/tools/list-runs.d.ts +3 -0
  67. package/dist/mcp/tools/list-runs.js +60 -0
  68. package/dist/mcp/tools/run-cloud-test.d.ts +14 -0
  69. package/dist/mcp/tools/run-cloud-test.js +233 -0
  70. package/dist/methods.d.ts +34 -5
  71. package/dist/methods.js +266 -215
  72. package/dist/services/device-validation.service.d.ts +9 -1
  73. package/dist/services/device-validation.service.js +56 -40
  74. package/dist/services/execution-plan.service.js +40 -31
  75. package/dist/services/execution-plan.utils.d.ts +3 -0
  76. package/dist/services/execution-plan.utils.js +25 -55
  77. package/dist/services/flow-paths.d.ts +17 -0
  78. package/dist/services/flow-paths.js +52 -0
  79. package/dist/services/metadata-extractor.service.d.ts +0 -2
  80. package/dist/services/metadata-extractor.service.js +75 -78
  81. package/dist/services/moropo.service.js +33 -34
  82. package/dist/services/report-download.service.d.ts +12 -1
  83. package/dist/services/report-download.service.js +34 -27
  84. package/dist/services/results-polling.service.d.ts +23 -9
  85. package/dist/services/results-polling.service.js +257 -123
  86. package/dist/services/telemetry.service.d.ts +49 -0
  87. package/dist/services/telemetry.service.js +252 -0
  88. package/dist/services/test-submission.service.d.ts +21 -4
  89. package/dist/services/test-submission.service.js +51 -33
  90. package/dist/services/version.service.d.ts +4 -3
  91. package/dist/services/version.service.js +28 -16
  92. package/dist/types/domain/auth.types.d.ts +20 -0
  93. package/dist/types/domain/auth.types.js +1 -0
  94. package/dist/types/domain/device.types.js +8 -11
  95. package/dist/types/domain/live.types.d.ts +76 -0
  96. package/dist/types/domain/live.types.js +3 -0
  97. package/dist/types/generated/schema.types.js +1 -2
  98. package/dist/types/index.d.ts +2 -2
  99. package/dist/types/index.js +2 -18
  100. package/dist/types.js +1 -2
  101. package/dist/utils/auth.d.ts +13 -0
  102. package/dist/utils/auth.js +141 -0
  103. package/dist/utils/ci.d.ts +12 -0
  104. package/dist/utils/ci.js +39 -0
  105. package/dist/utils/cli.d.ts +35 -0
  106. package/dist/utils/cli.js +118 -0
  107. package/dist/utils/compatibility.d.ts +2 -1
  108. package/dist/utils/compatibility.js +6 -8
  109. package/dist/utils/config-store.d.ts +35 -0
  110. package/dist/utils/config-store.js +115 -0
  111. package/dist/utils/connectivity.js +8 -7
  112. package/dist/utils/expo.js +29 -24
  113. package/dist/utils/orgs.d.ts +11 -0
  114. package/dist/utils/orgs.js +36 -0
  115. package/dist/utils/paths.d.ts +11 -0
  116. package/dist/utils/paths.js +21 -0
  117. package/dist/utils/progress.d.ts +13 -0
  118. package/dist/utils/progress.js +47 -0
  119. package/dist/utils/styling.d.ts +42 -36
  120. package/dist/utils/styling.js +78 -82
  121. package/dist/utils/ui.d.ts +41 -0
  122. package/dist/utils/ui.js +95 -0
  123. package/package.json +36 -45
  124. package/bin/dev.cmd +0 -3
  125. package/bin/dev.js +0 -6
  126. package/bin/run.cmd +0 -3
  127. package/bin/run.js +0 -7
  128. package/dist/types/schema.types.d.ts +0 -2702
  129. package/dist/types/schema.types.js +0 -3
  130. package/oclif.manifest.json +0 -884
@@ -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,3 @@
1
+ // Hand-defined: the swagger spec has no /live routes, so openapi-typescript
2
+ // cannot generate these in schema.types.ts.
3
+ export {};
@@ -1,3 +1,2 @@
1
- "use strict";
2
1
  /* eslint-disable prettier/prettier */
3
- Object.defineProperty(exports, "__esModule", { value: true });
2
+ export {};
@@ -2,5 +2,5 @@
2
2
  * Centralized type exports.
3
3
  * Types are organized by domain in subdirectories.
4
4
  */
5
- export * from './domain/device.types';
6
- export * from './generated/schema.types';
5
+ export * from './domain/device.types.js';
6
+ export * from './generated/schema.types.js';
@@ -1,24 +1,8 @@
1
- "use strict";
2
1
  /**
3
2
  * Centralized type exports.
4
3
  * Types are organized by domain in subdirectories.
5
4
  */
6
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
- if (k2 === undefined) k2 = k;
8
- var desc = Object.getOwnPropertyDescriptor(m, k);
9
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
- desc = { enumerable: true, get: function() { return m[k]; } };
11
- }
12
- Object.defineProperty(o, k2, desc);
13
- }) : (function(o, m, k, k2) {
14
- if (k2 === undefined) k2 = k;
15
- o[k2] = m[k];
16
- }));
17
- var __exportStar = (this && this.__exportStar) || function(m, exports) {
18
- for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
19
- };
20
- Object.defineProperty(exports, "__esModule", { value: true });
21
5
  // Domain-specific types
22
- __exportStar(require("./domain/device.types"), exports);
6
+ export * from './domain/device.types.js';
23
7
  // Generated types from OpenAPI schema
24
- __exportStar(require("./generated/schema.types"), exports);
8
+ export * from './generated/schema.types.js';
package/dist/types.js CHANGED
@@ -1,2 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
1
+ export {};
@@ -0,0 +1,13 @@
1
+ import type { AuthContext } from '../types/domain/auth.types.js';
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,141 @@
1
+ /**
2
+ * Resolves which credential a command should use and returns the fetch headers
3
+ * to send. Precedence: --api-key flag > DEVICE_CLOUD_API_KEY env > stored
4
+ * Supabase session (refreshed if near expiry). Throws CliError if none found.
5
+ *
6
+ * The refresh path writes the rotated tokens back to disk atomically. Callers
7
+ * use the returned AuthContext for the duration of the command — one resolve
8
+ * per invocation, not per request.
9
+ */
10
+ import { closeSync, openSync, rmSync, statSync } from 'node:fs';
11
+ import { ENVIRONMENTS } from '../config/environments.js';
12
+ import { CliAuthGateway } from '../gateways/cli-auth-gateway.js';
13
+ import { telemetry } from '../services/telemetry.service.js';
14
+ import { CliError } from './cli.js';
15
+ import { getConfigPath, readConfig, writeConfig, } from './config-store.js';
16
+ const REFRESH_SKEW_SECONDS = 60;
17
+ // Refresh lock tuning: a refresh is a single HTTP round-trip, so anything
18
+ // holding the lock longer than this is presumed dead.
19
+ const LOCK_STALE_MS = 10_000;
20
+ const LOCK_WAIT_MS = 10_000;
21
+ const LOCK_POLL_MS = 250;
22
+ export async function resolveAuth(opts) {
23
+ if (!opts.sessionOnly) {
24
+ const flag = opts.apiKeyFlag?.trim();
25
+ if (flag) {
26
+ const auth = {
27
+ mode: 'apiKey',
28
+ headers: { 'x-app-api-key': flag },
29
+ };
30
+ telemetry.configure({ auth });
31
+ return auth;
32
+ }
33
+ const env = process.env.DEVICE_CLOUD_API_KEY?.trim();
34
+ if (env) {
35
+ const auth = {
36
+ mode: 'apiKey',
37
+ headers: { 'x-app-api-key': env },
38
+ };
39
+ telemetry.configure({ auth });
40
+ return auth;
41
+ }
42
+ }
43
+ if (opts.skipSession) {
44
+ throw missingCredentialsError();
45
+ }
46
+ let config = readConfig();
47
+ if (!config?.session) {
48
+ throw missingCredentialsError();
49
+ }
50
+ const now = Math.floor(Date.now() / 1000);
51
+ let session = config.session;
52
+ if (session.expires_at <= now + REFRESH_SKEW_SECONDS) {
53
+ ({ config, session } = await refreshSessionWithLock(config));
54
+ }
55
+ if (!config.current_org_id) {
56
+ throw new CliError('No active organization set. Run `dcd switch-org <slug>` to pick one.');
57
+ }
58
+ const auth = {
59
+ mode: 'bearer',
60
+ accessToken: session.access_token,
61
+ env: config.env,
62
+ orgId: config.current_org_id,
63
+ userEmail: session.user_email,
64
+ headers: {
65
+ authorization: `Bearer ${session.access_token}`,
66
+ 'x-dcd-org': config.current_org_id,
67
+ },
68
+ };
69
+ telemetry.configure({ auth, apiUrl: config.api_url });
70
+ return auth;
71
+ }
72
+ /**
73
+ * Refresh the stored session under a config-adjacent lockfile so two
74
+ * concurrent `dcd` invocations (CI matrices) can't both consume the same
75
+ * Supabase refresh token — token rotation would revoke the session family.
76
+ */
77
+ async function refreshSessionWithLock(initial) {
78
+ const lockPath = `${getConfigPath()}.lock`;
79
+ await acquireRefreshLock(lockPath);
80
+ try {
81
+ // Re-read: another process may have refreshed while we waited.
82
+ const current = readConfig() ?? initial;
83
+ const session = current.session ?? initial.session;
84
+ const now = Math.floor(Date.now() / 1000);
85
+ if (session.expires_at > now + REFRESH_SKEW_SECONDS) {
86
+ return { config: current, session };
87
+ }
88
+ const { anonKey } = ENVIRONMENTS[current.env].supabase;
89
+ const refreshed = await CliAuthGateway.refresh(current.supabase_url, anonKey, session);
90
+ // Re-read again and merge only `session` so a concurrent `switch-org`
91
+ // write (org fields) isn't reverted by our pre-refresh snapshot.
92
+ const merged = { ...(readConfig() ?? current), session: refreshed };
93
+ writeConfig(merged);
94
+ return { config: merged, session: refreshed };
95
+ }
96
+ finally {
97
+ try {
98
+ rmSync(lockPath, { force: true });
99
+ }
100
+ catch { /* best effort */ }
101
+ }
102
+ }
103
+ async function acquireRefreshLock(lockPath) {
104
+ const deadline = Date.now() + LOCK_WAIT_MS;
105
+ for (;;) {
106
+ try {
107
+ closeSync(openSync(lockPath, 'wx'));
108
+ return;
109
+ }
110
+ catch {
111
+ if (Date.now() >= deadline) {
112
+ // Don't hang the command forever: steal the lock if possible and
113
+ // proceed regardless — worst case we race like the pre-lock code did.
114
+ try {
115
+ rmSync(lockPath, { force: true });
116
+ }
117
+ catch { /* best effort */ }
118
+ try {
119
+ closeSync(openSync(lockPath, 'wx'));
120
+ }
121
+ catch { /* best effort */ }
122
+ return;
123
+ }
124
+ try {
125
+ if (Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS) {
126
+ // Holder presumed dead — take over.
127
+ rmSync(lockPath, { force: true });
128
+ continue;
129
+ }
130
+ }
131
+ catch {
132
+ // Lock vanished between open and stat — fall through to a short
133
+ // sleep (not an immediate retry) so persistent fs errors can't spin.
134
+ }
135
+ await new Promise((resolve) => { setTimeout(resolve, LOCK_POLL_MS); });
136
+ }
137
+ }
138
+ }
139
+ function missingCredentialsError() {
140
+ return new CliError('Not authenticated. Provide an API key via --api-key or the DEVICE_CLOUD_API_KEY environment variable, or run `dcd login`.');
141
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Best-effort detection of non-interactive / CI environments. Used to suppress
3
+ * interactive niceties (e.g. the `dcd login` nudge) that only make sense for a
4
+ * human at a terminal. Dependency-free on purpose — these are the env vars the
5
+ * major providers set, plus a TTY check for piped/redirected output.
6
+ */
7
+ /**
8
+ * Returns true when running under CI or otherwise non-interactively (no TTY on
9
+ * stdout, e.g. output piped to a file). A truthy value for any known CI env var
10
+ * counts — providers set `CI=true`, but a bare presence check is the safe net.
11
+ */
12
+ export declare function isCI(): boolean;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Best-effort detection of non-interactive / CI environments. Used to suppress
3
+ * interactive niceties (e.g. the `dcd login` nudge) that only make sense for a
4
+ * human at a terminal. Dependency-free on purpose — these are the env vars the
5
+ * major providers set, plus a TTY check for piped/redirected output.
6
+ */
7
+ // Generic + per-provider markers. `CI` is set by virtually every provider;
8
+ // the rest cover platforms that historically didn't set it.
9
+ const CI_ENV_VARS = [
10
+ 'CI',
11
+ 'CONTINUOUS_INTEGRATION',
12
+ 'BUILD_NUMBER',
13
+ 'GITHUB_ACTIONS',
14
+ 'GITLAB_CI',
15
+ 'CIRCLECI',
16
+ 'TRAVIS',
17
+ 'BUILDKITE',
18
+ 'DRONE',
19
+ 'TEAMCITY_VERSION',
20
+ 'TF_BUILD', // Azure Pipelines
21
+ 'JENKINS_URL',
22
+ 'BITBUCKET_BUILD_NUMBER',
23
+ 'APPVEYOR',
24
+ 'CODEBUILD_BUILD_ID',
25
+ ];
26
+ /**
27
+ * Returns true when running under CI or otherwise non-interactively (no TTY on
28
+ * stdout, e.g. output piped to a file). A truthy value for any known CI env var
29
+ * counts — providers set `CI=true`, but a bare presence check is the safe net.
30
+ */
31
+ export function isCI() {
32
+ for (const name of CI_ENV_VARS) {
33
+ const value = process.env[name];
34
+ if (value !== undefined && value !== '' && value !== 'false' && value !== '0') {
35
+ return true;
36
+ }
37
+ }
38
+ return !process.stdout.isTTY;
39
+ }
@@ -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,118 @@
1
+ /**
2
+ * Shared helpers for command files.
3
+ * - Version lookup from package.json
4
+ * - Enum validation for string flags (citty 0.1.6 has no native enum)
5
+ * - Array coercion for repeatable flags with comma-separated values
6
+ * - A minimal Logger mirroring the oclif Command log/warn/error shape so call
7
+ * sites ported from oclif keep working.
8
+ */
9
+ import { readFileSync } from 'node:fs';
10
+ import { telemetry } from '../services/telemetry.service.js';
11
+ import { symbols } from './styling.js';
12
+ // Resolve version at runtime — read the file rather than importing it, so
13
+ // package.json never gets pulled into the tsc program / dist rootDir.
14
+ export function getCliVersion() {
15
+ try {
16
+ const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
17
+ return pkg.version;
18
+ }
19
+ catch {
20
+ return '0.0.0';
21
+ }
22
+ }
23
+ // The Bun runtime sets process.versions.bun; bun-compiled standalone binaries
24
+ // inherit this. Node-run installs (npm/pnpm/npx/tsx) don't expose it.
25
+ export function getInstallMethod() {
26
+ return typeof process.versions.bun === 'string'
27
+ ? 'binary'
28
+ : 'npm';
29
+ }
30
+ export function getUpgradeCommand() {
31
+ return getInstallMethod() === 'binary'
32
+ ? 'dcd upgrade'
33
+ : 'npm install -g @devicecloud.dev/dcd@latest';
34
+ }
35
+ export class CliError extends Error {
36
+ exitCode;
37
+ constructor(message, exitCode = 1) {
38
+ super(message);
39
+ this.exitCode = exitCode;
40
+ this.name = 'CliError';
41
+ }
42
+ }
43
+ export const logger = {
44
+ log(message) {
45
+ // eslint-disable-next-line no-console
46
+ console.log(message);
47
+ },
48
+ warn(message) {
49
+ // eslint-disable-next-line no-console
50
+ console.warn(symbols.warning + ' ' + message);
51
+ },
52
+ error(message, opts = {}) {
53
+ const text = message instanceof Error ? message.message : message;
54
+ if (opts.json) {
55
+ // When --json is active, emit failures as JSON on stdout so that
56
+ // scripts consuming the output never need to parse stderr too.
57
+ // eslint-disable-next-line no-console
58
+ console.log(JSON.stringify({ status: 'FAILED', error: text }, null, 2));
59
+ }
60
+ else {
61
+ // The literal "Error:" prefix is important for tests and for grep-friendly
62
+ // CI logs — it survives color stripping and matches /error/i assertions.
63
+ // eslint-disable-next-line no-console
64
+ console.error(symbols.error + ' Error: ' + text);
65
+ }
66
+ // process.exit bypasses beforeExit, so async fetch in telemetry.flush()
67
+ // would be killed mid-flight. flushSync uses curl to ship synchronously
68
+ // before we exit; it's a no-op if telemetry never reached configure().
69
+ const exitCode = opts.exit ?? 1;
70
+ telemetry.recordCommandFailure({ error: message, exitCode });
71
+ telemetry.flushSync();
72
+ process.exit(exitCode);
73
+ },
74
+ exit(code = 0) {
75
+ process.exit(code);
76
+ },
77
+ };
78
+ /**
79
+ * Validate that a string flag value is one of the allowed options.
80
+ * Returns the value untouched on success; throws CliError otherwise.
81
+ */
82
+ export function validateEnum(value, allowed, flagName) {
83
+ if (value === undefined || value === null || value === '')
84
+ return undefined;
85
+ if (!allowed.includes(value)) {
86
+ throw new CliError(`Invalid value for --${flagName}: "${value}". Expected one of: ${allowed.join(', ')}`);
87
+ }
88
+ return value;
89
+ }
90
+ /**
91
+ * Coerce a flag value (possibly a single string, array, or undefined) into a
92
+ * flat string array. Comma-separated values inside each entry are split out.
93
+ * Used for repeatable flags like --include-tags, --env, --metadata where citty
94
+ * surfaces a string (single use) or string[] (repeated).
95
+ */
96
+ export function coerceArray(value, split = true) {
97
+ if (value === undefined)
98
+ return [];
99
+ const arr = Array.isArray(value) ? value : [value];
100
+ if (!split)
101
+ return arr;
102
+ return arr.flatMap((v) => v.split(','));
103
+ }
104
+ /**
105
+ * Parse an integer flag. Returns undefined if the value is undefined/empty.
106
+ * Throws CliError if the value is not a valid integer.
107
+ */
108
+ export function parseIntFlag(value, flagName) {
109
+ if (value === undefined || value === null || value === '')
110
+ return undefined;
111
+ // All integer flags (limit/offset/retry) are non-negative; also rejects
112
+ // trailing garbage that parseInt would silently accept ("20abc" -> 20).
113
+ const trimmed = String(value).trim();
114
+ if (!/^\d+$/.test(trimmed)) {
115
+ throw new CliError(`Invalid integer value for --${flagName}: "${value}"`);
116
+ }
117
+ return Number.parseInt(trimmed, 10);
118
+ }
@@ -1,3 +1,4 @@
1
+ import type { AuthContext } from '../types/domain/auth.types.js';
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;
@@ -1,9 +1,5 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.fetchCompatibilityData = fetchCompatibilityData;
4
- exports.clearCompatibilityCache = clearCompatibilityCache;
5
1
  let cachedCompatibilityData = null;
6
- async function fetchCompatibilityData(apiUrl, apiKey) {
2
+ export async function fetchCompatibilityData(apiUrl, auth) {
7
3
  if (cachedCompatibilityData) {
8
4
  return cachedCompatibilityData;
9
5
  }
@@ -11,7 +7,7 @@ async function fetchCompatibilityData(apiUrl, apiKey) {
11
7
  const response = await fetch(`${apiUrl}/results/compatibility/data`, {
12
8
  headers: {
13
9
  'Content-Type': 'application/json',
14
- 'x-app-api-key': apiKey,
10
+ ...auth.headers,
15
11
  },
16
12
  method: 'GET',
17
13
  });
@@ -27,9 +23,11 @@ async function fetchCompatibilityData(apiUrl, apiKey) {
27
23
  }
28
24
  catch (error) {
29
25
  const errorMessage = error instanceof Error ? error.message : String(error);
30
- throw new Error(`Failed to fetch compatibility data from API: ${errorMessage}`);
26
+ throw new Error(`Failed to fetch compatibility data from API: ${errorMessage}`, {
27
+ cause: error,
28
+ });
31
29
  }
32
30
  }
33
- function clearCompatibilityCache() {
31
+ export function clearCompatibilityCache() {
34
32
  cachedCompatibilityData = null;
35
33
  }
@@ -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;