@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.
Files changed (40) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/adapters/extension/inject.mjs +1 -0
  3. package/adapters/extension/launch-browser.cjs +15 -3
  4. package/adapters/extension/lib/extension-id.cjs +36 -0
  5. package/adapters/extension/live.sh +5 -3
  6. package/adapters/extension/readiness.mjs +48 -0
  7. package/adapters/extension/reattach.sh +163 -55
  8. package/adapters/extension/sidepanel-toggle.sh +96 -24
  9. package/adapters/extension/verify.sh +11 -0
  10. package/adapters/extension/wallet-fixture-state.cjs +6 -11
  11. package/adapters/mobile/open-device.sh +32 -5
  12. package/adapters/shared/resolve-slot-ports-core.mjs +14 -4
  13. package/dist/adapters/core/surface.js +1 -0
  14. package/dist/adapters/extension/runtime.js +14 -5
  15. package/dist/adapters/extension/surface.js +1 -0
  16. package/dist/adapters/mobile/provision.js +49 -4
  17. package/dist/adapters/mobile/surface.js +1 -0
  18. package/dist/adapters/slot-ports.js +11 -4
  19. package/dist/cli.js +4 -0
  20. package/dist/commands/call.js +19 -2
  21. package/dist/commands/check.js +326 -0
  22. package/dist/commands/core-readiness.js +75 -0
  23. package/dist/commands/device-target.js +113 -10
  24. package/dist/commands/doctor.js +30 -18
  25. package/dist/commands/launch/extension.js +105 -9
  26. package/dist/commands/launch/index.js +116 -15
  27. package/dist/commands/mobile-device-view.js +140 -0
  28. package/dist/commands/parse-args.js +4 -1
  29. package/dist/commands/recipe-quality.js +6 -2
  30. package/dist/commands/run-engine.js +28 -2
  31. package/dist/commands/run-report.js +115 -0
  32. package/dist/commands/run.js +113 -8
  33. package/dist/commands/shared.js +2 -1
  34. package/dist/commands/status-probe.js +9 -3
  35. package/dist/commands/status.js +40 -60
  36. package/dist/live-adapter-contract.js +5 -1
  37. package/dist/mm-harness-cli.js +61 -22
  38. package/docs/CLI-SPEC.md +25 -0
  39. package/library/actions/extension/platform/cdp.mjs +7 -4
  40. 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
+ };
@@ -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
- const artifactsDir = requiredOption(options, "artifactsDir", "run requires --artifacts-dir <dir>.");
37
- const prepared = await prepareHeal(adapter, target, options, json);
38
- if (typeof prepared === "number") return prepared;
39
- const { state, heal } = prepared;
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
- console.log(`MetaMask recipe run: ${result.status}
83
- Artifacts: ${result.artifactManifestPath}`);
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 emitRunUsageError(json, adapter, recipeFile, code, message) {
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
  }
@@ -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 explicit = str(options, "adapter") ?? str(options, "platform");
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
- if (platformEntries.length === 1) return platformEntries[0];
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);
@@ -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 { listConnectedDevices } from "../devices.js";
6
- import { deviceSelected, renderDeviceList } from "./device-target.js";
7
- import { probeMobileLiveState } from "./status-probe.js";
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 devices = adapter === "mobile" ? slotScopedDevices(listConnectedDevices()) : [];
38
+ const deviceView = adapter === "mobile" ? mobileDeviceView(allDevices) : null;
39
+ const devices = deviceView?.devices ?? [];
23
40
  if (json) {
24
- const doProbe = !fast && adapter === "mobile" && devices.length > 0;
41
+ const doProbe = !fast && adapter === "mobile" && deviceView !== null && deviceView.allConnectedDevices.length > 0;
25
42
  if (doProbe) {
26
- const liveMap = await probeMobileLiveState(target, devices);
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
- { schemaVersion: 1, command: "status", adapter, target, devices: devicesWithLive, next: nextForLive(next, liveMap) },
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 (adapter === "mobile") renderDeviceList(devices, out);
45
- if (!fast && adapter === "mobile" && devices.length > 0) {
46
- const liveMap = await probeMobileLiveState(target, devices);
47
- renderLiveBlock(devices, liveMap, out);
48
- console.log(`${out("label", "Next:")} ${out("cmd", nextForLive(next, liveMap))}`);
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 { TSX_TSCONFIG_PATH: tsconfigPath };
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);