@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.
- package/CHANGELOG.md +37 -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/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 +53 -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
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { detectAdapter } from "../harness.js";
|
|
3
|
+
import { assertAdapter, manifestPath } from "../paths.js";
|
|
4
|
+
import { ADAPTER_DETECT_NEXT } from "./shared.js";
|
|
5
|
+
import { EXIT } from "./shared.js";
|
|
6
|
+
class CliError extends Error {
|
|
7
|
+
exitCode;
|
|
8
|
+
constructor(message, exitCode) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "CliError";
|
|
11
|
+
this.exitCode = exitCode;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function usageError(message) {
|
|
15
|
+
return new CliError(message, EXIT.usage);
|
|
16
|
+
}
|
|
17
|
+
function parseArgs(argv, command) {
|
|
18
|
+
const positional = [];
|
|
19
|
+
const options = {};
|
|
20
|
+
const booleanOptions = /* @__PURE__ */ new Set([
|
|
21
|
+
"json",
|
|
22
|
+
"launchExistingDist",
|
|
23
|
+
"startWatch",
|
|
24
|
+
"record",
|
|
25
|
+
"plan",
|
|
26
|
+
"raw",
|
|
27
|
+
"fix",
|
|
28
|
+
"force",
|
|
29
|
+
"resolveOnly",
|
|
30
|
+
"help"
|
|
31
|
+
]);
|
|
32
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
33
|
+
const arg = argv[i];
|
|
34
|
+
if (!arg.startsWith("--")) {
|
|
35
|
+
positional.push(arg);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const body = arg.slice(2);
|
|
39
|
+
const equalsIndex = body.indexOf("=");
|
|
40
|
+
const rawKey = equalsIndex === -1 ? body : body.slice(0, equalsIndex);
|
|
41
|
+
const inlineValue = equalsIndex === -1 ? void 0 : body.slice(equalsIndex + 1);
|
|
42
|
+
const key = normalizeOptionKey(rawKey);
|
|
43
|
+
if (key === "recordVideo") {
|
|
44
|
+
options.recordVideo = parseRecordVideoMode(inlineValue);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (key === "recordBaseline") {
|
|
48
|
+
options.record = true;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (key === "record") {
|
|
52
|
+
if (command === "runtime-decision") {
|
|
53
|
+
options.record = true;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
options.recordVideo = "full-run";
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (booleanOptions.has(key)) {
|
|
60
|
+
options[key] = true;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (inlineValue !== void 0) {
|
|
64
|
+
options[key] = inlineValue;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (i + 1 >= argv.length) throw usageError(`Missing value for ${arg}`);
|
|
68
|
+
options[key] = argv[i + 1];
|
|
69
|
+
i += 1;
|
|
70
|
+
}
|
|
71
|
+
return { positional, options, rawArgv: [...argv] };
|
|
72
|
+
}
|
|
73
|
+
function parseRecordVideoMode(value) {
|
|
74
|
+
if (value === void 0 || value === "" || value === "true") return "full-run";
|
|
75
|
+
if (value === "off" || value === "false") return false;
|
|
76
|
+
if (value === "proof-window" || value === "proof_window") {
|
|
77
|
+
throw usageError(
|
|
78
|
+
"--record-video=proof-window is not supported yet; use --record-video=full-run."
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (value !== "full-run") {
|
|
82
|
+
throw usageError("--record-video must be full-run or off.");
|
|
83
|
+
}
|
|
84
|
+
return "full-run";
|
|
85
|
+
}
|
|
86
|
+
function normalizeOptionKey(key) {
|
|
87
|
+
return key.replace(/-([a-z])/gu, (_, character) => character.toUpperCase());
|
|
88
|
+
}
|
|
89
|
+
function optionString(options, key) {
|
|
90
|
+
const value = options[key];
|
|
91
|
+
if (value === void 0) return void 0;
|
|
92
|
+
if (typeof value !== "string") throw usageError(`--${key} requires a value.`);
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
function optionFlag(options, key) {
|
|
96
|
+
const value = options[key];
|
|
97
|
+
return value === true;
|
|
98
|
+
}
|
|
99
|
+
function applyRuntimeDirOption(options) {
|
|
100
|
+
const runtimeDir = optionString(options, "runtimeDir");
|
|
101
|
+
if (runtimeDir) process.env.RECIPE_RUNTIME_DIR = runtimeDir;
|
|
102
|
+
}
|
|
103
|
+
function applyWatcherPortOption(options) {
|
|
104
|
+
const watcherPort = optionString(options, "watcherPort");
|
|
105
|
+
if (!watcherPort) return;
|
|
106
|
+
process.env.WATCHER_PORT = watcherPort;
|
|
107
|
+
process.env.RECIPE_WATCHER_PORT = watcherPort;
|
|
108
|
+
process.env.METRO_PORT = watcherPort;
|
|
109
|
+
}
|
|
110
|
+
function requiredOption(options, key, message) {
|
|
111
|
+
const value = optionString(options, key);
|
|
112
|
+
if (!value) throw usageError(message);
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
function adapterOption(options) {
|
|
116
|
+
const adapter = optionString(options, "adapter");
|
|
117
|
+
try {
|
|
118
|
+
assertAdapter(adapter);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
throw usageError(error instanceof Error ? error.message : String(error));
|
|
121
|
+
}
|
|
122
|
+
return adapter;
|
|
123
|
+
}
|
|
124
|
+
function targetPath(options) {
|
|
125
|
+
return path.resolve(optionString(options, "target") ?? optionString(options, "projectRoot") ?? process.cwd());
|
|
126
|
+
}
|
|
127
|
+
function actionManifestPathOption(options, adapter) {
|
|
128
|
+
const configured = optionString(options, "actionManifest");
|
|
129
|
+
return configured ? path.resolve(configured) : manifestPath(adapter);
|
|
130
|
+
}
|
|
131
|
+
function parsePort(value, errorMessage) {
|
|
132
|
+
const port = Number(value);
|
|
133
|
+
if (!Number.isInteger(port) || port <= 0) throw usageError(errorMessage);
|
|
134
|
+
return port;
|
|
135
|
+
}
|
|
136
|
+
function runtimeOptionsFromCli(options) {
|
|
137
|
+
const recordVideo = options.recordVideo;
|
|
138
|
+
return {
|
|
139
|
+
cdpPort: optionString(options, "cdpPort"),
|
|
140
|
+
watcherPort: optionString(options, "watcherPort") ?? optionString(options, "metroPort"),
|
|
141
|
+
launchExistingDist: optionFlag(options, "launchExistingDist"),
|
|
142
|
+
slot: optionString(options, "slot"),
|
|
143
|
+
validationRuntimeDir: optionString(options, "validationRuntimeDir"),
|
|
144
|
+
recordVideo: recordVideo === "full-run" ? "full-run" : false
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function resolveAdapter(options) {
|
|
148
|
+
const target = targetPath(options);
|
|
149
|
+
const explicit = optionString(options, "adapter") ?? optionString(options, "platform");
|
|
150
|
+
const adapter = explicit ?? detectAdapter(target);
|
|
151
|
+
if (!adapter) {
|
|
152
|
+
throw usageError(`could not detect the MetaMask repo type for ${target}
|
|
153
|
+
Next: ${ADAPTER_DETECT_NEXT}`);
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
assertAdapter(adapter);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
throw usageError(error instanceof Error ? error.message : String(error));
|
|
159
|
+
}
|
|
160
|
+
return { adapter, target };
|
|
161
|
+
}
|
|
162
|
+
function shellQuote(value) {
|
|
163
|
+
return `'${value.replace(/'/gu, `'\\''`)}'`;
|
|
164
|
+
}
|
|
165
|
+
function shellQuoteArg(value) {
|
|
166
|
+
return /^[A-Za-z0-9_./:=@+-]+$/u.test(value) ? value : shellQuote(value);
|
|
167
|
+
}
|
|
168
|
+
function isRecord(value) {
|
|
169
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
170
|
+
}
|
|
171
|
+
export {
|
|
172
|
+
CliError,
|
|
173
|
+
actionManifestPathOption,
|
|
174
|
+
adapterOption,
|
|
175
|
+
applyRuntimeDirOption,
|
|
176
|
+
applyWatcherPortOption,
|
|
177
|
+
isRecord,
|
|
178
|
+
optionFlag,
|
|
179
|
+
optionString,
|
|
180
|
+
parseArgs,
|
|
181
|
+
parsePort,
|
|
182
|
+
requiredOption,
|
|
183
|
+
resolveAdapter,
|
|
184
|
+
runtimeOptionsFromCli,
|
|
185
|
+
shellQuote,
|
|
186
|
+
shellQuoteArg,
|
|
187
|
+
targetPath,
|
|
188
|
+
usageError
|
|
189
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { detectAdapter } from "../harness.js";
|
|
2
|
+
import { assertAdapter } from "../paths.js";
|
|
3
|
+
import { getAdapterSurface } from "../adapters/surface.js";
|
|
4
|
+
import { ADAPTER_DETECT_NEXT, usageOut } from "./shared.js";
|
|
5
|
+
import {
|
|
6
|
+
applyWatcherPortOption,
|
|
7
|
+
optionFlag,
|
|
8
|
+
optionString,
|
|
9
|
+
shellQuote,
|
|
10
|
+
shellQuoteArg,
|
|
11
|
+
targetPath
|
|
12
|
+
} from "./parse-args.js";
|
|
13
|
+
async function handleProvision({ positional, options, rawArgv }) {
|
|
14
|
+
applyWatcherPortOption(options);
|
|
15
|
+
const json = optionFlag(options, "json");
|
|
16
|
+
const target = targetPath(options);
|
|
17
|
+
const adapter = optionString(options, "adapter") ?? detectAdapter(target);
|
|
18
|
+
if (!adapter) {
|
|
19
|
+
return usageOut(json, "provision", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
assertAdapter(adapter);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
return usageOut(json, "provision", error instanceof Error ? error.message : String(error), ADAPTER_DETECT_NEXT);
|
|
25
|
+
}
|
|
26
|
+
const surface = getAdapterSurface(adapter);
|
|
27
|
+
const rerunCommand = provisionRerunCommand(rawArgv, options, adapter, target);
|
|
28
|
+
const result = await surface.runwayProvision.run(target, {
|
|
29
|
+
json,
|
|
30
|
+
platform: optionString(options, "platform") ?? optionString(options, "devicePlatform") ?? (positional[0] === "runway" ? positional[1] : positional[0]) ?? "ios",
|
|
31
|
+
branch: optionString(options, "branch"),
|
|
32
|
+
defaultBranch: optionString(options, "defaultBranch"),
|
|
33
|
+
run: optionString(options, "run"),
|
|
34
|
+
cacheRoot: optionString(options, "cacheRoot"),
|
|
35
|
+
simulator: optionString(options, "simulator") ?? optionString(options, "device"),
|
|
36
|
+
runtime: optionString(options, "runtime"),
|
|
37
|
+
deviceType: optionString(options, "deviceType"),
|
|
38
|
+
slot: optionString(options, "slot"),
|
|
39
|
+
watcherPort: optionString(options, "watcherPort"),
|
|
40
|
+
runtimeDir: optionString(options, "runtimeDir"),
|
|
41
|
+
force: optionFlag(options, "force"),
|
|
42
|
+
resolveOnly: optionFlag(options, "resolveOnly"),
|
|
43
|
+
rerunCommand
|
|
44
|
+
});
|
|
45
|
+
if (json) {
|
|
46
|
+
console.log(JSON.stringify(result, null, 2));
|
|
47
|
+
} else if (result.status === "pass") {
|
|
48
|
+
const cache = typeof result.cache === "object" && result.cache ? result.cache : void 0;
|
|
49
|
+
const simulator = typeof result.simulator === "object" && result.simulator ? result.simulator : void 0;
|
|
50
|
+
const artifact = typeof result.artifact === "object" && result.artifact ? result.artifact : void 0;
|
|
51
|
+
if (result.resolveOnly) {
|
|
52
|
+
console.error(`\u2713 resolved ${adapter} ${result.platform ?? ""} run=${artifact?.runId ?? "unknown"} revision=${artifact?.revision ?? "unknown"} artifact=${artifact?.artifactName ?? "unknown"}`);
|
|
53
|
+
} else {
|
|
54
|
+
const action = result.skipped ? "already provisioned" : "provisioned";
|
|
55
|
+
console.error(`\u2713 ${action} ${adapter} ${result.platform ?? ""} simulator=${simulator?.name ?? "unknown"} cache=${cache?.status ?? "unknown"}`);
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
console.error(`\u2717 mm-harness provision: ${result.error?.message ?? "provision failed"}
|
|
59
|
+
Next: ${result.error?.userAction ?? rerunCommand}`);
|
|
60
|
+
}
|
|
61
|
+
return result.exitCode;
|
|
62
|
+
}
|
|
63
|
+
function provisionRerunCommand(rawArgv, options, adapter, target) {
|
|
64
|
+
const parts = ["mm-harness", "provision"];
|
|
65
|
+
for (const positional of provisionPositionals(rawArgv)) parts.push(shellQuoteArg(positional));
|
|
66
|
+
parts.push("--adapter", adapter, "--target", shellQuote(target));
|
|
67
|
+
const aliases = [
|
|
68
|
+
["platform", ["--platform", "--device-platform"]],
|
|
69
|
+
["branch", ["--branch"]],
|
|
70
|
+
["defaultBranch", ["--default-branch"]],
|
|
71
|
+
["run", ["--run"]],
|
|
72
|
+
["cacheRoot", ["--cache-root"]],
|
|
73
|
+
["simulator", ["--simulator", "--device"]],
|
|
74
|
+
["runtime", ["--runtime"]],
|
|
75
|
+
["deviceType", ["--device-type"]],
|
|
76
|
+
["slot", ["--slot"]],
|
|
77
|
+
["watcherPort", ["--watcher-port"]],
|
|
78
|
+
["runtimeDir", ["--runtime-dir"]]
|
|
79
|
+
];
|
|
80
|
+
for (const [key, flags] of aliases) {
|
|
81
|
+
const found = findRawOption(rawArgv, flags);
|
|
82
|
+
const value = found?.value ?? optionString(options, key);
|
|
83
|
+
if (value) parts.push(found?.flag ?? flags[0], shellQuoteArg(value));
|
|
84
|
+
}
|
|
85
|
+
if (optionFlag(options, "force")) parts.push("--force");
|
|
86
|
+
if (optionFlag(options, "resolveOnly")) parts.push("--resolve-only");
|
|
87
|
+
if (optionFlag(options, "json")) parts.push("--json");
|
|
88
|
+
return parts.join(" ");
|
|
89
|
+
}
|
|
90
|
+
function provisionPositionals(rawArgv) {
|
|
91
|
+
const positionals = [];
|
|
92
|
+
const valueFlags = /* @__PURE__ */ new Set([
|
|
93
|
+
"--adapter",
|
|
94
|
+
"--target",
|
|
95
|
+
"--project-root",
|
|
96
|
+
"--platform",
|
|
97
|
+
"--device-platform",
|
|
98
|
+
"--branch",
|
|
99
|
+
"--default-branch",
|
|
100
|
+
"--run",
|
|
101
|
+
"--cache-root",
|
|
102
|
+
"--simulator",
|
|
103
|
+
"--device",
|
|
104
|
+
"--runtime",
|
|
105
|
+
"--device-type",
|
|
106
|
+
"--slot",
|
|
107
|
+
"--watcher-port",
|
|
108
|
+
"--runtime-dir"
|
|
109
|
+
]);
|
|
110
|
+
for (let i = 0; i < rawArgv.length; i += 1) {
|
|
111
|
+
const arg = rawArgv[i];
|
|
112
|
+
if (!arg.startsWith("--")) {
|
|
113
|
+
positionals.push(arg);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const key = arg.includes("=") ? arg.slice(0, arg.indexOf("=")) : arg;
|
|
117
|
+
if (!arg.includes("=") && valueFlags.has(key)) i += 1;
|
|
118
|
+
}
|
|
119
|
+
return positionals;
|
|
120
|
+
}
|
|
121
|
+
function findRawOption(rawArgv, flags) {
|
|
122
|
+
for (let i = 0; i < rawArgv.length; i += 1) {
|
|
123
|
+
const arg = rawArgv[i];
|
|
124
|
+
for (const flag of flags) {
|
|
125
|
+
if (arg === flag) {
|
|
126
|
+
const value = rawArgv[i + 1];
|
|
127
|
+
return value && !value.startsWith("--") ? { flag, value } : void 0;
|
|
128
|
+
}
|
|
129
|
+
if (arg.startsWith(`${flag}=`)) return { flag, value: arg.slice(flag.length + 1) };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return void 0;
|
|
133
|
+
}
|
|
134
|
+
export {
|
|
135
|
+
handleProvision
|
|
136
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { resolveExtensionId } from "../adapters/extension/extension-id.js";
|
|
2
|
+
import {
|
|
3
|
+
adapterOption,
|
|
4
|
+
optionFlag,
|
|
5
|
+
optionString,
|
|
6
|
+
parsePort,
|
|
7
|
+
targetPath
|
|
8
|
+
} from "./parse-args.js";
|
|
9
|
+
async function handleResolveExtension({ options }) {
|
|
10
|
+
const adapter = adapterOption(options);
|
|
11
|
+
if (adapter !== "extension") throw new Error("resolve-extension currently applies to the extension adapter.");
|
|
12
|
+
const target = targetPath(options);
|
|
13
|
+
const cdpPortRaw = optionString(options, "cdpPort") ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT;
|
|
14
|
+
const cdpPort = cdpPortRaw === void 0 ? void 0 : parsePort(cdpPortRaw, "resolve-extension --cdp-port must be a port.");
|
|
15
|
+
const result = await resolveExtensionId(target, { cdpPort });
|
|
16
|
+
if (optionFlag(options, "json")) console.log(JSON.stringify(result, null, 2));
|
|
17
|
+
else if (result.extensionId) console.log(result.extensionId);
|
|
18
|
+
else console.error("Could not resolve a MetaMask extension id (no dist key and no single CDP extension).");
|
|
19
|
+
return result.extensionId ? 0 : 1;
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
handleResolveExtension
|
|
23
|
+
};
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { mkdtemp } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
ensureOverlay,
|
|
7
|
+
newHealState,
|
|
8
|
+
parseHeal,
|
|
9
|
+
recipeRunning,
|
|
10
|
+
checkHealBounds
|
|
11
|
+
} from "../heal-bounds.js";
|
|
12
|
+
import { loadActionManifest, validateManifest } from "../manifest.js";
|
|
13
|
+
import { getAdapterSurface } from "../adapters/surface.js";
|
|
14
|
+
import {
|
|
15
|
+
importRecipeHarness,
|
|
16
|
+
importRecipeProtocol,
|
|
17
|
+
runnerDir
|
|
18
|
+
} from "../paths.js";
|
|
19
|
+
import { captureHelperSupportsRecordSessionSnapshots } from "../recording-target.js";
|
|
20
|
+
import { startRecipeRecording, stopRecipeRecording } from "../run-recording.js";
|
|
21
|
+
import { EXIT } from "./shared.js";
|
|
22
|
+
import {
|
|
23
|
+
actionManifestPathOption,
|
|
24
|
+
optionString,
|
|
25
|
+
isRecord,
|
|
26
|
+
usageError
|
|
27
|
+
} from "./parse-args.js";
|
|
28
|
+
async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions = {}) {
|
|
29
|
+
const previousCdpPort = process.env.CDP_PORT;
|
|
30
|
+
const previousRecipeCdpPort = process.env.RECIPE_CDP_PORT;
|
|
31
|
+
const previousWatcherPort = process.env.WATCHER_PORT;
|
|
32
|
+
const previousMetroPort = process.env.METRO_PORT;
|
|
33
|
+
const previousExtensionAutolaunch = process.env.METAMASK_RECIPE_EXTENSION_AUTOLAUNCH;
|
|
34
|
+
getAdapterSurface(adapter).resolveSlotPorts(projectRoot);
|
|
35
|
+
if (runtimeOptions.cdpPort) {
|
|
36
|
+
process.env.CDP_PORT = runtimeOptions.cdpPort;
|
|
37
|
+
process.env.RECIPE_CDP_PORT = runtimeOptions.cdpPort;
|
|
38
|
+
}
|
|
39
|
+
if (runtimeOptions.watcherPort) {
|
|
40
|
+
process.env.WATCHER_PORT = runtimeOptions.watcherPort;
|
|
41
|
+
process.env.METRO_PORT = runtimeOptions.watcherPort;
|
|
42
|
+
}
|
|
43
|
+
if (runtimeOptions.launchExistingDist) {
|
|
44
|
+
process.env.METAMASK_RECIPE_EXTENSION_AUTOLAUNCH = "1";
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
await prepareRuntimeIfNeeded(adapter, projectRoot, runtimeOptions);
|
|
48
|
+
const absoluteArtifactsDir = path.resolve(artifactsDir);
|
|
49
|
+
const recordVideo = runtimeOptions.recordVideo ?? false;
|
|
50
|
+
const useFramedExtensionRecording = adapter === "extension" && recordVideo === "full-run" && captureHelperSupportsRecordSessionSnapshots(projectRoot);
|
|
51
|
+
const recording = useFramedExtensionRecording ? await startRecipeRecording(adapter, projectRoot, absoluteArtifactsDir, {
|
|
52
|
+
record: true,
|
|
53
|
+
cdpPort: runtimeOptions.cdpPort
|
|
54
|
+
}) : void 0;
|
|
55
|
+
try {
|
|
56
|
+
const manifest = loadActionManifest(adapter, actionManifestPath);
|
|
57
|
+
await validateManifest(manifest);
|
|
58
|
+
const { createMetaMaskRunner } = await import("../runner.js");
|
|
59
|
+
const runner = await createMetaMaskRunner(adapter, manifest, {
|
|
60
|
+
quietStdout: runtimeOptions.stdoutIsMachineContract === true
|
|
61
|
+
});
|
|
62
|
+
const runRequest = {
|
|
63
|
+
recipePath: path.resolve(recipe),
|
|
64
|
+
artifactsDir: absoluteArtifactsDir,
|
|
65
|
+
projectRoot,
|
|
66
|
+
env: recipeRunEnv(adapter, runtimeOptions),
|
|
67
|
+
recordVideo: useFramedExtensionRecording ? false : recordVideo,
|
|
68
|
+
...runtimeOptions.librarySources ? { librarySources: runtimeOptions.librarySources } : {}
|
|
69
|
+
};
|
|
70
|
+
const result = await runner.run(runRequest);
|
|
71
|
+
await stopRecipeRecording(recording, result);
|
|
72
|
+
return result;
|
|
73
|
+
} finally {
|
|
74
|
+
await stopRecipeRecording(recording);
|
|
75
|
+
}
|
|
76
|
+
} finally {
|
|
77
|
+
restoreEnv("CDP_PORT", previousCdpPort);
|
|
78
|
+
restoreEnv("RECIPE_CDP_PORT", previousRecipeCdpPort);
|
|
79
|
+
restoreEnv("WATCHER_PORT", previousWatcherPort);
|
|
80
|
+
restoreEnv("METRO_PORT", previousMetroPort);
|
|
81
|
+
restoreEnv("METAMASK_RECIPE_EXTENSION_AUTOLAUNCH", previousExtensionAutolaunch);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function recipeRunEnv(adapter, runtimeOptions = {}) {
|
|
85
|
+
const base = {
|
|
86
|
+
CDP_PORT: runtimeOptions.cdpPort ?? process.env.CDP_PORT,
|
|
87
|
+
RECIPE_CDP_PORT: runtimeOptions.cdpPort ?? process.env.RECIPE_CDP_PORT,
|
|
88
|
+
FARMSLOT_SLOT_ID: runtimeOptions.slot ?? process.env.FARMSLOT_SLOT_ID,
|
|
89
|
+
SLOT_ID: runtimeOptions.slot ?? process.env.SLOT_ID,
|
|
90
|
+
PLATFORM: process.env.PLATFORM
|
|
91
|
+
};
|
|
92
|
+
if (adapter !== "mobile") return base;
|
|
93
|
+
return {
|
|
94
|
+
...base,
|
|
95
|
+
WATCHER_PORT: process.env.WATCHER_PORT ?? base.CDP_PORT ?? base.RECIPE_CDP_PORT,
|
|
96
|
+
METRO_PORT: process.env.METRO_PORT ?? process.env.WATCHER_PORT ?? base.CDP_PORT ?? base.RECIPE_CDP_PORT,
|
|
97
|
+
IOS_SIMULATOR: process.env.IOS_SIMULATOR,
|
|
98
|
+
ANDROID_DEVICE: process.env.ANDROID_DEVICE,
|
|
99
|
+
ADB_SERIAL: process.env.ADB_SERIAL,
|
|
100
|
+
ANDROID_SERIAL: process.env.ANDROID_SERIAL
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
async function prepareRuntimeIfNeeded(adapter, projectRoot, runtimeOptions) {
|
|
104
|
+
if (adapter !== "extension" || runtimeOptions.skipExtensionRuntimePrepare === true) return;
|
|
105
|
+
const { prepareExtensionRuntime } = await import("../adapters/extension/runtime.js");
|
|
106
|
+
await prepareExtensionRuntime({
|
|
107
|
+
projectRoot,
|
|
108
|
+
cdpPort: runtimeOptions.cdpPort,
|
|
109
|
+
slot: runtimeOptions.slot,
|
|
110
|
+
launchExistingDist: runtimeOptions.launchExistingDist === true,
|
|
111
|
+
validationRuntimeDir: runtimeOptions.validationRuntimeDir
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function restoreEnv(key, value) {
|
|
115
|
+
if (value === void 0) delete process.env[key];
|
|
116
|
+
else process.env[key] = value;
|
|
117
|
+
}
|
|
118
|
+
async function validateRecipeAdapterAware(recipe, manifest, librarySources) {
|
|
119
|
+
const { validateRecipeDocument, validateRecipeWithManifest } = await importRecipeProtocol();
|
|
120
|
+
let externalFlowIds;
|
|
121
|
+
if (librarySources && librarySources.length > 0) {
|
|
122
|
+
const harness = await importRecipeHarness();
|
|
123
|
+
const resolution = await harness.loadRecipeLibraries(librarySources);
|
|
124
|
+
externalFlowIds = new Set(resolution.flows.keys());
|
|
125
|
+
}
|
|
126
|
+
const validationOptions = externalFlowIds !== void 0 ? { externalFlowIds } : void 0;
|
|
127
|
+
const schema = validateRecipeDocument(recipe, validationOptions);
|
|
128
|
+
const withManifest = validateRecipeWithManifest(recipe, manifest, validationOptions);
|
|
129
|
+
const findings = [...schema.findings, ...withManifest.findings];
|
|
130
|
+
const errors = findings.filter((finding) => finding.severity === "error").length;
|
|
131
|
+
const warnings = findings.length - errors;
|
|
132
|
+
return { status: errors > 0 ? "invalid" : "valid", findings, summary: { errors, warnings } };
|
|
133
|
+
}
|
|
134
|
+
async function validateRunRecipeStatic(recipeArg, adapter, options) {
|
|
135
|
+
const recipeFile = path.resolve(recipeArg);
|
|
136
|
+
const empty = { recipe: void 0, recipeFile, findings: [], errorCount: 0, manifestOk: false, schemaValid: false };
|
|
137
|
+
if (!fs.existsSync(recipeFile)) {
|
|
138
|
+
return { ...empty, usageError: { code: "RECIPE_NOT_FOUND", message: `recipe not found: ${recipeFile}` } };
|
|
139
|
+
}
|
|
140
|
+
let recipe;
|
|
141
|
+
try {
|
|
142
|
+
recipe = JSON.parse(fs.readFileSync(recipeFile, "utf8"));
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return {
|
|
145
|
+
...empty,
|
|
146
|
+
usageError: {
|
|
147
|
+
code: "RECIPE_UNPARSEABLE",
|
|
148
|
+
message: `recipe is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const findings = [];
|
|
153
|
+
const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
|
|
154
|
+
let manifestOk = true;
|
|
155
|
+
try {
|
|
156
|
+
await validateManifest(manifest);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
manifestOk = false;
|
|
159
|
+
findings.push({
|
|
160
|
+
severity: "error",
|
|
161
|
+
code: "manifest.invalid",
|
|
162
|
+
path: actionManifestPathOption(options, adapter),
|
|
163
|
+
message: error instanceof Error ? error.message : String(error)
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
|
|
167
|
+
const validation = manifestOk ? await validateRecipeAdapterAware(recipe, manifest, librarySources) : { status: "invalid", findings: [], summary: { errors: 1, warnings: 0 } };
|
|
168
|
+
findings.push(...validation.findings);
|
|
169
|
+
const errorCount = findings.filter((finding) => finding.severity === "error").length;
|
|
170
|
+
return { recipe, recipeFile, findings, errorCount, manifestOk, schemaValid: validation.status === "valid" };
|
|
171
|
+
}
|
|
172
|
+
async function resolveMetaMaskLibrarySources(libraryEntry) {
|
|
173
|
+
const harness = await importRecipeHarness();
|
|
174
|
+
if (typeof harness.resolveRecipeLibrarySources !== "function") {
|
|
175
|
+
if (libraryEntry) {
|
|
176
|
+
throw usageError(
|
|
177
|
+
"--library requires @farmslot/recipe-harness >= 0.3.3 (recipe-library support / resolveRecipeLibrarySources). The pinned 0.3.0 lacks it, and npm 0.3.2 still does not export it \u2014 recipe-library support is PENDING PUBLISH from farmslot."
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return void 0;
|
|
181
|
+
}
|
|
182
|
+
const sources = await harness.resolveRecipeLibrarySources(
|
|
183
|
+
libraryEntry ? { cliEntries: [libraryEntry] } : void 0
|
|
184
|
+
);
|
|
185
|
+
sources.push({ name: "metamask", root: path.join(runnerDir, "library") });
|
|
186
|
+
return sources;
|
|
187
|
+
}
|
|
188
|
+
function synthesizeOneNodeRecipe(action, args) {
|
|
189
|
+
return {
|
|
190
|
+
schema_version: 1,
|
|
191
|
+
title: `mm-harness call ${action}`,
|
|
192
|
+
description: `Ad-hoc single-action execution of ${action} via the real engine path (mm-harness call).`,
|
|
193
|
+
validate: {
|
|
194
|
+
workflow: {
|
|
195
|
+
entry: "call",
|
|
196
|
+
nodes: {
|
|
197
|
+
call: { action, ...args, next: "done", intent: `Call ${action} in isolation` },
|
|
198
|
+
done: { action: "end", status: "pass" }
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
async function runOneNode(adapter, action, args, target, actionManifest) {
|
|
205
|
+
const manifest = loadActionManifest(adapter, actionManifest);
|
|
206
|
+
const recipe = synthesizeOneNodeRecipe(action, args);
|
|
207
|
+
const validation = await validateRecipeAdapterAware(recipe, manifest);
|
|
208
|
+
if (validation.status === "invalid") return { status: "fail" };
|
|
209
|
+
const scratch = await mkdtemp(path.join(os.tmpdir(), "mm-harness-fixtures-"));
|
|
210
|
+
const recipeFile = path.join(scratch, "set.recipe.json");
|
|
211
|
+
fs.writeFileSync(recipeFile, `${JSON.stringify(recipe, null, 2)}
|
|
212
|
+
`);
|
|
213
|
+
const result = await runRecipe(adapter, recipeFile, path.join(scratch, "artifacts"), target, actionManifest, {
|
|
214
|
+
librarySources: await resolveMetaMaskLibrarySources(void 0)
|
|
215
|
+
});
|
|
216
|
+
return { status: result.status === "pass" ? "pass" : "fail" };
|
|
217
|
+
}
|
|
218
|
+
async function prepareHeal(adapter, target, options, json) {
|
|
219
|
+
if (recipeRunning(target)) {
|
|
220
|
+
const msg = "a recipe is currently running \u2014 refusing to start while another recipe executes.";
|
|
221
|
+
if (json) {
|
|
222
|
+
console.log(JSON.stringify({ schemaVersion: 1, status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message: msg } }, null, 2));
|
|
223
|
+
} else {
|
|
224
|
+
console.error(`\u2717 mm-harness: ${msg}`);
|
|
225
|
+
}
|
|
226
|
+
return EXIT.bounded;
|
|
227
|
+
}
|
|
228
|
+
const healValue = optionString(options, "heal");
|
|
229
|
+
const healOpts = {};
|
|
230
|
+
if (healValue !== void 0) healOpts.heal = healValue;
|
|
231
|
+
const heal = parseHeal(healOpts, "infra-only");
|
|
232
|
+
if (typeof heal !== "string") {
|
|
233
|
+
console.error(heal.error);
|
|
234
|
+
return EXIT.usage;
|
|
235
|
+
}
|
|
236
|
+
const state = newHealState();
|
|
237
|
+
const ensured = await ensureOverlay(adapter, target, heal, state, json);
|
|
238
|
+
if (!ensured.ok) {
|
|
239
|
+
if (json) {
|
|
240
|
+
console.log(
|
|
241
|
+
JSON.stringify(
|
|
242
|
+
{ schemaVersion: 1, status: "fail", recoverable: false, mutations: state.mutations, error: { code: "OVERLAY_INSTALL_FAILED", message: ensured.error } },
|
|
243
|
+
null,
|
|
244
|
+
2
|
|
245
|
+
)
|
|
246
|
+
);
|
|
247
|
+
} else {
|
|
248
|
+
console.error(`\u2717 overlay auto-ensure failed: ${ensured.error}`);
|
|
249
|
+
}
|
|
250
|
+
return EXIT.infra;
|
|
251
|
+
}
|
|
252
|
+
return { state, heal };
|
|
253
|
+
}
|
|
254
|
+
const RUN_RECOVERY_CODE = {
|
|
255
|
+
mobile: "metro.restarted",
|
|
256
|
+
extension: "chrome.reopened",
|
|
257
|
+
core: "runtime.reset"
|
|
258
|
+
};
|
|
259
|
+
function readRunFailureText(result) {
|
|
260
|
+
try {
|
|
261
|
+
const trace = JSON.parse(fs.readFileSync(result.tracePath, "utf8"));
|
|
262
|
+
const entries = Array.isArray(trace.entries) ? trace.entries : [];
|
|
263
|
+
return entries.filter((entry) => entry && entry.ok === false && typeof entry.error === "string").map((entry) => entry.error).join("\n").trim();
|
|
264
|
+
} catch {
|
|
265
|
+
return "";
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
async function executeWithHealBounds(exec, adapter, target, heal, state) {
|
|
269
|
+
let result = await exec();
|
|
270
|
+
for (; ; ) {
|
|
271
|
+
if (result.status === "pass") return { result, violation: null };
|
|
272
|
+
const violation = checkHealBounds(target, readRunFailureText(result), state);
|
|
273
|
+
if (violation !== null) return { result, violation };
|
|
274
|
+
if (heal === "off") return { result, violation: null };
|
|
275
|
+
const recoveryCode = RUN_RECOVERY_CODE[adapter];
|
|
276
|
+
state.attemptedRecoveries.push(recoveryCode);
|
|
277
|
+
result = await exec();
|
|
278
|
+
if (result.status === "pass") {
|
|
279
|
+
state.recovered.push(recoveryCode);
|
|
280
|
+
return { result, violation: null };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function emitHealViolation(json, command, result, violation, state, adapter) {
|
|
285
|
+
const userAction = adapter === "core" && violation.code === "WALLET_STATE_REQUIRED" ? 'set MM_TEST_ACCOUNT_ADDRESS=<0x\u2026> in env, or add "account": "<0x\u2026>" to the node block in the recipe' : violation.userAction;
|
|
286
|
+
if (json) {
|
|
287
|
+
console.log(
|
|
288
|
+
JSON.stringify(
|
|
289
|
+
{
|
|
290
|
+
schemaVersion: 1,
|
|
291
|
+
command,
|
|
292
|
+
status: "fail",
|
|
293
|
+
recoverable: false,
|
|
294
|
+
recovered: state.recovered,
|
|
295
|
+
mutations: state.mutations,
|
|
296
|
+
attemptedRecoveries: state.attemptedRecoveries,
|
|
297
|
+
summaryPath: result.summaryPath,
|
|
298
|
+
tracePath: result.tracePath,
|
|
299
|
+
artifactManifestPath: result.artifactManifestPath,
|
|
300
|
+
exitCode: violation.exitCode,
|
|
301
|
+
error: {
|
|
302
|
+
code: violation.code,
|
|
303
|
+
message: violation.message,
|
|
304
|
+
retryable: false,
|
|
305
|
+
userAction: userAction ?? null,
|
|
306
|
+
originalError: violation.originalError ?? null
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
null,
|
|
310
|
+
2
|
|
311
|
+
)
|
|
312
|
+
);
|
|
313
|
+
} else {
|
|
314
|
+
console.error(
|
|
315
|
+
`\u2717 mm-harness ${command}: ${violation.message}` + (violation.originalError ? `
|
|
316
|
+
--- original failure ---
|
|
317
|
+
${violation.originalError}` : "") + (userAction ? `
|
|
318
|
+
Next: ${userAction}` : "")
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
return violation.exitCode;
|
|
322
|
+
}
|
|
323
|
+
function countRecipeNodes(recipe) {
|
|
324
|
+
if (!isRecord(recipe)) return void 0;
|
|
325
|
+
const validate = isRecord(recipe.validate) ? recipe.validate : void 0;
|
|
326
|
+
const workflow = validate && isRecord(validate.workflow) ? validate.workflow : void 0;
|
|
327
|
+
const nodes = workflow && isRecord(workflow.nodes) ? workflow.nodes : void 0;
|
|
328
|
+
return nodes ? Object.keys(nodes).length : void 0;
|
|
329
|
+
}
|
|
330
|
+
export {
|
|
331
|
+
countRecipeNodes,
|
|
332
|
+
emitHealViolation,
|
|
333
|
+
executeWithHealBounds,
|
|
334
|
+
prepareHeal,
|
|
335
|
+
resolveMetaMaskLibrarySources,
|
|
336
|
+
runOneNode,
|
|
337
|
+
runRecipe,
|
|
338
|
+
synthesizeOneNodeRecipe,
|
|
339
|
+
validateRecipeAdapterAware,
|
|
340
|
+
validateRunRecipeStatic
|
|
341
|
+
};
|