| .. | .. |
|---|
| 3 | 3 | # |
|---|
| 4 | 4 | # Installs a personal (Pro-unlocked) PAILot build onto one or more iOS devices. |
|---|
| 5 | 5 | # |
|---|
| 6 | | -# ── Remote / "fully remote" targets ──────────────────────────────────────── |
|---|
| 7 | | -# Installation uses `xcrun devicectl device install`, which reaches the device |
|---|
| 8 | | -# over Apple's CoreDevice tunnel — USB, local network, OR any routable network |
|---|
| 9 | | -# path where the device is paired (e.g. across Tailscale). There is NO special |
|---|
| 10 | | -# remote logic to add: once a device has been paired, `--device <id>` works |
|---|
| 11 | | -# whether it sits on your desk or across the world, as long as it is awake and |
|---|
| 12 | | -# reachable. That is exactly how the iPad here installs — it is a `localNetwork` |
|---|
| 13 | | -# device, never plugged in. (First-time discovery of a NEW device uses .local / |
|---|
| 14 | | -# mDNS, which does not cross networks, so a device must be paired at least once |
|---|
| 15 | | -# on the local network before it can be driven fully remotely.) |
|---|
| 16 | | -# Check reachability any time with: bash tools/deploy.sh --check |
|---|
| 6 | +# ── Two transports ───────────────────────────────────────────────────────── |
|---|
| 7 | +# 1. devicectl (default): `xcrun devicectl device install`. Reaches the device |
|---|
| 8 | +# over Apple's CoreDevice tunnel, but DISCOVERY is mDNS/Bonjour — so it only |
|---|
| 9 | +# works when the Mac and device share the local network (USB or same wifi). |
|---|
| 10 | +# It does NOT traverse Tailscale: a device on a remote network shows |
|---|
| 11 | +# "unavailable" and both its .ts.net name and tailnet IP are rejected. |
|---|
| 12 | +# |
|---|
| 13 | +# 2. --ota (fully remote): build + publish the IPA to the aibroker-ota hub, which |
|---|
| 14 | +# serves it over Tailscale (HTTPS via Tailscale Serve). Open the returned URL |
|---|
| 15 | +# in Safari on ANY device on the tailnet and tap Install. This is the path to |
|---|
| 16 | +# use when you are NOT on the local wifi. Requires `aibroker ota up` once. |
|---|
| 17 | +# (The device UDID must still be in the provisioning profile.) |
|---|
| 17 | 18 | # |
|---|
| 18 | 19 | # ── Usage ────────────────────────────────────────────────────────────────── |
|---|
| 19 | | -# bash tools/deploy.sh # Matthias' iPhone (default) |
|---|
| 20 | +# bash tools/deploy.sh # Matthias' iPhone (default, local network) |
|---|
| 20 | 21 | # bash tools/deploy.sh -a # Amelie's iPhone |
|---|
| 21 | 22 | # bash tools/deploy.sh -i # Matthias' iPad |
|---|
| 22 | 23 | # bash tools/deploy.sh --all # every known device (continues on failure) |
|---|
| 23 | | -# bash tools/deploy.sh --device ID # explicit CoreDevice identifier (any remote device) |
|---|
| 24 | +# bash tools/deploy.sh --device ID # explicit CoreDevice id (local network) |
|---|
| 25 | +# bash tools/deploy.sh --ota # remote install over Tailscale (prints URL) |
|---|
| 24 | 26 | # bash tools/deploy.sh --build # force a fresh Pro-unlocked build first |
|---|
| 25 | 27 | # bash tools/deploy.sh --check # list devices devicectl can currently reach |
|---|
| 26 | 28 | set -euo pipefail |
|---|
| .. | .. |
|---|
| 29 | 31 | APP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" |
|---|
| 30 | 32 | cd "$APP_DIR" |
|---|
| 31 | 33 | |
|---|
| 32 | | -IPA="build/ios/ipa/pailot.ipa" |
|---|
| 34 | +IPA_DIR="build/ios/ipa" |
|---|
| 35 | +IPA="" # resolved after build (flutter names it PAILot.ipa) |
|---|
| 36 | +resolve_ipa() { IPA="$(ls -t "$IPA_DIR"/*.ipa 2>/dev/null | head -1 || true)"; } |
|---|
| 33 | 37 | |
|---|
| 34 | 38 | # Known devices — CoreDevice identifiers from `xcrun devicectl list devices`. |
|---|
| 35 | 39 | IPHONE_MATTHIAS="8708CADC-3330-50B6-AA0A-1655526A573A" |
|---|
| .. | .. |
|---|
| 38 | 42 | |
|---|
| 39 | 43 | TARGETS=() # entries: "identifier|label" |
|---|
| 40 | 44 | FORCE_BUILD=false |
|---|
| 45 | +OTA=false |
|---|
| 41 | 46 | |
|---|
| 42 | 47 | while [[ $# -gt 0 ]]; do |
|---|
| 43 | 48 | case "$1" in |
|---|
| .. | .. |
|---|
| 50 | 55 | "$IPHONE_AMELIE|Amelie's iPhone") |
|---|
| 51 | 56 | shift ;; |
|---|
| 52 | 57 | --device) TARGETS+=("$2|device $2"); shift 2 ;; |
|---|
| 58 | + --ota) OTA=true; shift ;; |
|---|
| 53 | 59 | --build) FORCE_BUILD=true; shift ;; |
|---|
| 54 | 60 | --check) echo "=== Devices devicectl can reach ==="; xcrun devicectl list devices; exit 0 ;; |
|---|
| 55 | | - -h|--help) sed -n '2,30p' "$0"; exit 0 ;; |
|---|
| 61 | + -h|--help) sed -n '2,33p' "$0"; exit 0 ;; |
|---|
| 56 | 62 | *) echo "Unknown option: $1" >&2; exit 2 ;; |
|---|
| 57 | 63 | esac |
|---|
| 58 | 64 | done |
|---|
| 59 | | - |
|---|
| 60 | | -# Default target: Matthias' iPhone. |
|---|
| 61 | | -if [[ ${#TARGETS[@]} -eq 0 ]]; then |
|---|
| 62 | | - TARGETS+=("$IPHONE_MATTHIAS|Matthias' iPhone") |
|---|
| 63 | | -fi |
|---|
| 64 | 65 | |
|---|
| 65 | 66 | # ── Build a Pro-unlocked personal IPA when needed ────────────────────────── |
|---|
| 66 | 67 | # PAILOT_PRO unlocks Pro for personal / sideloaded installs ONLY. App Store |
|---|
| 67 | 68 | # builds go through tools/build-appstore.sh / tools/release.sh, which do NOT |
|---|
| 68 | 69 | # pass this define — paying users still see the paywall. |
|---|
| 69 | | -if [[ "$FORCE_BUILD" == true || ! -f "$IPA" ]]; then |
|---|
| 70 | +resolve_ipa |
|---|
| 71 | +if [[ "$FORCE_BUILD" == true || -z "$IPA" ]]; then |
|---|
| 70 | 72 | echo "=== Building Pro-unlocked IPA (PAILOT_PRO=true) ===" |
|---|
| 71 | 73 | flutter build ipa --release --no-pub --export-method development \ |
|---|
| 72 | 74 | --no-tree-shake-icons --dart-define=PAILOT_PRO=true |
|---|
| .. | .. |
|---|
| 75 | 77 | plutil -replace Name -string "PAILot" "$ARCHIVE" 2>/dev/null || true |
|---|
| 76 | 78 | plutil -replace SchemeName -string "PAILot" "$ARCHIVE" 2>/dev/null || true |
|---|
| 77 | 79 | fi |
|---|
| 80 | + resolve_ipa |
|---|
| 78 | 81 | else |
|---|
| 79 | 82 | echo "=== Using existing IPA: $IPA ===" |
|---|
| 80 | 83 | echo " (Pro status depends on how it was built — use --build to force a fresh Pro build)" |
|---|
| 84 | +fi |
|---|
| 85 | +[[ -n "$IPA" && -f "$IPA" ]] || { echo "ERROR: no IPA found in $IPA_DIR" >&2; exit 1; } |
|---|
| 86 | + |
|---|
| 87 | +# ── OTA (remote over Tailscale) ──────────────────────────────────────────── |
|---|
| 88 | +if [[ "$OTA" == true ]]; then |
|---|
| 89 | + VERSION="$(grep -E '^version:' pubspec.yaml | sed -E 's/version:[[:space:]]*//; s/\+.*//' | head -1)" |
|---|
| 90 | + echo "" |
|---|
| 91 | + echo "=== Publishing to aibroker-ota hub (Tailscale) ===" |
|---|
| 92 | + resp="$(curl -sf -X POST http://127.0.0.1:8765/api/apps \ |
|---|
| 93 | + -F slug=pailot -F name=PAILot -F bundleId=com.tekmidian.pailot \ |
|---|
| 94 | + -F "version=${VERSION:-1.0.0}" -F platform=ios -F "file=@${IPA}" 2>&1)" || { |
|---|
| 95 | + echo "ERROR: publish failed — is the hub up? Run: aibroker ota up" >&2; exit 1; } |
|---|
| 96 | + # Derive the tailnet host for the HTTPS install URL. |
|---|
| 97 | + ts="tailscale"; command -v tailscale >/dev/null 2>&1 || ts="/Applications/Tailscale.app/Contents/MacOS/Tailscale" |
|---|
| 98 | + host="$("$ts" status --json 2>/dev/null | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{console.log(JSON.parse(s).Self.DNSName.replace(/\.$/,""))}catch{}})' 2>/dev/null || true)" |
|---|
| 99 | + echo "" |
|---|
| 100 | + if [[ -n "$host" ]]; then |
|---|
| 101 | + echo " Install on any tailnet device — open in Safari:" |
|---|
| 102 | + echo " https://${host}/install/pailot/" |
|---|
| 103 | + else |
|---|
| 104 | + echo " Published. Open the install page in Safari on a tailnet device:" |
|---|
| 105 | + echo " https://<your-mac-tailnet-host>/install/pailot/" |
|---|
| 106 | + fi |
|---|
| 107 | + echo "" |
|---|
| 108 | + echo "=== Done (OTA). Tap Install in Safari, then force-quit & reopen PAILot. ===" |
|---|
| 109 | + exit 0 |
|---|
| 110 | +fi |
|---|
| 111 | + |
|---|
| 112 | +# ── devicectl install (local network) ────────────────────────────────────── |
|---|
| 113 | +# Default target: Matthias' iPhone. |
|---|
| 114 | +if [[ ${#TARGETS[@]} -eq 0 ]]; then |
|---|
| 115 | + TARGETS+=("$IPHONE_MATTHIAS|Matthias' iPhone") |
|---|
| 81 | 116 | fi |
|---|
| 82 | 117 | |
|---|
| 83 | 118 | install_one() { |
|---|
| .. | .. |
|---|
| 87 | 122 | if xcrun devicectl device install app --device "$id" "$IPA"; then |
|---|
| 88 | 123 | echo " OK: $label" |
|---|
| 89 | 124 | else |
|---|
| 90 | | - echo " WARNING: install on $label failed — is it awake & reachable?" |
|---|
| 91 | | - echo " Try: bash tools/deploy.sh --check" |
|---|
| 125 | + echo " WARNING: install on $label failed — awake & on the same network?" |
|---|
| 126 | + echo " Off the local wifi? Use: bash tools/deploy.sh --ota" |
|---|
| 92 | 127 | return 1 |
|---|
| 93 | 128 | fi |
|---|
| 94 | 129 | } |
|---|