use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GardenMeta {
pub name: String,
pub path: PathBuf,
pub created_at: u64,
pub container_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct GardensRegistry {
pub gardens: Vec<GardenMeta>,
}
impl GardensRegistry {
pub fn load() -> Result<Self> {
let path = Self::gardens_json_path()?;
if !path.exists() {
return Ok(Self::default());
}
let content = std::fs::read_to_string(&path).context("Failed to read gardens.json")?;
serde_json::from_str(&content).context("Failed to parse gardens.json")
}
pub fn save(&self) -> Result<()> {
let path = Self::gardens_json_path()?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).context("Failed to create ~/.garden directory")?;
}
let content =
serde_json::to_string_pretty(self).context("Failed to serialize gardens.json")?;
std::fs::write(&path, content).context("Failed to write gardens.json")?;
Ok(())
}
pub fn garden_dir(&self, name: &str) -> PathBuf {
Self::gardens_dir().join(name)
}
pub fn workspace_dir(&self, name: &str) -> PathBuf {
self.garden_dir(name).join("workspace")
}
pub fn compose_file(&self, name: &str) -> PathBuf {
self.garden_dir(name).join("docker-compose.yml")
}
pub fn env_file(&self, name: &str) -> PathBuf {
self.garden_dir(name).join(".env")
}
pub fn find(&self, name: &str) -> Option<&GardenMeta> {
self.gardens.iter().find(|g| g.name == name)
}
#[allow(dead_code)]
pub fn find_mut(&mut self, name: &str) -> Option<&mut GardenMeta> {
self.gardens.iter_mut().find(|g| g.name == name)
}
pub fn exists(&self, name: &str) -> bool {
self.find(name).is_some()
}
pub fn add(&mut self, name: String, path: PathBuf) -> Result<&GardenMeta> {
if self.exists(&name) {
anyhow::bail!("Garden '{}' already exists", name);
}
let created_at = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let meta = GardenMeta {
name: name.clone(),
path: path.clone(),
created_at,
container_name: format!("garden-{}", name),
};
self.gardens.push(meta.clone());
self.save()?;
Ok(self.find(&name).unwrap())
}
pub fn remove(&mut self, name: &str) -> Result<()> {
let idx = self
.gardens
.iter()
.position(|g| g.name == name)
.context("Garden not found")?;
self.gardens.remove(idx);
self.save()?;
Ok(())
}
pub fn list(&self) -> &[GardenMeta] {
&self.gardens
}
pub fn gardens_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".garden")
}
fn gardens_json_path() -> Result<PathBuf> {
Ok(Self::gardens_dir().join("gardens.json"))
}
}
pub fn load_gardens() -> Result<GardensRegistry> {
GardensRegistry::load()
}
pub fn list_gardens() -> Result<()> {
let registry = load_gardens()?;
if registry.gardens.is_empty() {
println!();
crate::ui::hint("No gardens found. Plant one with:");
println!(" {}garden new{}", "\x1b[38;5;77m", "\x1b[0m");
println!();
return Ok(());
}
println!();
println!(
" {} Registered Gardens {}",
"\x1b[1m\x1b[38;5;255m", "\x1b[0m"
);
println!();
for (i, garden) in registry.list().iter().enumerate() {
let status = if garden.path.exists() {
"\x1b[38;5;77m●\x1b[0m" } else {
"\x1b[38;5;203m●\x1b[0m" };
println!(
" {} {:<2}. {}{:<20}{} {}",
status,
i + 1,
"\x1b[1m",
garden.name,
"\x1b[0m",
garden.container_name,
);
println!(" {}{}{}", "\x1b[2m", garden.path.display(), "\x1b[0m",);
}
println!();
crate::ui::divider();
println!(" {} garden(s) in total", registry.gardens.len());
println!();
Ok(())
}
pub fn remove_garden(name: &str, delete_files: bool) -> Result<()> {
let mut registry = load_gardens()?;
if !registry.exists(name) {
anyhow::bail!("Garden '{}' not found", name);
}
let compose_file = registry.compose_file(name);
if compose_file.exists() {
crate::ui::spinner("Stopping container...", 400);
let _ = std::process::Command::new(crate::compose::docker_compose_bin())
.args(["-f", compose_file.to_str().unwrap(), "down"])
.output();
}
registry.remove(name)?;
if delete_files {
let garden_dir = registry.garden_dir(name);
if garden_dir.exists() {
crate::ui::spinner("Removing garden files...", 400);
std::fs::remove_dir_all(garden_dir).context("Failed to delete garden directory")?;
}
}
println!();
crate::ui::success(&format!("Garden '{}' removed.", name));
Ok(())
}