clawgarden-cli 0.7.3

ClawGarden CLI - Multi-bot/multi-agent Garden management tool
//! Secret management for ClawGarden CLI
//!
//! Manages `.secrets.toml` file in the garden directory.
//! Secrets include API keys, bot tokens, and other sensitive values.

use anyhow::{Context, Result};
use clawgarden_proto::config::SecretsToml;
use std::path::PathBuf;

use crate::garden::load_gardens;
use crate::ui;

/// Get the secrets file path for a garden
fn secrets_path(registry: &crate::garden::GardensRegistry, name: &str) -> PathBuf {
    registry.garden_dir(name).join(".secrets.toml")
}

/// Load secrets for a garden
fn load_secrets(name: &str) -> Result<(SecretsToml, PathBuf)> {
    let registry = load_gardens()?;
    if !registry.exists(name) {
        anyhow::bail!("Garden '{}' not found. Run 'garden new' first.", name);
    }
    let path = secrets_path(&registry, name);
    let secrets = SecretsToml::load_from(&path);
    Ok((secrets, path))
}

/// Migrate from legacy `.env` to `.secrets.toml`
fn migrate_from_env(name: &str) -> Result<bool> {
    let registry = load_gardens()?;
    let env_path = registry.env_file(name);
    let secrets_path = secrets_path(&registry, name);

    // Already have secrets file
    if secrets_path.exists() {
        return Ok(false);
    }

    // No .env to migrate
    if !env_path.exists() {
        return Ok(false);
    }

    let secrets = SecretsToml::from_env_file(&env_path);
    if secrets.secrets.is_empty() {
        return Ok(false);
    }

    secrets
        .save_to(&secrets_path)
        .map_err(|e| anyhow::anyhow!("Failed to write .secrets.toml: {}", e))?;

    ui::success(&format!(
        "Migrated {} secrets from .env โ†’ .secrets.toml",
        secrets.secrets.len()
    ));
    Ok(true)
}

/// `garden secret list` โ€” show all secrets (masked values)
pub fn cmd_list(name: Option<&str>) -> Result<()> {
    let name = crate::garden::resolve_garden_name(name)?;

    // Auto-migrate if needed
    let _ = migrate_from_env(&name);

    let (secrets, path) = load_secrets(&name)?;

    println!();
    ui::section_header_no_step("๐Ÿ”", &format!("Secrets ยท {}", name));

    if secrets.secrets.is_empty() {
        println!();
        ui::hint("No secrets stored.");
        ui::hint("Add one with: garden secret set <KEY>");
        println!();
        return Ok(());
    }

    let masked = secrets.list_masked();

    let mut rows = vec![
        (
            "๐Ÿ“".to_string(),
            "File".to_string(),
            path.display().to_string(),
        ),
        (
            "๐Ÿ”ข".to_string(),
            "Count".to_string(),
            format!("{} secret(s)", masked.len()),
        ),
    ];

    for (key, masked_value) in &masked {
        rows.push(("๐Ÿ”‘".to_string(), key.clone(), masked_value.clone()));
    }

    ui::summary_box(&format!("๐Ÿ” {} โ€” Secrets", name), &rows);

    Ok(())
}

/// `garden secret get <KEY>` โ€” print a secret value (for scripting)
pub fn cmd_get(name: Option<&str>, key: &str) -> Result<()> {
    let name = crate::garden::resolve_garden_name(name)?;
    let _ = migrate_from_env(&name);
    let (secrets, _) = load_secrets(&name)?;

    match secrets.get(key) {
        Some(value) => println!("{}", value),
        None => {
            anyhow::bail!("Secret '{}' not found.", key);
        }
    }

    Ok(())
}

/// `garden secret set <KEY> [VALUE]` โ€” set a secret (interactive if value omitted)
pub fn cmd_set(name: Option<&str>, key: &str, value: Option<&str>) -> Result<()> {
    let name = crate::garden::resolve_garden_name(name)?;
    let _ = migrate_from_env(&name);
    let (mut secrets, path) = load_secrets(&name)?;

    let value = match value {
        Some(v) => v.to_string(),
        None => {
            // Interactive prompt
            let prompt_text = format!("  Enter value for {}:", key);
            ui::retry_prompt(|| {
                inquire::Password::new(&prompt_text)
                    .with_help_message("Value will be stored in .secrets.toml (gitignored)")
                    .without_confirmation()
                    .prompt()
            })?
        }
    };

    let is_update = secrets.get(key).is_some();
    secrets.set(key.to_string(), value);

    secrets
        .save_to(&path)
        .map_err(|e| anyhow::anyhow!("Failed to save secret: {}", e))?;

    // Also regenerate .env for Docker compatibility
    regenerate_env(&name)?;

    if is_update {
        ui::success(&format!("Secret '{}' updated.", key));
    } else {
        ui::success(&format!("Secret '{}' set.", key));
    }

    Ok(())
}

/// `garden secret remove <KEY>` โ€” remove a secret
pub fn cmd_remove(name: Option<&str>, key: &str) -> Result<()> {
    let name = crate::garden::resolve_garden_name(name)?;
    let _ = migrate_from_env(&name);
    let (mut secrets, path) = load_secrets(&name)?;

    if secrets.get(key).is_none() {
        anyhow::bail!("Secret '{}' not found.", key);
    }

    let confirm = ui::retry_prompt(|| {
        inquire::Confirm::new(&format!("  Remove secret '{}'?", key))
            .with_default(false)
            .prompt()
    })?;

    if !confirm {
        ui::warn("Cancelled.");
        return Ok(());
    }

    secrets.remove(key);

    secrets
        .save_to(&path)
        .map_err(|e| anyhow::anyhow!("Failed to save secrets: {}", e))?;

    // Also regenerate .env for Docker compatibility
    regenerate_env(&name)?;

    ui::success(&format!("Secret '{}' removed.", key));
    Ok(())
}

/// `garden secret migrate` โ€” explicitly migrate from .env to .secrets.toml
pub fn cmd_migrate(name: Option<&str>) -> Result<()> {
    let name = crate::garden::resolve_garden_name(name)?;

    println!();
    ui::section_header_no_step("๐Ÿ”„", &format!("Secret Migration ยท {}", name));
    println!();

    let migrated = migrate_from_env(&name)?;

    if !migrated {
        ui::hint("No .env secrets to migrate, or .secrets.toml already exists.");
    }

    Ok(())
}

/// `garden secret env` โ€” export secrets as .env format (for Docker)
pub fn cmd_env(name: Option<&str>) -> Result<()> {
    let name = crate::garden::resolve_garden_name(name)?;
    let _ = migrate_from_env(&name);
    let (secrets, _) = load_secrets(&name)?;

    print!("{}", secrets.to_env_content());
    Ok(())
}

/// Regenerate the .env file from secrets (for Docker compatibility)
fn regenerate_env(name: &str) -> Result<()> {
    let registry = load_gardens()?;
    let secrets_path = secrets_path(&registry, name);
    let env_path = registry.env_file(name);

    let secrets = SecretsToml::load_from(&secrets_path);
    let env_content = secrets.to_env_content();

    std::fs::write(&env_path, env_content).context("Failed to write .env file")?;
    Ok(())
}