@testrigor/testrigor-cli 2.0.1 → 2.0.2

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.
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "testrigor-cli-appium-server",
3
+ "private": true,
4
+ "description": "Appium server installed by the testRigor CLI on the first mobile run. See src/util/appium.ts.",
5
+ "dependencies": {
6
+ "appium": "3.8.0"
7
+ },
8
+ "overrides": {
9
+ "shell-quote": "^1.12.0"
10
+ }
11
+ }
package/bin/dev CHANGED
@@ -1,7 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const oclif = require('@oclif/core')
4
-
5
3
  const path = require('path')
6
4
  const project = path.join(__dirname, '..', 'tsconfig.json')
7
5
 
@@ -10,9 +8,5 @@ process.env.NODE_ENV = 'development'
10
8
 
11
9
  require('ts-node').register({project})
12
10
 
13
- // In dev mode, always show stack traces
14
- oclif.settings.debug = true;
15
-
16
-
17
- // Start the CLI
18
- oclif.run().then(oclif.flush).catch(oclif.Errors.handle)
11
+ // development: true shows stack traces and loads commands from src/
12
+ require('@oclif/core').execute({development: true, dir: __dirname})
package/bin/run CHANGED
@@ -1,5 +1,3 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const oclif = require('@oclif/core')
4
-
5
- oclif.run().then(require('@oclif/core/flush')).catch(require('@oclif/core/handle'))
3
+ require('@oclif/core').execute({dir: __dirname})
@@ -4,10 +4,10 @@ export type CommandFlags<T extends typeof Command> = Interfaces.InferredFlags<(t
4
4
  export type Args<T extends typeof Command> = Interfaces.InferredArgs<T["args"]>;
5
5
  export declare abstract class CommandBase<T extends typeof Command> extends Command {
6
6
  static baseFlags: {
7
- "auth-token": Interfaces.OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
8
- token: Interfaces.OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
9
- "min-port": Interfaces.OptionFlag<number, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
10
- "max-port": Interfaces.OptionFlag<number, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
7
+ "auth-token": Interfaces.OptionFlag<string | undefined, Interfaces.CustomOptions>;
8
+ token: Interfaces.OptionFlag<string | undefined, Interfaces.CustomOptions>;
9
+ "min-port": Interfaces.OptionFlag<number, Interfaces.CustomOptions>;
10
+ "max-port": Interfaces.OptionFlag<number, Interfaces.CustomOptions>;
11
11
  verbose: Interfaces.BooleanFlag<boolean>;
12
12
  };
13
13
  protected flags: CommandFlags<T>;
@@ -3,17 +3,15 @@ export default class Authenticate extends Command {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static flags: {
6
- token: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
6
+ token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
7
  };
8
8
  run(): Promise<void>;
9
9
  /**
10
10
  * Where the token comes from, in order: the --token flag, an interactive
11
11
  * hidden prompt, or piped stdin.
12
12
  *
13
- * The hidden prompt is only usable on a TTY. Without one, @oclif/core falls
14
- * back to `sh -c 'read -s ...'`, which fails on any system where /bin/sh is
15
- * dash (the Debian and Ubuntu default) with "read: Illegal option -s" and
16
- * then silently skips the setup.
13
+ * The hidden prompt is only usable on a TTY: without one there is nothing
14
+ * to read keystrokes from, so piped stdin is tried instead.
17
15
  */
18
16
  private resolveToken;
19
17
  private readStdin;
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
3
4
  const core_1 = require("@oclif/core");
4
5
  const core_2 = require("@oclif/core");
6
+ const password_1 = tslib_1.__importDefault(require("@inquirer/password"));
5
7
  const config_1 = require("../util/config");
6
8
  // Time to wait for a token on a piped stdin before giving up. Stdin that is
7
9
  // neither a TTY nor a closed/redirected stream would otherwise hang forever.
@@ -20,21 +22,21 @@ class Authenticate extends core_2.Command {
20
22
  if (config.authToken) {
21
23
  delete config.authToken;
22
24
  await (0, config_1.writeConfig)(config);
23
- core_1.ux.info("Authentication token removed");
25
+ core_1.ux.stdout("Authentication token removed");
24
26
  }
25
27
  else {
26
- core_1.ux.info("Authentication token setup skipped");
28
+ core_1.ux.stdout("Authentication token setup skipped");
27
29
  }
28
30
  return;
29
31
  }
30
32
  config.authToken = token.trim();
31
33
  await (0, config_1.writeConfig)(config);
32
- core_1.ux.info("Authentication token successfully saved");
34
+ core_1.ux.stdout("Authentication token successfully saved");
33
35
  if (config.defaultTestSuiteId) {
34
- core_1.ux.info("You can now use 'testrigor run' to run your default test suite");
36
+ core_1.ux.stdout("You can now use 'testrigor run' to run your default test suite");
35
37
  }
36
38
  else {
37
- core_1.ux.info("Use 'testrigor test-suite config --default TEST_SUITE_ID' to set your default test suite ID, then use 'testrigor run'");
39
+ core_1.ux.stdout("Use 'testrigor test-suite config --default TEST_SUITE_ID' to set your default test suite ID, then use 'testrigor run'");
38
40
  }
39
41
  }
40
42
  catch (error) {
@@ -45,19 +47,19 @@ class Authenticate extends core_2.Command {
45
47
  * Where the token comes from, in order: the --token flag, an interactive
46
48
  * hidden prompt, or piped stdin.
47
49
  *
48
- * The hidden prompt is only usable on a TTY. Without one, @oclif/core falls
49
- * back to `sh -c 'read -s ...'`, which fails on any system where /bin/sh is
50
- * dash (the Debian and Ubuntu default) with "read: Illegal option -s" and
51
- * then silently skips the setup.
50
+ * The hidden prompt is only usable on a TTY: without one there is nothing
51
+ * to read keystrokes from, so piped stdin is tried instead.
52
52
  */
53
53
  async resolveToken(flagToken, hasExistingToken) {
54
54
  if (flagToken !== undefined) {
55
55
  return flagToken;
56
56
  }
57
57
  if (process.stdin.isTTY) {
58
- return core_1.ux.prompt(hasExistingToken
59
- ? "Enter your testRigor API token (press enter to remove)"
60
- : "Enter your testRigor API token (press enter to skip)", { type: "hide", required: false });
58
+ return (0, password_1.default)({
59
+ message: hasExistingToken
60
+ ? "Enter your testRigor API token (press enter to remove)"
61
+ : "Enter your testRigor API token (press enter to skip)",
62
+ });
61
63
  }
62
64
  const piped = await this.readStdin();
63
65
  if (piped) {
@@ -3,9 +3,9 @@ export default class Config extends Command {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static flags: {
6
- default: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
7
- delete: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
8
- show: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
6
+ default: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ delete: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ show: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
9
  };
10
10
  run(): Promise<void>;
11
11
  private setDefaultTestSuite;
@@ -32,45 +32,45 @@ class Config extends core_1.Command {
32
32
  const config = await (0, config_1.readConfig)();
33
33
  config.defaultTestSuiteId = testSuiteId.trim();
34
34
  await (0, config_1.writeConfig)(config);
35
- core_1.ux.info(`Default test suite ID set to: ${config.defaultTestSuiteId}`);
35
+ core_1.ux.stdout(`Default test suite ID set to: ${config.defaultTestSuiteId}`);
36
36
  if (config.authToken) {
37
- core_1.ux.info("You can now use 'testrigor run' to run your default test suite");
37
+ core_1.ux.stdout("You can now use 'testrigor run' to run your default test suite");
38
38
  }
39
39
  else {
40
- core_1.ux.info("Use 'testrigor authenticate' to set your authentication token");
40
+ core_1.ux.stdout("Use 'testrigor authenticate' to set your authentication token");
41
41
  }
42
42
  }
43
43
  async deleteDefaultTestSuite() {
44
44
  const config = await (0, config_1.readConfig)();
45
45
  if (!config.defaultTestSuiteId) {
46
- core_1.ux.info("No default test suite ID is currently set");
46
+ core_1.ux.stdout("No default test suite ID is currently set");
47
47
  return;
48
48
  }
49
49
  delete config.defaultTestSuiteId;
50
50
  await (0, config_1.writeConfig)(config);
51
- core_1.ux.info("Default test suite ID removed");
51
+ core_1.ux.stdout("Default test suite ID removed");
52
52
  if (config.authToken) {
53
- core_1.ux.info("Authentication token preserved");
53
+ core_1.ux.stdout("Authentication token preserved");
54
54
  }
55
55
  }
56
56
  async showConfiguration() {
57
57
  const config = await (0, config_1.readConfig)();
58
58
  if (Object.keys(config).length === 0) {
59
- core_1.ux.info("No configuration found");
59
+ core_1.ux.stdout("No configuration found");
60
60
  return;
61
61
  }
62
- core_1.ux.info("Current testRigor configuration:");
62
+ core_1.ux.stdout("Current testRigor configuration:");
63
63
  if (config.defaultTestSuiteId) {
64
- core_1.ux.info(` Default test suite ID: ${config.defaultTestSuiteId}`);
64
+ core_1.ux.stdout(` Default test suite ID: ${config.defaultTestSuiteId}`);
65
65
  }
66
66
  else {
67
- core_1.ux.info(" Default test suite ID: not set");
67
+ core_1.ux.stdout(" Default test suite ID: not set");
68
68
  }
69
69
  if (config.authToken) {
70
- core_1.ux.info(" Authentication token: configured");
70
+ core_1.ux.stdout(" Authentication token: configured");
71
71
  }
72
72
  else {
73
- core_1.ux.info(" Authentication token: not set");
73
+ core_1.ux.stdout(" Authentication token: not set");
74
74
  }
75
75
  }
76
76
  }
@@ -6,33 +6,33 @@ export default class Run extends CommandBase<typeof Run> {
6
6
  private TESTRIGOR_API_URL;
7
7
  private TESTRIGOR_API2_URL;
8
8
  static flags: {
9
- async: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
10
- branch: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
11
- commit: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
12
- localhost: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
13
- url: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
14
- "file-path": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
15
- "junit-report-save-path": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
16
- "variables-path": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
17
- "test-cases-path": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
18
- "rules-path": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
19
- "settings-path": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
20
- "local-test-nodes-path": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
21
- "test-case-uuid": import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
22
- "auto-create-ai-rules": import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
23
- "explicit-mutations": import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
24
- labels: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
25
- "excluded-labels": import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
26
- "base-url-web": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
27
- "base-url-api": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
28
- "chrome-extension": import("@oclif/core/lib/interfaces").OptionFlag<{
9
+ async: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ commit: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ localhost: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ url: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ "file-path": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ "junit-report-save-path": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ "variables-path": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
+ "test-cases-path": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
+ "rules-path": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
+ "settings-path": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
20
+ "local-test-nodes-path": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
21
+ "test-case-uuid": import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
22
+ "auto-create-ai-rules": import("@oclif/core/interfaces").BooleanFlag<boolean>;
23
+ "explicit-mutations": import("@oclif/core/interfaces").BooleanFlag<boolean>;
24
+ labels: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
25
+ "excluded-labels": import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
26
+ "base-url-web": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
27
+ "base-url-api": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
28
+ "chrome-extension": import("@oclif/core/interfaces").OptionFlag<{
29
29
  name: string;
30
30
  file: string;
31
- } | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
32
- "xray-execution-key": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
31
+ }[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
32
+ "xray-execution-key": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
33
33
  };
34
34
  static args: {
35
- testSuiteId: import("@oclif/core/lib/interfaces/parser").Arg<string | undefined, Record<string, unknown>>;
35
+ testSuiteId: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
36
36
  };
37
37
  private initializeUrls;
38
38
  run(): Promise<void>;
@@ -194,7 +194,7 @@ class Run extends command_base_1.CommandBase {
194
194
  }
195
195
  if (filePath) {
196
196
  if (url) {
197
- core_1.ux.info("WARNING: File path will be ignored because --url is used");
197
+ core_1.ux.stdout("WARNING: File path will be ignored because --url is used");
198
198
  delete flags["file-path"];
199
199
  }
200
200
  else {
@@ -231,8 +231,12 @@ class Run extends command_base_1.CommandBase {
231
231
  return task.newListr([
232
232
  {
233
233
  task: async (_ctx, subtask) => {
234
+ if (!(await (0, appium_1.isAppiumInstalled)())) {
235
+ subtask.title = `Installing mobile server (appium@${(0, appium_1.getAppiumVersion)()}, first run only)`;
236
+ await (0, appium_1.installAppium)();
237
+ }
234
238
  await (0, appium_1.checkAppiumInstalled)();
235
- subtask.title = "Mobile server binary found";
239
+ subtask.title = "Mobile server installed";
236
240
  },
237
241
  rendererOptions: { timer: listr2_1.PRESET_TIMER },
238
242
  },
@@ -4,14 +4,14 @@ export default class ValidateSyntax extends CommandBase<typeof ValidateSyntax> {
4
4
  static examples: string[];
5
5
  private TESTRIGOR_API2_URL;
6
6
  static flags: {
7
- steps: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
8
- "test-cases-path": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
9
- "base-url-api": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
10
- "rules-path": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
11
- "explicit-mutations": import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
7
+ steps: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ "test-cases-path": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ "base-url-api": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ "rules-path": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ "explicit-mutations": import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
12
  };
13
13
  static args: {
14
- testSuiteId: import("@oclif/core/lib/interfaces/parser").Arg<string | undefined, Record<string, unknown>>;
14
+ testSuiteId: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
15
15
  };
16
16
  run(): Promise<void>;
17
17
  private resolveStepsToValidate;
@@ -42,20 +42,20 @@ class ValidateSyntax extends command_base_1.CommandBase {
42
42
  const response = await this.callValidateSyntax(testSuiteId, testCase.customSteps, token, rules, explicitMutations);
43
43
  if (response.data && response.data.length > 0) {
44
44
  const parserErrors = response.data;
45
- core_2.ux.info(`${testCase.description}: invalid`);
45
+ core_2.ux.stdout(`${testCase.description}: invalid`);
46
46
  for (const parserError of parserErrors) {
47
- core_2.ux.info(` ${parserError.command} - ${parserError.errorMessage}`);
47
+ core_2.ux.stdout(` ${parserError.command} - ${parserError.errorMessage}`);
48
48
  }
49
49
  hasAnyInvalidTestCases = true;
50
50
  }
51
51
  else {
52
- core_2.ux.info(`${testCase.description}: valid`);
52
+ core_2.ux.stdout(`${testCase.description}: valid`);
53
53
  }
54
54
  }
55
55
  if (hasAnyInvalidTestCases) {
56
56
  this.exit(1);
57
57
  }
58
- core_2.ux.info("All provided steps are valid.");
58
+ core_2.ux.stdout("All provided steps are valid.");
59
59
  this.exit(0);
60
60
  }
61
61
  async callValidateSyntax(testSuiteId, steps, token, rules, explicitMutations) {
@@ -1,4 +1,7 @@
1
- export declare const ChromeExtensionFlag: import("@oclif/core/lib/interfaces/parser").FlagDefinition<{
1
+ export declare const ChromeExtensionFlag: import("@oclif/core/interfaces").FlagDefinition<{
2
2
  name: string;
3
3
  file: string;
4
- }, Record<string, unknown>>;
4
+ }, import("@oclif/core/interfaces").CustomOptions, {
5
+ multiple: true;
6
+ requiredOrDefaulted: false;
7
+ }>;
@@ -7,6 +7,15 @@ declare const REQUIRED_DRIVERS: Array<{
7
7
  version: string;
8
8
  }>;
9
9
  export { DEFAULT_PORT, DEFAULT_BASE_PATH, REQUIRED_DRIVERS, PID_DIR, PID_FILE };
10
+ export declare function getAppiumVersion(): string;
11
+ export declare function isAppiumInstalled(): Promise<boolean>;
12
+ /**
13
+ * Installs the pinned Appium with `npm ci` from the lockfile in appium-server/.
14
+ * The install runs in a staging directory that is renamed into place, so an
15
+ * interrupted install never looks complete and two concurrent runs do not
16
+ * install into the same directory.
17
+ */
18
+ export declare function installAppium(): Promise<void>;
10
19
  export declare function checkAppiumInstalled(): Promise<void>;
11
20
  export type DriverStatus = {
12
21
  installed: boolean;
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PID_FILE = exports.PID_DIR = exports.REQUIRED_DRIVERS = exports.DEFAULT_BASE_PATH = exports.DEFAULT_PORT = void 0;
4
+ exports.getAppiumVersion = getAppiumVersion;
5
+ exports.isAppiumInstalled = isAppiumInstalled;
6
+ exports.installAppium = installAppium;
4
7
  exports.checkAppiumInstalled = checkAppiumInstalled;
5
8
  exports.getDriverStatus = getDriverStatus;
6
9
  exports.ensureDriver = ensureDriver;
@@ -36,8 +39,28 @@ const REQUIRED_DRIVERS = [
36
39
  { name: "xcuitest", version: "10.43.1" },
37
40
  ];
38
41
  exports.REQUIRED_DRIVERS = REQUIRED_DRIVERS;
39
- function getAppiumBin() {
40
- return path.resolve(PROJECT_ROOT, "node_modules", ".bin", "appium");
42
+ // Appium is not a dependency of the CLI: its tree pins a vulnerable
43
+ // shell-quote, and npm re-resolves anything forced through our published
44
+ // shrinkwrap to satisfy that pin. appium-server/ instead ships a lockfile with
45
+ // an override, installed on the first mobile run under the CLI home, one
46
+ // directory per Appium version.
47
+ const APPIUM_SERVER_SOURCE = path.join(PROJECT_ROOT, "appium-server");
48
+ let appiumVersion;
49
+ function getAppiumVersion() {
50
+ if (!appiumVersion) {
51
+ const pkg = JSON.parse(fs.readFileSync(path.join(APPIUM_SERVER_SOURCE, "package.json"), "utf8"));
52
+ appiumVersion = pkg.dependencies.appium;
53
+ }
54
+ return appiumVersion;
55
+ }
56
+ function getAppiumInstallDir() {
57
+ return path.join((0, utils_1.getHomeDir)(), "appium-server", getAppiumVersion());
58
+ }
59
+ // Run Appium's entry script with the Node running the CLI rather than through
60
+ // node_modules/.bin, whose shims differ per platform (`appium.cmd` on Windows).
61
+ function getAppiumCommand(args) {
62
+ const entry = path.join(getAppiumInstallDir(), "node_modules", "appium", "index.js");
63
+ return [process.execPath, [entry, ...args]];
41
64
  }
42
65
  function getAppiumEnv() {
43
66
  return {
@@ -46,6 +69,13 @@ function getAppiumEnv() {
46
69
  CHROMELABS_URL: "https://googlechromelabs.github.io",
47
70
  };
48
71
  }
72
+ function runAppium(args) {
73
+ const [command, commandArgs] = getAppiumCommand(args);
74
+ return execFile(command, commandArgs, {
75
+ encoding: "utf8",
76
+ env: getAppiumEnv(),
77
+ });
78
+ }
49
79
  /**
50
80
  * Appium refuses to start unless APPIUM_HOME already exists, so create it
51
81
  * before any invocation and surface a permission problem as itself rather
@@ -61,25 +91,78 @@ async function ensureAppiumHome() {
61
91
  "Check that you own that directory, or point APPIUM_HOME at a writable one.");
62
92
  }
63
93
  }
94
+ async function isAppiumInstalled() {
95
+ try {
96
+ const pkg = JSON.parse(await fs.promises.readFile(path.join(getAppiumInstallDir(), "node_modules", "appium", "package.json"), "utf8"));
97
+ return pkg.version === getAppiumVersion();
98
+ }
99
+ catch {
100
+ return false;
101
+ }
102
+ }
103
+ /**
104
+ * Installs the pinned Appium with `npm ci` from the lockfile in appium-server/.
105
+ * The install runs in a staging directory that is renamed into place, so an
106
+ * interrupted install never looks complete and two concurrent runs do not
107
+ * install into the same directory.
108
+ */
109
+ async function installAppium() {
110
+ const target = getAppiumInstallDir();
111
+ await fs.promises.mkdir(path.dirname(target), { recursive: true });
112
+ const staging = await fs.promises.mkdtemp(`${target}.tmp-`);
113
+ for (const file of ["package.json", "package-lock.json"]) {
114
+ await fs.promises.copyFile(path.join(APPIUM_SERVER_SOURCE, file), path.join(staging, file));
115
+ }
116
+ try {
117
+ // npm is a .cmd on Windows, which Node only spawns through a shell. The
118
+ // arguments are constants, so the shell adds no injection surface.
119
+ await execFile("npm", ["ci", "--omit=dev", "--no-audit", "--no-fund"], {
120
+ cwd: staging,
121
+ shell: process.platform === "win32",
122
+ maxBuffer: 64 * 1024 * 1024,
123
+ });
124
+ }
125
+ catch (error) {
126
+ await fs.promises.rm(staging, { recursive: true, force: true });
127
+ const detail = error instanceof Error ? error.message : String(error);
128
+ throw new Error(`Could not install the mobile server (appium@${getAppiumVersion()}) into ${target}. ` +
129
+ `npm must be installed and able to reach the registry.\n${detail}`);
130
+ }
131
+ try {
132
+ // A damaged install (e.g. files deleted by hand) would block the rename.
133
+ if (!(await isAppiumInstalled())) {
134
+ await fs.promises.rm(target, { recursive: true, force: true });
135
+ }
136
+ await fs.promises.rename(staging, target);
137
+ }
138
+ catch (error) {
139
+ await fs.promises.rm(staging, { recursive: true, force: true });
140
+ // Another run finished installing first.
141
+ if (!(await isAppiumInstalled()))
142
+ throw error;
143
+ }
144
+ }
64
145
  async function checkAppiumInstalled() {
65
- const appiumBin = getAppiumBin();
66
- if (!(await fs_1.default.exists(appiumBin))) {
67
- throw new Error(`Appium binary not found at ${appiumBin}. ` +
68
- "Reinstall the CLI with: npm install -g @testrigor/testrigor-cli");
146
+ if (!(await isAppiumInstalled())) {
147
+ throw new Error(`The mobile server (appium@${getAppiumVersion()}) is not installed in ${getAppiumInstallDir()}.`);
69
148
  }
70
149
  await ensureAppiumHome();
71
150
  try {
72
- await execFile(appiumBin, ["--version"], { env: getAppiumEnv() });
151
+ await runAppium(["--version"]);
73
152
  }
74
153
  catch (error) {
75
154
  const detail = error instanceof Error ? error.message : String(error);
76
- throw new Error(`Appium binary at ${appiumBin} failed to run (APPIUM_HOME=${APPIUM_HOME}).\n${detail}`);
155
+ throw new Error(`The mobile server in ${getAppiumInstallDir()} failed to run (APPIUM_HOME=${APPIUM_HOME}).\n${detail}`);
77
156
  }
78
157
  }
79
158
  async function getDriverStatus(driverName, requiredVersion) {
80
- const appiumBin = getAppiumBin();
81
159
  try {
82
- const { stdout } = await execFile(appiumBin, ["driver", "list", "--installed", "--json"], { encoding: "utf8", env: getAppiumEnv() });
160
+ const { stdout } = await runAppium([
161
+ "driver",
162
+ "list",
163
+ "--installed",
164
+ "--json",
165
+ ]);
83
166
  const drivers = JSON.parse(stdout);
84
167
  const info = drivers[driverName];
85
168
  if (!info || !info.installed) {
@@ -96,20 +179,15 @@ async function getDriverStatus(driverName, requiredVersion) {
96
179
  }
97
180
  }
98
181
  async function ensureDriver(driverName, requiredVersion) {
99
- const appiumBin = getAppiumBin();
100
- const env = getAppiumEnv();
101
182
  const status = await getDriverStatus(driverName, requiredVersion);
102
183
  if (status.installed && status.upToDate) {
103
184
  return null;
104
185
  }
105
186
  try {
106
187
  if (status.installed) {
107
- await execFile(appiumBin, ["driver", "uninstall", driverName], {
108
- encoding: "utf8",
109
- env,
110
- });
188
+ await runAppium(["driver", "uninstall", driverName]);
111
189
  }
112
- await execFile(appiumBin, ["driver", "install", `${driverName}@${requiredVersion}`], { encoding: "utf8", env });
190
+ await runAppium(["driver", "install", `${driverName}@${requiredVersion}`]);
113
191
  return null;
114
192
  }
115
193
  catch (error) {
@@ -198,11 +276,10 @@ async function startAppium(port = DEFAULT_PORT, logLevel = "debug") {
198
276
  if (existingPid) {
199
277
  return { pid: existingPid, alreadyRunning: true };
200
278
  }
201
- const appiumBin = getAppiumBin();
202
279
  await ensureAppiumHome();
203
280
  const logStream = fs.openSync(LOG_FILE, "a");
204
281
  const logOffset = fs.fstatSync(logStream).size;
205
- const appiumProcess = childProcess.spawn(appiumBin, [
282
+ const [command, args] = getAppiumCommand([
206
283
  "--port",
207
284
  String(port),
208
285
  "--base-path",
@@ -211,7 +288,8 @@ async function startAppium(port = DEFAULT_PORT, logLevel = "debug") {
211
288
  "chromedriver_autodownload",
212
289
  "--log-level",
213
290
  logLevel,
214
- ], {
291
+ ]);
292
+ const appiumProcess = childProcess.spawn(command, args, {
215
293
  detached: true,
216
294
  stdio: ["ignore", logStream, logStream],
217
295
  env: getAppiumEnv(),