@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
@@ -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
+ }
@@ -1,15 +1,26 @@
1
- #!/usr/bin/env node
2
1
  /**
3
- * cli-commands.mjs — single source of truth for recipe CLI tab completion.
2
+ * cli-commands.ts — single source of truth for recipe CLI tab completion.
4
3
  */
5
4
  import fs from 'node:fs';
6
5
  import path from 'node:path';
7
6
  import { fileURLToPath } from 'node:url';
8
7
 
9
- /** @typedef {{ name: string, aliases?: string[], desc?: string, flags?: string[], args?: string[] }} CommandSpec */
8
+ interface CommandSpec {
9
+ name: string;
10
+ aliases?: string[];
11
+ desc?: string;
12
+ flags?: string[];
13
+ args?: string[];
14
+ }
15
+
16
+ interface CommandSpecMap {
17
+ shared: CommandSpec[];
18
+ extension: CommandSpec[];
19
+ mobile: CommandSpec[];
20
+ shim: CommandSpec[];
21
+ }
10
22
 
11
- /** @type {{ shared: CommandSpec[], extension: CommandSpec[], mobile: CommandSpec[], shim: CommandSpec[] }} */
12
- export const SPEC = {
23
+ export const SPEC: CommandSpecMap = {
13
24
  shim: [
14
25
  { name: 'sync', desc: 'Copy fixture, write slot context, install harness', flags: ['--dry-run', '--no-install'] },
15
26
  { name: 'help', aliases: ['-h', '--help'], desc: 'Show usage' },
@@ -34,9 +45,7 @@ export const SPEC = {
34
45
  { name: 'ready', aliases: ['ensure-ready'], desc: 'Ensure one healthy extension home tab' },
35
46
  { name: 'watch', aliases: ['start-watch', 'watcher'], desc: 'Start/reuse webpack watcher', flags: ['--full', '-f'] },
36
47
  { name: 'stop', aliases: ['stop-watch', 'stop-watcher'], desc: 'Stop this checkout watcher' },
37
- { name: 'rebuild', aliases: ['reset', 'reopen', 'reload', 'browser', 'launch', 'full-launch', 'runtime-launch'], desc: 'Clean webpack + browser launch', flags: ['--json', '--dry-run', '--full'] },
38
- { name: 'refresh', aliases: ['build'], desc: 'Fast refresh with active watcher' },
39
- { name: 'refresh:once', aliases: ['refresh-once', 'build:once', 'build-once'], desc: 'One-shot refresh-build helper' },
48
+ { name: 'rebuild', aliases: ['reset', 'reopen', 'reload', 'browser', 'launch', 'full-launch', 'runtime-launch', 'build', 'build:once', 'build-once', 'refresh-once'], desc: 'Clean webpack + browser launch', flags: ['--json', '--dry-run', '--full'] },
40
49
  { name: 'sidepanel', desc: 'Side panel helper', args: ['cycle', 'open', 'close', 'toggle', 'status'] },
41
50
  { name: 'prepare', flags: ['--target', '--cdp-port', '--runtime-dir', '--validate'] },
42
51
  { name: 'debug', args: ['page', 'worker'], flags: ['--json', '--no-open'] },
@@ -46,7 +55,6 @@ export const SPEC = {
46
55
  { name: 'android', desc: 'Start Metro + launch Android dev client' },
47
56
  { name: 'setup:ios', desc: 'iOS launch + wallet fixture setup' },
48
57
  { name: 'setup:android', desc: 'Android launch + wallet fixture setup' },
49
- { name: 'refresh', aliases: ['reload', 'relaunch'], desc: 'Refresh Metro + relaunch dev client' },
50
58
  { name: 'stop', desc: 'Stop Metro for this slot' },
51
59
  { name: 'app-status', aliases: ['bridge-status'], desc: 'Bridge/runtime status' },
52
60
  { name: 'route', aliases: ['get-route'], desc: 'Current navigation route' },
@@ -65,9 +73,9 @@ export const SPEC = {
65
73
 
66
74
  const GLOBAL_FLAGS = ['--json', '--dry-run', '--full', '-f', '--help', '-h', '--no-open', '--no-color'];
67
75
 
68
- function mergeCommands(adapter) {
69
- const byName = new Map();
70
- const add = (cmd) => {
76
+ function mergeCommands(adapter: string): CommandSpec[] {
77
+ const byName = new Map<string, CommandSpec & { aliases: string[]; flags: string[]; args: string[] }>();
78
+ const add = (cmd: CommandSpec) => {
71
79
  const existing = byName.get(cmd.name);
72
80
  if (!existing) {
73
81
  byName.set(cmd.name, {
@@ -84,12 +92,12 @@ function mergeCommands(adapter) {
84
92
  if (!existing.desc && cmd.desc) existing.desc = cmd.desc;
85
93
  };
86
94
  for (const cmd of SPEC.shared) add(cmd);
87
- for (const cmd of SPEC[adapter] || []) add(cmd);
95
+ for (const cmd of SPEC[adapter as keyof CommandSpecMap] || []) add(cmd);
88
96
  return [...byName.values()];
89
97
  }
90
98
 
91
- export function commandTokens(adapter, { includeAliases = true } = {}) {
92
- const tokens = new Set();
99
+ export function commandTokens(adapter: string, { includeAliases = true }: { includeAliases?: boolean } = {}): string[] {
100
+ const tokens = new Set<string>();
93
101
  for (const cmd of mergeCommands(adapter)) {
94
102
  tokens.add(cmd.name);
95
103
  if (includeAliases) for (const alias of cmd.aliases || []) tokens.add(alias);
@@ -97,8 +105,8 @@ export function commandTokens(adapter, { includeAliases = true } = {}) {
97
105
  return [...tokens].sort();
98
106
  }
99
107
 
100
- export function shimCommandTokens() {
101
- const tokens = new Set();
108
+ export function shimCommandTokens(): string[] {
109
+ const tokens = new Set<string>();
102
110
  for (const cmd of SPEC.shim) {
103
111
  tokens.add(cmd.name);
104
112
  for (const alias of cmd.aliases || []) tokens.add(alias);
@@ -106,7 +114,7 @@ export function shimCommandTokens() {
106
114
  return [...tokens].sort();
107
115
  }
108
116
 
109
- export function lookupCommand(adapter, token) {
117
+ export function lookupCommand(adapter: string, token: string): CommandSpec | null {
110
118
  for (const cmd of mergeCommands(adapter)) {
111
119
  if (cmd.name === token) return cmd;
112
120
  if ((cmd.aliases || []).includes(token)) return cmd;
@@ -118,28 +126,27 @@ export function lookupCommand(adapter, token) {
118
126
  return null;
119
127
  }
120
128
 
121
- export function flagsForPosition(adapter, commandToken) {
129
+ export function flagsForPosition(adapter: string, commandToken: string): string[] {
122
130
  const cmd = lookupCommand(adapter, commandToken);
123
131
  const flags = new Set(GLOBAL_FLAGS);
124
132
  if (cmd) for (const flag of cmd.flags || []) flags.add(flag);
125
133
  return [...flags].sort();
126
134
  }
127
135
 
128
- export function argsForPosition(adapter, commandToken, argIndex) {
136
+ export function argsForPosition(adapter: string, commandToken: string, argIndex: number): string[] {
129
137
  const cmd = lookupCommand(adapter, commandToken);
130
138
  if (!cmd?.args?.length) return [];
131
139
  const value = cmd.args[Math.max(0, argIndex)];
132
140
  return value ? [value] : cmd.args;
133
141
  }
134
142
 
135
- const RUN_RECIPE_DIRS = [
143
+ const RUN_RECIPE_DIRS: ((adapter: string, root: string) => string)[] = [
136
144
  (adapter, root) => path.join(root, 'temp/recipe/harness', adapter, 'runner/recipes'),
137
145
  (adapter, root) => path.join(root, 'temp/agentic/recipe-harness', adapter, 'runner/recipes'),
138
146
  ];
139
147
 
140
- /** Harness runner recipes only used for `recipe run <tab>`. */
141
- export function listRunRecipes(adapter, root, cwd = process.cwd()) {
142
- const byBase = new Map();
148
+ export function listRunRecipes(adapter: string, root: string, cwd = process.cwd()): string[] {
149
+ const byBase = new Map<string, string>();
143
150
  for (const resolveDir of RUN_RECIPE_DIRS) {
144
151
  const dir = resolveDir(adapter, root);
145
152
  if (!fs.existsSync(dir)) continue;
@@ -153,20 +160,20 @@ export function listRunRecipes(adapter, root, cwd = process.cwd()) {
153
160
  .sort();
154
161
  }
155
162
 
156
- function usage() {
163
+ function usage(): void {
157
164
  process.stderr.write(`Usage:
158
- cli-commands.mjs dump --adapter extension|mobile|shim [--aliases]
159
- cli-commands.mjs describe --adapter extension|mobile|shim
160
- cli-commands.mjs flags --adapter <adapter> --command <name>
161
- cli-commands.mjs args --adapter <adapter> --command <name> [--index <n>]
162
- cli-commands.mjs run-recipes --adapter extension|mobile --root <repo> [--cwd <dir>]
163
- cli-commands.mjs global-flags
165
+ cli-commands dump --adapter extension|mobile|shim [--aliases]
166
+ cli-commands describe --adapter extension|mobile|shim
167
+ cli-commands flags --adapter <adapter> --command <name>
168
+ cli-commands args --adapter <adapter> --command <name> [--index <n>]
169
+ cli-commands run-recipes --adapter extension|mobile --root <repo> [--cwd <dir>]
170
+ cli-commands global-flags
164
171
  `);
165
172
  }
166
173
 
167
- async function main() {
168
- const { parseArgs } = await import('node:util');
169
- const { values, positionals } = parseArgs({
174
+ async function main(): Promise<void> {
175
+ const { parseArgs: parseArgsUtil } = await import('node:util');
176
+ const { values, positionals } = parseArgsUtil({
170
177
  allowPositionals: true,
171
178
  args: process.argv.slice(2),
172
179
  options: {
@@ -197,12 +204,12 @@ async function main() {
197
204
  }
198
205
 
199
206
  if (command === 'dump') {
200
- process.stdout.write(`${commandTokens(adapter, { includeAliases: values.aliases }).join('\n')}\n`);
207
+ process.stdout.write(`${commandTokens(adapter as string, { includeAliases: values.aliases }).join('\n')}\n`);
201
208
  return;
202
209
  }
203
210
 
204
211
  if (command === 'describe') {
205
- const rows = adapter === 'shim' ? SPEC.shim : mergeCommands(adapter);
212
+ const rows = adapter === 'shim' ? SPEC.shim : mergeCommands(adapter as string);
206
213
  for (const cmd of rows) {
207
214
  const aliases = (cmd.aliases || []).join('|');
208
215
  const label = aliases ? `${cmd.name}|${aliases}` : cmd.name;
@@ -212,20 +219,20 @@ async function main() {
212
219
  }
213
220
 
214
221
  if (command === 'flags') {
215
- process.stdout.write(`${flagsForPosition(adapter, values.command || '').join('\n')}\n`);
222
+ process.stdout.write(`${flagsForPosition(adapter as string, values.command || '').join('\n')}\n`);
216
223
  return;
217
224
  }
218
225
 
219
226
  if (command === 'args') {
220
- const index = Number.parseInt(values.index, 10) || 0;
221
- process.stdout.write(`${argsForPosition(adapter, values.command || '', index).join('\n')}\n`);
227
+ const index = Number.parseInt(values.index as string, 10) || 0;
228
+ process.stdout.write(`${argsForPosition(adapter as string, values.command || '', index).join('\n')}\n`);
222
229
  return;
223
230
  }
224
231
 
225
232
  if (command === 'run-recipes') {
226
233
  const root = values.root ? path.resolve(values.root) : process.cwd();
227
234
  const cwd = values.cwd ? path.resolve(values.cwd) : process.cwd();
228
- process.stdout.write(`${listRunRecipes(adapter, root, cwd).join('\n')}\n`);
235
+ process.stdout.write(`${listRunRecipes(adapter as string, root, cwd).join('\n')}\n`);
229
236
  return;
230
237
  }
231
238
 
@@ -233,11 +240,11 @@ async function main() {
233
240
  process.exit(2);
234
241
  }
235
242
 
236
- const entry = process.argv[1] ? path.resolve(process.argv[1]) : '';
237
- const self = fileURLToPath(import.meta.url);
238
- if (entry === self) {
239
- main().catch((error) => {
243
+ const _filename = fileURLToPath(import.meta.url);
244
+ const isMain = process.argv[1] && path.resolve(process.argv[1]) === _filename;
245
+ if (isMain) {
246
+ main().catch((error: unknown) => {
240
247
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
241
248
  process.exit(1);
242
249
  });
243
- }
250
+ }
@@ -1,13 +1,12 @@
1
- #!/usr/bin/env node
2
1
  /**
3
- * cli-version.mjs — detect stale installed harness scripts vs the active runner.
2
+ * cli-version.ts — detect stale installed harness scripts vs the active runner.
4
3
  */
5
4
  import fs from 'node:fs';
6
5
  import path from 'node:path';
7
6
  import { spawnSync } from 'node:child_process';
8
7
  import { parseArgs } from 'node:util';
9
8
 
10
- function readJson(file) {
9
+ function readJson(file: string): unknown {
11
10
  try {
12
11
  return JSON.parse(fs.readFileSync(file, 'utf8'));
13
12
  } catch {
@@ -15,31 +14,49 @@ function readJson(file) {
15
14
  }
16
15
  }
17
16
 
18
- function readPackageVersion(dir) {
17
+ function readPackageVersion(dir: string): string {
19
18
  try {
20
- const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
19
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')) as { version?: string };
21
20
  return pkg.version || 'unknown';
22
21
  } catch {
23
22
  return 'unknown';
24
23
  }
25
24
  }
26
25
 
27
- function gitHead(dir) {
26
+ function gitHead(dir: string): string {
28
27
  const result = spawnSync('git', ['-C', dir, 'rev-parse', 'HEAD'], { encoding: 'utf8' });
29
28
  return result.status === 0 ? result.stdout.trim() : 'unknown';
30
29
  }
31
30
 
32
- export function checkHarness({ runnerRoot, manifestPath, installedShaPath }) {
31
+ interface HarnessCheckInput {
32
+ runnerRoot: string;
33
+ manifestPath: string;
34
+ installedShaPath?: string;
35
+ }
36
+
37
+ interface HarnessCheckResult {
38
+ stale: boolean;
39
+ reasons: string[];
40
+ installedRevision?: string | null;
41
+ currentRevision?: string;
42
+ installedVersion?: string | null;
43
+ currentVersion?: string;
44
+ manifest?: string;
45
+ }
46
+
47
+ export function checkHarness({ runnerRoot, manifestPath }: HarnessCheckInput): HarnessCheckResult {
33
48
  const manifest = readJson(manifestPath);
34
- const reasons = [];
49
+ const reasons: string[] = [];
35
50
  if (!manifest) {
36
51
  return { stale: false, reasons: ['no harness manifest'] };
37
52
  }
38
53
 
39
54
  const currentRevision = gitHead(runnerRoot);
40
55
  const currentVersion = readPackageVersion(runnerRoot);
41
- const installedRevision = manifest.source?.runnerRevision || null;
42
- const installedVersion = manifest.runnerPackageVersion || null;
56
+ const manifestRecord = manifest as Record<string, unknown>;
57
+ const source = manifestRecord.source as Record<string, unknown> | undefined;
58
+ const installedRevision = (source?.runnerRevision as string) || null;
59
+ const installedVersion = (manifestRecord.runnerPackageVersion as string) || null;
43
60
 
44
61
  if (installedRevision && installedRevision !== 'unknown' && installedRevision !== currentRevision) {
45
62
  reasons.push(`runner revision ${installedRevision.slice(0, 8)} != ${currentRevision.slice(0, 8)}`);
@@ -58,12 +75,12 @@ export function checkHarness({ runnerRoot, manifestPath, installedShaPath }) {
58
75
  };
59
76
  }
60
77
 
61
- function usage() {
78
+ function usage(): void {
62
79
  console.error(`Usage:
63
- cli-version.mjs check --runner-root <dir> --manifest <path> [--installed-sha <path>] [--json]`);
80
+ cli-version check --runner-root <dir> --manifest <path> [--installed-sha <path>] [--json]`);
64
81
  }
65
82
 
66
- async function main() {
83
+ async function main(): Promise<void> {
67
84
  const [command, ...rest] = process.argv.slice(2);
68
85
  const { values } = parseArgs({
69
86
  args: rest,
@@ -93,7 +110,7 @@ async function main() {
93
110
  process.exit(2);
94
111
  }
95
112
 
96
- const payload = {
113
+ const payload: HarnessCheckResult & { manifest: string } = {
97
114
  ...checkHarness({
98
115
  runnerRoot,
99
116
  manifestPath,
@@ -115,9 +132,10 @@ async function main() {
115
132
  process.stdout.write('harness up to date\n');
116
133
  }
117
134
 
118
- if (import.meta.url === new URL(process.argv[1], 'file:').href) {
119
- main().catch((error) => {
135
+ const isMain = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(import.meta.filename ?? '');
136
+ if (isMain) {
137
+ main().catch((error: unknown) => {
120
138
  console.error(error instanceof Error ? error.message : String(error));
121
139
  process.exit(1);
122
140
  });
123
- }
141
+ }
@@ -23,10 +23,10 @@ import {
23
23
  checkHealBounds,
24
24
  } from './heal-bounds.ts';
25
25
  import type { HealPolicy, HealState, HealBoundViolation } from './heal-bounds.ts';
26
- import { ensureExtensionReady } from '../../orchestration/extension/ensure-ready.ts';
27
- import { resolveExtensionId } from '../../orchestration/extension/extension-id.ts';
28
- import { decideExtensionReadiness } from '../../orchestration/extension/runtime-decision.ts';
29
- import { decideMobileReadiness } from '../../orchestration/mobile/runtime-decision.ts';
26
+ import { ensureExtensionReady } from './adapters/extension/ensure-ready.ts';
27
+ import { resolveExtensionId } from './adapters/extension/extension-id.ts';
28
+ import { decideExtensionReadiness } from './adapters/extension/runtime-decision.ts';
29
+ import { decideMobileReadiness } from './adapters/mobile/runtime-decision.ts';
30
30
  // NOTE: extension-runtime.ts loads the recipe harness at module scope, so it
31
31
  // is imported LAZILY (dynamic import) only inside the handlers that drive a live
32
32
  // runtime. Static-import it here and every command — manifest, doctor,
@@ -372,7 +372,7 @@ async function prepareRuntimeIfNeeded(
372
372
  runtimeOptions: RuntimeOptions,
373
373
  ): Promise<void> {
374
374
  if (adapter !== 'extension' || runtimeOptions.skipExtensionRuntimePrepare === true) return;
375
- const { prepareExtensionRuntime } = await import('../../orchestration/extension/runtime.ts');
375
+ const { prepareExtensionRuntime } = await import('./adapters/extension/runtime.ts');
376
376
  await prepareExtensionRuntime({
377
377
  projectRoot,
378
378
  cdpPort: runtimeOptions.cdpPort,
@@ -390,7 +390,7 @@ function restoreEnv(key: string, value: string | undefined) {
390
390
  async function runSelfTest(options: CliOptions) {
391
391
  const root = optionString(options, 'artifactsDir')
392
392
  ? path.resolve(requiredOption(options, 'artifactsDir', 'self-test artifacts dir missing.'))
393
- : await mkdtemp(path.join(os.tmpdir(), 'metamask-recipe-runner-'));
393
+ : await mkdtemp(path.join(os.tmpdir(), 'mm-harness-runner-'));
394
394
  const previousAutoHud = process.env.METAMASK_RECIPE_AUTO_HUD;
395
395
  const runs = [];
396
396
  try {
@@ -596,7 +596,7 @@ async function handleRuntimeHealth({ options }: ParsedArgs): Promise<number> {
596
596
  optionString(options, 'cdpPort') ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT,
597
597
  'runtime-health requires --cdp-port <port>.',
598
598
  );
599
- const { checkExtensionRuntimeHealth, formatHealthFailure } = await import('../../orchestration/extension/runtime.ts');
599
+ const { checkExtensionRuntimeHealth, formatHealthFailure } = await import('./adapters/extension/runtime.ts');
600
600
  const report = await checkExtensionRuntimeHealth(target, cdpPort);
601
601
  if (optionFlag(options, 'json')) console.log(JSON.stringify(report, null, 2));
602
602
  else if (report.status === 'PASS') console.log(`PASS extension runtime cdp=${cdpPort} target=${report.targetUrl}`);
@@ -645,7 +645,7 @@ async function handleRuntimeLaunch({ options }: ParsedArgs): Promise<number> {
645
645
  cdpPort,
646
646
  artifactsDir,
647
647
  reason: 'harness_live_script_missing',
648
- fix: `Run metamask-recipe extension prepare --target ${shellQuote(target)}, then rerun: ${runtimeLaunchCommand(target, cdpPort, chromeUserDataDir)}`,
648
+ fix: `Run mm-harness install --adapter extension --target ${shellQuote(target)}, then rerun: ${runtimeLaunchCommand(target, cdpPort, chromeUserDataDir)}`,
649
649
  command,
650
650
  });
651
651
  printRuntimeLaunchReport(report, optionFlag(options, 'json'));
@@ -710,7 +710,7 @@ async function handleRuntimeLaunch({ options }: ParsedArgs): Promise<number> {
710
710
  }
711
711
 
712
712
  function runtimeLaunchCommand(target: string, cdpPort: number, chromeUserDataDir?: string, startWatch = false): string {
713
- const base = `metamask-recipe runtime-launch --adapter extension --target ${shellQuote(target)} --cdp-port ${cdpPort}`;
713
+ const base = `mm-harness runtime-launch --adapter extension --target ${shellQuote(target)} --cdp-port ${cdpPort}`;
714
714
  const withMode = startWatch ? `${base} --start-watch` : base;
715
715
  return chromeUserDataDir ? `${withMode} --chrome-user-data-dir ${shellQuote(chromeUserDataDir)}` : withMode;
716
716
  }
@@ -828,7 +828,7 @@ async function handleEnsureReady({ options }: ParsedArgs): Promise<number> {
828
828
  // 5 validation failure (run, run --plan, call — adapter-aware validation errors).
829
829
  const EXIT = { ok: 0, runtime: 1, usage: 2, infra: 3, bounded: 4, validation: 5 } as const;
830
830
 
831
- // Typed exit-code error: carries exitCode so both the global catch (metamask-recipe bin)
831
+ // Typed exit-code error: carries exitCode so both the global catch (mm-harness bin)
832
832
  // and the mm-harness delegate() wrapper classify the error correctly rather than always
833
833
  // returning 1. Usage errors (bad args / unsupported flags) carry EXIT.usage (2);
834
834
  // validation errors carry EXIT.validation (5). Never throw a plain new Error() for
@@ -1184,7 +1184,7 @@ async function handleCall(argv: string[]): Promise<number> {
1184
1184
  heal,
1185
1185
  state,
1186
1186
  );
1187
- if (violation !== null) return emitHealViolation(json, 'call', result, violation, state);
1187
+ if (violation !== null) return emitHealViolation(json, 'call', result, violation, state, adapter);
1188
1188
 
1189
1189
  if (json) {
1190
1190
  console.log(
@@ -1286,7 +1286,7 @@ function synthesizeOneNodeRecipe(action: string, args: Record<string, string>):
1286
1286
 
1287
1287
  // Reusable one-node engine path (the same machinery `call` uses), exposed so
1288
1288
  // `fixtures set` can apply the canonical wallet fixture through the real engine on
1289
- // the extension (which has no standalone porcelain set arm).
1289
+ // the extension (which has no standalone shell set arm).
1290
1290
  async function runOneNode(
1291
1291
  adapter: MetaMaskRecipeAdapter,
1292
1292
  action: string,
@@ -1520,7 +1520,7 @@ async function handleRun({ positional, options }: ParsedArgs): Promise<number> {
1520
1520
  heal,
1521
1521
  state,
1522
1522
  );
1523
- if (violation !== null) return emitHealViolation(json, 'run', result, violation, state);
1523
+ if (violation !== null) return emitHealViolation(json, 'run', result, violation, state, adapter);
1524
1524
  const exitCode = result.status === 'pass' ? EXIT.ok : EXIT.runtime;
1525
1525
  if (json) {
1526
1526
  // mm-harness envelope (matches launch/verify/doctor): the engine's result is
@@ -1619,7 +1619,14 @@ function emitHealViolation(
1619
1619
  result: RecipeRunResult,
1620
1620
  violation: HealBoundViolation,
1621
1621
  state: HealState,
1622
+ adapter?: MetaMaskRecipeAdapter,
1622
1623
  ): number {
1624
+ // core is headless: `fixtures set` is blocked. Teach the env-var / recipe-node
1625
+ // path instead so the user has a reachable next step.
1626
+ const userAction =
1627
+ adapter === 'core' && violation.code === 'WALLET_STATE_REQUIRED'
1628
+ ? 'set MM_TEST_ACCOUNT_ADDRESS=<0x…> in env, or add "account": "<0x…>" to the node block in the recipe'
1629
+ : violation.userAction;
1623
1630
  if (json) {
1624
1631
  console.log(
1625
1632
  JSON.stringify(
@@ -1639,7 +1646,7 @@ function emitHealViolation(
1639
1646
  code: violation.code,
1640
1647
  message: violation.message,
1641
1648
  retryable: false,
1642
- userAction: violation.userAction ?? null,
1649
+ userAction: userAction ?? null,
1643
1650
  originalError: violation.originalError ?? null,
1644
1651
  },
1645
1652
  },
@@ -1651,7 +1658,7 @@ function emitHealViolation(
1651
1658
  console.error(
1652
1659
  `✗ mm-harness ${command}: ${violation.message}` +
1653
1660
  (violation.originalError ? `\n --- original failure ---\n${violation.originalError}` : '') +
1654
- (violation.userAction ? `\n Next: ${violation.userAction}` : ''),
1661
+ (userAction ? `\n Next: ${userAction}` : ''),
1655
1662
  );
1656
1663
  }
1657
1664
  return violation.exitCode;
@@ -1792,7 +1799,7 @@ export async function main(argv: string[]): Promise<number> {
1792
1799
  }
1793
1800
  return code;
1794
1801
  }
1795
- // launch/logs/debug/fixtures compose the porcelain paths and own policy,
1802
+ // launch/logs/debug/fixtures compose adapter scripts directly and own policy,
1796
1803
  // healing, teaching, and the --json contract.
1797
1804
  if (command === 'launch') return handleLaunch(argv.slice(1));
1798
1805
  if (command === 'logs') return handleLogs(argv.slice(1));
@@ -1803,8 +1810,8 @@ export async function main(argv: string[]): Promise<number> {
1803
1810
  return handler(parseArgs(argv.slice(1), command));
1804
1811
  }
1805
1812
 
1806
- // Auto-run only when this file IS the process entry (orchestration/porcelain/metamask-recipe
1807
- // execs it directly). The commander front (`runner/src/mm-harness-cli.ts`) sets this flag
1813
+ // Auto-run only when this file IS the process entry (bin/mm-harness execs it
1814
+ // directly). The commander front (`src/mm-harness-cli.ts`) sets this flag
1808
1815
  // before importing so it can reuse `main` without triggering a second dispatch.
1809
1816
  if (!(globalThis as Record<string, unknown>).__MM_HARNESS_WRAPPER__) {
1810
1817
  try {
@@ -0,0 +1,61 @@
1
+ // debug — open the debug console by composing open-debug.mjs directly:
2
+ // extension `debug` (--worker → service-worker target); mobile `debug`
3
+ // (--dev-menu → developer menu). Cross-platform flag misuse teaches (exit 2).
4
+
5
+ import path from 'node:path';
6
+
7
+ import { runnerDir } from '../paths.ts';
8
+ import { EXIT, flag, parseFlags, resolveAdapter, spawnScript, targetOf, usageOut } from './shared.ts';
9
+
10
+ const DEBUG_BOOLEANS = new Set(['worker', 'devMenu', 'json']);
11
+
12
+ export async function handleDebug(argv: string[]): Promise<number> {
13
+ const { options } = parseFlags(argv, DEBUG_BOOLEANS);
14
+ const json = flag(options, 'json');
15
+ const target = targetOf(options);
16
+ const adapter = resolveAdapter(options, target);
17
+
18
+ if (!adapter) return usageOut(json, 'debug', `could not detect the MetaMask repo type for ${target}`);
19
+ if (adapter === 'core') return usageOut(json, 'debug', 'core is headless; there is no debug console.');
20
+
21
+ const worker = flag(options, 'worker');
22
+ const devMenu = flag(options, 'devMenu');
23
+ if (adapter === 'mobile' && worker) {
24
+ return usageOut(
25
+ json,
26
+ 'debug',
27
+ '--worker is extension-only (service-worker DevTools). Use --dev-menu on mobile.\n Next: mm-harness debug --dev-menu',
28
+ );
29
+ }
30
+ if (adapter === 'extension' && devMenu) {
31
+ return usageOut(
32
+ json,
33
+ 'debug',
34
+ '--dev-menu is mobile-only (RN developer menu). Use --worker on the extension.\n Next: mm-harness debug --worker',
35
+ );
36
+ }
37
+
38
+ const openDebug = path.join(runnerDir, 'adapters/shared/open-debug.mjs');
39
+ const debugArgs: string[] = ['--adapter', adapter];
40
+
41
+ if (adapter === 'extension') {
42
+ if (process.env.CDP_PORT) debugArgs.push('--cdp-port', process.env.CDP_PORT);
43
+ debugArgs.push('--target', worker ? 'worker' : 'page');
44
+ } else {
45
+ if (process.env.WATCHER_PORT) debugArgs.push('--port', process.env.WATCHER_PORT);
46
+ debugArgs.push('--action', devMenu ? 'dev-menu' : 'debug');
47
+ }
48
+ if (json) debugArgs.push('--json');
49
+
50
+ const result = spawnScript(process.execPath, [openDebug, ...debugArgs], target, json);
51
+ if (json) {
52
+ console.log(
53
+ JSON.stringify(
54
+ { schemaVersion: 1, command: 'debug', adapter, mode: worker ? 'worker' : devMenu ? 'dev-menu' : 'default', exitCode: result.status === 0 ? EXIT.ok : EXIT.runtime },
55
+ null,
56
+ 2,
57
+ ),
58
+ );
59
+ }
60
+ return result.status === 0 ? EXIT.ok : EXIT.runtime;
61
+ }