Files
sshell/src/cli.rs
T

169 lines
4.9 KiB
Rust

use crate::config::{ConnectionType, CredentialEntry, SshellConfig, SyncBackend, config_path, find_binary};
use crate::sync;
use crate::{connection, import, ui};
use anyhow::{Context, Result, bail};
use clap::{Parser, Subcommand};
use std::path::Path;
#[derive(Debug, Parser)]
#[command(
name = "sshell",
version,
about = "Personal SSH and shell connection manager"
)]
pub struct Cli {
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Debug, Subcommand)]
enum Command {
Tui,
Connect {
name: String,
},
Import,
Sync,
Doctor {
name: Option<String>,
},
ConfigPath,
}
pub fn run() -> Result<()> {
let cli = Cli::parse();
match cli.command.unwrap_or(Command::Tui) {
Command::Tui => ui::app::run(),
Command::Connect { name } => {
let cfg = SshellConfig::load()?;
connection::connect(&name, &cfg)
}
Command::Import => {
let mut cfg = SshellConfig::load()?;
let candidates = import::load_candidates(&cfg)?;
let count = import::import_candidates(&mut cfg, &candidates)?;
println!("imported {count} connections");
Ok(())
}
Command::Sync => run_sync(),
Command::Doctor { name } => doctor(name),
Command::ConfigPath => {
println!("{}", config_path()?.display());
Ok(())
}
}
}
fn run_sync() -> Result<()> {
let mut cfg = SshellConfig::load()?;
let report = sync::run_sync(&mut cfg)?;
println!("{report}");
Ok(())
}
fn doctor(name: Option<String>) -> Result<()> {
let path = config_path()?;
println!("config: {}", path.display());
if path.exists() {
check_config_permissions(&path)?;
} else {
println!("config status: missing, will be created on first run");
}
let cfg = SshellConfig::load()?;
println!("connections: {}", cfg.connections.len());
println!("credentials: {}", cfg.credentials.entries.len());
let synced = cfg.connections.values().filter(|p| p.sync()).count();
println!("synced connections: {synced}");
println!(
"sync backend: {}",
match cfg.settings.backend {
SyncBackend::Gist => "gist",
SyncBackend::Webdav => "webdav",
}
);
println!(
"ssh binary: {}",
find_binary("ssh").unwrap_or_else(|| "not found".to_string())
);
println!(
"sshpass binary: {}",
find_binary("sshpass").unwrap_or_else(|| "not found".to_string())
);
if let Some(name) = name {
let profile = cfg
.connections
.get(&name)
.with_context(|| format!("connection {name} not found"))?;
check_connection(&cfg, &name, profile)?;
}
Ok(())
}
fn check_connection(
cfg: &SshellConfig,
name: &str,
profile: &crate::config::ConnectionProfile,
) -> Result<()> {
println!("connection: {name}");
match &profile.kind {
ConnectionType::Ssh {
host,
port,
user,
auth_ref,
sync,
} => {
println!("type: ssh");
println!("target: {user}@{host}:{port}");
println!("sync: {}", if *sync { "yes" } else { "no" });
println!("credential: {auth_ref}");
let credential = cfg
.credential(auth_ref)
.with_context(|| format!("credential {auth_ref} missing"))?;
match credential {
CredentialEntry::Password { .. } => println!("auth: password"),
CredentialEntry::PrivateKey { value, .. } => {
if value.as_deref().is_some_and(|v| !v.is_empty()) {
println!(
"auth: embedded private key ({} bytes)",
value.as_deref().unwrap_or_default().len()
);
} else {
bail!("private key credential is empty");
}
}
}
println!(
"ssh command: ssh -o StrictHostKeyChecking=accept-new -p {port} {user}@{host}"
);
}
ConnectionType::Shell {
command,
..
} => {
println!("type: shell");
let merged_args = profile.merged_shell_args();
println!("command: {command} {}", merged_args.join(" "));
}
}
Ok(())
}
#[cfg(unix)]
fn check_config_permissions(path: &Path) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let mode = path.metadata()?.permissions().mode() & 0o777;
println!("config permissions: {mode:o}");
if mode != 0o600 {
println!("warning: config should be 600; saving from sshell will fix it");
}
Ok(())
}
#[cfg(not(unix))]
fn check_config_permissions(_path: &Path) -> Result<()> {
println!("config permissions: not checked on this platform");
Ok(())
}