@deeeed/metamask-harness 0.13.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -4
- package/adapters/extension/inject.mjs +1 -0
- package/adapters/extension/launch-browser.cjs +15 -3
- package/adapters/extension/lib/extension-id.cjs +36 -0
- package/adapters/extension/live.sh +5 -3
- package/adapters/extension/readiness.mjs +48 -0
- package/adapters/extension/reattach.sh +163 -55
- package/adapters/extension/sidepanel-toggle.sh +96 -24
- package/adapters/extension/verify.sh +15 -4
- package/adapters/extension/wallet-fixture-state.cjs +6 -11
- package/adapters/mobile/open-device.sh +32 -5
- package/adapters/mobile/start-metro.sh +15 -1
- package/adapters/mobile/verify.sh +50 -13
- package/adapters/shared/log-tui.mjs +7 -3
- package/adapters/shared/resolve-slot-ports-core.mjs +14 -4
- package/dist/adapters/core/surface.js +1 -0
- package/dist/adapters/extension/runtime.js +14 -5
- package/dist/adapters/extension/surface.js +1 -0
- package/dist/adapters/mobile/provision.js +49 -4
- package/dist/adapters/mobile/surface.js +1 -0
- package/dist/adapters/slot-ports.js +11 -4
- package/dist/cli-commands.js +5 -4
- package/dist/cli.js +4 -0
- package/dist/commands/call.js +79 -6
- package/dist/commands/check.js +326 -0
- package/dist/commands/core-readiness.js +75 -0
- package/dist/commands/device-target.js +123 -13
- package/dist/commands/doctor.js +30 -18
- package/dist/commands/fixtures.js +2 -1
- package/dist/commands/launch/extension.js +105 -9
- package/dist/commands/launch/index.js +116 -15
- package/dist/commands/mobile-device-view.js +140 -0
- package/dist/commands/parse-args.js +4 -1
- package/dist/commands/recipe-quality.js +6 -2
- package/dist/commands/run-engine.js +90 -5
- package/dist/commands/run-report.js +115 -0
- package/dist/commands/run.js +115 -8
- package/dist/commands/self-test.js +1 -1
- package/dist/commands/shared.js +2 -1
- package/dist/commands/status-probe.js +9 -3
- package/dist/commands/status.js +45 -50
- package/dist/live-adapter-contract.js +5 -1
- package/dist/mm-harness-cli.js +68 -27
- package/dist/recipe-files.js +14 -0
- package/docs/CLI-SPEC.md +25 -0
- package/docs/architecture.md +2 -2
- package/docs/live-adapter-contract.md +1 -1
- package/docs/recipe-libraries.md +21 -18
- package/library/actions/extension/platform/cdp.mjs +7 -4
- package/library/recipes/{app-lifecycle-android-smoke.mobile.recipe.json → app/lifecycle.android-smoke.mobile.recipe.json} +1 -1
- package/library/recipes/{perps-performance.mobile.recipe.json → perps/performance.mobile.recipe.json} +1 -1
- package/library/recipes/perps/smoke.core.recipe.json +39 -0
- package/library/recipes/perps/smoke.extension.recipe.json +51 -0
- package/library/recipes/perps/smoke.mobile.recipe.json +51 -0
- package/library/recipes/{action-validation.extension.recipe.json → runner/action-validation.extension.recipe.json} +7 -7
- package/library/recipes/{action-validation.mobile.recipe.json → runner/action-validation.mobile.recipe.json} +7 -7
- package/package.json +1 -1
- /package/library/recipes/{perps-lifecycle.recipe.json → perps/lifecycle.recipe.json} +0 -0
- /package/library/recipes/{order-lifecycle.core.recipe.json → perps/order-lifecycle.core.recipe.json} +0 -0
- /package/library/recipes/{perps-performance-background-resume.mobile.recipe.json → perps/performance.background-resume.mobile.recipe.json} +0 -0
- /package/library/recipes/{perps-performance-cold-start.mobile.recipe.json → perps/performance.cold-start.mobile.recipe.json} +0 -0
- /package/library/recipes/{perps-performance-warm-start.mobile.recipe.json → perps/performance.warm-start.mobile.recipe.json} +0 -0
- /package/library/recipes/{read-markets.core.recipe.json → perps/read-markets.core.recipe.json} +0 -0
- /package/library/recipes/{trading-lifecycle.core.recipe.json → perps/trading-lifecycle.core.recipe.json} +0 -0
- /package/library/recipes/{smoke.extension.recipe.json → runner/smoke.extension.recipe.json} +0 -0
- /package/library/recipes/{smoke.mobile.recipe.json → runner/smoke.mobile.recipe.json} +0 -0
package/dist/commands/run.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { walletFixturePath } from "../paths.js";
|
|
4
|
+
import { color } from "../cli-color.js";
|
|
5
|
+
import { recipeRunning } from "../heal-bounds.js";
|
|
4
6
|
import { EXIT } from "./shared.js";
|
|
5
7
|
import {
|
|
6
8
|
optionFlag,
|
|
@@ -8,6 +10,7 @@ import {
|
|
|
8
10
|
requiredOption,
|
|
9
11
|
resolveAdapter,
|
|
10
12
|
runtimeOptionsFromCli,
|
|
13
|
+
isRecord,
|
|
11
14
|
usageError
|
|
12
15
|
} from "./parse-args.js";
|
|
13
16
|
import {
|
|
@@ -20,6 +23,9 @@ import {
|
|
|
20
23
|
} from "./run-engine.js";
|
|
21
24
|
import { handleListExecutables } from "./list-executables.js";
|
|
22
25
|
import { applyDeviceTargeting } from "./device-target.js";
|
|
26
|
+
import { getAdapterSurface } from "../adapters/surface.js";
|
|
27
|
+
import { coreDependencyBlock } from "./core-readiness.js";
|
|
28
|
+
import { writeRunReport } from "./run-report.js";
|
|
23
29
|
async function handleRun({ positional, options }) {
|
|
24
30
|
if (optionFlag(options, "list")) return handleListExecutables("run", options);
|
|
25
31
|
const targetRecipe = positional[0];
|
|
@@ -27,14 +33,15 @@ async function handleRun({ positional, options }) {
|
|
|
27
33
|
if (optionFlag(options, "plan")) return handleRunPlan(targetRecipe, options);
|
|
28
34
|
const { adapter, target } = resolveAdapter(options);
|
|
29
35
|
const json = optionFlag(options, "json");
|
|
36
|
+
getAdapterSurface(adapter).resolveSlotPorts(target);
|
|
30
37
|
const dtResult = applyDeviceTargeting("run", adapter, options, { gate: true, rerun: "" });
|
|
31
38
|
if ("code" in dtResult) {
|
|
32
39
|
return emitRunUsageError(json, adapter, targetRecipe, dtResult.code, dtResult.message);
|
|
33
40
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
if (recipeRunning(target)) return emitRunRecipeRunning(json);
|
|
42
|
+
if (optionString(options, "artifactsDir") === void 0 && runArgLooksLikeRecipeFile(targetRecipe)) {
|
|
43
|
+
requiredOption(options, "artifactsDir", "run requires --artifacts-dir <dir>.");
|
|
44
|
+
}
|
|
38
45
|
const validated = await validateRunRecipeStatic(targetRecipe, adapter, options);
|
|
39
46
|
if (validated.usageError) {
|
|
40
47
|
return emitRunUsageError(json, adapter, validated.recipeFile, validated.usageError.code, validated.usageError.message);
|
|
@@ -42,6 +49,14 @@ async function handleRun({ positional, options }) {
|
|
|
42
49
|
if (validated.errorCount > 0) {
|
|
43
50
|
return emitRunValidationError(json, adapter, validated.recipeFile, validated.findings, validated.errorCount);
|
|
44
51
|
}
|
|
52
|
+
const depsBlock = adapter === "core" && recipeUsesCoreController(validated.recipe, validated.librarySources) ? coreDependencyBlock(target) : null;
|
|
53
|
+
if (depsBlock) {
|
|
54
|
+
return emitRunUsageError(json, adapter, validated.recipeFile, depsBlock.code, depsBlock.message, depsBlock.userAction);
|
|
55
|
+
}
|
|
56
|
+
const artifactsDir = requiredOption(options, "artifactsDir", "run requires --artifacts-dir <dir>.");
|
|
57
|
+
const prepared = await prepareHeal(adapter, target, options, json);
|
|
58
|
+
if (typeof prepared === "number") return prepared;
|
|
59
|
+
const { state, heal } = prepared;
|
|
45
60
|
const librarySources = validated.librarySources;
|
|
46
61
|
const runtimeOptions = {
|
|
47
62
|
...runtimeOptionsFromCli(options),
|
|
@@ -58,6 +73,7 @@ async function handleRun({ positional, options }) {
|
|
|
58
73
|
state
|
|
59
74
|
);
|
|
60
75
|
if (violation !== null) return emitHealViolation(json, "run", result, violation, state, adapter);
|
|
76
|
+
const report = writeRunReport(result);
|
|
61
77
|
const exitCode = result.status === "pass" ? EXIT.ok : EXIT.runtime;
|
|
62
78
|
if (json) {
|
|
63
79
|
console.log(
|
|
@@ -70,6 +86,7 @@ async function handleRun({ positional, options }) {
|
|
|
70
86
|
exitCode,
|
|
71
87
|
recovered: state.recovered,
|
|
72
88
|
mutations: state.mutations,
|
|
89
|
+
reportPath: report.path,
|
|
73
90
|
result
|
|
74
91
|
},
|
|
75
92
|
null,
|
|
@@ -77,11 +94,88 @@ async function handleRun({ positional, options }) {
|
|
|
77
94
|
)
|
|
78
95
|
);
|
|
79
96
|
} else {
|
|
80
|
-
|
|
81
|
-
|
|
97
|
+
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
98
|
+
console.log(`${out(result.status === "pass" ? "ok" : "err", result.status.toUpperCase())} ${out("bold", "recipe run")} ${out("dim", `[${adapter}]`)}`);
|
|
99
|
+
if (report.preview.length > 0) {
|
|
100
|
+
console.log(out("label", "summary:"));
|
|
101
|
+
for (const line of report.preview) console.log(` ${formatPreviewLine(line, out)}`);
|
|
102
|
+
}
|
|
103
|
+
console.log(`${out("label", "report:")} ${out("path", report.path)}`);
|
|
104
|
+
console.log(`${out("label", "artifacts:")} ${out("path", result.artifactManifestPath)}`);
|
|
82
105
|
}
|
|
83
106
|
return exitCode;
|
|
84
107
|
}
|
|
108
|
+
function formatPreviewLine(line, out) {
|
|
109
|
+
const match = /^(PASS|FAIL)\s+(.+)$/u.exec(line);
|
|
110
|
+
if (!match) return line;
|
|
111
|
+
const status = match[1];
|
|
112
|
+
const rest = match[2];
|
|
113
|
+
return `${out(status === "PASS" ? "ok" : "err", status)} ${rest}`;
|
|
114
|
+
}
|
|
115
|
+
function recipeUsesCoreController(recipe, librarySources) {
|
|
116
|
+
if (objectUsesCoreController(recipe)) return true;
|
|
117
|
+
const refs = collectCallRefs(recipe);
|
|
118
|
+
if (refs.length === 0) return false;
|
|
119
|
+
const flows = loadFlowCatalogs(librarySources ?? []);
|
|
120
|
+
const visited = /* @__PURE__ */ new Set();
|
|
121
|
+
const visit = (ref) => {
|
|
122
|
+
if (visited.has(ref)) return false;
|
|
123
|
+
visited.add(ref);
|
|
124
|
+
const flow = flows.get(ref);
|
|
125
|
+
if (!flow) return false;
|
|
126
|
+
if (objectUsesCoreController(flow)) return true;
|
|
127
|
+
return collectCallRefs(flow).some(visit);
|
|
128
|
+
};
|
|
129
|
+
return refs.some(visit);
|
|
130
|
+
}
|
|
131
|
+
function objectUsesCoreController(value) {
|
|
132
|
+
if (Array.isArray(value)) return value.some(objectUsesCoreController);
|
|
133
|
+
if (!isRecord(value)) return false;
|
|
134
|
+
if (actionUsesCoreController(value.action)) return true;
|
|
135
|
+
return Object.values(value).some(objectUsesCoreController);
|
|
136
|
+
}
|
|
137
|
+
function collectCallRefs(value) {
|
|
138
|
+
const refs = [];
|
|
139
|
+
const visit = (node) => {
|
|
140
|
+
if (Array.isArray(node)) {
|
|
141
|
+
for (const item of node) visit(item);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (!isRecord(node)) return;
|
|
145
|
+
if (node.action === "call" && typeof node.ref === "string" && node.ref) refs.push(node.ref);
|
|
146
|
+
for (const child of Object.values(node)) visit(child);
|
|
147
|
+
};
|
|
148
|
+
visit(value);
|
|
149
|
+
return [...new Set(refs)];
|
|
150
|
+
}
|
|
151
|
+
function loadFlowCatalogs(librarySources) {
|
|
152
|
+
const flows = /* @__PURE__ */ new Map();
|
|
153
|
+
for (const source of librarySources) {
|
|
154
|
+
const flowsDir = path.join(source.root, "flows");
|
|
155
|
+
let entries;
|
|
156
|
+
try {
|
|
157
|
+
entries = fs.readdirSync(flowsDir);
|
|
158
|
+
} catch {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
for (const entry of entries) {
|
|
162
|
+
if (!entry.endsWith(".json")) continue;
|
|
163
|
+
try {
|
|
164
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(flowsDir, entry), "utf8"));
|
|
165
|
+
if (!isRecord(parsed) || !isRecord(parsed.flows)) continue;
|
|
166
|
+
for (const [id, flow] of Object.entries(parsed.flows)) {
|
|
167
|
+
if (!flows.has(id)) flows.set(id, flow);
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return flows;
|
|
175
|
+
}
|
|
176
|
+
function actionUsesCoreController(action) {
|
|
177
|
+
return typeof action === "string" && action.startsWith("metamask.perps.");
|
|
178
|
+
}
|
|
85
179
|
async function handleRunPlan(recipeArg, options) {
|
|
86
180
|
const json = optionFlag(options, "json");
|
|
87
181
|
const { adapter, target } = resolveAdapter(options);
|
|
@@ -174,17 +268,30 @@ function emitPlanUsageError(json, adapter, recipeFile, code, message) {
|
|
|
174
268
|
}
|
|
175
269
|
return EXIT.usage;
|
|
176
270
|
}
|
|
177
|
-
function
|
|
271
|
+
function runArgLooksLikeRecipeFile(value) {
|
|
272
|
+
return path.isAbsolute(value) || value.includes("/") || value.includes(path.sep) || value.endsWith(".json") || fs.existsSync(path.resolve(value));
|
|
273
|
+
}
|
|
274
|
+
function emitRunRecipeRunning(json) {
|
|
275
|
+
const message = "a recipe is currently running \u2014 refusing to start while another recipe executes.";
|
|
276
|
+
if (json) {
|
|
277
|
+
console.log(JSON.stringify({ schemaVersion: 1, status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message } }, null, 2));
|
|
278
|
+
} else {
|
|
279
|
+
console.error(`\u2717 mm-harness: ${message}`);
|
|
280
|
+
}
|
|
281
|
+
return EXIT.bounded;
|
|
282
|
+
}
|
|
283
|
+
function emitRunUsageError(json, adapter, recipeFile, code, message, userAction) {
|
|
178
284
|
if (json) {
|
|
179
285
|
console.log(
|
|
180
286
|
JSON.stringify(
|
|
181
|
-
{ schemaVersion: 1, command: "run", adapter, status: "fail", exitCode: EXIT.usage, recipe: recipeFile, error: { code, message } },
|
|
287
|
+
{ schemaVersion: 1, command: "run", adapter, status: "fail", exitCode: EXIT.usage, recipe: recipeFile, error: { code, message, ...userAction ? { userAction } : {} } },
|
|
182
288
|
null,
|
|
183
289
|
2
|
|
184
290
|
)
|
|
185
291
|
);
|
|
186
292
|
} else {
|
|
187
293
|
console.error(`\u2717 run: ${message}`);
|
|
294
|
+
if (userAction) console.error(` Next: ${userAction}`);
|
|
188
295
|
}
|
|
189
296
|
return EXIT.usage;
|
|
190
297
|
}
|
|
@@ -26,7 +26,7 @@ async function runSelfTest(options) {
|
|
|
26
26
|
const manifest = loadActionManifest(adapter);
|
|
27
27
|
const manifestValidation = await validateManifest(manifest);
|
|
28
28
|
const smokeRecipe = recipePath(
|
|
29
|
-
adapter === "mobile" ? "smoke.mobile.recipe.json" : "smoke.extension.recipe.json"
|
|
29
|
+
adapter === "mobile" ? "runner/smoke.mobile.recipe.json" : "runner/smoke.extension.recipe.json"
|
|
30
30
|
);
|
|
31
31
|
const artifactsDir = path.join(root, adapter);
|
|
32
32
|
const result = await runRecipe(adapter, smokeRecipe, artifactsDir, runnerDir, void 0, {
|
package/dist/commands/shared.js
CHANGED
|
@@ -47,7 +47,8 @@ function targetOf(options) {
|
|
|
47
47
|
}
|
|
48
48
|
const ADAPTER_TOKENS = ["mobile", "extension", "core"];
|
|
49
49
|
function resolveAdapter(options, target, hint) {
|
|
50
|
-
const
|
|
50
|
+
const platform = str(options, "platform");
|
|
51
|
+
const explicit = str(options, "adapter") ?? (platform === "ios" || platform === "android" ? "mobile" : platform);
|
|
51
52
|
if (explicit && ADAPTER_TOKENS.includes(explicit)) return explicit;
|
|
52
53
|
if (hint) return hint;
|
|
53
54
|
return detectAdapter(target);
|
|
@@ -38,15 +38,21 @@ function parseBridgeEntries(raw) {
|
|
|
38
38
|
...typeof e.agenticPresent === "boolean" ? { agenticPresent: e.agenticPresent } : {}
|
|
39
39
|
}));
|
|
40
40
|
}
|
|
41
|
-
function matchBridgeEntry(device, entries) {
|
|
41
|
+
function matchBridgeEntry(device, entries, devices) {
|
|
42
42
|
const byId = entries.find((e) => e.deviceName === device.id);
|
|
43
43
|
if (byId) return byId;
|
|
44
44
|
if (device.name) {
|
|
45
45
|
const byName = entries.find((e) => e.deviceName === device.name);
|
|
46
46
|
if (byName) return byName;
|
|
47
|
+
if (device.platform === "android") {
|
|
48
|
+
const model = device.name.replace(/_/gu, " ");
|
|
49
|
+
const byModel = entries.find((e) => e.deviceName === model || e.deviceName.startsWith(`${model} -`));
|
|
50
|
+
if (byModel) return byModel;
|
|
51
|
+
}
|
|
47
52
|
}
|
|
48
53
|
const platformEntries = entries.filter((e) => e.platform === device.platform);
|
|
49
|
-
|
|
54
|
+
const platformDevices = devices.filter((d) => d.platform === device.platform);
|
|
55
|
+
if (!device.name && platformEntries.length === 1 && platformDevices.length === 1) return platformEntries[0];
|
|
50
56
|
return null;
|
|
51
57
|
}
|
|
52
58
|
function probeMobileBridge(target) {
|
|
@@ -123,7 +129,7 @@ async function probeMobileLiveState(target, devices) {
|
|
|
123
129
|
const matched = /* @__PURE__ */ new Map();
|
|
124
130
|
const usedEntries = /* @__PURE__ */ new Set();
|
|
125
131
|
for (const device of devices) {
|
|
126
|
-
const entry = matchBridgeEntry(device, entries.filter((e) => !usedEntries.has(e)));
|
|
132
|
+
const entry = matchBridgeEntry(device, entries.filter((e) => !usedEntries.has(e)), devices);
|
|
127
133
|
if (entry) {
|
|
128
134
|
matched.set(device.id, entry);
|
|
129
135
|
usedEntries.add(entry);
|
package/dist/commands/status.js
CHANGED
|
@@ -2,30 +2,57 @@ import { color } from "../cli-color.js";
|
|
|
2
2
|
import { detectAdapter } from "../harness.js";
|
|
3
3
|
import { assertAdapter } from "../paths.js";
|
|
4
4
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
import {
|
|
6
|
+
applyDeviceTargeting
|
|
7
|
+
} from "./device-target.js";
|
|
8
|
+
import {
|
|
9
|
+
mobileDeviceView,
|
|
10
|
+
mobileDeviceLiveView,
|
|
11
|
+
nextForLive,
|
|
12
|
+
renderAndroidPortHints,
|
|
13
|
+
renderMobileDeviceList,
|
|
14
|
+
renderAdditionalReachableDevices,
|
|
15
|
+
renderMobileLiveBlock
|
|
16
|
+
} from "./mobile-device-view.js";
|
|
8
17
|
import { ADAPTER_DETECT_NEXT, EXIT, usageOut } from "./shared.js";
|
|
9
18
|
import { optionFlag, targetPath } from "./parse-args.js";
|
|
10
19
|
async function handleStatus({ options }) {
|
|
11
20
|
const target = targetPath(options);
|
|
12
21
|
const json = optionFlag(options, "json");
|
|
13
22
|
const fast = optionFlag(options, "fast");
|
|
23
|
+
const allDevices = optionFlag(options, "allDevices");
|
|
14
24
|
const adapter = detectAdapter(target);
|
|
15
25
|
if (!adapter) {
|
|
16
26
|
return usageOut(json, "status", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
17
27
|
}
|
|
18
28
|
assertAdapter(adapter);
|
|
19
|
-
const
|
|
20
|
-
|
|
29
|
+
const surface = getAdapterSurface(adapter);
|
|
30
|
+
surface.resolveSlotPorts(target);
|
|
31
|
+
if (adapter === "mobile") {
|
|
32
|
+
const dtResult = applyDeviceTargeting("status", adapter, options, { gate: false, rerun: "" });
|
|
33
|
+
if ("code" in dtResult) {
|
|
34
|
+
return usageOut(json, "status", dtResult.message, "mm-harness status --device <id> or --all-devices");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const next = surface.hints.relaunch;
|
|
38
|
+
const deviceView = adapter === "mobile" ? mobileDeviceView(allDevices) : null;
|
|
39
|
+
const devices = deviceView?.devices ?? [];
|
|
21
40
|
if (json) {
|
|
22
|
-
const doProbe = !fast && adapter === "mobile" &&
|
|
41
|
+
const doProbe = !fast && adapter === "mobile" && deviceView !== null && deviceView.allConnectedDevices.length > 0;
|
|
23
42
|
if (doProbe) {
|
|
24
|
-
const
|
|
25
|
-
const devicesWithLive = devices.map((d) => mergeDeviceLive(d, liveMap.get(d.id)));
|
|
43
|
+
const liveView = await mobileDeviceLiveView(target, deviceView);
|
|
26
44
|
console.log(
|
|
27
45
|
JSON.stringify(
|
|
28
|
-
{
|
|
46
|
+
{
|
|
47
|
+
schemaVersion: 1,
|
|
48
|
+
command: "status",
|
|
49
|
+
adapter,
|
|
50
|
+
target,
|
|
51
|
+
devices: liveView.devicesWithLive,
|
|
52
|
+
...liveView.additionalReachableDevices.length > 0 ? { additionalReachableDevices: liveView.additionalReachableDevices } : {},
|
|
53
|
+
...liveView.androidPortHints.length > 0 ? { androidPortHints: liveView.androidPortHints } : {},
|
|
54
|
+
next: nextForLive(next, liveView.liveMap)
|
|
55
|
+
},
|
|
29
56
|
null,
|
|
30
57
|
2
|
|
31
58
|
)
|
|
@@ -39,50 +66,18 @@ async function handleStatus({ options }) {
|
|
|
39
66
|
}
|
|
40
67
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
41
68
|
console.log(`${out("label", "status")} ${out("bold", adapter)} ${out("dim", target)}`);
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
69
|
+
if (deviceView) renderMobileDeviceList(deviceView, out);
|
|
70
|
+
if (!fast && adapter === "mobile" && deviceView !== null && deviceView.allConnectedDevices.length > 0) {
|
|
71
|
+
const liveView = await mobileDeviceLiveView(target, deviceView);
|
|
72
|
+
if (liveView.additionalReachableDevices.length > 0) renderAdditionalReachableDevices(liveView.additionalReachableDevices, out);
|
|
73
|
+
renderMobileLiveBlock(devices, liveView.liveMap, out);
|
|
74
|
+
renderAndroidPortHints(liveView.androidPortHints, out);
|
|
75
|
+
console.log(`${out("label", "Next:")} ${out("cmd", nextForLive(next, liveView.liveMap))}`);
|
|
76
|
+
} else {
|
|
77
|
+
console.log(`${out("label", "Next:")} ${out("cmd", next)}`);
|
|
47
78
|
}
|
|
48
79
|
return EXIT.ok;
|
|
49
80
|
}
|
|
50
|
-
function mergeDeviceLive(device, live) {
|
|
51
|
-
if (!live) return device;
|
|
52
|
-
return {
|
|
53
|
-
...device,
|
|
54
|
-
fixtureStatus: live.fixtureStatus,
|
|
55
|
-
...live.liveState !== void 0 ? { liveState: live.liveState } : {},
|
|
56
|
-
...live.currentScreen !== void 0 ? { currentScreen: live.currentScreen } : {},
|
|
57
|
-
...live.walletState !== void 0 ? { walletState: live.walletState } : {},
|
|
58
|
-
...live.selectedAccount !== void 0 ? { selectedAccount: live.selectedAccount } : {}
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
function renderLiveBlock(devices, liveMap, out) {
|
|
62
|
-
console.log(`${out("label", "live:")}`);
|
|
63
|
-
for (const device of devices) {
|
|
64
|
-
const live = liveMap.get(device.id);
|
|
65
|
-
const prefix = ` ${device.platform} ${out("dim", device.id)}${device.name ? ` (${device.name})` : ""}`;
|
|
66
|
-
if (!live || live.liveState === "no-bridge") {
|
|
67
|
-
console.log(`${prefix}: ${out("dim", "(no-bridge)")}`);
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
if (live.liveState === "bridge-absent") {
|
|
71
|
-
console.log(`${prefix}: ${out("warn", "(bridge-absent \u2014 app attached, build lacks __AGENTIC__; rebuild/reinstall a dev build)")}`);
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
const parts = [];
|
|
75
|
-
if (live.currentScreen !== void 0) parts.push(`screen=${out("cmd", live.currentScreen)}`);
|
|
76
|
-
if (live.walletState !== void 0) parts.push(`wallet=${out(live.walletState === "unlocked" ? "ok" : "warn", live.walletState)}`);
|
|
77
|
-
if (live.selectedAccount !== void 0) {
|
|
78
|
-
parts.push(`account=${live.selectedAccount.label} ${out("dim", `(${live.selectedAccount.address})`)}`);
|
|
79
|
-
}
|
|
80
|
-
if (live.fixtureStatus !== void 0) {
|
|
81
|
-
parts.push(`fixture=${out(live.fixtureStatus === "READY" ? "ok" : "warn", live.fixtureStatus)}`);
|
|
82
|
-
}
|
|
83
|
-
console.log(`${prefix}: ${parts.join(" ")}`);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
81
|
export {
|
|
87
82
|
handleStatus
|
|
88
83
|
};
|
|
@@ -4,6 +4,7 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { resolveRequiredLocalProtocolRoot, runnerDir } from "./paths.js";
|
|
7
|
+
import { corePnpNodeOptions } from "./commands/core-readiness.js";
|
|
7
8
|
function actionFileStem(action) {
|
|
8
9
|
return String(action).replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
9
10
|
}
|
|
@@ -199,7 +200,10 @@ async function platformAdapterEnv(platform, projectRoot, tempDir) {
|
|
|
199
200
|
`${JSON.stringify({ compilerOptions: { baseUrl: projectRoot, paths } }, null, 2)}
|
|
200
201
|
`
|
|
201
202
|
);
|
|
202
|
-
return {
|
|
203
|
+
return {
|
|
204
|
+
TSX_TSCONFIG_PATH: tsconfigPath,
|
|
205
|
+
NODE_OPTIONS: corePnpNodeOptions(projectRoot, process.env.NODE_OPTIONS)
|
|
206
|
+
};
|
|
203
207
|
}
|
|
204
208
|
async function runLiveAdapterScript({ platform, action, node, context }) {
|
|
205
209
|
const script = await resolveLiveAdapter(platform, action);
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -6,6 +6,8 @@ import { Command } from "commander";
|
|
|
6
6
|
import { color } from "./cli-color.js";
|
|
7
7
|
import { handleUpdate, maybeNudge } from "./commands/update.js";
|
|
8
8
|
import { handleCallHelp } from "./commands/call.js";
|
|
9
|
+
import { getAdapterSurface } from "./adapters/surface.js";
|
|
10
|
+
import { detectAdapter } from "./harness.js";
|
|
9
11
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
12
|
globalThis.__MM_HARNESS_WRAPPER__ = true;
|
|
11
13
|
const { main: recipeMain } = await import("./cli.js");
|
|
@@ -24,9 +26,13 @@ const REAL = [
|
|
|
24
26
|
--target <path> Checkout path (default: cwd)
|
|
25
27
|
--json Machine-readable envelope { adapter, target, devices[], next }
|
|
26
28
|
--fast Skip all live-state probes (instant output, safe for scripts)
|
|
29
|
+
--all-devices Mobile only: show every connected device instead of the slot-scoped target
|
|
27
30
|
|
|
28
31
|
Human output: static device list prints immediately; live-state lines append
|
|
29
32
|
after the ~2s probe window (screen, wallet, account, fixture per device).
|
|
33
|
+
By default mobile status shows the configured target only; --device <id>
|
|
34
|
+
switches the active target for that command. Other devices that answer on the
|
|
35
|
+
same Metro are reported as additional reachable targets.
|
|
30
36
|
|
|
31
37
|
The devices[] section carries per device:
|
|
32
38
|
{ platform, id, name, state, selected } always present
|
|
@@ -90,15 +96,16 @@ Example:
|
|
|
90
96
|
name: "call",
|
|
91
97
|
summary: "Run one action in isolation as a one-node recipe through the real engine path (fuzzy short names; --arg k=v; same trace/evidence as run).",
|
|
92
98
|
example: "mm-harness call ensure_unlocked",
|
|
93
|
-
helpText: `mm-harness call <action> [--arg k=v ...] [flags]
|
|
99
|
+
helpText: `mm-harness call <action> [key=value ...] [--arg k=v ...] [flags]
|
|
94
100
|
|
|
95
101
|
Run one action in isolation as a one-node recipe through the real engine path.
|
|
96
102
|
Fuzzy short-name: 'ensure_unlocked' resolves to 'metamask.wallet.ensure_unlocked'
|
|
97
|
-
if unique; ambiguous = exit 2.
|
|
98
|
-
with
|
|
103
|
+
if unique; ambiguous = exit 2. Pass action fields as key=value shorthand or
|
|
104
|
+
with --arg k=v. Actions differ per adapter \u2014 list this checkout's with:
|
|
105
|
+
mm-harness actions.
|
|
99
106
|
|
|
100
107
|
--list List everything invocable for the adapter (actions + flows); no <action> needed
|
|
101
|
-
--arg k=v Action field value (repeatable)
|
|
108
|
+
--arg k=v Action field value (repeatable; equivalent to key=value shorthand)
|
|
102
109
|
--device <udid|serial|name> Mobile only: target this device (env: IOS_SIMULATOR / ADB_SERIAL). Without it, >1 connected mobile device fails fast and lists them.
|
|
103
110
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
104
111
|
--target <path> Checkout path (default: cwd)
|
|
@@ -110,7 +117,8 @@ Example:
|
|
|
110
117
|
|
|
111
118
|
Example (real actions; run mm-harness actions for this checkout's full set):
|
|
112
119
|
mm-harness call ensure_unlocked --adapter extension # a wallet action (extension/mobile)
|
|
113
|
-
mm-harness call
|
|
120
|
+
mm-harness call navigate page=perps --adapter mobile
|
|
121
|
+
mm-harness call command cmd="echo hi" --adapter core # the universal action (all adapters)`
|
|
114
122
|
},
|
|
115
123
|
{
|
|
116
124
|
name: "flows",
|
|
@@ -166,6 +174,7 @@ Example:
|
|
|
166
174
|
--expect-live Exit 0 iff the runtime is live (extension: watcher+CDP; mobile: Metro+bridge; core: deps), non-zero + teaching escape otherwise
|
|
167
175
|
--cdp-port <port> Extension CDP port for the liveness probe (env: CDP_PORT / RECIPE_CDP_PORT)
|
|
168
176
|
--device <udid|serial|name> Mobile only: target this device (env: IOS_SIMULATOR / ADB_SERIAL). doctor reports the connected devices; it never gates on ambiguity.
|
|
177
|
+
--all-devices Mobile only: show every connected device instead of the slot-scoped target
|
|
169
178
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
170
179
|
--target <path> Checkout path (default: cwd)
|
|
171
180
|
--runtime-dir <dir> Runtime dir containing agentic-runtime.json (relative to target)
|
|
@@ -177,6 +186,27 @@ Example:
|
|
|
177
186
|
mm-harness doctor --adapter extension --target /path/to/checkout --cdp-port 6662 --expect-live
|
|
178
187
|
mm-harness doctor --adapter mobile --target /path/to/checkout`
|
|
179
188
|
},
|
|
189
|
+
{
|
|
190
|
+
name: "check",
|
|
191
|
+
summary: "Run bounded repo-local checks for an active git diff and write validation artifacts.",
|
|
192
|
+
example: "mm-harness check diff --profile fast",
|
|
193
|
+
helpText: `mm-harness check diff [flags]
|
|
194
|
+
|
|
195
|
+
Run bounded repo-local checks for the active git diff. Fast profile runs changed-file
|
|
196
|
+
ESLint, Prettier, and changed test files. Full profile also runs repo typecheck
|
|
197
|
+
when available. Does not launch an app or run a recipe.
|
|
198
|
+
|
|
199
|
+
--target <path> Checkout path (default: cwd)
|
|
200
|
+
--adapter <mobile|extension|core> Adapter label (auto-detected when possible)
|
|
201
|
+
--base <ref> Diff base (default: PR base, then remote HEAD, then repo fallback)
|
|
202
|
+
--profile <fast|full> Validation depth (default: fast)
|
|
203
|
+
--artifacts-dir <dir> Write validation-summary.json/.md and per-check logs
|
|
204
|
+
--json Machine-readable envelope
|
|
205
|
+
|
|
206
|
+
Example:
|
|
207
|
+
mm-harness check diff --profile fast --artifacts-dir artifacts/validation
|
|
208
|
+
mm-harness check diff --profile full --json`
|
|
209
|
+
},
|
|
180
210
|
{
|
|
181
211
|
name: "recipe-quality",
|
|
182
212
|
summary: "Build the recipe-quality artifact from a compact verdict JSON (validates before writing).",
|
|
@@ -186,7 +216,12 @@ Example:
|
|
|
186
216
|
Build artifacts/recipe-quality.json from the compact fields a recipe-quality pass
|
|
187
217
|
produces (verdict + reasons + optional guidance/dimensions/findings/delta/training).
|
|
188
218
|
The artifact is validated against the RecipeQualityArtifact schema before it is
|
|
189
|
-
written \u2014 an invalid input never reaches disk.
|
|
219
|
+
written \u2014 an invalid input never reaches disk. Shorthand without "build" is
|
|
220
|
+
also accepted when --input/--output are present.
|
|
221
|
+
|
|
222
|
+
This CLI does not run the recipe critique itself; use the fs-recipe-quality
|
|
223
|
+
skill to judge recipe/evidence quality, then use this command to build the
|
|
224
|
+
canonical artifact from that compact verdict.
|
|
190
225
|
|
|
191
226
|
--input <compact.json> Compact verdict JSON: { "verdict": "pass|warn|fail", "reasons": [..],
|
|
192
227
|
"betterVersionGuidance"?: [..], "dimensions"?: {..}, "trainingFields"?: {..}, \u2026 }
|
|
@@ -197,6 +232,7 @@ Example:
|
|
|
197
232
|
|
|
198
233
|
Example:
|
|
199
234
|
mm-harness recipe-quality build --input compact.json --output artifacts/recipe-quality.json
|
|
235
|
+
mm-harness recipe-quality --input compact.json --output artifacts/recipe-quality.json
|
|
200
236
|
mm-harness recipe-quality build --input compact.json --output artifacts/recipe-quality.json --json`
|
|
201
237
|
},
|
|
202
238
|
{
|
|
@@ -295,7 +331,7 @@ Example:
|
|
|
295
331
|
},
|
|
296
332
|
{
|
|
297
333
|
name: "launch",
|
|
298
|
-
summary: "Launch the app
|
|
334
|
+
summary: "Launch the app and adapter dev server, auto-ensuring the runtime overlay first. Mobile: ios|android required.",
|
|
299
335
|
example: "mm-harness launch ios",
|
|
300
336
|
helpText: `mm-harness launch [ios|android] [flags]
|
|
301
337
|
|
|
@@ -310,7 +346,7 @@ Example:
|
|
|
310
346
|
--runway Post-launch runway check (mobile only; teaching error elsewhere)
|
|
311
347
|
--watch Persistent webpack watcher then relaunch (extension only)
|
|
312
348
|
--sidepanel | --fullscreen Extension display mode (default --fullscreen)
|
|
313
|
-
--url <dapp-url> Open a dapp
|
|
349
|
+
--url <dapp-url> Open a dapp beside the sidepanel (default: MetaMask test dapp)
|
|
314
350
|
--heal <off|infra-only|auto> Healing policy (default auto); bounds always enforced
|
|
315
351
|
--device <udid|name> Target simulator/device (env: IOS_SIMULATOR / ADB_SERIAL)
|
|
316
352
|
--cdp-port <port> Extension CDP port (env: CDP_PORT / RECIPE_CDP_PORT)
|
|
@@ -422,14 +458,6 @@ Example:
|
|
|
422
458
|
mm-harness fixtures finalize --fixture wallet-fixture.json --state fixture-state.json --cdp-port 6661 --extension-dir dist/chrome`
|
|
423
459
|
}
|
|
424
460
|
];
|
|
425
|
-
const RETIRED_INTERNAL = [
|
|
426
|
-
"runtime-health",
|
|
427
|
-
"runtime-decision",
|
|
428
|
-
"runtime-launch",
|
|
429
|
-
"resolve-extension",
|
|
430
|
-
"ensure-ready",
|
|
431
|
-
"self-test"
|
|
432
|
-
];
|
|
433
461
|
const RETIRED = [
|
|
434
462
|
{
|
|
435
463
|
name: "live",
|
|
@@ -443,14 +471,7 @@ Replacement: mm-harness launch --verify (install overlay \u2192 launch \u2192 CD
|
|
|
443
471
|
|
|
444
472
|
Replacement: mm-harness actions --raw (works now \u2014 dumps the raw action registry JSON,
|
|
445
473
|
identical to the old \`manifest --json\`). Manifest validation moved into doctor / run --plan.`
|
|
446
|
-
}
|
|
447
|
-
...RETIRED_INTERNAL.map((name) => ({
|
|
448
|
-
name,
|
|
449
|
-
message: `mm-harness ${name} is retired (exit 2).
|
|
450
|
-
|
|
451
|
-
It is internal now \u2014 its logic lives inside doctor / launch / verify self-healing.
|
|
452
|
-
Use: mm-harness doctor`
|
|
453
|
-
}))
|
|
474
|
+
}
|
|
454
475
|
];
|
|
455
476
|
const HELP_GROUPS = [
|
|
456
477
|
{
|
|
@@ -466,7 +487,7 @@ const HELP_GROUPS = [
|
|
|
466
487
|
{
|
|
467
488
|
title: "PROVE",
|
|
468
489
|
blurb: "run recipes and inspect readiness",
|
|
469
|
-
commands: ["run", "doctor", "recipe-quality"]
|
|
490
|
+
commands: ["run", "doctor", "check", "recipe-quality"]
|
|
470
491
|
},
|
|
471
492
|
{
|
|
472
493
|
title: "RUNTIME OVERLAY",
|
|
@@ -484,6 +505,14 @@ function commandMeta(name) {
|
|
|
484
505
|
if (real) return { summary: real.summary, example: real.example, planned: false };
|
|
485
506
|
return { summary: "", example: "", planned: false };
|
|
486
507
|
}
|
|
508
|
+
function adapterFromRuntimeContext(ctx) {
|
|
509
|
+
const raw = typeof ctx.platform === "string" ? ctx.platform : void 0;
|
|
510
|
+
if (!raw) return void 0;
|
|
511
|
+
if (raw === "mobile" || raw === "ios" || raw === "android") return "mobile";
|
|
512
|
+
if (raw === "extension" || raw === "chrome-extension") return "extension";
|
|
513
|
+
if (raw === "core") return "core";
|
|
514
|
+
return void 0;
|
|
515
|
+
}
|
|
487
516
|
function detectedSlotLine(out) {
|
|
488
517
|
const ctxPath = path.join(
|
|
489
518
|
process.cwd(),
|
|
@@ -492,10 +521,14 @@ function detectedSlotLine(out) {
|
|
|
492
521
|
);
|
|
493
522
|
try {
|
|
494
523
|
const ctx = JSON.parse(fs.readFileSync(ctxPath, "utf8"));
|
|
524
|
+
const adapter = adapterFromRuntimeContext(ctx) ?? detectAdapter(process.cwd());
|
|
495
525
|
const parts = [];
|
|
496
526
|
if (ctx.slotId) parts.push(`slot ${out("ok", String(ctx.slotId))}`);
|
|
497
527
|
if (ctx.simulator) parts.push(`device ${out("ok", String(ctx.simulator))}`);
|
|
498
|
-
|
|
528
|
+
const devServerPort = ctx.watcherPort ?? ctx.devServerPort ?? ctx.metroPort;
|
|
529
|
+
if (adapter && adapter !== "core" && devServerPort) {
|
|
530
|
+
parts.push(`${getAdapterSurface(adapter).devServer.label} :${out("ok", String(devServerPort))}`);
|
|
531
|
+
}
|
|
499
532
|
if (ctx.gitBranch) parts.push(`branch ${out("info", String(ctx.gitBranch))}`);
|
|
500
533
|
if (parts.length === 0) return null;
|
|
501
534
|
return `${out("label", "SLOT")} \u2014 this checkout is a prepared slot: ${parts.join(" \xB7 ")}`;
|
|
@@ -580,7 +613,15 @@ for (const command of REAL) {
|
|
|
580
613
|
process.exit(await delegate(argv));
|
|
581
614
|
});
|
|
582
615
|
}
|
|
583
|
-
const HIDDEN = [
|
|
616
|
+
const HIDDEN = [
|
|
617
|
+
"completion-candidates",
|
|
618
|
+
"runtime-health",
|
|
619
|
+
"runtime-decision",
|
|
620
|
+
"runtime-launch",
|
|
621
|
+
"resolve-extension",
|
|
622
|
+
"ensure-ready",
|
|
623
|
+
"self-test"
|
|
624
|
+
];
|
|
584
625
|
for (const name of HIDDEN) {
|
|
585
626
|
program.command(name, { hidden: true }).allowUnknownOption().helpOption(false).argument("[args...]").action(async () => {
|
|
586
627
|
process.exit(await delegate(rawArgv));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
function listRecipeFiles(root, base = "") {
|
|
4
|
+
const out = [];
|
|
5
|
+
for (const entry of fs.readdirSync(path.join(root, base), { withFileTypes: true })) {
|
|
6
|
+
const rel = path.join(base, entry.name);
|
|
7
|
+
if (entry.isDirectory()) out.push(...listRecipeFiles(root, rel));
|
|
8
|
+
else if (entry.isFile() && entry.name.endsWith(".recipe.json")) out.push(rel);
|
|
9
|
+
}
|
|
10
|
+
return out;
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
listRecipeFiles
|
|
14
|
+
};
|