@deeeed/metamask-harness 0.7.3 → 0.7.4
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 +15 -0
- package/adapters/extension/readiness.mjs +43 -14
- package/dist/adapters/extension/ensure-ready.js +64 -23
- package/dist/cli-commands.js +1 -0
- package/dist/cli.js +2 -2
- package/dist/commands/ensure-ready.js +1 -1
- package/dist/commands/fixtures.js +159 -3
- package/dist/commands/shared.js +5 -0
- package/dist/mm-harness-cli.js +19 -7
- package/docs/CLI-SPEC.md +23 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.4 - 2026-07-07
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **`fixtures generate`** — renders the extension wallet `fixture-state.json` from a wallet fixture by fronting `adapters/extension/wallet-fixture-state.cjs generate` (no reimplementation). Extension-only (mobile/core teach); requires `--fixture <wallet-fixture.json>` and `--out <fixture-state.json>`, with a `--json` machine envelope and teaching escapes on missing inputs or a non-extension adapter. This is the public surface farm packs call instead of reaching into the internal leaf, so the Extension pack's `setup/generate-fixture.cjs` resolver can be replaced by a one-line `mm-harness fixtures generate` hook.
|
|
7
|
+
- **`fixtures finalize`** — seeds account labels/selection into the **live** extension over CDP and validates import parity, by fronting `adapters/extension/wallet-fixture-state.cjs seed-cdp` (no reimplementation). Extension-only, post-launch (mobile/core teach); requires `--fixture`, `--state`, `--cdp-port`, `--extension-dir` (optional `--extension-id-file` / `--out`), with a pure `--json` envelope (leaf progress on stderr), the shared `LEAF_MISSING` pre-check, and teaching escapes on missing flags or a non-extension adapter. This is the public surface for the Extension pack's `setup/preflight.sh` account-label finalization, retiring its last `--resolve-script` reach-in into the internal leaf.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **`fixtures generate` teaches on a missing leaf instead of leaking a node error.** When the `wallet-fixture-state.cjs` leaf is absent (a corrupt packed install), `generate` now pre-checks the leaf on disk and fails with a one-sentence teaching error plus a `Next:` reinstall command (and a clean `--json` `LEAF_MISSING` envelope), instead of surfacing node's opaque "Cannot find module".
|
|
11
|
+
- **Extension readiness no longer spawns a duplicate home tab on a transient CDP hiccup.** `ensureExtensionReady` treated any `/json/list` failure as an empty tab set, so a list that transiently failed mid-prepare (unlock → home rerender) looked like "no home tab" and opened a second one next to the healthy tab — the following health check then found two targets and failed prepare. `/json/list` now distinguishes "CDP answered: N targets" from "CDP unreachable" (retried a few times), returns `reasonCode: 'cdp-unreachable'` without touching any tab when the state is unknown, confirms a zero count with a second listing before opening, and converges over a few passes so a wrong action is corrected within the call instead of tripping the health check.
|
|
12
|
+
- **`readiness` no longer opens a duplicate home tab when the existing one is attached.** `findPageTarget` required `webSocketDebuggerUrl`, but Chrome omits that field for any target another client is attached to — during prepare the fixture finalizer / smoke recipe / CDP evals attach to the healthy home tab, so it listed without a wsUrl, looked absent, and `readiness` opened a second `home.html` that then tripped the exactly-one-home health gate. Page existence is now decided by URL in any attach state; the websocket only gates UI inspection (a present-but-attached tab is reported `pagePresent: true`, `pageInspected: false` and left alone instead of duplicated). URL matching also tolerates the `home.html#/` router rewrite and query suffixes. New additive report fields: `pagePresent`, `pageInspected`.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- **`install --adapter core` is documented and contract-locked as the public core-runner install surface for farm packs.** It already fronts `adapters/core/inject.sh` and produces the runner delegate at `<harness>/core/runner/bin/mm-harness`; a new CLI-level contract test (`tests/contract/core-install-cli.test.sh`) pins the happy path, `--json` envelope purity, and the adapter-detection teaching escape. Packs call `mm-harness install --adapter core --target <repo>` (inheriting `RECIPE_HARNESS_ROOT` / `FARMSLOT_ROOT`) instead of cloning a runner checkout and invoking the internal leaf directly — no separate top-level command is minted.
|
|
16
|
+
- **`EnsureReadyResult` gains a machine-readable `action` field** (`none` | `opened` | `reopened` | `skipped`) stating the tab decision the call took, so a consumer distinguishes "opened one because a zero was confirmed" from "touched nothing because CDP state was unknown" straight from the JSON envelope.
|
|
17
|
+
|
|
3
18
|
## 0.7.3
|
|
4
19
|
|
|
5
20
|
### Fixed
|
|
@@ -271,21 +271,40 @@ async function openExtensionPage(cdpPort, extensionId, pagePath) {
|
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
// The page path only, with any '#fragment' or '?query' stripped. The app router
|
|
275
|
+
// rewrites home.html to home.html#/ once loaded, so an exact '/home.html' suffix
|
|
276
|
+
// match must compare against this, not the raw url.
|
|
277
|
+
function pagePathOf(url) {
|
|
278
|
+
return String(url || '').split('#')[0].split('?')[0];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Find the extension home page target. Existence is decided by URL alone and is
|
|
282
|
+
// deliberately independent of webSocketDebuggerUrl: Chrome OMITS that field for a
|
|
283
|
+
// target another client is already attached to (the fixture finalizer / smoke
|
|
284
|
+
// recipe / CDP evals attach to the healthy home tab during prepare). Requiring it
|
|
285
|
+
// here made an attached-but-healthy tab look absent, so the caller opened a second
|
|
286
|
+
// home tab and the exactly-one-home health gate then failed. wsUrl only gates
|
|
287
|
+
// whether the chosen page can be inspected (see inspectCdp), never whether it
|
|
288
|
+
// exists. Among matches, prefer the requested page, then a non-popup page, then an
|
|
289
|
+
// inspectable (wsUrl-bearing) one — so a fresh unattached tab is inspected when one
|
|
290
|
+
// is available, while a sole attached tab is still recognized as present.
|
|
274
291
|
function findPageTarget(targets, selectedExtensionId, preferredPagePath = '') {
|
|
275
292
|
const extensionPages = targets.filter((target) => {
|
|
276
293
|
const url = String(target.url || '');
|
|
277
|
-
return (
|
|
278
|
-
target.type === 'page' &&
|
|
279
|
-
url.startsWith(`chrome-extension://${selectedExtensionId}/`) &&
|
|
280
|
-
typeof target.webSocketDebuggerUrl === 'string'
|
|
281
|
-
);
|
|
294
|
+
return target.type === 'page' && url.startsWith(`chrome-extension://${selectedExtensionId}/`);
|
|
282
295
|
});
|
|
296
|
+
if (extensionPages.length === 0) return undefined;
|
|
283
297
|
const normalizedPreferred = String(preferredPagePath || '').replace(/^\/+/u, '');
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
298
|
+
const score = (target) => {
|
|
299
|
+
const pagePath = pagePathOf(target.url);
|
|
300
|
+
let value = 0;
|
|
301
|
+
if (normalizedPreferred && pagePath.endsWith(`/${normalizedPreferred}`)) value += 4;
|
|
302
|
+
if (!pagePath.endsWith('/popup-init.html')) value += 2;
|
|
303
|
+
if (typeof target.webSocketDebuggerUrl === 'string') value += 1;
|
|
304
|
+
return value;
|
|
305
|
+
};
|
|
306
|
+
// reduce keeps the FIRST target on a score tie, preserving /json/list order.
|
|
307
|
+
return extensionPages.reduce((best, current) => (score(current) > score(best) ? current : best), extensionPages[0]);
|
|
289
308
|
}
|
|
290
309
|
|
|
291
310
|
async function inspectCdp(target, cdpPort, expectedExtensionId, expectedServiceWorker, extensionPagePath) {
|
|
@@ -299,14 +318,21 @@ async function inspectCdp(target, cdpPort, expectedExtensionId, expectedServiceW
|
|
|
299
318
|
let pageTarget = findPageTarget(targets, selectedExtensionId, extensionPagePath);
|
|
300
319
|
let openedPage = false;
|
|
301
320
|
if (!pageTarget) {
|
|
321
|
+
// Confirmed absence in ANY attach state (findPageTarget ignores wsUrl), so a
|
|
322
|
+
// home tab that merely has a client attached is never mistaken for missing.
|
|
323
|
+
// Only a genuinely absent page reaches here; open one and recheck.
|
|
302
324
|
openedPage = await openExtensionPage(cdpPort, selectedExtensionId, extensionPagePath);
|
|
303
325
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
304
326
|
targets = await httpJson(`http://127.0.0.1:${cdpPort}/json/list`);
|
|
305
327
|
({ extensionIds, selectedExtensionId } = chooseExtensionId(targets, expectedExtensionId, expectedServiceWorker));
|
|
306
328
|
pageTarget = findPageTarget(targets, selectedExtensionId, extensionPagePath);
|
|
307
329
|
}
|
|
330
|
+
const pagePresent = Boolean(pageTarget);
|
|
331
|
+
let pageInspected = false;
|
|
308
332
|
let ui = null;
|
|
309
|
-
|
|
333
|
+
// Inspect only when the chosen page exposes a websocket. A present-but-attached
|
|
334
|
+
// tab has none; skip inspection and report it rather than opening a duplicate.
|
|
335
|
+
if (pageTarget && typeof pageTarget.webSocketDebuggerUrl === 'string') {
|
|
310
336
|
ui = await cdpEvaluate(
|
|
311
337
|
target,
|
|
312
338
|
pageTarget.webSocketDebuggerUrl,
|
|
@@ -330,18 +356,19 @@ async function inspectCdp(target, cdpPort, expectedExtensionId, expectedServiceW
|
|
|
330
356
|
};
|
|
331
357
|
})()`,
|
|
332
358
|
);
|
|
333
|
-
|
|
359
|
+
pageInspected = Boolean(ui) && !ui.skipped;
|
|
360
|
+
if (pageInspected && ui.hasStartupError) {
|
|
334
361
|
throw Object.assign(new Error('MetaMask extension page loaded startup error UI'), {
|
|
335
362
|
report: { cdp: { browser: version.Browser || 'unknown', selectedExtensionId, ui } },
|
|
336
363
|
});
|
|
337
364
|
}
|
|
338
|
-
if (
|
|
365
|
+
if (pageInspected && ui.hasErrorBoundary) {
|
|
339
366
|
const detail = [ui.errorBoundaryName, ui.errorBoundaryMessage].filter(Boolean).join(': ');
|
|
340
367
|
throw Object.assign(new Error(`MetaMask UI crashed (React error boundary)${detail ? `: ${detail}` : ''}`), {
|
|
341
368
|
report: { cdp: { browser: version.Browser || 'unknown', selectedExtensionId, ui } },
|
|
342
369
|
});
|
|
343
370
|
}
|
|
344
|
-
if (
|
|
371
|
+
if (pageInspected && String(ui.url || '').startsWith('chrome-error://')) {
|
|
345
372
|
throw Object.assign(new Error('MetaMask extension page loaded Chrome error UI'), {
|
|
346
373
|
report: { cdp: { browser: version.Browser || 'unknown', selectedExtensionId, ui } },
|
|
347
374
|
});
|
|
@@ -355,6 +382,8 @@ async function inspectCdp(target, cdpPort, expectedExtensionId, expectedServiceW
|
|
|
355
382
|
targetCount: targets.length,
|
|
356
383
|
openedPage,
|
|
357
384
|
openedPagePath: extensionPagePath,
|
|
385
|
+
pagePresent,
|
|
386
|
+
pageInspected,
|
|
358
387
|
ui,
|
|
359
388
|
};
|
|
360
389
|
}
|
|
@@ -2,14 +2,18 @@ import path from "node:path";
|
|
|
2
2
|
import { resolveExtensionId } from "./extension-id.js";
|
|
3
3
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
4
4
|
async function jsonList(port) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
6
|
+
try {
|
|
7
|
+
const res = await fetch(`http://127.0.0.1:${port}/json/list`, { signal: AbortSignal.timeout(5e3) });
|
|
8
|
+
if (res.ok) {
|
|
9
|
+
const value = await res.json();
|
|
10
|
+
if (Array.isArray(value)) return { ok: true, targets: value };
|
|
11
|
+
}
|
|
12
|
+
} catch {
|
|
13
|
+
}
|
|
14
|
+
if (attempt < 2) await sleep(500);
|
|
12
15
|
}
|
|
16
|
+
return { ok: false, targets: [] };
|
|
13
17
|
}
|
|
14
18
|
async function closeTab(port, id) {
|
|
15
19
|
try {
|
|
@@ -53,6 +57,7 @@ async function ensureExtensionReady(target, options) {
|
|
|
53
57
|
cdpPort,
|
|
54
58
|
extensionId: null,
|
|
55
59
|
opened: false,
|
|
60
|
+
action: "none",
|
|
56
61
|
homeTabs: { before: 0, closed: 0, after: 0 },
|
|
57
62
|
ready: false,
|
|
58
63
|
reasonCode: "unknown",
|
|
@@ -60,32 +65,67 @@ async function ensureExtensionReady(target, options) {
|
|
|
60
65
|
...extra
|
|
61
66
|
});
|
|
62
67
|
const { extensionId } = await resolveExtensionId(resolved, { cdpPort });
|
|
63
|
-
if (!extensionId) return base({ reasonCode: "no-extension-id" });
|
|
64
|
-
const
|
|
65
|
-
|
|
68
|
+
if (!extensionId) return base({ reasonCode: "no-extension-id", action: "skipped" });
|
|
69
|
+
const initial = await jsonList(cdpPort);
|
|
70
|
+
if (!initial.ok) return base({ extensionId, reasonCode: "cdp-unreachable", action: "skipped" });
|
|
71
|
+
const before = homePages(initial.targets, extensionId).length;
|
|
72
|
+
const MAX_PASSES = 3;
|
|
66
73
|
let opened = false;
|
|
67
74
|
let closed = 0;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
let action = "none";
|
|
76
|
+
let targets = initial.targets;
|
|
77
|
+
for (let pass = 0; pass < MAX_PASSES; pass += 1) {
|
|
78
|
+
const homes = homePages(targets, extensionId);
|
|
79
|
+
if (homes.length === 1) break;
|
|
80
|
+
if (homes.length === 0) {
|
|
81
|
+
await sleep(750);
|
|
82
|
+
const recheck = await jsonList(cdpPort);
|
|
83
|
+
if (!recheck.ok) {
|
|
84
|
+
return base({ extensionId, opened, action: action === "none" ? "skipped" : action, homeTabs: { before, closed, after: 0 }, reasonCode: "cdp-unreachable" });
|
|
85
|
+
}
|
|
86
|
+
if (homePages(recheck.targets, extensionId).length !== 0) {
|
|
87
|
+
targets = recheck.targets;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
action = "opened";
|
|
91
|
+
opened = await openHome(cdpPort, extensionId) || opened;
|
|
92
|
+
await sleep(1500);
|
|
93
|
+
} else {
|
|
94
|
+
action = "reopened";
|
|
95
|
+
for (const h of homes) {
|
|
96
|
+
await closeTab(cdpPort, String(h.id));
|
|
97
|
+
closed += 1;
|
|
98
|
+
}
|
|
99
|
+
await sleep(500);
|
|
100
|
+
opened = await openHome(cdpPort, extensionId) || opened;
|
|
101
|
+
await sleep(1500);
|
|
72
102
|
}
|
|
73
|
-
await
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
103
|
+
const next = await jsonList(cdpPort);
|
|
104
|
+
if (!next.ok) {
|
|
105
|
+
return base({
|
|
106
|
+
extensionId,
|
|
107
|
+
opened,
|
|
108
|
+
action,
|
|
109
|
+
homeTabs: { before, closed, after: homePages(targets, extensionId).length },
|
|
110
|
+
reasonCode: "cdp-unreachable"
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
targets = next.targets;
|
|
79
114
|
}
|
|
80
115
|
const converged = await jsonList(cdpPort);
|
|
81
|
-
const
|
|
116
|
+
const convergedTargets = converged.ok ? converged.targets : targets;
|
|
117
|
+
const homesNow = homePages(convergedTargets, extensionId);
|
|
82
118
|
if (homesNow.length >= 1) {
|
|
83
|
-
for (const t of
|
|
119
|
+
for (const t of convergedTargets) {
|
|
84
120
|
if (t.type === "page" && t.id && isStrayTab(t.url)) await closeTab(cdpPort, String(t.id));
|
|
85
121
|
}
|
|
86
122
|
if (homesNow[0]?.id) await activateTab(cdpPort, String(homesNow[0].id));
|
|
87
123
|
}
|
|
88
|
-
const
|
|
124
|
+
const finalListing = await jsonList(cdpPort);
|
|
125
|
+
if (!finalListing.ok) {
|
|
126
|
+
return base({ extensionId, opened, action, homeTabs: { before, closed, after: homesNow.length }, reasonCode: "cdp-unreachable" });
|
|
127
|
+
}
|
|
128
|
+
const after = homePages(finalListing.targets, extensionId).length;
|
|
89
129
|
let health = { status: "unknown", findings: [] };
|
|
90
130
|
try {
|
|
91
131
|
const { checkExtensionRuntimeHealth } = await import("./runtime.js");
|
|
@@ -98,6 +138,7 @@ async function ensureExtensionReady(target, options) {
|
|
|
98
138
|
return base({
|
|
99
139
|
extensionId,
|
|
100
140
|
opened,
|
|
141
|
+
action,
|
|
101
142
|
homeTabs: { before, closed, after },
|
|
102
143
|
ready,
|
|
103
144
|
reasonCode: ready ? "ready" : after !== 1 ? "tab-count" : "unhealthy",
|
package/dist/cli-commands.js
CHANGED
|
@@ -13,6 +13,7 @@ const SPEC = {
|
|
|
13
13
|
{ name: "sync", desc: "Refresh harness + canonicalize wallet fixture", flags: ["--json"] },
|
|
14
14
|
{ name: "logs", aliases: ["tail"], desc: "Compact build events or full log", flags: ["--full", "-f", "--window", "--events", "--source", "--json"] },
|
|
15
15
|
{ name: "debug", aliases: ["devtools", "inspect"], desc: "Open DevTools UI", flags: ["--json", "--no-open"] },
|
|
16
|
+
{ name: "fixtures", desc: "Manage the canonical wallet fixture (sync/set/generate)", args: ["sync", "set", "generate"], flags: ["--fixture", "--out", "--adapter", "--target", "--json"] },
|
|
16
17
|
{ name: "actions", desc: "List runnable recipe actions", flags: ["--json"] },
|
|
17
18
|
{ name: "doctor", desc: "Check harness/orchestration health", flags: ["--json", "--target", "--adapter", "--runtime-dir", "--expect-live", "--cdp-port"] },
|
|
18
19
|
{ name: "run", desc: "Execute a proof recipe", args: ["recipe.json"], flags: ["--list"] },
|
package/dist/cli.js
CHANGED
|
@@ -45,8 +45,8 @@ DAILY LOOP \u2014 what a teammate runs many times a day:
|
|
|
45
45
|
mm-harness logs
|
|
46
46
|
debug Open the debug console (extension DevTools / mobile RN).
|
|
47
47
|
mm-harness debug
|
|
48
|
-
fixtures Sync
|
|
49
|
-
mm-harness fixtures sync # or:
|
|
48
|
+
fixtures Sync files + set the wallet + generate fixture-state + finalize labels over CDP.
|
|
49
|
+
mm-harness fixtures sync # or: set | generate --fixture <f> --out <o> | finalize \u2026
|
|
50
50
|
|
|
51
51
|
PROVE \u2014 run recipes and inspect capabilities:
|
|
52
52
|
run Run a recipe and write evidence (summary/trace/artifacts).
|
|
@@ -16,7 +16,7 @@ async function handleEnsureReady({ options }) {
|
|
|
16
16
|
);
|
|
17
17
|
const result = await ensureExtensionReady(target, { cdpPort });
|
|
18
18
|
if (optionFlag(options, "json")) console.log(JSON.stringify(result, null, 2));
|
|
19
|
-
else console.log(`${result.ready ? "READY" : "NOT-READY"} ${result.reasonCode} \u2014 id=${result.extensionId} homeTabs ${result.homeTabs.before}\u2192${result.homeTabs.after} (closed ${result.homeTabs.closed})`);
|
|
19
|
+
else console.log(`${result.ready ? "READY" : "NOT-READY"} ${result.reasonCode} (${result.action}) \u2014 id=${result.extensionId} homeTabs ${result.homeTabs.before}\u2192${result.homeTabs.after} (closed ${result.homeTabs.closed})`);
|
|
20
20
|
return result.ready ? 0 : 1;
|
|
21
21
|
}
|
|
22
22
|
export {
|
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { runnerDir, walletFixturePath } from "../paths.js";
|
|
4
4
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
5
|
-
import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, spawnScript, spawnScriptStreaming, str, targetOf, usageOut } from "./shared.js";
|
|
5
|
+
import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, scriptOverride, spawnScript, spawnScriptStreaming, str, targetOf, usageOut } from "./shared.js";
|
|
6
6
|
const FIXTURES_BOOLEANS = /* @__PURE__ */ new Set(["json"]);
|
|
7
7
|
const RECOVERABLE_SETUP_WALLET_PATTERNS = [
|
|
8
8
|
"CDP not reachable",
|
|
@@ -23,14 +23,16 @@ async function handleFixtures(argv, deps) {
|
|
|
23
23
|
const { positional, options } = parseFlags(argv, FIXTURES_BOOLEANS);
|
|
24
24
|
const json = flag(options, "json");
|
|
25
25
|
const sub = positional[0];
|
|
26
|
-
if (sub !== "sync" && sub !== "set") {
|
|
27
|
-
return usageOut(json, "fixtures", "fixtures requires a subcommand: mm-harness fixtures <sync|set>", "mm-harness fixtures sync or mm-harness fixtures set");
|
|
26
|
+
if (sub !== "sync" && sub !== "set" && sub !== "generate" && sub !== "finalize") {
|
|
27
|
+
return usageOut(json, "fixtures", "fixtures requires a subcommand: mm-harness fixtures <sync|set|generate|finalize>", "mm-harness fixtures sync or mm-harness fixtures set or mm-harness fixtures generate or mm-harness fixtures finalize");
|
|
28
28
|
}
|
|
29
29
|
const target = targetOf(options);
|
|
30
30
|
const adapter = resolveAdapter(options, target);
|
|
31
31
|
if (!adapter) {
|
|
32
32
|
return usageOut(json, "fixtures", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
33
33
|
}
|
|
34
|
+
if (sub === "generate") return fixturesGenerate(adapter, target, options, json);
|
|
35
|
+
if (sub === "finalize") return fixturesFinalize(adapter, target, options, json);
|
|
34
36
|
const surface = getAdapterSurface(adapter);
|
|
35
37
|
if (surface.headless) return usageOut(json, "fixtures", "core is headless; it has no wallet fixture.", surface.hints.launch);
|
|
36
38
|
const canonicalFixture = walletFixturePath(target);
|
|
@@ -128,6 +130,160 @@ async function handleFixtures(argv, deps) {
|
|
|
128
130
|
}
|
|
129
131
|
return status === "pass" ? EXIT.ok : EXIT.runtime;
|
|
130
132
|
}
|
|
133
|
+
const EXT_FIXTURE_LEAF = "adapters/extension/wallet-fixture-state.cjs";
|
|
134
|
+
function emitLeafMissing(json, action, fields, leaf) {
|
|
135
|
+
const message = `leaf could not start: ${path.basename(leaf)} (ENOENT).`;
|
|
136
|
+
const userAction = "reinstall mm-harness (npm i -g @deeeed/metamask-harness) \u2014 the fixture-state leaf is missing";
|
|
137
|
+
if (json) {
|
|
138
|
+
console.log(
|
|
139
|
+
JSON.stringify(
|
|
140
|
+
{ schemaVersion: 1, command: "fixtures", action, ...fields, status: "fail", exitCode: EXIT.runtime, error: { code: "LEAF_MISSING", message, userAction } },
|
|
141
|
+
null,
|
|
142
|
+
2
|
|
143
|
+
)
|
|
144
|
+
);
|
|
145
|
+
} else {
|
|
146
|
+
console.error(`\u2717 mm-harness fixtures ${action}: ${message}
|
|
147
|
+
Next: ${userAction}`);
|
|
148
|
+
}
|
|
149
|
+
return EXIT.runtime;
|
|
150
|
+
}
|
|
151
|
+
function fixturesGenerate(adapter, target, options, json) {
|
|
152
|
+
if (adapter !== "extension") {
|
|
153
|
+
return usageOut(
|
|
154
|
+
json,
|
|
155
|
+
"fixtures",
|
|
156
|
+
`generate builds the extension wallet fixture-state and is extension-only (detected ${adapter}).`,
|
|
157
|
+
"run it inside a metamask-extension checkout, or pass --adapter extension"
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
const fixture = str(options, "fixture");
|
|
161
|
+
const out = str(options, "out");
|
|
162
|
+
if (!fixture || !out) {
|
|
163
|
+
return usageOut(
|
|
164
|
+
json,
|
|
165
|
+
"fixtures",
|
|
166
|
+
"generate requires --fixture <wallet-fixture.json> and --out <fixture-state.json>.",
|
|
167
|
+
"mm-harness fixtures generate --fixture <wallet-fixture.json> --out <fixture-state.json>"
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
const fixturePath = path.resolve(fixture);
|
|
171
|
+
const outPath = path.resolve(out);
|
|
172
|
+
const leaf = path.join(runnerDir, EXT_FIXTURE_LEAF);
|
|
173
|
+
if (!fs.existsSync(scriptOverride(leaf) ?? leaf)) {
|
|
174
|
+
return emitLeafMissing(json, "generate", { adapter, fixture: fixturePath, out: outPath }, leaf);
|
|
175
|
+
}
|
|
176
|
+
const result = spawnScript(
|
|
177
|
+
process.execPath,
|
|
178
|
+
[leaf, "generate", "--target", target, "--fixture", fixturePath, "--out", outPath],
|
|
179
|
+
target,
|
|
180
|
+
json
|
|
181
|
+
);
|
|
182
|
+
const status = result.status === 0 ? "pass" : "fail";
|
|
183
|
+
const exitCode = result.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
184
|
+
const retry = `mm-harness fixtures generate --fixture ${fixturePath} --out ${outPath}`;
|
|
185
|
+
if (json) {
|
|
186
|
+
console.log(
|
|
187
|
+
JSON.stringify(
|
|
188
|
+
{
|
|
189
|
+
schemaVersion: 1,
|
|
190
|
+
command: "fixtures",
|
|
191
|
+
action: "generate",
|
|
192
|
+
adapter,
|
|
193
|
+
fixture: fixturePath,
|
|
194
|
+
out: outPath,
|
|
195
|
+
status,
|
|
196
|
+
exitCode,
|
|
197
|
+
error: status === "fail" ? {
|
|
198
|
+
code: "FIXTURE_GENERATE_FAILED",
|
|
199
|
+
message: "wallet fixture-state generation failed",
|
|
200
|
+
userAction: `check the wallet fixture at ${fixturePath} and the extension checkout at ${target}, then re-run: ${retry}`
|
|
201
|
+
} : null
|
|
202
|
+
},
|
|
203
|
+
null,
|
|
204
|
+
2
|
|
205
|
+
)
|
|
206
|
+
);
|
|
207
|
+
} else if (status === "fail") {
|
|
208
|
+
console.error(`\u2717 mm-harness fixtures generate failed.
|
|
209
|
+
Next: check the wallet fixture at ${fixturePath} and the extension checkout, then re-run: ${retry}`);
|
|
210
|
+
} else {
|
|
211
|
+
console.error(`Wallet fixture-state written: ${outPath}`);
|
|
212
|
+
}
|
|
213
|
+
return exitCode;
|
|
214
|
+
}
|
|
215
|
+
async function fixturesFinalize(adapter, target, options, json) {
|
|
216
|
+
if (adapter !== "extension") {
|
|
217
|
+
return usageOut(
|
|
218
|
+
json,
|
|
219
|
+
"fixtures",
|
|
220
|
+
`finalize seeds account labels into the live extension over CDP and is extension-only (detected ${adapter}).`,
|
|
221
|
+
"run it inside a metamask-extension checkout, or pass --adapter extension"
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
const fixture = str(options, "fixture");
|
|
225
|
+
const state = str(options, "state");
|
|
226
|
+
const cdpPort = str(options, "cdpPort");
|
|
227
|
+
const extensionDir = str(options, "extensionDir");
|
|
228
|
+
if (!fixture || !state || !cdpPort || !extensionDir) {
|
|
229
|
+
return usageOut(
|
|
230
|
+
json,
|
|
231
|
+
"fixtures",
|
|
232
|
+
"finalize requires --fixture <wallet-fixture.json>, --state <fixture-state.json>, --cdp-port <port>, and --extension-dir <dist/chrome>.",
|
|
233
|
+
"mm-harness fixtures finalize --fixture <wallet-fixture.json> --state <fixture-state.json> --cdp-port <port> --extension-dir <dist/chrome> [--extension-id-file <path>] [--out <report.json>]"
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
const fixturePath = path.resolve(fixture);
|
|
237
|
+
const statePath = path.resolve(state);
|
|
238
|
+
const extensionDirPath = path.resolve(extensionDir);
|
|
239
|
+
const extensionIdFile = str(options, "extensionIdFile");
|
|
240
|
+
const out = str(options, "out");
|
|
241
|
+
const outPath = out ? path.resolve(out) : void 0;
|
|
242
|
+
const leaf = path.join(runnerDir, EXT_FIXTURE_LEAF);
|
|
243
|
+
const echo = { adapter, fixture: fixturePath, state: statePath, cdpPort, extensionDir: extensionDirPath };
|
|
244
|
+
if (!fs.existsSync(scriptOverride(leaf) ?? leaf)) {
|
|
245
|
+
return emitLeafMissing(json, "finalize", echo, leaf);
|
|
246
|
+
}
|
|
247
|
+
const leafArgs = [leaf, "seed-cdp", "--target", target, "--fixture", fixturePath, "--state", statePath, "--cdp-port", cdpPort, "--extension-dir", extensionDirPath];
|
|
248
|
+
if (extensionIdFile) leafArgs.push("--extension-id-file", path.resolve(extensionIdFile));
|
|
249
|
+
if (outPath) leafArgs.push("--out", outPath);
|
|
250
|
+
process.stderr.write(`\u2192 fixtures finalize extension \u2014 seeding account labels via CDP on port ${cdpPort} (can take ~30s)\u2026
|
|
251
|
+
`);
|
|
252
|
+
const result = await spawnScriptStreaming(process.execPath, leafArgs, target);
|
|
253
|
+
const status = result.status === 0 ? "pass" : "fail";
|
|
254
|
+
const exitCode = result.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
255
|
+
const retry = `mm-harness fixtures finalize --fixture ${fixturePath} --state ${statePath} --cdp-port ${cdpPort} --extension-dir ${extensionDirPath}`;
|
|
256
|
+
if (json) {
|
|
257
|
+
console.log(
|
|
258
|
+
JSON.stringify(
|
|
259
|
+
{
|
|
260
|
+
schemaVersion: 1,
|
|
261
|
+
command: "fixtures",
|
|
262
|
+
action: "finalize",
|
|
263
|
+
adapter,
|
|
264
|
+
fixture: fixturePath,
|
|
265
|
+
state: statePath,
|
|
266
|
+
cdpPort,
|
|
267
|
+
extensionDir: extensionDirPath,
|
|
268
|
+
out: outPath ?? null,
|
|
269
|
+
status,
|
|
270
|
+
exitCode,
|
|
271
|
+
error: status === "fail" ? {
|
|
272
|
+
code: "FIXTURE_FINALIZE_FAILED",
|
|
273
|
+
message: "wallet fixture CDP finalization failed",
|
|
274
|
+
userAction: `confirm the extension is running with CDP on port ${cdpPort} (relaunch if not), then re-run: ${retry}`
|
|
275
|
+
} : null
|
|
276
|
+
},
|
|
277
|
+
null,
|
|
278
|
+
2
|
|
279
|
+
)
|
|
280
|
+
);
|
|
281
|
+
} else if (status === "fail") {
|
|
282
|
+
console.error(`\u2717 mm-harness fixtures finalize failed.
|
|
283
|
+
Next: confirm the extension is running with CDP on port ${cdpPort} (relaunch if not), then re-run: ${retry}`);
|
|
284
|
+
}
|
|
285
|
+
return exitCode;
|
|
286
|
+
}
|
|
131
287
|
function fixturesSync(adapter, target, json) {
|
|
132
288
|
const mmHarnessBin = path.join(runnerDir, "bin/mm-harness");
|
|
133
289
|
const installResult = spawnScript(
|
package/dist/commands/shared.js
CHANGED
|
@@ -52,6 +52,10 @@ function resolveAdapter(options, target, hint) {
|
|
|
52
52
|
if (hint) return hint;
|
|
53
53
|
return detectAdapter(target);
|
|
54
54
|
}
|
|
55
|
+
function scriptOverride(scriptPath) {
|
|
56
|
+
const stem = path.basename(scriptPath).replace(/[^A-Za-z0-9]/gu, "_").toUpperCase();
|
|
57
|
+
return process.env[`MM_HARNESS_SCRIPT_BIN_${stem}`];
|
|
58
|
+
}
|
|
55
59
|
function spawnScript(script, args, cwd, json, env) {
|
|
56
60
|
const isNodeScript = script === process.execPath && args.length > 0;
|
|
57
61
|
const stem = isNodeScript ? path.basename(args[0]).replace(/[^A-Za-z0-9]/gu, "_").toUpperCase() : path.basename(script).replace(/[^A-Za-z0-9]/gu, "_").toUpperCase();
|
|
@@ -170,6 +174,7 @@ export {
|
|
|
170
174
|
flag,
|
|
171
175
|
parseFlags,
|
|
172
176
|
resolveAdapter,
|
|
177
|
+
scriptOverride,
|
|
173
178
|
spawnInherit,
|
|
174
179
|
spawnScript,
|
|
175
180
|
spawnScriptStreaming,
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -350,25 +350,37 @@ Example:
|
|
|
350
350
|
},
|
|
351
351
|
{
|
|
352
352
|
name: "fixtures",
|
|
353
|
-
summary: "Manage the canonical wallet fixture (wallet DATA only) \u2014 sync
|
|
353
|
+
summary: "Manage the canonical wallet fixture (wallet DATA only) \u2014 sync / set / generate / finalize.",
|
|
354
354
|
example: "mm-harness fixtures set",
|
|
355
|
-
helpText: `mm-harness fixtures <sync|set> [flags]
|
|
355
|
+
helpText: `mm-harness fixtures <sync|set|generate|finalize> [flags]
|
|
356
356
|
|
|
357
357
|
Manage the ONE canonical wallet fixture per checkout \u2014 wallet DATA only.
|
|
358
|
-
sync
|
|
359
|
-
set
|
|
360
|
-
|
|
358
|
+
sync Refresh the wallet fixture files on the target.
|
|
359
|
+
set Apply the canonical fixture (SRP/password/accounts); the password is
|
|
360
|
+
read FROM the fixture, never typed.
|
|
361
|
+
generate Render the extension fixture-state.json from a wallet fixture, for
|
|
362
|
+
pre-launch profile prefill (extension-only). Requires --fixture and --out.
|
|
363
|
+
finalize Seed account labels/selection into the LIVE extension over CDP and
|
|
364
|
+
validate parity (extension-only, post-launch). Requires --fixture,
|
|
365
|
+
--state, --cdp-port, --extension-dir.
|
|
361
366
|
Want different accounts? Edit the fixture file directly:
|
|
362
367
|
<checkout>/temp/recipe/runtime/wallet-fixture.json
|
|
363
368
|
|
|
364
|
-
--fixture <path>
|
|
369
|
+
--fixture <path> Wallet fixture path (generate/finalize input; agent override for set/sync \u2014 env: RECIPE_WALLET_FIXTURE)
|
|
370
|
+
--out <path> generate: output fixture-state.json; finalize: optional validation report
|
|
371
|
+
--state <path> finalize: the pre-launch fixture-state.json to seed
|
|
372
|
+
--cdp-port <port> finalize: CDP port of the running extension
|
|
373
|
+
--extension-dir <path> finalize: loaded extension dist (e.g. dist/chrome)
|
|
374
|
+
--extension-id-file <path> finalize: optional file to read/write the resolved extension id
|
|
365
375
|
--adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
|
|
366
376
|
--target <path> Checkout path (default: cwd)
|
|
367
377
|
--json Machine-readable output
|
|
368
378
|
|
|
369
379
|
Example:
|
|
370
380
|
mm-harness fixtures sync
|
|
371
|
-
mm-harness fixtures set
|
|
381
|
+
mm-harness fixtures set
|
|
382
|
+
mm-harness fixtures generate --fixture wallet-fixture.json --out fixture-state.json
|
|
383
|
+
mm-harness fixtures finalize --fixture wallet-fixture.json --state fixture-state.json --cdp-port 6661 --extension-dir dist/chrome`
|
|
372
384
|
}
|
|
373
385
|
];
|
|
374
386
|
const RETIRED_INTERNAL = [
|
package/docs/CLI-SPEC.md
CHANGED
|
@@ -287,21 +287,30 @@ Recovery is silent in human mode. With `--json`: `"recovered": ["metro.restarted
|
|
|
287
287
|
|
|
288
288
|
**One canonical wallet fixture per checkout** — same SSOT model as the farm installer. There is no fixture selection, listing, or bare status verb. **Fixture status is reported by `doctor` automatically** (grounded: `doctor.ts` `fixtureSummary()` already returns `{ status: 'missing'|'ready'|'incomplete'|'invalid', path, accountCount, hasPassword }` and is included in `createDoctorReport()` at line 125). The human edits `wallet-fixture.json` directly; `fixtures set` always teaches where.
|
|
289
289
|
|
|
290
|
-
**Synopsis:** `mm-harness fixtures <sync|set> [flags]` — subcommand required.
|
|
290
|
+
**Synopsis:** `mm-harness fixtures <sync|set|generate|finalize> [flags]` — subcommand required.
|
|
291
291
|
|
|
292
292
|
| Subcommand | Behavior |
|
|
293
293
|
|---|---|
|
|
294
294
|
| `sync` | Refresh **wallet fixture files** on the target (wallet DATA only — overlay refresh is `--heal` / `doctor --fix` territory, NOT `fixtures`; they are separate concerns) |
|
|
295
295
|
| `set` | Apply THE canonical fixture (SRP/password/accounts) to the running slot — password read FROM the fixture, never typed; always prints "Want different accounts? Edit: <absolute-path>/wallet-fixture.json" |
|
|
296
|
+
| `generate` | Render the extension `fixture-state.json` from a wallet fixture for pre-launch profile prefill (fronts `adapters/extension/wallet-fixture-state.cjs generate`). **Extension-only** (mobile/core teach). Requires `--fixture <wallet-fixture.json>` and `--out <fixture-state.json>`. This is the public surface for farm packs that need the pre-launch fixture-state file, replacing direct reach-in to the internal leaf. |
|
|
297
|
+
| `finalize` | Seed account labels/selection into the **live** extension over CDP and validate import parity (fronts `adapters/extension/wallet-fixture-state.cjs seed-cdp`). **Extension-only, post-launch** (mobile/core teach). Requires `--fixture`, `--state`, `--cdp-port`, `--extension-dir`. This is the public surface for farm packs that finalize account labels after launch, replacing the last direct reach-in to the internal leaf. |
|
|
296
298
|
|
|
297
299
|
| Flag | Type | Default | ENV (agent) | Audience | Description |
|
|
298
300
|
|---|---|---|---|---|---|
|
|
299
|
-
| `--fixture <json>` | path | slot's `wallet-fixture.json` | `RECIPE_WALLET_FIXTURE` | **agent
|
|
301
|
+
| `--fixture <json>` | path | slot's `wallet-fixture.json` (`set`/`sync`); **required** for `generate`/`finalize` (input wallet fixture) | `RECIPE_WALLET_FIXTURE` | **agent** (`set`/`sync`); both (`generate`/`finalize`) | `set`/`sync`: override fixture path (human form = edit the canonical file directly). `generate`/`finalize`: the input wallet fixture. |
|
|
302
|
+
| `--out <json>` | path | `generate`: **required**; `finalize`: leaf default report path | — | both | `generate`: output `fixture-state.json`. `finalize`: optional validation report path. |
|
|
303
|
+
| `--state <json>` | path | — (**required** for `finalize`) | — | both | `finalize` only: the pre-launch `fixture-state.json` to seed into the live wallet |
|
|
304
|
+
| `--cdp-port <port>` | number | — (**required** for `finalize`) | — | both | `finalize` only: CDP port of the running extension |
|
|
305
|
+
| `--extension-dir <path>` | path | — (**required** for `finalize`) | — | both | `finalize` only: the loaded extension dist (e.g. `dist/chrome`) |
|
|
306
|
+
| `--extension-id-file <path>` | path | — | — | agent | `finalize` only: file to read/write the resolved extension id |
|
|
300
307
|
| `--platform <p>` | mobile\|extension | auto | `PLATFORM` | agent | Force platform |
|
|
301
308
|
|
|
302
|
-
**Exit:** 0 · 1 apply failed · 2 bad args.
|
|
309
|
+
**Exit:** 0 · 1 apply/generate/finalize failed · 2 bad args (incl. missing required flags, or `generate`/`finalize` on a non-extension adapter).
|
|
303
310
|
**`set` teaching output:** always ends with: `Wallet fixture applied. Want different accounts? Edit: <absolute-path>/wallet-fixture.json`.
|
|
304
|
-
|
|
311
|
+
**`generate` --json:** `{ schemaVersion, command:"fixtures", action:"generate", adapter:"extension", fixture, out, status, exitCode, error }` — leaf output stays on stderr; stdout carries only the envelope.
|
|
312
|
+
**`finalize` --json:** `{ schemaVersion, command:"fixtures", action:"finalize", adapter:"extension", fixture, state, cdpPort, extensionDir, out, status, exitCode, error }` — leaf progress streams to stderr; stdout carries only the envelope. A missing packed leaf yields `error.code:"LEAF_MISSING"` (exit 1), matching `generate`.
|
|
313
|
+
**Maps-to:** `sync` ← C:`sync`, D:`sync`,`update\|sync-runtime`; `set` ← C:`setup-wallet\|wallet-setup`,`unlock`,`setup:ios/android`(wallet half); `generate` ← Extension farm `setup/generate-fixture.cjs` (retires the pack's internal-leaf resolver); `finalize` ← Extension farm `setup/preflight.sh` seed-cdp finalization (retires the pack's last `--resolve-script` reach-in).
|
|
305
314
|
**Extension `set`:** [GAP] no standalone path today — the first-run wallet is seeded by the legacy `mme-recipe up`; the end-state home is `fixtures set` (launch / `launch --verify` never seed).
|
|
306
315
|
|
|
307
316
|
|
|
@@ -620,6 +629,16 @@ per-checkout runtime overlay. Final home is top-level (no `harness` prefix). `li
|
|
|
620
629
|
**JSON:** `{ schemaVersion, command:"harness", action, adapter, target, autoDetected, status, exitCode }`.
|
|
621
630
|
**Maps-to:** A:`harness <verb>` (ROUTES-NOW); E: skill (REMOVE); F: orchestration entry points (KEEP-INTERNAL); C/D:`prepare`,`ready\|ensure-ready` (ABSORB-LATER).
|
|
622
631
|
|
|
632
|
+
**Core-runner install — the public surface for farm packs.** `mm-harness install
|
|
633
|
+
--adapter core --target <repo>` is the pack-facing way to install the headless
|
|
634
|
+
core runner into a slot: it fronts `adapters/core/inject.sh` (the same leaf a farm
|
|
635
|
+
pack used to clone-and-run itself) and produces the runner delegate at
|
|
636
|
+
`<harness>/core/runner/bin/mm-harness`. Because the inject leaf ships inside the
|
|
637
|
+
published package, packs no longer clone a runner checkout — they call this command
|
|
638
|
+
and inherit `RECIPE_HARNESS_ROOT` / `FARMSLOT_ROOT` through the environment. There
|
|
639
|
+
is no separate top-level `install-core` command; `--adapter core` on the existing
|
|
640
|
+
`install` is the surface (grounded: `tests/contract/core-install-cli.test.sh`).
|
|
641
|
+
|
|
623
642
|
## Advanced (collapsed → KEEP-INTERNAL)
|
|
624
643
|
|
|
625
644
|
**All six ADVANCED verbs are removed from the CLI surface and `--help`** — their logic
|