@deeeed/metamask-harness 0.33.0 → 0.33.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/adapters/mobile/open-device.sh +80 -23
- package/adapters/shared/resolve-slot-ports-core.mjs +44 -3
- package/adapters/shared/resolve-slot-ports.sh +3 -0
- package/dist/adapters/mobile/perps-env.js +3 -9
- package/dist/adapters/mobile/prepare.js +4 -1
- package/dist/adapters/mobile/provision.js +32 -7
- package/dist/adapters/mobile/runtime-decision.js +1 -1
- package/dist/adapters/slot-ports.js +8 -1
- package/dist/commands/launch/index.js +29 -1
- package/dist/commands/provision.js +5 -3
- package/dist/harness.js +5 -3
- package/dist/runtime-context.js +4 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.33.1 - 2026-08-04
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Make Mobile onboarding deterministic by pinning provisioning, native builds, launch, and readiness to one simulator UDID and Metro port, while ensuring `doctor --fix` enables Perps without inventing `OVERRIDE_REMOTE_FEATURE_FLAGS`.
|
|
10
|
+
|
|
5
11
|
## 0.33.0 - 2026-08-03
|
|
6
12
|
|
|
7
13
|
### Added
|
|
@@ -23,7 +23,7 @@ set -uo pipefail
|
|
|
23
23
|
|
|
24
24
|
PLATFORM="ios"
|
|
25
25
|
TARGET="$PWD"
|
|
26
|
-
SIMULATOR="${
|
|
26
|
+
SIMULATOR="${SIM_UDID:-${IOS_SIMULATOR:-booted}}"
|
|
27
27
|
ADB_SERIAL_ARG="${ADB_SERIAL:-${ANDROID_SERIAL:-}}"
|
|
28
28
|
ADB_BIN="${MM_HARNESS_ADB_PATH:-}"
|
|
29
29
|
PREFLIGHT_MODE="${MOBILE_PREFLIGHT_MODE:-fast}"
|
|
@@ -89,15 +89,42 @@ ios_app_installed() {
|
|
|
89
89
|
xcrun simctl get_app_container "$sim_target" "$bundle_id" app >/dev/null 2>&1
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
run_ios_native_build()
|
|
93
|
-
local sim_target="$1" reason="$2"
|
|
92
|
+
run_ios_native_build() (
|
|
93
|
+
local sim_target="$1" reason="$2" build_env=""
|
|
94
94
|
printf '%s; installing with yarn start:ios (WATCHER_PORT=%s)\n' "$reason" "$PORT" >&2
|
|
95
|
+
build_env="$(mktemp "${TMPDIR:-/tmp}/mm-harness-ios-build-env.XXXXXX")" || return 1
|
|
96
|
+
chmod 600 "$build_env" || { rm -f "$build_env"; return 1; }
|
|
97
|
+
trap 'rm -f "$build_env"' EXIT HUP INT TERM
|
|
98
|
+
{
|
|
99
|
+
printf 'export MM_HARNESS_BUILD_WATCHER_PORT=%q\n' "$PORT"
|
|
100
|
+
printf 'export MM_HARNESS_BUILD_IOS_SIMULATOR=%q\n' "$sim_target"
|
|
101
|
+
cat <<'BUILD_ENV'
|
|
102
|
+
mm_harness_restore_build_target() {
|
|
103
|
+
export WATCHER_PORT="$MM_HARNESS_BUILD_WATCHER_PORT"
|
|
104
|
+
export METRO_PORT="$MM_HARNESS_BUILD_WATCHER_PORT"
|
|
105
|
+
export IOS_SIMULATOR="$MM_HARNESS_BUILD_IOS_SIMULATOR"
|
|
106
|
+
}
|
|
107
|
+
eval() {
|
|
108
|
+
builtin eval "$@"
|
|
109
|
+
local status=$?
|
|
110
|
+
mm_harness_restore_build_target
|
|
111
|
+
return "$status"
|
|
112
|
+
}
|
|
113
|
+
source() {
|
|
114
|
+
builtin source "$@"
|
|
115
|
+
local status=$?
|
|
116
|
+
mm_harness_restore_build_target
|
|
117
|
+
return "$status"
|
|
118
|
+
}
|
|
119
|
+
mm_harness_restore_build_target
|
|
120
|
+
BUILD_ENV
|
|
121
|
+
} > "$build_env"
|
|
95
122
|
(
|
|
96
123
|
cd "$TARGET"
|
|
97
124
|
command -v activate_repo_ruby_best_effort >/dev/null 2>&1 && activate_repo_ruby_best_effort "$TARGET"
|
|
98
|
-
WATCHER_PORT="$PORT" METRO_PORT="$PORT" IOS_SIMULATOR="$sim_target" EXPO_NO_TYPESCRIPT_SETUP=1 yarn start:ios 2>&1
|
|
125
|
+
BASH_ENV="$build_env" WATCHER_PORT="$PORT" METRO_PORT="$PORT" IOS_SIMULATOR="$sim_target" EXPO_NO_TYPESCRIPT_SETUP=1 yarn start:ios 2>&1
|
|
99
126
|
)
|
|
100
|
-
|
|
127
|
+
)
|
|
101
128
|
|
|
102
129
|
ensure_ios_app_for_mode() {
|
|
103
130
|
local sim_target="$1" bundle_id="$2" mode="$3"
|
|
@@ -107,7 +134,6 @@ ensure_ios_app_for_mode() {
|
|
|
107
134
|
printf >&2 'open-device: fast mode requires an installed iOS dev client (%s) on %s.\n' \
|
|
108
135
|
"$bundle_id" "$sim_target"
|
|
109
136
|
printf >&2 'Fast mode only prewarms the bundle and opens the existing client.\n'
|
|
110
|
-
printf >&2 'Next: use --preflight-mode auto to install if missing.\n'
|
|
111
137
|
return 1
|
|
112
138
|
;;
|
|
113
139
|
auto|default)
|
|
@@ -124,10 +150,6 @@ ensure_ios_app_for_mode() {
|
|
|
124
150
|
|
|
125
151
|
sim_udid_for_target() {
|
|
126
152
|
local target="$1"
|
|
127
|
-
# Already a UDID
|
|
128
|
-
if printf '%s' "$target" | grep -Eq '^[0-9A-Fa-f-]{36}$'; then
|
|
129
|
-
printf '%s\n' "$target"; return 0
|
|
130
|
-
fi
|
|
131
153
|
xcrun simctl list devices available --json 2>/dev/null | TARGET_SIMULATOR="$target" node -e '
|
|
132
154
|
let input = "";
|
|
133
155
|
process.stdin.setEncoding("utf8");
|
|
@@ -136,34 +158,54 @@ process.stdin.on("end", () => {
|
|
|
136
158
|
const target = process.env.TARGET_SIMULATOR || "";
|
|
137
159
|
try {
|
|
138
160
|
const parsed = JSON.parse(input);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
161
|
+
const devices = Object.values(parsed.devices || {}).flatMap((value) => Array.isArray(value) ? value : [])
|
|
162
|
+
.filter((device) => device && device.isAvailable !== false && device.udid);
|
|
163
|
+
const matches = /^[0-9A-Fa-f-]{36}$/.test(target)
|
|
164
|
+
? devices.filter((device) => device.udid.toLowerCase() === target.toLowerCase())
|
|
165
|
+
: target === "booted"
|
|
166
|
+
? devices.filter((device) => device.state === "Booted")
|
|
167
|
+
: devices.filter((device) => device.name === target);
|
|
168
|
+
const booted = matches.filter((device) => device.state === "Booted");
|
|
169
|
+
const found = matches.length === 1 ? matches[0] : booted.length === 1 ? booted[0] : null;
|
|
170
|
+
if (found) {
|
|
171
|
+
process.stdout.write(`${found.udid}\n`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (matches.length > 1) {
|
|
175
|
+
process.stderr.write(`open-device: iOS simulator target ${JSON.stringify(target)} is ambiguous; use one UDID:\n`);
|
|
176
|
+
for (const device of matches) {
|
|
177
|
+
process.stderr.write(` - ${device.udid} (${device.name}) [${device.state || "unknown"}]\n`);
|
|
146
178
|
}
|
|
179
|
+
process.exit(2);
|
|
147
180
|
}
|
|
181
|
+
process.exit(1);
|
|
148
182
|
} catch {
|
|
149
|
-
|
|
183
|
+
process.exit(1);
|
|
150
184
|
}
|
|
151
185
|
});
|
|
152
186
|
'
|
|
153
187
|
}
|
|
154
188
|
|
|
189
|
+
print_missing_simulator_recovery() {
|
|
190
|
+
local target="$1"
|
|
191
|
+
if printf '%s' "$target" | grep -Eq '^[0-9A-Fa-f-]{36}$'; then
|
|
192
|
+
printf "Next: mm-harness status --adapter mobile --target '%s' --all-devices --json\n" "$TARGET" >&2
|
|
193
|
+
else
|
|
194
|
+
printf "Next: mm-harness provision runway ios --adapter mobile --target '%s' --device '%s'\n" "$TARGET" "$target" >&2
|
|
195
|
+
fi
|
|
196
|
+
}
|
|
197
|
+
|
|
155
198
|
boot_simulator_if_needed() {
|
|
156
199
|
local target="$1" state=""
|
|
157
|
-
[ "$target" != "booted" ] || return 0
|
|
158
200
|
local udid=""
|
|
159
201
|
udid="$(sim_udid_for_target "$target")"
|
|
160
202
|
if [ -z "$udid" ]; then
|
|
161
203
|
printf "open-device: configured iOS simulator '%s' does not exist.\n" "$target" >&2
|
|
162
|
-
|
|
204
|
+
print_missing_simulator_recovery "$target"
|
|
163
205
|
return 1
|
|
164
206
|
fi
|
|
165
207
|
local line=""
|
|
166
|
-
line="$(xcrun simctl list devices available 2>/dev/null | grep -F "${
|
|
208
|
+
line="$(xcrun simctl list devices available 2>/dev/null | grep -F "(${udid})" | head -1 || true)"
|
|
167
209
|
case "$line" in *"(Booted)"*) state="Booted" ;; *"(Shutdown)"*) state="Shutdown" ;; esac
|
|
168
210
|
[ "$state" = "Booted" ] && return 0
|
|
169
211
|
printf 'Booting iOS simulator %s%s\n' "$target" "${state:+ ($state)}" >&2
|
|
@@ -235,7 +277,6 @@ ensure_android_app_for_mode() {
|
|
|
235
277
|
fast)
|
|
236
278
|
if android_app_installed "$pkg" "$@"; then return 0; fi
|
|
237
279
|
printf >&2 'open-device: fast mode requires an installed Android dev client (%s).\n' "$pkg"
|
|
238
|
-
printf >&2 'Next: use --preflight-mode auto to install if missing.\n'
|
|
239
280
|
return 1
|
|
240
281
|
;;
|
|
241
282
|
auto|default)
|
|
@@ -264,7 +305,17 @@ BUNDLE_IDS=("${IOS_BUNDLE_ID:-io.metamask.MetaMask}" "io.metamask" "io.metamask.
|
|
|
264
305
|
ANDROID_PKGS=("${ANDROID_PACKAGE_ID:-io.metamask}" "io.metamask.flask" "io.metamask.qa")
|
|
265
306
|
|
|
266
307
|
if [ "$PLATFORM" = "ios" ]; then
|
|
267
|
-
SIM_TARGET="$SIMULATOR"
|
|
308
|
+
SIM_TARGET="$(sim_udid_for_target "$SIMULATOR")"
|
|
309
|
+
SIM_RESOLVE_STATUS=$?
|
|
310
|
+
if [ "$SIM_RESOLVE_STATUS" -ne 0 ]; then
|
|
311
|
+
if [ "$SIM_RESOLVE_STATUS" -eq 1 ]; then
|
|
312
|
+
printf "open-device: configured iOS simulator '%s' does not exist.\n" "$SIMULATOR" >&2
|
|
313
|
+
print_missing_simulator_recovery "$SIMULATOR"
|
|
314
|
+
else
|
|
315
|
+
printf "Next: mm-harness status --adapter mobile --target '%s' --all-devices --json\n" "$TARGET" >&2
|
|
316
|
+
fi
|
|
317
|
+
exit 1
|
|
318
|
+
fi
|
|
268
319
|
# Expo dev-client URL scheme — drives both the deep link and the scheme-approval key.
|
|
269
320
|
DEV_CLIENT_SCHEME="${IOS_DEV_CLIENT_SCHEME:-expo-metamask}"
|
|
270
321
|
|
|
@@ -319,6 +370,7 @@ if [ "$PLATFORM" = "ios" ]; then
|
|
|
319
370
|
if [ "$LAUNCHED" = false ]; then
|
|
320
371
|
printf 'open-device: no MetaMask bundle found on simulator %s\n' "$SIM_TARGET" >&2
|
|
321
372
|
printf ' tried: %s\n' "${BUNDLE_IDS[*]}" >&2
|
|
373
|
+
printf "Next: mm-harness provision runway ios --adapter mobile --target '%s' --device '%s' --force\n" "$TARGET" "$SIM_TARGET" >&2
|
|
322
374
|
exit 1
|
|
323
375
|
fi
|
|
324
376
|
|
|
@@ -356,6 +408,11 @@ else
|
|
|
356
408
|
printf 'open-device: no MetaMask package found on Android device %s\n' \
|
|
357
409
|
"${ADB_SERIAL_ARG:-<default>}" >&2
|
|
358
410
|
printf ' tried: %s\n' "${ANDROID_PKGS[*]}" >&2
|
|
411
|
+
if [ -n "$ADB_SERIAL_ARG" ]; then
|
|
412
|
+
printf "Next: mm-harness launch android --build --target '%s' --device '%s'\n" "$TARGET" "$ADB_SERIAL_ARG" >&2
|
|
413
|
+
else
|
|
414
|
+
printf "Next: mm-harness launch android --build --target '%s'\n" "$TARGET" >&2
|
|
415
|
+
fi
|
|
359
416
|
exit 1
|
|
360
417
|
fi
|
|
361
418
|
fi
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Reads the orchestrator's pool JSON (the on-disk `pool/` directory) when a slot
|
|
3
3
|
// repo matches, else the local-extension-N suffix formula.
|
|
4
4
|
|
|
5
|
+
import { execFileSync } from 'node:child_process';
|
|
5
6
|
import fs from 'node:fs';
|
|
6
7
|
import os from 'node:os';
|
|
7
8
|
import path from 'node:path';
|
|
@@ -31,6 +32,7 @@ export function formatKvLines(kv) {
|
|
|
31
32
|
if (kv.WATCHER_PORT !== undefined) lines.push(`WATCHER_PORT=${kv.WATCHER_PORT}`);
|
|
32
33
|
if (kv.SLOT_ID) lines.push(`SLOT_ID=${kv.SLOT_ID}`);
|
|
33
34
|
if (kv.IOS_SIMULATOR) lines.push(`IOS_SIMULATOR=${kv.IOS_SIMULATOR}`);
|
|
35
|
+
if (kv.SIM_UDID) lines.push(`SIM_UDID=${kv.SIM_UDID}`);
|
|
34
36
|
if (kv.ADB_SERIAL) lines.push(`ADB_SERIAL=${kv.ADB_SERIAL}`);
|
|
35
37
|
if (kv.ANDROID_DEVICE) lines.push(`ANDROID_DEVICE=${kv.ANDROID_DEVICE}`);
|
|
36
38
|
return lines.length ? `${lines.join('\n')}\n` : '';
|
|
@@ -239,10 +241,15 @@ export function resolveMobileRuntimeContext(repo) {
|
|
|
239
241
|
const c = JSON.parse(fs.readFileSync(ctxPath, 'utf8'));
|
|
240
242
|
const adbSerial = c.adbSerial ?? c.androidSerial;
|
|
241
243
|
const androidDevice = c.androidDevice;
|
|
242
|
-
|
|
244
|
+
const contextUdid = c.simulatorUdid && iosSimulatorExists(c.simulatorUdid)
|
|
245
|
+
? c.simulatorUdid
|
|
246
|
+
: matchingProvisionedSimulatorUdid(repo, c.simulator);
|
|
247
|
+
const simulator = c.simulator ?? contextUdid;
|
|
248
|
+
if (simulator || adbSerial || androidDevice || (c.metroPort != null && c.metroPort !== '')) {
|
|
243
249
|
return formatKvLines({
|
|
244
250
|
...(c.metroPort != null && c.metroPort !== '' ? { WATCHER_PORT: c.metroPort } : {}),
|
|
245
|
-
...(
|
|
251
|
+
...(simulator ? { IOS_SIMULATOR: simulator } : {}),
|
|
252
|
+
...(contextUdid ? { SIM_UDID: contextUdid } : {}),
|
|
246
253
|
...(adbSerial ? { ADB_SERIAL: adbSerial } : {}),
|
|
247
254
|
...(androidDevice ? { ANDROID_DEVICE: androidDevice } : {}),
|
|
248
255
|
...(c.slotId ? { SLOT_ID: c.slotId } : {}),
|
|
@@ -255,6 +262,21 @@ export function resolveMobileRuntimeContext(repo) {
|
|
|
255
262
|
return resolveMobileProvisionBaseline(repo);
|
|
256
263
|
}
|
|
257
264
|
|
|
265
|
+
function matchingProvisionedSimulatorUdid(repo, simulator) {
|
|
266
|
+
if (!simulator) return undefined;
|
|
267
|
+
const basePath = path.join(repo, recipeRuntimeDir(), 'runway-provision.json');
|
|
268
|
+
try {
|
|
269
|
+
const c = JSON.parse(fs.readFileSync(basePath, 'utf8'));
|
|
270
|
+
const recordedUdid = c.simulator?.udid;
|
|
271
|
+
const recordedName = c.simulator?.name;
|
|
272
|
+
const matches = simulator === recordedName ||
|
|
273
|
+
(recordedUdid && simulator.toLowerCase() === recordedUdid.toLowerCase());
|
|
274
|
+
return matches && recordedUdid && iosSimulatorExists(recordedUdid) ? recordedUdid : undefined;
|
|
275
|
+
} catch {
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
258
280
|
// A provisioned-but-unprepared slot has no agentic-runtime.json yet, but the
|
|
259
281
|
// provision baseline records the same authoritative simulator/port identity —
|
|
260
282
|
// without it, a fresh slot's first launch degrades to the simctl `booted`
|
|
@@ -264,12 +286,16 @@ function resolveMobileProvisionBaseline(repo) {
|
|
|
264
286
|
if (!fs.existsSync(basePath)) return null;
|
|
265
287
|
try {
|
|
266
288
|
const c = JSON.parse(fs.readFileSync(basePath, 'utf8'));
|
|
267
|
-
const
|
|
289
|
+
const recordedUdid = c.simulator?.udid;
|
|
290
|
+
const recordedName = c.simulator?.name;
|
|
291
|
+
const validUdid = recordedUdid && iosSimulatorExists(recordedUdid) ? recordedUdid : undefined;
|
|
292
|
+
const simulator = recordedName || validUdid || recordedUdid;
|
|
268
293
|
const watcherPort = c.watcherPort != null && c.watcherPort !== '' ? c.watcherPort : undefined;
|
|
269
294
|
if (!simulator && watcherPort === undefined) return null;
|
|
270
295
|
return formatKvLines({
|
|
271
296
|
...(watcherPort !== undefined ? { WATCHER_PORT: watcherPort } : {}),
|
|
272
297
|
...(simulator ? { IOS_SIMULATOR: simulator } : {}),
|
|
298
|
+
...(validUdid ? { SIM_UDID: validUdid } : {}),
|
|
273
299
|
...(c.slotId ? { SLOT_ID: c.slotId } : {}),
|
|
274
300
|
});
|
|
275
301
|
} catch {
|
|
@@ -277,6 +303,21 @@ function resolveMobileProvisionBaseline(repo) {
|
|
|
277
303
|
}
|
|
278
304
|
}
|
|
279
305
|
|
|
306
|
+
function iosSimulatorExists(udid) {
|
|
307
|
+
try {
|
|
308
|
+
const output = execFileSync(
|
|
309
|
+
'xcrun',
|
|
310
|
+
['simctl', 'list', 'devices', 'available', '--json'],
|
|
311
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 },
|
|
312
|
+
);
|
|
313
|
+
const devices = Object.values(JSON.parse(output).devices ?? {}).flat();
|
|
314
|
+
return devices.some((device) =>
|
|
315
|
+
device?.udid?.toLowerCase() === String(udid).toLowerCase() && device.isAvailable !== false);
|
|
316
|
+
} catch {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
280
321
|
export function resolveMobileRuntimePorts(repo) {
|
|
281
322
|
return (
|
|
282
323
|
resolveMobileRuntimeContext(repo)
|
|
@@ -118,6 +118,9 @@ apply_resolved_mobile_ports() {
|
|
|
118
118
|
IOS_SIMULATOR)
|
|
119
119
|
if [ "$from_pool" = true ] || [ -z "${IOS_SIMULATOR:-}" ]; then IOS_SIMULATOR="$val"; fi
|
|
120
120
|
;;
|
|
121
|
+
SIM_UDID)
|
|
122
|
+
if [ "$from_pool" = true ] || [ -z "${SIM_UDID:-}" ]; then SIM_UDID="$val"; fi
|
|
123
|
+
;;
|
|
121
124
|
SLOT_ID)
|
|
122
125
|
if [ "$from_pool" = true ] || [ -z "${RECIPE_SLOT_ID:-}" ]; then RECIPE_SLOT_ID="$val"; fi
|
|
123
126
|
;;
|
|
@@ -2,8 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
const DEFAULTS = {
|
|
5
|
-
MM_PERPS_ENABLED: "true"
|
|
6
|
-
OVERRIDE_REMOTE_FEATURE_FLAGS: "true"
|
|
5
|
+
MM_PERPS_ENABLED: "true"
|
|
7
6
|
};
|
|
8
7
|
function mobilePerpsEnvironment(target) {
|
|
9
8
|
const file = path.join(target, ".js.env");
|
|
@@ -14,16 +13,14 @@ function mobilePerpsEnvironment(target) {
|
|
|
14
13
|
return {
|
|
15
14
|
file: ".js.env",
|
|
16
15
|
fileExists,
|
|
17
|
-
status: perpsEnabled.status === "missing"
|
|
16
|
+
status: perpsEnabled.status === "missing" ? "incomplete" : "ready",
|
|
18
17
|
perpsEnabled,
|
|
19
18
|
remoteFeatureFlagOverride
|
|
20
19
|
};
|
|
21
20
|
}
|
|
22
21
|
function ensureMobilePerpsEnvironment(target) {
|
|
23
22
|
const current = mobilePerpsEnvironment(target);
|
|
24
|
-
const missing =
|
|
25
|
-
(key) => settingForKey(current, key).status === "missing"
|
|
26
|
-
);
|
|
23
|
+
const missing = current.perpsEnabled.status === "missing" ? ["MM_PERPS_ENABLED"] : [];
|
|
27
24
|
if (missing.length === 0) return [];
|
|
28
25
|
const file = path.join(fs.realpathSync(target), current.file);
|
|
29
26
|
const snapshot = readEnvironmentSnapshot(file);
|
|
@@ -84,9 +81,6 @@ function readSetting(source, key) {
|
|
|
84
81
|
if (value === "false") return { status: "disabled", value };
|
|
85
82
|
return { status: "configured", value };
|
|
86
83
|
}
|
|
87
|
-
function settingForKey(environment, key) {
|
|
88
|
-
return key === "MM_PERPS_ENABLED" ? environment.perpsEnabled : environment.remoteFeatureFlagOverride;
|
|
89
|
-
}
|
|
90
84
|
function normalizeValue(raw) {
|
|
91
85
|
const value = raw.trim();
|
|
92
86
|
if (value.length >= 2) {
|
|
@@ -402,6 +402,7 @@ async function dispatchAction(action, target, platform, json, preflightMode = "f
|
|
|
402
402
|
}
|
|
403
403
|
case "launch-mobile-runtime": {
|
|
404
404
|
const leaf = path.join(runnerDir, "adapters/mobile/open-device.sh");
|
|
405
|
+
const simulatorArgs = platform === "ios" ? ["--simulator", process.env.SIM_UDID || process.env.IOS_SIMULATOR || "booted"] : [];
|
|
405
406
|
return spawnScriptStreaming(
|
|
406
407
|
leaf,
|
|
407
408
|
withWatcherPort(
|
|
@@ -412,6 +413,7 @@ async function dispatchAction(action, target, platform, json, preflightMode = "f
|
|
|
412
413
|
cwd,
|
|
413
414
|
"--preflight-mode",
|
|
414
415
|
preflightMode,
|
|
416
|
+
...simulatorArgs,
|
|
415
417
|
...action.argv ?? []
|
|
416
418
|
],
|
|
417
419
|
watcherPort
|
|
@@ -422,10 +424,11 @@ async function dispatchAction(action, target, platform, json, preflightMode = "f
|
|
|
422
424
|
}
|
|
423
425
|
case "ensure-device-ui": {
|
|
424
426
|
const leaf = path.join(runnerDir, "adapters/mobile/open-device.sh");
|
|
427
|
+
const simulatorArgs = platform === "ios" ? ["--simulator", process.env.SIM_UDID || process.env.IOS_SIMULATOR || "booted"] : [];
|
|
425
428
|
return spawnScriptStreaming(
|
|
426
429
|
leaf,
|
|
427
430
|
withWatcherPort(
|
|
428
|
-
["--platform", platform, "--target", cwd, "--ui-only"],
|
|
431
|
+
["--platform", platform, "--target", cwd, ...simulatorArgs, "--ui-only"],
|
|
429
432
|
watcherPort
|
|
430
433
|
),
|
|
431
434
|
target,
|
|
@@ -12,6 +12,8 @@ const RUNWAY_IOS_METADATA = {
|
|
|
12
12
|
appDirName: "MetaMask.app",
|
|
13
13
|
fallbackRepo: "MetaMask/metamask-mobile"
|
|
14
14
|
};
|
|
15
|
+
class SimulatorTargetError extends Error {
|
|
16
|
+
}
|
|
15
17
|
function resolvePoolIdentity(target) {
|
|
16
18
|
const out = resolveSlotPortsByRepo(target);
|
|
17
19
|
const id = {};
|
|
@@ -35,7 +37,7 @@ async function provisionRunwayMobile(target, options) {
|
|
|
35
37
|
if (platform !== "ios") {
|
|
36
38
|
return fail(resolvedTarget, platform, "UNSUPPORTED_PLATFORM", "runway provisioning currently installs the iOS .app artifact only.", command, slot);
|
|
37
39
|
}
|
|
38
|
-
let simulator = options.simulator ?? slot.simulator ?? process.env.IOS_SIMULATOR;
|
|
40
|
+
let simulator = options.simulator ?? slot.simulatorUdid ?? slot.simulator ?? process.env.SIM_UDID ?? process.env.IOS_SIMULATOR;
|
|
39
41
|
try {
|
|
40
42
|
const pool = resolvePoolIdentity(resolvedTarget);
|
|
41
43
|
if (!simulator && pool.simulator) simulator = pool.simulator;
|
|
@@ -133,6 +135,16 @@ async function provisionRunwayMobile(target, options) {
|
|
|
133
135
|
reason
|
|
134
136
|
};
|
|
135
137
|
} catch (error) {
|
|
138
|
+
if (error instanceof SimulatorTargetError) {
|
|
139
|
+
return fail(
|
|
140
|
+
resolvedTarget,
|
|
141
|
+
platform,
|
|
142
|
+
"SIMULATOR_AMBIGUOUS",
|
|
143
|
+
error.message,
|
|
144
|
+
`mm-harness status --adapter mobile --target ${JSON.stringify(resolvedTarget)} --all-devices --json`,
|
|
145
|
+
slot
|
|
146
|
+
);
|
|
147
|
+
}
|
|
136
148
|
return fail(resolvedTarget, platform, "PROVISION_FAILED", errorMessage(error), command, slot);
|
|
137
149
|
}
|
|
138
150
|
}
|
|
@@ -178,6 +190,7 @@ function readSlotContext(target, runtimeDir) {
|
|
|
178
190
|
slotId: stringField(data, "slotId"),
|
|
179
191
|
platform: stringField(data, "platform"),
|
|
180
192
|
simulator: stringField(data, "simulator") ?? stringField(data, "iosSimulator"),
|
|
193
|
+
simulatorUdid: stringField(data, "simulatorUdid"),
|
|
181
194
|
runtime: stringField(data, "runtime") ?? stringField(data, "iosRuntime"),
|
|
182
195
|
deviceType: stringField(data, "deviceType") ?? stringField(data, "iosDeviceType"),
|
|
183
196
|
watcherPort: stringField(data, "watcherPort") ?? stringField(data, "metroPort") ?? stringField(data, "devServerPort"),
|
|
@@ -456,7 +469,7 @@ function preapproveDeepLinkScheme(device, bundleId, options) {
|
|
|
456
469
|
}
|
|
457
470
|
function ensureSimulator(name, runtime, deviceType) {
|
|
458
471
|
const existing = findSimulator(name);
|
|
459
|
-
if (existing) return { name, udid: existing, created: false, runtime, deviceType };
|
|
472
|
+
if (existing) return { name: existing.name, udid: existing.udid, created: false, runtime, deviceType };
|
|
460
473
|
const resolvedRuntime = runtime || latestIosRuntime();
|
|
461
474
|
const resolvedDeviceType = deviceType || preferredIphoneDeviceType();
|
|
462
475
|
if (!resolvedRuntime || !resolvedDeviceType) {
|
|
@@ -465,11 +478,23 @@ function ensureSimulator(name, runtime, deviceType) {
|
|
|
465
478
|
const udid = execFileSync("xcrun", ["simctl", "create", name, resolvedDeviceType, resolvedRuntime], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
466
479
|
return { name, udid: udid || name, created: true, runtime: resolvedRuntime, deviceType: resolvedDeviceType };
|
|
467
480
|
}
|
|
468
|
-
function findSimulator(
|
|
469
|
-
const data = execJson(
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
481
|
+
function findSimulator(target) {
|
|
482
|
+
const data = execJson(
|
|
483
|
+
"xcrun",
|
|
484
|
+
["simctl", "list", "devices", "--json"]
|
|
485
|
+
);
|
|
486
|
+
const devices = Object.values(data.devices ?? {}).flat().filter((device) => device.isAvailable !== false && Boolean(device.name && device.udid));
|
|
487
|
+
const exact = devices.find((device) => device.udid.toLowerCase() === target.toLowerCase());
|
|
488
|
+
if (exact) return exact;
|
|
489
|
+
const matches = target === "booted" ? devices.filter((device) => device.state === "Booted") : devices.filter((device) => device.name === target);
|
|
490
|
+
if (matches.length === 1) return matches[0];
|
|
491
|
+
const booted = matches.filter((device) => device.state === "Booted");
|
|
492
|
+
if (booted.length === 1) return booted[0];
|
|
493
|
+
if (matches.length > 1) {
|
|
494
|
+
throw new SimulatorTargetError(
|
|
495
|
+
`iOS simulator target '${target}' is ambiguous; use one UDID:
|
|
496
|
+
` + matches.map((device) => ` - ${device.udid} (${device.name}) [${device.state ?? "unknown"}]`).join("\n")
|
|
497
|
+
);
|
|
473
498
|
}
|
|
474
499
|
return void 0;
|
|
475
500
|
}
|
|
@@ -78,7 +78,7 @@ function appRunningOnDevice(platform) {
|
|
|
78
78
|
});
|
|
79
79
|
return out2.includes("io.metamask");
|
|
80
80
|
}
|
|
81
|
-
const device = process.env.IOS_SIMULATOR || "booted";
|
|
81
|
+
const device = process.env.SIM_UDID || process.env.IOS_SIMULATOR || "booted";
|
|
82
82
|
const out = execFileSync("xcrun", ["simctl", "spawn", device, "launchctl", "list"], {
|
|
83
83
|
encoding: "utf8",
|
|
84
84
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -22,7 +22,14 @@ function applyKVLines(output, overwrite) {
|
|
|
22
22
|
}
|
|
23
23
|
break;
|
|
24
24
|
case "IOS_SIMULATOR":
|
|
25
|
-
if (overwrite || !process.env["IOS_SIMULATOR"])
|
|
25
|
+
if (overwrite || !process.env["IOS_SIMULATOR"]) {
|
|
26
|
+
process.env["IOS_SIMULATOR"] = val;
|
|
27
|
+
if (/^[0-9A-Fa-f-]{36}$/u.test(val)) process.env["SIM_UDID"] = val;
|
|
28
|
+
else delete process.env["SIM_UDID"];
|
|
29
|
+
}
|
|
30
|
+
break;
|
|
31
|
+
case "SIM_UDID":
|
|
32
|
+
if (overwrite || !process.env["SIM_UDID"]) process.env["SIM_UDID"] = val;
|
|
26
33
|
break;
|
|
27
34
|
case "ADB_SERIAL":
|
|
28
35
|
if (overwrite || !process.env["ADB_SERIAL"]) {
|
|
@@ -269,6 +269,28 @@ async function handleLaunchLocked(argv, stream) {
|
|
|
269
269
|
originalError: rebuildAttempt.output.trim() || void 0
|
|
270
270
|
});
|
|
271
271
|
}
|
|
272
|
+
const missingIosSimulator = adapter === "mobile" ? missingConfiguredIosSimulator(attempt.output) : void 0;
|
|
273
|
+
const ambiguousIosSimulator = adapter === "mobile" ? ambiguousConfiguredIosSimulator(attempt.output) : void 0;
|
|
274
|
+
if (ambiguousIosSimulator) {
|
|
275
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
276
|
+
code: "MOBILE_DEVICE_AMBIGUOUS",
|
|
277
|
+
message: `more than one iOS simulator matches ${ambiguousIosSimulator}.`,
|
|
278
|
+
recoverable: false,
|
|
279
|
+
userAction: `mm-harness status --adapter mobile --target ${shellQuote(target)} --all-devices --json`,
|
|
280
|
+
exitCode: EXIT.runtime,
|
|
281
|
+
originalError: attempt.output.trim() || void 0
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
if (missingIosSimulator && isIosSimulatorUdid(missingIosSimulator)) {
|
|
285
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
286
|
+
code: "MOBILE_DEVICE_NOT_FOUND",
|
|
287
|
+
message: `the configured iOS simulator ${missingIosSimulator} no longer exists.`,
|
|
288
|
+
recoverable: false,
|
|
289
|
+
userAction: `mm-harness status --adapter mobile --target ${shellQuote(target)} --all-devices --json`,
|
|
290
|
+
exitCode: EXIT.runtime,
|
|
291
|
+
originalError: attempt.output.trim() || void 0
|
|
292
|
+
});
|
|
293
|
+
}
|
|
272
294
|
if (adapter === "mobile" && mobileProvisioningBlocked(attempt.output)) {
|
|
273
295
|
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
274
296
|
code: "MOBILE_PROVISION_REQUIRED",
|
|
@@ -331,12 +353,18 @@ function extensionProductConfigMissing(output) {
|
|
|
331
353
|
function mobileProvisioningBlocked(output) {
|
|
332
354
|
return /open-device: configured iOS simulator '.+' does not exist|open-device: no MetaMask bundle found|fast mode requires an installed (?:iOS dev client|Android dev client)/u.test(output);
|
|
333
355
|
}
|
|
356
|
+
function missingConfiguredIosSimulator(output) {
|
|
357
|
+
return /open-device: configured iOS simulator '([^']+)' does not exist/u.exec(output)?.[1];
|
|
358
|
+
}
|
|
359
|
+
function ambiguousConfiguredIosSimulator(output) {
|
|
360
|
+
return /open-device: iOS simulator target ("[^"]+") is ambiguous/u.exec(output)?.[1];
|
|
361
|
+
}
|
|
334
362
|
function mobileBridgeTargetMissing(output) {
|
|
335
363
|
return /no bridge target matched/iu.test(output);
|
|
336
364
|
}
|
|
337
365
|
function mobileProvisionCommand(target, mobileTarget) {
|
|
338
366
|
const platform = mobileTarget === "android" ? "android" : "ios";
|
|
339
|
-
const device = platform === "ios" ? process.env.
|
|
367
|
+
const device = platform === "ios" ? process.env.SIM_UDID || process.env.IOS_SIMULATOR : process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || process.env.ANDROID_DEVICE;
|
|
340
368
|
const parts = ["mm-harness", "provision", "runway", platform, "--adapter", "mobile", "--target", shellQuote(target)];
|
|
341
369
|
if (device) parts.push("--device", shellQuote(device));
|
|
342
370
|
return parts.join(" ");
|
|
@@ -46,19 +46,21 @@ async function handleProvision({ positional, options, rawArgv }) {
|
|
|
46
46
|
resolveOnly: optionFlag(options, "resolveOnly"),
|
|
47
47
|
rerunCommand
|
|
48
48
|
});
|
|
49
|
-
const
|
|
49
|
+
const provisionedSimulator = typeof result.simulator === "object" && result.simulator ? result.simulator : void 0;
|
|
50
|
+
const provisionedDevice = typeof provisionedSimulator?.udid === "string" ? provisionedSimulator.udid : typeof provisionedSimulator?.name === "string" ? provisionedSimulator.name : void 0;
|
|
51
|
+
const next = result.status === "pass" && adapter === "mobile" && result.resolveOnly !== true ? `mm-harness launch ${String(result.platform ?? platform)} --adapter mobile --target ${shellQuote(target)}` + (provisionedDevice ? ` --device ${shellQuoteArg(provisionedDevice)}` : "") : void 0;
|
|
50
52
|
const output = next ? { ...result, next } : result;
|
|
51
53
|
if (json) {
|
|
52
54
|
console.log(JSON.stringify(output, null, 2));
|
|
53
55
|
} else if (result.status === "pass") {
|
|
54
56
|
const cache = typeof result.cache === "object" && result.cache ? result.cache : void 0;
|
|
55
|
-
const simulator =
|
|
57
|
+
const simulator = provisionedSimulator;
|
|
56
58
|
const artifact = typeof result.artifact === "object" && result.artifact ? result.artifact : void 0;
|
|
57
59
|
if (result.resolveOnly) {
|
|
58
60
|
console.error(`\u2713 resolved ${adapter} ${result.platform ?? ""} run=${artifact?.runId ?? "unknown"} revision=${artifact?.revision ?? "unknown"} artifact=${artifact?.artifactName ?? "unknown"}`);
|
|
59
61
|
} else {
|
|
60
62
|
const action = result.skipped ? "already provisioned" : "provisioned";
|
|
61
|
-
console.error(`\u2713 ${action} ${adapter} ${result.platform ?? ""} simulator=${simulator?.name ?? "unknown"} cache=${cache?.status ?? "unknown"}`);
|
|
63
|
+
console.error(`\u2713 ${action} ${adapter} ${result.platform ?? ""} simulator=${simulator?.name ?? "unknown"} udid=${simulator?.udid ?? "unknown"} cache=${cache?.status ?? "unknown"}`);
|
|
62
64
|
}
|
|
63
65
|
if (next) console.error(` Next: ${next}`);
|
|
64
66
|
} else {
|
package/dist/harness.js
CHANGED
|
@@ -428,19 +428,21 @@ async function handleRunwayInstall(adapter, target, forward, json) {
|
|
|
428
428
|
resolveOnly: hasArg(forward, "--resolve-only"),
|
|
429
429
|
rerunCommand
|
|
430
430
|
});
|
|
431
|
-
const
|
|
431
|
+
const provisionedSimulator = typeof result.simulator === "object" && result.simulator ? result.simulator : void 0;
|
|
432
|
+
const provisionedDevice = typeof provisionedSimulator?.udid === "string" ? provisionedSimulator.udid : typeof provisionedSimulator?.name === "string" ? provisionedSimulator.name : void 0;
|
|
433
|
+
const next = result.status === "pass" && adapter === "mobile" && result.resolveOnly !== true ? `mm-harness launch ${String(result.platform ?? "ios")} --adapter mobile --target ${shellQuote(target)}` + (provisionedDevice ? ` --device ${shellQuote(provisionedDevice)}` : "") : void 0;
|
|
432
434
|
const output = next ? { ...result, next } : result;
|
|
433
435
|
if (json) {
|
|
434
436
|
console.log(JSON.stringify(output, null, 2));
|
|
435
437
|
} else if (result.status === "pass") {
|
|
436
438
|
const cache = typeof result.cache === "object" && result.cache ? result.cache : void 0;
|
|
437
|
-
const simulator =
|
|
439
|
+
const simulator = provisionedSimulator;
|
|
438
440
|
const artifact = typeof result.artifact === "object" && result.artifact ? result.artifact : void 0;
|
|
439
441
|
if (result.resolveOnly) {
|
|
440
442
|
console.error(`\u2713 resolved Runway app for ${adapter} ${result.platform ?? ""} run=${artifact?.runId ?? "unknown"} revision=${artifact?.revision ?? "unknown"} artifact=${artifact?.artifactName ?? "unknown"}`);
|
|
441
443
|
} else {
|
|
442
444
|
const action = result.skipped ? "already provisioned" : "installed Runway app";
|
|
443
|
-
console.error(`\u2713 ${action} for ${adapter} ${result.platform ?? ""} simulator=${simulator?.name ?? "unknown"} cache=${cache?.status ?? "skip"}`);
|
|
445
|
+
console.error(`\u2713 ${action} for ${adapter} ${result.platform ?? ""} simulator=${simulator?.name ?? "unknown"} udid=${simulator?.udid ?? "unknown"} cache=${cache?.status ?? "skip"}`);
|
|
444
446
|
}
|
|
445
447
|
if (next) console.error(` Next: ${next}`);
|
|
446
448
|
} else {
|
package/dist/runtime-context.js
CHANGED
|
@@ -65,6 +65,7 @@ function validExistingContext(context, repoRoot, runtimeDir, adapter) {
|
|
|
65
65
|
"machine",
|
|
66
66
|
"runtimeOwner",
|
|
67
67
|
"simulator",
|
|
68
|
+
"simulatorUdid",
|
|
68
69
|
"adbSerial",
|
|
69
70
|
"distDir",
|
|
70
71
|
"extensionId",
|
|
@@ -90,7 +91,7 @@ function validExistingContext(context, repoRoot, runtimeDir, adapter) {
|
|
|
90
91
|
if (runtimeStart[field] !== void 0 && (typeof runtimeStart[field] !== "string" || runtimeStart[field].length === 0)) return false;
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
|
-
const forbiddenResources = adapter === "core" ? ["cdpPort", "watcherPort", "metroPort", "devServerPort", "simulator", "adbSerial", "extensionId"] : adapter === "extension" ? ["metroPort", "simulator", "adbSerial"] : ["cdpPort", "extensionId"];
|
|
94
|
+
const forbiddenResources = adapter === "core" ? ["cdpPort", "watcherPort", "metroPort", "devServerPort", "simulator", "simulatorUdid", "adbSerial", "extensionId"] : adapter === "extension" ? ["metroPort", "simulator", "simulatorUdid", "adbSerial"] : ["cdpPort", "extensionId"];
|
|
94
95
|
if (forbiddenResources.some((field) => context[field] !== void 0)) return false;
|
|
95
96
|
return true;
|
|
96
97
|
}
|
|
@@ -168,6 +169,7 @@ function runtimeResources(adapter, existing, defaults) {
|
|
|
168
169
|
} else {
|
|
169
170
|
shared.metroPort = watcherPort;
|
|
170
171
|
shared.simulator = process.env.IOS_SIMULATOR || existing.simulator;
|
|
172
|
+
shared.simulatorUdid = process.env.SIM_UDID || existing.simulatorUdid;
|
|
171
173
|
shared.adbSerial = process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || existing.adbSerial;
|
|
172
174
|
}
|
|
173
175
|
return shared;
|
|
@@ -217,6 +219,7 @@ function hydrateRuntimeEnv(contextPath, context) {
|
|
|
217
219
|
setEnv("RECIPE_WATCHER_PORT", context.watcherPort);
|
|
218
220
|
setEnv("METRO_PORT", context.metroPort ?? context.watcherPort);
|
|
219
221
|
setEnv("IOS_SIMULATOR", context.simulator);
|
|
222
|
+
setEnv("SIM_UDID", context.simulatorUdid);
|
|
220
223
|
setEnv("ADB_SERIAL", context.adbSerial);
|
|
221
224
|
setEnv("ANDROID_SERIAL", context.adbSerial);
|
|
222
225
|
}
|