@deeeed/metamask-harness 0.2.0 → 0.3.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 (109) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +20 -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 +250 -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 +5 -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 +11 -3
  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 +25 -18
  54. package/src/commands/debug.ts +61 -0
  55. package/src/commands/fixtures.ts +196 -0
  56. package/{runner/src → src}/commands/launch.ts +185 -41
  57. package/src/commands/logs.ts +86 -0
  58. package/{runner/src → src}/commands/shared.ts +26 -26
  59. package/{runner/src → src}/harness.ts +97 -10
  60. package/src/index.ts +15 -0
  61. package/{runner/src → src}/live-adapter-contract.ts +1 -1
  62. package/{runner/src → src}/mm-harness-cli.ts +19 -3
  63. package/{runner/src → src}/paths.ts +2 -2
  64. package/{orchestration/lib/progress.mjs → src/progress.ts} +31 -21
  65. package/docs/extension-runtime-commands.md +0 -60
  66. package/docs/harness-cli.md +0 -43
  67. package/orchestration/compat-overlays/mobile/README.md +0 -13
  68. package/orchestration/doctor.mjs +0 -72
  69. package/orchestration/lib/cli-home.mjs +0 -354
  70. package/orchestration/lib/recipe-paths.mjs +0 -26
  71. package/orchestration/mobile/launch.sh +0 -137
  72. package/orchestration/mobile/live.sh +0 -125
  73. package/orchestration/porcelain/metamask-recipe +0 -99
  74. package/orchestration/porcelain/mm-recipe +0 -1591
  75. package/orchestration/porcelain/mme-recipe +0 -1181
  76. package/runner/src/commands/debug.ts +0 -44
  77. package/runner/src/commands/fixtures.ts +0 -99
  78. package/runner/src/commands/logs.ts +0 -60
  79. package/runner/src/index.ts +0 -6
  80. /package/{orchestration → adapters}/extension/launch-browser.cjs +0 -0
  81. /package/{orchestration → adapters}/extension/pin-remote-flags.cjs +0 -0
  82. /package/{orchestration → adapters}/extension/sidepanel-toggle.sh +0 -0
  83. /package/{orchestration → adapters}/extension/snapshot-dist.sh +0 -0
  84. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/cdp-eval.cjs +0 -0
  85. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/config.cjs +0 -0
  86. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/issue-capture.cjs +0 -0
  87. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/target-discovery.cjs +0 -0
  88. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/ws-client.cjs +0 -0
  89. /package/{library/actions → adapters}/mobile/bridge-runtime/setup-wallet.sh +0 -0
  90. /package/{library/actions/mobile/app-overlay → adapters/mobile/overlay}/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -0
  91. /package/{library/actions/mobile/app-overlay → adapters/mobile/overlay}/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -0
  92. /package/{orchestration/compat-overlays/mobile → adapters/mobile/overlay/compat}/rn81-message-event-source.patch +0 -0
  93. /package/{orchestration/lib → adapters/shared}/activate-repo-node.sh +0 -0
  94. /package/{orchestration/lib → adapters/shared}/ensure-runner-deps.sh +0 -0
  95. /package/{orchestration/lib → adapters/shared}/harness-path.sh +0 -0
  96. /package/{orchestration/lib → adapters/shared}/hash-helpers.sh +0 -0
  97. /package/{orchestration/lib → adapters/shared}/json-field.sh +0 -0
  98. /package/{orchestration/lib → adapters/shared}/path-defaults.json +0 -0
  99. /package/{orchestration → src/adapters}/extension/ensure-ready.ts +0 -0
  100. /package/{orchestration → src/adapters}/extension/extension-id.ts +0 -0
  101. /package/{orchestration → src/adapters}/mobile/deps-markers.ts +0 -0
  102. /package/{runner/src → src}/completions-cache.ts +0 -0
  103. /package/{runner/src → src}/doctor.ts +0 -0
  104. /package/{runner/src → src}/heal-bounds.ts +0 -0
  105. /package/{runner/src → src}/manifest.ts +0 -0
  106. /package/{runner/src → src}/recording-target.ts +0 -0
  107. /package/{runner/src → src}/run-recording.ts +0 -0
  108. /package/{runner/src → src}/runner.ts +0 -0
  109. /package/{runner/src → src}/types.ts +0 -0
@@ -1,12 +1,11 @@
1
1
  // Shared helpers for the mm-harness command modules: flag parsing, adapter
2
- // resolution, porcelain composition, and the teaching-error emitter. Each command
3
- // module (launch/logs/debug/fixtures) composes mobile/extension porcelain scripts
4
- // that live under orchestration/porcelain/; these helpers are the common seam.
2
+ // resolution, script composition, and the teaching-error emitter. Each command
3
+ // module composes adapters/ scripts directly via spawnScript.
5
4
  //
6
- // Composition seams (overridable for CI/agents and contract tests):
7
- // MM_HARNESS_MM_RECIPE_BINmobile porcelain (default: <runner>/orchestration/porcelain/mm-recipe)
8
- // MM_HARNESS_MME_RECIPE_BIN extension porcelain (default: <runner>/orchestration/porcelain/mme-recipe)
9
- // A stub that records argv against one of these paths is the contract-test lever.
5
+ // Composition seam (overridable for contract tests):
6
+ // MM_HARNESS_SCRIPT_BIN_<STEM>override a specific script by its basename.
7
+ // For node invocations (bin === process.execPath) the stem is derived from the
8
+ // script path in args[0], e.g. MM_HARNESS_SCRIPT_BIN_OPEN_DEBUG_MJS.
10
9
 
11
10
  import { spawnSync } from 'node:child_process';
12
11
  import path from 'node:path';
@@ -89,36 +88,37 @@ export function resolveAdapter(
89
88
  return detectAdapter(target);
90
89
  }
91
90
 
92
- function porcelainBin(adapter: DeviceAdapter): string {
93
- if (adapter === 'mobile') {
94
- return process.env.MM_HARNESS_MM_RECIPE_BIN ?? path.join(runnerDir, 'orchestration', 'porcelain', 'mm-recipe');
95
- }
96
- return process.env.MM_HARNESS_MME_RECIPE_BIN ?? path.join(runnerDir, 'orchestration', 'porcelain', 'mme-recipe');
97
- }
98
-
99
- export interface PorcelainResult {
91
+ export interface ScriptResult {
100
92
  status: number;
101
93
  output: string;
102
94
  }
103
95
 
104
- // Compose the porcelain. Output is always captured (needed for heal
105
- // classification) and, in human mode, forwarded to our stderr so the human still
106
- // sees it. --json keeps stdout clean for the machine summary.
107
- export function spawnPorcelain(
108
- adapter: DeviceAdapter,
96
+ // Compose an adapters/ script directly. Output is always captured (needed for
97
+ // heal classification) and, in human mode, forwarded to stderr. --json keeps
98
+ // stdout clean for the machine summary.
99
+ export function spawnScript(
100
+ script: string,
109
101
  args: string[],
110
- target: string,
102
+ cwd: string,
111
103
  json: boolean,
112
- ): PorcelainResult {
113
- const bin = porcelainBin(adapter);
114
- const result = spawnSync(bin, args, {
115
- cwd: target,
104
+ ): ScriptResult {
105
+ // For node invocations (script === process.execPath) the seam stem is derived
106
+ // from args[0] so each spawned script has its own override key.
107
+ const isNodeScript = script === process.execPath && args.length > 0;
108
+ const stem = isNodeScript
109
+ ? path.basename(args[0]).replace(/[^A-Za-z0-9]/gu, '_').toUpperCase()
110
+ : path.basename(script).replace(/[^A-Za-z0-9]/gu, '_').toUpperCase();
111
+ const override = process.env[`MM_HARNESS_SCRIPT_BIN_${stem}`];
112
+ const bin = override ?? script;
113
+ const spawnArgs = override !== undefined && isNodeScript ? args.slice(1) : args;
114
+ const result = spawnSync(bin, spawnArgs, {
115
+ cwd,
116
116
  encoding: 'utf8',
117
117
  env: process.env,
118
118
  maxBuffer: 64 * 1024 * 1024,
119
119
  });
120
120
  if (result.error) {
121
- return { status: 1, output: `porcelain could not start: ${result.error.message}` };
121
+ return { status: 1, output: `script could not start (${path.basename(script)}): ${result.error.message}` };
122
122
  }
123
123
  const output = `${result.stdout ?? ''}${result.stderr ?? ''}`;
124
124
  if (!json && output) process.stderr.write(output);
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
 
5
5
  import { recipeHarnessPath, recipeRuntimeDir, runnerDir } from './paths.ts';
6
6
  import type { MetaMaskRecipeAdapter } from './types.ts';
7
+ import { prepareMobile } from './adapters/mobile/prepare.ts';
7
8
 
8
9
  // The `harness` command moves the recipe-harness skill's runner-locate + dispatch
9
10
  // bash (resolve-runner-source.sh + recipe-harness.sh dispatch_adapter_action)
@@ -277,11 +278,14 @@ function resolveEntry(base: string, candidates: string[], mode: 'exec' | 'file')
277
278
 
278
279
  // Content of an installed overlay's runner source pointer, if present. Command
279
280
  // substitution in the skill stripped trailing newlines, so trim here too.
281
+ // Validates the path exists so stale stamps (e.g. from a renamed package dir)
282
+ // fall back to the current runnerDir rather than producing ENOENT at cleanup.
280
283
  function installedRunnerSource(target: string, adapter: MetaMaskRecipeAdapter): string | undefined {
281
284
  const pointer = path.join(recipeHarnessPath(target, adapter), 'runner', '.runner-source');
282
285
  if (!fs.existsSync(pointer)) return undefined;
283
286
  const value = fs.readFileSync(pointer, 'utf8').trim();
284
- return value || undefined;
287
+ if (!value || !fs.existsSync(value)) return undefined;
288
+ return value;
285
289
  }
286
290
 
287
291
  function isExecutable(file: string): boolean {
@@ -299,15 +303,15 @@ interface HarnessDispatch {
299
303
  }
300
304
 
301
305
  const INJECT_CANDIDATES: Record<MetaMaskRecipeAdapter, { entry: string; fallback: string }> = {
302
- mobile: { entry: 'orchestration/mobile/inject.sh', fallback: 'scripts/inject-mobile-harness.sh' },
303
- extension: { entry: 'orchestration/extension/inject.mjs', fallback: 'scripts/inject-extension-harness.mjs' },
304
- core: { entry: 'orchestration/core/inject.sh', fallback: 'scripts/inject-core-harness.sh' },
306
+ mobile: { entry: 'adapters/mobile/inject.sh', fallback: 'scripts/inject-mobile-harness.sh' },
307
+ extension: { entry: 'adapters/extension/inject.mjs', fallback: 'scripts/inject-extension-harness.mjs' },
308
+ core: { entry: 'adapters/core/inject.sh', fallback: 'scripts/inject-core-harness.sh' },
305
309
  };
306
310
 
307
311
  const CLEANUP_CANDIDATES: Record<MetaMaskRecipeAdapter, { entry: string; fallback: string }> = {
308
- mobile: { entry: 'orchestration/mobile/cleanup.sh', fallback: 'scripts/cleanup-mobile-harness.sh' },
309
- extension: { entry: 'orchestration/extension/cleanup.mjs', fallback: 'scripts/cleanup-extension-harness.mjs' },
310
- core: { entry: 'orchestration/core/cleanup.sh', fallback: 'scripts/cleanup-core-harness.sh' },
312
+ mobile: { entry: 'adapters/mobile/cleanup.sh', fallback: 'scripts/cleanup-mobile-harness.sh' },
313
+ extension: { entry: 'adapters/extension/cleanup.mjs', fallback: 'scripts/cleanup-extension-harness.mjs' },
314
+ core: { entry: 'adapters/core/cleanup.sh', fallback: 'scripts/cleanup-core-harness.sh' },
311
315
  };
312
316
 
313
317
  // Resolve the exact command + fixed prefix args to exec, mirroring
@@ -337,7 +341,7 @@ function resolveHarnessDispatch(
337
341
  }
338
342
 
339
343
  if (adapter === 'core' && action === 'verify') {
340
- const delegate = path.join(recipeHarnessPath(target, 'core'), 'runner', 'bin', 'metamask-recipe');
344
+ const delegate = path.join(recipeHarnessPath(target, 'core'), 'runner', 'bin', 'mm-harness');
341
345
  if (!isExecutable(delegate)) {
342
346
  return {
343
347
  error:
@@ -351,19 +355,97 @@ function resolveHarnessDispatch(
351
355
  }
352
356
 
353
357
  // mobile/extension live|verify: prefer the installed helper, else the runner's
354
- // own orchestration/runner/scripts helper (first executable candidate wins).
358
+ // own adapters/scripts helper (first executable candidate wins).
355
359
  const installedScript = path.join(recipeHarnessPath(target, adapter), 'scripts', `${action}.sh`);
356
360
  if (isExecutable(installedScript)) {
357
361
  return { command: installedScript, prefixArgs: [] };
358
362
  }
359
363
  const command = resolveEntry(
360
364
  runnerDir,
361
- [`orchestration/${adapter}/${action}.sh`, `runner/${adapter}/${action}.sh`, `scripts/${adapter}/${action}.sh`],
365
+ [`adapters/${adapter}/${action}.sh`, `scripts/${adapter}/${action}.sh`],
362
366
  'exec',
363
367
  );
364
368
  return { command, prefixArgs: [] };
365
369
  }
366
370
 
371
+ // Mobile live: prepare device in-process then run the verify leaf.
372
+ // Replaces the retired live.sh → launch.sh → mm-harness-mobile-prepare chain.
373
+ // Control flows down only: TS calls prepareMobile(), then spawns verify.sh.
374
+ async function handleMobileLive(
375
+ target: string,
376
+ forwardArgs: string[],
377
+ json: boolean,
378
+ autoDetected: boolean,
379
+ ): Promise<number> {
380
+ const platform = argValue(forwardArgs, '--platform') ?? 'ios';
381
+ const watcherPortStr = argValue(forwardArgs, '--watcher-port') ?? process.env.WATCHER_PORT;
382
+ const watcherPort = watcherPortStr ? parseInt(watcherPortStr, 10) : undefined;
383
+ const start = Date.now();
384
+
385
+ if (!json) {
386
+ const detected = autoDetected ? ', auto-detected' : '';
387
+ console.error(`→ live (mobile${detected}) — target: ${target}`);
388
+ }
389
+
390
+ const prepResult = await prepareMobile(target, { platform, json, watcherPort });
391
+ if (prepResult.status !== 0) {
392
+ const elapsed = ((Date.now() - start) / 1000).toFixed(1);
393
+ if (json) {
394
+ console.log(
395
+ harnessSummary('live', 'mobile', target, 'fail', prepResult.status, autoDetected, {
396
+ code: 'MOBILE_PREPARE_FAILED',
397
+ message: `mobile prepare failed (exit ${prepResult.status})`,
398
+ }),
399
+ );
400
+ } else {
401
+ console.error(
402
+ `✗ live mobile failed (exit ${prepResult.status}, ${elapsed}s)\n ${failureHint('mobile', 'live')}`,
403
+ );
404
+ }
405
+ return prepResult.status;
406
+ }
407
+
408
+ // Verify leaf: --no-auto-start because launch already started the app.
409
+ const installedVerify = path.join(recipeHarnessPath(target, 'mobile'), 'scripts', 'verify.sh');
410
+ const verifySh = isExecutable(installedVerify)
411
+ ? installedVerify
412
+ : resolveEntry(runnerDir, ['adapters/mobile/verify.sh', 'scripts/mobile/verify.sh'], 'exec');
413
+
414
+ const verifyArgs = hasArg(forwardArgs, '--no-auto-start')
415
+ ? [...forwardArgs]
416
+ : ['--no-auto-start', ...forwardArgs];
417
+
418
+ const result = spawnSync(verifySh, verifyArgs, {
419
+ stdio: json ? ['inherit', 2, 'inherit'] : 'inherit',
420
+ env: process.env,
421
+ });
422
+ const elapsed = ((Date.now() - start) / 1000).toFixed(1);
423
+ const exitCode = result.status ?? 1;
424
+
425
+ if (json) {
426
+ console.log(
427
+ harnessSummary(
428
+ 'live',
429
+ 'mobile',
430
+ target,
431
+ exitCode === 0 ? 'pass' : 'fail',
432
+ exitCode,
433
+ autoDetected,
434
+ exitCode === 0
435
+ ? undefined
436
+ : { code: 'MOBILE_VERIFY_FAILED', message: `mobile live verify failed (exit ${exitCode})` },
437
+ ),
438
+ );
439
+ } else if (exitCode === 0) {
440
+ console.error(`✓ live mobile passed (${elapsed}s)`);
441
+ } else {
442
+ console.error(
443
+ `✗ live mobile failed (exit ${exitCode}, ${elapsed}s)\n ${failureHint('mobile', 'live')}`,
444
+ );
445
+ }
446
+ return exitCode;
447
+ }
448
+
367
449
  export async function handleHarness(argv: string[]): Promise<number> {
368
450
  const action = argv[0];
369
451
  if (!action || action === '-h' || action === '--help') {
@@ -425,6 +507,11 @@ export async function handleHarness(argv: string[]): Promise<number> {
425
507
  forwardArgs = applyExtensionRuntimeEnv(target, harnessAction, forwardArgs);
426
508
  }
427
509
 
510
+ // Mobile live is orchestrated in-process (prepareMobile → verify.sh leaf).
511
+ if (adapter === 'mobile' && harnessAction === 'live') {
512
+ return handleMobileLive(target, forwardArgs, json, autoDetected);
513
+ }
514
+
428
515
  const dispatch = resolveHarnessDispatch(adapter, harnessAction, target);
429
516
  if ('error' in dispatch) {
430
517
  if (json) {
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ // Keep the package entrypoint narrow: runner factory, manifest, doctor, and the
2
+ // path/import helpers that library actions are allowed to consume (R9 — no
3
+ // deep-importing src internals past this file).
4
+ export { createDoctorReport } from './doctor.ts';
5
+ export { loadActionManifest, validateManifest } from './manifest.ts';
6
+ export { createMetaMaskExtensionRunner, createMetaMaskMobileRunner, createMetaMaskRunner } from './runner.ts';
7
+ export type { CreateMetaMaskRunnerOptions, MetaMaskDoctorReport, MetaMaskRecipeAdapter } from './types.ts';
8
+ export {
9
+ extensionIdPath,
10
+ importRecipeHarnessRuntimeBrowserExtension,
11
+ importRecipeHarnessRuntimeCdp,
12
+ recipeHarnessPath,
13
+ walletFixturePath,
14
+ } from './paths.ts';
15
+ export { captureActiveRecipeRecordingSnapshot } from './run-recording.ts';
@@ -223,7 +223,7 @@ export async function runLiveAdapterScript({ platform, action, node, context }:
223
223
  const script = await resolveLiveAdapter(platform, action);
224
224
  if (!script) return null;
225
225
 
226
- const tempDir = await mkdtemp(path.join(os.tmpdir(), 'metamask-recipe-live-adapter-'));
226
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), 'mm-harness-live-adapter-'));
227
227
  const inputPath = path.join(tempDir, 'input.json');
228
228
  const outputPath = path.join(tempDir, 'output.json');
229
229
  const input = {
@@ -12,8 +12,8 @@ import { fileURLToPath } from 'node:url';
12
12
 
13
13
  import { Command } from 'commander';
14
14
 
15
- // runner root (…/runner) → its parent is the package root that holds scripts/.
16
- const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
15
+ // …/src → its parent is the package root that holds scripts/.
16
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
17
17
 
18
18
  // Signal cli.ts NOT to auto-run its own dispatch when we import it below: we
19
19
  // reuse its `main` in-process. Must be set BEFORE the dynamic import runs the
@@ -474,7 +474,23 @@ for (const retired of RETIRED) {
474
474
  .allowUnknownOption()
475
475
  .helpOption(false)
476
476
  .argument('[args...]')
477
- .action(() => {
477
+ .action(async () => {
478
+ // `live --adapter mobile` (or android) is handled in-process via handleMobileLive.
479
+ // All other `live` invocations still teach the replacement.
480
+ if (retired.name === 'live') {
481
+ const adapterIdx = rawArgv.indexOf('--adapter');
482
+ const aliasIdx = rawArgv.indexOf('-a');
483
+ const adapterVal =
484
+ adapterIdx !== -1
485
+ ? rawArgv[adapterIdx + 1]
486
+ : aliasIdx !== -1
487
+ ? rawArgv[aliasIdx + 1]
488
+ : undefined;
489
+ if (adapterVal === 'mobile' || adapterVal === 'android') {
490
+ process.exit(await delegate(rawArgv));
491
+ return;
492
+ }
493
+ }
478
494
  console.error(retired.message);
479
495
  process.exit(2);
480
496
  });
@@ -11,7 +11,7 @@ import type {
11
11
  MetaMaskRecipeAdapter,
12
12
  } from './types.ts';
13
13
 
14
- export const runnerDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
14
+ export const runnerDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
15
15
  const pathDefaults = readPathDefaults();
16
16
  export const DEFAULT_RECIPE_RUNTIME_DIR = pathDefaults.recipeRuntimeDir;
17
17
  export const DEFAULT_RECIPE_HARNESS_ROOT = pathDefaults.recipeHarnessRoot;
@@ -26,7 +26,7 @@ export function recipeHarnessRoot() {
26
26
 
27
27
 
28
28
  function readPathDefaults(): { recipeRuntimeDir: string; recipeHarnessRoot: string } {
29
- const defaultsPath = path.join(runnerDir, 'orchestration/lib/path-defaults.json');
29
+ const defaultsPath = path.join(runnerDir, 'adapters/shared/path-defaults.json');
30
30
  const parsed = JSON.parse(fs.readFileSync(defaultsPath, 'utf8')) as Partial<{ recipeRuntimeDir: string; recipeHarnessRoot: string }>;
31
31
  return {
32
32
  recipeRuntimeDir: validateRelativeRecipePath('recipeRuntimeDir', parsed.recipeRuntimeDir || ''),
@@ -1,20 +1,29 @@
1
- #!/usr/bin/env node
2
1
  /**
3
- * progress.mjs — structured progress events for long recipe CLI operations.
2
+ * progress.ts — structured progress events for long recipe CLI operations.
4
3
  *
5
4
  * Human mode: short lines on stderr.
6
5
  * Machine mode: JSONL on stderr when --json or RECIPE_PROGRESS_JSON=1.
7
6
  */
7
+ import path from 'node:path';
8
8
  import { parseArgs } from 'node:util';
9
- import { color } from './cli-color.mjs';
9
+ import { color } from './cli-color.ts';
10
10
 
11
- function shouldEmitJson(explicit) {
11
+ function shouldEmitJson(explicit: boolean): boolean {
12
12
  if (explicit) return true;
13
13
  const env = process.env.RECIPE_PROGRESS_JSON;
14
14
  return env === '1' || env === 'true';
15
15
  }
16
16
 
17
- export function emitProgress(event, { json = false } = {}) {
17
+ interface ProgressEvent {
18
+ command?: string;
19
+ phase?: string;
20
+ message?: string;
21
+ status?: string;
22
+ elapsedMs?: number;
23
+ [key: string]: unknown;
24
+ }
25
+
26
+ export function emitProgress(event: ProgressEvent, { json = false }: { json?: boolean } = {}): void {
18
27
  const payload = {
19
28
  schemaVersion: 1,
20
29
  type: 'progress',
@@ -30,28 +39,28 @@ export function emitProgress(event, { json = false } = {}) {
30
39
  if (payload.phase === 'done') {
31
40
  const statusStyle = payload.status === 'pass' ? 'ok' : 'err';
32
41
  const statusWord = payload.status === 'pass' ? 'done' : 'failed';
33
- const verb = payload.verb ? color('cmd', `${payload.verb}`, { stream }) : '';
42
+ const cmd = payload.command ? color('cmd', `${payload.command}`, { stream }) : '';
34
43
  const elapsed = Number.isFinite(payload.elapsedMs)
35
- ? color('comment', ` (${Math.round(payload.elapsedMs / 1000)}s)`, { stream })
44
+ ? color('comment', ` (${Math.round((payload.elapsedMs as number) / 1000)}s)`, { stream })
36
45
  : '';
37
- process.stderr.write(`${prefix}: ${verb} ${color(statusStyle, statusWord, { stream })} ${payload.message || ''}${elapsed}\n`);
46
+ process.stderr.write(`${prefix}: ${cmd} ${color(statusStyle, statusWord, { stream })} ${payload.message || ''}${elapsed}\n`);
38
47
  return;
39
48
  }
40
- const verb = payload.verb ? `${color('cmd', payload.verb, { stream })}: ` : '';
49
+ const cmd = payload.command ? `${color('cmd', payload.command, { stream })}: ` : '';
41
50
  const phase = payload.phase ? `${color('accent', `[${payload.phase}]`, { stream })} ` : '';
42
51
  const elapsed = Number.isFinite(payload.elapsedMs)
43
- ? color('comment', ` (${Math.round(payload.elapsedMs / 1000)}s)`, { stream })
52
+ ? color('comment', ` (${Math.round((payload.elapsedMs as number) / 1000)}s)`, { stream })
44
53
  : '';
45
- process.stderr.write(`${prefix}: ${verb}${phase}${payload.message || ''}${elapsed}\n`);
54
+ process.stderr.write(`${prefix}: ${cmd}${phase}${payload.message || ''}${elapsed}\n`);
46
55
  }
47
56
 
48
- function usage() {
57
+ function usage(): void {
49
58
  console.error(`Usage:
50
- progress.mjs emit --verb <name> --phase <id> --message <text> [--elapsed-ms <n>] [--json]
51
- progress.mjs done --verb <name> --status pass|fail --message <text> [--elapsed-ms <n>] [--json]`);
59
+ progress emit --command <name> --phase <id> --message <text> [--elapsed-ms <n>] [--json]
60
+ progress done --command <name> --status pass|fail --message <text> [--elapsed-ms <n>] [--json]`);
52
61
  }
53
62
 
54
- async function main() {
63
+ async function main(): Promise<void> {
55
64
  const [command, ...rest] = process.argv.slice(2);
56
65
  if (!command || command === '--help' || command === '-h') {
57
66
  usage();
@@ -61,7 +70,7 @@ async function main() {
61
70
  const { values } = parseArgs({
62
71
  args: rest,
63
72
  options: {
64
- verb: { type: 'string' },
73
+ command: { type: 'string' },
65
74
  phase: { type: 'string' },
66
75
  message: { type: 'string' },
67
76
  status: { type: 'string' },
@@ -76,7 +85,7 @@ async function main() {
76
85
 
77
86
  if (command === 'emit') {
78
87
  emitProgress({
79
- verb: values.verb || 'recipe',
88
+ command: values.command || 'recipe',
80
89
  phase: values.phase || 'working',
81
90
  message: values.message || '',
82
91
  elapsedMs: Number.isFinite(elapsedMs) ? elapsedMs : undefined,
@@ -86,7 +95,7 @@ async function main() {
86
95
 
87
96
  if (command === 'done') {
88
97
  emitProgress({
89
- verb: values.verb || 'recipe',
98
+ command: values.command || 'recipe',
90
99
  phase: 'done',
91
100
  status: values.status || 'pass',
92
101
  message: values.message || '',
@@ -99,9 +108,10 @@ async function main() {
99
108
  process.exit(2);
100
109
  }
101
110
 
102
- if (import.meta.url === new URL(process.argv[1], 'file:').href) {
103
- main().catch((error) => {
111
+ const isMain = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(import.meta.filename ?? '');
112
+ if (isMain) {
113
+ main().catch((error: unknown) => {
104
114
  console.error(error instanceof Error ? error.message : String(error));
105
115
  process.exit(1);
106
116
  });
107
- }
117
+ }
@@ -1,60 +0,0 @@
1
- # Extension runtime lifecycle (internal notes)
2
-
3
- Internal implementation notes for the MetaMask Extension runtime lifecycle — the
4
- self-healing path that gets a checkout from "installed" to "a live, verified
5
- extension under CDP control". These are **not** a public command surface: the
6
- probes below are internal functions, composed for you by the daily-loop commands
7
- (`launch --verify`) and the readiness commands (`doctor --fix`, `verify`). The
8
- public surface is defined in [CLI-SPEC.md](CLI-SPEC.md); this file explains what
9
- happens underneath.
10
-
11
- The runner is the source of truth for Extension harness injection, readiness
12
- decisions, extension-id resolution, and live CDP health checks.
13
-
14
- ## Public entry points
15
-
16
- - **`install`** writes the Extension runtime overlay under the configured harness
17
- root. CI/agents run it explicitly; the daily-loop commands auto-ensure it.
18
- - **`verify`** confirms the overlay is present and healthy — a live CDP health
19
- probe when a runtime is reachable, no launch.
20
- - **`launch --verify`** installs the overlay if missing, launches/attaches the
21
- extension, and polls CDP until the runtime is ready.
22
- - **`doctor` / `doctor --fix`** report readiness and (with `--fix`) run the
23
- healing steps below WITHOUT starting the app.
24
-
25
- ## Internal lifecycle stages
26
-
27
- The healing path composes these internal functions in order. Each is idempotent
28
- and reports the cheapest next action, so the composed path converges rather than
29
- rebuilding from scratch.
30
-
31
- ### Extension-id resolution
32
-
33
- Resolves the deterministic unpacked extension id from
34
- `dist/chrome/manifest.json`, and can verify it against a live CDP target. Every
35
- later stage keys off this id, so it runs first.
36
-
37
- ### Readiness decision
38
-
39
- Inspects the checkout + live browser and returns the cheapest next action:
40
- `install`, `build`, `relaunch`, or `ready`, plus the concrete `actions[]` to
41
- execute. The healing path branches on this decision instead of always launching.
42
-
43
- ### Runtime launch
44
-
45
- Launches Chrome with the installed harness helper, seeds the wallet fixture, and
46
- runs live smoke verification. Two tiers: reuse the existing dist (quick relaunch),
47
- or the clean-build path — clear the webpack cache, start the harness-owned
48
- watcher, wait for a clean compile, then launch and verify.
49
-
50
- ### Convergence
51
-
52
- Converges the live browser to exactly one healthy `home.html` tab (closing
53
- strays, reopening if needed) and confirms it with the health probe. This is the
54
- step that makes "launch" deterministic across a messy pre-existing browser state.
55
-
56
- ### Health probe
57
-
58
- Read-only liveness probe for the running extension over CDP. Used both as the
59
- final gate of the convergence step and standalone by `verify` to answer "is the
60
- runtime healthy right now?" without mutating anything.
@@ -1,43 +0,0 @@
1
- # `mm-harness` — Command Reference
2
-
3
- > This page is a pointer. The full, authoritative command contract lives in
4
- > [CLI-SPEC.md](CLI-SPEC.md); the before→after overview is in
5
- > [MENTAL-MODEL.md](MENTAL-MODEL.md). This file exists only so links to
6
- > `harness-cli.md` keep resolving.
7
-
8
- `mm-harness` is the MetaMask recipe harness — the single front door for launching
9
- the app, proving behavior, and managing the per-checkout **runtime overlay**. It
10
- is the only bin: platform (`mobile | extension | core`) is auto-detected from the
11
- checkout, a positional target forces it (`mm-harness launch ios`), and depth lives
12
- in flags (`--json`, ports, build tiers), never in extra command names.
13
-
14
- Naming: **"harness" is the tool** (`mm-harness`) and the generic engine package
15
- (`@farmslot/recipe-harness`). The files the tool installs into a target checkout
16
- are the **runtime overlay** — never "the harness".
17
-
18
- ## Command surface (all shipped)
19
-
20
- ```
21
- DAILY LOOP launch · logs · debug · fixtures what a teammate runs many times a day
22
- DISCOVER actions · call · flows compose recipes from the vocabulary + flow library
23
- PROVE run · doctor run recipes, check readiness
24
- RUNTIME OVERLAY install · verify · cleanup install/verify/clean the per-checkout overlay
25
- ```
26
-
27
- Grammar: `mm-harness <command> [target] [flags]`. `--json` is the agent contract on
28
- every command; the bare command is the human happy path.
29
-
30
- - `run` validates first, then executes; `run --plan` validates and prints the plan
31
- without touching a device.
32
- - `call <action>` runs one action from the adapter vocabulary through the same
33
- engine path `run` uses.
34
- - `doctor` is read-only; `doctor --fix` runs the self-healing steps (overlay
35
- auto-ensure; never a fixture reseed) without launching the app and reports
36
- `fixed[]` / `failed[]`.
37
- - `flows` lists the recipe library (bare = `flows list`); `flows promote` keeps a
38
- proven flow.
39
- - `install` / `verify` / `cleanup` manage the runtime overlay; the DAILY LOOP
40
- commands auto-ensure it.
41
-
42
- For flags, exit-code taxonomy, `--json` shapes, and per-command detail, see
43
- [CLI-SPEC.md](CLI-SPEC.md).
@@ -1,13 +0,0 @@
1
- # Mobile historical compatibility overlays
2
-
3
- These patch files document temporary source overlays used by Recipe v1 historical
4
- injection evals when an older MetaMask Mobile checkout cannot boot under the
5
- current local Xcode/Hermes toolchain.
6
-
7
- They are intentionally kept in the external runner repository, not Farmslot root
8
- scripts, so a `/recipe-harness` skill can apply the same reversible overlay before
9
- a historical rebuild and record the overlay path in validation evidence.
10
-
11
- - `rn81-message-event-source.patch`: adds a read-only `MessageEvent.prototype.source`
12
- getter for React Native 0.81 historical checkouts whose bundled polyfill omits
13
- it while CDP websocket clients expect browser-compatible `MessageEvent` shape.
@@ -1,72 +0,0 @@
1
- #!/usr/bin/env node
2
- // doctor.mjs — orchestration feature-surface doctor
3
- //
4
- // Purpose:
5
- // Verifies orchestration/manifest.json against reality: every entry point
6
- // exists; every script entry answers --help with exit 0; and every script
7
- // under orchestration/{mobile,extension,core} is listed (no doc drift).
8
- //
9
- // Inputs (flags): --json (machine-readable report)
10
- // Outputs: per-feature lines (or JSON report) on stdout.
11
- // Exit 0 — surface healthy; 1 — missing entry, failing --help, or
12
- // unlisted script; 2 — bad args.
13
- // Never touches: anything (read-only + --help subprocesses).
14
- import fs from 'node:fs';
15
- import path from 'node:path';
16
- import { spawnSync } from 'node:child_process';
17
- import { fileURLToPath } from 'node:url';
18
-
19
- let json = false;
20
- for (const arg of process.argv.slice(2)) {
21
- if (arg === '--json') json = true;
22
- else if (arg === '-h' || arg === '--help') {
23
- console.log('Usage: doctor.mjs [--json]');
24
- process.exit(0);
25
- } else {
26
- console.error(`Unknown arg: ${arg}`);
27
- process.exit(2);
28
- }
29
- }
30
-
31
- const orchestrationDir = path.dirname(fileURLToPath(import.meta.url));
32
- const runnerRoot = path.resolve(orchestrationDir, '..');
33
- const manifest = JSON.parse(fs.readFileSync(path.join(orchestrationDir, 'manifest.json'), 'utf8'));
34
-
35
- const results = [];
36
- for (const feature of manifest.features) {
37
- const entryAbs = path.join(runnerRoot, feature.entry);
38
- const checks = { exists: fs.existsSync(entryAbs), help: null };
39
- if (checks.exists && (feature.kind === 'bash' || feature.kind === 'node')) {
40
- const runner = feature.kind === 'bash' ? 'bash' : process.execPath;
41
- const run = spawnSync(runner, [entryAbs, '--help'], { encoding: 'utf8', timeout: 30000 });
42
- checks.help = run.status === 0;
43
- }
44
- const ok = checks.exists && (checks.help === null || checks.help === true);
45
- results.push({ id: feature.id, entry: feature.entry, kind: feature.kind, ...checks, ok });
46
- }
47
-
48
- // drift guard: every script in the orchestration trees must be listed
49
- const listed = new Set(manifest.features.map((feature) => feature.entry));
50
- const unlisted = [];
51
- for (const platform of ['mobile', 'extension', 'core', 'lib']) {
52
- const dir = path.join(orchestrationDir, platform);
53
- if (!fs.existsSync(dir)) continue;
54
- for (const entry of fs.readdirSync(dir)) {
55
- const rel = `orchestration/${platform}/${entry}`;
56
- if (!/\.(?:sh|mjs|cjs|json)$/u.test(entry)) continue;
57
- if (!listed.has(rel)) unlisted.push(rel);
58
- }
59
- }
60
-
61
- const failed = results.filter((result) => !result.ok);
62
- const status = failed.length === 0 && unlisted.length === 0 ? 'pass' : 'fail';
63
- if (json) {
64
- console.log(JSON.stringify({ status, features: results, unlisted }, null, 2));
65
- } else {
66
- for (const result of results) {
67
- console.log(`${result.ok ? 'ok ' : 'FAIL'} ${result.id} (${result.entry})${result.help === false ? ' [--help failed]' : ''}${result.exists ? '' : ' [missing]'}`);
68
- }
69
- for (const rel of unlisted) console.log(`FAIL unlisted script not in manifest: ${rel}`);
70
- console.log(`orchestration surface: ${status} (${results.length - failed.length}/${results.length} features, ${unlisted.length} unlisted)`);
71
- }
72
- process.exit(status === 'pass' ? 0 : 1);