From 8bf243fe47e9ca4f8a06bd2ed9af2cc6de37f7c4 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sun, 19 Apr 2026 16:56:53 -0400 Subject: [PATCH] feat(cli): add `fabro archive` and `fabro unarchive` commands Two new top-level commands mirror `fabro rm`'s bulk-by-ID shape: positional run identifiers, per-ID success/error aggregation, and a final non-zero exit if any item failed. Calls the new server endpoints from Unit 5. Shared bulk loop covers both directions and emits structured JSON with an `archived` or `unarchived` list alongside `errors`. Top-level help snapshot updated. --- lib/crates/fabro-cli/src/args.rs | 27 ++++ .../fabro-cli/src/commands/runs/archive.rs | 145 ++++++++++++++++++ lib/crates/fabro-cli/src/commands/runs/mod.rs | 7 + lib/crates/fabro-cli/src/server_client.rs | 20 +++ lib/crates/fabro-cli/tests/it/cmd/fabro.rs | 2 + 5 files changed, 201 insertions(+) create mode 100644 lib/crates/fabro-cli/src/commands/runs/archive.rs diff --git a/lib/crates/fabro-cli/src/args.rs b/lib/crates/fabro-cli/src/args.rs index c80c5d11a..57ad1e2f9 100644 --- a/lib/crates/fabro-cli/src/args.rs +++ b/lib/crates/fabro-cli/src/args.rs @@ -292,6 +292,26 @@ pub(crate) struct RunsRemoveArgs { pub(crate) force: bool, } +#[derive(Args)] +pub(crate) struct RunsArchiveArgs { + #[command(flatten)] + pub(crate) server: ServerTargetArgs, + + /// Run IDs or workflow names to archive + #[arg(required = true)] + pub(crate) runs: Vec, +} + +#[derive(Args)] +pub(crate) struct RunsUnarchiveArgs { + #[command(flatten)] + pub(crate) server: ServerTargetArgs, + + /// Run IDs or workflow names to unarchive + #[arg(required = true)] + pub(crate) runs: Vec, +} + #[derive(Args)] pub(crate) struct LogsArgs { #[command(flatten)] @@ -942,6 +962,11 @@ pub(crate) enum RunsCommands { Rm(RunsRemoveArgs), /// Show detailed information about a workflow run Inspect(InspectArgs), + /// Mark terminal runs as archived (reviewed, no further action needed). + /// Archived runs are hidden from default listings. + Archive(RunsArchiveArgs), + /// Restore archived runs to their prior terminal status. + Unarchive(RunsUnarchiveArgs), } impl RunsCommands { @@ -950,6 +975,8 @@ impl RunsCommands { Self::Ps(_) => "ps", Self::Rm(_) => "rm", Self::Inspect(_) => "inspect", + Self::Archive(_) => "archive", + Self::Unarchive(_) => "unarchive", } } } diff --git a/lib/crates/fabro-cli/src/commands/runs/archive.rs b/lib/crates/fabro-cli/src/commands/runs/archive.rs new file mode 100644 index 000000000..531724048 --- /dev/null +++ b/lib/crates/fabro-cli/src/commands/runs/archive.rs @@ -0,0 +1,145 @@ +use anyhow::{Result, bail}; +use fabro_types::settings::CliSettings; +use fabro_types::settings::cli::{CliLayer, OutputFormat}; +use fabro_util::printer::Printer; + +use super::short_run_id; +use crate::args::{RunsArchiveArgs, RunsUnarchiveArgs}; +use crate::command_context::CommandContext; +use crate::server_client; +use crate::server_runs::{ + ServerRunSummaryInfo, ServerSummaryLookup, resolve_server_run_from_summaries, +}; +use crate::shared::print_json_pretty; + +pub(crate) async fn archive_command( + args: &RunsArchiveArgs, + cli: &CliSettings, + cli_layer: &CliLayer, + printer: Printer, +) -> Result<()> { + let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; + let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; + run_bulk( + Action::Archive, + &args.runs, + lookup.client(), + lookup.runs(), + cli, + printer, + ) + .await +} + +pub(crate) async fn unarchive_command( + args: &RunsUnarchiveArgs, + cli: &CliSettings, + cli_layer: &CliLayer, + printer: Printer, +) -> Result<()> { + let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?; + let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?; + run_bulk( + Action::Unarchive, + &args.runs, + lookup.client(), + lookup.runs(), + cli, + printer, + ) + .await +} + +#[derive(Clone, Copy)] +enum Action { + Archive, + Unarchive, +} + +impl Action { + fn verb_ing(self) -> &'static str { + match self { + Self::Archive => "archive", + Self::Unarchive => "unarchive", + } + } + + fn past(self) -> &'static str { + match self { + Self::Archive => "archived", + Self::Unarchive => "unarchived", + } + } + + fn json_key(self) -> &'static str { + self.past() + } +} + +async fn run_bulk( + action: Action, + identifiers: &[String], + client: &server_client::ServerStoreClient, + runs: &[ServerRunSummaryInfo], + cli: &CliSettings, + printer: Printer, +) -> Result<()> { + let json = cli.output.format == OutputFormat::Json; + let mut had_errors = false; + let mut changed = Vec::new(); + let mut errors = Vec::new(); + + for identifier in identifiers { + let run = match resolve_server_run_from_summaries(runs, identifier) { + Ok(run) => run, + Err(err) => { + if !json { + fabro_util::printerr!(printer, "error: {identifier}: {err}"); + } + errors.push(serde_json::json!({ + "identifier": identifier, + "error": err.to_string(), + })); + had_errors = true; + continue; + } + }; + + let run_id = run.run_id(); + let result = match action { + Action::Archive => client.archive_run(&run_id).await, + Action::Unarchive => client.unarchive_run(&run_id).await, + }; + match result { + Ok(()) => { + let run_id_string = run_id.to_string(); + changed.push(run_id_string.clone()); + if !json { + fabro_util::printerr!(printer, "{}", short_run_id(&run_id_string)); + } + } + Err(err) => { + if !json { + fabro_util::printerr!(printer, "error: {identifier}: {err}"); + } + errors.push(serde_json::json!({ + "identifier": identifier, + "error": err.to_string(), + })); + had_errors = true; + } + } + } + + if json { + let mut body = serde_json::Map::new(); + body.insert(action.json_key().to_string(), serde_json::json!(changed)); + body.insert("errors".to_string(), serde_json::json!(errors)); + print_json_pretty(&serde_json::Value::Object(body))?; + } + + if had_errors { + bail!("some runs could not be {}", action.verb_ing()); + } + Ok(()) +} diff --git a/lib/crates/fabro-cli/src/commands/runs/mod.rs b/lib/crates/fabro-cli/src/commands/runs/mod.rs index 589d553c8..f3900a94e 100644 --- a/lib/crates/fabro-cli/src/commands/runs/mod.rs +++ b/lib/crates/fabro-cli/src/commands/runs/mod.rs @@ -6,6 +6,7 @@ use fabro_util::terminal::Styles; use crate::args::RunsCommands; +pub(crate) mod archive; pub(crate) mod inspect; pub(crate) mod list; pub(crate) mod rm; @@ -23,6 +24,12 @@ pub(crate) async fn dispatch( } RunsCommands::Rm(args) => rm::remove_command(&args, cli, cli_layer, printer).await, RunsCommands::Inspect(args) => inspect::run(&args, cli, cli_layer, printer).await, + RunsCommands::Archive(args) => { + archive::archive_command(&args, cli, cli_layer, printer).await + } + RunsCommands::Unarchive(args) => { + archive::unarchive_command(&args, cli, cli_layer, printer).await + } } } diff --git a/lib/crates/fabro-cli/src/server_client.rs b/lib/crates/fabro-cli/src/server_client.rs index da033e83e..9132dcbc1 100644 --- a/lib/crates/fabro-cli/src/server_client.rs +++ b/lib/crates/fabro-cli/src/server_client.rs @@ -594,6 +594,26 @@ impl ServerStoreClient { Ok(()) } + pub(crate) async fn archive_run(&self, run_id: &RunId) -> Result<()> { + self.client + .archive_run() + .id(run_id.to_string()) + .send() + .await + .map_err(map_api_error)?; + Ok(()) + } + + pub(crate) async fn unarchive_run(&self, run_id: &RunId) -> Result<()> { + self.client + .unarchive_run() + .id(run_id.to_string()) + .send() + .await + .map_err(map_api_error)?; + Ok(()) + } + pub(crate) async fn list_store_runs(&self) -> Result> { let mut all_runs = Vec::new(); let mut offset = 0_u64; diff --git a/lib/crates/fabro-cli/tests/it/cmd/fabro.rs b/lib/crates/fabro-cli/tests/it/cmd/fabro.rs index 0e67c798d..b4e203046 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/fabro.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/fabro.rs @@ -28,6 +28,8 @@ fn help() { store Export store-backed run state for debugging rm Remove one or more workflow runs inspect Show detailed information about a workflow run + archive Mark terminal runs as archived (reviewed, no further action needed). Archived runs are hidden from default listings + unarchive Restore archived runs to their prior terminal status model List and test LLM models server Server operations doctor Check environment and integration health