@deeeed/metamask-harness 0.5.0 → 0.6.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 (102) hide show
  1. package/CHANGELOG.md +51 -20
  2. package/adapters/core/inject.sh +1 -0
  3. package/adapters/extension/cleanup.mjs +1 -1
  4. package/adapters/extension/inject.mjs +4 -1
  5. package/adapters/extension/launch-browser.cjs +18 -4
  6. package/adapters/extension/live.sh +14 -2
  7. package/adapters/extension/refresh-build.sh +2 -2
  8. package/adapters/extension/sidepanel-toggle.sh +33 -12
  9. package/adapters/extension/start-watch.sh +2 -2
  10. package/adapters/extension/wallet-fixture-state.cjs +38 -24
  11. package/adapters/manifest.json +32 -3
  12. package/adapters/mobile/inject.sh +1 -0
  13. package/adapters/mobile/start-metro.sh +1 -1
  14. package/adapters/mobile/stop-metro.sh +20 -0
  15. package/adapters/mobile/verify.sh +1 -1
  16. package/adapters/shared/activate-repo-node.sh +1 -1
  17. package/adapters/shared/cli-ux.sh +44 -25
  18. package/adapters/shared/ensure-runner-deps.sh +30 -0
  19. package/adapters/shared/log-tui.mjs +4 -4
  20. package/adapters/shared/open-debug.mjs +1 -1
  21. package/adapters/shared/reap-checkout-metros.sh +53 -0
  22. package/adapters/shared/recipe-harness-root.mjs +23 -0
  23. package/adapters/shared/resolve-farmslot-ports-core.mjs +205 -0
  24. package/adapters/shared/resolve-farmslot-ports.mjs +20 -0
  25. package/adapters/shared/resolve-farmslot-ports.sh +19 -126
  26. package/bin/mm-harness +45 -4
  27. package/dist/adapters/extension/runtime.js +3 -1
  28. package/dist/adapters/mobile/provision.js +34 -1
  29. package/dist/adapters/mobile/runtime-decision.js +3 -1
  30. package/dist/adapters/resolve-farmslot-ports.js +22 -0
  31. package/dist/adapters/slot-ports.js +18 -29
  32. package/dist/cli.js +32 -1463
  33. package/dist/commands/call.js +183 -0
  34. package/dist/commands/completion-candidates.js +58 -0
  35. package/dist/commands/doctor.js +101 -0
  36. package/dist/commands/ensure-ready.js +24 -0
  37. package/dist/commands/flows.js +62 -0
  38. package/dist/commands/launch/extension.js +40 -0
  39. package/dist/commands/{launch.js → launch/index.js} +15 -47
  40. package/dist/commands/launch/mobile.js +10 -0
  41. package/dist/commands/manifest.js +72 -0
  42. package/dist/commands/parse-args.js +189 -0
  43. package/dist/commands/provision.js +136 -0
  44. package/dist/commands/resolve-extension.js +23 -0
  45. package/dist/commands/run-engine.js +341 -0
  46. package/dist/commands/run.js +217 -0
  47. package/dist/commands/runtime-decision.js +58 -0
  48. package/dist/commands/runtime-health.js +25 -0
  49. package/dist/commands/runtime-launch.js +139 -0
  50. package/dist/commands/self-test.js +52 -0
  51. package/dist/commands/stop.js +52 -0
  52. package/dist/harness.js +8 -48
  53. package/dist/mm-harness-cli.js +13 -8
  54. package/docs/CLI-SPEC.md +1 -1
  55. package/docs/CODE-MAP.md +62 -0
  56. package/library/README.md +14 -0
  57. package/library/actions/core/perps/_controller.mjs +1 -1
  58. package/library/actions/extension/platform/cdp.mjs +2 -2
  59. package/library/actions/extension/wallet/ensure_unlocked.mjs +7 -1
  60. package/library/actions/harness-exports.mjs +27 -0
  61. package/library/actions/mobile/wallet/ensure_unlocked.mjs +14 -2
  62. package/library/actions/mobile/wallet/setup.mjs +1 -1
  63. package/package.json +6 -7
  64. package/src/adapters/core/surface.ts +0 -71
  65. package/src/adapters/extension/ensure-ready.ts +0 -185
  66. package/src/adapters/extension/extension-id.ts +0 -107
  67. package/src/adapters/extension/runtime-decision.ts +0 -445
  68. package/src/adapters/extension/runtime.ts +0 -407
  69. package/src/adapters/extension/surface.ts +0 -88
  70. package/src/adapters/mobile/deps-markers.ts +0 -21
  71. package/src/adapters/mobile/prepare.ts +0 -246
  72. package/src/adapters/mobile/provision.ts +0 -594
  73. package/src/adapters/mobile/runtime-decision.ts +0 -459
  74. package/src/adapters/mobile/surface.ts +0 -71
  75. package/src/adapters/slot-ports.ts +0 -165
  76. package/src/adapters/surface.ts +0 -117
  77. package/src/adapters.ts +0 -601
  78. package/src/cli-color.ts +0 -92
  79. package/src/cli-commands.ts +0 -250
  80. package/src/cli-version.ts +0 -141
  81. package/src/cli.ts +0 -2091
  82. package/src/commands/debug.ts +0 -65
  83. package/src/commands/fixtures.ts +0 -198
  84. package/src/commands/launch.ts +0 -470
  85. package/src/commands/logs.ts +0 -99
  86. package/src/commands/shared.ts +0 -235
  87. package/src/commands/update.ts +0 -316
  88. package/src/completions-cache.ts +0 -86
  89. package/src/doctor.ts +0 -215
  90. package/src/harness.ts +0 -797
  91. package/src/heal-bounds.ts +0 -198
  92. package/src/index.ts +0 -15
  93. package/src/leaf-invoke.ts +0 -28
  94. package/src/live-adapter-contract.ts +0 -274
  95. package/src/manifest.ts +0 -47
  96. package/src/mm-harness-cli.ts +0 -655
  97. package/src/paths.ts +0 -198
  98. package/src/progress.ts +0 -117
  99. package/src/recording-target.ts +0 -147
  100. package/src/run-recording.ts +0 -329
  101. package/src/runner.ts +0 -108
  102. package/src/types.ts +0 -57
@@ -1,470 +0,0 @@
1
- // launch — start the app (Metro/build + surface), auto-ensure the runtime overlay,
2
- // and heal transport within bounds. Owns launch grammar, build-tier selection,
3
- // quick-launch change detection, and the --json contract. Device orchestration
4
- // composes adapters/ scripts directly; --verify reuses the overlay `live`
5
- // sequence via handleHarness.
6
-
7
- import { execFileSync } from 'node:child_process';
8
- import fs from 'node:fs';
9
- import path from 'node:path';
10
-
11
- import { color } from '../cli-color.ts';
12
- import { handleHarness } from '../harness.ts';
13
- import { recipeHarnessPath, recipeRuntimeDir, recipeRuntimePath, runnerDir } from '../paths.ts';
14
- import type { MetaMaskRecipeAdapter } from '../types.ts';
15
- import { prepareMobile } from '../adapters/mobile/prepare.ts';
16
- import { getAdapterSurface } from '../adapters/surface.ts';
17
- import { stopExtensionWatcher } from '../adapters/slot-ports.ts';
18
- import {
19
- ADAPTER_DETECT_NEXT,
20
- EXIT,
21
- flag,
22
- parseFlags,
23
- resolveAdapter,
24
- spawnScript,
25
- spawnScriptStreaming,
26
- str,
27
- targetOf,
28
- usageOut,
29
- } from './shared.ts';
30
- import type { DeviceAdapter, ScriptResult } from './shared.ts';
31
- import {
32
- RECOVERY_CODE,
33
- checkHealBounds,
34
- ensureOverlay,
35
- newHealState,
36
- parseHeal,
37
- } from '../heal-bounds.ts';
38
- import type { HealState } from '../heal-bounds.ts';
39
-
40
- interface LaunchFailure {
41
- code: string;
42
- message: string;
43
- recoverable: boolean;
44
- userAction?: string;
45
- exitCode: number;
46
- // The original script output, surfaced verbatim (never replaced by the note).
47
- originalError?: string;
48
- }
49
-
50
- const LAUNCH_BOOLEANS = new Set([
51
- 'build',
52
- 'watch',
53
- 'verify',
54
- 'sidepanel',
55
- 'fullscreen',
56
- 'runway',
57
- 'json',
58
- 'jsonStream',
59
- 'yes',
60
- ]);
61
-
62
- export async function handleLaunch(argv: string[]): Promise<number> {
63
- const { positional, options } = parseFlags(argv, LAUNCH_BOOLEANS);
64
- const json = flag(options, 'json');
65
- const target = targetOf(options);
66
-
67
- const heal = parseHeal(options, 'auto');
68
- if (typeof heal !== 'string') return usageOut(json, 'launch', heal.error, 'use --heal off|infra-only|auto');
69
-
70
- // Grammar: mobile requires an ios|android positional (also disambiguates the
71
- // adapter); extension takes no positional; core is headless (teaching error).
72
- const posToken = positional[0];
73
- const mobileTargetToken = posToken === 'ios' || posToken === 'android' ? posToken : undefined;
74
- const platformFlag = str(options, 'platform');
75
- const platformMobileToken =
76
- platformFlag === 'ios' || platformFlag === 'android' ? platformFlag : undefined;
77
- const mobileTarget = mobileTargetToken ?? platformMobileToken;
78
- const adapterHint: MetaMaskRecipeAdapter | undefined = mobileTarget ? 'mobile' : undefined;
79
- const adapter = resolveAdapter(options, target, adapterHint);
80
-
81
- if (!adapter) {
82
- return usageOut(json, 'launch', `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
83
- }
84
- if (adapter === 'core') {
85
- return usageOut(json, 'launch', 'core is headless; there is nothing to launch.', 'mm-harness verify');
86
- }
87
- if (adapter === 'mobile' && !mobileTarget) {
88
- return usageOut(
89
- json,
90
- 'launch',
91
- 'target is required for mobile.',
92
- 'mm-harness launch ios or mm-harness launch android',
93
- );
94
- }
95
-
96
- const wantBuild = flag(options, 'build');
97
- const wantVerify = flag(options, 'verify');
98
- const wantWatch = flag(options, 'watch');
99
- const wantRunway = flag(options, 'runway');
100
-
101
- // --runway is mobile-only.
102
- if (wantRunway && adapter !== 'mobile') {
103
- return usageOut(json, 'launch', 'runway is mobile-only.', 'drop --runway for the extension');
104
- }
105
- // --fullscreen is the explicit override when both display flags are passed.
106
- const displayMode =
107
- flag(options, 'sidepanel') && !flag(options, 'fullscreen') ? 'sidepanel' : 'fullscreen';
108
-
109
- // Env-gap flags (docs/CLI-SPEC.md Part 4): --device / --cdp-port / --watcher-port.
110
- // Resolution is flag > env > config/default: setting the env from the flag makes
111
- // the flag win; an absent flag leaves the agent/CI env untouched; the porcelain
112
- // owns config/default. Ports must be numeric (teaching error otherwise).
113
- for (const portFlag of ['cdpPort', 'watcherPort'] as const) {
114
- const value = str(options, portFlag);
115
- if (value !== undefined && !/^\d+$/u.test(value)) {
116
- return usageOut(
117
- json,
118
- 'launch',
119
- `--${portFlag === 'cdpPort' ? 'cdp-port' : 'watcher-port'} must be numeric (got: ${value}).`,
120
- `pass a numeric port, e.g. --${portFlag === 'cdpPort' ? 'cdp-port 6663' : 'watcher-port 8081'}`,
121
- );
122
- }
123
- }
124
- applyLaunchEnvOverrides(options, adapter, mobileTarget, target);
125
-
126
- const tier = wantVerify ? 'verify' : wantBuild ? 'build' : 'quick';
127
- const state = newHealState();
128
-
129
- // Quick-launch change detection: refuse a quick relaunch on a stale native
130
- // build (native inputs changed since the recorded build baseline).
131
- if (tier === 'quick' && nativeInputsChanged(target, adapter)) {
132
- return usageOut(
133
- json,
134
- 'launch',
135
- 'native build inputs changed since last build.',
136
- `mm-harness launch ${adapter === 'mobile' ? `${mobileTarget} ` : ''}--build`,
137
- );
138
- }
139
-
140
- // Auto-ensure overlay (install phase).
141
- const ensured = await ensureOverlay(adapter, target, heal, state, json);
142
- if (!ensured.ok) {
143
- return launchFail(json, adapter, mobileTarget, tier, state, {
144
- code: 'OVERLAY_INSTALL_FAILED',
145
- message: ensured.error ?? 'overlay install failed',
146
- recoverable: false,
147
- exitCode: EXIT.infra,
148
- });
149
- }
150
-
151
- // --verify absorbs the retired `live` sequence: overlay ensured above → launch
152
- // → CDP/bridge poll → smoke verify, all via the existing overlay orchestration.
153
- if (tier === 'verify') {
154
- const liveArgs = ['live', '--adapter', adapter, '--target', target];
155
- if (adapter === 'mobile' && mobileTarget) liveArgs.push('--platform', mobileTarget);
156
- if (json) liveArgs.push('--json');
157
- const code = await handleHarness(liveArgs);
158
- // handleHarness owns its own summary in --json mode; the overlay mutations we
159
- // performed are surfaced to stderr in human mode above.
160
- return code;
161
- }
162
-
163
- // Compose the launch tier: mobile runs in-process; extension spawns adapters/ scripts.
164
- let attempt = await executeComposition(adapter, mobileTarget, tier, wantWatch, target, json);
165
-
166
- if (attempt.status === 0) {
167
- return finishLaunch(json, adapter, mobileTarget, tier, displayMode, target, state);
168
- }
169
-
170
- // Bounds are ALWAYS enforced regardless of --heal.
171
- if (heal === 'off') {
172
- return launchFail(json, adapter, mobileTarget, tier, state, {
173
- code: 'LAUNCH_FAILED',
174
- message: `launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ''} failed (healing off)`,
175
- recoverable: false,
176
- exitCode: EXIT.infra,
177
- });
178
- }
179
- // Healing bounds (shared module — policy, not per-command). null = ok to proceed.
180
- const bound = checkHealBounds(target, attempt.output, state);
181
- if (bound !== null) {
182
- return launchFail(json, adapter, mobileTarget, tier, state, {
183
- code: bound.code,
184
- message: bound.message,
185
- recoverable: false,
186
- userAction: bound.userAction,
187
- exitCode: bound.exitCode,
188
- originalError: bound.originalError,
189
- });
190
- }
191
-
192
- // Infra recovery: attempt the SAME launch exactly once (Metro restarts /
193
- // Chrome reopens). Push recoveryCode BEFORE the attempt so any subsequent
194
- // checkHealBounds call sees it and refuses a second loop.
195
- const recoveryCode = RECOVERY_CODE[adapter];
196
- state.attemptedRecoveries.push(recoveryCode);
197
- attempt = await executeComposition(adapter, mobileTarget, tier, wantWatch, target, json);
198
- if (attempt.status === 0) {
199
- state.recovered.push(recoveryCode);
200
- return finishLaunch(json, adapter, mobileTarget, tier, displayMode, target, state);
201
- }
202
- // Second attempt failed — same-recovery-twice bound hit.
203
- return launchFail(json, adapter, mobileTarget, tier, state, {
204
- code: 'SAME_RECOVERY_TWICE',
205
- message: `launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ''} failed again after ${recoveryCode} recovery — refusing to loop.`,
206
- recoverable: false,
207
- exitCode: EXIT.bounded,
208
- originalError: attempt.output.trim() || undefined,
209
- });
210
- }
211
-
212
- // Map the env-gap flags onto the env vars the leaf scripts read. For mobile, slot
213
- // port/simulator defaults are resolved first (pool-wins, then formula-fills-empty)
214
- // so that the harness respects slot isolation; explicit CLI flags applied below
215
- // win over everything. Extension: flag > env > config/default.
216
- function applyLaunchEnvOverrides(
217
- options: Record<string, string | boolean>,
218
- adapter: DeviceAdapter,
219
- mobileTarget: string | undefined,
220
- target: string,
221
- ): void {
222
- // Slot isolation: resolve ports/device from the checkout's own slot context,
223
- // the farmslot pool, or the directory-suffix formula BEFORE applying explicit
224
- // flags — extension needs this as much as mobile (CDP_PORT), so neither
225
- // adapter hard-fails on a value its slot already knows. The surface owns the
226
- // per-platform resolution; this command never branches on adapter for it.
227
- getAdapterSurface(adapter).resolveSlotPorts(target);
228
-
229
- const device = str(options, 'device');
230
- if (device && adapter === 'mobile') {
231
- if (mobileTarget === 'android') {
232
- process.env.ADB_SERIAL = device;
233
- process.env.ANDROID_SERIAL = device;
234
- process.env.ANDROID_DEVICE = device;
235
- } else {
236
- process.env.IOS_SIMULATOR = device;
237
- }
238
- }
239
- const cdpPort = str(options, 'cdpPort');
240
- if (cdpPort) {
241
- process.env.CDP_PORT = cdpPort;
242
- process.env.RECIPE_CDP_PORT = cdpPort;
243
- }
244
- const watcherPort = str(options, 'watcherPort');
245
- if (watcherPort) {
246
- process.env.WATCHER_PORT = watcherPort;
247
- process.env.RECIPE_WATCHER_PORT = watcherPort;
248
- process.env.METRO_PORT = watcherPort;
249
- }
250
- }
251
-
252
- // Native build inputs changed vs the recorded build baseline (git diff heuristic,
253
- // mirrors the farm-layer native-diff). No baseline recorded → cannot be stale →
254
- // allow the quick launch.
255
- function nativeInputsChanged(target: string, adapter: MetaMaskRecipeAdapter): boolean {
256
- const baselineFile = recipeRuntimePath(target, '.last-build-ref');
257
- let baseline: string;
258
- try {
259
- baseline = fs.readFileSync(baselineFile, 'utf8').trim();
260
- } catch {
261
- return false;
262
- }
263
- if (!baseline) return false;
264
- const paths =
265
- adapter === 'mobile'
266
- ? ['ios', 'android', 'package.json', 'yarn.lock']
267
- : ['package.json', 'yarn.lock', 'webpack.config.js'];
268
- try {
269
- const diff = execFileSync('git', ['-C', target, 'diff', '--name-only', baseline, '--', ...paths], {
270
- encoding: 'utf8',
271
- stdio: ['ignore', 'pipe', 'ignore'],
272
- }).trim();
273
- return diff.length > 0;
274
- } catch {
275
- return false;
276
- }
277
- }
278
-
279
- // Execute the launch composition for (adapter, tier). Mobile runs prepareMobile()
280
- // in-process (no shell orchestrator); extension arms spawn adapters/ scripts directly.
281
- //
282
- // mobile quick|build → prepareMobile() in-process → leaf scripts
283
- // extension rebuild → kill watcher + clear rebuild.log + live.sh --start-watch
284
- // extension build → adapters/extension/start-watch.sh
285
- // extension watch → adapters/extension/start-watch.sh
286
- async function executeComposition(
287
- adapter: DeviceAdapter,
288
- mobileTarget: string | undefined,
289
- tier: 'quick' | 'build' | 'verify',
290
- wantWatch: boolean,
291
- target: string,
292
- json: boolean,
293
- ): Promise<ScriptResult> {
294
- if (adapter === 'mobile') {
295
- const platform = mobileTarget ?? 'ios';
296
- const watcherPort = process.env.WATCHER_PORT ? parseInt(process.env.WATCHER_PORT, 10) : undefined;
297
- const preflightMode = tier === 'build' ? 'auto' : 'fast';
298
- return prepareMobile(target, { platform, json, watcherPort, preflightMode });
299
- }
300
-
301
- // Extension rebuild (quick relaunch): kill watcher → clear rebuild.log → live.sh --start-watch.
302
- if (!wantWatch && tier === 'quick') {
303
- return extensionRebuild(target, json);
304
- }
305
-
306
- // Extension build or watch: full webpack compile via start-watch.sh. Streams
307
- // live — a webpack compile is minutes long and must never look hung.
308
- const startWatchSh = path.join(runnerDir, 'adapters/extension/start-watch.sh');
309
- const watchArgs = ['--target', target];
310
- if (process.env.WATCHER_PORT) watchArgs.push('--watcher-port', process.env.WATCHER_PORT);
311
- console.error(`→ extension ${wantWatch ? 'watch' : 'build'} — webpack :${process.env.WATCHER_PORT ?? 'default'} (output streams below)`);
312
- return spawnScriptStreaming(startWatchSh, watchArgs, target);
313
- }
314
-
315
- // Extension rebuild: kill the harness-owned webpack watcher (pid file + ps-scan
316
- // for orphans), clear the rebuild log, then drive the installed overlay's
317
- // live.sh --start-watch and tee its output into the rebuild log.
318
- async function extensionRebuild(target: string, json: boolean): Promise<ScriptResult> {
319
- const runtimeDirRel = recipeRuntimeDir();
320
- const runtimeAbs = path.join(target, runtimeDirRel);
321
- const rebuildLog = path.join(runtimeAbs, 'rebuild.log');
322
-
323
- // E1a: Kill harness-owned watcher via pid file, then ps-scan for orphans.
324
- stopExtensionWatcher(target);
325
-
326
- // E1c: Clear the rebuild log (directory must exist for tee).
327
- fs.mkdirSync(path.dirname(rebuildLog), { recursive: true });
328
- fs.writeFileSync(rebuildLog, '');
329
-
330
- const liveScript = recipeHarnessPath(target, 'extension', 'scripts', 'live.sh');
331
- const liveArgs = ['--target', target, '--start-watch'];
332
- if (process.env.CDP_PORT) liveArgs.push('--cdp-port', process.env.CDP_PORT);
333
- if (process.env.WATCHER_PORT) liveArgs.push('--watcher-port', process.env.WATCHER_PORT);
334
- // Streams live: the watcher restart + Chromium boot runs for minutes and the
335
- // command must show progress immediately, not a silent prompt.
336
- console.error(`→ extension quick relaunch — webpack :${process.env.WATCHER_PORT ?? 'default'} · CDP :${process.env.CDP_PORT ?? 'default'} (output streams below)`);
337
- const result = await spawnScriptStreaming(liveScript, liveArgs, target);
338
- // Tee live.sh output into rebuild.log for post-mortem diagnosis.
339
- if (result.output) {
340
- try { fs.appendFileSync(rebuildLog, result.output); } catch { /* best-effort */ }
341
- }
342
- return result;
343
- }
344
-
345
- // Apply the extension display mode AFTER the base launch succeeded, then emit
346
- // the pass summary. Fullscreen = the plain rebuild/build/watch launch above
347
- // (default popup/home window). Sidepanel additionally drives the
348
- // chrome.sidePanel mechanism via adapters/extension/sidepanel-toggle.sh.
349
- // --sidepanel and --fullscreen compose DIFFERENT script calls and the display
350
- // mode actually reaches the browser. Mobile has no side panel; the flag is a
351
- // no-op there.
352
- function finishLaunch(
353
- json: boolean,
354
- adapter: DeviceAdapter,
355
- mobileTarget: string | undefined,
356
- tier: string,
357
- displayMode: string,
358
- target: string,
359
- state: HealState,
360
- ): number {
361
- if (adapter === 'extension' && displayMode === 'sidepanel') {
362
- const sidepanelSh = path.join(runnerDir, 'adapters/extension/sidepanel-toggle.sh');
363
- const sidepanelArgs = ['open'];
364
- if (process.env.CDP_PORT) sidepanelArgs.push('--cdp-port', process.env.CDP_PORT);
365
- const sidepanel = spawnScript(sidepanelSh, sidepanelArgs, target, json);
366
- if (sidepanel.status !== 0) {
367
- return launchFail(json, adapter, mobileTarget, tier, state, {
368
- code: 'SIDEPANEL_OPEN_FAILED',
369
- message: 'app launched but opening the side panel failed.',
370
- recoverable: false,
371
- userAction: `bash adapters/extension/sidepanel-toggle.sh open --cdp-port ${process.env.CDP_PORT ?? '<CDP_PORT>'}`,
372
- exitCode: EXIT.runtime,
373
- originalError: sidepanel.output.trim() || undefined,
374
- });
375
- }
376
- }
377
- return launchPass(json, adapter, mobileTarget, tier, displayMode, state);
378
- }
379
-
380
- function launchPass(
381
- json: boolean,
382
- adapter: DeviceAdapter,
383
- mobileTarget: string | undefined,
384
- tier: string,
385
- displayMode: string,
386
- state: HealState,
387
- ): number {
388
- if (json) {
389
- console.log(
390
- JSON.stringify(
391
- {
392
- schemaVersion: 1,
393
- command: 'launch',
394
- adapter,
395
- platform: mobileTarget ?? (adapter === 'extension' ? displayMode : null),
396
- tier,
397
- status: 'pass',
398
- phase: 'launch',
399
- recovered: state.recovered,
400
- mutations: state.mutations,
401
- exitCode: EXIT.ok,
402
- },
403
- null,
404
- 2,
405
- ),
406
- );
407
- } else {
408
- // The summary must say what actually happened where: the target device,
409
- // what the tier meant, and that the app+bridge are up — "(quick) ready"
410
- // alone reads as a no-op even when a launch occurred.
411
- const device =
412
- adapter === 'mobile'
413
- ? process.env.IOS_SIMULATOR || process.env.ADB_SERIAL || 'booted device'
414
- : displayMode;
415
- const tierNote = tier === 'quick' ? 'quick relaunch, no native build' : tier;
416
- const devNote = process.env.MM_HARNESS_BIN ? ` ${color('dim', '[dev: MM_HARNESS_BIN]')}` : '';
417
- console.error(
418
- `${color('ok', '✓')} launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ''} — ${color('ok', String(device))} · ${tierNote} · app + bridge ready${devNote}`,
419
- );
420
- }
421
- return EXIT.ok;
422
- }
423
-
424
- function launchFail(
425
- json: boolean,
426
- adapter: DeviceAdapter,
427
- mobileTarget: string | undefined,
428
- tier: string,
429
- state: HealState,
430
- failure: LaunchFailure,
431
- ): number {
432
- if (json) {
433
- console.log(
434
- JSON.stringify(
435
- {
436
- schemaVersion: 1,
437
- command: 'launch',
438
- adapter,
439
- platform: mobileTarget ?? null,
440
- tier,
441
- status: 'fail',
442
- phase: state.attemptedRecoveries.length ? 'recover' : 'launch',
443
- recovered: state.recovered,
444
- mutations: state.mutations,
445
- recoverable: failure.recoverable,
446
- attemptedRecoveries: state.attemptedRecoveries,
447
- exitCode: failure.exitCode,
448
- error: {
449
- code: failure.code,
450
- message: failure.message,
451
- retryable: failure.recoverable,
452
- userAction: failure.userAction ?? null,
453
- // The classification note is in `message`; the REAL failure text lives
454
- // here verbatim so it is never lost.
455
- originalError: failure.originalError ?? null,
456
- },
457
- },
458
- null,
459
- 2,
460
- ),
461
- );
462
- } else {
463
- console.error(
464
- `✗ launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ''} failed\n ${failure.message}` +
465
- (failure.originalError ? `\n --- original failure ---\n${failure.originalError}` : '') +
466
- (failure.userAction ? `\n Next: ${failure.userAction}` : ''),
467
- );
468
- }
469
- return failure.exitCode;
470
- }
@@ -1,99 +0,0 @@
1
- // logs — tail Metro/webpack + app logs by composing log-tui.mjs (compact) or
2
- // plain `tail -f` (--full). Teaches (exit 2) when nothing is running for the
3
- // checkout.
4
-
5
- import fs from 'node:fs';
6
- import path from 'node:path';
7
-
8
- import { runnerDir } from '../paths.ts';
9
- import { getAdapterSurface } from '../adapters/surface.ts';
10
- import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, spawnScript, str, targetOf, usageOut } from './shared.ts';
11
-
12
- const LOGS_BOOLEANS = new Set(['full', 'json']);
13
-
14
- export async function handleLogs(argv: string[]): Promise<number> {
15
- const { options } = parseFlags(argv, LOGS_BOOLEANS);
16
- const json = flag(options, 'json');
17
- const target = targetOf(options);
18
- const adapter = resolveAdapter(options, target);
19
-
20
- if (!adapter) {
21
- return usageOut(json, 'logs', `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
22
- }
23
- const surface = getAdapterSurface(adapter);
24
- if (surface.headless) {
25
- return usageOut(json, 'logs', 'core is headless; it has no dev server logs.', surface.hints.launch);
26
- }
27
-
28
- // Source selection is platform-scoped: the valid names and the default come
29
- // from THIS adapter's dev-server logs (mobile: metro; extension: webpack), plus
30
- // the app-log source. No adapter's vocabulary is hardcoded here — every
31
- // non-headless adapter provides at least one log source (headless core returned
32
- // above), so the default is that adapter's first source.
33
- const logSources = surface.logSources(target);
34
- const sourceLabels = logSources.map((entry) => entry.label);
35
- const defaultSource = sourceLabels[0];
36
- const source = str(options, 'source') ?? defaultSource;
37
- const validSources = [...sourceLabels, 'app'];
38
- if (!validSources.includes(source)) {
39
- return usageOut(json, 'logs', `--source must be one of: ${validSources.join(', ')}.`, `mm-harness logs --source ${defaultSource}`);
40
- }
41
-
42
- // Env-gap flag (docs/CLI-SPEC.md Part 4): --events sets the compact event count
43
- // read from RECIPE_LOG_EVENTS. Resolution is flag > env > default: the flag wins
44
- // by setting the env; an absent flag leaves the agent/CI env alone.
45
- const events = str(options, 'events');
46
- if (events !== undefined) {
47
- if (!/^\d+$/u.test(events)) {
48
- return usageOut(json, 'logs', `--events must be numeric (got: ${events}).`, 'mm-harness logs --events 20');
49
- }
50
- process.env.RECIPE_LOG_EVENTS = events;
51
- }
52
-
53
- // Nothing running → teaching error pointing at launch. A dev-server log file is
54
- // the signal that the dev server has been started for this checkout; the surface
55
- // owns which files a platform writes (mobile: metro.log; extension: webpack +
56
- // watcher + rebuild logs). A `--source` naming a specific dev-server log is
57
- // preferred; otherwise the most-relevant existing candidate is tailed.
58
- const requested = logSources.find((entry) => entry.label === source);
59
- const ordered = requested ? [requested, ...logSources.filter((entry) => entry !== requested)] : logSources;
60
- const logFile = ordered.find((entry) => fs.existsSync(entry.path))?.path;
61
- if (!logFile) {
62
- const names = logSources.map((entry) => path.basename(entry.path)).join(' / ');
63
- return usageOut(
64
- json,
65
- 'logs',
66
- `nothing running for this checkout (no ${names}).`,
67
- surface.hints.launch,
68
- );
69
- }
70
-
71
- const full = flag(options, 'full');
72
- if (full) {
73
- // Raw follow: the human explicitly wants unfiltered output.
74
- const result = spawnScript('tail', ['-n', '+1', '-F', logFile], target, json);
75
- return result.status === 0 ? EXIT.ok : EXIT.runtime;
76
- }
77
-
78
- // Compact mode: last N events + pointer to full log.
79
- const logTui = path.join(runnerDir, 'adapters/shared/log-tui.mjs');
80
- const eventCount = process.env.RECIPE_LOG_EVENTS ?? '20';
81
- const uiMode = process.env.RECIPE_LOG_UI_MODE ?? 'compact';
82
- const result = spawnScript(
83
- process.execPath,
84
- [logTui, 'tail', '--log', logFile, '--events', eventCount, '--follow', '--mode', uiMode],
85
- target,
86
- json,
87
- );
88
-
89
- if (json) {
90
- console.log(
91
- JSON.stringify(
92
- { schemaVersion: 1, command: 'logs', adapter, source, logFile, exitCode: result.status === 0 ? EXIT.ok : EXIT.runtime },
93
- null,
94
- 2,
95
- ),
96
- );
97
- }
98
- return result.status === 0 ? EXIT.ok : EXIT.runtime;
99
- }