@@ -62,6 +62,10 @@ struct Args {
6262 #[ arg( long) ]
6363 root_summary : bool ,
6464
65+ /// Whether to generate TypeScript sidebar files
66+ #[ arg( long) ]
67+ sidebar : bool ,
68+
6569 /// Print verbose output
6670 #[ arg( short, long) ]
6771 verbose : bool ,
@@ -140,10 +144,18 @@ fn main() -> io::Result<()> {
140144 if args. verbose {
141145 println ! ( "Updating root summary in \" {}\" " , path. to_string_lossy( ) ) ;
142146 }
143- // TODO: This is where we update the cli reference sidebar.ts
144147 update_root_summary ( path, & root_summary) ?;
145148 }
146149
150+ // Generate TypeScript sidebar files.
151+ if args. sidebar {
152+ let vocs_dir = Path :: new ( args. root_dir . as_str ( ) ) . join ( "vocs" ) ;
153+ if args. verbose {
154+ println ! ( "Generating TypeScript sidebar files in \" {}\" " , vocs_dir. display( ) ) ;
155+ }
156+ generate_sidebar_files ( & vocs_dir, & output, args. verbose ) ?;
157+ }
158+
147159 Ok ( ( ) )
148160}
149161
@@ -247,6 +259,186 @@ fn update_root_summary(root_dir: &Path, root_summary: &str) -> io::Result<()> {
247259 write_file ( & summary_file, root_summary)
248260}
249261
262+ /// Generates TypeScript sidebar files for each command.
263+ fn generate_sidebar_files ( vocs_dir : & Path , output : & [ ( Cmd , String ) ] , verbose : bool ) -> io:: Result < ( ) > {
264+ // Group commands by their root command name (reth or op-reth)
265+ // Also create a map of commands to their help output
266+ let mut commands_by_root: std:: collections:: HashMap < String , Vec < & Cmd > > = std:: collections:: HashMap :: new ( ) ;
267+ let mut help_map: std:: collections:: HashMap < String , String > = std:: collections:: HashMap :: new ( ) ;
268+
269+ for ( cmd, help_output) in output {
270+ let root_name = cmd. command_name ( ) . to_string ( ) ;
271+ commands_by_root. entry ( root_name. clone ( ) ) . or_insert_with ( Vec :: new) . push ( cmd) ;
272+ // Store help output for each command using its string representation as key
273+ help_map. insert ( cmd. to_string ( ) , help_output. clone ( ) ) ;
274+ }
275+
276+ // Generate sidebar file for each root command
277+ for ( root_name, cmds) in commands_by_root {
278+ // Get root help output
279+ let root_help = help_map. get ( & root_name) . map ( |s| s. as_str ( ) ) ;
280+ let sidebar_content = generate_sidebar_ts ( & root_name, cmds, root_help, & help_map) ?;
281+ let file_name = match root_name. as_str ( ) {
282+ "reth" => "sidebar-cli-reth.ts" ,
283+ "op-reth" => "sidebar-cli-op-reth.ts" ,
284+ _ => {
285+ if verbose {
286+ println ! ( "Skipping unknown command: {}" , root_name) ;
287+ }
288+ continue ;
289+ }
290+ } ;
291+
292+ let sidebar_file = vocs_dir. join ( file_name) ;
293+ if verbose {
294+ println ! ( "Writing sidebar file: {}" , sidebar_file. display( ) ) ;
295+ }
296+ write_file ( & sidebar_file, & sidebar_content) ?;
297+ }
298+
299+ Ok ( ( ) )
300+ }
301+
302+ /// Generates TypeScript code for a sidebar file.
303+ fn generate_sidebar_ts (
304+ root_name : & str ,
305+ commands : Vec < & Cmd > ,
306+ root_help : Option < & str > ,
307+ help_map : & std:: collections:: HashMap < String , String > ,
308+ ) -> io:: Result < String > {
309+ // Find all top-level commands (commands with exactly one subcommand)
310+ let mut top_level_commands: Vec < & Cmd > = commands
311+ . iter ( )
312+ . copied ( )
313+ . filter ( |cmd| cmd. subcommands . len ( ) == 1 )
314+ . collect ( ) ;
315+
316+ // Remove duplicates using a set
317+ let mut seen = std:: collections:: HashSet :: new ( ) ;
318+ top_level_commands. retain ( |cmd| {
319+ let key = & cmd. subcommands [ 0 ] ;
320+ seen. insert ( key. clone ( ) )
321+ } ) ;
322+
323+ // Sort by the order they appear in help output, not alphabetically
324+ if let Some ( help) = root_help {
325+ let help_order = parse_sub_commands ( help) ;
326+ top_level_commands. sort_by ( |a, b| {
327+ let a_name = & a. subcommands [ 0 ] ;
328+ let b_name = & b. subcommands [ 0 ] ;
329+ let a_pos = help_order. iter ( ) . position ( |x| x == a_name) . unwrap_or ( usize:: MAX ) ;
330+ let b_pos = help_order. iter ( ) . position ( |x| x == b_name) . unwrap_or ( usize:: MAX ) ;
331+ a_pos. cmp ( & b_pos)
332+ } ) ;
333+ }
334+
335+ // Generate TypeScript code
336+ let var_name = match root_name {
337+ "reth" => "rethCliSidebar" ,
338+ "op-reth" => "opRethCliSidebar" ,
339+ _ => "cliSidebar" ,
340+ } ;
341+
342+ let mut ts_code = String :: from ( "import { SidebarItem } from \" vocs\" ;\n \n " ) ;
343+ ts_code. push_str ( & format ! ( "export const {}: SidebarItem = {{\n " , var_name) ) ;
344+ ts_code. push_str ( & format ! ( " text: \" {}\" ,\n " , root_name) ) ;
345+ ts_code. push_str ( & format ! ( " link: \" /cli/{}\" ,\n " , root_name) ) ;
346+ ts_code. push_str ( " collapsed: false,\n " ) ;
347+ ts_code. push_str ( " items: [\n " ) ;
348+
349+ for ( idx, cmd) in top_level_commands. iter ( ) . enumerate ( ) {
350+ let is_last = idx == top_level_commands. len ( ) - 1 ;
351+ if let Some ( item_str) = build_sidebar_item ( root_name, cmd, & commands, 1 , help_map, is_last) {
352+ ts_code. push_str ( & item_str) ;
353+ }
354+ }
355+
356+ ts_code. push_str ( " ]\n " ) ;
357+ ts_code. push_str ( "};\n \n " ) ;
358+
359+ Ok ( ts_code)
360+ }
361+
362+ /// Builds a sidebar item for a command and its children.
363+ /// Returns TypeScript code string.
364+ fn build_sidebar_item (
365+ root_name : & str ,
366+ cmd : & Cmd ,
367+ all_commands : & [ & Cmd ] ,
368+ depth : usize ,
369+ help_map : & std:: collections:: HashMap < String , String > ,
370+ is_last : bool ,
371+ ) -> Option < String > {
372+ let full_cmd_name = cmd. to_string ( ) ;
373+ let link_path = format ! ( "/cli/{}" , full_cmd_name. replace( " " , "/" ) ) ;
374+
375+ // Find all direct child commands (commands whose subcommands start with this command's subcommands)
376+ let mut children: Vec < & Cmd > = all_commands
377+ . iter ( )
378+ . copied ( )
379+ . filter ( |other_cmd| {
380+ other_cmd. subcommands . len ( ) == cmd. subcommands . len ( ) + 1
381+ && other_cmd. subcommands [ ..cmd. subcommands . len ( ) ] == cmd. subcommands [ ..]
382+ } )
383+ . collect ( ) ;
384+
385+ // Sort children by the order they appear in help output, not alphabetically
386+ if children. len ( ) > 1 {
387+ // Get help output for this command to determine subcommand order
388+ if let Some ( help_output) = help_map. get ( & full_cmd_name) {
389+ let help_order = parse_sub_commands ( help_output) ;
390+ children. sort_by ( |a, b| {
391+ let a_name = a. subcommands . last ( ) . unwrap ( ) ;
392+ let b_name = b. subcommands . last ( ) . unwrap ( ) ;
393+ let a_pos = help_order. iter ( ) . position ( |x| x == a_name) . unwrap_or ( usize:: MAX ) ;
394+ let b_pos = help_order. iter ( ) . position ( |x| x == b_name) . unwrap_or ( usize:: MAX ) ;
395+ a_pos. cmp ( & b_pos)
396+ } ) ;
397+ } else {
398+ // Fall back to alphabetical if we can't get help
399+ children. sort_by ( |a, b| {
400+ a. subcommands . last ( ) . unwrap ( ) . cmp ( b. subcommands . last ( ) . unwrap ( ) )
401+ } ) ;
402+ }
403+ }
404+
405+ let indent = " " . repeat ( depth) ;
406+ let mut item_str = String :: new ( ) ;
407+
408+ item_str. push_str ( & format ! ( "{}{{\n " , indent) ) ;
409+ item_str. push_str ( & format ! ( "{} text: \" {}\" ,\n " , indent, full_cmd_name) ) ;
410+ item_str. push_str ( & format ! ( "{} link: \" {}\" " , indent, link_path) ) ;
411+
412+ if !children. is_empty ( ) {
413+ item_str. push_str ( ",\n " ) ;
414+ item_str. push_str ( & format ! ( "{} collapsed: true,\n " , indent) ) ;
415+ item_str. push_str ( & format ! ( "{} items: [\n " , indent) ) ;
416+
417+ for ( idx, child_cmd) in children. iter ( ) . enumerate ( ) {
418+ let child_is_last = idx == children. len ( ) - 1 ;
419+ if let Some ( child_str) = build_sidebar_item ( root_name, child_cmd, all_commands, depth + 1 , help_map, child_is_last) {
420+ item_str. push_str ( & child_str) ;
421+ }
422+ }
423+
424+ item_str. push_str ( & format ! ( "{} ]\n " , indent) ) ;
425+ if is_last {
426+ item_str. push_str ( & format ! ( "{}}}\n " , indent) ) ;
427+ } else {
428+ item_str. push_str ( & format ! ( "{}}},\n " , indent) ) ;
429+ }
430+ } else {
431+ item_str. push_str ( "\n " ) ;
432+ if is_last {
433+ item_str. push_str ( & format ! ( "{}}}\n " , indent) ) ;
434+ } else {
435+ item_str. push_str ( & format ! ( "{}}},\n " , indent) ) ;
436+ }
437+ }
438+
439+ Some ( item_str)
440+ }
441+
250442/// Preprocesses the help output of a command.
251443fn preprocess_help ( s : & str ) -> Cow < ' _ , str > {
252444 static REPLACEMENTS : LazyLock < Vec < ( Regex , & str ) > > = LazyLock :: new ( || {
0 commit comments