browser-debugger-cli 0.12.0 → 0.14.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 (224) hide show
  1. package/.claude/skills/bdg/SKILL.md +100 -186
  2. package/README.md +5 -4
  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 +67 -13
  7. package/dist/commands/dom/DomElementResolver.d.ts +3 -1
  8. package/dist/commands/dom/DomElementResolver.js +10 -3
  9. package/dist/commands/dom/a11y.d.ts +1 -1
  10. package/dist/commands/dom/a11y.js +23 -22
  11. package/dist/commands/dom/eval.d.ts +4 -2
  12. package/dist/commands/dom/eval.js +31 -7
  13. package/dist/commands/dom/form.js +10 -9
  14. package/dist/commands/dom/formInteraction.js +9 -8
  15. package/dist/commands/dom/get.js +32 -14
  16. package/dist/commands/dom/helpers/index.d.ts +1 -1
  17. package/dist/commands/dom/helpers/index.js +1 -1
  18. package/dist/commands/dom/helpers/query.d.ts +27 -3
  19. package/dist/commands/dom/helpers/query.js +152 -64
  20. package/dist/commands/dom/helpers/screenshot.js +13 -13
  21. package/dist/commands/dom/index.js +10 -3
  22. package/dist/commands/dom/query.d.ts +20 -2
  23. package/dist/commands/dom/query.js +39 -6
  24. package/dist/commands/dom/screenshot.js +3 -1
  25. package/dist/commands/dom/semanticUtils.d.ts +3 -2
  26. package/dist/commands/dom/semanticUtils.js +40 -9
  27. package/dist/commands/helpJson.d.ts +82 -19
  28. package/dist/commands/helpJson.js +112 -41
  29. package/dist/commands/helpTopic.d.ts +16 -1
  30. package/dist/commands/helpTopic.js +59 -1
  31. package/dist/commands/installSkill.d.ts +15 -5
  32. package/dist/commands/installSkill.js +86 -16
  33. package/dist/commands/network/list.js +65 -12
  34. package/dist/commands/optionBehaviors.d.ts +25 -2
  35. package/dist/commands/optionBehaviors.js +81 -46
  36. package/dist/commands/peek.js +3 -0
  37. package/dist/commands/shared/CommandRunner.js +13 -13
  38. package/dist/commands/shared/daemonErrorHandler.d.ts +5 -2
  39. package/dist/commands/shared/daemonErrorHandler.js +21 -10
  40. package/dist/commands/shared/dataFetcher.d.ts +14 -4
  41. package/dist/commands/shared/dataFetcher.js +20 -4
  42. package/dist/commands/shared/followMode.d.ts +9 -1
  43. package/dist/commands/shared/followMode.js +22 -4
  44. package/dist/commands/shared/handleValidationError.js +3 -3
  45. package/dist/commands/shared/optionTypes.d.ts +17 -3
  46. package/dist/commands/shared/outputFile.js +6 -1
  47. package/dist/commands/shared/startHelpers.js +3 -3
  48. package/dist/commands/start.d.ts +7 -5
  49. package/dist/commands/start.js +65 -21
  50. package/dist/commands/stop.d.ts +11 -0
  51. package/dist/commands/stop.js +24 -1
  52. package/dist/commands.js +1 -1
  53. package/dist/connection/cdp.d.ts +7 -0
  54. package/dist/connection/cdp.js +9 -0
  55. package/dist/connection/chromeIdentity.d.ts +8 -2
  56. package/dist/connection/chromeIdentity.js +85 -13
  57. package/dist/connection/launcher.js +3 -2
  58. package/dist/constants.d.ts +29 -1
  59. package/dist/constants.js +35 -1
  60. package/dist/daemon/SessionController.js +8 -1
  61. package/dist/daemon/launcher.d.ts +3 -2
  62. package/dist/daemon/launcher.js +47 -3
  63. package/dist/daemon/session/Session.d.ts +5 -1
  64. package/dist/daemon/session/Session.js +42 -3
  65. package/dist/daemon/session/TelemetryStore.d.ts +15 -1
  66. package/dist/daemon/session/TelemetryStore.js +19 -1
  67. package/dist/daemon/session/commandRegistry.js +52 -18
  68. package/dist/daemon/session/interactions.d.ts +2 -1
  69. package/dist/daemon/session/interactions.js +13 -1
  70. package/dist/daemon/session/matchedStylesReset.d.ts +26 -0
  71. package/dist/daemon/session/matchedStylesReset.js +46 -0
  72. package/dist/daemon/session/plugins.js +17 -2
  73. package/dist/daemon/session/teardown.js +1 -1
  74. package/dist/daemon/session/triggeredRequests.d.ts +0 -5
  75. package/dist/daemon/session/triggeredRequests.js +13 -7
  76. package/dist/daemon.js +2385 -1229
  77. package/dist/errors/messages.d.ts +62 -11
  78. package/dist/errors/messages.js +119 -22
  79. package/dist/index.js +14995 -9866
  80. package/dist/ipc/client.d.ts +18 -2
  81. package/dist/ipc/client.js +26 -5
  82. package/dist/ipc/protocol/auditTypes.d.ts +8 -2
  83. package/dist/ipc/protocol/commands.d.ts +16 -0
  84. package/dist/ipc/protocol/domTypes.d.ts +12 -0
  85. package/dist/ipc/protocol/inspectTypes.d.ts +7 -2
  86. package/dist/ipc/session/types.d.ts +7 -1
  87. package/dist/program.d.ts +14 -0
  88. package/dist/program.js +53 -0
  89. package/dist/runtime/dom/actionEffects.d.ts +5 -1
  90. package/dist/runtime/dom/actionEffects.js +26 -14
  91. package/dist/runtime/dom/audit.js +3 -2
  92. package/dist/runtime/dom/auditModel.js +6 -1
  93. package/dist/runtime/dom/auditScripts.d.ts +9 -3
  94. package/dist/runtime/dom/auditScripts.js +41 -5
  95. package/dist/runtime/dom/elementGeometry.d.ts +33 -3
  96. package/dist/runtime/dom/elementGeometry.js +44 -19
  97. package/dist/runtime/dom/elementInfo.d.ts +76 -18
  98. package/dist/runtime/dom/elementInfo.js +190 -40
  99. package/dist/runtime/dom/evalHelpers.d.ts +12 -2
  100. package/dist/runtime/dom/evalHelpers.js +67 -7
  101. package/dist/runtime/dom/formDiscovery.d.ts +6 -2
  102. package/dist/runtime/dom/formDiscovery.js +20 -3
  103. package/dist/runtime/dom/formFillHelpers/fill.js +7 -11
  104. package/dist/runtime/dom/formFillHelpers/pressKey.js +2 -2
  105. package/dist/runtime/dom/formFillHelpers/shared.d.ts +16 -10
  106. package/dist/runtime/dom/formFillHelpers/shared.js +19 -52
  107. package/dist/runtime/dom/formSubmitHelpers.js +4 -3
  108. package/dist/runtime/dom/frameLayout.js +1 -0
  109. package/dist/runtime/dom/frameScopedConnection.d.ts +7 -0
  110. package/dist/runtime/dom/frameScopedConnection.js +2 -2
  111. package/dist/runtime/dom/inspect.d.ts +17 -3
  112. package/dist/runtime/dom/inspect.js +45 -32
  113. package/dist/runtime/dom/inspectAllStyles.js +1 -0
  114. package/dist/runtime/dom/inspectHints.d.ts +1 -1
  115. package/dist/runtime/dom/inspectModel.d.ts +5 -4
  116. package/dist/runtime/dom/inspectModel.js +7 -3
  117. package/dist/runtime/dom/inspectPaintModel.d.ts +2 -0
  118. package/dist/runtime/dom/inspectPaintModel.js +3 -1
  119. package/dist/runtime/dom/inspectRules.d.ts +29 -3
  120. package/dist/runtime/dom/inspectRules.js +205 -11
  121. package/dist/runtime/dom/inspectScripts.d.ts +29 -2
  122. package/dist/runtime/dom/inspectScripts.js +49 -10
  123. package/dist/runtime/dom/layout.d.ts +0 -2
  124. package/dist/runtime/dom/layout.js +10 -9
  125. package/dist/runtime/dom/reactEventHelpers.d.ts +17 -4
  126. package/dist/runtime/dom/reactEventHelpers.js +71 -28
  127. package/dist/runtime/dom/targetNode.d.ts +27 -10
  128. package/dist/runtime/dom/targetNode.js +283 -16
  129. package/dist/runtime/dom/wait.js +2 -1
  130. package/dist/runtime/page/bdgWorld.d.ts +57 -0
  131. package/dist/runtime/page/bdgWorld.js +180 -0
  132. package/dist/runtime/page/replacedBuiltins.d.ts +28 -0
  133. package/dist/runtime/page/replacedBuiltins.js +136 -0
  134. package/dist/session/QueryCacheManager.d.ts +4 -1
  135. package/dist/session/QueryCacheManager.js +5 -2
  136. package/dist/session/chrome.d.ts +4 -1
  137. package/dist/session/chrome.js +7 -1
  138. package/dist/session/cleanup/staleSession.d.ts +21 -4
  139. package/dist/session/cleanup/staleSession.js +79 -9
  140. package/dist/session/cleanup/userCommands.d.ts +4 -1
  141. package/dist/session/cleanup/userCommands.js +10 -5
  142. package/dist/session/daemonSocket.d.ts +10 -0
  143. package/dist/session/daemonSocket.js +22 -0
  144. package/dist/session/lastSession.d.ts +6 -3
  145. package/dist/session/lastSession.js +11 -5
  146. package/dist/session/paths.d.ts +3 -1
  147. package/dist/session/paths.js +5 -5
  148. package/dist/session/portClaims.js +4 -3
  149. package/dist/session/sessionList.d.ts +13 -5
  150. package/dist/session/sessionList.js +31 -7
  151. package/dist/telemetry/a11y.d.ts +15 -1
  152. package/dist/telemetry/a11y.js +85 -2
  153. package/dist/telemetry/console.d.ts +2 -1
  154. package/dist/telemetry/console.js +30 -21
  155. package/dist/telemetry/har/builder.js +1 -1
  156. package/dist/telemetry/network.d.ts +13 -16
  157. package/dist/telemetry/network.js +30 -52
  158. package/dist/telemetry/networkRetention.d.ts +83 -0
  159. package/dist/telemetry/networkRetention.js +117 -0
  160. package/dist/telemetry/pageCrash.d.ts +26 -0
  161. package/dist/telemetry/pageCrash.js +53 -0
  162. package/dist/types.d.ts +42 -0
  163. package/dist/ui/OutputBuilder.d.ts +10 -0
  164. package/dist/ui/OutputBuilder.js +12 -0
  165. package/dist/ui/formatters/a11y.d.ts +5 -7
  166. package/dist/ui/formatters/a11y.js +7 -61
  167. package/dist/ui/formatters/audit.js +14 -5
  168. package/dist/ui/formatters/cdp.d.ts +138 -0
  169. package/dist/ui/formatters/cdp.js +131 -0
  170. package/dist/ui/formatters/console/chronological.js +7 -5
  171. package/dist/ui/formatters/console/follow.d.ts +5 -2
  172. package/dist/ui/formatters/console/follow.js +7 -4
  173. package/dist/ui/formatters/console/json.d.ts +4 -7
  174. package/dist/ui/formatters/console/json.js +16 -14
  175. package/dist/ui/formatters/console/shared.d.ts +47 -2
  176. package/dist/ui/formatters/console/shared.js +33 -0
  177. package/dist/ui/formatters/console/summarize.d.ts +9 -2
  178. package/dist/ui/formatters/console/summarize.js +57 -11
  179. package/dist/ui/formatters/console.d.ts +3 -2
  180. package/dist/ui/formatters/console.js +8 -10
  181. package/dist/ui/formatters/details.js +4 -2
  182. package/dist/ui/formatters/dom.d.ts +14 -5
  183. package/dist/ui/formatters/dom.js +30 -13
  184. package/dist/ui/formatters/helpFormatters.js +1 -1
  185. package/dist/ui/formatters/inspect.js +9 -3
  186. package/dist/ui/formatters/installSkill.d.ts +9 -1
  187. package/dist/ui/formatters/installSkill.js +32 -6
  188. package/dist/ui/formatters/layout.js +4 -2
  189. package/dist/ui/formatters/longValues.d.ts +14 -0
  190. package/dist/ui/formatters/longValues.js +23 -0
  191. package/dist/ui/formatters/networkList.d.ts +8 -2
  192. package/dist/ui/formatters/networkList.js +11 -3
  193. package/dist/ui/formatters/preview.d.ts +6 -1
  194. package/dist/ui/formatters/preview.js +67 -15
  195. package/dist/ui/formatters/sessions.d.ts +2 -2
  196. package/dist/ui/formatters/sessions.js +9 -2
  197. package/dist/ui/formatters/status.js +7 -0
  198. package/dist/ui/formatters/triggeredRequests.js +2 -1
  199. package/dist/ui/logging/logger.d.ts +1 -1
  200. package/dist/ui/messages/chrome.d.ts +20 -1
  201. package/dist/ui/messages/chrome.js +29 -3
  202. package/dist/ui/messages/commands.d.ts +153 -12
  203. package/dist/ui/messages/commands.js +198 -15
  204. package/dist/ui/messages/consoleMessages.d.ts +24 -0
  205. package/dist/ui/messages/consoleMessages.js +32 -0
  206. package/dist/ui/messages/networkMessages.d.ts +24 -0
  207. package/dist/ui/messages/networkMessages.js +45 -0
  208. package/dist/ui/messages/preview.d.ts +6 -0
  209. package/dist/ui/messages/preview.js +9 -1
  210. package/dist/ui/messages/session.d.ts +13 -2
  211. package/dist/ui/messages/session.js +22 -3
  212. package/dist/utils/directories.d.ts +34 -0
  213. package/dist/utils/directories.js +88 -0
  214. package/dist/utils/display.d.ts +16 -0
  215. package/dist/utils/display.js +42 -0
  216. package/dist/utils/exitCodes.d.ts +1 -0
  217. package/dist/utils/exitCodes.js +6 -0
  218. package/dist/utils/http.d.ts +9 -2
  219. package/dist/utils/http.js +4 -3
  220. package/dist/utils/process.d.ts +12 -0
  221. package/dist/utils/process.js +25 -0
  222. package/dist/utils/strings.d.ts +19 -0
  223. package/dist/utils/strings.js +16 -0
  224. package/package.json +2 -2
@@ -3,8 +3,8 @@
3
3
  */
4
4
  import { sessionUnavailableSuggestion } from '../../errors/messages.js';
5
5
  import { genericError } from '../../errors/messages.js';
6
- import { OutputBuilder } from '../../ui/OutputBuilder.js';
7
- import { connectionLostRetryMessage, connectionLostStopHintMessage, } from '../../ui/messages/preview.js';
6
+ import { OutputBuilder, stringifyEnvelope } from '../../ui/OutputBuilder.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) : stringifyEnvelope(envelope));
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 };
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import type { PeekSection } from '../../ipc/protocol/commands.js';
5
5
  import type { BdgOutput, ConsoleMessage, NetworkRequest } from '../../types.js';
6
+ import type { NetworkEvictionCounts } from '../../ui/messages/networkMessages.js';
6
7
  export type FetchSuccess<T> = {
7
8
  success: true;
8
9
  data: T;
@@ -47,18 +48,27 @@ export declare function fetchPreviewData(query?: PreviewQuery): Promise<FetchRes
47
48
  * Fetch all captured network requests from daemon.
48
49
  *
49
50
  * @param withHeaders - Include request/response headers (needed by header filters)
50
- * @returns Requests or a fetch error
51
+ * @returns Requests, when the page crashed (while it is not loaded again)
52
+ * and what the session let go at its capture limits, or a fetch error
51
53
  */
52
- export declare function fetchNetworkRequests(withHeaders?: boolean): Promise<FetchResult<NetworkRequest[]>>;
54
+ export declare function fetchNetworkRequests(withHeaders?: boolean): Promise<FetchResult<{
55
+ requests: NetworkRequest[];
56
+ pageCrashedAt: number | undefined;
57
+ evictions: NetworkEvictionCounts;
58
+ }>>;
53
59
  /**
54
60
  * Fetch all console messages from daemon.
55
61
  *
56
- * @returns Messages (with their session-wide index) and the navigation id of
57
- * the page currently loaded
62
+ * @returns Messages (with their session-wide index), the navigation id of
63
+ * the page currently loaded, how many of the oldest messages the session
64
+ * dropped at its limit and when the page crashed (while it is not loaded
65
+ * again)
58
66
  */
59
67
  export declare function fetchConsoleMessages(): Promise<FetchResult<{
60
68
  messages: ConsoleMessage[];
61
69
  currentNavigationId: number | undefined;
70
+ dropped: number;
71
+ pageCrashedAt: number | undefined;
62
72
  }>>;
63
73
  interface ErrorResult {
64
74
  success: false;
@@ -90,19 +90,33 @@ 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, when the page crashed (while it is not loaded again)
94
+ * and what the session let go at its capture limits, 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
+ const { totals, pageCrashedAt } = result.data.output;
101
+ return {
102
+ success: true,
103
+ data: {
104
+ requests: result.data.network,
105
+ pageCrashedAt,
106
+ evictions: {
107
+ requestsDropped: totals?.networkDropped ?? 0,
108
+ bodiesEvicted: totals?.networkBodiesEvicted ?? 0,
109
+ },
110
+ },
111
+ };
100
112
  }
101
113
  /**
102
114
  * Fetch all console messages from daemon.
103
115
  *
104
- * @returns Messages (with their session-wide index) and the navigation id of
105
- * the page currently loaded
116
+ * @returns Messages (with their session-wide index), the navigation id of
117
+ * the page currently loaded, how many of the oldest messages the session
118
+ * dropped at its limit and when the page crashed (while it is not loaded
119
+ * again)
106
120
  */
107
121
  export async function fetchConsoleMessages() {
108
122
  const result = await fetchPreviewData({ lastN: 0, only: 'console' });
@@ -113,6 +127,8 @@ export async function fetchConsoleMessages() {
113
127
  data: {
114
128
  messages: result.data.console,
115
129
  currentNavigationId: result.data.output.currentNavigationId,
130
+ dropped: result.data.output.totals?.consoleDropped ?? 0,
131
+ pageCrashedAt: result.data.output.pageCrashedAt,
116
132
  },
117
133
  };
118
134
  }
@@ -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
@@ -1,6 +1,6 @@
1
1
  import { CommandError } from '../../errors/index.js';
2
2
  import { genericError } from '../../errors/messages.js';
3
- import { OutputBuilder } from '../../ui/OutputBuilder.js';
3
+ import { OutputBuilder, stringifyEnvelope } from '../../ui/OutputBuilder.js';
4
4
  import { escapeControlChars } from '../../ui/formatting.js';
5
5
  import { EXIT_CODES } from '../../utils/exitCodes.js';
6
6
  /**
@@ -24,7 +24,7 @@ export function handleValidationError(error, json) {
24
24
  if (error.metadata.suggestion) {
25
25
  errorOptions.suggestion = error.metadata.suggestion;
26
26
  }
27
- console.log(JSON.stringify(OutputBuilder.buildJsonError(error.message, errorOptions), null, 2));
27
+ console.log(stringifyEnvelope(OutputBuilder.buildJsonError(error.message, errorOptions)));
28
28
  }
29
29
  else {
30
30
  console.error(genericError(error.message));
@@ -38,7 +38,7 @@ export function handleValidationError(error, json) {
38
38
  const envelope = OutputBuilder.buildJsonError(message, {
39
39
  exitCode: EXIT_CODES.INVALID_ARGUMENTS,
40
40
  });
41
- console.log(JSON.stringify(envelope, null, 2));
41
+ console.log(stringifyEnvelope(envelope));
42
42
  }
43
43
  else {
44
44
  console.error(genericError(message));
@@ -142,10 +142,13 @@ export type DetailsCommandOptions = BaseOptions & {
142
142
  id: string;
143
143
  };
144
144
  /** Options for DOM query command */
145
- export type DomQueryCommandOptions = BaseOptions;
145
+ export type DomQueryCommandOptions = BaseOptions & {
146
+ /** Matches listed (0 = all); default 50, or 100 with --json */
147
+ limit?: number;
148
+ };
146
149
  /** Options for DOM get command */
147
150
  export type DomGetCommandOptions = BaseOptions & RawOptions & SelectionOptions & {
148
- /** All of the element's text instead of its first 500 characters (semantic output) */
151
+ /** All of the element's text (semantic output) or HTML (`--raw`), instead of its start */
149
152
  full?: boolean;
150
153
  /** Which match of the selector (0-based); `--nth` is its alias */
151
154
  index?: number;
@@ -156,6 +159,8 @@ export type DomScreenshotCommandOptions = BaseOptions & ScreenshotOptions;
156
159
  export interface DomEvalCommandOptions extends BaseOptions {
157
160
  /** Iframe to evaluate in: index, name/id attribute, or part of the name, id or URL */
158
161
  frame?: string;
162
+ /** The whole value instead of its first 20000 characters */
163
+ full?: boolean;
159
164
  }
160
165
  /** Options for DOM frames command */
161
166
  export type DomFramesCommandOptions = BaseOptions;
@@ -273,7 +278,12 @@ export interface WaitCommandOptions extends BaseOptions {
273
278
  timeout: number;
274
279
  }
275
280
  /** Options for A11y tree command */
276
- export type A11yTreeCommandOptions = BaseOptions;
281
+ export interface A11yTreeCommandOptions extends BaseOptions {
282
+ /** Nodes to list (0 = all); default 50, also with --json */
283
+ limit?: number;
284
+ /** Levels below the root to list (0 = root only) */
285
+ depth?: number;
286
+ }
277
287
  /** Options for A11y query command */
278
288
  export interface A11yQueryCommandOptions extends BaseOptions {
279
289
  /** Matches to list (0 = all) */
@@ -351,6 +361,8 @@ export interface ConsoleCommandOptions extends BaseOptions {
351
361
  history?: boolean;
352
362
  /** Filter by message level (error, warning, info, debug) */
353
363
  level?: ConsoleLevel;
364
+ /** Message texts whole instead of cut */
365
+ full?: boolean;
354
366
  }
355
367
  /**
356
368
  * Options for preview display.
@@ -377,6 +389,8 @@ export interface PeekCommandOptions extends BaseOptions, PreviewDisplayOptions {
377
389
  type?: string;
378
390
  /** Refresh interval of --follow in ms (string from CLI, default: 1000) */
379
391
  interval?: string;
392
+ /** Console message texts whole instead of cut */
393
+ full?: boolean;
380
394
  }
381
395
  /**
382
396
  * 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
@@ -14,7 +14,7 @@ import { IPCErrorCode, } from '../../ipc/index.js';
14
14
  import { IPCTimeoutError } from '../../ipc/transport/index.js';
15
15
  import { isConnectionError } from '../../ipc/utils/errors.js';
16
16
  import { getSessionName } from '../../session/paths.js';
17
- import { OutputBuilder, buildSuccessResponse } from '../../ui/OutputBuilder.js';
17
+ import { OutputBuilder, buildSuccessResponse, stringifyEnvelope } from '../../ui/OutputBuilder.js';
18
18
  import { escapeControlChars, joinLines } from '../../ui/formatting.js';
19
19
  import { createLogger } from '../../ui/logging/index.js';
20
20
  import { daemonStillExitingHint, daemonStillExitingSuggestion, startNotices, } from '../../ui/messages/session.js';
@@ -318,7 +318,7 @@ function reportStartOutcome(outcome, options) {
318
318
  exitCode: outcome.exitCode,
319
319
  ...outcome.details,
320
320
  });
321
- console.log(JSON.stringify(envelope, null, 2));
321
+ console.log(stringifyEnvelope(envelope));
322
322
  }
323
323
  else {
324
324
  console.error(escapeControlChars(outcome.human));
@@ -340,7 +340,7 @@ function reportStartOutcome(outcome, options) {
340
340
  daemonPid: data.daemonPid,
341
341
  ...(autoStopAt && { autoStopAt: autoStopAt.toISOString() }),
342
342
  };
343
- console.log(JSON.stringify(buildSuccessResponse(result), null, 2));
343
+ console.log(stringifyEnvelope(buildSuccessResponse(result)));
344
344
  }
345
345
  else {
346
346
  const page = {
@@ -84,15 +84,17 @@ export declare function parseViewport(value: string): ViewportSize;
84
84
  */
85
85
  export declare function parseColorScheme(value: string): ColorScheme;
86
86
  /**
87
- * Reject a subcommand typed without its group (`bdg query x` for
88
- * `bdg dom query x`) before Commander reads it as a start URL with extra
89
- * 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.
90
92
  *
91
93
  * @param program - Root command with all commands registered
92
94
  * @param argv - Process arguments
93
- * @throws CommandError (81) naming the full command
95
+ * @throws CommandError (81) naming the command meant
94
96
  */
95
- export declare function assertNotGroupSubcommand(program: Command, argv: string[]): void;
97
+ export declare function assertNotMistypedCommand(program: Command, argv: string[]): void;
96
98
  /**
97
99
  * Register the start command
98
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,7 +67,7 @@ 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)
@@ -180,6 +173,8 @@ function buildSessionOptions(options) {
180
173
  }
181
174
  /** Telemetry collected by every session. */
182
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;
183
178
  /** Flags users type as commands (`bdg version`). */
184
179
  const FLAG_WORDS = { version: '--version', help: '--help' };
185
180
  /**
@@ -194,29 +189,59 @@ const FLAG_WORDS = { version: '--version', help: '--help' };
194
189
  * @throws CommandError (81) for a bare word other than `localhost`
195
190
  */
196
191
  function assertNotCommandTypo(arg, commandNames) {
197
- if (!/^[a-z][a-z0-9_-]*$/i.test(arg) || arg.toLowerCase() === 'localhost')
192
+ if (!BARE_WORD.test(arg) || arg.toLowerCase() === 'localhost')
198
193
  return;
199
194
  const flag = FLAG_WORDS[arg.toLowerCase()];
200
195
  const err = unknownCommandError(arg, flag ? [flag] : findSimilar(arg, commandNames));
201
196
  throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
202
197
  }
203
198
  /**
204
- * Reject a subcommand typed without its group (`bdg query x` for
205
- * `bdg dom query x`) before Commander reads it as a start URL with extra
206
- * 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.
207
226
  *
208
227
  * @param program - Root command with all commands registered
209
228
  * @param argv - Process arguments
210
- * @throws CommandError (81) naming the full command
229
+ * @throws CommandError (81) naming the command meant
211
230
  */
212
- export function assertNotGroupSubcommand(program, argv) {
213
- const [first] = argv.slice(2).filter((arg) => !arg.startsWith('-'));
214
- 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))
215
235
  return;
216
236
  const group = program.commands.find((command) => command.commands.some((sub) => sub.name() === first));
217
- 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)
218
243
  return;
219
- const err = unknownCommandError(first, [`${group.name()} ${first}`]);
244
+ const err = unknownCommandError(first, similar);
220
245
  throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
221
246
  }
222
247
  /**
@@ -247,7 +272,10 @@ function validateStartInput(url, options, program) {
247
272
  ...(process.env['BDG_CHROME_FLAGS']?.split(' ') ?? []),
248
273
  ...(options.chromeFlags?.split(' ') ?? []),
249
274
  ]);
250
- return { url, sessionOptions: buildSessionOptions(options) };
275
+ const sessionOptions = buildSessionOptions(options);
276
+ if (sessionOptions.userDataDir !== undefined)
277
+ assertUsableProfile(sessionOptions.userDataDir);
278
+ return { url, sessionOptions };
251
279
  }
252
280
  /**
253
281
  * Register the start command
@@ -346,6 +374,22 @@ function assertUserDataDir(value) {
346
374
  const err = invalidUserDataDirError(value, reason);
347
375
  throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
348
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
+ }
349
393
  /**
350
394
  * Options given that only apply to a Chrome bdg launches. `--headless` has a
351
395
  * default, so it counts only when given on the command line.
@@ -1,4 +1,15 @@
1
1
  import type { Command } from 'commander';
2
+ /**
3
+ * Wait until the stopped session's daemon has exited: it removes the
4
+ * session's files on the way out, and a command run right after (`bdg
5
+ * sessions`, a new start) would otherwise still see the session. The wait is
6
+ * bounded, as after a failed start.
7
+ *
8
+ * @param pid - Daemon PID read before the stop, or null when unknown
9
+ * @param waitMs - Milliseconds to wait at most
10
+ * @returns Warning when the daemon still runs after the wait
11
+ */
12
+ export declare function waitForDaemonExit(pid: number | null, waitMs?: number): Promise<string | undefined>;
2
13
  /**
3
14
  * Register stop command
4
15
  *
@@ -3,13 +3,33 @@ import { jsonOption } from './shared/commonOptions.js';
3
3
  import { stopSession } from '../ipc/client.js';
4
4
  import { IPCErrorCode } from '../ipc/index.js';
5
5
  import { IPCTimeoutError } from '../ipc/transport/index.js';
6
+ import { readLiveDaemonPid } from '../session/cleanup/staleSession.js';
6
7
  import { joinLines } from '../ui/formatting.js';
7
8
  import { chromeClosedMessage, orphanedDaemonsCleanedMessage, warningMessage, } from '../ui/messages/commands.js';
8
- import { sessionStopped, STOP_MESSAGES, stopFailedError } from '../ui/messages/session.js';
9
+ import { daemonStillExitingHint, daemonStillExitingSuggestion, sessionStopped, STOP_MESSAGES, stopFailedError, } from '../ui/messages/session.js';
9
10
  import { noActiveSessionMessage, sessionCommand, startSessionSuggestion, } from '../ui/messages/sessionCommand.js';
11
+ import { waitUntil } from '../utils/async.js';
10
12
  import { getExitCodeForIPCError, isDaemonNotRunningError } from '../utils/errorMapping.js';
11
13
  import { getErrorMessage } from '../utils/errors.js';
12
14
  import { EXIT_CODES } from '../utils/exitCodes.js';
15
+ import { isProcessAlive } from '../utils/process.js';
16
+ /** How long `bdg stop` waits for the session's daemon to exit */
17
+ const DAEMON_EXIT_WAIT_MS = 3000;
18
+ /**
19
+ * Wait until the stopped session's daemon has exited: it removes the
20
+ * session's files on the way out, and a command run right after (`bdg
21
+ * sessions`, a new start) would otherwise still see the session. The wait is
22
+ * bounded, as after a failed start.
23
+ *
24
+ * @param pid - Daemon PID read before the stop, or null when unknown
25
+ * @param waitMs - Milliseconds to wait at most
26
+ * @returns Warning when the daemon still runs after the wait
27
+ */
28
+ export async function waitForDaemonExit(pid, waitMs = DAEMON_EXIT_WAIT_MS) {
29
+ if (pid === null || (await waitUntil(() => !isProcessAlive(pid), waitMs)))
30
+ return undefined;
31
+ return `${daemonStillExitingHint(pid, waitMs)}; ${daemonStillExitingSuggestion()}`;
32
+ }
13
33
  /**
14
34
  * Format stop result for human-readable output.
15
35
  *
@@ -36,8 +56,10 @@ export function registerStopCommand(program) {
36
56
  .action(async (options) => {
37
57
  await runCommand(async () => {
38
58
  try {
59
+ const daemonPid = readLiveDaemonPid();
39
60
  const response = await stopSession();
40
61
  if (response.status === 'ok') {
62
+ const warning = await waitForDaemonExit(daemonPid);
41
63
  return {
42
64
  success: true,
43
65
  data: {
@@ -48,6 +70,7 @@ export function registerStopCommand(program) {
48
70
  },
49
71
  orphanedDaemonsCount: 0,
50
72
  message: response.message ?? STOP_MESSAGES.SUCCESS,
73
+ ...(warning && { warnings: [warning] }),
51
74
  },
52
75
  };
53
76
  }
package/dist/commands.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { registerCdpCommand } from './commands/cdp.js';
2
2
  import { registerCleanupCommand } from './commands/cleanup.js';
3
3
  import { registerConsoleCommand } from './commands/console.js';
4
+ import { registerCssCommands } from './commands/css.js';
4
5
  import { registerDetailsCommand } from './commands/details.js';
5
6
  import { registerFormInteractionCommands } from './commands/dom/formInteraction.js';
6
7
  import { registerDomCommands } from './commands/dom/index.js';
7
8
  import { registerInstallSkillCommand } from './commands/installSkill.js';
8
9
  import { registerNetworkCommands } from './commands/network/index.js';
9
- import { registerCssCommands } from './commands/css.js';
10
10
  import { registerPageCommands } from './commands/page.js';
11
11
  import { registerPeekCommand } from './commands/peek.js';
12
12
  import { registerSessionsCommand } from './commands/sessions.js';
@@ -181,6 +181,13 @@ export declare class CDPConnection implements CDPEventSource {
181
181
  * @returns Calculated delay in milliseconds (base delay * 2^attempt, capped at maxDelay)
182
182
  */
183
183
  private calculateBackoffDelay;
184
+ /**
185
+ * Fail every command still waiting for an answer, e.g. after the page's
186
+ * renderer crashed: commands it was running are never answered.
187
+ *
188
+ * @param error - Error the waiting commands fail with
189
+ */
190
+ rejectPending(error: Error): void;
184
191
  /**
185
192
  * Clear all pending command promises with the given error.
186
193
  *
@@ -390,6 +390,15 @@ export class CDPConnection {
390
390
  calculateBackoffDelay(attempt, maxDelay) {
391
391
  return Math.min(this.config.baseRetryDelay * Math.pow(2, attempt), maxDelay);
392
392
  }
393
+ /**
394
+ * Fail every command still waiting for an answer, e.g. after the page's
395
+ * renderer crashed: commands it was running are never answered.
396
+ *
397
+ * @param error - Error the waiting commands fail with
398
+ */
399
+ rejectPending(error) {
400
+ this.clearPendingMessages(error);
401
+ }
393
402
  /**
394
403
  * Clear all pending command promises with the given error.
395
404
  *