diff --git a/.claude/settings.json b/.claude/settings.json index f5ac854fc..00bb06ad6 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -6,7 +6,7 @@ "hooks": [ { "type": "command", - "command": "FILE=$(jq -r '.tool_input.file_path') && case \"$FILE\" in *.rs) cargo fmt -- \"$FILE\" ;; esac" + "command": "FILE=$(jq -r '.tool_input.file_path') && case \"$FILE\" in *.rs) cargo +nightly fmt -- \"$FILE\" ;; esac" } ] } diff --git a/lib/crates/fabro-cli/src/command_context.rs b/lib/crates/fabro-cli/src/command_context.rs index 465984c95..9ddc24d86 100644 --- a/lib/crates/fabro-cli/src/command_context.rs +++ b/lib/crates/fabro-cli/src/command_context.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use anyhow::{Context as _, Result, bail}; use fabro_types::settings::{CliSettings, SettingsLayer}; +use fabro_util::printer::Printer; use tokio::sync::OnceCell; use crate::args::{ServerConnectionArgs, ServerTargetArgs}; @@ -22,6 +23,8 @@ pub(crate) enum ServerMode { } pub(crate) struct CommandContext { + #[allow(dead_code)] + printer: Printer, cwd: PathBuf, base_config_path: PathBuf, machine_settings: SettingsLayer, @@ -31,24 +34,24 @@ pub(crate) struct CommandContext { } impl CommandContext { - pub(crate) fn base() -> Result { - Self::new(ServerMode::None) + pub(crate) fn base(printer: Printer) -> Result { + Self::new(printer, ServerMode::None) } - pub(crate) fn for_target(args: &ServerTargetArgs) -> Result { - Self::new(ServerMode::ByTarget { + pub(crate) fn for_target(args: &ServerTargetArgs, printer: Printer) -> Result { + Self::new(printer, ServerMode::ByTarget { target_override: args.server.clone(), }) } - pub(crate) fn for_connection(args: &ServerConnectionArgs) -> Result { - Self::new(ServerMode::ByStorageDir { + 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(), }) } - fn new(server_mode: ServerMode) -> Result { + fn new(printer: Printer, server_mode: ServerMode) -> 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 { @@ -61,6 +64,7 @@ impl CommandContext { let cli_settings = user_config::resolve_cli_settings(&machine_settings)?; Ok(Self { + printer, cwd, base_config_path, machine_settings, @@ -70,6 +74,11 @@ impl CommandContext { }) } + #[allow(dead_code)] + pub(crate) fn printer(&self) -> Printer { + self.printer + } + pub(crate) fn cwd(&self) -> &Path { &self.cwd } diff --git a/lib/crates/fabro-cli/src/commands/artifact/cp.rs b/lib/crates/fabro-cli/src/commands/artifact/cp.rs index c120f5a26..03c633cea 100644 --- a/lib/crates/fabro-cli/src/commands/artifact/cp.rs +++ b/lib/crates/fabro-cli/src/commands/artifact/cp.rs @@ -1,18 +1,24 @@ use std::path::{Path, PathBuf}; use anyhow::{Context, Result, bail}; +use fabro_util::printer::Printer; use crate::args::{ArtifactCpArgs, GlobalArgs}; use crate::server_client::ServerStoreClient; use crate::shared::{print_json_pretty, split_run_path}; -pub(super) async fn cp_command(args: &ArtifactCpArgs, globals: &GlobalArgs) -> Result<()> { +pub(super) async fn cp_command( + args: &ArtifactCpArgs, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { let (run_id_selector, asset_path) = parse_source(&args.source); let (run_id, client, entries) = super::resolve_artifacts( &args.server, run_id_selector, args.node.as_deref(), args.retry, + printer, ) .await?; @@ -57,7 +63,12 @@ pub(super) async fn cp_command(args: &ArtifactCpArgs, globals: &GlobalArgs) -> R }], }))?; } else { - println!("Copied {} to {}", entry.relative_path, dest_file.display()); + fabro_util::printout!( + printer, + "Copied {} to {}", + entry.relative_path, + dest_file.display() + ); } return Ok(()); } @@ -108,7 +119,8 @@ pub(super) async fn cp_command(args: &ArtifactCpArgs, globals: &GlobalArgs) -> R if globals.json { print_json_pretty(&serde_json::json!({ "copied": copied }))?; } else { - println!( + fabro_util::printout!( + printer, "Copied {} artifact(s) to {}", entries.len(), args.dest.display() diff --git a/lib/crates/fabro-cli/src/commands/artifact/list.rs b/lib/crates/fabro-cli/src/commands/artifact/list.rs index 157bff5e2..24c6d638c 100644 --- a/lib/crates/fabro-cli/src/commands/artifact/list.rs +++ b/lib/crates/fabro-cli/src/commands/artifact/list.rs @@ -1,19 +1,29 @@ use anyhow::Result; +use fabro_util::printer::Printer; use crate::args::{ArtifactListArgs, GlobalArgs}; -pub(super) async fn list_command(args: &ArtifactListArgs, globals: &GlobalArgs) -> Result<()> { - let (_run_id, _client, entries) = - super::resolve_artifacts(&args.server, &args.run_id, args.node.as_deref(), args.retry) - .await?; +pub(super) async fn list_command( + args: &ArtifactListArgs, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { + let (_run_id, _client, entries) = super::resolve_artifacts( + &args.server, + &args.run_id, + args.node.as_deref(), + args.retry, + printer, + ) + .await?; if globals.json { - println!("{}", serde_json::to_string_pretty(&entries)?); + fabro_util::printout!(printer, "{}", serde_json::to_string_pretty(&entries)?); return Ok(()); } if entries.is_empty() { - println!("No artifacts found for this run."); + fabro_util::printout!(printer, "No artifacts found for this run."); return Ok(()); } @@ -30,15 +40,23 @@ pub(super) async fn list_command(args: &ArtifactListArgs, globals: &GlobalArgs) .unwrap_or(5) .max(5); - println!("{:retry_width$} PATH", "NODE", "RETRY"); + fabro_util::printout!( + printer, + "{:retry_width$} PATH", + "NODE", + "RETRY" + ); for entry in &entries { - println!( + fabro_util::printout!( + printer, "{:retry_width$} {}", - entry.node_slug, entry.retry, entry.relative_path + entry.node_slug, + entry.retry, + entry.relative_path ); } - println!(); - println!("{} artifact(s)", entries.len()); + fabro_util::printout!(printer, ""); + fabro_util::printout!(printer, "{} artifact(s)", entries.len()); Ok(()) } diff --git a/lib/crates/fabro-cli/src/commands/artifact/mod.rs b/lib/crates/fabro-cli/src/commands/artifact/mod.rs index f01c8aadf..df9dc4563 100644 --- a/lib/crates/fabro-cli/src/commands/artifact/mod.rs +++ b/lib/crates/fabro-cli/src/commands/artifact/mod.rs @@ -3,6 +3,7 @@ mod list; use anyhow::{Context, Result}; use fabro_types::{RunId, StageId}; +use fabro_util::printer::Printer; use crate::args::{ArtifactCommand, ArtifactNamespace, GlobalArgs, ServerTargetArgs}; use crate::command_context::CommandContext; @@ -24,8 +25,9 @@ pub(super) async fn resolve_artifacts( run_selector: &str, node: Option<&str>, retry: Option, + printer: Printer, ) -> Result<(RunId, ServerStoreClient, Vec)> { - let ctx = CommandContext::for_target(server)?; + let ctx = CommandContext::for_target(server, printer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(run_selector)?; let run_id = run.run_id(); @@ -60,9 +62,13 @@ pub(super) async fn resolve_artifacts( Ok((run_id, client, entries)) } -pub(crate) async fn dispatch(ns: ArtifactNamespace, globals: &GlobalArgs) -> Result<()> { +pub(crate) async fn dispatch( + ns: ArtifactNamespace, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { match ns.command { - ArtifactCommand::List(args) => list::list_command(&args, globals).await, - ArtifactCommand::Cp(args) => cp::cp_command(&args, globals).await, + ArtifactCommand::List(args) => list::list_command(&args, globals, printer).await, + ArtifactCommand::Cp(args) => cp::cp_command(&args, globals, 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 a9aec0b0b..54f36b757 100644 --- a/lib/crates/fabro-cli/src/commands/config/mod.rs +++ b/lib/crates/fabro-cli/src/commands/config/mod.rs @@ -4,6 +4,7 @@ use std::path::Path; use fabro_config::effective_settings::{EffectiveSettingsLayers, EffectiveSettingsMode}; use fabro_config::{effective_settings, load_settings_project, project}; use fabro_types::settings::SettingsLayer; +use fabro_util::printer::Printer; use crate::args::{GlobalArgs, SettingsArgs}; use crate::command_context::CommandContext; @@ -56,8 +57,8 @@ fn workflow_and_project_layers( Ok((workflow_layer, project_layer)) } -async fn merged_config(args: &SettingsArgs) -> anyhow::Result { - let base_ctx = CommandContext::base()?; +async fn merged_config(args: &SettingsArgs, printer: Printer) -> anyhow::Result { + let base_ctx = CommandContext::base(printer)?; let layers = config_layers(&base_ctx, args.workflow.as_deref())?; if args.local { return Ok(effective_settings::resolve_settings( @@ -67,7 +68,7 @@ async fn merged_config(args: &SettingsArgs) -> anyhow::Result { )?); } - let ctx = CommandContext::for_target(&args.target)?; + let ctx = CommandContext::for_target(&args.target, printer)?; let target = user_config::resolve_server_target(&args.target, ctx.machine_settings())?; let server_settings = ctx.server().await?.retrieve_server_settings().await?; let mode = match target { @@ -82,8 +83,12 @@ async fn merged_config(args: &SettingsArgs) -> anyhow::Result { )?) } -pub(crate) async fn execute(args: &SettingsArgs, globals: &GlobalArgs) -> anyhow::Result<()> { - let config = Box::pin(merged_config(args)).await?; +pub(crate) async fn execute( + args: &SettingsArgs, + globals: &GlobalArgs, + printer: Printer, +) -> anyhow::Result<()> { + let config = Box::pin(merged_config(args, printer)).await?; if globals.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 989a94f94..02d3fa387 100644 --- a/lib/crates/fabro-cli/src/commands/doctor.rs +++ b/lib/crates/fabro-cli/src/commands/doctor.rs @@ -12,6 +12,7 @@ use fabro_config::user::{ pub(crate) use fabro_util::check_report::{ CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus, }; +use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use fabro_util::version::FABRO_VERSION; use regex::Regex; @@ -301,18 +302,23 @@ fn render_report_text( report.render(styles, verbose, None, max_width) } -fn render_report(report: &CheckReport, styles: &Styles, verbose: bool) { +fn render_report(report: &CheckReport, styles: &Styles, verbose: bool, printer: Printer) { let term_width = console::Term::stderr().size().1; - print!( - "{}", - render_report_text(report, styles, verbose, Some(term_width)) - ); + { + use std::fmt::Write as _; + let _ = write!( + printer.stdout(), + "{}", + render_report_text(report, styles, verbose, Some(term_width)) + ); + } } pub(crate) async fn run_doctor( args: &DoctorArgs, verbose: bool, globals: &GlobalArgs, + printer: Printer, ) -> Result { let styles = Styles::detect_stdout(); let spinner = if globals.json { @@ -360,7 +366,7 @@ pub(crate) async fn run_doctor( }], }; - let ctx = match CommandContext::for_target(&args.target) { + let ctx = match CommandContext::for_target(&args.target, printer) { Ok(ctx) => ctx, Err(err) => { report.sections.push(CheckSection { @@ -384,7 +390,7 @@ pub(crate) async fn run_doctor( if globals.json { print_json_pretty(&report)?; } else { - render_report(&report, &styles, verbose); + render_report(&report, &styles, verbose, printer); } return Ok(1); } @@ -414,7 +420,7 @@ pub(crate) async fn run_doctor( if globals.json { print_json_pretty(&report)?; } else { - render_report(&report, &styles, verbose); + render_report(&report, &styles, verbose, printer); } return Ok(1); } @@ -443,7 +449,7 @@ pub(crate) async fn run_doctor( if globals.json { print_json_pretty(&report)?; } else { - render_report(&report, &styles, verbose); + render_report(&report, &styles, verbose, printer); } return Ok(1); } @@ -484,7 +490,7 @@ pub(crate) async fn run_doctor( if globals.json { print_json_pretty(&report)?; } else { - render_report(&report, &styles, verbose); + render_report(&report, &styles, verbose, printer); } Ok(i32::from(report.has_errors())) diff --git a/lib/crates/fabro-cli/src/commands/exec.rs b/lib/crates/fabro-cli/src/commands/exec.rs index 0e73b6a62..ca9a9be67 100644 --- a/lib/crates/fabro-cli/src/commands/exec.rs +++ b/lib/crates/fabro-cli/src/commands/exec.rs @@ -9,6 +9,7 @@ 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_util::printer::Printer; use crate::args::{ExecArgs, GlobalArgs}; use crate::user_config; @@ -97,7 +98,11 @@ fn runtime_mcp_server(name: &str, entry: &McpEntryLayer) -> McpServerSettings { } } -pub(crate) async fn execute(mut args: ExecArgs, globals: &GlobalArgs) -> Result<()> { +pub(crate) async fn execute( + mut args: ExecArgs, + globals: &GlobalArgs, + _printer: Printer, +) -> Result<()> { use fabro_agent::cli::PermissionLevel as AgentPermissionLevel; use fabro_types::settings::run::AgentPermissions; diff --git a/lib/crates/fabro-cli/src/commands/graph.rs b/lib/crates/fabro-cli/src/commands/graph.rs index d7bc13a03..d3b42b19c 100644 --- a/lib/crates/fabro-cli/src/commands/graph.rs +++ b/lib/crates/fabro-cli/src/commands/graph.rs @@ -5,6 +5,7 @@ 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_util::printer::Printer; use fabro_util::terminal::Styles; use tracing::debug; @@ -18,12 +19,13 @@ pub(crate) async fn run( args: &GraphArgs, styles: &Styles, globals: &GlobalArgs, + printer: Printer, ) -> anyhow::Result<()> { if globals.json && args.output.is_none() { globals.require_no_json()?; } - let ctx = CommandContext::for_target(&args.target)?; + let ctx = CommandContext::for_target(&args.target, printer)?; let built = build_run_manifest(ManifestBuildInput { workflow: args.workflow.clone(), cwd: ctx.cwd().to_path_buf(), @@ -37,7 +39,7 @@ pub(crate) async fn run( let preflight = client.run_preflight(built.manifest.clone()).await?; let diagnostics = api_diagnostics_to_local(&preflight.workflow.diagnostics); - print_diagnostics(&diagnostics, styles); + print_diagnostics(&diagnostics, styles, printer); if diagnostics .iter() .any(|diagnostic| diagnostic.severity == fabro_validate::Severity::Error) diff --git a/lib/crates/fabro-cli/src/commands/install.rs b/lib/crates/fabro-cli/src/commands/install.rs index b415601dd..cdb8d4213 100644 --- a/lib/crates/fabro-cli/src/commands/install.rs +++ b/lib/crates/fabro-cli/src/commands/install.rs @@ -17,6 +17,7 @@ use fabro_config::user::SETTINGS_CONFIG_FILENAME; use fabro_config::{Storage, legacy_env}; use fabro_model::Provider; use fabro_server::secret_store::SecretStore; +use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use rand::Rng; use tokio::net::TcpListener; @@ -430,6 +431,7 @@ async fn setup_github_app( web_url: &str, owner: &GitHubAppOwner, username: Option<&str>, + printer: Printer, ) -> Result> { let app_name = owner.app_name(username); @@ -521,13 +523,14 @@ async fn setup_github_app( // Open browser let url = format!("http://127.0.0.1:{port}/"); - eprintln!(" {}", s.dim.apply_to("Opening browser...")); + fabro_util::printerr!(printer, " {}", s.dim.apply_to("Opening browser...")); if let Err(e) = open::that(&url) { - eprintln!(" Could not open browser automatically: {e}"); - eprintln!(" Please open this URL manually: {url}"); + fabro_util::printerr!(printer, " Could not open browser automatically: {e}"); + fabro_util::printerr!(printer, " Please open this URL manually: {url}"); } - eprintln!( + fabro_util::printerr!( + printer, " {}", s.dim.apply_to("Waiting for GitHub... (Ctrl+C to cancel)") ); @@ -538,7 +541,11 @@ async fn setup_github_app( .context("did not receive callback from GitHub (was the browser flow completed?)")?; // Exchange code for app credentials - eprintln!(" {}", s.dim.apply_to("Exchanging code with GitHub...")); + fabro_util::printerr!( + printer, + " {}", + s.dim.apply_to("Exchanging code with GitHub...") + ); let client = reqwest::Client::new(); let resp = client .post(format!( @@ -601,12 +608,14 @@ async fn setup_github_app( git_table.insert("slug".into(), toml::Value::String(slug.clone())); git_table.insert("client_id".into(), toml::Value::String(client_id)); std::fs::write(&user_toml_path, toml::to_string_pretty(&doc)?)?; - eprintln!( + fabro_util::printerr!( + printer, " {}", s.dim .apply_to(format!("Wrote {}", user_toml_path.display())) ); - eprintln!( + fabro_util::printerr!( + printer, " {}", s.dim .apply_to(format!("App: https://github.com/apps/{slug}")) @@ -657,7 +666,11 @@ async fn persist_install_secrets( Ok(()) } -pub(crate) async fn run_install(args: &InstallArgs, globals: &GlobalArgs) -> Result<()> { +pub(crate) async fn run_install( + args: &InstallArgs, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { globals.require_no_json()?; let web_url = &args.web_url; let s = Styles::detect_stderr(); @@ -666,16 +679,21 @@ pub(crate) async fn run_install(args: &InstallArgs, globals: &GlobalArgs) -> Res let storage_dir = user_config::storage_dir(&cli_settings)?; let server_was_running = record::active_server_record(&storage_dir).is_some(); - eprintln!(); - eprintln!(" {}{}", emoji, s.bold.apply_to("Fabro Install")); - eprintln!(); - eprintln!( + fabro_util::printerr!(printer, ""); + fabro_util::printerr!(printer, " {}{}", emoji, s.bold.apply_to("Fabro Install")); + fabro_util::printerr!(printer, ""); + fabro_util::printerr!( + printer, " {}", s.dim .apply_to("Let's get Fabro set up. This will configure your") ); - eprintln!(" {}", s.dim.apply_to("LLM providers and GitHub App.")); - eprintln!(); + fabro_util::printerr!( + printer, + " {}", + s.dim.apply_to("LLM providers and GitHub App.") + ); + fabro_util::printerr!(printer, ""); let fabro_dir = fabro_util::Home::from_env().root().to_path_buf(); std::fs::create_dir_all(&fabro_dir)?; @@ -683,17 +701,19 @@ pub(crate) async fn run_install(args: &InstallArgs, globals: &GlobalArgs) -> Res { let env_path = legacy_env::legacy_env_file_path(); if env_path.exists() { - eprintln!( + fabro_util::printerr!( + printer, " Warning: {} is no longer read by fabro server. This install will persist credentials in the server secret store instead.", env_path.display() ); - eprintln!(); + fabro_util::printerr!(printer, ""); } } // Pre-flight checks { - eprintln!( + fabro_util::printerr!( + printer, " {}", s.dim.apply_to("[Pre-flight] System dependency checks") ); @@ -701,9 +721,9 @@ pub(crate) async fn run_install(args: &InstallArgs, globals: &GlobalArgs) -> Res let dep_check = doctor::check_system_deps(doctor::DEP_SPECS, &dep_outcomes); if dep_check.status == doctor::CheckStatus::Error { - eprintln!(" Missing required system dependencies:"); + fabro_util::printerr!(printer, " Missing required system dependencies:"); for detail in &dep_check.details { - eprintln!(" {}", detail.text); + fabro_util::printerr!(printer, " {}", detail.text); } bail!("Install missing required tools before running setup"); } @@ -723,22 +743,22 @@ pub(crate) async fn run_install(args: &InstallArgs, globals: &GlobalArgs) -> Res .status() .context("failed to run brew install graphviz")?; if !status.success() { - eprintln!(" Warning: brew install graphviz failed"); + fabro_util::printerr!(printer, " Warning: brew install graphviz failed"); } } } } for detail in &dep_check.details { - eprintln!(" {}", detail.text); + fabro_util::printerr!(printer, " {}", detail.text); } - eprintln!(); + fabro_util::printerr!(printer, ""); } // Step 1: LLM Providers - eprintln!(" {}", s.bold.apply_to("Step 1 · LLM Providers")); - eprintln!(" {}", s.dim.apply_to("──────────────────────")); - eprintln!(); + fabro_util::printerr!(printer, " {}", s.bold.apply_to("Step 1 · LLM Providers")); + fabro_util::printerr!(printer, " {}", s.dim.apply_to("──────────────────────")); + fabro_util::printerr!(printer, ""); let mut secret_pairs: Vec<(String, String)> = Vec::new(); let mut configured_providers: Vec = Vec::new(); @@ -757,7 +777,7 @@ pub(crate) async fn run_install(args: &InstallArgs, globals: &GlobalArgs) -> Res .await??; if use_oauth { - let pairs = run_openai_oauth_or_api_key(&s).await?; + let pairs = run_openai_oauth_or_api_key(&s, printer).await?; secret_pairs.extend(pairs); configured_providers.push(Provider::OpenAi); openai_via_oauth = true; @@ -780,14 +800,14 @@ pub(crate) async fn run_install(args: &InstallArgs, globals: &GlobalArgs) -> Res let first_provider = primary_providers[primary_idx]; { - let (env_var, key) = prompt_and_validate_key(first_provider, &s).await?; + let (env_var, key) = prompt_and_validate_key(first_provider, &s, printer).await?; secret_pairs.push((env_var, key)); configured_providers.push(first_provider); } } // Additional providers - eprintln!(); + fabro_util::printerr!(printer, ""); let add_more = spawn_blocking(|| prompt_confirm("Set up additional LLM providers?", false)).await??; @@ -814,16 +834,16 @@ pub(crate) async fn run_install(args: &InstallArgs, globals: &GlobalArgs) -> Res for idx in selected_indices { let provider = remaining_providers[idx]; - let (env_var, key) = prompt_and_validate_key(provider, &s).await?; + let (env_var, key) = prompt_and_validate_key(provider, &s, printer).await?; secret_pairs.push((env_var, key)); } } - eprintln!(); + fabro_util::printerr!(printer, ""); // Step 2: GitHub App - eprintln!(" {}", s.bold.apply_to("Step 2 · GitHub App")); - eprintln!(" {}", s.dim.apply_to("───────────────────")); - eprintln!(); + fabro_util::printerr!(printer, " {}", s.bold.apply_to("Step 2 · GitHub App")); + fabro_util::printerr!(printer, " {}", s.dim.apply_to("───────────────────")); + fabro_util::printerr!(printer, ""); { let setup_github = @@ -831,8 +851,15 @@ pub(crate) async fn run_install(args: &InstallArgs, globals: &GlobalArgs) -> Res if setup_github { let (owner, username) = prompt_github_app_owner(&s).await?; - let github_env_pairs = - setup_github_app(&fabro_dir, &s, web_url, &owner, username.as_deref()).await?; + let github_env_pairs = setup_github_app( + &fabro_dir, + &s, + web_url, + &owner, + username.as_deref(), + printer, + ) + .await?; let slug = { let user_toml_path = fabro_dir.join(SETTINGS_CONFIG_FILENAME); let toml_content = std::fs::read_to_string(&user_toml_path).unwrap_or_default(); @@ -844,23 +871,24 @@ pub(crate) async fn run_install(args: &InstallArgs, globals: &GlobalArgs) -> Res .unwrap_or("unknown") .to_string() }; - eprintln!( + fabro_util::printerr!( + printer, " {} GitHub App registered ({})", s.green.apply_to("✔"), slug ); secret_pairs.extend(github_env_pairs); } else { - eprintln!(" Skipped"); + fabro_util::printerr!(printer, " Skipped"); } } - eprintln!(); + fabro_util::printerr!(printer, ""); // Server configuration { - eprintln!(" {}", s.bold.apply_to("Server · Configuration")); - eprintln!(" {}", s.dim.apply_to("─────────────────────")); - eprintln!(); + fabro_util::printerr!(printer, " {}", s.bold.apply_to("Server · Configuration")); + fabro_util::printerr!(printer, " {}", s.dim.apply_to("─────────────────────")); + fabro_util::printerr!(printer, ""); let config_path = fabro_dir.join(SETTINGS_CONFIG_FILENAME); let write_config = if config_path.exists() { @@ -884,32 +912,47 @@ pub(crate) async fn run_install(args: &InstallArgs, globals: &GlobalArgs) -> Res }; merge_server_settings(&mut doc, &username)?; std::fs::write(&config_path, toml::to_string_pretty(&doc)?)?; - eprintln!( + fabro_util::printerr!( + printer, " {}", s.dim.apply_to(format!("Wrote {}", config_path.display())) ); } else { - eprintln!(" {}", s.dim.apply_to("Keeping existing settings.toml")); + fabro_util::printerr!( + printer, + " {}", + s.dim.apply_to("Keeping existing settings.toml") + ); } - eprintln!(); + fabro_util::printerr!(printer, ""); } // Secrets and certificates { - eprintln!( + fabro_util::printerr!( + printer, " {}", s.dim.apply_to("Generating secrets and certificates...") ); let session_secret = generate_session_secret(); - eprintln!(" {} Session secret generated", s.green.apply_to("✔")); + fabro_util::printerr!( + printer, + " {} Session secret generated", + s.green.apply_to("✔") + ); let (jwt_private_pem, jwt_public_pem) = generate_jwt_keypair()?; - eprintln!(" {} Ed25519 JWT keypair generated", s.green.apply_to("✔")); + fabro_util::printerr!( + printer, + " {} Ed25519 JWT keypair generated", + s.green.apply_to("✔") + ); let certs_dir = fabro_dir.join("certs"); generate_mtls_certs(&certs_dir)?; - eprintln!( + fabro_util::printerr!( + printer, " {} mTLS CA + server certificates generated", s.green.apply_to("✔") ); @@ -923,43 +966,46 @@ pub(crate) async fn run_install(args: &InstallArgs, globals: &GlobalArgs) -> Res ("SESSION_SECRET".to_string(), session_secret), ]; secret_pairs.extend(server_env_pairs); - eprintln!(); + fabro_util::printerr!(printer, ""); - eprintln!(" To start Fabro, run these commands:"); - eprintln!(); - eprintln!(" fabro server start"); - eprintln!(); + fabro_util::printerr!(printer, " To start Fabro, run these commands:"); + fabro_util::printerr!(printer, ""); + fabro_util::printerr!(printer, " fabro server start"); + fabro_util::printerr!(printer, ""); } persist_install_secrets(&storage_dir, &secret_pairs, server_was_running).await?; - eprintln!( + fabro_util::printerr!( + printer, " {} Saved {} secrets to {}", s.green.apply_to("✔"), secret_pairs.len(), Storage::new(&storage_dir).secrets_path().display() ); if server_was_running { - eprintln!( + fabro_util::printerr!( + printer, " Warning: the local fabro server was already running. Restart it to pick up startup-time features that only initialize at boot." ); } - eprintln!(); + fabro_util::printerr!(printer, ""); // Verify setup let run_doctor = spawn_blocking(|| prompt_confirm("Run fabro doctor to verify?", true)).await??; if run_doctor { - eprintln!(); + fabro_util::printerr!(printer, ""); let doctor_args = DoctorArgs { target: ServerTargetArgs::default(), verbose: true, }; - let _ = doctor::run_doctor(&doctor_args, true, globals).await?; + let _ = doctor::run_doctor(&doctor_args, true, globals, printer).await?; } - eprintln!(); - eprintln!( + fabro_util::printerr!(printer, ""); + fabro_util::printerr!( + printer, " Setup complete! Go to your project and run {} to get started.", s.bold_cyan.apply_to("fabro repo init") ); diff --git a/lib/crates/fabro-cli/src/commands/model.rs b/lib/crates/fabro-cli/src/commands/model.rs index 17816c297..2b35c67f9 100644 --- a/lib/crates/fabro-cli/src/commands/model.rs +++ b/lib/crates/fabro-cli/src/commands/model.rs @@ -3,6 +3,7 @@ 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_util::printer::Printer; use fabro_util::terminal::Styles; use serde::Serialize; use serde::de::DeserializeOwned; @@ -37,13 +38,17 @@ struct ModelTestOutput { failures: u32, } -pub(crate) async fn execute(command: Option, globals: &GlobalArgs) -> Result<()> { +pub(crate) async fn execute( + command: Option, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { let command = command.unwrap_or_default(); let target_args = match &command { ModelsCommand::List(args) => &args.target, ModelsCommand::Test(args) => &args.target, }; - let ctx = CommandContext::for_target(target_args)?; + let ctx = CommandContext::for_target(target_args, printer)?; let server = ctx.server().await?; run_models(command, server.api(), globals.json).await diff --git a/lib/crates/fabro-cli/src/commands/parse.rs b/lib/crates/fabro-cli/src/commands/parse.rs index dfb3f44bb..ce6722e20 100644 --- a/lib/crates/fabro-cli/src/commands/parse.rs +++ b/lib/crates/fabro-cli/src/commands/parse.rs @@ -2,11 +2,12 @@ use std::io::Write; use fabro_config::project::resolve_workflow; use fabro_graphviz::parser::parse_ast; +use fabro_util::printer::Printer; use crate::args::{GlobalArgs, ParseArgs}; use crate::shared::read_workflow_file; -pub(crate) fn run(args: &ParseArgs, globals: &GlobalArgs) -> anyhow::Result<()> { +pub(crate) fn run(args: &ParseArgs, globals: &GlobalArgs, _printer: Printer) -> anyhow::Result<()> { let _ = globals; 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 f24897245..1ceeb30da 100644 --- a/lib/crates/fabro-cli/src/commands/pr/close.rs +++ b/lib/crates/fabro-cli/src/commands/pr/close.rs @@ -1,4 +1,5 @@ use anyhow::{Context, Result}; +use fabro_util::printer::Printer; use tracing::info; use crate::args::{GlobalArgs, PrCloseArgs}; @@ -8,8 +9,9 @@ pub(super) async fn close_command( args: PrCloseArgs, github_app: Option, globals: &GlobalArgs, + printer: Printer, ) -> Result<()> { - let (record, _run_id) = super::load_pr_record(&args.server, &args.run_id).await?; + let (record, _run_id) = super::load_pr_record(&args.server, &args.run_id, printer).await?; let creds = github_app.context( "GitHub App credentials required — set GITHUB_APP_PRIVATE_KEY and configure app_id", @@ -32,7 +34,7 @@ pub(super) async fn close_command( "html_url": record.html_url, }))?; } else { - println!("Closed #{} ({})", record.number, record.html_url); + fabro_util::printout!(printer, "Closed #{} ({})", record.number, record.html_url); } Ok(()) diff --git a/lib/crates/fabro-cli/src/commands/pr/create.rs b/lib/crates/fabro-cli/src/commands/pr/create.rs index 03cbb2db8..415b18abc 100644 --- a/lib/crates/fabro-cli/src/commands/pr/create.rs +++ b/lib/crates/fabro-cli/src/commands/pr/create.rs @@ -1,6 +1,7 @@ use anyhow::{Context, Result, bail}; use fabro_model::Catalog; use fabro_sandbox::daytona::detect_repo_info; +use fabro_util::printer::Printer; use fabro_workflow::outcome::StageStatus; use fabro_workflow::pull_request::maybe_open_pull_request; use tracing::info; @@ -16,8 +17,9 @@ pub(super) async fn create_command( args: PrCreateArgs, github_app: Option, globals: &GlobalArgs, + printer: Printer, ) -> Result<()> { - let ctx = CommandContext::for_target(&args.server)?; + let ctx = CommandContext::for_target(&args.server, printer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(&args.run_id)?; let run_id = run.run_id(); @@ -117,14 +119,14 @@ pub(super) async fn create_command( if globals.json { print_json_pretty(&record)?; } else { - println!("{}", record.html_url); + fabro_util::printout!(printer, "{}", record.html_url); } } None => { if globals.json { print_json_pretty(&serde_json::Value::Null)?; } else { - println!("No pull request created (empty diff)."); + 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 3d885807a..a25165d66 100644 --- a/lib/crates/fabro-cli/src/commands/pr/list.rs +++ b/lib/crates/fabro-cli/src/commands/pr/list.rs @@ -1,4 +1,5 @@ use anyhow::{Context, Result}; +use fabro_util::printer::Printer; use futures::future::join_all; use serde::Serialize; use tracing::info; @@ -21,11 +22,12 @@ pub(super) async fn list_command( args: PrListArgs, github_app: Option, globals: &GlobalArgs, + printer: Printer, ) -> Result<()> { let creds = github_app.context( "GitHub App credentials required — set GITHUB_APP_PRIVATE_KEY and configure app_id", )?; - let ctx = CommandContext::for_target(&args.server)?; + let ctx = CommandContext::for_target(&args.server, printer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let mut entries = Vec::new(); @@ -42,7 +44,7 @@ pub(super) async fn list_command( print_json_pretty(&Vec::::new())?; return Ok(()); } - println!("No pull requests found."); + fabro_util::printout!(printer, "No pull requests found."); return Ok(()); } @@ -104,13 +106,20 @@ pub(super) async fn list_command( } if rows.is_empty() { - println!("No open pull requests found. Use --all to include closed/merged."); + fabro_util::printout!( + printer, + "No open pull requests found. Use --all to include closed/merged." + ); return Ok(()); } - println!( + fabro_util::printout!( + printer, "{:<12} {:<6} {:<8} {:<50} URL", - "RUN", "#", "STATE", "TITLE" + "RUN", + "#", + "STATE", + "TITLE" ); for row in &rows { let short_id = if row.run_id.len() > 12 { @@ -123,9 +132,14 @@ pub(super) async fn list_command( } else { row.title.clone() }; - println!( + fabro_util::printout!( + printer, "{:<12} {:<6} {:<8} {:<50} {}", - short_id, row.number, row.state, short_title, row.url + short_id, + row.number, + row.state, + short_title, + row.url ); } diff --git a/lib/crates/fabro-cli/src/commands/pr/merge.rs b/lib/crates/fabro-cli/src/commands/pr/merge.rs index 7117daf3a..4f4e46fe3 100644 --- a/lib/crates/fabro-cli/src/commands/pr/merge.rs +++ b/lib/crates/fabro-cli/src/commands/pr/merge.rs @@ -1,4 +1,5 @@ use anyhow::{Context, Result}; +use fabro_util::printer::Printer; use tracing::info; use crate::args::{GlobalArgs, PrMergeArgs}; @@ -8,8 +9,9 @@ pub(super) async fn merge_command( args: PrMergeArgs, github_app: Option, globals: &GlobalArgs, + printer: Printer, ) -> Result<()> { - let (record, _run_id) = super::load_pr_record(&args.server, &args.run_id).await?; + let (record, _run_id) = super::load_pr_record(&args.server, &args.run_id, printer).await?; let creds = github_app.context( "GitHub App credentials required — set GITHUB_APP_PRIVATE_KEY and configure app_id", @@ -34,7 +36,7 @@ pub(super) async fn merge_command( "method": args.method, }))?; } else { - println!("Merged #{} ({})", record.number, record.html_url); + fabro_util::printout!(printer, "Merged #{} ({})", record.number, record.html_url); } Ok(()) diff --git a/lib/crates/fabro-cli/src/commands/pr/mod.rs b/lib/crates/fabro-cli/src/commands/pr/mod.rs index 2e53783d6..639e17c3f 100644 --- a/lib/crates/fabro-cli/src/commands/pr/mod.rs +++ b/lib/crates/fabro-cli/src/commands/pr/mod.rs @@ -7,14 +7,19 @@ mod view; use anyhow::{Context, Result}; use fabro_types::PullRequestRecord; use fabro_types::settings::InterpString; +use fabro_util::printer::Printer; use crate::args::{GlobalArgs, PrCommand, PrNamespace, ServerTargetArgs}; use crate::command_context::CommandContext; use crate::server_runs::ServerSummaryLookup; use crate::shared::github::build_github_app_credentials; -pub(crate) async fn dispatch(ns: PrNamespace, globals: &GlobalArgs) -> Result<()> { - let ctx = CommandContext::base()?; +pub(crate) async fn dispatch( + ns: PrNamespace, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { + let ctx = CommandContext::base(printer)?; let server_settings = fabro_config::resolve_server_from_file(ctx.machine_settings()).map_err(|errors| { anyhow::anyhow!( @@ -37,20 +42,21 @@ pub(crate) async fn dispatch(ns: PrNamespace, globals: &GlobalArgs) -> Result<() )?; match ns.command { PrCommand::Create(args) => { - Box::pin(create::create_command(args, github_app, globals)).await + Box::pin(create::create_command(args, github_app, globals, printer)).await } - PrCommand::List(args) => list::list_command(args, github_app, globals).await, - PrCommand::View(args) => view::view_command(args, github_app, globals).await, - PrCommand::Merge(args) => merge::merge_command(args, github_app, globals).await, - PrCommand::Close(args) => close::close_command(args, github_app, globals).await, + PrCommand::List(args) => list::list_command(args, github_app, globals, printer).await, + PrCommand::View(args) => view::view_command(args, github_app, globals, printer).await, + PrCommand::Merge(args) => merge::merge_command(args, github_app, globals, printer).await, + PrCommand::Close(args) => close::close_command(args, github_app, globals, printer).await, } } pub(crate) async fn load_pr_record( server: &ServerTargetArgs, run_id: &str, + printer: Printer, ) -> Result<(PullRequestRecord, fabro_types::RunId)> { - let ctx = CommandContext::for_target(server)?; + let ctx = CommandContext::for_target(server, printer)?; 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 11a719dfa..735a602db 100644 --- a/lib/crates/fabro-cli/src/commands/pr/view.rs +++ b/lib/crates/fabro-cli/src/commands/pr/view.rs @@ -1,4 +1,5 @@ use anyhow::{Context, Result}; +use fabro_util::printer::Printer; use tracing::info; use crate::args::{GlobalArgs, PrViewArgs}; @@ -8,8 +9,9 @@ pub(super) async fn view_command( args: PrViewArgs, github_app: Option, globals: &GlobalArgs, + printer: Printer, ) -> Result<()> { - let (record, _run_id) = super::load_pr_record(&args.server, &args.run_id).await?; + let (record, _run_id) = super::load_pr_record(&args.server, &args.run_id, printer).await?; let creds = github_app.context( "GitHub App credentials required — set GITHUB_APP_PRIVATE_KEY and configure app_id", @@ -32,23 +34,28 @@ pub(super) async fn view_command( return Ok(()); } - println!("#{} {}", detail.number, detail.title); + fabro_util::printout!(printer, "#{} {}", detail.number, detail.title); let state_display = if detail.draft { "draft" } else { &detail.state }; - println!("State: {state_display}"); - println!("URL: {}", detail.html_url); - println!( + fabro_util::printout!(printer, "State: {state_display}"); + fabro_util::printout!(printer, "URL: {}", detail.html_url); + fabro_util::printout!( + printer, "Branch: {} -> {}", - detail.head.ref_name, detail.base.ref_name + detail.head.ref_name, + detail.base.ref_name ); - println!("Author: {}", detail.user.login); - println!( + fabro_util::printout!(printer, "Author: {}", detail.user.login); + fabro_util::printout!( + printer, "Changes: +{} -{} ({} files)", - detail.additions, detail.deletions, detail.changed_files + detail.additions, + detail.deletions, + detail.changed_files ); if let Some(body) = &detail.body { if !body.is_empty() { - println!(); - println!("{body}"); + fabro_util::printout!(printer, ""); + fabro_util::printout!(printer, "{body}"); } } diff --git a/lib/crates/fabro-cli/src/commands/preflight.rs b/lib/crates/fabro-cli/src/commands/preflight.rs index 82e80c82a..a6689468e 100644 --- a/lib/crates/fabro-cli/src/commands/preflight.rs +++ b/lib/crates/fabro-cli/src/commands/preflight.rs @@ -2,6 +2,7 @@ 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_util::printer::Printer; use fabro_util::terminal::Styles; use crate::args::{GlobalArgs, PreflightArgs}; @@ -13,9 +14,13 @@ use crate::commands::run::overrides::preflight_args_layer; use crate::manifest_builder::{ManifestBuildInput, build_run_manifest, preflight_manifest_args}; use crate::shared::print_json_pretty; -pub(crate) async fn execute(mut args: PreflightArgs, globals: &GlobalArgs) -> anyhow::Result<()> { +pub(crate) async fn execute( + mut args: PreflightArgs, + globals: &GlobalArgs, + printer: Printer, +) -> anyhow::Result<()> { let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); - let ctx = CommandContext::for_target(&args.target)?; + let ctx = CommandContext::for_target(&args.target, printer)?; args.verbose = args.verbose || ctx.cli_settings().output.verbosity == OutputVerbosity::Verbose; let manifest = build_run_manifest(ManifestBuildInput { @@ -34,7 +39,12 @@ pub(crate) async fn execute(mut args: PreflightArgs, globals: &GlobalArgs) -> an if globals.json { print_json_pretty(&response)?; } else { - print_preflight_workflow_summary(&response.workflow, Some(&manifest.target_path), styles); + print_preflight_workflow_summary( + &response.workflow, + Some(&manifest.target_path), + styles, + printer, + ); if diagnostics .iter() .any(|diagnostic| diagnostic.severity == fabro_validate::Severity::Error) @@ -43,7 +53,14 @@ pub(crate) async fn execute(mut args: PreflightArgs, globals: &GlobalArgs) -> an } let report = api_check_report_to_local(&response.checks); let term_width = console::Term::stderr().size().1; - print!("{}", report.render(styles, true, None, Some(term_width))); + { + use std::fmt::Write as _; + let _ = write!( + printer.stdout(), + "{}", + report.render(styles, true, None, Some(term_width)) + ); + } } if diagnostics diff --git a/lib/crates/fabro-cli/src/commands/provider/login.rs b/lib/crates/fabro-cli/src/commands/provider/login.rs index 33a6537e0..f0ca944ae 100644 --- a/lib/crates/fabro-cli/src/commands/provider/login.rs +++ b/lib/crates/fabro-cli/src/commands/provider/login.rs @@ -2,6 +2,7 @@ use anyhow::Result; use fabro_api::types; use fabro_config::legacy_env; use fabro_model::Provider; +use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use tokio::task::spawn_blocking; @@ -9,10 +10,14 @@ use crate::args::{GlobalArgs, ProviderLoginArgs}; use crate::command_context::CommandContext; use crate::shared::provider_auth; -pub(super) async fn login_command(args: ProviderLoginArgs, globals: &GlobalArgs) -> Result<()> { +pub(super) async fn login_command( + args: ProviderLoginArgs, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { globals.require_no_json()?; let s = Styles::detect_stderr(); - let ctx = CommandContext::for_target(&args.target)?; + let ctx = CommandContext::for_target(&args.target, printer)?; let server = ctx.server().await?; let use_oauth = args.provider == Provider::OpenAi @@ -20,16 +25,18 @@ pub(super) async fn login_command(args: ProviderLoginArgs, globals: &GlobalArgs) .await??; let env_pairs = if use_oauth { - provider_auth::run_openai_oauth_or_api_key(&s).await? + provider_auth::run_openai_oauth_or_api_key(&s, printer).await? } else { - let (env_var, key) = provider_auth::prompt_and_validate_key(args.provider, &s).await?; + let (env_var, key) = + provider_auth::prompt_and_validate_key(args.provider, &s, printer).await?; vec![(env_var, key)] }; { let path = legacy_env::legacy_env_file_path(); if path.exists() { - eprintln!( + fabro_util::printerr!( + printer, " Warning: {} is no longer read by fabro server. Re-enter credentials with `fabro provider login` or `fabro secret set`.", path.display() ); @@ -44,7 +51,7 @@ pub(super) async fn login_command(args: ProviderLoginArgs, globals: &GlobalArgs) .body(types::SetSecretRequest { value }) .send() .await?; - eprintln!(" {} Saved {}", s.green.apply_to("✔"), name); + fabro_util::printerr!(printer, " {} Saved {}", s.green.apply_to("✔"), name); } Ok(()) } diff --git a/lib/crates/fabro-cli/src/commands/provider/mod.rs b/lib/crates/fabro-cli/src/commands/provider/mod.rs index c1fcb9431..79915a73a 100644 --- a/lib/crates/fabro-cli/src/commands/provider/mod.rs +++ b/lib/crates/fabro-cli/src/commands/provider/mod.rs @@ -1,11 +1,16 @@ mod login; use anyhow::Result; +use fabro_util::printer::Printer; use crate::args::{GlobalArgs, ProviderCommand, ProviderNamespace}; -pub(crate) async fn dispatch(ns: ProviderNamespace, globals: &GlobalArgs) -> Result<()> { +pub(crate) async fn dispatch( + ns: ProviderNamespace, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { match ns.command { - ProviderCommand::Login(args) => login::login_command(args, globals).await, + ProviderCommand::Login(args) => login::login_command(args, globals, 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 8155302c1..926f91205 100644 --- a/lib/crates/fabro-cli/src/commands/repo/deinit.rs +++ b/lib/crates/fabro-cli/src/commands/repo/deinit.rs @@ -1,8 +1,9 @@ use anyhow::{Context, Result, bail}; +use fabro_util::printer::Printer; use crate::args::GlobalArgs; -pub(crate) fn run_deinit(globals: &GlobalArgs) -> Result> { +pub(crate) fn run_deinit(globals: &GlobalArgs, printer: Printer) -> Result> { let repo_root = super::init::git_repo_root()?; let mut removed = Vec::new(); @@ -20,7 +21,8 @@ pub(crate) fn run_deinit(globals: &GlobalArgs) -> Result> { .with_context(|| format!("failed to remove {}", fabro_dir.display()))?; removed.push(".fabro/".to_string()); if !globals.json { - eprintln!( + fabro_util::printerr!( + printer, " {} {}", green.apply_to("✔"), dim.apply_to("removed .fabro/") @@ -28,7 +30,8 @@ pub(crate) fn run_deinit(globals: &GlobalArgs) -> Result> { } if !globals.json { - eprintln!( + fabro_util::printerr!( + printer, "\n{}", console::Style::new() .bold() diff --git a/lib/crates/fabro-cli/src/commands/repo/init.rs b/lib/crates/fabro-cli/src/commands/repo/init.rs index 97c319524..4e908350d 100644 --- a/lib/crates/fabro-cli/src/commands/repo/init.rs +++ b/lib/crates/fabro-cli/src/commands/repo/init.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use anyhow::{Context, Result, bail}; +use fabro_util::printer::Printer; use tokio::task::spawn_blocking; use crate::args::{GlobalArgs, RepoInitArgs, ServerTargetArgs}; @@ -21,7 +22,11 @@ pub(super) fn git_repo_root() -> Result { )) } -pub(crate) async fn run_init(args: &RepoInitArgs, globals: &GlobalArgs) -> Result> { +pub(crate) async fn run_init( + args: &RepoInitArgs, + globals: &GlobalArgs, + printer: Printer, +) -> Result> { let repo_root = git_repo_root()?; let mut created = Vec::new(); @@ -60,7 +65,8 @@ draft = true let bold = console::Style::new().bold(); let dim = console::Style::new().dim(); if !globals.json { - eprintln!( + fabro_util::printerr!( + printer, " {} {}", green.apply_to("✔"), dim.apply_to(".fabro/project.toml") @@ -92,7 +98,8 @@ draft = true .with_context(|| format!("failed to write {}", dot_path.display()))?; created.push(".fabro/workflows/hello/workflow.fabro".to_string()); if !globals.json { - eprintln!( + fabro_util::printerr!( + printer, " {} {}", green.apply_to("✔"), dim.apply_to(".fabro/workflows/hello/workflow.fabro") @@ -108,7 +115,8 @@ draft = true .with_context(|| format!("failed to write {}", toml_path.display()))?; created.push(".fabro/workflows/hello/workflow.toml".to_string()); if !globals.json { - eprintln!( + fabro_util::printerr!( + printer, " {} {}", green.apply_to("✔"), dim.apply_to(".fabro/workflows/hello/workflow.toml") @@ -116,7 +124,8 @@ draft = true } if !globals.json { - eprintln!( + fabro_util::printerr!( + printer, "\n{} Run a workflow with:\n\n {}", bold.apply_to("Project initialized!"), console::Style::new() @@ -127,13 +136,13 @@ draft = true } if !globals.json { - check_github_app_installation(&args.target).await; + check_github_app_installation(&args.target, printer).await; } Ok(created) } -async fn check_github_app_installation(target: &ServerTargetArgs) { +async fn check_github_app_installation(target: &ServerTargetArgs, printer: Printer) { // Get the git remote origin URL let output = match std::process::Command::new("git") .args(["remote", "get-url", "origin"]) @@ -143,11 +152,13 @@ async fn check_github_app_installation(target: &ServerTargetArgs) { _ => { let yellow = console::Style::new().yellow(); let dim = console::Style::new().dim(); - eprintln!( + fabro_util::printerr!( + printer, "\n {} No git remote found — skipping GitHub App check", yellow.apply_to("!") ); - eprintln!( + fabro_util::printerr!( + printer, " {}", dim.apply_to( "Run `git remote add origin ` then `fabro install` to set up the GitHub App" @@ -168,10 +179,13 @@ async fn check_github_app_installation(target: &ServerTargetArgs) { return; // Not a GitHub repo — skip silently }; - let ctx = match CommandContext::for_target(target) { + let ctx = match CommandContext::for_target(target, printer) { Ok(ctx) => ctx, Err(err) => { - eprintln!("\n Warning: could not resolve fabro server settings: {err}"); + fabro_util::printerr!( + printer, + "\n Warning: could not resolve fabro server settings: {err}" + ); return; } }; @@ -179,7 +193,10 @@ async fn check_github_app_installation(target: &ServerTargetArgs) { let server = match ctx.server().await { Ok(server) => server, Err(err) => { - eprintln!("\n Warning: could not connect to fabro server: {err}"); + fabro_util::printerr!( + printer, + "\n Warning: could not connect to fabro server: {err}" + ); return; } }; @@ -194,14 +211,18 @@ async fn check_github_app_installation(target: &ServerTargetArgs) { { Ok(response) => response.into_inner(), Err(err) => { - eprintln!("\n Warning: could not check GitHub App installation: {err}"); + fabro_util::printerr!( + printer, + "\n Warning: could not check GitHub App installation: {err}" + ); return; } }; if check.accessible { let green = console::Style::new().green(); - eprintln!( + fabro_util::printerr!( + printer, "\n {} GitHub App is installed for {owner}/{repo}", green.apply_to("✔") ); @@ -209,16 +230,17 @@ async fn check_github_app_installation(target: &ServerTargetArgs) { } let yellow = console::Style::new().yellow(); - eprintln!( + fabro_util::printerr!( + printer, "\n {} GitHub App is not installed for {owner}/{repo}", yellow.apply_to("!") ); if let Some(url) = &check.install_url { - eprintln!(" Install at: {url}"); + fabro_util::printerr!(printer, " Install at: {url}"); } if std::io::IsTerminal::is_terminal(&std::io::stdin()) { - eprintln!(" Press Enter to continue after installing..."); + fabro_util::printerr!(printer, " Press Enter to continue after installing..."); let _ = spawn_blocking(|| { let mut buf = String::new(); let _ = std::io::stdin().read_line(&mut buf); @@ -237,19 +259,23 @@ async fn check_github_app_installation(target: &ServerTargetArgs) { let response = response.into_inner(); if response.accessible { let green = console::Style::new().green(); - eprintln!( + fabro_util::printerr!( + printer, " {} GitHub App is installed for {owner}/{repo}", green.apply_to("✔") ); } else { - eprintln!(" GitHub App is still not installed."); + fabro_util::printerr!(printer, " GitHub App is still not installed."); if let Some(url) = &check.install_url { - eprintln!(" Install at: {url}"); + fabro_util::printerr!(printer, " Install at: {url}"); } } } Err(err) => { - eprintln!(" Warning: could not re-check GitHub App installation: {err}"); + fabro_util::printerr!( + printer, + " Warning: could not re-check GitHub App installation: {err}" + ); } } } diff --git a/lib/crates/fabro-cli/src/commands/repo/mod.rs b/lib/crates/fabro-cli/src/commands/repo/mod.rs index 393a07b0d..29d1be6b1 100644 --- a/lib/crates/fabro-cli/src/commands/repo/mod.rs +++ b/lib/crates/fabro-cli/src/commands/repo/mod.rs @@ -2,21 +2,26 @@ pub(crate) mod deinit; pub(crate) mod init; use anyhow::Result; +use fabro_util::printer::Printer; use crate::args::{GlobalArgs, RepoCommand, RepoNamespace}; use crate::shared::print_json_pretty; -pub(crate) async fn dispatch(ns: RepoNamespace, globals: &GlobalArgs) -> Result<()> { +pub(crate) async fn dispatch( + ns: RepoNamespace, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { match ns.command { RepoCommand::Init(args) => { - let created = init::run_init(&args, globals).await?; + let created = init::run_init(&args, globals, printer).await?; if globals.json { print_json_pretty(&serde_json::json!({ "created": created }))?; } Ok(()) } RepoCommand::Deinit => { - let removed = deinit::run_deinit(globals)?; + let removed = deinit::run_deinit(globals, printer)?; if globals.json { print_json_pretty(&serde_json::json!({ "removed": removed }))?; } diff --git a/lib/crates/fabro-cli/src/commands/run/attach.rs b/lib/crates/fabro-cli/src/commands/run/attach.rs index 372b80600..31e600ea6 100644 --- a/lib/crates/fabro-cli/src/commands/run/attach.rs +++ b/lib/crates/fabro-cli/src/commands/run/attach.rs @@ -47,7 +47,15 @@ pub(crate) async fn attach_run( if let (Some(storage_dir), Some(run_id)) = (storage_dir.as_deref(), run_id.as_ref()) { let client = server_client::connect_server(storage_dir).await?; - return attach_run_with_client(&client, run_id, kill_on_detach, styles, json_output).await; + return attach_run_with_client( + &client, + run_id, + kill_on_detach, + styles, + json_output, + fabro_util::printer::Printer::Default, + ) + .await; } Err(anyhow::anyhow!( @@ -61,6 +69,7 @@ pub(crate) async fn attach_run_with_client( kill_on_detach: bool, styles: &'static Styles, json_output: bool, + printer: fabro_util::printer::Printer, ) -> Result { let state = client.get_run_state(run_id).await?; let auto_approve = state.run.as_ref().is_some_and(|record| { @@ -103,6 +112,7 @@ pub(crate) async fn attach_run_with_client( kill_on_detach, json_output, }, + printer, ) .await } @@ -140,6 +150,7 @@ async fn attach_live_run_with_client( mut stream: server_client::RunAttachEventStream, styles: &'static Styles, opts: AttachOptions, + printer: fabro_util::printer::Printer, ) -> Result { let is_tty = std::io::stderr().is_terminal(); let mut progress_ui = run_progress::ProgressUI::new(is_tty, opts.verbose); @@ -158,6 +169,7 @@ async fn attach_live_run_with_client( &mut progress_ui, styles, opts.json_output, + printer, ) .await? { @@ -167,7 +179,7 @@ async fn attach_live_run_with_client( loop { let next_event = tokio::select! { _ = &mut ctrl_c_signal => { - handle_detach_signal(client, run_id, opts.kill_on_detach).await; + handle_detach_signal(client, run_id, opts.kill_on_detach, printer).await; finish_progress(&mut progress_ui, opts.json_output); return Ok(ExitCode::from(1)); } @@ -195,6 +207,7 @@ async fn attach_live_run_with_client( &mut progress_ui, styles, opts.json_output, + printer, ) .await? { @@ -211,13 +224,14 @@ async fn handle_pending_server_interview( progress_ui: &mut run_progress::ProgressUI, styles: &'static Styles, json_output: bool, + printer: fabro_util::printer::Printer, ) -> Result> { let Some(question) = client.list_run_questions(run_id).await?.into_iter().next() else { return Ok(None); }; if json_pending_interview_requires_manual_input(json_output, auto_approve) { - eprintln!("{JSON_INTERVIEW_MESSAGE}"); + fabro_util::printerr!(printer, "{JSON_INTERVIEW_MESSAGE}"); return Ok(Some(ExitCode::from(1))); } if json_output { @@ -231,7 +245,7 @@ async fn handle_pending_server_interview( show_progress(progress_ui, json_output); if answer_requires_reattach(&answer) { - eprintln!("{INTERVIEW_UNANSWERED_MESSAGE}"); + fabro_util::printerr!(printer, "{INTERVIEW_UNANSWERED_MESSAGE}"); return Ok(Some(ExitCode::from(1))); } @@ -243,6 +257,7 @@ async fn handle_detach_signal( client: &server_client::ServerStoreClient, run_id: &RunId, kill_on_detach: bool, + printer: fabro_util::printer::Printer, ) { if kill_on_detach { let _ = client.cancel_run(run_id).await; @@ -258,7 +273,10 @@ async fn handle_detach_signal( sleep(Duration::from_millis(100)).await; } } else { - eprintln!("Detached from run (engine continues in background)"); + fabro_util::printerr!( + printer, + "Detached from run (engine continues in background)" + ); } } @@ -598,7 +616,13 @@ mod tests { }); let client = server_client::ServerStoreClient::new_no_proxy(&server.base_url()).unwrap(); - handle_detach_signal(&client, &run_id, true).await; + handle_detach_signal( + &client, + &run_id, + true, + fabro_util::printer::Printer::Default, + ) + .await; cancel_mock.assert(); state_mock.assert(); diff --git a/lib/crates/fabro-cli/src/commands/run/command.rs b/lib/crates/fabro-cli/src/commands/run/command.rs index 0dd791505..8874a34e0 100644 --- a/lib/crates/fabro-cli/src/commands/run/command.rs +++ b/lib/crates/fabro-cli/src/commands/run/command.rs @@ -1,5 +1,6 @@ use anyhow::Result; use fabro_types::settings::cli::OutputVerbosity; +use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use crate::args::{GlobalArgs, RunArgs}; @@ -7,15 +8,22 @@ 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) -> Result<()> { +pub(crate) async fn execute( + mut args: RunArgs, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); - let ctx = CommandContext::for_target(&args.target)?; + 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 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)).await?; + let created_run = Box::pin(super::create::create_run( + &ctx, &args, cli, styles, quiet, printer, + )) + .await?; #[cfg(feature = "sleep_inhibitor")] let _sleep_guard = crate::sleep_inhibitor::guard(prevent_idle_sleep); @@ -30,7 +38,7 @@ pub(crate) async fn execute(mut args: RunArgs, globals: &GlobalArgs) -> Result<( if globals.json { print_json_pretty(&serde_json::json!({ "run_id": created_run.run_id }))?; } else { - println!("{}", created_run.run_id); + fabro_util::printout!(printer, "{}", created_run.run_id); } } else { let exit_code = super::attach::attach_run_with_client( @@ -39,6 +47,7 @@ pub(crate) async fn execute(mut args: RunArgs, globals: &GlobalArgs) -> Result<( true, styles, globals.json, + printer, ) .await?; if !globals.json { @@ -47,6 +56,7 @@ pub(crate) async fn execute(mut args: RunArgs, globals: &GlobalArgs) -> Result<( &created_run.run_id, created_run.local_run_dir.as_deref(), styles, + printer, ) .await?; } diff --git a/lib/crates/fabro-cli/src/commands/run/cp.rs b/lib/crates/fabro-cli/src/commands/run/cp.rs index 52245830d..6dbd1f9a1 100644 --- a/lib/crates/fabro-cli/src/commands/run/cp.rs +++ b/lib/crates/fabro-cli/src/commands/run/cp.rs @@ -1,6 +1,7 @@ use std::path::{Path, PathBuf}; use anyhow::{Context, Result, bail}; +use fabro_util::printer::Printer; use tokio::fs; use tracing::{debug, info}; @@ -24,7 +25,7 @@ enum CopyDirection { }, } -pub(crate) async fn cp_command(args: CpArgs, globals: &GlobalArgs) -> Result<()> { +pub(crate) async fn cp_command(args: CpArgs, globals: &GlobalArgs, printer: Printer) -> Result<()> { let direction = parse_direction(&args.src, &args.dst)?; match direction { @@ -33,7 +34,8 @@ pub(crate) async fn cp_command(args: CpArgs, globals: &GlobalArgs) -> Result<()> remote_path, local_path, } => { - let (client, run_id) = resolve_client_and_run_id(&args.server, &run_prefix).await?; + let (client, run_id) = + resolve_client_and_run_id(&args.server, &run_prefix, printer).await?; let file_count = if args.recursive { Some(download_recursive(&client, &run_id, &remote_path, &local_path).await?) @@ -63,7 +65,8 @@ pub(crate) async fn cp_command(args: CpArgs, globals: &GlobalArgs) -> Result<()> run_prefix, remote_path, } => { - let (client, run_id) = resolve_client_and_run_id(&args.server, &run_prefix).await?; + let (client, run_id) = + resolve_client_and_run_id(&args.server, &run_prefix, printer).await?; let file_count = if args.recursive { Some(upload_recursive(&client, &run_id, &local_path, &remote_path).await?) @@ -117,8 +120,9 @@ fn parse_direction(src: &str, dst: &str) -> Result { async fn resolve_client_and_run_id( server: &ServerTargetArgs, run_prefix: &str, + printer: Printer, ) -> Result<(ServerStoreClient, fabro_types::RunId)> { - let ctx = CommandContext::for_target(server)?; + let ctx = CommandContext::for_target(server, printer)?; 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/create.rs b/lib/crates/fabro-cli/src/commands/run/create.rs index 603b4a647..8019aa27c 100644 --- a/lib/crates/fabro-cli/src/commands/run/create.rs +++ b/lib/crates/fabro-cli/src/commands/run/create.rs @@ -5,6 +5,7 @@ use fabro_config::load::load_settings_user; use fabro_config::user::active_settings_path; use fabro_types::RunId; use fabro_types::settings::SettingsLayer; +use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use super::output::{api_diagnostics_to_local, print_preflight_workflow_summary}; @@ -29,6 +30,7 @@ pub(crate) async fn create_run( _cli_defaults: SettingsLayer, styles: &Styles, quiet: bool, + printer: Printer, ) -> anyhow::Result { let workflow_path = args .workflow @@ -61,7 +63,12 @@ pub(crate) async fn create_run( .iter() .any(|diagnostic| diagnostic.severity == fabro_validate::Severity::Error) { - print_preflight_workflow_summary(&preflight.workflow, Some(&built.target_path), styles); + print_preflight_workflow_summary( + &preflight.workflow, + Some(&built.target_path), + styles, + printer, + ); } } diff --git a/lib/crates/fabro-cli/src/commands/run/diff.rs b/lib/crates/fabro-cli/src/commands/run/diff.rs index 81ab88890..7d7f3dcfc 100644 --- a/lib/crates/fabro-cli/src/commands/run/diff.rs +++ b/lib/crates/fabro-cli/src/commands/run/diff.rs @@ -1,6 +1,7 @@ use std::io::{self, IsTerminal, Write}; use anyhow::{Context, Result, bail}; +use fabro_util::printer::Printer; use tracing::{debug, info}; use crate::args::{DiffArgs, GlobalArgs}; @@ -9,9 +10,9 @@ 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) -> Result<()> { +pub(crate) async fn run(args: DiffArgs, globals: &GlobalArgs, printer: Printer) -> Result<()> { info!(run_id = %args.run, "Showing diff"); - let ctx = CommandContext::for_target(&args.server)?; + let ctx = CommandContext::for_target(&args.server, printer)?; 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/run/fork.rs b/lib/crates/fabro-cli/src/commands/run/fork.rs index 155ae55f7..203cc8ff9 100644 --- a/lib/crates/fabro-cli/src/commands/run/fork.rs +++ b/lib/crates/fabro-cli/src/commands/run/fork.rs @@ -1,5 +1,6 @@ use anyhow::{Context, Result}; use fabro_checkpoint::git::Store; +use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use fabro_workflow::operations::{ForkRunInput, RewindTarget, build_timeline_or_rebuild, fork}; use git2::Repository; @@ -11,9 +12,14 @@ use crate::server_runs::ServerSummaryLookup; use crate::shared::print_json_pretty; use crate::shared::repo::ensure_matching_repo_origin; -pub(crate) async fn run(args: &ForkArgs, styles: &Styles, globals: &GlobalArgs) -> Result<()> { +pub(crate) async fn run( + args: &ForkArgs, + styles: &Styles, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { let repo = Repository::discover(".").context("not in a git repository")?; - let ctx = CommandContext::for_target(&args.server)?; + let ctx = CommandContext::for_target(&args.server, printer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(&args.run_id)?; let run_id = run.run_id(); @@ -31,7 +37,7 @@ pub(crate) async fn run(args: &ForkArgs, styles: &Styles, globals: &GlobalArgs) print_json_pretty(&super::rewind::timeline_entries_json(&timeline))?; return Ok(()); } - super::rewind::print_timeline(&timeline, styles); + super::rewind::print_timeline(&timeline, styles, printer); return Ok(()); } @@ -57,12 +63,14 @@ pub(crate) async fn run(args: &ForkArgs, styles: &Styles, globals: &GlobalArgs) "target": target, }))?; } else { - eprintln!( + fabro_util::printerr!( + printer, "\nForked run {} -> {}", &run_id_string[..8.min(run_id_string.len())], &new_run_id_string[..8.min(new_run_id_string.len())] ); - eprintln!( + fabro_util::printerr!( + printer, "To resume: fabro resume {}", &new_run_id_string[..8.min(new_run_id_string.len())] ); diff --git a/lib/crates/fabro-cli/src/commands/run/logs.rs b/lib/crates/fabro-cli/src/commands/run/logs.rs index 9cde08d5e..ec3e65b07 100644 --- a/lib/crates/fabro-cli/src/commands/run/logs.rs +++ b/lib/crates/fabro-cli/src/commands/run/logs.rs @@ -5,6 +5,7 @@ use std::time::Duration; use anyhow::{Context, Result, bail}; use chrono::{DateTime, Utc}; use fabro_util::json::normalize_json_value; +use fabro_util::printer::Printer; use fabro_util::redact::redact_jsonl_line; use fabro_util::terminal::Styles; use tokio::time; @@ -18,8 +19,13 @@ use crate::shared::format_usd_micros; const FOLLOW_TERMINAL_GRACE: Duration = Duration::from_millis(500); -pub(crate) async fn run(args: &LogsArgs, styles: &Styles, globals: &GlobalArgs) -> Result<()> { - let ctx = CommandContext::for_target(&args.server)?; +pub(crate) async fn run( + args: &LogsArgs, + styles: &Styles, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { + let ctx = CommandContext::for_target(&args.server, printer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(&args.run)?; let client = lookup.client(); diff --git a/lib/crates/fabro-cli/src/commands/run/mod.rs b/lib/crates/fabro-cli/src/commands/run/mod.rs index 4f186895d..a42ad6f0a 100644 --- a/lib/crates/fabro-cli/src/commands/run/mod.rs +++ b/lib/crates/fabro-cli/src/commands/run/mod.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use crate::args::{AttachArgs, GlobalArgs, RunArgs, RunCommands, RunWorkerArgs, StartArgs}; @@ -31,27 +32,32 @@ fn apply_json_defaults(args: &mut RunArgs, globals: &GlobalArgs) { } } -pub(crate) async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<()> { +pub(crate) async fn dispatch( + cmd: RunCommands, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { match cmd { RunCommands::Run(mut args) => { apply_json_defaults(&mut args, globals); - Box::pin(command::execute(args, globals)).await + Box::pin(command::execute(args, globals, printer)).await } RunCommands::Create(mut args) => { apply_json_defaults(&mut args, globals); 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)?; - let created_run = Box::pin(create::create_run(&ctx, &args, cli, styles, true)).await?; + 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 { print_json_pretty(&serde_json::json!({ "run_id": created_run.run_id }))?; } else { - println!("{}", created_run.run_id); + fabro_util::printout!(printer, "{}", created_run.run_id); } Ok(()) } RunCommands::Start(StartArgs { server, run }) => { - let ctx = CommandContext::for_target(&server)?; + let ctx = CommandContext::for_target(&server, printer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run_info = lookup.resolve(&run)?; let run_id = run_info.run_id(); @@ -63,7 +69,7 @@ pub(crate) async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<( } RunCommands::Attach(AttachArgs { server, run }) => { let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); - let ctx = CommandContext::for_target(&server)?; + let ctx = CommandContext::for_target(&server, printer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run_info = lookup.resolve(&run)?; let run_id = run_info.run_id(); @@ -73,6 +79,7 @@ pub(crate) async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<( false, styles, globals.json, + printer, ) .await?; if exit_code != std::process::ExitCode::SUCCESS { @@ -87,31 +94,31 @@ pub(crate) async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<( run_id, mode, }) => runner::execute(run_id, server, artifact_upload_token, run_dir, mode).await, - RunCommands::Diff(args) => diff::run(args, globals).await, + RunCommands::Diff(args) => diff::run(args, globals, printer).await, RunCommands::Logs(args) => { let styles = Styles::detect_stdout(); - logs::run(&args, &styles, globals).await + logs::run(&args, &styles, globals, 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)?; + let ctx = CommandContext::for_target(&args.server, printer)?; crate::sleep_inhibitor::guard(ctx.cli_settings().exec.prevent_idle_sleep) }; - resume::resume_command(args, styles, globals).await + resume::resume_command(args, styles, globals, printer).await } RunCommands::Rewind(args) => { let styles = Styles::detect_stderr(); - Box::pin(rewind::run(&args, &styles, globals)).await + Box::pin(rewind::run(&args, &styles, globals, printer)).await } RunCommands::Fork(args) => { let styles = Styles::detect_stderr(); - Box::pin(fork::run(&args, &styles, globals)).await + Box::pin(fork::run(&args, &styles, globals, printer)).await } RunCommands::Wait(args) => { let styles = Styles::detect_stderr(); - wait::run(&args, &styles, globals).await + wait::run(&args, &styles, globals, printer).await } } } diff --git a/lib/crates/fabro-cli/src/commands/run/output.rs b/lib/crates/fabro-cli/src/commands/run/output.rs index 8eb06a944..e51a9cddf 100644 --- a/lib/crates/fabro-cli/src/commands/run/output.rs +++ b/lib/crates/fabro-cli/src/commands/run/output.rs @@ -7,6 +7,7 @@ use fabro_types::{ PullRequestRecord, RunBlobId, RunId, parse_blob_ref, parse_legacy_blob_file_ref, }; use fabro_util::check_report::{CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus}; +use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use fabro_util::text::strip_goal_decoration; use fabro_workflow::outcome::StageStatus; @@ -22,6 +23,7 @@ pub(crate) fn print_preflight_workflow_summary( workflow: &types::PreflightWorkflowSummary, graph_path_override: Option<&Path>, styles: &Styles, + printer: Printer, ) { let graph_path = graph_path_override .map(relative_path) @@ -42,7 +44,8 @@ pub(crate) fn print_preflight_workflow_summary( .map(api_diagnostic_to_local) .collect::>(); - eprintln!( + fabro_util::printerr!( + printer, "{} {} {}", styles.bold.apply_to("Workflow:"), workflow.name, @@ -51,7 +54,8 @@ pub(crate) fn print_preflight_workflow_summary( workflow.nodes, workflow.edges )), ); - eprintln!( + fabro_util::printerr!( + printer, "{} {}", styles.dim.apply_to("Graph:"), styles.dim.apply_to(graph_path), @@ -59,10 +63,10 @@ pub(crate) fn print_preflight_workflow_summary( if !workflow.goal.is_empty() { let stripped = strip_goal_decoration(&workflow.goal); - eprintln!("{} {stripped}\n", styles.bold.apply_to("Goal:")); + fabro_util::printerr!(printer, "{} {stripped}\n", styles.bold.apply_to("Goal:")); } - print_diagnostics(&diagnostics, styles); + print_diagnostics(&diagnostics, styles, printer); } fn api_diagnostic_to_local(diagnostic: &types::WorkflowDiagnostic) -> fabro_validate::Diagnostic { @@ -129,6 +133,7 @@ pub(crate) async fn print_run_summary_with_client( run_id: &fabro_types::RunId, local_run_dir: Option<&Path>, styles: &Styles, + printer: Printer, ) -> Result<()> { let run_state = client.get_run_state(run_id).await?; let checkpoint = run_state.checkpoint.clone(); @@ -148,12 +153,13 @@ pub(crate) async fn print_run_summary_with_client( None, pr_url.as_deref(), styles, + printer, ); let final_output = resolve_final_output_with_client(client, run_id, checkpoint.as_ref()).await?; - print_final_output(final_output.as_deref(), styles); + print_final_output(final_output.as_deref(), styles, printer); if local_run_dir.is_some() { - print_assets_with_client(client, run_id, styles).await?; + print_assets_with_client(client, run_id, styles, printer).await?; } Ok(()) } @@ -165,18 +171,24 @@ pub(crate) fn print_run_conclusion( pushed_branch: Option<&str>, pr_url: Option<&str>, styles: &Styles, + printer: Printer, ) { let run_id = run_id.to_string(); - eprintln!("\n{}", styles.bold.apply_to("=== Run Result ===")); - eprintln!("{}", styles.dim.apply_to(format!("Run: {run_id}"))); + fabro_util::printerr!(printer, "\n{}", styles.bold.apply_to("=== Run Result ===")); + fabro_util::printerr!( + printer, + "{}", + styles.dim.apply_to(format!("Run: {run_id}")) + ); let status_str = conclusion.status.to_string().to_uppercase(); let status_color = match conclusion.status { StageStatus::Success | StageStatus::PartialSuccess => &styles.bold_green, _ => &styles.bold_red, }; - eprintln!("Status: {}", status_color.apply_to(&status_str)); - eprintln!( + fabro_util::printerr!(printer, "Status: {}", status_color.apply_to(&status_str)); + fabro_util::printerr!( + printer, "Duration: {}", HumanDuration(Duration::from_millis(conclusion.duration_ms)) ); @@ -186,7 +198,8 @@ pub(crate) fn print_run_conclusion( if total_tokens > 0 { if let Some(total_usd_micros) = billing.total_usd_micros { if total_usd_micros > 0 { - eprintln!( + fabro_util::printerr!( + printer, "{}", styles.dim.apply_to(format!( "Cost: {} ({} toks)", @@ -196,7 +209,8 @@ pub(crate) fn print_run_conclusion( ); } } else { - eprintln!( + fabro_util::printerr!( + printer, "{}", styles .dim @@ -204,7 +218,8 @@ pub(crate) fn print_run_conclusion( ); } if billing.cache_read_tokens > 0 || billing.cache_write_tokens > 0 { - eprintln!( + fabro_util::printerr!( + printer, "{}", styles.dim.apply_to(format!( "Cache: {} read, {} write", @@ -214,7 +229,8 @@ pub(crate) fn print_run_conclusion( ); } if billing.reasoning_tokens > 0 { - eprintln!( + fabro_util::printerr!( + printer, "{}", styles.dim.apply_to(format!( "Reasoning: {} tokens", @@ -223,7 +239,8 @@ pub(crate) fn print_run_conclusion( ); } } else if billing.total_usd_micros.is_none() { - eprintln!( + fabro_util::printerr!( + printer, "{}", styles .dim @@ -233,7 +250,8 @@ pub(crate) fn print_run_conclusion( } if let Some(run_dir) = run_dir { - eprintln!( + fabro_util::printerr!( + printer, "{}", styles .dim @@ -242,28 +260,32 @@ pub(crate) fn print_run_conclusion( } if let Some(ref failure) = conclusion.failure_reason { - eprintln!("Failure: {}", styles.red.apply_to(failure)); + fabro_util::printerr!(printer, "Failure: {}", styles.red.apply_to(failure)); } if pushed_branch.is_some() || pr_url.is_some() { - eprintln!(); + fabro_util::printerr!(printer, ""); if let Some(branch) = pushed_branch { - eprintln!("{} {branch}", styles.bold.apply_to("Pushed branch:")); + fabro_util::printerr!( + printer, + "{} {branch}", + styles.bold.apply_to("Pushed branch:") + ); } if let Some(url) = pr_url { - eprintln!("{} {url}", styles.bold.apply_to("Pull request:")); + fabro_util::printerr!(printer, "{} {url}", styles.bold.apply_to("Pull request:")); } } } -pub(crate) fn print_final_output(output: Option<&str>, styles: &Styles) { +pub(crate) fn print_final_output(output: Option<&str>, styles: &Styles, printer: Printer) { let Some(output) = output else { return; }; let text = output.trim(); if !text.is_empty() { - eprintln!("\n{}", styles.bold.apply_to("=== Output ===")); - eprintln!("{}", styles.render_markdown(text)); + fabro_util::printerr!(printer, "\n{}", styles.bold.apply_to("=== Output ===")); + fabro_util::printerr!(printer, "{}", styles.render_markdown(text)); } } @@ -335,6 +357,7 @@ async fn print_assets_with_client( client: &server_client::ServerStoreClient, run_id: &RunId, styles: &Styles, + printer: Printer, ) -> Result<()> { let entries = list_artifact_display_entries_with_client(client, run_id).await?; if entries.is_empty() { @@ -354,13 +377,22 @@ async fn print_assets_with_client( .unwrap_or(5) .max(5); - eprintln!("\n{}", styles.bold.apply_to("=== Artifacts ===")); - eprintln!("{:retry_width$} PATH", "NODE", "RETRY"); + fabro_util::printerr!(printer, "\n{}", styles.bold.apply_to("=== Artifacts ===")); + fabro_util::printerr!( + printer, + "{:retry_width$} PATH", + "NODE", + "RETRY" + ); for (node_slug, retry, relative_path) in &entries { - eprintln!("{node_slug:retry_width$} {relative_path}"); + fabro_util::printerr!( + printer, + "{node_slug:retry_width$} {relative_path}" + ); } - eprintln!(); - eprintln!( + fabro_util::printerr!(printer, ""); + fabro_util::printerr!( + printer, "{}", styles.dim.apply_to(format!( "Copy with: fabro artifact cp {run_id}: --node --retry " diff --git a/lib/crates/fabro-cli/src/commands/run/preview.rs b/lib/crates/fabro-cli/src/commands/run/preview.rs index ecdd4b3ae..a39b1f0dc 100644 --- a/lib/crates/fabro-cli/src/commands/run/preview.rs +++ b/lib/crates/fabro-cli/src/commands/run/preview.rs @@ -1,4 +1,5 @@ use anyhow::{Context, Result}; +use fabro_util::printer::Printer; use tracing::info; use crate::args::{GlobalArgs, PreviewArgs}; @@ -6,8 +7,8 @@ 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) -> Result<()> { - let ctx = CommandContext::for_target(&args.server)?; +pub(crate) async fn run(args: PreviewArgs, globals: &GlobalArgs, printer: Printer) -> Result<()> { + let ctx = CommandContext::for_target(&args.server, printer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(&args.run)?; let run_id = run.run_id(); @@ -35,9 +36,19 @@ pub(crate) async fn run(args: PreviewArgs, globals: &GlobalArgs) -> Result<()> { } } } else if let Some(token) = response.token.as_deref() { - print!("{}", format_standard_output(&response.url, token)); + { + use std::fmt::Write as _; + let _ = write!( + printer.stdout(), + "{}", + format_standard_output(&response.url, token) + ); + } } else { - print!("{}", format_signed_output(&response.url)); + { + use std::fmt::Write as _; + let _ = write!(printer.stdout(), "{}", format_signed_output(&response.url)); + } } if args.open && !globals.json { diff --git a/lib/crates/fabro-cli/src/commands/run/resume.rs b/lib/crates/fabro-cli/src/commands/run/resume.rs index b79e52c21..eb37be3d0 100644 --- a/lib/crates/fabro-cli/src/commands/run/resume.rs +++ b/lib/crates/fabro-cli/src/commands/run/resume.rs @@ -1,3 +1,4 @@ +use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use crate::args::{GlobalArgs, ResumeArgs}; @@ -14,8 +15,9 @@ pub(crate) async fn resume_command( args: ResumeArgs, styles: &'static Styles, globals: &GlobalArgs, + printer: Printer, ) -> anyhow::Result<()> { - let ctx = CommandContext::for_target(&args.server)?; + let ctx = CommandContext::for_target(&args.server, printer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(&args.run)?; let run_id = run.run_id(); @@ -26,7 +28,7 @@ pub(crate) async fn resume_command( if globals.json { print_json_pretty(&serde_json::json!({ "run_id": run_id }))?; } else { - println!("{run_id}"); + fabro_util::printout!(printer, "{run_id}"); } } else { let exit_code = super::attach::attach_run_with_client( @@ -35,11 +37,18 @@ pub(crate) async fn resume_command( true, styles, globals.json, + printer, ) .await?; if !globals.json { - super::output::print_run_summary_with_client(lookup.client(), &run_id, None, styles) - .await?; + super::output::print_run_summary_with_client( + lookup.client(), + &run_id, + None, + styles, + printer, + ) + .await?; } if exit_code != std::process::ExitCode::SUCCESS { std::process::exit(1); diff --git a/lib/crates/fabro-cli/src/commands/run/rewind.rs b/lib/crates/fabro-cli/src/commands/run/rewind.rs index 95caf18cb..89dc129ef 100644 --- a/lib/crates/fabro-cli/src/commands/run/rewind.rs +++ b/lib/crates/fabro-cli/src/commands/run/rewind.rs @@ -4,6 +4,7 @@ use cli_table::{Cell, CellStruct, Color, Style, Table}; use fabro_checkpoint::git::Store; use fabro_types::run_event::{CheckpointCompletedProps, RunRewoundProps, RunSubmittedProps}; use fabro_types::{EventBody, RunEvent}; +use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use fabro_workflow::git::MetadataStore; use fabro_workflow::operations::{ @@ -28,9 +29,14 @@ pub(crate) struct TimelineEntryJson { run_commit_sha: Option, } -pub(crate) async fn run(args: &RewindArgs, styles: &Styles, globals: &GlobalArgs) -> Result<()> { +pub(crate) async fn run( + args: &RewindArgs, + styles: &Styles, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { let repo = Repository::discover(".").context("not in a git repository")?; - let ctx = CommandContext::for_target(&args.server)?; + let ctx = CommandContext::for_target(&args.server, printer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(&args.run_id)?; let run_id = run.run_id(); @@ -48,7 +54,7 @@ pub(crate) async fn run(args: &RewindArgs, styles: &Styles, globals: &GlobalArgs print_json_pretty(&timeline_entries_json(&timeline))?; return Ok(()); } - print_timeline(&timeline, styles); + print_timeline(&timeline, styles, printer); return Ok(()); } @@ -70,7 +76,8 @@ pub(crate) async fn run(args: &RewindArgs, styles: &Styles, globals: &GlobalArgs "target": args.target.as_deref().unwrap(), }))?; } else { - eprintln!( + fabro_util::printerr!( + printer, "\nTo resume: fabro resume {}", &run_id_string[..8.min(run_id_string.len())] ); @@ -205,9 +212,9 @@ fn run_event(run_id: fabro_types::RunId, node_id: Option, body: EventBod } } -pub(crate) fn print_timeline(timeline: &RunTimeline, styles: &Styles) { +pub(crate) fn print_timeline(timeline: &RunTimeline, styles: &Styles, printer: Printer) { if timeline.entries.is_empty() { - eprintln!("No checkpoints found."); + fabro_util::printerr!(printer, "No checkpoints found."); return; } diff --git a/lib/crates/fabro-cli/src/commands/run/ssh.rs b/lib/crates/fabro-cli/src/commands/run/ssh.rs index 725622c24..9242489d5 100644 --- a/lib/crates/fabro-cli/src/commands/run/ssh.rs +++ b/lib/crates/fabro-cli/src/commands/run/ssh.rs @@ -1,4 +1,5 @@ use anyhow::{Result, bail}; +use fabro_util::printer::Printer; use tracing::info; use crate::args::{GlobalArgs, SshArgs}; @@ -6,12 +7,12 @@ 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) -> Result<()> { +pub(crate) async fn run(args: SshArgs, globals: &GlobalArgs, printer: Printer) -> Result<()> { if globals.json && !args.print { globals.require_no_json()?; } - let ctx = CommandContext::for_target(&args.server)?; + let ctx = CommandContext::for_target(&args.server, printer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(&args.run)?; let run_id = run.run_id(); @@ -26,7 +27,10 @@ pub(crate) async fn run(args: SshArgs, globals: &GlobalArgs) -> Result<()> { if globals.json { print_json_pretty(&serde_json::json!({ "command": ssh.command }))?; } else { - print!("{}", format_output(&ssh.command)); + { + use std::fmt::Write as _; + let _ = write!(printer.stdout(), "{}", format_output(&ssh.command)); + } } } else { exec_ssh(&ssh.command)?; diff --git a/lib/crates/fabro-cli/src/commands/run/wait.rs b/lib/crates/fabro-cli/src/commands/run/wait.rs index f655645f9..e99bc363e 100644 --- a/lib/crates/fabro-cli/src/commands/run/wait.rs +++ b/lib/crates/fabro-cli/src/commands/run/wait.rs @@ -2,6 +2,7 @@ use std::io::Write; use anyhow::{Result, bail}; use fabro_types::RunId; +use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use fabro_workflow::records::Conclusion; use fabro_workflow::run_status::RunStatus; @@ -17,8 +18,13 @@ const WAIT_STARTUP_GRACE: std::time::Duration = std::time::Duration::from_millis #[cfg(not(test))] const WAIT_STARTUP_GRACE: std::time::Duration = std::time::Duration::from_secs(3); -pub(crate) async fn run(args: &WaitArgs, styles: &Styles, globals: &GlobalArgs) -> Result<()> { - let ctx = CommandContext::for_target(&args.server)?; +pub(crate) async fn run( + args: &WaitArgs, + styles: &Styles, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { + let ctx = CommandContext::for_target(&args.server, printer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run_info = lookup.resolve(&args.run)?; let client = lookup.client(); @@ -73,7 +79,7 @@ pub(crate) async fn run(args: &WaitArgs, styles: &Styles, globals: &GlobalArgs) serde_json::to_writer_pretty(&mut out, &json_value)?; writeln!(out)?; } else { - print_human_output(final_status, &run_id, conclusion.as_ref(), styles); + print_human_output(final_status, &run_id, conclusion.as_ref(), styles, printer); } if final_status == RunStatus::Succeeded { @@ -110,6 +116,7 @@ fn print_human_output( run_id: &RunId, conclusion: Option<&Conclusion>, styles: &Styles, + printer: Printer, ) { let (style, label) = match status { RunStatus::Succeeded => (&styles.bold_green, "Succeeded"), @@ -134,7 +141,8 @@ fn print_human_output( None => String::new(), }; - eprintln!( + fabro_util::printerr!( + printer, "{} {}{details}", status_display, styles.dim.apply_to(run_id), @@ -239,13 +247,25 @@ mod tests { total_retries: 0, }; // Just verify no panic; actual stderr output is hard to capture - print_human_output(RunStatus::Succeeded, &run_id, Some(&conclusion), &styles); + print_human_output( + RunStatus::Succeeded, + &run_id, + Some(&conclusion), + &styles, + Printer::Default, + ); } #[test] fn human_output_failed_no_conclusion() { let styles = no_color_styles(); - print_human_output(RunStatus::Failed, &fixtures::RUN_6, None, &styles); + print_human_output( + RunStatus::Failed, + &fixtures::RUN_6, + None, + &styles, + Printer::Default, + ); } #[test] diff --git a/lib/crates/fabro-cli/src/commands/runs/inspect.rs b/lib/crates/fabro-cli/src/commands/runs/inspect.rs index 3933c3fed..b6f9b01dd 100644 --- a/lib/crates/fabro-cli/src/commands/runs/inspect.rs +++ b/lib/crates/fabro-cli/src/commands/runs/inspect.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use fabro_util::printer::Printer; use fabro_workflow::run_status::RunStatus; use serde::Serialize; @@ -18,15 +19,15 @@ pub(crate) struct InspectOutput { pub sandbox: Option, } -pub(crate) async fn run(args: &InspectArgs, _globals: &GlobalArgs) -> Result<()> { - let ctx = CommandContext::for_target(&args.server)?; +pub(crate) async fn run(args: &InspectArgs, _globals: &GlobalArgs, printer: Printer) -> Result<()> { + let ctx = CommandContext::for_target(&args.server, printer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let run = lookup.resolve(&args.run)?; let run_id = run.run_id(); let state = lookup.client().get_run_state(&run_id).await?; let output = inspect_run_state(&run, state); let json = serde_json::to_string_pretty(&[output])?; - println!("{json}"); + fabro_util::printout!(printer, "{json}"); Ok(()) } diff --git a/lib/crates/fabro-cli/src/commands/runs/list.rs b/lib/crates/fabro-cli/src/commands/runs/list.rs index 9d8168310..ca09c0aa8 100644 --- a/lib/crates/fabro-cli/src/commands/runs/list.rs +++ b/lib/crates/fabro-cli/src/commands/runs/list.rs @@ -4,6 +4,7 @@ use anyhow::Result; use chrono::Utc; use cli_table::format::{Border, Separator}; use cli_table::{Cell, CellStruct, Color, Style, Table}; +use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use fabro_util::text::strip_goal_decoration; use fabro_workflow::run_status::RunStatus; @@ -14,13 +15,13 @@ use crate::command_context::CommandContext; use crate::server_runs::{ServerSummaryLookup, filter_server_runs}; use crate::shared::{color_if, format_duration_ms, tilde_path}; -#[allow(clippy::print_stdout)] pub(crate) async fn list_command( args: &RunsListArgs, styles: &Styles, globals: &GlobalArgs, + printer: Printer, ) -> Result<()> { - let ctx = CommandContext::for_target(&args.server)?; + let ctx = CommandContext::for_target(&args.server, printer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; let label_filters = parse_label_filters(&args.filter.label); let filtered = filter_server_runs( @@ -50,13 +51,13 @@ pub(crate) async fn list_command( }) }) .collect(); - println!("{}", serde_json::to_string_pretty(&json_rows)?); + fabro_util::printout!(printer, "{}", serde_json::to_string_pretty(&json_rows)?); return Ok(()); } if args.quiet { for run in &filtered { - println!("{}", run.run_id()); + fabro_util::printout!(printer, "{}", run.run_id()); } return Ok(()); } @@ -130,9 +131,9 @@ pub(crate) async fn list_command( .color_choice(color_choice) .border(Border::builder().build()) .separator(Separator::builder().build()); - println!("{}", table.display()?); + fabro_util::printout!(printer, "{}", table.display()?); - eprintln!("\n{} run(s) listed.", display_runs.len()); + fabro_util::printerr!(printer, "\n{} run(s) listed.", display_runs.len()); Ok(()) } diff --git a/lib/crates/fabro-cli/src/commands/runs/mod.rs b/lib/crates/fabro-cli/src/commands/runs/mod.rs index a05f1d76b..22dc39a0e 100644 --- a/lib/crates/fabro-cli/src/commands/runs/mod.rs +++ b/lib/crates/fabro-cli/src/commands/runs/mod.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use crate::args::{GlobalArgs, RunsCommands}; @@ -7,14 +8,18 @@ pub(crate) mod inspect; pub(crate) mod list; pub(crate) mod rm; -pub(crate) async fn dispatch(cmd: RunsCommands, globals: &GlobalArgs) -> Result<()> { +pub(crate) async fn dispatch( + cmd: RunsCommands, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { match cmd { RunsCommands::Ps(args) => { let styles = Styles::detect_stdout(); - list::list_command(&args, &styles, globals).await + list::list_command(&args, &styles, globals, printer).await } - RunsCommands::Rm(args) => rm::remove_command(&args, globals).await, - RunsCommands::Inspect(args) => inspect::run(&args, globals).await, + RunsCommands::Rm(args) => rm::remove_command(&args, globals, printer).await, + RunsCommands::Inspect(args) => inspect::run(&args, globals, 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 065261cf1..4c8a33f57 100644 --- a/lib/crates/fabro-cli/src/commands/runs/rm.rs +++ b/lib/crates/fabro-cli/src/commands/runs/rm.rs @@ -1,4 +1,5 @@ use anyhow::{Context, Result, bail}; +use fabro_util::printer::Printer; use super::short_run_id; use crate::args::{GlobalArgs, RunsRemoveArgs}; @@ -9,10 +10,14 @@ use crate::server_runs::{ }; use crate::shared::print_json_pretty; -pub(crate) async fn remove_command(args: &RunsRemoveArgs, globals: &GlobalArgs) -> Result<()> { - let ctx = CommandContext::for_target(&args.server)?; +pub(crate) async fn remove_command( + args: &RunsRemoveArgs, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { + let ctx = CommandContext::for_target(&args.server, printer)?; let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; - remove_from(args, lookup.client(), lookup.runs(), globals).await + remove_from(args, lookup.client(), lookup.runs(), globals, printer).await } async fn remove_from( @@ -20,6 +25,7 @@ async fn remove_from( client: &server_client::ServerStoreClient, runs: &[ServerRunSummaryInfo], globals: &GlobalArgs, + printer: Printer, ) -> Result<()> { let mut had_errors = false; let mut removed = Vec::new(); @@ -30,7 +36,7 @@ async fn remove_from( Ok(run) => run, Err(err) => { if !globals.json { - eprintln!("error: {identifier}: {err}"); + fabro_util::printerr!(printer, "error: {identifier}: {err}"); } errors.push(serde_json::json!({ "identifier": identifier, @@ -49,7 +55,7 @@ async fn remove_from( run.status() ); if !globals.json { - eprintln!("{error}"); + fabro_util::printerr!(printer, "{error}"); } errors.push(serde_json::json!({ "identifier": identifier, @@ -62,7 +68,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 { - eprintln!("error: {identifier}: {err}"); + fabro_util::printerr!(printer, "error: {identifier}: {err}"); } errors.push(serde_json::json!({ "identifier": identifier, @@ -73,7 +79,7 @@ async fn remove_from( } removed.push(run_id.clone()); if !globals.json { - eprintln!("{}", short_run_id(&run_id)); + fabro_util::printerr!(printer, "{}", short_run_id(&run_id)); } } diff --git a/lib/crates/fabro-cli/src/commands/sandbox/mod.rs b/lib/crates/fabro-cli/src/commands/sandbox/mod.rs index 354cff411..85885729f 100644 --- a/lib/crates/fabro-cli/src/commands/sandbox/mod.rs +++ b/lib/crates/fabro-cli/src/commands/sandbox/mod.rs @@ -1,11 +1,16 @@ use anyhow::Result; +use fabro_util::printer::Printer; use crate::args::{GlobalArgs, SandboxCommand}; -pub(crate) async fn dispatch(command: SandboxCommand, globals: &GlobalArgs) -> Result<()> { +pub(crate) async fn dispatch( + command: SandboxCommand, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { match command { - SandboxCommand::Cp(args) => super::run::cp::cp_command(args, globals).await, - SandboxCommand::Preview(args) => super::run::preview::run(args, globals).await, - SandboxCommand::Ssh(args) => super::run::ssh::run(args, globals).await, + 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, } } diff --git a/lib/crates/fabro-cli/src/commands/secret/list.rs b/lib/crates/fabro-cli/src/commands/secret/list.rs index ad2dcf879..88a1d63ae 100644 --- a/lib/crates/fabro-cli/src/commands/secret/list.rs +++ b/lib/crates/fabro-cli/src/commands/secret/list.rs @@ -1,5 +1,6 @@ use anyhow::Result; use fabro_api::Client; +use fabro_util::printer::Printer; use crate::args::{GlobalArgs, SecretListArgs}; use crate::server_client; @@ -9,6 +10,7 @@ pub(super) async fn list_command( client: &Client, args: &SecretListArgs, globals: &GlobalArgs, + printer: Printer, ) -> Result<()> { let response = client .list_secrets() @@ -22,7 +24,7 @@ pub(super) async fn list_command( } let _ = args; for secret in secrets { - println!("{}\t{}", secret.name, secret.updated_at); + fabro_util::printout!(printer, "{}\t{}", secret.name, secret.updated_at); } Ok(()) } diff --git a/lib/crates/fabro-cli/src/commands/secret/mod.rs b/lib/crates/fabro-cli/src/commands/secret/mod.rs index fde139022..5dff3ac2d 100644 --- a/lib/crates/fabro-cli/src/commands/secret/mod.rs +++ b/lib/crates/fabro-cli/src/commands/secret/mod.rs @@ -3,16 +3,23 @@ mod rm; mod set; use anyhow::Result; +use fabro_util::printer::Printer; use crate::args::{GlobalArgs, SecretCommand, SecretNamespace}; use crate::command_context::CommandContext; -pub(crate) async fn dispatch(ns: SecretNamespace, globals: &GlobalArgs) -> Result<()> { - let ctx = CommandContext::for_target(&ns.target)?; +pub(crate) async fn dispatch( + ns: SecretNamespace, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { + let ctx = CommandContext::for_target(&ns.target, printer)?; let server = ctx.server().await?; match ns.command { - SecretCommand::List(args) => list::list_command(server.api(), &args, globals).await, - SecretCommand::Rm(args) => rm::rm_command(server.api(), &args, globals).await, - SecretCommand::Set(args) => set::set_command(server.api(), &args, globals).await, + 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, } } diff --git a/lib/crates/fabro-cli/src/commands/secret/rm.rs b/lib/crates/fabro-cli/src/commands/secret/rm.rs index 376abe5a6..742a2e949 100644 --- a/lib/crates/fabro-cli/src/commands/secret/rm.rs +++ b/lib/crates/fabro-cli/src/commands/secret/rm.rs @@ -1,5 +1,6 @@ use anyhow::Result; use fabro_api::Client; +use fabro_util::printer::Printer; use crate::args::{GlobalArgs, SecretRmArgs}; use crate::server_client; @@ -9,6 +10,7 @@ pub(super) async fn rm_command( client: &Client, args: &SecretRmArgs, globals: &GlobalArgs, + printer: Printer, ) -> Result<()> { client .delete_secret() @@ -19,7 +21,7 @@ pub(super) async fn rm_command( if globals.json { print_json_pretty(&serde_json::json!({ "key": args.key }))?; } else { - eprintln!("Removed {}", args.key); + fabro_util::printerr!(printer, "Removed {}", args.key); } Ok(()) } diff --git a/lib/crates/fabro-cli/src/commands/secret/set.rs b/lib/crates/fabro-cli/src/commands/secret/set.rs index 9ea94ab5b..bd9ec5d5a 100644 --- a/lib/crates/fabro-cli/src/commands/secret/set.rs +++ b/lib/crates/fabro-cli/src/commands/secret/set.rs @@ -1,5 +1,6 @@ use anyhow::Result; use fabro_api::{Client, types}; +use fabro_util::printer::Printer; use crate::args::{GlobalArgs, SecretSetArgs}; use crate::server_client; @@ -9,6 +10,7 @@ pub(super) async fn set_command( client: &Client, args: &SecretSetArgs, globals: &GlobalArgs, + printer: Printer, ) -> Result<()> { let meta = client .set_secret() @@ -23,7 +25,7 @@ pub(super) async fn set_command( if globals.json { print_json_pretty(&meta)?; } else { - eprintln!("Set {}", meta.name); + fabro_util::printerr!(printer, "Set {}", meta.name); } Ok(()) } diff --git a/lib/crates/fabro-cli/src/commands/server/foreground.rs b/lib/crates/fabro-cli/src/commands/server/foreground.rs index ba469d061..79a6501fa 100644 --- a/lib/crates/fabro-cli/src/commands/server/foreground.rs +++ b/lib/crates/fabro-cli/src/commands/server/foreground.rs @@ -6,6 +6,7 @@ use fabro_config::Storage; use fabro_server::bind::BindRequest; use fabro_server::serve; use fabro_server::serve::ServeArgs; +use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use super::record; @@ -16,7 +17,9 @@ pub(crate) async fn execute( bind: BindRequest, storage_dir: Option, styles: &'static Styles, + printer: Printer, ) -> Result<()> { + let _ = printer; serve_args.bind = Some(bind.to_string()); let _record_guard = scopeguard::guard(record_path.clone(), |path| { diff --git a/lib/crates/fabro-cli/src/commands/server/mod.rs b/lib/crates/fabro-cli/src/commands/server/mod.rs index 01de6dc1d..68ffa9fac 100644 --- a/lib/crates/fabro-cli/src/commands/server/mod.rs +++ b/lib/crates/fabro-cli/src/commands/server/mod.rs @@ -10,6 +10,7 @@ use anyhow::Result; use fabro_server::bind; use fabro_server::bind::BindRequest; use fabro_server::serve::ServeArgs; +use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use crate::args::{ @@ -17,7 +18,11 @@ use crate::args::{ }; use crate::user_config; -pub(crate) async fn dispatch(command: ServerCommand, _globals: &GlobalArgs) -> Result<()> { +pub(crate) async fn dispatch( + command: ServerCommand, + _globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { match command { ServerCommand::Start(ServerStartArgs { storage_dir, @@ -40,6 +45,7 @@ pub(crate) async fn dispatch(command: ServerCommand, _globals: &GlobalArgs) -> R serve_args, storage_dir, styles, + printer, )) .await } @@ -49,13 +55,13 @@ pub(crate) async fn dispatch(command: ServerCommand, _globals: &GlobalArgs) -> R }) => { let settings = user_config::load_settings_with_storage_dir(storage_dir.as_deref())?; let storage_dir = user_config::storage_dir(&settings)?; - stop::execute(&storage_dir, Duration::from_secs(timeout)); + stop::execute(&storage_dir, Duration::from_secs(timeout), printer); Ok(()) } ServerCommand::Status(ServerStatusArgs { storage_dir, json }) => { let settings = user_config::load_settings_with_storage_dir(storage_dir.as_deref())?; let storage_dir = user_config::storage_dir(&settings)?; - status::execute(&storage_dir, json) + status::execute(&storage_dir, json, printer) } ServerCommand::Serve(ServerServeArgs { storage_dir, @@ -85,6 +91,7 @@ pub(crate) async fn dispatch(command: ServerCommand, _globals: &GlobalArgs) -> R bind_addr, storage_dir.clone_path(), styles, + printer, )) .await } diff --git a/lib/crates/fabro-cli/src/commands/server/start.rs b/lib/crates/fabro-cli/src/commands/server/start.rs index c15680923..24435d955 100644 --- a/lib/crates/fabro-cli/src/commands/server/start.rs +++ b/lib/crates/fabro-cli/src/commands/server/start.rs @@ -10,6 +10,7 @@ use fabro_server::bind::{Bind, BindRequest}; use fabro_server::jwt_auth::FABRO_LOCAL_NO_AUTH_ENV; use fabro_server::serve; use fabro_server::serve::{DEFAULT_TCP_PORT, ServeArgs}; +use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use super::record; @@ -20,13 +21,14 @@ pub(crate) async fn execute( mut serve_args: ServeArgs, storage_dir: PathBuf, styles: &'static Styles, + printer: Printer, ) -> Result<()> { serve_args.bind = Some(bind.to_string()); if foreground { Box::pin(execute_foreground(bind, serve_args, storage_dir, styles)).await } else { - execute_daemon(&bind, &serve_args, &storage_dir, true) + execute_daemon(&bind, &serve_args, &storage_dir, true, printer) } } @@ -85,7 +87,13 @@ fn ensure_server_running_with_bind( Bind::Tcp(addr) => BindRequest::Tcp(*addr), }; - match execute_daemon(&bind_request, &serve_args, storage_dir, false) { + match execute_daemon( + &bind_request, + &serve_args, + storage_dir, + false, + Printer::Silent, + ) { Ok(()) => Ok(bind), Err(err) => { if let Some(existing) = record::active_server_record(storage_dir) { @@ -168,6 +176,7 @@ fn execute_daemon( serve_args: &ServeArgs, storage_dir: &Path, announce: bool, + printer: Printer, ) -> Result<()> { let lock_file = acquire_lock(storage_dir)?; let _lock_file = lock_file; // keep alive until function returns @@ -245,7 +254,7 @@ fn execute_daemon( record::remove_server_record(&record_path); let tail = read_log_tail(&log_path, 20); if !tail.is_empty() { - eprintln!("{tail}"); + fabro_util::printerr!(printer, "{tail}"); } bail!("Server exited immediately with status {status}"); } @@ -258,8 +267,13 @@ fn execute_daemon( if let Some(record) = record::read_server_record(&record_path) { if try_connect(&record.bind) { if announce { - maybe_warn_host_port_fallback(bind, &record.bind); - eprintln!("Server started (pid {}) on {}", child.id(), record.bind); + maybe_warn_host_port_fallback(bind, &record.bind, printer); + fabro_util::printerr!( + printer, + "Server started (pid {}) on {}", + child.id(), + record.bind + ); } return Ok(()); } @@ -269,7 +283,7 @@ fn execute_daemon( record::remove_server_record(&record_path); let tail = read_log_tail(&log_path, 20); if !tail.is_empty() { - eprintln!("{tail}"); + fabro_util::printerr!(printer, "{tail}"); } bail!("Server exited during startup with status {status}"); } @@ -283,7 +297,7 @@ fn execute_daemon( let _ = child.wait(); let tail = read_log_tail(&log_path, 20); if !tail.is_empty() { - eprintln!("{tail}"); + fabro_util::printerr!(printer, "{tail}"); } bail!("Server did not become ready within {timeout:?}"); } @@ -327,7 +341,7 @@ fn try_connect(bind: &Bind) -> bool { } } -fn maybe_warn_host_port_fallback(requested: &BindRequest, resolved: &Bind) { +fn maybe_warn_host_port_fallback(requested: &BindRequest, resolved: &Bind, printer: Printer) { let BindRequest::TcpHost(host) = requested else { return; }; @@ -335,7 +349,8 @@ fn maybe_warn_host_port_fallback(requested: &BindRequest, resolved: &Bind) { return; }; if addr.ip() == *host && addr.port() != DEFAULT_TCP_PORT { - eprintln!( + fabro_util::printerr!( + printer, "Warning: TCP port {DEFAULT_TCP_PORT} is unavailable on {host}; falling back to a random port." ); } diff --git a/lib/crates/fabro-cli/src/commands/server/status.rs b/lib/crates/fabro-cli/src/commands/server/status.rs index 2d84fbd90..298328786 100644 --- a/lib/crates/fabro-cli/src/commands/server/status.rs +++ b/lib/crates/fabro-cli/src/commands/server/status.rs @@ -2,15 +2,16 @@ use std::path::Path; use anyhow::Result; use chrono::Utc; +use fabro_util::printer::Printer; use super::record; -pub(crate) fn execute(storage_dir: &Path, json: bool) -> Result<()> { +pub(crate) fn execute(storage_dir: &Path, json: bool, printer: Printer) -> Result<()> { let Some(record) = record::active_server_record(storage_dir) else { if json { - println!(r#"{{"status":"stopped"}}"#); + fabro_util::printout!(printer, r#"{{"status":"stopped"}}"#); } else { - eprintln!("Server is not running"); + fabro_util::printerr!(printer, "Server is not running"); } std::process::exit(1); }; @@ -24,12 +25,15 @@ pub(crate) fn execute(storage_dir: &Path, json: bool) -> Result<()> { "started_at": record.started_at.to_rfc3339(), "uptime_seconds": uptime_seconds, }); - println!("{}", serde_json::to_string_pretty(&output)?); + fabro_util::printout!(printer, "{}", serde_json::to_string_pretty(&output)?); } else { let uptime = format_uptime(Utc::now() - record.started_at); - eprintln!( + fabro_util::printerr!( + printer, "Server running (pid {}) on {}, started {} ago", - record.pid, record.bind, uptime + record.pid, + record.bind, + uptime ); } diff --git a/lib/crates/fabro-cli/src/commands/server/stop.rs b/lib/crates/fabro-cli/src/commands/server/stop.rs index 067639b8c..c10dd49c5 100644 --- a/lib/crates/fabro-cli/src/commands/server/stop.rs +++ b/lib/crates/fabro-cli/src/commands/server/stop.rs @@ -3,12 +3,13 @@ use std::thread; use std::time::Duration; use fabro_server::bind::Bind; +use fabro_util::printer::Printer; use super::record; -pub(crate) fn execute(storage_dir: &Path, timeout: Duration) { +pub(crate) fn execute(storage_dir: &Path, timeout: Duration, printer: Printer) { let Some(active) = record::active_server_record_details(storage_dir) else { - eprintln!("Server is not running"); + fabro_util::printerr!(printer, "Server is not running"); std::process::exit(1); }; let record = active.record; @@ -36,5 +37,5 @@ pub(crate) fn execute(storage_dir: &Path, timeout: Duration) { let _ = std::fs::remove_file(path); } - eprintln!("Server stopped"); + fabro_util::printerr!(printer, "Server stopped"); } diff --git a/lib/crates/fabro-cli/src/commands/store/dump.rs b/lib/crates/fabro-cli/src/commands/store/dump.rs index 16b4020c9..fd63ad3fd 100644 --- a/lib/crates/fabro-cli/src/commands/store/dump.rs +++ b/lib/crates/fabro-cli/src/commands/store/dump.rs @@ -7,6 +7,7 @@ use bytes::Bytes; use fabro_store::{ArtifactStore, RunDatabase}; use fabro_store::{EventEnvelope, RunProjection, StageId}; use fabro_types::{RunBlobId, RunId}; +use fabro_util::printer::Printer; use fabro_workflow::run_dump::RunDump; use futures::future::BoxFuture; #[cfg(test)] @@ -18,7 +19,11 @@ use crate::server_runs::ServerRunLookup; use crate::shared::{absolute_or_current, print_json_pretty}; use crate::user_config::{load_settings_with_storage_dir, storage_dir}; -pub(crate) async fn dump_command(args: &StoreDumpArgs, globals: &GlobalArgs) -> Result<()> { +pub(crate) async fn dump_command( + args: &StoreDumpArgs, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { let cli_settings = load_settings_with_storage_dir(args.storage_dir.as_deref())?; let lookup = ServerRunLookup::connect(&storage_dir(&cli_settings)?).await?; let run = lookup.resolve(&args.run)?; @@ -33,7 +38,8 @@ pub(crate) async fn dump_command(args: &StoreDumpArgs, globals: &GlobalArgs) -> "file_count": file_count, }))?; } else { - println!( + fabro_util::printout!( + printer, "Exported {file_count} files for run {} to {}", run_id, args.output.display() diff --git a/lib/crates/fabro-cli/src/commands/store/mod.rs b/lib/crates/fabro-cli/src/commands/store/mod.rs index 158d97b2a..3947c5dd5 100644 --- a/lib/crates/fabro-cli/src/commands/store/mod.rs +++ b/lib/crates/fabro-cli/src/commands/store/mod.rs @@ -2,11 +2,16 @@ pub(crate) mod dump; pub(crate) mod rebuild; use anyhow::Result; +use fabro_util::printer::Printer; use crate::args::{GlobalArgs, StoreCommand, StoreNamespace}; -pub(crate) async fn dispatch(ns: StoreNamespace, globals: &GlobalArgs) -> Result<()> { +pub(crate) async fn dispatch( + ns: StoreNamespace, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { match ns.command { - StoreCommand::Dump(args) => dump::dump_command(&args, globals).await, + StoreCommand::Dump(args) => dump::dump_command(&args, globals, 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 5c970336a..f76ab1ef2 100644 --- a/lib/crates/fabro-cli/src/commands/system/df.rs +++ b/lib/crates/fabro-cli/src/commands/system/df.rs @@ -3,14 +3,19 @@ use chrono::{DateTime, Utc}; use cli_table::format::{Border, Justify, Separator}; use cli_table::{Cell, CellStruct, Style, Table}; use fabro_api::types; +use fabro_util::printer::Printer; use crate::args::{DfArgs, GlobalArgs}; 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) -> Result<()> { - let ctx = CommandContext::for_connection(&args.connection)?; +pub(super) async fn df_command( + args: &DfArgs, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { + let ctx = CommandContext::for_connection(&args.connection, printer)?; let server = ctx.server().await?; let output = server .api() diff --git a/lib/crates/fabro-cli/src/commands/system/events.rs b/lib/crates/fabro-cli/src/commands/system/events.rs index 03fa6dc9f..64ed80d7e 100644 --- a/lib/crates/fabro-cli/src/commands/system/events.rs +++ b/lib/crates/fabro-cli/src/commands/system/events.rs @@ -1,12 +1,17 @@ use anyhow::Result; +use fabro_util::printer::Printer; use futures::StreamExt; use crate::args::{GlobalArgs, SystemEventsArgs}; use crate::command_context::CommandContext; use crate::{server_client, sse}; -pub(super) async fn events_command(args: &SystemEventsArgs, globals: &GlobalArgs) -> Result<()> { - let ctx = CommandContext::for_connection(&args.connection)?; +pub(super) async fn events_command( + args: &SystemEventsArgs, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { + let ctx = CommandContext::for_connection(&args.connection, printer)?; let server = ctx.server().await?; let mut request = server.api().attach_events(); diff --git a/lib/crates/fabro-cli/src/commands/system/info.rs b/lib/crates/fabro-cli/src/commands/system/info.rs index 23934d6a4..43d0549cd 100644 --- a/lib/crates/fabro-cli/src/commands/system/info.rs +++ b/lib/crates/fabro-cli/src/commands/system/info.rs @@ -1,12 +1,17 @@ use anyhow::Result; +use fabro_util::printer::Printer; use crate::args::{GlobalArgs, 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) -> Result<()> { - let ctx = CommandContext::for_connection(&args.connection)?; +pub(super) async fn info_command( + args: &SystemInfoArgs, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { + let ctx = CommandContext::for_connection(&args.connection, printer)?; let server = ctx.server().await?; let response = server .api() diff --git a/lib/crates/fabro-cli/src/commands/system/mod.rs b/lib/crates/fabro-cli/src/commands/system/mod.rs index ec3c86e15..cb734381d 100644 --- a/lib/crates/fabro-cli/src/commands/system/mod.rs +++ b/lib/crates/fabro-cli/src/commands/system/mod.rs @@ -4,15 +4,20 @@ mod info; mod prune; use anyhow::Result; +use fabro_util::printer::Printer; pub(crate) use prune::parse_duration; use crate::args::{GlobalArgs, SystemCommand, SystemNamespace}; -pub(crate) async fn dispatch(ns: SystemNamespace, globals: &GlobalArgs) -> Result<()> { +pub(crate) async fn dispatch( + ns: SystemNamespace, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { match ns.command { - SystemCommand::Info(args) => info::info_command(&args, globals).await, - SystemCommand::Prune(args) => prune::prune_command(&args, globals).await, - SystemCommand::Df(args) => df::df_command(&args, globals).await, - SystemCommand::Events(args) => events::events_command(&args, globals).await, + 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, } } diff --git a/lib/crates/fabro-cli/src/commands/system/prune.rs b/lib/crates/fabro-cli/src/commands/system/prune.rs index df5c7e1c7..5f405ae7e 100644 --- a/lib/crates/fabro-cli/src/commands/system/prune.rs +++ b/lib/crates/fabro-cli/src/commands/system/prune.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use anyhow::{Context, Result, bail}; use fabro_api::types; +use fabro_util::printer::Printer; use tracing::{debug, info}; use crate::args::{GlobalArgs, RunsPruneArgs}; @@ -9,8 +10,12 @@ 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) -> Result<()> { - let ctx = CommandContext::for_connection(&args.connection)?; +pub(super) async fn prune_command( + args: &RunsPruneArgs, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { + let ctx = CommandContext::for_connection(&args.connection, printer)?; let server = ctx.server().await?; let response = server .api() @@ -27,7 +32,7 @@ pub(super) async fn prune_command(args: &RunsPruneArgs, globals: &GlobalArgs) -> .await .map_err(server_client::map_api_error)? .into_inner(); - prune_from(&response, globals) + prune_from(&response, globals, printer) } pub(crate) fn parse_duration(s: &str) -> Result { @@ -46,7 +51,11 @@ pub(crate) fn parse_duration(s: &str) -> Result { } } -fn prune_from(response: &types::PruneRunsResponse, globals: &GlobalArgs) -> Result<()> { +fn prune_from( + response: &types::PruneRunsResponse, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { let total_count = response.total_count.unwrap_or_default(); let total_size_bytes = response.total_size_bytes.unwrap_or_default(); @@ -63,7 +72,7 @@ fn prune_from(response: &types::PruneRunsResponse, globals: &GlobalArgs) -> Resu } if total_count == 0 { - eprintln!("No matching runs to prune."); + fabro_util::printerr!(printer, "No matching runs to prune."); return Ok(()); } @@ -73,13 +82,15 @@ fn prune_from(response: &types::PruneRunsResponse, globals: &GlobalArgs) -> Resu run_id = run.run_id.as_deref().unwrap_or("-"), "would delete run (dry-run)" ); - println!( + fabro_util::printout!( + printer, "would delete: {} ({})", run.dir_name.as_deref().unwrap_or("-"), run.workflow_name.as_deref().unwrap_or("-") ); } - eprintln!( + fabro_util::printerr!( + printer, "\n{} run(s) would be deleted ({} freed). Pass --yes to confirm.", total_count, format_size(as_u64(total_size_bytes)) @@ -87,7 +98,8 @@ fn prune_from(response: &types::PruneRunsResponse, globals: &GlobalArgs) -> Resu return Ok(()); } - eprintln!( + fabro_util::printerr!( + printer, "{} run(s) deleted ({} freed).", response.deleted_count.unwrap_or(total_count), format_size(as_u64(response.freed_bytes.unwrap_or(total_size_bytes))) diff --git a/lib/crates/fabro-cli/src/commands/uninstall.rs b/lib/crates/fabro-cli/src/commands/uninstall.rs index 17cfd0b13..ffe48cffe 100644 --- a/lib/crates/fabro-cli/src/commands/uninstall.rs +++ b/lib/crates/fabro-cli/src/commands/uninstall.rs @@ -5,6 +5,7 @@ use std::time::Duration; use anyhow::{Context, Result}; use fabro_util::Home; +use fabro_util::printer::Printer; use serde::Serialize; use tracing::warn; @@ -26,7 +27,11 @@ struct Inventory { } #[allow(clippy::unused_async)] // call site requires async -pub(crate) async fn run_uninstall(args: &UninstallArgs, globals: &GlobalArgs) -> Result<()> { +pub(crate) async fn run_uninstall( + args: &UninstallArgs, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { let home = Home::from_env(); let home_root = home.root().to_path_buf(); @@ -34,7 +39,7 @@ pub(crate) async fn run_uninstall(args: &UninstallArgs, globals: &GlobalArgs) -> if globals.json { print_json_pretty(&serde_json::json!({ "status": "not_installed" }))?; } else { - eprintln!("Fabro is not installed."); + fabro_util::printerr!(printer, "Fabro is not installed."); } return Ok(()); } @@ -50,12 +55,12 @@ pub(crate) async fn run_uninstall(args: &UninstallArgs, globals: &GlobalArgs) -> if globals.json { print_json_pretty(&inventory)?; } else { - print_preview(&inventory); + print_preview(&inventory, printer); } return Ok(()); } - execute_uninstall(&inventory, globals.json) + execute_uninstall(&inventory, globals.json, printer) } fn build_inventory(home_root: &Path, storage_dir: &Path) -> Inventory { @@ -135,13 +140,18 @@ fn resolve_binary(home_root: &Path) -> (Option, bool) { (binary_path, is_managed) } -fn print_preview(inventory: &Inventory) { +fn print_preview(inventory: &Inventory, printer: Printer) { let green = console::Style::new().green(); let dim = console::Style::new().dim(); let bold = console::Style::new().bold(); - eprintln!("\n{}", bold.apply_to("The following will be removed:")); - eprintln!( + fabro_util::printerr!( + printer, + "\n{}", + bold.apply_to("The following will be removed:") + ); + fabro_util::printerr!( + printer, " {} {} {}", green.apply_to("~"), tilde_path(&inventory.home_root), @@ -152,7 +162,8 @@ fn print_preview(inventory: &Inventory) { && !inventory.storage_dir.starts_with(&inventory.home_root) { let storage_size = dir_size(&inventory.storage_dir); - eprintln!( + fabro_util::printerr!( + printer, " {} {} {}", green.apply_to("~"), tilde_path(&inventory.storage_dir), @@ -161,29 +172,32 @@ fn print_preview(inventory: &Inventory) { } if inventory.server_running { - eprintln!( + fabro_util::printerr!( + printer, "\n {} A running server will be stopped first.", console::Style::new().yellow().apply_to("!") ); } if !inventory.shell_configs.is_empty() { - eprintln!("\n Shell configs with PATH entries:"); + fabro_util::printerr!(printer, "\n Shell configs with PATH entries:"); for path in &inventory.shell_configs { - eprintln!(" {}", tilde_path(path)); + fabro_util::printerr!(printer, " {}", tilde_path(path)); } } match (&inventory.binary_path, inventory.binary_is_managed) { (Some(_), true) => { - eprintln!( + fabro_util::printerr!( + printer, "\n {} Binary is inside {} and will be removed.", dim.apply_to("i"), tilde_path(&inventory.home_root) ); } (Some(bin), false) => { - eprintln!( + fabro_util::printerr!( + printer, "\n {} Binary at {} is outside {} and must be removed manually.", dim.apply_to("i"), tilde_path(bin), @@ -191,14 +205,15 @@ fn print_preview(inventory: &Inventory) { ); } (None, _) => { - eprintln!( + fabro_util::printerr!( + printer, "\n {} Could not determine binary location.", dim.apply_to("i") ); } } - eprintln!("\nPass --yes to confirm."); + fabro_util::printerr!(printer, "\nPass --yes to confirm."); } #[derive(Debug, Serialize)] @@ -211,7 +226,7 @@ struct UninstallResult { binary_hint: Option, } -fn execute_uninstall(inventory: &Inventory, json: bool) -> Result<()> { +fn execute_uninstall(inventory: &Inventory, json: bool, printer: Printer) -> Result<()> { let green = console::Style::new().green(); let dim = console::Style::new().dim(); let bold = console::Style::new().bold(); @@ -227,13 +242,17 @@ fn execute_uninstall(inventory: &Inventory, json: bool) -> Result<()> { // Unit 3a: Server stop if inventory.server_running { - server::stop::execute(&inventory.storage_dir, Duration::from_secs(5)); + server::stop::execute(&inventory.storage_dir, Duration::from_secs(5), printer); result.server_stopped = true; } // Unit 3b: Safety guardrails if let Err(e) = validate_safe_to_delete(&inventory.home_root) { - eprintln!("Refusing to delete {}: {e}", inventory.home_root.display()); + fabro_util::printerr!( + printer, + "Refusing to delete {}: {e}", + inventory.home_root.display() + ); return Err(e); } @@ -242,7 +261,8 @@ fn execute_uninstall(inventory: &Inventory, json: bool) -> Result<()> { Ok(()) => { result.home_removed = true; if !json { - eprintln!( + fabro_util::printerr!( + printer, " {} Removed {}", green.apply_to("\u{2714}"), tilde_path(&inventory.home_root) @@ -252,7 +272,8 @@ fn execute_uninstall(inventory: &Inventory, json: bool) -> Result<()> { Err(e) if e.kind() == std::io::ErrorKind::NotFound => { result.home_removed = true; if !json { - eprintln!( + fabro_util::printerr!( + printer, " {} {} already removed", dim.apply_to("-"), tilde_path(&inventory.home_root) @@ -261,7 +282,8 @@ fn execute_uninstall(inventory: &Inventory, json: bool) -> Result<()> { } Err(e) => { if !json { - eprintln!( + fabro_util::printerr!( + printer, " Failed to remove {}: {e}", tilde_path(&inventory.home_root) ); @@ -274,7 +296,8 @@ fn execute_uninstall(inventory: &Inventory, json: bool) -> Result<()> { if !inventory.storage_dir.starts_with(&inventory.home_root) && inventory.storage_dir.exists() { if let Err(e) = validate_safe_to_delete(&inventory.storage_dir) { if !json { - eprintln!( + fabro_util::printerr!( + printer, "Refusing to delete storage dir {}: {e}", inventory.storage_dir.display() ); @@ -284,7 +307,8 @@ fn execute_uninstall(inventory: &Inventory, json: bool) -> Result<()> { match fs::remove_dir_all(&inventory.storage_dir) { Ok(()) => { if !json { - eprintln!( + fabro_util::printerr!( + printer, " {} Removed {}", green.apply_to("\u{2714}"), tilde_path(&inventory.storage_dir) @@ -294,7 +318,8 @@ fn execute_uninstall(inventory: &Inventory, json: bool) -> Result<()> { Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} Err(e) => { if !json { - eprintln!( + fabro_util::printerr!( + printer, " Failed to remove {}: {e}", tilde_path(&inventory.storage_dir) ); @@ -311,7 +336,8 @@ fn execute_uninstall(inventory: &Inventory, json: bool) -> Result<()> { Ok(()) => { result.shell_configs_cleaned.push(path.clone()); if !json { - eprintln!( + fabro_util::printerr!( + printer, " {} Cleaned {}", green.apply_to("\u{2714}"), tilde_path(path) @@ -321,7 +347,8 @@ fn execute_uninstall(inventory: &Inventory, json: bool) -> Result<()> { Err(e) => { warn!("Failed to clean shell config {}: {e}", path.display()); if !json { - eprintln!( + fabro_util::printerr!( + printer, " {} Could not clean {}: {e}", console::Style::new().yellow().apply_to("!"), tilde_path(path) @@ -336,7 +363,8 @@ fn execute_uninstall(inventory: &Inventory, json: bool) -> Result<()> { (Some(_), true) => { result.binary_removed = true; if !json { - eprintln!( + fabro_util::printerr!( + printer, " {} Binary removed {}", green.apply_to("\u{2714}"), dim.apply_to("(was inside ~/.fabro/bin/)") @@ -347,7 +375,7 @@ fn execute_uninstall(inventory: &Inventory, json: bool) -> Result<()> { let hint = binary_removal_hint(bin); result.binary_hint = Some(hint.clone()); if !json { - eprintln!("\n {} {}", dim.apply_to("i"), hint); + fabro_util::printerr!(printer, "\n {} {}", dim.apply_to("i"), hint); } } (None, _) => { @@ -363,7 +391,11 @@ fn execute_uninstall(inventory: &Inventory, json: bool) -> Result<()> { if json { print_json_pretty(&result)?; } else { - eprintln!("\n{}", bold.apply_to("Fabro has been uninstalled.")); + fabro_util::printerr!( + printer, + "\n{}", + bold.apply_to("Fabro has been uninstalled.") + ); } if critical_failure { diff --git a/lib/crates/fabro-cli/src/commands/upgrade.rs b/lib/crates/fabro-cli/src/commands/upgrade.rs index ee88accd8..451bc1fd0 100644 --- a/lib/crates/fabro-cli/src/commands/upgrade.rs +++ b/lib/crates/fabro-cli/src/commands/upgrade.rs @@ -3,6 +3,7 @@ use std::io::{IsTerminal, Write}; use std::path::{Path, PathBuf}; use anyhow::{Context, Result, bail}; +use fabro_util::printer::Printer; use semver::Version; use sha2::{Digest, Sha256}; use tokio::process::Command as TokioCommand; @@ -223,7 +224,11 @@ impl UpgradeCheckState { // ── Main upgrade command ─────────────────────────────────────────────────── -pub(crate) async fn run_upgrade(args: UpgradeArgs, globals: &GlobalArgs) -> Result<()> { +pub(crate) async fn run_upgrade( + args: UpgradeArgs, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { let backend = select_backend().await; let current = @@ -249,7 +254,7 @@ pub(crate) async fn run_upgrade(args: UpgradeArgs, globals: &GlobalArgs) -> Resu ); } // Explicit --version: warn + prompt - eprintln!("Warning: downgrading from {current} to {target}"); + fabro_util::printerr!(printer, "Warning: downgrading from {current} to {target}"); if std::io::stdin().is_terminal() { let confirm = dialoguer::Confirm::new() .with_prompt("Continue with downgrade?") @@ -269,7 +274,7 @@ pub(crate) async fn run_upgrade(args: UpgradeArgs, globals: &GlobalArgs) -> Resu "installed_version": current.to_string(), }))?; } else { - eprintln!("Already on version {current}"); + fabro_util::printerr!(printer, "Already on version {current}"); } return Ok(()); } @@ -284,9 +289,9 @@ pub(crate) async fn run_upgrade(args: UpgradeArgs, globals: &GlobalArgs) -> Resu "dry_run": true, }))?; } else { - eprintln!("Would upgrade fabro from {current} to {target}"); - eprintln!(" tag: {tag}"); - eprintln!(" target: {}", detect_target()?); + fabro_util::printerr!(printer, "Would upgrade fabro from {current} to {target}"); + fabro_util::printerr!(printer, " tag: {tag}"); + fabro_util::printerr!(printer, " target: {}", detect_target()?); } return Ok(()); } @@ -305,7 +310,7 @@ pub(crate) async fn run_upgrade(args: UpgradeArgs, globals: &GlobalArgs) -> Resu .context("failed to create temp directory")?; // Download tarball and checksum in parallel - eprintln!("Downloading fabro {target}..."); + fabro_util::printerr!(printer, "Downloading fabro {target}..."); let (tarball_path, checksum_path) = tokio::try_join!( backend.download_release(&tag, &tarball_name, tmp_dir.path()), backend.download_release(&tag, &checksum_name, tmp_dir.path()), @@ -358,7 +363,7 @@ pub(crate) async fn run_upgrade(args: UpgradeArgs, globals: &GlobalArgs) -> Resu "installed_version": target.to_string(), }))?; } else { - eprintln!("Upgraded fabro to {target}"); + fabro_util::printerr!(printer, "Upgraded fabro to {target}"); } Ok(()) } @@ -371,18 +376,19 @@ pub(crate) async fn run_upgrade(args: UpgradeArgs, globals: &GlobalArgs) -> Resu pub(crate) fn spawn_upgrade_check( no_upgrade_check: bool, upgrade_check_enabled: bool, + printer: Printer, ) -> Option> { if no_upgrade_check || !upgrade_check_enabled { return None; } - Some(tokio::spawn(async { - if let Err(e) = check_and_print_notice().await { + Some(tokio::spawn(async move { + if let Err(e) = check_and_print_notice(printer).await { debug!(%e, "Upgrade check failed (silently swallowed)"); } })) } -async fn check_and_print_notice() -> Result<()> { +async fn check_and_print_notice(printer: Printer) -> Result<()> { let state_path = fabro_util::Home::from_env().root().join(LAST_CHECK_FILE); let current = Version::parse(env!("CARGO_PKG_VERSION"))?; @@ -392,7 +398,7 @@ async fn check_and_print_notice() -> Result<()> { if !state.is_stale() { if let Ok(latest) = Version::parse(&state.latest_version) { if latest > current { - print_notice(¤t, &latest); + print_notice(¤t, &latest, printer); } } return Ok(()); @@ -416,15 +422,18 @@ async fn check_and_print_notice() -> Result<()> { let _ = state.save(&state_path); if latest > current { - print_notice(¤t, &latest); + print_notice(¤t, &latest, printer); } Ok(()) } -fn print_notice(current: &Version, latest: &Version) { - eprintln!("A new version of fabro is available: {latest} (current: {current})"); - eprintln!("Run `fabro upgrade` to update."); +fn print_notice(current: &Version, latest: &Version, printer: Printer) { + fabro_util::printerr!( + printer, + "A new version of fabro is available: {latest} (current: {current})" + ); + fabro_util::printerr!(printer, "Run `fabro upgrade` to update."); } // ── Tests ────────────────────────────────────────────────────────────────── diff --git a/lib/crates/fabro-cli/src/commands/validate.rs b/lib/crates/fabro-cli/src/commands/validate.rs index f8a86fbaa..7de1102c5 100644 --- a/lib/crates/fabro-cli/src/commands/validate.rs +++ b/lib/crates/fabro-cli/src/commands/validate.rs @@ -2,6 +2,7 @@ use anyhow::bail; use fabro_config::load::load_settings_user; use fabro_config::user::active_settings_path; use fabro_types::settings::SettingsLayer; +use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use crate::args::{GlobalArgs, ValidateArgs}; @@ -14,8 +15,9 @@ pub(crate) async fn run( args: &ValidateArgs, styles: &Styles, globals: &GlobalArgs, + printer: Printer, ) -> anyhow::Result<()> { - let ctx = CommandContext::for_target(&args.target)?; + let ctx = CommandContext::for_target(&args.target, printer)?; let built = build_run_manifest(ManifestBuildInput { workflow: args.workflow.clone(), cwd: ctx.cwd().to_path_buf(), @@ -47,7 +49,8 @@ pub(crate) async fn run( return Ok(()); } - eprintln!( + fabro_util::printerr!( + printer, "{} ({} nodes, {} edges)", styles .bold @@ -55,13 +58,14 @@ pub(crate) async fn run( response.workflow.nodes, response.workflow.edges, ); - eprintln!( + fabro_util::printerr!( + printer, "{} {}", styles.dim.apply_to("Graph:"), styles.dim.apply_to(relative_path(&built.target_path)), ); - print_diagnostics(&diagnostics, styles); + print_diagnostics(&diagnostics, styles, printer); if diagnostics .iter() @@ -70,6 +74,6 @@ pub(crate) async fn run( bail!("Validation failed"); } - eprintln!("Validation: {}", styles.green.apply_to("OK")); + fabro_util::printerr!(printer, "Validation: {}", styles.green.apply_to("OK")); Ok(()) } diff --git a/lib/crates/fabro-cli/src/commands/workflow/create.rs b/lib/crates/fabro-cli/src/commands/workflow/create.rs index d6b80d058..ee3437f2b 100644 --- a/lib/crates/fabro-cli/src/commands/workflow/create.rs +++ b/lib/crates/fabro-cli/src/commands/workflow/create.rs @@ -2,11 +2,16 @@ use std::path::Path; use anyhow::{Context, Result, bail}; use fabro_config::project::{discover_project_config, resolve_fabro_root}; +use fabro_util::printer::Printer; use crate::args::{GlobalArgs, WorkflowCreateArgs}; use crate::shared::{print_json_pretty, relative_path}; -pub(super) fn create_command(args: &WorkflowCreateArgs, globals: &GlobalArgs) -> Result<()> { +pub(super) fn create_command( + args: &WorkflowCreateArgs, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { let cwd = std::env::current_dir()?; let Some((config_path, config)) = discover_project_config(&cwd)? else { @@ -35,27 +40,36 @@ pub(super) fn create_command(args: &WorkflowCreateArgs, globals: &GlobalArgs) -> let dim = console::Style::new().dim(); let rel_dir = relative_path(&workflows_dir); - eprintln!( + fabro_util::printerr!( + printer, " {} {}", green.apply_to("✔"), dim.apply_to(format!("{rel_dir}/workflow.fabro")) ); - eprintln!( + fabro_util::printerr!( + printer, " {} {}", green.apply_to("✔"), dim.apply_to(format!("{rel_dir}/workflow.toml")) ); - eprintln!("\n{} Next steps:\n", bold.apply_to("Workflow created!")); - eprintln!( + fabro_util::printerr!( + printer, + "\n{} Next steps:\n", + bold.apply_to("Workflow created!") + ); + fabro_util::printerr!( + printer, " 1. Edit the graph: {}", cyan_bold.apply_to(format!("{rel_dir}/workflow.fabro")) ); - eprintln!( + fabro_util::printerr!( + printer, " 2. Validate: {}", cyan_bold.apply_to(format!("fabro validate {}", args.name)) ); - eprintln!( + fabro_util::printerr!( + printer, " 3. Run: {}", cyan_bold.apply_to(format!("fabro run {}", 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 79cabbe89..5603200cf 100644 --- a/lib/crates/fabro-cli/src/commands/workflow/list.rs +++ b/lib/crates/fabro-cli/src/commands/workflow/list.rs @@ -3,6 +3,7 @@ use fabro_config::project::{ WorkflowInfo, WorkflowSource, discover_project_config, list_workflows_detailed, resolve_fabro_root, }; +use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use crate::args::{GlobalArgs, WorkflowListArgs}; @@ -10,7 +11,11 @@ use crate::shared::{print_json_pretty, relative_path}; const GOAL_MAX_LEN: usize = 60; -pub(super) fn list_command(_args: &WorkflowListArgs, globals: &GlobalArgs) -> Result<()> { +pub(super) fn list_command( + _args: &WorkflowListArgs, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { let styles = Styles::detect_stderr(); let cwd = std::env::current_dir()?; @@ -43,7 +48,8 @@ pub(super) fn list_command(_args: &WorkflowListArgs, globals: &GlobalArgs) -> Re let name_width = workflows.iter().map(|w| w.name.len()).max().unwrap_or(0); - eprintln!( + fabro_util::printerr!( + printer, "{} workflow(s) found\n", styles.bold.apply_to(workflows.len()) ); @@ -51,9 +57,16 @@ pub(super) fn list_command(_args: &WorkflowListArgs, globals: &GlobalArgs) -> Re let user_path = user_wf_dir .as_deref() .map_or_else(|| "~/.fabro/workflows".to_string(), relative_path); - print_section("User Workflows", &user_path, &user, name_width, &styles); + print_section( + "User Workflows", + &user_path, + &user, + name_width, + &styles, + printer, + ); - eprintln!(); + fabro_util::printerr!(printer, ""); print_section( "Project Workflows", @@ -61,6 +74,7 @@ pub(super) fn list_command(_args: &WorkflowListArgs, globals: &GlobalArgs) -> Re &project, name_width, &styles, + printer, ); Ok(()) @@ -72,18 +86,21 @@ fn print_section( workflows: &[&WorkflowInfo], name_width: usize, styles: &Styles, + printer: Printer, ) { - eprintln!( + fabro_util::printerr!( + printer, "{} {}", styles.bold.apply_to(title), styles.dim.apply_to(format!("({path})")), ); if workflows.is_empty() { - eprintln!(" {}", styles.dim.apply_to("(none)")); + fabro_util::printerr!(printer, " {}", styles.dim.apply_to("(none)")); return; } - eprintln!(); - eprintln!( + fabro_util::printerr!(printer, ""); + fabro_util::printerr!( + printer, " {: Result<()> { +pub(crate) fn dispatch( + ns: WorkflowNamespace, + globals: &GlobalArgs, + printer: Printer, +) -> Result<()> { match ns.command { - WorkflowCommand::List(args) => list::list_command(&args, globals), - WorkflowCommand::Create(args) => create::create_command(&args, globals), + WorkflowCommand::List(args) => list::list_command(&args, globals, printer), + WorkflowCommand::Create(args) => create::create_command(&args, globals, printer), } } diff --git a/lib/crates/fabro-cli/src/main.rs b/lib/crates/fabro-cli/src/main.rs index 36c9eee99..768ba0ce4 100644 --- a/lib/crates/fabro-cli/src/main.rs +++ b/lib/crates/fabro-cli/src/main.rs @@ -1,4 +1,4 @@ -#![allow(clippy::print_stdout, clippy::print_stderr, clippy::exit)] +#![allow(clippy::exit)] mod args; mod command_context; @@ -53,6 +53,7 @@ impl Cli { } } +#[expect(clippy::print_stderr, reason = "fatal error reporting before exit")] #[tokio::main] async fn main() { tel_panic::install_panic_hook(); @@ -119,7 +120,7 @@ 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 printer = Printer::from_flags(globals.quiet, globals.verbose); let command_name = command.name().to_string(); let (config_log_level, upgrade_check_enabled) = { @@ -165,7 +166,7 @@ async fn main_inner() -> (String, Result<()>) { }; if let Err(err) = logging::init_tracing(globals.debug, config_log_level.as_deref(), log_prefix) { - eprintln!("Warning: failed to initialize logging: {err:#}"); + fabro_util::printerr!(printer, "Warning: failed to initialize logging: {err:#}"); } debug!(command = %command_name, "CLI command started"); @@ -177,33 +178,43 @@ async fn main_inner() -> (String, Result<()>) { | Commands::Repo(_) | Commands::Install(_) ) { - commands::upgrade::spawn_upgrade_check(globals.no_upgrade_check, upgrade_check_enabled) + commands::upgrade::spawn_upgrade_check( + globals.no_upgrade_check, + upgrade_check_enabled, + printer, + ) } else { None }; let result = Box::pin(async move { match *command { - Commands::Exec(args) => commands::exec::execute(args, &globals).await?, - Commands::RunCmd(cmd) => Box::pin(commands::run::dispatch(cmd, &globals)).await?, - Commands::Preflight(args) => commands::preflight::execute(args, &globals).await?, + Commands::Exec(args) => commands::exec::execute(args, &globals, printer).await?, + Commands::RunCmd(cmd) => { + Box::pin(commands::run::dispatch(cmd, &globals, printer)).await?; + } + Commands::Preflight(args) => { + commands::preflight::execute(args, &globals, printer).await?; + } Commands::Validate(args) => { let styles = Styles::detect_stderr(); - commands::validate::run(&args, &styles, &globals).await?; + commands::validate::run(&args, &styles, &globals, printer).await?; } Commands::Graph(args) => { let styles = Styles::detect_stderr(); - commands::graph::run(&args, &styles, &globals).await?; + commands::graph::run(&args, &styles, &globals, printer).await?; } Commands::Parse(args) => { - commands::parse::run(&args, &globals)?; + commands::parse::run(&args, &globals, printer)?; + } + 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::Artifact(ns) => commands::artifact::dispatch(ns, &globals).await?, - Commands::Store(ns) => commands::store::dispatch(ns, &globals).await?, - Commands::RunsCmd(cmd) => commands::runs::dispatch(cmd, &globals).await?, - Commands::Model { command } => commands::model::execute(command, &globals).await?, Commands::Server(ns) => { - Box::pin(commands::server::dispatch(ns.command, &globals)).await?; + Box::pin(commands::server::dispatch(ns.command, &globals, printer)).await?; } Commands::Doctor(args) => { let cli_settings = user_config::load_settings()?; @@ -212,7 +223,8 @@ async fn main_inner() -> (String, Result<()>) { .output .verbosity == OutputVerbosity::Verbose; - let exit_code = commands::doctor::run_doctor(&args, verbose, &globals).await?; + let exit_code = + commands::doctor::run_doctor(&args, verbose, &globals, printer).await?; std::process::exit(exit_code); } Commands::Discord => { @@ -233,25 +245,27 @@ async fn main_inner() -> (String, Result<()>) { open::that("https://docs.fabro.sh/")?; } } - Commands::Repo(ns) => commands::repo::dispatch(ns, &globals).await?, + Commands::Repo(ns) => commands::repo::dispatch(ns, &globals, printer).await?, Commands::Install(args) => { - commands::install::run_install(&args, &globals).await?; + commands::install::run_install(&args, &globals, printer).await?; } Commands::Uninstall(args) => { - commands::uninstall::run_uninstall(&args, &globals).await?; + commands::uninstall::run_uninstall(&args, &globals, printer).await?; } - Commands::Pr(ns) => Box::pin(commands::pr::dispatch(ns, &globals)).await?, - Commands::Secret(ns) => commands::secret::dispatch(ns, &globals).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)).await?; + Box::pin(commands::config::execute(&args, &globals, printer)).await?; } - Commands::Workflow(ns) => commands::workflow::dispatch(ns, &globals)?, + Commands::Workflow(ns) => commands::workflow::dispatch(ns, &globals, printer)?, Commands::Upgrade(args) => { - commands::upgrade::run_upgrade(args, &globals).await?; + commands::upgrade::run_upgrade(args, &globals, printer).await?; } - Commands::Provider(ns) => commands::provider::dispatch(ns, &globals).await?, - Commands::Sandbox { command } => commands::sandbox::dispatch(command, &globals).await?, - Commands::System(ns) => commands::system::dispatch(ns, &globals).await?, + Commands::Provider(ns) => commands::provider::dispatch(ns, &globals, printer).await?, + Commands::Sandbox { command } => { + commands::sandbox::dispatch(command, &globals, printer).await?; + } + Commands::System(ns) => commands::system::dispatch(ns, &globals, printer).await?, Commands::Completion(args) => { globals.require_no_json()?; let mut cmd = Cli::command(); @@ -288,7 +302,7 @@ async fn main_inner() -> (String, Result<()>) { Commands::TestPanic { message } => { let event = tel_panic::build_event(&message); let json = serde_json::to_string_pretty(&event)?; - println!("{json}"); + fabro_util::printout!(printer, "{json}"); } } diff --git a/lib/crates/fabro-cli/src/shared/provider_auth.rs b/lib/crates/fabro-cli/src/shared/provider_auth.rs index 50815b28b..524ae2cff 100644 --- a/lib/crates/fabro-cli/src/shared/provider_auth.rs +++ b/lib/crates/fabro-cli/src/shared/provider_auth.rs @@ -7,6 +7,7 @@ use dialoguer::{Confirm, Password}; use fabro_llm::client::Client as LlmClient; use fabro_llm::generate::{GenerateParams, generate}; use fabro_model::{Catalog, Provider}; +use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use tokio::task::spawn_blocking; use tokio::time::timeout; @@ -74,8 +75,12 @@ pub(crate) fn openai_oauth_env_pairs( /// Run the OpenAI OAuth browser flow, falling back to manual API key entry on /// failure. Returns the env-var pairs to persist. -pub(crate) async fn run_openai_oauth_or_api_key(s: &Styles) -> Result> { - eprintln!( +pub(crate) async fn run_openai_oauth_or_api_key( + s: &Styles, + printer: Printer, +) -> Result> { + fabro_util::printerr!( + printer, " {}", s.dim.apply_to("Opening browser for OpenAI login...") ); @@ -100,7 +105,8 @@ pub(crate) async fn run_openai_oauth_or_api_key(s: &Styles) -> Result Result { tracing::warn!(error = %e, "OpenAI OAuth browser flow failed"); - eprintln!(" Browser login failed: {e}"); - eprintln!( + fabro_util::printerr!(printer, " Browser login failed: {e}"); + fabro_util::printerr!( + printer, " {}", s.dim.apply_to("Falling back to manual API key entry.") ); - let (env_var, key) = prompt_and_validate_key(Provider::OpenAi, s).await?; + let (env_var, key) = prompt_and_validate_key(Provider::OpenAi, s, printer).await?; Ok(vec![(env_var, key)]) } } @@ -173,10 +180,12 @@ pub(crate) async fn validate_api_key(provider: Provider, api_key: &str) -> Resul pub(crate) async fn prompt_and_validate_key( provider: Provider, s: &Styles, + printer: Printer, ) -> Result<(String, String)> { let env_var = provider.api_key_env_vars()[0]; let url = provider_key_url(provider); - eprintln!( + fabro_util::printerr!( + printer, " {}", s.dim.apply_to(format!("Get your API key at: {url}")) ); @@ -185,14 +194,14 @@ pub(crate) async fn prompt_and_validate_key( let prompt = env_var.to_string(); let key: String = spawn_blocking(move || prompt_password(&prompt)).await??; - eprintln!(" {}", s.dim.apply_to("Validating API key...")); + fabro_util::printerr!(printer, " {}", s.dim.apply_to("Validating API key...")); match validate_api_key(provider, &key).await { Ok(()) => { - eprintln!(" {} API key is valid", s.green.apply_to("✔")); + fabro_util::printerr!(printer, " {} API key is valid", s.green.apply_to("✔")); return Ok((env_var.to_string(), key)); } Err(e) => { - eprintln!(" [error] API key validation failed: {e}"); + fabro_util::printerr!(printer, " [error] API key validation failed: {e}"); let retry = spawn_blocking(|| prompt_confirm("Try again with a different key?", true)) .await??; diff --git a/lib/crates/fabro-cli/src/shared/utilities.rs b/lib/crates/fabro-cli/src/shared/utilities.rs index 8936a4756..e2e2e77d6 100644 --- a/lib/crates/fabro-cli/src/shared/utilities.rs +++ b/lib/crates/fabro-cli/src/shared/utilities.rs @@ -3,6 +3,7 @@ use std::path::{Path, PathBuf}; use std::time::Duration; use cli_table::Color; +use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use fabro_validate::{Diagnostic, Severity}; use serde::Serialize; @@ -23,7 +24,7 @@ where Ok(()) } -pub(crate) fn print_diagnostics(diagnostics: &[Diagnostic], styles: &Styles) { +pub(crate) fn print_diagnostics(diagnostics: &[Diagnostic], styles: &Styles, printer: Printer) { for d in diagnostics { let location = match (&d.node_id, &d.edge) { (Some(node), _) => format!(" [node: {node}]"), @@ -31,19 +32,22 @@ pub(crate) fn print_diagnostics(diagnostics: &[Diagnostic], styles: &Styles) { _ => String::new(), }; match d.severity { - Severity::Error => eprintln!( + Severity::Error => fabro_util::printerr!( + printer, "{}{location}: {} ({})", styles.red.apply_to("error"), d.message, styles.dim.apply_to(&d.rule), ), - Severity::Warning => eprintln!( + Severity::Warning => fabro_util::printerr!( + printer, "{}{location}: {} ({})", styles.yellow.apply_to("warning"), d.message, styles.dim.apply_to(&d.rule), ), - Severity::Info => eprintln!( + Severity::Info => fabro_util::printerr!( + printer, "{}", styles .dim diff --git a/lib/crates/fabro-util/src/printer.rs b/lib/crates/fabro-util/src/printer.rs index 1390a073f..fda1e3f9a 100644 --- a/lib/crates/fabro-util/src/printer.rs +++ b/lib/crates/fabro-util/src/printer.rs @@ -1,3 +1,31 @@ +/// Like `println!`, but respects the `Printer`'s verbosity level. +/// +/// ```ignore +/// printout!(printer, "Created workflow: {name}"); +/// ``` +#[macro_export] +macro_rules! printout { + ($printer:expr, $($arg:tt)*) => {{ + use ::std::fmt::Write as _; + let mut out = $printer.stdout_important(); + let _ = writeln!(out, $($arg)*); + }}; +} + +/// Like `eprintln!`, but respects the `Printer`'s verbosity level. +/// +/// ```ignore +/// printerr!(printer, "Connecting to {url}…"); +/// ``` +#[macro_export] +macro_rules! printerr { + ($printer:expr, $($arg:tt)*) => {{ + use ::std::fmt::Write as _; + let mut out = $printer.stderr(); + let _ = writeln!(out, $($arg)*); + }}; +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Printer { /// Suppresses all output.