agent-device-proxy 0.1.5 → 0.2.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.
@@ -0,0 +1,1117 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # SSH-friendly host setup for self-hosted macOS mobile execution nodes.
5
+ # Installs Xcode toolchain + simulator defaults, Android SDK/emulator defaults,
6
+ # Node.js toolchain, and CLI dependencies used by runtime scripts.
7
+
8
+ MACOS_SETUP_BREW_UPDATE="${MACOS_SETUP_BREW_UPDATE:-true}"
9
+ MACOS_SETUP_INSTALL_XCODE="${MACOS_SETUP_INSTALL_XCODE:-true}"
10
+ MACOS_SETUP_XCODE_VERSION="${MACOS_SETUP_XCODE_VERSION:-latest}"
11
+ MACOS_SETUP_STRICT_XCODE="${MACOS_SETUP_STRICT_XCODE:-true}"
12
+ MACOS_SETUP_INSTALL_IOS_RUNTIME="${MACOS_SETUP_INSTALL_IOS_RUNTIME:-true}"
13
+ MACOS_SETUP_IOS_SIMULATOR_NAME="${MACOS_SETUP_IOS_SIMULATOR_NAME:-iPhone 17 Pro}"
14
+ MACOS_SETUP_INSTALL_ANDROID="${MACOS_SETUP_INSTALL_ANDROID:-true}"
15
+ MACOS_SETUP_ANDROID_API="${MACOS_SETUP_ANDROID_API:-latest}"
16
+ MACOS_SETUP_ANDROID_DEVICE_PROFILE="${MACOS_SETUP_ANDROID_DEVICE_PROFILE:-pixel_9_pro_xl}"
17
+ MACOS_SETUP_ANDROID_AVD_NAME="${MACOS_SETUP_ANDROID_AVD_NAME:-Pixel_9_Pro_XL}"
18
+ MACOS_SETUP_INSTALL_AGENT_DEVICE="${MACOS_SETUP_INSTALL_AGENT_DEVICE:-true}"
19
+ MACOS_SETUP_INSTALL_COCOAPODS="${MACOS_SETUP_INSTALL_COCOAPODS:-true}"
20
+ MACOS_SETUP_INSTALL_ZULU_JDK="${MACOS_SETUP_INSTALL_ZULU_JDK:-${MACOS_SETUP_INSTALL_OPENJDK:-true}}"
21
+ MACOS_SETUP_SHELL_PROFILE="${MACOS_SETUP_SHELL_PROFILE:-$HOME/.zprofile}"
22
+ ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT:-$HOME/Library/Android/sdk}"
23
+ BREW_READY="false"
24
+ BREW_UPDATED="false"
25
+ DRY_RUN="false"
26
+ DRY_RUN_INSTALLS=()
27
+ DRY_RUN_SKIPS=()
28
+
29
+ prime_default_macos_paths() {
30
+ local candidate
31
+ for candidate in /opt/homebrew/bin /opt/homebrew/sbin /usr/local/bin /usr/local/sbin; do
32
+ if [[ -d "$candidate" && ":$PATH:" != *":$candidate:"* ]]; then
33
+ PATH="$candidate:$PATH"
34
+ fi
35
+ done
36
+ export PATH
37
+ }
38
+
39
+ resolve_login_shell() {
40
+ if [[ -n "${SHELL:-}" && -x "${SHELL:-}" ]]; then
41
+ printf "%s\n" "$SHELL"
42
+ return
43
+ fi
44
+ if command -v dscl >/dev/null 2>&1; then
45
+ dscl . -read "/Users/$USER" UserShell 2>/dev/null | awk '/UserShell:/ {print $2; exit}'
46
+ return
47
+ fi
48
+ printf "\n"
49
+ }
50
+
51
+ prime_login_shell_environment() {
52
+ local login_shell
53
+ login_shell="$(resolve_login_shell)"
54
+ if [[ -z "$login_shell" || ! -x "$login_shell" ]]; then
55
+ return
56
+ fi
57
+
58
+ local shell_output
59
+ shell_output="$(
60
+ "$login_shell" -lc '
61
+ printf "__MACOS_REMOTE_SETUP_ENV_START__\n"
62
+ printf "PATH=%s\n" "$PATH"
63
+ printf "ANDROID_SDK_ROOT=%s\n" "${ANDROID_SDK_ROOT:-}"
64
+ printf "ANDROID_HOME=%s\n" "${ANDROID_HOME:-}"
65
+ printf "JAVA_HOME=%s\n" "${JAVA_HOME:-}"
66
+ printf "__MACOS_REMOTE_SETUP_ENV_END__\n"
67
+ ' 2>/dev/null || true
68
+ )"
69
+
70
+ if [[ "$shell_output" != *"__MACOS_REMOTE_SETUP_ENV_START__"* ]]; then
71
+ return
72
+ fi
73
+
74
+ local env_lines
75
+ env_lines="$(
76
+ printf "%s\n" "$shell_output" | awk '
77
+ $0 == "__MACOS_REMOTE_SETUP_ENV_START__" { capture=1; next }
78
+ $0 == "__MACOS_REMOTE_SETUP_ENV_END__" { capture=0; exit }
79
+ capture { print }
80
+ '
81
+ )"
82
+
83
+ local key value
84
+ while IFS='=' read -r key value; do
85
+ case "$key" in
86
+ PATH)
87
+ if [[ -n "$value" ]]; then
88
+ PATH="$value"
89
+ fi
90
+ ;;
91
+ ANDROID_SDK_ROOT)
92
+ if [[ -n "$value" ]]; then
93
+ ANDROID_SDK_ROOT="$value"
94
+ fi
95
+ ;;
96
+ ANDROID_HOME)
97
+ if [[ -n "$value" ]]; then
98
+ ANDROID_HOME="$value"
99
+ fi
100
+ ;;
101
+ JAVA_HOME)
102
+ if [[ -n "$value" ]]; then
103
+ JAVA_HOME="$value"
104
+ fi
105
+ ;;
106
+ esac
107
+ done <<<"$env_lines"
108
+
109
+ export PATH ANDROID_SDK_ROOT ANDROID_HOME JAVA_HOME
110
+ }
111
+
112
+ normalize_bool() {
113
+ local raw
114
+ raw="$(printf "%s" "$1" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
115
+ case "$raw" in
116
+ 1|true|yes|on) printf "true\n" ;;
117
+ 0|false|no|off) printf "false\n" ;;
118
+ *) echo "Error: expected boolean-like value, got '$1'." >&2; exit 64 ;;
119
+ esac
120
+ }
121
+
122
+ log() {
123
+ printf "[macos-remote-setup] %s\n" "$1"
124
+ }
125
+
126
+ warn() {
127
+ printf "[macos-remote-setup] Warning: %s\n" "$1" >&2
128
+ }
129
+
130
+ fail() {
131
+ printf "[macos-remote-setup] Error: %s\n" "$1" >&2
132
+ exit 64
133
+ }
134
+
135
+ usage() {
136
+ cat <<'EOF_USAGE'
137
+ Usage: macos-remote-setup.sh [--dry-run|-n] [--help|-h]
138
+
139
+ Options:
140
+ -n, --dry-run Detect and print what would be installed/skipped without applying changes.
141
+ -h, --help Show this help.
142
+ EOF_USAGE
143
+ }
144
+
145
+ parse_args() {
146
+ while [[ "$#" -gt 0 ]]; do
147
+ case "$1" in
148
+ -n|--dry-run)
149
+ DRY_RUN="true"
150
+ ;;
151
+ -h|--help)
152
+ usage
153
+ exit 0
154
+ ;;
155
+ *)
156
+ fail "unknown argument: $1"
157
+ ;;
158
+ esac
159
+ shift
160
+ done
161
+ }
162
+
163
+ record_dry_run_install() {
164
+ local label="$1"
165
+ DRY_RUN_INSTALLS+=("$label")
166
+ }
167
+
168
+ record_dry_run_skip() {
169
+ local label="$1"
170
+ DRY_RUN_SKIPS+=("$label")
171
+ }
172
+
173
+ log_dry_run_command() {
174
+ local rendered=""
175
+ local arg
176
+ for arg in "$@"; do
177
+ rendered+=$(printf "%q " "$arg")
178
+ done
179
+ log "[dry-run] would run: ${rendered% }"
180
+ }
181
+
182
+ run_as_admin() {
183
+ if [[ "$DRY_RUN" == "true" ]]; then
184
+ if [[ "$(id -u)" == "0" ]]; then
185
+ log_dry_run_command "$@"
186
+ else
187
+ log_dry_run_command sudo "$@"
188
+ fi
189
+ return
190
+ fi
191
+ if [[ "$(id -u)" == "0" ]]; then
192
+ "$@"
193
+ return
194
+ fi
195
+ sudo "$@"
196
+ }
197
+
198
+ ensure_macos_host() {
199
+ if [[ "$(uname -s)" != "Darwin" ]]; then
200
+ fail "this script requires macOS (Darwin)."
201
+ fi
202
+ }
203
+
204
+ ensure_homebrew() {
205
+ if command -v brew >/dev/null 2>&1; then
206
+ if [[ "$DRY_RUN" == "true" ]]; then
207
+ record_dry_run_skip "homebrew"
208
+ fi
209
+ return
210
+ fi
211
+
212
+ if [[ "$DRY_RUN" == "true" ]]; then
213
+ record_dry_run_install "homebrew"
214
+ log "Homebrew not found."
215
+ log "[dry-run] would run: NONINTERACTIVE=1 /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""
216
+ return
217
+ fi
218
+ log "Homebrew not found. Installing Homebrew."
219
+ NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
220
+ }
221
+
222
+ load_brew_shellenv() {
223
+ local brew_bin
224
+ for brew_bin in /opt/homebrew/bin/brew /usr/local/bin/brew; do
225
+ if [[ -x "$brew_bin" ]]; then
226
+ eval "$("$brew_bin" shellenv)"
227
+ return
228
+ fi
229
+ done
230
+ fail "brew executable was not found after installation."
231
+ }
232
+
233
+ ensure_brew_ready() {
234
+ if [[ "$BREW_READY" == "true" ]]; then
235
+ return
236
+ fi
237
+ if ! command -v brew >/dev/null 2>&1; then
238
+ ensure_homebrew
239
+ if [[ "$DRY_RUN" == "true" ]]; then
240
+ BREW_READY="true"
241
+ return
242
+ fi
243
+ fi
244
+ load_brew_shellenv
245
+ BREW_READY="true"
246
+ if [[ "$MACOS_SETUP_BREW_UPDATE" == "true" && "$BREW_UPDATED" != "true" ]]; then
247
+ if [[ "$DRY_RUN" == "true" ]]; then
248
+ log_dry_run_command brew update
249
+ else
250
+ log "Running brew update."
251
+ brew update
252
+ fi
253
+ BREW_UPDATED="true"
254
+ fi
255
+ }
256
+
257
+ brew_install_formula_if_missing() {
258
+ local name="$1"
259
+ ensure_brew_ready
260
+ if ! command -v brew >/dev/null 2>&1; then
261
+ if [[ "$DRY_RUN" == "true" ]]; then
262
+ record_dry_run_install "brew formula: $name"
263
+ log_dry_run_command brew install "$name"
264
+ return
265
+ fi
266
+ fail "brew is required to install formula: $name"
267
+ fi
268
+ if brew list --formula "$name" >/dev/null 2>&1; then
269
+ if [[ "$DRY_RUN" == "true" ]]; then
270
+ record_dry_run_skip "brew formula: $name"
271
+ fi
272
+ return
273
+ fi
274
+ if [[ "$DRY_RUN" == "true" ]]; then
275
+ record_dry_run_install "brew formula: $name"
276
+ log_dry_run_command brew install "$name"
277
+ return
278
+ fi
279
+ log "Installing Homebrew formula: $name"
280
+ brew install "$name"
281
+ }
282
+
283
+ brew_install_cask_if_missing() {
284
+ local name="$1"
285
+ ensure_brew_ready
286
+ if ! command -v brew >/dev/null 2>&1; then
287
+ if [[ "$DRY_RUN" == "true" ]]; then
288
+ record_dry_run_install "brew cask: $name"
289
+ log_dry_run_command brew install --cask "$name"
290
+ return
291
+ fi
292
+ fail "brew is required to install cask: $name"
293
+ fi
294
+ if brew list --cask "$name" >/dev/null 2>&1; then
295
+ if [[ "$DRY_RUN" == "true" ]]; then
296
+ record_dry_run_skip "brew cask: $name"
297
+ fi
298
+ return
299
+ fi
300
+ if [[ "$DRY_RUN" == "true" ]]; then
301
+ record_dry_run_install "brew cask: $name"
302
+ log_dry_run_command brew install --cask "$name"
303
+ return
304
+ fi
305
+ log "Installing Homebrew cask: $name"
306
+ brew install --cask "$name"
307
+ }
308
+
309
+ ensure_profile_export_block() {
310
+ local profile_path="$1"
311
+ local marker_begin="# >>> macos-remote-setup >>>"
312
+ local marker_end="# <<< macos-remote-setup <<<"
313
+ local block
314
+ block="$(cat <<EOF_BLOCK
315
+ $marker_begin
316
+ export ANDROID_SDK_ROOT="$ANDROID_SDK_ROOT"
317
+ export ANDROID_HOME="$ANDROID_SDK_ROOT"
318
+ if [[ -d "/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home" ]]; then
319
+ export JAVA_HOME="/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home"
320
+ elif [[ -x "/usr/libexec/java_home" ]]; then
321
+ export JAVA_HOME="$(/usr/libexec/java_home -v 17 2>/dev/null || true)"
322
+ fi
323
+ if [[ -n "\${JAVA_HOME:-}" ]]; then
324
+ export PATH="\$JAVA_HOME/bin:\$PATH"
325
+ fi
326
+ export PATH="\$PATH:\$ANDROID_SDK_ROOT/platform-tools:\$ANDROID_SDK_ROOT/emulator:\$ANDROID_SDK_ROOT/cmdline-tools/latest/bin"
327
+ if [[ -d "\$HOME/.bun/bin" ]]; then
328
+ export PATH="\$HOME/.bun/bin:\$PATH"
329
+ fi
330
+ if command -v brew >/dev/null 2>&1; then
331
+ eval "\$(brew shellenv)"
332
+ fi
333
+ $marker_end
334
+ EOF_BLOCK
335
+ )"
336
+
337
+ if [[ "$DRY_RUN" == "true" ]]; then
338
+ if [[ -f "$profile_path" ]]; then
339
+ if grep -Fq "$marker_begin" "$profile_path"; then
340
+ record_dry_run_skip "shell profile block: $profile_path"
341
+ else
342
+ record_dry_run_install "shell profile block: $profile_path"
343
+ fi
344
+ else
345
+ record_dry_run_install "shell profile block: $profile_path"
346
+ fi
347
+ log_dry_run_command update-shell-profile "$profile_path"
348
+ return
349
+ fi
350
+
351
+ mkdir -p "$(dirname "$profile_path")"
352
+ touch "$profile_path"
353
+ if grep -Fq "$marker_begin" "$profile_path"; then
354
+ awk -v begin="$marker_begin" -v end="$marker_end" -v replace="$block" '
355
+ $0 == begin { print replace; in_block=1; next }
356
+ $0 == end { in_block=0; next }
357
+ !in_block { print }
358
+ ' "$profile_path" >"${profile_path}.tmp"
359
+ mv "${profile_path}.tmp" "$profile_path"
360
+ return
361
+ fi
362
+
363
+ {
364
+ printf "\n%s\n" "$block"
365
+ } >>"$profile_path"
366
+ }
367
+
368
+ ensure_path_for_current_session() {
369
+ export ANDROID_HOME="$ANDROID_SDK_ROOT"
370
+ if [[ -d "/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home" ]]; then
371
+ export JAVA_HOME="/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home"
372
+ elif [[ -x "/usr/libexec/java_home" ]]; then
373
+ export JAVA_HOME="$(/usr/libexec/java_home -v 17 2>/dev/null || true)"
374
+ fi
375
+ if [[ -n "${JAVA_HOME:-}" ]]; then
376
+ export PATH="$JAVA_HOME/bin:$PATH"
377
+ fi
378
+ export PATH="$PATH:$ANDROID_SDK_ROOT/platform-tools:$ANDROID_SDK_ROOT/emulator:$ANDROID_SDK_ROOT/cmdline-tools/latest/bin"
379
+ if [[ -d "$HOME/.bun/bin" ]]; then
380
+ export PATH="$HOME/.bun/bin:$PATH"
381
+ fi
382
+ }
383
+
384
+ find_sdkmanager_bin() {
385
+ find_android_cmdline_tool "sdkmanager"
386
+ }
387
+
388
+ find_avdmanager_bin() {
389
+ find_android_cmdline_tool "avdmanager"
390
+ }
391
+
392
+ find_android_cmdline_tool() {
393
+ local tool_name="$1"
394
+ local candidate
395
+ local brew_prefix=""
396
+ if command -v "$tool_name" >/dev/null 2>&1; then
397
+ command -v "$tool_name"
398
+ return
399
+ fi
400
+ if command -v brew >/dev/null 2>&1; then
401
+ brew_prefix="$(brew --prefix 2>/dev/null || true)"
402
+ fi
403
+
404
+ for candidate in \
405
+ "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/$tool_name" \
406
+ "$brew_prefix/share/android-commandlinetools/cmdline-tools/latest/bin/$tool_name" \
407
+ "/opt/homebrew/share/android-commandlinetools/cmdline-tools/latest/bin/$tool_name" \
408
+ "/usr/local/share/android-commandlinetools/cmdline-tools/latest/bin/$tool_name"; do
409
+ if [[ -x "$candidate" ]]; then
410
+ printf "%s\n" "$candidate"
411
+ return
412
+ fi
413
+ done
414
+ printf "\n"
415
+ }
416
+
417
+ find_emulator_bin() {
418
+ if command -v emulator >/dev/null 2>&1; then
419
+ command -v emulator
420
+ return
421
+ fi
422
+ if [[ -x "$ANDROID_SDK_ROOT/emulator/emulator" ]]; then
423
+ printf "%s\n" "$ANDROID_SDK_ROOT/emulator/emulator"
424
+ return
425
+ fi
426
+ printf "\n"
427
+ }
428
+
429
+ current_xcode_version() {
430
+ xcodebuild -version 2>/dev/null | awk '/^Xcode / {print $2; exit}' || true
431
+ }
432
+
433
+ resolve_latest_ios_runtime_identifier() {
434
+ xcrun simctl list runtimes --json 2>/dev/null | node -e '
435
+ const fs = require("node:fs");
436
+ const parsed = JSON.parse(fs.readFileSync(0, "utf8") || "{}");
437
+ const runtimes = Array.isArray(parsed.runtimes) ? parsed.runtimes : [];
438
+ const ios = runtimes.filter((r) =>
439
+ r && typeof r.identifier === "string" &&
440
+ r.identifier.includes(".iOS-") &&
441
+ r.isAvailable !== false &&
442
+ !(typeof r.availability === "string" && r.availability.includes("unavailable"))
443
+ );
444
+ ios.sort((a, b) => String(a.identifier).localeCompare(String(b.identifier), undefined, { numeric: true }));
445
+ process.stdout.write(ios.length > 0 ? ios[ios.length - 1].identifier : "");
446
+ ' || true
447
+ }
448
+
449
+ android_package_installed() {
450
+ local pkg="$1"
451
+ case "$pkg" in
452
+ platform-tools)
453
+ [[ -x "$ANDROID_SDK_ROOT/platform-tools/adb" ]]
454
+ ;;
455
+ emulator)
456
+ [[ -x "$ANDROID_SDK_ROOT/emulator/emulator" ]]
457
+ ;;
458
+ "cmdline-tools;latest")
459
+ [[ -x "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" ]]
460
+ ;;
461
+ *)
462
+ [[ -d "$ANDROID_SDK_ROOT/${pkg//;/\/}" ]]
463
+ ;;
464
+ esac
465
+ }
466
+
467
+ resolve_java17_home() {
468
+ if [[ -d "/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home" ]]; then
469
+ printf "%s\n" "/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home"
470
+ return
471
+ fi
472
+ if [[ -x "/usr/libexec/java_home" ]]; then
473
+ /usr/libexec/java_home -v 17 2>/dev/null || true
474
+ return
475
+ fi
476
+ printf "\n"
477
+ }
478
+
479
+ find_zulu17_pkg() {
480
+ if ! command -v brew >/dev/null 2>&1; then
481
+ printf "\n"
482
+ return
483
+ fi
484
+ local cask_dir
485
+ cask_dir="$(brew --prefix)/Caskroom/zulu@17"
486
+ if [[ ! -d "$cask_dir" ]]; then
487
+ printf "\n"
488
+ return
489
+ fi
490
+ find "$cask_dir" -maxdepth 3 -type f -name "*.pkg" -print 2>/dev/null | head -n 1 || true
491
+ }
492
+
493
+ ensure_zulu17_jdk() {
494
+ if [[ "$(normalize_bool "$MACOS_SETUP_INSTALL_ZULU_JDK")" != "true" ]]; then
495
+ if [[ "$DRY_RUN" == "true" ]]; then
496
+ record_dry_run_skip "zulu@17 jdk"
497
+ fi
498
+ return
499
+ fi
500
+
501
+ local java_home
502
+ java_home="$(resolve_java17_home)"
503
+ if [[ -n "$java_home" ]]; then
504
+ if [[ "$DRY_RUN" == "true" ]]; then
505
+ record_dry_run_skip "zulu@17 jdk"
506
+ fi
507
+ export JAVA_HOME="$java_home"
508
+ export PATH="$JAVA_HOME/bin:$PATH"
509
+ return
510
+ fi
511
+
512
+ if [[ "$DRY_RUN" == "true" ]]; then
513
+ record_dry_run_install "zulu@17 jdk"
514
+ fi
515
+ brew_install_cask_if_missing "zulu@17"
516
+ local zulu_pkg
517
+ zulu_pkg="$(find_zulu17_pkg)"
518
+ if [[ -n "$zulu_pkg" ]]; then
519
+ log "Installing Azul Zulu JDK 17 package."
520
+ run_as_admin installer -pkg "$zulu_pkg" -target /
521
+ else
522
+ warn "Zulu JDK installer package not found in Caskroom; manual install may be required."
523
+ brew info --cask zulu@17 || true
524
+ if [[ "$DRY_RUN" == "true" ]]; then
525
+ return
526
+ fi
527
+ fi
528
+
529
+ if [[ "$DRY_RUN" == "true" ]]; then
530
+ log "[dry-run] would export JAVA_HOME from installed JDK 17 path."
531
+ return
532
+ fi
533
+
534
+ java_home="$(resolve_java17_home)"
535
+ if [[ -z "$java_home" ]]; then
536
+ fail "JDK 17 not detected after zulu@17 install. Install 'Double-Click to Install Azul Zulu JDK 17.pkg' from the zulu@17 Caskroom and rerun."
537
+ fi
538
+
539
+ export JAVA_HOME="$java_home"
540
+ export PATH="$JAVA_HOME/bin:$PATH"
541
+ }
542
+
543
+ ensure_xcodes_cli() {
544
+ if command -v xcodes >/dev/null 2>&1; then
545
+ if [[ "$DRY_RUN" == "true" ]]; then
546
+ record_dry_run_skip "xcodes cli"
547
+ fi
548
+ return
549
+ fi
550
+ if [[ "$DRY_RUN" == "true" ]]; then
551
+ record_dry_run_install "xcodes cli"
552
+ fi
553
+ ensure_brew_ready
554
+ log "Installing xcodes CLI."
555
+ if [[ "$DRY_RUN" == "true" ]]; then
556
+ log_dry_run_command brew tap xcodesorg/made
557
+ log_dry_run_command brew install xcodes
558
+ return
559
+ fi
560
+ brew tap xcodesorg/made >/dev/null 2>&1 || true
561
+ if ! brew install xcodes >/dev/null 2>&1; then
562
+ brew install xcodesorg/made/xcodes
563
+ fi
564
+ }
565
+
566
+ ensure_latest_xcode_install() {
567
+ local strict="$1"
568
+ local install_help
569
+ local -a install_args=("install")
570
+
571
+ if [[ "$(normalize_bool "$MACOS_SETUP_INSTALL_XCODE")" != "true" ]]; then
572
+ if [[ "$DRY_RUN" == "true" ]]; then
573
+ record_dry_run_skip "xcode"
574
+ fi
575
+ return
576
+ fi
577
+ if [[ "$MACOS_SETUP_XCODE_VERSION" == "latest" && -n "$(resolve_latest_xcode_developer_dir)" ]]; then
578
+ log "Xcode app already present; skipping xcodes install."
579
+ if [[ "$DRY_RUN" == "true" ]]; then
580
+ record_dry_run_skip "xcode"
581
+ fi
582
+ return
583
+ fi
584
+ if [[ "$MACOS_SETUP_XCODE_VERSION" != "latest" ]]; then
585
+ local installed_version
586
+ installed_version="$(current_xcode_version)"
587
+ if [[ -n "$installed_version" && "$installed_version" == "$MACOS_SETUP_XCODE_VERSION" ]]; then
588
+ log "Requested Xcode version already active ($installed_version); skipping xcodes install."
589
+ if [[ "$DRY_RUN" == "true" ]]; then
590
+ record_dry_run_skip "xcode $MACOS_SETUP_XCODE_VERSION"
591
+ fi
592
+ return
593
+ fi
594
+ fi
595
+
596
+ if [[ "$DRY_RUN" == "true" ]]; then
597
+ if [[ "$MACOS_SETUP_XCODE_VERSION" == "latest" ]]; then
598
+ record_dry_run_install "xcode latest"
599
+ else
600
+ record_dry_run_install "xcode $MACOS_SETUP_XCODE_VERSION"
601
+ fi
602
+ fi
603
+ ensure_xcodes_cli
604
+ if command -v xcodes >/dev/null 2>&1; then
605
+ install_help="$(xcodes help install 2>/dev/null || xcodes install --help 2>/dev/null || true)"
606
+ else
607
+ install_help=""
608
+ fi
609
+
610
+ if [[ "$MACOS_SETUP_XCODE_VERSION" == "latest" ]]; then
611
+ if grep -Fq -- "--latest" <<<"$install_help"; then
612
+ install_args+=("--latest")
613
+ else
614
+ install_args+=("latest")
615
+ fi
616
+ else
617
+ install_args+=("$MACOS_SETUP_XCODE_VERSION")
618
+ fi
619
+
620
+ if grep -Fq -- "--select" <<<"$install_help"; then
621
+ install_args+=("--select")
622
+ fi
623
+
624
+ log "Installing/selecting Xcode via xcodes (${install_args[*]})."
625
+ if [[ "$DRY_RUN" == "true" ]]; then
626
+ log_dry_run_command xcodes "${install_args[@]}"
627
+ return
628
+ fi
629
+ if ! xcodes "${install_args[@]}"; then
630
+ if [[ "$strict" == "true" ]]; then
631
+ fail "xcodes install failed. Authenticate with xcodes and rerun."
632
+ fi
633
+ warn "xcodes install failed. Continuing because strict mode is disabled."
634
+ fi
635
+ }
636
+
637
+ resolve_latest_xcode_developer_dir() {
638
+ local app_path
639
+ app_path="$(ls -dt /Applications/Xcode*.app 2>/dev/null | head -n 1 || true)"
640
+ if [[ -n "$app_path" && -d "$app_path/Contents/Developer" ]]; then
641
+ printf "%s\n" "$app_path/Contents/Developer"
642
+ return
643
+ fi
644
+ printf "\n"
645
+ }
646
+
647
+ ensure_xcode_cli_setup() {
648
+ local developer_dir
649
+ developer_dir="$(resolve_latest_xcode_developer_dir)"
650
+ if [[ -z "$developer_dir" ]]; then
651
+ if [[ "$DRY_RUN" == "true" ]]; then
652
+ warn "no local Xcode app currently detected; dry-run cannot validate xcode-select setup."
653
+ return
654
+ fi
655
+ fail "could not resolve /Applications/Xcode*.app. Install Xcode first."
656
+ fi
657
+
658
+ log "Setting active developer directory: $developer_dir"
659
+ run_as_admin xcode-select -s "$developer_dir"
660
+ run_as_admin xcodebuild -license accept >/dev/null 2>&1 || true
661
+ run_as_admin xcodebuild -runFirstLaunch >/dev/null 2>&1 || true
662
+ if [[ "$DRY_RUN" == "true" ]]; then
663
+ return
664
+ fi
665
+
666
+ if ! xcode-select -p >/dev/null 2>&1; then
667
+ fail "xcode-select is not configured after setup."
668
+ fi
669
+ }
670
+
671
+ ensure_ios_runtime_and_simulator() {
672
+ local runtime_identifier
673
+ runtime_identifier="$(resolve_latest_ios_runtime_identifier)"
674
+ if [[ -n "$runtime_identifier" ]]; then
675
+ log "iOS simulator runtime already available; skipping runtime download."
676
+ if [[ "$DRY_RUN" == "true" ]]; then
677
+ record_dry_run_skip "ios simulator runtime"
678
+ fi
679
+ elif [[ "$(normalize_bool "$MACOS_SETUP_INSTALL_IOS_RUNTIME")" == "true" ]]; then
680
+ if [[ "$DRY_RUN" == "true" ]]; then
681
+ record_dry_run_install "ios simulator runtime"
682
+ fi
683
+ log "Attempting to ensure iOS simulator platform runtime."
684
+ if [[ "$DRY_RUN" == "true" ]]; then
685
+ log_dry_run_command xcodebuild -downloadPlatform iOS
686
+ else
687
+ xcodebuild -downloadPlatform iOS >/dev/null 2>&1 || warn "xcodebuild -downloadPlatform iOS failed; continuing."
688
+ runtime_identifier="$(resolve_latest_ios_runtime_identifier)"
689
+ fi
690
+ elif [[ "$DRY_RUN" == "true" ]]; then
691
+ record_dry_run_skip "ios simulator runtime (disabled)"
692
+ fi
693
+
694
+ if [[ -z "$runtime_identifier" ]]; then
695
+ if [[ "$DRY_RUN" == "true" ]]; then
696
+ warn "no available iOS runtime currently; simulator creation plan may be incomplete in dry-run mode."
697
+ return
698
+ fi
699
+ fail "no available iOS simulator runtime found after setup."
700
+ fi
701
+
702
+ local existing_udid
703
+ existing_udid="$(xcrun simctl list devices --json 2>/dev/null | node -e '
704
+ const fs = require("node:fs");
705
+ const target = process.argv[1];
706
+ const parsed = JSON.parse(fs.readFileSync(0, "utf8") || "{}");
707
+ for (const list of Object.values(parsed.devices || {})) {
708
+ if (!Array.isArray(list)) continue;
709
+ for (const device of list) {
710
+ if (!device || typeof device !== "object") continue;
711
+ if (device.name !== target) continue;
712
+ if (device.isAvailable === false) continue;
713
+ if (typeof device.udid === "string") {
714
+ process.stdout.write(device.udid);
715
+ process.exit(0);
716
+ }
717
+ }
718
+ }
719
+ ' "$MACOS_SETUP_IOS_SIMULATOR_NAME" || true)"
720
+ if [[ -n "$existing_udid" ]]; then
721
+ if [[ "$DRY_RUN" == "true" ]]; then
722
+ record_dry_run_skip "ios simulator: $MACOS_SETUP_IOS_SIMULATOR_NAME"
723
+ fi
724
+ return
725
+ fi
726
+
727
+ if [[ "$DRY_RUN" == "true" ]]; then
728
+ record_dry_run_install "ios simulator: $MACOS_SETUP_IOS_SIMULATOR_NAME"
729
+ fi
730
+
731
+ local device_type_identifier
732
+ device_type_identifier="$(xcrun simctl list devicetypes --json 2>/dev/null | node -e '
733
+ const fs = require("node:fs");
734
+ const target = process.argv[1].trim().toLowerCase();
735
+ const parsed = JSON.parse(fs.readFileSync(0, "utf8") || "{}");
736
+ const list = Array.isArray(parsed.devicetypes) ? parsed.devicetypes : [];
737
+ const exact = list.find((e) =>
738
+ e && typeof e.name === "string" && typeof e.identifier === "string" &&
739
+ e.name.trim().toLowerCase() === target
740
+ );
741
+ if (exact) {
742
+ process.stdout.write(exact.identifier);
743
+ process.exit(0);
744
+ }
745
+ const fallback = list.find((e) =>
746
+ e && typeof e.name === "string" && typeof e.identifier === "string" && /^iphone/i.test(e.name)
747
+ );
748
+ if (fallback) process.stdout.write(fallback.identifier);
749
+ ' "$MACOS_SETUP_IOS_SIMULATOR_NAME" || true)"
750
+
751
+ if [[ -z "$device_type_identifier" ]]; then
752
+ if [[ "$DRY_RUN" == "true" ]]; then
753
+ warn "unable to resolve iOS simulator device type in dry-run mode."
754
+ return
755
+ fi
756
+ fail "failed to resolve iOS simulator device type."
757
+ fi
758
+
759
+ log "Creating default iOS simulator: $MACOS_SETUP_IOS_SIMULATOR_NAME"
760
+ if [[ "$DRY_RUN" == "true" ]]; then
761
+ log_dry_run_command xcrun simctl create "$MACOS_SETUP_IOS_SIMULATOR_NAME" "$device_type_identifier" "$runtime_identifier"
762
+ return
763
+ fi
764
+ xcrun simctl create "$MACOS_SETUP_IOS_SIMULATOR_NAME" "$device_type_identifier" "$runtime_identifier" >/dev/null
765
+ }
766
+
767
+ ensure_node_and_package_managers() {
768
+ if ! command -v node >/dev/null 2>&1; then
769
+ brew_install_formula_if_missing "node"
770
+ elif [[ "$DRY_RUN" == "true" ]]; then
771
+ record_dry_run_skip "node"
772
+ fi
773
+ if ! command -v jq >/dev/null 2>&1; then
774
+ brew_install_formula_if_missing "jq"
775
+ elif [[ "$DRY_RUN" == "true" ]]; then
776
+ record_dry_run_skip "jq"
777
+ fi
778
+
779
+ if [[ "$(normalize_bool "$MACOS_SETUP_INSTALL_COCOAPODS")" == "true" ]] && ! command -v pod >/dev/null 2>&1; then
780
+ brew_install_formula_if_missing "cocoapods"
781
+ elif [[ "$DRY_RUN" == "true" ]]; then
782
+ if [[ "$(normalize_bool "$MACOS_SETUP_INSTALL_COCOAPODS")" == "true" ]]; then
783
+ record_dry_run_skip "cocoapods"
784
+ else
785
+ record_dry_run_skip "cocoapods (disabled)"
786
+ fi
787
+ fi
788
+ ensure_zulu17_jdk
789
+ ensure_path_for_current_session
790
+
791
+ if command -v corepack >/dev/null 2>&1; then
792
+ if [[ "$DRY_RUN" == "true" ]]; then
793
+ log_dry_run_command corepack enable
794
+ else
795
+ corepack enable >/dev/null 2>&1 || true
796
+ fi
797
+ if ! command -v pnpm >/dev/null 2>&1; then
798
+ if [[ "$DRY_RUN" == "true" ]]; then
799
+ record_dry_run_install "pnpm"
800
+ log_dry_run_command corepack prepare pnpm@latest --activate
801
+ else
802
+ corepack prepare pnpm@latest --activate
803
+ fi
804
+ elif [[ "$DRY_RUN" == "true" ]]; then
805
+ record_dry_run_skip "pnpm"
806
+ fi
807
+ if ! command -v yarn >/dev/null 2>&1; then
808
+ if [[ "$DRY_RUN" == "true" ]]; then
809
+ record_dry_run_install "yarn"
810
+ log_dry_run_command corepack prepare yarn@stable --activate
811
+ else
812
+ corepack prepare yarn@stable --activate
813
+ fi
814
+ elif [[ "$DRY_RUN" == "true" ]]; then
815
+ record_dry_run_skip "yarn"
816
+ fi
817
+ else
818
+ local -a missing_pkg_managers=()
819
+ if ! command -v pnpm >/dev/null 2>&1; then
820
+ missing_pkg_managers+=("pnpm")
821
+ fi
822
+ if ! command -v yarn >/dev/null 2>&1; then
823
+ missing_pkg_managers+=("yarn")
824
+ fi
825
+ if [[ "${#missing_pkg_managers[@]}" -gt 0 ]]; then
826
+ if [[ "$DRY_RUN" == "true" ]]; then
827
+ local manager
828
+ for manager in "${missing_pkg_managers[@]}"; do
829
+ record_dry_run_install "$manager"
830
+ done
831
+ log_dry_run_command npm install -g "${missing_pkg_managers[@]}"
832
+ else
833
+ npm install -g "${missing_pkg_managers[@]}"
834
+ fi
835
+ elif [[ "$DRY_RUN" == "true" ]]; then
836
+ record_dry_run_skip "pnpm"
837
+ record_dry_run_skip "yarn"
838
+ fi
839
+ fi
840
+
841
+ ensure_bun
842
+ }
843
+
844
+ ensure_bun() {
845
+ if command -v bun >/dev/null 2>&1; then
846
+ if [[ "$DRY_RUN" == "true" ]]; then
847
+ record_dry_run_skip "bun"
848
+ fi
849
+ return
850
+ fi
851
+
852
+ if [[ "$DRY_RUN" == "true" ]]; then
853
+ record_dry_run_install "bun"
854
+ log_dry_run_command /bin/bash -c "curl -fsSL https://bun.sh/install | bash"
855
+ return
856
+ fi
857
+
858
+ log "Installing Bun with the official install script."
859
+ /bin/bash -c "$(curl -fsSL https://bun.sh/install)"
860
+ if [[ -d "$HOME/.bun/bin" ]]; then
861
+ export PATH="$HOME/.bun/bin:$PATH"
862
+ fi
863
+ }
864
+
865
+ ensure_android_sdk_and_emulator() {
866
+ if [[ "$(normalize_bool "$MACOS_SETUP_INSTALL_ANDROID")" != "true" ]]; then
867
+ if [[ "$DRY_RUN" == "true" ]]; then
868
+ record_dry_run_skip "android sdk/emulator"
869
+ fi
870
+ return
871
+ fi
872
+
873
+ if [[ "$DRY_RUN" == "true" ]]; then
874
+ log_dry_run_command mkdir -p "$ANDROID_SDK_ROOT"
875
+ else
876
+ mkdir -p "$ANDROID_SDK_ROOT"
877
+ fi
878
+ if ! command -v sdkmanager >/dev/null 2>&1 || ! command -v avdmanager >/dev/null 2>&1; then
879
+ brew_install_cask_if_missing "android-commandlinetools"
880
+ elif [[ "$DRY_RUN" == "true" ]]; then
881
+ record_dry_run_skip "brew cask: android-commandlinetools"
882
+ fi
883
+ if ! command -v adb >/dev/null 2>&1; then
884
+ brew_install_formula_if_missing "android-platform-tools"
885
+ elif [[ "$DRY_RUN" == "true" ]]; then
886
+ record_dry_run_skip "brew formula: android-platform-tools"
887
+ fi
888
+ ensure_path_for_current_session
889
+ if [[ -z "$(resolve_java17_home)" ]]; then
890
+ if [[ "$DRY_RUN" == "true" ]]; then
891
+ warn "java runtime 17 not found currently; dry-run cannot validate android sdk steps."
892
+ return
893
+ fi
894
+ fail "java runtime 17 not found. Enable MACOS_SETUP_INSTALL_ZULU_JDK=true and rerun."
895
+ fi
896
+
897
+ local sdkmanager_bin
898
+ sdkmanager_bin="$(find_sdkmanager_bin)"
899
+ if [[ -z "$sdkmanager_bin" ]]; then
900
+ if [[ "$DRY_RUN" == "true" ]]; then
901
+ warn "sdkmanager not found currently; dry-run cannot resolve exact Android package plan."
902
+ return
903
+ fi
904
+ fail "sdkmanager not found after android-commandlinetools install."
905
+ fi
906
+ local avdmanager_bin
907
+ avdmanager_bin="$(find_avdmanager_bin)"
908
+ if [[ -z "$avdmanager_bin" ]]; then
909
+ if [[ "$DRY_RUN" == "true" ]]; then
910
+ warn "avdmanager not found currently; dry-run cannot resolve exact Android AVD plan."
911
+ return
912
+ fi
913
+ fail "avdmanager not found after android-commandlinetools install."
914
+ fi
915
+
916
+ log "Accepting Android SDK licenses."
917
+ if [[ "$DRY_RUN" == "true" ]]; then
918
+ log_dry_run_command "$sdkmanager_bin" --sdk_root="$ANDROID_SDK_ROOT" --licenses
919
+ else
920
+ yes | "$sdkmanager_bin" --sdk_root="$ANDROID_SDK_ROOT" --licenses >/dev/null || true
921
+ fi
922
+
923
+ local sdk_list
924
+ sdk_list="$("$sdkmanager_bin" --sdk_root="$ANDROID_SDK_ROOT" --list | tr -d '\r')"
925
+ local api_level
926
+ if [[ "$MACOS_SETUP_ANDROID_API" == "latest" ]]; then
927
+ api_level="$(printf "%s\n" "$sdk_list" | grep -Eo 'system-images;android-[0-9]+;google_apis(_playstore)?;(arm64-v8a|x86_64)' | sed -E 's/^system-images;android-([0-9]+);.*$/\1/' | sort -uV | tail -n 1 || true)"
928
+ else
929
+ api_level="$MACOS_SETUP_ANDROID_API"
930
+ fi
931
+
932
+ if [[ -z "$api_level" ]]; then
933
+ if [[ "$DRY_RUN" == "true" ]]; then
934
+ warn "unable to resolve Android API level from current sdkmanager list in dry-run."
935
+ return
936
+ fi
937
+ fail "unable to resolve Android API level from sdkmanager system image list."
938
+ fi
939
+
940
+ local arch_suffix="x86_64"
941
+ if [[ "$(uname -m)" == "arm64" ]]; then
942
+ arch_suffix="arm64-v8a"
943
+ fi
944
+
945
+ local system_image_pkg=""
946
+ local candidate
947
+ for candidate in \
948
+ "system-images;android-$api_level;google_apis;$arch_suffix" \
949
+ "system-images;android-$api_level;google_apis_playstore;$arch_suffix"; do
950
+ if printf "%s\n" "$sdk_list" | grep -Fq "$candidate"; then
951
+ system_image_pkg="$candidate"
952
+ break
953
+ fi
954
+ done
955
+ if [[ -z "$system_image_pkg" ]]; then
956
+ fail "unable to resolve Android system image package for api=$api_level arch=$arch_suffix."
957
+ fi
958
+
959
+ local -a packages=("cmdline-tools;latest" "platform-tools" "emulator" "$system_image_pkg")
960
+ local -a missing_packages=()
961
+ local pkg
962
+ for pkg in "${packages[@]}"; do
963
+ if ! android_package_installed "$pkg"; then
964
+ if [[ "$DRY_RUN" == "true" ]]; then
965
+ record_dry_run_install "android sdk package: $pkg"
966
+ fi
967
+ missing_packages+=("$pkg")
968
+ elif [[ "$DRY_RUN" == "true" ]]; then
969
+ record_dry_run_skip "android sdk package: $pkg"
970
+ fi
971
+ done
972
+
973
+ if [[ "${#missing_packages[@]}" -gt 0 ]]; then
974
+ log "Installing missing Android SDK packages: ${missing_packages[*]}"
975
+ if [[ "$DRY_RUN" == "true" ]]; then
976
+ log_dry_run_command "$sdkmanager_bin" --sdk_root="$ANDROID_SDK_ROOT" --install "${missing_packages[@]}"
977
+ else
978
+ "$sdkmanager_bin" --sdk_root="$ANDROID_SDK_ROOT" --install "${missing_packages[@]}"
979
+ fi
980
+ else
981
+ log "Android SDK packages already present; skipping sdkmanager install."
982
+ fi
983
+
984
+ local emulator_bin
985
+ emulator_bin="$(find_emulator_bin)"
986
+ if [[ -z "$emulator_bin" ]]; then
987
+ if [[ "$DRY_RUN" == "true" ]]; then
988
+ warn "emulator binary not found currently; dry-run cannot validate AVD creation step."
989
+ return
990
+ fi
991
+ fail "emulator binary not found after SDK package install."
992
+ fi
993
+
994
+ if ! "$emulator_bin" -list-avds | grep -Fxq "$MACOS_SETUP_ANDROID_AVD_NAME"; then
995
+ if [[ "$DRY_RUN" == "true" ]]; then
996
+ record_dry_run_install "android avd: $MACOS_SETUP_ANDROID_AVD_NAME"
997
+ fi
998
+ log "Creating default Android AVD: $MACOS_SETUP_ANDROID_AVD_NAME"
999
+ if [[ "$DRY_RUN" == "true" ]]; then
1000
+ log_dry_run_command "$avdmanager_bin" create avd -n "$MACOS_SETUP_ANDROID_AVD_NAME" -k "$system_image_pkg" -d "$MACOS_SETUP_ANDROID_DEVICE_PROFILE" --force
1001
+ return
1002
+ fi
1003
+ if ! (set +o pipefail; echo "no" | "$avdmanager_bin" create avd \
1004
+ -n "$MACOS_SETUP_ANDROID_AVD_NAME" \
1005
+ -k "$system_image_pkg" \
1006
+ -d "$MACOS_SETUP_ANDROID_DEVICE_PROFILE" \
1007
+ --force >/dev/null 2>&1); then
1008
+ (set +o pipefail; echo "no" | "$avdmanager_bin" create avd \
1009
+ -n "$MACOS_SETUP_ANDROID_AVD_NAME" \
1010
+ -k "$system_image_pkg" \
1011
+ --force >/dev/null)
1012
+ fi
1013
+ elif [[ "$DRY_RUN" == "true" ]]; then
1014
+ record_dry_run_skip "android avd: $MACOS_SETUP_ANDROID_AVD_NAME"
1015
+ fi
1016
+ }
1017
+
1018
+ ensure_agent_device_cli() {
1019
+ if [[ "$(normalize_bool "$MACOS_SETUP_INSTALL_AGENT_DEVICE")" != "true" ]]; then
1020
+ if [[ "$DRY_RUN" == "true" ]]; then
1021
+ record_dry_run_skip "agent-device"
1022
+ record_dry_run_skip "agent-device-proxy"
1023
+ fi
1024
+ return
1025
+ fi
1026
+ if command -v agent-device >/dev/null 2>&1 && command -v agent-device-proxy >/dev/null 2>&1; then
1027
+ log "agent-device and agent-device-proxy already installed; skipping npm global install."
1028
+ if [[ "$DRY_RUN" == "true" ]]; then
1029
+ record_dry_run_skip "agent-device"
1030
+ record_dry_run_skip "agent-device-proxy"
1031
+ fi
1032
+ return
1033
+ fi
1034
+ if [[ "$DRY_RUN" == "true" ]]; then
1035
+ record_dry_run_install "agent-device"
1036
+ record_dry_run_install "agent-device-proxy"
1037
+ log_dry_run_command npm install -g agent-device@latest agent-device-proxy@latest
1038
+ return
1039
+ fi
1040
+ log "Installing/updating global agent-device and agent-device-proxy CLIs."
1041
+ npm install -g agent-device@latest agent-device-proxy@latest
1042
+
1043
+ local missing_bins=()
1044
+ local candidate
1045
+ for candidate in agent-device agent-device-proxy; do
1046
+ if ! command -v "$candidate" >/dev/null 2>&1; then
1047
+ missing_bins+=("$candidate")
1048
+ fi
1049
+ done
1050
+
1051
+ if [[ "${#missing_bins[@]}" -gt 0 ]]; then
1052
+ fail "npm global install completed, but these commands are still unavailable on PATH: ${missing_bins[*]}"
1053
+ fi
1054
+
1055
+ log "Installed agent-device: $(command -v agent-device)"
1056
+ log "Installed agent-device-proxy: $(command -v agent-device-proxy)"
1057
+ }
1058
+
1059
+ print_dry_run_summary() {
1060
+ local item
1061
+ if [[ "$DRY_RUN" != "true" ]]; then
1062
+ return
1063
+ fi
1064
+
1065
+ log "Dry-run summary:"
1066
+ if [[ "${#DRY_RUN_INSTALLS[@]}" -gt 0 ]]; then
1067
+ log " would install:"
1068
+ for item in "${DRY_RUN_INSTALLS[@]}"; do
1069
+ log " - $item"
1070
+ done
1071
+ else
1072
+ log " would install: none"
1073
+ fi
1074
+
1075
+ if [[ "${#DRY_RUN_SKIPS[@]}" -gt 0 ]]; then
1076
+ log " would skip:"
1077
+ for item in "${DRY_RUN_SKIPS[@]}"; do
1078
+ log " - $item"
1079
+ done
1080
+ else
1081
+ log " would skip: none"
1082
+ fi
1083
+ log "No changes were applied."
1084
+ }
1085
+
1086
+ main() {
1087
+ parse_args "$@"
1088
+ ensure_macos_host
1089
+ prime_login_shell_environment
1090
+ prime_default_macos_paths
1091
+ MACOS_SETUP_BREW_UPDATE="$(normalize_bool "$MACOS_SETUP_BREW_UPDATE")"
1092
+ MACOS_SETUP_STRICT_XCODE="$(normalize_bool "$MACOS_SETUP_STRICT_XCODE")"
1093
+
1094
+ ensure_node_and_package_managers
1095
+ ensure_latest_xcode_install "$MACOS_SETUP_STRICT_XCODE"
1096
+ ensure_xcode_cli_setup
1097
+ ensure_ios_runtime_and_simulator
1098
+ ensure_android_sdk_and_emulator
1099
+ ensure_agent_device_cli
1100
+ ensure_profile_export_block "$MACOS_SETUP_SHELL_PROFILE"
1101
+ print_dry_run_summary
1102
+
1103
+ if [[ "$DRY_RUN" == "true" ]]; then
1104
+ log "Dry-run complete."
1105
+ return
1106
+ fi
1107
+
1108
+ log "Setup complete."
1109
+ log "Run these verification commands:"
1110
+ log " xcode-select -p"
1111
+ log " xcrun simctl list runtimes | grep iOS"
1112
+ log " pnpm --version && yarn --version && bun --version && node -v"
1113
+ log " sdkmanager --sdk_root=\"$ANDROID_SDK_ROOT\" --list | head -n 20"
1114
+ log " pnpm --filter @platform/runtime podman:ios:preflight"
1115
+ }
1116
+
1117
+ main "$@"