@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.
- package/README.md +40 -2
- package/dist/commands/artifacts.d.ts +47 -18
- package/dist/commands/artifacts.js +68 -60
- package/dist/commands/cloud.d.ts +228 -88
- package/dist/commands/cloud.js +389 -288
- package/dist/commands/list.d.ts +39 -38
- package/dist/commands/list.js +122 -127
- package/dist/commands/live.d.ts +2 -0
- package/dist/commands/live.js +513 -0
- package/dist/commands/login.d.ts +17 -0
- package/dist/commands/login.js +250 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.js +32 -0
- package/dist/commands/status.d.ts +23 -42
- package/dist/commands/status.js +162 -173
- package/dist/commands/switch-org.d.ts +12 -0
- package/dist/commands/switch-org.js +78 -0
- package/dist/commands/upgrade.d.ts +2 -0
- package/dist/commands/upgrade.js +122 -0
- package/dist/commands/upload.d.ts +33 -18
- package/dist/commands/upload.js +62 -67
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +34 -0
- package/dist/config/environments.d.ts +31 -0
- package/dist/config/environments.js +58 -0
- package/dist/config/flags/api.flags.d.ts +10 -2
- package/dist/config/flags/api.flags.js +12 -10
- package/dist/config/flags/binary.flags.d.ts +17 -4
- package/dist/config/flags/binary.flags.js +13 -14
- package/dist/config/flags/device.flags.d.ts +49 -11
- package/dist/config/flags/device.flags.js +41 -33
- package/dist/config/flags/environment.flags.d.ts +27 -6
- package/dist/config/flags/environment.flags.js +23 -25
- package/dist/config/flags/execution.flags.d.ts +35 -8
- package/dist/config/flags/execution.flags.js +30 -37
- package/dist/config/flags/github.flags.d.ts +23 -5
- package/dist/config/flags/github.flags.js +18 -11
- package/dist/config/flags/output.flags.d.ts +57 -13
- package/dist/config/flags/output.flags.js +47 -43
- package/dist/constants.d.ts +218 -51
- package/dist/constants.js +2 -2
- package/dist/gateways/api-gateway.d.ts +43 -12
- package/dist/gateways/api-gateway.js +240 -100
- package/dist/gateways/cli-auth-gateway.d.ts +13 -0
- package/dist/gateways/cli-auth-gateway.js +57 -0
- package/dist/gateways/supabase-gateway.d.ts +11 -11
- package/dist/gateways/supabase-gateway.js +15 -39
- package/dist/index.d.ts +2 -1
- package/dist/index.js +93 -2
- package/dist/methods.d.ts +3 -5
- package/dist/methods.js +170 -178
- package/dist/services/device-validation.service.d.ts +8 -0
- package/dist/services/device-validation.service.js +55 -35
- package/dist/services/execution-plan.service.js +27 -15
- package/dist/services/execution-plan.utils.d.ts +3 -0
- package/dist/services/execution-plan.utils.js +10 -32
- package/dist/services/metadata-extractor.service.d.ts +0 -2
- package/dist/services/metadata-extractor.service.js +57 -57
- package/dist/services/moropo.service.js +25 -24
- package/dist/services/report-download.service.d.ts +12 -1
- package/dist/services/report-download.service.js +31 -20
- package/dist/services/results-polling.service.d.ts +6 -7
- package/dist/services/results-polling.service.js +80 -33
- package/dist/services/telemetry.service.d.ts +40 -0
- package/dist/services/telemetry.service.js +230 -0
- package/dist/services/test-submission.service.js +2 -1
- package/dist/services/version.service.d.ts +3 -2
- package/dist/services/version.service.js +27 -11
- package/dist/types/domain/auth.types.d.ts +12 -0
- package/dist/types/{schema.types.js → domain/auth.types.js} +0 -1
- package/dist/types/domain/live.types.d.ts +76 -0
- package/dist/types/domain/live.types.js +4 -0
- package/dist/utils/auth.d.ts +13 -0
- package/dist/utils/auth.js +142 -0
- package/dist/utils/cli.d.ts +35 -0
- package/dist/utils/cli.js +127 -0
- package/dist/utils/compatibility.d.ts +2 -1
- package/dist/utils/compatibility.js +2 -2
- package/dist/utils/config-store.d.ts +35 -0
- package/dist/utils/config-store.js +125 -0
- package/dist/utils/connectivity.js +7 -3
- package/dist/utils/expo.js +14 -3
- package/dist/utils/orgs.d.ts +11 -0
- package/dist/utils/orgs.js +40 -0
- package/dist/utils/paths.d.ts +11 -0
- package/dist/utils/paths.js +24 -0
- package/dist/utils/progress.d.ts +13 -0
- package/dist/utils/progress.js +50 -0
- package/dist/utils/styling.d.ts +13 -5
- package/dist/utils/styling.js +37 -7
- package/package.json +26 -38
- 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/oclif.manifest.json +0 -884
|
@@ -4,8 +4,9 @@ import { CompatibilityData } from '../utils/compatibility';
|
|
|
4
4
|
*/
|
|
5
5
|
export declare class VersionService {
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Fetch the latest published CLI version from the release manifest.
|
|
8
|
+
* Works for both npm- and binary-installed users (no `npm` shell-out).
|
|
9
|
+
* Silently returns null on any failure — this check is informational only.
|
|
9
10
|
*/
|
|
10
11
|
checkLatestCliVersion(): Promise<null | string>;
|
|
11
12
|
/**
|
|
@@ -1,27 +1,34 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.VersionService = void 0;
|
|
4
|
-
const
|
|
4
|
+
const DEFAULT_MANIFEST_URL = 'https://get.devicecloud.dev/latest.json';
|
|
5
|
+
const MANIFEST_TIMEOUT_MS = 3000;
|
|
5
6
|
/**
|
|
6
7
|
* Service for handling version validation and checking
|
|
7
8
|
*/
|
|
8
9
|
class VersionService {
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
* Fetch the latest published CLI version from the release manifest.
|
|
12
|
+
* Works for both npm- and binary-installed users (no `npm` shell-out).
|
|
13
|
+
* Silently returns null on any failure — this check is informational only.
|
|
12
14
|
*/
|
|
13
15
|
async checkLatestCliVersion() {
|
|
16
|
+
const url = process.env.DCD_MANIFEST_URL ?? DEFAULT_MANIFEST_URL;
|
|
17
|
+
const controller = new AbortController();
|
|
18
|
+
const timer = setTimeout(() => controller.abort(), MANIFEST_TIMEOUT_MS);
|
|
14
19
|
try {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return
|
|
20
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
21
|
+
if (!res.ok)
|
|
22
|
+
return null;
|
|
23
|
+
const data = (await res.json());
|
|
24
|
+
return typeof data.version === 'string' ? data.version : null;
|
|
20
25
|
}
|
|
21
26
|
catch {
|
|
22
|
-
// Silently fail - version check is informational only
|
|
23
27
|
return null;
|
|
24
28
|
}
|
|
29
|
+
finally {
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
}
|
|
25
32
|
}
|
|
26
33
|
/**
|
|
27
34
|
* Compare two semantic version strings
|
|
@@ -30,8 +37,14 @@ class VersionService {
|
|
|
30
37
|
* @returns true if current is older than latest
|
|
31
38
|
*/
|
|
32
39
|
isOutdated(current, latest) {
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
// Strip any prerelease suffix ("1.2.3-beta.1" -> "1.2.3") and default
|
|
41
|
+
// missing segments to 0 so short/prerelease versions still compare.
|
|
42
|
+
const parts = (version) => {
|
|
43
|
+
const nums = version.split('-')[0].split('.').map(Number);
|
|
44
|
+
return [nums[0] || 0, nums[1] || 0, nums[2] || 0];
|
|
45
|
+
};
|
|
46
|
+
const currentParts = parts(current);
|
|
47
|
+
const latestParts = parts(latest);
|
|
35
48
|
for (let i = 0; i < 3; i++) {
|
|
36
49
|
if (currentParts[i] < latestParts[i])
|
|
37
50
|
return true;
|
|
@@ -68,6 +81,9 @@ class VersionService {
|
|
|
68
81
|
log(`[DEBUG] Using default Maestro version ${defaultVersion}`);
|
|
69
82
|
}
|
|
70
83
|
}
|
|
84
|
+
if (!resolvedVersion) {
|
|
85
|
+
throw new Error('Unable to resolve a Maestro version: compatibility data did not provide a default.');
|
|
86
|
+
}
|
|
71
87
|
// Validate Maestro version
|
|
72
88
|
if (!supportedVersions.includes(resolvedVersion)) {
|
|
73
89
|
throw new Error(`Maestro version ${resolvedVersion} is not supported. Supported versions: ${supportedVersions.join(', ')}`);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth context threaded through gateways and services. Callers build this once
|
|
3
|
+
* (via resolveAuth) and the gateway spreads .headers into every fetch.
|
|
4
|
+
*/
|
|
5
|
+
export interface AuthContext {
|
|
6
|
+
headers: Record<string, string>;
|
|
7
|
+
mode: 'apiKey' | 'bearer';
|
|
8
|
+
/** Present when mode === 'bearer'. */
|
|
9
|
+
orgId?: string;
|
|
10
|
+
/** Present when mode === 'bearer'. */
|
|
11
|
+
userEmail?: string;
|
|
12
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** Summary returned when a live session is created. */
|
|
2
|
+
export interface LiveSessionSummary {
|
|
3
|
+
id: number;
|
|
4
|
+
platform: string;
|
|
5
|
+
session_name: string;
|
|
6
|
+
status: string;
|
|
7
|
+
}
|
|
8
|
+
/** One node of the device view hierarchy, as the live API exposes it. */
|
|
9
|
+
export interface LiveHierarchyElement {
|
|
10
|
+
id: string;
|
|
11
|
+
text?: string;
|
|
12
|
+
accessibilityText?: string;
|
|
13
|
+
resourceId?: string;
|
|
14
|
+
resourceIdIndex?: number;
|
|
15
|
+
textIndex?: number;
|
|
16
|
+
bounds?: {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** The device view hierarchy plus the frame it was captured against. */
|
|
24
|
+
export interface LiveHierarchy {
|
|
25
|
+
elements: LiveHierarchyElement[];
|
|
26
|
+
width: number;
|
|
27
|
+
height: number;
|
|
28
|
+
/** base64 PNG of the frame the hierarchy was captured against. */
|
|
29
|
+
screenshot: string;
|
|
30
|
+
}
|
|
31
|
+
/** Where the device is in its boot/stream lifecycle. */
|
|
32
|
+
export interface LiveDeviceState {
|
|
33
|
+
phase: string;
|
|
34
|
+
/** Server-derived human label for `phase` — prefer this over mapping it. */
|
|
35
|
+
phase_label?: string;
|
|
36
|
+
last_seen_at?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Full live session record returned by `GET /live/:identifier`. The API returns
|
|
40
|
+
* far more than the create summary — including the current screenshot, the view
|
|
41
|
+
* hierarchy, and a `ready` flag — which the screenshot/hierarchy/--wait commands
|
|
42
|
+
* read directly.
|
|
43
|
+
*/
|
|
44
|
+
export interface LiveSession extends LiveSessionSummary {
|
|
45
|
+
binary_upload_id: null | string;
|
|
46
|
+
created_at: string;
|
|
47
|
+
device_state?: LiveDeviceState | null;
|
|
48
|
+
/** Current device frame as a `data:image/png;base64,...` URL. */
|
|
49
|
+
screenshot_data_url?: null | string;
|
|
50
|
+
hierarchy?: LiveHierarchy | null;
|
|
51
|
+
/** True when the session can accept exec requests (RUNNING + streaming). */
|
|
52
|
+
ready?: boolean;
|
|
53
|
+
/** Model the simulator actually cloned, e.g. "pixel-7-api-34". */
|
|
54
|
+
device_model?: null | string;
|
|
55
|
+
/** Locale requested for the session, e.g. "de_DE", or null for default. */
|
|
56
|
+
device_locale?: null | string;
|
|
57
|
+
seconds_until_auto_cancel?: null | number;
|
|
58
|
+
}
|
|
59
|
+
/** Result of executing Maestro YAML against a live session. */
|
|
60
|
+
export interface LiveExecResult {
|
|
61
|
+
error?: string;
|
|
62
|
+
output?: string;
|
|
63
|
+
success: boolean;
|
|
64
|
+
/** Async mode only: id of the queued command to poll, and its submit status. */
|
|
65
|
+
commandId?: string;
|
|
66
|
+
status?: string;
|
|
67
|
+
}
|
|
68
|
+
/** Status of a queued live command (async exec poll). */
|
|
69
|
+
export interface LiveCommandStatus {
|
|
70
|
+
status: string;
|
|
71
|
+
/** True once the command reached a terminal state (passed/failed/timeout). */
|
|
72
|
+
done: boolean;
|
|
73
|
+
success: boolean;
|
|
74
|
+
output?: string;
|
|
75
|
+
error?: string;
|
|
76
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AuthContext } from '../types/domain/auth.types';
|
|
2
|
+
export interface ResolveAuthOptions {
|
|
3
|
+
apiKeyFlag: string | undefined;
|
|
4
|
+
/** When true, bypass stored session entirely (for `dcd login` itself). */
|
|
5
|
+
skipSession?: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* When true, ignore the --api-key flag / DEVICE_CLOUD_API_KEY env and
|
|
8
|
+
* resolve straight from the stored session (for `dcd switch-org`, which
|
|
9
|
+
* needs a browser login even when an API key is exported).
|
|
10
|
+
*/
|
|
11
|
+
sessionOnly?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function resolveAuth(opts: ResolveAuthOptions): Promise<AuthContext>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveAuth = resolveAuth;
|
|
4
|
+
/**
|
|
5
|
+
* Resolves which credential a command should use and returns the fetch headers
|
|
6
|
+
* to send. Precedence: --api-key flag > DEVICE_CLOUD_API_KEY env > stored
|
|
7
|
+
* Supabase session (refreshed if near expiry). Throws CliError if none found.
|
|
8
|
+
*
|
|
9
|
+
* The refresh path writes the rotated tokens back to disk atomically. Callers
|
|
10
|
+
* use the returned AuthContext for the duration of the command — one resolve
|
|
11
|
+
* per invocation, not per request.
|
|
12
|
+
*/
|
|
13
|
+
const node_fs_1 = require("node:fs");
|
|
14
|
+
const environments_1 = require("../config/environments");
|
|
15
|
+
const cli_auth_gateway_1 = require("../gateways/cli-auth-gateway");
|
|
16
|
+
const telemetry_service_1 = require("../services/telemetry.service");
|
|
17
|
+
const cli_1 = require("./cli");
|
|
18
|
+
const config_store_1 = require("./config-store");
|
|
19
|
+
const REFRESH_SKEW_SECONDS = 60;
|
|
20
|
+
// Refresh lock tuning: a refresh is a single HTTP round-trip, so anything
|
|
21
|
+
// holding the lock longer than this is presumed dead.
|
|
22
|
+
const LOCK_STALE_MS = 10_000;
|
|
23
|
+
const LOCK_WAIT_MS = 10_000;
|
|
24
|
+
const LOCK_POLL_MS = 250;
|
|
25
|
+
async function resolveAuth(opts) {
|
|
26
|
+
if (!opts.sessionOnly) {
|
|
27
|
+
const flag = opts.apiKeyFlag?.trim();
|
|
28
|
+
if (flag) {
|
|
29
|
+
const auth = {
|
|
30
|
+
mode: 'apiKey',
|
|
31
|
+
headers: { 'x-app-api-key': flag },
|
|
32
|
+
};
|
|
33
|
+
telemetry_service_1.telemetry.configure({ auth });
|
|
34
|
+
return auth;
|
|
35
|
+
}
|
|
36
|
+
const env = process.env.DEVICE_CLOUD_API_KEY?.trim();
|
|
37
|
+
if (env) {
|
|
38
|
+
const auth = {
|
|
39
|
+
mode: 'apiKey',
|
|
40
|
+
headers: { 'x-app-api-key': env },
|
|
41
|
+
};
|
|
42
|
+
telemetry_service_1.telemetry.configure({ auth });
|
|
43
|
+
return auth;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (opts.skipSession) {
|
|
47
|
+
throw missingCredentialsError();
|
|
48
|
+
}
|
|
49
|
+
let config = (0, config_store_1.readConfig)();
|
|
50
|
+
if (!config?.session) {
|
|
51
|
+
throw missingCredentialsError();
|
|
52
|
+
}
|
|
53
|
+
const now = Math.floor(Date.now() / 1000);
|
|
54
|
+
let session = config.session;
|
|
55
|
+
if (session.expires_at <= now + REFRESH_SKEW_SECONDS) {
|
|
56
|
+
({ config, session } = await refreshSessionWithLock(config));
|
|
57
|
+
}
|
|
58
|
+
if (!config.current_org_id) {
|
|
59
|
+
throw new cli_1.CliError('No active organization set. Run `dcd switch-org <slug>` to pick one.');
|
|
60
|
+
}
|
|
61
|
+
const auth = {
|
|
62
|
+
mode: 'bearer',
|
|
63
|
+
orgId: config.current_org_id,
|
|
64
|
+
userEmail: session.user_email,
|
|
65
|
+
headers: {
|
|
66
|
+
authorization: `Bearer ${session.access_token}`,
|
|
67
|
+
'x-dcd-org': config.current_org_id,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
telemetry_service_1.telemetry.configure({ auth, apiUrl: config.api_url });
|
|
71
|
+
return auth;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Refresh the stored session under a config-adjacent lockfile so two
|
|
75
|
+
* concurrent `dcd` invocations (CI matrices) can't both consume the same
|
|
76
|
+
* Supabase refresh token — token rotation would revoke the session family.
|
|
77
|
+
*/
|
|
78
|
+
async function refreshSessionWithLock(initial) {
|
|
79
|
+
const lockPath = `${(0, config_store_1.getConfigPath)()}.lock`;
|
|
80
|
+
await acquireRefreshLock(lockPath);
|
|
81
|
+
try {
|
|
82
|
+
// Re-read: another process may have refreshed while we waited.
|
|
83
|
+
const current = (0, config_store_1.readConfig)() ?? initial;
|
|
84
|
+
const session = current.session ?? initial.session;
|
|
85
|
+
const now = Math.floor(Date.now() / 1000);
|
|
86
|
+
if (session.expires_at > now + REFRESH_SKEW_SECONDS) {
|
|
87
|
+
return { config: current, session };
|
|
88
|
+
}
|
|
89
|
+
const { anonKey } = environments_1.ENVIRONMENTS[current.env].supabase;
|
|
90
|
+
const refreshed = await cli_auth_gateway_1.CliAuthGateway.refresh(current.supabase_url, anonKey, session);
|
|
91
|
+
// Re-read again and merge only `session` so a concurrent `switch-org`
|
|
92
|
+
// write (org fields) isn't reverted by our pre-refresh snapshot.
|
|
93
|
+
const merged = { ...((0, config_store_1.readConfig)() ?? current), session: refreshed };
|
|
94
|
+
(0, config_store_1.writeConfig)(merged);
|
|
95
|
+
return { config: merged, session: refreshed };
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
try {
|
|
99
|
+
(0, node_fs_1.rmSync)(lockPath, { force: true });
|
|
100
|
+
}
|
|
101
|
+
catch { /* best effort */ }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function acquireRefreshLock(lockPath) {
|
|
105
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
106
|
+
for (;;) {
|
|
107
|
+
try {
|
|
108
|
+
(0, node_fs_1.closeSync)((0, node_fs_1.openSync)(lockPath, 'wx'));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
if (Date.now() >= deadline) {
|
|
113
|
+
// Don't hang the command forever: steal the lock if possible and
|
|
114
|
+
// proceed regardless — worst case we race like the pre-lock code did.
|
|
115
|
+
try {
|
|
116
|
+
(0, node_fs_1.rmSync)(lockPath, { force: true });
|
|
117
|
+
}
|
|
118
|
+
catch { /* best effort */ }
|
|
119
|
+
try {
|
|
120
|
+
(0, node_fs_1.closeSync)((0, node_fs_1.openSync)(lockPath, 'wx'));
|
|
121
|
+
}
|
|
122
|
+
catch { /* best effort */ }
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
if (Date.now() - (0, node_fs_1.statSync)(lockPath).mtimeMs > LOCK_STALE_MS) {
|
|
127
|
+
// Holder presumed dead — take over.
|
|
128
|
+
(0, node_fs_1.rmSync)(lockPath, { force: true });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Lock vanished between open and stat — fall through to a short
|
|
134
|
+
// sleep (not an immediate retry) so persistent fs errors can't spin.
|
|
135
|
+
}
|
|
136
|
+
await new Promise((resolve) => { setTimeout(resolve, LOCK_POLL_MS); });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function missingCredentialsError() {
|
|
141
|
+
return new cli_1.CliError('Not authenticated. Provide an API key via --api-key or the DEVICE_CLOUD_API_KEY environment variable, or run `dcd login`.');
|
|
142
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export declare function getCliVersion(): string;
|
|
2
|
+
export type InstallMethod = 'binary' | 'npm';
|
|
3
|
+
export declare function getInstallMethod(): InstallMethod;
|
|
4
|
+
export declare function getUpgradeCommand(): string;
|
|
5
|
+
export declare class CliError extends Error {
|
|
6
|
+
exitCode: number;
|
|
7
|
+
constructor(message: string, exitCode?: number);
|
|
8
|
+
}
|
|
9
|
+
export interface Logger {
|
|
10
|
+
log(message: string): void;
|
|
11
|
+
warn(message: string): void;
|
|
12
|
+
error(message: Error | string, opts?: {
|
|
13
|
+
exit?: number;
|
|
14
|
+
json?: boolean;
|
|
15
|
+
}): never;
|
|
16
|
+
exit(code?: number): never;
|
|
17
|
+
}
|
|
18
|
+
export declare const logger: Logger;
|
|
19
|
+
/**
|
|
20
|
+
* Validate that a string flag value is one of the allowed options.
|
|
21
|
+
* Returns the value untouched on success; throws CliError otherwise.
|
|
22
|
+
*/
|
|
23
|
+
export declare function validateEnum<T extends string>(value: string | undefined, allowed: readonly T[], flagName: string): T | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Coerce a flag value (possibly a single string, array, or undefined) into a
|
|
26
|
+
* flat string array. Comma-separated values inside each entry are split out.
|
|
27
|
+
* Used for repeatable flags like --include-tags, --env, --metadata where citty
|
|
28
|
+
* surfaces a string (single use) or string[] (repeated).
|
|
29
|
+
*/
|
|
30
|
+
export declare function coerceArray(value: string | string[] | undefined, split?: boolean): string[];
|
|
31
|
+
/**
|
|
32
|
+
* Parse an integer flag. Returns undefined if the value is undefined/empty.
|
|
33
|
+
* Throws CliError if the value is not a valid integer.
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseIntFlag(value: string | undefined, flagName: string): number | undefined;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.logger = exports.CliError = void 0;
|
|
4
|
+
exports.getCliVersion = getCliVersion;
|
|
5
|
+
exports.getInstallMethod = getInstallMethod;
|
|
6
|
+
exports.getUpgradeCommand = getUpgradeCommand;
|
|
7
|
+
exports.validateEnum = validateEnum;
|
|
8
|
+
exports.coerceArray = coerceArray;
|
|
9
|
+
exports.parseIntFlag = parseIntFlag;
|
|
10
|
+
/**
|
|
11
|
+
* Shared helpers for command files.
|
|
12
|
+
* - Version lookup from package.json
|
|
13
|
+
* - Enum validation for string flags (citty 0.1.6 has no native enum)
|
|
14
|
+
* - Array coercion for repeatable flags with comma-separated values
|
|
15
|
+
* - A minimal Logger mirroring the oclif Command log/warn/error shape so call
|
|
16
|
+
* sites ported from oclif keep working.
|
|
17
|
+
*/
|
|
18
|
+
const telemetry_service_1 = require("../services/telemetry.service");
|
|
19
|
+
const styling_1 = require("./styling");
|
|
20
|
+
// Resolve version at runtime — avoids pulling package.json into the tsbuildinfo rootDir.
|
|
21
|
+
function getCliVersion() {
|
|
22
|
+
try {
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
24
|
+
const pkg = require('../../package.json');
|
|
25
|
+
return pkg.version;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return '0.0.0';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// The Bun runtime sets process.versions.bun; bun-compiled standalone binaries
|
|
32
|
+
// inherit this. Node-run installs (npm/pnpm/npx/tsx) don't expose it.
|
|
33
|
+
function getInstallMethod() {
|
|
34
|
+
return typeof process.versions.bun === 'string'
|
|
35
|
+
? 'binary'
|
|
36
|
+
: 'npm';
|
|
37
|
+
}
|
|
38
|
+
function getUpgradeCommand() {
|
|
39
|
+
return getInstallMethod() === 'binary'
|
|
40
|
+
? 'dcd upgrade'
|
|
41
|
+
: 'npm install -g @devicecloud.dev/dcd@latest';
|
|
42
|
+
}
|
|
43
|
+
class CliError extends Error {
|
|
44
|
+
exitCode;
|
|
45
|
+
constructor(message, exitCode = 1) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.exitCode = exitCode;
|
|
48
|
+
this.name = 'CliError';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.CliError = CliError;
|
|
52
|
+
exports.logger = {
|
|
53
|
+
log(message) {
|
|
54
|
+
// eslint-disable-next-line no-console
|
|
55
|
+
console.log(message);
|
|
56
|
+
},
|
|
57
|
+
warn(message) {
|
|
58
|
+
// eslint-disable-next-line no-console
|
|
59
|
+
console.warn(styling_1.symbols.warning + ' ' + message);
|
|
60
|
+
},
|
|
61
|
+
error(message, opts = {}) {
|
|
62
|
+
const text = message instanceof Error ? message.message : message;
|
|
63
|
+
if (opts.json) {
|
|
64
|
+
// When --json is active, emit failures as JSON on stdout so that
|
|
65
|
+
// scripts consuming the output never need to parse stderr too.
|
|
66
|
+
// eslint-disable-next-line no-console
|
|
67
|
+
console.log(JSON.stringify({ status: 'FAILED', error: text }, null, 2));
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
// The literal "Error:" prefix is important for tests and for grep-friendly
|
|
71
|
+
// CI logs — it survives color stripping and matches /error/i assertions.
|
|
72
|
+
// eslint-disable-next-line no-console
|
|
73
|
+
console.error(styling_1.symbols.error + ' Error: ' + text);
|
|
74
|
+
}
|
|
75
|
+
// process.exit bypasses beforeExit, so async fetch in telemetry.flush()
|
|
76
|
+
// would be killed mid-flight. flushSync uses curl to ship synchronously
|
|
77
|
+
// before we exit; it's a no-op if telemetry never reached configure().
|
|
78
|
+
const exitCode = opts.exit ?? 1;
|
|
79
|
+
telemetry_service_1.telemetry.recordCommandFailure({ error: message, exitCode });
|
|
80
|
+
telemetry_service_1.telemetry.flushSync();
|
|
81
|
+
process.exit(exitCode);
|
|
82
|
+
},
|
|
83
|
+
exit(code = 0) {
|
|
84
|
+
process.exit(code);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Validate that a string flag value is one of the allowed options.
|
|
89
|
+
* Returns the value untouched on success; throws CliError otherwise.
|
|
90
|
+
*/
|
|
91
|
+
function validateEnum(value, allowed, flagName) {
|
|
92
|
+
if (value === undefined || value === null || value === '')
|
|
93
|
+
return undefined;
|
|
94
|
+
if (!allowed.includes(value)) {
|
|
95
|
+
throw new CliError(`Invalid value for --${flagName}: "${value}". Expected one of: ${allowed.join(', ')}`);
|
|
96
|
+
}
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Coerce a flag value (possibly a single string, array, or undefined) into a
|
|
101
|
+
* flat string array. Comma-separated values inside each entry are split out.
|
|
102
|
+
* Used for repeatable flags like --include-tags, --env, --metadata where citty
|
|
103
|
+
* surfaces a string (single use) or string[] (repeated).
|
|
104
|
+
*/
|
|
105
|
+
function coerceArray(value, split = true) {
|
|
106
|
+
if (value === undefined)
|
|
107
|
+
return [];
|
|
108
|
+
const arr = Array.isArray(value) ? value : [value];
|
|
109
|
+
if (!split)
|
|
110
|
+
return arr;
|
|
111
|
+
return arr.flatMap((v) => v.split(','));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Parse an integer flag. Returns undefined if the value is undefined/empty.
|
|
115
|
+
* Throws CliError if the value is not a valid integer.
|
|
116
|
+
*/
|
|
117
|
+
function parseIntFlag(value, flagName) {
|
|
118
|
+
if (value === undefined || value === null || value === '')
|
|
119
|
+
return undefined;
|
|
120
|
+
// All integer flags (limit/offset/retry) are non-negative; also rejects
|
|
121
|
+
// trailing garbage that parseInt would silently accept ("20abc" -> 20).
|
|
122
|
+
const trimmed = String(value).trim();
|
|
123
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
124
|
+
throw new CliError(`Invalid integer value for --${flagName}: "${value}"`);
|
|
125
|
+
}
|
|
126
|
+
return Number.parseInt(trimmed, 10);
|
|
127
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AuthContext } from '../types/domain/auth.types';
|
|
1
2
|
export interface CompatibilityData {
|
|
2
3
|
android: Record<string, string[]>;
|
|
3
4
|
androidPlay: Record<string, string[]>;
|
|
@@ -8,5 +9,5 @@ export interface CompatibilityData {
|
|
|
8
9
|
supportedVersions: string[];
|
|
9
10
|
};
|
|
10
11
|
}
|
|
11
|
-
export declare function fetchCompatibilityData(apiUrl: string,
|
|
12
|
+
export declare function fetchCompatibilityData(apiUrl: string, auth: AuthContext): Promise<CompatibilityData>;
|
|
12
13
|
export declare function clearCompatibilityCache(): void;
|
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.fetchCompatibilityData = fetchCompatibilityData;
|
|
4
4
|
exports.clearCompatibilityCache = clearCompatibilityCache;
|
|
5
5
|
let cachedCompatibilityData = null;
|
|
6
|
-
async function fetchCompatibilityData(apiUrl,
|
|
6
|
+
async function fetchCompatibilityData(apiUrl, auth) {
|
|
7
7
|
if (cachedCompatibilityData) {
|
|
8
8
|
return cachedCompatibilityData;
|
|
9
9
|
}
|
|
@@ -11,7 +11,7 @@ async function fetchCompatibilityData(apiUrl, apiKey) {
|
|
|
11
11
|
const response = await fetch(`${apiUrl}/results/compatibility/data`, {
|
|
12
12
|
headers: {
|
|
13
13
|
'Content-Type': 'application/json',
|
|
14
|
-
|
|
14
|
+
...auth.headers,
|
|
15
15
|
},
|
|
16
16
|
method: 'GET',
|
|
17
17
|
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export declare const CONFIG_SCHEMA_VERSION = 1;
|
|
2
|
+
export interface StoredSession {
|
|
3
|
+
access_token: string;
|
|
4
|
+
refresh_token: string;
|
|
5
|
+
/** Unix epoch seconds. */
|
|
6
|
+
expires_at: number;
|
|
7
|
+
user_email: string;
|
|
8
|
+
user_id: string;
|
|
9
|
+
}
|
|
10
|
+
export interface StoredConfig {
|
|
11
|
+
version: number;
|
|
12
|
+
env: 'dev' | 'prod';
|
|
13
|
+
api_url: string;
|
|
14
|
+
supabase_url: string;
|
|
15
|
+
session?: StoredSession;
|
|
16
|
+
current_org_id?: string;
|
|
17
|
+
current_org_name?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function getConfigDir(): string;
|
|
20
|
+
export declare function getConfigPath(): string;
|
|
21
|
+
export declare function readConfig(): StoredConfig | null;
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the API URL a command should talk to. Precedence:
|
|
24
|
+
* 1. explicit --api-url flag
|
|
25
|
+
* 2. api_url stored by `dcd login` (honors the env the user logged into)
|
|
26
|
+
* 3. prod default
|
|
27
|
+
*
|
|
28
|
+
* Without this, session commands default to prod and a dev/staging Bearer
|
|
29
|
+
* token is rejected with a misleading "Invalid or expired JWT". `switch-org`
|
|
30
|
+
* has always done this; this helper extends it to every command.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolveApiUrl(flag: string | undefined): string;
|
|
33
|
+
export declare function writeConfig(config: StoredConfig): void;
|
|
34
|
+
export declare function clearConfig(): void;
|
|
35
|
+
export declare function configFileMode(): number | null;
|