1use super::*;
2
3pub 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 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 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 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}