//! Bot command service — domain logic for dispatching slash commands. //! //! Extracted from `http/bot_command.rs` so that argument parsing and dispatch //! are independently testable without an HTTP layer. //! //! Conventions: `docs/architecture/service-modules.md` //! //! # Structure //! - `mod.rs` (this file) — public API and typed `Error` type //! - `parse.rs` — pure argument parsing, no I/O //! - `io.rs` — all side-effectful calls (transport handlers, stores, agent pool) pub(super) mod io; /// Pure argument parsing for bot slash commands. pub mod parse; use crate::agents::AgentPool; use std::path::Path; use std::sync::Arc; // ── Error type ──────────────────────────────────────────────────────────────── /// Typed errors returned by `service::bot_command::execute`. /// /// HTTP handlers map these to specific status codes: /// - [`Error::UnknownCommand`] → 404 Not Found /// - [`Error::BadArgs`] → 400 Bad Request /// - [`Error::CommandFailed`] → 500 Internal Server Error #[derive(Debug)] #[allow(dead_code)] // CommandFailed is part of the public API contract; not yet reachable pub enum Error { /// The command keyword does not match any registered command. UnknownCommand(String), /// The command exists but the provided arguments are invalid. BadArgs(String), /// The command ran but failed with an internal error. CommandFailed(String), } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::UnknownCommand(msg) | Self::BadArgs(msg) | Self::CommandFailed(msg) => { write!(f, "{msg}") } } } } // ── Public API ──────────────────────────────────────────────────────────────── /// Execute a bot command and return the markdown response. /// /// Dispatches to the same handlers used by the Matrix and Slack bots. The /// `cmd` argument is the lower-cased command keyword (e.g. `"status"`, /// `"start"`). The `args` argument is any text after the keyword, already /// trimmed. /// /// # Errors /// - [`Error::UnknownCommand`] if the command keyword is not registered. /// - [`Error::BadArgs`] if the arguments fail validation. /// - [`Error::CommandFailed`] if command execution raises an internal error. pub async fn execute( cmd: &str, args: &str, project_root: &Path, agents: &Arc, ) -> Result { match cmd { "assign" => { let parsed = parse::parse_assign(args).map_err(Error::BadArgs)?; Ok(io::call_assign(&parsed, project_root, agents).await) } "start" => { let parsed = parse::parse_start(args).map_err(Error::BadArgs)?; Ok(io::call_start(&parsed, project_root, agents).await) } "delete" => { let number = parse::parse_number("delete", args).map_err(Error::BadArgs)?; Ok(io::call_delete(&number, project_root, agents).await) } "rmtree" => { let number = parse::parse_number("rmtree", args).map_err(Error::BadArgs)?; Ok(io::call_rmtree(&number, project_root, agents).await) } "rebuild" => Ok(io::call_rebuild(project_root, agents).await), "timer" => io::call_timer(args, project_root) .await .map_err(Error::BadArgs), "htop" => Ok(io::call_htop(args, agents)), _ => match io::call_sync(cmd, args, project_root, agents) { Some(response) => Ok(response), None => Err(Error::UnknownCommand(format!( "Unknown command: `/{cmd}`. Type `/help` to see available commands." ))), }, } }