@devicecloud.dev/dcd 4.4.9 → 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 -288
  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
@@ -0,0 +1,250 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loginCommand = void 0;
4
+ /**
5
+ * `dcd login` — delegates auth to the web frontend and persists the resulting
6
+ * Supabase session locally.
7
+ *
8
+ * Flow (PKCE S256, no loopback):
9
+ * 1. CLI mints:
10
+ * - `state`: 32 random bytes, hex
11
+ * - `code_verifier`:32 random bytes, base64url
12
+ * - `code_challenge` = base64url(sha256(code_verifier))
13
+ * 2. CLI opens <frontend>/cli-login?state=...&code_challenge=... in the browser.
14
+ * 3. User signs in (OTP or SSO) and explicitly authorizes the handoff on
15
+ * the frontend.
16
+ * 4. Frontend POSTs {state, code_challenge, session...} to the dcd api at
17
+ * POST /cli-login/handoff. The api stores a short-TTL row keyed by state.
18
+ * 5. Meanwhile, the CLI polls POST /cli-login/claim with {state, code_verifier}.
19
+ * Once the api has the row, it verifies sha256(verifier) === challenge,
20
+ * deletes the row, and returns the session.
21
+ * 6. CLI fetches /me/orgs with the fresh Bearer token and prompts the user to
22
+ * pick an org. Org selection lives here, not in the web UI — that way the
23
+ * CLI login and `dcd switch-org` share one picker.
24
+ *
25
+ * Why PKCE + server rendezvous (instead of a localhost loopback): Safari
26
+ * blocks HTTPS → HTTP fetches to 127.0.0.1 as mixed content, and a loopback
27
+ * flow can't work at all over SSH / "open this URL on your phone". The
28
+ * rendezvous endpoint makes both cases trivial. A leaked URL is inert: it
29
+ * carries the challenge, not the verifier.
30
+ */
31
+ const p = require("@clack/prompts");
32
+ const citty_1 = require("citty");
33
+ const node_child_process_1 = require("node:child_process");
34
+ const node_crypto_1 = require("node:crypto");
35
+ const environments_1 = require("../config/environments");
36
+ const cli_1 = require("../utils/cli");
37
+ const config_store_1 = require("../utils/config-store");
38
+ const orgs_1 = require("../utils/orgs");
39
+ const styling_1 = require("../utils/styling");
40
+ const LOGIN_TIMEOUT_MS = 10 * 60 * 1000;
41
+ // 1s feels snappy after the user clicks "Authorize" in the browser. The api
42
+ // rate-limits /cli-login/claim at 60/min which is exactly this cadence.
43
+ const POLL_INTERVAL_MS = 1000;
44
+ exports.loginCommand = (0, citty_1.defineCommand)({
45
+ meta: {
46
+ name: 'login',
47
+ description: 'Authenticate with devicecloud.dev via your browser',
48
+ },
49
+ args: {
50
+ 'api-url': {
51
+ type: 'string',
52
+ default: 'https://api.devicecloud.dev',
53
+ description: 'API base URL',
54
+ },
55
+ 'frontend-url': {
56
+ type: 'string',
57
+ description: 'Override the frontend URL used to complete login (defaults per env)',
58
+ },
59
+ // citty treats `--no-<flag>` as negating `<flag>`, so the flag has to be
60
+ // named `browser` (defaulting true) for `--no-browser` to set it false.
61
+ browser: {
62
+ type: 'boolean',
63
+ default: true,
64
+ description: 'Open the login URL in a browser (pass --no-browser to just print it)',
65
+ },
66
+ },
67
+ async run({ args }) {
68
+ const apiUrl = args['api-url'].replace(/\/$/, '');
69
+ const frontendOverride = args['frontend-url'];
70
+ const noBrowser = args.browser === false;
71
+ // If there's an existing stored session, make the user confirm before we
72
+ // overwrite it. Silent clobber is fine for power users but surprising if
73
+ // someone runs `dcd login` by mistake while already authenticated.
74
+ const existing = (0, config_store_1.readConfig)();
75
+ if (existing?.session) {
76
+ const currentOrg = existing.current_org_name ?? existing.current_org_id;
77
+ const ok = await p.confirm({
78
+ message: `Already logged in as ${existing.session.user_email}` +
79
+ (currentOrg ? ` (org ${currentOrg})` : '') +
80
+ `. Sign out and log in again?`,
81
+ initialValue: false,
82
+ });
83
+ if (p.isCancel(ok) || !ok) {
84
+ cli_1.logger.log(`${styling_1.symbols.info} Keeping existing session.`);
85
+ return;
86
+ }
87
+ }
88
+ const env = (0, environments_1.inferEnvFromApiUrl)(apiUrl);
89
+ const SUPABASE_URL = environments_1.ENVIRONMENTS[env].supabase.url;
90
+ const frontendUrl = (frontendOverride ?? (0, environments_1.resolveFrontendUrl)(apiUrl)).replace(/\/$/, '');
91
+ const state = (0, node_crypto_1.randomBytes)(32).toString('hex');
92
+ const codeVerifier = base64url((0, node_crypto_1.randomBytes)(32));
93
+ const codeChallenge = base64url((0, node_crypto_1.createHash)('sha256').update(codeVerifier, 'ascii').digest());
94
+ const loginUrl = `${frontendUrl}/cli-login?state=${state}&code_challenge=${codeChallenge}`;
95
+ cli_1.logger.log((0, styling_1.sectionHeader)('Signing in to devicecloud.dev'));
96
+ if (noBrowser) {
97
+ cli_1.logger.log(` ${styling_1.colors.dim('Open this URL in a browser to finish login:')}`);
98
+ cli_1.logger.log(` ${styling_1.colors.highlight(loginUrl)}`);
99
+ }
100
+ else {
101
+ cli_1.logger.log(` ${styling_1.colors.dim('Opening your browser...')}`);
102
+ const opened = openBrowser(loginUrl);
103
+ if (!opened) {
104
+ cli_1.logger.log(` ${styling_1.colors.dim('Could not launch a browser. Open this URL manually:')}`);
105
+ cli_1.logger.log(` ${styling_1.colors.highlight(loginUrl)}`);
106
+ }
107
+ }
108
+ cli_1.logger.log(` ${styling_1.colors.dim('Waiting for login to complete...')}\n`);
109
+ try {
110
+ const payload = await pollForClaim(apiUrl, state, codeVerifier);
111
+ if (!payload.access_token || typeof payload.access_token !== 'string') {
112
+ throw new cli_1.CliError('Claim response did not include an access_token.');
113
+ }
114
+ // Fetch orgs with the just-minted Bearer token. We don't write the
115
+ // config until the user has picked an org — half-completed state isn't
116
+ // useful and the handoff row is already single-use consumed.
117
+ const bearerHeaders = {
118
+ authorization: `Bearer ${payload.access_token}`,
119
+ };
120
+ let orgs;
121
+ try {
122
+ orgs = await (0, orgs_1.fetchOrgs)(apiUrl, bearerHeaders);
123
+ }
124
+ catch (err) {
125
+ // The api rejected the JWT. Before blaming the api, hit Supabase's
126
+ // own /auth/v1/user directly with the same token — if Supabase also
127
+ // rejects it, the token itself is bad; if Supabase accepts, the
128
+ // api's JwtService is misconfigured.
129
+ const iss = decodeJwtIssuer(payload.access_token);
130
+ const supabaseUser = await probeSupabaseUser(iss, environments_1.ENVIRONMENTS[env].supabase.anonKey, payload.access_token);
131
+ const parts = [
132
+ iss ? `token iss: ${iss}` : null,
133
+ `supabase /auth/v1/user: ${supabaseUser}`,
134
+ ].filter(Boolean);
135
+ throw new cli_1.CliError(`${err.message} [${parts.join(' | ')}]`);
136
+ }
137
+ const chosen = await (0, orgs_1.pickOrg)(orgs);
138
+ (0, config_store_1.writeConfig)({
139
+ version: 1,
140
+ env,
141
+ api_url: apiUrl,
142
+ supabase_url: SUPABASE_URL,
143
+ session: {
144
+ access_token: payload.access_token,
145
+ refresh_token: payload.refresh_token,
146
+ expires_at: payload.expires_at,
147
+ user_email: payload.user_email,
148
+ user_id: payload.user_id,
149
+ },
150
+ current_org_id: chosen.id,
151
+ current_org_name: chosen.name,
152
+ });
153
+ cli_1.logger.log(`${styling_1.symbols.success} ${styling_1.colors.bold('Logged in')} as ${styling_1.colors.highlight(payload.user_email)}`);
154
+ cli_1.logger.log(` ${styling_1.colors.dim('Organization:')} ${styling_1.colors.highlight(chosen.name)}`);
155
+ if (orgs.length > 1) {
156
+ cli_1.logger.log(` ${styling_1.colors.dim('Switch orgs later with')} ${styling_1.colors.highlight('dcd switch-org')}`);
157
+ }
158
+ }
159
+ catch (error) {
160
+ cli_1.logger.error(error, { exit: 1 });
161
+ }
162
+ },
163
+ });
164
+ function openBrowser(url) {
165
+ const cmd = process.platform === 'darwin'
166
+ ? { bin: 'open', args: [url] }
167
+ : process.platform === 'win32'
168
+ ? { bin: 'cmd', args: ['/c', 'start', '""', url] }
169
+ : { bin: 'xdg-open', args: [url] };
170
+ try {
171
+ const child = (0, node_child_process_1.spawn)(cmd.bin, cmd.args, { detached: true, stdio: 'ignore' });
172
+ child.unref();
173
+ return true;
174
+ }
175
+ catch {
176
+ return false;
177
+ }
178
+ }
179
+ async function pollForClaim(apiUrl, state, codeVerifier) {
180
+ const claimUrl = `${apiUrl}/cli-login/claim`;
181
+ const deadline = Date.now() + LOGIN_TIMEOUT_MS;
182
+ while (Date.now() < deadline) {
183
+ let res;
184
+ try {
185
+ res = await fetch(claimUrl, {
186
+ method: 'POST',
187
+ headers: { 'content-type': 'application/json' },
188
+ body: JSON.stringify({ state, code_verifier: codeVerifier }),
189
+ });
190
+ }
191
+ catch {
192
+ // Network blip — don't kill the login, just back off and try again.
193
+ await sleep(POLL_INTERVAL_MS);
194
+ continue;
195
+ }
196
+ if (res.ok) {
197
+ return (await res.json());
198
+ }
199
+ if (res.status === 404) {
200
+ // Not yet handed off (or already claimed elsewhere, or expired).
201
+ await sleep(POLL_INTERVAL_MS);
202
+ continue;
203
+ }
204
+ if (res.status === 429) {
205
+ // Polled too fast — back off more aggressively before retrying.
206
+ await sleep(POLL_INTERVAL_MS * 3);
207
+ continue;
208
+ }
209
+ const body = await res.text().catch(() => '');
210
+ throw new cli_1.CliError(`Login failed: ${res.status} ${res.statusText}${body ? ` — ${body}` : ''}`);
211
+ }
212
+ throw new cli_1.CliError('Login timed out. Please run `dcd login` again.');
213
+ }
214
+ function base64url(buf) {
215
+ return buf.toString('base64').replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
216
+ }
217
+ async function probeSupabaseUser(iss, anonKey, token) {
218
+ if (!iss)
219
+ return 'skipped (no iss)';
220
+ try {
221
+ const res = await fetch(`${iss}/user`, {
222
+ headers: {
223
+ authorization: `Bearer ${token}`,
224
+ apikey: anonKey,
225
+ },
226
+ });
227
+ const bodyText = await res.text().catch(() => '');
228
+ return `HTTP ${res.status}${bodyText ? ` — ${bodyText.slice(0, 200)}` : ''}`;
229
+ }
230
+ catch (err) {
231
+ return `network error: ${err.message}`;
232
+ }
233
+ }
234
+ function decodeJwtIssuer(token) {
235
+ try {
236
+ const [, payloadB64] = token.split('.');
237
+ if (!payloadB64)
238
+ return null;
239
+ const json = Buffer.from(payloadB64.replaceAll('-', '+').replaceAll('_', '/'), 'base64').toString('utf8');
240
+ const parsed = JSON.parse(json);
241
+ return parsed.iss ?? null;
242
+ }
243
+ catch {
244
+ return null;
245
+ }
246
+ }
247
+ function sleep(ms) {
248
+ return new Promise((resolve) => setTimeout(resolve, ms));
249
+ }
250
+ exports.default = exports.loginCommand;
@@ -0,0 +1,2 @@
1
+ export declare const logoutCommand: import("citty").CommandDef<import("citty").ArgsDef>;
2
+ export default logoutCommand;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.logoutCommand = void 0;
4
+ /**
5
+ * `dcd logout` — clears the stored Supabase session and best-effort revokes
6
+ * it on Supabase. Leaves DEVICE_CLOUD_API_KEY (env/flag) untouched.
7
+ */
8
+ const citty_1 = require("citty");
9
+ const environments_1 = require("../config/environments");
10
+ const cli_auth_gateway_1 = require("../gateways/cli-auth-gateway");
11
+ const cli_1 = require("../utils/cli");
12
+ const config_store_1 = require("../utils/config-store");
13
+ const styling_1 = require("../utils/styling");
14
+ exports.logoutCommand = (0, citty_1.defineCommand)({
15
+ meta: {
16
+ name: 'logout',
17
+ description: 'Clear the stored devicecloud.dev session',
18
+ },
19
+ async run() {
20
+ const config = (0, config_store_1.readConfig)();
21
+ if (!config?.session) {
22
+ cli_1.logger.log(`${styling_1.symbols.info} No active session found (${styling_1.colors.dim((0, config_store_1.getConfigPath)())}).`);
23
+ (0, config_store_1.clearConfig)();
24
+ return;
25
+ }
26
+ const { anonKey } = environments_1.ENVIRONMENTS[config.env].supabase;
27
+ await cli_auth_gateway_1.CliAuthGateway.signOut(config.supabase_url, anonKey, config.session);
28
+ (0, config_store_1.clearConfig)();
29
+ cli_1.logger.log(`${styling_1.symbols.success} Logged out ${styling_1.colors.dim(`(${config.session.user_email})`)}.`);
30
+ },
31
+ });
32
+ exports.default = exports.logoutCommand;
@@ -1,44 +1,25 @@
1
- import { Command } from '@oclif/core';
2
- import { ConnectivityCheckResult } from '../utils/connectivity';
3
- type StatusResponse = {
4
- appBinaryId?: string;
5
- attempts?: number;
6
- connectivityCheck?: {
7
- connected: boolean;
8
- endpointResults: ConnectivityCheckResult['endpointResults'];
9
- message: string;
1
+ export declare const statusCommand: import("citty").CommandDef<{
2
+ json: {
3
+ type: "boolean";
4
+ description: string;
10
5
  };
11
- consoleUrl?: string;
12
- createdAt?: string;
13
- error?: string;
14
- name?: string;
15
- status: 'CANCELLED' | 'FAILED' | 'PASSED' | 'PENDING' | 'QUEUED' | 'RUNNING';
16
- tests: {
17
- createdAt?: string;
18
- durationSeconds?: number;
19
- failReason?: string;
20
- name: string;
21
- status: 'CANCELLED' | 'FAILED' | 'PASSED' | 'PENDING' | 'QUEUED' | 'RUNNING';
22
- }[];
23
- uploadId?: string;
24
- };
25
- export default class Status extends Command {
26
- static description: string;
27
- static enableJsonFlag: boolean;
28
- static examples: string[];
29
- static flags: {
30
- apiKey: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
31
- apiUrl: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
32
- json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
33
- name: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
34
- 'upload-id': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
6
+ name: {
7
+ type: "string";
8
+ description: string;
35
9
  };
36
- run(): Promise<StatusResponse | void>;
37
- /**
38
- * Format an ISO date string to a human-readable local date/time
39
- * @param isoString - ISO 8601 date string
40
- * @returns Formatted local date/time string
41
- */
42
- private formatDateTime;
43
- }
44
- export {};
10
+ 'upload-id': {
11
+ type: "string";
12
+ description: string;
13
+ };
14
+ 'api-key': {
15
+ readonly type: "string";
16
+ readonly alias: ["apiKey"];
17
+ readonly description: "API key for devicecloud.dev (find this in the console UI). You can also set the DEVICE_CLOUD_API_KEY environment variable.";
18
+ };
19
+ 'api-url': {
20
+ readonly type: "string";
21
+ readonly alias: ["apiURL", "apiUrl"];
22
+ readonly description: "API base URL (defaults to the URL stored by `dcd login`, else prod)";
23
+ };
24
+ }>;
25
+ export default statusCommand;