1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(tag = "command", rename_all = "snake_case")]
9pub enum ChatCommand {
10 Run {
12 workflow: String,
13 shadow: bool,
14 variables: HashMap<String, String>,
15 },
16 Workflows,
18 Status,
20 Audit { workflow_id: Option<String> },
22 Help,
24 Stop { execution_id: String },
26 Approve { execution_id: String },
28 Deny { execution_id: String },
30 Schedule,
32 Skills,
34 Unknown { text: String },
36}
37
38pub fn parse_command(text: &str) -> ChatCommand {
40 let text = text.trim();
41
42 if !text.starts_with('/') {
44 return ChatCommand::Unknown {
45 text: text.to_string(),
46 };
47 }
48
49 let parts: Vec<&str> = text.split_whitespace().collect();
50 let cmd = parts.first().map(|s| s.to_lowercase()).unwrap_or_default();
51
52 match cmd.as_str() {
53 "/run" => {
54 let workflow = parts.get(1).unwrap_or(&"").to_string();
55 if workflow.is_empty() {
56 return ChatCommand::Help;
57 }
58 let shadow = parts.contains(&"--shadow");
59 let mut variables = HashMap::new();
60 for (i, part) in parts.iter().enumerate() {
61 if *part == "--var" {
62 if let Some(kv) = parts.get(i + 1) {
63 if let Some((k, v)) = kv.split_once('=') {
64 variables.insert(k.to_string(), v.to_string());
65 }
66 }
67 }
68 }
69 ChatCommand::Run {
70 workflow,
71 shadow,
72 variables,
73 }
74 }
75 "/workflows" | "/wf" => ChatCommand::Workflows,
76 "/status" => ChatCommand::Status,
77 "/audit" => ChatCommand::Audit {
78 workflow_id: parts.get(1).map(|s| s.to_string()),
79 },
80 "/help" => ChatCommand::Help,
81 "/stop" => ChatCommand::Stop {
82 execution_id: parts.get(1).unwrap_or(&"").to_string(),
83 },
84 "/approve" => ChatCommand::Approve {
85 execution_id: parts.get(1).unwrap_or(&"").to_string(),
86 },
87 "/deny" => ChatCommand::Deny {
88 execution_id: parts.get(1).unwrap_or(&"").to_string(),
89 },
90 "/schedule" | "/schedules" => ChatCommand::Schedule,
91 "/skills" => ChatCommand::Skills,
92 _ => ChatCommand::Unknown {
93 text: text.to_string(),
94 },
95 }
96}
97
98pub fn help_text() -> String {
100 r#"⚡ *MUR Commander* — Available Commands:
101
102`/run <workflow> [--shadow]` — Execute a workflow
103`/workflows` — List available workflows
104`/status` — Show daemon status
105`/audit [workflow_id]` — View audit log
106`/stop <execution_id>` — Stop running workflow
107`/approve <execution_id>` — Approve a breakpoint
108`/deny <execution_id>` — Deny a breakpoint
109`/schedule` — List scheduled jobs
110`/skills` — List installed skills
111`/help` — Show this help"#
112 .to_string()
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn test_parse_run() {
121 let cmd = parse_command("/run deploy --shadow --var env=staging");
122 match cmd {
123 ChatCommand::Run {
124 workflow,
125 shadow,
126 variables,
127 } => {
128 assert_eq!(workflow, "deploy");
129 assert!(shadow);
130 assert_eq!(variables.get("env").unwrap(), "staging");
131 }
132 _ => panic!("Expected Run command"),
133 }
134 }
135
136 #[test]
137 fn test_parse_workflows() {
138 assert!(matches!(parse_command("/workflows"), ChatCommand::Workflows));
139 assert!(matches!(parse_command("/wf"), ChatCommand::Workflows));
140 }
141
142 #[test]
143 fn test_parse_unknown() {
144 let cmd = parse_command("hello");
145 assert!(matches!(cmd, ChatCommand::Unknown { .. }));
146 }
147
148 #[test]
149 fn test_parse_help() {
150 assert!(matches!(parse_command("/help"), ChatCommand::Help));
151 }
152
153 #[test]
154 fn test_parse_audit_with_filter() {
155 let cmd = parse_command("/audit deploy-wf");
156 match cmd {
157 ChatCommand::Audit { workflow_id } => {
158 assert_eq!(workflow_id.unwrap(), "deploy-wf");
159 }
160 _ => panic!("Expected Audit command"),
161 }
162 }
163
164 #[test]
165 fn test_parse_approve() {
166 let cmd = parse_command("/approve exec-123");
167 match cmd {
168 ChatCommand::Approve { execution_id } => {
169 assert_eq!(execution_id, "exec-123");
170 }
171 _ => panic!("Expected Approve command"),
172 }
173 }
174
175 #[test]
176 fn test_parse_deny() {
177 let cmd = parse_command("/deny exec-456");
178 match cmd {
179 ChatCommand::Deny { execution_id } => {
180 assert_eq!(execution_id, "exec-456");
181 }
182 _ => panic!("Expected Deny command"),
183 }
184 }
185
186 #[test]
187 fn test_parse_schedule() {
188 assert!(matches!(parse_command("/schedule"), ChatCommand::Schedule));
189 assert!(matches!(parse_command("/schedules"), ChatCommand::Schedule));
190 }
191
192 #[test]
193 fn test_parse_skills() {
194 assert!(matches!(parse_command("/skills"), ChatCommand::Skills));
195 }
196}