cbrowser 19.5.0 → 20.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/README.md +292 -232
- package/THIRD_PARTY_LICENSES.md +455 -7
- package/dist/analysis/accessibility-empathy.d.ts.map +1 -1
- package/dist/analysis/accessibility-empathy.js +2 -0
- package/dist/analysis/accessibility-empathy.js.map +1 -1
- package/dist/analysis/competitive-benchmark.d.ts.map +1 -1
- package/dist/analysis/competitive-benchmark.js +70 -558
- package/dist/analysis/competitive-benchmark.js.map +1 -1
- package/dist/analysis/natural-language.d.ts.map +1 -1
- package/dist/analysis/natural-language.js +43 -0
- package/dist/analysis/natural-language.js.map +1 -1
- package/dist/analysis/persona-comparison.d.ts +7 -0
- package/dist/analysis/persona-comparison.d.ts.map +1 -1
- package/dist/analysis/persona-comparison.js +8 -1
- package/dist/analysis/persona-comparison.js.map +1 -1
- package/dist/browser-options.d.ts +23 -0
- package/dist/browser-options.d.ts.map +1 -0
- package/dist/browser-options.js +24 -0
- package/dist/browser-options.js.map +1 -0
- package/dist/browser.d.ts +36 -14
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +172 -202
- package/dist/browser.js.map +1 -1
- package/dist/cli-args.d.ts +9 -1
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +35 -7
- package/dist/cli-args.js.map +1 -1
- package/dist/cli-browser-flags.d.ts +33 -0
- package/dist/cli-browser-flags.d.ts.map +1 -0
- package/dist/cli-browser-flags.js +89 -0
- package/dist/cli-browser-flags.js.map +1 -0
- package/dist/cli.js +1058 -478
- package/dist/cli.js.map +1 -1
- package/dist/cognitive/index.d.ts +5 -13
- package/dist/cognitive/index.d.ts.map +1 -1
- package/dist/cognitive/index.js +44 -15
- package/dist/cognitive/index.js.map +1 -1
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +16 -6
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +9 -5
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +102 -36
- package/dist/daemon.js.map +1 -1
- package/dist/device-names.d.ts +10 -0
- package/dist/device-names.d.ts.map +1 -0
- package/dist/device-names.js +16 -0
- package/dist/device-names.js.map +1 -0
- package/dist/device-presets-devtools.d.ts +3 -0
- package/dist/device-presets-devtools.d.ts.map +1 -0
- package/dist/device-presets-devtools.js +823 -0
- package/dist/device-presets-devtools.js.map +1 -0
- package/dist/devices.d.ts +35 -0
- package/dist/devices.d.ts.map +1 -0
- package/dist/devices.js +214 -0
- package/dist/devices.js.map +1 -0
- package/dist/eval-script.d.ts +29 -0
- package/dist/eval-script.d.ts.map +1 -0
- package/dist/eval-script.js +46 -0
- package/dist/eval-script.js.map +1 -0
- package/dist/help-client.d.ts +48 -0
- package/dist/help-client.d.ts.map +1 -0
- package/dist/help-client.js +96 -0
- package/dist/help-client.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +165 -2281
- package/dist/mcp-server.js.map +1 -1
- package/dist/mcp-tools/base/advanced-interaction-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/advanced-interaction-tools.js +21 -21
- package/dist/mcp-tools/base/advanced-interaction-tools.js.map +1 -1
- package/dist/mcp-tools/base/assertion-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/assertion-tools.js +6 -3
- package/dist/mcp-tools/base/assertion-tools.js.map +1 -1
- package/dist/mcp-tools/base/audit-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/audit-tools.js +15 -4
- package/dist/mcp-tools/base/audit-tools.js.map +1 -1
- package/dist/mcp-tools/base/browser-state-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/browser-state-tools.js +33 -27
- package/dist/mcp-tools/base/browser-state-tools.js.map +1 -1
- package/dist/mcp-tools/base/bug-analysis-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/bug-analysis-tools.js +9 -2
- package/dist/mcp-tools/base/bug-analysis-tools.js.map +1 -1
- package/dist/mcp-tools/base/capture-tools.d.ts +60 -4
- package/dist/mcp-tools/base/capture-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/capture-tools.js +118 -14
- package/dist/mcp-tools/base/capture-tools.js.map +1 -1
- package/dist/mcp-tools/base/index.d.ts +6 -5
- package/dist/mcp-tools/base/index.d.ts.map +1 -1
- package/dist/mcp-tools/base/index.js +7 -6
- package/dist/mcp-tools/base/index.js.map +1 -1
- package/dist/mcp-tools/base/interaction-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/interaction-tools.js +20 -6
- package/dist/mcp-tools/base/interaction-tools.js.map +1 -1
- package/dist/mcp-tools/base/navigation-tools.d.ts +1 -1
- package/dist/mcp-tools/base/navigation-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/navigation-tools.js +83 -3
- package/dist/mcp-tools/base/navigation-tools.js.map +1 -1
- package/dist/mcp-tools/base/site-knowledge-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/site-knowledge-tools.js +11 -23
- package/dist/mcp-tools/base/site-knowledge-tools.js.map +1 -1
- package/dist/mcp-tools/base/visual-testing-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/visual-testing-tools.js +13 -3
- package/dist/mcp-tools/base/visual-testing-tools.js.map +1 -1
- package/dist/mcp-tools/enterprise-stubs.d.ts +2 -2
- package/dist/mcp-tools/enterprise-stubs.d.ts.map +1 -1
- package/dist/mcp-tools/enterprise-stubs.js +29 -16
- package/dist/mcp-tools/enterprise-stubs.js.map +1 -1
- package/dist/mcp-tools/index.d.ts +19 -14
- package/dist/mcp-tools/index.d.ts.map +1 -1
- package/dist/mcp-tools/index.js +2 -2
- package/dist/mcp-tools/index.js.map +1 -1
- package/dist/mcp-tools/red-zone-refusal.d.ts +33 -0
- package/dist/mcp-tools/red-zone-refusal.d.ts.map +1 -0
- package/dist/mcp-tools/red-zone-refusal.js +43 -0
- package/dist/mcp-tools/red-zone-refusal.js.map +1 -0
- package/dist/mcp-tools/tier-gate.js +1 -1
- package/dist/mcp-tools/tier-gate.js.map +1 -1
- package/dist/mcp-tools/tool-categories.d.ts.map +1 -1
- package/dist/mcp-tools/tool-categories.js +2 -0
- package/dist/mcp-tools/tool-categories.js.map +1 -1
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +18 -2
- package/dist/output.js.map +1 -1
- package/dist/performance/metrics.d.ts +8 -3
- package/dist/performance/metrics.d.ts.map +1 -1
- package/dist/performance/metrics.js +16 -5
- package/dist/performance/metrics.js.map +1 -1
- package/dist/recording/contact-sheet.js.map +1 -1
- package/dist/security/tool-permissions.d.ts.map +1 -1
- package/dist/security/tool-permissions.js +1 -0
- package/dist/security/tool-permissions.js.map +1 -1
- package/dist/testing/coverage.d.ts +2 -2
- package/dist/testing/coverage.d.ts.map +1 -1
- package/dist/testing/coverage.js +6 -2
- package/dist/testing/coverage.js.map +1 -1
- package/dist/testing/flaky-detection.d.ts +2 -2
- package/dist/testing/flaky-detection.d.ts.map +1 -1
- package/dist/testing/flaky-detection.js +11 -3
- package/dist/testing/flaky-detection.js.map +1 -1
- package/dist/testing/nl-test-suite.d.ts +20 -0
- package/dist/testing/nl-test-suite.d.ts.map +1 -1
- package/dist/testing/nl-test-suite.js +7 -1
- package/dist/testing/nl-test-suite.js.map +1 -1
- package/dist/testing/suite-results.d.ts +27 -0
- package/dist/testing/suite-results.d.ts.map +1 -0
- package/dist/testing/suite-results.js +54 -0
- package/dist/testing/suite-results.js.map +1 -0
- package/dist/testing/test-repair.d.ts +2 -2
- package/dist/testing/test-repair.d.ts.map +1 -1
- package/dist/testing/test-repair.js +6 -2
- package/dist/testing/test-repair.js.map +1 -1
- package/dist/types.d.ts +61 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +17 -103
- package/dist/types.js.map +1 -1
- package/dist/visual/ab-comparison.d.ts.map +1 -1
- package/dist/visual/ab-comparison.js +7 -2
- package/dist/visual/ab-comparison.js.map +1 -1
- package/dist/visual/cross-browser.d.ts +7 -2
- package/dist/visual/cross-browser.d.ts.map +1 -1
- package/dist/visual/cross-browser.js +33 -5
- package/dist/visual/cross-browser.js.map +1 -1
- package/dist/visual/distance-metrics.d.ts +7 -0
- package/dist/visual/distance-metrics.d.ts.map +1 -1
- package/dist/visual/distance-metrics.js.map +1 -1
- package/dist/visual/heatmap-overlay.js.map +1 -1
- package/dist/visual/llm-relevance.d.ts.map +1 -1
- package/dist/visual/llm-relevance.js.map +1 -1
- package/dist/visual/regression.d.ts +17 -7
- package/dist/visual/regression.d.ts.map +1 -1
- package/dist/visual/regression.js +77 -38
- package/dist/visual/regression.js.map +1 -1
- package/dist/visual/responsive.d.ts +22 -0
- package/dist/visual/responsive.d.ts.map +1 -1
- package/dist/visual/responsive.js +44 -7
- package/dist/visual/responsive.js.map +1 -1
- package/dist/visual/visual-overlays.js.map +1 -1
- package/dist/webhooks.d.ts +67 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +128 -0
- package/dist/webhooks.js.map +1 -0
- package/package.json +23 -24
package/dist/cli.js
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { CBrowser } from "./browser.js";
|
|
13
13
|
import { parseArgs } from "./cli-args.js";
|
|
14
|
+
import { browserFlagsOrExit, refuseBrowserFlags } from "./cli-browser-flags.js";
|
|
15
|
+
import { browserLaunchConfig } from "./browser-options.js";
|
|
16
|
+
import { SENT_WEBHOOK_EVENTS, WEBHOOK_FORMATS, WEBHOOK_TIMEOUT_MS, loadWebhooks, sendWebhookEvent, webhookUrlProblem, webhooksPath } from "./webhooks.js";
|
|
17
|
+
import { loadSuiteForExport, resultsName, saveSuiteResult } from "./testing/suite-results.js";
|
|
14
18
|
// Analysis module imports
|
|
15
19
|
import { executeNaturalLanguage, executeNaturalLanguageScript, huntBugs, runChaosTest, comparePersonas, formatComparisonReport, findElementByIntent, runAgentReadyAudit, formatAgentReadyReport, generateAgentReadyHtmlReport, runCompetitiveBenchmark, formatCompetitiveBenchmarkReport, generateCompetitiveBenchmarkHtmlReport, runEmpathyAudit, formatEmpathyAuditReport, generateEmpathyAuditHtmlReport, runWebMCPReadyAudit, generateWebMCPReadyHtmlReport, runAIReadinessBenchmark } from "./analysis/index.js";
|
|
16
20
|
// Testing module imports
|
|
@@ -18,9 +22,11 @@ import { parseNLInstruction, parseNLTestSuite, runNLTestSuite, formatNLTestRepor
|
|
|
18
22
|
// Performance module imports
|
|
19
23
|
import { capturePerformanceBaseline, listPerformanceBaselines, loadPerformanceBaseline, deletePerformanceBaseline, detectPerformanceRegression, formatPerformanceRegressionReport } from "./performance/index.js";
|
|
20
24
|
// Visual module imports
|
|
21
|
-
import { captureVisualBaseline, listVisualBaselines, getVisualBaseline, deleteVisualBaseline, runVisualRegression, runVisualRegressionSuite, formatVisualRegressionReport, generateVisualRegressionHtmlReport, runCrossBrowserTest, runCrossBrowserSuite, formatCrossBrowserReport, generateCrossBrowserHtmlReport, runResponsiveTest, runResponsiveSuite, formatResponsiveReport, generateResponsiveHtmlReport, listViewportPresets, runABComparison, runABSuite, formatABReport, generateABHtmlReport, crossBrowserDiff } from "./visual/index.js";
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
25
|
+
import { captureVisualBaseline, listVisualBaselines, getVisualBaseline, deleteVisualBaseline, runVisualRegression, runVisualRegressionSuite, formatVisualRegressionReport, generateVisualRegressionHtmlReport, runCrossBrowserTest, runCrossBrowserSuite, formatCrossBrowserReport, generateCrossBrowserHtmlReport, runResponsiveTest, runResponsiveSuite, formatResponsiveReport, generateResponsiveHtmlReport, listViewportPresets, formatViewportPresetList, runABComparison, runABSuite, formatABReport, generateABHtmlReport, crossBrowserDiff } from "./visual/index.js";
|
|
26
|
+
import { VIEWPORT_PRESETS } from "./types.js";
|
|
27
|
+
import { BUILTIN_PERSONAS, loadCustomPersonas, saveCustomPersona, deleteCustomPersona, isBuiltinPersona, generatePersonaFromDescription, listAccessibilityPersonas, PERSONA_ALIASES, } from "./personas.js";
|
|
28
|
+
import { LOCATION_PRESETS } from "./types.js";
|
|
29
|
+
import { CBROWSER_DEVICE_KEYS, DEVICE_PRESETS, resolveDevice } from "./devices.js";
|
|
24
30
|
import { startMcpServer } from "./mcp-server.js";
|
|
25
31
|
import { startRemoteMcpServer } from "./mcp-server-remote.js";
|
|
26
32
|
import { startDaemon, stopDaemon, getDaemonStatus, isDaemonRunning, sendToDaemon, runDaemonServer } from "./daemon.js";
|
|
@@ -30,6 +36,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync,
|
|
|
30
36
|
import { resolveCookieDomain, hostFromUrl } from "./utils.js";
|
|
31
37
|
import { dirname, join, resolve } from "node:path";
|
|
32
38
|
import { guardConsole, guardTerminalOutput } from "./terminal-safe.js";
|
|
39
|
+
import { evaluateScript } from "./eval-script.js";
|
|
33
40
|
import { VideoCaptureSession } from "./recording/engine.js";
|
|
34
41
|
import { changeSignal, parseManifest } from "./recording/types.js";
|
|
35
42
|
import { printEnterpriseStatus } from "./stealth/index.js";
|
|
@@ -81,7 +88,7 @@ function helpText() {
|
|
|
81
88
|
|
|
82
89
|
NAVIGATION
|
|
83
90
|
navigate <url> Navigate and take screenshot
|
|
84
|
-
screenshot [path]
|
|
91
|
+
screenshot [url] [path] Take a screenshot (of <url> when given, else the current page)
|
|
85
92
|
--no-restore Don't reload saved URL (capture current state after interactions)
|
|
86
93
|
|
|
87
94
|
INTERACTION
|
|
@@ -116,6 +123,7 @@ SCRIPTING
|
|
|
116
123
|
--raw Print the raw value, unquoted
|
|
117
124
|
--wait-for <selector> Wait for the selector before evaluating
|
|
118
125
|
--expect-truthy Exit 1 when the result is falsy (CI assertion)
|
|
126
|
+
--timeout <ms> How long --wait-for waits: ms, or 5s / 2m (default: 30000)
|
|
119
127
|
Examples:
|
|
120
128
|
cbrowser evaluate "document.title" --force
|
|
121
129
|
cbrowser evaluate --file check.js --arg '"#main"' --expect-truthy --force
|
|
@@ -128,6 +136,7 @@ KEYBOARD
|
|
|
128
136
|
--hold <mod> Hold a modifier across the whole sequence
|
|
129
137
|
--repeat <n> Repeat the whole sequence n times
|
|
130
138
|
--text Treat every token as literal text (for "+" in text)
|
|
139
|
+
--url <url> Navigate to URL first
|
|
131
140
|
A token with modifiers is a chord (Control+A); a known key name is that key
|
|
132
141
|
(Enter, Tab, ArrowLeft); anything else is typed as literal text. Unknown
|
|
133
142
|
key names are rejected, not typed. For a single key use 'press', for a
|
|
@@ -137,12 +146,12 @@ KEYBOARD
|
|
|
137
146
|
cbrowser keyboard "ArrowRight" --repeat 5 --hold Shift
|
|
138
147
|
cbrowser keyboard "Tab" "Tab" "Enter" --selector "#email" --delay 120
|
|
139
148
|
|
|
140
|
-
SITE EXPLORATION (
|
|
141
|
-
explore <persona>
|
|
149
|
+
SITE EXPLORATION (API-powered)
|
|
150
|
+
explore <persona> Short cognitive journey (needs an Anthropic API key; costs tokens)
|
|
142
151
|
--start <url> Starting URL (required)
|
|
143
152
|
--goal <goal> What to accomplish
|
|
144
153
|
--record-video Record exploration as video
|
|
145
|
-
Note:
|
|
154
|
+
Note: the keyless heuristic explorer was removed in 20.0.0; its success verdicts were not accurate.
|
|
146
155
|
|
|
147
156
|
COGNITIVE JOURNEY (API-powered, realistic user simulation)
|
|
148
157
|
cognitive-journey Simulate a real user with emotions, patience, and abandonment
|
|
@@ -158,10 +167,13 @@ COGNITIVE JOURNEY (API-powered, realistic user simulation)
|
|
|
158
167
|
--verbose Show step-by-step narration and internal monologue
|
|
159
168
|
--no-vision Disable vision mode (text-only, less accurate but faster)
|
|
160
169
|
--output <file> Save JSON report to file
|
|
161
|
-
--
|
|
170
|
+
--trace Record the journey to ~/.cbrowser/traces/<id>.jsonl
|
|
171
|
+
--replay-trace <file> Replay a recorded journey: no model calls, no API key
|
|
172
|
+
--persistent Run in the shared browser profile (default: a fresh one,
|
|
173
|
+
so the persona starts as a new visitor)
|
|
162
174
|
--timezone <tz> Override persona timezone (e.g., "America/New_York", "Europe/London")
|
|
163
175
|
--locale <loc> Override persona locale (e.g., "en-US", "de-DE")
|
|
164
|
-
--
|
|
176
|
+
--geo <preset|lat,lon> Override persona location (also --geolocation <lat,lng>)
|
|
165
177
|
|
|
166
178
|
Why use this over 'explore'?
|
|
167
179
|
• Tracks cognitive state: patience, frustration, confusion
|
|
@@ -177,12 +189,14 @@ COGNITIVE JOURNEY (API-powered, realistic user simulation)
|
|
|
177
189
|
|
|
178
190
|
Note: Requires Anthropic API key. Set with: cbrowser config set-api-key <key>
|
|
179
191
|
|
|
180
|
-
MULTI-PERSONA COMPARISON
|
|
192
|
+
MULTI-PERSONA COMPARISON
|
|
181
193
|
compare-personas Compare multiple personas on the same journey
|
|
182
194
|
--start <url> Starting URL (required)
|
|
183
195
|
--goal <goal> What to accomplish (required)
|
|
184
196
|
--personas <list> Comma-separated persona names
|
|
185
197
|
--concurrency <n> Max parallel browsers (default: 3)
|
|
198
|
+
--device <name> Device every journey emulates
|
|
199
|
+
--viewport <WxH> Page size for every journey
|
|
186
200
|
--output <file> Save JSON report to file
|
|
187
201
|
--html Generate HTML report
|
|
188
202
|
Examples:
|
|
@@ -190,8 +204,10 @@ MULTI-PERSONA COMPARISON (v6.0.0)
|
|
|
190
204
|
--goal "Complete checkout" \\
|
|
191
205
|
--personas power-user,first-timer,elderly-user,mobile-user
|
|
192
206
|
|
|
193
|
-
NATURAL LANGUAGE TEST SUITES
|
|
207
|
+
NATURAL LANGUAGE TEST SUITES
|
|
194
208
|
test-suite <file.txt> Run tests written in plain English
|
|
209
|
+
Saves each run for 'export'; sends test.pass or
|
|
210
|
+
test.fail to webhooks ('webhook add')
|
|
195
211
|
--continue-on-failure Keep running after a test fails
|
|
196
212
|
--screenshot-on-failure Take screenshots on failure (default: true)
|
|
197
213
|
--output <file> Save JSON report to file
|
|
@@ -226,29 +242,31 @@ NATURAL LANGUAGE TEST SUITES (v6.1.0)
|
|
|
226
242
|
click search button
|
|
227
243
|
verify page contains "results"
|
|
228
244
|
|
|
229
|
-
AI TEST REPAIR
|
|
245
|
+
AI TEST REPAIR
|
|
230
246
|
repair-tests <file.txt> Analyze failing tests and suggest/apply repairs
|
|
231
247
|
--auto-apply Automatically apply the best repair
|
|
232
248
|
--verify Re-run tests after repair to verify they pass
|
|
233
249
|
--output <file> Save repaired tests to new file
|
|
234
|
-
--json <file>
|
|
250
|
+
--json-out <file> Save repair report as JSON (--json prints it)
|
|
251
|
+
--device, --viewport, --browser, --no-headless The browser the tests run in
|
|
235
252
|
Examples:
|
|
236
253
|
cbrowser repair-tests broken-test.txt
|
|
237
254
|
cbrowser repair-tests tests.txt --auto-apply --verify
|
|
238
255
|
cbrowser repair-tests tests.txt --auto-apply --output fixed-tests.txt
|
|
239
256
|
|
|
240
|
-
FLAKY TEST DETECTION
|
|
257
|
+
FLAKY TEST DETECTION
|
|
241
258
|
flaky-check <file.txt> Run tests multiple times to detect flaky tests
|
|
242
259
|
--runs <n> Number of runs per test (default: 5)
|
|
243
260
|
--threshold <n> Flakiness % threshold to flag (default: 20)
|
|
244
261
|
--delay <ms> Delay between runs (default: 500)
|
|
245
262
|
--output <file> Save JSON report to file
|
|
263
|
+
--device, --viewport, --browser, --no-headless The browser every run uses
|
|
246
264
|
Examples:
|
|
247
265
|
cbrowser flaky-check tests.txt
|
|
248
266
|
cbrowser flaky-check tests.txt --runs 10
|
|
249
267
|
cbrowser flaky-check tests.txt --runs 5 --threshold 30 --output flaky-report.json
|
|
250
268
|
|
|
251
|
-
TEST COVERAGE MAP
|
|
269
|
+
TEST COVERAGE MAP
|
|
252
270
|
coverage <url> Generate test coverage map for a site
|
|
253
271
|
--tests <glob> Test files to analyze (default: tests/*.txt)
|
|
254
272
|
--sitemap <url> Use sitemap.xml instead of crawling
|
|
@@ -257,7 +275,8 @@ TEST COVERAGE MAP (v6.5.0)
|
|
|
257
275
|
--exclude <pattern> Exclude paths matching pattern
|
|
258
276
|
--min-coverage <n> Min coverage % to not flag (default: 50)
|
|
259
277
|
--html Generate HTML report
|
|
260
|
-
--output <file> Save
|
|
278
|
+
--output <file> Save the report (JSON if the name ends in .json, else text)
|
|
279
|
+
--device, --viewport, --browser, --no-headless The crawl's browser (not with --sitemap)
|
|
261
280
|
Examples:
|
|
262
281
|
cbrowser coverage "https://example.com" --tests "tests/*.txt"
|
|
263
282
|
cbrowser coverage "https://example.com" --sitemap "https://example.com/sitemap.xml"
|
|
@@ -280,6 +299,8 @@ PERSONAS
|
|
|
280
299
|
persona delete <name> Delete a custom persona
|
|
281
300
|
persona export <name> Export persona to JSON file
|
|
282
301
|
persona import <file> Import persona from JSON file
|
|
302
|
+
persona cleanup Remove custom personas that fail validation
|
|
303
|
+
--dry-run List them without removing anything
|
|
283
304
|
|
|
284
305
|
SESSION MANAGEMENT
|
|
285
306
|
session save <name> Save browser session (cookies, storage, URL)
|
|
@@ -287,6 +308,12 @@ SESSION MANAGEMENT
|
|
|
287
308
|
session load <name> Load a saved session
|
|
288
309
|
session list List all saved sessions
|
|
289
310
|
session delete <name> Delete a saved session
|
|
311
|
+
session show <name> Show a saved session's cookies, storage and URL
|
|
312
|
+
session cleanup Delete saved sessions older than N days
|
|
313
|
+
--older-than <days> Age in days (default: 30)
|
|
314
|
+
session export <name> Export a session to a JSON file
|
|
315
|
+
--output <file> File to write (default: <name>.json)
|
|
316
|
+
session import <file> Import a session from a JSON file
|
|
290
317
|
|
|
291
318
|
COOKIE MANAGEMENT
|
|
292
319
|
cookie list List all cookies for current page
|
|
@@ -299,7 +326,7 @@ COOKIE MANAGEMENT
|
|
|
299
326
|
cookie clear Clear all cookies
|
|
300
327
|
|
|
301
328
|
DEVICE EMULATION
|
|
302
|
-
device list
|
|
329
|
+
device list [filter] List device presets, one per line (filter by name)
|
|
303
330
|
device set <name> Set device emulation for session
|
|
304
331
|
Example: cbrowser device set iphone-15
|
|
305
332
|
|
|
@@ -317,10 +344,12 @@ PERFORMANCE
|
|
|
317
344
|
--budget-fcp <ms> FCP budget (default: 1800)
|
|
318
345
|
--budget-cls <score> CLS budget (default: 0.1)
|
|
319
346
|
|
|
320
|
-
PERFORMANCE REGRESSION
|
|
347
|
+
PERFORMANCE REGRESSION
|
|
321
348
|
perf-baseline save <url> Capture and save performance baseline
|
|
322
349
|
--name <name> Human-readable name for baseline
|
|
323
350
|
--runs <n> Number of runs to average (default: 3)
|
|
351
|
+
--device <name> Device to measure as; perf-regression renders the same
|
|
352
|
+
--viewport <WxH> Page size; perf-regression renders the same
|
|
324
353
|
Examples:
|
|
325
354
|
cbrowser perf-baseline save "https://example.com" --name "homepage"
|
|
326
355
|
cbrowser perf-baseline save "https://example.com/checkout" --runs 5
|
|
@@ -334,6 +363,9 @@ PERFORMANCE REGRESSION (v6.4.0)
|
|
|
334
363
|
--threshold-lcp <n> Max LCP increase % (default: 20)
|
|
335
364
|
--threshold-cls <n> Max CLS increase (default: 0.1)
|
|
336
365
|
--threshold-fcp <n> Max FCP increase % (default: 20)
|
|
366
|
+
--threshold-ttfb <n> Max TTFB increase % (default: 30)
|
|
367
|
+
--threshold-tti <n> Max TTI increase %
|
|
368
|
+
--threshold-tbt <n> Max TBT increase %
|
|
337
369
|
--output <file> Save JSON report to file
|
|
338
370
|
Examples:
|
|
339
371
|
cbrowser perf-regression "https://example.com" homepage
|
|
@@ -344,14 +376,17 @@ NETWORK / HAR (need the daemon: each command is its own process)
|
|
|
344
376
|
daemon start Start the daemon first; it holds the browser between commands
|
|
345
377
|
har start Start recording HAR in the daemon's browser
|
|
346
378
|
har stop [output] Stop and save HAR file
|
|
347
|
-
network list List network requests the daemon's browser captured
|
|
379
|
+
network list List network requests the daemon's browser captured, newest first
|
|
380
|
+
--filter <text> Only requests whose URL contains <text>, or of that type or method
|
|
381
|
+
--all List every request (default: the newest 20)
|
|
382
|
+
--json Print the list as JSON
|
|
348
383
|
network clear Clear that list
|
|
349
384
|
console List the daemon tab's console messages (errors, warnings, logs)
|
|
350
385
|
--filter <type> all | error | warning | log | info (default all)
|
|
351
386
|
--clear Empty the console buffer after reading
|
|
352
387
|
--json Print the raw messages as JSON
|
|
353
388
|
|
|
354
|
-
VISUAL REGRESSION
|
|
389
|
+
VISUAL REGRESSION
|
|
355
390
|
visual save <name> Save baseline screenshot
|
|
356
391
|
--url <url> Navigate to URL first
|
|
357
392
|
visual compare <name> Compare current page against baseline
|
|
@@ -359,19 +394,22 @@ VISUAL REGRESSION (v2.5.0)
|
|
|
359
394
|
visual list List all saved baselines
|
|
360
395
|
visual delete <name> Delete a baseline
|
|
361
396
|
|
|
362
|
-
AI VISUAL REGRESSION
|
|
397
|
+
AI VISUAL REGRESSION
|
|
363
398
|
ai-visual capture <url> Capture AI-analyzed visual baseline
|
|
364
399
|
--name <name> Baseline name (required)
|
|
365
400
|
--selector <sel> Capture specific element instead of viewport
|
|
366
401
|
--device <device> Device emulation (iphone-15, pixel-7, etc.)
|
|
402
|
+
--viewport <WxH> Page size (default: 1920x1080)
|
|
367
403
|
--width <n> Viewport width (default: 1920)
|
|
368
404
|
--height <n> Viewport height (default: 1080)
|
|
405
|
+
--browser <engine> chromium, firefox or webkit; the baseline keeps it
|
|
369
406
|
--wait <ms|selector> Wait before capture (ms or CSS selector)
|
|
370
407
|
Examples:
|
|
371
408
|
cbrowser ai-visual capture "https://example.com" --name homepage
|
|
372
409
|
cbrowser ai-visual capture "https://example.com/dashboard" --name dashboard --device iphone-15
|
|
373
410
|
|
|
374
411
|
ai-visual test <url> <baseline> Test current page against baseline using AI
|
|
412
|
+
Renders with the baseline's engine, device and size
|
|
375
413
|
--threshold <n> Similarity threshold 0-1 (default: 0.9)
|
|
376
414
|
--sensitivity <level> Detection sensitivity: low, medium, high (default: medium)
|
|
377
415
|
--html Generate HTML report
|
|
@@ -389,41 +427,13 @@ AI VISUAL REGRESSION (v7.0.0)
|
|
|
389
427
|
ai-visual show <name> Show baseline details
|
|
390
428
|
ai-visual delete <name> Delete a baseline
|
|
391
429
|
|
|
392
|
-
|
|
393
|
-
smart-baseline capture <url> Capture consensus baseline from multiple screenshots
|
|
394
|
-
--name <name> Baseline name (required)
|
|
395
|
-
--captures <n> Number of screenshots to take (default: 5)
|
|
396
|
-
--delay <ms> Delay between captures (default: 1500)
|
|
397
|
-
--selector <sel> Capture specific element
|
|
398
|
-
--device <device> Device emulation
|
|
399
|
-
--outlier-threshold <n> Outlier rejection threshold in stddev (default: 2.0)
|
|
400
|
-
Examples:
|
|
401
|
-
cbrowser smart-baseline capture "https://example.com" --name homepage
|
|
402
|
-
cbrowser smart-baseline capture "https://example.com" --name hero --captures 10
|
|
403
|
-
|
|
404
|
-
smart-baseline test <url> <name> Test against smart baseline
|
|
405
|
-
--threshold <n> Override adaptive threshold (0-1)
|
|
406
|
-
--transport-map Also generate visual transport map
|
|
407
|
-
Examples:
|
|
408
|
-
cbrowser smart-baseline test "https://staging.example.com" homepage
|
|
409
|
-
cbrowser smart-baseline test "https://staging.example.com" homepage --transport-map
|
|
410
|
-
|
|
411
|
-
smart-baseline list List all smart baselines
|
|
412
|
-
smart-baseline delete <name> Delete a smart baseline
|
|
413
|
-
|
|
414
|
-
transport-map <baseline> <current> Generate visual transport map between two screenshots
|
|
415
|
-
--cell-size <n> Grid cell size in pixels (default: 32)
|
|
416
|
-
--output <file.svg> Save SVG to file
|
|
417
|
-
--hotspots <n> Number of hotspots to identify (default: 5)
|
|
418
|
-
Examples:
|
|
419
|
-
cbrowser transport-map baseline.png current.png
|
|
420
|
-
cbrowser transport-map baseline.png current.png --cell-size 16 --output changes.svg
|
|
421
|
-
|
|
422
|
-
CROSS-BROWSER VISUAL TESTING (v7.3.0)
|
|
430
|
+
CROSS-BROWSER VISUAL TESTING
|
|
423
431
|
cross-browser <url> Compare visual rendering across browsers
|
|
424
432
|
--browsers <list> Browsers to test: chromium,firefox,webkit (default: all)
|
|
433
|
+
--viewport <WxH> Page size (default: 1920x1080)
|
|
425
434
|
--width <n> Viewport width (default: 1920)
|
|
426
435
|
--height <n> Viewport height (default: 1080)
|
|
436
|
+
--device <name> Device preset (firefox cannot emulate a mobile one)
|
|
427
437
|
--wait <ms> Wait before screenshot (ms)
|
|
428
438
|
--wait-for <selector> Wait for selector before screenshot
|
|
429
439
|
--sensitivity <level> Comparison sensitivity: low, medium, high
|
|
@@ -445,9 +455,11 @@ CROSS-BROWSER VISUAL TESTING (v7.3.0)
|
|
|
445
455
|
"options": { "browsers": ["chromium", "firefox"] }
|
|
446
456
|
}
|
|
447
457
|
|
|
448
|
-
RESPONSIVE VISUAL TESTING
|
|
458
|
+
RESPONSIVE VISUAL TESTING
|
|
449
459
|
responsive <url> Test visual rendering across viewport sizes
|
|
450
460
|
--viewports <list> Viewports to test (default: mobile,tablet,desktop)
|
|
461
|
+
--browser <engine> chromium, firefox or webkit (firefox cannot emulate
|
|
462
|
+
the mobile and tablet presets)
|
|
451
463
|
--wait <ms> Wait before screenshot (ms)
|
|
452
464
|
--wait-for <selector> Wait for selector before screenshot
|
|
453
465
|
--sensitivity <level> Comparison sensitivity: low, medium, high
|
|
@@ -471,18 +483,19 @@ RESPONSIVE VISUAL TESTING (v7.3.0)
|
|
|
471
483
|
"options": { "viewports": ["mobile", "tablet", "desktop"] }
|
|
472
484
|
}
|
|
473
485
|
|
|
474
|
-
Available viewport presets:
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
desktop-sm (1280x800) desktop (1440x900) desktop-lg (1920x1080)
|
|
478
|
-
desktop-xl (2560x1440)
|
|
486
|
+
Available viewport presets (mobile and tablet emulate their device: touch,
|
|
487
|
+
user agent, pixel ratio):
|
|
488
|
+
${formatViewportPresetList(" ")}
|
|
479
489
|
|
|
480
|
-
A/B VISUAL COMPARISON
|
|
490
|
+
A/B VISUAL COMPARISON
|
|
481
491
|
ab <urlA> <urlB> Compare two URLs visually
|
|
482
492
|
--label-a <name> Label for URL A (default: "Version A")
|
|
483
493
|
--label-b <name> Label for URL B (default: "Version B")
|
|
494
|
+
--viewport <WxH> Page size (default: 1920x1080)
|
|
484
495
|
--width <n> Viewport width (default: 1920)
|
|
485
496
|
--height <n> Viewport height (default: 1080)
|
|
497
|
+
--device <name> Device preset
|
|
498
|
+
--browser <engine> chromium, firefox or webkit
|
|
486
499
|
--wait <ms> Wait before screenshot (ms)
|
|
487
500
|
--wait-for <selector> Wait for selector before screenshot
|
|
488
501
|
--sensitivity <level> Comparison sensitivity: low, medium, high
|
|
@@ -512,6 +525,9 @@ AI FRIENDLINESS - Audit sites for AI agent compatibility
|
|
|
512
525
|
|
|
513
526
|
agent-ready-audit <url> Audit site for AI-agent friendliness (A-F grade)
|
|
514
527
|
--headless Run in headless mode (default: true)
|
|
528
|
+
--device <name> Device to audit on, e.g. iphone-16
|
|
529
|
+
--geo-region <region> Run through a configured regional proxy (exits 1 if not configured)
|
|
530
|
+
--lightpanda Use Lightpanda instead of Chromium (see LIGHTPANDA)
|
|
515
531
|
--output <file> Save JSON report to file
|
|
516
532
|
--html Generate HTML report
|
|
517
533
|
Grades sites on: Findability (35%), Stability (30%),
|
|
@@ -522,6 +538,7 @@ AI FRIENDLINESS - Audit sites for AI agent compatibility
|
|
|
522
538
|
|
|
523
539
|
ai-benchmark Compare AI-friendliness across multiple sites
|
|
524
540
|
--urls <list> Comma-separated URLs to benchmark (required)
|
|
541
|
+
--concurrency <n> Sites audited at once (default: 3)
|
|
525
542
|
--output <file> Save JSON report to file
|
|
526
543
|
--html Generate HTML report
|
|
527
544
|
Runs agent-ready-audit on each URL and produces ranked comparison.
|
|
@@ -530,14 +547,17 @@ AI FRIENDLINESS - Audit sites for AI agent compatibility
|
|
|
530
547
|
cbrowser ai-benchmark --urls "https://amazon.com,https://ebay.com" --html
|
|
531
548
|
|
|
532
549
|
empathy-audit <url> Simulate disability experience on site
|
|
533
|
-
--goal <goal> Task goal (
|
|
534
|
-
--personas <list> Disability personas (comma-separated)
|
|
550
|
+
--goal <goal> Task goal (optional; without one the page is checked)
|
|
551
|
+
--personas <list> Disability personas (comma-separated; also --disabilities)
|
|
552
|
+
--device <name> Device to audit on, e.g. iphone-16
|
|
553
|
+
--scope <scope> viewport (default) or full_page
|
|
535
554
|
--wcag-level <level> WCAG level: A, AA, AAA (default: AA)
|
|
555
|
+
--max-steps <n> Steps per persona when a goal is given
|
|
556
|
+
--max-time <s> Seconds per persona when a goal is given
|
|
536
557
|
--output <file> Save JSON report to file
|
|
537
558
|
--html Generate HTML report
|
|
538
559
|
Available personas:
|
|
539
|
-
|
|
540
|
-
dyslexic-user, deaf-user, elderly-low-vision, color-blind-deuteranopia
|
|
560
|
+
${listAccessibilityPersonas().join(", ")}
|
|
541
561
|
Examples:
|
|
542
562
|
cbrowser empathy-audit "https://example.com" \\
|
|
543
563
|
--goal "complete checkout" \\
|
|
@@ -555,13 +575,13 @@ AI FRIENDLINESS - Audit sites for AI agent compatibility
|
|
|
555
575
|
cbrowser webmcp-ready "https://demo.cbrowser.ai/mcp"
|
|
556
576
|
cbrowser webmcp-ready "https://your-server.com/mcp" --api-key KEY --html
|
|
557
577
|
|
|
558
|
-
hunt-bugs <url> Automatically find UX bugs on a page
|
|
578
|
+
hunt-bugs <url> Automatically find UX bugs on a page (alias: hunt)
|
|
559
579
|
--max-pages <n> Max pages to crawl (default: 10)
|
|
560
580
|
--timeout <ms> Timeout in ms (default: 60000)
|
|
561
581
|
Examples:
|
|
562
582
|
cbrowser hunt-bugs "https://example.com"
|
|
563
583
|
|
|
564
|
-
competitive-benchmark Compare UX across competitor sites
|
|
584
|
+
competitive-benchmark Compare UX across competitor sites (needs an Anthropic API key)
|
|
565
585
|
--sites <list> Comma-separated site URLs (required)
|
|
566
586
|
--goal <goal> Task goal (required)
|
|
567
587
|
--persona <name> Persona to use (default: first-timer)
|
|
@@ -574,12 +594,12 @@ AI FRIENDLINESS - Audit sites for AI agent compatibility
|
|
|
574
594
|
--sites "https://yoursite.com,https://competitor.com" \\
|
|
575
595
|
--goal "sign up for free trial" --html
|
|
576
596
|
|
|
577
|
-
ACCESSIBILITY
|
|
597
|
+
ACCESSIBILITY
|
|
578
598
|
a11y audit Run WCAG accessibility audit
|
|
579
599
|
--url <url> Navigate to URL first
|
|
580
600
|
a11y audit [url] Audit a specific URL
|
|
581
601
|
|
|
582
|
-
TEST RECORDING
|
|
602
|
+
TEST RECORDING
|
|
583
603
|
record start Start recording interactions
|
|
584
604
|
--url <url> Navigate to URL to begin recording
|
|
585
605
|
record stop Stop recording and show actions
|
|
@@ -591,7 +611,7 @@ SCREEN CAPTURE (video/GIF — distinct from 'record', which captures actions)
|
|
|
591
611
|
capture start <url> Capture the screen to GIF/WebP/video
|
|
592
612
|
--fps <n> Target frames per second (default: 10)
|
|
593
613
|
--duration <5s> Auto-stop after this long (5s, 2m, 1500ms)
|
|
594
|
-
--viewport <WxH> Viewport size, e.g. 1280x720
|
|
614
|
+
--viewport <WxH> Viewport size, e.g. 1280x720, or a size name (mobile, tablet, desktop...)
|
|
595
615
|
--region <x,y,w,h> Capture a fixed region instead of the viewport
|
|
596
616
|
--element <selector> Track an element; crop follows it as it moves
|
|
597
617
|
--element-padding <n> Expand the element box by n CSS px per side
|
|
@@ -625,13 +645,16 @@ SCREEN CAPTURE (video/GIF — distinct from 'record', which captures actions)
|
|
|
625
645
|
and the still stretches are reported as frame_gaps rather than padded out.
|
|
626
646
|
mp4 needs a full ffmpeg (CBROWSER_FFMPEG_PATH); the bundled one does webm.
|
|
627
647
|
|
|
628
|
-
TEST EXPORT
|
|
648
|
+
TEST EXPORT
|
|
629
649
|
export junit <name> [output] Export test results as JUnit XML
|
|
630
650
|
export tap <name> [output] Export test results as TAP format
|
|
651
|
+
<name> is a suite test-suite has run (its file name); each run is saved
|
|
631
652
|
|
|
632
|
-
WEBHOOKS
|
|
633
|
-
webhook add <name> <url> Add webhook notification
|
|
634
|
-
--events <events>
|
|
653
|
+
WEBHOOKS
|
|
654
|
+
webhook add <name> <url> Add webhook notification (http or https)
|
|
655
|
+
--events <events> test.pass,test.fail,journey.complete (default: test.fail);
|
|
656
|
+
test-suite sends one per run, cognitive-journey one per
|
|
657
|
+
journey; JSON, 10s timeout, no retries
|
|
635
658
|
--format <format> slack, discord, or generic
|
|
636
659
|
webhook list List configured webhooks
|
|
637
660
|
webhook delete <name> Delete a webhook
|
|
@@ -639,30 +662,37 @@ WEBHOOKS (v2.5.0)
|
|
|
639
662
|
|
|
640
663
|
PERSONA QUESTIONNAIRE
|
|
641
664
|
persona-questionnaire start Build a persona from a behavioural questionnaire
|
|
642
|
-
--comprehensive All
|
|
665
|
+
--comprehensive All 30 traits (default: 13 core traits)
|
|
643
666
|
--name <name> Persona name (prompted if omitted)
|
|
644
667
|
--output <file> Save answers to JSON
|
|
645
668
|
persona-questionnaire list-traits List available traits with descriptions
|
|
646
669
|
persona-questionnaire lookup Look up behaviours for a trait value
|
|
647
670
|
|
|
648
|
-
PARALLEL EXECUTION
|
|
671
|
+
PARALLEL EXECUTION
|
|
649
672
|
parallel devices <url> Run same URL across multiple devices
|
|
650
673
|
--devices <list> Comma-separated device names (default: all)
|
|
651
674
|
--concurrency <n> Max parallel browsers (default: 3)
|
|
675
|
+
--browser, --no-headless The browser for every device
|
|
652
676
|
parallel urls <urls> Run same task across multiple URLs
|
|
653
677
|
--concurrency <n> Max parallel browsers (default: 3)
|
|
678
|
+
--device, --viewport, --browser, --no-headless The browser for every URL
|
|
654
679
|
parallel perf <urls> Performance audit multiple URLs in parallel
|
|
655
680
|
--concurrency <n> Max parallel browsers (default: 3)
|
|
681
|
+
--device, --viewport, --browser, --no-headless The browser for every URL
|
|
656
682
|
|
|
657
|
-
NATURAL LANGUAGE
|
|
683
|
+
NATURAL LANGUAGE
|
|
658
684
|
run "<command>" Execute natural language command
|
|
659
685
|
Examples:
|
|
660
686
|
cbrowser run "go to https://example.com"
|
|
661
687
|
cbrowser run "click the login button"
|
|
662
688
|
cbrowser run "type 'hello' in the search box"
|
|
663
689
|
script <file> Execute script file with natural language commands
|
|
690
|
+
scroll <top|bottom|up|down> [px] Scroll the page (up/down default 300px)
|
|
691
|
+
--url <url> Navigate to URL first
|
|
692
|
+
Note: without the daemon the next command reloads the page, so the
|
|
693
|
+
scroll position does not carry over; start the daemon first.
|
|
664
694
|
|
|
665
|
-
VISUAL AI
|
|
695
|
+
VISUAL AI
|
|
666
696
|
ai find "<intent>" Find element by semantic intent
|
|
667
697
|
Examples:
|
|
668
698
|
cbrowser ai find "the cheapest product"
|
|
@@ -670,23 +700,20 @@ VISUAL AI (v4.0.0)
|
|
|
670
700
|
cbrowser ai find "search box"
|
|
671
701
|
ai click "<intent>" Find and click element by intent
|
|
672
702
|
|
|
673
|
-
|
|
674
|
-
hunt <url> Automatically find bugs on a page
|
|
675
|
-
--max-pages <n> Max pages to crawl (default: 10)
|
|
676
|
-
--timeout <ms> Timeout in ms (default: 60000)
|
|
677
|
-
|
|
678
|
-
CROSS-BROWSER (v4.0.0)
|
|
703
|
+
BROWSER DIFF
|
|
679
704
|
diff <url> Compare page across browsers
|
|
680
705
|
--browsers <list> chromium,firefox,webkit (default: all)
|
|
706
|
+
--viewport <WxH> Page size (default: 1920x1080)
|
|
707
|
+
--device <name> Device preset (firefox cannot emulate a mobile one)
|
|
681
708
|
|
|
682
|
-
CHAOS ENGINEERING
|
|
709
|
+
CHAOS ENGINEERING
|
|
683
710
|
chaos <url> Test app resilience
|
|
684
711
|
--latency <ms> Add network latency
|
|
685
712
|
--offline Simulate offline mode
|
|
686
713
|
--block <patterns> Block URL patterns (comma-separated)
|
|
687
714
|
--fail-api <pattern:status> Fail specific API calls
|
|
688
715
|
|
|
689
|
-
SMART RETRY & ASSERTIONS
|
|
716
|
+
SMART RETRY & ASSERTIONS
|
|
690
717
|
smart-click <selector> Click with auto-retry and alternative selectors
|
|
691
718
|
--url <url> Navigate to URL first
|
|
692
719
|
--max-retries <n> Maximum retry attempts (default: 3)
|
|
@@ -705,7 +732,7 @@ SMART RETRY & ASSERTIONS (v5.0.0)
|
|
|
705
732
|
cbrowser assert "'Login' button exists"
|
|
706
733
|
cbrowser assert "page has 3 items"
|
|
707
734
|
|
|
708
|
-
SELF-HEALING SELECTORS
|
|
735
|
+
SELF-HEALING SELECTORS
|
|
709
736
|
heal stats Show selector cache statistics
|
|
710
737
|
heal list List all cached selector mappings
|
|
711
738
|
--domain <domain> Filter by domain
|
|
@@ -714,18 +741,22 @@ SELF-HEALING SELECTORS (v5.0.0)
|
|
|
714
741
|
heal test <selector> Test if a selector would be healed
|
|
715
742
|
--url <url> Navigate to URL first
|
|
716
743
|
|
|
717
|
-
AI TEST GENERATION
|
|
744
|
+
AI TEST GENERATION
|
|
718
745
|
generate <url> Analyze page and generate test scenarios
|
|
719
746
|
--format <format> Output: summary, cbrowser, playwright, all (default: summary)
|
|
720
747
|
--output <file> Save to file instead of stdout
|
|
721
748
|
analyze <url> Show page analysis without generating tests
|
|
749
|
+
--output <file> Save the analysis as JSON
|
|
722
750
|
|
|
723
|
-
MCP SERVER
|
|
751
|
+
MCP SERVER
|
|
724
752
|
mcp-server Start CBrowser as MCP server for Claude integration
|
|
725
753
|
Use with Claude Desktop or other MCP-compatible clients
|
|
726
754
|
mcp-remote Start remote HTTP MCP server for claude.ai connectors
|
|
727
755
|
--port <port> Port to listen on (default: 3000)
|
|
728
756
|
--host <host> Host to bind to (default: 0.0.0.0)
|
|
757
|
+
--stateful Use stateful session mode
|
|
758
|
+
Smart visual baselines and transport maps have no CLI command; they are the
|
|
759
|
+
MCP tools visual_baseline / visual_regression (smart mode) and transport_map.
|
|
729
760
|
|
|
730
761
|
UNINSTALL
|
|
731
762
|
uninstall Remove CBrowser data: credentials, sessions, captures
|
|
@@ -740,47 +771,58 @@ CLAUDE SKILL INSTALLATION & SYNC
|
|
|
740
771
|
Downloads latest skill files from GitHub
|
|
741
772
|
Run after 'npm update -g cbrowser' to stay in sync
|
|
742
773
|
Add to skill-index.json after first installation
|
|
743
|
-
--stateful Use stateful session mode
|
|
744
774
|
|
|
745
|
-
DAEMON MODE
|
|
775
|
+
DAEMON MODE
|
|
746
776
|
daemon start Start background daemon (keeps browser running)
|
|
747
777
|
--port <port> Daemon port (default: 9222)
|
|
748
|
-
--
|
|
778
|
+
--device <name> Device the daemon's browser emulates
|
|
779
|
+
--viewport <WxH> Page size of the daemon's browser
|
|
780
|
+
--timeout <min> Idle timeout: minutes, or 90s / 45m / 2h (default: 30)
|
|
749
781
|
daemon stop Stop the running daemon
|
|
750
|
-
daemon status Check if daemon is running
|
|
782
|
+
daemon status Check if daemon is running (exits 1 when it is not)
|
|
751
783
|
daemon run Run daemon in foreground (internal use)
|
|
752
|
-
Note:
|
|
753
|
-
|
|
784
|
+
Note: while the daemon runs, navigate, click, fill, hover, type, press,
|
|
785
|
+
keyboard, screenshot, extract, run, scroll, evaluate, capture, har,
|
|
786
|
+
network and console use its browser. Commands that would open a second
|
|
787
|
+
browser on its profile (script, ai, explore, a11y, perf, visual,
|
|
788
|
+
drag, upload, reset and others) refuse until you stop it.
|
|
754
789
|
|
|
755
|
-
API CONFIGURATION
|
|
790
|
+
API CONFIGURATION
|
|
756
791
|
config set-api-key <key> Set Anthropic API key for cognitive journeys
|
|
757
792
|
config show-api-key Show configured API key (masked)
|
|
758
793
|
config remove-api-key Remove stored API key
|
|
759
794
|
config set-model <model> Set Claude model (default: claude-sonnet-5)
|
|
760
795
|
|
|
761
|
-
LIGHTPANDA
|
|
796
|
+
LIGHTPANDA - High-Performance Headless Browser (OPT-IN ONLY)
|
|
762
797
|
lightpanda-status Check Lightpanda availability and configuration
|
|
763
798
|
lightpanda-setup Show setup instructions with security warnings
|
|
764
799
|
⚠️ SECURITY NOTICE:
|
|
765
800
|
• Lightpanda is BETA software with no security audit
|
|
766
801
|
• Cloud mode exposes traffic to lightpanda.io servers
|
|
767
802
|
• NEVER use for auth, credentials, or payments
|
|
768
|
-
•
|
|
803
|
+
• Used only by agent-ready-audit, and only with --lightpanda
|
|
769
804
|
Environment Variables:
|
|
770
805
|
LIGHTPANDA_ENDPOINT WebSocket endpoint (e.g., ws://127.0.0.1:9222)
|
|
771
806
|
LIGHTPANDA_TOKEN Cloud API token (⚠️ data visible to lightpanda.io)
|
|
772
807
|
Examples:
|
|
773
808
|
cbrowser lightpanda-status
|
|
774
|
-
cbrowser screenshot https://example.com --lightpanda
|
|
775
809
|
cbrowser agent-ready-audit https://example.com --lightpanda
|
|
776
810
|
|
|
811
|
+
PRODUCT HELP
|
|
812
|
+
ask "<question>" Ask the CBrowser docs a question; prints an AI answer and its sources
|
|
813
|
+
--context <text> What you are looking at (a URL, a tool, a score)
|
|
814
|
+
--json Print the answer as JSON
|
|
815
|
+
Sends your question to cbrowser.ai. No account or API key; rate-limited.
|
|
816
|
+
Example:
|
|
817
|
+
cbrowser ask "Which persona should I use for a screen reader user?"
|
|
818
|
+
|
|
777
819
|
DIAGNOSTICS
|
|
778
820
|
version, -v, --version Show version number
|
|
779
821
|
status Show environment status and diagnostics
|
|
780
822
|
Displays data directories, browsers, config, heal cache
|
|
781
823
|
doctor Check environment health (Node, Playwright, API key)
|
|
782
824
|
|
|
783
|
-
|
|
825
|
+
OUTPUT MODES (accessibility)
|
|
784
826
|
--no-color Disable all color output (also respects NO_COLOR env var)
|
|
785
827
|
--plain Disable emoji, box-drawing, and unicode decorations
|
|
786
828
|
--json-output Output structured JSON (for scripts and screen readers)
|
|
@@ -792,11 +834,16 @@ STORAGE & CLEANUP
|
|
|
792
834
|
--older-than <days> Delete files older than N days (default: 7)
|
|
793
835
|
--keep-screenshots <n> Keep at least N screenshots (default: 10)
|
|
794
836
|
--keep-journeys <n> Keep at least N journeys (default: 5)
|
|
837
|
+
--keep-sessions <n> Keep at least N sessions (default: 3)
|
|
795
838
|
reset Clear persistent browser state (cookies, storage)
|
|
796
839
|
|
|
797
840
|
OPTIONS
|
|
798
841
|
--browser <type> Browser: chromium, firefox, webkit (default: chromium)
|
|
799
|
-
--device <name> Device preset: iphone-
|
|
842
|
+
--device <name> Device preset: iphone-16, pixel-9, ipad-pro-13, ... ('device list')
|
|
843
|
+
--viewport <WxH> Page size, e.g. 390x844; with --device it sets the size and
|
|
844
|
+
the device keeps its pixel ratio, touch and user agent
|
|
845
|
+
Size names work too (mobile, tablet, desktop-md...): size
|
|
846
|
+
only; use --device for touch and a mobile user agent
|
|
800
847
|
--geo <location> Location preset or lat,lon coordinates
|
|
801
848
|
--locale <locale> Browser locale (e.g., en-US, fr-FR)
|
|
802
849
|
--timezone <tz> Timezone (e.g., America/New_York)
|
|
@@ -864,8 +911,27 @@ const COMMAND_ALIASES = {
|
|
|
864
911
|
"chaos-test": "chaos",
|
|
865
912
|
"generate-tests": "generate",
|
|
866
913
|
journey: "explore",
|
|
914
|
+
// Dispatch aliases that `help` used to resolve by text search, so
|
|
915
|
+
// `help stats` showed "heal stats" (audit 2026-10-21).
|
|
916
|
+
hunt: "hunt-bugs",
|
|
917
|
+
flaky: "flaky-check",
|
|
918
|
+
repair: "repair-tests",
|
|
919
|
+
stats: "storage",
|
|
867
920
|
};
|
|
868
921
|
/** Escape a string for safe use inside a RegExp. */
|
|
922
|
+
/**
|
|
923
|
+
* Print a command result that is data. Under --json-output it becomes the
|
|
924
|
+
* envelope's `data`, untouched; otherwise pretty JSON on stdout. Before 20.0
|
|
925
|
+
* extract went through the text capture, which stored its JSON as text lines
|
|
926
|
+
* (audit 2026-10-21).
|
|
927
|
+
*/
|
|
928
|
+
function printData(command, data) {
|
|
929
|
+
if (getOutputOptions().json) {
|
|
930
|
+
emitJson({ success: true, command, data, meta: { version: VERSION } });
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
console.log(JSON.stringify(data, null, 2));
|
|
934
|
+
}
|
|
869
935
|
function escapeForRegExp(value) {
|
|
870
936
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
871
937
|
}
|
|
@@ -970,15 +1036,35 @@ function showCommandHelp(command) {
|
|
|
970
1036
|
* against a second browser while the daemon holds the first. Anything listed
|
|
971
1037
|
* here must either be daemon-routed or refuse to run.
|
|
972
1038
|
*/
|
|
1039
|
+
/** What `extract` can return (browser.extract). */
|
|
1040
|
+
const EXTRACT_KINDS = ["text", "links", "headings", "images", "forms"];
|
|
973
1041
|
const DRIVES_THE_PAGE = [
|
|
974
1042
|
"navigate", "click", "fill", "hover", "screenshot", "extract", "run",
|
|
975
1043
|
"keyboard", "press", "type", "evaluate", "eval", "drag", "upload",
|
|
976
1044
|
"smart-click", "dismiss-overlay", "assert",
|
|
1045
|
+
// These also launch the shared profile the daemon holds, so they acted on
|
|
1046
|
+
// a page the user was not watching or failed on its lock (audit
|
|
1047
|
+
// 2026-10-21). reset deleted the profile under the daemon, which wrote its
|
|
1048
|
+
// cookies back on exit.
|
|
1049
|
+
"script", "ai", "explore", "analyze", "generate", "hunt", "hunt-bugs",
|
|
1050
|
+
"chaos", "a11y", "perf", "visual", "reset",
|
|
977
1051
|
];
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1052
|
+
/**
|
|
1053
|
+
* Flags the shared CBrowser construction consumes, for every command that uses
|
|
1054
|
+
* it. One list, spread into each command's set below: when this list was copied
|
|
1055
|
+
* into each set, --viewport (2026-10-21) reached some commands and was refused
|
|
1056
|
+
* by evaluate and keyboard.
|
|
1057
|
+
*/
|
|
1058
|
+
const SHARED_BROWSER_FLAGS = [
|
|
1059
|
+
"url", "browser", "device", "viewport", "headless", "persistent", "restore",
|
|
1060
|
+
"locale", "timezone", "geo", "verbose",
|
|
981
1061
|
];
|
|
1062
|
+
/**
|
|
1063
|
+
* Globals every command accepts. --plain and --json-output are the documented
|
|
1064
|
+
* output modes (src/output.ts); before 2026-10-21 the validated commands
|
|
1065
|
+
* rejected them.
|
|
1066
|
+
*/
|
|
1067
|
+
const GLOBAL_FLAGS = [...SHARED_BROWSER_FLAGS, "help", "no-color", "json", "plain", "json-output"];
|
|
982
1068
|
/**
|
|
983
1069
|
* Declared options per command.
|
|
984
1070
|
*
|
|
@@ -1009,25 +1095,19 @@ const COMMAND_FLAGS = {
|
|
|
1009
1095
|
evaluate: ["file", "arg", "json", "raw", "wait-for", "timeout", "expect-truthy", "force"],
|
|
1010
1096
|
eval: ["file", "arg", "json", "raw", "wait-for", "timeout", "expect-truthy", "force"],
|
|
1011
1097
|
keyboard: ["delay", "selector", "hold", "repeat", "text", "force"],
|
|
1098
|
+
// test-suite and cognitive-journey list only the flags their code reads
|
|
1099
|
+
// (measured 2026-10-21). They used to declare every report command's flags,
|
|
1100
|
+
// so `test-suite --persona x` passed validation and did nothing.
|
|
1101
|
+
ask: ["context", "json"],
|
|
1012
1102
|
"test-suite": [
|
|
1013
1103
|
"capture", "capture-fps", "capture-format", "capture-out",
|
|
1014
|
-
"
|
|
1015
|
-
"
|
|
1016
|
-
"html", "include", "inline", "length", "lightpanda", "max-pages", "max-steps",
|
|
1017
|
-
"max-time", "min-coverage", "name", "no-vision", "oauth-token", "output",
|
|
1018
|
-
"persona", "retries", "runs", "screenshot-on-failure", "sensitivity",
|
|
1019
|
-
"sitemap", "sites", "start", "step-through", "tests", "threshold",
|
|
1020
|
-
"threshold-cls", "threshold-fcp", "threshold-lcp", "threshold-tbt",
|
|
1021
|
-
"threshold-ttfb", "threshold-tti", "timeout", "trait", "urls", "value",
|
|
1022
|
-
"verify", "wcag-level", "geolocation", "geo-region",
|
|
1104
|
+
"continue-on-failure", "dry-run", "fuzzy-match", "html", "inline", "output",
|
|
1105
|
+
"screenshot-on-failure", "step-through", "timeout",
|
|
1023
1106
|
],
|
|
1024
1107
|
"cognitive-journey": [
|
|
1025
1108
|
"capture", "capture-fps", "capture-format", "capture-out",
|
|
1026
|
-
"
|
|
1027
|
-
"
|
|
1028
|
-
"oauth-token", "output", "persona", "sites", "start", "timeout", "trait",
|
|
1029
|
-
"urls", "value", "wcag-level", "geolocation", "geo-region", "vision",
|
|
1030
|
-
"trace", "replay-trace",
|
|
1109
|
+
"goal", "persona", "start", "max-steps", "max-time", "no-vision", "output",
|
|
1110
|
+
"geolocation", "trace", "replay-trace",
|
|
1031
1111
|
],
|
|
1032
1112
|
};
|
|
1033
1113
|
/**
|
|
@@ -1044,11 +1124,29 @@ const UNVALIDATED_REASON = "not yet declared - see COMMAND_FLAGS in src/cli.ts";
|
|
|
1044
1124
|
* Fails on the FIRST invalid flag with the command's full accepted set, so a
|
|
1045
1125
|
* typo is a one-command fix rather than a guessing game.
|
|
1046
1126
|
*/
|
|
1127
|
+
/**
|
|
1128
|
+
* Every flag a validated command accepts: its declared flags, the globals, and
|
|
1129
|
+
* both halves of each on/off pair. The parser turns --no-x into "no-x" and
|
|
1130
|
+
* "x"=false, so declaring either half must accept both; before 2026-10-21
|
|
1131
|
+
* --no-color failed with "does not support --color", a flag the user never
|
|
1132
|
+
* typed. The capture, evaluate and keyboard gates read this same set, so one
|
|
1133
|
+
* command line can no longer pass one gate and fail the other.
|
|
1134
|
+
*/
|
|
1135
|
+
function allowedFlags(command) {
|
|
1136
|
+
const allowed = new Set([...(COMMAND_FLAGS[command] ?? []), ...GLOBAL_FLAGS]);
|
|
1137
|
+
for (const flag of [...allowed]) {
|
|
1138
|
+
if (flag.startsWith("no-"))
|
|
1139
|
+
allowed.add(flag.slice(3));
|
|
1140
|
+
else
|
|
1141
|
+
allowed.add(`no-${flag}`);
|
|
1142
|
+
}
|
|
1143
|
+
return allowed;
|
|
1144
|
+
}
|
|
1047
1145
|
function validateCommandFlags(command, options) {
|
|
1048
1146
|
const declared = COMMAND_FLAGS[command];
|
|
1049
1147
|
if (!declared)
|
|
1050
1148
|
return; // UNVALIDATED_REASON
|
|
1051
|
-
const allowed =
|
|
1149
|
+
const allowed = allowedFlags(command);
|
|
1052
1150
|
const unknown = Object.keys(options).filter((flag) => !allowed.has(flag));
|
|
1053
1151
|
if (unknown.length === 0)
|
|
1054
1152
|
return;
|
|
@@ -1073,14 +1171,66 @@ function captureStatePath() {
|
|
|
1073
1171
|
return join(getPaths().videosDir, "active.json");
|
|
1074
1172
|
}
|
|
1075
1173
|
/** Parse "5s", "2m", "1500ms" or a bare millisecond count. */
|
|
1174
|
+
/**
|
|
1175
|
+
* A numeric option, checked. parseInt/parseFloat let "abc" through as NaN and
|
|
1176
|
+
* read "0.2" as 0 (flaky-check --threshold flagged every test), so a value
|
|
1177
|
+
* outside the range or not a number exits 1 naming the flag (audit F21).
|
|
1178
|
+
*/
|
|
1179
|
+
function numberOption(flag, value, fallback, range) {
|
|
1180
|
+
if (value === undefined)
|
|
1181
|
+
return fallback;
|
|
1182
|
+
const n = typeof value === "string" && value.trim() !== "" ? Number(value) : NaN;
|
|
1183
|
+
if (!Number.isFinite(n) || n < range.min || n > range.max) {
|
|
1184
|
+
console.error(`Error: --${flag} must be a number from ${range.min} to ${range.max}, got "${String(value)}"`);
|
|
1185
|
+
process.exit(1);
|
|
1186
|
+
}
|
|
1187
|
+
return n;
|
|
1188
|
+
}
|
|
1189
|
+
/** An option from a fixed set of words; anything else exits 1 (audit F21). */
|
|
1190
|
+
function enumOption(flag, value, allowed) {
|
|
1191
|
+
if (value === undefined)
|
|
1192
|
+
return undefined;
|
|
1193
|
+
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
1194
|
+
console.error(`Error: --${flag} must be ${allowed.join(", ")}, got "${String(value)}"`);
|
|
1195
|
+
process.exit(1);
|
|
1196
|
+
}
|
|
1197
|
+
return value;
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Where --html writes when --output names the JSON report. The HTML used to
|
|
1201
|
+
* take `output.replace(".json", ".html")`, so `--output report` or
|
|
1202
|
+
* `--output report.html` wrote the HTML over the JSON (audit 2026-10-21).
|
|
1203
|
+
*/
|
|
1204
|
+
function htmlReportPath(output, fallback) {
|
|
1205
|
+
if (typeof output !== "string" || output === "")
|
|
1206
|
+
return fallback;
|
|
1207
|
+
const base = output.replace(/\.(json|html?)$/i, "");
|
|
1208
|
+
const html = `${base}.html`;
|
|
1209
|
+
return html === output ? `${base}.report.html` : html;
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* A --timeout value in milliseconds: a bare number is ms, and 5s / 2m / 1h work.
|
|
1213
|
+
* Exits 1 on anything else rather than guessing.
|
|
1214
|
+
*/
|
|
1215
|
+
function timeoutOption(value, fallbackMs) {
|
|
1216
|
+
if (value === undefined)
|
|
1217
|
+
return fallbackMs;
|
|
1218
|
+
try {
|
|
1219
|
+
return parseDurationMs(String(value));
|
|
1220
|
+
}
|
|
1221
|
+
catch {
|
|
1222
|
+
console.error(`Error: --timeout must be a positive number of milliseconds or a duration (5s, 2m, 1h), got "${String(value)}"`);
|
|
1223
|
+
process.exit(1);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1076
1226
|
function parseDurationMs(input) {
|
|
1077
|
-
const match = /^(\d+(?:\.\d+)?)\s*(ms|s|m)?$/i.exec(input.trim());
|
|
1227
|
+
const match = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h)?$/i.exec(input.trim());
|
|
1078
1228
|
if (!match) {
|
|
1079
|
-
throw new Error(`Invalid duration "${input}" - use 5s, 2m, 1500ms or a millisecond count`);
|
|
1229
|
+
throw new Error(`Invalid duration "${input}" - use 5s, 2m, 1h, 1500ms or a millisecond count`);
|
|
1080
1230
|
}
|
|
1081
1231
|
const value = parseFloat(match[1]);
|
|
1082
1232
|
const unit = (match[2] || "ms").toLowerCase();
|
|
1083
|
-
const ms = unit === "s" ? value * 1000 : unit === "m" ? value * 60_000 : value;
|
|
1233
|
+
const ms = unit === "s" ? value * 1000 : unit === "m" ? value * 60_000 : unit === "h" ? value * 3_600_000 : value;
|
|
1084
1234
|
if (ms <= 0)
|
|
1085
1235
|
throw new Error(`Invalid duration "${input}" - must be greater than zero`);
|
|
1086
1236
|
return Math.round(ms);
|
|
@@ -1099,9 +1249,13 @@ function parseFormats(input) {
|
|
|
1099
1249
|
}
|
|
1100
1250
|
/** Parse "1280x720". */
|
|
1101
1251
|
function parseViewportSize(input) {
|
|
1252
|
+
const preset = VIEWPORT_PRESETS.find((p) => p.name === input.trim().toLowerCase());
|
|
1253
|
+
if (preset)
|
|
1254
|
+
return { width: preset.width, height: preset.height };
|
|
1102
1255
|
const match = /^(\d+)x(\d+)$/i.exec(input.trim());
|
|
1103
|
-
if (!match)
|
|
1104
|
-
throw new Error(`Invalid viewport "${input}" - use WIDTHxHEIGHT, e.g. 1280x720`);
|
|
1256
|
+
if (!match) {
|
|
1257
|
+
throw new Error(`Invalid viewport "${input}" - use WIDTHxHEIGHT, e.g. 1280x720, or a size name: ${VIEWPORT_PRESETS.map((p) => p.name).join(", ")}`);
|
|
1258
|
+
}
|
|
1105
1259
|
const width = parseInt(match[1], 10);
|
|
1106
1260
|
const height = parseInt(match[2], 10);
|
|
1107
1261
|
if (width <= 0 || height <= 0) {
|
|
@@ -1139,36 +1293,23 @@ function parseRegion(input) {
|
|
|
1139
1293
|
* confident "✓" for a capture the user did not ask for, which is worse than
|
|
1140
1294
|
* an error. The globals are the ones the CLI itself consumes before dispatch.
|
|
1141
1295
|
*/
|
|
1142
|
-
const CAPTURE_FLAGS =
|
|
1143
|
-
// capture-specific
|
|
1144
|
-
"fps", "duration", "format", "out", "name", "quality", "max-frames",
|
|
1145
|
-
"viewport", "region", "element", "element-padding",
|
|
1146
|
-
"contact-sheet", "attention-overlay", "overlay-persona", "overlay-goal",
|
|
1147
|
-
"after", "after-element", "after-delay",
|
|
1148
|
-
"until-element", "until-element-gone", "until-idle", "timeout",
|
|
1149
|
-
// globals honoured by the shared CBrowser construction
|
|
1150
|
-
"url", "browser", "device", "headless", "persistent", "restore",
|
|
1151
|
-
"locale", "timezone", "geo", "verbose",
|
|
1152
|
-
]);
|
|
1296
|
+
const CAPTURE_FLAGS = allowedFlags("capture");
|
|
1153
1297
|
/**
|
|
1154
1298
|
* Resolve `--device` to a real preset key, or exit naming the bad value.
|
|
1155
1299
|
*
|
|
1156
|
-
*
|
|
1157
|
-
*
|
|
1158
|
-
*
|
|
1159
|
-
*
|
|
1300
|
+
* `capture start` checks here, before it builds anything. Every other command
|
|
1301
|
+
* gets the same check from CBrowser.launch() (src/devices.ts resolveDevice);
|
|
1302
|
+
* before 2026-10-21 the browser skipped names it did not know and rendered
|
|
1303
|
+
* 1280x800 desktop. Case and spaces do not matter: "iPhone 15" is "iphone-15".
|
|
1160
1304
|
*/
|
|
1161
1305
|
function resolveDevicePreset(value) {
|
|
1162
|
-
const
|
|
1163
|
-
if (
|
|
1164
|
-
return
|
|
1165
|
-
const known = Object.keys(DEVICE_PRESETS);
|
|
1166
|
-
const stem = normalised.replace(/-?\d+.*$/, "");
|
|
1167
|
-
const close = known.filter((k) => k.startsWith(stem) && stem.length > 0);
|
|
1306
|
+
const resolved = resolveDevice(value);
|
|
1307
|
+
if ("key" in resolved)
|
|
1308
|
+
return resolved.key;
|
|
1168
1309
|
console.error(`Error: unknown --device "${value}"`);
|
|
1169
|
-
if (
|
|
1170
|
-
console.error(` Did you mean: ${
|
|
1171
|
-
console.error(`
|
|
1310
|
+
if (resolved.suggestions.length > 0)
|
|
1311
|
+
console.error(` Did you mean: ${resolved.suggestions.join(", ")}?`);
|
|
1312
|
+
console.error(` 'cbrowser device list' shows all ${Object.keys(DEVICE_PRESETS).length} presets; --viewport WIDTHxHEIGHT sets any size.`);
|
|
1172
1313
|
process.exit(1);
|
|
1173
1314
|
}
|
|
1174
1315
|
/** Reject any flag `capture start` cannot honour, naming it. */
|
|
@@ -1505,12 +1646,19 @@ function failCaptureCommand(message) {
|
|
|
1505
1646
|
* later `cbrowser click` or `cbrowser fill` appear in the recording.
|
|
1506
1647
|
*/
|
|
1507
1648
|
async function runCaptureStartViaDaemon(url, options) {
|
|
1649
|
+
// The daemon's page size and device are fixed when it starts. These were
|
|
1650
|
+
// accepted here and dropped, so a capture "at 390x844" recorded at the
|
|
1651
|
+
// daemon's size and reported success (audit 2026-10-21).
|
|
1652
|
+
for (const flag of ["viewport", "device", "browser"]) {
|
|
1653
|
+
if (options[flag] !== undefined) {
|
|
1654
|
+
failCaptureCommand(`--${flag} cannot change a running daemon's browser. Restart it with: cbrowser daemon stop && cbrowser daemon start --${flag} <value>`);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1508
1657
|
const target = buildCaptureTarget(options);
|
|
1509
1658
|
const triggers = buildCaptureTriggers(options);
|
|
1510
1659
|
const slug = slugFor(options);
|
|
1511
1660
|
const response = await sendToDaemon("captureStart", {
|
|
1512
1661
|
url,
|
|
1513
|
-
engine: options.browser === "firefox" ? "firefox" : options.browser === "webkit" ? "webkit" : "chromium",
|
|
1514
1662
|
options: {
|
|
1515
1663
|
fps: options.fps !== undefined ? Number(options.fps) : undefined,
|
|
1516
1664
|
quality: options.quality !== undefined ? Number(options.quality) : undefined,
|
|
@@ -1520,6 +1668,16 @@ async function runCaptureStartViaDaemon(url, options) {
|
|
|
1520
1668
|
contactSheet: options["contact-sheet"] === true,
|
|
1521
1669
|
slug,
|
|
1522
1670
|
target,
|
|
1671
|
+
// Plain strings, so the overlay crosses the HTTP boundary intact; the
|
|
1672
|
+
// daemon path used to drop it.
|
|
1673
|
+
...(options["attention-overlay"]
|
|
1674
|
+
? {
|
|
1675
|
+
saliencyOverlay: {
|
|
1676
|
+
...(typeof options["overlay-persona"] === "string" ? { persona: options["overlay-persona"] } : {}),
|
|
1677
|
+
...(typeof options["overlay-goal"] === "string" ? { goal: options["overlay-goal"] } : {}),
|
|
1678
|
+
},
|
|
1679
|
+
}
|
|
1680
|
+
: {}),
|
|
1523
1681
|
...triggers,
|
|
1524
1682
|
},
|
|
1525
1683
|
});
|
|
@@ -1750,11 +1908,7 @@ function reportAutoCapture(capture) {
|
|
|
1750
1908
|
// evaluate — run JavaScript in the page
|
|
1751
1909
|
// =========================================================================
|
|
1752
1910
|
/** Flags `evaluate` understands; anything else is rejected rather than ignored. */
|
|
1753
|
-
const EVALUATE_FLAGS =
|
|
1754
|
-
"file", "arg", "json", "raw", "wait-for", "timeout", "expect-truthy", "force",
|
|
1755
|
-
"url", "browser", "device", "headless", "persistent", "restore",
|
|
1756
|
-
"locale", "timezone", "geo", "verbose",
|
|
1757
|
-
]);
|
|
1911
|
+
const EVALUATE_FLAGS = allowedFlags("evaluate");
|
|
1758
1912
|
/**
|
|
1759
1913
|
* `--force` is a bare flag, but the parser gives any flag the next non-flag
|
|
1760
1914
|
* word as its value, so `evaluate --force "document.title"` reads the SCRIPT as
|
|
@@ -1787,61 +1941,14 @@ function repeatedFlag(name) {
|
|
|
1787
1941
|
return values;
|
|
1788
1942
|
}
|
|
1789
1943
|
/**
|
|
1790
|
-
*
|
|
1791
|
-
*
|
|
1792
|
-
*
|
|
1793
|
-
*
|
|
1794
|
-
*
|
|
1795
|
-
*
|
|
1796
|
-
* own, and a function/arrow literal is called with the arguments.
|
|
1797
|
-
*/
|
|
1798
|
-
async function evaluateScript(page, script, args) {
|
|
1799
|
-
const source = script.trim();
|
|
1800
|
-
// A function or arrow literal is called with the arguments directly.
|
|
1801
|
-
const isCallable = /^(async\s+)?function\b/.test(source) || /=>/.test(source);
|
|
1802
|
-
return page.evaluate((payload) => {
|
|
1803
|
-
if (payload.callable) {
|
|
1804
|
-
const fn = (0, eval)(`(${payload.src})`);
|
|
1805
|
-
return fn(...payload.a);
|
|
1806
|
-
}
|
|
1807
|
-
const compile = (src) => {
|
|
1808
|
-
// 1. A bare expression: implicit return.
|
|
1809
|
-
try {
|
|
1810
|
-
return new Function("args", `return (${src});`);
|
|
1811
|
-
}
|
|
1812
|
-
catch { /* not an expression */ }
|
|
1813
|
-
// 2. Statements with no explicit return: return the final expression,
|
|
1814
|
-
// which is what a REPL and the old one-arg form both did.
|
|
1815
|
-
if (!/\breturn\b/.test(src)) {
|
|
1816
|
-
const trimmed = src.replace(/;\s*$/, "");
|
|
1817
|
-
const split = Math.max(trimmed.lastIndexOf(";"), trimmed.lastIndexOf("\n"));
|
|
1818
|
-
if (split > -1) {
|
|
1819
|
-
const head = trimmed.slice(0, split + 1);
|
|
1820
|
-
const tail = trimmed.slice(split + 1).trim();
|
|
1821
|
-
if (tail) {
|
|
1822
|
-
try {
|
|
1823
|
-
return new Function("args", `${head}\nreturn (${tail});`);
|
|
1824
|
-
}
|
|
1825
|
-
catch { /* the tail is not an expression either */ }
|
|
1826
|
-
}
|
|
1827
|
-
}
|
|
1828
|
-
}
|
|
1829
|
-
// 3. Statements as written. Any syntax error surfaces from here, which
|
|
1830
|
-
// is the error the user should actually see.
|
|
1831
|
-
return new Function("args", src);
|
|
1832
|
-
};
|
|
1833
|
-
return compile(payload.src)(payload.a);
|
|
1834
|
-
}, { src: source, a: args, callable: isCallable });
|
|
1835
|
-
}
|
|
1836
|
-
/**
|
|
1837
|
-
* Run JavaScript in the page.
|
|
1838
|
-
*
|
|
1839
|
-
* The bare `evaluate "<js>"` form is unchanged. Everything else is additive:
|
|
1840
|
-
* a script from a file (shell quoting mangles multi-line scripts), JSON
|
|
1841
|
-
* arguments (so a script is reusable instead of string-interpolated), an
|
|
1842
|
-
* explicit output mode, and an assertion mode for CI.
|
|
1944
|
+
* Validate evaluate's flags and build the script, its arguments and the wait.
|
|
1945
|
+
* Exits with a message on any error. Shared by the local path and the daemon
|
|
1946
|
+
* route so they cannot drift: before 2026-10-21 the daemon route sent only the
|
|
1947
|
+
* script, so --arg, --url, --wait-for, --timeout, --raw, --json and
|
|
1948
|
+
* --expect-truthy did nothing when a daemon was running, and an
|
|
1949
|
+
* --expect-truthy CI check passed on a falsy result.
|
|
1843
1950
|
*/
|
|
1844
|
-
async function
|
|
1951
|
+
async function prepareEvaluate(args, options) {
|
|
1845
1952
|
const unknown = Object.keys(options).filter((f) => !EVALUATE_FLAGS.has(f));
|
|
1846
1953
|
if (unknown.length > 0) {
|
|
1847
1954
|
console.error(`Error: 'evaluate' does not support ${unknown.map((f) => `--${f}`).join(", ")}`);
|
|
@@ -1910,45 +2017,53 @@ async function runEvaluate(browser, args, options) {
|
|
|
1910
2017
|
}
|
|
1911
2018
|
// Said on stderr so stdout stays exactly the result (--raw / --json pipes).
|
|
1912
2019
|
console.error("⚠ Red zone: running caller-supplied page script (--force)");
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
if (typeof options["wait-for"] === "string") {
|
|
1917
|
-
// Bounded by the caller, not by a constant.
|
|
1918
|
-
//
|
|
1919
|
-
// This was a hardcoded 30s, which is two defects wearing one number. For a
|
|
1920
|
-
// user it meant no way to fail fast on a page that renders in 200ms and no
|
|
1921
|
-
// way to wait longer on one that does not. For the suite it meant the test
|
|
1922
|
-
// asserting that a never-matching selector fails loudly spent 30 of its 60
|
|
1923
|
-
// second budget deliberately waiting, so browser launch plus teardown had
|
|
1924
|
-
// to fit in the rest -- which held when the test ran alone and did not when
|
|
1925
|
-
// the full suite ran browsers in parallel. The test read as flaky and was
|
|
1926
|
-
// not: it was structurally tight against a constant it could not reach.
|
|
1927
|
-
// (2026-08-02)
|
|
1928
|
-
// Falls back to the configured default rather than a fresh literal.
|
|
1929
|
-
// `CBROWSER_TIMEOUT` is documented in --help as "Default timeout in ms" and
|
|
1930
|
-
// config.ts already resolves it; a second hardcoded 30_000 here would mean
|
|
1931
|
-
// the documented env var silently did not apply to this one wait.
|
|
2020
|
+
const waitFor = typeof options["wait-for"] === "string" ? options["wait-for"] : undefined;
|
|
2021
|
+
let waitMs = 30_000;
|
|
2022
|
+
if (waitFor) {
|
|
1932
2023
|
const { getDefaultConfig } = await import("./config.js");
|
|
1933
2024
|
const configuredMs = getDefaultConfig().timeout ?? 30_000;
|
|
1934
|
-
|
|
1935
|
-
: typeof options.timeout === "number" ? options.timeout
|
|
1936
|
-
: configuredMs;
|
|
2025
|
+
waitMs = typeof options.timeout === "number" ? options.timeout : timeoutOption(options.timeout, configuredMs);
|
|
1937
2026
|
if (!Number.isFinite(waitMs) || waitMs <= 0) {
|
|
1938
2027
|
console.error(`Error: --timeout must be a positive number of milliseconds, got: ${options.timeout}`);
|
|
1939
2028
|
process.exit(1);
|
|
1940
2029
|
}
|
|
2030
|
+
}
|
|
2031
|
+
return { script, evalArgs, waitFor, waitMs };
|
|
2032
|
+
}
|
|
2033
|
+
/** Print an evaluate result the same way for the local path and the daemon. */
|
|
2034
|
+
function printEvaluateResult(result, options) {
|
|
2035
|
+
if (options.raw === true) {
|
|
2036
|
+
console.log(String(result));
|
|
2037
|
+
}
|
|
2038
|
+
else if (options.json === true) {
|
|
2039
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2040
|
+
}
|
|
2041
|
+
else {
|
|
2042
|
+
// Legacy default, kept so the existing one-arg form behaves identically.
|
|
2043
|
+
console.log(typeof result === "string" ? result : JSON.stringify(result, null, 2));
|
|
2044
|
+
}
|
|
2045
|
+
if (options["expect-truthy"] === true && !result) {
|
|
2046
|
+
console.error(`✗ Expected a truthy result, got ${JSON.stringify(result)}`);
|
|
2047
|
+
process.exit(1);
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
async function runEvaluate(browser, args, options) {
|
|
2051
|
+
const prep = await prepareEvaluate(args, options);
|
|
2052
|
+
if (options.url)
|
|
2053
|
+
await browser.navigate(options.url);
|
|
2054
|
+
const page = await browser.getPage();
|
|
2055
|
+
if (prep.waitFor) {
|
|
1941
2056
|
try {
|
|
1942
|
-
await page.waitForSelector(
|
|
2057
|
+
await page.waitForSelector(prep.waitFor, { timeout: prep.waitMs });
|
|
1943
2058
|
}
|
|
1944
2059
|
catch {
|
|
1945
|
-
console.error(`✗ Timed out waiting for selector: ${
|
|
2060
|
+
console.error(`✗ Timed out waiting for selector: ${prep.waitFor} (after ${prep.waitMs}ms)`);
|
|
1946
2061
|
process.exit(1);
|
|
1947
2062
|
}
|
|
1948
2063
|
}
|
|
1949
2064
|
let result;
|
|
1950
2065
|
try {
|
|
1951
|
-
result = await evaluateScript(page, script, evalArgs);
|
|
2066
|
+
result = await evaluateScript(page, prep.script, prep.evalArgs);
|
|
1952
2067
|
}
|
|
1953
2068
|
catch (e) {
|
|
1954
2069
|
const error = e;
|
|
@@ -1958,20 +2073,7 @@ async function runEvaluate(browser, args, options) {
|
|
|
1958
2073
|
console.error(error.stack.split("\n").slice(1).join("\n"));
|
|
1959
2074
|
process.exit(1);
|
|
1960
2075
|
}
|
|
1961
|
-
|
|
1962
|
-
console.log(String(result));
|
|
1963
|
-
}
|
|
1964
|
-
else if (options.json === true) {
|
|
1965
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1966
|
-
}
|
|
1967
|
-
else {
|
|
1968
|
-
// Legacy default, kept so the existing one-arg form behaves identically.
|
|
1969
|
-
console.log(typeof result === "string" ? result : JSON.stringify(result, null, 2));
|
|
1970
|
-
}
|
|
1971
|
-
if (options["expect-truthy"] === true && !result) {
|
|
1972
|
-
console.error(`✗ Expected a truthy result, got ${JSON.stringify(result)}`);
|
|
1973
|
-
process.exit(1);
|
|
1974
|
-
}
|
|
2076
|
+
printEvaluateResult(result, options);
|
|
1975
2077
|
}
|
|
1976
2078
|
// =========================================================================
|
|
1977
2079
|
// keyboard — sequences, chords and held modifiers
|
|
@@ -2069,12 +2171,26 @@ function parseKeyboardToken(token, literal = false) {
|
|
|
2069
2171
|
}
|
|
2070
2172
|
return { kind: "text", text: token };
|
|
2071
2173
|
}
|
|
2174
|
+
/**
|
|
2175
|
+
* --delay and --repeat for keyboard, checked the same way whether the keys go
|
|
2176
|
+
* to the local browser or the daemon. The daemon route used to skip this, so
|
|
2177
|
+
* --repeat 0 typed nothing and reported success, and --repeat 2.5 ran 3 passes.
|
|
2178
|
+
*/
|
|
2179
|
+
function parseKeyboardNumbers(options) {
|
|
2180
|
+
const delay = options.delay !== undefined ? Number(options.delay) : 50;
|
|
2181
|
+
if (!Number.isFinite(delay) || delay < 0) {
|
|
2182
|
+
console.error(`Error: --delay must be a non-negative number (got "${String(options.delay)}")`);
|
|
2183
|
+
process.exit(1);
|
|
2184
|
+
}
|
|
2185
|
+
const repeat = options.repeat !== undefined ? Number(options.repeat) : 1;
|
|
2186
|
+
if (!Number.isInteger(repeat) || repeat < 1) {
|
|
2187
|
+
console.error(`Error: --repeat must be a positive integer (got "${String(options.repeat)}")`);
|
|
2188
|
+
process.exit(1);
|
|
2189
|
+
}
|
|
2190
|
+
return { delay, repeat };
|
|
2191
|
+
}
|
|
2072
2192
|
/** Flags `keyboard` understands. */
|
|
2073
|
-
const KEYBOARD_FLAGS =
|
|
2074
|
-
"delay", "selector", "hold", "repeat", "text", "force",
|
|
2075
|
-
"url", "browser", "device", "headless", "persistent", "restore",
|
|
2076
|
-
"locale", "timezone", "geo", "verbose",
|
|
2077
|
-
]);
|
|
2193
|
+
const KEYBOARD_FLAGS = allowedFlags("keyboard");
|
|
2078
2194
|
/**
|
|
2079
2195
|
* Drive a keyboard sequence.
|
|
2080
2196
|
*
|
|
@@ -2094,16 +2210,7 @@ async function runKeyboard(browser, args, options) {
|
|
|
2094
2210
|
console.error(" Run 'cbrowser help keyboard' for options");
|
|
2095
2211
|
process.exit(1);
|
|
2096
2212
|
}
|
|
2097
|
-
const
|
|
2098
|
-
if (!Number.isFinite(delay) || delay < 0) {
|
|
2099
|
-
console.error(`Error: --delay must be a non-negative number (got "${String(options.delay)}")`);
|
|
2100
|
-
process.exit(1);
|
|
2101
|
-
}
|
|
2102
|
-
const repeat = options.repeat !== undefined ? Number(options.repeat) : 1;
|
|
2103
|
-
if (!Number.isInteger(repeat) || repeat < 1) {
|
|
2104
|
-
console.error(`Error: --repeat must be a positive integer (got "${String(options.repeat)}")`);
|
|
2105
|
-
process.exit(1);
|
|
2106
|
-
}
|
|
2213
|
+
const { delay, repeat } = parseKeyboardNumbers(options);
|
|
2107
2214
|
const hold = typeof options.hold === "string"
|
|
2108
2215
|
? options.hold.split("+").map((m) => m.trim()).filter(Boolean)
|
|
2109
2216
|
: [];
|
|
@@ -2613,8 +2720,29 @@ function parseGeoLocation(location) {
|
|
|
2613
2720
|
}
|
|
2614
2721
|
return null;
|
|
2615
2722
|
}
|
|
2723
|
+
/**
|
|
2724
|
+
* `scroll <top|bottom|up|down> [px]` is `run "scroll ..."`: the same executor,
|
|
2725
|
+
* the same daemon route. The CLI had no way to scroll at all (audit 2026-10-21,
|
|
2726
|
+
* daemon lane: the likely cause of a screenshot stuck at the top of the page).
|
|
2727
|
+
*/
|
|
2728
|
+
function rewriteScroll(parsed) {
|
|
2729
|
+
if (parsed.command !== "scroll")
|
|
2730
|
+
return parsed;
|
|
2731
|
+
const [where, px] = parsed.args;
|
|
2732
|
+
const dir = (where ?? "").toLowerCase();
|
|
2733
|
+
let nl;
|
|
2734
|
+
if ((dir === "top" || dir === "bottom") && px === undefined)
|
|
2735
|
+
nl = `scroll to ${dir}`;
|
|
2736
|
+
else if ((dir === "up" || dir === "down") && (px === undefined || /^\d+$/.test(px)))
|
|
2737
|
+
nl = `scroll ${dir}${px ? ` ${px}` : ""}`;
|
|
2738
|
+
if (!nl) {
|
|
2739
|
+
console.error("Usage: cbrowser scroll <top|bottom|up|down> [pixels] (default 300 for up/down)");
|
|
2740
|
+
process.exit(1);
|
|
2741
|
+
}
|
|
2742
|
+
return { command: "run", args: [nl], options: parsed.options };
|
|
2743
|
+
}
|
|
2616
2744
|
async function main() {
|
|
2617
|
-
const { command, args, options } = parseArgs(process.argv.slice(2));
|
|
2745
|
+
const { command, args, options } = rewriteScroll(parseArgs(process.argv.slice(2)));
|
|
2618
2746
|
// Initialize accessible output system (v18.54.0)
|
|
2619
2747
|
const outputOpts = detectOutputOptions(options);
|
|
2620
2748
|
setOutputOptions(outputOpts);
|
|
@@ -2656,6 +2784,35 @@ async function main() {
|
|
|
2656
2784
|
// path unreachable from a shell no matter what the checks found.
|
|
2657
2785
|
process.exit(process.exitCode ?? 0);
|
|
2658
2786
|
}
|
|
2787
|
+
// ask: product help from the CBrowser docs (src/help-client.ts). No browser.
|
|
2788
|
+
if (command === "ask") {
|
|
2789
|
+
const question = args.join(" ").trim();
|
|
2790
|
+
if (!question) {
|
|
2791
|
+
console.error('Usage: cbrowser ask "<question>" [--context <text>] [--json]');
|
|
2792
|
+
console.error(' e.g. cbrowser ask "How do I test my site on an iPhone?"');
|
|
2793
|
+
process.exit(1);
|
|
2794
|
+
}
|
|
2795
|
+
const { askCbrowser } = await import("./help-client.js");
|
|
2796
|
+
try {
|
|
2797
|
+
const r = await askCbrowser(question, { context: typeof options.context === "string" ? options.context : undefined });
|
|
2798
|
+
if (options.json === true || getOutputOptions().json) {
|
|
2799
|
+
printData("ask", { question: r.question, answer: r.answer, sources: r.sources });
|
|
2800
|
+
}
|
|
2801
|
+
else {
|
|
2802
|
+
console.log(r.answer);
|
|
2803
|
+
if (r.sources.length > 0) {
|
|
2804
|
+
console.log("\nSources:");
|
|
2805
|
+
for (const s of r.sources)
|
|
2806
|
+
console.log(` - ${s.split("\n")[0].slice(0, 120)}`);
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
process.exit(0);
|
|
2810
|
+
}
|
|
2811
|
+
catch (e) {
|
|
2812
|
+
console.error(`Error: ${e.message}`);
|
|
2813
|
+
process.exit(1);
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2659
2816
|
// MCP Server mode - runs before browser instantiation
|
|
2660
2817
|
if (command === "mcp-server") {
|
|
2661
2818
|
console.error("🔌 Starting CBrowser MCP Server...");
|
|
@@ -2799,17 +2956,10 @@ async function main() {
|
|
|
2799
2956
|
console.log("Creating ~/.claude/skills directory...");
|
|
2800
2957
|
fs.mkdirSync(skillsDir, { recursive: true });
|
|
2801
2958
|
}
|
|
2802
|
-
//
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
}
|
|
2807
|
-
// Create directories
|
|
2808
|
-
console.log("Creating skill directory structure...");
|
|
2809
|
-
fs.mkdirSync(path.join(skillDir, "Workflows"), { recursive: true });
|
|
2810
|
-
fs.mkdirSync(path.join(skillDir, "Tools"), { recursive: true });
|
|
2811
|
-
fs.mkdirSync(path.join(skillDir, ".memory", "sessions"), { recursive: true });
|
|
2812
|
-
// Download files using native fetch (cross-platform, better error handling)
|
|
2959
|
+
// Download everything first and change nothing unless every file arrived.
|
|
2960
|
+
// This used to delete the installed skill (its .memory/sessions included)
|
|
2961
|
+
// before downloading, then print "Installed Successfully" even when every
|
|
2962
|
+
// download failed (audit 2026-10-21).
|
|
2813
2963
|
console.log("Downloading skill files...");
|
|
2814
2964
|
const files = [
|
|
2815
2965
|
"SKILL.md",
|
|
@@ -2826,22 +2976,35 @@ async function main() {
|
|
|
2826
2976
|
"Workflows/Journey.md",
|
|
2827
2977
|
"Tools/CBrowser.ts",
|
|
2828
2978
|
];
|
|
2979
|
+
const downloaded = new Map();
|
|
2980
|
+
const failed = [];
|
|
2829
2981
|
for (const file of files) {
|
|
2830
2982
|
try {
|
|
2831
2983
|
const url = `${repoUrl}/skill/${file}`;
|
|
2832
|
-
const dest = path.join(skillDir, file);
|
|
2833
2984
|
console.log(` - ${file}`);
|
|
2834
|
-
const response = await fetch(url);
|
|
2985
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(30_000) });
|
|
2835
2986
|
if (!response.ok) {
|
|
2836
2987
|
throw new Error(`HTTP ${response.status}`);
|
|
2837
2988
|
}
|
|
2838
|
-
|
|
2839
|
-
fs.writeFileSync(dest, content, "utf-8");
|
|
2989
|
+
downloaded.set(file, await response.text());
|
|
2840
2990
|
}
|
|
2841
2991
|
catch (e) {
|
|
2842
|
-
|
|
2992
|
+
failed.push(`${file} (${e.message})`);
|
|
2843
2993
|
}
|
|
2844
2994
|
}
|
|
2995
|
+
if (failed.length > 0) {
|
|
2996
|
+
console.error(`✗ ${failed.length} of ${files.length} files could not be downloaded; the installed skill was left unchanged:`);
|
|
2997
|
+
for (const f of failed)
|
|
2998
|
+
console.error(` ${f}`);
|
|
2999
|
+
process.exit(1);
|
|
3000
|
+
}
|
|
3001
|
+
console.log(fs.existsSync(skillDir) ? "Updating the installed skill..." : "Creating skill directory structure...");
|
|
3002
|
+
fs.mkdirSync(path.join(skillDir, "Workflows"), { recursive: true });
|
|
3003
|
+
fs.mkdirSync(path.join(skillDir, "Tools"), { recursive: true });
|
|
3004
|
+
fs.mkdirSync(path.join(skillDir, ".memory", "sessions"), { recursive: true });
|
|
3005
|
+
for (const [file, content] of downloaded) {
|
|
3006
|
+
fs.writeFileSync(path.join(skillDir, file), content, "utf-8");
|
|
3007
|
+
}
|
|
2845
3008
|
console.log(`
|
|
2846
3009
|
╔═══════════════════════════════════════════════════════════════╗
|
|
2847
3010
|
║ CBrowser Skill Installed Successfully! ║
|
|
@@ -2868,9 +3031,69 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
2868
3031
|
const port = parseInt(options.port) || 9222;
|
|
2869
3032
|
switch (subCommand) {
|
|
2870
3033
|
case "start": {
|
|
3034
|
+
// The daemon's browser takes its device and size from here. Both are
|
|
3035
|
+
// checked now: a bad value inside the detached daemon would only show
|
|
3036
|
+
// up as a daemon that failed to start.
|
|
3037
|
+
const launchArgs = [];
|
|
3038
|
+
if (typeof options.device === "string")
|
|
3039
|
+
launchArgs.push("--device", resolveDevicePreset(options.device));
|
|
3040
|
+
if (typeof options.viewport === "string") {
|
|
3041
|
+
try {
|
|
3042
|
+
parseViewportList(options.viewport);
|
|
3043
|
+
}
|
|
3044
|
+
catch (e) {
|
|
3045
|
+
console.error(`Error: ${e.message}`);
|
|
3046
|
+
process.exit(1);
|
|
3047
|
+
}
|
|
3048
|
+
launchArgs.push("--viewport", options.viewport);
|
|
3049
|
+
}
|
|
3050
|
+
// The rest of the shared browser settings. daemon start used to forward
|
|
3051
|
+
// only --device and --viewport, so --browser firefox or --no-headless
|
|
3052
|
+
// silently gave a headless chromium daemon.
|
|
3053
|
+
if (typeof options.browser === "string") {
|
|
3054
|
+
if (!["chromium", "firefox", "webkit"].includes(options.browser)) {
|
|
3055
|
+
console.error(`Error: --browser must be chromium, firefox or webkit, got: ${options.browser}`);
|
|
3056
|
+
process.exit(1);
|
|
3057
|
+
}
|
|
3058
|
+
launchArgs.push("--browser", options.browser);
|
|
3059
|
+
}
|
|
3060
|
+
if (options.headless === false)
|
|
3061
|
+
launchArgs.push("--no-headless");
|
|
3062
|
+
for (const flag of ["locale", "timezone"]) {
|
|
3063
|
+
if (typeof options[flag] === "string")
|
|
3064
|
+
launchArgs.push(`--${flag}`, options[flag]);
|
|
3065
|
+
}
|
|
3066
|
+
if (typeof options.geo === "string") {
|
|
3067
|
+
if (!parseGeoLocation(options.geo)) {
|
|
3068
|
+
console.error(`Error: invalid --geo: ${options.geo} (a preset name or lat,lon)`);
|
|
3069
|
+
process.exit(1);
|
|
3070
|
+
}
|
|
3071
|
+
launchArgs.push("--geo", options.geo);
|
|
3072
|
+
}
|
|
3073
|
+
// --timeout: a bare number is minutes, as the help has always said;
|
|
3074
|
+
// 90s / 45m / 2h also work. It used to be accepted and ignored.
|
|
3075
|
+
if (options.timeout !== undefined) {
|
|
3076
|
+
const raw = String(options.timeout).trim();
|
|
3077
|
+
let ms;
|
|
3078
|
+
try {
|
|
3079
|
+
ms = /^\d+(\.\d+)?$/.test(raw) ? Math.round(parseFloat(raw) * 60_000) : parseDurationMs(raw);
|
|
3080
|
+
}
|
|
3081
|
+
catch {
|
|
3082
|
+
console.error(`Error: --timeout must be a positive number of minutes or a duration (90s, 45m, 2h), got "${raw}"`);
|
|
3083
|
+
process.exit(1);
|
|
3084
|
+
}
|
|
3085
|
+
if (!(ms > 0)) {
|
|
3086
|
+
console.error(`Error: --timeout must be greater than zero, got: ${raw}`);
|
|
3087
|
+
process.exit(1);
|
|
3088
|
+
}
|
|
3089
|
+
launchArgs.push("--idle-timeout-ms", String(ms));
|
|
3090
|
+
}
|
|
2871
3091
|
console.log("🚀 Starting CBrowser daemon...");
|
|
2872
|
-
const result = await startDaemon(port);
|
|
3092
|
+
const result = await startDaemon(port, launchArgs);
|
|
2873
3093
|
console.log(result.success ? `✓ ${result.message}` : `✗ ${result.message}`);
|
|
3094
|
+
if (result.alreadyRunning && launchArgs.length > 0) {
|
|
3095
|
+
console.error("⚠ The browser flags were not applied: the daemon was already running. Run cbrowser daemon stop first.");
|
|
3096
|
+
}
|
|
2874
3097
|
process.exit(result.success ? 0 : 1);
|
|
2875
3098
|
break;
|
|
2876
3099
|
}
|
|
@@ -2878,13 +3101,14 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
2878
3101
|
console.log("🛑 Stopping CBrowser daemon...");
|
|
2879
3102
|
const result = await stopDaemon();
|
|
2880
3103
|
console.log(result.success ? `✓ ${result.message}` : `✗ ${result.message}`);
|
|
2881
|
-
process.exit(0);
|
|
3104
|
+
process.exit(result.success ? 0 : 1);
|
|
2882
3105
|
break;
|
|
2883
3106
|
}
|
|
2884
3107
|
case "status": {
|
|
2885
3108
|
const status = await getDaemonStatus();
|
|
2886
3109
|
console.log(status);
|
|
2887
|
-
|
|
3110
|
+
// Non-zero when nothing is running, so `cbrowser daemon status && ...` works.
|
|
3111
|
+
process.exit((await isDaemonRunning()) ? 0 : 1);
|
|
2888
3112
|
break;
|
|
2889
3113
|
}
|
|
2890
3114
|
case "run": {
|
|
@@ -2894,10 +3118,16 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
2894
3118
|
: options.browser === "webkit" ? "webkit"
|
|
2895
3119
|
: "chromium";
|
|
2896
3120
|
// runDaemonServer will merge with defaults internally
|
|
3121
|
+
const runViewport = typeof options.viewport === "string" ? parseViewportList(options.viewport)[0] : undefined;
|
|
2897
3122
|
await runDaemonServer({
|
|
2898
3123
|
browser: browserType,
|
|
2899
3124
|
headless: options.headless !== false && options.headless !== "false",
|
|
2900
|
-
|
|
3125
|
+
...(typeof options.device === "string" ? { device: options.device } : {}),
|
|
3126
|
+
...(runViewport ? { viewportWidth: runViewport.width, viewportHeight: runViewport.height, viewportOverride: runViewport } : {}),
|
|
3127
|
+
...(typeof options.locale === "string" ? { locale: options.locale } : {}),
|
|
3128
|
+
...(typeof options.timezone === "string" ? { timezone: options.timezone } : {}),
|
|
3129
|
+
...(typeof options.geo === "string" && parseGeoLocation(options.geo) ? { geolocation: parseGeoLocation(options.geo) } : {}),
|
|
3130
|
+
}, port, typeof options["idle-timeout-ms"] === "string" ? Number(options["idle-timeout-ms"]) : undefined);
|
|
2901
3131
|
return;
|
|
2902
3132
|
}
|
|
2903
3133
|
default:
|
|
@@ -2918,21 +3148,35 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
2918
3148
|
"keyboard", "press", "type", "evaluate", "eval",
|
|
2919
3149
|
];
|
|
2920
3150
|
if (daemonRunning && INTERACTION_COMMANDS.includes(command)) {
|
|
2921
|
-
|
|
2922
|
-
|
|
3151
|
+
// Status on stderr: stdout carries only the command's result, so piping
|
|
3152
|
+
// it (--raw, --json) works the same with and without a daemon.
|
|
3153
|
+
console.error("🔌 Connected to running daemon");
|
|
3154
|
+
// The daemon's browser keeps the settings it started with; a flag on a
|
|
3155
|
+
// command it runs cannot change them, so say so instead of ignoring it.
|
|
3156
|
+
const browserFlags = ["device", "viewport", "browser", "headless", "locale", "timezone", "geo"]
|
|
3157
|
+
.filter((f) => options[f] !== undefined);
|
|
3158
|
+
if (browserFlags.length > 0) {
|
|
3159
|
+
console.error(`⚠ ${browserFlags.map((f) => `--${f}`).join(", ")} do not apply to commands the daemon runs: its browser keeps the settings it started with.`);
|
|
3160
|
+
console.error(" To change them: cbrowser daemon stop, then cbrowser daemon start with those flags");
|
|
3161
|
+
}
|
|
3162
|
+
// The daemon knows "evaluate"; "eval" reached it unchanged and failed.
|
|
3163
|
+
const daemonCommand = command === "eval" ? "evaluate" : command;
|
|
2923
3164
|
let daemonArgs = {};
|
|
3165
|
+
// --url means "act on this page" for every routed command; the daemon
|
|
3166
|
+
// navigates first, as the local path does.
|
|
3167
|
+
const urlArg = typeof options.url === "string" ? { url: options.url } : {};
|
|
2924
3168
|
switch (command) {
|
|
2925
3169
|
case "navigate":
|
|
2926
3170
|
daemonArgs = { url: args[0] || options.url };
|
|
2927
3171
|
break;
|
|
2928
3172
|
case "click":
|
|
2929
|
-
daemonArgs = { selector: args[0], force: options.force === true };
|
|
3173
|
+
daemonArgs = { selector: args[0], force: options.force === true, ...urlArg };
|
|
2930
3174
|
break;
|
|
2931
3175
|
case "hover":
|
|
2932
|
-
daemonArgs = { selector: args[0] };
|
|
3176
|
+
daemonArgs = { selector: args[0], ...urlArg };
|
|
2933
3177
|
break;
|
|
2934
3178
|
case "fill":
|
|
2935
|
-
daemonArgs = { selector: args[0], value: args[1] };
|
|
3179
|
+
daemonArgs = { selector: args[0], value: args[1], ...urlArg };
|
|
2936
3180
|
break;
|
|
2937
3181
|
case "keyboard": {
|
|
2938
3182
|
validateCommandFlags("keyboard", options);
|
|
@@ -2954,42 +3198,45 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
2954
3198
|
process.exit(1);
|
|
2955
3199
|
}
|
|
2956
3200
|
}
|
|
3201
|
+
const { delay, repeat } = parseKeyboardNumbers(options);
|
|
2957
3202
|
daemonArgs = {
|
|
2958
3203
|
steps,
|
|
2959
3204
|
hold,
|
|
2960
|
-
delay
|
|
2961
|
-
repeat
|
|
3205
|
+
delay,
|
|
3206
|
+
repeat,
|
|
2962
3207
|
selector: typeof options.selector === "string" ? options.selector : undefined,
|
|
2963
3208
|
force: options.force === true,
|
|
3209
|
+
...urlArg,
|
|
2964
3210
|
};
|
|
2965
3211
|
break;
|
|
2966
3212
|
}
|
|
2967
3213
|
case "press":
|
|
2968
|
-
daemonArgs = { key: args[0], force: options.force === true };
|
|
3214
|
+
daemonArgs = { key: args[0], force: options.force === true, ...urlArg };
|
|
2969
3215
|
break;
|
|
2970
|
-
case "type":
|
|
2971
|
-
|
|
3216
|
+
case "type": {
|
|
3217
|
+
const delay = options.delay !== undefined ? Number(options.delay) : 50;
|
|
3218
|
+
if (!Number.isFinite(delay) || delay < 0) {
|
|
3219
|
+
console.error(`Error: --delay must be a number of milliseconds, got: ${options.delay}`);
|
|
3220
|
+
process.exit(1);
|
|
3221
|
+
}
|
|
3222
|
+
daemonArgs = { text: args[0], delay, force: options.force === true, ...urlArg };
|
|
2972
3223
|
break;
|
|
3224
|
+
}
|
|
2973
3225
|
case "eval":
|
|
2974
3226
|
case "evaluate": {
|
|
2975
3227
|
validateCommandFlags(command, options);
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
console.error(`✗ ${refusal}`);
|
|
2989
|
-
console.error(" Re-run with --force to run it anyway.");
|
|
2990
|
-
process.exit(1);
|
|
2991
|
-
}
|
|
2992
|
-
daemonArgs = { body: `return (function(){ ${/\breturn\b/.test(script) ? script : `return (${script});`} })();`, args: [], force: true };
|
|
3228
|
+
// Same validation as the local path (prepareEvaluate), then the whole
|
|
3229
|
+
// request: refused here and again daemon-side, since anything on this
|
|
3230
|
+
// machine can POST to the daemon. The daemon compiles the raw script
|
|
3231
|
+
// with the same evaluateScript as the local path.
|
|
3232
|
+
const prep = await prepareEvaluate(args, options);
|
|
3233
|
+
daemonArgs = {
|
|
3234
|
+
script: prep.script,
|
|
3235
|
+
args: prep.evalArgs,
|
|
3236
|
+
force: true,
|
|
3237
|
+
...(typeof options.url === "string" ? { url: options.url } : {}),
|
|
3238
|
+
...(prep.waitFor ? { waitFor: prep.waitFor, timeout: prep.waitMs } : {}),
|
|
3239
|
+
};
|
|
2993
3240
|
break;
|
|
2994
3241
|
}
|
|
2995
3242
|
case "screenshot": {
|
|
@@ -3005,7 +3252,9 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
3005
3252
|
ssPath = args[0];
|
|
3006
3253
|
}
|
|
3007
3254
|
}
|
|
3008
|
-
|
|
3255
|
+
// Resolved here: the daemon's cwd is wherever `daemon start` ran, so a
|
|
3256
|
+
// relative path used to land there instead of where the user is.
|
|
3257
|
+
daemonArgs = { path: ssPath ? resolve(ssPath) : undefined, url: ssUrl || options.url };
|
|
3009
3258
|
break;
|
|
3010
3259
|
}
|
|
3011
3260
|
case "extract": {
|
|
@@ -3020,17 +3269,30 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
3020
3269
|
extractWhat = args[0];
|
|
3021
3270
|
}
|
|
3022
3271
|
}
|
|
3272
|
+
if (!EXTRACT_KINDS.includes(extractWhat.toLowerCase())) {
|
|
3273
|
+
console.error(`Error: extract takes one of: ${EXTRACT_KINDS.join(", ")} (got "${extractWhat}")`);
|
|
3274
|
+
process.exit(1);
|
|
3275
|
+
}
|
|
3023
3276
|
daemonArgs = { what: extractWhat, url: extractUrl || options.url };
|
|
3024
3277
|
break;
|
|
3025
3278
|
}
|
|
3026
3279
|
case "run":
|
|
3027
|
-
daemonArgs = { command: args.join(" ") };
|
|
3280
|
+
daemonArgs = { command: args.join(" "), ...urlArg };
|
|
3028
3281
|
break;
|
|
3029
3282
|
}
|
|
3030
3283
|
const result = await sendToDaemon(daemonCommand, daemonArgs);
|
|
3031
3284
|
if (result.success) {
|
|
3032
|
-
console.
|
|
3033
|
-
if (
|
|
3285
|
+
console.error("✓ Command executed via daemon");
|
|
3286
|
+
if (daemonCommand === "evaluate") {
|
|
3287
|
+
// The same output and --expect-truthy rule as the local path; the
|
|
3288
|
+
// daemon used to print strings JSON-quoted.
|
|
3289
|
+
printEvaluateResult(result.result, options);
|
|
3290
|
+
}
|
|
3291
|
+
else if (daemonCommand === "extract" && result.result && typeof result.result === "object" && "data" in result.result) {
|
|
3292
|
+
// As the local path prints it: the extracted data, not the wrapper.
|
|
3293
|
+
printData("extract", result.result.data);
|
|
3294
|
+
}
|
|
3295
|
+
else if (result.result) {
|
|
3034
3296
|
console.log(JSON.stringify(result.result, null, 2));
|
|
3035
3297
|
}
|
|
3036
3298
|
}
|
|
@@ -3092,15 +3354,17 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
3092
3354
|
process.exit(1);
|
|
3093
3355
|
}
|
|
3094
3356
|
}
|
|
3357
|
+
// Only what the user passed: an unset flag leaves the env var or config file
|
|
3358
|
+
// value in place (mergeConfig drops undefined keys too).
|
|
3095
3359
|
const browser = new CBrowser({
|
|
3096
|
-
browser: browserType,
|
|
3097
|
-
headless,
|
|
3360
|
+
...(options.browser !== undefined ? { browser: browserType } : {}),
|
|
3361
|
+
...(options.headless !== undefined ? { headless } : {}),
|
|
3098
3362
|
device: options.device,
|
|
3099
|
-
...(viewportOverride ? { viewportWidth: viewportOverride.width, viewportHeight: viewportOverride.height } : {}),
|
|
3363
|
+
...(viewportOverride ? { viewportWidth: viewportOverride.width, viewportHeight: viewportOverride.height, viewportOverride } : {}),
|
|
3100
3364
|
geolocation,
|
|
3101
3365
|
locale: options.locale,
|
|
3102
3366
|
timezone: options.timezone,
|
|
3103
|
-
recordVideo: options["record-video"] === true,
|
|
3367
|
+
...(options["record-video"] !== undefined ? { recordVideo: options["record-video"] === true } : {}),
|
|
3104
3368
|
persistent: persistentMode,
|
|
3105
3369
|
skipSessionRestore: skipRestore,
|
|
3106
3370
|
verbose: true,
|
|
@@ -3114,7 +3378,14 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
3114
3378
|
process.exit(1);
|
|
3115
3379
|
}
|
|
3116
3380
|
const result = await browser.navigate(url);
|
|
3117
|
-
|
|
3381
|
+
// A navigation that landed somewhere else (an interstitial, a
|
|
3382
|
+
// cross-host redirect) reports success: false; it used to print
|
|
3383
|
+
// "Navigated to" and exit 0.
|
|
3384
|
+
if (result.success === false) {
|
|
3385
|
+
console.error(`✗ Navigation to ${url} did not land on that page (now at ${result.url})`);
|
|
3386
|
+
process.exitCode = 1;
|
|
3387
|
+
}
|
|
3388
|
+
console.log(`${result.success === false ? "✗" : "✓"} Navigated to: ${result.url}`);
|
|
3118
3389
|
console.log(` Title: ${result.title}`);
|
|
3119
3390
|
console.log(` Load time: ${result.loadTime}ms`);
|
|
3120
3391
|
console.log(` Screenshot: ${result.screenshot}`);
|
|
@@ -3185,7 +3456,13 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
3185
3456
|
if (options.url)
|
|
3186
3457
|
await browser.navigate(options.url);
|
|
3187
3458
|
const page = await browser.getPage();
|
|
3188
|
-
|
|
3459
|
+
// parseArgs yields strings, so the old `typeof === "number"` test was
|
|
3460
|
+
// never true and --delay was always 50 ms (audit 2026-10-21).
|
|
3461
|
+
const delay = typeof options.delay === "string" ? Number(options.delay) : 50;
|
|
3462
|
+
if (!Number.isFinite(delay) || delay < 0) {
|
|
3463
|
+
console.error(`Error: --delay must be a number of milliseconds, got: ${options.delay}`);
|
|
3464
|
+
process.exit(1);
|
|
3465
|
+
}
|
|
3189
3466
|
const refusal = await browser.keystrokeRedZone({ text }, options.force === true);
|
|
3190
3467
|
if (refusal) {
|
|
3191
3468
|
console.error(`✗ ${refusal}`);
|
|
@@ -3305,8 +3582,15 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
3305
3582
|
if (url) {
|
|
3306
3583
|
await browser.navigate(url);
|
|
3307
3584
|
}
|
|
3585
|
+
// Five kinds exist. Anything else used to return the whole page text, so
|
|
3586
|
+
// a typo ("link") or a description ("all product names") looked like a
|
|
3587
|
+
// result (audit 2026-10-21).
|
|
3588
|
+
if (!EXTRACT_KINDS.includes(what.toLowerCase())) {
|
|
3589
|
+
console.error(`Error: extract takes one of: ${EXTRACT_KINDS.join(", ")} (got "${what}")`);
|
|
3590
|
+
process.exit(1);
|
|
3591
|
+
}
|
|
3308
3592
|
const result = await browser.extract(what);
|
|
3309
|
-
|
|
3593
|
+
printData("extract", result.data);
|
|
3310
3594
|
break;
|
|
3311
3595
|
}
|
|
3312
3596
|
// =========================================================================
|
|
@@ -3344,7 +3628,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
3344
3628
|
else {
|
|
3345
3629
|
console.error(`\n✗ ${result.message}`);
|
|
3346
3630
|
if (result.aiSuggestion) {
|
|
3347
|
-
console.
|
|
3631
|
+
console.error(`\n💡 Suggestion:\n${result.aiSuggestion}`);
|
|
3348
3632
|
}
|
|
3349
3633
|
process.exit(1);
|
|
3350
3634
|
}
|
|
@@ -3511,6 +3795,11 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
3511
3795
|
console.log(`\n🧪 Analyzing page and generating tests...\n`);
|
|
3512
3796
|
const result = await browser.generateTests(url);
|
|
3513
3797
|
const format = options.format || "summary";
|
|
3798
|
+
// An unknown format printed nothing and exited 0.
|
|
3799
|
+
if (!["summary", "cbrowser", "playwright", "all"].includes(format)) {
|
|
3800
|
+
console.error(`Error: --format must be summary, cbrowser, playwright or all, got: ${format}`);
|
|
3801
|
+
process.exit(1);
|
|
3802
|
+
}
|
|
3514
3803
|
const outputFile = options.output;
|
|
3515
3804
|
let output = "";
|
|
3516
3805
|
if (format === "summary" || format === "all") {
|
|
@@ -3562,6 +3851,17 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
3562
3851
|
}
|
|
3563
3852
|
await browser.navigate(url);
|
|
3564
3853
|
const analysis = await browser.analyzePage();
|
|
3854
|
+
// --output was accepted and ignored, so the GitHub Action's url mode
|
|
3855
|
+
// (`analyze "$URL" --output .../analysis.json`) never wrote its file.
|
|
3856
|
+
if (typeof options.output === "string") {
|
|
3857
|
+
const { writeFileSync } = await import("node:fs");
|
|
3858
|
+
writeFileSync(options.output, JSON.stringify(analysis, null, 2));
|
|
3859
|
+
console.error(`📄 Analysis saved: ${options.output}`);
|
|
3860
|
+
}
|
|
3861
|
+
if (getOutputOptions().json) {
|
|
3862
|
+
printData("analyze", analysis);
|
|
3863
|
+
break;
|
|
3864
|
+
}
|
|
3565
3865
|
console.log(`\n📊 Page Analysis: ${analysis.url}\n`);
|
|
3566
3866
|
console.log(`Title: ${analysis.title}`);
|
|
3567
3867
|
console.log(`\n📝 Forms (${analysis.forms.length}):`);
|
|
@@ -3613,20 +3913,28 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
3613
3913
|
console.warn("⚠️ 'journey' is deprecated. Use 'explore' instead.\n");
|
|
3614
3914
|
}
|
|
3615
3915
|
const persona = args[0] || "first-timer";
|
|
3616
|
-
|
|
3916
|
+
// --url works too (audit F25: four spellings of "start URL").
|
|
3917
|
+
const startUrl = (options.start ?? options.url);
|
|
3617
3918
|
const goal = options.goal || "Explore the site";
|
|
3618
3919
|
if (!startUrl) {
|
|
3619
3920
|
console.error("Error: --start URL required");
|
|
3620
3921
|
process.exit(1);
|
|
3621
3922
|
}
|
|
3923
|
+
// 20.0.0: no keyless mode. The heuristic explorer reported success
|
|
3924
|
+
// whenever any goal word ("the" included) was on the page (2026-10-21).
|
|
3925
|
+
if (!isApiKeyConfigured()) {
|
|
3926
|
+
console.error("Error: explore needs an Anthropic API key (cbrowser config set-api-key <key>). The keyless heuristic mode was removed in 20.0.0 because its results were not accurate.");
|
|
3927
|
+
process.exit(1);
|
|
3928
|
+
}
|
|
3622
3929
|
console.log(`🔍 Starting exploration as "${persona}"...`);
|
|
3623
3930
|
console.log(` Goal: ${goal}`);
|
|
3624
|
-
console.log(` Mode: Heuristic (free, no API)`);
|
|
3625
3931
|
console.log("");
|
|
3626
3932
|
const result = await browser.journey({ persona, startUrl, goal });
|
|
3627
3933
|
console.log("");
|
|
3628
3934
|
console.log(`📊 Journey Results`);
|
|
3629
3935
|
console.log(` Success: ${result.success ? "✓" : "✗"}`);
|
|
3936
|
+
if (!result.success)
|
|
3937
|
+
process.exitCode = 1;
|
|
3630
3938
|
console.log(` Steps: ${result.steps.length}`);
|
|
3631
3939
|
console.log(` Time: ${result.totalTime}ms`);
|
|
3632
3940
|
console.log(` Friction points: ${result.frictionPoints.length}`);
|
|
@@ -3661,12 +3969,18 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
3661
3969
|
const concurrency = options.concurrency
|
|
3662
3970
|
? parseInt(options.concurrency)
|
|
3663
3971
|
: 3;
|
|
3972
|
+
const personaFlags = browserFlagsOrExit("compare-personas", options);
|
|
3973
|
+
refuseBrowserFlags("compare-personas", personaFlags, {
|
|
3974
|
+
browser: "it runs cognitive journeys, which take no engine option",
|
|
3975
|
+
});
|
|
3664
3976
|
const comparison = await comparePersonas({
|
|
3665
3977
|
startUrl,
|
|
3666
3978
|
goal,
|
|
3667
3979
|
personas: personaNames,
|
|
3668
3980
|
maxConcurrency: concurrency,
|
|
3669
3981
|
headless,
|
|
3982
|
+
device: personaFlags.device,
|
|
3983
|
+
viewport: personaFlags.viewport,
|
|
3670
3984
|
});
|
|
3671
3985
|
// Print formatted report
|
|
3672
3986
|
const report = formatComparisonReport(comparison);
|
|
@@ -3681,7 +3995,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
3681
3995
|
if (options.html) {
|
|
3682
3996
|
const fs = await import("fs");
|
|
3683
3997
|
const htmlReport = generateHtmlReport(comparison);
|
|
3684
|
-
const htmlPath = options.output
|
|
3998
|
+
const htmlPath = htmlReportPath(options.output, "comparison-report.html");
|
|
3685
3999
|
fs.writeFileSync(htmlPath, htmlReport);
|
|
3686
4000
|
console.log(`\n🌐 HTML report saved: ${htmlPath}`);
|
|
3687
4001
|
}
|
|
@@ -3780,7 +4094,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
3780
4094
|
// Save the persona
|
|
3781
4095
|
const filepath = saveCustomPersona(persona);
|
|
3782
4096
|
console.log(`\n✓ Persona saved: ${filepath}`);
|
|
3783
|
-
console.log(`\nUse with: cbrowser journey ${personaName} --start <url> --goal "<goal>"`);
|
|
4097
|
+
console.log(`\nUse with: cbrowser cognitive-journey --persona ${personaName} --start <url> --goal "<goal>"`);
|
|
3784
4098
|
break;
|
|
3785
4099
|
}
|
|
3786
4100
|
case "show": {
|
|
@@ -3923,6 +4237,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
3923
4237
|
console.error(" cbrowser persona delete my-custom-persona");
|
|
3924
4238
|
console.error(" cbrowser persona cleanup --dry-run # Preview invalid personas");
|
|
3925
4239
|
console.error(" cbrowser persona cleanup # Remove invalid personas");
|
|
4240
|
+
process.exit(1);
|
|
3926
4241
|
}
|
|
3927
4242
|
break;
|
|
3928
4243
|
}
|
|
@@ -4103,6 +4418,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4103
4418
|
}
|
|
4104
4419
|
else {
|
|
4105
4420
|
console.error(`✗ Session not found: ${name}`);
|
|
4421
|
+
process.exit(1);
|
|
4106
4422
|
}
|
|
4107
4423
|
break;
|
|
4108
4424
|
}
|
|
@@ -4116,6 +4432,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4116
4432
|
console.error(" cleanup --older-than <days> Delete old sessions");
|
|
4117
4433
|
console.error(" export <name> --output <file> Export session to file");
|
|
4118
4434
|
console.error(" import <file> --name <name> Import session from file");
|
|
4435
|
+
process.exit(1);
|
|
4119
4436
|
}
|
|
4120
4437
|
break;
|
|
4121
4438
|
}
|
|
@@ -4231,6 +4548,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4231
4548
|
}
|
|
4232
4549
|
default:
|
|
4233
4550
|
console.error("Usage: cbrowser cookie [list|set|delete|clear]");
|
|
4551
|
+
process.exit(1);
|
|
4234
4552
|
}
|
|
4235
4553
|
break;
|
|
4236
4554
|
}
|
|
@@ -4241,14 +4559,18 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4241
4559
|
const subcommand = args[0];
|
|
4242
4560
|
switch (subcommand) {
|
|
4243
4561
|
case "list": {
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4562
|
+
// One line per preset: there are a few hundred now. An optional
|
|
4563
|
+
// filter matches the key or the display name.
|
|
4564
|
+
const filter = (args[1] ?? "").toLowerCase();
|
|
4565
|
+
const rows = Object.entries(DEVICE_PRESETS).filter(([key, d]) => !filter || key.includes(filter) || d.name.toLowerCase().includes(filter));
|
|
4566
|
+
const width = Math.max(...rows.map(([key]) => key.length), 4);
|
|
4567
|
+
console.log(`\n📱 Device presets${filter ? ` matching "${filter}"` : ""}: ${rows.length}\n`);
|
|
4568
|
+
for (const [key, d] of rows) {
|
|
4569
|
+
const kind = d.isMobile ? "mobile" : "desktop";
|
|
4570
|
+
const touch = d.hasTouch ? ", touch" : "";
|
|
4571
|
+
console.log(` ${key.padEnd(width)} ${d.viewport.width}x${d.viewport.height} @${d.deviceScaleFactor}x ${kind}${touch} (${d.name})`);
|
|
4572
|
+
}
|
|
4573
|
+
console.log("\n Case and spaces do not matter: --device \"iPhone 16\" is iphone-16.");
|
|
4252
4574
|
break;
|
|
4253
4575
|
}
|
|
4254
4576
|
case "set": {
|
|
@@ -4257,20 +4579,17 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4257
4579
|
console.error("Usage: cbrowser device set <name>");
|
|
4258
4580
|
process.exit(1);
|
|
4259
4581
|
}
|
|
4260
|
-
|
|
4261
|
-
console.error(`Unknown device: ${deviceName}`);
|
|
4262
|
-
console.error("Run 'cbrowser device list' to see available devices");
|
|
4263
|
-
process.exit(1);
|
|
4264
|
-
}
|
|
4582
|
+
const presetKey = resolveDevicePreset(deviceName);
|
|
4265
4583
|
// Save device setting to persistent session state
|
|
4266
4584
|
const deviceBrowser = new CBrowser({ persistent: true, headless: true, verbose: false });
|
|
4267
|
-
deviceBrowser.saveDeviceSetting(
|
|
4268
|
-
console.log(`✓ Device set: ${
|
|
4585
|
+
deviceBrowser.saveDeviceSetting(presetKey);
|
|
4586
|
+
console.log(`✓ Device set: ${presetKey}`);
|
|
4269
4587
|
console.log(" Device emulation will apply to all subsequent commands");
|
|
4270
4588
|
break;
|
|
4271
4589
|
}
|
|
4272
4590
|
default:
|
|
4273
4591
|
console.error("Usage: cbrowser device [list|set]");
|
|
4592
|
+
process.exit(1);
|
|
4274
4593
|
}
|
|
4275
4594
|
break;
|
|
4276
4595
|
}
|
|
@@ -4301,12 +4620,18 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4301
4620
|
console.error(`Invalid location: ${location}`);
|
|
4302
4621
|
process.exit(1);
|
|
4303
4622
|
}
|
|
4304
|
-
|
|
4623
|
+
// Saved to session state like `device set`: each command is its own
|
|
4624
|
+
// process, so a runtime change here died with this one.
|
|
4625
|
+
const geoBrowser = new CBrowser({ persistent: true, headless: true, verbose: false });
|
|
4626
|
+
if (!geoBrowser.saveGeolocationSetting(geo))
|
|
4627
|
+
process.exit(1);
|
|
4305
4628
|
console.log(`✓ Geolocation set: ${geo.latitude}, ${geo.longitude}`);
|
|
4629
|
+
console.log(" Applies to later commands in the persistent session (for an hour, like device set)");
|
|
4306
4630
|
break;
|
|
4307
4631
|
}
|
|
4308
4632
|
default:
|
|
4309
4633
|
console.error("Usage: cbrowser geo [list|set]");
|
|
4634
|
+
process.exit(1);
|
|
4310
4635
|
}
|
|
4311
4636
|
break;
|
|
4312
4637
|
}
|
|
@@ -4335,6 +4660,8 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4335
4660
|
console.log("\n📊 Performance Audit:\n");
|
|
4336
4661
|
console.log(` URL: ${result.url}`);
|
|
4337
4662
|
console.log(` Result: ${result.passed ? "✓ PASSED" : "✗ FAILED"}`);
|
|
4663
|
+
if (!result.passed)
|
|
4664
|
+
process.exitCode = 1;
|
|
4338
4665
|
console.log("");
|
|
4339
4666
|
console.log(" Metrics:");
|
|
4340
4667
|
if (result.metrics.lcp)
|
|
@@ -4396,7 +4723,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4396
4723
|
const subcommand = args[0];
|
|
4397
4724
|
if (subcommand !== "start" && subcommand !== "stop") {
|
|
4398
4725
|
console.error("Usage: cbrowser har [start|stop]");
|
|
4399
|
-
|
|
4726
|
+
process.exit(1);
|
|
4400
4727
|
}
|
|
4401
4728
|
// A recording has to span several commands, and each cbrowser command is
|
|
4402
4729
|
// its own process: only the daemon's browser outlives them. Recorded here,
|
|
@@ -4481,7 +4808,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4481
4808
|
const subcommand = args[0];
|
|
4482
4809
|
if (subcommand !== "list" && subcommand !== "clear") {
|
|
4483
4810
|
console.error("Usage: cbrowser network [list|clear]");
|
|
4484
|
-
|
|
4811
|
+
process.exit(1);
|
|
4485
4812
|
}
|
|
4486
4813
|
// Same reason as har: the request log lives in a browser, and only the
|
|
4487
4814
|
// daemon's browser outlives a command. A fresh process always listed
|
|
@@ -4509,19 +4836,32 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4509
4836
|
}
|
|
4510
4837
|
switch (subcommand) {
|
|
4511
4838
|
case "list": {
|
|
4512
|
-
const
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4839
|
+
const all = listed.result.requests;
|
|
4840
|
+
// Newest first, as the MCP tool lists them; it used to print the
|
|
4841
|
+
// oldest of the last 20 first and offered no way past 20.
|
|
4842
|
+
const filter = typeof options.filter === "string" ? options.filter.toLowerCase() : undefined;
|
|
4843
|
+
const matching = all.filter((r) => !filter || r.url.toLowerCase().includes(filter)
|
|
4844
|
+
|| r.resourceType.toLowerCase() === filter || r.method.toLowerCase() === filter);
|
|
4845
|
+
const newestFirst = [...matching].reverse();
|
|
4846
|
+
const shown = options.all === true ? newestFirst : newestFirst.slice(0, 20);
|
|
4847
|
+
if (options.json === true || getOutputOptions().json) {
|
|
4848
|
+
printData("network", { total: all.length, matching: matching.length, requests: shown });
|
|
4849
|
+
break;
|
|
4850
|
+
}
|
|
4851
|
+
if (matching.length === 0) {
|
|
4852
|
+
console.log(filter ? `No captured request matches "${filter}"` : "No network requests captured");
|
|
4853
|
+
if (!filter)
|
|
4854
|
+
console.log("Navigate to a page first to capture requests");
|
|
4516
4855
|
}
|
|
4517
4856
|
else {
|
|
4518
|
-
console.log(`\n🌐 Network Requests (${
|
|
4519
|
-
for (const req of
|
|
4520
|
-
|
|
4857
|
+
console.log(`\n🌐 Network Requests (${matching.length}${filter ? ` of ${all.length}` : ""}, newest first):\n`);
|
|
4858
|
+
for (const req of shown) {
|
|
4859
|
+
const status = req.status !== undefined ? ` ${req.status}` : "";
|
|
4860
|
+
console.log(` ${req.method}${status} ${req.url.substring(0, 80)}${req.url.length > 80 ? "..." : ""}`);
|
|
4521
4861
|
console.log(` Type: ${req.resourceType} | Time: ${req.timestamp}`);
|
|
4522
4862
|
}
|
|
4523
|
-
if (
|
|
4524
|
-
console.log(`\n ... and ${
|
|
4863
|
+
if (matching.length > shown.length) {
|
|
4864
|
+
console.log(`\n ... and ${matching.length - shown.length} older requests (--all lists them)`);
|
|
4525
4865
|
}
|
|
4526
4866
|
}
|
|
4527
4867
|
break;
|
|
@@ -4558,13 +4898,15 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4558
4898
|
if (options.url) {
|
|
4559
4899
|
await browser.navigate(options.url);
|
|
4560
4900
|
}
|
|
4561
|
-
const threshold =
|
|
4901
|
+
const threshold = numberOption("threshold", options.threshold, 0.1, { min: 0, max: 1 });
|
|
4562
4902
|
const result = await browser.compareBaseline(name, threshold);
|
|
4563
4903
|
console.log("\n🔍 Visual Comparison:\n");
|
|
4564
4904
|
console.log(` Baseline: ${name}`);
|
|
4565
4905
|
console.log(` Difference: ${(result.diffPercentage * 100).toFixed(2)}%`);
|
|
4566
4906
|
console.log(` Threshold: ${(threshold * 100).toFixed(0)}%`);
|
|
4567
4907
|
console.log(` Result: ${result.passed ? "✓ PASSED" : "✗ FAILED"}`);
|
|
4908
|
+
if (!result.passed)
|
|
4909
|
+
process.exitCode = 1;
|
|
4568
4910
|
if (result.diffPath) {
|
|
4569
4911
|
console.log(` Diff image: ${result.diffPath}`);
|
|
4570
4912
|
}
|
|
@@ -4608,6 +4950,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4608
4950
|
}
|
|
4609
4951
|
default:
|
|
4610
4952
|
console.error("Usage: cbrowser visual [save|compare|list|delete]");
|
|
4953
|
+
process.exit(1);
|
|
4611
4954
|
}
|
|
4612
4955
|
break;
|
|
4613
4956
|
}
|
|
@@ -4616,6 +4959,12 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4616
4959
|
// =========================================================================
|
|
4617
4960
|
case "ai-visual": {
|
|
4618
4961
|
const subcommand = args[0];
|
|
4962
|
+
// A test renders the way its baseline was captured.
|
|
4963
|
+
const BASELINE_SHAPE_FLAGS = {
|
|
4964
|
+
browser: "a test renders with its baseline's engine; capture a new baseline to change it",
|
|
4965
|
+
device: "a test renders with its baseline's device; capture a new baseline to change it",
|
|
4966
|
+
viewport: "a test renders at its baseline's size; capture a new baseline to change it",
|
|
4967
|
+
};
|
|
4619
4968
|
switch (subcommand) {
|
|
4620
4969
|
case "capture": {
|
|
4621
4970
|
const url = args[1];
|
|
@@ -4623,20 +4972,18 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4623
4972
|
console.error("Usage: cbrowser ai-visual capture <url> --name <name> [options]");
|
|
4624
4973
|
console.error(" --selector <sel> Capture specific element");
|
|
4625
4974
|
console.error(" --device <device> Device emulation");
|
|
4626
|
-
console.error(" --
|
|
4627
|
-
console.error(" --height
|
|
4975
|
+
console.error(" --viewport <WxH> Page size (default: 1920x1080)");
|
|
4976
|
+
console.error(" --width/--height One side of the page size");
|
|
4977
|
+
console.error(" --browser <engine> chromium, firefox or webkit");
|
|
4628
4978
|
console.error(" --wait <ms|sel> Wait before capture");
|
|
4629
4979
|
process.exit(1);
|
|
4630
4980
|
}
|
|
4981
|
+
const flags = browserFlagsOrExit("ai-visual capture", options);
|
|
4631
4982
|
console.log(`📸 Capturing visual baseline: ${options.name}`);
|
|
4632
4983
|
console.log(` URL: ${url}`);
|
|
4633
4984
|
const baseline = await captureVisualBaseline(url, options.name, {
|
|
4634
4985
|
selector: options.selector,
|
|
4635
|
-
|
|
4636
|
-
viewport: options.width || options.height ? {
|
|
4637
|
-
width: parseInt(options.width) || 1920,
|
|
4638
|
-
height: parseInt(options.height) || 1080,
|
|
4639
|
-
} : undefined,
|
|
4986
|
+
...flags,
|
|
4640
4987
|
waitFor: options.wait ? (/^\d+$/.test(options.wait)
|
|
4641
4988
|
? parseInt(options.wait)
|
|
4642
4989
|
: options.wait) : undefined,
|
|
@@ -4644,6 +4991,10 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4644
4991
|
console.log(`\n✅ Baseline captured successfully!`);
|
|
4645
4992
|
console.log(` ID: ${baseline.id}`);
|
|
4646
4993
|
console.log(` Viewport: ${baseline.viewport.width}x${baseline.viewport.height}`);
|
|
4994
|
+
if (baseline.device)
|
|
4995
|
+
console.log(` Device: ${baseline.device}`);
|
|
4996
|
+
if (baseline.browser)
|
|
4997
|
+
console.log(` Browser: ${baseline.browser}`);
|
|
4647
4998
|
console.log(` Screenshot: ${baseline.screenshotPath}`);
|
|
4648
4999
|
break;
|
|
4649
5000
|
}
|
|
@@ -4658,13 +5009,16 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4658
5009
|
console.error(" --output <file> Save JSON report");
|
|
4659
5010
|
process.exit(1);
|
|
4660
5011
|
}
|
|
5012
|
+
const flags = browserFlagsOrExit("ai-visual test", options);
|
|
5013
|
+
refuseBrowserFlags("ai-visual test", flags, BASELINE_SHAPE_FLAGS);
|
|
4661
5014
|
console.log(`\n🔍 Running AI visual regression test...`);
|
|
4662
5015
|
console.log(` URL: ${url}`);
|
|
4663
5016
|
console.log(` Baseline: ${baselineName}\n`);
|
|
4664
5017
|
const result = await runVisualRegression(url, baselineName, {
|
|
4665
|
-
threshold:
|
|
4666
|
-
sensitivity: options.sensitivity
|
|
5018
|
+
threshold: numberOption("threshold", options.threshold, 0.9, { min: 0, max: 1 }),
|
|
5019
|
+
sensitivity: enumOption("sensitivity", options.sensitivity, ["low", "medium", "high"]) ?? "medium",
|
|
4667
5020
|
generateDiff: true,
|
|
5021
|
+
headless: flags.headless,
|
|
4668
5022
|
});
|
|
4669
5023
|
// Print report
|
|
4670
5024
|
console.log(formatVisualRegressionReport(result));
|
|
@@ -4716,10 +5070,13 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4716
5070
|
console.error(`Suite file not found: ${suiteFile}`);
|
|
4717
5071
|
process.exit(1);
|
|
4718
5072
|
}
|
|
5073
|
+
const flags = browserFlagsOrExit("ai-visual suite", options);
|
|
5074
|
+
refuseBrowserFlags("ai-visual suite", flags, BASELINE_SHAPE_FLAGS);
|
|
4719
5075
|
const suite = JSON.parse(fs.readFileSync(suiteFile, "utf-8"));
|
|
4720
5076
|
console.log(`\n🔍 Running visual regression suite: ${suite.name}`);
|
|
4721
5077
|
const result = await runVisualRegressionSuite(suite, {
|
|
4722
|
-
threshold:
|
|
5078
|
+
threshold: numberOption("threshold", options.threshold, 0.9, { min: 0, max: 1 }),
|
|
5079
|
+
headless: flags.headless,
|
|
4723
5080
|
});
|
|
4724
5081
|
// Save outputs
|
|
4725
5082
|
if (options.output && !options.html) {
|
|
@@ -4810,6 +5167,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4810
5167
|
console.error(" cbrowser ai-visual list");
|
|
4811
5168
|
console.error(" cbrowser ai-visual show <name>");
|
|
4812
5169
|
console.error(" cbrowser ai-visual delete <name>");
|
|
5170
|
+
process.exit(1);
|
|
4813
5171
|
}
|
|
4814
5172
|
break;
|
|
4815
5173
|
}
|
|
@@ -4818,6 +5176,10 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4818
5176
|
// =========================================================================
|
|
4819
5177
|
case "cross-browser": {
|
|
4820
5178
|
const subcommand = args[0];
|
|
5179
|
+
const flags = browserFlagsOrExit("cross-browser", options);
|
|
5180
|
+
refuseBrowserFlags("cross-browser", flags, {
|
|
5181
|
+
browser: "it runs each engine in turn; choose them with --browsers chromium,firefox,webkit",
|
|
5182
|
+
});
|
|
4821
5183
|
if (subcommand === "suite") {
|
|
4822
5184
|
// Cross-browser suite
|
|
4823
5185
|
const suiteFile = args[1];
|
|
@@ -4833,7 +5195,8 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4833
5195
|
process.exit(1);
|
|
4834
5196
|
}
|
|
4835
5197
|
const suite = JSON.parse(fs.readFileSync(suiteFile, "utf-8"));
|
|
4836
|
-
|
|
5198
|
+
// Flags given on the command line win over the suite file's options.
|
|
5199
|
+
const result = await runCrossBrowserSuite({ ...suite, options: { ...suite.options, ...flags } });
|
|
4837
5200
|
// Save outputs
|
|
4838
5201
|
if (options.output && !options.html) {
|
|
4839
5202
|
fs.writeFileSync(options.output, JSON.stringify(result, null, 2));
|
|
@@ -4861,8 +5224,9 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4861
5224
|
console.error(" cbrowser cross-browser suite <file.json>");
|
|
4862
5225
|
console.error("\nOptions:");
|
|
4863
5226
|
console.error(" --browsers <list> chromium,firefox,webkit (default: all)");
|
|
4864
|
-
console.error(" --
|
|
4865
|
-
console.error(" --height
|
|
5227
|
+
console.error(" --viewport <WxH> Page size (default: 1920x1080)");
|
|
5228
|
+
console.error(" --width/--height One side of the page size");
|
|
5229
|
+
console.error(" --device <name> Device preset (firefox cannot emulate a mobile one)");
|
|
4866
5230
|
console.error(" --wait <ms> Wait before screenshot");
|
|
4867
5231
|
console.error(" --wait-for <sel> Wait for selector");
|
|
4868
5232
|
console.error(" --sensitivity <l> low, medium, high");
|
|
@@ -4875,13 +5239,10 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4875
5239
|
: undefined;
|
|
4876
5240
|
const result = await runCrossBrowserTest(url, {
|
|
4877
5241
|
browsers,
|
|
4878
|
-
|
|
4879
|
-
width: parseInt(options.width) || 1920,
|
|
4880
|
-
height: parseInt(options.height) || 1080,
|
|
4881
|
-
} : undefined,
|
|
5242
|
+
...flags,
|
|
4882
5243
|
waitBeforeCapture: options.wait ? parseInt(options.wait) : undefined,
|
|
4883
5244
|
waitForSelector: options["wait-for"],
|
|
4884
|
-
sensitivity: options.sensitivity,
|
|
5245
|
+
sensitivity: enumOption("sensitivity", options.sensitivity, ["low", "medium", "high"]),
|
|
4885
5246
|
});
|
|
4886
5247
|
// Print report
|
|
4887
5248
|
console.log("\n" + formatCrossBrowserReport(result));
|
|
@@ -4920,6 +5281,19 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4920
5281
|
// =========================================================================
|
|
4921
5282
|
case "responsive": {
|
|
4922
5283
|
const subcommand = args[0];
|
|
5284
|
+
// Headless and the engine apply; the page shapes are the presets'.
|
|
5285
|
+
const responsiveFlags = () => {
|
|
5286
|
+
const flags = browserFlagsOrExit("responsive", options);
|
|
5287
|
+
refuseBrowserFlags("responsive", flags, {
|
|
5288
|
+
device: "the sizes come from --viewports, whose mobile and tablet presets emulate their own devices",
|
|
5289
|
+
viewport: "the sizes come from --viewports ('cbrowser responsive viewports' lists them)",
|
|
5290
|
+
});
|
|
5291
|
+
// Only the flags given, so a suite file's own options stand otherwise.
|
|
5292
|
+
return {
|
|
5293
|
+
...(flags.headless !== undefined ? { headless: flags.headless } : {}),
|
|
5294
|
+
...(flags.browser ? { browser: flags.browser } : {}),
|
|
5295
|
+
};
|
|
5296
|
+
};
|
|
4923
5297
|
if (subcommand === "viewports") {
|
|
4924
5298
|
// List viewport presets
|
|
4925
5299
|
const presets = listViewportPresets();
|
|
@@ -4945,6 +5319,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4945
5319
|
}
|
|
4946
5320
|
else if (subcommand === "suite") {
|
|
4947
5321
|
// Responsive suite
|
|
5322
|
+
const flags = responsiveFlags();
|
|
4948
5323
|
const suiteFile = args[1];
|
|
4949
5324
|
if (!suiteFile) {
|
|
4950
5325
|
console.error("Usage: cbrowser responsive suite <file.json> [options]");
|
|
@@ -4958,7 +5333,8 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4958
5333
|
process.exit(1);
|
|
4959
5334
|
}
|
|
4960
5335
|
const suite = JSON.parse(fs.readFileSync(suiteFile, "utf-8"));
|
|
4961
|
-
|
|
5336
|
+
// Flags given on the command line win over the suite file's options.
|
|
5337
|
+
const result = await runResponsiveSuite({ ...suite, options: { ...suite.options, ...flags } });
|
|
4962
5338
|
// Save outputs
|
|
4963
5339
|
if (options.output && !options.html) {
|
|
4964
5340
|
fs.writeFileSync(options.output, JSON.stringify(result, null, 2));
|
|
@@ -4993,19 +5369,20 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4993
5369
|
console.error(" --sensitivity <l> low, medium, high");
|
|
4994
5370
|
console.error(" --html Generate HTML report");
|
|
4995
5371
|
console.error(" --output <file> Save report");
|
|
4996
|
-
console.error("\nViewport presets:
|
|
4997
|
-
console.error("
|
|
4998
|
-
console.error(" desktop-lg, desktop-xl");
|
|
5372
|
+
console.error("\nViewport presets:");
|
|
5373
|
+
console.error(formatViewportPresetList(" "));
|
|
4999
5374
|
process.exit(1);
|
|
5000
5375
|
}
|
|
5376
|
+
const flags = responsiveFlags();
|
|
5001
5377
|
const viewports = options.viewports
|
|
5002
5378
|
? options.viewports.split(",")
|
|
5003
5379
|
: undefined;
|
|
5004
5380
|
const result = await runResponsiveTest(url, {
|
|
5005
5381
|
viewports,
|
|
5382
|
+
...flags,
|
|
5006
5383
|
waitBeforeCapture: options.wait ? parseInt(options.wait) : undefined,
|
|
5007
5384
|
waitForSelector: options["wait-for"],
|
|
5008
|
-
sensitivity: options.sensitivity,
|
|
5385
|
+
sensitivity: enumOption("sensitivity", options.sensitivity, ["low", "medium", "high"]),
|
|
5009
5386
|
});
|
|
5010
5387
|
// Print report
|
|
5011
5388
|
console.log("\n" + formatResponsiveReport(result));
|
|
@@ -5046,6 +5423,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5046
5423
|
// =========================================================================
|
|
5047
5424
|
case "ab": {
|
|
5048
5425
|
const subcommand = args[0];
|
|
5426
|
+
const flags = browserFlagsOrExit("ab", options);
|
|
5049
5427
|
if (subcommand === "suite") {
|
|
5050
5428
|
// A/B suite
|
|
5051
5429
|
const suiteFile = args[1];
|
|
@@ -5061,7 +5439,8 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5061
5439
|
process.exit(1);
|
|
5062
5440
|
}
|
|
5063
5441
|
const suite = JSON.parse(fs.readFileSync(suiteFile, "utf-8"));
|
|
5064
|
-
|
|
5442
|
+
// Flags given on the command line win over the suite file's options.
|
|
5443
|
+
const result = await runABSuite({ ...suite, options: { ...suite.options, ...flags } });
|
|
5065
5444
|
// Save outputs
|
|
5066
5445
|
if (options.output && !options.html) {
|
|
5067
5446
|
fs.writeFileSync(options.output, JSON.stringify(result, null, 2));
|
|
@@ -5091,8 +5470,9 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5091
5470
|
console.error("\nOptions:");
|
|
5092
5471
|
console.error(" --label-a <name> Label for URL A (default: 'Version A')");
|
|
5093
5472
|
console.error(" --label-b <name> Label for URL B (default: 'Version B')");
|
|
5094
|
-
console.error(" --
|
|
5095
|
-
console.error(" --height
|
|
5473
|
+
console.error(" --viewport <WxH> Page size (default: 1920x1080)");
|
|
5474
|
+
console.error(" --width/--height One side of the page size");
|
|
5475
|
+
console.error(" --device <name> Device preset");
|
|
5096
5476
|
console.error(" --wait <ms> Wait before screenshot");
|
|
5097
5477
|
console.error(" --wait-for <sel> Wait for selector");
|
|
5098
5478
|
console.error(" --sensitivity <l> low, medium, high");
|
|
@@ -5105,13 +5485,10 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5105
5485
|
a: options["label-a"] || "Version A",
|
|
5106
5486
|
b: options["label-b"] || "Version B",
|
|
5107
5487
|
} : undefined,
|
|
5108
|
-
|
|
5109
|
-
width: parseInt(options.width) || 1920,
|
|
5110
|
-
height: parseInt(options.height) || 1080,
|
|
5111
|
-
} : undefined,
|
|
5488
|
+
...flags,
|
|
5112
5489
|
waitBeforeCapture: options.wait ? parseInt(options.wait) : undefined,
|
|
5113
5490
|
waitForSelector: options["wait-for"],
|
|
5114
|
-
sensitivity: options.sensitivity,
|
|
5491
|
+
sensitivity: enumOption("sensitivity", options.sensitivity, ["low", "medium", "high"]),
|
|
5115
5492
|
});
|
|
5116
5493
|
// Print report
|
|
5117
5494
|
console.log("\n" + formatABReport(result));
|
|
@@ -5177,6 +5554,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5177
5554
|
}
|
|
5178
5555
|
else {
|
|
5179
5556
|
console.error("Usage: cbrowser a11y audit [url]");
|
|
5557
|
+
process.exit(1);
|
|
5180
5558
|
}
|
|
5181
5559
|
break;
|
|
5182
5560
|
}
|
|
@@ -5185,6 +5563,17 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5185
5563
|
// =========================================================================
|
|
5186
5564
|
case "record": {
|
|
5187
5565
|
const subcommand = args[0];
|
|
5566
|
+
// A recording has to span several commands, and each command is its own
|
|
5567
|
+
// process, so start / stop / save each saw a fresh, empty recorder: stop
|
|
5568
|
+
// always reported 0 actions and save wrote an empty test (audit
|
|
5569
|
+
// 2026-10-21, the class HAR had until 19.4.3). Only list and generate
|
|
5570
|
+
// work on their own; the rest are refused instead of pretending.
|
|
5571
|
+
if (subcommand === "start" || subcommand === "stop" || subcommand === "save") {
|
|
5572
|
+
console.error(`Error: 'record ${subcommand}' cannot capture anything: each cbrowser command is its own process, so a recording does not survive between them.`);
|
|
5573
|
+
console.error(" To record a flow, write it as a test and run it in one process:");
|
|
5574
|
+
console.error(' cbrowser test-suite --inline "go to https://example.com ; click Sign in ; verify page contains Welcome" --capture');
|
|
5575
|
+
process.exit(1);
|
|
5576
|
+
}
|
|
5188
5577
|
switch (subcommand) {
|
|
5189
5578
|
case "start": {
|
|
5190
5579
|
const url = options.url;
|
|
@@ -5261,6 +5650,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5261
5650
|
}
|
|
5262
5651
|
default:
|
|
5263
5652
|
console.error("Usage: cbrowser record [start|stop|save|list|generate]");
|
|
5653
|
+
process.exit(1);
|
|
5264
5654
|
}
|
|
5265
5655
|
break;
|
|
5266
5656
|
}
|
|
@@ -5298,31 +5688,25 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5298
5688
|
console.error("Usage: cbrowser export [junit|tap] <name> [output]");
|
|
5299
5689
|
process.exit(1);
|
|
5300
5690
|
}
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
const resultsPath = path.join(browser.getDataDir(), "results", `${name}.json`);
|
|
5305
|
-
let suite;
|
|
5306
|
-
if (fs.existsSync(resultsPath)) {
|
|
5307
|
-
suite = JSON.parse(fs.readFileSync(resultsPath, "utf-8"));
|
|
5691
|
+
if (format !== "junit" && format !== "tap") {
|
|
5692
|
+
console.error("Unknown export format. Use 'junit' or 'tap'");
|
|
5693
|
+
process.exit(1);
|
|
5308
5694
|
}
|
|
5309
|
-
|
|
5695
|
+
// The last `test-suite` run of that suite (it saves every run).
|
|
5696
|
+
const suite = loadSuiteForExport(browser.getDataDir(), name);
|
|
5697
|
+
if (!suite) {
|
|
5310
5698
|
console.error(`Test results not found: ${name}`);
|
|
5311
|
-
console.error(
|
|
5699
|
+
console.error(` Run 'cbrowser test-suite <file>' first; it saves results as ${resultsName(name)}.json under ${join(browser.getDataDir(), "results")}`);
|
|
5312
5700
|
process.exit(1);
|
|
5313
5701
|
}
|
|
5314
5702
|
if (format === "junit") {
|
|
5315
5703
|
const exportPath = browser.exportJUnit(suite, output);
|
|
5316
5704
|
console.log(`✓ JUnit XML exported: ${exportPath}`);
|
|
5317
5705
|
}
|
|
5318
|
-
else
|
|
5706
|
+
else {
|
|
5319
5707
|
const exportPath = browser.exportTAP(suite, output);
|
|
5320
5708
|
console.log(`✓ TAP exported: ${exportPath}`);
|
|
5321
5709
|
}
|
|
5322
|
-
else {
|
|
5323
|
-
console.error("Unknown export format. Use 'junit' or 'tap'");
|
|
5324
|
-
process.exit(1);
|
|
5325
|
-
}
|
|
5326
5710
|
break;
|
|
5327
5711
|
}
|
|
5328
5712
|
// =========================================================================
|
|
@@ -5331,13 +5715,9 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5331
5715
|
case "webhook": {
|
|
5332
5716
|
const subcommand = args[0];
|
|
5333
5717
|
const fs = await import("fs");
|
|
5334
|
-
const
|
|
5335
|
-
const webhooksPath = path.join(browser.getDataDir(), "webhooks.json");
|
|
5718
|
+
const hooksFile = webhooksPath(browser.getDataDir());
|
|
5336
5719
|
// Load existing webhooks
|
|
5337
|
-
let webhooks =
|
|
5338
|
-
if (fs.existsSync(webhooksPath)) {
|
|
5339
|
-
webhooks = JSON.parse(fs.readFileSync(webhooksPath, "utf-8"));
|
|
5340
|
-
}
|
|
5720
|
+
let webhooks = loadWebhooks(browser.getDataDir());
|
|
5341
5721
|
switch (subcommand) {
|
|
5342
5722
|
case "add": {
|
|
5343
5723
|
const name = args[1];
|
|
@@ -5346,14 +5726,30 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5346
5726
|
console.error("Usage: cbrowser webhook add <name> <url> [--events <events>] [--format <format>]");
|
|
5347
5727
|
process.exit(1);
|
|
5348
5728
|
}
|
|
5349
|
-
const
|
|
5350
|
-
|
|
5351
|
-
:
|
|
5352
|
-
|
|
5729
|
+
const urlProblem = webhookUrlProblem(url);
|
|
5730
|
+
if (urlProblem) {
|
|
5731
|
+
console.error(`Error: ${urlProblem}`);
|
|
5732
|
+
process.exit(1);
|
|
5733
|
+
}
|
|
5734
|
+
// Only events something sends. journey.complete was the default
|
|
5735
|
+
// and nothing ever sent it, nor test.fail (audit O17).
|
|
5736
|
+
const events = options.events === undefined
|
|
5737
|
+
? ["test.fail"]
|
|
5738
|
+
: String(options.events).split(",").map((e) => e.trim()).filter(Boolean);
|
|
5739
|
+
const unsent = events.filter((e) => !SENT_WEBHOOK_EVENTS.includes(e));
|
|
5740
|
+
if (events.length === 0 || unsent.length > 0) {
|
|
5741
|
+
console.error(`Error: no command sends ${unsent.join(", ") || "an empty event list"}. Events sent: ${SENT_WEBHOOK_EVENTS.join(", ")} (test.pass/test.fail by test-suite once per run, journey.complete by cognitive-journey)`);
|
|
5742
|
+
process.exit(1);
|
|
5743
|
+
}
|
|
5744
|
+
const format = options.format === undefined ? "generic" : String(options.format);
|
|
5745
|
+
if (!WEBHOOK_FORMATS.includes(format)) {
|
|
5746
|
+
console.error(`Error: --format must be ${WEBHOOK_FORMATS.join(", ")}, got "${format}"`);
|
|
5747
|
+
process.exit(1);
|
|
5748
|
+
}
|
|
5353
5749
|
// Remove existing webhook with same name
|
|
5354
5750
|
webhooks = webhooks.filter(w => w.name !== name);
|
|
5355
5751
|
webhooks.push({ name, url, events, format });
|
|
5356
|
-
fs.writeFileSync(
|
|
5752
|
+
fs.writeFileSync(hooksFile, JSON.stringify(webhooks, null, 2));
|
|
5357
5753
|
console.log(`✓ Webhook added: ${name}`);
|
|
5358
5754
|
console.log(` URL: ${url}`);
|
|
5359
5755
|
console.log(` Events: ${events.join(", ")}`);
|
|
@@ -5367,9 +5763,11 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5367
5763
|
else {
|
|
5368
5764
|
console.log("\n🔔 Configured Webhooks:\n");
|
|
5369
5765
|
for (const w of webhooks) {
|
|
5766
|
+
// An event no command sends (a typo, or one stored before 20.0 checked) is named as such.
|
|
5767
|
+
const unsent = w.events.filter((e) => !SENT_WEBHOOK_EVENTS.includes(e));
|
|
5370
5768
|
console.log(` ${w.name}`);
|
|
5371
5769
|
console.log(` URL: ${w.url}`);
|
|
5372
|
-
console.log(` Events: ${w.events.join(", ")}`);
|
|
5770
|
+
console.log(` Events: ${w.events.join(", ")}${unsent.length > 0 ? ` (never sent: ${unsent.join(", ")})` : ""}`);
|
|
5373
5771
|
console.log(` Format: ${w.format}`);
|
|
5374
5772
|
console.log("");
|
|
5375
5773
|
}
|
|
@@ -5385,7 +5783,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5385
5783
|
const originalLength = webhooks.length;
|
|
5386
5784
|
webhooks = webhooks.filter(w => w.name !== name);
|
|
5387
5785
|
if (webhooks.length < originalLength) {
|
|
5388
|
-
fs.writeFileSync(
|
|
5786
|
+
fs.writeFileSync(hooksFile, JSON.stringify(webhooks, null, 2));
|
|
5389
5787
|
console.log(`✓ Webhook deleted: ${name}`);
|
|
5390
5788
|
}
|
|
5391
5789
|
else {
|
|
@@ -5405,6 +5803,11 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5405
5803
|
console.error(`✗ Webhook not found: ${name}`);
|
|
5406
5804
|
process.exit(1);
|
|
5407
5805
|
}
|
|
5806
|
+
const urlProblem = webhookUrlProblem(webhook.url);
|
|
5807
|
+
if (urlProblem) {
|
|
5808
|
+
console.error(`✗ ${urlProblem}`);
|
|
5809
|
+
process.exit(1);
|
|
5810
|
+
}
|
|
5408
5811
|
// Send test notification
|
|
5409
5812
|
const testPayload = webhook.format === "slack"
|
|
5410
5813
|
? { text: "🔔 CBrowser test notification" }
|
|
@@ -5416,6 +5819,8 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5416
5819
|
method: "POST",
|
|
5417
5820
|
headers: { "Content-Type": "application/json" },
|
|
5418
5821
|
body: JSON.stringify(testPayload),
|
|
5822
|
+
redirect: "error",
|
|
5823
|
+
signal: AbortSignal.timeout(WEBHOOK_TIMEOUT_MS),
|
|
5419
5824
|
});
|
|
5420
5825
|
if (response.ok) {
|
|
5421
5826
|
console.log(`✓ Test notification sent to: ${name}`);
|
|
@@ -5433,6 +5838,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5433
5838
|
}
|
|
5434
5839
|
default:
|
|
5435
5840
|
console.error("Usage: cbrowser webhook [add|list|delete|test]");
|
|
5841
|
+
process.exit(1);
|
|
5436
5842
|
}
|
|
5437
5843
|
break;
|
|
5438
5844
|
}
|
|
@@ -5441,6 +5847,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5441
5847
|
// =========================================================================
|
|
5442
5848
|
case "parallel": {
|
|
5443
5849
|
const subcommand = args[0];
|
|
5850
|
+
const flags = browserFlagsOrExit("parallel", options);
|
|
5444
5851
|
switch (subcommand) {
|
|
5445
5852
|
case "devices": {
|
|
5446
5853
|
const url = args[1];
|
|
@@ -5448,9 +5855,14 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5448
5855
|
console.error("Usage: cbrowser parallel devices <url> [--devices <list>] [--concurrency <n>]");
|
|
5449
5856
|
process.exit(1);
|
|
5450
5857
|
}
|
|
5858
|
+
refuseBrowserFlags("parallel devices", flags, {
|
|
5859
|
+
device: "it runs every device in --devices; list them there",
|
|
5860
|
+
viewport: "each device renders at its own size",
|
|
5861
|
+
});
|
|
5862
|
+
// Without --devices, cbrowser's own presets, not all few hundred.
|
|
5451
5863
|
const deviceList = options.devices
|
|
5452
|
-
? options.devices.split(",")
|
|
5453
|
-
:
|
|
5864
|
+
? options.devices.split(",").map((d) => resolveDevicePreset(d))
|
|
5865
|
+
: [...CBROWSER_DEVICE_KEYS];
|
|
5454
5866
|
const concurrency = options.concurrency ? parseInt(options.concurrency) : 3;
|
|
5455
5867
|
console.log(`\n🚀 Running parallel device tests...`);
|
|
5456
5868
|
console.log(` URL: ${url}`);
|
|
@@ -5460,11 +5872,12 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5460
5872
|
const nav = await b.navigate(url);
|
|
5461
5873
|
const screenshot = await b.screenshot();
|
|
5462
5874
|
return { title: nav.title, loadTime: nav.loadTime, screenshot };
|
|
5463
|
-
}, { maxConcurrency: concurrency });
|
|
5875
|
+
}, { maxConcurrency: concurrency, config: browserLaunchConfig(flags) });
|
|
5464
5876
|
console.log("📊 Results:\n");
|
|
5465
5877
|
for (const r of results) {
|
|
5466
5878
|
if (r.error) {
|
|
5467
5879
|
console.log(` ✗ ${r.device}: ${r.error} (${r.duration}ms)`);
|
|
5880
|
+
process.exitCode = 1;
|
|
5468
5881
|
}
|
|
5469
5882
|
else {
|
|
5470
5883
|
console.log(` ✓ ${r.device}: ${r.result?.title} - ${r.result?.loadTime}ms (${r.duration}ms total)`);
|
|
@@ -5487,11 +5900,12 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5487
5900
|
const results = await CBrowser.parallelUrls(urls, async (b, url) => {
|
|
5488
5901
|
const nav = await b.navigate(url);
|
|
5489
5902
|
return { title: nav.title, loadTime: nav.loadTime };
|
|
5490
|
-
}, { maxConcurrency: concurrency });
|
|
5903
|
+
}, { maxConcurrency: concurrency, config: browserLaunchConfig(flags) });
|
|
5491
5904
|
console.log("📊 Results:\n");
|
|
5492
5905
|
for (const r of results) {
|
|
5493
5906
|
if (r.error) {
|
|
5494
5907
|
console.log(` ✗ ${r.url}: ${r.error}`);
|
|
5908
|
+
process.exitCode = 1;
|
|
5495
5909
|
}
|
|
5496
5910
|
else {
|
|
5497
5911
|
console.log(` ✓ ${r.url}: ${r.result?.title} (${r.result?.loadTime}ms)`);
|
|
@@ -5512,11 +5926,12 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5512
5926
|
const results = await CBrowser.parallelUrls(urls, async (b, url) => {
|
|
5513
5927
|
await b.navigate(url);
|
|
5514
5928
|
return await b.getPerformanceMetrics();
|
|
5515
|
-
}, { maxConcurrency: concurrency });
|
|
5929
|
+
}, { maxConcurrency: concurrency, config: browserLaunchConfig(flags) });
|
|
5516
5930
|
console.log("📊 Performance Results:\n");
|
|
5517
5931
|
for (const r of results) {
|
|
5518
5932
|
if (r.error) {
|
|
5519
5933
|
console.log(` ✗ ${r.url}: ${r.error}`);
|
|
5934
|
+
process.exitCode = 1;
|
|
5520
5935
|
}
|
|
5521
5936
|
else {
|
|
5522
5937
|
const m = r.result;
|
|
@@ -5533,6 +5948,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5533
5948
|
}
|
|
5534
5949
|
default:
|
|
5535
5950
|
console.error("Usage: cbrowser parallel [devices|urls|perf]");
|
|
5951
|
+
process.exit(1);
|
|
5536
5952
|
}
|
|
5537
5953
|
break;
|
|
5538
5954
|
}
|
|
@@ -5550,6 +5966,8 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5550
5966
|
process.exit(1);
|
|
5551
5967
|
}
|
|
5552
5968
|
console.log(`\n🗣️ Executing: "${nlCommand}"\n`);
|
|
5969
|
+
if (typeof options.url === "string")
|
|
5970
|
+
await browser.navigate(options.url);
|
|
5553
5971
|
const result = await executeNaturalLanguage(browser, nlCommand);
|
|
5554
5972
|
if (result.success) {
|
|
5555
5973
|
console.log(`✓ Action: ${result.action}`);
|
|
@@ -5563,6 +5981,8 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5563
5981
|
console.log(` ${r.message}`);
|
|
5564
5982
|
if (r.screenshot)
|
|
5565
5983
|
console.log(` Screenshot: ${r.screenshot}`);
|
|
5984
|
+
if (typeof r.scrollY === "number")
|
|
5985
|
+
console.log(` scrollY: ${r.scrollY}`);
|
|
5566
5986
|
}
|
|
5567
5987
|
}
|
|
5568
5988
|
else {
|
|
@@ -5593,7 +6013,8 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5593
6013
|
}
|
|
5594
6014
|
else {
|
|
5595
6015
|
console.log(`✗ ${r.command}`);
|
|
5596
|
-
|
|
6016
|
+
process.exitCode = 1;
|
|
6017
|
+
console.error(` Error: ${r.error}`);
|
|
5597
6018
|
}
|
|
5598
6019
|
}
|
|
5599
6020
|
const passed = results.filter(r => r.success).length;
|
|
@@ -5630,7 +6051,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5630
6051
|
console.log(` Description: ${result.description}`);
|
|
5631
6052
|
}
|
|
5632
6053
|
else {
|
|
5633
|
-
console.
|
|
6054
|
+
console.error(`✗ Could not find element matching: "${intent}"`);
|
|
5634
6055
|
process.exit(1);
|
|
5635
6056
|
}
|
|
5636
6057
|
break;
|
|
@@ -5640,17 +6061,24 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5640
6061
|
const result = await findElementByIntent(browser, intent);
|
|
5641
6062
|
if (result) {
|
|
5642
6063
|
console.log(`✓ Found: ${result.description}`);
|
|
5643
|
-
|
|
6064
|
+
// The click's own result decides: a red-zone refusal or a miss used
|
|
6065
|
+
// to print "Clicked element" and exit 0 (audit 2026-10-21).
|
|
6066
|
+
const clicked = await browser.click(result.selector, { force: options.force === true });
|
|
6067
|
+
if (!clicked.success) {
|
|
6068
|
+
console.error(`✗ ${clicked.message}`);
|
|
6069
|
+
process.exit(1);
|
|
6070
|
+
}
|
|
5644
6071
|
console.log(`✓ Clicked element`);
|
|
5645
6072
|
}
|
|
5646
6073
|
else {
|
|
5647
|
-
console.
|
|
6074
|
+
console.error(`✗ Could not find element matching: "${intent}"`);
|
|
5648
6075
|
process.exit(1);
|
|
5649
6076
|
}
|
|
5650
6077
|
break;
|
|
5651
6078
|
}
|
|
5652
6079
|
default:
|
|
5653
6080
|
console.error("Usage: cbrowser ai [find|click] \"<intent>\"");
|
|
6081
|
+
process.exit(1);
|
|
5654
6082
|
}
|
|
5655
6083
|
break;
|
|
5656
6084
|
}
|
|
@@ -5665,7 +6093,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5665
6093
|
process.exit(1);
|
|
5666
6094
|
}
|
|
5667
6095
|
const maxPages = options["max-pages"] ? parseInt(options["max-pages"]) : 10;
|
|
5668
|
-
const timeout =
|
|
6096
|
+
const timeout = timeoutOption(options.timeout, 60000);
|
|
5669
6097
|
console.log(`\n🔍 Bug Hunter starting...`);
|
|
5670
6098
|
console.log(` URL: ${url}`);
|
|
5671
6099
|
console.log(` Max pages: ${maxPages}`);
|
|
@@ -5700,16 +6128,24 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5700
6128
|
case "diff": {
|
|
5701
6129
|
const url = args[0];
|
|
5702
6130
|
if (!url) {
|
|
5703
|
-
console.error("Usage: cbrowser diff <url> [--browsers <list>]");
|
|
6131
|
+
console.error("Usage: cbrowser diff <url> [--browsers <list>] [--viewport <WxH>] [--device <name>]");
|
|
5704
6132
|
process.exit(1);
|
|
5705
6133
|
}
|
|
6134
|
+
const flags = browserFlagsOrExit("diff", options);
|
|
6135
|
+
refuseBrowserFlags("diff", flags, {
|
|
6136
|
+
browser: "it runs each engine in turn; choose them with --browsers chromium,firefox,webkit",
|
|
6137
|
+
});
|
|
5706
6138
|
const browserList = options.browsers
|
|
5707
6139
|
? options.browsers.split(",")
|
|
5708
6140
|
: ["chromium", "firefox", "webkit"];
|
|
5709
6141
|
console.log(`\n🔀 Cross-Browser Diff`);
|
|
5710
6142
|
console.log(` URL: ${url}`);
|
|
5711
6143
|
console.log(` Browsers: ${browserList.join(", ")}\n`);
|
|
5712
|
-
const result = await crossBrowserDiff(url, [...browserList]
|
|
6144
|
+
const result = await crossBrowserDiff(url, [...browserList], {
|
|
6145
|
+
headless: flags.headless,
|
|
6146
|
+
device: flags.device,
|
|
6147
|
+
viewport: flags.viewport,
|
|
6148
|
+
});
|
|
5713
6149
|
console.log(`📊 Results:\n`);
|
|
5714
6150
|
console.log(` Metrics:`);
|
|
5715
6151
|
for (const [browser, metrics] of Object.entries(result.metrics)) {
|
|
@@ -5761,6 +6197,8 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5761
6197
|
const result = await runChaosTest(browser, url, chaosConfig);
|
|
5762
6198
|
console.log(`📊 Results:\n`);
|
|
5763
6199
|
console.log(` Passed: ${result.passed ? "✅ Yes" : "❌ No"}`);
|
|
6200
|
+
if (!result.passed)
|
|
6201
|
+
process.exitCode = 1;
|
|
5764
6202
|
console.log(` Duration: ${result.duration}ms`);
|
|
5765
6203
|
if (result.errors.length > 0) {
|
|
5766
6204
|
console.log(`\n Errors:`);
|
|
@@ -5807,10 +6245,9 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5807
6245
|
console.log("");
|
|
5808
6246
|
}
|
|
5809
6247
|
console.log("Usage (OPT-IN REQUIRED):");
|
|
5810
|
-
console.log(" cbrowser screenshot https://example.com --lightpanda");
|
|
5811
6248
|
console.log(" cbrowser agent-ready-audit https://example.com --lightpanda");
|
|
5812
6249
|
console.log("");
|
|
5813
|
-
console.log("Note: Lightpanda is NOT used automatically.
|
|
6250
|
+
console.log("Note: Lightpanda is NOT used automatically. Only agent-ready-audit uses it, with --lightpanda.");
|
|
5814
6251
|
}
|
|
5815
6252
|
else {
|
|
5816
6253
|
console.log("⚠️ Configured but unavailable");
|
|
@@ -5912,7 +6349,11 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5912
6349
|
});
|
|
5913
6350
|
console.log(`\n🔍 Step-through mode enabled. Press Enter to execute each step.\n`);
|
|
5914
6351
|
const { CBrowser } = await import("./browser.js");
|
|
5915
|
-
const stepBrowser = new CBrowser({
|
|
6352
|
+
const stepBrowser = new CBrowser({
|
|
6353
|
+
headless,
|
|
6354
|
+
...(typeof options.device === "string" ? { device: options.device } : {}),
|
|
6355
|
+
...(viewportOverride ? { viewportWidth: viewportOverride.width, viewportHeight: viewportOverride.height, viewportOverride } : {}),
|
|
6356
|
+
});
|
|
5916
6357
|
await stepBrowser.launch();
|
|
5917
6358
|
try {
|
|
5918
6359
|
const totalSteps = suite.tests.reduce((sum, t) => sum + t.steps.length, 0);
|
|
@@ -5977,6 +6418,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5977
6418
|
}
|
|
5978
6419
|
catch (e) {
|
|
5979
6420
|
console.log(` ✗ Failed: ${e.message} (${Date.now() - start}ms)`);
|
|
6421
|
+
process.exitCode = 1;
|
|
5980
6422
|
}
|
|
5981
6423
|
}
|
|
5982
6424
|
}
|
|
@@ -5989,10 +6431,16 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5989
6431
|
}
|
|
5990
6432
|
assertCaptureNamespaceFlags("test-suite", options);
|
|
5991
6433
|
const suiteOptions = {
|
|
5992
|
-
stepTimeout:
|
|
6434
|
+
stepTimeout: timeoutOption(options.timeout, 30000),
|
|
5993
6435
|
continueOnFailure: options["continue-on-failure"] === true,
|
|
5994
6436
|
screenshotOnFailure: options["screenshot-on-failure"] !== false,
|
|
5995
6437
|
headless,
|
|
6438
|
+
device: typeof options.device === "string" ? options.device : undefined,
|
|
6439
|
+
viewport: viewportOverride,
|
|
6440
|
+
...(options.browser !== undefined ? { browser: browserType } : {}),
|
|
6441
|
+
...(geolocation ? { geolocation } : {}),
|
|
6442
|
+
...(typeof options.locale === "string" ? { locale: options.locale } : {}),
|
|
6443
|
+
...(typeof options.timezone === "string" ? { timezone: options.timezone } : {}),
|
|
5996
6444
|
fuzzyMatch: options["fuzzy-match"] === true,
|
|
5997
6445
|
capture: buildAutoCaptureSetting(options, `test-suite-${suite.name.replace(/[^a-zA-Z0-9-_]+/g, "-").toLowerCase()}-${Date.now()}`),
|
|
5998
6446
|
};
|
|
@@ -6001,6 +6449,22 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6001
6449
|
const report = formatNLTestReport(result);
|
|
6002
6450
|
console.log(report);
|
|
6003
6451
|
reportAutoCapture(result.capture);
|
|
6452
|
+
// Saved for `cbrowser export junit|tap`, which read this directory
|
|
6453
|
+
// while nothing wrote it (audit O17).
|
|
6454
|
+
const savedPath = saveSuiteResult(browser.getDataDir(), result);
|
|
6455
|
+
console.log(`\n💾 Results saved: ${savedPath} (export: cbrowser export junit ${resultsName(result.name)})`);
|
|
6456
|
+
// Webhooks: one event per run. A failed send is a warning, never the
|
|
6457
|
+
// run's result.
|
|
6458
|
+
await sendWebhookEvent(browser.getDataDir(), result.summary.failed > 0 ? "test.fail" : "test.pass", {
|
|
6459
|
+
suite: result.name,
|
|
6460
|
+
summary: {
|
|
6461
|
+
total: result.summary.total,
|
|
6462
|
+
passed: result.summary.passed,
|
|
6463
|
+
failed: result.summary.failed,
|
|
6464
|
+
skipped: result.summary.skipped,
|
|
6465
|
+
},
|
|
6466
|
+
failedTests: result.testResults.filter((t) => !t.passed).map((t) => ({ name: t.name, ...(t.error ? { error: t.error } : {}) })),
|
|
6467
|
+
});
|
|
6004
6468
|
// Save JSON output if requested
|
|
6005
6469
|
if (options.output) {
|
|
6006
6470
|
const fs = await import("fs");
|
|
@@ -6011,7 +6475,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6011
6475
|
if (options.html) {
|
|
6012
6476
|
const fs = await import("fs");
|
|
6013
6477
|
const htmlReport = generateTestSuiteHtmlReport(result);
|
|
6014
|
-
const htmlPath = options.output
|
|
6478
|
+
const htmlPath = htmlReportPath(options.output, "test-report.html");
|
|
6015
6479
|
fs.writeFileSync(htmlPath, htmlReport);
|
|
6016
6480
|
console.log(`\n🌐 HTML report saved: ${htmlPath}`);
|
|
6017
6481
|
}
|
|
@@ -6034,7 +6498,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6034
6498
|
console.error(" --auto-apply Automatically apply the best repair suggestion");
|
|
6035
6499
|
console.error(" --verify Re-run repaired tests to verify they pass");
|
|
6036
6500
|
console.error(" --output <file> Save repaired tests to a new file");
|
|
6037
|
-
console.error(" --json <file>
|
|
6501
|
+
console.error(" --json-out <file> Save repair report as JSON (--json prints it)");
|
|
6038
6502
|
console.error("");
|
|
6039
6503
|
console.error("Examples:");
|
|
6040
6504
|
console.error(" cbrowser repair-tests broken-test.txt");
|
|
@@ -6054,8 +6518,18 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6054
6518
|
for (const test of suite.tests) {
|
|
6055
6519
|
console.log(` - ${test.name}: ${test.steps.length} steps`);
|
|
6056
6520
|
}
|
|
6521
|
+
// --output writes the repaired file, which only exists with --auto-apply;
|
|
6522
|
+
// without it the flag used to do nothing and say nothing.
|
|
6523
|
+
if (options.output && options["auto-apply"] !== true) {
|
|
6524
|
+
console.error("Error: --output saves the repaired tests, which needs --auto-apply");
|
|
6525
|
+
process.exit(1);
|
|
6526
|
+
}
|
|
6527
|
+
const repairFlags = browserFlagsOrExit("repair-tests", options);
|
|
6057
6528
|
const repairOptions = {
|
|
6058
6529
|
headless,
|
|
6530
|
+
browser: repairFlags.browser,
|
|
6531
|
+
device: repairFlags.device,
|
|
6532
|
+
viewport: repairFlags.viewport,
|
|
6059
6533
|
autoApply: options["auto-apply"] === true,
|
|
6060
6534
|
verifyRepairs: options.verify === true,
|
|
6061
6535
|
maxRetries: options.retries ? parseInt(options.retries) : 3,
|
|
@@ -6064,10 +6538,18 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6064
6538
|
// Print formatted report
|
|
6065
6539
|
const report = formatRepairReport(result);
|
|
6066
6540
|
console.log(report);
|
|
6067
|
-
// Save JSON report
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6541
|
+
// Save the JSON report: --json-out <file>, or the older --json=<file>.
|
|
6542
|
+
// --json alone is the on/off flag every other command uses, so here it
|
|
6543
|
+
// prints the report to stdout instead of passing `true` as a path.
|
|
6544
|
+
// (Audit 2026-10-21: --json was a path here and a switch elsewhere.)
|
|
6545
|
+
const jsonOut = typeof options["json-out"] === "string" ? options["json-out"]
|
|
6546
|
+
: typeof options.json === "string" ? options.json : undefined;
|
|
6547
|
+
if (jsonOut) {
|
|
6548
|
+
fs.writeFileSync(jsonOut, JSON.stringify(result, null, 2));
|
|
6549
|
+
console.log(`\n📄 JSON report saved: ${jsonOut}`);
|
|
6550
|
+
}
|
|
6551
|
+
else if (options.json === true) {
|
|
6552
|
+
console.log(JSON.stringify(result, null, 2));
|
|
6071
6553
|
}
|
|
6072
6554
|
// Save repaired tests if requested
|
|
6073
6555
|
if (options.output && repairOptions.autoApply) {
|
|
@@ -6084,6 +6566,11 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6084
6566
|
console.log("\n💡 Run with --auto-apply to automatically fix issues");
|
|
6085
6567
|
process.exit(1);
|
|
6086
6568
|
}
|
|
6569
|
+
// With --auto-apply, a test that failed and was not repaired still fails.
|
|
6570
|
+
if (result.summary.testsRepaired < result.summary.testsWithFailures) {
|
|
6571
|
+
console.error(`✗ ${result.summary.testsWithFailures - result.summary.testsRepaired} failing test(s) could not be repaired`);
|
|
6572
|
+
process.exit(1);
|
|
6573
|
+
}
|
|
6087
6574
|
break;
|
|
6088
6575
|
}
|
|
6089
6576
|
// =========================================================================
|
|
@@ -6119,10 +6606,15 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6119
6606
|
for (const test of suite.tests) {
|
|
6120
6607
|
console.log(` - ${test.name}: ${test.steps.length} steps`);
|
|
6121
6608
|
}
|
|
6609
|
+
const flakyFlags = browserFlagsOrExit("flaky-check", options);
|
|
6122
6610
|
const flakyOptions = {
|
|
6123
6611
|
headless,
|
|
6612
|
+
browser: flakyFlags.browser,
|
|
6613
|
+
device: flakyFlags.device,
|
|
6614
|
+
viewport: flakyFlags.viewport,
|
|
6124
6615
|
runs: options.runs ? parseInt(options.runs) : 5,
|
|
6125
|
-
|
|
6616
|
+
// A percentage: parseInt read "0.2" as 0, which flagged every test.
|
|
6617
|
+
flakinessThreshold: numberOption("threshold", options.threshold, 20, { min: 0, max: 100 }),
|
|
6126
6618
|
delayBetweenRuns: options.delay ? parseInt(options.delay) : 500,
|
|
6127
6619
|
};
|
|
6128
6620
|
const result = await detectFlakyTests(suite, flakyOptions);
|
|
@@ -6153,9 +6645,15 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6153
6645
|
console.error("Usage: cbrowser perf-baseline save <url> [--name <name>] [--runs <n>]");
|
|
6154
6646
|
process.exit(1);
|
|
6155
6647
|
}
|
|
6648
|
+
const perfFlags = browserFlagsOrExit("perf-baseline save", options);
|
|
6649
|
+
refuseBrowserFlags("perf-baseline save", perfFlags, {
|
|
6650
|
+
browser: "each run disables the HTTP cache through Chromium's DevTools protocol; on another engine the runs would mix cold and warm loads",
|
|
6651
|
+
});
|
|
6156
6652
|
console.log(`\n📊 Capturing performance baseline for: ${url}`);
|
|
6157
6653
|
const baselineOptions = {
|
|
6158
6654
|
headless,
|
|
6655
|
+
device: perfFlags.device,
|
|
6656
|
+
viewport: perfFlags.viewport,
|
|
6159
6657
|
name: options.name,
|
|
6160
6658
|
runs: options.runs ? parseInt(options.runs) : 3,
|
|
6161
6659
|
};
|
|
@@ -6164,6 +6662,8 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6164
6662
|
console.log(`\n✅ Baseline saved: ${baseline.name}`);
|
|
6165
6663
|
console.log(` ID: ${baseline.id}`);
|
|
6166
6664
|
console.log(` URL: ${baseline.url}`);
|
|
6665
|
+
const env = baseline.environment;
|
|
6666
|
+
console.log(` Page: ${env.viewport.width}x${env.viewport.height}${env.device ? ` (${env.device})` : ""}`);
|
|
6167
6667
|
console.log(` Timestamp: ${new Date(baseline.timestamp).toLocaleString()}`);
|
|
6168
6668
|
console.log(`\n📈 Metrics (averaged over ${baseline.runsAveraged} runs):`);
|
|
6169
6669
|
if (baseline.metrics.lcp !== undefined) {
|
|
@@ -6283,7 +6783,19 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6283
6783
|
console.error(" cbrowser perf-regression https://example.com homepage --threshold-lcp 30");
|
|
6284
6784
|
process.exit(1);
|
|
6285
6785
|
}
|
|
6786
|
+
// A regression renders as its baseline was captured; a different
|
|
6787
|
+
// device or size here would compare unlike pages (audit F20: these
|
|
6788
|
+
// flags were accepted and dropped).
|
|
6789
|
+
refuseBrowserFlags("perf-regression", browserFlagsOrExit("perf-regression", options), {
|
|
6790
|
+
device: "it renders with the baseline's recorded device; capture a new baseline with perf-baseline save --device",
|
|
6791
|
+
viewport: "it renders with the baseline's recorded size; capture a new baseline with perf-baseline save --viewport",
|
|
6792
|
+
browser: "performance baselines are measured in Chromium",
|
|
6793
|
+
});
|
|
6286
6794
|
const sensitivity = options.sensitivity || "normal";
|
|
6795
|
+
if (!["strict", "normal", "ci", "lenient"].includes(sensitivity)) {
|
|
6796
|
+
console.error(`Error: --sensitivity must be strict, normal, ci or lenient, got: ${sensitivity}`);
|
|
6797
|
+
process.exit(1);
|
|
6798
|
+
}
|
|
6287
6799
|
console.log(`\n🔍 Checking for performance regressions...`);
|
|
6288
6800
|
console.log(` URL: ${url}`);
|
|
6289
6801
|
console.log(` Baseline: ${baselineName}`);
|
|
@@ -6355,6 +6867,12 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6355
6867
|
}
|
|
6356
6868
|
return files;
|
|
6357
6869
|
}
|
|
6870
|
+
// The browser flags reach the crawl. A sitemap is read without one.
|
|
6871
|
+
const coverageFlags = browserFlagsOrExit("coverage", options);
|
|
6872
|
+
if (options.sitemap) {
|
|
6873
|
+
const noBrowser = "--sitemap reads the sitemap without a browser, so there is no page to apply it to";
|
|
6874
|
+
refuseBrowserFlags("coverage", coverageFlags, { headless: noBrowser, browser: noBrowser, device: noBrowser, viewport: noBrowser });
|
|
6875
|
+
}
|
|
6358
6876
|
// Handle "coverage gaps" subcommand
|
|
6359
6877
|
if (subcommand === "gaps") {
|
|
6360
6878
|
const url = args[1];
|
|
@@ -6374,6 +6892,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6374
6892
|
sitemapUrl: options.sitemap,
|
|
6375
6893
|
maxPages: 50, // Quick mode
|
|
6376
6894
|
minCoverage: 50,
|
|
6895
|
+
...coverageFlags,
|
|
6377
6896
|
};
|
|
6378
6897
|
const result = await generateCoverageMap(url, testFiles, coverageOptions);
|
|
6379
6898
|
// Show only gaps
|
|
@@ -6414,6 +6933,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6414
6933
|
includePattern: options.include,
|
|
6415
6934
|
excludePattern: options.exclude,
|
|
6416
6935
|
minCoverage: options["min-coverage"] ? parseInt(options["min-coverage"]) : 50,
|
|
6936
|
+
...coverageFlags,
|
|
6417
6937
|
};
|
|
6418
6938
|
if (coverageOptions.sitemapUrl) {
|
|
6419
6939
|
console.log(` Using sitemap: ${coverageOptions.sitemapUrl}`);
|
|
@@ -6453,8 +6973,16 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6453
6973
|
// Cognitive Journey (v8.0.0) - API-powered autonomous user simulation
|
|
6454
6974
|
// =========================================================================
|
|
6455
6975
|
case "cognitive-journey": {
|
|
6976
|
+
// A replay makes no model calls, so it needs no key (audit 2026-10-21:
|
|
6977
|
+
// the key gate blocked replays, and --replay-trace never reached the
|
|
6978
|
+
// engine, so a "replay" was a live, paid run).
|
|
6979
|
+
const replayTrace = typeof options["replay-trace"] === "string" ? resolve(options["replay-trace"]) : undefined;
|
|
6980
|
+
if (replayTrace && !existsSync(replayTrace)) {
|
|
6981
|
+
console.error(`Error: trace file not found: ${replayTrace}`);
|
|
6982
|
+
process.exit(1);
|
|
6983
|
+
}
|
|
6456
6984
|
// Check API key first
|
|
6457
|
-
if (!isApiKeyConfigured()) {
|
|
6985
|
+
if (!replayTrace && !isApiKeyConfigured()) {
|
|
6458
6986
|
// Output structured prompt for Claude Code integration
|
|
6459
6987
|
// Format matches Claude Agent SDK's AskUserQuestion tool
|
|
6460
6988
|
const apiKeyPrompt = {
|
|
@@ -6489,10 +7017,10 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6489
7017
|
console.log(JSON.stringify(apiKeyPrompt, null, 2));
|
|
6490
7018
|
process.exit(1);
|
|
6491
7019
|
}
|
|
6492
|
-
const startUrl = options.start;
|
|
7020
|
+
const startUrl = (options.start ?? options.url); // --url works too (audit F25)
|
|
6493
7021
|
const goal = options.goal;
|
|
6494
7022
|
if (!startUrl) {
|
|
6495
|
-
console.error("Error: --start <url> is required");
|
|
7023
|
+
console.error("Error: --start <url> is required (--url works too)");
|
|
6496
7024
|
process.exit(1);
|
|
6497
7025
|
}
|
|
6498
7026
|
if (!goal) {
|
|
@@ -6503,12 +7031,18 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6503
7031
|
const maxSteps = options["max-steps"] ? parseInt(options["max-steps"]) : 50;
|
|
6504
7032
|
const maxTime = options["max-time"] ? parseInt(options["max-time"]) : 120;
|
|
6505
7033
|
const verbose = options.verbose === true;
|
|
6506
|
-
|
|
7034
|
+
// Undefined unless asked, so the engine decides from $DISPLAY. This was
|
|
7035
|
+
// `=== true`, which made a plain journey launch headed on a server
|
|
7036
|
+
// with no display, unlike every other command.
|
|
7037
|
+
const headless = options.headless === true ? true : options.headless === false ? false : undefined;
|
|
6507
7038
|
const vision = options["no-vision"] !== true; // Vision ON by default (v17.1.0)
|
|
6508
7039
|
// Parse location options
|
|
6509
7040
|
let locationOverride;
|
|
6510
|
-
if (options.timezone || options.locale || options.geolocation) {
|
|
7041
|
+
if (options.timezone || options.locale || options.geolocation || geolocation) {
|
|
6511
7042
|
locationOverride = {};
|
|
7043
|
+
// The global --geo (a preset or lat,lon) was accepted here and ignored.
|
|
7044
|
+
if (geolocation)
|
|
7045
|
+
locationOverride.geolocation = { latitude: geolocation.latitude, longitude: geolocation.longitude };
|
|
6512
7046
|
if (options.timezone)
|
|
6513
7047
|
locationOverride.timezone = options.timezone;
|
|
6514
7048
|
if (options.locale)
|
|
@@ -6567,6 +7101,14 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6567
7101
|
maxTime,
|
|
6568
7102
|
verbose,
|
|
6569
7103
|
headless,
|
|
7104
|
+
device: typeof options.device === "string" ? options.device : undefined,
|
|
7105
|
+
viewport: viewportOverride,
|
|
7106
|
+
trace: options.trace === true,
|
|
7107
|
+
replayTrace,
|
|
7108
|
+
// A persona is a new visitor: a fresh profile unless --persistent.
|
|
7109
|
+
// The engine's default used the shared logged-in profile, so a
|
|
7110
|
+
// "first-timer" carried the user's cookies (audit 2026-10-21).
|
|
7111
|
+
persistent: options.persistent === true,
|
|
6570
7112
|
vision,
|
|
6571
7113
|
customTraits, // Pass custom traits from env var
|
|
6572
7114
|
location: locationOverride, // Pass location override
|
|
@@ -6577,6 +7119,10 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6577
7119
|
});
|
|
6578
7120
|
console.log("\n");
|
|
6579
7121
|
reportAutoCapture(result.capture);
|
|
7122
|
+
if (result.tracePath) {
|
|
7123
|
+
console.log(`🧾 Trace: ${result.tracePath}`);
|
|
7124
|
+
console.log(` Replay it with no API calls: cbrowser cognitive-journey --start <url> --goal <goal> --replay-trace ${result.tracePath}`);
|
|
7125
|
+
}
|
|
6580
7126
|
if (result.goalAchieved) {
|
|
6581
7127
|
console.log(`✅ GOAL ACHIEVED in ${result.stepCount} steps (${result.totalTime.toFixed(1)}s)`);
|
|
6582
7128
|
}
|
|
@@ -6618,7 +7164,20 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6618
7164
|
// goalAchieved genuinely means the persona reached the goal — unlike
|
|
6619
7165
|
// the empathy audit's field of the same old name, which measured
|
|
6620
7166
|
// barriers and was renamed to noBlockingBarriers.
|
|
6621
|
-
|
|
7167
|
+
// journey.complete, once per journey (audit O17: it was the default
|
|
7168
|
+
// event and nothing ever sent it). Never fails the journey.
|
|
7169
|
+
await sendWebhookEvent(browser.getDataDir(), "journey.complete", {
|
|
7170
|
+
persona: personaName,
|
|
7171
|
+
goal,
|
|
7172
|
+
startUrl,
|
|
7173
|
+
goalAchieved: result.goalAchieved,
|
|
7174
|
+
...(result.abandonmentReason ? { abandonmentReason: result.abandonmentReason } : {}),
|
|
7175
|
+
stepCount: result.stepCount,
|
|
7176
|
+
totalTimeSeconds: Math.round(result.totalTime * 10) / 10,
|
|
7177
|
+
});
|
|
7178
|
+
// Keep a failure reportAutoCapture already set: a lost capture used
|
|
7179
|
+
// to be overwritten by the goal's 0.
|
|
7180
|
+
process.exit(result.goalAchieved ? (process.exitCode ?? 0) : 1);
|
|
6622
7181
|
}
|
|
6623
7182
|
catch (error) {
|
|
6624
7183
|
console.error(`\n❌ Error: ${error.message}`);
|
|
@@ -6652,7 +7211,11 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6652
7211
|
auditProxy = { server: geoProxy.server, username: geoProxy.username, password: geoProxy.password };
|
|
6653
7212
|
}
|
|
6654
7213
|
else {
|
|
6655
|
-
|
|
7214
|
+
// The README promises this fails loudly; it warned, ran from the
|
|
7215
|
+
// local IP, and still printed "(via <region>)" (audit 2026-10-21).
|
|
7216
|
+
console.error(`Error: geo-region "${geoRegion}" is not configured, so the audit would run from this machine's own IP.`);
|
|
7217
|
+
console.error(" Configure it in ~/.cbrowser/geo-proxy.json, or run without --geo-region.");
|
|
7218
|
+
process.exit(1);
|
|
6656
7219
|
}
|
|
6657
7220
|
}
|
|
6658
7221
|
console.log(`\n🤖 Running Agent-Ready Audit on ${url}...${useLightpanda ? " (with Lightpanda)" : ""}${geoRegion ? ` (via ${geoRegion})` : ""}\n`);
|
|
@@ -6660,6 +7223,10 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6660
7223
|
headless,
|
|
6661
7224
|
useLightpanda,
|
|
6662
7225
|
...(auditProxy ? { proxy: auditProxy } : {}),
|
|
7226
|
+
// Accepted by the engine and dropped here until 20.0, so a mobile
|
|
7227
|
+
// audit from the CLI was silently a desktop audit.
|
|
7228
|
+
...(typeof options.device === "string" ? { device: resolveDevicePreset(options.device) } : {}),
|
|
7229
|
+
...(typeof options.locale === "string" ? { locale: options.locale } : {}),
|
|
6663
7230
|
});
|
|
6664
7231
|
// Print formatted report
|
|
6665
7232
|
const report = formatAgentReadyReport(result);
|
|
@@ -6674,7 +7241,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6674
7241
|
if (options.html) {
|
|
6675
7242
|
const fs = await import("fs");
|
|
6676
7243
|
const htmlReport = generateAgentReadyHtmlReport(result);
|
|
6677
|
-
const htmlPath = options.output
|
|
7244
|
+
const htmlPath = htmlReportPath(options.output, "agent-ready-report.html");
|
|
6678
7245
|
fs.writeFileSync(htmlPath, htmlReport);
|
|
6679
7246
|
console.log(`🌐 HTML report saved: ${htmlPath}`);
|
|
6680
7247
|
}
|
|
@@ -6697,8 +7264,16 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6697
7264
|
process.exit(1);
|
|
6698
7265
|
}
|
|
6699
7266
|
console.log(`\n🏁 Running AI Readiness Benchmark on ${urls.length} sites...\n`);
|
|
7267
|
+
// --device was accepted and dropped (audit F20). The audit can emulate a
|
|
7268
|
+
// device but not set a bare size or another engine.
|
|
7269
|
+
const benchFlags = browserFlagsOrExit("ai-benchmark", options);
|
|
7270
|
+
refuseBrowserFlags("ai-benchmark", benchFlags, {
|
|
7271
|
+
viewport: "the agent-ready audit takes a device, not a bare size; use --device",
|
|
7272
|
+
browser: "the agent-ready audit runs in Chromium",
|
|
7273
|
+
});
|
|
6700
7274
|
const result = await runAIReadinessBenchmark({
|
|
6701
7275
|
urls,
|
|
7276
|
+
...(benchFlags.device ? { device: benchFlags.device } : {}),
|
|
6702
7277
|
headless: options.headless !== false,
|
|
6703
7278
|
maxConcurrency: options.concurrency ? parseInt(options.concurrency) : 3,
|
|
6704
7279
|
});
|
|
@@ -6711,6 +7286,9 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6711
7286
|
for (const site of result.sites) {
|
|
6712
7287
|
const rank = result.ranking.find(r => r.site === site.siteName)?.rank || "-";
|
|
6713
7288
|
const score = site.score !== null ? site.score.toString().padStart(3) : "ERR";
|
|
7289
|
+
// A site that could not be scored is a failed run, not a blank row.
|
|
7290
|
+
if (site.score === null)
|
|
7291
|
+
process.exitCode = 1;
|
|
6714
7292
|
const grade = site.grade || "-";
|
|
6715
7293
|
const find = site.scoreBreakdown?.findability?.toString().padStart(3) || "-";
|
|
6716
7294
|
const stab = site.scoreBreakdown?.stability?.toString().padStart(3) || "-";
|
|
@@ -6753,7 +7331,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6753
7331
|
// Generate HTML report if requested
|
|
6754
7332
|
if (options.html) {
|
|
6755
7333
|
const fs = await import("fs");
|
|
6756
|
-
const htmlPath = options.output
|
|
7334
|
+
const htmlPath = htmlReportPath(options.output, "ai-benchmark-report.html");
|
|
6757
7335
|
// Simple HTML table report
|
|
6758
7336
|
const html = `<!DOCTYPE html>
|
|
6759
7337
|
<html lang="en">
|
|
@@ -6839,7 +7417,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6839
7417
|
const result = await runWebMCPReadyAudit(url, {
|
|
6840
7418
|
apiKey: options["api-key"],
|
|
6841
7419
|
oauthToken: options["oauth-token"],
|
|
6842
|
-
timeout:
|
|
7420
|
+
timeout: timeoutOption(options.timeout, 30000),
|
|
6843
7421
|
});
|
|
6844
7422
|
// Print formatted summary
|
|
6845
7423
|
console.log(`╔══════════════════════════════════════════════════════════════╗`);
|
|
@@ -6879,7 +7457,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6879
7457
|
// Generate HTML report if requested
|
|
6880
7458
|
if (options.html) {
|
|
6881
7459
|
const fs = await import("fs");
|
|
6882
|
-
const htmlPath = options.output
|
|
7460
|
+
const htmlPath = htmlReportPath(options.output, "webmcp-ready-report.html");
|
|
6883
7461
|
const htmlReport = generateWebMCPReadyHtmlReport(result);
|
|
6884
7462
|
fs.writeFileSync(htmlPath, htmlReport);
|
|
6885
7463
|
console.log(`🌐 HTML report saved: ${htmlPath}`);
|
|
@@ -6903,6 +7481,11 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6903
7481
|
console.error("Usage: cbrowser competitive-benchmark --sites <urls> --goal <goal> [--persona <name>] [--html]");
|
|
6904
7482
|
process.exit(1);
|
|
6905
7483
|
}
|
|
7484
|
+
// 20.0.0: no keyless mode (its goal verdicts were not accurate).
|
|
7485
|
+
if (!isApiKeyConfigured()) {
|
|
7486
|
+
console.error("Error: competitive-benchmark needs an Anthropic API key (cbrowser config set-api-key <key>). The keyless heuristic mode was removed in 20.0.0 because its goal verdicts were not accurate.");
|
|
7487
|
+
process.exit(1);
|
|
7488
|
+
}
|
|
6906
7489
|
const sites = sitesArg.split(",").map((s) => ({ url: s.trim() }));
|
|
6907
7490
|
const personaName = options.persona || "first-timer";
|
|
6908
7491
|
const maxSteps = options["max-steps"] ? parseInt(options["max-steps"]) : 30;
|
|
@@ -6911,6 +7494,11 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6911
7494
|
console.log(` Sites: ${sites.map((s) => s.url).join(", ")}`);
|
|
6912
7495
|
console.log(` Goal: ${goal}`);
|
|
6913
7496
|
console.log(` Persona: ${personaName}\n`);
|
|
7497
|
+
// --device and --viewport were accepted and dropped (audit F20).
|
|
7498
|
+
const journeyFlags = browserFlagsOrExit("competitive-benchmark", options);
|
|
7499
|
+
refuseBrowserFlags("competitive-benchmark", journeyFlags, {
|
|
7500
|
+
browser: "cognitive journeys run in Chromium",
|
|
7501
|
+
});
|
|
6914
7502
|
const result = await runCompetitiveBenchmark({
|
|
6915
7503
|
sites,
|
|
6916
7504
|
goal,
|
|
@@ -6918,6 +7506,8 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6918
7506
|
maxSteps,
|
|
6919
7507
|
maxTime,
|
|
6920
7508
|
headless,
|
|
7509
|
+
...(journeyFlags.device ? { device: journeyFlags.device } : {}),
|
|
7510
|
+
...(journeyFlags.viewport ? { viewport: journeyFlags.viewport } : {}),
|
|
6921
7511
|
});
|
|
6922
7512
|
// Print formatted report
|
|
6923
7513
|
const report = formatCompetitiveBenchmarkReport(result);
|
|
@@ -6932,7 +7522,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6932
7522
|
if (options.html) {
|
|
6933
7523
|
const fs = await import("fs");
|
|
6934
7524
|
const htmlReport = generateCompetitiveBenchmarkHtmlReport(result);
|
|
6935
|
-
const htmlPath = options.output
|
|
7525
|
+
const htmlPath = htmlReportPath(options.output, "competitive-benchmark-report.html");
|
|
6936
7526
|
fs.writeFileSync(htmlPath, htmlReport);
|
|
6937
7527
|
console.log(`🌐 HTML report saved: ${htmlPath}`);
|
|
6938
7528
|
}
|
|
@@ -6941,41 +7531,22 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6941
7531
|
case "empathy-audit": {
|
|
6942
7532
|
const url = args[0];
|
|
6943
7533
|
const goal = options.goal;
|
|
6944
|
-
|
|
7534
|
+
// --personas is what the help and its examples say; --disabilities is the
|
|
7535
|
+
// older name. Before 2026-10-21 only --disabilities was read, so the
|
|
7536
|
+
// documented form silently ran every accessibility persona.
|
|
7537
|
+
const disabilitiesArg = (typeof options.personas === "string" ? options.personas : options.disabilities);
|
|
6945
7538
|
if (!url) {
|
|
6946
7539
|
console.error("Error: URL required");
|
|
6947
|
-
console.error("Usage: cbrowser empathy-audit <url> --goal <goal> [--
|
|
6948
|
-
process.exit(1);
|
|
6949
|
-
}
|
|
6950
|
-
if (!goal) {
|
|
6951
|
-
console.error("Error: --goal required");
|
|
6952
|
-
console.error("Usage: cbrowser empathy-audit <url> --goal <goal> [--disabilities <list>] [--html]");
|
|
7540
|
+
console.error("Usage: cbrowser empathy-audit <url> [--goal <goal>] [--personas <list>] [--device <name>] [--html]");
|
|
6953
7541
|
process.exit(1);
|
|
6954
7542
|
}
|
|
7543
|
+
// --goal is optional, as it is over MCP: without one the audit checks
|
|
7544
|
+
// the page rather than a task (it was required here only).
|
|
6955
7545
|
// Parse disabilities or use all
|
|
6956
7546
|
const allAccessibilityNames = listAccessibilityPersonas();
|
|
6957
|
-
//
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
"motor": "motor-impairment-tremor",
|
|
6961
|
-
"tremor": "motor-impairment-tremor",
|
|
6962
|
-
"low-vision": "low-vision-magnified",
|
|
6963
|
-
"vision": "low-vision-magnified",
|
|
6964
|
-
"magnified": "low-vision-magnified",
|
|
6965
|
-
"adhd": "cognitive-adhd",
|
|
6966
|
-
"cognitive": "cognitive-adhd",
|
|
6967
|
-
"attention": "cognitive-adhd",
|
|
6968
|
-
"dyslexia": "dyslexic-user",
|
|
6969
|
-
"dyslexic": "dyslexic-user",
|
|
6970
|
-
"reading": "dyslexic-user",
|
|
6971
|
-
"deaf": "deaf-user",
|
|
6972
|
-
"hearing": "deaf-user",
|
|
6973
|
-
"elderly": "elderly-low-vision",
|
|
6974
|
-
"senior": "elderly-low-vision",
|
|
6975
|
-
"color-blind": "color-blind-deuteranopia",
|
|
6976
|
-
"colorblind": "color-blind-deuteranopia",
|
|
6977
|
-
"deuteranopia": "color-blind-deuteranopia",
|
|
6978
|
-
};
|
|
7547
|
+
// The shared alias table (personas.ts), as MCP uses. This command had its
|
|
7548
|
+
// own stale copy, so --disabilities autism failed here and worked there.
|
|
7549
|
+
const aliasMap = PERSONA_ALIASES;
|
|
6979
7550
|
const disabilityNames = disabilitiesArg
|
|
6980
7551
|
? disabilitiesArg.split(",").map((d) => {
|
|
6981
7552
|
const trimmed = d.trim().toLowerCase();
|
|
@@ -6987,7 +7558,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6987
7558
|
if (invalidNames.length > 0) {
|
|
6988
7559
|
console.error(`Error: Unknown disability persona(s): ${invalidNames.join(", ")}`);
|
|
6989
7560
|
console.error(`Available: ${allAccessibilityNames.join(", ")}`);
|
|
6990
|
-
console.error(`Aliases:
|
|
7561
|
+
console.error(`Aliases: ${Object.keys(aliasMap).filter((a) => allAccessibilityNames.includes(aliasMap[a]) && a !== aliasMap[a]).join(", ")}`);
|
|
6991
7562
|
process.exit(1);
|
|
6992
7563
|
}
|
|
6993
7564
|
const wcagLevel = options["wcag-level"] || "AA";
|
|
@@ -6998,13 +7569,22 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6998
7569
|
console.log(` Goal: ${goal}`);
|
|
6999
7570
|
console.log(` Disabilities: ${disabilityNames.join(", ")}`);
|
|
7000
7571
|
console.log(` WCAG Level: ${wcagLevel}\n`);
|
|
7572
|
+
const scope = options.scope === undefined ? undefined : String(options.scope);
|
|
7573
|
+
if (scope !== undefined && scope !== "viewport" && scope !== "full_page") {
|
|
7574
|
+
console.error(`Error: --scope must be viewport or full_page, got: ${scope}`);
|
|
7575
|
+
process.exit(1);
|
|
7576
|
+
}
|
|
7001
7577
|
const result = await runEmpathyAudit(url, {
|
|
7002
|
-
goal,
|
|
7578
|
+
...(goal ? { goal } : {}),
|
|
7003
7579
|
disabilities: disabilityNames,
|
|
7004
7580
|
wcagLevel: wcagLevel,
|
|
7005
7581
|
maxSteps,
|
|
7006
7582
|
maxTime,
|
|
7007
7583
|
headless,
|
|
7584
|
+
// Accepted by the engine and dropped here until 20.0 (the MCP tool
|
|
7585
|
+
// passed it), so a mobile empathy audit from the CLI was a desktop one.
|
|
7586
|
+
...(typeof options.device === "string" ? { device: resolveDevicePreset(options.device) } : {}),
|
|
7587
|
+
...(scope ? { scope: scope } : {}),
|
|
7008
7588
|
});
|
|
7009
7589
|
// Print formatted report
|
|
7010
7590
|
const report = formatEmpathyAuditReport(result);
|
|
@@ -7019,7 +7599,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
7019
7599
|
if (options.html) {
|
|
7020
7600
|
const fs = await import("fs");
|
|
7021
7601
|
const htmlReport = generateEmpathyAuditHtmlReport(result);
|
|
7022
|
-
const htmlPath = options.output
|
|
7602
|
+
const htmlPath = htmlReportPath(options.output, "empathy-audit-report.html");
|
|
7023
7603
|
fs.writeFileSync(htmlPath, htmlReport);
|
|
7024
7604
|
console.log(`🌐 HTML report saved: ${htmlPath}`);
|
|
7025
7605
|
}
|
|
@@ -7048,7 +7628,7 @@ Commands:
|
|
|
7048
7628
|
persona-questionnaire lookup Look up behaviors for a specific trait value
|
|
7049
7629
|
|
|
7050
7630
|
Options for 'start':
|
|
7051
|
-
--comprehensive Include all
|
|
7631
|
+
--comprehensive Include all 30 traits (default: 13 core traits)
|
|
7052
7632
|
--name <name> Name for the persona (prompted if not provided)
|
|
7053
7633
|
--output <file> Save questionnaire answers to JSON file
|
|
7054
7634
|
|