@supportpages.io/wtfm 0.0.0-stage → 0.3.0

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 (275) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +23 -0
  3. package/README.md +392 -2
  4. package/dist/actions.d.ts +98 -0
  5. package/dist/actions.js +86 -0
  6. package/dist/actions.js.map +1 -0
  7. package/dist/agent-settings.d.ts +22 -0
  8. package/dist/agent-settings.js +35 -0
  9. package/dist/agent-settings.js.map +1 -0
  10. package/dist/api.d.ts +14 -0
  11. package/dist/api.js +159 -0
  12. package/dist/api.js.map +1 -0
  13. package/dist/article-link.d.ts +16 -0
  14. package/dist/article-link.js +25 -0
  15. package/dist/article-link.js.map +1 -0
  16. package/dist/artifacts.d.ts +45 -0
  17. package/dist/artifacts.js +144 -0
  18. package/dist/artifacts.js.map +1 -0
  19. package/dist/brand.d.ts +4 -0
  20. package/dist/brand.js +9 -0
  21. package/dist/brand.js.map +1 -0
  22. package/dist/bridge.d.ts +2964 -0
  23. package/dist/bridge.js +1036 -0
  24. package/dist/bridge.js.map +1 -0
  25. package/dist/capacity.d.ts +5 -0
  26. package/dist/capacity.js +11 -0
  27. package/dist/capacity.js.map +1 -0
  28. package/dist/credentials.d.ts +13 -0
  29. package/dist/credentials.js +88 -0
  30. package/dist/credentials.js.map +1 -0
  31. package/dist/development-tls.d.ts +8 -0
  32. package/dist/development-tls.js +38 -0
  33. package/dist/development-tls.js.map +1 -0
  34. package/dist/errors.d.ts +15 -0
  35. package/dist/errors.js +18 -0
  36. package/dist/errors.js.map +1 -0
  37. package/dist/export.d.ts +11 -0
  38. package/dist/export.js +91 -0
  39. package/dist/export.js.map +1 -0
  40. package/dist/hosted-operations.d.ts +108 -0
  41. package/dist/hosted-operations.js +120 -0
  42. package/dist/hosted-operations.js.map +1 -0
  43. package/dist/hosting-benefits.d.ts +66 -0
  44. package/dist/hosting-benefits.js +67 -0
  45. package/dist/hosting-benefits.js.map +1 -0
  46. package/dist/index.d.ts +2 -0
  47. package/dist/index.js +49 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/local-inventory.d.ts +22 -0
  50. package/dist/local-inventory.js +59 -0
  51. package/dist/local-inventory.js.map +1 -0
  52. package/dist/local-setup.d.ts +208 -0
  53. package/dist/local-setup.js +140 -0
  54. package/dist/local-setup.js.map +1 -0
  55. package/dist/pairing.d.ts +71 -0
  56. package/dist/pairing.js +235 -0
  57. package/dist/pairing.js.map +1 -0
  58. package/dist/preferences.d.ts +18 -0
  59. package/dist/preferences.js +44 -0
  60. package/dist/preferences.js.map +1 -0
  61. package/dist/progress.d.ts +205 -0
  62. package/dist/progress.js +224 -0
  63. package/dist/progress.js.map +1 -0
  64. package/dist/reminders.d.ts +31 -0
  65. package/dist/reminders.js +73 -0
  66. package/dist/reminders.js.map +1 -0
  67. package/dist/replace-connection.d.ts +6 -0
  68. package/dist/replace-connection.js +77 -0
  69. package/dist/replace-connection.js.map +1 -0
  70. package/dist/repository-actions.d.ts +18 -0
  71. package/dist/repository-actions.js +9 -0
  72. package/dist/repository-actions.js.map +1 -0
  73. package/dist/repository-benefits.d.ts +96 -0
  74. package/dist/repository-benefits.js +64 -0
  75. package/dist/repository-benefits.js.map +1 -0
  76. package/dist/run-update.d.ts +17 -0
  77. package/dist/run-update.js +27 -0
  78. package/dist/run-update.js.map +1 -0
  79. package/dist/runs.d.ts +541 -0
  80. package/dist/runs.js +146 -0
  81. package/dist/runs.js.map +1 -0
  82. package/dist/runtime.d.ts +12 -0
  83. package/dist/runtime.js +58 -0
  84. package/dist/runtime.js.map +1 -0
  85. package/dist/schema.d.ts +277 -0
  86. package/dist/schema.js +66 -0
  87. package/dist/schema.js.map +1 -0
  88. package/dist/server.d.ts +4 -0
  89. package/dist/server.js +298 -0
  90. package/dist/server.js.map +1 -0
  91. package/dist/session.d.ts +1714 -0
  92. package/dist/session.js +619 -0
  93. package/dist/session.js.map +1 -0
  94. package/dist/settings.d.ts +9 -0
  95. package/dist/settings.js +21 -0
  96. package/dist/settings.js.map +1 -0
  97. package/dist/sync.d.ts +495 -0
  98. package/dist/sync.js +191 -0
  99. package/dist/sync.js.map +1 -0
  100. package/dist/telemetry-scrub.d.ts +13 -0
  101. package/dist/telemetry-scrub.js +51 -0
  102. package/dist/telemetry-scrub.js.map +1 -0
  103. package/dist/telemetry.d.ts +73 -0
  104. package/dist/telemetry.js +173 -0
  105. package/dist/telemetry.js.map +1 -0
  106. package/dist/walkthroughs.d.ts +224 -0
  107. package/dist/walkthroughs.js +109 -0
  108. package/dist/walkthroughs.js.map +1 -0
  109. package/dist/workspace.d.ts +14 -0
  110. package/dist/workspace.js +127 -0
  111. package/dist/workspace.js.map +1 -0
  112. package/dist/writer-agent.d.ts +21 -0
  113. package/dist/writer-agent.js +27 -0
  114. package/dist/writer-agent.js.map +1 -0
  115. package/dist/writer-entry.d.ts +171 -0
  116. package/dist/writer-entry.js +233 -0
  117. package/dist/writer-entry.js.map +1 -0
  118. package/dist/writing-style.d.ts +7 -0
  119. package/dist/writing-style.js +52 -0
  120. package/dist/writing-style.js.map +1 -0
  121. package/engine/SYNC.json +4 -0
  122. package/engine/VERSION +1 -0
  123. package/engine/detect-project/README.md +141 -0
  124. package/engine/detect-project/SKILL.md +1421 -0
  125. package/engine/detect-project/assets/desktop/desktop-frame.css +428 -0
  126. package/engine/detect-project/assets/game/game-frame.css +132 -0
  127. package/engine/detect-project/assets/macosui/LICENSE-puppertino.txt +21 -0
  128. package/engine/detect-project/assets/macosui/VERSIONS.txt +1 -0
  129. package/engine/detect-project/assets/macosui/fonts.css +15 -0
  130. package/engine/detect-project/assets/macosui/macos-frame.css +481 -0
  131. package/engine/detect-project/assets/macosui/puppertino.css +2153 -0
  132. package/engine/detect-project/assets/mobileui/LICENSE-fonts.txt +13 -0
  133. package/engine/detect-project/assets/mobileui/LICENSE-framework7.txt +52 -0
  134. package/engine/detect-project/assets/mobileui/VERSIONS.txt +6 -0
  135. package/engine/detect-project/assets/mobileui/device-frame.css +316 -0
  136. package/engine/detect-project/assets/mobileui/f7-color-theme.mjs +1345 -0
  137. package/engine/detect-project/assets/mobileui/f7-icons-names.json +1254 -0
  138. package/engine/detect-project/assets/mobileui/fonts.css +16 -0
  139. package/engine/detect-project/assets/mobileui/framework7-components.css +39 -0
  140. package/engine/detect-project/assets/mobileui/framework7-core.css +5245 -0
  141. package/engine/detect-project/assets/mobileui/icons.css +31 -0
  142. package/engine/detect-project/assets/mobileui/md3-defaults.css +89 -0
  143. package/engine/detect-project/assets/mobileui/platforms.json +46 -0
  144. package/engine/detect-project/assets/tailwind-fallback.css +1729 -0
  145. package/engine/detect-project/assets/webtui/LICENSE-webtui.txt +28 -0
  146. package/engine/detect-project/assets/webtui/VERSIONS.txt +7 -0
  147. package/engine/detect-project/assets/webtui/terminal-frame.css +195 -0
  148. package/engine/detect-project/assets/webtui/theme-catppuccin.css +1 -0
  149. package/engine/detect-project/assets/webtui/theme-everforest.css +1 -0
  150. package/engine/detect-project/assets/webtui/theme-gruvbox.css +1 -0
  151. package/engine/detect-project/assets/webtui/theme-nord.css +1 -0
  152. package/engine/detect-project/assets/webtui/theme-vitesse.css +1 -0
  153. package/engine/detect-project/assets/webtui/themes.json +37 -0
  154. package/engine/detect-project/assets/webtui/webtui-core.css +1 -0
  155. package/engine/detect-project/assets/win32ui/7css.css +2 -0
  156. package/engine/detect-project/assets/win32ui/LICENSE-7css.txt +21 -0
  157. package/engine/detect-project/assets/win32ui/VERSIONS.txt +1 -0
  158. package/engine/detect-project/assets/win32ui/win32-frame.css +278 -0
  159. package/engine/detect-project/package-lock.json +12 -0
  160. package/engine/detect-project/package.json +10 -0
  161. package/engine/detect-project/scripts/apply_runtime_profiles.js +313 -0
  162. package/engine/detect-project/scripts/check_css_health.js +412 -0
  163. package/engine/detect-project/scripts/check_project_map.js +150 -0
  164. package/engine/detect-project/scripts/check_runtime_coverage.js +311 -0
  165. package/engine/detect-project/scripts/check_runtime_recipe_quality.js +184 -0
  166. package/engine/detect-project/scripts/classify_app_type.sh +246 -0
  167. package/engine/detect-project/scripts/classify_surface.sh +95 -0
  168. package/engine/detect-project/scripts/classify_workspace.js +39 -0
  169. package/engine/detect-project/scripts/compile_css.sh +447 -0
  170. package/engine/detect-project/scripts/detect_static.js +645 -0
  171. package/engine/detect-project/scripts/detect_structure.js +187 -0
  172. package/engine/detect-project/scripts/include_census.js +451 -0
  173. package/engine/detect-project/scripts/json_get.js +142 -0
  174. package/engine/detect-project/scripts/merge_json.js +52 -0
  175. package/engine/detect-project/scripts/recommend_model_tier.js +252 -0
  176. package/engine/detect-project/scripts/resolve_route_chains.js +135 -0
  177. package/engine/detect-project/scripts/run_css_build.sh +40 -0
  178. package/engine/detect-project/scripts/sanitize_css.js +83 -0
  179. package/engine/detect-project/scripts/test_classify_surface.js +108 -0
  180. package/engine/detect-project/scripts/test_node_helpers.js +142 -0
  181. package/engine/detect-project/scripts/test_recommend_model_tier.js +119 -0
  182. package/engine/detect-project/scripts/test_resolve_route_chains.js +182 -0
  183. package/engine/detect-project/scripts/test_runtime_coverage.js +323 -0
  184. package/engine/detect-project/scripts/test_runtime_recipe_quality.js +187 -0
  185. package/engine/detect-project/scripts/theme_overrides.js +169 -0
  186. package/engine/detect-project/scripts/write_branding.js +129 -0
  187. package/engine/generate-illustrated-article/SKILL.md +461 -0
  188. package/engine/generate-illustrated-article/contracts/desktop.md +90 -0
  189. package/engine/generate-illustrated-article/contracts/game.md +38 -0
  190. package/engine/generate-illustrated-article/contracts/label-evidence.md +38 -0
  191. package/engine/generate-illustrated-article/contracts/macos.md +79 -0
  192. package/engine/generate-illustrated-article/contracts/mobile.md +40 -0
  193. package/engine/generate-illustrated-article/contracts/terminal.md +29 -0
  194. package/engine/generate-illustrated-article/contracts/win32.md +43 -0
  195. package/engine/generate-illustrated-article/package-lock.json +366 -0
  196. package/engine/generate-illustrated-article/package.json +15 -0
  197. package/engine/generate-illustrated-article/scripts/article_blocks.js +34 -0
  198. package/engine/generate-illustrated-article/scripts/check_article_json.js +105 -0
  199. package/engine/generate-illustrated-article/scripts/emit_walkthrough_signals.js +100 -0
  200. package/engine/generate-illustrated-article/scripts/extract_images.js +187 -0
  201. package/engine/generate-illustrated-article/scripts/generate_content_images.js +376 -0
  202. package/engine/generate-illustrated-article/scripts/include_census.js +451 -0
  203. package/engine/generate-illustrated-article/scripts/inject_assets.js +1691 -0
  204. package/engine/generate-illustrated-article/scripts/jit_mockup_css.js +191 -0
  205. package/engine/generate-illustrated-article/scripts/label_evidence.js +87 -0
  206. package/engine/generate-illustrated-article/scripts/lint_article_copy.js +252 -0
  207. package/engine/generate-illustrated-article/scripts/lint_mockup_fidelity.js +2403 -0
  208. package/engine/generate-illustrated-article/scripts/polish_tickets.js +1148 -0
  209. package/engine/generate-illustrated-article/scripts/related_repos.sh +52 -0
  210. package/engine/generate-illustrated-article/scripts/render_all.js +177 -0
  211. package/engine/generate-illustrated-article/scripts/render_mockup.js +665 -0
  212. package/engine/generate-illustrated-article/scripts/render_ready.js +164 -0
  213. package/engine/generate-illustrated-article/scripts/resolve_workspace.sh +86 -0
  214. package/engine/generate-illustrated-article/scripts/runtime_region_geometry.js +55 -0
  215. package/engine/generate-illustrated-article/scripts/source_paths.js +49 -0
  216. package/engine/generate-illustrated-article/scripts/test_control_visibility.js +30 -0
  217. package/engine/generate-illustrated-article/scripts/test_emit_walkthrough_signals.js +157 -0
  218. package/engine/generate-illustrated-article/scripts/test_generate_content_images.js +168 -0
  219. package/engine/generate-illustrated-article/scripts/test_include_census.js +126 -0
  220. package/engine/generate-illustrated-article/scripts/test_label_evidence.js +49 -0
  221. package/engine/generate-illustrated-article/scripts/test_lint_article_copy.js +138 -0
  222. package/engine/generate-illustrated-article/scripts/test_lint_mockup_fidelity.js +653 -0
  223. package/engine/generate-illustrated-article/scripts/test_node_ports.js +152 -0
  224. package/engine/generate-illustrated-article/scripts/test_polish_tickets.js +481 -0
  225. package/engine/generate-illustrated-article/scripts/test_related_repos.js +64 -0
  226. package/engine/generate-illustrated-article/scripts/test_render_ready.js +85 -0
  227. package/engine/generate-illustrated-article/scripts/test_source_paths.js +68 -0
  228. package/engine/generate-illustrated-article/scripts/trace_hook.js +79 -0
  229. package/engine/generate-illustrated-article/scripts/trace_hook.sh +4 -0
  230. package/engine/generate-illustrated-article/scripts/validate_html.js +114 -0
  231. package/engine/generate-illustrated-article/scripts/watermark.js +69 -0
  232. package/install.sh +14 -0
  233. package/package.json +57 -4
  234. package/scripts/auth.mjs +33 -0
  235. package/scripts/auto-update.mjs +7 -0
  236. package/scripts/build-plugin.mjs +58 -0
  237. package/scripts/build-release.mjs +94 -0
  238. package/scripts/check-release.mjs +40 -0
  239. package/scripts/check-runtime.mjs +10 -0
  240. package/scripts/cli.mjs +8 -0
  241. package/scripts/install.mjs +64 -0
  242. package/scripts/lib/agent-runner.mjs +181 -0
  243. package/scripts/lib/agent-settings.mjs +215 -0
  244. package/scripts/lib/article-skills.mjs +82 -0
  245. package/scripts/lib/auto-update.mjs +50 -0
  246. package/scripts/lib/brand.mjs +9 -0
  247. package/scripts/lib/browser.mjs +12 -0
  248. package/scripts/lib/claude-connection.mjs +33 -0
  249. package/scripts/lib/claude-permissions.mjs +47 -0
  250. package/scripts/lib/claude-plugin.mjs +15 -0
  251. package/scripts/lib/claude-writer.mjs +10 -0
  252. package/scripts/lib/cli-main.mjs +85 -0
  253. package/scripts/lib/cli.mjs +888 -0
  254. package/scripts/lib/codex-config.mjs +51 -0
  255. package/scripts/lib/codex-integration.mjs +61 -0
  256. package/scripts/lib/codex-skill.mjs +34 -0
  257. package/scripts/lib/harness-models.mjs +109 -0
  258. package/scripts/lib/install.mjs +269 -0
  259. package/scripts/lib/managed-writer.mjs +38 -0
  260. package/scripts/lib/planning.mjs +263 -0
  261. package/scripts/lib/prepare-update.mjs +85 -0
  262. package/scripts/lib/refresh-writers.mjs +12 -0
  263. package/scripts/lib/remove.mjs +167 -0
  264. package/scripts/lib/renderer.mjs +34 -0
  265. package/scripts/lib/terminal.mjs +252 -0
  266. package/scripts/lib/uninit.mjs +72 -0
  267. package/scripts/lib/update.mjs +55 -0
  268. package/scripts/lib/writer-recovery.mjs +35 -0
  269. package/scripts/lib/yolo.mjs +362 -0
  270. package/scripts/plugin-session.mjs +65 -0
  271. package/scripts/prepare-update.mjs +12 -0
  272. package/scripts/publish-release.mjs +92 -0
  273. package/scripts/skills/supportpages/SKILL.md +120 -0
  274. package/scripts/smoke-release.mjs +101 -0
  275. package/server.json +27 -0
@@ -0,0 +1,645 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * detect_branding.js — Identify the CSS framework + design tokens used by the
4
+ * codebase at <cwd_to_scan> and produce branding.json (+ optional copy of the
5
+ * compiled CSS) at <output_dir>. Static parsing only — no LLM, no API call.
6
+ * Runs in ~100ms on a typical Rails or Next.js project.
7
+ *
8
+ * Usage: detect_branding.js <output_dir> <cwd_to_scan>
9
+ *
10
+ * Detection passes (results merged into one output object):
11
+ * 1. Framework identification (Tailwind, Bootstrap, Bulma, Foundation, MUI, custom, none)
12
+ * 2. Compiled CSS extraction (find the built bundle, copy if ≤500KB)
13
+ * 3. Source CSS :root + .dark blocks
14
+ * 4. SCSS variable extraction ($primary, $secondary, etc.)
15
+ * 5. Tailwind config theme.extend
16
+ * 6. Default brand colors from Ruby model code (Rails-style .presence || "#hex")
17
+ * 7. Google Fonts URLs from layout templates
18
+ * 8. External stylesheet <link> URLs from layouts
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+
24
+ const MAX_COMPILED_CSS_BYTES = 500 * 1024;
25
+ const SCAN_FILE_LIMIT = 10000;
26
+ const SKIP_DIRS = new Set([
27
+ 'node_modules', '.git', '.next', '.nuxt', 'tmp', 'log', 'logs',
28
+ 'coverage', '.cache', '.parcel-cache', 'bower_components', 'vendor',
29
+ '.DS_Store', '.bundle',
30
+ ]);
31
+
32
+ // ─── Filesystem helpers ──────────────────────────────────────────────────────
33
+
34
+ function readFileSafe(p) {
35
+ try { return fs.readFileSync(p, 'utf8'); } catch { return null; }
36
+ }
37
+ function fileExists(p) {
38
+ try { return fs.statSync(p).isFile(); } catch { return false; }
39
+ }
40
+ function dirExists(p) {
41
+ try { return fs.statSync(p).isDirectory(); } catch { return false; }
42
+ }
43
+
44
+ function* walk(root, opts = {}) {
45
+ const maxDepth = opts.maxDepth ?? 8;
46
+ const stack = [[root, 0]];
47
+ let yielded = 0;
48
+ while (stack.length) {
49
+ const [dir, depth] = stack.pop();
50
+ if (depth > maxDepth) continue;
51
+ let entries;
52
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
53
+ for (const e of entries) {
54
+ if (SKIP_DIRS.has(e.name)) continue;
55
+ const full = path.join(dir, e.name);
56
+ if (e.isDirectory()) {
57
+ stack.push([full, depth + 1]);
58
+ } else if (e.isFile()) {
59
+ yield full;
60
+ if (++yielded > SCAN_FILE_LIMIT) return;
61
+ }
62
+ }
63
+ }
64
+ }
65
+
66
+ function findFiles(root, predicate, maxResults = 200) {
67
+ const results = [];
68
+ for (const f of walk(root)) {
69
+ if (predicate(f)) {
70
+ results.push(f);
71
+ if (results.length >= maxResults) break;
72
+ }
73
+ }
74
+ return results;
75
+ }
76
+
77
+ // ─── Pass 1: Framework identification ────────────────────────────────────────
78
+
79
+ const FRAMEWORK_SIGNALS = {
80
+ tailwind: {
81
+ package: ['tailwindcss'],
82
+ gem: ['tailwindcss-rails'],
83
+ configs: ['tailwind.config.js', 'tailwind.config.ts', 'tailwind.config.cjs', 'tailwind.config.mjs'],
84
+ cssImports: [/@tailwind\s+(base|components|utilities)/, /@import\s+["']tailwindcss/i],
85
+ },
86
+ bootstrap: {
87
+ package: ['bootstrap'],
88
+ gem: ['bootstrap', 'bootstrap-sass'],
89
+ cssImports: [/@import\s+["']bootstrap/i, /@use\s+["']bootstrap/i],
90
+ classPatterns: [/\bbtn\s+btn-(primary|secondary|success|danger|warning|info|light|dark)\b/, /\bnavbar-(brand|nav|expand|toggler)/],
91
+ },
92
+ webpixels: {
93
+ // Bootstrap-compatible utility/component library with extra primitives:
94
+ // .btn-neutral, .btn-square, .avatar, .icon, .text-heading, etc.
95
+ // Detected as its own framework so downstream uses the right CDN URL
96
+ // and so refinement knows the project has extra component classes.
97
+ package: ['@webpixels/css'],
98
+ cssImports: [/@import\s+["']@webpixels\/css/i, /@use\s+["']@webpixels\/css/i],
99
+ classPatterns: [/\bavatar(\s+avatar-(xs|sm|md|lg|xl|circle|square))?\b/, /\bbtn-neutral\b/, /\bbtn-square\b/, /\btext-heading\b/, /\bbg-section-secondary\b/],
100
+ },
101
+ bulma: {
102
+ package: ['bulma'],
103
+ gem: ['bulma-rails'],
104
+ cssImports: [/@import\s+["']bulma/i, /@use\s+["']bulma/i],
105
+ classPatterns: [/\bis-(primary|info|warning|danger|success|link)\b/, /\bbutton\s+is-/],
106
+ },
107
+ foundation: {
108
+ package: ['foundation-sites'],
109
+ gem: ['foundation-rails'],
110
+ cssImports: [/@import\s+["']foundation/i],
111
+ },
112
+ mui: {
113
+ package: ['@mui/material', '@mui/core', '@material-ui/core'],
114
+ jsImports: [/from\s+["']@mui\//, /from\s+["']@material-ui\//, /createTheme\s*\(/],
115
+ },
116
+ };
117
+
118
+ function detectFrameworks(cwd) {
119
+ const packageJsonPath = path.join(cwd, 'package.json');
120
+ const packageDeps = {};
121
+ if (fileExists(packageJsonPath)) {
122
+ try {
123
+ const pkg = JSON.parse(readFileSafe(packageJsonPath));
124
+ Object.assign(packageDeps, pkg.dependencies || {}, pkg.devDependencies || {});
125
+ } catch { /* invalid json */ }
126
+ }
127
+
128
+ const gemfile = readFileSafe(path.join(cwd, 'Gemfile')) || '';
129
+ const gemfileLock = readFileSafe(path.join(cwd, 'Gemfile.lock')) || '';
130
+
131
+ const scores = {};
132
+ const versions = {};
133
+
134
+ for (const [name, signals] of Object.entries(FRAMEWORK_SIGNALS)) {
135
+ let score = 0;
136
+ let version = null;
137
+
138
+ for (const dep of (signals.package || [])) {
139
+ if (packageDeps[dep]) { score += 3; version = version || packageDeps[dep]; }
140
+ }
141
+ for (const gem of (signals.gem || [])) {
142
+ if (new RegExp(`\\bgem\\s+["']${gem}["']`, 'i').test(gemfile)) {
143
+ score += 3;
144
+ const lockMatch = gemfileLock.match(new RegExp(`\\b${gem}\\s+\\(([\\d.]+)\\)`, 'i'));
145
+ if (lockMatch) version = version || lockMatch[1];
146
+ }
147
+ }
148
+ for (const cfg of (signals.configs || [])) {
149
+ if (fileExists(path.join(cwd, cfg))) score += 5;
150
+ }
151
+
152
+ scores[name] = score;
153
+ if (version) versions[name] = version;
154
+ }
155
+
156
+ // Scan CSS files for @import / @tailwind
157
+ const cssFiles = findFiles(cwd, f => /\.(css|scss|sass)$/i.test(f), 200);
158
+ for (const f of cssFiles) {
159
+ const content = readFileSafe(f);
160
+ if (!content) continue;
161
+ for (const [name, signals] of Object.entries(FRAMEWORK_SIGNALS)) {
162
+ for (const re of (signals.cssImports || [])) {
163
+ if (re.test(content)) scores[name] = (scores[name] || 0) + 2;
164
+ }
165
+ }
166
+ }
167
+
168
+ // Scan JS for MUI imports
169
+ const jsFiles = findFiles(cwd, f => /\.(jsx?|tsx?)$/i.test(f), 200);
170
+ for (const f of jsFiles) {
171
+ const content = readFileSafe(f);
172
+ if (!content) continue;
173
+ for (const re of (FRAMEWORK_SIGNALS.mui.jsImports || [])) {
174
+ if (re.test(content)) scores.mui = (scores.mui || 0) + 1;
175
+ }
176
+ }
177
+
178
+ // Scan templates for class-name patterns
179
+ const templateFiles = findFiles(cwd, f => /\.(erb|haml|slim|heex|eex|html|jsx|tsx|vue|svelte)$/i.test(f), 200);
180
+ const templateBlob = templateFiles.map(f => readFileSafe(f) || '').join('\n');
181
+ for (const [name, signals] of Object.entries(FRAMEWORK_SIGNALS)) {
182
+ for (const re of (signals.classPatterns || [])) {
183
+ const matches = templateBlob.match(new RegExp(re.source, 'g'));
184
+ if (matches) scores[name] = (scores[name] || 0) + Math.min(matches.length, 3);
185
+ }
186
+ }
187
+
188
+ const winners = Object.entries(scores)
189
+ .filter(([_, s]) => s >= 3)
190
+ .sort((a, b) => b[1] - a[1])
191
+ .map(([n]) => n);
192
+
193
+ if (winners.length === 0) {
194
+ if (cssFiles.length > 0) return { framework: 'custom', version: null };
195
+ return { framework: 'none', version: null };
196
+ }
197
+
198
+ const primary = winners[0];
199
+ return {
200
+ framework: winners.length > 1 ? winners.join('+') : primary,
201
+ version: versions[primary] || null,
202
+ };
203
+ }
204
+
205
+ // ─── Pass 2: Compiled CSS extraction ─────────────────────────────────────────
206
+
207
+ function findCompiledCss(cwd) {
208
+ const candidates = [];
209
+ for (const f of walk(cwd)) {
210
+ if (!f.endsWith('.css')) continue;
211
+ const rel = path.relative(cwd, f);
212
+ if (!/^(app\/assets\/builds|public\/(build|css|assets)|dist|build|static\/css|_next\/static\/css)\//.test(rel)) continue;
213
+ try {
214
+ const size = fs.statSync(f).size;
215
+ candidates.push({ path: f, rel, size });
216
+ } catch { /* skip */ }
217
+ }
218
+ if (candidates.length === 0) return null;
219
+ // Prefer non-vendor files (not named bootstrap.min.css etc.), then largest
220
+ candidates.sort((a, b) => {
221
+ const aVendor = /\b(bootstrap|bulma|foundation|normalize|reset)\b/i.test(a.rel) ? 1 : 0;
222
+ const bVendor = /\b(bootstrap|bulma|foundation|normalize|reset)\b/i.test(b.rel) ? 1 : 0;
223
+ return aVendor - bVendor || b.size - a.size;
224
+ });
225
+ return candidates[0];
226
+ }
227
+
228
+ // ─── Pass 3: Source CSS :root + .dark extraction ─────────────────────────────
229
+
230
+ function extractRootBlocks(cwd) {
231
+ let rootCss = null;
232
+ let darkCss = null;
233
+ let rootSource = null;
234
+ for (const f of walk(cwd)) {
235
+ if (!/\.(css|scss)$/i.test(f)) continue;
236
+ const rel = path.relative(cwd, f);
237
+ if (!/(app\/assets\/(tailwind|stylesheets)|src\/|styles\/|app\/styles\/)/.test(rel)) continue;
238
+ const content = readFileSafe(f);
239
+ if (!content) continue;
240
+ if (!rootCss) {
241
+ const m = content.match(/:root\s*\{([^}]*)\}/m);
242
+ if (m) { rootCss = m[1].trim(); rootSource = rel; }
243
+ }
244
+ if (!darkCss) {
245
+ const m = content.match(/\.dark\s*\{([^}]*)\}/m);
246
+ if (m) darkCss = m[1].trim();
247
+ }
248
+ if (rootCss && darkCss) break;
249
+ }
250
+ return { rootCss, darkCss, rootSource };
251
+ }
252
+
253
+ // ─── Pass 3b: SCSS / Tailwind entry-point detection ──────────────────────────
254
+
255
+ // The "entry" is the single file that @imports everything else — what the
256
+ // project's build pipeline feeds into sass/tailwindcss. Without this,
257
+ // compile_css.sh has nothing to compile and falls back to CDN.
258
+
259
+ function extractScssEntry(cwd) {
260
+ const candidates = [
261
+ // Webpacker / shakapacker (Rails) — common when Rails uses webpack
262
+ 'app/frontend/packs/application.scss',
263
+ 'app/frontend/packs/application.css.scss',
264
+ 'app/frontend/styling/application.scss',
265
+ 'app/javascript/packs/application.scss',
266
+ // Rails asset pipeline
267
+ 'app/assets/stylesheets/application.scss',
268
+ 'app/assets/stylesheets/application.sass',
269
+ 'app/assets/stylesheets/application.css.scss',
270
+ // Generic Node / framework patterns
271
+ 'src/styles/main.scss',
272
+ 'src/styles/application.scss',
273
+ 'src/styles/globals.scss',
274
+ 'src/styles/index.scss',
275
+ 'src/main.scss',
276
+ 'src/index.scss',
277
+ 'src/app.scss',
278
+ 'styles/main.scss',
279
+ 'styles/globals.scss',
280
+ 'assets/scss/main.scss',
281
+ 'assets/styles/main.scss',
282
+ ];
283
+ for (const p of candidates) {
284
+ const full = path.join(cwd, p);
285
+ if (!fileExists(full)) continue;
286
+ const content = readFileSafe(full) || '';
287
+ // Verify it's actually an entry — contains @import / @use directives
288
+ if (/@(import|use)\s+["][^"]+["]/.test(content) || /@(import|use)\s+'[^']+'/.test(content)) {
289
+ return p;
290
+ }
291
+ }
292
+ return null;
293
+ }
294
+
295
+ function extractTailwindEntry(cwd) {
296
+ const candidates = [
297
+ 'app/assets/tailwind/application.css',
298
+ 'app/assets/tailwind/index.css',
299
+ 'app/assets/stylesheets/application.tailwind.css',
300
+ 'src/styles/globals.css',
301
+ 'src/styles/main.css',
302
+ 'src/styles/tailwind.css',
303
+ 'src/index.css',
304
+ 'src/app.css',
305
+ 'styles/globals.css',
306
+ 'styles/main.css',
307
+ 'app/globals.css', // Next.js app router
308
+ 'app/[locale]/globals.css',
309
+ ];
310
+ for (const p of candidates) {
311
+ const full = path.join(cwd, p);
312
+ if (!fileExists(full)) continue;
313
+ const content = readFileSafe(full) || '';
314
+ if (/@tailwind\s+(base|components|utilities)/.test(content) ||
315
+ /@import\s+["']tailwindcss/.test(content)) {
316
+ return p;
317
+ }
318
+ }
319
+ return null;
320
+ }
321
+
322
+ // ─── Pass 4: SCSS variables ──────────────────────────────────────────────────
323
+
324
+ function extractScssVars(cwd) {
325
+ const vars = {};
326
+ const sources = [];
327
+ const candidates = [
328
+ 'app/assets/stylesheets/_variables.scss',
329
+ 'app/assets/stylesheets/_colors.scss',
330
+ 'app/assets/stylesheets/_brand.scss',
331
+ 'app/assets/stylesheets/_theme.scss',
332
+ 'src/styles/_variables.scss',
333
+ 'styles/_variables.scss',
334
+ ];
335
+ for (const p of candidates) {
336
+ const full = path.join(cwd, p);
337
+ if (!fileExists(full)) continue;
338
+ const content = readFileSafe(full) || '';
339
+ sources.push(p);
340
+ const re = /\$([a-z][\w-]*)\s*:\s*(#[\da-f]{3,8}|[a-z][\w-]*)\s*(?:!default)?\s*;/gi;
341
+ for (const m of content.matchAll(re)) {
342
+ const name = m[1].toLowerCase();
343
+ if (/^(primary|secondary|accent|brand|brand-color|color-primary|theme-primary|success|warning|danger|info|body-color)$/.test(name)) {
344
+ vars[name] = m[2];
345
+ }
346
+ }
347
+ }
348
+ return { vars, sources };
349
+ }
350
+
351
+ // ─── Pass 5: Tailwind config parsing ─────────────────────────────────────────
352
+
353
+ function extractTailwindExtend(cwd) {
354
+ for (const name of ['tailwind.config.js', 'tailwind.config.ts', 'tailwind.config.cjs', 'tailwind.config.mjs']) {
355
+ const full = path.join(cwd, name);
356
+ if (!fileExists(full)) continue;
357
+ const content = readFileSafe(full) || '';
358
+ // Best-effort regex extract of the extend block. Matches "extend: { ... }" allowing
359
+ // one level of nesting (handles colors: { brand: '#...' } common case).
360
+ const m = content.match(/extend\s*:\s*(\{(?:[^{}]|\{[^{}]*\})*\})/m);
361
+ return { extend: m ? m[1] : null, source: name };
362
+ }
363
+ return { extend: null, source: null };
364
+ }
365
+
366
+ // ─── Pass 6: Default brand colors from Ruby model code ───────────────────────
367
+
368
+ function extractDefaultBrandColors(cwd) {
369
+ const colors = {};
370
+ const dynamic_sources = []; // {key, hex, source_file, source_kind, snippet}
371
+ const sources = new Set();
372
+
373
+ function record(kind, hex, file, sourceKind, snippet) {
374
+ const key = kind.includes('accent') ? 'accent'
375
+ : kind.includes('gradient') ? 'gradient_start'
376
+ : kind.includes('secondary') ? 'secondary'
377
+ : 'primary';
378
+ if (!colors[key]) {
379
+ colors[key] = hex;
380
+ sources.add(file);
381
+ dynamic_sources.push({ key, hex, source_file: file, source_kind: sourceKind, snippet: snippet.trim().slice(0, 120) });
382
+ }
383
+ }
384
+
385
+ // 1. Ruby model / helper / service / job code
386
+ const rubyDirs = ['app/models', 'app/services', 'app/jobs', 'app/helpers', 'app/controllers', 'config/initializers', 'lib'];
387
+ for (const d of rubyDirs) {
388
+ const full = path.join(cwd, d);
389
+ if (!dirExists(full)) continue;
390
+ for (const f of walk(full)) {
391
+ if (!f.endsWith('.rb')) continue;
392
+ const content = readFileSafe(f);
393
+ if (!content) continue;
394
+ const rel = path.relative(cwd, f);
395
+
396
+ // Pattern A: foo_color.presence || "#hex" (RTFM model-default style)
397
+ for (const m of content.matchAll(/(\w*?(?:primary|brand|accent|gradient|secondary)\w*)_color[^"\n]*?\.presence\s*\|\|\s*["'](#[0-9a-fA-F]{3,8})["']/gi)) {
398
+ record(m[1].toLowerCase(), m[2], rel, 'model_presence_default', m[0]);
399
+ }
400
+
401
+ // Pattern B: ENV.fetch("PRIMARY_COLOR", "#hex") (env-var with default)
402
+ for (const m of content.matchAll(/ENV\.fetch\s*\(\s*["'][^"']*?(?:PRIMARY|BRAND|ACCENT|GRADIENT|SECONDARY)[^"']*?["']\s*,\s*["'](#[0-9a-fA-F]{3,8})["']/gi)) {
403
+ const kind = (m[0].match(/(PRIMARY|BRAND|ACCENT|GRADIENT|SECONDARY)/i) || [''])[0].toLowerCase();
404
+ record(kind, m[1], rel, 'env_fetch_default', m[0]);
405
+ }
406
+
407
+ // Pattern C: ENV["FOO_COLOR"] || "#hex"
408
+ for (const m of content.matchAll(/ENV\s*\[\s*["'][^"']*?(?:PRIMARY|BRAND|ACCENT|GRADIENT|SECONDARY)[^"']*?["']\s*\]\s*\|\|\s*["'](#[0-9a-fA-F]{3,8})["']/gi)) {
409
+ const kind = (m[0].match(/(PRIMARY|BRAND|ACCENT|GRADIENT|SECONDARY)/i) || [''])[0].toLowerCase();
410
+ record(kind, m[1], rel, 'env_or_default', m[0]);
411
+ }
412
+
413
+ // Pattern D: PRIMARY_COLOR = "#hex" (top-level constant)
414
+ for (const m of content.matchAll(/(?:^|\s)(PRIMARY|BRAND|ACCENT|GRADIENT|SECONDARY)(?:_COLOR)?\s*=\s*["'](#[0-9a-fA-F]{3,8})["']/gm)) {
415
+ record(m[1].toLowerCase(), m[2], rel, 'ruby_constant', m[0]);
416
+ }
417
+
418
+ // Pattern E: DEFAULT_COLORS = %w[#abc #def]
419
+ const defMatch = content.match(/DEFAULT_COLORS?\s*=\s*%w\[\s*((?:#[0-9a-fA-F]{3,8}\s*)+)\]/);
420
+ if (defMatch) {
421
+ const hexes = defMatch[1].trim().split(/\s+/);
422
+ if (hexes[0]) record('primary', hexes[0], rel, 'default_colors_constant', defMatch[0]);
423
+ if (hexes[1]) record('accent', hexes[1], rel, 'default_colors_constant', defMatch[0]);
424
+ }
425
+ }
426
+ }
427
+
428
+ // 2. DB schema column defaults — db/schema.rb: t.string "primary_color", default: "#hex"
429
+ const schemaPath = path.join(cwd, 'db/schema.rb');
430
+ if (fileExists(schemaPath)) {
431
+ const content = readFileSafe(schemaPath) || '';
432
+ for (const m of content.matchAll(/t\.\w+\s+["'](\w*?(?:primary|brand|accent|gradient|secondary)\w*_color)["'][^,\n]*?,\s*default:\s*["'](#[0-9a-fA-F]{3,8})["']/gi)) {
433
+ const kind = m[1].toLowerCase().replace(/_color$/, '');
434
+ record(kind, m[2], 'db/schema.rb', 'schema_column_default', m[0]);
435
+ }
436
+ }
437
+
438
+ // 3. YAML / config file defaults (Settings, Rails config) — config/*.yml,
439
+ // config/settings.yml, config/branding.yml
440
+ const yamlCandidates = ['config/settings.yml', 'config/branding.yml', 'config/theme.yml'];
441
+ for (const p of yamlCandidates) {
442
+ const full = path.join(cwd, p);
443
+ if (!fileExists(full)) continue;
444
+ const content = readFileSafe(full) || '';
445
+ for (const m of content.matchAll(/(\w*?(?:primary|brand|accent|gradient|secondary)\w*?_color)\s*:\s*["']?(#[0-9a-fA-F]{3,8})/gi)) {
446
+ const kind = m[1].toLowerCase().replace(/_color$/, '');
447
+ record(kind, m[2], p, 'yaml_default', m[0]);
448
+ }
449
+ }
450
+
451
+ return { colors, sources: Array.from(sources), dynamic_sources };
452
+ }
453
+
454
+ // ─── Pass 7+8: stylesheets from layout templates ─────────────────────────────
455
+
456
+ function extractStylesheets(cwd) {
457
+ const layoutFiles = [];
458
+ const candidateDirs = ['app/views/layouts', 'app', 'pages', 'src', 'public'];
459
+ for (const d of candidateDirs) {
460
+ const full = path.join(cwd, d);
461
+ if (!dirExists(full)) continue;
462
+ for (const f of walk(full, { maxDepth: 4 })) {
463
+ if (!/\.(erb|haml|slim|heex|eex|html|jsx|tsx)$/i.test(f)) continue;
464
+ const base = path.basename(f).toLowerCase();
465
+ if (base.startsWith('layout') || base.startsWith('application')
466
+ || base.startsWith('_document') || base.startsWith('root_layout')
467
+ || base === 'app.tsx' || base === 'app.jsx' || base === 'index.html') {
468
+ layoutFiles.push(f);
469
+ }
470
+ }
471
+ }
472
+ // Top-level common files
473
+ for (const p of ['index.html', 'public/index.html']) {
474
+ const full = path.join(cwd, p);
475
+ if (fileExists(full) && !layoutFiles.includes(full)) layoutFiles.push(full);
476
+ }
477
+
478
+ const googleFonts = new Set();
479
+ const externalStylesheets = new Set();
480
+ const sources = new Set();
481
+
482
+ for (const f of layoutFiles) {
483
+ const content = readFileSafe(f);
484
+ if (!content) continue;
485
+ let matched = false;
486
+ for (const m of content.matchAll(/https:\/\/fonts\.googleapis\.com\/[^\s"'<>]+/g)) {
487
+ googleFonts.add(m[0]); matched = true;
488
+ }
489
+ for (const m of content.matchAll(/<link[^>]*\bhref=["'](https?:\/\/[^"']+\.css[^"']*)["'][^>]*\brel=["']stylesheet["']/gi)) {
490
+ if (!m[1].includes('fonts.googleapis.com')) { externalStylesheets.add(m[1]); matched = true; }
491
+ }
492
+ for (const m of content.matchAll(/<link[^>]*\brel=["']stylesheet["'][^>]*\bhref=["'](https?:\/\/[^"']+)["']/gi)) {
493
+ if (!m[1].includes('fonts.googleapis.com')) { externalStylesheets.add(m[1]); matched = true; }
494
+ }
495
+ if (matched) sources.add(path.relative(cwd, f));
496
+ }
497
+
498
+ return {
499
+ googleFonts: Array.from(googleFonts),
500
+ externalStylesheets: Array.from(externalStylesheets),
501
+ sources: Array.from(sources),
502
+ };
503
+ }
504
+
505
+ // ─── Framework CDN URLs ──────────────────────────────────────────────────────
506
+
507
+ function frameworkCdn(framework, version) {
508
+ if (!framework) return null;
509
+ if (framework.includes('tailwind')) return 'https://cdn.tailwindcss.com';
510
+ const cleanVersion = (version || '').replace(/^[\^~]/, '');
511
+ if (framework.includes('webpixels')) {
512
+ const v = cleanVersion || 'latest';
513
+ return `https://cdn.jsdelivr.net/npm/@webpixels/css@${v}/dist/index.css`;
514
+ }
515
+ if (framework.includes('bootstrap')) {
516
+ const v = cleanVersion || '5.3.2';
517
+ return `https://cdn.jsdelivr.net/npm/bootstrap@${v}/dist/css/bootstrap.min.css`;
518
+ }
519
+ if (framework.includes('bulma')) {
520
+ const v = cleanVersion || '0.9.4';
521
+ return `https://cdn.jsdelivr.net/npm/bulma@${v}/css/bulma.min.css`;
522
+ }
523
+ if (framework.includes('foundation')) {
524
+ const v = cleanVersion || '6.7.5';
525
+ return `https://cdn.jsdelivr.net/npm/foundation-sites@${v}/dist/css/foundation.min.css`;
526
+ }
527
+ return null;
528
+ }
529
+
530
+ // ─── CSS variable overrides (Bootstrap 5 --bs-* etc.) ────────────────────────
531
+
532
+ function buildCssVarOverrides(framework, version, scssVars) {
533
+ const overrides = {};
534
+ if (framework && framework.includes('bootstrap')) {
535
+ const major = parseInt((version || '5').replace(/^[\^~]/, '').split('.')[0] || '5', 10);
536
+ if (major >= 5) {
537
+ const map = {
538
+ primary: '--bs-primary', secondary: '--bs-secondary',
539
+ success: '--bs-success', danger: '--bs-danger',
540
+ warning: '--bs-warning', info: '--bs-info',
541
+ 'body-color': '--bs-body-color',
542
+ };
543
+ for (const [scss, css] of Object.entries(map)) {
544
+ if (scssVars[scss]) overrides[css] = scssVars[scss];
545
+ }
546
+ }
547
+ }
548
+ return overrides;
549
+ }
550
+
551
+ // ─── Main ────────────────────────────────────────────────────────────────────
552
+
553
+ // Stamp the running skills version + this file's content hash to stderr, so a
554
+ // debug bundle reveals exactly which code ran (vendored copies of the skills can
555
+ // drift behind a release). Hash is authoritative.
556
+ function logSkillsVersion() {
557
+ const p = require('path'), f = require('fs');
558
+ const here = p.dirname(f.realpathSync(__filename));
559
+ let version = 'unknown';
560
+ for (const c of [p.join(here, '..', '..', 'VERSION'), p.join(here, '..', 'VERSION')]) {
561
+ try { const v = f.readFileSync(c, 'utf8').trim(); if (v) { version = v; break; } } catch { /* next */ }
562
+ }
563
+ let sha = '?';
564
+ try { sha = require('crypto').createHash('sha256').update(f.readFileSync(f.realpathSync(__filename))).digest('hex').slice(0, 8); } catch { /* ignore */ }
565
+ console.error(`rtfm-skills detect_static.js — v${version} (sha ${sha})`);
566
+ }
567
+
568
+ function main() {
569
+ logSkillsVersion();
570
+ const outputDir = process.argv[2];
571
+ const cwd = process.argv[3];
572
+ if (!outputDir || !cwd) {
573
+ console.error('Usage: detect_branding.js <output_dir> <cwd_to_scan>');
574
+ process.exit(1);
575
+ }
576
+ if (!dirExists(cwd)) {
577
+ console.error(`cwd_to_scan does not exist: ${cwd}`);
578
+ process.exit(1);
579
+ }
580
+ if (!dirExists(outputDir)) {
581
+ try { fs.mkdirSync(outputDir, { recursive: true }); }
582
+ catch (err) { console.error(`Could not create ${outputDir}: ${err.message}`); process.exit(1); }
583
+ }
584
+
585
+ const { framework, version } = detectFrameworks(cwd);
586
+ const compiled = findCompiledCss(cwd);
587
+ const { rootCss, darkCss, rootSource } = extractRootBlocks(cwd);
588
+ const scssEntry = extractScssEntry(cwd);
589
+ const tailwindEntry = extractTailwindEntry(cwd);
590
+ const { vars: scssVars, sources: scssSources } = extractScssVars(cwd);
591
+ const { extend: tailwindExtend, source: twSource } = extractTailwindExtend(cwd);
592
+ const { colors: defaultColors, sources: defaultSources, dynamic_sources: brandingSources } = extractDefaultBrandColors(cwd);
593
+ const { googleFonts, externalStylesheets, sources: layoutSources } = extractStylesheets(cwd);
594
+
595
+ let compiledCssPath = null;
596
+ if (compiled && compiled.size <= MAX_COMPILED_CSS_BYTES) {
597
+ try {
598
+ fs.copyFileSync(compiled.path, path.join(outputDir, 'branding.css'));
599
+ compiledCssPath = 'branding.css';
600
+ } catch (err) {
601
+ console.warn(`Failed to copy compiled CSS: ${err.message}`);
602
+ }
603
+ }
604
+
605
+ const cdn = frameworkCdn(framework, version);
606
+ const cssVarOverrides = buildCssVarOverrides(framework, version, scssVars);
607
+
608
+ const branding = {};
609
+ if (framework && framework !== 'none') branding.framework = framework;
610
+ if (version) branding.framework_version = version;
611
+ if (cdn) branding.framework_cdn = cdn;
612
+ if (compiledCssPath) branding.compiled_css_path = compiledCssPath;
613
+ if (compiled) branding.compiled_css_source = path.relative(cwd, compiled.path);
614
+ if (scssEntry) branding.scss_entry = scssEntry;
615
+ if (tailwindEntry) branding.tailwind_entry = tailwindEntry;
616
+ if (rootCss) branding.root_css = rootCss;
617
+ if (darkCss) branding.dark_css = darkCss;
618
+ if (googleFonts.length) branding.google_fonts = googleFonts;
619
+ if (externalStylesheets.length) branding.external_stylesheets = externalStylesheets;
620
+ if (Object.keys(defaultColors).length) branding.default_colors = defaultColors;
621
+ if (brandingSources && brandingSources.length) branding.branding_sources = brandingSources;
622
+ if (tailwindExtend) branding.tailwind_extend_raw = tailwindExtend;
623
+ if (Object.keys(cssVarOverrides).length) branding.css_var_overrides = cssVarOverrides;
624
+ if (Object.keys(scssVars).length) branding.scss_vars = scssVars;
625
+
626
+ const sources = [];
627
+ if (compiled) sources.push(path.relative(cwd, compiled.path));
628
+ if (rootSource) sources.push(rootSource);
629
+ if (twSource) sources.push(twSource);
630
+ for (const s of scssSources) sources.push(s);
631
+ for (const s of defaultSources) sources.push(s);
632
+ for (const s of layoutSources) sources.push(s);
633
+ if (sources.length) branding.sources = Array.from(new Set(sources));
634
+
635
+ fs.writeFileSync(path.join(outputDir, 'branding.json'), JSON.stringify(branding, null, 2));
636
+
637
+ const summary = [`framework=${branding.framework || 'none'}`];
638
+ if (compiledCssPath) summary.push(`compiled-css=${Math.round(compiled.size / 1024)}KB`);
639
+ if (rootCss) summary.push('root-vars');
640
+ if (Object.keys(defaultColors).length) summary.push(`defaults=[${Object.entries(defaultColors).map(([k, v]) => k + ':' + v).join(',')}]`);
641
+ if (googleFonts.length) summary.push(`fonts=${googleFonts.length}`);
642
+ console.log(`Branding detected: ${summary.join(', ')}`);
643
+ }
644
+
645
+ main();