@deeeed/metamask-harness 0.14.0 → 0.14.2
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 +31 -0
- 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 +11 -0
- package/adapters/extension/wallet-fixture-state.cjs +6 -11
- package/adapters/mobile/open-device.sh +32 -5
- 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.js +4 -0
- package/dist/commands/call.js +19 -2
- package/dist/commands/check.js +326 -0
- package/dist/commands/core-readiness.js +75 -0
- package/dist/commands/device-target.js +113 -10
- package/dist/commands/doctor.js +30 -18
- 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 +28 -2
- package/dist/commands/run-report.js +115 -0
- package/dist/commands/run.js +113 -8
- package/dist/commands/shared.js +2 -1
- package/dist/commands/status-probe.js +9 -3
- package/dist/commands/status.js +40 -60
- package/dist/live-adapter-contract.js +5 -1
- package/dist/mm-harness-cli.js +61 -22
- package/docs/CLI-SPEC.md +25 -0
- package/library/actions/extension/platform/cdp.mjs +7 -4
- package/package.json +4 -4
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isRecord } from "./parse-args.js";
|
|
4
|
+
function writeRunReport(result) {
|
|
5
|
+
const summaryPath = String(result.summaryPath);
|
|
6
|
+
const tracePath = String(result.tracePath);
|
|
7
|
+
const artifactManifestPath = String(result.artifactManifestPath);
|
|
8
|
+
const artifactsDir = path.dirname(summaryPath);
|
|
9
|
+
const summary = JSON.parse(fs.readFileSync(summaryPath, "utf8"));
|
|
10
|
+
const trace = JSON.parse(fs.readFileSync(tracePath, "utf8"));
|
|
11
|
+
const entries = Array.isArray(trace.entries) ? trace.entries : [];
|
|
12
|
+
const lines = renderRunReport(summary, entries);
|
|
13
|
+
const reportPath = path.join(artifactsDir, "report.md");
|
|
14
|
+
fs.writeFileSync(reportPath, `${lines.join("\n")}
|
|
15
|
+
`);
|
|
16
|
+
indexRunReportArtifact(artifactManifestPath);
|
|
17
|
+
return {
|
|
18
|
+
path: reportPath,
|
|
19
|
+
preview: lines.filter((line) => line.startsWith("- ")).slice(0, 6).map((line) => line.slice(2))
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function renderRunReport(summary, entries) {
|
|
23
|
+
const lines = [
|
|
24
|
+
"# MetaMask Recipe Run",
|
|
25
|
+
"",
|
|
26
|
+
`Status: ${String(summary.status ?? "unknown")}`,
|
|
27
|
+
`Duration: ${formatDuration(Number(summary.durationMs ?? 0))}`,
|
|
28
|
+
`Nodes: ${Number(summary.passed ?? 0)}/${Number(summary.total ?? entries.length)} passed`,
|
|
29
|
+
"",
|
|
30
|
+
"## Steps"
|
|
31
|
+
];
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
if (!isRecord(entry)) continue;
|
|
34
|
+
const mark = entry.ok === false ? "FAIL" : "PASS";
|
|
35
|
+
const nodeId = String(entry.nodeId ?? "(node)");
|
|
36
|
+
const action = String(entry.action ?? "(action)");
|
|
37
|
+
const duration = formatDuration(Number(entry.durationMs ?? 0));
|
|
38
|
+
const detail = summarizeNodeOutput(entry.output);
|
|
39
|
+
lines.push(`- ${mark} ${nodeId} (${action}, ${duration})${detail ? `: ${detail}` : ""}`);
|
|
40
|
+
}
|
|
41
|
+
return lines;
|
|
42
|
+
}
|
|
43
|
+
function summarizeNodeOutput(output) {
|
|
44
|
+
if (!isRecord(output)) return "";
|
|
45
|
+
const parts = [];
|
|
46
|
+
const preferredKeys = [
|
|
47
|
+
"platform",
|
|
48
|
+
"screen",
|
|
49
|
+
"route",
|
|
50
|
+
"page",
|
|
51
|
+
"network",
|
|
52
|
+
"account",
|
|
53
|
+
"count",
|
|
54
|
+
"matchingCount",
|
|
55
|
+
"proofPath",
|
|
56
|
+
"screenshot",
|
|
57
|
+
"path"
|
|
58
|
+
];
|
|
59
|
+
for (const key of preferredKeys) addPart(parts, labelFor(key), summarizeValue(output[key]));
|
|
60
|
+
const accountState = isRecord(output.accountState) ? output.accountState : void 0;
|
|
61
|
+
if (accountState) {
|
|
62
|
+
addPart(parts, "totalBalance", accountState.totalBalance);
|
|
63
|
+
addPart(parts, "spendable", accountState.spendableBalance);
|
|
64
|
+
addPart(parts, "marginUsed", accountState.marginUsed);
|
|
65
|
+
addPart(parts, "unrealizedPnl", accountState.unrealizedPnl);
|
|
66
|
+
}
|
|
67
|
+
if (parts.length === 0) {
|
|
68
|
+
for (const [key, value] of Object.entries(output)) {
|
|
69
|
+
if (parts.length >= 5) break;
|
|
70
|
+
if (key === "liveAdapter" || key === "artifacts") continue;
|
|
71
|
+
addPart(parts, key, summarizeValue(value));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return parts.join(", ");
|
|
75
|
+
}
|
|
76
|
+
function labelFor(key) {
|
|
77
|
+
return key === "matchingCount" ? "matching" : key === "proofPath" ? "proof" : key;
|
|
78
|
+
}
|
|
79
|
+
function summarizeValue(value) {
|
|
80
|
+
if (typeof value === "string") return shortenAddress(value);
|
|
81
|
+
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
82
|
+
if (Array.isArray(value)) return `${value.length} item${value.length === 1 ? "" : "s"}`;
|
|
83
|
+
return void 0;
|
|
84
|
+
}
|
|
85
|
+
function addPart(parts, label, value) {
|
|
86
|
+
if (value === void 0 || value === null || value === "") return;
|
|
87
|
+
parts.push(`${label}=${String(value)}`);
|
|
88
|
+
}
|
|
89
|
+
function shortenAddress(value) {
|
|
90
|
+
return /^0x[a-fA-F0-9]{40}$/u.test(value) ? `${value.slice(0, 6)}...${value.slice(-4)}` : value;
|
|
91
|
+
}
|
|
92
|
+
function formatDuration(ms) {
|
|
93
|
+
if (!Number.isFinite(ms) || ms <= 0) return "0ms";
|
|
94
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
95
|
+
return `${(ms / 1e3).toFixed(ms < 1e4 ? 1 : 0)}s`;
|
|
96
|
+
}
|
|
97
|
+
function indexRunReportArtifact(artifactManifestPath) {
|
|
98
|
+
let manifest;
|
|
99
|
+
try {
|
|
100
|
+
manifest = JSON.parse(fs.readFileSync(artifactManifestPath, "utf8"));
|
|
101
|
+
} catch {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!isRecord(manifest)) return;
|
|
105
|
+
const artifacts = Array.isArray(manifest.artifacts) ? manifest.artifacts : [];
|
|
106
|
+
manifest.artifacts = [
|
|
107
|
+
...artifacts.filter((artifact) => !isRecord(artifact) || artifact.path !== "report.md"),
|
|
108
|
+
{ path: "report.md", type: "report", label: "Human run report", category: "system" }
|
|
109
|
+
];
|
|
110
|
+
fs.writeFileSync(artifactManifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
111
|
+
`);
|
|
112
|
+
}
|
|
113
|
+
export {
|
|
114
|
+
writeRunReport
|
|
115
|
+
};
|
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 {
|
|
@@ -21,6 +24,8 @@ import {
|
|
|
21
24
|
import { handleListExecutables } from "./list-executables.js";
|
|
22
25
|
import { applyDeviceTargeting } from "./device-target.js";
|
|
23
26
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
27
|
+
import { coreDependencyBlock } from "./core-readiness.js";
|
|
28
|
+
import { writeRunReport } from "./run-report.js";
|
|
24
29
|
async function handleRun({ positional, options }) {
|
|
25
30
|
if (optionFlag(options, "list")) return handleListExecutables("run", options);
|
|
26
31
|
const targetRecipe = positional[0];
|
|
@@ -33,10 +38,10 @@ async function handleRun({ positional, options }) {
|
|
|
33
38
|
if ("code" in dtResult) {
|
|
34
39
|
return emitRunUsageError(json, adapter, targetRecipe, dtResult.code, dtResult.message);
|
|
35
40
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
+
}
|
|
40
45
|
const validated = await validateRunRecipeStatic(targetRecipe, adapter, options);
|
|
41
46
|
if (validated.usageError) {
|
|
42
47
|
return emitRunUsageError(json, adapter, validated.recipeFile, validated.usageError.code, validated.usageError.message);
|
|
@@ -44,6 +49,14 @@ async function handleRun({ positional, options }) {
|
|
|
44
49
|
if (validated.errorCount > 0) {
|
|
45
50
|
return emitRunValidationError(json, adapter, validated.recipeFile, validated.findings, validated.errorCount);
|
|
46
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;
|
|
47
60
|
const librarySources = validated.librarySources;
|
|
48
61
|
const runtimeOptions = {
|
|
49
62
|
...runtimeOptionsFromCli(options),
|
|
@@ -60,6 +73,7 @@ async function handleRun({ positional, options }) {
|
|
|
60
73
|
state
|
|
61
74
|
);
|
|
62
75
|
if (violation !== null) return emitHealViolation(json, "run", result, violation, state, adapter);
|
|
76
|
+
const report = writeRunReport(result);
|
|
63
77
|
const exitCode = result.status === "pass" ? EXIT.ok : EXIT.runtime;
|
|
64
78
|
if (json) {
|
|
65
79
|
console.log(
|
|
@@ -72,6 +86,7 @@ async function handleRun({ positional, options }) {
|
|
|
72
86
|
exitCode,
|
|
73
87
|
recovered: state.recovered,
|
|
74
88
|
mutations: state.mutations,
|
|
89
|
+
reportPath: report.path,
|
|
75
90
|
result
|
|
76
91
|
},
|
|
77
92
|
null,
|
|
@@ -79,11 +94,88 @@ async function handleRun({ positional, options }) {
|
|
|
79
94
|
)
|
|
80
95
|
);
|
|
81
96
|
} else {
|
|
82
|
-
|
|
83
|
-
|
|
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)}`);
|
|
84
105
|
}
|
|
85
106
|
return exitCode;
|
|
86
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
|
+
}
|
|
87
179
|
async function handleRunPlan(recipeArg, options) {
|
|
88
180
|
const json = optionFlag(options, "json");
|
|
89
181
|
const { adapter, target } = resolveAdapter(options);
|
|
@@ -176,17 +268,30 @@ function emitPlanUsageError(json, adapter, recipeFile, code, message) {
|
|
|
176
268
|
}
|
|
177
269
|
return EXIT.usage;
|
|
178
270
|
}
|
|
179
|
-
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) {
|
|
180
284
|
if (json) {
|
|
181
285
|
console.log(
|
|
182
286
|
JSON.stringify(
|
|
183
|
-
{ 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 } : {} } },
|
|
184
288
|
null,
|
|
185
289
|
2
|
|
186
290
|
)
|
|
187
291
|
);
|
|
188
292
|
} else {
|
|
189
293
|
console.error(`\u2717 run: ${message}`);
|
|
294
|
+
if (userAction) console.error(` Next: ${userAction}`);
|
|
190
295
|
}
|
|
191
296
|
return EXIT.usage;
|
|
192
297
|
}
|
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,15 +2,25 @@ 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);
|
|
@@ -18,16 +28,31 @@ async function handleStatus({ options }) {
|
|
|
18
28
|
assertAdapter(adapter);
|
|
19
29
|
const surface = getAdapterSurface(adapter);
|
|
20
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
|
+
}
|
|
21
37
|
const next = surface.hints.relaunch;
|
|
22
|
-
const
|
|
38
|
+
const deviceView = adapter === "mobile" ? mobileDeviceView(allDevices) : null;
|
|
39
|
+
const devices = deviceView?.devices ?? [];
|
|
23
40
|
if (json) {
|
|
24
|
-
const doProbe = !fast && adapter === "mobile" &&
|
|
41
|
+
const doProbe = !fast && adapter === "mobile" && deviceView !== null && deviceView.allConnectedDevices.length > 0;
|
|
25
42
|
if (doProbe) {
|
|
26
|
-
const
|
|
27
|
-
const devicesWithLive = devices.map((d) => mergeDeviceLive(d, liveMap.get(d.id)));
|
|
43
|
+
const liveView = await mobileDeviceLiveView(target, deviceView);
|
|
28
44
|
console.log(
|
|
29
45
|
JSON.stringify(
|
|
30
|
-
{
|
|
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
|
+
},
|
|
31
56
|
null,
|
|
32
57
|
2
|
|
33
58
|
)
|
|
@@ -41,63 +66,18 @@ async function handleStatus({ options }) {
|
|
|
41
66
|
}
|
|
42
67
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
43
68
|
console.log(`${out("label", "status")} ${out("bold", adapter)} ${out("dim", target)}`);
|
|
44
|
-
if (
|
|
45
|
-
if (!fast && adapter === "mobile" &&
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
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))}`);
|
|
49
76
|
} else {
|
|
50
77
|
console.log(`${out("label", "Next:")} ${out("cmd", next)}`);
|
|
51
78
|
}
|
|
52
79
|
return EXIT.ok;
|
|
53
80
|
}
|
|
54
|
-
function nextForLive(fallback, liveMap) {
|
|
55
|
-
for (const live of liveMap.values()) {
|
|
56
|
-
if (live.liveState === void 0) return "mm-harness logs";
|
|
57
|
-
}
|
|
58
|
-
return fallback;
|
|
59
|
-
}
|
|
60
|
-
function slotScopedDevices(devices) {
|
|
61
|
-
const withSelection = devices.map((device) => ({ ...device, selected: deviceSelected(device) }));
|
|
62
|
-
const selected = withSelection.filter((device) => device.selected);
|
|
63
|
-
return selected.length > 0 ? selected : withSelection;
|
|
64
|
-
}
|
|
65
|
-
function mergeDeviceLive(device, live) {
|
|
66
|
-
if (!live) return device;
|
|
67
|
-
return {
|
|
68
|
-
...device,
|
|
69
|
-
fixtureStatus: live.fixtureStatus,
|
|
70
|
-
...live.liveState !== void 0 ? { liveState: live.liveState } : {},
|
|
71
|
-
...live.currentScreen !== void 0 ? { currentScreen: live.currentScreen } : {},
|
|
72
|
-
...live.walletState !== void 0 ? { walletState: live.walletState } : {},
|
|
73
|
-
...live.selectedAccount !== void 0 ? { selectedAccount: live.selectedAccount } : {}
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
function renderLiveBlock(devices, liveMap, out) {
|
|
77
|
-
console.log(`${out("label", "live:")}`);
|
|
78
|
-
for (const device of devices) {
|
|
79
|
-
const live = liveMap.get(device.id);
|
|
80
|
-
const prefix = ` ${device.platform} ${out("dim", device.id)}${device.name ? ` (${device.name})` : ""}`;
|
|
81
|
-
if (!live || live.liveState === "no-bridge") {
|
|
82
|
-
console.log(`${prefix}: ${out("dim", "(no-bridge)")}`);
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
if (live.liveState === "bridge-absent") {
|
|
86
|
-
console.log(`${prefix}: ${out("warn", "(bridge-absent \u2014 app attached, build lacks __AGENTIC__; rebuild/reinstall a dev build)")}`);
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
const parts = [];
|
|
90
|
-
if (live.currentScreen !== void 0) parts.push(`screen=${out("cmd", live.currentScreen)}`);
|
|
91
|
-
if (live.walletState !== void 0) parts.push(`wallet=${out(live.walletState === "unlocked" ? "ok" : "warn", live.walletState)}`);
|
|
92
|
-
if (live.selectedAccount !== void 0) {
|
|
93
|
-
parts.push(`account=${live.selectedAccount.label} ${out("dim", `(${live.selectedAccount.address})`)}`);
|
|
94
|
-
}
|
|
95
|
-
if (live.fixtureStatus !== void 0) {
|
|
96
|
-
parts.push(`fixture=${out(live.fixtureStatus === "READY" ? "ok" : "warn", live.fixtureStatus)}`);
|
|
97
|
-
}
|
|
98
|
-
console.log(`${prefix}: ${parts.join(" ")}`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
81
|
export {
|
|
102
82
|
handleStatus
|
|
103
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);
|