use crate::client::TrustyCommand;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SlackCommand {
Sessions,
Status(String),
Approve(String),
Deny(String),
Overseer,
Tmux,
Projects,
Discover,
Adopt(String),
Config(String),
Snapshot(String),
Kill(String),
Send(String),
Alerts,
Doctor,
Health,
Launch(String),
Fleet,
Get(String),
Msend(String),
Answer(String),
Activity(String),
Resume(String),
Decommission(String),
Help,
}
impl SlackCommand {
pub fn all_names() -> &'static [&'static str] {
&[
"sessions",
"status",
"approve",
"deny",
"overseer",
"tmux",
"projects",
"discover",
"adopt",
"config",
"snapshot",
"kill",
"send",
"alerts",
"doctor",
"health",
"launch",
"fleet",
"get",
"msend",
"answer",
"activity",
"resume",
"decommission",
"help",
]
}
}
pub fn parse_slash(command: &str, text: &str) -> Option<SlackCommand> {
let verb = command.trim().trim_start_matches('/').to_ascii_lowercase();
let arg = text.trim().to_string();
let cmd = match verb.as_str() {
"sessions" => SlackCommand::Sessions,
"status" => SlackCommand::Status(arg),
"approve" => SlackCommand::Approve(arg),
"deny" => SlackCommand::Deny(arg),
"overseer" => SlackCommand::Overseer,
"tmux" => SlackCommand::Tmux,
"projects" => SlackCommand::Projects,
"discover" => SlackCommand::Discover,
"adopt" => SlackCommand::Adopt(arg),
"config" => SlackCommand::Config(arg),
"snapshot" => SlackCommand::Snapshot(arg),
"kill" => SlackCommand::Kill(arg),
"send" => SlackCommand::Send(arg),
"alerts" => SlackCommand::Alerts,
"doctor" => SlackCommand::Doctor,
"health" => SlackCommand::Health,
"launch" => SlackCommand::Launch(arg),
"fleet" => SlackCommand::Fleet,
"get" => SlackCommand::Get(arg),
"msend" => SlackCommand::Msend(arg),
"answer" => SlackCommand::Answer(arg),
"activity" => SlackCommand::Activity(arg),
"resume" => SlackCommand::Resume(arg),
"decommission" => SlackCommand::Decommission(arg),
"help" => SlackCommand::Help,
_ => return None,
};
Some(cmd)
}
impl From<SlackCommand> for TrustyCommand {
fn from(cmd: SlackCommand) -> Self {
match cmd {
SlackCommand::Sessions => TrustyCommand::Sessions,
SlackCommand::Status(session_id) => TrustyCommand::Status { session_id },
SlackCommand::Approve(session_id) => TrustyCommand::Approve { session_id },
SlackCommand::Deny(session_id) => TrustyCommand::Deny { session_id },
SlackCommand::Overseer => TrustyCommand::Overseer,
SlackCommand::Tmux => TrustyCommand::Tmux,
SlackCommand::Projects => TrustyCommand::Projects,
SlackCommand::Discover => TrustyCommand::Discover,
SlackCommand::Adopt(session) => TrustyCommand::Adopt { session },
SlackCommand::Config(project) => TrustyCommand::Config { project },
SlackCommand::Snapshot(session) => TrustyCommand::Snapshot { session },
SlackCommand::Kill(session_id) => TrustyCommand::Kill { session_id },
SlackCommand::Send(args) => {
let (session, prompt) = split_send_args(&args);
TrustyCommand::Send { session, prompt }
}
SlackCommand::Alerts => TrustyCommand::Alerts,
SlackCommand::Doctor => TrustyCommand::Doctor,
SlackCommand::Health => TrustyCommand::Health,
SlackCommand::Launch(args) => {
let (repo_url, task) = split_launch_args(&args);
TrustyCommand::ManagedNew {
repo_url,
git_ref: String::new(),
task,
name_hint: None,
runtime: None,
inject_task: None,
deliverable_id: None,
force_new: false,
}
}
SlackCommand::Fleet => TrustyCommand::ManagedList,
SlackCommand::Get(target) => TrustyCommand::ManagedGet {
target: target.trim().to_string(),
},
SlackCommand::Msend(args) => {
let (target, text) = split_target_args(&args);
TrustyCommand::ManagedSend { target, text }
}
SlackCommand::Answer(args) => {
let (target, answer) = split_target_args(&args);
TrustyCommand::ManagedAnswer { target, answer }
}
SlackCommand::Activity(target) => TrustyCommand::ManagedActivity {
target: target.trim().to_string(),
},
SlackCommand::Resume(target) => TrustyCommand::ManagedResume {
target: target.trim().to_string(),
},
SlackCommand::Decommission(target) => TrustyCommand::ManagedDecommission {
target: target.trim().to_string(),
},
SlackCommand::Help => TrustyCommand::Help,
}
}
}
fn split_launch_args(args: &str) -> (String, String) {
match args.split_once('|') {
Some((workdir, task)) => (workdir.trim().to_string(), task.trim().to_string()),
None => (args.trim().to_string(), String::new()),
}
}
fn split_target_args(args: &str) -> (String, String) {
let trimmed = args.trim();
match trimmed.split_once(char::is_whitespace) {
Some((target, text)) => (target.to_string(), text.trim().to_string()),
None => (trimmed.to_string(), String::new()),
}
}
fn split_send_args(args: &str) -> (String, String) {
let trimmed = args.trim();
match trimmed.split_once(char::is_whitespace) {
Some((session, prompt)) => (session.to_string(), prompt.trim().to_string()),
None => (trimmed.to_string(), String::new()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_slash_strips_leading_slash() {
assert_eq!(parse_slash("/fleet", ""), Some(SlackCommand::Fleet));
assert_eq!(parse_slash("fleet", ""), Some(SlackCommand::Fleet));
assert_eq!(parse_slash("/FLEET", ""), Some(SlackCommand::Fleet));
}
#[test]
fn parse_slash_unknown_is_none() {
assert_eq!(parse_slash("/notacommand", "blah"), None);
assert_eq!(parse_slash("", ""), None);
}
#[test]
fn parse_slash_carries_argument_tail() {
assert_eq!(
parse_slash("/status", " abc-123 "),
Some(SlackCommand::Status("abc-123".into()))
);
assert_eq!(
parse_slash("/msend", "sess-1 build it now"),
Some(SlackCommand::Msend("sess-1 build it now".into()))
);
}
#[test]
fn parse_slash_maps_every_verb() {
let names = SlackCommand::all_names();
assert!(!names.is_empty(), "all_names() must advertise verbs");
for name in names {
let with_slash = format!("/{name}");
assert!(
parse_slash(&with_slash, "").is_some(),
"verb `{name}` failed to parse back",
);
}
}
#[test]
fn slack_command_converts_to_trusty() {
assert_eq!(
TrustyCommand::from(SlackCommand::Sessions),
TrustyCommand::Sessions
);
assert_eq!(TrustyCommand::from(SlackCommand::Help), TrustyCommand::Help);
assert_eq!(
TrustyCommand::from(SlackCommand::Fleet),
TrustyCommand::ManagedList
);
assert_eq!(
TrustyCommand::from(SlackCommand::Status("abc-123".into())),
TrustyCommand::Status {
session_id: "abc-123".into()
}
);
assert_eq!(
TrustyCommand::from(SlackCommand::Get(" sess-1 ".into())),
TrustyCommand::ManagedGet {
target: "sess-1".into()
}
);
assert_eq!(
TrustyCommand::from(SlackCommand::Health),
TrustyCommand::Health
);
}
#[test]
fn launch_command_converts() {
assert_eq!(
TrustyCommand::from(SlackCommand::Launch("/work/repo | build it".into())),
TrustyCommand::ManagedNew {
repo_url: "/work/repo".into(),
git_ref: String::new(),
task: "build it".into(),
name_hint: None,
runtime: None,
inject_task: None,
deliverable_id: None,
force_new: false,
}
);
}
#[test]
fn msend_and_answer_commands_convert() {
assert_eq!(
TrustyCommand::from(SlackCommand::Msend("sess-1 build now".into())),
TrustyCommand::ManagedSend {
target: "sess-1".into(),
text: "build now".into(),
}
);
assert_eq!(
TrustyCommand::from(SlackCommand::Answer("sess-1 yes proceed".into())),
TrustyCommand::ManagedAnswer {
target: "sess-1".into(),
answer: "yes proceed".into(),
}
);
}
#[test]
fn activity_resume_decommission_convert() {
assert_eq!(
TrustyCommand::from(SlackCommand::Activity("a".into())),
TrustyCommand::ManagedActivity { target: "a".into() }
);
assert_eq!(
TrustyCommand::from(SlackCommand::Resume("b".into())),
TrustyCommand::ManagedResume { target: "b".into() }
);
assert_eq!(
TrustyCommand::from(SlackCommand::Decommission("c".into())),
TrustyCommand::ManagedDecommission { target: "c".into() }
);
}
#[test]
fn split_launch_args_splits_on_pipe() {
let (workdir, task) = split_launch_args("/work/repo | run the full test suite");
assert_eq!(workdir, "/work/repo");
assert_eq!(task, "run the full test suite");
let (workdir, task) = split_launch_args("/work/repo");
assert_eq!(workdir, "/work/repo");
assert!(task.is_empty());
}
#[test]
fn split_target_args_separates_target_and_text() {
let (target, text) = split_target_args("sess-1 please continue now");
assert_eq!(target, "sess-1");
assert_eq!(text, "please continue now");
let (target, text) = split_target_args("sess-1");
assert_eq!(target, "sess-1");
assert!(text.is_empty());
}
#[test]
fn split_send_args_separates_session_and_prompt() {
let (session, prompt) = split_send_args("frontend run the tests please");
assert_eq!(session, "frontend");
assert_eq!(prompt, "run the tests please");
let (session, prompt) = split_send_args("frontend");
assert_eq!(session, "frontend");
assert!(prompt.is_empty());
}
}