@deeeed/metamask-harness 0.26.5 → 0.27.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 +10 -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 +19 -6
- package/dist/commands/device-target.js +4 -1
- package/dist/commands/doctor.js +28 -2
- package/dist/commands/mobile-device-view.js +4 -1
- package/dist/devices.js +4 -1
- package/dist/doctor.js +25 -2
- package/dist/run-recording.js +128 -92
- package/library/actions/extension/platform/cdp.mjs +170 -117
- package/library/actions/mobile/platform/bridge.mjs +14 -5
- package/library/actions/mobile/platform/observe-ui.mjs +416 -0
- package/library/actions/mobile/platform/tool-paths.mjs +122 -0
- package/library/actions/shared/analytics/collector.mjs +50 -5
- package/library/manifests/mobile.action-manifest.json +25 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.27.0 - 2026-07-31
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- 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.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
5
15
|
## 0.26.5 - 2026-07-31
|
|
6
16
|
|
|
7
17
|
### 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,20 @@ 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
|
+
try {
|
|
309
|
+
await bridgeCommand(
|
|
310
|
+
mobileUiInput(context, "observe", {
|
|
311
|
+
...payload,
|
|
312
|
+
bridge_timeout_ms: 2e3,
|
|
313
|
+
cdp_timeout_ms: 2e3
|
|
314
|
+
}),
|
|
315
|
+
["hide-step"]
|
|
316
|
+
);
|
|
317
|
+
} catch {
|
|
318
|
+
}
|
|
319
|
+
return observeNativeUi(payload, context);
|
|
320
|
+
}
|
|
305
321
|
async function handleMobileNavigate(payload, context) {
|
|
306
322
|
const live = await runLiveAdapterScript({
|
|
307
323
|
platform: "mobile",
|
|
@@ -496,7 +512,8 @@ function createMetaMaskUiTransport(platform, harness, preparedLiveAdapters) {
|
|
|
496
512
|
);
|
|
497
513
|
}
|
|
498
514
|
});
|
|
499
|
-
|
|
515
|
+
return {
|
|
516
|
+
...base.observe ? { observe: base.observe.bind(base) } : {},
|
|
500
517
|
async execute(action, node, context) {
|
|
501
518
|
if (action === "ui.navigate") {
|
|
502
519
|
const live = await runLiveFirst(
|
|
@@ -513,10 +530,6 @@ function createMetaMaskUiTransport(platform, harness, preparedLiveAdapters) {
|
|
|
513
530
|
return base.execute(action, action === "ui.wait_for" ? normalizeUiWaitNode(node) : node, context);
|
|
514
531
|
}
|
|
515
532
|
};
|
|
516
|
-
if (base.observe) {
|
|
517
|
-
transport.observe = (refs, node, context) => base.observe(refs, node, context);
|
|
518
|
-
}
|
|
519
|
-
return transport;
|
|
520
533
|
}
|
|
521
534
|
function normalizeUiWaitNode(node) {
|
|
522
535
|
if (node.expected !== void 0 || typeof node.visible !== "boolean") return node;
|
|
@@ -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));
|
|
@@ -1,4 +1,5 @@
|
|
|
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 { renderDeviceList, scopedDevices } from "./device-target.js";
|
|
4
5
|
import { probeMobileLiveState } from "./status-probe.js";
|
|
@@ -116,8 +117,10 @@ function selectedAndroidPortHints(devices, liveMap) {
|
|
|
116
117
|
}).filter((hint) => hint.reversePorts.length > 0);
|
|
117
118
|
}
|
|
118
119
|
function androidReversePorts(serial) {
|
|
120
|
+
const adbPath = resolveMobileToolPath("adb");
|
|
121
|
+
if (!adbPath) return [];
|
|
119
122
|
try {
|
|
120
|
-
const out = execFileSync(
|
|
123
|
+
const out = execFileSync(adbPath, ["-s", serial, "reverse", "--list"], {
|
|
121
124
|
encoding: "utf8",
|
|
122
125
|
stdio: ["ignore", "pipe", "ignore"],
|
|
123
126
|
timeout: 5e3
|
package/dist/devices.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { resolveMobileToolPath } from "../library/actions/mobile/platform/tool-paths.mjs";
|
|
2
3
|
function listConnectedDevices(platform) {
|
|
3
4
|
const devices = [];
|
|
4
5
|
if (!platform || platform === "android") devices.push(...listAndroidDevices());
|
|
@@ -6,9 +7,11 @@ function listConnectedDevices(platform) {
|
|
|
6
7
|
return devices;
|
|
7
8
|
}
|
|
8
9
|
function listAndroidDevices() {
|
|
10
|
+
const adbPath = resolveMobileToolPath("adb");
|
|
11
|
+
if (!adbPath) return [];
|
|
9
12
|
let output;
|
|
10
13
|
try {
|
|
11
|
-
output = execFileSync(
|
|
14
|
+
output = execFileSync(adbPath, ["devices", "-l"], {
|
|
12
15
|
encoding: "utf8",
|
|
13
16
|
timeout: 5e3,
|
|
14
17
|
stdio: ["ignore", "pipe", "ignore"]
|
package/dist/doctor.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { color } from "./cli-color.js";
|
|
4
|
+
import {
|
|
5
|
+
mobileToolRecovery,
|
|
6
|
+
resolveMobileToolPath
|
|
7
|
+
} from "../library/actions/mobile/platform/tool-paths.mjs";
|
|
4
8
|
import { mobilePerpsEnvironment } from "./adapters/mobile/perps-env.js";
|
|
5
9
|
import { readRuntimeContextField, resolveRuntimeContextPath } from "./harness.js";
|
|
6
10
|
import { manifestPath, readJson, recipeHarnessRoot, recipeRuntimeDir, runnerDir } from "./paths.js";
|
|
@@ -128,7 +132,7 @@ function renderRuntimeContext(runtimeContext) {
|
|
|
128
132
|
}
|
|
129
133
|
return lines.join("\n");
|
|
130
134
|
}
|
|
131
|
-
function createDoctorReport(adapter, target, manifestValidation, actionManifestPath = manifestPath(adapter)) {
|
|
135
|
+
function createDoctorReport(adapter, target, manifestValidation, actionManifestPath = manifestPath(adapter), mobilePlatform) {
|
|
132
136
|
const mode = compatibilityMode(adapter, target);
|
|
133
137
|
const manifestErrors = Number(manifestValidation.summary?.errors ?? 0);
|
|
134
138
|
const checks = [
|
|
@@ -143,7 +147,8 @@ function createDoctorReport(adapter, target, manifestValidation, actionManifestP
|
|
|
143
147
|
status: mode === "unsupported/no bridge" ? "fail" : "pass",
|
|
144
148
|
required: false,
|
|
145
149
|
message: mode === "unsupported/no bridge" ? `No ${adapter} bridge is available for this checkout.` : `${adapter} compatibility mode: ${mode}.`
|
|
146
|
-
}
|
|
150
|
+
},
|
|
151
|
+
...adapter === "mobile" ? mobileToolDoctorChecks(mobilePlatform) : []
|
|
147
152
|
];
|
|
148
153
|
const requiredChecks = requiredDoctorCheckSummary(checks);
|
|
149
154
|
return {
|
|
@@ -164,6 +169,23 @@ function createDoctorReport(adapter, target, manifestValidation, actionManifestP
|
|
|
164
169
|
manifestValidation: manifestValidation.summary
|
|
165
170
|
};
|
|
166
171
|
}
|
|
172
|
+
function mobileToolDoctorChecks(platform) {
|
|
173
|
+
const specs = platform === "android" ? [{ tool: "adb", platform: "Android" }] : platform === "ios" ? [{ tool: "idb", platform: "iOS" }] : [
|
|
174
|
+
{ tool: "adb", platform: "Android" },
|
|
175
|
+
{ tool: "idb", platform: "iOS" }
|
|
176
|
+
];
|
|
177
|
+
return specs.map(({ tool, platform: platformName }) => {
|
|
178
|
+
const resolved = resolveMobileToolPath(tool);
|
|
179
|
+
return {
|
|
180
|
+
id: `device-tool-${tool}`,
|
|
181
|
+
status: resolved ? "pass" : "fail",
|
|
182
|
+
required: platform !== void 0,
|
|
183
|
+
message: resolved ? `${platformName} device tool ${tool} is ready.` : `${platformName} device tool ${tool} is unavailable.`,
|
|
184
|
+
...resolved ? { detail: resolved } : {},
|
|
185
|
+
...!resolved ? { userAction: mobileToolRecovery(tool) } : {}
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
}
|
|
167
189
|
function runnerInstallKind(runnerRoot, invokedPath, executablePath) {
|
|
168
190
|
const normalizedRoot = path.normalize(runnerRoot);
|
|
169
191
|
const nodeModulesSegment = `${path.sep}node_modules${path.sep}`;
|
|
@@ -230,6 +252,7 @@ export {
|
|
|
230
252
|
createDoctorReport,
|
|
231
253
|
fixtureFileSummary,
|
|
232
254
|
fixtureSummary,
|
|
255
|
+
mobileToolDoctorChecks,
|
|
233
256
|
renderRuntimeContext,
|
|
234
257
|
repoShape,
|
|
235
258
|
requiredDoctorCheckSummary,
|