@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,115 @@
1
+ /**
2
+ * Persistent CLI config: Supabase session tokens + chosen org.
3
+ *
4
+ * Storage: $XDG_CONFIG_HOME/dcd/config.json (fallback ~/.dcd/config.json).
5
+ * File mode 0600, directory mode 0700 — owner-only.
6
+ * Writes are atomic (tmp file + rename) so a crash mid-refresh can't
7
+ * leave a corrupt config behind that logs the user out.
8
+ */
9
+ import { randomBytes } from 'node:crypto';
10
+ import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
11
+ import { homedir } from 'node:os';
12
+ import * as path from 'node:path';
13
+ import { ENVIRONMENTS } from '../config/environments.js';
14
+ export const CONFIG_SCHEMA_VERSION = 1;
15
+ export function getConfigDir() {
16
+ if (process.env.DCD_CONFIG_DIR)
17
+ return process.env.DCD_CONFIG_DIR;
18
+ const xdg = process.env.XDG_CONFIG_HOME;
19
+ if (xdg && xdg.trim().length > 0)
20
+ return path.join(xdg, 'dcd');
21
+ return path.join(homedir(), '.dcd');
22
+ }
23
+ export function getConfigPath() {
24
+ return path.join(getConfigDir(), 'config.json');
25
+ }
26
+ export function readConfig() {
27
+ const p = getConfigPath();
28
+ if (!existsSync(p))
29
+ return null;
30
+ try {
31
+ const raw = readFileSync(p, 'utf8');
32
+ const parsed = JSON.parse(raw);
33
+ if (parsed.version !== CONFIG_SCHEMA_VERSION) {
34
+ // eslint-disable-next-line no-console
35
+ 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.`);
36
+ return null;
37
+ }
38
+ return parsed;
39
+ }
40
+ catch {
41
+ // Surface the corruption instead of silently behaving as logged-out, so
42
+ // downstream "Not authenticated" errors aren't mystifying.
43
+ // eslint-disable-next-line no-console
44
+ console.warn(`Warning: could not parse config at ${p}; treating as logged out. Run \`dcd login\` to recreate it.`);
45
+ return null;
46
+ }
47
+ }
48
+ /**
49
+ * Resolve the API URL a command should talk to. Precedence:
50
+ * 1. explicit --api-url flag
51
+ * 2. api_url stored by `dcd login` (honors the env the user logged into)
52
+ * 3. prod default
53
+ *
54
+ * Without this, session commands default to prod and a dev/staging Bearer
55
+ * token is rejected with a misleading "Invalid or expired JWT". `switch-org`
56
+ * has always done this; this helper extends it to every command.
57
+ */
58
+ export function resolveApiUrl(flag) {
59
+ const explicit = flag?.trim();
60
+ if (explicit)
61
+ return explicit;
62
+ return readConfig()?.api_url ?? ENVIRONMENTS.prod.apiUrl;
63
+ }
64
+ export function writeConfig(config) {
65
+ const dir = getConfigDir();
66
+ if (!existsSync(dir)) {
67
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
68
+ }
69
+ else {
70
+ try {
71
+ chmodSync(dir, 0o700);
72
+ }
73
+ catch { /* best effort */ }
74
+ }
75
+ const finalPath = getConfigPath();
76
+ // Best-effort cleanup of orphaned tmp files left behind by crashed writes.
77
+ // Only remove old ones — a concurrent process may be between its own
78
+ // writeFileSync and renameSync right now.
79
+ try {
80
+ const base = path.basename(finalPath);
81
+ for (const entry of readdirSync(dir)) {
82
+ if (!entry.startsWith(`${base}.`) || !entry.endsWith('.tmp'))
83
+ continue;
84
+ const tmp = path.join(dir, entry);
85
+ try {
86
+ if (Date.now() - statSync(tmp).mtimeMs > 60_000)
87
+ unlinkSync(tmp);
88
+ }
89
+ catch { /* best effort */ }
90
+ }
91
+ }
92
+ catch { /* best effort */ }
93
+ const tmpPath = `${finalPath}.${randomBytes(6).toString('hex')}.tmp`;
94
+ writeFileSync(tmpPath, JSON.stringify(config, null, 2), { mode: 0o600 });
95
+ try {
96
+ chmodSync(tmpPath, 0o600);
97
+ }
98
+ catch { /* best effort on platforms w/o chmod */ }
99
+ renameSync(tmpPath, finalPath);
100
+ try {
101
+ chmodSync(finalPath, 0o600);
102
+ }
103
+ catch { /* best effort */ }
104
+ }
105
+ export function clearConfig() {
106
+ const p = getConfigPath();
107
+ if (existsSync(p))
108
+ unlinkSync(p);
109
+ }
110
+ export function configFileMode() {
111
+ const p = getConfigPath();
112
+ if (!existsSync(p))
113
+ return null;
114
+ return statSync(p).mode & 0o777;
115
+ }
@@ -1,16 +1,13 @@
1
- "use strict";
2
1
  /**
3
2
  * Utility for checking internet connectivity using third-party endpoints
4
3
  */
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.checkInternetConnectivity = checkInternetConnectivity;
7
4
  /**
8
5
  * Check if the system has internet connectivity by testing against
9
6
  * multiple reliable third-party endpoints with detailed diagnostics.
10
7
  *
11
8
  * @returns Promise<ConnectivityCheckResult> - Detailed connectivity check results
12
9
  */
13
- async function checkInternetConnectivity() {
10
+ export async function checkInternetConnectivity() {
14
11
  // Use multiple reliable endpoints to test connectivity
15
12
  const testEndpoints = [
16
13
  { url: 'https://www.google.com/generate_204', description: 'Google' },
@@ -22,16 +19,15 @@ async function checkInternetConnectivity() {
22
19
  // Try each endpoint with a short timeout
23
20
  for (const { url, description } of testEndpoints) {
24
21
  const startTime = Date.now();
22
+ const controller = new AbortController();
23
+ const timeoutId = setTimeout(() => controller.abort(), 3000); // 3 second timeout
25
24
  try {
26
- const controller = new AbortController();
27
- const timeoutId = setTimeout(() => controller.abort(), 3000); // 3 second timeout
28
25
  const response = await fetch(url, {
29
26
  method: 'HEAD', // Use HEAD to minimize data transfer
30
27
  signal: controller.signal,
31
28
  // Disable redirects to get faster response
32
29
  redirect: 'manual',
33
30
  });
34
- clearTimeout(timeoutId);
35
31
  const latencyMs = Date.now() - startTime;
36
32
  // Any response (including 3xx redirects) indicates connectivity
37
33
  if (response) {
@@ -81,6 +77,11 @@ async function checkInternetConnectivity() {
81
77
  // Continue to next endpoint if this one fails
82
78
  continue;
83
79
  }
80
+ finally {
81
+ // Always clear — a dangling abort timer keeps the event loop alive
82
+ // for up to 3s per failed endpoint.
83
+ clearTimeout(timeoutId);
84
+ }
84
85
  }
85
86
  // Generate developer-friendly message
86
87
  let message;
@@ -1,17 +1,11 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isUrl = isUrl;
4
- exports.downloadExpoUrl = downloadExpoUrl;
5
- exports.extractTarGz = extractTarGz;
6
- exports.findAppBundle = findAppBundle;
7
- const node_crypto_1 = require("node:crypto");
8
- const fs = require("node:fs");
9
- const fsp = require("node:fs/promises");
10
- const os = require("node:os");
11
- const path = require("node:path");
12
- const node_stream_1 = require("node:stream");
13
- const promises_1 = require("node:stream/promises");
14
- const tar = require("tar");
1
+ import { randomUUID } from 'node:crypto';
2
+ import * as fs from 'node:fs';
3
+ import * as fsp from 'node:fs/promises';
4
+ import * as os from 'node:os';
5
+ import * as path from 'node:path';
6
+ import { Readable } from 'node:stream';
7
+ import { pipeline } from 'node:stream/promises';
8
+ import * as tar from 'tar';
15
9
  const DOWNLOAD_RETRY_ATTEMPTS = 3;
16
10
  const DOWNLOAD_RETRY_DELAY_MS = 2000;
17
11
  /**
@@ -19,7 +13,7 @@ const DOWNLOAD_RETRY_DELAY_MS = 2000;
19
13
  * @param input - String to test
20
14
  * @returns True if the string begins with http:// or https://
21
15
  */
22
- function isUrl(input) {
16
+ export function isUrl(input) {
23
17
  return input.startsWith('http://') || input.startsWith('https://');
24
18
  }
25
19
  /**
@@ -31,8 +25,8 @@ function isUrl(input) {
31
25
  * @param debug - Whether to emit debug log lines
32
26
  * @returns Absolute path to the downloaded temp file
33
27
  */
34
- async function downloadExpoUrl(url, debug) {
35
- const destPath = path.join(os.tmpdir(), `dcd-expo-${(0, node_crypto_1.randomUUID)()}.tar.gz`);
28
+ export async function downloadExpoUrl(url, debug) {
29
+ const destPath = path.join(os.tmpdir(), `dcd-expo-${randomUUID()}.tar.gz`);
36
30
  for (let attempt = 1; attempt <= DOWNLOAD_RETRY_ATTEMPTS; attempt++) {
37
31
  if (debug) {
38
32
  console.log(`[DEBUG] Downloading Expo URL (attempt ${attempt}/${DOWNLOAD_RETRY_ATTEMPTS}): ${url}`);
@@ -40,17 +34,27 @@ async function downloadExpoUrl(url, debug) {
40
34
  try {
41
35
  const response = await fetch(url);
42
36
  if (!response.ok) {
37
+ // 4xx responses (expired signed URL, 404, ...) won't get better on
38
+ // retry — flag them so the catch below rethrows immediately.
39
+ const permanent = response.status >= 400 && response.status < 500;
40
+ let error;
43
41
  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.`);
42
+ 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
43
  }
46
- throw new Error(`Failed to download Expo build from URL (HTTP ${response.status}).`);
44
+ else {
45
+ error = new Error(`Failed to download Expo build from URL (HTTP ${response.status}).`);
46
+ }
47
+ if (permanent) {
48
+ error.permanent = true;
49
+ }
50
+ throw error;
47
51
  }
48
52
  if (!response.body) {
49
53
  throw new Error('No response body received from Expo URL.');
50
54
  }
51
55
  // Stream to disk using pipeline to handle backpressure and avoid loading
52
56
  // the entire archive in memory
53
- await (0, promises_1.pipeline)(node_stream_1.Readable.fromWeb(response.body), fs.createWriteStream(destPath));
57
+ await pipeline(Readable.fromWeb(response.body), fs.createWriteStream(destPath));
54
58
  if (debug) {
55
59
  const stat = await fsp.stat(destPath);
56
60
  console.log(`[DEBUG] Downloaded ${(stat.size / 1024 / 1024).toFixed(2)} MB to ${destPath}`);
@@ -60,8 +64,9 @@ async function downloadExpoUrl(url, debug) {
60
64
  catch (error) {
61
65
  // Clean up any partial file before retrying
62
66
  await fsp.rm(destPath, { force: true }).catch(() => { });
67
+ const isPermanent = Boolean(error?.permanent);
63
68
  const isLastAttempt = attempt === DOWNLOAD_RETRY_ATTEMPTS;
64
- if (isLastAttempt) {
69
+ if (isPermanent || isLastAttempt) {
65
70
  throw error;
66
71
  }
67
72
  if (debug) {
@@ -80,8 +85,8 @@ async function downloadExpoUrl(url, debug) {
80
85
  * @param debug - Whether to emit debug log lines
81
86
  * @returns Absolute path to the newly created extract directory
82
87
  */
83
- async function extractTarGz(tarPath, debug) {
84
- const extractDir = path.join(os.tmpdir(), `dcd-expo-${(0, node_crypto_1.randomUUID)()}`);
88
+ export async function extractTarGz(tarPath, debug) {
89
+ const extractDir = path.join(os.tmpdir(), `dcd-expo-${randomUUID()}`);
85
90
  await fsp.mkdir(extractDir, { recursive: true });
86
91
  if (debug) {
87
92
  console.log(`[DEBUG] Extracting ${tarPath} to ${extractDir}`);
@@ -98,7 +103,7 @@ async function extractTarGz(tarPath, debug) {
98
103
  * @param dir - Directory to search within
99
104
  * @returns Absolute path to the .app directory
100
105
  */
101
- async function findAppBundle(dir) {
106
+ export async function findAppBundle(dir) {
102
107
  const candidates = [];
103
108
  async function walk(current, depth) {
104
109
  const entries = await fsp.readdir(current, { withFileTypes: true });
@@ -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,36 @@
1
+ /**
2
+ * Shared helpers for fetching and picking from /me/orgs. Used by both
3
+ * `dcd login` (after the PKCE claim returns a session) and `dcd switch-org`.
4
+ */
5
+ import * as p from '@clack/prompts';
6
+ import { CliError } from './cli.js';
7
+ export async function fetchOrgs(apiUrl, headers) {
8
+ const res = await fetch(`${apiUrl.replace(/\/$/, '')}/me/orgs`, { headers });
9
+ if (!res.ok) {
10
+ const body = await res.text().catch(() => '');
11
+ throw new CliError(`Failed to list organizations: HTTP ${res.status}${body ? ` — ${body}` : ''}`);
12
+ }
13
+ const body = (await res.json());
14
+ return body.orgs ?? [];
15
+ }
16
+ /**
17
+ * Interactive org picker. Auto-selects when there's only one org (returns it
18
+ * without prompting). Throws on zero orgs or user cancellation.
19
+ */
20
+ export async function pickOrg(orgs, message = 'Pick an organization') {
21
+ if (orgs.length === 0) {
22
+ throw new CliError('No organizations found for this user.');
23
+ }
24
+ if (orgs.length === 1)
25
+ return orgs[0];
26
+ const picked = await p.select({
27
+ message,
28
+ options: orgs.map((o) => ({ value: o.id, label: o.name })),
29
+ });
30
+ if (p.isCancel(picked))
31
+ throw new CliError('Cancelled.');
32
+ const chosen = orgs.find((o) => o.id === picked);
33
+ if (!chosen)
34
+ throw new CliError('No organization selected.');
35
+ return chosen;
36
+ }
@@ -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,21 @@
1
+ import * as path from 'node:path';
2
+ /**
3
+ * Convert an absolute path into the portable './'-prefixed, forward-slash
4
+ * relative form used as flow keys across submission, metadata maps, and
5
+ * polling. `commonRoot` must be a whole-segment prefix of `absolutePath`
6
+ * without a trailing separator (or '' when no common root exists, in which
7
+ * case the path is kept whole apart from separator normalization).
8
+ *
9
+ * Replaces the old `replaceAll(commonRoot, '.')` pattern, which corrupted
10
+ * paths when the root substring recurred mid-path or collapsed to ''.
11
+ */
12
+ export function toPortableRelativePath(absolutePath, commonRoot) {
13
+ let relative = absolutePath;
14
+ if (commonRoot && absolutePath.startsWith(commonRoot)) {
15
+ relative = absolutePath.slice(commonRoot.length);
16
+ }
17
+ if (!relative.startsWith(path.sep)) {
18
+ relative = path.sep + relative;
19
+ }
20
+ return ('.' + relative).split(path.sep).join('/');
21
+ }
@@ -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,47 @@
1
+ /**
2
+ * Progress adapter that wraps @clack/prompts spinner with a
3
+ * drop-in API for existing services that used oclif's `ux.action` / `ux.info`.
4
+ *
5
+ * Keeps call sites unchanged while migrating away from @oclif/core.
6
+ */
7
+ import * as p from '@clack/prompts';
8
+ class Action {
9
+ current = null;
10
+ _status = '';
11
+ start(title, initialStatus, _opts) {
12
+ if (this.current) {
13
+ this.current.stop();
14
+ }
15
+ this.current = p.spinner();
16
+ this._status = initialStatus ?? '';
17
+ this.current.start(initialStatus ? `${title} — ${initialStatus}` : title);
18
+ }
19
+ stop(message) {
20
+ if (!this.current) {
21
+ if (message) {
22
+ // eslint-disable-next-line no-console
23
+ console.log(message);
24
+ }
25
+ return;
26
+ }
27
+ this.current.stop(message ?? '');
28
+ this.current = null;
29
+ this._status = '';
30
+ }
31
+ set status(value) {
32
+ this._status = value;
33
+ if (this.current && value) {
34
+ this.current.message(value);
35
+ }
36
+ }
37
+ get status() {
38
+ return this._status;
39
+ }
40
+ }
41
+ export const ux = {
42
+ action: new Action(),
43
+ info(message) {
44
+ // eslint-disable-next-line no-console
45
+ console.log(message);
46
+ },
47
+ };
@@ -1,8 +1,9 @@
1
- import chalk = require('chalk');
2
1
  /**
3
2
  * Centralized styling utilities for CLI output
4
3
  * Provides consistent, developer-friendly visual formatting
5
4
  */
5
+ /** Strip ANSI color escape sequences for visible-width calculations. */
6
+ export declare const stripAnsi: (s: string) => string;
6
7
  /**
7
8
  * Status symbols with associated colors
8
9
  */
@@ -21,50 +22,49 @@ export declare const symbols: {
21
22
  * Color utility functions for semantic styling
22
23
  */
23
24
  export declare const colors: {
24
- readonly bold: chalk.Chalk;
25
- readonly dim: chalk.Chalk;
26
- readonly error: chalk.Chalk;
27
- readonly highlight: chalk.Chalk;
28
- readonly info: chalk.Chalk;
29
- readonly success: chalk.Chalk;
30
- readonly url: chalk.Chalk;
31
- readonly warning: chalk.Chalk;
25
+ readonly bold: import("chalk").ChalkInstance;
26
+ readonly dim: import("chalk").ChalkInstance;
27
+ readonly error: import("chalk").ChalkInstance;
28
+ readonly highlight: import("chalk").ChalkInstance;
29
+ readonly info: import("chalk").ChalkInstance;
30
+ readonly success: import("chalk").ChalkInstance;
31
+ readonly url: import("chalk").ChalkInstance;
32
+ readonly warning: import("chalk").ChalkInstance;
32
33
  };
33
34
  /**
34
- * Dividers for visual separation
35
+ * Structural glyphs that give the CLI its tree-shaped layout. `section` marks a
36
+ * top-level heading; `branch` opens the group of detail rows beneath it.
37
+ * See STYLE_GUIDE.md.
35
38
  */
36
- export declare const dividers: {
37
- readonly heavy: string;
38
- readonly light: string;
39
- readonly short: string;
39
+ export declare const glyphs: {
40
+ readonly branch: "⎿";
41
+ readonly section: "⏺";
40
42
  };
41
43
  /**
42
- * Format a status with appropriate symbol and color
44
+ * The single source of truth mapping a run/test status to its colour and
45
+ * (already-coloured) symbol. Both {@link formatStatus} and the `ui` status
46
+ * helpers build on this, so every status reads identically everywhere.
47
+ * @param status - The status string (case-insensitive)
48
+ */
49
+ export declare function statusPalette(status: string): {
50
+ color: (s: string) => string;
51
+ symbol: string;
52
+ };
53
+ /**
54
+ * Format a status as a coloured symbol followed by the lowercased status word,
55
+ * e.g. `✓ passed`.
43
56
  * @param status - The status string to format
44
57
  * @returns Formatted status string with color and symbol
45
58
  */
46
59
  export declare function formatStatus(status: string): string;
47
60
  /**
48
- * Format a section header
61
+ * Format a top-level section header in the tree style: a `⏺` marker followed by
62
+ * the bold title, preceded by a blank line for separation. Detail rows belong
63
+ * underneath in a branch group (see the `ui` helpers).
49
64
  * @param title - The title of the section
50
65
  * @returns Formatted section header
51
66
  */
52
67
  export declare function sectionHeader(title: string): string;
53
- /**
54
- * Format a key-value pair with optional icon
55
- * @param icon - Icon to display before the key
56
- * @param key - The key name
57
- * @param value - The value to display
58
- * @returns Formatted key-value string
59
- */
60
- export declare function keyValue(icon: string, key: string, value: string): string;
61
- /**
62
- * Format a list item
63
- * @param text - The text of the list item
64
- * @param prefix - The prefix character (default: '•')
65
- * @returns Formatted list item
66
- */
67
- export declare function listItem(text: string, prefix?: string): string;
68
68
  /**
69
69
  * Format a URL
70
70
  * @param url - The URL to format
@@ -92,14 +92,20 @@ export declare function formatTestSummary(summary: {
92
92
  total: number;
93
93
  }): string;
94
94
  /**
95
- * Format a box with content
96
- * @param content - The content to display in the box
97
- * @returns Formatted box with borders
95
+ * Minimal column table renderer.
96
+ * Columns are defined with a `get(row) => string` accessor and optional header label.
97
+ * Matches the subset of oclif's ux.table used by this CLI.
98
98
  */
99
- export declare function box(content: string): string;
99
+ export declare function table<T>(rows: T[], columns: Record<string, {
100
+ get: (row: T) => string;
101
+ header?: string;
102
+ }>, options?: {
103
+ printLine?: (line: string) => void;
104
+ }): void;
100
105
  /**
101
106
  * Generate console URL based on API URL
102
- * If a non-default API URL is used, prepends "dev." to the console subdomain
107
+ * Derives the console host from the known environment matching the API URL;
108
+ * unknown API URLs fall back to the dev console (historical behavior).
103
109
  * @param apiUrl - The API URL being used
104
110
  * @param uploadId - The upload ID
105
111
  * @param resultId - The result ID