browser-debugger-cli
Version:
DevTools telemetry in your terminal. For humans and agents. Direct WebSocket to Chrome's debugging port.
390 lines • 14.5 kB
JavaScript
import { normalizeMethod } from '../cdp/protocol.js';
import { getAllDomainSummaries, getDomainMethods, getDomainSummary, getMethodSchema, } from '../cdp/schema.js';
import { runCommand } from '../commands/shared/CommandRunner.js';
import { callCDP } from '../ipc/client.js';
import { validateIPCResponse } from '../ipc/index.js';
import { CommandError } from '../ui/errors/index.js';
import { getErrorMessage } from '../utils/errors.js';
import { EXIT_CODES } from '../utils/exitCodes.js';
import { findSimilar } from '../utils/suggestions.js';
/**
* Domain-specific notes for event-based or special behavior CDP domains.
*
* These notes are shown in --describe output and when methods return empty results.
* Helps agents understand async/event-based CDP patterns that don't fit request-response model.
*/
const DOMAIN_NOTES = {
Audits: 'Event-based domain. Results arrive via events (e.g., Audits.issueAdded), not method responses. ' +
"For contrast checking, use: bdg dom eval 'getComputedStyle(el).color'",
Overlay: 'Visual debugging domain. Methods like highlightNode show overlays but return empty. ' +
'Use Overlay.hideHighlight to clear.',
Profiler: 'Sampling profiler. Call Profiler.start, perform actions, then Profiler.stop to get results.',
HeapProfiler: 'Heap profiler. Results collected via events after takeHeapSnapshot or startSampling.',
Tracing: 'Performance tracing. Call Tracing.start, perform actions, then Tracing.end. ' +
'Data arrives via Tracing.dataCollected events.',
};
/**
* Method-specific notes for methods with non-obvious behavior.
*/
const METHOD_NOTES = {
'Audits.checkContrast': 'This method triggers contrast analysis but results are sent via Audits.issueAdded events. ' +
'Alternative: bdg dom eval with getComputedStyle() for direct contrast checking.',
'Audits.enable': 'Enables the Audits domain. Issues will arrive via Audits.issueAdded events.',
'Overlay.highlightNode': 'Highlights a node visually. Returns empty on success. Use Overlay.hideHighlight to clear.',
'Profiler.start': 'Starts CPU profiling. Returns empty. Call Profiler.stop to get the profile data.',
'Tracing.start': 'Starts tracing. Returns empty. Data arrives via events after Tracing.end.',
};
/**
* Check if a CDP result is empty (null, undefined, or empty object).
*/
function isEmptyResult(result) {
if (result === null || result === undefined) {
return true;
}
if (typeof result === 'object' && Object.keys(result).length === 0) {
return true;
}
return false;
}
/**
* Get contextual hint for a method based on domain notes and result.
*/
function getMethodHint(methodName, result) {
if (METHOD_NOTES[methodName]) {
return METHOD_NOTES[methodName];
}
const domain = methodName.split('.')[0];
if (domain && DOMAIN_NOTES[domain] && isEmptyResult(result)) {
return DOMAIN_NOTES[domain];
}
return undefined;
}
/**
* Register CDP command with full introspection support.
*
* Supports multiple modes:
* - Execution: `bdg cdp Network.getCookies --params '{...}'`
* - List domains: `bdg cdp --list`
* - List methods: `bdg cdp Network --list`
* - Describe method: `bdg cdp Network.getCookies --describe`
* - Search: `bdg cdp --search cookie`
*
* All modes support case-insensitive input and provide structured JSON output.
*
* @param program - Commander.js Command instance to register commands on
*/
export function registerCdpCommand(program) {
program
.command('cdp')
.description('CDP protocol introspection and execution (53 domains, 300+ methods)\n' +
' Discovery: --list, --search, --describe\n' +
' Execution: case-insensitive (network.getcookies works)')
.argument('[method]', 'CDP method name (e.g., Network.getCookies, network.getcookies)')
.option('--params <json>', 'Method parameters as JSON')
.option('--list', 'List all domains or methods in a domain')
.option('--describe', 'Show method signature and parameters')
.option('--search <query>', 'Search methods by keyword')
.action(async (method, options) => {
await runCommand(async (opts) => {
if (opts.search) {
return await handleSearch(opts.search);
}
if (opts.list && !method) {
return handleListDomains();
}
if (opts.list && method) {
return handleListDomainMethods(method);
}
if (opts.describe && method) {
return handleDescribeMethod(method);
}
if (method) {
return await handleExecuteMethod(method, opts.params);
}
throw new CommandError('Missing required argument or flag', {
suggestion: 'Usage: bdg cdp [method] [--params <json>] [--list] [--describe] [--search <query>]',
}, EXIT_CODES.INVALID_ARGUMENTS);
}, { ...options, json: true });
});
}
/**
* Find similar methods to suggest when a method is not found.
* Returns up to 3 closest matches based on edit distance.
*
* Uses the shared findSimilar utility for consistency with other typo detection.
*
* @param methodName - The method name that was not found
* @param domain - Optional domain to search within
* @returns Array of similar method names
*/
function findSimilarMethods(methodName, domain) {
const allDomains = getAllDomainSummaries();
const candidates = [];
for (const domainSummary of allDomains) {
if (domain && domainSummary.name.toLowerCase() !== domain.toLowerCase()) {
continue;
}
const methods = getDomainMethods(domainSummary.name);
for (const method of methods) {
candidates.push(method.name);
}
}
return findSimilar(methodName, candidates, {
maxDistance: Math.max(Math.floor(methodName.length / 2), 3),
maxSuggestions: 3,
caseInsensitive: true,
});
}
/**
* Handle search mode: Find methods by keyword.
*
* @param query - Search query
* @returns Success result with matching methods
*/
async function handleSearch(query) {
const { searchMethods } = await import('../cdp/schema.js');
const results = searchMethods(query);
return {
success: true,
data: {
query,
count: results.length,
methods: results.map((m) => ({
name: m.name,
domain: m.domain,
method: m.method,
description: m.description,
experimental: m.experimental,
deprecated: m.deprecated,
parameterCount: m.parameters.length,
example: m.example?.command,
})),
},
};
}
/**
* Handle list domains mode: Show all available domains.
*
* @returns Success result with domain summaries
*/
function handleListDomains() {
const summaries = getAllDomainSummaries();
return {
success: true,
data: {
count: summaries.length,
domains: summaries.map((s) => ({
name: s.name,
description: s.description,
commands: s.commandCount,
events: s.eventCount,
experimental: s.experimental,
deprecated: s.deprecated,
dependencies: s.dependencies,
})),
},
};
}
/**
* Handle list domain methods mode: Show all methods in a domain.
*
* @param domainName - Domain name (case-insensitive)
* @returns Success result with method summaries
*/
function handleListDomainMethods(domainName) {
const summary = getDomainSummary(domainName);
if (!summary) {
return {
success: false,
error: `Domain '${domainName}' not found`,
exitCode: EXIT_CODES.INVALID_ARGUMENTS,
errorContext: {
suggestion: 'Use: bdg cdp --list (to see all domains)',
},
};
}
const methods = getDomainMethods(domainName);
return {
success: true,
data: {
domain: summary.name,
description: summary.description,
count: methods.length,
methods: methods.map((m) => ({
name: m.method,
fullName: m.name,
description: m.description,
experimental: m.experimental,
deprecated: m.deprecated,
parameterCount: m.parameters.length,
parameters: m.parameters.map((p) => ({
name: p.name,
type: p.type,
required: p.required,
})),
returns: m.returns.map((r) => ({
name: r.name,
type: r.type,
})),
example: m.example?.command,
})),
},
};
}
/**
* Handle describe method mode: Show method signature and parameters.
*
* @param methodName - Method name (case-insensitive, with or without domain)
* @returns Success result with method schema
*/
function handleDescribeMethod(methodName) {
const [domainName, method] = methodName.includes('.')
? methodName.split('.')
: [methodName, undefined];
if (!method) {
const summary = getDomainSummary(domainName);
if (!summary) {
const similar = findSimilarMethods(methodName);
const suggestions = ['Use: bdg cdp --list (to see all domains)'];
if (similar.length > 0) {
suggestions.push('');
suggestions.push('Did you mean:');
similar.forEach((name) => suggestions.push(` - ${name}`));
}
return {
success: false,
error: `Domain or method '${methodName}' not found`,
exitCode: EXIT_CODES.INVALID_ARGUMENTS,
errorContext: {
suggestion: suggestions.join('\n'),
},
};
}
const domainNote = DOMAIN_NOTES[summary.name];
return {
success: true,
data: {
type: 'domain',
domain: summary.name,
description: summary.description,
commands: summary.commandCount,
events: summary.eventCount,
experimental: summary.experimental,
deprecated: summary.deprecated,
note: domainNote,
nextStep: `Use: bdg cdp ${summary.name} --list (to see all methods)`,
},
};
}
const schema = getMethodSchema(domainName, method);
if (!schema) {
const similar = findSimilarMethods(methodName, domainName);
const suggestions = [`Use: bdg cdp ${domainName} --list (to see all ${domainName} methods)`];
if (similar.length > 0) {
suggestions.push('');
suggestions.push('Did you mean:');
similar.forEach((name) => suggestions.push(` - ${name}`));
}
return {
success: false,
error: `Method '${methodName}' not found`,
exitCode: EXIT_CODES.INVALID_ARGUMENTS,
errorContext: {
suggestion: suggestions.join('\n'),
},
};
}
const methodNote = METHOD_NOTES[schema.name] ?? DOMAIN_NOTES[schema.domain];
return {
success: true,
data: {
type: 'method',
name: schema.name,
domain: schema.domain,
method: schema.method,
description: schema.description,
experimental: schema.experimental,
deprecated: schema.deprecated,
note: methodNote,
parameters: schema.parameters.map((p) => ({
name: p.name,
type: p.type,
required: p.required,
description: p.description,
enum: p.enum,
items: p.items,
deprecated: p.deprecated,
})),
returns: schema.returns.map((r) => ({
name: r.name,
type: r.type,
optional: r.optional,
description: r.description,
items: r.items,
})),
example: schema.example,
},
};
}
/**
* Handle execute method mode: Call CDP method.
*
* @param methodName - Method name (case-insensitive)
* @param paramsJson - Parameters as JSON string
* @returns Success result with method response
*/
async function handleExecuteMethod(methodName, paramsJson) {
const normalized = normalizeMethod(methodName);
if (!normalized) {
const similar = findSimilarMethods(methodName);
const suggestions = ['Use: bdg cdp --search <keyword> (to search for methods)'];
if (similar.length > 0) {
suggestions.push('');
suggestions.push('Did you mean:');
similar.forEach((name) => suggestions.push(` - ${name}`));
}
return {
success: false,
error: `Method '${methodName}' not found`,
exitCode: EXIT_CODES.INVALID_ARGUMENTS,
errorContext: {
suggestion: suggestions.join('\n'),
},
};
}
let params;
if (paramsJson) {
try {
params = JSON.parse(paramsJson);
}
catch (error) {
return {
success: false,
error: `Error parsing --params: ${getErrorMessage(error)}. Parameters must be valid JSON.`,
exitCode: EXIT_CODES.INVALID_ARGUMENTS,
errorContext: {
suggestion: `Use: bdg cdp ${normalized} --describe (to see parameter schema)`,
},
};
}
}
const response = await callCDP(normalized, params);
validateIPCResponse(response);
const cdpResult = response.data?.result;
const result = {
success: true,
data: {
method: normalized,
result: cdpResult,
},
};
if (response.data?.hint) {
result.hint = response.data.hint;
}
const methodHint = getMethodHint(normalized, cdpResult);
if (methodHint) {
result.hint = result.hint ? `${result.hint}\n${methodHint}` : methodHint;
}
return result;
}
//# sourceMappingURL=cdp.js.map