cbrowser 19.6.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 +416 -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 +139 -196
- 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 +979 -401
- package/dist/cli.js.map +1 -1
- package/dist/cognitive/index.d.ts +0 -13
- package/dist/cognitive/index.d.ts.map +1 -1
- package/dist/cognitive/index.js +39 -15
- package/dist/cognitive/index.js.map +1 -1
- 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 +5 -4
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +87 -29
- package/dist/daemon.js.map +1 -1
- package/dist/devices.d.ts +20 -0
- package/dist/devices.d.ts.map +1 -1
- package/dist/devices.js +18 -0
- package/dist/devices.js.map +1 -1
- 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/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 +13 -0
- package/dist/testing/nl-test-suite.d.ts.map +1 -1
- package/dist/testing/nl-test-suite.js +5 -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 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +17 -4
- 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,8 +22,9 @@ 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 {
|
|
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";
|
|
23
28
|
import { LOCATION_PRESETS } from "./types.js";
|
|
24
29
|
import { CBROWSER_DEVICE_KEYS, DEVICE_PRESETS, resolveDevice } from "./devices.js";
|
|
25
30
|
import { startMcpServer } from "./mcp-server.js";
|
|
@@ -83,7 +88,7 @@ function helpText() {
|
|
|
83
88
|
|
|
84
89
|
NAVIGATION
|
|
85
90
|
navigate <url> Navigate and take screenshot
|
|
86
|
-
screenshot [path]
|
|
91
|
+
screenshot [url] [path] Take a screenshot (of <url> when given, else the current page)
|
|
87
92
|
--no-restore Don't reload saved URL (capture current state after interactions)
|
|
88
93
|
|
|
89
94
|
INTERACTION
|
|
@@ -118,6 +123,7 @@ SCRIPTING
|
|
|
118
123
|
--raw Print the raw value, unquoted
|
|
119
124
|
--wait-for <selector> Wait for the selector before evaluating
|
|
120
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)
|
|
121
127
|
Examples:
|
|
122
128
|
cbrowser evaluate "document.title" --force
|
|
123
129
|
cbrowser evaluate --file check.js --arg '"#main"' --expect-truthy --force
|
|
@@ -130,6 +136,7 @@ KEYBOARD
|
|
|
130
136
|
--hold <mod> Hold a modifier across the whole sequence
|
|
131
137
|
--repeat <n> Repeat the whole sequence n times
|
|
132
138
|
--text Treat every token as literal text (for "+" in text)
|
|
139
|
+
--url <url> Navigate to URL first
|
|
133
140
|
A token with modifiers is a chord (Control+A); a known key name is that key
|
|
134
141
|
(Enter, Tab, ArrowLeft); anything else is typed as literal text. Unknown
|
|
135
142
|
key names are rejected, not typed. For a single key use 'press', for a
|
|
@@ -139,12 +146,12 @@ KEYBOARD
|
|
|
139
146
|
cbrowser keyboard "ArrowRight" --repeat 5 --hold Shift
|
|
140
147
|
cbrowser keyboard "Tab" "Tab" "Enter" --selector "#email" --delay 120
|
|
141
148
|
|
|
142
|
-
SITE EXPLORATION (
|
|
143
|
-
explore <persona>
|
|
149
|
+
SITE EXPLORATION (API-powered)
|
|
150
|
+
explore <persona> Short cognitive journey (needs an Anthropic API key; costs tokens)
|
|
144
151
|
--start <url> Starting URL (required)
|
|
145
152
|
--goal <goal> What to accomplish
|
|
146
153
|
--record-video Record exploration as video
|
|
147
|
-
Note:
|
|
154
|
+
Note: the keyless heuristic explorer was removed in 20.0.0; its success verdicts were not accurate.
|
|
148
155
|
|
|
149
156
|
COGNITIVE JOURNEY (API-powered, realistic user simulation)
|
|
150
157
|
cognitive-journey Simulate a real user with emotions, patience, and abandonment
|
|
@@ -160,10 +167,13 @@ COGNITIVE JOURNEY (API-powered, realistic user simulation)
|
|
|
160
167
|
--verbose Show step-by-step narration and internal monologue
|
|
161
168
|
--no-vision Disable vision mode (text-only, less accurate but faster)
|
|
162
169
|
--output <file> Save JSON report to file
|
|
163
|
-
--
|
|
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)
|
|
164
174
|
--timezone <tz> Override persona timezone (e.g., "America/New_York", "Europe/London")
|
|
165
175
|
--locale <loc> Override persona locale (e.g., "en-US", "de-DE")
|
|
166
|
-
--
|
|
176
|
+
--geo <preset|lat,lon> Override persona location (also --geolocation <lat,lng>)
|
|
167
177
|
|
|
168
178
|
Why use this over 'explore'?
|
|
169
179
|
• Tracks cognitive state: patience, frustration, confusion
|
|
@@ -179,12 +189,14 @@ COGNITIVE JOURNEY (API-powered, realistic user simulation)
|
|
|
179
189
|
|
|
180
190
|
Note: Requires Anthropic API key. Set with: cbrowser config set-api-key <key>
|
|
181
191
|
|
|
182
|
-
MULTI-PERSONA COMPARISON
|
|
192
|
+
MULTI-PERSONA COMPARISON
|
|
183
193
|
compare-personas Compare multiple personas on the same journey
|
|
184
194
|
--start <url> Starting URL (required)
|
|
185
195
|
--goal <goal> What to accomplish (required)
|
|
186
196
|
--personas <list> Comma-separated persona names
|
|
187
197
|
--concurrency <n> Max parallel browsers (default: 3)
|
|
198
|
+
--device <name> Device every journey emulates
|
|
199
|
+
--viewport <WxH> Page size for every journey
|
|
188
200
|
--output <file> Save JSON report to file
|
|
189
201
|
--html Generate HTML report
|
|
190
202
|
Examples:
|
|
@@ -192,8 +204,10 @@ MULTI-PERSONA COMPARISON (v6.0.0)
|
|
|
192
204
|
--goal "Complete checkout" \\
|
|
193
205
|
--personas power-user,first-timer,elderly-user,mobile-user
|
|
194
206
|
|
|
195
|
-
NATURAL LANGUAGE TEST SUITES
|
|
207
|
+
NATURAL LANGUAGE TEST SUITES
|
|
196
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')
|
|
197
211
|
--continue-on-failure Keep running after a test fails
|
|
198
212
|
--screenshot-on-failure Take screenshots on failure (default: true)
|
|
199
213
|
--output <file> Save JSON report to file
|
|
@@ -228,29 +242,31 @@ NATURAL LANGUAGE TEST SUITES (v6.1.0)
|
|
|
228
242
|
click search button
|
|
229
243
|
verify page contains "results"
|
|
230
244
|
|
|
231
|
-
AI TEST REPAIR
|
|
245
|
+
AI TEST REPAIR
|
|
232
246
|
repair-tests <file.txt> Analyze failing tests and suggest/apply repairs
|
|
233
247
|
--auto-apply Automatically apply the best repair
|
|
234
248
|
--verify Re-run tests after repair to verify they pass
|
|
235
249
|
--output <file> Save repaired tests to new file
|
|
236
|
-
--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
|
|
237
252
|
Examples:
|
|
238
253
|
cbrowser repair-tests broken-test.txt
|
|
239
254
|
cbrowser repair-tests tests.txt --auto-apply --verify
|
|
240
255
|
cbrowser repair-tests tests.txt --auto-apply --output fixed-tests.txt
|
|
241
256
|
|
|
242
|
-
FLAKY TEST DETECTION
|
|
257
|
+
FLAKY TEST DETECTION
|
|
243
258
|
flaky-check <file.txt> Run tests multiple times to detect flaky tests
|
|
244
259
|
--runs <n> Number of runs per test (default: 5)
|
|
245
260
|
--threshold <n> Flakiness % threshold to flag (default: 20)
|
|
246
261
|
--delay <ms> Delay between runs (default: 500)
|
|
247
262
|
--output <file> Save JSON report to file
|
|
263
|
+
--device, --viewport, --browser, --no-headless The browser every run uses
|
|
248
264
|
Examples:
|
|
249
265
|
cbrowser flaky-check tests.txt
|
|
250
266
|
cbrowser flaky-check tests.txt --runs 10
|
|
251
267
|
cbrowser flaky-check tests.txt --runs 5 --threshold 30 --output flaky-report.json
|
|
252
268
|
|
|
253
|
-
TEST COVERAGE MAP
|
|
269
|
+
TEST COVERAGE MAP
|
|
254
270
|
coverage <url> Generate test coverage map for a site
|
|
255
271
|
--tests <glob> Test files to analyze (default: tests/*.txt)
|
|
256
272
|
--sitemap <url> Use sitemap.xml instead of crawling
|
|
@@ -259,7 +275,8 @@ TEST COVERAGE MAP (v6.5.0)
|
|
|
259
275
|
--exclude <pattern> Exclude paths matching pattern
|
|
260
276
|
--min-coverage <n> Min coverage % to not flag (default: 50)
|
|
261
277
|
--html Generate HTML report
|
|
262
|
-
--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)
|
|
263
280
|
Examples:
|
|
264
281
|
cbrowser coverage "https://example.com" --tests "tests/*.txt"
|
|
265
282
|
cbrowser coverage "https://example.com" --sitemap "https://example.com/sitemap.xml"
|
|
@@ -282,6 +299,8 @@ PERSONAS
|
|
|
282
299
|
persona delete <name> Delete a custom persona
|
|
283
300
|
persona export <name> Export persona to JSON file
|
|
284
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
|
|
285
304
|
|
|
286
305
|
SESSION MANAGEMENT
|
|
287
306
|
session save <name> Save browser session (cookies, storage, URL)
|
|
@@ -289,6 +308,12 @@ SESSION MANAGEMENT
|
|
|
289
308
|
session load <name> Load a saved session
|
|
290
309
|
session list List all saved sessions
|
|
291
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
|
|
292
317
|
|
|
293
318
|
COOKIE MANAGEMENT
|
|
294
319
|
cookie list List all cookies for current page
|
|
@@ -319,10 +344,12 @@ PERFORMANCE
|
|
|
319
344
|
--budget-fcp <ms> FCP budget (default: 1800)
|
|
320
345
|
--budget-cls <score> CLS budget (default: 0.1)
|
|
321
346
|
|
|
322
|
-
PERFORMANCE REGRESSION
|
|
347
|
+
PERFORMANCE REGRESSION
|
|
323
348
|
perf-baseline save <url> Capture and save performance baseline
|
|
324
349
|
--name <name> Human-readable name for baseline
|
|
325
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
|
|
326
353
|
Examples:
|
|
327
354
|
cbrowser perf-baseline save "https://example.com" --name "homepage"
|
|
328
355
|
cbrowser perf-baseline save "https://example.com/checkout" --runs 5
|
|
@@ -336,6 +363,9 @@ PERFORMANCE REGRESSION (v6.4.0)
|
|
|
336
363
|
--threshold-lcp <n> Max LCP increase % (default: 20)
|
|
337
364
|
--threshold-cls <n> Max CLS increase (default: 0.1)
|
|
338
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 %
|
|
339
369
|
--output <file> Save JSON report to file
|
|
340
370
|
Examples:
|
|
341
371
|
cbrowser perf-regression "https://example.com" homepage
|
|
@@ -346,14 +376,17 @@ NETWORK / HAR (need the daemon: each command is its own process)
|
|
|
346
376
|
daemon start Start the daemon first; it holds the browser between commands
|
|
347
377
|
har start Start recording HAR in the daemon's browser
|
|
348
378
|
har stop [output] Stop and save HAR file
|
|
349
|
-
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
|
|
350
383
|
network clear Clear that list
|
|
351
384
|
console List the daemon tab's console messages (errors, warnings, logs)
|
|
352
385
|
--filter <type> all | error | warning | log | info (default all)
|
|
353
386
|
--clear Empty the console buffer after reading
|
|
354
387
|
--json Print the raw messages as JSON
|
|
355
388
|
|
|
356
|
-
VISUAL REGRESSION
|
|
389
|
+
VISUAL REGRESSION
|
|
357
390
|
visual save <name> Save baseline screenshot
|
|
358
391
|
--url <url> Navigate to URL first
|
|
359
392
|
visual compare <name> Compare current page against baseline
|
|
@@ -361,19 +394,22 @@ VISUAL REGRESSION (v2.5.0)
|
|
|
361
394
|
visual list List all saved baselines
|
|
362
395
|
visual delete <name> Delete a baseline
|
|
363
396
|
|
|
364
|
-
AI VISUAL REGRESSION
|
|
397
|
+
AI VISUAL REGRESSION
|
|
365
398
|
ai-visual capture <url> Capture AI-analyzed visual baseline
|
|
366
399
|
--name <name> Baseline name (required)
|
|
367
400
|
--selector <sel> Capture specific element instead of viewport
|
|
368
401
|
--device <device> Device emulation (iphone-15, pixel-7, etc.)
|
|
402
|
+
--viewport <WxH> Page size (default: 1920x1080)
|
|
369
403
|
--width <n> Viewport width (default: 1920)
|
|
370
404
|
--height <n> Viewport height (default: 1080)
|
|
405
|
+
--browser <engine> chromium, firefox or webkit; the baseline keeps it
|
|
371
406
|
--wait <ms|selector> Wait before capture (ms or CSS selector)
|
|
372
407
|
Examples:
|
|
373
408
|
cbrowser ai-visual capture "https://example.com" --name homepage
|
|
374
409
|
cbrowser ai-visual capture "https://example.com/dashboard" --name dashboard --device iphone-15
|
|
375
410
|
|
|
376
411
|
ai-visual test <url> <baseline> Test current page against baseline using AI
|
|
412
|
+
Renders with the baseline's engine, device and size
|
|
377
413
|
--threshold <n> Similarity threshold 0-1 (default: 0.9)
|
|
378
414
|
--sensitivity <level> Detection sensitivity: low, medium, high (default: medium)
|
|
379
415
|
--html Generate HTML report
|
|
@@ -391,41 +427,13 @@ AI VISUAL REGRESSION (v7.0.0)
|
|
|
391
427
|
ai-visual show <name> Show baseline details
|
|
392
428
|
ai-visual delete <name> Delete a baseline
|
|
393
429
|
|
|
394
|
-
|
|
395
|
-
smart-baseline capture <url> Capture consensus baseline from multiple screenshots
|
|
396
|
-
--name <name> Baseline name (required)
|
|
397
|
-
--captures <n> Number of screenshots to take (default: 5)
|
|
398
|
-
--delay <ms> Delay between captures (default: 1500)
|
|
399
|
-
--selector <sel> Capture specific element
|
|
400
|
-
--device <device> Device emulation
|
|
401
|
-
--outlier-threshold <n> Outlier rejection threshold in stddev (default: 2.0)
|
|
402
|
-
Examples:
|
|
403
|
-
cbrowser smart-baseline capture "https://example.com" --name homepage
|
|
404
|
-
cbrowser smart-baseline capture "https://example.com" --name hero --captures 10
|
|
405
|
-
|
|
406
|
-
smart-baseline test <url> <name> Test against smart baseline
|
|
407
|
-
--threshold <n> Override adaptive threshold (0-1)
|
|
408
|
-
--transport-map Also generate visual transport map
|
|
409
|
-
Examples:
|
|
410
|
-
cbrowser smart-baseline test "https://staging.example.com" homepage
|
|
411
|
-
cbrowser smart-baseline test "https://staging.example.com" homepage --transport-map
|
|
412
|
-
|
|
413
|
-
smart-baseline list List all smart baselines
|
|
414
|
-
smart-baseline delete <name> Delete a smart baseline
|
|
415
|
-
|
|
416
|
-
transport-map <baseline> <current> Generate visual transport map between two screenshots
|
|
417
|
-
--cell-size <n> Grid cell size in pixels (default: 32)
|
|
418
|
-
--output <file.svg> Save SVG to file
|
|
419
|
-
--hotspots <n> Number of hotspots to identify (default: 5)
|
|
420
|
-
Examples:
|
|
421
|
-
cbrowser transport-map baseline.png current.png
|
|
422
|
-
cbrowser transport-map baseline.png current.png --cell-size 16 --output changes.svg
|
|
423
|
-
|
|
424
|
-
CROSS-BROWSER VISUAL TESTING (v7.3.0)
|
|
430
|
+
CROSS-BROWSER VISUAL TESTING
|
|
425
431
|
cross-browser <url> Compare visual rendering across browsers
|
|
426
432
|
--browsers <list> Browsers to test: chromium,firefox,webkit (default: all)
|
|
433
|
+
--viewport <WxH> Page size (default: 1920x1080)
|
|
427
434
|
--width <n> Viewport width (default: 1920)
|
|
428
435
|
--height <n> Viewport height (default: 1080)
|
|
436
|
+
--device <name> Device preset (firefox cannot emulate a mobile one)
|
|
429
437
|
--wait <ms> Wait before screenshot (ms)
|
|
430
438
|
--wait-for <selector> Wait for selector before screenshot
|
|
431
439
|
--sensitivity <level> Comparison sensitivity: low, medium, high
|
|
@@ -447,9 +455,11 @@ CROSS-BROWSER VISUAL TESTING (v7.3.0)
|
|
|
447
455
|
"options": { "browsers": ["chromium", "firefox"] }
|
|
448
456
|
}
|
|
449
457
|
|
|
450
|
-
RESPONSIVE VISUAL TESTING
|
|
458
|
+
RESPONSIVE VISUAL TESTING
|
|
451
459
|
responsive <url> Test visual rendering across viewport sizes
|
|
452
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)
|
|
453
463
|
--wait <ms> Wait before screenshot (ms)
|
|
454
464
|
--wait-for <selector> Wait for selector before screenshot
|
|
455
465
|
--sensitivity <level> Comparison sensitivity: low, medium, high
|
|
@@ -473,18 +483,19 @@ RESPONSIVE VISUAL TESTING (v7.3.0)
|
|
|
473
483
|
"options": { "viewports": ["mobile", "tablet", "desktop"] }
|
|
474
484
|
}
|
|
475
485
|
|
|
476
|
-
Available viewport presets:
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
desktop-sm (1280x800) desktop (1440x900) desktop-lg (1920x1080)
|
|
480
|
-
desktop-xl (2560x1440)
|
|
486
|
+
Available viewport presets (mobile and tablet emulate their device: touch,
|
|
487
|
+
user agent, pixel ratio):
|
|
488
|
+
${formatViewportPresetList(" ")}
|
|
481
489
|
|
|
482
|
-
A/B VISUAL COMPARISON
|
|
490
|
+
A/B VISUAL COMPARISON
|
|
483
491
|
ab <urlA> <urlB> Compare two URLs visually
|
|
484
492
|
--label-a <name> Label for URL A (default: "Version A")
|
|
485
493
|
--label-b <name> Label for URL B (default: "Version B")
|
|
494
|
+
--viewport <WxH> Page size (default: 1920x1080)
|
|
486
495
|
--width <n> Viewport width (default: 1920)
|
|
487
496
|
--height <n> Viewport height (default: 1080)
|
|
497
|
+
--device <name> Device preset
|
|
498
|
+
--browser <engine> chromium, firefox or webkit
|
|
488
499
|
--wait <ms> Wait before screenshot (ms)
|
|
489
500
|
--wait-for <selector> Wait for selector before screenshot
|
|
490
501
|
--sensitivity <level> Comparison sensitivity: low, medium, high
|
|
@@ -514,6 +525,9 @@ AI FRIENDLINESS - Audit sites for AI agent compatibility
|
|
|
514
525
|
|
|
515
526
|
agent-ready-audit <url> Audit site for AI-agent friendliness (A-F grade)
|
|
516
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)
|
|
517
531
|
--output <file> Save JSON report to file
|
|
518
532
|
--html Generate HTML report
|
|
519
533
|
Grades sites on: Findability (35%), Stability (30%),
|
|
@@ -524,6 +538,7 @@ AI FRIENDLINESS - Audit sites for AI agent compatibility
|
|
|
524
538
|
|
|
525
539
|
ai-benchmark Compare AI-friendliness across multiple sites
|
|
526
540
|
--urls <list> Comma-separated URLs to benchmark (required)
|
|
541
|
+
--concurrency <n> Sites audited at once (default: 3)
|
|
527
542
|
--output <file> Save JSON report to file
|
|
528
543
|
--html Generate HTML report
|
|
529
544
|
Runs agent-ready-audit on each URL and produces ranked comparison.
|
|
@@ -532,14 +547,17 @@ AI FRIENDLINESS - Audit sites for AI agent compatibility
|
|
|
532
547
|
cbrowser ai-benchmark --urls "https://amazon.com,https://ebay.com" --html
|
|
533
548
|
|
|
534
549
|
empathy-audit <url> Simulate disability experience on site
|
|
535
|
-
--goal <goal> Task goal (
|
|
536
|
-
--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
|
|
537
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
|
|
538
557
|
--output <file> Save JSON report to file
|
|
539
558
|
--html Generate HTML report
|
|
540
559
|
Available personas:
|
|
541
|
-
|
|
542
|
-
dyslexic-user, deaf-user, elderly-low-vision, color-blind-deuteranopia
|
|
560
|
+
${listAccessibilityPersonas().join(", ")}
|
|
543
561
|
Examples:
|
|
544
562
|
cbrowser empathy-audit "https://example.com" \\
|
|
545
563
|
--goal "complete checkout" \\
|
|
@@ -557,13 +575,13 @@ AI FRIENDLINESS - Audit sites for AI agent compatibility
|
|
|
557
575
|
cbrowser webmcp-ready "https://demo.cbrowser.ai/mcp"
|
|
558
576
|
cbrowser webmcp-ready "https://your-server.com/mcp" --api-key KEY --html
|
|
559
577
|
|
|
560
|
-
hunt-bugs <url> Automatically find UX bugs on a page
|
|
578
|
+
hunt-bugs <url> Automatically find UX bugs on a page (alias: hunt)
|
|
561
579
|
--max-pages <n> Max pages to crawl (default: 10)
|
|
562
580
|
--timeout <ms> Timeout in ms (default: 60000)
|
|
563
581
|
Examples:
|
|
564
582
|
cbrowser hunt-bugs "https://example.com"
|
|
565
583
|
|
|
566
|
-
competitive-benchmark Compare UX across competitor sites
|
|
584
|
+
competitive-benchmark Compare UX across competitor sites (needs an Anthropic API key)
|
|
567
585
|
--sites <list> Comma-separated site URLs (required)
|
|
568
586
|
--goal <goal> Task goal (required)
|
|
569
587
|
--persona <name> Persona to use (default: first-timer)
|
|
@@ -576,12 +594,12 @@ AI FRIENDLINESS - Audit sites for AI agent compatibility
|
|
|
576
594
|
--sites "https://yoursite.com,https://competitor.com" \\
|
|
577
595
|
--goal "sign up for free trial" --html
|
|
578
596
|
|
|
579
|
-
ACCESSIBILITY
|
|
597
|
+
ACCESSIBILITY
|
|
580
598
|
a11y audit Run WCAG accessibility audit
|
|
581
599
|
--url <url> Navigate to URL first
|
|
582
600
|
a11y audit [url] Audit a specific URL
|
|
583
601
|
|
|
584
|
-
TEST RECORDING
|
|
602
|
+
TEST RECORDING
|
|
585
603
|
record start Start recording interactions
|
|
586
604
|
--url <url> Navigate to URL to begin recording
|
|
587
605
|
record stop Stop recording and show actions
|
|
@@ -593,7 +611,7 @@ SCREEN CAPTURE (video/GIF — distinct from 'record', which captures actions)
|
|
|
593
611
|
capture start <url> Capture the screen to GIF/WebP/video
|
|
594
612
|
--fps <n> Target frames per second (default: 10)
|
|
595
613
|
--duration <5s> Auto-stop after this long (5s, 2m, 1500ms)
|
|
596
|
-
--viewport <WxH> Viewport size, e.g. 1280x720
|
|
614
|
+
--viewport <WxH> Viewport size, e.g. 1280x720, or a size name (mobile, tablet, desktop...)
|
|
597
615
|
--region <x,y,w,h> Capture a fixed region instead of the viewport
|
|
598
616
|
--element <selector> Track an element; crop follows it as it moves
|
|
599
617
|
--element-padding <n> Expand the element box by n CSS px per side
|
|
@@ -627,13 +645,16 @@ SCREEN CAPTURE (video/GIF — distinct from 'record', which captures actions)
|
|
|
627
645
|
and the still stretches are reported as frame_gaps rather than padded out.
|
|
628
646
|
mp4 needs a full ffmpeg (CBROWSER_FFMPEG_PATH); the bundled one does webm.
|
|
629
647
|
|
|
630
|
-
TEST EXPORT
|
|
648
|
+
TEST EXPORT
|
|
631
649
|
export junit <name> [output] Export test results as JUnit XML
|
|
632
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
|
|
633
652
|
|
|
634
|
-
WEBHOOKS
|
|
635
|
-
webhook add <name> <url> Add webhook notification
|
|
636
|
-
--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
|
|
637
658
|
--format <format> slack, discord, or generic
|
|
638
659
|
webhook list List configured webhooks
|
|
639
660
|
webhook delete <name> Delete a webhook
|
|
@@ -641,30 +662,37 @@ WEBHOOKS (v2.5.0)
|
|
|
641
662
|
|
|
642
663
|
PERSONA QUESTIONNAIRE
|
|
643
664
|
persona-questionnaire start Build a persona from a behavioural questionnaire
|
|
644
|
-
--comprehensive All
|
|
665
|
+
--comprehensive All 30 traits (default: 13 core traits)
|
|
645
666
|
--name <name> Persona name (prompted if omitted)
|
|
646
667
|
--output <file> Save answers to JSON
|
|
647
668
|
persona-questionnaire list-traits List available traits with descriptions
|
|
648
669
|
persona-questionnaire lookup Look up behaviours for a trait value
|
|
649
670
|
|
|
650
|
-
PARALLEL EXECUTION
|
|
671
|
+
PARALLEL EXECUTION
|
|
651
672
|
parallel devices <url> Run same URL across multiple devices
|
|
652
673
|
--devices <list> Comma-separated device names (default: all)
|
|
653
674
|
--concurrency <n> Max parallel browsers (default: 3)
|
|
675
|
+
--browser, --no-headless The browser for every device
|
|
654
676
|
parallel urls <urls> Run same task across multiple URLs
|
|
655
677
|
--concurrency <n> Max parallel browsers (default: 3)
|
|
678
|
+
--device, --viewport, --browser, --no-headless The browser for every URL
|
|
656
679
|
parallel perf <urls> Performance audit multiple URLs in parallel
|
|
657
680
|
--concurrency <n> Max parallel browsers (default: 3)
|
|
681
|
+
--device, --viewport, --browser, --no-headless The browser for every URL
|
|
658
682
|
|
|
659
|
-
NATURAL LANGUAGE
|
|
683
|
+
NATURAL LANGUAGE
|
|
660
684
|
run "<command>" Execute natural language command
|
|
661
685
|
Examples:
|
|
662
686
|
cbrowser run "go to https://example.com"
|
|
663
687
|
cbrowser run "click the login button"
|
|
664
688
|
cbrowser run "type 'hello' in the search box"
|
|
665
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.
|
|
666
694
|
|
|
667
|
-
VISUAL AI
|
|
695
|
+
VISUAL AI
|
|
668
696
|
ai find "<intent>" Find element by semantic intent
|
|
669
697
|
Examples:
|
|
670
698
|
cbrowser ai find "the cheapest product"
|
|
@@ -672,23 +700,20 @@ VISUAL AI (v4.0.0)
|
|
|
672
700
|
cbrowser ai find "search box"
|
|
673
701
|
ai click "<intent>" Find and click element by intent
|
|
674
702
|
|
|
675
|
-
|
|
676
|
-
hunt <url> Automatically find bugs on a page
|
|
677
|
-
--max-pages <n> Max pages to crawl (default: 10)
|
|
678
|
-
--timeout <ms> Timeout in ms (default: 60000)
|
|
679
|
-
|
|
680
|
-
CROSS-BROWSER (v4.0.0)
|
|
703
|
+
BROWSER DIFF
|
|
681
704
|
diff <url> Compare page across browsers
|
|
682
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)
|
|
683
708
|
|
|
684
|
-
CHAOS ENGINEERING
|
|
709
|
+
CHAOS ENGINEERING
|
|
685
710
|
chaos <url> Test app resilience
|
|
686
711
|
--latency <ms> Add network latency
|
|
687
712
|
--offline Simulate offline mode
|
|
688
713
|
--block <patterns> Block URL patterns (comma-separated)
|
|
689
714
|
--fail-api <pattern:status> Fail specific API calls
|
|
690
715
|
|
|
691
|
-
SMART RETRY & ASSERTIONS
|
|
716
|
+
SMART RETRY & ASSERTIONS
|
|
692
717
|
smart-click <selector> Click with auto-retry and alternative selectors
|
|
693
718
|
--url <url> Navigate to URL first
|
|
694
719
|
--max-retries <n> Maximum retry attempts (default: 3)
|
|
@@ -707,7 +732,7 @@ SMART RETRY & ASSERTIONS (v5.0.0)
|
|
|
707
732
|
cbrowser assert "'Login' button exists"
|
|
708
733
|
cbrowser assert "page has 3 items"
|
|
709
734
|
|
|
710
|
-
SELF-HEALING SELECTORS
|
|
735
|
+
SELF-HEALING SELECTORS
|
|
711
736
|
heal stats Show selector cache statistics
|
|
712
737
|
heal list List all cached selector mappings
|
|
713
738
|
--domain <domain> Filter by domain
|
|
@@ -716,18 +741,22 @@ SELF-HEALING SELECTORS (v5.0.0)
|
|
|
716
741
|
heal test <selector> Test if a selector would be healed
|
|
717
742
|
--url <url> Navigate to URL first
|
|
718
743
|
|
|
719
|
-
AI TEST GENERATION
|
|
744
|
+
AI TEST GENERATION
|
|
720
745
|
generate <url> Analyze page and generate test scenarios
|
|
721
746
|
--format <format> Output: summary, cbrowser, playwright, all (default: summary)
|
|
722
747
|
--output <file> Save to file instead of stdout
|
|
723
748
|
analyze <url> Show page analysis without generating tests
|
|
749
|
+
--output <file> Save the analysis as JSON
|
|
724
750
|
|
|
725
|
-
MCP SERVER
|
|
751
|
+
MCP SERVER
|
|
726
752
|
mcp-server Start CBrowser as MCP server for Claude integration
|
|
727
753
|
Use with Claude Desktop or other MCP-compatible clients
|
|
728
754
|
mcp-remote Start remote HTTP MCP server for claude.ai connectors
|
|
729
755
|
--port <port> Port to listen on (default: 3000)
|
|
730
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.
|
|
731
760
|
|
|
732
761
|
UNINSTALL
|
|
733
762
|
uninstall Remove CBrowser data: credentials, sessions, captures
|
|
@@ -742,49 +771,58 @@ CLAUDE SKILL INSTALLATION & SYNC
|
|
|
742
771
|
Downloads latest skill files from GitHub
|
|
743
772
|
Run after 'npm update -g cbrowser' to stay in sync
|
|
744
773
|
Add to skill-index.json after first installation
|
|
745
|
-
--stateful Use stateful session mode
|
|
746
774
|
|
|
747
|
-
DAEMON MODE
|
|
775
|
+
DAEMON MODE
|
|
748
776
|
daemon start Start background daemon (keeps browser running)
|
|
749
777
|
--port <port> Daemon port (default: 9222)
|
|
750
778
|
--device <name> Device the daemon's browser emulates
|
|
751
779
|
--viewport <WxH> Page size of the daemon's browser
|
|
752
|
-
--timeout <min> Idle timeout
|
|
780
|
+
--timeout <min> Idle timeout: minutes, or 90s / 45m / 2h (default: 30)
|
|
753
781
|
daemon stop Stop the running daemon
|
|
754
|
-
daemon status Check if daemon is running
|
|
782
|
+
daemon status Check if daemon is running (exits 1 when it is not)
|
|
755
783
|
daemon run Run daemon in foreground (internal use)
|
|
756
|
-
Note:
|
|
757
|
-
|
|
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.
|
|
758
789
|
|
|
759
|
-
API CONFIGURATION
|
|
790
|
+
API CONFIGURATION
|
|
760
791
|
config set-api-key <key> Set Anthropic API key for cognitive journeys
|
|
761
792
|
config show-api-key Show configured API key (masked)
|
|
762
793
|
config remove-api-key Remove stored API key
|
|
763
794
|
config set-model <model> Set Claude model (default: claude-sonnet-5)
|
|
764
795
|
|
|
765
|
-
LIGHTPANDA
|
|
796
|
+
LIGHTPANDA - High-Performance Headless Browser (OPT-IN ONLY)
|
|
766
797
|
lightpanda-status Check Lightpanda availability and configuration
|
|
767
798
|
lightpanda-setup Show setup instructions with security warnings
|
|
768
799
|
⚠️ SECURITY NOTICE:
|
|
769
800
|
• Lightpanda is BETA software with no security audit
|
|
770
801
|
• Cloud mode exposes traffic to lightpanda.io servers
|
|
771
802
|
• NEVER use for auth, credentials, or payments
|
|
772
|
-
•
|
|
803
|
+
• Used only by agent-ready-audit, and only with --lightpanda
|
|
773
804
|
Environment Variables:
|
|
774
805
|
LIGHTPANDA_ENDPOINT WebSocket endpoint (e.g., ws://127.0.0.1:9222)
|
|
775
806
|
LIGHTPANDA_TOKEN Cloud API token (⚠️ data visible to lightpanda.io)
|
|
776
807
|
Examples:
|
|
777
808
|
cbrowser lightpanda-status
|
|
778
|
-
cbrowser screenshot https://example.com --lightpanda
|
|
779
809
|
cbrowser agent-ready-audit https://example.com --lightpanda
|
|
780
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
|
+
|
|
781
819
|
DIAGNOSTICS
|
|
782
820
|
version, -v, --version Show version number
|
|
783
821
|
status Show environment status and diagnostics
|
|
784
822
|
Displays data directories, browsers, config, heal cache
|
|
785
823
|
doctor Check environment health (Node, Playwright, API key)
|
|
786
824
|
|
|
787
|
-
|
|
825
|
+
OUTPUT MODES (accessibility)
|
|
788
826
|
--no-color Disable all color output (also respects NO_COLOR env var)
|
|
789
827
|
--plain Disable emoji, box-drawing, and unicode decorations
|
|
790
828
|
--json-output Output structured JSON (for scripts and screen readers)
|
|
@@ -796,6 +834,7 @@ STORAGE & CLEANUP
|
|
|
796
834
|
--older-than <days> Delete files older than N days (default: 7)
|
|
797
835
|
--keep-screenshots <n> Keep at least N screenshots (default: 10)
|
|
798
836
|
--keep-journeys <n> Keep at least N journeys (default: 5)
|
|
837
|
+
--keep-sessions <n> Keep at least N sessions (default: 3)
|
|
799
838
|
reset Clear persistent browser state (cookies, storage)
|
|
800
839
|
|
|
801
840
|
OPTIONS
|
|
@@ -803,6 +842,8 @@ OPTIONS
|
|
|
803
842
|
--device <name> Device preset: iphone-16, pixel-9, ipad-pro-13, ... ('device list')
|
|
804
843
|
--viewport <WxH> Page size, e.g. 390x844; with --device it sets the size and
|
|
805
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
|
|
806
847
|
--geo <location> Location preset or lat,lon coordinates
|
|
807
848
|
--locale <locale> Browser locale (e.g., en-US, fr-FR)
|
|
808
849
|
--timezone <tz> Timezone (e.g., America/New_York)
|
|
@@ -870,8 +911,27 @@ const COMMAND_ALIASES = {
|
|
|
870
911
|
"chaos-test": "chaos",
|
|
871
912
|
"generate-tests": "generate",
|
|
872
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",
|
|
873
920
|
};
|
|
874
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
|
+
}
|
|
875
935
|
function escapeForRegExp(value) {
|
|
876
936
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
877
937
|
}
|
|
@@ -976,10 +1036,18 @@ function showCommandHelp(command) {
|
|
|
976
1036
|
* against a second browser while the daemon holds the first. Anything listed
|
|
977
1037
|
* here must either be daemon-routed or refuse to run.
|
|
978
1038
|
*/
|
|
1039
|
+
/** What `extract` can return (browser.extract). */
|
|
1040
|
+
const EXTRACT_KINDS = ["text", "links", "headings", "images", "forms"];
|
|
979
1041
|
const DRIVES_THE_PAGE = [
|
|
980
1042
|
"navigate", "click", "fill", "hover", "screenshot", "extract", "run",
|
|
981
1043
|
"keyboard", "press", "type", "evaluate", "eval", "drag", "upload",
|
|
982
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",
|
|
983
1051
|
];
|
|
984
1052
|
/**
|
|
985
1053
|
* Flags the shared CBrowser construction consumes, for every command that uses
|
|
@@ -991,7 +1059,12 @@ const SHARED_BROWSER_FLAGS = [
|
|
|
991
1059
|
"url", "browser", "device", "viewport", "headless", "persistent", "restore",
|
|
992
1060
|
"locale", "timezone", "geo", "verbose",
|
|
993
1061
|
];
|
|
994
|
-
|
|
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"];
|
|
995
1068
|
/**
|
|
996
1069
|
* Declared options per command.
|
|
997
1070
|
*
|
|
@@ -1022,25 +1095,19 @@ const COMMAND_FLAGS = {
|
|
|
1022
1095
|
evaluate: ["file", "arg", "json", "raw", "wait-for", "timeout", "expect-truthy", "force"],
|
|
1023
1096
|
eval: ["file", "arg", "json", "raw", "wait-for", "timeout", "expect-truthy", "force"],
|
|
1024
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"],
|
|
1025
1102
|
"test-suite": [
|
|
1026
1103
|
"capture", "capture-fps", "capture-format", "capture-out",
|
|
1027
|
-
"
|
|
1028
|
-
"
|
|
1029
|
-
"html", "include", "inline", "length", "lightpanda", "max-pages", "max-steps",
|
|
1030
|
-
"max-time", "min-coverage", "name", "no-vision", "oauth-token", "output",
|
|
1031
|
-
"persona", "retries", "runs", "screenshot-on-failure", "sensitivity",
|
|
1032
|
-
"sitemap", "sites", "start", "step-through", "tests", "threshold",
|
|
1033
|
-
"threshold-cls", "threshold-fcp", "threshold-lcp", "threshold-tbt",
|
|
1034
|
-
"threshold-ttfb", "threshold-tti", "timeout", "trait", "urls", "value",
|
|
1035
|
-
"verify", "wcag-level", "geolocation", "geo-region",
|
|
1104
|
+
"continue-on-failure", "dry-run", "fuzzy-match", "html", "inline", "output",
|
|
1105
|
+
"screenshot-on-failure", "step-through", "timeout",
|
|
1036
1106
|
],
|
|
1037
1107
|
"cognitive-journey": [
|
|
1038
1108
|
"capture", "capture-fps", "capture-format", "capture-out",
|
|
1039
|
-
"
|
|
1040
|
-
"
|
|
1041
|
-
"oauth-token", "output", "persona", "sites", "start", "timeout", "trait",
|
|
1042
|
-
"urls", "value", "wcag-level", "geolocation", "geo-region", "vision",
|
|
1043
|
-
"trace", "replay-trace",
|
|
1109
|
+
"goal", "persona", "start", "max-steps", "max-time", "no-vision", "output",
|
|
1110
|
+
"geolocation", "trace", "replay-trace",
|
|
1044
1111
|
],
|
|
1045
1112
|
};
|
|
1046
1113
|
/**
|
|
@@ -1057,11 +1124,29 @@ const UNVALIDATED_REASON = "not yet declared - see COMMAND_FLAGS in src/cli.ts";
|
|
|
1057
1124
|
* Fails on the FIRST invalid flag with the command's full accepted set, so a
|
|
1058
1125
|
* typo is a one-command fix rather than a guessing game.
|
|
1059
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
|
+
}
|
|
1060
1145
|
function validateCommandFlags(command, options) {
|
|
1061
1146
|
const declared = COMMAND_FLAGS[command];
|
|
1062
1147
|
if (!declared)
|
|
1063
1148
|
return; // UNVALIDATED_REASON
|
|
1064
|
-
const allowed =
|
|
1149
|
+
const allowed = allowedFlags(command);
|
|
1065
1150
|
const unknown = Object.keys(options).filter((flag) => !allowed.has(flag));
|
|
1066
1151
|
if (unknown.length === 0)
|
|
1067
1152
|
return;
|
|
@@ -1086,14 +1171,66 @@ function captureStatePath() {
|
|
|
1086
1171
|
return join(getPaths().videosDir, "active.json");
|
|
1087
1172
|
}
|
|
1088
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
|
+
}
|
|
1089
1226
|
function parseDurationMs(input) {
|
|
1090
|
-
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());
|
|
1091
1228
|
if (!match) {
|
|
1092
|
-
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`);
|
|
1093
1230
|
}
|
|
1094
1231
|
const value = parseFloat(match[1]);
|
|
1095
1232
|
const unit = (match[2] || "ms").toLowerCase();
|
|
1096
|
-
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;
|
|
1097
1234
|
if (ms <= 0)
|
|
1098
1235
|
throw new Error(`Invalid duration "${input}" - must be greater than zero`);
|
|
1099
1236
|
return Math.round(ms);
|
|
@@ -1112,9 +1249,13 @@ function parseFormats(input) {
|
|
|
1112
1249
|
}
|
|
1113
1250
|
/** Parse "1280x720". */
|
|
1114
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 };
|
|
1115
1255
|
const match = /^(\d+)x(\d+)$/i.exec(input.trim());
|
|
1116
|
-
if (!match)
|
|
1117
|
-
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
|
+
}
|
|
1118
1259
|
const width = parseInt(match[1], 10);
|
|
1119
1260
|
const height = parseInt(match[2], 10);
|
|
1120
1261
|
if (width <= 0 || height <= 0) {
|
|
@@ -1152,15 +1293,7 @@ function parseRegion(input) {
|
|
|
1152
1293
|
* confident "✓" for a capture the user did not ask for, which is worse than
|
|
1153
1294
|
* an error. The globals are the ones the CLI itself consumes before dispatch.
|
|
1154
1295
|
*/
|
|
1155
|
-
const CAPTURE_FLAGS =
|
|
1156
|
-
// capture-specific
|
|
1157
|
-
"fps", "duration", "format", "out", "name", "quality", "max-frames",
|
|
1158
|
-
"viewport", "region", "element", "element-padding",
|
|
1159
|
-
"contact-sheet", "attention-overlay", "overlay-persona", "overlay-goal",
|
|
1160
|
-
"after", "after-element", "after-delay",
|
|
1161
|
-
"until-element", "until-element-gone", "until-idle", "timeout",
|
|
1162
|
-
...SHARED_BROWSER_FLAGS,
|
|
1163
|
-
]);
|
|
1296
|
+
const CAPTURE_FLAGS = allowedFlags("capture");
|
|
1164
1297
|
/**
|
|
1165
1298
|
* Resolve `--device` to a real preset key, or exit naming the bad value.
|
|
1166
1299
|
*
|
|
@@ -1513,12 +1646,19 @@ function failCaptureCommand(message) {
|
|
|
1513
1646
|
* later `cbrowser click` or `cbrowser fill` appear in the recording.
|
|
1514
1647
|
*/
|
|
1515
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
|
+
}
|
|
1516
1657
|
const target = buildCaptureTarget(options);
|
|
1517
1658
|
const triggers = buildCaptureTriggers(options);
|
|
1518
1659
|
const slug = slugFor(options);
|
|
1519
1660
|
const response = await sendToDaemon("captureStart", {
|
|
1520
1661
|
url,
|
|
1521
|
-
engine: options.browser === "firefox" ? "firefox" : options.browser === "webkit" ? "webkit" : "chromium",
|
|
1522
1662
|
options: {
|
|
1523
1663
|
fps: options.fps !== undefined ? Number(options.fps) : undefined,
|
|
1524
1664
|
quality: options.quality !== undefined ? Number(options.quality) : undefined,
|
|
@@ -1528,6 +1668,16 @@ async function runCaptureStartViaDaemon(url, options) {
|
|
|
1528
1668
|
contactSheet: options["contact-sheet"] === true,
|
|
1529
1669
|
slug,
|
|
1530
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
|
+
: {}),
|
|
1531
1681
|
...triggers,
|
|
1532
1682
|
},
|
|
1533
1683
|
});
|
|
@@ -1758,10 +1908,7 @@ function reportAutoCapture(capture) {
|
|
|
1758
1908
|
// evaluate — run JavaScript in the page
|
|
1759
1909
|
// =========================================================================
|
|
1760
1910
|
/** Flags `evaluate` understands; anything else is rejected rather than ignored. */
|
|
1761
|
-
const EVALUATE_FLAGS =
|
|
1762
|
-
"file", "arg", "json", "raw", "wait-for", "timeout", "expect-truthy", "force",
|
|
1763
|
-
...SHARED_BROWSER_FLAGS,
|
|
1764
|
-
]);
|
|
1911
|
+
const EVALUATE_FLAGS = allowedFlags("evaluate");
|
|
1765
1912
|
/**
|
|
1766
1913
|
* `--force` is a bare flag, but the parser gives any flag the next non-flag
|
|
1767
1914
|
* word as its value, so `evaluate --force "document.title"` reads the SCRIPT as
|
|
@@ -1794,14 +1941,14 @@ function repeatedFlag(name) {
|
|
|
1794
1941
|
return values;
|
|
1795
1942
|
}
|
|
1796
1943
|
/**
|
|
1797
|
-
*
|
|
1798
|
-
*
|
|
1799
|
-
*
|
|
1800
|
-
*
|
|
1801
|
-
*
|
|
1802
|
-
*
|
|
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.
|
|
1803
1950
|
*/
|
|
1804
|
-
async function
|
|
1951
|
+
async function prepareEvaluate(args, options) {
|
|
1805
1952
|
const unknown = Object.keys(options).filter((f) => !EVALUATE_FLAGS.has(f));
|
|
1806
1953
|
if (unknown.length > 0) {
|
|
1807
1954
|
console.error(`Error: 'evaluate' does not support ${unknown.map((f) => `--${f}`).join(", ")}`);
|
|
@@ -1870,45 +2017,53 @@ async function runEvaluate(browser, args, options) {
|
|
|
1870
2017
|
}
|
|
1871
2018
|
// Said on stderr so stdout stays exactly the result (--raw / --json pipes).
|
|
1872
2019
|
console.error("⚠ Red zone: running caller-supplied page script (--force)");
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
if (typeof options["wait-for"] === "string") {
|
|
1877
|
-
// Bounded by the caller, not by a constant.
|
|
1878
|
-
//
|
|
1879
|
-
// This was a hardcoded 30s, which is two defects wearing one number. For a
|
|
1880
|
-
// user it meant no way to fail fast on a page that renders in 200ms and no
|
|
1881
|
-
// way to wait longer on one that does not. For the suite it meant the test
|
|
1882
|
-
// asserting that a never-matching selector fails loudly spent 30 of its 60
|
|
1883
|
-
// second budget deliberately waiting, so browser launch plus teardown had
|
|
1884
|
-
// to fit in the rest -- which held when the test ran alone and did not when
|
|
1885
|
-
// the full suite ran browsers in parallel. The test read as flaky and was
|
|
1886
|
-
// not: it was structurally tight against a constant it could not reach.
|
|
1887
|
-
// (2026-08-02)
|
|
1888
|
-
// Falls back to the configured default rather than a fresh literal.
|
|
1889
|
-
// `CBROWSER_TIMEOUT` is documented in --help as "Default timeout in ms" and
|
|
1890
|
-
// config.ts already resolves it; a second hardcoded 30_000 here would mean
|
|
1891
|
-
// 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) {
|
|
1892
2023
|
const { getDefaultConfig } = await import("./config.js");
|
|
1893
2024
|
const configuredMs = getDefaultConfig().timeout ?? 30_000;
|
|
1894
|
-
|
|
1895
|
-
: typeof options.timeout === "number" ? options.timeout
|
|
1896
|
-
: configuredMs;
|
|
2025
|
+
waitMs = typeof options.timeout === "number" ? options.timeout : timeoutOption(options.timeout, configuredMs);
|
|
1897
2026
|
if (!Number.isFinite(waitMs) || waitMs <= 0) {
|
|
1898
2027
|
console.error(`Error: --timeout must be a positive number of milliseconds, got: ${options.timeout}`);
|
|
1899
2028
|
process.exit(1);
|
|
1900
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) {
|
|
1901
2056
|
try {
|
|
1902
|
-
await page.waitForSelector(
|
|
2057
|
+
await page.waitForSelector(prep.waitFor, { timeout: prep.waitMs });
|
|
1903
2058
|
}
|
|
1904
2059
|
catch {
|
|
1905
|
-
console.error(`✗ Timed out waiting for selector: ${
|
|
2060
|
+
console.error(`✗ Timed out waiting for selector: ${prep.waitFor} (after ${prep.waitMs}ms)`);
|
|
1906
2061
|
process.exit(1);
|
|
1907
2062
|
}
|
|
1908
2063
|
}
|
|
1909
2064
|
let result;
|
|
1910
2065
|
try {
|
|
1911
|
-
result = await evaluateScript(page, script, evalArgs);
|
|
2066
|
+
result = await evaluateScript(page, prep.script, prep.evalArgs);
|
|
1912
2067
|
}
|
|
1913
2068
|
catch (e) {
|
|
1914
2069
|
const error = e;
|
|
@@ -1918,20 +2073,7 @@ async function runEvaluate(browser, args, options) {
|
|
|
1918
2073
|
console.error(error.stack.split("\n").slice(1).join("\n"));
|
|
1919
2074
|
process.exit(1);
|
|
1920
2075
|
}
|
|
1921
|
-
|
|
1922
|
-
console.log(String(result));
|
|
1923
|
-
}
|
|
1924
|
-
else if (options.json === true) {
|
|
1925
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1926
|
-
}
|
|
1927
|
-
else {
|
|
1928
|
-
// Legacy default, kept so the existing one-arg form behaves identically.
|
|
1929
|
-
console.log(typeof result === "string" ? result : JSON.stringify(result, null, 2));
|
|
1930
|
-
}
|
|
1931
|
-
if (options["expect-truthy"] === true && !result) {
|
|
1932
|
-
console.error(`✗ Expected a truthy result, got ${JSON.stringify(result)}`);
|
|
1933
|
-
process.exit(1);
|
|
1934
|
-
}
|
|
2076
|
+
printEvaluateResult(result, options);
|
|
1935
2077
|
}
|
|
1936
2078
|
// =========================================================================
|
|
1937
2079
|
// keyboard — sequences, chords and held modifiers
|
|
@@ -2029,11 +2171,26 @@ function parseKeyboardToken(token, literal = false) {
|
|
|
2029
2171
|
}
|
|
2030
2172
|
return { kind: "text", text: token };
|
|
2031
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
|
+
}
|
|
2032
2192
|
/** Flags `keyboard` understands. */
|
|
2033
|
-
const KEYBOARD_FLAGS =
|
|
2034
|
-
"delay", "selector", "hold", "repeat", "text", "force",
|
|
2035
|
-
...SHARED_BROWSER_FLAGS,
|
|
2036
|
-
]);
|
|
2193
|
+
const KEYBOARD_FLAGS = allowedFlags("keyboard");
|
|
2037
2194
|
/**
|
|
2038
2195
|
* Drive a keyboard sequence.
|
|
2039
2196
|
*
|
|
@@ -2053,16 +2210,7 @@ async function runKeyboard(browser, args, options) {
|
|
|
2053
2210
|
console.error(" Run 'cbrowser help keyboard' for options");
|
|
2054
2211
|
process.exit(1);
|
|
2055
2212
|
}
|
|
2056
|
-
const
|
|
2057
|
-
if (!Number.isFinite(delay) || delay < 0) {
|
|
2058
|
-
console.error(`Error: --delay must be a non-negative number (got "${String(options.delay)}")`);
|
|
2059
|
-
process.exit(1);
|
|
2060
|
-
}
|
|
2061
|
-
const repeat = options.repeat !== undefined ? Number(options.repeat) : 1;
|
|
2062
|
-
if (!Number.isInteger(repeat) || repeat < 1) {
|
|
2063
|
-
console.error(`Error: --repeat must be a positive integer (got "${String(options.repeat)}")`);
|
|
2064
|
-
process.exit(1);
|
|
2065
|
-
}
|
|
2213
|
+
const { delay, repeat } = parseKeyboardNumbers(options);
|
|
2066
2214
|
const hold = typeof options.hold === "string"
|
|
2067
2215
|
? options.hold.split("+").map((m) => m.trim()).filter(Boolean)
|
|
2068
2216
|
: [];
|
|
@@ -2572,8 +2720,29 @@ function parseGeoLocation(location) {
|
|
|
2572
2720
|
}
|
|
2573
2721
|
return null;
|
|
2574
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
|
+
}
|
|
2575
2744
|
async function main() {
|
|
2576
|
-
const { command, args, options } = parseArgs(process.argv.slice(2));
|
|
2745
|
+
const { command, args, options } = rewriteScroll(parseArgs(process.argv.slice(2)));
|
|
2577
2746
|
// Initialize accessible output system (v18.54.0)
|
|
2578
2747
|
const outputOpts = detectOutputOptions(options);
|
|
2579
2748
|
setOutputOptions(outputOpts);
|
|
@@ -2615,6 +2784,35 @@ async function main() {
|
|
|
2615
2784
|
// path unreachable from a shell no matter what the checks found.
|
|
2616
2785
|
process.exit(process.exitCode ?? 0);
|
|
2617
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
|
+
}
|
|
2618
2816
|
// MCP Server mode - runs before browser instantiation
|
|
2619
2817
|
if (command === "mcp-server") {
|
|
2620
2818
|
console.error("🔌 Starting CBrowser MCP Server...");
|
|
@@ -2758,17 +2956,10 @@ async function main() {
|
|
|
2758
2956
|
console.log("Creating ~/.claude/skills directory...");
|
|
2759
2957
|
fs.mkdirSync(skillsDir, { recursive: true });
|
|
2760
2958
|
}
|
|
2761
|
-
//
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
}
|
|
2766
|
-
// Create directories
|
|
2767
|
-
console.log("Creating skill directory structure...");
|
|
2768
|
-
fs.mkdirSync(path.join(skillDir, "Workflows"), { recursive: true });
|
|
2769
|
-
fs.mkdirSync(path.join(skillDir, "Tools"), { recursive: true });
|
|
2770
|
-
fs.mkdirSync(path.join(skillDir, ".memory", "sessions"), { recursive: true });
|
|
2771
|
-
// 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).
|
|
2772
2963
|
console.log("Downloading skill files...");
|
|
2773
2964
|
const files = [
|
|
2774
2965
|
"SKILL.md",
|
|
@@ -2785,22 +2976,35 @@ async function main() {
|
|
|
2785
2976
|
"Workflows/Journey.md",
|
|
2786
2977
|
"Tools/CBrowser.ts",
|
|
2787
2978
|
];
|
|
2979
|
+
const downloaded = new Map();
|
|
2980
|
+
const failed = [];
|
|
2788
2981
|
for (const file of files) {
|
|
2789
2982
|
try {
|
|
2790
2983
|
const url = `${repoUrl}/skill/${file}`;
|
|
2791
|
-
const dest = path.join(skillDir, file);
|
|
2792
2984
|
console.log(` - ${file}`);
|
|
2793
|
-
const response = await fetch(url);
|
|
2985
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(30_000) });
|
|
2794
2986
|
if (!response.ok) {
|
|
2795
2987
|
throw new Error(`HTTP ${response.status}`);
|
|
2796
2988
|
}
|
|
2797
|
-
|
|
2798
|
-
fs.writeFileSync(dest, content, "utf-8");
|
|
2989
|
+
downloaded.set(file, await response.text());
|
|
2799
2990
|
}
|
|
2800
2991
|
catch (e) {
|
|
2801
|
-
|
|
2992
|
+
failed.push(`${file} (${e.message})`);
|
|
2802
2993
|
}
|
|
2803
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
|
+
}
|
|
2804
3008
|
console.log(`
|
|
2805
3009
|
╔═══════════════════════════════════════════════════════════════╗
|
|
2806
3010
|
║ CBrowser Skill Installed Successfully! ║
|
|
@@ -2843,11 +3047,52 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
2843
3047
|
}
|
|
2844
3048
|
launchArgs.push("--viewport", options.viewport);
|
|
2845
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
|
+
}
|
|
2846
3091
|
console.log("🚀 Starting CBrowser daemon...");
|
|
2847
3092
|
const result = await startDaemon(port, launchArgs);
|
|
2848
3093
|
console.log(result.success ? `✓ ${result.message}` : `✗ ${result.message}`);
|
|
2849
3094
|
if (result.alreadyRunning && launchArgs.length > 0) {
|
|
2850
|
-
console.error("⚠
|
|
3095
|
+
console.error("⚠ The browser flags were not applied: the daemon was already running. Run cbrowser daemon stop first.");
|
|
2851
3096
|
}
|
|
2852
3097
|
process.exit(result.success ? 0 : 1);
|
|
2853
3098
|
break;
|
|
@@ -2856,13 +3101,14 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
2856
3101
|
console.log("🛑 Stopping CBrowser daemon...");
|
|
2857
3102
|
const result = await stopDaemon();
|
|
2858
3103
|
console.log(result.success ? `✓ ${result.message}` : `✗ ${result.message}`);
|
|
2859
|
-
process.exit(0);
|
|
3104
|
+
process.exit(result.success ? 0 : 1);
|
|
2860
3105
|
break;
|
|
2861
3106
|
}
|
|
2862
3107
|
case "status": {
|
|
2863
3108
|
const status = await getDaemonStatus();
|
|
2864
3109
|
console.log(status);
|
|
2865
|
-
|
|
3110
|
+
// Non-zero when nothing is running, so `cbrowser daemon status && ...` works.
|
|
3111
|
+
process.exit((await isDaemonRunning()) ? 0 : 1);
|
|
2866
3112
|
break;
|
|
2867
3113
|
}
|
|
2868
3114
|
case "run": {
|
|
@@ -2878,7 +3124,10 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
2878
3124
|
headless: options.headless !== false && options.headless !== "false",
|
|
2879
3125
|
...(typeof options.device === "string" ? { device: options.device } : {}),
|
|
2880
3126
|
...(runViewport ? { viewportWidth: runViewport.width, viewportHeight: runViewport.height, viewportOverride: runViewport } : {}),
|
|
2881
|
-
|
|
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);
|
|
2882
3131
|
return;
|
|
2883
3132
|
}
|
|
2884
3133
|
default:
|
|
@@ -2899,27 +3148,35 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
2899
3148
|
"keyboard", "press", "type", "evaluate", "eval",
|
|
2900
3149
|
];
|
|
2901
3150
|
if (daemonRunning && INTERACTION_COMMANDS.includes(command)) {
|
|
2902
|
-
|
|
2903
|
-
//
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
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");
|
|
2908
3161
|
}
|
|
2909
|
-
|
|
3162
|
+
// The daemon knows "evaluate"; "eval" reached it unchanged and failed.
|
|
3163
|
+
const daemonCommand = command === "eval" ? "evaluate" : command;
|
|
2910
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 } : {};
|
|
2911
3168
|
switch (command) {
|
|
2912
3169
|
case "navigate":
|
|
2913
3170
|
daemonArgs = { url: args[0] || options.url };
|
|
2914
3171
|
break;
|
|
2915
3172
|
case "click":
|
|
2916
|
-
daemonArgs = { selector: args[0], force: options.force === true };
|
|
3173
|
+
daemonArgs = { selector: args[0], force: options.force === true, ...urlArg };
|
|
2917
3174
|
break;
|
|
2918
3175
|
case "hover":
|
|
2919
|
-
daemonArgs = { selector: args[0] };
|
|
3176
|
+
daemonArgs = { selector: args[0], ...urlArg };
|
|
2920
3177
|
break;
|
|
2921
3178
|
case "fill":
|
|
2922
|
-
daemonArgs = { selector: args[0], value: args[1] };
|
|
3179
|
+
daemonArgs = { selector: args[0], value: args[1], ...urlArg };
|
|
2923
3180
|
break;
|
|
2924
3181
|
case "keyboard": {
|
|
2925
3182
|
validateCommandFlags("keyboard", options);
|
|
@@ -2941,46 +3198,45 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
2941
3198
|
process.exit(1);
|
|
2942
3199
|
}
|
|
2943
3200
|
}
|
|
3201
|
+
const { delay, repeat } = parseKeyboardNumbers(options);
|
|
2944
3202
|
daemonArgs = {
|
|
2945
3203
|
steps,
|
|
2946
3204
|
hold,
|
|
2947
|
-
delay
|
|
2948
|
-
repeat
|
|
3205
|
+
delay,
|
|
3206
|
+
repeat,
|
|
2949
3207
|
selector: typeof options.selector === "string" ? options.selector : undefined,
|
|
2950
3208
|
force: options.force === true,
|
|
3209
|
+
...urlArg,
|
|
2951
3210
|
};
|
|
2952
3211
|
break;
|
|
2953
3212
|
}
|
|
2954
3213
|
case "press":
|
|
2955
|
-
daemonArgs = { key: args[0], force: options.force === true };
|
|
3214
|
+
daemonArgs = { key: args[0], force: options.force === true, ...urlArg };
|
|
2956
3215
|
break;
|
|
2957
|
-
case "type":
|
|
2958
|
-
|
|
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 };
|
|
2959
3223
|
break;
|
|
3224
|
+
}
|
|
2960
3225
|
case "eval":
|
|
2961
3226
|
case "evaluate": {
|
|
2962
3227
|
validateCommandFlags(command, options);
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
console.error(`✗ ${refusal}`);
|
|
2976
|
-
console.error(" Re-run with --force to run it anyway.");
|
|
2977
|
-
process.exit(1);
|
|
2978
|
-
}
|
|
2979
|
-
// The raw script: the daemon compiles it with the same evaluateScript as the
|
|
2980
|
-
// local path. This hand-built body treated a "return" anywhere - even inside
|
|
2981
|
-
// a nested function - as statements, and an IIFE's value was dropped.
|
|
2982
|
-
// (2026-10-13)
|
|
2983
|
-
daemonArgs = { 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
|
+
};
|
|
2984
3240
|
break;
|
|
2985
3241
|
}
|
|
2986
3242
|
case "screenshot": {
|
|
@@ -2996,7 +3252,9 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
2996
3252
|
ssPath = args[0];
|
|
2997
3253
|
}
|
|
2998
3254
|
}
|
|
2999
|
-
|
|
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 };
|
|
3000
3258
|
break;
|
|
3001
3259
|
}
|
|
3002
3260
|
case "extract": {
|
|
@@ -3011,19 +3269,30 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
3011
3269
|
extractWhat = args[0];
|
|
3012
3270
|
}
|
|
3013
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
|
+
}
|
|
3014
3276
|
daemonArgs = { what: extractWhat, url: extractUrl || options.url };
|
|
3015
3277
|
break;
|
|
3016
3278
|
}
|
|
3017
3279
|
case "run":
|
|
3018
|
-
daemonArgs = { command: args.join(" ") };
|
|
3280
|
+
daemonArgs = { command: args.join(" "), ...urlArg };
|
|
3019
3281
|
break;
|
|
3020
3282
|
}
|
|
3021
3283
|
const result = await sendToDaemon(daemonCommand, daemonArgs);
|
|
3022
3284
|
if (result.success) {
|
|
3023
|
-
console.
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
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) {
|
|
3027
3296
|
console.log(JSON.stringify(result.result, null, 2));
|
|
3028
3297
|
}
|
|
3029
3298
|
}
|
|
@@ -3085,15 +3354,17 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
3085
3354
|
process.exit(1);
|
|
3086
3355
|
}
|
|
3087
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).
|
|
3088
3359
|
const browser = new CBrowser({
|
|
3089
|
-
browser: browserType,
|
|
3090
|
-
headless,
|
|
3360
|
+
...(options.browser !== undefined ? { browser: browserType } : {}),
|
|
3361
|
+
...(options.headless !== undefined ? { headless } : {}),
|
|
3091
3362
|
device: options.device,
|
|
3092
3363
|
...(viewportOverride ? { viewportWidth: viewportOverride.width, viewportHeight: viewportOverride.height, viewportOverride } : {}),
|
|
3093
3364
|
geolocation,
|
|
3094
3365
|
locale: options.locale,
|
|
3095
3366
|
timezone: options.timezone,
|
|
3096
|
-
recordVideo: options["record-video"] === true,
|
|
3367
|
+
...(options["record-video"] !== undefined ? { recordVideo: options["record-video"] === true } : {}),
|
|
3097
3368
|
persistent: persistentMode,
|
|
3098
3369
|
skipSessionRestore: skipRestore,
|
|
3099
3370
|
verbose: true,
|
|
@@ -3107,7 +3378,14 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
3107
3378
|
process.exit(1);
|
|
3108
3379
|
}
|
|
3109
3380
|
const result = await browser.navigate(url);
|
|
3110
|
-
|
|
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}`);
|
|
3111
3389
|
console.log(` Title: ${result.title}`);
|
|
3112
3390
|
console.log(` Load time: ${result.loadTime}ms`);
|
|
3113
3391
|
console.log(` Screenshot: ${result.screenshot}`);
|
|
@@ -3178,7 +3456,13 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
3178
3456
|
if (options.url)
|
|
3179
3457
|
await browser.navigate(options.url);
|
|
3180
3458
|
const page = await browser.getPage();
|
|
3181
|
-
|
|
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
|
+
}
|
|
3182
3466
|
const refusal = await browser.keystrokeRedZone({ text }, options.force === true);
|
|
3183
3467
|
if (refusal) {
|
|
3184
3468
|
console.error(`✗ ${refusal}`);
|
|
@@ -3298,8 +3582,15 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
3298
3582
|
if (url) {
|
|
3299
3583
|
await browser.navigate(url);
|
|
3300
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
|
+
}
|
|
3301
3592
|
const result = await browser.extract(what);
|
|
3302
|
-
|
|
3593
|
+
printData("extract", result.data);
|
|
3303
3594
|
break;
|
|
3304
3595
|
}
|
|
3305
3596
|
// =========================================================================
|
|
@@ -3337,7 +3628,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
3337
3628
|
else {
|
|
3338
3629
|
console.error(`\n✗ ${result.message}`);
|
|
3339
3630
|
if (result.aiSuggestion) {
|
|
3340
|
-
console.
|
|
3631
|
+
console.error(`\n💡 Suggestion:\n${result.aiSuggestion}`);
|
|
3341
3632
|
}
|
|
3342
3633
|
process.exit(1);
|
|
3343
3634
|
}
|
|
@@ -3504,6 +3795,11 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
3504
3795
|
console.log(`\n🧪 Analyzing page and generating tests...\n`);
|
|
3505
3796
|
const result = await browser.generateTests(url);
|
|
3506
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
|
+
}
|
|
3507
3803
|
const outputFile = options.output;
|
|
3508
3804
|
let output = "";
|
|
3509
3805
|
if (format === "summary" || format === "all") {
|
|
@@ -3555,6 +3851,17 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
3555
3851
|
}
|
|
3556
3852
|
await browser.navigate(url);
|
|
3557
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
|
+
}
|
|
3558
3865
|
console.log(`\n📊 Page Analysis: ${analysis.url}\n`);
|
|
3559
3866
|
console.log(`Title: ${analysis.title}`);
|
|
3560
3867
|
console.log(`\n📝 Forms (${analysis.forms.length}):`);
|
|
@@ -3606,20 +3913,28 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
3606
3913
|
console.warn("⚠️ 'journey' is deprecated. Use 'explore' instead.\n");
|
|
3607
3914
|
}
|
|
3608
3915
|
const persona = args[0] || "first-timer";
|
|
3609
|
-
|
|
3916
|
+
// --url works too (audit F25: four spellings of "start URL").
|
|
3917
|
+
const startUrl = (options.start ?? options.url);
|
|
3610
3918
|
const goal = options.goal || "Explore the site";
|
|
3611
3919
|
if (!startUrl) {
|
|
3612
3920
|
console.error("Error: --start URL required");
|
|
3613
3921
|
process.exit(1);
|
|
3614
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
|
+
}
|
|
3615
3929
|
console.log(`🔍 Starting exploration as "${persona}"...`);
|
|
3616
3930
|
console.log(` Goal: ${goal}`);
|
|
3617
|
-
console.log(` Mode: Heuristic (free, no API)`);
|
|
3618
3931
|
console.log("");
|
|
3619
3932
|
const result = await browser.journey({ persona, startUrl, goal });
|
|
3620
3933
|
console.log("");
|
|
3621
3934
|
console.log(`📊 Journey Results`);
|
|
3622
3935
|
console.log(` Success: ${result.success ? "✓" : "✗"}`);
|
|
3936
|
+
if (!result.success)
|
|
3937
|
+
process.exitCode = 1;
|
|
3623
3938
|
console.log(` Steps: ${result.steps.length}`);
|
|
3624
3939
|
console.log(` Time: ${result.totalTime}ms`);
|
|
3625
3940
|
console.log(` Friction points: ${result.frictionPoints.length}`);
|
|
@@ -3654,12 +3969,18 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
3654
3969
|
const concurrency = options.concurrency
|
|
3655
3970
|
? parseInt(options.concurrency)
|
|
3656
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
|
+
});
|
|
3657
3976
|
const comparison = await comparePersonas({
|
|
3658
3977
|
startUrl,
|
|
3659
3978
|
goal,
|
|
3660
3979
|
personas: personaNames,
|
|
3661
3980
|
maxConcurrency: concurrency,
|
|
3662
3981
|
headless,
|
|
3982
|
+
device: personaFlags.device,
|
|
3983
|
+
viewport: personaFlags.viewport,
|
|
3663
3984
|
});
|
|
3664
3985
|
// Print formatted report
|
|
3665
3986
|
const report = formatComparisonReport(comparison);
|
|
@@ -3674,7 +3995,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
3674
3995
|
if (options.html) {
|
|
3675
3996
|
const fs = await import("fs");
|
|
3676
3997
|
const htmlReport = generateHtmlReport(comparison);
|
|
3677
|
-
const htmlPath = options.output
|
|
3998
|
+
const htmlPath = htmlReportPath(options.output, "comparison-report.html");
|
|
3678
3999
|
fs.writeFileSync(htmlPath, htmlReport);
|
|
3679
4000
|
console.log(`\n🌐 HTML report saved: ${htmlPath}`);
|
|
3680
4001
|
}
|
|
@@ -3773,7 +4094,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
3773
4094
|
// Save the persona
|
|
3774
4095
|
const filepath = saveCustomPersona(persona);
|
|
3775
4096
|
console.log(`\n✓ Persona saved: ${filepath}`);
|
|
3776
|
-
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>"`);
|
|
3777
4098
|
break;
|
|
3778
4099
|
}
|
|
3779
4100
|
case "show": {
|
|
@@ -3916,6 +4237,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
3916
4237
|
console.error(" cbrowser persona delete my-custom-persona");
|
|
3917
4238
|
console.error(" cbrowser persona cleanup --dry-run # Preview invalid personas");
|
|
3918
4239
|
console.error(" cbrowser persona cleanup # Remove invalid personas");
|
|
4240
|
+
process.exit(1);
|
|
3919
4241
|
}
|
|
3920
4242
|
break;
|
|
3921
4243
|
}
|
|
@@ -4096,6 +4418,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4096
4418
|
}
|
|
4097
4419
|
else {
|
|
4098
4420
|
console.error(`✗ Session not found: ${name}`);
|
|
4421
|
+
process.exit(1);
|
|
4099
4422
|
}
|
|
4100
4423
|
break;
|
|
4101
4424
|
}
|
|
@@ -4109,6 +4432,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4109
4432
|
console.error(" cleanup --older-than <days> Delete old sessions");
|
|
4110
4433
|
console.error(" export <name> --output <file> Export session to file");
|
|
4111
4434
|
console.error(" import <file> --name <name> Import session from file");
|
|
4435
|
+
process.exit(1);
|
|
4112
4436
|
}
|
|
4113
4437
|
break;
|
|
4114
4438
|
}
|
|
@@ -4224,6 +4548,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4224
4548
|
}
|
|
4225
4549
|
default:
|
|
4226
4550
|
console.error("Usage: cbrowser cookie [list|set|delete|clear]");
|
|
4551
|
+
process.exit(1);
|
|
4227
4552
|
}
|
|
4228
4553
|
break;
|
|
4229
4554
|
}
|
|
@@ -4264,6 +4589,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4264
4589
|
}
|
|
4265
4590
|
default:
|
|
4266
4591
|
console.error("Usage: cbrowser device [list|set]");
|
|
4592
|
+
process.exit(1);
|
|
4267
4593
|
}
|
|
4268
4594
|
break;
|
|
4269
4595
|
}
|
|
@@ -4294,12 +4620,18 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4294
4620
|
console.error(`Invalid location: ${location}`);
|
|
4295
4621
|
process.exit(1);
|
|
4296
4622
|
}
|
|
4297
|
-
|
|
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);
|
|
4298
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)");
|
|
4299
4630
|
break;
|
|
4300
4631
|
}
|
|
4301
4632
|
default:
|
|
4302
4633
|
console.error("Usage: cbrowser geo [list|set]");
|
|
4634
|
+
process.exit(1);
|
|
4303
4635
|
}
|
|
4304
4636
|
break;
|
|
4305
4637
|
}
|
|
@@ -4328,6 +4660,8 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4328
4660
|
console.log("\n📊 Performance Audit:\n");
|
|
4329
4661
|
console.log(` URL: ${result.url}`);
|
|
4330
4662
|
console.log(` Result: ${result.passed ? "✓ PASSED" : "✗ FAILED"}`);
|
|
4663
|
+
if (!result.passed)
|
|
4664
|
+
process.exitCode = 1;
|
|
4331
4665
|
console.log("");
|
|
4332
4666
|
console.log(" Metrics:");
|
|
4333
4667
|
if (result.metrics.lcp)
|
|
@@ -4389,7 +4723,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4389
4723
|
const subcommand = args[0];
|
|
4390
4724
|
if (subcommand !== "start" && subcommand !== "stop") {
|
|
4391
4725
|
console.error("Usage: cbrowser har [start|stop]");
|
|
4392
|
-
|
|
4726
|
+
process.exit(1);
|
|
4393
4727
|
}
|
|
4394
4728
|
// A recording has to span several commands, and each cbrowser command is
|
|
4395
4729
|
// its own process: only the daemon's browser outlives them. Recorded here,
|
|
@@ -4474,7 +4808,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4474
4808
|
const subcommand = args[0];
|
|
4475
4809
|
if (subcommand !== "list" && subcommand !== "clear") {
|
|
4476
4810
|
console.error("Usage: cbrowser network [list|clear]");
|
|
4477
|
-
|
|
4811
|
+
process.exit(1);
|
|
4478
4812
|
}
|
|
4479
4813
|
// Same reason as har: the request log lives in a browser, and only the
|
|
4480
4814
|
// daemon's browser outlives a command. A fresh process always listed
|
|
@@ -4502,19 +4836,32 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4502
4836
|
}
|
|
4503
4837
|
switch (subcommand) {
|
|
4504
4838
|
case "list": {
|
|
4505
|
-
const
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
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");
|
|
4509
4855
|
}
|
|
4510
4856
|
else {
|
|
4511
|
-
console.log(`\n🌐 Network Requests (${
|
|
4512
|
-
for (const req of
|
|
4513
|
-
|
|
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 ? "..." : ""}`);
|
|
4514
4861
|
console.log(` Type: ${req.resourceType} | Time: ${req.timestamp}`);
|
|
4515
4862
|
}
|
|
4516
|
-
if (
|
|
4517
|
-
console.log(`\n ... and ${
|
|
4863
|
+
if (matching.length > shown.length) {
|
|
4864
|
+
console.log(`\n ... and ${matching.length - shown.length} older requests (--all lists them)`);
|
|
4518
4865
|
}
|
|
4519
4866
|
}
|
|
4520
4867
|
break;
|
|
@@ -4551,13 +4898,15 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4551
4898
|
if (options.url) {
|
|
4552
4899
|
await browser.navigate(options.url);
|
|
4553
4900
|
}
|
|
4554
|
-
const threshold =
|
|
4901
|
+
const threshold = numberOption("threshold", options.threshold, 0.1, { min: 0, max: 1 });
|
|
4555
4902
|
const result = await browser.compareBaseline(name, threshold);
|
|
4556
4903
|
console.log("\n🔍 Visual Comparison:\n");
|
|
4557
4904
|
console.log(` Baseline: ${name}`);
|
|
4558
4905
|
console.log(` Difference: ${(result.diffPercentage * 100).toFixed(2)}%`);
|
|
4559
4906
|
console.log(` Threshold: ${(threshold * 100).toFixed(0)}%`);
|
|
4560
4907
|
console.log(` Result: ${result.passed ? "✓ PASSED" : "✗ FAILED"}`);
|
|
4908
|
+
if (!result.passed)
|
|
4909
|
+
process.exitCode = 1;
|
|
4561
4910
|
if (result.diffPath) {
|
|
4562
4911
|
console.log(` Diff image: ${result.diffPath}`);
|
|
4563
4912
|
}
|
|
@@ -4601,6 +4950,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4601
4950
|
}
|
|
4602
4951
|
default:
|
|
4603
4952
|
console.error("Usage: cbrowser visual [save|compare|list|delete]");
|
|
4953
|
+
process.exit(1);
|
|
4604
4954
|
}
|
|
4605
4955
|
break;
|
|
4606
4956
|
}
|
|
@@ -4609,6 +4959,12 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4609
4959
|
// =========================================================================
|
|
4610
4960
|
case "ai-visual": {
|
|
4611
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
|
+
};
|
|
4612
4968
|
switch (subcommand) {
|
|
4613
4969
|
case "capture": {
|
|
4614
4970
|
const url = args[1];
|
|
@@ -4616,20 +4972,18 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4616
4972
|
console.error("Usage: cbrowser ai-visual capture <url> --name <name> [options]");
|
|
4617
4973
|
console.error(" --selector <sel> Capture specific element");
|
|
4618
4974
|
console.error(" --device <device> Device emulation");
|
|
4619
|
-
console.error(" --
|
|
4620
|
-
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");
|
|
4621
4978
|
console.error(" --wait <ms|sel> Wait before capture");
|
|
4622
4979
|
process.exit(1);
|
|
4623
4980
|
}
|
|
4981
|
+
const flags = browserFlagsOrExit("ai-visual capture", options);
|
|
4624
4982
|
console.log(`📸 Capturing visual baseline: ${options.name}`);
|
|
4625
4983
|
console.log(` URL: ${url}`);
|
|
4626
4984
|
const baseline = await captureVisualBaseline(url, options.name, {
|
|
4627
4985
|
selector: options.selector,
|
|
4628
|
-
|
|
4629
|
-
viewport: options.width || options.height ? {
|
|
4630
|
-
width: parseInt(options.width) || 1920,
|
|
4631
|
-
height: parseInt(options.height) || 1080,
|
|
4632
|
-
} : undefined,
|
|
4986
|
+
...flags,
|
|
4633
4987
|
waitFor: options.wait ? (/^\d+$/.test(options.wait)
|
|
4634
4988
|
? parseInt(options.wait)
|
|
4635
4989
|
: options.wait) : undefined,
|
|
@@ -4637,6 +4991,10 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4637
4991
|
console.log(`\n✅ Baseline captured successfully!`);
|
|
4638
4992
|
console.log(` ID: ${baseline.id}`);
|
|
4639
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}`);
|
|
4640
4998
|
console.log(` Screenshot: ${baseline.screenshotPath}`);
|
|
4641
4999
|
break;
|
|
4642
5000
|
}
|
|
@@ -4651,13 +5009,16 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4651
5009
|
console.error(" --output <file> Save JSON report");
|
|
4652
5010
|
process.exit(1);
|
|
4653
5011
|
}
|
|
5012
|
+
const flags = browserFlagsOrExit("ai-visual test", options);
|
|
5013
|
+
refuseBrowserFlags("ai-visual test", flags, BASELINE_SHAPE_FLAGS);
|
|
4654
5014
|
console.log(`\n🔍 Running AI visual regression test...`);
|
|
4655
5015
|
console.log(` URL: ${url}`);
|
|
4656
5016
|
console.log(` Baseline: ${baselineName}\n`);
|
|
4657
5017
|
const result = await runVisualRegression(url, baselineName, {
|
|
4658
|
-
threshold:
|
|
4659
|
-
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",
|
|
4660
5020
|
generateDiff: true,
|
|
5021
|
+
headless: flags.headless,
|
|
4661
5022
|
});
|
|
4662
5023
|
// Print report
|
|
4663
5024
|
console.log(formatVisualRegressionReport(result));
|
|
@@ -4709,10 +5070,13 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4709
5070
|
console.error(`Suite file not found: ${suiteFile}`);
|
|
4710
5071
|
process.exit(1);
|
|
4711
5072
|
}
|
|
5073
|
+
const flags = browserFlagsOrExit("ai-visual suite", options);
|
|
5074
|
+
refuseBrowserFlags("ai-visual suite", flags, BASELINE_SHAPE_FLAGS);
|
|
4712
5075
|
const suite = JSON.parse(fs.readFileSync(suiteFile, "utf-8"));
|
|
4713
5076
|
console.log(`\n🔍 Running visual regression suite: ${suite.name}`);
|
|
4714
5077
|
const result = await runVisualRegressionSuite(suite, {
|
|
4715
|
-
threshold:
|
|
5078
|
+
threshold: numberOption("threshold", options.threshold, 0.9, { min: 0, max: 1 }),
|
|
5079
|
+
headless: flags.headless,
|
|
4716
5080
|
});
|
|
4717
5081
|
// Save outputs
|
|
4718
5082
|
if (options.output && !options.html) {
|
|
@@ -4803,6 +5167,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4803
5167
|
console.error(" cbrowser ai-visual list");
|
|
4804
5168
|
console.error(" cbrowser ai-visual show <name>");
|
|
4805
5169
|
console.error(" cbrowser ai-visual delete <name>");
|
|
5170
|
+
process.exit(1);
|
|
4806
5171
|
}
|
|
4807
5172
|
break;
|
|
4808
5173
|
}
|
|
@@ -4811,6 +5176,10 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4811
5176
|
// =========================================================================
|
|
4812
5177
|
case "cross-browser": {
|
|
4813
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
|
+
});
|
|
4814
5183
|
if (subcommand === "suite") {
|
|
4815
5184
|
// Cross-browser suite
|
|
4816
5185
|
const suiteFile = args[1];
|
|
@@ -4826,7 +5195,8 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4826
5195
|
process.exit(1);
|
|
4827
5196
|
}
|
|
4828
5197
|
const suite = JSON.parse(fs.readFileSync(suiteFile, "utf-8"));
|
|
4829
|
-
|
|
5198
|
+
// Flags given on the command line win over the suite file's options.
|
|
5199
|
+
const result = await runCrossBrowserSuite({ ...suite, options: { ...suite.options, ...flags } });
|
|
4830
5200
|
// Save outputs
|
|
4831
5201
|
if (options.output && !options.html) {
|
|
4832
5202
|
fs.writeFileSync(options.output, JSON.stringify(result, null, 2));
|
|
@@ -4854,8 +5224,9 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4854
5224
|
console.error(" cbrowser cross-browser suite <file.json>");
|
|
4855
5225
|
console.error("\nOptions:");
|
|
4856
5226
|
console.error(" --browsers <list> chromium,firefox,webkit (default: all)");
|
|
4857
|
-
console.error(" --
|
|
4858
|
-
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)");
|
|
4859
5230
|
console.error(" --wait <ms> Wait before screenshot");
|
|
4860
5231
|
console.error(" --wait-for <sel> Wait for selector");
|
|
4861
5232
|
console.error(" --sensitivity <l> low, medium, high");
|
|
@@ -4868,13 +5239,10 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4868
5239
|
: undefined;
|
|
4869
5240
|
const result = await runCrossBrowserTest(url, {
|
|
4870
5241
|
browsers,
|
|
4871
|
-
|
|
4872
|
-
width: parseInt(options.width) || 1920,
|
|
4873
|
-
height: parseInt(options.height) || 1080,
|
|
4874
|
-
} : undefined,
|
|
5242
|
+
...flags,
|
|
4875
5243
|
waitBeforeCapture: options.wait ? parseInt(options.wait) : undefined,
|
|
4876
5244
|
waitForSelector: options["wait-for"],
|
|
4877
|
-
sensitivity: options.sensitivity,
|
|
5245
|
+
sensitivity: enumOption("sensitivity", options.sensitivity, ["low", "medium", "high"]),
|
|
4878
5246
|
});
|
|
4879
5247
|
// Print report
|
|
4880
5248
|
console.log("\n" + formatCrossBrowserReport(result));
|
|
@@ -4913,6 +5281,19 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4913
5281
|
// =========================================================================
|
|
4914
5282
|
case "responsive": {
|
|
4915
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
|
+
};
|
|
4916
5297
|
if (subcommand === "viewports") {
|
|
4917
5298
|
// List viewport presets
|
|
4918
5299
|
const presets = listViewportPresets();
|
|
@@ -4938,6 +5319,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4938
5319
|
}
|
|
4939
5320
|
else if (subcommand === "suite") {
|
|
4940
5321
|
// Responsive suite
|
|
5322
|
+
const flags = responsiveFlags();
|
|
4941
5323
|
const suiteFile = args[1];
|
|
4942
5324
|
if (!suiteFile) {
|
|
4943
5325
|
console.error("Usage: cbrowser responsive suite <file.json> [options]");
|
|
@@ -4951,7 +5333,8 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4951
5333
|
process.exit(1);
|
|
4952
5334
|
}
|
|
4953
5335
|
const suite = JSON.parse(fs.readFileSync(suiteFile, "utf-8"));
|
|
4954
|
-
|
|
5336
|
+
// Flags given on the command line win over the suite file's options.
|
|
5337
|
+
const result = await runResponsiveSuite({ ...suite, options: { ...suite.options, ...flags } });
|
|
4955
5338
|
// Save outputs
|
|
4956
5339
|
if (options.output && !options.html) {
|
|
4957
5340
|
fs.writeFileSync(options.output, JSON.stringify(result, null, 2));
|
|
@@ -4986,19 +5369,20 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
4986
5369
|
console.error(" --sensitivity <l> low, medium, high");
|
|
4987
5370
|
console.error(" --html Generate HTML report");
|
|
4988
5371
|
console.error(" --output <file> Save report");
|
|
4989
|
-
console.error("\nViewport presets:
|
|
4990
|
-
console.error("
|
|
4991
|
-
console.error(" desktop-lg, desktop-xl");
|
|
5372
|
+
console.error("\nViewport presets:");
|
|
5373
|
+
console.error(formatViewportPresetList(" "));
|
|
4992
5374
|
process.exit(1);
|
|
4993
5375
|
}
|
|
5376
|
+
const flags = responsiveFlags();
|
|
4994
5377
|
const viewports = options.viewports
|
|
4995
5378
|
? options.viewports.split(",")
|
|
4996
5379
|
: undefined;
|
|
4997
5380
|
const result = await runResponsiveTest(url, {
|
|
4998
5381
|
viewports,
|
|
5382
|
+
...flags,
|
|
4999
5383
|
waitBeforeCapture: options.wait ? parseInt(options.wait) : undefined,
|
|
5000
5384
|
waitForSelector: options["wait-for"],
|
|
5001
|
-
sensitivity: options.sensitivity,
|
|
5385
|
+
sensitivity: enumOption("sensitivity", options.sensitivity, ["low", "medium", "high"]),
|
|
5002
5386
|
});
|
|
5003
5387
|
// Print report
|
|
5004
5388
|
console.log("\n" + formatResponsiveReport(result));
|
|
@@ -5039,6 +5423,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5039
5423
|
// =========================================================================
|
|
5040
5424
|
case "ab": {
|
|
5041
5425
|
const subcommand = args[0];
|
|
5426
|
+
const flags = browserFlagsOrExit("ab", options);
|
|
5042
5427
|
if (subcommand === "suite") {
|
|
5043
5428
|
// A/B suite
|
|
5044
5429
|
const suiteFile = args[1];
|
|
@@ -5054,7 +5439,8 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5054
5439
|
process.exit(1);
|
|
5055
5440
|
}
|
|
5056
5441
|
const suite = JSON.parse(fs.readFileSync(suiteFile, "utf-8"));
|
|
5057
|
-
|
|
5442
|
+
// Flags given on the command line win over the suite file's options.
|
|
5443
|
+
const result = await runABSuite({ ...suite, options: { ...suite.options, ...flags } });
|
|
5058
5444
|
// Save outputs
|
|
5059
5445
|
if (options.output && !options.html) {
|
|
5060
5446
|
fs.writeFileSync(options.output, JSON.stringify(result, null, 2));
|
|
@@ -5084,8 +5470,9 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5084
5470
|
console.error("\nOptions:");
|
|
5085
5471
|
console.error(" --label-a <name> Label for URL A (default: 'Version A')");
|
|
5086
5472
|
console.error(" --label-b <name> Label for URL B (default: 'Version B')");
|
|
5087
|
-
console.error(" --
|
|
5088
|
-
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");
|
|
5089
5476
|
console.error(" --wait <ms> Wait before screenshot");
|
|
5090
5477
|
console.error(" --wait-for <sel> Wait for selector");
|
|
5091
5478
|
console.error(" --sensitivity <l> low, medium, high");
|
|
@@ -5098,13 +5485,10 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5098
5485
|
a: options["label-a"] || "Version A",
|
|
5099
5486
|
b: options["label-b"] || "Version B",
|
|
5100
5487
|
} : undefined,
|
|
5101
|
-
|
|
5102
|
-
width: parseInt(options.width) || 1920,
|
|
5103
|
-
height: parseInt(options.height) || 1080,
|
|
5104
|
-
} : undefined,
|
|
5488
|
+
...flags,
|
|
5105
5489
|
waitBeforeCapture: options.wait ? parseInt(options.wait) : undefined,
|
|
5106
5490
|
waitForSelector: options["wait-for"],
|
|
5107
|
-
sensitivity: options.sensitivity,
|
|
5491
|
+
sensitivity: enumOption("sensitivity", options.sensitivity, ["low", "medium", "high"]),
|
|
5108
5492
|
});
|
|
5109
5493
|
// Print report
|
|
5110
5494
|
console.log("\n" + formatABReport(result));
|
|
@@ -5170,6 +5554,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5170
5554
|
}
|
|
5171
5555
|
else {
|
|
5172
5556
|
console.error("Usage: cbrowser a11y audit [url]");
|
|
5557
|
+
process.exit(1);
|
|
5173
5558
|
}
|
|
5174
5559
|
break;
|
|
5175
5560
|
}
|
|
@@ -5178,6 +5563,17 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5178
5563
|
// =========================================================================
|
|
5179
5564
|
case "record": {
|
|
5180
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
|
+
}
|
|
5181
5577
|
switch (subcommand) {
|
|
5182
5578
|
case "start": {
|
|
5183
5579
|
const url = options.url;
|
|
@@ -5254,6 +5650,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5254
5650
|
}
|
|
5255
5651
|
default:
|
|
5256
5652
|
console.error("Usage: cbrowser record [start|stop|save|list|generate]");
|
|
5653
|
+
process.exit(1);
|
|
5257
5654
|
}
|
|
5258
5655
|
break;
|
|
5259
5656
|
}
|
|
@@ -5291,31 +5688,25 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5291
5688
|
console.error("Usage: cbrowser export [junit|tap] <name> [output]");
|
|
5292
5689
|
process.exit(1);
|
|
5293
5690
|
}
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
const resultsPath = path.join(browser.getDataDir(), "results", `${name}.json`);
|
|
5298
|
-
let suite;
|
|
5299
|
-
if (fs.existsSync(resultsPath)) {
|
|
5300
|
-
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);
|
|
5301
5694
|
}
|
|
5302
|
-
|
|
5695
|
+
// The last `test-suite` run of that suite (it saves every run).
|
|
5696
|
+
const suite = loadSuiteForExport(browser.getDataDir(), name);
|
|
5697
|
+
if (!suite) {
|
|
5303
5698
|
console.error(`Test results not found: ${name}`);
|
|
5304
|
-
console.error(
|
|
5699
|
+
console.error(` Run 'cbrowser test-suite <file>' first; it saves results as ${resultsName(name)}.json under ${join(browser.getDataDir(), "results")}`);
|
|
5305
5700
|
process.exit(1);
|
|
5306
5701
|
}
|
|
5307
5702
|
if (format === "junit") {
|
|
5308
5703
|
const exportPath = browser.exportJUnit(suite, output);
|
|
5309
5704
|
console.log(`✓ JUnit XML exported: ${exportPath}`);
|
|
5310
5705
|
}
|
|
5311
|
-
else
|
|
5706
|
+
else {
|
|
5312
5707
|
const exportPath = browser.exportTAP(suite, output);
|
|
5313
5708
|
console.log(`✓ TAP exported: ${exportPath}`);
|
|
5314
5709
|
}
|
|
5315
|
-
else {
|
|
5316
|
-
console.error("Unknown export format. Use 'junit' or 'tap'");
|
|
5317
|
-
process.exit(1);
|
|
5318
|
-
}
|
|
5319
5710
|
break;
|
|
5320
5711
|
}
|
|
5321
5712
|
// =========================================================================
|
|
@@ -5324,13 +5715,9 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5324
5715
|
case "webhook": {
|
|
5325
5716
|
const subcommand = args[0];
|
|
5326
5717
|
const fs = await import("fs");
|
|
5327
|
-
const
|
|
5328
|
-
const webhooksPath = path.join(browser.getDataDir(), "webhooks.json");
|
|
5718
|
+
const hooksFile = webhooksPath(browser.getDataDir());
|
|
5329
5719
|
// Load existing webhooks
|
|
5330
|
-
let webhooks =
|
|
5331
|
-
if (fs.existsSync(webhooksPath)) {
|
|
5332
|
-
webhooks = JSON.parse(fs.readFileSync(webhooksPath, "utf-8"));
|
|
5333
|
-
}
|
|
5720
|
+
let webhooks = loadWebhooks(browser.getDataDir());
|
|
5334
5721
|
switch (subcommand) {
|
|
5335
5722
|
case "add": {
|
|
5336
5723
|
const name = args[1];
|
|
@@ -5339,14 +5726,30 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5339
5726
|
console.error("Usage: cbrowser webhook add <name> <url> [--events <events>] [--format <format>]");
|
|
5340
5727
|
process.exit(1);
|
|
5341
5728
|
}
|
|
5342
|
-
const
|
|
5343
|
-
|
|
5344
|
-
:
|
|
5345
|
-
|
|
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
|
+
}
|
|
5346
5749
|
// Remove existing webhook with same name
|
|
5347
5750
|
webhooks = webhooks.filter(w => w.name !== name);
|
|
5348
5751
|
webhooks.push({ name, url, events, format });
|
|
5349
|
-
fs.writeFileSync(
|
|
5752
|
+
fs.writeFileSync(hooksFile, JSON.stringify(webhooks, null, 2));
|
|
5350
5753
|
console.log(`✓ Webhook added: ${name}`);
|
|
5351
5754
|
console.log(` URL: ${url}`);
|
|
5352
5755
|
console.log(` Events: ${events.join(", ")}`);
|
|
@@ -5360,9 +5763,11 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5360
5763
|
else {
|
|
5361
5764
|
console.log("\n🔔 Configured Webhooks:\n");
|
|
5362
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));
|
|
5363
5768
|
console.log(` ${w.name}`);
|
|
5364
5769
|
console.log(` URL: ${w.url}`);
|
|
5365
|
-
console.log(` Events: ${w.events.join(", ")}`);
|
|
5770
|
+
console.log(` Events: ${w.events.join(", ")}${unsent.length > 0 ? ` (never sent: ${unsent.join(", ")})` : ""}`);
|
|
5366
5771
|
console.log(` Format: ${w.format}`);
|
|
5367
5772
|
console.log("");
|
|
5368
5773
|
}
|
|
@@ -5378,7 +5783,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5378
5783
|
const originalLength = webhooks.length;
|
|
5379
5784
|
webhooks = webhooks.filter(w => w.name !== name);
|
|
5380
5785
|
if (webhooks.length < originalLength) {
|
|
5381
|
-
fs.writeFileSync(
|
|
5786
|
+
fs.writeFileSync(hooksFile, JSON.stringify(webhooks, null, 2));
|
|
5382
5787
|
console.log(`✓ Webhook deleted: ${name}`);
|
|
5383
5788
|
}
|
|
5384
5789
|
else {
|
|
@@ -5398,6 +5803,11 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5398
5803
|
console.error(`✗ Webhook not found: ${name}`);
|
|
5399
5804
|
process.exit(1);
|
|
5400
5805
|
}
|
|
5806
|
+
const urlProblem = webhookUrlProblem(webhook.url);
|
|
5807
|
+
if (urlProblem) {
|
|
5808
|
+
console.error(`✗ ${urlProblem}`);
|
|
5809
|
+
process.exit(1);
|
|
5810
|
+
}
|
|
5401
5811
|
// Send test notification
|
|
5402
5812
|
const testPayload = webhook.format === "slack"
|
|
5403
5813
|
? { text: "🔔 CBrowser test notification" }
|
|
@@ -5409,6 +5819,8 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5409
5819
|
method: "POST",
|
|
5410
5820
|
headers: { "Content-Type": "application/json" },
|
|
5411
5821
|
body: JSON.stringify(testPayload),
|
|
5822
|
+
redirect: "error",
|
|
5823
|
+
signal: AbortSignal.timeout(WEBHOOK_TIMEOUT_MS),
|
|
5412
5824
|
});
|
|
5413
5825
|
if (response.ok) {
|
|
5414
5826
|
console.log(`✓ Test notification sent to: ${name}`);
|
|
@@ -5426,6 +5838,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5426
5838
|
}
|
|
5427
5839
|
default:
|
|
5428
5840
|
console.error("Usage: cbrowser webhook [add|list|delete|test]");
|
|
5841
|
+
process.exit(1);
|
|
5429
5842
|
}
|
|
5430
5843
|
break;
|
|
5431
5844
|
}
|
|
@@ -5434,6 +5847,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5434
5847
|
// =========================================================================
|
|
5435
5848
|
case "parallel": {
|
|
5436
5849
|
const subcommand = args[0];
|
|
5850
|
+
const flags = browserFlagsOrExit("parallel", options);
|
|
5437
5851
|
switch (subcommand) {
|
|
5438
5852
|
case "devices": {
|
|
5439
5853
|
const url = args[1];
|
|
@@ -5441,6 +5855,10 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5441
5855
|
console.error("Usage: cbrowser parallel devices <url> [--devices <list>] [--concurrency <n>]");
|
|
5442
5856
|
process.exit(1);
|
|
5443
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
|
+
});
|
|
5444
5862
|
// Without --devices, cbrowser's own presets, not all few hundred.
|
|
5445
5863
|
const deviceList = options.devices
|
|
5446
5864
|
? options.devices.split(",").map((d) => resolveDevicePreset(d))
|
|
@@ -5454,11 +5872,12 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5454
5872
|
const nav = await b.navigate(url);
|
|
5455
5873
|
const screenshot = await b.screenshot();
|
|
5456
5874
|
return { title: nav.title, loadTime: nav.loadTime, screenshot };
|
|
5457
|
-
}, { maxConcurrency: concurrency });
|
|
5875
|
+
}, { maxConcurrency: concurrency, config: browserLaunchConfig(flags) });
|
|
5458
5876
|
console.log("📊 Results:\n");
|
|
5459
5877
|
for (const r of results) {
|
|
5460
5878
|
if (r.error) {
|
|
5461
5879
|
console.log(` ✗ ${r.device}: ${r.error} (${r.duration}ms)`);
|
|
5880
|
+
process.exitCode = 1;
|
|
5462
5881
|
}
|
|
5463
5882
|
else {
|
|
5464
5883
|
console.log(` ✓ ${r.device}: ${r.result?.title} - ${r.result?.loadTime}ms (${r.duration}ms total)`);
|
|
@@ -5481,11 +5900,12 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5481
5900
|
const results = await CBrowser.parallelUrls(urls, async (b, url) => {
|
|
5482
5901
|
const nav = await b.navigate(url);
|
|
5483
5902
|
return { title: nav.title, loadTime: nav.loadTime };
|
|
5484
|
-
}, { maxConcurrency: concurrency });
|
|
5903
|
+
}, { maxConcurrency: concurrency, config: browserLaunchConfig(flags) });
|
|
5485
5904
|
console.log("📊 Results:\n");
|
|
5486
5905
|
for (const r of results) {
|
|
5487
5906
|
if (r.error) {
|
|
5488
5907
|
console.log(` ✗ ${r.url}: ${r.error}`);
|
|
5908
|
+
process.exitCode = 1;
|
|
5489
5909
|
}
|
|
5490
5910
|
else {
|
|
5491
5911
|
console.log(` ✓ ${r.url}: ${r.result?.title} (${r.result?.loadTime}ms)`);
|
|
@@ -5506,11 +5926,12 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5506
5926
|
const results = await CBrowser.parallelUrls(urls, async (b, url) => {
|
|
5507
5927
|
await b.navigate(url);
|
|
5508
5928
|
return await b.getPerformanceMetrics();
|
|
5509
|
-
}, { maxConcurrency: concurrency });
|
|
5929
|
+
}, { maxConcurrency: concurrency, config: browserLaunchConfig(flags) });
|
|
5510
5930
|
console.log("📊 Performance Results:\n");
|
|
5511
5931
|
for (const r of results) {
|
|
5512
5932
|
if (r.error) {
|
|
5513
5933
|
console.log(` ✗ ${r.url}: ${r.error}`);
|
|
5934
|
+
process.exitCode = 1;
|
|
5514
5935
|
}
|
|
5515
5936
|
else {
|
|
5516
5937
|
const m = r.result;
|
|
@@ -5527,6 +5948,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5527
5948
|
}
|
|
5528
5949
|
default:
|
|
5529
5950
|
console.error("Usage: cbrowser parallel [devices|urls|perf]");
|
|
5951
|
+
process.exit(1);
|
|
5530
5952
|
}
|
|
5531
5953
|
break;
|
|
5532
5954
|
}
|
|
@@ -5544,6 +5966,8 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5544
5966
|
process.exit(1);
|
|
5545
5967
|
}
|
|
5546
5968
|
console.log(`\n🗣️ Executing: "${nlCommand}"\n`);
|
|
5969
|
+
if (typeof options.url === "string")
|
|
5970
|
+
await browser.navigate(options.url);
|
|
5547
5971
|
const result = await executeNaturalLanguage(browser, nlCommand);
|
|
5548
5972
|
if (result.success) {
|
|
5549
5973
|
console.log(`✓ Action: ${result.action}`);
|
|
@@ -5557,6 +5981,8 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5557
5981
|
console.log(` ${r.message}`);
|
|
5558
5982
|
if (r.screenshot)
|
|
5559
5983
|
console.log(` Screenshot: ${r.screenshot}`);
|
|
5984
|
+
if (typeof r.scrollY === "number")
|
|
5985
|
+
console.log(` scrollY: ${r.scrollY}`);
|
|
5560
5986
|
}
|
|
5561
5987
|
}
|
|
5562
5988
|
else {
|
|
@@ -5587,7 +6013,8 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5587
6013
|
}
|
|
5588
6014
|
else {
|
|
5589
6015
|
console.log(`✗ ${r.command}`);
|
|
5590
|
-
|
|
6016
|
+
process.exitCode = 1;
|
|
6017
|
+
console.error(` Error: ${r.error}`);
|
|
5591
6018
|
}
|
|
5592
6019
|
}
|
|
5593
6020
|
const passed = results.filter(r => r.success).length;
|
|
@@ -5624,7 +6051,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5624
6051
|
console.log(` Description: ${result.description}`);
|
|
5625
6052
|
}
|
|
5626
6053
|
else {
|
|
5627
|
-
console.
|
|
6054
|
+
console.error(`✗ Could not find element matching: "${intent}"`);
|
|
5628
6055
|
process.exit(1);
|
|
5629
6056
|
}
|
|
5630
6057
|
break;
|
|
@@ -5634,17 +6061,24 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5634
6061
|
const result = await findElementByIntent(browser, intent);
|
|
5635
6062
|
if (result) {
|
|
5636
6063
|
console.log(`✓ Found: ${result.description}`);
|
|
5637
|
-
|
|
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
|
+
}
|
|
5638
6071
|
console.log(`✓ Clicked element`);
|
|
5639
6072
|
}
|
|
5640
6073
|
else {
|
|
5641
|
-
console.
|
|
6074
|
+
console.error(`✗ Could not find element matching: "${intent}"`);
|
|
5642
6075
|
process.exit(1);
|
|
5643
6076
|
}
|
|
5644
6077
|
break;
|
|
5645
6078
|
}
|
|
5646
6079
|
default:
|
|
5647
6080
|
console.error("Usage: cbrowser ai [find|click] \"<intent>\"");
|
|
6081
|
+
process.exit(1);
|
|
5648
6082
|
}
|
|
5649
6083
|
break;
|
|
5650
6084
|
}
|
|
@@ -5659,7 +6093,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5659
6093
|
process.exit(1);
|
|
5660
6094
|
}
|
|
5661
6095
|
const maxPages = options["max-pages"] ? parseInt(options["max-pages"]) : 10;
|
|
5662
|
-
const timeout =
|
|
6096
|
+
const timeout = timeoutOption(options.timeout, 60000);
|
|
5663
6097
|
console.log(`\n🔍 Bug Hunter starting...`);
|
|
5664
6098
|
console.log(` URL: ${url}`);
|
|
5665
6099
|
console.log(` Max pages: ${maxPages}`);
|
|
@@ -5694,16 +6128,24 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5694
6128
|
case "diff": {
|
|
5695
6129
|
const url = args[0];
|
|
5696
6130
|
if (!url) {
|
|
5697
|
-
console.error("Usage: cbrowser diff <url> [--browsers <list>]");
|
|
6131
|
+
console.error("Usage: cbrowser diff <url> [--browsers <list>] [--viewport <WxH>] [--device <name>]");
|
|
5698
6132
|
process.exit(1);
|
|
5699
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
|
+
});
|
|
5700
6138
|
const browserList = options.browsers
|
|
5701
6139
|
? options.browsers.split(",")
|
|
5702
6140
|
: ["chromium", "firefox", "webkit"];
|
|
5703
6141
|
console.log(`\n🔀 Cross-Browser Diff`);
|
|
5704
6142
|
console.log(` URL: ${url}`);
|
|
5705
6143
|
console.log(` Browsers: ${browserList.join(", ")}\n`);
|
|
5706
|
-
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
|
+
});
|
|
5707
6149
|
console.log(`📊 Results:\n`);
|
|
5708
6150
|
console.log(` Metrics:`);
|
|
5709
6151
|
for (const [browser, metrics] of Object.entries(result.metrics)) {
|
|
@@ -5755,6 +6197,8 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5755
6197
|
const result = await runChaosTest(browser, url, chaosConfig);
|
|
5756
6198
|
console.log(`📊 Results:\n`);
|
|
5757
6199
|
console.log(` Passed: ${result.passed ? "✅ Yes" : "❌ No"}`);
|
|
6200
|
+
if (!result.passed)
|
|
6201
|
+
process.exitCode = 1;
|
|
5758
6202
|
console.log(` Duration: ${result.duration}ms`);
|
|
5759
6203
|
if (result.errors.length > 0) {
|
|
5760
6204
|
console.log(`\n Errors:`);
|
|
@@ -5801,10 +6245,9 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5801
6245
|
console.log("");
|
|
5802
6246
|
}
|
|
5803
6247
|
console.log("Usage (OPT-IN REQUIRED):");
|
|
5804
|
-
console.log(" cbrowser screenshot https://example.com --lightpanda");
|
|
5805
6248
|
console.log(" cbrowser agent-ready-audit https://example.com --lightpanda");
|
|
5806
6249
|
console.log("");
|
|
5807
|
-
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.");
|
|
5808
6251
|
}
|
|
5809
6252
|
else {
|
|
5810
6253
|
console.log("⚠️ Configured but unavailable");
|
|
@@ -5975,6 +6418,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5975
6418
|
}
|
|
5976
6419
|
catch (e) {
|
|
5977
6420
|
console.log(` ✗ Failed: ${e.message} (${Date.now() - start}ms)`);
|
|
6421
|
+
process.exitCode = 1;
|
|
5978
6422
|
}
|
|
5979
6423
|
}
|
|
5980
6424
|
}
|
|
@@ -5987,12 +6431,16 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
5987
6431
|
}
|
|
5988
6432
|
assertCaptureNamespaceFlags("test-suite", options);
|
|
5989
6433
|
const suiteOptions = {
|
|
5990
|
-
stepTimeout:
|
|
6434
|
+
stepTimeout: timeoutOption(options.timeout, 30000),
|
|
5991
6435
|
continueOnFailure: options["continue-on-failure"] === true,
|
|
5992
6436
|
screenshotOnFailure: options["screenshot-on-failure"] !== false,
|
|
5993
6437
|
headless,
|
|
5994
6438
|
device: typeof options.device === "string" ? options.device : undefined,
|
|
5995
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)
|
|
@@ -6569,6 +7103,12 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6569
7103
|
headless,
|
|
6570
7104
|
device: typeof options.device === "string" ? options.device : undefined,
|
|
6571
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,
|
|
6572
7112
|
vision,
|
|
6573
7113
|
customTraits, // Pass custom traits from env var
|
|
6574
7114
|
location: locationOverride, // Pass location override
|
|
@@ -6579,6 +7119,10 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6579
7119
|
});
|
|
6580
7120
|
console.log("\n");
|
|
6581
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
|
+
}
|
|
6582
7126
|
if (result.goalAchieved) {
|
|
6583
7127
|
console.log(`✅ GOAL ACHIEVED in ${result.stepCount} steps (${result.totalTime.toFixed(1)}s)`);
|
|
6584
7128
|
}
|
|
@@ -6620,7 +7164,20 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6620
7164
|
// goalAchieved genuinely means the persona reached the goal — unlike
|
|
6621
7165
|
// the empathy audit's field of the same old name, which measured
|
|
6622
7166
|
// barriers and was renamed to noBlockingBarriers.
|
|
6623
|
-
|
|
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);
|
|
6624
7181
|
}
|
|
6625
7182
|
catch (error) {
|
|
6626
7183
|
console.error(`\n❌ Error: ${error.message}`);
|
|
@@ -6654,7 +7211,11 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6654
7211
|
auditProxy = { server: geoProxy.server, username: geoProxy.username, password: geoProxy.password };
|
|
6655
7212
|
}
|
|
6656
7213
|
else {
|
|
6657
|
-
|
|
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);
|
|
6658
7219
|
}
|
|
6659
7220
|
}
|
|
6660
7221
|
console.log(`\n🤖 Running Agent-Ready Audit on ${url}...${useLightpanda ? " (with Lightpanda)" : ""}${geoRegion ? ` (via ${geoRegion})` : ""}\n`);
|
|
@@ -6662,6 +7223,10 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6662
7223
|
headless,
|
|
6663
7224
|
useLightpanda,
|
|
6664
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 } : {}),
|
|
6665
7230
|
});
|
|
6666
7231
|
// Print formatted report
|
|
6667
7232
|
const report = formatAgentReadyReport(result);
|
|
@@ -6676,7 +7241,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6676
7241
|
if (options.html) {
|
|
6677
7242
|
const fs = await import("fs");
|
|
6678
7243
|
const htmlReport = generateAgentReadyHtmlReport(result);
|
|
6679
|
-
const htmlPath = options.output
|
|
7244
|
+
const htmlPath = htmlReportPath(options.output, "agent-ready-report.html");
|
|
6680
7245
|
fs.writeFileSync(htmlPath, htmlReport);
|
|
6681
7246
|
console.log(`🌐 HTML report saved: ${htmlPath}`);
|
|
6682
7247
|
}
|
|
@@ -6699,8 +7264,16 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6699
7264
|
process.exit(1);
|
|
6700
7265
|
}
|
|
6701
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
|
+
});
|
|
6702
7274
|
const result = await runAIReadinessBenchmark({
|
|
6703
7275
|
urls,
|
|
7276
|
+
...(benchFlags.device ? { device: benchFlags.device } : {}),
|
|
6704
7277
|
headless: options.headless !== false,
|
|
6705
7278
|
maxConcurrency: options.concurrency ? parseInt(options.concurrency) : 3,
|
|
6706
7279
|
});
|
|
@@ -6713,6 +7286,9 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6713
7286
|
for (const site of result.sites) {
|
|
6714
7287
|
const rank = result.ranking.find(r => r.site === site.siteName)?.rank || "-";
|
|
6715
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;
|
|
6716
7292
|
const grade = site.grade || "-";
|
|
6717
7293
|
const find = site.scoreBreakdown?.findability?.toString().padStart(3) || "-";
|
|
6718
7294
|
const stab = site.scoreBreakdown?.stability?.toString().padStart(3) || "-";
|
|
@@ -6755,7 +7331,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6755
7331
|
// Generate HTML report if requested
|
|
6756
7332
|
if (options.html) {
|
|
6757
7333
|
const fs = await import("fs");
|
|
6758
|
-
const htmlPath = options.output
|
|
7334
|
+
const htmlPath = htmlReportPath(options.output, "ai-benchmark-report.html");
|
|
6759
7335
|
// Simple HTML table report
|
|
6760
7336
|
const html = `<!DOCTYPE html>
|
|
6761
7337
|
<html lang="en">
|
|
@@ -6841,7 +7417,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6841
7417
|
const result = await runWebMCPReadyAudit(url, {
|
|
6842
7418
|
apiKey: options["api-key"],
|
|
6843
7419
|
oauthToken: options["oauth-token"],
|
|
6844
|
-
timeout:
|
|
7420
|
+
timeout: timeoutOption(options.timeout, 30000),
|
|
6845
7421
|
});
|
|
6846
7422
|
// Print formatted summary
|
|
6847
7423
|
console.log(`╔══════════════════════════════════════════════════════════════╗`);
|
|
@@ -6881,7 +7457,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6881
7457
|
// Generate HTML report if requested
|
|
6882
7458
|
if (options.html) {
|
|
6883
7459
|
const fs = await import("fs");
|
|
6884
|
-
const htmlPath = options.output
|
|
7460
|
+
const htmlPath = htmlReportPath(options.output, "webmcp-ready-report.html");
|
|
6885
7461
|
const htmlReport = generateWebMCPReadyHtmlReport(result);
|
|
6886
7462
|
fs.writeFileSync(htmlPath, htmlReport);
|
|
6887
7463
|
console.log(`🌐 HTML report saved: ${htmlPath}`);
|
|
@@ -6905,6 +7481,11 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6905
7481
|
console.error("Usage: cbrowser competitive-benchmark --sites <urls> --goal <goal> [--persona <name>] [--html]");
|
|
6906
7482
|
process.exit(1);
|
|
6907
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
|
+
}
|
|
6908
7489
|
const sites = sitesArg.split(",").map((s) => ({ url: s.trim() }));
|
|
6909
7490
|
const personaName = options.persona || "first-timer";
|
|
6910
7491
|
const maxSteps = options["max-steps"] ? parseInt(options["max-steps"]) : 30;
|
|
@@ -6913,6 +7494,11 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6913
7494
|
console.log(` Sites: ${sites.map((s) => s.url).join(", ")}`);
|
|
6914
7495
|
console.log(` Goal: ${goal}`);
|
|
6915
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
|
+
});
|
|
6916
7502
|
const result = await runCompetitiveBenchmark({
|
|
6917
7503
|
sites,
|
|
6918
7504
|
goal,
|
|
@@ -6920,6 +7506,8 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6920
7506
|
maxSteps,
|
|
6921
7507
|
maxTime,
|
|
6922
7508
|
headless,
|
|
7509
|
+
...(journeyFlags.device ? { device: journeyFlags.device } : {}),
|
|
7510
|
+
...(journeyFlags.viewport ? { viewport: journeyFlags.viewport } : {}),
|
|
6923
7511
|
});
|
|
6924
7512
|
// Print formatted report
|
|
6925
7513
|
const report = formatCompetitiveBenchmarkReport(result);
|
|
@@ -6934,7 +7522,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6934
7522
|
if (options.html) {
|
|
6935
7523
|
const fs = await import("fs");
|
|
6936
7524
|
const htmlReport = generateCompetitiveBenchmarkHtmlReport(result);
|
|
6937
|
-
const htmlPath = options.output
|
|
7525
|
+
const htmlPath = htmlReportPath(options.output, "competitive-benchmark-report.html");
|
|
6938
7526
|
fs.writeFileSync(htmlPath, htmlReport);
|
|
6939
7527
|
console.log(`🌐 HTML report saved: ${htmlPath}`);
|
|
6940
7528
|
}
|
|
@@ -6943,41 +7531,22 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6943
7531
|
case "empathy-audit": {
|
|
6944
7532
|
const url = args[0];
|
|
6945
7533
|
const goal = options.goal;
|
|
6946
|
-
|
|
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);
|
|
6947
7538
|
if (!url) {
|
|
6948
7539
|
console.error("Error: URL required");
|
|
6949
|
-
console.error("Usage: cbrowser empathy-audit <url> --goal <goal> [--
|
|
6950
|
-
process.exit(1);
|
|
6951
|
-
}
|
|
6952
|
-
if (!goal) {
|
|
6953
|
-
console.error("Error: --goal required");
|
|
6954
|
-
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]");
|
|
6955
7541
|
process.exit(1);
|
|
6956
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).
|
|
6957
7545
|
// Parse disabilities or use all
|
|
6958
7546
|
const allAccessibilityNames = listAccessibilityPersonas();
|
|
6959
|
-
//
|
|
6960
|
-
|
|
6961
|
-
|
|
6962
|
-
"motor": "motor-impairment-tremor",
|
|
6963
|
-
"tremor": "motor-impairment-tremor",
|
|
6964
|
-
"low-vision": "low-vision-magnified",
|
|
6965
|
-
"vision": "low-vision-magnified",
|
|
6966
|
-
"magnified": "low-vision-magnified",
|
|
6967
|
-
"adhd": "cognitive-adhd",
|
|
6968
|
-
"cognitive": "cognitive-adhd",
|
|
6969
|
-
"attention": "cognitive-adhd",
|
|
6970
|
-
"dyslexia": "dyslexic-user",
|
|
6971
|
-
"dyslexic": "dyslexic-user",
|
|
6972
|
-
"reading": "dyslexic-user",
|
|
6973
|
-
"deaf": "deaf-user",
|
|
6974
|
-
"hearing": "deaf-user",
|
|
6975
|
-
"elderly": "elderly-low-vision",
|
|
6976
|
-
"senior": "elderly-low-vision",
|
|
6977
|
-
"color-blind": "color-blind-deuteranopia",
|
|
6978
|
-
"colorblind": "color-blind-deuteranopia",
|
|
6979
|
-
"deuteranopia": "color-blind-deuteranopia",
|
|
6980
|
-
};
|
|
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;
|
|
6981
7550
|
const disabilityNames = disabilitiesArg
|
|
6982
7551
|
? disabilitiesArg.split(",").map((d) => {
|
|
6983
7552
|
const trimmed = d.trim().toLowerCase();
|
|
@@ -6989,7 +7558,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
6989
7558
|
if (invalidNames.length > 0) {
|
|
6990
7559
|
console.error(`Error: Unknown disability persona(s): ${invalidNames.join(", ")}`);
|
|
6991
7560
|
console.error(`Available: ${allAccessibilityNames.join(", ")}`);
|
|
6992
|
-
console.error(`Aliases:
|
|
7561
|
+
console.error(`Aliases: ${Object.keys(aliasMap).filter((a) => allAccessibilityNames.includes(aliasMap[a]) && a !== aliasMap[a]).join(", ")}`);
|
|
6993
7562
|
process.exit(1);
|
|
6994
7563
|
}
|
|
6995
7564
|
const wcagLevel = options["wcag-level"] || "AA";
|
|
@@ -7000,13 +7569,22 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
7000
7569
|
console.log(` Goal: ${goal}`);
|
|
7001
7570
|
console.log(` Disabilities: ${disabilityNames.join(", ")}`);
|
|
7002
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
|
+
}
|
|
7003
7577
|
const result = await runEmpathyAudit(url, {
|
|
7004
|
-
goal,
|
|
7578
|
+
...(goal ? { goal } : {}),
|
|
7005
7579
|
disabilities: disabilityNames,
|
|
7006
7580
|
wcagLevel: wcagLevel,
|
|
7007
7581
|
maxSteps,
|
|
7008
7582
|
maxTime,
|
|
7009
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 } : {}),
|
|
7010
7588
|
});
|
|
7011
7589
|
// Print formatted report
|
|
7012
7590
|
const report = formatEmpathyAuditReport(result);
|
|
@@ -7021,7 +7599,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
|
|
|
7021
7599
|
if (options.html) {
|
|
7022
7600
|
const fs = await import("fs");
|
|
7023
7601
|
const htmlReport = generateEmpathyAuditHtmlReport(result);
|
|
7024
|
-
const htmlPath = options.output
|
|
7602
|
+
const htmlPath = htmlReportPath(options.output, "empathy-audit-report.html");
|
|
7025
7603
|
fs.writeFileSync(htmlPath, htmlReport);
|
|
7026
7604
|
console.log(`🌐 HTML report saved: ${htmlPath}`);
|
|
7027
7605
|
}
|
|
@@ -7050,7 +7628,7 @@ Commands:
|
|
|
7050
7628
|
persona-questionnaire lookup Look up behaviors for a specific trait value
|
|
7051
7629
|
|
|
7052
7630
|
Options for 'start':
|
|
7053
|
-
--comprehensive Include all
|
|
7631
|
+
--comprehensive Include all 30 traits (default: 13 core traits)
|
|
7054
7632
|
--name <name> Name for the persona (prompted if not provided)
|
|
7055
7633
|
--output <file> Save questionnaire answers to JSON file
|
|
7056
7634
|
|