smart-tree 8.0.1

Smart Tree - An intelligent, AI-friendly directory visualization tool
Documentation
//! Environment Templates - Pre-configured development environments
//!
//! Templates define what a user's space looks like:
//! - Tools and languages installed
//! - Shell configuration
//! - Default environment variables
//! - Container image (if using podman)

use super::space::IsolationLevel;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;

/// A development environment template
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Template {
    /// Template name (e.g., "rust-dev", "node-dev", "minimal")
    pub name: String,

    /// Human-readable description
    pub description: String,

    /// Base container image (for podman isolation)
    pub image: Option<String>,

    /// Default isolation level
    #[serde(default)]
    pub default_isolation: IsolationLevel,

    /// Shell to use
    #[serde(default = "default_shell")]
    pub shell: String,

    /// Environment variables
    #[serde(default)]
    pub env: Vec<(String, String)>,

    /// Packages/tools to install
    #[serde(default)]
    pub packages: Vec<String>,

    /// Shell init commands (run on space start)
    #[serde(default)]
    pub init_commands: Vec<String>,

    /// Files to copy into the space
    #[serde(default)]
    pub files: HashMap<String, String>,

    /// Author of the template
    pub author: Option<String>,

    /// Tags for discoverability
    #[serde(default)]
    pub tags: Vec<String>,
}

fn default_shell() -> String {
    "/bin/bash".to_string()
}

impl Default for Template {
    fn default() -> Self {
        Template {
            name: "minimal".to_string(),
            description: "Minimal development environment".to_string(),
            image: None,
            default_isolation: IsolationLevel::None,
            shell: default_shell(),
            env: Vec::new(),
            packages: Vec::new(),
            init_commands: Vec::new(),
            files: HashMap::new(),
            author: None,
            tags: vec!["minimal".to_string()],
        }
    }
}

impl Template {
    /// Create a new template
    pub fn new(name: &str, description: &str) -> Self {
        Template {
            name: name.to_string(),
            description: description.to_string(),
            ..Default::default()
        }
    }

    /// Rust development template
    pub fn rust_dev() -> Self {
        Template {
            name: "rust-dev".to_string(),
            description: "Rust development with cargo, clippy, rustfmt".to_string(),
            image: Some("rust:latest".to_string()),
            default_isolation: IsolationLevel::None,
            shell: "/bin/bash".to_string(),
            env: vec![
                ("CARGO_HOME".to_string(), "/usr/local/cargo".to_string()),
                ("RUSTUP_HOME".to_string(), "/usr/local/rustup".to_string()),
            ],
            packages: vec![
                "rust-analyzer".to_string(),
                "cargo-watch".to_string(),
                "cargo-edit".to_string(),
            ],
            init_commands: vec!["rustup component add clippy rustfmt".to_string()],
            files: HashMap::new(),
            author: Some("smart-tree".to_string()),
            tags: vec!["rust".to_string(), "systems".to_string()],
        }
    }

    /// Node.js development template
    pub fn node_dev() -> Self {
        Template {
            name: "node-dev".to_string(),
            description: "Node.js/TypeScript development with pnpm".to_string(),
            image: Some("node:20".to_string()),
            default_isolation: IsolationLevel::None,
            shell: "/bin/bash".to_string(),
            env: vec![("PNPM_HOME".to_string(), "/usr/local/pnpm".to_string())],
            packages: vec![
                "pnpm".to_string(),
                "typescript".to_string(),
                "tsx".to_string(),
            ],
            init_commands: vec!["corepack enable".to_string()],
            files: HashMap::new(),
            author: Some("smart-tree".to_string()),
            tags: vec!["node".to_string(), "typescript".to_string(), "web".to_string()],
        }
    }

    /// Python development template
    pub fn python_dev() -> Self {
        Template {
            name: "python-dev".to_string(),
            description: "Python development with uv and ruff".to_string(),
            image: Some("python:3.12".to_string()),
            default_isolation: IsolationLevel::None,
            shell: "/bin/bash".to_string(),
            env: vec![
                ("UV_SYSTEM_PYTHON".to_string(), "1".to_string()),
                ("PYTHONDONTWRITEBYTECODE".to_string(), "1".to_string()),
            ],
            packages: vec!["uv".to_string(), "ruff".to_string(), "mypy".to_string()],
            init_commands: vec![],
            files: HashMap::new(),
            author: Some("smart-tree".to_string()),
            tags: vec!["python".to_string(), "ml".to_string(), "data".to_string()],
        }
    }

    /// AI assistant template (for Claude, etc.)
    pub fn ai_assistant() -> Self {
        Template {
            name: "ai-assistant".to_string(),
            description: "Environment for AI code assistants".to_string(),
            image: None,
            default_isolation: IsolationLevel::Namespace, // Light isolation for safety
            shell: "/bin/bash".to_string(),
            env: vec![
                ("AI_MODE".to_string(), "1".to_string()),
                ("TERM".to_string(), "xterm-256color".to_string()),
            ],
            packages: vec![], // AI brings its own tools via MCP
            init_commands: vec![],
            files: HashMap::new(),
            author: Some("smart-tree".to_string()),
            tags: vec!["ai".to_string(), "assistant".to_string(), "mcp".to_string()],
        }
    }

    /// Load a template from a TOML file
    pub fn load(path: &PathBuf) -> Result<Self> {
        let content = std::fs::read_to_string(path)
            .with_context(|| format!("Failed to read template: {:?}", path))?;
        toml::from_str(&content).with_context(|| format!("Failed to parse template: {:?}", path))
    }

    /// Save template to a TOML file
    pub fn save(&self, path: &PathBuf) -> Result<()> {
        let content = toml::to_string_pretty(self)?;
        std::fs::write(path, content)
            .with_context(|| format!("Failed to write template: {:?}", path))?;
        Ok(())
    }
}

/// Template registry - manages available templates
#[derive(Debug, Default)]
pub struct TemplateRegistry {
    templates: HashMap<String, Template>,
    search_paths: Vec<PathBuf>,
}

impl TemplateRegistry {
    /// Create a new registry with default templates
    pub fn new() -> Self {
        let mut registry = TemplateRegistry {
            templates: HashMap::new(),
            search_paths: vec![],
        };

        // Register built-in templates
        registry.register(Template::default());
        registry.register(Template::rust_dev());
        registry.register(Template::node_dev());
        registry.register(Template::python_dev());
        registry.register(Template::ai_assistant());

        registry
    }

    /// Add a search path for template files
    pub fn add_search_path(&mut self, path: PathBuf) {
        self.search_paths.push(path);
    }

    /// Register a template
    pub fn register(&mut self, template: Template) {
        self.templates.insert(template.name.clone(), template);
    }

    /// Get a template by name
    pub fn get(&self, name: &str) -> Option<&Template> {
        self.templates.get(name)
    }

    /// List all available templates
    pub fn list(&self) -> Vec<&Template> {
        self.templates.values().collect()
    }

    /// Search templates by tag
    pub fn search_by_tag(&self, tag: &str) -> Vec<&Template> {
        self.templates
            .values()
            .filter(|t| t.tags.iter().any(|t| t.contains(tag)))
            .collect()
    }

    /// Load templates from all search paths
    pub fn load_all(&mut self) -> Result<usize> {
        let mut count = 0;
        for path in self.search_paths.clone() {
            if path.is_dir() {
                for entry in std::fs::read_dir(&path)? {
                    let entry = entry?;
                    let file_path = entry.path();
                    if file_path.extension().is_some_and(|e| e == "toml") {
                        if let Ok(template) = Template::load(&file_path) {
                            self.register(template);
                            count += 1;
                        }
                    }
                }
            }
        }
        Ok(count)
    }

    /// Create a template from the current environment
    pub fn capture_current(name: &str, description: &str) -> Result<Template> {
        let mut template = Template::new(name, description);

        // Capture current shell
        if let Ok(shell) = std::env::var("SHELL") {
            template.shell = shell;
        }

        // Capture relevant environment variables
        let capture_vars = ["CARGO_HOME", "RUSTUP_HOME", "PNPM_HOME", "GOPATH", "PYENV_ROOT"];
        for var in capture_vars {
            if let Ok(value) = std::env::var(var) {
                template.env.push((var.to_string(), value));
            }
        }

        Ok(template)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_default_template() {
        let template = Template::default();
        assert_eq!(template.name, "minimal");
        assert_eq!(template.shell, "/bin/bash");
    }

    #[test]
    fn test_rust_dev_template() {
        let template = Template::rust_dev();
        assert_eq!(template.name, "rust-dev");
        assert!(template.tags.contains(&"rust".to_string()));
    }

    #[test]
    fn test_registry() {
        let registry = TemplateRegistry::new();
        assert!(registry.get("minimal").is_some());
        assert!(registry.get("rust-dev").is_some());
        assert!(registry.get("node-dev").is_some());
    }

    #[test]
    fn test_search_by_tag() {
        let registry = TemplateRegistry::new();
        let web = registry.search_by_tag("web");
        assert!(!web.is_empty());
    }
}