1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(tag = "command", rename_all = "snake_case")]
12pub enum ChatCommand {
13 Run {
15 workflow: String,
16 shadow: bool,
17 variables: HashMap<String, String>,
18 },
19 Workflow {
21 action: WorkflowAction,
22 },
23 Workflows,
25 Status,
27 Audit { workflow_id: Option<String> },
29 Help,
31 Stop { execution_id: String },
33 Approve { execution_id: String },
35 Deny { execution_id: String },
37 Schedule,
39 Skills,
41 Unknown { text: String },
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47#[serde(tag = "action", rename_all = "snake_case")]
48pub enum WorkflowAction {
49 Run {
51 workflow: String,
52 shadow: bool,
53 variables: HashMap<String, String>,
54 },
55 List,
57 Status { execution_id: String },
59 Cancel { execution_id: String },
61}
62
63pub fn parse_command(text: &str) -> ChatCommand {
65 let text = text.trim();
66
67 if !text.starts_with('/') {
69 return ChatCommand::Unknown {
70 text: text.to_string(),
71 };
72 }
73
74 let parts: Vec<&str> = text.split_whitespace().collect();
75 let cmd = parts.first().map(|s| s.to_lowercase()).unwrap_or_default();
76
77 match cmd.as_str() {
78 "/run" => parse_run_args(&parts),
79 "/workflow" | "/wf" => parse_workflow_command(&parts),
80 "/workflows" => ChatCommand::Workflows,
81 "/status" => ChatCommand::Status,
82 "/audit" => ChatCommand::Audit {
83 workflow_id: parts.get(1).map(|s| s.to_string()),
84 },
85 "/help" => ChatCommand::Help,
86 "/stop" => {
87 let execution_id = parts.get(1).unwrap_or(&"").to_string();
88 if execution_id.is_empty() {
89 return ChatCommand::Unknown {
90 text: "Usage: /stop <execution_id>".to_string(),
91 };
92 }
93 ChatCommand::Stop { execution_id }
94 }
95 "/approve" => {
96 let execution_id = parts.get(1).unwrap_or(&"").to_string();
97 if execution_id.is_empty() {
98 return ChatCommand::Unknown {
99 text: "Usage: /approve <execution_id>".to_string(),
100 };
101 }
102 ChatCommand::Approve { execution_id }
103 }
104 "/deny" => {
105 let execution_id = parts.get(1).unwrap_or(&"").to_string();
106 if execution_id.is_empty() {
107 return ChatCommand::Unknown {
108 text: "Usage: /deny <execution_id>".to_string(),
109 };
110 }
111 ChatCommand::Deny { execution_id }
112 }
113 "/schedule" | "/schedules" => ChatCommand::Schedule,
114 "/skills" => ChatCommand::Skills,
115 _ => ChatCommand::Unknown {
116 text: text.to_string(),
117 },
118 }
119}
120
121fn parse_run_args(parts: &[&str]) -> ChatCommand {
123 let workflow = parts.get(1).unwrap_or(&"").to_string();
124 if workflow.is_empty() {
125 return ChatCommand::Help;
126 }
127 let shadow = parts.contains(&"--shadow");
128 let variables = extract_variables(parts);
129 ChatCommand::Run {
130 workflow,
131 shadow,
132 variables,
133 }
134}
135
136fn parse_workflow_command(parts: &[&str]) -> ChatCommand {
138 let sub = parts
139 .get(1)
140 .map(|s| s.to_lowercase())
141 .unwrap_or_default();
142
143 match sub.as_str() {
144 "run" => {
145 let workflow = parts.get(2).unwrap_or(&"").to_string();
146 if workflow.is_empty() {
147 return ChatCommand::Help;
148 }
149 let shadow = parts.contains(&"--shadow");
150 let variables = extract_variables(parts);
151 ChatCommand::Workflow {
152 action: WorkflowAction::Run {
153 workflow,
154 shadow,
155 variables,
156 },
157 }
158 }
159 "list" | "ls" => ChatCommand::Workflow {
160 action: WorkflowAction::List,
161 },
162 "status" => {
163 let execution_id = parts.get(2).unwrap_or(&"").to_string();
164 ChatCommand::Workflow {
165 action: WorkflowAction::Status { execution_id },
166 }
167 }
168 "cancel" | "stop" => {
169 let execution_id = parts.get(2).unwrap_or(&"").to_string();
170 ChatCommand::Workflow {
171 action: WorkflowAction::Cancel { execution_id },
172 }
173 }
174 name if !name.is_empty() => {
176 let shadow = parts.contains(&"--shadow");
177 let variables = extract_variables(parts);
178 ChatCommand::Workflow {
179 action: WorkflowAction::Run {
180 workflow: name.to_string(),
181 shadow,
182 variables,
183 },
184 }
185 }
186 _ => ChatCommand::Workflows,
187 }
188}
189
190fn extract_variables(parts: &[&str]) -> HashMap<String, String> {
192 let mut variables = HashMap::new();
193 for (i, part) in parts.iter().enumerate() {
194 if *part == "--var" {
195 if let Some(kv) = parts.get(i + 1) {
196 if let Some((k, v)) = kv.split_once('=') {
197 variables.insert(k.to_string(), v.to_string());
198 }
199 }
200 }
201 }
202 variables
203}
204
205pub fn help_text() -> String {
207 r#"⚡ *MUR Commander* — Available Commands:
208
209`/run <workflow> [--shadow]` — Execute a workflow
210`/workflow run <name>` — Execute a workflow (alias)
211`/workflow list` — List available workflows
212`/workflow status <id>` — Check execution status
213`/workflow cancel <id>` — Cancel a running workflow
214`/workflows` — List available workflows
215`/status` — Show daemon status
216`/audit [workflow_id]` — View audit log
217`/stop <execution_id>` — Stop running workflow
218`/approve <execution_id>` — Approve a breakpoint
219`/deny <execution_id>` — Deny a breakpoint
220`/schedule` — List scheduled jobs
221`/skills` — List installed skills
222`/help` — Show this help"#
223 .to_string()
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn test_parse_run() {
232 let cmd = parse_command("/run deploy --shadow --var env=staging");
233 match cmd {
234 ChatCommand::Run {
235 workflow,
236 shadow,
237 variables,
238 } => {
239 assert_eq!(workflow, "deploy");
240 assert!(shadow);
241 assert_eq!(variables.get("env").unwrap(), "staging");
242 }
243 _ => panic!("Expected Run command"),
244 }
245 }
246
247 #[test]
248 fn test_parse_workflows() {
249 assert!(matches!(
250 parse_command("/workflows"),
251 ChatCommand::Workflows
252 ));
253 }
254
255 #[test]
256 fn test_parse_unknown() {
257 let cmd = parse_command("hello");
258 assert!(matches!(cmd, ChatCommand::Unknown { .. }));
259 }
260
261 #[test]
262 fn test_parse_help() {
263 assert!(matches!(parse_command("/help"), ChatCommand::Help));
264 }
265
266 #[test]
267 fn test_parse_audit_with_filter() {
268 let cmd = parse_command("/audit deploy-wf");
269 match cmd {
270 ChatCommand::Audit { workflow_id } => {
271 assert_eq!(workflow_id.unwrap(), "deploy-wf");
272 }
273 _ => panic!("Expected Audit command"),
274 }
275 }
276
277 #[test]
278 fn test_parse_approve() {
279 let cmd = parse_command("/approve exec-123");
280 match cmd {
281 ChatCommand::Approve { execution_id } => {
282 assert_eq!(execution_id, "exec-123");
283 }
284 _ => panic!("Expected Approve command"),
285 }
286 }
287
288 #[test]
289 fn test_parse_deny() {
290 let cmd = parse_command("/deny exec-456");
291 match cmd {
292 ChatCommand::Deny { execution_id } => {
293 assert_eq!(execution_id, "exec-456");
294 }
295 _ => panic!("Expected Deny command"),
296 }
297 }
298
299 #[test]
300 fn test_parse_schedule() {
301 assert!(matches!(
302 parse_command("/schedule"),
303 ChatCommand::Schedule
304 ));
305 assert!(matches!(
306 parse_command("/schedules"),
307 ChatCommand::Schedule
308 ));
309 }
310
311 #[test]
312 fn test_parse_skills() {
313 assert!(matches!(parse_command("/skills"), ChatCommand::Skills));
314 }
315
316 #[test]
317 fn test_parse_workflow_run() {
318 let cmd = parse_command("/workflow run deploy --shadow");
319 match cmd {
320 ChatCommand::Workflow {
321 action: WorkflowAction::Run { workflow, shadow, .. },
322 } => {
323 assert_eq!(workflow, "deploy");
324 assert!(shadow);
325 }
326 _ => panic!("Expected Workflow Run command"),
327 }
328 }
329
330 #[test]
331 fn test_parse_workflow_list() {
332 let cmd = parse_command("/workflow list");
333 match cmd {
334 ChatCommand::Workflow {
335 action: WorkflowAction::List,
336 } => {}
337 _ => panic!("Expected Workflow List command"),
338 }
339 }
340
341 #[test]
342 fn test_parse_workflow_status() {
343 let cmd = parse_command("/workflow status exec-789");
344 match cmd {
345 ChatCommand::Workflow {
346 action: WorkflowAction::Status { execution_id },
347 } => {
348 assert_eq!(execution_id, "exec-789");
349 }
350 _ => panic!("Expected Workflow Status command"),
351 }
352 }
353
354 #[test]
355 fn test_parse_workflow_cancel() {
356 let cmd = parse_command("/workflow cancel exec-789");
357 match cmd {
358 ChatCommand::Workflow {
359 action: WorkflowAction::Cancel { execution_id },
360 } => {
361 assert_eq!(execution_id, "exec-789");
362 }
363 _ => panic!("Expected Workflow Cancel command"),
364 }
365 }
366
367 #[test]
368 fn test_parse_workflow_shorthand() {
369 let cmd = parse_command("/workflow deploy");
371 match cmd {
372 ChatCommand::Workflow {
373 action: WorkflowAction::Run { workflow, shadow, .. },
374 } => {
375 assert_eq!(workflow, "deploy");
376 assert!(!shadow);
377 }
378 _ => panic!("Expected Workflow Run shorthand, got {:?}", cmd),
379 }
380 }
381
382 #[test]
383 fn test_parse_wf_alias() {
384 let cmd = parse_command("/wf list");
386 match cmd {
387 ChatCommand::Workflow {
388 action: WorkflowAction::List,
389 } => {}
390 _ => panic!("Expected Workflow List from /wf alias"),
391 }
392 }
393}