pairling 0.2.4 → 0.2.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pairling",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Pair your iPhone with the AI coding agents running on your Mac. CLI and local runtime installer for the Pairling iOS app.",
5
5
  "keywords": [
6
6
  "pairling",
@@ -15,10 +15,6 @@
15
15
  "bugs": {
16
16
  "url": "https://pairling.dev/support"
17
17
  },
18
- "repository": {
19
- "type": "git",
20
- "url": "https://github.com/mergimg0/pairling-helper"
21
- },
22
18
  "license": "UNLICENSED",
23
19
  "author": "Pairling (https://pairling.dev)",
24
20
  "type": "module",
@@ -39,8 +35,12 @@
39
35
  "publishConfig": {
40
36
  "access": "public"
41
37
  },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/mergimg0/pairling-helper"
41
+ },
42
42
  "optionalDependencies": {
43
- "@pairling/runtime-darwin-arm64": "0.2.4",
44
- "@pairling/runtime-darwin-x64": "0.2.4"
43
+ "@pairling/runtime-darwin-arm64": "0.2.5",
44
+ "@pairling/runtime-darwin-x64": "0.2.5"
45
45
  }
46
46
  }
@@ -1 +1 @@
1
- 7f88c1b
1
+ 2f094bd
@@ -1 +1 @@
1
- 0.2.4
1
+ 0.2.5
@@ -747,7 +747,20 @@ ptybroker_live_session_count() {
747
747
  import json
748
748
  import sys
749
749
 
750
- payload = json.loads(sys.argv[1])
750
+ def load_json_arg(raw):
751
+ text = str(raw or "").strip()
752
+ decoder = json.JSONDecoder()
753
+ for index, char in enumerate(text):
754
+ if char not in "{[":
755
+ continue
756
+ try:
757
+ value, _ = decoder.raw_decode(text[index:])
758
+ return value
759
+ except json.JSONDecodeError:
760
+ continue
761
+ return {}
762
+
763
+ payload = load_json_arg(sys.argv[1])
751
764
  status = payload.get("status") if isinstance(payload.get("status"), dict) else {}
752
765
  print(status.get("live_session_count", "unknown"))
753
766
  PY
@@ -799,7 +812,20 @@ ptybroker_live_revision() {
799
812
  import json
800
813
  import sys
801
814
 
802
- payload = json.loads(sys.argv[1])
815
+ def load_json_arg(raw):
816
+ text = str(raw or "").strip()
817
+ decoder = json.JSONDecoder()
818
+ for index, char in enumerate(text):
819
+ if char not in "{[":
820
+ continue
821
+ try:
822
+ value, _ = decoder.raw_decode(text[index:])
823
+ return value
824
+ except json.JSONDecodeError:
825
+ continue
826
+ return {}
827
+
828
+ payload = load_json_arg(sys.argv[1])
803
829
  status = payload.get("status") if isinstance(payload.get("status"), dict) else payload
804
830
  print(status.get("source_revision") or "")
805
831
  PY
@@ -813,7 +839,20 @@ import sys
813
839
  from pathlib import Path
814
840
 
815
841
  current = Path(sys.argv[1])
816
- payload = json.loads(sys.argv[2])
842
+ def load_json_arg(raw):
843
+ text = str(raw or "").strip()
844
+ decoder = json.JSONDecoder()
845
+ for index, char in enumerate(text):
846
+ if char not in "{[":
847
+ continue
848
+ try:
849
+ value, _ = decoder.raw_decode(text[index:])
850
+ return value
851
+ except json.JSONDecodeError:
852
+ continue
853
+ return {}
854
+
855
+ payload = load_json_arg(sys.argv[2])
817
856
  live = payload.get("status") if isinstance(payload.get("status"), dict) else payload
818
857
 
819
858
  def read_revision(root: Path):
@@ -881,7 +920,20 @@ ptybroker_report_deferred_restart() {
881
920
  import json
882
921
  import sys
883
922
 
884
- state = json.loads(sys.argv[1])
923
+ def load_json_arg(raw):
924
+ text = str(raw or "").strip()
925
+ decoder = json.JSONDecoder()
926
+ for index, char in enumerate(text):
927
+ if char not in "{[":
928
+ continue
929
+ try:
930
+ value, _ = decoder.raw_decode(text[index:])
931
+ return value
932
+ except json.JSONDecodeError:
933
+ continue
934
+ return {}
935
+
936
+ state = load_json_arg(sys.argv[1])
885
937
  if state.get("state") != "stale_deferred":
886
938
  raise SystemExit(0)
887
939
  live = state.get("live") if isinstance(state.get("live"), dict) else {}
@@ -902,7 +954,20 @@ ptybroker_state_field() {
902
954
  import json
903
955
  import sys
904
956
 
905
- payload = json.loads(sys.argv[1])
957
+ def load_json_arg(raw):
958
+ text = str(raw or "").strip()
959
+ decoder = json.JSONDecoder()
960
+ for index, char in enumerate(text):
961
+ if char not in "{[":
962
+ continue
963
+ try:
964
+ value, _ = decoder.raw_decode(text[index:])
965
+ return value
966
+ except json.JSONDecodeError:
967
+ continue
968
+ return {}
969
+
970
+ payload = load_json_arg(sys.argv[1])
906
971
  value = payload
907
972
  for part in sys.argv[2].split("."):
908
973
  if isinstance(value, dict):
@@ -1089,7 +1154,7 @@ install_runtime() {
1089
1154
  log "Installed Pairling runtime $RELEASE_NAME"
1090
1155
  if ! is_dry_run; then
1091
1156
  log ""
1092
- if ! pair_runtime --qr; then
1157
+ if ! PAIRLING_CONNECTD_ROUTE_WAIT_SECONDS="${PAIRLING_CONNECTD_ROUTE_WAIT_SECONDS:-35}" pair_runtime --qr; then
1093
1158
  log "Pairling installed, but setup could not generate a pairing invitation. Run: pairling doctor --json; pairling pair --qr" >&2
1094
1159
  exit 1
1095
1160
  fi
@@ -1156,6 +1221,7 @@ import os
1156
1221
  import socket
1157
1222
  import subprocess
1158
1223
  import sys
1224
+ import time
1159
1225
  import urllib.parse
1160
1226
  import urllib.error
1161
1227
  import urllib.request
@@ -1268,34 +1334,64 @@ def detected_tailnet_ip() -> str:
1268
1334
  return ip
1269
1335
  return ""
1270
1336
 
1337
+ def connectd_route_wait_seconds() -> float:
1338
+ try:
1339
+ return min(max(float(os.environ.get("PAIRLING_CONNECTD_ROUTE_WAIT_SECONDS") or "0"), 0.0), 60.0)
1340
+ except ValueError:
1341
+ return 0.0
1342
+
1343
+ def connectd_route_poll_seconds() -> float:
1344
+ try:
1345
+ return min(max(float(os.environ.get("PAIRLING_CONNECTD_ROUTE_POLL_SECONDS") or "0.5"), 0.1), 2.0)
1346
+ except ValueError:
1347
+ return 0.5
1348
+
1349
+ def status_could_be_ready_soon(status: dict) -> bool:
1350
+ if not status:
1351
+ return True
1352
+ if status.get("auth_url_present"):
1353
+ return False
1354
+ return True
1355
+
1356
+ def ready_connectd_route():
1357
+ wait_seconds = connectd_route_wait_seconds()
1358
+ poll_seconds = connectd_route_poll_seconds()
1359
+ deadline = time.monotonic() + wait_seconds
1360
+ while True:
1361
+ status = fetch_connectd_status(timeout_seconds=0.7)
1362
+ connect_routes = advertised_pairling_connect_routes(status)
1363
+ if connect_routes:
1364
+ return connect_routes[0]
1365
+ if wait_seconds <= 0 or time.monotonic() >= deadline or not status_could_be_ready_soon(status):
1366
+ return None
1367
+ time.sleep(min(poll_seconds, max(0.0, deadline - time.monotonic())))
1368
+
1271
1369
  def default_pair_route(port_number: int) -> dict:
1272
1370
  for key in ("PAIRLING_PAIR_BASE_URL", "PAIRLING_PUBLIC_BASE_URL"):
1273
1371
  value = os.environ.get(key)
1274
1372
  if value:
1275
1373
  return {"base_url": value, "source": "explicit_override", "status": "override"}
1276
- # First-pair QR claims go through iOS URLSession before any persisted
1277
- # Pairling Connect route exists. TestFlight builds allow local HTTP via
1278
- # NSAllowsLocalNetworking, but ATS blocks plain HTTP to 100.64/10 tailnet
1279
- # addresses. Prefer ATS-local LAN/Bonjour bases for bootstrap; remote
1280
- # tailnet routes are validated and promoted after the claim.
1281
- lan_ip = detected_lan_ip()
1282
- if lan_ip:
1283
- return {"base_url": f"http://{lan_ip}:{port_number}", "source": "lan", "status": "fallback", "kind": "lan"}
1284
- if os.environ.get("PAIRLING_DISABLE_BONJOUR") != "1" and os.environ.get("PAIRLING_TEST_DISABLE_BONJOUR") != "1":
1285
- return {"base_url": f"http://{socket.gethostname()}.local:{port_number}", "source": "bonjour", "status": "fallback", "kind": "bonjour"}
1286
- connect_routes = advertised_pairling_connect_routes(fetch_connectd_status(timeout_seconds=0.7))
1287
- if connect_routes:
1288
- route = connect_routes[0]
1374
+ # Remote-first pairing: if connectd reports a ready Pairling Connect route,
1375
+ # the QR advertises that route and the iOS app claims it through the
1376
+ # embedded pre-pair transport. LAN/Bonjour are explicit degraded fallbacks
1377
+ # when Pairling Connect is not ready.
1378
+ route = ready_connectd_route()
1379
+ if route:
1289
1380
  return {
1290
1381
  "base_url": route["base_url"],
1291
1382
  "source": route["source"],
1292
1383
  "status": route["status"],
1293
1384
  "kind": route["kind"],
1294
1385
  }
1386
+ lan_ip = detected_lan_ip()
1387
+ if lan_ip:
1388
+ return {"base_url": f"http://{lan_ip}:{port_number}", "source": "lan", "status": "fallback", "kind": "lan"}
1389
+ if os.environ.get("PAIRLING_DISABLE_BONJOUR") != "1" and os.environ.get("PAIRLING_TEST_DISABLE_BONJOUR") != "1":
1390
+ return {"base_url": f"http://{socket.gethostname()}.local:{port_number}", "source": "bonjour", "status": "fallback", "kind": "bonjour"}
1295
1391
  tailnet_ip = detected_tailnet_ip()
1296
1392
  if tailnet_ip:
1297
- return {"base_url": f"http://{tailnet_ip}:{port_number}", "source": "standalone_tailnet", "status": "fallback"}
1298
- return {"base_url": f"http://{socket.gethostname()}.local:{port_number}", "source": "bonjour", "status": "fallback"}
1393
+ return {"base_url": f"http://{tailnet_ip}:{port_number}", "source": "standalone_tailnet", "status": "fallback", "kind": "standalone_tailnet"}
1394
+ return {"base_url": f"http://{socket.gethostname()}.local:{port_number}", "source": "bonjour", "status": "fallback", "kind": "bonjour"}
1299
1395
 
1300
1396
  pair_route = default_pair_route(int(port))
1301
1397
  base_url = str(pair_route.get("base_url") or "")
@@ -1316,6 +1412,11 @@ if pair_id and secret:
1316
1412
  pair_params["route_status"] = "ready"
1317
1413
  pair_params["route_kind"] = str(pair_route.get("kind") or "tailnet")
1318
1414
  pair_params["route_contract"] = "pairling-runtime-v1"
1415
+ elif pair_route.get("status") == "fallback":
1416
+ pair_params["route_source"] = "local_fallback"
1417
+ pair_params["route_status"] = "degraded"
1418
+ pair_params["route_kind"] = str(pair_route.get("kind") or pair_route.get("source") or "local")
1419
+ pair_params["route_contract"] = "pairling-runtime-v1"
1319
1420
  manual = {
1320
1421
  "base_url": base_url,
1321
1422
  "pair_id": pair_id,
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "connectd": {
3
3
  "darwin-arm64": {
4
- "sha256": "d77d7299f92da3a72d38981dd8f31f7b5935cd297a29b0231a2b79b8b37ef7f4",
4
+ "sha256": "96400014bc7d32bd1b976216eb6dc76a6621d9da34a2ef02da87c72957dea170",
5
5
  "team_id": "965AVD34A3"
6
6
  },
7
7
  "darwin-x64": {
8
- "sha256": "393c95dcd6128699614423d6414a21962d796a2baf8f249789d98d247668248e",
8
+ "sha256": "bb4df01a72f6c2032c8d7341f5d243fa3e6c99295c033dbf3f99a7d7eed650b7",
9
9
  "team_id": "965AVD34A3"
10
10
  }
11
11
  },
@@ -20,11 +20,11 @@
20
20
  },
21
21
  {
22
22
  "path": "payload/mac/SOURCE_REVISION",
23
- "sha256": "e88a4e5e02bc17d75bd064d71908ab84750f877613ce56e1efb4be3d453a5a81"
23
+ "sha256": "6fba526ba8b27c395d71b9d6067bb09fe9458c5d650a92af80541ad7971a3ba3"
24
24
  },
25
25
  {
26
26
  "path": "payload/mac/VERSION",
27
- "sha256": "1725bfa6524c5265e7c171cf06568417d39b947fff49c242f03859479c82334b"
27
+ "sha256": "e959f750e92dbb7614ae28ac96f0a37272caed81d8bbb758791af7bfbb6106a0"
28
28
  },
29
29
  {
30
30
  "path": "payload/mac/companiond/app_attest_lan.py",
@@ -252,7 +252,7 @@
252
252
  },
253
253
  {
254
254
  "path": "payload/mac/install/install-runtime.sh",
255
- "sha256": "29e3e3ffd11d1dc5de96a29d5724752c4651168c5cf15b6ab8e5797e85e6a1ee"
255
+ "sha256": "87c1db89b281b78d0809b4360c54c5c57b8e4f25a473496373315236ffeb6117"
256
256
  },
257
257
  {
258
258
  "path": "payload/mac/install/psk_dependency_check.py",
@@ -276,8 +276,8 @@
276
276
  }
277
277
  ],
278
278
  "package": "pairling",
279
- "package_version": "0.2.4",
279
+ "package_version": "0.2.5",
280
280
  "schema_version": 1,
281
281
  "source_dirty": false,
282
- "source_revision": "7f88c1b"
282
+ "source_revision": "2f094bd"
283
283
  }