@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,668 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ App sandbox / UserDefaults / Core Data inspector for iOS simulators.
4
+
5
+ Wraps `xcrun simctl get_app_container` to provide semantic access to an
6
+ app's data container — no manual CoreSimulator directory archaeology needed.
7
+
8
+ Key features:
9
+ - List files in the app data container (--ls)
10
+ - Read a file from the container (--cat, with progressive cache for large files)
11
+ - Inspect UserDefaults as JSON or key=value (--userdefaults)
12
+ - Locate Core Data stores (--core-data-path)
13
+ - Export the full data container (--export)
14
+ - JSON output mode (--json) and auto-UDID detection
15
+
16
+ Keychain is explicitly out of scope — requires entitlements and is risky.
17
+ """
18
+
19
+ import argparse
20
+ import contextlib
21
+ import json
22
+ import os
23
+ import plistlib
24
+ import shutil
25
+ import subprocess
26
+ import sys
27
+ from decimal import Decimal
28
+ from pathlib import Path
29
+
30
+ from common.cache_utils import ProgressiveCache
31
+ from common.device_utils import resolve_device_identifier
32
+
33
+
34
+ def _env_int(name: str, default: int, *, min_value: int = 0) -> int:
35
+ """Read an integer from the environment with a minimum clamp."""
36
+ raw = os.environ.get(name)
37
+ if raw is None:
38
+ return default
39
+ try:
40
+ return max(min_value, int(raw))
41
+ except ValueError:
42
+ return default
43
+
44
+
45
+ # Threshold in bytes above which --cat output is cached instead of printed inline
46
+ _CAT_CACHE_THRESHOLD_BYTES = _env_int("IOS_SIM_CAT_CACHE_BYTES", 8_192, min_value=512)
47
+
48
+
49
+ class ContainerInspector:
50
+ """Inspect an app's data sandbox on an iOS simulator."""
51
+
52
+ def __init__(self, udid: str | None = None):
53
+ """Initialize inspector with optional device UDID.
54
+
55
+ Args:
56
+ udid: Simulator UDID (uses booted device if None)
57
+ """
58
+ self.udid = udid
59
+ self._cache = ProgressiveCache()
60
+
61
+ # === PUBLIC API ===
62
+
63
+ def get_container_path(self, bundle_id: str) -> tuple[bool, str]:
64
+ """Resolve the data container root for a bundle ID.
65
+
66
+ Args:
67
+ bundle_id: App bundle identifier (e.g. com.example.app)
68
+
69
+ Returns:
70
+ (success, path_or_error_message)
71
+ """
72
+ device = self.udid or "booted"
73
+ try:
74
+ result = subprocess.run(
75
+ ["xcrun", "simctl", "get_app_container", device, bundle_id, "data"],
76
+ check=False,
77
+ capture_output=True,
78
+ text=True,
79
+ timeout=15,
80
+ )
81
+ except subprocess.TimeoutExpired:
82
+ return False, "get_app_container timed out"
83
+ except Exception as e:
84
+ return False, f"get_app_container error: {e}"
85
+
86
+ if result.returncode != 0:
87
+ error = result.stderr.strip() or f"exit code {result.returncode}"
88
+ return False, f"Cannot find container for {bundle_id}: {error}"
89
+
90
+ container_path = result.stdout.strip()
91
+ if not container_path:
92
+ return False, f"No data container found for {bundle_id}"
93
+
94
+ return True, container_path
95
+
96
+ def list_files(
97
+ self,
98
+ bundle_id: str,
99
+ relative_path: str = "",
100
+ max_depth: int = 3,
101
+ ) -> tuple[bool, dict]:
102
+ """List files in the app's data container.
103
+
104
+ Args:
105
+ bundle_id: App bundle identifier
106
+ relative_path: Sub-path within container (default: container root)
107
+ max_depth: Recursive listing depth (default: 3)
108
+
109
+ Returns:
110
+ (success, result_dict) where result_dict contains 'path', 'entries'
111
+ """
112
+ ok, container = self.get_container_path(bundle_id)
113
+ if not ok:
114
+ return False, {"error": container}
115
+
116
+ container_resolved = Path(container).resolve()
117
+ target = container_resolved
118
+ if relative_path:
119
+ target = (container_resolved / relative_path).resolve()
120
+ if not target.is_relative_to(container_resolved):
121
+ return False, {"error": f"Path escapes container boundary: {relative_path}"}
122
+
123
+ if not target.exists():
124
+ return False, {"error": f"Path does not exist: {target}"}
125
+
126
+ if target.is_file():
127
+ return True, {
128
+ "path": str(target),
129
+ "entries": [_describe_path(target, Path(container))],
130
+ "total_files": 1,
131
+ }
132
+
133
+ entries = _walk_directory(target, Path(container), current_depth=0, max_depth=max_depth)
134
+ return True, {
135
+ "bundle_id": bundle_id,
136
+ "container_root": container,
137
+ "listed_path": str(target),
138
+ "max_depth": max_depth,
139
+ "entries": entries,
140
+ "total_entries": len(entries),
141
+ }
142
+
143
+ def cat_file(self, bundle_id: str, relative_path: str) -> tuple[bool, dict]:
144
+ """Read a file from the container.
145
+
146
+ Large files (> 8 KB) are stored in ProgressiveCache and a cache_id is
147
+ returned instead of the raw content — matching the pattern used by
148
+ build_and_test.py and log_monitor.py.
149
+
150
+ Args:
151
+ bundle_id: App bundle identifier
152
+ relative_path: Path relative to container root
153
+
154
+ Returns:
155
+ (success, result_dict) with 'content' or 'cache_id' key
156
+ """
157
+ ok, container = self.get_container_path(bundle_id)
158
+ if not ok:
159
+ return False, {"error": container}
160
+
161
+ container_resolved = Path(container).resolve()
162
+ file_path = (container_resolved / relative_path).resolve()
163
+ if not file_path.is_relative_to(container_resolved):
164
+ return False, {"error": f"Path escapes container boundary: {relative_path}"}
165
+ if not file_path.exists():
166
+ return False, {"error": f"File not found: {file_path}"}
167
+
168
+ if file_path.is_dir():
169
+ return False, {"error": f"Path is a directory — use --ls instead: {relative_path}"}
170
+
171
+ if file_path.is_symlink() and not file_path.resolve().exists():
172
+ return False, {"error": f"Broken symlink: {relative_path}"}
173
+
174
+ try:
175
+ file_bytes = file_path.read_bytes()
176
+ except OSError as exc:
177
+ return False, {"error": f"Cannot read file: {exc}"}
178
+
179
+ size_bytes = len(file_bytes)
180
+
181
+ # Attempt plist decode first (handles both binary and XML plists)
182
+ plist_data, is_plist = _try_parse_plist(file_bytes)
183
+
184
+ if is_plist:
185
+ content = plist_data
186
+ content_type = "plist"
187
+ else:
188
+ # Try UTF-8 text; fall back to hex summary for binary blobs
189
+ try:
190
+ content = file_bytes.decode("utf-8")
191
+ content_type = "text"
192
+ except UnicodeDecodeError:
193
+ content = f"<binary file: {size_bytes} bytes — use --export to retrieve>"
194
+ content_type = "binary"
195
+
196
+ base_result = {
197
+ "bundle_id": bundle_id,
198
+ "path": relative_path,
199
+ "size_bytes": size_bytes,
200
+ "content_type": content_type,
201
+ }
202
+
203
+ # Cache large text / plist content
204
+ if size_bytes > _CAT_CACHE_THRESHOLD_BYTES and content_type != "binary":
205
+ cache_data = {**base_result, "content": content}
206
+ cache_id = self._cache.save(cache_data, "container-cat")
207
+ return True, {
208
+ **base_result,
209
+ "cache_id": cache_id,
210
+ "note": (
211
+ f"File is {size_bytes:,} bytes — full content cached. "
212
+ f"Retrieve with cache_id '{cache_id}'."
213
+ ),
214
+ }
215
+
216
+ return True, {**base_result, "content": content}
217
+
218
+ def read_userdefaults(self, bundle_id: str) -> tuple[bool, dict]:
219
+ """Read UserDefaults plist for the app.
220
+
221
+ Looks in Library/Preferences/<bundle_id>.plist (both binary and XML
222
+ formats are handled via stdlib plistlib).
223
+
224
+ Args:
225
+ bundle_id: App bundle identifier
226
+
227
+ Returns:
228
+ (success, result_dict) with 'preferences' key containing decoded data
229
+ """
230
+ ok, container = self.get_container_path(bundle_id)
231
+ if not ok:
232
+ return False, {"error": container}
233
+
234
+ plist_path = Path(container) / "Library" / "Preferences" / f"{bundle_id}.plist"
235
+
236
+ if not plist_path.exists():
237
+ return False, {
238
+ "error": (
239
+ f"UserDefaults plist not found: {plist_path}. "
240
+ "The app may not have written any defaults yet."
241
+ )
242
+ }
243
+
244
+ try:
245
+ raw_bytes = plist_path.read_bytes()
246
+ except OSError as exc:
247
+ return False, {"error": f"Cannot read plist: {exc}"}
248
+
249
+ plist_data, is_plist = _try_parse_plist(raw_bytes)
250
+ if not is_plist:
251
+ return False, {"error": f"File is not a valid plist: {plist_path}"}
252
+
253
+ return True, {
254
+ "bundle_id": bundle_id,
255
+ "plist_path": str(plist_path),
256
+ "preferences": plist_data,
257
+ "total_keys": len(plist_data) if isinstance(plist_data, dict) else None,
258
+ }
259
+
260
+ def find_core_data_paths(self, bundle_id: str) -> tuple[bool, dict]:
261
+ """Locate Core Data SQLite stores in the container.
262
+
263
+ Searches Library/Application Support/ and Documents/ for *.sqlite,
264
+ *.sqlite-wal, and *.sqlite-shm files. Paths are reported only — no
265
+ file opening occurs.
266
+
267
+ Args:
268
+ bundle_id: App bundle identifier
269
+
270
+ Returns:
271
+ (success, result_dict) with 'stores' list
272
+ """
273
+ ok, container = self.get_container_path(bundle_id)
274
+ if not ok:
275
+ return False, {"error": container}
276
+
277
+ container_path = Path(container)
278
+ search_dirs = [
279
+ container_path / "Library" / "Application Support",
280
+ container_path / "Documents",
281
+ ]
282
+
283
+ stores: list[dict] = []
284
+ sqlite_extensions = {".sqlite", ".sqlite-wal", ".sqlite-shm"}
285
+
286
+ for search_dir in search_dirs:
287
+ if not search_dir.exists():
288
+ continue
289
+ for file_path in search_dir.rglob("*"):
290
+ if file_path.suffix.lower() in sqlite_extensions and file_path.is_file():
291
+ relative = str(file_path.relative_to(container_path))
292
+ size_bytes = file_path.stat().st_size
293
+ stores.append(
294
+ {
295
+ "path": relative,
296
+ "absolute_path": str(file_path),
297
+ "size_bytes": size_bytes,
298
+ "type": _classify_sqlite_file(file_path.name),
299
+ }
300
+ )
301
+
302
+ return True, {
303
+ "bundle_id": bundle_id,
304
+ "container_root": container,
305
+ "stores": stores,
306
+ "total_stores": len(stores),
307
+ }
308
+
309
+ def export_container(self, bundle_id: str, dest_dir: str) -> tuple[bool, dict]:
310
+ """Copy the full data container to dest_dir.
311
+
312
+ Uses shutil.copytree with symlinks=True to preserve symlink structure.
313
+ Keychain is not included — it is not part of the data container path.
314
+
315
+ Args:
316
+ bundle_id: App bundle identifier
317
+ dest_dir: Destination directory (will be created if absent)
318
+
319
+ Returns:
320
+ (success, result_dict) with 'destination' and 'size_bytes'
321
+ """
322
+ ok, container = self.get_container_path(bundle_id)
323
+ if not ok:
324
+ return False, {"error": container}
325
+
326
+ dest = Path(dest_dir)
327
+ target = dest / bundle_id
328
+
329
+ if target.exists():
330
+ return False, {
331
+ "error": (
332
+ f"Destination already exists: {target}. "
333
+ "Remove it or choose a different path."
334
+ )
335
+ }
336
+
337
+ dest.mkdir(parents=True, exist_ok=True)
338
+
339
+ try:
340
+ shutil.copytree(
341
+ container,
342
+ str(target),
343
+ symlinks=True,
344
+ ignore_dangling_symlinks=True,
345
+ )
346
+ except shutil.Error as exc:
347
+ return False, {"error": f"Export failed: {exc}"}
348
+ except OSError as exc:
349
+ return False, {"error": f"Export OS error: {exc}"}
350
+
351
+ total_size = _directory_size_bytes(target)
352
+
353
+ return True, {
354
+ "bundle_id": bundle_id,
355
+ "source": container,
356
+ "destination": str(target),
357
+ "size_bytes": total_size,
358
+ "size_human": _human_bytes(total_size),
359
+ }
360
+
361
+
362
+ # === PRIVATE HELPERS ===
363
+
364
+
365
+ def _try_parse_plist(data: bytes) -> tuple[dict | list | None, bool]:
366
+ """Attempt to parse bytes as a plist (binary or XML).
367
+
368
+ Args:
369
+ data: Raw file bytes
370
+
371
+ Returns:
372
+ (parsed_data, is_plist) — parsed_data is None if not a valid plist
373
+ """
374
+ try:
375
+ parsed = plistlib.loads(data)
376
+ return _make_json_serializable(parsed), True
377
+ except Exception:
378
+ return None, False
379
+
380
+
381
+ def _make_json_serializable(obj):
382
+ """Recursively convert plist types to JSON-serializable equivalents.
383
+
384
+ Handles bytes (→ hex string), sets (→ list), and datetime (→ ISO string).
385
+ """
386
+ if isinstance(obj, dict):
387
+ return {k: _make_json_serializable(v) for k, v in obj.items()}
388
+ if isinstance(obj, list):
389
+ return [_make_json_serializable(i) for i in obj]
390
+ if isinstance(obj, bytes):
391
+ return obj.hex()
392
+ if isinstance(obj, set):
393
+ return sorted(_make_json_serializable(i) for i in obj)
394
+ if isinstance(obj, Decimal):
395
+ return float(obj)
396
+ # datetime — plistlib returns datetime.datetime for plist dates
397
+ if hasattr(obj, "isoformat"):
398
+ return obj.isoformat()
399
+ return obj
400
+
401
+
402
+ def _describe_path(path: Path, container_root: Path) -> dict:
403
+ """Build a metadata dict for a single filesystem path."""
404
+ try:
405
+ stat = path.stat()
406
+ size_bytes = stat.st_size if path.is_file() else None
407
+ except OSError:
408
+ size_bytes = None
409
+
410
+ relative = str(path.relative_to(container_root))
411
+ kind = "symlink" if path.is_symlink() else ("dir" if path.is_dir() else "file")
412
+
413
+ entry: dict = {"path": relative, "kind": kind}
414
+ if size_bytes is not None:
415
+ entry["size_bytes"] = size_bytes
416
+ if path.is_symlink():
417
+ try:
418
+ entry["symlink_target"] = str(path.readlink())
419
+ except OSError:
420
+ entry["symlink_target"] = "<unreadable>"
421
+
422
+ return entry
423
+
424
+
425
+ def _walk_directory(
426
+ directory: Path,
427
+ container_root: Path,
428
+ current_depth: int,
429
+ max_depth: int,
430
+ ) -> list[dict]:
431
+ """Recursively walk a directory up to max_depth, returning metadata entries."""
432
+ entries: list[dict] = []
433
+ try:
434
+ children = sorted(directory.iterdir())
435
+ except PermissionError:
436
+ return entries
437
+
438
+ for child in children:
439
+ entries.append(_describe_path(child, container_root))
440
+ if child.is_dir() and not child.is_symlink() and current_depth < max_depth:
441
+ entries.extend(_walk_directory(child, container_root, current_depth + 1, max_depth))
442
+
443
+ return entries
444
+
445
+
446
+ def _classify_sqlite_file(filename: str) -> str:
447
+ """Return a human-readable type label for a SQLite-related file."""
448
+ lower = filename.lower()
449
+ if lower.endswith(".sqlite-wal"):
450
+ return "write-ahead-log"
451
+ if lower.endswith(".sqlite-shm"):
452
+ return "shared-memory"
453
+ return "database"
454
+
455
+
456
+ def _directory_size_bytes(path: Path) -> int:
457
+ """Return the total size of all files under a directory."""
458
+ total = 0
459
+ for dirpath, _, filenames in os.walk(path, followlinks=False):
460
+ for fname in filenames:
461
+ fpath = Path(dirpath) / fname
462
+ with contextlib.suppress(OSError):
463
+ total += fpath.stat().st_size
464
+ return total
465
+
466
+
467
+ def _human_bytes(num: int) -> str:
468
+ """Format byte count as a human-readable string (KB / MB / GB)."""
469
+ for unit in ("B", "KB", "MB", "GB"):
470
+ if num < 1024:
471
+ return f"{num:.1f} {unit}"
472
+ num /= 1024
473
+ return f"{num:.1f} TB"
474
+
475
+
476
+ def _format_userdefaults(preferences: dict, bundle_id: str, verbose: bool) -> str:
477
+ """Format UserDefaults output as key=value lines."""
478
+ lines = [f"UserDefaults: {bundle_id}"]
479
+ if not preferences:
480
+ lines.append(" (no keys found)")
481
+ return "\n".join(lines)
482
+
483
+ for key, value in sorted(preferences.items()):
484
+ if isinstance(value, dict):
485
+ lines.append(f" {key} = <dict with {len(value)} keys>")
486
+ if verbose:
487
+ for sub_key, sub_value in sorted(value.items()):
488
+ lines.append(f" {sub_key} = {sub_value!r}")
489
+ elif isinstance(value, list):
490
+ lines.append(f" {key} = <list with {len(value)} items>")
491
+ if verbose:
492
+ for item in value:
493
+ lines.append(f" - {item!r}")
494
+ else:
495
+ lines.append(f" {key} = {value!r}")
496
+
497
+ return "\n".join(lines)
498
+
499
+
500
+ # === CLI ===
501
+
502
+
503
+ def main():
504
+ """Main entry point."""
505
+ parser = argparse.ArgumentParser(
506
+ description="App sandbox / UserDefaults / Core Data inspector for iOS simulators",
507
+ formatter_class=argparse.RawDescriptionHelpFormatter,
508
+ epilog="""
509
+ Examples:
510
+ python container.py --ls com.example.app
511
+ python container.py --ls com.example.app Library/Preferences
512
+ python container.py --cat com.example.app Library/Preferences/com.example.app.plist
513
+ python container.py --userdefaults com.example.app
514
+ python container.py --core-data-path com.example.app
515
+ python container.py --export com.example.app ./snapshot/
516
+ python container.py --ls com.example.app --udid <DEVICE_UDID> --json
517
+ """,
518
+ )
519
+
520
+ # Operation flags — mutually exclusive
521
+ ops = parser.add_mutually_exclusive_group(required=True)
522
+ ops.add_argument(
523
+ "--ls",
524
+ metavar="BUNDLE_ID",
525
+ help="List files in the data container. Optionally pass a sub-path as second argument.",
526
+ )
527
+ ops.add_argument(
528
+ "--cat",
529
+ nargs=2,
530
+ metavar=("BUNDLE_ID", "PATH"),
531
+ help="Print a file from the container. Large files are cached.",
532
+ )
533
+ ops.add_argument(
534
+ "--userdefaults",
535
+ metavar="BUNDLE_ID",
536
+ help="Read and display UserDefaults (Library/Preferences/<bundle>.plist).",
537
+ )
538
+ ops.add_argument(
539
+ "--core-data-path",
540
+ metavar="BUNDLE_ID",
541
+ help="List Core Data SQLite store paths inside the container.",
542
+ )
543
+ ops.add_argument(
544
+ "--export",
545
+ nargs=2,
546
+ metavar=("BUNDLE_ID", "DEST_DIR"),
547
+ help="Copy the full data container to DEST_DIR/BUNDLE_ID.",
548
+ )
549
+
550
+ # Sub-path for --ls
551
+ parser.add_argument(
552
+ "path",
553
+ nargs="?",
554
+ default="",
555
+ help="Sub-path for --ls (optional)",
556
+ )
557
+
558
+ # Common flags
559
+ parser.add_argument("--udid", help="Simulator UDID (uses booted device if omitted)")
560
+ parser.add_argument("--json", action="store_true", help="Output as JSON")
561
+ parser.add_argument("--verbose", action="store_true", help="Show extended detail")
562
+ parser.add_argument(
563
+ "--depth",
564
+ type=int,
565
+ default=3,
566
+ help="Recursive depth for --ls (default: 3)",
567
+ )
568
+
569
+ args = parser.parse_args()
570
+
571
+ # Resolve device
572
+ try:
573
+ udid = resolve_device_identifier(args.udid)
574
+ except RuntimeError as exc:
575
+ print(f"Error: {exc}", file=sys.stderr)
576
+ sys.exit(1)
577
+
578
+ inspector = ContainerInspector(udid=udid)
579
+
580
+ # === --ls ===
581
+ if args.ls:
582
+ success, result = inspector.list_files(
583
+ args.ls, relative_path=args.path, max_depth=args.depth
584
+ )
585
+ if args.json:
586
+ print(json.dumps({"action": "ls", "success": success, **result}, indent=2))
587
+ elif not success:
588
+ print(f"Error: {result.get('error', result)}", file=sys.stderr)
589
+ else:
590
+ print(f"Container: {result['container_root']}")
591
+ print(f"Listed: {result['listed_path']} ({result['total_entries']} entries)")
592
+ listing_root = args.path or ""
593
+ root_depth = listing_root.count("/") + 1 if listing_root else 0
594
+ for entry in result["entries"]:
595
+ relative_depth = entry["path"].count("/") - root_depth
596
+ prefix = " " * max(0, relative_depth)
597
+ size = f" [{_human_bytes(entry['size_bytes'])}]" if entry.get("size_bytes") else ""
598
+ print(f" {prefix}{entry['path']} ({entry['kind']}){size}")
599
+ sys.exit(0 if success else 1)
600
+
601
+ # === --cat ===
602
+ if args.cat:
603
+ bundle_id, rel_path = args.cat
604
+ success, result = inspector.cat_file(bundle_id, rel_path)
605
+ if args.json:
606
+ print(json.dumps({"action": "cat", "success": success, **result}, indent=2))
607
+ elif not success:
608
+ print(f"Error: {result.get('error', result)}", file=sys.stderr)
609
+ elif "cache_id" in result:
610
+ print(f"[{result['path']}] {_human_bytes(result['size_bytes'])} (cached)")
611
+ print(result["note"])
612
+ else:
613
+ content = result.get("content", "")
614
+ if isinstance(content, dict | list):
615
+ print(json.dumps(content, indent=2))
616
+ else:
617
+ print(content)
618
+ sys.exit(0 if success else 1)
619
+
620
+ # === --userdefaults ===
621
+ if args.userdefaults:
622
+ success, result = inspector.read_userdefaults(args.userdefaults)
623
+ if args.json:
624
+ print(json.dumps({"action": "userdefaults", "success": success, **result}, indent=2))
625
+ elif not success:
626
+ print(f"Error: {result.get('error', result)}", file=sys.stderr)
627
+ else:
628
+ prefs = result.get("preferences", {})
629
+ print(_format_userdefaults(prefs, args.userdefaults, verbose=args.verbose))
630
+ print(f"\n({result['total_keys']} keys • {result['plist_path']})")
631
+ sys.exit(0 if success else 1)
632
+
633
+ # === --core-data-path ===
634
+ if args.core_data_path:
635
+ success, result = inspector.find_core_data_paths(args.core_data_path)
636
+ if args.json:
637
+ print(json.dumps({"action": "core-data-path", "success": success, **result}, indent=2))
638
+ elif not success:
639
+ print(f"Error: {result.get('error', result)}", file=sys.stderr)
640
+ else:
641
+ stores = result.get("stores", [])
642
+ if not stores:
643
+ print(f"No Core Data stores found for {args.core_data_path}")
644
+ else:
645
+ print(f"Core Data stores for {args.core_data_path}:")
646
+ for store in stores:
647
+ size = _human_bytes(store["size_bytes"])
648
+ print(f" {store['path']} [{size}] ({store['type']})")
649
+ sys.exit(0 if success else 1)
650
+
651
+ # === --export ===
652
+ if args.export:
653
+ bundle_id, dest_dir = args.export
654
+ success, result = inspector.export_container(bundle_id, dest_dir)
655
+ if args.json:
656
+ print(json.dumps({"action": "export", "success": success, **result}, indent=2))
657
+ elif not success:
658
+ print(f"Error: {result.get('error', result)}", file=sys.stderr)
659
+ else:
660
+ print(
661
+ f"Exported: {result['bundle_id']} → {result['destination']} "
662
+ f"[{result['size_human']}]"
663
+ )
664
+ sys.exit(0 if success else 1)
665
+
666
+
667
+ if __name__ == "__main__":
668
+ main()