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.
This commit is contained in:
Bryan Helmkamp 2026-04-19 16:56:53 -04:00
parent 03fc375cdd
commit 8bf243fe47
No known key found for this signature in database
5 changed files with 201 additions and 0 deletions

View file

@ -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<String>,
}
#[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<String>,
}
#[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",
}
}
}

View file

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

View file

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

View file

@ -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<Vec<RunSummary>> {
let mut all_runs = Vec::new();
let mut offset = 0_u64;

View file

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