@deeeed/metamask-harness 0.19.1 → 0.21.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 +28 -0
- package/adapters/core/inject.sh +1 -3
- package/adapters/extension/ensure-browser.sh +6 -4
- package/adapters/extension/inject.mjs +1 -1
- 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/inject.sh +1 -1
- 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/command-contract.js +58 -5
- package/dist/commands/call.js +7 -4
- 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 +86 -25
- package/dist/commands/run-engine.js +7 -2
- package/dist/heal-bounds.js +1 -1
- package/dist/manifest.js +85 -101
- package/dist/metamask-action-validation.js +45 -0
- package/dist/mm-harness-cli.js +7 -4
- package/dist/runner.js +9 -4
- package/docs/CONTRIBUTING.md +8 -0
- package/docs/RECIPES.md +2 -11
- package/library/actions/extension/platform/cdp.mjs +8 -3
- package/library/actions/extension/ui/navigate.mjs +239 -16
- package/library/actions/mobile/ui/navigate.mjs +1 -1
- package/library/manifests/core.action-manifest.json +357 -489
- package/library/manifests/extension.action-manifest.json +616 -885
- package/library/manifests/mobile.action-manifest.json +647 -931
- 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/library/library.json +0 -7
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 } : {},
|
|
@@ -467,7 +470,7 @@ function renderCallExamples(short, examples) {
|
|
|
467
470
|
if (!Array.isArray(examples)) return [];
|
|
468
471
|
const out = [];
|
|
469
472
|
for (const example of examples.slice(0, 2)) {
|
|
470
|
-
const node = isRecord(example)
|
|
473
|
+
const node = isRecord(example) ? example : void 0;
|
|
471
474
|
if (!node) continue;
|
|
472
475
|
const tokens = Object.entries(node).filter(([key]) => key !== "action" && key !== "intent").map(([key, value]) => `${key}=${typeof value === "string" ? value : JSON.stringify(value)}`);
|
|
473
476
|
out.push(`mm-harness call ${short} ${tokens.join(" ")}`.trim());
|
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,14 @@ 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 {
|
|
15
|
+
OFFICIAL_RECIPE_ACTIONS,
|
|
14
16
|
officialRecipeActionCapabilities
|
|
15
17
|
} from "@farmslot/protocol";
|
|
16
18
|
import { metaMaskActionExecutionCapabilities } from "../recipe-security.js";
|
|
19
|
+
import { closest } from "../command-contract.js";
|
|
20
|
+
const OFFICIAL_ACTIONS = new Set(OFFICIAL_RECIPE_ACTIONS);
|
|
17
21
|
async function handleActions({ options, positional }) {
|
|
18
22
|
const { adapter, target } = resolveAdapter(options);
|
|
19
23
|
const librarySources = await resolveMetaMaskLibrarySources(optionStrings(options, "library"));
|
|
@@ -174,7 +178,7 @@ function renderHumanActionCatalog(actions, options) {
|
|
|
174
178
|
return lines.join("\n");
|
|
175
179
|
}
|
|
176
180
|
function renderHumanActionExample(entry, adapter, target, executable) {
|
|
177
|
-
const node = authoredExampleNode(entry.examples);
|
|
181
|
+
const node = authoredExampleNode(entry.examples, void 0, entry.schema);
|
|
178
182
|
if (!node) return void 0;
|
|
179
183
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
180
184
|
const command = actionExampleCommand(entry, adapter, target, executable);
|
|
@@ -187,12 +191,12 @@ function renderHumanActionExample(entry, adapter, target, executable) {
|
|
|
187
191
|
];
|
|
188
192
|
return lines.join("\n");
|
|
189
193
|
}
|
|
190
|
-
function actionExampleCommand(entry, adapter, target, executable) {
|
|
191
|
-
const node = authoredExampleNode(entry.examples);
|
|
194
|
+
function actionExampleCommand(entry, adapter, target, executable, preferredValues) {
|
|
195
|
+
const node = authoredExampleNode(entry.examples, preferredValues, entry.schema);
|
|
192
196
|
if (!node) return void 0;
|
|
193
197
|
const commandName = executable.endsWith("/mm-harness") ? "mm-harness" : executable;
|
|
194
198
|
const args = entry.fields.flatMap(
|
|
195
|
-
(field) => Object.hasOwn(node, field) ? [shellQuoteArg(`${field}=${
|
|
199
|
+
(field) => Object.hasOwn(node, field) ? [shellQuoteArg(`${field}=${safeActionCallValue(field, node[field])}`)] : []
|
|
196
200
|
);
|
|
197
201
|
return [
|
|
198
202
|
shellQuoteArg(commandName),
|
|
@@ -205,16 +209,81 @@ function actionExampleCommand(entry, adapter, target, executable) {
|
|
|
205
209
|
shellQuoteArg(target)
|
|
206
210
|
].join(" ");
|
|
207
211
|
}
|
|
208
|
-
function authoredExampleNode(examples) {
|
|
212
|
+
function authoredExampleNode(examples, preferredValues, schema) {
|
|
209
213
|
if (!Array.isArray(examples)) return void 0;
|
|
214
|
+
if (preferredValues && Object.keys(preferredValues).length > 0) {
|
|
215
|
+
const normalizedValues = normalizePreferredValues(preferredValues, schema);
|
|
216
|
+
const nodes = examples.filter(isRecord);
|
|
217
|
+
if (nodes.length === 0) return void 0;
|
|
218
|
+
const ranked = nodes.map((node) => ({
|
|
219
|
+
node,
|
|
220
|
+
completeness: Object.keys(node).filter((name) => !["action", "intent", "next"].includes(name)).length,
|
|
221
|
+
score: Object.entries(normalizedValues).reduce((score, [name, value]) => {
|
|
222
|
+
if (!Object.hasOwn(node, name)) return score;
|
|
223
|
+
return score + (sameActionValue(name, node[name], value) ? 3 : -1);
|
|
224
|
+
}, 0)
|
|
225
|
+
})).sort(
|
|
226
|
+
(left, right) => right.score - left.score || right.completeness - left.completeness
|
|
227
|
+
);
|
|
228
|
+
return { ...ranked[0].node, ...normalizedValues };
|
|
229
|
+
}
|
|
210
230
|
for (const example of examples) {
|
|
211
|
-
if (isRecord(example)
|
|
231
|
+
if (isRecord(example)) return example;
|
|
212
232
|
}
|
|
213
233
|
return void 0;
|
|
214
234
|
}
|
|
235
|
+
function normalizePreferredValues(preferredValues, schema) {
|
|
236
|
+
const properties = isRecord(schema) && isRecord(schema.properties) ? schema.properties : {};
|
|
237
|
+
return Object.fromEntries(
|
|
238
|
+
Object.entries(preferredValues).flatMap(([name, value]) => {
|
|
239
|
+
const property = isRecord(properties[name]) ? properties[name] : void 0;
|
|
240
|
+
if (property && !matchesSchemaType(property.type, value)) return [];
|
|
241
|
+
const values = property && Array.isArray(property.enum) ? property.enum : void 0;
|
|
242
|
+
if (!values) {
|
|
243
|
+
return [[name, value]];
|
|
244
|
+
}
|
|
245
|
+
const compatibleValue = values.find(
|
|
246
|
+
(candidate) => sameActionValue(name, candidate, value)
|
|
247
|
+
);
|
|
248
|
+
if (compatibleValue !== void 0) return [[name, compatibleValue]];
|
|
249
|
+
const strings = values.filter((candidate) => typeof candidate === "string");
|
|
250
|
+
const suggestion = typeof value === "string" ? closest(value, strings) : void 0;
|
|
251
|
+
return suggestion === void 0 ? [] : [[name, suggestion]];
|
|
252
|
+
})
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
function matchesSchemaType(type, value) {
|
|
256
|
+
const types = Array.isArray(type) ? type : [type];
|
|
257
|
+
if (types.every((candidate) => typeof candidate !== "string")) return true;
|
|
258
|
+
return types.some((candidate) => {
|
|
259
|
+
if (candidate === "null") return value === null;
|
|
260
|
+
if (candidate === "array") return Array.isArray(value);
|
|
261
|
+
if (candidate === "object") return isRecord(value);
|
|
262
|
+
if (candidate === "integer") return typeof value === "number" && Number.isInteger(value);
|
|
263
|
+
return typeof value === candidate;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
function sameActionValue(name, left, right) {
|
|
267
|
+
if (name === "state") {
|
|
268
|
+
const canonicalState = (value) => {
|
|
269
|
+
if (value === "present") return "open";
|
|
270
|
+
if (value === "absent") return "none";
|
|
271
|
+
return value;
|
|
272
|
+
};
|
|
273
|
+
left = canonicalState(left);
|
|
274
|
+
right = canonicalState(right);
|
|
275
|
+
}
|
|
276
|
+
const scalar = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
277
|
+
if (scalar(left) && scalar(right)) return String(left) === String(right);
|
|
278
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
279
|
+
}
|
|
215
280
|
function actionCallValue(value) {
|
|
216
281
|
return typeof value === "string" ? value : JSON.stringify(value);
|
|
217
282
|
}
|
|
283
|
+
function safeActionCallValue(field, value) {
|
|
284
|
+
if (isSensitiveKey(field)) return `<${field}>`;
|
|
285
|
+
return actionCallValue(redactStructuredValue(value));
|
|
286
|
+
}
|
|
218
287
|
function humanFieldNames(entry) {
|
|
219
288
|
const schema = isRecord(entry.schema) ? entry.schema : void 0;
|
|
220
289
|
const properties = schema && isRecord(schema.properties) ? schema.properties : void 0;
|
|
@@ -238,22 +307,15 @@ function summarizeActionCategories(actions) {
|
|
|
238
307
|
}
|
|
239
308
|
function describeManifestActions(manifest, actionSources = /* @__PURE__ */ new Map()) {
|
|
240
309
|
const manifestRecord = isRecord(manifest) ? manifest : {};
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
return [];
|
|
252
|
-
}) : [];
|
|
253
|
-
return [
|
|
254
|
-
...official.map((name) => describeManifestAction(name, "official", metadata[name], actionSources.get(name))),
|
|
255
|
-
...custom.map((entry) => describeManifestAction(entry.name, "custom", entry.metadata, actionSources.get(entry.name)))
|
|
256
|
-
];
|
|
310
|
+
const actions = isRecord(manifestRecord.actions) ? manifestRecord.actions : {};
|
|
311
|
+
return Object.entries(actions).map(
|
|
312
|
+
([name, metadata]) => describeManifestAction(
|
|
313
|
+
name,
|
|
314
|
+
OFFICIAL_ACTIONS.has(name) ? "official" : "custom",
|
|
315
|
+
metadata,
|
|
316
|
+
actionSources.get(name)
|
|
317
|
+
)
|
|
318
|
+
);
|
|
257
319
|
}
|
|
258
320
|
function describeManifestAction(name, kind, metadata, source) {
|
|
259
321
|
const record = isRecord(metadata) ? metadata : {};
|
|
@@ -263,7 +325,7 @@ function describeManifestAction(name, kind, metadata, source) {
|
|
|
263
325
|
return {
|
|
264
326
|
name,
|
|
265
327
|
kind,
|
|
266
|
-
category: actionCategory(name
|
|
328
|
+
category: actionCategory(name),
|
|
267
329
|
description: typeof record.description === "string" ? record.description : "",
|
|
268
330
|
fields: properties,
|
|
269
331
|
schema,
|
|
@@ -280,8 +342,7 @@ function describeManifestAction(name, kind, metadata, source) {
|
|
|
280
342
|
])]
|
|
281
343
|
};
|
|
282
344
|
}
|
|
283
|
-
function actionCategory(name
|
|
284
|
-
if (typeof configured === "string" && configured.trim()) return configured.trim().toLowerCase();
|
|
345
|
+
function actionCategory(name) {
|
|
285
346
|
const segments = name.split(".");
|
|
286
347
|
if (segments[0] === "metamask" && segments.length > 2) return segments[1] ?? "metamask";
|
|
287
348
|
if (segments[0] === "app" || segments[0] === "cdp") return "runtime";
|