From 77911f6418ceb771c5f0b6b2aa33328c7aa5530a Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Wed, 08 Jul 2026 18:28:29 +0200
Subject: [PATCH] feat: continuous text selection, Cmd+Enter to send, deploy.sh OTA/iPad targets

---
 tools/deploy.sh |   81 +++++++++++++++++++++++++++++-----------
 1 files changed, 58 insertions(+), 23 deletions(-)

diff --git a/tools/deploy.sh b/tools/deploy.sh
index b8bfc45..c64824d 100755
--- a/tools/deploy.sh
+++ b/tools/deploy.sh
@@ -3,24 +3,26 @@
 #
 # Installs a personal (Pro-unlocked) PAILot build onto one or more iOS devices.
 #
-# ── Remote / "fully remote" targets ────────────────────────────────────────
-# Installation uses `xcrun devicectl device install`, which reaches the device
-# over Apple's CoreDevice tunnel — USB, local network, OR any routable network
-# path where the device is paired (e.g. across Tailscale). There is NO special
-# remote logic to add: once a device has been paired, `--device <id>` works
-# whether it sits on your desk or across the world, as long as it is awake and
-# reachable. That is exactly how the iPad here installs — it is a `localNetwork`
-# device, never plugged in. (First-time discovery of a NEW device uses .local /
-# mDNS, which does not cross networks, so a device must be paired at least once
-# on the local network before it can be driven fully remotely.)
-# Check reachability any time with:  bash tools/deploy.sh --check
+# ── 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)
+#   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 identifier (any remote device)
+#   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
@@ -29,7 +31,9 @@
 APP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
 cd "$APP_DIR"
 
-IPA="build/ios/ipa/pailot.ipa"
+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"
@@ -38,6 +42,7 @@
 
 TARGETS=()          # entries: "identifier|label"
 FORCE_BUILD=false
+OTA=false
 
 while [[ $# -gt 0 ]]; do
   case "$1" in
@@ -50,23 +55,20 @@
                 "$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,30p' "$0"; exit 0 ;;
+    -h|--help)   sed -n '2,33p' "$0"; exit 0 ;;
     *) echo "Unknown option: $1" >&2; exit 2 ;;
   esac
 done
-
-# Default target: Matthias' iPhone.
-if [[ ${#TARGETS[@]} -eq 0 ]]; then
-  TARGETS+=("$IPHONE_MATTHIAS|Matthias' iPhone")
-fi
 
 # ── 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.
-if [[ "$FORCE_BUILD" == true || ! -f "$IPA" ]]; then
+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
@@ -75,9 +77,42 @@
     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) ==="
+  resp="$(curl -sf -X POST http://127.0.0.1:8765/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 ""
+  if [[ -n "$host" ]]; then
+    echo "  Install on any tailnet device — open in Safari:"
+    echo "    https://${host}/install/pailot/"
+  else
+    echo "  Published. Open the install page in Safari on a tailnet device:"
+    echo "    https://<your-mac-tailnet-host>/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() {
@@ -87,8 +122,8 @@
   if xcrun devicectl device install app --device "$id" "$IPA"; then
     echo "  OK: $label"
   else
-    echo "  WARNING: install on $label failed — is it awake & reachable?"
-    echo "           Try: bash tools/deploy.sh --check"
+    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
 }

--
Gitblit v1.3.1