169 lines
4.9 KiB
Rust
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(())
|
|
}
|