@devicecloud.dev/dcd 3.5.0 → 3.6.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.
@@ -39,8 +39,10 @@ export default class Cloud extends Command {
39
39
  'app-binary-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
40
40
  'app-file': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
41
41
  async: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
42
+ config: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
42
43
  'device-locale': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
43
44
  'download-artifacts': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
45
+ 'artifacts-path': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
44
46
  env: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
45
47
  'exclude-flows': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
46
48
  'exclude-tags': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
@@ -80,7 +80,7 @@ class Cloud extends core_1.Command {
80
80
  let output = null;
81
81
  try {
82
82
  const { args, flags, raw } = await this.parse(Cloud);
83
- let { '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, 'runner-type': runnerType, 'x86-arch': x86Arch, json, ...rest } = flags;
83
+ let { '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, 'artifacts-path': artifactsPath, async, config: configFile, '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, 'runner-type': runnerType, 'x86-arch': x86Arch, json, ...rest } = flags;
84
84
  // If in JSON mode, temporarily intercept stdout to suppress the warning
85
85
  if (json) {
86
86
  const originalStdoutWrite = process.stdout.write;
@@ -118,6 +118,19 @@ class Cloud extends core_1.Command {
118
118
  throw new Error('runnerType m4 only supports iOS');
119
119
  }
120
120
  }
121
+ if (runnerType === 'm1') {
122
+ this.log('Note: Runner Type m1 is experimental and currently supports Android only, iOS will revert to default.');
123
+ // todo - better platform checking
124
+ if (iOSDevice || iOSVersion) {
125
+ this.log('runnerType m1 only supports Android, reverting to default');
126
+ runnerType = 'default';
127
+ }
128
+ if (androidApiLevel || androidDevice) {
129
+ this.log('Runner Type m1 only supports API Level 34 and Pixel 7, unsetting your android options.');
130
+ androidApiLevel = undefined;
131
+ androidDevice = undefined;
132
+ }
133
+ }
121
134
  const additionalAppBinaryIds = nonFlatAdditionalAppBinaryIds?.flat();
122
135
  const additionalAppFiles = nonFlatAdditionalAppFiles?.flat();
123
136
  const { firstFile, secondFile } = args;
@@ -164,7 +177,7 @@ class Cloud extends core_1.Command {
164
177
  }
165
178
  let executionPlan;
166
179
  try {
167
- executionPlan = await (0, plan_1.plan)(flowFile, includeTags.flat(), excludeTags.flat(), excludeFlows.flat());
180
+ executionPlan = await (0, plan_1.plan)(flowFile, includeTags.flat(), excludeTags.flat(), excludeFlows.flat(), configFile);
168
181
  }
169
182
  catch (error) {
170
183
  throw error;
@@ -375,9 +388,9 @@ class Cloud extends core_1.Command {
375
388
  'content-type': 'application/json',
376
389
  'x-app-api-key': apiKey,
377
390
  },
378
- });
391
+ }, artifactsPath);
379
392
  this.log('\n');
380
- this.log('Test artifacts have been downloaded to ./artifacts.zip');
393
+ this.log(`Test artifacts have been downloaded to ${artifactsPath || './artifacts.zip'}`);
381
394
  }
382
395
  catch {
383
396
  this.warn('Failed to download artifacts');
@@ -9,8 +9,10 @@ export declare const flags: {
9
9
  'app-binary-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
10
10
  'app-file': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
11
11
  async: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
12
+ config: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
12
13
  'device-locale': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
13
14
  'download-artifacts': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
15
+ 'artifacts-path': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
14
16
  env: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
15
17
  'exclude-flows': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
16
18
  'exclude-tags': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
package/dist/constants.js CHANGED
@@ -52,6 +52,9 @@ exports.flags = {
52
52
  async: core_1.Flags.boolean({
53
53
  description: 'Immediately return (exit code 0) from the command without waiting for the results of the run (useful for saving CI minutes)',
54
54
  }),
55
+ config: core_1.Flags.file({
56
+ description: 'Path to custom config.yaml file. If not provided, defaults to config.yaml in root flows folders.',
57
+ }),
55
58
  'device-locale': core_1.Flags.string({
56
59
  description: 'Locale that will be set to a device, ISO-639-1 code and uppercase ISO-3166-1 code e.g. "de_DE" for Germany',
57
60
  }),
@@ -59,6 +62,10 @@ exports.flags = {
59
62
  description: 'Download a zip containing the logs, screenshots and videos for each result in this run. You will debited a $0.01 egress fee for each result. Use --download-artifacts=FAILED for failures only or --download-artifacts=ALL for every result.',
60
63
  options: ['ALL', 'FAILED'],
61
64
  }),
65
+ 'artifacts-path': core_1.Flags.string({
66
+ description: 'Custom file path for downloaded artifacts (default: ./artifacts.zip)',
67
+ dependsOn: ['download-artifacts'],
68
+ }),
62
69
  env: core_1.Flags.file({
63
70
  char: 'e',
64
71
  description: 'One or more environment variables to inject into your flows',
@@ -162,9 +169,9 @@ exports.flags = {
162
169
  description: 'Automatically retry the run up to the number of times specified (same as pressing retry in the UI) - this is free of charge',
163
170
  }),
164
171
  'runner-type': core_1.Flags.string({
165
- description: '[iOS only] [experimental] The type of runner to use - note: anything other than default will incur advanced pricing tiers',
172
+ description: '[experimental] The type of runner to use - note: anything other than default will incur premium pricing tiers, see https://docs.devicecloud.dev/reference/runner-type for more information',
166
173
  default: 'default',
167
- options: ['default', 'm4'],
174
+ options: ['default', 'm4', 'm1'],
168
175
  }),
169
176
  report: core_1.Flags.string({
170
177
  aliases: ['format'],
package/dist/methods.d.ts CHANGED
@@ -8,7 +8,7 @@ export declare const typeSafePost: <T extends keyof paths>(baseUrl: string, path
8
8
  export declare const typeSafePostDownload: (baseUrl: string, path: string, init?: {
9
9
  body?: BodyInit;
10
10
  headers?: HeadersInit;
11
- }) => Promise<void>;
11
+ }, artifactsPath?: string) => Promise<void>;
12
12
  export declare const typeSafeGet: <T extends keyof paths>(baseUrl: string, path: string, init?: {
13
13
  body?: FormData;
14
14
  headers?: HeadersInit;
package/dist/methods.js CHANGED
@@ -18,6 +18,8 @@ const StreamZip = require("node-stream-zip");
18
18
  const plist_1 = require("plist");
19
19
  const node_crypto_1 = require("node:crypto");
20
20
  const path = require("path");
21
+ const node_path_1 = require("node:path");
22
+ const os = require("node:os");
21
23
  const cloud_1 = require("./commands/cloud");
22
24
  const PERMITTED_EXTENSIONS = new Set([
23
25
  'yml',
@@ -40,7 +42,7 @@ const typeSafePost = async (baseUrl, path, init) => {
40
42
  return res.json();
41
43
  };
42
44
  exports.typeSafePost = typeSafePost;
43
- const typeSafePostDownload = async (baseUrl, path, init) => {
45
+ const typeSafePostDownload = async (baseUrl, path, init, artifactsPath) => {
44
46
  const res = await fetch(baseUrl + path, {
45
47
  ...init,
46
48
  method: 'POST',
@@ -48,7 +50,25 @@ const typeSafePostDownload = async (baseUrl, path, init) => {
48
50
  if (!res.ok) {
49
51
  throw new Error(await res.text());
50
52
  }
51
- const fileStream = (0, node_fs_1.createWriteStream)('./artifacts.zip', { flags: 'wx' });
53
+ let outputPath = artifactsPath || './artifacts.zip';
54
+ // Handle tilde expansion for home directory
55
+ if (outputPath.startsWith('~/') || outputPath === '~') {
56
+ outputPath = outputPath.replace(/^~(?=$|\/|\\)/, os.homedir());
57
+ }
58
+ // Create directory structure if it doesn't exist
59
+ const directory = (0, node_path_1.dirname)(outputPath);
60
+ if (directory !== '.') {
61
+ try {
62
+ (0, node_fs_1.mkdirSync)(directory, { recursive: true });
63
+ }
64
+ catch (error) {
65
+ // Ignore if directory already exists
66
+ if (error.code !== 'EEXIST') {
67
+ throw error;
68
+ }
69
+ }
70
+ }
71
+ const fileStream = (0, node_fs_1.createWriteStream)(outputPath, { flags: 'wx' });
52
72
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
53
73
  await (0, promises_2.finished)(node_stream_1.Readable.fromWeb(res.body).pipe(fileStream));
54
74
  };
package/dist/plan.d.ts CHANGED
@@ -25,5 +25,5 @@ interface IFlowSequence {
25
25
  continueOnFailure?: boolean;
26
26
  flows: string[];
27
27
  }
28
- export declare function plan(input: string, includeTags: string[], excludeTags: string[], excludeFlows?: string[]): Promise<IExecutionPlan>;
28
+ export declare function plan(input: string, includeTags: string[], excludeTags: string[], excludeFlows?: string[], configFile?: string): Promise<IExecutionPlan>;
29
29
  export {};
package/dist/plan.js CHANGED
@@ -52,7 +52,7 @@ function getWorkspaceConfig(input, unfilteredFlowFiles) {
52
52
  : {};
53
53
  return config;
54
54
  }
55
- async function plan(input, includeTags, excludeTags, excludeFlows) {
55
+ async function plan(input, includeTags, excludeTags, excludeFlows, configFile) {
56
56
  const normalizedInput = path.normalize(input);
57
57
  if (!fs.existsSync(normalizedInput)) {
58
58
  throw new Error(`Flow path does not exist: ${path.resolve(normalizedInput)}`);
@@ -60,7 +60,7 @@ async function plan(input, includeTags, excludeTags, excludeFlows) {
60
60
  if (fs.lstatSync(normalizedInput).isFile()) {
61
61
  if (normalizedInput.endsWith('config.yaml') ||
62
62
  normalizedInput.endsWith('config.yml')) {
63
- throw new Error('If using config.yaml, pass the workspace folder path, not the config file');
63
+ throw new Error('If using config.yaml, pass the workspace folder path, not the config file or a custom path via --config');
64
64
  }
65
65
  const checkedDependancies = await checkDependencies(normalizedInput);
66
66
  return {
@@ -74,7 +74,17 @@ async function plan(input, includeTags, excludeTags, excludeFlows) {
74
74
  throw new Error(`Flow directory does not contain any Flow files: ${path.resolve(normalizedInput)}`);
75
75
  }
76
76
  unfilteredFlowFiles = filterFlowFiles(unfilteredFlowFiles, excludeFlows);
77
- const workspaceConfig = getWorkspaceConfig(normalizedInput, unfilteredFlowFiles);
77
+ let workspaceConfig;
78
+ if (configFile) {
79
+ const configFilePath = path.resolve(process.cwd(), configFile);
80
+ if (!fs.existsSync(configFilePath)) {
81
+ throw new Error(`Config file does not exist: ${configFilePath}`);
82
+ }
83
+ workspaceConfig = (0, planMethods_1.readYamlFileAsJson)(configFilePath);
84
+ }
85
+ else {
86
+ workspaceConfig = getWorkspaceConfig(normalizedInput, unfilteredFlowFiles);
87
+ }
78
88
  if (workspaceConfig.flows) {
79
89
  const globs = workspaceConfig.flows.map((glob) => glob);
80
90
  const matchedFiles = await (0, glob_1.glob)(globs, {
@@ -84,12 +94,16 @@ async function plan(input, includeTags, excludeTags, excludeFlows) {
84
94
  // overwrite the list of files with the globbed ones
85
95
  unfilteredFlowFiles = matchedFiles
86
96
  .filter((file) => file !== 'config.yaml' &&
97
+ file !== 'config.yml' &&
98
+ (!configFile || file !== path.basename(configFile)) &&
87
99
  (file.endsWith('.yaml') || file.endsWith('.yml')))
88
100
  .map((file) => path.resolve(normalizedInput, file));
89
101
  }
90
102
  else {
91
103
  // workspace config has no flows, so we need to remove the config file from the test list
92
- unfilteredFlowFiles = unfilteredFlowFiles.filter((file) => !file.endsWith('config.yaml') && !file.endsWith('config.yml'));
104
+ unfilteredFlowFiles = unfilteredFlowFiles.filter((file) => !file.endsWith('config.yaml') &&
105
+ !file.endsWith('config.yml') &&
106
+ (!configFile || !file.endsWith(configFile)));
93
107
  }
94
108
  if (unfilteredFlowFiles.length === 0) {
95
109
  const error = workspaceConfig.flows
@@ -120,6 +120,13 @@
120
120
  "allowNo": false,
121
121
  "type": "boolean"
122
122
  },
123
+ "config": {
124
+ "description": "Path to custom config.yaml file. If not provided, defaults to config.yaml in root flows folders.",
125
+ "name": "config",
126
+ "hasDynamicHelp": false,
127
+ "multiple": false,
128
+ "type": "option"
129
+ },
123
130
  "device-locale": {
124
131
  "description": "Locale that will be set to a device, ISO-639-1 code and uppercase ISO-3166-1 code e.g. \"de_DE\" for Germany",
125
132
  "name": "device-locale",
@@ -138,6 +145,16 @@
138
145
  ],
139
146
  "type": "option"
140
147
  },
148
+ "artifacts-path": {
149
+ "dependsOn": [
150
+ "download-artifacts"
151
+ ],
152
+ "description": "Custom file path for downloaded artifacts (default: ./artifacts.zip)",
153
+ "name": "artifacts-path",
154
+ "hasDynamicHelp": false,
155
+ "multiple": false,
156
+ "type": "option"
157
+ },
141
158
  "env": {
142
159
  "char": "e",
143
160
  "description": "One or more environment variables to inject into your flows",
@@ -300,14 +317,15 @@
300
317
  "type": "option"
301
318
  },
302
319
  "runner-type": {
303
- "description": "[iOS only] [experimental] The type of runner to use - note: anything other than default will incur advanced pricing tiers",
320
+ "description": "[experimental] The type of runner to use - note: anything other than default will incur premium pricing tiers, see https://docs.devicecloud.dev/reference/runner-type for more information",
304
321
  "name": "runner-type",
305
322
  "default": "default",
306
323
  "hasDynamicHelp": false,
307
324
  "multiple": false,
308
325
  "options": [
309
326
  "default",
310
- "m4"
327
+ "m4",
328
+ "m1"
311
329
  ],
312
330
  "type": "option"
313
331
  },
@@ -483,5 +501,5 @@
483
501
  ]
484
502
  }
485
503
  },
486
- "version": "3.5.0"
504
+ "version": "3.6.1"
487
505
  }
package/package.json CHANGED
@@ -71,16 +71,15 @@
71
71
  "scripts": {
72
72
  "dcd": "./bin/dev.js",
73
73
  "prod": "./bin/run.js",
74
- "build": "shx rm -rf dist && tsc -b",
74
+ "build": "shx rm -rf dist && tsc -b",
75
75
  "lint": "eslint . --ext .ts",
76
76
  "postpack": "shx rm -f oclif.manifest.json",
77
77
  "posttest": "yarn lint",
78
78
  "prepack": "yarn build && oclif manifest",
79
79
  "prepare": "yarn build",
80
- "test": "mocha --forbid-only \"test/**/*.test.ts\"",
81
80
  "version": "oclif readme && git add README.md"
82
81
  },
83
- "version": "3.5.0",
82
+ "version": "3.6.1",
84
83
  "bugs": {
85
84
  "url": "https://discord.gg/gm3mJwcNw8"
86
85
  },