@deeeed/metamask-harness 0.17.3 → 0.17.5
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 +6 -96
- package/README.md +136 -96
- package/adapters/extension/console-tail.mjs +24 -51
- package/adapters/extension/ensure-browser.sh +7 -1
- package/adapters/extension/inject.mjs +15 -5
- package/adapters/extension/launch-browser.cjs +4 -3
- package/adapters/extension/launch.sh +11 -8
- package/adapters/extension/lib/chrome-args.cjs +24 -0
- package/adapters/extension/live.sh +1 -2
- package/adapters/extension/reattach.sh +0 -1
- package/adapters/extension/seed-fixture.sh +12 -4
- package/adapters/extension/sidepanel-toggle.sh +1 -4
- package/adapters/extension/start-watch.sh +4 -17
- package/adapters/extension/stop-viewers.sh +1 -1
- package/adapters/extension/verify.sh +1 -1
- package/adapters/manifest.json +45 -53
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -16
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
- package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +1 -3
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +3 -2
- package/adapters/mobile/cleanup.sh +176 -16
- package/adapters/mobile/inject.sh +671 -90
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
- package/adapters/mobile/overlay/compat/README.md +28 -0
- package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +42 -0
- package/adapters/mobile/start-metro.sh +41 -0
- package/adapters/mobile/verify.sh +61 -4
- package/adapters/mobile/wait-for-bridge.sh +8 -22
- package/adapters/shared/harness-source-fingerprint.mjs +49 -0
- package/adapters/shared/install-repo-deps.sh +1 -5
- package/adapters/shared/open-debug.mjs +184 -103
- package/adapters/shared/resolve-farmslot-ports-core.mjs +3 -0
- package/adapters/shared/resolve-farmslot-ports.mjs +5 -0
- package/adapters/shared/resolve-farmslot-ports.sh +7 -0
- package/adapters/shared/resolve-slot-ports-core.mjs +6 -0
- package/adapters/shared/resolve-slot-ports.sh +7 -2
- package/bin/mm-harness +0 -7
- package/dist/adapters/extension/harness-freshness.js +39 -0
- package/dist/adapters/extension/runtime-decision.js +4 -73
- package/dist/adapters/extension/surface.js +2 -3
- package/dist/adapters/mobile/perps-env.js +3 -43
- package/dist/adapters/mobile/prepare.js +7 -22
- package/dist/adapters/mobile/source-freshness.js +26 -41
- package/dist/adapters/mobile/surface.js +1 -1
- package/dist/adapters/resolve-slot-ports.js +2 -0
- package/dist/adapters/slot-ports.js +0 -16
- package/dist/adapters.js +5 -12
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +12 -6
- package/dist/commands/call.js +22 -56
- package/dist/commands/check.js +3 -9
- package/dist/commands/checklist.js +4 -1
- package/dist/commands/debug.js +31 -38
- package/dist/commands/device-target.js +12 -27
- package/dist/commands/doctor.js +23 -77
- package/dist/commands/fixtures.js +17 -27
- package/dist/commands/launch/extension.js +2 -0
- package/dist/commands/launch/index.js +59 -167
- package/dist/commands/logs.js +6 -8
- package/dist/commands/manifest.js +25 -151
- package/dist/commands/parse-args.js +0 -2
- package/dist/commands/provision.js +3 -10
- package/dist/commands/run-engine.js +69 -71
- package/dist/commands/run-report.js +3 -12
- package/dist/commands/run.js +35 -245
- package/dist/commands/shared.js +3 -55
- package/dist/commands/status-probe.js +1 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/stop.js +2 -7
- package/dist/commands/update.js +28 -16
- package/dist/doctor.js +4 -1
- package/dist/harness.js +134 -43
- package/dist/heal-bounds.js +1 -1
- package/dist/live-adapter-contract.js +20 -6
- package/dist/mm-harness-cli.js +48 -123
- package/dist/paths.js +5 -2
- package/dist/runner.js +2 -37
- package/dist/runtime-context.js +8 -26
- package/docs/ADAPTER-SURFACE.md +112 -19
- package/docs/CHEATSHEET.md +52 -39
- package/docs/CLI-SPEC.md +1098 -0
- package/docs/CODE-MAP.md +1 -1
- package/docs/DEBUG-HANDOVER.md +36 -0
- package/docs/MENTAL-MODEL.md +295 -0
- package/docs/UX-PRINCIPLES.md +1 -3
- package/docs/architecture.md +375 -65
- package/docs/live-adapter-contract.md +2 -4
- package/docs/recipe-libraries.md +275 -45
- package/library/README.md +0 -4
- package/library/actions/core/perps/_controller.mjs +55 -10
- package/library/actions/mobile/platform/bridge.mjs +1 -5
- package/library/manifests/core.action-manifest.json +7 -7
- package/library/manifests/mobile.action-manifest.json +3 -28
- package/package.json +1 -4
- package/adapters/extension/launch-webpack.cjs +0 -83
- package/adapters/extension/stamp-runtime-title.cjs +0 -49
- package/adapters/extension/sync-webpack-dist.cjs +0 -105
- package/adapters/mobile/launch-console-forwarder.cjs +0 -39
- package/adapters/mobile/start-console-forwarder.sh +0 -70
- package/adapters/shared/update-check-worker.mjs +0 -35
- package/dist/adapters/extension/console-capture.js +0 -93
- package/dist/command-contract.js +0 -441
- package/dist/command-journal.js +0 -225
- package/dist/commands/last.js +0 -52
- package/dist/json-stream.js +0 -57
- package/dist/run-diagnostics.js +0 -261
- package/docs/CLI-ERGONOMICS-AUDIT.md +0 -32
- package/docs/CLI-ERGONOMICS-HUMAN-QA.md +0 -64
- package/docs/VIDEO-DEMO-VALIDATION.md +0 -74
- package/library/recipes/runner/smoke.core.recipe.json +0 -27
- package/library/recipes/wallet/smoke.extension.recipe.json +0 -42
- package/library/recipes/wallet/smoke.mobile.recipe.json +0 -42
- package/scripts/validate-human-outcomes.mjs +0 -169
package/dist/mm-harness-cli.js
CHANGED
|
@@ -4,17 +4,10 @@ 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";
|
|
9
7
|
import { handleUpdate, maybeNudge } from "./commands/update.js";
|
|
10
8
|
import { handleCallHelp } from "./commands/call.js";
|
|
11
9
|
import { getAdapterSurface } from "./adapters/surface.js";
|
|
12
10
|
import { detectAdapter } from "./harness.js";
|
|
13
|
-
import {
|
|
14
|
-
PUBLIC_COMMAND_CONTRACTS,
|
|
15
|
-
publicCommandNames,
|
|
16
|
-
validatePublicInvocation
|
|
17
|
-
} from "./command-contract.js";
|
|
18
11
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
19
12
|
globalThis.__MM_HARNESS_WRAPPER__ = true;
|
|
20
13
|
const { main: recipeMain } = await import("./cli.js");
|
|
@@ -76,16 +69,13 @@ Example:
|
|
|
76
69
|
},
|
|
77
70
|
{
|
|
78
71
|
name: "actions",
|
|
79
|
-
summary: "
|
|
72
|
+
summary: "List the action vocabulary + field schemas (--raw dumps the raw action registry JSON).",
|
|
80
73
|
example: "mm-harness actions --adapter mobile",
|
|
81
|
-
helpText: `mm-harness actions [
|
|
74
|
+
helpText: `mm-harness actions [flags]
|
|
82
75
|
|
|
83
|
-
|
|
76
|
+
List the action vocabulary + field schemas for the checkout adapter.
|
|
84
77
|
|
|
85
|
-
query Search names, categories, fields, and descriptions (typo-tolerant)
|
|
86
78
|
--action <name> Describe one action; fuzzy-resolves like call (short or full name)
|
|
87
|
-
--categories List compact action categories and counts
|
|
88
|
-
--category <name> List only one category (for example ui, wallet, or perps)
|
|
89
79
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
90
80
|
--target <path> Checkout path (default: cwd)
|
|
91
81
|
--raw Dump raw action registry JSON
|
|
@@ -93,9 +83,6 @@ Example:
|
|
|
93
83
|
|
|
94
84
|
Example:
|
|
95
85
|
mm-harness actions --adapter mobile
|
|
96
|
-
mm-harness actions positions --adapter mobile
|
|
97
|
-
mm-harness actions --adapter mobile --categories --json
|
|
98
|
-
mm-harness actions --adapter mobile --category ui --json
|
|
99
86
|
mm-harness actions --adapter mobile --action assert_orders
|
|
100
87
|
mm-harness actions --adapter extension --raw`
|
|
101
88
|
},
|
|
@@ -152,11 +139,11 @@ Example (real actions; run mm-harness actions for this checkout's full set):
|
|
|
152
139
|
},
|
|
153
140
|
{
|
|
154
141
|
name: "flows",
|
|
155
|
-
summary: "Browse reusable
|
|
142
|
+
summary: "Browse the reusable flow library (bare = list; `flows promote` publishes a flow up a tier). Resolves across libraries \u2014 personal > team > canonical, highest tier wins.",
|
|
156
143
|
example: "mm-harness flows",
|
|
157
144
|
helpText: `mm-harness flows [flags]
|
|
158
145
|
|
|
159
|
-
Browse reusable
|
|
146
|
+
Browse the reusable flow library. Flows resolve across libraries by precedence
|
|
160
147
|
(personal > team > canonical); the highest-tier copy wins and shadows lower ones.
|
|
161
148
|
Flow resolution is adapter-global, so there is no --adapter flag here.
|
|
162
149
|
|
|
@@ -169,12 +156,11 @@ Example:
|
|
|
169
156
|
},
|
|
170
157
|
{
|
|
171
158
|
name: "run",
|
|
172
|
-
summary: "
|
|
159
|
+
summary: "Validate + run a recipe and write evidence (summary/trace/artifacts). --plan validates + prints the plan, touching nothing.",
|
|
173
160
|
example: "mm-harness run recipe.json",
|
|
174
161
|
helpText: `mm-harness run <recipe.json> [flags]
|
|
175
162
|
|
|
176
|
-
Validate
|
|
177
|
-
and artifacts. A recipe may use actions directly or call reusable flows.
|
|
163
|
+
Validate + run a recipe and write evidence (summary / trace / artifacts).
|
|
178
164
|
|
|
179
165
|
--list List everything invocable for the adapter (actions + flows); no <recipe> needed
|
|
180
166
|
--plan Validate + print execution plan, touching nothing. Exit 5 if invalid.
|
|
@@ -186,29 +172,12 @@ Example:
|
|
|
186
172
|
--library <name=path> Add/override a recipe-library source (repeatable)
|
|
187
173
|
--heal <off|infra-only|auto> Healing policy (default: infra-only); auto-ensures the overlay
|
|
188
174
|
--json Machine-readable output
|
|
189
|
-
--json-stream Line-flushed JSONL progress + terminal event
|
|
190
175
|
--record-video=full-run Record a video of the run
|
|
191
176
|
|
|
192
177
|
Example:
|
|
193
178
|
mm-harness run recipe.json --plan --adapter mobile
|
|
194
179
|
mm-harness run recipe.json --adapter extension --artifacts-dir ./out`
|
|
195
180
|
},
|
|
196
|
-
{
|
|
197
|
-
name: "last",
|
|
198
|
-
summary: "Show the last significant command, verdict, timestamps, and evidence paths for this checkout.",
|
|
199
|
-
example: "mm-harness last --json",
|
|
200
|
-
helpText: `mm-harness last [flags]
|
|
201
|
-
|
|
202
|
-
Read the atomic per-checkout resumability journal. Discovery commands do not
|
|
203
|
-
replace it, and an interrupted process remains recorded as verdict=running.
|
|
204
|
-
|
|
205
|
-
--target <path> Checkout path (default: cwd)
|
|
206
|
-
--runtime-dir <dir> Runtime dir containing last-command.json
|
|
207
|
-
--json Machine-readable envelope
|
|
208
|
-
|
|
209
|
-
Example:
|
|
210
|
-
mm-harness last --json`
|
|
211
|
-
},
|
|
212
181
|
{
|
|
213
182
|
name: "doctor",
|
|
214
183
|
summary: "Readiness check for a checkout \u2014 no app launch. --fix repairs the overlay/runtime without launching.",
|
|
@@ -406,7 +375,6 @@ Example:
|
|
|
406
375
|
--adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
|
|
407
376
|
--target <path> Checkout path (default: cwd)
|
|
408
377
|
--json Machine-readable summary (recovered[] / mutations[] / phase)
|
|
409
|
-
--json-stream Line-flushed JSONL progress + terminal event
|
|
410
378
|
|
|
411
379
|
Example:
|
|
412
380
|
mm-harness launch ios
|
|
@@ -425,8 +393,8 @@ Example:
|
|
|
425
393
|
--full Raw log tail (default = compact) (env: RECIPE_LOG_UI)
|
|
426
394
|
--window (Re)open the read-only tmux tail window for the resolved dev-server port (leaves the process untouched)
|
|
427
395
|
--events <n> Compact event count (default 10) (env: RECIPE_LOG_EVENTS)
|
|
428
|
-
--source <label> Log source per adapter \u2014 mobile: metro|app;
|
|
429
|
-
extension:
|
|
396
|
+
--source <label> Log source per adapter \u2014 mobile: metro|app (default metro);
|
|
397
|
+
extension: webpack|watcher|rebuild|app (default webpack).
|
|
430
398
|
Core is headless (teaching error).
|
|
431
399
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
432
400
|
--target <path> Checkout path (default: cwd)
|
|
@@ -505,7 +473,6 @@ Example:
|
|
|
505
473
|
--cdp-port <port> finalize: CDP port of the running extension
|
|
506
474
|
--extension-dir <path> finalize: loaded extension dist (e.g. dist/chrome)
|
|
507
475
|
--extension-id-file <path> finalize: optional file to read/write the resolved extension id
|
|
508
|
-
--action-manifest <path> Extension set: override the wallet action manifest
|
|
509
476
|
--adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
|
|
510
477
|
--target <path> Checkout path (default: cwd)
|
|
511
478
|
--device <udid|serial|name> Mobile only: target this device for sync/set
|
|
@@ -520,28 +487,21 @@ Example:
|
|
|
520
487
|
mm-harness fixtures finalize --fixture wallet-fixture.json --state fixture-state.json --cdp-port 6661 --extension-dir dist/chrome`
|
|
521
488
|
}
|
|
522
489
|
];
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
)
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
const contractedAliases = [...PUBLIC_COMMAND_CONTRACTS[command.name]?.aliases ?? []].sort();
|
|
537
|
-
if (registeredAliases.join("\n") !== contractedAliases.join("\n")) {
|
|
538
|
-
throw new Error(
|
|
539
|
-
`public command alias contract drift for ${command.name}: registered=[${registeredAliases.join(", ")}], contracted=[${contractedAliases.join(", ")}]`
|
|
540
|
-
);
|
|
541
|
-
}
|
|
490
|
+
const RETIRED = [
|
|
491
|
+
{
|
|
492
|
+
name: "live",
|
|
493
|
+
message: `mm-harness live is retired (exit 2).
|
|
494
|
+
|
|
495
|
+
Replacement: mm-harness launch --verify (install overlay \u2192 launch \u2192 CDP/bridge poll \u2192 smoke verify).`
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
name: "manifest",
|
|
499
|
+
message: `mm-harness manifest is retired (exit 2).
|
|
500
|
+
|
|
501
|
+
Replacement: mm-harness actions --raw (works now \u2014 dumps the raw action registry JSON,
|
|
502
|
+
identical to the old \`manifest --json\`). Manifest validation moved into doctor / run --plan.`
|
|
542
503
|
}
|
|
543
|
-
|
|
544
|
-
assertPublicContractMatchesSurface();
|
|
504
|
+
];
|
|
545
505
|
const HELP_GROUPS = [
|
|
546
506
|
{
|
|
547
507
|
title: "DAILY LOOP",
|
|
@@ -556,7 +516,7 @@ const HELP_GROUPS = [
|
|
|
556
516
|
{
|
|
557
517
|
title: "PROVE",
|
|
558
518
|
blurb: "run recipes and inspect readiness",
|
|
559
|
-
commands: ["run", "
|
|
519
|
+
commands: ["run", "doctor", "check", "checklist", "recipe-quality"]
|
|
560
520
|
},
|
|
561
521
|
{
|
|
562
522
|
title: "RUNTIME OVERLAY",
|
|
@@ -644,7 +604,7 @@ function groupedHelp() {
|
|
|
644
604
|
lines.push("DEV/PROD \u2014 the global npm install is prod; set MM_HARNESS_BIN to a dev checkout to override:");
|
|
645
605
|
lines.push(" MM_HARNESS_BIN=/path/to/checkout/bin/mm-harness # unset = the installed/global bin");
|
|
646
606
|
lines.push("");
|
|
647
|
-
lines.push("See
|
|
607
|
+
lines.push("See docs/MENTAL-MODEL.md (overview) and docs/CLI-SPEC.md (full contract).");
|
|
648
608
|
return `${lines.join("\n")}
|
|
649
609
|
`;
|
|
650
610
|
}
|
|
@@ -656,6 +616,11 @@ async function delegate(argv) {
|
|
|
656
616
|
return error !== null && typeof error === "object" && "exitCode" in error && typeof error.exitCode === "number" ? error.exitCode : 1;
|
|
657
617
|
}
|
|
658
618
|
}
|
|
619
|
+
function translateActionsRaw(argv) {
|
|
620
|
+
const rest = argv.slice(1).filter((arg) => arg !== "--raw");
|
|
621
|
+
const withJson = rest.includes("--json") ? rest : [...rest, "--json"];
|
|
622
|
+
return ["manifest", ...withJson];
|
|
623
|
+
}
|
|
659
624
|
const pkgVersion = (() => {
|
|
660
625
|
try {
|
|
661
626
|
return JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")).version ?? "unknown";
|
|
@@ -673,7 +638,8 @@ for (const command of REAL) {
|
|
|
673
638
|
if (command.name === "update") {
|
|
674
639
|
process.exit(await handleUpdate(rawArgv.slice(1)));
|
|
675
640
|
}
|
|
676
|
-
|
|
641
|
+
const argv = command.name === "actions" && rawArgv.includes("--raw") ? translateActionsRaw(rawArgv) : rawArgv;
|
|
642
|
+
process.exit(await delegate(argv));
|
|
677
643
|
});
|
|
678
644
|
}
|
|
679
645
|
const HIDDEN = [
|
|
@@ -695,9 +661,24 @@ program.command("completions").description("Install/print bundled shell tab-comp
|
|
|
695
661
|
const result = spawnSync("bash", [script, ...rawArgv.slice(1)], { stdio: "inherit" });
|
|
696
662
|
process.exit(result.status ?? 1);
|
|
697
663
|
});
|
|
664
|
+
for (const retired of RETIRED) {
|
|
665
|
+
program.command(retired.name).description(`${retired.name} is retired (see teaching error)`).allowUnknownOption().helpOption(false).argument("[args...]").action(async () => {
|
|
666
|
+
if (retired.name === "live") {
|
|
667
|
+
const adapterIdx = rawArgv.indexOf("--adapter");
|
|
668
|
+
const aliasIdx = rawArgv.indexOf("-a");
|
|
669
|
+
const adapterVal = adapterIdx !== -1 ? rawArgv[adapterIdx + 1] : aliasIdx !== -1 ? rawArgv[aliasIdx + 1] : void 0;
|
|
670
|
+
if (adapterVal === "mobile" || adapterVal === "android") {
|
|
671
|
+
process.exit(await delegate(rawArgv));
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
console.error(retired.message);
|
|
676
|
+
process.exit(2);
|
|
677
|
+
});
|
|
678
|
+
}
|
|
698
679
|
const NUDGE_SKIP = ["update", "completions", "completion-candidates"];
|
|
699
680
|
if (rawArgv.length > 0 && !NUDGE_SKIP.includes(rawArgv[0])) {
|
|
700
|
-
maybeNudge();
|
|
681
|
+
setImmediate(() => void maybeNudge());
|
|
701
682
|
}
|
|
702
683
|
function hasPassthroughHelp(argv) {
|
|
703
684
|
const divider = argv.indexOf("--");
|
|
@@ -711,66 +692,10 @@ function isCallActionHelp(argv) {
|
|
|
711
692
|
const scope = divider === -1 ? argv : argv.slice(0, divider);
|
|
712
693
|
return scope.includes("--help") || scope.includes("-h");
|
|
713
694
|
}
|
|
714
|
-
function jsonRequestedBeforePassthrough(argv) {
|
|
715
|
-
const divider = argv.indexOf("--");
|
|
716
|
-
const scope = divider === -1 ? argv : argv.slice(0, divider);
|
|
717
|
-
return scope.some((argument) => argument === "--json" || argument.startsWith("--json="));
|
|
718
|
-
}
|
|
719
|
-
function jsonStreamRequestedBeforePassthrough(argv) {
|
|
720
|
-
const divider = argv.indexOf("--");
|
|
721
|
-
const scope = divider === -1 ? argv : argv.slice(0, divider);
|
|
722
|
-
return scope.includes("--json-stream");
|
|
723
|
-
}
|
|
724
|
-
function emitUsageError(error, json, jsonStream) {
|
|
725
|
-
if (jsonStream) {
|
|
726
|
-
const stream = new JsonStreamWriter(error.command, true);
|
|
727
|
-
stream.error({ code: error.code, message: error.message, userAction: error.userAction });
|
|
728
|
-
stream.complete("fail", 2);
|
|
729
|
-
} else if (json) {
|
|
730
|
-
process.stdout.write(
|
|
731
|
-
`${JSON.stringify(
|
|
732
|
-
{
|
|
733
|
-
schemaVersion: 1,
|
|
734
|
-
command: error.command,
|
|
735
|
-
status: "fail",
|
|
736
|
-
error: {
|
|
737
|
-
code: error.code,
|
|
738
|
-
message: error.message,
|
|
739
|
-
userAction: error.userAction
|
|
740
|
-
},
|
|
741
|
-
exitCode: 2
|
|
742
|
-
},
|
|
743
|
-
null,
|
|
744
|
-
2
|
|
745
|
-
)}
|
|
746
|
-
`
|
|
747
|
-
);
|
|
748
|
-
} else {
|
|
749
|
-
const scope = error.command === "mm-harness" ? "" : ` ${error.command}`;
|
|
750
|
-
process.stderr.write(`\u2717 mm-harness${scope}: ${error.message}
|
|
751
|
-
Next: ${error.userAction}
|
|
752
|
-
`);
|
|
753
|
-
}
|
|
754
|
-
process.exit(2);
|
|
755
|
-
}
|
|
756
695
|
if (rawArgv.length === 0) {
|
|
757
696
|
process.stdout.write(groupedHelp());
|
|
758
697
|
process.exit(0);
|
|
759
698
|
}
|
|
760
|
-
const preflightBypass = /* @__PURE__ */ new Set([
|
|
761
|
-
...HIDDEN,
|
|
762
|
-
"completions"
|
|
763
|
-
]);
|
|
764
|
-
if (!preflightBypass.has(rawArgv[0] ?? "")) {
|
|
765
|
-
const usageError = validatePublicInvocation(rawArgv, PUBLIC_COMMAND_EXAMPLES);
|
|
766
|
-
if (usageError) {
|
|
767
|
-
emitUsageError(
|
|
768
|
-
usageError,
|
|
769
|
-
jsonRequestedBeforePassthrough(rawArgv),
|
|
770
|
-
jsonStreamRequestedBeforePassthrough(rawArgv)
|
|
771
|
-
);
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
699
|
if (hasPassthroughHelp(rawArgv)) {
|
|
775
700
|
process.exit(await delegate(rawArgv));
|
|
776
701
|
}
|
package/dist/paths.js
CHANGED
|
@@ -47,7 +47,10 @@ 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).
|
|
50
52
|
process.env.METAMASK_RUNNER_PROTOCOL_ROOT,
|
|
53
|
+
process.env.FARMSLOT_ROOT,
|
|
51
54
|
readConfiguredProtocolRoot(),
|
|
52
55
|
findProtocolRoot(runnerDir),
|
|
53
56
|
findProtocolRoot(process.cwd())
|
|
@@ -59,7 +62,7 @@ function resolveRequiredLocalProtocolRoot(reason) {
|
|
|
59
62
|
const root = resolveLocalProtocolRoot();
|
|
60
63
|
if (!root) {
|
|
61
64
|
throw new Error(
|
|
62
|
-
`${reason} requires a local protocol/runtime checkout. Set METAMASK_RUNNER_PROTOCOL_ROOT or create .farmslot-root for this dev-only path.`
|
|
65
|
+
`${reason} requires a local protocol/runtime checkout. Set METAMASK_RUNNER_PROTOCOL_ROOT (or the legacy FARMSLOT_ROOT), or create .farmslot-root for this dev-only path.`
|
|
63
66
|
);
|
|
64
67
|
}
|
|
65
68
|
return root;
|
|
@@ -149,7 +152,7 @@ async function importProtocolPackage(packageName, localSourceEntry) {
|
|
|
149
152
|
const root = resolveLocalProtocolRoot();
|
|
150
153
|
if (!root) {
|
|
151
154
|
throw new Error(
|
|
152
|
-
`${packageName} is not installed. Install @farmslot/* packages normally, or set
|
|
155
|
+
`${packageName} is not installed. Install @farmslot/* packages normally, or set FARMSLOT_ROOT/use npm run dev:link-farmslot while co-developing protocol packages locally.`
|
|
153
156
|
);
|
|
154
157
|
}
|
|
155
158
|
return import(pathToFileURL(path.join(root, localSourceEntry)).href);
|
package/dist/runner.js
CHANGED
|
@@ -58,7 +58,7 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
|
|
|
58
58
|
const logger = options.quietStdout ? new console.Console(process.stderr) : console;
|
|
59
59
|
return createRecipeRunner({
|
|
60
60
|
actionManifest,
|
|
61
|
-
adapters:
|
|
61
|
+
adapters: [...core, ...ui, ...lifecycle, ...custom],
|
|
62
62
|
logger,
|
|
63
63
|
recording: {
|
|
64
64
|
targetProvider: createMetaMaskRecordingTargetProvider(adapter)
|
|
@@ -76,36 +76,6 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
|
|
|
76
76
|
}
|
|
77
77
|
});
|
|
78
78
|
}
|
|
79
|
-
function withActionProgress(adapters, onActionEvent) {
|
|
80
|
-
if (!onActionEvent) return adapters;
|
|
81
|
-
return adapters.map((entry) => ({
|
|
82
|
-
...entry,
|
|
83
|
-
async execute(node, context) {
|
|
84
|
-
const event = { nodeId: context.nodeId, action: entry.action };
|
|
85
|
-
const reportProgress = !isAutomaticHudProgress(entry.action, node, context.nodeId);
|
|
86
|
-
if (reportProgress) onActionEvent({ ...event, status: "running" });
|
|
87
|
-
try {
|
|
88
|
-
const result = await entry.execute(node, context);
|
|
89
|
-
if (reportProgress) {
|
|
90
|
-
onActionEvent({ ...event, status: result.status === "fail" ? "failed" : "passed" });
|
|
91
|
-
}
|
|
92
|
-
return result;
|
|
93
|
-
} catch (error) {
|
|
94
|
-
if (reportProgress) onActionEvent({ ...event, status: "failed" });
|
|
95
|
-
throw error;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}));
|
|
99
|
-
}
|
|
100
|
-
function isAutomaticHudProgress(action, node, nodeId) {
|
|
101
|
-
if (action !== "app.hud" || node === null || typeof node !== "object" || Array.isArray(node)) {
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
const payload = node;
|
|
105
|
-
if (typeof payload.action_name === "string") return true;
|
|
106
|
-
const progress = payload.progress;
|
|
107
|
-
return nodeId === "recipe-complete" && progress !== null && typeof progress === "object" && !Array.isArray(progress) && progress.complete === true;
|
|
108
|
-
}
|
|
109
79
|
function mobileSourceAwareLifecycleAdapters(adapter, lifecycle) {
|
|
110
80
|
if (adapter !== "mobile") return lifecycle;
|
|
111
81
|
return lifecycle.map((entry) => ({
|
|
@@ -113,11 +83,7 @@ function mobileSourceAwareLifecycleAdapters(adapter, lifecycle) {
|
|
|
113
83
|
async execute(node, context) {
|
|
114
84
|
const command = node.command ?? node.event ?? node.state;
|
|
115
85
|
const reloadsSource = command === "launch" || command === "foreground" || command === "restart";
|
|
116
|
-
const
|
|
117
|
-
if (source?.status === "unavailable") {
|
|
118
|
-
throw new Error(`${source.error.message} Next: ${source.error.userAction}`);
|
|
119
|
-
}
|
|
120
|
-
const fingerprint = source?.fingerprint;
|
|
86
|
+
const fingerprint = reloadsSource ? mobileSourceFingerprint(context.projectRoot) : void 0;
|
|
121
87
|
const result = await entry.execute(node, context);
|
|
122
88
|
if (fingerprint !== void 0 && !recordMobileSourceBaseline(context.projectRoot, fingerprint)) {
|
|
123
89
|
throw new Error(
|
|
@@ -150,6 +116,5 @@ export {
|
|
|
150
116
|
createMetaMaskExtensionRunner,
|
|
151
117
|
createMetaMaskMobileRunner,
|
|
152
118
|
createMetaMaskRunner,
|
|
153
|
-
isAutomaticHudProgress,
|
|
154
119
|
mobileSourceAwareLifecycleAdapters
|
|
155
120
|
};
|
package/dist/runtime-context.js
CHANGED
|
@@ -53,32 +53,6 @@ function readExistingContext(contextPath, repoRoot, runtimeDir, adapter) {
|
|
|
53
53
|
return { context, existed: true };
|
|
54
54
|
}
|
|
55
55
|
function validExistingContext(context, repoRoot, runtimeDir, adapter) {
|
|
56
|
-
const allowedFields = /* @__PURE__ */ new Set([
|
|
57
|
-
"schemaVersion",
|
|
58
|
-
"repoRoot",
|
|
59
|
-
"project",
|
|
60
|
-
"slotId",
|
|
61
|
-
"machine",
|
|
62
|
-
"runtimeOwner",
|
|
63
|
-
"strict",
|
|
64
|
-
"platform",
|
|
65
|
-
"runtimeDir",
|
|
66
|
-
"gitDirty",
|
|
67
|
-
"simulator",
|
|
68
|
-
"adbSerial",
|
|
69
|
-
"distDir",
|
|
70
|
-
"extensionId",
|
|
71
|
-
"gitHead",
|
|
72
|
-
"gitBranch",
|
|
73
|
-
"gitCommit",
|
|
74
|
-
"createdAt",
|
|
75
|
-
"session",
|
|
76
|
-
"cdpPort",
|
|
77
|
-
"watcherPort",
|
|
78
|
-
"metroPort",
|
|
79
|
-
"devServerPort"
|
|
80
|
-
]);
|
|
81
|
-
if (Object.keys(context).some((field) => !allowedFields.has(field))) return false;
|
|
82
56
|
if (context.schemaVersion !== void 0 && context.schemaVersion !== 1) return false;
|
|
83
57
|
if (context.repoRoot !== void 0 && (typeof context.repoRoot !== "string" || path.resolve(context.repoRoot) !== repoRoot)) return false;
|
|
84
58
|
if (context.platform !== void 0 && context.platform !== adapter) return false;
|
|
@@ -108,6 +82,14 @@ function validExistingContext(context, repoRoot, runtimeDir, adapter) {
|
|
|
108
82
|
const value = context[field];
|
|
109
83
|
if (value !== void 0 && (!Number.isInteger(value) || Number(value) < 1 || Number(value) > 65535)) return false;
|
|
110
84
|
}
|
|
85
|
+
if (context.runtimeStart !== void 0) {
|
|
86
|
+
if (!context.runtimeStart || typeof context.runtimeStart !== "object" || Array.isArray(context.runtimeStart)) return false;
|
|
87
|
+
const runtimeStart = context.runtimeStart;
|
|
88
|
+
if (runtimeStart.approved !== void 0 && typeof runtimeStart.approved !== "boolean") return false;
|
|
89
|
+
for (const field of ["command", "readyUrl"]) {
|
|
90
|
+
if (runtimeStart[field] !== void 0 && (typeof runtimeStart[field] !== "string" || runtimeStart[field].length === 0)) return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
111
93
|
const forbiddenResources = adapter === "core" ? ["cdpPort", "watcherPort", "metroPort", "devServerPort", "simulator", "adbSerial", "extensionId"] : adapter === "extension" ? ["metroPort", "simulator", "adbSerial"] : ["cdpPort", "extensionId"];
|
|
112
94
|
if (forbiddenResources.some((field) => context[field] !== void 0)) return false;
|
|
113
95
|
return true;
|
package/docs/ADAPTER-SURFACE.md
CHANGED
|
@@ -1,26 +1,119 @@
|
|
|
1
|
-
#
|
|
1
|
+
# The adapter surface — one seam for per-platform behavior
|
|
2
2
|
|
|
3
|
-
`
|
|
4
|
-
|
|
3
|
+
`mm-harness` runs against three platforms (mobile | extension | core). The
|
|
4
|
+
danger is that every command re-derives "what does this platform do?" with its
|
|
5
|
+
own `if (adapter === 'mobile') … else if (adapter === 'extension') …` ladder.
|
|
6
|
+
When it does, a platform gets forgotten in one command but not another — the
|
|
7
|
+
class of bug where `doctor` reported a live runtime section for mobile only,
|
|
8
|
+
`stop` refused every non-mobile adapter, and `logs` knew a single extension log
|
|
9
|
+
file instead of all three. Those are *platform-blind* commands.
|
|
5
10
|
|
|
6
|
-
|
|
11
|
+
The **adapter surface** is the fix: one interface per platform, resolved through
|
|
12
|
+
a registry, that owns the behavior a command would otherwise branch on.
|
|
7
13
|
|
|
8
|
-
|
|
9
|
-
- slot-owned ports;
|
|
10
|
-
- dev-server lifecycle;
|
|
11
|
-
- log sources;
|
|
12
|
-
- launch/recovery hints;
|
|
13
|
-
- headless capability.
|
|
14
|
+
## The interface
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
`src/adapters/surface.ts` defines `AdapterSurface` and the registry
|
|
17
|
+
`getAdapterSurface(adapter)`. Each platform ships one implementation:
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
- `src/adapters/mobile/surface.ts`
|
|
20
|
+
- `src/adapters/extension/surface.ts`
|
|
21
|
+
- `src/adapters/core/surface.ts`
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
```ts
|
|
24
|
+
interface AdapterSurface {
|
|
25
|
+
readonly adapter: MetaMaskRecipeAdapter;
|
|
26
|
+
readonly headless: boolean; // core runs no app/dev server
|
|
27
|
+
resolveSlotPorts(target: string): void; // context > pool > formula (no-op for core)
|
|
28
|
+
runtimeStatus(target: string): Promise<AdapterRuntimeStatus>; // read-only readiness for doctor
|
|
29
|
+
devServer: {
|
|
30
|
+
describe(): string; // "Metro" | "webpack watcher" | "no dev server (headless)"
|
|
31
|
+
stop(target: string): AdapterDevServerStop; // idempotent, slot-scoped; headless = teach
|
|
32
|
+
};
|
|
33
|
+
logSources(target: string): AdapterLogSource[]; // ordered candidate log files (empty for core)
|
|
34
|
+
hints: { launch: string; relaunch: string }; // platform-phrased Next: hints
|
|
35
|
+
}
|
|
36
|
+
```
|
|
24
37
|
|
|
25
|
-
|
|
26
|
-
|
|
38
|
+
`runtimeStatus` returns a normalized shape so `doctor` renders one line the same
|
|
39
|
+
way for every platform:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
interface AdapterRuntimeStatus {
|
|
43
|
+
decision: string;
|
|
44
|
+
reasonCode?: string;
|
|
45
|
+
reasons: string[];
|
|
46
|
+
deps?: string;
|
|
47
|
+
devServer?: { label: string; status: string }; // absent for headless core
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The implementations are thin: they delegate to the readiness/port plumbing that
|
|
52
|
+
already existed (`decideExtensionReadiness`, `mobileRuntimeStatus`, the slot-port
|
|
53
|
+
resolvers and the webpack-watcher stop, all re-homed to
|
|
54
|
+
`src/adapters/slot-ports.ts`). The surface is an organizing seam, not a rewrite.
|
|
55
|
+
|
|
56
|
+
## The rule
|
|
57
|
+
|
|
58
|
+
**A command never branches on adapter for behavior the surface owns.**
|
|
59
|
+
|
|
60
|
+
- Do not write `if (adapter === 'core')` — ask `surface.headless`.
|
|
61
|
+
- Do not write `adapter === 'mobile' ? metroPorts() : extensionPorts()` — call
|
|
62
|
+
`surface.resolveSlotPorts(target)`.
|
|
63
|
+
- Do not print `adapter === 'mobile' ? 'mm-harness launch ios' : 'mm-harness
|
|
64
|
+
launch'` — use `surface.hints.launch`.
|
|
65
|
+
|
|
66
|
+
A new platform behavior is added by **extending the surface** (a new member on
|
|
67
|
+
the interface plus its three implementations), never by adding another branch to
|
|
68
|
+
a command. TypeScript then makes it impossible to ship a platform that forgot the
|
|
69
|
+
new member, because each implementation is annotated `: AdapterSurface`.
|
|
70
|
+
|
|
71
|
+
What the surface deliberately does **not** own: a command may still branch on
|
|
72
|
+
adapter for a *mechanism* that is genuinely platform-specific and not part of the
|
|
73
|
+
interface — e.g. `fixtures set` uses a shell arm on mobile and the engine path on
|
|
74
|
+
extension. Those branches select a mechanism; they never re-derive readiness,
|
|
75
|
+
ports, log locations, dev-server lifecycle, or Next: phrasing, which are the
|
|
76
|
+
surface's responsibility.
|
|
77
|
+
|
|
78
|
+
## Migration status
|
|
79
|
+
|
|
80
|
+
Every command that used to branch on adapter now resolves through the surface:
|
|
81
|
+
|
|
82
|
+
| command | surface-backed | uses |
|
|
83
|
+
| --- | --- | --- |
|
|
84
|
+
| `doctor` | yes | `resolveSlotPorts` + `runtimeStatus` (mobile/extension/core runtime section) |
|
|
85
|
+
| `launch` | yes | `resolveSlotPorts` |
|
|
86
|
+
| `stop` | yes | `resolveSlotPorts` + `devServer.stop` (+ headless teaching) |
|
|
87
|
+
| `logs` | yes | `logSources` + `hints.launch` + `headless` |
|
|
88
|
+
| `debug` | yes | `headless` + `hints.relaunch` (core teaching); flag semantics stay per-command |
|
|
89
|
+
| `fixtures` | yes | `headless` + `hints` (retry/launch); the set *mechanism* stays per-platform |
|
|
90
|
+
| `run` / `call` | n/a | engine path; core-headless is handled by the heal contract, not the surface |
|
|
91
|
+
| `flows` | no (by design) | recipe-library flows are adapter-global; not a platform-owned behavior |
|
|
92
|
+
| `completion-candidates` | partial | `actions` scope to the detected checkout adapter; `flows` are adapter-global |
|
|
93
|
+
|
|
94
|
+
`flows` is intentionally not surface-backed: a recipe-library flow can compose
|
|
95
|
+
actions across platforms, so flows are adapter-global by design rather than a
|
|
96
|
+
platform-blind gap. `completion-candidates actions` already resolves the adapter
|
|
97
|
+
from the checkout context (cwd/`--target`); only the adapter-global `flows`
|
|
98
|
+
candidates are unscoped, matching the flows model.
|
|
99
|
+
|
|
100
|
+
## How this composes with the UX principles
|
|
101
|
+
|
|
102
|
+
`docs/UX-PRINCIPLES.md` principle 1 (*Context-aware by default* — "output is
|
|
103
|
+
scoped to the platform… help, flag lists, and completion candidates shrink to
|
|
104
|
+
what applies here") states the intent. The adapter surface is its **enforcement
|
|
105
|
+
mechanism**: a command that resolves platform behavior through the surface is
|
|
106
|
+
context-aware by construction, and one that hand-rolls an adapter ladder is the
|
|
107
|
+
exact failure principle 1 warns against. When adding a command or output path,
|
|
108
|
+
satisfy principle 1 by going through the surface.
|
|
109
|
+
|
|
110
|
+
## Adding a platform behavior — checklist
|
|
111
|
+
|
|
112
|
+
1. Add the member to `AdapterSurface` in `src/adapters/surface.ts`.
|
|
113
|
+
2. Implement it in all three `src/adapters/*/surface.ts` (TypeScript will not
|
|
114
|
+
compile until you do).
|
|
115
|
+
3. Have the command call `getAdapterSurface(adapter).<member>` instead of
|
|
116
|
+
branching.
|
|
117
|
+
4. Cover the closed blind spot with a contract test (see
|
|
118
|
+
`tests/contract/adapter-surface.test.sh`), and keep the registry-completeness
|
|
119
|
+
assertion green.
|