@deeeed/metamask-harness 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +47 -17
  3. package/{orchestration → adapters}/core/cleanup.sh +2 -2
  4. package/{orchestration → adapters}/core/inject.sh +8 -8
  5. package/{orchestration → adapters}/extension/cleanup.mjs +1 -1
  6. package/{orchestration → adapters}/extension/console-tail.mjs +3 -0
  7. package/{orchestration → adapters}/extension/ensure-browser.sh +4 -4
  8. package/{orchestration → adapters}/extension/inject.mjs +27 -31
  9. package/{orchestration → adapters}/extension/launch.sh +4 -4
  10. package/{orchestration → adapters}/extension/live.sh +5 -5
  11. package/{orchestration → adapters}/extension/readiness.mjs +12 -2
  12. package/{orchestration → adapters}/extension/refresh-build.sh +1 -1
  13. package/{orchestration → adapters}/extension/seed-fixture.sh +1 -1
  14. package/{orchestration → adapters}/extension/start-watch.sh +7 -7
  15. package/{runner → adapters}/extension/verify.sh +11 -11
  16. package/{orchestration → adapters}/extension/wallet-fixture-state.cjs +2 -2
  17. package/{orchestration → adapters}/manifest.json +96 -96
  18. package/{library/actions → adapters}/mobile/bridge-runtime/cdp-bridge.cjs +5 -5
  19. package/{orchestration → adapters}/mobile/cleanup.sh +3 -3
  20. package/{orchestration → adapters}/mobile/inject.sh +22 -24
  21. package/adapters/mobile/open-device.sh +260 -0
  22. package/{orchestration/compat-overlays → adapters/mobile/overlay/compat}/README.md +13 -4
  23. package/adapters/mobile/prewarm-bundle.sh +95 -0
  24. package/adapters/mobile/start-metro.sh +228 -0
  25. package/{runner → adapters}/mobile/verify.sh +6 -6
  26. package/adapters/mobile/wait-for-bridge.sh +108 -0
  27. package/adapters/mobile/yarn-setup.sh +20 -0
  28. package/{orchestration/lib → adapters/shared}/cli-ux.sh +5 -9
  29. package/{orchestration/lib → adapters/shared}/log-tui.mjs +57 -1
  30. package/{orchestration/lib → adapters/shared}/open-debug.mjs +32 -10
  31. package/{orchestration/lib → adapters/shared}/resolve-farmslot-ports.sh +47 -0
  32. package/adapters/shared/sync-wallet-fixture.sh +78 -0
  33. package/bin/mm-harness +37 -6
  34. package/docs/CLI-SPEC.md +41 -2
  35. package/docs/architecture.md +69 -38
  36. package/docs/runtime-file-conventions.md +3 -3
  37. package/library/actions/core/perps/_controller.mjs +1 -1
  38. package/library/actions/extension/platform/cdp.mjs +2 -2
  39. package/library/actions/extension/wallet/ensure_unlocked.mjs +1 -1
  40. package/library/actions/mobile/platform/bridge.mjs +1 -1
  41. package/library/actions/mobile/wallet/ensure_unlocked.mjs +1 -1
  42. package/library/actions/mobile/wallet/setup.mjs +2 -2
  43. package/library/library.json +1 -1
  44. package/package.json +8 -8
  45. package/{orchestration → src/adapters}/extension/runtime-decision.ts +5 -5
  46. package/{orchestration → src/adapters}/extension/runtime.ts +1 -1
  47. package/src/adapters/mobile/prepare.ts +210 -0
  48. package/{orchestration → src/adapters}/mobile/runtime-decision.ts +105 -5
  49. package/{runner/src → src}/adapters.ts +3 -3
  50. package/{orchestration/lib/cli-color.mjs → src/cli-color.ts} +21 -13
  51. package/{orchestration/lib/cli-commands.mjs → src/cli-commands.ts} +52 -45
  52. package/{orchestration/lib/cli-version.mjs → src/cli-version.ts} +35 -17
  53. package/{runner/src → src}/cli.ts +48 -40
  54. package/src/commands/debug.ts +63 -0
  55. package/src/commands/fixtures.ts +193 -0
  56. package/{runner/src → src}/commands/launch.ts +200 -54
  57. package/src/commands/logs.ts +83 -0
  58. package/{runner/src → src}/commands/shared.ts +40 -30
  59. package/src/commands/update.ts +316 -0
  60. package/{runner/src → src}/harness.ts +119 -16
  61. package/{runner/src → src}/heal-bounds.ts +20 -1
  62. package/src/index.ts +15 -0
  63. package/{runner/src → src}/live-adapter-contract.ts +1 -1
  64. package/{runner/src → src}/mm-harness-cli.ts +64 -3
  65. package/{runner/src → src}/paths.ts +2 -2
  66. package/{orchestration/lib/progress.mjs → src/progress.ts} +31 -21
  67. package/docs/extension-runtime-commands.md +0 -60
  68. package/docs/harness-cli.md +0 -43
  69. package/orchestration/compat-overlays/mobile/README.md +0 -13
  70. package/orchestration/doctor.mjs +0 -72
  71. package/orchestration/lib/cli-home.mjs +0 -354
  72. package/orchestration/lib/recipe-paths.mjs +0 -26
  73. package/orchestration/mobile/launch.sh +0 -137
  74. package/orchestration/mobile/live.sh +0 -125
  75. package/orchestration/porcelain/metamask-recipe +0 -99
  76. package/orchestration/porcelain/mm-recipe +0 -1591
  77. package/orchestration/porcelain/mme-recipe +0 -1181
  78. package/runner/src/commands/debug.ts +0 -44
  79. package/runner/src/commands/fixtures.ts +0 -99
  80. package/runner/src/commands/logs.ts +0 -60
  81. package/runner/src/index.ts +0 -6
  82. /package/{orchestration → adapters}/extension/launch-browser.cjs +0 -0
  83. /package/{orchestration → adapters}/extension/pin-remote-flags.cjs +0 -0
  84. /package/{orchestration → adapters}/extension/sidepanel-toggle.sh +0 -0
  85. /package/{orchestration → adapters}/extension/snapshot-dist.sh +0 -0
  86. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/cdp-eval.cjs +0 -0
  87. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/config.cjs +0 -0
  88. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/issue-capture.cjs +0 -0
  89. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/target-discovery.cjs +0 -0
  90. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/ws-client.cjs +0 -0
  91. /package/{library/actions → adapters}/mobile/bridge-runtime/setup-wallet.sh +0 -0
  92. /package/{library/actions/mobile/app-overlay → adapters/mobile/overlay}/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -0
  93. /package/{library/actions/mobile/app-overlay → adapters/mobile/overlay}/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -0
  94. /package/{orchestration/compat-overlays/mobile → adapters/mobile/overlay/compat}/rn81-message-event-source.patch +0 -0
  95. /package/{orchestration/lib → adapters/shared}/activate-repo-node.sh +0 -0
  96. /package/{orchestration/lib → adapters/shared}/ensure-runner-deps.sh +0 -0
  97. /package/{orchestration/lib → adapters/shared}/harness-path.sh +0 -0
  98. /package/{orchestration/lib → adapters/shared}/hash-helpers.sh +0 -0
  99. /package/{orchestration/lib → adapters/shared}/json-field.sh +0 -0
  100. /package/{orchestration/lib → adapters/shared}/path-defaults.json +0 -0
  101. /package/{orchestration → src/adapters}/extension/ensure-ready.ts +0 -0
  102. /package/{orchestration → src/adapters}/extension/extension-id.ts +0 -0
  103. /package/{orchestration → src/adapters}/mobile/deps-markers.ts +0 -0
  104. /package/{runner/src → src}/completions-cache.ts +0 -0
  105. /package/{runner/src → src}/doctor.ts +0 -0
  106. /package/{runner/src → src}/manifest.ts +0 -0
  107. /package/{runner/src → src}/recording-target.ts +0 -0
  108. /package/{runner/src → src}/run-recording.ts +0 -0
  109. /package/{runner/src → src}/runner.ts +0 -0
  110. /package/{runner/src → src}/types.ts +0 -0
@@ -0,0 +1,210 @@
1
+ // Mobile runtime prepare — in-process decision-and-action loop.
2
+ //
3
+ // Replaces the retired shell orchestrators (prepare-device.sh + launch.sh).
4
+ // All sequencing is in TypeScript; leaf shell scripts handle single OS ops.
5
+ // Control flows down only: TS → adapters/mobile/{yarn-setup,start-metro,open-device}.sh
6
+ // No leaf script may call back into mm-harness (A1).
7
+
8
+ import path from 'node:path';
9
+
10
+ import { EXIT, spawnScript } from '../../commands/shared.ts';
11
+ import type { ScriptResult } from '../../commands/shared.ts';
12
+ import { runnerDir } from '../../paths.ts';
13
+ import {
14
+ decideMobileReadiness,
15
+ type MobileRuntimeDecisionReport,
16
+ type MobileRuntimeDecisionOptions,
17
+ } from './runtime-decision.ts';
18
+
19
+ export { type MobileRuntimeDecisionReport };
20
+
21
+ export interface PrepareMobileOptions extends MobileRuntimeDecisionOptions {
22
+ /** Pass through to leaf-script spawnScript calls so output is suppressed in --json mode. */
23
+ json?: boolean;
24
+ /**
25
+ * Passed to open-device.sh --preflight-mode.
26
+ * 'fast' = error if not installed; 'auto' = install if missing; 'rebuild-native'/'clean' = always rebuild.
27
+ * Defaults to 'fast'.
28
+ */
29
+ preflightMode?: string;
30
+ /**
31
+ * Force --clear on start-metro (reset bundle cache).
32
+ * Used by the setup-wallet clean-Metro retry path (M3).
33
+ */
34
+ clearMetro?: boolean;
35
+ }
36
+
37
+ /**
38
+ * Read current runtime readiness without executing any action.
39
+ * No side effects: just calls decideMobileReadiness and returns the report.
40
+ */
41
+ export async function mobileRuntimeStatus(
42
+ target: string,
43
+ opts: PrepareMobileOptions = {},
44
+ ): Promise<MobileRuntimeDecisionReport> {
45
+ return decideMobileReadiness(target, {
46
+ watcherPort: opts.watcherPort,
47
+ metroLog: opts.metroLog,
48
+ platform: opts.platform,
49
+ record: opts.record,
50
+ });
51
+ }
52
+
53
+ /**
54
+ * Prepare the mobile runtime: run the readiness decision then execute each
55
+ * action in sequence via single-purpose leaf shell scripts. Returns a
56
+ * ScriptResult so the launch heal loop can classify outcomes uniformly.
57
+ */
58
+ export async function prepareMobile(
59
+ target: string,
60
+ opts: PrepareMobileOptions = {},
61
+ ): Promise<ScriptResult> {
62
+ const json = opts.json ?? false;
63
+ const platform = opts.platform ?? 'ios';
64
+ const preflightMode = opts.preflightMode ?? 'fast';
65
+ const clearMetro = opts.clearMetro ?? false;
66
+
67
+ const report = await decideMobileReadiness(target, {
68
+ watcherPort: opts.watcherPort,
69
+ metroLog: opts.metroLog,
70
+ platform,
71
+ record: opts.record,
72
+ });
73
+
74
+ if (report.decision === 'blocked') {
75
+ const reasons = report.reasons.join(' ');
76
+ const msg = `mobile prepare blocked: ${reasons}\n Next: fix the bundle error in app code before retrying recipe up.`;
77
+ if (!json) process.stderr.write(`${msg}\n`);
78
+ return { status: EXIT.runtime, output: msg };
79
+ }
80
+
81
+ if (report.decision === 'ready') {
82
+ return { status: 0, output: '' };
83
+ }
84
+
85
+ if (report.decision === 'unknown') {
86
+ const msg = 'mobile prepare: runtime state unknown\n Next: run mm-harness verify --adapter mobile --target <checkout>';
87
+ if (!json) process.stderr.write(`${msg}\n`);
88
+ return { status: EXIT.runtime, output: msg };
89
+ }
90
+
91
+ // decision is 'install' or 'launch' — execute each action in sequence.
92
+ // clearMetro forces --clear on start-metro regardless of what decideMobileReadiness set.
93
+ const actions = clearMetro
94
+ ? report.actions.map((a) =>
95
+ a.id === 'start-metro' && !a.argv?.includes('--clear')
96
+ ? { ...a, argv: [...(a.argv ?? []), '--clear'] }
97
+ : a,
98
+ )
99
+ : report.actions;
100
+ for (const action of actions) {
101
+ const result = dispatchAction(action, target, platform, json, preflightMode);
102
+ if (result.status !== 0) return result;
103
+ }
104
+
105
+ // After install: re-decide and run appropriate actions.
106
+ // RECIPE_UP_INSTALL_ATTEMPTED guards against infinite loops if yarn-setup was a no-op.
107
+ if (report.decision === 'install' && !process.env['RECIPE_UP_INSTALL_ATTEMPTED']) {
108
+ process.env['RECIPE_UP_INSTALL_ATTEMPTED'] = '1';
109
+ const postInstall = await decideMobileReadiness(target, {
110
+ watcherPort: opts.watcherPort,
111
+ metroLog: opts.metroLog,
112
+ platform,
113
+ record: true,
114
+ });
115
+ switch (postInstall.decision) {
116
+ case 'install': {
117
+ // Yarn setup ran but runtime is still unresolved — teach and fail.
118
+ const msg =
119
+ `mobile prepare: dependencies installed but runtime is still unresolved ` +
120
+ `(${postInstall.reasons.join('; ')})\n` +
121
+ ` Next: inspect the checkout — node_modules may be incomplete or yarn.lock drifted.`;
122
+ if (!json) process.stderr.write(`${msg}\n`);
123
+ return { status: EXIT.runtime, output: msg };
124
+ }
125
+ case 'ready': {
126
+ // Deps resolved and runtime appears ready — still confirm bridge is live.
127
+ const bridge = dispatchAction({ id: 'wait-for-bridge', cwd: target }, target, platform, json, preflightMode);
128
+ if (bridge.status !== 0) return bridge;
129
+ break;
130
+ }
131
+ case 'launch': {
132
+ for (const action of postInstall.actions) {
133
+ const result = dispatchAction(action, target, platform, json, preflightMode);
134
+ if (result.status !== 0) return result;
135
+ }
136
+ break;
137
+ }
138
+ default: {
139
+ // 'blocked' or 'unknown' after install — surface as error.
140
+ const msg =
141
+ `mobile prepare: post-install state is ${postInstall.decision}: ${postInstall.reasons.join('; ')}\n` +
142
+ ` Next: run mm-harness verify --adapter mobile --target <checkout>`;
143
+ if (!json) process.stderr.write(`${msg}\n`);
144
+ return { status: EXIT.runtime, output: msg };
145
+ }
146
+ }
147
+ }
148
+
149
+ return { status: 0, output: '' };
150
+ }
151
+
152
+ function dispatchAction(
153
+ action: { id: string; argv?: string[]; cwd?: string },
154
+ target: string,
155
+ platform: string,
156
+ json: boolean,
157
+ preflightMode = 'fast',
158
+ ): ScriptResult {
159
+ const cwd = action.cwd ?? target;
160
+ switch (action.id) {
161
+ case 'yarn-setup': {
162
+ // Leaf: install node_modules (deps missing or stale).
163
+ const leaf = path.join(runnerDir, 'adapters/mobile/yarn-setup.sh');
164
+ return spawnScript(leaf, ['--target', cwd], target, json);
165
+ }
166
+ case 'start-metro': {
167
+ // Leaf: ensure Metro is running; argv may carry --clear for cache reset.
168
+ const leaf = path.join(runnerDir, 'adapters/mobile/start-metro.sh');
169
+ const extra: string[] = action.argv ?? [];
170
+ return spawnScript(leaf, ['--target', cwd, ...extra], target, json);
171
+ }
172
+ case 'prewarm-bundle': {
173
+ // Leaf: curl the bundle URL before opening the dev client.
174
+ const leaf = path.join(runnerDir, 'adapters/mobile/prewarm-bundle.sh');
175
+ return spawnScript(leaf, ['--platform', platform, '--target', cwd], target, json);
176
+ }
177
+ case 'wait-for-bridge': {
178
+ // Leaf: poll CDP bridge until a route target registers.
179
+ const leaf = path.join(runnerDir, 'adapters/mobile/wait-for-bridge.sh');
180
+ return spawnScript(leaf, ['--target', cwd], target, json);
181
+ }
182
+ case 'clear-metro-cache': {
183
+ // Legacy alias for start-metro --clear (kept for backward compat).
184
+ const leaf = path.join(runnerDir, 'adapters/mobile/start-metro.sh');
185
+ return spawnScript(leaf, ['--target', cwd, '--clear'], target, json);
186
+ }
187
+ case 'launch-mobile-runtime': {
188
+ // Leaf: open the MetaMask Mobile dev client on the simulator/device.
189
+ const leaf = path.join(runnerDir, 'adapters/mobile/open-device.sh');
190
+ return spawnScript(
191
+ leaf,
192
+ ['--platform', platform, '--target', cwd, '--preflight-mode', preflightMode],
193
+ target,
194
+ json,
195
+ );
196
+ }
197
+ case 'rebuild-native-dev-client': {
198
+ // Native rebuild required — the dev client binary predates a native Nitro
199
+ // module. Inform the caller; a full native build is not automated here.
200
+ const msg =
201
+ 'rebuild-native-dev-client required: native module mismatch detected.\n' +
202
+ ' Next: yarn start:ios or yarn start:android to rebuild the native dev client, ' +
203
+ 'then re-run mm-harness launch.';
204
+ if (!json) process.stderr.write(`${msg}\n`);
205
+ return { status: EXIT.runtime, output: msg };
206
+ }
207
+ default:
208
+ return { status: 0, output: '' };
209
+ }
210
+ }
@@ -68,9 +68,31 @@ function resolveMetroLog(target: string, metroLog?: string): string | null {
68
68
  return fs.existsSync(abs) ? abs : null;
69
69
  }
70
70
 
71
+ // A metro.log is stale when the yarn install markers are newer — a yarn install
72
+ // happened after the last bundle run, so the log describes a now-superseded module
73
+ // tree. Ignoring it lets launch start fresh instead of deciding off stale content.
74
+ function isMetroLogStale(target: string, logAbs: string): boolean {
75
+ try {
76
+ const logMtime = fs.statSync(logAbs).mtimeMs;
77
+ // Check against the same install markers as depsCheck; they are updated by
78
+ // yarn on every successful install, making them a reliable freshness signal.
79
+ const INSTALL_MARKERS = ['node_modules/.yarn-state.yml', '.yarn/install-state.gz'];
80
+ return INSTALL_MARKERS.some((rel) => {
81
+ try {
82
+ return logMtime < fs.statSync(path.join(target, rel)).mtimeMs;
83
+ } catch {
84
+ return false;
85
+ }
86
+ });
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+
71
92
  function metroLogCheck(target: string, metroLog?: string): MetroLogCheck {
72
93
  const abs = resolveMetroLog(target, metroLog);
73
94
  if (!abs) return { status: 'no-log' };
95
+ if (isMetroLogStale(target, abs)) return { status: 'no-log' };
74
96
  const logText = fs.readFileSync(abs, 'utf8');
75
97
  return analyzeBundleLog({
76
98
  target,
@@ -80,16 +102,72 @@ function metroLogCheck(target: string, metroLog?: string): MetroLogCheck {
80
102
  });
81
103
  }
82
104
 
105
+ // Returns names of top-level package.json dependencies absent from node_modules.
106
+ // Catches the case where a dep was added in a branch merge but yarn install was not
107
+ // re-run — install markers and fingerprint may both be satisfied while a required
108
+ // package directory is simply missing (real case: 249s Metro bundle discovers it
109
+ // too late).
110
+ function missingRequiredDeps(target: string): string[] {
111
+ try {
112
+ const pkg = JSON.parse(fs.readFileSync(path.join(target, 'package.json'), 'utf8')) as {
113
+ dependencies?: Record<string, unknown>;
114
+ devDependencies?: Record<string, unknown>;
115
+ };
116
+ const all = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
117
+ return Object.keys(all).filter(
118
+ (name) => !fs.existsSync(path.join(target, 'node_modules', name)),
119
+ );
120
+ } catch {
121
+ return [];
122
+ }
123
+ }
124
+
125
+ // Returns true when any manifest input (package.json or yarn.lock) is newer than
126
+ // the install markers written by yarn on a successful install. A reliable signal
127
+ // that deps changed since the last install, even when depsCheck found no recorded
128
+ // baseline drift (e.g. git checkout preserved an old author-date mtime on the
129
+ // manifest, then a new dep was committed, making the manifest newer than markers).
130
+ function depsStaleByMtime(target: string): boolean {
131
+ // Use the same install markers as depsCheck: the files yarn writes on install.
132
+ const INSTALL_MARKERS = ['node_modules/.yarn-state.yml', '.yarn/install-state.gz'];
133
+ let markerMtime = 0;
134
+ for (const rel of INSTALL_MARKERS) {
135
+ try {
136
+ const mt = fs.statSync(path.join(target, rel)).mtimeMs;
137
+ if (mt > markerMtime) markerMtime = mt;
138
+ } catch {
139
+ // marker absent; keep searching
140
+ }
141
+ }
142
+ if (markerMtime === 0) return false; // no markers → depsCheck already handles via 'missing'
143
+ for (const file of ['package.json', 'yarn.lock']) {
144
+ try {
145
+ if (fs.statSync(path.join(target, file)).mtimeMs > markerMtime) return true;
146
+ } catch {
147
+ // manifest absent; skip
148
+ }
149
+ }
150
+ return false;
151
+ }
152
+
83
153
  const installActions = (target: string): MobileRuntimeDecisionAction[] => [
84
154
  { id: 'yarn-setup', argv: ['yarn', 'setup'], cwd: target },
85
155
  ];
86
156
 
87
157
  const launchActions = (target: string, clearMetro = false): MobileRuntimeDecisionAction[] => {
88
158
  const actions: MobileRuntimeDecisionAction[] = [];
89
- if (clearMetro) {
90
- actions.push({ id: 'clear-metro-cache', argv: ['yarn', 'expo', 'start', '--clear'], cwd: target });
91
- }
159
+ // start-metro ensures Metro is running; --clear resets the bundle cache.
160
+ actions.push({
161
+ id: 'start-metro',
162
+ argv: clearMetro ? ['--clear'] : [],
163
+ cwd: target,
164
+ });
165
+ // Prewarm the bundle before opening the dev client so the app connects instantly.
166
+ actions.push({ id: 'prewarm-bundle', cwd: target });
167
+ // Open the dev client via the expo-development-client deep link.
92
168
  actions.push({ id: 'launch-mobile-runtime' });
169
+ // Poll the CDP bridge until a route target registers (confirms app is bridged).
170
+ actions.push({ id: 'wait-for-bridge', cwd: target });
93
171
  return actions;
94
172
  };
95
173
 
@@ -100,9 +178,28 @@ export async function decideMobileReadiness(
100
178
  const resolved = path.resolve(target);
101
179
  if (options.record) recordDepsBaseline(resolved);
102
180
 
103
- const deps = depsCheck(resolved, {
181
+ const rawDeps = depsCheck(resolved, {
104
182
  productMarkers: mobileProductMarkers(options.platform),
105
183
  });
184
+ // Mtime-based fallback: when depsCheck found no recorded baseline, supplement
185
+ // with a direct mtime comparison — if package.json or yarn.lock is newer than
186
+ // node_modules, a dep change landed after the last install. Fail fast at
187
+ // pre-flight rather than launching a doomed Metro bundle. Skip when a baseline
188
+ // was found (hasBaseline=true) so fingerprint-verified 'current' is trusted.
189
+ let deps: DepsCheck =
190
+ rawDeps.status === 'current' && !rawDeps.hasBaseline && depsStaleByMtime(resolved)
191
+ ? { installed: rawDeps.installed, status: 'stale', hasBaseline: rawDeps.hasBaseline }
192
+ : rawDeps;
193
+ // Required-dep absence: even when fingerprint + mtime reports 'current', a
194
+ // top-level dependency could be absent from node_modules (e.g. added in a
195
+ // branch merge but install not re-run). Catch at pre-flight before Metro
196
+ // discovers it during a 249s bundle run.
197
+ if (deps.status === 'current') {
198
+ const absent = missingRequiredDeps(resolved);
199
+ if (absent.length > 0) {
200
+ deps = { installed: deps.installed, status: 'partial', hasBaseline: deps.hasBaseline, missingProducts: absent };
201
+ }
202
+ }
106
203
  const metroLog = metroLogCheck(resolved, options.metroLog);
107
204
  const metro = options.watcherPort
108
205
  ? await probeMetroPackager(options.watcherPort)
@@ -155,7 +252,10 @@ export async function decideMobileReadiness(
155
252
  }
156
253
 
157
254
  const metroLogAbs = resolveMetroLog(resolved, options.metroLog);
158
- const metroLogText = metroLogAbs ? fs.readFileSync(metroLogAbs, 'utf8') : '';
255
+ const metroLogText =
256
+ metroLogAbs && !isMetroLogStale(resolved, metroLogAbs)
257
+ ? fs.readFileSync(metroLogAbs, 'utf8')
258
+ : '';
159
259
  const staleNativeModule = supersededErrorCapture(metroLogText, {
160
260
  errorPattern: NATIVE_MODULE_STALE,
161
261
  okPattern: BUNDLE_OK,
@@ -2,8 +2,8 @@ import http from 'node:http';
2
2
  import path from 'node:path';
3
3
  import { compatibilityMode, fixtureSummary, repoShape } from './doctor.ts';
4
4
  import { runLiveAdapterScript } from './live-adapter-contract.ts';
5
- import { withExtensionPage } from '../../library/actions/extension/platform/cdp.mjs';
6
- import { bridgeCommand, evalSync, simulatorScreenshot } from '../../library/actions/mobile/platform/bridge.mjs';
5
+ import { withExtensionPage } from '../library/actions/extension/platform/cdp.mjs';
6
+ import { bridgeCommand, evalSync, simulatorScreenshot } from '../library/actions/mobile/platform/bridge.mjs';
7
7
  import type {
8
8
  ActionAdapter,
9
9
  ActionExecutionContext,
@@ -284,7 +284,7 @@ function mobileProbeOutput(status: unknown, input: MetaMaskUiActionInput, projec
284
284
  }
285
285
 
286
286
  function mobileBridgePath(_projectRoot: string): string {
287
- return process.env.METAMASK_RECIPE_MOBILE_BRIDGE_SCRIPT || 'runner:library/actions/mobile/bridge-runtime/cdp-bridge.cjs';
287
+ return process.env.METAMASK_RECIPE_MOBILE_BRIDGE_SCRIPT || 'runner:adapters/mobile/bridge-runtime/cdp-bridge.cjs';
288
288
  }
289
289
 
290
290
  async function waitForMobileTarget(input: ReturnType<typeof uiInputFor>, payload: ActionNode) {
@@ -1,11 +1,14 @@
1
- #!/usr/bin/env node
2
1
  /**
3
- * cli-color.mjs — shared ANSI styling for recipe CLI human output.
2
+ * cli-color.ts — shared ANSI styling for recipe CLI human output.
4
3
  *
5
4
  * Honors NO_COLOR, RECIPE_NO_COLOR, FORCE_COLOR, RECIPE_COLOR, and TTY detection.
6
5
  */
7
6
 
8
- const STYLES = {
7
+ type StyleName =
8
+ | 'reset' | 'bold' | 'dim' | 'label' | 'cmd' | 'ok' | 'warn'
9
+ | 'err' | 'info' | 'accent' | 'path' | 'comment' | 'active';
10
+
11
+ const STYLES: Record<StyleName, string> = {
9
12
  reset: '\x1b[0m',
10
13
  bold: '\x1b[1m',
11
14
  dim: '\x1b[2m',
@@ -21,32 +24,37 @@ const STYLES = {
21
24
  active: '\x1b[1m',
22
25
  };
23
26
 
24
- export function colorEnabled(stream = process.stderr) {
27
+ export function colorEnabled(stream: NodeJS.WriteStream = process.stderr): boolean {
25
28
  if (process.env.NO_COLOR != null && process.env.NO_COLOR !== '' && process.env.NO_COLOR !== '0') {
26
29
  return false;
27
30
  }
28
31
  if (process.env.RECIPE_NO_COLOR === '1') return false;
29
32
  if (process.env.RECIPE_COLOR === '1' || process.env.FORCE_COLOR === '1') return true;
30
- return Boolean(stream?.isTTY);
33
+ return Boolean((stream as NodeJS.WriteStream & { isTTY?: boolean })?.isTTY);
31
34
  }
32
35
 
33
- export function stripAnsi(text) {
36
+ export function stripAnsi(text: string): string {
34
37
  return String(text).replace(/\x1b\[[0-9;]*m/gu, '');
35
38
  }
36
39
 
37
- export function color(style, text, { stream = process.stderr } = {}) {
40
+ export function color(style: string, text: string, { stream = process.stderr }: { stream?: NodeJS.WriteStream } = {}): string {
38
41
  const value = String(text);
39
42
  if (!colorEnabled(stream)) return value;
40
- const code = STYLES[style];
43
+ const code = STYLES[style as StyleName];
41
44
  if (!code) return value;
42
45
  return `${code}${value}${STYLES.reset}`;
43
46
  }
44
47
 
45
- export function colorKv(label, value, valueStyle = 'info', { stream = process.stderr } = {}) {
48
+ export function colorKv(
49
+ label: string,
50
+ value: string,
51
+ valueStyle = 'info',
52
+ { stream = process.stderr }: { stream?: NodeJS.WriteStream } = {},
53
+ ): string {
46
54
  return `${color('label', `${label}:`, { stream })} ${color(valueStyle, value, { stream })}`;
47
55
  }
48
56
 
49
- export function colorStatusWord(word, { stream = process.stderr } = {}) {
57
+ export function colorStatusWord(word: string, { stream = process.stderr }: { stream?: NodeJS.WriteStream } = {}): string {
50
58
  const normalized = String(word || '').toLowerCase();
51
59
  if (['up', 'ready', 'yes', 'pass', 'running', 'present', 'healthy'].includes(normalized)) {
52
60
  return color('ok', word, { stream });
@@ -60,7 +68,7 @@ export function colorStatusWord(word, { stream = process.stderr } = {}) {
60
68
  return color('info', word, { stream });
61
69
  }
62
70
 
63
- export function classifyLogEvent(event) {
71
+ export function classifyLogEvent(event: string): string {
64
72
  const text = String(event || '');
65
73
  if (/Module build failed|^ERROR in |BUILD FAILED|compiled with [1-9][0-9]* error/iu.test(text)) {
66
74
  return 'err';
@@ -77,8 +85,8 @@ export function classifyLogEvent(event) {
77
85
  return 'info';
78
86
  }
79
87
 
80
- export function colorLogEvent(event, { latest = false, stream = process.stderr } = {}) {
88
+ export function colorLogEvent(event: string, { latest = false, stream = process.stderr }: { latest?: boolean; stream?: NodeJS.WriteStream } = {}): string {
81
89
  const style = classifyLogEvent(event);
82
90
  const painted = color(style === 'label' ? 'label' : style, event, { stream });
83
91
  return latest ? color('active', painted, { stream }) : painted;
84
- }
92
+ }