@deeeed/metamask-harness 0.8.0 → 0.9.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 +6 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +5 -2
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +37 -11
- package/dist/cli-commands.js +1 -1
- package/dist/commands/parse-args.js +1 -0
- package/dist/commands/status-probe.js +167 -0
- package/dist/commands/status.js +58 -1
- package/dist/mm-harness-cli.js +18 -5
- package/docs/CLI-SPEC.md +39 -4
- package/library/manifests/core.action-manifest.json +1465 -1225
- package/library/manifests/extension.action-manifest.json +324 -5
- package/library/manifests/mobile.action-manifest.json +321 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0 - 2026-07-07
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **`status` auto-probes live app state per device** — the `yarn a:status` successor. Static info prints instantly; within a strict 2s window each device line is enriched with `screen=` (active route via the `__AGENTIC__` bridge, Route objects normalised), `wallet=locked|unlocked|onboarding`, `account=<label> (0x1234…abcd)` and `fixture=READY|missing`. `--fast` skips all probes (guaranteed-instant for scripts); `--json` carries the enriched `devices[]` in one envelope. All RN targets attached to the checkout's Metro are probed and matched per device (exact id → exact name → platform-uniqueness → leftover 1:1), so android + ios side by side on one Metro both report. Honest degradation: `no-bridge` (nothing matched/answering) vs `bridge-absent` (target attached but the installed build predates `__AGENTIC__`, rendered with a rebuild hint) — surfacing pre-bridge builds required the target discovery probe to become three-state (agentic/responsive/unreachable) instead of silently dropping them.
|
|
7
|
+
- **Every action now carries self-discovery metadata.** `action_metadata` (description + example recipe node, derived from the real handler implementations) covers all official and custom actions across the mobile, extension and core manifests — previously `app.status`, `cdp.target` and all 16 `metamask.wallet.*`/`metamask.perps.*` actions were bare names to agents. A new contract guard enforces coverage (including example structure: `node.action` must match), and locks mobile ↔ extension action-set parity with an explicit platform-only allowlist.
|
|
8
|
+
|
|
3
9
|
## 0.8.0 - 2026-07-07
|
|
4
10
|
|
|
5
11
|
### Added
|
|
@@ -173,10 +173,11 @@ const COMMANDS = {
|
|
|
173
173
|
|
|
174
174
|
async status(client, _args, { deviceName, platform } = {}) {
|
|
175
175
|
const expr = `(function() {
|
|
176
|
+
var agenticPresent = typeof globalThis.__AGENTIC__ !== 'undefined';
|
|
176
177
|
var route = globalThis.__AGENTIC__?.getRoute() || null;
|
|
177
178
|
var account = null;
|
|
178
179
|
try { account = globalThis.__AGENTIC__?.getSelectedAccount() || null; } catch(e) {}
|
|
179
|
-
return { route: route, account: account };
|
|
180
|
+
return { route: route, account: account, agenticPresent: agenticPresent };
|
|
180
181
|
})()`;
|
|
181
182
|
const snapshot = await cdpEval(client, expr);
|
|
182
183
|
return { ...snapshot, deviceName: deviceName || '', platform: platform || '' };
|
|
@@ -658,7 +659,9 @@ Environment:
|
|
|
658
659
|
const result = await handler(client, args.slice(1), { deviceName: target.deviceName, platform });
|
|
659
660
|
results.push(result);
|
|
660
661
|
} catch {
|
|
661
|
-
//
|
|
662
|
+
// Recovery is correct: status probes every target advisory; an
|
|
663
|
+
// unresponsive one degrades to absent-from-the-listing rather than
|
|
664
|
+
// failing the whole multi-target command.
|
|
662
665
|
} finally {
|
|
663
666
|
if (client) client.close();
|
|
664
667
|
}
|
|
@@ -48,6 +48,18 @@ async function fetchJSON(url) {
|
|
|
48
48
|
* Returns true if __AGENTIC__ is installed, false otherwise.
|
|
49
49
|
*/
|
|
50
50
|
async function probeTarget(wsUrl) {
|
|
51
|
+
return (await probeTargetDetailed(wsUrl)) === 'agentic';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Three-state probe so callers can tell a pre-__AGENTIC__ build apart from a
|
|
56
|
+
* dead target:
|
|
57
|
+
* 'agentic' — JS runtime answered and __AGENTIC__ is installed.
|
|
58
|
+
* 'responsive' — the target evaluated JS but has no __AGENTIC__ (an app build
|
|
59
|
+
* that predates the bridge, or a non-runtime page).
|
|
60
|
+
* 'unreachable' — connect/eval failed.
|
|
61
|
+
*/
|
|
62
|
+
async function probeTargetDetailed(wsUrl) {
|
|
51
63
|
try {
|
|
52
64
|
const client = await createWSClient(wsUrl, 3000);
|
|
53
65
|
try {
|
|
@@ -56,13 +68,13 @@ async function probeTarget(wsUrl) {
|
|
|
56
68
|
returnByValue: true,
|
|
57
69
|
awaitPromise: false,
|
|
58
70
|
});
|
|
59
|
-
return result?.result?.value === 'object';
|
|
71
|
+
return result?.result?.value === 'object' ? 'agentic' : 'responsive';
|
|
60
72
|
} finally {
|
|
61
73
|
client.close();
|
|
62
74
|
}
|
|
63
75
|
} catch {
|
|
64
|
-
// Connection failed — target is not
|
|
65
|
-
return
|
|
76
|
+
// Connection failed — target is not usable at all
|
|
77
|
+
return 'unreachable';
|
|
66
78
|
}
|
|
67
79
|
}
|
|
68
80
|
|
|
@@ -186,18 +198,32 @@ async function discoverAllTargets(port) {
|
|
|
186
198
|
return bPage - aPage;
|
|
187
199
|
});
|
|
188
200
|
|
|
189
|
-
// Group by deviceName, probe each to find the JS runtime target
|
|
190
|
-
|
|
191
|
-
|
|
201
|
+
// Group by deviceName, probe each to find the JS runtime target. Prefer the
|
|
202
|
+
// device's __AGENTIC__-bearing target; when a device has none, keep its first
|
|
203
|
+
// RESPONSIVE candidate (candidates are sorted JS-runtime-first) so a build
|
|
204
|
+
// that predates the bridge still surfaces — the status consumer reports it as
|
|
205
|
+
// bridge-absent instead of the device silently vanishing from discovery.
|
|
206
|
+
// Known ambiguity: when a device's JS runtime is attached-but-busy while its
|
|
207
|
+
// native C++ Hermes page answers, that page is also 'responsive' (no
|
|
208
|
+
// __AGENTIC__ in the native context), so the device reports bridge-absent even
|
|
209
|
+
// though the app build may carry the bridge. Low probability, and the degraded
|
|
210
|
+
// report is informative rather than harmful.
|
|
211
|
+
const agenticByDevice = new Map();
|
|
212
|
+
const responsiveByDevice = new Map();
|
|
192
213
|
for (const candidate of candidates) {
|
|
193
214
|
const device = candidate.deviceName || candidate.id || candidate.webSocketDebuggerUrl;
|
|
194
|
-
if (
|
|
195
|
-
const
|
|
196
|
-
if (
|
|
197
|
-
|
|
198
|
-
|
|
215
|
+
if (agenticByDevice.has(device)) continue;
|
|
216
|
+
const state = await probeTargetDetailed(candidate.webSocketDebuggerUrl);
|
|
217
|
+
if (state === 'agentic') {
|
|
218
|
+
agenticByDevice.set(device, { wsUrl: candidate.webSocketDebuggerUrl, deviceName: device });
|
|
219
|
+
} else if (state === 'responsive' && !responsiveByDevice.has(device)) {
|
|
220
|
+
responsiveByDevice.set(device, { wsUrl: candidate.webSocketDebuggerUrl, deviceName: device });
|
|
199
221
|
}
|
|
200
222
|
}
|
|
223
|
+
const results = [...agenticByDevice.values()];
|
|
224
|
+
for (const [device, entry] of responsiveByDevice) {
|
|
225
|
+
if (!agenticByDevice.has(device)) results.push(entry);
|
|
226
|
+
}
|
|
201
227
|
return results;
|
|
202
228
|
}
|
|
203
229
|
|
package/dist/cli-commands.js
CHANGED
|
@@ -7,7 +7,7 @@ const SPEC = {
|
|
|
7
7
|
{ name: "help", aliases: ["-h", "--help"], desc: "Show usage" }
|
|
8
8
|
],
|
|
9
9
|
shared: [
|
|
10
|
-
{ name: "status", aliases: ["health", "home"], desc: "Home status + next commands", flags: ["--json"] },
|
|
10
|
+
{ name: "status", aliases: ["health", "home"], desc: "Home status + next commands", flags: ["--json", "--fast"] },
|
|
11
11
|
{ name: "ports", desc: "Slot ports and runtime paths", flags: ["--json"] },
|
|
12
12
|
{ name: "up", desc: "Decide + run minimum work to reach ready", flags: ["--json", "--dry-run"] },
|
|
13
13
|
{ name: "sync", desc: "Refresh harness + canonicalize wallet fixture", flags: ["--json"] },
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fixtureSummary } from "../doctor.js";
|
|
5
|
+
import { runnerDir } from "../paths.js";
|
|
6
|
+
const PROBE_TIMEOUT_MS = 2e3;
|
|
7
|
+
function bridgeScriptPath() {
|
|
8
|
+
return process.env.METAMASK_RECIPE_MOBILE_BRIDGE_SCRIPT ?? path.join(runnerDir, "adapters", "mobile", "bridge-runtime", "cdp-bridge.cjs");
|
|
9
|
+
}
|
|
10
|
+
function truncateAddress(address) {
|
|
11
|
+
if (!address || address.length <= 10) return address;
|
|
12
|
+
return `${address.slice(0, 6)}\u2026${address.slice(-4)}`;
|
|
13
|
+
}
|
|
14
|
+
function routeToString(route) {
|
|
15
|
+
if (typeof route === "string") return route;
|
|
16
|
+
if (route && typeof route === "object" && "name" in route) {
|
|
17
|
+
const n = route.name;
|
|
18
|
+
if (typeof n === "string") return n;
|
|
19
|
+
}
|
|
20
|
+
return void 0;
|
|
21
|
+
}
|
|
22
|
+
function deriveWalletState(account, route) {
|
|
23
|
+
const routeStr = (routeToString(route) ?? "").toLowerCase();
|
|
24
|
+
if (routeStr.includes("onboard")) return "onboarding";
|
|
25
|
+
if (account !== null && account !== void 0 && typeof account === "object") return "unlocked";
|
|
26
|
+
return "locked";
|
|
27
|
+
}
|
|
28
|
+
function parseBridgeEntries(raw) {
|
|
29
|
+
if (!raw || typeof raw !== "object") return [];
|
|
30
|
+
const items = Array.isArray(raw) ? raw : [raw];
|
|
31
|
+
return items.filter((e) => Boolean(e && typeof e === "object" && !Array.isArray(e))).map((e) => ({
|
|
32
|
+
route: e.route,
|
|
33
|
+
account: e.account,
|
|
34
|
+
deviceName: typeof e.deviceName === "string" ? e.deviceName : "",
|
|
35
|
+
platform: typeof e.platform === "string" ? e.platform : "",
|
|
36
|
+
// agenticPresent is only set when the bridge explicitly sends it (false = absent).
|
|
37
|
+
// Omitted by older bridge versions → leave undefined so caller treats as present.
|
|
38
|
+
...typeof e.agenticPresent === "boolean" ? { agenticPresent: e.agenticPresent } : {}
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
function matchBridgeEntry(device, entries) {
|
|
42
|
+
const byId = entries.find((e) => e.deviceName === device.id);
|
|
43
|
+
if (byId) return byId;
|
|
44
|
+
if (device.name) {
|
|
45
|
+
const byName = entries.find((e) => e.deviceName === device.name);
|
|
46
|
+
if (byName) return byName;
|
|
47
|
+
}
|
|
48
|
+
const platformEntries = entries.filter((e) => e.platform === device.platform);
|
|
49
|
+
if (platformEntries.length === 1) return platformEntries[0];
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
function probeMobileBridge(target) {
|
|
53
|
+
const script = bridgeScriptPath();
|
|
54
|
+
if (!fs.existsSync(script)) {
|
|
55
|
+
return Promise.resolve({ ok: false, liveState: "no-bridge" });
|
|
56
|
+
}
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
const env = {
|
|
59
|
+
...process.env,
|
|
60
|
+
APP_ROOT: target,
|
|
61
|
+
CDP_TIMEOUT: String(PROBE_TIMEOUT_MS)
|
|
62
|
+
};
|
|
63
|
+
let settled = false;
|
|
64
|
+
let stdout = "";
|
|
65
|
+
const child = spawn(process.execPath, [script, "status"], {
|
|
66
|
+
cwd: target,
|
|
67
|
+
env,
|
|
68
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
69
|
+
});
|
|
70
|
+
const timer = setTimeout(() => {
|
|
71
|
+
if (settled) return;
|
|
72
|
+
settled = true;
|
|
73
|
+
child.kill("SIGTERM");
|
|
74
|
+
const killTimer = setTimeout(() => child.kill("SIGKILL"), 500);
|
|
75
|
+
child.once("close", () => clearTimeout(killTimer));
|
|
76
|
+
resolve({ ok: false, liveState: "no-bridge" });
|
|
77
|
+
}, PROBE_TIMEOUT_MS);
|
|
78
|
+
child.stdout.on("data", (chunk) => {
|
|
79
|
+
stdout += chunk;
|
|
80
|
+
});
|
|
81
|
+
child.on("close", (code) => {
|
|
82
|
+
if (settled) return;
|
|
83
|
+
settled = true;
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
if (code !== 0) {
|
|
86
|
+
resolve({ ok: false, liveState: "no-bridge" });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const raw = JSON.parse(stdout);
|
|
91
|
+
const entries = parseBridgeEntries(raw);
|
|
92
|
+
if (entries.length === 0) {
|
|
93
|
+
resolve({ ok: false, liveState: "no-bridge" });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
resolve({ ok: true, entries });
|
|
97
|
+
} catch {
|
|
98
|
+
resolve({ ok: false, liveState: "no-bridge" });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
child.on("error", () => {
|
|
102
|
+
if (settled) return;
|
|
103
|
+
settled = true;
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
resolve({ ok: false, liveState: "no-bridge" });
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function resolveFixtureStatus(target) {
|
|
110
|
+
const summary = fixtureSummary(target);
|
|
111
|
+
return summary.status === "ready" ? "READY" : "missing";
|
|
112
|
+
}
|
|
113
|
+
async function probeMobileLiveState(target, devices) {
|
|
114
|
+
const result = /* @__PURE__ */ new Map();
|
|
115
|
+
if (devices.length === 0) return result;
|
|
116
|
+
const fixtureStatus = resolveFixtureStatus(target);
|
|
117
|
+
const probeResult = await probeMobileBridge(target);
|
|
118
|
+
if (!probeResult.ok) {
|
|
119
|
+
for (const device of devices) result.set(device.id, { fixtureStatus, liveState: "no-bridge" });
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
const { entries } = probeResult;
|
|
123
|
+
const matched = /* @__PURE__ */ new Map();
|
|
124
|
+
const usedEntries = /* @__PURE__ */ new Set();
|
|
125
|
+
for (const device of devices) {
|
|
126
|
+
const entry = matchBridgeEntry(device, entries.filter((e) => !usedEntries.has(e)));
|
|
127
|
+
if (entry) {
|
|
128
|
+
matched.set(device.id, entry);
|
|
129
|
+
usedEntries.add(entry);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const leftoverEntries = entries.filter(
|
|
133
|
+
(e) => !usedEntries.has(e) && e.platform === "" && e.agenticPresent === false
|
|
134
|
+
);
|
|
135
|
+
const unmatchedDevices = devices.filter((d) => !matched.has(d.id));
|
|
136
|
+
if (leftoverEntries.length === 1 && unmatchedDevices.length === 1) {
|
|
137
|
+
matched.set(unmatchedDevices[0].id, leftoverEntries[0]);
|
|
138
|
+
}
|
|
139
|
+
for (const device of devices) {
|
|
140
|
+
const entry = matched.get(device.id);
|
|
141
|
+
if (!entry) {
|
|
142
|
+
result.set(device.id, { fixtureStatus, liveState: "no-bridge" });
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (entry.agenticPresent === false) {
|
|
146
|
+
result.set(device.id, { fixtureStatus, liveState: "bridge-absent" });
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const account = entry.account;
|
|
150
|
+
const walletState = deriveWalletState(account, entry.route);
|
|
151
|
+
const currentScreen = routeToString(entry.route);
|
|
152
|
+
const addrRaw = typeof account?.address === "string" ? account.address : void 0;
|
|
153
|
+
const labelRaw = typeof account?.name === "string" ? account.name : void 0;
|
|
154
|
+
const liveEntry = {
|
|
155
|
+
fixtureStatus,
|
|
156
|
+
walletState,
|
|
157
|
+
...currentScreen !== void 0 ? { currentScreen } : {},
|
|
158
|
+
...labelRaw !== void 0 && addrRaw !== void 0 ? { selectedAccount: { label: labelRaw, address: truncateAddress(addrRaw) } } : {}
|
|
159
|
+
};
|
|
160
|
+
result.set(device.id, liveEntry);
|
|
161
|
+
}
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
export {
|
|
165
|
+
probeMobileLiveState,
|
|
166
|
+
resolveFixtureStatus
|
|
167
|
+
};
|
package/dist/commands/status.js
CHANGED
|
@@ -4,11 +4,13 @@ import { assertAdapter } from "../paths.js";
|
|
|
4
4
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
5
5
|
import { listConnectedDevices } from "../devices.js";
|
|
6
6
|
import { deviceSelected, renderDeviceList } from "./device-target.js";
|
|
7
|
+
import { probeMobileLiveState } from "./status-probe.js";
|
|
7
8
|
import { ADAPTER_DETECT_NEXT, EXIT, usageOut } from "./shared.js";
|
|
8
9
|
import { optionFlag, targetPath } from "./parse-args.js";
|
|
9
10
|
async function handleStatus({ options }) {
|
|
10
11
|
const target = targetPath(options);
|
|
11
12
|
const json = optionFlag(options, "json");
|
|
13
|
+
const fast = optionFlag(options, "fast");
|
|
12
14
|
const adapter = detectAdapter(target);
|
|
13
15
|
if (!adapter) {
|
|
14
16
|
return usageOut(json, "status", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
@@ -17,15 +19,70 @@ async function handleStatus({ options }) {
|
|
|
17
19
|
const devices = adapter === "mobile" ? listConnectedDevices().map((device) => ({ ...device, selected: deviceSelected(device) })) : [];
|
|
18
20
|
const next = getAdapterSurface(adapter).hints.relaunch;
|
|
19
21
|
if (json) {
|
|
20
|
-
|
|
22
|
+
const doProbe = !fast && adapter === "mobile" && devices.length > 0;
|
|
23
|
+
if (doProbe) {
|
|
24
|
+
const liveMap = await probeMobileLiveState(target, devices);
|
|
25
|
+
const devicesWithLive = devices.map((d) => mergeDeviceLive(d, liveMap.get(d.id)));
|
|
26
|
+
console.log(
|
|
27
|
+
JSON.stringify(
|
|
28
|
+
{ schemaVersion: 1, command: "status", adapter, target, devices: devicesWithLive, next },
|
|
29
|
+
null,
|
|
30
|
+
2
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
} else {
|
|
34
|
+
console.log(
|
|
35
|
+
JSON.stringify({ schemaVersion: 1, command: "status", adapter, target, devices, next }, null, 2)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
21
38
|
return EXIT.ok;
|
|
22
39
|
}
|
|
23
40
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
24
41
|
console.log(`${out("label", "status")} ${out("bold", adapter)} ${out("dim", target)}`);
|
|
25
42
|
if (adapter === "mobile") renderDeviceList(devices, out);
|
|
26
43
|
console.log(`${out("label", "Next:")} ${out("cmd", next)}`);
|
|
44
|
+
if (!fast && adapter === "mobile" && devices.length > 0) {
|
|
45
|
+
const liveMap = await probeMobileLiveState(target, devices);
|
|
46
|
+
renderLiveBlock(devices, liveMap, out);
|
|
47
|
+
}
|
|
27
48
|
return EXIT.ok;
|
|
28
49
|
}
|
|
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
|
+
}
|
|
29
86
|
export {
|
|
30
87
|
handleStatus
|
|
31
88
|
};
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -18,18 +18,31 @@ const REAL = [
|
|
|
18
18
|
helpText: `mm-harness status [flags]
|
|
19
19
|
|
|
20
20
|
Home dashboard for the current checkout \u2014 the detected adapter, the next command
|
|
21
|
-
to run, and (mobile only) the connected devices with
|
|
22
|
-
target (matches ADB_SERIAL / IOS_SIMULATOR). Aliases: health, home.
|
|
21
|
+
to run, and (mobile only) the connected devices with live app state. Aliases: health, home.
|
|
23
22
|
|
|
24
23
|
--target <path> Checkout path (default: cwd)
|
|
25
24
|
--json Machine-readable envelope { adapter, target, devices[], next }
|
|
25
|
+
--fast Skip all live-state probes (instant output, safe for scripts)
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
Human output: static device list prints immediately; live-state lines append
|
|
28
|
+
after the ~2s probe window (screen, wallet, account, fixture per device).
|
|
29
|
+
|
|
30
|
+
The devices[] section carries per device:
|
|
31
|
+
{ platform, id, name, state, selected } always present
|
|
32
|
+
{ currentScreen, walletState, selectedAccount, fixtureStatus } when bridge reachable
|
|
33
|
+
{ liveState: 'no-bridge' } when bridge unreachable or --fast
|
|
34
|
+
|
|
35
|
+
All RN targets attached to the checkout's Metro are probed and matched to their
|
|
36
|
+
devices; a device whose target cannot be confidently matched (Metro's device
|
|
37
|
+
description matches neither the serial nor the model name) degrades to
|
|
38
|
+
liveState:'no-bridge'. An attached target whose build predates __AGENTIC__
|
|
39
|
+
reports liveState:'bridge-absent'.
|
|
40
|
+
Extension/core live-state probing is V1-pending (use doctor for runtime status).
|
|
29
41
|
|
|
30
42
|
Example:
|
|
31
43
|
mm-harness status
|
|
32
|
-
mm-harness status --json
|
|
44
|
+
mm-harness status --json
|
|
45
|
+
mm-harness status --fast`
|
|
33
46
|
},
|
|
34
47
|
{
|
|
35
48
|
name: "actions",
|
package/docs/CLI-SPEC.md
CHANGED
|
@@ -450,7 +450,7 @@ Readiness check for a checkout without launching the app. Doctor is the single p
|
|
|
450
450
|
|
|
451
451
|
## `status` (aliases `health`, `home`) — home dashboard + devices[] (REAL)
|
|
452
452
|
|
|
453
|
-
`status` is the compact home dashboard for a checkout: the detected adapter, the next command to run (`next`), and — for **mobile** checkouts — a `devices[]` section. Extension/core checkouts report an empty `devices[]
|
|
453
|
+
`status` is the compact home dashboard for a checkout: the detected adapter, the next command to run (`next`), and — for **mobile** checkouts — a `devices[]` section with live app state. Extension/core checkouts report an empty `devices[]`. Additive envelope:
|
|
454
454
|
|
|
455
455
|
```json
|
|
456
456
|
{
|
|
@@ -459,14 +459,49 @@ Readiness check for a checkout without launching the app. Doctor is the single p
|
|
|
459
459
|
"adapter": "mobile",
|
|
460
460
|
"target": "/path/to/checkout",
|
|
461
461
|
"devices": [
|
|
462
|
-
{
|
|
463
|
-
|
|
462
|
+
{
|
|
463
|
+
"platform": "android", "id": "emulator-5554", "name": "Pixel_6",
|
|
464
|
+
"state": "device", "selected": true,
|
|
465
|
+
"currentScreen": "Wallet", "walletState": "unlocked",
|
|
466
|
+
"selectedAccount": { "label": "Account 1", "address": "0xabcd…ef12" },
|
|
467
|
+
"fixtureStatus": "READY"
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
"platform": "ios", "id": "AAAA-BBBB", "name": "iPhone 15",
|
|
471
|
+
"state": "Booted", "selected": false,
|
|
472
|
+
"fixtureStatus": "READY", "liveState": "no-bridge"
|
|
473
|
+
}
|
|
464
474
|
],
|
|
465
475
|
"next": "mm-harness launch ios"
|
|
466
476
|
}
|
|
467
477
|
```
|
|
468
478
|
|
|
469
|
-
|
|
479
|
+
**Per-device live fields** (additive — absent or null when unreachable):
|
|
480
|
+
|
|
481
|
+
| Field | Type | Description |
|
|
482
|
+
|---|---|---|
|
|
483
|
+
| `currentScreen` | string | Active route/screen in the running app (mobile bridge `status` route field) |
|
|
484
|
+
| `walletState` | `'locked'\|'unlocked'\|'onboarding'` | Derived from bridge status: account present → unlocked; onboarding route → onboarding; else locked |
|
|
485
|
+
| `selectedAccount` | `{ label, address }` | Active account name + address truncated to `0x1234…abcd` form |
|
|
486
|
+
| `fixtureStatus` | `'READY'\|'missing'` | Checkout-level: whether `wallet-fixture.json` is present and well-formed |
|
|
487
|
+
| `liveState` | `'no-bridge'\|'bridge-absent'` | Set when the bridge is unreachable/times out (`no-bridge`) or the target answers but `__AGENTIC__` is absent (`bridge-absent`); live fields omitted in both cases |
|
|
488
|
+
|
|
489
|
+
**`--fast` flag:** skips all live-state probes. Output is instant (same envelope shape, live fields absent). Safe for scripts and CI where probe latency is unacceptable.
|
|
490
|
+
|
|
491
|
+
**Human output** is progressive: static info (adapter, device list, next) prints immediately; a `live:` block appends after the ~2s probe window closes. `--fast` suppresses the live block entirely.
|
|
492
|
+
|
|
493
|
+
`selected` is `true` when the device id matches `ADB_SERIAL`/`ANDROID_SERIAL` (android) or `IOS_SIMULATOR` (ios) env. `doctor --json` carries the same `devices[]` shape (with `selected`) for mobile checkouts.
|
|
494
|
+
|
|
495
|
+
The mobile bridge probes all connected RN targets in one call and returns an array; each device is matched to its entry by name or platform-uniqueness, so multi-device checkouts (e.g. Android + iOS simulator) receive per-device live state.
|
|
496
|
+
|
|
497
|
+
`liveState` in the JSON envelope (absent = fully enriched):
|
|
498
|
+
|
|
499
|
+
| Value | Meaning |
|
|
500
|
+
|---|---|
|
|
501
|
+
| `'no-bridge'` | No matching/responding target on Metro — bridge genuinely unreachable or app not running. |
|
|
502
|
+
| `'bridge-absent'` | Target IS reachable and answered Runtime.evaluate, but `typeof globalThis.__AGENTIC__ === 'undefined'` — the installed build predates the bridge. Reinstall a dev build to get live state. |
|
|
503
|
+
|
|
504
|
+
Extension/core live-state probing (CDP home-tab route) is V1-pending — use `doctor` for runtime status.
|
|
470
505
|
|
|
471
506
|
**Exit:** 0 (2 when the repo type cannot be detected).
|
|
472
507
|
|