Matthias Nott
yesterday e6eb093cc63ba020799844905439f1cabfddcd3c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
#!/bin/bash
# PAILot iOS Deploy Script
#
# Installs a personal (Pro-unlocked) PAILot build onto one or more iOS devices.
#
# ── Two transports ─────────────────────────────────────────────────────────
# 1. devicectl (default): `xcrun devicectl device install`. Reaches the device
#    over Apple's CoreDevice tunnel, but DISCOVERY is mDNS/Bonjour — so it only
#    works when the Mac and device share the local network (USB or same wifi).
#    It does NOT traverse Tailscale: a device on a remote network shows
#    "unavailable" and both its .ts.net name and tailnet IP are rejected.
#
# 2. --ota (fully remote): build + publish the IPA to the aibroker-ota hub, which
#    serves it over Tailscale (HTTPS via Tailscale Serve). Open the returned URL
#    in Safari on ANY device on the tailnet and tap Install. This is the path to
#    use when you are NOT on the local wifi. Requires `aibroker ota up` once.
#    (The device UDID must still be in the provisioning profile.)
#
# ── Usage ──────────────────────────────────────────────────────────────────
#   bash tools/deploy.sh              # Matthias' iPhone (default, local network)
#   bash tools/deploy.sh -a           # Amelie's iPhone
#   bash tools/deploy.sh -i           # Matthias' iPad
#   bash tools/deploy.sh --all        # every known device (continues on failure)
#   bash tools/deploy.sh --device ID  # explicit CoreDevice id (local network)
#   bash tools/deploy.sh --ota        # remote install over Tailscale (prints URL)
#   bash tools/deploy.sh --build      # force a fresh Pro-unlocked build first
#   bash tools/deploy.sh --check      # list devices devicectl can currently reach
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
APP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$APP_DIR"
IPA_DIR="build/ios/ipa"
IPA=""                       # resolved after build (flutter names it PAILot.ipa)
resolve_ipa() { IPA="$(ls -t "$IPA_DIR"/*.ipa 2>/dev/null | head -1 || true)"; }
# Known devices — CoreDevice identifiers from `xcrun devicectl list devices`.
IPHONE_MATTHIAS="8708CADC-3330-50B6-AA0A-1655526A573A"
IPHONE_AMELIE="E3F823F4-8DC8-5C88-948E-C7FA2B4C4C72"
IPAD_MATTHIAS="8037E780-6BC0-5B9D-9ED4-D8F28DEEFD3F"
TARGETS=()          # entries: "identifier|label"
FORCE_BUILD=false
OTA=false
while [[ $# -gt 0 ]]; do
  case "$1" in
    -a|--amelie) TARGETS+=("$IPHONE_AMELIE|Amelie's iPhone"); shift ;;
    -i|--ipad)   TARGETS+=("$IPAD_MATTHIAS|Matthias' iPad"); shift ;;
    --iphone)    TARGETS+=("$IPHONE_MATTHIAS|Matthias' iPhone"); shift ;;
    --all)
      TARGETS+=("$IPHONE_MATTHIAS|Matthias' iPhone" \
                "$IPAD_MATTHIAS|Matthias' iPad" \
                "$IPHONE_AMELIE|Amelie's iPhone")
      shift ;;
    --device)    TARGETS+=("$2|device $2"); shift 2 ;;
    --ota)       OTA=true; shift ;;
    --build)     FORCE_BUILD=true; shift ;;
    --check)     echo "=== Devices devicectl can reach ==="; xcrun devicectl list devices; exit 0 ;;
    -h|--help)   sed -n '2,33p' "$0"; exit 0 ;;
    *) echo "Unknown option: $1" >&2; exit 2 ;;
  esac
done
# ── Build a Pro-unlocked personal IPA when needed ──────────────────────────
# PAILOT_PRO unlocks Pro for personal / sideloaded installs ONLY. App Store
# builds go through tools/build-appstore.sh / tools/release.sh, which do NOT
# pass this define — paying users still see the paywall.
resolve_ipa
if [[ "$FORCE_BUILD" == true || -z "$IPA" ]]; then
  echo "=== Building Pro-unlocked IPA (PAILOT_PRO=true) ==="
  flutter build ipa --release --no-pub --export-method development \
    --no-tree-shake-icons --dart-define=PAILOT_PRO=true
  ARCHIVE="build/ios/archive/Runner.xcarchive/Info.plist"
  if [ -f "$ARCHIVE" ]; then
    plutil -replace Name -string "PAILot" "$ARCHIVE" 2>/dev/null || true
    plutil -replace SchemeName -string "PAILot" "$ARCHIVE" 2>/dev/null || true
  fi
  resolve_ipa
else
  echo "=== Using existing IPA: $IPA ==="
  echo "    (Pro status depends on how it was built — use --build to force a fresh Pro build)"
fi
[[ -n "$IPA" && -f "$IPA" ]] || { echo "ERROR: no IPA found in $IPA_DIR" >&2; exit 1; }
# ── OTA (remote over Tailscale) ────────────────────────────────────────────
if [[ "$OTA" == true ]]; then
  VERSION="$(grep -E '^version:' pubspec.yaml | sed -E 's/version:[[:space:]]*//; s/\+.*//' | head -1)"
  echo ""
  echo "=== Publishing to aibroker-ota hub (Tailscale) ==="
  # 8767, not 8765. The AIBroker daemon's PAILot MQTT broker owns 8765, so this
  # POST went to an MQTT listener and curl came back with nothing.
  resp="$(curl -sf -X POST http://127.0.0.1:8767/api/apps \
    -F slug=pailot -F name=PAILot -F bundleId=com.tekmidian.pailot \
    -F "version=${VERSION:-1.0.0}" -F platform=ios -F "file=@${IPA}" 2>&1)" || {
      echo "ERROR: publish failed — is the hub up? Run: aibroker ota up" >&2; exit 1; }
  # Derive the tailnet host for the HTTPS install URL.
  ts="tailscale"; command -v tailscale >/dev/null 2>&1 || ts="/Applications/Tailscale.app/Contents/MacOS/Tailscale"
  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)"
  echo ""
  # The :8443 is not optional. `aibroker ota up` maps /install/ and /api/ under
  # the TAILNET-ONLY listener on 8443; port 443 on the same host is a Funnel
  # carrying the Todoist webhook and nothing else. Printing the URL without the
  # port sent you to that Funnel, which 404s — and it did so on the road, which
  # is the only place this command is ever used.
  if [[ -n "$host" ]]; then
    echo "  Install on any tailnet device — open in Safari:"
    echo "    https://${host}:8443/install/pailot/"
  else
    echo "  Published. Open the install page in Safari on a tailnet device:"
    echo "    https://<your-mac-tailnet-host>:8443/install/pailot/"
  fi
  echo ""
  echo "=== Done (OTA). Tap Install in Safari, then force-quit & reopen PAILot. ==="
  exit 0
fi
# ── devicectl install (local network) ──────────────────────────────────────
# Default target: Matthias' iPhone.
if [[ ${#TARGETS[@]} -eq 0 ]]; then
  TARGETS+=("$IPHONE_MATTHIAS|Matthias' iPhone")
fi
install_one() {
  local id="$1" label="$2"
  echo ""
  echo "=== Installing on $label ($id) ==="
  if xcrun devicectl device install app --device "$id" "$IPA"; then
    echo "  OK: $label"
  else
    echo "  WARNING: install on $label failed — awake & on the same network?"
    echo "           Off the local wifi? Use: bash tools/deploy.sh --ota"
    return 1
  fi
}
fail=0
for t in "${TARGETS[@]}"; do
  install_one "${t%%|*}" "${t#*|}" || fail=1
done
echo ""
if [[ $fail -eq 0 ]]; then
  echo "=== Done — force-quit and reopen PAILot on the device(s). ==="
else
  echo "=== Done, with failures (see WARNINGs above). ==="
  exit 1
fi