2026-03-22 19:07:07 +00:00
|
|
|
//! Rebuild command: trigger a server rebuild and restart.
|
|
|
|
|
//!
|
|
|
|
|
//! `{bot_name} rebuild` stops all running agents, rebuilds the server binary
|
|
|
|
|
//! with `cargo build`, and re-execs the process with the new binary. If the
|
|
|
|
|
//! build fails the error is reported back to the room and the server keeps
|
|
|
|
|
//! running.
|
|
|
|
|
|
|
|
|
|
use crate::agents::AgentPool;
|
2026-03-28 18:33:22 +00:00
|
|
|
use crate::chat::util::strip_bot_mention;
|
2026-03-22 19:07:07 +00:00
|
|
|
use std::path::Path;
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
|
|
|
|
/// A parsed rebuild command.
|
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
|
|
|
pub struct RebuildCommand;
|
|
|
|
|
|
|
|
|
|
/// Parse a rebuild command from a raw message body.
|
|
|
|
|
///
|
|
|
|
|
/// Strips the bot mention prefix and checks whether the command word is
|
|
|
|
|
/// `rebuild`. Returns `None` when the message is not a rebuild command.
|
|
|
|
|
pub fn extract_rebuild_command(
|
|
|
|
|
message: &str,
|
|
|
|
|
bot_name: &str,
|
|
|
|
|
bot_user_id: &str,
|
|
|
|
|
) -> Option<RebuildCommand> {
|
2026-03-28 18:33:22 +00:00
|
|
|
let stripped = strip_bot_mention(message, bot_name, bot_user_id);
|
2026-03-22 19:07:07 +00:00
|
|
|
let trimmed = stripped
|
|
|
|
|
.trim()
|
|
|
|
|
.trim_start_matches(|c: char| !c.is_alphanumeric());
|
|
|
|
|
|
|
|
|
|
let cmd = match trimmed.split_once(char::is_whitespace) {
|
|
|
|
|
Some((c, _)) => c,
|
|
|
|
|
None => trimmed,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if cmd.eq_ignore_ascii_case("rebuild") {
|
|
|
|
|
Some(RebuildCommand)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Handle a rebuild command: trigger server rebuild and restart.
|
|
|
|
|
///
|
|
|
|
|
/// Returns a string describing the outcome. On build failure the error
|
|
|
|
|
/// message is returned so it can be posted to the room; the server keeps
|
|
|
|
|
/// running. On success this function never returns (the process re-execs).
|
|
|
|
|
pub async fn handle_rebuild(
|
|
|
|
|
bot_name: &str,
|
|
|
|
|
project_root: &Path,
|
|
|
|
|
agents: &Arc<AgentPool>,
|
|
|
|
|
) -> String {
|
|
|
|
|
crate::slog!("[matrix-bot] rebuild command received (bot={bot_name})");
|
2026-03-22 19:08:41 +00:00
|
|
|
match crate::rebuild::rebuild_and_restart(agents, project_root, None).await {
|
2026-03-22 19:07:07 +00:00
|
|
|
Ok(msg) => msg,
|
|
|
|
|
Err(e) => format!("Rebuild failed: {e}"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Tests
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn extract_with_display_name() {
|
|
|
|
|
let cmd = extract_rebuild_command("Timmy rebuild", "Timmy", "@timmy:home.local");
|
|
|
|
|
assert_eq!(cmd, Some(RebuildCommand));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn extract_with_full_user_id() {
|
2026-04-13 14:07:08 +00:00
|
|
|
let cmd =
|
|
|
|
|
extract_rebuild_command("@timmy:home.local rebuild", "Timmy", "@timmy:home.local");
|
2026-03-22 19:07:07 +00:00
|
|
|
assert_eq!(cmd, Some(RebuildCommand));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn extract_with_localpart() {
|
|
|
|
|
let cmd = extract_rebuild_command("@timmy rebuild", "Timmy", "@timmy:home.local");
|
|
|
|
|
assert_eq!(cmd, Some(RebuildCommand));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn extract_case_insensitive() {
|
|
|
|
|
let cmd = extract_rebuild_command("Timmy REBUILD", "Timmy", "@timmy:home.local");
|
|
|
|
|
assert_eq!(cmd, Some(RebuildCommand));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn extract_non_rebuild_returns_none() {
|
|
|
|
|
let cmd = extract_rebuild_command("Timmy help", "Timmy", "@timmy:home.local");
|
|
|
|
|
assert_eq!(cmd, None);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn extract_ignores_extra_args() {
|
|
|
|
|
// "rebuild" with trailing text is still a rebuild command
|
|
|
|
|
let cmd = extract_rebuild_command("Timmy rebuild now", "Timmy", "@timmy:home.local");
|
|
|
|
|
assert_eq!(cmd, Some(RebuildCommand));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn extract_no_match_returns_none() {
|
|
|
|
|
let cmd = extract_rebuild_command("Timmy status", "Timmy", "@timmy:home.local");
|
|
|
|
|
assert_eq!(cmd, None);
|
|
|
|
|
}
|
|
|
|
|
}
|