real-prototypes-skill 0.1.1 → 0.1.3
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/.claude/skills/real-prototypes-skill/SKILL.md +212 -16
- package/.claude/skills/real-prototypes-skill/cli.js +523 -17
- package/.claude/skills/real-prototypes-skill/scripts/detect-prototype.js +652 -0
- package/.claude/skills/real-prototypes-skill/scripts/extract-components.js +731 -0
- package/.claude/skills/real-prototypes-skill/scripts/extract-css.js +557 -0
- package/.claude/skills/real-prototypes-skill/scripts/generate-plan.js +744 -0
- package/.claude/skills/real-prototypes-skill/scripts/html-to-react.js +645 -0
- package/.claude/skills/real-prototypes-skill/scripts/inject-component.js +604 -0
- package/.claude/skills/real-prototypes-skill/scripts/project-structure.js +457 -0
- package/.claude/skills/real-prototypes-skill/scripts/visual-diff.js +474 -0
- package/.claude/skills/real-prototypes-skill/validation/color-validator.js +496 -0
- package/bin/cli.js +66 -15
- package/package.json +4 -1
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Color Validator Module
|
|
5
|
+
*
|
|
6
|
+
* Validates that all colors used in generated prototype files
|
|
7
|
+
* match the design tokens extracted from the captured platform.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Scans TSX/JSX/CSS files for color values
|
|
11
|
+
* - Extracts hex colors from inline styles, CSS, and Tailwind arbitrary values
|
|
12
|
+
* - Finds closest matching color in design tokens
|
|
13
|
+
* - Reports violations with line numbers and suggestions
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* node color-validator.js --project <name>
|
|
17
|
+
* node color-validator.js --proto ./prototype --tokens ./references/design-tokens.json
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
|
|
23
|
+
// Patterns to find colors
|
|
24
|
+
const COLOR_PATTERNS = {
|
|
25
|
+
// Hex colors (#fff, #ffffff, #ffffffff)
|
|
26
|
+
hex: /#([0-9a-fA-F]{3,8})\b/g,
|
|
27
|
+
|
|
28
|
+
// RGB/RGBA
|
|
29
|
+
rgb: /rgba?\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*[\d.]+)?\s*\)/g,
|
|
30
|
+
|
|
31
|
+
// HSL/HSLA
|
|
32
|
+
hsl: /hsla?\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})%?\s*,\s*(\d{1,3})%?\s*(?:,\s*[\d.]+)?\s*\)/g,
|
|
33
|
+
|
|
34
|
+
// Tailwind default colors (not allowed)
|
|
35
|
+
tailwindDefault: /\b(bg|text|border|ring|fill|stroke)-(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(\d{2,3})\b/g,
|
|
36
|
+
|
|
37
|
+
// Tailwind arbitrary values
|
|
38
|
+
tailwindArbitrary: /\[(#[0-9a-fA-F]{3,8})\]/g
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Named colors to avoid
|
|
42
|
+
const NAMED_COLORS = [
|
|
43
|
+
'red', 'blue', 'green', 'yellow', 'orange', 'purple', 'pink',
|
|
44
|
+
'gray', 'grey', 'black', 'white', 'cyan', 'magenta', 'lime',
|
|
45
|
+
'maroon', 'navy', 'olive', 'teal', 'aqua', 'fuchsia', 'silver'
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
class ColorValidator {
|
|
49
|
+
constructor(options = {}) {
|
|
50
|
+
this.designTokens = options.designTokens || null;
|
|
51
|
+
this.prototypeDir = options.prototypeDir || './prototype';
|
|
52
|
+
this.violations = [];
|
|
53
|
+
this.validColors = new Set();
|
|
54
|
+
this.colorMap = new Map(); // hex -> token name mapping
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Load design tokens from file
|
|
59
|
+
*/
|
|
60
|
+
loadDesignTokens(tokensPath) {
|
|
61
|
+
if (!fs.existsSync(tokensPath)) {
|
|
62
|
+
throw new Error(`Design tokens file not found: ${tokensPath}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.designTokens = JSON.parse(fs.readFileSync(tokensPath, 'utf-8'));
|
|
66
|
+
this.buildColorMap();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build a map of valid colors from design tokens
|
|
71
|
+
*/
|
|
72
|
+
buildColorMap() {
|
|
73
|
+
if (!this.designTokens) return;
|
|
74
|
+
|
|
75
|
+
const extractColors = (obj, prefix = '') => {
|
|
76
|
+
if (!obj || typeof obj !== 'object') return;
|
|
77
|
+
|
|
78
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
79
|
+
const tokenName = prefix ? `${prefix}.${key}` : key;
|
|
80
|
+
|
|
81
|
+
if (typeof value === 'string' && value.startsWith('#')) {
|
|
82
|
+
const normalizedHex = this.normalizeHex(value);
|
|
83
|
+
this.validColors.add(normalizedHex);
|
|
84
|
+
this.colorMap.set(normalizedHex, tokenName);
|
|
85
|
+
} else if (typeof value === 'object') {
|
|
86
|
+
extractColors(value, tokenName);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Extract from colors section
|
|
92
|
+
if (this.designTokens.colors) {
|
|
93
|
+
extractColors(this.designTokens.colors);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Extract from raw colors if available
|
|
97
|
+
if (this.designTokens.rawColors) {
|
|
98
|
+
for (const color of this.designTokens.rawColors) {
|
|
99
|
+
const normalizedHex = this.normalizeHex(color.hex || color);
|
|
100
|
+
this.validColors.add(normalizedHex);
|
|
101
|
+
if (!this.colorMap.has(normalizedHex)) {
|
|
102
|
+
this.colorMap.set(normalizedHex, `raw.${normalizedHex}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Also add black and white as they're always valid
|
|
108
|
+
this.validColors.add('#000000');
|
|
109
|
+
this.validColors.add('#ffffff');
|
|
110
|
+
this.colorMap.set('#000000', 'black');
|
|
111
|
+
this.colorMap.set('#ffffff', 'white');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Normalize hex color to 6-digit lowercase format
|
|
116
|
+
*/
|
|
117
|
+
normalizeHex(hex) {
|
|
118
|
+
if (!hex) return null;
|
|
119
|
+
|
|
120
|
+
let cleaned = hex.toLowerCase().replace('#', '');
|
|
121
|
+
|
|
122
|
+
// Convert 3-digit to 6-digit
|
|
123
|
+
if (cleaned.length === 3) {
|
|
124
|
+
cleaned = cleaned.split('').map(c => c + c).join('');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Handle 8-digit (with alpha)
|
|
128
|
+
if (cleaned.length === 8) {
|
|
129
|
+
cleaned = cleaned.substring(0, 6);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return '#' + cleaned;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Convert RGB to hex
|
|
137
|
+
*/
|
|
138
|
+
rgbToHex(r, g, b) {
|
|
139
|
+
return '#' + [r, g, b].map(x => {
|
|
140
|
+
const hex = parseInt(x, 10).toString(16);
|
|
141
|
+
return hex.length === 1 ? '0' + hex : hex;
|
|
142
|
+
}).join('');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Calculate color distance (simple Euclidean in RGB space)
|
|
147
|
+
*/
|
|
148
|
+
colorDistance(hex1, hex2) {
|
|
149
|
+
const rgb1 = this.hexToRgb(hex1);
|
|
150
|
+
const rgb2 = this.hexToRgb(hex2);
|
|
151
|
+
|
|
152
|
+
if (!rgb1 || !rgb2) return Infinity;
|
|
153
|
+
|
|
154
|
+
return Math.sqrt(
|
|
155
|
+
Math.pow(rgb1.r - rgb2.r, 2) +
|
|
156
|
+
Math.pow(rgb1.g - rgb2.g, 2) +
|
|
157
|
+
Math.pow(rgb1.b - rgb2.b, 2)
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Convert hex to RGB object
|
|
163
|
+
*/
|
|
164
|
+
hexToRgb(hex) {
|
|
165
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
166
|
+
return result ? {
|
|
167
|
+
r: parseInt(result[1], 16),
|
|
168
|
+
g: parseInt(result[2], 16),
|
|
169
|
+
b: parseInt(result[3], 16)
|
|
170
|
+
} : null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Find closest matching color from design tokens
|
|
175
|
+
*/
|
|
176
|
+
findClosestColor(hex) {
|
|
177
|
+
const normalizedHex = this.normalizeHex(hex);
|
|
178
|
+
let closestColor = null;
|
|
179
|
+
let closestDistance = Infinity;
|
|
180
|
+
let closestName = null;
|
|
181
|
+
|
|
182
|
+
for (const [validHex, tokenName] of this.colorMap.entries()) {
|
|
183
|
+
const distance = this.colorDistance(normalizedHex, validHex);
|
|
184
|
+
if (distance < closestDistance) {
|
|
185
|
+
closestDistance = distance;
|
|
186
|
+
closestColor = validHex;
|
|
187
|
+
closestName = tokenName;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
hex: closestColor,
|
|
193
|
+
name: closestName,
|
|
194
|
+
distance: closestDistance,
|
|
195
|
+
isExact: closestDistance === 0
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Validate a single file
|
|
201
|
+
*/
|
|
202
|
+
validateFile(filePath) {
|
|
203
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
204
|
+
const lines = content.split('\n');
|
|
205
|
+
const relativePath = path.relative(this.prototypeDir, filePath);
|
|
206
|
+
const fileViolations = [];
|
|
207
|
+
|
|
208
|
+
lines.forEach((line, index) => {
|
|
209
|
+
const lineNumber = index + 1;
|
|
210
|
+
|
|
211
|
+
// Check for hex colors
|
|
212
|
+
const hexMatches = [...line.matchAll(COLOR_PATTERNS.hex)];
|
|
213
|
+
for (const match of hexMatches) {
|
|
214
|
+
const hex = this.normalizeHex(match[0]);
|
|
215
|
+
if (!this.validColors.has(hex)) {
|
|
216
|
+
const closest = this.findClosestColor(hex);
|
|
217
|
+
fileViolations.push({
|
|
218
|
+
file: relativePath,
|
|
219
|
+
line: lineNumber,
|
|
220
|
+
column: match.index + 1,
|
|
221
|
+
type: 'invalid-hex',
|
|
222
|
+
value: match[0],
|
|
223
|
+
normalized: hex,
|
|
224
|
+
suggestion: closest.hex,
|
|
225
|
+
suggestionName: closest.name,
|
|
226
|
+
distance: closest.distance,
|
|
227
|
+
context: line.trim().substring(0, 80)
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Check for RGB colors
|
|
233
|
+
const rgbMatches = [...line.matchAll(COLOR_PATTERNS.rgb)];
|
|
234
|
+
for (const match of rgbMatches) {
|
|
235
|
+
const hex = this.rgbToHex(match[1], match[2], match[3]);
|
|
236
|
+
const normalized = this.normalizeHex(hex);
|
|
237
|
+
if (!this.validColors.has(normalized)) {
|
|
238
|
+
const closest = this.findClosestColor(hex);
|
|
239
|
+
fileViolations.push({
|
|
240
|
+
file: relativePath,
|
|
241
|
+
line: lineNumber,
|
|
242
|
+
column: match.index + 1,
|
|
243
|
+
type: 'invalid-rgb',
|
|
244
|
+
value: match[0],
|
|
245
|
+
normalized: normalized,
|
|
246
|
+
suggestion: closest.hex,
|
|
247
|
+
suggestionName: closest.name,
|
|
248
|
+
distance: closest.distance,
|
|
249
|
+
context: line.trim().substring(0, 80)
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Check for Tailwind default colors (always a violation)
|
|
255
|
+
const tailwindMatches = [...line.matchAll(COLOR_PATTERNS.tailwindDefault)];
|
|
256
|
+
for (const match of tailwindMatches) {
|
|
257
|
+
fileViolations.push({
|
|
258
|
+
file: relativePath,
|
|
259
|
+
line: lineNumber,
|
|
260
|
+
column: match.index + 1,
|
|
261
|
+
type: 'tailwind-default',
|
|
262
|
+
value: match[0],
|
|
263
|
+
suggestion: 'Use inline style={{ }} with design token hex color',
|
|
264
|
+
context: line.trim().substring(0, 80)
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Check for Tailwind arbitrary values
|
|
269
|
+
const arbitraryMatches = [...line.matchAll(COLOR_PATTERNS.tailwindArbitrary)];
|
|
270
|
+
for (const match of arbitraryMatches) {
|
|
271
|
+
const hex = this.normalizeHex(match[1]);
|
|
272
|
+
if (!this.validColors.has(hex)) {
|
|
273
|
+
const closest = this.findClosestColor(hex);
|
|
274
|
+
fileViolations.push({
|
|
275
|
+
file: relativePath,
|
|
276
|
+
line: lineNumber,
|
|
277
|
+
column: match.index + 1,
|
|
278
|
+
type: 'invalid-arbitrary',
|
|
279
|
+
value: match[0],
|
|
280
|
+
normalized: hex,
|
|
281
|
+
suggestion: `[${closest.hex}]`,
|
|
282
|
+
suggestionName: closest.name,
|
|
283
|
+
distance: closest.distance,
|
|
284
|
+
context: line.trim().substring(0, 80)
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
this.violations.push(...fileViolations);
|
|
291
|
+
return fileViolations;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Recursively scan directory for files
|
|
296
|
+
*/
|
|
297
|
+
scanDirectory(dir, extensions = ['.tsx', '.jsx', '.ts', '.js', '.css', '.scss']) {
|
|
298
|
+
if (!fs.existsSync(dir)) return;
|
|
299
|
+
|
|
300
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
301
|
+
|
|
302
|
+
for (const entry of entries) {
|
|
303
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
304
|
+
|
|
305
|
+
const fullPath = path.join(dir, entry.name);
|
|
306
|
+
|
|
307
|
+
if (entry.isDirectory()) {
|
|
308
|
+
this.scanDirectory(fullPath, extensions);
|
|
309
|
+
} else if (entry.isFile()) {
|
|
310
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
311
|
+
if (extensions.includes(ext)) {
|
|
312
|
+
this.validateFile(fullPath);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Run validation on prototype directory
|
|
320
|
+
*/
|
|
321
|
+
validate() {
|
|
322
|
+
this.violations = [];
|
|
323
|
+
this.scanDirectory(this.prototypeDir);
|
|
324
|
+
return this.violations;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Get validation summary
|
|
329
|
+
*/
|
|
330
|
+
getSummary() {
|
|
331
|
+
const byType = {};
|
|
332
|
+
const byFile = {};
|
|
333
|
+
|
|
334
|
+
for (const violation of this.violations) {
|
|
335
|
+
byType[violation.type] = (byType[violation.type] || 0) + 1;
|
|
336
|
+
byFile[violation.file] = (byFile[violation.file] || 0) + 1;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
total: this.violations.length,
|
|
341
|
+
byType,
|
|
342
|
+
byFile,
|
|
343
|
+
validColorsCount: this.validColors.size,
|
|
344
|
+
passed: this.violations.length === 0
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Format violations for CLI output
|
|
350
|
+
*/
|
|
351
|
+
formatViolations() {
|
|
352
|
+
if (this.violations.length === 0) {
|
|
353
|
+
return '\x1b[32m✓ No color violations found\x1b[0m';
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const lines = [
|
|
357
|
+
`\x1b[31m✗ Found ${this.violations.length} color violation(s)\x1b[0m\n`
|
|
358
|
+
];
|
|
359
|
+
|
|
360
|
+
// Group by file
|
|
361
|
+
const byFile = {};
|
|
362
|
+
for (const v of this.violations) {
|
|
363
|
+
if (!byFile[v.file]) byFile[v.file] = [];
|
|
364
|
+
byFile[v.file].push(v);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
for (const [file, violations] of Object.entries(byFile)) {
|
|
368
|
+
lines.push(`\x1b[1m${file}\x1b[0m`);
|
|
369
|
+
|
|
370
|
+
for (const v of violations) {
|
|
371
|
+
lines.push(` Line ${v.line}: \x1b[33m${v.value}\x1b[0m`);
|
|
372
|
+
|
|
373
|
+
if (v.type === 'tailwind-default') {
|
|
374
|
+
lines.push(` \x1b[31mError:\x1b[0m Tailwind default colors are not allowed`);
|
|
375
|
+
lines.push(` \x1b[32mFix:\x1b[0m Use style={{ backgroundColor: "#hexcolor" }} with design token`);
|
|
376
|
+
} else {
|
|
377
|
+
lines.push(` \x1b[31mError:\x1b[0m Color ${v.normalized} not found in design-tokens.json`);
|
|
378
|
+
if (v.suggestion && v.suggestionName) {
|
|
379
|
+
lines.push(` \x1b[32mSuggestion:\x1b[0m Use ${v.suggestion} (${v.suggestionName})`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (v.context) {
|
|
384
|
+
lines.push(` \x1b[90mContext: ${v.context}\x1b[0m`);
|
|
385
|
+
}
|
|
386
|
+
lines.push('');
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return lines.join('\n');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Generate JSON report
|
|
395
|
+
*/
|
|
396
|
+
toJSON() {
|
|
397
|
+
return {
|
|
398
|
+
timestamp: new Date().toISOString(),
|
|
399
|
+
prototypeDir: this.prototypeDir,
|
|
400
|
+
summary: this.getSummary(),
|
|
401
|
+
validColors: Array.from(this.validColors),
|
|
402
|
+
violations: this.violations
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Validate colors in prototype against design tokens
|
|
409
|
+
*/
|
|
410
|
+
function validateColors(prototypeDir, tokensPath) {
|
|
411
|
+
const validator = new ColorValidator({ prototypeDir });
|
|
412
|
+
validator.loadDesignTokens(tokensPath);
|
|
413
|
+
validator.validate();
|
|
414
|
+
return validator;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// CLI execution
|
|
418
|
+
if (require.main === module) {
|
|
419
|
+
const args = process.argv.slice(2);
|
|
420
|
+
let prototypeDir = './prototype';
|
|
421
|
+
let tokensPath = './references/design-tokens.json';
|
|
422
|
+
let jsonOutput = false;
|
|
423
|
+
|
|
424
|
+
for (let i = 0; i < args.length; i++) {
|
|
425
|
+
switch (args[i]) {
|
|
426
|
+
case '--proto':
|
|
427
|
+
case '-p':
|
|
428
|
+
prototypeDir = args[++i];
|
|
429
|
+
break;
|
|
430
|
+
case '--tokens':
|
|
431
|
+
case '-t':
|
|
432
|
+
tokensPath = args[++i];
|
|
433
|
+
break;
|
|
434
|
+
case '--project':
|
|
435
|
+
const projectName = args[++i];
|
|
436
|
+
const SKILL_DIR = path.dirname(__dirname);
|
|
437
|
+
const PROJECTS_DIR = path.resolve(SKILL_DIR, '../../../projects');
|
|
438
|
+
prototypeDir = path.join(PROJECTS_DIR, projectName, 'prototype');
|
|
439
|
+
tokensPath = path.join(PROJECTS_DIR, projectName, 'references', 'design-tokens.json');
|
|
440
|
+
break;
|
|
441
|
+
case '--json':
|
|
442
|
+
jsonOutput = true;
|
|
443
|
+
break;
|
|
444
|
+
case '--help':
|
|
445
|
+
case '-h':
|
|
446
|
+
console.log(`
|
|
447
|
+
Usage: node color-validator.js [options]
|
|
448
|
+
|
|
449
|
+
Options:
|
|
450
|
+
--proto, -p <path> Path to prototype directory
|
|
451
|
+
--tokens, -t <path> Path to design-tokens.json
|
|
452
|
+
--project <name> Project name (sets proto and tokens paths)
|
|
453
|
+
--json Output as JSON
|
|
454
|
+
--help, -h Show this help
|
|
455
|
+
|
|
456
|
+
Examples:
|
|
457
|
+
node color-validator.js --project my-app
|
|
458
|
+
node color-validator.js --proto ./prototype --tokens ./references/design-tokens.json
|
|
459
|
+
`);
|
|
460
|
+
process.exit(0);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
try {
|
|
465
|
+
const validator = validateColors(prototypeDir, tokensPath);
|
|
466
|
+
|
|
467
|
+
if (jsonOutput) {
|
|
468
|
+
console.log(JSON.stringify(validator.toJSON(), null, 2));
|
|
469
|
+
} else {
|
|
470
|
+
console.log(`\n\x1b[1mColor Validation Report\x1b[0m`);
|
|
471
|
+
console.log(`Prototype: ${prototypeDir}`);
|
|
472
|
+
console.log(`Design Tokens: ${tokensPath}`);
|
|
473
|
+
console.log(`Valid Colors: ${validator.validColors.size}`);
|
|
474
|
+
console.log('');
|
|
475
|
+
console.log(validator.formatViolations());
|
|
476
|
+
|
|
477
|
+
const summary = validator.getSummary();
|
|
478
|
+
if (!summary.passed) {
|
|
479
|
+
console.log(`\n\x1b[1mSummary by Type:\x1b[0m`);
|
|
480
|
+
for (const [type, count] of Object.entries(summary.byType)) {
|
|
481
|
+
console.log(` ${type}: ${count}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
process.exit(validator.violations.length === 0 ? 0 : 1);
|
|
487
|
+
} catch (error) {
|
|
488
|
+
console.error(`\x1b[31mError:\x1b[0m ${error.message}`);
|
|
489
|
+
process.exit(1);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
module.exports = {
|
|
494
|
+
ColorValidator,
|
|
495
|
+
validateColors
|
|
496
|
+
};
|
package/bin/cli.js
CHANGED
|
@@ -13,10 +13,54 @@
|
|
|
13
13
|
const fs = require('fs');
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const os = require('os');
|
|
16
|
+
const https = require('https');
|
|
16
17
|
|
|
17
|
-
const VERSION = '
|
|
18
|
+
const VERSION = require('../package.json').version;
|
|
18
19
|
const SKILL_NAME = 'real-prototypes-skill';
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Check for newer version on npm and notify user
|
|
23
|
+
*/
|
|
24
|
+
function checkForUpdates() {
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
const req = https.get(
|
|
27
|
+
`https://registry.npmjs.org/${SKILL_NAME}/latest`,
|
|
28
|
+
{ timeout: 3000 },
|
|
29
|
+
(res) => {
|
|
30
|
+
let data = '';
|
|
31
|
+
res.on('data', chunk => data += chunk);
|
|
32
|
+
res.on('end', () => {
|
|
33
|
+
try {
|
|
34
|
+
const latest = JSON.parse(data).version;
|
|
35
|
+
if (latest && latest !== VERSION) {
|
|
36
|
+
const current = VERSION.split('.').map(Number);
|
|
37
|
+
const remote = latest.split('.').map(Number);
|
|
38
|
+
|
|
39
|
+
// Check if remote is newer
|
|
40
|
+
const isNewer = remote[0] > current[0] ||
|
|
41
|
+
(remote[0] === current[0] && remote[1] > current[1]) ||
|
|
42
|
+
(remote[0] === current[0] && remote[1] === current[1] && remote[2] > current[2]);
|
|
43
|
+
|
|
44
|
+
if (isNewer) {
|
|
45
|
+
console.log(`
|
|
46
|
+
\x1b[33m╔═══════════════════════════════════════════════════════════╗
|
|
47
|
+
║ UPDATE AVAILABLE: ${VERSION} → ${latest.padEnd(43)}║
|
|
48
|
+
║ ║
|
|
49
|
+
║ Run: npx ${SKILL_NAME}@latest --force ║
|
|
50
|
+
╚═══════════════════════════════════════════════════════════╝\x1b[0m
|
|
51
|
+
`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch (e) { /* ignore parse errors */ }
|
|
55
|
+
resolve();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
req.on('error', () => resolve());
|
|
60
|
+
req.on('timeout', () => { req.destroy(); resolve(); });
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
20
64
|
function log(message, type = 'info') {
|
|
21
65
|
const styles = {
|
|
22
66
|
info: '\x1b[36m→\x1b[0m',
|
|
@@ -302,18 +346,25 @@ function parseArgs(args) {
|
|
|
302
346
|
}
|
|
303
347
|
|
|
304
348
|
// Main
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
349
|
+
async function main() {
|
|
350
|
+
const args = process.argv.slice(2);
|
|
351
|
+
const options = parseArgs(args);
|
|
352
|
+
|
|
353
|
+
// Check for updates (non-blocking, 3s timeout)
|
|
354
|
+
await checkForUpdates();
|
|
355
|
+
|
|
356
|
+
switch (options.command) {
|
|
357
|
+
case 'install':
|
|
358
|
+
install(options);
|
|
359
|
+
break;
|
|
360
|
+
case 'uninstall':
|
|
361
|
+
uninstall(options);
|
|
362
|
+
break;
|
|
363
|
+
case 'help':
|
|
364
|
+
default:
|
|
365
|
+
showHelp();
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
319
368
|
}
|
|
369
|
+
|
|
370
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "real-prototypes-skill",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Capture any web platform and generate pixel-perfect prototypes that match its design. A Claude Code skill for rapid feature prototyping.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -48,6 +48,9 @@
|
|
|
48
48
|
"pngjs": "^7.0.0"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
+
"@babel/generator": "^7.29.0",
|
|
52
|
+
"@babel/parser": "^7.29.0",
|
|
53
|
+
"@babel/traverse": "^7.29.0",
|
|
51
54
|
"class-variance-authority": "^0.7.1",
|
|
52
55
|
"clsx": "^2.1.1",
|
|
53
56
|
"jsdom": "^27.4.0",
|