@deeeed/metamask-harness 0.28.0 → 0.29.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 (52) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +41 -0
  3. package/adapters/extension/build-lavamoat.sh +2 -1
  4. package/adapters/extension/ensure-browser.sh +82 -9
  5. package/adapters/extension/inject.mjs +1 -0
  6. package/adapters/extension/launch-browser.cjs +83 -1
  7. package/adapters/extension/lib/chrome-args.cjs +325 -1
  8. package/adapters/extension/lib/playwright-cdp.cjs +34 -0
  9. package/adapters/extension/lib/slot-title.cjs +2 -4
  10. package/adapters/extension/lib/validation-launch-supervisor.cjs +292 -0
  11. package/adapters/extension/lib/validation-process-ownership.cjs +69 -0
  12. package/adapters/extension/reattach.sh +2 -1
  13. package/adapters/extension/sidepanel-toggle.sh +14 -96
  14. package/adapters/extension/wallet-fixture-state.cjs +8 -31
  15. package/adapters/manifest.json +16 -0
  16. package/adapters/shared/private-atomic-write.cjs +47 -0
  17. package/adapters/shared/setup-base.sh +864 -0
  18. package/dist/adapters/extension/runtime.js +367 -24
  19. package/dist/adapters/extension/validation-process-ownership.js +10 -0
  20. package/dist/cli-commands.js +1 -0
  21. package/dist/command-contract.js +12 -0
  22. package/dist/commands/launch/extension.js +130 -19
  23. package/dist/commands/setup-base.js +24 -0
  24. package/dist/mm-harness-cli.js +28 -2
  25. package/library/actions/extension/analytics/consent.mjs +203 -0
  26. package/library/actions/extension/analytics/set_consent.mjs +19 -143
  27. package/library/actions/extension/perps/perps.mjs +2 -16
  28. package/library/actions/extension/perps/state.mjs +20 -0
  29. package/library/actions/extension/wallet/list_accounts.mjs +3 -25
  30. package/library/actions/extension/wallet/read_state.mjs +3 -23
  31. package/library/actions/extension/wallet/select_account.mjs +6 -33
  32. package/library/actions/extension/wallet/setup.mjs +2 -20
  33. package/library/actions/extension/wallet/state.mjs +111 -0
  34. package/library/recipes/runner/action-validation.extension.recipe.json +1 -1
  35. package/library/recipes/runner/action-validation.mobile.recipe.json +1 -1
  36. package/package.json +7 -4
  37. package/scripts/site-contrast.mjs +538 -0
  38. package/site/architecture.html +415 -0
  39. package/site/assets/progress.mjs +272 -0
  40. package/site/assets/style.css +808 -0
  41. package/site/cheatsheet.html +305 -0
  42. package/site/index.html +643 -0
  43. package/site/recipes.html +396 -0
  44. package/site/reviewers.html +374 -0
  45. package/site/tutorials/index.html +180 -0
  46. package/site/tutorials/v1.html +211 -0
  47. package/site/tutorials/v2.html +207 -0
  48. package/site/tutorials/v3.html +214 -0
  49. package/site/tutorials/v4.html +195 -0
  50. package/site/tutorials/v5.html +163 -0
  51. package/site/tutorials/v6.html +165 -0
  52. package/site/tutorials/v7.html +184 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.29.0 - 2026-08-01
6
+
7
+ ### Added
8
+
9
+ - Add `mm-harness setup-base` to bootstrap the shared default MetaMask checkout layout with interactive and scripted selection, persisted preferences, disk checks, dependency installation, and machine-readable summaries.
10
+ - Ship a dependency-free getting-started site with an interactive walkthrough, platform-filterable command reference, architecture guide, tutorials, reviewer evidence guide, and automated browser acceptance and contrast checks.
11
+
12
+ ### Changed
13
+
14
+ - Upgrade the Farmslot runtime dependencies to their structured suite-evidence releases and converge on protocol 0.15.0.
15
+
16
+ ### Fixed
17
+
18
+ - Verify Extension MetaMetrics consent from the production background controller's flat state instead of relying on test-only page state hooks.
19
+ - Resolve Extension Perps network state from both flattened and nested controller stores.
20
+ - Update both supported `brace-expansion` lines to their patched releases for CVE-2026-14257.
21
+ - Build the non-test Extension proof runtime with its webpack sources and supported debug hooks, then read wallet actions from clean app state and wait for account selection to converge.
22
+ - Bind Extension runtime reuse to a per-launch nonce echoed by Chrome, with owner-only state, exact process-argument checks, and machine-global quarantine for unproven detached launches; keep validation launches on the invoking runner with checkout-contained runtime files; route Playwright-owned Extension injections through raw CDP; and require the hardened-page-safe recipe harness for page scrolling.
23
+
5
24
  ## 0.28.0 - 2026-07-31
6
25
 
7
26
  ### Added
package/README.md CHANGED
@@ -10,6 +10,47 @@ and runtime paths are detected automatically.
10
10
  The generic graph engine and evidence schemas live in Farmslot packages.
11
11
  `mm-harness` owns MetaMask runtime control and domain capabilities.
12
12
 
13
+ ## Getting started from zero
14
+
15
+ No checkouts yet? `mm-harness setup-base` clones the MetaMask product repos into one
16
+ standard layout and runs each repo's own dependency install — no install of this
17
+ package required to start:
18
+
19
+ ```bash
20
+ npx -p @deeeed/metamask-harness mm-harness setup-base
21
+ ```
22
+
23
+ It produces, under `~/dev/metamask` by default:
24
+
25
+ ```text
26
+ metamask-extension-1..N metamask-mobile-1..N core-1..N
27
+ ```
28
+
29
+ Run without flags in a terminal, it asks which projects you want and how many
30
+ copies of each. Pass flags to skip the prompt entirely — which is how scripts,
31
+ CI, and parent tools should invoke it:
32
+
33
+ ```bash
34
+ mm-harness setup-base --only core --counts core=1
35
+ mm-harness setup-base --dir ~/work/metamask --counts extension=3,mobile=1,core=2
36
+ mm-harness setup-base --only mobile --dry-run # plan only, changes nothing
37
+ ```
38
+
39
+ The base directory resolves by precedence: `--dir`, then `$MM_HARNESS_BASE_DIR`,
40
+ then saved preferences, then `~/dev/metamask`. A successful run saves the base
41
+ directory and counts it used, so later runs adopt them — inspect with
42
+ `--show-config`, clear with `--reset-config`. Re-running is safe: existing
43
+ clones are fetched, never reset and never deleted.
44
+
45
+ **Scope fence.** It clones and installs dependencies. Nothing else — no platform
46
+ toolchains, no simulators, no `.env` files, no builds. Workflow onboarding is a
47
+ prompt; environment readiness is `mm-harness doctor`, which is exactly where it
48
+ points you next.
49
+
50
+ `setup-base` is a normal command on the single `mm-harness` entrypoint. Its work
51
+ stays in one readable shell leaf, and `npx -p` makes that same command available
52
+ before a global install.
53
+
13
54
  ## Install
14
55
 
15
56
  ```bash
@@ -49,7 +49,8 @@ rm -f "$RUNTIME_DIR/webpack.log" "$RUNTIME_DIR/recipe-harness-webpack.log"
49
49
 
50
50
  echo "[recipe-harness] clean non-test LavaMoat build"
51
51
  rm -rf node_modules/.cache/webpack
52
- yarn webpack:lavamoat:build
52
+ yarn webpack:tsc
53
+ METAMASK_DEBUG=true yarn webpack:lavamoat:build
53
54
  test -f dist/chrome/manifest.json || {
54
55
  echo "build-lavamoat: build completed without dist/chrome/manifest.json" >&2
55
56
  exit 1
@@ -200,6 +200,21 @@ kill_profile_processes() {
200
200
  fi
201
201
  }
202
202
 
203
+ # Invalidate the old browser identity before signaling it. A concurrent quick
204
+ # launch must fall through to process ownership while replacement is underway.
205
+ node - "$SCRIPT_DIR/lib/chrome-args.cjs" "$AGENT_DIR" <<'NODE'
206
+ const [modulePath, runtimeDir] = process.argv.slice(2);
207
+ try {
208
+ require(modulePath).removeRuntimeIdentity(runtimeDir);
209
+ } catch (error) {
210
+ const detail = error instanceof Error ? error.message : String(error);
211
+ throw new Error(
212
+ 'Failed to invalidate Extension runtime identity under ' + runtimeDir + ': ' + detail +
213
+ '. Next: remove the invalid runtime identity and fix owner permissions, then rerun: mm-harness launch --adapter extension --build'
214
+ );
215
+ }
216
+ NODE
217
+
203
218
  # --- Kill existing browser (by PID only — don't touch other slots) ---
204
219
  PREV_PID=$(cat "${AGENT_DIR}/browser.pid" 2>/dev/null || true)
205
220
  if [ -n "$PREV_PID" ] && kill -0 "$PREV_PID" 2>/dev/null; then
@@ -275,12 +290,16 @@ exec node -e "
275
290
  const path = require('path');
276
291
  const fs = require('fs');
277
292
  const http = require('http');
278
- const { execSync, execFileSync } = require('child_process');
293
+ const { execFileSync } = require('child_process');
279
294
  let chromium; try { chromium = require('@playwright/test').chromium; } catch { chromium = require('playwright').chromium; }
280
295
  const {
281
296
  automationRuntimeArgs,
297
+ createRuntimeIdentityNonce,
282
298
  isolatedProfileArgs,
283
299
  remoteDebuggingArgs,
300
+ removeRuntimeIdentity,
301
+ runtimeIdentityArgs,
302
+ writeRuntimeIdentity,
284
303
  } = require(path.join('${SCRIPT_DIR}', 'lib/chrome-args.cjs'));
285
304
  const { captureMacFrontmostProcess, restoreMacFrontmostProcess } = require(path.join('${SCRIPT_DIR}', 'lib/macos-focus.cjs'));
286
305
 
@@ -304,11 +323,14 @@ const resumeWebpack = () => {
304
323
  };
305
324
 
306
325
  (async () => {
326
+ const runtimeNonce = createRuntimeIdentityNonce();
327
+ const startedAt = Date.now();
307
328
  const args = [
308
329
  '--user-data-dir=' + PROFILE,
309
330
  '--disable-extensions-except=' + EXTENSION,
310
331
  '--load-extension=' + EXTENSION,
311
332
  ...automationRuntimeArgs(),
333
+ ...runtimeIdentityArgs(runtimeNonce),
312
334
  '--no-first-run',
313
335
  '--no-default-browser-check',
314
336
  ...isolatedProfileArgs(),
@@ -340,14 +362,63 @@ const resumeWebpack = () => {
340
362
  }
341
363
  const browser = await chromium.connectOverCDP('http://127.0.0.1:' + CDP_PORT);
342
364
  const ctx = browser.contexts()[0];
365
+ let listenerPids;
343
366
  try {
344
- const browserPid = execSync('lsof -ti tcp:' + CDP_PORT + ' -sTCP:LISTEN | head -1', { timeout: 2000 }).toString().trim();
345
- if (browserPid) {
346
- fs.writeFileSync(path.join(AGENT_DIR, 'browser.pid'), browserPid);
347
- fs.writeFileSync(path.join(AGENT_DIR, 'chromium.pid'), browserPid);
348
- console.log('[reopen] Chromium PID ' + browserPid);
349
- }
350
- } catch {}
367
+ listenerPids = execFileSync(
368
+ 'lsof',
369
+ ['-nP', '-iTCP:' + CDP_PORT, '-sTCP:LISTEN', '-t'],
370
+ { encoding: 'utf8', timeout: 10000 },
371
+ ).split(/\s+/).filter((value) => /^\d+$/.test(value));
372
+ } catch {
373
+ throw new Error(
374
+ 'Reopened Chrome ownership could not be inspected with lsof. ' +
375
+ 'Next: install or restore lsof, then rerun: mm-harness launch --adapter extension --build'
376
+ );
377
+ }
378
+ let browserPid = '';
379
+ for (const pid of listenerPids) {
380
+ try {
381
+ const command = execFileSync(
382
+ 'ps',
383
+ ['-ww', '-o', 'command=', '-p', pid],
384
+ { encoding: 'utf8', timeout: 2000 },
385
+ );
386
+ if (
387
+ command.includes('--user-data-dir=' + PROFILE) &&
388
+ (
389
+ command.includes('--load-extension=' + EXTENSION) ||
390
+ command.includes('--disable-extensions-except=' + EXTENSION)
391
+ )
392
+ ) {
393
+ browserPid = pid;
394
+ break;
395
+ }
396
+ } catch {}
397
+ }
398
+ if (!browserPid) {
399
+ throw new Error(
400
+ 'Reopened Chrome did not expose an owned CDP listener on port ' + CDP_PORT +
401
+ '. Next: inspect the slot browser process, then rerun: mm-harness launch --adapter extension --build'
402
+ );
403
+ }
404
+ fs.writeFileSync(path.join(AGENT_DIR, 'browser.pid'), browserPid);
405
+ fs.writeFileSync(path.join(AGENT_DIR, 'chromium.pid'), browserPid);
406
+ try {
407
+ writeRuntimeIdentity(AGENT_DIR, {
408
+ port: Number(CDP_PORT),
409
+ pid: Number(browserPid),
410
+ startedAt,
411
+ nonce: runtimeNonce,
412
+ });
413
+ } catch (error) {
414
+ try { process.kill(Number(browserPid), 'SIGTERM'); } catch {}
415
+ const detail = error && error.message ? error.message : String(error);
416
+ throw new Error(
417
+ 'Failed to persist Extension runtime identity under ' + AGENT_DIR + ': ' + detail +
418
+ '. Next: fix owner write permissions for the runtime directory, then rerun: mm-harness launch --adapter extension --build'
419
+ );
420
+ }
421
+ console.log('[reopen] Chromium PID ' + browserPid);
351
422
 
352
423
  // Detect extension ID via the recipe-runner — the single source of truth.
353
424
  // Deterministic id from the loaded dist's manifest key; replaces a fragile
@@ -371,7 +442,8 @@ const resumeWebpack = () => {
371
442
 
372
443
  // Set window title to slot ID (persists across SPA navigations)
373
444
  const { applyPersistentSlotTitle } = require(path.join('${SCRIPT_DIR}', 'lib/slot-title.cjs'));
374
- await page.evaluate(applyPersistentSlotTitle, SLOT_ID);
445
+ const { evaluatePageViaCdp } = require(path.join('${SCRIPT_DIR}', 'lib/playwright-cdp.cjs'));
446
+ await evaluatePageViaCdp(page, applyPersistentSlotTitle, SLOT_ID);
375
447
 
376
448
  // Wait for runtime state to settle, then unlock if needed.
377
449
  const unlockSelector = '[data-testid=\"unlock-password\"]';
@@ -438,6 +510,7 @@ const resumeWebpack = () => {
438
510
  process.exit(0);
439
511
  })().catch(e => {
440
512
  resumeWebpack();
513
+ try { removeRuntimeIdentity(AGENT_DIR); } catch {}
441
514
  try { fs.unlinkSync(path.join(AGENT_DIR, 'browser.pid')); } catch {}
442
515
  try { fs.unlinkSync(path.join(AGENT_DIR, 'chromium.pid')); } catch {}
443
516
  try { fs.unlinkSync(path.join(AGENT_DIR, 'extension.id')); } catch {}
@@ -126,6 +126,7 @@ copyFile(path.join(runnerDir, 'adapters/shared/tmux-session.sh'), path.join(harn
126
126
  copyFile(path.join(runnerDir, 'adapters/shared/tmux-viewer.sh'), path.join(harnessDir, 'scripts/lib/tmux-viewer.sh'));
127
127
  copyFile(path.join(runnerDir, 'adapters/shared/log-tui.mjs'), path.join(harnessDir, 'scripts/lib/log-tui.mjs'));
128
128
  copyFile(path.join(runnerDir, 'adapters/extension/lib/extension-id.cjs'), path.join(harnessDir, 'scripts/lib/extension-id.cjs'));
129
+ copyFile(path.join(runnerDir, 'adapters/extension/lib/playwright-cdp.cjs'), path.join(harnessDir, 'scripts/lib/playwright-cdp.cjs'));
129
130
  copyFile(path.join(runnerDir, 'adapters/extension/lib/slot-title.cjs'), path.join(harnessDir, 'scripts/lib/slot-title.cjs'));
130
131
  copyFile(path.join(runnerDir, 'adapters/extension/lib/chrome-args.cjs'), path.join(harnessDir, 'scripts/lib/chrome-args.cjs'));
131
132
  copyFile(path.join(runnerDir, 'adapters/extension/lib/macos-focus.cjs'), path.join(harnessDir, 'scripts/lib/macos-focus.cjs'));
@@ -26,8 +26,19 @@ const { execFileSync, spawn, spawnSync } = require('node:child_process');
26
26
  const { extensionIdFromExtensionDir } = require('./lib/extension-id.cjs');
27
27
  const {
28
28
  automationRuntimeArgs,
29
+ clearDetachedLaunchUnproven,
30
+ clearValidationPortQuarantine,
31
+ createRuntimeIdentityNonce,
32
+ hasDetachedLaunchUnproven,
29
33
  isolatedProfileArgs,
34
+ markDetachedLaunchUnproven,
35
+ markValidationPortLaunchUnproven,
30
36
  remoteDebuggingArgs,
37
+ removeRuntimeIdentity,
38
+ runtimeIdentityArgs,
39
+ readValidationPortQuarantine,
40
+ validationLaunchQuarantineError,
41
+ writeRuntimeIdentity,
31
42
  } = require('./lib/chrome-args.cjs');
32
43
  const { captureMacFrontmostProcess, restoreMacFrontmostProcess } = require('./lib/macos-focus.cjs');
33
44
 
@@ -46,6 +57,7 @@ if (!Number.isInteger(cdpPort) || cdpPort <= 0) {
46
57
  for (const key of ['chrome-bin', 'profile', 'extension-dir', 'chrome-log', 'chrome-pid']) {
47
58
  if (!args[key]) throw new Error(`Missing --${key}`);
48
59
  }
60
+ const runtimeDir = path.dirname(path.resolve(args['extension-dir']));
49
61
  // --stop-only releases the profile before the dist snapshot exists on a first
50
62
  // run, so launch-input validation applies only to a real launch.
51
63
  if (args['stop-only'] === undefined) {
@@ -80,6 +92,15 @@ if (foreignPids.length > 0) {
80
92
  const ownedPids = new Set(listenerPids);
81
93
  const previousPid = readPidFile(args['chrome-pid']);
82
94
  if (previousPid !== null && processIsOwnedHarnessBrowser(previousPid, args.profile, args['extension-dir'])) ownedPids.add(previousPid);
95
+ try {
96
+ removeRuntimeIdentity(runtimeDir);
97
+ } catch (error) {
98
+ const detail = error instanceof Error ? error.message : String(error);
99
+ throw new Error(
100
+ `Failed to invalidate Extension runtime identity under ${runtimeDir}: ${detail}. ` +
101
+ 'Next: remove the invalid runtime identity and fix owner permissions, then rerun: mm-harness launch --adapter extension --build',
102
+ );
103
+ }
83
104
  terminatePids([...ownedPids]);
84
105
  if (args['reset-profile'] !== undefined) {
85
106
  fs.rmSync(args.profile, { recursive: true, force: true });
@@ -95,7 +116,25 @@ if (args['stop-only'] !== undefined) {
95
116
  process.exit(0);
96
117
  }
97
118
 
119
+ const requestedProfileQuarantined = hasDetachedLaunchUnproven(args.profile);
120
+ const portQuarantine = readValidationPortQuarantine(cdpPort);
121
+ const validationPortLease = process.env.MM_HARNESS_VALIDATION_PORT_LEASE;
122
+ const activeValidationLease = Boolean(
123
+ portQuarantine?.profile === path.resolve(args.profile) &&
124
+ portQuarantine.lease === validationPortLease &&
125
+ /^[a-f0-9]{64}$/u.test(validationPortLease || ''),
126
+ );
127
+ if (requestedProfileQuarantined || (portQuarantine && !activeValidationLease)) {
128
+ throw validationLaunchQuarantineError(
129
+ cdpPort,
130
+ portQuarantine?.profile || (requestedProfileQuarantined ? args.profile : undefined),
131
+ requestedProfileQuarantined ? args.profile : undefined,
132
+ );
133
+ }
134
+
98
135
  const initialUrl = args['start-url'] || extensionHomeUrl(args['extension-dir']) || 'chrome://extensions/';
136
+ const runtimeNonce = createRuntimeIdentityNonce();
137
+ const startedAt = Date.now();
99
138
  const chromeArgs = [
100
139
  `--user-data-dir=${args.profile}`,
101
140
  // Debug-port trio (address + port + scoped allow-origins) from the shared module,
@@ -104,6 +143,7 @@ const chromeArgs = [
104
143
  '--no-first-run',
105
144
  '--disable-first-run-ui',
106
145
  ...automationRuntimeArgs(),
146
+ ...runtimeIdentityArgs(runtimeNonce),
107
147
  ...isolatedProfileArgs(),
108
148
  '--disable-default-apps',
109
149
  '--disable-popup-blocking',
@@ -123,6 +163,7 @@ let browserPid;
123
163
  try {
124
164
  const application = process.platform === 'darwin' ? macApplicationForExecutable(args['chrome-bin']) : null;
125
165
  if (application) {
166
+ beginDetachedLaunch(cdpPort, args.profile, activeValidationLease);
126
167
  execFileSync('open', ['-g', '-n', '-a', application, '--args', ...chromeArgs], {
127
168
  env: sanitizedChildEnv(),
128
169
  stdio: ['ignore', logFd, logFd],
@@ -132,18 +173,46 @@ try {
132
173
  throw new Error(`Chrome launched but did not expose CDP on 127.0.0.1:${cdpPort}`);
133
174
  }
134
175
  } else {
176
+ beginDetachedLaunch(cdpPort, args.profile, activeValidationLease);
135
177
  const child = spawn(args['chrome-bin'], chromeArgs, {
136
178
  detached: true,
137
179
  env: sanitizedChildEnv(),
138
180
  stdio: ['ignore', logFd, logFd],
139
181
  });
140
182
  child.unref();
141
- browserPid = child.pid;
183
+ browserPid = waitForOwnedCdpPid(cdpPort, args.profile, args['extension-dir']);
184
+ if (browserPid === null) {
185
+ if (child.pid) terminatePids([child.pid]);
186
+ throw new Error(
187
+ `Chrome launched but did not expose an owned CDP listener on 127.0.0.1:${cdpPort}. ` +
188
+ `Next: inspect ${args['chrome-log']}, then rerun: mm-harness launch --adapter extension --build`,
189
+ );
190
+ }
142
191
  }
143
192
  } finally {
144
193
  fs.closeSync(logFd);
145
194
  }
146
195
  fs.writeFileSync(args['chrome-pid'], `${browserPid}\n`);
196
+ try {
197
+ writeRuntimeIdentity(runtimeDir, {
198
+ port: cdpPort,
199
+ pid: browserPid,
200
+ startedAt,
201
+ nonce: runtimeNonce,
202
+ });
203
+ clearDetachedLaunchUnproven(args.profile);
204
+ if (!activeValidationLease) {
205
+ clearValidationPortQuarantine(cdpPort, args.profile);
206
+ }
207
+ } catch (error) {
208
+ terminatePids([browserPid]);
209
+ fs.rmSync(args['chrome-pid'], { force: true });
210
+ const detail = error instanceof Error ? error.message : String(error);
211
+ throw new Error(
212
+ `Failed to persist Extension runtime identity under ${runtimeDir}: ${detail}. ` +
213
+ 'Next: fix owner write permissions for the runtime directory, then rerun: mm-harness launch --adapter extension --build',
214
+ );
215
+ }
147
216
  if (process.platform === 'darwin') {
148
217
  const wake = spawn('caffeinate', ['-dimsu', '-w', String(browserPid)], {
149
218
  detached: true,
@@ -167,6 +236,19 @@ function sanitizedChildEnv() {
167
236
  };
168
237
  }
169
238
 
239
+ function beginDetachedLaunch(port, profile, activeLease) {
240
+ try {
241
+ if (!activeLease) markValidationPortLaunchUnproven(port, profile);
242
+ markDetachedLaunchUnproven(profile);
243
+ } catch (error) {
244
+ if (error.code === 'EEXIST') {
245
+ const quarantine = readValidationPortQuarantine(port);
246
+ throw validationLaunchQuarantineError(port, quarantine?.profile || profile);
247
+ }
248
+ throw error;
249
+ }
250
+ }
251
+
170
252
  function macApplicationForExecutable(executable) {
171
253
  let current = path.resolve(executable);
172
254
  while (current !== path.dirname(current)) {