From 4b30a5f16e3111539491e8aadacf53da98d004c1 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sat, 11 Apr 2026 14:40:33 -0400 Subject: [PATCH] refactor(cli): route command output through Printer Plumb the CLI printer through command dispatch, replace direct stdout and stderr writes with printer helpers, preserve important stdout in quiet mode, and update the Claude Rust formatting hook to use cargo +nightly fmt. --- .claude/settings.json | 2 +- lib/crates/fabro-cli/src/command_context.rs | 23 ++- .../fabro-cli/src/commands/artifact/cp.rs | 18 +- .../fabro-cli/src/commands/artifact/list.rs | 40 +++-- .../fabro-cli/src/commands/artifact/mod.rs | 14 +- .../fabro-cli/src/commands/config/mod.rs | 15 +- lib/crates/fabro-cli/src/commands/doctor.rs | 26 +-- lib/crates/fabro-cli/src/commands/exec.rs | 7 +- lib/crates/fabro-cli/src/commands/graph.rs | 6 +- lib/crates/fabro-cli/src/commands/install.rs | 166 +++++++++++------- lib/crates/fabro-cli/src/commands/model.rs | 9 +- lib/crates/fabro-cli/src/commands/parse.rs | 3 +- lib/crates/fabro-cli/src/commands/pr/close.rs | 6 +- .../fabro-cli/src/commands/pr/create.rs | 8 +- lib/crates/fabro-cli/src/commands/pr/list.rs | 28 ++- lib/crates/fabro-cli/src/commands/pr/merge.rs | 6 +- lib/crates/fabro-cli/src/commands/pr/mod.rs | 22 ++- lib/crates/fabro-cli/src/commands/pr/view.rs | 29 +-- .../fabro-cli/src/commands/preflight.rs | 25 ++- .../fabro-cli/src/commands/provider/login.rs | 19 +- .../fabro-cli/src/commands/provider/mod.rs | 9 +- .../fabro-cli/src/commands/repo/deinit.rs | 9 +- .../fabro-cli/src/commands/repo/init.rs | 68 ++++--- lib/crates/fabro-cli/src/commands/repo/mod.rs | 11 +- .../fabro-cli/src/commands/run/attach.rs | 36 +++- .../fabro-cli/src/commands/run/command.rs | 18 +- lib/crates/fabro-cli/src/commands/run/cp.rs | 12 +- .../fabro-cli/src/commands/run/create.rs | 9 +- lib/crates/fabro-cli/src/commands/run/diff.rs | 5 +- lib/crates/fabro-cli/src/commands/run/fork.rs | 18 +- lib/crates/fabro-cli/src/commands/run/logs.rs | 10 +- lib/crates/fabro-cli/src/commands/run/mod.rs | 35 ++-- .../fabro-cli/src/commands/run/output.rs | 88 +++++++--- .../fabro-cli/src/commands/run/preview.rs | 19 +- .../fabro-cli/src/commands/run/resume.rs | 17 +- .../fabro-cli/src/commands/run/rewind.rs | 19 +- lib/crates/fabro-cli/src/commands/run/ssh.rs | 10 +- lib/crates/fabro-cli/src/commands/run/wait.rs | 32 +++- .../fabro-cli/src/commands/runs/inspect.rs | 7 +- .../fabro-cli/src/commands/runs/list.rs | 13 +- lib/crates/fabro-cli/src/commands/runs/mod.rs | 13 +- lib/crates/fabro-cli/src/commands/runs/rm.rs | 20 ++- .../fabro-cli/src/commands/sandbox/mod.rs | 13 +- .../fabro-cli/src/commands/secret/list.rs | 4 +- .../fabro-cli/src/commands/secret/mod.rs | 17 +- .../fabro-cli/src/commands/secret/rm.rs | 4 +- .../fabro-cli/src/commands/secret/set.rs | 4 +- .../src/commands/server/foreground.rs | 3 + .../fabro-cli/src/commands/server/mod.rs | 13 +- .../fabro-cli/src/commands/server/start.rs | 33 +++- .../fabro-cli/src/commands/server/status.rs | 16 +- .../fabro-cli/src/commands/server/stop.rs | 7 +- .../fabro-cli/src/commands/store/dump.rs | 10 +- .../fabro-cli/src/commands/store/mod.rs | 9 +- .../fabro-cli/src/commands/system/df.rs | 9 +- .../fabro-cli/src/commands/system/events.rs | 9 +- .../fabro-cli/src/commands/system/info.rs | 9 +- .../fabro-cli/src/commands/system/mod.rs | 15 +- .../fabro-cli/src/commands/system/prune.rs | 28 ++- .../fabro-cli/src/commands/uninstall.rs | 90 +++++++--- lib/crates/fabro-cli/src/commands/upgrade.rs | 41 +++-- lib/crates/fabro-cli/src/commands/validate.rs | 14 +- .../fabro-cli/src/commands/workflow/create.rs | 28 ++- .../fabro-cli/src/commands/workflow/list.rs | 36 +++- .../fabro-cli/src/commands/workflow/mod.rs | 11 +- lib/crates/fabro-cli/src/main.rs | 70 +++++--- .../fabro-cli/src/shared/provider_auth.rs | 29 +-- lib/crates/fabro-cli/src/shared/utilities.rs | 12 +- lib/crates/fabro-util/src/printer.rs | 28 +++ 69 files changed, 1057 insertions(+), 455 deletions(-) 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.