iota/iota-ipc/src/text_commands.rs
2026-07-25 18:23:36 +02:00

324 lines
9.1 KiB
Rust

use crate::LocalRequest;
pub const COMMANDS: &[&str] = &[
"status",
"tasks",
"users list",
"users show ",
"users add ",
"users remove ",
"users import ",
"omikron status",
"reconnect",
"identity rotate",
"daemon status",
"config get",
"config set ",
"config reload",
"components",
"logs",
"update check",
"community list",
"restart",
"stop",
];
pub fn completions(prefix: &str) -> Vec<&'static str> {
let normalized = prefix.trim_start_matches('/');
COMMANDS
.iter()
.copied()
.filter(|command| command.starts_with(normalized))
.collect()
}
pub fn validation_error(line: &str) -> Option<String> {
let normalized = line.trim_start_matches('/').trim();
if normalized == "help" || parse(normalized).is_some() {
None
} else {
Some(format!("Unknown command `{normalized}`. Use /help or Tab completion."))
}
}
/// Parse a text command string into a typed IPC request.
///
/// Both the CLI console and the TUI command palette use this single parser.
/// Commands are case-insensitive and support an optional leading `/`.
pub fn parse(line: &str) -> Option<LocalRequest> {
let parts: Vec<&str> = line.trim_start_matches('/').split_whitespace().collect();
match parts.as_slice() {
["help"] => None,
["status"] => Some(LocalRequest::GetStatus),
["tasks"] => Some(LocalRequest::ListTasks),
["users"] | ["user", "list"] | ["users", "list"] => Some(LocalRequest::ListUsers),
["user" | "users", "show", id_str] => {
let user_id = id_str.parse::<i64>().ok()?;
Some(LocalRequest::GetUser { user_id })
}
["user" | "users", "add", username] => Some(LocalRequest::CreateUser {
username: username.to_string(),
}),
["user" | "users", "remove", id_str] => {
let user_id = id_str.parse::<i64>().ok()?;
Some(LocalRequest::RemoveUser { user_id })
}
["user" | "users", "import", username] => Some(LocalRequest::ImportUser {
username: username.to_string(),
}),
["reconnect"] => Some(LocalRequest::ReconnectOmikron),
["regenerate", "keys"] | ["identity", "rotate"] => Some(LocalRequest::RotateIotaIdentity),
["reload"] | ["restart"] => Some(LocalRequest::RequestProcessExit {
intent: crate::ExitIntent::Restart,
}),
["shutdown"] | ["stop"] => Some(LocalRequest::RequestProcessExit {
intent: crate::ExitIntent::Stop,
}),
["daemon", "status"] => Some(LocalRequest::GetDaemonStatus),
["config", "get"] => Some(LocalRequest::GetConfig),
["config", "set", key, value] => Some(LocalRequest::SetConfig {
key: key.to_string(),
value: value.to_string(),
}),
["config", "reload"] => Some(LocalRequest::ReloadConfig),
["omikron", "status"] => Some(LocalRequest::GetOmikronStatus),
["components"] => Some(LocalRequest::ListComponents),
["logs"] => Some(LocalRequest::GetLogs { limit: 100 }),
["update", "check"] => Some(LocalRequest::CheckUpdate),
["community", "list"] | ["communities"] => Some(LocalRequest::ListCommunities),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_status() {
assert!(matches!(parse("status"), Some(LocalRequest::GetStatus)));
}
#[test]
fn parses_tasks() {
assert!(matches!(parse("tasks"), Some(LocalRequest::ListTasks)));
}
#[test]
fn parses_user_list_shortcuts() {
assert!(matches!(parse("users"), Some(LocalRequest::ListUsers)));
assert!(matches!(parse("user list"), Some(LocalRequest::ListUsers)));
}
#[test]
fn parses_user_add() {
let req = parse("user add alice").unwrap();
match req {
LocalRequest::CreateUser { username } => assert_eq!(username, "alice"),
_ => panic!("expected CreateUser"),
}
}
#[test]
fn accepts_the_headless_cli_user_vocabulary() {
assert!(matches!(
parse("users list"),
Some(LocalRequest::ListUsers)
));
assert!(matches!(
parse("users add alice"),
Some(LocalRequest::CreateUser { .. })
));
assert!(matches!(
parse("users remove 42"),
Some(LocalRequest::RemoveUser { user_id: 42 })
));
assert!(matches!(
parse("identity rotate"),
Some(LocalRequest::RotateIotaIdentity)
));
}
#[test]
fn parses_user_remove_by_id() {
let req = parse("user remove 42").unwrap();
match req {
LocalRequest::RemoveUser { user_id } => assert_eq!(user_id, 42),
_ => panic!("expected RemoveUser"),
}
}
#[test]
fn user_remove_requires_numeric_id() {
assert!(parse("user remove alice").is_none());
}
#[test]
fn parses_reconnect() {
assert!(matches!(
parse("reconnect"),
Some(LocalRequest::ReconnectOmikron)
));
}
#[test]
fn parses_regenerate_keys() {
assert!(matches!(
parse("regenerate keys"),
Some(LocalRequest::RotateIotaIdentity)
));
}
#[test]
fn parses_restart_aliases() {
assert!(matches!(
parse("restart"),
Some(LocalRequest::RequestProcessExit { .. })
));
assert!(matches!(
parse("reload"),
Some(LocalRequest::RequestProcessExit { .. })
));
}
#[test]
fn parses_stop_aliases() {
assert!(matches!(
parse("stop"),
Some(LocalRequest::RequestProcessExit { .. })
));
assert!(matches!(
parse("shutdown"),
Some(LocalRequest::RequestProcessExit { .. })
));
}
#[test]
fn parses_daemon_status() {
assert!(matches!(
parse("daemon status"),
Some(LocalRequest::GetDaemonStatus)
));
}
#[test]
fn parses_config_get() {
assert!(matches!(
parse("config get"),
Some(LocalRequest::GetConfig)
));
}
#[test]
fn parses_config_reload() {
assert!(matches!(
parse("config reload"),
Some(LocalRequest::ReloadConfig)
));
}
#[test]
fn parses_omikron_status() {
assert!(matches!(
parse("omikron status"),
Some(LocalRequest::GetOmikronStatus)
));
}
#[test]
fn parses_components() {
assert!(matches!(
parse("components"),
Some(LocalRequest::ListComponents)
));
}
#[test]
fn parses_users_show() {
let req = parse("users show 42").unwrap();
match req {
LocalRequest::GetUser { user_id } => assert_eq!(user_id, 42),
_ => panic!("expected GetUser"),
}
}
#[test]
fn user_show_requires_numeric_id() {
assert!(parse("users show alice").is_none());
}
#[test]
fn parses_config_set() {
let req = parse("config set port 8080").unwrap();
match req {
LocalRequest::SetConfig { key, value } => {
assert_eq!(key, "port");
assert_eq!(value, "8080");
}
_ => panic!("expected SetConfig"),
}
}
#[test]
fn parses_logs() {
assert!(matches!(parse("logs"), Some(LocalRequest::GetLogs { .. })));
}
#[test]
fn parses_update_check() {
assert!(matches!(
parse("update check"),
Some(LocalRequest::CheckUpdate)
));
}
#[test]
fn parses_community_list() {
assert!(matches!(
parse("community list"),
Some(LocalRequest::ListCommunities)
));
}
#[test]
fn parses_communities_alias() {
assert!(matches!(
parse("communities"),
Some(LocalRequest::ListCommunities)
));
}
#[test]
fn parses_users_import() {
let req = parse("users import alice").unwrap();
match req {
LocalRequest::ImportUser { username } => assert_eq!(username, "alice"),
_ => panic!("expected ImportUser"),
}
}
#[test]
fn parses_with_slash_prefix() {
assert!(matches!(parse("/status"), Some(LocalRequest::GetStatus)));
assert!(matches!(parse("/tasks"), Some(LocalRequest::ListTasks)));
}
#[test]
fn unknown_returns_none() {
assert!(parse("nonexistent").is_none());
}
#[test]
fn completion_is_prefix_based_and_deterministic() {
assert_eq!(completions("identity r"), vec!["identity rotate"]);
assert_eq!(completions("/users a"), vec!["users add "]);
assert!(completions("definitely-unknown").is_empty());
}
#[test]
fn validation_distinguishes_help_and_unknown_commands() {
assert_eq!(validation_error("/help"), None);
assert!(validation_error("status").is_none());
assert!(validation_error("statuz").unwrap().contains("Unknown command"));
}
}