stella-coder 3.9.2 → 5.0.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 (176) hide show
  1. package/.mcp.json +11 -0
  2. package/.opencode/skills/banner-design/SKILL.md +196 -0
  3. package/.opencode/skills/banner-design/references/banner-sizes-and-styles.md +118 -0
  4. package/.opencode/skills/brand/SKILL.md +97 -0
  5. package/.opencode/skills/brand/references/approval-checklist.md +169 -0
  6. package/.opencode/skills/brand/references/asset-organization.md +157 -0
  7. package/.opencode/skills/brand/references/brand-guideline-template.md +140 -0
  8. package/.opencode/skills/brand/references/color-palette-management.md +186 -0
  9. package/.opencode/skills/brand/references/consistency-checklist.md +94 -0
  10. package/.opencode/skills/brand/references/logo-usage-rules.md +185 -0
  11. package/.opencode/skills/brand/references/messaging-framework.md +85 -0
  12. package/.opencode/skills/brand/references/typography-specifications.md +214 -0
  13. package/.opencode/skills/brand/references/update.md +118 -0
  14. package/.opencode/skills/brand/references/visual-identity.md +96 -0
  15. package/.opencode/skills/brand/references/voice-framework.md +88 -0
  16. package/.opencode/skills/brand/scripts/extract-colors.cjs +341 -0
  17. package/.opencode/skills/brand/scripts/inject-brand-context.cjs +349 -0
  18. package/.opencode/skills/brand/scripts/sync-brand-to-tokens.cjs +248 -0
  19. package/.opencode/skills/brand/scripts/tests/test_sync_brand_to_tokens.py +52 -0
  20. package/.opencode/skills/brand/scripts/validate-asset.cjs +387 -0
  21. package/.opencode/skills/brand/templates/brand-guidelines-starter.md +275 -0
  22. package/.opencode/skills/design/SKILL.md +313 -0
  23. package/.opencode/skills/design/data/cip/deliverables.csv +51 -0
  24. package/.opencode/skills/design/data/cip/industries.csv +21 -0
  25. package/.opencode/skills/design/data/cip/mockup-contexts.csv +21 -0
  26. package/.opencode/skills/design/data/cip/styles.csv +21 -0
  27. package/.opencode/skills/design/data/icon/styles.csv +16 -0
  28. package/.opencode/skills/design/data/logo/colors.csv +56 -0
  29. package/.opencode/skills/design/data/logo/industries.csv +56 -0
  30. package/.opencode/skills/design/data/logo/styles.csv +56 -0
  31. package/.opencode/skills/design/references/banner-sizes-and-styles.md +118 -0
  32. package/.opencode/skills/design/references/cip-deliverable-guide.md +95 -0
  33. package/.opencode/skills/design/references/cip-design.md +121 -0
  34. package/.opencode/skills/design/references/cip-prompt-engineering.md +84 -0
  35. package/.opencode/skills/design/references/cip-style-guide.md +68 -0
  36. package/.opencode/skills/design/references/design-routing.md +207 -0
  37. package/.opencode/skills/design/references/icon-design.md +122 -0
  38. package/.opencode/skills/design/references/logo-color-psychology.md +101 -0
  39. package/.opencode/skills/design/references/logo-design.md +92 -0
  40. package/.opencode/skills/design/references/logo-prompt-engineering.md +158 -0
  41. package/.opencode/skills/design/references/logo-style-guide.md +109 -0
  42. package/.opencode/skills/design/references/slides-copywriting-formulas.md +84 -0
  43. package/.opencode/skills/design/references/slides-create.md +4 -0
  44. package/.opencode/skills/design/references/slides-html-template.md +295 -0
  45. package/.opencode/skills/design/references/slides-layout-patterns.md +137 -0
  46. package/.opencode/skills/design/references/slides-strategies.md +94 -0
  47. package/.opencode/skills/design/references/slides.md +42 -0
  48. package/.opencode/skills/design/references/social-photos-design.md +329 -0
  49. package/.opencode/skills/design/scripts/cip/core.py +215 -0
  50. package/.opencode/skills/design/scripts/cip/generate.py +484 -0
  51. package/.opencode/skills/design/scripts/cip/render-html.py +424 -0
  52. package/.opencode/skills/design/scripts/cip/search.py +127 -0
  53. package/.opencode/skills/design/scripts/icon/generate.py +487 -0
  54. package/.opencode/skills/design/scripts/logo/core.py +175 -0
  55. package/.opencode/skills/design/scripts/logo/generate.py +362 -0
  56. package/.opencode/skills/design/scripts/logo/search.py +114 -0
  57. package/.opencode/skills/design-system/SKILL.md +244 -0
  58. package/.opencode/skills/design-system/data/slide-backgrounds.csv +11 -0
  59. package/.opencode/skills/design-system/data/slide-charts.csv +26 -0
  60. package/.opencode/skills/design-system/data/slide-color-logic.csv +14 -0
  61. package/.opencode/skills/design-system/data/slide-copy.csv +26 -0
  62. package/.opencode/skills/design-system/data/slide-layout-logic.csv +16 -0
  63. package/.opencode/skills/design-system/data/slide-layouts.csv +26 -0
  64. package/.opencode/skills/design-system/data/slide-strategies.csv +16 -0
  65. package/.opencode/skills/design-system/data/slide-typography.csv +15 -0
  66. package/.opencode/skills/design-system/references/component-specs.md +236 -0
  67. package/.opencode/skills/design-system/references/component-tokens.md +214 -0
  68. package/.opencode/skills/design-system/references/primitive-tokens.md +203 -0
  69. package/.opencode/skills/design-system/references/semantic-tokens.md +215 -0
  70. package/.opencode/skills/design-system/references/states-and-variants.md +241 -0
  71. package/.opencode/skills/design-system/references/tailwind-integration.md +251 -0
  72. package/.opencode/skills/design-system/references/token-architecture.md +224 -0
  73. package/.opencode/skills/design-system/scripts/embed-tokens.cjs +99 -0
  74. package/.opencode/skills/design-system/scripts/fetch-background.py +317 -0
  75. package/.opencode/skills/design-system/scripts/generate-slide.py +770 -0
  76. package/.opencode/skills/design-system/scripts/generate-tokens.cjs +205 -0
  77. package/.opencode/skills/design-system/scripts/html-token-validator.py +327 -0
  78. package/.opencode/skills/design-system/scripts/search-slides.py +218 -0
  79. package/.opencode/skills/design-system/scripts/slide-token-validator.py +35 -0
  80. package/.opencode/skills/design-system/scripts/slide_search_core.py +453 -0
  81. package/.opencode/skills/design-system/scripts/tests/test_validate_tokens.py +48 -0
  82. package/.opencode/skills/design-system/scripts/validate-tokens.cjs +246 -0
  83. package/.opencode/skills/design-system/templates/design-tokens-starter.json +143 -0
  84. package/.opencode/skills/slides/SKILL.md +40 -0
  85. package/.opencode/skills/slides/references/copywriting-formulas.md +84 -0
  86. package/.opencode/skills/slides/references/create.md +4 -0
  87. package/.opencode/skills/slides/references/html-template.md +295 -0
  88. package/.opencode/skills/slides/references/layout-patterns.md +137 -0
  89. package/.opencode/skills/slides/references/slide-strategies.md +94 -0
  90. package/.opencode/skills/ui-styling/LICENSE.txt +202 -0
  91. package/.opencode/skills/ui-styling/SKILL.md +324 -0
  92. package/.opencode/skills/ui-styling/references/canvas-design-system.md +320 -0
  93. package/.opencode/skills/ui-styling/references/shadcn-accessibility.md +471 -0
  94. package/.opencode/skills/ui-styling/references/shadcn-components.md +424 -0
  95. package/.opencode/skills/ui-styling/references/shadcn-theming.md +373 -0
  96. package/.opencode/skills/ui-styling/references/tailwind-customization.md +483 -0
  97. package/.opencode/skills/ui-styling/references/tailwind-responsive.md +382 -0
  98. package/.opencode/skills/ui-styling/references/tailwind-utilities.md +455 -0
  99. package/.opencode/skills/ui-styling/scripts/requirements.txt +17 -0
  100. package/.opencode/skills/ui-styling/scripts/shadcn_add.py +308 -0
  101. package/.opencode/skills/ui-styling/scripts/tailwind_config_gen.py +473 -0
  102. package/.opencode/skills/ui-styling/scripts/tests/coverage-ui.json +1 -0
  103. package/.opencode/skills/ui-styling/scripts/tests/requirements.txt +3 -0
  104. package/.opencode/skills/ui-styling/scripts/tests/test_shadcn_add.py +266 -0
  105. package/.opencode/skills/ui-styling/scripts/tests/test_tailwind_config_gen.py +394 -0
  106. package/.opencode/skills/ui-ux-pro-max/SKILL.md +396 -0
  107. package/.opencode/skills/ui-ux-pro-max/data/_sync_all.py +414 -0
  108. package/.opencode/skills/ui-ux-pro-max/data/app-interface.csv +31 -0
  109. package/.opencode/skills/ui-ux-pro-max/data/charts.csv +26 -0
  110. package/.opencode/skills/ui-ux-pro-max/data/colors.csv +193 -0
  111. package/.opencode/skills/ui-ux-pro-max/data/design.csv +1776 -0
  112. package/.opencode/skills/ui-ux-pro-max/data/draft.csv +1779 -0
  113. package/.opencode/skills/ui-ux-pro-max/data/google-fonts.csv +1924 -0
  114. package/.opencode/skills/ui-ux-pro-max/data/icons.csv +106 -0
  115. package/.opencode/skills/ui-ux-pro-max/data/landing.csv +35 -0
  116. package/.opencode/skills/ui-ux-pro-max/data/motion.csv +17 -0
  117. package/.opencode/skills/ui-ux-pro-max/data/products.csv +193 -0
  118. package/.opencode/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
  119. package/.opencode/skills/ui-ux-pro-max/data/stacks/angular.csv +51 -0
  120. package/.opencode/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
  121. package/.opencode/skills/ui-ux-pro-max/data/stacks/avalonia.csv +57 -0
  122. package/.opencode/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  123. package/.opencode/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  124. package/.opencode/skills/ui-ux-pro-max/data/stacks/javafx.csv +76 -0
  125. package/.opencode/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
  126. package/.opencode/skills/ui-ux-pro-max/data/stacks/laravel.csv +51 -0
  127. package/.opencode/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  128. package/.opencode/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +71 -0
  129. package/.opencode/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  130. package/.opencode/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  131. package/.opencode/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
  132. package/.opencode/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
  133. package/.opencode/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  134. package/.opencode/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  135. package/.opencode/skills/ui-ux-pro-max/data/stacks/threejs.csv +54 -0
  136. package/.opencode/skills/ui-ux-pro-max/data/stacks/uno.csv +60 -0
  137. package/.opencode/skills/ui-ux-pro-max/data/stacks/uwp.csv +56 -0
  138. package/.opencode/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  139. package/.opencode/skills/ui-ux-pro-max/data/stacks/winui.csv +60 -0
  140. package/.opencode/skills/ui-ux-pro-max/data/stacks/wpf.csv +57 -0
  141. package/.opencode/skills/ui-ux-pro-max/data/styles.csv +85 -0
  142. package/.opencode/skills/ui-ux-pro-max/data/typography.csv +75 -0
  143. package/.opencode/skills/ui-ux-pro-max/data/ui-reasoning.csv +162 -0
  144. package/.opencode/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  145. package/.opencode/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-312.pyc +0 -0
  146. package/.opencode/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-312.pyc +0 -0
  147. package/.opencode/skills/ui-ux-pro-max/scripts/core.py +274 -0
  148. package/.opencode/skills/ui-ux-pro-max/scripts/design_system.py +1329 -0
  149. package/.opencode/skills/ui-ux-pro-max/scripts/search.py +127 -0
  150. package/README.md +25 -30
  151. package/STELLA.md +1 -1
  152. package/dist/antimalware/database.mjs +871 -0
  153. package/dist/antimalware/index.mjs +8 -0
  154. package/dist/antimalware/scanner.mjs +591 -0
  155. package/dist/antimalware/ui.mjs +570 -0
  156. package/dist/banner.mjs +46 -0
  157. package/dist/index.mjs +3073 -0
  158. package/dist/markdown.mjs +100 -0
  159. package/dist/sea-config.json +4 -0
  160. package/dist/security.mjs +237 -0
  161. package/dist/theme.mjs +89 -0
  162. package/dist/tools.mjs +3145 -0
  163. package/install-stella.bat +44 -0
  164. package/package.json +5 -4
  165. package/stella-cli/coding-brain.mjs +753 -0
  166. package/stella-cli/index.mjs +931 -15
  167. package/stella-cli/mcp.mjs +296 -0
  168. package/stella-cli/presentations.mjs +1106 -0
  169. package/stella-cli/subagents.mjs +142 -0
  170. package/stella-cli/telegram-bot.mjs +824 -0
  171. package/stella-cli/tg-bg.bat +13 -0
  172. package/stella-cli/tg-server.bat +15 -0
  173. package/stella-cli/tg-server.mjs +116 -0
  174. package/stella-cli/theme.mjs +6 -4
  175. package/create_cats_pptx.py +0 -121
  176. package/hello.py +0 -2
@@ -0,0 +1,349 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * inject-brand-context.cjs
4
+ *
5
+ * Extracts brand context from markdown brand guidelines
6
+ * and outputs a formatted system prompt addition.
7
+ *
8
+ * Usage:
9
+ * node inject-brand-context.cjs [path-to-guidelines]
10
+ * node inject-brand-context.cjs --json [path-to-guidelines]
11
+ *
12
+ * Default path: docs/brand-guidelines.md
13
+ */
14
+
15
+ const fs = require("fs");
16
+ const path = require("path");
17
+
18
+ // Default brand guidelines path
19
+ const DEFAULT_GUIDELINES_PATH = "docs/brand-guidelines.md";
20
+
21
+ /**
22
+ * Extract hex colors from text
23
+ */
24
+ function extractHexColors(text) {
25
+ const hexPattern = /#[0-9A-Fa-f]{6}\b/g;
26
+ return [...new Set(text.match(hexPattern) || [])];
27
+ }
28
+
29
+ /**
30
+ * Extract color data from markdown table
31
+ */
32
+ function extractColorsFromTable(content) {
33
+ const colors = {
34
+ primary: [],
35
+ secondary: [],
36
+ neutral: [],
37
+ semantic: [],
38
+ };
39
+
40
+ // Find color tables
41
+ const primaryMatch = content.match(
42
+ /### Primary Colors[\s\S]*?\|[\s\S]*?(?=###|$)/i
43
+ );
44
+ const secondaryMatch = content.match(
45
+ /### Secondary Colors[\s\S]*?\|[\s\S]*?(?=###|$)/i
46
+ );
47
+ const neutralMatch = content.match(
48
+ /### Neutral[\s\S]*?\|[\s\S]*?(?=###|$)/i
49
+ );
50
+ const semanticMatch = content.match(
51
+ /### Semantic[\s\S]*?\|[\s\S]*?(?=###|$)/i
52
+ );
53
+
54
+ if (primaryMatch) colors.primary = extractHexColors(primaryMatch[0]);
55
+ if (secondaryMatch) colors.secondary = extractHexColors(secondaryMatch[0]);
56
+ if (neutralMatch) colors.neutral = extractHexColors(neutralMatch[0]);
57
+ if (semanticMatch) colors.semantic = extractHexColors(semanticMatch[0]);
58
+
59
+ return colors;
60
+ }
61
+
62
+ /**
63
+ * Extract typography info
64
+ */
65
+ function extractTypography(content) {
66
+ const typography = {
67
+ heading: null,
68
+ body: null,
69
+ mono: null,
70
+ };
71
+
72
+ // Look for font definitions
73
+ const headingMatch = content.match(/--font-heading:\s*['"]([^'"]+)['"]/);
74
+ const bodyMatch = content.match(/--font-body:\s*['"]([^'"]+)['"]/);
75
+ const monoMatch = content.match(/--font-mono:\s*['"]([^'"]+)['"]/);
76
+
77
+ // Fallback: look in tables
78
+ const fontStackMatch = content.match(/### Font Stack[\s\S]*?(?=###|##|$)/i);
79
+ if (fontStackMatch) {
80
+ const stackText = fontStackMatch[0];
81
+ const headingAlt = stackText.match(/heading[^']*['"]([^'"]+)['"]/i);
82
+ const bodyAlt = stackText.match(/body[^']*['"]([^'"]+)['"]/i);
83
+
84
+ if (headingAlt) typography.heading = headingAlt[1];
85
+ if (bodyAlt) typography.body = bodyAlt[1];
86
+ }
87
+
88
+ if (headingMatch) typography.heading = headingMatch[1];
89
+ if (bodyMatch) typography.body = bodyMatch[1];
90
+ if (monoMatch) typography.mono = monoMatch[1];
91
+
92
+ return typography;
93
+ }
94
+
95
+ /**
96
+ * Extract voice/tone information
97
+ */
98
+ function extractVoice(content) {
99
+ const voice = {
100
+ traits: [],
101
+ prohibited: [],
102
+ personality: "",
103
+ };
104
+
105
+ // Extract personality traits from table
106
+ const personalityMatch = content.match(
107
+ /### Brand Personality[\s\S]*?\|[\s\S]*?(?=###|##|$)/i
108
+ );
109
+ if (personalityMatch) {
110
+ const traits = personalityMatch[0].match(
111
+ /\*\*([^*]+)\*\*\s*\|\s*([^|]+)/g
112
+ );
113
+ if (traits) {
114
+ voice.traits = traits.map((t) => {
115
+ const match = t.match(/\*\*([^*]+)\*\*/);
116
+ return match ? match[1].trim() : "";
117
+ }).filter(Boolean);
118
+ }
119
+ }
120
+
121
+ // Extract prohibited terms
122
+ const prohibitedMatch = content.match(
123
+ /### Prohibited[\s\S]*?(?=###|##|$)/i
124
+ );
125
+ if (prohibitedMatch) {
126
+ const terms = prohibitedMatch[0].match(/\|\s*([^|]+)\s*\|/g);
127
+ if (terms) {
128
+ voice.prohibited = terms
129
+ .map((t) => t.replace(/\|/g, "").trim())
130
+ .filter((t) => t && !t.includes("Avoid") && !t.includes("---"));
131
+ }
132
+ }
133
+
134
+ // Fallback: look for Forbidden Phrases
135
+ const forbiddenMatch = content.match(
136
+ /### Forbidden Phrases[\s\S]*?(?=###|##|$)/i
137
+ );
138
+ if (forbiddenMatch && voice.prohibited.length === 0) {
139
+ const items = forbiddenMatch[0].match(/-\s*["']?([^"'\n(]+)/g);
140
+ if (items) {
141
+ voice.prohibited = items
142
+ .map((item) => item.replace(/^-\s*["']?/, "").trim())
143
+ .filter(Boolean);
144
+ }
145
+ }
146
+
147
+ voice.personality = voice.traits.join(", ");
148
+
149
+ return voice;
150
+ }
151
+
152
+ /**
153
+ * Extract core attributes
154
+ */
155
+ function extractCoreAttributes(content) {
156
+ const attributes = [];
157
+
158
+ const attributesMatch = content.match(
159
+ /### Core Attributes[\s\S]*?\|[\s\S]*?(?=###|##|$)/i
160
+ );
161
+ if (attributesMatch) {
162
+ const rows = attributesMatch[0].match(
163
+ /\|\s*\*\*([^*]+)\*\*\s*\|\s*([^|]+)\|/g
164
+ );
165
+ if (rows) {
166
+ rows.forEach((row) => {
167
+ const match = row.match(/\*\*([^*]+)\*\*\s*\|\s*([^|]+)/);
168
+ if (match) {
169
+ attributes.push({
170
+ name: match[1].trim(),
171
+ description: match[2].trim(),
172
+ });
173
+ }
174
+ });
175
+ }
176
+ }
177
+
178
+ return attributes;
179
+ }
180
+
181
+ /**
182
+ * Extract AI image generation context
183
+ */
184
+ function extractImageStyle(content) {
185
+ const imageStyle = {
186
+ basePrompt: "",
187
+ keywords: [],
188
+ mood: [],
189
+ donts: [],
190
+ examplePrompts: [],
191
+ };
192
+
193
+ // Extract base prompt template (content between ``` blocks after "Base Prompt Template")
194
+ const basePromptMatch = content.match(
195
+ /### Base Prompt Template[\s\S]*?```\n?([\s\S]*?)```/i
196
+ );
197
+ if (basePromptMatch) {
198
+ imageStyle.basePrompt = basePromptMatch[1].trim().replace(/\n/g, " ");
199
+ }
200
+
201
+ // Extract style keywords from table
202
+ const keywordsMatch = content.match(
203
+ /### Style Keywords[\s\S]*?\|[\s\S]*?(?=###|##|$)/i
204
+ );
205
+ if (keywordsMatch) {
206
+ const keywordRows = keywordsMatch[0].match(/\|\s*\*\*[^*]+\*\*\s*\|\s*([^|]+)\|/g);
207
+ if (keywordRows) {
208
+ keywordRows.forEach((row) => {
209
+ const match = row.match(/\|\s*\*\*[^*]+\*\*\s*\|\s*([^|]+)\|/);
210
+ if (match) {
211
+ const keywords = match[1].split(",").map((k) => k.trim()).filter(Boolean);
212
+ imageStyle.keywords.push(...keywords);
213
+ }
214
+ });
215
+ }
216
+ }
217
+
218
+ // Extract visual mood descriptors (bullet points)
219
+ const moodMatch = content.match(
220
+ /### Visual Mood Descriptors[\s\S]*?(?=###|##|$)/i
221
+ );
222
+ if (moodMatch) {
223
+ const moodItems = moodMatch[0].match(/-\s*([^\n]+)/g);
224
+ if (moodItems) {
225
+ imageStyle.mood = moodItems.map((item) => item.replace(/^-\s*/, "").trim());
226
+ }
227
+ }
228
+
229
+ // Extract visual don'ts from table
230
+ const dontsMatch = content.match(
231
+ /### Visual Don'ts[\s\S]*?\|[\s\S]*?(?=###|##|$)/i
232
+ );
233
+ if (dontsMatch) {
234
+ const dontRows = dontsMatch[0].match(/\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|/g);
235
+ if (dontRows) {
236
+ dontRows.forEach((row) => {
237
+ const match = row.match(/\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|/);
238
+ if (match && !match[1].includes("Avoid") && !match[1].includes("---")) {
239
+ imageStyle.donts.push(match[1].trim());
240
+ }
241
+ });
242
+ }
243
+ }
244
+
245
+ // Extract example prompts (content between ``` blocks after specific headers)
246
+ const exampleMatch = content.match(/### Example Prompts[\s\S]*?(?=##|$)/i);
247
+ if (exampleMatch) {
248
+ const prompts = exampleMatch[0].match(/\*\*([^*]+)\*\*:\s*```\n?([\s\S]*?)```/g);
249
+ if (prompts) {
250
+ prompts.forEach((p) => {
251
+ const match = p.match(/\*\*([^*]+)\*\*:\s*```\n?([\s\S]*?)```/);
252
+ if (match) {
253
+ imageStyle.examplePrompts.push({
254
+ type: match[1].trim(),
255
+ prompt: match[2].trim().replace(/\n/g, " "),
256
+ });
257
+ }
258
+ });
259
+ }
260
+ }
261
+
262
+ return imageStyle;
263
+ }
264
+
265
+ /**
266
+ * Generate system prompt addition
267
+ */
268
+ function generatePromptAddition(brandContext) {
269
+ const { colors, typography, voice, attributes, imageStyle } = brandContext;
270
+
271
+ let prompt = `
272
+ BRAND CONTEXT:
273
+ ==============
274
+
275
+ VISUAL IDENTITY:
276
+ - Primary Colors: ${colors.primary.join(", ") || "Not specified"}
277
+ - Secondary Colors: ${colors.secondary.join(", ") || "Not specified"}
278
+ - Typography: ${typography.heading || typography.body || "System fonts"}
279
+
280
+ BRAND VOICE:
281
+ - Personality: ${voice.personality || "Professional"}
282
+ - Core Attributes: ${attributes.map((a) => a.name).join(", ") || "Not specified"}
283
+
284
+ CONTENT RULES:
285
+ - Prohibited Terms: ${voice.prohibited.join(", ") || "None specified"}
286
+ `;
287
+
288
+ // Add image style context if available
289
+ if (imageStyle && imageStyle.basePrompt) {
290
+ prompt += `
291
+ IMAGE GENERATION:
292
+ - Base Prompt: ${imageStyle.basePrompt}
293
+ - Style Keywords: ${imageStyle.keywords.slice(0, 10).join(", ") || "Not specified"}
294
+ - Visual Mood: ${imageStyle.mood.slice(0, 5).join("; ") || "Not specified"}
295
+ - Avoid: ${imageStyle.donts.join(", ") || "None specified"}
296
+ `;
297
+ }
298
+
299
+ prompt += `
300
+ Apply these brand guidelines to all generated content.
301
+ Maintain consistent voice, colors, and messaging.
302
+ `;
303
+
304
+ return prompt.trim();
305
+ }
306
+
307
+ /**
308
+ * Main function
309
+ */
310
+ function main() {
311
+ const args = process.argv.slice(2);
312
+ const jsonOutput = args.includes("--json");
313
+ const guidelinesPath = args.find((a) => !a.startsWith("--")) || DEFAULT_GUIDELINES_PATH;
314
+
315
+ // Resolve path
316
+ const resolvedPath = path.isAbsolute(guidelinesPath)
317
+ ? guidelinesPath
318
+ : path.join(process.cwd(), guidelinesPath);
319
+
320
+ // Check if file exists
321
+ if (!fs.existsSync(resolvedPath)) {
322
+ console.error(`Error: Brand guidelines not found at ${resolvedPath}`);
323
+ console.error(`Create brand guidelines at ${DEFAULT_GUIDELINES_PATH} or specify a path.`);
324
+ process.exit(1);
325
+ }
326
+
327
+ // Read file
328
+ const content = fs.readFileSync(resolvedPath, "utf-8");
329
+
330
+ // Extract brand context
331
+ const brandContext = {
332
+ colors: extractColorsFromTable(content),
333
+ typography: extractTypography(content),
334
+ voice: extractVoice(content),
335
+ attributes: extractCoreAttributes(content),
336
+ imageStyle: extractImageStyle(content),
337
+ source: resolvedPath,
338
+ extractedAt: new Date().toISOString(),
339
+ };
340
+
341
+ // Output
342
+ if (jsonOutput) {
343
+ console.log(JSON.stringify(brandContext, null, 2));
344
+ } else {
345
+ console.log(generatePromptAddition(brandContext));
346
+ }
347
+ }
348
+
349
+ main();
@@ -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 = `ClaudeKit 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"