use clap::{CommandFactory, Parser, Subcommand}; #[derive(Subcommand)] enum ServiceCommand { /// Install and enable the systemd user service Install, /// Stop the systemd user service Stop, /// Show the systemd user service status Status, /// Start the systemd user service Start, /// Restart the systemd user service Restart, /// Stop, disable, and remove the systemd user service Uninstall, } #[derive(Parser)] #[command(name = "picobot")] #[command(about = "A CLI chatbot", long_about = None)] #[command(version = "1.1.1")] enum Command { /// Connect to gateway Chat { /// Gateway WebSocket URL (e.g., ws://127.0.0.1:19876/ws) #[arg(long)] gateway_url: Option, /// One-time pairing code; saves the issued client token locally #[arg(long)] pair_code: Option, }, /// Start gateway server Gateway { /// Host to bind to #[arg(long)] host: Option, /// Port to listen on #[arg(long)] port: Option, }, /// Generate a one-time browser pairing code from the local gateway Pair { /// Gateway WebSocket or HTTP URL #[arg(long)] gateway_url: Option, /// Revoke every paired browser and CLI token before issuing the code #[arg(long)] revoke_all: bool, }, /// Manage the PicoBot systemd user service Service { #[command(subcommand)] command: ServiceCommand, }, } #[tokio::main] async fn main() -> Result<(), Box> { let mut cmd = Command::command(); // If no arguments, print help if std::env::args().len() <= 1 { cmd.print_help()?; println!(); return Ok(()); } match Command::parse() { Command::Chat { gateway_url, pair_code, } => { let config = picobot::config::Config::load_default().ok(); let url = gateway_url .or_else(|| config.as_ref().map(|c| c.client.gateway_url.clone())) .unwrap_or_else(|| "ws://127.0.0.1:19876/ws".to_string()); picobot::client::run(&url, pair_code.as_deref()).await?; } Command::Gateway { host, port } => { picobot::gateway::run(host, port).await?; } Command::Pair { gateway_url, revoke_all, } => { let config = picobot::config::Config::load_default().ok(); let url = gateway_url .or_else(|| config.as_ref().map(|c| c.client.gateway_url.clone())) .unwrap_or_else(|| "ws://127.0.0.1:19876/ws".to_string()); let mut endpoint = gateway_api_url(&url, "/api/auth/code")?; if revoke_all { endpoint.query_pairs_mut().append_pair("revoke_all", "true"); } let admin_token_path = picobot::config::get_user_config_dir().join("web_admin_token"); let admin_token = std::fs::read_to_string(&admin_token_path).map_err(|error| { format!( "cannot read local gateway admin token {}: {error}", admin_token_path.display() ) })?; let response = reqwest::Client::new() .post(endpoint) .header("X-Picobot-Admin-Token", admin_token.trim()) .send() .await?; let status = response.status(); let body: serde_json::Value = response.json().await?; if !status.is_success() { return Err(body .get("error") .and_then(serde_json::Value::as_str) .unwrap_or("failed to generate pairing code") .to_string() .into()); } let code = body .get("pairing_code") .and_then(serde_json::Value::as_str) .ok_or("gateway did not return a pairing code")?; println!("Pairing code: {code}"); println!("Expires in 5 minutes and can be used once."); if revoke_all { println!("All existing paired devices were revoked."); } } Command::Service { command } => { let command = match command { ServiceCommand::Install => picobot::service::ServiceCommand::Install, ServiceCommand::Stop => picobot::service::ServiceCommand::Stop, ServiceCommand::Status => picobot::service::ServiceCommand::Status, ServiceCommand::Start => picobot::service::ServiceCommand::Start, ServiceCommand::Restart => picobot::service::ServiceCommand::Restart, ServiceCommand::Uninstall => picobot::service::ServiceCommand::Uninstall, }; picobot::service::execute(command).await?; } } Ok(()) } fn gateway_api_url( gateway_url: &str, path: &str, ) -> Result> { let mut url = reqwest::Url::parse(gateway_url)?; let scheme = match url.scheme() { "ws" => "http", "wss" => "https", "http" => "http", "https" => "https", other => return Err(format!("unsupported gateway URL scheme: {other}").into()), }; url.set_scheme(scheme) .map_err(|_| "failed to set gateway URL scheme")?; url.set_path(path); url.set_query(None); url.set_fragment(None); Ok(url) }