cbrowser 6.3.2 → 6.5.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
@@ -10,11 +10,12 @@ const browser_js_1 = require("./browser.js");
10
10
  const personas_js_1 = require("./personas.js");
11
11
  const types_js_1 = require("./types.js");
12
12
  const mcp_server_js_1 = require("./mcp-server.js");
13
+ const daemon_js_1 = require("./daemon.js");
13
14
  function showHelp() {
14
15
  console.log(`
15
16
  ╔══════════════════════════════════════════════════════════════════════════════╗
16
- ║ CBrowser CLI v6.3.0 ║
17
- ║ AI-powered browser automation with flaky test detection
17
+ ║ CBrowser CLI v6.5.0 ║
18
+ ║ AI-powered browser automation with test coverage mapping
18
19
  ╚══════════════════════════════════════════════════════════════════════════════╝
19
20
 
20
21
  NAVIGATION
@@ -98,6 +99,26 @@ FLAKY TEST DETECTION (v6.3.0)
98
99
  cbrowser flaky-check tests.txt --runs 10
99
100
  cbrowser flaky-check tests.txt --runs 5 --threshold 30 --output flaky-report.json
100
101
 
102
+ TEST COVERAGE MAP (v6.5.0)
103
+ coverage <url> Generate test coverage map for a site
104
+ --tests <glob> Test files to analyze (default: tests/*.txt)
105
+ --sitemap <url> Use sitemap.xml instead of crawling
106
+ --max-pages <n> Max pages to crawl (default: 100)
107
+ --include <pattern> Only include paths matching pattern
108
+ --exclude <pattern> Exclude paths matching pattern
109
+ --min-coverage <n> Min coverage % to not flag (default: 50)
110
+ --html Generate HTML report
111
+ --output <file> Save JSON report to file
112
+ Examples:
113
+ cbrowser coverage "https://example.com" --tests "tests/*.txt"
114
+ cbrowser coverage "https://example.com" --sitemap "https://example.com/sitemap.xml"
115
+ cbrowser coverage "https://example.com" --html --output coverage.html
116
+ cbrowser coverage "https://example.com" --exclude "/admin" --min-coverage 70
117
+
118
+ coverage gaps <url> Show only untested pages (quick analysis)
119
+ --tests <glob> Test files to analyze
120
+ --sitemap <url> Use sitemap.xml
121
+
101
122
  PERSONAS
102
123
  persona list List all personas (built-in + custom)
103
124
  persona create "<desc>" Create persona from natural language description
@@ -147,6 +168,27 @@ PERFORMANCE
147
168
  --budget-fcp <ms> FCP budget (default: 1800)
148
169
  --budget-cls <score> CLS budget (default: 0.1)
149
170
 
171
+ PERFORMANCE REGRESSION (v6.4.0)
172
+ perf-baseline save <url> Capture and save performance baseline
173
+ --name <name> Human-readable name for baseline
174
+ --runs <n> Number of runs to average (default: 3)
175
+ Examples:
176
+ cbrowser perf-baseline save "https://example.com" --name "homepage"
177
+ cbrowser perf-baseline save "https://example.com/checkout" --runs 5
178
+
179
+ perf-baseline list List all saved baselines
180
+ perf-baseline show <name> Show baseline details
181
+ perf-baseline delete <name> Delete a baseline
182
+
183
+ perf-regression <url> <baseline> Compare current performance against baseline
184
+ --threshold-lcp <n> Max LCP increase % (default: 20)
185
+ --threshold-cls <n> Max CLS increase (default: 0.1)
186
+ --threshold-fcp <n> Max FCP increase % (default: 20)
187
+ --output <file> Save JSON report to file
188
+ Examples:
189
+ cbrowser perf-regression "https://example.com" homepage
190
+ cbrowser perf-regression "https://example.com" homepage --threshold-lcp 30
191
+
150
192
  NETWORK / HAR
151
193
  har start Start recording HAR
152
194
  har stop [output] Stop and save HAR file
@@ -259,6 +301,16 @@ MCP SERVER (v5.0.0)
259
301
  mcp-server Start CBrowser as MCP server for Claude integration
260
302
  Use with Claude Desktop or other MCP-compatible clients
261
303
 
304
+ DAEMON MODE (v6.4.0)
305
+ daemon start Start background daemon (keeps browser running)
306
+ --port <port> Daemon port (default: 9222)
307
+ --timeout <min> Idle timeout in minutes (default: 30)
308
+ daemon stop Stop the running daemon
309
+ daemon status Check if daemon is running
310
+ daemon run Run daemon in foreground (internal use)
311
+ Note: When daemon is running, all commands automatically connect to it
312
+ instead of launching a new browser - much faster for iteration!
313
+
262
314
  STORAGE & CLEANUP
263
315
  storage Show storage usage statistics
264
316
  cleanup Clean up old files
@@ -661,6 +713,90 @@ async function main() {
661
713
  await (0, mcp_server_js_1.startMcpServer)();
662
714
  return;
663
715
  }
716
+ // Daemon mode commands
717
+ if (command === "daemon") {
718
+ const subCommand = args[0];
719
+ const port = parseInt(options.port) || 9222;
720
+ switch (subCommand) {
721
+ case "start": {
722
+ console.log("🚀 Starting CBrowser daemon...");
723
+ const result = await (0, daemon_js_1.startDaemon)(port);
724
+ console.log(result.success ? `✓ ${result.message}` : `✗ ${result.message}`);
725
+ process.exit(result.success ? 0 : 1);
726
+ break;
727
+ }
728
+ case "stop": {
729
+ console.log("🛑 Stopping CBrowser daemon...");
730
+ const result = await (0, daemon_js_1.stopDaemon)();
731
+ console.log(result.success ? `✓ ${result.message}` : `✗ ${result.message}`);
732
+ process.exit(0);
733
+ break;
734
+ }
735
+ case "status": {
736
+ const status = await (0, daemon_js_1.getDaemonStatus)();
737
+ console.log(status);
738
+ process.exit(0);
739
+ break;
740
+ }
741
+ case "run": {
742
+ // Internal: run daemon in foreground
743
+ console.log("🔧 Running daemon in foreground mode...");
744
+ const browserType = options.browser === "firefox" ? "firefox"
745
+ : options.browser === "webkit" ? "webkit"
746
+ : "chromium";
747
+ // runDaemonServer will merge with defaults internally
748
+ await (0, daemon_js_1.runDaemonServer)({
749
+ browser: browserType,
750
+ headless: options.headless !== false && options.headless !== "false",
751
+ }, port);
752
+ return;
753
+ }
754
+ default:
755
+ console.error(`Unknown daemon command: ${subCommand}`);
756
+ console.error("Use: daemon start | daemon stop | daemon status");
757
+ process.exit(1);
758
+ }
759
+ return;
760
+ }
761
+ // Check if daemon is running and use it for supported commands
762
+ const daemonRunning = await (0, daemon_js_1.isDaemonRunning)();
763
+ if (daemonRunning && ["navigate", "click", "fill", "screenshot", "extract", "run"].includes(command)) {
764
+ console.log("🔌 Connected to running daemon");
765
+ let daemonCommand = command;
766
+ let daemonArgs = {};
767
+ switch (command) {
768
+ case "navigate":
769
+ daemonArgs = { url: args[0] };
770
+ break;
771
+ case "click":
772
+ daemonArgs = { selector: args[0] };
773
+ break;
774
+ case "fill":
775
+ daemonArgs = { selector: args[0], value: args[1] };
776
+ break;
777
+ case "screenshot":
778
+ daemonArgs = { path: args[0] };
779
+ break;
780
+ case "extract":
781
+ daemonArgs = { what: args[0] };
782
+ break;
783
+ case "run":
784
+ daemonArgs = { command: args.join(" ") };
785
+ break;
786
+ }
787
+ const result = await (0, daemon_js_1.sendToDaemon)(daemonCommand, daemonArgs);
788
+ if (result.success) {
789
+ console.log("✓ Command executed via daemon");
790
+ if (result.result) {
791
+ console.log(JSON.stringify(result.result, null, 2));
792
+ }
793
+ }
794
+ else {
795
+ console.error(`✗ Daemon error: ${result.error}`);
796
+ process.exit(1);
797
+ }
798
+ return;
799
+ }
664
800
  // Parse browser type
665
801
  const browserType = options.browser === "firefox" ? "firefox"
666
802
  : options.browser === "webkit" ? "webkit"
@@ -2551,6 +2687,303 @@ async function main() {
2551
2687
  }
2552
2688
  break;
2553
2689
  }
2690
+ // =========================================================================
2691
+ // Performance Regression Detection (Tier 6 - v6.4.0)
2692
+ // =========================================================================
2693
+ case "perf-baseline": {
2694
+ const subcommand = args[0];
2695
+ const fs = await import("fs");
2696
+ switch (subcommand) {
2697
+ case "save": {
2698
+ const url = args[1];
2699
+ if (!url) {
2700
+ console.error("Usage: cbrowser perf-baseline save <url> [--name <name>] [--runs <n>]");
2701
+ process.exit(1);
2702
+ }
2703
+ console.log(`\n📊 Capturing performance baseline for: ${url}`);
2704
+ const baselineOptions = {
2705
+ headless,
2706
+ name: options.name,
2707
+ runs: options.runs ? parseInt(options.runs) : 3,
2708
+ };
2709
+ console.log(` Running ${baselineOptions.runs} measurement(s)...`);
2710
+ const baseline = await (0, browser_js_1.capturePerformanceBaseline)(url, baselineOptions);
2711
+ console.log(`\n✅ Baseline saved: ${baseline.name}`);
2712
+ console.log(` ID: ${baseline.id}`);
2713
+ console.log(` URL: ${baseline.url}`);
2714
+ console.log(` Timestamp: ${new Date(baseline.timestamp).toLocaleString()}`);
2715
+ console.log(`\n📈 Metrics (averaged over ${baseline.runsAveraged} runs):`);
2716
+ if (baseline.metrics.lcp !== undefined) {
2717
+ console.log(` LCP: ${baseline.metrics.lcp.toFixed(0)}ms (${baseline.metrics.lcpRating})`);
2718
+ }
2719
+ if (baseline.metrics.fcp !== undefined) {
2720
+ console.log(` FCP: ${baseline.metrics.fcp.toFixed(0)}ms`);
2721
+ }
2722
+ if (baseline.metrics.cls !== undefined) {
2723
+ console.log(` CLS: ${baseline.metrics.cls.toFixed(3)} (${baseline.metrics.clsRating})`);
2724
+ }
2725
+ if (baseline.metrics.ttfb !== undefined) {
2726
+ console.log(` TTFB: ${baseline.metrics.ttfb.toFixed(0)}ms`);
2727
+ }
2728
+ if (baseline.metrics.tti !== undefined) {
2729
+ console.log(` TTI: ${baseline.metrics.tti.toFixed(0)}ms`);
2730
+ }
2731
+ if (baseline.metrics.transferSize !== undefined) {
2732
+ console.log(` Transfer: ${(baseline.metrics.transferSize / 1024).toFixed(1)}KB`);
2733
+ }
2734
+ break;
2735
+ }
2736
+ case "list": {
2737
+ const baselines = (0, browser_js_1.listPerformanceBaselines)();
2738
+ if (baselines.length === 0) {
2739
+ console.log("\n📊 No performance baselines saved yet.");
2740
+ console.log(" Use: cbrowser perf-baseline save <url> --name <name>");
2741
+ }
2742
+ else {
2743
+ console.log(`\n📊 Performance Baselines (${baselines.length}):\n`);
2744
+ for (const b of baselines) {
2745
+ const date = new Date(b.timestamp).toLocaleDateString();
2746
+ const lcp = b.metrics.lcp ? `LCP: ${b.metrics.lcp.toFixed(0)}ms` : "";
2747
+ console.log(` ${b.name}`);
2748
+ console.log(` ID: ${b.id}`);
2749
+ console.log(` URL: ${b.url}`);
2750
+ console.log(` Date: ${date} | ${lcp}`);
2751
+ console.log("");
2752
+ }
2753
+ }
2754
+ break;
2755
+ }
2756
+ case "show": {
2757
+ const name = args[1];
2758
+ if (!name) {
2759
+ console.error("Usage: cbrowser perf-baseline show <name|id>");
2760
+ process.exit(1);
2761
+ }
2762
+ const baseline = (0, browser_js_1.loadPerformanceBaseline)(name);
2763
+ if (!baseline) {
2764
+ console.error(`Baseline not found: ${name}`);
2765
+ process.exit(1);
2766
+ }
2767
+ console.log(`\n📊 Performance Baseline: ${baseline.name}`);
2768
+ console.log(` ID: ${baseline.id}`);
2769
+ console.log(` URL: ${baseline.url}`);
2770
+ console.log(` Timestamp: ${new Date(baseline.timestamp).toLocaleString()}`);
2771
+ console.log(` Runs Averaged: ${baseline.runsAveraged}`);
2772
+ console.log(`\n📈 Metrics:`);
2773
+ console.log(JSON.stringify(baseline.metrics, null, 2));
2774
+ console.log(`\n🖥️ Environment:`);
2775
+ console.log(JSON.stringify(baseline.environment, null, 2));
2776
+ break;
2777
+ }
2778
+ case "delete": {
2779
+ const name = args[1];
2780
+ if (!name) {
2781
+ console.error("Usage: cbrowser perf-baseline delete <name|id>");
2782
+ process.exit(1);
2783
+ }
2784
+ const deleted = (0, browser_js_1.deletePerformanceBaseline)(name);
2785
+ if (deleted) {
2786
+ console.log(`\n✅ Deleted baseline: ${name}`);
2787
+ }
2788
+ else {
2789
+ console.error(`Baseline not found: ${name}`);
2790
+ process.exit(1);
2791
+ }
2792
+ break;
2793
+ }
2794
+ default:
2795
+ console.error("Usage: cbrowser perf-baseline <save|list|show|delete>");
2796
+ console.error("");
2797
+ console.error("Subcommands:");
2798
+ console.error(" save <url> Capture and save performance baseline");
2799
+ console.error(" list List all saved baselines");
2800
+ console.error(" show <name> Show baseline details");
2801
+ console.error(" delete <name> Delete a baseline");
2802
+ process.exit(1);
2803
+ }
2804
+ break;
2805
+ }
2806
+ case "perf-regression": {
2807
+ const url = args[0];
2808
+ const baselineName = args[1];
2809
+ if (!url || !baselineName) {
2810
+ console.error("Usage: cbrowser perf-regression <url> <baseline-name> [options]");
2811
+ console.error("");
2812
+ console.error("Options:");
2813
+ console.error(" --threshold-lcp <n> Max LCP increase % (default: 20)");
2814
+ console.error(" --threshold-cls <n> Max CLS increase absolute (default: 0.1)");
2815
+ console.error(" --threshold-fcp <n> Max FCP increase % (default: 20)");
2816
+ console.error(" --threshold-ttfb <n> Max TTFB increase % (default: 30)");
2817
+ console.error(" --output <file> Save JSON report to file");
2818
+ console.error("");
2819
+ console.error("Examples:");
2820
+ console.error(" cbrowser perf-regression https://example.com homepage");
2821
+ console.error(" cbrowser perf-regression https://example.com homepage --threshold-lcp 30");
2822
+ process.exit(1);
2823
+ }
2824
+ console.log(`\n🔍 Checking for performance regressions...`);
2825
+ console.log(` URL: ${url}`);
2826
+ console.log(` Baseline: ${baselineName}`);
2827
+ const thresholds = {};
2828
+ if (options["threshold-lcp"])
2829
+ thresholds.lcp = parseInt(options["threshold-lcp"]);
2830
+ if (options["threshold-cls"])
2831
+ thresholds.cls = parseFloat(options["threshold-cls"]);
2832
+ if (options["threshold-fcp"])
2833
+ thresholds.fcp = parseInt(options["threshold-fcp"]);
2834
+ if (options["threshold-ttfb"])
2835
+ thresholds.ttfb = parseInt(options["threshold-ttfb"]);
2836
+ if (options["threshold-tti"])
2837
+ thresholds.tti = parseInt(options["threshold-tti"]);
2838
+ if (options["threshold-tbt"])
2839
+ thresholds.tbt = parseInt(options["threshold-tbt"]);
2840
+ const regressionOptions = {
2841
+ headless,
2842
+ thresholds: Object.keys(thresholds).length > 0 ? thresholds : undefined,
2843
+ };
2844
+ try {
2845
+ const result = await (0, browser_js_1.detectPerformanceRegression)(url, baselineName, regressionOptions);
2846
+ // Print formatted report
2847
+ const report = (0, browser_js_1.formatPerformanceRegressionReport)(result);
2848
+ console.log("\n" + report);
2849
+ // Save JSON report if requested
2850
+ const fs = await import("fs");
2851
+ if (options.output) {
2852
+ fs.writeFileSync(options.output, JSON.stringify(result, null, 2));
2853
+ console.log(`\n📄 JSON report saved: ${options.output}`);
2854
+ }
2855
+ // Exit with error code if regressions found
2856
+ if (!result.passed) {
2857
+ process.exit(1);
2858
+ }
2859
+ }
2860
+ catch (error) {
2861
+ console.error(`\n❌ Error: ${error.message}`);
2862
+ process.exit(1);
2863
+ }
2864
+ break;
2865
+ }
2866
+ // =========================================================================
2867
+ // Test Coverage Map (Tier 6 - v6.5.0)
2868
+ // =========================================================================
2869
+ case "coverage": {
2870
+ const fs = await import("fs");
2871
+ const path = await import("path");
2872
+ const subcommand = args[0];
2873
+ // Simple glob function for test files
2874
+ function findTestFiles(pattern) {
2875
+ const files = [];
2876
+ const parts = pattern.split("/");
2877
+ const dir = parts.slice(0, -1).join("/") || ".";
2878
+ const filePattern = parts[parts.length - 1];
2879
+ const regex = new RegExp("^" + filePattern.replace(/\*/g, ".*").replace(/\?/g, ".") + "$");
2880
+ try {
2881
+ const dirFiles = fs.readdirSync(dir);
2882
+ for (const file of dirFiles) {
2883
+ if (regex.test(file)) {
2884
+ files.push(path.join(dir, file));
2885
+ }
2886
+ }
2887
+ }
2888
+ catch {
2889
+ // Directory doesn't exist
2890
+ }
2891
+ return files;
2892
+ }
2893
+ // Handle "coverage gaps" subcommand
2894
+ if (subcommand === "gaps") {
2895
+ const url = args[1];
2896
+ if (!url) {
2897
+ console.error("Usage: cbrowser coverage gaps <url> [--tests <glob>] [--sitemap <url>]");
2898
+ process.exit(1);
2899
+ }
2900
+ console.log(`\n🔍 Finding untested pages for: ${url}`);
2901
+ const testPattern = options.tests || "tests/*.txt";
2902
+ const testFiles = findTestFiles(testPattern);
2903
+ if (testFiles.length === 0) {
2904
+ console.error(`No test files found matching: ${testPattern}`);
2905
+ process.exit(1);
2906
+ }
2907
+ console.log(` Analyzing ${testFiles.length} test file(s)...`);
2908
+ const coverageOptions = {
2909
+ sitemapUrl: options.sitemap,
2910
+ maxPages: 50, // Quick mode
2911
+ minCoverage: 50,
2912
+ };
2913
+ const result = await (0, browser_js_1.generateCoverageMap)(url, testFiles, coverageOptions);
2914
+ // Show only gaps
2915
+ console.log(`\n🕳️ Coverage Gaps (${result.gaps.length} found):\n`);
2916
+ const priorityEmoji = { critical: "🚨", high: "🔴", medium: "🟡", low: "🟢" };
2917
+ for (const gap of result.gaps) {
2918
+ const emoji = priorityEmoji[gap.priority];
2919
+ console.log(` ${emoji} ${gap.page.path}`);
2920
+ console.log(` Priority: ${gap.priority} | Reason: ${gap.reason}`);
2921
+ }
2922
+ console.log(`\n📊 Coverage: ${result.analysis.coveragePercent}% (${result.analysis.testedPages}/${result.analysis.totalPages} pages)`);
2923
+ break;
2924
+ }
2925
+ // Main coverage command
2926
+ const url = subcommand;
2927
+ if (!url || url.startsWith("-")) {
2928
+ console.error("Usage: cbrowser coverage <url> [--tests <glob>] [--sitemap <url>] [--html] [--output <file>]");
2929
+ process.exit(1);
2930
+ }
2931
+ console.log(`\n📊 Generating test coverage map for: ${url}`);
2932
+ const testPattern = options.tests || "tests/*.txt";
2933
+ const testFiles = findTestFiles(testPattern);
2934
+ if (testFiles.length === 0) {
2935
+ console.error(`No test files found matching: ${testPattern}`);
2936
+ console.error("Use --tests <glob> to specify test files");
2937
+ process.exit(1);
2938
+ }
2939
+ console.log(` Found ${testFiles.length} test file(s)`);
2940
+ for (const f of testFiles.slice(0, 5)) {
2941
+ console.log(` - ${f}`);
2942
+ }
2943
+ if (testFiles.length > 5) {
2944
+ console.log(` ... and ${testFiles.length - 5} more`);
2945
+ }
2946
+ const coverageOptions = {
2947
+ sitemapUrl: options.sitemap,
2948
+ maxPages: options["max-pages"] ? parseInt(options["max-pages"]) : 100,
2949
+ includePattern: options.include,
2950
+ excludePattern: options.exclude,
2951
+ minCoverage: options["min-coverage"] ? parseInt(options["min-coverage"]) : 50,
2952
+ };
2953
+ if (coverageOptions.sitemapUrl) {
2954
+ console.log(` Using sitemap: ${coverageOptions.sitemapUrl}`);
2955
+ }
2956
+ else {
2957
+ console.log(` Crawling site (max ${coverageOptions.maxPages} pages)...`);
2958
+ }
2959
+ const result = await (0, browser_js_1.generateCoverageMap)(url, testFiles, coverageOptions);
2960
+ // Output format
2961
+ if (options.html) {
2962
+ const htmlReport = (0, browser_js_1.generateCoverageHtmlReport)(result);
2963
+ const outputPath = options.output || "coverage-report.html";
2964
+ fs.writeFileSync(outputPath, htmlReport);
2965
+ console.log(`\n✅ HTML report saved: ${outputPath}`);
2966
+ }
2967
+ else if (options.output && options.output.endsWith(".json")) {
2968
+ fs.writeFileSync(options.output, JSON.stringify(result, null, 2));
2969
+ console.log(`\n✅ JSON report saved: ${options.output}`);
2970
+ }
2971
+ else {
2972
+ // Print text report
2973
+ const report = (0, browser_js_1.formatCoverageReport)(result);
2974
+ console.log(report);
2975
+ if (options.output) {
2976
+ fs.writeFileSync(options.output, report);
2977
+ console.log(`\n📄 Report saved: ${options.output}`);
2978
+ }
2979
+ }
2980
+ // Exit with error if coverage too low
2981
+ if (result.analysis.coveragePercent < (coverageOptions.minCoverage || 50)) {
2982
+ console.log(`\n⚠️ Coverage (${result.analysis.coveragePercent}%) is below threshold (${coverageOptions.minCoverage}%)`);
2983
+ process.exit(1);
2984
+ }
2985
+ break;
2986
+ }
2554
2987
  default:
2555
2988
  console.error(`Unknown command: ${command}`);
2556
2989
  console.error("Run 'cbrowser help' for usage");