real-prototypes-skill 0.1.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 +375 -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,1153 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Platform Capture Engine
5
+ *
6
+ * A comprehensive, enterprise-grade web platform capture system.
7
+ * Automatically discovers and captures all pages, states, and interactions.
8
+ *
9
+ * Usage:
10
+ * node capture-engine.js --config ./capture-config.json
11
+ * node capture-engine.js --url https://app.example.com --email user@test.com --password secret
12
+ */
13
+
14
+ const { execSync } = require('child_process');
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ class CaptureEngine {
19
+ constructor(config) {
20
+ this.config = this.normalizeConfig(config);
21
+ this.capturedPages = new Map();
22
+ this.discoveredUrls = new Set();
23
+ this.visitedUrls = new Set();
24
+ this.interactions = [];
25
+ this.errors = [];
26
+ this.warnings = [];
27
+ this.stats = {
28
+ pagesDiscovered: 0,
29
+ pagesCaptured: 0,
30
+ screenshotsTaken: 0,
31
+ htmlCaptured: 0,
32
+ interactionsPerformed: 0,
33
+ errorsEncountered: 0
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Normalize config to handle both old and new field naming conventions
39
+ * Supports backwards compatibility while standardizing internally
40
+ */
41
+ normalizeConfig(config) {
42
+ const normalized = JSON.parse(JSON.stringify(config)); // Deep clone
43
+
44
+ // Normalize capture section
45
+ if (normalized.capture) {
46
+ // Support both 'manualPages' and 'include' (prefer manualPages, fallback to include)
47
+ if (normalized.capture.manualPages && !normalized.capture.include) {
48
+ normalized.capture.include = normalized.capture.manualPages;
49
+ }
50
+ // Also support the reverse for users who use 'include'
51
+ if (normalized.capture.include && !normalized.capture.manualPages) {
52
+ normalized.capture.manualPages = normalized.capture.include;
53
+ }
54
+ }
55
+
56
+ // Normalize auth credentials section
57
+ if (normalized.auth?.credentials) {
58
+ const creds = normalized.auth.credentials;
59
+
60
+ // Support both 'emailSelector' and 'emailField'
61
+ if (creds.emailSelector && !creds.emailField) {
62
+ // emailSelector is CSS, keep it as selector
63
+ }
64
+ if (creds.emailField && !creds.emailSelector) {
65
+ // emailField might be a label name or a selector
66
+ // If it looks like a CSS selector, treat it as such
67
+ if (creds.emailField.startsWith('#') || creds.emailField.startsWith('.') || creds.emailField.includes('[')) {
68
+ creds.emailSelector = creds.emailField;
69
+ }
70
+ }
71
+
72
+ // Support both 'passwordSelector' and 'passwordField'
73
+ if (creds.passwordSelector && !creds.passwordField) {
74
+ // passwordSelector is CSS, keep it
75
+ }
76
+ if (creds.passwordField && !creds.passwordSelector) {
77
+ if (creds.passwordField.startsWith('#') || creds.passwordField.startsWith('.') || creds.passwordField.includes('[')) {
78
+ creds.passwordSelector = creds.passwordField;
79
+ }
80
+ }
81
+
82
+ // Support both 'submitSelector' and 'submitButton'
83
+ if (creds.submitSelector && !creds.submitButton) {
84
+ // submitSelector is CSS
85
+ }
86
+ if (creds.submitButton && !creds.submitSelector) {
87
+ if (creds.submitButton.startsWith('#') || creds.submitButton.startsWith('.') || creds.submitButton.includes('[')) {
88
+ creds.submitSelector = creds.submitButton;
89
+ }
90
+ }
91
+ }
92
+
93
+ // Validate and warn about unknown fields
94
+ this.validateConfigFields(normalized);
95
+
96
+ return normalized;
97
+ }
98
+
99
+ /**
100
+ * Validate config and warn about unknown/deprecated fields
101
+ */
102
+ validateConfigFields(config) {
103
+ const knownFields = {
104
+ platform: ['name', 'baseUrl'],
105
+ auth: ['type', 'loginUrl', 'credentials', 'successUrl', 'waitAfterLogin'],
106
+ 'auth.credentials': ['email', 'password', 'emailField', 'emailSelector', 'passwordField', 'passwordSelector', 'submitButton', 'submitSelector'],
107
+ capture: ['mode', 'maxPages', 'maxDepth', 'viewports', 'interactions', 'include', 'manualPages', 'exclude', 'waitAfterLoad', 'waitAfterInteraction'],
108
+ output: ['directory', 'screenshots', 'html', 'designTokens'],
109
+ validation: ['minPages', 'minColors', 'requireDetailPages', 'requireAllTabs']
110
+ };
111
+
112
+ const warnings = [];
113
+
114
+ // Check top-level unknown fields
115
+ const topLevelKnown = ['platform', 'auth', 'capture', 'output', 'validation'];
116
+ Object.keys(config).forEach(key => {
117
+ if (!topLevelKnown.includes(key)) {
118
+ warnings.push(`Unknown config field: '${key}' - this will be ignored`);
119
+ }
120
+ });
121
+
122
+ // Check nested fields
123
+ Object.entries(knownFields).forEach(([section, fields]) => {
124
+ const sectionParts = section.split('.');
125
+ let obj = config;
126
+ for (const part of sectionParts) {
127
+ obj = obj?.[part];
128
+ }
129
+ if (obj && typeof obj === 'object') {
130
+ Object.keys(obj).forEach(key => {
131
+ if (!fields.includes(key) && typeof obj[key] !== 'object') {
132
+ warnings.push(`Unknown config field in ${section}: '${key}' - this will be ignored`);
133
+ }
134
+ });
135
+ }
136
+ });
137
+
138
+ if (warnings.length > 0) {
139
+ console.log('\n⚠ Configuration warnings:');
140
+ warnings.forEach(w => console.log(` - ${w}`));
141
+ console.log('');
142
+ }
143
+ }
144
+
145
+ log(message, type = 'info') {
146
+ const colors = {
147
+ info: '\x1b[36m', // cyan
148
+ success: '\x1b[32m', // green
149
+ warning: '\x1b[33m', // yellow
150
+ error: '\x1b[31m', // red
151
+ step: '\x1b[90m', // gray
152
+ progress: '\x1b[35m', // magenta
153
+ reset: '\x1b[0m'
154
+ };
155
+ const prefix = {
156
+ info: '→',
157
+ success: '✓',
158
+ warning: '⚠',
159
+ error: '✗',
160
+ step: '•',
161
+ progress: '◐'
162
+ }[type] || '→';
163
+ console.log(`${colors[type] || ''}${prefix} ${message}${colors.reset}`);
164
+ }
165
+
166
+ /**
167
+ * Display progress for multi-item operations
168
+ */
169
+ logProgress(current, total, message) {
170
+ const percent = Math.round((current / total) * 100);
171
+ const bar = this.createProgressBar(percent);
172
+ process.stdout.write(`\r\x1b[35m${bar}\x1b[0m ${current}/${total} ${message} `);
173
+ if (current === total) {
174
+ console.log(''); // New line when complete
175
+ }
176
+ }
177
+
178
+ createProgressBar(percent, width = 20) {
179
+ const filled = Math.round((percent / 100) * width);
180
+ const empty = width - filled;
181
+ return `[${'█'.repeat(filled)}${'░'.repeat(empty)}] ${percent}%`;
182
+ }
183
+
184
+ /**
185
+ * Display spinner for ongoing operations
186
+ */
187
+ showSpinner(message) {
188
+ const frames = ['◐', '◓', '◑', '◒'];
189
+ let i = 0;
190
+ this._spinnerInterval = setInterval(() => {
191
+ process.stdout.write(`\r\x1b[35m${frames[i]} ${message}...\x1b[0m `);
192
+ i = (i + 1) % frames.length;
193
+ }, 100);
194
+ }
195
+
196
+ hideSpinner(message, success = true) {
197
+ if (this._spinnerInterval) {
198
+ clearInterval(this._spinnerInterval);
199
+ this._spinnerInterval = null;
200
+ }
201
+ const icon = success ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m';
202
+ console.log(`\r${icon} ${message} `);
203
+ }
204
+
205
+ exec(command) {
206
+ try {
207
+ const result = execSync(command, { encoding: 'utf-8', timeout: 60000 });
208
+ return { success: true, output: result.trim() };
209
+ } catch (error) {
210
+ return { success: false, error: error.message };
211
+ }
212
+ }
213
+
214
+ browser(action) {
215
+ return this.exec(`agent-browser ${action}`);
216
+ }
217
+
218
+ async run() {
219
+ this.log('Starting Platform Capture Engine', 'info');
220
+ this.log(`Platform: ${this.config.platform.name}`, 'step');
221
+ this.log(`Base URL: ${this.config.platform.baseUrl}`, 'step');
222
+
223
+ try {
224
+ // Phase 1: Setup
225
+ await this.setup();
226
+
227
+ // Phase 2: Authentication
228
+ if (this.config.auth.type !== 'none') {
229
+ await this.authenticate();
230
+ }
231
+
232
+ // Phase 3: Discovery
233
+ await this.discoverPages();
234
+
235
+ // Phase 4: Capture
236
+ await this.captureAllPages();
237
+
238
+ // Phase 5: Extract Design Tokens
239
+ await this.extractDesignTokens();
240
+
241
+ // Phase 6: Validation
242
+ const validationResult = await this.validate();
243
+
244
+ // Phase 7: Generate Manifest
245
+ await this.generateManifest();
246
+
247
+ // Phase 8: Report
248
+ this.generateReport(validationResult);
249
+
250
+ return { success: validationResult.passed, stats: this.stats };
251
+
252
+ } catch (error) {
253
+ this.log(`Fatal error: ${error.message}`, 'error');
254
+ this.errors.push({ phase: 'execution', error: error.message });
255
+ return { success: false, error: error.message };
256
+ } finally {
257
+ this.browser('close');
258
+ }
259
+ }
260
+
261
+ async setup() {
262
+ this.log('Setting up capture environment...', 'info');
263
+
264
+ const outputDir = this.config.output?.directory || './references';
265
+ const dirs = [
266
+ outputDir,
267
+ path.join(outputDir, 'screenshots'),
268
+ path.join(outputDir, 'html'),
269
+ path.join(outputDir, 'viewports')
270
+ ];
271
+
272
+ dirs.forEach(dir => {
273
+ if (!fs.existsSync(dir)) {
274
+ fs.mkdirSync(dir, { recursive: true });
275
+ this.log(`Created directory: ${dir}`, 'step');
276
+ }
277
+ });
278
+
279
+ // Set viewport
280
+ const defaultViewport = this.config.capture?.viewports?.[0] || { width: 1920, height: 1080 };
281
+ this.browser(`set viewport ${defaultViewport.width} ${defaultViewport.height}`);
282
+
283
+ this.log('Setup complete', 'success');
284
+ }
285
+
286
+ async authenticate() {
287
+ const auth = this.config.auth;
288
+ const loginUrl = `${this.config.platform.baseUrl}${auth.loginUrl || '/login'}`;
289
+
290
+ this.showSpinner('Navigating to login page');
291
+ this.browser(`open ${loginUrl}`);
292
+ this.browser(`wait 2000`);
293
+ this.hideSpinner('Loaded login page', true);
294
+
295
+ if (auth.type === 'form') {
296
+ // Get credentials from environment or config
297
+ const email = process.env.PLATFORM_EMAIL || auth.credentials?.email;
298
+ const password = process.env.PLATFORM_PASSWORD || auth.credentials?.password;
299
+
300
+ if (!email || !password) {
301
+ throw new Error('Missing credentials. Set PLATFORM_EMAIL and PLATFORM_PASSWORD environment variables.');
302
+ }
303
+
304
+ // Get snapshot of interactive elements to find form fields
305
+ const snapshot = this.browser('snapshot -i');
306
+ if (!snapshot.success) {
307
+ throw new Error('Could not get page snapshot for login form');
308
+ }
309
+
310
+ // Parse snapshot to find form elements
311
+ const formElements = this.parseLoginForm(snapshot.output, auth.credentials);
312
+
313
+ if (!formElements.emailRef && !formElements.emailSelector) {
314
+ this.log('Login form analysis:', 'warning');
315
+ this.log(snapshot.output.substring(0, 1000), 'info');
316
+ throw new Error(
317
+ 'Could not find email/username field on login page.\n\n' +
318
+ 'Troubleshooting:\n' +
319
+ '1. Check the login URL is correct: ' + loginUrl + '\n' +
320
+ '2. Specify selectors in config:\n' +
321
+ ' auth.credentials.emailSelector = "#email"\n' +
322
+ ' auth.credentials.passwordSelector = "#password"\n' +
323
+ ' auth.credentials.submitSelector = "button[type=submit]"\n' +
324
+ '3. Run with --headed flag to see the browser'
325
+ );
326
+ }
327
+
328
+ // Fill email field (prefer ref, fallback to selector)
329
+ if (formElements.emailRef) {
330
+ this.browser(`fill ${formElements.emailRef} "${email}"`);
331
+ } else if (formElements.emailSelector) {
332
+ this.browser(`fill "${formElements.emailSelector}" "${email}"`);
333
+ }
334
+
335
+ // Fill password field
336
+ if (formElements.passwordRef) {
337
+ this.browser(`fill ${formElements.passwordRef} "${password}"`);
338
+ } else if (formElements.passwordSelector) {
339
+ this.browser(`fill "${formElements.passwordSelector}" "${password}"`);
340
+ }
341
+
342
+ // Click submit button
343
+ this.showSpinner('Logging in');
344
+ if (formElements.submitRef) {
345
+ this.browser(`click ${formElements.submitRef}`);
346
+ } else if (formElements.submitSelector) {
347
+ this.browser(`click "${formElements.submitSelector}"`);
348
+ } else {
349
+ // Fallback: try pressing Enter
350
+ this.browser('press Enter');
351
+ }
352
+
353
+ this.browser(`wait 3000`);
354
+ this.hideSpinner('Login submitted', true);
355
+
356
+ // Verify login success
357
+ const currentUrl = this.browser('get url').output;
358
+ if (currentUrl && currentUrl.includes('login')) {
359
+ // Get page state for debugging
360
+ const pageSnapshot = this.browser('snapshot -i').output || '';
361
+ const errorText = this.extractErrorMessages(pageSnapshot);
362
+
363
+ throw new Error(
364
+ 'Authentication failed - still on login page.\n\n' +
365
+ 'Possible causes:\n' +
366
+ '1. Incorrect credentials\n' +
367
+ '2. CAPTCHA or 2FA required\n' +
368
+ '3. Account locked or needs verification\n' +
369
+ (errorText ? '\nPage errors found: ' + errorText + '\n' : '') +
370
+ '\nCurrent URL: ' + currentUrl + '\n' +
371
+ '\nTry running with --headed to see the browser window.'
372
+ );
373
+ }
374
+ }
375
+
376
+ this.log('Authentication successful', 'success');
377
+ }
378
+
379
+ /**
380
+ * Parse login form snapshot to find email, password, and submit elements
381
+ * Uses refs from snapshot for unambiguous element targeting
382
+ */
383
+ parseLoginForm(snapshot, credentials = {}) {
384
+ const elements = {
385
+ emailRef: null,
386
+ emailSelector: credentials?.emailSelector || null,
387
+ passwordRef: null,
388
+ passwordSelector: credentials?.passwordSelector || null,
389
+ submitRef: null,
390
+ submitSelector: credentials?.submitSelector || null
391
+ };
392
+
393
+ // If explicit selectors provided, use those
394
+ if (elements.emailSelector && elements.passwordSelector) {
395
+ return elements;
396
+ }
397
+
398
+ const lines = snapshot.split('\n');
399
+
400
+ // Keywords to identify email fields
401
+ const emailKeywords = ['email', 'username', 'user', 'login', 'account', 'e-mail'];
402
+ const passwordKeywords = ['password', 'pass', 'pwd', 'secret'];
403
+ const submitKeywords = ['sign in', 'log in', 'login', 'submit', 'continue', 'next'];
404
+
405
+ for (const line of lines) {
406
+ const lowerLine = line.toLowerCase();
407
+ const refMatch = line.match(/\[ref=(\w+)\]/);
408
+ const ref = refMatch ? `@${refMatch[1]}` : null;
409
+
410
+ // Look for email/username field
411
+ if (!elements.emailRef && ref) {
412
+ if (lowerLine.includes('textbox') || lowerLine.includes('input')) {
413
+ // Check for email type or email-related text
414
+ if (lowerLine.includes('type="email"') ||
415
+ emailKeywords.some(kw => lowerLine.includes(kw))) {
416
+ elements.emailRef = ref;
417
+ continue;
418
+ }
419
+ }
420
+ }
421
+
422
+ // Look for password field
423
+ if (!elements.passwordRef && ref) {
424
+ if (lowerLine.includes('type="password"') ||
425
+ (lowerLine.includes('textbox') && passwordKeywords.some(kw => lowerLine.includes(kw)))) {
426
+ elements.passwordRef = ref;
427
+ continue;
428
+ }
429
+ }
430
+
431
+ // Look for submit button
432
+ if (!elements.submitRef && ref) {
433
+ if (lowerLine.includes('button') && !lowerLine.includes('[disabled]')) {
434
+ if (submitKeywords.some(kw => lowerLine.includes(kw))) {
435
+ elements.submitRef = ref;
436
+ }
437
+ }
438
+ }
439
+ }
440
+
441
+ // Fallback: if we found a password field but no email, find the first textbox before it
442
+ if (elements.passwordRef && !elements.emailRef) {
443
+ let foundPassword = false;
444
+ for (let i = lines.length - 1; i >= 0; i--) {
445
+ const line = lines[i];
446
+ const lowerLine = line.toLowerCase();
447
+ const refMatch = line.match(/\[ref=(\w+)\]/);
448
+ const ref = refMatch ? `@${refMatch[1]}` : null;
449
+
450
+ if (ref === elements.passwordRef) {
451
+ foundPassword = true;
452
+ continue;
453
+ }
454
+
455
+ if (foundPassword && ref &&
456
+ (lowerLine.includes('textbox') || lowerLine.includes('input')) &&
457
+ !lowerLine.includes('type="password"')) {
458
+ elements.emailRef = ref;
459
+ break;
460
+ }
461
+ }
462
+ }
463
+
464
+ return elements;
465
+ }
466
+
467
+ /**
468
+ * Extract error messages from page snapshot
469
+ */
470
+ extractErrorMessages(snapshot) {
471
+ const errorPatterns = [
472
+ /error[:\s]+"([^"]+)"/gi,
473
+ /invalid[:\s]+"([^"]+)"/gi,
474
+ /failed[:\s]+"([^"]+)"/gi,
475
+ /alert[:\s]+"([^"]+)"/gi
476
+ ];
477
+
478
+ const errors = [];
479
+ for (const pattern of errorPatterns) {
480
+ const matches = snapshot.match(pattern);
481
+ if (matches) {
482
+ errors.push(...matches);
483
+ }
484
+ }
485
+
486
+ return errors.slice(0, 3).join(', ');
487
+ }
488
+
489
+ async discoverPages() {
490
+ const mode = this.config.capture?.mode || 'auto';
491
+ const maxPages = this.config.capture?.maxPages || 100;
492
+ const maxDepth = this.config.capture?.maxDepth || 5;
493
+
494
+ this.showSpinner(`Discovering pages (mode: ${mode}, max: ${maxPages})`);
495
+ console.log(''); // New line for spinner
496
+
497
+ // Start from current page after login
498
+ const startUrl = this.browser('get url').output;
499
+ this.discoveredUrls.add(startUrl);
500
+
501
+ if (mode === 'auto' || mode === 'hybrid') {
502
+ await this.autoDiscover(startUrl, 0, maxDepth, maxPages);
503
+ }
504
+
505
+ if (mode === 'manual' || mode === 'hybrid') {
506
+ const manualPages = this.config.capture?.include || [];
507
+ manualPages.forEach(pattern => {
508
+ const url = `${this.config.platform.baseUrl}${pattern}`;
509
+ this.discoveredUrls.add(url);
510
+ });
511
+ }
512
+
513
+ this.stats.pagesDiscovered = this.discoveredUrls.size;
514
+ this.hideSpinner(`Discovered ${this.discoveredUrls.size} pages`, true);
515
+ }
516
+
517
+ async autoDiscover(url, depth, maxDepth, maxPages) {
518
+ if (depth > maxDepth || this.discoveredUrls.size >= maxPages) return;
519
+ if (this.visitedUrls.has(url)) return;
520
+
521
+ this.visitedUrls.add(url);
522
+
523
+ // Navigate to page
524
+ this.browser(`open ${url}`);
525
+ this.browser(`wait ${this.config.capture?.waitAfterLoad || 2000}`);
526
+
527
+ // Get all links on the page
528
+ const snapshot = this.browser('snapshot').output;
529
+ const links = this.extractLinks(snapshot);
530
+
531
+ for (const link of links) {
532
+ if (this.shouldCapture(link) && !this.discoveredUrls.has(link)) {
533
+ this.discoveredUrls.add(link);
534
+ this.log(`Found: ${link}`, 'step');
535
+
536
+ if (this.discoveredUrls.size < maxPages) {
537
+ await this.autoDiscover(link, depth + 1, maxDepth, maxPages);
538
+ }
539
+ }
540
+ }
541
+ }
542
+
543
+ extractLinks(snapshot) {
544
+ const links = [];
545
+ const baseUrl = this.config.platform.baseUrl;
546
+
547
+ // Parse snapshot for links (simplified - real implementation would parse properly)
548
+ const linkPatterns = snapshot.match(/href="([^"]+)"/g) || [];
549
+ const buttonLinks = snapshot.match(/link "([^"]+)"/g) || [];
550
+
551
+ [...linkPatterns, ...buttonLinks].forEach(match => {
552
+ const href = match.replace(/href="|link "|"/g, '');
553
+ if (href.startsWith('/')) {
554
+ links.push(`${baseUrl}${href}`);
555
+ } else if (href.startsWith(baseUrl)) {
556
+ links.push(href);
557
+ }
558
+ });
559
+
560
+ return [...new Set(links)];
561
+ }
562
+
563
+ shouldCapture(url) {
564
+ const exclude = this.config.capture?.exclude || ['/logout', '/signout', '/delete'];
565
+ const baseUrl = this.config.platform.baseUrl;
566
+
567
+ // Must be same domain
568
+ if (!url.startsWith(baseUrl)) return false;
569
+
570
+ // Check exclusions
571
+ for (const pattern of exclude) {
572
+ if (url.includes(pattern)) return false;
573
+ }
574
+
575
+ return true;
576
+ }
577
+
578
+ async captureAllPages() {
579
+ this.log('Capturing pages...', 'info');
580
+
581
+ const outputDir = this.config.output?.directory || './references';
582
+ const viewports = this.config.capture?.viewports || [{ name: 'desktop', width: 1920, height: 1080 }];
583
+ const totalPages = this.discoveredUrls.size;
584
+ let currentPage = 0;
585
+
586
+ console.log(` Viewports: ${viewports.map(v => v.name).join(', ')}`);
587
+ console.log(` Total pages to capture: ${totalPages}\n`);
588
+
589
+ for (const url of this.discoveredUrls) {
590
+ currentPage++;
591
+ const pageName = this.urlToPageName(url);
592
+ this.logProgress(currentPage, totalPages, pageName);
593
+
594
+ try {
595
+ await this.capturePage(url, outputDir, viewports);
596
+ } catch (error) {
597
+ this.log(`Failed to capture ${url}: ${error.message}`, 'error');
598
+ this.errors.push({ url, error: error.message });
599
+ this.stats.errorsEncountered++;
600
+ }
601
+ }
602
+
603
+ console.log('');
604
+ this.log(`Captured ${this.stats.pagesCaptured} pages, ${this.stats.screenshotsTaken} screenshots`, 'success');
605
+ }
606
+
607
+ async capturePage(url, outputDir, viewports) {
608
+ this.browser(`open ${url}`);
609
+ this.browser(`wait ${this.config.capture?.waitAfterLoad || 2000}`);
610
+
611
+ const pageName = this.urlToPageName(url);
612
+ const pageData = {
613
+ name: pageName,
614
+ url: url.replace(this.config.platform.baseUrl, ''),
615
+ captures: [],
616
+ tabs: [],
617
+ interactions: []
618
+ };
619
+
620
+ // Capture for each viewport
621
+ for (const viewport of viewports) {
622
+ this.browser(`set viewport ${viewport.width} ${viewport.height}`);
623
+ this.browser(`wait 500`);
624
+
625
+ const screenshotPath = path.join(outputDir, 'screenshots', `${pageName}-${viewport.name}.png`);
626
+ this.browser(`screenshot ${screenshotPath}`);
627
+ this.stats.screenshotsTaken++;
628
+
629
+ pageData.captures.push({
630
+ viewport: viewport.name,
631
+ screenshot: `screenshots/${pageName}-${viewport.name}.png`
632
+ });
633
+ }
634
+
635
+ // Reset to desktop for HTML capture
636
+ this.browser(`set viewport ${viewports[0].width} ${viewports[0].height}`);
637
+
638
+ // Capture HTML
639
+ if (this.config.output?.html !== false) {
640
+ const htmlPath = path.join(outputDir, 'html', `${pageName}.html`);
641
+ const htmlContent = this.browser('eval "document.documentElement.outerHTML"').output;
642
+ if (htmlContent) {
643
+ fs.writeFileSync(htmlPath, htmlContent);
644
+ pageData.html = `html/${pageName}.html`;
645
+ this.stats.htmlCaptured++;
646
+ }
647
+ }
648
+
649
+ // Capture interactions (tabs, dropdowns, etc.)
650
+ if (this.config.capture?.interactions?.clickTabs !== false) {
651
+ await this.captureTabStates(pageName, outputDir, pageData);
652
+ }
653
+
654
+ if (this.config.capture?.interactions?.clickDropdowns !== false) {
655
+ await this.captureDropdownStates(pageName, outputDir, pageData);
656
+ }
657
+
658
+ if (this.config.capture?.interactions?.clickTableRows !== false) {
659
+ await this.captureDetailPages(pageName, outputDir, pageData);
660
+ }
661
+
662
+ this.capturedPages.set(url, pageData);
663
+ this.stats.pagesCaptured++;
664
+ this.log(`Captured: ${pageName}`, 'success');
665
+ }
666
+
667
+ async captureTabStates(pageName, outputDir, pageData) {
668
+ const snapshot = this.browser('snapshot -i').output;
669
+ const tabs = this.findTabs(snapshot);
670
+
671
+ for (const tab of tabs) {
672
+ try {
673
+ this.browser(`click ${tab.ref}`);
674
+ this.browser(`wait ${this.config.capture?.waitAfterInteraction || 1000}`);
675
+
676
+ const tabName = this.sanitizeName(tab.name);
677
+ const screenshotPath = path.join(outputDir, 'screenshots', `${pageName}-tab-${tabName}.png`);
678
+ this.browser(`screenshot ${screenshotPath}`);
679
+ this.stats.screenshotsTaken++;
680
+ this.stats.interactionsPerformed++;
681
+
682
+ pageData.tabs.push({
683
+ name: tab.name,
684
+ screenshot: `screenshots/${pageName}-tab-${tabName}.png`
685
+ });
686
+ } catch (error) {
687
+ this.warnings.push(`Failed to capture tab ${tab.name}: ${error.message}`);
688
+ }
689
+ }
690
+ }
691
+
692
+ async captureDropdownStates(pageName, outputDir, pageData) {
693
+ const snapshot = this.browser('snapshot -i').output;
694
+ const dropdowns = this.findDropdowns(snapshot);
695
+
696
+ for (const dropdown of dropdowns) {
697
+ try {
698
+ this.browser(`click ${dropdown.ref}`);
699
+ this.browser(`wait 500`);
700
+
701
+ const dropdownName = this.sanitizeName(dropdown.name);
702
+ const screenshotPath = path.join(outputDir, 'screenshots', `${pageName}-dropdown-${dropdownName}.png`);
703
+ this.browser(`screenshot ${screenshotPath}`);
704
+ this.stats.screenshotsTaken++;
705
+ this.stats.interactionsPerformed++;
706
+
707
+ pageData.interactions.push({
708
+ type: 'dropdown',
709
+ name: dropdown.name,
710
+ screenshot: `screenshots/${pageName}-dropdown-${dropdownName}.png`
711
+ });
712
+
713
+ // Close dropdown
714
+ this.browser('press Escape');
715
+ } catch (error) {
716
+ this.warnings.push(`Failed to capture dropdown ${dropdown.name}: ${error.message}`);
717
+ }
718
+ }
719
+ }
720
+
721
+ async captureDetailPages(pageName, outputDir, pageData) {
722
+ // Only for list pages
723
+ if (!pageName.includes('list') && !pageName.includes('accounts') && !pageName.includes('contacts')) {
724
+ return;
725
+ }
726
+
727
+ const snapshot = this.browser('snapshot -i').output;
728
+ const tableRows = this.findClickableTableRows(snapshot);
729
+
730
+ // Capture first row as example detail page
731
+ if (tableRows.length > 0) {
732
+ try {
733
+ const firstRow = tableRows[0];
734
+ this.browser(`click ${firstRow.ref}`);
735
+ this.browser(`wait 2000`);
736
+
737
+ const newUrl = this.browser('get url').output;
738
+ if (!this.discoveredUrls.has(newUrl)) {
739
+ this.discoveredUrls.add(newUrl);
740
+ this.log(`Discovered detail page: ${newUrl}`, 'step');
741
+ }
742
+
743
+ // Go back
744
+ this.browser('back');
745
+ this.browser(`wait 1000`);
746
+ } catch (error) {
747
+ this.warnings.push(`Failed to capture detail page: ${error.message}`);
748
+ }
749
+ }
750
+ }
751
+
752
+ findTabs(snapshot) {
753
+ const tabs = [];
754
+ const lines = snapshot.split('\n');
755
+
756
+ for (const line of lines) {
757
+ if (line.includes('tab ') || line.includes('button') && (
758
+ line.toLowerCase().includes('overview') ||
759
+ line.toLowerCase().includes('activity') ||
760
+ line.toLowerCase().includes('settings') ||
761
+ line.toLowerCase().includes('details')
762
+ )) {
763
+ const refMatch = line.match(/\[ref=(\w+)\]/);
764
+ const nameMatch = line.match(/"([^"]+)"/);
765
+ if (refMatch && nameMatch) {
766
+ tabs.push({ ref: `@${refMatch[1]}`, name: nameMatch[1] });
767
+ }
768
+ }
769
+ }
770
+ return tabs;
771
+ }
772
+
773
+ findDropdowns(snapshot) {
774
+ const dropdowns = [];
775
+ const lines = snapshot.split('\n');
776
+
777
+ for (const line of lines) {
778
+ if (line.includes('combobox') || (line.includes('button') && line.toLowerCase().includes('action'))) {
779
+ const refMatch = line.match(/\[ref=(\w+)\]/);
780
+ const nameMatch = line.match(/"([^"]+)"/);
781
+ if (refMatch) {
782
+ dropdowns.push({
783
+ ref: `@${refMatch[1]}`,
784
+ name: nameMatch ? nameMatch[1] : 'dropdown'
785
+ });
786
+ }
787
+ }
788
+ }
789
+ return dropdowns;
790
+ }
791
+
792
+ findClickableTableRows(snapshot) {
793
+ const rows = [];
794
+ const lines = snapshot.split('\n');
795
+
796
+ for (const line of lines) {
797
+ if (line.includes('button') && !line.includes('[disabled]')) {
798
+ const refMatch = line.match(/\[ref=(\w+)\]/);
799
+ const nameMatch = line.match(/"([^"]+)"/);
800
+ if (refMatch && nameMatch && nameMatch[1].length > 2) {
801
+ rows.push({ ref: `@${refMatch[1]}`, name: nameMatch[1] });
802
+ }
803
+ }
804
+ }
805
+ return rows.slice(0, 5); // Limit to first 5
806
+ }
807
+
808
+ urlToPageName(url) {
809
+ const path = url.replace(this.config.platform.baseUrl, '');
810
+ return this.sanitizeName(path) || 'home';
811
+ }
812
+
813
+ sanitizeName(str) {
814
+ return str
815
+ .toLowerCase()
816
+ .replace(/^\//, '')
817
+ .replace(/[^a-z0-9]+/g, '-')
818
+ .replace(/^-|-$/g, '')
819
+ .substring(0, 50);
820
+ }
821
+
822
+ async extractDesignTokens() {
823
+ this.log('Extracting design tokens...', 'info');
824
+
825
+ const outputDir = this.config.output?.directory || './references';
826
+ const htmlDir = path.join(outputDir, 'html');
827
+
828
+ if (!fs.existsSync(htmlDir)) {
829
+ this.warnings.push('No HTML files to extract tokens from');
830
+ return;
831
+ }
832
+
833
+ const colors = new Map();
834
+ const fonts = new Set();
835
+
836
+ const htmlFiles = fs.readdirSync(htmlDir).filter(f => f.endsWith('.html'));
837
+
838
+ for (const file of htmlFiles) {
839
+ const content = fs.readFileSync(path.join(htmlDir, file), 'utf-8');
840
+
841
+ // Extract colors
842
+ const colorMatches = content.match(/#[0-9a-fA-F]{3,8}\b/g) || [];
843
+ colorMatches.forEach(color => {
844
+ const normalized = color.toLowerCase();
845
+ colors.set(normalized, (colors.get(normalized) || 0) + 1);
846
+ });
847
+
848
+ // Extract RGB colors
849
+ const rgbMatches = content.match(/rgb\([^)]+\)/g) || [];
850
+ rgbMatches.forEach(rgb => {
851
+ colors.set(rgb, (colors.get(rgb) || 0) + 1);
852
+ });
853
+
854
+ // Extract fonts
855
+ const fontMatches = content.match(/font-family:\s*([^;}"]+)/g) || [];
856
+ fontMatches.forEach(font => {
857
+ fonts.add(font.replace('font-family:', '').trim());
858
+ });
859
+ }
860
+
861
+ // Sort colors by frequency
862
+ const sortedColors = [...colors.entries()]
863
+ .sort((a, b) => b[1] - a[1])
864
+ .slice(0, 50);
865
+
866
+ // Categorize colors
867
+ const designTokens = {
868
+ extractedAt: new Date().toISOString(),
869
+ totalColorsFound: colors.size,
870
+ colors: this.categorizeColors(sortedColors),
871
+ fonts: {
872
+ families: [...fonts],
873
+ primary: [...fonts][0] || 'system-ui'
874
+ },
875
+ rawColors: sortedColors
876
+ };
877
+
878
+ fs.writeFileSync(
879
+ path.join(outputDir, 'design-tokens.json'),
880
+ JSON.stringify(designTokens, null, 2)
881
+ );
882
+
883
+ this.log(`Extracted ${colors.size} colors, ${fonts.size} fonts`, 'success');
884
+ }
885
+
886
+ categorizeColors(sortedColors) {
887
+ const colors = {
888
+ primary: null,
889
+ secondary: null,
890
+ background: {},
891
+ text: {},
892
+ border: {},
893
+ status: {}
894
+ };
895
+
896
+ for (const [color, count] of sortedColors) {
897
+ const hex = color.startsWith('#') ? color : this.rgbToHex(color);
898
+ const rgb = this.hexToRgb(hex);
899
+ if (!rgb) continue;
900
+
901
+ const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
902
+
903
+ // Categorize based on color properties
904
+ if (rgb.b > rgb.r && rgb.b > rgb.g && !colors.primary) {
905
+ colors.primary = hex;
906
+ } else if (luminance > 0.9) {
907
+ if (!colors.background.white) colors.background.white = hex;
908
+ } else if (luminance > 0.8) {
909
+ if (!colors.background.light) colors.background.light = hex;
910
+ } else if (luminance < 0.2) {
911
+ if (!colors.text.primary) colors.text.primary = hex;
912
+ } else if (luminance < 0.5) {
913
+ if (!colors.text.secondary) colors.text.secondary = hex;
914
+ }
915
+
916
+ // Status colors
917
+ if (rgb.r > 200 && rgb.g < 100 && rgb.b < 100) {
918
+ if (!colors.status.error) colors.status.error = hex;
919
+ }
920
+ if (rgb.g > 150 && rgb.r < 100 && rgb.b < 100) {
921
+ if (!colors.status.success) colors.status.success = hex;
922
+ }
923
+ }
924
+
925
+ return colors;
926
+ }
927
+
928
+ hexToRgb(hex) {
929
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
930
+ return result ? {
931
+ r: parseInt(result[1], 16),
932
+ g: parseInt(result[2], 16),
933
+ b: parseInt(result[3], 16)
934
+ } : null;
935
+ }
936
+
937
+ rgbToHex(rgb) {
938
+ const match = rgb.match(/\d+/g);
939
+ if (!match || match.length < 3) return null;
940
+ return '#' + match.slice(0, 3).map(x => {
941
+ const hex = parseInt(x).toString(16);
942
+ return hex.length === 1 ? '0' + hex : hex;
943
+ }).join('');
944
+ }
945
+
946
+ async validate() {
947
+ this.log('Validating capture...', 'info');
948
+
949
+ const validation = this.config.validation || {};
950
+ const results = {
951
+ passed: true,
952
+ checks: []
953
+ };
954
+
955
+ // Check minimum pages
956
+ const minPages = validation.minPages || 5;
957
+ const pageCheck = {
958
+ name: 'Minimum pages captured',
959
+ expected: minPages,
960
+ actual: this.stats.pagesCaptured,
961
+ passed: this.stats.pagesCaptured >= minPages
962
+ };
963
+ results.checks.push(pageCheck);
964
+ if (!pageCheck.passed) results.passed = false;
965
+
966
+ // Check for detail pages
967
+ if (validation.requireDetailPages !== false) {
968
+ const listPages = [...this.capturedPages.keys()].filter(url =>
969
+ url.includes('list') || url.includes('accounts') || url.includes('contacts')
970
+ );
971
+ const detailPages = [...this.capturedPages.keys()].filter(url =>
972
+ url.includes('detail') || url.includes('-id') || url.match(/\/\d+\//)
973
+ );
974
+
975
+ const detailCheck = {
976
+ name: 'Detail pages captured',
977
+ expected: `${listPages.length} list pages should have detail pages`,
978
+ actual: `${detailPages.length} detail pages found`,
979
+ passed: listPages.length === 0 || detailPages.length > 0
980
+ };
981
+ results.checks.push(detailCheck);
982
+ if (!detailCheck.passed) {
983
+ results.passed = false;
984
+ this.warnings.push('List pages found but no detail pages captured');
985
+ }
986
+ }
987
+
988
+ // Check design tokens
989
+ const outputDir = this.config.output?.directory || './references';
990
+ const tokensPath = path.join(outputDir, 'design-tokens.json');
991
+ if (fs.existsSync(tokensPath)) {
992
+ const tokens = JSON.parse(fs.readFileSync(tokensPath, 'utf-8'));
993
+ const minColors = validation.minColors || 10;
994
+ const colorCheck = {
995
+ name: 'Design tokens extracted',
996
+ expected: `At least ${minColors} colors`,
997
+ actual: `${tokens.totalColorsFound} colors`,
998
+ passed: tokens.totalColorsFound >= minColors
999
+ };
1000
+ results.checks.push(colorCheck);
1001
+ if (!colorCheck.passed) results.passed = false;
1002
+ }
1003
+
1004
+ // Check for errors
1005
+ const errorCheck = {
1006
+ name: 'No critical errors',
1007
+ expected: '0 errors',
1008
+ actual: `${this.errors.length} errors`,
1009
+ passed: this.errors.length === 0
1010
+ };
1011
+ results.checks.push(errorCheck);
1012
+ if (!errorCheck.passed) results.passed = false;
1013
+
1014
+ // Log results
1015
+ results.checks.forEach(check => {
1016
+ const icon = check.passed ? '✓' : '✗';
1017
+ this.log(`${icon} ${check.name}: ${check.actual}`, check.passed ? 'success' : 'error');
1018
+ });
1019
+
1020
+ return results;
1021
+ }
1022
+
1023
+ async generateManifest() {
1024
+ this.log('Generating manifest...', 'info');
1025
+
1026
+ const outputDir = this.config.output?.directory || './references';
1027
+ const manifest = {
1028
+ platform: {
1029
+ name: this.config.platform.name,
1030
+ baseUrl: this.config.platform.baseUrl,
1031
+ capturedAt: new Date().toISOString()
1032
+ },
1033
+ pages: [...this.capturedPages.entries()].map(([url, data]) => ({
1034
+ name: data.name,
1035
+ url: data.url,
1036
+ screenshot: data.captures[0]?.screenshot,
1037
+ html: data.html,
1038
+ description: this.generateDescription(data),
1039
+ captures: data.captures,
1040
+ tabs: data.tabs,
1041
+ interactions: data.interactions
1042
+ })),
1043
+ stats: this.stats,
1044
+ designTokens: 'design-tokens.json'
1045
+ };
1046
+
1047
+ fs.writeFileSync(
1048
+ path.join(outputDir, 'manifest.json'),
1049
+ JSON.stringify(manifest, null, 2)
1050
+ );
1051
+
1052
+ this.log('Manifest generated', 'success');
1053
+ }
1054
+
1055
+ generateDescription(pageData) {
1056
+ const name = pageData.name;
1057
+ if (name.includes('detail')) return `${name.replace('-detail', '')} detail page`;
1058
+ if (name.includes('list')) return `${name.replace('-list', '')} list page`;
1059
+ if (name.includes('settings')) return 'Application settings';
1060
+ return `${name} page`;
1061
+ }
1062
+
1063
+ generateReport(validationResult) {
1064
+ console.log('\n' + '='.repeat(60));
1065
+ console.log('CAPTURE REPORT');
1066
+ console.log('='.repeat(60));
1067
+
1068
+ console.log('\nStatistics:');
1069
+ console.log(` Pages discovered: ${this.stats.pagesDiscovered}`);
1070
+ console.log(` Pages captured: ${this.stats.pagesCaptured}`);
1071
+ console.log(` Screenshots taken: ${this.stats.screenshotsTaken}`);
1072
+ console.log(` HTML files: ${this.stats.htmlCaptured}`);
1073
+ console.log(` Interactions: ${this.stats.interactionsPerformed}`);
1074
+ console.log(` Errors: ${this.stats.errorsEncountered}`);
1075
+
1076
+ if (this.warnings.length > 0) {
1077
+ console.log('\nWarnings:');
1078
+ this.warnings.forEach(w => console.log(` ⚠ ${w}`));
1079
+ }
1080
+
1081
+ if (this.errors.length > 0) {
1082
+ console.log('\nErrors:');
1083
+ this.errors.forEach(e => console.log(` ✗ ${e.url || e.phase}: ${e.error}`));
1084
+ }
1085
+
1086
+ console.log('\nValidation:');
1087
+ console.log(` Status: ${validationResult.passed ? 'PASSED ✓' : 'FAILED ✗'}`);
1088
+
1089
+ console.log('\n' + '='.repeat(60));
1090
+ }
1091
+ }
1092
+
1093
+ // CLI Interface
1094
+ function parseArgs() {
1095
+ const args = process.argv.slice(2);
1096
+ const config = {
1097
+ platform: {},
1098
+ auth: { type: 'form', credentials: {} },
1099
+ capture: {},
1100
+ output: {},
1101
+ validation: {}
1102
+ };
1103
+
1104
+ for (let i = 0; i < args.length; i++) {
1105
+ switch (args[i]) {
1106
+ case '--config':
1107
+ const configFile = args[++i];
1108
+ const fileConfig = JSON.parse(fs.readFileSync(configFile, 'utf-8'));
1109
+ return fileConfig;
1110
+ case '--url':
1111
+ config.platform.baseUrl = args[++i];
1112
+ config.platform.name = new URL(config.platform.baseUrl).hostname;
1113
+ break;
1114
+ case '--email':
1115
+ config.auth.credentials.email = args[++i];
1116
+ break;
1117
+ case '--password':
1118
+ config.auth.credentials.password = args[++i];
1119
+ break;
1120
+ case '--output':
1121
+ config.output.directory = args[++i];
1122
+ break;
1123
+ case '--help':
1124
+ console.log(`
1125
+ Platform Capture Engine
1126
+
1127
+ Usage:
1128
+ node capture-engine.js --config ./config.json
1129
+ node capture-engine.js --url https://app.example.com --email user@test.com --password secret
1130
+
1131
+ Options:
1132
+ --config Path to JSON configuration file
1133
+ --url Platform base URL
1134
+ --email Login email
1135
+ --password Login password
1136
+ --output Output directory (default: ./references)
1137
+ --help Show this help
1138
+ `);
1139
+ process.exit(0);
1140
+ }
1141
+ }
1142
+
1143
+ return config;
1144
+ }
1145
+
1146
+ // Main
1147
+ const config = parseArgs();
1148
+ const engine = new CaptureEngine(config);
1149
+ engine.run().then(result => {
1150
+ process.exit(result.success ? 0 : 1);
1151
+ });
1152
+
1153
+ module.exports = { CaptureEngine };