claudecode-omc 5.6.6 → 5.6.8

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 (179) hide show
  1. package/.local/skills/THIRD_PARTY_LICENSES/AvdLee-SwiftUI-Agent-Skill.LICENSE +21 -0
  2. package/.local/skills/THIRD_PARTY_LICENSES/Dimillian-Skills.LICENSE +21 -0
  3. package/.local/skills/THIRD_PARTY_LICENSES/README.md +36 -0
  4. package/.local/skills/THIRD_PARTY_LICENSES/twostraws-swiftui-agent-skill.LICENSE +21 -0
  5. package/.local/skills/h5-to-swiftui/SKILL.md +201 -0
  6. package/.local/skills/h5-to-swiftui/assets/calibration/README.md +176 -0
  7. package/.local/skills/h5-to-swiftui/assets/calibration/h5-twin/index.html +52 -0
  8. package/.local/skills/h5-to-swiftui/assets/calibration/h5-twin/style.css +133 -0
  9. package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin/Package.swift +26 -0
  10. package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin/Sources/CalibrationScreen/CalibrationScreen.swift +142 -0
  11. package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin-divergent/Package.swift +32 -0
  12. package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin-divergent/Sources/CalibrationScreenDivergent/CalibrationScreenDivergent.swift +122 -0
  13. package/.local/skills/h5-to-swiftui/assets/calibration/tokens.json +42 -0
  14. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/index.html +14 -0
  15. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/package.json +20 -0
  16. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/public/api/articles/001.json +96 -0
  17. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/public/api/articles/index.json +89 -0
  18. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/App.jsx +22 -0
  19. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/App.module.css +11 -0
  20. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/ArticleCard.jsx +53 -0
  21. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/ArticleCard.module.css +139 -0
  22. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/NavBar.jsx +37 -0
  23. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/NavBar.module.css +72 -0
  24. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TagCloud.jsx +30 -0
  25. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TagCloud.module.css +50 -0
  26. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TrendChart.jsx +159 -0
  27. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TrendChart.module.css +21 -0
  28. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/main.jsx +12 -0
  29. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/ArticleScreen.jsx +182 -0
  30. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/ArticleScreen.module.css +294 -0
  31. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/FeedScreen.jsx +147 -0
  32. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/FeedScreen.module.css +161 -0
  33. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/styles/global.css +50 -0
  34. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/styles/tokens.css +103 -0
  35. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/vite.config.js +6 -0
  36. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/data/tasks.js +67 -0
  37. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/index.html +26 -0
  38. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/router.js +73 -0
  39. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/detail.js +164 -0
  40. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/home.js +53 -0
  41. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/list.js +87 -0
  42. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/styles/app.css +342 -0
  43. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/styles/tokens.css +68 -0
  44. package/.local/skills/h5-to-swiftui/references/css-to-swiftui-map.md +205 -0
  45. package/.local/skills/h5-to-swiftui/references/design-token-extraction.md +209 -0
  46. package/.local/skills/h5-to-swiftui/references/high-risk-triage.md +209 -0
  47. package/.local/skills/h5-to-swiftui/references/render-equivalence-calibration.md +193 -0
  48. package/.local/skills/h5-to-swiftui/references/stack-detection.md +160 -0
  49. package/.local/skills/h5-to-swiftui/references/visual-diff-loop-protocol.md +365 -0
  50. package/.local/skills/h5-to-swiftui/scripts/_calib-consts.mjs +150 -0
  51. package/.local/skills/h5-to-swiftui/scripts/_imglib.mjs +547 -0
  52. package/.local/skills/h5-to-swiftui/scripts/_provenance.mjs +123 -0
  53. package/.local/skills/h5-to-swiftui/scripts/calibrate-render.mjs +625 -0
  54. package/.local/skills/h5-to-swiftui/scripts/capture-reference.mjs +386 -0
  55. package/.local/skills/h5-to-swiftui/scripts/detect-stack.mjs +305 -0
  56. package/.local/skills/h5-to-swiftui/scripts/evaluate-convergence.mjs +1093 -0
  57. package/.local/skills/h5-to-swiftui/scripts/extract-tokens.mjs +600 -0
  58. package/.local/skills/h5-to-swiftui/scripts/mark-overlay.mjs +379 -0
  59. package/.local/skills/h5-to-swiftui/scripts/pixel-diff.mjs +530 -0
  60. package/.local/skills/h5-to-swiftui/scripts/sim-screenshot.sh +544 -0
  61. package/.local/skills/ios-debugger-agent/SKILL.md +51 -0
  62. package/.local/skills/ios-debugger-agent/agents/openai.yaml +4 -0
  63. package/.local/skills/swift-concurrency-expert/SKILL.md +105 -0
  64. package/.local/skills/swift-concurrency-expert/agents/openai.yaml +4 -0
  65. package/.local/skills/swift-concurrency-expert/references/approachable-concurrency.md +63 -0
  66. package/.local/skills/swift-concurrency-expert/references/swift-6-2-concurrency.md +272 -0
  67. package/.local/skills/swift-concurrency-expert/references/swiftui-concurrency-tour-wwdc.md +33 -0
  68. package/.local/skills/swiftui-expert-skill/SKILL.md +162 -0
  69. package/.local/skills/swiftui-expert-skill/references/accessibility-patterns.md +215 -0
  70. package/.local/skills/swiftui-expert-skill/references/animation-advanced.md +403 -0
  71. package/.local/skills/swiftui-expert-skill/references/animation-basics.md +284 -0
  72. package/.local/skills/swiftui-expert-skill/references/animation-transitions.md +326 -0
  73. package/.local/skills/swiftui-expert-skill/references/charts-accessibility.md +135 -0
  74. package/.local/skills/swiftui-expert-skill/references/charts.md +602 -0
  75. package/.local/skills/swiftui-expert-skill/references/focus-patterns.md +299 -0
  76. package/.local/skills/swiftui-expert-skill/references/image-optimization.md +203 -0
  77. package/.local/skills/swiftui-expert-skill/references/latest-apis.md +488 -0
  78. package/.local/skills/swiftui-expert-skill/references/layout-best-practices.md +266 -0
  79. package/.local/skills/swiftui-expert-skill/references/liquid-glass.md +423 -0
  80. package/.local/skills/swiftui-expert-skill/references/list-patterns.md +446 -0
  81. package/.local/skills/swiftui-expert-skill/references/macos-scenes.md +318 -0
  82. package/.local/skills/swiftui-expert-skill/references/macos-views.md +357 -0
  83. package/.local/skills/swiftui-expert-skill/references/macos-window-styling.md +303 -0
  84. package/.local/skills/swiftui-expert-skill/references/performance-patterns.md +403 -0
  85. package/.local/skills/swiftui-expert-skill/references/scroll-patterns.md +293 -0
  86. package/.local/skills/swiftui-expert-skill/references/sheet-navigation-patterns.md +363 -0
  87. package/.local/skills/swiftui-expert-skill/references/state-management.md +388 -0
  88. package/.local/skills/swiftui-expert-skill/references/text-patterns.md +32 -0
  89. package/.local/skills/swiftui-expert-skill/references/trace-analysis.md +295 -0
  90. package/.local/skills/swiftui-expert-skill/references/trace-recording.md +134 -0
  91. package/.local/skills/swiftui-expert-skill/references/view-structure.md +780 -0
  92. package/.local/skills/swiftui-expert-skill/scripts/__pycache__/analyze_trace.cpython-313.pyc +0 -0
  93. package/.local/skills/swiftui-expert-skill/scripts/__pycache__/record_trace.cpython-313.pyc +0 -0
  94. package/.local/skills/swiftui-expert-skill/scripts/analyze_trace.py +301 -0
  95. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__init__.py +1 -0
  96. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/__init__.cpython-313.pyc +0 -0
  97. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/causes.cpython-313.pyc +0 -0
  98. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/correlate.cpython-313.pyc +0 -0
  99. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/events.cpython-313.pyc +0 -0
  100. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/hangs.cpython-313.pyc +0 -0
  101. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/hitches.cpython-313.pyc +0 -0
  102. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/summary.cpython-313.pyc +0 -0
  103. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/swiftui.cpython-313.pyc +0 -0
  104. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/time_profiler.cpython-313.pyc +0 -0
  105. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/xctrace.cpython-313.pyc +0 -0
  106. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/xml_utils.cpython-313.pyc +0 -0
  107. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/causes.py +187 -0
  108. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/correlate.py +179 -0
  109. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/events.py +291 -0
  110. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/hangs.py +108 -0
  111. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/hitches.py +145 -0
  112. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/summary.py +243 -0
  113. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/swiftui.py +195 -0
  114. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/time_profiler.py +135 -0
  115. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/xctrace.py +117 -0
  116. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/xml_utils.py +224 -0
  117. package/.local/skills/swiftui-expert-skill/scripts/record_trace.py +252 -0
  118. package/.local/skills/swiftui-liquid-glass/SKILL.md +90 -0
  119. package/.local/skills/swiftui-liquid-glass/agents/openai.yaml +4 -0
  120. package/.local/skills/swiftui-liquid-glass/references/liquid-glass.md +280 -0
  121. package/.local/skills/swiftui-performance-audit/SKILL.md +106 -0
  122. package/.local/skills/swiftui-performance-audit/agents/openai.yaml +4 -0
  123. package/.local/skills/swiftui-performance-audit/references/code-smells.md +150 -0
  124. package/.local/skills/swiftui-performance-audit/references/demystify-swiftui-performance-wwdc23.md +46 -0
  125. package/.local/skills/swiftui-performance-audit/references/optimizing-swiftui-performance-instruments.md +29 -0
  126. package/.local/skills/swiftui-performance-audit/references/profiling-intake.md +44 -0
  127. package/.local/skills/swiftui-performance-audit/references/report-template.md +47 -0
  128. package/.local/skills/swiftui-performance-audit/references/understanding-hangs-in-your-app.md +33 -0
  129. package/.local/skills/swiftui-performance-audit/references/understanding-improving-swiftui-performance.md +52 -0
  130. package/.local/skills/swiftui-pro/SKILL.md +108 -0
  131. package/.local/skills/swiftui-pro/agents/openai.yaml +10 -0
  132. package/.local/skills/swiftui-pro/assets/swiftui-pro-icon.png +0 -0
  133. package/.local/skills/swiftui-pro/assets/swiftui-pro-icon.svg +29 -0
  134. package/.local/skills/swiftui-pro/references/accessibility.md +13 -0
  135. package/.local/skills/swiftui-pro/references/api.md +39 -0
  136. package/.local/skills/swiftui-pro/references/data.md +43 -0
  137. package/.local/skills/swiftui-pro/references/design.md +32 -0
  138. package/.local/skills/swiftui-pro/references/hygiene.md +9 -0
  139. package/.local/skills/swiftui-pro/references/navigation.md +14 -0
  140. package/.local/skills/swiftui-pro/references/performance.md +46 -0
  141. package/.local/skills/swiftui-pro/references/swift.md +56 -0
  142. package/.local/skills/swiftui-pro/references/views.md +36 -0
  143. package/.local/skills/swiftui-ui-patterns/SKILL.md +95 -0
  144. package/.local/skills/swiftui-ui-patterns/agents/openai.yaml +4 -0
  145. package/.local/skills/swiftui-ui-patterns/references/app-wiring.md +201 -0
  146. package/.local/skills/swiftui-ui-patterns/references/async-state.md +96 -0
  147. package/.local/skills/swiftui-ui-patterns/references/components-index.md +50 -0
  148. package/.local/skills/swiftui-ui-patterns/references/controls.md +57 -0
  149. package/.local/skills/swiftui-ui-patterns/references/deeplinks.md +66 -0
  150. package/.local/skills/swiftui-ui-patterns/references/focus.md +90 -0
  151. package/.local/skills/swiftui-ui-patterns/references/form.md +97 -0
  152. package/.local/skills/swiftui-ui-patterns/references/grids.md +71 -0
  153. package/.local/skills/swiftui-ui-patterns/references/haptics.md +71 -0
  154. package/.local/skills/swiftui-ui-patterns/references/input-toolbar.md +51 -0
  155. package/.local/skills/swiftui-ui-patterns/references/lightweight-clients.md +93 -0
  156. package/.local/skills/swiftui-ui-patterns/references/list.md +86 -0
  157. package/.local/skills/swiftui-ui-patterns/references/loading-placeholders.md +38 -0
  158. package/.local/skills/swiftui-ui-patterns/references/macos-settings.md +71 -0
  159. package/.local/skills/swiftui-ui-patterns/references/matched-transitions.md +59 -0
  160. package/.local/skills/swiftui-ui-patterns/references/media.md +73 -0
  161. package/.local/skills/swiftui-ui-patterns/references/menu-bar.md +101 -0
  162. package/.local/skills/swiftui-ui-patterns/references/navigationstack.md +159 -0
  163. package/.local/skills/swiftui-ui-patterns/references/overlay.md +45 -0
  164. package/.local/skills/swiftui-ui-patterns/references/performance.md +62 -0
  165. package/.local/skills/swiftui-ui-patterns/references/previews.md +48 -0
  166. package/.local/skills/swiftui-ui-patterns/references/scroll-reveal.md +133 -0
  167. package/.local/skills/swiftui-ui-patterns/references/scrollview.md +87 -0
  168. package/.local/skills/swiftui-ui-patterns/references/searchable.md +71 -0
  169. package/.local/skills/swiftui-ui-patterns/references/sheets.md +155 -0
  170. package/.local/skills/swiftui-ui-patterns/references/split-views.md +72 -0
  171. package/.local/skills/swiftui-ui-patterns/references/tabview.md +114 -0
  172. package/.local/skills/swiftui-ui-patterns/references/theming.md +71 -0
  173. package/.local/skills/swiftui-ui-patterns/references/title-menus.md +93 -0
  174. package/.local/skills/swiftui-ui-patterns/references/top-bar.md +49 -0
  175. package/.local/skills/swiftui-view-refactor/SKILL.md +202 -0
  176. package/.local/skills/swiftui-view-refactor/agents/openai.yaml +4 -0
  177. package/.local/skills/swiftui-view-refactor/references/mv-patterns.md +161 -0
  178. package/bundled/manifest.json +1 -1
  179. package/package.json +1 -1
@@ -0,0 +1,386 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * capture-reference.mjs — Stage 2: Playwright reference screenshot capture
4
+ *
5
+ * Usage:
6
+ * node capture-reference.mjs <h5-url-or-build> --device "iPhone 15 Pro" --out <dir>
7
+ * [--screen <name>] [--mask x,y,w,h] [--settle-ms <ms>]
8
+ *
9
+ * Outputs under <dir>/reference/:
10
+ * <screen>/full.png — full-page screenshot (animations frozen)
11
+ * <screen>/<component>.png — per-landmark crops by getBoundingClientRect()
12
+ * manifest.json — schema:"h5-to-swiftui/reference@1", device, logical_size,
13
+ * screens:[{name,components:[{name,bbox_css_px,selector}]}], masks
14
+ *
15
+ * Exit codes:
16
+ * 0 — screenshots written, manifest.json produced
17
+ * 1 — fatal error (bad args, navigation failed)
18
+ * 2 — playwright not installed (actionable hint printed)
19
+ *
20
+ * Examples:
21
+ * node capture-reference.mjs http://localhost:5173 --device "iPhone 15 Pro" --out ./artifacts
22
+ * node capture-reference.mjs http://localhost:5173 --device "iPhone 15 Pro" --out ./artifacts --screen home --mask 0,0,393,50
23
+ * node capture-reference.mjs http://localhost:5173 --device "iPhone 15 Pro" --out ./artifacts --settle-ms 1500
24
+ */
25
+
26
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
27
+ import { resolve, join } from 'node:path';
28
+
29
+ // ── Device profiles (logical px, deviceScaleFactor) ──────────────────────────
30
+
31
+ const DEVICES = {
32
+ 'iPhone 15 Pro': { width: 393, height: 852, deviceScaleFactor: 3, userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1' },
33
+ 'iPhone 14 Pro': { width: 393, height: 852, deviceScaleFactor: 3, userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1' },
34
+ 'iPhone 15': { width: 390, height: 844, deviceScaleFactor: 3, userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1' },
35
+ 'iPhone SE': { width: 375, height: 667, deviceScaleFactor: 2, userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1' },
36
+ };
37
+
38
+ // Landmark selectors for per-component crops
39
+ const LANDMARK_SELECTORS = [
40
+ { name: 'nav', selector: 'nav, [role="navigation"]' },
41
+ { name: 'header', selector: 'header, [role="banner"]' },
42
+ { name: 'main', selector: 'main, [role="main"]' },
43
+ { name: 'footer', selector: 'footer, [role="contentinfo"]' },
44
+ { name: 'hero', selector: '[class*="hero"], [class*="banner"], [class*="jumbotron"]' },
45
+ { name: 'cta', selector: '[class*="cta"], [class*="call-to-action"]' },
46
+ { name: 'card', selector: '[class*="card"]:first-of-type' },
47
+ { name: 'list', selector: 'ul:first-of-type, ol:first-of-type, [class*="list"]:first-of-type' },
48
+ { name: 'form', selector: 'form:first-of-type' },
49
+ { name: 'button', selector: 'button:first-of-type, [class*="btn"]:first-of-type' },
50
+ ];
51
+
52
+ // ── CLI parsing ───────────────────────────────────────────────────────────────
53
+
54
+ const args = process.argv.slice(2);
55
+
56
+ if (args.includes('--help') || args.includes('-h')) {
57
+ console.log(`capture-reference.mjs — Stage 2 Playwright reference capture
58
+
59
+ Usage:
60
+ node capture-reference.mjs <h5-url-or-build> --device "iPhone 15 Pro" --out <dir>
61
+ [--screen <name>] [--mask x,y,w,h] [--settle-ms <ms>]
62
+
63
+ Arguments:
64
+ <h5-url-or-build> Running URL (http://...) or build directory
65
+ --device <name> Device profile (required)
66
+ --out <dir> Output directory (required)
67
+ --screen <name> Screen name for the manifest (default: "main")
68
+ --mask x,y,w,h Mask rectangle in CSS px — repeatable
69
+ --settle-ms <ms> Additional settle delay after networkidle (default: 300)
70
+
71
+ Supported devices:
72
+ "iPhone 15 Pro" 393×852 @3x (default)
73
+ "iPhone 14 Pro" 393×852 @3x
74
+ "iPhone 15" 390×844 @3x
75
+ "iPhone SE" 375×667 @2x
76
+
77
+ Prerequisites:
78
+ playwright must be installed:
79
+ npm install --save-dev playwright && npx playwright install chromium
80
+
81
+ Outputs:
82
+ <out>/reference/<screen>/full.png Full-page screenshot
83
+ <out>/reference/<screen>/<component>.png Per-landmark crops
84
+ <out>/reference/manifest.json Capture manifest
85
+
86
+ Exit codes:
87
+ 0 Success
88
+ 1 Fatal error (bad args / navigation failed)
89
+ 2 playwright not installed
90
+
91
+ Examples:
92
+ node capture-reference.mjs http://localhost:5173 --device "iPhone 15 Pro" --out ./artifacts
93
+ node capture-reference.mjs http://localhost:5173 --device "iPhone 15 Pro" --out ./artifacts \\
94
+ --screen home --mask 0,0,393,50 --settle-ms 1000
95
+ `);
96
+ process.exit(0);
97
+ }
98
+
99
+ // Parse positional: first non-flag arg
100
+ let h5Target = null;
101
+ for (const a of args) {
102
+ if (!a.startsWith('--')) { h5Target = a; break; }
103
+ }
104
+
105
+ if (!h5Target) {
106
+ console.error('Error: <h5-url-or-build> is required.\nRun with --help for usage.');
107
+ process.exit(1);
108
+ }
109
+
110
+ function getFlag(flag, fallback = null) {
111
+ const idx = args.indexOf(flag);
112
+ if (idx === -1) return fallback;
113
+ const val = args[idx + 1];
114
+ if (!val || val.startsWith('--')) {
115
+ console.error(`Error: ${flag} requires an argument.`);
116
+ process.exit(1);
117
+ }
118
+ return val;
119
+ }
120
+
121
+ function getAllFlags(flag) {
122
+ const results = [];
123
+ for (let i = 0; i < args.length; i++) {
124
+ if (args[i] === flag && args[i + 1] && !args[i + 1].startsWith('--')) {
125
+ results.push(args[i + 1]);
126
+ }
127
+ }
128
+ return results;
129
+ }
130
+
131
+ const deviceName = getFlag('--device', 'iPhone 15 Pro');
132
+ const outDirRaw = getFlag('--out');
133
+ const screenName = getFlag('--screen', 'main');
134
+ const settleMsRaw = getFlag('--settle-ms', '300');
135
+ const maskStrings = getAllFlags('--mask');
136
+
137
+ if (!outDirRaw) {
138
+ console.error('Error: --out <dir> is required.\nRun with --help for usage.');
139
+ process.exit(1);
140
+ }
141
+
142
+ const outDir = resolve(outDirRaw);
143
+ const settleMs = parseInt(settleMsRaw, 10);
144
+
145
+ if (isNaN(settleMs) || settleMs < 0) {
146
+ console.error(`Error: --settle-ms must be a non-negative integer, got: ${settleMsRaw}`);
147
+ process.exit(1);
148
+ }
149
+
150
+ const device = DEVICES[deviceName];
151
+ if (!device) {
152
+ console.error(`Error: unknown device "${deviceName}".\nSupported: ${Object.keys(DEVICES).map(d => `"${d}"`).join(', ')}`);
153
+ process.exit(1);
154
+ }
155
+
156
+ // Parse mask strings: "x,y,w,h"
157
+ const masks = [];
158
+ for (const ms of maskStrings) {
159
+ const parts = ms.split(',').map(p => parseFloat(p.trim()));
160
+ if (parts.length !== 4 || parts.some(isNaN)) {
161
+ console.error(`Error: --mask must be "x,y,w,h" (CSS px), got: "${ms}"`);
162
+ process.exit(1);
163
+ }
164
+ masks.push({ x: parts[0], y: parts[1], w: parts[2], h: parts[3] });
165
+ }
166
+
167
+ // Resolve URL: if it looks like a path, serve via file URL or note static serve needed
168
+ let targetUrl = h5Target;
169
+ if (!h5Target.startsWith('http://') && !h5Target.startsWith('https://')) {
170
+ const absPath = resolve(h5Target);
171
+ if (!existsSync(absPath)) {
172
+ console.error(`Error: path "${h5Target}" does not exist. Provide a running URL or an existing build directory.`);
173
+ process.exit(1);
174
+ }
175
+ // For a directory, construct a file URL to index.html — may not work for all apps
176
+ const indexHtml = join(absPath, 'index.html');
177
+ if (!existsSync(indexHtml)) {
178
+ console.error(`Error: no index.html found in "${absPath}". Start a dev server and pass its URL instead.`);
179
+ process.exit(1);
180
+ }
181
+ targetUrl = `file://${indexHtml}`;
182
+ console.warn(`Warning: using file:// URL. Some apps may require a running dev server for correct rendering.`);
183
+ }
184
+
185
+ // ── Playwright check ──────────────────────────────────────────────────────────
186
+
187
+ let playwright;
188
+ try {
189
+ playwright = await import('playwright');
190
+ } catch {
191
+ console.error(
192
+ '\nError: playwright is not installed. capture-reference.mjs requires it.\n\n' +
193
+ 'Install with:\n' +
194
+ ' npm install --save-dev playwright && npx playwright install chromium\n\n' +
195
+ 'Then re-run this script.'
196
+ );
197
+ process.exit(2);
198
+ }
199
+
200
+ // ── Setup output dirs ─────────────────────────────────────────────────────────
201
+
202
+ const refDir = join(outDir, 'reference');
203
+ const screenDir = join(refDir, screenName);
204
+ mkdirSync(screenDir, { recursive: true });
205
+
206
+ // ── Browser launch + capture ──────────────────────────────────────────────────
207
+
208
+ console.log(`Device: ${deviceName} (${device.width}×${device.height} @${device.deviceScaleFactor}x)`);
209
+ console.log(`URL: ${targetUrl}`);
210
+ console.log(`Output: ${screenDir}`);
211
+ console.log(`Masks: ${masks.length} region(s)`);
212
+ console.log(`Settle: ${settleMs}ms`);
213
+
214
+ const { chromium } = playwright;
215
+
216
+ const browser = await chromium.launch();
217
+ const context = await browser.newContext({
218
+ viewport: { width: device.width, height: device.height },
219
+ deviceScaleFactor: device.deviceScaleFactor,
220
+ userAgent: device.userAgent,
221
+ colorScheme: 'light',
222
+ });
223
+
224
+ const page = await context.newPage();
225
+
226
+ // ── Freeze animations per render-equivalence-calibration.md ──────────────────
227
+
228
+ await page.addInitScript(() => {
229
+ const style = document.createElement('style');
230
+ style.textContent = `
231
+ *, *::before, *::after {
232
+ animation: none !important;
233
+ transition: none !important;
234
+ caret-color: transparent !important;
235
+ }
236
+ `;
237
+ document.head.appendChild(style);
238
+ });
239
+
240
+ // Navigate
241
+ console.log(`\nNavigating...`);
242
+ try {
243
+ await page.goto(targetUrl, { waitUntil: 'networkidle', timeout: 60_000 });
244
+ } catch (e) {
245
+ await browser.close();
246
+ console.error(`Error: navigation failed: ${e.message}`);
247
+ process.exit(1);
248
+ }
249
+
250
+ // Wait for fonts (render-equivalence-calibration.md: webfont/async settle)
251
+ try {
252
+ await page.waitForFunction(() => document.fonts.ready, { timeout: 10_000 });
253
+ } catch {
254
+ console.warn('Warning: fonts.ready timed out — continuing');
255
+ }
256
+
257
+ // Additional settle
258
+ if (settleMs > 0) {
259
+ await new Promise(r => setTimeout(r, settleMs));
260
+ }
261
+
262
+ // Inject animation freeze again after page load (catches dynamically added styles)
263
+ await page.addStyleTag({
264
+ content: `*, *::before, *::after { animation: none !important; transition: none !important; caret-color: transparent !important; }`,
265
+ });
266
+
267
+ // ── Apply masks (draw opaque rects over sensitive regions) ───────────────────
268
+
269
+ if (masks.length > 0) {
270
+ await page.evaluate((maskList) => {
271
+ for (const m of maskList) {
272
+ const div = document.createElement('div');
273
+ Object.assign(div.style, {
274
+ position: 'fixed',
275
+ left: `${m.x}px`,
276
+ top: `${m.y}px`,
277
+ width: `${m.w}px`,
278
+ height: `${m.h}px`,
279
+ background: '#000',
280
+ zIndex: '2147483647',
281
+ pointerEvents: 'none',
282
+ });
283
+ document.body.appendChild(div);
284
+ }
285
+ }, masks);
286
+ }
287
+
288
+ // ── Full-page screenshot ──────────────────────────────────────────────────────
289
+
290
+ const fullPath = join(screenDir, 'full.png');
291
+ await page.screenshot({ path: fullPath, fullPage: true });
292
+ console.log(`\nFull screenshot: ${fullPath}`);
293
+
294
+ // ── Per-component crops ───────────────────────────────────────────────────────
295
+
296
+ const components = [];
297
+
298
+ for (const { name: compName, selector } of LANDMARK_SELECTORS) {
299
+ let bbox = null;
300
+ try {
301
+ bbox = await page.evaluate((sel) => {
302
+ const el = document.querySelector(sel);
303
+ if (!el) return null;
304
+ const rect = el.getBoundingClientRect();
305
+ return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
306
+ }, selector);
307
+ } catch {
308
+ continue;
309
+ }
310
+
311
+ if (!bbox || bbox.width <= 0 || bbox.height <= 0) continue;
312
+
313
+ // Clip must be within the logical viewport
314
+ const clip = {
315
+ x: Math.max(0, Math.floor(bbox.x)),
316
+ y: Math.max(0, Math.floor(bbox.y)),
317
+ width: Math.min(device.width - Math.max(0, Math.floor(bbox.x)), Math.ceil(bbox.width)),
318
+ height: Math.min(device.height - Math.max(0, Math.floor(bbox.y)), Math.ceil(bbox.height)),
319
+ };
320
+
321
+ if (clip.width <= 0 || clip.height <= 0) continue;
322
+
323
+ const compPath = join(screenDir, `${compName}.png`);
324
+ try {
325
+ await page.screenshot({ path: compPath, clip });
326
+ console.log(` Component "${compName}": bbox=${JSON.stringify(bbox)} → ${compPath}`);
327
+ components.push({
328
+ name: compName,
329
+ bbox_css_px: { x: bbox.x, y: bbox.y, width: bbox.width, height: bbox.height },
330
+ selector,
331
+ file: `${screenName}/${compName}.png`,
332
+ });
333
+ } catch (e) {
334
+ console.warn(` Warning: could not crop "${compName}": ${e.message}`);
335
+ }
336
+ }
337
+
338
+ await browser.close();
339
+
340
+ // ── Manifest ──────────────────────────────────────────────────────────────────
341
+
342
+ const manifestPath = join(refDir, 'manifest.json');
343
+
344
+ // Load existing manifest if present (multiple screen runs)
345
+ let manifest;
346
+ try {
347
+ manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
348
+ } catch {
349
+ manifest = {
350
+ schema: 'h5-to-swiftui/reference@1',
351
+ device: deviceName,
352
+ logical_size: [device.width, device.height],
353
+ device_scale_factor: device.deviceScaleFactor,
354
+ screens: [],
355
+ masks: [],
356
+ };
357
+ }
358
+
359
+ // Update masks list (union)
360
+ for (const m of masks) {
361
+ const dup = manifest.masks.some(e => e.x === m.x && e.y === m.y && e.w === m.w && e.h === m.h);
362
+ if (!dup) manifest.masks.push(m);
363
+ }
364
+
365
+ // Update or append screen entry
366
+ const existingIdx = manifest.screens.findIndex(s => s.name === screenName);
367
+ const screenEntry = {
368
+ name: screenName,
369
+ full_screenshot: `${screenName}/full.png`,
370
+ components,
371
+ captured_at: new Date().toISOString(),
372
+ settle_ms: settleMs,
373
+ };
374
+
375
+ if (existingIdx !== -1) {
376
+ manifest.screens[existingIdx] = screenEntry;
377
+ } else {
378
+ manifest.screens.push(screenEntry);
379
+ }
380
+
381
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
382
+
383
+ console.log(`\nManifest: ${manifestPath}`);
384
+ console.log(` Screens: ${manifest.screens.length}, components this run: ${components.length}`);
385
+ console.log('\nCapture complete.');
386
+ process.exit(0);
@@ -0,0 +1,305 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * detect-stack.mjs — Stage 0: H5 stack detection + v1 scope gate
4
+ *
5
+ * Usage:
6
+ * node detect-stack.mjs <h5-src> [--out <dir>]
7
+ *
8
+ * Outputs:
9
+ * <h5-src>/stack-report.json (or --out dir)
10
+ *
11
+ * Exit codes:
12
+ * 0 — report written (check in_v1_scope for gate result)
13
+ * 1 — fatal error (bad args, unreadable source)
14
+ *
15
+ * Examples:
16
+ * node detect-stack.mjs ./my-app
17
+ * node detect-stack.mjs ./my-app --out ./artifacts
18
+ */
19
+
20
+ import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'node:fs';
21
+ import { resolve, join } from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+
24
+ // ── CLI ──────────────────────────────────────────────────────────────────────
25
+
26
+ const args = process.argv.slice(2);
27
+
28
+ if (args.includes('--help') || args.includes('-h')) {
29
+ console.log(`detect-stack.mjs — H5 stack detection and v1 scope gate
30
+
31
+ Usage:
32
+ node detect-stack.mjs <h5-src> [--out <dir>]
33
+
34
+ Arguments:
35
+ <h5-src> Path to the H5 project root (required)
36
+ --out <dir> Directory to write stack-report.json (default: <h5-src>)
37
+
38
+ Exit codes:
39
+ 0 Report written (check in_v1_scope field for gate result)
40
+ 1 Fatal error
41
+
42
+ Output schema (stack-report.json):
43
+ {
44
+ "schema": "h5-to-swiftui/stack@1",
45
+ "framework": "vanilla" | "react" | "vue" | "angular" | "svelte" | "unknown",
46
+ "buildTool": "vite" | "webpack" | "parcel" | "rollup" | "none" | "unknown",
47
+ "styling": "tailwind" | "css-modules" | "styled-components" | "emotion" | "sass" | "plain-css" | "unknown",
48
+ "router": "react-router" | "vue-router" | "tanstack-router" | "history" | "none" | "unknown",
49
+ "confidence": 0.0–1.0,
50
+ "in_v1_scope": true | false
51
+ }
52
+
53
+ Examples:
54
+ node detect-stack.mjs ./my-vanilla-app
55
+ node detect-stack.mjs ./my-react-app --out ./artifacts
56
+ `);
57
+ process.exit(0);
58
+ }
59
+
60
+ if (args.length === 0 || args[0].startsWith('--')) {
61
+ console.error('Error: <h5-src> is required.\nRun with --help for usage.');
62
+ process.exit(1);
63
+ }
64
+
65
+ const h5Src = resolve(args[0]);
66
+ let outDir = h5Src;
67
+
68
+ const outIdx = args.indexOf('--out');
69
+ if (outIdx !== -1) {
70
+ if (!args[outIdx + 1]) {
71
+ console.error('Error: --out requires a directory argument.');
72
+ process.exit(1);
73
+ }
74
+ outDir = resolve(args[outIdx + 1]);
75
+ }
76
+
77
+ if (!existsSync(h5Src)) {
78
+ console.error(`Error: h5-src path does not exist: ${h5Src}`);
79
+ process.exit(1);
80
+ }
81
+
82
+ if (!statSync(h5Src).isDirectory()) {
83
+ console.error(`Error: h5-src must be a directory, got: ${h5Src}`);
84
+ process.exit(1);
85
+ }
86
+
87
+ // ── helpers ──────────────────────────────────────────────────────────────────
88
+
89
+ function readJson(p) {
90
+ try { return JSON.parse(readFileSync(p, 'utf8')); } catch { return null; }
91
+ }
92
+
93
+ function readText(p) {
94
+ try { return readFileSync(p, 'utf8'); } catch { return null; }
95
+ }
96
+
97
+ /**
98
+ * Recursively collect file paths up to maxDepth, skipping heavy dirs.
99
+ */
100
+ function collectFiles(dir, maxDepth = 4, _depth = 0) {
101
+ const skip = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.nuxt', 'out', 'coverage']);
102
+ if (_depth > maxDepth) return [];
103
+ const results = [];
104
+ let entries;
105
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return results; }
106
+ for (const e of entries) {
107
+ if (skip.has(e.name)) continue;
108
+ const full = join(dir, e.name);
109
+ if (e.isDirectory()) {
110
+ results.push(...collectFiles(full, maxDepth, _depth + 1));
111
+ } else {
112
+ results.push(full);
113
+ }
114
+ }
115
+ return results;
116
+ }
117
+
118
+ function fileContains(path, patterns) {
119
+ const text = readText(path);
120
+ if (!text) return false;
121
+ return patterns.some(p => (typeof p === 'string' ? text.includes(p) : p.test(text)));
122
+ }
123
+
124
+ // ── detection ────────────────────────────────────────────────────────────────
125
+
126
+ const pkgPath = join(h5Src, 'package.json');
127
+ const pkg = readJson(pkgPath);
128
+ const allDeps = Object.assign({}, pkg?.dependencies, pkg?.devDependencies, pkg?.peerDependencies);
129
+ const depNames = new Set(Object.keys(allDeps));
130
+
131
+ const files = collectFiles(h5Src);
132
+ const srcFiles = files.filter(f => /\.(js|jsx|ts|tsx|mjs|cjs|html|css|scss|sass|vue|svelte)$/.test(f));
133
+
134
+ // Helper: match dep name prefix or contains
135
+ const hasDep = (...names) => names.some(n => depNames.has(n) || [...depNames].some(k => k.startsWith(n)));
136
+
137
+ // ── Framework ────────────────────────────────────────────────────────────────
138
+
139
+ let framework = 'unknown';
140
+ let frameworkConf = 0;
141
+
142
+ const signals = { react: 0, vue: 0, angular: 0, svelte: 0, vanilla: 0 };
143
+
144
+ // Dep-based signals
145
+ if (hasDep('react', 'react-dom', 'react-scripts', 'next')) { signals.react += 3; }
146
+ if (hasDep('vue', '@vue/core', 'nuxt')) { signals.vue += 3; }
147
+ if (hasDep('@angular/core', '@angular/common')) { signals.angular += 3; }
148
+ if (hasDep('svelte', '@sveltejs/kit')) { signals.svelte += 3; }
149
+
150
+ // Source-based signals — sample up to 60 files to keep it fast
151
+ const sample = srcFiles.slice(0, 60);
152
+ for (const f of sample) {
153
+ const t = readText(f) ?? '';
154
+ if (/from ['"]react['"]|require\(['"]react['"]\)|jsx|\.tsx?$/.test(t)) signals.react += 1;
155
+ if (/from ['"]vue['"]|<template>|\.vue$/.test(t) || f.endsWith('.vue')) signals.vue += 1;
156
+ if (/@Component\(|@NgModule\(|platformBrowserDynamic/.test(t)) signals.angular += 1;
157
+ if (/from ['"]svelte['"]|<script.*svelte|\.svelte$/.test(t) || f.endsWith('.svelte')) signals.svelte += 1;
158
+ }
159
+
160
+ // HTML-only / no framework
161
+ const hasHtmlEntry = files.some(f => f.endsWith('.html'));
162
+ const hasNoJsFrameworkDep = !hasDep('react', 'vue', '@angular', 'svelte');
163
+ if (hasHtmlEntry && hasNoJsFrameworkDep) signals.vanilla += 1;
164
+ // Extra: if package.json missing entirely, lean vanilla
165
+ if (!pkg) signals.vanilla += 2;
166
+
167
+ const topSignal = Object.entries(signals).sort((a, b) => b[1] - a[1])[0];
168
+ const totalSignals = Object.values(signals).reduce((s, v) => s + v, 0);
169
+
170
+ if (topSignal[1] === 0 && hasHtmlEntry) {
171
+ framework = 'vanilla';
172
+ frameworkConf = 0.5;
173
+ } else if (topSignal[1] > 0) {
174
+ framework = topSignal[0];
175
+ frameworkConf = Math.min(0.95, totalSignals > 0 ? topSignal[1] / Math.max(totalSignals, 1) * 1.5 : 0.3);
176
+ if (framework === 'vanilla') frameworkConf = Math.min(0.8, frameworkConf);
177
+ }
178
+
179
+ // ── Build tool ───────────────────────────────────────────────────────────────
180
+
181
+ let buildTool = 'unknown';
182
+ let buildConf = 0;
183
+
184
+ if (hasDep('vite') || files.some(f => /vite\.config\.(js|ts|mjs)$/.test(f))) {
185
+ buildTool = 'vite'; buildConf = 0.95;
186
+ } else if (hasDep('webpack') || files.some(f => /webpack\.config\.(js|ts)$/.test(f))) {
187
+ buildTool = 'webpack'; buildConf = 0.9;
188
+ } else if (hasDep('parcel')) {
189
+ buildTool = 'parcel'; buildConf = 0.9;
190
+ } else if (hasDep('rollup') || files.some(f => /rollup\.config\.(js|ts|mjs)$/.test(f))) {
191
+ buildTool = 'rollup'; buildConf = 0.85;
192
+ } else if (hasDep('react-scripts')) {
193
+ buildTool = 'webpack'; buildConf = 0.8; // CRA uses webpack
194
+ } else if (!pkg) {
195
+ buildTool = 'none'; buildConf = 0.7;
196
+ } else {
197
+ buildTool = 'none'; buildConf = 0.4;
198
+ }
199
+
200
+ // ── Styling ──────────────────────────────────────────────────────────────────
201
+
202
+ let styling = 'unknown';
203
+ let stylingConf = 0;
204
+
205
+ const stylingSignals = { tailwind: 0, 'css-modules': 0, 'styled-components': 0, emotion: 0, sass: 0, 'plain-css': 0 };
206
+
207
+ if (hasDep('tailwindcss') || files.some(f => /tailwind\.config\.(js|ts|cjs|mjs)$/.test(f))) {
208
+ stylingSignals.tailwind += 3;
209
+ }
210
+ if (hasDep('styled-components')) stylingSignals['styled-components'] += 3;
211
+ if (hasDep('@emotion/react', '@emotion/styled', 'emotion')) stylingSignals.emotion += 3;
212
+ if (hasDep('sass', 'node-sass', 'sass-loader') || srcFiles.some(f => /\.(scss|sass)$/.test(f))) {
213
+ stylingSignals.sass += 2;
214
+ }
215
+
216
+ // CSS modules check: look for *.module.css imports
217
+ const cssModuleHit = sample.some(f => fileContains(f, [/\.module\.css['"]/, /\.module\.scss['"]/]));
218
+ if (cssModuleHit) stylingSignals['css-modules'] += 3;
219
+
220
+ const hasCss = srcFiles.some(f => f.endsWith('.css'));
221
+ if (hasCss) stylingSignals['plain-css'] += 1;
222
+
223
+ const topStyle = Object.entries(stylingSignals).sort((a, b) => b[1] - a[1])[0];
224
+ if (topStyle[1] > 0) {
225
+ styling = topStyle[0];
226
+ stylingConf = Math.min(0.95, 0.5 + topStyle[1] * 0.1);
227
+ } else {
228
+ styling = 'plain-css';
229
+ stylingConf = 0.3;
230
+ }
231
+
232
+ // ── Router ───────────────────────────────────────────────────────────────────
233
+
234
+ let router = 'none';
235
+ let routerConf = 0.6;
236
+
237
+ if (hasDep('react-router', 'react-router-dom')) { router = 'react-router'; routerConf = 0.95; }
238
+ else if (hasDep('@tanstack/react-router', '@tanstack/router')) { router = 'tanstack-router'; routerConf = 0.95; }
239
+ else if (hasDep('vue-router')) { router = 'vue-router'; routerConf = 0.95; }
240
+ else if (hasDep('history')) { router = 'history'; routerConf = 0.85; }
241
+ else {
242
+ // Check source for router usage
243
+ const routerUsage = sample.some(f =>
244
+ fileContains(f, ['BrowserRouter', 'HashRouter', 'createBrowserRouter', 'useNavigate', 'useRouter'])
245
+ );
246
+ if (routerUsage) { router = 'react-router'; routerConf = 0.6; }
247
+ else { router = 'none'; routerConf = 0.7; }
248
+ }
249
+
250
+ // ── Aggregate confidence ──────────────────────────────────────────────────────
251
+
252
+ const confidence = Math.round(
253
+ (frameworkConf * 0.5 + buildConf * 0.2 + stylingConf * 0.15 + routerConf * 0.15) * 100
254
+ ) / 100;
255
+
256
+ // ── v1 scope gate ────────────────────────────────────────────────────────────
257
+
258
+ const V1_FRAMEWORKS = new Set(['vanilla', 'react']);
259
+ const inV1Scope = V1_FRAMEWORKS.has(framework);
260
+
261
+ // ── Write report ─────────────────────────────────────────────────────────────
262
+
263
+ const report = {
264
+ schema: 'h5-to-swiftui/stack@1',
265
+ framework,
266
+ buildTool,
267
+ styling,
268
+ router,
269
+ confidence,
270
+ in_v1_scope: inV1Scope,
271
+ };
272
+
273
+ // Ensure outDir exists
274
+ try {
275
+ const { mkdirSync } = await import('node:fs');
276
+ mkdirSync(outDir, { recursive: true });
277
+ } catch (e) {
278
+ console.error(`Error: cannot create output directory ${outDir}: ${e.message}`);
279
+ process.exit(1);
280
+ }
281
+
282
+ const reportPath = join(outDir, 'stack-report.json');
283
+ try {
284
+ writeFileSync(reportPath, JSON.stringify(report, null, 2) + '\n', 'utf8');
285
+ } catch (e) {
286
+ console.error(`Error: cannot write ${reportPath}: ${e.message}`);
287
+ process.exit(1);
288
+ }
289
+
290
+ // ── Output ───────────────────────────────────────────────────────────────────
291
+
292
+ console.log(`Stack detection complete → ${reportPath}`);
293
+ console.log(JSON.stringify(report, null, 2));
294
+
295
+ if (!inV1Scope) {
296
+ console.log('');
297
+ console.log('╔══════════════════════════════════════════════════════════════╗');
298
+ console.log(`║ STOP — framework "${framework}" is outside v1 scope. `);
299
+ console.log('║ v1 supports: vanilla, react only. ');
300
+ console.log('║ Conversion pipeline will NOT proceed. ');
301
+ console.log('║ stack-report.json written with in_v1_scope: false. ');
302
+ console.log('╚══════════════════════════════════════════════════════════════╝');
303
+ }
304
+
305
+ process.exit(0);