appium-webdriveragent 10.5.4 → 11.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/Scripts/{build-webdriveragent.js → build-webdriveragent.mjs} +13 -7
  3. package/Scripts/{fetch-prebuilt-wda.js → fetch-prebuilt-wda.mjs} +17 -9
  4. package/Scripts/update-wda-version.mjs +42 -0
  5. package/WebDriverAgentLib/Info.plist +2 -2
  6. package/build/index.d.ts +1 -1
  7. package/build/index.d.ts.map +1 -1
  8. package/build/index.js +1 -2
  9. package/build/index.js.map +1 -1
  10. package/build/lib/check-dependencies.d.ts +2 -7
  11. package/build/lib/check-dependencies.d.ts.map +1 -1
  12. package/build/lib/check-dependencies.js +0 -11
  13. package/build/lib/check-dependencies.js.map +1 -1
  14. package/build/lib/constants.d.ts +12 -12
  15. package/build/lib/constants.d.ts.map +1 -1
  16. package/build/lib/constants.js +14 -26
  17. package/build/lib/constants.js.map +1 -1
  18. package/build/lib/logger.d.ts +1 -2
  19. package/build/lib/logger.d.ts.map +1 -1
  20. package/build/lib/logger.js +2 -2
  21. package/build/lib/logger.js.map +1 -1
  22. package/build/lib/no-session-proxy.d.ts +5 -5
  23. package/build/lib/no-session-proxy.d.ts.map +1 -1
  24. package/build/lib/no-session-proxy.js +0 -1
  25. package/build/lib/no-session-proxy.js.map +1 -1
  26. package/build/lib/utils.d.ts +53 -96
  27. package/build/lib/utils.d.ts.map +1 -1
  28. package/build/lib/utils.js +124 -110
  29. package/build/lib/utils.js.map +1 -1
  30. package/build/lib/webdriveragent.d.ts +123 -170
  31. package/build/lib/webdriveragent.d.ts.map +1 -1
  32. package/build/lib/webdriveragent.js +319 -349
  33. package/build/lib/webdriveragent.js.map +1 -1
  34. package/build/lib/xcodebuild.d.ts +66 -77
  35. package/build/lib/xcodebuild.d.ts.map +1 -1
  36. package/build/lib/xcodebuild.js +123 -92
  37. package/build/lib/xcodebuild.js.map +1 -1
  38. package/build/test/functional/desired.d.ts +3 -0
  39. package/build/test/functional/desired.d.ts.map +1 -0
  40. package/build/test/functional/desired.js +9 -0
  41. package/build/test/functional/desired.js.map +1 -0
  42. package/build/test/functional/helpers/simulator.d.ts +5 -0
  43. package/build/test/functional/helpers/simulator.d.ts.map +1 -0
  44. package/build/test/functional/helpers/simulator.js +39 -0
  45. package/build/test/functional/helpers/simulator.js.map +1 -0
  46. package/build/test/functional/webdriveragent-e2e-specs.d.ts +2 -0
  47. package/build/test/functional/webdriveragent-e2e-specs.d.ts.map +1 -0
  48. package/build/test/functional/webdriveragent-e2e-specs.js +133 -0
  49. package/build/test/functional/webdriveragent-e2e-specs.js.map +1 -0
  50. package/build/test/unit/utils-specs.d.ts +2 -0
  51. package/build/test/unit/utils-specs.d.ts.map +1 -0
  52. package/build/test/unit/utils-specs.js +161 -0
  53. package/build/test/unit/utils-specs.js.map +1 -0
  54. package/build/test/unit/webdriveragent-specs.d.ts +2 -0
  55. package/build/test/unit/webdriveragent-specs.d.ts.map +1 -0
  56. package/build/test/unit/webdriveragent-specs.js +424 -0
  57. package/build/test/unit/webdriveragent-specs.js.map +1 -0
  58. package/build/tsconfig.tsbuildinfo +1 -0
  59. package/index.ts +1 -1
  60. package/lib/{check-dependencies.js → check-dependencies.ts} +4 -13
  61. package/lib/constants.ts +18 -0
  62. package/lib/logger.ts +3 -0
  63. package/lib/{no-session-proxy.js → no-session-proxy.ts} +4 -5
  64. package/lib/{utils.js → utils.ts} +101 -111
  65. package/lib/{webdriveragent.js → webdriveragent.ts} +353 -390
  66. package/lib/{xcodebuild.js → xcodebuild.ts} +145 -114
  67. package/package.json +7 -9
  68. package/lib/constants.js +0 -24
  69. package/lib/logger.js +0 -5
@@ -1,24 +1,34 @@
1
1
  import { fs, plist } from '@appium/support';
2
- import { exec } from 'teen_process';
3
- import path from 'path';
4
- import log from './logger';
2
+ import { exec, SubProcess } from 'teen_process';
3
+ import path, { dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { log } from './logger';
5
6
  import _ from 'lodash';
6
7
  import { WDA_RUNNER_BUNDLE_ID, PLATFORM_NAME_TVOS } from './constants';
7
8
  import B from 'bluebird';
8
- import _fs from 'fs';
9
+ import _fs from 'node:fs';
9
10
  import { waitForCondition } from 'asyncbox';
10
- import { arch } from 'os';
11
+ import { arch } from 'node:os';
12
+ import type { DeviceInfo } from './types';
11
13
 
12
14
  const PROJECT_FILE = 'project.pbxproj';
13
15
 
16
+ // Get current filename - works in both CommonJS and ESM
17
+ const currentFilename =
18
+ typeof __filename !== 'undefined'
19
+ ? __filename
20
+ : fileURLToPath(new Function('return import.meta.url')());
21
+
22
+ const currentDirname = dirname(currentFilename);
23
+
14
24
  /**
15
25
  * Calculates the path to the current module's root folder
16
26
  *
17
27
  * @returns {string} The full path to module root
18
28
  * @throws {Error} If the current module root folder cannot be determined
19
29
  */
20
- const getModuleRoot = _.memoize(function getModuleRoot () {
21
- let currentDir = path.dirname(path.resolve(__filename));
30
+ const getModuleRoot = _.memoize(function getModuleRoot (): string {
31
+ let currentDir = currentDirname;
22
32
  let isAtFsRoot = false;
23
33
  while (!isAtFsRoot) {
24
34
  const manifestPath = path.join(currentDir, 'package.json');
@@ -36,24 +46,7 @@ const getModuleRoot = _.memoize(function getModuleRoot () {
36
46
 
37
47
  export const BOOTSTRAP_PATH = getModuleRoot();
38
48
 
39
- async function getPIDsUsingPattern (pattern) {
40
- const args = [
41
- '-if', // case insensitive, full cmdline match
42
- pattern,
43
- ];
44
- try {
45
- const {stdout} = await exec('pgrep', args);
46
- return stdout.split(/\s+/)
47
- .map((x) => parseInt(x, 10))
48
- .filter(_.isInteger)
49
- .map((x) => `${x}`);
50
- } catch (err) {
51
- log.debug(`'pgrep ${args.join(' ')}' didn't detect any matching processes. Return code: ${err.code}`);
52
- return [];
53
- }
54
- }
55
-
56
- async function killAppUsingPattern (pgrepPattern) {
49
+ export async function killAppUsingPattern (pgrepPattern: string): Promise<void> {
57
50
  const signals = [2, 15, 9];
58
51
  for (const signal of signals) {
59
52
  const matchedPids = await getPIDsUsingPattern(pgrepPattern);
@@ -63,7 +56,7 @@ async function killAppUsingPattern (pgrepPattern) {
63
56
  const args = [`-${signal}`, ...matchedPids];
64
57
  try {
65
58
  await exec('kill', args);
66
- } catch (err) {
59
+ } catch (err: any) {
67
60
  log.debug(`kill ${args.join(' ')} -> ${err.message}`);
68
61
  }
69
62
  if (signal === _.last(signals)) {
@@ -94,36 +87,27 @@ async function killAppUsingPattern (pgrepPattern) {
94
87
 
95
88
  /**
96
89
  * Return true if the platformName is tvOS
97
- * @param {string} platformName The name of the platorm
98
- * @returns {boolean} Return true if the platformName is tvOS
90
+ * @param platformName The name of the platorm
91
+ * @returns Return true if the platformName is tvOS
99
92
  */
100
- function isTvOS (platformName) {
93
+ export function isTvOS (platformName: string): boolean {
101
94
  return _.toLower(platformName) === _.toLower(PLATFORM_NAME_TVOS);
102
95
  }
103
96
 
104
- async function replaceInFile (file, find, replace) {
105
- let contents = await fs.readFile(file, 'utf8');
106
-
107
- let newContents = contents.replace(find, replace);
108
- if (newContents !== contents) {
109
- await fs.writeFile(file, newContents, 'utf8');
110
- }
111
- }
112
-
113
97
  /**
114
98
  * Update WebDriverAgentRunner project bundle ID with newBundleId.
115
99
  * This method assumes project file is in the correct state.
116
- * @param {string} agentPath - Path to the .xcodeproj directory.
117
- * @param {string} newBundleId the new bundle ID used to update.
100
+ * @param agentPath - Path to the .xcodeproj directory.
101
+ * @param newBundleId the new bundle ID used to update.
118
102
  */
119
- async function updateProjectFile (agentPath, newBundleId) {
120
- let projectFilePath = path.resolve(agentPath, PROJECT_FILE);
103
+ export async function updateProjectFile (agentPath: string, newBundleId: string): Promise<void> {
104
+ const projectFilePath = path.resolve(agentPath, PROJECT_FILE);
121
105
  try {
122
106
  // Assuming projectFilePath is in the correct state, create .old from projectFilePath
123
107
  await fs.copyFile(projectFilePath, `${projectFilePath}.old`);
124
108
  await replaceInFile(projectFilePath, new RegExp(_.escapeRegExp(WDA_RUNNER_BUNDLE_ID), 'g'), newBundleId);
125
109
  log.debug(`Successfully updated '${projectFilePath}' with bundle id '${newBundleId}'`);
126
- } catch (err) {
110
+ } catch (err: any) {
127
111
  log.debug(`Error updating project file: ${err.message}`);
128
112
  log.warn(`Unable to update project file '${projectFilePath}' with ` +
129
113
  `bundle id '${newBundleId}'. WebDriverAgent may not start`);
@@ -132,9 +116,9 @@ async function updateProjectFile (agentPath, newBundleId) {
132
116
 
133
117
  /**
134
118
  * Reset WebDriverAgentRunner project bundle ID to correct state.
135
- * @param {string} agentPath - Path to the .xcodeproj directory.
119
+ * @param agentPath - Path to the .xcodeproj directory.
136
120
  */
137
- async function resetProjectFile (agentPath) {
121
+ export async function resetProjectFile (agentPath: string): Promise<void> {
138
122
  const projectFilePath = path.join(agentPath, PROJECT_FILE);
139
123
  try {
140
124
  // restore projectFilePath from .old file
@@ -143,7 +127,7 @@ async function resetProjectFile (agentPath) {
143
127
  }
144
128
  await fs.mv(`${projectFilePath}.old`, projectFilePath);
145
129
  log.debug(`Successfully reset '${projectFilePath}' with bundle id '${WDA_RUNNER_BUNDLE_ID}'`);
146
- } catch (err) {
130
+ } catch (err: any) {
147
131
  log.debug(`Error resetting project file: ${err.message}`);
148
132
  log.warn(`Unable to reset project file '${projectFilePath}' with ` +
149
133
  `bundle id '${WDA_RUNNER_BUNDLE_ID}'. WebDriverAgent has been ` +
@@ -151,27 +135,24 @@ async function resetProjectFile (agentPath) {
151
135
  }
152
136
  }
153
137
 
154
- async function setRealDeviceSecurity (keychainPath, keychainPassword) {
138
+ export async function setRealDeviceSecurity (keychainPath: string, keychainPassword: string): Promise<void> {
155
139
  log.debug('Setting security for iOS device');
156
140
  await exec('security', ['-v', 'list-keychains', '-s', keychainPath]);
157
141
  await exec('security', ['-v', 'unlock-keychain', '-p', keychainPassword, keychainPath]);
158
142
  await exec('security', ['set-keychain-settings', '-t', '3600', '-l', keychainPath]);
159
143
  }
160
144
 
161
- /**
162
- * Information of the device under test
163
- * @typedef {import('./types').DeviceInfo} DeviceInfo
164
- */
165
-
166
145
  /**
167
146
  * Arguments for setting xctestrun file
168
- * @typedef {Object} XctestrunFileArgs
169
- * @property {DeviceInfo} deviceInfo - Information of the device under test
170
- * @property {string} sdkVersion - The Xcode SDK version of OS.
171
- * @property {string} bootstrapPath - The folder path containing xctestrun file.
172
- * @property {number|string} wdaRemotePort - The remote port WDA is listening on.
173
- * @property {string} [wdaBindingIP] - The IP address to bind to. If not given, it binds to all interfaces.
174
147
  */
148
+ export interface XctestrunFileArgs {
149
+ deviceInfo: DeviceInfo;
150
+ sdkVersion: string;
151
+ bootstrapPath: string;
152
+ wdaRemotePort: number | string;
153
+ wdaBindingIP?: string;
154
+ }
155
+
175
156
  /**
176
157
  * Creates xctestrun file per device & platform version.
177
158
  * We expects to have WebDriverAgentRunner_iphoneos${sdkVersion|platformVersion}-arm64.xctestrun for real device
@@ -180,13 +161,13 @@ async function setRealDeviceSecurity (keychainPath, keychainPassword) {
180
161
  * e.g. Xcode which has iOS SDK Version 12.2 on an intel Mac host machine generates WebDriverAgentRunner_iphonesimulator.2-x86_64.xctestrun
181
162
  * even if the cap has platform version 11.4
182
163
  *
183
- * @param {XctestrunFileArgs} args
184
- * @return {Promise<string>} returns xctestrunFilePath for given device
164
+ * @param args
165
+ * @return returns xctestrunFilePath for given device
185
166
  * @throws if WebDriverAgentRunner_iphoneos${sdkVersion|platformVersion}-arm64.xctestrun for real device
186
167
  * or WebDriverAgentRunner_iphonesimulator${sdkVersion|platformVersion}-x86_64.xctestrun for simulator is not found @bootstrapPath,
187
168
  * then it will throw a file not found exception
188
169
  */
189
- async function setXctestrunFile (args) {
170
+ export async function setXctestrunFile (args: XctestrunFileArgs): Promise<string> {
190
171
  const {deviceInfo, sdkVersion, bootstrapPath, wdaRemotePort, wdaBindingIP} = args;
191
172
  const xctestrunFilePath = await getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath);
192
173
  const xctestRunContent = await plist.parsePlistFile(xctestrunFilePath);
@@ -199,12 +180,12 @@ async function setXctestrunFile (args) {
199
180
 
200
181
  /**
201
182
  * Return the WDA object which appends existing xctest runner content
202
- * @param {string} platformName - The name of the platform
203
- * @param {number|string} wdaRemotePort - The remote port number
204
- * @param {string} [wdaBindingIP] - The IP address to bind to. If not given, it binds to all interfaces.
205
- * @return {object} returns a runner object which has USE_PORT and optionally USE_IP
183
+ * @param platformName - The name of the platform
184
+ * @param wdaRemotePort - The remote port number
185
+ * @param wdaBindingIP - The IP address to bind to. If not given, it binds to all interfaces.
186
+ * @return returns a runner object which has USE_PORT and optionally USE_IP
206
187
  */
207
- function getAdditionalRunContent (platformName, wdaRemotePort, wdaBindingIP) {
188
+ export function getAdditionalRunContent (platformName: string, wdaRemotePort: number | string, wdaBindingIP?: string): Record<string, any> {
208
189
  const runner = `WebDriverAgentRunner${isTvOS(platformName) ? '_tvOS' : ''}`;
209
190
  return {
210
191
  [runner]: {
@@ -219,19 +200,18 @@ function getAdditionalRunContent (platformName, wdaRemotePort, wdaBindingIP) {
219
200
 
220
201
  /**
221
202
  * Return the path of xctestrun if it exists
222
- * @param {DeviceInfo} deviceInfo
223
- * @param {string} sdkVersion - The Xcode SDK version of OS.
224
- * @param {string} bootstrapPath - The folder path containing xctestrun file.
225
- * @returns {Promise<string>}
203
+ * @param deviceInfo
204
+ * @param sdkVersion - The Xcode SDK version of OS.
205
+ * @param bootstrapPath - The folder path containing xctestrun file.
226
206
  */
227
- async function getXctestrunFilePath (deviceInfo, sdkVersion, bootstrapPath) {
207
+ export async function getXctestrunFilePath (deviceInfo: DeviceInfo, sdkVersion: string, bootstrapPath: string): Promise<string> {
228
208
  // First try the SDK path, for Xcode 10 (at least)
229
- const sdkBased = [
209
+ const sdkBased: [string, string] = [
230
210
  path.resolve(bootstrapPath, `${deviceInfo.udid}_${sdkVersion}.xctestrun`),
231
211
  sdkVersion,
232
212
  ];
233
213
  // Next try Platform path, for earlier Xcode versions
234
- const platformBased = [
214
+ const platformBased: [string, string] = [
235
215
  path.resolve(bootstrapPath, `${deviceInfo.udid}_${deviceInfo.platformVersion}.xctestrun`),
236
216
  deviceInfo.platformVersion,
237
217
  ];
@@ -261,11 +241,11 @@ async function getXctestrunFilePath (deviceInfo, sdkVersion, bootstrapPath) {
261
241
 
262
242
  /**
263
243
  * Return the name of xctestrun file
264
- * @param {DeviceInfo} deviceInfo
265
- * @param {string} version - The Xcode SDK version of OS.
266
- * @return {string} returns xctestrunFilePath for given device
244
+ * @param deviceInfo
245
+ * @param version - The Xcode SDK version of OS.
246
+ * @return returns xctestrunFilePath for given device
267
247
  */
268
- function getXctestrunFileName (deviceInfo, version) {
248
+ export function getXctestrunFileName (deviceInfo: DeviceInfo, version: string): string {
269
249
  const archSuffix = deviceInfo.isRealDevice
270
250
  ? `os${version}-arm64`
271
251
  : `simulator${version}-${arch() === 'arm64' ? 'arm64' : 'x86_64'}`;
@@ -274,12 +254,8 @@ function getXctestrunFileName (deviceInfo, version) {
274
254
 
275
255
  /**
276
256
  * Ensures the process is killed after the timeout
277
- *
278
- * @param {string} name
279
- * @param {import('teen_process').SubProcess} proc
280
- * @returns {Promise<void>}
281
257
  */
282
- async function killProcess (name, proc) {
258
+ export async function killProcess (name: string, proc: SubProcess | null | undefined): Promise<void> {
283
259
  if (!proc || !proc.isRunning) {
284
260
  return;
285
261
  }
@@ -290,7 +266,7 @@ async function killProcess (name, proc) {
290
266
  try {
291
267
  await proc.stop('SIGTERM', 1000);
292
268
  return;
293
- } catch (err) {
269
+ } catch (err: any) {
294
270
  if (!err.message.includes(`Process didn't end after`)) {
295
271
  throw err;
296
272
  }
@@ -300,7 +276,7 @@ async function killProcess (name, proc) {
300
276
  log.info(`Sending 'SIGKILL'...`);
301
277
  try {
302
278
  await proc.stop('SIGKILL');
303
- } catch (err) {
279
+ } catch (err: any) {
304
280
  if (err.message.includes('not currently running')) {
305
281
  // the process ended but for some reason we were not informed
306
282
  return;
@@ -310,21 +286,16 @@ async function killProcess (name, proc) {
310
286
  }
311
287
 
312
288
  /**
313
- * Generate a random integer.
314
- *
315
- * @return {number} A random integer number in range [low, hight). `low`` is inclusive and `high` is exclusive.
289
+ * Generate a random integer in range [low, high). `low` is inclusive and `high` is exclusive.
316
290
  */
317
- function randomInt (low, high) {
291
+ export function randomInt (low: number, high: number): number {
318
292
  return Math.floor(Math.random() * (high - low) + low);
319
293
  }
320
294
 
321
295
  /**
322
- * Retrieves WDA upgrade timestamp
323
- *
324
- * @return {Promise<number?>} The UNIX timestamp of the package manifest. The manifest only gets modified on
325
- * package upgrade.
296
+ * Retrieves WDA upgrade timestamp. The manifest only gets modified on package upgrade.
326
297
  */
327
- async function getWDAUpgradeTimestamp () {
298
+ export async function getWDAUpgradeTimestamp (): Promise<number | null> {
328
299
  const packageManifest = path.resolve(getModuleRoot(), 'package.json');
329
300
  if (!await fs.exists(packageManifest)) {
330
301
  return null;
@@ -335,11 +306,8 @@ async function getWDAUpgradeTimestamp () {
335
306
 
336
307
  /**
337
308
  * Kills running XCTest processes for the particular device.
338
- *
339
- * @param {string} udid - The device UDID.
340
- * @param {boolean} isSimulator - Equals to true if the current device is a Simulator
341
309
  */
342
- async function resetTestProcesses (udid, isSimulator) {
310
+ export async function resetTestProcesses (udid: string, isSimulator: boolean): Promise<void> {
343
311
  const processPatterns = [`xcodebuild.*${udid}`];
344
312
  if (isSimulator) {
345
313
  processPatterns.push(`${udid}.*XCTRunner`);
@@ -355,21 +323,21 @@ async function resetTestProcesses (udid, isSimulator) {
355
323
  * It is also possible to apply additional filtering based on the
356
324
  * process command line.
357
325
  *
358
- * @param {string|number} port - The port number.
359
- * @param {?Function} filteringFunc - Optional lambda function, which
326
+ * @param port - The port number.
327
+ * @param filteringFunc - Optional lambda function, which
360
328
  * receives command line string of the particular process
361
329
  * listening on given port, and is expected to return
362
330
  * either true or false to include/exclude the corresponding PID
363
331
  * from the resulting array.
364
- * @returns {Promise<string[]>} - the list of matched process ids.
332
+ * @returns - the list of matched process ids.
365
333
  */
366
- async function getPIDsListeningOnPort (port, filteringFunc = null) {
367
- const result = [];
334
+ export async function getPIDsListeningOnPort (port: string | number, filteringFunc: ((cmdline: string) => boolean | Promise<boolean>) | null = null): Promise<string[]> {
335
+ const result: string[] = [];
368
336
  try {
369
337
  // This only works since Mac OS X El Capitan
370
338
  const {stdout} = await exec('lsof', ['-ti', `tcp:${port}`]);
371
339
  result.push(...(stdout.trim().split(/\n+/)));
372
- } catch (e) {
340
+ } catch (e: any) {
373
341
  if (e.code !== 1) {
374
342
  // code 1 means no processes. Other errors need reporting
375
343
  log.debug(`Error getting processes listening on port '${port}': ${e.stderr || e.message}`);
@@ -381,10 +349,10 @@ async function getPIDsListeningOnPort (port, filteringFunc = null) {
381
349
  return result;
382
350
  }
383
351
  return await B.filter(result, async (pid) => {
384
- let stdout;
352
+ let stdout: string;
385
353
  try {
386
354
  ({stdout} = await exec('ps', ['-p', pid, '-o', 'command']));
387
- } catch (e) {
355
+ } catch (e: any) {
388
356
  if (e.code === 1) {
389
357
  // The process does not exist anymore, there's nothing to filter
390
358
  return false;
@@ -395,9 +363,31 @@ async function getPIDsListeningOnPort (port, filteringFunc = null) {
395
363
  });
396
364
  }
397
365
 
398
- export { updateProjectFile, resetProjectFile, setRealDeviceSecurity,
399
- getAdditionalRunContent, getXctestrunFileName,
400
- setXctestrunFile, getXctestrunFilePath, killProcess, randomInt,
401
- getWDAUpgradeTimestamp, resetTestProcesses,
402
- getPIDsListeningOnPort, killAppUsingPattern, isTvOS
403
- };
366
+ // Private functions
367
+
368
+ async function getPIDsUsingPattern (pattern: string): Promise<string[]> {
369
+ const args = [
370
+ '-if', // case insensitive, full cmdline match
371
+ pattern,
372
+ ];
373
+ try {
374
+ const {stdout} = await exec('pgrep', args);
375
+ return stdout.split(/\s+/)
376
+ .map((x) => parseInt(x, 10))
377
+ .filter(_.isInteger)
378
+ .map((x) => `${x}`);
379
+ } catch (err: any) {
380
+ log.debug(`'pgrep ${args.join(' ')}' didn't detect any matching processes. Return code: ${err.code}`);
381
+ return [];
382
+ }
383
+ }
384
+
385
+ async function replaceInFile (file: string, find: string | RegExp, replace: string): Promise<void> {
386
+ const contents = await fs.readFile(file, 'utf8');
387
+
388
+ const newContents = contents.replace(find, replace);
389
+ if (newContents !== contents) {
390
+ await fs.writeFile(file, newContents, 'utf8');
391
+ }
392
+ }
393
+