@@ -17,6 +17,7 @@ import * as net from "net";
1717import { spawn } from "child_process" ;
1818import * as readline from "readline" ;
1919import type { Protocol } from "devtools-protocol" ;
20+ import WebSocket from "ws" ;
2021import { version as VERSION } from "../package.json" ;
2122import {
2223 DEFAULT_LOCAL_CONFIG ,
@@ -2921,6 +2922,261 @@ networkCmd
29212922 }
29222923 } ) ;
29232924
2925+ // ==================== CDP TAILING ====================
2926+
2927+ interface CDPMessage {
2928+ id ?: number ;
2929+ method ?: string ;
2930+ params ?: unknown ;
2931+ result ?: unknown ;
2932+ error ?: { code : number ; message : string } ;
2933+ sessionId ?: string ;
2934+ }
2935+
2936+ const CDP_DEFAULT_DOMAINS = [ "Network" , "Console" , "Runtime" , "Log" , "Page" ] ;
2937+
2938+ program
2939+ . command ( "cdp <url|port>" )
2940+ . description (
2941+ "Attach to a CDP target and stream DevTools protocol events as NDJSON.\n" +
2942+ "Accepts a WebSocket URL (ws://...) or a bare port number (e.g. 9222).\n" +
2943+ "Output is one JSON object per line, suitable for piping to files or jq." ,
2944+ )
2945+ . option (
2946+ "--domain <domains...>" ,
2947+ `CDP domains to enable (repeatable). Default: ${ CDP_DEFAULT_DOMAINS . join ( "," ) } ` ,
2948+ )
2949+ . option ( "--pretty" , "Human-readable output instead of JSON" )
2950+ . action (
2951+ async (
2952+ target : string ,
2953+ cmdOpts : { domain ?: string [ ] ; pretty ?: boolean } ,
2954+ ) => {
2955+ const wsUrl = await resolveWsTarget ( target ) ;
2956+ const domains = cmdOpts . domain ?? CDP_DEFAULT_DOMAINS ;
2957+ const usePretty = cmdOpts . pretty ?? process . stdout . isTTY ?? false ;
2958+
2959+ let messageId = 1 ;
2960+ const pendingIds = new Set < number > ( ) ;
2961+ const targetSessionMap = new Map < string , string > ( ) ;
2962+
2963+ function sendCDP (
2964+ ws : WebSocket ,
2965+ method : string ,
2966+ params : Record < string , unknown > = { } ,
2967+ sessionId ?: string ,
2968+ ) : number {
2969+ const id = messageId ++ ;
2970+ pendingIds . add ( id ) ;
2971+ const msg : Record < string , unknown > = { id, method, params } ;
2972+ if ( sessionId ) msg . sessionId = sessionId ;
2973+ ws . send ( JSON . stringify ( msg ) ) ;
2974+ return id ;
2975+ }
2976+
2977+ function enableDomainsForSession ( ws : WebSocket , sessionId : string ) : void {
2978+ for ( const domain of domains ) {
2979+ if ( domain === "Network" ) {
2980+ sendCDP (
2981+ ws ,
2982+ "Network.enable" ,
2983+ { maxTotalBufferSize : 1000000 , maxResourceBufferSize : 100000 } ,
2984+ sessionId ,
2985+ ) ;
2986+ } else {
2987+ sendCDP ( ws , `${ domain } .enable` , { } , sessionId ) ;
2988+ }
2989+ }
2990+ }
2991+
2992+ function writeEvent ( message : CDPMessage ) : void {
2993+ try {
2994+ process . stdout . write ( JSON . stringify ( message ) + "\n" ) ;
2995+ } catch ( err : unknown ) {
2996+ if ( ( err as NodeJS . ErrnoException ) . code === "EPIPE" ) process . exit ( 0 ) ;
2997+ throw err ;
2998+ }
2999+ }
3000+
3001+ function writePrettyEvent ( message : CDPMessage ) : void {
3002+ if ( ! message . method ) return ;
3003+ const params = message . params as Record < string , unknown > | undefined ;
3004+ let line = `[${ message . method } ]` ;
3005+
3006+ try {
3007+ switch ( message . method ) {
3008+ case "Network.requestWillBeSent" : {
3009+ const req = params ?. request as
3010+ | { method ?: string ; url ?: string }
3011+ | undefined ;
3012+ if ( req ) line += ` ${ req . method ?? "?" } ${ req . url ?? "" } ` ;
3013+ break ;
3014+ }
3015+ case "Network.responseReceived" : {
3016+ const resp = params ?. response as
3017+ | { status ?: number ; url ?: string }
3018+ | undefined ;
3019+ if ( resp ) line += ` ${ resp . status ?? "?" } ${ resp . url ?? "" } ` ;
3020+ break ;
3021+ }
3022+ case "Network.loadingFailed" : {
3023+ const errorText =
3024+ ( params ?. errorText as string ) ??
3025+ ( params ?. canceled ? "Canceled" : "Unknown" ) ;
3026+ line += ` ${ errorText } ` ;
3027+ break ;
3028+ }
3029+ case "Runtime.consoleAPICalled" : {
3030+ const type = ( params ?. type as string ) ?? "log" ;
3031+ const args =
3032+ ( params ?. args as Array < {
3033+ value ?: unknown ;
3034+ description ?: string ;
3035+ } > ) ?? [ ] ;
3036+ const text = args
3037+ . map ( ( a ) => a . description ?? a . value ?? "" )
3038+ . join ( " " ) ;
3039+ line += ` [${ type } ] ${ text } ` ;
3040+ break ;
3041+ }
3042+ case "Runtime.exceptionThrown" : {
3043+ const detail = params ?. exceptionDetails as
3044+ | {
3045+ text ?: string ;
3046+ exception ?: { description ?: string } ;
3047+ }
3048+ | undefined ;
3049+ line += ` ${ detail ?. exception ?. description ?? detail ?. text ?? "Unknown exception" } ` ;
3050+ break ;
3051+ }
3052+ case "Page.frameNavigated" : {
3053+ const url = ( params ?. frame as { url ?: string } ) ?. url ?? "" ;
3054+ if ( url ) line += ` ${ url } ` ;
3055+ break ;
3056+ }
3057+ case "Target.attachedToTarget" : {
3058+ const info = params ?. targetInfo as
3059+ | { type ?: string ; url ?: string }
3060+ | undefined ;
3061+ if ( info ) line += ` [${ info . type ?? "?" } ] ${ info . url ?? "" } ` ;
3062+ break ;
3063+ }
3064+ default :
3065+ break ;
3066+ }
3067+ } catch {
3068+ // Formatting failed — use method name only
3069+ }
3070+
3071+ try {
3072+ process . stdout . write ( line + "\n" ) ;
3073+ } catch ( err : unknown ) {
3074+ if ( ( err as NodeJS . ErrnoException ) . code === "EPIPE" ) process . exit ( 0 ) ;
3075+ throw err ;
3076+ }
3077+ }
3078+
3079+ const emit = usePretty ? writePrettyEvent : writeEvent ;
3080+
3081+ await new Promise < void > ( ( resolve ) => {
3082+ const ws = new WebSocket ( wsUrl ) ;
3083+ let closed = false ;
3084+
3085+ function cleanup ( ) : void {
3086+ if ( closed ) return ;
3087+ closed = true ;
3088+ if (
3089+ ws . readyState === WebSocket . OPEN ||
3090+ ws . readyState === WebSocket . CONNECTING
3091+ ) {
3092+ ws . close ( ) ;
3093+ }
3094+ resolve ( ) ;
3095+ }
3096+
3097+ process . on ( "SIGINT" , cleanup ) ;
3098+ process . on ( "SIGTERM" , cleanup ) ;
3099+
3100+ ws . on ( "open" , ( ) => {
3101+ if ( usePretty ) {
3102+ process . stderr . write ( `Connected to ${ wsUrl } \n` ) ;
3103+ }
3104+
3105+ // Auto-attach to page targets
3106+ sendCDP ( ws , "Target.setAutoAttach" , {
3107+ autoAttach : true ,
3108+ flatten : true ,
3109+ waitForDebuggerOnStart : false ,
3110+ filter : [ { type : "page" } ] ,
3111+ } ) ;
3112+
3113+ sendCDP ( ws , "Target.setDiscoverTargets" , {
3114+ discover : true ,
3115+ filter : [ { type : "page" } ] ,
3116+ } ) ;
3117+ } ) ;
3118+
3119+ ws . on ( "message" , ( raw : WebSocket . RawData ) => {
3120+ let data : CDPMessage ;
3121+ try {
3122+ data = JSON . parse ( raw . toString ( ) ) as CDPMessage ;
3123+ } catch {
3124+ return ;
3125+ }
3126+
3127+ // Filter out responses to our own commands
3128+ if ( data . id !== undefined && pendingIds . has ( data . id ) ) {
3129+ pendingIds . delete ( data . id ) ;
3130+ if ( data . error ) {
3131+ process . stderr . write (
3132+ `CDP error (id=${ data . id } ): ${ data . error . message } \n` ,
3133+ ) ;
3134+ }
3135+ return ;
3136+ }
3137+
3138+ // Track page targets and enable domains
3139+ if ( data . method === "Target.attachedToTarget" && data . params ) {
3140+ const p = data . params as {
3141+ sessionId : string ;
3142+ targetInfo : { targetId : string ; type : string } ;
3143+ } ;
3144+ if ( p . targetInfo ?. type === "page" ) {
3145+ targetSessionMap . set ( p . targetInfo . targetId , p . sessionId ) ;
3146+ enableDomainsForSession ( ws , p . sessionId ) ;
3147+ }
3148+ }
3149+
3150+ if ( data . method === "Target.detachedFromTarget" && data . params ) {
3151+ const p = data . params as {
3152+ sessionId : string ;
3153+ targetId ?: string ;
3154+ } ;
3155+ const targetId =
3156+ p . targetId ??
3157+ [ ...targetSessionMap . entries ( ) ] . find (
3158+ ( [ , sid ] ) => sid === p . sessionId ,
3159+ ) ?. [ 0 ] ;
3160+ if ( targetId ) targetSessionMap . delete ( targetId ) ;
3161+ }
3162+
3163+ emit ( data ) ;
3164+ } ) ;
3165+
3166+ ws . on ( "error" , ( err : Error ) => {
3167+ process . stderr . write ( `Error: ${ err . message } \n` ) ;
3168+ } ) ;
3169+
3170+ ws . on ( "close" , ( ) => {
3171+ if ( ! closed && usePretty ) {
3172+ process . stderr . write ( "Disconnected.\n" ) ;
3173+ }
3174+ cleanup ( ) ;
3175+ } ) ;
3176+ } ) ;
3177+ } ,
3178+ ) ;
3179+
29243180// ==================== RUN ====================
29253181
29263182program . parse ( ) ;
0 commit comments