pub mod agent_control;
pub mod copilot_review;
pub mod docker;
pub mod external;
pub mod file_pr;
pub mod filesystem;
pub mod git;
pub mod github;
pub mod local;
pub mod log;
pub mod popup;
pub mod secrets;
pub mod zellij_events;
pub use self::agent_control::{
AgentControlService, AgentInfo, BatchCleanupResult, BatchSpawnResult, SpawnOptions, SpawnResult,
};
use self::docker::CommandExecutor;
pub use self::filesystem::FileSystemService;
use self::git::GitService;
use self::github::GitHubService;
use self::local::LocalExecutor;
use self::log::{HasLogService, LogService};
pub use self::secrets::Secrets;
use std::path::PathBuf;
use std::sync::Arc;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ServicesError {
#[error("failed to get current directory: {0}")]
CurrentDirFailed(String),
#[error("working directory does not exist: {path}")]
WorkingDirNotFound { path: PathBuf },
#[error("git executable not found on PATH")]
GitNotFound,
#[error("gh CLI not found on PATH (required when GitHub service is enabled)")]
GhCliNotFound,
#[error("command execution failed: {0}")]
CommandFailed(String),
}
#[derive(Clone)]
pub struct Services {
pub log: LogService,
pub git: Arc<GitService>,
pub github: Option<GitHubService>,
pub agent_control: Arc<AgentControlService>,
pub filesystem: Arc<FileSystemService>,
pub zellij_session: Option<String>,
}
#[derive(Clone)]
pub struct ValidatedServices(Services);
impl Services {
pub fn new() -> Self {
let local = LocalExecutor::new();
let local_arc: Arc<dyn CommandExecutor> = Arc::new(local);
Self::with_executor(local_arc)
}
fn with_executor(executor: Arc<dyn CommandExecutor>) -> Self {
let secrets = Secrets::load();
let git = Arc::new(GitService::new(executor.clone()));
let github = secrets
.github_token()
.and_then(|t| GitHubService::new(t).ok());
let project_dir = std::env::current_dir().unwrap_or_default();
let agent_control = Arc::new(AgentControlService::new(
project_dir.clone(),
github.clone(),
GitService::new(executor.clone()),
));
let filesystem = Arc::new(FileSystemService::new(project_dir));
Self {
log: LogService,
git,
github,
agent_control,
filesystem,
zellij_session: None,
}
}
pub fn with_zellij_session(mut self, session: String) -> Self {
self.zellij_session = Some(session);
self
}
}
impl Default for Services {
fn default() -> Self {
Self::new()
}
}
impl HasLogService for Services {
fn log_service(&self) -> &LogService {
&self.log
}
}
impl Services {
pub fn validate(self) -> Result<ValidatedServices, ServicesError> {
let git_check = std::process::Command::new("git").arg("--version").output();
match git_check {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Err(ServicesError::GitNotFound);
}
Err(e) => {
return Err(ServicesError::CommandFailed(format!(
"git --version: {}",
e
)));
}
Ok(output) if !output.status.success() => {
return Err(ServicesError::CommandFailed(format!(
"git --version exited with: {}",
output.status
)));
}
Ok(_) => {} }
if self.github.is_some() {
let gh_check = std::process::Command::new("gh").arg("--version").output();
match gh_check {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Err(ServicesError::GhCliNotFound);
}
Err(e) => {
return Err(ServicesError::CommandFailed(format!("gh --version: {}", e)));
}
Ok(output) if !output.status.success() => {
return Err(ServicesError::CommandFailed(format!(
"gh --version exited with: {}",
output.status
)));
}
Ok(_) => {} }
}
Ok(ValidatedServices(self))
}
}
impl ValidatedServices {
pub fn log(&self) -> &LogService {
&self.0.log
}
pub fn git(&self) -> &Arc<GitService> {
&self.0.git
}
pub fn github(&self) -> &Option<GitHubService> {
&self.0.github
}
pub fn agent_control(&self) -> &Arc<AgentControlService> {
&self.0.agent_control
}
pub fn filesystem(&self) -> &Arc<FileSystemService> {
&self.0.filesystem
}
pub fn zellij_session(&self) -> Option<&str> {
self.0.zellij_session.as_deref()
}
pub fn inner(&self) -> &Services {
&self.0
}
pub fn into_inner(self) -> Services {
self.0
}
}
impl HasLogService for ValidatedServices {
fn log_service(&self) -> &LogService {
&self.0.log
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_services_error_display() {
let err = ServicesError::GitNotFound;
assert_eq!(err.to_string(), "git executable not found on PATH");
let err = ServicesError::GhCliNotFound;
assert_eq!(
err.to_string(),
"gh CLI not found on PATH (required when GitHub service is enabled)"
);
let err = ServicesError::CurrentDirFailed("permission denied".to_string());
assert_eq!(
err.to_string(),
"failed to get current directory: permission denied"
);
let err = ServicesError::CommandFailed("git --version failed".to_string());
assert_eq!(
err.to_string(),
"command execution failed: git --version failed"
);
}
}