//! Remote-host strategy adapters (ARCHITECTURE §5, L9). //! //! A [`RemoteHost`] is the **factory** that decides *where* a project's I/O //! happens — it hands out the location-appropriate [`FileSystem`], //! [`ProcessSpawner`] and [`PtyPort`]. Routing every use case through the //! project's host is what makes local and remote execution transparent (Liskov, //! ARCHITECTURE §1.2). //! //! This module ships [`LocalHost`] (the local strategy, fully wired and tested) //! and the [`remote_host`] selector. The SSH and WSL strategies (`SshHost` via //! russh/SFTP, `WslHost` via `wsl.exe`) are the remaining L9 work; their //! integration tests are environment-gated (no SSH server / no WSL here), so they //! are not yet wired in to avoid shipping unverified adapters. Until then the //! selector reports them as unsupported rather than silently failing later. use std::sync::Arc; use async_trait::async_trait; use domain::ports::{FileSystem, ProcessSpawner, PtyPort, RemoteError, RemoteHost}; use domain::remote::{RemoteKind, RemoteRef}; use crate::{LocalFileSystem, LocalProcessSpawner, PortablePtyAdapter}; /// The local execution strategy: the project lives on this machine, so the host /// simply hands out the local adapters. #[derive(Clone)] pub struct LocalHost { fs: Arc, spawner: Arc, pty: Arc, } impl LocalHost { /// Builds a local host wired to the local FS / process / PTY adapters. #[must_use] pub fn new() -> Self { Self { fs: Arc::new(LocalFileSystem::new()), spawner: Arc::new(LocalProcessSpawner::new()), pty: Arc::new( PortablePtyAdapter::new().with_sandbox_enforcer(crate::sandbox::default_enforcer()), ), } } } impl Default for LocalHost { fn default() -> Self { Self::new() } } #[async_trait] impl RemoteHost for LocalHost { fn kind(&self) -> RemoteKind { RemoteKind::Local } async fn connect(&self) -> Result<(), RemoteError> { // Nothing to establish for the local machine. Ok(()) } fn file_system(&self) -> Arc { Arc::clone(&self.fs) } fn process_spawner(&self) -> Arc { Arc::clone(&self.spawner) } fn pty(&self) -> Arc { Arc::clone(&self.pty) } } /// Selects and builds the [`RemoteHost`] for a [`RemoteRef`]. /// /// `Local` yields a [`LocalHost`]. `Ssh`/`Wsl` are not yet wired (their adapters /// are the remaining, environment-gated L9 work) and return a clear /// [`RemoteError::Connection`] so callers fail fast with an actionable message /// instead of a confusing downstream error. /// /// # Errors /// [`RemoteError::Connection`] for SSH/WSL remotes until their adapters land. pub fn remote_host(remote: &RemoteRef) -> Result, RemoteError> { match remote { RemoteRef::Local => Ok(Arc::new(LocalHost::new())), RemoteRef::Ssh { host, .. } => Err(RemoteError::Connection(format!( "SSH remote ({host}) is not yet supported" ))), RemoteRef::Wsl { distro } => Err(RemoteError::Connection(format!( "WSL remote ({distro}) is not yet supported" ))), } }