#![allow(clippy::print_stdout)]
use crate::cli::{GpgKeyCommand, SshKeyCommand};
use crate::client::Client;
use crate::output::print_json;
use anyhow::Context;
use std::fs;
pub fn run_ssh(cmd: SshKeyCommand) -> anyhow::Result<()> {
match cmd {
SshKeyCommand::List { json, hostname } => list_ssh_keys(json, hostname.as_deref()),
SshKeyCommand::Add {
title,
file,
body,
hostname,
} => add_ssh_key(
&title,
file.as_deref(),
body.as_deref(),
hostname.as_deref(),
),
SshKeyCommand::Delete {
key_id,
yes,
hostname,
} => delete_ssh_key(&key_id, yes, hostname.as_deref()),
}
}
pub fn run_gpg(cmd: GpgKeyCommand) -> anyhow::Result<()> {
match cmd {
GpgKeyCommand::List { json, hostname } => list_gpg_keys(json, hostname.as_deref()),
GpgKeyCommand::Add {
file,
body,
hostname,
} => add_gpg_key(file.as_deref(), body.as_deref(), hostname.as_deref()),
GpgKeyCommand::Delete {
key_id,
yes,
hostname,
} => delete_gpg_key(&key_id, yes, hostname.as_deref()),
}
}
fn list_ssh_keys(json: Option<Vec<String>>, hostname: Option<&str>) -> anyhow::Result<()> {
let host = hostname.unwrap_or("github.com");
let client = Client::new(host).context("failed to create HTTP client")?;
let response = client
.get("/user/keys")
.context("failed to fetch SSH keys")?;
let status = response.status();
if !status.is_success() {
anyhow::bail!("failed to list SSH keys: HTTP {status}");
}
let keys: Vec<serde_json::Value> = response
.json()
.context("failed to parse SSH keys response")?;
if let Some(fields) = json {
let fields_ref: Option<&[String]> = if fields.is_empty() {
None
} else {
Some(&fields)
};
print_json(&keys, fields_ref);
return Ok(());
}
if keys.is_empty() {
println!("No SSH keys found.");
return Ok(());
}
let title_width = 30;
let type_width = 10;
println!("{:<title_width$} {:<type_width$} KEY", "TITLE", "TYPE");
for key in &keys {
let title = key["title"].as_str().unwrap_or("—");
let key_type = key["key"]
.as_str()
.unwrap_or("")
.split(' ')
.next()
.unwrap_or("—");
let key_short = key["key"]
.as_str()
.unwrap_or("")
.split(' ')
.nth(1)
.unwrap_or("")
.chars()
.take(40)
.collect::<String>();
let title_truncated = crate::cmd::util::truncate(title, title_width);
println!("{title_truncated:<title_width$} {key_type:<type_width$} {key_short}");
}
Ok(())
}
fn add_ssh_key(
title: &str,
file: Option<&str>,
body: Option<&str>,
hostname: Option<&str>,
) -> anyhow::Result<()> {
let key_body = if let Some(b) = body {
b.to_string()
} else if let Some(f) = file {
fs::read_to_string(f)
.with_context(|| format!("failed to read SSH key file: {f}"))?
.trim()
.to_string()
} else {
anyhow::bail!("no SSH key provided (use --file or --body)");
};
if key_body.is_empty() {
anyhow::bail!("SSH key body is empty");
}
let host = hostname.unwrap_or("github.com");
let client = Client::new(host).context("failed to create HTTP client")?;
let body_value = serde_json::json!({
"title": title,
"key": key_body,
});
let response = client
.post("/user/keys", &body_value)
.context("failed to add SSH key")?;
let status = response.status();
if !status.is_success() {
let err_body: serde_json::Value = response.json().unwrap_or_default();
let msg = err_body["message"].as_str().unwrap_or("add failed");
anyhow::bail!("failed to add SSH key: {msg}");
}
let result: serde_json::Value = response.json().context("failed to parse response")?;
let key_id = result["id"].as_u64().unwrap_or(0);
println!("SSH key added: {key_id} ({title})");
Ok(())
}
fn delete_ssh_key(key_id: &str, yes: bool, hostname: Option<&str>) -> anyhow::Result<()> {
if !yes {
use std::io::Write;
print!("Are you sure you want to delete SSH key '{key_id}'? [y/N] ");
std::io::stdout().flush().ok();
let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.context("failed to read input")?;
let input = input.trim().to_lowercase();
if input != "y" && input != "yes" {
println!("Cancelled.");
return Ok(());
}
}
let host = hostname.unwrap_or("github.com");
let client = Client::new(host).context("failed to create HTTP client")?;
let path = format!("/user/keys/{key_id}");
let response = client
.request("DELETE", &path, &[], None)
.context("failed to delete SSH key")?;
let status = response.status();
if !status.is_success() {
let err_body: serde_json::Value = response.json().unwrap_or_default();
let msg = err_body["message"].as_str().unwrap_or("delete failed");
anyhow::bail!("failed to delete SSH key '{key_id}': {msg}");
}
println!("SSH key '{key_id}' deleted.");
Ok(())
}
fn list_gpg_keys(json: Option<Vec<String>>, hostname: Option<&str>) -> anyhow::Result<()> {
let host = hostname.unwrap_or("github.com");
let client = Client::new(host).context("failed to create HTTP client")?;
let response = client
.get("/user/gpg_keys")
.context("failed to fetch GPG keys")?;
let status = response.status();
if !status.is_success() {
anyhow::bail!("failed to list GPG keys: HTTP {status}");
}
let keys: Vec<serde_json::Value> = response
.json()
.context("failed to parse GPG keys response")?;
if let Some(fields) = json {
let fields_ref: Option<&[String]> = if fields.is_empty() {
None
} else {
Some(&fields)
};
print_json(&keys, fields_ref);
return Ok(());
}
if keys.is_empty() {
println!("No GPG keys found.");
return Ok(());
}
let id_width = 16;
let name_width = 30;
println!("{:<id_width$} {:<name_width$} EMAILS", "KEY ID", "NAME");
for key in &keys {
let key_id = key["key_id"].as_str().unwrap_or("—");
let name = key["name"].as_str().unwrap_or("—");
let emails: Vec<String> = key["emails"]
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|e| e["email"].as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let name_truncated = crate::cmd::util::truncate(name, name_width);
let emails_str = emails.join(", ");
println!("{key_id:<id_width$} {name_truncated:<name_width$} {emails_str}");
}
Ok(())
}
fn add_gpg_key(
file: Option<&str>,
body: Option<&str>,
hostname: Option<&str>,
) -> anyhow::Result<()> {
let armored_key = if let Some(b) = body {
b.to_string()
} else if let Some(f) = file {
fs::read_to_string(f)
.with_context(|| format!("failed to read GPG key file: {f}"))?
.trim()
.to_string()
} else {
anyhow::bail!("no GPG key provided (use --file or --body)");
};
if armored_key.is_empty() {
anyhow::bail!("GPG key body is empty");
}
let host = hostname.unwrap_or("github.com");
let client = Client::new(host).context("failed to create HTTP client")?;
let body_value = serde_json::json!({
"armored_public_key": armored_key,
});
let response = client
.post("/user/gpg_keys", &body_value)
.context("failed to add GPG key")?;
let status = response.status();
if !status.is_success() {
let err_body: serde_json::Value = response.json().unwrap_or_default();
let msg = err_body["message"].as_str().unwrap_or("add failed");
anyhow::bail!("failed to add GPG key: {msg}");
}
let result: serde_json::Value = response.json().context("failed to parse response")?;
let key_id = result["key_id"].as_str().unwrap_or("—");
println!("GPG key added: {key_id}");
Ok(())
}
fn delete_gpg_key(key_id: &str, yes: bool, hostname: Option<&str>) -> anyhow::Result<()> {
if !yes {
use std::io::Write;
print!("Are you sure you want to delete GPG key '{key_id}'? [y/N] ");
std::io::stdout().flush().ok();
let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.context("failed to read input")?;
let input = input.trim().to_lowercase();
if input != "y" && input != "yes" {
println!("Cancelled.");
return Ok(());
}
}
let host = hostname.unwrap_or("github.com");
let client = Client::new(host).context("failed to create HTTP client")?;
let path = format!("/user/gpg_keys/{key_id}");
let response = client
.request("DELETE", &path, &[], None)
.context("failed to delete GPG key")?;
let status = response.status();
if !status.is_success() {
let err_body: serde_json::Value = response.json().unwrap_or_default();
let msg = err_body["message"].as_str().unwrap_or("delete failed");
anyhow::bail!("failed to delete GPG key '{key_id}': {msg}");
}
println!("GPG key '{key_id}' deleted.");
Ok(())
}