CLI: propagate alias rename errors, doctor config resilience

aliases: rename now propagates every meta.json I/O and parse error
instead of silently ignoring failures. Previously, a corrupt or
unreadable meta.json after directory rename would leave the alias
in an inconsistent state with no user-visible error.

doctor: Config::load failure now falls back to Config::default()
with a warning instead of aborting the entire health check. Doctor
should still run diagnostics even when config is missing or corrupt
— that's exactly when you need it most.
This commit is contained in:
teernisse
2026-02-12 16:14:01 -05:00
parent 75d9344b44
commit 0b9a8a36c5
3 changed files with 29 additions and 16 deletions

View File

@@ -304,16 +304,22 @@ fn cmd_rename(names: &[String], robot: bool, start: Instant) -> Result<(), Swagg
)) ))
})?; })?;
// Update meta.json alias field // Update meta.json alias field -- propagate errors so the cache
// doesn't end up with a stale alias name in metadata.
let meta_path = new_dir.join("meta.json"); let meta_path = new_dir.join("meta.json");
if let Ok(bytes) = std::fs::read(&meta_path) let meta_bytes = std::fs::read(&meta_path).map_err(|e| {
&& let Ok(mut meta) = serde_json::from_slice::<CacheMetadata>(&bytes) SwaggerCliError::Cache(format!("failed to read meta.json after rename: {e}"))
{ })?;
meta.alias = new.clone(); let mut meta: CacheMetadata = serde_json::from_slice(&meta_bytes).map_err(|e| {
if let Ok(updated_bytes) = serde_json::to_vec_pretty(&meta) { SwaggerCliError::Cache(format!("failed to parse meta.json after rename: {e}"))
let _ = std::fs::write(&meta_path, updated_bytes); })?;
} meta.alias = new.clone();
} let updated_bytes = serde_json::to_vec_pretty(&meta).map_err(|e| {
SwaggerCliError::Cache(format!("failed to serialize meta.json after rename: {e}"))
})?;
std::fs::write(&meta_path, updated_bytes).map_err(|e| {
SwaggerCliError::Cache(format!("failed to write meta.json after rename: {e}"))
})?;
// Update config if old was the default // Update config if old was the default
let cfg_path = config_path(None); let cfg_path = config_path(None);

View File

@@ -32,8 +32,8 @@ pub struct Args {
#[arg(long)] #[arg(long)]
pub prune_stale: bool, pub prune_stale: bool,
/// Days before an alias is considered stale (default: 90) /// Days before an alias is considered stale (default: 30, matching config)
#[arg(long, default_value_t = 90)] #[arg(long, default_value_t = 30)]
pub prune_threshold: u32, pub prune_threshold: u32,
/// Evict least-recently-used aliases until total size is under this limit (MB) /// Evict least-recently-used aliases until total size is under this limit (MB)
@@ -290,13 +290,13 @@ fn execute_prune(args: &Args, robot: bool, start: Instant) -> Result<(), Swagger
"cache", "cache",
start.elapsed(), start.elapsed(),
); );
} else if stale.is_empty() { } else if pruned.is_empty() {
println!( println!(
"No stale aliases (threshold: {} days).", "No stale aliases (threshold: {} days).",
args.prune_threshold args.prune_threshold
); );
} else { } else {
println!("Pruned {} stale alias(es).", stale.len()); println!("Pruned {} stale alias(es).", pruned.len());
} }
Ok(()) Ok(())
} }

View File

@@ -344,12 +344,19 @@ fn try_fix_alias(cm: &CacheManager, alias: &str) -> Result<Vec<String>, Vec<Stri
pub async fn execute(args: &Args, robot_mode: bool) -> Result<(), SwaggerCliError> { pub async fn execute(args: &Args, robot_mode: bool) -> Result<(), SwaggerCliError> {
let start = Instant::now(); let start = Instant::now();
// Load config // Load config -- doctor should work even when config is missing or
// corrupt, so fall back to defaults and report a warning.
let cfg_path = config_path(None); let cfg_path = config_path(None);
let config = Config::load(&cfg_path)?; let mut warnings: Vec<String> = Vec::new();
let config = match Config::load(&cfg_path) {
Ok(cfg) => cfg,
Err(e) => {
warnings.push(format!("could not load config (using defaults): {e}"));
Config::default()
}
};
// Check config dir exists // Check config dir exists
let mut warnings: Vec<String> = Vec::new();
if let Some(parent) = cfg_path.parent() if let Some(parent) = cfg_path.parent()
&& !parent.exists() && !parent.exists()
{ {