toride_ssh/lib.rs
1#![warn(missing_docs)]
2#![allow(dead_code, reason = "scaffolding for modules under active development")]
3#![expect(
4 clippy::must_use_candidate,
5 reason = "service methods are call-and-forget; callers rarely use return"
6)]
7#![expect(
8 clippy::doc_markdown,
9 reason = "SSH-specific terms like ed25519 trigger false positives"
10)]
11
12//! `toride-ssh` — async SSH manager library.
13//!
14//! This is the **facade crate** that re-exports all sub-crates behind a
15//! unified API. The entry point is [`SshManager`], which resolves `~/.ssh`
16//! paths and provides accessor methods for each subsystem.
17//!
18//! # Subsystem accessors
19//!
20//! - [`SshManager::keys`] — key generation, inventory, repair
21//! - [`SshManager::config`] — config parsing, editing, resolution
22//! - [`SshManager::agent`] — SSH agent interaction *(feature: `agent`)*
23//! - [`SshManager::authorized_keys`] — authorized_keys management *(feature: `authorized-keys`)*
24//! - [`SshManager::doctor`] — diagnostic checks *(feature: `doctor`)*
25//! - [`SshManager::known_hosts`] — known_hosts management *(feature: `known-hosts`)*
26//! - [`SshManager::forward`] — port forwarding via ControlMaster *(feature: `forward`)*
27//! - [`SshManager::certificate`] — SSH certificate/CA operations *(feature: `certificate`)*
28//!
29//! # Testing with a custom CLI runner
30//!
31//! Use [`SshManager::with_cli_runner`] to inject a [`MockCliRunner`] so that
32//! no real SSH processes are spawned during tests.
33
34// Re-export core types (Error, Result, SshKey, CliRunner, etc.)
35pub use toride_ssh_core::*;
36
37// Re-export config subsystem
38pub use toride_ssh_config as config;
39
40// Re-export key subsystem
41pub use toride_ssh_key as key;
42
43// Feature-gated subsystem re-exports
44#[cfg(feature = "agent")]
45pub use toride_ssh_agent as agent;
46#[cfg(feature = "authorized-keys")]
47pub use toride_ssh_authorized_keys as authorized_keys;
48#[cfg(feature = "certificate")]
49pub use toride_ssh_certificate as certificate;
50#[cfg(feature = "doctor")]
51pub use toride_ssh_doctor as doctor;
52#[cfg(feature = "forward")]
53pub use toride_ssh_forward as forward;
54#[cfg(feature = "known-hosts")]
55pub use toride_ssh_known_hosts as known_hosts;
56
57use std::sync::Arc;
58
59/// Entry point for all SSH management operations.
60///
61/// `SshManager` is cheaply [`Clone`]-able and safe to share
62/// across async tasks. Each subsystem is accessed via a dedicated
63/// accessor method (e.g. [`keys()`](Self::keys), [`config()`](Self::config)).
64///
65/// # Examples
66///
67/// ```rust,no_run
68/// use toride_ssh::SshManager;
69///
70/// # async fn example() -> toride_ssh::Result<()> {
71/// let mgr = SshManager::new()?;
72/// let keys = mgr.keys().list().await?;
73/// println!("Found {} keys", keys.len());
74/// # Ok(())
75/// # }
76/// ```
77#[derive(Clone)]
78pub struct SshManager {
79 paths: SshPaths,
80 runner: Arc<dyn CliRunner>,
81}
82
83impl Default for SshManager {
84 /// Return a best-effort manager using [`DefaultCliRunner`].
85 ///
86 /// Falls back to `~/.ssh` if the home directory is unavailable.
87 fn default() -> Self {
88 Self {
89 paths: SshPaths::default(),
90 runner: Arc::new(DefaultCliRunner),
91 }
92 }
93}
94
95impl SshManager {
96 /// Create a new manager resolving `~/.ssh` from the user's home directory.
97 ///
98 /// Uses [`DefaultCliRunner`] for all CLI operations. For tests, prefer
99 /// [`with_cli_runner`](Self::with_cli_runner) with a [`MockCliRunner`].
100 ///
101 /// # Errors
102 ///
103 /// Returns [`Error::HomeNotFound`] if the user's home directory cannot
104 /// be resolved.
105 pub fn new() -> Result<Self> {
106 let paths = SshPaths::new()?;
107 Ok(Self {
108 paths,
109 runner: Arc::new(DefaultCliRunner),
110 })
111 }
112
113 /// Create a manager with a custom [`CliRunner`].
114 ///
115 /// This is the primary injection point for tests: pass a
116 /// [`MockCliRunner`] to control command execution without spawning
117 /// real SSH processes.
118 ///
119 /// # Errors
120 ///
121 /// Returns [`Error::HomeNotFound`] if the user's home directory cannot
122 /// be resolved.
123 ///
124 /// # Examples
125 ///
126 /// ```rust
127 /// use std::sync::Arc;
128 /// use toride_ssh::{SshManager, MockCliRunner};
129 ///
130 /// # fn example() -> toride_ssh::Result<()> {
131 /// let mock = Arc::new(MockCliRunner::new());
132 /// mock.set_tool_exists("ssh-keygen", true);
133 /// let mgr = SshManager::with_cli_runner(mock)?;
134 /// # Ok(())
135 /// # }
136 /// ```
137 pub fn with_cli_runner(runner: Arc<dyn CliRunner>) -> Result<Self> {
138 let paths = SshPaths::new()?;
139 Ok(Self { paths, runner })
140 }
141
142 /// Key management operations.
143 pub fn keys(&self) -> key::KeyService<'_> {
144 key::KeyService::new(&self.paths, &*self.runner)
145 }
146
147 /// SSH config operations.
148 pub fn config(&self) -> config::ConfigService<'_> {
149 config::ConfigService::new(&self.paths)
150 }
151
152 /// SSH agent operations.
153 #[cfg(feature = "agent")]
154 pub fn agent(&self) -> agent::AgentService<'_> {
155 agent::AgentService::new(&self.paths, &*self.runner)
156 }
157
158 /// `authorized_keys` management (listing, adding, removing keys).
159 #[cfg(feature = "authorized-keys")]
160 pub fn authorized_keys(&self) -> authorized_keys::AuthorizedKeysService<'_> {
161 authorized_keys::AuthorizedKeysService::new(&self.paths)
162 }
163
164 /// Diagnostic checks.
165 #[cfg(feature = "doctor")]
166 pub fn doctor(&self) -> doctor::DoctorService<'_> {
167 doctor::DoctorService::new(&self.paths, &*self.runner)
168 }
169
170 /// Known hosts management (listing, scanning, adding, removing).
171 #[cfg(feature = "known-hosts")]
172 pub fn known_hosts(&self) -> known_hosts::KnownHostsService<'_> {
173 known_hosts::KnownHostsService::new(&self.paths, &*self.runner)
174 }
175
176 /// SSH certificate and CA operations (inspection, validity, KRL).
177 #[cfg(feature = "certificate")]
178 pub fn certificate(&self) -> certificate::CertificateService {
179 certificate::CertificateService::new()
180 }
181
182 /// Port forwarding management via ControlMaster.
183 #[cfg(feature = "forward")]
184 pub fn forward(&self) -> forward::ForwardService<'_> {
185 forward::ForwardService::new(&self.paths)
186 }
187}