browser-debugger-cli 0.11.0 → 0.13.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 (222) hide show
  1. package/.claude/skills/bdg/SKILL.md +4 -4
  2. package/README.md +143 -79
  3. package/dist/commands/cdp.d.ts +22 -1
  4. package/dist/commands/cdp.js +100 -43
  5. package/dist/commands/console.d.ts +12 -0
  6. package/dist/commands/console.js +62 -12
  7. package/dist/commands/css.d.ts +13 -0
  8. package/dist/commands/css.js +53 -0
  9. package/dist/commands/dom/DomElementResolver.d.ts +3 -1
  10. package/dist/commands/dom/DomElementResolver.js +10 -3
  11. package/dist/commands/dom/a11y.js +3 -2
  12. package/dist/commands/dom/audit.d.ts +14 -0
  13. package/dist/commands/dom/audit.js +87 -0
  14. package/dist/commands/dom/eval.d.ts +3 -2
  15. package/dist/commands/dom/eval.js +11 -5
  16. package/dist/commands/dom/form.js +10 -9
  17. package/dist/commands/dom/formInteraction.js +42 -11
  18. package/dist/commands/dom/get.js +8 -8
  19. package/dist/commands/dom/helpers/index.d.ts +1 -1
  20. package/dist/commands/dom/helpers/index.js +1 -1
  21. package/dist/commands/dom/helpers/keyAttributes.d.ts +3 -2
  22. package/dist/commands/dom/helpers/keyAttributes.js +6 -4
  23. package/dist/commands/dom/helpers/query.d.ts +27 -3
  24. package/dist/commands/dom/helpers/query.js +152 -64
  25. package/dist/commands/dom/helpers/screenshot.d.ts +1 -0
  26. package/dist/commands/dom/helpers/screenshot.js +169 -49
  27. package/dist/commands/dom/index.js +7 -2
  28. package/dist/commands/dom/query.d.ts +19 -2
  29. package/dist/commands/dom/query.js +37 -6
  30. package/dist/commands/dom/screenshot.js +12 -7
  31. package/dist/commands/dom/semanticUtils.d.ts +3 -2
  32. package/dist/commands/dom/semanticUtils.js +40 -9
  33. package/dist/commands/dom/wait.js +5 -3
  34. package/dist/commands/helpJson.d.ts +82 -19
  35. package/dist/commands/helpJson.js +112 -41
  36. package/dist/commands/helpTopic.d.ts +16 -1
  37. package/dist/commands/helpTopic.js +59 -1
  38. package/dist/commands/installSkill.d.ts +15 -5
  39. package/dist/commands/installSkill.js +86 -16
  40. package/dist/commands/network/list.js +22 -12
  41. package/dist/commands/optionBehaviors.js +53 -16
  42. package/dist/commands/page.js +7 -4
  43. package/dist/commands/peek.d.ts +7 -0
  44. package/dist/commands/peek.js +65 -23
  45. package/dist/commands/shared/daemonErrorHandler.d.ts +5 -2
  46. package/dist/commands/shared/daemonErrorHandler.js +20 -9
  47. package/dist/commands/shared/dataFetcher.d.ts +12 -4
  48. package/dist/commands/shared/dataFetcher.js +12 -4
  49. package/dist/commands/shared/followMode.d.ts +9 -1
  50. package/dist/commands/shared/followMode.js +22 -4
  51. package/dist/commands/shared/optionTypes.d.ts +9 -2
  52. package/dist/commands/shared/outputFile.js +6 -1
  53. package/dist/commands/start.d.ts +20 -5
  54. package/dist/commands/start.js +84 -23
  55. package/dist/commands/stop.d.ts +11 -0
  56. package/dist/commands/stop.js +24 -1
  57. package/dist/commands/tail.d.ts +7 -1
  58. package/dist/commands/tail.js +13 -62
  59. package/dist/commands.js +2 -0
  60. package/dist/connection/cdp.d.ts +7 -0
  61. package/dist/connection/cdp.js +9 -0
  62. package/dist/connection/launcher.js +3 -2
  63. package/dist/daemon/SessionController.js +6 -1
  64. package/dist/daemon/launcher.d.ts +3 -2
  65. package/dist/daemon/launcher.js +47 -3
  66. package/dist/daemon/session/Session.d.ts +4 -1
  67. package/dist/daemon/session/Session.js +33 -2
  68. package/dist/daemon/session/TelemetryStore.d.ts +8 -1
  69. package/dist/daemon/session/TelemetryStore.js +13 -1
  70. package/dist/daemon/session/commandRegistry.js +36 -14
  71. package/dist/daemon/session/interactions.d.ts +2 -1
  72. package/dist/daemon/session/interactions.js +13 -1
  73. package/dist/daemon/session/plugins.js +19 -53
  74. package/dist/daemon/session/teardown.js +1 -1
  75. package/dist/daemon.js +9234 -7222
  76. package/dist/errors/messages.d.ts +88 -15
  77. package/dist/errors/messages.js +177 -27
  78. package/dist/index.js +19322 -13961
  79. package/dist/ipc/client.d.ts +22 -2
  80. package/dist/ipc/client.js +34 -5
  81. package/dist/ipc/protocol/auditTypes.d.ts +135 -0
  82. package/dist/ipc/protocol/auditTypes.js +6 -0
  83. package/dist/ipc/protocol/commands.d.ts +35 -0
  84. package/dist/ipc/protocol/commands.js +2 -0
  85. package/dist/ipc/protocol/domTypes.d.ts +16 -0
  86. package/dist/ipc/protocol/inspectTypes.d.ts +73 -8
  87. package/dist/ipc/session/types.d.ts +2 -0
  88. package/dist/runtime/css/search.d.ts +39 -0
  89. package/dist/runtime/css/search.js +122 -0
  90. package/dist/runtime/dom/actionEffects.d.ts +9 -2
  91. package/dist/runtime/dom/actionEffects.js +30 -14
  92. package/dist/runtime/dom/audit.d.ts +19 -0
  93. package/dist/runtime/dom/audit.js +37 -0
  94. package/dist/runtime/dom/auditModel.d.ts +45 -0
  95. package/dist/runtime/dom/auditModel.js +220 -0
  96. package/dist/runtime/dom/auditScripts.d.ts +113 -0
  97. package/dist/runtime/dom/auditScripts.js +148 -0
  98. package/dist/runtime/dom/elementGeometry.d.ts +16 -3
  99. package/dist/runtime/dom/elementGeometry.js +49 -10
  100. package/dist/runtime/dom/elementInfo.d.ts +74 -17
  101. package/dist/runtime/dom/elementInfo.js +187 -34
  102. package/dist/runtime/dom/evalHelpers.d.ts +12 -2
  103. package/dist/runtime/dom/evalHelpers.js +67 -7
  104. package/dist/runtime/dom/formDiscovery.d.ts +6 -2
  105. package/dist/runtime/dom/formDiscovery.js +20 -3
  106. package/dist/runtime/dom/formFillHelpers/fill.js +8 -12
  107. package/dist/runtime/dom/formFillHelpers/pressKey.js +2 -2
  108. package/dist/runtime/dom/formFillHelpers/shared.d.ts +16 -10
  109. package/dist/runtime/dom/formFillHelpers/shared.js +19 -52
  110. package/dist/runtime/dom/formSubmitHelpers.js +4 -3
  111. package/dist/runtime/dom/frameLayout.js +1 -0
  112. package/dist/runtime/dom/inspect.d.ts +7 -0
  113. package/dist/runtime/dom/inspect.js +92 -28
  114. package/dist/runtime/dom/inspectAllStyles.d.ts +16 -4
  115. package/dist/runtime/dom/inspectAllStyles.js +90 -7
  116. package/dist/runtime/dom/inspectCascade.d.ts +19 -2
  117. package/dist/runtime/dom/inspectCascade.js +214 -44
  118. package/dist/runtime/dom/inspectCascadeModel.d.ts +8 -0
  119. package/dist/runtime/dom/inspectCascadeModel.js +108 -34
  120. package/dist/runtime/dom/inspectHints.d.ts +26 -3
  121. package/dist/runtime/dom/inspectHints.js +125 -9
  122. package/dist/runtime/dom/inspectModel.d.ts +5 -1
  123. package/dist/runtime/dom/inspectModel.js +37 -10
  124. package/dist/runtime/dom/inspectPaintModel.d.ts +50 -22
  125. package/dist/runtime/dom/inspectPaintModel.js +182 -68
  126. package/dist/runtime/dom/inspectRules.d.ts +19 -0
  127. package/dist/runtime/dom/inspectRules.js +21 -5
  128. package/dist/runtime/dom/inspectScripts.d.ts +112 -12
  129. package/dist/runtime/dom/inspectScripts.js +357 -32
  130. package/dist/runtime/dom/inspectTree.js +10 -2
  131. package/dist/runtime/dom/inspectWhyModel.d.ts +2 -1
  132. package/dist/runtime/dom/inspectWhyModel.js +52 -10
  133. package/dist/runtime/dom/layout.js +40 -16
  134. package/dist/runtime/dom/reactEventHelpers.d.ts +21 -4
  135. package/dist/runtime/dom/reactEventHelpers.js +90 -36
  136. package/dist/runtime/dom/targetNode.d.ts +18 -5
  137. package/dist/runtime/dom/targetNode.js +268 -8
  138. package/dist/runtime/dom/wait.js +2 -1
  139. package/dist/runtime/page/bdgWorld.d.ts +57 -0
  140. package/dist/runtime/page/bdgWorld.js +180 -0
  141. package/dist/runtime/page/emulation.d.ts +13 -4
  142. package/dist/runtime/page/emulation.js +69 -4
  143. package/dist/runtime/page/replacedBuiltins.d.ts +28 -0
  144. package/dist/runtime/page/replacedBuiltins.js +136 -0
  145. package/dist/runtime/page/userAgent.d.ts +17 -0
  146. package/dist/runtime/page/userAgent.js +57 -0
  147. package/dist/session/QueryCacheManager.d.ts +4 -1
  148. package/dist/session/QueryCacheManager.js +5 -2
  149. package/dist/session/chrome.d.ts +4 -1
  150. package/dist/session/chrome.js +7 -1
  151. package/dist/session/cleanup/staleSession.d.ts +21 -4
  152. package/dist/session/cleanup/staleSession.js +79 -9
  153. package/dist/session/cleanup/userCommands.d.ts +4 -1
  154. package/dist/session/cleanup/userCommands.js +10 -5
  155. package/dist/session/daemonSocket.d.ts +10 -0
  156. package/dist/session/daemonSocket.js +22 -0
  157. package/dist/session/lastSession.d.ts +6 -3
  158. package/dist/session/lastSession.js +11 -5
  159. package/dist/session/paths.d.ts +3 -1
  160. package/dist/session/paths.js +5 -5
  161. package/dist/session/portClaims.js +4 -3
  162. package/dist/session/sessionList.d.ts +13 -5
  163. package/dist/session/sessionList.js +31 -7
  164. package/dist/telemetry/a11y.js +2 -2
  165. package/dist/telemetry/console.d.ts +2 -1
  166. package/dist/telemetry/console.js +30 -21
  167. package/dist/telemetry/pageCrash.d.ts +26 -0
  168. package/dist/telemetry/pageCrash.js +53 -0
  169. package/dist/types.d.ts +20 -0
  170. package/dist/ui/formatters/audit.d.ts +19 -0
  171. package/dist/ui/formatters/audit.js +115 -0
  172. package/dist/ui/formatters/cdp.d.ts +138 -0
  173. package/dist/ui/formatters/cdp.js +131 -0
  174. package/dist/ui/formatters/console/chronological.js +3 -1
  175. package/dist/ui/formatters/console/follow.d.ts +2 -1
  176. package/dist/ui/formatters/console/follow.js +2 -2
  177. package/dist/ui/formatters/console/json.d.ts +2 -2
  178. package/dist/ui/formatters/console/json.js +11 -5
  179. package/dist/ui/formatters/console/shared.d.ts +30 -0
  180. package/dist/ui/formatters/console/shared.js +16 -0
  181. package/dist/ui/formatters/console/summarize.d.ts +9 -2
  182. package/dist/ui/formatters/console/summarize.js +40 -9
  183. package/dist/ui/formatters/console.d.ts +2 -1
  184. package/dist/ui/formatters/console.js +7 -5
  185. package/dist/ui/formatters/details.js +3 -1
  186. package/dist/ui/formatters/dom.d.ts +2 -2
  187. package/dist/ui/formatters/dom.js +10 -8
  188. package/dist/ui/formatters/helpFormatters.js +1 -1
  189. package/dist/ui/formatters/inspect.js +50 -17
  190. package/dist/ui/formatters/installSkill.d.ts +9 -1
  191. package/dist/ui/formatters/installSkill.js +32 -6
  192. package/dist/ui/formatters/layout.js +2 -1
  193. package/dist/ui/formatters/networkList.d.ts +1 -1
  194. package/dist/ui/formatters/networkList.js +1 -2
  195. package/dist/ui/formatters/preview.d.ts +2 -0
  196. package/dist/ui/formatters/preview.js +17 -7
  197. package/dist/ui/formatters/sessions.d.ts +2 -2
  198. package/dist/ui/formatters/sessions.js +9 -2
  199. package/dist/ui/formatters/status.js +1 -1
  200. package/dist/ui/logging/logger.d.ts +1 -1
  201. package/dist/ui/messages/commands.d.ts +168 -11
  202. package/dist/ui/messages/commands.js +245 -18
  203. package/dist/ui/messages/consoleMessages.d.ts +24 -0
  204. package/dist/ui/messages/consoleMessages.js +32 -0
  205. package/dist/ui/messages/preview.d.ts +12 -0
  206. package/dist/ui/messages/preview.js +18 -2
  207. package/dist/ui/messages/session.d.ts +13 -2
  208. package/dist/ui/messages/session.js +22 -3
  209. package/dist/utils/cssValues.js +36 -4
  210. package/dist/utils/decisionTrees.js +0 -5
  211. package/dist/utils/directories.d.ts +34 -0
  212. package/dist/utils/directories.js +88 -0
  213. package/dist/utils/display.d.ts +16 -0
  214. package/dist/utils/display.js +42 -0
  215. package/dist/utils/exitCodes.d.ts +1 -0
  216. package/dist/utils/exitCodes.js +6 -0
  217. package/dist/utils/process.d.ts +12 -0
  218. package/dist/utils/process.js +25 -0
  219. package/dist/utils/suggestions.d.ts +4 -2
  220. package/dist/utils/suggestions.js +7 -5
  221. package/dist/utils/taskMappings.js +1 -1
  222. package/package.json +3 -2
@@ -8,9 +8,12 @@ import { fetchPreviewOutput, createErrorResult, } from './shared/dataFetcher.js'
8
8
  import { followFetchFailure, setupFollowMode, } from './shared/followMode.js';
9
9
  import { handleValidationError } from './shared/handleValidationError.js';
10
10
  import { MAX_LAST_ITEMS, positiveIntRule, resourceTypeRule } from './shared/validation.js';
11
+ import { CommandError } from '../errors/index.js';
12
+ import { intervalWithoutFollowError } from '../errors/messages.js';
11
13
  import { filterByResourceType } from '../telemetry/filters.js';
12
14
  import { buildPreviewJsonData, formatPreview, } from '../ui/formatters/preview.js';
13
15
  import { followingPreviewMessage, stoppedFollowingPreviewMessage } from '../ui/messages/preview.js';
16
+ import { EXIT_CODES } from '../utils/exitCodes.js';
14
17
  function parseOptions(options) {
15
18
  const lastN = positiveIntRule({
16
19
  name: '--last',
@@ -20,7 +23,17 @@ function parseOptions(options) {
20
23
  allowZeroForAll: true,
21
24
  }).validate(options.last);
22
25
  const resourceTypes = resourceTypeRule().validate(options.type);
23
- return { lastN, resourceTypes };
26
+ if (options.interval !== undefined && !options.follow) {
27
+ const err = intervalWithoutFollowError();
28
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
29
+ }
30
+ const interval = positiveIntRule({
31
+ name: '--interval',
32
+ min: 100,
33
+ max: 60000,
34
+ default: 1000,
35
+ }).validate(options.interval);
36
+ return { lastN, resourceTypes, interval };
24
37
  }
25
38
  /**
26
39
  * Data section selected by `--network` / `--console`, if any.
@@ -75,11 +88,12 @@ function createPreviewOptions(base, resourceTypes, unfilteredCount) {
75
88
  }
76
89
  return options;
77
90
  }
78
- async function runFollowMode(options, lastN, resourceTypes, baseOptions) {
91
+ async function runFollowMode(options, parsed, baseOptions) {
92
+ const { lastN, resourceTypes, interval } = parsed;
79
93
  const showPreview = async () => {
80
94
  const result = await fetchAndFilterPreview(lastN, resourceTypes, peekSection(options));
81
95
  if (!result.success) {
82
- return followFetchFailure(result, { json: options.json, retryIntervalMs: 1000 });
96
+ return followFetchFailure(result, { json: options.json, retryIntervalMs: interval });
83
97
  }
84
98
  noteFollowConnected();
85
99
  if (!options.json)
@@ -91,9 +105,51 @@ async function runFollowMode(options, lastN, resourceTypes, baseOptions) {
91
105
  await setupFollowMode(showPreview, {
92
106
  startMessage: followingPreviewMessage,
93
107
  stopMessage: stoppedFollowingPreviewMessage,
94
- intervalMs: 1000,
108
+ intervalMs: interval,
95
109
  });
96
110
  }
111
+ /**
112
+ * Parse the options, reporting an invalid one and exiting.
113
+ *
114
+ * @param options - Peek options
115
+ * @returns Parsed options
116
+ */
117
+ function parsePeekOptions(options) {
118
+ try {
119
+ return parseOptions(options);
120
+ }
121
+ catch (error) {
122
+ handleValidationError(error, options.json ?? false);
123
+ }
124
+ }
125
+ /**
126
+ * How the preview is shown.
127
+ *
128
+ * @param options - Peek options
129
+ * @param lastN - Items to show
130
+ * @returns Preview options
131
+ */
132
+ function previewDisplayOptions(options, lastN) {
133
+ return {
134
+ json: options.json,
135
+ network: options.network,
136
+ console: options.console,
137
+ last: lastN,
138
+ verbose: options.verbose,
139
+ follow: options.follow,
140
+ };
141
+ }
142
+ /**
143
+ * Watch the session data (`peek --follow`, and the deprecated `tail`).
144
+ *
145
+ * @param options - Peek options (follow implied)
146
+ */
147
+ export async function followPreview(options) {
148
+ const following = { ...options, follow: true };
149
+ showBothSectionsWhenBothRequested(following);
150
+ const parsed = parsePeekOptions(following);
151
+ await runFollowMode(following, parsed, previewDisplayOptions(following, parsed.lastN));
152
+ }
97
153
  export function registerPeekCommand(program) {
98
154
  program
99
155
  .command('peek')
@@ -103,6 +159,7 @@ export function registerPeekCommand(program) {
103
159
  .option('-n, --network', 'Show only network requests', false)
104
160
  .option('-c, --console', 'Show only console messages', false)
105
161
  .option('-f, --follow', 'Watch for updates (like tail -f)', false)
162
+ .option('--interval <ms>', 'Refresh interval of --follow in ms, 100-60000 (default: 1000)')
106
163
  .option('--last <count>', 'Show last N items, 0 for all', '10')
107
164
  .option('--type <types>', 'Filter network requests by resource type (comma-separated: Document,XHR,Fetch,etc.)')
108
165
  .action(async (options) => {
@@ -110,26 +167,11 @@ export function registerPeekCommand(program) {
110
167
  if (options.network && !options.json) {
111
168
  console.error('Note: "bdg peek --network" is deprecated. Use "bdg network list" for enhanced filtering.');
112
169
  }
113
- let lastN;
114
- let resourceTypes;
115
- try {
116
- const parsed = parseOptions(options);
117
- lastN = parsed.lastN;
118
- resourceTypes = parsed.resourceTypes;
119
- }
120
- catch (error) {
121
- handleValidationError(error, options.json ?? false);
122
- }
123
- const baseOptions = {
124
- json: options.json,
125
- network: options.network,
126
- console: options.console,
127
- last: lastN,
128
- verbose: options.verbose,
129
- follow: options.follow,
130
- };
170
+ const parsed = parsePeekOptions(options);
171
+ const { lastN, resourceTypes } = parsed;
172
+ const baseOptions = previewDisplayOptions(options, lastN);
131
173
  if (options.follow) {
132
- await runFollowMode(options, lastN, resourceTypes, baseOptions);
174
+ await runFollowMode(options, parsed, baseOptions);
133
175
  return;
134
176
  }
135
177
  await runCommand(async () => {
@@ -28,8 +28,11 @@ export declare function noteFollowConnected(): void;
28
28
  * Handle daemon connection errors with consistent formatting and behavior.
29
29
  *
30
30
  * Outside follow mode the command exits. In follow mode, a session that never
31
- * answered exits too (there is nothing to follow, exit 83); a session that
32
- * goes away is reported once and retried until a new one starts.
31
+ * answered exits too (there is nothing to follow, exit 83), and so does one
32
+ * that ends while followed (no session any more, exit 83), so a follower
33
+ * running in the background finds out. Other failures (a busy page, a
34
+ * timeout) are retried: reported once in text, and on every failed refresh
35
+ * in JSON, one object per line.
33
36
  *
34
37
  * @param error - Error message to display
35
38
  * @param options - Error handling options
@@ -4,7 +4,7 @@
4
4
  import { sessionUnavailableSuggestion } from '../../errors/messages.js';
5
5
  import { genericError } from '../../errors/messages.js';
6
6
  import { OutputBuilder } from '../../ui/OutputBuilder.js';
7
- import { connectionLostRetryMessage, connectionLostStopHintMessage, } from '../../ui/messages/preview.js';
7
+ import { connectionLostRetryMessage, connectionLostStopHintMessage, followedSessionEndedMessage, } from '../../ui/messages/preview.js';
8
8
  import { EXIT_CODES } from '../../utils/exitCodes.js';
9
9
  /** Follow-mode state: whether a session ever answered, and whether its loss was reported */
10
10
  const followState = { connected: false, lossReported: false };
@@ -20,8 +20,11 @@ export function noteFollowConnected() {
20
20
  * Handle daemon connection errors with consistent formatting and behavior.
21
21
  *
22
22
  * Outside follow mode the command exits. In follow mode, a session that never
23
- * answered exits too (there is nothing to follow, exit 83); a session that
24
- * goes away is reported once and retried until a new one starts.
23
+ * answered exits too (there is nothing to follow, exit 83), and so does one
24
+ * that ends while followed (no session any more, exit 83), so a follower
25
+ * running in the background finds out. Other failures (a busy page, a
26
+ * timeout) are retried: reported once in text, and on every failed refresh
27
+ * in JSON, one object per line.
25
28
  *
26
29
  * @param error - Error message to display
27
30
  * @param options - Error handling options
@@ -29,22 +32,30 @@ export function noteFollowConnected() {
29
32
  */
30
33
  export function handleDaemonConnectionError(error, options) {
31
34
  const { json = false, follow = false, retryIntervalMs = 1000, exitCode = EXIT_CODES.RESOURCE_NOT_FOUND, } = options;
32
- const exits = !follow || !followState.connected;
33
- if (exits || !followState.lossReported) {
35
+ const sessionGone = exitCode === EXIT_CODES.RESOURCE_NOT_FOUND;
36
+ const exits = !follow || !followState.connected || sessionGone;
37
+ const message = follow && followState.connected && sessionGone ? followedSessionEndedMessage() : error;
38
+ if (exits || json || !followState.lossReported) {
34
39
  if (json) {
35
40
  const suggestion = exits ? sessionUnavailableSuggestion(exitCode) : undefined;
36
- console.log(JSON.stringify(OutputBuilder.buildJsonError(error, { exitCode, ...(suggestion && { suggestion }) }), null, 2));
41
+ const envelope = OutputBuilder.buildJsonError(message, {
42
+ exitCode,
43
+ ...(suggestion && { suggestion }),
44
+ });
45
+ console.log(follow ? JSON.stringify(envelope) : JSON.stringify(envelope, null, 2));
37
46
  }
38
47
  else {
39
- console.error(genericError(error));
48
+ console.error(genericError(message));
40
49
  }
41
50
  }
42
51
  if (exits)
43
52
  return { shouldExit: true, exitCode };
44
53
  if (!followState.lossReported) {
45
54
  const retryMessage = retryIntervalMs >= 1000 ? `${retryIntervalMs / 1000}s` : `${retryIntervalMs}ms`;
46
- console.error(connectionLostRetryMessage(new Date().toISOString(), retryMessage));
47
- console.error(connectionLostStopHintMessage());
55
+ if (!json) {
56
+ console.error(connectionLostRetryMessage(new Date().toISOString(), retryMessage));
57
+ console.error(connectionLostStopHintMessage());
58
+ }
48
59
  followState.lossReported = true;
49
60
  }
50
61
  return { shouldExit: false };
@@ -47,18 +47,26 @@ export declare function fetchPreviewData(query?: PreviewQuery): Promise<FetchRes
47
47
  * Fetch all captured network requests from daemon.
48
48
  *
49
49
  * @param withHeaders - Include request/response headers (needed by header filters)
50
- * @returns Requests or a fetch error
50
+ * @returns Requests, and when the page crashed (while it is not loaded
51
+ * again), or a fetch error
51
52
  */
52
- export declare function fetchNetworkRequests(withHeaders?: boolean): Promise<FetchResult<NetworkRequest[]>>;
53
+ export declare function fetchNetworkRequests(withHeaders?: boolean): Promise<FetchResult<{
54
+ requests: NetworkRequest[];
55
+ pageCrashedAt: number | undefined;
56
+ }>>;
53
57
  /**
54
58
  * Fetch all console messages from daemon.
55
59
  *
56
- * @returns Messages (with their session-wide index) and the navigation id of
57
- * the page currently loaded
60
+ * @returns Messages (with their session-wide index), the navigation id of
61
+ * the page currently loaded, how many of the oldest messages the session
62
+ * dropped at its limit and when the page crashed (while it is not loaded
63
+ * again)
58
64
  */
59
65
  export declare function fetchConsoleMessages(): Promise<FetchResult<{
60
66
  messages: ConsoleMessage[];
61
67
  currentNavigationId: number | undefined;
68
+ dropped: number;
69
+ pageCrashedAt: number | undefined;
62
70
  }>>;
63
71
  interface ErrorResult {
64
72
  success: false;
@@ -90,19 +90,25 @@ export async function fetchPreviewData(query = {}) {
90
90
  * Fetch all captured network requests from daemon.
91
91
  *
92
92
  * @param withHeaders - Include request/response headers (needed by header filters)
93
- * @returns Requests or a fetch error
93
+ * @returns Requests, and when the page crashed (while it is not loaded
94
+ * again), or a fetch error
94
95
  */
95
96
  export async function fetchNetworkRequests(withHeaders = false) {
96
97
  const result = await fetchPreviewData({ lastN: 0, only: 'network', withHeaders });
97
98
  if (!result.success)
98
99
  return result;
99
- return { success: true, data: result.data.network };
100
+ return {
101
+ success: true,
102
+ data: { requests: result.data.network, pageCrashedAt: result.data.output.pageCrashedAt },
103
+ };
100
104
  }
101
105
  /**
102
106
  * Fetch all console messages from daemon.
103
107
  *
104
- * @returns Messages (with their session-wide index) and the navigation id of
105
- * the page currently loaded
108
+ * @returns Messages (with their session-wide index), the navigation id of
109
+ * the page currently loaded, how many of the oldest messages the session
110
+ * dropped at its limit and when the page crashed (while it is not loaded
111
+ * again)
106
112
  */
107
113
  export async function fetchConsoleMessages() {
108
114
  const result = await fetchPreviewData({ lastN: 0, only: 'console' });
@@ -113,6 +119,8 @@ export async function fetchConsoleMessages() {
113
119
  data: {
114
120
  messages: result.data.console,
115
121
  currentNavigationId: result.data.output.currentNavigationId,
122
+ dropped: result.data.output.totals?.consoleDropped ?? 0,
123
+ pageCrashedAt: result.data.output.pageCrashedAt,
116
124
  },
117
125
  };
118
126
  }
@@ -23,6 +23,14 @@ export declare function followFetchFailure(failure: {
23
23
  json?: boolean | undefined;
24
24
  retryIntervalMs: number;
25
25
  }): FollowPoll;
26
+ /**
27
+ * Report each page crash of a stream once: a page loaded again that crashes
28
+ * again is a new crash.
29
+ *
30
+ * @returns Function taking the crash time a refresh fetched, returning it
31
+ * when that crash was not reported yet
32
+ */
33
+ export declare function newPageCrashes(): (crashedAt: number | undefined) => number | undefined;
26
34
  /**
27
35
  * Options for configuring follow mode behavior.
28
36
  */
@@ -42,7 +50,7 @@ export interface FollowModeOptions {
42
50
  * - Initial display of start message
43
51
  * - First refresh call (awaited)
44
52
  * - Periodic interval-based refresh
45
- * - SIGINT handler for graceful shutdown
53
+ * - SIGINT/SIGTERM handlers that stop with 130/143, as shells expect
46
54
  * - Stopping with the exit code a refresh returns (e.g. the session is gone)
47
55
  *
48
56
  * @param refreshFn - Async function to call on each refresh cycle
@@ -27,6 +27,22 @@ export function followFetchFailure(failure, options) {
27
27
  ? { exitCode: result.exitCode ?? EXIT_CODES.RESOURCE_NOT_FOUND }
28
28
  : undefined;
29
29
  }
30
+ /**
31
+ * Report each page crash of a stream once: a page loaded again that crashes
32
+ * again is a new crash.
33
+ *
34
+ * @returns Function taking the crash time a refresh fetched, returning it
35
+ * when that crash was not reported yet
36
+ */
37
+ export function newPageCrashes() {
38
+ let reported;
39
+ return (crashedAt) => {
40
+ if (crashedAt === undefined || crashedAt === reported)
41
+ return undefined;
42
+ reported = crashedAt;
43
+ return crashedAt;
44
+ };
45
+ }
30
46
  /**
31
47
  * Sets up follow mode with periodic refresh and graceful shutdown.
32
48
  *
@@ -35,7 +51,7 @@ export function followFetchFailure(failure, options) {
35
51
  * - Initial display of start message
36
52
  * - First refresh call (awaited)
37
53
  * - Periodic interval-based refresh
38
- * - SIGINT handler for graceful shutdown
54
+ * - SIGINT/SIGTERM handlers that stop with 130/143, as shells expect
39
55
  * - Stopping with the exit code a refresh returns (e.g. the session is gone)
40
56
  *
41
57
  * @param refreshFn - Async function to call on each refresh cycle
@@ -71,10 +87,12 @@ export async function setupFollowMode(refreshFn, options) {
71
87
  console.error(genericError(getErrorMessage(error)));
72
88
  });
73
89
  }, intervalMs);
74
- process.on('SIGINT', () => {
90
+ const stop = (exitCode) => {
75
91
  clearInterval(intervalId);
76
92
  console.error(stopMessage());
77
- process.exit(EXIT_CODES.SUCCESS);
78
- });
93
+ process.exit(exitCode);
94
+ };
95
+ process.on('SIGINT', () => stop(EXIT_CODES.INTERRUPTED));
96
+ process.on('SIGTERM', () => stop(EXIT_CODES.TERMINATED));
79
97
  }
80
98
  //# sourceMappingURL=followMode.js.map
@@ -93,8 +93,10 @@ export interface ScreenshotOptions {
93
93
  fullPage?: boolean;
94
94
  /** CSS selector for element capture */
95
95
  selector?: string;
96
- /** Cached element index (0-based) from previous query */
96
+ /** Cached element index (0-based) from previous query, or with `selector`, which of its matches */
97
97
  index?: number;
98
+ /** Extra space (CSS px) around an element capture */
99
+ padding?: number;
98
100
  /** Continuous capture mode to directory */
99
101
  follow?: boolean;
100
102
  /** Capture interval in ms for follow mode (string from CLI) */
@@ -140,7 +142,10 @@ export type DetailsCommandOptions = BaseOptions & {
140
142
  id: string;
141
143
  };
142
144
  /** Options for DOM query command */
143
- export type DomQueryCommandOptions = BaseOptions;
145
+ export type DomQueryCommandOptions = BaseOptions & {
146
+ /** Matches listed (0 = all); default 50, or 1000 with --json */
147
+ limit?: number;
148
+ };
144
149
  /** Options for DOM get command */
145
150
  export type DomGetCommandOptions = BaseOptions & RawOptions & SelectionOptions & {
146
151
  /** All of the element's text instead of its first 500 characters (semantic output) */
@@ -373,6 +378,8 @@ export interface PeekCommandOptions extends BaseOptions, PreviewDisplayOptions {
373
378
  last?: string;
374
379
  /** Filter network requests by resource type (comma-separated) */
375
380
  type?: string;
381
+ /** Refresh interval of --follow in ms (string from CLI, default: 1000) */
382
+ interval?: string;
376
383
  }
377
384
  /**
378
385
  * Options for tail command.
@@ -7,6 +7,7 @@ import * as path from 'path';
7
7
  import { CommandError } from '../../errors/index.js';
8
8
  import { emptyOutputPathError, outputFileError } from '../../errors/messages.js';
9
9
  import { AtomicFileWriter } from '../../utils/atomicFile.js';
10
+ import { makeDirectory } from '../../utils/directories.js';
10
11
  import { EXIT_CODES } from '../../utils/exitCodes.js';
11
12
  /** What went wrong with a path, by error code */
12
13
  const PATH_PROBLEMS = {
@@ -19,6 +20,10 @@ const PATH_PROBLEMS = {
19
20
  ENAMETOOLONG: { reason: 'the name is too long', exitCode: EXIT_CODES.INVALID_ARGUMENTS },
20
21
  ENOENT: { reason: 'the directory cannot be created', exitCode: EXIT_CODES.INVALID_ARGUMENTS },
21
22
  ENOSPC: { reason: 'no space left on the device', exitCode: EXIT_CODES.SESSION_FILE_ERROR },
23
+ EPSEUDOFS: {
24
+ reason: 'it is on a pseudo-filesystem (/proc, /sys)',
25
+ exitCode: EXIT_CODES.INVALID_ARGUMENTS,
26
+ },
22
27
  };
23
28
  /**
24
29
  * A file-system error as a user-facing error about the given path.
@@ -64,7 +69,7 @@ export async function writeOutputFile(filePath, data, extension) {
64
69
  assertFilePath(filePath, extension);
65
70
  const absolutePath = path.resolve(filePath);
66
71
  try {
67
- fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
72
+ makeDirectory(path.dirname(absolutePath));
68
73
  if (typeof data === 'string')
69
74
  await AtomicFileWriter.writeAsync(absolutePath, data);
70
75
  else
@@ -26,6 +26,8 @@ export interface CollectorOptions {
26
26
  chromeFlags?: string;
27
27
  /** Viewport size, e.g. `1280x800`. */
28
28
  viewport?: string;
29
+ /** Emulate a phone (`--mobile`) */
30
+ mobile?: boolean;
29
31
  /** `prefers-color-scheme` to emulate: light or dark. */
30
32
  colorScheme?: string;
31
33
  }
@@ -53,6 +55,17 @@ export declare function extractUserDataDirFromFlags(flags: string[]): {
53
55
  * @returns The modified Command instance with all telemetry options applied
54
56
  */
55
57
  export declare function applyCollectorOptions(command: Command): Command;
58
+ /** Viewport of `--mobile` without `--viewport` (a common phone, CSS px) */
59
+ export declare const MOBILE_VIEWPORT: ViewportSize;
60
+ /**
61
+ * The viewport to emulate from `--viewport` and `--mobile`.
62
+ *
63
+ * @param viewport - `--viewport` value
64
+ * @param mobile - `--mobile` was given
65
+ * @returns Viewport (a phone's with `--mobile`), or undefined for neither
66
+ * @throws CommandError (81) for an invalid size
67
+ */
68
+ export declare function requestedViewport(viewport: string | undefined, mobile: boolean | undefined): ViewportSize | undefined;
56
69
  /**
57
70
  * Parse a `--viewport` value: width and height in CSS px joined by `x`
58
71
  * (`1280x800`; `X`, `×` and `,` work too).
@@ -71,15 +84,17 @@ export declare function parseViewport(value: string): ViewportSize;
71
84
  */
72
85
  export declare function parseColorScheme(value: string): ColorScheme;
73
86
  /**
74
- * Reject a subcommand typed without its group (`bdg query x` for
75
- * `bdg dom query x`) before Commander reads it as a start URL with extra
76
- * arguments.
87
+ * Reject a mistyped command before Commander reads it as a start URL with
88
+ * extra arguments (`too many arguments`): a subcommand typed without its
89
+ * group (`bdg query x` for `bdg dom query x`), or a word close to a command
90
+ * followed by more words (`bdg netwrk list`). A single word is left to the
91
+ * start command, which checks it the same way.
77
92
  *
78
93
  * @param program - Root command with all commands registered
79
94
  * @param argv - Process arguments
80
- * @throws CommandError (81) naming the full command
95
+ * @throws CommandError (81) naming the command meant
81
96
  */
82
- export declare function assertNotGroupSubcommand(program: Command, argv: string[]): void;
97
+ export declare function assertNotMistypedCommand(program: Command, argv: string[]): void;
83
98
  /**
84
99
  * Register the start command
85
100
  *
@@ -9,19 +9,12 @@ import { PORT_OPTION_DESCRIPTION } from '../constants.js';
9
9
  import { CommandError } from '../errors/index.js';
10
10
  import { chromeWsUrlConflictError, externalChromeUnreachableError, invalidChromeFlagError, notDevToolsEndpointError, invalidColorSchemeError, invalidUserDataDirError, invalidViewportError, missingStartUrlError, unknownCommandError, } from '../errors/messages.js';
11
11
  import { startCommandHelpMessage } from '../ui/messages/commands.js';
12
+ import { directoryProblem } from '../utils/directories.js';
13
+ import { hasDisplay } from '../utils/display.js';
12
14
  import { EXIT_CODES } from '../utils/exitCodes.js';
13
15
  import { probeDevToolsEndpoint } from '../utils/http.js';
14
16
  import { findSimilar } from '../utils/suggestions.js';
15
17
  import { devToolsHttpEndpoint, validateChromeWsUrl, validateUrl } from '../utils/url.js';
16
- /**
17
- * Check if a display server (X11 or Wayland) is available.
18
- * Used to determine default headless mode.
19
- */
20
- function hasDisplay() {
21
- const display = process.env['DISPLAY'];
22
- const wayland = process.env['WAYLAND_DISPLAY'];
23
- return (display !== undefined && display !== '') || (wayland !== undefined && wayland !== '');
24
- }
25
18
  /**
26
19
  * Expand a leading `~/` in a path to the user's home directory.
27
20
  * Chrome itself does not expand `~`, so bdg normalizes it for users.
@@ -74,14 +67,31 @@ export function applyCollectorOptions(command) {
74
67
  .option('-a, --all', 'Include all data: no tracking/analytics filtering, and capture every response body (incl. binary)', false)
75
68
  .option('-m, --max-body-size <megabytes>', 'Maximum response body size in MB', '5')
76
69
  .addOption(new Option('--compact', 'No effect; kept for compatibility').hideHelp())
77
- .option('--headless', 'Run Chrome without a window (default unless DISPLAY or WAYLAND_DISPLAY is set)', defaultHeadless)
70
+ .option('--headless', 'Run Chrome without a window (default without a display: Linux without DISPLAY or WAYLAND_DISPLAY, macOS over SSH or in CI)', defaultHeadless)
78
71
  .option('--no-headless', 'Show browser window')
79
72
  .option('--chrome-ws-url <url>', 'Connect to an existing Chrome: its DevTools port (9222, host:port, http://host:port), or a WebSocket URL: browser (ws://host:port/devtools/browser/<id>, uses the first tab) or page (.../devtools/page/<id>)')
80
73
  .option('-q, --quiet', 'Quiet mode - minimal output for AI agents', false)
81
74
  .addOption(jsonOption())
82
75
  .option('--chrome-flags <flags>', 'Custom Chrome flags (space-separated, e.g., --chrome-flags="--ignore-certificate-errors --disable-web-security")')
83
76
  .option('--viewport <WxH>', 'Viewport size in CSS px for the whole session, e.g. 1280x800 (default: 1920x1080 window)')
84
- .option('--color-scheme <scheme>', 'Emulate prefers-color-scheme for the session: light or dark (default: the system setting)');
77
+ .option('--color-scheme <scheme>', 'Emulate prefers-color-scheme for the session: light or dark (default: the system setting)')
78
+ .option('--mobile', `Emulate a phone for the session: mobile viewport (${MOBILE_VIEWPORT.width}x${MOBILE_VIEWPORT.height} unless --viewport), touch, mobile user agent`);
79
+ }
80
+ /** Viewport of `--mobile` without `--viewport` (a common phone, CSS px) */
81
+ export const MOBILE_VIEWPORT = { width: 390, height: 844 };
82
+ /**
83
+ * The viewport to emulate from `--viewport` and `--mobile`.
84
+ *
85
+ * @param viewport - `--viewport` value
86
+ * @param mobile - `--mobile` was given
87
+ * @returns Viewport (a phone's with `--mobile`), or undefined for neither
88
+ * @throws CommandError (81) for an invalid size
89
+ */
90
+ export function requestedViewport(viewport, mobile) {
91
+ const size = viewport !== undefined ? parseViewport(viewport) : undefined;
92
+ if (!mobile)
93
+ return size;
94
+ return { ...(size ?? MOBILE_VIEWPORT), mobile: true };
85
95
  }
86
96
  /** Largest viewport side accepted by `--viewport` (CSS px) */
87
97
  const MAX_VIEWPORT_SIDE = 10000;
@@ -157,12 +167,14 @@ function buildSessionOptions(options) {
157
167
  quiet: options.quiet ?? false,
158
168
  json: options.json ?? false,
159
169
  chromeFlags,
160
- viewport: options.viewport !== undefined ? parseViewport(options.viewport) : undefined,
170
+ viewport: requestedViewport(options.viewport, options.mobile),
161
171
  colorScheme: options.colorScheme !== undefined ? parseColorScheme(options.colorScheme) : undefined,
162
172
  };
163
173
  }
164
174
  /** Telemetry collected by every session. */
165
175
  const SESSION_TELEMETRY = ['dom', 'network', 'console'];
176
+ /** A word that cannot be a URL: no dot, colon or slash */
177
+ const BARE_WORD = /^[a-z][a-z0-9_-]*$/i;
166
178
  /** Flags users type as commands (`bdg version`). */
167
179
  const FLAG_WORDS = { version: '--version', help: '--help' };
168
180
  /**
@@ -177,29 +189,59 @@ const FLAG_WORDS = { version: '--version', help: '--help' };
177
189
  * @throws CommandError (81) for a bare word other than `localhost`
178
190
  */
179
191
  function assertNotCommandTypo(arg, commandNames) {
180
- if (!/^[a-z][a-z0-9_-]*$/i.test(arg) || arg.toLowerCase() === 'localhost')
192
+ if (!BARE_WORD.test(arg) || arg.toLowerCase() === 'localhost')
181
193
  return;
182
194
  const flag = FLAG_WORDS[arg.toLowerCase()];
183
195
  const err = unknownCommandError(arg, flag ? [flag] : findSimilar(arg, commandNames));
184
196
  throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
185
197
  }
186
198
  /**
187
- * Reject a subcommand typed without its group (`bdg query x` for
188
- * `bdg dom query x`) before Commander reads it as a start URL with extra
189
- * arguments.
199
+ * The words of the command line that are not options or option values of
200
+ * the root command (`bdg --session a netwrk list` gives `netwrk`, `list`).
201
+ *
202
+ * @param program - Root command
203
+ * @param args - Arguments after the executable and script
204
+ * @returns Positional words, in order
205
+ */
206
+ function positionalWords(program, args) {
207
+ const words = [];
208
+ for (let i = 0; i < args.length; i++) {
209
+ const arg = args[i] ?? '';
210
+ if (!arg.startsWith('-')) {
211
+ words.push(arg);
212
+ continue;
213
+ }
214
+ const option = program.options.find((o) => o.long === arg || o.short === arg);
215
+ if (option && (option.required || option.optional))
216
+ i++;
217
+ }
218
+ return words;
219
+ }
220
+ /**
221
+ * Reject a mistyped command before Commander reads it as a start URL with
222
+ * extra arguments (`too many arguments`): a subcommand typed without its
223
+ * group (`bdg query x` for `bdg dom query x`), or a word close to a command
224
+ * followed by more words (`bdg netwrk list`). A single word is left to the
225
+ * start command, which checks it the same way.
190
226
  *
191
227
  * @param program - Root command with all commands registered
192
228
  * @param argv - Process arguments
193
- * @throws CommandError (81) naming the full command
229
+ * @throws CommandError (81) naming the command meant
194
230
  */
195
- export function assertNotGroupSubcommand(program, argv) {
196
- const [first] = argv.slice(2).filter((arg) => !arg.startsWith('-'));
197
- if (first === undefined || program.commands.some((command) => command.name() === first))
231
+ export function assertNotMistypedCommand(program, argv) {
232
+ const [first, ...rest] = positionalWords(program, argv.slice(2));
233
+ const commandNames = program.commands.map((command) => command.name());
234
+ if (first === undefined || commandNames.includes(first))
198
235
  return;
199
236
  const group = program.commands.find((command) => command.commands.some((sub) => sub.name() === first));
200
- if (!group)
237
+ const similar = group
238
+ ? [`${group.name()} ${first}`]
239
+ : rest.length > 0 && BARE_WORD.test(first)
240
+ ? findSimilar(first, commandNames)
241
+ : [];
242
+ if (similar.length === 0)
201
243
  return;
202
- const err = unknownCommandError(first, [`${group.name()} ${first}`]);
244
+ const err = unknownCommandError(first, similar);
203
245
  throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
204
246
  }
205
247
  /**
@@ -230,7 +272,10 @@ function validateStartInput(url, options, program) {
230
272
  ...(process.env['BDG_CHROME_FLAGS']?.split(' ') ?? []),
231
273
  ...(options.chromeFlags?.split(' ') ?? []),
232
274
  ]);
233
- return { url, sessionOptions: buildSessionOptions(options) };
275
+ const sessionOptions = buildSessionOptions(options);
276
+ if (sessionOptions.userDataDir !== undefined)
277
+ assertUsableProfile(sessionOptions.userDataDir);
278
+ return { url, sessionOptions };
234
279
  }
235
280
  /**
236
281
  * Register the start command
@@ -329,6 +374,22 @@ function assertUserDataDir(value) {
329
374
  const err = invalidUserDataDirError(value, reason);
330
375
  throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
331
376
  }
377
+ /**
378
+ * Check that Chrome can create and write its profile directory (given with
379
+ * `-u` or in `--chrome-flags`), before a daemon is spawned: a profile under
380
+ * `/proc` spun the daemon at full CPU and wedged the session.
381
+ *
382
+ * @param dir - Profile directory, `~/` expanded
383
+ * @throws CommandError (81) for a path that cannot hold a directory, (82)
384
+ * when its nearest existing directory is not writable
385
+ */
386
+ function assertUsableProfile(dir) {
387
+ const problem = directoryProblem(dir);
388
+ if (!problem)
389
+ return;
390
+ const err = invalidUserDataDirError(dir, problem.reason);
391
+ throw new CommandError(err.message, { suggestion: err.suggestion }, problem.denied ? EXIT_CODES.PERMISSION_DENIED : EXIT_CODES.INVALID_ARGUMENTS);
392
+ }
332
393
  /**
333
394
  * Options given that only apply to a Chrome bdg launches. `--headless` has a
334
395
  * default, so it counts only when given on the command line.