use crate::api::kasl_server::{AGENT_TOKEN_PROMPT, AGENT_TOKEN_SECRET, KaslServer, normalize_url};
use crate::libs::config::{Config, KaslServerConfig};
use crate::libs::messages::Message;
use crate::libs::secret::Secret;
use crate::{msg_error_anyhow, msg_info, msg_print, msg_success, msg_warning};
use anyhow::{Context, Result};
use clap::{Args, Subcommand};
use dialoguer::{Input, Password, theme::ColorfulTheme};
#[derive(Debug, Args)]
pub struct ServerArgs {
#[command(subcommand)]
command: ServerCommand,
}
#[derive(Debug, Subcommand)]
enum ServerCommand {
#[command(about = "Connect this machine to a kasl-server")]
Connect(ConnectArgs),
#[command(about = "Show the current connection to a kasl-server")]
Status,
#[command(about = "Forget the connection and the stored agent token")]
Disconnect,
}
#[derive(Debug, Args)]
pub struct ConnectArgs {
#[arg(long, value_name = "URL")]
url: Option<String>,
#[arg(long, value_name = "PATH")]
ca_certificate: Option<String>,
}
pub async fn cmd(args: ServerArgs) -> Result<()> {
match args.command {
ServerCommand::Connect(args) => connect(args).await,
ServerCommand::Status => status().await,
ServerCommand::Disconnect => disconnect(),
}
}
async fn connect(args: ConnectArgs) -> Result<()> {
crate::libs::prompt::ensure_interactive("`kasl server connect` needs a terminal to ask for the agent token")?;
let mut config = Config::read().unwrap_or_default();
let url = match args.url {
Some(url) => normalize_url(&url),
None => {
let entered: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt(Message::PromptKaslServerUrl.to_string())
.with_initial_text(config.kasl_server.as_ref().map(|s| s.url.clone()).unwrap_or_default())
.interact_text()?;
normalize_url(&entered)
}
};
if !url.starts_with("http://") && !url.starts_with("https://") {
return Err(msg_error_anyhow!(Message::KaslServerUrlNeedsScheme(url)));
}
let candidate = KaslServerConfig {
url: url.clone(),
ca_certificate: args
.ca_certificate
.or_else(|| config.kasl_server.as_ref().and_then(|s| s.ca_certificate.clone())),
};
let client = KaslServer::new(&candidate)?;
let health = client.health().await?;
msg_info!(Message::KaslServerReached {
url: url.clone(),
version: health.version.clone(),
});
if health.database != "ok" {
msg_warning!(Message::KaslServerDatabaseUnhealthy(health.database.clone()));
}
let secret = Secret::new(AGENT_TOKEN_SECRET, AGENT_TOKEN_PROMPT);
let token: String = Password::with_theme(&ColorfulTheme::default())
.with_prompt(Message::PromptKaslServerToken.to_string())
.interact()?;
let token = token.trim().to_string();
if token.is_empty() {
return Err(msg_error_anyhow!(Message::KaslServerTokenEmpty));
}
let identity = client.identify(&token).await?;
secret
.store(&token)
.context("the token was accepted but could not be stored in the OS keyring")?;
config.kasl_server = Some(candidate);
config.save()?;
msg_success!(Message::KaslServerConnected {
user_name: identity.user_name,
agent_name: identity.agent_name,
});
Ok(())
}
async fn status() -> Result<()> {
let config = Config::read().unwrap_or_default();
let Some(server_config) = config.kasl_server else {
msg_print!(Message::KaslServerNotConnected);
return Ok(());
};
msg_info!(Message::KaslServerConfigured(server_config.url.clone()));
let secret = Secret::new(AGENT_TOKEN_SECRET, AGENT_TOKEN_PROMPT);
let Some(token) = secret.try_get_cached() else {
msg_warning!(Message::KaslServerTokenMissing);
return Ok(());
};
let client = KaslServer::new(&server_config)?;
match client.health().await {
Ok(health) => msg_info!(Message::KaslServerReached {
url: server_config.url.clone(),
version: health.version,
}),
Err(error) => {
msg_warning!(Message::KaslServerUnreachable(error.to_string()));
return Ok(());
}
}
match client.identify(&token).await {
Ok(identity) => msg_success!(Message::KaslServerConnected {
user_name: identity.user_name,
agent_name: identity.agent_name,
}),
Err(error) => msg_warning!(Message::KaslServerTokenRejected(error.to_string())),
}
Ok(())
}
fn disconnect() -> Result<()> {
let mut config = Config::read().unwrap_or_default();
if let Err(error) = Secret::new(AGENT_TOKEN_SECRET, AGENT_TOKEN_PROMPT).delete() {
msg_warning!(Message::KaslServerTokenNotRemoved(error.to_string()));
}
if config.kasl_server.take().is_some() {
config.save()?;
msg_success!(Message::KaslServerDisconnected);
} else {
msg_print!(Message::KaslServerNotConnected);
}
Ok(())
}