mermaid_cli/cli/
commands.rs1use 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
12pub 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), Commands::Run { .. } => Ok(false), }
50}
51
52pub 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
67pub fn show_version() {
69 println!("Mermaid v{}", env!("CARGO_PKG_VERSION"));
70 println!(" An open-source, model-agnostic AI pair programmer");
71}
72
73fn 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
112async fn show_status(config: &Config) -> Result<()> {
114 println!("Mermaid Status:");
115 println!();
116
117 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 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 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 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 {
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 show_provider_status(config);
204
205 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
215fn show_provider_status(config: &Config) {
220 let mut configured: Vec<(String, String)> = Vec::new(); 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 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 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}