@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,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
- }