use crate::git::run_command;
use crate::models::{Account, Platform};
use colored::Colorize;
use std::fs;
use std::io;
use std::path::PathBuf;
use std::process::{Command, Output};
pub fn get_ssh_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("~"))
.join(".ssh")
}
pub fn update_ssh_config(accounts: &[Account]) -> io::Result<()> {
let ssh_dir = get_ssh_dir();
fs::create_dir_all(&ssh_dir)?;
let config_path = ssh_dir.join("config");
const START_MARKER: &str = "# >>> gitcore managed block >>>";
const END_MARKER: &str = "# <<< gitcore managed block <<<";
let mut managed_block = String::new();
managed_block.push_str("# Generated by gitcore - DO NOT EDIT MANUALLY\n");
managed_block.push_str(START_MARKER);
managed_block.push('\n');
for acc in accounts {
let key_full_path = ssh_dir.join(&acc.key_path);
let key_path_str = key_full_path.to_string_lossy().replace('\\', "/");
managed_block.push_str(&format!("Host {}\n", acc.host_alias));
managed_block.push_str(&format!(" HostName {}\n", acc.platform.host()));
managed_block.push_str(" User git\n");
managed_block.push_str(&format!(" IdentityFile {}\n", key_path_str));
managed_block.push_str(" AddKeysToAgent yes\n");
managed_block.push_str(" IdentitiesOnly yes\n\n");
}
managed_block.push_str(END_MARKER);
managed_block.push('\n');
let existing = fs::read_to_string(&config_path).unwrap_or_default();
let new_content = if let Some(start) = existing.find(START_MARKER) {
if let Some(end_rel) = existing[start..].find(END_MARKER) {
let end = start + end_rel + END_MARKER.len();
let before = existing[..start].trim_end();
let after = existing[end..].trim_start();
let mut merged = String::new();
if !before.is_empty() {
merged.push_str(before);
merged.push_str("\n\n");
}
merged.push_str(&managed_block);
if !after.is_empty() {
merged.push('\n');
merged.push_str(after);
if !merged.ends_with('\n') {
merged.push('\n');
}
}
merged
} else {
format!("{}\n{}", existing.trim_end(), managed_block)
}
} else if existing.trim().is_empty() {
managed_block.clone()
} else {
format!("{}\n\n{}", existing.trim_end(), managed_block)
};
fs::write(&config_path, new_content)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(&config_path, fs::Permissions::from_mode(0o600));
}
println!("{}", "Success: SSH config updated".green());
Ok(())
}
pub fn generate_ssh_key(key_path: &str, email: &str, passphrase: &str) -> io::Result<String> {
let ssh_dir = get_ssh_dir();
let key_full = ssh_dir.join(key_path);
if key_full.exists() {
println!("{}", "Key already exists, using existing key".yellow());
} else {
println!("{}", "[*] Generating SSH key...".cyan());
run_command(
"ssh-keygen",
&[
"-t",
"ed25519",
"-f",
key_full.to_str().unwrap(),
"-N",
passphrase,
"-C",
email,
],
)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(&key_full, fs::Permissions::from_mode(0o600));
let pub_key_path = ssh_dir.join(format!("{}.pub", key_path));
let _ = fs::set_permissions(&pub_key_path, fs::Permissions::from_mode(0o644));
}
println!("{}", "Success: SSH key generated".green());
if passphrase.is_empty() {
println!(
"{}",
"Warning: No passphrase - key is not protected".yellow()
);
} else {
println!("{}", "Success: Key is protected with passphrase".green());
}
}
let pub_key_path = ssh_dir.join(format!("{}.pub", key_path));
let pub_key = fs::read_to_string(pub_key_path)?;
Ok(pub_key)
}
pub fn delete_account_keys(name: &str) -> io::Result<Vec<PathBuf>> {
let ssh_dir = get_ssh_dir();
let key_name = format!("id_ed25519_{}", name);
let paths = [
ssh_dir.join(&key_name),
ssh_dir.join(format!("{}.pub", key_name)),
];
let mut deleted = Vec::new();
for path in paths {
if path.exists() {
fs::remove_file(&path)?;
deleted.push(path);
}
}
Ok(deleted)
}
pub fn test_ssh_connection(host_alias: &str) -> io::Result<Output> {
Command::new("ssh")
.args([
"-o",
"BatchMode=yes",
"-o",
"ConnectTimeout=10",
"-o",
"StrictHostKeyChecking=accept-new",
"-T",
host_alias,
])
.output()
}
pub fn check_host_key(platform_host: &str) -> HostKeyStatus {
let known_hosts_path = get_ssh_dir().join("known_hosts");
if !known_hosts_path.exists() {
return HostKeyStatus::Unknown;
}
match fs::read_to_string(&known_hosts_path) {
Ok(content) => {
if content.contains(platform_host) {
HostKeyStatus::Known
} else {
HostKeyStatus::New
}
}
Err(_) => HostKeyStatus::Unknown,
}
}
#[derive(Debug, Clone, Copy)]
pub enum HostKeyStatus {
Known,
New,
Unknown,
}
pub fn provider_key_url(platform: &Platform) -> &'static str {
match platform {
Platform::Github => "https://github.com/settings/keys",
Platform::Gitlab => "https://gitlab.com/-/profile/keys",
Platform::Codeberg => "https://codeberg.org/user/keys",
Platform::Bitbucket => "https://bitbucket.org/account/settings/ssh-keys/",
}
}