@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,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CONFIG_SCHEMA_VERSION = void 0;
4
+ exports.getConfigDir = getConfigDir;
5
+ exports.getConfigPath = getConfigPath;
6
+ exports.readConfig = readConfig;
7
+ exports.resolveApiUrl = resolveApiUrl;
8
+ exports.writeConfig = writeConfig;
9
+ exports.clearConfig = clearConfig;
10
+ exports.configFileMode = configFileMode;
11
+ /**
12
+ * Persistent CLI config: Supabase session tokens + chosen org.
13
+ *
14
+ * Storage: $XDG_CONFIG_HOME/dcd/config.json (fallback ~/.dcd/config.json).
15
+ * File mode 0600, directory mode 0700 — owner-only.
16
+ * Writes are atomic (tmp file + rename) so a crash mid-refresh can't
17
+ * leave a corrupt config behind that logs the user out.
18
+ */
19
+ const node_crypto_1 = require("node:crypto");
20
+ const node_fs_1 = require("node:fs");
21
+ const node_os_1 = require("node:os");
22
+ const path = require("node:path");
23
+ const environments_1 = require("../config/environments");
24
+ exports.CONFIG_SCHEMA_VERSION = 1;
25
+ function getConfigDir() {
26
+ if (process.env.DCD_CONFIG_DIR)
27
+ return process.env.DCD_CONFIG_DIR;
28
+ const xdg = process.env.XDG_CONFIG_HOME;
29
+ if (xdg && xdg.trim().length > 0)
30
+ return path.join(xdg, 'dcd');
31
+ return path.join((0, node_os_1.homedir)(), '.dcd');
32
+ }
33
+ function getConfigPath() {
34
+ return path.join(getConfigDir(), 'config.json');
35
+ }
36
+ function readConfig() {
37
+ const p = getConfigPath();
38
+ if (!(0, node_fs_1.existsSync)(p))
39
+ return null;
40
+ try {
41
+ const raw = (0, node_fs_1.readFileSync)(p, 'utf8');
42
+ const parsed = JSON.parse(raw);
43
+ if (parsed.version !== exports.CONFIG_SCHEMA_VERSION) {
44
+ // eslint-disable-next-line no-console
45
+ console.warn(`Warning: config at ${p} was written by an incompatible CLI version (config version ${parsed.version}); ignoring it. Run \`dcd login\` to recreate it.`);
46
+ return null;
47
+ }
48
+ return parsed;
49
+ }
50
+ catch {
51
+ // Surface the corruption instead of silently behaving as logged-out, so
52
+ // downstream "Not authenticated" errors aren't mystifying.
53
+ // eslint-disable-next-line no-console
54
+ console.warn(`Warning: could not parse config at ${p}; treating as logged out. Run \`dcd login\` to recreate it.`);
55
+ return null;
56
+ }
57
+ }
58
+ /**
59
+ * Resolve the API URL a command should talk to. Precedence:
60
+ * 1. explicit --api-url flag
61
+ * 2. api_url stored by `dcd login` (honors the env the user logged into)
62
+ * 3. prod default
63
+ *
64
+ * Without this, session commands default to prod and a dev/staging Bearer
65
+ * token is rejected with a misleading "Invalid or expired JWT". `switch-org`
66
+ * has always done this; this helper extends it to every command.
67
+ */
68
+ function resolveApiUrl(flag) {
69
+ const explicit = flag?.trim();
70
+ if (explicit)
71
+ return explicit;
72
+ return readConfig()?.api_url ?? environments_1.ENVIRONMENTS.prod.apiUrl;
73
+ }
74
+ function writeConfig(config) {
75
+ const dir = getConfigDir();
76
+ if (!(0, node_fs_1.existsSync)(dir)) {
77
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true, mode: 0o700 });
78
+ }
79
+ else {
80
+ try {
81
+ (0, node_fs_1.chmodSync)(dir, 0o700);
82
+ }
83
+ catch { /* best effort */ }
84
+ }
85
+ const finalPath = getConfigPath();
86
+ // Best-effort cleanup of orphaned tmp files left behind by crashed writes.
87
+ // Only remove old ones — a concurrent process may be between its own
88
+ // writeFileSync and renameSync right now.
89
+ try {
90
+ const base = path.basename(finalPath);
91
+ for (const entry of (0, node_fs_1.readdirSync)(dir)) {
92
+ if (!entry.startsWith(`${base}.`) || !entry.endsWith('.tmp'))
93
+ continue;
94
+ const tmp = path.join(dir, entry);
95
+ try {
96
+ if (Date.now() - (0, node_fs_1.statSync)(tmp).mtimeMs > 60_000)
97
+ (0, node_fs_1.unlinkSync)(tmp);
98
+ }
99
+ catch { /* best effort */ }
100
+ }
101
+ }
102
+ catch { /* best effort */ }
103
+ const tmpPath = `${finalPath}.${(0, node_crypto_1.randomBytes)(6).toString('hex')}.tmp`;
104
+ (0, node_fs_1.writeFileSync)(tmpPath, JSON.stringify(config, null, 2), { mode: 0o600 });
105
+ try {
106
+ (0, node_fs_1.chmodSync)(tmpPath, 0o600);
107
+ }
108
+ catch { /* best effort on platforms w/o chmod */ }
109
+ (0, node_fs_1.renameSync)(tmpPath, finalPath);
110
+ try {
111
+ (0, node_fs_1.chmodSync)(finalPath, 0o600);
112
+ }
113
+ catch { /* best effort */ }
114
+ }
115
+ function clearConfig() {
116
+ const p = getConfigPath();
117
+ if ((0, node_fs_1.existsSync)(p))
118
+ (0, node_fs_1.unlinkSync)(p);
119
+ }
120
+ function configFileMode() {
121
+ const p = getConfigPath();
122
+ if (!(0, node_fs_1.existsSync)(p))
123
+ return null;
124
+ return (0, node_fs_1.statSync)(p).mode & 0o777;
125
+ }
@@ -22,16 +22,15 @@ async function checkInternetConnectivity() {
22
22
  // Try each endpoint with a short timeout
23
23
  for (const { url, description } of testEndpoints) {
24
24
  const startTime = Date.now();
25
+ const controller = new AbortController();
26
+ const timeoutId = setTimeout(() => controller.abort(), 3000); // 3 second timeout
25
27
  try {
26
- const controller = new AbortController();
27
- const timeoutId = setTimeout(() => controller.abort(), 3000); // 3 second timeout
28
28
  const response = await fetch(url, {
29
29
  method: 'HEAD', // Use HEAD to minimize data transfer
30
30
  signal: controller.signal,
31
31
  // Disable redirects to get faster response
32
32
  redirect: 'manual',
33
33
  });
34
- clearTimeout(timeoutId);
35
34
  const latencyMs = Date.now() - startTime;
36
35
  // Any response (including 3xx redirects) indicates connectivity
37
36
  if (response) {
@@ -81,6 +80,11 @@ async function checkInternetConnectivity() {
81
80
  // Continue to next endpoint if this one fails
82
81
  continue;
83
82
  }
83
+ finally {
84
+ // Always clear — a dangling abort timer keeps the event loop alive
85
+ // for up to 3s per failed endpoint.
86
+ clearTimeout(timeoutId);
87
+ }
84
88
  }
85
89
  // Generate developer-friendly message
86
90
  let message;
@@ -40,10 +40,20 @@ async function downloadExpoUrl(url, debug) {
40
40
  try {
41
41
  const response = await fetch(url);
42
42
  if (!response.ok) {
43
+ // 4xx responses (expired signed URL, 404, ...) won't get better on
44
+ // retry — flag them so the catch below rethrows immediately.
45
+ const permanent = response.status >= 400 && response.status < 500;
46
+ let error;
43
47
  if (response.status === 403 || response.status === 401) {
44
- throw new Error(`Failed to download Expo build from URL (HTTP ${response.status}). Expo signed URLs expire after ~1 hour — please generate a fresh URL with 'eas build' and try again.`);
48
+ error = new Error(`Failed to download Expo build from URL (HTTP ${response.status}). Expo signed URLs expire after ~1 hour — please generate a fresh URL with 'eas build' and try again.`);
45
49
  }
46
- throw new Error(`Failed to download Expo build from URL (HTTP ${response.status}).`);
50
+ else {
51
+ error = new Error(`Failed to download Expo build from URL (HTTP ${response.status}).`);
52
+ }
53
+ if (permanent) {
54
+ error.permanent = true;
55
+ }
56
+ throw error;
47
57
  }
48
58
  if (!response.body) {
49
59
  throw new Error('No response body received from Expo URL.');
@@ -60,8 +70,9 @@ async function downloadExpoUrl(url, debug) {
60
70
  catch (error) {
61
71
  // Clean up any partial file before retrying
62
72
  await fsp.rm(destPath, { force: true }).catch(() => { });
73
+ const isPermanent = Boolean(error?.permanent);
63
74
  const isLastAttempt = attempt === DOWNLOAD_RETRY_ATTEMPTS;
64
- if (isLastAttempt) {
75
+ if (isPermanent || isLastAttempt) {
65
76
  throw error;
66
77
  }
67
78
  if (debug) {
@@ -0,0 +1,11 @@
1
+ export interface OrgListItem {
2
+ id: string;
3
+ name: string;
4
+ slug?: string;
5
+ }
6
+ export declare function fetchOrgs(apiUrl: string, headers: Record<string, string>): Promise<OrgListItem[]>;
7
+ /**
8
+ * Interactive org picker. Auto-selects when there's only one org (returns it
9
+ * without prompting). Throws on zero orgs or user cancellation.
10
+ */
11
+ export declare function pickOrg(orgs: OrgListItem[], message?: string): Promise<OrgListItem>;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fetchOrgs = fetchOrgs;
4
+ exports.pickOrg = pickOrg;
5
+ /**
6
+ * Shared helpers for fetching and picking from /me/orgs. Used by both
7
+ * `dcd login` (after the PKCE claim returns a session) and `dcd switch-org`.
8
+ */
9
+ const p = require("@clack/prompts");
10
+ const cli_1 = require("./cli");
11
+ async function fetchOrgs(apiUrl, headers) {
12
+ const res = await fetch(`${apiUrl.replace(/\/$/, '')}/me/orgs`, { headers });
13
+ if (!res.ok) {
14
+ const body = await res.text().catch(() => '');
15
+ throw new cli_1.CliError(`Failed to list organizations: HTTP ${res.status}${body ? ` — ${body}` : ''}`);
16
+ }
17
+ const body = (await res.json());
18
+ return body.orgs ?? [];
19
+ }
20
+ /**
21
+ * Interactive org picker. Auto-selects when there's only one org (returns it
22
+ * without prompting). Throws on zero orgs or user cancellation.
23
+ */
24
+ async function pickOrg(orgs, message = 'Pick an organization') {
25
+ if (orgs.length === 0) {
26
+ throw new cli_1.CliError('No organizations found for this user.');
27
+ }
28
+ if (orgs.length === 1)
29
+ return orgs[0];
30
+ const picked = await p.select({
31
+ message,
32
+ options: orgs.map((o) => ({ value: o.id, label: o.name })),
33
+ });
34
+ if (p.isCancel(picked))
35
+ throw new cli_1.CliError('Cancelled.');
36
+ const chosen = orgs.find((o) => o.id === picked);
37
+ if (!chosen)
38
+ throw new cli_1.CliError('No organization selected.');
39
+ return chosen;
40
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Convert an absolute path into the portable './'-prefixed, forward-slash
3
+ * relative form used as flow keys across submission, metadata maps, and
4
+ * polling. `commonRoot` must be a whole-segment prefix of `absolutePath`
5
+ * without a trailing separator (or '' when no common root exists, in which
6
+ * case the path is kept whole apart from separator normalization).
7
+ *
8
+ * Replaces the old `replaceAll(commonRoot, '.')` pattern, which corrupted
9
+ * paths when the root substring recurred mid-path or collapsed to ''.
10
+ */
11
+ export declare function toPortableRelativePath(absolutePath: string, commonRoot: string): string;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toPortableRelativePath = toPortableRelativePath;
4
+ const path = require("node:path");
5
+ /**
6
+ * Convert an absolute path into the portable './'-prefixed, forward-slash
7
+ * relative form used as flow keys across submission, metadata maps, and
8
+ * polling. `commonRoot` must be a whole-segment prefix of `absolutePath`
9
+ * without a trailing separator (or '' when no common root exists, in which
10
+ * case the path is kept whole apart from separator normalization).
11
+ *
12
+ * Replaces the old `replaceAll(commonRoot, '.')` pattern, which corrupted
13
+ * paths when the root substring recurred mid-path or collapsed to ''.
14
+ */
15
+ function toPortableRelativePath(absolutePath, commonRoot) {
16
+ let relative = absolutePath;
17
+ if (commonRoot && absolutePath.startsWith(commonRoot)) {
18
+ relative = absolutePath.slice(commonRoot.length);
19
+ }
20
+ if (!relative.startsWith(path.sep)) {
21
+ relative = path.sep + relative;
22
+ }
23
+ return ('.' + relative).split(path.sep).join('/');
24
+ }
@@ -0,0 +1,13 @@
1
+ declare class Action {
2
+ private current;
3
+ private _status;
4
+ start(title: string, initialStatus?: string, _opts?: unknown): void;
5
+ stop(message?: string): void;
6
+ set status(value: string);
7
+ get status(): string;
8
+ }
9
+ export declare const ux: {
10
+ action: Action;
11
+ info(message: string): void;
12
+ };
13
+ export {};
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ux = void 0;
4
+ /**
5
+ * Progress adapter that wraps @clack/prompts spinner with a
6
+ * drop-in API for existing services that used oclif's `ux.action` / `ux.info`.
7
+ *
8
+ * Keeps call sites unchanged while migrating away from @oclif/core.
9
+ */
10
+ const p = require("@clack/prompts");
11
+ class Action {
12
+ current = null;
13
+ _status = '';
14
+ start(title, initialStatus, _opts) {
15
+ if (this.current) {
16
+ this.current.stop();
17
+ }
18
+ this.current = p.spinner();
19
+ this._status = initialStatus ?? '';
20
+ this.current.start(initialStatus ? `${title} — ${initialStatus}` : title);
21
+ }
22
+ stop(message) {
23
+ if (!this.current) {
24
+ if (message) {
25
+ // eslint-disable-next-line no-console
26
+ console.log(message);
27
+ }
28
+ return;
29
+ }
30
+ this.current.stop(message ?? '');
31
+ this.current = null;
32
+ this._status = '';
33
+ }
34
+ set status(value) {
35
+ this._status = value;
36
+ if (this.current && value) {
37
+ this.current.message(value);
38
+ }
39
+ }
40
+ get status() {
41
+ return this._status;
42
+ }
43
+ }
44
+ exports.ux = {
45
+ action: new Action(),
46
+ info(message) {
47
+ // eslint-disable-next-line no-console
48
+ console.log(message);
49
+ },
50
+ };
@@ -1,8 +1,4 @@
1
1
  import chalk = require('chalk');
2
- /**
3
- * Centralized styling utilities for CLI output
4
- * Provides consistent, developer-friendly visual formatting
5
- */
6
2
  /**
7
3
  * Status symbols with associated colors
8
4
  */
@@ -97,9 +93,21 @@ export declare function formatTestSummary(summary: {
97
93
  * @returns Formatted box with borders
98
94
  */
99
95
  export declare function box(content: string): string;
96
+ /**
97
+ * Minimal column table renderer.
98
+ * Columns are defined with a `get(row) => string` accessor and optional header label.
99
+ * Matches the subset of oclif's ux.table used by this CLI.
100
+ */
101
+ export declare function table<T>(rows: T[], columns: Record<string, {
102
+ get: (row: T) => string;
103
+ header?: string;
104
+ }>, options?: {
105
+ printLine?: (line: string) => void;
106
+ }): void;
100
107
  /**
101
108
  * Generate console URL based on API URL
102
- * If a non-default API URL is used, prepends "dev." to the console subdomain
109
+ * Derives the console host from the known environment matching the API URL;
110
+ * unknown API URLs fall back to the dev console (historical behavior).
103
111
  * @param apiUrl - The API URL being used
104
112
  * @param uploadId - The upload ID
105
113
  * @param resultId - The result ID
@@ -9,12 +9,17 @@ exports.formatUrl = formatUrl;
9
9
  exports.formatId = formatId;
10
10
  exports.formatTestSummary = formatTestSummary;
11
11
  exports.box = box;
12
+ exports.table = table;
12
13
  exports.getConsoleUrl = getConsoleUrl;
13
14
  const chalk = require("chalk");
15
+ const environments_1 = require("../config/environments");
14
16
  /**
15
17
  * Centralized styling utilities for CLI output
16
18
  * Provides consistent, developer-friendly visual formatting
17
19
  */
20
+ /** Strip ANSI color escape sequences for visible-width calculations. */
21
+ // eslint-disable-next-line no-control-regex -- matches ANSI escape sequences
22
+ const stripAnsi = (s) => s.replace(/\u001B\[[0-9;]*m/g, '');
18
23
  /**
19
24
  * Status symbols with associated colors
20
25
  */
@@ -147,25 +152,50 @@ function formatTestSummary(summary) {
147
152
  */
148
153
  function box(content) {
149
154
  const lines = content.split('\n');
150
- const maxLength = Math.max(...lines.map((l) => l.length));
155
+ const visibleLen = (s) => stripAnsi(s).length;
156
+ const maxLength = Math.max(...lines.map((l) => visibleLen(l)));
151
157
  const top = chalk.gray('┌' + '─'.repeat(maxLength + 2) + '┐');
152
158
  const bottom = chalk.gray('└' + '─'.repeat(maxLength + 2) + '┘');
153
159
  const middle = lines
154
- .map((line) => chalk.gray('│ ') + line.padEnd(maxLength) + chalk.gray(' │'))
160
+ .map((line) => chalk.gray('│ ') + line + ' '.repeat(Math.max(0, maxLength - visibleLen(line))) + chalk.gray(' │'))
155
161
  .join('\n');
156
162
  return `${top}\n${middle}\n${bottom}`;
157
163
  }
164
+ /**
165
+ * Minimal column table renderer.
166
+ * Columns are defined with a `get(row) => string` accessor and optional header label.
167
+ * Matches the subset of oclif's ux.table used by this CLI.
168
+ */
169
+ function table(rows, columns, options = {}) {
170
+ const printLine = options.printLine ?? ((line) => {
171
+ // eslint-disable-next-line no-console
172
+ console.log(line);
173
+ });
174
+ const keys = Object.keys(columns);
175
+ const headers = keys.map((k) => columns[k].header ?? k);
176
+ const cells = rows.map((row) => keys.map((k) => String(columns[k].get(row) ?? '')));
177
+ const widths = keys.map((_, i) => Math.max(stripAnsi(headers[i]).length, ...cells.map((r) => stripAnsi(r[i]).length)));
178
+ const pad = (s, width) => {
179
+ const visibleLen = stripAnsi(s).length;
180
+ return s + ' '.repeat(Math.max(0, width - visibleLen));
181
+ };
182
+ printLine(headers.map((h, i) => pad(chalk.bold(h), widths[i])).join(' '));
183
+ printLine(widths.map((w) => chalk.gray('─'.repeat(w))).join(' '));
184
+ for (const row of cells) {
185
+ printLine(row.map((c, i) => pad(c, widths[i])).join(' '));
186
+ }
187
+ }
158
188
  /**
159
189
  * Generate console URL based on API URL
160
- * If a non-default API URL is used, prepends "dev." to the console subdomain
190
+ * Derives the console host from the known environment matching the API URL;
191
+ * unknown API URLs fall back to the dev console (historical behavior).
161
192
  * @param apiUrl - The API URL being used
162
193
  * @param uploadId - The upload ID
163
194
  * @param resultId - The result ID
164
195
  * @returns The appropriate console URL
165
196
  */
166
197
  function getConsoleUrl(apiUrl, uploadId, resultId) {
167
- const DEFAULT_API_URL = 'https://api.devicecloud.dev';
168
- const isDefaultApi = apiUrl === DEFAULT_API_URL;
169
- const consoleSubdomain = isDefaultApi ? 'console' : 'dev.console';
170
- return `https://${consoleSubdomain}.devicecloud.dev/results?upload=${uploadId}&result=${resultId}`;
198
+ const env = (0, environments_1.findEnvByApiUrl)(apiUrl);
199
+ const base = env?.frontendUrl ?? 'https://dev.console.devicecloud.dev';
200
+ return `${base}/results?upload=${uploadId}&result=${resultId}`;
171
201
  }
package/package.json CHANGED
@@ -1,66 +1,54 @@
1
1
  {
2
2
  "author": "devicecloud.dev",
3
3
  "bin": {
4
- "dcd": "bin/run.js"
4
+ "dcd": "dist/index.js"
5
5
  },
6
6
  "dependencies": {
7
- "@oclif/core": "^3.27.0",
8
- "@oclif/plugin-help": "^6.2.37",
7
+ "@clack/prompts": "^1.2.0",
9
8
  "@supabase/supabase-js": "^2.99.1",
10
- "app-info-parser": "^1.1.6",
11
- "archiver": "^7.0.1",
12
9
  "bplist-parser": "^0.3.2",
13
10
  "chalk": "4.1.2",
14
- "glob": "^13.0.6",
11
+ "citty": "^0.1.6",
15
12
  "js-yaml": "^4.1.1",
13
+ "node-apk": "^1.2.1",
16
14
  "node-stream-zip": "^1.15.0",
17
15
  "plist": "^3.1.0",
18
16
  "tar": "^7.5.11",
19
- "tus-js-client": "^4.3.1"
17
+ "tus-js-client": "^4.3.1",
18
+ "yazl": "^3.3.1"
20
19
  },
21
20
  "description": "Better cloud maestro testing",
22
21
  "devDependencies": {
23
- "@oclif/prettier-config": "^0.2.1",
24
- "@oclif/test": "^4.1.16",
25
- "@types/archiver": "^7.0.0",
26
- "@types/chai": "^5.2.3",
27
- "@types/glob-to-regexp": "^0.4.4",
22
+ "@eslint/js": "^9.39.4",
23
+ "@types/chai": "^4.3.20",
28
24
  "@types/js-yaml": "^4.0.9",
29
25
  "@types/mocha": "^10.0.10",
30
26
  "@types/node": "^25.4.0",
31
27
  "@types/plist": "^3.0.5",
32
- "@types/tar": "^7.0.87",
33
- "chai": "^6.2.2",
34
- "eslint": "^8.57.1",
35
- "eslint-config-oclif": "^5.2.2",
36
- "eslint-config-oclif-typescript": "^3.1.14",
28
+ "@types/yazl": "^3.3.1",
29
+ "chai": "^4.5.0",
30
+ "eslint": "^9.16.0",
37
31
  "eslint-config-prettier": "^10.1.8",
32
+ "eslint-plugin-import": "^2.32.0",
33
+ "eslint-plugin-unicorn": "^64.0.0",
34
+ "husky": "^9.1.7",
38
35
  "mocha": "^11.7.5",
39
- "oclif": "^4.22.96",
36
+ "prettier": "^3.3.3",
40
37
  "shx": "^0.4.0",
41
- "ts-node": "^10.9.2",
42
- "typescript": "^5.9.3"
38
+ "tsx": "^4.19.2",
39
+ "typescript": "^5.9.3",
40
+ "typescript-eslint": "^8.59.0"
43
41
  },
44
42
  "engines": {
45
43
  "node": ">=22.0.0"
46
44
  },
47
45
  "files": [
48
- "/bin",
49
- "/dist",
50
- "/oclif.manifest.json"
46
+ "/dist"
51
47
  ],
52
48
  "homepage": "https://devicecloud.dev",
53
49
  "license": "MIT",
54
50
  "main": "dist/index.js",
55
51
  "name": "@devicecloud.dev/dcd",
56
- "oclif": {
57
- "bin": "dcd",
58
- "dirname": "dcd",
59
- "commands": "./dist/commands",
60
- "plugins": [
61
- "@oclif/plugin-help"
62
- ]
63
- },
64
52
  "private": false,
65
53
  "publishConfig": {
66
54
  "access": "public"
@@ -69,7 +57,7 @@
69
57
  "type": "git",
70
58
  "url": "https://devicecloud.dev"
71
59
  },
72
- "version": "4.4.9",
60
+ "version": "5.0.0-beta.0",
73
61
  "bugs": {
74
62
  "url": "https://discord.gg/gm3mJwcNw8"
75
63
  },
@@ -83,11 +71,11 @@
83
71
  ],
84
72
  "types": "dist/index.d.ts",
85
73
  "scripts": {
86
- "dcd": "./bin/dev.js",
87
- "prod": "./bin/run.js",
88
- "build": "shx rm -rf dist && tsc -b",
89
- "lint": "eslint . --ext .ts",
90
- "version": "oclif readme && git add README.md",
91
- "test": "node scripts/test-runner.mjs"
74
+ "dcd": "tsx src/index.ts",
75
+ "build": "shx rm -rf dist && tsc -b && shx chmod +x dist/index.js",
76
+ "build:binaries": "node scripts/build-binaries.mjs",
77
+ "lint": "eslint src test --ext .ts",
78
+ "test": "node scripts/test-runner.mjs",
79
+ "typecheck": "tsc --noEmit -p tsconfig.test.json"
92
80
  }
93
81
  }
package/bin/dev.cmd DELETED
@@ -1,3 +0,0 @@
1
- @echo off
2
-
3
- node "%~dp0\dev" %*
package/bin/dev.js DELETED
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env node_modules/.bin/ts-node
2
- // eslint-disable-next-line node/shebang, unicorn/prefer-top-level-await
3
- ;(async () => {
4
- const oclif = await import('@oclif/core')
5
- await oclif.execute({development: true, dir: __dirname})
6
- })()
package/bin/run.cmd DELETED
@@ -1,3 +0,0 @@
1
- @echo off
2
-
3
- node "%~dp0\run" %*
package/bin/run.js DELETED
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // eslint-disable-next-line unicorn/prefer-top-level-await
4
- (async () => {
5
- const oclif = await import('@oclif/core');
6
- await oclif.execute({ development: false, dir: __dirname });
7
- })();