@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.
- package/README.md +75 -2
- package/dist/commands/artifacts.d.ts +47 -18
- package/dist/commands/artifacts.js +69 -64
- package/dist/commands/cloud.d.ts +228 -88
- package/dist/commands/cloud.js +430 -342
- package/dist/commands/list.d.ts +39 -38
- package/dist/commands/list.js +124 -131
- package/dist/commands/live.d.ts +2 -0
- package/dist/commands/live.js +520 -0
- package/dist/commands/login.d.ts +17 -0
- package/dist/commands/login.js +252 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.js +30 -0
- package/dist/commands/status.d.ts +23 -42
- package/dist/commands/status.js +170 -179
- package/dist/commands/switch-org.d.ts +12 -0
- package/dist/commands/switch-org.js +76 -0
- package/dist/commands/upgrade.d.ts +2 -0
- package/dist/commands/upgrade.js +120 -0
- package/dist/commands/upload.d.ts +33 -18
- package/dist/commands/upload.js +72 -78
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +31 -0
- package/dist/config/environments.d.ts +31 -0
- package/dist/config/environments.js +52 -0
- package/dist/config/flags/api.flags.d.ts +10 -2
- package/dist/config/flags/api.flags.js +13 -14
- package/dist/config/flags/binary.flags.d.ts +17 -4
- package/dist/config/flags/binary.flags.js +14 -18
- package/dist/config/flags/device.flags.d.ts +49 -11
- package/dist/config/flags/device.flags.js +43 -38
- package/dist/config/flags/environment.flags.d.ts +27 -6
- package/dist/config/flags/environment.flags.js +24 -29
- package/dist/config/flags/execution.flags.d.ts +35 -8
- package/dist/config/flags/execution.flags.js +31 -41
- package/dist/config/flags/github.flags.d.ts +23 -5
- package/dist/config/flags/github.flags.js +19 -15
- package/dist/config/flags/output.flags.d.ts +57 -13
- package/dist/config/flags/output.flags.js +48 -47
- package/dist/constants.d.ts +218 -51
- package/dist/constants.js +17 -20
- package/dist/gateways/api-gateway.d.ts +72 -16
- package/dist/gateways/api-gateway.js +298 -104
- package/dist/gateways/cli-auth-gateway.d.ts +13 -0
- package/dist/gateways/cli-auth-gateway.js +54 -0
- package/dist/gateways/realtime-gateway.d.ts +32 -0
- package/dist/gateways/realtime-gateway.js +103 -0
- package/dist/gateways/supabase-gateway.d.ts +11 -11
- package/dist/gateways/supabase-gateway.js +20 -48
- package/dist/index.d.ts +2 -1
- package/dist/index.js +98 -4
- package/dist/mcp/context.d.ts +33 -0
- package/dist/mcp/context.js +33 -0
- package/dist/mcp/helpers.d.ts +16 -0
- package/dist/mcp/helpers.js +34 -0
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +24 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +27 -0
- package/dist/mcp/tools/download-artifacts.d.ts +11 -0
- package/dist/mcp/tools/download-artifacts.js +84 -0
- package/dist/mcp/tools/get-status.d.ts +7 -0
- package/dist/mcp/tools/get-status.js +39 -0
- package/dist/mcp/tools/list-devices.d.ts +7 -0
- package/dist/mcp/tools/list-devices.js +27 -0
- package/dist/mcp/tools/list-runs.d.ts +3 -0
- package/dist/mcp/tools/list-runs.js +60 -0
- package/dist/mcp/tools/run-cloud-test.d.ts +14 -0
- package/dist/mcp/tools/run-cloud-test.js +233 -0
- package/dist/methods.d.ts +34 -5
- package/dist/methods.js +266 -215
- package/dist/services/device-validation.service.d.ts +9 -1
- package/dist/services/device-validation.service.js +56 -40
- package/dist/services/execution-plan.service.js +40 -31
- package/dist/services/execution-plan.utils.d.ts +3 -0
- package/dist/services/execution-plan.utils.js +25 -55
- package/dist/services/flow-paths.d.ts +17 -0
- package/dist/services/flow-paths.js +52 -0
- package/dist/services/metadata-extractor.service.d.ts +0 -2
- package/dist/services/metadata-extractor.service.js +75 -78
- package/dist/services/moropo.service.js +33 -34
- package/dist/services/report-download.service.d.ts +12 -1
- package/dist/services/report-download.service.js +34 -27
- package/dist/services/results-polling.service.d.ts +23 -9
- package/dist/services/results-polling.service.js +257 -123
- package/dist/services/telemetry.service.d.ts +49 -0
- package/dist/services/telemetry.service.js +252 -0
- package/dist/services/test-submission.service.d.ts +21 -4
- package/dist/services/test-submission.service.js +51 -33
- package/dist/services/version.service.d.ts +4 -3
- package/dist/services/version.service.js +28 -16
- package/dist/types/domain/auth.types.d.ts +20 -0
- package/dist/types/domain/auth.types.js +1 -0
- package/dist/types/domain/device.types.js +8 -11
- package/dist/types/domain/live.types.d.ts +76 -0
- package/dist/types/domain/live.types.js +3 -0
- package/dist/types/generated/schema.types.js +1 -2
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.js +2 -18
- package/dist/types.js +1 -2
- package/dist/utils/auth.d.ts +13 -0
- package/dist/utils/auth.js +141 -0
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +39 -0
- package/dist/utils/cli.d.ts +35 -0
- package/dist/utils/cli.js +118 -0
- package/dist/utils/compatibility.d.ts +2 -1
- package/dist/utils/compatibility.js +6 -8
- package/dist/utils/config-store.d.ts +35 -0
- package/dist/utils/config-store.js +115 -0
- package/dist/utils/connectivity.js +8 -7
- package/dist/utils/expo.js +29 -24
- package/dist/utils/orgs.d.ts +11 -0
- package/dist/utils/orgs.js +36 -0
- package/dist/utils/paths.d.ts +11 -0
- package/dist/utils/paths.js +21 -0
- package/dist/utils/progress.d.ts +13 -0
- package/dist/utils/progress.js +47 -0
- package/dist/utils/styling.d.ts +42 -36
- package/dist/utils/styling.js +78 -82
- package/dist/utils/ui.d.ts +41 -0
- package/dist/utils/ui.js +95 -0
- package/package.json +36 -45
- package/bin/dev.cmd +0 -3
- package/bin/dev.js +0 -6
- package/bin/run.cmd +0 -3
- package/bin/run.js +0 -7
- package/dist/types/schema.types.d.ts +0 -2702
- package/dist/types/schema.types.js +0 -3
- 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;
|
package/dist/utils/expo.js
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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-${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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-${
|
|
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
|
+
};
|
package/dist/utils/styling.d.ts
CHANGED
|
@@ -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.
|
|
25
|
-
readonly dim: chalk.
|
|
26
|
-
readonly error: chalk.
|
|
27
|
-
readonly highlight: chalk.
|
|
28
|
-
readonly info: chalk.
|
|
29
|
-
readonly success: chalk.
|
|
30
|
-
readonly url: chalk.
|
|
31
|
-
readonly warning: 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
|
-
*
|
|
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
|
|
37
|
-
readonly
|
|
38
|
-
readonly
|
|
39
|
-
readonly short: string;
|
|
39
|
+
export declare const glyphs: {
|
|
40
|
+
readonly branch: "⎿";
|
|
41
|
+
readonly section: "⏺";
|
|
40
42
|
};
|
|
41
43
|
/**
|
|
42
|
-
*
|
|
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
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|