real-prototypes-skill 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.claude/skills/agent-browser-skill/SKILL.md +252 -0
  2. package/.claude/skills/real-prototypes-skill/.gitignore +188 -0
  3. package/.claude/skills/real-prototypes-skill/ACCESSIBILITY.md +668 -0
  4. package/.claude/skills/real-prototypes-skill/INSTALL.md +259 -0
  5. package/.claude/skills/real-prototypes-skill/LICENSE +21 -0
  6. package/.claude/skills/real-prototypes-skill/PUBLISH.md +310 -0
  7. package/.claude/skills/real-prototypes-skill/QUICKSTART.md +240 -0
  8. package/.claude/skills/real-prototypes-skill/README.md +442 -0
  9. package/.claude/skills/real-prototypes-skill/SKILL.md +329 -0
  10. package/.claude/skills/real-prototypes-skill/capture/capture-engine.js +1153 -0
  11. package/.claude/skills/real-prototypes-skill/capture/config.schema.json +170 -0
  12. package/.claude/skills/real-prototypes-skill/cli.js +596 -0
  13. package/.claude/skills/real-prototypes-skill/docs/TROUBLESHOOTING.md +278 -0
  14. package/.claude/skills/real-prototypes-skill/docs/schemas/capture-config.md +167 -0
  15. package/.claude/skills/real-prototypes-skill/docs/schemas/design-tokens.md +183 -0
  16. package/.claude/skills/real-prototypes-skill/docs/schemas/manifest.md +169 -0
  17. package/.claude/skills/real-prototypes-skill/examples/CLAUDE.md.example +73 -0
  18. package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/CLAUDE.md +136 -0
  19. package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/FEATURES.md +222 -0
  20. package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/README.md +82 -0
  21. package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/references/design-tokens.json +87 -0
  22. package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/references/screenshots/homepage-viewport.png +0 -0
  23. package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/references/screenshots/prototype-chatbot-final.png +0 -0
  24. package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/references/screenshots/prototype-fullpage-v2.png +0 -0
  25. package/.claude/skills/real-prototypes-skill/references/accessibility-fixes.md +298 -0
  26. package/.claude/skills/real-prototypes-skill/references/accessibility-report.json +253 -0
  27. package/.claude/skills/real-prototypes-skill/scripts/CAPTURE-ENHANCEMENTS.md +344 -0
  28. package/.claude/skills/real-prototypes-skill/scripts/IMPLEMENTATION-SUMMARY.md +517 -0
  29. package/.claude/skills/real-prototypes-skill/scripts/QUICK-START.md +229 -0
  30. package/.claude/skills/real-prototypes-skill/scripts/QUICKSTART-layout-analysis.md +148 -0
  31. package/.claude/skills/real-prototypes-skill/scripts/README-analyze-layout.md +407 -0
  32. package/.claude/skills/real-prototypes-skill/scripts/analyze-layout.js +880 -0
  33. package/.claude/skills/real-prototypes-skill/scripts/capture-platform.js +203 -0
  34. package/.claude/skills/real-prototypes-skill/scripts/comprehensive-capture.js +597 -0
  35. package/.claude/skills/real-prototypes-skill/scripts/create-manifest.js +338 -0
  36. package/.claude/skills/real-prototypes-skill/scripts/enterprise-pipeline.js +428 -0
  37. package/.claude/skills/real-prototypes-skill/scripts/extract-tokens.js +468 -0
  38. package/.claude/skills/real-prototypes-skill/scripts/full-site-capture.js +738 -0
  39. package/.claude/skills/real-prototypes-skill/scripts/generate-tailwind-config.js +296 -0
  40. package/.claude/skills/real-prototypes-skill/scripts/integrate-accessibility.sh +161 -0
  41. package/.claude/skills/real-prototypes-skill/scripts/manifest-schema.json +302 -0
  42. package/.claude/skills/real-prototypes-skill/scripts/setup-prototype.sh +167 -0
  43. package/.claude/skills/real-prototypes-skill/scripts/test-analyze-layout.js +338 -0
  44. package/.claude/skills/real-prototypes-skill/scripts/test-validation.js +307 -0
  45. package/.claude/skills/real-prototypes-skill/scripts/validate-accessibility.js +598 -0
  46. package/.claude/skills/real-prototypes-skill/scripts/validate-manifest.js +499 -0
  47. package/.claude/skills/real-prototypes-skill/scripts/validate-output.js +361 -0
  48. package/.claude/skills/real-prototypes-skill/scripts/validate-prerequisites.js +319 -0
  49. package/.claude/skills/real-prototypes-skill/scripts/verify-layout-analysis.sh +77 -0
  50. package/.claude/skills/real-prototypes-skill/templates/dashboard-widget.tsx.template +91 -0
  51. package/.claude/skills/real-prototypes-skill/templates/data-table.tsx.template +193 -0
  52. package/.claude/skills/real-prototypes-skill/templates/form-section.tsx.template +250 -0
  53. package/.claude/skills/real-prototypes-skill/templates/modal-dialog.tsx.template +239 -0
  54. package/.claude/skills/real-prototypes-skill/templates/nav-item.tsx.template +265 -0
  55. package/.claude/skills/real-prototypes-skill/validation/validation-engine.js +559 -0
  56. package/.env.example +74 -0
  57. package/LICENSE +21 -0
  58. package/README.md +444 -0
  59. package/bin/cli.js +319 -0
  60. package/package.json +59 -0
@@ -0,0 +1,597 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * COMPREHENSIVE PLATFORM CAPTURE
4
+ *
5
+ * This script captures ALL states of a platform by:
6
+ * 1. Capturing every page
7
+ * 2. Clicking every interactive element (buttons, tabs, dropdowns)
8
+ * 3. Capturing each state change
9
+ * 4. Extracting design tokens from ALL captured HTML
10
+ *
11
+ * Output Structure:
12
+ * references/
13
+ * ├── screenshots/
14
+ * │ ├── page-name.png # Base state
15
+ * │ ├── page-name--button-1.png # After clicking button 1
16
+ * │ ├── page-name--modal-open.png # Modal state
17
+ * │ └── ...
18
+ * ├── html/
19
+ * │ └── (same structure as screenshots)
20
+ * ├── design-tokens.json # Extracted colors, fonts, spacing
21
+ * ├── component-styles.json # Button, input, card styles
22
+ * ├── interactions.json # Map of all interactive elements
23
+ * └── manifest.json # Complete capture manifest
24
+ */
25
+
26
+ const { execSync } = require('child_process');
27
+ const fs = require('fs');
28
+ const path = require('path');
29
+
30
+ // Configuration
31
+ const CONFIG = {
32
+ outputDir: 'references',
33
+ screenshotDir: 'references/screenshots',
34
+ htmlDir: 'references/html',
35
+ waitAfterClick: 1000,
36
+ waitAfterLoad: 2000,
37
+ maxInteractionsPerPage: 50,
38
+ viewport: { width: 1920, height: 1080 }
39
+ };
40
+
41
+ // Agent-browser wrapper
42
+ function browser(cmd) {
43
+ try {
44
+ const result = execSync(`agent-browser ${cmd}`, {
45
+ encoding: 'utf8',
46
+ maxBuffer: 50 * 1024 * 1024 // 50MB buffer for large outputs
47
+ });
48
+ return result.trim();
49
+ } catch (error) {
50
+ console.error(`Browser command failed: ${cmd}`);
51
+ console.error(error.message);
52
+ return null;
53
+ }
54
+ }
55
+
56
+ // Ensure directories exist
57
+ function ensureDirectories() {
58
+ [CONFIG.outputDir, CONFIG.screenshotDir, CONFIG.htmlDir].forEach(dir => {
59
+ if (!fs.existsSync(dir)) {
60
+ fs.mkdirSync(dir, { recursive: true });
61
+ }
62
+ });
63
+ }
64
+
65
+ // Extract all interactive elements from current page
66
+ function getInteractiveElements() {
67
+ const snapshot = browser('snapshot -i --json');
68
+ if (!snapshot) return [];
69
+
70
+ try {
71
+ const data = JSON.parse(snapshot);
72
+ const elements = [];
73
+
74
+ // Recursively find all interactive elements
75
+ function traverse(node, path = []) {
76
+ if (!node) return;
77
+
78
+ const isInteractive =
79
+ node.role === 'button' ||
80
+ node.role === 'link' ||
81
+ node.role === 'tab' ||
82
+ node.role === 'menuitem' ||
83
+ node.role === 'checkbox' ||
84
+ node.role === 'radio' ||
85
+ node.role === 'combobox' ||
86
+ node.role === 'switch' ||
87
+ (node.tag === 'button') ||
88
+ (node.tag === 'a' && node.attributes?.href) ||
89
+ (node.attributes?.onclick) ||
90
+ (node.attributes?.['data-action']);
91
+
92
+ if (isInteractive && node.ref) {
93
+ elements.push({
94
+ ref: node.ref,
95
+ role: node.role,
96
+ name: node.name || node.text || `${node.role}-${elements.length}`,
97
+ tag: node.tag,
98
+ path: [...path, node.role || node.tag]
99
+ });
100
+ }
101
+
102
+ // Traverse children
103
+ if (node.children) {
104
+ node.children.forEach((child, i) => {
105
+ traverse(child, [...path, `${node.role || node.tag}[${i}]`]);
106
+ });
107
+ }
108
+ }
109
+
110
+ traverse(data);
111
+ return elements;
112
+ } catch (e) {
113
+ console.error('Failed to parse snapshot:', e.message);
114
+ return [];
115
+ }
116
+ }
117
+
118
+ // Capture current state (screenshot + HTML)
119
+ function captureState(name) {
120
+ const screenshotPath = path.join(CONFIG.screenshotDir, `${name}.png`);
121
+ const htmlPath = path.join(CONFIG.htmlDir, `${name}.html`);
122
+
123
+ // Capture screenshot
124
+ browser(`screenshot --full "${screenshotPath}"`);
125
+
126
+ // Capture HTML
127
+ const html = browser('eval "document.documentElement.outerHTML"');
128
+ if (html) {
129
+ fs.writeFileSync(htmlPath, html);
130
+ }
131
+
132
+ console.log(` ✓ Captured: ${name}`);
133
+
134
+ return {
135
+ screenshot: screenshotPath,
136
+ html: htmlPath,
137
+ timestamp: new Date().toISOString()
138
+ };
139
+ }
140
+
141
+ // Extract colors from HTML content
142
+ function extractColorsFromHTML(htmlContent) {
143
+ const colors = new Map();
144
+
145
+ // Match hex colors
146
+ const hexMatches = htmlContent.match(/#[0-9a-fA-F]{3,8}/g) || [];
147
+ hexMatches.forEach(color => {
148
+ const normalized = color.toLowerCase();
149
+ colors.set(normalized, (colors.get(normalized) || 0) + 1);
150
+ });
151
+
152
+ // Match rgb/rgba colors
153
+ const rgbMatches = htmlContent.match(/rgba?\([^)]+\)/g) || [];
154
+ rgbMatches.forEach(color => {
155
+ colors.set(color, (colors.get(color) || 0) + 1);
156
+ });
157
+
158
+ return colors;
159
+ }
160
+
161
+ // Extract font information from HTML
162
+ function extractFontsFromHTML(htmlContent) {
163
+ const fonts = new Set();
164
+
165
+ // Match font-family declarations
166
+ const fontMatches = htmlContent.match(/font-family:\s*([^;}"]+)/gi) || [];
167
+ fontMatches.forEach(match => {
168
+ const font = match.replace(/font-family:\s*/i, '').trim();
169
+ fonts.add(font);
170
+ });
171
+
172
+ return Array.from(fonts);
173
+ }
174
+
175
+ // Extract component styles
176
+ function extractComponentStyles(htmlContent) {
177
+ const styles = {
178
+ buttons: [],
179
+ inputs: [],
180
+ cards: [],
181
+ tables: []
182
+ };
183
+
184
+ // This would be more sophisticated in production
185
+ // For now, extract inline styles from specific element types
186
+
187
+ // Button styles
188
+ const buttonStyleMatches = htmlContent.match(/<button[^>]*style="([^"]+)"/gi) || [];
189
+ buttonStyleMatches.forEach(match => {
190
+ const style = match.match(/style="([^"]+)"/);
191
+ if (style) styles.buttons.push(style[1]);
192
+ });
193
+
194
+ // Input styles
195
+ const inputStyleMatches = htmlContent.match(/<input[^>]*style="([^"]+)"/gi) || [];
196
+ inputStyleMatches.forEach(match => {
197
+ const style = match.match(/style="([^"]+)"/);
198
+ if (style) styles.inputs.push(style[1]);
199
+ });
200
+
201
+ return styles;
202
+ }
203
+
204
+ // Generate design tokens from all captured HTML
205
+ function generateDesignTokens(htmlFiles) {
206
+ console.log('\n📊 Extracting design tokens from captured HTML...');
207
+
208
+ const allColors = new Map();
209
+ const allFonts = new Set();
210
+ const allComponentStyles = {
211
+ buttons: [],
212
+ inputs: [],
213
+ cards: [],
214
+ tables: []
215
+ };
216
+
217
+ htmlFiles.forEach(htmlPath => {
218
+ if (fs.existsSync(htmlPath)) {
219
+ const content = fs.readFileSync(htmlPath, 'utf8');
220
+
221
+ // Extract colors
222
+ const colors = extractColorsFromHTML(content);
223
+ colors.forEach((count, color) => {
224
+ allColors.set(color, (allColors.get(color) || 0) + count);
225
+ });
226
+
227
+ // Extract fonts
228
+ const fonts = extractFontsFromHTML(content);
229
+ fonts.forEach(font => allFonts.add(font));
230
+
231
+ // Extract component styles
232
+ const compStyles = extractComponentStyles(content);
233
+ Object.keys(compStyles).forEach(key => {
234
+ allComponentStyles[key].push(...compStyles[key]);
235
+ });
236
+ }
237
+ });
238
+
239
+ // Sort colors by frequency
240
+ const sortedColors = Array.from(allColors.entries())
241
+ .sort((a, b) => b[1] - a[1]);
242
+
243
+ // Categorize colors
244
+ const categorizedColors = categorizeColors(sortedColors);
245
+
246
+ const tokens = {
247
+ extractedAt: new Date().toISOString(),
248
+ totalColorsFound: sortedColors.length,
249
+ colors: categorizedColors,
250
+ fonts: {
251
+ families: Array.from(allFonts),
252
+ primary: Array.from(allFonts)[0] || 'Inter, system-ui'
253
+ },
254
+ rawColors: sortedColors.slice(0, 100) // Top 100 colors with counts
255
+ };
256
+
257
+ // Write design tokens
258
+ const tokensPath = path.join(CONFIG.outputDir, 'design-tokens.json');
259
+ fs.writeFileSync(tokensPath, JSON.stringify(tokens, null, 2));
260
+ console.log(` ✓ Written: ${tokensPath}`);
261
+
262
+ // Write component styles
263
+ const stylesPath = path.join(CONFIG.outputDir, 'component-styles.json');
264
+ fs.writeFileSync(stylesPath, JSON.stringify(allComponentStyles, null, 2));
265
+ console.log(` ✓ Written: ${stylesPath}`);
266
+
267
+ return tokens;
268
+ }
269
+
270
+ // Categorize colors by their likely usage
271
+ function categorizeColors(sortedColors) {
272
+ const colors = {
273
+ // Will be filled based on analysis
274
+ primary: null,
275
+ secondary: null,
276
+ background: {},
277
+ text: {},
278
+ border: {},
279
+ status: {},
280
+ sidebar: {},
281
+ all: {}
282
+ };
283
+
284
+ sortedColors.forEach(([color, count]) => {
285
+ // Store all colors
286
+ colors.all[color] = count;
287
+
288
+ // Categorize based on color characteristics
289
+ const hex = color.startsWith('#') ? color : null;
290
+ if (!hex) return;
291
+
292
+ const rgb = hexToRgb(hex);
293
+ if (!rgb) return;
294
+
295
+ const brightness = (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000;
296
+
297
+ // Very dark colors (likely sidebar, dark text)
298
+ if (brightness < 50) {
299
+ if (!colors.sidebar.dark) colors.sidebar.dark = color;
300
+ if (!colors.text.primary && count > 100) colors.text.primary = color;
301
+ }
302
+ // Dark colors (likely text, headings)
303
+ else if (brightness < 100) {
304
+ if (!colors.text.heading) colors.text.heading = color;
305
+ }
306
+ // Very light colors (likely backgrounds)
307
+ else if (brightness > 240) {
308
+ if (!colors.background.white) colors.background.white = color;
309
+ }
310
+ // Light colors (likely light backgrounds, borders)
311
+ else if (brightness > 200) {
312
+ if (!colors.background.light) colors.background.light = color;
313
+ if (!colors.border.light) colors.border.light = color;
314
+ }
315
+ // Medium colors (likely borders, muted text)
316
+ else if (brightness > 150) {
317
+ if (!colors.border.default) colors.border.default = color;
318
+ if (!colors.text.muted) colors.text.muted = color;
319
+ }
320
+
321
+ // Check for saturated colors (likely primary/accent)
322
+ const saturation = getColorSaturation(rgb);
323
+ if (saturation > 50 && count > 50) {
324
+ // Blue-ish (likely primary)
325
+ if (rgb.b > rgb.r && rgb.b > rgb.g) {
326
+ if (!colors.primary) colors.primary = color;
327
+ }
328
+ // Green-ish (likely success)
329
+ else if (rgb.g > rgb.r && rgb.g > rgb.b) {
330
+ if (!colors.status.success) colors.status.success = color;
331
+ }
332
+ // Red-ish (likely error)
333
+ else if (rgb.r > rgb.g && rgb.r > rgb.b) {
334
+ if (!colors.status.error) colors.status.error = color;
335
+ }
336
+ }
337
+ });
338
+
339
+ return colors;
340
+ }
341
+
342
+ // Helper: hex to RGB
343
+ function hexToRgb(hex) {
344
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
345
+ return result ? {
346
+ r: parseInt(result[1], 16),
347
+ g: parseInt(result[2], 16),
348
+ b: parseInt(result[3], 16)
349
+ } : null;
350
+ }
351
+
352
+ // Helper: get color saturation
353
+ function getColorSaturation(rgb) {
354
+ const max = Math.max(rgb.r, rgb.g, rgb.b);
355
+ const min = Math.min(rgb.r, rgb.g, rgb.b);
356
+ const l = (max + min) / 2;
357
+
358
+ if (max === min) return 0;
359
+
360
+ const d = max - min;
361
+ return l > 0.5 ? d / (510 - max - min) * 100 : d / (max + min) * 100;
362
+ }
363
+
364
+ // Capture all states of a single page
365
+ async function capturePageStates(pageUrl, pageName) {
366
+ console.log(`\n📸 Capturing page: ${pageName}`);
367
+
368
+ const captures = [];
369
+ const interactions = [];
370
+
371
+ // Navigate to page
372
+ browser(`open "${pageUrl}"`);
373
+ browser(`wait ${CONFIG.waitAfterLoad}`);
374
+
375
+ // Capture base state
376
+ captures.push(captureState(pageName));
377
+
378
+ // Get all interactive elements
379
+ const elements = getInteractiveElements();
380
+ console.log(` Found ${elements.length} interactive elements`);
381
+
382
+ // Track clicked elements to avoid duplicates
383
+ const clickedElements = new Set();
384
+ let interactionCount = 0;
385
+
386
+ for (const element of elements) {
387
+ if (interactionCount >= CONFIG.maxInteractionsPerPage) {
388
+ console.log(` ⚠ Reached max interactions limit (${CONFIG.maxInteractionsPerPage})`);
389
+ break;
390
+ }
391
+
392
+ // Skip if already clicked similar element
393
+ const elementKey = `${element.role}-${element.name}`;
394
+ if (clickedElements.has(elementKey)) continue;
395
+ clickedElements.add(elementKey);
396
+
397
+ // Skip navigation links that would leave the page
398
+ if (element.role === 'link' && element.tag === 'a') {
399
+ continue; // Will be captured as separate pages
400
+ }
401
+
402
+ try {
403
+ // Click the element
404
+ console.log(` → Clicking: ${element.name} (${element.ref})`);
405
+ browser(`click ${element.ref}`);
406
+ browser(`wait ${CONFIG.waitAfterClick}`);
407
+
408
+ // Capture the new state
409
+ const stateName = `${pageName}--${sanitizeFileName(element.name)}`;
410
+ const capture = captureState(stateName);
411
+
412
+ captures.push(capture);
413
+ interactions.push({
414
+ element: element,
415
+ stateName: stateName,
416
+ capture: capture
417
+ });
418
+
419
+ interactionCount++;
420
+
421
+ // Try to close any modals/dialogs that opened
422
+ closeAnyModals();
423
+
424
+ // Return to base state if page changed significantly
425
+ const currentUrl = browser('get url');
426
+ if (currentUrl && !currentUrl.includes(pageUrl)) {
427
+ browser(`open "${pageUrl}"`);
428
+ browser(`wait ${CONFIG.waitAfterLoad}`);
429
+ }
430
+
431
+ } catch (error) {
432
+ console.log(` ⚠ Failed to interact with: ${element.name}`);
433
+ }
434
+ }
435
+
436
+ return { captures, interactions };
437
+ }
438
+
439
+ // Try to close any open modals
440
+ function closeAnyModals() {
441
+ // Look for common close button patterns
442
+ const closePatterns = [
443
+ 'button[aria-label*="close"]',
444
+ 'button[aria-label*="Close"]',
445
+ '.modal-close',
446
+ '.dialog-close',
447
+ '[data-dismiss="modal"]'
448
+ ];
449
+
450
+ // Try pressing Escape
451
+ browser('press Escape');
452
+ browser('wait 300');
453
+ }
454
+
455
+ // Sanitize filename
456
+ function sanitizeFileName(name) {
457
+ return name
458
+ .toLowerCase()
459
+ .replace(/[^a-z0-9]+/g, '-')
460
+ .replace(/^-|-$/g, '')
461
+ .substring(0, 50);
462
+ }
463
+
464
+ // Main capture function
465
+ async function runCapture(config) {
466
+ console.log('🚀 Starting Comprehensive Platform Capture\n');
467
+ console.log('='.repeat(50));
468
+
469
+ ensureDirectories();
470
+
471
+ const manifest = {
472
+ platform: {
473
+ name: config.platformName || 'Unknown Platform',
474
+ baseUrl: config.baseUrl,
475
+ capturedAt: new Date().toISOString()
476
+ },
477
+ pages: [],
478
+ totalScreenshots: 0,
479
+ totalInteractions: 0
480
+ };
481
+
482
+ const allHtmlFiles = [];
483
+
484
+ // Capture each page
485
+ for (const page of config.pages) {
486
+ const pageUrl = page.startsWith('http') ? page : `${config.baseUrl}${page}`;
487
+ const pageName = sanitizeFileName(page.replace(config.baseUrl, '').replace(/^\//, '') || 'home');
488
+
489
+ const { captures, interactions } = await capturePageStates(pageUrl, pageName);
490
+
491
+ manifest.pages.push({
492
+ name: pageName,
493
+ url: pageUrl,
494
+ captures: captures,
495
+ interactions: interactions.length
496
+ });
497
+
498
+ manifest.totalScreenshots += captures.length;
499
+ manifest.totalInteractions += interactions.length;
500
+
501
+ // Collect HTML files for token extraction
502
+ captures.forEach(c => {
503
+ if (c.html) allHtmlFiles.push(c.html);
504
+ });
505
+ }
506
+
507
+ // Generate design tokens from all captured HTML
508
+ const tokens = generateDesignTokens(allHtmlFiles);
509
+
510
+ // Validate tokens
511
+ validateExtractedTokens(tokens);
512
+
513
+ // Write manifest
514
+ const manifestPath = path.join(CONFIG.outputDir, 'manifest.json');
515
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
516
+ console.log(`\n✓ Written: ${manifestPath}`);
517
+
518
+ // Write interactions map
519
+ const interactionsPath = path.join(CONFIG.outputDir, 'interactions.json');
520
+ const allInteractions = manifest.pages.flatMap(p => p.interactions || []);
521
+ fs.writeFileSync(interactionsPath, JSON.stringify(allInteractions, null, 2));
522
+ console.log(`✓ Written: ${interactionsPath}`);
523
+
524
+ // Summary
525
+ console.log('\n' + '='.repeat(50));
526
+ console.log('📊 CAPTURE SUMMARY');
527
+ console.log('='.repeat(50));
528
+ console.log(`Pages captured: ${manifest.pages.length}`);
529
+ console.log(`Total screenshots: ${manifest.totalScreenshots}`);
530
+ console.log(`Total interactions: ${manifest.totalInteractions}`);
531
+ console.log(`Colors extracted: ${tokens.totalColorsFound}`);
532
+ console.log(`Font families: ${tokens.fonts.families.length}`);
533
+ console.log('='.repeat(50));
534
+
535
+ return manifest;
536
+ }
537
+
538
+ // Validate extracted tokens meet minimum requirements
539
+ function validateExtractedTokens(tokens) {
540
+ const errors = [];
541
+
542
+ if (!tokens.colors.primary) {
543
+ errors.push('Could not identify primary color');
544
+ }
545
+ if (!tokens.colors.text.primary) {
546
+ errors.push('Could not identify primary text color');
547
+ }
548
+ if (tokens.totalColorsFound < 10) {
549
+ errors.push(`Only ${tokens.totalColorsFound} colors found (minimum 10 required)`);
550
+ }
551
+ if (tokens.fonts.families.length === 0) {
552
+ errors.push('No font families extracted');
553
+ }
554
+
555
+ if (errors.length > 0) {
556
+ console.log('\n⚠️ TOKEN EXTRACTION WARNINGS:');
557
+ errors.forEach(e => console.log(` - ${e}`));
558
+ } else {
559
+ console.log('\n✅ Token extraction passed validation');
560
+ }
561
+
562
+ return errors.length === 0;
563
+ }
564
+
565
+ // CLI interface
566
+ if (require.main === module) {
567
+ const args = process.argv.slice(2);
568
+
569
+ if (args.length === 0) {
570
+ console.log(`
571
+ Usage: node comprehensive-capture.js <base-url> [pages...]
572
+
573
+ Example:
574
+ node comprehensive-capture.js https://app.example.com /dashboard /settings /profile
575
+
576
+ Or with config file:
577
+ node comprehensive-capture.js --config capture-config.json
578
+ `);
579
+ process.exit(1);
580
+ }
581
+
582
+ if (args[0] === '--config') {
583
+ const config = JSON.parse(fs.readFileSync(args[1], 'utf8'));
584
+ runCapture(config);
585
+ } else {
586
+ const baseUrl = args[0];
587
+ const pages = args.slice(1).length > 0 ? args.slice(1) : ['/'];
588
+
589
+ runCapture({
590
+ baseUrl,
591
+ pages,
592
+ platformName: new URL(baseUrl).hostname
593
+ });
594
+ }
595
+ }
596
+
597
+ module.exports = { runCapture, generateDesignTokens, extractColorsFromHTML };