Skip to main content

mermaid_cli/cli/
commands.rs

1use anyhow::Result;
2
3use crate::{
4    app::{Config, get_config_dir, init_config, load_config},
5    models::{ModelFactory, PROVIDER_REGISTRY, lookup_provider},
6    ollama::is_installed as is_ollama_installed,
7    utils::resolve_api_key,
8};
9
10use super::Commands;
11
12/// Handle CLI subcommands
13/// Returns Ok(true) if the command was handled and we should exit
14/// Returns Ok(false) if we should continue to the main application
15pub async fn handle_command(command: &Commands, config: &Config) -> Result<bool> {
16    match command {
17        Commands::Init => {
18            println!("Initializing Mermaid configuration...");
19            init_config()?;
20            println!("Configuration initialized successfully!");
21            Ok(true)
22        },
23        Commands::List => {
24            list_models(config).await?;
25            Ok(true)
26        },
27        Commands::Version => {
28            show_version();
29            Ok(true)
30        },
31        Commands::Status => {
32            show_status(config).await?;
33            Ok(true)
34        },
35        Commands::Add { name } => {
36            crate::mcp::add_server(name).await?;
37            Ok(true)
38        },
39        Commands::Remove { name } => {
40            crate::mcp::remove_server(name).await?;
41            Ok(true)
42        },
43        Commands::Mcp => {
44            show_mcp_servers();
45            Ok(true)
46        },
47        Commands::Chat => Ok(false),       // Continue to chat interface
48        Commands::Run { .. } => Ok(false), // Handled by main.rs
49    }
50}
51
52/// List available models across all backends (honors user config).
53pub async fn list_models(config: &Config) -> Result<()> {
54    let models = ModelFactory::list_all_models(config).await?;
55
56    if models.is_empty() {
57        println!("No models found across any backends");
58    } else {
59        println!("Available models:");
60        for model in models {
61            println!("  - {}", model);
62        }
63    }
64    Ok(())
65}
66
67/// Show version information
68pub fn show_version() {
69    println!("Mermaid v{}", env!("CARGO_PKG_VERSION"));
70    println!("   An open-source, model-agnostic AI pair programmer");
71}
72
73/// Show configured MCP servers
74fn show_mcp_servers() {
75    let config = load_config().unwrap_or_default();
76
77    if config.mcp_servers.is_empty() {
78        println!("No MCP servers configured.\n");
79        println!("Add one with: mermaid add <name>");
80        println!("Examples:");
81        println!("  mermaid add context7     # Library documentation");
82        println!("  mermaid add playwright   # Browser automation");
83        println!("  mermaid add memory       # Persistent knowledge graph");
84        return;
85    }
86
87    println!("Configured MCP servers:\n");
88    for (name, server_cfg) in &config.mcp_servers {
89        let package = server_cfg
90            .args
91            .iter()
92            .find(|a| !a.starts_with('-'))
93            .unwrap_or(&server_cfg.command);
94        let env_keys: Vec<&String> = server_cfg.env.keys().collect();
95        let env_display = if env_keys.is_empty() {
96            String::new()
97        } else {
98            format!(
99                " (env: {})",
100                env_keys
101                    .iter()
102                    .map(|k| k.as_str())
103                    .collect::<Vec<_>>()
104                    .join(", ")
105            )
106        };
107        println!("  {} — {}{}", name, package, env_display);
108    }
109    println!("\nManage with: mermaid add <name> / mermaid remove <name>");
110}
111
112/// Show status of all dependencies
113async fn show_status(config: &Config) -> Result<()> {
114    println!("Mermaid Status:");
115    println!();
116
117    // Check available backends (use user's config for host/port)
118    let factory = ModelFactory::from_config(config);
119    let backends = factory.available_providers_pub().await;
120    if backends.is_empty() {
121        println!("  [WARNING] Backends: None available");
122    } else {
123        println!("  [OK] Backends: {}", backends.join(", "));
124    }
125
126    // Check Ollama (via HTTP, so remote deployments are honored)
127    if is_ollama_installed() {
128        let models = factory.list_models("ollama").await.unwrap_or_default();
129        if models.is_empty() {
130            println!("  [WARNING] Ollama: Installed (no models)");
131        } else {
132            println!("  [OK] Ollama: Running ({} models installed)", models.len());
133            for model in models.iter().take(3) {
134                println!("      - {}", model);
135            }
136            if models.len() > 3 {
137                println!("      ... and {} more", models.len() - 3);
138            }
139        }
140    } else {
141        println!("  [ERROR] Ollama: Not installed");
142    }
143
144    // Check configuration (uses platform-specific path via ProjectDirs)
145    if let Ok(config_dir) = get_config_dir() {
146        let config_path = config_dir.join("config.toml");
147        if config_path.exists() {
148            println!("  [OK] Configuration: {}", config_path.display());
149        } else {
150            println!("  [WARNING] Configuration: Not found (using defaults)");
151        }
152    }
153
154    // MCP Servers
155    if config.mcp_servers.is_empty() {
156        println!("  [INFO] MCP Servers: None configured (use 'mermaid add <name>')");
157    } else {
158        println!(
159            "  [OK] MCP Servers: {} configured",
160            config.mcp_servers.len()
161        );
162        for (name, server_cfg) in &config.mcp_servers {
163            println!(
164                "      - {} ({})",
165                name,
166                server_cfg.args.get(1).unwrap_or(&server_cfg.command)
167            );
168        }
169    }
170
171    // Project instructions (Step 5h). Walks UP from cwd to git root or
172    // $HOME to find the nearest MERMAID.md.
173    {
174        let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
175        match crate::app::instructions::find_mermaid_md(&cwd) {
176            Some(path) => match crate::app::instructions::load_from_path(&path) {
177                Some(loaded) => {
178                    println!(
179                        "  [OK] MERMAID.md: {} ({} bytes{})",
180                        loaded.path.display(),
181                        loaded.byte_len,
182                        if loaded.truncated { ", truncated" } else { "" }
183                    );
184                },
185                None => {
186                    println!(
187                        "  [WARNING] MERMAID.md: found at {} but unreadable",
188                        path.display()
189                    );
190                },
191            },
192            None => {
193                println!(
194                    "  [INFO] MERMAID.md: not found (create one to add persistent project instructions)"
195                );
196            },
197        }
198    }
199
200    // OpenAI-compatible providers — list anything from the built-in
201    // registry whose API key resolves, plus any user-defined custom
202    // providers. No network probe (would slow `mermaid status`).
203    show_provider_status(config);
204
205    // Environment variables (for API providers)
206    println!("\n  Environment:");
207    if std::env::var("OLLAMA_API_KEY").is_ok() {
208        println!("    - OLLAMA_API_KEY: Set (for Ollama Cloud)");
209    }
210
211    println!();
212    Ok(())
213}
214
215/// Print the remote-providers status block. Includes Anthropic (bespoke
216/// Messages API) and any OpenAI-compatible provider whose API key resolves.
217/// Custom providers from `[providers.<name>]` are listed if `base_url`
218/// and `api_key_env` are both set and the env var resolves.
219fn show_provider_status(config: &Config) {
220    let mut configured: Vec<(String, String)> = Vec::new(); // (name, base_url)
221
222    // Anthropic — checked first because it's not in the OpenAI-compat
223    // registry but is a top-tier provider users care about.
224    let anth_cfg = config.providers.get("anthropic");
225    if resolve_api_key(
226        "ANTHROPIC_API_KEY",
227        anth_cfg.and_then(|c| c.api_key_env.as_deref()),
228    )
229    .is_some()
230    {
231        let url = anth_cfg
232            .and_then(|c| c.base_url.clone())
233            .unwrap_or_else(|| "https://api.anthropic.com/v1".to_string());
234        configured.push(("anthropic".to_string(), url));
235    }
236
237    // Gemini — also bespoke (not in OpenAI-compat registry).
238    let gem_cfg = config.providers.get("gemini");
239    if resolve_api_key(
240        "GOOGLE_API_KEY",
241        gem_cfg.and_then(|c| c.api_key_env.as_deref()),
242    )
243    .is_some()
244    {
245        let url = gem_cfg
246            .and_then(|c| c.base_url.clone())
247            .unwrap_or_else(|| "https://generativelanguage.googleapis.com/v1beta".to_string());
248        configured.push(("gemini".to_string(), url));
249    }
250
251    for profile in PROVIDER_REGISTRY {
252        let user_cfg = config.providers.get(profile.name);
253        let api_key_present = resolve_api_key(
254            profile.api_key_env,
255            user_cfg.and_then(|c| c.api_key_env.as_deref()),
256        )
257        .is_some();
258        if api_key_present {
259            let url = user_cfg
260                .and_then(|c| c.base_url.clone())
261                .unwrap_or_else(|| profile.base_url.to_string());
262            configured.push((profile.name.to_string(), url));
263        }
264    }
265
266    // Custom providers — anything in config.providers not in registry
267    // and not "anthropic" / "gemini" (already handled above).
268    for (name, cfg) in &config.providers {
269        if name == "anthropic" || name == "gemini" || lookup_provider(name).is_some() {
270            continue;
271        }
272        if let (Some(url), Some(env)) = (&cfg.base_url, cfg.api_key_env.as_deref())
273            && resolve_api_key(env, None).is_some()
274        {
275            configured.push((name.clone(), url.clone()));
276        }
277    }
278
279    if configured.is_empty() {
280        println!(
281            "  [INFO] Remote providers: None configured (set $ANTHROPIC_API_KEY, \
282             $GOOGLE_API_KEY, $OPENAI_API_KEY, $GROQ_API_KEY, $OPENROUTER_API_KEY, etc., or \
283             add [providers.<name>] to config.toml)"
284        );
285    } else {
286        println!("  [OK] Remote providers: {} configured", configured.len());
287        for (name, url) in configured {
288            println!("      - {} ({})", name, url);
289        }
290    }
291}