@deeeed/metamask-harness 0.7.5 → 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 +13 -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 +3 -3
- package/dist/cli.js +4 -0
- package/dist/commands/call.js +7 -0
- package/dist/commands/device-target.js +105 -0
- package/dist/commands/doctor.js +17 -4
- package/dist/commands/parse-args.js +1 -0
- package/dist/commands/run-engine.js +41 -3
- package/dist/commands/run.js +8 -1
- package/dist/commands/status-probe.js +167 -0
- package/dist/commands/status.js +88 -0
- package/dist/devices.js +66 -0
- package/dist/mm-harness-cli.js +41 -2
- package/docs/CLI-SPEC.md +70 -0
- 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
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { color } from "../cli-color.js";
|
|
2
|
+
import { detectAdapter } from "../harness.js";
|
|
3
|
+
import { assertAdapter } from "../paths.js";
|
|
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";
|
|
8
|
+
import { ADAPTER_DETECT_NEXT, EXIT, usageOut } from "./shared.js";
|
|
9
|
+
import { optionFlag, targetPath } from "./parse-args.js";
|
|
10
|
+
async function handleStatus({ options }) {
|
|
11
|
+
const target = targetPath(options);
|
|
12
|
+
const json = optionFlag(options, "json");
|
|
13
|
+
const fast = optionFlag(options, "fast");
|
|
14
|
+
const adapter = detectAdapter(target);
|
|
15
|
+
if (!adapter) {
|
|
16
|
+
return usageOut(json, "status", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
17
|
+
}
|
|
18
|
+
assertAdapter(adapter);
|
|
19
|
+
const devices = adapter === "mobile" ? listConnectedDevices().map((device) => ({ ...device, selected: deviceSelected(device) })) : [];
|
|
20
|
+
const next = getAdapterSurface(adapter).hints.relaunch;
|
|
21
|
+
if (json) {
|
|
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
|
+
}
|
|
38
|
+
return EXIT.ok;
|
|
39
|
+
}
|
|
40
|
+
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
41
|
+
console.log(`${out("label", "status")} ${out("bold", adapter)} ${out("dim", target)}`);
|
|
42
|
+
if (adapter === "mobile") renderDeviceList(devices, out);
|
|
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
|
+
}
|
|
48
|
+
return EXIT.ok;
|
|
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
|
+
}
|
|
86
|
+
export {
|
|
87
|
+
handleStatus
|
|
88
|
+
};
|
package/dist/devices.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
function listConnectedDevices(platform) {
|
|
3
|
+
const devices = [];
|
|
4
|
+
if (!platform || platform === "android") devices.push(...listAndroidDevices());
|
|
5
|
+
if (!platform || platform === "ios") devices.push(...listIosSimulators());
|
|
6
|
+
return devices;
|
|
7
|
+
}
|
|
8
|
+
function listAndroidDevices() {
|
|
9
|
+
let output;
|
|
10
|
+
try {
|
|
11
|
+
output = execFileSync("adb", ["devices", "-l"], {
|
|
12
|
+
encoding: "utf8",
|
|
13
|
+
timeout: 5e3,
|
|
14
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
15
|
+
});
|
|
16
|
+
} catch {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
const devices = [];
|
|
20
|
+
for (const line of output.split("\n").slice(1)) {
|
|
21
|
+
const trimmed = line.trim();
|
|
22
|
+
if (!trimmed) continue;
|
|
23
|
+
const fields = trimmed.split(/\s+/u);
|
|
24
|
+
const id = fields[0];
|
|
25
|
+
const state = fields[1] ?? "unknown";
|
|
26
|
+
if (!id) continue;
|
|
27
|
+
const modelField = fields.find((f) => f.startsWith("model:"));
|
|
28
|
+
const name = modelField ? modelField.slice("model:".length) : void 0;
|
|
29
|
+
devices.push({ platform: "android", id, state, ...name ? { name } : {} });
|
|
30
|
+
}
|
|
31
|
+
return devices;
|
|
32
|
+
}
|
|
33
|
+
function listIosSimulators() {
|
|
34
|
+
let output;
|
|
35
|
+
try {
|
|
36
|
+
output = execFileSync("xcrun", ["simctl", "list", "devices", "booted", "-j"], {
|
|
37
|
+
encoding: "utf8",
|
|
38
|
+
timeout: 5e3,
|
|
39
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
40
|
+
});
|
|
41
|
+
} catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
let parsed;
|
|
45
|
+
try {
|
|
46
|
+
parsed = JSON.parse(output);
|
|
47
|
+
} catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
const devices = [];
|
|
51
|
+
for (const runtimeDevices of Object.values(parsed.devices ?? {})) {
|
|
52
|
+
for (const device of runtimeDevices) {
|
|
53
|
+
if (!device.udid) continue;
|
|
54
|
+
devices.push({
|
|
55
|
+
platform: "ios",
|
|
56
|
+
id: device.udid,
|
|
57
|
+
state: device.state ?? "unknown",
|
|
58
|
+
...device.name ? { name: device.name } : {}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return devices;
|
|
63
|
+
}
|
|
64
|
+
export {
|
|
65
|
+
listConnectedDevices
|
|
66
|
+
};
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -10,6 +10,40 @@ globalThis.__MM_HARNESS_WRAPPER__ = true;
|
|
|
10
10
|
const { main: recipeMain } = await import("./cli.js");
|
|
11
11
|
const rawArgv = process.argv.slice(2);
|
|
12
12
|
const REAL = [
|
|
13
|
+
{
|
|
14
|
+
name: "status",
|
|
15
|
+
aliases: ["health", "home"],
|
|
16
|
+
summary: "Home dashboard for a checkout: detected adapter, next command, and (mobile) connected devices with the selected target.",
|
|
17
|
+
example: "mm-harness status",
|
|
18
|
+
helpText: `mm-harness status [flags]
|
|
19
|
+
|
|
20
|
+
Home dashboard for the current checkout \u2014 the detected adapter, the next command
|
|
21
|
+
to run, and (mobile only) the connected devices with live app state. Aliases: health, home.
|
|
22
|
+
|
|
23
|
+
--target <path> Checkout path (default: cwd)
|
|
24
|
+
--json Machine-readable envelope { adapter, target, devices[], next }
|
|
25
|
+
--fast Skip all live-state probes (instant output, safe for scripts)
|
|
26
|
+
|
|
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).
|
|
41
|
+
|
|
42
|
+
Example:
|
|
43
|
+
mm-harness status
|
|
44
|
+
mm-harness status --json
|
|
45
|
+
mm-harness status --fast`
|
|
46
|
+
},
|
|
13
47
|
{
|
|
14
48
|
name: "actions",
|
|
15
49
|
summary: "List the action vocabulary + field schemas (--raw dumps the raw action registry JSON).",
|
|
@@ -64,6 +98,7 @@ Example:
|
|
|
64
98
|
|
|
65
99
|
--list List everything invocable for the adapter (actions + flows); no <action> needed
|
|
66
100
|
--arg k=v Action field value (repeatable)
|
|
101
|
+
--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.
|
|
67
102
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
68
103
|
--target <path> Checkout path (default: cwd)
|
|
69
104
|
--artifacts-dir <dir> Where to write evidence (default: temp dir)
|
|
@@ -103,6 +138,7 @@ Example:
|
|
|
103
138
|
|
|
104
139
|
--list List everything invocable for the adapter (actions + flows); no <recipe> needed
|
|
105
140
|
--plan Validate + print execution plan, touching nothing. Exit 5 if invalid.
|
|
141
|
+
--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.
|
|
106
142
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
107
143
|
--target <path> Checkout path (default: cwd)
|
|
108
144
|
--artifacts-dir <dir> Where to write evidence (required unless --plan)
|
|
@@ -128,6 +164,7 @@ Example:
|
|
|
128
164
|
--fix Repair the overlay/runtime-context WITHOUT launching (no fixture reseed); --json adds fixed[]/failed[]
|
|
129
165
|
--expect-live Exit 0 iff the runtime is live (extension: watcher+CDP; mobile: Metro+bridge; core: deps), non-zero + teaching escape otherwise
|
|
130
166
|
--cdp-port <port> Extension CDP port for the liveness probe (env: CDP_PORT / RECIPE_CDP_PORT)
|
|
167
|
+
--device <udid|serial|name> Mobile only: target this device (env: IOS_SIMULATOR / ADB_SERIAL). doctor reports the connected devices; it never gates on ambiguity.
|
|
131
168
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
132
169
|
--target <path> Checkout path (default: cwd)
|
|
133
170
|
--runtime-dir <dir> Runtime dir containing agentic-runtime.json (relative to target)
|
|
@@ -417,7 +454,7 @@ const HELP_GROUPS = [
|
|
|
417
454
|
{
|
|
418
455
|
title: "DAILY LOOP",
|
|
419
456
|
blurb: "what a teammate runs many times a day (auto-ensures the overlay; --heal owns recovery)",
|
|
420
|
-
commands: ["launch", "stop", "logs", "debug", "fixtures"]
|
|
457
|
+
commands: ["status", "launch", "stop", "logs", "debug", "fixtures"]
|
|
421
458
|
},
|
|
422
459
|
{
|
|
423
460
|
title: "DISCOVER",
|
|
@@ -530,7 +567,9 @@ const pkgVersion = (() => {
|
|
|
530
567
|
const program = new Command();
|
|
531
568
|
program.name("mm-harness").description("the MetaMask recipe harness: launch the app, prove behavior, manage the runtime overlay").version(pkgVersion, "-v, --version", "Print the mm-harness version").helpOption("-h, --help", "Show grouped help").showHelpAfterError("(run `mm-harness --help` for the full surface)").configureHelp({ formatHelp: () => groupedHelp() });
|
|
532
569
|
for (const command of REAL) {
|
|
533
|
-
program.command(command.name).description(command.summary).allowUnknownOption().helpOption("-h, --help", "Show command help")
|
|
570
|
+
const registered = program.command(command.name).description(command.summary).allowUnknownOption().helpOption("-h, --help", "Show command help");
|
|
571
|
+
if (command.aliases?.length) registered.aliases(command.aliases);
|
|
572
|
+
registered.configureHelp({ formatHelp: () => `${command.helpText}
|
|
534
573
|
` }).argument("[args...]").action(async () => {
|
|
535
574
|
if (command.name === "update") {
|
|
536
575
|
process.exit(await handleUpdate(rawArgv.slice(1)));
|
package/docs/CLI-SPEC.md
CHANGED
|
@@ -437,6 +437,76 @@ Readiness check for a checkout without launching the app. Doctor is the single p
|
|
|
437
437
|
|
|
438
438
|
---
|
|
439
439
|
|
|
440
|
+
## `--device <id>` — first-class mobile device targeting (REAL)
|
|
441
|
+
|
|
442
|
+
`--device` is the uniform mobile device selector on `run`, `call`, and `doctor`. It sets the **same env `launch` does** before the engine reads `process.env`, so the engine needs no changes: an Android serial → `ADB_SERIAL` + `ANDROID_SERIAL` + `ANDROID_DEVICE`; an iOS UDID/simulator name → `IOS_SIMULATOR`. The android-vs-ios decision comes from matching the id against the connected-device lists (`adb devices -l` + booted `xcrun simctl` simulators); an id that matches neither list is a teaching usage error that lists the connected devices.
|
|
443
|
+
|
|
444
|
+
| Verb | `--device` given | `--device` omitted (mobile) |
|
|
445
|
+
|---|---|---|
|
|
446
|
+
| `run` / `call` | resolve id → set serial/simulator env; proceed | **ambiguity gate:** >1 connected mobile device (across both android and ios, counting only targetable ones: android state `device`, iOS state `Booted`) → fail fast (exit 2), listing connected devices and `--device <id>` hints. Exactly one targetable (or zero — existing engine errors speak) → unchanged behavior. |
|
|
447
|
+
| `doctor` | resolve id → set env | no gate — doctor is diagnostic and **reports** the device list (`devices[]`) instead. |
|
|
448
|
+
|
|
449
|
+
`--device` on the **extension/core** adapter is a teaching usage error (those adapters have no device to target). `run --plan` is static (touches no device) and is exempt from the gate.
|
|
450
|
+
|
|
451
|
+
## `status` (aliases `health`, `home`) — home dashboard + devices[] (REAL)
|
|
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 with live app state. Extension/core checkouts report an empty `devices[]`. Additive envelope:
|
|
454
|
+
|
|
455
|
+
```json
|
|
456
|
+
{
|
|
457
|
+
"schemaVersion": 1,
|
|
458
|
+
"command": "status",
|
|
459
|
+
"adapter": "mobile",
|
|
460
|
+
"target": "/path/to/checkout",
|
|
461
|
+
"devices": [
|
|
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
|
+
}
|
|
474
|
+
],
|
|
475
|
+
"next": "mm-harness launch ios"
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
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.
|
|
505
|
+
|
|
506
|
+
**Exit:** 0 (2 when the repo type cannot be detected).
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
440
510
|
## DISCOVER — agent composition input
|
|
441
511
|
|
|
442
512
|
> **Decided (Arthur):** DISCOVER is a distinct group, not a sub-PROVE. The audience
|