@deeeed/metamask-harness 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +51 -20
  2. package/adapters/core/inject.sh +1 -0
  3. package/adapters/extension/cleanup.mjs +1 -1
  4. package/adapters/extension/inject.mjs +4 -1
  5. package/adapters/extension/launch-browser.cjs +18 -4
  6. package/adapters/extension/live.sh +14 -2
  7. package/adapters/extension/refresh-build.sh +2 -2
  8. package/adapters/extension/sidepanel-toggle.sh +33 -12
  9. package/adapters/extension/start-watch.sh +2 -2
  10. package/adapters/extension/wallet-fixture-state.cjs +38 -24
  11. package/adapters/manifest.json +32 -3
  12. package/adapters/mobile/inject.sh +1 -0
  13. package/adapters/mobile/start-metro.sh +1 -1
  14. package/adapters/mobile/stop-metro.sh +20 -0
  15. package/adapters/mobile/verify.sh +1 -1
  16. package/adapters/shared/activate-repo-node.sh +1 -1
  17. package/adapters/shared/cli-ux.sh +44 -25
  18. package/adapters/shared/ensure-runner-deps.sh +30 -0
  19. package/adapters/shared/log-tui.mjs +4 -4
  20. package/adapters/shared/open-debug.mjs +1 -1
  21. package/adapters/shared/reap-checkout-metros.sh +53 -0
  22. package/adapters/shared/recipe-harness-root.mjs +23 -0
  23. package/adapters/shared/resolve-farmslot-ports-core.mjs +205 -0
  24. package/adapters/shared/resolve-farmslot-ports.mjs +20 -0
  25. package/adapters/shared/resolve-farmslot-ports.sh +19 -126
  26. package/bin/mm-harness +45 -4
  27. package/dist/adapters/extension/runtime.js +3 -1
  28. package/dist/adapters/mobile/provision.js +34 -1
  29. package/dist/adapters/mobile/runtime-decision.js +3 -1
  30. package/dist/adapters/resolve-farmslot-ports.js +22 -0
  31. package/dist/adapters/slot-ports.js +18 -29
  32. package/dist/cli.js +32 -1463
  33. package/dist/commands/call.js +183 -0
  34. package/dist/commands/completion-candidates.js +58 -0
  35. package/dist/commands/doctor.js +101 -0
  36. package/dist/commands/ensure-ready.js +24 -0
  37. package/dist/commands/flows.js +62 -0
  38. package/dist/commands/launch/extension.js +40 -0
  39. package/dist/commands/{launch.js → launch/index.js} +15 -47
  40. package/dist/commands/launch/mobile.js +10 -0
  41. package/dist/commands/manifest.js +72 -0
  42. package/dist/commands/parse-args.js +189 -0
  43. package/dist/commands/provision.js +136 -0
  44. package/dist/commands/resolve-extension.js +23 -0
  45. package/dist/commands/run-engine.js +341 -0
  46. package/dist/commands/run.js +217 -0
  47. package/dist/commands/runtime-decision.js +58 -0
  48. package/dist/commands/runtime-health.js +25 -0
  49. package/dist/commands/runtime-launch.js +139 -0
  50. package/dist/commands/self-test.js +52 -0
  51. package/dist/commands/stop.js +52 -0
  52. package/dist/harness.js +8 -48
  53. package/dist/mm-harness-cli.js +13 -8
  54. package/docs/CLI-SPEC.md +1 -1
  55. package/docs/CODE-MAP.md +62 -0
  56. package/library/README.md +14 -0
  57. package/library/actions/core/perps/_controller.mjs +1 -1
  58. package/library/actions/extension/platform/cdp.mjs +2 -2
  59. package/library/actions/extension/wallet/ensure_unlocked.mjs +7 -1
  60. package/library/actions/harness-exports.mjs +27 -0
  61. package/library/actions/mobile/wallet/ensure_unlocked.mjs +14 -2
  62. package/library/actions/mobile/wallet/setup.mjs +1 -1
  63. package/package.json +6 -7
  64. package/src/adapters/core/surface.ts +0 -71
  65. package/src/adapters/extension/ensure-ready.ts +0 -185
  66. package/src/adapters/extension/extension-id.ts +0 -107
  67. package/src/adapters/extension/runtime-decision.ts +0 -445
  68. package/src/adapters/extension/runtime.ts +0 -407
  69. package/src/adapters/extension/surface.ts +0 -88
  70. package/src/adapters/mobile/deps-markers.ts +0 -21
  71. package/src/adapters/mobile/prepare.ts +0 -246
  72. package/src/adapters/mobile/provision.ts +0 -594
  73. package/src/adapters/mobile/runtime-decision.ts +0 -459
  74. package/src/adapters/mobile/surface.ts +0 -71
  75. package/src/adapters/slot-ports.ts +0 -165
  76. package/src/adapters/surface.ts +0 -117
  77. package/src/adapters.ts +0 -601
  78. package/src/cli-color.ts +0 -92
  79. package/src/cli-commands.ts +0 -250
  80. package/src/cli-version.ts +0 -141
  81. package/src/cli.ts +0 -2091
  82. package/src/commands/debug.ts +0 -65
  83. package/src/commands/fixtures.ts +0 -198
  84. package/src/commands/launch.ts +0 -470
  85. package/src/commands/logs.ts +0 -99
  86. package/src/commands/shared.ts +0 -235
  87. package/src/commands/update.ts +0 -316
  88. package/src/completions-cache.ts +0 -86
  89. package/src/doctor.ts +0 -215
  90. package/src/harness.ts +0 -797
  91. package/src/heal-bounds.ts +0 -198
  92. package/src/index.ts +0 -15
  93. package/src/leaf-invoke.ts +0 -28
  94. package/src/live-adapter-contract.ts +0 -274
  95. package/src/manifest.ts +0 -47
  96. package/src/mm-harness-cli.ts +0 -655
  97. package/src/paths.ts +0 -198
  98. package/src/progress.ts +0 -117
  99. package/src/recording-target.ts +0 -147
  100. package/src/run-recording.ts +0 -329
  101. package/src/runner.ts +0 -108
  102. package/src/types.ts +0 -57
@@ -1,198 +0,0 @@
1
- // Healing policy + recovery bounds — shared by launch, run, and call (policy, not
2
- // per-command). Owns: overlay auto-ensure, failure classification, the recovery
3
- // bounds that stop blind retry loops, and the --heal parser.
4
- //
5
- // Overlay auto-ensure seam (overridable for CI/agents and contract tests):
6
- // MM_HARNESS_INSTALL_BIN — overlay installer (default: handleHarness install)
7
-
8
- import { spawnSync } from 'node:child_process';
9
- import fs from 'node:fs';
10
- import path from 'node:path';
11
-
12
- import { handleHarness } from './harness.ts';
13
- import { recipeHarnessPath, recipeRuntimePath } from './paths.ts';
14
- import type { MetaMaskRecipeAdapter } from './types.ts';
15
- import { EXIT } from './commands/shared.ts';
16
- import type { DeviceAdapter } from './commands/shared.ts';
17
-
18
- export type HealPolicy = 'off' | 'infra-only' | 'auto';
19
-
20
- interface Mutation {
21
- type: string;
22
- action: string;
23
- [key: string]: unknown;
24
- }
25
-
26
- export interface HealState {
27
- recovered: string[];
28
- mutations: Mutation[];
29
- attemptedRecoveries: string[];
30
- }
31
-
32
- export function newHealState(): HealState {
33
- return { recovered: [], mutations: [], attemptedRecoveries: [] };
34
- }
35
-
36
- export function parseHeal(
37
- options: Record<string, string | boolean>,
38
- fallback: HealPolicy,
39
- ): HealPolicy | { error: string } {
40
- const value = options.heal;
41
- if (value === undefined) return fallback;
42
- if (value === 'off' || value === 'infra-only' || value === 'auto') return value;
43
- return { error: `--heal must be off, infra-only, or auto (got "${String(value)}").` };
44
- }
45
-
46
- function overlayDir(target: string, adapter: MetaMaskRecipeAdapter): string {
47
- return recipeHarnessPath(target, adapter);
48
- }
49
-
50
- function overlayPresent(target: string, adapter: MetaMaskRecipeAdapter): boolean {
51
- try {
52
- return fs.statSync(overlayDir(target, adapter)).isDirectory();
53
- } catch {
54
- return false;
55
- }
56
- }
57
-
58
- // Validates that the installed overlay's runner-source stamp points to an existing
59
- // runner directory whose binary is still present. Two failure modes are detected:
60
- // 1. Stale stamp — runner dir moved or renamed since install (dir absent).
61
- // 2. Stale exec target — runner dir exists but its bin/mm-harness was removed
62
- // (e.g. the package was unlinked/purged inside an otherwise-live dir).
63
- // Either way the delegate binary will fail to exec; treat the overlay as absent
64
- // so auto-ensure re-installs it from the current runner.
65
- function overlayDelegateValid(target: string, adapter: MetaMaskRecipeAdapter): boolean {
66
- if (adapter === 'core') return true;
67
- const pointer = path.join(recipeHarnessPath(target, adapter), 'runner', '.runner-source');
68
- if (!fs.existsSync(pointer)) return true; // no stamp = running from local runner, valid
69
- const runnerPath = fs.readFileSync(pointer, 'utf8').trim();
70
- if (!Boolean(runnerPath) || !fs.existsSync(runnerPath)) return false;
71
- // Runner dir is present; verify the binary inside still exists — the directory
72
- // can survive a package removal while the binary inside is deleted.
73
- return fs.existsSync(path.join(runnerPath, 'bin', 'mm-harness'));
74
- }
75
-
76
- // Auto-ensure the runtime overlay (install phase). Missing + heal != off →
77
- // install inline, emit a first-install notice to stderr, and record the mutation.
78
- // heal === off disables auto-install (repro-preserving); we proceed and let the
79
- // porcelain surface any missing-overlay failure itself.
80
- export async function ensureOverlay(
81
- adapter: MetaMaskRecipeAdapter,
82
- target: string,
83
- heal: HealPolicy,
84
- state: HealState,
85
- json: boolean,
86
- ): Promise<{ ok: boolean; error?: string }> {
87
- // core is headless — the engine path used by run/call needs no launch overlay.
88
- if (adapter === 'core') return { ok: true };
89
- if (overlayPresent(target, adapter) && overlayDelegateValid(target, adapter)) return { ok: true };
90
- if (heal === 'off') return { ok: true };
91
-
92
- const installBin = process.env.MM_HARNESS_INSTALL_BIN;
93
- let code: number;
94
- if (installBin) {
95
- const result = spawnSync(installBin, ['install', '--platform', adapter, '--target', target], {
96
- cwd: target,
97
- stdio: ['ignore', 2, 'inherit'],
98
- env: process.env,
99
- });
100
- code = result.status ?? 1;
101
- } else {
102
- code = await handleHarness(['install', '--platform', adapter, '--target', target, ...(json ? ['--json'] : [])]);
103
- }
104
- if (code !== 0 || !overlayPresent(target, adapter)) {
105
- return { ok: false, error: `runtime overlay install failed (exit ${code})` };
106
- }
107
- const dir = overlayDir(target, adapter);
108
- state.mutations.push({ type: 'file', action: 'created', path: dir });
109
- // First-install notice → stderr (human) / mutations[] only (json; stdout clean).
110
- if (!json) process.stderr.write(`installed mm-harness overlay → ${dir}\n`);
111
- return { ok: true };
112
- }
113
-
114
- // A recovery that would need a seeded wallet is NEVER auto-performed (healing
115
- // never touches fixtures). Only POSITIVELY-IDENTIFIED infra transport failures
116
- // are healable. Unknown/unclassified failures → 'app' (safe default: no healing
117
- // attempted, failure surfaced verbatim). Defaulting to 'infra' would cause
118
- // self-healing to mask real app-logic breakage.
119
- export function classifyFailure(output: string): 'wallet' | 'infra' | 'app' {
120
- if (/wallet|fixture|keyring|not seeded|\bsrp\b|password|onboard/iu.test(output)) return 'wallet';
121
- if (/metro|cdp|chrome|bundle|packager|port\b|econnrefused|not reachable|watcher|dev client|websocket/iu.test(output)) {
122
- return 'infra';
123
- }
124
- return 'app';
125
- }
126
-
127
- // Refuse ALL recovery while a recipe is executing (mid-run recovery would corrupt
128
- // state). Signalled by a lock file or MM_HARNESS_RECIPE_RUNNING=1.
129
- export function recipeRunning(target: string): boolean {
130
- if (process.env.MM_HARNESS_RECIPE_RUNNING === '1') return true;
131
- return fs.existsSync(recipeRuntimePath(target, 'recipe.lock'));
132
- }
133
-
134
- // Stable recovery codes for the launch infra retry (surfaced in recovered[]).
135
- export const RECOVERY_CODE: Record<DeviceAdapter, string> = {
136
- mobile: 'metro.restarted',
137
- extension: 'chrome.reopened',
138
- };
139
-
140
- export interface HealBoundViolation {
141
- code: string;
142
- exitCode: number;
143
- message: string;
144
- userAction?: string;
145
- // The ORIGINAL failure output that drove this classification, carried verbatim.
146
- // The classification `message` accompanies it — it NEVER replaces it: an
147
- // app-logic assertion like "expected balance 10, got 7" must survive so the
148
- // human/agent sees the real cause, not just the note.
149
- originalError?: string;
150
- }
151
-
152
- // Returns null (ok to proceed) or a violation descriptor when a bound is hit.
153
- // Using null instead of { ok: true } keeps the return typeof-narrowable without
154
- // relying on discriminated-union narrowing (avoids strict-mode tsconfig issues).
155
- export function checkHealBounds(
156
- target: string,
157
- output: string,
158
- state: HealState,
159
- ): HealBoundViolation | null {
160
- // Carry the original failure output verbatim on every violation so the caller
161
- // can surface it (--json + human) alongside the classification note.
162
- const originalError = output.trim() || undefined;
163
- if (recipeRunning(target)) {
164
- return {
165
- code: 'RECIPE_RUNNING',
166
- exitCode: EXIT.bounded,
167
- message: 'a recipe is currently running — refusing recovery to avoid corrupting mid-run state.',
168
- originalError,
169
- };
170
- }
171
- const failureClass = classifyFailure(output);
172
- if (failureClass === 'wallet') {
173
- return {
174
- code: 'WALLET_STATE_REQUIRED',
175
- exitCode: EXIT.bounded,
176
- message: 'recovery would require a seeded wallet — healing never touches fixtures.',
177
- userAction: 'run mm-harness fixtures set',
178
- originalError,
179
- };
180
- }
181
- if (failureClass === 'app') {
182
- return {
183
- code: 'APP_LOGIC_FAILURE',
184
- exitCode: EXIT.runtime,
185
- message: 'failure looks like app-logic — healing cannot help; surface verbatim.',
186
- originalError,
187
- };
188
- }
189
- if (state.attemptedRecoveries.length > 0) {
190
- return {
191
- code: 'SAME_RECOVERY_TWICE',
192
- exitCode: EXIT.bounded,
193
- message: 'same recovery already failed once this invocation — refusing to loop.',
194
- originalError,
195
- };
196
- }
197
- return null;
198
- }
package/src/index.ts DELETED
@@ -1,15 +0,0 @@
1
- // Keep the package entrypoint narrow: runner factory, manifest, doctor, and the
2
- // path/import helpers that library actions are allowed to consume (R9 — no
3
- // deep-importing src internals past this file).
4
- export { createDoctorReport } from './doctor.ts';
5
- export { loadActionManifest, validateManifest } from './manifest.ts';
6
- export { createMetaMaskExtensionRunner, createMetaMaskMobileRunner, createMetaMaskRunner } from './runner.ts';
7
- export type { CreateMetaMaskRunnerOptions, MetaMaskDoctorReport, MetaMaskRecipeAdapter } from './types.ts';
8
- export {
9
- extensionIdPath,
10
- importRecipeHarnessRuntimeBrowserExtension,
11
- importRecipeHarnessRuntimeCdp,
12
- recipeHarnessPath,
13
- walletFixturePath,
14
- } from './paths.ts';
15
- export { captureActiveRecipeRecordingSnapshot } from './run-recording.ts';
@@ -1,28 +0,0 @@
1
- // Interpreter routing for adapter shell and node leaves.
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
-
5
- // Returns the interpreter and args for invoking a leaf. Shell leaves (.sh) run
6
- // through bash so the file mode does not need to be executable. Node leaves
7
- // are already invoked through process.execPath by their callers and are
8
- // returned unchanged.
9
- export function resolveLeafInvoke(command: string, args: string[]): { bin: string; args: string[] } {
10
- if (command.endsWith('.sh')) return { bin: 'bash', args: [command, ...args] };
11
- return { bin: command, args };
12
- }
13
-
14
- // Teaching diagnostic for a shell leaf that is absent from disk. Bash exits
15
- // 127 (not a Node spawn error) for a missing file, so callers pre-check and
16
- // emit this rather than relying on result.error.
17
- export function missingShellLeafMessage(leafPath: string): string {
18
- const leaf = path.basename(leafPath);
19
- return (
20
- `leaf could not start: ${leaf} (ENOENT)\n` +
21
- ` Next: reinstall mm-harness (npm i -g @deeeed/metamask-harness) — the shell leaf is missing or not executable`
22
- );
23
- }
24
-
25
- // Returns true when a .sh leaf path does not exist on disk.
26
- export function shellLeafMissing(leafPath: string): boolean {
27
- return leafPath.endsWith('.sh') && !fs.existsSync(leafPath);
28
- }
@@ -1,274 +0,0 @@
1
- import { access, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
2
- import { existsSync, readFileSync } from 'node:fs';
3
- import os from 'node:os';
4
- import path from 'node:path';
5
- import { spawn } from 'node:child_process';
6
-
7
- import { resolveRequiredLocalProtocolRoot, runnerDir } from './paths.ts';
8
-
9
- function actionFileStem(action: string) {
10
- return String(action).replace(/[^a-zA-Z0-9._-]/g, '_');
11
- }
12
-
13
- function actionParts(action: string) {
14
- const parts = String(action).split('.').filter(Boolean);
15
- if (parts.length >= 3 && parts[0] === 'metamask') {
16
- return { family: parts[1], localName: parts.slice(2).join('.') };
17
- }
18
- if (parts.length >= 2) {
19
- return { family: parts[0], localName: parts.slice(1).join('.') };
20
- }
21
- return { family: 'actions', localName: String(action) };
22
- }
23
-
24
- function candidateStems(action: string) {
25
- const stem = actionFileStem(action);
26
- const { localName } = actionParts(action);
27
- const localStem = actionFileStem(localName);
28
- const stems = [stem];
29
- if (localStem && localStem !== stem) stems.push(localStem);
30
- return stems;
31
- }
32
-
33
- function candidateFamilies(action: string) {
34
- const { family } = actionParts(action);
35
- const families = [family];
36
- return [...new Set(families)];
37
- }
38
-
39
- function candidatePaths(platform: string, action: string) {
40
- const stems = candidateStems(action);
41
- const families = candidateFamilies(action);
42
- const roots = [
43
- process.env.METAMASK_RECIPE_LIVE_ADAPTER_DIR,
44
- path.join(runnerDir, 'library/actions'),
45
- ].filter(Boolean);
46
- const files = [];
47
- for (const root of roots) {
48
- for (const family of families) {
49
- for (const candidateStem of stems) {
50
- pushCandidateFiles(files, root, platform, family, candidateStem);
51
- }
52
- pushDomainDispatcherFiles(files, root, platform, family);
53
- }
54
- for (const candidateStem of stems) {
55
- pushFlatCandidateFiles(files, root, platform, candidateStem);
56
- }
57
- }
58
- return files;
59
- }
60
-
61
- function pushCandidateFiles(files: string[], root: string, platform: string, family: string, stem: string) {
62
- for (const extension of ['mjs', 'js', 'sh']) {
63
- files.push(path.join(root, platform, family, `${stem}.${extension}`));
64
- files.push(path.join(root, 'shared', family, `${stem}.${extension}`));
65
- }
66
- }
67
-
68
-
69
- function pushDomainDispatcherFiles(files: string[], root: string, platform: string, family: string) {
70
- for (const extension of ['mjs', 'js', 'sh']) {
71
- files.push(path.join(root, platform, family, `${family}.${extension}`));
72
- files.push(path.join(root, 'shared', family, `${family}.${extension}`));
73
- }
74
- }
75
-
76
- function pushFlatCandidateFiles(files: string[], root: string, platform: string, stem: string) {
77
- for (const extension of ['mjs', 'js', 'sh']) {
78
- files.push(path.join(root, platform, `${stem}.${extension}`));
79
- files.push(path.join(root, 'shared', `${stem}.${extension}`));
80
- }
81
- }
82
-
83
- async function firstExecutablePath(paths: string[]) {
84
- for (const file of paths) {
85
- try {
86
- await access(file);
87
- return file;
88
- } catch (error) {
89
- // Try the next candidate path. Missing optional live adapters are reported by the caller.
90
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
91
- }
92
- }
93
- return null;
94
- }
95
-
96
- function runProcess(
97
- command: string,
98
- args: string[],
99
- options: { cwd: string; env: NodeJS.ProcessEnv; timeoutMs?: number },
100
- ): Promise<{ exitCode: number | null; stdout: string; stderr: string; timedOut: boolean }> {
101
- return new Promise((resolve, reject) => {
102
- let settled = false;
103
- const child = spawn(command, args, {
104
- cwd: options.cwd,
105
- env: options.env,
106
- stdio: ['ignore', 'pipe', 'pipe'],
107
- });
108
- let stdout = '';
109
- let stderr = '';
110
- child.stdout.on('data', (chunk) => {
111
- stdout += chunk;
112
- });
113
- child.stderr.on('data', (chunk) => {
114
- stderr += chunk;
115
- });
116
- const timeout = options.timeoutMs
117
- ? setTimeout(() => {
118
- if (settled) return;
119
- settled = true;
120
- child.kill('SIGTERM');
121
- setTimeout(() => {
122
- if (!child.killed) child.kill('SIGKILL');
123
- }, 1000);
124
- resolve({ exitCode: null, stdout, stderr, timedOut: true });
125
- }, options.timeoutMs)
126
- : undefined;
127
- child.on('error', (error) => {
128
- if (settled) return;
129
- settled = true;
130
- if (timeout) clearTimeout(timeout);
131
- reject(error);
132
- });
133
- child.on('close', (exitCode) => {
134
- if (settled) return;
135
- settled = true;
136
- if (timeout) clearTimeout(timeout);
137
- resolve({ exitCode, stdout, stderr, timedOut: false });
138
- });
139
- });
140
- }
141
-
142
- function commandFor(file: string) {
143
- if (file.endsWith('.sh')) return { command: 'bash', args: [file] };
144
- if (file.endsWith('.mjs') || file.endsWith('.js')) {
145
- if (!importsSourceTypescript(file)) return { command: process.execPath, args: [file] };
146
- }
147
- const localTsx = path.join(runnerDir, 'node_modules/.bin/tsx');
148
- const tsxBin =
149
- process.env.TSX_BIN ||
150
- (existsSync(localTsx)
151
- ? localTsx
152
- : path.join(
153
- resolveRequiredLocalProtocolRoot('TypeScript live adapter execution'),
154
- 'node_modules/.bin/tsx',
155
- ));
156
- return { command: tsxBin, args: [file] };
157
- }
158
-
159
- function importsSourceTypescript(file: string): boolean {
160
- return importsSourceTypescriptFrom(file, new Set());
161
- }
162
-
163
- function importsSourceTypescriptFrom(file: string, visited: Set<string>): boolean {
164
- const absolute = path.resolve(file);
165
- if (visited.has(absolute)) return false;
166
- visited.add(absolute);
167
- const source = readFileSync(file, 'utf8');
168
- const importPattern = /(?:from\s+|import\(\s*)['"]([^'"]+)['"]/gu;
169
- for (const match of source.matchAll(importPattern)) {
170
- const specifier = match[1] ?? '';
171
- if (specifier.endsWith('.ts')) return true;
172
- if (!specifier.startsWith('.')) continue;
173
- if (!specifier.endsWith('.mjs') && !specifier.endsWith('.js')) continue;
174
- if (importsSourceTypescriptFrom(path.resolve(path.dirname(absolute), specifier), visited)) {
175
- return true;
176
- }
177
- }
178
- return false;
179
- }
180
-
181
- export async function resolveLiveAdapter(platform: string, action: string) {
182
- return firstExecutablePath(candidatePaths(platform, action));
183
- }
184
-
185
- // Workspace packages the headless core adapter imports as runtime values. In an
186
- // unbuilt MetaMask/core checkout these have no dist/, so their package "exports"
187
- // (which point only at dist) cannot resolve. We point tsx at the package src via
188
- // a generated tsconfig paths map so the adapter can import the controller without
189
- // building or modifying the core checkout.
190
- const CORE_WORKSPACE_PACKAGES = [
191
- '@metamask/base-controller',
192
- '@metamask/messenger',
193
- '@metamask/controller-utils',
194
- '@metamask/keyring-controller',
195
- ];
196
-
197
- async function platformAdapterEnv(
198
- platform: string,
199
- projectRoot: string,
200
- tempDir: string,
201
- ): Promise<NodeJS.ProcessEnv> {
202
- if (platform !== 'core') return {};
203
- const paths: Record<string, string[]> = {};
204
- for (const pkg of CORE_WORKSPACE_PACKAGES) {
205
- const packageDir = path.join(projectRoot, 'packages', pkg.replace('@metamask/', ''));
206
- const distEntry = path.join(packageDir, 'dist/index.cjs');
207
- if (existsSync(distEntry)) continue; // built package resolves normally
208
- const srcDir = path.join(packageDir, 'src');
209
- if (!existsSync(path.join(srcDir, 'index.ts'))) continue;
210
- paths[pkg] = [path.join(srcDir, 'index.ts')];
211
- paths[`${pkg}/*`] = [path.join(srcDir, '*')];
212
- }
213
- if (Object.keys(paths).length === 0) return {};
214
- const tsconfigPath = path.join(tempDir, 'core-adapter.tsconfig.json');
215
- await writeFile(
216
- tsconfigPath,
217
- `${JSON.stringify({ compilerOptions: { baseUrl: projectRoot, paths } }, null, 2)}\n`,
218
- );
219
- return { TSX_TSCONFIG_PATH: tsconfigPath };
220
- }
221
-
222
- export async function runLiveAdapterScript({ platform, action, node, context }: { platform: string; action: string; node: Record<string, any>; context: any }) {
223
- const script = await resolveLiveAdapter(platform, action);
224
- if (!script) return null;
225
-
226
- const tempDir = await mkdtemp(path.join(os.tmpdir(), 'mm-harness-live-adapter-'));
227
- const inputPath = path.join(tempDir, 'input.json');
228
- const outputPath = path.join(tempDir, 'output.json');
229
- const input = {
230
- schemaVersion: 1,
231
- platform,
232
- action,
233
- node,
234
- context: {
235
- nodeId: context.nodeId,
236
- projectRoot: context.projectRoot,
237
- artifactsDir: context.artifactsDir,
238
- },
239
- outputPath,
240
- };
241
- await writeFile(inputPath, `${JSON.stringify(input, null, 2)}\n`);
242
- const command = commandFor(script);
243
- const platformEnv = await platformAdapterEnv(platform, context.projectRoot, tempDir);
244
- const result = await runProcess(command.command, [...command.args, inputPath], {
245
- cwd: context.projectRoot,
246
- env: {
247
- ...process.env,
248
- ...context.env,
249
- ...platformEnv,
250
- METAMASK_RECIPE_ADAPTER_INPUT: inputPath,
251
- METAMASK_RECIPE_ADAPTER_OUTPUT: outputPath,
252
- },
253
- timeoutMs: Number(node.live_adapter_timeout_ms ?? node.timeout_ms ?? 60000),
254
- });
255
- try {
256
- if (result.timedOut) {
257
- throw new Error(`Live adapter ${script} timed out after ${Number(node.live_adapter_timeout_ms ?? node.timeout_ms ?? 60000)}ms.`);
258
- }
259
- if (result.exitCode !== 0) {
260
- throw new Error(`Live adapter ${script} exited ${result.exitCode}: ${result.stderr || result.stdout}`);
261
- }
262
- let parsed = null;
263
- try {
264
- parsed = JSON.parse(await readFile(outputPath, 'utf8'));
265
- } catch (_error) {
266
- const stdout = result.stdout.trim();
267
- if (!stdout) throw new Error(`Live adapter ${script} did not write JSON output.`);
268
- parsed = JSON.parse(stdout);
269
- }
270
- return { script, result: parsed };
271
- } finally {
272
- await rm(tempDir, { recursive: true, force: true });
273
- }
274
- }
package/src/manifest.ts DELETED
@@ -1,47 +0,0 @@
1
- import path from 'node:path';
2
-
3
- import type { RecipeActionManifestDocument, RecipeValidationResult } from '@farmslot/protocol';
4
-
5
- import { manifestPath, readJson, importRecipeProtocol } from './paths.ts';
6
- import type { MetaMaskRecipeAdapter } from './types.ts';
7
-
8
- export function loadMetaMaskMobileActionManifest(): RecipeActionManifestDocument {
9
- return asActionManifest(readJson(manifestPath('mobile')));
10
- }
11
-
12
- export function loadMetaMaskExtensionActionManifest(): RecipeActionManifestDocument {
13
- return asActionManifest(readJson(manifestPath('extension')));
14
- }
15
-
16
- export function loadMetaMaskCoreActionManifest(): RecipeActionManifestDocument {
17
- return asActionManifest(readJson(manifestPath('core')));
18
- }
19
-
20
- export function loadActionManifest(
21
- adapter: MetaMaskRecipeAdapter,
22
- overridePath?: string,
23
- ): RecipeActionManifestDocument {
24
- if (overridePath) return asActionManifest(readJson(path.resolve(overridePath)));
25
- if (adapter === 'mobile') return loadMetaMaskMobileActionManifest();
26
- if (adapter === 'core') return loadMetaMaskCoreActionManifest();
27
- return loadMetaMaskExtensionActionManifest();
28
- }
29
-
30
- export async function validateManifest(
31
- manifest: RecipeActionManifestDocument,
32
- ): Promise<RecipeValidationResult> {
33
- const { validateRecipeActionManifestDocument } = await importRecipeProtocol();
34
- const result = validateRecipeActionManifestDocument(manifest);
35
- if (result.status === 'invalid') {
36
- throw new Error(
37
- `Manifest invalid: ${result.findings
38
- .map((finding) => `${finding.code} ${finding.path}`)
39
- .join(', ')}`,
40
- );
41
- }
42
- return result;
43
- }
44
-
45
- function asActionManifest(value: unknown): RecipeActionManifestDocument {
46
- return value as RecipeActionManifestDocument;
47
- }