@leejungkiin/awkit 1.7.1 → 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 (245) 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 +35 -2
  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/video-edit/SKILL.md +36 -0
  197. package/skills/video-edit/scripts/video_edit.py +324 -0
  198. package/templates/project-identity/android.json +2 -2
  199. package/templates/project-identity/backend-nestjs.json +2 -2
  200. package/templates/project-identity/expo.json +2 -2
  201. package/templates/project-identity/ios.json +2 -2
  202. package/templates/project-identity/web-nextjs.json +2 -2
  203. package/templates/setup-mapping.json +48 -0
  204. package/templates/specs/design-template.md +161 -71
  205. package/templates/specs/requirements-template.md +65 -133
  206. package/templates/specs/task-spec-template.xml +3 -0
  207. package/workflows/_uncategorized/critic.md +40 -0
  208. package/workflows/_uncategorized/git-rebase-flow.md +81 -0
  209. package/workflows/_uncategorized/image-gen.md +118 -0
  210. package/workflows/_uncategorized/multi-model-pipeline.md +60 -0
  211. package/workflows/_uncategorized/pixel-gen.md +86 -0
  212. package/workflows/_uncategorized/pixel-setup.md +90 -0
  213. package/workflows/_uncategorized/ponytail-review.md +59 -0
  214. package/workflows/_uncategorized/reverse-android-build.md +222 -0
  215. package/workflows/_uncategorized/reverse-android-design.md +139 -0
  216. package/workflows/_uncategorized/reverse-android-discover.md +150 -0
  217. package/workflows/_uncategorized/reverse-android-scan.md +158 -0
  218. package/workflows/_uncategorized/reverse-android.md +143 -0
  219. package/workflows/_uncategorized/reverse-ios-build.md +240 -0
  220. package/workflows/_uncategorized/reverse-ios-design.md +112 -0
  221. package/workflows/_uncategorized/reverse-ios-discover.md +120 -0
  222. package/workflows/_uncategorized/reverse-ios-scan.md +155 -0
  223. package/workflows/_uncategorized/reverse-ios.md +152 -0
  224. package/workflows/_uncategorized/safety-router.md +34 -0
  225. package/workflows/_uncategorized/teach.md +89 -0
  226. package/workflows/_uncategorized/verify-ui.md +53 -0
  227. package/workflows/_uncategorized/visualize-screenshots.md +34 -0
  228. package/workflows/ads/ads-analyst.md +201 -0
  229. package/workflows/ads/ads-audit.md +106 -0
  230. package/workflows/ads/ads-optimize.md +97 -0
  231. package/workflows/ads/ads-targeting.md +241 -0
  232. package/workflows/ads/adsExpert.md +160 -0
  233. package/workflows/ads/smali-ads-config.md +400 -0
  234. package/workflows/ads/smali-ads-flow.md +331 -0
  235. package/workflows/ads/smali-ads-interstitial.md +377 -0
  236. package/workflows/ads/smali-ads-native.md +382 -0
  237. package/workflows/context/teach.md +89 -0
  238. package/workflows/gitnexus.md +8 -8
  239. package/workflows/lifecycle/brainstorm.md +43 -0
  240. package/workflows/lifecycle/code.md +5 -0
  241. package/workflows/lifecycle/init.md +23 -5
  242. package/workflows/lifecycle/multi-model-pipeline.md +60 -0
  243. package/workflows/quality/ponytail-review.md +59 -0
  244. package/workflows/roles/critic.md +40 -0
  245. package/workflows/roles/safety-router.md +34 -0
@@ -0,0 +1,323 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test Recorder for iOS Simulator Testing
4
+
5
+ Records test execution with automatic screenshots and documentation.
6
+ Optimized for minimal token output during execution.
7
+
8
+ Usage:
9
+ As a script: python scripts/test_recorder.py --test-name "Test Name" --output dir/
10
+ As a module: from scripts.test_recorder import TestRecorder
11
+ """
12
+
13
+ import argparse
14
+ import json
15
+ import subprocess
16
+ import time
17
+ from datetime import datetime
18
+ from pathlib import Path
19
+
20
+ from common import (
21
+ capture_screenshot,
22
+ count_elements,
23
+ generate_screenshot_name,
24
+ get_accessibility_tree,
25
+ resolve_udid,
26
+ )
27
+
28
+
29
+ class TestRecorder:
30
+ """Records test execution with screenshots and accessibility snapshots."""
31
+
32
+ def __init__(
33
+ self,
34
+ test_name: str,
35
+ output_dir: str = "test-artifacts",
36
+ udid: str | None = None,
37
+ inline: bool = False,
38
+ screenshot_size: str = "half",
39
+ app_name: str | None = None,
40
+ ):
41
+ """
42
+ Initialize test recorder.
43
+
44
+ Args:
45
+ test_name: Name of the test being recorded
46
+ output_dir: Directory for test artifacts
47
+ udid: Optional device UDID (uses booted if not specified)
48
+ inline: If True, return screenshots as base64 (for vision-based automation)
49
+ screenshot_size: 'full', 'half', 'quarter', 'thumb' (default: 'half')
50
+ app_name: App name for semantic screenshot naming
51
+ """
52
+ self.test_name = test_name
53
+ self.udid = udid
54
+ self.inline = inline
55
+ self.screenshot_size = screenshot_size
56
+ self.app_name = app_name
57
+ self.start_time = time.time()
58
+ self.steps: list[dict] = []
59
+ self.current_step = 0
60
+
61
+ # Create timestamped output directory
62
+ timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
63
+ safe_name = test_name.lower().replace(" ", "-")
64
+ self.output_dir = Path(output_dir) / f"{safe_name}-{timestamp}"
65
+ self.output_dir.mkdir(parents=True, exist_ok=True)
66
+
67
+ # Create subdirectories (only if not in inline mode)
68
+ if not inline:
69
+ self.screenshots_dir = self.output_dir / "screenshots"
70
+ self.screenshots_dir.mkdir(exist_ok=True)
71
+ else:
72
+ self.screenshots_dir = None
73
+
74
+ self.accessibility_dir = self.output_dir / "accessibility"
75
+ self.accessibility_dir.mkdir(exist_ok=True)
76
+
77
+ # Token-efficient output
78
+ mode_str = "(inline mode)" if inline else ""
79
+ print(f"Recording: {test_name} {mode_str}")
80
+ print(f"Output: {self.output_dir}/")
81
+
82
+ def step(
83
+ self,
84
+ description: str,
85
+ screen_name: str | None = None,
86
+ state: str | None = None,
87
+ assertion: str | None = None,
88
+ metadata: dict | None = None,
89
+ ):
90
+ """
91
+ Record a test step with automatic screenshot.
92
+
93
+ Args:
94
+ description: Step description
95
+ screen_name: Screen name for semantic naming
96
+ state: State description for semantic naming
97
+ assertion: Optional assertion to verify
98
+ metadata: Optional metadata for the step
99
+ """
100
+ self.current_step += 1
101
+ step_time = time.time() - self.start_time
102
+
103
+ # Capture screenshot using new utility
104
+ screenshot_result = capture_screenshot(
105
+ self.udid,
106
+ size=self.screenshot_size,
107
+ inline=self.inline,
108
+ app_name=self.app_name,
109
+ screen_name=screen_name or description,
110
+ state=state,
111
+ )
112
+
113
+ # Capture accessibility tree
114
+ accessibility_path = (
115
+ self.accessibility_dir
116
+ / f"{self.current_step:03d}-{description.lower().replace(' ', '-')[:20]}.json"
117
+ )
118
+ element_count = self._capture_accessibility(accessibility_path)
119
+
120
+ # Store step data
121
+ step_data = {
122
+ "number": self.current_step,
123
+ "description": description,
124
+ "timestamp": step_time,
125
+ "element_count": element_count,
126
+ "accessibility": accessibility_path.name,
127
+ "screenshot_mode": screenshot_result["mode"],
128
+ "screenshot_size": self.screenshot_size,
129
+ }
130
+
131
+ # Handle screenshot data based on mode
132
+ if screenshot_result["mode"] == "file":
133
+ step_data["screenshot"] = screenshot_result["file_path"]
134
+ step_data["screenshot_name"] = Path(screenshot_result["file_path"]).name
135
+ else:
136
+ # Inline mode
137
+ step_data["screenshot_base64"] = screenshot_result["base64_data"]
138
+ step_data["screenshot_dimensions"] = (
139
+ screenshot_result["width"],
140
+ screenshot_result["height"],
141
+ )
142
+
143
+ if assertion:
144
+ step_data["assertion"] = assertion
145
+ step_data["assertion_passed"] = True
146
+
147
+ if metadata:
148
+ step_data["metadata"] = metadata
149
+
150
+ self.steps.append(step_data)
151
+
152
+ # Token-efficient output (single line)
153
+ status = "✓" if not assertion or step_data.get("assertion_passed") else "✗"
154
+ screenshot_info = (
155
+ f" [{screenshot_result['width']}x{screenshot_result['height']}]" if self.inline else ""
156
+ )
157
+ print(
158
+ f"{status} Step {self.current_step}: {description} ({step_time:.1f}s){screenshot_info}"
159
+ )
160
+
161
+ def _capture_screenshot(self, output_path: Path) -> bool:
162
+ """Capture screenshot using simctl."""
163
+ cmd = ["xcrun", "simctl", "io"]
164
+
165
+ if self.udid:
166
+ cmd.append(self.udid)
167
+ else:
168
+ cmd.append("booted")
169
+
170
+ cmd.extend(["screenshot", str(output_path)])
171
+
172
+ try:
173
+ subprocess.run(cmd, capture_output=True, check=True)
174
+ return True
175
+ except subprocess.CalledProcessError:
176
+ return False
177
+
178
+ def _capture_accessibility(self, output_path: Path) -> int:
179
+ """Capture accessibility tree and return element count."""
180
+ try:
181
+ # Use shared utility to fetch tree
182
+ tree = get_accessibility_tree(self.udid, nested=True)
183
+
184
+ # Save tree
185
+ with open(output_path, "w") as f:
186
+ json.dump(tree, f, indent=2)
187
+
188
+ # Count elements using shared utility
189
+ return count_elements(tree)
190
+ except Exception:
191
+ return 0
192
+
193
+ def generate_report(self) -> dict[str, str]:
194
+ """
195
+ Generate markdown test report.
196
+
197
+ Returns:
198
+ Dictionary with paths to generated files
199
+ """
200
+ duration = time.time() - self.start_time
201
+ report_path = self.output_dir / "report.md"
202
+
203
+ # Generate markdown
204
+ with open(report_path, "w") as f:
205
+ f.write(f"# Test Report: {self.test_name}\n\n")
206
+ f.write(f"**Date:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
207
+ f.write(f"**Duration:** {duration:.1f} seconds\n")
208
+ f.write(f"**Steps:** {len(self.steps)}\n\n")
209
+
210
+ # Steps section
211
+ f.write("## Test Steps\n\n")
212
+ for step in self.steps:
213
+ f.write(
214
+ f"### Step {step['number']}: {step['description']} ({step['timestamp']:.1f}s)\n\n"
215
+ )
216
+ f.write(f"![Screenshot](screenshots/{step['screenshot']})\n\n")
217
+
218
+ if step.get("assertion"):
219
+ status = "✓" if step.get("assertion_passed") else "✗"
220
+ f.write(f"**Assertion:** {step['assertion']} {status}\n\n")
221
+
222
+ if step.get("metadata"):
223
+ f.write("**Metadata:**\n")
224
+ for key, value in step["metadata"].items():
225
+ f.write(f"- {key}: {value}\n")
226
+ f.write("\n")
227
+
228
+ f.write(f"**Accessibility Elements:** {step['element_count']}\n\n")
229
+ f.write("---\n\n")
230
+
231
+ # Summary
232
+ f.write("## Summary\n\n")
233
+ f.write(f"- Total steps: {len(self.steps)}\n")
234
+ f.write(f"- Duration: {duration:.1f}s\n")
235
+ f.write(f"- Screenshots: {len(self.steps)}\n")
236
+ f.write(f"- Accessibility snapshots: {len(self.steps)}\n")
237
+
238
+ # Save metadata JSON
239
+ metadata_path = self.output_dir / "metadata.json"
240
+ with open(metadata_path, "w") as f:
241
+ json.dump(
242
+ {
243
+ "test_name": self.test_name,
244
+ "duration": duration,
245
+ "steps": self.steps,
246
+ "timestamp": datetime.now().isoformat(),
247
+ },
248
+ f,
249
+ indent=2,
250
+ )
251
+
252
+ # Token-efficient output
253
+ print(f"Report: {report_path}")
254
+
255
+ return {
256
+ "markdown_path": str(report_path),
257
+ "metadata_path": str(metadata_path),
258
+ "output_dir": str(self.output_dir),
259
+ }
260
+
261
+
262
+ def main():
263
+ """Main entry point for command-line usage."""
264
+ parser = argparse.ArgumentParser(
265
+ description="Record test execution with screenshots and documentation"
266
+ )
267
+ parser.add_argument("--test-name", required=True, help="Name of the test being recorded")
268
+ parser.add_argument(
269
+ "--output", default="test-artifacts", help="Output directory for test artifacts"
270
+ )
271
+ parser.add_argument(
272
+ "--udid",
273
+ help="Device UDID (auto-detects booted simulator if not provided)",
274
+ )
275
+ parser.add_argument(
276
+ "--inline",
277
+ action="store_true",
278
+ help="Return screenshots as base64 (inline mode for vision-based automation)",
279
+ )
280
+ parser.add_argument(
281
+ "--size",
282
+ choices=["full", "half", "quarter", "thumb"],
283
+ default="half",
284
+ help="Screenshot size for token optimization (default: half)",
285
+ )
286
+ parser.add_argument("--app-name", help="App name for semantic screenshot naming")
287
+
288
+ args = parser.parse_args()
289
+
290
+ # Resolve UDID with auto-detection
291
+ try:
292
+ udid = resolve_udid(args.udid)
293
+ except RuntimeError as e:
294
+ print(f"Error: {e}")
295
+ import sys
296
+
297
+ sys.exit(1)
298
+
299
+ # Create recorder
300
+ TestRecorder(
301
+ test_name=args.test_name,
302
+ output_dir=args.output,
303
+ udid=udid,
304
+ inline=args.inline,
305
+ screenshot_size=args.size,
306
+ app_name=args.app_name,
307
+ )
308
+
309
+ print("Test recorder initialized. Use the following methods:")
310
+ print(' recorder.step("description") - Record a test step')
311
+ print(" recorder.generate_report() - Generate final report")
312
+ print()
313
+ print("Example:")
314
+ print(' recorder.step("Launch app", screen_name="Splash")')
315
+ print(
316
+ ' recorder.step("Enter credentials", screen_name="Login", state="Empty", metadata={"user": "test"})'
317
+ )
318
+ print(' recorder.step("Verify login", assertion="Home screen visible")')
319
+ print(" recorder.generate_report()")
320
+
321
+
322
+ if __name__ == "__main__":
323
+ main()
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Visual Diff Tool for iOS Simulator Screenshots
4
+
5
+ Compares two screenshots pixel-by-pixel to detect visual changes.
6
+ Optimized for minimal token output.
7
+
8
+ Usage: python scripts/visual_diff.py baseline.png current.png [options]
9
+ """
10
+
11
+ import argparse
12
+ import json
13
+ import os
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ try:
18
+ from PIL import Image, ImageChops, ImageDraw
19
+ except ImportError:
20
+ print("Error: Pillow not installed. Run: pip3 install pillow")
21
+ sys.exit(1)
22
+
23
+
24
+ class VisualDiffer:
25
+ """Performs visual comparison between screenshots."""
26
+
27
+ def __init__(self, threshold: float = 0.01):
28
+ """
29
+ Initialize differ with threshold.
30
+
31
+ Args:
32
+ threshold: Maximum acceptable difference ratio (0.01 = 1%)
33
+ """
34
+ self.threshold = threshold
35
+
36
+ def compare(self, baseline_path: str, current_path: str) -> dict:
37
+ """
38
+ Compare two images and return difference metrics.
39
+
40
+ Args:
41
+ baseline_path: Path to baseline image
42
+ current_path: Path to current image
43
+
44
+ Returns:
45
+ Dictionary with comparison results
46
+ """
47
+ # Load images
48
+ try:
49
+ baseline = Image.open(baseline_path)
50
+ current = Image.open(current_path)
51
+ except FileNotFoundError as e:
52
+ print(f"Error: Image not found - {e}")
53
+ sys.exit(1)
54
+ except Exception as e:
55
+ print(f"Error: Failed to load image - {e}")
56
+ sys.exit(1)
57
+
58
+ # Verify dimensions match
59
+ if baseline.size != current.size:
60
+ return {
61
+ "error": "Image dimensions do not match",
62
+ "baseline_size": baseline.size,
63
+ "current_size": current.size,
64
+ }
65
+
66
+ # Convert to RGB if needed
67
+ if baseline.mode != "RGB":
68
+ baseline = baseline.convert("RGB")
69
+ if current.mode != "RGB":
70
+ current = current.convert("RGB")
71
+
72
+ # Calculate difference
73
+ diff = ImageChops.difference(baseline, current)
74
+
75
+ # Calculate metrics
76
+ total_pixels = baseline.size[0] * baseline.size[1]
77
+ diff_pixels = self._count_different_pixels(diff)
78
+ diff_percentage = (diff_pixels / total_pixels) * 100
79
+
80
+ # Determine pass/fail
81
+ passed = diff_percentage <= (self.threshold * 100)
82
+
83
+ return {
84
+ "dimensions": baseline.size,
85
+ "total_pixels": total_pixels,
86
+ "different_pixels": diff_pixels,
87
+ "difference_percentage": round(diff_percentage, 2),
88
+ "threshold_percentage": self.threshold * 100,
89
+ "passed": passed,
90
+ "verdict": "PASS" if passed else "FAIL",
91
+ }
92
+
93
+ def _count_different_pixels(self, diff_image: Image.Image) -> int:
94
+ """Count number of pixels that are different."""
95
+ # Convert to grayscale for easier processing
96
+ diff_gray = diff_image.convert("L")
97
+
98
+ # Count non-zero pixels (different)
99
+ pixels = diff_gray.getdata()
100
+ return sum(1 for pixel in pixels if pixel > 10) # Threshold for noise
101
+
102
+ def generate_diff_image(self, baseline_path: str, current_path: str, output_path: str) -> None:
103
+ """Generate highlighted difference image."""
104
+ baseline = Image.open(baseline_path).convert("RGB")
105
+ current = Image.open(current_path).convert("RGB")
106
+
107
+ # Create difference image
108
+ diff = ImageChops.difference(baseline, current)
109
+
110
+ # Enhance differences with red overlay
111
+ diff_enhanced = Image.new("RGB", baseline.size)
112
+ for x in range(baseline.size[0]):
113
+ for y in range(baseline.size[1]):
114
+ diff_pixel = diff.getpixel((x, y))
115
+ if sum(diff_pixel) > 30: # Threshold for visibility
116
+ # Highlight in red
117
+ diff_enhanced.putpixel((x, y), (255, 0, 0))
118
+ else:
119
+ # Keep original
120
+ diff_enhanced.putpixel((x, y), current.getpixel((x, y)))
121
+
122
+ diff_enhanced.save(output_path)
123
+
124
+ def generate_side_by_side(
125
+ self, baseline_path: str, current_path: str, output_path: str
126
+ ) -> None:
127
+ """Generate side-by-side comparison image."""
128
+ baseline = Image.open(baseline_path)
129
+ current = Image.open(current_path)
130
+
131
+ # Create combined image
132
+ width = baseline.size[0] * 2 + 10 # 10px separator
133
+ height = max(baseline.size[1], current.size[1])
134
+ combined = Image.new("RGB", (width, height), color=(128, 128, 128))
135
+
136
+ # Paste images
137
+ combined.paste(baseline, (0, 0))
138
+ combined.paste(current, (baseline.size[0] + 10, 0))
139
+
140
+ combined.save(output_path)
141
+
142
+
143
+ def main():
144
+ """Main entry point."""
145
+ parser = argparse.ArgumentParser(description="Compare screenshots for visual differences")
146
+ parser.add_argument("baseline", help="Path to baseline screenshot")
147
+ parser.add_argument("current", help="Path to current screenshot")
148
+ parser.add_argument(
149
+ "--output",
150
+ default=".",
151
+ help="Output directory for diff artifacts (default: current directory)",
152
+ )
153
+ parser.add_argument(
154
+ "--threshold",
155
+ type=float,
156
+ default=0.01,
157
+ help="Acceptable difference threshold (0.01 = 1%%, default: 0.01)",
158
+ )
159
+ parser.add_argument(
160
+ "--details", action="store_true", help="Show detailed output (increases tokens)"
161
+ )
162
+
163
+ args = parser.parse_args()
164
+
165
+ # Create output directory if needed
166
+ output_dir = Path(args.output)
167
+ output_dir.mkdir(parents=True, exist_ok=True)
168
+
169
+ # Initialize differ
170
+ differ = VisualDiffer(threshold=args.threshold)
171
+
172
+ # Perform comparison
173
+ result = differ.compare(args.baseline, args.current)
174
+
175
+ # Handle dimension mismatch
176
+ if "error" in result:
177
+ print(f"Error: {result['error']}")
178
+ print(f"Baseline: {result['baseline_size']}")
179
+ print(f"Current: {result['current_size']}")
180
+ sys.exit(1)
181
+
182
+ # Generate artifacts
183
+ diff_image_path = output_dir / "diff.png"
184
+ comparison_image_path = output_dir / "side-by-side.png"
185
+
186
+ try:
187
+ differ.generate_diff_image(args.baseline, args.current, str(diff_image_path))
188
+ differ.generate_side_by_side(args.baseline, args.current, str(comparison_image_path))
189
+ except Exception as e:
190
+ print(f"Warning: Could not generate images - {e}")
191
+
192
+ # Output results (token-optimized)
193
+ if args.details:
194
+ # Detailed output
195
+ report = {
196
+ "summary": {
197
+ "baseline": args.baseline,
198
+ "current": args.current,
199
+ "threshold": args.threshold,
200
+ "passed": result["passed"],
201
+ },
202
+ "results": result,
203
+ "artifacts": {
204
+ "diff_image": str(diff_image_path),
205
+ "comparison_image": str(comparison_image_path),
206
+ },
207
+ }
208
+ print(json.dumps(report, indent=2))
209
+ else:
210
+ # Minimal output (default)
211
+ print(f"Difference: {result['difference_percentage']}% ({result['verdict']})")
212
+ if result["different_pixels"] > 0:
213
+ print(f"Changed pixels: {result['different_pixels']:,}")
214
+ print(f"Artifacts saved to: {output_dir}/")
215
+
216
+ # Save JSON report
217
+ report_path = output_dir / "diff-report.json"
218
+ with open(report_path, "w") as f:
219
+ json.dump(
220
+ {
221
+ "baseline": os.path.basename(args.baseline),
222
+ "current": os.path.basename(args.current),
223
+ "results": result,
224
+ "artifacts": {"diff": "diff.png", "comparison": "side-by-side.png"},
225
+ },
226
+ f,
227
+ indent=2,
228
+ )
229
+
230
+ # Exit with error if test failed
231
+ sys.exit(0 if result["passed"] else 1)
232
+
233
+
234
+ if __name__ == "__main__":
235
+ main()
@@ -0,0 +1,13 @@
1
+ """
2
+ Xcode build automation module.
3
+
4
+ Provides structured, modular access to xcodebuild and xcresult functionality.
5
+ """
6
+
7
+ from .builder import BuildRunner
8
+ from .cache import XCResultCache
9
+ from .config import Config
10
+ from .reporter import OutputFormatter
11
+ from .xcresult import XCResultParser
12
+
13
+ __all__ = ["BuildRunner", "Config", "OutputFormatter", "XCResultCache", "XCResultParser"]