@deeeed/metamask-harness 0.16.0 → 0.17.1
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 +44 -0
- package/README.md +90 -137
- package/adapters/core/cleanup.sh +0 -0
- package/adapters/core/inject.sh +0 -0
- package/adapters/extension/cleanup.mjs +0 -0
- package/adapters/extension/console-tail.mjs +51 -24
- package/adapters/extension/ensure-browser.sh +0 -0
- package/adapters/extension/inject.mjs +5 -9
- package/adapters/extension/launch-browser.cjs +0 -0
- package/adapters/extension/launch-webpack.cjs +83 -0
- package/adapters/extension/launch.sh +8 -11
- package/adapters/extension/live.sh +2 -0
- package/adapters/extension/readiness.mjs +0 -0
- package/adapters/extension/reattach.sh +1 -0
- package/adapters/extension/refresh-build.sh +0 -0
- package/adapters/extension/seed-fixture.sh +4 -12
- package/adapters/extension/sidepanel-toggle.sh +4 -1
- package/adapters/extension/snapshot-dist.sh +0 -0
- package/adapters/extension/stamp-runtime-title.cjs +49 -0
- package/adapters/extension/start-watch.sh +16 -4
- package/adapters/extension/stop-viewers.sh +1 -1
- package/adapters/extension/sync-webpack-dist.cjs +105 -0
- package/adapters/extension/verify.sh +1 -1
- package/adapters/extension/wallet-fixture-state.cjs +0 -0
- package/adapters/manifest.json +46 -38
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -0
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
- package/adapters/mobile/bridge-runtime/setup-wallet.sh +0 -0
- package/adapters/mobile/cleanup.sh +16 -176
- package/adapters/mobile/inject.sh +88 -671
- package/adapters/mobile/launch-console-forwarder.cjs +39 -0
- package/adapters/mobile/lib/metro-listener.sh +0 -0
- package/adapters/mobile/lib/tmux-viewer.sh +0 -0
- package/adapters/mobile/open-device.sh +0 -0
- package/adapters/mobile/prewarm-bundle.sh +0 -0
- package/adapters/mobile/start-console-forwarder.sh +70 -0
- package/adapters/mobile/start-metro.sh +0 -41
- package/adapters/mobile/stop-metro.sh +0 -0
- package/adapters/mobile/verify.sh +4 -61
- package/adapters/mobile/wait-for-bridge.sh +0 -0
- package/adapters/mobile/yarn-setup.sh +0 -0
- package/adapters/shared/activate-repo-node.sh +0 -0
- package/adapters/shared/activate-repo-ruby.sh +0 -0
- package/adapters/shared/cli-ux.sh +0 -0
- package/adapters/shared/ensure-runner-deps.sh +0 -0
- package/adapters/shared/harness-path.sh +0 -0
- package/adapters/shared/hash-helpers.sh +0 -0
- package/adapters/shared/install-repo-deps.sh +0 -0
- package/adapters/shared/json-field.sh +0 -0
- package/adapters/shared/open-debug.mjs +90 -36
- package/adapters/shared/open-log-window.sh +0 -0
- package/adapters/shared/reap-checkout-metros.sh +0 -0
- package/adapters/shared/resolve-slot-ports-core.mjs +0 -6
- package/adapters/shared/resolve-slot-ports.mjs +0 -0
- package/adapters/shared/resolve-slot-ports.sh +0 -5
- package/adapters/shared/sync-wallet-fixture.sh +0 -0
- package/adapters/shared/tmux-session.sh +0 -0
- package/adapters/shared/tmux-viewer.sh +0 -0
- package/dist/adapters/extension/console-capture.js +84 -0
- package/dist/adapters/extension/runtime-decision.js +14 -2
- package/dist/adapters/extension/surface.js +3 -2
- package/dist/adapters/mobile/prepare.js +22 -7
- package/dist/adapters/mobile/surface.js +1 -1
- package/dist/adapters/resolve-slot-ports.js +0 -2
- package/dist/adapters/slot-ports.js +8 -0
- package/dist/cli.js +5 -11
- package/dist/command-contract.js +441 -0
- package/dist/command-journal.js +225 -0
- package/dist/commands/call.js +43 -20
- package/dist/commands/check.js +9 -3
- package/dist/commands/debug.js +38 -31
- package/dist/commands/device-target.js +27 -12
- package/dist/commands/doctor.js +19 -6
- package/dist/commands/fixtures.js +19 -11
- package/dist/commands/last.js +52 -0
- package/dist/commands/launch/index.js +166 -59
- package/dist/commands/logs.js +8 -6
- package/dist/commands/manifest.js +114 -29
- package/dist/commands/parse-args.js +1 -0
- package/dist/commands/provision.js +10 -3
- package/dist/commands/run-engine.js +27 -42
- package/dist/commands/run-report.js +1 -1
- package/dist/commands/run.js +243 -41
- package/dist/commands/shared.js +11 -1
- package/dist/commands/status-probe.js +1 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/stop.js +7 -2
- package/dist/doctor.js +1 -4
- package/dist/harness.js +19 -125
- package/dist/json-stream.js +57 -0
- package/dist/live-adapter-contract.js +6 -20
- package/dist/mm-harness-cli.js +111 -41
- package/dist/paths.js +2 -5
- package/dist/run-diagnostics.js +35 -40
- package/dist/runner.js +32 -1
- package/dist/runtime-context.js +26 -8
- package/docs/CHEATSHEET.md +39 -52
- package/docs/CLI-ERGONOMICS-AUDIT.md +32 -0
- package/docs/CLI-ERGONOMICS-HUMAN-QA.md +48 -0
- package/docs/CODE-MAP.md +1 -1
- package/docs/UX-PRINCIPLES.md +2 -0
- package/docs/VIDEO-DEMO-VALIDATION.md +62 -0
- package/docs/architecture.md +65 -375
- package/docs/live-adapter-contract.md +3 -1
- package/library/actions/core/perps/_controller.mjs +10 -55
- package/library/manifests/mobile.action-manifest.json +1 -1
- package/package.json +3 -2
- package/scripts/completions.sh +0 -0
- package/scripts/install-completions.sh +0 -0
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
- package/adapters/mobile/overlay/compat/README.md +0 -28
- package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
- package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
- package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
- package/adapters/shared/resolve-farmslot-ports.sh +0 -7
- package/docs/CLI-SPEC.md +0 -1102
- package/docs/MENTAL-MODEL.md +0 -295
package/dist/harness.js
CHANGED
|
@@ -4,13 +4,8 @@ import path from "node:path";
|
|
|
4
4
|
import { resolveLeafInvoke, shellLeafMissing, missingShellLeafMessage } from "./leaf-invoke.js";
|
|
5
5
|
import { resolveExtensionSlotPorts } from "./adapters/slot-ports.js";
|
|
6
6
|
import { recipeHarnessPath, recipeRuntimeDir, runnerDir } from "./paths.js";
|
|
7
|
-
|
|
8
|
-
const HARNESS_ACTIONS = ["install", "verify", "cleanup", "live"];
|
|
7
|
+
const HARNESS_ACTIONS = ["install", "verify", "cleanup"];
|
|
9
8
|
const ADAPTERS = ["mobile", "extension", "core"];
|
|
10
|
-
function isValidAdapterAction(adapter, action) {
|
|
11
|
-
if (adapter === "core") return action === "install" || action === "verify" || action === "cleanup";
|
|
12
|
-
return true;
|
|
13
|
-
}
|
|
14
9
|
function harnessUsage() {
|
|
15
10
|
console.error(`mm-harness \u2014 install and validate the MetaMask recipe runtime in a checkout.
|
|
16
11
|
|
|
@@ -50,6 +45,12 @@ function failureHint(adapter, action) {
|
|
|
50
45
|
const helper = adapter === "mobile" ? "mm-harness launch ios # or launch android" : "mm-harness launch";
|
|
51
46
|
return `Read the error above for the specific cause. To (re)start the runtime, run: ${helper}`;
|
|
52
47
|
}
|
|
48
|
+
function successNext(adapter, action, target) {
|
|
49
|
+
const base = `--adapter ${adapter} --target ${shellQuote(target)}`;
|
|
50
|
+
if (action === "install") return `mm-harness verify ${base}`;
|
|
51
|
+
if (action === "cleanup") return `mm-harness install ${base}`;
|
|
52
|
+
return `mm-harness status --target ${shellQuote(target)} --json`;
|
|
53
|
+
}
|
|
53
54
|
function hasArg(args, needle) {
|
|
54
55
|
return args.some((arg) => arg === needle || arg.startsWith(`${needle}=`));
|
|
55
56
|
}
|
|
@@ -161,7 +162,7 @@ function readRuntimeContextField(contextPath, field) {
|
|
|
161
162
|
if (node === void 0 || node === null || node === "") return void 0;
|
|
162
163
|
return String(node);
|
|
163
164
|
}
|
|
164
|
-
function applyExtensionRuntimeEnv(target,
|
|
165
|
+
function applyExtensionRuntimeEnv(target, args) {
|
|
165
166
|
const contextPath = resolveRuntimeContextPath(target);
|
|
166
167
|
const contextExists = fs.existsSync(contextPath);
|
|
167
168
|
if (contextExists) {
|
|
@@ -174,19 +175,6 @@ function applyExtensionRuntimeEnv(target, action, args) {
|
|
|
174
175
|
const extensionId = readRuntimeContextField(contextPath, "extensionId");
|
|
175
176
|
if (extensionId) process.env.RECIPE_HARNESS_EXTENSION_ID = extensionId;
|
|
176
177
|
}
|
|
177
|
-
if (process.env.RECIPE_RUNTIME_START_APPROVED === void 0) {
|
|
178
|
-
const approved = readRuntimeContextField(contextPath, "runtimeStart.approved");
|
|
179
|
-
if (approved === "true" || approved === "True" || approved === "1") process.env.RECIPE_RUNTIME_START_APPROVED = "1";
|
|
180
|
-
else if (approved === "false" || approved === "False" || approved === "0") process.env.RECIPE_RUNTIME_START_APPROVED = "0";
|
|
181
|
-
}
|
|
182
|
-
if (!process.env.RECIPE_RUNTIME_START_CMD) {
|
|
183
|
-
const command = readRuntimeContextField(contextPath, "runtimeStart.command");
|
|
184
|
-
if (command) process.env.RECIPE_RUNTIME_START_CMD = command;
|
|
185
|
-
}
|
|
186
|
-
if (!process.env.RECIPE_RUNTIME_READY_URL) {
|
|
187
|
-
const readyUrl = readRuntimeContextField(contextPath, "runtimeStart.readyUrl");
|
|
188
|
-
if (readyUrl) process.env.RECIPE_RUNTIME_READY_URL = readyUrl;
|
|
189
|
-
}
|
|
190
178
|
} else {
|
|
191
179
|
delete process.env.RECIPE_RUNTIME_CONTEXT;
|
|
192
180
|
}
|
|
@@ -200,11 +188,6 @@ function applyExtensionRuntimeEnv(target, action, args) {
|
|
|
200
188
|
result = [...result, "--cdp-port", cdpPort];
|
|
201
189
|
}
|
|
202
190
|
}
|
|
203
|
-
if (action === "live" && !hasArg(result, "--prepare-cmd")) {
|
|
204
|
-
if (process.env.RECIPE_RUNTIME_START_APPROVED === "1" && process.env.RECIPE_RUNTIME_START_CMD) {
|
|
205
|
-
result = [...result, "--prepare-cmd", process.env.RECIPE_RUNTIME_START_CMD];
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
191
|
return result;
|
|
209
192
|
}
|
|
210
193
|
function resolveEntry(base, candidates, mode) {
|
|
@@ -281,81 +264,6 @@ Next: mm-harness install --platform core --target ${target}`
|
|
|
281
264
|
);
|
|
282
265
|
return { command, prefixArgs: [] };
|
|
283
266
|
}
|
|
284
|
-
async function handleMobileLive(target, forwardArgs, json, autoDetected) {
|
|
285
|
-
const platform = argValue(forwardArgs, "--platform") ?? "ios";
|
|
286
|
-
const watcherPortStr = argValue(forwardArgs, "--watcher-port") ?? process.env.WATCHER_PORT;
|
|
287
|
-
const watcherPort = watcherPortStr ? parseInt(watcherPortStr, 10) : void 0;
|
|
288
|
-
const start = Date.now();
|
|
289
|
-
if (!json) {
|
|
290
|
-
const detected = autoDetected ? ", auto-detected" : "";
|
|
291
|
-
console.error(`\u2192 live (mobile${detected}) \u2014 target: ${target}`);
|
|
292
|
-
}
|
|
293
|
-
const prepResult = await prepareMobile(target, { platform, json, watcherPort });
|
|
294
|
-
if (prepResult.status !== 0) {
|
|
295
|
-
const elapsed2 = ((Date.now() - start) / 1e3).toFixed(1);
|
|
296
|
-
if (json) {
|
|
297
|
-
console.log(
|
|
298
|
-
harnessSummary("live", "mobile", target, "fail", prepResult.status, autoDetected, {
|
|
299
|
-
code: "MOBILE_PREPARE_FAILED",
|
|
300
|
-
message: `mobile prepare failed (exit ${prepResult.status})`,
|
|
301
|
-
userAction: failureHint("mobile", "live")
|
|
302
|
-
})
|
|
303
|
-
);
|
|
304
|
-
} else {
|
|
305
|
-
console.error(
|
|
306
|
-
`\u2717 live mobile failed (exit ${prepResult.status}, ${elapsed2}s)
|
|
307
|
-
${failureHint("mobile", "live")}`
|
|
308
|
-
);
|
|
309
|
-
}
|
|
310
|
-
return prepResult.status;
|
|
311
|
-
}
|
|
312
|
-
const installedVerify = path.join(recipeHarnessPath(target, "mobile"), "scripts", "verify.sh");
|
|
313
|
-
const verifySh = fs.existsSync(installedVerify) ? installedVerify : resolveEntry(runnerDir, ["adapters/mobile/verify.sh", "scripts/mobile/verify.sh"], "file");
|
|
314
|
-
const verifyArgs = hasArg(forwardArgs, "--no-auto-start") ? [...forwardArgs] : ["--no-auto-start", ...forwardArgs];
|
|
315
|
-
if (shellLeafMissing(verifySh)) {
|
|
316
|
-
const message = missingShellLeafMessage(verifySh);
|
|
317
|
-
if (json) {
|
|
318
|
-
console.log(
|
|
319
|
-
harnessSummary("live", "mobile", target, "fail", 1, autoDetected, {
|
|
320
|
-
code: "HARNESS_SPAWN_FAILED",
|
|
321
|
-
message,
|
|
322
|
-
userAction: failureHint("mobile", "live")
|
|
323
|
-
})
|
|
324
|
-
);
|
|
325
|
-
} else {
|
|
326
|
-
console.error(`\u2717 ${message}`);
|
|
327
|
-
}
|
|
328
|
-
return 1;
|
|
329
|
-
}
|
|
330
|
-
const { bin: verifyBin, args: verifySpawnArgs } = resolveLeafInvoke(verifySh, verifyArgs);
|
|
331
|
-
const result = spawnSync(verifyBin, verifySpawnArgs, {
|
|
332
|
-
stdio: json ? ["inherit", 2, "inherit"] : "inherit",
|
|
333
|
-
env: process.env
|
|
334
|
-
});
|
|
335
|
-
const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
|
|
336
|
-
const exitCode = result.status ?? 1;
|
|
337
|
-
if (json) {
|
|
338
|
-
console.log(
|
|
339
|
-
harnessSummary(
|
|
340
|
-
"live",
|
|
341
|
-
"mobile",
|
|
342
|
-
target,
|
|
343
|
-
exitCode === 0 ? "pass" : "fail",
|
|
344
|
-
exitCode,
|
|
345
|
-
autoDetected,
|
|
346
|
-
exitCode === 0 ? void 0 : { code: "MOBILE_VERIFY_FAILED", message: `mobile live verify failed (exit ${exitCode})`, userAction: failureHint("mobile", "live") }
|
|
347
|
-
)
|
|
348
|
-
);
|
|
349
|
-
} else if (exitCode === 0) {
|
|
350
|
-
console.error(`\u2713 live mobile passed (${elapsed}s)`);
|
|
351
|
-
} else {
|
|
352
|
-
console.error(
|
|
353
|
-
`\u2717 live mobile failed (exit ${exitCode}, ${elapsed}s)
|
|
354
|
-
${failureHint("mobile", "live")}`
|
|
355
|
-
);
|
|
356
|
-
}
|
|
357
|
-
return exitCode;
|
|
358
|
-
}
|
|
359
267
|
async function handleHarness(argv) {
|
|
360
268
|
const action = argv[0];
|
|
361
269
|
if (!action || action === "-h" || action === "--help") {
|
|
@@ -387,32 +295,13 @@ async function handleHarness(argv) {
|
|
|
387
295
|
}
|
|
388
296
|
return 2;
|
|
389
297
|
}
|
|
390
|
-
if (!isValidAdapterAction(adapter, harnessAction)) {
|
|
391
|
-
if (json) {
|
|
392
|
-
console.log(
|
|
393
|
-
harnessSummary(harnessAction, adapter, target, "fail", 2, parsedAdapter === void 0, {
|
|
394
|
-
code: "UNSUPPORTED_ACTION",
|
|
395
|
-
message: `${harnessAction} is not supported for ${adapter} (core supports install, verify, cleanup).`,
|
|
396
|
-
userAction: "run mm-harness doctor to see which actions are supported for this adapter"
|
|
397
|
-
})
|
|
398
|
-
);
|
|
399
|
-
} else {
|
|
400
|
-
console.error(
|
|
401
|
-
`\u2717 ${harnessAction} is not supported for ${adapter} (core supports install, verify, cleanup).`
|
|
402
|
-
);
|
|
403
|
-
}
|
|
404
|
-
return 2;
|
|
405
|
-
}
|
|
406
298
|
const autoDetected = parsedAdapter === void 0;
|
|
407
299
|
let forwardArgs = hasArg(forward, "--target") ? [...forward] : ["--target", target, ...forward];
|
|
408
300
|
if (harnessAction === "install" && hasArg(forward, "--runway")) {
|
|
409
301
|
return handleRunwayInstall(adapter, target, forward, json);
|
|
410
302
|
}
|
|
411
|
-
if (adapter === "extension" &&
|
|
412
|
-
forwardArgs = applyExtensionRuntimeEnv(target,
|
|
413
|
-
}
|
|
414
|
-
if (adapter === "mobile" && harnessAction === "live") {
|
|
415
|
-
return handleMobileLive(target, forwardArgs, json, autoDetected);
|
|
303
|
+
if (adapter === "extension" && harnessAction === "verify") {
|
|
304
|
+
forwardArgs = applyExtensionRuntimeEnv(target, forwardArgs);
|
|
416
305
|
}
|
|
417
306
|
const dispatch = resolveHarnessDispatch(adapter, harnessAction, target);
|
|
418
307
|
if ("error" in dispatch) {
|
|
@@ -492,7 +381,8 @@ async function handleHarness(argv) {
|
|
|
492
381
|
)
|
|
493
382
|
);
|
|
494
383
|
} else if (exitCode === 0) {
|
|
495
|
-
console.error(`\u2713 ${harnessAction} ${adapter} passed (${seconds}s)
|
|
384
|
+
console.error(`\u2713 ${harnessAction} ${adapter} passed (${seconds}s)
|
|
385
|
+
Next: ${successNext(adapter, harnessAction, target)}`);
|
|
496
386
|
} else {
|
|
497
387
|
console.error(`\u2717 ${harnessAction} ${adapter} failed (exit ${exitCode}, ${seconds}s)
|
|
498
388
|
${failureHint(adapter, harnessAction)}`);
|
|
@@ -522,8 +412,10 @@ async function handleRunwayInstall(adapter, target, forward, json) {
|
|
|
522
412
|
resolveOnly: hasArg(forward, "--resolve-only"),
|
|
523
413
|
rerunCommand
|
|
524
414
|
});
|
|
415
|
+
const next = result.status === "pass" && adapter === "mobile" && result.resolveOnly !== true ? `mm-harness launch ${String(result.platform ?? "ios")} --adapter mobile --target ${shellQuote(target)}` : void 0;
|
|
416
|
+
const output = next ? { ...result, next } : result;
|
|
525
417
|
if (json) {
|
|
526
|
-
console.log(JSON.stringify(
|
|
418
|
+
console.log(JSON.stringify(output, null, 2));
|
|
527
419
|
} else if (result.status === "pass") {
|
|
528
420
|
const cache = typeof result.cache === "object" && result.cache ? result.cache : void 0;
|
|
529
421
|
const simulator = typeof result.simulator === "object" && result.simulator ? result.simulator : void 0;
|
|
@@ -534,6 +426,7 @@ async function handleRunwayInstall(adapter, target, forward, json) {
|
|
|
534
426
|
const action = result.skipped ? "already provisioned" : "installed Runway app";
|
|
535
427
|
console.error(`\u2713 ${action} for ${adapter} ${result.platform ?? ""} simulator=${simulator?.name ?? "unknown"} cache=${cache?.status ?? "skip"}`);
|
|
536
428
|
}
|
|
429
|
+
if (next) console.error(` Next: ${next}`);
|
|
537
430
|
} else {
|
|
538
431
|
console.error(`\u2717 mm-harness install --runway: ${result.error?.message ?? "runway install failed"}
|
|
539
432
|
Next: ${result.error?.userAction ?? rerunCommand}`);
|
|
@@ -576,9 +469,10 @@ function harnessSummary(action, adapter, target, status, exitCode, autoDetected,
|
|
|
576
469
|
status,
|
|
577
470
|
exitCode,
|
|
578
471
|
// Error contract: every --json failure carries a stable machine code + human
|
|
579
|
-
// message
|
|
472
|
+
// message. userAction is included when present so callers
|
|
580
473
|
// can surface the reachable escape without parsing the human message.
|
|
581
|
-
...status === "fail" && error ? { error } : {}
|
|
474
|
+
...status === "fail" && error ? { error } : {},
|
|
475
|
+
...status === "pass" && adapter ? { next: successNext(adapter, action, target) } : {}
|
|
582
476
|
});
|
|
583
477
|
}
|
|
584
478
|
export {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
class JsonStreamWriter {
|
|
2
|
+
enabled;
|
|
3
|
+
command;
|
|
4
|
+
completed = false;
|
|
5
|
+
writeLine;
|
|
6
|
+
constructor(command, enabled, output = process.stdout) {
|
|
7
|
+
this.command = command;
|
|
8
|
+
this.enabled = enabled;
|
|
9
|
+
const write = output.write.bind(output);
|
|
10
|
+
this.writeLine = (line) => {
|
|
11
|
+
write(`${line}
|
|
12
|
+
`);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
isolateStdout() {
|
|
16
|
+
if (!this.enabled) return () => void 0;
|
|
17
|
+
const original = process.stdout.write;
|
|
18
|
+
const redirect = ((chunk, encoding, callback) => process.stderr.write(chunk, encoding, callback));
|
|
19
|
+
process.stdout.write = redirect;
|
|
20
|
+
return () => {
|
|
21
|
+
if (process.stdout.write === redirect) process.stdout.write = original;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
emit(event, fields = {}) {
|
|
25
|
+
if (!this.enabled || this.completed) return;
|
|
26
|
+
this.writeLine(JSON.stringify({
|
|
27
|
+
schemaVersion: 1,
|
|
28
|
+
command: this.command,
|
|
29
|
+
event,
|
|
30
|
+
...fields,
|
|
31
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
phase(phase, fields = {}) {
|
|
35
|
+
this.emit("phase", { phase, ...fields });
|
|
36
|
+
}
|
|
37
|
+
node(nodeId, action, status) {
|
|
38
|
+
this.emit("node", { nodeId, action, status });
|
|
39
|
+
}
|
|
40
|
+
mutation(mutation) {
|
|
41
|
+
this.emit("mutation", { mutation });
|
|
42
|
+
}
|
|
43
|
+
recovery(code) {
|
|
44
|
+
this.emit("recovery", { code });
|
|
45
|
+
}
|
|
46
|
+
error(error) {
|
|
47
|
+
this.emit("error", { error });
|
|
48
|
+
}
|
|
49
|
+
complete(status, exitCode, fields = {}) {
|
|
50
|
+
if (!this.enabled || this.completed) return;
|
|
51
|
+
this.emit("complete", { status, exitCode, ...fields });
|
|
52
|
+
this.completed = true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
JsonStreamWriter
|
|
57
|
+
};
|
|
@@ -46,29 +46,16 @@ function candidatePaths(platform, action) {
|
|
|
46
46
|
}
|
|
47
47
|
pushDomainDispatcherFiles(files, root, platform, family);
|
|
48
48
|
}
|
|
49
|
-
for (const candidateStem of stems) {
|
|
50
|
-
pushFlatCandidateFiles(files, root, platform, candidateStem);
|
|
51
|
-
}
|
|
52
49
|
}
|
|
53
50
|
return files;
|
|
54
51
|
}
|
|
55
52
|
function pushCandidateFiles(files, root, platform, family, stem) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
files.push(path.join(root, "shared", family, `${stem}.${extension}`));
|
|
59
|
-
}
|
|
53
|
+
files.push(path.join(root, platform, family, `${stem}.mjs`));
|
|
54
|
+
files.push(path.join(root, "shared", family, `${stem}.mjs`));
|
|
60
55
|
}
|
|
61
56
|
function pushDomainDispatcherFiles(files, root, platform, family) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
files.push(path.join(root, "shared", family, `${family}.${extension}`));
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
function pushFlatCandidateFiles(files, root, platform, stem) {
|
|
68
|
-
for (const extension of ["mjs", "js", "sh"]) {
|
|
69
|
-
files.push(path.join(root, platform, `${stem}.${extension}`));
|
|
70
|
-
files.push(path.join(root, "shared", `${stem}.${extension}`));
|
|
71
|
-
}
|
|
57
|
+
files.push(path.join(root, platform, family, `${family}.mjs`));
|
|
58
|
+
files.push(path.join(root, "shared", family, `${family}.mjs`));
|
|
72
59
|
}
|
|
73
60
|
async function firstExecutablePath(paths) {
|
|
74
61
|
for (const file of paths) {
|
|
@@ -120,9 +107,8 @@ function runProcess(command, args, options) {
|
|
|
120
107
|
});
|
|
121
108
|
}
|
|
122
109
|
function commandFor(file, projectRoot, platform) {
|
|
123
|
-
if (file.endsWith(".sh")) return { command: "bash", args: [file] };
|
|
124
110
|
const needsTsx = platform === "core" || importsSourceTypescript(file);
|
|
125
|
-
if (!needsTsx
|
|
111
|
+
if (!needsTsx) {
|
|
126
112
|
return { command: process.execPath, args: [file] };
|
|
127
113
|
}
|
|
128
114
|
const tsxBin = resolveTsxBin(projectRoot);
|
|
@@ -165,7 +151,7 @@ function importsSourceTypescriptFrom(file, visited) {
|
|
|
165
151
|
const specifier = match[1] ?? "";
|
|
166
152
|
if (specifier.endsWith(".ts")) return true;
|
|
167
153
|
if (!specifier.startsWith(".")) continue;
|
|
168
|
-
if (!specifier.endsWith(".mjs")
|
|
154
|
+
if (!specifier.endsWith(".mjs")) continue;
|
|
169
155
|
if (importsSourceTypescriptFrom(path.resolve(path.dirname(absolute), specifier), visited)) {
|
|
170
156
|
return true;
|
|
171
157
|
}
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -4,10 +4,17 @@ import path from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
import { color } from "./cli-color.js";
|
|
7
|
+
import { withCommandJournal } from "./command-journal.js";
|
|
8
|
+
import { JsonStreamWriter } from "./json-stream.js";
|
|
7
9
|
import { handleUpdate, maybeNudge } from "./commands/update.js";
|
|
8
10
|
import { handleCallHelp } from "./commands/call.js";
|
|
9
11
|
import { getAdapterSurface } from "./adapters/surface.js";
|
|
10
12
|
import { detectAdapter } from "./harness.js";
|
|
13
|
+
import {
|
|
14
|
+
PUBLIC_COMMAND_CONTRACTS,
|
|
15
|
+
publicCommandNames,
|
|
16
|
+
validatePublicInvocation
|
|
17
|
+
} from "./command-contract.js";
|
|
11
18
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
12
19
|
globalThis.__MM_HARNESS_WRAPPER__ = true;
|
|
13
20
|
const { main: recipeMain } = await import("./cli.js");
|
|
@@ -71,10 +78,11 @@ Example:
|
|
|
71
78
|
name: "actions",
|
|
72
79
|
summary: "List the action vocabulary + field schemas (--raw dumps the raw action registry JSON).",
|
|
73
80
|
example: "mm-harness actions --adapter mobile",
|
|
74
|
-
helpText: `mm-harness actions [flags]
|
|
81
|
+
helpText: `mm-harness actions [query] [flags]
|
|
75
82
|
|
|
76
83
|
List the action vocabulary + field schemas for the checkout adapter.
|
|
77
84
|
|
|
85
|
+
query Search names, categories, fields, and descriptions (typo-tolerant)
|
|
78
86
|
--action <name> Describe one action; fuzzy-resolves like call (short or full name)
|
|
79
87
|
--categories List compact action categories and counts
|
|
80
88
|
--category <name> List only one category (for example ui, wallet, or perps)
|
|
@@ -85,6 +93,7 @@ Example:
|
|
|
85
93
|
|
|
86
94
|
Example:
|
|
87
95
|
mm-harness actions --adapter mobile
|
|
96
|
+
mm-harness actions positions --adapter mobile
|
|
88
97
|
mm-harness actions --adapter mobile --categories --json
|
|
89
98
|
mm-harness actions --adapter mobile --category ui --json
|
|
90
99
|
mm-harness actions --adapter mobile --action assert_orders
|
|
@@ -176,12 +185,29 @@ Example:
|
|
|
176
185
|
--library <name=path> Add/override a recipe-library source (repeatable)
|
|
177
186
|
--heal <off|infra-only|auto> Healing policy (default: infra-only); auto-ensures the overlay
|
|
178
187
|
--json Machine-readable output
|
|
188
|
+
--json-stream Line-flushed JSONL progress + terminal event
|
|
179
189
|
--record-video=full-run Record a video of the run
|
|
180
190
|
|
|
181
191
|
Example:
|
|
182
192
|
mm-harness run recipe.json --plan --adapter mobile
|
|
183
193
|
mm-harness run recipe.json --adapter extension --artifacts-dir ./out`
|
|
184
194
|
},
|
|
195
|
+
{
|
|
196
|
+
name: "last",
|
|
197
|
+
summary: "Show the last significant command, verdict, timestamps, and evidence paths for this checkout.",
|
|
198
|
+
example: "mm-harness last --json",
|
|
199
|
+
helpText: `mm-harness last [flags]
|
|
200
|
+
|
|
201
|
+
Read the atomic per-checkout resumability journal. Discovery commands do not
|
|
202
|
+
replace it, and an interrupted process remains recorded as verdict=running.
|
|
203
|
+
|
|
204
|
+
--target <path> Checkout path (default: cwd)
|
|
205
|
+
--runtime-dir <dir> Runtime dir containing last-command.json
|
|
206
|
+
--json Machine-readable envelope
|
|
207
|
+
|
|
208
|
+
Example:
|
|
209
|
+
mm-harness last --json`
|
|
210
|
+
},
|
|
185
211
|
{
|
|
186
212
|
name: "doctor",
|
|
187
213
|
summary: "Readiness check for a checkout \u2014 no app launch. --fix repairs the overlay/runtime without launching.",
|
|
@@ -379,6 +405,7 @@ Example:
|
|
|
379
405
|
--adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
|
|
380
406
|
--target <path> Checkout path (default: cwd)
|
|
381
407
|
--json Machine-readable summary (recovered[] / mutations[] / phase)
|
|
408
|
+
--json-stream Line-flushed JSONL progress + terminal event
|
|
382
409
|
|
|
383
410
|
Example:
|
|
384
411
|
mm-harness launch ios
|
|
@@ -397,8 +424,8 @@ Example:
|
|
|
397
424
|
--full Raw log tail (default = compact) (env: RECIPE_LOG_UI)
|
|
398
425
|
--window (Re)open the read-only tmux tail window for the resolved dev-server port (leaves the process untouched)
|
|
399
426
|
--events <n> Compact event count (default 10) (env: RECIPE_LOG_EVENTS)
|
|
400
|
-
--source <label> Log source per adapter \u2014 mobile: metro|app
|
|
401
|
-
extension: webpack|watcher|rebuild
|
|
427
|
+
--source <label> Log source per adapter \u2014 mobile: metro|app;
|
|
428
|
+
extension: extension|dapp|webpack|watcher|rebuild.
|
|
402
429
|
Core is headless (teaching error).
|
|
403
430
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
404
431
|
--target <path> Checkout path (default: cwd)
|
|
@@ -477,6 +504,7 @@ Example:
|
|
|
477
504
|
--cdp-port <port> finalize: CDP port of the running extension
|
|
478
505
|
--extension-dir <path> finalize: loaded extension dist (e.g. dist/chrome)
|
|
479
506
|
--extension-id-file <path> finalize: optional file to read/write the resolved extension id
|
|
507
|
+
--action-manifest <path> Extension set: override the wallet action manifest
|
|
480
508
|
--adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
|
|
481
509
|
--target <path> Checkout path (default: cwd)
|
|
482
510
|
--device <udid|serial|name> Mobile only: target this device for sync/set
|
|
@@ -491,21 +519,28 @@ Example:
|
|
|
491
519
|
mm-harness fixtures finalize --fixture wallet-fixture.json --state fixture-state.json --cdp-port 6661 --extension-dir dist/chrome`
|
|
492
520
|
}
|
|
493
521
|
];
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
Replacement: mm-harness actions --raw (works now \u2014 dumps the raw action registry JSON,
|
|
506
|
-
identical to the old \`manifest --json\`). Manifest validation moved into doctor / run --plan.`
|
|
522
|
+
const PUBLIC_COMMAND_EXAMPLES = Object.fromEntries(
|
|
523
|
+
REAL.map((command) => [command.name, command.example])
|
|
524
|
+
);
|
|
525
|
+
function assertPublicContractMatchesSurface() {
|
|
526
|
+
const registered = REAL.map((command) => command.name).sort();
|
|
527
|
+
const contracted = publicCommandNames().sort();
|
|
528
|
+
if (registered.join("\n") !== contracted.join("\n")) {
|
|
529
|
+
throw new Error(
|
|
530
|
+
`public command contract drift: registered=[${registered.join(", ")}], contracted=[${contracted.join(", ")}]`
|
|
531
|
+
);
|
|
507
532
|
}
|
|
508
|
-
|
|
533
|
+
for (const command of REAL) {
|
|
534
|
+
const registeredAliases = [...command.aliases ?? []].sort();
|
|
535
|
+
const contractedAliases = [...PUBLIC_COMMAND_CONTRACTS[command.name]?.aliases ?? []].sort();
|
|
536
|
+
if (registeredAliases.join("\n") !== contractedAliases.join("\n")) {
|
|
537
|
+
throw new Error(
|
|
538
|
+
`public command alias contract drift for ${command.name}: registered=[${registeredAliases.join(", ")}], contracted=[${contractedAliases.join(", ")}]`
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
assertPublicContractMatchesSurface();
|
|
509
544
|
const HELP_GROUPS = [
|
|
510
545
|
{
|
|
511
546
|
title: "DAILY LOOP",
|
|
@@ -520,7 +555,7 @@ const HELP_GROUPS = [
|
|
|
520
555
|
{
|
|
521
556
|
title: "PROVE",
|
|
522
557
|
blurb: "run recipes and inspect readiness",
|
|
523
|
-
commands: ["run", "doctor", "check", "checklist", "recipe-quality"]
|
|
558
|
+
commands: ["run", "last", "doctor", "check", "checklist", "recipe-quality"]
|
|
524
559
|
},
|
|
525
560
|
{
|
|
526
561
|
title: "RUNTIME OVERLAY",
|
|
@@ -608,7 +643,7 @@ function groupedHelp() {
|
|
|
608
643
|
lines.push("DEV/PROD \u2014 the global npm install is prod; set MM_HARNESS_BIN to a dev checkout to override:");
|
|
609
644
|
lines.push(" MM_HARNESS_BIN=/path/to/checkout/bin/mm-harness # unset = the installed/global bin");
|
|
610
645
|
lines.push("");
|
|
611
|
-
lines.push("See
|
|
646
|
+
lines.push("See README.md for the workflow and docs/CHEATSHEET.md for examples.");
|
|
612
647
|
return `${lines.join("\n")}
|
|
613
648
|
`;
|
|
614
649
|
}
|
|
@@ -620,11 +655,6 @@ async function delegate(argv) {
|
|
|
620
655
|
return error !== null && typeof error === "object" && "exitCode" in error && typeof error.exitCode === "number" ? error.exitCode : 1;
|
|
621
656
|
}
|
|
622
657
|
}
|
|
623
|
-
function translateActionsRaw(argv) {
|
|
624
|
-
const rest = argv.slice(1).filter((arg) => arg !== "--raw");
|
|
625
|
-
const withJson = rest.includes("--json") ? rest : [...rest, "--json"];
|
|
626
|
-
return ["manifest", ...withJson];
|
|
627
|
-
}
|
|
628
658
|
const pkgVersion = (() => {
|
|
629
659
|
try {
|
|
630
660
|
return JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")).version ?? "unknown";
|
|
@@ -642,8 +672,7 @@ for (const command of REAL) {
|
|
|
642
672
|
if (command.name === "update") {
|
|
643
673
|
process.exit(await handleUpdate(rawArgv.slice(1)));
|
|
644
674
|
}
|
|
645
|
-
|
|
646
|
-
process.exit(await delegate(argv));
|
|
675
|
+
process.exit(await withCommandJournal(command.name, rawArgv, () => delegate(rawArgv)));
|
|
647
676
|
});
|
|
648
677
|
}
|
|
649
678
|
const HIDDEN = [
|
|
@@ -665,21 +694,6 @@ program.command("completions").description("Install/print bundled shell tab-comp
|
|
|
665
694
|
const result = spawnSync("bash", [script, ...rawArgv.slice(1)], { stdio: "inherit" });
|
|
666
695
|
process.exit(result.status ?? 1);
|
|
667
696
|
});
|
|
668
|
-
for (const retired of RETIRED) {
|
|
669
|
-
program.command(retired.name).description(`${retired.name} is retired (see teaching error)`).allowUnknownOption().helpOption(false).argument("[args...]").action(async () => {
|
|
670
|
-
if (retired.name === "live") {
|
|
671
|
-
const adapterIdx = rawArgv.indexOf("--adapter");
|
|
672
|
-
const aliasIdx = rawArgv.indexOf("-a");
|
|
673
|
-
const adapterVal = adapterIdx !== -1 ? rawArgv[adapterIdx + 1] : aliasIdx !== -1 ? rawArgv[aliasIdx + 1] : void 0;
|
|
674
|
-
if (adapterVal === "mobile" || adapterVal === "android") {
|
|
675
|
-
process.exit(await delegate(rawArgv));
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
console.error(retired.message);
|
|
680
|
-
process.exit(2);
|
|
681
|
-
});
|
|
682
|
-
}
|
|
683
697
|
const NUDGE_SKIP = ["update", "completions", "completion-candidates"];
|
|
684
698
|
if (rawArgv.length > 0 && !NUDGE_SKIP.includes(rawArgv[0])) {
|
|
685
699
|
setImmediate(() => void maybeNudge());
|
|
@@ -696,10 +710,66 @@ function isCallActionHelp(argv) {
|
|
|
696
710
|
const scope = divider === -1 ? argv : argv.slice(0, divider);
|
|
697
711
|
return scope.includes("--help") || scope.includes("-h");
|
|
698
712
|
}
|
|
713
|
+
function jsonRequestedBeforePassthrough(argv) {
|
|
714
|
+
const divider = argv.indexOf("--");
|
|
715
|
+
const scope = divider === -1 ? argv : argv.slice(0, divider);
|
|
716
|
+
return scope.some((argument) => argument === "--json" || argument.startsWith("--json="));
|
|
717
|
+
}
|
|
718
|
+
function jsonStreamRequestedBeforePassthrough(argv) {
|
|
719
|
+
const divider = argv.indexOf("--");
|
|
720
|
+
const scope = divider === -1 ? argv : argv.slice(0, divider);
|
|
721
|
+
return scope.includes("--json-stream");
|
|
722
|
+
}
|
|
723
|
+
function emitUsageError(error, json, jsonStream) {
|
|
724
|
+
if (jsonStream) {
|
|
725
|
+
const stream = new JsonStreamWriter(error.command, true);
|
|
726
|
+
stream.error({ code: error.code, message: error.message, userAction: error.userAction });
|
|
727
|
+
stream.complete("fail", 2);
|
|
728
|
+
} else if (json) {
|
|
729
|
+
process.stdout.write(
|
|
730
|
+
`${JSON.stringify(
|
|
731
|
+
{
|
|
732
|
+
schemaVersion: 1,
|
|
733
|
+
command: error.command,
|
|
734
|
+
status: "fail",
|
|
735
|
+
error: {
|
|
736
|
+
code: error.code,
|
|
737
|
+
message: error.message,
|
|
738
|
+
userAction: error.userAction
|
|
739
|
+
},
|
|
740
|
+
exitCode: 2
|
|
741
|
+
},
|
|
742
|
+
null,
|
|
743
|
+
2
|
|
744
|
+
)}
|
|
745
|
+
`
|
|
746
|
+
);
|
|
747
|
+
} else {
|
|
748
|
+
const scope = error.command === "mm-harness" ? "" : ` ${error.command}`;
|
|
749
|
+
process.stderr.write(`\u2717 mm-harness${scope}: ${error.message}
|
|
750
|
+
Next: ${error.userAction}
|
|
751
|
+
`);
|
|
752
|
+
}
|
|
753
|
+
process.exit(2);
|
|
754
|
+
}
|
|
699
755
|
if (rawArgv.length === 0) {
|
|
700
756
|
process.stdout.write(groupedHelp());
|
|
701
757
|
process.exit(0);
|
|
702
758
|
}
|
|
759
|
+
const preflightBypass = /* @__PURE__ */ new Set([
|
|
760
|
+
...HIDDEN,
|
|
761
|
+
"completions"
|
|
762
|
+
]);
|
|
763
|
+
if (!preflightBypass.has(rawArgv[0] ?? "")) {
|
|
764
|
+
const usageError = validatePublicInvocation(rawArgv, PUBLIC_COMMAND_EXAMPLES);
|
|
765
|
+
if (usageError) {
|
|
766
|
+
emitUsageError(
|
|
767
|
+
usageError,
|
|
768
|
+
jsonRequestedBeforePassthrough(rawArgv),
|
|
769
|
+
jsonStreamRequestedBeforePassthrough(rawArgv)
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
703
773
|
if (hasPassthroughHelp(rawArgv)) {
|
|
704
774
|
process.exit(await delegate(rawArgv));
|
|
705
775
|
}
|
package/dist/paths.js
CHANGED
|
@@ -47,10 +47,7 @@ function recipeWatchLogCandidates() {
|
|
|
47
47
|
}
|
|
48
48
|
function resolveLocalProtocolRoot() {
|
|
49
49
|
const candidates = [
|
|
50
|
-
// Orchestrator-neutral name is primary; FARMSLOT_ROOT stays a back-compat alias
|
|
51
|
-
// for one release (the installer injects METAMASK_RUNNER_PROTOCOL_ROOT).
|
|
52
50
|
process.env.METAMASK_RUNNER_PROTOCOL_ROOT,
|
|
53
|
-
process.env.FARMSLOT_ROOT,
|
|
54
51
|
readConfiguredProtocolRoot(),
|
|
55
52
|
findProtocolRoot(runnerDir),
|
|
56
53
|
findProtocolRoot(process.cwd())
|
|
@@ -62,7 +59,7 @@ function resolveRequiredLocalProtocolRoot(reason) {
|
|
|
62
59
|
const root = resolveLocalProtocolRoot();
|
|
63
60
|
if (!root) {
|
|
64
61
|
throw new Error(
|
|
65
|
-
`${reason} requires a local protocol/runtime checkout. Set METAMASK_RUNNER_PROTOCOL_ROOT
|
|
62
|
+
`${reason} requires a local protocol/runtime checkout. Set METAMASK_RUNNER_PROTOCOL_ROOT or create .farmslot-root for this dev-only path.`
|
|
66
63
|
);
|
|
67
64
|
}
|
|
68
65
|
return root;
|
|
@@ -152,7 +149,7 @@ async function importProtocolPackage(packageName, localSourceEntry) {
|
|
|
152
149
|
const root = resolveLocalProtocolRoot();
|
|
153
150
|
if (!root) {
|
|
154
151
|
throw new Error(
|
|
155
|
-
`${packageName} is not installed. Install @farmslot/* packages normally, or set
|
|
152
|
+
`${packageName} is not installed. Install @farmslot/* packages normally, or set METAMASK_RUNNER_PROTOCOL_ROOT/use npm run dev:link-farmslot while co-developing protocol packages locally.`
|
|
156
153
|
);
|
|
157
154
|
}
|
|
158
155
|
return import(pathToFileURL(path.join(root, localSourceEntry)).href);
|