omgkit 2.28.0 → 2.30.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 (51) hide show
  1. package/README.md +72 -1
  2. package/bin/omgkit.js +188 -1
  3. package/lib/cli.js +58 -4
  4. package/lib/theme.js +1220 -0
  5. package/package.json +2 -2
  6. package/plugin/agents/fullstack-developer.md +1 -0
  7. package/plugin/agents/ui-ux-designer.md +175 -41
  8. package/plugin/commands/design/add.md +86 -0
  9. package/plugin/commands/design/builder.md +96 -0
  10. package/plugin/commands/design/from-screenshot.md +64 -0
  11. package/plugin/commands/design/from-url.md +74 -0
  12. package/plugin/commands/design/preview.md +55 -0
  13. package/plugin/commands/design/rebuild.md +153 -0
  14. package/plugin/commands/design/reset.md +65 -0
  15. package/plugin/commands/design/rollback.md +179 -0
  16. package/plugin/commands/design/scan.md +155 -0
  17. package/plugin/commands/design/theme.md +65 -0
  18. package/plugin/commands/design/themes.md +50 -0
  19. package/plugin/registry.yaml +15 -3
  20. package/plugin/skills/frontend/design-system-context/SKILL.md +252 -0
  21. package/templates/design/schema/theme.schema.json +102 -0
  22. package/templates/design/themes/corporate-enterprise/consulting.json +81 -0
  23. package/templates/design/themes/corporate-enterprise/corporate-indigo.json +81 -0
  24. package/templates/design/themes/corporate-enterprise/finance.json +81 -0
  25. package/templates/design/themes/corporate-enterprise/healthcare.json +81 -0
  26. package/templates/design/themes/corporate-enterprise/legal.json +81 -0
  27. package/templates/design/themes/corporate-enterprise/ocean-blue.json +81 -0
  28. package/templates/design/themes/creative-bold/candy.json +81 -0
  29. package/templates/design/themes/creative-bold/coral-sunset.json +81 -0
  30. package/templates/design/themes/creative-bold/gradient-dream.json +81 -0
  31. package/templates/design/themes/creative-bold/neon.json +81 -0
  32. package/templates/design/themes/creative-bold/retro.json +81 -0
  33. package/templates/design/themes/creative-bold/studio.json +81 -0
  34. package/templates/design/themes/minimal-clean/minimal-slate.json +81 -0
  35. package/templates/design/themes/minimal-clean/mono.json +81 -0
  36. package/templates/design/themes/minimal-clean/nordic.json +81 -0
  37. package/templates/design/themes/minimal-clean/paper.json +81 -0
  38. package/templates/design/themes/minimal-clean/swiss.json +81 -0
  39. package/templates/design/themes/minimal-clean/zen.json +81 -0
  40. package/templates/design/themes/nature-organic/arctic.json +81 -0
  41. package/templates/design/themes/nature-organic/autumn.json +81 -0
  42. package/templates/design/themes/nature-organic/desert.json +81 -0
  43. package/templates/design/themes/nature-organic/forest.json +81 -0
  44. package/templates/design/themes/nature-organic/lavender.json +81 -0
  45. package/templates/design/themes/nature-organic/ocean.json +81 -0
  46. package/templates/design/themes/tech-ai/electric-cyan.json +81 -0
  47. package/templates/design/themes/tech-ai/hologram.json +81 -0
  48. package/templates/design/themes/tech-ai/matrix-green.json +81 -0
  49. package/templates/design/themes/tech-ai/neo-tokyo.json +81 -0
  50. package/templates/design/themes/tech-ai/neural-dark.json +81 -0
  51. package/templates/design/themes/tech-ai/quantum-purple.json +81 -0
package/lib/theme.js ADDED
@@ -0,0 +1,1220 @@
1
+ /**
2
+ * OMGKIT Theme Processing Library
3
+ * Handles theme loading, validation, CSS generation, and extraction
4
+ *
5
+ * @module lib/theme
6
+ */
7
+
8
+ import { existsSync, readFileSync, writeFileSync, readdirSync, mkdirSync } from 'fs';
9
+ import { join, dirname } from 'path';
10
+ import { fileURLToPath } from 'url';
11
+
12
+ // Package root detection
13
+ let PACKAGE_ROOT;
14
+
15
+ /**
16
+ * Set the package root directory (for testing)
17
+ * @param {string} root - Package root path
18
+ */
19
+ export function setThemePackageRoot(root) {
20
+ PACKAGE_ROOT = root;
21
+ }
22
+
23
+ /**
24
+ * Get the package root directory
25
+ * @returns {string} Package root path
26
+ */
27
+ export function getThemePackageRoot() {
28
+ if (PACKAGE_ROOT) return PACKAGE_ROOT;
29
+ const __filename = fileURLToPath(import.meta.url);
30
+ const __dirname = dirname(__filename);
31
+ PACKAGE_ROOT = join(__dirname, '..');
32
+ return PACKAGE_ROOT;
33
+ }
34
+
35
+ /**
36
+ * Theme category definitions
37
+ */
38
+ export const THEME_CATEGORIES = {
39
+ 'tech-ai': {
40
+ name: 'Tech & AI',
41
+ description: 'Futuristic, cyberpunk, and technology-inspired themes',
42
+ emoji: '⚡'
43
+ },
44
+ 'minimal-clean': {
45
+ name: 'Minimal & Clean',
46
+ description: 'Simple, elegant, and distraction-free themes',
47
+ emoji: '✨'
48
+ },
49
+ 'corporate-enterprise': {
50
+ name: 'Corporate & Enterprise',
51
+ description: 'Professional themes for business applications',
52
+ emoji: '🏢'
53
+ },
54
+ 'creative-bold': {
55
+ name: 'Creative & Bold',
56
+ description: 'Vibrant, expressive themes for creative projects',
57
+ emoji: '🎨'
58
+ },
59
+ 'nature-organic': {
60
+ name: 'Nature & Organic',
61
+ description: 'Earthy, natural color palettes inspired by nature',
62
+ emoji: '🌿'
63
+ }
64
+ };
65
+
66
+ /**
67
+ * Required color variables for shadcn compatibility
68
+ */
69
+ export const REQUIRED_COLORS = [
70
+ 'background', 'foreground',
71
+ 'primary', 'primary-foreground',
72
+ 'secondary', 'secondary-foreground',
73
+ 'muted', 'muted-foreground',
74
+ 'accent', 'accent-foreground',
75
+ 'destructive', 'destructive-foreground',
76
+ 'border', 'input', 'ring',
77
+ 'card', 'card-foreground',
78
+ 'popover', 'popover-foreground'
79
+ ];
80
+
81
+ /**
82
+ * Optional color variables (charts, sidebar)
83
+ */
84
+ export const OPTIONAL_COLORS = [
85
+ 'chart-1', 'chart-2', 'chart-3', 'chart-4', 'chart-5',
86
+ 'sidebar-background', 'sidebar-foreground',
87
+ 'sidebar-primary', 'sidebar-primary-foreground',
88
+ 'sidebar-accent', 'sidebar-accent-foreground',
89
+ 'sidebar-border', 'sidebar-ring'
90
+ ];
91
+
92
+ /**
93
+ * Load all available themes from templates/design/themes
94
+ * @returns {Object} Themes grouped by category
95
+ */
96
+ export function loadAllThemes() {
97
+ const themesDir = join(getThemePackageRoot(), 'templates', 'design', 'themes');
98
+ const themes = {};
99
+
100
+ for (const categoryId of Object.keys(THEME_CATEGORIES)) {
101
+ const categoryDir = join(themesDir, categoryId);
102
+ if (!existsSync(categoryDir)) {
103
+ themes[categoryId] = [];
104
+ continue;
105
+ }
106
+
107
+ themes[categoryId] = [];
108
+ const files = readdirSync(categoryDir).filter(f => f.endsWith('.json'));
109
+
110
+ for (const file of files) {
111
+ try {
112
+ const themePath = join(categoryDir, file);
113
+ const theme = JSON.parse(readFileSync(themePath, 'utf8'));
114
+ themes[categoryId].push(theme);
115
+ } catch (err) {
116
+ console.warn(`Failed to load theme ${file}: ${err.message}`);
117
+ }
118
+ }
119
+ }
120
+
121
+ return themes;
122
+ }
123
+
124
+ /**
125
+ * Get a specific theme by ID
126
+ * @param {string} themeId - Theme identifier
127
+ * @returns {Object|null} Theme object or null if not found
128
+ */
129
+ export function getThemeById(themeId) {
130
+ const themes = loadAllThemes();
131
+ for (const category of Object.values(themes)) {
132
+ const theme = category.find(t => t.id === themeId);
133
+ if (theme) return theme;
134
+ }
135
+ return null;
136
+ }
137
+
138
+ /**
139
+ * Get all theme IDs
140
+ * @returns {string[]} Array of theme IDs
141
+ */
142
+ export function getAllThemeIds() {
143
+ const themes = loadAllThemes();
144
+ const ids = [];
145
+ for (const category of Object.values(themes)) {
146
+ for (const theme of category) {
147
+ ids.push(theme.id);
148
+ }
149
+ }
150
+ return ids;
151
+ }
152
+
153
+ /**
154
+ * Validate theme against schema
155
+ * @param {Object} theme - Theme object to validate
156
+ * @returns {{valid: boolean, errors: string[]}} Validation result
157
+ */
158
+ export function validateTheme(theme) {
159
+ const errors = [];
160
+
161
+ // Check required fields
162
+ const requiredFields = ['name', 'id', 'category', 'colors'];
163
+ for (const field of requiredFields) {
164
+ if (!theme[field]) {
165
+ errors.push(`Missing required field: ${field}`);
166
+ }
167
+ }
168
+
169
+ // Validate ID format
170
+ if (theme.id && !/^[a-z0-9-]+$/.test(theme.id)) {
171
+ errors.push('ID must be kebab-case (lowercase letters, numbers, hyphens)');
172
+ }
173
+
174
+ // Validate category
175
+ if (theme.category && !THEME_CATEGORIES[theme.category]) {
176
+ errors.push(`Invalid category: ${theme.category}. Must be one of: ${Object.keys(THEME_CATEGORIES).join(', ')}`);
177
+ }
178
+
179
+ // Validate colors
180
+ if (theme.colors) {
181
+ if (!theme.colors.light) errors.push('Missing light color palette');
182
+ if (!theme.colors.dark) errors.push('Missing dark color palette');
183
+
184
+ for (const mode of ['light', 'dark']) {
185
+ if (theme.colors[mode]) {
186
+ for (const color of REQUIRED_COLORS) {
187
+ if (!theme.colors[mode][color]) {
188
+ errors.push(`Missing ${mode}.${color} color`);
189
+ }
190
+ }
191
+
192
+ // Validate HSL format
193
+ for (const [key, value] of Object.entries(theme.colors[mode])) {
194
+ if (typeof value === 'string' && !/^\d+(\.\d+)?\s+\d+(\.\d+)?%\s+\d+(\.\d+)?%$/.test(value)) {
195
+ errors.push(`Invalid HSL format for ${mode}.${key}: "${value}". Expected format: "H S% L%" (e.g., "220 14.3% 95.9%")`);
196
+ }
197
+ }
198
+ }
199
+ }
200
+ }
201
+
202
+ return { valid: errors.length === 0, errors };
203
+ }
204
+
205
+ /**
206
+ * Generate CSS variables from theme
207
+ * @param {Object} theme - Theme object
208
+ * @returns {string} CSS content with variables
209
+ */
210
+ export function generateThemeCSS(theme) {
211
+ const generateColorVars = (colors) => {
212
+ let css = '';
213
+ for (const [key, value] of Object.entries(colors)) {
214
+ css += ` --${key}: ${value};\n`;
215
+ }
216
+ return css;
217
+ };
218
+
219
+ const lightVars = generateColorVars(theme.colors.light);
220
+ const darkVars = generateColorVars(theme.colors.dark);
221
+
222
+ return `/* OMGKIT Theme: ${theme.name} */
223
+ /* Theme ID: ${theme.id} */
224
+ /* Category: ${theme.category} */
225
+ /* Generated by OMGKIT Design System */
226
+
227
+ @layer base {
228
+ :root {
229
+ ${lightVars} --radius: ${theme.radius || '0.5rem'};
230
+ }
231
+
232
+ .dark {
233
+ ${darkVars} }
234
+ }
235
+
236
+ @layer base {
237
+ * {
238
+ @apply border-border;
239
+ }
240
+ body {
241
+ @apply bg-background text-foreground;
242
+ }
243
+ }
244
+ `;
245
+ }
246
+
247
+ /**
248
+ * Generate components.json for shadcn
249
+ * @param {Object} theme - Theme object
250
+ * @param {Object} options - Configuration options
251
+ * @returns {Object} components.json content
252
+ */
253
+ export function generateComponentsJson(theme, options = {}) {
254
+ const {
255
+ cssPath = 'app/globals.css',
256
+ tailwindConfig = 'tailwind.config.ts',
257
+ style = 'new-york',
258
+ rsc = true,
259
+ tsx = true
260
+ } = options;
261
+
262
+ return {
263
+ "$schema": "https://ui.shadcn.com/schema.json",
264
+ "style": style,
265
+ "rsc": rsc,
266
+ "tsx": tsx,
267
+ "tailwind": {
268
+ "config": tailwindConfig,
269
+ "css": cssPath,
270
+ "baseColor": "slate",
271
+ "cssVariables": true,
272
+ "prefix": ""
273
+ },
274
+ "aliases": {
275
+ "components": "@/components",
276
+ "utils": "@/lib/utils",
277
+ "ui": "@/components/ui",
278
+ "lib": "@/lib",
279
+ "hooks": "@/hooks"
280
+ },
281
+ "iconLibrary": "lucide"
282
+ };
283
+ }
284
+
285
+ /**
286
+ * Generate tailwind.config.ts content
287
+ * @param {Object} theme - Theme object
288
+ * @returns {string} Tailwind config content
289
+ */
290
+ export function generateTailwindConfig(theme) {
291
+ const fontSans = theme.fontFamily?.sans || 'Inter, system-ui, sans-serif';
292
+ const fontMono = theme.fontFamily?.mono || 'JetBrains Mono, monospace';
293
+
294
+ return `import type { Config } from "tailwindcss";
295
+
296
+ const config: Config = {
297
+ darkMode: ["class"],
298
+ content: [
299
+ "./pages/**/*.{js,ts,jsx,tsx,mdx}",
300
+ "./components/**/*.{js,ts,jsx,tsx,mdx}",
301
+ "./app/**/*.{js,ts,jsx,tsx,mdx}",
302
+ "./src/**/*.{js,ts,jsx,tsx,mdx}",
303
+ ],
304
+ theme: {
305
+ extend: {
306
+ colors: {
307
+ background: "hsl(var(--background))",
308
+ foreground: "hsl(var(--foreground))",
309
+ card: {
310
+ DEFAULT: "hsl(var(--card))",
311
+ foreground: "hsl(var(--card-foreground))",
312
+ },
313
+ popover: {
314
+ DEFAULT: "hsl(var(--popover))",
315
+ foreground: "hsl(var(--popover-foreground))",
316
+ },
317
+ primary: {
318
+ DEFAULT: "hsl(var(--primary))",
319
+ foreground: "hsl(var(--primary-foreground))",
320
+ },
321
+ secondary: {
322
+ DEFAULT: "hsl(var(--secondary))",
323
+ foreground: "hsl(var(--secondary-foreground))",
324
+ },
325
+ muted: {
326
+ DEFAULT: "hsl(var(--muted))",
327
+ foreground: "hsl(var(--muted-foreground))",
328
+ },
329
+ accent: {
330
+ DEFAULT: "hsl(var(--accent))",
331
+ foreground: "hsl(var(--accent-foreground))",
332
+ },
333
+ destructive: {
334
+ DEFAULT: "hsl(var(--destructive))",
335
+ foreground: "hsl(var(--destructive-foreground))",
336
+ },
337
+ border: "hsl(var(--border))",
338
+ input: "hsl(var(--input))",
339
+ ring: "hsl(var(--ring))",
340
+ chart: {
341
+ "1": "hsl(var(--chart-1))",
342
+ "2": "hsl(var(--chart-2))",
343
+ "3": "hsl(var(--chart-3))",
344
+ "4": "hsl(var(--chart-4))",
345
+ "5": "hsl(var(--chart-5))",
346
+ },
347
+ sidebar: {
348
+ DEFAULT: "hsl(var(--sidebar-background))",
349
+ foreground: "hsl(var(--sidebar-foreground))",
350
+ primary: "hsl(var(--sidebar-primary))",
351
+ "primary-foreground": "hsl(var(--sidebar-primary-foreground))",
352
+ accent: "hsl(var(--sidebar-accent))",
353
+ "accent-foreground": "hsl(var(--sidebar-accent-foreground))",
354
+ border: "hsl(var(--sidebar-border))",
355
+ ring: "hsl(var(--sidebar-ring))",
356
+ },
357
+ },
358
+ borderRadius: {
359
+ lg: "var(--radius)",
360
+ md: "calc(var(--radius) - 2px)",
361
+ sm: "calc(var(--radius) - 4px)",
362
+ },
363
+ fontFamily: {
364
+ sans: ["${fontSans}"],
365
+ mono: ["${fontMono}"],
366
+ },
367
+ },
368
+ },
369
+ plugins: [require("tailwindcss-animate")],
370
+ };
371
+
372
+ export default config;
373
+ `;
374
+ }
375
+
376
+ /**
377
+ * Apply theme to project directory
378
+ * @param {Object} theme - Theme object
379
+ * @param {string} projectDir - Project directory path
380
+ * @returns {{themeJson: string, themeCss: string}} Created file paths
381
+ */
382
+ export function applyThemeToProject(theme, projectDir) {
383
+ const designDir = join(projectDir, '.omgkit', 'design');
384
+
385
+ // Create design directory
386
+ mkdirSync(designDir, { recursive: true });
387
+
388
+ // Write theme.json
389
+ const themeJsonPath = join(designDir, 'theme.json');
390
+ writeFileSync(themeJsonPath, JSON.stringify(theme, null, 2));
391
+
392
+ // Write theme.css
393
+ const themeCssPath = join(designDir, 'theme.css');
394
+ writeFileSync(themeCssPath, generateThemeCSS(theme));
395
+
396
+ return {
397
+ themeJson: themeJsonPath,
398
+ themeCss: themeCssPath
399
+ };
400
+ }
401
+
402
+ /**
403
+ * Get project's current theme
404
+ * @param {string} projectDir - Project directory path
405
+ * @returns {Object|null} Theme object or null if not found
406
+ */
407
+ export function getProjectTheme(projectDir) {
408
+ const themeJsonPath = join(projectDir, '.omgkit', 'design', 'theme.json');
409
+ if (!existsSync(themeJsonPath)) return null;
410
+
411
+ try {
412
+ return JSON.parse(readFileSync(themeJsonPath, 'utf8'));
413
+ } catch {
414
+ return null;
415
+ }
416
+ }
417
+
418
+ /**
419
+ * List all themes with preview data
420
+ * @returns {Array} Array of category objects with themes
421
+ */
422
+ export function listThemesWithPreview() {
423
+ const themes = loadAllThemes();
424
+ const result = [];
425
+
426
+ for (const [categoryId, categoryThemes] of Object.entries(themes)) {
427
+ const category = THEME_CATEGORIES[categoryId];
428
+ if (!category) continue;
429
+
430
+ result.push({
431
+ categoryId,
432
+ categoryName: category.name,
433
+ emoji: category.emoji,
434
+ description: category.description,
435
+ themes: categoryThemes.map(t => ({
436
+ id: t.id,
437
+ name: t.name,
438
+ description: t.description,
439
+ primaryLight: t.colors.light.primary,
440
+ primaryDark: t.colors.dark.primary,
441
+ backgroundLight: t.colors.light.background,
442
+ backgroundDark: t.colors.dark.background
443
+ }))
444
+ });
445
+ }
446
+
447
+ return result;
448
+ }
449
+
450
+ /**
451
+ * Get prompt for Claude Vision screenshot extraction
452
+ * @returns {string} Extraction prompt
453
+ */
454
+ export function getScreenshotExtractionPrompt() {
455
+ return `Analyze this screenshot and extract a cohesive color theme for a web application.
456
+
457
+ For each color, provide the HSL value in this format: "H S% L%" (e.g., "220 14.3% 95.9%")
458
+
459
+ Extract these colors:
460
+ 1. **background** - Main page background
461
+ 2. **foreground** - Primary text color
462
+ 3. **primary** - Brand/accent color (buttons, links)
463
+ 4. **primary-foreground** - Text on primary color
464
+ 5. **secondary** - Secondary backgrounds
465
+ 6. **secondary-foreground** - Text on secondary
466
+ 7. **muted** - Subtle backgrounds
467
+ 8. **muted-foreground** - Subtle text
468
+ 9. **accent** - Highlights, hovers
469
+ 10. **accent-foreground** - Text on accent
470
+ 11. **destructive** - Error/danger color
471
+ 12. **destructive-foreground** - Text on destructive
472
+ 13. **border** - Border colors
473
+ 14. **input** - Input field borders
474
+ 15. **ring** - Focus ring color
475
+ 16. **card** - Card background
476
+ 17. **card-foreground** - Card text
477
+ 18. **popover** - Popover background
478
+ 19. **popover-foreground** - Popover text
479
+
480
+ Return a JSON object with this structure:
481
+ {
482
+ "name": "Extracted Theme",
483
+ "id": "extracted-theme",
484
+ "category": "custom",
485
+ "description": "Theme extracted from screenshot",
486
+ "colors": {
487
+ "light": { ... all colors ... },
488
+ "dark": { ... inverted/dark mode colors ... }
489
+ },
490
+ "radius": "0.5rem"
491
+ }`;
492
+ }
493
+
494
+ /**
495
+ * Convert hex color to HSL string
496
+ * @param {string} hex - Hex color (e.g., "#E11D48")
497
+ * @returns {string} HSL string (e.g., "346.8 77.2% 49.8%")
498
+ */
499
+ export function hexToHsl(hex) {
500
+ // Remove # if present
501
+ hex = hex.replace(/^#/, '');
502
+
503
+ // Parse hex
504
+ const r = parseInt(hex.slice(0, 2), 16) / 255;
505
+ const g = parseInt(hex.slice(2, 4), 16) / 255;
506
+ const b = parseInt(hex.slice(4, 6), 16) / 255;
507
+
508
+ const max = Math.max(r, g, b);
509
+ const min = Math.min(r, g, b);
510
+ let h, s, l = (max + min) / 2;
511
+
512
+ if (max === min) {
513
+ h = s = 0;
514
+ } else {
515
+ const d = max - min;
516
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
517
+
518
+ switch (max) {
519
+ case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
520
+ case g: h = ((b - r) / d + 2) / 6; break;
521
+ case b: h = ((r - g) / d + 4) / 6; break;
522
+ }
523
+ }
524
+
525
+ h = Math.round(h * 360 * 10) / 10;
526
+ s = Math.round(s * 100 * 10) / 10;
527
+ l = Math.round(l * 100 * 10) / 10;
528
+
529
+ return `${h} ${s}% ${l}%`;
530
+ }
531
+
532
+ /**
533
+ * Convert HSL string to hex color
534
+ * @param {string} hsl - HSL string (e.g., "346.8 77.2% 49.8%")
535
+ * @returns {string} Hex color (e.g., "#E11D48")
536
+ */
537
+ export function hslToHex(hsl) {
538
+ const [h, s, l] = hsl.split(/\s+/).map((v, i) => {
539
+ const num = parseFloat(v);
540
+ return i === 0 ? num / 360 : num / 100;
541
+ });
542
+
543
+ let r, g, b;
544
+
545
+ if (s === 0) {
546
+ r = g = b = l;
547
+ } else {
548
+ const hue2rgb = (p, q, t) => {
549
+ if (t < 0) t += 1;
550
+ if (t > 1) t -= 1;
551
+ if (t < 1/6) return p + (q - p) * 6 * t;
552
+ if (t < 1/2) return q;
553
+ if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
554
+ return p;
555
+ };
556
+
557
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
558
+ const p = 2 * l - q;
559
+
560
+ r = hue2rgb(p, q, h + 1/3);
561
+ g = hue2rgb(p, q, h);
562
+ b = hue2rgb(p, q, h - 1/3);
563
+ }
564
+
565
+ const toHex = x => {
566
+ const hex = Math.round(x * 255).toString(16);
567
+ return hex.length === 1 ? '0' + hex : hex;
568
+ };
569
+
570
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
571
+ }
572
+
573
+ // ============================================================================
574
+ // THEME REBUILD & ROLLBACK FUNCTIONS
575
+ // ============================================================================
576
+
577
+ /**
578
+ * Directories to scan for color references (standard React/Next.js paths)
579
+ */
580
+ export const SCAN_DIRECTORIES = ['app', 'components', 'src', 'pages'];
581
+
582
+ /**
583
+ * File extensions to scan
584
+ */
585
+ export const SCAN_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js'];
586
+
587
+ /**
588
+ * Directories to always exclude from scanning
589
+ */
590
+ export const EXCLUDE_DIRS = ['node_modules', '.git', '.omgkit', 'dist', 'build', '.next', 'out'];
591
+
592
+ /**
593
+ * Color patterns to detect non-compliant colors
594
+ */
595
+ export const COLOR_PATTERNS = {
596
+ // Tailwind default colors (should use theme vars)
597
+ tailwindDefaults: /\b(bg|text|border|ring|fill|stroke|outline|divide|from|via|to|shadow|decoration)-(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|white|black)-(\d{2,3})\b/g,
598
+
599
+ // Hardcoded hex colors in className or style
600
+ hexColors: /#([0-9A-Fa-f]{3}){1,2}\b/g,
601
+
602
+ // Hardcoded RGB/HSL in styles
603
+ rgbHsl: /\b(rgb|hsl)a?\([^)]+\)/g
604
+ };
605
+
606
+ /**
607
+ * Mapping of hardcoded Tailwind colors to theme variables
608
+ */
609
+ export const THEME_VAR_MAP = {
610
+ // Background mappings
611
+ 'bg-white': 'bg-background',
612
+ 'bg-gray-50': 'bg-muted',
613
+ 'bg-gray-100': 'bg-muted',
614
+ 'bg-gray-200': 'bg-muted',
615
+ 'bg-gray-900': 'bg-foreground',
616
+ 'bg-slate-50': 'bg-muted',
617
+ 'bg-slate-100': 'bg-muted',
618
+ 'bg-slate-900': 'bg-foreground',
619
+ 'bg-zinc-50': 'bg-muted',
620
+ 'bg-zinc-100': 'bg-muted',
621
+ 'bg-zinc-900': 'bg-foreground',
622
+
623
+ // Text mappings
624
+ 'text-black': 'text-foreground',
625
+ 'text-white': 'text-background',
626
+ 'text-gray-900': 'text-foreground',
627
+ 'text-gray-800': 'text-foreground',
628
+ 'text-gray-700': 'text-foreground',
629
+ 'text-gray-600': 'text-muted-foreground',
630
+ 'text-gray-500': 'text-muted-foreground',
631
+ 'text-gray-400': 'text-muted-foreground',
632
+ 'text-slate-900': 'text-foreground',
633
+ 'text-slate-600': 'text-muted-foreground',
634
+ 'text-slate-500': 'text-muted-foreground',
635
+ 'text-zinc-900': 'text-foreground',
636
+ 'text-zinc-600': 'text-muted-foreground',
637
+ 'text-zinc-500': 'text-muted-foreground',
638
+
639
+ // Border mappings
640
+ 'border-gray-100': 'border-border',
641
+ 'border-gray-200': 'border-border',
642
+ 'border-gray-300': 'border-input',
643
+ 'border-slate-200': 'border-border',
644
+ 'border-slate-300': 'border-input',
645
+ 'border-zinc-200': 'border-border',
646
+ 'border-zinc-300': 'border-input',
647
+
648
+ // Primary/accent colors (common patterns)
649
+ 'bg-blue-500': 'bg-primary',
650
+ 'bg-blue-600': 'bg-primary',
651
+ 'bg-blue-700': 'bg-primary',
652
+ 'text-blue-500': 'text-primary',
653
+ 'text-blue-600': 'text-primary',
654
+ 'text-blue-700': 'text-primary',
655
+ 'ring-blue-500': 'ring-ring',
656
+ 'ring-blue-600': 'ring-ring',
657
+
658
+ // Destructive
659
+ 'bg-red-500': 'bg-destructive',
660
+ 'bg-red-600': 'bg-destructive',
661
+ 'text-red-500': 'text-destructive',
662
+ 'text-red-600': 'text-destructive',
663
+ 'border-red-500': 'border-destructive',
664
+
665
+ // Secondary/accent patterns
666
+ 'bg-gray-100': 'bg-secondary',
667
+ 'bg-slate-100': 'bg-secondary',
668
+ 'hover:bg-gray-100': 'hover:bg-accent',
669
+ 'hover:bg-slate-100': 'hover:bg-accent'
670
+ };
671
+
672
+ /**
673
+ * Recursively get all files in a directory
674
+ * @param {string} dir - Directory to scan
675
+ * @param {string[]} extensions - File extensions to include
676
+ * @param {string[]} excludeDirs - Directories to exclude
677
+ * @returns {string[]} Array of file paths
678
+ */
679
+ function getFilesRecursive(dir, extensions, excludeDirs) {
680
+ const files = [];
681
+ if (!existsSync(dir)) return files;
682
+
683
+ const entries = readdirSync(dir, { withFileTypes: true });
684
+
685
+ for (const entry of entries) {
686
+ const fullPath = join(dir, entry.name);
687
+
688
+ if (entry.isDirectory()) {
689
+ if (!excludeDirs.includes(entry.name)) {
690
+ files.push(...getFilesRecursive(fullPath, extensions, excludeDirs));
691
+ }
692
+ } else if (entry.isFile()) {
693
+ const ext = '.' + entry.name.split('.').pop();
694
+ if (extensions.includes(ext)) {
695
+ files.push(fullPath);
696
+ }
697
+ }
698
+ }
699
+
700
+ return files;
701
+ }
702
+
703
+ /**
704
+ * Scan project for files with non-compliant color references
705
+ * @param {string} projectDir - Project root directory
706
+ * @returns {Object} { files: [], totalReferences: number, nonCompliant: [], compliant: number }
707
+ */
708
+ export function scanProjectColors(projectDir) {
709
+ const result = {
710
+ files: [],
711
+ totalReferences: 0,
712
+ nonCompliant: [],
713
+ compliant: 0,
714
+ scannedFiles: 0
715
+ };
716
+
717
+ // Find all files to scan
718
+ const filesToScan = [];
719
+ for (const scanDir of SCAN_DIRECTORIES) {
720
+ const fullPath = join(projectDir, scanDir);
721
+ if (existsSync(fullPath)) {
722
+ filesToScan.push(...getFilesRecursive(fullPath, SCAN_EXTENSIONS, EXCLUDE_DIRS));
723
+ }
724
+ }
725
+
726
+ result.scannedFiles = filesToScan.length;
727
+
728
+ // Scan each file
729
+ for (const filePath of filesToScan) {
730
+ try {
731
+ const content = readFileSync(filePath, 'utf8');
732
+ const lines = content.split('\n');
733
+ const relativePath = filePath.replace(projectDir + '/', '');
734
+ const fileMatches = [];
735
+
736
+ for (let i = 0; i < lines.length; i++) {
737
+ const line = lines[i];
738
+ const lineNum = i + 1;
739
+
740
+ // Check for Tailwind default colors
741
+ let match;
742
+ const pattern = new RegExp(COLOR_PATTERNS.tailwindDefaults.source, 'g');
743
+ while ((match = pattern.exec(line)) !== null) {
744
+ result.totalReferences++;
745
+ const fullMatch = match[0];
746
+
747
+ // Check if this has a theme-compliant mapping
748
+ const mapping = THEME_VAR_MAP[fullMatch];
749
+ if (mapping) {
750
+ fileMatches.push({
751
+ file: relativePath,
752
+ line: lineNum,
753
+ column: match.index,
754
+ match: fullMatch,
755
+ suggestion: mapping,
756
+ type: 'tailwind-default',
757
+ fixable: true
758
+ });
759
+ result.nonCompliant.push({
760
+ file: relativePath,
761
+ line: lineNum,
762
+ match: fullMatch,
763
+ suggestion: mapping
764
+ });
765
+ } else {
766
+ // Unmapped color - warn only
767
+ fileMatches.push({
768
+ file: relativePath,
769
+ line: lineNum,
770
+ column: match.index,
771
+ match: fullMatch,
772
+ suggestion: null,
773
+ type: 'unmapped',
774
+ fixable: false
775
+ });
776
+ result.nonCompliant.push({
777
+ file: relativePath,
778
+ line: lineNum,
779
+ match: fullMatch,
780
+ suggestion: null
781
+ });
782
+ }
783
+ }
784
+
785
+ // Check for hex colors in className or style attributes
786
+ const hexPattern = new RegExp(COLOR_PATTERNS.hexColors.source, 'g');
787
+ while ((match = hexPattern.exec(line)) !== null) {
788
+ // Only flag if it appears to be in className or style context
789
+ const before = line.slice(0, match.index);
790
+ if (before.includes('className') || before.includes('style') || before.includes('bg-[') || before.includes('text-[')) {
791
+ result.totalReferences++;
792
+ fileMatches.push({
793
+ file: relativePath,
794
+ line: lineNum,
795
+ column: match.index,
796
+ match: match[0],
797
+ suggestion: 'Use CSS variable (e.g., bg-background)',
798
+ type: 'hex-color',
799
+ fixable: false
800
+ });
801
+ result.nonCompliant.push({
802
+ file: relativePath,
803
+ line: lineNum,
804
+ match: match[0],
805
+ suggestion: 'Use CSS variable'
806
+ });
807
+ }
808
+ }
809
+ }
810
+
811
+ if (fileMatches.length > 0) {
812
+ result.files.push({
813
+ path: relativePath,
814
+ matches: fileMatches
815
+ });
816
+ }
817
+ } catch (err) {
818
+ // Skip files that can't be read
819
+ }
820
+ }
821
+
822
+ result.compliant = result.totalReferences - result.nonCompliant.length;
823
+
824
+ return result;
825
+ }
826
+
827
+ /**
828
+ * Create a theme backup before rebuild
829
+ * @param {string} projectDir - Project root directory
830
+ * @param {string} newThemeId - ID of new theme being applied
831
+ * @returns {Object} { success, backupId, backupPath, error }
832
+ */
833
+ export function createThemeBackup(projectDir, newThemeId = 'unknown') {
834
+ const designDir = join(projectDir, '.omgkit', 'design');
835
+ const backupsDir = join(designDir, 'backups');
836
+
837
+ // Create backups directory
838
+ mkdirSync(backupsDir, { recursive: true });
839
+
840
+ // Generate backup ID
841
+ const now = new Date();
842
+ const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
843
+ const backupId = `${timestamp}-${newThemeId}`;
844
+ const backupPath = join(backupsDir, backupId);
845
+
846
+ try {
847
+ mkdirSync(backupPath, { recursive: true });
848
+
849
+ // Get current theme
850
+ const currentTheme = getProjectTheme(projectDir);
851
+ const previousThemeId = currentTheme?.id || 'none';
852
+
853
+ // Create manifest
854
+ const manifest = {
855
+ id: backupId,
856
+ previousTheme: previousThemeId,
857
+ newTheme: newThemeId,
858
+ timestamp: now.toISOString(),
859
+ changedFiles: []
860
+ };
861
+
862
+ // Backup theme.json if exists
863
+ const themeJsonPath = join(designDir, 'theme.json');
864
+ if (existsSync(themeJsonPath)) {
865
+ const content = readFileSync(themeJsonPath, 'utf8');
866
+ writeFileSync(join(backupPath, 'theme.json.bak'), content);
867
+ manifest.changedFiles.push({ path: '.omgkit/design/theme.json', backup: 'theme.json.bak' });
868
+ }
869
+
870
+ // Backup theme.css if exists
871
+ const themeCssPath = join(designDir, 'theme.css');
872
+ if (existsSync(themeCssPath)) {
873
+ const content = readFileSync(themeCssPath, 'utf8');
874
+ writeFileSync(join(backupPath, 'theme.css.bak'), content);
875
+ manifest.changedFiles.push({ path: '.omgkit/design/theme.css', backup: 'theme.css.bak' });
876
+ }
877
+
878
+ // Backup tailwind.config.ts if exists
879
+ const tailwindConfigPath = join(projectDir, 'tailwind.config.ts');
880
+ if (existsSync(tailwindConfigPath)) {
881
+ const content = readFileSync(tailwindConfigPath, 'utf8');
882
+ writeFileSync(join(backupPath, 'tailwind.config.ts.bak'), content);
883
+ manifest.changedFiles.push({ path: 'tailwind.config.ts', backup: 'tailwind.config.ts.bak' });
884
+ }
885
+
886
+ // Also check for .js version
887
+ const tailwindConfigJsPath = join(projectDir, 'tailwind.config.js');
888
+ if (existsSync(tailwindConfigJsPath)) {
889
+ const content = readFileSync(tailwindConfigJsPath, 'utf8');
890
+ writeFileSync(join(backupPath, 'tailwind.config.js.bak'), content);
891
+ manifest.changedFiles.push({ path: 'tailwind.config.js', backup: 'tailwind.config.js.bak' });
892
+ }
893
+
894
+ // Write manifest
895
+ writeFileSync(join(backupPath, 'manifest.json'), JSON.stringify(manifest, null, 2));
896
+
897
+ return { success: true, backupId, backupPath, manifest };
898
+ } catch (err) {
899
+ return { success: false, error: err.message };
900
+ }
901
+ }
902
+
903
+ /**
904
+ * List available theme backups
905
+ * @param {string} projectDir - Project root directory
906
+ * @returns {Array} Array of backup info objects
907
+ */
908
+ export function listThemeBackups(projectDir) {
909
+ const backupsDir = join(projectDir, '.omgkit', 'design', 'backups');
910
+ if (!existsSync(backupsDir)) return [];
911
+
912
+ const backups = [];
913
+ const entries = readdirSync(backupsDir, { withFileTypes: true });
914
+
915
+ for (const entry of entries) {
916
+ if (!entry.isDirectory()) continue;
917
+
918
+ const manifestPath = join(backupsDir, entry.name, 'manifest.json');
919
+ if (!existsSync(manifestPath)) continue;
920
+
921
+ try {
922
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
923
+ backups.push({
924
+ id: manifest.id,
925
+ previousTheme: manifest.previousTheme,
926
+ newTheme: manifest.newTheme,
927
+ timestamp: manifest.timestamp,
928
+ date: new Date(manifest.timestamp).toLocaleString(),
929
+ filesChanged: manifest.changedFiles.length,
930
+ path: join(backupsDir, entry.name)
931
+ });
932
+ } catch {
933
+ // Skip invalid backup
934
+ }
935
+ }
936
+
937
+ // Sort by timestamp descending (newest first)
938
+ return backups.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
939
+ }
940
+
941
+ /**
942
+ * Rollback to a previous theme state
943
+ * @param {string} projectDir - Project root directory
944
+ * @param {string} backupId - Backup ID to restore (optional, defaults to latest)
945
+ * @returns {Object} { success, restoredTheme, restoredFiles, error }
946
+ */
947
+ export function rollbackTheme(projectDir, backupId = null) {
948
+ const backups = listThemeBackups(projectDir);
949
+
950
+ if (backups.length === 0) {
951
+ return { success: false, error: 'No theme backups found' };
952
+ }
953
+
954
+ // Find backup to restore
955
+ let backup;
956
+ if (backupId) {
957
+ backup = backups.find(b => b.id === backupId);
958
+ if (!backup) {
959
+ return { success: false, error: `Backup not found: ${backupId}` };
960
+ }
961
+ } else {
962
+ backup = backups[0]; // Latest
963
+ }
964
+
965
+ try {
966
+ const manifestPath = join(backup.path, 'manifest.json');
967
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
968
+ const restoredFiles = [];
969
+
970
+ // Create a new backup before rollback (safety)
971
+ createThemeBackup(projectDir, `rollback-from-${manifest.newTheme}`);
972
+
973
+ // Restore each file
974
+ for (const file of manifest.changedFiles) {
975
+ const backupFilePath = join(backup.path, file.backup);
976
+ const targetPath = join(projectDir, file.path);
977
+
978
+ if (existsSync(backupFilePath)) {
979
+ const content = readFileSync(backupFilePath, 'utf8');
980
+ mkdirSync(dirname(targetPath), { recursive: true });
981
+ writeFileSync(targetPath, content);
982
+ restoredFiles.push(file.path);
983
+ }
984
+ }
985
+
986
+ return {
987
+ success: true,
988
+ restoredTheme: manifest.previousTheme,
989
+ restoredFiles,
990
+ backupUsed: backup.id
991
+ };
992
+ } catch (err) {
993
+ return { success: false, error: err.message };
994
+ }
995
+ }
996
+
997
+ /**
998
+ * Update file replacing hardcoded colors with theme variables
999
+ * @param {string} filePath - File to update
1000
+ * @param {string} projectDir - Project root directory
1001
+ * @returns {Object} { changed, replacements, content }
1002
+ */
1003
+ export function updateFileColors(filePath, projectDir) {
1004
+ const fullPath = join(projectDir, filePath);
1005
+ if (!existsSync(fullPath)) {
1006
+ return { changed: false, replacements: [], error: 'File not found' };
1007
+ }
1008
+
1009
+ let content = readFileSync(fullPath, 'utf8');
1010
+ const replacements = [];
1011
+ let changed = false;
1012
+
1013
+ // Apply theme variable mappings
1014
+ for (const [pattern, replacement] of Object.entries(THEME_VAR_MAP)) {
1015
+ // Escape special regex characters in pattern
1016
+ const escapedPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1017
+ const regex = new RegExp(`\\b${escapedPattern}\\b`, 'g');
1018
+
1019
+ const matches = content.match(regex);
1020
+ if (matches && matches.length > 0) {
1021
+ content = content.replace(regex, replacement);
1022
+ replacements.push({
1023
+ from: pattern,
1024
+ to: replacement,
1025
+ count: matches.length
1026
+ });
1027
+ changed = true;
1028
+ }
1029
+ }
1030
+
1031
+ return { changed, replacements, content };
1032
+ }
1033
+
1034
+ /**
1035
+ * Update project's tailwind.config file with new theme
1036
+ * @param {Object} theme - Theme object
1037
+ * @param {string} projectDir - Project root directory
1038
+ * @returns {Object} { success, path, error }
1039
+ */
1040
+ export function updateProjectTailwindConfig(theme, projectDir) {
1041
+ // Check for tailwind.config.ts first, then .js
1042
+ let configPath = join(projectDir, 'tailwind.config.ts');
1043
+ let isTs = true;
1044
+
1045
+ if (!existsSync(configPath)) {
1046
+ configPath = join(projectDir, 'tailwind.config.js');
1047
+ isTs = false;
1048
+ if (!existsSync(configPath)) {
1049
+ // Create new config
1050
+ configPath = join(projectDir, 'tailwind.config.ts');
1051
+ isTs = true;
1052
+ }
1053
+ }
1054
+
1055
+ try {
1056
+ const newConfig = generateTailwindConfig(theme);
1057
+ writeFileSync(configPath, newConfig);
1058
+ return { success: true, path: configPath, isTs };
1059
+ } catch (err) {
1060
+ return { success: false, error: err.message };
1061
+ }
1062
+ }
1063
+
1064
+ /**
1065
+ * Ensure globals.css imports theme.css
1066
+ * @param {string} projectDir - Project root directory
1067
+ * @returns {Object} { updated, path, alreadyImported }
1068
+ */
1069
+ export function ensureThemeImport(projectDir) {
1070
+ // Look for globals.css in common locations
1071
+ const possiblePaths = [
1072
+ join(projectDir, 'app', 'globals.css'),
1073
+ join(projectDir, 'src', 'app', 'globals.css'),
1074
+ join(projectDir, 'styles', 'globals.css'),
1075
+ join(projectDir, 'src', 'styles', 'globals.css')
1076
+ ];
1077
+
1078
+ let globalsPath = null;
1079
+ for (const p of possiblePaths) {
1080
+ if (existsSync(p)) {
1081
+ globalsPath = p;
1082
+ break;
1083
+ }
1084
+ }
1085
+
1086
+ if (!globalsPath) {
1087
+ return { updated: false, path: null, error: 'globals.css not found' };
1088
+ }
1089
+
1090
+ let content = readFileSync(globalsPath, 'utf8');
1091
+ const themeImport = "@import '../.omgkit/design/theme.css';";
1092
+ const altThemeImport = "@import '../../.omgkit/design/theme.css';";
1093
+
1094
+ // Check if already imported
1095
+ if (content.includes('.omgkit/design/theme.css')) {
1096
+ return { updated: false, path: globalsPath, alreadyImported: true };
1097
+ }
1098
+
1099
+ // Add import at the beginning
1100
+ const relativePath = globalsPath.includes('/src/') ? altThemeImport : themeImport;
1101
+ content = `${relativePath}\n${content}`;
1102
+
1103
+ writeFileSync(globalsPath, content);
1104
+ return { updated: true, path: globalsPath, alreadyImported: false };
1105
+ }
1106
+
1107
+ /**
1108
+ * Rebuild entire project with a new theme
1109
+ * @param {string} projectDir - Project root directory
1110
+ * @param {string} themeId - New theme ID
1111
+ * @param {Object} options - { dryRun, force, fixColors }
1112
+ * @returns {Object} { success, backupPath, changedFiles, warnings, error }
1113
+ */
1114
+ export function rebuildProjectTheme(projectDir, themeId, options = {}) {
1115
+ const { dryRun = false, force = false, fixColors = true } = options;
1116
+
1117
+ // Validate project has .omgkit
1118
+ if (!existsSync(join(projectDir, '.omgkit'))) {
1119
+ return { success: false, error: 'Not an OMGKIT project. Run: omgkit init' };
1120
+ }
1121
+
1122
+ // Get new theme
1123
+ const newTheme = getThemeById(themeId);
1124
+ if (!newTheme) {
1125
+ return { success: false, error: `Theme not found: ${themeId}. Run /design:themes to see available themes.` };
1126
+ }
1127
+
1128
+ // Validate theme
1129
+ const validation = validateTheme(newTheme);
1130
+ if (!validation.valid) {
1131
+ return { success: false, error: `Invalid theme: ${validation.errors.join(', ')}` };
1132
+ }
1133
+
1134
+ const result = {
1135
+ success: true,
1136
+ newTheme: themeId,
1137
+ backupId: null,
1138
+ backupPath: null,
1139
+ changedFiles: [],
1140
+ fixedColors: [],
1141
+ warnings: [],
1142
+ dryRun
1143
+ };
1144
+
1145
+ // Step 1: Create backup (unless dry-run)
1146
+ if (!dryRun) {
1147
+ const backup = createThemeBackup(projectDir, themeId);
1148
+ if (!backup.success) {
1149
+ return { success: false, error: `Failed to create backup: ${backup.error}` };
1150
+ }
1151
+ result.backupId = backup.backupId;
1152
+ result.backupPath = backup.backupPath;
1153
+ }
1154
+
1155
+ // Step 2: Apply new theme
1156
+ if (!dryRun) {
1157
+ const applied = applyThemeToProject(newTheme, projectDir);
1158
+ result.changedFiles.push(applied.themeJson.replace(projectDir + '/', ''));
1159
+ result.changedFiles.push(applied.themeCss.replace(projectDir + '/', ''));
1160
+ } else {
1161
+ result.changedFiles.push('.omgkit/design/theme.json');
1162
+ result.changedFiles.push('.omgkit/design/theme.css');
1163
+ }
1164
+
1165
+ // Step 3: Update tailwind config
1166
+ if (!dryRun) {
1167
+ const tailwindResult = updateProjectTailwindConfig(newTheme, projectDir);
1168
+ if (tailwindResult.success) {
1169
+ result.changedFiles.push(tailwindResult.path.replace(projectDir + '/', ''));
1170
+ }
1171
+ } else {
1172
+ result.changedFiles.push('tailwind.config.ts');
1173
+ }
1174
+
1175
+ // Step 4: Ensure theme import in globals.css
1176
+ if (!dryRun) {
1177
+ const importResult = ensureThemeImport(projectDir);
1178
+ if (importResult.updated) {
1179
+ result.changedFiles.push(importResult.path.replace(projectDir + '/', ''));
1180
+ }
1181
+ }
1182
+
1183
+ // Step 5: Scan and fix colors (if enabled)
1184
+ if (fixColors) {
1185
+ const scanResult = scanProjectColors(projectDir);
1186
+
1187
+ for (const fileInfo of scanResult.files) {
1188
+ const fixableMatches = fileInfo.matches.filter(m => m.fixable);
1189
+
1190
+ if (fixableMatches.length > 0) {
1191
+ if (!dryRun) {
1192
+ const updateResult = updateFileColors(fileInfo.path, projectDir);
1193
+ if (updateResult.changed) {
1194
+ // Write updated content
1195
+ writeFileSync(join(projectDir, fileInfo.path), updateResult.content);
1196
+ result.changedFiles.push(fileInfo.path);
1197
+ result.fixedColors.push({
1198
+ file: fileInfo.path,
1199
+ replacements: updateResult.replacements
1200
+ });
1201
+ }
1202
+ } else {
1203
+ // Dry run - just report what would be changed
1204
+ result.fixedColors.push({
1205
+ file: fileInfo.path,
1206
+ replacements: fixableMatches.map(m => ({ from: m.match, to: m.suggestion }))
1207
+ });
1208
+ }
1209
+ }
1210
+
1211
+ // Add warnings for unfixable colors
1212
+ const unfixable = fileInfo.matches.filter(m => !m.fixable);
1213
+ for (const u of unfixable) {
1214
+ result.warnings.push(`${u.file}:${u.line} - ${u.match} (manual review needed)`);
1215
+ }
1216
+ }
1217
+ }
1218
+
1219
+ return result;
1220
+ }