#![allow( clippy::exit, reason = "The CLI exits explicitly with the computed process status." )] mod args; mod command_context; mod commands; mod gh; mod landing; mod local_server; mod logging; #[allow( unreachable_pub, reason = "The library exports manifest builder helpers for tests; the binary includes the same module privately." )] mod manifest_builder; mod server_client; mod server_runs; mod shared; #[cfg(feature = "sleep_inhibitor")] mod sleep_inhibitor; mod user_config; use std::fmt::{self, Debug, Display}; use anyhow::Result; use args::{ Cli, Commands, RunCommands, ServerCommand, ServerNamespace, global_args_cli_layer, require_no_json_override, }; use clap::CommandFactory; use fabro_static::EnvVars; use fabro_telemetry::{git, panic as tel_panic, sanitize, sender}; use fabro_types::settings::LogDestination; use fabro_util::exit::ExitClass; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use fabro_util::{browser, exit}; use rustls::crypto::ring::default_provider; use tracing::debug; use crate::command_context::CommandContext; #[expect(clippy::print_stderr, reason = "fatal error reporting before exit")] #[tokio::main] async fn main() { let raw_args: Vec = std::env::args().collect(); let subcommand = raw_args.get(1).map(String::as_str); let subcommand_arg = raw_args.get(2).map(String::as_str); if subcommand == Some("__cli-reference") && !matches!(subcommand_arg, Some("--help" | "-h")) { std::process::exit(commands::cli_reference::execute()); } if subcommand == Some("__render-graph") && !matches!(subcommand_arg, Some("--help" | "-h")) { std::process::exit(commands::render_graph::execute()); } // Capture the worker bearer token immediately and scrub it from the process // env before any subprocess can be spawned. Every descendant of the worker // (hooks, sandbox commands, devcontainer setup, MCP stdio, etc.) therefore // inherits a process env that no longer contains FABRO_WORKER_TOKEN, so an // unscrubbed spawn site cannot leak it. The token flows to `runner::execute` // through an explicit function argument instead of the environment. let worker_token = if subcommand == Some("__run-worker") { let token = process_env_var(EnvVars::FABRO_WORKER_TOKEN); #[expect( clippy::disallowed_methods, reason = "Scrub the worker bearer from this process's env before any \ child process is spawned, so no descendant can inherit it." )] { std::env::remove_var(EnvVars::FABRO_WORKER_TOKEN); } token } else { None }; install_miette_hook(); tel_panic::install_panic_hook(); fabro_telemetry::init_cli(); let start = std::time::Instant::now(); let (command_name, result) = Box::pin(main_inner(worker_token)).await; let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX); let exit_code = result.as_ref().err().map_or(0, exit::exit_code_for); let is_error = result.is_err(); // An empty command_name means no subcommand was invoked (landing was shown); // don't emit a tracking event for that case. if !command_name.is_empty() { let command = sanitize::sanitize_command(&raw_args, &command_name); let repository = git::repository_identifier(); let ci = process_env_var(EnvVars::CI).is_some(); if is_error { fabro_telemetry::track!("CLI Errored", { "subcommand": command_name, "command": command, "durationMs": duration_ms, "repository": repository, "ci": ci, "success": false, "exitCode": exit_code, }, error); } else { fabro_telemetry::track!("CLI Executed", { "subcommand": command_name, "command": command, "durationMs": duration_ms, "repository": repository, "ci": ci, "success": true, "exitCode": 0, }); } } fabro_telemetry::shutdown(); if let Err(err) = result { let json_mode = raw_args.iter().any(|a| a == "--json"); eprintln!( "{:?}", miette::Report::new(CliDiagnostic::new(err, !json_mode)) ); std::process::exit(exit_code); } } fn install_miette_hook() { let _ = miette::set_hook(Box::new(|_| { Box::new( miette::MietteHandlerOpts::new() .with_cause_chain() .wrap_lines(false) .break_words(false) .build(), ) })); } struct CliDiagnostic { err: anyhow::Error, show_auth_hint: bool, } impl CliDiagnostic { fn new(err: anyhow::Error, show_auth_hint: bool) -> Self { Self { err, show_auth_hint, } } } impl Display for CliDiagnostic { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { Display::fmt(&self.err, formatter) } } impl Debug for CliDiagnostic { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { Debug::fmt(&self.err, formatter) } } impl std::error::Error for CliDiagnostic { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { self.err.source() } } impl miette::Diagnostic for CliDiagnostic { fn help<'a>(&'a self) -> Option> { if self.show_auth_hint && exit::exit_class_for(&self.err) == Some(ExitClass::AuthRequired) { Some(Box::new("Run `fabro auth login` to authenticate.")) } else { None } } } #[expect( clippy::disallowed_methods, reason = "CLI main reads documented process-env controls before telemetry and worker dispatch." )] fn process_env_var(name: &str) -> Option { std::env::var(name).ok() } async fn main_inner(worker_token: Option) -> (String, Result<()>) { let _ = default_provider().install_default(); let cli = Cli::parse(); let Cli { globals, command } = cli; let Some(command) = command else { landing::print(); return (String::new(), Ok(())); }; let bootstrap_printer = Printer::from_flags(globals.quiet, globals.verbose); let cli_layer = global_args_cli_layer(&globals); let process_local_json = globals.json; let command_name = command.name().to_string(); let pre_tracing_bootstrap = match pre_tracing_bootstrap(command.as_ref()).await { Ok(bootstrap) => bootstrap, Err(err) => return (command_name, Err(err)), }; let base_ctx = match CommandContext::from_disk(&cli_layer, process_local_json) { Ok(ctx) => ctx, Err(err) => return (command_name, Err(err)), }; let printer = base_ctx.printer(); let config_log_level = match &pre_tracing_bootstrap.sink { logging::InternalLogSink::Cli => base_ctx.user_settings().cli.logging.level.clone(), logging::InternalLogSink::Server { .. } | logging::InternalLogSink::Worker { .. } => { pre_tracing_bootstrap.config_log_level.clone() } }; if let Err(err) = logging::init_tracing( globals.debug, config_log_level.as_deref(), &pre_tracing_bootstrap.sink, ) { fabro_util::printerr!( bootstrap_printer, "Warning: failed to initialize logging: {err:#}" ); } debug!(command = %command_name, "CLI command started"); let foreground_server_log_bootstrap = pre_tracing_bootstrap.foreground_server_log_bootstrap; let upgrade_handle = if matches!( command.as_ref(), Commands::RunCmd(RunCommands::Run(_) | RunCommands::Create(_)) | Commands::Exec(_) | Commands::Repo(_) | Commands::Install { .. } ) { commands::upgrade::spawn_upgrade_check(base_ctx.user_settings().cli.updates.check, printer) } else { None }; let result = Box::pin(async move { match *command { Commands::Exec(args) => { commands::exec::execute(args, &base_ctx).await?; } Commands::RunCmd(cmd) => { Box::pin(commands::run::dispatch(cmd, &base_ctx, worker_token)).await?; } Commands::Preflight(args) => { commands::preflight::execute(args, &base_ctx).await?; } Commands::Validate(args) => { let styles = Styles::detect_stderr(); commands::validate::run(&args, &styles, &base_ctx)?; } Commands::Graph(args) => { let styles = Styles::detect_stderr(); commands::graph::run(&args, &styles, &base_ctx).await?; } Commands::Parse(args) => { commands::parse::run(&args)?; } Commands::Artifact(ns) => { commands::artifact::dispatch(ns, &base_ctx).await?; } Commands::Dump(args) => { commands::dump::run(&args, &base_ctx).await?; } Commands::RunsCmd(cmd) => { commands::runs::dispatch(cmd, &base_ctx).await?; } Commands::Model { command } => { commands::model::execute(command, &base_ctx).await?; } Commands::Server(ns) => { Box::pin(commands::server::dispatch( ns.command, &globals, foreground_server_log_bootstrap, printer, )) .await?; } Commands::Doctor(args) => { let exit_code = Box::pin(commands::doctor::run_doctor(&args, &base_ctx)).await?; std::process::exit(exit_code); } Commands::Version(args) => { commands::version::version_command(&args, &base_ctx).await?; } Commands::Discord => { if process_local_json { shared::print_json_pretty(&serde_json::json!({ "url": "https://fabro.sh/discord", }))?; } else { browser::try_open("https://fabro.sh/discord")?; } } Commands::Docs => { if process_local_json { shared::print_json_pretty(&serde_json::json!({ "url": "https://docs.fabro.sh/", }))?; } else { browser::try_open("https://docs.fabro.sh/")?; } } Commands::Repo(ns) => { commands::repo::dispatch(ns, &base_ctx).await?; } Commands::Install { args, command } => { Box::pin(commands::install::execute(&args, command, &base_ctx)).await?; } Commands::Uninstall(args) => { commands::uninstall::run_uninstall(&args, &base_ctx).await?; } Commands::Auth(ns) => { commands::auth::dispatch(ns, &base_ctx).await?; } Commands::Pr(ns) => { Box::pin(commands::pr::dispatch(ns, &base_ctx)).await?; } Commands::Secret(ns) => { commands::secret::dispatch(ns, &base_ctx).await?; } Commands::Settings(args) => { Box::pin(commands::config::execute(&args, &base_ctx)).await?; } Commands::Workflow(ns) => { commands::workflow::dispatch(ns, &base_ctx)?; } Commands::Upgrade(args) => { commands::upgrade::run_upgrade(args, &base_ctx).await?; } Commands::Provider(ns) => { commands::provider::dispatch(ns, &base_ctx).await?; } Commands::Sandbox { command } => { commands::sandbox::dispatch(command, &base_ctx).await?; } Commands::System(ns) => { commands::system::dispatch(ns, &base_ctx).await?; } Commands::Completion(args) => { require_no_json_override(process_local_json)?; let mut cmd = Cli::command(); let shell = args.shell; let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let mut buf = Vec::new(); clap_complete::generate(shell, &mut cmd, "fabro", &mut buf); buf })); match result { Ok(buf) => { #[expect( clippy::disallowed_types, clippy::disallowed_methods, reason = "sync CLI: stream shell completions to stdout" )] { use std::io::Write; std::io::stdout().write_all(&buf)?; } } Err(_) => { anyhow::bail!( "Failed to generate completions for {shell}. \ Try zsh, fish, elvish, or powershell instead." ); } } } Commands::SendAnalytics { path } => { let result = sender::upload(&path).await; let _ = std::fs::remove_file(&path); result?; } Commands::SendPanic { path } => { let result = tel_panic::capture(&path); let _ = std::fs::remove_file(&path); result?; } Commands::CliReference => { unreachable!("__cli-reference handled before CLI bootstrap") } Commands::RenderGraph => unreachable!("__render-graph handled before CLI bootstrap"), #[cfg(debug_assertions)] Commands::TestPanic { message } => { let event = tel_panic::build_event(&message); let json = serde_json::to_string_pretty(&event)?; fabro_util::printout!(printer, "{json}"); } } Ok(()) }) .await; // Print upgrade notice after command completes (non-blocking during execution) if let Some(handle) = upgrade_handle { let _ = handle.await; } (command_name, result) } struct PreTracingBootstrap { sink: logging::InternalLogSink, config_log_level: Option, foreground_server_log_bootstrap: Option, } impl PreTracingBootstrap { fn cli() -> Self { Self { sink: logging::InternalLogSink::Cli, config_log_level: None, foreground_server_log_bootstrap: None, } } } async fn pre_tracing_bootstrap(command: &Commands) -> Result { match command { Commands::Server(ServerNamespace { command: ServerCommand::Start(args), }) if args.foreground => { prepare_server_bootstrap( args.serve_args.config.as_deref(), args.storage_dir.as_deref(), true, ) .await } Commands::Server(ServerNamespace { command: ServerCommand::Restart(args), }) if args.foreground => { prepare_server_bootstrap( args.serve_args.config.as_deref(), args.storage_dir.as_deref(), true, ) .await } Commands::Server(ServerNamespace { command: ServerCommand::Serve(args), }) => { prepare_server_bootstrap( args.serve_args.config.as_deref(), args.storage_dir.as_deref(), false, ) .await } Commands::RunCmd(RunCommands::RunWorker(args)) => { prepare_run_worker_bootstrap(args.storage_dir.as_deref(), &args.run_dir) } _ => Ok(PreTracingBootstrap::cli()), } } async fn prepare_server_bootstrap( config_path: Option<&std::path::Path>, storage_dir: Option<&std::path::Path>, foreground: bool, ) -> Result { let local_config = local_server::LocalServerConfig::load(config_path, storage_dir)?; let storage_dir = local_config.storage_dir().to_path_buf(); let runtime_directory = fabro_config::RuntimeDirectory::new(storage_dir.clone()); let log_destination = fabro_config::resolve_log_destination( local_config.config_log_destination().unwrap_or_default(), )?; let foreground_server_log_bootstrap = if foreground { Some( commands::server::start::prepare_foreground_server_log( &runtime_directory, log_destination, ) .await?, ) } else { None }; let log = log_sink(log_destination, &runtime_directory); Ok(PreTracingBootstrap { sink: logging::InternalLogSink::Server { log }, config_log_level: local_config.config_log_level().map(str::to_owned), foreground_server_log_bootstrap, }) } fn prepare_run_worker_bootstrap( storage_dir: Option<&std::path::Path>, run_dir: &std::path::Path, ) -> Result { let local_config = local_server::LocalServerConfig::load_with_storage_dir(storage_dir)?; let runtime_directory = fabro_config::RuntimeDirectory::new(local_config.storage_dir()); let log_destination = fabro_config::resolve_log_destination( local_config.config_log_destination().unwrap_or_default(), )?; let server_log = log_sink(log_destination, &runtime_directory); Ok(PreTracingBootstrap { sink: logging::InternalLogSink::Worker { server_log, per_run_log_path: run_dir.join("runtime").join("server.log"), }, config_log_level: None, foreground_server_log_bootstrap: None, }) } fn log_sink( destination: LogDestination, runtime_directory: &fabro_config::RuntimeDirectory, ) -> logging::LogSink { match destination { LogDestination::File => logging::LogSink::File(runtime_directory.log_path()), LogDestination::Stdout => logging::LogSink::Stdout, } } #[cfg(test)] #[expect( clippy::disallowed_methods, reason = "main.rs tests stage CLI settings fixtures with sync std::fs::write" )] mod tests { use args::{ AuthCommand, AuthNamespace, Commands, InstallGitHubStrategyArg, ModelsCommand, ProviderCommand, ProviderNamespace, }; use temp_env::with_var; use tokio::runtime::Runtime; use super::*; fn runtime() -> Runtime { Runtime::new().expect("runtime should build") } fn write_test_settings(path: &std::path::Path) { write_test_settings_with_logging(path, "warn", "stdout"); } fn write_test_settings_with_logging(path: &std::path::Path, level: &str, destination: &str) { std::fs::write( path, format!( r#" _version = 1 [server.logging] level = "{level}" destination = "{destination}" "# ), ) .unwrap(); } fn expect_bootstrap_err(result: Result) -> anyhow::Error { match result { Ok(_) => panic!("bootstrap should have failed"), Err(err) => err, } } #[test] fn pre_tracing_bootstrap_uses_cli_sink_for_normal_cli_command() { let cli = Cli::try_parse_from(["fabro", "uninstall"]).expect("should parse"); let command = cli.command.as_deref().unwrap(); let bootstrap = runtime() .block_on(pre_tracing_bootstrap(command)) .expect("bootstrap should resolve"); assert_eq!(bootstrap.sink, logging::InternalLogSink::Cli); assert!(bootstrap.config_log_level.is_none()); assert!(bootstrap.foreground_server_log_bootstrap.is_none()); } #[test] fn parse_provider_login_openai() { let cli = Cli::try_parse_from(["fabro", "provider", "login", "--provider", "openai"]) .expect("should parse"); match *cli.command.unwrap() { Commands::Provider(ProviderNamespace { command: ProviderCommand::Login(args), }) => { assert_eq!(args.provider, fabro_model::Provider::OpenAi); } _ => panic!("unexpected command variant"), } } #[test] fn parse_provider_login_anthropic() { let cli = Cli::try_parse_from(["fabro", "provider", "login", "--provider", "anthropic"]) .expect("should parse"); match *cli.command.unwrap() { Commands::Provider(ProviderNamespace { command: ProviderCommand::Login(args), }) => { assert_eq!(args.provider, fabro_model::Provider::Anthropic); } _ => panic!("unexpected command variant"), } } #[test] fn parse_provider_login_api_key_stdin() { let cli = Cli::try_parse_from([ "fabro", "provider", "login", "--provider", "anthropic", "--api-key-stdin", ]) .expect("should parse"); match *cli.command.unwrap() { Commands::Provider(ProviderNamespace { command: ProviderCommand::Login(args), }) => { assert_eq!(args.provider, fabro_model::Provider::Anthropic); assert!(args.api_key_stdin); } _ => panic!("unexpected command variant"), } } #[test] fn parse_auth_logout_all() { let cli = Cli::try_parse_from(["fabro", "auth", "logout", "--all"]).expect("should parse"); match *cli.command.unwrap() { Commands::Auth(AuthNamespace { command: AuthCommand::Logout(args), }) => { assert!(args.all); assert!(args.server.server.is_none()); } _ => panic!("unexpected command variant"), } } #[test] fn parse_install_non_interactive_accepts_token_strategy() { let cli = Cli::try_parse_from([ "fabro", "install", "--non-interactive", "--llm-provider", "anthropic", "--llm-api-key-env", "ANTHROPIC_API_KEY", "--github-strategy", "token", "--github-username", "brynary", ]) .expect("should parse"); match *cli.command.unwrap() { Commands::Install { args, command: None, } => { assert!(args.non_interactive); assert_eq!( args.scripted.github_strategy, Some(InstallGitHubStrategyArg::Token) ); } _ => panic!("unexpected command variant"), } } #[test] fn parse_install_github_non_interactive_accepts_token_strategy() { let cli = Cli::try_parse_from([ "fabro", "install", "github", "--non-interactive", "--strategy", "token", ]) .expect("should parse"); match *cli.command.unwrap() { Commands::Install { args, command: Some(args::InstallCommand::Github(github_args)), } => { assert!(args.non_interactive); assert_eq!(github_args.strategy, Some(InstallGitHubStrategyArg::Token)); assert_eq!(github_args.owner, None); } _ => panic!("unexpected command variant"), } } #[test] fn pre_tracing_bootstrap_uses_server_sink_for_server_start_foreground() { let storage_dir = tempfile::tempdir().unwrap(); let config_dir = tempfile::tempdir().unwrap(); let config_path = config_dir.path().join("settings.toml"); write_test_settings(&config_path); let cli = Cli::try_parse_from([ "fabro", "server", "start", "--foreground", "--storage-dir", storage_dir.path().to_str().unwrap(), "--config", config_path.to_str().unwrap(), ]) .expect("should parse"); let command = cli.command.as_deref().unwrap(); let bootstrap = runtime() .block_on(pre_tracing_bootstrap(command)) .expect("bootstrap should resolve"); assert_eq!(bootstrap.sink, logging::InternalLogSink::Server { log: logging::LogSink::Stdout, }); assert_eq!(bootstrap.config_log_level.as_deref(), Some("warn")); assert!(bootstrap.foreground_server_log_bootstrap.is_some()); } #[test] fn pre_tracing_bootstrap_uses_server_sink_for_server_restart_foreground() { let storage_dir = tempfile::tempdir().unwrap(); let config_dir = tempfile::tempdir().unwrap(); let config_path = config_dir.path().join("settings.toml"); write_test_settings(&config_path); let cli = Cli::try_parse_from([ "fabro", "server", "restart", "--foreground", "--storage-dir", storage_dir.path().to_str().unwrap(), "--config", config_path.to_str().unwrap(), ]) .expect("should parse"); let command = cli.command.as_deref().unwrap(); let bootstrap = runtime() .block_on(pre_tracing_bootstrap(command)) .expect("bootstrap should resolve"); assert_eq!(bootstrap.sink, logging::InternalLogSink::Server { log: logging::LogSink::Stdout, }); assert_eq!(bootstrap.config_log_level.as_deref(), Some("warn")); assert!(bootstrap.foreground_server_log_bootstrap.is_some()); } #[test] fn pre_tracing_bootstrap_uses_server_sink_for_server_serve() { let storage_dir = tempfile::tempdir().unwrap(); let config_dir = tempfile::tempdir().unwrap(); let config_path = config_dir.path().join("settings.toml"); write_test_settings(&config_path); let cli = Cli::try_parse_from([ "fabro", "server", "__serve", "--storage-dir", storage_dir.path().to_str().unwrap(), "--config", config_path.to_str().unwrap(), ]) .expect("should parse"); let command = cli.command.as_deref().unwrap(); let bootstrap = runtime() .block_on(pre_tracing_bootstrap(command)) .expect("bootstrap should resolve"); assert_eq!(bootstrap.sink, logging::InternalLogSink::Server { log: logging::LogSink::Stdout, }); assert_eq!(bootstrap.config_log_level.as_deref(), Some("warn")); assert!(bootstrap.foreground_server_log_bootstrap.is_none()); } #[test] fn pre_tracing_bootstrap_env_destination_overrides_config_file() { let storage_dir = tempfile::tempdir().unwrap(); let config_dir = tempfile::tempdir().unwrap(); let config_path = config_dir.path().join("settings.toml"); write_test_settings_with_logging(&config_path, "warn", "file"); let cli = Cli::try_parse_from([ "fabro", "server", "start", "--foreground", "--storage-dir", storage_dir.path().to_str().unwrap(), "--config", config_path.to_str().unwrap(), ]) .expect("should parse"); let command = cli.command.as_deref().unwrap(); with_var(EnvVars::FABRO_LOG_DESTINATION, Some("stdout"), || { let bootstrap = runtime() .block_on(pre_tracing_bootstrap(command)) .expect("bootstrap should resolve"); assert_eq!(bootstrap.sink, logging::InternalLogSink::Server { log: logging::LogSink::Stdout, }); }); } #[test] fn pre_tracing_bootstrap_rejects_invalid_env_destination() { let storage_dir = tempfile::tempdir().unwrap(); let config_dir = tempfile::tempdir().unwrap(); let config_path = config_dir.path().join("settings.toml"); write_test_settings_with_logging(&config_path, "warn", "file"); let cli = Cli::try_parse_from([ "fabro", "server", "start", "--foreground", "--storage-dir", storage_dir.path().to_str().unwrap(), "--config", config_path.to_str().unwrap(), ]) .expect("should parse"); let command = cli.command.as_deref().unwrap(); with_var(EnvVars::FABRO_LOG_DESTINATION, Some("stdot"), || { let err = expect_bootstrap_err(runtime().block_on(pre_tracing_bootstrap(command))); let message = err.to_string(); assert!(message.contains(EnvVars::FABRO_LOG_DESTINATION)); assert!(message.contains("stdot")); }); } #[test] fn pre_tracing_bootstrap_rejects_invalid_config_log_level() { let storage_dir = tempfile::tempdir().unwrap(); let config_dir = tempfile::tempdir().unwrap(); let config_path = config_dir.path().join("settings.toml"); write_test_settings_with_logging(&config_path, "definitely not a filter", "file"); let cli = Cli::try_parse_from([ "fabro", "server", "start", "--foreground", "--storage-dir", storage_dir.path().to_str().unwrap(), "--config", config_path.to_str().unwrap(), ]) .expect("should parse"); let command = cli.command.as_deref().unwrap(); let err = expect_bootstrap_err(runtime().block_on(pre_tracing_bootstrap(command))); assert!( err.to_string().contains("server.logging.level"), "unexpected error: {err}" ); } #[test] fn pre_tracing_bootstrap_rejects_invalid_config_destination() { let storage_dir = tempfile::tempdir().unwrap(); let config_dir = tempfile::tempdir().unwrap(); let config_path = config_dir.path().join("settings.toml"); write_test_settings_with_logging(&config_path, "warn", "stdot"); let cli = Cli::try_parse_from([ "fabro", "server", "start", "--foreground", "--storage-dir", storage_dir.path().to_str().unwrap(), "--config", config_path.to_str().unwrap(), ]) .expect("should parse"); let command = cli.command.as_deref().unwrap(); let err = expect_bootstrap_err(runtime().block_on(pre_tracing_bootstrap(command))); assert!( err.to_string().contains("server.logging.destination"), "unexpected error: {err}" ); } #[test] fn pre_tracing_bootstrap_uses_worker_sink_for_run_worker() { let storage_dir = tempfile::tempdir().unwrap(); let run_dir = tempfile::tempdir().unwrap(); let cli = Cli::try_parse_from([ "fabro", "__run-worker", "--server", "/tmp/fabro.sock", "--storage-dir", storage_dir.path().to_str().unwrap(), "--run-dir", run_dir.path().to_str().unwrap(), "--run-id", "01ARZ3NDEKTSV4RRFFQ69G5FAV", "--mode", "start", ]) .expect("should parse"); let command = cli.command.as_deref().unwrap(); let bootstrap = runtime() .block_on(pre_tracing_bootstrap(command)) .expect("bootstrap should resolve"); assert_eq!(bootstrap.sink, logging::InternalLogSink::Worker { server_log: logging::LogSink::File( storage_dir.path().join("logs").join("server.log"), ), per_run_log_path: run_dir.path().join("runtime").join("server.log"), }); assert!(bootstrap.config_log_level.is_none()); assert!(bootstrap.foreground_server_log_bootstrap.is_none()); } #[test] fn pre_tracing_bootstrap_worker_uses_stdout_when_env_overrides() { let storage_dir = tempfile::tempdir().unwrap(); let run_dir = tempfile::tempdir().unwrap(); let cli = Cli::try_parse_from([ "fabro", "__run-worker", "--server", "/tmp/fabro.sock", "--storage-dir", storage_dir.path().to_str().unwrap(), "--run-dir", run_dir.path().to_str().unwrap(), "--run-id", "01ARZ3NDEKTSV4RRFFQ69G5FAV", "--mode", "start", ]) .expect("should parse"); let command = cli.command.as_deref().unwrap(); with_var(EnvVars::FABRO_LOG_DESTINATION, Some("stdout"), || { let bootstrap = runtime() .block_on(pre_tracing_bootstrap(command)) .expect("bootstrap should resolve"); assert_eq!(bootstrap.sink, logging::InternalLogSink::Worker { server_log: logging::LogSink::Stdout, per_run_log_path: run_dir.path().join("runtime").join("server.log"), }); }); } #[test] fn pre_tracing_bootstrap_uses_cli_sink_for_server_start_daemon_wrapper() { let storage_dir = tempfile::tempdir().unwrap(); let config_dir = tempfile::tempdir().unwrap(); let config_path = config_dir.path().join("settings.toml"); write_test_settings(&config_path); let cli = Cli::try_parse_from([ "fabro", "server", "start", "--storage-dir", storage_dir.path().to_str().unwrap(), "--config", config_path.to_str().unwrap(), ]) .expect("should parse"); let command = cli.command.as_deref().unwrap(); let bootstrap = runtime() .block_on(pre_tracing_bootstrap(command)) .expect("bootstrap should resolve"); assert_eq!(bootstrap.sink, logging::InternalLogSink::Cli); assert!(bootstrap.config_log_level.is_none()); assert!(bootstrap.foreground_server_log_bootstrap.is_none()); } #[test] fn pre_tracing_bootstrap_uses_cli_sink_for_server_restart_daemon_wrapper() { let storage_dir = tempfile::tempdir().unwrap(); let config_dir = tempfile::tempdir().unwrap(); let config_path = config_dir.path().join("settings.toml"); write_test_settings(&config_path); let cli = Cli::try_parse_from([ "fabro", "server", "restart", "--storage-dir", storage_dir.path().to_str().unwrap(), "--config", config_path.to_str().unwrap(), ]) .expect("should parse"); let command = cli.command.as_deref().unwrap(); let bootstrap = runtime() .block_on(pre_tracing_bootstrap(command)) .expect("bootstrap should resolve"); assert_eq!(bootstrap.sink, logging::InternalLogSink::Cli); assert!(bootstrap.config_log_level.is_none()); assert!(bootstrap.foreground_server_log_bootstrap.is_none()); } #[test] fn parse_provider_login_missing_provider_flag() { let result = Cli::try_parse_from(["fabro", "provider", "login"]); assert!(result.is_err(), "should fail without --provider"); } #[test] fn parse_provider_login_bogus_provider() { let result = Cli::try_parse_from(["fabro", "provider", "login", "--provider", "bogus"]); assert!(result.is_err(), "should fail with unknown provider"); } #[test] fn parse_create_command() { let cli = Cli::try_parse_from(["fabro", "create", "my-workflow.toml", "--goal", "test"]) .expect("should parse"); match *cli.command.unwrap() { Commands::RunCmd(RunCommands::Create(args)) => { assert_eq!( args.workflow.as_deref(), Some(std::path::Path::new("my-workflow.toml")) ); assert_eq!(args.goal.as_deref(), Some("test")); } _ => panic!("unexpected command variant"), } } #[test] fn parse_run_storage_dir_after_subcommand_is_rejected() { let result = Cli::try_parse_from([ "fabro", "run", "test/simple.fabro", "--storage-dir", "/tmp/fabro", ]); assert!(result.is_err(), "should reject run --storage-dir"); } #[test] fn parse_model_list_server_target_after_subcommand() { let cli = Cli::try_parse_from([ "fabro", "model", "list", "--server", "http://localhost:3000/api/v1", ]) .expect("should parse"); match *cli.command.unwrap() { Commands::Model { command: Some(ModelsCommand::List(args)), } => assert_eq!(args.target.as_deref(), Some("http://localhost:3000/api/v1")), _ => panic!("unexpected command variant"), } } #[test] fn parse_exec_server_target_after_subcommand() { let cli = Cli::try_parse_from([ "fabro", "exec", "--server", "http://localhost:3000/api/v1", "fix the bug", ]) .expect("should parse"); match *cli.command.unwrap() { Commands::Exec(args) => { assert_eq!(args.server.as_deref(), Some("http://localhost:3000/api/v1")); } _ => panic!("unexpected command variant"), } } #[test] fn parse_model_server_target_conflicts_with_storage_dir() { let result = Cli::try_parse_from([ "fabro", "model", "list", "--storage-dir", "/tmp/fabro", "--server", "http://localhost:3000", ]); assert!( result.is_err(), "should fail with conflicting model target flags" ); } #[test] fn parse_global_server_target_before_subcommand_is_rejected() { let result = Cli::try_parse_from([ "fabro", "--server", "http://localhost:3000/api/v1", "model", "list", ]); assert!(result.is_err(), "should reject top-level --server"); } #[test] fn parse_global_storage_dir_before_subcommand_is_rejected() { let result = Cli::try_parse_from([ "fabro", "--storage-dir", "/tmp/fabro", "run", "test/simple.fabro", ]); assert!(result.is_err(), "should reject top-level --storage-dir"); } #[test] fn parse_upgrade_prerelease_conflicts_with_version() { use clap::error::ErrorKind; let err = Cli::try_parse_from(["fabro", "upgrade", "--prerelease", "--version", "0.1.0"]) .err() .expect("should reject --prerelease combined with --version"); assert_eq!( err.kind(), ErrorKind::ArgumentConflict, "expected ArgumentConflict, got {:?}: {err}", err.kind() ); } #[test] fn parse_dump_command() { let cli = Cli::try_parse_from(["fabro", "dump", "ABC123", "-o", "./out"]).expect("should parse"); match *cli.command.unwrap() { Commands::Dump(args) => { assert_eq!(args.run, "ABC123"); assert_eq!(args.output, std::path::PathBuf::from("./out")); } _ => panic!("unexpected command variant"), } } #[test] fn parse_start_command() { let cli = Cli::try_parse_from(["fabro", "start", "ABC123"]).expect("should parse"); match *cli.command.unwrap() { Commands::RunCmd(RunCommands::Start(args)) => { assert_eq!(args.run, "ABC123"); } _ => panic!("unexpected command variant"), } } #[test] fn parse_attach_command() { let cli = Cli::try_parse_from(["fabro", "attach", "ABC123"]).expect("should parse"); match *cli.command.unwrap() { Commands::RunCmd(RunCommands::Attach(args)) => { assert_eq!(args.run, "ABC123"); } _ => panic!("unexpected command variant"), } } #[test] fn parse_sandbox_cp_command() { let cli = Cli::try_parse_from(["fabro", "sandbox", "cp", "ABC123:/tmp/file", "./file"]) .expect("should parse"); match *cli.command.unwrap() { Commands::Sandbox { command: args::SandboxCommand::Cp(args), } => { assert_eq!(args.src, "ABC123:/tmp/file"); assert_eq!(args.dst, "./file"); assert!(!args.recursive); } _ => panic!("unexpected command variant"), } } #[test] fn parse_run_worker_command() { let cli = Cli::try_parse_from([ "fabro", "__run-worker", "--server", "/tmp/fabro.sock", "--run-dir", "/tmp/run", "--run-id", "01ARZ3NDEKTSV4RRFFQ69G5FAV", "--mode", "start", ]) .expect("should parse"); match *cli.command.unwrap() { Commands::RunCmd(RunCommands::RunWorker(args)) => { assert_eq!(args.server, "/tmp/fabro.sock"); assert_eq!(args.run_dir, std::path::PathBuf::from("/tmp/run")); assert_eq!(args.run_id, "01ARZ3NDEKTSV4RRFFQ69G5FAV".parse().unwrap()); assert!(matches!(args.mode, args::RunWorkerMode::Start)); } _ => panic!("unexpected command variant"), } } #[test] fn parse_run_worker_with_resume_mode() { let cli = Cli::try_parse_from([ "fabro", "__run-worker", "--server", "http://127.0.0.1:3000", "--run-dir", "/tmp/run", "--run-id", "01ARZ3NDEKTSV4RRFFQ69G5FAV", "--mode", "resume", ]) .expect("should parse"); match *cli.command.unwrap() { Commands::RunCmd(RunCommands::RunWorker(args)) => { assert_eq!(args.server, "http://127.0.0.1:3000"); assert_eq!(args.run_dir, std::path::PathBuf::from("/tmp/run")); assert_eq!(args.run_id, "01ARZ3NDEKTSV4RRFFQ69G5FAV".parse().unwrap()); assert!(matches!(args.mode, args::RunWorkerMode::Resume)); } _ => panic!("unexpected command variant"), } } #[test] fn parse_render_graph_command() { let cli = Cli::try_parse_from(["fabro", "__render-graph"]).expect("should parse"); match *cli.command.unwrap() { Commands::RenderGraph => {} _ => panic!("unexpected command variant"), } } #[test] fn parse_cli_reference_command() { let cli = Cli::try_parse_from(["fabro", "__cli-reference"]).expect("should parse"); match *cli.command.unwrap() { Commands::CliReference => {} _ => panic!("unexpected command variant"), } } #[test] fn parse_settings_command() { let cli = Cli::try_parse_from(["fabro", "settings"]).expect("should parse"); assert_eq!(cli.command.as_ref().unwrap().name(), "settings"); match *cli.command.unwrap() { Commands::Settings(args) => { assert!(args.target.server.is_none()); } _ => panic!("unexpected command variant"), } } #[test] fn parse_settings_rejects_workflow_argument() { let result = Cli::try_parse_from(["fabro", "settings", "demo"]); assert!(result.is_err(), "should reject settings workflow argument"); } #[test] fn parse_settings_rejects_local_flag() { let result = Cli::try_parse_from(["fabro", "settings", "--local"]); assert!(result.is_err(), "should reject settings --local"); } #[test] fn parse_quiet_flag() { let cli = Cli::try_parse_from(["fabro", "--quiet", "settings"]).expect("should parse"); assert!(cli.globals.quiet); assert!(!cli.globals.verbose); } #[test] fn parse_verbose_flag() { let cli = Cli::try_parse_from(["fabro", "--verbose", "settings"]).expect("should parse"); assert!(!cli.globals.quiet); assert!(cli.globals.verbose); } #[test] fn quiet_and_verbose_conflict() { let result = Cli::try_parse_from(["fabro", "--quiet", "--verbose", "settings"]); assert!( result.is_err(), "should fail when both --quiet and --verbose" ); } }