@deeeed/metamask-harness 0.7.4 → 0.8.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 +16 -0
- package/adapters/core/cleanup.sh +0 -0
- package/adapters/core/inject.sh +0 -0
- package/adapters/extension/cleanup.mjs +0 -0
- package/adapters/extension/ensure-browser.sh +0 -0
- package/adapters/extension/inject.mjs +0 -0
- package/adapters/extension/launch-browser.cjs +0 -0
- package/adapters/extension/launch.sh +0 -0
- package/adapters/extension/live.sh +0 -0
- package/adapters/extension/readiness.mjs +0 -0
- package/adapters/extension/reattach.sh +0 -0
- package/adapters/extension/refresh-build.sh +0 -0
- package/adapters/extension/seed-fixture.sh +0 -0
- package/adapters/extension/sidepanel-toggle.sh +0 -0
- package/adapters/extension/snapshot-dist.sh +0 -0
- package/adapters/extension/start-watch.sh +0 -0
- package/adapters/extension/verify.sh +0 -0
- package/adapters/extension/wallet-fixture-state.cjs +0 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -0
- package/adapters/mobile/bridge-runtime/setup-wallet.sh +0 -0
- package/adapters/mobile/cleanup.sh +0 -0
- package/adapters/mobile/inject.sh +0 -0
- package/adapters/mobile/lib/metro-listener.sh +0 -0
- package/adapters/mobile/lib/tmux-viewer.sh +0 -0
- package/adapters/mobile/open-device.sh +0 -0
- package/adapters/mobile/prewarm-bundle.sh +0 -0
- package/adapters/mobile/start-metro.sh +0 -0
- package/adapters/mobile/stop-metro.sh +0 -0
- package/adapters/mobile/verify.sh +0 -0
- package/adapters/mobile/wait-for-bridge.sh +0 -0
- package/adapters/mobile/yarn-setup.sh +0 -0
- package/adapters/shared/activate-repo-node.sh +0 -0
- package/adapters/shared/activate-repo-ruby.sh +0 -0
- package/adapters/shared/cli-ux.sh +0 -0
- package/adapters/shared/ensure-runner-deps.sh +0 -0
- package/adapters/shared/harness-path.sh +0 -0
- package/adapters/shared/hash-helpers.sh +0 -0
- package/adapters/shared/json-field.sh +0 -0
- package/adapters/shared/open-log-window.sh +0 -0
- package/adapters/shared/reap-checkout-metros.sh +0 -0
- package/adapters/shared/resolve-farmslot-ports.mjs +0 -0
- package/adapters/shared/resolve-farmslot-ports.sh +0 -0
- package/adapters/shared/resolve-slot-ports.mjs +0 -0
- package/adapters/shared/resolve-slot-ports.sh +0 -0
- package/adapters/shared/sync-wallet-fixture.sh +0 -0
- package/adapters/shared/tmux-session.sh +0 -0
- package/dist/adapters/extension/ensure-ready.js +38 -11
- package/dist/cli-commands.js +2 -2
- 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/launch/index.js +8 -0
- package/dist/commands/run-engine.js +41 -3
- package/dist/commands/run.js +8 -1
- package/dist/commands/status.js +31 -0
- package/dist/devices.js +66 -0
- package/dist/mm-harness-cli.js +28 -2
- package/docs/CLI-SPEC.md +35 -0
- package/package.json +2 -2
- package/scripts/completions.sh +0 -0
- package/scripts/install-completions.sh +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0 - 2026-07-07
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **`run <name>` — the recipe positional resolves packaged-library recipe names.** An existing file path (absolute or cwd-relative) wins; otherwise the arg is a library recipe name, adapter-scoped first (`run smoke` on mobile → `smoke.mobile.recipe.json`), then adapterless (`run perps-lifecycle`), then the exact library filename. A miss teaches `RECIPE_NOT_FOUND` naming the library recipes available for the adapter — no more `$LIB`-style path prefixes in docs or demo scripts. A directory shadowing a library name never wins, and path-shaped args (containing a separator) never fall through to the library.
|
|
7
|
+
- **`--device <udid|serial|name>` — first-class mobile device targeting on `run`, `call`, and `doctor`.** Resolves against connected devices (adb + booted simulators; exact id first, then exact name with an ambiguity teaching error) and sets the same env `launch --device` does. Mobile `run`/`call` **without** `--device` while more than one targetable device is connected (across android + ios; android state `device`, iOS `Booted`) fail fast listing the devices and the `--device` hint, so a recipe never lands on the wrong device silently. `run --plan`/`--list` are exempt; extension/core teach. Errors honor `--json` with structured envelopes.
|
|
8
|
+
- **`status` gains `devices[]`** (additive): `{platform, id, name, state, selected}` for connected android devices and booted iOS simulators on mobile checkouts — the `yarn a:status` successor; `selected` reflects the current env pinning.
|
|
9
|
+
|
|
10
|
+
## 0.7.5 - 2026-07-07
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **`launch --verify` converges duplicate extension home tabs instead of failing prepare forever.** A second bare `home.html` tab can appear beside the operator's slot-titled one outside the harness's control (the extension opens its own tab after the build-freeze reload), and the live verify health gate requires exactly one home tab — but nothing in launch/verify ever collapsed duplicates (`ensureExtensionReady` was only reachable via the retired `ensure-ready` verb), so every preflight retry recounted two tabs and failed. `launch --verify` (extension, with a CDP port) now runs the convergence before the live verify phase.
|
|
14
|
+
- **Convergence keeps the operator's tab.** With multiple home tabs, `ensureExtensionReady` now prunes the extras keeping the preferred tab — attached (another client is driving it) over custom slot title (`<slot-id> — MetaMask`, how operators tell which agent owns a browser) over first listed — instead of closing all and reopening fresh, which discarded the slot title. The deterministic close-all + fresh-open remains as a one-shot fallback when the kept tab fails the health check, and a failed re-listing during the fallback reports `cdp-unreachable` instead of a stale tab count.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- **`EnsureReadyAction` gains `pruned`** — extras were closed keeping the preferred tab; `reopened` now specifically means the pruned keeper failed the health check and one fresh tab was reopened.
|
|
18
|
+
|
|
3
19
|
## 0.7.4 - 2026-07-07
|
|
4
20
|
|
|
5
21
|
### Added
|
package/adapters/core/cleanup.sh
CHANGED
|
File without changes
|
package/adapters/core/inject.sh
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -47,6 +47,12 @@ function homePages(targets, extensionId) {
|
|
|
47
47
|
(t) => t.type === "page" && typeof t.url === "string" && t.url.startsWith(prefix) && t.url.includes("/home.html") && t.id
|
|
48
48
|
);
|
|
49
49
|
}
|
|
50
|
+
function pickHomeKeeper(homes) {
|
|
51
|
+
const attached = homes.filter((t) => typeof t.webSocketDebuggerUrl !== "string");
|
|
52
|
+
const pool = attached.length > 0 ? attached : homes;
|
|
53
|
+
const marked = pool.find((t) => typeof t.title === "string" && t.title.trim() !== "" && t.title.trim() !== "MetaMask");
|
|
54
|
+
return marked ?? pool[0];
|
|
55
|
+
}
|
|
50
56
|
async function ensureExtensionReady(target, options) {
|
|
51
57
|
const resolved = path.resolve(target);
|
|
52
58
|
const { cdpPort } = options;
|
|
@@ -91,14 +97,14 @@ async function ensureExtensionReady(target, options) {
|
|
|
91
97
|
opened = await openHome(cdpPort, extensionId) || opened;
|
|
92
98
|
await sleep(1500);
|
|
93
99
|
} else {
|
|
94
|
-
action = "
|
|
100
|
+
action = "pruned";
|
|
101
|
+
const keeper = pickHomeKeeper(homes);
|
|
95
102
|
for (const h of homes) {
|
|
103
|
+
if (h === keeper) continue;
|
|
96
104
|
await closeTab(cdpPort, String(h.id));
|
|
97
105
|
closed += 1;
|
|
98
106
|
}
|
|
99
107
|
await sleep(500);
|
|
100
|
-
opened = await openHome(cdpPort, extensionId) || opened;
|
|
101
|
-
await sleep(1500);
|
|
102
108
|
}
|
|
103
109
|
const next = await jsonList(cdpPort);
|
|
104
110
|
if (!next.ok) {
|
|
@@ -125,14 +131,35 @@ async function ensureExtensionReady(target, options) {
|
|
|
125
131
|
if (!finalListing.ok) {
|
|
126
132
|
return base({ extensionId, opened, action, homeTabs: { before, closed, after: homesNow.length }, reasonCode: "cdp-unreachable" });
|
|
127
133
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
let after = homePages(finalListing.targets, extensionId).length;
|
|
135
|
+
const checkHealth = async () => {
|
|
136
|
+
try {
|
|
137
|
+
const { checkExtensionRuntimeHealth } = await import("./runtime.js");
|
|
138
|
+
const report = await checkExtensionRuntimeHealth(resolved, cdpPort);
|
|
139
|
+
return { status: report.status, findings: report.findings };
|
|
140
|
+
} catch (error) {
|
|
141
|
+
return { status: "FAIL", findings: [error instanceof Error ? error.message : String(error)] };
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
let health = await checkHealth();
|
|
145
|
+
if (action === "pruned" && (health.status !== "PASS" || after !== 1)) {
|
|
146
|
+
const listing = await jsonList(cdpPort);
|
|
147
|
+
if (listing.ok) {
|
|
148
|
+
action = "reopened";
|
|
149
|
+
for (const h of homePages(listing.targets, extensionId)) {
|
|
150
|
+
await closeTab(cdpPort, String(h.id));
|
|
151
|
+
closed += 1;
|
|
152
|
+
}
|
|
153
|
+
await sleep(500);
|
|
154
|
+
opened = await openHome(cdpPort, extensionId) || opened;
|
|
155
|
+
await sleep(1500);
|
|
156
|
+
const relisted = await jsonList(cdpPort);
|
|
157
|
+
if (!relisted.ok) {
|
|
158
|
+
return base({ extensionId, opened, action, homeTabs: { before, closed, after }, reasonCode: "cdp-unreachable" });
|
|
159
|
+
}
|
|
160
|
+
after = homePages(relisted.targets, extensionId).length;
|
|
161
|
+
health = await checkHealth();
|
|
162
|
+
}
|
|
136
163
|
}
|
|
137
164
|
const ready = health.status === "PASS" && after === 1;
|
|
138
165
|
return base({
|
package/dist/cli-commands.js
CHANGED
|
@@ -15,8 +15,8 @@ const SPEC = {
|
|
|
15
15
|
{ name: "debug", aliases: ["devtools", "inspect"], desc: "Open DevTools UI", flags: ["--json", "--no-open"] },
|
|
16
16
|
{ name: "fixtures", desc: "Manage the canonical wallet fixture (sync/set/generate)", args: ["sync", "set", "generate"], flags: ["--fixture", "--out", "--adapter", "--target", "--json"] },
|
|
17
17
|
{ name: "actions", desc: "List runnable recipe actions", flags: ["--json"] },
|
|
18
|
-
{ name: "doctor", desc: "Check harness/orchestration health", flags: ["--json", "--target", "--adapter", "--runtime-dir", "--expect-live", "--cdp-port"] },
|
|
19
|
-
{ name: "run", desc: "Execute a proof recipe", args: ["recipe.json"], flags: ["--list"] },
|
|
18
|
+
{ name: "doctor", desc: "Check harness/orchestration health", flags: ["--json", "--target", "--adapter", "--runtime-dir", "--expect-live", "--cdp-port", "--device"] },
|
|
19
|
+
{ name: "run", desc: "Execute a proof recipe (path or library name, e.g. run smoke)", args: ["recipe.json|name"], flags: ["--list", "--device"] },
|
|
20
20
|
{ name: "recipe-quality", desc: "Build the recipe-quality artifact from compact JSON", args: ["build"], flags: ["--input", "--output", "--json"] },
|
|
21
21
|
{ name: "interactive", aliases: ["menu"], desc: "Interactive command menu" },
|
|
22
22
|
{ name: "prepare", desc: "Install harness (+ optional validate)", flags: ["--target", "--runtime-dir", "--json"] },
|
package/dist/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ import { handleLogs } from "./commands/logs.js";
|
|
|
18
18
|
import { handleDebug } from "./commands/debug.js";
|
|
19
19
|
import { handleFixtures } from "./commands/fixtures.js";
|
|
20
20
|
import { handleRecipeQuality } from "./commands/recipe-quality.js";
|
|
21
|
+
import { handleStatus } from "./commands/status.js";
|
|
21
22
|
import { parseArgs, targetPath } from "./commands/parse-args.js";
|
|
22
23
|
import { runOneNode } from "./commands/run-engine.js";
|
|
23
24
|
const COMMANDS = {
|
|
@@ -92,6 +93,9 @@ async function main(argv) {
|
|
|
92
93
|
usage();
|
|
93
94
|
return command ? 0 : 2;
|
|
94
95
|
}
|
|
96
|
+
if (command === "status" || command === "health" || command === "home") {
|
|
97
|
+
return handleStatus(parseArgs(argv.slice(1), "status"));
|
|
98
|
+
}
|
|
95
99
|
if (command === "flows") return handleFlows(argv.slice(1));
|
|
96
100
|
if (command === "call") return handleCall(argv.slice(1));
|
|
97
101
|
if (command === "completion-candidates") return handleCompletionCandidates(argv.slice(1));
|
package/dist/commands/call.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
usageError
|
|
16
16
|
} from "./parse-args.js";
|
|
17
17
|
import { handleListExecutables } from "./list-executables.js";
|
|
18
|
+
import { applyDeviceTargeting } from "./device-target.js";
|
|
18
19
|
import {
|
|
19
20
|
emitHealViolation,
|
|
20
21
|
executeWithHealBounds,
|
|
@@ -57,6 +58,12 @@ async function handleCall(argv) {
|
|
|
57
58
|
return EXIT.usage;
|
|
58
59
|
}
|
|
59
60
|
const { adapter, target } = resolveAdapter(options);
|
|
61
|
+
const dtResult = applyDeviceTargeting("call", adapter, options, { gate: true, rerun: "" });
|
|
62
|
+
if ("code" in dtResult) {
|
|
63
|
+
if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, error: { code: dtResult.code, message: dtResult.message } }, null, 2));
|
|
64
|
+
else console.error(`\u2717 call: ${dtResult.message}`);
|
|
65
|
+
return EXIT.usage;
|
|
66
|
+
}
|
|
60
67
|
if (recipeRunning(target)) {
|
|
61
68
|
const msg = "a recipe is currently running \u2014 refusing to start while another recipe executes.";
|
|
62
69
|
if (json) {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { listConnectedDevices } from "../devices.js";
|
|
2
|
+
import { optionString } from "./parse-args.js";
|
|
3
|
+
function setAndroidDeviceEnv(id) {
|
|
4
|
+
process.env.ADB_SERIAL = id;
|
|
5
|
+
process.env.ANDROID_SERIAL = id;
|
|
6
|
+
process.env.ANDROID_DEVICE = id;
|
|
7
|
+
}
|
|
8
|
+
function setIosDeviceEnv(id) {
|
|
9
|
+
process.env.IOS_SIMULATOR = id;
|
|
10
|
+
}
|
|
11
|
+
function formatConnectedDevices(devices) {
|
|
12
|
+
return devices.map((d) => ` - ${d.id}${d.name ? ` (${d.name})` : ""} [${d.state}] ${d.platform}`).join("\n");
|
|
13
|
+
}
|
|
14
|
+
function renderDeviceList(devices, out) {
|
|
15
|
+
console.log(
|
|
16
|
+
`${out("label", "devices:")} ${devices.length === 0 ? out("dim", "none connected (adb devices / booted iOS simulators)") : `${devices.length} connected`}`
|
|
17
|
+
);
|
|
18
|
+
for (const device of devices) {
|
|
19
|
+
const marker = device.selected ? out("ok", "\u25CF selected") : out("dim", "\u25CB");
|
|
20
|
+
console.log(
|
|
21
|
+
` ${marker} ${device.id}${device.name ? ` (${device.name})` : ""} ${out("dim", `[${device.state}] ${device.platform}`)}`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function deviceSelected(device) {
|
|
26
|
+
if (device.platform === "android") {
|
|
27
|
+
return process.env.ADB_SERIAL === device.id || process.env.ANDROID_SERIAL === device.id;
|
|
28
|
+
}
|
|
29
|
+
return process.env.IOS_SIMULATOR === device.id;
|
|
30
|
+
}
|
|
31
|
+
function isTargetable(device) {
|
|
32
|
+
if (device.platform === "android") return device.state === "device";
|
|
33
|
+
if (device.platform === "ios") return device.state === "Booted";
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
function applyDeviceTargeting(command, adapter, options, opts) {
|
|
37
|
+
const device = optionString(options, "device");
|
|
38
|
+
if (adapter !== "mobile") {
|
|
39
|
+
if (device !== void 0) {
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
code: "DEVICE_WRONG_ADAPTER",
|
|
43
|
+
message: `--device is only supported on the mobile adapter (the ${adapter} adapter has no device to target).
|
|
44
|
+
Drop --device, or run this ${command} inside a metamask-mobile checkout.`
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return { ok: true };
|
|
48
|
+
}
|
|
49
|
+
if (device !== void 0) {
|
|
50
|
+
const connected2 = listConnectedDevices();
|
|
51
|
+
const androidById = connected2.find((d) => d.platform === "android" && d.id === device);
|
|
52
|
+
if (androidById) {
|
|
53
|
+
setAndroidDeviceEnv(device);
|
|
54
|
+
return { ok: true };
|
|
55
|
+
}
|
|
56
|
+
const iosById = connected2.find((d) => d.platform === "ios" && d.id === device);
|
|
57
|
+
if (iosById) {
|
|
58
|
+
setIosDeviceEnv(device);
|
|
59
|
+
return { ok: true };
|
|
60
|
+
}
|
|
61
|
+
const byName = connected2.filter((d) => d.name === device);
|
|
62
|
+
if (byName.length === 1) {
|
|
63
|
+
const matched = byName[0];
|
|
64
|
+
if (matched.platform === "android") setAndroidDeviceEnv(matched.id);
|
|
65
|
+
else setIosDeviceEnv(matched.id);
|
|
66
|
+
return { ok: true };
|
|
67
|
+
}
|
|
68
|
+
if (byName.length > 1) {
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
code: "DEVICE_NAME_AMBIGUOUS",
|
|
72
|
+
message: `device name '${device}' is ambiguous; use the id:
|
|
73
|
+
` + byName.map((d) => ` - ${d.id}${d.name ? ` (${d.name})` : ""} [${d.state}] ${d.platform}`).join("\n")
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
code: "DEVICE_NOT_FOUND",
|
|
79
|
+
message: `--device ${device} did not match any connected device.
|
|
80
|
+
` + (connected2.length > 0 ? ` Connected devices:
|
|
81
|
+
${formatConnectedDevices(connected2)}` : ` No devices are currently connected (adb devices / booted iOS simulators).`)
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (!opts.gate) return { ok: true };
|
|
85
|
+
const connected = listConnectedDevices();
|
|
86
|
+
const targetable = connected.filter(isTargetable);
|
|
87
|
+
if (targetable.length > 1) {
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
code: "DEVICE_AMBIGUOUS",
|
|
91
|
+
message: `${targetable.length} mobile devices available \u2014 ${command} needs exactly one target.
|
|
92
|
+
Connected devices:
|
|
93
|
+
${formatConnectedDevices(connected)}
|
|
94
|
+
Add --device <id> to disambiguate, e.g.:${targetable.map((d) => `
|
|
95
|
+
--device ${d.id}`).join("")}`
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return { ok: true };
|
|
99
|
+
}
|
|
100
|
+
export {
|
|
101
|
+
applyDeviceTargeting,
|
|
102
|
+
deviceSelected,
|
|
103
|
+
formatConnectedDevices,
|
|
104
|
+
renderDeviceList
|
|
105
|
+
};
|
package/dist/commands/doctor.js
CHANGED
|
@@ -7,6 +7,8 @@ import { assertAdapter, runnerDir } from "../paths.js";
|
|
|
7
7
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
8
8
|
import { loadActionManifest, validateManifest } from "../manifest.js";
|
|
9
9
|
import { ensureOverlay, newHealState, recipeRunning } from "../heal-bounds.js";
|
|
10
|
+
import { listConnectedDevices } from "../devices.js";
|
|
11
|
+
import { applyDeviceTargeting, deviceSelected, renderDeviceList } from "./device-target.js";
|
|
10
12
|
import { ADAPTER_DETECT_NEXT, EXIT, usageOut } from "./shared.js";
|
|
11
13
|
import {
|
|
12
14
|
actionManifestPathOption,
|
|
@@ -39,6 +41,12 @@ async function handleDoctor({ options }) {
|
|
|
39
41
|
return usageOut(json, "doctor", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
40
42
|
}
|
|
41
43
|
assertAdapter(adapter);
|
|
44
|
+
const dtResult = applyDeviceTargeting("doctor", adapter, options, { gate: false, rerun: "" });
|
|
45
|
+
if ("code" in dtResult) {
|
|
46
|
+
if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "doctor", adapter, target, error: { code: dtResult.code, message: dtResult.message } }, null, 2));
|
|
47
|
+
else console.error(`\u2717 doctor: ${dtResult.message}`);
|
|
48
|
+
return EXIT.usage;
|
|
49
|
+
}
|
|
42
50
|
const actionManifestPath = actionManifestPathOption(options, adapter);
|
|
43
51
|
const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
|
|
44
52
|
const manifestValidation = await validateManifest(manifest);
|
|
@@ -60,8 +68,9 @@ async function handleDoctor({ options }) {
|
|
|
60
68
|
}
|
|
61
69
|
const orphanMetros = adapter === "mobile" ? detectOrphanMetros(target, process.env.WATCHER_PORT) : [];
|
|
62
70
|
const capture = captureHelperHealth();
|
|
63
|
-
|
|
64
|
-
if (
|
|
71
|
+
const devices = adapter === "mobile" ? enumerateSelectedDevices() : [];
|
|
72
|
+
if (expectLive) return emitExpectLive(adapter, target, runtime, result, orphanMetros, capture, devices, json);
|
|
73
|
+
if (json) console.log(JSON.stringify({ ...result, runtime, orphanMetros, capture, devices }, null, 2));
|
|
65
74
|
else {
|
|
66
75
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
67
76
|
const stateStyle = (value, good) => value === good ? "ok" : "warn";
|
|
@@ -91,13 +100,17 @@ async function handleDoctor({ options }) {
|
|
|
91
100
|
console.log(` ${out("dim", "Next: grant Screen Recording (System Settings \u2192 Privacy & Security \u2192 Screen Recording), or run: capture-helper doctor --open-permissions")}`);
|
|
92
101
|
}
|
|
93
102
|
}
|
|
103
|
+
if (adapter === "mobile") renderDeviceList(devices, out);
|
|
94
104
|
}
|
|
95
105
|
return result.status === "pass" ? 0 : 1;
|
|
96
106
|
}
|
|
97
|
-
function
|
|
107
|
+
function enumerateSelectedDevices() {
|
|
108
|
+
return listConnectedDevices().map((device) => ({ ...device, selected: deviceSelected(device) }));
|
|
109
|
+
}
|
|
110
|
+
function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture, devices, json) {
|
|
98
111
|
const live = runtime?.decision === "ready";
|
|
99
112
|
if (json) {
|
|
100
|
-
console.log(JSON.stringify({ ...result, runtime, orphanMetros, capture }, null, 2));
|
|
113
|
+
console.log(JSON.stringify({ ...result, runtime, orphanMetros, capture, devices }, null, 2));
|
|
101
114
|
return live ? EXIT.ok : EXIT.runtime;
|
|
102
115
|
}
|
|
103
116
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
@@ -104,6 +104,14 @@ async function handleLaunch(argv) {
|
|
|
104
104
|
});
|
|
105
105
|
}
|
|
106
106
|
if (tier === "verify") {
|
|
107
|
+
const verifyCdpPort = Number(process.env.CDP_PORT ?? "");
|
|
108
|
+
if (adapter === "extension" && Number.isInteger(verifyCdpPort) && verifyCdpPort > 0) {
|
|
109
|
+
const { ensureExtensionReady } = await import("../../adapters/extension/ensure-ready.js");
|
|
110
|
+
const convergence = await ensureExtensionReady(target, { cdpPort: verifyCdpPort });
|
|
111
|
+
console.error(
|
|
112
|
+
`[launch] home-tab convergence: ${convergence.action} (${convergence.homeTabs.before}\u2192${convergence.homeTabs.after} tabs, closed ${convergence.homeTabs.closed})${convergence.ready ? "" : ` \u2014 ${convergence.reasonCode}`}`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
107
115
|
const liveArgs = ["live", "--adapter", adapter, "--target", target];
|
|
108
116
|
if (adapter === "mobile" && mobileTarget) liveArgs.push("--platform", mobileTarget);
|
|
109
117
|
if (json) liveArgs.push("--json");
|
|
@@ -14,6 +14,7 @@ import { getAdapterSurface } from "../adapters/surface.js";
|
|
|
14
14
|
import {
|
|
15
15
|
importRecipeHarness,
|
|
16
16
|
importRecipeProtocol,
|
|
17
|
+
recipePath,
|
|
17
18
|
runnerDir
|
|
18
19
|
} from "../paths.js";
|
|
19
20
|
import { captureHelperSupportsRecordSessionSnapshots } from "../recording-target.js";
|
|
@@ -131,11 +132,47 @@ async function validateRecipeAdapterAware(recipe, manifest, librarySources) {
|
|
|
131
132
|
const warnings = findings.length - errors;
|
|
132
133
|
return { status: errors > 0 ? "invalid" : "valid", findings, summary: { errors, warnings } };
|
|
133
134
|
}
|
|
135
|
+
function isRecipeFile(p) {
|
|
136
|
+
try {
|
|
137
|
+
return fs.statSync(p).isFile();
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function resolveRunRecipeArg(recipeArg, adapter) {
|
|
143
|
+
const direct = path.resolve(recipeArg);
|
|
144
|
+
if (isRecipeFile(direct)) return { recipeFile: direct };
|
|
145
|
+
if (!recipeArg.includes("/") && !recipeArg.includes(path.sep)) {
|
|
146
|
+
for (const name of [`${recipeArg}.${adapter}.recipe.json`, `${recipeArg}.recipe.json`, recipeArg]) {
|
|
147
|
+
const file = recipePath(name);
|
|
148
|
+
if (isRecipeFile(file)) return { recipeFile: file };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const names = libraryRecipeNames(adapter);
|
|
152
|
+
return {
|
|
153
|
+
notFound: `recipe not found: ${recipeArg} \u2014 not a file, and no packaged library recipe matched. ` + (names.length > 0 ? `Library recipes for ${adapter}: ${names.join(", ")} (mm-harness run <name>).` : `The packaged library has no recipes for ${adapter}.`)
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function libraryRecipeNames(adapter) {
|
|
157
|
+
let entries;
|
|
158
|
+
try {
|
|
159
|
+
entries = fs.readdirSync(recipePath(""));
|
|
160
|
+
} catch {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
const scopes = ["mobile", "extension", "core"];
|
|
164
|
+
const names = entries.filter((f) => f.endsWith(".recipe.json")).map((f) => f.slice(0, -".recipe.json".length)).filter((n) => {
|
|
165
|
+
const scope = n.split(".").pop() ?? "";
|
|
166
|
+
return !scopes.includes(scope) || scope === adapter;
|
|
167
|
+
}).map((n) => n.endsWith(`.${adapter}`) ? n.slice(0, -(adapter.length + 1)) : n);
|
|
168
|
+
return [...new Set(names)].sort();
|
|
169
|
+
}
|
|
134
170
|
async function validateRunRecipeStatic(recipeArg, adapter, options) {
|
|
135
|
-
const
|
|
171
|
+
const resolved = resolveRunRecipeArg(recipeArg, adapter);
|
|
172
|
+
const recipeFile = "recipeFile" in resolved ? resolved.recipeFile : path.resolve(recipeArg);
|
|
136
173
|
const empty = { recipe: void 0, recipeFile, findings: [], errorCount: 0, manifestOk: false, schemaValid: false };
|
|
137
|
-
if (
|
|
138
|
-
return { ...empty, usageError: { code: "RECIPE_NOT_FOUND", message:
|
|
174
|
+
if ("notFound" in resolved) {
|
|
175
|
+
return { ...empty, usageError: { code: "RECIPE_NOT_FOUND", message: resolved.notFound } };
|
|
139
176
|
}
|
|
140
177
|
let recipe;
|
|
141
178
|
try {
|
|
@@ -333,6 +370,7 @@ export {
|
|
|
333
370
|
executeWithHealBounds,
|
|
334
371
|
prepareHeal,
|
|
335
372
|
resolveMetaMaskLibrarySources,
|
|
373
|
+
resolveRunRecipeArg,
|
|
336
374
|
runOneNode,
|
|
337
375
|
runRecipe,
|
|
338
376
|
synthesizeOneNodeRecipe,
|
package/dist/commands/run.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
validateRunRecipeStatic
|
|
21
21
|
} from "./run-engine.js";
|
|
22
22
|
import { handleListExecutables } from "./list-executables.js";
|
|
23
|
+
import { applyDeviceTargeting } from "./device-target.js";
|
|
23
24
|
async function handleRun({ positional, options }) {
|
|
24
25
|
if (optionFlag(options, "list")) return handleListExecutables("run", options);
|
|
25
26
|
const targetRecipe = positional[0];
|
|
@@ -27,6 +28,10 @@ async function handleRun({ positional, options }) {
|
|
|
27
28
|
if (optionFlag(options, "plan")) return handleRunPlan(targetRecipe, options);
|
|
28
29
|
const { adapter, target } = resolveAdapter(options);
|
|
29
30
|
const json = optionFlag(options, "json");
|
|
31
|
+
const dtResult = applyDeviceTargeting("run", adapter, options, { gate: true, rerun: "" });
|
|
32
|
+
if ("code" in dtResult) {
|
|
33
|
+
return emitRunUsageError(json, adapter, targetRecipe, dtResult.code, dtResult.message);
|
|
34
|
+
}
|
|
30
35
|
const artifactsDir = requiredOption(options, "artifactsDir", "run requires --artifacts-dir <dir>.");
|
|
31
36
|
const prepared = await prepareHeal(adapter, target, options, json);
|
|
32
37
|
if (typeof prepared === "number") return prepared;
|
|
@@ -45,7 +50,9 @@ async function handleRun({ positional, options }) {
|
|
|
45
50
|
stdoutIsMachineContract: json
|
|
46
51
|
};
|
|
47
52
|
const { result, violation } = await executeWithHealBounds(
|
|
48
|
-
|
|
53
|
+
// validated.recipeFile, not the raw arg: the arg may be a library recipe NAME
|
|
54
|
+
// that only the resolver knows how to turn into a file.
|
|
55
|
+
() => runRecipe(adapter, validated.recipeFile, artifactsDir, target, optionString(options, "actionManifest"), runtimeOptions),
|
|
49
56
|
adapter,
|
|
50
57
|
target,
|
|
51
58
|
heal,
|
|
@@ -0,0 +1,31 @@
|
|
|
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 { ADAPTER_DETECT_NEXT, EXIT, usageOut } from "./shared.js";
|
|
8
|
+
import { optionFlag, targetPath } from "./parse-args.js";
|
|
9
|
+
async function handleStatus({ options }) {
|
|
10
|
+
const target = targetPath(options);
|
|
11
|
+
const json = optionFlag(options, "json");
|
|
12
|
+
const adapter = detectAdapter(target);
|
|
13
|
+
if (!adapter) {
|
|
14
|
+
return usageOut(json, "status", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
15
|
+
}
|
|
16
|
+
assertAdapter(adapter);
|
|
17
|
+
const devices = adapter === "mobile" ? listConnectedDevices().map((device) => ({ ...device, selected: deviceSelected(device) })) : [];
|
|
18
|
+
const next = getAdapterSurface(adapter).hints.relaunch;
|
|
19
|
+
if (json) {
|
|
20
|
+
console.log(JSON.stringify({ schemaVersion: 1, command: "status", adapter, target, devices, next }, null, 2));
|
|
21
|
+
return EXIT.ok;
|
|
22
|
+
}
|
|
23
|
+
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
24
|
+
console.log(`${out("label", "status")} ${out("bold", adapter)} ${out("dim", target)}`);
|
|
25
|
+
if (adapter === "mobile") renderDeviceList(devices, out);
|
|
26
|
+
console.log(`${out("label", "Next:")} ${out("cmd", next)}`);
|
|
27
|
+
return EXIT.ok;
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
handleStatus
|
|
31
|
+
};
|
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,27 @@ 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 which one is the selected
|
|
22
|
+
target (matches ADB_SERIAL / IOS_SIMULATOR). Aliases: health, home.
|
|
23
|
+
|
|
24
|
+
--target <path> Checkout path (default: cwd)
|
|
25
|
+
--json Machine-readable envelope { adapter, target, devices[], next }
|
|
26
|
+
|
|
27
|
+
The devices[] section carries { platform, id, name, state, selected } per device;
|
|
28
|
+
extension/core checkouts report an empty list (no device surface).
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
mm-harness status
|
|
32
|
+
mm-harness status --json`
|
|
33
|
+
},
|
|
13
34
|
{
|
|
14
35
|
name: "actions",
|
|
15
36
|
summary: "List the action vocabulary + field schemas (--raw dumps the raw action registry JSON).",
|
|
@@ -64,6 +85,7 @@ Example:
|
|
|
64
85
|
|
|
65
86
|
--list List everything invocable for the adapter (actions + flows); no <action> needed
|
|
66
87
|
--arg k=v Action field value (repeatable)
|
|
88
|
+
--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
89
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
68
90
|
--target <path> Checkout path (default: cwd)
|
|
69
91
|
--artifacts-dir <dir> Where to write evidence (default: temp dir)
|
|
@@ -103,6 +125,7 @@ Example:
|
|
|
103
125
|
|
|
104
126
|
--list List everything invocable for the adapter (actions + flows); no <recipe> needed
|
|
105
127
|
--plan Validate + print execution plan, touching nothing. Exit 5 if invalid.
|
|
128
|
+
--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
129
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
107
130
|
--target <path> Checkout path (default: cwd)
|
|
108
131
|
--artifacts-dir <dir> Where to write evidence (required unless --plan)
|
|
@@ -128,6 +151,7 @@ Example:
|
|
|
128
151
|
--fix Repair the overlay/runtime-context WITHOUT launching (no fixture reseed); --json adds fixed[]/failed[]
|
|
129
152
|
--expect-live Exit 0 iff the runtime is live (extension: watcher+CDP; mobile: Metro+bridge; core: deps), non-zero + teaching escape otherwise
|
|
130
153
|
--cdp-port <port> Extension CDP port for the liveness probe (env: CDP_PORT / RECIPE_CDP_PORT)
|
|
154
|
+
--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
155
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
132
156
|
--target <path> Checkout path (default: cwd)
|
|
133
157
|
--runtime-dir <dir> Runtime dir containing agentic-runtime.json (relative to target)
|
|
@@ -417,7 +441,7 @@ const HELP_GROUPS = [
|
|
|
417
441
|
{
|
|
418
442
|
title: "DAILY LOOP",
|
|
419
443
|
blurb: "what a teammate runs many times a day (auto-ensures the overlay; --heal owns recovery)",
|
|
420
|
-
commands: ["launch", "stop", "logs", "debug", "fixtures"]
|
|
444
|
+
commands: ["status", "launch", "stop", "logs", "debug", "fixtures"]
|
|
421
445
|
},
|
|
422
446
|
{
|
|
423
447
|
title: "DISCOVER",
|
|
@@ -530,7 +554,9 @@ const pkgVersion = (() => {
|
|
|
530
554
|
const program = new Command();
|
|
531
555
|
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
556
|
for (const command of REAL) {
|
|
533
|
-
program.command(command.name).description(command.summary).allowUnknownOption().helpOption("-h, --help", "Show command help")
|
|
557
|
+
const registered = program.command(command.name).description(command.summary).allowUnknownOption().helpOption("-h, --help", "Show command help");
|
|
558
|
+
if (command.aliases?.length) registered.aliases(command.aliases);
|
|
559
|
+
registered.configureHelp({ formatHelp: () => `${command.helpText}
|
|
534
560
|
` }).argument("[args...]").action(async () => {
|
|
535
561
|
if (command.name === "update") {
|
|
536
562
|
process.exit(await handleUpdate(rawArgv.slice(1)));
|
package/docs/CLI-SPEC.md
CHANGED
|
@@ -437,6 +437,41 @@ 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. Extension/core checkouts report an empty `devices[]` (CDP tab detail is out of scope). Additive envelope:
|
|
454
|
+
|
|
455
|
+
```json
|
|
456
|
+
{
|
|
457
|
+
"schemaVersion": 1,
|
|
458
|
+
"command": "status",
|
|
459
|
+
"adapter": "mobile",
|
|
460
|
+
"target": "/path/to/checkout",
|
|
461
|
+
"devices": [
|
|
462
|
+
{ "platform": "android", "id": "emulator-5554", "name": "Pixel_6", "state": "device", "selected": true },
|
|
463
|
+
{ "platform": "ios", "id": "AAAA-BBBB", "name": "iPhone 15", "state": "Booted", "selected": false }
|
|
464
|
+
],
|
|
465
|
+
"next": "mm-harness launch ios"
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
`selected` is `true` when the device id matches the current `ADB_SERIAL`/`ANDROID_SERIAL` (android) or `IOS_SIMULATOR` (ios) env. `doctor --json` carries the same `devices[]` shape (with `selected`) for mobile checkouts.
|
|
470
|
+
|
|
471
|
+
**Exit:** 0 (2 when the repo type cannot be detected).
|
|
472
|
+
|
|
473
|
+
---
|
|
474
|
+
|
|
440
475
|
## DISCOVER — agent composition input
|
|
441
476
|
|
|
442
477
|
> **Decided (Arthur):** DISCOVER is a distinct group, not a sub-PROVE. The audience
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deeeed/metamask-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mm-harness": "bin/mm-harness"
|
|
@@ -69,4 +69,4 @@
|
|
|
69
69
|
"url": "https://github.com/MetaMask/experimental-metamask-harness/issues"
|
|
70
70
|
},
|
|
71
71
|
"homepage": "https://github.com/MetaMask/experimental-metamask-harness#readme"
|
|
72
|
-
}
|
|
72
|
+
}
|
package/scripts/completions.sh
CHANGED
|
File without changes
|
|
File without changes
|