UNPKG

browser-debugger-cli

Version:

DevTools telemetry in your terminal. For humans and agents. Direct WebSocket to Chrome's debugging port.

216 lines 8 kB
import * as fs from 'fs'; import * as path from 'path'; import { runCommand } from '../commands/shared/CommandRunner.js'; import { jsonOption } from '../commands/shared/commonOptions.js'; import { getHARData, callCDP, getNetworkHeaders } from '../ipc/client.js'; import { validateIPCResponse } from '../ipc/index.js'; import { getSessionFilePath } from '../session/paths.js'; import { buildHAR } from '../telemetry/har/builder.js'; import { isDaemonConnectionError } from '../ui/errors/utils.js'; import { formatCookies, formatNetworkHeaders } from '../ui/formatters/index.js'; import { sessionNotActiveError } from '../ui/messages/errors.js'; import { AtomicFileWriter } from '../utils/atomicFile.js'; import { EXIT_CODES } from '../utils/exitCodes.js'; import { VERSION } from '../utils/version.js'; /** * Generate timestamped filename for HAR export in ~/.bdg/ directory. * * @returns Full path to HAR file in ~/.bdg/capture-YYYY-MM-DD-HHMMSS.har * * @example * ```typescript * generateHARFilename(); // "~/.bdg/capture-2025-11-19-143045.har" * ``` */ function generateHARFilename() { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0'); const filename = `capture-${year}-${month}-${day}-${hours}${minutes}${seconds}.har`; const sessionDir = path.dirname(getSessionFilePath('OUTPUT')); return path.join(sessionDir, filename); } /** * Fetch network requests from live daemon session. * * @returns Network requests array * @throws Error if daemon connection fails or no network data available */ async function fetchFromLiveSession() { const response = await getHARData(); validateIPCResponse(response); if (!response.data?.requests) { throw new Error('No network data in response'); } return response.data.requests; } /** * Fetch network requests from offline session.json file. * * @returns Network requests array * @throws Error if session file not found or no network data available */ function fetchFromOfflineSession() { const sessionPath = getSessionFilePath('OUTPUT'); if (!fs.existsSync(sessionPath)) { throw new Error(sessionNotActiveError('export network data'), { cause: { code: EXIT_CODES.RESOURCE_NOT_FOUND }, }); } const sessionData = JSON.parse(fs.readFileSync(sessionPath, 'utf-8')); if (!sessionData.data?.network) { throw new Error('No network data in session file', { cause: { code: EXIT_CODES.RESOURCE_NOT_FOUND }, }); } return sessionData.data.network; } /** * Check if error indicates daemon is unavailable. * * @param error - Error to check * @returns True if error indicates no active session or daemon connection failure */ function isDaemonUnavailable(error) { if (isDaemonConnectionError(error)) { return true; } const errorMessage = error instanceof Error ? error.message : String(error); return errorMessage.includes('No active session'); } /** * Get network requests from live session or session.json. * * Tries live daemon first, falls back to offline session file. * * @returns Network requests array * @throws Error if no session available (live or offline) */ async function getNetworkRequests() { try { return await fetchFromLiveSession(); } catch (error) { if (isDaemonUnavailable(error)) { return fetchFromOfflineSession(); } throw error; } } /** * Format HAR export success message for human output. * * @param data - HAR export result data * @returns Formatted success message */ function formatHARExport(data) { return `✓ Exported ${data.entries} requests to ${data.file}`; } /** * Register network commands. * * @param program - Commander.js Command instance to register commands on */ export function registerNetworkCommands(program) { const networkCmd = program.command('network').description('Inspect network state and resources'); networkCmd .command('har [output-file]') .description('Export network data as HAR 1.2 format') .addOption(jsonOption()) .action(async (outputFile, options) => { await runCommand(async () => { const requests = await getNetworkRequests(); const outputPath = outputFile ?? generateHARFilename(); const dir = path.dirname(outputPath); if (dir !== '.' && !fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } const har = buildHAR(requests, { version: VERSION, }); await AtomicFileWriter.writeAsync(outputPath, JSON.stringify(har, null, 2)); return { success: true, data: { file: outputPath, entries: har.log.entries.length, }, }; }, options, formatHARExport); }); networkCmd .command('getCookies') .description('List cookies from the current page') .option('--url <url>', 'Filter cookies by URL') .addOption(jsonOption()) .action(async (options) => { await runCommand(async (opts) => { const params = {}; if (opts.url) { params['urls'] = [opts.url]; } const response = await callCDP('Network.getCookies', params); validateIPCResponse(response); const cookies = response.data?.result?.cookies ?? []; return { success: true, data: cookies, }; }, options, formatCookies); }); networkCmd .command('headers [id]') .description('Show HTTP headers (defaults to current main document)') .option('--header <name>', 'Filter to specific header name') .addOption(jsonOption()) .addHelpText('after', '\nNote: Without [id], shows headers for the current main document.\n If the page has navigated, this will be the latest navigation, not the original URL.') .action(async (id, options) => { await runCommand(async (opts) => { const response = await getNetworkHeaders({ ...(id && { id }), ...(opts.header && { headerName: opts.header }), }); validateIPCResponse(response); if (!response.data) { return { success: false, error: 'No data returned from worker', exitCode: EXIT_CODES.RESOURCE_NOT_FOUND, }; } return { success: true, data: response.data, }; }, options, formatNetworkHeaders); }); networkCmd .command('document') .description('Show main HTML document request details (alias for headers without ID)') .option('--header <name>', 'Filter to specific header name') .addOption(jsonOption()) .action(async (options) => { await runCommand(async (opts) => { const response = await getNetworkHeaders({ ...(opts.header && { headerName: opts.header }), }); validateIPCResponse(response); if (!response.data) { return { success: false, error: 'No data returned from worker', exitCode: EXIT_CODES.RESOURCE_NOT_FOUND, }; } return { success: true, data: response.data, }; }, options, formatNetworkHeaders); }); } //# sourceMappingURL=network.js.map