1use anyhow::Result;
2use std::sync::Arc;
3
4use crate::{
5 app::{Config, get_config_dir, init_config, load_config},
6 models::{BackendConfig, Model, PROVIDER_REGISTRY, lookup_provider},
7 ollama::is_installed as is_ollama_installed,
8 utils::{resolve_api_key, resolve_api_key_with_fallback},
9};
10
11use super::Commands;
12
13pub async fn handle_command(command: &Commands, config: &Config) -> Result<bool> {
17 match command {
18 Commands::Init => {
19 println!("Initializing Mermaid configuration...");
20 init_config()?;
21 println!("Configuration initialized successfully!");
22 Ok(true)
23 },
24 Commands::List => {
25 list_models(config).await?;
26 Ok(true)
27 },
28 Commands::Version => {
29 show_version();
30 Ok(true)
31 },
32 Commands::Status => {
33 show_status(config).await?;
34 Ok(true)
35 },
36 Commands::Add { name } => {
37 crate::mcp::add_server(name).await?;
38 Ok(true)
39 },
40 Commands::Remove { name } => {
41 crate::mcp::remove_server(name).await?;
42 Ok(true)
43 },
44 Commands::Mcp => {
45 show_mcp_servers();
46 Ok(true)
47 },
48 Commands::CloudSetup => {
49 let _ = crate::ollama::setup_cloud_interactive();
53 Ok(true)
54 },
55 Commands::Chat => Ok(false), Commands::Run { .. } => Ok(false), }
58}
59
60pub async fn list_models(config: &Config) -> Result<()> {
62 let ollama_models = list_ollama_models(config).await;
63 if ollama_models.is_empty() {
64 println!("No Ollama models installed locally.");
65 } else {
66 println!("Ollama models (local/cloud):");
67 for name in &ollama_models {
68 println!(" - ollama/{}", name);
69 }
70 }
71
72 println!("\nConfigured remote providers:");
73 let mut any = false;
74 for profile in PROVIDER_REGISTRY {
75 let env = config
76 .providers
77 .get(profile.name)
78 .and_then(|c| c.api_key_env.as_deref())
79 .unwrap_or(profile.api_key_env);
80 if resolve_api_key(env, None).is_some() {
81 any = true;
82 println!(" - {} (via ${})", profile.name, env);
83 }
84 }
85 if !any {
86 println!(" (none — set a provider API key env var to enable)");
87 }
88 println!("\nSwitch models in-session with /model <name>.");
89 Ok(())
90}
91
92async fn list_ollama_models(config: &Config) -> Vec<String> {
96 use crate::models::adapters::ollama::OllamaAdapter;
97 let backend = BackendConfig {
98 ollama_url: format!("http://{}:{}", config.ollama.host, config.ollama.port),
99 timeout_secs: 5,
100 max_idle_per_host: 2,
101 };
102 match OllamaAdapter::new("__list__", Arc::new(backend)).await {
103 Ok(adapter) => adapter.list_models().await.unwrap_or_default(),
104 Err(_) => Vec::new(),
105 }
106}
107
108pub fn show_version() {
110 println!("Mermaid v{}", env!("CARGO_PKG_VERSION"));
111 println!(" An open-source, model-agnostic AI pair programmer");
112}
113
114fn show_mcp_servers() {
116 let config = load_config().unwrap_or_default();
117
118 if config.mcp_servers.is_empty() {
119 println!("No MCP servers configured.\n");
120 println!("Add one with: mermaid add <name>");
121 println!("Examples:");
122 println!(" mermaid add context7 # Library documentation");
123 println!(" mermaid add playwright # Browser automation");
124 println!(" mermaid add memory # Persistent knowledge graph");
125 return;
126 }
127
128 println!("Configured MCP servers:\n");
129 for (name, server_cfg) in &config.mcp_servers {
130 let package = server_cfg
131 .args
132 .iter()
133 .find(|a| !a.starts_with('-'))
134 .unwrap_or(&server_cfg.command);
135 let env_keys: Vec<&String> = server_cfg.env.keys().collect();
136 let env_display = if env_keys.is_empty() {
137 String::new()
138 } else {
139 format!(
140 " (env: {})",
141 env_keys
142 .iter()
143 .map(|k| k.as_str())
144 .collect::<Vec<_>>()
145 .join(", ")
146 )
147 };
148 println!(" {} — {}{}", name, package, env_display);
149 }
150 println!("\nManage with: mermaid add <name> / mermaid remove <name>");
151}
152
153async fn show_status(config: &Config) -> Result<()> {
155 println!("Mermaid Status:");
156 println!();
157
158 let mut available: Vec<&'static str> = Vec::new();
161 for profile in PROVIDER_REGISTRY {
162 let env = config
163 .providers
164 .get(profile.name)
165 .and_then(|c| c.api_key_env.as_deref())
166 .unwrap_or(profile.api_key_env);
167 if resolve_api_key(env, None).is_some() {
168 available.push(profile.name);
169 }
170 }
171 if available.is_empty() {
172 println!(" [WARNING] Remote providers: none (no API keys in env)");
173 } else {
174 println!(" [OK] Remote providers: {}", available.join(", "));
175 }
176
177 if is_ollama_installed() {
179 let models = list_ollama_models(config).await;
180 if models.is_empty() {
181 println!(" [WARNING] Ollama: Installed (no models)");
182 } else {
183 println!(" [OK] Ollama: Running ({} models installed)", models.len());
184 for model in models.iter().take(3) {
185 println!(" - {}", model);
186 }
187 if models.len() > 3 {
188 println!(" ... and {} more", models.len() - 3);
189 }
190 }
191 } else {
192 println!(" [ERROR] Ollama: Not installed");
193 }
194
195 if let Ok(config_dir) = get_config_dir() {
197 let config_path = config_dir.join("config.toml");
198 if config_path.exists() {
199 println!(" [OK] Configuration: {}", config_path.display());
200 } else {
201 println!(" [WARNING] Configuration: Not found (using defaults)");
202 }
203 }
204
205 if config.mcp_servers.is_empty() {
207 println!(" [INFO] MCP Servers: None configured (use 'mermaid add <name>')");
208 } else {
209 println!(
210 " [OK] MCP Servers: {} configured",
211 config.mcp_servers.len()
212 );
213 for (name, server_cfg) in &config.mcp_servers {
214 println!(
215 " - {} ({})",
216 name,
217 server_cfg.args.get(1).unwrap_or(&server_cfg.command)
218 );
219 }
220 }
221
222 {
225 let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
226 match crate::app::instructions::find_mermaid_md(&cwd) {
227 Some(path) => match crate::app::instructions::load_from_path(&path) {
228 Some(loaded) => {
229 println!(
230 " [OK] MERMAID.md: {} ({} bytes{})",
231 loaded.path.display(),
232 loaded.byte_len,
233 if loaded.truncated { ", truncated" } else { "" }
234 );
235 },
236 None => {
237 println!(
238 " [WARNING] MERMAID.md: found at {} but unreadable",
239 path.display()
240 );
241 },
242 },
243 None => {
244 println!(
245 " [INFO] MERMAID.md: not found (create one to add persistent project instructions)"
246 );
247 },
248 }
249 }
250
251 show_provider_status(config);
255
256 println!("\n Environment:");
258 if std::env::var("OLLAMA_API_KEY").is_ok() {
259 println!(" - OLLAMA_API_KEY: Set (for Ollama Cloud)");
260 }
261
262 println!();
263 Ok(())
264}
265
266fn show_provider_status(config: &Config) {
271 let mut configured: Vec<(String, String)> = Vec::new(); let anth_cfg = config.providers.get("anthropic");
276 if resolve_api_key(
277 "ANTHROPIC_API_KEY",
278 anth_cfg.and_then(|c| c.api_key_env.as_deref()),
279 )
280 .is_some()
281 {
282 let url = anth_cfg
283 .and_then(|c| c.base_url.clone())
284 .unwrap_or_else(|| "https://api.anthropic.com/v1".to_string());
285 configured.push(("anthropic".to_string(), url));
286 }
287
288 let gem_cfg = config.providers.get("gemini");
290 if resolve_api_key_with_fallback(
291 "GOOGLE_API_KEY",
292 "GEMINI_API_KEY",
293 gem_cfg.and_then(|c| c.api_key_env.as_deref()),
294 )
295 .is_some()
296 {
297 let url = gem_cfg
298 .and_then(|c| c.base_url.clone())
299 .unwrap_or_else(|| "https://generativelanguage.googleapis.com/v1beta".to_string());
300 configured.push(("gemini".to_string(), url));
301 }
302
303 for profile in PROVIDER_REGISTRY {
304 let user_cfg = config.providers.get(profile.name);
305 let api_key_present = resolve_api_key(
306 profile.api_key_env,
307 user_cfg.and_then(|c| c.api_key_env.as_deref()),
308 )
309 .is_some();
310 if api_key_present {
311 let url = user_cfg
312 .and_then(|c| c.base_url.clone())
313 .unwrap_or_else(|| profile.base_url.to_string());
314 configured.push((profile.name.to_string(), url));
315 }
316 }
317
318 for (name, cfg) in &config.providers {
321 if name == "anthropic" || name == "gemini" || lookup_provider(name).is_some() {
322 continue;
323 }
324 if let (Some(url), Some(env)) = (&cfg.base_url, cfg.api_key_env.as_deref())
325 && resolve_api_key(env, None).is_some()
326 {
327 configured.push((name.clone(), url.clone()));
328 }
329 }
330
331 if configured.is_empty() {
332 println!(
333 " [INFO] Remote providers: None configured (set $ANTHROPIC_API_KEY, \
334 $GOOGLE_API_KEY, $OPENAI_API_KEY, $GROQ_API_KEY, $OPENROUTER_API_KEY, etc., or \
335 add [providers.<name>] to config.toml)"
336 );
337 } else {
338 println!(" [OK] Remote providers: {} configured", configured.len());
339 for (name, url) in configured {
340 println!(" - {} ({})", name, url);
341 }
342 }
343}