omgkit 2.29.0 → 2.31.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.
- package/README.md +92 -10
- package/bin/omgkit.js +178 -0
- package/lib/cli.js +1 -1
- package/lib/generators/css.generator.js +151 -0
- package/lib/generators/figma.generator.js +311 -0
- package/lib/generators/index.js +135 -0
- package/lib/generators/scss.generator.js +249 -0
- package/lib/generators/style-dictionary.generator.js +456 -0
- package/lib/generators/tailwind.generator.js +251 -0
- package/lib/theme-v2.js +755 -0
- package/lib/theme.js +746 -4
- package/package.json +2 -2
- package/plugin/agents/fullstack-developer.md +1 -0
- package/plugin/agents/ui-ux-designer.md +163 -54
- package/plugin/commands/design/export.md +232 -0
- package/plugin/commands/design/rebuild.md +199 -0
- package/plugin/commands/design/rollback.md +179 -0
- package/plugin/commands/design/scan.md +155 -0
- package/plugin/commands/design/validate.md +223 -0
- package/plugin/registry.yaml +7 -3
- package/plugin/skills/frontend/design-system-context/SKILL.md +252 -0
- package/templates/design/schema/theme-v2.schema.json +384 -0
- package/templates/design/themes/tech-ai/electric-cyan-v2.json +362 -0
package/lib/theme.js
CHANGED
|
@@ -9,6 +9,15 @@ import { existsSync, readFileSync, writeFileSync, readdirSync, mkdirSync } from
|
|
|
9
9
|
import { join, dirname } from 'path';
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
11
|
|
|
12
|
+
// V2 imports (lazy-loaded to avoid circular deps)
|
|
13
|
+
let themeV2Module = null;
|
|
14
|
+
async function getThemeV2Module() {
|
|
15
|
+
if (!themeV2Module) {
|
|
16
|
+
themeV2Module = await import('./theme-v2.js');
|
|
17
|
+
}
|
|
18
|
+
return themeV2Module;
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
// Package root detection
|
|
13
22
|
let PACKAGE_ROOT;
|
|
14
23
|
|
|
@@ -89,6 +98,57 @@ export const OPTIONAL_COLORS = [
|
|
|
89
98
|
'sidebar-border', 'sidebar-ring'
|
|
90
99
|
];
|
|
91
100
|
|
|
101
|
+
/**
|
|
102
|
+
* V2 extended tokens (new in v2 schema)
|
|
103
|
+
*/
|
|
104
|
+
export const V2_EXTENDED_TOKENS = [
|
|
105
|
+
'surface', 'surface-hover', 'surface-active',
|
|
106
|
+
'primary-hover', 'secondary-hover', 'accent-hover',
|
|
107
|
+
'border-hover', 'input-hover',
|
|
108
|
+
'ring-offset', 'panel', 'panel-translucent', 'overlay'
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* V2 status colors
|
|
113
|
+
*/
|
|
114
|
+
export const V2_STATUS_COLORS = [
|
|
115
|
+
'success', 'success-foreground',
|
|
116
|
+
'warning', 'warning-foreground',
|
|
117
|
+
'info', 'info-foreground'
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Detect theme schema version
|
|
122
|
+
* @param {Object} theme - Theme object
|
|
123
|
+
* @returns {'1.0' | '2.0'} Theme version
|
|
124
|
+
*/
|
|
125
|
+
export function detectThemeVersion(theme) {
|
|
126
|
+
if (!theme) return '1.0';
|
|
127
|
+
|
|
128
|
+
// Explicit version check
|
|
129
|
+
if (theme.version === '2.0' || theme.version === 2) return '2.0';
|
|
130
|
+
|
|
131
|
+
// V2 indicators (any of these marks it as v2)
|
|
132
|
+
if (theme.scales) return '2.0';
|
|
133
|
+
if (theme.semanticTokens) return '2.0';
|
|
134
|
+
if (theme.effects) return '2.0';
|
|
135
|
+
if (theme.animations) return '2.0';
|
|
136
|
+
if (theme.colorSystem) return '2.0';
|
|
137
|
+
if (theme.statusColors) return '2.0';
|
|
138
|
+
|
|
139
|
+
// Default to v1
|
|
140
|
+
return '1.0';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Check if theme is v2 format
|
|
145
|
+
* @param {Object} theme - Theme object
|
|
146
|
+
* @returns {boolean} True if v2 theme
|
|
147
|
+
*/
|
|
148
|
+
export function isV2Theme(theme) {
|
|
149
|
+
return detectThemeVersion(theme) === '2.0';
|
|
150
|
+
}
|
|
151
|
+
|
|
92
152
|
/**
|
|
93
153
|
* Load all available themes from templates/design/themes
|
|
94
154
|
* @returns {Object} Themes grouped by category
|
|
@@ -203,11 +263,13 @@ export function validateTheme(theme) {
|
|
|
203
263
|
}
|
|
204
264
|
|
|
205
265
|
/**
|
|
206
|
-
* Generate CSS variables from theme
|
|
266
|
+
* Generate CSS variables from theme (sync version, v1 only)
|
|
207
267
|
* @param {Object} theme - Theme object
|
|
208
268
|
* @returns {string} CSS content with variables
|
|
209
269
|
*/
|
|
210
270
|
export function generateThemeCSS(theme) {
|
|
271
|
+
// Check if v2 theme - if so, use simple generation for compatibility
|
|
272
|
+
// For full v2 features, use generateThemeCSSAsync or processTheme from theme-v2.js
|
|
211
273
|
const generateColorVars = (colors) => {
|
|
212
274
|
let css = '';
|
|
213
275
|
for (const [key, value] of Object.entries(colors)) {
|
|
@@ -216,17 +278,24 @@ export function generateThemeCSS(theme) {
|
|
|
216
278
|
return css;
|
|
217
279
|
};
|
|
218
280
|
|
|
219
|
-
|
|
220
|
-
const
|
|
281
|
+
// Handle v2 themes with semanticTokens
|
|
282
|
+
const lightColors = theme.colors?.light || theme.semanticTokens?.light || {};
|
|
283
|
+
const darkColors = theme.colors?.dark || theme.semanticTokens?.dark || {};
|
|
284
|
+
|
|
285
|
+
const lightVars = generateColorVars(lightColors);
|
|
286
|
+
const darkVars = generateColorVars(darkColors);
|
|
287
|
+
|
|
288
|
+
const version = isV2Theme(theme) ? '2.0' : '1.0';
|
|
221
289
|
|
|
222
290
|
return `/* OMGKIT Theme: ${theme.name} */
|
|
223
291
|
/* Theme ID: ${theme.id} */
|
|
224
292
|
/* Category: ${theme.category} */
|
|
293
|
+
/* Version: ${version} */
|
|
225
294
|
/* Generated by OMGKIT Design System */
|
|
226
295
|
|
|
227
296
|
@layer base {
|
|
228
297
|
:root {
|
|
229
|
-
${lightVars} --radius: ${theme.radius || '0.5rem'};
|
|
298
|
+
${lightVars} --radius: ${theme.spacing?.radius || theme.radius || '0.5rem'};
|
|
230
299
|
}
|
|
231
300
|
|
|
232
301
|
.dark {
|
|
@@ -244,6 +313,30 @@ ${darkVars} }
|
|
|
244
313
|
`;
|
|
245
314
|
}
|
|
246
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Generate CSS variables from theme (async version, full v2 support)
|
|
318
|
+
* @param {Object} theme - Theme object (v1 or v2)
|
|
319
|
+
* @returns {Promise<string>} CSS content with all v2 features
|
|
320
|
+
*/
|
|
321
|
+
export async function generateThemeCSSAsync(theme) {
|
|
322
|
+
if (isV2Theme(theme)) {
|
|
323
|
+
const v2 = await getThemeV2Module();
|
|
324
|
+
return v2.generateV2ThemeCSS(theme);
|
|
325
|
+
}
|
|
326
|
+
return generateThemeCSS(theme);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Process theme with unified processor (async)
|
|
331
|
+
* @param {Object} theme - Theme object (v1 or v2)
|
|
332
|
+
* @param {Object} options - Processing options
|
|
333
|
+
* @returns {Promise<Object>} Processed theme result
|
|
334
|
+
*/
|
|
335
|
+
export async function processThemeUnified(theme, options = {}) {
|
|
336
|
+
const v2 = await getThemeV2Module();
|
|
337
|
+
return v2.processTheme(theme, options);
|
|
338
|
+
}
|
|
339
|
+
|
|
247
340
|
/**
|
|
248
341
|
* Generate components.json for shadcn
|
|
249
342
|
* @param {Object} theme - Theme object
|
|
@@ -569,3 +662,652 @@ export function hslToHex(hsl) {
|
|
|
569
662
|
|
|
570
663
|
return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
|
|
571
664
|
}
|
|
665
|
+
|
|
666
|
+
// ============================================================================
|
|
667
|
+
// THEME REBUILD & ROLLBACK FUNCTIONS
|
|
668
|
+
// ============================================================================
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Directories to scan for color references (standard React/Next.js paths)
|
|
672
|
+
*/
|
|
673
|
+
export const SCAN_DIRECTORIES = ['app', 'components', 'src', 'pages'];
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* File extensions to scan
|
|
677
|
+
*/
|
|
678
|
+
export const SCAN_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js'];
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Directories to always exclude from scanning
|
|
682
|
+
*/
|
|
683
|
+
export const EXCLUDE_DIRS = ['node_modules', '.git', '.omgkit', 'dist', 'build', '.next', 'out'];
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Color patterns to detect non-compliant colors
|
|
687
|
+
*/
|
|
688
|
+
export const COLOR_PATTERNS = {
|
|
689
|
+
// Tailwind default colors (should use theme vars)
|
|
690
|
+
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,
|
|
691
|
+
|
|
692
|
+
// Hardcoded hex colors in className or style
|
|
693
|
+
hexColors: /#([0-9A-Fa-f]{3}){1,2}\b/g,
|
|
694
|
+
|
|
695
|
+
// Hardcoded RGB/HSL in styles
|
|
696
|
+
rgbHsl: /\b(rgb|hsl)a?\([^)]+\)/g
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Mapping of hardcoded Tailwind colors to theme variables
|
|
701
|
+
*/
|
|
702
|
+
export const THEME_VAR_MAP = {
|
|
703
|
+
// Background mappings
|
|
704
|
+
'bg-white': 'bg-background',
|
|
705
|
+
'bg-gray-50': 'bg-muted',
|
|
706
|
+
'bg-gray-100': 'bg-muted',
|
|
707
|
+
'bg-gray-200': 'bg-muted',
|
|
708
|
+
'bg-gray-900': 'bg-foreground',
|
|
709
|
+
'bg-slate-50': 'bg-muted',
|
|
710
|
+
'bg-slate-100': 'bg-muted',
|
|
711
|
+
'bg-slate-900': 'bg-foreground',
|
|
712
|
+
'bg-zinc-50': 'bg-muted',
|
|
713
|
+
'bg-zinc-100': 'bg-muted',
|
|
714
|
+
'bg-zinc-900': 'bg-foreground',
|
|
715
|
+
|
|
716
|
+
// Text mappings
|
|
717
|
+
'text-black': 'text-foreground',
|
|
718
|
+
'text-white': 'text-background',
|
|
719
|
+
'text-gray-900': 'text-foreground',
|
|
720
|
+
'text-gray-800': 'text-foreground',
|
|
721
|
+
'text-gray-700': 'text-foreground',
|
|
722
|
+
'text-gray-600': 'text-muted-foreground',
|
|
723
|
+
'text-gray-500': 'text-muted-foreground',
|
|
724
|
+
'text-gray-400': 'text-muted-foreground',
|
|
725
|
+
'text-slate-900': 'text-foreground',
|
|
726
|
+
'text-slate-600': 'text-muted-foreground',
|
|
727
|
+
'text-slate-500': 'text-muted-foreground',
|
|
728
|
+
'text-zinc-900': 'text-foreground',
|
|
729
|
+
'text-zinc-600': 'text-muted-foreground',
|
|
730
|
+
'text-zinc-500': 'text-muted-foreground',
|
|
731
|
+
|
|
732
|
+
// Border mappings
|
|
733
|
+
'border-gray-100': 'border-border',
|
|
734
|
+
'border-gray-200': 'border-border',
|
|
735
|
+
'border-gray-300': 'border-input',
|
|
736
|
+
'border-slate-200': 'border-border',
|
|
737
|
+
'border-slate-300': 'border-input',
|
|
738
|
+
'border-zinc-200': 'border-border',
|
|
739
|
+
'border-zinc-300': 'border-input',
|
|
740
|
+
|
|
741
|
+
// Primary/accent colors (common patterns)
|
|
742
|
+
'bg-blue-500': 'bg-primary',
|
|
743
|
+
'bg-blue-600': 'bg-primary',
|
|
744
|
+
'bg-blue-700': 'bg-primary',
|
|
745
|
+
'text-blue-500': 'text-primary',
|
|
746
|
+
'text-blue-600': 'text-primary',
|
|
747
|
+
'text-blue-700': 'text-primary',
|
|
748
|
+
'ring-blue-500': 'ring-ring',
|
|
749
|
+
'ring-blue-600': 'ring-ring',
|
|
750
|
+
|
|
751
|
+
// Destructive
|
|
752
|
+
'bg-red-500': 'bg-destructive',
|
|
753
|
+
'bg-red-600': 'bg-destructive',
|
|
754
|
+
'text-red-500': 'text-destructive',
|
|
755
|
+
'text-red-600': 'text-destructive',
|
|
756
|
+
'border-red-500': 'border-destructive',
|
|
757
|
+
|
|
758
|
+
// Secondary/accent patterns
|
|
759
|
+
'bg-gray-100': 'bg-secondary',
|
|
760
|
+
'bg-slate-100': 'bg-secondary',
|
|
761
|
+
'hover:bg-gray-100': 'hover:bg-accent',
|
|
762
|
+
'hover:bg-slate-100': 'hover:bg-accent'
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Recursively get all files in a directory
|
|
767
|
+
* @param {string} dir - Directory to scan
|
|
768
|
+
* @param {string[]} extensions - File extensions to include
|
|
769
|
+
* @param {string[]} excludeDirs - Directories to exclude
|
|
770
|
+
* @returns {string[]} Array of file paths
|
|
771
|
+
*/
|
|
772
|
+
function getFilesRecursive(dir, extensions, excludeDirs) {
|
|
773
|
+
const files = [];
|
|
774
|
+
if (!existsSync(dir)) return files;
|
|
775
|
+
|
|
776
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
777
|
+
|
|
778
|
+
for (const entry of entries) {
|
|
779
|
+
const fullPath = join(dir, entry.name);
|
|
780
|
+
|
|
781
|
+
if (entry.isDirectory()) {
|
|
782
|
+
if (!excludeDirs.includes(entry.name)) {
|
|
783
|
+
files.push(...getFilesRecursive(fullPath, extensions, excludeDirs));
|
|
784
|
+
}
|
|
785
|
+
} else if (entry.isFile()) {
|
|
786
|
+
const ext = '.' + entry.name.split('.').pop();
|
|
787
|
+
if (extensions.includes(ext)) {
|
|
788
|
+
files.push(fullPath);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
return files;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Scan project for files with non-compliant color references
|
|
798
|
+
* @param {string} projectDir - Project root directory
|
|
799
|
+
* @returns {Object} { files: [], totalReferences: number, nonCompliant: [], compliant: number }
|
|
800
|
+
*/
|
|
801
|
+
export function scanProjectColors(projectDir) {
|
|
802
|
+
const result = {
|
|
803
|
+
files: [],
|
|
804
|
+
totalReferences: 0,
|
|
805
|
+
nonCompliant: [],
|
|
806
|
+
compliant: 0,
|
|
807
|
+
scannedFiles: 0
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
// Find all files to scan
|
|
811
|
+
const filesToScan = [];
|
|
812
|
+
for (const scanDir of SCAN_DIRECTORIES) {
|
|
813
|
+
const fullPath = join(projectDir, scanDir);
|
|
814
|
+
if (existsSync(fullPath)) {
|
|
815
|
+
filesToScan.push(...getFilesRecursive(fullPath, SCAN_EXTENSIONS, EXCLUDE_DIRS));
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
result.scannedFiles = filesToScan.length;
|
|
820
|
+
|
|
821
|
+
// Scan each file
|
|
822
|
+
for (const filePath of filesToScan) {
|
|
823
|
+
try {
|
|
824
|
+
const content = readFileSync(filePath, 'utf8');
|
|
825
|
+
const lines = content.split('\n');
|
|
826
|
+
const relativePath = filePath.replace(projectDir + '/', '');
|
|
827
|
+
const fileMatches = [];
|
|
828
|
+
|
|
829
|
+
for (let i = 0; i < lines.length; i++) {
|
|
830
|
+
const line = lines[i];
|
|
831
|
+
const lineNum = i + 1;
|
|
832
|
+
|
|
833
|
+
// Check for Tailwind default colors
|
|
834
|
+
let match;
|
|
835
|
+
const pattern = new RegExp(COLOR_PATTERNS.tailwindDefaults.source, 'g');
|
|
836
|
+
while ((match = pattern.exec(line)) !== null) {
|
|
837
|
+
result.totalReferences++;
|
|
838
|
+
const fullMatch = match[0];
|
|
839
|
+
|
|
840
|
+
// Check if this has a theme-compliant mapping
|
|
841
|
+
const mapping = THEME_VAR_MAP[fullMatch];
|
|
842
|
+
if (mapping) {
|
|
843
|
+
fileMatches.push({
|
|
844
|
+
file: relativePath,
|
|
845
|
+
line: lineNum,
|
|
846
|
+
column: match.index,
|
|
847
|
+
match: fullMatch,
|
|
848
|
+
suggestion: mapping,
|
|
849
|
+
type: 'tailwind-default',
|
|
850
|
+
fixable: true
|
|
851
|
+
});
|
|
852
|
+
result.nonCompliant.push({
|
|
853
|
+
file: relativePath,
|
|
854
|
+
line: lineNum,
|
|
855
|
+
match: fullMatch,
|
|
856
|
+
suggestion: mapping
|
|
857
|
+
});
|
|
858
|
+
} else {
|
|
859
|
+
// Unmapped color - warn only
|
|
860
|
+
fileMatches.push({
|
|
861
|
+
file: relativePath,
|
|
862
|
+
line: lineNum,
|
|
863
|
+
column: match.index,
|
|
864
|
+
match: fullMatch,
|
|
865
|
+
suggestion: null,
|
|
866
|
+
type: 'unmapped',
|
|
867
|
+
fixable: false
|
|
868
|
+
});
|
|
869
|
+
result.nonCompliant.push({
|
|
870
|
+
file: relativePath,
|
|
871
|
+
line: lineNum,
|
|
872
|
+
match: fullMatch,
|
|
873
|
+
suggestion: null
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Check for hex colors in className or style attributes
|
|
879
|
+
const hexPattern = new RegExp(COLOR_PATTERNS.hexColors.source, 'g');
|
|
880
|
+
while ((match = hexPattern.exec(line)) !== null) {
|
|
881
|
+
// Only flag if it appears to be in className or style context
|
|
882
|
+
const before = line.slice(0, match.index);
|
|
883
|
+
if (before.includes('className') || before.includes('style') || before.includes('bg-[') || before.includes('text-[')) {
|
|
884
|
+
result.totalReferences++;
|
|
885
|
+
fileMatches.push({
|
|
886
|
+
file: relativePath,
|
|
887
|
+
line: lineNum,
|
|
888
|
+
column: match.index,
|
|
889
|
+
match: match[0],
|
|
890
|
+
suggestion: 'Use CSS variable (e.g., bg-background)',
|
|
891
|
+
type: 'hex-color',
|
|
892
|
+
fixable: false
|
|
893
|
+
});
|
|
894
|
+
result.nonCompliant.push({
|
|
895
|
+
file: relativePath,
|
|
896
|
+
line: lineNum,
|
|
897
|
+
match: match[0],
|
|
898
|
+
suggestion: 'Use CSS variable'
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
if (fileMatches.length > 0) {
|
|
905
|
+
result.files.push({
|
|
906
|
+
path: relativePath,
|
|
907
|
+
matches: fileMatches
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
} catch (err) {
|
|
911
|
+
// Skip files that can't be read
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
result.compliant = result.totalReferences - result.nonCompliant.length;
|
|
916
|
+
|
|
917
|
+
return result;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Create a theme backup before rebuild
|
|
922
|
+
* @param {string} projectDir - Project root directory
|
|
923
|
+
* @param {string} newThemeId - ID of new theme being applied
|
|
924
|
+
* @returns {Object} { success, backupId, backupPath, error }
|
|
925
|
+
*/
|
|
926
|
+
export function createThemeBackup(projectDir, newThemeId = 'unknown') {
|
|
927
|
+
const designDir = join(projectDir, '.omgkit', 'design');
|
|
928
|
+
const backupsDir = join(designDir, 'backups');
|
|
929
|
+
|
|
930
|
+
// Create backups directory
|
|
931
|
+
mkdirSync(backupsDir, { recursive: true });
|
|
932
|
+
|
|
933
|
+
// Generate backup ID
|
|
934
|
+
const now = new Date();
|
|
935
|
+
const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
936
|
+
const backupId = `${timestamp}-${newThemeId}`;
|
|
937
|
+
const backupPath = join(backupsDir, backupId);
|
|
938
|
+
|
|
939
|
+
try {
|
|
940
|
+
mkdirSync(backupPath, { recursive: true });
|
|
941
|
+
|
|
942
|
+
// Get current theme
|
|
943
|
+
const currentTheme = getProjectTheme(projectDir);
|
|
944
|
+
const previousThemeId = currentTheme?.id || 'none';
|
|
945
|
+
|
|
946
|
+
// Create manifest
|
|
947
|
+
const manifest = {
|
|
948
|
+
id: backupId,
|
|
949
|
+
previousTheme: previousThemeId,
|
|
950
|
+
newTheme: newThemeId,
|
|
951
|
+
timestamp: now.toISOString(),
|
|
952
|
+
changedFiles: []
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
// Backup theme.json if exists
|
|
956
|
+
const themeJsonPath = join(designDir, 'theme.json');
|
|
957
|
+
if (existsSync(themeJsonPath)) {
|
|
958
|
+
const content = readFileSync(themeJsonPath, 'utf8');
|
|
959
|
+
writeFileSync(join(backupPath, 'theme.json.bak'), content);
|
|
960
|
+
manifest.changedFiles.push({ path: '.omgkit/design/theme.json', backup: 'theme.json.bak' });
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Backup theme.css if exists
|
|
964
|
+
const themeCssPath = join(designDir, 'theme.css');
|
|
965
|
+
if (existsSync(themeCssPath)) {
|
|
966
|
+
const content = readFileSync(themeCssPath, 'utf8');
|
|
967
|
+
writeFileSync(join(backupPath, 'theme.css.bak'), content);
|
|
968
|
+
manifest.changedFiles.push({ path: '.omgkit/design/theme.css', backup: 'theme.css.bak' });
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Backup tailwind.config.ts if exists
|
|
972
|
+
const tailwindConfigPath = join(projectDir, 'tailwind.config.ts');
|
|
973
|
+
if (existsSync(tailwindConfigPath)) {
|
|
974
|
+
const content = readFileSync(tailwindConfigPath, 'utf8');
|
|
975
|
+
writeFileSync(join(backupPath, 'tailwind.config.ts.bak'), content);
|
|
976
|
+
manifest.changedFiles.push({ path: 'tailwind.config.ts', backup: 'tailwind.config.ts.bak' });
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// Also check for .js version
|
|
980
|
+
const tailwindConfigJsPath = join(projectDir, 'tailwind.config.js');
|
|
981
|
+
if (existsSync(tailwindConfigJsPath)) {
|
|
982
|
+
const content = readFileSync(tailwindConfigJsPath, 'utf8');
|
|
983
|
+
writeFileSync(join(backupPath, 'tailwind.config.js.bak'), content);
|
|
984
|
+
manifest.changedFiles.push({ path: 'tailwind.config.js', backup: 'tailwind.config.js.bak' });
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// Write manifest
|
|
988
|
+
writeFileSync(join(backupPath, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
989
|
+
|
|
990
|
+
return { success: true, backupId, backupPath, manifest };
|
|
991
|
+
} catch (err) {
|
|
992
|
+
return { success: false, error: err.message };
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* List available theme backups
|
|
998
|
+
* @param {string} projectDir - Project root directory
|
|
999
|
+
* @returns {Array} Array of backup info objects
|
|
1000
|
+
*/
|
|
1001
|
+
export function listThemeBackups(projectDir) {
|
|
1002
|
+
const backupsDir = join(projectDir, '.omgkit', 'design', 'backups');
|
|
1003
|
+
if (!existsSync(backupsDir)) return [];
|
|
1004
|
+
|
|
1005
|
+
const backups = [];
|
|
1006
|
+
const entries = readdirSync(backupsDir, { withFileTypes: true });
|
|
1007
|
+
|
|
1008
|
+
for (const entry of entries) {
|
|
1009
|
+
if (!entry.isDirectory()) continue;
|
|
1010
|
+
|
|
1011
|
+
const manifestPath = join(backupsDir, entry.name, 'manifest.json');
|
|
1012
|
+
if (!existsSync(manifestPath)) continue;
|
|
1013
|
+
|
|
1014
|
+
try {
|
|
1015
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
1016
|
+
backups.push({
|
|
1017
|
+
id: manifest.id,
|
|
1018
|
+
previousTheme: manifest.previousTheme,
|
|
1019
|
+
newTheme: manifest.newTheme,
|
|
1020
|
+
timestamp: manifest.timestamp,
|
|
1021
|
+
date: new Date(manifest.timestamp).toLocaleString(),
|
|
1022
|
+
filesChanged: manifest.changedFiles.length,
|
|
1023
|
+
path: join(backupsDir, entry.name)
|
|
1024
|
+
});
|
|
1025
|
+
} catch {
|
|
1026
|
+
// Skip invalid backup
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Sort by timestamp descending (newest first)
|
|
1031
|
+
return backups.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* Rollback to a previous theme state
|
|
1036
|
+
* @param {string} projectDir - Project root directory
|
|
1037
|
+
* @param {string} backupId - Backup ID to restore (optional, defaults to latest)
|
|
1038
|
+
* @returns {Object} { success, restoredTheme, restoredFiles, error }
|
|
1039
|
+
*/
|
|
1040
|
+
export function rollbackTheme(projectDir, backupId = null) {
|
|
1041
|
+
const backups = listThemeBackups(projectDir);
|
|
1042
|
+
|
|
1043
|
+
if (backups.length === 0) {
|
|
1044
|
+
return { success: false, error: 'No theme backups found' };
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Find backup to restore
|
|
1048
|
+
let backup;
|
|
1049
|
+
if (backupId) {
|
|
1050
|
+
backup = backups.find(b => b.id === backupId);
|
|
1051
|
+
if (!backup) {
|
|
1052
|
+
return { success: false, error: `Backup not found: ${backupId}` };
|
|
1053
|
+
}
|
|
1054
|
+
} else {
|
|
1055
|
+
backup = backups[0]; // Latest
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
try {
|
|
1059
|
+
const manifestPath = join(backup.path, 'manifest.json');
|
|
1060
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
1061
|
+
const restoredFiles = [];
|
|
1062
|
+
|
|
1063
|
+
// Create a new backup before rollback (safety)
|
|
1064
|
+
createThemeBackup(projectDir, `rollback-from-${manifest.newTheme}`);
|
|
1065
|
+
|
|
1066
|
+
// Restore each file
|
|
1067
|
+
for (const file of manifest.changedFiles) {
|
|
1068
|
+
const backupFilePath = join(backup.path, file.backup);
|
|
1069
|
+
const targetPath = join(projectDir, file.path);
|
|
1070
|
+
|
|
1071
|
+
if (existsSync(backupFilePath)) {
|
|
1072
|
+
const content = readFileSync(backupFilePath, 'utf8');
|
|
1073
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
1074
|
+
writeFileSync(targetPath, content);
|
|
1075
|
+
restoredFiles.push(file.path);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
return {
|
|
1080
|
+
success: true,
|
|
1081
|
+
restoredTheme: manifest.previousTheme,
|
|
1082
|
+
restoredFiles,
|
|
1083
|
+
backupUsed: backup.id
|
|
1084
|
+
};
|
|
1085
|
+
} catch (err) {
|
|
1086
|
+
return { success: false, error: err.message };
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Update file replacing hardcoded colors with theme variables
|
|
1092
|
+
* @param {string} filePath - File to update
|
|
1093
|
+
* @param {string} projectDir - Project root directory
|
|
1094
|
+
* @returns {Object} { changed, replacements, content }
|
|
1095
|
+
*/
|
|
1096
|
+
export function updateFileColors(filePath, projectDir) {
|
|
1097
|
+
const fullPath = join(projectDir, filePath);
|
|
1098
|
+
if (!existsSync(fullPath)) {
|
|
1099
|
+
return { changed: false, replacements: [], error: 'File not found' };
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
let content = readFileSync(fullPath, 'utf8');
|
|
1103
|
+
const replacements = [];
|
|
1104
|
+
let changed = false;
|
|
1105
|
+
|
|
1106
|
+
// Apply theme variable mappings
|
|
1107
|
+
for (const [pattern, replacement] of Object.entries(THEME_VAR_MAP)) {
|
|
1108
|
+
// Escape special regex characters in pattern
|
|
1109
|
+
const escapedPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1110
|
+
const regex = new RegExp(`\\b${escapedPattern}\\b`, 'g');
|
|
1111
|
+
|
|
1112
|
+
const matches = content.match(regex);
|
|
1113
|
+
if (matches && matches.length > 0) {
|
|
1114
|
+
content = content.replace(regex, replacement);
|
|
1115
|
+
replacements.push({
|
|
1116
|
+
from: pattern,
|
|
1117
|
+
to: replacement,
|
|
1118
|
+
count: matches.length
|
|
1119
|
+
});
|
|
1120
|
+
changed = true;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
return { changed, replacements, content };
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/**
|
|
1128
|
+
* Update project's tailwind.config file with new theme
|
|
1129
|
+
* @param {Object} theme - Theme object
|
|
1130
|
+
* @param {string} projectDir - Project root directory
|
|
1131
|
+
* @returns {Object} { success, path, error }
|
|
1132
|
+
*/
|
|
1133
|
+
export function updateProjectTailwindConfig(theme, projectDir) {
|
|
1134
|
+
// Check for tailwind.config.ts first, then .js
|
|
1135
|
+
let configPath = join(projectDir, 'tailwind.config.ts');
|
|
1136
|
+
let isTs = true;
|
|
1137
|
+
|
|
1138
|
+
if (!existsSync(configPath)) {
|
|
1139
|
+
configPath = join(projectDir, 'tailwind.config.js');
|
|
1140
|
+
isTs = false;
|
|
1141
|
+
if (!existsSync(configPath)) {
|
|
1142
|
+
// Create new config
|
|
1143
|
+
configPath = join(projectDir, 'tailwind.config.ts');
|
|
1144
|
+
isTs = true;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
try {
|
|
1149
|
+
const newConfig = generateTailwindConfig(theme);
|
|
1150
|
+
writeFileSync(configPath, newConfig);
|
|
1151
|
+
return { success: true, path: configPath, isTs };
|
|
1152
|
+
} catch (err) {
|
|
1153
|
+
return { success: false, error: err.message };
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/**
|
|
1158
|
+
* Ensure globals.css imports theme.css
|
|
1159
|
+
* @param {string} projectDir - Project root directory
|
|
1160
|
+
* @returns {Object} { updated, path, alreadyImported }
|
|
1161
|
+
*/
|
|
1162
|
+
export function ensureThemeImport(projectDir) {
|
|
1163
|
+
// Look for globals.css in common locations
|
|
1164
|
+
const possiblePaths = [
|
|
1165
|
+
join(projectDir, 'app', 'globals.css'),
|
|
1166
|
+
join(projectDir, 'src', 'app', 'globals.css'),
|
|
1167
|
+
join(projectDir, 'styles', 'globals.css'),
|
|
1168
|
+
join(projectDir, 'src', 'styles', 'globals.css')
|
|
1169
|
+
];
|
|
1170
|
+
|
|
1171
|
+
let globalsPath = null;
|
|
1172
|
+
for (const p of possiblePaths) {
|
|
1173
|
+
if (existsSync(p)) {
|
|
1174
|
+
globalsPath = p;
|
|
1175
|
+
break;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
if (!globalsPath) {
|
|
1180
|
+
return { updated: false, path: null, error: 'globals.css not found' };
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
let content = readFileSync(globalsPath, 'utf8');
|
|
1184
|
+
const themeImport = "@import '../.omgkit/design/theme.css';";
|
|
1185
|
+
const altThemeImport = "@import '../../.omgkit/design/theme.css';";
|
|
1186
|
+
|
|
1187
|
+
// Check if already imported
|
|
1188
|
+
if (content.includes('.omgkit/design/theme.css')) {
|
|
1189
|
+
return { updated: false, path: globalsPath, alreadyImported: true };
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// Add import at the beginning
|
|
1193
|
+
const relativePath = globalsPath.includes('/src/') ? altThemeImport : themeImport;
|
|
1194
|
+
content = `${relativePath}\n${content}`;
|
|
1195
|
+
|
|
1196
|
+
writeFileSync(globalsPath, content);
|
|
1197
|
+
return { updated: true, path: globalsPath, alreadyImported: false };
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/**
|
|
1201
|
+
* Rebuild entire project with a new theme
|
|
1202
|
+
* @param {string} projectDir - Project root directory
|
|
1203
|
+
* @param {string} themeId - New theme ID
|
|
1204
|
+
* @param {Object} options - { dryRun, force, fixColors }
|
|
1205
|
+
* @returns {Object} { success, backupPath, changedFiles, warnings, error }
|
|
1206
|
+
*/
|
|
1207
|
+
export function rebuildProjectTheme(projectDir, themeId, options = {}) {
|
|
1208
|
+
const { dryRun = false, force = false, fixColors = true } = options;
|
|
1209
|
+
|
|
1210
|
+
// Validate project has .omgkit
|
|
1211
|
+
if (!existsSync(join(projectDir, '.omgkit'))) {
|
|
1212
|
+
return { success: false, error: 'Not an OMGKIT project. Run: omgkit init' };
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// Get new theme
|
|
1216
|
+
const newTheme = getThemeById(themeId);
|
|
1217
|
+
if (!newTheme) {
|
|
1218
|
+
return { success: false, error: `Theme not found: ${themeId}. Run /design:themes to see available themes.` };
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// Validate theme
|
|
1222
|
+
const validation = validateTheme(newTheme);
|
|
1223
|
+
if (!validation.valid) {
|
|
1224
|
+
return { success: false, error: `Invalid theme: ${validation.errors.join(', ')}` };
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
const result = {
|
|
1228
|
+
success: true,
|
|
1229
|
+
newTheme: themeId,
|
|
1230
|
+
backupId: null,
|
|
1231
|
+
backupPath: null,
|
|
1232
|
+
changedFiles: [],
|
|
1233
|
+
fixedColors: [],
|
|
1234
|
+
warnings: [],
|
|
1235
|
+
dryRun
|
|
1236
|
+
};
|
|
1237
|
+
|
|
1238
|
+
// Step 1: Create backup (unless dry-run)
|
|
1239
|
+
if (!dryRun) {
|
|
1240
|
+
const backup = createThemeBackup(projectDir, themeId);
|
|
1241
|
+
if (!backup.success) {
|
|
1242
|
+
return { success: false, error: `Failed to create backup: ${backup.error}` };
|
|
1243
|
+
}
|
|
1244
|
+
result.backupId = backup.backupId;
|
|
1245
|
+
result.backupPath = backup.backupPath;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// Step 2: Apply new theme
|
|
1249
|
+
if (!dryRun) {
|
|
1250
|
+
const applied = applyThemeToProject(newTheme, projectDir);
|
|
1251
|
+
result.changedFiles.push(applied.themeJson.replace(projectDir + '/', ''));
|
|
1252
|
+
result.changedFiles.push(applied.themeCss.replace(projectDir + '/', ''));
|
|
1253
|
+
} else {
|
|
1254
|
+
result.changedFiles.push('.omgkit/design/theme.json');
|
|
1255
|
+
result.changedFiles.push('.omgkit/design/theme.css');
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// Step 3: Update tailwind config
|
|
1259
|
+
if (!dryRun) {
|
|
1260
|
+
const tailwindResult = updateProjectTailwindConfig(newTheme, projectDir);
|
|
1261
|
+
if (tailwindResult.success) {
|
|
1262
|
+
result.changedFiles.push(tailwindResult.path.replace(projectDir + '/', ''));
|
|
1263
|
+
}
|
|
1264
|
+
} else {
|
|
1265
|
+
result.changedFiles.push('tailwind.config.ts');
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// Step 4: Ensure theme import in globals.css
|
|
1269
|
+
if (!dryRun) {
|
|
1270
|
+
const importResult = ensureThemeImport(projectDir);
|
|
1271
|
+
if (importResult.updated) {
|
|
1272
|
+
result.changedFiles.push(importResult.path.replace(projectDir + '/', ''));
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// Step 5: Scan and fix colors (if enabled)
|
|
1277
|
+
if (fixColors) {
|
|
1278
|
+
const scanResult = scanProjectColors(projectDir);
|
|
1279
|
+
|
|
1280
|
+
for (const fileInfo of scanResult.files) {
|
|
1281
|
+
const fixableMatches = fileInfo.matches.filter(m => m.fixable);
|
|
1282
|
+
|
|
1283
|
+
if (fixableMatches.length > 0) {
|
|
1284
|
+
if (!dryRun) {
|
|
1285
|
+
const updateResult = updateFileColors(fileInfo.path, projectDir);
|
|
1286
|
+
if (updateResult.changed) {
|
|
1287
|
+
// Write updated content
|
|
1288
|
+
writeFileSync(join(projectDir, fileInfo.path), updateResult.content);
|
|
1289
|
+
result.changedFiles.push(fileInfo.path);
|
|
1290
|
+
result.fixedColors.push({
|
|
1291
|
+
file: fileInfo.path,
|
|
1292
|
+
replacements: updateResult.replacements
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
} else {
|
|
1296
|
+
// Dry run - just report what would be changed
|
|
1297
|
+
result.fixedColors.push({
|
|
1298
|
+
file: fileInfo.path,
|
|
1299
|
+
replacements: fixableMatches.map(m => ({ from: m.match, to: m.suggestion }))
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// Add warnings for unfixable colors
|
|
1305
|
+
const unfixable = fileInfo.matches.filter(m => !m.fixable);
|
|
1306
|
+
for (const u of unfixable) {
|
|
1307
|
+
result.warnings.push(`${u.file}:${u.line} - ${u.match} (manual review needed)`);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
return result;
|
|
1313
|
+
}
|