1use std::path::PathBuf;
12
13use bamboo_skills::{SkillStore, SkillStoreConfig};
14use colored::Colorize;
15
16fn resolve_data_dir(data_dir: Option<PathBuf>) -> PathBuf {
18 data_dir.unwrap_or_else(bamboo_config::paths::resolve_bamboo_dir)
19}
20
21fn truncate(s: &str, max: usize) -> String {
23 if s.chars().count() <= max {
24 return s.to_string();
25 }
26 let head: String = s.chars().take(max.saturating_sub(1)).collect();
27 format!("{head}…")
28}
29
30pub async fn skills_list(data_dir: Option<PathBuf>) -> anyhow::Result<()> {
33 let data_dir = resolve_data_dir(data_dir);
34 let skills_dir = data_dir.join("skills");
35
36 let store = SkillStore::new(SkillStoreConfig {
39 skills_dir: skills_dir.clone(),
40 project_dir: None,
41 active_mode: None,
42 });
43 let skills = store.list_skills(None, true).await;
44
45 if skills.is_empty() {
46 println!(
47 "(no skills in {})",
48 bamboo_config::paths::path_to_display_string(&skills_dir)
49 );
50 return Ok(());
51 }
52
53 println!("{:<28} {:<32} DESCRIPTION", "ID", "NAME");
56 for skill in &skills {
57 println!(
58 "{:<28} {:<32} {}",
59 truncate(&skill.id, 28),
60 truncate(&skill.name, 32),
61 truncate(&skill.description, 60)
62 );
63 }
64 println!("\n{} skill(s).", skills.len());
65 Ok(())
66}
67
68pub async fn mcp_list(data_dir: Option<PathBuf>) -> anyhow::Result<()> {
72 use bamboo_domain::mcp_config::TransportConfig;
73
74 let data_dir = resolve_data_dir(data_dir);
75 let config = bamboo_config::Config::from_data_dir(Some(data_dir));
76 let servers = &config.mcp.servers;
77
78 if servers.is_empty() {
79 println!("(no MCP servers configured)");
80 return Ok(());
81 }
82
83 println!("{:<24} {:<8} {:<16} NAME", "ID", "ENABLED", "TRANSPORT");
86 for server in servers {
87 let transport = match &server.transport {
88 TransportConfig::Stdio(_) => "stdio",
89 TransportConfig::Sse(_) => "sse",
90 TransportConfig::StreamableHttp(_) => "streamable_http",
91 };
92 let name = server.name.as_deref().unwrap_or("");
93 println!(
94 "{:<24} {:<8} {:<16} {}",
95 truncate(&server.id, 24),
96 if server.enabled { "yes" } else { "no" },
97 transport,
98 name
99 );
100 }
101 let live = servers.iter().filter(|s| s.enabled).count();
102 println!(
103 "\n{live} enabled of {}. Live status: {}",
104 servers.len(),
105 "bamboo status".cyan()
106 );
107 Ok(())
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn truncate_shortens_long_cells() {
116 assert_eq!(truncate("hello", 10), "hello");
117 assert_eq!(truncate("hello-world", 5), "hell…");
118 }
119
120 #[tokio::test]
121 async fn skills_list_empty_dir_is_ok() {
122 let dir = tempfile::tempdir().expect("tempdir");
123 assert!(skills_list(Some(dir.path().to_path_buf())).await.is_ok());
125 }
126}