@deeeed/metamask-harness 0.17.4 → 0.18.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 (93) hide show
  1. package/CHANGELOG.md +775 -0
  2. package/README.md +65 -71
  3. package/adapters/extension/ensure-browser.sh +12 -1
  4. package/adapters/extension/inject.mjs +7 -0
  5. package/adapters/extension/launch-browser.cjs +10 -3
  6. package/adapters/extension/lib/chrome-args.cjs +31 -0
  7. package/adapters/extension/lib/macos-focus.cjs +32 -0
  8. package/adapters/extension/live.sh +10 -20
  9. package/adapters/manifest.json +8 -0
  10. package/adapters/mobile/open-device.sh +45 -7
  11. package/adapters/mobile/verify.sh +15 -3
  12. package/adapters/shared/harness-source-fingerprint.mjs +49 -0
  13. package/adapters/shared/install-repo-deps.sh +1 -5
  14. package/adapters/shared/open-debug.mjs +184 -103
  15. package/adapters/shared/resolve-slot-ports-core.mjs +23 -6
  16. package/adapters/shared/resolve-slot-ports.sh +22 -5
  17. package/bin/mm-harness +15 -3
  18. package/dist/adapters/core/surface.js +4 -1
  19. package/dist/adapters/extension/console-capture.js +3 -1
  20. package/dist/adapters/extension/harness-freshness.js +39 -0
  21. package/dist/adapters/extension/product-config.js +110 -0
  22. package/dist/adapters/extension/runtime-decision.js +20 -71
  23. package/dist/adapters/extension/surface.js +19 -1
  24. package/dist/adapters/mobile/prepare.js +17 -0
  25. package/dist/adapters/mobile/source-freshness.js +26 -41
  26. package/dist/adapters/mobile/surface.js +4 -1
  27. package/dist/adapters/resolve-slot-ports.js +2 -0
  28. package/dist/adapters/slot-ports.js +13 -32
  29. package/dist/adapters.js +50 -17
  30. package/dist/checkout-lock.js +27 -2
  31. package/dist/cli-color.js +19 -0
  32. package/dist/cli-commands.js +1 -1
  33. package/dist/cli.js +2 -3
  34. package/dist/command-contract.js +13 -3
  35. package/dist/commands/call.js +115 -29
  36. package/dist/commands/checklist.js +4 -1
  37. package/dist/commands/completion-candidates.js +20 -13
  38. package/dist/commands/debug.js +31 -38
  39. package/dist/commands/doctor.js +33 -6
  40. package/dist/commands/fixtures.js +65 -17
  41. package/dist/commands/flows.js +39 -10
  42. package/dist/commands/launch/extension.js +40 -15
  43. package/dist/commands/launch/index.js +41 -5
  44. package/dist/commands/list-executables.js +151 -29
  45. package/dist/commands/manifest.js +127 -18
  46. package/dist/commands/parse-args.js +11 -1
  47. package/dist/commands/run-engine.js +384 -56
  48. package/dist/commands/run.js +112 -17
  49. package/dist/commands/shared.js +22 -2
  50. package/dist/commands/status-probe.js +3 -0
  51. package/dist/commands/status.js +1 -0
  52. package/dist/completions-cache.js +1 -1
  53. package/dist/doctor.js +56 -6
  54. package/dist/harness.js +6 -5
  55. package/dist/heal-bounds.js +1 -1
  56. package/dist/live-adapter-contract.js +132 -12
  57. package/dist/manifest.js +161 -1
  58. package/dist/mm-harness-cli.js +13 -7
  59. package/dist/recipe-security.js +178 -0
  60. package/dist/runner.js +87 -13
  61. package/dist/runtime-context.js +8 -26
  62. package/docs/CONTRIBUTING.md +137 -0
  63. package/docs/QA.md +185 -0
  64. package/docs/RECIPES.md +161 -0
  65. package/docs/SECURITY.md +88 -0
  66. package/library/actions/core/perps/read_account.mjs +2 -2
  67. package/library/actions/core/perps/read_orders.mjs +2 -1
  68. package/library/actions/core/perps/read_positions.mjs +2 -1
  69. package/library/actions/core/wallet/list_accounts.mjs +95 -0
  70. package/library/actions/extension/platform/cdp.mjs +1 -0
  71. package/library/actions/extension/wallet/list_accounts.mjs +41 -0
  72. package/library/actions/mobile/platform/bridge.mjs +1 -5
  73. package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
  74. package/library/manifests/core.action-manifest.json +61 -0
  75. package/library/manifests/extension.action-manifest.json +53 -0
  76. package/library/manifests/mobile.action-manifest.json +53 -0
  77. package/library/recipes/runner/action-validation.extension.recipe.json +8 -1
  78. package/library/recipes/runner/action-validation.mobile.recipe.json +8 -1
  79. package/package.json +10 -6
  80. package/scripts/completions.sh +7 -7
  81. package/docs/ADAPTER-SURFACE.md +0 -26
  82. package/docs/CHEATSHEET.md +0 -48
  83. package/docs/CLI-ERGONOMICS-AUDIT.md +0 -32
  84. package/docs/CLI-ERGONOMICS-HUMAN-QA.md +0 -64
  85. package/docs/CODE-MAP.md +0 -62
  86. package/docs/UX-PRINCIPLES.md +0 -66
  87. package/docs/VIDEO-DEMO-VALIDATION.md +0 -74
  88. package/docs/architecture.md +0 -88
  89. package/docs/live-adapter-contract.md +0 -190
  90. package/docs/package-boundaries.md +0 -47
  91. package/docs/perps-flow-catalog.md +0 -235
  92. package/docs/recipe-libraries.md +0 -71
  93. package/docs/runtime-file-conventions.md +0 -36
package/README.md CHANGED
@@ -1,22 +1,15 @@
1
1
  # mm-harness
2
2
 
3
- `mm-harness` gives people and agents one CLI for operating MetaMask Mobile,
4
- Extension, and Core. Run it inside a checkout; the product is auto-detected.
3
+ One CLI for operating MetaMask Extension, Mobile, and Core and producing
4
+ reviewable recipe evidence. Run it inside a checkout; the product, slot, ports,
5
+ and runtime paths are detected automatically.
5
6
 
6
- It has two jobs:
7
+ - **Action:** one typed operation.
8
+ - **Flow:** a reusable, parameterized sequence of actions.
9
+ - **Recipe:** the complete proof for a task or regression.
7
10
 
8
- - operate the app with the same controls a developer needs: launch, logs, debug,
9
- fixtures, actions, and reusable flows;
10
- - execute deterministic recipes and save reviewable proof.
11
-
12
- The generic graph engine lives in `@farmslot/recipe-harness`. This package owns
13
- only MetaMask-specific actions and runtime control.
14
-
15
- One mental model:
16
-
17
- - **Action** — one typed operation, such as reading wallet state.
18
- - **Flow** — a reusable, parameterized sequence of actions.
19
- - **Recipe** — the complete executable proof for a task or regression.
11
+ The generic graph engine and evidence schemas live in Farmslot packages.
12
+ `mm-harness` owns MetaMask runtime control and domain capabilities.
20
13
 
21
14
  ## Install
22
15
 
@@ -26,112 +19,113 @@ mm-harness --version
26
19
  mm-harness doctor
27
20
  ```
28
21
 
29
- Use `mm-harness update` to install the latest published version.
22
+ `doctor` is read-only. `doctor --fix` repairs harness-owned runtime state but
23
+ does not launch an app, invent credentials, or choose a wallet fixture.
24
+
25
+ ```bash
26
+ mm-harness doctor --fix
27
+ mm-harness fixtures init --from /secure/path/wallet-fixture.json
28
+ # Disposable public testing only; never fund this wallet:
29
+ mm-harness fixtures init --dev
30
+ ```
30
31
 
31
- ## Daily workflow
32
+ Use `mm-harness update` to update a published installation.
32
33
 
33
- From a MetaMask checkout:
34
+ ## Operate
34
35
 
35
36
  ```bash
36
37
  # Extension
37
- mm-harness launch # starts/reuses Chrome and the incremental watcher
38
- mm-harness launch --sidepanel # open the sidepanel surface
38
+ mm-harness launch
39
+ mm-harness launch --sidepanel
39
40
 
40
41
  # Mobile
41
42
  mm-harness launch ios
42
43
  mm-harness launch android
43
44
 
44
- # Any product
45
+ # Any checkout
45
46
  mm-harness status
47
+ mm-harness stop
46
48
  mm-harness logs
47
49
  mm-harness debug
48
50
  mm-harness fixtures set
49
51
  ```
50
52
 
51
- `launch` installs the checkout-local runtime overlay when needed. On Extension,
52
- the normal launch keeps `yarn start` running after the command exits. Source
53
- edits compile incrementally into the isolated runtime; refresh the active page or
54
- sidepanel to see them. Use `launch --build` only when a clean rebuild is needed.
53
+ Extension `launch` keeps its incremental watcher running. Refresh the active
54
+ page after a successful rebuild; use `launch --build` only for a clean rebuild.
55
55
 
56
- `debug` opens DevTools for the active Extension target in that checkout's Chrome
57
- profile. Mobile opens the React Native developer surface.
58
-
59
- Filter logs when needed:
56
+ Log sources stay separate:
60
57
 
61
58
  ```bash
62
- mm-harness logs --source extension # Extension console
63
- mm-harness logs --source dapp # active dapp console
64
- mm-harness logs --source webpack # Extension compiler
65
- mm-harness logs --source app # Mobile app events
59
+ mm-harness logs --source extension # Extension page
60
+ mm-harness logs --source dapp # active dapp
61
+ mm-harness logs --source webpack # compiler
62
+ mm-harness logs --source watcher # watcher lifecycle
63
+ mm-harness logs --source rebuild # incremental rebuilds
64
+ mm-harness logs --source app # Mobile app
66
65
  mm-harness logs --source metro # Mobile bundler
67
66
  ```
68
67
 
69
- ## Build and run proof
68
+ ## Discover and prove
70
69
 
71
70
  ```bash
72
71
  mm-harness actions positions
73
72
  mm-harness actions --action metamask.wallet.ensure_unlocked
74
73
  mm-harness call metamask.wallet.ensure_unlocked
75
74
  mm-harness flows
75
+ mm-harness flows describe perps.clean_market_testnet
76
76
 
77
+ mm-harness run --list
78
+ mm-harness run wallet.smoke --describe
77
79
  mm-harness run path/to/recipe.json --plan
78
- mm-harness run path/to/recipe.json --artifacts-dir temp/recipe-proof
79
- mm-harness run wallet.smoke --artifacts-dir temp/wallet-proof
80
+ mm-harness run path/to/recipe.json
80
81
  ```
81
82
 
82
- `call` exercises one action. A recipe composes actions and flows into a
83
- deterministic proof.
84
-
85
- A run prints its findings, diagnostics, and absolute evidence paths. The artifact
86
- directory contains the executed recipe, summary, trace, diagnostics when
87
- applicable, screenshots/logs, and `artifact-manifest.json`. Core is headless, so
88
- browser/device diagnostics do not apply.
83
+ `run` selects a checkout-local artifact directory unless
84
+ `--artifacts-dir <dir>` overrides it. Human output prints diagnostics and
85
+ absolute paths to the report, trace, executed recipe, and artifact manifest.
86
+ Use `mm-harness last --json` to resume without repeating the last operation.
89
87
 
90
- Use `mm-harness last --json` to resume after interruption without repeating a
91
- side-effecting command.
88
+ For automation, `--json` emits one stable document. `run --json-stream` emits
89
+ line-buffered JSONL progress and a terminal event.
92
90
 
93
- ## Team recipe libraries
94
-
95
- Add a named shared directory with `RECIPE_LIBRARY_PATH`:
91
+ ## Team libraries
96
92
 
97
93
  ```bash
98
94
  export RECIPE_LIBRARY_PATH="team=$HOME/shared-library/team-recipes"
99
95
  mm-harness flows
96
+ mm-harness run --list
100
97
  ```
101
98
 
102
- Team flows can wrap repeated product behavior while each checklist or task keeps
103
- its own acceptance criteria. See [recipe libraries](docs/recipe-libraries.md).
99
+ Shared libraries hold durable actions, flows, and recipes. Task acceptance
100
+ criteria remain task-local. See [Recipes](docs/RECIPES.md).
104
101
 
105
- ## Health and recovery
102
+ ## Recover
106
103
 
107
104
  ```bash
108
- mm-harness doctor # inspect; does not launch
109
- mm-harness doctor --fix # repair local harness setup; does not launch
110
- mm-harness verify # verify the installed runtime overlay
111
- mm-harness cleanup # remove the runtime overlay
105
+ mm-harness doctor
106
+ mm-harness doctor --fix
107
+ mm-harness verify
108
+ mm-harness cleanup
112
109
  ```
113
110
 
114
- Human output gives a next command. `--json` gives stable machine-readable errors
115
- and keeps stdout free of decoration. `--json-stream` emits line-delimited progress
116
- for long recipe runs.
111
+ Failures name one exact next action. Core is headless; browser, device, logs,
112
+ and debugger capabilities are reported as unavailable instead of fabricated.
113
+ Stable exit codes are `1` runtime/action failure, `2` invalid CLI usage, `3`
114
+ infrastructure failure, `4` bounded recovery refusal, and `5` validation/trust
115
+ failure.
117
116
 
118
- ## Development
117
+ ## Reference
119
118
 
120
- Use a source checkout without replacing the global install:
119
+ - [Recipes](docs/RECIPES.md) discover, compose, author, and share proof.
120
+ - [Security](docs/SECURITY.md) — trust, approval, fixtures, and evidence safety.
121
+ - [QA](docs/QA.md) — clean-machine and human release checks.
122
+ - [Contributing](docs/CONTRIBUTING.md) — ownership, layout, and change gates.
123
+
124
+ For development, point the installed command at a source checkout:
121
125
 
122
126
  ```bash
123
127
  export MM_HARNESS_BIN=/path/to/metamask-harness/bin/mm-harness
124
128
  mm-harness --version
125
129
  ```
126
130
 
127
- Unset `MM_HARNESS_BIN` to return to the published install.
128
-
129
- Validate changes with:
130
-
131
- ```bash
132
- yarn check
133
- ```
134
-
135
- Current references: [cheatsheet](docs/CHEATSHEET.md), [recipe libraries](docs/recipe-libraries.md),
136
- and [human QA](docs/CLI-ERGONOMICS-HUMAN-QA.md). Maintainers also have the
137
- [architecture](docs/architecture.md) and [video gate](docs/VIDEO-DEMO-VALIDATION.md).
131
+ Unset `MM_HARNESS_BIN` to return to the published installation.
@@ -277,6 +277,8 @@ const fs = require('fs');
277
277
  const http = require('http');
278
278
  const { execSync, execFileSync } = require('child_process');
279
279
  let chromium; try { chromium = require('@playwright/test').chromium; } catch { chromium = require('playwright').chromium; }
280
+ const { isolatedProfileArgs, remoteDebuggingArgs } = require(path.join('${SCRIPT_DIR}', 'lib/chrome-args.cjs'));
281
+ const { captureMacFrontmostProcess, restoreMacFrontmostProcess } = require(path.join('${SCRIPT_DIR}', 'lib/macos-focus.cjs'));
280
282
 
281
283
  const SLOT_ID = '${SLOT_ID}';
282
284
  const AGENT_DIR = '${AGENT_DIR}';
@@ -307,11 +309,18 @@ const resumeWebpack = () => {
307
309
  '--disable-renderer-backgrounding',
308
310
  '--no-first-run',
309
311
  '--no-default-browser-check',
312
+ ...isolatedProfileArgs(),
310
313
  '--window-size=420,800',
311
314
  ];
312
- if (CDP_PORT) args.push('--remote-debugging-port=' + CDP_PORT);
315
+ // Debug-port trio (address + port + scoped allow-origins) from the shared module
316
+ // so the DevTools origin allow-list can never drift away from the port; without
317
+ // it Chrome 403s every browser DevTools WebSocket and mm-harness debug can't attach.
318
+ if (CDP_PORT) {
319
+ args.push(...remoteDebuggingArgs(CDP_PORT));
320
+ }
313
321
 
314
322
  const chromiumApp = path.dirname(path.dirname(path.dirname(chromium.executablePath())));
323
+ const previousFrontmostPid = captureMacFrontmostProcess();
315
324
  execFileSync('open', ['-g', '-n', '-a', chromiumApp, '--args', ...args], { stdio: 'ignore' });
316
325
  for (let i = 0; i < 60; i++) {
317
326
  try {
@@ -414,6 +423,8 @@ const resumeWebpack = () => {
414
423
  }
415
424
  if (process.env.MM_HARNESS_FOCUS_BROWSER === '1') {
416
425
  await page.bringToFront().catch(() => {});
426
+ } else {
427
+ restoreMacFrontmostProcess(previousFrontmostPid);
417
428
  }
418
429
 
419
430
  fs.writeFileSync(path.join(AGENT_DIR, 'extension.id'), extId);
@@ -24,6 +24,7 @@ import path from 'node:path';
24
24
  import { spawnSync } from 'node:child_process';
25
25
  import process from 'node:process';
26
26
  import { recipeHarnessRoot } from '../shared/recipe-harness-root.mjs';
27
+ import { harnessSourceFingerprint } from '../shared/harness-source-fingerprint.mjs';
27
28
 
28
29
  function usage() {
29
30
  console.error('Usage: inject-extension-harness.mjs [--target <metamask-extension>] [--no-git-exclude]');
@@ -127,6 +128,8 @@ copyFile(path.join(runnerDir, 'adapters/shared/tmux-viewer.sh'), path.join(harne
127
128
  copyFile(path.join(runnerDir, 'adapters/shared/log-tui.mjs'), path.join(harnessDir, 'scripts/lib/log-tui.mjs'));
128
129
  copyFile(path.join(runnerDir, 'adapters/extension/lib/extension-id.cjs'), path.join(harnessDir, 'scripts/lib/extension-id.cjs'));
129
130
  copyFile(path.join(runnerDir, 'adapters/extension/lib/slot-title.cjs'), path.join(harnessDir, 'scripts/lib/slot-title.cjs'));
131
+ copyFile(path.join(runnerDir, 'adapters/extension/lib/chrome-args.cjs'), path.join(harnessDir, 'scripts/lib/chrome-args.cjs'));
132
+ copyFile(path.join(runnerDir, 'adapters/extension/lib/macos-focus.cjs'), path.join(harnessDir, 'scripts/lib/macos-focus.cjs'));
130
133
  makeExecutableTree(path.join(harnessDir, 'scripts'));
131
134
  fs.writeFileSync(path.join(harnessDir, 'installed-scripts.sha256'), `${dirContentHash(path.join(harnessDir, 'scripts'))}\n`);
132
135
 
@@ -136,6 +139,10 @@ const manifest = {
136
139
  adapter: 'extension',
137
140
  installedAt: new Date().toISOString(),
138
141
  runnerPackageVersion,
142
+ // Fingerprint of the runner source that produced this overlay. The launch-time
143
+ // freshness guard re-injects when the runner's fingerprint no longer matches,
144
+ // so a slot never silently runs a stale injected launcher.
145
+ sourceFingerprint: harnessSourceFingerprint(runnerDir),
139
146
  source: {
140
147
  runnerDir,
141
148
  runnerRevision,
@@ -24,6 +24,8 @@ const os = require('node:os');
24
24
  const path = require('node:path');
25
25
  const { execFileSync, spawn, spawnSync } = require('node:child_process');
26
26
  const { extensionIdFromExtensionDir } = require('./lib/extension-id.cjs');
27
+ const { isolatedProfileArgs, remoteDebuggingArgs } = require('./lib/chrome-args.cjs');
28
+ const { captureMacFrontmostProcess, restoreMacFrontmostProcess } = require('./lib/macos-focus.cjs');
27
29
 
28
30
  if (process.argv.includes('--help') || process.argv.includes('-h')) {
29
31
  console.log(
@@ -92,10 +94,12 @@ if (args['stop-only'] !== undefined) {
92
94
  const initialUrl = args['start-url'] || extensionHomeUrl(args['extension-dir']) || 'chrome://extensions/';
93
95
  const chromeArgs = [
94
96
  `--user-data-dir=${args.profile}`,
95
- '--remote-debugging-address=127.0.0.1',
96
- `--remote-debugging-port=${cdpPort}`,
97
+ // Debug-port trio (address + port + scoped allow-origins) from the shared module,
98
+ // so the DevTools origin allow-list can never drift away from the port.
99
+ ...remoteDebuggingArgs(cdpPort),
97
100
  '--no-first-run',
98
101
  '--disable-first-run-ui',
102
+ ...isolatedProfileArgs(),
99
103
  '--disable-default-apps',
100
104
  '--disable-popup-blocking',
101
105
  '--disable-extensions-file-access-check',
@@ -109,11 +113,11 @@ const chromeArgs = [
109
113
  initialUrl,
110
114
  ];
111
115
  const logFd = fs.openSync(args['chrome-log'], 'a');
116
+ const previousFrontmostPid = captureMacFrontmostProcess();
112
117
  let browserPid;
113
118
  try {
114
119
  const application = process.platform === 'darwin' ? macApplicationForExecutable(args['chrome-bin']) : null;
115
120
  if (application) {
116
- // Launch Services does not attach Chrome's stdio, so chrome.log may stay empty; CDP ownership is the readiness signal.
117
121
  execFileSync('open', ['-g', '-n', '-a', application, '--args', ...chromeArgs], {
118
122
  env: sanitizedChildEnv(),
119
123
  stdio: ['ignore', logFd, logFd],
@@ -135,6 +139,9 @@ try {
135
139
  fs.closeSync(logFd);
136
140
  }
137
141
  fs.writeFileSync(args['chrome-pid'], `${browserPid}\n`);
142
+ if (process.env.MM_HARNESS_FOCUS_BROWSER !== '1') {
143
+ restoreMacFrontmostProcess(previousFrontmostPid);
144
+ }
138
145
 
139
146
  function sanitizedChildEnv() {
140
147
  return {
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+ // chrome-args.cjs — single source of truth for the remote-debugging launch flags
3
+ // shared by BOTH extension launchers: the fresh spawn (launch-browser.cjs) and the
4
+ // reopen path (ensure-browser.sh → reopen-browser.sh). Keeping the debug-port trio
5
+ // here means `--remote-allow-origins` can never drift away from the port it scopes:
6
+ // the two are declared once and always travel together. Without the allow-origins
7
+ // entry Chrome 403s every browser DevTools WebSocket on the debug port and
8
+ // `mm-harness debug` cannot attach a visible console.
9
+
10
+ // The exact origin allowed is the locally-served bundled DevTools frontend, which
11
+ // Chrome serves at http://127.0.0.1:<cdpPort>. Scoped to that origin — never `*`.
12
+ function remoteDebuggingArgs(cdpPort) {
13
+ const port = Number(cdpPort);
14
+ if (!Number.isInteger(port) || port <= 0) {
15
+ throw new Error(`remoteDebuggingArgs: invalid cdp port: ${cdpPort}`);
16
+ }
17
+ return [
18
+ '--remote-debugging-address=127.0.0.1',
19
+ `--remote-debugging-port=${port}`,
20
+ `--remote-allow-origins=http://127.0.0.1:${port}`,
21
+ ];
22
+ }
23
+
24
+ // An isolated automation profile must never ask for or write credentials in the
25
+ // operator's macOS login keychain. Chromium's mock keychain keeps profile-local
26
+ // encryption deterministic and prevents modal prompts from blocking startup.
27
+ function isolatedProfileArgs(platform = process.platform) {
28
+ return platform === 'darwin' ? ['--use-mock-keychain'] : [];
29
+ }
30
+
31
+ module.exports = { isolatedProfileArgs, remoteDebuggingArgs };
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+
3
+ const { execFileSync } = require('node:child_process');
4
+
5
+ function captureMacFrontmostProcess() {
6
+ if (process.platform !== 'darwin') return null;
7
+ try {
8
+ const value = execFileSync('osascript', [
9
+ '-e',
10
+ 'tell application "System Events" to get unix id of first application process whose frontmost is true',
11
+ ], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
12
+ const pid = Number.parseInt(value, 10);
13
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ function restoreMacFrontmostProcess(pid) {
20
+ if (process.platform !== 'darwin' || !Number.isInteger(pid) || pid <= 0) return false;
21
+ try {
22
+ execFileSync('osascript', [
23
+ '-e',
24
+ `tell application "System Events" to set frontmost of first application process whose unix id is ${pid} to true`,
25
+ ], { stdio: 'ignore' });
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ module.exports = { captureMacFrontmostProcess, restoreMacFrontmostProcess };
@@ -32,6 +32,7 @@
32
32
  set -euo pipefail
33
33
 
34
34
  TARGET="$PWD"
35
+ INVOCATION_DIR="$PWD"
35
36
  CDP_PORT=""
36
37
  ARTIFACTS=""
37
38
  OUT=""
@@ -39,7 +40,7 @@ PREPARE_CMD="${RECIPE_HARNESS_EXTENSION_LAUNCH_CMD:-}"
39
40
  LAUNCH_EXISTING_DIST=false
40
41
  START_WATCH=false
41
42
  DIST_DIR="dist/chrome"
42
- CHROME_USER_DATA_DIR=""
43
+ CHROME_USER_DATA_DIR="${CHROME_USER_DATA_DIR:-}"
43
44
  REMOTE_FLAGS=""
44
45
  REMOTE_FLAGS_APPLIED=false
45
46
  while [ "$#" -gt 0 ]; do
@@ -60,6 +61,11 @@ while [ "$#" -gt 0 ]; do
60
61
  esac
61
62
  done
62
63
 
64
+ case "$CHROME_USER_DATA_DIR" in
65
+ ""|/*) ;;
66
+ *) CHROME_USER_DATA_DIR="$INVOCATION_DIR/$CHROME_USER_DATA_DIR" ;;
67
+ esac
68
+
63
69
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
64
70
  # shellcheck disable=SC1091
65
71
  for _hp in "$SCRIPT_DIR/lib/harness-path.sh" "$SCRIPT_DIR/../shared/harness-path.sh"; do
@@ -211,11 +217,11 @@ try {
211
217
  executable = chromium.executablePath();
212
218
  } catch (error) {
213
219
  const message = error && error.message ? error.message : String(error);
214
- console.error(`[recipe-harness] Could not resolve Playwright Chromium executable: ${message}. Manual approval required before installing the Playwright Chromium browser cache (no package.json changes); ask the user before running yarn exec playwright install chromium.`);
220
+ console.error(`[recipe-harness] Could not resolve Playwright Chromium executable: ${message}. Manual approval required before installing the Playwright Chromium browser cache (no package.json changes); ask the user before running yarn playwright install chromium.`);
215
221
  process.exit(1);
216
222
  }
217
223
  if (!fs.existsSync(executable)) {
218
- console.error(`[recipe-harness] Playwright Chromium is not installed at ${executable}. Manual approval required before installing the Playwright Chromium browser cache (no package.json changes). Ask the user for approval; if they agree, run: cd ${shellQuote(process.cwd())} && yarn exec playwright install chromium`);
224
+ console.error(`[recipe-harness] Playwright Chromium is not installed at ${executable}. Manual approval required before installing the Playwright Chromium browser cache (no package.json changes). Ask the user for approval; if they agree, run: cd ${shellQuote(process.cwd())} && yarn playwright install chromium`);
219
225
  console.error('[recipe-harness] To use a browser that is already installed, set RECIPE_HARNESS_CHROME_BIN=/path/to/chrome explicitly.');
220
226
  process.exit(1);
221
227
  }
@@ -227,25 +233,9 @@ NODE
227
233
  rm -f "$_chrome_probe"
228
234
  fi
229
235
 
230
- if [ -z "${CHROME_BIN:-}" ]; then
231
- for candidate in \
232
- "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
233
- "/Applications/Chromium.app/Contents/MacOS/Chromium" \
234
- "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary" \
235
- "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"; do
236
- if [ -x "$candidate" ]; then
237
- CHROME_BIN="$candidate"
238
- break
239
- fi
240
- done
241
- if [ -n "${CHROME_BIN:-}" ]; then
242
- echo "[recipe-harness] WARN: Playwright Chromium unavailable; falling back to system browser: $CHROME_BIN" >&2
243
- fi
244
- fi
245
-
246
236
  if [ -z "${CHROME_BIN:-}" ]; then
247
237
  [ -n "${CHROME_PROBE_LOG:-}" ] && cat "$CHROME_PROBE_LOG" >&2 2>/dev/null || true
248
- echo "[recipe-harness] No approved Chromium binary selected; stopping before live Extension launch." >&2
238
+ echo "[recipe-harness] No compatible Chromium binary selected; stopping before build or live Extension launch." >&2
249
239
  exit 1
250
240
  fi
251
241
  quoted_chrome="$(printf '%q' "$CHROME_BIN")"
@@ -487,6 +487,14 @@
487
487
  "inputs": "--adapter extension|mobile [--cdp-port|--port] [--target page|worker] [--json] [--no-open]",
488
488
  "outputs": "debug URL/report on stdout; exit 0/1"
489
489
  },
490
+ {
491
+ "id": "lib/harness-source-fingerprint",
492
+ "entry": "adapters/shared/harness-source-fingerprint.mjs",
493
+ "kind": "lib",
494
+ "purpose": "Compute the deterministic source fingerprint stamped into injected extension harness manifests.",
495
+ "inputs": "imported by injection and launch freshness checks",
496
+ "outputs": "stable SHA-256 fingerprint"
497
+ },
490
498
  {
491
499
  "id": "lib/update-check-worker",
492
500
  "entry": "adapters/shared/update-check-worker.mjs",
@@ -13,6 +13,7 @@
13
13
  # --simulator <udid|name|"booted"> iOS simulator (default: IOS_SIMULATOR env, else "booted")
14
14
  # --adb-serial <serial> Android device serial (default: ADB_SERIAL env)
15
15
  # --preflight-mode fast|auto|rebuild-native|clean (default: fast; env MOBILE_PREFLIGHT_MODE)
16
+ # --ui-only iOS only: ensure Simulator.app is visible; do not relaunch the app
16
17
  # Outputs:
17
18
  # Progress on stderr; exit 0 pass; 1 open failed; 2 bad args.
18
19
  #
@@ -25,6 +26,7 @@ SIMULATOR="${IOS_SIMULATOR:-${SIM_UDID:-booted}}"
25
26
  ADB_SERIAL_ARG="${ADB_SERIAL:-${ANDROID_SERIAL:-}}"
26
27
  PREFLIGHT_MODE="${MOBILE_PREFLIGHT_MODE:-fast}"
27
28
  PORT="${WATCHER_PORT:-${METRO_PORT:-8081}}"
29
+ UI_ONLY=false
28
30
 
29
31
  while [ "$#" -gt 0 ]; do
30
32
  case "$1" in
@@ -34,10 +36,11 @@ while [ "$#" -gt 0 ]; do
34
36
  --adb-serial) ADB_SERIAL_ARG="$2"; shift 2 ;;
35
37
  --preflight-mode) PREFLIGHT_MODE="$2"; shift 2 ;;
36
38
  --port) PORT="$2"; shift 2 ;;
39
+ --ui-only) UI_ONLY=true; shift ;;
37
40
  -h|--help)
38
41
  printf 'Usage: open-device.sh [--platform ios|android] [--target <dir>]\n'
39
42
  printf ' [--simulator <udid|name|booted>] [--adb-serial <serial>]\n'
40
- printf ' [--preflight-mode fast|auto|rebuild-native|clean] [--port <n>]\n'
43
+ printf ' [--preflight-mode fast|auto|rebuild-native|clean] [--port <n>] [--ui-only]\n'
41
44
  exit 0
42
45
  ;;
43
46
  *) printf 'open-device: unknown arg: %s\n' "$1" >&2; exit 2 ;;
@@ -48,6 +51,10 @@ case "$PLATFORM" in
48
51
  ios|android) ;;
49
52
  *) printf 'open-device: --platform must be ios or android (got: %s)\n' "$PLATFORM" >&2; exit 2 ;;
50
53
  esac
54
+ if $UI_ONLY && [ "$PLATFORM" != "ios" ]; then
55
+ printf 'open-device: --ui-only is supported only for ios.\n' >&2
56
+ exit 2
57
+ fi
51
58
 
52
59
  TARGET="$(cd "$TARGET" && pwd)"
53
60
 
@@ -149,19 +156,46 @@ boot_simulator_if_needed() {
149
156
  xcrun simctl bootstatus "$udid" -b >/dev/null
150
157
  }
151
158
 
152
- show_simulator() {
159
+ simulator_ui_running() {
160
+ pgrep -x Simulator >/dev/null 2>&1
161
+ }
162
+
163
+ open_simulator_in_background_if_needed() {
153
164
  local target="$1" udid=""
165
+ [ "${MM_HARNESS_SHOW_SIMULATOR:-1}" != "0" ] || return 0
166
+ simulator_ui_running && return 0
167
+ printf 'Opening Simulator UI in the background\n' >&2
154
168
  if [ "$target" = "booted" ]; then
155
- printf 'Opening Simulator UI\n' >&2
156
- open -a Simulator >/dev/null 2>&1 || true
169
+ open -g -a Simulator >/dev/null 2>&1 || {
170
+ printf 'open-device: failed to open Simulator UI.\n' >&2
171
+ return 1
172
+ }
157
173
  else
158
174
  udid="$(sim_udid_for_target "$target")"
159
175
  if [ -n "$udid" ]; then
160
- open -a Simulator --args -CurrentDeviceUDID "$udid" >/dev/null 2>&1 || true
176
+ open -g -a Simulator --args -CurrentDeviceUDID "$udid" >/dev/null 2>&1 || {
177
+ printf 'open-device: failed to open Simulator UI for %s.\n' "$target" >&2
178
+ return 1
179
+ }
161
180
  else
162
- open -a Simulator >/dev/null 2>&1 || true
181
+ open -g -a Simulator >/dev/null 2>&1 || {
182
+ printf 'open-device: failed to open Simulator UI.\n' >&2
183
+ return 1
184
+ }
163
185
  fi
164
186
  fi
187
+ }
188
+
189
+ focus_simulator() {
190
+ local target="$1" udid=""
191
+ if [ "$target" != "booted" ]; then
192
+ udid="$(sim_udid_for_target "$target")"
193
+ fi
194
+ if [ -n "$udid" ]; then
195
+ open -a Simulator --args -CurrentDeviceUDID "$udid" >/dev/null 2>&1 || true
196
+ else
197
+ open -a Simulator >/dev/null 2>&1 || true
198
+ fi
165
199
  osascript -e 'tell application "Simulator" to activate' >/dev/null 2>&1 || true
166
200
  }
167
201
 
@@ -220,8 +254,12 @@ if [ "$PLATFORM" = "ios" ]; then
220
254
  DEV_CLIENT_SCHEME="${IOS_DEV_CLIENT_SCHEME:-expo-metamask}"
221
255
 
222
256
  boot_simulator_if_needed "$SIM_TARGET" || exit 1
257
+ open_simulator_in_background_if_needed "$SIM_TARGET" || exit 1
223
258
  if [ "${MM_HARNESS_FOCUS_SIMULATOR:-0}" = "1" ]; then
224
- show_simulator "$SIM_TARGET"
259
+ focus_simulator "$SIM_TARGET"
260
+ fi
261
+ if $UI_ONLY; then
262
+ exit 0
225
263
  fi
226
264
 
227
265
  LAUNCHED=false
@@ -329,9 +329,21 @@ JSON
329
329
 
330
330
  ensure_live_runtime() {
331
331
  local device_target="$1"
332
- if live_status_ok "$ARTIFACTS/logs/app-status-precheck.log" "$device_target"; then
333
- return 0
334
- fi
332
+ local attempt
333
+ for attempt in 1 2 3 4 5; do
334
+ if live_status_ok "$ARTIFACTS/logs/app-status-precheck.log" "$device_target"; then
335
+ return 0
336
+ fi
337
+ # A freshly booted simulator can expose its Metro bridge just before
338
+ # `simctl list devices booted` publishes the device. Bound that launch→verify
339
+ # race instead of reporting a false DEVICE_NOT_FOUND immediately.
340
+ if [ "$attempt" -lt 5 ] && grep -q '"code"[[:space:]]*:[[:space:]]*"DEVICE_NOT_FOUND"' "$ARTIFACTS/logs/app-status-precheck.log"; then
341
+ echo "Waiting for Mobile device discovery (${attempt}/5): ${device_target:-configured target}" >&2
342
+ sleep 1
343
+ continue
344
+ fi
345
+ break
346
+ done
335
347
  if [ "$AUTO_START" = true ]; then
336
348
  cat >&2 <<'EOF'
337
349
  Mobile auto-start is not allowed from product-local scripts. Start or prepare the app through the runner/slot runtime, then rerun verify with --no-auto-start.
@@ -0,0 +1,49 @@
1
+ // harness-source-fingerprint.mjs — deterministic fingerprint of the runner files
2
+ // that get injected into a slot's harness overlay.
3
+ //
4
+ // inject.mjs stamps this into the slot manifest at install time; the launch-time
5
+ // freshness guard recomputes it and re-injects when they differ, so a slot can
6
+ // never silently run a stale injected launcher after the runner changes. This is
7
+ // what makes the runner the single source of truth: edit a runner adapter file and
8
+ // the next launch refreshes every slot from it.
9
+ //
10
+ // stat-only (size + mtime) so it is cheap enough to run on every launch; no file
11
+ // contents are read and no Date.now()/random is used (both would break determinism).
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import crypto from 'node:crypto';
15
+
16
+ // Trees whose contents are copied into <slot>/harness by inject.mjs. Any change
17
+ // here means injected slots are stale.
18
+ const SOURCE_DIRS = ['adapters/extension', 'adapters/shared', 'library'];
19
+
20
+ export function harnessSourceFingerprint(runnerDir) {
21
+ const entries = [];
22
+ for (const rel of SOURCE_DIRS) {
23
+ collect(path.join(runnerDir, rel), runnerDir, entries);
24
+ }
25
+ entries.sort();
26
+ return crypto.createHash('sha256').update(entries.join('\n')).digest('hex');
27
+ }
28
+
29
+ function collect(dir, root, out) {
30
+ let dirents;
31
+ try {
32
+ dirents = fs.readdirSync(dir, { withFileTypes: true });
33
+ } catch {
34
+ return; // absent tree contributes nothing
35
+ }
36
+ for (const dirent of dirents) {
37
+ // Skip hidden noise (.DS_Store, editor swap files) so the fingerprint tracks
38
+ // only real source and does not thrash re-injects.
39
+ if (dirent.name.startsWith('.')) continue;
40
+ const full = path.join(dir, dirent.name);
41
+ if (dirent.isDirectory()) {
42
+ collect(full, root, out);
43
+ continue;
44
+ }
45
+ if (!dirent.isFile()) continue;
46
+ const stat = fs.statSync(full);
47
+ out.push(`${path.relative(root, full)}|${stat.size}|${Math.floor(stat.mtimeMs)}`);
48
+ }
49
+ }
@@ -4,11 +4,7 @@ set -euo pipefail
4
4
  TARGET=""
5
5
  while [[ $# -gt 0 ]]; do
6
6
  case "$1" in
7
- --target)
8
- [[ $# -ge 2 ]] || { echo "Usage: install-repo-deps.sh --target <checkout>" >&2; exit 2; }
9
- TARGET="$2"
10
- shift 2
11
- ;;
7
+ --target) TARGET="$2"; shift 2 ;;
12
8
  -h|--help)
13
9
  echo "Usage: install-repo-deps.sh --target <checkout>"
14
10
  exit 0