@deeeed/metamask-harness 0.17.4 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +775 -0
- package/README.md +65 -71
- package/adapters/extension/ensure-browser.sh +12 -1
- package/adapters/extension/inject.mjs +7 -0
- package/adapters/extension/launch-browser.cjs +10 -3
- package/adapters/extension/lib/chrome-args.cjs +31 -0
- package/adapters/extension/lib/macos-focus.cjs +32 -0
- package/adapters/extension/live.sh +10 -20
- package/adapters/manifest.json +8 -0
- package/adapters/mobile/open-device.sh +45 -7
- package/adapters/mobile/verify.sh +15 -3
- 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-slot-ports-core.mjs +23 -6
- package/adapters/shared/resolve-slot-ports.sh +22 -5
- package/bin/mm-harness +15 -3
- package/dist/adapters/core/surface.js +4 -1
- package/dist/adapters/extension/console-capture.js +3 -1
- package/dist/adapters/extension/harness-freshness.js +39 -0
- package/dist/adapters/extension/product-config.js +110 -0
- package/dist/adapters/extension/runtime-decision.js +20 -71
- package/dist/adapters/extension/surface.js +19 -1
- package/dist/adapters/mobile/prepare.js +17 -0
- package/dist/adapters/mobile/source-freshness.js +26 -41
- package/dist/adapters/mobile/surface.js +4 -1
- package/dist/adapters/resolve-slot-ports.js +2 -0
- package/dist/adapters/slot-ports.js +13 -32
- package/dist/adapters.js +50 -17
- package/dist/checkout-lock.js +27 -2
- package/dist/cli-color.js +19 -0
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +2 -3
- package/dist/command-contract.js +13 -3
- package/dist/commands/call.js +115 -29
- package/dist/commands/checklist.js +4 -1
- package/dist/commands/completion-candidates.js +20 -13
- package/dist/commands/debug.js +31 -38
- package/dist/commands/doctor.js +33 -6
- package/dist/commands/fixtures.js +65 -17
- package/dist/commands/flows.js +39 -10
- package/dist/commands/launch/extension.js +40 -15
- package/dist/commands/launch/index.js +41 -5
- package/dist/commands/list-executables.js +151 -29
- package/dist/commands/manifest.js +127 -18
- package/dist/commands/parse-args.js +11 -1
- package/dist/commands/run-engine.js +384 -56
- package/dist/commands/run.js +112 -17
- package/dist/commands/shared.js +22 -2
- package/dist/commands/status-probe.js +3 -0
- package/dist/commands/status.js +1 -0
- package/dist/completions-cache.js +1 -1
- package/dist/doctor.js +56 -6
- package/dist/harness.js +6 -5
- package/dist/heal-bounds.js +1 -1
- package/dist/live-adapter-contract.js +132 -12
- package/dist/manifest.js +161 -1
- package/dist/mm-harness-cli.js +13 -7
- package/dist/recipe-security.js +178 -0
- package/dist/runner.js +87 -13
- package/dist/runtime-context.js +8 -26
- package/docs/CONTRIBUTING.md +137 -0
- package/docs/QA.md +185 -0
- package/docs/RECIPES.md +161 -0
- package/docs/SECURITY.md +88 -0
- package/library/actions/core/perps/read_account.mjs +2 -2
- package/library/actions/core/perps/read_orders.mjs +2 -1
- package/library/actions/core/perps/read_positions.mjs +2 -1
- package/library/actions/core/wallet/list_accounts.mjs +95 -0
- package/library/actions/extension/platform/cdp.mjs +1 -0
- package/library/actions/extension/wallet/list_accounts.mjs +41 -0
- package/library/actions/mobile/platform/bridge.mjs +1 -5
- package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
- package/library/manifests/core.action-manifest.json +61 -0
- package/library/manifests/extension.action-manifest.json +53 -0
- package/library/manifests/mobile.action-manifest.json +53 -0
- package/library/recipes/runner/action-validation.extension.recipe.json +8 -1
- package/library/recipes/runner/action-validation.mobile.recipe.json +8 -1
- package/package.json +10 -6
- package/scripts/completions.sh +7 -7
- package/docs/ADAPTER-SURFACE.md +0 -26
- package/docs/CHEATSHEET.md +0 -48
- package/docs/CLI-ERGONOMICS-AUDIT.md +0 -32
- package/docs/CLI-ERGONOMICS-HUMAN-QA.md +0 -64
- package/docs/CODE-MAP.md +0 -62
- package/docs/UX-PRINCIPLES.md +0 -66
- package/docs/VIDEO-DEMO-VALIDATION.md +0 -74
- package/docs/architecture.md +0 -88
- package/docs/live-adapter-contract.md +0 -190
- package/docs/package-boundaries.md +0 -47
- package/docs/perps-flow-catalog.md +0 -235
- package/docs/recipe-libraries.md +0 -71
- package/docs/runtime-file-conventions.md +0 -36
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveActionManifest } from "../manifest.js";
|
|
2
2
|
import {
|
|
3
3
|
invalidateCompletionCache,
|
|
4
4
|
readFreshCandidates,
|
|
@@ -13,13 +13,13 @@ import {
|
|
|
13
13
|
resolveAdapter,
|
|
14
14
|
targetPath
|
|
15
15
|
} from "./parse-args.js";
|
|
16
|
-
import { resolveMetaMaskLibrarySources } from "./run-engine.js";
|
|
16
|
+
import { listRunnableRecipes, resolveMetaMaskLibrarySources } from "./run-engine.js";
|
|
17
17
|
async function handleCompletionCandidates(argv) {
|
|
18
18
|
const kind = argv[0];
|
|
19
19
|
const { options } = parseArgs(argv.slice(1), "completion-candidates");
|
|
20
20
|
const json = optionFlag(options, "json");
|
|
21
|
-
if (kind !== "actions" && kind !== "flows") {
|
|
22
|
-
console.error("completion-candidates requires <actions|flows>.");
|
|
21
|
+
if (kind !== "actions" && kind !== "flows" && kind !== "recipes") {
|
|
22
|
+
console.error("completion-candidates requires <actions|flows|recipes>.");
|
|
23
23
|
return EXIT.usage;
|
|
24
24
|
}
|
|
25
25
|
const target = targetPath(options);
|
|
@@ -27,17 +27,15 @@ async function handleCompletionCandidates(argv) {
|
|
|
27
27
|
let candidates;
|
|
28
28
|
if (kind === "actions") {
|
|
29
29
|
adapter = resolveAdapter(options).adapter;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
} else {
|
|
30
|
+
const sources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
|
|
31
|
+
const { manifest } = await resolveActionManifest(adapter, optionString(options, "actionManifest"), sources);
|
|
32
|
+
const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
|
|
33
|
+
candidates = getRecipeActionManifestActionNames(manifest);
|
|
34
|
+
writeCompletionCandidates(target, "actions", candidates);
|
|
35
|
+
} else if (kind === "flows") {
|
|
38
36
|
candidates = readFreshCandidates(target, "flows");
|
|
39
37
|
if (!candidates) {
|
|
40
|
-
const sources = await resolveMetaMaskLibrarySources(
|
|
38
|
+
const sources = await resolveMetaMaskLibrarySources(optionString(options, "library")).catch(() => void 0);
|
|
41
39
|
if (!sources) {
|
|
42
40
|
candidates = [];
|
|
43
41
|
} else {
|
|
@@ -47,6 +45,15 @@ async function handleCompletionCandidates(argv) {
|
|
|
47
45
|
writeCompletionCandidates(target, "flows", candidates);
|
|
48
46
|
}
|
|
49
47
|
}
|
|
48
|
+
} else {
|
|
49
|
+
adapter = resolveAdapter(options).adapter;
|
|
50
|
+
const cacheKey = `recipes:${adapter}`;
|
|
51
|
+
candidates = readFreshCandidates(target, cacheKey);
|
|
52
|
+
if (!candidates) {
|
|
53
|
+
const sources = await resolveMetaMaskLibrarySources(optionString(options, "library")).catch(() => void 0);
|
|
54
|
+
candidates = sources ? listRunnableRecipes(adapter, sources).map((recipe) => recipe.name) : [];
|
|
55
|
+
writeCompletionCandidates(target, cacheKey, candidates);
|
|
56
|
+
}
|
|
50
57
|
}
|
|
51
58
|
if (json) console.log(JSON.stringify({ schemaVersion: 1, kind, adapter, candidates }, null, 2));
|
|
52
59
|
else for (const candidate of candidates) console.log(candidate);
|
package/dist/commands/debug.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { runnerDir } from "../paths.js";
|
|
3
|
-
import { acquireCheckoutLock } from "../checkout-lock.js";
|
|
4
3
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
5
|
-
import { ADAPTER_DETECT_NEXT,
|
|
4
|
+
import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, spawnScript, targetOf, usageOut } from "./shared.js";
|
|
6
5
|
const DEBUG_BOOLEANS = /* @__PURE__ */ new Set(["worker", "devMenu", "json", "noOpen"]);
|
|
7
6
|
async function handleDebug(argv) {
|
|
8
7
|
const { options } = parseFlags(argv, DEBUG_BOOLEANS);
|
|
@@ -32,43 +31,37 @@ async function handleDebug(argv) {
|
|
|
32
31
|
"mm-harness debug --worker"
|
|
33
32
|
);
|
|
34
33
|
}
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
2
|
|
65
|
-
)
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
return result.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
69
|
-
} finally {
|
|
70
|
-
lock.release();
|
|
34
|
+
const openDebug = path.join(runnerDir, "adapters/shared/open-debug.mjs");
|
|
35
|
+
const debugArgs = ["--adapter", adapter];
|
|
36
|
+
if (adapter === "extension") {
|
|
37
|
+
if (process.env.CDP_PORT) debugArgs.push("--cdp-port", process.env.CDP_PORT);
|
|
38
|
+
debugArgs.push("--target", worker ? "worker" : "page");
|
|
39
|
+
} else {
|
|
40
|
+
if (process.env.WATCHER_PORT) debugArgs.push("--port", process.env.WATCHER_PORT);
|
|
41
|
+
debugArgs.push("--action", devMenu ? "dev-menu" : "debug");
|
|
42
|
+
}
|
|
43
|
+
if (json) debugArgs.push("--json");
|
|
44
|
+
if (noOpen) debugArgs.push("--no-open");
|
|
45
|
+
const result = spawnScript(process.execPath, [openDebug, ...debugArgs], target, json);
|
|
46
|
+
if (json) {
|
|
47
|
+
const leaf = parseDebugLeaf(result.output);
|
|
48
|
+
console.log(
|
|
49
|
+
JSON.stringify(
|
|
50
|
+
{
|
|
51
|
+
...leaf,
|
|
52
|
+
schemaVersion: 1,
|
|
53
|
+
command: "debug",
|
|
54
|
+
adapter,
|
|
55
|
+
mode: worker ? "worker" : devMenu ? "dev-menu" : "default",
|
|
56
|
+
opened: leaf.opened === true,
|
|
57
|
+
exitCode: result.status === 0 ? EXIT.ok : EXIT.runtime
|
|
58
|
+
},
|
|
59
|
+
null,
|
|
60
|
+
2
|
|
61
|
+
)
|
|
62
|
+
);
|
|
71
63
|
}
|
|
64
|
+
return result.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
72
65
|
}
|
|
73
66
|
function parseDebugLeaf(output) {
|
|
74
67
|
try {
|
package/dist/commands/doctor.js
CHANGED
|
@@ -7,6 +7,7 @@ import { createDoctorReport, fixtureSummary, renderRuntimeContext, requiredDocto
|
|
|
7
7
|
import { detectAdapter, resolveRuntimeContextPath } from "../harness.js";
|
|
8
8
|
import { assertAdapter, runnerDir } from "../paths.js";
|
|
9
9
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
10
|
+
import { extensionProductConfigBlock } from "../adapters/extension/product-config.js";
|
|
10
11
|
import { ensureMobilePerpsEnvironment, mobilePerpsEnvironment } from "../adapters/mobile/perps-env.js";
|
|
11
12
|
import { loadActionManifest, validateManifest } from "../manifest.js";
|
|
12
13
|
import { ensureOverlay, newHealState, recipeRunning } from "../heal-bounds.js";
|
|
@@ -96,7 +97,19 @@ async function handleDoctor({ options }) {
|
|
|
96
97
|
lock.release();
|
|
97
98
|
}
|
|
98
99
|
const result2 = createDoctorReport(adapter, target, manifestValidation, actionManifestPath);
|
|
100
|
+
let runtime2;
|
|
101
|
+
let runtimeProbeError2;
|
|
102
|
+
try {
|
|
103
|
+
runtime2 = await surface.runtimeStatus(target);
|
|
104
|
+
} catch (error2) {
|
|
105
|
+
runtimeProbeError2 = error2 instanceof Error ? error2.message : String(error2);
|
|
106
|
+
}
|
|
107
|
+
const ready = runtime2?.decision === "ready";
|
|
108
|
+
if (adapter === "extension" && runtime2?.reasonCode === "product-config-required") {
|
|
109
|
+
failed.push("extension-product-config");
|
|
110
|
+
}
|
|
99
111
|
const nextActions = doctorFixNextActions(failed, result2.fixture, adapter, target);
|
|
112
|
+
if (failed.length === 0 && !ready && runtime2?.nextAction) nextActions.push(runtime2.nextAction);
|
|
100
113
|
const status = result2.status === "pass" && failed.length === 0 ? "pass" : "fail";
|
|
101
114
|
const retryDoctor = `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
|
|
102
115
|
const fixUserAction = failed.includes("wallet-fixture") ? `choose one wallet fixture source from nextActions, run it, then retry: ${retryDoctor}` : nextActions[0] ?? retryDoctor;
|
|
@@ -105,9 +118,15 @@ async function handleDoctor({ options }) {
|
|
|
105
118
|
message: "doctor --fix could not make every required check pass",
|
|
106
119
|
userAction: fixUserAction
|
|
107
120
|
} : void 0;
|
|
108
|
-
if (json) console.log(JSON.stringify({ ...result2, status, fixed, failed, nextActions, ...error ? { error } : {} }, null, 2));
|
|
121
|
+
if (json) console.log(JSON.stringify({ ...result2, status, fixed, failed, ready, runtime: runtime2, ...runtimeProbeError2 ? { runtimeProbeError: runtimeProbeError2 } : {}, nextActions, ...error ? { error } : {} }, null, 2));
|
|
109
122
|
else {
|
|
110
|
-
console.log(`${status} ${adapter} ${result2.compatibilityMode} manifest=${actionManifestPath} fixed=[${fixed.join(",")}] failed=[${failed.join(",")}]`);
|
|
123
|
+
console.log(`${status} ${adapter} ${result2.compatibilityMode} ready=${ready} manifest=${actionManifestPath} fixed=[${fixed.join(",")}] failed=[${failed.join(",")}]`);
|
|
124
|
+
if (runtime2) {
|
|
125
|
+
console.log(`runtime: decision=${runtime2.decision}${runtime2.reasonCode ? ` (${runtime2.reasonCode})` : ""}`);
|
|
126
|
+
for (const reason of runtime2.reasons) console.log(` ${reason}`);
|
|
127
|
+
} else if (runtimeProbeError2) {
|
|
128
|
+
console.log(`runtime probe failed: ${runtimeProbeError2}`);
|
|
129
|
+
}
|
|
111
130
|
for (const action of nextActions) console.error(` Next: ${action}`);
|
|
112
131
|
}
|
|
113
132
|
return status === "pass" ? 0 : 1;
|
|
@@ -161,11 +180,15 @@ async function handleDoctor({ options }) {
|
|
|
161
180
|
}
|
|
162
181
|
const doctorUserAction = `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
|
|
163
182
|
const doctorError = doctorResult.status === "fail" ? { code: "DOCTOR_CHECKS_FAILED", message: "one or more required doctor checks failed", userAction: doctorUserAction } : void 0;
|
|
164
|
-
|
|
183
|
+
const next = doctorError?.userAction ?? (runtime?.decision === "ready" ? void 0 : runtime?.nextAction);
|
|
184
|
+
if (json) console.log(JSON.stringify({ ...doctorResult, ready: runtime?.decision === "ready", runtime, orphanMetros, capture, devices, additionalReachableDevices, ...next ? { next } : {}, ...doctorError ? { error: doctorError } : {} }, null, 2));
|
|
165
185
|
else {
|
|
166
186
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
167
187
|
const stateStyle = (value, good) => value === good ? "ok" : "warn";
|
|
168
188
|
console.log(`${out(doctorResult.status === "pass" ? "ok" : "err", doctorResult.status)} ${out("bold", adapter)} ${doctorResult.compatibilityMode} ${out("dim", `manifest=${actionManifestPath}`)}`);
|
|
189
|
+
console.log(
|
|
190
|
+
`${out("label", "harness:")} ${doctorResult.runner.packageName}@${doctorResult.runner.version} ${out("accent", doctorResult.runner.installKind)} ${out("dim", `executable=${doctorResult.runner.executablePath}`)}`
|
|
191
|
+
);
|
|
169
192
|
if (runtime) {
|
|
170
193
|
const provisionedDepsPending = runtime.reasonCode === "app-installed-deps-pending";
|
|
171
194
|
const decisionStyle = provisionedDepsPending ? "info" : runtime.decision === "ready" ? "ok" : runtime.decision === "blocked" ? "err" : "warn";
|
|
@@ -198,7 +221,7 @@ async function handleDoctor({ options }) {
|
|
|
198
221
|
if (additionalReachableDevices.length > 0) renderAdditionalReachableDevices(additionalReachableDevices, out);
|
|
199
222
|
if (liveView) renderMobileLiveBlock(deviceView?.devices ?? [], liveView.liveMap, out);
|
|
200
223
|
}
|
|
201
|
-
if (
|
|
224
|
+
if (next) console.log(` ${out("dim", `Next: ${next}`)}`);
|
|
202
225
|
}
|
|
203
226
|
return doctorResult.status === "pass" ? 0 : 1;
|
|
204
227
|
}
|
|
@@ -236,6 +259,10 @@ function doctorFixNextActions(failed, fixture, adapter, target) {
|
|
|
236
259
|
);
|
|
237
260
|
}
|
|
238
261
|
if (failed.includes("overlay")) actions.push(`${executable} install ${scope}`);
|
|
262
|
+
if (adapter === "extension" && failed.includes("extension-product-config")) {
|
|
263
|
+
const block = extensionProductConfigBlock(target);
|
|
264
|
+
if (block) actions.push(block.userAction);
|
|
265
|
+
}
|
|
239
266
|
return actions;
|
|
240
267
|
}
|
|
241
268
|
function emitExpectLiveVerdict(adapter, target, runtime, verdict) {
|
|
@@ -243,7 +270,7 @@ function emitExpectLiveVerdict(adapter, target, runtime, verdict) {
|
|
|
243
270
|
const detail = runtime ? `${runtime.decision}${runtime.reasonCode ? ` (${runtime.reasonCode})` : ""}` : "runtime probe unavailable";
|
|
244
271
|
console.error(`${out("err", verdict)} ${out("bold", adapter)} ${detail} ${out("dim", target)}`);
|
|
245
272
|
for (const reason of runtime?.reasons ?? []) console.error(` ${out("dim", reason)}`);
|
|
246
|
-
console.error(` Next: ${getAdapterSurface(adapter).hints.
|
|
273
|
+
console.error(` Next: ${runtime?.nextAction ?? getAdapterSurface(adapter).hints.runtimeProbeRecovery(target)}`);
|
|
247
274
|
}
|
|
248
275
|
function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture, devices, additionalReachableDevices, json, printReady) {
|
|
249
276
|
const live = runtime?.decision === "ready";
|
|
@@ -261,7 +288,7 @@ function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture,
|
|
|
261
288
|
return EXIT.runtime;
|
|
262
289
|
}
|
|
263
290
|
if (json) {
|
|
264
|
-
const userAction =
|
|
291
|
+
const userAction = runtime?.nextAction ?? getAdapterSurface(adapter).hints.runtimeProbeRecovery(target);
|
|
265
292
|
const error = live ? void 0 : { code: "RUNTIME_NOT_LIVE", message: `${adapter} runtime is not live`, userAction };
|
|
266
293
|
console.log(JSON.stringify({ ...result, ready: live, runtime, orphanMetros, capture, devices, additionalReachableDevices, ...error ? { error } : {} }, null, 2));
|
|
267
294
|
return live ? EXIT.ok : EXIT.runtime;
|
|
@@ -5,21 +5,22 @@ import { runnerDir, walletFixturePath } from "../paths.js";
|
|
|
5
5
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
6
6
|
import { fixtureFileSummary, fixtureSummary } from "../doctor.js";
|
|
7
7
|
import { acquireCheckoutLock } from "../checkout-lock.js";
|
|
8
|
+
import { colorHumanMessage } from "../cli-color.js";
|
|
8
9
|
import { applyDeviceTargeting } from "./device-target.js";
|
|
9
10
|
import { ADAPTER_DETECT_NEXT, checkoutBusyOut, EXIT, flag, parseFlags, resolveAdapter, scriptOverride, spawnScript, spawnScriptStreaming, str, targetOf, usageOut } from "./shared.js";
|
|
10
11
|
const FIXTURES_BOOLEANS = /* @__PURE__ */ new Set(["dev", "force", "json"]);
|
|
11
12
|
const FIXTURE_STREAM_TIMEOUT_MS = 12e4;
|
|
12
|
-
async function handleFixtures(argv
|
|
13
|
+
async function handleFixtures(argv) {
|
|
13
14
|
const { options } = parseFlags(argv, FIXTURES_BOOLEANS);
|
|
14
15
|
const json = flag(options, "json");
|
|
15
16
|
const target = targetOf(options);
|
|
16
|
-
if (!fs.existsSync(target)) return handleFixturesLocked(argv
|
|
17
|
+
if (!fs.existsSync(target)) return handleFixturesLocked(argv);
|
|
17
18
|
const lock = acquireCheckoutLock(target, "fixtures");
|
|
18
19
|
if ("message" in lock) {
|
|
19
20
|
return checkoutBusyOut(json, "fixtures", lock.message, lock.path);
|
|
20
21
|
}
|
|
21
22
|
try {
|
|
22
|
-
return await handleFixturesLocked(argv
|
|
23
|
+
return await handleFixturesLocked(argv);
|
|
23
24
|
} finally {
|
|
24
25
|
lock.release();
|
|
25
26
|
}
|
|
@@ -49,7 +50,7 @@ function ensureFixturePublicationParent(target) {
|
|
|
49
50
|
}
|
|
50
51
|
return fs.realpathSync(parent) === parent ? null : `wallet fixture runtime path escapes the checkout: ${parent}`;
|
|
51
52
|
}
|
|
52
|
-
async function handleFixturesLocked(argv
|
|
53
|
+
async function handleFixturesLocked(argv) {
|
|
53
54
|
const { positional, options } = parseFlags(argv, FIXTURES_BOOLEANS);
|
|
54
55
|
const json = flag(options, "json");
|
|
55
56
|
const sub = positional[0];
|
|
@@ -112,6 +113,9 @@ async function handleFixturesLocked(argv, deps) {
|
|
|
112
113
|
"mm-harness fixtures init --dev && mm-harness fixtures set"
|
|
113
114
|
);
|
|
114
115
|
}
|
|
116
|
+
if (adapter === "extension") {
|
|
117
|
+
return applyExtensionFixture(target, fixturePath, canonicalFixture, json);
|
|
118
|
+
}
|
|
115
119
|
process.stderr.write(`\u2192 fixtures set ${adapter} \u2014 connecting bridge + applying wallet fixture (can take ~30s)\u2026
|
|
116
120
|
`);
|
|
117
121
|
let status;
|
|
@@ -144,19 +148,6 @@ async function handleFixturesLocked(argv, deps) {
|
|
|
144
148
|
if (!json && status === "fail") {
|
|
145
149
|
console.error(` Next: ${retryHint}`);
|
|
146
150
|
}
|
|
147
|
-
} else {
|
|
148
|
-
const previousFixtureEnv = process.env.RECIPE_WALLET_FIXTURE;
|
|
149
|
-
process.env.RECIPE_WALLET_FIXTURE = fixturePath;
|
|
150
|
-
try {
|
|
151
|
-
const result = await deps.runOneNode("extension", "metamask.wallet.setup", {}, target, str(options, "actionManifest"));
|
|
152
|
-
status = result.status;
|
|
153
|
-
} finally {
|
|
154
|
-
if (previousFixtureEnv === void 0) delete process.env.RECIPE_WALLET_FIXTURE;
|
|
155
|
-
else process.env.RECIPE_WALLET_FIXTURE = previousFixtureEnv;
|
|
156
|
-
}
|
|
157
|
-
if (!json && status === "fail") {
|
|
158
|
-
console.error(` Next: ${retryHint}`);
|
|
159
|
-
}
|
|
160
151
|
}
|
|
161
152
|
const teaching = status === "pass" ? `Wallet fixture applied. Want different accounts? Edit: ${canonicalFixture}` : "Wallet fixture was not applied.";
|
|
162
153
|
if (json) {
|
|
@@ -187,6 +178,63 @@ async function handleFixturesLocked(argv, deps) {
|
|
|
187
178
|
}
|
|
188
179
|
return status === "pass" ? EXIT.ok : EXIT.runtime;
|
|
189
180
|
}
|
|
181
|
+
async function applyExtensionFixture(target, fixturePath, canonicalFixture, json) {
|
|
182
|
+
const distManifest = path.join(target, "dist/chrome/manifest.json");
|
|
183
|
+
const userAction = `mm-harness launch --build --verify --target ${JSON.stringify(target)}`;
|
|
184
|
+
if (!fs.existsSync(distManifest)) {
|
|
185
|
+
const message2 = `Extension fixture needs an existing dist/chrome build; no manifest was found at ${distManifest}.`;
|
|
186
|
+
if (json) {
|
|
187
|
+
console.log(JSON.stringify({
|
|
188
|
+
schemaVersion: 1,
|
|
189
|
+
command: "fixtures",
|
|
190
|
+
action: "set",
|
|
191
|
+
adapter: "extension",
|
|
192
|
+
fixture: fixturePath,
|
|
193
|
+
canonicalFixture,
|
|
194
|
+
status: "fail",
|
|
195
|
+
exitCode: EXIT.runtime,
|
|
196
|
+
error: { code: "EXTENSION_FIXTURE_BUILD_REQUIRED", message: message2, userAction }
|
|
197
|
+
}, null, 2));
|
|
198
|
+
} else {
|
|
199
|
+
console.error(`\u2717 mm-harness fixtures set: ${message2}
|
|
200
|
+
Next: ${userAction}`);
|
|
201
|
+
}
|
|
202
|
+
return EXIT.runtime;
|
|
203
|
+
}
|
|
204
|
+
process.stderr.write(`${colorHumanMessage("\u2192 fixtures set extension \u2014 resetting and seeding the slot-owned profile from the existing build\u2026")}
|
|
205
|
+
`);
|
|
206
|
+
const liveScript = path.join(runnerDir, "adapters/extension/live.sh");
|
|
207
|
+
const args = ["--target", target, "--launch-existing-dist"];
|
|
208
|
+
if (process.env.CDP_PORT) args.push("--cdp-port", process.env.CDP_PORT);
|
|
209
|
+
const result = await spawnScriptStreaming(liveScript, args, target, {
|
|
210
|
+
env: { RECIPE_WALLET_FIXTURE: fixturePath },
|
|
211
|
+
timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
|
|
212
|
+
});
|
|
213
|
+
const status = result.status === 0 ? "pass" : "fail";
|
|
214
|
+
const exitCode = status === "pass" ? EXIT.ok : EXIT.runtime;
|
|
215
|
+
const message = status === "pass" ? `Wallet fixture applied and validated. Want different accounts? Edit: ${canonicalFixture}` : "Extension wallet fixture could not be applied and validated.";
|
|
216
|
+
if (json) {
|
|
217
|
+
console.log(JSON.stringify({
|
|
218
|
+
schemaVersion: 1,
|
|
219
|
+
command: "fixtures",
|
|
220
|
+
action: "set",
|
|
221
|
+
adapter: "extension",
|
|
222
|
+
fixture: fixturePath,
|
|
223
|
+
canonicalFixture,
|
|
224
|
+
status,
|
|
225
|
+
exitCode,
|
|
226
|
+
profileReset: status === "pass",
|
|
227
|
+
message,
|
|
228
|
+
error: status === "fail" ? { code: "EXTENSION_FIXTURE_APPLY_FAILED", message, userAction } : null
|
|
229
|
+
}, null, 2));
|
|
230
|
+
} else if (status === "pass") {
|
|
231
|
+
console.error(message);
|
|
232
|
+
} else {
|
|
233
|
+
console.error(`\u2717 mm-harness fixtures set: ${message}
|
|
234
|
+
Next: ${userAction}`);
|
|
235
|
+
}
|
|
236
|
+
return exitCode;
|
|
237
|
+
}
|
|
190
238
|
const EXT_FIXTURE_LEAF = "adapters/extension/wallet-fixture-state.cjs";
|
|
191
239
|
function emitLeafMissing(json, action, fields, leaf) {
|
|
192
240
|
const message = `leaf could not start: ${path.basename(leaf)} (ENOENT).`;
|
package/dist/commands/flows.js
CHANGED
|
@@ -2,18 +2,18 @@ import path from "node:path";
|
|
|
2
2
|
import { color } from "../cli-color.js";
|
|
3
3
|
import { importRecipeHarnessCli } from "../paths.js";
|
|
4
4
|
import { EXIT } from "./shared.js";
|
|
5
|
-
import { usageError } from "./parse-args.js";
|
|
5
|
+
import { parseArgs, usageError } from "./parse-args.js";
|
|
6
6
|
import { resolveMetaMaskLibrarySources } from "./run-engine.js";
|
|
7
|
-
const FLOWS_SUBCOMMANDS = ["list", "promote"];
|
|
8
|
-
function
|
|
7
|
+
const FLOWS_SUBCOMMANDS = ["list", "describe", "promote"];
|
|
8
|
+
function stripFrontOnlyFlags(argv) {
|
|
9
9
|
const out = [];
|
|
10
10
|
for (let i = 0; i < argv.length; i += 1) {
|
|
11
11
|
const arg = argv[i];
|
|
12
|
-
if (arg === "--target") {
|
|
12
|
+
if (arg === "--target" || arg === "--library") {
|
|
13
13
|
i += 1;
|
|
14
14
|
continue;
|
|
15
15
|
}
|
|
16
|
-
if (arg.startsWith("--target=")) continue;
|
|
16
|
+
if (arg.startsWith("--target=") || arg.startsWith("--library=")) continue;
|
|
17
17
|
out.push(arg);
|
|
18
18
|
}
|
|
19
19
|
return out;
|
|
@@ -30,14 +30,37 @@ function restoreEnv(key, value) {
|
|
|
30
30
|
if (value === void 0) delete process.env[key];
|
|
31
31
|
else process.env[key] = value;
|
|
32
32
|
}
|
|
33
|
+
function libraryEntries(argv) {
|
|
34
|
+
const entries = [];
|
|
35
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
36
|
+
const argument = argv[index];
|
|
37
|
+
if (argument === "--library") {
|
|
38
|
+
const value = argv[index + 1];
|
|
39
|
+
if (value !== void 0) entries.push(value);
|
|
40
|
+
index += 1;
|
|
41
|
+
} else if (argument.startsWith("--library=")) {
|
|
42
|
+
entries.push(argument.slice("--library=".length));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return entries;
|
|
46
|
+
}
|
|
33
47
|
async function handleFlows(argv) {
|
|
34
|
-
const
|
|
48
|
+
const { positional } = parseArgs(argv);
|
|
49
|
+
const subcommand = positional[0];
|
|
50
|
+
if (subcommand === "describe" && !positional[1]) {
|
|
51
|
+
throw usageError("flows describe requires <ref>. Next: mm-harness flows list");
|
|
52
|
+
}
|
|
53
|
+
if (subcommand !== "describe" && positional[1]) {
|
|
54
|
+
throw usageError(`flows ${subcommand ?? "list"} does not accept positional ${positional[1]}.`);
|
|
55
|
+
}
|
|
56
|
+
const configuredLibraries = libraryEntries(argv);
|
|
57
|
+
const sources = await resolveMetaMaskLibrarySources(configuredLibraries);
|
|
35
58
|
if (!sources) {
|
|
36
59
|
throw usageError(
|
|
37
60
|
"flows requires @farmslot/recipe-harness >= 0.3.3 (recipe-library support; flows resolve personal > team > canonical). Next: update the dependency and run yarn install."
|
|
38
61
|
);
|
|
39
62
|
}
|
|
40
|
-
const forwarded =
|
|
63
|
+
const forwarded = stripFrontOnlyFlags(argv);
|
|
41
64
|
const jsonMode = forwarded.includes("--json");
|
|
42
65
|
if (!jsonMode) {
|
|
43
66
|
console.error(color("label", "flows resolve by precedence (highest wins; local shadows canonical):"));
|
|
@@ -46,16 +69,22 @@ async function handleFlows(argv) {
|
|
|
46
69
|
console.error(` ${index + 1}. ${color("cmd", name)} ${color("dim", `[${flowSourceTier(source, index, sources.length)}]`)}`);
|
|
47
70
|
});
|
|
48
71
|
}
|
|
49
|
-
const
|
|
72
|
+
const resolvedSubcommand = subcommand && FLOWS_SUBCOMMANDS.includes(subcommand) ? subcommand : "list";
|
|
73
|
+
const forwardedSubcommandIndex = forwarded.indexOf(resolvedSubcommand);
|
|
74
|
+
const delegatedArgs = forwardedSubcommandIndex === -1 ? forwarded : forwarded.filter((_, index) => index !== forwardedSubcommandIndex);
|
|
75
|
+
const withSubcommand = [resolvedSubcommand, ...delegatedArgs];
|
|
50
76
|
const { runRecipeHarnessCli } = await importRecipeHarnessCli();
|
|
51
77
|
const previousLibraryPath = process.env.RECIPE_LIBRARY_PATH;
|
|
78
|
+
const previousExitCode = process.exitCode;
|
|
52
79
|
process.env.RECIPE_LIBRARY_PATH = serializeLibrarySources(sources);
|
|
80
|
+
process.exitCode = void 0;
|
|
53
81
|
try {
|
|
54
|
-
await runRecipeHarnessCli(["flows", ...withSubcommand]);
|
|
82
|
+
await runRecipeHarnessCli(["flows", ...withSubcommand], { commandName: "mm-harness" });
|
|
83
|
+
return typeof process.exitCode === "number" ? process.exitCode : EXIT.ok;
|
|
55
84
|
} finally {
|
|
56
85
|
restoreEnv("RECIPE_LIBRARY_PATH", previousLibraryPath);
|
|
86
|
+
process.exitCode = previousExitCode;
|
|
57
87
|
}
|
|
58
|
-
return EXIT.ok;
|
|
59
88
|
}
|
|
60
89
|
export {
|
|
61
90
|
handleFlows
|
|
@@ -3,12 +3,15 @@ import http from "node:http";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { depsCheck } from "@farmslot/recipe-harness/runtime/deps-readiness";
|
|
6
|
+
import { colorHumanMessage } from "../../cli-color.js";
|
|
6
7
|
import { recipeHarnessPath, recipeRuntimeDir, runnerDir } from "../../paths.js";
|
|
7
8
|
import { extensionIdFromKey } from "../../adapters/extension/extension-id.js";
|
|
9
|
+
import { extensionProductConfigBlock } from "../../adapters/extension/product-config.js";
|
|
8
10
|
import { isExtensionDistStale } from "../../adapters/extension/runtime-decision.js";
|
|
9
11
|
import { checkExtensionRuntimeHealth } from "../../adapters/extension/runtime.js";
|
|
12
|
+
import { ensureExtensionHarnessFresh } from "../../adapters/extension/harness-freshness.js";
|
|
10
13
|
import { stopExtensionWatcher } from "../../adapters/slot-ports.js";
|
|
11
|
-
import { spawnScriptStreaming } from "../shared.js";
|
|
14
|
+
import { EXIT, spawnScriptStreaming } from "../shared.js";
|
|
12
15
|
function extensionDepsBlock(target) {
|
|
13
16
|
if (!fs.existsSync(path.join(target, "package.json"))) return null;
|
|
14
17
|
const deps = depsCheck(target);
|
|
@@ -29,17 +32,32 @@ function installExtensionDeps(target) {
|
|
|
29
32
|
return spawnScriptStreaming(installScript, ["--target", target], target);
|
|
30
33
|
}
|
|
31
34
|
async function launchExtension(target, tier, wantWatch, displayMode = "fullscreen") {
|
|
35
|
+
if (process.env.CHROME_USER_DATA_DIR) {
|
|
36
|
+
process.env.CHROME_USER_DATA_DIR = path.resolve(process.env.CHROME_USER_DATA_DIR);
|
|
37
|
+
}
|
|
38
|
+
const reusable = !wantWatch && tier !== "build" && await extensionRuntimeReusable(target);
|
|
39
|
+
if (!reusable) {
|
|
40
|
+
const block = extensionProductConfigBlock(target);
|
|
41
|
+
if (block) {
|
|
42
|
+
return {
|
|
43
|
+
status: EXIT.infra,
|
|
44
|
+
output: `EXTENSION_PRODUCT_CONFIG_REQUIRED: ${block.message}
|
|
45
|
+
Next: ${block.userAction}`
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
await ensureExtensionHarnessFresh(target);
|
|
32
50
|
if (wantWatch) {
|
|
33
51
|
const startWatchSh = path.join(runnerDir, "adapters/extension/start-watch.sh");
|
|
34
52
|
const watchArgs = ["--target", target];
|
|
35
53
|
if (process.env.WATCHER_PORT) watchArgs.push("--watcher-port", process.env.WATCHER_PORT);
|
|
36
|
-
console.error(`\u2192 extension watch \u2014 webpack :${process.env.WATCHER_PORT ?? "default"} (output streams below)`);
|
|
54
|
+
console.error(colorHumanMessage(`\u2192 extension watch \u2014 webpack :${process.env.WATCHER_PORT ?? "default"} (output streams below)`));
|
|
37
55
|
return spawnScriptStreaming(startWatchSh, watchArgs, target);
|
|
38
56
|
}
|
|
39
57
|
if (tier === "build") {
|
|
40
58
|
return extensionRebuild(target);
|
|
41
59
|
}
|
|
42
|
-
if (
|
|
60
|
+
if (reusable) {
|
|
43
61
|
return extensionReattach(target, displayMode);
|
|
44
62
|
}
|
|
45
63
|
return extensionRebuild(target);
|
|
@@ -47,12 +65,16 @@ async function launchExtension(target, tier, wantWatch, displayMode = "fullscree
|
|
|
47
65
|
async function extensionRuntimeReusable(target) {
|
|
48
66
|
const cdpPort = process.env.CDP_PORT;
|
|
49
67
|
if (!cdpPort) return false;
|
|
50
|
-
if (!cdpOwnedByExpectedRuntime(cdpPort, target)) return false;
|
|
51
|
-
if (!await cdpVersionReachable(cdpPort)) return false;
|
|
52
|
-
if (!await cdpHasExpectedExtensionTarget(cdpPort, target)) return false;
|
|
53
|
-
if (!await cdpRuntimeHealthy(cdpPort, target)) return false;
|
|
54
68
|
if (isExtensionDistStale(target)) return false;
|
|
55
|
-
|
|
69
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
70
|
+
const reachable = await cdpVersionReachable(cdpPort);
|
|
71
|
+
const owned = reachable && cdpOwnedByExpectedRuntime(cdpPort, target);
|
|
72
|
+
const targetPresent = owned && await cdpHasExpectedExtensionTarget(cdpPort, target);
|
|
73
|
+
const healthy = targetPresent && await cdpRuntimeHealthy(cdpPort, target);
|
|
74
|
+
if (healthy) return true;
|
|
75
|
+
if (attempt < 2) await new Promise((resolve) => setTimeout(resolve, 250));
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
56
78
|
}
|
|
57
79
|
function expectedChromeProfile(target) {
|
|
58
80
|
const profileName = process.env.RECIPE_CHROME_PROFILE_NAME || "chrome-profile";
|
|
@@ -168,12 +190,15 @@ function cdpHasExpectedExtensionTarget(port, target) {
|
|
|
168
190
|
});
|
|
169
191
|
}
|
|
170
192
|
async function cdpRuntimeHealthy(port, target) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
193
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
194
|
+
try {
|
|
195
|
+
const report = await checkExtensionRuntimeHealth(target, Number(port), { pageMode: "home-or-sidepanel" });
|
|
196
|
+
if (report.status === "PASS") return true;
|
|
197
|
+
} catch {
|
|
198
|
+
}
|
|
199
|
+
if (attempt < 2) await new Promise((resolve) => setTimeout(resolve, 250));
|
|
176
200
|
}
|
|
201
|
+
return false;
|
|
177
202
|
}
|
|
178
203
|
async function extensionReattach(target, displayMode = "fullscreen") {
|
|
179
204
|
const reattachScript = recipeHarnessPath(target, "extension", "scripts", "reattach.sh");
|
|
@@ -181,7 +206,7 @@ async function extensionReattach(target, displayMode = "fullscreen") {
|
|
|
181
206
|
if (process.env.CDP_PORT) reattachArgs.push("--cdp-port", process.env.CDP_PORT);
|
|
182
207
|
if (process.env.WATCHER_PORT) reattachArgs.push("--watcher-port", process.env.WATCHER_PORT);
|
|
183
208
|
if (process.env.EXTENSION_START_URL) reattachArgs.push("--start-url", process.env.EXTENSION_START_URL);
|
|
184
|
-
console.error(`\u2192 extension quick reattach \u2014 reload in place \xB7 CDP :${process.env.CDP_PORT ?? "default"} (no rebuild, no relaunch)`);
|
|
209
|
+
console.error(colorHumanMessage(`\u2192 extension quick reattach \u2014 reload in place \xB7 CDP :${process.env.CDP_PORT ?? "default"} (no rebuild, no relaunch)`));
|
|
185
210
|
return spawnScriptStreaming(reattachScript, reattachArgs, target);
|
|
186
211
|
}
|
|
187
212
|
async function extensionRebuild(target) {
|
|
@@ -195,7 +220,7 @@ async function extensionRebuild(target) {
|
|
|
195
220
|
const liveArgs = ["--target", target, "--start-watch"];
|
|
196
221
|
if (process.env.CDP_PORT) liveArgs.push("--cdp-port", process.env.CDP_PORT);
|
|
197
222
|
if (process.env.EXTENSION_START_URL) liveArgs.push("--start-url", process.env.EXTENSION_START_URL);
|
|
198
|
-
console.error(`\u2192 extension quick relaunch \u2014 webpack :${process.env.WATCHER_PORT ?? "default"} \xB7 CDP :${process.env.CDP_PORT ?? "default"} (output streams below)`);
|
|
223
|
+
console.error(colorHumanMessage(`\u2192 extension quick relaunch \u2014 webpack :${process.env.WATCHER_PORT ?? "default"} \xB7 CDP :${process.env.CDP_PORT ?? "default"} (output streams below)`));
|
|
199
224
|
const result = await spawnScriptStreaming(liveScript, liveArgs, target);
|
|
200
225
|
if (result.output) {
|
|
201
226
|
try {
|