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
|
@@ -18,7 +18,17 @@ const { execSync } = require('child_process');
|
|
|
18
18
|
|
|
19
19
|
const SKILL_DIR = __dirname;
|
|
20
20
|
const PROJECTS_DIR = path.resolve(SKILL_DIR, '../../../projects');
|
|
21
|
-
const VERSION = '1.
|
|
21
|
+
const VERSION = '1.4.0';
|
|
22
|
+
|
|
23
|
+
// Import new modules
|
|
24
|
+
const { detectPrototype, formatResult } = require('./scripts/detect-prototype');
|
|
25
|
+
const { ColorValidator, validateColors } = require('./validation/color-validator');
|
|
26
|
+
const { HTMLToReactConverter, convertHTMLToReact, writeComponents } = require('./scripts/html-to-react');
|
|
27
|
+
const { CSSExtractor, extractCSS } = require('./scripts/extract-css');
|
|
28
|
+
const { VisualDiffComparator } = require('./scripts/visual-diff');
|
|
29
|
+
const { ComponentExtractor, extractComponents } = require('./scripts/extract-components');
|
|
30
|
+
const { PlanGenerator, generatePlan } = require('./scripts/generate-plan');
|
|
31
|
+
const { ProjectStructure } = require('./scripts/project-structure');
|
|
22
32
|
|
|
23
33
|
function log(message, type = 'info') {
|
|
24
34
|
const styles = {
|
|
@@ -51,17 +61,27 @@ function showHelp() {
|
|
|
51
61
|
real-prototypes-skill <command> [options]
|
|
52
62
|
|
|
53
63
|
\x1b[1mCOMMANDS\x1b[0m
|
|
54
|
-
new
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
new Create a new project
|
|
65
|
+
detect Detect existing prototype in project
|
|
66
|
+
capture Capture a web platform
|
|
67
|
+
validate Validate capture or prototype
|
|
68
|
+
validate-colors Validate colors against design tokens
|
|
69
|
+
convert Convert captured HTML to React components
|
|
70
|
+
extract-css Extract and analyze CSS from captured HTML
|
|
71
|
+
extract-lib Extract reusable component library from HTML
|
|
72
|
+
visual-diff Compare screenshots for visual accuracy
|
|
73
|
+
plan Generate implementation plan
|
|
74
|
+
generate Generate prototype from capture
|
|
75
|
+
pipeline Run full capture → validate → generate pipeline
|
|
76
|
+
init Initialize a new capture configuration
|
|
77
|
+
list List all projects
|
|
61
78
|
|
|
62
79
|
\x1b[1mPROJECT OPTIONS\x1b[0m
|
|
63
80
|
--project Project name (required for capture/validate/generate/pipeline)
|
|
64
81
|
|
|
82
|
+
\x1b[1mNEW PROJECT OPTIONS\x1b[0m
|
|
83
|
+
--force-create Required flag to create new project (blocks by default)
|
|
84
|
+
|
|
65
85
|
\x1b[1mCAPTURE OPTIONS\x1b[0m
|
|
66
86
|
--url Platform URL (required)
|
|
67
87
|
--email Login email
|
|
@@ -118,7 +138,8 @@ function parseArgs(args) {
|
|
|
118
138
|
phase: 'all',
|
|
119
139
|
refs: null,
|
|
120
140
|
proto: null,
|
|
121
|
-
features: []
|
|
141
|
+
features: [],
|
|
142
|
+
forceCreate: false
|
|
122
143
|
};
|
|
123
144
|
|
|
124
145
|
for (let i = 1; i < args.length; i++) {
|
|
@@ -156,6 +177,9 @@ function parseArgs(args) {
|
|
|
156
177
|
case '--feature':
|
|
157
178
|
options.features.push(args[++i]);
|
|
158
179
|
break;
|
|
180
|
+
case '--force-create':
|
|
181
|
+
options.forceCreate = true;
|
|
182
|
+
break;
|
|
159
183
|
case '--help':
|
|
160
184
|
case '-h':
|
|
161
185
|
showHelp();
|
|
@@ -197,6 +221,24 @@ function runNew(options) {
|
|
|
197
221
|
process.exit(1);
|
|
198
222
|
}
|
|
199
223
|
|
|
224
|
+
// Block project creation by default - require explicit flag
|
|
225
|
+
if (!options.forceCreate) {
|
|
226
|
+
console.log('');
|
|
227
|
+
log('═══════════════════════════════════════════', 'warning');
|
|
228
|
+
log('PROJECT CREATION BLOCKED', 'warning');
|
|
229
|
+
log('═══════════════════════════════════════════', 'warning');
|
|
230
|
+
console.log('');
|
|
231
|
+
log('This skill is for capturing EXISTING platforms, not creating new designs.', 'info');
|
|
232
|
+
console.log('');
|
|
233
|
+
log('If you have an existing platform to capture:', 'info');
|
|
234
|
+
log(` 1. Create project: node cli.js new --project ${options.project} --force-create`, 'info');
|
|
235
|
+
log(` 2. Capture it: node cli.js capture --project ${options.project} --url <URL>`, 'info');
|
|
236
|
+
console.log('');
|
|
237
|
+
log('Use --force-create only if you understand this creates an EMPTY project', 'warning');
|
|
238
|
+
log('that must be populated by capturing an existing platform.', 'warning');
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
|
|
200
242
|
const projectDir = getProjectDir(options.project);
|
|
201
243
|
const refsDir = path.join(projectDir, 'references');
|
|
202
244
|
const protoDir = path.join(projectDir, 'prototype');
|
|
@@ -384,8 +426,74 @@ async function runValidate(options) {
|
|
|
384
426
|
}
|
|
385
427
|
}
|
|
386
428
|
|
|
429
|
+
/**
|
|
430
|
+
* Pre-flight check - MANDATORY before any generation
|
|
431
|
+
* Validates that required captures exist before proceeding
|
|
432
|
+
*/
|
|
433
|
+
function runPreflight(options) {
|
|
434
|
+
const errors = [];
|
|
435
|
+
|
|
436
|
+
// Required files
|
|
437
|
+
const required = [
|
|
438
|
+
{ path: path.join(options.refs, 'design-tokens.json'), name: 'design-tokens.json' },
|
|
439
|
+
{ path: path.join(options.refs, 'manifest.json'), name: 'manifest.json' }
|
|
440
|
+
];
|
|
441
|
+
|
|
442
|
+
for (const { path: filePath, name } of required) {
|
|
443
|
+
if (!fs.existsSync(filePath)) {
|
|
444
|
+
errors.push(`${name} missing - captures required before generation`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Screenshots directory
|
|
449
|
+
const screenshotsDir = path.join(options.refs, 'screenshots');
|
|
450
|
+
if (!fs.existsSync(screenshotsDir)) {
|
|
451
|
+
errors.push('screenshots/ directory missing - run capture first');
|
|
452
|
+
} else {
|
|
453
|
+
const screenshots = fs.readdirSync(screenshotsDir).filter(f => f.endsWith('.png'));
|
|
454
|
+
if (screenshots.length === 0) {
|
|
455
|
+
errors.push('No screenshots found - run capture first');
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (errors.length > 0) {
|
|
460
|
+
console.log('');
|
|
461
|
+
log('═══════════════════════════════════════════', 'error');
|
|
462
|
+
log('PRE-FLIGHT CHECK FAILED - CANNOT PROCEED', 'error');
|
|
463
|
+
log('═══════════════════════════════════════════', 'error');
|
|
464
|
+
console.log('');
|
|
465
|
+
errors.forEach(e => log(e, 'error'));
|
|
466
|
+
console.log('');
|
|
467
|
+
log('You MUST capture the existing platform first:', 'warning');
|
|
468
|
+
log(` node cli.js capture --project ${options.project} --url <PLATFORM_URL>`, 'info');
|
|
469
|
+
console.log('');
|
|
470
|
+
log('This skill is for adding features to EXISTING platforms.', 'info');
|
|
471
|
+
log('It does NOT create new designs from scratch.', 'info');
|
|
472
|
+
process.exit(1);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
log('Pre-flight check passed', 'success');
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
|
|
387
479
|
async function runGenerate(options) {
|
|
388
480
|
requireProject(options, 'generate');
|
|
481
|
+
|
|
482
|
+
// MANDATORY: Pre-flight check before ANY generation
|
|
483
|
+
runPreflight(options);
|
|
484
|
+
|
|
485
|
+
// Auto-detect existing prototype
|
|
486
|
+
const protoInfo = detectPrototype(options.proto);
|
|
487
|
+
if (protoInfo.exists) {
|
|
488
|
+
console.log('');
|
|
489
|
+
log('═══════════════════════════════════════════', 'warning');
|
|
490
|
+
log('EXTEND MODE ACTIVE - Existing prototype detected', 'warning');
|
|
491
|
+
log('═══════════════════════════════════════════', 'warning');
|
|
492
|
+
log(`Framework: ${protoInfo.framework}`, 'info');
|
|
493
|
+
log('All changes MUST modify existing files only', 'warning');
|
|
494
|
+
console.log('');
|
|
495
|
+
}
|
|
496
|
+
|
|
389
497
|
log(`Generating prototype for project: ${options.project}`, 'title');
|
|
390
498
|
|
|
391
499
|
// This would integrate with your prototype generation logic
|
|
@@ -408,24 +516,47 @@ async function runGenerate(options) {
|
|
|
408
516
|
const tokens = JSON.parse(fs.readFileSync(tokensPath, 'utf-8'));
|
|
409
517
|
|
|
410
518
|
console.log(`
|
|
411
|
-
\x1b[
|
|
519
|
+
\x1b[1m════════════════════════════════════════════════════════════\x1b[0m
|
|
520
|
+
\x1b[1m PROJECT PATHS \x1b[0m
|
|
521
|
+
\x1b[1m════════════════════════════════════════════════════════════\x1b[0m
|
|
522
|
+
|
|
523
|
+
\x1b[1mPrototype Output Directory:\x1b[0m
|
|
524
|
+
${options.proto}
|
|
525
|
+
|
|
526
|
+
\x1b[1mReference Files:\x1b[0m
|
|
527
|
+
Screenshots: ${path.join(options.refs, 'screenshots')}
|
|
528
|
+
HTML: ${path.join(options.refs, 'html')}
|
|
529
|
+
Tokens: ${tokensPath}
|
|
530
|
+
Manifest: ${manifestPath}
|
|
531
|
+
|
|
532
|
+
\x1b[1m════════════════════════════════════════════════════════════\x1b[0m
|
|
533
|
+
\x1b[1m CAPTURE SUMMARY \x1b[0m
|
|
534
|
+
\x1b[1m════════════════════════════════════════════════════════════\x1b[0m
|
|
535
|
+
|
|
412
536
|
Platform: ${manifest.platform.name}
|
|
413
537
|
Pages: ${manifest.pages.length}
|
|
414
538
|
Colors: ${tokens.totalColorsFound}
|
|
415
539
|
Primary Color: ${tokens.colors?.primary || 'Not identified'}
|
|
416
540
|
|
|
417
|
-
\x1b[
|
|
418
|
-
1. Use ONLY colors from design-tokens.json
|
|
419
|
-
2. Match layout from screenshots exactly
|
|
420
|
-
3. Use inline styles for colors (Tailwind custom colors may not work)
|
|
421
|
-
4. Validate with: real-prototypes-skill validate --phase post-gen
|
|
422
|
-
|
|
423
|
-
\x1b[1mRequired Colors:\x1b[0m
|
|
541
|
+
\x1b[1mRequired Colors (from design-tokens.json):\x1b[0m
|
|
424
542
|
Primary: ${tokens.colors?.primary || 'N/A'}
|
|
425
543
|
Text: ${tokens.colors?.text?.primary || 'N/A'}
|
|
426
544
|
Background: ${tokens.colors?.background?.white || 'N/A'}
|
|
427
545
|
Border: ${tokens.colors?.border?.default || 'N/A'}
|
|
428
546
|
|
|
547
|
+
\x1b[1m════════════════════════════════════════════════════════════\x1b[0m
|
|
548
|
+
\x1b[1m INSTRUCTIONS \x1b[0m
|
|
549
|
+
\x1b[1m════════════════════════════════════════════════════════════\x1b[0m
|
|
550
|
+
|
|
551
|
+
\x1b[33mALL prototype files MUST be created in:\x1b[0m
|
|
552
|
+
${options.proto}
|
|
553
|
+
|
|
554
|
+
\x1b[1mGeneration Rules:\x1b[0m
|
|
555
|
+
1. Use ONLY colors from design-tokens.json
|
|
556
|
+
2. Match layout from screenshots exactly
|
|
557
|
+
3. Use inline styles for colors (Tailwind custom colors may not work)
|
|
558
|
+
4. Validate with: node cli.js validate-colors --project ${options.project}
|
|
559
|
+
|
|
429
560
|
\x1b[1mFeatures to add:\x1b[0m
|
|
430
561
|
${options.features.length > 0 ? options.features.map(f => ` - ${f}`).join('\n') : ' (none specified)'}
|
|
431
562
|
`);
|
|
@@ -492,6 +623,360 @@ async function runPipeline(options) {
|
|
|
492
623
|
`);
|
|
493
624
|
}
|
|
494
625
|
|
|
626
|
+
function runValidateColors(options) {
|
|
627
|
+
requireProject(options, 'validate-colors');
|
|
628
|
+
showBanner();
|
|
629
|
+
log(`Validating colors for project: ${options.project}`, 'title');
|
|
630
|
+
|
|
631
|
+
const tokensPath = path.join(options.refs, 'design-tokens.json');
|
|
632
|
+
|
|
633
|
+
if (!fs.existsSync(tokensPath)) {
|
|
634
|
+
log('design-tokens.json not found - run capture first', 'error');
|
|
635
|
+
process.exit(1);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (!fs.existsSync(options.proto)) {
|
|
639
|
+
log('Prototype directory not found', 'error');
|
|
640
|
+
process.exit(1);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
try {
|
|
644
|
+
const validator = validateColors(options.proto, tokensPath);
|
|
645
|
+
console.log('');
|
|
646
|
+
console.log(validator.formatViolations());
|
|
647
|
+
|
|
648
|
+
const summary = validator.getSummary();
|
|
649
|
+
if (summary.passed) {
|
|
650
|
+
log('All colors validated successfully!', 'success');
|
|
651
|
+
} else {
|
|
652
|
+
log(`Found ${summary.total} color violation(s)`, 'error');
|
|
653
|
+
process.exit(1);
|
|
654
|
+
}
|
|
655
|
+
} catch (error) {
|
|
656
|
+
log(`Validation failed: ${error.message}`, 'error');
|
|
657
|
+
process.exit(1);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function runConvert(options, args) {
|
|
662
|
+
requireProject(options, 'convert');
|
|
663
|
+
showBanner();
|
|
664
|
+
|
|
665
|
+
// Get page name from args
|
|
666
|
+
let pageName = null;
|
|
667
|
+
for (let i = 0; i < args.length; i++) {
|
|
668
|
+
if (args[i] === '--page') {
|
|
669
|
+
pageName = args[++i];
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (!pageName) {
|
|
674
|
+
log('--page is required for convert command', 'error');
|
|
675
|
+
log('Example: real-prototypes-skill convert --project my-app --page homepage', 'info');
|
|
676
|
+
process.exit(1);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
log(`Converting HTML to React for: ${pageName}`, 'title');
|
|
680
|
+
|
|
681
|
+
const htmlPath = path.join(options.refs, 'html', `${pageName}.html`);
|
|
682
|
+
const outputDir = path.join(options.proto, 'src', 'components', 'extracted');
|
|
683
|
+
|
|
684
|
+
if (!fs.existsSync(htmlPath)) {
|
|
685
|
+
log(`HTML file not found: ${htmlPath}`, 'error');
|
|
686
|
+
process.exit(1);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
try {
|
|
690
|
+
const result = convertHTMLToReact(htmlPath);
|
|
691
|
+
|
|
692
|
+
console.log(`\n\x1b[1mDetected Components (${result.boundaries.length}):\x1b[0m`);
|
|
693
|
+
for (const boundary of result.boundaries.slice(0, 10)) {
|
|
694
|
+
console.log(` ${boundary.suggestedName} (${boundary.type})`);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
console.log(`\n\x1b[1mExtracted Components (${result.components.length}):\x1b[0m`);
|
|
698
|
+
for (const comp of result.components) {
|
|
699
|
+
console.log(` ${comp.name}`);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Write components
|
|
703
|
+
const written = writeComponents(result.components, outputDir);
|
|
704
|
+
console.log(`\n\x1b[32m✓ Wrote ${written.length} components to: ${outputDir}\x1b[0m`);
|
|
705
|
+
|
|
706
|
+
} catch (error) {
|
|
707
|
+
log(`Conversion failed: ${error.message}`, 'error');
|
|
708
|
+
process.exit(1);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function runExtractCSS(options, args) {
|
|
713
|
+
requireProject(options, 'extract-css');
|
|
714
|
+
showBanner();
|
|
715
|
+
|
|
716
|
+
let pageName = null;
|
|
717
|
+
for (let i = 0; i < args.length; i++) {
|
|
718
|
+
if (args[i] === '--page') {
|
|
719
|
+
pageName = args[++i];
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (!pageName) {
|
|
724
|
+
log('--page is required for extract-css command', 'error');
|
|
725
|
+
log('Example: real-prototypes-skill extract-css --project my-app --page homepage', 'info');
|
|
726
|
+
process.exit(1);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
log(`Extracting CSS from: ${pageName}`, 'title');
|
|
730
|
+
|
|
731
|
+
const htmlPath = path.join(options.refs, 'html', `${pageName}.html`);
|
|
732
|
+
|
|
733
|
+
if (!fs.existsSync(htmlPath)) {
|
|
734
|
+
log(`HTML file not found: ${htmlPath}`, 'error');
|
|
735
|
+
process.exit(1);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
try {
|
|
739
|
+
const extractor = new CSSExtractor();
|
|
740
|
+
extractor.loadFromFile(htmlPath);
|
|
741
|
+
|
|
742
|
+
console.log('');
|
|
743
|
+
console.log(extractor.formatResults());
|
|
744
|
+
|
|
745
|
+
} catch (error) {
|
|
746
|
+
log(`Extraction failed: ${error.message}`, 'error');
|
|
747
|
+
process.exit(1);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function runVisualDiff(options, args) {
|
|
752
|
+
requireProject(options, 'visual-diff');
|
|
753
|
+
showBanner();
|
|
754
|
+
|
|
755
|
+
let pageName = null;
|
|
756
|
+
let listMode = false;
|
|
757
|
+
for (let i = 0; i < args.length; i++) {
|
|
758
|
+
if (args[i] === '--page') {
|
|
759
|
+
pageName = args[++i];
|
|
760
|
+
}
|
|
761
|
+
if (args[i] === '--list') {
|
|
762
|
+
listMode = true;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const screenshotsDir = path.join(options.refs, 'screenshots');
|
|
767
|
+
|
|
768
|
+
if (listMode) {
|
|
769
|
+
log('Available reference screenshots:', 'title');
|
|
770
|
+
if (fs.existsSync(screenshotsDir)) {
|
|
771
|
+
const files = fs.readdirSync(screenshotsDir).filter(f => f.endsWith('.png'));
|
|
772
|
+
for (const file of files) {
|
|
773
|
+
console.log(` ${file.replace('.png', '')}`);
|
|
774
|
+
}
|
|
775
|
+
} else {
|
|
776
|
+
log('No screenshots found', 'warning');
|
|
777
|
+
}
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (!pageName) {
|
|
782
|
+
log('--page is required (or use --list to see available)', 'error');
|
|
783
|
+
log('Example: real-prototypes-skill visual-diff --project my-app --page homepage', 'info');
|
|
784
|
+
process.exit(1);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
log(`Visual diff for: ${pageName}`, 'title');
|
|
788
|
+
|
|
789
|
+
// Find reference screenshot
|
|
790
|
+
const refScreenshots = fs.existsSync(screenshotsDir)
|
|
791
|
+
? fs.readdirSync(screenshotsDir).filter(f => f.toLowerCase().includes(pageName.toLowerCase()) && f.endsWith('.png'))
|
|
792
|
+
: [];
|
|
793
|
+
|
|
794
|
+
if (refScreenshots.length === 0) {
|
|
795
|
+
log(`No reference screenshot found for: ${pageName}`, 'error');
|
|
796
|
+
log('Use --list to see available screenshots', 'info');
|
|
797
|
+
process.exit(1);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const refPath = path.join(screenshotsDir, refScreenshots[0]);
|
|
801
|
+
console.log(` Reference: ${refScreenshots[0]}`);
|
|
802
|
+
|
|
803
|
+
// Look for generated screenshot
|
|
804
|
+
const genScreenshotsDir = path.join(options.proto, 'screenshots');
|
|
805
|
+
const genScreenshots = fs.existsSync(genScreenshotsDir)
|
|
806
|
+
? fs.readdirSync(genScreenshotsDir).filter(f => f.toLowerCase().includes(pageName.toLowerCase()) && f.endsWith('.png'))
|
|
807
|
+
: [];
|
|
808
|
+
|
|
809
|
+
if (genScreenshots.length === 0) {
|
|
810
|
+
log('No generated screenshot found to compare', 'warning');
|
|
811
|
+
console.log(`
|
|
812
|
+
\x1b[1mTo generate a screenshot:\x1b[0m
|
|
813
|
+
1. Start your prototype: npm run dev
|
|
814
|
+
2. Take a screenshot of the page
|
|
815
|
+
3. Save it to: ${genScreenshotsDir}/${pageName}.png
|
|
816
|
+
`);
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const genPath = path.join(genScreenshotsDir, genScreenshots[0]);
|
|
821
|
+
console.log(` Generated: ${genScreenshots[0]}`);
|
|
822
|
+
|
|
823
|
+
// Run comparison
|
|
824
|
+
(async () => {
|
|
825
|
+
try {
|
|
826
|
+
const comparator = new VisualDiffComparator({ minSimilarity: 95 });
|
|
827
|
+
const diffPath = path.join(options.refs, 'diff', `${pageName}-diff.png`);
|
|
828
|
+
|
|
829
|
+
const result = await comparator.compare(refPath, genPath, diffPath);
|
|
830
|
+
|
|
831
|
+
console.log('');
|
|
832
|
+
console.log(comparator.formatResults());
|
|
833
|
+
|
|
834
|
+
} catch (error) {
|
|
835
|
+
log(`Visual diff failed: ${error.message}`, 'error');
|
|
836
|
+
process.exit(1);
|
|
837
|
+
}
|
|
838
|
+
})();
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function runDetect(options) {
|
|
842
|
+
requireProject(options, 'detect');
|
|
843
|
+
showBanner();
|
|
844
|
+
log(`Detecting existing prototype for project: ${options.project}`, 'title');
|
|
845
|
+
|
|
846
|
+
const projectDir = getProjectDir(options.project);
|
|
847
|
+
const protoDir = path.join(projectDir, 'prototype');
|
|
848
|
+
const manifestPath = path.join(projectDir, 'references', 'manifest.json');
|
|
849
|
+
|
|
850
|
+
// First check if prototype directory exists
|
|
851
|
+
if (!fs.existsSync(protoDir)) {
|
|
852
|
+
log('No prototype directory found', 'warning');
|
|
853
|
+
log(`Expected at: ${protoDir}`, 'info');
|
|
854
|
+
console.log(`
|
|
855
|
+
\x1b[1mTo create a prototype:\x1b[0m
|
|
856
|
+
1. Run capture first: real-prototypes-skill capture --project ${options.project} --url <URL>
|
|
857
|
+
2. Then generate: real-prototypes-skill generate --project ${options.project}
|
|
858
|
+
`);
|
|
859
|
+
return { exists: false };
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// Run detection
|
|
863
|
+
const result = detectPrototype(protoDir);
|
|
864
|
+
|
|
865
|
+
// Try to map pages if manifest exists
|
|
866
|
+
if (fs.existsSync(manifestPath)) {
|
|
867
|
+
try {
|
|
868
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
869
|
+
const { mapPages } = require('./scripts/detect-prototype');
|
|
870
|
+
result.mappedPages = mapPages(protoDir, manifest);
|
|
871
|
+
} catch (e) {
|
|
872
|
+
log(`Could not load manifest: ${e.message}`, 'warning');
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
console.log('');
|
|
877
|
+
console.log(formatResult(result));
|
|
878
|
+
console.log('');
|
|
879
|
+
|
|
880
|
+
if (result.exists) {
|
|
881
|
+
log('Existing prototype detected - use EXTEND mode', 'success');
|
|
882
|
+
console.log(`
|
|
883
|
+
\x1b[1mRecommendation:\x1b[0m
|
|
884
|
+
When generating new features, modify existing files instead of creating new ones.
|
|
885
|
+
|
|
886
|
+
\x1b[1mExisting prototype details:\x1b[0m
|
|
887
|
+
Framework: ${result.framework || 'Unknown'}
|
|
888
|
+
Styling: ${result.styling.join(', ')}
|
|
889
|
+
Pages: ${result.pages.length}
|
|
890
|
+
Components: ${result.components.length}
|
|
891
|
+
`);
|
|
892
|
+
} else {
|
|
893
|
+
log('No existing prototype found - safe to CREATE new', 'info');
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
return result;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function runExtractLib(options) {
|
|
900
|
+
requireProject(options, 'extract-lib');
|
|
901
|
+
showBanner();
|
|
902
|
+
log(`Extracting component library for project: ${options.project}`, 'title');
|
|
903
|
+
|
|
904
|
+
const htmlDir = path.join(options.refs, 'html');
|
|
905
|
+
const outputDir = path.join(options.proto, 'src', 'components', 'extracted');
|
|
906
|
+
|
|
907
|
+
if (!fs.existsSync(htmlDir)) {
|
|
908
|
+
log('HTML directory not found - run capture first', 'error');
|
|
909
|
+
process.exit(1);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
try {
|
|
913
|
+
const extractor = new ComponentExtractor();
|
|
914
|
+
extractor.analyzeDirectory(htmlDir);
|
|
915
|
+
|
|
916
|
+
console.log('');
|
|
917
|
+
console.log(extractor.formatSummary());
|
|
918
|
+
|
|
919
|
+
// Write components
|
|
920
|
+
const written = extractor.writeComponents(outputDir);
|
|
921
|
+
console.log(`\n\x1b[32m✓ Wrote ${written.length} files to: ${outputDir}\x1b[0m`);
|
|
922
|
+
for (const file of written) {
|
|
923
|
+
console.log(` ${path.basename(file)}`);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
} catch (error) {
|
|
927
|
+
log(`Extraction failed: ${error.message}`, 'error');
|
|
928
|
+
process.exit(1);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function runPlan(options, args) {
|
|
933
|
+
requireProject(options, 'plan');
|
|
934
|
+
showBanner();
|
|
935
|
+
|
|
936
|
+
let feature = '';
|
|
937
|
+
let targetPage = null;
|
|
938
|
+
let outputPath = null;
|
|
939
|
+
|
|
940
|
+
for (let i = 0; i < args.length; i++) {
|
|
941
|
+
if (args[i] === '--feature') {
|
|
942
|
+
feature = args[++i];
|
|
943
|
+
} else if (args[i] === '--target') {
|
|
944
|
+
targetPage = args[++i];
|
|
945
|
+
} else if (args[i] === '--output' || args[i] === '-o') {
|
|
946
|
+
outputPath = args[++i];
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
log(`Generating implementation plan for project: ${options.project}`, 'title');
|
|
951
|
+
|
|
952
|
+
const projectDir = getProjectDir(options.project);
|
|
953
|
+
|
|
954
|
+
try {
|
|
955
|
+
const generator = new PlanGenerator(projectDir, {
|
|
956
|
+
featureDescription: feature,
|
|
957
|
+
targetPage
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
generator.generate();
|
|
961
|
+
console.log('');
|
|
962
|
+
console.log(generator.formatPlan());
|
|
963
|
+
|
|
964
|
+
if (outputPath) {
|
|
965
|
+
generator.writePlan(outputPath);
|
|
966
|
+
console.log(`\n\x1b[32m✓ Plan written to: ${outputPath}\x1b[0m`);
|
|
967
|
+
} else {
|
|
968
|
+
// Write to project directory by default
|
|
969
|
+
const defaultPath = path.join(projectDir, 'plan.json');
|
|
970
|
+
generator.writePlan(defaultPath);
|
|
971
|
+
console.log(`\n\x1b[32m✓ Plan written to: ${defaultPath}\x1b[0m`);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
} catch (error) {
|
|
975
|
+
log(`Plan generation failed: ${error.message}`, 'error');
|
|
976
|
+
process.exit(1);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
495
980
|
function runInit(options) {
|
|
496
981
|
showBanner();
|
|
497
982
|
log('Initializing capture configuration...', 'title');
|
|
@@ -565,6 +1050,9 @@ switch (options.command) {
|
|
|
565
1050
|
case 'new':
|
|
566
1051
|
runNew(options);
|
|
567
1052
|
break;
|
|
1053
|
+
case 'detect':
|
|
1054
|
+
runDetect(options);
|
|
1055
|
+
break;
|
|
568
1056
|
case 'list':
|
|
569
1057
|
runList();
|
|
570
1058
|
break;
|
|
@@ -574,6 +1062,24 @@ switch (options.command) {
|
|
|
574
1062
|
case 'validate':
|
|
575
1063
|
runValidate(options);
|
|
576
1064
|
break;
|
|
1065
|
+
case 'validate-colors':
|
|
1066
|
+
runValidateColors(options);
|
|
1067
|
+
break;
|
|
1068
|
+
case 'convert':
|
|
1069
|
+
runConvert(options, args);
|
|
1070
|
+
break;
|
|
1071
|
+
case 'extract-css':
|
|
1072
|
+
runExtractCSS(options, args);
|
|
1073
|
+
break;
|
|
1074
|
+
case 'extract-lib':
|
|
1075
|
+
runExtractLib(options);
|
|
1076
|
+
break;
|
|
1077
|
+
case 'visual-diff':
|
|
1078
|
+
runVisualDiff(options, args);
|
|
1079
|
+
break;
|
|
1080
|
+
case 'plan':
|
|
1081
|
+
runPlan(options, args);
|
|
1082
|
+
break;
|
|
577
1083
|
case 'generate':
|
|
578
1084
|
runGenerate(options);
|
|
579
1085
|
break;
|