cbrowser 2.3.0 → 3.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.
package/dist/cli.js CHANGED
@@ -8,11 +8,12 @@
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  const browser_js_1 = require("./browser.js");
10
10
  const personas_js_1 = require("./personas.js");
11
+ const types_js_1 = require("./types.js");
11
12
  function showHelp() {
12
13
  console.log(`
13
14
  ╔══════════════════════════════════════════════════════════════════════════════╗
14
- ║ CBrowser CLI v2.3.0 ║
15
- AI-powered browser automation with multi-browser support
15
+ ║ CBrowser CLI v3.0.0 ║
16
+ AI-powered browser automation with natural language & fluent API
16
17
  ╚══════════════════════════════════════════════════════════════════════════════╝
17
18
 
18
19
  NAVIGATION
@@ -30,6 +31,7 @@ AUTONOMOUS JOURNEYS
30
31
  journey <persona> Run autonomous exploration
31
32
  --start <url> Starting URL (required)
32
33
  --goal <goal> What to accomplish
34
+ --record-video Record journey as video
33
35
 
34
36
  PERSONAS
35
37
  persona list List available personas
@@ -41,6 +43,90 @@ SESSION MANAGEMENT
41
43
  session list List all saved sessions
42
44
  session delete <name> Delete a saved session
43
45
 
46
+ COOKIE MANAGEMENT
47
+ cookie list List all cookies for current page
48
+ --url <url> Navigate to URL first
49
+ cookie set <name> <value> Set a cookie
50
+ --domain <domain> Cookie domain
51
+ --path <path> Cookie path (default: /)
52
+ cookie delete <name> Delete a cookie
53
+ --domain <domain> Only delete for this domain
54
+ cookie clear Clear all cookies
55
+
56
+ DEVICE EMULATION
57
+ device list List available device presets
58
+ device set <name> Set device emulation for session
59
+ Example: cbrowser device set iphone-15
60
+
61
+ GEOLOCATION
62
+ geo list List available location presets
63
+ geo set <location> Set geolocation (preset name or lat,lon)
64
+ Examples:
65
+ cbrowser geo set new-york
66
+ cbrowser geo set 37.7749,-122.4194
67
+
68
+ PERFORMANCE
69
+ perf [url] Collect performance metrics
70
+ perf audit [url] Run performance audit against budget
71
+ --budget-lcp <ms> LCP budget (default: 2500)
72
+ --budget-fcp <ms> FCP budget (default: 1800)
73
+ --budget-cls <score> CLS budget (default: 0.1)
74
+
75
+ NETWORK / HAR
76
+ har start Start recording HAR
77
+ har stop [output] Stop and save HAR file
78
+ network list List captured network requests
79
+
80
+ VISUAL REGRESSION (v2.5.0)
81
+ visual save <name> Save baseline screenshot
82
+ --url <url> Navigate to URL first
83
+ visual compare <name> Compare current page against baseline
84
+ --threshold <n> Diff threshold 0-1 (default: 0.1)
85
+ visual list List all saved baselines
86
+ visual delete <name> Delete a baseline
87
+
88
+ ACCESSIBILITY (v2.5.0)
89
+ a11y audit Run WCAG accessibility audit
90
+ --url <url> Navigate to URL first
91
+ a11y audit [url] Audit a specific URL
92
+
93
+ TEST RECORDING (v2.5.0)
94
+ record start Start recording interactions
95
+ --url <url> Navigate to URL to begin recording
96
+ record stop Stop recording and show actions
97
+ record save <name> Save recorded test
98
+ record list List saved recordings
99
+ record generate <name> Generate Playwright test code
100
+
101
+ TEST EXPORT (v2.5.0)
102
+ export junit <name> [output] Export test results as JUnit XML
103
+ export tap <name> [output] Export test results as TAP format
104
+
105
+ WEBHOOKS (v2.5.0)
106
+ webhook add <name> <url> Add webhook notification
107
+ --events <events> Comma-separated: test.pass,test.fail,journey.complete
108
+ --format <format> slack, discord, or generic
109
+ webhook list List configured webhooks
110
+ webhook delete <name> Delete a webhook
111
+ webhook test <name> Send test notification
112
+
113
+ PARALLEL EXECUTION (v2.5.0)
114
+ parallel devices <url> Run same URL across multiple devices
115
+ --devices <list> Comma-separated device names (default: all)
116
+ --concurrency <n> Max parallel browsers (default: 3)
117
+ parallel urls <urls> Run same task across multiple URLs
118
+ --concurrency <n> Max parallel browsers (default: 3)
119
+ parallel perf <urls> Performance audit multiple URLs in parallel
120
+ --concurrency <n> Max parallel browsers (default: 3)
121
+
122
+ NATURAL LANGUAGE (v3.0.0)
123
+ run "<command>" Execute natural language command
124
+ Examples:
125
+ cbrowser run "go to https://example.com"
126
+ cbrowser run "click the login button"
127
+ cbrowser run "type 'hello' in the search box"
128
+ script <file> Execute script file with natural language commands
129
+
44
130
  STORAGE & CLEANUP
45
131
  storage Show storage usage statistics
46
132
  cleanup Clean up old files
@@ -50,23 +136,53 @@ STORAGE & CLEANUP
50
136
  --keep-journeys <n> Keep at least N journeys (default: 5)
51
137
 
52
138
  OPTIONS
53
- --browser <type> Browser engine: chromium, firefox, webkit (default: chromium)
139
+ --browser <type> Browser: chromium, firefox, webkit (default: chromium)
140
+ --device <name> Device preset: iphone-15, pixel-8, ipad-pro-12, etc.
141
+ --geo <location> Location preset or lat,lon coordinates
142
+ --locale <locale> Browser locale (e.g., en-US, fr-FR)
143
+ --timezone <tz> Timezone (e.g., America/New_York)
144
+ --record-video Enable video recording
54
145
  --force Bypass red zone safety checks
55
146
  --headless Run browser in headless mode
56
147
 
57
148
  ENVIRONMENT VARIABLES
58
149
  CBROWSER_DATA_DIR Custom data directory (default: ~/.cbrowser)
59
150
  CBROWSER_BROWSER Browser engine (chromium/firefox/webkit)
151
+ CBROWSER_DEVICE Device preset name
152
+ CBROWSER_LOCALE Browser locale
153
+ CBROWSER_TIMEZONE Timezone
60
154
  CBROWSER_HEADLESS Run headless by default (true/false)
61
155
  CBROWSER_TIMEOUT Default timeout in ms (default: 30000)
156
+ CBROWSER_RECORD_VIDEO Record video by default (true/false)
157
+
158
+ CONFIG FILE
159
+ CBrowser looks for config in these locations:
160
+ .cbrowserrc.json Project config
161
+ .cbrowserrc Project config
162
+ cbrowser.config.json Project config
163
+ ~/.cbrowser/config.json User config
164
+ ~/.cbrowserrc.json User config
165
+
166
+ Example .cbrowserrc.json:
167
+ {
168
+ "browser": "chromium",
169
+ "device": "iphone-15",
170
+ "geolocation": "new-york",
171
+ "locale": "en-US",
172
+ "recordVideo": true,
173
+ "performanceBudget": {
174
+ "lcp": 2500,
175
+ "cls": 0.1
176
+ }
177
+ }
62
178
 
63
179
  EXAMPLES
64
180
  npx cbrowser navigate "https://example.com"
65
- npx cbrowser click "Sign in"
66
- npx cbrowser fill "email" "user@example.com"
67
- npx cbrowser journey first-timer --start "https://example.com" --goal "Find products"
68
- npx cbrowser session save "logged-in" --url "https://myapp.com"
69
- npx cbrowser cleanup --dry-run
181
+ npx cbrowser navigate "https://example.com" --device iphone-15
182
+ npx cbrowser navigate "https://example.com" --geo san-francisco
183
+ npx cbrowser perf audit "https://example.com" --budget-lcp 2000
184
+ npx cbrowser journey first-timer --start "https://example.com" --record-video
185
+ npx cbrowser cookie list --url "https://example.com"
70
186
  `);
71
187
  }
72
188
  function parseArgs(args) {
@@ -98,6 +214,22 @@ function formatBytes(bytes) {
98
214
  return `${(bytes / 1024).toFixed(1)} KB`;
99
215
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
100
216
  }
217
+ function parseGeoLocation(location) {
218
+ // Check if it's a preset
219
+ if (types_js_1.LOCATION_PRESETS[location]) {
220
+ return types_js_1.LOCATION_PRESETS[location];
221
+ }
222
+ // Try parsing as lat,lon
223
+ const parts = location.split(",");
224
+ if (parts.length === 2) {
225
+ const lat = parseFloat(parts[0]);
226
+ const lon = parseFloat(parts[1]);
227
+ if (!isNaN(lat) && !isNaN(lon)) {
228
+ return { latitude: lat, longitude: lon };
229
+ }
230
+ }
231
+ return null;
232
+ }
101
233
  async function main() {
102
234
  const { command, args, options } = parseArgs(process.argv.slice(2));
103
235
  if (command === "help" || options.help) {
@@ -108,9 +240,24 @@ async function main() {
108
240
  const browserType = options.browser === "firefox" ? "firefox"
109
241
  : options.browser === "webkit" ? "webkit"
110
242
  : "chromium";
243
+ // Parse geolocation
244
+ let geolocation = undefined;
245
+ if (options.geo) {
246
+ geolocation = parseGeoLocation(options.geo);
247
+ if (!geolocation) {
248
+ console.error(`Invalid geolocation: ${options.geo}`);
249
+ console.error("Use a preset name (new-york, london, etc.) or lat,lon format");
250
+ process.exit(1);
251
+ }
252
+ }
111
253
  const browser = new browser_js_1.CBrowser({
112
254
  browser: browserType,
113
255
  headless: options.headless === true || options.headless === "true",
256
+ device: options.device,
257
+ geolocation,
258
+ locale: options.locale,
259
+ timezone: options.timezone,
260
+ recordVideo: options["record-video"] === true,
114
261
  });
115
262
  try {
116
263
  switch (command) {
@@ -332,6 +479,805 @@ async function main() {
332
479
  console.log(` TOTAL: ${result.deleted} files | ${formatBytes(result.freedBytes)} ${cleanupOptions.dryRun ? "would be " : ""}freed`);
333
480
  break;
334
481
  }
482
+ // =========================================================================
483
+ // Cookie Management
484
+ // =========================================================================
485
+ case "cookie": {
486
+ const subcommand = args[0];
487
+ switch (subcommand) {
488
+ case "list": {
489
+ if (options.url) {
490
+ await browser.navigate(options.url);
491
+ }
492
+ const cookies = await browser.getCookies();
493
+ if (cookies.length === 0) {
494
+ console.log("No cookies found");
495
+ }
496
+ else {
497
+ console.log("\n🍪 Cookies:\n");
498
+ for (const cookie of cookies) {
499
+ console.log(` ${cookie.name}`);
500
+ console.log(` Value: ${cookie.value.substring(0, 50)}${cookie.value.length > 50 ? "..." : ""}`);
501
+ console.log(` Domain: ${cookie.domain}`);
502
+ console.log(` Path: ${cookie.path}`);
503
+ console.log(` Expires: ${cookie.expires === -1 ? "Session" : new Date(cookie.expires * 1000).toISOString()}`);
504
+ console.log("");
505
+ }
506
+ }
507
+ break;
508
+ }
509
+ case "set": {
510
+ const name = args[1];
511
+ const value = args[2];
512
+ if (!name || !value) {
513
+ console.error("Usage: cbrowser cookie set <name> <value> [--domain <domain>]");
514
+ process.exit(1);
515
+ }
516
+ if (options.url) {
517
+ await browser.navigate(options.url);
518
+ }
519
+ const domain = options.domain || "localhost";
520
+ const path = options.path || "/";
521
+ await browser.setCookies([{
522
+ name,
523
+ value,
524
+ domain,
525
+ path,
526
+ expires: -1,
527
+ httpOnly: false,
528
+ secure: false,
529
+ sameSite: "Lax",
530
+ }]);
531
+ console.log(`✓ Cookie set: ${name}=${value}`);
532
+ break;
533
+ }
534
+ case "delete": {
535
+ const name = args[1];
536
+ if (!name) {
537
+ console.error("Usage: cbrowser cookie delete <name> [--domain <domain>]");
538
+ process.exit(1);
539
+ }
540
+ if (options.url) {
541
+ await browser.navigate(options.url);
542
+ }
543
+ await browser.deleteCookie(name, options.domain);
544
+ console.log(`✓ Cookie deleted: ${name}`);
545
+ break;
546
+ }
547
+ case "clear": {
548
+ await browser.clearCookies();
549
+ console.log("✓ All cookies cleared");
550
+ break;
551
+ }
552
+ default:
553
+ console.error("Usage: cbrowser cookie [list|set|delete|clear]");
554
+ }
555
+ break;
556
+ }
557
+ // =========================================================================
558
+ // Device Emulation
559
+ // =========================================================================
560
+ case "device": {
561
+ const subcommand = args[0];
562
+ switch (subcommand) {
563
+ case "list": {
564
+ console.log("\n📱 Available Device Presets:\n");
565
+ for (const [name, device] of Object.entries(types_js_1.DEVICE_PRESETS)) {
566
+ console.log(` ${name}`);
567
+ console.log(` ${device.name}`);
568
+ console.log(` ${device.viewport.width}x${device.viewport.height} @${device.deviceScaleFactor}x`);
569
+ console.log(` Mobile: ${device.isMobile} | Touch: ${device.hasTouch}`);
570
+ console.log("");
571
+ }
572
+ break;
573
+ }
574
+ case "set": {
575
+ const deviceName = args[1];
576
+ if (!deviceName) {
577
+ console.error("Usage: cbrowser device set <name>");
578
+ process.exit(1);
579
+ }
580
+ if (!types_js_1.DEVICE_PRESETS[deviceName]) {
581
+ console.error(`Unknown device: ${deviceName}`);
582
+ console.error("Run 'cbrowser device list' to see available devices");
583
+ process.exit(1);
584
+ }
585
+ console.log(`✓ Device set: ${deviceName}`);
586
+ console.log(" Note: Device emulation applies to new browser sessions");
587
+ break;
588
+ }
589
+ default:
590
+ console.error("Usage: cbrowser device [list|set]");
591
+ }
592
+ break;
593
+ }
594
+ // =========================================================================
595
+ // Geolocation
596
+ // =========================================================================
597
+ case "geo": {
598
+ const subcommand = args[0];
599
+ switch (subcommand) {
600
+ case "list": {
601
+ console.log("\n🌍 Available Location Presets:\n");
602
+ for (const [name, loc] of Object.entries(types_js_1.LOCATION_PRESETS)) {
603
+ console.log(` ${name}`);
604
+ console.log(` Lat: ${loc.latitude}, Lon: ${loc.longitude}`);
605
+ console.log("");
606
+ }
607
+ break;
608
+ }
609
+ case "set": {
610
+ const location = args[1];
611
+ if (!location) {
612
+ console.error("Usage: cbrowser geo set <location>");
613
+ console.error(" Location can be a preset name or lat,lon coordinates");
614
+ process.exit(1);
615
+ }
616
+ const geo = parseGeoLocation(location);
617
+ if (!geo) {
618
+ console.error(`Invalid location: ${location}`);
619
+ process.exit(1);
620
+ }
621
+ await browser.setGeolocationRuntime(geo);
622
+ console.log(`✓ Geolocation set: ${geo.latitude}, ${geo.longitude}`);
623
+ break;
624
+ }
625
+ default:
626
+ console.error("Usage: cbrowser geo [list|set]");
627
+ }
628
+ break;
629
+ }
630
+ // =========================================================================
631
+ // Performance
632
+ // =========================================================================
633
+ case "perf": {
634
+ const subcommand = args[0];
635
+ if (subcommand === "audit") {
636
+ const url = args[1];
637
+ if (url) {
638
+ await browser.navigate(url);
639
+ }
640
+ else if (options.url) {
641
+ await browser.navigate(options.url);
642
+ }
643
+ // Create performance budget from options
644
+ const budget = {
645
+ lcp: options["budget-lcp"] ? parseInt(options["budget-lcp"]) : 2500,
646
+ fcp: options["budget-fcp"] ? parseInt(options["budget-fcp"]) : 1800,
647
+ cls: options["budget-cls"] ? parseFloat(options["budget-cls"]) : 0.1,
648
+ };
649
+ // Temporarily set budget in config
650
+ browser.config.performanceBudget = budget;
651
+ const result = await browser.auditPerformance();
652
+ console.log("\n📊 Performance Audit:\n");
653
+ console.log(` URL: ${result.url}`);
654
+ console.log(` Result: ${result.passed ? "✓ PASSED" : "✗ FAILED"}`);
655
+ console.log("");
656
+ console.log(" Metrics:");
657
+ if (result.metrics.lcp)
658
+ console.log(` LCP: ${result.metrics.lcp.toFixed(0)}ms (${result.metrics.lcpRating})`);
659
+ if (result.metrics.fcp)
660
+ console.log(` FCP: ${result.metrics.fcp.toFixed(0)}ms`);
661
+ if (result.metrics.cls !== undefined)
662
+ console.log(` CLS: ${result.metrics.cls.toFixed(3)} (${result.metrics.clsRating})`);
663
+ if (result.metrics.ttfb)
664
+ console.log(` TTFB: ${result.metrics.ttfb.toFixed(0)}ms`);
665
+ if (result.metrics.load)
666
+ console.log(` Load: ${result.metrics.load.toFixed(0)}ms`);
667
+ if (result.metrics.resourceCount)
668
+ console.log(` Resources: ${result.metrics.resourceCount}`);
669
+ if (result.metrics.transferSize)
670
+ console.log(` Transfer: ${formatBytes(result.metrics.transferSize)}`);
671
+ if (result.violations.length > 0) {
672
+ console.log("");
673
+ console.log(" ⚠️ Budget Violations:");
674
+ for (const v of result.violations) {
675
+ console.log(` - ${v}`);
676
+ }
677
+ }
678
+ }
679
+ else {
680
+ // Just collect metrics
681
+ const url = args[0];
682
+ if (url) {
683
+ await browser.navigate(url);
684
+ }
685
+ else if (options.url) {
686
+ await browser.navigate(options.url);
687
+ }
688
+ const metrics = await browser.getPerformanceMetrics();
689
+ console.log("\n📊 Performance Metrics:\n");
690
+ if (metrics.lcp)
691
+ console.log(` LCP: ${metrics.lcp.toFixed(0)}ms (${metrics.lcpRating})`);
692
+ if (metrics.fcp)
693
+ console.log(` FCP: ${metrics.fcp.toFixed(0)}ms`);
694
+ if (metrics.cls !== undefined)
695
+ console.log(` CLS: ${metrics.cls.toFixed(3)} (${metrics.clsRating})`);
696
+ if (metrics.ttfb)
697
+ console.log(` TTFB: ${metrics.ttfb.toFixed(0)}ms`);
698
+ if (metrics.domContentLoaded)
699
+ console.log(` DOMContentLoaded: ${metrics.domContentLoaded.toFixed(0)}ms`);
700
+ if (metrics.load)
701
+ console.log(` Load: ${metrics.load.toFixed(0)}ms`);
702
+ if (metrics.resourceCount)
703
+ console.log(` Resources: ${metrics.resourceCount}`);
704
+ if (metrics.transferSize)
705
+ console.log(` Transfer Size: ${formatBytes(metrics.transferSize)}`);
706
+ }
707
+ break;
708
+ }
709
+ // =========================================================================
710
+ // HAR Recording
711
+ // =========================================================================
712
+ case "har": {
713
+ const subcommand = args[0];
714
+ switch (subcommand) {
715
+ case "start": {
716
+ browser.startHarRecording();
717
+ console.log("✓ HAR recording started");
718
+ console.log(" Navigate and interact with pages, then run 'cbrowser har stop'");
719
+ break;
720
+ }
721
+ case "stop": {
722
+ const output = args[1];
723
+ const filename = await browser.exportHar(output);
724
+ console.log(`✓ HAR saved: ${filename}`);
725
+ break;
726
+ }
727
+ default:
728
+ console.error("Usage: cbrowser har [start|stop]");
729
+ }
730
+ break;
731
+ }
732
+ // =========================================================================
733
+ // Network
734
+ // =========================================================================
735
+ case "network": {
736
+ const subcommand = args[0];
737
+ switch (subcommand) {
738
+ case "list": {
739
+ const requests = browser.getNetworkRequests();
740
+ if (requests.length === 0) {
741
+ console.log("No network requests captured");
742
+ console.log("Navigate to a page first to capture requests");
743
+ }
744
+ else {
745
+ console.log(`\n🌐 Network Requests (${requests.length}):\n`);
746
+ for (const req of requests.slice(-20)) {
747
+ console.log(` ${req.method} ${req.url.substring(0, 80)}${req.url.length > 80 ? "..." : ""}`);
748
+ console.log(` Type: ${req.resourceType} | Time: ${req.timestamp}`);
749
+ }
750
+ if (requests.length > 20) {
751
+ console.log(`\n ... and ${requests.length - 20} more requests`);
752
+ }
753
+ }
754
+ break;
755
+ }
756
+ case "clear": {
757
+ browser.clearNetworkHistory();
758
+ console.log("✓ Network history cleared");
759
+ break;
760
+ }
761
+ default:
762
+ console.error("Usage: cbrowser network [list|clear]");
763
+ }
764
+ break;
765
+ }
766
+ // =========================================================================
767
+ // Visual Regression (Tier 2)
768
+ // =========================================================================
769
+ case "visual": {
770
+ const subcommand = args[0];
771
+ switch (subcommand) {
772
+ case "save": {
773
+ const name = args[1];
774
+ if (!name) {
775
+ console.error("Usage: cbrowser visual save <name> [--url <url>]");
776
+ process.exit(1);
777
+ }
778
+ if (options.url) {
779
+ await browser.navigate(options.url);
780
+ }
781
+ const path = await browser.saveBaseline(name);
782
+ console.log(`✓ Baseline saved: ${name}`);
783
+ console.log(` Path: ${path}`);
784
+ break;
785
+ }
786
+ case "compare": {
787
+ const name = args[1];
788
+ if (!name) {
789
+ console.error("Usage: cbrowser visual compare <name> [--threshold <n>]");
790
+ process.exit(1);
791
+ }
792
+ if (options.url) {
793
+ await browser.navigate(options.url);
794
+ }
795
+ const threshold = options.threshold ? parseFloat(options.threshold) : 0.1;
796
+ const result = await browser.compareBaseline(name, threshold);
797
+ console.log("\n🔍 Visual Comparison:\n");
798
+ console.log(` Baseline: ${name}`);
799
+ console.log(` Difference: ${(result.diffPercentage * 100).toFixed(2)}%`);
800
+ console.log(` Threshold: ${(threshold * 100).toFixed(0)}%`);
801
+ console.log(` Result: ${result.passed ? "✓ PASSED" : "✗ FAILED"}`);
802
+ if (result.diffPath) {
803
+ console.log(` Diff image: ${result.diffPath}`);
804
+ }
805
+ if (!result.passed) {
806
+ process.exit(1);
807
+ }
808
+ break;
809
+ }
810
+ case "list": {
811
+ const baselines = browser.listBaselines();
812
+ if (baselines.length === 0) {
813
+ console.log("No baselines saved");
814
+ }
815
+ else {
816
+ console.log("\n📸 Visual Baselines:\n");
817
+ for (const b of baselines) {
818
+ console.log(` - ${b}`);
819
+ }
820
+ }
821
+ break;
822
+ }
823
+ case "delete": {
824
+ const name = args[1];
825
+ if (!name) {
826
+ console.error("Usage: cbrowser visual delete <name>");
827
+ process.exit(1);
828
+ }
829
+ // Delete baseline file
830
+ const fs = await import("fs");
831
+ const path = await import("path");
832
+ const baselinePath = path.join(browser.getDataDir(), "baselines", `${name}.png`);
833
+ if (fs.existsSync(baselinePath)) {
834
+ fs.unlinkSync(baselinePath);
835
+ console.log(`✓ Baseline deleted: ${name}`);
836
+ }
837
+ else {
838
+ console.error(`✗ Baseline not found: ${name}`);
839
+ process.exit(1);
840
+ }
841
+ break;
842
+ }
843
+ default:
844
+ console.error("Usage: cbrowser visual [save|compare|list|delete]");
845
+ }
846
+ break;
847
+ }
848
+ // =========================================================================
849
+ // Accessibility (Tier 2)
850
+ // =========================================================================
851
+ case "a11y": {
852
+ const subcommand = args[0];
853
+ if (subcommand === "audit") {
854
+ const url = args[1];
855
+ if (url) {
856
+ await browser.navigate(url);
857
+ }
858
+ else if (options.url) {
859
+ await browser.navigate(options.url);
860
+ }
861
+ const result = await browser.auditAccessibility();
862
+ console.log("\n♿ Accessibility Audit:\n");
863
+ console.log(` URL: ${result.url}`);
864
+ console.log(` Score: ${result.score}/100`);
865
+ console.log(` Passes: ${result.passes}`);
866
+ console.log(` Violations: ${result.violations.length}`);
867
+ if (result.violations.length > 0) {
868
+ console.log("\n ⚠️ Violations:\n");
869
+ for (const v of result.violations) {
870
+ console.log(` [${v.impact.toUpperCase()}] ${v.id}`);
871
+ console.log(` ${v.description}`);
872
+ console.log(` Help: ${v.helpUrl}`);
873
+ console.log("");
874
+ }
875
+ }
876
+ }
877
+ else {
878
+ console.error("Usage: cbrowser a11y audit [url]");
879
+ }
880
+ break;
881
+ }
882
+ // =========================================================================
883
+ // Test Recording (Tier 2)
884
+ // =========================================================================
885
+ case "record": {
886
+ const subcommand = args[0];
887
+ switch (subcommand) {
888
+ case "start": {
889
+ const url = options.url;
890
+ await browser.startRecording(url);
891
+ console.log("✓ Recording started");
892
+ if (url) {
893
+ console.log(` Navigated to: ${url}`);
894
+ }
895
+ console.log(" Interact with the page, then run 'cbrowser record stop'");
896
+ break;
897
+ }
898
+ case "stop": {
899
+ const actions = browser.stopRecording();
900
+ console.log(`✓ Recording stopped`);
901
+ console.log(` Captured ${actions.length} actions`);
902
+ if (actions.length > 0) {
903
+ console.log("\n Actions:");
904
+ for (const action of actions) {
905
+ console.log(` ${action.type}: ${action.selector || action.url || action.value || ""}`);
906
+ }
907
+ }
908
+ break;
909
+ }
910
+ case "save": {
911
+ const name = args[1];
912
+ if (!name) {
913
+ console.error("Usage: cbrowser record save <name>");
914
+ process.exit(1);
915
+ }
916
+ const path = browser.saveRecording(name);
917
+ console.log(`✓ Recording saved: ${name}`);
918
+ console.log(` Path: ${path}`);
919
+ break;
920
+ }
921
+ case "list": {
922
+ const fs = await import("fs");
923
+ const path = await import("path");
924
+ const recordingsDir = path.join(browser.getDataDir(), "recordings");
925
+ if (!fs.existsSync(recordingsDir)) {
926
+ console.log("No recordings saved");
927
+ }
928
+ else {
929
+ const files = fs.readdirSync(recordingsDir).filter((f) => f.endsWith(".json"));
930
+ if (files.length === 0) {
931
+ console.log("No recordings saved");
932
+ }
933
+ else {
934
+ console.log("\n🎬 Saved Recordings:\n");
935
+ for (const f of files) {
936
+ console.log(` - ${f.replace(".json", "")}`);
937
+ }
938
+ }
939
+ }
940
+ break;
941
+ }
942
+ case "generate": {
943
+ const name = args[1];
944
+ if (!name) {
945
+ console.error("Usage: cbrowser record generate <name>");
946
+ process.exit(1);
947
+ }
948
+ const fs = await import("fs");
949
+ const path = await import("path");
950
+ const recordingPath = path.join(browser.getDataDir(), "recordings", `${name}.json`);
951
+ if (!fs.existsSync(recordingPath)) {
952
+ console.error(`Recording not found: ${name}`);
953
+ process.exit(1);
954
+ }
955
+ const recording = JSON.parse(fs.readFileSync(recordingPath, "utf-8"));
956
+ const code = browser.generateTestCode(name, recording.actions);
957
+ console.log(code);
958
+ break;
959
+ }
960
+ default:
961
+ console.error("Usage: cbrowser record [start|stop|save|list|generate]");
962
+ }
963
+ break;
964
+ }
965
+ // =========================================================================
966
+ // Test Export (Tier 2)
967
+ // =========================================================================
968
+ case "export": {
969
+ const format = args[0];
970
+ const name = args[1];
971
+ const output = args[2];
972
+ if (!format || !name) {
973
+ console.error("Usage: cbrowser export [junit|tap] <name> [output]");
974
+ process.exit(1);
975
+ }
976
+ // Load test results (for now, create a mock suite)
977
+ const fs = await import("fs");
978
+ const path = await import("path");
979
+ const resultsPath = path.join(browser.getDataDir(), "results", `${name}.json`);
980
+ let suite;
981
+ if (fs.existsSync(resultsPath)) {
982
+ suite = JSON.parse(fs.readFileSync(resultsPath, "utf-8"));
983
+ }
984
+ else {
985
+ console.error(`Test results not found: ${name}`);
986
+ console.error("Run tests first to generate results");
987
+ process.exit(1);
988
+ }
989
+ if (format === "junit") {
990
+ const exportPath = browser.exportJUnit(suite, output);
991
+ console.log(`✓ JUnit XML exported: ${exportPath}`);
992
+ }
993
+ else if (format === "tap") {
994
+ const exportPath = browser.exportTAP(suite, output);
995
+ console.log(`✓ TAP exported: ${exportPath}`);
996
+ }
997
+ else {
998
+ console.error("Unknown export format. Use 'junit' or 'tap'");
999
+ process.exit(1);
1000
+ }
1001
+ break;
1002
+ }
1003
+ // =========================================================================
1004
+ // Webhooks (Tier 2)
1005
+ // =========================================================================
1006
+ case "webhook": {
1007
+ const subcommand = args[0];
1008
+ const fs = await import("fs");
1009
+ const path = await import("path");
1010
+ const webhooksPath = path.join(browser.getDataDir(), "webhooks.json");
1011
+ // Load existing webhooks
1012
+ let webhooks = [];
1013
+ if (fs.existsSync(webhooksPath)) {
1014
+ webhooks = JSON.parse(fs.readFileSync(webhooksPath, "utf-8"));
1015
+ }
1016
+ switch (subcommand) {
1017
+ case "add": {
1018
+ const name = args[1];
1019
+ const url = args[2];
1020
+ if (!name || !url) {
1021
+ console.error("Usage: cbrowser webhook add <name> <url> [--events <events>] [--format <format>]");
1022
+ process.exit(1);
1023
+ }
1024
+ const events = options.events
1025
+ ? options.events.split(",")
1026
+ : ["test.fail", "journey.complete"];
1027
+ const format = options.format || "generic";
1028
+ // Remove existing webhook with same name
1029
+ webhooks = webhooks.filter(w => w.name !== name);
1030
+ webhooks.push({ name, url, events, format });
1031
+ fs.writeFileSync(webhooksPath, JSON.stringify(webhooks, null, 2));
1032
+ console.log(`✓ Webhook added: ${name}`);
1033
+ console.log(` URL: ${url}`);
1034
+ console.log(` Events: ${events.join(", ")}`);
1035
+ console.log(` Format: ${format}`);
1036
+ break;
1037
+ }
1038
+ case "list": {
1039
+ if (webhooks.length === 0) {
1040
+ console.log("No webhooks configured");
1041
+ }
1042
+ else {
1043
+ console.log("\n🔔 Configured Webhooks:\n");
1044
+ for (const w of webhooks) {
1045
+ console.log(` ${w.name}`);
1046
+ console.log(` URL: ${w.url}`);
1047
+ console.log(` Events: ${w.events.join(", ")}`);
1048
+ console.log(` Format: ${w.format}`);
1049
+ console.log("");
1050
+ }
1051
+ }
1052
+ break;
1053
+ }
1054
+ case "delete": {
1055
+ const name = args[1];
1056
+ if (!name) {
1057
+ console.error("Usage: cbrowser webhook delete <name>");
1058
+ process.exit(1);
1059
+ }
1060
+ const originalLength = webhooks.length;
1061
+ webhooks = webhooks.filter(w => w.name !== name);
1062
+ if (webhooks.length < originalLength) {
1063
+ fs.writeFileSync(webhooksPath, JSON.stringify(webhooks, null, 2));
1064
+ console.log(`✓ Webhook deleted: ${name}`);
1065
+ }
1066
+ else {
1067
+ console.error(`✗ Webhook not found: ${name}`);
1068
+ process.exit(1);
1069
+ }
1070
+ break;
1071
+ }
1072
+ case "test": {
1073
+ const name = args[1];
1074
+ if (!name) {
1075
+ console.error("Usage: cbrowser webhook test <name>");
1076
+ process.exit(1);
1077
+ }
1078
+ const webhook = webhooks.find(w => w.name === name);
1079
+ if (!webhook) {
1080
+ console.error(`✗ Webhook not found: ${name}`);
1081
+ process.exit(1);
1082
+ }
1083
+ // Send test notification
1084
+ const testPayload = webhook.format === "slack"
1085
+ ? { text: "🔔 CBrowser test notification" }
1086
+ : webhook.format === "discord"
1087
+ ? { content: "🔔 CBrowser test notification" }
1088
+ : { event: "test", message: "CBrowser test notification", timestamp: new Date().toISOString() };
1089
+ try {
1090
+ const response = await fetch(webhook.url, {
1091
+ method: "POST",
1092
+ headers: { "Content-Type": "application/json" },
1093
+ body: JSON.stringify(testPayload),
1094
+ });
1095
+ if (response.ok) {
1096
+ console.log(`✓ Test notification sent to: ${name}`);
1097
+ }
1098
+ else {
1099
+ console.error(`✗ Webhook returned ${response.status}`);
1100
+ process.exit(1);
1101
+ }
1102
+ }
1103
+ catch (e) {
1104
+ console.error(`✗ Failed to send notification: ${e.message}`);
1105
+ process.exit(1);
1106
+ }
1107
+ break;
1108
+ }
1109
+ default:
1110
+ console.error("Usage: cbrowser webhook [add|list|delete|test]");
1111
+ }
1112
+ break;
1113
+ }
1114
+ // =========================================================================
1115
+ // Parallel Execution (Tier 2)
1116
+ // =========================================================================
1117
+ case "parallel": {
1118
+ const subcommand = args[0];
1119
+ switch (subcommand) {
1120
+ case "devices": {
1121
+ const url = args[1];
1122
+ if (!url) {
1123
+ console.error("Usage: cbrowser parallel devices <url> [--devices <list>] [--concurrency <n>]");
1124
+ process.exit(1);
1125
+ }
1126
+ const deviceList = options.devices
1127
+ ? options.devices.split(",")
1128
+ : Object.keys(types_js_1.DEVICE_PRESETS);
1129
+ const concurrency = options.concurrency ? parseInt(options.concurrency) : 3;
1130
+ console.log(`\n🚀 Running parallel device tests...`);
1131
+ console.log(` URL: ${url}`);
1132
+ console.log(` Devices: ${deviceList.length}`);
1133
+ console.log(` Concurrency: ${concurrency}\n`);
1134
+ const results = await browser_js_1.CBrowser.parallelDevices(deviceList, async (b, device) => {
1135
+ const nav = await b.navigate(url);
1136
+ const screenshot = await b.screenshot();
1137
+ return { title: nav.title, loadTime: nav.loadTime, screenshot };
1138
+ }, { maxConcurrency: concurrency });
1139
+ console.log("📊 Results:\n");
1140
+ for (const r of results) {
1141
+ if (r.error) {
1142
+ console.log(` ✗ ${r.device}: ${r.error} (${r.duration}ms)`);
1143
+ }
1144
+ else {
1145
+ console.log(` ✓ ${r.device}: ${r.result?.title} - ${r.result?.loadTime}ms (${r.duration}ms total)`);
1146
+ }
1147
+ }
1148
+ const passed = results.filter(r => !r.error).length;
1149
+ console.log(`\n Summary: ${passed}/${results.length} passed`);
1150
+ break;
1151
+ }
1152
+ case "urls": {
1153
+ const urls = args.slice(1);
1154
+ if (urls.length === 0) {
1155
+ console.error("Usage: cbrowser parallel urls <url1> <url2> ... [--concurrency <n>]");
1156
+ process.exit(1);
1157
+ }
1158
+ const concurrency = options.concurrency ? parseInt(options.concurrency) : 3;
1159
+ console.log(`\n🚀 Running parallel URL tests...`);
1160
+ console.log(` URLs: ${urls.length}`);
1161
+ console.log(` Concurrency: ${concurrency}\n`);
1162
+ const results = await browser_js_1.CBrowser.parallelUrls(urls, async (b, url) => {
1163
+ const nav = await b.navigate(url);
1164
+ return { title: nav.title, loadTime: nav.loadTime };
1165
+ }, { maxConcurrency: concurrency });
1166
+ console.log("📊 Results:\n");
1167
+ for (const r of results) {
1168
+ if (r.error) {
1169
+ console.log(` ✗ ${r.url}: ${r.error}`);
1170
+ }
1171
+ else {
1172
+ console.log(` ✓ ${r.url}: ${r.result?.title} (${r.result?.loadTime}ms)`);
1173
+ }
1174
+ }
1175
+ break;
1176
+ }
1177
+ case "perf": {
1178
+ const urls = args.slice(1);
1179
+ if (urls.length === 0) {
1180
+ console.error("Usage: cbrowser parallel perf <url1> <url2> ... [--concurrency <n>]");
1181
+ process.exit(1);
1182
+ }
1183
+ const concurrency = options.concurrency ? parseInt(options.concurrency) : 3;
1184
+ console.log(`\n🚀 Running parallel performance audits...`);
1185
+ console.log(` URLs: ${urls.length}`);
1186
+ console.log(` Concurrency: ${concurrency}\n`);
1187
+ const results = await browser_js_1.CBrowser.parallelUrls(urls, async (b, url) => {
1188
+ await b.navigate(url);
1189
+ return await b.getPerformanceMetrics();
1190
+ }, { maxConcurrency: concurrency });
1191
+ console.log("📊 Performance Results:\n");
1192
+ for (const r of results) {
1193
+ if (r.error) {
1194
+ console.log(` ✗ ${r.url}: ${r.error}`);
1195
+ }
1196
+ else {
1197
+ const m = r.result;
1198
+ console.log(` ✓ ${r.url}`);
1199
+ if (m?.lcp)
1200
+ console.log(` LCP: ${m.lcp.toFixed(0)}ms (${m.lcpRating})`);
1201
+ if (m?.fcp)
1202
+ console.log(` FCP: ${m.fcp.toFixed(0)}ms`);
1203
+ if (m?.cls !== undefined)
1204
+ console.log(` CLS: ${m.cls.toFixed(3)}`);
1205
+ }
1206
+ }
1207
+ break;
1208
+ }
1209
+ default:
1210
+ console.error("Usage: cbrowser parallel [devices|urls|perf]");
1211
+ }
1212
+ break;
1213
+ }
1214
+ // =========================================================================
1215
+ // Natural Language (Tier 3)
1216
+ // =========================================================================
1217
+ case "run": {
1218
+ const nlCommand = args.join(" ");
1219
+ if (!nlCommand) {
1220
+ console.error("Usage: cbrowser run \"<natural language command>\"");
1221
+ console.error("Examples:");
1222
+ console.error(" cbrowser run \"go to https://example.com\"");
1223
+ console.error(" cbrowser run \"click the login button\"");
1224
+ console.error(" cbrowser run \"type 'hello' in the search box\"");
1225
+ process.exit(1);
1226
+ }
1227
+ console.log(`\n🗣️ Executing: "${nlCommand}"\n`);
1228
+ const result = await (0, browser_js_1.executeNaturalLanguage)(browser, nlCommand);
1229
+ if (result.success) {
1230
+ console.log(`✓ Action: ${result.action}`);
1231
+ if (result.result && typeof result.result === "object") {
1232
+ const r = result.result;
1233
+ if (r.url)
1234
+ console.log(` URL: ${r.url}`);
1235
+ if (r.title)
1236
+ console.log(` Title: ${r.title}`);
1237
+ if (r.message)
1238
+ console.log(` ${r.message}`);
1239
+ if (r.screenshot)
1240
+ console.log(` Screenshot: ${r.screenshot}`);
1241
+ }
1242
+ }
1243
+ else {
1244
+ console.error(`✗ ${result.error}`);
1245
+ process.exit(1);
1246
+ }
1247
+ break;
1248
+ }
1249
+ case "script": {
1250
+ const scriptFile = args[0];
1251
+ if (!scriptFile) {
1252
+ console.error("Usage: cbrowser script <file>");
1253
+ process.exit(1);
1254
+ }
1255
+ const fs = await import("fs");
1256
+ if (!fs.existsSync(scriptFile)) {
1257
+ console.error(`Script file not found: ${scriptFile}`);
1258
+ process.exit(1);
1259
+ }
1260
+ const content = fs.readFileSync(scriptFile, "utf-8");
1261
+ const commands = content.split("\n").filter(line => line.trim() && !line.trim().startsWith("#"));
1262
+ console.log(`\n📜 Executing script: ${scriptFile}`);
1263
+ console.log(` Commands: ${commands.length}\n`);
1264
+ const results = await (0, browser_js_1.executeNaturalLanguageScript)(browser, commands);
1265
+ for (const r of results) {
1266
+ if (r.success) {
1267
+ console.log(`✓ ${r.command}`);
1268
+ }
1269
+ else {
1270
+ console.log(`✗ ${r.command}`);
1271
+ console.log(` Error: ${r.error}`);
1272
+ }
1273
+ }
1274
+ const passed = results.filter(r => r.success).length;
1275
+ console.log(`\n Summary: ${passed}/${results.length} commands succeeded`);
1276
+ if (passed < results.length) {
1277
+ process.exit(1);
1278
+ }
1279
+ break;
1280
+ }
335
1281
  default:
336
1282
  console.error(`Unknown command: ${command}`);
337
1283
  console.error("Run 'cbrowser help' for usage");