use anyhow::{Context, Result};
use assay_evidence::store::BundleStore;
use assay_evidence::{resolve_store_url, ObjectStoreBundleStore, StoreSpec};
use clap::{Args, ValueEnum};
use std::path::PathBuf;
#[derive(Debug, Args, Clone)]
pub struct ListArgs {
#[arg(long)]
pub run_id: Option<String>,
#[arg(long)]
pub prefix: Option<String>,
#[arg(long, default_value = "100")]
pub limit: usize,
#[arg(long, env = "ASSAY_STORE_URL")]
pub store: Option<String>,
#[arg(long)]
pub store_config: Option<PathBuf>,
#[arg(long, value_enum, default_value = "plain")]
pub format: ListFormat,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum ListFormat {
Plain,
Json,
Table,
}
pub async fn cmd_list(args: ListArgs) -> Result<i32> {
let url = resolve_store_url(args.store.as_deref(), args.store_config.as_deref())
.map_err(|e| anyhow::anyhow!("{}", e))?;
let spec = StoreSpec::parse(&url).with_context(|| format!("invalid store URL: {}", url))?;
let store = ObjectStoreBundleStore::from_spec(&spec)
.await
.with_context(|| "failed to connect to store")?;
if let Some(run_id) = &args.run_id {
list_for_run(&store, run_id, args.format).await
} else {
list_all(&store, args.prefix.as_deref(), args.limit, args.format).await
}
}
async fn list_for_run(
store: &ObjectStoreBundleStore,
run_id: &str,
format: ListFormat,
) -> Result<i32> {
let bundle_ids = store
.list_bundles_for_run(run_id)
.await
.with_context(|| format!("failed to list bundles for run: {}", run_id))?;
match format {
ListFormat::Plain => {
for id in &bundle_ids {
println!("{}", id);
}
}
ListFormat::Json => {
let json = serde_json::json!({
"run_id": run_id,
"bundles": bundle_ids,
"count": bundle_ids.len()
});
println!("{}", serde_json::to_string_pretty(&json)?);
}
ListFormat::Table => {
eprintln!("Run: {}", run_id);
eprintln!("Bundles: {}", bundle_ids.len());
eprintln!();
println!("{:<60} ", "BUNDLE_ID");
println!("{:-<60}", "");
for id in &bundle_ids {
println!("{:<60}", id);
}
}
}
if bundle_ids.is_empty() && !matches!(format, ListFormat::Json) {
eprintln!("(no bundles found)");
}
Ok(0)
}
async fn list_all(
store: &ObjectStoreBundleStore,
prefix: Option<&str>,
limit: usize,
format: ListFormat,
) -> Result<i32> {
let metas = store
.list_bundles(prefix, Some(limit))
.await
.context("failed to list bundles")?;
match format {
ListFormat::Plain => {
for meta in &metas {
println!("{}", meta.bundle_id);
}
}
ListFormat::Json => {
let json = serde_json::json!({
"bundles": metas.iter().map(|m| {
serde_json::json!({
"bundle_id": m.bundle_id,
"size": m.size,
"modified": m.modified.map(|t| t.to_rfc3339())
})
}).collect::<Vec<_>>(),
"count": metas.len(),
"prefix": prefix,
"limit": limit
});
println!("{}", serde_json::to_string_pretty(&json)?);
}
ListFormat::Table => {
if let Some(p) = prefix {
eprintln!("Prefix: {}", p);
}
eprintln!("Showing up to {} bundle(s)", limit);
eprintln!();
println!("{:<50} {:>12} MODIFIED", "BUNDLE_ID", "SIZE");
println!("{:-<50} {:->12} {:-<20}", "", "", "");
for meta in &metas {
let size_str = meta
.size
.map(format_size)
.unwrap_or_else(|| "-".to_string());
let modified_str = meta
.modified
.map(|t| t.format("%Y-%m-%d %H:%M").to_string())
.unwrap_or_else(|| "-".to_string());
let id_display = if meta.bundle_id.len() > 47 {
format!("{}...", &meta.bundle_id[..47])
} else {
meta.bundle_id.clone()
};
println!("{:<50} {:>12} {}", id_display, size_str, modified_str);
}
}
}
if metas.is_empty() && !matches!(format, ListFormat::Json) {
eprintln!("(no bundles found)");
}
Ok(0)
}
fn format_size(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if bytes >= GB {
format!("{:.1} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.1} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.1} KB", bytes as f64 / KB as f64)
} else {
format!("{} B", bytes)
}
}