@deeeed/metamask-harness 0.5.1 → 0.6.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.
- package/CHANGELOG.md +47 -20
- package/adapters/core/inject.sh +1 -0
- package/adapters/extension/cleanup.mjs +1 -1
- package/adapters/extension/inject.mjs +2 -1
- package/adapters/extension/launch-browser.cjs +18 -4
- package/adapters/extension/live.sh +14 -2
- package/adapters/extension/sidepanel-toggle.sh +33 -12
- package/adapters/extension/wallet-fixture-state.cjs +38 -24
- package/adapters/manifest.json +13 -0
- package/adapters/mobile/inject.sh +1 -0
- package/adapters/mobile/start-metro.sh +6 -0
- package/adapters/mobile/stop-metro.sh +20 -0
- package/adapters/shared/cli-ux.sh +20 -2
- package/adapters/shared/ensure-runner-deps.sh +30 -0
- package/adapters/shared/reap-checkout-metros.sh +58 -0
- package/adapters/shared/recipe-harness-root.mjs +23 -0
- package/adapters/shared/resolve-farmslot-ports-core.mjs +15 -6
- package/bin/mm-harness +45 -4
- package/dist/adapters/extension/runtime.js +3 -1
- package/dist/adapters/mobile/provision.js +34 -1
- package/dist/adapters/slot-ports.js +3 -8
- package/dist/cli.js +32 -1463
- package/dist/commands/call.js +183 -0
- package/dist/commands/completion-candidates.js +58 -0
- package/dist/commands/doctor.js +101 -0
- package/dist/commands/ensure-ready.js +24 -0
- package/dist/commands/flows.js +62 -0
- package/dist/commands/launch/extension.js +40 -0
- package/dist/commands/{launch.js → launch/index.js} +15 -47
- package/dist/commands/launch/mobile.js +10 -0
- package/dist/commands/manifest.js +72 -0
- package/dist/commands/parse-args.js +189 -0
- package/dist/commands/provision.js +136 -0
- package/dist/commands/resolve-extension.js +23 -0
- package/dist/commands/run-engine.js +341 -0
- package/dist/commands/run.js +217 -0
- package/dist/commands/runtime-decision.js +58 -0
- package/dist/commands/runtime-health.js +25 -0
- package/dist/commands/runtime-launch.js +139 -0
- package/dist/commands/self-test.js +52 -0
- package/dist/commands/stop.js +52 -0
- package/dist/harness.js +8 -48
- package/dist/mm-harness-cli.js +13 -8
- package/docs/CLI-SPEC.md +1 -1
- package/docs/CODE-MAP.md +62 -0
- package/library/README.md +14 -0
- package/library/actions/extension/platform/cdp.mjs +1 -1
- package/library/actions/extension/wallet/ensure_unlocked.mjs +6 -0
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +13 -1
- package/package.json +6 -7
- package/src/adapters/core/surface.ts +0 -71
- package/src/adapters/extension/ensure-ready.ts +0 -185
- package/src/adapters/extension/extension-id.ts +0 -107
- package/src/adapters/extension/runtime-decision.ts +0 -445
- package/src/adapters/extension/runtime.ts +0 -407
- package/src/adapters/extension/surface.ts +0 -88
- package/src/adapters/mobile/deps-markers.ts +0 -21
- package/src/adapters/mobile/prepare.ts +0 -246
- package/src/adapters/mobile/provision.ts +0 -594
- package/src/adapters/mobile/runtime-decision.ts +0 -466
- package/src/adapters/mobile/surface.ts +0 -71
- package/src/adapters/resolve-farmslot-ports.ts +0 -13
- package/src/adapters/slot-ports.ts +0 -158
- package/src/adapters/surface.ts +0 -117
- package/src/adapters.ts +0 -601
- package/src/cli-color.ts +0 -92
- package/src/cli-commands.ts +0 -250
- package/src/cli-version.ts +0 -141
- package/src/cli.ts +0 -2091
- package/src/commands/debug.ts +0 -65
- package/src/commands/fixtures.ts +0 -198
- package/src/commands/launch.ts +0 -470
- package/src/commands/logs.ts +0 -99
- package/src/commands/shared.ts +0 -235
- package/src/commands/update.ts +0 -316
- package/src/completions-cache.ts +0 -86
- package/src/doctor.ts +0 -215
- package/src/harness.ts +0 -797
- package/src/heal-bounds.ts +0 -198
- package/src/index.ts +0 -15
- package/src/leaf-invoke.ts +0 -28
- package/src/live-adapter-contract.ts +0 -274
- package/src/manifest.ts +0 -47
- package/src/mm-harness-cli.ts +0 -655
- package/src/paths.ts +0 -198
- package/src/progress.ts +0 -117
- package/src/recording-target.ts +0 -147
- package/src/run-recording.ts +0 -329
- package/src/runner.ts +0 -108
- package/src/types.ts +0 -57
package/src/heal-bounds.ts
DELETED
|
@@ -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';
|
package/src/leaf-invoke.ts
DELETED
|
@@ -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
|
-
}
|