@leejungkiin/awkit 1.7.0 → 1.7.4

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.
Files changed (241) hide show
  1. package/bin/awk.js +576 -84
  2. package/core/CLAUDE.md +1 -1
  3. package/core/GEMINI.md +148 -167
  4. package/core/GEMINI.md.bak +149 -116
  5. package/core/skill-runtime-manifest.json +3 -0
  6. package/docs/Claude Fable 5.md +3826 -0
  7. package/docs/android_kotlin_system_instruction.md +210 -0
  8. package/docs/brainstorm_ponytail_integration.md +146 -0
  9. package/docs/brainstorm_smart_setup.md +113 -0
  10. package/docs/deep-research-report (1).md +293 -0
  11. package/docs/history/GEMINI.v1.md +135 -0
  12. package/docs/history/brainstorm_antigravity_unified_architecture.v1.md +105 -0
  13. package/docs/history/implementation_plan.v1.md +58 -0
  14. package/package.json +4 -1
  15. package/scripts/artifact-storage.js +130 -0
  16. package/scripts/automation-gate.js +40 -7
  17. package/scripts/claude-plan.js +76 -0
  18. package/scripts/dependency-manager.js +210 -0
  19. package/scripts/exec-rtk.js +11 -5
  20. package/scripts/i18n-helper.js +381 -0
  21. package/scripts/multi-model-pipeline.js +144 -0
  22. package/skill-packs/mobile-ios/pack.json +4 -2
  23. package/skill-packs/reverse-engineering/pack.json +1 -0
  24. package/skills/CATALOG.md +20 -0
  25. package/skills/GEMINI.md +9 -1
  26. package/skills/TRIGGER_INDEX.md +10 -0
  27. package/skills/ai-music/SKILL.md +275 -0
  28. package/skills/android-re-analyzer/SKILL.md +238 -0
  29. package/skills/android-re-analyzer/references/api-extraction-patterns.md +119 -0
  30. package/skills/android-re-analyzer/references/call-flow-analysis.md +176 -0
  31. package/skills/android-re-analyzer/references/fernflower-usage.md +115 -0
  32. package/skills/android-re-analyzer/references/jadx-usage.md +116 -0
  33. package/skills/android-re-analyzer/references/setup-guide.md +221 -0
  34. package/skills/android-re-analyzer/scripts/check-deps.sh +129 -0
  35. package/skills/android-re-analyzer/scripts/decompile.sh +375 -0
  36. package/skills/android-re-analyzer/scripts/find-api-calls.sh +118 -0
  37. package/skills/android-re-analyzer/scripts/install-dep.sh +448 -0
  38. package/skills/animal-island-ui-style/SKILL.md +1450 -0
  39. package/skills/app-store-review-agent/SKILL.md +164 -0
  40. package/skills/app-store-review-agent/references/guidelines/README.md +154 -0
  41. package/skills/app-store-review-agent/references/guidelines/by-app-type/ai_apps.md +37 -0
  42. package/skills/app-store-review-agent/references/guidelines/by-app-type/all_apps.md +50 -0
  43. package/skills/app-store-review-agent/references/guidelines/by-app-type/crypto_finance.md +31 -0
  44. package/skills/app-store-review-agent/references/guidelines/by-app-type/games.md +31 -0
  45. package/skills/app-store-review-agent/references/guidelines/by-app-type/health_fitness.md +31 -0
  46. package/skills/app-store-review-agent/references/guidelines/by-app-type/kids.md +27 -0
  47. package/skills/app-store-review-agent/references/guidelines/by-app-type/macos.md +38 -0
  48. package/skills/app-store-review-agent/references/guidelines/by-app-type/social_ugc.md +32 -0
  49. package/skills/app-store-review-agent/references/guidelines/by-app-type/subscription_iap.md +34 -0
  50. package/skills/app-store-review-agent/references/guidelines/by-app-type/vpn.md +18 -0
  51. package/skills/app-store-review-agent/references/rules/design/minimum_functionality.md +96 -0
  52. package/skills/app-store-review-agent/references/rules/design/sign_in_with_apple.md +54 -0
  53. package/skills/app-store-review-agent/references/rules/entitlements/unused_entitlements.md +83 -0
  54. package/skills/app-store-review-agent/references/rules/metadata/accurate_metadata.md +54 -0
  55. package/skills/app-store-review-agent/references/rules/metadata/apple_trademark.md +99 -0
  56. package/skills/app-store-review-agent/references/rules/metadata/china_storefront.md +72 -0
  57. package/skills/app-store-review-agent/references/rules/metadata/competitor_terms.md +56 -0
  58. package/skills/app-store-review-agent/references/rules/metadata/subscription_metadata.md +81 -0
  59. package/skills/app-store-review-agent/references/rules/privacy/privacy_manifest.md +84 -0
  60. package/skills/app-store-review-agent/references/rules/privacy/unnecessary_data.md +60 -0
  61. package/skills/app-store-review-agent/references/rules/subscription/misleading_pricing.md +63 -0
  62. package/skills/app-store-review-agent/references/rules/subscription/missing_tos_pp.md +54 -0
  63. package/skills/awf-ponytail/SKILL.md +91 -0
  64. package/skills/awf-ponytail-review/SKILL.md +67 -0
  65. package/skills/awf-session-restore/SKILL.md +3 -3
  66. package/skills/brainstorm-agent/SKILL.md +11 -2
  67. package/skills/brainstorm-agent/templates/brief-template.md +8 -0
  68. package/skills/claude-planner/SKILL.md +47 -0
  69. package/skills/code-review/SKILL.md +87 -0
  70. package/skills/expo-game-development/SKILL.md +163 -0
  71. package/skills/flutter/LICENSE.txt +202 -0
  72. package/skills/flutter/SKILL.md +127 -0
  73. package/skills/flutter-project-creater/LICENSE.txt +202 -0
  74. package/skills/flutter-project-creater/SKILL.md +106 -0
  75. package/skills/game-developer/SKILL.md +163 -0
  76. package/skills/game-developer/references/ecs-patterns.md +501 -0
  77. package/skills/game-developer/references/multiplayer-networking.md +475 -0
  78. package/skills/game-developer/references/performance-optimization.md +422 -0
  79. package/skills/game-developer/references/unity-patterns.md +271 -0
  80. package/skills/game-developer/references/unreal-cpp.md +352 -0
  81. package/skills/generate-gui-assets/SKILL.md +305 -0
  82. package/skills/generate-gui-assets/agents/openai.yaml +4 -0
  83. package/skills/generate-gui-assets/references/catalog-schema.md +58 -0
  84. package/skills/generate-gui-assets/references/extraction-techniques.md +21 -0
  85. package/skills/generate-gui-assets/references/prompt-patterns.md +58 -0
  86. package/skills/generate-gui-assets/scripts/__pycache__/clean_chroma_edges.cpython-311.pyc +0 -0
  87. package/skills/generate-gui-assets/scripts/build_gui_contact_sheet.py +51 -0
  88. package/skills/generate-gui-assets/scripts/clean_chroma_edges.py +262 -0
  89. package/skills/generate-gui-assets/scripts/copy_approved_icons.py +64 -0
  90. package/skills/generate-gui-assets/scripts/prepare_gui_asset_run.py +91 -0
  91. package/skills/generate-gui-assets/scripts/suggest_grid_options.py +63 -0
  92. package/skills/generate-gui-assets/scripts/validate_gui_catalog.py +50 -0
  93. package/skills/godot-game-development/SKILL.md +142 -0
  94. package/skills/hatch-pet/LICENSE.txt +201 -0
  95. package/skills/hatch-pet/SKILL.md +420 -0
  96. package/skills/hatch-pet/agents/openai.yaml +4 -0
  97. package/skills/hatch-pet/references/animation-rows.md +29 -0
  98. package/skills/hatch-pet/references/codex-pet-contract.md +35 -0
  99. package/skills/hatch-pet/references/qa-rubric.md +60 -0
  100. package/skills/hatch-pet/scripts/__pycache__/clean_chroma_edges.cpython-311.pyc +0 -0
  101. package/skills/hatch-pet/scripts/clean_chroma_edges.py +262 -0
  102. package/skills/hatch-pet/scripts/compose_atlas.py +150 -0
  103. package/skills/hatch-pet/scripts/derive_running_left_from_running_right.py +143 -0
  104. package/skills/hatch-pet/scripts/extract_strip_frames.py +323 -0
  105. package/skills/hatch-pet/scripts/finalize_pet_run.py +382 -0
  106. package/skills/hatch-pet/scripts/generate_pet_images.py +287 -0
  107. package/skills/hatch-pet/scripts/inspect_frames.py +246 -0
  108. package/skills/hatch-pet/scripts/make_contact_sheet.py +96 -0
  109. package/skills/hatch-pet/scripts/package_custom_pet.py +108 -0
  110. package/skills/hatch-pet/scripts/pet_job_status.py +117 -0
  111. package/skills/hatch-pet/scripts/prepare_pet_run.py +673 -0
  112. package/skills/hatch-pet/scripts/queue_pet_repairs.py +172 -0
  113. package/skills/hatch-pet/scripts/record_imagegen_result.py +250 -0
  114. package/skills/hatch-pet/scripts/render_animation_videos.py +134 -0
  115. package/skills/hatch-pet/scripts/render_animation_videos.sh +5 -0
  116. package/skills/hatch-pet/scripts/validate_atlas.py +139 -0
  117. package/skills/i18n-orchestrator/SKILL.md +37 -0
  118. package/skills/ios-simulator-skill/SKILL.md +390 -0
  119. package/skills/ios-simulator-skill/scripts/accessibility_audit.py +300 -0
  120. package/skills/ios-simulator-skill/scripts/app_launcher.py +326 -0
  121. package/skills/ios-simulator-skill/scripts/app_state_capture.py +400 -0
  122. package/skills/ios-simulator-skill/scripts/appearance.py +385 -0
  123. package/skills/ios-simulator-skill/scripts/build_and_test.py +348 -0
  124. package/skills/ios-simulator-skill/scripts/clipboard.py +103 -0
  125. package/skills/ios-simulator-skill/scripts/common/__init__.py +61 -0
  126. package/skills/ios-simulator-skill/scripts/common/cache_utils.py +289 -0
  127. package/skills/ios-simulator-skill/scripts/common/device_utils.py +462 -0
  128. package/skills/ios-simulator-skill/scripts/common/env_config.py +35 -0
  129. package/skills/ios-simulator-skill/scripts/common/hang_pipeline.py +862 -0
  130. package/skills/ios-simulator-skill/scripts/common/hang_sessions.py +490 -0
  131. package/skills/ios-simulator-skill/scripts/common/idb_utils.py +180 -0
  132. package/skills/ios-simulator-skill/scripts/common/screenshot_utils.py +338 -0
  133. package/skills/ios-simulator-skill/scripts/container.py +668 -0
  134. package/skills/ios-simulator-skill/scripts/gesture.py +394 -0
  135. package/skills/ios-simulator-skill/scripts/hang_watcher.py +1533 -0
  136. package/skills/ios-simulator-skill/scripts/keyboard.py +391 -0
  137. package/skills/ios-simulator-skill/scripts/localization_audit.py +483 -0
  138. package/skills/ios-simulator-skill/scripts/location.py +467 -0
  139. package/skills/ios-simulator-skill/scripts/log_monitor.py +493 -0
  140. package/skills/ios-simulator-skill/scripts/model_inspector.py +645 -0
  141. package/skills/ios-simulator-skill/scripts/navigator.py +461 -0
  142. package/skills/ios-simulator-skill/scripts/privacy_manager.py +310 -0
  143. package/skills/ios-simulator-skill/scripts/push_notification.py +240 -0
  144. package/skills/ios-simulator-skill/scripts/screen_mapper.py +296 -0
  145. package/skills/ios-simulator-skill/scripts/sim_health_check.sh +245 -0
  146. package/skills/ios-simulator-skill/scripts/sim_list.py +299 -0
  147. package/skills/ios-simulator-skill/scripts/simctl_boot.py +312 -0
  148. package/skills/ios-simulator-skill/scripts/simctl_create.py +316 -0
  149. package/skills/ios-simulator-skill/scripts/simctl_delete.py +357 -0
  150. package/skills/ios-simulator-skill/scripts/simctl_erase.py +351 -0
  151. package/skills/ios-simulator-skill/scripts/simctl_shutdown.py +290 -0
  152. package/skills/ios-simulator-skill/scripts/simulator_selector.py +375 -0
  153. package/skills/ios-simulator-skill/scripts/status_bar.py +250 -0
  154. package/skills/ios-simulator-skill/scripts/test_recorder.py +323 -0
  155. package/skills/ios-simulator-skill/scripts/visual_diff.py +235 -0
  156. package/skills/ios-simulator-skill/scripts/xcode/__init__.py +13 -0
  157. package/skills/ios-simulator-skill/scripts/xcode/builder.py +397 -0
  158. package/skills/ios-simulator-skill/scripts/xcode/cache.py +204 -0
  159. package/skills/ios-simulator-skill/scripts/xcode/config.py +178 -0
  160. package/skills/ios-simulator-skill/scripts/xcode/reporter.py +343 -0
  161. package/skills/ios-simulator-skill/scripts/xcode/xcresult.py +451 -0
  162. package/skills/ios-visual-qa-strategist/SKILL.md +111 -0
  163. package/skills/ios-visual-qa-strategist/agents/openai.yaml +4 -0
  164. package/skills/ios-visual-qa-strategist/references/ios-tool-selection.md +61 -0
  165. package/skills/ios-visual-qa-strategist/references/minimal-capture-policy.md +56 -0
  166. package/skills/ios-visual-qa-strategist/references/visual-reasoning-heuristics.md +53 -0
  167. package/skills/orchestrator/SKILL.md +0 -20
  168. package/skills/persistent-storage/SKILL.md +55 -0
  169. package/skills/short-maker/SKILL.md +23 -0
  170. package/skills/short-maker/scripts/effects.js +56 -0
  171. package/skills/short-maker/scripts/shortmaker-bridge.js +332 -0
  172. package/skills/short-maker/scripts/videomix.js +601 -0
  173. package/skills/short-maker/templates/hyperframes/cinematic-character.template.html +172 -0
  174. package/skills/short-maker/templates/hyperframes/index.template.html +194 -0
  175. package/skills/smali-to-kotlin/SKILL.md +128 -0
  176. package/skills/smali-to-kotlin/examples/getting-started/tech-stack.md +58 -0
  177. package/skills/smali-to-kotlin/examples/pipeline/data-ui-parity.md +118 -0
  178. package/skills/smali-to-kotlin/examples/pipeline/scanner-and-bootstrap.md +106 -0
  179. package/skills/smali-to-kotlin/library-patterns.md +189 -0
  180. package/skills/smali-to-kotlin/phase-0-discovery.md +128 -0
  181. package/skills/smali-to-kotlin/phase-1-architecture.md +166 -0
  182. package/skills/smali-to-kotlin/phase-2-blueprint-ui.md +347 -0
  183. package/skills/smali-to-kotlin/phase-2-blueprint.md +228 -0
  184. package/skills/smali-to-kotlin/phase-3-build.md +248 -0
  185. package/skills/smali-to-kotlin/phase-3-logic-build.md +268 -0
  186. package/skills/smali-to-kotlin/smali-reading-guide.md +310 -0
  187. package/skills/smali-to-kotlin/templates/app-map.md +101 -0
  188. package/skills/smali-to-kotlin/templates/architecture.md +142 -0
  189. package/skills/smali-to-kotlin/templates/blueprint.md +145 -0
  190. package/skills/spec-gate/SKILL.md +6 -2
  191. package/skills/symphony-enforcer/SKILL.md +8 -0
  192. package/skills/symphony-enforcer/examples/mindful-stop.md +2 -0
  193. package/skills/symphony-enforcer/examples/three-phase.md +16 -0
  194. package/skills/symphony-enforcer/examples/trigger-points.md +7 -1
  195. package/skills/unity-game-development/SKILL.md +231 -0
  196. package/skills/verification-gate/SKILL.md +4 -2
  197. package/skills/video-edit/SKILL.md +36 -0
  198. package/skills/video-edit/scripts/video_edit.py +324 -0
  199. package/templates/setup-mapping.json +48 -0
  200. package/templates/specs/design-template.md +161 -71
  201. package/templates/specs/requirements-template.md +65 -133
  202. package/templates/specs/task-spec-template.xml +3 -0
  203. package/workflows/_uncategorized/critic.md +40 -0
  204. package/workflows/_uncategorized/git-rebase-flow.md +81 -0
  205. package/workflows/_uncategorized/image-gen.md +118 -0
  206. package/workflows/_uncategorized/multi-model-pipeline.md +60 -0
  207. package/workflows/_uncategorized/pixel-gen.md +86 -0
  208. package/workflows/_uncategorized/pixel-setup.md +90 -0
  209. package/workflows/_uncategorized/ponytail-review.md +59 -0
  210. package/workflows/_uncategorized/reverse-android-build.md +222 -0
  211. package/workflows/_uncategorized/reverse-android-design.md +139 -0
  212. package/workflows/_uncategorized/reverse-android-discover.md +150 -0
  213. package/workflows/_uncategorized/reverse-android-scan.md +158 -0
  214. package/workflows/_uncategorized/reverse-android.md +143 -0
  215. package/workflows/_uncategorized/reverse-ios-build.md +240 -0
  216. package/workflows/_uncategorized/reverse-ios-design.md +112 -0
  217. package/workflows/_uncategorized/reverse-ios-discover.md +120 -0
  218. package/workflows/_uncategorized/reverse-ios-scan.md +155 -0
  219. package/workflows/_uncategorized/reverse-ios.md +152 -0
  220. package/workflows/_uncategorized/safety-router.md +34 -0
  221. package/workflows/_uncategorized/teach.md +89 -0
  222. package/workflows/_uncategorized/verify-ui.md +53 -0
  223. package/workflows/_uncategorized/visualize-screenshots.md +34 -0
  224. package/workflows/ads/ads-analyst.md +201 -0
  225. package/workflows/ads/ads-audit.md +106 -0
  226. package/workflows/ads/ads-optimize.md +97 -0
  227. package/workflows/ads/ads-targeting.md +241 -0
  228. package/workflows/ads/adsExpert.md +160 -0
  229. package/workflows/ads/smali-ads-config.md +400 -0
  230. package/workflows/ads/smali-ads-flow.md +331 -0
  231. package/workflows/ads/smali-ads-interstitial.md +377 -0
  232. package/workflows/ads/smali-ads-native.md +382 -0
  233. package/workflows/context/teach.md +89 -0
  234. package/workflows/gitnexus.md +8 -8
  235. package/workflows/lifecycle/brainstorm.md +43 -0
  236. package/workflows/lifecycle/code.md +5 -0
  237. package/workflows/lifecycle/init.md +23 -5
  238. package/workflows/lifecycle/multi-model-pipeline.md +60 -0
  239. package/workflows/quality/ponytail-review.md +59 -0
  240. package/workflows/roles/critic.md +40 -0
  241. package/workflows/roles/safety-router.md +34 -0
@@ -0,0 +1,467 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ GPS location simulation for iOS simulators.
4
+
5
+ Set a fixed coordinate, use a named city preset, play back a GPX route,
6
+ walk a multi-waypoint path, or clear any active location override.
7
+
8
+ Wraps `xcrun simctl location <udid> set/clear/start/run`.
9
+
10
+ Key features:
11
+ - Set precise lat/lng coordinates
12
+ - 15 built-in city presets
13
+ - GPX scenario playback (built-in scenarios only — simctl limitation)
14
+ - Multi-waypoint path with configurable speed and update interval
15
+ - Clear active location override
16
+ - Standard --json, --verbose, and --udid flags
17
+
18
+ Usage examples:
19
+ python scripts/location.py --lat 53.3498 --lng -6.2603
20
+ python scripts/location.py --city Dublin
21
+ python scripts/location.py --gpx FreewayDrive
22
+ python scripts/location.py --waypoints "37.33,-122.03 37.78,-122.41" --speed 10
23
+ python scripts/location.py --clear
24
+ """
25
+
26
+ import argparse
27
+ import json
28
+ import subprocess
29
+ import sys
30
+
31
+ from common import resolve_udid
32
+
33
+ # === PRESETS ===
34
+
35
+ _CITY_ALIASES: set[str] = {"nyc", "sf", "la"}
36
+
37
+ CITY_PRESETS: dict[str, tuple[float, float]] = {
38
+ "dublin": (53.3498, -6.2603),
39
+ "london": (51.5074, -0.1278),
40
+ "newyork": (40.7128, -74.0060),
41
+ "nyc": (40.7128, -74.0060),
42
+ "sanfrancisco": (37.7749, -122.4194),
43
+ "sf": (37.7749, -122.4194),
44
+ "tokyo": (35.6762, 139.6503),
45
+ "sydney": (-33.8688, 151.2093),
46
+ "paris": (48.8566, 2.3522),
47
+ "berlin": (52.5200, 13.4050),
48
+ "beijing": (39.9042, 116.4074),
49
+ "mumbai": (19.0760, 72.8777),
50
+ "cairo": (30.0444, 31.2357),
51
+ "saopaulo": (-23.5505, -46.6333),
52
+ "losangeles": (34.0522, -118.2437),
53
+ "la": (34.0522, -118.2437),
54
+ }
55
+
56
+
57
+ # === MAIN CLASS ===
58
+
59
+
60
+ class LocationManager:
61
+ """Manage simulated GPS location on an iOS simulator."""
62
+
63
+ def __init__(self, udid: str | None = None):
64
+ """Initialize with optional device UDID.
65
+
66
+ Args:
67
+ udid: Simulator UDID — auto-detects booted device if None.
68
+ """
69
+ self.udid = udid
70
+
71
+ def set_coordinate(self, lat: float, lng: float, verbose: bool = False) -> tuple[bool, str]:
72
+ """
73
+ Set simulator GPS to a fixed coordinate.
74
+
75
+ Args:
76
+ lat: Latitude in decimal degrees (-90 to 90).
77
+ lng: Longitude in decimal degrees (-180 to 180).
78
+ verbose: Include extra context in the returned message.
79
+
80
+ Returns:
81
+ (success, message) tuple.
82
+ """
83
+ if not (-90 <= lat <= 90):
84
+ return False, f"Invalid latitude {lat}: must be between -90 and 90"
85
+ if not (-180 <= lng <= 180):
86
+ return False, f"Invalid longitude {lng}: must be between -180 and 180"
87
+
88
+ coord = f"{lat},{lng}"
89
+ cmd = ["xcrun", "simctl", "location", self.udid, "set", coord]
90
+
91
+ try:
92
+ result = subprocess.run(cmd, check=False, capture_output=True, text=True, timeout=15)
93
+ except subprocess.TimeoutExpired:
94
+ return False, "Error: location set timed out"
95
+ except Exception as e:
96
+ return False, f"Error: {e}"
97
+
98
+ if result.returncode != 0:
99
+ error = result.stderr.strip() or "unknown error"
100
+ return False, f"location set failed: {error}"
101
+
102
+ if verbose:
103
+ return True, (
104
+ f"Location set\n"
105
+ f" Latitude: {lat}\n"
106
+ f" Longitude: {lng}\n"
107
+ f" Device: {self.udid}"
108
+ )
109
+ return True, f"Location set: {lat}, {lng}"
110
+
111
+ def set_city(self, city_name: str, verbose: bool = False) -> tuple[bool, str]:
112
+ """
113
+ Set location to a named city preset.
114
+
115
+ Args:
116
+ city_name: Case-insensitive city name from CITY_PRESETS.
117
+ verbose: Include coordinate detail in the returned message.
118
+
119
+ Returns:
120
+ (success, message) tuple.
121
+ """
122
+ key = city_name.lower().replace(" ", "")
123
+ if key not in CITY_PRESETS:
124
+ available = ", ".join(sorted({k for k in CITY_PRESETS if k not in _CITY_ALIASES}))
125
+ return False, f"Unknown city '{city_name}'. Available: {available}"
126
+
127
+ lat, lng = CITY_PRESETS[key]
128
+ success, message = self.set_coordinate(lat, lng, verbose=verbose)
129
+
130
+ if success and not verbose:
131
+ return True, f"Location set: {city_name.title()} ({lat}, {lng})"
132
+ return success, message
133
+
134
+ def run_gpx_scenario(self, scenario: str, verbose: bool = False) -> tuple[bool, str]:
135
+ """
136
+ Run a named GPX location scenario built into the simulator.
137
+
138
+ Use `xcrun simctl location <udid> list` to see available scenarios.
139
+
140
+ Args:
141
+ scenario: Scenario name (e.g. "FreewayDrive", "ApplePark").
142
+ verbose: Include extra context in the returned message.
143
+
144
+ Returns:
145
+ (success, message) tuple.
146
+ """
147
+ cmd = ["xcrun", "simctl", "location", self.udid, "run", scenario]
148
+
149
+ try:
150
+ result = subprocess.run(cmd, check=False, capture_output=True, text=True, timeout=15)
151
+ except subprocess.TimeoutExpired:
152
+ return False, "Error: location run timed out"
153
+ except Exception as e:
154
+ return False, f"Error: {e}"
155
+
156
+ if result.returncode != 0:
157
+ error = result.stderr.strip() or "unknown error"
158
+ return False, f"GPX scenario failed: {error}"
159
+
160
+ if verbose:
161
+ return True, f"GPX scenario running\n Scenario: {scenario}\n Device: {self.udid}"
162
+ return True, f"GPX scenario running: {scenario}"
163
+
164
+ def list_scenarios(self) -> tuple[bool, list[str], str]:
165
+ """
166
+ List built-in location scenarios available on this simulator.
167
+
168
+ Returns:
169
+ (success, scenarios, error) tuple where scenarios is a list of names
170
+ and error is an empty string on success or a description of the failure.
171
+ """
172
+ cmd = ["xcrun", "simctl", "location", self.udid, "list"]
173
+
174
+ try:
175
+ result = subprocess.run(cmd, check=False, capture_output=True, text=True, timeout=15)
176
+ except subprocess.TimeoutExpired:
177
+ return False, [], "Error: list scenarios timed out"
178
+ except Exception as e:
179
+ return False, [], f"Error: {e}"
180
+
181
+ if result.returncode != 0:
182
+ error = result.stderr.strip() or "unknown error"
183
+ return False, [], f"list scenarios failed: {error}"
184
+
185
+ lines = [ln.strip() for ln in result.stdout.splitlines() if ln.strip()]
186
+ return True, lines, ""
187
+
188
+ def start_waypoints(
189
+ self,
190
+ waypoints: list[tuple[float, float]],
191
+ speed_mps: float = 20.0,
192
+ interval_seconds: float | None = None,
193
+ distance_meters: float | None = None,
194
+ verbose: bool = False,
195
+ ) -> tuple[bool, str]:
196
+ """
197
+ Animate the location along a series of waypoints.
198
+
199
+ Requires at least two waypoints.
200
+
201
+ Args:
202
+ waypoints: List of (lat, lng) pairs.
203
+ speed_mps: Movement speed in metres per second (default 20 m/s).
204
+ interval_seconds: Location update interval in seconds; mutually exclusive with distance_meters.
205
+ distance_meters: Location update distance in metres; mutually exclusive with interval_seconds.
206
+ verbose: Include waypoint list in the returned message.
207
+
208
+ Returns:
209
+ (success, message) tuple.
210
+ """
211
+ if len(waypoints) < 2:
212
+ return False, "At least two waypoints are required for --waypoints"
213
+
214
+ coord_args = [f"{lat},{lng}" for lat, lng in waypoints]
215
+ cmd = [
216
+ "xcrun",
217
+ "simctl",
218
+ "location",
219
+ self.udid,
220
+ "start",
221
+ f"--speed={speed_mps}",
222
+ ]
223
+ if interval_seconds is not None:
224
+ cmd.append(f"--interval={interval_seconds}")
225
+ if distance_meters is not None:
226
+ cmd.append(f"--distance={distance_meters}")
227
+ cmd.extend(coord_args)
228
+
229
+ try:
230
+ result = subprocess.run(cmd, check=False, capture_output=True, text=True, timeout=15)
231
+ except subprocess.TimeoutExpired:
232
+ return False, "Error: location start timed out"
233
+ except Exception as e:
234
+ return False, f"Error: {e}"
235
+
236
+ if result.returncode != 0:
237
+ error = result.stderr.strip() or "unknown error"
238
+ return False, f"Waypoint route failed: {error}"
239
+
240
+ if verbose:
241
+ pts = "\n".join(f" {i + 1}. {lat}, {lng}" for i, (lat, lng) in enumerate(waypoints))
242
+ return True, (
243
+ f"Waypoint route started\n"
244
+ f" Speed: {speed_mps} m/s\n"
245
+ f" Waypoints:\n{pts}\n"
246
+ f" Device: {self.udid}"
247
+ )
248
+ return True, f"Waypoint route started: {len(waypoints)} points at {speed_mps} m/s"
249
+
250
+ def clear(self, verbose: bool = False) -> tuple[bool, str]:
251
+ """
252
+ Clear any active location simulation and restore real GPS.
253
+
254
+ Args:
255
+ verbose: Include extra context in the returned message.
256
+
257
+ Returns:
258
+ (success, message) tuple.
259
+ """
260
+ cmd = ["xcrun", "simctl", "location", self.udid, "clear"]
261
+
262
+ try:
263
+ result = subprocess.run(cmd, check=False, capture_output=True, text=True, timeout=15)
264
+ except subprocess.TimeoutExpired:
265
+ return False, "Error: location clear timed out"
266
+ except Exception as e:
267
+ return False, f"Error: {e}"
268
+
269
+ if result.returncode != 0:
270
+ error = result.stderr.strip() or "unknown error"
271
+ return False, f"location clear failed: {error}"
272
+
273
+ if verbose:
274
+ return True, f"Location cleared\n Device: {self.udid}"
275
+ return True, "Location cleared"
276
+
277
+
278
+ # === CLI ===
279
+
280
+
281
+ def _parse_waypoints(raw: str) -> list[tuple[float, float]]:
282
+ """
283
+ Parse a whitespace-separated string of 'lat,lng' pairs.
284
+
285
+ Args:
286
+ raw: e.g. "53.34,-6.26 51.50,-0.12"
287
+
288
+ Returns:
289
+ List of (lat, lng) float tuples.
290
+
291
+ Raises:
292
+ ValueError: If any pair is malformed.
293
+ """
294
+ pairs = []
295
+ for token in raw.split():
296
+ parts = token.split(",")
297
+ if len(parts) != 2:
298
+ raise ValueError(f"Expected 'lat,lng', got: {token!r}")
299
+ try:
300
+ pairs.append((float(parts[0]), float(parts[1])))
301
+ except ValueError:
302
+ raise ValueError(f"Non-numeric coordinate in: {token!r}") from None
303
+ return pairs
304
+
305
+
306
+ def main() -> None:
307
+ """CLI entry point."""
308
+ parser = argparse.ArgumentParser(
309
+ description="Simulate GPS location on an iOS simulator",
310
+ formatter_class=argparse.RawDescriptionHelpFormatter,
311
+ epilog="""
312
+ Examples:
313
+ python scripts/location.py --lat 53.3498 --lng -6.2603
314
+ python scripts/location.py --city Dublin
315
+ python scripts/location.py --gpx FreewayDrive
316
+ python scripts/location.py --waypoints "37.33,-122.03 37.78,-122.41" --speed 10
317
+ python scripts/location.py --clear
318
+ python scripts/location.py --list-scenarios
319
+ """,
320
+ )
321
+
322
+ # Device selection
323
+ parser.add_argument("--udid", help="Target device UDID (auto-detects booted simulator)")
324
+
325
+ # Location actions (mutually exclusive)
326
+ action_group = parser.add_mutually_exclusive_group(required=True)
327
+ action_group.add_argument("--lat", type=float, help="Latitude (requires --lng)")
328
+ action_group.add_argument("--city", metavar="NAME", help="Named city preset")
329
+ action_group.add_argument("--gpx", metavar="SCENARIO", help="Run a built-in GPX scenario name")
330
+ action_group.add_argument(
331
+ "--waypoints",
332
+ metavar="'lat,lng ...'",
333
+ help="Whitespace-separated lat,lng pairs for animated route",
334
+ )
335
+ action_group.add_argument("--clear", action="store_true", help="Clear location override")
336
+ action_group.add_argument(
337
+ "--list-scenarios", action="store_true", help="List available built-in GPX scenarios"
338
+ )
339
+
340
+ # Coordinate companion
341
+ parser.add_argument("--lng", type=float, help="Longitude (used with --lat)")
342
+
343
+ # Waypoint options
344
+ parser.add_argument(
345
+ "--speed",
346
+ type=float,
347
+ default=20.0,
348
+ metavar="M_PER_SEC",
349
+ help="Movement speed in m/s for --waypoints (default: 20)",
350
+ )
351
+
352
+ waypoint_pace = parser.add_mutually_exclusive_group()
353
+ waypoint_pace.add_argument(
354
+ "--interval",
355
+ type=float,
356
+ metavar="SECONDS",
357
+ help="Location update interval in seconds for --waypoints (mutually exclusive with --distance)",
358
+ )
359
+ waypoint_pace.add_argument(
360
+ "--distance",
361
+ type=float,
362
+ metavar="METERS",
363
+ help="Location update distance in metres for --waypoints (mutually exclusive with --interval)",
364
+ )
365
+
366
+ # Output flags
367
+ parser.add_argument("--json", action="store_true", help="Output as JSON")
368
+ parser.add_argument("--verbose", action="store_true", help="Verbose output")
369
+
370
+ args = parser.parse_args()
371
+
372
+ # Validate --lat and --lng must be provided together
373
+ if (args.lat is None) != (args.lng is None):
374
+ parser.error("--lat and --lng must be provided together")
375
+
376
+ # Resolve device UDID
377
+ try:
378
+ udid = resolve_udid(args.udid)
379
+ except RuntimeError as e:
380
+ print(f"Error: {e}", file=sys.stderr)
381
+ sys.exit(1)
382
+
383
+ manager = LocationManager(udid=udid)
384
+
385
+ # === Dispatch ===
386
+
387
+ if args.lat is not None:
388
+ success, message = manager.set_coordinate(args.lat, args.lng, verbose=args.verbose)
389
+ action = "set_coordinate"
390
+ extra: dict = {"lat": args.lat, "lng": args.lng}
391
+
392
+ elif args.city:
393
+ success, message = manager.set_city(args.city, verbose=args.verbose)
394
+ action = "set_city"
395
+ key = args.city.lower().replace(" ", "")
396
+ extra = {
397
+ "city": args.city,
398
+ "lat": CITY_PRESETS[key][0] if key in CITY_PRESETS else None,
399
+ "lng": CITY_PRESETS[key][1] if key in CITY_PRESETS else None,
400
+ }
401
+
402
+ elif args.gpx:
403
+ success, message = manager.run_gpx_scenario(args.gpx, verbose=args.verbose)
404
+ action = "run_gpx"
405
+ extra = {"scenario": args.gpx}
406
+
407
+ elif args.waypoints:
408
+ try:
409
+ waypoints = _parse_waypoints(args.waypoints)
410
+ except ValueError as e:
411
+ print(f"Error: {e}", file=sys.stderr)
412
+ sys.exit(1)
413
+ success, message = manager.start_waypoints(
414
+ waypoints,
415
+ speed_mps=args.speed,
416
+ interval_seconds=args.interval,
417
+ distance_meters=args.distance,
418
+ verbose=args.verbose,
419
+ )
420
+ action = "start_waypoints"
421
+ extra = {
422
+ "waypoints": [{"lat": lat, "lng": lng} for lat, lng in waypoints],
423
+ "speed_mps": args.speed,
424
+ }
425
+
426
+ elif args.list_scenarios:
427
+ ok, scenarios, list_error = manager.list_scenarios()
428
+ if args.json:
429
+ payload: dict = {"action": "list_scenarios", "udid": udid, "scenarios": scenarios}
430
+ if not ok:
431
+ payload["error"] = list_error
432
+ print(json.dumps(payload))
433
+ elif ok and scenarios:
434
+ print("\n".join(f" {s}" for s in scenarios))
435
+ elif ok:
436
+ print("No scenarios available")
437
+ else:
438
+ print(list_error, file=sys.stderr)
439
+ sys.exit(0 if ok else 1)
440
+
441
+ else: # --clear
442
+ success, message = manager.clear(verbose=args.verbose)
443
+ action = "clear"
444
+ extra = {}
445
+
446
+ # === Output ===
447
+
448
+ if args.json:
449
+ print(
450
+ json.dumps(
451
+ {
452
+ "action": action,
453
+ "udid": udid,
454
+ "success": success,
455
+ "message": message,
456
+ **extra,
457
+ }
458
+ )
459
+ )
460
+ else:
461
+ print(message)
462
+
463
+ sys.exit(0 if success else 1)
464
+
465
+
466
+ if __name__ == "__main__":
467
+ main()