@devicecloud.dev/dcd 3.3.6 → 3.3.8

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.
@@ -57,6 +57,17 @@ export default class Cloud extends Command {
57
57
  quiet: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
58
58
  retry: import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
59
59
  report: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
60
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
60
61
  };
61
- run(): Promise<void>;
62
+ static enableJsonFlag: boolean;
63
+ private versionCheck;
64
+ run(): Promise<{
65
+ uploadId: string;
66
+ consoleUrl: string;
67
+ status: string;
68
+ tests: {
69
+ name: string;
70
+ status: string;
71
+ }[];
72
+ } | undefined>;
62
73
  }
@@ -63,15 +63,30 @@ class Cloud extends core_1.Command {
63
63
  static description = `Test a Flow or set of Flows on devicecloud.dev (https://devicecloud.dev)\nProvide your application file and a folder with Maestro flows to run them in parallel on multiple devices in devicecloud.dev\nThe command will block until all analyses have completed`;
64
64
  static examples = ['<%= config.bin %> <%= command.id %>'];
65
65
  static flags = constants_1.flags;
66
+ static enableJsonFlag = true;
67
+ versionCheck = async () => {
68
+ const versionResponse = await fetch('https://registry.npmjs.org/@devicecloud.dev/dcd/latest');
69
+ const versionResponseJson = await versionResponse.json();
70
+ const latestVersion = versionResponseJson.version;
71
+ if (latestVersion !== this.config.version) {
72
+ this.log(`
73
+ -------------------
74
+ A new version of the devicecloud.dev CLI is available: ${latestVersion}
75
+ Run 'npm install -g @devicecloud.dev/dcd@latest' to update to the latest version
76
+ -------------------
77
+ `);
78
+ }
79
+ };
66
80
  async run() {
81
+ let output = null;
67
82
  try {
68
83
  const [major] = process.versions.node.split('.').map(Number);
69
84
  if (major < 18) {
70
85
  throw new Error(`You are using node version ${major}. DCD requires node version 18 or later`);
71
86
  }
72
- await (0, methods_1.versionCheck)(this.config.version);
87
+ await this.versionCheck();
73
88
  const { args, flags, raw } = await this.parse(Cloud);
74
- const { 'additional-app-binary-ids': nonFlatAdditionalAppBinaryIds, 'additional-app-files': nonFlatAdditionalAppFiles, 'android-api-level': androidApiLevel, 'android-device': androidDevice, apiKey: apiKeyFlag, apiUrl, 'app-binary-id': appBinaryId, 'app-file': appFile, async, 'device-locale': deviceLocale, 'download-artifacts': downloadArtifacts, env, 'exclude-flows': excludeFlows, 'exclude-tags': excludeTags, flows, 'google-play': googlePlay, 'include-tags': includeTags, 'ignore-sha-check': ignoreShaCheck, 'ios-device': iOSDevice, 'ios-version': iOSVersion, 'maestro-version': maestroVersion, name, orientation, quiet, retry, report, 'x86-arch': x86Arch, ...rest } = flags;
89
+ const { 'additional-app-binary-ids': nonFlatAdditionalAppBinaryIds, 'additional-app-files': nonFlatAdditionalAppFiles, 'android-api-level': androidApiLevel, 'android-device': androidDevice, apiKey: apiKeyFlag, apiUrl, 'app-binary-id': appBinaryId, 'app-file': appFile, async, 'device-locale': deviceLocale, 'download-artifacts': downloadArtifacts, env, 'exclude-flows': excludeFlows, 'exclude-tags': excludeTags, flows, 'google-play': googlePlay, 'include-tags': includeTags, 'ignore-sha-check': ignoreShaCheck, 'ios-device': iOSDevice, 'ios-version': iOSVersion, 'maestro-version': maestroVersion, name, orientation, quiet, retry, report, 'x86-arch': x86Arch, json, ...rest } = flags;
75
90
  const apiKey = apiKeyFlag || process.env.DEVICE_CLOUD_API_KEY;
76
91
  if (!apiKey)
77
92
  throw new Error('You must provide an API key via --api-key flag or DEVICE_CLOUD_API_KEY environment variable');
@@ -181,12 +196,12 @@ class Cloud extends core_1.Command {
181
196
  if (!finalBinaryId) {
182
197
  if (!finalAppFile)
183
198
  throw new Error('You must provide either an app binary id or an app file');
184
- const binaryId = await (0, methods_1.uploadBinary)(finalAppFile, apiUrl, apiKey, ignoreShaCheck);
199
+ const binaryId = await (0, methods_1.uploadBinary)(finalAppFile, apiUrl, apiKey, ignoreShaCheck, !json);
185
200
  finalBinaryId = binaryId;
186
201
  }
187
202
  let uploadedBinaryIds = [];
188
203
  if (additionalAppFiles?.length) {
189
- uploadedBinaryIds = await (0, methods_1.uploadBinaries)(additionalAppFiles, apiUrl, apiKey, ignoreShaCheck);
204
+ uploadedBinaryIds = await (0, methods_1.uploadBinaries)(additionalAppFiles, apiUrl, apiKey, ignoreShaCheck, !json);
190
205
  finalAdditionalBinaryIds = [
191
206
  ...finalAdditionalBinaryIds,
192
207
  ...uploadedBinaryIds,
@@ -269,14 +284,32 @@ class Cloud extends core_1.Command {
269
284
  .join(', ')}\n`);
270
285
  this.log('Run triggered, you can access the results at:');
271
286
  const url = `https://console.devicecloud.dev/results?upload=${results[0].test_upload_id}&result=${results[0].id}`;
272
- core_1.ux.url(url, url);
287
+ this.log(url);
288
+ this.log(`\n`);
289
+ this.log(`Your upload ID is: ${results[0].test_upload_id}`);
290
+ this.log(`Poll upload status using: dcd status --api-key ... --upload-id ${results[0].test_upload_id}`);
273
291
  if (async) {
292
+ if (json) {
293
+ return {
294
+ uploadId: results[0].test_upload_id,
295
+ consoleUrl: url,
296
+ status: 'PENDING',
297
+ tests: results.map((r) => ({
298
+ name: r.test_file_name,
299
+ status: r.status,
300
+ })),
301
+ };
302
+ }
274
303
  this.log('Not waiting for results as async flag is set to true');
275
304
  return;
276
305
  }
277
306
  // poll for the run status every 5 seconds
278
- core_1.ux.action.start('Waiting for results', 'Initializing', { stdout: true });
279
- this.log('\nYou can safely close this terminal and the tests will continue\n');
307
+ if (!json) {
308
+ core_1.ux.action.start('Waiting for results', 'Initializing', {
309
+ stdout: true,
310
+ });
311
+ this.log('\nYou can safely close this terminal and the tests will continue\n');
312
+ }
280
313
  let sequentialPollFaillures = 0;
281
314
  await new Promise((resolve, reject) => {
282
315
  const intervalId = setInterval(async () => {
@@ -287,7 +320,7 @@ class Cloud extends core_1.Command {
287
320
  if (!updatedResults) {
288
321
  throw new Error('no results');
289
322
  }
290
- if (!quiet) {
323
+ if (!quiet || !json) {
291
324
  core_1.ux.action.status =
292
325
  '\nStatus Test\n─────────── ───────────────';
293
326
  for (const { retry_of: isRetry, status, test_file_name: test, } of updatedResults) {
@@ -295,18 +328,20 @@ class Cloud extends core_1.Command {
295
328
  }
296
329
  }
297
330
  if (updatedResults.every((result) => !['PENDING', 'RUNNING'].includes(result.status))) {
298
- core_1.ux.action.stop('completed');
299
- this.log('\n');
300
- (0, cli_ux_1.table)(updatedResults, {
301
- status: { get: (row) => row.status },
302
- test: {
303
- get: (row) => `${row.test_file_name} ${row.retry_of ? '(retry)' : ''}`,
304
- },
305
- }, { printLine: this.log.bind(this) });
306
- this.log('\n');
307
- this.log('Run completed, you can access the results at:');
308
- core_1.ux.url(url, url);
309
- this.log('\n');
331
+ if (!json) {
332
+ core_1.ux.action.stop('completed');
333
+ this.log('\n');
334
+ (0, cli_ux_1.table)(updatedResults, {
335
+ status: { get: (row) => row.status },
336
+ test: {
337
+ get: (row) => `${row.test_file_name} ${row.retry_of ? '(retry)' : ''}`,
338
+ },
339
+ }, { printLine: this.log.bind(this) });
340
+ this.log('\n');
341
+ this.log('Run completed, you can access the results at:');
342
+ this.log(url);
343
+ this.log('\n');
344
+ }
310
345
  clearInterval(intervalId);
311
346
  if (downloadArtifacts) {
312
347
  try {
@@ -330,8 +365,32 @@ class Cloud extends core_1.Command {
330
365
  return result.id === Math.max(...tries.map((t) => t.id));
331
366
  });
332
367
  if (resultsWithoutEarlierTries.some((result) => result.status === 'FAILED')) {
368
+ if (json) {
369
+ output = {
370
+ uploadId: results[0].test_upload_id,
371
+ consoleUrl: url,
372
+ status: 'FAILED',
373
+ tests: resultsWithoutEarlierTries.map((r) => ({
374
+ name: r.test_file_name,
375
+ status: r.status,
376
+ })),
377
+ };
378
+ }
333
379
  reject(new Error('RUN_FAILED'));
334
380
  }
381
+ else {
382
+ if (json) {
383
+ output = {
384
+ uploadId: results[0].test_upload_id,
385
+ consoleUrl: url,
386
+ status: 'PASSED',
387
+ tests: resultsWithoutEarlierTries.map((r) => ({
388
+ name: r.test_file_name,
389
+ status: r.status,
390
+ })),
391
+ };
392
+ }
393
+ }
335
394
  sequentialPollFaillures = 0;
336
395
  resolve();
337
396
  }
@@ -354,6 +413,11 @@ class Cloud extends core_1.Command {
354
413
  }
355
414
  this.error(error, { exit: 1 });
356
415
  }
416
+ finally {
417
+ if (output) {
418
+ return output;
419
+ }
420
+ }
357
421
  }
358
422
  }
359
423
  exports.default = Cloud;
@@ -1,7 +1,18 @@
1
1
  import { Command } from '@oclif/core';
2
+ type StatusResponse = {
3
+ status: 'FAILED' | 'PASSED' | 'CANCELLED' | 'PENDING' | 'RUNNING';
4
+ tests: {
5
+ name: string;
6
+ status: 'FAILED' | 'PASSED' | 'CANCELLED' | 'PENDING' | 'RUNNING';
7
+ }[];
8
+ uploadId?: string;
9
+ appBinaryId?: string;
10
+ consoleUrl?: string;
11
+ };
2
12
  export default class Status extends Command {
3
13
  static description: string;
4
14
  static examples: string[];
15
+ static enableJsonFlag: boolean;
5
16
  static flags: {
6
17
  json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
7
18
  name: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
@@ -10,5 +21,6 @@ export default class Status extends Command {
10
21
  apiUrl: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
11
22
  };
12
23
  private getStatusSymbol;
13
- run(): Promise<void>;
24
+ run(): Promise<StatusResponse | void>;
14
25
  }
26
+ export {};
@@ -9,6 +9,7 @@ class Status extends core_1.Command {
9
9
  '<%= config.bin %> <%= command.id %> --name my-upload-name',
10
10
  '<%= config.bin %> <%= command.id %> --upload-id 123e4567-e89b-12d3-a456-426614174000 --json',
11
11
  ];
12
+ static enableJsonFlag = true;
12
13
  static flags = {
13
14
  json: core_1.Flags.boolean({
14
15
  description: 'output in json format',
@@ -40,7 +41,7 @@ class Status extends core_1.Command {
40
41
  }
41
42
  async run() {
42
43
  const { flags } = await this.parse(Status);
43
- const { apiUrl, apiKey, name, 'upload-id': uploadId } = flags;
44
+ const { apiUrl, apiKey, name, 'upload-id': uploadId, json } = flags;
44
45
  if (!apiKey) {
45
46
  this.error('API Key is required. Please provide it via --api-key flag or DEVICE_CLOUD_API_KEY environment variable.');
46
47
  return;
@@ -54,8 +55,8 @@ class Status extends core_1.Command {
54
55
  name,
55
56
  uploadId,
56
57
  }));
57
- if (flags.json) {
58
- this.log(JSON.stringify(status, null, 2));
58
+ if (json) {
59
+ return status;
59
60
  }
60
61
  else {
61
62
  this.log('\n📊 Upload Status');
@@ -27,6 +27,7 @@ export declare const flags: {
27
27
  quiet: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
28
28
  retry: import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
29
29
  report: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
30
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
30
31
  };
31
32
  export declare const iOSCompatibilityLookup: {
32
33
  [k in EiOSDevices]: string[];
package/dist/constants.js CHANGED
@@ -166,6 +166,9 @@ exports.flags = {
166
166
  description: 'Runs Maestro with the --format flag, this will generate a report in the specified format',
167
167
  options: ['junit', 'html'],
168
168
  }),
169
+ json: core_1.Flags.boolean({
170
+ description: 'Output results in JSON format - note: will always provide exit code 0',
171
+ }),
169
172
  };
170
173
  exports.iOSCompatibilityLookup = {
171
174
  'ipad-pro-6th-gen': ['16', '17', '18'],
package/dist/methods.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import * as archiver from 'archiver';
2
2
  import { paths } from '../../api/schema.types';
3
3
  import { TAppMetadata } from './types';
4
- export declare const versionCheck: (currentVersion: string) => Promise<void>;
5
4
  export declare const typeSafePost: <T extends keyof paths>(baseUrl: string, path: string, init?: {
6
5
  body?: BodyInit;
7
6
  headers?: HeadersInit;
@@ -22,8 +21,8 @@ export declare const verifyAppZip: (zipPath: string) => Promise<void>;
22
21
  export declare const extractAppMetadataAndroid: (appFilePath: string) => Promise<TAppMetadata>;
23
22
  export declare const extractAppMetadataIosZip: (appFilePath: string) => Promise<TAppMetadata>;
24
23
  export declare const extractAppMetadataIos: (appFolderPath: string) => Promise<TAppMetadata>;
25
- export declare const uploadBinary: (filePath: string, apiUrl: string, apiKey: string, ignoreShaCheck?: boolean) => Promise<string>;
26
- export declare const uploadBinaries: (finalAppFiles: string[], apiUrl: string, apiKey: string, ignoreShaCheck?: boolean) => Promise<string[]>;
24
+ export declare const uploadBinary: (filePath: string, apiUrl: string, apiKey: string, ignoreShaCheck?: boolean, log?: boolean) => Promise<string>;
25
+ export declare const uploadBinaries: (finalAppFiles: string[], apiUrl: string, apiKey: string, ignoreShaCheck?: boolean, log?: boolean) => Promise<string[]>;
27
26
  export declare const verifyAdditionalAppFiles: (appFiles: string[] | undefined) => Promise<void>;
28
27
  export declare const getUploadStatus: (apiUrl: string, apiKey: string, options: {
29
28
  uploadId?: string;
package/dist/methods.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getUploadStatus = exports.verifyAdditionalAppFiles = exports.uploadBinaries = exports.uploadBinary = exports.extractAppMetadataIos = exports.extractAppMetadataIosZip = exports.extractAppMetadataAndroid = exports.verifyAppZip = exports.compressFilesFromRelativePath = exports.compressFolderToBlob = exports.compressDir = exports.toBuffer = exports.typeSafeGet = exports.typeSafePostDownload = exports.typeSafePost = exports.versionCheck = void 0;
3
+ exports.getUploadStatus = exports.verifyAdditionalAppFiles = exports.uploadBinaries = exports.uploadBinary = exports.extractAppMetadataIos = exports.extractAppMetadataIosZip = exports.extractAppMetadataAndroid = exports.verifyAppZip = exports.compressFilesFromRelativePath = exports.compressFolderToBlob = exports.compressDir = exports.toBuffer = exports.typeSafeGet = exports.typeSafePostDownload = exports.typeSafePost = void 0;
4
4
  const core_1 = require("@oclif/core");
5
5
  const supabase_js_1 = require("@supabase/supabase-js");
6
6
  // required polyfill for node 18
@@ -29,20 +29,6 @@ const PERMITTED_EXTENSIONS = new Set([
29
29
  'mp4',
30
30
  'js',
31
31
  ]);
32
- const versionCheck = async (currentVersion) => {
33
- const versionResponse = await fetch('https://registry.npmjs.org/@devicecloud.dev/dcd/latest');
34
- const versionResponseJson = await versionResponse.json();
35
- const latestVersion = versionResponseJson.version;
36
- if (latestVersion !== currentVersion) {
37
- console.log(`
38
- -------------------
39
- A new version of the devicecloud.dev CLI is available: ${latestVersion}
40
- Run 'npm install -g @devicecloud.dev/dcd@latest' to update to the latest version
41
- -------------------
42
- `);
43
- }
44
- };
45
- exports.versionCheck = versionCheck;
46
32
  const typeSafePost = async (baseUrl, path, init) => {
47
33
  const res = await fetch(baseUrl + path, {
48
34
  ...init,
@@ -193,10 +179,7 @@ const extractAppMetadataIosZip = async (appFilePath) => new Promise((resolve, re
193
179
  })
194
180
  .catch(reject);
195
181
  });
196
- zip.on('error', (err) => {
197
- console.error(err);
198
- reject(err);
199
- });
182
+ zip.on('error', reject);
200
183
  });
201
184
  exports.extractAppMetadataIosZip = extractAppMetadataIosZip;
202
185
  const extractAppMetadataIos = async (appFolderPath) => {
@@ -207,10 +190,12 @@ const extractAppMetadataIos = async (appFolderPath) => {
207
190
  return { appId, platform: 'ios' };
208
191
  };
209
192
  exports.extractAppMetadataIos = extractAppMetadataIos;
210
- const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false) => {
211
- core_1.ux.action.start('Checking and uploading binary', 'Initializing', {
212
- stdout: true,
213
- });
193
+ const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false, log = true) => {
194
+ if (log) {
195
+ core_1.ux.action.start('Checking and uploading binary', 'Initializing', {
196
+ stdout: true,
197
+ });
198
+ }
214
199
  let file;
215
200
  if (filePath?.endsWith('.app')) {
216
201
  const zippedAppBlob = await (0, exports.compressFolderToBlob)(filePath);
@@ -227,7 +212,9 @@ const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false) =>
227
212
  sha = await getFileHashFromFile(file);
228
213
  }
229
214
  catch (e) {
230
- console.warn('Warning: Failed to get file hash', e);
215
+ if (log) {
216
+ console.warn('Warning: Failed to get file hash', e);
217
+ }
231
218
  }
232
219
  if (!ignoreShaCheck && sha) {
233
220
  try {
@@ -239,8 +226,10 @@ const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false) =>
239
226
  },
240
227
  });
241
228
  if (exists) {
242
- core_1.ux.info(`sha hash matches existing binary with id: ${appBinaryId}, skipping upload. Force upload with --ignore-sha-check`);
243
- core_1.ux.action.stop(`Skipping upload.`);
229
+ if (log) {
230
+ core_1.ux.info(`sha hash matches existing binary with id: ${appBinaryId}, skipping upload. Force upload with --ignore-sha-check`);
231
+ core_1.ux.action.stop(`Skipping upload.`);
232
+ }
244
233
  return appBinaryId;
245
234
  }
246
235
  }
@@ -268,7 +257,9 @@ const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false) =>
268
257
  : await (0, exports.extractAppMetadataIos)(filePath);
269
258
  }
270
259
  catch {
271
- core_1.ux.warn('Failed to extract app metadata, please share with support@devicecloud.dev so we can improve our parsing.');
260
+ if (log) {
261
+ core_1.ux.warn('Failed to extract app metadata, please share with support@devicecloud.dev so we can improve our parsing.');
262
+ }
272
263
  }
273
264
  // this needs to made nicer by using envs or maybe fetching the keys from the getSignedURL call
274
265
  const SB = {
@@ -297,11 +288,13 @@ const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false) =>
297
288
  });
298
289
  if (error)
299
290
  throw new Error(error);
300
- core_1.ux.action.stop(`\nBinary uploaded with id: ${id}`);
291
+ if (log) {
292
+ core_1.ux.action.stop(`\nBinary uploaded with id: ${id}`);
293
+ }
301
294
  return id;
302
295
  };
303
296
  exports.uploadBinary = uploadBinary;
304
- const uploadBinaries = async (finalAppFiles, apiUrl, apiKey, ignoreShaCheck = false) => Promise.all(finalAppFiles.map((f) => (0, exports.uploadBinary)(f, apiUrl, apiKey, ignoreShaCheck)));
297
+ const uploadBinaries = async (finalAppFiles, apiUrl, apiKey, ignoreShaCheck = false, log = true) => Promise.all(finalAppFiles.map((f) => (0, exports.uploadBinary)(f, apiUrl, apiKey, ignoreShaCheck, log)));
305
298
  exports.uploadBinaries = uploadBinaries;
306
299
  const verifyAdditionalAppFiles = async (appFiles) => {
307
300
  if (appFiles?.length) {
@@ -19,6 +19,12 @@
19
19
  "<%= config.bin %> <%= command.id %>"
20
20
  ],
21
21
  "flags": {
22
+ "json": {
23
+ "description": "Output results in JSON format - note: will always provide exit code 0",
24
+ "name": "json",
25
+ "allowNo": false,
26
+ "type": "boolean"
27
+ },
22
28
  "additional-app-binary-ids": {
23
29
  "description": "The ID of the additional app binary(s) previously uploaded to devicecloud.dev to install before execution",
24
30
  "name": "additional-app-binary-ids",
@@ -315,7 +321,7 @@
315
321
  "pluginName": "@devicecloud.dev/dcd",
316
322
  "pluginType": "core",
317
323
  "strict": true,
318
- "enableJsonFlag": false,
324
+ "enableJsonFlag": true,
319
325
  "isESM": false,
320
326
  "relativePath": [
321
327
  "dist",
@@ -389,7 +395,7 @@
389
395
  "pluginName": "@devicecloud.dev/dcd",
390
396
  "pluginType": "core",
391
397
  "strict": true,
392
- "enableJsonFlag": false,
398
+ "enableJsonFlag": true,
393
399
  "isESM": false,
394
400
  "relativePath": [
395
401
  "dist",
@@ -398,5 +404,5 @@
398
404
  ]
399
405
  }
400
406
  },
401
- "version": "3.3.6"
407
+ "version": "3.3.8"
402
408
  }
package/package.json CHANGED
@@ -80,7 +80,7 @@
80
80
  "test": "mocha --forbid-only \"test/**/*.test.ts\"",
81
81
  "version": "oclif readme && git add README.md"
82
82
  },
83
- "version": "3.3.6",
83
+ "version": "3.3.8",
84
84
  "bugs": {
85
85
  "url": "https://discord.gg/gm3mJwcNw8"
86
86
  },