@deeeed/metamask-harness 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +37 -20
  2. package/adapters/core/inject.sh +1 -0
  3. package/adapters/extension/cleanup.mjs +1 -1
  4. package/adapters/extension/inject.mjs +2 -1
  5. package/adapters/extension/launch-browser.cjs +18 -4
  6. package/adapters/extension/live.sh +14 -2
  7. package/adapters/extension/sidepanel-toggle.sh +33 -12
  8. package/adapters/extension/wallet-fixture-state.cjs +38 -24
  9. package/adapters/manifest.json +13 -0
  10. package/adapters/mobile/inject.sh +1 -0
  11. package/adapters/mobile/stop-metro.sh +20 -0
  12. package/adapters/shared/cli-ux.sh +20 -2
  13. package/adapters/shared/ensure-runner-deps.sh +30 -0
  14. package/adapters/shared/reap-checkout-metros.sh +53 -0
  15. package/adapters/shared/recipe-harness-root.mjs +23 -0
  16. package/adapters/shared/resolve-farmslot-ports-core.mjs +15 -6
  17. package/bin/mm-harness +45 -4
  18. package/dist/adapters/extension/runtime.js +3 -1
  19. package/dist/adapters/mobile/provision.js +34 -1
  20. package/dist/adapters/slot-ports.js +3 -8
  21. package/dist/cli.js +32 -1463
  22. package/dist/commands/call.js +183 -0
  23. package/dist/commands/completion-candidates.js +58 -0
  24. package/dist/commands/doctor.js +101 -0
  25. package/dist/commands/ensure-ready.js +24 -0
  26. package/dist/commands/flows.js +62 -0
  27. package/dist/commands/launch/extension.js +40 -0
  28. package/dist/commands/{launch.js → launch/index.js} +15 -47
  29. package/dist/commands/launch/mobile.js +10 -0
  30. package/dist/commands/manifest.js +72 -0
  31. package/dist/commands/parse-args.js +189 -0
  32. package/dist/commands/provision.js +136 -0
  33. package/dist/commands/resolve-extension.js +23 -0
  34. package/dist/commands/run-engine.js +341 -0
  35. package/dist/commands/run.js +217 -0
  36. package/dist/commands/runtime-decision.js +58 -0
  37. package/dist/commands/runtime-health.js +25 -0
  38. package/dist/commands/runtime-launch.js +139 -0
  39. package/dist/commands/self-test.js +52 -0
  40. package/dist/commands/stop.js +52 -0
  41. package/dist/harness.js +8 -48
  42. package/dist/mm-harness-cli.js +13 -8
  43. package/docs/CLI-SPEC.md +1 -1
  44. package/docs/CODE-MAP.md +62 -0
  45. package/library/README.md +14 -0
  46. package/library/actions/extension/platform/cdp.mjs +1 -1
  47. package/library/actions/extension/wallet/ensure_unlocked.mjs +6 -0
  48. package/library/actions/mobile/wallet/ensure_unlocked.mjs +13 -1
  49. package/package.json +6 -7
  50. package/src/adapters/core/surface.ts +0 -71
  51. package/src/adapters/extension/ensure-ready.ts +0 -185
  52. package/src/adapters/extension/extension-id.ts +0 -107
  53. package/src/adapters/extension/runtime-decision.ts +0 -445
  54. package/src/adapters/extension/runtime.ts +0 -407
  55. package/src/adapters/extension/surface.ts +0 -88
  56. package/src/adapters/mobile/deps-markers.ts +0 -21
  57. package/src/adapters/mobile/prepare.ts +0 -246
  58. package/src/adapters/mobile/provision.ts +0 -594
  59. package/src/adapters/mobile/runtime-decision.ts +0 -466
  60. package/src/adapters/mobile/surface.ts +0 -71
  61. package/src/adapters/resolve-farmslot-ports.ts +0 -13
  62. package/src/adapters/slot-ports.ts +0 -158
  63. package/src/adapters/surface.ts +0 -117
  64. package/src/adapters.ts +0 -601
  65. package/src/cli-color.ts +0 -92
  66. package/src/cli-commands.ts +0 -250
  67. package/src/cli-version.ts +0 -141
  68. package/src/cli.ts +0 -2091
  69. package/src/commands/debug.ts +0 -65
  70. package/src/commands/fixtures.ts +0 -198
  71. package/src/commands/launch.ts +0 -470
  72. package/src/commands/logs.ts +0 -99
  73. package/src/commands/shared.ts +0 -235
  74. package/src/commands/update.ts +0 -316
  75. package/src/completions-cache.ts +0 -86
  76. package/src/doctor.ts +0 -215
  77. package/src/harness.ts +0 -797
  78. package/src/heal-bounds.ts +0 -198
  79. package/src/index.ts +0 -15
  80. package/src/leaf-invoke.ts +0 -28
  81. package/src/live-adapter-contract.ts +0 -274
  82. package/src/manifest.ts +0 -47
  83. package/src/mm-harness-cli.ts +0 -655
  84. package/src/paths.ts +0 -198
  85. package/src/progress.ts +0 -117
  86. package/src/recording-target.ts +0 -147
  87. package/src/run-recording.ts +0 -329
  88. package/src/runner.ts +0 -108
  89. package/src/types.ts +0 -57
@@ -1,158 +0,0 @@
1
- // Slot-port + dev-server process plumbing shared by the per-platform surfaces.
2
- // Re-homed here (out of commands/launch.ts) so the surface implementations own
3
- // port/device resolution and the extension watcher-stop without launch.ts and
4
- // the surface registry forming an import cycle.
5
-
6
- import { execFileSync } from 'node:child_process';
7
- import fs from 'node:fs';
8
- import path from 'node:path';
9
-
10
- import { readRuntimeContextField, resolveRuntimeContextPath } from '../harness.ts';
11
- import { recipeRuntimeDir } from '../paths.ts';
12
- import {
13
- resolveDefaultExtensionPorts,
14
- resolveFarmslotPortsByRepo,
15
- resolveMobileRuntimeContext,
16
- resolveMobileSlotDefaults,
17
- } from './resolve-farmslot-ports.ts';
18
-
19
- // Apply KEY=VALUE lines from slot resolution to process.env.
20
- // overwrite=true → pool/context match, always overrides existing env.
21
- // overwrite=false → formula match, only fills vars that are unset.
22
- export function applyKVLines(output: string, overwrite: boolean): void {
23
- for (const line of output.split('\n')) {
24
- const m = /^([A-Z_]+)=(.+)$/u.exec(line.trim());
25
- if (!m) continue;
26
- const [, key, val] = m;
27
- switch (key) {
28
- case 'WATCHER_PORT':
29
- if (overwrite || !process.env['WATCHER_PORT']) {
30
- process.env['WATCHER_PORT'] = val;
31
- process.env['METRO_PORT'] = val;
32
- process.env['RECIPE_WATCHER_PORT'] = val;
33
- }
34
- break;
35
- case 'IOS_SIMULATOR':
36
- if (overwrite || !process.env['IOS_SIMULATOR']) process.env['IOS_SIMULATOR'] = val;
37
- break;
38
- case 'SLOT_ID':
39
- if (overwrite || !process.env['RECIPE_SLOT_ID']) process.env['RECIPE_SLOT_ID'] = val;
40
- break;
41
- case 'CDP_PORT':
42
- if (overwrite || !process.env['CDP_PORT']) {
43
- process.env['CDP_PORT'] = val;
44
- process.env['RECIPE_CDP_PORT'] = val;
45
- }
46
- break;
47
- }
48
- }
49
- }
50
-
51
- // Resolve mobile slot port/simulator: the slot context the orchestrator wrote
52
- // into the checkout wins first, then the farmslot pool (both overwrite env),
53
- // then the slot-suffix formula (only fills unset vars). Called before explicit
54
- // CLI flag overrides so flags always win at the top.
55
- export function resolveMobileSlotPorts(target: string): void {
56
- // The checkout's own runtime context is authoritative — it names the exact
57
- // simulator/port this slot was prepared with, surviving pool renames.
58
- const ctxOut = resolveMobileRuntimeContext(target);
59
- if (ctxOut?.trim()) {
60
- applyKVLines(ctxOut, true);
61
- return;
62
- }
63
- // Pool match always wins — overwrite whatever is in the environment.
64
- const poolOut = resolveFarmslotPortsByRepo(target);
65
- if (poolOut?.trim()) {
66
- applyKVLines(poolOut, true);
67
- return;
68
- }
69
- // Formula match only fills unset vars (never overrides explicit env/pool).
70
- const defOut = resolveMobileSlotDefaults(target);
71
- if (defOut?.trim()) applyKVLines(defOut, false);
72
- }
73
-
74
- // Resolve extension slot ports the same way as mobile: the checkout's runtime
75
- // context first (cdpPort/devServerPort written by the orchestrator's prepare),
76
- // then the farmslot pool, then the directory-suffix formula (fills unset only).
77
- export function resolveExtensionSlotPorts(target: string): void {
78
- // The prepared checkout's context OVERWRITES inherited env (same authority as
79
- // mobile's context/pool resolution): a stale CDP_PORT from the shell must not
80
- // hijack the slot's browser. Explicit CLI flags are applied after and win.
81
- const contextPath = resolveRuntimeContextPath(target);
82
- const cdp = readRuntimeContextField(contextPath, 'cdpPort');
83
- if (cdp) {
84
- process.env['CDP_PORT'] = cdp;
85
- process.env['RECIPE_CDP_PORT'] = cdp;
86
- }
87
- const dev = readRuntimeContextField(contextPath, 'devServerPort');
88
- if (dev) {
89
- process.env['WATCHER_PORT'] = dev;
90
- process.env['RECIPE_WATCHER_PORT'] = dev;
91
- }
92
- if (process.env['CDP_PORT']) return;
93
- const poolOut = resolveFarmslotPortsByRepo(target);
94
- if (poolOut?.trim()) {
95
- applyKVLines(poolOut, true);
96
- return;
97
- }
98
- const defOut = resolveDefaultExtensionPorts(target);
99
- if (defOut?.trim()) applyKVLines(defOut, false);
100
- }
101
-
102
- // Kill the harness-owned webpack watcher for this checkout: pid file first,
103
- // then a ps-scan for orphans (argv or lsof-cwd match), TERM then KILL. Scoped
104
- // to the target checkout — watchers of other slots are never touched. Returns
105
- // how many processes were signalled so callers can state the outcome.
106
- export function stopExtensionWatcher(target: string): number {
107
- const runtimeAbs = path.join(target, recipeRuntimeDir());
108
- const webpackPidFile = path.join(runtimeAbs, 'recipe-harness-webpack.pid');
109
- let signalled = 0;
110
- try {
111
- const pid = fs.readFileSync(webpackPidFile, 'utf8').trim();
112
- if (/^\d+$/u.test(pid)) {
113
- try { process.kill(Number(pid), 'SIGTERM'); signalled += 1; } catch { /* already dead */ }
114
- }
115
- fs.rmSync(webpackPidFile, { force: true });
116
- } catch { /* no pid file */ }
117
- // Scan for any remaining orphan webpack/yarn-start processes in this checkout.
118
- try {
119
- const psOut = execFileSync('ps', ['-axo', 'pid=,command='], { encoding: 'utf8' });
120
- const orphanPids: number[] = [];
121
- for (const line of psOut.split('\n')) {
122
- const match = /^\s*(\d+)\s+(.*)$/u.exec(line);
123
- if (!match) continue;
124
- const [, pidStr, cmd] = match;
125
- const isWatcher =
126
- cmd.includes('yarn start') ||
127
- cmd.includes('webpack --watch') ||
128
- cmd.includes('development/webpack/launch.ts --watch');
129
- if (!isWatcher) continue;
130
- if (cmd.includes(target)) {
131
- orphanPids.push(Number(pidStr));
132
- continue;
133
- }
134
- // lsof cwd fallback for processes that don't embed the path in argv.
135
- try {
136
- const cwd = execFileSync('lsof', ['-a', `-p${pidStr}`, '-dcwd', '-Fn'], {
137
- encoding: 'utf8',
138
- timeout: 2000,
139
- });
140
- if (cwd.split('\n').some((l) => l.startsWith('n') && l.slice(1) === target)) {
141
- orphanPids.push(Number(pidStr));
142
- }
143
- } catch { /* lsof unavailable or permission denied */ }
144
- }
145
- if (orphanPids.length > 0) {
146
- for (const pid of orphanPids) {
147
- try { process.kill(pid, 'SIGTERM'); } catch { /* already dead */ }
148
- }
149
- // Brief pause then force-kill survivors.
150
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000);
151
- for (const pid of orphanPids) {
152
- try { process.kill(pid, 'SIGKILL'); } catch { /* already dead */ }
153
- }
154
- signalled += orphanPids.length;
155
- }
156
- } catch { /* ps not available */ }
157
- return signalled;
158
- }
@@ -1,117 +0,0 @@
1
- // The per-platform surface: one interface every command resolves platform
2
- // behavior through, so no command hand-rolls `if (adapter === …)` for behavior a
3
- // platform owns. A command NEVER branches on adapter for behavior this interface
4
- // owns; a new platform behavior extends the surface (a new member here + its
5
- // three implementations), not the command. This is the enforcement mechanism for
6
- // UX-PRINCIPLES.md principle 1 (context-aware by default). See
7
- // docs/ADAPTER-SURFACE.md.
8
-
9
- import type { MetaMaskRecipeAdapter } from '../types.ts';
10
- import { coreSurface } from './core/surface.ts';
11
- import { extensionSurface } from './extension/surface.ts';
12
- import { mobileSurface } from './mobile/surface.ts';
13
-
14
- // Read-only runtime readiness, normalized across platforms so `doctor` renders
15
- // one line the same way regardless of adapter. Device platforms fill deps +
16
- // devServer; core (headless) reports deps presence only (no devServer).
17
- export interface AdapterRuntimeStatus {
18
- decision: string;
19
- reasonCode?: string;
20
- reasons: string[];
21
- deps?: string;
22
- // The platform's dev server (Metro for mobile, webpack watcher for extension),
23
- // labelled so the render names the right thing. Absent for headless core.
24
- devServer?: { label: string; status: string };
25
- }
26
-
27
- // One log file a platform writes, most-relevant first. `logs` tails the first
28
- // candidate that exists.
29
- export interface AdapterLogSource {
30
- label: string;
31
- path: string;
32
- }
33
-
34
- // Result of stopping the platform's dev server. `headless` is the core case —
35
- // there is nothing to stop, so `stop` teaches instead. `stopped` carries the
36
- // exit status plus, when the platform counts them, how many processes were
37
- // signalled and the raw leaf output for the --json envelope.
38
- export type AdapterDevServerStop =
39
- | { kind: 'headless'; message: string; userAction: string }
40
- | { kind: 'stopped'; status: number; summary: string; signalled?: number; output?: string };
41
-
42
- export interface AdapterDevServer {
43
- // One noun for the dev server this platform runs ("Metro", "webpack watcher").
44
- describe(): string;
45
- // Stop the dev server this checkout owns, port/pid-scoped. Idempotent:
46
- // nothing-to-stop is success, never an error.
47
- stop(target: string): AdapterDevServerStop;
48
- }
49
-
50
- // Platform-phrased Next: hints so no command prints another platform's vocabulary.
51
- // `launch` (re)starts the app + dev server; `relaunch` rebuilds first (the
52
- // fixtures-set retry). core has no app, so its hints teach the headless path.
53
- export interface AdapterHints {
54
- launch: string;
55
- relaunch: string;
56
- }
57
-
58
- export interface AdapterRunwayProvisionOptions {
59
- json?: boolean;
60
- platform?: string;
61
- branch?: string;
62
- defaultBranch?: string;
63
- run?: string;
64
- cacheRoot?: string;
65
- simulator?: string;
66
- runtime?: string;
67
- deviceType?: string;
68
- slot?: string;
69
- watcherPort?: string;
70
- runtimeDir?: string;
71
- force?: boolean;
72
- resolveOnly?: boolean;
73
- rerunCommand: string;
74
- }
75
-
76
- export interface AdapterRunwayProvisionResult {
77
- schemaVersion: 1;
78
- command: 'provision';
79
- adapter: MetaMaskRecipeAdapter;
80
- target: string;
81
- platform?: string;
82
- status: 'pass' | 'fail';
83
- exitCode: number;
84
- error?: { code: string; message: string; userAction: string };
85
- [key: string]: unknown;
86
- }
87
-
88
- export interface AdapterRunwayProvision {
89
- run(target: string, options: AdapterRunwayProvisionOptions): Promise<AdapterRunwayProvisionResult>;
90
- }
91
-
92
- export interface AdapterSurface {
93
- readonly adapter: MetaMaskRecipeAdapter;
94
- // core runs no app/dev server; device adapters (mobile/extension) do. Commands
95
- // ask this instead of testing `adapter === 'core'`.
96
- readonly headless: boolean;
97
- // Resolve slot ports/device into the environment (checkout context > pool >
98
- // formula). No-op for core.
99
- resolveSlotPorts(target: string): void;
100
- // Read-only readiness for `doctor`. Never launches or mutates.
101
- runtimeStatus(target: string): Promise<AdapterRuntimeStatus>;
102
- devServer: AdapterDevServer;
103
- runwayProvision: AdapterRunwayProvision;
104
- // Ordered candidate log files for `logs`. Empty for core.
105
- logSources(target: string): AdapterLogSource[];
106
- hints: AdapterHints;
107
- }
108
-
109
- const SURFACES: Record<MetaMaskRecipeAdapter, AdapterSurface> = {
110
- mobile: mobileSurface,
111
- extension: extensionSurface,
112
- core: coreSurface,
113
- };
114
-
115
- export function getAdapterSurface(adapter: MetaMaskRecipeAdapter): AdapterSurface {
116
- return SURFACES[adapter];
117
- }