cbrowser 19.4.3 → 19.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/THIRD_PARTY_LICENSES.md +39 -0
  2. package/dist/browser.d.ts.map +1 -1
  3. package/dist/browser.js +34 -7
  4. package/dist/browser.js.map +1 -1
  5. package/dist/cli.js +147 -95
  6. package/dist/cli.js.map +1 -1
  7. package/dist/cognitive/index.d.ts +5 -0
  8. package/dist/cognitive/index.d.ts.map +1 -1
  9. package/dist/cognitive/index.js +5 -0
  10. package/dist/cognitive/index.js.map +1 -1
  11. package/dist/config.d.ts +9 -0
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/config.js.map +1 -1
  14. package/dist/daemon.d.ts +4 -1
  15. package/dist/daemon.d.ts.map +1 -1
  16. package/dist/daemon.js +30 -7
  17. package/dist/daemon.js.map +1 -1
  18. package/dist/device-names.d.ts +10 -0
  19. package/dist/device-names.d.ts.map +1 -0
  20. package/dist/device-names.js +16 -0
  21. package/dist/device-names.js.map +1 -0
  22. package/dist/device-presets-devtools.d.ts +3 -0
  23. package/dist/device-presets-devtools.d.ts.map +1 -0
  24. package/dist/device-presets-devtools.js +823 -0
  25. package/dist/device-presets-devtools.js.map +1 -0
  26. package/dist/devices.d.ts +15 -0
  27. package/dist/devices.d.ts.map +1 -0
  28. package/dist/devices.js +196 -0
  29. package/dist/devices.js.map +1 -0
  30. package/dist/eval-script.d.ts +29 -0
  31. package/dist/eval-script.d.ts.map +1 -0
  32. package/dist/eval-script.js +46 -0
  33. package/dist/eval-script.js.map +1 -0
  34. package/dist/index.d.ts +1 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +1 -0
  37. package/dist/index.js.map +1 -1
  38. package/dist/mcp-tools/base/audit-tools.js +1 -1
  39. package/dist/mcp-tools/base/audit-tools.js.map +1 -1
  40. package/dist/terminal-safe.d.ts +12 -0
  41. package/dist/terminal-safe.d.ts.map +1 -0
  42. package/dist/terminal-safe.js +46 -0
  43. package/dist/terminal-safe.js.map +1 -0
  44. package/dist/testing/nl-test-suite.d.ts +7 -0
  45. package/dist/testing/nl-test-suite.d.ts.map +1 -1
  46. package/dist/testing/nl-test-suite.js +3 -1
  47. package/dist/testing/nl-test-suite.js.map +1 -1
  48. package/dist/types.d.ts +0 -1
  49. package/dist/types.d.ts.map +1 -1
  50. package/dist/types.js +0 -99
  51. package/dist/types.js.map +1 -1
  52. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -20,7 +20,8 @@ import { capturePerformanceBaseline, listPerformanceBaselines, loadPerformanceBa
20
20
  // Visual module imports
21
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
22
  import { BUILTIN_PERSONAS, loadCustomPersonas, saveCustomPersona, deleteCustomPersona, isBuiltinPersona, generatePersonaFromDescription, listAccessibilityPersonas, } from "./personas.js";
23
- import { DEVICE_PRESETS, LOCATION_PRESETS } from "./types.js";
23
+ import { LOCATION_PRESETS } from "./types.js";
24
+ import { CBROWSER_DEVICE_KEYS, DEVICE_PRESETS, resolveDevice } from "./devices.js";
24
25
  import { startMcpServer } from "./mcp-server.js";
25
26
  import { startRemoteMcpServer } from "./mcp-server-remote.js";
26
27
  import { startDaemon, stopDaemon, getDaemonStatus, isDaemonRunning, sendToDaemon, runDaemonServer } from "./daemon.js";
@@ -29,6 +30,8 @@ import { getStatusInfo, formatStatus, getDataDir, getPaths } from "./config.js";
29
30
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
30
31
  import { resolveCookieDomain, hostFromUrl } from "./utils.js";
31
32
  import { dirname, join, resolve } from "node:path";
33
+ import { guardConsole, guardTerminalOutput } from "./terminal-safe.js";
34
+ import { evaluateScript } from "./eval-script.js";
32
35
  import { VideoCaptureSession } from "./recording/engine.js";
33
36
  import { changeSignal, parseManifest } from "./recording/types.js";
34
37
  import { printEnterpriseStatus } from "./stealth/index.js";
@@ -298,7 +301,7 @@ COOKIE MANAGEMENT
298
301
  cookie clear Clear all cookies
299
302
 
300
303
  DEVICE EMULATION
301
- device list List available device presets
304
+ device list [filter] List device presets, one per line (filter by name)
302
305
  device set <name> Set device emulation for session
303
306
  Example: cbrowser device set iphone-15
304
307
 
@@ -345,6 +348,10 @@ NETWORK / HAR (need the daemon: each command is its own process)
345
348
  har stop [output] Stop and save HAR file
346
349
  network list List network requests the daemon's browser captured
347
350
  network clear Clear that list
351
+ console List the daemon tab's console messages (errors, warnings, logs)
352
+ --filter <type> all | error | warning | log | info (default all)
353
+ --clear Empty the console buffer after reading
354
+ --json Print the raw messages as JSON
348
355
 
349
356
  VISUAL REGRESSION (v2.5.0)
350
357
  visual save <name> Save baseline screenshot
@@ -740,6 +747,8 @@ CLAUDE SKILL INSTALLATION & SYNC
740
747
  DAEMON MODE (v6.4.0)
741
748
  daemon start Start background daemon (keeps browser running)
742
749
  --port <port> Daemon port (default: 9222)
750
+ --device <name> Device the daemon's browser emulates
751
+ --viewport <WxH> Page size of the daemon's browser
743
752
  --timeout <min> Idle timeout in minutes (default: 30)
744
753
  daemon stop Stop the running daemon
745
754
  daemon status Check if daemon is running
@@ -791,7 +800,9 @@ STORAGE & CLEANUP
791
800
 
792
801
  OPTIONS
793
802
  --browser <type> Browser: chromium, firefox, webkit (default: chromium)
794
- --device <name> Device preset: iphone-15, pixel-8, ipad-pro-12, etc.
803
+ --device <name> Device preset: iphone-16, pixel-9, ipad-pro-13, ... ('device list')
804
+ --viewport <WxH> Page size, e.g. 390x844; with --device it sets the size and
805
+ the device keeps its pixel ratio, touch and user agent
795
806
  --geo <location> Location preset or lat,lon coordinates
796
807
  --locale <locale> Browser locale (e.g., en-US, fr-FR)
797
808
  --timezone <tz> Timezone (e.g., America/New_York)
@@ -970,10 +981,17 @@ const DRIVES_THE_PAGE = [
970
981
  "keyboard", "press", "type", "evaluate", "eval", "drag", "upload",
971
982
  "smart-click", "dismiss-overlay", "assert",
972
983
  ];
973
- const GLOBAL_FLAGS = [
974
- "url", "browser", "device", "headless", "persistent", "restore",
975
- "locale", "timezone", "geo", "verbose", "help", "no-color", "quiet", "json",
984
+ /**
985
+ * Flags the shared CBrowser construction consumes, for every command that uses
986
+ * it. One list, spread into each command's set below: when this list was copied
987
+ * into each set, --viewport (2026-10-21) reached some commands and was refused
988
+ * by evaluate and keyboard.
989
+ */
990
+ const SHARED_BROWSER_FLAGS = [
991
+ "url", "browser", "device", "viewport", "headless", "persistent", "restore",
992
+ "locale", "timezone", "geo", "verbose",
976
993
  ];
994
+ const GLOBAL_FLAGS = [...SHARED_BROWSER_FLAGS, "help", "no-color", "quiet", "json"];
977
995
  /**
978
996
  * Declared options per command.
979
997
  *
@@ -1141,29 +1159,24 @@ const CAPTURE_FLAGS = new Set([
1141
1159
  "contact-sheet", "attention-overlay", "overlay-persona", "overlay-goal",
1142
1160
  "after", "after-element", "after-delay",
1143
1161
  "until-element", "until-element-gone", "until-idle", "timeout",
1144
- // globals honoured by the shared CBrowser construction
1145
- "url", "browser", "device", "headless", "persistent", "restore",
1146
- "locale", "timezone", "geo", "verbose",
1162
+ ...SHARED_BROWSER_FLAGS,
1147
1163
  ]);
1148
1164
  /**
1149
1165
  * Resolve `--device` to a real preset key, or exit naming the bad value.
1150
1166
  *
1151
- * CBrowser silently ignores an unknown device name (browser.ts: the preset
1152
- * lookup is inside the `if`), which would hand back a 1280x800 desktop capture
1153
- * for `--device "iPhone 14"` and call it success. Spaces and case are
1154
- * normalised so "iPhone 15" finds "iphone-15".
1167
+ * `capture start` checks here, before it builds anything. Every other command
1168
+ * gets the same check from CBrowser.launch() (src/devices.ts resolveDevice);
1169
+ * before 2026-10-21 the browser skipped names it did not know and rendered
1170
+ * 1280x800 desktop. Case and spaces do not matter: "iPhone 15" is "iphone-15".
1155
1171
  */
1156
1172
  function resolveDevicePreset(value) {
1157
- const normalised = value.trim().toLowerCase().replace(/\s+/g, "-");
1158
- if (DEVICE_PRESETS[normalised])
1159
- return normalised;
1160
- const known = Object.keys(DEVICE_PRESETS);
1161
- const stem = normalised.replace(/-?\d+.*$/, "");
1162
- const close = known.filter((k) => k.startsWith(stem) && stem.length > 0);
1173
+ const resolved = resolveDevice(value);
1174
+ if ("key" in resolved)
1175
+ return resolved.key;
1163
1176
  console.error(`Error: unknown --device "${value}"`);
1164
- if (close.length > 0)
1165
- console.error(` Did you mean: ${close.join(", ")}?`);
1166
- console.error(` Available presets: ${known.join(", ")}`);
1177
+ if (resolved.suggestions.length > 0)
1178
+ console.error(` Did you mean: ${resolved.suggestions.join(", ")}?`);
1179
+ console.error(` 'cbrowser device list' shows all ${Object.keys(DEVICE_PRESETS).length} presets; --viewport WIDTHxHEIGHT sets any size.`);
1167
1180
  process.exit(1);
1168
1181
  }
1169
1182
  /** Reject any flag `capture start` cannot honour, naming it. */
@@ -1747,8 +1760,7 @@ function reportAutoCapture(capture) {
1747
1760
  /** Flags `evaluate` understands; anything else is rejected rather than ignored. */
1748
1761
  const EVALUATE_FLAGS = new Set([
1749
1762
  "file", "arg", "json", "raw", "wait-for", "timeout", "expect-truthy", "force",
1750
- "url", "browser", "device", "headless", "persistent", "restore",
1751
- "locale", "timezone", "geo", "verbose",
1763
+ ...SHARED_BROWSER_FLAGS,
1752
1764
  ]);
1753
1765
  /**
1754
1766
  * `--force` is a bare flag, but the parser gives any flag the next non-flag
@@ -1781,53 +1793,6 @@ function repeatedFlag(name) {
1781
1793
  }
1782
1794
  return values;
1783
1795
  }
1784
- /**
1785
- * Evaluate a script in the page, accepting all three shapes people write.
1786
- *
1787
- * `--file` exists because multi-line scripts die in shell quoting, so it has to
1788
- * accept real scripts - `const`, `let`, an explicit `return` - not only a
1789
- * single expression. The source is compiled inside the page as a function
1790
- * body: a bare expression gets an implicit return, a statement body keeps its
1791
- * own, and a function/arrow literal is called with the arguments.
1792
- */
1793
- async function evaluateScript(page, script, args) {
1794
- const source = script.trim();
1795
- // A function or arrow literal is called with the arguments directly.
1796
- const isCallable = /^(async\s+)?function\b/.test(source) || /=>/.test(source);
1797
- return page.evaluate((payload) => {
1798
- if (payload.callable) {
1799
- const fn = (0, eval)(`(${payload.src})`);
1800
- return fn(...payload.a);
1801
- }
1802
- const compile = (src) => {
1803
- // 1. A bare expression: implicit return.
1804
- try {
1805
- return new Function("args", `return (${src});`);
1806
- }
1807
- catch { /* not an expression */ }
1808
- // 2. Statements with no explicit return: return the final expression,
1809
- // which is what a REPL and the old one-arg form both did.
1810
- if (!/\breturn\b/.test(src)) {
1811
- const trimmed = src.replace(/;\s*$/, "");
1812
- const split = Math.max(trimmed.lastIndexOf(";"), trimmed.lastIndexOf("\n"));
1813
- if (split > -1) {
1814
- const head = trimmed.slice(0, split + 1);
1815
- const tail = trimmed.slice(split + 1).trim();
1816
- if (tail) {
1817
- try {
1818
- return new Function("args", `${head}\nreturn (${tail});`);
1819
- }
1820
- catch { /* the tail is not an expression either */ }
1821
- }
1822
- }
1823
- }
1824
- // 3. Statements as written. Any syntax error surfaces from here, which
1825
- // is the error the user should actually see.
1826
- return new Function("args", src);
1827
- };
1828
- return compile(payload.src)(payload.a);
1829
- }, { src: source, a: args, callable: isCallable });
1830
- }
1831
1796
  /**
1832
1797
  * Run JavaScript in the page.
1833
1798
  *
@@ -2067,8 +2032,7 @@ function parseKeyboardToken(token, literal = false) {
2067
2032
  /** Flags `keyboard` understands. */
2068
2033
  const KEYBOARD_FLAGS = new Set([
2069
2034
  "delay", "selector", "hold", "repeat", "text", "force",
2070
- "url", "browser", "device", "headless", "persistent", "restore",
2071
- "locale", "timezone", "geo", "verbose",
2035
+ ...SHARED_BROWSER_FLAGS,
2072
2036
  ]);
2073
2037
  /**
2074
2038
  * Drive a keyboard sequence.
@@ -2614,6 +2578,12 @@ async function main() {
2614
2578
  const outputOpts = detectOutputOptions(options);
2615
2579
  setOutputOptions(outputOpts);
2616
2580
  installOutputWrappers();
2581
+ // Page-derived text (console messages, titles, extracted text) reaches stdout in
2582
+ // many places; escape sequences and bidi overrides in it come out as visible
2583
+ // escapes instead of acting on the terminal. (2026-10-12)
2584
+ guardTerminalOutput(process.stdout);
2585
+ guardTerminalOutput(process.stderr);
2586
+ guardConsole();
2617
2587
  // --json-output must never leave stdout empty. Only two commands in this file
2618
2588
  // call emitJson, so without this every other command printed nothing at all and
2619
2589
  // still exited 0 — a CI step piping to jq saw empty input beside a success code.
@@ -2857,9 +2827,28 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
2857
2827
  const port = parseInt(options.port) || 9222;
2858
2828
  switch (subCommand) {
2859
2829
  case "start": {
2830
+ // The daemon's browser takes its device and size from here. Both are
2831
+ // checked now: a bad value inside the detached daemon would only show
2832
+ // up as a daemon that failed to start.
2833
+ const launchArgs = [];
2834
+ if (typeof options.device === "string")
2835
+ launchArgs.push("--device", resolveDevicePreset(options.device));
2836
+ if (typeof options.viewport === "string") {
2837
+ try {
2838
+ parseViewportList(options.viewport);
2839
+ }
2840
+ catch (e) {
2841
+ console.error(`Error: ${e.message}`);
2842
+ process.exit(1);
2843
+ }
2844
+ launchArgs.push("--viewport", options.viewport);
2845
+ }
2860
2846
  console.log("šŸš€ Starting CBrowser daemon...");
2861
- const result = await startDaemon(port);
2847
+ const result = await startDaemon(port, launchArgs);
2862
2848
  console.log(result.success ? `āœ“ ${result.message}` : `āœ— ${result.message}`);
2849
+ if (result.alreadyRunning && launchArgs.length > 0) {
2850
+ console.error("⚠ --device and --viewport were not applied: the daemon was already running. Run cbrowser daemon stop first.");
2851
+ }
2863
2852
  process.exit(result.success ? 0 : 1);
2864
2853
  break;
2865
2854
  }
@@ -2883,9 +2872,12 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
2883
2872
  : options.browser === "webkit" ? "webkit"
2884
2873
  : "chromium";
2885
2874
  // runDaemonServer will merge with defaults internally
2875
+ const runViewport = typeof options.viewport === "string" ? parseViewportList(options.viewport)[0] : undefined;
2886
2876
  await runDaemonServer({
2887
2877
  browser: browserType,
2888
2878
  headless: options.headless !== false && options.headless !== "false",
2879
+ ...(typeof options.device === "string" ? { device: options.device } : {}),
2880
+ ...(runViewport ? { viewportWidth: runViewport.width, viewportHeight: runViewport.height, viewportOverride: runViewport } : {}),
2889
2881
  }, port);
2890
2882
  return;
2891
2883
  }
@@ -2908,6 +2900,12 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
2908
2900
  ];
2909
2901
  if (daemonRunning && INTERACTION_COMMANDS.includes(command)) {
2910
2902
  console.log("šŸ”Œ Connected to running daemon");
2903
+ // The daemon's browser keeps the device and size it started with; a flag on
2904
+ // a command it runs cannot change them, so say so instead of ignoring it.
2905
+ if (typeof options.device === "string" || typeof options.viewport === "string") {
2906
+ console.error("⚠ --device and --viewport do not apply to commands the daemon runs: its browser keeps the device and size it started with.");
2907
+ console.error(" To change them: cbrowser daemon stop, then cbrowser daemon start --device <name> --viewport <WxH>");
2908
+ }
2911
2909
  const daemonCommand = command;
2912
2910
  let daemonArgs = {};
2913
2911
  switch (command) {
@@ -2978,7 +2976,11 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
2978
2976
  console.error(" Re-run with --force to run it anyway.");
2979
2977
  process.exit(1);
2980
2978
  }
2981
- daemonArgs = { body: `return (function(){ ${/\breturn\b/.test(script) ? script : `return (${script});`} })();`, args: [], force: true };
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 };
2982
2984
  break;
2983
2985
  }
2984
2986
  case "screenshot": {
@@ -3019,7 +3021,9 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
3019
3021
  const result = await sendToDaemon(daemonCommand, daemonArgs);
3020
3022
  if (result.success) {
3021
3023
  console.log("āœ“ Command executed via daemon");
3022
- if (result.result) {
3024
+ // evaluate's value is the answer, so 0, false and "" print too; for other
3025
+ // commands an empty result means nothing to show. (2026-10-13)
3026
+ if (daemonCommand === "evaluate" ? result.result !== undefined : result.result) {
3023
3027
  console.log(JSON.stringify(result.result, null, 2));
3024
3028
  }
3025
3029
  }
@@ -3085,7 +3089,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
3085
3089
  browser: browserType,
3086
3090
  headless,
3087
3091
  device: options.device,
3088
- ...(viewportOverride ? { viewportWidth: viewportOverride.width, viewportHeight: viewportOverride.height } : {}),
3092
+ ...(viewportOverride ? { viewportWidth: viewportOverride.width, viewportHeight: viewportOverride.height, viewportOverride } : {}),
3089
3093
  geolocation,
3090
3094
  locale: options.locale,
3091
3095
  timezone: options.timezone,
@@ -4230,14 +4234,18 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
4230
4234
  const subcommand = args[0];
4231
4235
  switch (subcommand) {
4232
4236
  case "list": {
4233
- console.log("\nšŸ“± Available Device Presets:\n");
4234
- for (const [name, device] of Object.entries(DEVICE_PRESETS)) {
4235
- console.log(` ${name}`);
4236
- console.log(` ${device.name}`);
4237
- console.log(` ${device.viewport.width}x${device.viewport.height} @${device.deviceScaleFactor}x`);
4238
- console.log(` Mobile: ${device.isMobile} | Touch: ${device.hasTouch}`);
4239
- console.log("");
4240
- }
4237
+ // One line per preset: there are a few hundred now. An optional
4238
+ // filter matches the key or the display name.
4239
+ const filter = (args[1] ?? "").toLowerCase();
4240
+ const rows = Object.entries(DEVICE_PRESETS).filter(([key, d]) => !filter || key.includes(filter) || d.name.toLowerCase().includes(filter));
4241
+ const width = Math.max(...rows.map(([key]) => key.length), 4);
4242
+ console.log(`\nšŸ“± Device presets${filter ? ` matching "${filter}"` : ""}: ${rows.length}\n`);
4243
+ for (const [key, d] of rows) {
4244
+ const kind = d.isMobile ? "mobile" : "desktop";
4245
+ const touch = d.hasTouch ? ", touch" : "";
4246
+ console.log(` ${key.padEnd(width)} ${d.viewport.width}x${d.viewport.height} @${d.deviceScaleFactor}x ${kind}${touch} (${d.name})`);
4247
+ }
4248
+ console.log("\n Case and spaces do not matter: --device \"iPhone 16\" is iphone-16.");
4241
4249
  break;
4242
4250
  }
4243
4251
  case "set": {
@@ -4246,15 +4254,11 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
4246
4254
  console.error("Usage: cbrowser device set <name>");
4247
4255
  process.exit(1);
4248
4256
  }
4249
- if (!DEVICE_PRESETS[deviceName]) {
4250
- console.error(`Unknown device: ${deviceName}`);
4251
- console.error("Run 'cbrowser device list' to see available devices");
4252
- process.exit(1);
4253
- }
4257
+ const presetKey = resolveDevicePreset(deviceName);
4254
4258
  // Save device setting to persistent session state
4255
4259
  const deviceBrowser = new CBrowser({ persistent: true, headless: true, verbose: false });
4256
- deviceBrowser.saveDeviceSetting(deviceName);
4257
- console.log(`āœ“ Device set: ${deviceName}`);
4260
+ deviceBrowser.saveDeviceSetting(presetKey);
4261
+ console.log(`āœ“ Device set: ${presetKey}`);
4258
4262
  console.log(" Device emulation will apply to all subsequent commands");
4259
4263
  break;
4260
4264
  }
@@ -4427,6 +4431,45 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
4427
4431
  // =========================================================================
4428
4432
  // Network
4429
4433
  // =========================================================================
4434
+ case "console": {
4435
+ // The daemon tab's console (log, info, warning, error, uncaught exceptions,
4436
+ // failed resources), as get_console_messages returns it over MCP. Served by
4437
+ // the daemon: a fresh process has no tab and would always list nothing.
4438
+ const filter = typeof options.filter === "string" ? options.filter : "all";
4439
+ if (!["all", "error", "warning", "log", "info"].includes(filter)) {
4440
+ console.error(`Error: --filter must be one of all, error, warning, log, info (got "${filter}")`);
4441
+ process.exit(1);
4442
+ }
4443
+ if (!(await isDaemonRunning())) {
4444
+ console.error("Error: the console log only spans commands in the daemon's browser; each cbrowser command is");
4445
+ console.error(" its own process. Start the daemon first:");
4446
+ console.error(" cbrowser daemon start -> navigate/click... -> cbrowser console [--filter error] [--clear] [--json]");
4447
+ process.exit(1);
4448
+ }
4449
+ const response = await sendToDaemon("consoleMessages", { filter, clear: options.clear === true });
4450
+ if (!response.success) {
4451
+ console.error(`āœ— Daemon error: ${response.error}`);
4452
+ process.exit(1);
4453
+ }
4454
+ const out = response.result;
4455
+ if (options.json === true) {
4456
+ console.log(JSON.stringify(out, null, 2));
4457
+ }
4458
+ else if (out.count === 0) {
4459
+ console.log(filter === "all" ? "No console messages" : `No ${filter} console messages`);
4460
+ }
4461
+ else {
4462
+ console.log(`\nšŸ–„ Console (${out.count}${out.dropped ? `, ${out.dropped} older dropped` : ""}):\n`);
4463
+ for (const m of out.messages) {
4464
+ const where = m.url ? ` (${m.url}${m.lineNumber !== undefined ? `:${m.lineNumber}` : ""})` : "";
4465
+ console.log(` [${m.type}${m.source === "pageerror" ? ", uncaught" : ""}] ${m.text}${where}`);
4466
+ console.log(` on ${m.pageUrl}`);
4467
+ }
4468
+ }
4469
+ if (options.clear === true)
4470
+ console.log("āœ“ Console cleared");
4471
+ process.exit(0);
4472
+ }
4430
4473
  case "network": {
4431
4474
  const subcommand = args[0];
4432
4475
  if (subcommand !== "list" && subcommand !== "clear") {
@@ -5398,9 +5441,10 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
5398
5441
  console.error("Usage: cbrowser parallel devices <url> [--devices <list>] [--concurrency <n>]");
5399
5442
  process.exit(1);
5400
5443
  }
5444
+ // Without --devices, cbrowser's own presets, not all few hundred.
5401
5445
  const deviceList = options.devices
5402
- ? options.devices.split(",")
5403
- : Object.keys(DEVICE_PRESETS);
5446
+ ? options.devices.split(",").map((d) => resolveDevicePreset(d))
5447
+ : [...CBROWSER_DEVICE_KEYS];
5404
5448
  const concurrency = options.concurrency ? parseInt(options.concurrency) : 3;
5405
5449
  console.log(`\nšŸš€ Running parallel device tests...`);
5406
5450
  console.log(` URL: ${url}`);
@@ -5862,7 +5906,11 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
5862
5906
  });
5863
5907
  console.log(`\nšŸ” Step-through mode enabled. Press Enter to execute each step.\n`);
5864
5908
  const { CBrowser } = await import("./browser.js");
5865
- const stepBrowser = new CBrowser({ headless });
5909
+ const stepBrowser = new CBrowser({
5910
+ headless,
5911
+ ...(typeof options.device === "string" ? { device: options.device } : {}),
5912
+ ...(viewportOverride ? { viewportWidth: viewportOverride.width, viewportHeight: viewportOverride.height, viewportOverride } : {}),
5913
+ });
5866
5914
  await stepBrowser.launch();
5867
5915
  try {
5868
5916
  const totalSteps = suite.tests.reduce((sum, t) => sum + t.steps.length, 0);
@@ -5943,6 +5991,8 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
5943
5991
  continueOnFailure: options["continue-on-failure"] === true,
5944
5992
  screenshotOnFailure: options["screenshot-on-failure"] !== false,
5945
5993
  headless,
5994
+ device: typeof options.device === "string" ? options.device : undefined,
5995
+ viewport: viewportOverride,
5946
5996
  fuzzyMatch: options["fuzzy-match"] === true,
5947
5997
  capture: buildAutoCaptureSetting(options, `test-suite-${suite.name.replace(/[^a-zA-Z0-9-_]+/g, "-").toLowerCase()}-${Date.now()}`),
5948
5998
  };
@@ -6517,6 +6567,8 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
6517
6567
  maxTime,
6518
6568
  verbose,
6519
6569
  headless,
6570
+ device: typeof options.device === "string" ? options.device : undefined,
6571
+ viewport: viewportOverride,
6520
6572
  vision,
6521
6573
  customTraits, // Pass custom traits from env var
6522
6574
  location: locationOverride, // Pass location override