diff --git a/lib/crates/fabro-cli/src/args.rs b/lib/crates/fabro-cli/src/args.rs index 6295c0012..d6f665a78 100644 --- a/lib/crates/fabro-cli/src/args.rs +++ b/lib/crates/fabro-cli/src/args.rs @@ -3,6 +3,10 @@ use std::path::{Path, PathBuf}; use clap::{Args, Subcommand, ValueEnum}; use fabro_agent::cli::AgentArgs; +use fabro_types::settings::cli::{ + CliLayer, CliLoggingLayer, CliOutputLayer, CliUpdatesLayer, OutputFormat, OutputVerbosity, +}; +use fabro_util::printer::Printer; pub(crate) const LONG_VERSION: &str = concat!( env!("CARGO_PKG_VERSION"), @@ -36,13 +40,46 @@ pub(crate) struct GlobalArgs { pub verbose: bool, } -impl GlobalArgs { - pub(crate) fn require_no_json(&self) -> anyhow::Result<()> { - anyhow::ensure!(!self.json, "--json is not supported for this command"); - Ok(()) +pub(crate) fn global_args_cli_layer(globals: &GlobalArgs) -> CliLayer { + let format = globals.json.then_some(OutputFormat::Json); + let verbosity = match (globals.quiet, globals.verbose) { + (true, _) => Some(OutputVerbosity::Quiet), + (_, true) => Some(OutputVerbosity::Verbose), + _ => None, + }; + let output = + (format.is_some() || verbosity.is_some()).then_some(CliOutputLayer { format, verbosity }); + let updates = globals + .no_upgrade_check + .then_some(CliUpdatesLayer { check: Some(false) }); + let logging = globals.debug.then_some(CliLoggingLayer { + level: Some("debug".to_string()), + }); + + CliLayer { + output, + updates, + logging, + ..CliLayer::default() } } +pub(crate) fn printer_from_verbosity(verbosity: OutputVerbosity) -> Printer { + match verbosity { + OutputVerbosity::Quiet => Printer::Quiet, + OutputVerbosity::Normal => Printer::Default, + OutputVerbosity::Verbose => Printer::Verbose, + } +} + +pub(crate) fn require_no_json_override(process_local_json: bool) -> anyhow::Result<()> { + anyhow::ensure!( + !process_local_json, + "--json is not supported for this command" + ); + Ok(()) +} + #[derive(Args, Debug, Clone, Default)] pub(crate) struct StorageDirArgs { /// Local storage directory (default: ~/.fabro/storage) diff --git a/lib/crates/fabro-cli/src/command_context.rs b/lib/crates/fabro-cli/src/command_context.rs index 9ddc24d86..2361e8f36 100644 --- a/lib/crates/fabro-cli/src/command_context.rs +++ b/lib/crates/fabro-cli/src/command_context.rs @@ -2,6 +2,8 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use anyhow::{Context as _, Result, bail}; +use fabro_config::merge::combine_files; +use fabro_types::settings::cli::CliLayer; use fabro_types::settings::{CliSettings, SettingsLayer}; use fabro_util::printer::Printer; use tokio::sync::OnceCell; @@ -34,34 +36,66 @@ pub(crate) struct CommandContext { } impl CommandContext { - pub(crate) fn base(printer: Printer) -> Result { - Self::new(printer, ServerMode::None) + pub(crate) fn base( + printer: Printer, + cli_settings: CliSettings, + cli_layer: &CliLayer, + ) -> Result { + Self::new(printer, ServerMode::None, cli_settings, cli_layer) } - pub(crate) fn for_target(args: &ServerTargetArgs, printer: Printer) -> Result { - Self::new(printer, ServerMode::ByTarget { - target_override: args.server.clone(), - }) + pub(crate) fn for_target( + args: &ServerTargetArgs, + printer: Printer, + cli_settings: CliSettings, + cli_layer: &CliLayer, + ) -> Result { + Self::new( + printer, + ServerMode::ByTarget { + target_override: args.server.clone(), + }, + cli_settings, + cli_layer, + ) } - pub(crate) fn for_connection(args: &ServerConnectionArgs, printer: Printer) -> Result { - Self::new(printer, ServerMode::ByStorageDir { - target_override: args.target.server.clone(), - storage_dir_override: args.storage_dir.clone_path(), - }) + pub(crate) fn for_connection( + args: &ServerConnectionArgs, + printer: Printer, + cli_settings: CliSettings, + cli_layer: &CliLayer, + ) -> Result { + Self::new( + printer, + ServerMode::ByStorageDir { + target_override: args.target.server.clone(), + storage_dir_override: args.storage_dir.clone_path(), + }, + cli_settings, + cli_layer, + ) } - fn new(printer: Printer, server_mode: ServerMode) -> Result { + fn new( + printer: Printer, + server_mode: ServerMode, + cli_settings: CliSettings, + cli_layer: &CliLayer, + ) -> Result { let cwd = std::env::current_dir().context("Failed to get current directory")?; let base_config_path = user_config::active_settings_path(None); - let machine_settings = match &server_mode { + let disk_settings = match &server_mode { ServerMode::None | ServerMode::ByTarget { .. } => user_config::load_settings()?, ServerMode::ByStorageDir { storage_dir_override, .. } => user_config::load_settings_with_storage_dir(storage_dir_override.as_deref())?, }; - let cli_settings = user_config::resolve_cli_settings(&machine_settings)?; + let machine_settings = combine_files(disk_settings, SettingsLayer { + cli: Some(cli_layer.clone()), + ..SettingsLayer::default() + }); Ok(Self { printer, diff --git a/lib/crates/fabro-cli/src/commands/artifact/cp.rs b/lib/crates/fabro-cli/src/commands/artifact/cp.rs index 03c633cea..dba10bd53 100644 --- a/lib/crates/fabro-cli/src/commands/artifact/cp.rs +++ b/lib/crates/fabro-cli/src/commands/artifact/cp.rs @@ -1,15 +1,18 @@ use std::path::{Path, PathBuf}; use anyhow::{Context, Result, bail}; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; -use crate::args::{ArtifactCpArgs, GlobalArgs}; +use crate::args::ArtifactCpArgs; use crate::server_client::ServerStoreClient; use crate::shared::{print_json_pretty, split_run_path}; pub(super) async fn cp_command( args: &ArtifactCpArgs, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { let (run_id_selector, asset_path) = parse_source(&args.source); @@ -18,6 +21,8 @@ pub(super) async fn cp_command( run_id_selector, args.node.as_deref(), args.retry, + cli, + cli_layer, printer, ) .await?; @@ -55,7 +60,7 @@ pub(super) async fn cp_command( .unwrap_or_else(|| std::ffi::OsStr::new(&entry.relative_path)), ); write_artifact_file(&client, &run_id, entry, &dest_file).await?; - if globals.json { + if cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "copied": [{ "relative_path": entry.relative_path, @@ -116,7 +121,7 @@ pub(super) async fn cp_command( } } - if globals.json { + if cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "copied": copied }))?; } else { fabro_util::printout!( diff --git a/lib/crates/fabro-cli/src/commands/artifact/list.rs b/lib/crates/fabro-cli/src/commands/artifact/list.rs index 9b51564ed..fefe8b61c 100644 --- a/lib/crates/fabro-cli/src/commands/artifact/list.rs +++ b/lib/crates/fabro-cli/src/commands/artifact/list.rs @@ -1,14 +1,17 @@ use anyhow::Result; use cli_table::format::{Border, Justify, Separator}; use cli_table::{Cell, CellStruct, Style, Table}; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; -use crate::args::{ArtifactListArgs, GlobalArgs}; +use crate::args::ArtifactListArgs; pub(super) async fn list_command( args: &ArtifactListArgs, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { let (_run_id, _client, entries) = super::resolve_artifacts( @@ -16,11 +19,13 @@ pub(super) async fn list_command( &args.run_id, args.node.as_deref(), args.retry, + cli, + cli_layer, printer, ) .await?; - if globals.json { + if cli.output.format == OutputFormat::Json { fabro_util::printout!(printer, "{}", serde_json::to_string_pretty(&entries)?); return Ok(()); } diff --git a/lib/crates/fabro-cli/src/commands/artifact/mod.rs b/lib/crates/fabro-cli/src/commands/artifact/mod.rs index df9dc4563..930dc422f 100644 --- a/lib/crates/fabro-cli/src/commands/artifact/mod.rs +++ b/lib/crates/fabro-cli/src/commands/artifact/mod.rs @@ -2,10 +2,12 @@ mod cp; mod list; use anyhow::{Context, Result}; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::CliLayer; use fabro_types::{RunId, StageId}; use fabro_util::printer::Printer; -use crate::args::{ArtifactCommand, ArtifactNamespace, GlobalArgs, ServerTargetArgs}; +use crate::args::{ArtifactCommand, ArtifactNamespace, ServerTargetArgs}; use crate::command_context::CommandContext; use crate::server_client::ServerStoreClient; use crate::server_runs::ServerSummaryLookup; @@ -25,9 +27,11 @@ pub(super) async fn resolve_artifacts( run_selector: &str, node: Option<&str>, retry: Option, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result<(RunId, ServerStoreClient, Vec)> { - let ctx = CommandContext::for_target(server, printer)?; + let ctx = CommandContext::for_target(server, printer, cli.clone(), cli_layer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(run_selector)?; let run_id = run.run_id(); @@ -64,11 +68,12 @@ pub(super) async fn resolve_artifacts( pub(crate) async fn dispatch( ns: ArtifactNamespace, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { match ns.command { - ArtifactCommand::List(args) => list::list_command(&args, globals, printer).await, - ArtifactCommand::Cp(args) => cp::cp_command(&args, globals, printer).await, + ArtifactCommand::List(args) => list::list_command(&args, cli, cli_layer, printer).await, + ArtifactCommand::Cp(args) => cp::cp_command(&args, cli, cli_layer, printer).await, } } diff --git a/lib/crates/fabro-cli/src/commands/config/mod.rs b/lib/crates/fabro-cli/src/commands/config/mod.rs index 12a935589..90b0a8586 100644 --- a/lib/crates/fabro-cli/src/commands/config/mod.rs +++ b/lib/crates/fabro-cli/src/commands/config/mod.rs @@ -3,10 +3,11 @@ use std::path::Path; use fabro_config::effective_settings::{EffectiveSettingsLayers, EffectiveSettingsMode}; use fabro_config::{load_and_resolve, load_settings_project, project}; -use fabro_types::settings::SettingsLayer; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; +use fabro_types::settings::{CliSettings, SettingsLayer}; use fabro_util::printer::Printer; -use crate::args::{GlobalArgs, SettingsArgs}; +use crate::args::SettingsArgs; use crate::command_context::CommandContext; use crate::shared::print_json_pretty; use crate::user_config; @@ -76,9 +77,11 @@ fn strip_nulls(value: &mut serde_json::Value) { fn local_settings_value( args: &SettingsArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> anyhow::Result { - let base_ctx = CommandContext::base(printer)?; + let base_ctx = CommandContext::base(printer, cli.clone(), cli_layer)?; let layers = config_layers(&base_ctx, args.workflow.as_deref())?; let mut value = serde_json::to_value(load_and_resolve( layers, @@ -91,15 +94,17 @@ fn local_settings_value( async fn rendered_config( args: &SettingsArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> anyhow::Result { if args.local { - return local_settings_value(args, printer); + return local_settings_value(args, cli, cli_layer, printer); } if args.workflow.is_some() { anyhow::bail!("WORKFLOW requires --local; use `fabro settings --local WORKFLOW`"); } - let ctx = CommandContext::for_target(&args.target, printer)?; + let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?; ctx.server() .await? .retrieve_resolved_server_settings() @@ -108,11 +113,12 @@ async fn rendered_config( pub(crate) async fn execute( args: &SettingsArgs, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> anyhow::Result<()> { - let config = Box::pin(rendered_config(args, printer)).await?; - if globals.json { + let config = Box::pin(rendered_config(args, cli, cli_layer, printer)).await?; + if cli.output.format == OutputFormat::Json { print_json_pretty(&config)?; return Ok(()); } diff --git a/lib/crates/fabro-cli/src/commands/doctor.rs b/lib/crates/fabro-cli/src/commands/doctor.rs index bfe271407..4842f5148 100644 --- a/lib/crates/fabro-cli/src/commands/doctor.rs +++ b/lib/crates/fabro-cli/src/commands/doctor.rs @@ -7,6 +7,8 @@ use fabro_config::user::{ active_settings_path, legacy_old_user_config_path, legacy_server_config_path, legacy_user_config_path, }; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; pub(crate) use fabro_util::check_report::{ CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus, }; @@ -14,7 +16,7 @@ use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use fabro_util::version::FABRO_VERSION; -use crate::args::{DoctorArgs, GlobalArgs}; +use crate::args::DoctorArgs; use crate::command_context::CommandContext; use crate::shared::print_json_pretty; use crate::user_config; @@ -231,11 +233,13 @@ fn render_report(report: &CheckReport, styles: &Styles, verbose: bool, printer: pub(crate) async fn run_doctor( args: &DoctorArgs, verbose: bool, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result { let styles = Styles::detect_stdout(); - let spinner = if globals.json { + let json = cli.output.format == OutputFormat::Json; + let spinner = if json { None } else { let spinner = indicatif::ProgressBar::new_spinner(); @@ -290,7 +294,7 @@ pub(crate) async fn run_doctor( }], }; - let ctx = match CommandContext::for_target(&args.target, printer) { + let ctx = match CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer) { Ok(ctx) => ctx, Err(err) => { report.sections.push(CheckSection { @@ -311,7 +315,7 @@ pub(crate) async fn run_doctor( spinner.finish_and_clear(); } - if globals.json { + if json { print_json_pretty(&report)?; } else { render_report(&report, &styles, verbose, printer); @@ -341,7 +345,7 @@ pub(crate) async fn run_doctor( spinner.finish_and_clear(); } - if globals.json { + if json { print_json_pretty(&report)?; } else { render_report(&report, &styles, verbose, printer); @@ -370,7 +374,7 @@ pub(crate) async fn run_doctor( spinner.finish_and_clear(); } - if globals.json { + if json { print_json_pretty(&report)?; } else { render_report(&report, &styles, verbose, printer); @@ -411,7 +415,7 @@ pub(crate) async fn run_doctor( spinner.finish_and_clear(); } - if globals.json { + if json { print_json_pretty(&report)?; } else { render_report(&report, &styles, verbose, printer); diff --git a/lib/crates/fabro-cli/src/commands/exec.rs b/lib/crates/fabro-cli/src/commands/exec.rs index cf87fedab..28fe4b69f 100644 --- a/lib/crates/fabro-cli/src/commands/exec.rs +++ b/lib/crates/fabro-cli/src/commands/exec.rs @@ -6,12 +6,12 @@ use fabro_agent::cli::{OutputFormat, run_with_args, run_with_args_and_client}; use fabro_llm::client::Client; use fabro_llm::providers::FabroServerAdapter; use fabro_mcp::config::{McpServerSettings, McpTransport}; -use fabro_types::settings::InterpString; use fabro_types::settings::cli::OutputFormat as SettingsOutputFormat; use fabro_types::settings::run::McpEntryLayer; +use fabro_types::settings::{CliSettings, InterpString}; use fabro_util::printer::Printer; -use crate::args::{ExecArgs, GlobalArgs}; +use crate::args::ExecArgs; use crate::user_config; fn runtime_mcp_server(name: &str, entry: &McpEntryLayer) -> McpServerSettings { @@ -100,34 +100,28 @@ fn runtime_mcp_server(name: &str, entry: &McpEntryLayer) -> McpServerSettings { pub(crate) async fn execute( mut args: ExecArgs, - globals: &GlobalArgs, + cli: &CliSettings, _printer: Printer, ) -> Result<()> { use fabro_agent::cli::PermissionLevel as AgentPermissionLevel; use fabro_types::settings::run::AgentPermissions; - let cli_settings = user_config::load_settings()?; - let resolved_cli = user_config::resolve_cli_settings(&cli_settings)?; + let raw_settings = user_config::load_settings()?; #[cfg(feature = "sleep_inhibitor")] - let _sleep_guard = crate::sleep_inhibitor::guard(resolved_cli.exec.prevent_idle_sleep); - let provider_str = resolved_cli + let _sleep_guard = crate::sleep_inhibitor::guard(cli.exec.prevent_idle_sleep); + let provider_str = cli .exec .model .provider .as_ref() .map(InterpString::as_source); - let model_str = resolved_cli - .exec - .model - .name - .as_ref() - .map(InterpString::as_source); - let permissions = resolved_cli.exec.agent.permissions.map(|p| match p { + let model_str = cli.exec.model.name.as_ref().map(InterpString::as_source); + let permissions = cli.exec.agent.permissions.map(|p| match p { AgentPermissions::ReadOnly => AgentPermissionLevel::ReadOnly, AgentPermissions::ReadWrite => AgentPermissionLevel::ReadWrite, AgentPermissions::Full => AgentPermissionLevel::Full, }); - let output_format = Some(match resolved_cli.output.format { + let output_format = Some(match cli.output.format { SettingsOutputFormat::Text => OutputFormat::Text, SettingsOutputFormat::Json => OutputFormat::Json, }); @@ -137,16 +131,12 @@ pub(crate) async fn execute( permissions, output_format, ); - if globals.json { - args.agent.output_format = Some(OutputFormat::Json); - } - let server_target = user_config::exec_server_target(&args.server, &cli_settings)?; + let server_target = user_config::exec_server_target(&args.server, &raw_settings)?; // v2 MCPs live under `cli.exec.agent.mcps` (owner-specific) or // `run.agent.mcps`. For `fabro exec` we use the cli.exec path, falling // back to run.agent.mcps if unset. - let mcp_servers: Vec = if !resolved_cli.exec.agent.mcps.is_empty() { - resolved_cli - .exec + let mcp_servers: Vec = if !cli.exec.agent.mcps.is_empty() { + cli.exec .agent .mcps .values() @@ -157,7 +147,7 @@ pub(crate) async fn execute( tool_timeout_secs: server.tool_timeout_secs, }) .collect() - } else if let Some(mcps) = cli_settings + } else if let Some(mcps) = raw_settings .cli .as_ref() .and_then(|cli| cli.exec.as_ref()) @@ -169,7 +159,7 @@ pub(crate) async fn execute( .map(|(name, entry)| runtime_mcp_server(name, entry)) .collect() } else { - fabro_config::resolve_run_from_file(&cli_settings) + fabro_config::resolve_run_from_file(&raw_settings) .map(|settings| { settings .agent diff --git a/lib/crates/fabro-cli/src/commands/graph.rs b/lib/crates/fabro-cli/src/commands/graph.rs index 5f9b0afc3..12a873dc8 100644 --- a/lib/crates/fabro-cli/src/commands/graph.rs +++ b/lib/crates/fabro-cli/src/commands/graph.rs @@ -4,12 +4,13 @@ use anyhow::bail; use fabro_api::types; use fabro_config::load::load_settings_user; use fabro_config::user::active_settings_path; -use fabro_types::settings::SettingsLayer; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; +use fabro_types::settings::{CliSettings, SettingsLayer}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use tracing::debug; -use crate::args::{GlobalArgs, GraphArgs, GraphDirection}; +use crate::args::{GraphArgs, GraphDirection, require_no_json_override}; use crate::command_context::CommandContext; use crate::commands::run::output::api_diagnostics_to_local; use crate::manifest_builder::{ManifestBuildInput, build_run_manifest}; @@ -18,14 +19,16 @@ use crate::shared::{absolute_or_current, print_diagnostics, print_json_pretty, r pub(crate) async fn run( args: &GraphArgs, styles: &Styles, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, + process_local_json: bool, printer: Printer, ) -> anyhow::Result<()> { - if globals.json && args.output.is_none() { - globals.require_no_json()?; + if process_local_json && args.output.is_none() { + require_no_json_override(process_local_json)?; } - let ctx = CommandContext::for_target(&args.target, printer)?; + let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?; let built = build_run_manifest(ManifestBuildInput { workflow: args.workflow.clone(), cwd: ctx.cwd().to_path_buf(), @@ -60,7 +63,7 @@ pub(crate) async fn run( if let Some(ref output_path) = args.output { std::fs::write(output_path, &rendered)?; - if globals.json { + if cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "path": absolute_or_current(output_path), "format": args.format.to_string(), diff --git a/lib/crates/fabro-cli/src/commands/install.rs b/lib/crates/fabro-cli/src/commands/install.rs index 0e31e8f3e..9a3ba2ef1 100644 --- a/lib/crates/fabro-cli/src/commands/install.rs +++ b/lib/crates/fabro-cli/src/commands/install.rs @@ -18,6 +18,8 @@ use fabro_auth::{AuthCredential, AuthMethod, codex_oauth_config, credential_id_f use fabro_config::user::SETTINGS_CONFIG_FILENAME; use fabro_config::{Storage, envfile, legacy_env}; use fabro_model::Provider; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use fabro_util::{dev_token, session_secret}; @@ -32,8 +34,7 @@ use tokio::task::spawn_blocking; use super::doctor; use crate::args::{ - DoctorArgs, GlobalArgs, InstallArgs, InstallGitHubStrategyArg, InstallNonInteractiveArgs, - ServerTargetArgs, + DoctorArgs, InstallArgs, InstallGitHubStrategyArg, InstallNonInteractiveArgs, ServerTargetArgs, }; use crate::commands::server::{record, stop}; use crate::gh::GhCli; @@ -1229,15 +1230,18 @@ async fn persist_install_outputs_with_settings( pub(crate) async fn run_install( args: &InstallArgs, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, + process_local_json: bool, printer: Printer, ) -> Result<()> { - if globals.json && !args.non_interactive { + let json = cli.output.format == OutputFormat::Json; + if process_local_json && !args.non_interactive { bail!("--json is only supported for install with --non-interactive"); } - let result = Box::pin(run_install_inner(args, globals, printer)).await; - if globals.json { + let result = Box::pin(run_install_inner(args, cli, cli_layer, printer)).await; + if json { let emit_result = match &result { Ok(()) => emit_install_json_event(&install_complete_event()), Err(err) => emit_install_json_event(&install_error_event(&err.to_string())), @@ -1252,9 +1256,11 @@ pub(crate) async fn run_install( async fn run_install_inner( args: &InstallArgs, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { + let json = cli.output.format == OutputFormat::Json; let web_url = &args.web_url; let s = Styles::detect_stderr(); let emoji = console::Emoji("⚒️ ", ""); @@ -1360,7 +1366,7 @@ async fn run_install_inner( } else { GitHubAppHandoffMode::Interactive }, - globals.json, + json, printer, ) .await?; @@ -1539,7 +1545,7 @@ async fn run_install_inner( target: ServerTargetArgs::default(), verbose: true, }; - let _ = doctor::run_doctor(&doctor_args, true, globals, printer).await?; + let _ = doctor::run_doctor(&doctor_args, true, cli, cli_layer, printer).await?; } fabro_util::printerr!(printer, ""); diff --git a/lib/crates/fabro-cli/src/commands/model.rs b/lib/crates/fabro-cli/src/commands/model.rs index e66e1a9ae..68c23407b 100644 --- a/lib/crates/fabro-cli/src/commands/model.rs +++ b/lib/crates/fabro-cli/src/commands/model.rs @@ -3,12 +3,14 @@ use cli_table::format::{Border, Justify, Separator}; use cli_table::{Cell, CellStruct, Color, Style, Table}; use fabro_api::{self, types as api_types}; use fabro_model::{Catalog, Model, Provider}; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use serde::Serialize; use serde::de::DeserializeOwned; -use crate::args::{GlobalArgs, ModelListArgs, ModelTestArgs, ModelsCommand}; +use crate::args::{ModelListArgs, ModelTestArgs, ModelsCommand}; use crate::command_context::CommandContext; use crate::server_client; @@ -41,7 +43,8 @@ struct ModelTestOutput { pub(crate) async fn execute( command: Option, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { let command = command.unwrap_or_default(); @@ -49,10 +52,15 @@ pub(crate) async fn execute( ModelsCommand::List(args) => &args.target, ModelsCommand::Test(args) => &args.target, }; - let ctx = CommandContext::for_target(target_args, printer)?; + let ctx = CommandContext::for_target(target_args, printer, cli.clone(), cli_layer)?; let server = ctx.server().await?; - run_models(command, server.api(), globals.json).await + run_models( + command, + server.api(), + cli.output.format == OutputFormat::Json, + ) + .await } fn format_context_window(tokens: i64) -> String { diff --git a/lib/crates/fabro-cli/src/commands/parse.rs b/lib/crates/fabro-cli/src/commands/parse.rs index ce6722e20..32fad0040 100644 --- a/lib/crates/fabro-cli/src/commands/parse.rs +++ b/lib/crates/fabro-cli/src/commands/parse.rs @@ -2,13 +2,13 @@ use std::io::Write; use fabro_config::project::resolve_workflow; use fabro_graphviz::parser::parse_ast; +use fabro_types::settings::CliSettings; use fabro_util::printer::Printer; -use crate::args::{GlobalArgs, ParseArgs}; +use crate::args::ParseArgs; use crate::shared::read_workflow_file; -pub(crate) fn run(args: &ParseArgs, globals: &GlobalArgs, _printer: Printer) -> anyhow::Result<()> { - let _ = globals; +pub(crate) fn run(args: &ParseArgs, _cli: &CliSettings, _printer: Printer) -> anyhow::Result<()> { let stdout = std::io::stdout(); run_to(args, stdout.lock()) } diff --git a/lib/crates/fabro-cli/src/commands/pr/close.rs b/lib/crates/fabro-cli/src/commands/pr/close.rs index a1e63aacf..1732c6556 100644 --- a/lib/crates/fabro-cli/src/commands/pr/close.rs +++ b/lib/crates/fabro-cli/src/commands/pr/close.rs @@ -1,18 +1,22 @@ use anyhow::Result; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use tracing::info; -use crate::args::{GlobalArgs, PrCloseArgs}; +use crate::args::PrCloseArgs; use crate::shared::print_json_pretty; pub(super) async fn close_command( args: PrCloseArgs, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { - let (record, _run_id) = super::load_pr_record(&args.server, &args.run_id, printer).await?; + let (record, _run_id) = + super::load_pr_record(&args.server, &args.run_id, cli, cli_layer, printer).await?; - let creds = super::load_github_credentials_required(printer)?; + let creds = super::load_github_credentials_required(cli, cli_layer, printer)?; fabro_github::close_pull_request( &creds, @@ -25,7 +29,7 @@ pub(super) async fn close_command( .map_err(|err| anyhow::anyhow!("{err}"))?; info!(number = record.number, owner = %record.owner, repo = %record.repo, "Closed pull request"); - if globals.json { + if cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "number": record.number, "html_url": record.html_url, diff --git a/lib/crates/fabro-cli/src/commands/pr/create.rs b/lib/crates/fabro-cli/src/commands/pr/create.rs index 1da9a3e50..73f371e7c 100644 --- a/lib/crates/fabro-cli/src/commands/pr/create.rs +++ b/lib/crates/fabro-cli/src/commands/pr/create.rs @@ -1,12 +1,14 @@ use anyhow::{Context, Result, bail}; use fabro_model::Catalog; use fabro_sandbox::daytona::detect_repo_info; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use fabro_workflow::outcome::StageStatus; use fabro_workflow::pull_request::maybe_open_pull_request; use tracing::info; -use crate::args::{GlobalArgs, PrCreateArgs}; +use crate::args::PrCreateArgs; use crate::command_context::CommandContext; use crate::commands::store::rebuild::rebuild_run_store; use crate::server_runs::ServerSummaryLookup; @@ -15,10 +17,11 @@ use crate::shared::repo::ensure_matching_repo_origin; pub(super) async fn create_command( args: PrCreateArgs, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { - let ctx = CommandContext::for_target(&args.server, printer)?; + let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(&args.run_id)?; let run_id = run.run_id(); @@ -74,7 +77,7 @@ pub(super) async fn create_command( let (owner, repo) = fabro_github::parse_github_owner_repo(&https_url) .map_err(|err| anyhow::anyhow!("{err}"))?; - let creds = super::load_github_credentials_required(printer)?; + let creds = super::load_github_credentials_required(cli, cli_layer, printer)?; let branch_found = fabro_github::branch_exists( &creds, @@ -116,14 +119,14 @@ pub(super) async fn create_command( match record { Some(record) => { info!(pr_url = %record.html_url, "Pull request created"); - if globals.json { + if cli.output.format == OutputFormat::Json { print_json_pretty(&record)?; } else { fabro_util::printout!(printer, "{}", record.html_url); } } None => { - if globals.json { + if cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::Value::Null)?; } else { fabro_util::printout!(printer, "No pull request created (empty diff)."); diff --git a/lib/crates/fabro-cli/src/commands/pr/list.rs b/lib/crates/fabro-cli/src/commands/pr/list.rs index 94854c4ef..bcd497a12 100644 --- a/lib/crates/fabro-cli/src/commands/pr/list.rs +++ b/lib/crates/fabro-cli/src/commands/pr/list.rs @@ -1,13 +1,15 @@ use anyhow::Result; use cli_table::format::{Border, Separator}; use cli_table::{Cell, CellStruct, Color, Style, Table}; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use futures::future::join_all; use serde::Serialize; use tracing::info; -use crate::args::{GlobalArgs, PrListArgs}; +use crate::args::PrListArgs; use crate::command_context::CommandContext; use crate::server_runs::ServerSummaryLookup; use crate::shared::{color_if, print_json_pretty}; @@ -23,10 +25,11 @@ struct PrRow { pub(super) async fn list_command( args: PrListArgs, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { - let ctx = CommandContext::for_target(&args.server, printer)?; + let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let mut entries = Vec::new(); @@ -39,7 +42,7 @@ pub(super) async fn list_command( } if entries.is_empty() { - if globals.json { + if cli.output.format == OutputFormat::Json { print_json_pretty(&Vec::::new())?; return Ok(()); } @@ -47,7 +50,7 @@ pub(super) async fn list_command( return Ok(()); } - let creds = super::load_github_credentials_required(printer)?; + let creds = super::load_github_credentials_required(cli, cli_layer, printer)?; let futures: Vec<_> = entries .iter() @@ -101,7 +104,7 @@ pub(super) async fn list_command( .collect() }; - if globals.json { + if cli.output.format == OutputFormat::Json { print_json_pretty(&rows)?; return Ok(()); } diff --git a/lib/crates/fabro-cli/src/commands/pr/merge.rs b/lib/crates/fabro-cli/src/commands/pr/merge.rs index 34ac28d3b..7e68240c8 100644 --- a/lib/crates/fabro-cli/src/commands/pr/merge.rs +++ b/lib/crates/fabro-cli/src/commands/pr/merge.rs @@ -1,18 +1,22 @@ use anyhow::Result; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use tracing::info; -use crate::args::{GlobalArgs, PrMergeArgs}; +use crate::args::PrMergeArgs; use crate::shared::print_json_pretty; pub(super) async fn merge_command( args: PrMergeArgs, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { - let (record, _run_id) = super::load_pr_record(&args.server, &args.run_id, printer).await?; + let (record, _run_id) = + super::load_pr_record(&args.server, &args.run_id, cli, cli_layer, printer).await?; - let creds = super::load_github_credentials_required(printer)?; + let creds = super::load_github_credentials_required(cli, cli_layer, printer)?; fabro_github::merge_pull_request( &creds, @@ -26,7 +30,7 @@ pub(super) async fn merge_command( .map_err(|err| anyhow::anyhow!("{err}"))?; info!(number = record.number, owner = %record.owner, repo = %record.repo, method = %args.method, "Merged pull request"); - if globals.json { + if cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "number": record.number, "html_url": record.html_url, diff --git a/lib/crates/fabro-cli/src/commands/pr/mod.rs b/lib/crates/fabro-cli/src/commands/pr/mod.rs index 58f008d18..c5147cccd 100644 --- a/lib/crates/fabro-cli/src/commands/pr/mod.rs +++ b/lib/crates/fabro-cli/src/commands/pr/mod.rs @@ -8,10 +8,11 @@ use anyhow::{Context, Result, anyhow}; use fabro_config::Storage; use fabro_github::GitHubCredentials; use fabro_types::PullRequestRecord; -use fabro_types::settings::InterpString; +use fabro_types::settings::cli::CliLayer; +use fabro_types::settings::{CliSettings, InterpString}; use fabro_util::printer::Printer; -use crate::args::{GlobalArgs, PrCommand, PrNamespace, ServerTargetArgs}; +use crate::args::{PrCommand, PrNamespace, ServerTargetArgs}; use crate::command_context::CommandContext; use crate::server_runs::ServerSummaryLookup; use crate::shared::github::build_github_credentials; @@ -22,20 +23,27 @@ const GITHUB_CREDENTIALS_REQUIRED: &str = pub(crate) async fn dispatch( ns: PrNamespace, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { match ns.command { - PrCommand::Create(args) => Box::pin(create::create_command(args, globals, printer)).await, - PrCommand::List(args) => list::list_command(args, globals, printer).await, - PrCommand::View(args) => view::view_command(args, globals, printer).await, - PrCommand::Merge(args) => merge::merge_command(args, globals, printer).await, - PrCommand::Close(args) => close::close_command(args, globals, printer).await, + PrCommand::Create(args) => { + Box::pin(create::create_command(args, cli, cli_layer, printer)).await + } + PrCommand::List(args) => list::list_command(args, cli, cli_layer, printer).await, + PrCommand::View(args) => view::view_command(args, cli, cli_layer, printer).await, + PrCommand::Merge(args) => merge::merge_command(args, cli, cli_layer, printer).await, + PrCommand::Close(args) => close::close_command(args, cli, cli_layer, printer).await, } } -fn load_github_credentials_required(printer: Printer) -> Result { - let ctx = CommandContext::base(printer)?; +fn load_github_credentials_required( + cli: &CliSettings, + cli_layer: &CliLayer, + printer: Printer, +) -> Result { + let ctx = CommandContext::base(printer, cli.clone(), cli_layer)?; let server_settings = fabro_config::resolve_server_from_file(ctx.machine_settings()).map_err(|errors| { anyhow!( @@ -68,9 +76,11 @@ fn load_github_credentials_required(printer: Printer) -> Result Result<(PullRequestRecord, fabro_types::RunId)> { - let ctx = CommandContext::for_target(server, printer)?; + let ctx = CommandContext::for_target(server, printer, cli.clone(), cli_layer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(run_id)?; let run_id = run.run_id(); diff --git a/lib/crates/fabro-cli/src/commands/pr/view.rs b/lib/crates/fabro-cli/src/commands/pr/view.rs index ae0fee547..f093838dc 100644 --- a/lib/crates/fabro-cli/src/commands/pr/view.rs +++ b/lib/crates/fabro-cli/src/commands/pr/view.rs @@ -1,18 +1,22 @@ use anyhow::Result; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use tracing::info; -use crate::args::{GlobalArgs, PrViewArgs}; +use crate::args::PrViewArgs; use crate::shared::print_json_pretty; pub(super) async fn view_command( args: PrViewArgs, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { - let (record, _run_id) = super::load_pr_record(&args.server, &args.run_id, printer).await?; + let (record, _run_id) = + super::load_pr_record(&args.server, &args.run_id, cli, cli_layer, printer).await?; - let creds = super::load_github_credentials_required(printer)?; + let creds = super::load_github_credentials_required(cli, cli_layer, printer)?; let detail = fabro_github::get_pull_request( &creds, @@ -26,7 +30,7 @@ pub(super) async fn view_command( info!(number = detail.number, owner = %record.owner, repo = %record.repo, "Viewing pull request"); - if globals.json { + if cli.output.format == OutputFormat::Json { print_json_pretty(&detail)?; return Ok(()); } diff --git a/lib/crates/fabro-cli/src/commands/preflight.rs b/lib/crates/fabro-cli/src/commands/preflight.rs index a6689468e..6ebba6168 100644 --- a/lib/crates/fabro-cli/src/commands/preflight.rs +++ b/lib/crates/fabro-cli/src/commands/preflight.rs @@ -1,11 +1,12 @@ use anyhow::bail; use fabro_config::load::load_settings_user; use fabro_config::user::active_settings_path; -use fabro_types::settings::cli::OutputVerbosity; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; -use crate::args::{GlobalArgs, PreflightArgs}; +use crate::args::PreflightArgs; use crate::command_context::CommandContext; use crate::commands::run::output::{ api_check_report_to_local, api_diagnostics_to_local, print_preflight_workflow_summary, @@ -16,12 +17,13 @@ use crate::shared::print_json_pretty; pub(crate) async fn execute( mut args: PreflightArgs, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> anyhow::Result<()> { let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); - let ctx = CommandContext::for_target(&args.target, printer)?; - args.verbose = args.verbose || ctx.cli_settings().output.verbosity == OutputVerbosity::Verbose; + let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?; + args.verbose = args.verbose || cli.output.verbosity == OutputVerbosity::Verbose; let manifest = build_run_manifest(ManifestBuildInput { workflow: args.workflow.clone(), @@ -36,7 +38,7 @@ pub(crate) async fn execute( let response = client.run_preflight(manifest.manifest).await?; let diagnostics = api_diagnostics_to_local(&response.workflow.diagnostics); - if globals.json { + if cli.output.format == OutputFormat::Json { print_json_pretty(&response)?; } else { print_preflight_workflow_summary( diff --git a/lib/crates/fabro-cli/src/commands/provider/login.rs b/lib/crates/fabro-cli/src/commands/provider/login.rs index 39c42fd29..9654145a4 100644 --- a/lib/crates/fabro-cli/src/commands/provider/login.rs +++ b/lib/crates/fabro-cli/src/commands/provider/login.rs @@ -2,21 +2,25 @@ use anyhow::Result; use fabro_api::types; use fabro_auth::credential_id_for; use fabro_config::legacy_env; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; -use crate::args::{GlobalArgs, ProviderLoginArgs}; +use crate::args::{ProviderLoginArgs, require_no_json_override}; use crate::command_context::CommandContext; use crate::shared::provider_auth; pub(super) async fn login_command( args: ProviderLoginArgs, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, + process_local_json: bool, printer: Printer, ) -> Result<()> { - globals.require_no_json()?; + require_no_json_override(process_local_json)?; let s = Styles::detect_stderr(); - let ctx = CommandContext::for_target(&args.target, printer)?; + let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?; let server = ctx.server().await?; let credential = if args.api_key_stdin { provider_auth::authenticate_provider_with_api_key_source( diff --git a/lib/crates/fabro-cli/src/commands/provider/mod.rs b/lib/crates/fabro-cli/src/commands/provider/mod.rs index 79915a73a..10db120ae 100644 --- a/lib/crates/fabro-cli/src/commands/provider/mod.rs +++ b/lib/crates/fabro-cli/src/commands/provider/mod.rs @@ -1,16 +1,22 @@ mod login; use anyhow::Result; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; -use crate::args::{GlobalArgs, ProviderCommand, ProviderNamespace}; +use crate::args::{ProviderCommand, ProviderNamespace}; pub(crate) async fn dispatch( ns: ProviderNamespace, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, + process_local_json: bool, printer: Printer, ) -> Result<()> { match ns.command { - ProviderCommand::Login(args) => login::login_command(args, globals, printer).await, + ProviderCommand::Login(args) => { + login::login_command(args, cli, cli_layer, process_local_json, printer).await + } } } diff --git a/lib/crates/fabro-cli/src/commands/repo/deinit.rs b/lib/crates/fabro-cli/src/commands/repo/deinit.rs index 926f91205..6d7ccd294 100644 --- a/lib/crates/fabro-cli/src/commands/repo/deinit.rs +++ b/lib/crates/fabro-cli/src/commands/repo/deinit.rs @@ -1,9 +1,9 @@ use anyhow::{Context, Result, bail}; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::OutputFormat; use fabro_util::printer::Printer; -use crate::args::GlobalArgs; - -pub(crate) fn run_deinit(globals: &GlobalArgs, printer: Printer) -> Result> { +pub(crate) fn run_deinit(cli: &CliSettings, printer: Printer) -> Result> { let repo_root = super::init::git_repo_root()?; let mut removed = Vec::new(); @@ -20,7 +20,7 @@ pub(crate) fn run_deinit(globals: &GlobalArgs, printer: Printer) -> Result Result Result { pub(crate) async fn run_init( args: &RepoInitArgs, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result> { let repo_root = spawn_blocking(git_repo_root) @@ -71,7 +74,7 @@ draft = true let green = console::Style::new().green(); let bold = console::Style::new().bold(); let dim = console::Style::new().dim(); - if !globals.json { + if cli.output.format != OutputFormat::Json { fabro_util::printerr!( printer, " {} {}", @@ -104,7 +107,7 @@ draft = true ) .with_context(|| format!("failed to write {}", dot_path.display()))?; created.push(".fabro/workflows/hello/workflow.fabro".to_string()); - if !globals.json { + if cli.output.format != OutputFormat::Json { fabro_util::printerr!( printer, " {} {}", @@ -121,7 +124,7 @@ draft = true ) .with_context(|| format!("failed to write {}", toml_path.display()))?; created.push(".fabro/workflows/hello/workflow.toml".to_string()); - if !globals.json { + if cli.output.format != OutputFormat::Json { fabro_util::printerr!( printer, " {} {}", @@ -130,7 +133,7 @@ draft = true ); } - if !globals.json { + if cli.output.format != OutputFormat::Json { fabro_util::printerr!( printer, "\n{} Run a workflow with:\n\n {}", @@ -142,14 +145,19 @@ draft = true ); } - if !globals.json { - check_github_app_installation(&args.target, printer).await; + if cli.output.format != OutputFormat::Json { + check_github_app_installation(&args.target, cli, cli_layer, printer).await; } Ok(created) } -async fn check_github_app_installation(target: &ServerTargetArgs, printer: Printer) { +async fn check_github_app_installation( + target: &ServerTargetArgs, + cli: &CliSettings, + cli_layer: &CliLayer, + printer: Printer, +) { // Get the git remote origin URL let output = match TokioCommand::new("git") .args(["remote", "get-url", "origin"]) @@ -187,7 +195,7 @@ async fn check_github_app_installation(target: &ServerTargetArgs, printer: Print return; // Not a GitHub repo — skip silently }; - let ctx = match CommandContext::for_target(target, printer) { + let ctx = match CommandContext::for_target(target, printer, cli.clone(), cli_layer) { Ok(ctx) => ctx, Err(err) => { fabro_util::printerr!( diff --git a/lib/crates/fabro-cli/src/commands/repo/mod.rs b/lib/crates/fabro-cli/src/commands/repo/mod.rs index 29d1be6b1..30ce0fd41 100644 --- a/lib/crates/fabro-cli/src/commands/repo/mod.rs +++ b/lib/crates/fabro-cli/src/commands/repo/mod.rs @@ -2,27 +2,30 @@ pub(crate) mod deinit; pub(crate) mod init; use anyhow::Result; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; -use crate::args::{GlobalArgs, RepoCommand, RepoNamespace}; +use crate::args::{RepoCommand, RepoNamespace}; use crate::shared::print_json_pretty; pub(crate) async fn dispatch( ns: RepoNamespace, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { match ns.command { RepoCommand::Init(args) => { - let created = init::run_init(&args, globals, printer).await?; - if globals.json { + let created = init::run_init(&args, cli, cli_layer, printer).await?; + if cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "created": created }))?; } Ok(()) } RepoCommand::Deinit => { - let removed = deinit::run_deinit(globals, printer)?; - if globals.json { + let removed = deinit::run_deinit(cli, printer)?; + if cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "removed": removed }))?; } Ok(()) diff --git a/lib/crates/fabro-cli/src/commands/run/command.rs b/lib/crates/fabro-cli/src/commands/run/command.rs index 8874a34e0..a4272ecbc 100644 --- a/lib/crates/fabro-cli/src/commands/run/command.rs +++ b/lib/crates/fabro-cli/src/commands/run/command.rs @@ -1,27 +1,34 @@ use anyhow::Result; -use fabro_types::settings::cli::OutputVerbosity; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; -use crate::args::{GlobalArgs, RunArgs}; +use crate::args::RunArgs; use crate::command_context::CommandContext; use crate::shared::print_json_pretty; use crate::user_config::settings_layer_with_storage_dir; pub(crate) async fn execute( mut args: RunArgs, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); - let ctx = CommandContext::for_target(&args.target, printer)?; - let cli = settings_layer_with_storage_dir(None)?; - args.verbose = args.verbose || ctx.cli_settings().output.verbosity == OutputVerbosity::Verbose; + let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?; + let cli_defaults = settings_layer_with_storage_dir(None)?; + args.verbose = args.verbose || cli.output.verbosity == OutputVerbosity::Verbose; let quiet = args.detach; let prevent_idle_sleep = ctx.cli_settings().exec.prevent_idle_sleep; let created_run = Box::pin(super::create::create_run( - &ctx, &args, cli, styles, quiet, printer, + &ctx, + &args, + cli_defaults, + styles, + quiet, + printer, )) .await?; @@ -34,8 +41,9 @@ pub(crate) async fn execute( let client = ctx.server().await?; super::start::start_run_with_client(&client, &created_run.run_id, false).await?; + let json = cli.output.format == OutputFormat::Json; if args.detach { - if globals.json { + if json { print_json_pretty(&serde_json::json!({ "run_id": created_run.run_id }))?; } else { fabro_util::printout!(printer, "{}", created_run.run_id); @@ -46,11 +54,11 @@ pub(crate) async fn execute( &created_run.run_id, true, styles, - globals.json, + json, printer, ) .await?; - if !globals.json { + if !json { super::output::print_run_summary_with_client( &client, &created_run.run_id, diff --git a/lib/crates/fabro-cli/src/commands/run/cp.rs b/lib/crates/fabro-cli/src/commands/run/cp.rs index 6dbd1f9a1..76a324b32 100644 --- a/lib/crates/fabro-cli/src/commands/run/cp.rs +++ b/lib/crates/fabro-cli/src/commands/run/cp.rs @@ -1,11 +1,13 @@ use std::path::{Path, PathBuf}; use anyhow::{Context, Result, bail}; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use tokio::fs; use tracing::{debug, info}; -use crate::args::{CpArgs, GlobalArgs, ServerTargetArgs}; +use crate::args::{CpArgs, ServerTargetArgs}; use crate::command_context::CommandContext; use crate::server_client::ServerStoreClient; use crate::server_runs::ServerSummaryLookup; @@ -25,7 +27,12 @@ enum CopyDirection { }, } -pub(crate) async fn cp_command(args: CpArgs, globals: &GlobalArgs, printer: Printer) -> Result<()> { +pub(crate) async fn cp_command( + args: CpArgs, + cli: &CliSettings, + cli_layer: &CliLayer, + printer: Printer, +) -> Result<()> { let direction = parse_direction(&args.src, &args.dst)?; match direction { @@ -35,7 +42,8 @@ pub(crate) async fn cp_command(args: CpArgs, globals: &GlobalArgs, printer: Prin local_path, } => { let (client, run_id) = - resolve_client_and_run_id(&args.server, &run_prefix, printer).await?; + resolve_client_and_run_id(&args.server, &run_prefix, cli, cli_layer, printer) + .await?; let file_count = if args.recursive { Some(download_recursive(&client, &run_id, &remote_path, &local_path).await?) @@ -45,7 +53,7 @@ pub(crate) async fn cp_command(args: CpArgs, globals: &GlobalArgs, printer: Prin None }; - if globals.json { + if cli.output.format == OutputFormat::Json { let mut value = serde_json::json!({ "direction": "download", "recursive": args.recursive, @@ -66,7 +74,8 @@ pub(crate) async fn cp_command(args: CpArgs, globals: &GlobalArgs, printer: Prin remote_path, } => { let (client, run_id) = - resolve_client_and_run_id(&args.server, &run_prefix, printer).await?; + resolve_client_and_run_id(&args.server, &run_prefix, cli, cli_layer, printer) + .await?; let file_count = if args.recursive { Some(upload_recursive(&client, &run_id, &local_path, &remote_path).await?) @@ -76,7 +85,7 @@ pub(crate) async fn cp_command(args: CpArgs, globals: &GlobalArgs, printer: Prin None }; - if globals.json { + if cli.output.format == OutputFormat::Json { let mut value = serde_json::json!({ "direction": "upload", "recursive": args.recursive, @@ -120,9 +129,11 @@ fn parse_direction(src: &str, dst: &str) -> Result { async fn resolve_client_and_run_id( server: &ServerTargetArgs, run_prefix: &str, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result<(ServerStoreClient, fabro_types::RunId)> { - let ctx = CommandContext::for_target(server, printer)?; + let ctx = CommandContext::for_target(server, printer, cli.clone(), cli_layer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(run_prefix)?; Ok((lookup.client().clone_for_reuse(), run.run_id())) diff --git a/lib/crates/fabro-cli/src/commands/run/diff.rs b/lib/crates/fabro-cli/src/commands/run/diff.rs index 7d7f3dcfc..73a0b4214 100644 --- a/lib/crates/fabro-cli/src/commands/run/diff.rs +++ b/lib/crates/fabro-cli/src/commands/run/diff.rs @@ -1,18 +1,25 @@ use std::io::{self, IsTerminal, Write}; use anyhow::{Context, Result, bail}; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use tracing::{debug, info}; -use crate::args::{DiffArgs, GlobalArgs}; +use crate::args::DiffArgs; use crate::command_context::CommandContext; use crate::server_client::RunProjection; use crate::server_runs::ServerSummaryLookup; use crate::shared::print_json_pretty; -pub(crate) async fn run(args: DiffArgs, globals: &GlobalArgs, printer: Printer) -> Result<()> { +pub(crate) async fn run( + args: DiffArgs, + cli: &CliSettings, + cli_layer: &CliLayer, + printer: Printer, +) -> Result<()> { info!(run_id = %args.run, "Showing diff"); - let ctx = CommandContext::for_target(&args.server, printer)?; + let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(&args.run)?; let run_id = run.run_id(); @@ -20,7 +27,7 @@ pub(crate) async fn run(args: DiffArgs, globals: &GlobalArgs, printer: Printer) let patch = resolve_diff(&state, &args)?; - if globals.json { + if cli.output.format == OutputFormat::Json { let value = serde_json::json!({ "run_id": run_id, "node": args.node, diff --git a/lib/crates/fabro-cli/src/commands/run/fork.rs b/lib/crates/fabro-cli/src/commands/run/fork.rs index 203cc8ff9..facd3ff3a 100644 --- a/lib/crates/fabro-cli/src/commands/run/fork.rs +++ b/lib/crates/fabro-cli/src/commands/run/fork.rs @@ -1,11 +1,13 @@ use anyhow::{Context, Result}; use fabro_checkpoint::git::Store; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use fabro_workflow::operations::{ForkRunInput, RewindTarget, build_timeline_or_rebuild, fork}; use git2::Repository; -use crate::args::{ForkArgs, GlobalArgs}; +use crate::args::ForkArgs; use crate::command_context::CommandContext; use crate::commands::store::rebuild::rebuild_run_store; use crate::server_runs::ServerSummaryLookup; @@ -15,11 +17,12 @@ use crate::shared::repo::ensure_matching_repo_origin; pub(crate) async fn run( args: &ForkArgs, styles: &Styles, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { let repo = Repository::discover(".").context("not in a git repository")?; - let ctx = CommandContext::for_target(&args.server, printer)?; + let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(&args.run_id)?; let run_id = run.run_id(); @@ -33,7 +36,7 @@ pub(crate) async fn run( let timeline = build_timeline_or_rebuild(&store, Some(&run_store), &run_id).await?; if args.list { - if globals.json { + if cli.output.format == OutputFormat::Json { print_json_pretty(&super::rewind::timeline_entries_json(&timeline))?; return Ok(()); } @@ -55,7 +58,7 @@ pub(crate) async fn run( let run_id_string = run_id.to_string(); let new_run_id_string = new_run_id.to_string(); - if globals.json { + if cli.output.format == OutputFormat::Json { let target = args.target.clone().unwrap_or_else(|| "latest".to_string()); print_json_pretty(&serde_json::json!({ "source_run_id": run_id_string, diff --git a/lib/crates/fabro-cli/src/commands/run/logs.rs b/lib/crates/fabro-cli/src/commands/run/logs.rs index ec3e65b07..121761d96 100644 --- a/lib/crates/fabro-cli/src/commands/run/logs.rs +++ b/lib/crates/fabro-cli/src/commands/run/logs.rs @@ -4,6 +4,8 @@ use std::time::Duration; use anyhow::{Context, Result, bail}; use chrono::{DateTime, Utc}; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::json::normalize_json_value; use fabro_util::printer::Printer; use fabro_util::redact::redact_jsonl_line; @@ -11,7 +13,7 @@ use fabro_util::terminal::Styles; use tokio::time; use tracing::{debug, info}; -use crate::args::{GlobalArgs, LogsArgs}; +use crate::args::LogsArgs; use crate::command_context::CommandContext; use crate::server_client; use crate::server_runs::ServerSummaryLookup; @@ -22,10 +24,11 @@ const FOLLOW_TERMINAL_GRACE: Duration = Duration::from_millis(500); pub(crate) async fn run( args: &LogsArgs, styles: &Styles, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { - let ctx = CommandContext::for_target(&args.server, printer)?; + let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(&args.run)?; let client = lookup.client(); @@ -52,7 +55,7 @@ pub(crate) async fn run( let stdout = io::stdout(); let is_tty = stdout.is_terminal(); let mut out = stdout.lock(); - let pretty = args.pretty && !globals.json; + let pretty = args.pretty && cli.output.format != OutputFormat::Json; for line in &filtered { if pretty { diff --git a/lib/crates/fabro-cli/src/commands/run/mod.rs b/lib/crates/fabro-cli/src/commands/run/mod.rs index 8b9b9bd6e..66a83c82c 100644 --- a/lib/crates/fabro-cli/src/commands/run/mod.rs +++ b/lib/crates/fabro-cli/src/commands/run/mod.rs @@ -1,8 +1,10 @@ use anyhow::Result; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; -use crate::args::{AttachArgs, GlobalArgs, RunArgs, RunCommands, RunWorkerArgs, StartArgs}; +use crate::args::{AttachArgs, RunCommands, RunWorkerArgs, StartArgs}; use crate::command_context::CommandContext; use crate::server_runs::ServerSummaryLookup; use crate::shared::print_json_pretty; @@ -26,30 +28,29 @@ pub(crate) mod ssh; pub(crate) mod start; pub(crate) mod wait; -fn apply_json_defaults(args: &mut RunArgs, globals: &GlobalArgs) { - if globals.json { - args.auto_approve = true; - } -} - pub(crate) async fn dispatch( cmd: RunCommands, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, + _process_local_json: bool, printer: Printer, ) -> Result<()> { match cmd { - RunCommands::Run(mut args) => { - apply_json_defaults(&mut args, globals); - Box::pin(command::execute(args, globals, printer)).await - } - RunCommands::Create(mut args) => { - apply_json_defaults(&mut args, globals); + RunCommands::Run(args) => Box::pin(command::execute(args, cli, cli_layer, printer)).await, + RunCommands::Create(args) => { let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); - let cli = settings_layer_with_storage_dir(None)?; - let ctx = CommandContext::for_target(&args.target, printer)?; - let created_run = - Box::pin(create::create_run(&ctx, &args, cli, styles, true, printer)).await?; - if globals.json { + let cli_defaults = settings_layer_with_storage_dir(None)?; + let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?; + let created_run = Box::pin(create::create_run( + &ctx, + &args, + cli_defaults, + styles, + true, + printer, + )) + .await?; + if cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "run_id": created_run.run_id }))?; } else { fabro_util::printout!(printer, "{}", created_run.run_id); @@ -57,19 +58,19 @@ pub(crate) async fn dispatch( Ok(()) } RunCommands::Start(StartArgs { server, run }) => { - let ctx = CommandContext::for_target(&server, printer)?; + let ctx = CommandContext::for_target(&server, printer, cli.clone(), cli_layer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run_info = lookup.resolve(&run)?; let run_id = run_info.run_id(); start::start_run_with_client(lookup.client(), &run_id, false).await?; - if globals.json { + if cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "run_id": run_id }))?; } Ok(()) } RunCommands::Attach(AttachArgs { server, run }) => { let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); - let ctx = CommandContext::for_target(&server, printer)?; + let ctx = CommandContext::for_target(&server, printer, cli.clone(), cli_layer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run_info = lookup.resolve(&run)?; let run_id = run_info.run_id(); @@ -78,7 +79,7 @@ pub(crate) async fn dispatch( &run_id, false, styles, - globals.json, + cli.output.format == OutputFormat::Json, printer, ) .await?; @@ -105,31 +106,32 @@ pub(crate) async fn dispatch( ) .await } - RunCommands::Diff(args) => diff::run(args, globals, printer).await, + RunCommands::Diff(args) => diff::run(args, cli, cli_layer, printer).await, RunCommands::Logs(args) => { let styles = Styles::detect_stdout(); - logs::run(&args, &styles, globals, printer).await + logs::run(&args, &styles, cli, cli_layer, printer).await } RunCommands::Resume(args) => { let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); #[cfg(feature = "sleep_inhibitor")] let _sleep_guard = { - let ctx = CommandContext::for_target(&args.server, printer)?; + let ctx = + CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; crate::sleep_inhibitor::guard(ctx.cli_settings().exec.prevent_idle_sleep) }; - resume::resume_command(args, styles, globals, printer).await + resume::resume_command(args, styles, cli, cli_layer, printer).await } RunCommands::Rewind(args) => { let styles = Styles::detect_stderr(); - Box::pin(rewind::run(&args, &styles, globals, printer)).await + Box::pin(rewind::run(&args, &styles, cli, cli_layer, printer)).await } RunCommands::Fork(args) => { let styles = Styles::detect_stderr(); - Box::pin(fork::run(&args, &styles, globals, printer)).await + Box::pin(fork::run(&args, &styles, cli, cli_layer, printer)).await } RunCommands::Wait(args) => { let styles = Styles::detect_stderr(); - wait::run(&args, &styles, globals, printer).await + wait::run(&args, &styles, cli, cli_layer, printer).await } } } diff --git a/lib/crates/fabro-cli/src/commands/run/preview.rs b/lib/crates/fabro-cli/src/commands/run/preview.rs index 67da6dd0f..dd58f40af 100644 --- a/lib/crates/fabro-cli/src/commands/run/preview.rs +++ b/lib/crates/fabro-cli/src/commands/run/preview.rs @@ -1,14 +1,22 @@ use anyhow::{Context, Result}; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use tracing::info; -use crate::args::{GlobalArgs, PreviewArgs}; +use crate::args::PreviewArgs; use crate::command_context::CommandContext; use crate::server_runs::ServerSummaryLookup; use crate::shared::print_json_pretty; -pub(crate) async fn run(args: PreviewArgs, globals: &GlobalArgs, printer: Printer) -> Result<()> { - let ctx = CommandContext::for_target(&args.server, printer)?; +pub(crate) async fn run( + args: PreviewArgs, + cli: &CliSettings, + cli_layer: &CliLayer, + process_local_json: bool, + printer: Printer, +) -> Result<()> { + let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(&args.run)?; let run_id = run.run_id(); @@ -26,7 +34,7 @@ pub(crate) async fn run(args: PreviewArgs, globals: &GlobalArgs, printer: Printe info!(run_id = %args.run, port = args.port, "Generating preview URL"); - if globals.json { + if cli.output.format == OutputFormat::Json { match response.token { Some(token) => { print_json_pretty(&serde_json::json!({ "url": response.url, "token": token }))?; @@ -51,7 +59,7 @@ pub(crate) async fn run(args: PreviewArgs, globals: &GlobalArgs, printer: Printe } } - if args.open && !globals.json { + if args.open && !process_local_json { #[expect( clippy::disallowed_methods, reason = "Preview URL opening is a fire-and-forget OS integration, not a Tokio-managed child process." diff --git a/lib/crates/fabro-cli/src/commands/run/resume.rs b/lib/crates/fabro-cli/src/commands/run/resume.rs index eb37be3d0..89604ee51 100644 --- a/lib/crates/fabro-cli/src/commands/run/resume.rs +++ b/lib/crates/fabro-cli/src/commands/run/resume.rs @@ -1,7 +1,9 @@ +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; -use crate::args::{GlobalArgs, ResumeArgs}; +use crate::args::ResumeArgs; use crate::command_context::CommandContext; use crate::server_runs::ServerSummaryLookup; use crate::shared::print_json_pretty; @@ -14,18 +16,20 @@ use crate::shared::print_json_pretty; pub(crate) async fn resume_command( args: ResumeArgs, styles: &'static Styles, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> anyhow::Result<()> { - let ctx = CommandContext::for_target(&args.server, printer)?; + let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(&args.run)?; let run_id = run.run_id(); super::start::start_run_with_client(lookup.client(), &run_id, true).await?; + let json = cli.output.format == OutputFormat::Json; if args.detach { - if globals.json { + if json { print_json_pretty(&serde_json::json!({ "run_id": run_id }))?; } else { fabro_util::printout!(printer, "{run_id}"); @@ -36,11 +40,11 @@ pub(crate) async fn resume_command( &run_id, true, styles, - globals.json, + json, printer, ) .await?; - if !globals.json { + if !json { super::output::print_run_summary_with_client( lookup.client(), &run_id, diff --git a/lib/crates/fabro-cli/src/commands/run/rewind.rs b/lib/crates/fabro-cli/src/commands/run/rewind.rs index 89dc129ef..b36cb1353 100644 --- a/lib/crates/fabro-cli/src/commands/run/rewind.rs +++ b/lib/crates/fabro-cli/src/commands/run/rewind.rs @@ -3,6 +3,8 @@ use cli_table::format::{Border, Separator}; use cli_table::{Cell, CellStruct, Color, Style, Table}; use fabro_checkpoint::git::Store; use fabro_types::run_event::{CheckpointCompletedProps, RunRewoundProps, RunSubmittedProps}; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_types::{EventBody, RunEvent}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -13,7 +15,7 @@ use fabro_workflow::operations::{ use git2::Repository; use serde::Serialize; -use crate::args::{GlobalArgs, RewindArgs}; +use crate::args::RewindArgs; use crate::command_context::CommandContext; use crate::commands::store::rebuild::rebuild_run_store; use crate::server_client::ServerStoreClient; @@ -32,11 +34,12 @@ pub(crate) struct TimelineEntryJson { pub(crate) async fn run( args: &RewindArgs, styles: &Styles, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { let repo = Repository::discover(".").context("not in a git repository")?; - let ctx = CommandContext::for_target(&args.server, printer)?; + let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(&args.run_id)?; let run_id = run.run_id(); @@ -50,7 +53,7 @@ pub(crate) async fn run( let timeline = build_timeline_or_rebuild(&store, Some(&run_store), &run_id).await?; if args.list || args.target.is_none() { - if globals.json { + if cli.output.format == OutputFormat::Json { print_json_pretty(&timeline_entries_json(&timeline))?; return Ok(()); } @@ -70,7 +73,7 @@ pub(crate) async fn run( let run_id_string = run_id.to_string(); - if globals.json { + if cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "run_id": run_id_string, "target": args.target.as_deref().unwrap(), diff --git a/lib/crates/fabro-cli/src/commands/run/ssh.rs b/lib/crates/fabro-cli/src/commands/run/ssh.rs index d960f408e..9f54c1997 100644 --- a/lib/crates/fabro-cli/src/commands/run/ssh.rs +++ b/lib/crates/fabro-cli/src/commands/run/ssh.rs @@ -1,18 +1,26 @@ use anyhow::{Result, bail}; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use tracing::info; -use crate::args::{GlobalArgs, SshArgs}; +use crate::args::{SshArgs, require_no_json_override}; use crate::command_context::CommandContext; use crate::server_runs::ServerSummaryLookup; use crate::shared::print_json_pretty; -pub(crate) async fn run(args: SshArgs, globals: &GlobalArgs, printer: Printer) -> Result<()> { - if globals.json && !args.print { - globals.require_no_json()?; +pub(crate) async fn run( + args: SshArgs, + cli: &CliSettings, + cli_layer: &CliLayer, + process_local_json: bool, + printer: Printer, +) -> Result<()> { + if process_local_json && !args.print { + require_no_json_override(process_local_json)?; } - let ctx = CommandContext::for_target(&args.server, printer)?; + let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(&args.run)?; let run_id = run.run_id(); @@ -24,7 +32,7 @@ pub(crate) async fn run(args: SshArgs, globals: &GlobalArgs, printer: Printer) - info!(run_id = %args.run, ttl_minutes = args.ttl, "Creating SSH access"); if args.print { - if globals.json { + if cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "command": ssh.command }))?; } else { { diff --git a/lib/crates/fabro-cli/src/commands/run/wait.rs b/lib/crates/fabro-cli/src/commands/run/wait.rs index 7acf6ffd3..33bfaf18a 100644 --- a/lib/crates/fabro-cli/src/commands/run/wait.rs +++ b/lib/crates/fabro-cli/src/commands/run/wait.rs @@ -2,6 +2,8 @@ use std::io::Write; use anyhow::{Result, bail}; use fabro_types::RunId; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use fabro_workflow::records::Conclusion; @@ -9,7 +11,7 @@ use fabro_workflow::run_status::RunStatus; use tokio::time; use tracing::info; -use crate::args::{GlobalArgs, WaitArgs}; +use crate::args::WaitArgs; use crate::command_context::CommandContext; use crate::server_runs::ServerSummaryLookup; use crate::shared::{format_duration_ms, format_usd_micros}; @@ -22,10 +24,11 @@ const WAIT_STARTUP_GRACE: std::time::Duration = std::time::Duration::from_secs(3 pub(crate) async fn run( args: &WaitArgs, styles: &Styles, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { - let ctx = CommandContext::for_target(&args.server, printer)?; + let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run_info = lookup.resolve(&args.run)?; let client = lookup.client(); @@ -74,7 +77,7 @@ pub(crate) async fn run( let conclusion = client.get_run_state(&run_id).await?.conclusion; - if globals.json { + if cli.output.format == OutputFormat::Json { let json_value = build_json_output(final_status, &run_id, conclusion.as_ref()); let mut out = std::io::stdout().lock(); serde_json::to_writer_pretty(&mut out, &json_value)?; diff --git a/lib/crates/fabro-cli/src/commands/runs/inspect.rs b/lib/crates/fabro-cli/src/commands/runs/inspect.rs index b6f9b01dd..a014e9d57 100644 --- a/lib/crates/fabro-cli/src/commands/runs/inspect.rs +++ b/lib/crates/fabro-cli/src/commands/runs/inspect.rs @@ -1,9 +1,11 @@ use anyhow::Result; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; use fabro_workflow::run_status::RunStatus; use serde::Serialize; -use crate::args::{GlobalArgs, InspectArgs}; +use crate::args::InspectArgs; use crate::command_context::CommandContext; use crate::server_client::RunProjection; use crate::server_runs::{ServerRunSummaryInfo, ServerSummaryLookup}; @@ -19,8 +21,13 @@ pub(crate) struct InspectOutput { pub sandbox: Option, } -pub(crate) async fn run(args: &InspectArgs, _globals: &GlobalArgs, printer: Printer) -> Result<()> { - let ctx = CommandContext::for_target(&args.server, printer)?; +pub(crate) async fn run( + args: &InspectArgs, + cli: &CliSettings, + cli_layer: &CliLayer, + printer: Printer, +) -> Result<()> { + let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(&args.run)?; let run_id = run.run_id(); diff --git a/lib/crates/fabro-cli/src/commands/runs/list.rs b/lib/crates/fabro-cli/src/commands/runs/list.rs index 47d2dfa45..dafe0d418 100644 --- a/lib/crates/fabro-cli/src/commands/runs/list.rs +++ b/lib/crates/fabro-cli/src/commands/runs/list.rs @@ -4,13 +4,15 @@ use anyhow::Result; use chrono::Utc; use cli_table::format::{Border, Separator}; use cli_table::{Cell, CellStruct, Color, Style, Table}; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use fabro_util::text::strip_goal_decoration; use fabro_workflow::run_status::RunStatus; use super::short_run_id; -use crate::args::{GlobalArgs, RunsListArgs}; +use crate::args::RunsListArgs; use crate::command_context::CommandContext; use crate::server_runs::{ServerSummaryLookup, filter_server_runs}; use crate::shared::{color_if, format_duration_ms, tilde_path}; @@ -18,10 +20,11 @@ use crate::shared::{color_if, format_duration_ms, tilde_path}; pub(crate) async fn list_command( args: &RunsListArgs, styles: &Styles, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { - let ctx = CommandContext::for_target(&args.server, printer)?; + let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let label_filters = parse_label_filters(&args.filter.label); let filtered = filter_server_runs( @@ -32,7 +35,7 @@ pub(crate) async fn list_command( !args.all, ); - if globals.json { + if cli.output.format == OutputFormat::Json { let json_rows: Vec<_> = filtered .iter() .map(|run| { diff --git a/lib/crates/fabro-cli/src/commands/runs/mod.rs b/lib/crates/fabro-cli/src/commands/runs/mod.rs index 22dc39a0e..589d553c8 100644 --- a/lib/crates/fabro-cli/src/commands/runs/mod.rs +++ b/lib/crates/fabro-cli/src/commands/runs/mod.rs @@ -1,8 +1,10 @@ use anyhow::Result; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; -use crate::args::{GlobalArgs, RunsCommands}; +use crate::args::RunsCommands; pub(crate) mod inspect; pub(crate) mod list; @@ -10,16 +12,17 @@ pub(crate) mod rm; pub(crate) async fn dispatch( cmd: RunsCommands, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { match cmd { RunsCommands::Ps(args) => { let styles = Styles::detect_stdout(); - list::list_command(&args, &styles, globals, printer).await + list::list_command(&args, &styles, cli, cli_layer, printer).await } - RunsCommands::Rm(args) => rm::remove_command(&args, globals, printer).await, - RunsCommands::Inspect(args) => inspect::run(&args, globals, printer).await, + RunsCommands::Rm(args) => rm::remove_command(&args, cli, cli_layer, printer).await, + RunsCommands::Inspect(args) => inspect::run(&args, cli, cli_layer, printer).await, } } diff --git a/lib/crates/fabro-cli/src/commands/runs/rm.rs b/lib/crates/fabro-cli/src/commands/runs/rm.rs index 4c8a33f57..590e5bca0 100644 --- a/lib/crates/fabro-cli/src/commands/runs/rm.rs +++ b/lib/crates/fabro-cli/src/commands/runs/rm.rs @@ -1,8 +1,10 @@ use anyhow::{Context, Result, bail}; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use super::short_run_id; -use crate::args::{GlobalArgs, RunsRemoveArgs}; +use crate::args::RunsRemoveArgs; use crate::command_context::CommandContext; use crate::server_client; use crate::server_runs::{ @@ -12,21 +14,23 @@ use crate::shared::print_json_pretty; pub(crate) async fn remove_command( args: &RunsRemoveArgs, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { - let ctx = CommandContext::for_target(&args.server, printer)?; + let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; - remove_from(args, lookup.client(), lookup.runs(), globals, printer).await + remove_from(args, lookup.client(), lookup.runs(), cli, printer).await } async fn remove_from( args: &RunsRemoveArgs, client: &server_client::ServerStoreClient, runs: &[ServerRunSummaryInfo], - globals: &GlobalArgs, + cli: &CliSettings, printer: Printer, ) -> Result<()> { + let json = cli.output.format == OutputFormat::Json; let mut had_errors = false; let mut removed = Vec::new(); let mut errors = Vec::new(); @@ -35,7 +39,7 @@ async fn remove_from( let run = match resolve_server_run_from_summaries(runs, identifier) { Ok(run) => run, Err(err) => { - if !globals.json { + if !json { fabro_util::printerr!(printer, "error: {identifier}: {err}"); } errors.push(serde_json::json!({ @@ -54,7 +58,7 @@ async fn remove_from( short_run_id(&run_id), run.status() ); - if !globals.json { + if !json { fabro_util::printerr!(printer, "{error}"); } errors.push(serde_json::json!({ @@ -67,7 +71,7 @@ async fn remove_from( let run_id = run.run_id().to_string(); if let Err(err) = delete_server_run(client, &run).await { - if !globals.json { + if !json { fabro_util::printerr!(printer, "error: {identifier}: {err}"); } errors.push(serde_json::json!({ @@ -78,12 +82,12 @@ async fn remove_from( continue; } removed.push(run_id.clone()); - if !globals.json { + if !json { fabro_util::printerr!(printer, "{}", short_run_id(&run_id)); } } - if globals.json { + if json { print_json_pretty(&serde_json::json!({ "removed": removed, "errors": errors, diff --git a/lib/crates/fabro-cli/src/commands/sandbox/mod.rs b/lib/crates/fabro-cli/src/commands/sandbox/mod.rs index 85885729f..c584f3e81 100644 --- a/lib/crates/fabro-cli/src/commands/sandbox/mod.rs +++ b/lib/crates/fabro-cli/src/commands/sandbox/mod.rs @@ -1,16 +1,24 @@ use anyhow::Result; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; -use crate::args::{GlobalArgs, SandboxCommand}; +use crate::args::SandboxCommand; pub(crate) async fn dispatch( command: SandboxCommand, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, + process_local_json: bool, printer: Printer, ) -> Result<()> { match command { - SandboxCommand::Cp(args) => super::run::cp::cp_command(args, globals, printer).await, - SandboxCommand::Preview(args) => super::run::preview::run(args, globals, printer).await, - SandboxCommand::Ssh(args) => super::run::ssh::run(args, globals, printer).await, + SandboxCommand::Cp(args) => super::run::cp::cp_command(args, cli, cli_layer, printer).await, + SandboxCommand::Preview(args) => { + super::run::preview::run(args, cli, cli_layer, process_local_json, printer).await + } + SandboxCommand::Ssh(args) => { + super::run::ssh::run(args, cli, cli_layer, process_local_json, printer).await + } } } diff --git a/lib/crates/fabro-cli/src/commands/secret/list.rs b/lib/crates/fabro-cli/src/commands/secret/list.rs index 20efbfc71..95dc999a0 100644 --- a/lib/crates/fabro-cli/src/commands/secret/list.rs +++ b/lib/crates/fabro-cli/src/commands/secret/list.rs @@ -3,10 +3,12 @@ use chrono::{DateTime, Utc}; use cli_table::format::{Border, Separator}; use cli_table::{Cell, CellStruct, Style, Table}; use fabro_api::Client; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::OutputFormat; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; -use crate::args::{GlobalArgs, SecretListArgs}; +use crate::args::SecretListArgs; use crate::server_client; use crate::shared::print_json_pretty; @@ -24,7 +26,7 @@ fn format_age(dt: DateTime, now: DateTime) -> String { pub(super) async fn list_command( client: &Client, _args: &SecretListArgs, - globals: &GlobalArgs, + cli: &CliSettings, printer: Printer, ) -> Result<()> { let response = client @@ -33,7 +35,7 @@ pub(super) async fn list_command( .await .map_err(server_client::map_api_error)?; let secrets = response.into_inner().data; - if globals.json { + if cli.output.format == OutputFormat::Json { print_json_pretty(&secrets)?; return Ok(()); } diff --git a/lib/crates/fabro-cli/src/commands/secret/mod.rs b/lib/crates/fabro-cli/src/commands/secret/mod.rs index 5dff3ac2d..c8ac00e8e 100644 --- a/lib/crates/fabro-cli/src/commands/secret/mod.rs +++ b/lib/crates/fabro-cli/src/commands/secret/mod.rs @@ -3,23 +3,24 @@ mod rm; mod set; use anyhow::Result; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; -use crate::args::{GlobalArgs, SecretCommand, SecretNamespace}; +use crate::args::{SecretCommand, SecretNamespace}; use crate::command_context::CommandContext; pub(crate) async fn dispatch( ns: SecretNamespace, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { - let ctx = CommandContext::for_target(&ns.target, printer)?; + let ctx = CommandContext::for_target(&ns.target, printer, cli.clone(), cli_layer)?; let server = ctx.server().await?; match ns.command { - SecretCommand::List(args) => { - list::list_command(server.api(), &args, globals, printer).await - } - SecretCommand::Rm(args) => rm::rm_command(server.api(), &args, globals, printer).await, - SecretCommand::Set(args) => set::set_command(server.api(), &args, globals, printer).await, + SecretCommand::List(args) => list::list_command(server.api(), &args, cli, printer).await, + SecretCommand::Rm(args) => rm::rm_command(server.api(), &args, cli, printer).await, + SecretCommand::Set(args) => set::set_command(server.api(), &args, cli, printer).await, } } diff --git a/lib/crates/fabro-cli/src/commands/secret/rm.rs b/lib/crates/fabro-cli/src/commands/secret/rm.rs index 62a97fe73..08e8fa430 100644 --- a/lib/crates/fabro-cli/src/commands/secret/rm.rs +++ b/lib/crates/fabro-cli/src/commands/secret/rm.rs @@ -1,15 +1,17 @@ use anyhow::Result; use fabro_api::{Client, types}; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::OutputFormat; use fabro_util::printer::Printer; -use crate::args::{GlobalArgs, SecretRmArgs}; +use crate::args::SecretRmArgs; use crate::server_client; use crate::shared::print_json_pretty; pub(super) async fn rm_command( client: &Client, args: &SecretRmArgs, - globals: &GlobalArgs, + cli: &CliSettings, printer: Printer, ) -> Result<()> { client @@ -20,7 +22,7 @@ pub(super) async fn rm_command( .send() .await .map_err(server_client::map_api_error)?; - if globals.json { + if cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "key": args.key }))?; } else { fabro_util::printerr!(printer, "Removed {}", args.key); diff --git a/lib/crates/fabro-cli/src/commands/secret/set.rs b/lib/crates/fabro-cli/src/commands/secret/set.rs index c70b8ad2b..1d3b8494f 100644 --- a/lib/crates/fabro-cli/src/commands/secret/set.rs +++ b/lib/crates/fabro-cli/src/commands/secret/set.rs @@ -1,8 +1,10 @@ use anyhow::Result; use fabro_api::{Client, types}; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::OutputFormat; use fabro_util::printer::Printer; -use crate::args::{GlobalArgs, SecretSetArgs, SecretTypeArg}; +use crate::args::{SecretSetArgs, SecretTypeArg}; use crate::server_client; use crate::shared::print_json_pretty; @@ -16,7 +18,7 @@ fn api_secret_type(secret_type: SecretTypeArg) -> types::SecretType { pub(super) async fn set_command( client: &Client, args: &SecretSetArgs, - globals: &GlobalArgs, + cli: &CliSettings, printer: Printer, ) -> Result<()> { let meta = client @@ -31,7 +33,7 @@ pub(super) async fn set_command( .await .map_err(server_client::map_api_error)? .into_inner(); - if globals.json { + if cli.output.format == OutputFormat::Json { print_json_pretty(&meta)?; } else { fabro_util::printerr!(printer, "Set {}", meta.name); diff --git a/lib/crates/fabro-cli/src/commands/store/dump.rs b/lib/crates/fabro-cli/src/commands/store/dump.rs index fd63ad3fd..6bf71e3cc 100644 --- a/lib/crates/fabro-cli/src/commands/store/dump.rs +++ b/lib/crates/fabro-cli/src/commands/store/dump.rs @@ -6,6 +6,8 @@ use bytes::Bytes; #[cfg(test)] use fabro_store::{ArtifactStore, RunDatabase}; use fabro_store::{EventEnvelope, RunProjection, StageId}; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::OutputFormat; use fabro_types::{RunBlobId, RunId}; use fabro_util::printer::Printer; use fabro_workflow::run_dump::RunDump; @@ -13,7 +15,7 @@ use futures::future::BoxFuture; #[cfg(test)] use serde::de::DeserializeOwned; -use crate::args::{GlobalArgs, StoreDumpArgs}; +use crate::args::StoreDumpArgs; use crate::server_client::ServerStoreClient; use crate::server_runs::ServerRunLookup; use crate::shared::{absolute_or_current, print_json_pretty}; @@ -21,7 +23,7 @@ use crate::user_config::{load_settings_with_storage_dir, storage_dir}; pub(crate) async fn dump_command( args: &StoreDumpArgs, - globals: &GlobalArgs, + cli: &CliSettings, printer: Printer, ) -> Result<()> { let cli_settings = load_settings_with_storage_dir(args.storage_dir.as_deref())?; @@ -31,7 +33,7 @@ pub(crate) async fn dump_command( let state = lookup.client().get_run_state(&run_id).await?; let source = ServerDumpSource::new(lookup.client(), &run_id); let file_count = export_run_from_source(&source, &state, &args.output).await?; - if globals.json { + if cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "run_id": run_id, "output_dir": absolute_or_current(&args.output), diff --git a/lib/crates/fabro-cli/src/commands/store/mod.rs b/lib/crates/fabro-cli/src/commands/store/mod.rs index 3947c5dd5..ac3c9c738 100644 --- a/lib/crates/fabro-cli/src/commands/store/mod.rs +++ b/lib/crates/fabro-cli/src/commands/store/mod.rs @@ -2,16 +2,17 @@ pub(crate) mod dump; pub(crate) mod rebuild; use anyhow::Result; +use fabro_types::settings::CliSettings; use fabro_util::printer::Printer; -use crate::args::{GlobalArgs, StoreCommand, StoreNamespace}; +use crate::args::{StoreCommand, StoreNamespace}; pub(crate) async fn dispatch( ns: StoreNamespace, - globals: &GlobalArgs, + cli: &CliSettings, printer: Printer, ) -> Result<()> { match ns.command { - StoreCommand::Dump(args) => dump::dump_command(&args, globals, printer).await, + StoreCommand::Dump(args) => dump::dump_command(&args, cli, printer).await, } } diff --git a/lib/crates/fabro-cli/src/commands/system/df.rs b/lib/crates/fabro-cli/src/commands/system/df.rs index f76ab1ef2..d13eb984a 100644 --- a/lib/crates/fabro-cli/src/commands/system/df.rs +++ b/lib/crates/fabro-cli/src/commands/system/df.rs @@ -3,19 +3,22 @@ use chrono::{DateTime, Utc}; use cli_table::format::{Border, Justify, Separator}; use cli_table::{Cell, CellStruct, Style, Table}; use fabro_api::types; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; -use crate::args::{DfArgs, GlobalArgs}; +use crate::args::DfArgs; use crate::command_context::CommandContext; use crate::server_client; use crate::shared::{format_size, print_json_pretty}; pub(super) async fn df_command( args: &DfArgs, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { - let ctx = CommandContext::for_connection(&args.connection, printer)?; + let ctx = CommandContext::for_connection(&args.connection, printer, cli.clone(), cli_layer)?; let server = ctx.server().await?; let output = server .api() @@ -26,7 +29,8 @@ pub(super) async fn df_command( .map_err(server_client::map_api_error)? .into_inner(); - let storage_dir = if globals.json { + let json = cli.output.format == OutputFormat::Json; + let storage_dir = if json { None } else { server @@ -39,14 +43,14 @@ pub(super) async fn df_command( .storage_dir }; - df_from(&output, storage_dir.as_deref(), globals) + df_from(&output, storage_dir.as_deref(), json) } #[allow(clippy::print_stdout)] fn df_from( output: &types::DiskUsageResponse, storage_dir: Option<&str>, - globals: &GlobalArgs, + json_output: bool, ) -> Result<()> { let runs_summary = output .summary @@ -82,7 +86,7 @@ fn df_from( }; let log_reclaim_pct = if total_log_size > 0 { 100 } else { 0 }; - if globals.json { + if json_output { print_json_pretty(output)?; return Ok(()); } diff --git a/lib/crates/fabro-cli/src/commands/system/events.rs b/lib/crates/fabro-cli/src/commands/system/events.rs index 64ed80d7e..7a90b7680 100644 --- a/lib/crates/fabro-cli/src/commands/system/events.rs +++ b/lib/crates/fabro-cli/src/commands/system/events.rs @@ -1,17 +1,20 @@ use anyhow::Result; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use futures::StreamExt; -use crate::args::{GlobalArgs, SystemEventsArgs}; +use crate::args::SystemEventsArgs; use crate::command_context::CommandContext; use crate::{server_client, sse}; pub(super) async fn events_command( args: &SystemEventsArgs, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { - let ctx = CommandContext::for_connection(&args.connection, printer)?; + let ctx = CommandContext::for_connection(&args.connection, printer, cli.clone(), cli_layer)?; let server = ctx.server().await?; let mut request = server.api().attach_events(); @@ -23,16 +26,17 @@ pub(super) async fn events_command( let mut stream = response.into_inner(); let mut pending = Vec::new(); + let json = cli.output.format == OutputFormat::Json; while let Some(chunk) = stream.next().await { let chunk = chunk.map_err(|err| anyhow::anyhow!("{err}"))?; pending.extend_from_slice(&chunk); for payload in sse::drain_sse_payloads(&mut pending, false) { - render_sse_payload(&payload, globals.json)?; + render_sse_payload(&payload, json)?; } } for payload in sse::drain_sse_payloads(&mut pending, true) { - render_sse_payload(&payload, globals.json)?; + render_sse_payload(&payload, json)?; } Ok(()) diff --git a/lib/crates/fabro-cli/src/commands/system/info.rs b/lib/crates/fabro-cli/src/commands/system/info.rs index 43d0549cd..c4628d542 100644 --- a/lib/crates/fabro-cli/src/commands/system/info.rs +++ b/lib/crates/fabro-cli/src/commands/system/info.rs @@ -1,17 +1,20 @@ use anyhow::Result; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; -use crate::args::{GlobalArgs, SystemInfoArgs}; +use crate::args::SystemInfoArgs; use crate::command_context::CommandContext; use crate::server_client; use crate::shared::print_json_pretty; pub(super) async fn info_command( args: &SystemInfoArgs, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { - let ctx = CommandContext::for_connection(&args.connection, printer)?; + let ctx = CommandContext::for_connection(&args.connection, printer, cli.clone(), cli_layer)?; let server = ctx.server().await?; let response = server .api() @@ -21,7 +24,7 @@ pub(super) async fn info_command( .map_err(server_client::map_api_error)? .into_inner(); - if globals.json { + if cli.output.format == OutputFormat::Json { print_json_pretty(&response)?; return Ok(()); } diff --git a/lib/crates/fabro-cli/src/commands/system/mod.rs b/lib/crates/fabro-cli/src/commands/system/mod.rs index cb734381d..86d35e367 100644 --- a/lib/crates/fabro-cli/src/commands/system/mod.rs +++ b/lib/crates/fabro-cli/src/commands/system/mod.rs @@ -4,20 +4,23 @@ mod info; mod prune; use anyhow::Result; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::CliLayer; use fabro_util::printer::Printer; pub(crate) use prune::parse_duration; -use crate::args::{GlobalArgs, SystemCommand, SystemNamespace}; +use crate::args::{SystemCommand, SystemNamespace}; pub(crate) async fn dispatch( ns: SystemNamespace, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { match ns.command { - SystemCommand::Info(args) => info::info_command(&args, globals, printer).await, - SystemCommand::Prune(args) => prune::prune_command(&args, globals, printer).await, - SystemCommand::Df(args) => df::df_command(&args, globals, printer).await, - SystemCommand::Events(args) => events::events_command(&args, globals, printer).await, + SystemCommand::Info(args) => info::info_command(&args, cli, cli_layer, printer).await, + SystemCommand::Prune(args) => prune::prune_command(&args, cli, cli_layer, printer).await, + SystemCommand::Df(args) => df::df_command(&args, cli, cli_layer, printer).await, + SystemCommand::Events(args) => events::events_command(&args, cli, cli_layer, printer).await, } } diff --git a/lib/crates/fabro-cli/src/commands/system/prune.rs b/lib/crates/fabro-cli/src/commands/system/prune.rs index 5f405ae7e..cb85235b6 100644 --- a/lib/crates/fabro-cli/src/commands/system/prune.rs +++ b/lib/crates/fabro-cli/src/commands/system/prune.rs @@ -2,20 +2,23 @@ use std::collections::HashMap; use anyhow::{Context, Result, bail}; use fabro_api::types; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; use fabro_util::printer::Printer; use tracing::{debug, info}; -use crate::args::{GlobalArgs, RunsPruneArgs}; +use crate::args::RunsPruneArgs; use crate::command_context::CommandContext; use crate::server_client; use crate::shared::{format_size, print_json_pretty}; pub(super) async fn prune_command( args: &RunsPruneArgs, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> Result<()> { - let ctx = CommandContext::for_connection(&args.connection, printer)?; + let ctx = CommandContext::for_connection(&args.connection, printer, cli.clone(), cli_layer)?; let server = ctx.server().await?; let response = server .api() @@ -32,7 +35,7 @@ pub(super) async fn prune_command( .await .map_err(server_client::map_api_error)? .into_inner(); - prune_from(&response, globals, printer) + prune_from(&response, cli.output.format == OutputFormat::Json, printer) } pub(crate) fn parse_duration(s: &str) -> Result { @@ -53,7 +56,7 @@ pub(crate) fn parse_duration(s: &str) -> Result { fn prune_from( response: &types::PruneRunsResponse, - globals: &GlobalArgs, + json_output: bool, printer: Printer, ) -> Result<()> { let total_count = response.total_count.unwrap_or_default(); @@ -66,7 +69,7 @@ fn prune_from( "pruning runs" ); - if globals.json { + if json_output { print_json_pretty(response)?; return Ok(()); } diff --git a/lib/crates/fabro-cli/src/commands/uninstall.rs b/lib/crates/fabro-cli/src/commands/uninstall.rs index 9cd9593b0..c1d56b49f 100644 --- a/lib/crates/fabro-cli/src/commands/uninstall.rs +++ b/lib/crates/fabro-cli/src/commands/uninstall.rs @@ -4,12 +4,14 @@ use std::path::{Path, PathBuf}; use std::time::Duration; use anyhow::{Context, Result}; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::OutputFormat; use fabro_util::Home; use fabro_util::printer::Printer; use serde::Serialize; use tracing::warn; -use crate::args::{GlobalArgs, UninstallArgs}; +use crate::args::UninstallArgs; use crate::commands::server; use crate::shared::{format_size, print_json_pretty, tilde_path}; use crate::user_config; @@ -29,14 +31,15 @@ struct Inventory { #[allow(clippy::unused_async)] // call site requires async pub(crate) async fn run_uninstall( args: &UninstallArgs, - globals: &GlobalArgs, + cli: &CliSettings, printer: Printer, ) -> Result<()> { + let json = cli.output.format == OutputFormat::Json; let home = Home::from_env(); let home_root = home.root().to_path_buf(); if !looks_like_fabro_home(&home_root) { - if globals.json { + if json { print_json_pretty(&serde_json::json!({ "status": "not_installed" }))?; } else { fabro_util::printerr!(printer, "Fabro is not installed."); @@ -52,7 +55,7 @@ pub(crate) async fn run_uninstall( let inventory = build_inventory(&home_root, &storage_dir); if !args.yes { - if globals.json { + if json { print_json_pretty(&inventory)?; } else { print_preview(&inventory, printer); @@ -60,7 +63,7 @@ pub(crate) async fn run_uninstall( return Ok(()); } - execute_uninstall(&inventory, globals.json, printer).await + execute_uninstall(&inventory, json, printer).await } fn build_inventory(home_root: &Path, storage_dir: &Path) -> Inventory { diff --git a/lib/crates/fabro-cli/src/commands/upgrade.rs b/lib/crates/fabro-cli/src/commands/upgrade.rs index 6b5886690..172b99123 100644 --- a/lib/crates/fabro-cli/src/commands/upgrade.rs +++ b/lib/crates/fabro-cli/src/commands/upgrade.rs @@ -3,6 +3,8 @@ use std::io::{IsTerminal, Write}; use std::path::{Path, PathBuf}; use anyhow::{Context, Result, bail}; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::OutputFormat; use fabro_util::printer::Printer; use semver::Version; use sha2::{Digest, Sha256}; @@ -10,7 +12,7 @@ use tokio::process::Command as TokioCommand; use tokio::task::JoinHandle; use tracing::debug; -use crate::args::{GlobalArgs, UpgradeArgs}; +use crate::args::UpgradeArgs; use crate::shared::print_json_pretty; // ── Download backend abstraction ─────────────────────────────────────────── @@ -226,7 +228,7 @@ impl UpgradeCheckState { pub(crate) async fn run_upgrade( args: UpgradeArgs, - globals: &GlobalArgs, + cli: &CliSettings, printer: Printer, ) -> Result<()> { let backend = select_backend().await; @@ -268,7 +270,7 @@ pub(crate) async fn run_upgrade( } } std::cmp::Ordering::Equal if !args.force => { - if globals.json { + if cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "previous_version": current.to_string(), "installed_version": current.to_string(), @@ -282,7 +284,7 @@ pub(crate) async fn run_upgrade( } if args.dry_run { - if globals.json { + if cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "previous_version": current.to_string(), "installed_version": target.to_string(), @@ -358,7 +360,7 @@ pub(crate) async fn run_upgrade( let _ = fs::set_permissions(¤t_exe, fs::Permissions::from_mode(0o755)); } - if globals.json { + if cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "previous_version": current.to_string(), "installed_version": target.to_string(), @@ -374,12 +376,8 @@ pub(crate) async fn run_upgrade( /// Spawn a background task that checks for a newer version and prints a notice /// to stderr after the main command completes. Returns a handle that should be /// awaited at the end of `main_inner`. -pub(crate) fn spawn_upgrade_check( - no_upgrade_check: bool, - upgrade_check_enabled: bool, - printer: Printer, -) -> Option> { - if no_upgrade_check || !upgrade_check_enabled { +pub(crate) fn spawn_upgrade_check(check: bool, printer: Printer) -> Option> { + if !check { return None; } Some(tokio::spawn(async move { diff --git a/lib/crates/fabro-cli/src/commands/validate.rs b/lib/crates/fabro-cli/src/commands/validate.rs index 7de1102c5..a530e5470 100644 --- a/lib/crates/fabro-cli/src/commands/validate.rs +++ b/lib/crates/fabro-cli/src/commands/validate.rs @@ -1,11 +1,12 @@ use anyhow::bail; use fabro_config::load::load_settings_user; use fabro_config::user::active_settings_path; -use fabro_types::settings::SettingsLayer; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; +use fabro_types::settings::{CliSettings, SettingsLayer}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; -use crate::args::{GlobalArgs, ValidateArgs}; +use crate::args::ValidateArgs; use crate::command_context::CommandContext; use crate::commands::run::output::api_diagnostics_to_local; use crate::manifest_builder::{ManifestBuildInput, build_run_manifest}; @@ -14,10 +15,11 @@ use crate::shared::{print_diagnostics, print_json_pretty, relative_path}; pub(crate) async fn run( args: &ValidateArgs, styles: &Styles, - globals: &GlobalArgs, + cli: &CliSettings, + cli_layer: &CliLayer, printer: Printer, ) -> anyhow::Result<()> { - let ctx = CommandContext::for_target(&args.target, printer)?; + let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?; let built = build_run_manifest(ManifestBuildInput { workflow: args.workflow.clone(), cwd: ctx.cwd().to_path_buf(), @@ -31,7 +33,7 @@ pub(crate) async fn run( let response = client.run_preflight(built.manifest).await?; let diagnostics = api_diagnostics_to_local(&response.workflow.diagnostics); - if globals.json { + if cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "workflow_name": response.workflow.name, "nodes": response.workflow.nodes, diff --git a/lib/crates/fabro-cli/src/commands/workflow/create.rs b/lib/crates/fabro-cli/src/commands/workflow/create.rs index ee3437f2b..c09b4ae1d 100644 --- a/lib/crates/fabro-cli/src/commands/workflow/create.rs +++ b/lib/crates/fabro-cli/src/commands/workflow/create.rs @@ -2,14 +2,16 @@ use std::path::Path; use anyhow::{Context, Result, bail}; use fabro_config::project::{discover_project_config, resolve_fabro_root}; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::OutputFormat; use fabro_util::printer::Printer; -use crate::args::{GlobalArgs, WorkflowCreateArgs}; +use crate::args::WorkflowCreateArgs; use crate::shared::{print_json_pretty, relative_path}; pub(super) fn create_command( args: &WorkflowCreateArgs, - globals: &GlobalArgs, + cli: &CliSettings, printer: Printer, ) -> Result<()> { let cwd = std::env::current_dir()?; @@ -24,7 +26,7 @@ pub(super) fn create_command( let fabro_root = resolve_fabro_root(&config_path, &config); let created = write_workflow_scaffold(args, &fabro_root)?; - if globals.json { + if cli.output.format == OutputFormat::Json { let created: Vec<_> = created.iter().map(|path| relative_path(path)).collect(); print_json_pretty(&serde_json::json!({ "name": args.name, diff --git a/lib/crates/fabro-cli/src/commands/workflow/list.rs b/lib/crates/fabro-cli/src/commands/workflow/list.rs index 15e66df33..5f79e8205 100644 --- a/lib/crates/fabro-cli/src/commands/workflow/list.rs +++ b/lib/crates/fabro-cli/src/commands/workflow/list.rs @@ -5,17 +5,19 @@ use fabro_config::project::{ WorkflowInfo, WorkflowSource, discover_project_config, list_workflows_detailed, resolve_fabro_root, }; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::OutputFormat; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; -use crate::args::{GlobalArgs, WorkflowListArgs}; +use crate::args::WorkflowListArgs; use crate::shared::{color_if, print_json_pretty, relative_path}; const GOAL_MAX_LEN: usize = 60; pub(super) fn list_command( _args: &WorkflowListArgs, - globals: &GlobalArgs, + cli: &CliSettings, printer: Printer, ) -> Result<()> { let styles = Styles::detect_stderr(); @@ -34,7 +36,7 @@ pub(super) fn list_command( let workflows = list_workflows_detailed(Some(&project_wf_dir), user_wf_dir.as_deref()); - if globals.json { + if cli.output.format == OutputFormat::Json { print_json_pretty(&workflows)?; return Ok(()); } diff --git a/lib/crates/fabro-cli/src/commands/workflow/mod.rs b/lib/crates/fabro-cli/src/commands/workflow/mod.rs index 0c33d24de..46478dd2c 100644 --- a/lib/crates/fabro-cli/src/commands/workflow/mod.rs +++ b/lib/crates/fabro-cli/src/commands/workflow/mod.rs @@ -2,17 +2,14 @@ mod create; mod list; use anyhow::Result; +use fabro_types::settings::CliSettings; use fabro_util::printer::Printer; -use crate::args::{GlobalArgs, WorkflowCommand, WorkflowNamespace}; +use crate::args::{WorkflowCommand, WorkflowNamespace}; -pub(crate) fn dispatch( - ns: WorkflowNamespace, - globals: &GlobalArgs, - printer: Printer, -) -> Result<()> { +pub(crate) fn dispatch(ns: WorkflowNamespace, cli: &CliSettings, printer: Printer) -> Result<()> { match ns.command { - WorkflowCommand::List(args) => list::list_command(&args, globals, printer), - WorkflowCommand::Create(args) => create::create_command(&args, globals, printer), + WorkflowCommand::List(args) => list::list_command(&args, cli, printer), + WorkflowCommand::Create(args) => create::create_command(&args, cli, printer), } } diff --git a/lib/crates/fabro-cli/src/main.rs b/lib/crates/fabro-cli/src/main.rs index 6f1a86d13..35481ee2c 100644 --- a/lib/crates/fabro-cli/src/main.rs +++ b/lib/crates/fabro-cli/src/main.rs @@ -18,10 +18,15 @@ mod user_config; use std::ffi::OsString; use anyhow::Result; -use args::{Commands, GlobalArgs, LONG_VERSION, RunCommands, ServerCommand, ServerNamespace}; +use args::{ + Commands, GlobalArgs, LONG_VERSION, RunCommands, ServerCommand, ServerNamespace, + global_args_cli_layer, printer_from_verbosity, require_no_json_override, +}; use clap::{CommandFactory, Parser}; +use fabro_config::merge::combine_files; use fabro_config::user::load_settings_config; use fabro_telemetry::{git, panic as tel_panic, sanitize, sender}; +use fabro_types::settings::SettingsLayer; use fabro_types::settings::cli::OutputVerbosity; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -120,43 +125,45 @@ async fn main_inner() -> (String, Result<()>) { let cli = Cli::parse(); let Cli { globals, command } = cli; - let printer = Printer::from_flags(globals.quiet, globals.verbose); + 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 (config_log_level, upgrade_check_enabled) = { - if let Commands::Server(ServerNamespace { - command: - ServerCommand::Start(args::ServerStartArgs { - serve_args: args, .. - }) - | ServerCommand::Serve(args::ServerServeArgs { - serve_args: args, .. - }), - }) = command.as_ref() - { - match load_settings_config(args.config.as_deref()) { - Ok(layer) => { - let server_settings = layer; - ( - server_settings - .server - .as_ref() - .and_then(|server| server.logging.as_ref()) - .and_then(|logging| logging.level.clone()), - false, - ) - } - Err(err) => return (command_name, Err(err.into())), - } - } else { - match user_config::load_settings() { - Ok(cli_settings) => match user_config::resolve_cli_settings(&cli_settings) { - Ok(resolved_cli) => (resolved_cli.logging.level, resolved_cli.updates.check), - Err(err) => return (command_name, Err(err)), - }, - Err(err) => return (command_name, Err(err)), - } + let user_settings = match user_config::load_settings() { + Ok(settings) => settings, + Err(err) => return (command_name, Err(err)), + }; + let combined_settings = combine_files(user_settings, SettingsLayer { + cli: Some(cli_layer.clone()), + ..SettingsLayer::default() + }); + let cli_settings = match user_config::resolve_cli_settings(&combined_settings) { + Ok(cli_settings) => cli_settings, + Err(err) => return (command_name, Err(err)), + }; + let printer = printer_from_verbosity(cli_settings.output.verbosity); + + let config_log_level = if let Commands::Server(ServerNamespace { + command: + ServerCommand::Start(args::ServerStartArgs { + serve_args: args, .. + }) + | ServerCommand::Serve(args::ServerServeArgs { + serve_args: args, .. + }), + }) = command.as_ref() + { + match load_settings_config(args.config.as_deref()) { + Ok(layer) => layer + .server + .as_ref() + .and_then(|server| server.logging.as_ref()) + .and_then(|logging| logging.level.clone()), + Err(err) => return (command_name, Err(err.into())), } + } else { + cli_settings.logging.level.clone() }; let log_prefix = if command_name == "server start" || command_name == "server __serve" { @@ -166,7 +173,10 @@ async fn main_inner() -> (String, Result<()>) { }; if let Err(err) = logging::init_tracing(globals.debug, config_log_level.as_deref(), log_prefix) { - fabro_util::printerr!(printer, "Warning: failed to initialize logging: {err:#}"); + fabro_util::printerr!( + bootstrap_printer, + "Warning: failed to initialize logging: {err:#}" + ); } debug!(command = %command_name, "CLI command started"); @@ -178,59 +188,74 @@ async fn main_inner() -> (String, Result<()>) { | Commands::Repo(_) | Commands::Install(_) ) { - commands::upgrade::spawn_upgrade_check( - globals.no_upgrade_check, - upgrade_check_enabled, - printer, - ) + commands::upgrade::spawn_upgrade_check(cli_settings.updates.check, printer) } else { None }; let result = Box::pin(async move { match *command { - Commands::Exec(args) => commands::exec::execute(args, &globals, printer).await?, + Commands::Exec(args) => commands::exec::execute(args, &cli_settings, printer).await?, Commands::RunCmd(cmd) => { - Box::pin(commands::run::dispatch(cmd, &globals, printer)).await?; + Box::pin(commands::run::dispatch( + cmd, + &cli_settings, + &cli_layer, + process_local_json, + printer, + )) + .await?; } Commands::Preflight(args) => { - commands::preflight::execute(args, &globals, printer).await?; + commands::preflight::execute(args, &cli_settings, &cli_layer, printer).await?; } Commands::Validate(args) => { let styles = Styles::detect_stderr(); - commands::validate::run(&args, &styles, &globals, printer).await?; + commands::validate::run(&args, &styles, &cli_settings, &cli_layer, printer).await?; } Commands::Graph(args) => { let styles = Styles::detect_stderr(); - commands::graph::run(&args, &styles, &globals, printer).await?; + commands::graph::run( + &args, + &styles, + &cli_settings, + &cli_layer, + process_local_json, + printer, + ) + .await?; } Commands::Parse(args) => { - commands::parse::run(&args, &globals, printer)?; + commands::parse::run(&args, &cli_settings, printer)?; + } + Commands::Artifact(ns) => { + commands::artifact::dispatch(ns, &cli_settings, &cli_layer, printer).await?; + } + Commands::Store(ns) => commands::store::dispatch(ns, &cli_settings, printer).await?, + Commands::RunsCmd(cmd) => { + commands::runs::dispatch(cmd, &cli_settings, &cli_layer, printer).await?; } - Commands::Artifact(ns) => commands::artifact::dispatch(ns, &globals, printer).await?, - Commands::Store(ns) => commands::store::dispatch(ns, &globals, printer).await?, - Commands::RunsCmd(cmd) => commands::runs::dispatch(cmd, &globals, printer).await?, Commands::Model { command } => { - commands::model::execute(command, &globals, printer).await?; + commands::model::execute(command, &cli_settings, &cli_layer, printer).await?; } Commands::Server(ns) => { Box::pin(commands::server::dispatch(ns.command, &globals, printer)).await?; } Commands::Doctor(args) => { - let cli_settings = user_config::load_settings()?; - let verbose = args.verbose - || user_config::resolve_cli_settings(&cli_settings)? - .output - .verbosity - == OutputVerbosity::Verbose; + let verbose = + args.verbose || cli_settings.output.verbosity == OutputVerbosity::Verbose; let exit_code = Box::pin(commands::doctor::run_doctor( - &args, verbose, &globals, printer, + &args, + verbose, + &cli_settings, + &cli_layer, + printer, )) .await?; std::process::exit(exit_code); } Commands::Discord => { - if globals.json { + if process_local_json { shared::print_json_pretty(&serde_json::json!({ "url": "https://fabro.sh/discord", }))?; @@ -239,7 +264,7 @@ async fn main_inner() -> (String, Result<()>) { } } Commands::Docs => { - if globals.json { + if process_local_json { shared::print_json_pretty(&serde_json::json!({ "url": "https://docs.fabro.sh/", }))?; @@ -247,29 +272,72 @@ async fn main_inner() -> (String, Result<()>) { open::that("https://docs.fabro.sh/")?; } } - Commands::Repo(ns) => commands::repo::dispatch(ns, &globals, printer).await?, + Commands::Repo(ns) => { + commands::repo::dispatch(ns, &cli_settings, &cli_layer, printer).await?; + } Commands::Install(args) => { - Box::pin(commands::install::run_install(&args, &globals, printer)).await?; + Box::pin(commands::install::run_install( + &args, + &cli_settings, + &cli_layer, + process_local_json, + printer, + )) + .await?; } Commands::Uninstall(args) => { - commands::uninstall::run_uninstall(&args, &globals, printer).await?; + commands::uninstall::run_uninstall(&args, &cli_settings, printer).await?; + } + Commands::Pr(ns) => { + Box::pin(commands::pr::dispatch( + ns, + &cli_settings, + &cli_layer, + printer, + )) + .await?; + } + Commands::Secret(ns) => { + commands::secret::dispatch(ns, &cli_settings, &cli_layer, printer).await?; } - Commands::Pr(ns) => Box::pin(commands::pr::dispatch(ns, &globals, printer)).await?, - Commands::Secret(ns) => commands::secret::dispatch(ns, &globals, printer).await?, Commands::Settings(args) => { - Box::pin(commands::config::execute(&args, &globals, printer)).await?; + Box::pin(commands::config::execute( + &args, + &cli_settings, + &cli_layer, + printer, + )) + .await?; } - Commands::Workflow(ns) => commands::workflow::dispatch(ns, &globals, printer)?, + Commands::Workflow(ns) => commands::workflow::dispatch(ns, &cli_settings, printer)?, Commands::Upgrade(args) => { - commands::upgrade::run_upgrade(args, &globals, printer).await?; + commands::upgrade::run_upgrade(args, &cli_settings, printer).await?; + } + Commands::Provider(ns) => { + commands::provider::dispatch( + ns, + &cli_settings, + &cli_layer, + process_local_json, + printer, + ) + .await?; } - Commands::Provider(ns) => commands::provider::dispatch(ns, &globals, printer).await?, Commands::Sandbox { command } => { - commands::sandbox::dispatch(command, &globals, printer).await?; + commands::sandbox::dispatch( + command, + &cli_settings, + &cli_layer, + process_local_json, + printer, + ) + .await?; + } + Commands::System(ns) => { + commands::system::dispatch(ns, &cli_settings, &cli_layer, printer).await?; } - Commands::System(ns) => commands::system::dispatch(ns, &globals, printer).await?, Commands::Completion(args) => { - globals.require_no_json()?; + 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(|| { diff --git a/lib/crates/fabro-cli/tests/it/cmd/create.rs b/lib/crates/fabro-cli/tests/it/cmd/create.rs index 7441032cc..5a5ba1f6a 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/create.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/create.rs @@ -409,7 +409,7 @@ fn create_persists_requested_overrides_into_store() { } #[test] -fn create_json_implies_auto_approve() { +fn create_json_does_not_imply_auto_approve() { let context = test_context!(); let workflow = fixture("simple.fabro"); let output = context @@ -442,7 +442,7 @@ fn create_json_implies_auto_approve() { ) .execution .approval - == fabro_types::settings::run::ApprovalMode::Auto + != fabro_types::settings::run::ApprovalMode::Auto ); } diff --git a/lib/crates/fabro-cli/tests/it/cmd/json_global.rs b/lib/crates/fabro-cli/tests/it/cmd/json_global.rs index 4b5a021b3..8bee410e6 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/json_global.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/json_global.rs @@ -32,6 +32,62 @@ fn settings_json_outputs_parseable_json() { assert!(value.is_object()); } +#[test] +fn settings_uses_json_output_format_from_home_config() { + let context = test_context!(); + context.write_home( + ".fabro/settings.toml", + "_version = 1\n\n[cli.output]\nformat = \"json\"\n", + ); + + let output = context.settings().output().expect("command should run"); + + assert!(output.status.success()); + let value: Value = + serde_json::from_slice(&output.stdout).expect("settings config JSON should parse"); + assert!(value.is_object()); +} + +#[test] +fn secret_list_uses_json_output_format_from_home_config() { + let context = test_context!(); + context.write_home( + ".fabro/settings.toml", + "_version = 1\n\n[cli.output]\nformat = \"json\"\n", + ); + + let output = context + .command() + .args(["secret", "list"]) + .output() + .expect("command should run"); + + assert!(output.status.success()); + let value: Value = + serde_json::from_slice(&output.stdout).expect("secret list config JSON should parse"); + assert_eq!(value, Value::Array(vec![])); +} + +#[test] +fn completion_succeeds_with_json_output_format_from_home_config() { + let context = test_context!(); + context.write_home( + ".fabro/settings.toml", + "_version = 1\n\n[cli.output]\nformat = \"json\"\n", + ); + + let output = context + .command() + .args(["completion", "zsh"]) + .output() + .expect("command should run"); + + assert!(output.status.success()); + let stdout = output_stdout(&output); + assert!(stdout.contains("#compdef")); + assert!(serde_json::from_slice::(&output.stdout).is_err()); +} + #[test] fn ps_supports_global_flag_and_env_var() { let context = test_context!(); diff --git a/lib/crates/fabro-cli/tests/it/cmd/run.rs b/lib/crates/fabro-cli/tests/it/cmd/run.rs index f799ff33b..3a5f16280 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/run.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/run.rs @@ -835,13 +835,13 @@ fn run_id_passthrough_uses_provided_ulid() { } #[test] -fn json_run_implies_auto_approve_for_human_gates() { +fn json_run_requires_manual_input_for_human_gates_without_auto_approve() { let context = test_context!(); let workflow = context.temp_dir.join("human-gate.fabro"); context.write_temp( "human-gate.fabro", r#"digraph HumanGate { - graph [goal="Route through the default approval path"] + graph [goal="Require explicit approval before continuing"] start [shape=Mdiamond, label="Start"] exit [shape=Msquare, label="Exit"] approve [shape=hexagon, label="Approve?"] @@ -870,806 +870,31 @@ fn json_run_implies_auto_approve_for_human_gates() { .expect("command should execute"); assert!( - output.status.success(), - "command failed:\nstdout:\n{}\nstderr:\n{}", + !output.status.success(), + "command unexpectedly succeeded:\nstdout:\n{}\nstderr:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); - let mut progress: Vec = String::from_utf8(output.stdout) + let stderr = String::from_utf8(output.stderr).expect("stderr should be UTF-8"); + assert!( + stderr.contains("This run is waiting for human input, but --json is non-interactive."), + "stderr should explain the non-interactive interview failure:\n{stderr}" + ); + + let progress: Vec = String::from_utf8(output.stdout) .expect("stdout should be UTF-8") .lines() .filter(|line| !line.trim().is_empty()) .map(|line| serde_json::from_str(line).expect("run JSON output should be JSONL")) .collect(); - for event in &mut progress { - if let Some(properties) = event.get_mut("properties").and_then(Value::as_object_mut) { - if properties.contains_key("manifest_blob") { - properties.insert( - "manifest_blob".to_string(), - Value::String("[BLOB_ID]".to_string()), - ); - } - if properties.contains_key("definition_blob") { - properties.insert( - "definition_blob".to_string(), - Value::String("[BLOB_ID]".to_string()), - ); - } - } - // Strip fields that vary between runs (version, server stanzas that - // carry machine-specific values, cli.target sockets). - if let Some(settings) = event - .pointer_mut("/properties/settings") - .and_then(Value::as_object_mut) - { - settings.remove("_version"); - settings.remove("server"); - settings.remove("version"); - } - if let Some(target) = event - .pointer_mut("/properties/settings/cli/target") - .and_then(Value::as_object_mut) - { - if target.contains_key("path") { - target.insert( - "path".to_string(), - Value::String("[CLI_SOCKET]".to_string()), - ); - } - } - let Some(model) = event.pointer_mut("/properties/settings/run/model") else { - continue; - }; - let Some(model) = model.as_object_mut() else { - continue; - }; - model.insert("name".to_string(), Value::String("[LLM_MODEL]".to_string())); - model.insert( - "provider".to_string(), - Value::String("[LLM_PROVIDER]".to_string()), - ); - } - fabro_json_snapshot!(context, &progress, @r#" - [ - { - "actor": { - "display": "dev", - "id": "dev", - "kind": "user" - }, - "event": "run.created", - "id": "[EVENT_ID]", - "properties": { - "graph": { - "attrs": { - "goal": { - "String": "Route through the default approval path" - } - }, - "edges": [ - { - "attrs": {}, - "from": "start", - "to": "approve" - }, - { - "attrs": { - "label": { - "String": "[A] Approve" - } - }, - "from": "approve", - "to": "ship" - }, - { - "attrs": { - "label": { - "String": "[R] Revise" - } - }, - "from": "approve", - "to": "revise" - }, - { - "attrs": {}, - "from": "ship", - "to": "exit" - }, - { - "attrs": {}, - "from": "revise", - "to": "exit" - } - ], - "name": "HumanGate", - "nodes": { - "approve": { - "attrs": { - "label": { - "String": "Approve?" - }, - "shape": { - "String": "hexagon" - } - }, - "id": "approve" - }, - "exit": { - "attrs": { - "label": { - "String": "Exit" - }, - "shape": { - "String": "Msquare" - } - }, - "id": "exit" - }, - "revise": { - "attrs": { - "script": { - "String": "echo revised" - }, - "shape": { - "String": "parallelogram" - } - }, - "id": "revise" - }, - "ship": { - "attrs": { - "script": { - "String": "echo shipped" - }, - "shape": { - "String": "parallelogram" - } - }, - "id": "ship" - }, - "start": { - "attrs": { - "label": { - "String": "Start" - }, - "shape": { - "String": "Mdiamond" - } - }, - "id": "start" - } - } - }, - "host_repo_path": "[TEMP_DIR]", - "manifest_blob": "[BLOB_ID]", - "provenance": { - "client": { - "name": "fabro-cli", - "user_agent": "fabro-cli/0.176.2", - "version": "0.176.2" - }, - "server": { - "version": "0.176.2" - }, - "subject": { - "auth_method": "dev_token", - "login": "dev" - } - }, - "run_dir": "[RUN_DIR]", - "settings": { - "workflow": { - "graph": "workflow.fabro" - }, - "cli": { - "exec": { - "prevent_idle_sleep": false - }, - "output": { - "format": "text", - "verbosity": "normal" - }, - "target": { - "path": "[CLI_SOCKET]", - "type": "unix" - }, - "updates": { - "check": true - } - }, - "features": { - "session_sandboxes": false - }, - "project": { - "directory": "." - }, - "run": { - "execution": { - "approval": "auto", - "mode": "normal", - "retros": false - }, - "goal": "Route through the default approval path", - "model": { - "name": "[LLM_MODEL]", - "provider": "[LLM_PROVIDER]" - }, - "prepare": { - "timeout": "5m" - }, - "sandbox": { - "devcontainer": false, - "local": { - "worktree_mode": "clean" - }, - "preserve": false, - "provider": "local" - } - } - }, - "workflow_slug": "human-gate", - "workflow_source": "digraph HumanGate {/n graph [goal=\"Route through the default approval path\"]/n start [shape=Mdiamond, label=\"Start\"]/n exit [shape=Msquare, label=\"Exit\"]/n approve [shape=hexagon, label=\"Approve?\"]/n ship [shape=parallelogram, script=\"echo shipped\"]/n revise [shape=parallelogram, script=\"echo revised\"]/n start -> approve/n approve -> ship [label=\"[A] Approve\"]/n approve -> revise [label=\"[R] Revise\"]/n ship -> exit/n revise -> exit/n}/n", - "working_directory": "[TEMP_DIR]" - }, - "run_id": "[ULID]", - "ts": "[TIMESTAMP]" - }, - { - "event": "run.submitted", - "id": "[EVENT_ID]", - "properties": { - "definition_blob": "[BLOB_ID]" - }, - "run_id": "[ULID]", - "ts": "[TIMESTAMP]" - }, - { - "event": "run.starting", - "id": "[EVENT_ID]", - "properties": { - "reason": "sandbox_initializing" - }, - "run_id": "[ULID]", - "ts": "[TIMESTAMP]" - }, - { - "event": "sandbox.initializing", - "id": "[EVENT_ID]", - "properties": { - "provider": "local" - }, - "run_id": "[ULID]", - "ts": "[TIMESTAMP]" - }, - { - "event": "sandbox.ready", - "id": "[EVENT_ID]", - "properties": { - "duration_ms": "[DURATION_MS]", - "provider": "local" - }, - "run_id": "[ULID]", - "ts": "[TIMESTAMP]" - }, - { - "event": "sandbox.initialized", - "id": "[EVENT_ID]", - "properties": { - "provider": "local", - "working_directory": "[TEMP_DIR]" - }, - "run_id": "[ULID]", - "ts": "[TIMESTAMP]" - }, - { - "event": "run.started", - "id": "[EVENT_ID]", - "properties": { - "goal": "Route through the default approval path", - "name": "HumanGate" - }, - "run_id": "[ULID]", - "ts": "[TIMESTAMP]" - }, - { - "event": "run.running", - "id": "[EVENT_ID]", - "properties": {}, - "run_id": "[ULID]", - "ts": "[TIMESTAMP]" - }, - { - "event": "stage.started", - "id": "[EVENT_ID]", - "node_id": "start", - "node_label": "Start", - "properties": { - "attempt": 1, - "handler_type": "start", - "index": 0, - "max_attempts": 1 - }, - "run_id": "[ULID]", - "stage_id": "start@1", - "ts": "[TIMESTAMP]" - }, - { - "event": "stage.completed", - "id": "[EVENT_ID]", - "node_id": "start", - "node_label": "Start", - "properties": { - "attempt": 1, - "context_values": { - "current.preamble": "Goal: Route through the default approval path/n", - "current_node": "start", - "graph.goal": "Route through the default approval path", - "internal.fidelity": "compact", - "internal.node_visit_count": 1, - "internal.run_id": "[ULID]", - "internal.thread_id": null - }, - "duration_ms": "[DURATION_MS]", - "index": 0, - "max_attempts": 1, - "node_visits": { - "start": 1 - }, - "status": "success" - }, - "run_id": "[ULID]", - "stage_id": "start@1", - "ts": "[TIMESTAMP]" - }, - { - "event": "edge.selected", - "id": "[EVENT_ID]", - "properties": { - "from_node": "start", - "is_jump": false, - "reason": "unconditional", - "stage_status": "success", - "to_node": "approve" - }, - "run_id": "[ULID]", - "ts": "[TIMESTAMP]" - }, - { - "event": "checkpoint.completed", - "id": "[EVENT_ID]", - "node_id": "start", - "node_label": "start", - "properties": { - "completed_nodes": [ - "start" - ], - "context_values": { - "current_node": "start", - "failure_class": "", - "failure_signature": "", - "graph.goal": "Route through the default approval path", - "internal.fidelity": "compact", - "internal.node_visit_count": 1, - "internal.retry_count.start": 0, - "internal.run_id": "[ULID]", - "internal.thread_id": null, - "outcome": "success" - }, - "current_node": "start", - "next_node_id": "approve", - "node_outcomes": { - "start": { - "status": "success", - "usage": null - } - }, - "node_visits": { - "start": 1 - }, - "status": "success" - }, - "run_id": "[ULID]", - "stage_id": "start@1", - "ts": "[TIMESTAMP]" - }, - { - "event": "stage.started", - "id": "[EVENT_ID]", - "node_id": "approve", - "node_label": "Approve?", - "properties": { - "attempt": 1, - "handler_type": "human", - "index": 1, - "max_attempts": 1 - }, - "run_id": "[ULID]", - "stage_id": "approve@1", - "ts": "[TIMESTAMP]" - }, - { - "event": "interview.started", - "id": "[EVENT_ID]", - "node_id": "approve", - "node_label": "approve", - "properties": { - "allow_freeform": false, - "options": [ - { - "key": "A", - "label": "[A] Approve" - }, - { - "key": "R", - "label": "[R] Revise" - } - ], - "question": "Approve?", - "question_id": "[ULID]", - "question_type": "multiple_choice", - "stage": "approve" - }, - "run_id": "[ULID]", - "stage_id": "approve@1", - "ts": "[TIMESTAMP]" - }, - { - "event": "interview.completed", - "id": "[EVENT_ID]", - "node_id": "approve", - "node_label": "approve", - "properties": { - "answer": "A", - "duration_ms": "[DURATION_MS]", - "question": "Approve?", - "question_id": "[ULID]" - }, - "run_id": "[ULID]", - "stage_id": "approve@1", - "ts": "[TIMESTAMP]" - }, - { - "event": "stage.completed", - "id": "[EVENT_ID]", - "node_id": "approve", - "node_label": "Approve?", - "properties": { - "attempt": 1, - "context_updates": { - "human.gate.label": "[A] Approve", - "human.gate.selected": "A" - }, - "context_values": { - "current.preamble": "Goal: Route through the default approval path/n", - "current_node": "approve", - "failure_class": "", - "failure_signature": "", - "graph.goal": "Route through the default approval path", - "internal.fidelity": "compact", - "internal.node_visit_count": 1, - "internal.retry_count.start": 0, - "internal.run_id": "[ULID]", - "internal.thread_id": "start", - "outcome": "success", - "thread.start.current_node": "approve" - }, - "duration_ms": "[DURATION_MS]", - "index": 1, - "max_attempts": 1, - "node_visits": { - "approve": 1, - "start": 1 - }, - "preferred_label": "[A] Approve", - "status": "success", - "suggested_next_ids": [ - "ship" - ] - }, - "run_id": "[ULID]", - "stage_id": "approve@1", - "ts": "[TIMESTAMP]" - }, - { - "event": "edge.selected", - "id": "[EVENT_ID]", - "properties": { - "from_node": "approve", - "is_jump": false, - "label": "[A] Approve", - "preferred_label": "[A] Approve", - "reason": "preferred_label", - "stage_status": "success", - "suggested_next_ids": [ - "ship" - ], - "to_node": "ship" - }, - "run_id": "[ULID]", - "ts": "[TIMESTAMP]" - }, - { - "event": "checkpoint.completed", - "id": "[EVENT_ID]", - "node_id": "approve", - "node_label": "approve", - "properties": { - "completed_nodes": [ - "start", - "approve" - ], - "context_values": { - "current_node": "approve", - "failure_class": "", - "failure_signature": "", - "graph.goal": "Route through the default approval path", - "human.gate.label": "[A] Approve", - "human.gate.selected": "A", - "internal.fidelity": "compact", - "internal.node_visit_count": 1, - "internal.retry_count.approve": 0, - "internal.retry_count.start": 0, - "internal.run_id": "[ULID]", - "internal.thread_id": "start", - "outcome": "success", - "preferred_label": "[A] Approve", - "thread.start.current_node": "approve" - }, - "current_node": "approve", - "next_node_id": "ship", - "node_outcomes": { - "approve": { - "context_updates": { - "human.gate.label": "[A] Approve", - "human.gate.selected": "A" - }, - "preferred_label": "[A] Approve", - "status": "success", - "suggested_next_ids": [ - "ship" - ], - "usage": null - }, - "start": { - "status": "success", - "usage": null - } - }, - "node_visits": { - "approve": 1, - "start": 1 - }, - "status": "success" - }, - "run_id": "[ULID]", - "stage_id": "approve@1", - "ts": "[TIMESTAMP]" - }, - { - "event": "stage.started", - "id": "[EVENT_ID]", - "node_id": "ship", - "node_label": "ship", - "properties": { - "attempt": 1, - "handler_type": "command", - "index": 2, - "max_attempts": 1 - }, - "run_id": "[ULID]", - "stage_id": "ship@1", - "ts": "[TIMESTAMP]" - }, - { - "event": "command.started", - "id": "[EVENT_ID]", - "node_id": "ship", - "node_label": "ship", - "properties": { - "command": "echo shipped", - "language": "shell", - "script": "echo shipped" - }, - "run_id": "[ULID]", - "stage_id": "ship@1", - "ts": "[TIMESTAMP]" - }, - { - "event": "command.completed", - "id": "[EVENT_ID]", - "node_id": "ship", - "node_label": "ship", - "properties": { - "duration_ms": "[DURATION_MS]", - "exit_code": 0, - "stderr": "", - "stdout": "shipped/n", - "timed_out": false - }, - "run_id": "[ULID]", - "stage_id": "ship@1", - "ts": "[TIMESTAMP]" - }, - { - "event": "stage.completed", - "id": "[EVENT_ID]", - "node_id": "ship", - "node_label": "ship", - "properties": { - "attempt": 1, - "context_updates": { - "command.output": "shipped/n", - "command.stderr": "" - }, - "context_values": { - "current.preamble": "Goal: Route through the default approval path/n/n## Completed stages/n- **approve**: success/n/n## Context/n- human.gate.label: [A] Approve/n- human.gate.selected: A/n", - "current_node": "ship", - "failure_class": "", - "failure_signature": "", - "graph.goal": "Route through the default approval path", - "human.gate.label": "[A] Approve", - "human.gate.selected": "A", - "internal.fidelity": "compact", - "internal.node_visit_count": 1, - "internal.retry_count.approve": 0, - "internal.retry_count.start": 0, - "internal.run_id": "[ULID]", - "internal.thread_id": "approve", - "outcome": "success", - "preferred_label": "[A] Approve", - "thread.approve.current_node": "ship", - "thread.start.current_node": "approve" - }, - "duration_ms": "[DURATION_MS]", - "index": 2, - "max_attempts": 1, - "node_visits": { - "approve": 1, - "ship": 1, - "start": 1 - }, - "notes": "Script completed: echo shipped", - "status": "success" - }, - "run_id": "[ULID]", - "stage_id": "ship@1", - "ts": "[TIMESTAMP]" - }, - { - "event": "edge.selected", - "id": "[EVENT_ID]", - "properties": { - "from_node": "ship", - "is_jump": false, - "reason": "unconditional", - "stage_status": "success", - "to_node": "exit" - }, - "run_id": "[ULID]", - "ts": "[TIMESTAMP]" - }, - { - "event": "checkpoint.completed", - "id": "[EVENT_ID]", - "node_id": "ship", - "node_label": "ship", - "properties": { - "completed_nodes": [ - "start", - "approve", - "ship" - ], - "context_values": { - "command.output": "shipped/n", - "command.stderr": "", - "current_node": "ship", - "failure_class": "", - "failure_signature": "", - "graph.goal": "Route through the default approval path", - "human.gate.label": "[A] Approve", - "human.gate.selected": "A", - "internal.fidelity": "compact", - "internal.node_visit_count": 1, - "internal.retry_count.approve": 0, - "internal.retry_count.ship": 0, - "internal.retry_count.start": 0, - "internal.run_id": "[ULID]", - "internal.thread_id": "approve", - "outcome": "success", - "preferred_label": "[A] Approve", - "thread.approve.current_node": "ship", - "thread.start.current_node": "approve" - }, - "current_node": "ship", - "next_node_id": "exit", - "node_outcomes": { - "approve": { - "context_updates": { - "human.gate.label": "[A] Approve", - "human.gate.selected": "A" - }, - "preferred_label": "[A] Approve", - "status": "success", - "suggested_next_ids": [ - "ship" - ], - "usage": null - }, - "ship": { - "context_updates": { - "command.output": "shipped/n", - "command.stderr": "" - }, - "notes": "Script completed: echo shipped", - "status": "success", - "usage": null - }, - "start": { - "status": "success", - "usage": null - } - }, - "node_visits": { - "approve": 1, - "ship": 1, - "start": 1 - }, - "status": "success" - }, - "run_id": "[ULID]", - "stage_id": "ship@1", - "ts": "[TIMESTAMP]" - }, - { - "event": "stage.started", - "id": "[EVENT_ID]", - "node_id": "exit", - "node_label": "Exit", - "properties": { - "attempt": 1, - "handler_type": "exit", - "index": 3, - "max_attempts": 1 - }, - "run_id": "[ULID]", - "stage_id": "exit@1", - "ts": "[TIMESTAMP]" - }, - { - "event": "stage.completed", - "id": "[EVENT_ID]", - "node_id": "exit", - "node_label": "Exit", - "properties": { - "attempt": 1, - "duration_ms": "[DURATION_MS]", - "index": 3, - "max_attempts": 1, - "status": "success" - }, - "run_id": "[ULID]", - "stage_id": "exit@1", - "ts": "[TIMESTAMP]" - }, - { - "event": "run.completed", - "id": "[EVENT_ID]", - "properties": { - "artifact_count": 0, - "duration_ms": "[DURATION_MS]", - "reason": "completed", - "status": "success" - }, - "run_id": "[ULID]", - "ts": "[TIMESTAMP]" - } - ] - "#); - assert_eq!( - progress[0].pointer("/properties/settings/run/execution/approval"), - Some(&serde_json::json!("auto")) + assert!( + progress + .iter() + .any(|event| event.get("event") == Some(&Value::String("interview.started".into()))), + "stdout should include the interview start event:\n{}", + serde_json::to_string_pretty(&progress).unwrap() ); }