Skip to main content

roboticus_cli/cli/admin/
skills.rs

1use super::*;
2
3// ── Skills ────────────────────────────────────────────────────
4
5pub async fn cmd_skills_list(url: &str, json: bool) -> Result<(), Box<dyn std::error::Error>> {
6    let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
7    let (OK, ACTION, WARN, DETAIL, ERR) = icons();
8    let c = RoboticusClient::new(url)?;
9    let data = c.get("/api/skills").await.map_err(|e| {
10        RoboticusClient::check_connectivity_hint(&*e);
11        e
12    })?;
13    if json {
14        println!("{}", serde_json::to_string_pretty(&data)?);
15        return Ok(());
16    }
17
18    heading("Skills");
19
20    let skills = data["skills"].as_array();
21    match skills {
22        Some(arr) if !arr.is_empty() => {
23            let widths = [22, 14, 34, 9];
24            table_header(&["Name", "Kind", "Description", "Enabled"], &widths);
25            for s in arr {
26                let name = s["name"].as_str().unwrap_or("").to_string();
27                let kind = s["kind"].as_str().unwrap_or("").to_string();
28                let desc = truncate_id(s["description"].as_str().unwrap_or(""), 31);
29                let enabled = if s["enabled"].as_bool().unwrap_or(false)
30                    || s["enabled"].as_i64().map(|v| v != 0).unwrap_or(false)
31                {
32                    format!("{OK} yes")
33                } else {
34                    format!("{RED}{ERR} no{RESET}")
35                };
36                table_row(
37                    &[format!("{ACCENT}{name}{RESET}"), kind, desc, enabled],
38                    &widths,
39                );
40            }
41            eprintln!();
42            eprintln!("    {DIM}{} skill(s){RESET}", arr.len());
43        }
44        _ => empty_state("No skills registered"),
45    }
46
47    eprintln!();
48    Ok(())
49}
50
51pub async fn cmd_skill_detail(
52    url: &str,
53    id: &str,
54    json: bool,
55) -> Result<(), Box<dyn std::error::Error>> {
56    let c = RoboticusClient::new(url)?;
57    let s = c.get(&format!("/api/skills/{id}")).await.map_err(|e| {
58        RoboticusClient::check_connectivity_hint(&*e);
59        e
60    })?;
61    if json {
62        println!("{}", serde_json::to_string_pretty(&s)?);
63        return Ok(());
64    }
65
66    heading(&format!("Skill: {}", s["name"].as_str().unwrap_or(id)));
67
68    kv_mono("ID", s["id"].as_str().unwrap_or(""));
69    kv_accent("Name", s["name"].as_str().unwrap_or(""));
70    kv("Kind", s["kind"].as_str().unwrap_or(""));
71    kv("Description", s["description"].as_str().unwrap_or(""));
72    kv_mono("Source", s["source_path"].as_str().unwrap_or(""));
73    kv_mono("Hash", s["content_hash"].as_str().unwrap_or(""));
74
75    let enabled = if s["enabled"].as_bool().unwrap_or(false)
76        || s["enabled"].as_i64().map(|v| v != 0).unwrap_or(false)
77    {
78        status_badge("running")
79    } else {
80        status_badge("dead")
81    };
82    kv("Enabled", &enabled);
83
84    if let Some(triggers) = s["triggers_json"].as_str()
85        && !triggers.is_empty()
86        && triggers != "null"
87    {
88        kv("Triggers", triggers);
89    }
90    if let Some(script) = s["script_path"].as_str()
91        && !script.is_empty()
92        && script != "null"
93    {
94        kv_mono("Script", script);
95    }
96
97    eprintln!();
98    Ok(())
99}
100
101pub async fn cmd_skills_reload(url: &str) -> Result<(), Box<dyn std::error::Error>> {
102    let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
103    let (OK, ACTION, WARN, DETAIL, ERR) = icons();
104    let c = RoboticusClient::new(url)?;
105    c.post("/api/skills/reload", serde_json::json!({}))
106        .await
107        .map_err(|e| {
108            RoboticusClient::check_connectivity_hint(&*e);
109            e
110        })?;
111    eprintln!();
112    eprintln!("  {OK} Skills reloaded from disk");
113    eprintln!();
114    Ok(())
115}
116
117pub async fn cmd_skills_catalog_list(
118    url: &str,
119    query: Option<&str>,
120    json: bool,
121) -> Result<(), Box<dyn std::error::Error>> {
122    let c = RoboticusClient::new(url)?;
123    let path = if let Some(q) = query {
124        format!("/api/skills/catalog?q={}", crate::cli::urlencoding(q))
125    } else {
126        "/api/skills/catalog".to_string()
127    };
128    let data = c.get(&path).await?;
129    if json {
130        println!("{}", serde_json::to_string_pretty(&data)?);
131        return Ok(());
132    }
133    heading("Skills & Plugins");
134
135    // ── Skills ──
136    let items = data["items"].as_array().cloned().unwrap_or_default();
137    let plugins = data["plugins"].as_array().cloned().unwrap_or_default();
138
139    if items.is_empty() && plugins.is_empty() {
140        empty_state("No catalog entries found");
141        eprintln!();
142        return Ok(());
143    }
144
145    if !items.is_empty() {
146        let (_, _, _, DETAIL, _) = icons();
147        eprintln!("  {DETAIL} Skills");
148        let widths = [28, 12, 50];
149        table_header(&["Name", "Kind", "Description"], &widths);
150        for item in items {
151            let name = item
152                .get("name")
153                .and_then(|v| v.as_str())
154                .unwrap_or("")
155                .to_string();
156            let kind = item
157                .get("kind")
158                .and_then(|v| v.as_str())
159                .unwrap_or("")
160                .to_string();
161            let description = item
162                .get("description")
163                .and_then(|v| v.as_str())
164                .unwrap_or("")
165                .to_string();
166            table_row(&[name, kind, description], &widths);
167        }
168        eprintln!();
169    }
170
171    // ── Plugins ──
172    if !plugins.is_empty() {
173        let (_, _, _, DETAIL, _) = icons();
174        eprintln!("  {DETAIL} Plugins");
175        let widths = [28, 10, 10, 42];
176        table_header(&["Name", "Version", "Tier", "Description"], &widths);
177        for plugin in plugins {
178            let name = plugin
179                .get("name")
180                .and_then(|v| v.as_str())
181                .unwrap_or("")
182                .to_string();
183            let version = plugin
184                .get("version")
185                .and_then(|v| v.as_str())
186                .unwrap_or("")
187                .to_string();
188            let tier = plugin
189                .get("tier")
190                .and_then(|v| v.as_str())
191                .unwrap_or("")
192                .to_string();
193            let description = plugin
194                .get("description")
195                .and_then(|v| v.as_str())
196                .unwrap_or("")
197                .to_string();
198            table_row(&[name, version, tier, description], &widths);
199        }
200        eprintln!();
201    }
202
203    Ok(())
204}
205
206pub async fn cmd_skills_catalog_install(
207    url: &str,
208    skills: &[String],
209    activate: bool,
210) -> Result<(), Box<dyn std::error::Error>> {
211    let c = RoboticusClient::new(url)?;
212    heading("Skills & Plugins — Install");
213    let data = match c
214        .post(
215            "/api/skills/catalog/install",
216            serde_json::json!({ "skills": skills, "activate": activate }),
217        )
218        .await
219    {
220        Ok(d) => d,
221        Err(e) => {
222            let (_, _, WARN, _, _) = icons();
223            // Extract human-readable error from JSON body if present
224            let msg = e.to_string();
225            if let Some(json_start) = msg.find('{')
226                && let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&msg[json_start..])
227                && let Some(detail) = parsed.get("error").and_then(|v| v.as_str())
228            {
229                eprintln!("  {WARN} {detail}");
230                eprintln!();
231                return Ok(());
232            }
233            eprintln!("  {WARN} {msg}");
234            eprintln!();
235            return Ok(());
236        }
237    };
238    if let Some(err) = data.get("error").and_then(|v| v.as_str()) {
239        let (_, _, WARN, _, _) = icons();
240        eprintln!("  {WARN} {err}");
241    } else {
242        kv("Installed", &data["installed"].to_string());
243        kv("Activated", &data["activated"].to_string());
244    }
245    eprintln!();
246    Ok(())
247}
248
249pub async fn cmd_skills_catalog_activate(
250    url: &str,
251    skills: &[String],
252) -> Result<(), Box<dyn std::error::Error>> {
253    let c = RoboticusClient::new(url)?;
254    let _ = c
255        .post(
256            "/api/skills/catalog/activate",
257            serde_json::json!({ "skills": skills }),
258        )
259        .await?;
260    heading("Skills & Plugins — Activate");
261    kv("Requested", &skills.join(", "));
262    eprintln!();
263    Ok(())
264}