@devicecloud.dev/dcd 5.0.0-beta.0 → 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 +35 -0
- package/dist/commands/artifacts.d.ts +28 -28
- package/dist/commands/artifacts.js +20 -23
- package/dist/commands/cloud.d.ts +57 -57
- package/dist/commands/cloud.js +173 -186
- package/dist/commands/list.d.ts +22 -22
- package/dist/commands/list.js +36 -38
- package/dist/commands/live.js +134 -127
- package/dist/commands/login.d.ts +11 -11
- package/dist/commands/login.js +46 -44
- package/dist/commands/logout.js +16 -18
- package/dist/commands/status.d.ts +11 -11
- package/dist/commands/status.js +45 -43
- package/dist/commands/switch-org.d.ts +7 -7
- package/dist/commands/switch-org.js +19 -21
- package/dist/commands/upgrade.js +29 -31
- package/dist/commands/upload.d.ts +10 -10
- package/dist/commands/upload.js +42 -43
- package/dist/commands/whoami.js +17 -20
- package/dist/config/environments.js +6 -12
- package/dist/config/flags/api.flags.js +1 -4
- package/dist/config/flags/binary.flags.js +1 -4
- package/dist/config/flags/device.flags.js +6 -9
- package/dist/config/flags/environment.flags.js +1 -4
- package/dist/config/flags/execution.flags.js +1 -4
- package/dist/config/flags/github.flags.js +1 -4
- package/dist/config/flags/output.flags.js +1 -4
- package/dist/constants.js +15 -18
- package/dist/gateways/api-gateway.d.ts +31 -6
- package/dist/gateways/api-gateway.js +70 -16
- package/dist/gateways/cli-auth-gateway.d.ts +1 -1
- package/dist/gateways/cli-auth-gateway.js +3 -6
- package/dist/gateways/realtime-gateway.d.ts +32 -0
- package/dist/gateways/realtime-gateway.js +103 -0
- package/dist/gateways/supabase-gateway.d.ts +1 -1
- package/dist/gateways/supabase-gateway.js +10 -14
- package/dist/index.js +41 -38
- 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 +32 -1
- package/dist/methods.js +125 -66
- package/dist/services/device-validation.service.d.ts +1 -1
- package/dist/services/device-validation.service.js +1 -5
- package/dist/services/execution-plan.service.js +14 -17
- package/dist/services/execution-plan.utils.js +15 -23
- package/dist/services/flow-paths.d.ts +17 -0
- package/dist/services/flow-paths.js +52 -0
- package/dist/services/metadata-extractor.service.js +22 -25
- package/dist/services/moropo.service.js +18 -20
- package/dist/services/report-download.service.d.ts +1 -1
- package/dist/services/report-download.service.js +5 -9
- package/dist/services/results-polling.service.d.ts +18 -3
- package/dist/services/results-polling.service.js +195 -108
- package/dist/services/telemetry.service.d.ts +10 -1
- package/dist/services/telemetry.service.js +40 -18
- package/dist/services/test-submission.service.d.ts +21 -4
- package/dist/services/test-submission.service.js +51 -34
- package/dist/services/version.service.d.ts +1 -1
- package/dist/services/version.service.js +1 -5
- package/dist/types/domain/auth.types.d.ts +8 -0
- package/dist/types/domain/auth.types.js +1 -2
- package/dist/types/domain/device.types.js +8 -11
- package/dist/types/domain/live.types.js +1 -2
- 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 +1 -1
- package/dist/utils/auth.js +27 -28
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +39 -0
- package/dist/utils/cli.js +18 -27
- package/dist/utils/compatibility.d.ts +1 -1
- package/dist/utils/compatibility.js +5 -7
- package/dist/utils/config-store.js +33 -43
- package/dist/utils/connectivity.js +1 -4
- package/dist/utils/expo.js +15 -21
- package/dist/utils/orgs.js +8 -12
- package/dist/utils/paths.js +2 -5
- package/dist/utils/progress.js +2 -5
- package/dist/utils/styling.d.ts +35 -37
- package/dist/utils/styling.js +52 -86
- package/dist/utils/ui.d.ts +41 -0
- package/dist/utils/ui.js +95 -0
- package/package.json +27 -24
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const path = require("node:path");
|
|
6
|
-
const methods_1 = require("../methods");
|
|
7
|
-
const paths_1 = require("../utils/paths");
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { compressFilesFromRelativePath } from '../methods.js';
|
|
4
|
+
import { toPortableRelativePath } from '../utils/paths.js';
|
|
8
5
|
const mimeTypeLookupByExtension = {
|
|
9
6
|
zip: 'application/zip',
|
|
10
7
|
};
|
|
11
8
|
/**
|
|
12
9
|
* Service for building test submission form data
|
|
13
10
|
*/
|
|
14
|
-
class TestSubmissionService {
|
|
11
|
+
export class TestSubmissionService {
|
|
15
12
|
/**
|
|
16
|
-
* Build
|
|
13
|
+
* Build the test-submission payload: the compressed flow zip plus every
|
|
14
|
+
* non-`file` field, each encoded exactly as it is sent today. The same
|
|
15
|
+
* `fields` feed both the new JSON `submitFlowTest` body and the legacy
|
|
16
|
+
* multipart `buildFormData`, guaranteeing byte-identical field encoding
|
|
17
|
+
* across both paths.
|
|
17
18
|
* @param config Test submission configuration
|
|
18
|
-
* @returns
|
|
19
|
+
* @returns The flow zip buffer, its SHA-256, and the string-encoded fields
|
|
19
20
|
*/
|
|
20
|
-
async
|
|
21
|
+
async buildTestPayload(config) {
|
|
21
22
|
const { appBinaryId, flowFile, executionPlan, commonRoot, cliVersion, env = [], metadata = [], googlePlay = false, androidApiLevel, androidDevice, androidNoSnapshot, iOSVersion, iOSDevice, name, runnerType, maestroVersion, deviceLocale, orientation, mitmHost, mitmPath, retry, continueOnFailure = true, report, showCrosshairs, maestroChromeOnboarding, raw, disableAnimations, debug = false, logger, } = config;
|
|
22
23
|
const { allExcludeTags, allIncludeTags, flowMetadata, flowOverrides, flowsToRun: testFileNames, referencedFiles, sequence, workspaceConfig, } = executionPlan;
|
|
23
24
|
const { flows: sequentialFlows = [] } = sequence ?? {};
|
|
24
|
-
const testFormData = new FormData();
|
|
25
25
|
const envObject = this.parseKeyValuePairs(env);
|
|
26
26
|
const metadataObject = this.parseKeyValuePairs(metadata);
|
|
27
27
|
if (Object.keys(envObject).length > 0) {
|
|
@@ -42,7 +42,7 @@ class TestSubmissionService {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
this.logDebug(debug, logger, `[DEBUG] Compressing files from path: ${flowFile}`);
|
|
45
|
-
const buffer = await
|
|
45
|
+
const buffer = await compressFilesFromRelativePath(flowFile?.endsWith('.yaml') || flowFile?.endsWith('.yml')
|
|
46
46
|
? path.dirname(flowFile)
|
|
47
47
|
: flowFile, [
|
|
48
48
|
...new Set([
|
|
@@ -53,19 +53,18 @@ class TestSubmissionService {
|
|
|
53
53
|
], commonRoot);
|
|
54
54
|
this.logDebug(debug, logger, `[DEBUG] Compressed file size: ${buffer.length} bytes`);
|
|
55
55
|
// Calculate SHA-256 hash of the flow ZIP
|
|
56
|
-
const sha =
|
|
56
|
+
const sha = createHash('sha256').update(buffer).digest('hex');
|
|
57
57
|
this.logDebug(debug, logger, `[DEBUG] Flow ZIP SHA-256: ${sha}`);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
testFormData.set('env', JSON.stringify(envObject));
|
|
58
|
+
// String-encoded fields, in the same order and with the same encoding as
|
|
59
|
+
// the legacy multipart FormData. Reused verbatim by both submission paths.
|
|
60
|
+
const fields = {};
|
|
61
|
+
fields.sha = sha;
|
|
62
|
+
fields.appBinaryId = appBinaryId;
|
|
63
|
+
fields.testFileNames = JSON.stringify(this.normalizePaths(testFileNames, commonRoot));
|
|
64
|
+
fields.flowMetadata = JSON.stringify(this.normalizePathMap(flowMetadata, commonRoot));
|
|
65
|
+
fields.testFileOverrides = JSON.stringify(this.normalizePathMap(flowOverrides, commonRoot));
|
|
66
|
+
fields.sequentialFlows = JSON.stringify(this.normalizePaths(sequentialFlows, commonRoot));
|
|
67
|
+
fields.env = JSON.stringify(envObject);
|
|
69
68
|
// Note: googlePlay is now included in configPayload below instead of as a separate field
|
|
70
69
|
// to work around a FormData parsing issue in the API
|
|
71
70
|
const targetPlatform = iOSDevice || iOSVersion ? 'ios' : 'android';
|
|
@@ -92,13 +91,13 @@ class TestSubmissionService {
|
|
|
92
91
|
disableAnimations: effectiveDisableAnimations,
|
|
93
92
|
version: cliVersion,
|
|
94
93
|
};
|
|
95
|
-
|
|
94
|
+
fields.config = JSON.stringify(configPayload);
|
|
96
95
|
if (Object.keys(metadataObject).length > 0) {
|
|
97
96
|
const metadataPayload = { userMetadata: metadataObject };
|
|
98
|
-
|
|
97
|
+
fields.metadata = JSON.stringify(metadataPayload);
|
|
99
98
|
this.logDebug(debug, logger, `[DEBUG] Sending metadata to API: ${JSON.stringify(metadataPayload)}`);
|
|
100
99
|
}
|
|
101
|
-
this.setOptionalFields(
|
|
100
|
+
this.setOptionalFields(fields, {
|
|
102
101
|
androidApiLevel,
|
|
103
102
|
androidDevice,
|
|
104
103
|
iOSDevice,
|
|
@@ -107,9 +106,28 @@ class TestSubmissionService {
|
|
|
107
106
|
runnerType,
|
|
108
107
|
});
|
|
109
108
|
if (workspaceConfig) {
|
|
110
|
-
|
|
109
|
+
fields.workspaceConfig = JSON.stringify(workspaceConfig);
|
|
110
|
+
}
|
|
111
|
+
return { buffer, fields, sha };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Wraps the payload fields and flow zip into multipart FormData for the
|
|
115
|
+
* legacy `POST /uploads/flow` fallback. `file` is set first to preserve the
|
|
116
|
+
* exact part ordering the old code produced.
|
|
117
|
+
* @param fields String-encoded fields from {@link buildTestPayload}
|
|
118
|
+
* @param buffer The compressed flow zip
|
|
119
|
+
* @returns FormData ready to be submitted to the multipart API
|
|
120
|
+
*/
|
|
121
|
+
buildFormData(fields, buffer) {
|
|
122
|
+
const formData = new FormData();
|
|
123
|
+
const blob = new Blob([buffer], {
|
|
124
|
+
type: mimeTypeLookupByExtension.zip,
|
|
125
|
+
});
|
|
126
|
+
formData.set('file', blob, 'flowFile.zip');
|
|
127
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
128
|
+
formData.set(key, value);
|
|
111
129
|
}
|
|
112
|
-
return
|
|
130
|
+
return formData;
|
|
113
131
|
}
|
|
114
132
|
logDebug(debug, logger, message) {
|
|
115
133
|
if (debug && logger) {
|
|
@@ -117,7 +135,7 @@ class TestSubmissionService {
|
|
|
117
135
|
}
|
|
118
136
|
}
|
|
119
137
|
normalizeFilePath(filePath, commonRoot) {
|
|
120
|
-
return
|
|
138
|
+
return toPortableRelativePath(filePath, commonRoot);
|
|
121
139
|
}
|
|
122
140
|
normalizePathMap(map, commonRoot) {
|
|
123
141
|
return Object.fromEntries(Object.entries(map).map(([key, value]) => [
|
|
@@ -137,12 +155,11 @@ class TestSubmissionService {
|
|
|
137
155
|
return acc;
|
|
138
156
|
}, {});
|
|
139
157
|
}
|
|
140
|
-
setOptionalFields(
|
|
158
|
+
setOptionalFields(target, fields) {
|
|
141
159
|
for (const [key, value] of Object.entries(fields)) {
|
|
142
160
|
if (value) {
|
|
143
|
-
|
|
161
|
+
target[key] = value.toString();
|
|
144
162
|
}
|
|
145
163
|
}
|
|
146
164
|
}
|
|
147
165
|
}
|
|
148
|
-
exports.TestSubmissionService = TestSubmissionService;
|
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.VersionService = void 0;
|
|
4
1
|
const DEFAULT_MANIFEST_URL = 'https://get.devicecloud.dev/latest.json';
|
|
5
2
|
const MANIFEST_TIMEOUT_MS = 3000;
|
|
6
3
|
/**
|
|
7
4
|
* Service for handling version validation and checking
|
|
8
5
|
*/
|
|
9
|
-
class VersionService {
|
|
6
|
+
export class VersionService {
|
|
10
7
|
/**
|
|
11
8
|
* Fetch the latest published CLI version from the release manifest.
|
|
12
9
|
* Works for both npm- and binary-installed users (no `npm` shell-out).
|
|
@@ -95,4 +92,3 @@ class VersionService {
|
|
|
95
92
|
return resolvedVersion;
|
|
96
93
|
}
|
|
97
94
|
}
|
|
98
|
-
exports.VersionService = VersionService;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DcdEnvName } from '../../config/environments.js';
|
|
1
2
|
/**
|
|
2
3
|
* Auth context threaded through gateways and services. Callers build this once
|
|
3
4
|
* (via resolveAuth) and the gateway spreads .headers into every fetch.
|
|
@@ -5,6 +6,13 @@
|
|
|
5
6
|
export interface AuthContext {
|
|
6
7
|
headers: Record<string, string>;
|
|
7
8
|
mode: 'apiKey' | 'bearer';
|
|
9
|
+
/**
|
|
10
|
+
* Supabase JWT — present when mode === 'bearer'. Lets realtime subscriptions
|
|
11
|
+
* authenticate the socket (RLS) without re-reading the session from disk.
|
|
12
|
+
*/
|
|
13
|
+
accessToken?: string;
|
|
14
|
+
/** Environment the session belongs to — present when mode === 'bearer'. */
|
|
15
|
+
env?: DcdEnvName;
|
|
8
16
|
/** Present when mode === 'bearer'. */
|
|
9
17
|
orgId?: string;
|
|
10
18
|
/** Present when mode === 'bearer'. */
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1
|
+
export {};
|
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* Device type definitions - should be kept in sync with API
|
|
4
3
|
* @see /Users/riglar/repos/dcd/api/src/common/types/device.types.ts
|
|
5
4
|
*/
|
|
6
|
-
|
|
7
|
-
exports.EAndroidApiLevels = exports.EiOSVersions = exports.EAndroidDevices = exports.EiOSDevices = void 0;
|
|
8
|
-
var EiOSDevices;
|
|
5
|
+
export var EiOSDevices;
|
|
9
6
|
(function (EiOSDevices) {
|
|
10
7
|
EiOSDevices["ipad-pro-6th-gen"] = "ipad-pro-6th-gen";
|
|
11
8
|
EiOSDevices["iphone-14"] = "iphone-14";
|
|
@@ -16,23 +13,23 @@ var EiOSDevices;
|
|
|
16
13
|
EiOSDevices["iphone-16-plus"] = "iphone-16-plus";
|
|
17
14
|
EiOSDevices["iphone-16-pro"] = "iphone-16-pro";
|
|
18
15
|
EiOSDevices["iphone-16-pro-max"] = "iphone-16-pro-max";
|
|
19
|
-
})(EiOSDevices || (
|
|
20
|
-
var EAndroidDevices;
|
|
16
|
+
})(EiOSDevices || (EiOSDevices = {}));
|
|
17
|
+
export var EAndroidDevices;
|
|
21
18
|
(function (EAndroidDevices) {
|
|
22
19
|
EAndroidDevices["generic-tablet"] = "generic-tablet";
|
|
23
20
|
EAndroidDevices["pixel-6"] = "pixel-6";
|
|
24
21
|
EAndroidDevices["pixel-6-pro"] = "pixel-6-pro";
|
|
25
22
|
EAndroidDevices["pixel-7"] = "pixel-7";
|
|
26
23
|
EAndroidDevices["pixel-7-pro"] = "pixel-7-pro";
|
|
27
|
-
})(EAndroidDevices || (
|
|
28
|
-
var EiOSVersions;
|
|
24
|
+
})(EAndroidDevices || (EAndroidDevices = {}));
|
|
25
|
+
export var EiOSVersions;
|
|
29
26
|
(function (EiOSVersions) {
|
|
30
27
|
EiOSVersions["eighteen"] = "18";
|
|
31
28
|
EiOSVersions["seventeen"] = "17";
|
|
32
29
|
EiOSVersions["sixteen"] = "16";
|
|
33
30
|
EiOSVersions["twentySix"] = "26";
|
|
34
|
-
})(EiOSVersions || (
|
|
35
|
-
var EAndroidApiLevels;
|
|
31
|
+
})(EiOSVersions || (EiOSVersions = {}));
|
|
32
|
+
export var EAndroidApiLevels;
|
|
36
33
|
(function (EAndroidApiLevels) {
|
|
37
34
|
EAndroidApiLevels["thirty"] = "30";
|
|
38
35
|
EAndroidApiLevels["thirtyFive"] = "35";
|
|
@@ -42,4 +39,4 @@ var EAndroidApiLevels;
|
|
|
42
39
|
EAndroidApiLevels["thirtyThree"] = "33";
|
|
43
40
|
EAndroidApiLevels["thirtyTwo"] = "32";
|
|
44
41
|
EAndroidApiLevels["twentyNine"] = "29";
|
|
45
|
-
})(EAndroidApiLevels || (
|
|
42
|
+
})(EAndroidApiLevels || (EAndroidApiLevels = {}));
|
package/dist/types/index.d.ts
CHANGED
package/dist/types/index.js
CHANGED
|
@@ -1,24 +1,8 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* Centralized type exports.
|
|
4
3
|
* Types are organized by domain in subdirectories.
|
|
5
4
|
*/
|
|
6
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
-
if (k2 === undefined) k2 = k;
|
|
8
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
-
}
|
|
12
|
-
Object.defineProperty(o, k2, desc);
|
|
13
|
-
}) : (function(o, m, k, k2) {
|
|
14
|
-
if (k2 === undefined) k2 = k;
|
|
15
|
-
o[k2] = m[k];
|
|
16
|
-
}));
|
|
17
|
-
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
18
|
-
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
19
|
-
};
|
|
20
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
5
|
// Domain-specific types
|
|
22
|
-
|
|
6
|
+
export * from './domain/device.types.js';
|
|
23
7
|
// Generated types from OpenAPI schema
|
|
24
|
-
|
|
8
|
+
export * from './generated/schema.types.js';
|
package/dist/types.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1
|
+
export {};
|
package/dist/utils/auth.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AuthContext } from '../types/domain/auth.types';
|
|
1
|
+
import type { AuthContext } from '../types/domain/auth.types.js';
|
|
2
2
|
export interface ResolveAuthOptions {
|
|
3
3
|
apiKeyFlag: string | undefined;
|
|
4
4
|
/** When true, bypass stored session entirely (for `dcd login` itself). */
|
package/dist/utils/auth.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.resolveAuth = resolveAuth;
|
|
4
1
|
/**
|
|
5
2
|
* Resolves which credential a command should use and returns the fetch headers
|
|
6
3
|
* to send. Precedence: --api-key flag > DEVICE_CLOUD_API_KEY env > stored
|
|
@@ -10,19 +7,19 @@ exports.resolveAuth = resolveAuth;
|
|
|
10
7
|
* use the returned AuthContext for the duration of the command — one resolve
|
|
11
8
|
* per invocation, not per request.
|
|
12
9
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
import { closeSync, openSync, rmSync, statSync } from 'node:fs';
|
|
11
|
+
import { ENVIRONMENTS } from '../config/environments.js';
|
|
12
|
+
import { CliAuthGateway } from '../gateways/cli-auth-gateway.js';
|
|
13
|
+
import { telemetry } from '../services/telemetry.service.js';
|
|
14
|
+
import { CliError } from './cli.js';
|
|
15
|
+
import { getConfigPath, readConfig, writeConfig, } from './config-store.js';
|
|
19
16
|
const REFRESH_SKEW_SECONDS = 60;
|
|
20
17
|
// Refresh lock tuning: a refresh is a single HTTP round-trip, so anything
|
|
21
18
|
// holding the lock longer than this is presumed dead.
|
|
22
19
|
const LOCK_STALE_MS = 10_000;
|
|
23
20
|
const LOCK_WAIT_MS = 10_000;
|
|
24
21
|
const LOCK_POLL_MS = 250;
|
|
25
|
-
async function resolveAuth(opts) {
|
|
22
|
+
export async function resolveAuth(opts) {
|
|
26
23
|
if (!opts.sessionOnly) {
|
|
27
24
|
const flag = opts.apiKeyFlag?.trim();
|
|
28
25
|
if (flag) {
|
|
@@ -30,7 +27,7 @@ async function resolveAuth(opts) {
|
|
|
30
27
|
mode: 'apiKey',
|
|
31
28
|
headers: { 'x-app-api-key': flag },
|
|
32
29
|
};
|
|
33
|
-
|
|
30
|
+
telemetry.configure({ auth });
|
|
34
31
|
return auth;
|
|
35
32
|
}
|
|
36
33
|
const env = process.env.DEVICE_CLOUD_API_KEY?.trim();
|
|
@@ -39,14 +36,14 @@ async function resolveAuth(opts) {
|
|
|
39
36
|
mode: 'apiKey',
|
|
40
37
|
headers: { 'x-app-api-key': env },
|
|
41
38
|
};
|
|
42
|
-
|
|
39
|
+
telemetry.configure({ auth });
|
|
43
40
|
return auth;
|
|
44
41
|
}
|
|
45
42
|
}
|
|
46
43
|
if (opts.skipSession) {
|
|
47
44
|
throw missingCredentialsError();
|
|
48
45
|
}
|
|
49
|
-
let config =
|
|
46
|
+
let config = readConfig();
|
|
50
47
|
if (!config?.session) {
|
|
51
48
|
throw missingCredentialsError();
|
|
52
49
|
}
|
|
@@ -56,10 +53,12 @@ async function resolveAuth(opts) {
|
|
|
56
53
|
({ config, session } = await refreshSessionWithLock(config));
|
|
57
54
|
}
|
|
58
55
|
if (!config.current_org_id) {
|
|
59
|
-
throw new
|
|
56
|
+
throw new CliError('No active organization set. Run `dcd switch-org <slug>` to pick one.');
|
|
60
57
|
}
|
|
61
58
|
const auth = {
|
|
62
59
|
mode: 'bearer',
|
|
60
|
+
accessToken: session.access_token,
|
|
61
|
+
env: config.env,
|
|
63
62
|
orgId: config.current_org_id,
|
|
64
63
|
userEmail: session.user_email,
|
|
65
64
|
headers: {
|
|
@@ -67,7 +66,7 @@ async function resolveAuth(opts) {
|
|
|
67
66
|
'x-dcd-org': config.current_org_id,
|
|
68
67
|
},
|
|
69
68
|
};
|
|
70
|
-
|
|
69
|
+
telemetry.configure({ auth, apiUrl: config.api_url });
|
|
71
70
|
return auth;
|
|
72
71
|
}
|
|
73
72
|
/**
|
|
@@ -76,27 +75,27 @@ async function resolveAuth(opts) {
|
|
|
76
75
|
* Supabase refresh token — token rotation would revoke the session family.
|
|
77
76
|
*/
|
|
78
77
|
async function refreshSessionWithLock(initial) {
|
|
79
|
-
const lockPath = `${
|
|
78
|
+
const lockPath = `${getConfigPath()}.lock`;
|
|
80
79
|
await acquireRefreshLock(lockPath);
|
|
81
80
|
try {
|
|
82
81
|
// Re-read: another process may have refreshed while we waited.
|
|
83
|
-
const current =
|
|
82
|
+
const current = readConfig() ?? initial;
|
|
84
83
|
const session = current.session ?? initial.session;
|
|
85
84
|
const now = Math.floor(Date.now() / 1000);
|
|
86
85
|
if (session.expires_at > now + REFRESH_SKEW_SECONDS) {
|
|
87
86
|
return { config: current, session };
|
|
88
87
|
}
|
|
89
|
-
const { anonKey } =
|
|
90
|
-
const refreshed = await
|
|
88
|
+
const { anonKey } = ENVIRONMENTS[current.env].supabase;
|
|
89
|
+
const refreshed = await CliAuthGateway.refresh(current.supabase_url, anonKey, session);
|
|
91
90
|
// Re-read again and merge only `session` so a concurrent `switch-org`
|
|
92
91
|
// write (org fields) isn't reverted by our pre-refresh snapshot.
|
|
93
|
-
const merged = { ...(
|
|
94
|
-
|
|
92
|
+
const merged = { ...(readConfig() ?? current), session: refreshed };
|
|
93
|
+
writeConfig(merged);
|
|
95
94
|
return { config: merged, session: refreshed };
|
|
96
95
|
}
|
|
97
96
|
finally {
|
|
98
97
|
try {
|
|
99
|
-
|
|
98
|
+
rmSync(lockPath, { force: true });
|
|
100
99
|
}
|
|
101
100
|
catch { /* best effort */ }
|
|
102
101
|
}
|
|
@@ -105,7 +104,7 @@ async function acquireRefreshLock(lockPath) {
|
|
|
105
104
|
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
106
105
|
for (;;) {
|
|
107
106
|
try {
|
|
108
|
-
|
|
107
|
+
closeSync(openSync(lockPath, 'wx'));
|
|
109
108
|
return;
|
|
110
109
|
}
|
|
111
110
|
catch {
|
|
@@ -113,19 +112,19 @@ async function acquireRefreshLock(lockPath) {
|
|
|
113
112
|
// Don't hang the command forever: steal the lock if possible and
|
|
114
113
|
// proceed regardless — worst case we race like the pre-lock code did.
|
|
115
114
|
try {
|
|
116
|
-
|
|
115
|
+
rmSync(lockPath, { force: true });
|
|
117
116
|
}
|
|
118
117
|
catch { /* best effort */ }
|
|
119
118
|
try {
|
|
120
|
-
|
|
119
|
+
closeSync(openSync(lockPath, 'wx'));
|
|
121
120
|
}
|
|
122
121
|
catch { /* best effort */ }
|
|
123
122
|
return;
|
|
124
123
|
}
|
|
125
124
|
try {
|
|
126
|
-
if (Date.now() -
|
|
125
|
+
if (Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS) {
|
|
127
126
|
// Holder presumed dead — take over.
|
|
128
|
-
|
|
127
|
+
rmSync(lockPath, { force: true });
|
|
129
128
|
continue;
|
|
130
129
|
}
|
|
131
130
|
}
|
|
@@ -138,5 +137,5 @@ async function acquireRefreshLock(lockPath) {
|
|
|
138
137
|
}
|
|
139
138
|
}
|
|
140
139
|
function missingCredentialsError() {
|
|
141
|
-
return new
|
|
140
|
+
return new CliError('Not authenticated. Provide an API key via --api-key or the DEVICE_CLOUD_API_KEY environment variable, or run `dcd login`.');
|
|
142
141
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort detection of non-interactive / CI environments. Used to suppress
|
|
3
|
+
* interactive niceties (e.g. the `dcd login` nudge) that only make sense for a
|
|
4
|
+
* human at a terminal. Dependency-free on purpose — these are the env vars the
|
|
5
|
+
* major providers set, plus a TTY check for piped/redirected output.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Returns true when running under CI or otherwise non-interactively (no TTY on
|
|
9
|
+
* stdout, e.g. output piped to a file). A truthy value for any known CI env var
|
|
10
|
+
* counts — providers set `CI=true`, but a bare presence check is the safe net.
|
|
11
|
+
*/
|
|
12
|
+
export declare function isCI(): boolean;
|
package/dist/utils/ci.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort detection of non-interactive / CI environments. Used to suppress
|
|
3
|
+
* interactive niceties (e.g. the `dcd login` nudge) that only make sense for a
|
|
4
|
+
* human at a terminal. Dependency-free on purpose — these are the env vars the
|
|
5
|
+
* major providers set, plus a TTY check for piped/redirected output.
|
|
6
|
+
*/
|
|
7
|
+
// Generic + per-provider markers. `CI` is set by virtually every provider;
|
|
8
|
+
// the rest cover platforms that historically didn't set it.
|
|
9
|
+
const CI_ENV_VARS = [
|
|
10
|
+
'CI',
|
|
11
|
+
'CONTINUOUS_INTEGRATION',
|
|
12
|
+
'BUILD_NUMBER',
|
|
13
|
+
'GITHUB_ACTIONS',
|
|
14
|
+
'GITLAB_CI',
|
|
15
|
+
'CIRCLECI',
|
|
16
|
+
'TRAVIS',
|
|
17
|
+
'BUILDKITE',
|
|
18
|
+
'DRONE',
|
|
19
|
+
'TEAMCITY_VERSION',
|
|
20
|
+
'TF_BUILD', // Azure Pipelines
|
|
21
|
+
'JENKINS_URL',
|
|
22
|
+
'BITBUCKET_BUILD_NUMBER',
|
|
23
|
+
'APPVEYOR',
|
|
24
|
+
'CODEBUILD_BUILD_ID',
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
27
|
+
* Returns true when running under CI or otherwise non-interactively (no TTY on
|
|
28
|
+
* stdout, e.g. output piped to a file). A truthy value for any known CI env var
|
|
29
|
+
* counts — providers set `CI=true`, but a bare presence check is the safe net.
|
|
30
|
+
*/
|
|
31
|
+
export function isCI() {
|
|
32
|
+
for (const name of CI_ENV_VARS) {
|
|
33
|
+
const value = process.env[name];
|
|
34
|
+
if (value !== undefined && value !== '' && value !== 'false' && value !== '0') {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return !process.stdout.isTTY;
|
|
39
|
+
}
|
package/dist/utils/cli.js
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
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
1
|
/**
|
|
11
2
|
* Shared helpers for command files.
|
|
12
3
|
* - Version lookup from package.json
|
|
@@ -15,13 +6,14 @@ exports.parseIntFlag = parseIntFlag;
|
|
|
15
6
|
* - A minimal Logger mirroring the oclif Command log/warn/error shape so call
|
|
16
7
|
* sites ported from oclif keep working.
|
|
17
8
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
9
|
+
import { readFileSync } from 'node:fs';
|
|
10
|
+
import { telemetry } from '../services/telemetry.service.js';
|
|
11
|
+
import { symbols } from './styling.js';
|
|
12
|
+
// Resolve version at runtime — read the file rather than importing it, so
|
|
13
|
+
// package.json never gets pulled into the tsc program / dist rootDir.
|
|
14
|
+
export function getCliVersion() {
|
|
22
15
|
try {
|
|
23
|
-
|
|
24
|
-
const pkg = require('../../package.json');
|
|
16
|
+
const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
25
17
|
return pkg.version;
|
|
26
18
|
}
|
|
27
19
|
catch {
|
|
@@ -30,17 +22,17 @@ function getCliVersion() {
|
|
|
30
22
|
}
|
|
31
23
|
// The Bun runtime sets process.versions.bun; bun-compiled standalone binaries
|
|
32
24
|
// inherit this. Node-run installs (npm/pnpm/npx/tsx) don't expose it.
|
|
33
|
-
function getInstallMethod() {
|
|
25
|
+
export function getInstallMethod() {
|
|
34
26
|
return typeof process.versions.bun === 'string'
|
|
35
27
|
? 'binary'
|
|
36
28
|
: 'npm';
|
|
37
29
|
}
|
|
38
|
-
function getUpgradeCommand() {
|
|
30
|
+
export function getUpgradeCommand() {
|
|
39
31
|
return getInstallMethod() === 'binary'
|
|
40
32
|
? 'dcd upgrade'
|
|
41
33
|
: 'npm install -g @devicecloud.dev/dcd@latest';
|
|
42
34
|
}
|
|
43
|
-
class CliError extends Error {
|
|
35
|
+
export class CliError extends Error {
|
|
44
36
|
exitCode;
|
|
45
37
|
constructor(message, exitCode = 1) {
|
|
46
38
|
super(message);
|
|
@@ -48,15 +40,14 @@ class CliError extends Error {
|
|
|
48
40
|
this.name = 'CliError';
|
|
49
41
|
}
|
|
50
42
|
}
|
|
51
|
-
|
|
52
|
-
exports.logger = {
|
|
43
|
+
export const logger = {
|
|
53
44
|
log(message) {
|
|
54
45
|
// eslint-disable-next-line no-console
|
|
55
46
|
console.log(message);
|
|
56
47
|
},
|
|
57
48
|
warn(message) {
|
|
58
49
|
// eslint-disable-next-line no-console
|
|
59
|
-
console.warn(
|
|
50
|
+
console.warn(symbols.warning + ' ' + message);
|
|
60
51
|
},
|
|
61
52
|
error(message, opts = {}) {
|
|
62
53
|
const text = message instanceof Error ? message.message : message;
|
|
@@ -70,14 +61,14 @@ exports.logger = {
|
|
|
70
61
|
// The literal "Error:" prefix is important for tests and for grep-friendly
|
|
71
62
|
// CI logs — it survives color stripping and matches /error/i assertions.
|
|
72
63
|
// eslint-disable-next-line no-console
|
|
73
|
-
console.error(
|
|
64
|
+
console.error(symbols.error + ' Error: ' + text);
|
|
74
65
|
}
|
|
75
66
|
// process.exit bypasses beforeExit, so async fetch in telemetry.flush()
|
|
76
67
|
// would be killed mid-flight. flushSync uses curl to ship synchronously
|
|
77
68
|
// before we exit; it's a no-op if telemetry never reached configure().
|
|
78
69
|
const exitCode = opts.exit ?? 1;
|
|
79
|
-
|
|
80
|
-
|
|
70
|
+
telemetry.recordCommandFailure({ error: message, exitCode });
|
|
71
|
+
telemetry.flushSync();
|
|
81
72
|
process.exit(exitCode);
|
|
82
73
|
},
|
|
83
74
|
exit(code = 0) {
|
|
@@ -88,7 +79,7 @@ exports.logger = {
|
|
|
88
79
|
* Validate that a string flag value is one of the allowed options.
|
|
89
80
|
* Returns the value untouched on success; throws CliError otherwise.
|
|
90
81
|
*/
|
|
91
|
-
function validateEnum(value, allowed, flagName) {
|
|
82
|
+
export function validateEnum(value, allowed, flagName) {
|
|
92
83
|
if (value === undefined || value === null || value === '')
|
|
93
84
|
return undefined;
|
|
94
85
|
if (!allowed.includes(value)) {
|
|
@@ -102,7 +93,7 @@ function validateEnum(value, allowed, flagName) {
|
|
|
102
93
|
* Used for repeatable flags like --include-tags, --env, --metadata where citty
|
|
103
94
|
* surfaces a string (single use) or string[] (repeated).
|
|
104
95
|
*/
|
|
105
|
-
function coerceArray(value, split = true) {
|
|
96
|
+
export function coerceArray(value, split = true) {
|
|
106
97
|
if (value === undefined)
|
|
107
98
|
return [];
|
|
108
99
|
const arr = Array.isArray(value) ? value : [value];
|
|
@@ -114,7 +105,7 @@ function coerceArray(value, split = true) {
|
|
|
114
105
|
* Parse an integer flag. Returns undefined if the value is undefined/empty.
|
|
115
106
|
* Throws CliError if the value is not a valid integer.
|
|
116
107
|
*/
|
|
117
|
-
function parseIntFlag(value, flagName) {
|
|
108
|
+
export function parseIntFlag(value, flagName) {
|
|
118
109
|
if (value === undefined || value === null || value === '')
|
|
119
110
|
return undefined;
|
|
120
111
|
// All integer flags (limit/offset/retry) are non-negative; also rejects
|