@deeeed/metamask-harness 0.19.0 → 0.20.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 +31 -0
- package/adapters/extension/ensure-browser.sh +6 -4
- package/adapters/extension/launch-browser.cjs +6 -1
- package/adapters/extension/lib/chrome-args.cjs +11 -1
- package/adapters/extension/start-watch.sh +1 -1
- package/adapters/extension/verify.sh +17 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +33 -0
- package/adapters/mobile/open-device.sh +12 -1
- package/adapters/mobile/wait-for-bridge.sh +1 -1
- package/dist/adapters/extension/ensure-ready.js +7 -2
- package/dist/adapters/extension/runtime-decision.js +4 -6
- package/dist/adapters/extension/runtime.js +114 -17
- package/dist/adapters/mobile/prepare.js +40 -6
- package/dist/adapters.js +6 -1
- package/dist/command-contract.js +58 -5
- package/dist/commands/call.js +6 -3
- package/dist/commands/check.js +9 -2
- package/dist/commands/checklist.js +117 -14
- package/dist/commands/launch/extension.js +8 -3
- package/dist/commands/launch/index.js +38 -25
- package/dist/commands/launch/mobile.js +2 -2
- package/dist/commands/manifest.js +72 -5
- package/dist/commands/run-engine.js +7 -2
- package/dist/heal-bounds.js +1 -1
- package/dist/live-adapter-contract.js +15 -2
- package/dist/metamask-action-validation.js +45 -0
- package/dist/mm-harness-cli.js +7 -4
- package/docs/CONTRIBUTING.md +8 -0
- package/docs/RECIPES.md +15 -0
- package/library/actions/core/perps/assert_orders.mjs +16 -5
- package/library/actions/core/perps/assert_positions.mjs +16 -5
- package/library/actions/extension/perps/perps.mjs +111 -19
- package/library/actions/extension/platform/cdp.mjs +8 -3
- package/library/actions/extension/ui/navigate.mjs +239 -16
- package/library/actions/mobile/perps/perps.mjs +108 -10
- package/library/actions/mobile/ui/navigate.mjs +1 -1
- package/library/manifests/core.action-manifest.json +100 -11
- package/library/manifests/extension.action-manifest.json +80 -12
- package/library/manifests/mobile.action-manifest.json +85 -10
- package/library/recipes/perps/lifecycle.recipe.json +3 -9
- package/library/recipes/runner/action-validation.extension.recipe.json +5 -4
- package/package.json +5 -4
- package/scripts/completions.sh +2 -2
package/dist/command-contract.js
CHANGED
|
@@ -58,8 +58,8 @@ const PUBLIC_COMMAND_CONTRACTS = {
|
|
|
58
58
|
})
|
|
59
59
|
},
|
|
60
60
|
checklist: {
|
|
61
|
-
usage: "mm-harness checklist mark <task-dir> <step> [options]",
|
|
62
|
-
options: options(HELP, {
|
|
61
|
+
usage: "mm-harness checklist <mark <task-dir> <step> | closeout <task-dir>> [options]",
|
|
62
|
+
options: options(HELP, JSON, {
|
|
63
63
|
"--mark-last": bool(),
|
|
64
64
|
"--already-fixed": bool(),
|
|
65
65
|
"--no-self-review": bool(),
|
|
@@ -67,10 +67,14 @@ const PUBLIC_COMMAND_CONTRACTS = {
|
|
|
67
67
|
"--skip-checklist": bool(),
|
|
68
68
|
"--reason": value(),
|
|
69
69
|
"--checklist": value(),
|
|
70
|
-
"--signal": value()
|
|
70
|
+
"--signal": value(),
|
|
71
|
+
"--share": bool(),
|
|
72
|
+
"--config": value(),
|
|
73
|
+
"--destination": value(),
|
|
74
|
+
"--metadata": value()
|
|
71
75
|
}),
|
|
72
76
|
positionals: [
|
|
73
|
-
{ label: "action", choices: ["mark"] },
|
|
77
|
+
{ label: "action", choices: ["mark", "closeout"] },
|
|
74
78
|
{ label: "task-dir" },
|
|
75
79
|
{
|
|
76
80
|
label: "step",
|
|
@@ -82,7 +86,7 @@ const PUBLIC_COMMAND_CONTRACTS = {
|
|
|
82
86
|
validDescription: "start|complete|no-change|blocked|a positive numeric step"
|
|
83
87
|
}
|
|
84
88
|
],
|
|
85
|
-
minimumPositionals:
|
|
89
|
+
minimumPositionals: 2
|
|
86
90
|
},
|
|
87
91
|
actions: {
|
|
88
92
|
options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
|
|
@@ -307,6 +311,55 @@ function validatePublicInvocation(argv, examples = {}) {
|
|
|
307
311
|
}
|
|
308
312
|
}
|
|
309
313
|
if (seenOptions.has("--help") || seenOptions.has("-h")) return null;
|
|
314
|
+
if (name === "checklist") {
|
|
315
|
+
const action = positionals[0];
|
|
316
|
+
const markOnly = [
|
|
317
|
+
"--mark-last",
|
|
318
|
+
"--already-fixed",
|
|
319
|
+
"--no-self-review",
|
|
320
|
+
"--skip-learnings",
|
|
321
|
+
"--skip-checklist",
|
|
322
|
+
"--reason",
|
|
323
|
+
"--checklist",
|
|
324
|
+
"--signal"
|
|
325
|
+
];
|
|
326
|
+
const closeoutOnly = [
|
|
327
|
+
"--json",
|
|
328
|
+
"--share",
|
|
329
|
+
"--config",
|
|
330
|
+
"--destination",
|
|
331
|
+
"--metadata"
|
|
332
|
+
];
|
|
333
|
+
const invalidOption = (action === "mark" ? closeoutOnly : markOnly).find(
|
|
334
|
+
(option) => seenOptions.has(option)
|
|
335
|
+
);
|
|
336
|
+
if (invalidOption && (action === "mark" || action === "closeout")) {
|
|
337
|
+
return usageFailure(
|
|
338
|
+
"CLI_UNKNOWN_OPTION",
|
|
339
|
+
name,
|
|
340
|
+
`${invalidOption} is not valid with checklist ${action}.`,
|
|
341
|
+
contract,
|
|
342
|
+
examples
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
if (action === "mark" && positionals.length < 3) {
|
|
346
|
+
return missingPositionalFailure(
|
|
347
|
+
name,
|
|
348
|
+
"checklist mark requires <step>.",
|
|
349
|
+
contract,
|
|
350
|
+
examples
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
if (action === "closeout" && positionals.length > 2) {
|
|
354
|
+
return usageFailure(
|
|
355
|
+
"CLI_EXCESS_POSITIONAL",
|
|
356
|
+
name,
|
|
357
|
+
`unexpected positional '${positionals[2]}'; checklist closeout accepts only <task-dir>.`,
|
|
358
|
+
contract,
|
|
359
|
+
examples
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
310
363
|
if (contract.leadingPositionals && !contract.requiredUnless?.some((option) => seenOptions.has(option)) && tokens.slice(0, contract.leadingPositionals).some((argument) => !argument || argument.startsWith("-"))) {
|
|
311
364
|
const missing = contract.positionals?.[0]?.label ?? "argument";
|
|
312
365
|
return missingPositionalFailure(name, `${name} requires <${missing}> first.`, contract, examples);
|
package/dist/commands/call.js
CHANGED
|
@@ -149,7 +149,8 @@ async function handleCall(argv) {
|
|
|
149
149
|
describedAction,
|
|
150
150
|
adapter,
|
|
151
151
|
target,
|
|
152
|
-
process.env.MM_HARNESS_INVOKED_AS ?? process.env.MM_HARNESS_EXECUTABLE ?? "mm-harness"
|
|
152
|
+
process.env.MM_HARNESS_INVOKED_AS ?? process.env.MM_HARNESS_EXECUTABLE ?? "mm-harness",
|
|
153
|
+
args
|
|
153
154
|
) ?? `mm-harness actions --action ${resolvedAction} --adapter ${adapter}` : `mm-harness actions --action ${resolvedAction} --adapter ${adapter}`;
|
|
154
155
|
if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, resolvedAction, args: redactCallValue(args), findings: validation.findings, ...parameterHelp.length > 0 ? { parameterHelp } : {}, error: { code: "RECIPE_VALIDATION_FAILED", message, userAction } }, null, 2));
|
|
155
156
|
else {
|
|
@@ -259,7 +260,8 @@ async function handleCall(argv) {
|
|
|
259
260
|
describedAction,
|
|
260
261
|
adapter,
|
|
261
262
|
target,
|
|
262
|
-
process.env.MM_HARNESS_INVOKED_AS ?? process.env.MM_HARNESS_EXECUTABLE ?? "mm-harness"
|
|
263
|
+
process.env.MM_HARNESS_INVOKED_AS ?? process.env.MM_HARNESS_EXECUTABLE ?? "mm-harness",
|
|
264
|
+
args
|
|
263
265
|
) : void 0;
|
|
264
266
|
const taughtViolation = violation.code === "APP_LOGIC_FAILURE" && conciseFailure.includes(" requires ") && example ? { ...violation, userAction: example } : violation;
|
|
265
267
|
return emitHealViolation(json, "call", result, taughtViolation, state, adapter);
|
|
@@ -331,12 +333,13 @@ function parameterValidationHelp(action, findings, args) {
|
|
|
331
333
|
const property = name && isRecord(properties[name]) ? properties[name] : void 0;
|
|
332
334
|
if (!name || !property) return [];
|
|
333
335
|
const validValues = Array.isArray(property.enum) ? property.enum : void 0;
|
|
336
|
+
const type = typeof property.type === "string" ? property.type : Array.isArray(property.type) ? property.type.filter((value) => typeof value === "string").join("|") : void 0;
|
|
334
337
|
const received = Object.hasOwn(args, name) ? args[name] : void 0;
|
|
335
338
|
const suggestion = issue === "invalid" && received !== void 0 && validValues ? closest(String(received), validValues.map(String)) : void 0;
|
|
336
339
|
return [{
|
|
337
340
|
issue,
|
|
338
341
|
name,
|
|
339
|
-
...
|
|
342
|
+
...type ? { type } : {},
|
|
340
343
|
...validValues ? { validValues } : {},
|
|
341
344
|
...typeof property.description === "string" ? { description: property.description } : {},
|
|
342
345
|
...issue === "invalid" && received !== void 0 ? { received } : {},
|
package/dist/commands/check.js
CHANGED
|
@@ -61,6 +61,7 @@ async function handleCheck(argv) {
|
|
|
61
61
|
});
|
|
62
62
|
const policyFailed = suppressionCheck.status === "fail";
|
|
63
63
|
const fixes = [];
|
|
64
|
+
const eslintConfigArgs = resolveEslintConfigArgs(packageJson);
|
|
64
65
|
if (fix && !policyFailed) {
|
|
65
66
|
fixes.push(
|
|
66
67
|
runToolCheck({
|
|
@@ -70,7 +71,7 @@ async function handleCheck(argv) {
|
|
|
70
71
|
artifactsDir,
|
|
71
72
|
files: existingChangedFiles.filter((file) => JS_TS_EXT_RE.test(file)),
|
|
72
73
|
command: executable(target, "eslint"),
|
|
73
|
-
args: ["--fix", "--cache", "--cache-location", path.join(artifactsDir, "eslintcache")]
|
|
74
|
+
args: [...eslintConfigArgs, "--fix", "--cache", "--cache-location", path.join(artifactsDir, "eslintcache")]
|
|
74
75
|
})
|
|
75
76
|
);
|
|
76
77
|
fixes.push(runFormatterFix({ target, artifactsDir, files: existingChangedFiles.filter((file) => FORMAT_EXT_RE.test(file)) }));
|
|
@@ -85,7 +86,7 @@ async function handleCheck(argv) {
|
|
|
85
86
|
artifactsDir,
|
|
86
87
|
files: existingChangedFiles.filter((file) => JS_TS_EXT_RE.test(file)),
|
|
87
88
|
command: executable(target, "eslint"),
|
|
88
|
-
args: ["--cache", "--cache-location", path.join(artifactsDir, "eslintcache")]
|
|
89
|
+
args: [...eslintConfigArgs, "--cache", "--cache-location", path.join(artifactsDir, "eslintcache")]
|
|
89
90
|
})
|
|
90
91
|
);
|
|
91
92
|
checks.push(runFormatterCheck({ target, artifactsDir, files: existingChangedFiles.filter((file) => FORMAT_EXT_RE.test(file)) }));
|
|
@@ -294,6 +295,12 @@ function executable(target, name) {
|
|
|
294
295
|
const local = path.join(target, "node_modules", ".bin", name);
|
|
295
296
|
return fs.existsSync(local) ? local : null;
|
|
296
297
|
}
|
|
298
|
+
function resolveEslintConfigArgs(packageJson) {
|
|
299
|
+
const lintScript = packageJson?.scripts?.["lint:eslint"];
|
|
300
|
+
if (typeof lintScript !== "string") return [];
|
|
301
|
+
const match = lintScript.match(/(?:^|\s)(?:-c|--config)\s+(['"]?)([^\s'"]+)\1/u);
|
|
302
|
+
return match?.[2] ? ["--config", match[2]] : [];
|
|
303
|
+
}
|
|
297
304
|
function runToolCheck(input) {
|
|
298
305
|
if (input.files.length === 0) {
|
|
299
306
|
return { id: input.id, label: input.label, status: "skip", reason: "no changed matching files" };
|
|
@@ -7,7 +7,96 @@ const require2 = createRequire(import.meta.url);
|
|
|
7
7
|
const MARK_SCRIPT = require2.resolve(
|
|
8
8
|
"@farmslot/agent-runtime/scripts/mark-checklist-step.cjs"
|
|
9
9
|
);
|
|
10
|
-
const USAGE = "mm-harness checklist mark <task-dir> <step> [options]";
|
|
10
|
+
const USAGE = "mm-harness checklist <mark <task-dir> <step> | closeout <task-dir>> [options]";
|
|
11
|
+
const CHECKLIST_VALUE_OPTIONS = /* @__PURE__ */ new Set([
|
|
12
|
+
"--reason",
|
|
13
|
+
"--checklist",
|
|
14
|
+
"--signal",
|
|
15
|
+
"--config",
|
|
16
|
+
"--destination",
|
|
17
|
+
"--metadata"
|
|
18
|
+
]);
|
|
19
|
+
function parseChecklistArgv(argv) {
|
|
20
|
+
const positionals = [];
|
|
21
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
22
|
+
const arg = argv[index] ?? "";
|
|
23
|
+
if (arg.startsWith("-")) {
|
|
24
|
+
const option = arg.split("=", 1)[0] ?? arg;
|
|
25
|
+
if (CHECKLIST_VALUE_OPTIONS.has(option) && !arg.includes("=")) index += 1;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
positionals.push({ index, value: arg });
|
|
29
|
+
}
|
|
30
|
+
const consumed = new Set(positionals.slice(0, 2).map(({ index }) => index));
|
|
31
|
+
return {
|
|
32
|
+
action: positionals[0]?.value,
|
|
33
|
+
taskDir: positionals[1]?.value,
|
|
34
|
+
step: positionals[2]?.value,
|
|
35
|
+
forwarded: argv.filter((_, index) => !consumed.has(index))
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function handoffCommand() {
|
|
39
|
+
if (process.env.HANDOFF_BIN) {
|
|
40
|
+
try {
|
|
41
|
+
fs.accessSync(process.env.HANDOFF_BIN, fs.constants.X_OK);
|
|
42
|
+
return { command: process.env.HANDOFF_BIN, prefix: [] };
|
|
43
|
+
} catch {
|
|
44
|
+
return { command: process.execPath, prefix: [process.env.HANDOFF_BIN] };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
command: process.execPath,
|
|
49
|
+
prefix: [require2.resolve("@farmslot/handoff/cli")]
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function closeoutArgs(taskDir, args) {
|
|
53
|
+
const normalized = args.flatMap((arg) => {
|
|
54
|
+
const option = ["--config", "--destination", "--metadata"].find(
|
|
55
|
+
(name) => arg.startsWith(`${name}=`)
|
|
56
|
+
);
|
|
57
|
+
return option ? [option, arg.slice(option.length + 1)] : [arg];
|
|
58
|
+
});
|
|
59
|
+
if (process.env.METAMASK_AGENTIC_LEARNINGS_DIR && !normalized.includes("--destination")) {
|
|
60
|
+
return [
|
|
61
|
+
"closeout",
|
|
62
|
+
taskDir,
|
|
63
|
+
...normalized,
|
|
64
|
+
"--destination",
|
|
65
|
+
process.env.METAMASK_AGENTIC_LEARNINGS_DIR
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
return ["closeout", taskDir, ...normalized];
|
|
69
|
+
}
|
|
70
|
+
function runCloseout(taskDir, args, stdio) {
|
|
71
|
+
try {
|
|
72
|
+
const { command, prefix } = handoffCommand();
|
|
73
|
+
return spawnSync(command, [...prefix, ...closeoutArgs(taskDir, args)], {
|
|
74
|
+
encoding: "utf8",
|
|
75
|
+
stdio
|
|
76
|
+
});
|
|
77
|
+
} catch (error) {
|
|
78
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
79
|
+
return {
|
|
80
|
+
status: 1,
|
|
81
|
+
stdout: "",
|
|
82
|
+
stderr: message,
|
|
83
|
+
error: new Error(message)
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function stageLearningPackage(taskDir, step, markArgs) {
|
|
88
|
+
if (!["complete", "no-change"].includes(step ?? "") || markArgs.includes("--skip-learnings") || !fs.existsSync(path.join(taskDir, "inputs", "handoff.json"))) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const result = runCloseout(taskDir, ["--json"], "pipe");
|
|
93
|
+
if (result.status === 0) return;
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
console.error(
|
|
97
|
+
`Warning: learning capture could not be staged; rerun: mm-harness checklist closeout ${taskDir}; task verdict is unchanged.`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
11
100
|
function terminalSignalStatus(taskDir) {
|
|
12
101
|
try {
|
|
13
102
|
const target = JSON.parse(
|
|
@@ -20,8 +109,7 @@ function terminalSignalStatus(taskDir) {
|
|
|
20
109
|
return null;
|
|
21
110
|
}
|
|
22
111
|
}
|
|
23
|
-
function preserveTerminalSignal(taskDir,
|
|
24
|
-
const step = markArgs[0];
|
|
112
|
+
function preserveTerminalSignal(taskDir, step) {
|
|
25
113
|
if (step !== "start" && numericMarkStep(step) === null) return false;
|
|
26
114
|
const status = terminalSignalStatus(taskDir);
|
|
27
115
|
if (!status) return false;
|
|
@@ -60,8 +148,7 @@ function checklistStepRequiresDiffPass(checklist, stepNumber) {
|
|
|
60
148
|
}
|
|
61
149
|
return false;
|
|
62
150
|
}
|
|
63
|
-
function checkDiffGateReady(taskDir,
|
|
64
|
-
const step = markArgs[0];
|
|
151
|
+
function checkDiffGateReady(taskDir, step) {
|
|
65
152
|
const stepNumber = numericMarkStep(step);
|
|
66
153
|
if (stepNumber === null) return true;
|
|
67
154
|
let checklist = "";
|
|
@@ -92,8 +179,8 @@ function checkDiffGateReady(taskDir, markArgs) {
|
|
|
92
179
|
console.error("Then rerun the same checklist mark command.");
|
|
93
180
|
return false;
|
|
94
181
|
}
|
|
95
|
-
function teachRecipeCompletion(taskDir,
|
|
96
|
-
if (
|
|
182
|
+
function teachRecipeCompletion(taskDir, step) {
|
|
183
|
+
if (step !== "complete") return;
|
|
97
184
|
const artifactsDir = path.join(taskDir, "artifacts");
|
|
98
185
|
if (!fs.existsSync(path.join(artifactsDir, "recipe.json"))) return;
|
|
99
186
|
const qualityPath = path.join(artifactsDir, "recipe-quality.json");
|
|
@@ -114,25 +201,41 @@ function teachRecipeCompletion(taskDir, markArgs) {
|
|
|
114
201
|
console.error("Then rerun the same checklist complete command.");
|
|
115
202
|
}
|
|
116
203
|
async function handleChecklist(argv) {
|
|
117
|
-
const
|
|
118
|
-
if (
|
|
204
|
+
const { action, taskDir, step, forwarded } = parseChecklistArgv(argv);
|
|
205
|
+
if (!taskDir) {
|
|
119
206
|
console.error(`usage: ${USAGE}`);
|
|
120
207
|
return EXIT.usage;
|
|
121
208
|
}
|
|
122
209
|
const resolvedTaskDir = path.resolve(taskDir);
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
210
|
+
if (action === "closeout") {
|
|
211
|
+
const result2 = runCloseout(resolvedTaskDir, forwarded, "inherit");
|
|
212
|
+
if (result2.error) {
|
|
213
|
+
console.error(`mm-harness checklist closeout: ${result2.error.message}`);
|
|
214
|
+
return EXIT.runtime;
|
|
215
|
+
}
|
|
216
|
+
return result2.status ?? EXIT.runtime;
|
|
217
|
+
}
|
|
218
|
+
if (action !== "mark" || !step) {
|
|
219
|
+
console.error(`usage: ${USAGE}`);
|
|
220
|
+
return EXIT.usage;
|
|
221
|
+
}
|
|
222
|
+
if (preserveTerminalSignal(resolvedTaskDir, step)) return EXIT.ok;
|
|
223
|
+
if (!checkDiffGateReady(resolvedTaskDir, step)) return EXIT.runtime;
|
|
224
|
+
teachRecipeCompletion(resolvedTaskDir, step);
|
|
126
225
|
const result = spawnSync(
|
|
127
226
|
process.execPath,
|
|
128
|
-
[MARK_SCRIPT, resolvedTaskDir, ...
|
|
227
|
+
[MARK_SCRIPT, resolvedTaskDir, ...forwarded],
|
|
129
228
|
{ stdio: "inherit" }
|
|
130
229
|
);
|
|
131
230
|
if (result.error) {
|
|
132
231
|
console.error(`mm-harness checklist mark: ${result.error.message}`);
|
|
133
232
|
return EXIT.runtime;
|
|
134
233
|
}
|
|
135
|
-
|
|
234
|
+
const status = result.status ?? EXIT.runtime;
|
|
235
|
+
if (status === EXIT.ok) {
|
|
236
|
+
stageLearningPackage(resolvedTaskDir, step, forwarded);
|
|
237
|
+
}
|
|
238
|
+
return status;
|
|
136
239
|
}
|
|
137
240
|
export {
|
|
138
241
|
handleChecklist
|
|
@@ -2,7 +2,10 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import http from "node:http";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
depsCheck,
|
|
7
|
+
recordDepsBaseline
|
|
8
|
+
} from "@farmslot/recipe-harness/runtime/deps-readiness";
|
|
6
9
|
import { colorHumanMessage } from "../../cli-color.js";
|
|
7
10
|
import { recipeHarnessPath, recipeRuntimeDir, runnerDir } from "../../paths.js";
|
|
8
11
|
import { extensionIdFromKey } from "../../adapters/extension/extension-id.js";
|
|
@@ -27,9 +30,11 @@ function extensionDepsBlock(target) {
|
|
|
27
30
|
}
|
|
28
31
|
return null;
|
|
29
32
|
}
|
|
30
|
-
function installExtensionDeps(target) {
|
|
33
|
+
async function installExtensionDeps(target) {
|
|
31
34
|
const installScript = path.join(runnerDir, "adapters/shared/install-repo-deps.sh");
|
|
32
|
-
|
|
35
|
+
const result = await spawnScriptStreaming(installScript, ["--target", target], target);
|
|
36
|
+
if (result.status === 0) recordDepsBaseline(target);
|
|
37
|
+
return result;
|
|
33
38
|
}
|
|
34
39
|
async function launchExtension(target, tier, wantWatch, displayMode = "fullscreen") {
|
|
35
40
|
if (process.env.CHROME_USER_DATA_DIR) {
|
|
@@ -196,28 +196,28 @@ async function handleLaunchLocked(argv, stream) {
|
|
|
196
196
|
exitCode: EXIT.infra
|
|
197
197
|
});
|
|
198
198
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const recoveryCode2 = "deps.installed";
|
|
205
|
-
state.attemptedRecoveries.push(recoveryCode2);
|
|
206
|
-
stream.phase("recover");
|
|
207
|
-
const repaired = await installExtensionDeps(target);
|
|
208
|
-
if (repaired.status !== 0 || extensionDepsBlock(target)) {
|
|
209
|
-
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
210
|
-
code: "DEPENDENCY_INSTALL_FAILED",
|
|
211
|
-
message: depsBlock.message,
|
|
212
|
-
recoverable: false,
|
|
213
|
-
userAction: "install the checkout-pinned Node version, then re-run mm-harness launch",
|
|
214
|
-
originalError: repaired.output,
|
|
215
|
-
exitCode: EXIT.infra
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
state.recovered.push(recoveryCode2);
|
|
219
|
-
state.mutations.push({ type: "dependencies", action: "installed", path: target });
|
|
199
|
+
}
|
|
200
|
+
const depsBlock = extensionDepsBlock(target);
|
|
201
|
+
if (depsBlock) {
|
|
202
|
+
if (heal === "off") {
|
|
203
|
+
return launchUsage(jsonOutput, stream, depsBlock.message, depsBlock.userAction);
|
|
220
204
|
}
|
|
205
|
+
const recoveryCode2 = "deps.installed";
|
|
206
|
+
state.attemptedRecoveries.push(recoveryCode2);
|
|
207
|
+
stream.phase("recover");
|
|
208
|
+
const repaired = await installExtensionDeps(target);
|
|
209
|
+
if (repaired.status !== 0 || extensionDepsBlock(target)) {
|
|
210
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
211
|
+
code: "DEPENDENCY_INSTALL_FAILED",
|
|
212
|
+
message: depsBlock.message,
|
|
213
|
+
recoverable: false,
|
|
214
|
+
userAction: "install the checkout-pinned Node version, then re-run mm-harness launch",
|
|
215
|
+
originalError: repaired.output,
|
|
216
|
+
exitCode: EXIT.infra
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
state.recovered.push(recoveryCode2);
|
|
220
|
+
state.mutations.push({ type: "dependencies", action: "installed", path: target });
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
223
|
stream.phase("launch");
|
|
@@ -290,10 +290,20 @@ async function handleLaunchLocked(argv, stream) {
|
|
|
290
290
|
originalError: bound.originalError
|
|
291
291
|
});
|
|
292
292
|
}
|
|
293
|
-
const
|
|
293
|
+
const restartMobileApp = adapter === "mobile" && mobileBridgeTargetMissing(attempt.output);
|
|
294
|
+
const recoveryCode = restartMobileApp ? "mobile.app-restarted" : RECOVERY_CODE[adapter];
|
|
294
295
|
state.attemptedRecoveries.push(recoveryCode);
|
|
295
296
|
stream.phase("recover");
|
|
296
|
-
attempt = await executeComposition(
|
|
297
|
+
attempt = await executeComposition(
|
|
298
|
+
adapter,
|
|
299
|
+
mobileTarget,
|
|
300
|
+
tier,
|
|
301
|
+
wantWatch,
|
|
302
|
+
target,
|
|
303
|
+
machine,
|
|
304
|
+
displayMode,
|
|
305
|
+
restartMobileApp
|
|
306
|
+
);
|
|
297
307
|
if (attempt.status === 0) {
|
|
298
308
|
state.recovered.push(recoveryCode);
|
|
299
309
|
return await finishLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify);
|
|
@@ -321,6 +331,9 @@ function extensionProductConfigMissing(output) {
|
|
|
321
331
|
function mobileProvisioningBlocked(output) {
|
|
322
332
|
return /open-device: configured iOS simulator '.+' does not exist|open-device: no MetaMask bundle found|fast mode requires an installed (?:iOS dev client|Android dev client)/u.test(output);
|
|
323
333
|
}
|
|
334
|
+
function mobileBridgeTargetMissing(output) {
|
|
335
|
+
return /no bridge target matched/iu.test(output);
|
|
336
|
+
}
|
|
324
337
|
function mobileProvisionCommand(target, mobileTarget) {
|
|
325
338
|
const platform = mobileTarget === "android" ? "android" : "ios";
|
|
326
339
|
const device = platform === "ios" ? process.env.IOS_SIMULATOR || process.env.SIM_UDID : process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || process.env.ANDROID_DEVICE;
|
|
@@ -406,9 +419,9 @@ function nativeInputsChanged(target, adapter) {
|
|
|
406
419
|
return false;
|
|
407
420
|
}
|
|
408
421
|
}
|
|
409
|
-
async function executeComposition(adapter, mobileTarget, tier, wantWatch, target, json, displayMode = "fullscreen") {
|
|
422
|
+
async function executeComposition(adapter, mobileTarget, tier, wantWatch, target, json, displayMode = "fullscreen", restartMobileApp = false) {
|
|
410
423
|
if (adapter === "mobile") {
|
|
411
|
-
return launchMobile(target, mobileTarget, tier, json);
|
|
424
|
+
return launchMobile(target, mobileTarget, tier, json, restartMobileApp);
|
|
412
425
|
}
|
|
413
426
|
return launchExtension(target, tier, wantWatch, displayMode);
|
|
414
427
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { prepareMobile } from "../../adapters/mobile/prepare.js";
|
|
2
2
|
import { ensureHarnessFresh } from "../../adapters/harness-freshness.js";
|
|
3
|
-
async function launchMobile(target, mobileTarget, tier, json) {
|
|
3
|
+
async function launchMobile(target, mobileTarget, tier, json, restartApp = false) {
|
|
4
4
|
await ensureHarnessFresh(target, "mobile");
|
|
5
5
|
const platform = mobileTarget ?? "ios";
|
|
6
6
|
const watcherPort = process.env.WATCHER_PORT ? parseInt(process.env.WATCHER_PORT, 10) : void 0;
|
|
7
7
|
const preflightMode = tier === "build" ? "auto" : "fast";
|
|
8
|
-
return prepareMobile(target, { platform, json, watcherPort, preflightMode });
|
|
8
|
+
return prepareMobile(target, { platform, json, watcherPort, preflightMode, restartApp });
|
|
9
9
|
}
|
|
10
10
|
export {
|
|
11
11
|
launchMobile
|
|
@@ -10,10 +10,12 @@ import {
|
|
|
10
10
|
shellQuoteArg
|
|
11
11
|
} from "./parse-args.js";
|
|
12
12
|
import { resolveMetaMaskLibrarySources } from "./run-engine.js";
|
|
13
|
+
import { isSensitiveKey, redactStructuredValue } from "../command-journal.js";
|
|
13
14
|
import {
|
|
14
15
|
officialRecipeActionCapabilities
|
|
15
16
|
} from "@farmslot/protocol";
|
|
16
17
|
import { metaMaskActionExecutionCapabilities } from "../recipe-security.js";
|
|
18
|
+
import { closest } from "../command-contract.js";
|
|
17
19
|
async function handleActions({ options, positional }) {
|
|
18
20
|
const { adapter, target } = resolveAdapter(options);
|
|
19
21
|
const librarySources = await resolveMetaMaskLibrarySources(optionStrings(options, "library"));
|
|
@@ -174,7 +176,7 @@ function renderHumanActionCatalog(actions, options) {
|
|
|
174
176
|
return lines.join("\n");
|
|
175
177
|
}
|
|
176
178
|
function renderHumanActionExample(entry, adapter, target, executable) {
|
|
177
|
-
const node = authoredExampleNode(entry.examples);
|
|
179
|
+
const node = authoredExampleNode(entry.examples, void 0, entry.schema);
|
|
178
180
|
if (!node) return void 0;
|
|
179
181
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
180
182
|
const command = actionExampleCommand(entry, adapter, target, executable);
|
|
@@ -187,12 +189,12 @@ function renderHumanActionExample(entry, adapter, target, executable) {
|
|
|
187
189
|
];
|
|
188
190
|
return lines.join("\n");
|
|
189
191
|
}
|
|
190
|
-
function actionExampleCommand(entry, adapter, target, executable) {
|
|
191
|
-
const node = authoredExampleNode(entry.examples);
|
|
192
|
+
function actionExampleCommand(entry, adapter, target, executable, preferredValues) {
|
|
193
|
+
const node = authoredExampleNode(entry.examples, preferredValues, entry.schema);
|
|
192
194
|
if (!node) return void 0;
|
|
193
195
|
const commandName = executable.endsWith("/mm-harness") ? "mm-harness" : executable;
|
|
194
196
|
const args = entry.fields.flatMap(
|
|
195
|
-
(field) => Object.hasOwn(node, field) ? [shellQuoteArg(`${field}=${
|
|
197
|
+
(field) => Object.hasOwn(node, field) ? [shellQuoteArg(`${field}=${safeActionCallValue(field, node[field])}`)] : []
|
|
196
198
|
);
|
|
197
199
|
return [
|
|
198
200
|
shellQuoteArg(commandName),
|
|
@@ -205,16 +207,81 @@ function actionExampleCommand(entry, adapter, target, executable) {
|
|
|
205
207
|
shellQuoteArg(target)
|
|
206
208
|
].join(" ");
|
|
207
209
|
}
|
|
208
|
-
function authoredExampleNode(examples) {
|
|
210
|
+
function authoredExampleNode(examples, preferredValues, schema) {
|
|
209
211
|
if (!Array.isArray(examples)) return void 0;
|
|
212
|
+
if (preferredValues && Object.keys(preferredValues).length > 0) {
|
|
213
|
+
const normalizedValues = normalizePreferredValues(preferredValues, schema);
|
|
214
|
+
const nodes = examples.map((example) => isRecord(example) && isRecord(example.node) ? example.node : void 0).filter((node) => node !== void 0);
|
|
215
|
+
if (nodes.length === 0) return void 0;
|
|
216
|
+
const ranked = nodes.map((node) => ({
|
|
217
|
+
node,
|
|
218
|
+
completeness: Object.keys(node).filter((name) => !["action", "intent", "next"].includes(name)).length,
|
|
219
|
+
score: Object.entries(normalizedValues).reduce((score, [name, value]) => {
|
|
220
|
+
if (!Object.hasOwn(node, name)) return score;
|
|
221
|
+
return score + (sameActionValue(name, node[name], value) ? 3 : -1);
|
|
222
|
+
}, 0)
|
|
223
|
+
})).sort(
|
|
224
|
+
(left, right) => right.score - left.score || right.completeness - left.completeness
|
|
225
|
+
);
|
|
226
|
+
return { ...ranked[0].node, ...normalizedValues };
|
|
227
|
+
}
|
|
210
228
|
for (const example of examples) {
|
|
211
229
|
if (isRecord(example) && isRecord(example.node)) return example.node;
|
|
212
230
|
}
|
|
213
231
|
return void 0;
|
|
214
232
|
}
|
|
233
|
+
function normalizePreferredValues(preferredValues, schema) {
|
|
234
|
+
const properties = isRecord(schema) && isRecord(schema.properties) ? schema.properties : {};
|
|
235
|
+
return Object.fromEntries(
|
|
236
|
+
Object.entries(preferredValues).flatMap(([name, value]) => {
|
|
237
|
+
const property = isRecord(properties[name]) ? properties[name] : void 0;
|
|
238
|
+
if (property && !matchesSchemaType(property.type, value)) return [];
|
|
239
|
+
const values = property && Array.isArray(property.enum) ? property.enum : void 0;
|
|
240
|
+
if (!values) {
|
|
241
|
+
return [[name, value]];
|
|
242
|
+
}
|
|
243
|
+
const compatibleValue = values.find(
|
|
244
|
+
(candidate) => sameActionValue(name, candidate, value)
|
|
245
|
+
);
|
|
246
|
+
if (compatibleValue !== void 0) return [[name, compatibleValue]];
|
|
247
|
+
const strings = values.filter((candidate) => typeof candidate === "string");
|
|
248
|
+
const suggestion = typeof value === "string" ? closest(value, strings) : void 0;
|
|
249
|
+
return suggestion === void 0 ? [] : [[name, suggestion]];
|
|
250
|
+
})
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
function matchesSchemaType(type, value) {
|
|
254
|
+
const types = Array.isArray(type) ? type : [type];
|
|
255
|
+
if (types.every((candidate) => typeof candidate !== "string")) return true;
|
|
256
|
+
return types.some((candidate) => {
|
|
257
|
+
if (candidate === "null") return value === null;
|
|
258
|
+
if (candidate === "array") return Array.isArray(value);
|
|
259
|
+
if (candidate === "object") return isRecord(value);
|
|
260
|
+
if (candidate === "integer") return typeof value === "number" && Number.isInteger(value);
|
|
261
|
+
return typeof value === candidate;
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
function sameActionValue(name, left, right) {
|
|
265
|
+
if (name === "state") {
|
|
266
|
+
const canonicalState = (value) => {
|
|
267
|
+
if (value === "present") return "open";
|
|
268
|
+
if (value === "absent") return "none";
|
|
269
|
+
return value;
|
|
270
|
+
};
|
|
271
|
+
left = canonicalState(left);
|
|
272
|
+
right = canonicalState(right);
|
|
273
|
+
}
|
|
274
|
+
const scalar = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
275
|
+
if (scalar(left) && scalar(right)) return String(left) === String(right);
|
|
276
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
277
|
+
}
|
|
215
278
|
function actionCallValue(value) {
|
|
216
279
|
return typeof value === "string" ? value : JSON.stringify(value);
|
|
217
280
|
}
|
|
281
|
+
function safeActionCallValue(field, value) {
|
|
282
|
+
if (isSensitiveKey(field)) return `<${field}>`;
|
|
283
|
+
return actionCallValue(redactStructuredValue(value));
|
|
284
|
+
}
|
|
218
285
|
function humanFieldNames(entry) {
|
|
219
286
|
const schema = isRecord(entry.schema) ? entry.schema : void 0;
|
|
220
287
|
const properties = schema && isRecord(schema.properties) ? schema.properties : void 0;
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from "../paths.js";
|
|
21
21
|
import { captureHelperSupportsRecordSessionSnapshots } from "../recording-target.js";
|
|
22
22
|
import { startRecipeRecording, stopRecipeRecording } from "../run-recording.js";
|
|
23
|
+
import { validateMetaMaskActionInputs } from "../metamask-action-validation.js";
|
|
23
24
|
import {
|
|
24
25
|
beginRunDiagnostics,
|
|
25
26
|
finishRunDiagnostics
|
|
@@ -276,7 +277,10 @@ async function validateRecipeAdapterAware(adapter, recipe, manifest, librarySour
|
|
|
276
277
|
manifest,
|
|
277
278
|
validationOptions
|
|
278
279
|
);
|
|
279
|
-
const findings = [
|
|
280
|
+
const findings = [
|
|
281
|
+
...withManifest.findings,
|
|
282
|
+
...validateMetaMaskActionInputs(recipe)
|
|
283
|
+
];
|
|
280
284
|
if (withManifest.status === "valid" && isRecord(recipe) && libraryResolution) {
|
|
281
285
|
try {
|
|
282
286
|
const digest = protocol.digestRecipeDocument(recipe);
|
|
@@ -304,7 +308,8 @@ async function validateRecipeAdapterAware(adapter, recipe, manifest, librarySour
|
|
|
304
308
|
dependency.document,
|
|
305
309
|
manifest,
|
|
306
310
|
validationOptions
|
|
307
|
-
).findings
|
|
311
|
+
).findings,
|
|
312
|
+
...validateMetaMaskActionInputs(dependency.document)
|
|
308
313
|
);
|
|
309
314
|
}
|
|
310
315
|
} catch (error) {
|
package/dist/heal-bounds.js
CHANGED
|
@@ -62,7 +62,7 @@ async function ensureOverlay(adapter, target, heal, state, json) {
|
|
|
62
62
|
}
|
|
63
63
|
function classifyFailure(output) {
|
|
64
64
|
if (/wallet|fixture|keyring|not seeded|\bsrp\b|password|onboard/iu.test(output)) return "wallet";
|
|
65
|
-
if (/metro|cdp|chrome|bundle|packager|port\b|econnrefused|not reachable|watcher|dev client|websocket/iu.test(output)) {
|
|
65
|
+
if (/metro|cdp|chrome|bridge|bundle|packager|port\b|econnrefused|not reachable|watcher|dev client|websocket/iu.test(output)) {
|
|
66
66
|
return "infra";
|
|
67
67
|
}
|
|
68
68
|
return "app";
|