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.
This commit is contained in:
Bryan Helmkamp 2026-04-11 14:40:33 -04:00
parent 6a87f0a071
commit 4b30a5f16e
69 changed files with 1057 additions and 455 deletions

View file

@ -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"
}
]
}

View file

@ -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> {
Self::new(ServerMode::None)
pub(crate) fn base(printer: Printer) -> Result<Self> {
Self::new(printer, ServerMode::None)
}
pub(crate) fn for_target(args: &ServerTargetArgs) -> Result<Self> {
Self::new(ServerMode::ByTarget {
pub(crate) fn for_target(args: &ServerTargetArgs, printer: Printer) -> Result<Self> {
Self::new(printer, ServerMode::ByTarget {
target_override: args.server.clone(),
})
}
pub(crate) fn for_connection(args: &ServerConnectionArgs) -> Result<Self> {
Self::new(ServerMode::ByStorageDir {
pub(crate) fn for_connection(args: &ServerConnectionArgs, printer: Printer) -> Result<Self> {
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<Self> {
fn new(printer: Printer, server_mode: ServerMode) -> Result<Self> {
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
}

View file

@ -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()

View file

@ -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!("{:<node_width$} {:>retry_width$} PATH", "NODE", "RETRY");
fabro_util::printout!(
printer,
"{:<node_width$} {:>retry_width$} PATH",
"NODE",
"RETRY"
);
for entry in &entries {
println!(
fabro_util::printout!(
printer,
"{:<node_width$} {:>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(())
}

View file

@ -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<u32>,
printer: Printer,
) -> Result<(RunId, ServerStoreClient, Vec<ArtifactEntry>)> {
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,
}
}

View file

@ -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<SettingsLayer> {
let base_ctx = CommandContext::base()?;
async fn merged_config(args: &SettingsArgs, printer: Printer) -> anyhow::Result<SettingsLayer> {
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<SettingsLayer> {
)?);
}
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<SettingsLayer> {
)?)
}
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(());

View file

@ -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<i32, anyhow::Error> {
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()))

View file

@ -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;

View file

@ -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)

View file

@ -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<Vec<(String, String)>> {
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<Provider> = 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")
);

View file

@ -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<ModelsCommand>, globals: &GlobalArgs) -> Result<()> {
pub(crate) async fn execute(
command: Option<ModelsCommand>,
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

View file

@ -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())

View file

@ -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<fabro_github::GitHubAppCredentials>,
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(())

View file

@ -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<fabro_github::GitHubAppCredentials>,
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).");
}
}
}

View file

@ -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<fabro_github::GitHubAppCredentials>,
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::<PrRow>::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
);
}

View file

@ -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<fabro_github::GitHubAppCredentials>,
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(())

View file

@ -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();

View file

@ -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<fabro_github::GitHubAppCredentials>,
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}");
}
}

View file

@ -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

View file

@ -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(())
}

View file

@ -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,
}
}

View file

@ -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<Vec<String>> {
pub(crate) fn run_deinit(globals: &GlobalArgs, printer: Printer) -> Result<Vec<String>> {
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<Vec<String>> {
.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<Vec<String>> {
}
if !globals.json {
eprintln!(
fabro_util::printerr!(
printer,
"\n{}",
console::Style::new()
.bold()

View file

@ -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<PathBuf> {
))
}
pub(crate) async fn run_init(args: &RepoInitArgs, globals: &GlobalArgs) -> Result<Vec<String>> {
pub(crate) async fn run_init(
args: &RepoInitArgs,
globals: &GlobalArgs,
printer: Printer,
) -> Result<Vec<String>> {
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 <url>` 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}"
);
}
}
}

View file

@ -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 }))?;
}

View file

@ -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<ExitCode> {
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<ExitCode> {
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<Option<ExitCode>> {
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();

View file

@ -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?;
}

View file

@ -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<CopyDirection> {
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()))

View file

@ -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<CreatedRun> {
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,
);
}
}

View file

@ -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();

View file

@ -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())]
);

View file

@ -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();

View file

@ -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
}
}
}

View file

@ -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::<Vec<_>>();
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!("{:<node_width$} {:>retry_width$} PATH", "NODE", "RETRY");
fabro_util::printerr!(printer, "\n{}", styles.bold.apply_to("=== Artifacts ==="));
fabro_util::printerr!(
printer,
"{:<node_width$} {:>retry_width$} PATH",
"NODE",
"RETRY"
);
for (node_slug, retry, relative_path) in &entries {
eprintln!("{node_slug:<node_width$} {retry:>retry_width$} {relative_path}");
fabro_util::printerr!(
printer,
"{node_slug:<node_width$} {retry:>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}:<path> <dest> --node <node_slug> --retry <retry>"

View file

@ -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 {

View file

@ -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);

View file

@ -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<String>,
}
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<String>, 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;
}

View file

@ -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)?;

View file

@ -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]

View file

@ -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<serde_json::Value>,
}
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(())
}

View file

@ -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(())
}

View file

@ -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,
}
}

View file

@ -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));
}
}

View file

@ -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,
}
}

View file

@ -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(())
}

View file

@ -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,
}
}

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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<PathBuf>,
styles: &'static Styles,
printer: Printer,
) -> Result<()> {
let _ = printer;
serve_args.bind = Some(bind.to_string());
let _record_guard = scopeguard::guard(record_path.clone(), |path| {

View file

@ -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
}

View file

@ -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."
);
}

View file

@ -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
);
}

View file

@ -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");
}

View file

@ -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()

View file

@ -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,
}
}

View file

@ -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()

View file

@ -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();

View file

@ -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()

View file

@ -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,
}
}

View file

@ -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<chrono::Duration> {
@ -46,7 +51,11 @@ pub(crate) fn parse_duration(s: &str) -> Result<chrono::Duration> {
}
}
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)))

View file

@ -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<PathBuf>, 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<String>,
}
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 {

View file

@ -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<JoinHandle<()>> {
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(&current, &latest);
print_notice(&current, &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(&current, &latest);
print_notice(&current, &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 ──────────────────────────────────────────────────────────────────

View file

@ -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(())
}

View file

@ -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))
);

View file

@ -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,
" {:<name_width$} {}",
styles.bold_dim.apply_to("NAME"),
styles.bold_dim.apply_to("DESCRIPTION"),
@ -94,7 +111,8 @@ fn print_section(
.as_deref()
.map(|g| truncate_str(g, GOAL_MAX_LEN))
.unwrap_or_default();
eprintln!(
fabro_util::printerr!(
printer,
" {:<name_width$} {}",
styles.cyan.apply_to(&w.name),
styles.dim.apply_to(goal_str),

View file

@ -2,12 +2,17 @@ mod create;
mod list;
use anyhow::Result;
use fabro_util::printer::Printer;
use crate::args::{GlobalArgs, WorkflowCommand, WorkflowNamespace};
pub(crate) fn dispatch(ns: WorkflowNamespace, globals: &GlobalArgs) -> 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),
}
}

View file

@ -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}");
}
}

View file

@ -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<Vec<(String, String)>> {
eprintln!(
pub(crate) async fn run_openai_oauth_or_api_key(
s: &Styles,
printer: Printer,
) -> Result<Vec<(String, String)>> {
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<Vec<(Strin
.ok_or_else(|| anyhow::anyhow!("OpenAI did not return a refresh token"))?;
let pairs =
openai_oauth_env_pairs(&tokens.access_token, refresh_token, account_id.as_deref());
eprintln!(
fabro_util::printerr!(
printer,
" {} OpenAI configured via browser login",
s.green.apply_to("")
);
@ -108,12 +114,13 @@ pub(crate) async fn run_openai_oauth_or_api_key(s: &Styles) -> Result<Vec<(Strin
}
Err(e) => {
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??;

View file

@ -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

View file

@ -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.