@deeeed/metamask-harness 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +47 -17
  3. package/{orchestration → adapters}/core/cleanup.sh +2 -2
  4. package/{orchestration → adapters}/core/inject.sh +8 -8
  5. package/{orchestration → adapters}/extension/cleanup.mjs +1 -1
  6. package/{orchestration → adapters}/extension/console-tail.mjs +3 -0
  7. package/{orchestration → adapters}/extension/ensure-browser.sh +4 -4
  8. package/{orchestration → adapters}/extension/inject.mjs +27 -31
  9. package/{orchestration → adapters}/extension/launch.sh +4 -4
  10. package/{orchestration → adapters}/extension/live.sh +5 -5
  11. package/{orchestration → adapters}/extension/readiness.mjs +12 -2
  12. package/{orchestration → adapters}/extension/refresh-build.sh +1 -1
  13. package/{orchestration → adapters}/extension/seed-fixture.sh +1 -1
  14. package/{orchestration → adapters}/extension/start-watch.sh +7 -7
  15. package/{runner → adapters}/extension/verify.sh +11 -11
  16. package/{orchestration → adapters}/extension/wallet-fixture-state.cjs +2 -2
  17. package/{orchestration → adapters}/manifest.json +96 -96
  18. package/{library/actions → adapters}/mobile/bridge-runtime/cdp-bridge.cjs +5 -5
  19. package/{orchestration → adapters}/mobile/cleanup.sh +3 -3
  20. package/{orchestration → adapters}/mobile/inject.sh +22 -24
  21. package/adapters/mobile/open-device.sh +260 -0
  22. package/{orchestration/compat-overlays → adapters/mobile/overlay/compat}/README.md +13 -4
  23. package/adapters/mobile/prewarm-bundle.sh +95 -0
  24. package/adapters/mobile/start-metro.sh +228 -0
  25. package/{runner → adapters}/mobile/verify.sh +6 -6
  26. package/adapters/mobile/wait-for-bridge.sh +108 -0
  27. package/adapters/mobile/yarn-setup.sh +20 -0
  28. package/{orchestration/lib → adapters/shared}/cli-ux.sh +5 -9
  29. package/{orchestration/lib → adapters/shared}/log-tui.mjs +57 -1
  30. package/{orchestration/lib → adapters/shared}/open-debug.mjs +32 -10
  31. package/{orchestration/lib → adapters/shared}/resolve-farmslot-ports.sh +47 -0
  32. package/adapters/shared/sync-wallet-fixture.sh +78 -0
  33. package/bin/mm-harness +37 -6
  34. package/docs/CLI-SPEC.md +41 -2
  35. package/docs/architecture.md +69 -38
  36. package/docs/runtime-file-conventions.md +3 -3
  37. package/library/actions/core/perps/_controller.mjs +1 -1
  38. package/library/actions/extension/platform/cdp.mjs +2 -2
  39. package/library/actions/extension/wallet/ensure_unlocked.mjs +1 -1
  40. package/library/actions/mobile/platform/bridge.mjs +1 -1
  41. package/library/actions/mobile/wallet/ensure_unlocked.mjs +1 -1
  42. package/library/actions/mobile/wallet/setup.mjs +2 -2
  43. package/library/library.json +1 -1
  44. package/package.json +8 -8
  45. package/{orchestration → src/adapters}/extension/runtime-decision.ts +5 -5
  46. package/{orchestration → src/adapters}/extension/runtime.ts +1 -1
  47. package/src/adapters/mobile/prepare.ts +210 -0
  48. package/{orchestration → src/adapters}/mobile/runtime-decision.ts +105 -5
  49. package/{runner/src → src}/adapters.ts +3 -3
  50. package/{orchestration/lib/cli-color.mjs → src/cli-color.ts} +21 -13
  51. package/{orchestration/lib/cli-commands.mjs → src/cli-commands.ts} +52 -45
  52. package/{orchestration/lib/cli-version.mjs → src/cli-version.ts} +35 -17
  53. package/{runner/src → src}/cli.ts +48 -40
  54. package/src/commands/debug.ts +63 -0
  55. package/src/commands/fixtures.ts +193 -0
  56. package/{runner/src → src}/commands/launch.ts +200 -54
  57. package/src/commands/logs.ts +83 -0
  58. package/{runner/src → src}/commands/shared.ts +40 -30
  59. package/src/commands/update.ts +316 -0
  60. package/{runner/src → src}/harness.ts +119 -16
  61. package/{runner/src → src}/heal-bounds.ts +20 -1
  62. package/src/index.ts +15 -0
  63. package/{runner/src → src}/live-adapter-contract.ts +1 -1
  64. package/{runner/src → src}/mm-harness-cli.ts +64 -3
  65. package/{runner/src → src}/paths.ts +2 -2
  66. package/{orchestration/lib/progress.mjs → src/progress.ts} +31 -21
  67. package/docs/extension-runtime-commands.md +0 -60
  68. package/docs/harness-cli.md +0 -43
  69. package/orchestration/compat-overlays/mobile/README.md +0 -13
  70. package/orchestration/doctor.mjs +0 -72
  71. package/orchestration/lib/cli-home.mjs +0 -354
  72. package/orchestration/lib/recipe-paths.mjs +0 -26
  73. package/orchestration/mobile/launch.sh +0 -137
  74. package/orchestration/mobile/live.sh +0 -125
  75. package/orchestration/porcelain/metamask-recipe +0 -99
  76. package/orchestration/porcelain/mm-recipe +0 -1591
  77. package/orchestration/porcelain/mme-recipe +0 -1181
  78. package/runner/src/commands/debug.ts +0 -44
  79. package/runner/src/commands/fixtures.ts +0 -99
  80. package/runner/src/commands/logs.ts +0 -60
  81. package/runner/src/index.ts +0 -6
  82. /package/{orchestration → adapters}/extension/launch-browser.cjs +0 -0
  83. /package/{orchestration → adapters}/extension/pin-remote-flags.cjs +0 -0
  84. /package/{orchestration → adapters}/extension/sidepanel-toggle.sh +0 -0
  85. /package/{orchestration → adapters}/extension/snapshot-dist.sh +0 -0
  86. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/cdp-eval.cjs +0 -0
  87. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/config.cjs +0 -0
  88. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/issue-capture.cjs +0 -0
  89. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/target-discovery.cjs +0 -0
  90. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/ws-client.cjs +0 -0
  91. /package/{library/actions → adapters}/mobile/bridge-runtime/setup-wallet.sh +0 -0
  92. /package/{library/actions/mobile/app-overlay → adapters/mobile/overlay}/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -0
  93. /package/{library/actions/mobile/app-overlay → adapters/mobile/overlay}/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -0
  94. /package/{orchestration/compat-overlays/mobile → adapters/mobile/overlay/compat}/rn81-message-event-source.patch +0 -0
  95. /package/{orchestration/lib → adapters/shared}/activate-repo-node.sh +0 -0
  96. /package/{orchestration/lib → adapters/shared}/ensure-runner-deps.sh +0 -0
  97. /package/{orchestration/lib → adapters/shared}/harness-path.sh +0 -0
  98. /package/{orchestration/lib → adapters/shared}/hash-helpers.sh +0 -0
  99. /package/{orchestration/lib → adapters/shared}/json-field.sh +0 -0
  100. /package/{orchestration/lib → adapters/shared}/path-defaults.json +0 -0
  101. /package/{orchestration → src/adapters}/extension/ensure-ready.ts +0 -0
  102. /package/{orchestration → src/adapters}/extension/extension-id.ts +0 -0
  103. /package/{orchestration → src/adapters}/mobile/deps-markers.ts +0 -0
  104. /package/{runner/src → src}/completions-cache.ts +0 -0
  105. /package/{runner/src → src}/doctor.ts +0 -0
  106. /package/{runner/src → src}/manifest.ts +0 -0
  107. /package/{runner/src → src}/recording-target.ts +0 -0
  108. /package/{runner/src → src}/run-recording.ts +0 -0
  109. /package/{runner/src → src}/runner.ts +0 -0
  110. /package/{runner/src → src}/types.ts +0 -0
@@ -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,
@@ -57,7 +57,7 @@ import type {
57
57
  RecipeValidationResult,
58
58
  } from '@farmslot/protocol';
59
59
  import type { MetaMaskRecipeAdapter } from './types.ts';
60
- import { usageOut } from './commands/shared.ts';
60
+ import { ADAPTER_DETECT_NEXT, usageOut } from './commands/shared.ts';
61
61
 
62
62
  type CliOptionValue = string | boolean;
63
63
  type CliOptions = Record<string, CliOptionValue>;
@@ -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 {
@@ -455,11 +455,7 @@ async function handleDoctor({ options }: ParsedArgs): Promise<number> {
455
455
  const explicitAdapter = optionString(options, 'adapter') ?? optionString(options, 'platform');
456
456
  const adapter = explicitAdapter ?? detectAdapter(target);
457
457
  if (!adapter) {
458
- return usageOut(
459
- json,
460
- 'doctor',
461
- `could not detect the MetaMask repo type for ${target}\n Next: pass --adapter mobile|extension|core`,
462
- );
458
+ return usageOut(json, 'doctor', `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
463
459
  }
464
460
  assertAdapter(adapter);
465
461
  const actionManifestPath = actionManifestPathOption(options, adapter);
@@ -596,7 +592,7 @@ async function handleRuntimeHealth({ options }: ParsedArgs): Promise<number> {
596
592
  optionString(options, 'cdpPort') ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT,
597
593
  'runtime-health requires --cdp-port <port>.',
598
594
  );
599
- const { checkExtensionRuntimeHealth, formatHealthFailure } = await import('../../orchestration/extension/runtime.ts');
595
+ const { checkExtensionRuntimeHealth, formatHealthFailure } = await import('./adapters/extension/runtime.ts');
600
596
  const report = await checkExtensionRuntimeHealth(target, cdpPort);
601
597
  if (optionFlag(options, 'json')) console.log(JSON.stringify(report, null, 2));
602
598
  else if (report.status === 'PASS') console.log(`PASS extension runtime cdp=${cdpPort} target=${report.targetUrl}`);
@@ -645,7 +641,7 @@ async function handleRuntimeLaunch({ options }: ParsedArgs): Promise<number> {
645
641
  cdpPort,
646
642
  artifactsDir,
647
643
  reason: 'harness_live_script_missing',
648
- fix: `Run metamask-recipe extension prepare --target ${shellQuote(target)}, then rerun: ${runtimeLaunchCommand(target, cdpPort, chromeUserDataDir)}`,
644
+ fix: `Run mm-harness install --adapter extension --target ${shellQuote(target)}, then rerun: ${runtimeLaunchCommand(target, cdpPort, chromeUserDataDir)}`,
649
645
  command,
650
646
  });
651
647
  printRuntimeLaunchReport(report, optionFlag(options, 'json'));
@@ -710,7 +706,7 @@ async function handleRuntimeLaunch({ options }: ParsedArgs): Promise<number> {
710
706
  }
711
707
 
712
708
  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}`;
709
+ const base = `mm-harness runtime-launch --adapter extension --target ${shellQuote(target)} --cdp-port ${cdpPort}`;
714
710
  const withMode = startWatch ? `${base} --start-watch` : base;
715
711
  return chromeUserDataDir ? `${withMode} --chrome-user-data-dir ${shellQuote(chromeUserDataDir)}` : withMode;
716
712
  }
@@ -828,7 +824,7 @@ async function handleEnsureReady({ options }: ParsedArgs): Promise<number> {
828
824
  // 5 validation failure (run, run --plan, call — adapter-aware validation errors).
829
825
  const EXIT = { ok: 0, runtime: 1, usage: 2, infra: 3, bounded: 4, validation: 5 } as const;
830
826
 
831
- // Typed exit-code error: carries exitCode so both the global catch (metamask-recipe bin)
827
+ // Typed exit-code error: carries exitCode so both the global catch (mm-harness bin)
832
828
  // and the mm-harness delegate() wrapper classify the error correctly rather than always
833
829
  // returning 1. Usage errors (bad args / unsupported flags) carry EXIT.usage (2);
834
830
  // validation errors carry EXIT.validation (5). Never throw a plain new Error() for
@@ -853,9 +849,7 @@ function resolveAdapter(options: CliOptions): { adapter: MetaMaskRecipeAdapter;
853
849
  const explicit = optionString(options, 'adapter') ?? optionString(options, 'platform');
854
850
  const adapter = explicit ?? detectAdapter(target);
855
851
  if (!adapter) {
856
- throw usageError(
857
- `could not detect the MetaMask repo type for ${target}\n Next: pass --adapter mobile|extension|core`,
858
- );
852
+ throw usageError(`could not detect the MetaMask repo type for ${target}\n Next: ${ADAPTER_DETECT_NEXT}`);
859
853
  }
860
854
  try {
861
855
  assertAdapter(adapter);
@@ -867,20 +861,24 @@ function resolveAdapter(options: CliOptions): { adapter: MetaMaskRecipeAdapter;
867
861
 
868
862
  // Adapter-aware recipe validation: schema (validateRecipeDocument) + action
869
863
  // existence/platform against the adapter's manifest (validateRecipeWithManifest).
870
- // This is the "run validates first" contract, shared by `run --plan` and `call`.
871
- //
872
- // NOTE (pinned deps): recipes that reference library flows by `call.ref` need
873
- // `externalFlowIds` sourced from resolveRecipeLibrarySources, which lands with
874
- // @farmslot/recipe-harness >= 0.3.3 (pending publish). Until then, self-contained
875
- // recipes (all in-repo recipes + every one-node `call` recipe) validate fully;
876
- // library-ref recipes would surface their refs here. Extends when 0.3.3 lands.
864
+ // When librarySources are provided (resolved from --library / personal / team
865
+ // tiers), external flow IDs from those sources are declared so `call.ref` nodes
866
+ // targeting library-defined flows pass schema validation.
877
867
  async function validateRecipeAdapterAware(
878
868
  recipe: unknown,
879
869
  manifest: RecipeActionManifestDocument,
870
+ librarySources?: MetaMaskLibrarySource[],
880
871
  ): Promise<RecipeValidationResult> {
881
872
  const { validateRecipeDocument, validateRecipeWithManifest } = await importRecipeProtocol();
882
- const schema = validateRecipeDocument(recipe);
883
- const withManifest = validateRecipeWithManifest(recipe, manifest);
873
+ let externalFlowIds: ReadonlySet<string> | undefined;
874
+ if (librarySources && librarySources.length > 0) {
875
+ const harness = await importRecipeHarness();
876
+ const resolution = await harness.loadRecipeLibraries(librarySources);
877
+ externalFlowIds = new Set(resolution.flows.keys());
878
+ }
879
+ const validationOptions = externalFlowIds !== undefined ? { externalFlowIds } : undefined;
880
+ const schema = validateRecipeDocument(recipe, validationOptions);
881
+ const withManifest = validateRecipeWithManifest(recipe, manifest, validationOptions);
884
882
  const findings = [...schema.findings, ...withManifest.findings];
885
883
  const errors = findings.filter((finding) => finding.severity === 'error').length;
886
884
  const warnings = findings.length - errors;
@@ -905,9 +903,10 @@ function countRecipeNodes(recipe: unknown): number | undefined {
905
903
  // Static, adapter-aware recipe validation shared by `run --plan` and the `run`
906
904
  // execute path so both "validate first" the same way: read the file (missing /
907
905
  // unparseable → usage error, nothing to validate), check the manifest, then run
908
- // the adapter-aware schema + action-existence validation. Touches nothing and
909
- // resolves no libraries, so it never emits the engine's "Recipe libraries: …"
910
- // log — safe to call before the machine-contract JSON is written.
906
+ // the adapter-aware schema + action-existence validation. Loads library flow IDs
907
+ // from --library (so `call.ref` nodes to external flows resolve correctly), but
908
+ // never emits the engine's "Recipe libraries: …" log — safe to call before the
909
+ // machine-contract JSON is written.
911
910
  interface RunRecipeStaticValidation {
912
911
  recipe: unknown;
913
912
  recipeFile: string;
@@ -958,8 +957,10 @@ async function validateRunRecipeStatic(
958
957
  }
959
958
 
960
959
  // validate.schema + validate.actions (static) — adapter-aware recipe validation.
960
+ // Library sources are resolved here so external `call.ref` flows validate correctly.
961
+ const librarySources = await resolveMetaMaskLibrarySources(optionString(options, 'library'));
961
962
  const validation = manifestOk
962
- ? await validateRecipeAdapterAware(recipe, manifest)
963
+ ? await validateRecipeAdapterAware(recipe, manifest, librarySources)
963
964
  : { status: 'invalid' as const, findings: [], summary: { errors: 1, warnings: 0 } };
964
965
  findings.push(...validation.findings);
965
966
 
@@ -1184,7 +1185,7 @@ async function handleCall(argv: string[]): Promise<number> {
1184
1185
  heal,
1185
1186
  state,
1186
1187
  );
1187
- if (violation !== null) return emitHealViolation(json, 'call', result, violation, state);
1188
+ if (violation !== null) return emitHealViolation(json, 'call', result, violation, state, adapter);
1188
1189
 
1189
1190
  if (json) {
1190
1191
  console.log(
@@ -1286,7 +1287,7 @@ function synthesizeOneNodeRecipe(action: string, args: Record<string, string>):
1286
1287
 
1287
1288
  // Reusable one-node engine path (the same machinery `call` uses), exposed so
1288
1289
  // `fixtures set` can apply the canonical wallet fixture through the real engine on
1289
- // the extension (which has no standalone porcelain set arm).
1290
+ // the extension (which has no standalone shell set arm).
1290
1291
  async function runOneNode(
1291
1292
  adapter: MetaMaskRecipeAdapter,
1292
1293
  action: string,
@@ -1520,7 +1521,7 @@ async function handleRun({ positional, options }: ParsedArgs): Promise<number> {
1520
1521
  heal,
1521
1522
  state,
1522
1523
  );
1523
- if (violation !== null) return emitHealViolation(json, 'run', result, violation, state);
1524
+ if (violation !== null) return emitHealViolation(json, 'run', result, violation, state, adapter);
1524
1525
  const exitCode = result.status === 'pass' ? EXIT.ok : EXIT.runtime;
1525
1526
  if (json) {
1526
1527
  // mm-harness envelope (matches launch/verify/doctor): the engine's result is
@@ -1619,7 +1620,14 @@ function emitHealViolation(
1619
1620
  result: RecipeRunResult,
1620
1621
  violation: HealBoundViolation,
1621
1622
  state: HealState,
1623
+ adapter?: MetaMaskRecipeAdapter,
1622
1624
  ): number {
1625
+ // core is headless: `fixtures set` is blocked. Teach the env-var / recipe-node
1626
+ // path instead so the user has a reachable next step.
1627
+ const userAction =
1628
+ adapter === 'core' && violation.code === 'WALLET_STATE_REQUIRED'
1629
+ ? 'set MM_TEST_ACCOUNT_ADDRESS=<0x…> in env, or add "account": "<0x…>" to the node block in the recipe'
1630
+ : violation.userAction;
1623
1631
  if (json) {
1624
1632
  console.log(
1625
1633
  JSON.stringify(
@@ -1639,7 +1647,7 @@ function emitHealViolation(
1639
1647
  code: violation.code,
1640
1648
  message: violation.message,
1641
1649
  retryable: false,
1642
- userAction: violation.userAction ?? null,
1650
+ userAction: userAction ?? null,
1643
1651
  originalError: violation.originalError ?? null,
1644
1652
  },
1645
1653
  },
@@ -1651,7 +1659,7 @@ function emitHealViolation(
1651
1659
  console.error(
1652
1660
  `✗ mm-harness ${command}: ${violation.message}` +
1653
1661
  (violation.originalError ? `\n --- original failure ---\n${violation.originalError}` : '') +
1654
- (violation.userAction ? `\n Next: ${violation.userAction}` : ''),
1662
+ (userAction ? `\n Next: ${userAction}` : ''),
1655
1663
  );
1656
1664
  }
1657
1665
  return violation.exitCode;
@@ -1792,7 +1800,7 @@ export async function main(argv: string[]): Promise<number> {
1792
1800
  }
1793
1801
  return code;
1794
1802
  }
1795
- // launch/logs/debug/fixtures compose the porcelain paths and own policy,
1803
+ // launch/logs/debug/fixtures compose adapter scripts directly and own policy,
1796
1804
  // healing, teaching, and the --json contract.
1797
1805
  if (command === 'launch') return handleLaunch(argv.slice(1));
1798
1806
  if (command === 'logs') return handleLogs(argv.slice(1));
@@ -1803,8 +1811,8 @@ export async function main(argv: string[]): Promise<number> {
1803
1811
  return handler(parseArgs(argv.slice(1), command));
1804
1812
  }
1805
1813
 
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
1814
+ // Auto-run only when this file IS the process entry (bin/mm-harness execs it
1815
+ // directly). The commander front (`src/mm-harness-cli.ts`) sets this flag
1808
1816
  // before importing so it can reuse `main` without triggering a second dispatch.
1809
1817
  if (!(globalThis as Record<string, unknown>).__MM_HARNESS_WRAPPER__) {
1810
1818
  try {
@@ -0,0 +1,63 @@
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 { ADAPTER_DETECT_NEXT, 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}`, ADAPTER_DETECT_NEXT);
19
+ if (adapter === 'core') return usageOut(json, 'debug', 'core is headless; there is no debug console.', 'mm-harness verify');
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.',
28
+ 'mm-harness debug --dev-menu',
29
+ );
30
+ }
31
+ if (adapter === 'extension' && devMenu) {
32
+ return usageOut(
33
+ json,
34
+ 'debug',
35
+ '--dev-menu is mobile-only (RN developer menu). Use --worker on the extension.',
36
+ 'mm-harness debug --worker',
37
+ );
38
+ }
39
+
40
+ const openDebug = path.join(runnerDir, 'adapters/shared/open-debug.mjs');
41
+ const debugArgs: string[] = ['--adapter', adapter];
42
+
43
+ if (adapter === 'extension') {
44
+ if (process.env.CDP_PORT) debugArgs.push('--cdp-port', process.env.CDP_PORT);
45
+ debugArgs.push('--target', worker ? 'worker' : 'page');
46
+ } else {
47
+ if (process.env.WATCHER_PORT) debugArgs.push('--port', process.env.WATCHER_PORT);
48
+ debugArgs.push('--action', devMenu ? 'dev-menu' : 'debug');
49
+ }
50
+ if (json) debugArgs.push('--json');
51
+
52
+ const result = spawnScript(process.execPath, [openDebug, ...debugArgs], target, json);
53
+ if (json) {
54
+ console.log(
55
+ JSON.stringify(
56
+ { schemaVersion: 1, command: 'debug', adapter, mode: worker ? 'worker' : devMenu ? 'dev-menu' : 'default', exitCode: result.status === 0 ? EXIT.ok : EXIT.runtime },
57
+ null,
58
+ 2,
59
+ ),
60
+ );
61
+ }
62
+ return result.status === 0 ? EXIT.ok : EXIT.runtime;
63
+ }