@deeeed/metamask-harness 0.26.5 → 0.28.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 +17 -0
- package/adapters/mobile/open-device.sh +16 -4
- package/dist/adapters/mobile/prepare.js +114 -4
- package/dist/adapters/mobile/runtime-decision.js +4 -1
- package/dist/adapters.js +8 -6
- package/dist/cli-commands.js +1 -1
- package/dist/command-contract.js +1 -0
- package/dist/commands/call.js +38 -1
- package/dist/commands/device-target.js +4 -1
- package/dist/commands/doctor.js +28 -2
- package/dist/commands/manifest.js +201 -6
- package/dist/commands/mobile-device-view.js +4 -1
- package/dist/commands/parse-args.js +1 -0
- package/dist/commands/run.js +74 -8
- package/dist/devices.js +4 -1
- package/dist/doctor.js +25 -2
- package/dist/mm-harness-cli.js +2 -0
- package/dist/recipe-security.js +1 -0
- package/dist/run-recording.js +128 -92
- package/library/actions/extension/platform/cdp.mjs +170 -117
- package/library/actions/extension/ui/locators.mjs +13 -0
- package/library/actions/mobile/platform/bridge.mjs +14 -5
- package/library/actions/mobile/platform/observe-ui.mjs +437 -0
- package/library/actions/mobile/platform/tool-paths.mjs +122 -0
- package/library/actions/mobile/ui/locators.mjs +17 -0
- package/library/actions/shared/analytics/collector.mjs +50 -5
- package/library/actions/shared/ui/locators.mjs +160 -0
- package/library/manifests/extension.action-manifest.json +18 -0
- package/library/manifests/mobile.action-manifest.json +43 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.28.0 - 2026-07-31
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Add `actions --matrix` and adapter-aware action refusals that name missing capabilities and list the adapters that satisfy them.
|
|
10
|
+
- Add `ui.locators` for ranked, copy-pasteable recipe targets derived from the current Extension or Mobile `ui.visible` observation.
|
|
11
|
+
|
|
12
|
+
## 0.27.0 - 2026-07-31
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Mobile UI actions now record bounded native-hierarchy observations, while defaulted Extension UI actions record screen and visible observations; Extension screenshots and recordings publish through symlink-safe private staging as regular `0600` files, Mobile launch and diagnostics resolve absolute device-tool paths, and iOS preparation enables application accessibility before relaunching.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Serve analytics capture on both IPv4 and IPv6 loopback so Mobile clients resolving `localhost` to `::1` can reach the collector without exposing it on external interfaces.
|
|
21
|
+
|
|
5
22
|
## 0.26.5 - 2026-07-31
|
|
6
23
|
|
|
7
24
|
### Fixed
|
|
@@ -25,6 +25,7 @@ PLATFORM="ios"
|
|
|
25
25
|
TARGET="$PWD"
|
|
26
26
|
SIMULATOR="${IOS_SIMULATOR:-${SIM_UDID:-booted}}"
|
|
27
27
|
ADB_SERIAL_ARG="${ADB_SERIAL:-${ANDROID_SERIAL:-}}"
|
|
28
|
+
ADB_BIN="${MM_HARNESS_ADB_PATH:-}"
|
|
28
29
|
PREFLIGHT_MODE="${MOBILE_PREFLIGHT_MODE:-fast}"
|
|
29
30
|
PORT="${WATCHER_PORT:-${METRO_PORT:-8081}}"
|
|
30
31
|
UI_ONLY=false
|
|
@@ -54,6 +55,17 @@ case "$PLATFORM" in
|
|
|
54
55
|
ios|android) ;;
|
|
55
56
|
*) printf 'open-device: --platform must be ios or android (got: %s)\n' "$PLATFORM" >&2; exit 2 ;;
|
|
56
57
|
esac
|
|
58
|
+
if [ "$PLATFORM" = "android" ]; then
|
|
59
|
+
if [ -z "$ADB_BIN" ]; then
|
|
60
|
+
ADB_BIN="$(command -v adb 2>/dev/null || true)"
|
|
61
|
+
fi
|
|
62
|
+
if [ -z "$ADB_BIN" ] || [ "${ADB_BIN#/}" = "$ADB_BIN" ] || [ ! -x "$ADB_BIN" ]; then
|
|
63
|
+
printf 'open-device: Android SDK Platform-Tools (adb) is required but no executable was found.\n' >&2
|
|
64
|
+
printf 'Next: brew install android-platform-tools\n' >&2
|
|
65
|
+
exit 1
|
|
66
|
+
fi
|
|
67
|
+
export MM_HARNESS_ADB_PATH="$ADB_BIN"
|
|
68
|
+
fi
|
|
57
69
|
if $UI_ONLY && [ "$PLATFORM" != "ios" ]; then
|
|
58
70
|
printf 'open-device: --ui-only is supported only for ios.\n' >&2
|
|
59
71
|
exit 2
|
|
@@ -207,7 +219,7 @@ focus_simulator() {
|
|
|
207
219
|
android_app_installed() {
|
|
208
220
|
local pkg="$1"
|
|
209
221
|
shift
|
|
210
|
-
|
|
222
|
+
"$ADB_BIN" "$@" shell pm list packages 2>/dev/null | grep -q "package:${pkg}"
|
|
211
223
|
}
|
|
212
224
|
|
|
213
225
|
run_android_native_build() {
|
|
@@ -315,14 +327,14 @@ else
|
|
|
315
327
|
[ -n "$ADB_SERIAL_ARG" ] && ADB_ARGS+=("-s" "$ADB_SERIAL_ARG")
|
|
316
328
|
|
|
317
329
|
# Reverse-tunnel Metro port so the device can reach localhost:PORT.
|
|
318
|
-
|
|
330
|
+
"$ADB_BIN" "${ADB_ARGS[@]}" reverse "tcp:${PORT}" "tcp:${PORT}" >/dev/null 2>&1 || \
|
|
319
331
|
printf 'open-device: adb reverse failed (non-fatal — device may already be reachable)\n' >&2
|
|
320
332
|
|
|
321
333
|
LAUNCHED=false
|
|
322
334
|
for PKG in "${ANDROID_PKGS[@]}"; do
|
|
323
335
|
ensure_android_app_for_mode "$PKG" "$PREFLIGHT_MODE" "${ADB_ARGS[@]}" || continue
|
|
324
336
|
if $RESTART; then
|
|
325
|
-
|
|
337
|
+
"$ADB_BIN" "${ADB_ARGS[@]}" shell am force-stop "$PKG" >/dev/null 2>&1 || true
|
|
326
338
|
printf 'Restarting Android dev client %s\n' "$PKG" >&2
|
|
327
339
|
fi
|
|
328
340
|
|
|
@@ -331,7 +343,7 @@ else
|
|
|
331
343
|
DEEP_LINK="expo-metamask://expo-development-client/?url=${ENCODED_URL}"
|
|
332
344
|
|
|
333
345
|
printf 'Launching Android dev client %s\n' "$PKG" >&2
|
|
334
|
-
if
|
|
346
|
+
if "$ADB_BIN" "${ADB_ARGS[@]}" shell am start \
|
|
335
347
|
-a android.intent.action.VIEW \
|
|
336
348
|
-d "$DEEP_LINK" >/dev/null 2>&1; then
|
|
337
349
|
LAUNCHED=true
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { recordDepsBaseline } from "@farmslot/recipe-harness/runtime/deps-readiness";
|
|
4
|
+
import { resolveAndExportMobileToolPaths } from "../../../library/actions/mobile/platform/tool-paths.mjs";
|
|
3
5
|
import { EXIT, spawnScriptStreaming } from "../../commands/shared.js";
|
|
4
6
|
import { runnerDir } from "../../paths.js";
|
|
5
7
|
import {
|
|
@@ -33,6 +35,14 @@ async function prepareMobile(target, opts = {}) {
|
|
|
33
35
|
const preflightMode = opts.preflightMode ?? "fast";
|
|
34
36
|
const clearMetro = opts.clearMetro ?? false;
|
|
35
37
|
const restartApp = opts.restartApp ?? false;
|
|
38
|
+
try {
|
|
39
|
+
resolveAndExportMobileToolPaths(platform);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
42
|
+
if (!json) process.stderr.write(`${message}
|
|
43
|
+
`);
|
|
44
|
+
return { status: EXIT.runtime, output: message };
|
|
45
|
+
}
|
|
36
46
|
const report = await decideMobileReadiness(target, {
|
|
37
47
|
watcherPort: opts.watcherPort,
|
|
38
48
|
metroLog: opts.metroLog,
|
|
@@ -49,8 +59,9 @@ async function prepareMobile(target, opts = {}) {
|
|
|
49
59
|
`);
|
|
50
60
|
return { status: EXIT.runtime, output: msg };
|
|
51
61
|
}
|
|
62
|
+
const iosAccessibilityChanged = platform === "ios" && report.decision !== "unknown" && enableIosAccessibility(json);
|
|
52
63
|
if (report.decision === "ready") {
|
|
53
|
-
if (restartApp) {
|
|
64
|
+
if (restartApp || iosAccessibilityChanged) {
|
|
54
65
|
const launch2 = await dispatchActionSequence(
|
|
55
66
|
withAppRestart(launchActions(path.resolve(target)), true),
|
|
56
67
|
target,
|
|
@@ -91,7 +102,10 @@ async function prepareMobile(target, opts = {}) {
|
|
|
91
102
|
);
|
|
92
103
|
}
|
|
93
104
|
const launch = await dispatchActionSequence(
|
|
94
|
-
withAppRestart(
|
|
105
|
+
withAppRestart(
|
|
106
|
+
launchActions(path.resolve(target)),
|
|
107
|
+
restartApp || iosAccessibilityChanged
|
|
108
|
+
),
|
|
95
109
|
target,
|
|
96
110
|
platform,
|
|
97
111
|
json,
|
|
@@ -110,7 +124,7 @@ async function prepareMobile(target, opts = {}) {
|
|
|
110
124
|
clearMetro ? report.actions.map(
|
|
111
125
|
(a) => a.id === "start-metro" && !a.argv?.includes("--clear") ? { ...a, argv: [...a.argv ?? [], "--clear"] } : a
|
|
112
126
|
) : report.actions,
|
|
113
|
-
restartApp
|
|
127
|
+
restartApp || iosAccessibilityChanged
|
|
114
128
|
);
|
|
115
129
|
const actionResult = await dispatchActionSequence(
|
|
116
130
|
actions,
|
|
@@ -158,7 +172,10 @@ async function prepareMobile(target, opts = {}) {
|
|
|
158
172
|
}
|
|
159
173
|
case "launch": {
|
|
160
174
|
const result = await dispatchActionSequence(
|
|
161
|
-
withAppRestart(
|
|
175
|
+
withAppRestart(
|
|
176
|
+
postInstall.actions,
|
|
177
|
+
restartApp || iosAccessibilityChanged
|
|
178
|
+
),
|
|
162
179
|
target,
|
|
163
180
|
platform,
|
|
164
181
|
json,
|
|
@@ -240,6 +257,99 @@ async function dispatchActionSequence(requestedActions, target, platform, json,
|
|
|
240
257
|
}
|
|
241
258
|
return { status: 0, output: "" };
|
|
242
259
|
}
|
|
260
|
+
function enableIosAccessibility(json) {
|
|
261
|
+
const simulator = process.env.SIM_UDID || process.env.IOS_SIMULATOR || "booted";
|
|
262
|
+
const defaultsArgs = [
|
|
263
|
+
"simctl",
|
|
264
|
+
"spawn",
|
|
265
|
+
simulator,
|
|
266
|
+
"defaults"
|
|
267
|
+
];
|
|
268
|
+
try {
|
|
269
|
+
execFileSync("xcrun", ["simctl", "boot", simulator], {
|
|
270
|
+
stdio: "ignore",
|
|
271
|
+
timeout: 15e3
|
|
272
|
+
});
|
|
273
|
+
} catch {
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
execFileSync("xcrun", ["simctl", "bootstatus", simulator, "-b"], {
|
|
277
|
+
stdio: "ignore",
|
|
278
|
+
timeout: 12e4
|
|
279
|
+
});
|
|
280
|
+
} catch (error) {
|
|
281
|
+
if (!json) {
|
|
282
|
+
process.stderr.write(
|
|
283
|
+
`mobile prepare: selected iOS simulator was unavailable (${error instanceof Error ? error.message : String(error)}); continuing with native-view fallback.
|
|
284
|
+
`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
const current = execFileSync(
|
|
291
|
+
"xcrun",
|
|
292
|
+
[...defaultsArgs, "read", "com.apple.Accessibility", "ApplicationAccessibilityEnabled"],
|
|
293
|
+
{
|
|
294
|
+
encoding: "utf8",
|
|
295
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
296
|
+
timeout: 5e3
|
|
297
|
+
}
|
|
298
|
+
).trim().toLowerCase();
|
|
299
|
+
if (current === "1" || current === "true" || current === "yes") return false;
|
|
300
|
+
} catch {
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
execFileSync(
|
|
304
|
+
"xcrun",
|
|
305
|
+
[
|
|
306
|
+
...defaultsArgs,
|
|
307
|
+
"write",
|
|
308
|
+
"com.apple.Accessibility",
|
|
309
|
+
"ApplicationAccessibilityEnabled",
|
|
310
|
+
"-bool",
|
|
311
|
+
"YES"
|
|
312
|
+
],
|
|
313
|
+
{
|
|
314
|
+
stdio: "ignore",
|
|
315
|
+
timeout: 5e3
|
|
316
|
+
}
|
|
317
|
+
);
|
|
318
|
+
try {
|
|
319
|
+
execFileSync("xcrun", ["simctl", "shutdown", simulator], {
|
|
320
|
+
stdio: "ignore",
|
|
321
|
+
timeout: 15e3
|
|
322
|
+
});
|
|
323
|
+
} catch {
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
execFileSync("xcrun", ["simctl", "boot", simulator], {
|
|
327
|
+
stdio: "ignore",
|
|
328
|
+
timeout: 15e3
|
|
329
|
+
});
|
|
330
|
+
} catch {
|
|
331
|
+
}
|
|
332
|
+
execFileSync("xcrun", ["simctl", "bootstatus", simulator, "-b"], {
|
|
333
|
+
stdio: "ignore",
|
|
334
|
+
timeout: 12e4
|
|
335
|
+
});
|
|
336
|
+
if (!json) {
|
|
337
|
+
process.stderr.write(
|
|
338
|
+
`Enabled iOS application accessibility on ${simulator}; restarted the simulator before app launch.
|
|
339
|
+
`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
return true;
|
|
343
|
+
} catch (error) {
|
|
344
|
+
if (!json) {
|
|
345
|
+
process.stderr.write(
|
|
346
|
+
`mobile prepare: iOS accessibility enablement was unavailable (${error instanceof Error ? error.message : String(error)}); continuing with native-view fallback.
|
|
347
|
+
`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
243
353
|
async function dispatchAction(action, target, platform, json, preflightMode = "fast", watcherPort) {
|
|
244
354
|
const cwd = action.cwd ?? target;
|
|
245
355
|
switch (action.id) {
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "@farmslot/recipe-harness/runtime/log-analysis";
|
|
14
14
|
import { probeMetroPackager } from "@farmslot/recipe-harness/runtime/metro-probe";
|
|
15
15
|
import { recipeRuntimePath } from "../../paths.js";
|
|
16
|
+
import { resolveMobileToolPath } from "../../../library/actions/mobile/platform/tool-paths.mjs";
|
|
16
17
|
import { mobileProductMarkers } from "./deps-markers.js";
|
|
17
18
|
import { applyMobileMetroEnvPolicy, mobileMetroEnvCheck } from "./metro-env.js";
|
|
18
19
|
const BUNDLE_ERR = /Bundling failed|Unable to resolve /u;
|
|
@@ -66,9 +67,11 @@ const installActions = (target) => [
|
|
|
66
67
|
function appRunningOnDevice(platform) {
|
|
67
68
|
try {
|
|
68
69
|
if (platform === "android") {
|
|
70
|
+
const adbPath = resolveMobileToolPath("adb");
|
|
71
|
+
if (!adbPath) return false;
|
|
69
72
|
const serial = process.env.ADB_SERIAL || process.env.ANDROID_SERIAL;
|
|
70
73
|
const args = serial ? ["-s", serial] : [];
|
|
71
|
-
const out2 = execFileSync(
|
|
74
|
+
const out2 = execFileSync(adbPath, [...args, "shell", "ps", "-A"], {
|
|
72
75
|
encoding: "utf8",
|
|
73
76
|
stdio: ["ignore", "pipe", "ignore"],
|
|
74
77
|
timeout: 5e3
|
package/dist/adapters.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from "./live-adapter-contract.js";
|
|
6
6
|
import { withExtensionPage } from "../library/actions/extension/platform/cdp.mjs";
|
|
7
7
|
import { bridgeCommand, evalSync, MOBILE_BRIDGE_ERROR_CODES, selectBridgeStatusEntry, simulatorScreenshot } from "../library/actions/mobile/platform/bridge.mjs";
|
|
8
|
+
import { observeNativeUi } from "../library/actions/mobile/platform/observe-ui.mjs";
|
|
8
9
|
function sleep(ms) {
|
|
9
10
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
11
|
}
|
|
@@ -276,7 +277,8 @@ const MOBILE_BRIDGE_HANDLERS = {
|
|
|
276
277
|
setInput: handleMobileSetInput,
|
|
277
278
|
scroll: handleMobileScroll,
|
|
278
279
|
waitFor: handleMobileWaitFor,
|
|
279
|
-
hud: handleMobileHud
|
|
280
|
+
hud: handleMobileHud,
|
|
281
|
+
observeUi: handleMobileObserveUi
|
|
280
282
|
};
|
|
281
283
|
function createMetaMaskMobileBridge() {
|
|
282
284
|
return {
|
|
@@ -302,6 +304,9 @@ async function handleMobileScreenshot(payload, context) {
|
|
|
302
304
|
async function handleMobileStatus(payload, context) {
|
|
303
305
|
return bridgeCommand(mobileUiInput(context, "status", payload), ["status"]);
|
|
304
306
|
}
|
|
307
|
+
async function handleMobileObserveUi(payload, context) {
|
|
308
|
+
return observeNativeUi(payload, context);
|
|
309
|
+
}
|
|
305
310
|
async function handleMobileNavigate(payload, context) {
|
|
306
311
|
const live = await runLiveAdapterScript({
|
|
307
312
|
platform: "mobile",
|
|
@@ -496,7 +501,8 @@ function createMetaMaskUiTransport(platform, harness, preparedLiveAdapters) {
|
|
|
496
501
|
);
|
|
497
502
|
}
|
|
498
503
|
});
|
|
499
|
-
|
|
504
|
+
return {
|
|
505
|
+
...base.observe ? { observe: base.observe.bind(base) } : {},
|
|
500
506
|
async execute(action, node, context) {
|
|
501
507
|
if (action === "ui.navigate") {
|
|
502
508
|
const live = await runLiveFirst(
|
|
@@ -513,10 +519,6 @@ function createMetaMaskUiTransport(platform, harness, preparedLiveAdapters) {
|
|
|
513
519
|
return base.execute(action, action === "ui.wait_for" ? normalizeUiWaitNode(node) : node, context);
|
|
514
520
|
}
|
|
515
521
|
};
|
|
516
|
-
if (base.observe) {
|
|
517
|
-
transport.observe = (refs, node, context) => base.observe(refs, node, context);
|
|
518
|
-
}
|
|
519
|
-
return transport;
|
|
520
522
|
}
|
|
521
523
|
function normalizeUiWaitNode(node) {
|
|
522
524
|
if (node.expected !== void 0 || typeof node.visible !== "boolean") return node;
|
package/dist/cli-commands.js
CHANGED
|
@@ -15,7 +15,7 @@ const SPEC = {
|
|
|
15
15
|
{ name: "logs", aliases: ["tail"], desc: "Compact build events or full log", flags: ["--full", "-f", "--window", "--events", "--source", "--json"] },
|
|
16
16
|
{ name: "debug", aliases: ["devtools", "inspect"], desc: "Open DevTools UI", flags: ["--json", "--no-open"] },
|
|
17
17
|
{ name: "fixtures", desc: "Manage the canonical wallet fixture (sync/set/generate)", args: ["sync", "set", "generate"], flags: ["--fixture", "--out", "--adapter", "--target", "--device", "--json"] },
|
|
18
|
-
{ name: "actions", desc: "List runnable recipe actions", flags: ["--json", "--categories", "--category", "--action", "--library"] },
|
|
18
|
+
{ name: "actions", desc: "List runnable recipe actions", flags: ["--json", "--matrix", "--categories", "--category", "--action", "--library"] },
|
|
19
19
|
{ name: "doctor", desc: "Check harness/orchestration health", flags: ["--json", "--target", "--adapter", "--runtime-dir", "--expect-live", "--print-ready", "--cdp-port", "--device"] },
|
|
20
20
|
{ name: "run", desc: "Execute a proof recipe (path or library name, e.g. run perps.smoke)", args: ["recipe.json|name"], flags: ["--list", "--device"] },
|
|
21
21
|
{ name: "execution-template", desc: "Discover and materialize shared agent checklists", args: ["list", "materialize", "lint", "new"], flags: ["--dir", "--domain-dir", "--project-worker", "--project-name", "--package-templates", "--package-id", "--flow", "--run-mode", "--platform", "--domain", "--id", "--provenance", "--include-shadowed", "--no-include-shadowed", "--title", "--force", "--json"] },
|
package/dist/command-contract.js
CHANGED
|
@@ -117,6 +117,7 @@ const PUBLIC_COMMAND_CONTRACTS = {
|
|
|
117
117
|
actions: {
|
|
118
118
|
options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
|
|
119
119
|
"--action": value(),
|
|
120
|
+
"--matrix": bool(),
|
|
120
121
|
"--categories": bool(),
|
|
121
122
|
"--category": value(),
|
|
122
123
|
"--action-manifest": value(),
|
package/dist/commands/call.js
CHANGED
|
@@ -6,7 +6,14 @@ import { importRecipeProtocol } from "../paths.js";
|
|
|
6
6
|
import { conciseFailureForHuman, recipeRunning } from "../heal-bounds.js";
|
|
7
7
|
import { color } from "../cli-color.js";
|
|
8
8
|
import { checkoutBusyOut, EXIT, usageOut } from "./shared.js";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
actionLibraryContextArgs,
|
|
11
|
+
actionExampleCommand,
|
|
12
|
+
describeManifestActions,
|
|
13
|
+
fuzzyResolveActions,
|
|
14
|
+
resolveActionCapabilityMatrix,
|
|
15
|
+
resolveActionCapabilityRefusal
|
|
16
|
+
} from "./manifest.js";
|
|
10
17
|
import {
|
|
11
18
|
parseArgs,
|
|
12
19
|
isRecord,
|
|
@@ -112,6 +119,36 @@ async function handleCall(argv) {
|
|
|
112
119
|
const names = getRecipeActionManifestActionNames(manifest);
|
|
113
120
|
const resolution = resolveActionName(shortName, names);
|
|
114
121
|
if (resolution.status === "unknown") {
|
|
122
|
+
const refusal = actionManifestOverride ? void 0 : resolveActionCapabilityRefusal(
|
|
123
|
+
shortName,
|
|
124
|
+
adapter,
|
|
125
|
+
await resolveActionCapabilityMatrix(librarySources)
|
|
126
|
+
);
|
|
127
|
+
if (refusal) {
|
|
128
|
+
const satisfying = refusal.satisfyingAdapters.join(", ");
|
|
129
|
+
const message2 = `missing action capability "${refusal.capability}" for the ${adapter} adapter.`;
|
|
130
|
+
const userAction2 = `Satisfying adapters for "${refusal.capability}": ${satisfying}. Inspect: mm-harness actions --matrix --action ${shellQuote(refusal.capability)}${actionLibraryContextArgs(librarySources)} --json`;
|
|
131
|
+
const error = {
|
|
132
|
+
code: "ACTION_CAPABILITY_UNAVAILABLE",
|
|
133
|
+
message: message2,
|
|
134
|
+
capability: refusal.capability,
|
|
135
|
+
satisfyingAdapters: refusal.satisfyingAdapters,
|
|
136
|
+
userAction: userAction2
|
|
137
|
+
};
|
|
138
|
+
if (json) {
|
|
139
|
+
console.log(JSON.stringify({
|
|
140
|
+
schemaVersion: 1,
|
|
141
|
+
command: "call",
|
|
142
|
+
adapter,
|
|
143
|
+
action: shortName,
|
|
144
|
+
error
|
|
145
|
+
}, null, 2));
|
|
146
|
+
} else {
|
|
147
|
+
console.error(`\u2717 call: ${message2}
|
|
148
|
+
Next: ${userAction2}`);
|
|
149
|
+
}
|
|
150
|
+
return EXIT.usage;
|
|
151
|
+
}
|
|
115
152
|
const message = `unknown action "${shortName}" for the ${adapter} adapter.`;
|
|
116
153
|
const userAction = `mm-harness actions --adapter ${adapter} --json`;
|
|
117
154
|
if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, error: { code: "ACTION_UNKNOWN", message, userAction } }, null, 2));
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { resolveMobileToolPath } from "../../library/actions/mobile/platform/tool-paths.mjs";
|
|
2
3
|
import { listConnectedDevices } from "../devices.js";
|
|
3
4
|
import { optionString, shellQuoteArg, targetPath } from "./parse-args.js";
|
|
4
5
|
function normalizeDeviceName(value) {
|
|
5
6
|
return value.replace(/_/gu, " ").trim();
|
|
6
7
|
}
|
|
7
8
|
function resolveAndroidModel(serial) {
|
|
9
|
+
const adbPath = resolveMobileToolPath("adb");
|
|
10
|
+
if (!adbPath) return void 0;
|
|
8
11
|
try {
|
|
9
|
-
const output = execFileSync(
|
|
12
|
+
const output = execFileSync(adbPath, ["-s", serial, "shell", "getprop", "ro.product.model"], {
|
|
10
13
|
encoding: "utf8",
|
|
11
14
|
stdio: ["ignore", "pipe", "ignore"],
|
|
12
15
|
timeout: 5e3
|
package/dist/commands/doctor.js
CHANGED
|
@@ -131,7 +131,31 @@ async function handleDoctor({ options }) {
|
|
|
131
131
|
}
|
|
132
132
|
return status === "pass" ? 0 : 1;
|
|
133
133
|
}
|
|
134
|
-
const
|
|
134
|
+
const mobilePlatform = adapter === "mobile" && (platformOption === "ios" || platformOption === "android") ? platformOption : void 0;
|
|
135
|
+
const result = createDoctorReport(
|
|
136
|
+
adapter,
|
|
137
|
+
target,
|
|
138
|
+
manifestValidation,
|
|
139
|
+
actionManifestPath,
|
|
140
|
+
mobilePlatform
|
|
141
|
+
);
|
|
142
|
+
const missingRequiredTool = result.checks.find(
|
|
143
|
+
(check) => check.required && check.status === "fail" && check.id.startsWith("device-tool-") && check.userAction
|
|
144
|
+
);
|
|
145
|
+
if (missingRequiredTool) {
|
|
146
|
+
const error = {
|
|
147
|
+
code: "DOCTOR_CHECKS_FAILED",
|
|
148
|
+
message: missingRequiredTool.message,
|
|
149
|
+
userAction: missingRequiredTool.userAction
|
|
150
|
+
};
|
|
151
|
+
if (json) {
|
|
152
|
+
console.log(JSON.stringify({ ...result, ready: false, error }, null, 2));
|
|
153
|
+
} else {
|
|
154
|
+
console.error(`\u2717 doctor: ${error.message}
|
|
155
|
+
Next: ${error.userAction}`);
|
|
156
|
+
}
|
|
157
|
+
return EXIT.runtime;
|
|
158
|
+
}
|
|
135
159
|
let runtime;
|
|
136
160
|
let runtimeProbeError;
|
|
137
161
|
try {
|
|
@@ -178,7 +202,9 @@ async function handleDoctor({ options }) {
|
|
|
178
202
|
printReady
|
|
179
203
|
);
|
|
180
204
|
}
|
|
181
|
-
const doctorUserAction =
|
|
205
|
+
const doctorUserAction = doctorResult.checks.find(
|
|
206
|
+
(check) => check.required && check.status === "fail" && check.userAction
|
|
207
|
+
)?.userAction ?? `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
|
|
182
208
|
const doctorError = doctorResult.status === "fail" ? { code: "DOCTOR_CHECKS_FAILED", message: "one or more required doctor checks failed", userAction: doctorUserAction } : void 0;
|
|
183
209
|
const next = doctorError?.userAction ?? (runtime?.decision === "ready" ? void 0 : runtime?.nextAction);
|
|
184
210
|
if (json) console.log(JSON.stringify({ ...doctorResult, ready: runtime?.decision === "ready", runtime, orphanMetros, capture, devices, additionalReachableDevices, ...next ? { next } : {}, ...doctorError ? { error: doctorError } : {} }, null, 2));
|