browser-debugger-cli 0.14.0 → 0.16.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 (167) hide show
  1. package/.claude/skills/bdg/SKILL.md +3 -2
  2. package/dist/cdp/methodTarget.d.ts +92 -0
  3. package/dist/cdp/methodTarget.js +159 -0
  4. package/dist/cdp/protocol.d.ts +16 -1
  5. package/dist/cdp/protocol.js +21 -0
  6. package/dist/cdp/schema.d.ts +55 -1
  7. package/dist/cdp/schema.js +134 -25
  8. package/dist/cdp/types.d.ts +3 -1
  9. package/dist/commands/cdp.d.ts +38 -1
  10. package/dist/commands/cdp.js +201 -133
  11. package/dist/commands/cleanup.js +21 -4
  12. package/dist/commands/dom/eval.d.ts +2 -1
  13. package/dist/commands/dom/eval.js +6 -21
  14. package/dist/commands/dom/formInteraction.js +8 -4
  15. package/dist/commands/dom/helpers/evalResult.d.ts +36 -0
  16. package/dist/commands/dom/helpers/evalResult.js +59 -0
  17. package/dist/commands/dom/helpers/index.d.ts +4 -4
  18. package/dist/commands/dom/helpers/index.js +3 -3
  19. package/dist/commands/dom/helpers/query.d.ts +2 -2
  20. package/dist/commands/dom/helpers/query.js +2 -2
  21. package/dist/commands/dom/helpers/screenshot.d.ts +21 -26
  22. package/dist/commands/dom/helpers/screenshot.js +50 -668
  23. package/dist/commands/dom/screenshot.js +56 -36
  24. package/dist/commands/helpJson.d.ts +1 -1
  25. package/dist/commands/helpJson.js +3 -3
  26. package/dist/commands/helpTopic.js +10 -4
  27. package/dist/commands/network/har.js +18 -14
  28. package/dist/commands/optionBehaviors.js +24 -9
  29. package/dist/commands/shared/CommandRunner.d.ts +5 -0
  30. package/dist/commands/shared/CommandRunner.js +18 -3
  31. package/dist/commands/shared/interrupt.d.ts +40 -0
  32. package/dist/commands/shared/interrupt.js +73 -0
  33. package/dist/commands/shared/optionTypes.d.ts +3 -0
  34. package/dist/commands/shared/outputFile.d.ts +2 -1
  35. package/dist/commands/shared/outputFile.js +7 -4
  36. package/dist/commands/shared/startHelpers.d.ts +26 -3
  37. package/dist/commands/shared/startHelpers.js +145 -23
  38. package/dist/commands/status.js +3 -1
  39. package/dist/commands/stop.js +2 -1
  40. package/dist/commands/types.d.ts +5 -0
  41. package/dist/connection/cdp.js +1 -16
  42. package/dist/connection/chromeIdentity.d.ts +24 -5
  43. package/dist/connection/chromeIdentity.js +53 -22
  44. package/dist/connection/launcher/flagsBuilder.d.ts +46 -0
  45. package/dist/connection/launcher/flagsBuilder.js +107 -23
  46. package/dist/connection/launcher.d.ts +35 -2
  47. package/dist/connection/launcher.js +99 -12
  48. package/dist/connection/typed-cdp.d.ts +3 -2
  49. package/dist/constants.d.ts +3 -5
  50. package/dist/constants.js +3 -5
  51. package/dist/daemon/SessionController.d.ts +10 -5
  52. package/dist/daemon/SessionController.js +15 -8
  53. package/dist/daemon/ipcServer.js +1 -1
  54. package/dist/daemon/launcher.d.ts +22 -3
  55. package/dist/daemon/launcher.js +45 -8
  56. package/dist/daemon/session/Session.d.ts +5 -1
  57. package/dist/daemon/session/Session.js +9 -8
  58. package/dist/daemon/session/TelemetryStore.d.ts +5 -0
  59. package/dist/daemon/session/TelemetryStore.js +4 -0
  60. package/dist/daemon/session/captureGate.d.ts +59 -0
  61. package/dist/daemon/session/captureGate.js +96 -0
  62. package/dist/daemon/session/chromeConnection.d.ts +16 -1
  63. package/dist/daemon/session/chromeConnection.js +34 -4
  64. package/dist/daemon/session/collectors.d.ts +15 -0
  65. package/dist/daemon/session/collectors.js +39 -2
  66. package/dist/daemon/session/commandRegistry.d.ts +14 -1
  67. package/dist/daemon/session/commandRegistry.js +48 -13
  68. package/dist/daemon/session/downloads.d.ts +32 -0
  69. package/dist/daemon/session/downloads.js +96 -0
  70. package/dist/daemon/session/interactions.d.ts +3 -2
  71. package/dist/daemon/session/interactions.js +7 -2
  72. package/dist/daemon/session/plugins.js +6 -0
  73. package/dist/daemon.js +18520 -17014
  74. package/dist/errors/CommandError.d.ts +2 -0
  75. package/dist/errors/issues.d.ts +1 -1
  76. package/dist/errors/messages.d.ts +81 -0
  77. package/dist/errors/messages.js +198 -6
  78. package/dist/index.js +1446 -1078
  79. package/dist/ipc/client.d.ts +20 -2
  80. package/dist/ipc/client.js +32 -6
  81. package/dist/ipc/protocol/commands.d.ts +36 -2
  82. package/dist/ipc/protocol/commands.js +1 -0
  83. package/dist/ipc/protocol/domTypes.d.ts +24 -1
  84. package/dist/ipc/session/queries.d.ts +3 -0
  85. package/dist/ipc/session/types.d.ts +5 -0
  86. package/dist/ipc/transport/IPCError.d.ts +9 -0
  87. package/dist/ipc/transport/IPCError.js +12 -0
  88. package/dist/ipc/transport/errors.d.ts +2 -1
  89. package/dist/ipc/transport/errors.js +4 -1
  90. package/dist/ipc/transport/index.d.ts +10 -2
  91. package/dist/ipc/transport/index.js +29 -4
  92. package/dist/runtime/dom/actionEffects.d.ts +48 -9
  93. package/dist/runtime/dom/actionEffects.js +269 -34
  94. package/dist/runtime/dom/actionEffectsScripts.d.ts +45 -0
  95. package/dist/runtime/dom/actionEffectsScripts.js +101 -2
  96. package/dist/runtime/dom/captureArea.d.ts +35 -0
  97. package/dist/runtime/dom/captureArea.js +203 -0
  98. package/dist/runtime/dom/elementInfo.d.ts +13 -4
  99. package/dist/runtime/dom/elementInfo.js +12 -3
  100. package/dist/runtime/dom/evalHelpers.d.ts +24 -4
  101. package/dist/runtime/dom/evalHelpers.js +40 -12
  102. package/dist/runtime/dom/formDiscovery.d.ts +1 -1
  103. package/dist/runtime/dom/frames.d.ts +2 -1
  104. package/dist/runtime/dom/frames.js +3 -1
  105. package/dist/runtime/page/bdgWorld.d.ts +9 -0
  106. package/dist/runtime/page/bdgWorld.js +11 -0
  107. package/dist/runtime/page/captureEmulation.d.ts +119 -0
  108. package/dist/runtime/page/captureEmulation.js +189 -0
  109. package/dist/runtime/page/captureScroll.d.ts +24 -0
  110. package/dist/runtime/page/captureScroll.js +124 -0
  111. package/dist/runtime/page/emulation.js +6 -5
  112. package/dist/runtime/page/screenshot.d.ts +41 -0
  113. package/dist/runtime/page/screenshot.js +394 -0
  114. package/dist/runtime/page/userAgent.d.ts +86 -2
  115. package/dist/runtime/page/userAgent.js +154 -33
  116. package/dist/session/paths.d.ts +52 -3
  117. package/dist/session/paths.js +179 -7
  118. package/dist/session/portClaims.d.ts +0 -8
  119. package/dist/session/portClaims.js +1 -22
  120. package/dist/session/sessionList.d.ts +5 -1
  121. package/dist/session/sessionList.js +5 -1
  122. package/dist/telemetry/downloads.d.ts +127 -0
  123. package/dist/telemetry/downloads.js +265 -0
  124. package/dist/telemetry/har/builder.d.ts +12 -1
  125. package/dist/telemetry/har/builder.js +32 -9
  126. package/dist/telemetry/har/sanitize.d.ts +28 -0
  127. package/dist/telemetry/har/sanitize.js +184 -0
  128. package/dist/telemetry/har/sanitizeBody.d.ts +78 -0
  129. package/dist/telemetry/har/sanitizeBody.js +541 -0
  130. package/dist/telemetry/har/types.d.ts +2 -0
  131. package/dist/telemetry/network.d.ts +4 -4
  132. package/dist/telemetry/network.js +38 -4
  133. package/dist/telemetry/networkRetention.d.ts +35 -14
  134. package/dist/telemetry/networkRetention.js +62 -26
  135. package/dist/types.d.ts +9 -14
  136. package/dist/ui/OutputBuilder.d.ts +3 -2
  137. package/dist/ui/OutputBuilder.js +4 -3
  138. package/dist/ui/formatters/cdp.d.ts +32 -9
  139. package/dist/ui/formatters/cdp.js +77 -6
  140. package/dist/ui/formatters/details.js +7 -15
  141. package/dist/ui/formatters/preview.d.ts +2 -0
  142. package/dist/ui/formatters/preview.js +7 -1
  143. package/dist/ui/formatters/sessions.d.ts +3 -2
  144. package/dist/ui/formatters/sessions.js +10 -3
  145. package/dist/ui/formatters/status.js +6 -1
  146. package/dist/ui/formatting.d.ts +7 -0
  147. package/dist/ui/formatting.js +13 -0
  148. package/dist/ui/logging/logger.d.ts +1 -1
  149. package/dist/ui/messages/chrome.d.ts +27 -6
  150. package/dist/ui/messages/chrome.js +78 -12
  151. package/dist/ui/messages/commands.d.ts +71 -3
  152. package/dist/ui/messages/commands.js +98 -3
  153. package/dist/ui/messages/networkMessages.d.ts +50 -5
  154. package/dist/ui/messages/networkMessages.js +50 -6
  155. package/dist/ui/messages/session.d.ts +8 -0
  156. package/dist/ui/messages/session.js +10 -0
  157. package/dist/utils/async.d.ts +3 -2
  158. package/dist/utils/async.js +16 -3
  159. package/dist/utils/atomicFile.d.ts +2 -1
  160. package/dist/utils/atomicFile.js +5 -2
  161. package/dist/utils/directories.d.ts +41 -0
  162. package/dist/utils/directories.js +48 -0
  163. package/dist/utils/http.d.ts +11 -4
  164. package/dist/utils/http.js +5 -3
  165. package/package.json +18 -4
  166. /package/dist/{commands/dom → runtime/page}/screenshotResize.d.ts +0 -0
  167. /package/dist/{commands/dom → runtime/page}/screenshotResize.js +0 -0
@@ -1,7 +1,7 @@
1
1
  import { type Command } from 'commander';
2
2
  import { type CommandResult } from './shared/CommandRunner.js';
3
3
  import type { CdpCommandOptions } from './shared/optionTypes.js';
4
- import { type CdpExecuteData } from '../ui/formatters/cdp.js';
4
+ import { type CdpDomainDescription, type CdpExecuteData, type CdpMethodDescription, type CdpTypeDescription } from '../ui/formatters/cdp.js';
5
5
  /**
6
6
  * Register CDP command with full introspection support.
7
7
  *
@@ -35,4 +35,41 @@ export declare function isBareDomain(method: string, options: CdpCommandOptions)
35
35
  * @returns Error result, or undefined when the result has no exception
36
36
  */
37
37
  export declare function pageExceptionResult(result: unknown): CommandResult<CdpExecuteData> | undefined;
38
+ /** A description `bdg cdp <name> --describe` gives */
39
+ type CdpDescription = CdpMethodDescription | CdpDomainDescription | CdpTypeDescription;
40
+ /**
41
+ * Handle describe mode: show a domain, a method's signature and parameters,
42
+ * or a protocol type's values or properties.
43
+ *
44
+ * @param name - Domain, `Domain.method` or `Domain.Type` (case-insensitive)
45
+ * @returns Success result with the description
46
+ */
47
+ export declare function handleDescribeMethod(name: string): CommandResult<CdpDescription>;
48
+ /**
49
+ * The method `bdg cdp <name>` sends: a bundled method with its casing, or a
50
+ * well-formed method the bundled protocol lacks, as typed with a warning.
51
+ *
52
+ * @param methodName - Method name as typed
53
+ * @param options - `sendAnyway` sends a close typo of bundled methods as typed
54
+ * @returns Method to send, and the warning for one the bundled protocol lacks
55
+ * @throws CommandError (exit 81) for a blocked method, a type, a close typo
56
+ * of bundled methods (without `sendAnyway`) or a name that is not `Domain.method`
57
+ */
58
+ export declare function methodToSend(methodName: string, options?: Pick<CdpCommandOptions, 'sendAnyway'>): {
59
+ method: string;
60
+ warning?: string;
61
+ };
62
+ /**
63
+ * The result of a CDP call: the page exception it reported (exit 91), or its
64
+ * result with the session's and the method's hints; either way with the
65
+ * warning for a method the bundled protocol lacks.
66
+ *
67
+ * @param method - Method called
68
+ * @param cdpResult - What Chrome returned
69
+ * @param warning - Warning for a method the bundled protocol lacks
70
+ * @param ipcHint - Hint the session gave (e.g. a repeated-call pattern)
71
+ * @returns Command result
72
+ */
73
+ export declare function cdpCallResult(method: string, cdpResult: unknown, warning?: string, ipcHint?: string): CommandResult<CdpExecuteData>;
74
+ export {};
38
75
  //# sourceMappingURL=cdp.d.ts.map
@@ -1,14 +1,16 @@
1
1
  import { Option } from 'commander';
2
- import { normalizeMethod } from '../cdp/protocol.js';
3
- import { getAllDomainSummaries, getDomainMethods, getProtocolCounts, getDomainSummary, getMethodSchema, } from '../cdp/schema.js';
2
+ import { resolveMethodTarget } from '../cdp/methodTarget.js';
3
+ import { getBundledProtocolVersion } from '../cdp/protocol.js';
4
+ import { getAllDomainSummaries, getDomainMethods, getProtocolCounts, getDomainSummary, getMethodSchema, getTypeSchema, } from '../cdp/schema.js';
4
5
  import { runCommand } from './shared/CommandRunner.js';
5
6
  import { jsonOption } from './shared/commonOptions.js';
6
7
  import { CommandError } from '../errors/index.js';
7
- import { emptyCdpSearchError, missingArgumentError, scriptExecutionError, } from '../errors/messages.js';
8
+ import { cdpMethodNotFoundError, cdpMethodNotInBundledProtocolError, cdpMethodTypoError, cdpTypeNotMethodError, emptyCdpSearchError, missingArgumentError, scriptExecutionError, } from '../errors/messages.js';
8
9
  import { callCDP } from '../ipc/client.js';
9
10
  import { validateIPCResponse } from '../ipc/index.js';
10
11
  import { describeException } from '../runtime/dom/evalHelpers.js';
11
12
  import { formatCdpDescription, formatCdpDomainMethods, formatCdpDomains, formatCdpResult, formatCdpSearch, isEmptyCdpResult, } from '../ui/formatters/cdp.js';
13
+ import { CDP_EXECUTION_HELP, cdpUnlistedMethodWarning } from '../ui/messages/commands.js';
12
14
  import { formatHint } from '../ui/messages/hints.js';
13
15
  import { sessionCommand } from '../ui/messages/sessionCommand.js';
14
16
  import { getErrorMessage } from '../utils/errors.js';
@@ -80,9 +82,10 @@ function getMethodHint(methodName, result) {
80
82
  export function registerCdpCommand(program) {
81
83
  program
82
84
  .command('cdp')
85
+ .summary('CDP protocol introspection and execution')
83
86
  .description('CDP protocol introspection and execution\n' +
84
87
  ' Discovery: --list, --search, --describe\n' +
85
- ' Execution: case-insensitive (network.getcookies works)')
88
+ CDP_EXECUTION_HELP)
86
89
  .argument('[method]', 'CDP method name (e.g., Network.getCookies, network.getcookies)')
87
90
  .addOption(new Option('--params <json>', 'Method parameters as JSON'))
88
91
  .addOption(new Option('--list', 'List all domains or methods in a domain').conflicts([
@@ -90,6 +93,7 @@ export function registerCdpCommand(program) {
90
93
  'params',
91
94
  ]))
92
95
  .addOption(new Option('--describe', 'Show method signature and parameters').conflicts('params'))
96
+ .addOption(new Option('--send-anyway', 'Send a Domain.method that looks like a typo of a bundled one as typed').conflicts(['list', 'describe', 'search']))
93
97
  .addOption(new Option('--search <query>', 'Search methods by keyword').conflicts([
94
98
  'list',
95
99
  'describe',
@@ -122,7 +126,7 @@ async function runCdpCommand(method, options) {
122
126
  return runCommand(async () => handleDescribeMethod(method), options, formatCdpDescription);
123
127
  }
124
128
  if (method) {
125
- return runCommand(async () => handleExecuteMethod(method, options.params), options, formatCdpResult);
129
+ return runCommand(async () => handleExecuteMethod(method, options), options, formatCdpResult);
126
130
  }
127
131
  return runCommand(async () => {
128
132
  const err = missingArgumentError(CDP_USAGE);
@@ -330,99 +334,116 @@ function handleListDomainMethods(domainName) {
330
334
  };
331
335
  }
332
336
  /**
333
- * Handle describe method mode: Show method signature and parameters.
337
+ * Handle describe mode: show a domain, a method's signature and parameters,
338
+ * or a protocol type's values or properties.
334
339
  *
335
- * @param methodName - Method name (case-insensitive, with or without domain)
336
- * @returns Success result with method schema
340
+ * @param name - Domain, `Domain.method` or `Domain.Type` (case-insensitive)
341
+ * @returns Success result with the description
337
342
  */
338
- function handleDescribeMethod(methodName) {
339
- const [domainName, method] = methodName.includes('.')
340
- ? methodName.split('.')
341
- : [methodName, undefined];
342
- if (!method) {
343
- const summary = getDomainSummary(domainName);
344
- if (!summary) {
345
- const similar = findSimilarMethods(methodName);
346
- const suggestions = ['Use: bdg cdp --list (to see all domains)'];
347
- if (similar.length > 0) {
348
- suggestions.push('');
349
- suggestions.push('Did you mean:');
350
- similar.forEach((name) => suggestions.push(` - ${name}`));
351
- }
352
- return {
353
- success: false,
354
- error: `Domain or method '${methodName}' not found`,
355
- exitCode: EXIT_CODES.INVALID_ARGUMENTS,
356
- errorContext: {
357
- suggestion: suggestions.join('\n'),
358
- },
359
- };
360
- }
361
- const domainNote = DOMAIN_NOTES[summary.name];
362
- return {
363
- success: true,
364
- data: {
365
- type: 'domain',
366
- domain: summary.name,
367
- description: summary.description,
368
- commands: summary.commandCount,
369
- events: summary.eventCount,
370
- experimental: summary.experimental,
371
- deprecated: summary.deprecated,
372
- note: domainNote,
373
- nextStep: `Use: bdg cdp ${summary.name} --list (to see all methods)`,
374
- },
375
- };
376
- }
377
- const schema = getMethodSchema(domainName, method);
378
- if (!schema) {
379
- const similar = findSimilarMethods(methodName, domainName);
380
- const suggestions = [`Use: bdg cdp ${domainName} --list (to see all ${domainName} methods)`];
381
- if (similar.length > 0) {
382
- suggestions.push('');
383
- suggestions.push('Did you mean:');
384
- similar.forEach((name) => suggestions.push(` - ${name}`));
385
- }
343
+ export function handleDescribeMethod(name) {
344
+ const [domainName = '', member] = name.includes('.') ? name.split('.') : [name, undefined];
345
+ if (!member)
346
+ return describeDomain(name);
347
+ const schema = getMethodSchema(domainName, member);
348
+ if (schema)
349
+ return { success: true, data: describeMethod(schema) };
350
+ const type = getTypeSchema(domainName, member);
351
+ if (type)
352
+ return { success: true, data: { type: 'type', ...type } };
353
+ const target = resolveMethodTarget(name);
354
+ const err = target.kind === 'unlisted'
355
+ ? cdpMethodNotInBundledProtocolError(target.method, getBundledProtocolVersion())
356
+ : cdpMethodNotFoundError(name, findSimilarMethods(name, domainName), `Use: bdg cdp ${domainName} --list (to see all ${domainName} methods)`);
357
+ return {
358
+ success: false,
359
+ error: err.message,
360
+ exitCode: EXIT_CODES.INVALID_ARGUMENTS,
361
+ errorContext: { suggestion: err.suggestion },
362
+ };
363
+ }
364
+ /**
365
+ * Describe a domain (`bdg cdp Network --describe`).
366
+ *
367
+ * @param domainName - Domain name (case-insensitive)
368
+ * @returns Domain description, or not found with similar names
369
+ */
370
+ function describeDomain(domainName) {
371
+ const summary = getDomainSummary(domainName);
372
+ if (!summary) {
373
+ const err = cdpMethodNotFoundError(domainName, findSimilarMethods(domainName), 'Use: bdg cdp --list (to see all domains)');
386
374
  return {
387
375
  success: false,
388
- error: `Method '${methodName}' not found`,
376
+ error: `Domain or method '${domainName}' not found`,
389
377
  exitCode: EXIT_CODES.INVALID_ARGUMENTS,
390
- errorContext: {
391
- suggestion: suggestions.join('\n'),
392
- },
378
+ errorContext: { suggestion: err.suggestion },
393
379
  };
394
380
  }
395
- const methodNote = METHOD_NOTES[schema.name] ?? DOMAIN_NOTES[schema.domain];
396
- const alternative = blockedAlternative(schema.name);
397
381
  return {
398
382
  success: true,
399
383
  data: {
400
- type: 'method',
401
- name: schema.name,
402
- domain: schema.domain,
403
- method: schema.method,
404
- description: schema.description,
405
- experimental: schema.experimental,
406
- deprecated: schema.deprecated,
407
- note: methodNote,
408
- parameters: schema.parameters.map((p) => ({
409
- name: p.name,
410
- type: p.type,
411
- required: p.required,
412
- description: p.description,
413
- enum: p.enum,
414
- items: p.items,
415
- deprecated: p.deprecated,
416
- })),
417
- returns: schema.returns.map((r) => ({
418
- name: r.name,
419
- type: r.type,
420
- optional: r.optional,
421
- description: r.description,
422
- items: r.items,
423
- })),
424
- example: alternative ? { command: alternative } : schema.example,
384
+ type: 'domain',
385
+ domain: summary.name,
386
+ description: summary.description,
387
+ commands: summary.commandCount,
388
+ events: summary.eventCount,
389
+ experimental: summary.experimental,
390
+ deprecated: summary.deprecated,
391
+ note: DOMAIN_NOTES[summary.name],
392
+ nextStep: `Use: bdg cdp ${summary.name} --list (to see all methods)`,
393
+ },
394
+ };
395
+ }
396
+ /**
397
+ * A parameter as `--describe` shows it.
398
+ *
399
+ * @param p - Parameter schema
400
+ * @returns Described parameter
401
+ */
402
+ function describeParameter(p) {
403
+ return {
404
+ name: p.name,
405
+ type: p.type,
406
+ required: p.required,
407
+ description: p.description,
408
+ enum: p.enum,
409
+ ref: p.ref,
410
+ refType: p.refType,
411
+ items: p.items,
412
+ experimental: p.experimental,
413
+ deprecated: p.deprecated,
414
+ };
415
+ }
416
+ /**
417
+ * Describe a method (`bdg cdp Network.getCookies --describe`).
418
+ *
419
+ * @param schema - Method schema
420
+ * @returns Method description with its redirect, note and example
421
+ */
422
+ function describeMethod(schema) {
423
+ const alternative = blockedAlternative(schema.name);
424
+ return {
425
+ type: 'method',
426
+ name: schema.name,
427
+ domain: schema.domain,
428
+ method: schema.method,
429
+ description: schema.description,
430
+ experimental: schema.experimental,
431
+ deprecated: schema.deprecated,
432
+ note: METHOD_NOTES[schema.name] ?? DOMAIN_NOTES[schema.domain],
433
+ parameters: schema.parameters.map(describeParameter),
434
+ returns: schema.returns.map((r) => ({
435
+ name: r.name,
436
+ type: r.type,
437
+ optional: r.optional,
438
+ description: r.description,
439
+ items: r.items,
440
+ })),
441
+ redirect: schema.redirect && {
442
+ method: schema.redirect.method,
443
+ resolved: schema.redirect.resolved,
444
+ parameters: schema.redirect.parameters.map(describeParameter),
425
445
  },
446
+ example: alternative ? { command: alternative } : schema.example,
426
447
  };
427
448
  }
428
449
  /**
@@ -444,44 +465,78 @@ const BLOCKED_CDP_METHODS = {
444
465
  },
445
466
  };
446
467
  /**
447
- * Handle execute method mode: Call CDP method.
468
+ * The method `bdg cdp <name>` sends: a bundled method with its casing, or a
469
+ * well-formed method the bundled protocol lacks, as typed with a warning.
448
470
  *
449
- * @param methodName - Method name (case-insensitive)
450
- * @param paramsJson - Parameters as JSON string
451
- * @returns Success result with method response
471
+ * @param methodName - Method name as typed
472
+ * @param options - `sendAnyway` sends a close typo of bundled methods as typed
473
+ * @returns Method to send, and the warning for one the bundled protocol lacks
474
+ * @throws CommandError (exit 81) for a blocked method, a type, a close typo
475
+ * of bundled methods (without `sendAnyway`) or a name that is not `Domain.method`
452
476
  */
453
- async function handleExecuteMethod(methodName, paramsJson) {
454
- const normalized = normalizeMethod(methodName);
455
- if (normalized && BLOCKED_CDP_METHODS[normalized]) {
456
- const blocked = BLOCKED_CDP_METHODS[normalized];
457
- return {
458
- success: false,
459
- error: `${normalized} is blocked via raw CDP: ${blocked.reason}`,
460
- exitCode: EXIT_CODES.INVALID_ARGUMENTS,
461
- errorContext: { suggestion: `Use: ${sessionCommand(blocked.alternative)}` },
462
- };
463
- }
464
- if (!normalized) {
465
- const similar = findSimilarMethods(methodName);
466
- const suggestions = ['Use: bdg cdp --search <keyword> (to search for methods)'];
467
- if (similar.length > 0) {
468
- suggestions.push('');
469
- suggestions.push('Did you mean:');
470
- similar.forEach((name) => suggestions.push(` - ${name}`));
477
+ export function methodToSend(methodName, options = {}) {
478
+ const target = resolveMethodTarget(methodName);
479
+ if (target.kind === 'known') {
480
+ const blocked = BLOCKED_CDP_METHODS[target.method];
481
+ if (blocked) {
482
+ throw new CommandError(`${target.method} is blocked via raw CDP: ${blocked.reason}`, { suggestion: `Use: ${sessionCommand(blocked.alternative)}` }, EXIT_CODES.INVALID_ARGUMENTS);
471
483
  }
484
+ return { method: target.method };
485
+ }
486
+ if (target.kind === 'unlisted' || (target.kind === 'typo' && options.sendAnyway)) {
472
487
  return {
473
- success: false,
474
- error: `Method '${methodName}' not found`,
475
- exitCode: EXIT_CODES.INVALID_ARGUMENTS,
476
- errorContext: {
477
- suggestion: suggestions.join('\n'),
478
- },
488
+ method: target.method,
489
+ warning: cdpUnlistedMethodWarning(target.method, getBundledProtocolVersion()),
479
490
  };
480
491
  }
492
+ const err = target.kind === 'type'
493
+ ? cdpTypeNotMethodError(target.name)
494
+ : unknownMethodError(methodName, target);
495
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
496
+ }
497
+ /**
498
+ * Not found, for a close typo of bundled methods or domains, or a name that
499
+ * is not `Domain.method`.
500
+ *
501
+ * @param methodName - Method name as typed
502
+ * @param target - Typo with its suggestions, or malformed
503
+ * @returns Message and suggestion
504
+ */
505
+ function unknownMethodError(methodName, target) {
506
+ if (target.kind === 'malformed') {
507
+ return cdpMethodNotFoundError(methodName, findSimilarMethods(methodName), 'Use: bdg cdp --search <keyword> (to search for methods)');
508
+ }
509
+ const [domainName = ''] = methodName.split('.');
510
+ return target.suggestions.length > 0
511
+ ? cdpMethodTypoError(methodName, target.suggestions.slice(0, 3), 'Use: bdg cdp --search <keyword> (to search for methods)')
512
+ : cdpMethodTypoError(methodName, [], domainSuggestion(domainName));
513
+ }
514
+ /**
515
+ * Add the warning about a method missing from the bundled protocol to the
516
+ * error Chrome gave for it, so a failed call still says it was sent as typed.
517
+ *
518
+ * @param error - Error of the call
519
+ * @param warning - Warning, when the method was not in the bundled protocol
520
+ * @returns The error, with the warning (top-level JSON `warning`) when there is one
521
+ */
522
+ function withWarning(error, warning) {
523
+ if (!warning || !(error instanceof CommandError))
524
+ return error;
525
+ return new CommandError(error.message, { ...error.metadata, warning }, error.exitCode);
526
+ }
527
+ /**
528
+ * Handle execute method mode: Call CDP method.
529
+ *
530
+ * @param methodName - Method name (case-insensitive for bundled methods)
531
+ * @param paramsJson - Parameters as JSON string
532
+ * @returns Success result with method response
533
+ */
534
+ async function handleExecuteMethod(methodName, options) {
535
+ const { method: normalized, warning } = methodToSend(methodName, options);
481
536
  let params;
482
- if (paramsJson) {
537
+ if (options.params) {
483
538
  try {
484
- params = JSON.parse(paramsJson);
539
+ params = JSON.parse(options.params);
485
540
  }
486
541
  catch (error) {
487
542
  return {
@@ -491,29 +546,42 @@ async function handleExecuteMethod(methodName, paramsJson) {
491
546
  errorContext: {
492
547
  suggestion: `Use: bdg cdp ${normalized} --describe (to see parameter schema)`,
493
548
  },
549
+ ...(warning && { warning }),
494
550
  };
495
551
  }
496
552
  }
497
553
  const response = await callCDP(normalized, params);
498
- validateIPCResponse(response);
499
- const cdpResult = response.data?.result;
554
+ try {
555
+ validateIPCResponse(response);
556
+ }
557
+ catch (error) {
558
+ throw withWarning(error, warning);
559
+ }
560
+ const ipcHint = response.data?.hint && formatHint(response.data.hint);
561
+ return cdpCallResult(normalized, response.data?.result, warning, ipcHint);
562
+ }
563
+ /**
564
+ * The result of a CDP call: the page exception it reported (exit 91), or its
565
+ * result with the session's and the method's hints; either way with the
566
+ * warning for a method the bundled protocol lacks.
567
+ *
568
+ * @param method - Method called
569
+ * @param cdpResult - What Chrome returned
570
+ * @param warning - Warning for a method the bundled protocol lacks
571
+ * @param ipcHint - Hint the session gave (e.g. a repeated-call pattern)
572
+ * @returns Command result
573
+ */
574
+ export function cdpCallResult(method, cdpResult, warning, ipcHint) {
575
+ const withCallWarning = warning ? { warning } : {};
500
576
  const exception = pageExceptionResult(cdpResult);
501
577
  if (exception)
502
- return exception;
503
- const result = {
578
+ return { ...exception, ...withCallWarning };
579
+ const hints = [ipcHint, getMethodHint(method, cdpResult)].filter(Boolean);
580
+ return {
504
581
  success: true,
505
- data: {
506
- method: normalized,
507
- result: cdpResult,
508
- },
582
+ data: { method, result: cdpResult },
583
+ ...withCallWarning,
584
+ ...(hints.length > 0 && { hint: hints.join('\n') }),
509
585
  };
510
- if (response.data?.hint) {
511
- result.hint = formatHint(response.data.hint);
512
- }
513
- const methodHint = getMethodHint(normalized, cdpResult);
514
- if (methodHint) {
515
- result.hint = result.hint ? `${result.hint}\n${methodHint}` : methodHint;
516
- }
517
- return result;
518
586
  }
519
587
  //# sourceMappingURL=cdp.js.map
@@ -5,10 +5,10 @@ import { purgeNeedsNamedSessionError, purgeRefusedError, sessionDirIsFileError,
5
5
  import { isSessionChrome } from '../session/cleanup/staleSession.js';
6
6
  import { performSessionCleanup } from '../session/cleanup/userCommands.js';
7
7
  import { isDaemonAlive } from '../session/daemonSocket.js';
8
- import { getSessionDir, getSessionFilePath, getSessionName } from '../session/paths.js';
8
+ import { getSessionDir, getSessionDownloadsDir, getSessionFilePath, getSessionName, } from '../session/paths.js';
9
9
  import { readDaemonPid, readPidFromFile } from '../session/pid.js';
10
10
  import { joinLines } from '../ui/formatting.js';
11
- import { sessionFilesCleanedMessage, sessionOutputRemovedMessage, sessionDirectoryCleanMessage, sessionDirectoryPurgedMessage, noSessionFilesMessage, sessionStillActiveError, sessionStillActiveSuggestion, warningMessage, } from '../ui/messages/commands.js';
11
+ import { downloadsKeptMessage, sessionFilesCleanedMessage, sessionOutputRemovedMessage, sessionDirectoryCleanMessage, sessionDirectoryPurgedMessage, noSessionFilesMessage, sessionStillActiveError, sessionStillActiveSuggestion, warningMessage, } from '../ui/messages/commands.js';
12
12
  import { delay } from '../utils/async.js';
13
13
  import { EXIT_CODES } from '../utils/exitCodes.js';
14
14
  import { isProcessAlive } from '../utils/process.js';
@@ -19,7 +19,7 @@ import { isProcessAlive } from '../utils/process.js';
19
19
  */
20
20
  function formatCleanup(data) {
21
21
  const { cleaned } = data;
22
- return joinLines(cleaned.session && sessionFilesCleanedMessage(), cleaned.output && sessionOutputRemovedMessage(), ...(data.warnings ?? []).map((warning) => warningMessage(warning)), '', data.message);
22
+ return joinLines(cleaned.session && sessionFilesCleanedMessage(), cleaned.output && sessionOutputRemovedMessage(), data.downloadsKept && downloadsKeptMessage(data.downloadsKept.dir, data.downloadsKept.files), ...(data.warnings ?? []).map((warning) => warningMessage(warning)), '', data.message);
23
23
  }
24
24
  /**
25
25
  * Delete the selected named session's directory (Chrome profile, logs, port).
@@ -136,6 +136,9 @@ async function cleanupBlocker(opts) {
136
136
  }
137
137
  /**
138
138
  * Clean up the selected session (and delete its directory with `--purge`).
139
+ * It does not run the session directory trust check (`secureSessionDir`): it
140
+ * sends no command, only probes the socket and signals PIDs verified by
141
+ * their command line, and must still clean up an untrusted directory.
139
142
  *
140
143
  * @param opts - Cleanup options
141
144
  * @returns Command result
@@ -159,11 +162,13 @@ async function cleanupSession(opts) {
159
162
  };
160
163
  }
161
164
  const didCleanup = Object.values(cleaned).some(Boolean) || purged !== undefined;
165
+ const downloadsKept = purged === undefined ? keptDownloads() : undefined;
162
166
  return {
163
167
  success: true,
164
168
  data: {
165
169
  cleaned,
166
170
  ...(purged !== undefined && { purged }),
171
+ ...(downloadsKept && { downloadsKept }),
167
172
  message: !didCleanup
168
173
  ? noSessionFilesMessage()
169
174
  : purged !== undefined
@@ -173,6 +178,18 @@ async function cleanupSession(opts) {
173
178
  },
174
179
  };
175
180
  }
181
+ /**
182
+ * Downloaded files of the session, which cleanup keeps.
183
+ *
184
+ * @returns Their directory and count, or undefined when there are none
185
+ */
186
+ function keptDownloads() {
187
+ const dir = getSessionDownloadsDir();
188
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory())
189
+ return undefined;
190
+ const files = fs.readdirSync(dir).length;
191
+ return files > 0 ? { dir, files } : undefined;
192
+ }
176
193
  /**
177
194
  * Register cleanup command
178
195
  *
@@ -181,7 +198,7 @@ async function cleanupSession(opts) {
181
198
  export function registerCleanupCommand(program) {
182
199
  program
183
200
  .command('cleanup')
184
- .description('Clean up stale session files')
201
+ .description('Clean up stale session files (downloaded files are kept)')
185
202
  .option('-f, --force', 'Kill a running (possibly hung) session, then clean up', false)
186
203
  .option('--remove-output', 'Also remove session.json output file', false)
187
204
  .option('--aggressive', 'Alias for --force (kept for compatibility)', false)
@@ -11,7 +11,8 @@ import type { DomEvalCommandOptions } from '../shared/optionTypes.js';
11
11
  * frame the script ran in is a `Frame:` line on stderr (JSON: `frame`), and
12
12
  * a warning about how the result was copied goes there too (JSON:
13
13
  * `warning`), so stdout stays the bare value for pipes. Long values are cut
14
- * (a string result in JSON with `truncatedFrom`) unless `--full`.
14
+ * unless `--full`: human output when formatted, JSON results by
15
+ * {@link boundEvalResult}.
15
16
  */
16
17
  export declare function handleDomEval(script: string, options: DomEvalCommandOptions): Promise<void>;
17
18
  //# sourceMappingURL=eval.d.ts.map
@@ -5,21 +5,21 @@
5
5
  * CLI-side handler. Actual evaluation happens in the daemon via the
6
6
  * `dom_eval` IPC command so the session's persistent CDP connection is reused.
7
7
  */
8
+ import { boundEvalResult } from './helpers/evalResult.js';
8
9
  import { documentReadyState } from './helpers/query.js';
9
10
  import { runCommand } from '../shared/CommandRunner.js';
10
- import { MAX_VALUE_LENGTH } from '../../constants.js';
11
11
  import { emptyScriptError, withLoadingHint } from '../../errors/messages.js';
12
12
  import { domEval } from '../../ipc/client.js';
13
13
  import { formatDomEval } from '../../ui/formatters/dom.js';
14
14
  import { evalFrameLine, warningMessage } from '../../ui/messages/commands.js';
15
15
  import { EXIT_CODES } from '../../utils/exitCodes.js';
16
- import { capLength } from '../../utils/strings.js';
17
16
  /**
18
17
  * Handle `bdg dom eval <script> [--frame <frame>]`. With `--frame`, the
19
18
  * frame the script ran in is a `Frame:` line on stderr (JSON: `frame`), and
20
19
  * a warning about how the result was copied goes there too (JSON:
21
20
  * `warning`), so stdout stays the bare value for pipes. Long values are cut
22
- * (a string result in JSON with `truncatedFrom`) unless `--full`.
21
+ * unless `--full`: human output when formatted, JSON results by
22
+ * {@link boundEvalResult}.
23
23
  */
24
24
  export async function handleDomEval(script, options) {
25
25
  await runCommand(async () => {
@@ -32,7 +32,7 @@ export async function handleDomEval(script, options) {
32
32
  errorContext: { suggestion: err.suggestion },
33
33
  };
34
34
  }
35
- const response = await domEval(script, options.frame);
35
+ const response = await domEval(script, options.frame, options.full);
36
36
  if (response.status === 'error' || !response.data) {
37
37
  const suggestion = await frameErrorSuggestion(response, options.frame);
38
38
  return {
@@ -42,7 +42,7 @@ export async function handleDomEval(script, options) {
42
42
  ...(suggestion && { errorContext: { suggestion } }),
43
43
  };
44
44
  }
45
- const { value, type, subtype, frame, warning } = response.data;
45
+ const { value, type, subtype, length, frame, warning } = response.data;
46
46
  const hint = [
47
47
  ...(frame !== undefined ? [evalFrameLine(frame)] : []),
48
48
  ...(warning ? [warningMessage(warning)] : []),
@@ -50,7 +50,7 @@ export async function handleDomEval(script, options) {
50
50
  return {
51
51
  success: true,
52
52
  data: {
53
- ...jsonResult(value, options),
53
+ ...(options.json && !options.full ? boundEvalResult(value, length) : { result: value }),
54
54
  type,
55
55
  ...(subtype && { subtype }),
56
56
  ...(frame !== undefined && { frame }),
@@ -60,21 +60,6 @@ export async function handleDomEval(script, options) {
60
60
  };
61
61
  }, options, (data) => formatDomEval(data, { full: options.full }));
62
62
  }
63
- /**
64
- * The result field of the output: a string result in JSON cut to
65
- * {@link MAX_VALUE_LENGTH} characters with `truncatedFrom`, unless `--full`
66
- * (human output is cut when formatted).
67
- *
68
- * @param value - Evaluated value
69
- * @param options - `--json`, `--full`
70
- * @returns `result`, and `truncatedFrom` when cut
71
- */
72
- function jsonResult(value, options) {
73
- if (!options.json || options.full || typeof value !== 'string')
74
- return { result: value };
75
- const { text, truncatedFrom } = capLength(value, MAX_VALUE_LENGTH);
76
- return { result: text, ...(truncatedFrom !== undefined && { truncatedFrom }) };
77
- }
78
63
  /**
79
64
  * Suggestion of a failed eval; a frame not found (83) while the page is
80
65
  * still loading says so (its iframes may not exist yet).
@@ -18,7 +18,7 @@ import { callCDP, domClick, domFill, domPressKey, domScroll, domSubmit } from '.
18
18
  import { findUnknownModifiers } from '../../runtime/dom/keyMapping.js';
19
19
  import { formatTriggeredRequestLines, formatTriggeredRequestsTitle, } from '../../ui/formatters/triggeredRequests.js';
20
20
  import { OutputFormatter } from '../../ui/formatting.js';
21
- import { CLICK_RESULT_WAIT_HELP, HOVER_OFF_DONE, HOVER_USAGE, POINTER_ACTION_DONE, POINTER_ACTION_NOUN, pointerScrollText, actionStatusLine, dialogConsoleText, moreMessagesText, newMessageText, pageNavigationText, shownElementText, stillChangingNote, } from '../../ui/messages/commands.js';
21
+ import { CLICK_RESULT_WAIT_HELP, HOVER_OFF_DONE, HOVER_USAGE, POINTER_ACTION_DONE, POINTER_ACTION_NOUN, pointerScrollText, actionStatusLine, dialogConsoleText, downloadText, moreMessagesText, newMessageText, pageNavigationText, shownElementText, stillChangingNote, } from '../../ui/messages/commands.js';
22
22
  import { sessionCommand } from '../../ui/messages/sessionCommand.js';
23
23
  import { EXIT_CODES } from '../../utils/exitCodes.js';
24
24
  /** Help of `--strict` on click and hover */
@@ -313,9 +313,9 @@ async function runPointerCommand(selectorOrIndex, options, action) {
313
313
  * "⚠ Element Clicked (page still changing)" with what it was still working
314
314
  * on, or "⚠ Element Clicked (no visible effect: …)"), the details, what
315
315
  * changed on the page (`Page:` navigation, `New text:` messages, `Shown:`
316
- * elements), then the network requests it triggered and the dialogs it
317
- * caused. No request list is shown when there were none (JSON has an empty
318
- * `triggeredRequests` then).
316
+ * elements), then the network requests it triggered, the downloads it
317
+ * started and the dialogs it caused. No request list is shown when there
318
+ * were none (JSON has an empty `triggeredRequests` then).
319
319
  *
320
320
  * @param done - What was done, e.g. "Element Clicked"
321
321
  * @param details - Label/value rows
@@ -353,6 +353,10 @@ function formatActionOutput(done, details, result, options = {}) {
353
353
  .blank()
354
354
  .section(formatTriggeredRequestsTitle(result.triggeredRequests ?? [], omitted), requests);
355
355
  }
356
+ if (result.downloads?.length)
357
+ fmt.blank();
358
+ for (const download of result.downloads ?? [])
359
+ fmt.text(downloadText(download));
356
360
  for (const dialog of result.dialogs ?? []) {
357
361
  fmt.blank();
358
362
  fmt.text(`Dialog: ${dialogConsoleText(dialog)}`);