use std::collections::HashSet;
use std::path::Path;
use anyhow::Context;
use astrid_core::dirs::AstridHome;
use colored::Colorize;
use super::capsule::meta::{CapsuleMeta, read_meta};
use crate::theme::Theme;
pub(crate) fn gc(force: bool) -> anyhow::Result<()> {
let home = AstridHome::resolve().context("failed to resolve Astrid home")?;
let wit_store = home.wit_dir();
if !wit_store.is_dir() {
println!(
"{}",
Theme::info(&format!(
"WIT store does not exist: {}",
wit_store.display()
))
);
return Ok(());
}
let marks = collect_marks(&home)?;
let mut orphans = Vec::new();
let mut total_blobs = 0_usize;
let mut total_bytes = 0_u64;
let mut orphan_bytes = 0_u64;
for entry in std::fs::read_dir(&wit_store)
.with_context(|| format!("failed to read WIT store: {}", wit_store.display()))?
{
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("wit") {
continue;
}
let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
continue;
};
if stem.contains(".tmp.") {
continue;
}
total_blobs = total_blobs.saturating_add(1);
if let Ok(meta) = std::fs::metadata(&path) {
total_bytes = total_bytes.saturating_add(meta.len());
}
if !marks.contains(stem) {
if let Ok(meta) = std::fs::metadata(&path) {
orphan_bytes = orphan_bytes.saturating_add(meta.len());
}
orphans.push(path);
}
}
println!("{}", Theme::header("WIT content store"));
println!(" Location: {}", wit_store.display());
println!(" Total blobs: {total_blobs}");
println!(
" Referenced: {}",
total_blobs.saturating_sub(orphans.len())
);
println!(" Orphaned: {}", orphans.len());
println!(" Total size: {total_bytes} bytes ({orphan_bytes} reclaimable)");
if orphans.is_empty() {
println!("{}", Theme::success("Nothing to do — no orphaned blobs."));
return Ok(());
}
if !force {
println!();
println!("{}", Theme::header("Orphaned blobs (dry run):"));
for path in &orphans {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
println!(" {}", name.yellow());
}
}
println!();
println!(
"{}",
Theme::info("Run with --force to actually delete these blobs.")
);
return Ok(());
}
println!();
println!("{}", Theme::warning("Deleting orphaned blobs..."));
let mut deleted = 0_usize;
for path in &orphans {
match std::fs::remove_file(path) {
Ok(()) => deleted = deleted.saturating_add(1),
Err(e) => {
eprintln!(
"{}",
Theme::warning(&format!("Failed to delete {}: {e}", path.display()))
);
},
}
}
println!(
"{} Deleted {deleted} blob(s), reclaimed {orphan_bytes} bytes",
Theme::success("OK")
);
Ok(())
}
fn collect_marks(home: &AstridHome) -> anyhow::Result<HashSet<String>> {
let mut marks = HashSet::new();
let home_root = home.home_dir();
if home_root.is_dir() {
for entry in std::fs::read_dir(&home_root)
.with_context(|| format!("failed to read {}", home_root.display()))?
{
let Ok(entry) = entry else {
continue;
};
let principal_root = entry.path();
if !principal_root.is_dir() {
continue;
}
let capsules_dir = principal_root.join(".local").join("capsules");
if capsules_dir.is_dir() {
collect_from_capsules_dir(&capsules_dir, &mut marks);
}
}
}
if let Ok(cwd) = std::env::current_dir() {
let ws_caps = cwd.join(".astrid").join("capsules");
if ws_caps.is_dir() {
collect_from_capsules_dir(&ws_caps, &mut marks);
}
}
Ok(marks)
}
fn collect_from_capsules_dir(dir: &Path, marks: &mut HashSet<String>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let capsule_dir = entry.path();
if !capsule_dir.is_dir() {
continue;
}
if let Some(meta) = read_meta(&capsule_dir) {
add_meta_marks(&meta, marks);
}
}
}
fn add_meta_marks(meta: &CapsuleMeta, marks: &mut HashSet<String>) {
for hash in meta.wit_files.values() {
marks.insert(hash.clone());
}
}