@@ -34,6 +34,29 @@ use rustyline::{Context, Editor, Helper};
3434const FNN_CLI_VERSION : & str = env ! ( "CARGO_PKG_VERSION" ) ;
3535const DEFAULT_RPC_URL : & str = "http://127.0.0.1:8227" ;
3636
37+ #[ derive( Debug , Default , serde:: Deserialize ) ]
38+ struct ConfigFile {
39+ url : Option < String > ,
40+ auth_token : Option < String > ,
41+ output_format : Option < String > ,
42+ }
43+
44+ fn default_config_dir ( ) -> std:: path:: PathBuf {
45+ dirs_home_inner ( )
46+ . map ( |h| h. join ( ".fnn" ) )
47+ . unwrap_or_else ( || std:: path:: PathBuf :: from ( ".fnn" ) )
48+ }
49+
50+ fn default_config_path ( ) -> std:: path:: PathBuf {
51+ default_config_dir ( ) . join ( "config.toml" )
52+ }
53+
54+ fn load_config_file ( path : & std:: path:: Path ) -> Result < ConfigFile , anyhow:: Error > {
55+ let content = std:: fs:: read_to_string ( path) ?;
56+ let config: ConfigFile = toml:: from_str ( & content) ?;
57+ Ok ( config)
58+ }
59+
3760const BANNER_LINES : & [ & str ] = & [
3861 r" ╔═╗╦╔╗ ╔═╗╦═╗ ╔╗╔╔═╗╔╦╗╦ ╦╔═╗╦═╗╦╔═ " ,
3962 r" ╠╣ ║╠╩╗║╣ ╠╦╝ ║║║║╣ ║ ║║║║ ║╠╦╝╠╩╗ " ,
@@ -56,13 +79,19 @@ fn build_cli() -> Command {
5679 . about ( "Fiber Network Node CLI - interactive command-line interface for FNN" )
5780 . version ( FNN_CLI_VERSION )
5881 . styles ( cli_styles ( ) )
82+ . arg (
83+ Arg :: new ( "config" )
84+ . long ( "config" )
85+ . global ( true )
86+ . value_name ( "FILE" )
87+ . help ( "Path to config file (default: ~/.fnn/config.toml)" ) ,
88+ )
5989 . arg (
6090 Arg :: new ( "url" )
6191 . long ( "url" )
6292 . short ( 'u' )
6393 . global ( true )
64- . default_value ( DEFAULT_RPC_URL )
65- . help ( "The RPC endpoint URL" ) ,
94+ . help ( "The RPC endpoint URL (default: http://127.0.0.1:8227, can also set via FNN_RPC_URL env or config file)" ) ,
6695 )
6796 . arg (
6897 Arg :: new ( "raw_data" )
@@ -76,8 +105,7 @@ fn build_cli() -> Command {
76105 . long ( "output-format" )
77106 . short ( 'o' )
78107 . global ( true )
79- . default_value ( "yaml" )
80- . help ( "Output format: json or yaml" ) ,
108+ . help ( "Output format: json or yaml (default: yaml, can also set via config file)" ) ,
81109 )
82110 . arg (
83111 Arg :: new ( "no_banner" )
@@ -404,7 +432,7 @@ async fn run_interactive(
404432 . build ( ) ;
405433 let mut rl = Editor :: with_config ( config) ?;
406434 rl. set_helper ( Some ( helper) ) ;
407- let history_path = dirs_home ( ) . join ( ".fnn_cli_history " ) ;
435+ let history_path = default_config_dir ( ) . join ( "history " ) ;
408436 let _ = rl. load_history ( & history_path) ;
409437
410438 loop {
@@ -513,10 +541,6 @@ fn shell_words(input: &str) -> Result<Vec<String>> {
513541 Ok ( words)
514542}
515543
516- fn dirs_home ( ) -> std:: path:: PathBuf {
517- dirs_home_inner ( ) . unwrap_or_else ( || std:: path:: PathBuf :: from ( "." ) )
518- }
519-
520544fn dirs_home_inner ( ) -> Option < std:: path:: PathBuf > {
521545 #[ cfg( target_os = "windows" ) ]
522546 {
@@ -533,14 +557,61 @@ async fn main() -> Result<()> {
533557 let cli = build_cli ( ) ;
534558 let matches = cli. get_matches ( ) ;
535559
536- let url = matches. get_one :: < String > ( "url" ) . unwrap ( ) . clone ( ) ;
560+ // Load config file if exists (default: ~/.fnn/config.toml)
561+ // Priority: CLI arg > env > config file > default
562+ let config_file_path = matches
563+ . get_one :: < String > ( "config" )
564+ . map ( std:: path:: PathBuf :: from)
565+ . unwrap_or_else ( default_config_path) ;
566+
567+ let config_file = match load_config_file ( & config_file_path) {
568+ Ok ( cfg) => Some ( cfg) ,
569+ Err ( e)
570+ if e. downcast_ref :: < std:: io:: Error > ( )
571+ . map ( |e| e. kind ( ) == std:: io:: ErrorKind :: NotFound )
572+ . unwrap_or ( false ) =>
573+ {
574+ // Config file not found - this is fine, use defaults
575+ None
576+ }
577+ Err ( e) => {
578+ eprintln ! ( "[config] Failed to load from {:?}: {}" , config_file_path, e) ;
579+ None
580+ }
581+ } ;
582+
583+ // Resolve URL: CLI > env > config file > default
584+ let url = matches
585+ . get_one :: < String > ( "url" )
586+ . cloned ( )
587+ . or_else ( || std:: env:: var ( "FNN_RPC_URL" ) . ok ( ) )
588+ . or_else ( || config_file. as_ref ( ) ?. url . clone ( ) )
589+ . unwrap_or_else ( || DEFAULT_RPC_URL . to_string ( ) ) ;
590+
591+ // Ensure URL has http:// prefix
592+ let url = if url. starts_with ( "http://" ) || url. starts_with ( "https://" ) {
593+ url
594+ } else {
595+ format ! ( "http://{}" , url)
596+ } ;
597+
537598 let raw_data = matches. get_flag ( "raw_data" ) ;
538- let output_format = matches. get_one :: < String > ( "output_format" ) . unwrap ( ) . clone ( ) ;
599+
600+ // Resolve output_format: CLI > config file > default
601+ let output_format = matches
602+ . get_one :: < String > ( "output_format" )
603+ . cloned ( )
604+ . or_else ( || config_file. as_ref ( ) . and_then ( |c| c. output_format . clone ( ) ) )
605+ . unwrap_or_else ( || "yaml" . to_string ( ) ) ;
606+
539607 let no_banner = matches. get_flag ( "no_banner" ) ;
540- let color_mode = matches. get_one :: < String > ( "color" ) . unwrap ( ) . clone ( ) ;
608+ let color_mode = matches. get_one :: < String > ( "color" ) . cloned ( ) . unwrap ( ) ;
541609
542- // Resolve auth token: --auth-token > --auth-token-file > FNN_AUTH_TOKEN env
543- let auth_token = match matches. get_one :: < String > ( "auth_token" ) . cloned ( ) {
610+ // Resolve auth token: CLI > env > config file
611+ // Priority: --auth-token > --auth-token-file > FNN_AUTH_TOKEN env > config file
612+ let auth_token_from_cli = matches. get_one :: < String > ( "auth_token" ) . cloned ( ) ;
613+ let auth_token_from_file = config_file. as_ref ( ) . and_then ( |c| c. auth_token . clone ( ) ) ;
614+ let auth_token = match auth_token_from_cli {
544615 Some ( token) => Some ( token) ,
545616 None => match matches. get_one :: < String > ( "auth_token_file" ) {
546617 Some ( path) => {
@@ -549,7 +620,7 @@ async fn main() -> Result<()> {
549620 } ) ?;
550621 Some ( content)
551622 }
552- None => None ,
623+ None => auth_token_from_file ,
553624 } ,
554625 } ;
555626
0 commit comments