clawgarden-cli 0.1.4

ClawGarden CLI - Multi-bot/multi-agent Garden management tool
//! Garden management - multi-garden support
//!
//! Manages ~/.garden/gardens.json which tracks all registered gardens.

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

/// Garden metadata stored in gardens.json
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GardenMeta {
    pub name: String,
    pub path: PathBuf,
    pub created_at: u64,
    pub container_name: String,
}

/// Gardens registry - manages all gardens on this host
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct GardensRegistry {
    pub gardens: Vec<GardenMeta>,
}

impl GardensRegistry {
    /// Load gardens registry from ~/.garden/gardens.json
    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")
    }

    /// Save gardens registry to ~/.garden/gardens.json
    pub fn save(&self) -> Result<()> {
        let path = Self::gardens_json_path()?;

        // Ensure directory exists
        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(())
    }

    /// Get garden directory path: ~/.garden/<name>
    pub fn garden_dir(&self, name: &str) -> PathBuf {
        Self::gardens_dir().join(name)
    }

    /// Get workspace path for a garden: ~/.garden/<name>/workspace
    pub fn workspace_dir(&self, name: &str) -> PathBuf {
        self.garden_dir(name).join("workspace")
    }

    /// Get docker-compose path for a garden: ~/.garden/<name>/docker-compose.yml
    pub fn compose_file(&self, name: &str) -> PathBuf {
        self.garden_dir(name).join("docker-compose.yml")
    }

    /// Get .env path for a garden: ~/.garden/<name>/.env
    pub fn env_file(&self, name: &str) -> PathBuf {
        self.garden_dir(name).join(".env")
    }

    /// Find garden by name
    pub fn find(&self, name: &str) -> Option<&GardenMeta> {
        self.gardens.iter().find(|g| g.name == name)
    }

    /// Find garden by name (mutable)
    #[allow(dead_code)]
    pub fn find_mut(&mut self, name: &str) -> Option<&mut GardenMeta> {
        self.gardens.iter_mut().find(|g| g.name == name)
    }

    /// Check if garden exists
    pub fn exists(&self, name: &str) -> bool {
        self.find(name).is_some()
    }

    /// Add a new garden
    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())
    }

    /// Remove a garden
    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(())
    }

    /// List all gardens
    pub fn list(&self) -> &[GardenMeta] {
        &self.gardens
    }

    /// Base directory for all gardens: ~/.garden/
    pub fn gardens_dir() -> PathBuf {
        dirs::home_dir()
            .unwrap_or_else(|| PathBuf::from("."))
            .join(".garden")
    }

    /// Path to gardens.json: ~/.garden/gardens.json
    fn gardens_json_path() -> Result<PathBuf> {
        Ok(Self::gardens_dir().join("gardens.json"))
    }
}

/// Get gardens registry (convenience function)
pub fn load_gardens() -> Result<GardensRegistry> {
    GardensRegistry::load()
}

/// Print list of gardens
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" // green dot
        } else {
            "\x1b[38;5;203m●\x1b[0m" // red dot (missing files)
        };
        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(())
}

/// Remove a garden by name
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);
    }

    // Stop container if running
    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();
    }

    // Remove from registry
    registry.remove(name)?;

    // Delete files if requested
    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(())
}