ui-pro-forge-cli 2.5.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 (188) hide show
  1. package/README.md +99 -0
  2. package/assets/data/app-interface.csv +31 -0
  3. package/assets/data/charts.csv +26 -0
  4. package/assets/data/colors.csv +194 -0
  5. package/assets/data/google-fonts.csv +1924 -0
  6. package/assets/data/icons.csv +106 -0
  7. package/assets/data/landing.csv +35 -0
  8. package/assets/data/motion.csv +17 -0
  9. package/assets/data/products.csv +193 -0
  10. package/assets/data/react-performance.csv +45 -0
  11. package/assets/data/stacks/angular.csv +51 -0
  12. package/assets/data/stacks/astro.csv +54 -0
  13. package/assets/data/stacks/avalonia.csv +57 -0
  14. package/assets/data/stacks/flutter.csv +53 -0
  15. package/assets/data/stacks/html-tailwind.csv +56 -0
  16. package/assets/data/stacks/javafx.csv +76 -0
  17. package/assets/data/stacks/jetpack-compose.csv +53 -0
  18. package/assets/data/stacks/laravel.csv +51 -0
  19. package/assets/data/stacks/nextjs.csv +53 -0
  20. package/assets/data/stacks/nuxt-ui.csv +71 -0
  21. package/assets/data/stacks/nuxtjs.csv +59 -0
  22. package/assets/data/stacks/react-native.csv +52 -0
  23. package/assets/data/stacks/react.csv +54 -0
  24. package/assets/data/stacks/shadcn.csv +61 -0
  25. package/assets/data/stacks/solidjs.csv +21 -0
  26. package/assets/data/stacks/svelte.csv +54 -0
  27. package/assets/data/stacks/swiftui.csv +51 -0
  28. package/assets/data/stacks/threejs.csv +54 -0
  29. package/assets/data/stacks/uno.csv +60 -0
  30. package/assets/data/stacks/vue.csv +50 -0
  31. package/assets/data/stacks/winui.csv +60 -0
  32. package/assets/data/styles.csv +86 -0
  33. package/assets/data/typography.csv +75 -0
  34. package/assets/data/ui-reasoning.csv +162 -0
  35. package/assets/data/ux-guidelines.csv +100 -0
  36. package/assets/scripts/core.py +463 -0
  37. package/assets/scripts/design_system.py +1371 -0
  38. package/assets/scripts/search.py +162 -0
  39. package/assets/scripts/tests/test_core.py +134 -0
  40. package/assets/scripts/validate_data.py +114 -0
  41. package/assets/skills/banner-design/SKILL.md +196 -0
  42. package/assets/skills/banner-design/references/banner-sizes-and-styles.md +118 -0
  43. package/assets/skills/brand/SKILL.md +97 -0
  44. package/assets/skills/brand/references/approval-checklist.md +169 -0
  45. package/assets/skills/brand/references/asset-organization.md +157 -0
  46. package/assets/skills/brand/references/brand-guideline-template.md +140 -0
  47. package/assets/skills/brand/references/color-palette-management.md +186 -0
  48. package/assets/skills/brand/references/consistency-checklist.md +94 -0
  49. package/assets/skills/brand/references/logo-usage-rules.md +185 -0
  50. package/assets/skills/brand/references/messaging-framework.md +85 -0
  51. package/assets/skills/brand/references/typography-specifications.md +214 -0
  52. package/assets/skills/brand/references/update.md +118 -0
  53. package/assets/skills/brand/references/visual-identity.md +96 -0
  54. package/assets/skills/brand/references/voice-framework.md +88 -0
  55. package/assets/skills/brand/scripts/extract-colors.cjs +341 -0
  56. package/assets/skills/brand/scripts/inject-brand-context.cjs +349 -0
  57. package/assets/skills/brand/scripts/sync-brand-to-tokens.cjs +248 -0
  58. package/assets/skills/brand/scripts/tests/test_sync_brand_to_tokens.py +52 -0
  59. package/assets/skills/brand/scripts/validate-asset.cjs +387 -0
  60. package/assets/skills/brand/templates/brand-guidelines-starter.md +275 -0
  61. package/assets/skills/design/SKILL.md +313 -0
  62. package/assets/skills/design/data/cip/deliverables.csv +51 -0
  63. package/assets/skills/design/data/cip/industries.csv +21 -0
  64. package/assets/skills/design/data/cip/mockup-contexts.csv +21 -0
  65. package/assets/skills/design/data/cip/styles.csv +21 -0
  66. package/assets/skills/design/data/icon/styles.csv +16 -0
  67. package/assets/skills/design/data/logo/colors.csv +56 -0
  68. package/assets/skills/design/data/logo/industries.csv +56 -0
  69. package/assets/skills/design/data/logo/styles.csv +56 -0
  70. package/assets/skills/design/references/banner-sizes-and-styles.md +118 -0
  71. package/assets/skills/design/references/cip-deliverable-guide.md +95 -0
  72. package/assets/skills/design/references/cip-design.md +121 -0
  73. package/assets/skills/design/references/cip-prompt-engineering.md +84 -0
  74. package/assets/skills/design/references/cip-style-guide.md +68 -0
  75. package/assets/skills/design/references/design-routing.md +207 -0
  76. package/assets/skills/design/references/icon-design.md +122 -0
  77. package/assets/skills/design/references/logo-color-psychology.md +101 -0
  78. package/assets/skills/design/references/logo-design.md +92 -0
  79. package/assets/skills/design/references/logo-prompt-engineering.md +158 -0
  80. package/assets/skills/design/references/logo-style-guide.md +109 -0
  81. package/assets/skills/design/references/slides-copywriting-formulas.md +84 -0
  82. package/assets/skills/design/references/slides-create.md +4 -0
  83. package/assets/skills/design/references/slides-html-template.md +295 -0
  84. package/assets/skills/design/references/slides-layout-patterns.md +137 -0
  85. package/assets/skills/design/references/slides-strategies.md +94 -0
  86. package/assets/skills/design/references/slides.md +42 -0
  87. package/assets/skills/design/references/social-photos-design.md +329 -0
  88. package/assets/skills/design/scripts/cip/core.py +215 -0
  89. package/assets/skills/design/scripts/cip/generate.py +484 -0
  90. package/assets/skills/design/scripts/cip/render-html.py +424 -0
  91. package/assets/skills/design/scripts/cip/search.py +127 -0
  92. package/assets/skills/design/scripts/icon/generate.py +487 -0
  93. package/assets/skills/design/scripts/logo/core.py +175 -0
  94. package/assets/skills/design/scripts/logo/generate.py +362 -0
  95. package/assets/skills/design/scripts/logo/search.py +114 -0
  96. package/assets/skills/design-system/SKILL.md +244 -0
  97. package/assets/skills/design-system/data/slide-backgrounds.csv +11 -0
  98. package/assets/skills/design-system/data/slide-charts.csv +26 -0
  99. package/assets/skills/design-system/data/slide-color-logic.csv +14 -0
  100. package/assets/skills/design-system/data/slide-copy.csv +26 -0
  101. package/assets/skills/design-system/data/slide-layout-logic.csv +16 -0
  102. package/assets/skills/design-system/data/slide-layouts.csv +26 -0
  103. package/assets/skills/design-system/data/slide-strategies.csv +16 -0
  104. package/assets/skills/design-system/data/slide-typography.csv +15 -0
  105. package/assets/skills/design-system/references/component-specs.md +236 -0
  106. package/assets/skills/design-system/references/component-tokens.md +214 -0
  107. package/assets/skills/design-system/references/primitive-tokens.md +203 -0
  108. package/assets/skills/design-system/references/semantic-tokens.md +215 -0
  109. package/assets/skills/design-system/references/states-and-variants.md +241 -0
  110. package/assets/skills/design-system/references/tailwind-integration.md +251 -0
  111. package/assets/skills/design-system/references/token-architecture.md +224 -0
  112. package/assets/skills/design-system/scripts/embed-tokens.cjs +99 -0
  113. package/assets/skills/design-system/scripts/fetch-background.py +317 -0
  114. package/assets/skills/design-system/scripts/generate-slide.py +770 -0
  115. package/assets/skills/design-system/scripts/generate-tokens.cjs +205 -0
  116. package/assets/skills/design-system/scripts/html-token-validator.py +327 -0
  117. package/assets/skills/design-system/scripts/search-slides.py +218 -0
  118. package/assets/skills/design-system/scripts/slide-token-validator.py +35 -0
  119. package/assets/skills/design-system/scripts/slide_search_core.py +453 -0
  120. package/assets/skills/design-system/scripts/tests/test_validate_tokens.py +48 -0
  121. package/assets/skills/design-system/scripts/validate-tokens.cjs +246 -0
  122. package/assets/skills/design-system/templates/design-tokens-starter.json +143 -0
  123. package/assets/skills/slides/SKILL.md +40 -0
  124. package/assets/skills/slides/references/copywriting-formulas.md +84 -0
  125. package/assets/skills/slides/references/create.md +4 -0
  126. package/assets/skills/slides/references/html-template.md +295 -0
  127. package/assets/skills/slides/references/layout-patterns.md +137 -0
  128. package/assets/skills/slides/references/slide-strategies.md +94 -0
  129. package/assets/skills/ui-styling/LICENSE.txt +202 -0
  130. package/assets/skills/ui-styling/SKILL.md +324 -0
  131. package/assets/skills/ui-styling/references/canvas-design-system.md +320 -0
  132. package/assets/skills/ui-styling/references/shadcn-accessibility.md +471 -0
  133. package/assets/skills/ui-styling/references/shadcn-components.md +424 -0
  134. package/assets/skills/ui-styling/references/shadcn-theming.md +373 -0
  135. package/assets/skills/ui-styling/references/tailwind-customization.md +483 -0
  136. package/assets/skills/ui-styling/references/tailwind-responsive.md +382 -0
  137. package/assets/skills/ui-styling/references/tailwind-utilities.md +455 -0
  138. package/assets/skills/ui-styling/scripts/requirements.txt +17 -0
  139. package/assets/skills/ui-styling/scripts/shadcn_add.py +308 -0
  140. package/assets/skills/ui-styling/scripts/tailwind_config_gen.py +473 -0
  141. package/assets/skills/ui-styling/scripts/tests/coverage-ui.json +1 -0
  142. package/assets/skills/ui-styling/scripts/tests/requirements.txt +3 -0
  143. package/assets/skills/ui-styling/scripts/tests/test_shadcn_add.py +266 -0
  144. package/assets/skills/ui-styling/scripts/tests/test_tailwind_config_gen.py +394 -0
  145. package/assets/templates/base/quick-reference.md +297 -0
  146. package/assets/templates/base/skill-content.md +384 -0
  147. package/assets/templates/platforms/agent.json +21 -0
  148. package/assets/templates/platforms/augment.json +18 -0
  149. package/assets/templates/platforms/claude.json +21 -0
  150. package/assets/templates/platforms/codebuddy.json +21 -0
  151. package/assets/templates/platforms/codewhale.json +21 -0
  152. package/assets/templates/platforms/codex.json +21 -0
  153. package/assets/templates/platforms/continue.json +21 -0
  154. package/assets/templates/platforms/copilot.json +21 -0
  155. package/assets/templates/platforms/cursor.json +21 -0
  156. package/assets/templates/platforms/droid.json +21 -0
  157. package/assets/templates/platforms/gemini.json +21 -0
  158. package/assets/templates/platforms/kilocode.json +21 -0
  159. package/assets/templates/platforms/kiro.json +21 -0
  160. package/assets/templates/platforms/opencode.json +21 -0
  161. package/assets/templates/platforms/qoder.json +21 -0
  162. package/assets/templates/platforms/roocode.json +21 -0
  163. package/assets/templates/platforms/trae.json +21 -0
  164. package/assets/templates/platforms/warp.json +18 -0
  165. package/assets/templates/platforms/windsurf.json +21 -0
  166. package/dist/commands/init.d.ts +11 -0
  167. package/dist/commands/init.js +165 -0
  168. package/dist/commands/uninstall.d.ts +7 -0
  169. package/dist/commands/uninstall.js +131 -0
  170. package/dist/commands/update.d.ts +7 -0
  171. package/dist/commands/update.js +73 -0
  172. package/dist/commands/versions.d.ts +5 -0
  173. package/dist/commands/versions.js +36 -0
  174. package/dist/index.d.ts +2 -0
  175. package/dist/index.js +78 -0
  176. package/dist/types/index.d.ts +40 -0
  177. package/dist/types/index.js +25 -0
  178. package/dist/utils/detect.d.ts +8 -0
  179. package/dist/utils/detect.js +115 -0
  180. package/dist/utils/extract.d.ts +15 -0
  181. package/dist/utils/extract.js +125 -0
  182. package/dist/utils/github.d.ts +12 -0
  183. package/dist/utils/github.js +97 -0
  184. package/dist/utils/logger.d.ts +8 -0
  185. package/dist/utils/logger.js +9 -0
  186. package/dist/utils/template.d.ts +50 -0
  187. package/dist/utils/template.js +249 -0
  188. package/package.json +65 -0
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * sync-brand-to-tokens.cjs
4
+ *
5
+ * Syncs brand-guidelines.md colors → design-tokens.json → design-tokens.css
6
+ *
7
+ * Usage:
8
+ * node sync-brand-to-tokens.cjs
9
+ * node sync-brand-to-tokens.cjs --dry-run
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { execFileSync } = require('child_process');
15
+
16
+ // Paths
17
+ const BRAND_GUIDELINES = 'docs/brand-guidelines.md';
18
+ const DESIGN_TOKENS_JSON = 'assets/design-tokens.json';
19
+ const DESIGN_TOKENS_CSS = 'assets/design-tokens.css';
20
+ const GENERATE_TOKENS_SCRIPT = '.claude/skills/design-system/scripts/generate-tokens.cjs';
21
+
22
+ /**
23
+ * Extract color info from brand guidelines markdown
24
+ */
25
+ function extractColorsFromMarkdown(content) {
26
+ const colors = {
27
+ primary: { name: 'primary', shades: {} },
28
+ secondary: { name: 'secondary', shades: {} },
29
+ accent: { name: 'accent', shades: {} }
30
+ };
31
+
32
+ // Match a "| Label | #hex |" markdown table row. Bold around the label
33
+ // (**Label**) is optional, so this handles both the bundled starter template
34
+ // ("| Primary Blue | #2563EB |") and bolded variants.
35
+ const rowRe = /\|\s*\*{0,2}([^*|]+?)\*{0,2}\s*\|\s*#([A-Fa-f0-9]{6})\b/g;
36
+
37
+ // 1) Quick Reference table — hex only, no parenthesized name required.
38
+ const quickRef = {
39
+ primary: /Primary Color\s*\|\s*#([A-Fa-f0-9]{6})/i,
40
+ secondary: /Secondary Color\s*\|\s*#([A-Fa-f0-9]{6})/i,
41
+ accent: /Accent Color\s*\|\s*#([A-Fa-f0-9]{6})/i
42
+ };
43
+ for (const key of Object.keys(quickRef)) {
44
+ const m = content.match(quickRef[key]);
45
+ if (m) colors[key].base = `#${m[1]}`;
46
+ }
47
+
48
+ // 2) Dedicated "### <Role> Colors" tables — assign base/dark/light by the
49
+ // row label keyword.
50
+ const assignFromSection = (heading, target) => {
51
+ const section = content.match(new RegExp(`### ${heading}[\\s\\S]*?(?=\\n###|$)`, 'i'));
52
+ if (!section) return;
53
+ for (const m of section[0].matchAll(rowRe)) {
54
+ const label = m[1].trim().toLowerCase();
55
+ const hex = `#${m[2]}`;
56
+ if (label.includes('dark')) target.dark = hex;
57
+ else if (label.includes('light')) target.light = hex;
58
+ else if (!target.base) target.base = hex;
59
+ }
60
+ };
61
+ assignFromSection('Primary Colors', colors.primary);
62
+ assignFromSection('Secondary Colors', colors.secondary);
63
+ assignFromSection('Accent Colors', colors.accent);
64
+
65
+ // 3) Fallback: an accent swatch may live in another table (the starter
66
+ // lists "Accent Green" under Secondary Colors).
67
+ if (!colors.accent.base) {
68
+ for (const m of content.matchAll(rowRe)) {
69
+ if (m[1].trim().toLowerCase().includes('accent')) {
70
+ colors.accent.base = `#${m[2]}`;
71
+ break;
72
+ }
73
+ }
74
+ }
75
+
76
+ return colors;
77
+ }
78
+
79
+ /**
80
+ * Generate color scale from base color (simple approach)
81
+ */
82
+ function generateColorScale(baseHex, darkHex, lightHex) {
83
+ // Use provided shades or generate approximations
84
+ return {
85
+ "50": { "$value": lightHex || adjustBrightness(baseHex, 0.9), "$type": "color" },
86
+ "100": { "$value": lightHex || adjustBrightness(baseHex, 0.8), "$type": "color" },
87
+ "200": { "$value": adjustBrightness(baseHex, 0.6), "$type": "color" },
88
+ "300": { "$value": adjustBrightness(baseHex, 0.4), "$type": "color" },
89
+ "400": { "$value": adjustBrightness(baseHex, 0.2), "$type": "color" },
90
+ "500": { "$value": baseHex, "$type": "color" },
91
+ "600": { "$value": darkHex || adjustBrightness(baseHex, -0.15), "$type": "color" },
92
+ "700": { "$value": adjustBrightness(baseHex, -0.3), "$type": "color" },
93
+ "800": { "$value": adjustBrightness(baseHex, -0.45), "$type": "color" },
94
+ "900": { "$value": adjustBrightness(baseHex, -0.6), "$type": "color" }
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Adjust hex color brightness
100
+ */
101
+ function adjustBrightness(hex, percent) {
102
+ if (typeof hex !== 'string') return '#000000';
103
+ const num = parseInt(hex.replace('#', ''), 16);
104
+ const r = Math.min(255, Math.max(0, (num >> 16) + Math.round(255 * percent)));
105
+ const g = Math.min(255, Math.max(0, ((num >> 8) & 0x00FF) + Math.round(255 * percent)));
106
+ const b = Math.min(255, Math.max(0, (num & 0x0000FF) + Math.round(255 * percent)));
107
+ return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0').toUpperCase()}`;
108
+ }
109
+
110
+ /**
111
+ * Update design tokens JSON
112
+ */
113
+ function updateDesignTokens(tokens, colors) {
114
+ // Update brand name
115
+ const brandName = `Acme Marketing - ${colors.primary.name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')}`;
116
+ tokens.brand = brandName;
117
+
118
+ // Update primitive colors with new names
119
+ tokens.primitive = tokens.primitive || {};
120
+ const primitiveColors = tokens.primitive.color || {};
121
+
122
+ // Remove old color keys, add new ones
123
+ delete primitiveColors.coral;
124
+ delete primitiveColors.purple;
125
+ delete primitiveColors.mint;
126
+
127
+ // Add new named colors. Skip any role with no base hex rather than crashing
128
+ // on an unexpected guidelines format.
129
+ for (const role of ['primary', 'secondary', 'accent']) {
130
+ const c = colors[role];
131
+ if (!c.base) {
132
+ console.warn(`⚠️ No base hex found for ${role} color — skipping its token scale.`);
133
+ continue;
134
+ }
135
+ primitiveColors[c.name] = generateColorScale(c.base, c.dark, c.light);
136
+ }
137
+
138
+ tokens.primitive.color = primitiveColors;
139
+
140
+ // Update ALL semantic color references
141
+ if (tokens.semantic?.color) {
142
+ const sem = tokens.semantic.color;
143
+ const p = colors.primary.name;
144
+ const s = colors.secondary.name;
145
+ const a = colors.accent.name;
146
+
147
+ // Primary variants
148
+ sem.primary = { "$value": `{primitive.color.${p}.500}`, "$type": "color" };
149
+ sem['primary-hover'] = { "$value": `{primitive.color.${p}.600}`, "$type": "color" };
150
+ sem['primary-active'] = { "$value": `{primitive.color.${p}.700}`, "$type": "color" };
151
+ sem['primary-light'] = { "$value": `{primitive.color.${p}.400}`, "$type": "color" };
152
+ sem['primary-lighter'] = { "$value": `{primitive.color.${p}.100}`, "$type": "color" };
153
+ sem['primary-dark'] = { "$value": `{primitive.color.${p}.600}`, "$type": "color" };
154
+
155
+ // Secondary variants
156
+ sem.secondary = { "$value": `{primitive.color.${s}.500}`, "$type": "color" };
157
+ sem['secondary-hover'] = { "$value": `{primitive.color.${s}.600}`, "$type": "color" };
158
+ sem['secondary-light'] = { "$value": `{primitive.color.${s}.300}`, "$type": "color" };
159
+ sem['secondary-dark'] = { "$value": `{primitive.color.${s}.600}`, "$type": "color" };
160
+
161
+ // Accent variants
162
+ sem.accent = { "$value": `{primitive.color.${a}.500}`, "$type": "color" };
163
+ sem['accent-hover'] = { "$value": `{primitive.color.${a}.600}`, "$type": "color" };
164
+ sem['accent-light'] = { "$value": `{primitive.color.${a}.300}`, "$type": "color" };
165
+
166
+ // Status colors (use accent for success, primary for error/info)
167
+ sem.success = { "$value": `{primitive.color.${a}.500}`, "$type": "color" };
168
+ sem['success-light'] = { "$value": `{primitive.color.${a}.300}`, "$type": "color" };
169
+ sem.error = { "$value": `{primitive.color.${p}.500}`, "$type": "color" };
170
+ sem['error-light'] = { "$value": `{primitive.color.${p}.300}`, "$type": "color" };
171
+ sem.info = { "$value": `{primitive.color.${s}.500}`, "$type": "color" };
172
+ sem['info-light'] = { "$value": `{primitive.color.${s}.300}`, "$type": "color" };
173
+ }
174
+
175
+ // Update component references (button uses primary color with opacity)
176
+ if (tokens.component?.button?.secondary && colors.primary.base) {
177
+ const primaryBase = colors.primary.base;
178
+ tokens.component.button.secondary['bg-hover'] = {
179
+ "$value": `${primaryBase}1A`,
180
+ "$type": "color"
181
+ };
182
+ }
183
+
184
+ return tokens;
185
+ }
186
+
187
+ /**
188
+ * Main
189
+ */
190
+ function main() {
191
+ const dryRun = process.argv.includes('--dry-run');
192
+
193
+ console.log('🔄 Syncing brand guidelines → design tokens\n');
194
+
195
+ // Read brand guidelines
196
+ const guidelinesPath = path.resolve(process.cwd(), BRAND_GUIDELINES);
197
+ if (!fs.existsSync(guidelinesPath)) {
198
+ console.error(`❌ Brand guidelines not found: ${guidelinesPath}`);
199
+ process.exit(1);
200
+ }
201
+ const guidelinesContent = fs.readFileSync(guidelinesPath, 'utf-8');
202
+
203
+ // Extract colors
204
+ const colors = extractColorsFromMarkdown(guidelinesContent);
205
+ console.log('📊 Extracted colors:');
206
+ console.log(` Primary: ${colors.primary.name} (${colors.primary.base})`);
207
+ console.log(` Secondary: ${colors.secondary.name} (${colors.secondary.base})`);
208
+ console.log(` Accent: ${colors.accent.name} (${colors.accent.base})\n`);
209
+
210
+ // Read existing tokens
211
+ const tokensPath = path.resolve(process.cwd(), DESIGN_TOKENS_JSON);
212
+ let tokens = {};
213
+ if (fs.existsSync(tokensPath)) {
214
+ tokens = JSON.parse(fs.readFileSync(tokensPath, 'utf-8'));
215
+ }
216
+
217
+ // Update tokens
218
+ tokens = updateDesignTokens(tokens, colors);
219
+
220
+ if (dryRun) {
221
+ console.log('📋 Would update design-tokens.json:');
222
+ console.log(JSON.stringify(tokens.primitive.color, null, 2).slice(0, 500) + '...');
223
+ console.log('\n⏭️ Dry run - no files changed');
224
+ return;
225
+ }
226
+
227
+ // Write updated tokens
228
+ fs.writeFileSync(tokensPath, JSON.stringify(tokens, null, 2));
229
+ console.log(`✅ Updated: ${DESIGN_TOKENS_JSON}`);
230
+
231
+ // Regenerate CSS
232
+ const generateScript = path.resolve(process.cwd(), GENERATE_TOKENS_SCRIPT);
233
+ if (fs.existsSync(generateScript)) {
234
+ try {
235
+ execFileSync('node', [generateScript, '--config', DESIGN_TOKENS_JSON, '-o', DESIGN_TOKENS_CSS], {
236
+ cwd: process.cwd(),
237
+ stdio: 'inherit'
238
+ });
239
+ console.log(`✅ Regenerated: ${DESIGN_TOKENS_CSS}`);
240
+ } catch (e) {
241
+ console.error('⚠️ Failed to regenerate CSS:', e.message);
242
+ }
243
+ }
244
+
245
+ console.log('\n✨ Brand sync complete!');
246
+ }
247
+
248
+ main();
@@ -0,0 +1,52 @@
1
+ """Regression test for sync-brand-to-tokens.cjs.
2
+
3
+ The color parser required a parenthesized name in the Quick Reference row
4
+ (`#2563EB (name)`) and a bolded label in the color tables (`**Primary Blue**`),
5
+ neither of which the bundled starter template uses. As a result the base hex
6
+ came back `undefined` and `adjustBrightness(undefined)` threw a TypeError —
7
+ i.e. the script crashed on its own documented happy path. This test runs the
8
+ sync against the bundled starter template and asserts it completes and writes
9
+ the expected base colors. It is pytest-based so the existing pytest CI runs it.
10
+ """
11
+
12
+ import json
13
+ import shutil
14
+ import subprocess
15
+ from pathlib import Path
16
+
17
+ import pytest
18
+
19
+ SCRIPTS = Path(__file__).resolve().parent.parent
20
+ SCRIPT = SCRIPTS / "sync-brand-to-tokens.cjs"
21
+ BRAND_STARTER = SCRIPTS.parent / "templates" / "brand-guidelines-starter.md"
22
+ TOKENS_STARTER = (
23
+ SCRIPTS.parent.parent / "design-system" / "templates" / "design-tokens-starter.json"
24
+ )
25
+
26
+
27
+ def test_sync_parses_bundled_starter_template(tmp_path):
28
+ node = shutil.which("node")
29
+ if not node:
30
+ pytest.skip("node not available")
31
+
32
+ (tmp_path / "docs").mkdir()
33
+ (tmp_path / "assets").mkdir()
34
+ shutil.copy(BRAND_STARTER, tmp_path / "docs" / "brand-guidelines.md")
35
+ shutil.copy(TOKENS_STARTER, tmp_path / "assets" / "design-tokens.json")
36
+
37
+ result = subprocess.run(
38
+ [node, str(SCRIPT)],
39
+ cwd=tmp_path,
40
+ capture_output=True,
41
+ text=True,
42
+ )
43
+
44
+ # Must not crash (the bug raised an unhandled TypeError).
45
+ assert "TypeError" not in result.stderr, result.stderr
46
+ assert result.returncode == 0, result.stderr + result.stdout
47
+
48
+ tokens = json.loads((tmp_path / "assets" / "design-tokens.json").read_text())
49
+ primitive = tokens["primitive"]["color"]
50
+ assert primitive["primary"]["500"]["$value"] == "#2563EB"
51
+ assert primitive["secondary"]["500"]["$value"] == "#8B5CF6"
52
+ assert primitive["accent"]["500"]["$value"] == "#10B981"
@@ -0,0 +1,387 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * validate-asset.cjs
4
+ *
5
+ * Validates marketing assets against brand guidelines.
6
+ * Checks: file naming, dimensions, file size, metadata.
7
+ *
8
+ * Usage:
9
+ * node validate-asset.cjs <asset-path>
10
+ * node validate-asset.cjs <asset-path> --json
11
+ * node validate-asset.cjs <asset-path> --fix
12
+ *
13
+ * For color validation of images, use with extract-colors.cjs
14
+ */
15
+
16
+ const fs = require("fs");
17
+ const path = require("path");
18
+
19
+ // Validation rules
20
+ const RULES = {
21
+ naming: {
22
+ pattern: /^[a-z]+_[a-z0-9-]+_[a-z0-9-]+_\d{8}(_[a-z0-9-]+)?\.[a-z]+$/,
23
+ description:
24
+ "{type}_{campaign}_{description}_{timestamp}_{variant}.{ext}",
25
+ examples: [
26
+ "banner_claude-launch_hero-image_20251209.png",
27
+ "logo_brand-refresh_horizontal_20251209_dark.svg",
28
+ ],
29
+ },
30
+ dimensions: {
31
+ banner: { minWidth: 600, minHeight: 300 },
32
+ logo: { minWidth: 100, minHeight: 100 },
33
+ design: { minWidth: 800, minHeight: 600 },
34
+ video: { minWidth: 640, minHeight: 480 },
35
+ default: { minWidth: 100, minHeight: 100 },
36
+ },
37
+ fileSize: {
38
+ image: { max: 5 * 1024 * 1024, recommended: 1 * 1024 * 1024 },
39
+ video: { max: 100 * 1024 * 1024, recommended: 50 * 1024 * 1024 },
40
+ svg: { max: 500 * 1024, recommended: 100 * 1024 },
41
+ },
42
+ formats: {
43
+ image: ["png", "jpg", "jpeg", "webp", "gif"],
44
+ vector: ["svg"],
45
+ video: ["mp4", "mov", "webm"],
46
+ document: ["pdf", "psd", "ai", "fig"],
47
+ },
48
+ };
49
+
50
+ /**
51
+ * Parse asset filename
52
+ */
53
+ function parseFilename(filename) {
54
+ const parts = filename.replace(/\.[^.]+$/, "").split("_");
55
+
56
+ if (parts.length < 4) {
57
+ return null;
58
+ }
59
+
60
+ return {
61
+ type: parts[0],
62
+ campaign: parts[1],
63
+ description: parts[2],
64
+ timestamp: parts[3],
65
+ variant: parts.length > 4 ? parts[4] : null,
66
+ extension: path.extname(filename).slice(1).toLowerCase(),
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Validate filename convention
72
+ */
73
+ function validateFilename(filename) {
74
+ const issues = [];
75
+ const suggestions = [];
76
+
77
+ // Check pattern match
78
+ if (!RULES.naming.pattern.test(filename)) {
79
+ issues.push("Filename does not match naming convention");
80
+ suggestions.push(`Expected format: ${RULES.naming.description}`);
81
+ suggestions.push(`Examples: ${RULES.naming.examples.join(", ")}`);
82
+ }
83
+
84
+ // Parse and check components
85
+ const parsed = parseFilename(filename);
86
+ if (parsed) {
87
+ // Check timestamp format
88
+ if (!/^\d{8}$/.test(parsed.timestamp)) {
89
+ issues.push("Timestamp should be YYYYMMDD format");
90
+ }
91
+
92
+ // Check kebab-case for campaign and description
93
+ if (parsed.campaign && !/^[a-z0-9-]+$/.test(parsed.campaign)) {
94
+ issues.push("Campaign name should be kebab-case");
95
+ }
96
+
97
+ if (parsed.description && !/^[a-z0-9-]+$/.test(parsed.description)) {
98
+ issues.push("Description should be kebab-case");
99
+ }
100
+
101
+ // Check valid type
102
+ const validTypes = [
103
+ "banner",
104
+ "logo",
105
+ "design",
106
+ "video",
107
+ "infographic",
108
+ "icon",
109
+ "photo",
110
+ ];
111
+ if (!validTypes.includes(parsed.type)) {
112
+ suggestions.push(`Consider using type: ${validTypes.join(", ")}`);
113
+ }
114
+ }
115
+
116
+ return { valid: issues.length === 0, issues, suggestions, parsed };
117
+ }
118
+
119
+ /**
120
+ * Validate file size
121
+ */
122
+ function validateFileSize(filepath, extension) {
123
+ const issues = [];
124
+ const warnings = [];
125
+
126
+ const stats = fs.statSync(filepath);
127
+ const size = stats.size;
128
+
129
+ let limits;
130
+ if (RULES.formats.video.includes(extension)) {
131
+ limits = RULES.fileSize.video;
132
+ } else if (extension === "svg") {
133
+ limits = RULES.fileSize.svg;
134
+ } else {
135
+ limits = RULES.fileSize.image;
136
+ }
137
+
138
+ if (size > limits.max) {
139
+ issues.push(
140
+ `File size (${formatBytes(size)}) exceeds maximum (${formatBytes(
141
+ limits.max
142
+ )})`
143
+ );
144
+ } else if (size > limits.recommended) {
145
+ warnings.push(
146
+ `File size (${formatBytes(size)}) exceeds recommended (${formatBytes(
147
+ limits.recommended
148
+ )})`
149
+ );
150
+ }
151
+
152
+ return { valid: issues.length === 0, issues, warnings, size };
153
+ }
154
+
155
+ /**
156
+ * Validate file format
157
+ */
158
+ function validateFormat(extension) {
159
+ const issues = [];
160
+ const info = { category: null };
161
+
162
+ const allFormats = [
163
+ ...RULES.formats.image,
164
+ ...RULES.formats.vector,
165
+ ...RULES.formats.video,
166
+ ...RULES.formats.document,
167
+ ];
168
+
169
+ if (!allFormats.includes(extension)) {
170
+ issues.push(`Unsupported file format: .${extension}`);
171
+ return { valid: false, issues, info };
172
+ }
173
+
174
+ // Determine category
175
+ if (RULES.formats.image.includes(extension)) info.category = "image";
176
+ else if (RULES.formats.vector.includes(extension)) info.category = "vector";
177
+ else if (RULES.formats.video.includes(extension)) info.category = "video";
178
+ else if (RULES.formats.document.includes(extension))
179
+ info.category = "document";
180
+
181
+ return { valid: true, issues, info };
182
+ }
183
+
184
+ /**
185
+ * Check if asset exists in manifest
186
+ */
187
+ function checkManifest(filepath) {
188
+ const manifestPath = path.join(process.cwd(), ".assets", "manifest.json");
189
+
190
+ if (!fs.existsSync(manifestPath)) {
191
+ return { registered: false, message: "Manifest not found" };
192
+ }
193
+
194
+ try {
195
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
196
+ const relativePath = path.relative(process.cwd(), filepath);
197
+ const found = manifest.assets?.find(
198
+ (a) => a.path === relativePath || a.path === filepath
199
+ );
200
+
201
+ return {
202
+ registered: !!found,
203
+ message: found ? "Asset registered in manifest" : "Asset not in manifest",
204
+ asset: found,
205
+ };
206
+ } catch {
207
+ return { registered: false, message: "Error reading manifest" };
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Generate suggested filename
213
+ */
214
+ function suggestFilename(original, parsed) {
215
+ if (!parsed) return null;
216
+
217
+ const today = new Date().toISOString().slice(0, 10).replace(/-/g, "");
218
+ const type = parsed.type || "asset";
219
+ const campaign = parsed.campaign || "general";
220
+ const description = parsed.description || "untitled";
221
+ const ext = parsed.extension || "png";
222
+
223
+ return `${type}_${campaign}_${description}_${today}.${ext}`;
224
+ }
225
+
226
+ /**
227
+ * Format bytes to human readable
228
+ */
229
+ function formatBytes(bytes) {
230
+ if (bytes === 0) return "0 Bytes";
231
+ const k = 1024;
232
+ const sizes = ["Bytes", "KB", "MB", "GB"];
233
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
234
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
235
+ }
236
+
237
+ /**
238
+ * Main validation function
239
+ */
240
+ function validateAsset(assetPath) {
241
+ const results = {
242
+ path: assetPath,
243
+ filename: path.basename(assetPath),
244
+ valid: true,
245
+ issues: [],
246
+ warnings: [],
247
+ suggestions: [],
248
+ checks: {},
249
+ };
250
+
251
+ // Check file exists
252
+ if (!fs.existsSync(assetPath)) {
253
+ results.valid = false;
254
+ results.issues.push(`File not found: ${assetPath}`);
255
+ return results;
256
+ }
257
+
258
+ const filename = path.basename(assetPath);
259
+ const extension = path.extname(filename).slice(1).toLowerCase();
260
+
261
+ // 1. Validate filename
262
+ const filenameResult = validateFilename(filename);
263
+ results.checks.filename = filenameResult;
264
+ if (!filenameResult.valid) {
265
+ results.issues.push(...filenameResult.issues);
266
+ results.suggestions.push(...filenameResult.suggestions);
267
+ }
268
+
269
+ // 2. Validate format
270
+ const formatResult = validateFormat(extension);
271
+ results.checks.format = formatResult;
272
+ if (!formatResult.valid) {
273
+ results.issues.push(...formatResult.issues);
274
+ }
275
+
276
+ // 3. Validate file size
277
+ const sizeResult = validateFileSize(assetPath, extension);
278
+ results.checks.fileSize = sizeResult;
279
+ if (!sizeResult.valid) {
280
+ results.issues.push(...sizeResult.issues);
281
+ }
282
+ results.warnings.push(...sizeResult.warnings);
283
+
284
+ // 4. Check manifest registration
285
+ const manifestResult = checkManifest(assetPath);
286
+ results.checks.manifest = manifestResult;
287
+ if (!manifestResult.registered) {
288
+ results.warnings.push("Asset not registered in manifest.json");
289
+ results.suggestions.push(
290
+ "Register asset in .assets/manifest.json for tracking"
291
+ );
292
+ }
293
+
294
+ // 5. Suggest corrected filename if needed
295
+ if (!filenameResult.valid && filenameResult.parsed) {
296
+ const suggested = suggestFilename(filename, filenameResult.parsed);
297
+ if (suggested) {
298
+ results.suggestions.push(`Suggested filename: ${suggested}`);
299
+ }
300
+ }
301
+
302
+ // Overall validity
303
+ results.valid = results.issues.length === 0;
304
+
305
+ return results;
306
+ }
307
+
308
+ /**
309
+ * Format output for console
310
+ */
311
+ function formatOutput(results) {
312
+ const lines = [];
313
+
314
+ lines.push("\n" + "=".repeat(60));
315
+ lines.push(`ASSET VALIDATION: ${results.filename}`);
316
+ lines.push("=".repeat(60));
317
+
318
+ lines.push(`\nStatus: ${results.valid ? "PASS" : "FAIL"}`);
319
+ lines.push(`Path: ${results.path}`);
320
+
321
+ if (results.issues.length > 0) {
322
+ lines.push("\nISSUES:");
323
+ results.issues.forEach((issue) => lines.push(` - ${issue}`));
324
+ }
325
+
326
+ if (results.warnings.length > 0) {
327
+ lines.push("\nWARNINGS:");
328
+ results.warnings.forEach((warning) => lines.push(` - ${warning}`));
329
+ }
330
+
331
+ if (results.suggestions.length > 0) {
332
+ lines.push("\nSUGGESTIONS:");
333
+ results.suggestions.forEach((suggestion) =>
334
+ lines.push(` - ${suggestion}`)
335
+ );
336
+ }
337
+
338
+ // File size info
339
+ if (results.checks.fileSize?.size) {
340
+ lines.push(`\nFile Size: ${formatBytes(results.checks.fileSize.size)}`);
341
+ }
342
+
343
+ lines.push("\n" + "=".repeat(60));
344
+
345
+ return lines.join("\n");
346
+ }
347
+
348
+ /**
349
+ * Main
350
+ */
351
+ function main() {
352
+ const args = process.argv.slice(2);
353
+ const jsonOutput = args.includes("--json");
354
+ const assetPath = args.find((a) => !a.startsWith("--"));
355
+
356
+ if (!assetPath) {
357
+ console.error("Usage: node validate-asset.cjs <asset-path> [--json]");
358
+ console.error("\nExamples:");
359
+ console.error(
360
+ " node validate-asset.cjs assets/banners/social-media/banner_launch_hero_20251209.png"
361
+ );
362
+ console.error(
363
+ " node validate-asset.cjs assets/logos/icon-only/logo-icon.svg --json"
364
+ );
365
+ process.exit(1);
366
+ }
367
+
368
+ // Resolve path
369
+ const resolvedPath = path.isAbsolute(assetPath)
370
+ ? assetPath
371
+ : path.join(process.cwd(), assetPath);
372
+
373
+ // Validate
374
+ const results = validateAsset(resolvedPath);
375
+
376
+ // Output
377
+ if (jsonOutput) {
378
+ console.log(JSON.stringify(results, null, 2));
379
+ } else {
380
+ console.log(formatOutput(results));
381
+ }
382
+
383
+ // Exit with appropriate code
384
+ process.exit(results.valid ? 0 : 1);
385
+ }
386
+
387
+ main();