@deeeed/metamask-harness 0.14.5 → 0.15.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 +96 -6
- package/adapters/extension/ensure-browser.sh +4 -2
- package/adapters/extension/launch-browser.cjs +73 -34
- package/adapters/extension/live.sh +21 -10
- package/adapters/extension/reattach.sh +3 -3
- package/adapters/extension/sidepanel-toggle.sh +10 -4
- package/adapters/extension/start-watch.sh +2 -2
- package/adapters/extension/verify.sh +19 -2
- package/adapters/extension/wallet-fixture-state.cjs +14 -1
- package/adapters/manifest.json +19 -3
- package/adapters/mobile/bridge-runtime/setup-wallet.sh +6 -4
- package/adapters/mobile/launch-metro.cjs +74 -0
- package/adapters/mobile/lib/tmux-viewer.sh +4 -4
- package/adapters/mobile/open-device.sh +3 -1
- package/adapters/mobile/start-metro.sh +12 -18
- package/adapters/mobile/stop-metro.sh +1 -0
- package/adapters/mobile/verify.sh +5 -5
- package/adapters/shared/install-repo-deps.sh +29 -0
- package/adapters/shared/open-debug.mjs +35 -4
- package/adapters/shared/open-log-window.sh +1 -1
- package/adapters/shared/resolve-slot-ports-core.mjs +72 -1
- package/adapters/shared/resolve-slot-ports.sh +21 -17
- package/adapters/shared/tmux-session.sh +0 -5
- package/adapters/shared/tmux-viewer.sh +7 -2
- package/dist/adapters/core/surface.js +3 -0
- package/dist/adapters/extension/ensure-ready.js +0 -7
- package/dist/adapters/extension/runtime-decision.js +93 -3
- package/dist/adapters/extension/surface.js +17 -6
- package/dist/adapters/mobile/metro-env.js +67 -0
- package/dist/adapters/mobile/perps-env.js +101 -0
- package/dist/adapters/mobile/prepare.js +32 -14
- package/dist/adapters/mobile/runtime-decision.js +21 -2
- package/dist/adapters/mobile/source-freshness.js +116 -0
- package/dist/adapters/mobile/surface.js +5 -1
- package/dist/adapters/resolve-slot-ports.js +4 -0
- package/dist/adapters/slot-ports.js +131 -49
- package/dist/adapters.js +7 -2
- package/dist/checkout-lock.js +72 -0
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +2 -0
- package/dist/commands/call.js +91 -54
- package/dist/commands/check.js +266 -46
- package/dist/commands/checklist.js +136 -0
- package/dist/commands/debug.js +21 -2
- package/dist/commands/doctor.js +233 -18
- package/dist/commands/farmslot-ready.js +25 -0
- package/dist/commands/fixtures.js +177 -42
- package/dist/commands/launch/extension.js +6 -1
- package/dist/commands/launch/index.js +54 -22
- package/dist/commands/logs.js +24 -4
- package/dist/commands/parse-args.js +1 -0
- package/dist/commands/run-engine.js +223 -7
- package/dist/commands/run.js +67 -50
- package/dist/commands/shared.js +86 -5
- package/dist/commands/stop.js +1 -2
- package/dist/doctor.js +39 -17
- package/dist/harness.js +5 -2
- package/dist/heal-bounds.js +1 -1
- package/dist/mm-harness-cli.js +41 -12
- package/dist/runner.js +35 -5
- package/dist/runtime-context.js +228 -0
- package/docs/CLI-SPEC.md +15 -11
- package/docs/MENTAL-MODEL.md +6 -6
- package/library/actions/extension/perps/perps.mjs +29 -8
- package/library/actions/extension/platform/cdp.mjs +128 -28
- package/library/actions/extension/ui/navigate.mjs +1 -1
- package/library/actions/mobile/perps/perps.mjs +13 -1
- package/library/actions/mobile/platform/bridge.mjs +302 -29
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +7 -2
- package/library/manifests/extension.action-manifest.json +32 -12
- package/library/manifests/mobile.action-manifest.json +25 -3
- package/package.json +4 -4
- package/scripts/completions.sh +7 -4
package/dist/commands/run.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { walletFixturePath } from "../paths.js";
|
|
4
4
|
import { color } from "../cli-color.js";
|
|
5
5
|
import { recipeRunning } from "../heal-bounds.js";
|
|
6
|
-
import { EXIT } from "./shared.js";
|
|
6
|
+
import { checkoutBusyOut, EXIT, usageOut } from "./shared.js";
|
|
7
7
|
import {
|
|
8
8
|
optionFlag,
|
|
9
9
|
optionString,
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
emitHealViolation,
|
|
19
19
|
executeWithHealBounds,
|
|
20
20
|
prepareHeal,
|
|
21
|
+
recoverRunInfra,
|
|
21
22
|
runRecipe,
|
|
22
23
|
validateRunRecipeStatic
|
|
23
24
|
} from "./run-engine.js";
|
|
@@ -26,6 +27,7 @@ import { applyDeviceTargeting } from "./device-target.js";
|
|
|
26
27
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
27
28
|
import { coreDependencyBlock } from "./core-readiness.js";
|
|
28
29
|
import { writeRunReport } from "./run-report.js";
|
|
30
|
+
import { acquireCheckoutLock } from "../checkout-lock.js";
|
|
29
31
|
async function handleRun({ positional, options }) {
|
|
30
32
|
if (optionFlag(options, "list")) return handleListExecutables("run", options);
|
|
31
33
|
const targetRecipe = positional[0];
|
|
@@ -33,6 +35,14 @@ async function handleRun({ positional, options }) {
|
|
|
33
35
|
if (optionFlag(options, "plan")) return handleRunPlan(targetRecipe, options);
|
|
34
36
|
const { adapter, target } = resolveAdapter(options);
|
|
35
37
|
const json = optionFlag(options, "json");
|
|
38
|
+
if (!fs.existsSync(target)) {
|
|
39
|
+
return usageOut(
|
|
40
|
+
json,
|
|
41
|
+
"run",
|
|
42
|
+
`target does not exist: ${target}`,
|
|
43
|
+
"pass --target <metamask-checkout> pointing to an existing checkout"
|
|
44
|
+
);
|
|
45
|
+
}
|
|
36
46
|
getAdapterSurface(adapter).resolveSlotPorts(target);
|
|
37
47
|
const dtResult = applyDeviceTargeting("run", adapter, options, { gate: true, rerun: "" });
|
|
38
48
|
if ("code" in dtResult) {
|
|
@@ -54,56 +64,63 @@ async function handleRun({ positional, options }) {
|
|
|
54
64
|
return emitRunUsageError(json, adapter, validated.recipeFile, depsBlock.code, depsBlock.message, depsBlock.userAction);
|
|
55
65
|
}
|
|
56
66
|
const artifactsDir = requiredOption(options, "artifactsDir", "run requires --artifacts-dir <dir>.");
|
|
57
|
-
const
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (json) {
|
|
79
|
-
console.log(
|
|
80
|
-
JSON.stringify(
|
|
81
|
-
{
|
|
82
|
-
schemaVersion: 1,
|
|
83
|
-
command: "run",
|
|
84
|
-
adapter,
|
|
85
|
-
status: result.status,
|
|
86
|
-
exitCode,
|
|
87
|
-
recovered: state.recovered,
|
|
88
|
-
mutations: state.mutations,
|
|
89
|
-
reportPath: report.path,
|
|
90
|
-
result
|
|
91
|
-
},
|
|
92
|
-
null,
|
|
93
|
-
2
|
|
94
|
-
)
|
|
67
|
+
const lock = acquireCheckoutLock(target, "run");
|
|
68
|
+
if ("message" in lock) return checkoutBusyOut(json, "run", lock.message, lock.path);
|
|
69
|
+
try {
|
|
70
|
+
const prepared = await prepareHeal(adapter, target, options, json);
|
|
71
|
+
if (typeof prepared === "number") return prepared;
|
|
72
|
+
const { state, heal } = prepared;
|
|
73
|
+
const librarySources = validated.librarySources;
|
|
74
|
+
const runtimeOptions = {
|
|
75
|
+
...runtimeOptionsFromCli(options),
|
|
76
|
+
...librarySources ? { librarySources } : {},
|
|
77
|
+
stdoutIsMachineContract: json
|
|
78
|
+
};
|
|
79
|
+
const { result, violation } = await executeWithHealBounds(
|
|
80
|
+
// validated.recipeFile, not the raw arg: the arg may be a library recipe NAME
|
|
81
|
+
// that only the resolver knows how to turn into a file.
|
|
82
|
+
() => runRecipe(adapter, validated.recipeFile, artifactsDir, target, optionString(options, "actionManifest"), runtimeOptions),
|
|
83
|
+
adapter,
|
|
84
|
+
target,
|
|
85
|
+
heal,
|
|
86
|
+
state,
|
|
87
|
+
() => recoverRunInfra(adapter, target, json)
|
|
95
88
|
);
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
console.log(
|
|
101
|
-
|
|
89
|
+
if (violation !== null) return emitHealViolation(json, "run", result, violation, state, adapter);
|
|
90
|
+
const report = writeRunReport(result);
|
|
91
|
+
const exitCode = result.status === "pass" ? EXIT.ok : EXIT.runtime;
|
|
92
|
+
if (json) {
|
|
93
|
+
console.log(
|
|
94
|
+
JSON.stringify(
|
|
95
|
+
{
|
|
96
|
+
schemaVersion: 1,
|
|
97
|
+
command: "run",
|
|
98
|
+
adapter,
|
|
99
|
+
status: result.status,
|
|
100
|
+
exitCode,
|
|
101
|
+
recovered: state.recovered,
|
|
102
|
+
mutations: state.mutations,
|
|
103
|
+
reportPath: report.path,
|
|
104
|
+
result
|
|
105
|
+
},
|
|
106
|
+
null,
|
|
107
|
+
2
|
|
108
|
+
)
|
|
109
|
+
);
|
|
110
|
+
} else {
|
|
111
|
+
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
112
|
+
console.log(`${out(result.status === "pass" ? "ok" : "err", result.status.toUpperCase())} ${out("bold", "recipe run")} ${out("dim", `[${adapter}]`)}`);
|
|
113
|
+
if (report.preview.length > 0) {
|
|
114
|
+
console.log(out("label", "summary:"));
|
|
115
|
+
for (const line of report.preview) console.log(` ${formatPreviewLine(line, out)}`);
|
|
116
|
+
}
|
|
117
|
+
console.log(`${out("label", "report:")} ${out("path", report.path)}`);
|
|
118
|
+
console.log(`${out("label", "artifacts:")} ${out("path", result.artifactManifestPath)}`);
|
|
102
119
|
}
|
|
103
|
-
|
|
104
|
-
|
|
120
|
+
return exitCode;
|
|
121
|
+
} finally {
|
|
122
|
+
lock.release();
|
|
105
123
|
}
|
|
106
|
-
return exitCode;
|
|
107
124
|
}
|
|
108
125
|
function formatPreviewLine(line, out) {
|
|
109
126
|
const match = /^(PASS|FAIL)\s+(.+)$/u.exec(line);
|
|
@@ -271,8 +288,8 @@ function emitPlanUsageError(json, adapter, recipeFile, code, message) {
|
|
|
271
288
|
function runArgLooksLikeRecipeFile(value) {
|
|
272
289
|
return path.isAbsolute(value) || value.includes("/") || value.includes(path.sep) || value.endsWith(".json") || fs.existsSync(path.resolve(value));
|
|
273
290
|
}
|
|
274
|
-
function emitRunRecipeRunning(json) {
|
|
275
|
-
const message = "a recipe is currently running \u2014 refusing to start while another recipe executes.";
|
|
291
|
+
function emitRunRecipeRunning(json, detail) {
|
|
292
|
+
const message = detail ?? "a recipe is currently running \u2014 refusing to start while another recipe executes.";
|
|
276
293
|
if (json) {
|
|
277
294
|
console.log(JSON.stringify({ schemaVersion: 1, status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message } }, null, 2));
|
|
278
295
|
} else {
|
package/dist/commands/shared.js
CHANGED
|
@@ -111,7 +111,8 @@ function spawnInherit(script, args, cwd) {
|
|
|
111
111
|
if (result.status !== null) return result.status;
|
|
112
112
|
return result.signal === "SIGINT" || result.signal === "SIGTERM" ? 0 : 1;
|
|
113
113
|
}
|
|
114
|
-
function spawnScriptStreaming(script, args, cwd,
|
|
114
|
+
function spawnScriptStreaming(script, args, cwd, options) {
|
|
115
|
+
const spawnOptions = options && ("env" in options || "timeoutMs" in options) ? options : { env: options };
|
|
115
116
|
const isNodeScript = script === process.execPath && args.length > 0;
|
|
116
117
|
const stem = isNodeScript ? path.basename(args[0]).replace(/[^A-Za-z0-9]/gu, "_").toUpperCase() : path.basename(script).replace(/[^A-Za-z0-9]/gu, "_").toUpperCase();
|
|
117
118
|
const override = process.env[`MM_HARNESS_SCRIPT_BIN_${stem}`];
|
|
@@ -126,12 +127,58 @@ function spawnScriptStreaming(script, args, cwd, env) {
|
|
|
126
127
|
}
|
|
127
128
|
const { bin: invokeBin, args: spawnArgs } = resolveLeafInvoke(bin, directArgs);
|
|
128
129
|
return new Promise((resolve) => {
|
|
130
|
+
const ownsProcessGroup = process.platform !== "win32" && Number(spawnOptions.timeoutMs) > 0;
|
|
129
131
|
const child = spawn(invokeBin, spawnArgs, {
|
|
130
132
|
cwd,
|
|
131
|
-
env: env ? { ...process.env, ...env } : process.env,
|
|
132
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
133
|
+
env: spawnOptions.env ? { ...process.env, ...spawnOptions.env } : process.env,
|
|
134
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
135
|
+
detached: ownsProcessGroup
|
|
133
136
|
});
|
|
134
137
|
let output = "";
|
|
138
|
+
let settled = false;
|
|
139
|
+
let didTimeout = false;
|
|
140
|
+
let escalation;
|
|
141
|
+
const parentSignals = ["SIGINT", "SIGTERM"];
|
|
142
|
+
const removeParentSignalHandlers = () => {
|
|
143
|
+
for (const signal of parentSignals) process.removeListener(signal, parentSignalHandlers[signal]);
|
|
144
|
+
};
|
|
145
|
+
const finish = (result) => {
|
|
146
|
+
if (settled) return;
|
|
147
|
+
settled = true;
|
|
148
|
+
if (timer) clearTimeout(timer);
|
|
149
|
+
if (escalation) clearTimeout(escalation);
|
|
150
|
+
removeParentSignalHandlers();
|
|
151
|
+
resolve(result);
|
|
152
|
+
};
|
|
153
|
+
const signalChildTree = (signal) => {
|
|
154
|
+
try {
|
|
155
|
+
if (ownsProcessGroup && child.pid) process.kill(-child.pid, signal);
|
|
156
|
+
else child.kill(signal);
|
|
157
|
+
} catch {
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
const parentSignalHandlers = Object.fromEntries(parentSignals.map((signal) => [signal, () => {
|
|
161
|
+
signalChildTree(signal);
|
|
162
|
+
removeParentSignalHandlers();
|
|
163
|
+
process.kill(process.pid, signal);
|
|
164
|
+
}]));
|
|
165
|
+
if (ownsProcessGroup) {
|
|
166
|
+
for (const signal of parentSignals) process.once(signal, parentSignalHandlers[signal]);
|
|
167
|
+
}
|
|
168
|
+
const timeoutMs = spawnOptions.timeoutMs;
|
|
169
|
+
const timer = Number.isFinite(timeoutMs) && Number(timeoutMs) > 0 ? setTimeout(() => {
|
|
170
|
+
didTimeout = true;
|
|
171
|
+
const message = `leaf timed out after ${String(timeoutMs)}ms: ${path.basename(script)}`;
|
|
172
|
+
output += `${output.endsWith("\n") || output.length === 0 ? "" : "\n"}${message}
|
|
173
|
+
`;
|
|
174
|
+
process.stderr.write(`${message}
|
|
175
|
+
`);
|
|
176
|
+
signalChildTree("SIGTERM");
|
|
177
|
+
escalation = setTimeout(() => {
|
|
178
|
+
signalChildTree("SIGKILL");
|
|
179
|
+
finish({ status: 1, output, timedOut: true, timeoutMs: Number(timeoutMs) });
|
|
180
|
+
}, 1e3);
|
|
181
|
+
}, Number(timeoutMs)) : void 0;
|
|
135
182
|
const tee = (chunk) => {
|
|
136
183
|
const text = chunk.toString("utf8");
|
|
137
184
|
output += text;
|
|
@@ -146,10 +193,20 @@ function spawnScriptStreaming(script, args, cwd, env) {
|
|
|
146
193
|
Next: reinstall mm-harness (npm i -g @deeeed/metamask-harness) \u2014 the shell leaf is missing or not executable`;
|
|
147
194
|
process.stderr.write(`${message}
|
|
148
195
|
`);
|
|
149
|
-
|
|
196
|
+
finish({ status: 1, output: message });
|
|
197
|
+
});
|
|
198
|
+
child.on("exit", () => {
|
|
199
|
+
if (!didTimeout) return;
|
|
200
|
+
signalChildTree("SIGKILL");
|
|
201
|
+
finish({ status: 1, output, timedOut: true, timeoutMs: Number(timeoutMs) });
|
|
150
202
|
});
|
|
151
203
|
child.on("close", (status) => {
|
|
152
|
-
|
|
204
|
+
if (didTimeout) signalChildTree("SIGKILL");
|
|
205
|
+
finish({
|
|
206
|
+
status: didTimeout ? 1 : status ?? 1,
|
|
207
|
+
output,
|
|
208
|
+
...didTimeout ? { timedOut: true, timeoutMs: Number(timeoutMs) } : {}
|
|
209
|
+
});
|
|
153
210
|
});
|
|
154
211
|
});
|
|
155
212
|
}
|
|
@@ -169,9 +226,33 @@ function usageOut(json, command, message, userAction) {
|
|
|
169
226
|
}
|
|
170
227
|
return EXIT.usage;
|
|
171
228
|
}
|
|
229
|
+
function checkoutBusyOut(json, command, message, lockPath) {
|
|
230
|
+
const userAction = `wait for the current owner, or inspect ${lockPath} if its process has exited`;
|
|
231
|
+
if (json) {
|
|
232
|
+
console.log(
|
|
233
|
+
JSON.stringify(
|
|
234
|
+
{
|
|
235
|
+
schemaVersion: 1,
|
|
236
|
+
command,
|
|
237
|
+
status: "fail",
|
|
238
|
+
exitCode: EXIT.bounded,
|
|
239
|
+
recoverable: false,
|
|
240
|
+
error: { code: "SANDBOX_BUSY", message, userAction }
|
|
241
|
+
},
|
|
242
|
+
null,
|
|
243
|
+
2
|
|
244
|
+
)
|
|
245
|
+
);
|
|
246
|
+
} else {
|
|
247
|
+
console.error(`\u2717 mm-harness ${command}: ${message}
|
|
248
|
+
Next: ${userAction}`);
|
|
249
|
+
}
|
|
250
|
+
return EXIT.bounded;
|
|
251
|
+
}
|
|
172
252
|
export {
|
|
173
253
|
ADAPTER_DETECT_NEXT,
|
|
174
254
|
EXIT,
|
|
255
|
+
checkoutBusyOut,
|
|
175
256
|
flag,
|
|
176
257
|
parseFlags,
|
|
177
258
|
resolveAdapter,
|
package/dist/commands/stop.js
CHANGED
|
@@ -12,12 +12,11 @@ async function handleStop(argv) {
|
|
|
12
12
|
const json = optionFlag(options, "json");
|
|
13
13
|
const { adapter, target } = resolveAdapter(options);
|
|
14
14
|
const surface = getAdapterSurface(adapter);
|
|
15
|
+
surface.resolveSlotPorts(target);
|
|
15
16
|
const explicitPort = optionString(options, "port") ?? optionString(options, "watcherPort");
|
|
16
17
|
if (explicitPort) {
|
|
17
18
|
process.env.WATCHER_PORT = explicitPort;
|
|
18
19
|
process.env.METRO_PORT = explicitPort;
|
|
19
|
-
} else {
|
|
20
|
-
surface.resolveSlotPorts(target);
|
|
21
20
|
}
|
|
22
21
|
const stop = surface.devServer.stop(target);
|
|
23
22
|
if (stop.kind === "headless") {
|
package/dist/doctor.js
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { color } from "./cli-color.js";
|
|
4
|
+
import { mobilePerpsEnvironment } from "./adapters/mobile/perps-env.js";
|
|
4
5
|
import { readRuntimeContextField, resolveRuntimeContextPath } from "./harness.js";
|
|
5
6
|
import { manifestPath, readJson, recipeHarnessRoot, recipeRuntimeDir, runnerDir } from "./paths.js";
|
|
7
|
+
function requiredDoctorCheckSummary(checks) {
|
|
8
|
+
const required = checks.filter((check) => check.required);
|
|
9
|
+
const failed = required.filter((check) => check.status === "fail").map((check) => check.id);
|
|
10
|
+
return {
|
|
11
|
+
status: failed.length === 0 ? "pass" : "fail",
|
|
12
|
+
total: required.length,
|
|
13
|
+
passed: required.length - failed.length,
|
|
14
|
+
failed
|
|
15
|
+
};
|
|
16
|
+
}
|
|
6
17
|
function repoShape(target) {
|
|
7
18
|
const exists = (rel) => fs.existsSync(path.join(target, rel));
|
|
8
19
|
const packageInfo = readPackageInfo(target);
|
|
@@ -56,31 +67,36 @@ function readPackageInfo(target) {
|
|
|
56
67
|
};
|
|
57
68
|
}
|
|
58
69
|
}
|
|
59
|
-
function fixtureSummary(target) {
|
|
70
|
+
function fixtureSummary(target, adapter) {
|
|
60
71
|
const candidates = [
|
|
61
72
|
`${recipeRuntimeDir()}/wallet-fixture.json`
|
|
62
73
|
];
|
|
63
74
|
const rel = candidates.find((candidate) => fs.existsSync(path.join(target, candidate)));
|
|
64
75
|
if (!rel) return { status: "missing", path: null };
|
|
76
|
+
return fixtureFileSummary(path.join(target, rel), adapter, rel);
|
|
77
|
+
}
|
|
78
|
+
function fixtureFileSummary(file, adapter, reportedPath = file) {
|
|
65
79
|
try {
|
|
66
|
-
const data = readJsonObject(
|
|
80
|
+
const data = readJsonObject(file);
|
|
81
|
+
const hasAccounts = Array.isArray(data.accounts) && data.accounts.length > 0;
|
|
82
|
+
const hasPassword = typeof data.password === "string" && data.password.length > 0;
|
|
67
83
|
return {
|
|
68
|
-
status:
|
|
69
|
-
path:
|
|
84
|
+
status: hasAccounts && (adapter === "core" || hasPassword) ? "ready" : "incomplete",
|
|
85
|
+
path: reportedPath,
|
|
70
86
|
accountCount: Array.isArray(data.accounts) ? data.accounts.length : 0,
|
|
71
|
-
hasPassword
|
|
87
|
+
hasPassword
|
|
72
88
|
};
|
|
73
89
|
} catch (error) {
|
|
74
|
-
return { status: "invalid", path:
|
|
90
|
+
return { status: "invalid", path: reportedPath, error: error instanceof Error ? error.message : String(error) };
|
|
75
91
|
}
|
|
76
92
|
}
|
|
77
93
|
const RUNTIME_CONTEXT_FIELDS = [
|
|
78
|
-
{ key: "slotId", envVars: ["RECIPE_SLOT_ID"], envVar: "RECIPE_SLOT_ID", customize: "
|
|
79
|
-
{ key: "extensionId", envVars: ["RECIPE_HARNESS_EXTENSION_ID"], envVar: "RECIPE_HARNESS_EXTENSION_ID", customize: "
|
|
80
|
-
{ key: "cdpPort", envVars: ["RECIPE_CDP_PORT", "CDP_PORT"], envVar: "CDP_PORT", customize: "
|
|
81
|
-
{ key: "runtimeStart.approved", envVars: ["RECIPE_RUNTIME_START_APPROVED"], envVar: "RECIPE_RUNTIME_START_APPROVED", customize: "
|
|
82
|
-
{ key: "runtimeStart.command", envVars: [], envVar: null, customize: "
|
|
83
|
-
{ key: "runtimeStart.readyUrl", envVars: ["RECIPE_RUNTIME_READY_URL"], envVar: "RECIPE_RUNTIME_READY_URL", customize: "
|
|
94
|
+
{ key: "slotId", envVars: ["RECIPE_SLOT_ID"], envVar: "RECIPE_SLOT_ID", customize: "doctor --fix creates local identity; Farmslot may override", adapters: ["mobile", "extension", "core"] },
|
|
95
|
+
{ key: "extensionId", envVars: ["RECIPE_HARNESS_EXTENSION_ID"], envVar: "RECIPE_HARNESS_EXTENSION_ID", customize: "resolved from the built extension", adapters: ["extension"] },
|
|
96
|
+
{ key: "cdpPort", envVars: ["RECIPE_CDP_PORT", "CDP_PORT"], envVar: "CDP_PORT", customize: "doctor --fix claims one; --cdp-port overrides", adapters: ["extension"] },
|
|
97
|
+
{ key: "runtimeStart.approved", envVars: ["RECIPE_RUNTIME_START_APPROVED"], envVar: "RECIPE_RUNTIME_START_APPROVED", customize: "optional orchestrator-provided legacy live context", adapters: ["mobile", "extension"] },
|
|
98
|
+
{ key: "runtimeStart.command", envVars: [], envVar: null, customize: "optional orchestrator-provided legacy live context", adapters: ["mobile", "extension"] },
|
|
99
|
+
{ key: "runtimeStart.readyUrl", envVars: ["RECIPE_RUNTIME_READY_URL"], envVar: "RECIPE_RUNTIME_READY_URL", customize: "optional orchestrator-provided legacy live context", adapters: ["mobile", "extension"] }
|
|
84
100
|
];
|
|
85
101
|
function runtimeContextSummary(target, adapter) {
|
|
86
102
|
const contextPath = resolveRuntimeContextPath(target);
|
|
@@ -104,7 +120,7 @@ function renderRuntimeContext(runtimeContext) {
|
|
|
104
120
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
105
121
|
const lines = [];
|
|
106
122
|
lines.push(
|
|
107
|
-
runtimeContext.fileExists ? `${out("label", "runtime-context:")} ${runtimeContext.file} ${out("ok", "(present)")}` : `${out("label", "runtime-context:")} ${runtimeContext.file} ${out("dim", "(absent \u2014
|
|
123
|
+
runtimeContext.fileExists ? `${out("label", "runtime-context:")} ${runtimeContext.file} ${out("ok", "(present)")}` : `${out("label", "runtime-context:")} ${runtimeContext.file} ${out("dim", "(absent \u2014 run mm-harness doctor --fix)")}`
|
|
108
124
|
);
|
|
109
125
|
for (const [key, field] of Object.entries(runtimeContext.fields)) {
|
|
110
126
|
const isSet = field.value !== void 0 && field.value !== null && field.value !== "";
|
|
@@ -118,25 +134,28 @@ function renderRuntimeContext(runtimeContext) {
|
|
|
118
134
|
function createDoctorReport(adapter, target, manifestValidation, actionManifestPath = manifestPath(adapter)) {
|
|
119
135
|
const mode = compatibilityMode(adapter, target);
|
|
120
136
|
const manifestErrors = Number(manifestValidation.summary?.errors ?? 0);
|
|
121
|
-
const status = manifestErrors > 0 ? "fail" : "pass";
|
|
122
137
|
const checks = [
|
|
123
138
|
{
|
|
124
139
|
id: "manifest",
|
|
125
140
|
status: manifestErrors === 0 ? "pass" : "fail",
|
|
141
|
+
required: true,
|
|
126
142
|
message: manifestErrors === 0 ? "Action manifest is valid Recipe v1." : `Action manifest has ${manifestErrors} validation error(s).`
|
|
127
143
|
},
|
|
128
144
|
{
|
|
129
145
|
id: "bridge",
|
|
130
146
|
status: mode === "unsupported/no bridge" ? "fail" : "pass",
|
|
147
|
+
required: false,
|
|
131
148
|
message: mode === "unsupported/no bridge" ? `No ${adapter} bridge is available for this checkout.` : `${adapter} compatibility mode: ${mode}.`
|
|
132
149
|
}
|
|
133
150
|
];
|
|
151
|
+
const requiredChecks = requiredDoctorCheckSummary(checks);
|
|
134
152
|
return {
|
|
135
153
|
schemaVersion: 1,
|
|
136
154
|
protocolVersion: "v1",
|
|
137
155
|
runner_protocol_version: 1,
|
|
138
|
-
status,
|
|
139
|
-
checks
|
|
156
|
+
status: requiredChecks.status,
|
|
157
|
+
checks,
|
|
158
|
+
requiredChecks,
|
|
140
159
|
adapter,
|
|
141
160
|
target,
|
|
142
161
|
runner: {
|
|
@@ -147,7 +166,8 @@ function createDoctorReport(adapter, target, manifestValidation, actionManifestP
|
|
|
147
166
|
},
|
|
148
167
|
compatibilityMode: mode,
|
|
149
168
|
shape: repoShape(target),
|
|
150
|
-
fixture: fixtureSummary(target),
|
|
169
|
+
fixture: fixtureSummary(target, adapter),
|
|
170
|
+
environment: adapter === "mobile" ? { mobilePerps: mobilePerpsEnvironment(target) } : {},
|
|
151
171
|
runtimeContext: runtimeContextSummary(target, adapter),
|
|
152
172
|
manifestValidation: manifestValidation.summary
|
|
153
173
|
};
|
|
@@ -162,8 +182,10 @@ function readJsonObject(file) {
|
|
|
162
182
|
export {
|
|
163
183
|
compatibilityMode,
|
|
164
184
|
createDoctorReport,
|
|
185
|
+
fixtureFileSummary,
|
|
165
186
|
fixtureSummary,
|
|
166
187
|
renderRuntimeContext,
|
|
167
188
|
repoShape,
|
|
189
|
+
requiredDoctorCheckSummary,
|
|
168
190
|
runtimeContextSummary
|
|
169
191
|
};
|
package/dist/harness.js
CHANGED
|
@@ -2,6 +2,7 @@ import { execFileSync, spawnSync } from "node:child_process";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { resolveLeafInvoke, shellLeafMissing, missingShellLeafMessage } from "./leaf-invoke.js";
|
|
5
|
+
import { resolveExtensionSlotPorts } from "./adapters/slot-ports.js";
|
|
5
6
|
import { recipeHarnessPath, recipeRuntimeDir, runnerDir } from "./paths.js";
|
|
6
7
|
import { prepareMobile } from "./adapters/mobile/prepare.js";
|
|
7
8
|
const HARNESS_ACTIONS = ["install", "verify", "cleanup", "live"];
|
|
@@ -186,11 +187,13 @@ function applyExtensionRuntimeEnv(target, action, args) {
|
|
|
186
187
|
const readyUrl = readRuntimeContextField(contextPath, "runtimeStart.readyUrl");
|
|
187
188
|
if (readyUrl) process.env.RECIPE_RUNTIME_READY_URL = readyUrl;
|
|
188
189
|
}
|
|
190
|
+
} else {
|
|
191
|
+
delete process.env.RECIPE_RUNTIME_CONTEXT;
|
|
189
192
|
}
|
|
193
|
+
resolveExtensionSlotPorts(target);
|
|
190
194
|
let result = [...args];
|
|
191
195
|
if (!hasArg(result, "--cdp-port")) {
|
|
192
|
-
const
|
|
193
|
-
const cdpPort = contextPort ?? process.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT;
|
|
196
|
+
const cdpPort = process.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT;
|
|
194
197
|
if (cdpPort) {
|
|
195
198
|
process.env.RECIPE_CDP_PORT = cdpPort;
|
|
196
199
|
process.env.CDP_PORT = cdpPort;
|
package/dist/heal-bounds.js
CHANGED
|
@@ -103,7 +103,7 @@ function checkHealBounds(target, output, state) {
|
|
|
103
103
|
return {
|
|
104
104
|
code: "SAME_RECOVERY_TWICE",
|
|
105
105
|
exitCode: EXIT.bounded,
|
|
106
|
-
message: "
|
|
106
|
+
message: state.recovered.length > 0 ? "one recovery already succeeded this invocation \u2014 bounded policy refuses a second recovery." : "one recovery was already attempted this invocation \u2014 refusing to loop.",
|
|
107
107
|
originalError
|
|
108
108
|
};
|
|
109
109
|
}
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -51,6 +51,22 @@ Example:
|
|
|
51
51
|
mm-harness status --json
|
|
52
52
|
mm-harness status --fast`
|
|
53
53
|
},
|
|
54
|
+
{
|
|
55
|
+
name: "checklist",
|
|
56
|
+
summary: "Mark task-local checklist progress through the bundled agent runtime.",
|
|
57
|
+
example: "mm-harness checklist mark <task-dir> start",
|
|
58
|
+
helpText: `mm-harness checklist mark <task-dir> <step> [options]
|
|
59
|
+
|
|
60
|
+
Mark checklist progress through mm-harness's bundled @farmslot/agent-runtime.
|
|
61
|
+
The task directory must contain CHECKLIST.md and checklist-target.json.
|
|
62
|
+
|
|
63
|
+
Steps: start | 1 | 2 | ... | complete | no-change | blocked
|
|
64
|
+
|
|
65
|
+
Example:
|
|
66
|
+
mm-harness checklist mark temp/tasks/recipe-cook/<task> start
|
|
67
|
+
mm-harness checklist mark temp/tasks/recipe-cook/<task> 1
|
|
68
|
+
mm-harness checklist mark temp/tasks/recipe-cook/<task> complete --mark-last`
|
|
69
|
+
},
|
|
54
70
|
{
|
|
55
71
|
name: "actions",
|
|
56
72
|
summary: "List the action vocabulary + field schemas (--raw dumps the raw action registry JSON).",
|
|
@@ -72,18 +88,18 @@ Example:
|
|
|
72
88
|
},
|
|
73
89
|
{
|
|
74
90
|
name: "stop",
|
|
75
|
-
summary: "Stop
|
|
91
|
+
summary: "Stop this checkout runtime (mobile Metro / Extension watcher + owned browser) and close its log windows.",
|
|
76
92
|
example: "mm-harness stop",
|
|
77
93
|
helpText: `mm-harness stop [flags]
|
|
78
94
|
|
|
79
|
-
Stop the
|
|
95
|
+
Stop the runtime this checkout owns and close its tmux log-tail windows,
|
|
80
96
|
scoped to this checkout so concurrent slots are untouched. Idempotent \u2014
|
|
81
97
|
nothing running is success, not an error. Behavior is per platform:
|
|
82
98
|
mobile stop the port-scoped Metro dev server
|
|
83
|
-
extension stop
|
|
99
|
+
extension stop webpack watcher, owned Chrome/CDP profile processes, viewers, and stale markers
|
|
84
100
|
core headless \u2014 no dev server to stop (teaching error)
|
|
85
101
|
|
|
86
|
-
--port <port>
|
|
102
|
+
--port <port> Metro/webpack port override; Extension CDP remains checkout-context scoped
|
|
87
103
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
88
104
|
--target <path> Checkout path (default: cwd)
|
|
89
105
|
--json Machine-readable output
|
|
@@ -101,7 +117,8 @@ Example:
|
|
|
101
117
|
Run one action in isolation as a one-node recipe through the real engine path.
|
|
102
118
|
Fuzzy short-name: 'ensure_unlocked' resolves to 'metamask.wallet.ensure_unlocked'
|
|
103
119
|
if unique; ambiguous = exit 2. Pass action fields as key=value shorthand or
|
|
104
|
-
with --arg k=v
|
|
120
|
+
with --arg k=v; numbers, booleans, arrays, and objects are parsed as JSON-like values.
|
|
121
|
+
Actions differ per adapter \u2014 list this checkout's with:
|
|
105
122
|
mm-harness actions.
|
|
106
123
|
|
|
107
124
|
--list List everything invocable for the adapter (actions + flows); no <action> needed
|
|
@@ -171,19 +188,20 @@ Example:
|
|
|
171
188
|
sections so there is no hunting for files.
|
|
172
189
|
|
|
173
190
|
--fix Repair the overlay/runtime-context WITHOUT launching (no fixture reseed); --json adds fixed[]/failed[]
|
|
174
|
-
--expect-live Exit
|
|
191
|
+
--expect-live Exit-coded liveness gate only (human verdict on stderr). Use without --print-ready when callers need pass/fail without a Farmslot indicator line
|
|
192
|
+
--print-ready Farmslot health_check mode: implies --expect-live and prints health.ready_indicator on stdout (extension/mobile: OK; core: ready)
|
|
175
193
|
--cdp-port <port> Extension CDP port for the liveness probe (env: CDP_PORT / RECIPE_CDP_PORT)
|
|
176
194
|
--device <udid|serial|name> Mobile only: target this device (env: IOS_SIMULATOR / ADB_SERIAL). doctor reports the connected devices; it never gates on ambiguity.
|
|
177
195
|
--all-devices Mobile only: show every connected device instead of the slot-scoped target
|
|
178
196
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
179
197
|
--target <path> Checkout path (default: cwd)
|
|
180
198
|
--runtime-dir <dir> Runtime dir containing agentic-runtime.json (relative to target)
|
|
181
|
-
--json Machine-readable output
|
|
199
|
+
--json Machine-readable output (incompatible with --print-ready)
|
|
182
200
|
|
|
183
201
|
Example:
|
|
184
202
|
mm-harness doctor
|
|
185
203
|
mm-harness doctor --fix --json
|
|
186
|
-
mm-harness doctor --adapter extension --target /path/to/checkout --cdp-port 6662 --
|
|
204
|
+
mm-harness doctor --adapter extension --target /path/to/checkout --cdp-port 6662 --print-ready
|
|
187
205
|
mm-harness doctor --adapter mobile --target /path/to/checkout`
|
|
188
206
|
},
|
|
189
207
|
{
|
|
@@ -200,11 +218,12 @@ Example:
|
|
|
200
218
|
--adapter <mobile|extension|core> Adapter label (auto-detected when possible)
|
|
201
219
|
--base <ref> Diff base (default: PR base, then remote HEAD, then repo fallback)
|
|
202
220
|
--profile <fast|full> Validation depth (default: fast)
|
|
221
|
+
--fix Fix changed files, then validate them
|
|
203
222
|
--artifacts-dir <dir> Write validation-summary.json/.md and per-check logs
|
|
204
223
|
--json Machine-readable envelope
|
|
205
224
|
|
|
206
225
|
Example:
|
|
207
|
-
mm-harness check diff --profile fast --artifacts-dir artifacts/validation
|
|
226
|
+
mm-harness check diff --fix --profile fast --artifacts-dir artifacts/validation
|
|
208
227
|
mm-harness check diff --profile full --json`
|
|
209
228
|
},
|
|
210
229
|
{
|
|
@@ -305,6 +324,8 @@ Example:
|
|
|
305
324
|
helpText: `mm-harness verify [flags]
|
|
306
325
|
|
|
307
326
|
Check the runtime overlay is present and healthy (no app launch).
|
|
327
|
+
Extension live checks infer the checkout CDP port from runtime context,
|
|
328
|
+
pool, or slot suffix when --cdp-port is omitted.
|
|
308
329
|
|
|
309
330
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
310
331
|
--target <path> Checkout path (default: cwd)
|
|
@@ -394,6 +415,7 @@ Example:
|
|
|
394
415
|
|
|
395
416
|
--worker Extension service-worker DevTools
|
|
396
417
|
--dev-menu Mobile RN developer menu
|
|
418
|
+
--no-open Resolve target/method/endpoint without opening a UI
|
|
397
419
|
--adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
|
|
398
420
|
--target <path> Checkout path (default: cwd)
|
|
399
421
|
--json Machine-readable output
|
|
@@ -424,11 +446,13 @@ Example:
|
|
|
424
446
|
},
|
|
425
447
|
{
|
|
426
448
|
name: "fixtures",
|
|
427
|
-
summary: "Manage the canonical wallet fixture (wallet DATA only) \u2014 sync / set / generate / finalize.",
|
|
449
|
+
summary: "Manage the canonical wallet fixture (wallet DATA only) \u2014 init / sync / set / generate / finalize.",
|
|
428
450
|
example: "mm-harness fixtures set",
|
|
429
|
-
helpText: `mm-harness fixtures <sync|set|generate|finalize> [flags]
|
|
451
|
+
helpText: `mm-harness fixtures <init|sync|set|generate|finalize> [flags]
|
|
430
452
|
|
|
431
453
|
Manage the ONE canonical wallet fixture per checkout \u2014 wallet DATA only.
|
|
454
|
+
init Create it from --from <path>, or explicitly choose --dev for a
|
|
455
|
+
disposable public test wallet that must never hold real funds.
|
|
432
456
|
sync Refresh the wallet fixture files on the target.
|
|
433
457
|
set Apply the canonical fixture (SRP/password/accounts); the password is
|
|
434
458
|
read FROM the fixture, never typed.
|
|
@@ -440,6 +464,9 @@ Example:
|
|
|
440
464
|
Want different accounts? Edit the fixture file directly:
|
|
441
465
|
<checkout>/temp/recipe/runtime/wallet-fixture.json
|
|
442
466
|
|
|
467
|
+
--from <path> Existing wallet fixture source (init)
|
|
468
|
+
--dev Create a disposable public test wallet (init)
|
|
469
|
+
--force Replace an existing canonical fixture (init)
|
|
443
470
|
--fixture <path> Wallet fixture path (generate/finalize input; agent override for set/sync \u2014 env: RECIPE_WALLET_FIXTURE)
|
|
444
471
|
--out <path> generate: output fixture-state.json; finalize: optional validation report
|
|
445
472
|
--state <path> finalize: the pre-launch fixture-state.json to seed
|
|
@@ -452,6 +479,8 @@ Example:
|
|
|
452
479
|
--json Machine-readable output
|
|
453
480
|
|
|
454
481
|
Example:
|
|
482
|
+
mm-harness fixtures init --from /secure/path/wallet-fixture.json
|
|
483
|
+
mm-harness fixtures init --dev
|
|
455
484
|
mm-harness fixtures sync
|
|
456
485
|
mm-harness fixtures set
|
|
457
486
|
mm-harness fixtures generate --fixture wallet-fixture.json --out fixture-state.json
|
|
@@ -487,7 +516,7 @@ const HELP_GROUPS = [
|
|
|
487
516
|
{
|
|
488
517
|
title: "PROVE",
|
|
489
518
|
blurb: "run recipes and inspect readiness",
|
|
490
|
-
commands: ["run", "doctor", "check", "recipe-quality"]
|
|
519
|
+
commands: ["run", "doctor", "check", "checklist", "recipe-quality"]
|
|
491
520
|
},
|
|
492
521
|
{
|
|
493
522
|
title: "RUNTIME OVERLAY",
|