@deeeed/metamask-harness 0.17.4 → 0.18.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 +775 -0
- package/README.md +65 -71
- package/adapters/extension/ensure-browser.sh +12 -1
- package/adapters/extension/inject.mjs +7 -0
- package/adapters/extension/launch-browser.cjs +10 -3
- package/adapters/extension/lib/chrome-args.cjs +31 -0
- package/adapters/extension/lib/macos-focus.cjs +32 -0
- package/adapters/extension/live.sh +10 -20
- package/adapters/manifest.json +8 -0
- package/adapters/mobile/open-device.sh +45 -7
- package/adapters/mobile/verify.sh +15 -3
- package/adapters/shared/harness-source-fingerprint.mjs +49 -0
- package/adapters/shared/install-repo-deps.sh +1 -5
- package/adapters/shared/open-debug.mjs +184 -103
- package/adapters/shared/resolve-slot-ports-core.mjs +23 -6
- package/adapters/shared/resolve-slot-ports.sh +22 -5
- package/bin/mm-harness +15 -3
- package/dist/adapters/core/surface.js +4 -1
- package/dist/adapters/extension/console-capture.js +3 -1
- package/dist/adapters/extension/harness-freshness.js +39 -0
- package/dist/adapters/extension/product-config.js +110 -0
- package/dist/adapters/extension/runtime-decision.js +20 -71
- package/dist/adapters/extension/surface.js +19 -1
- package/dist/adapters/mobile/prepare.js +17 -0
- package/dist/adapters/mobile/source-freshness.js +26 -41
- package/dist/adapters/mobile/surface.js +4 -1
- package/dist/adapters/resolve-slot-ports.js +2 -0
- package/dist/adapters/slot-ports.js +13 -32
- package/dist/adapters.js +50 -17
- package/dist/checkout-lock.js +27 -2
- package/dist/cli-color.js +19 -0
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +2 -3
- package/dist/command-contract.js +13 -3
- package/dist/commands/call.js +115 -29
- package/dist/commands/checklist.js +4 -1
- package/dist/commands/completion-candidates.js +20 -13
- package/dist/commands/debug.js +31 -38
- package/dist/commands/doctor.js +33 -6
- package/dist/commands/fixtures.js +65 -17
- package/dist/commands/flows.js +39 -10
- package/dist/commands/launch/extension.js +40 -15
- package/dist/commands/launch/index.js +41 -5
- package/dist/commands/list-executables.js +151 -29
- package/dist/commands/manifest.js +127 -18
- package/dist/commands/parse-args.js +11 -1
- package/dist/commands/run-engine.js +384 -56
- package/dist/commands/run.js +112 -17
- package/dist/commands/shared.js +22 -2
- package/dist/commands/status-probe.js +3 -0
- package/dist/commands/status.js +1 -0
- package/dist/completions-cache.js +1 -1
- package/dist/doctor.js +56 -6
- package/dist/harness.js +6 -5
- package/dist/heal-bounds.js +1 -1
- package/dist/live-adapter-contract.js +132 -12
- package/dist/manifest.js +161 -1
- package/dist/mm-harness-cli.js +13 -7
- package/dist/recipe-security.js +178 -0
- package/dist/runner.js +87 -13
- package/dist/runtime-context.js +8 -26
- package/docs/CONTRIBUTING.md +137 -0
- package/docs/QA.md +185 -0
- package/docs/RECIPES.md +161 -0
- package/docs/SECURITY.md +88 -0
- package/library/actions/core/perps/read_account.mjs +2 -2
- package/library/actions/core/perps/read_orders.mjs +2 -1
- package/library/actions/core/perps/read_positions.mjs +2 -1
- package/library/actions/core/wallet/list_accounts.mjs +95 -0
- package/library/actions/extension/platform/cdp.mjs +1 -0
- package/library/actions/extension/wallet/list_accounts.mjs +41 -0
- package/library/actions/mobile/platform/bridge.mjs +1 -5
- package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
- package/library/manifests/core.action-manifest.json +61 -0
- package/library/manifests/extension.action-manifest.json +53 -0
- package/library/manifests/mobile.action-manifest.json +53 -0
- package/library/recipes/runner/action-validation.extension.recipe.json +8 -1
- package/library/recipes/runner/action-validation.mobile.recipe.json +8 -1
- package/package.json +10 -6
- package/scripts/completions.sh +7 -7
- package/docs/ADAPTER-SURFACE.md +0 -26
- package/docs/CHEATSHEET.md +0 -48
- package/docs/CLI-ERGONOMICS-AUDIT.md +0 -32
- package/docs/CLI-ERGONOMICS-HUMAN-QA.md +0 -64
- package/docs/CODE-MAP.md +0 -62
- package/docs/UX-PRINCIPLES.md +0 -66
- package/docs/VIDEO-DEMO-VALIDATION.md +0 -74
- package/docs/architecture.md +0 -88
- package/docs/live-adapter-contract.md +0 -190
- package/docs/package-boundaries.md +0 -47
- package/docs/perps-flow-catalog.md +0 -235
- package/docs/recipe-libraries.md +0 -71
- package/docs/runtime-file-conventions.md +0 -36
package/dist/commands/run.js
CHANGED
|
@@ -7,7 +7,6 @@ import { checkoutBusyOut, EXIT, usageOut, writeInteractiveProgress } from "./sha
|
|
|
7
7
|
import {
|
|
8
8
|
optionFlag,
|
|
9
9
|
optionString,
|
|
10
|
-
requiredOption,
|
|
11
10
|
resolveAdapter,
|
|
12
11
|
runtimeOptionsFromCli,
|
|
13
12
|
isRecord,
|
|
@@ -19,12 +18,13 @@ import {
|
|
|
19
18
|
countRecipeNodes,
|
|
20
19
|
emitHealViolation,
|
|
21
20
|
executeWithHealBounds,
|
|
21
|
+
preflightRecipe,
|
|
22
22
|
prepareHeal,
|
|
23
23
|
recoverRunInfra,
|
|
24
24
|
runRecipe,
|
|
25
25
|
validateRunRecipeStatic
|
|
26
26
|
} from "./run-engine.js";
|
|
27
|
-
import { handleListExecutables } from "./list-executables.js";
|
|
27
|
+
import { handleDescribeRecipe, handleListExecutables } from "./list-executables.js";
|
|
28
28
|
import { applyDeviceTargeting } from "./device-target.js";
|
|
29
29
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
30
30
|
import { coreDependencyBlock } from "./core-readiness.js";
|
|
@@ -32,6 +32,7 @@ import { writeRunReport } from "./run-report.js";
|
|
|
32
32
|
import { acquireCheckoutLock } from "../checkout-lock.js";
|
|
33
33
|
import { JsonStreamWriter } from "../json-stream.js";
|
|
34
34
|
import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run-diagnostics.js";
|
|
35
|
+
import { recipeTrustFailure } from "../recipe-security.js";
|
|
35
36
|
async function handleRun(parsed) {
|
|
36
37
|
const stream = new JsonStreamWriter("run", optionFlag(parsed.options, "jsonStream"));
|
|
37
38
|
const restoreStdout = stream.isolateStdout();
|
|
@@ -41,6 +42,36 @@ async function handleRun(parsed) {
|
|
|
41
42
|
stream.complete(exitCode === EXIT.ok ? "pass" : "fail", exitCode);
|
|
42
43
|
return exitCode;
|
|
43
44
|
} catch (error) {
|
|
45
|
+
const trustFailure = recipeTrustFailure(error);
|
|
46
|
+
if (trustFailure) {
|
|
47
|
+
if (stream.enabled) {
|
|
48
|
+
stream.error(trustFailure);
|
|
49
|
+
stream.complete("fail", EXIT.validation);
|
|
50
|
+
} else if (optionFlag(parsed.options, "json")) {
|
|
51
|
+
console.log(JSON.stringify({
|
|
52
|
+
schemaVersion: 1,
|
|
53
|
+
command: "run",
|
|
54
|
+
status: "fail",
|
|
55
|
+
error: trustFailure,
|
|
56
|
+
exitCode: EXIT.validation
|
|
57
|
+
}, null, 2));
|
|
58
|
+
} else {
|
|
59
|
+
console.error(`\u2717 mm-harness run: ${trustFailure.message}`);
|
|
60
|
+
const blocked = trustFailure.details?.blocked;
|
|
61
|
+
if (blocked?.length) {
|
|
62
|
+
console.error(" Restricted plan nodes:");
|
|
63
|
+
for (const node of blocked.slice(0, 10)) {
|
|
64
|
+
const implementation = node.implementation?.digest ? ` implementation=${node.implementation.kind ?? "custom"}@${node.implementation.digest}` : "";
|
|
65
|
+
console.error(
|
|
66
|
+
` - ${node.nodeId}: ${node.action} [${node.capabilities.join(", ")}] source=${node.source}${implementation}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (blocked.length > 10) console.error(` - \u2026 ${blocked.length - 10} more (use --json)`);
|
|
70
|
+
}
|
|
71
|
+
console.error(` Next: ${trustFailure.userAction}`);
|
|
72
|
+
}
|
|
73
|
+
return EXIT.validation;
|
|
74
|
+
}
|
|
44
75
|
const exitCode = error !== null && typeof error === "object" && "exitCode" in error && typeof error.exitCode === "number" ? error.exitCode : EXIT.runtime;
|
|
45
76
|
stream.error({
|
|
46
77
|
code: exitCode === EXIT.usage ? "CLI_USAGE_ERROR" : "RUN_FAILED",
|
|
@@ -54,6 +85,15 @@ async function handleRun(parsed) {
|
|
|
54
85
|
}
|
|
55
86
|
}
|
|
56
87
|
async function handleRunInner({ positional, options }, stream) {
|
|
88
|
+
if (optionFlag(options, "describe") && (optionFlag(options, "list") || optionFlag(options, "plan"))) {
|
|
89
|
+
const message = "--describe cannot be combined with --list or --plan.";
|
|
90
|
+
const userAction = "choose one: mm-harness run --list, mm-harness run <recipe> --describe, or mm-harness run <recipe> --plan";
|
|
91
|
+
if (stream.enabled) {
|
|
92
|
+
stream.error({ code: "CLI_USAGE_ERROR", message, userAction });
|
|
93
|
+
return EXIT.usage;
|
|
94
|
+
}
|
|
95
|
+
return usageOut(optionFlag(options, "json"), "run", message, userAction);
|
|
96
|
+
}
|
|
57
97
|
if (optionFlag(options, "list")) {
|
|
58
98
|
if (stream.enabled) {
|
|
59
99
|
const message = "--json-stream is for recipe execution; use run --list --json for discovery.";
|
|
@@ -64,6 +104,18 @@ async function handleRunInner({ positional, options }, stream) {
|
|
|
64
104
|
}
|
|
65
105
|
const targetRecipe = positional[0];
|
|
66
106
|
if (!targetRecipe) throw usageError("run requires <recipe.json>.");
|
|
107
|
+
if (optionFlag(options, "describe")) {
|
|
108
|
+
if (stream.enabled) {
|
|
109
|
+
const message = "--json-stream is for recipe execution; use --describe --json for discovery.";
|
|
110
|
+
stream.error({
|
|
111
|
+
code: "JSON_STREAM_UNSUPPORTED_MODE",
|
|
112
|
+
message,
|
|
113
|
+
userAction: `mm-harness run ${shellQuote(targetRecipe)} --describe --json`
|
|
114
|
+
});
|
|
115
|
+
return EXIT.usage;
|
|
116
|
+
}
|
|
117
|
+
return handleDescribeRecipe(targetRecipe, options);
|
|
118
|
+
}
|
|
67
119
|
if (optionFlag(options, "plan")) return handleRunPlan(targetRecipe, options, stream);
|
|
68
120
|
const { adapter, target } = resolveAdapter(options);
|
|
69
121
|
const json = optionFlag(options, "json");
|
|
@@ -86,9 +138,6 @@ async function handleRunInner({ positional, options }, stream) {
|
|
|
86
138
|
return emitRunUsageError(jsonOutput, stream, adapter, targetRecipe, dtResult.code, dtResult.message, dtResult.userAction);
|
|
87
139
|
}
|
|
88
140
|
if (recipeRunning(target)) return emitRunRecipeRunning(jsonOutput, stream, target);
|
|
89
|
-
if (optionString(options, "artifactsDir") === void 0 && runArgLooksLikeRecipeFile(targetRecipe)) {
|
|
90
|
-
requiredOption(options, "artifactsDir", "run requires --artifacts-dir <dir>.");
|
|
91
|
-
}
|
|
92
141
|
stream.phase("validate");
|
|
93
142
|
const validated = await validateRunRecipeStatic(targetRecipe, adapter, options);
|
|
94
143
|
if (validated.usageError) {
|
|
@@ -115,7 +164,37 @@ async function handleRunInner({ positional, options }, stream) {
|
|
|
115
164
|
if (depsBlock) {
|
|
116
165
|
return emitRunUsageError(jsonOutput, stream, adapter, validated.recipeFile, depsBlock.code, depsBlock.message, depsBlock.userAction);
|
|
117
166
|
}
|
|
118
|
-
|
|
167
|
+
let artifactsDir;
|
|
168
|
+
try {
|
|
169
|
+
artifactsDir = resolveRunArtifactsDir(target, optionString(options, "artifactsDir"));
|
|
170
|
+
} catch (error) {
|
|
171
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
172
|
+
return emitRunUsageError(
|
|
173
|
+
jsonOutput,
|
|
174
|
+
stream,
|
|
175
|
+
adapter,
|
|
176
|
+
validated.recipeFile,
|
|
177
|
+
"ARTIFACT_DIR_INVALID",
|
|
178
|
+
message,
|
|
179
|
+
"set RECIPE_TASK_DIR/FARMSLOT_TASK_DIR inside the checkout or pass --artifacts-dir <path>"
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const librarySources = validated.librarySources;
|
|
183
|
+
const runtimeOptions = {
|
|
184
|
+
...runtimeOptionsFromCli(options),
|
|
185
|
+
...librarySources ? { librarySources } : {},
|
|
186
|
+
stdoutIsMachineContract: machine,
|
|
187
|
+
onActionEvent: ({ nodeId, action, status }) => stream.node(nodeId, action, status)
|
|
188
|
+
};
|
|
189
|
+
stream.phase("authorize");
|
|
190
|
+
let preflightedExecution = await preflightRecipe(
|
|
191
|
+
adapter,
|
|
192
|
+
validated.recipeFile,
|
|
193
|
+
artifactsDir,
|
|
194
|
+
target,
|
|
195
|
+
optionString(options, "actionManifest"),
|
|
196
|
+
runtimeOptions
|
|
197
|
+
);
|
|
119
198
|
const lock = acquireCheckoutLock(target, "run");
|
|
120
199
|
if ("message" in lock) {
|
|
121
200
|
const userAction = `wait for the current owner, or inspect ${lock.path} if its process has exited`;
|
|
@@ -135,18 +214,23 @@ async function handleRunInner({ positional, options }, stream) {
|
|
|
135
214
|
return prepared;
|
|
136
215
|
}
|
|
137
216
|
const { state, heal } = prepared;
|
|
138
|
-
const librarySources = validated.librarySources;
|
|
139
|
-
const runtimeOptions = {
|
|
140
|
-
...runtimeOptionsFromCli(options),
|
|
141
|
-
...librarySources ? { librarySources } : {},
|
|
142
|
-
stdoutIsMachineContract: machine,
|
|
143
|
-
onActionEvent: ({ nodeId, action, status }) => stream.node(nodeId, action, status)
|
|
144
|
-
};
|
|
145
217
|
stream.phase("execute");
|
|
146
218
|
const { result, violation } = await executeWithHealBounds(
|
|
147
219
|
// validated.recipeFile, not the raw arg: the arg may be a library recipe NAME
|
|
148
220
|
// that only the resolver knows how to turn into a file.
|
|
149
|
-
() =>
|
|
221
|
+
() => {
|
|
222
|
+
const execution = preflightedExecution;
|
|
223
|
+
preflightedExecution = void 0;
|
|
224
|
+
return runRecipe(
|
|
225
|
+
adapter,
|
|
226
|
+
validated.recipeFile,
|
|
227
|
+
artifactsDir,
|
|
228
|
+
target,
|
|
229
|
+
optionString(options, "actionManifest"),
|
|
230
|
+
runtimeOptions,
|
|
231
|
+
execution
|
|
232
|
+
);
|
|
233
|
+
},
|
|
150
234
|
adapter,
|
|
151
235
|
target,
|
|
152
236
|
heal,
|
|
@@ -238,6 +322,20 @@ function formatDiagnosticLine(line, out) {
|
|
|
238
322
|
const style = status === "CLEAN" ? "ok" : status === "ERROR" || status === "EXCEPTION" ? "err" : "warn";
|
|
239
323
|
return `${out(style, status)}${match[2]}`;
|
|
240
324
|
}
|
|
325
|
+
function resolveRunArtifactsDir(target, explicit) {
|
|
326
|
+
if (explicit !== void 0) return path.resolve(explicit);
|
|
327
|
+
const taskDir = process.env.RECIPE_TASK_DIR || process.env.FARMSLOT_TASK_DIR;
|
|
328
|
+
if (taskDir) {
|
|
329
|
+
const resolvedTask = path.resolve(target, taskDir);
|
|
330
|
+
const relative = path.relative(path.resolve(target), resolvedTask);
|
|
331
|
+
if (!relative.startsWith(`..${path.sep}`) && relative !== "..") {
|
|
332
|
+
return path.join(resolvedTask, "artifacts");
|
|
333
|
+
}
|
|
334
|
+
throw new Error(`task directory must be inside the target checkout: ${taskDir}`);
|
|
335
|
+
}
|
|
336
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:.TZ]/gu, "");
|
|
337
|
+
return path.join(target, "temp", "recipe", "runs", `${stamp}-${process.pid}`);
|
|
338
|
+
}
|
|
241
339
|
function runArtifactInventory(manifestPathValue) {
|
|
242
340
|
const manifestPath = path.resolve(String(manifestPathValue));
|
|
243
341
|
const root = path.dirname(manifestPath);
|
|
@@ -468,9 +566,6 @@ function emitPlanUsageError(json, stream, adapter, recipeFile, code, message, us
|
|
|
468
566
|
}
|
|
469
567
|
return EXIT.usage;
|
|
470
568
|
}
|
|
471
|
-
function runArgLooksLikeRecipeFile(value) {
|
|
472
|
-
return path.isAbsolute(value) || value.includes("/") || value.includes(path.sep) || value.endsWith(".json") || fs.existsSync(path.resolve(value));
|
|
473
|
-
}
|
|
474
569
|
function emitRunRecipeRunning(json, stream, target, detail) {
|
|
475
570
|
const message = detail ?? "a recipe is currently running \u2014 refusing to start while another recipe executes.";
|
|
476
571
|
const userAction = `inspect the checkout state with: mm-harness status --target ${shellQuote(target)} --json; retry after the active recipe finishes`;
|
package/dist/commands/shared.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { detectAdapter } from "../harness.js";
|
|
4
|
+
import { colorHumanMessage } from "../cli-color.js";
|
|
4
5
|
import { resolveLeafInvoke, shellLeafMissing } from "../leaf-invoke.js";
|
|
5
6
|
const EXIT = { ok: 0, runtime: 1, usage: 2, infra: 3, bounded: 4, validation: 5 };
|
|
6
7
|
function writeInteractiveProgress(json, message, {
|
|
@@ -8,7 +9,7 @@ function writeInteractiveProgress(json, message, {
|
|
|
8
9
|
stream = process.stderr
|
|
9
10
|
} = {}) {
|
|
10
11
|
if (json || !stdoutIsTTY) return false;
|
|
11
|
-
stream.write(`${message}
|
|
12
|
+
stream.write(`${colorHumanMessage(message, { stream })}
|
|
12
13
|
`);
|
|
13
14
|
return true;
|
|
14
15
|
}
|
|
@@ -147,6 +148,8 @@ function spawnScriptStreaming(script, args, cwd, options) {
|
|
|
147
148
|
let settled = false;
|
|
148
149
|
let didTimeout = false;
|
|
149
150
|
let escalation;
|
|
151
|
+
let forwardedParentSignal;
|
|
152
|
+
let parentSignalFallback;
|
|
150
153
|
const parentSignals = ["SIGINT", "SIGTERM"];
|
|
151
154
|
const removeParentSignalHandlers = () => {
|
|
152
155
|
for (const signal of parentSignals) process.removeListener(signal, parentSignalHandlers[signal]);
|
|
@@ -156,6 +159,7 @@ function spawnScriptStreaming(script, args, cwd, options) {
|
|
|
156
159
|
settled = true;
|
|
157
160
|
if (timer) clearTimeout(timer);
|
|
158
161
|
if (escalation) clearTimeout(escalation);
|
|
162
|
+
if (parentSignalFallback) clearTimeout(parentSignalFallback);
|
|
159
163
|
removeParentSignalHandlers();
|
|
160
164
|
resolve(result);
|
|
161
165
|
};
|
|
@@ -166,10 +170,22 @@ function spawnScriptStreaming(script, args, cwd, options) {
|
|
|
166
170
|
} catch {
|
|
167
171
|
}
|
|
168
172
|
};
|
|
173
|
+
const exitFromForwardedSignal = () => {
|
|
174
|
+
if (!forwardedParentSignal) return;
|
|
175
|
+
const signal = forwardedParentSignal;
|
|
176
|
+
forwardedParentSignal = void 0;
|
|
177
|
+
if (parentSignalFallback) clearTimeout(parentSignalFallback);
|
|
178
|
+
process.kill(process.pid, signal);
|
|
179
|
+
};
|
|
169
180
|
const parentSignalHandlers = Object.fromEntries(parentSignals.map((signal) => [signal, () => {
|
|
181
|
+
if (forwardedParentSignal) return;
|
|
182
|
+
forwardedParentSignal = signal;
|
|
170
183
|
signalChildTree(signal);
|
|
171
184
|
removeParentSignalHandlers();
|
|
172
|
-
|
|
185
|
+
parentSignalFallback = setTimeout(() => {
|
|
186
|
+
signalChildTree("SIGKILL");
|
|
187
|
+
exitFromForwardedSignal();
|
|
188
|
+
}, 1e3);
|
|
173
189
|
}]));
|
|
174
190
|
if (ownsProcessGroup) {
|
|
175
191
|
for (const signal of parentSignals) process.once(signal, parentSignalHandlers[signal]);
|
|
@@ -205,6 +221,10 @@ function spawnScriptStreaming(script, args, cwd, options) {
|
|
|
205
221
|
finish({ status: 1, output: message });
|
|
206
222
|
});
|
|
207
223
|
child.on("exit", () => {
|
|
224
|
+
if (forwardedParentSignal) {
|
|
225
|
+
exitFromForwardedSignal();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
208
228
|
if (!didTimeout) return;
|
|
209
229
|
signalChildTree("SIGKILL");
|
|
210
230
|
finish({ status: 1, output, timedOut: true, timeoutMs: Number(timeoutMs) });
|
|
@@ -22,6 +22,9 @@ function routeToString(route) {
|
|
|
22
22
|
function deriveWalletState(account, route) {
|
|
23
23
|
const routeStr = (routeToString(route) ?? "").toLowerCase();
|
|
24
24
|
if (routeStr.includes("onboard")) return "onboarding";
|
|
25
|
+
if (routeStr === "login" || routeStr === "lockscreen") {
|
|
26
|
+
return "locked";
|
|
27
|
+
}
|
|
25
28
|
if (account !== null && account !== void 0 && typeof account === "object") return "unlocked";
|
|
26
29
|
return "locked";
|
|
27
30
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -104,6 +104,7 @@ function renderRuntimeStatus(runtime, out) {
|
|
|
104
104
|
);
|
|
105
105
|
}
|
|
106
106
|
function nextForRuntime(launchHint, relaunchHint, runtime) {
|
|
107
|
+
if (runtime.nextAction) return runtime.nextAction;
|
|
107
108
|
if (runtime.decision === "ready") return "mm-harness logs";
|
|
108
109
|
if (runtime.decision === "relaunch") return launchHint;
|
|
109
110
|
return relaunchHint;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { recipeRuntimePath } from "./paths.js";
|
|
4
|
-
const COMPLETION_CACHE_VERSION =
|
|
4
|
+
const COMPLETION_CACHE_VERSION = 2;
|
|
5
5
|
const COMPLETION_CACHE_TTL_MS = 6e4;
|
|
6
6
|
function completionCachePath(projectRoot) {
|
|
7
7
|
return recipeRuntimePath(projectRoot, ".completion-cache.json");
|
package/dist/doctor.js
CHANGED
|
@@ -155,12 +155,7 @@ function createDoctorReport(adapter, target, manifestValidation, actionManifestP
|
|
|
155
155
|
requiredChecks,
|
|
156
156
|
adapter,
|
|
157
157
|
target,
|
|
158
|
-
runner:
|
|
159
|
-
name: "@metamask/recipe-runner",
|
|
160
|
-
runnerDir,
|
|
161
|
-
actionManifestPath,
|
|
162
|
-
harnessPackage: "@farmslot/recipe-harness"
|
|
163
|
-
},
|
|
158
|
+
runner: runnerProvenance(actionManifestPath),
|
|
164
159
|
compatibilityMode: mode,
|
|
165
160
|
shape: repoShape(target),
|
|
166
161
|
fixture: fixtureSummary(target, adapter),
|
|
@@ -169,6 +164,60 @@ function createDoctorReport(adapter, target, manifestValidation, actionManifestP
|
|
|
169
164
|
manifestValidation: manifestValidation.summary
|
|
170
165
|
};
|
|
171
166
|
}
|
|
167
|
+
function runnerInstallKind(runnerRoot, invokedPath, executablePath) {
|
|
168
|
+
const normalizedRoot = path.normalize(runnerRoot);
|
|
169
|
+
const nodeModulesSegment = `${path.sep}node_modules${path.sep}`;
|
|
170
|
+
const globalNodeModulesSegment = `${path.sep}lib${path.sep}node_modules${path.sep}`;
|
|
171
|
+
if (normalizedRoot.includes(globalNodeModulesSegment)) return "global-install";
|
|
172
|
+
const nodeModulesIndex = normalizedRoot.lastIndexOf(nodeModulesSegment);
|
|
173
|
+
if (nodeModulesIndex >= 0) {
|
|
174
|
+
const nodeModulesRoot = normalizedRoot.slice(
|
|
175
|
+
0,
|
|
176
|
+
nodeModulesIndex + nodeModulesSegment.length - 1
|
|
177
|
+
);
|
|
178
|
+
const invoked = path.resolve(invokedPath);
|
|
179
|
+
const projectBin = `${path.join(nodeModulesRoot, ".bin")}${path.sep}`;
|
|
180
|
+
if (invoked === path.resolve(executablePath) || invoked.startsWith(projectBin)) {
|
|
181
|
+
return "project-install";
|
|
182
|
+
}
|
|
183
|
+
return "global-install";
|
|
184
|
+
}
|
|
185
|
+
if (path.resolve(invokedPath) !== path.resolve(executablePath)) return "local-link";
|
|
186
|
+
return "source-checkout";
|
|
187
|
+
}
|
|
188
|
+
function runnerProvenance(actionManifestPath) {
|
|
189
|
+
const packagePath = path.join(runnerDir, "package.json");
|
|
190
|
+
const packageDocument = readJsonObject(packagePath);
|
|
191
|
+
const packageName = packageDocument.name;
|
|
192
|
+
const version = packageDocument.version;
|
|
193
|
+
if (packageName !== "@deeeed/metamask-harness") {
|
|
194
|
+
throw new Error(`Expected @deeeed/metamask-harness package name in ${packagePath}`);
|
|
195
|
+
}
|
|
196
|
+
if (typeof version !== "string" || !version) {
|
|
197
|
+
throw new Error(`Expected package version in ${packagePath}`);
|
|
198
|
+
}
|
|
199
|
+
const executablePath = path.resolve(
|
|
200
|
+
process.env.MM_HARNESS_EXECUTABLE ?? path.join(runnerDir, "bin/mm-harness")
|
|
201
|
+
);
|
|
202
|
+
const invokedPath = path.resolve(process.env.MM_HARNESS_INVOKED_AS ?? executablePath);
|
|
203
|
+
const installKind = runnerInstallKind(runnerDir, invokedPath, executablePath);
|
|
204
|
+
const packageSource = runnerDir.includes(`${path.sep}node_modules${path.sep}`) ? "node_modules" : "source-checkout";
|
|
205
|
+
return {
|
|
206
|
+
name: "@metamask/recipe-runner",
|
|
207
|
+
packageName,
|
|
208
|
+
version,
|
|
209
|
+
runnerDir,
|
|
210
|
+
packagePath,
|
|
211
|
+
packageSource,
|
|
212
|
+
installKind,
|
|
213
|
+
linked: installKind === "local-link",
|
|
214
|
+
global: installKind === "global-install",
|
|
215
|
+
invokedPath,
|
|
216
|
+
executablePath,
|
|
217
|
+
actionManifestPath,
|
|
218
|
+
harnessPackage: "@farmslot/recipe-harness"
|
|
219
|
+
};
|
|
220
|
+
}
|
|
172
221
|
function readJsonObject(file) {
|
|
173
222
|
const value = readJson(file);
|
|
174
223
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -184,5 +233,6 @@ export {
|
|
|
184
233
|
renderRuntimeContext,
|
|
185
234
|
repoShape,
|
|
186
235
|
requiredDoctorCheckSummary,
|
|
236
|
+
runnerInstallKind,
|
|
187
237
|
runtimeContextSummary
|
|
188
238
|
};
|
package/dist/harness.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { resolveLeafInvoke, shellLeafMissing, missingShellLeafMessage } from "./leaf-invoke.js";
|
|
5
5
|
import { resolveExtensionSlotPorts } from "./adapters/slot-ports.js";
|
|
6
6
|
import { recipeHarnessPath, recipeRuntimeDir, runnerDir } from "./paths.js";
|
|
7
|
+
import { colorHumanMessage } from "./cli-color.js";
|
|
7
8
|
const HARNESS_ACTIONS = ["install", "verify", "cleanup"];
|
|
8
9
|
const ADAPTERS = ["mobile", "extension", "core"];
|
|
9
10
|
function harnessUsage() {
|
|
@@ -332,7 +333,7 @@ async function handleHarness(argv) {
|
|
|
332
333
|
}
|
|
333
334
|
if (!json && !quiet) {
|
|
334
335
|
const detected = autoDetected ? ", auto-detected" : "";
|
|
335
|
-
console.error(`\u2192 ${harnessAction} (${adapter}${detected}) \u2014 target: ${target}`);
|
|
336
|
+
console.error(colorHumanMessage(`\u2192 ${harnessAction} (${adapter}${detected}) \u2014 target: ${target}`));
|
|
336
337
|
}
|
|
337
338
|
const start = Date.now();
|
|
338
339
|
if (shellLeafMissing(dispatch.command)) {
|
|
@@ -396,11 +397,11 @@ async function handleHarness(argv) {
|
|
|
396
397
|
)
|
|
397
398
|
);
|
|
398
399
|
} else if (exitCode === 0) {
|
|
399
|
-
console.error(`\u2713 ${harnessAction} ${adapter} passed (${seconds}s)
|
|
400
|
-
Next: ${successNext(adapter, harnessAction, target)}`);
|
|
400
|
+
console.error(colorHumanMessage(`\u2713 ${harnessAction} ${adapter} passed (${seconds}s)
|
|
401
|
+
Next: ${successNext(adapter, harnessAction, target)}`));
|
|
401
402
|
} else {
|
|
402
|
-
console.error(`\u2717 ${harnessAction} ${adapter} failed (exit ${exitCode}, ${seconds}s)
|
|
403
|
-
${failureHint(adapter, harnessAction)}`);
|
|
403
|
+
console.error(colorHumanMessage(`\u2717 ${harnessAction} ${adapter} failed (exit ${exitCode}, ${seconds}s)
|
|
404
|
+
${failureHint(adapter, harnessAction)}`));
|
|
404
405
|
}
|
|
405
406
|
return exitCode;
|
|
406
407
|
}
|
package/dist/heal-bounds.js
CHANGED
|
@@ -45,7 +45,7 @@ async function ensureOverlay(adapter, target, heal, state, json) {
|
|
|
45
45
|
});
|
|
46
46
|
code = result.status ?? 1;
|
|
47
47
|
} else {
|
|
48
|
-
code = await handleHarness(["install", "--platform", adapter, "--target", target,
|
|
48
|
+
code = await handleHarness(["install", "--platform", adapter, "--target", target, "--quiet"]);
|
|
49
49
|
}
|
|
50
50
|
if (code !== 0 || !overlayPresent(target, adapter)) {
|
|
51
51
|
return { ok: false, error: `runtime overlay install failed (exit ${code})` };
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { access, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
|
|
2
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
4
|
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { spawn } from "node:child_process";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
import { build } from "esbuild";
|
|
9
|
+
import { init, parse } from "es-module-lexer";
|
|
6
10
|
import { resolveRequiredLocalProtocolRoot, runnerDir } from "./paths.js";
|
|
7
11
|
import { corePnpNodeOptions } from "./commands/core-readiness.js";
|
|
8
12
|
function actionFileStem(action) {
|
|
@@ -31,13 +35,17 @@ function candidateFamilies(action) {
|
|
|
31
35
|
const families = [family];
|
|
32
36
|
return [...new Set(families)];
|
|
33
37
|
}
|
|
34
|
-
function candidatePaths(platform, action) {
|
|
38
|
+
function candidatePaths(platform, action, runtimeEnv = process.env) {
|
|
35
39
|
const stems = candidateStems(action);
|
|
36
40
|
const families = candidateFamilies(action);
|
|
37
|
-
const
|
|
38
|
-
process.env.
|
|
41
|
+
const declaredRoot = declaredActionRoot(
|
|
42
|
+
runtimeEnv.METAMASK_RECIPE_ACTION_SOURCE_MAP ?? process.env.METAMASK_RECIPE_ACTION_SOURCE_MAP,
|
|
43
|
+
action
|
|
44
|
+
);
|
|
45
|
+
const roots = [...new Set((declaredRoot ? [declaredRoot] : [
|
|
46
|
+
runtimeEnv.METAMASK_RECIPE_LIVE_ADAPTER_DIR ?? process.env.METAMASK_RECIPE_LIVE_ADAPTER_DIR,
|
|
39
47
|
path.join(runnerDir, "library/actions")
|
|
40
|
-
].filter(Boolean);
|
|
48
|
+
]).filter((entry) => Boolean(entry)))];
|
|
41
49
|
const files = [];
|
|
42
50
|
for (const root of roots) {
|
|
43
51
|
for (const family of families) {
|
|
@@ -49,6 +57,16 @@ function candidatePaths(platform, action) {
|
|
|
49
57
|
}
|
|
50
58
|
return files;
|
|
51
59
|
}
|
|
60
|
+
function declaredActionRoot(raw, action) {
|
|
61
|
+
if (!raw) return void 0;
|
|
62
|
+
try {
|
|
63
|
+
const parsed = JSON.parse(raw);
|
|
64
|
+
const root = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed[action] : void 0;
|
|
65
|
+
return typeof root === "string" && root.length > 0 ? root : void 0;
|
|
66
|
+
} catch {
|
|
67
|
+
return void 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
52
70
|
function pushCandidateFiles(files, root, platform, family, stem) {
|
|
53
71
|
files.push(path.join(root, platform, family, `${stem}.mjs`));
|
|
54
72
|
files.push(path.join(root, "shared", family, `${stem}.mjs`));
|
|
@@ -74,8 +92,16 @@ function runProcess(command, args, options) {
|
|
|
74
92
|
const child = spawn(command, args, {
|
|
75
93
|
cwd: options.cwd,
|
|
76
94
|
env: options.env,
|
|
77
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
95
|
+
stdio: [options.stdin == null ? "ignore" : "pipe", "pipe", "pipe"]
|
|
78
96
|
});
|
|
97
|
+
if (options.stdin != null) {
|
|
98
|
+
child.stdin.on("error", (error) => {
|
|
99
|
+
if (error.code === "EPIPE" || settled) return;
|
|
100
|
+
settled = true;
|
|
101
|
+
reject(error);
|
|
102
|
+
});
|
|
103
|
+
child.stdin.end(options.stdin);
|
|
104
|
+
}
|
|
79
105
|
let stdout = "";
|
|
80
106
|
let stderr = "";
|
|
81
107
|
child.stdout.on("data", (chunk) => {
|
|
@@ -106,6 +132,79 @@ function runProcess(command, args, options) {
|
|
|
106
132
|
});
|
|
107
133
|
});
|
|
108
134
|
}
|
|
135
|
+
async function prepareLiveAdapterScript({
|
|
136
|
+
platform,
|
|
137
|
+
action,
|
|
138
|
+
implementationRoot,
|
|
139
|
+
allowRuntimeImports = false
|
|
140
|
+
}) {
|
|
141
|
+
const entryPath = await firstExecutablePath(
|
|
142
|
+
candidatePaths(platform, action, {
|
|
143
|
+
METAMASK_RECIPE_ACTION_SOURCE_MAP: JSON.stringify({ [action]: implementationRoot })
|
|
144
|
+
})
|
|
145
|
+
);
|
|
146
|
+
if (!entryPath) return null;
|
|
147
|
+
const rootReal = await realpath(implementationRoot);
|
|
148
|
+
const entryReal = await realpath(entryPath);
|
|
149
|
+
if (!isPathWithin(rootReal, entryReal)) {
|
|
150
|
+
throw new Error(`Live adapter ${entryPath} resolves outside ${implementationRoot}.`);
|
|
151
|
+
}
|
|
152
|
+
const result = await build({
|
|
153
|
+
entryPoints: [entryReal],
|
|
154
|
+
bundle: true,
|
|
155
|
+
write: false,
|
|
156
|
+
platform: "node",
|
|
157
|
+
format: "esm",
|
|
158
|
+
target: `node${process.versions.node.split(".")[0]}`,
|
|
159
|
+
legalComments: "none",
|
|
160
|
+
sourcemap: false,
|
|
161
|
+
banner: { js: `process.argv[1] = ${JSON.stringify(entryReal)};` },
|
|
162
|
+
plugins: [bindImportMetaUrls()],
|
|
163
|
+
logLevel: "silent"
|
|
164
|
+
});
|
|
165
|
+
const output = result.outputFiles[0];
|
|
166
|
+
if (!output) throw new Error(`Bundling live adapter ${entryReal} produced no executable output.`);
|
|
167
|
+
const sourceText = output.text;
|
|
168
|
+
await init;
|
|
169
|
+
const [imports] = parse(sourceText);
|
|
170
|
+
if (!allowRuntimeImports && imports.some((entry) => entry.d >= 0)) {
|
|
171
|
+
const error = new Error(
|
|
172
|
+
`Live adapter ${entryReal} contains a runtime dynamic import that cannot be bound to the approved bundle. Replace it with a static import so its implementation bytes are included in the plan digest.`
|
|
173
|
+
);
|
|
174
|
+
Object.assign(error, {
|
|
175
|
+
code: "RECIPE_TRUST_UNBOUND_IMPORT",
|
|
176
|
+
userAction: "replace the computed import with a static import, or promote the adapter into an explicitly trusted engineer-owned library"
|
|
177
|
+
});
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
action,
|
|
182
|
+
platform,
|
|
183
|
+
entryPath: entryReal,
|
|
184
|
+
sourceText,
|
|
185
|
+
sourceDigest: `sha256:${createHash("sha256").update(sourceText).digest("hex")}`
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function bindImportMetaUrls() {
|
|
189
|
+
return {
|
|
190
|
+
name: "bind-import-meta-urls",
|
|
191
|
+
setup(buildContext) {
|
|
192
|
+
buildContext.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, async (args) => {
|
|
193
|
+
const contents = await readFile(args.path, "utf8");
|
|
194
|
+
if (!contents.includes("import.meta.url")) return void 0;
|
|
195
|
+
const extension = path.extname(args.path);
|
|
196
|
+
const loader = extension === ".ts" || extension === ".mts" || extension === ".cts" ? "ts" : extension === ".tsx" ? "tsx" : extension === ".jsx" ? "jsx" : "js";
|
|
197
|
+
return {
|
|
198
|
+
contents: contents.replaceAll(
|
|
199
|
+
"import.meta.url",
|
|
200
|
+
JSON.stringify(pathToFileURL(args.path).href)
|
|
201
|
+
),
|
|
202
|
+
loader
|
|
203
|
+
};
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
109
208
|
function commandFor(file, projectRoot, platform) {
|
|
110
209
|
const needsTsx = platform === "core" || importsSourceTypescript(file);
|
|
111
210
|
if (!needsTsx) {
|
|
@@ -158,8 +257,8 @@ function importsSourceTypescriptFrom(file, visited) {
|
|
|
158
257
|
}
|
|
159
258
|
return false;
|
|
160
259
|
}
|
|
161
|
-
async function resolveLiveAdapter(platform, action) {
|
|
162
|
-
return firstExecutablePath(candidatePaths(platform, action));
|
|
260
|
+
async function resolveLiveAdapter(platform, action, runtimeEnv) {
|
|
261
|
+
return firstExecutablePath(candidatePaths(platform, action, runtimeEnv));
|
|
163
262
|
}
|
|
164
263
|
const CORE_WORKSPACE_PACKAGES = [
|
|
165
264
|
"@metamask/base-controller",
|
|
@@ -191,8 +290,8 @@ async function platformAdapterEnv(platform, projectRoot, tempDir) {
|
|
|
191
290
|
NODE_OPTIONS: corePnpNodeOptions(projectRoot, process.env.NODE_OPTIONS)
|
|
192
291
|
};
|
|
193
292
|
}
|
|
194
|
-
async function runLiveAdapterScript({ platform, action, node, context }) {
|
|
195
|
-
const script = await resolveLiveAdapter(platform, action);
|
|
293
|
+
async function runLiveAdapterScript({ platform, action, node, context, prepared }) {
|
|
294
|
+
const script = prepared?.entryPath ?? await resolveLiveAdapter(platform, action, context?.env);
|
|
196
295
|
if (!script) return null;
|
|
197
296
|
const tempDir = await mkdtemp(path.join(os.tmpdir(), "mm-harness-live-adapter-"));
|
|
198
297
|
const inputPath = path.join(tempDir, "input.json");
|
|
@@ -211,7 +310,7 @@ async function runLiveAdapterScript({ platform, action, node, context }) {
|
|
|
211
310
|
};
|
|
212
311
|
await writeFile(inputPath, `${JSON.stringify(input, null, 2)}
|
|
213
312
|
`);
|
|
214
|
-
const command = commandFor(script, context.projectRoot, platform);
|
|
313
|
+
const command = prepared ? commandForPrepared(context.projectRoot, platform) : commandFor(script, context.projectRoot, platform);
|
|
215
314
|
const platformEnv = await platformAdapterEnv(platform, context.projectRoot, tempDir);
|
|
216
315
|
const result = await runProcess(command.command, [...command.args, inputPath], {
|
|
217
316
|
cwd: context.projectRoot,
|
|
@@ -222,7 +321,8 @@ async function runLiveAdapterScript({ platform, action, node, context }) {
|
|
|
222
321
|
METAMASK_RECIPE_ADAPTER_INPUT: inputPath,
|
|
223
322
|
METAMASK_RECIPE_ADAPTER_OUTPUT: outputPath
|
|
224
323
|
},
|
|
225
|
-
timeoutMs: Number(node.live_adapter_timeout_ms ?? node.timeout_ms ?? 6e4)
|
|
324
|
+
timeoutMs: Number(node.live_adapter_timeout_ms ?? node.timeout_ms ?? 6e4),
|
|
325
|
+
...prepared ? { stdin: prepared.sourceText } : {}
|
|
226
326
|
});
|
|
227
327
|
try {
|
|
228
328
|
if (result.timedOut) {
|
|
@@ -244,7 +344,27 @@ async function runLiveAdapterScript({ platform, action, node, context }) {
|
|
|
244
344
|
await rm(tempDir, { recursive: true, force: true });
|
|
245
345
|
}
|
|
246
346
|
}
|
|
347
|
+
function commandForPrepared(projectRoot, platform) {
|
|
348
|
+
if (platform !== "core") {
|
|
349
|
+
return { command: process.execPath, args: ["--input-type=module", "-"] };
|
|
350
|
+
}
|
|
351
|
+
const tsxBin = resolveTsxBin(projectRoot);
|
|
352
|
+
if (!tsxBin) {
|
|
353
|
+
const error = new Error(
|
|
354
|
+
`this action imports TypeScript from the checkout but no tsx runtime was found.
|
|
355
|
+
Next: run 'yarn install' in ${projectRoot}`
|
|
356
|
+
);
|
|
357
|
+
error.exitCode = 2;
|
|
358
|
+
throw error;
|
|
359
|
+
}
|
|
360
|
+
return { command: tsxBin, args: ["--input-type=module", "-"] };
|
|
361
|
+
}
|
|
362
|
+
function isPathWithin(root, candidate) {
|
|
363
|
+
const relative = path.relative(root, candidate);
|
|
364
|
+
return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
365
|
+
}
|
|
247
366
|
export {
|
|
367
|
+
prepareLiveAdapterScript,
|
|
248
368
|
resolveLiveAdapter,
|
|
249
369
|
runLiveAdapterScript
|
|
250
370
|
};
|