ref:main
use crate::client::Client;
use crate::output;
use clap::{Args, Subcommand};
use serde::Deserialize;
#[derive(Args)]
pub struct SshKeyArgs {
#[command(subcommand)]
pub command: SshKeyCommand,
}
#[derive(Subcommand)]
pub enum SshKeyCommand {
/// List your SSH keys
List,
/// Add an SSH key
Add {
/// Key name/label
#[arg(long)]
name: String,
/// Path to public key file (e.g., ~/.ssh/id_ed25519.pub)
#[arg(long)]
key_file: String,
},
/// Remove an SSH key
Remove {
/// Key ID
id: String,
},
}
#[derive(Debug, Deserialize)]
struct SshKey {
id: Option<String>,
name: Option<String>,
fingerprint: Option<String>,
last_used_at: Option<String>,
inserted_at: Option<String>,
}
pub async fn run(args: SshKeyArgs) -> Result<(), Box<dyn std::error::Error>> {
match args.command {
SshKeyCommand::List => list().await,
SshKeyCommand::Add { name, key_file } => add(&name, &key_file).await,
SshKeyCommand::Remove { id } => remove(&id).await,
}
}
async fn list() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let resp: serde_json::Value = client.get("/user/ssh-keys").await?;
let keys: Vec<SshKey> = resp
.get("ssh_keys")
.and_then(|v| serde_json::from_value(v.clone()).ok())
.unwrap_or_default();
output::header("SSH Keys");
let rows: Vec<Vec<String>> = keys
.iter()
.map(|k| {
vec![
k.id.clone().unwrap_or_default(),
k.name.clone().unwrap_or_default(),
k.fingerprint.clone().unwrap_or_default(),
k.last_used_at
.as_deref()
.map(output::format_time)
.unwrap_or("never".to_string()),
k.inserted_at
.as_deref()
.map(output::format_time)
.unwrap_or_default(),
]
})
.collect();
output::print_table(&["ID", "NAME", "FINGERPRINT", "LAST USED", "ADDED"], &rows);
Ok(())
}
async fn add(name: &str, key_file: &str) -> Result<(), Box<dyn std::error::Error>> {
let public_key = std::fs::read_to_string(key_file)
.map_err(|e| format!("Failed to read key file '{}': {}", key_file, e))?;
let client = Client::from_config()?;
let resp: serde_json::Value = client
.post(
"/user/ssh-keys",
&serde_json::json!({"name": name, "public_key": public_key.trim()}),
)
.await?;
let fingerprint = resp
.get("fingerprint")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
output::success(&format!("Added SSH key '{name}'"));
output::detail("Fingerprint", fingerprint);
Ok(())
}
async fn remove(id: &str) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
client.delete_empty(&format!("/user/ssh-keys/{id}")).await?;
output::success(&format!("Removed SSH key {id}"));
Ok(())
}