use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::process::ExitStatus; use std::sync::Arc; use bytes::Bytes; use forge_app::{ CommandInfra, DirectoryReaderInfra, EnvironmentInfra, FileDirectoryInfra, FileInfoInfra, FileReaderInfra, FileRemoverInfra, FileWriterInfra, GrpcInfra, HttpInfra, McpServerInfra, StrategyFactory, UserInfra, WalkerInfra, }; use forge_domain::{ AuthMethod, CommandOutput, FileInfo as FileInfoData, McpServerConfig, ProviderId, URLParamSpec, }; use forge_eventsource::EventSource; use reqwest::header::HeaderMap; use reqwest::{Response, Url}; use crate::auth::{AnyAuthStrategy, ForgeAuthStrategyFactory}; use crate::console::StdConsoleWriter; use crate::env::{ForgeEnvironmentInfra, to_environment}; use crate::executor::ForgeCommandExecutorService; use crate::fs_create_dirs::ForgeCreateDirsService; use crate::fs_meta::ForgeFileMetaService; use crate::fs_read::ForgeFileReadService; use crate::fs_read_dir::ForgeDirectoryReaderService; use crate::fs_remove::ForgeFileRemoveService; use crate::fs_write::ForgeFileWriteService; use crate::grpc::ForgeGrpcClient; use crate::http::ForgeHttpInfra; use crate::inquire::ForgeInquire; use crate::mcp_client::ForgeMcpClient; use crate::mcp_server::ForgeMcpServer; use crate::walker::ForgeWalkerService; #[derive(Clone)] pub struct ForgeInfra { // TODO: Drop the "Service" suffix. Use names like ForgeFileReader, ForgeFileWriter, // ForgeHttpClient etc. file_read_service: Arc, file_write_service: Arc, file_remove_service: Arc, config_infra: Arc, file_meta_service: Arc, create_dirs_service: Arc, directory_reader_service: Arc, command_executor_service: Arc, inquire_service: Arc, mcp_server: ForgeMcpServer, walker_service: Arc, http_service: Arc>, strategy_factory: Arc, grpc_client: Arc, output_printer: Arc, } impl ForgeInfra { /// Creates a new [`ForgeInfra`] with all infrastructure services /// initialized. /// /// # Arguments /// * `cwd` - The working directory for command execution and environment /// resolution /// * `config` - Pre-read application configuration; used only at /// construction time to initialize infrastructure services /// * `services_url` - Pre-validated URL for the gRPC workspace server pub fn new(cwd: PathBuf, config: forge_config::ForgeConfig) -> Self { let env = to_environment(cwd.clone()); let config_infra = Arc::new(ForgeEnvironmentInfra::new(cwd, config.clone())); let file_write_service = Arc::new(ForgeFileWriteService::new()); let config = config_infra.cached_config().unwrap_or(config); let http_service = Arc::new(ForgeHttpInfra::new( config.clone(), file_write_service.clone(), )); let file_read_service = Arc::new(ForgeFileReadService::new()); let file_meta_service = Arc::new(ForgeFileMetaService); let directory_reader_service = Arc::new(ForgeDirectoryReaderService::new( config_infra .cached_config() .map(|c| c.max_parallel_file_reads) .unwrap_or(4), )); let grpc_client = Arc::new(ForgeGrpcClient::new(config.services_url.clone())); let output_printer = Arc::new(StdConsoleWriter::default()); Self { file_read_service, file_write_service, file_remove_service: Arc::new(ForgeFileRemoveService::new()), config_infra, file_meta_service, create_dirs_service: Arc::new(ForgeCreateDirsService), directory_reader_service, command_executor_service: Arc::new(ForgeCommandExecutorService::new( env.clone(), output_printer.clone(), )), inquire_service: Arc::new(ForgeInquire::new()), mcp_server: ForgeMcpServer, walker_service: Arc::new(ForgeWalkerService::new()), strategy_factory: Arc::new(ForgeAuthStrategyFactory::new(env.clone())), http_service, grpc_client, output_printer, } } } impl ForgeInfra { /// Returns the current application configuration, re-reading from disk if /// the cache was invalidated by a prior `update_environment` call. /// /// # Errors /// /// Returns an error if the disk read fails. pub fn config(&self) -> anyhow::Result { self.config_infra.cached_config() } } impl EnvironmentInfra for ForgeInfra { type Config = forge_config::ForgeConfig; fn get_env_var(&self, key: &str) -> Option { self.config_infra.get_env_var(key) } fn get_env_vars(&self) -> BTreeMap { self.config_infra.get_env_vars() } fn get_environment(&self) -> forge_domain::Environment { self.config_infra.get_environment() } fn get_config(&self) -> anyhow::Result { self.config_infra.get_config() } async fn update_environment( &self, ops: Vec, ) -> anyhow::Result<()> { self.config_infra.update_environment(ops).await } } #[async_trait::async_trait] impl FileReaderInfra for ForgeInfra { async fn read_utf8(&self, path: &Path) -> anyhow::Result { self.file_read_service.read_utf8(path).await } fn read_batch_utf8( &self, batch_size: usize, paths: Vec, ) -> impl futures::Stream)> + Send { self.file_read_service.read_batch_utf8(batch_size, paths) } async fn read(&self, path: &Path) -> anyhow::Result> { self.file_read_service.read(path).await } async fn range_read_utf8( &self, path: &Path, start_line: u64, end_line: u64, ) -> anyhow::Result<(String, FileInfoData)> { self.file_read_service .range_read_utf8(path, start_line, end_line) .await } } #[async_trait::async_trait] impl FileWriterInfra for ForgeInfra { async fn write(&self, path: &Path, contents: Bytes) -> anyhow::Result<()> { self.file_write_service.write(path, contents).await } async fn append(&self, path: &Path, contents: Bytes) -> anyhow::Result<()> { self.file_write_service.append(path, contents).await } async fn write_temp(&self, prefix: &str, ext: &str, content: &str) -> anyhow::Result { self.file_write_service .write_temp(prefix, ext, content) .await } } #[async_trait::async_trait] impl FileInfoInfra for ForgeInfra { async fn is_binary(&self, path: &Path) -> anyhow::Result { self.file_meta_service.is_binary(path).await } async fn is_file(&self, path: &Path) -> anyhow::Result { self.file_meta_service.is_file(path).await } async fn exists(&self, path: &Path) -> anyhow::Result { self.file_meta_service.exists(path).await } async fn file_size(&self, path: &Path) -> anyhow::Result { self.file_meta_service.file_size(path).await } } #[async_trait::async_trait] impl FileRemoverInfra for ForgeInfra { async fn remove(&self, path: &Path) -> anyhow::Result<()> { self.file_remove_service.remove(path).await } } #[async_trait::async_trait] impl FileDirectoryInfra for ForgeInfra { async fn create_dirs(&self, path: &Path) -> anyhow::Result<()> { self.create_dirs_service.create_dirs(path).await } } #[async_trait::async_trait] impl CommandInfra for ForgeInfra { async fn execute_command( &self, command: String, working_dir: PathBuf, silent: bool, env_vars: Option>, ) -> anyhow::Result { self.command_executor_service .execute_command(command, working_dir, silent, env_vars) .await } async fn execute_command_raw( &self, command: &str, working_dir: PathBuf, env_vars: Option>, ) -> anyhow::Result { self.command_executor_service .execute_command_raw(command, working_dir, env_vars) .await } } #[async_trait::async_trait] impl UserInfra for ForgeInfra { async fn prompt_question(&self, question: &str) -> anyhow::Result> { self.inquire_service.prompt_question(question).await } async fn select_one( &self, message: &str, options: Vec, ) -> anyhow::Result> { self.inquire_service.select_one(message, options).await } async fn select_many( &self, message: &str, options: Vec, ) -> anyhow::Result>> { self.inquire_service.select_many(message, options).await } } #[async_trait::async_trait] impl McpServerInfra for ForgeInfra { type Client = ForgeMcpClient; async fn connect( &self, config: McpServerConfig, env_vars: &BTreeMap, environment: &forge_domain::Environment, ) -> anyhow::Result { self.mcp_server.connect(config, env_vars, environment).await } } #[async_trait::async_trait] impl WalkerInfra for ForgeInfra { async fn walk(&self, config: forge_app::Walker) -> anyhow::Result> { self.walker_service.walk(config).await } } #[async_trait::async_trait] impl HttpInfra for ForgeInfra { async fn http_get(&self, url: &Url, headers: Option) -> anyhow::Result { self.http_service.http_get(url, headers).await } async fn http_post( &self, url: &Url, headers: Option, body: Bytes, ) -> anyhow::Result { self.http_service.http_post(url, headers, body).await } async fn http_delete(&self, url: &Url) -> anyhow::Result { self.http_service.http_delete(url).await } async fn http_eventsource( &self, url: &Url, headers: Option, body: Bytes, ) -> anyhow::Result { self.http_service.http_eventsource(url, headers, body).await } } #[async_trait::async_trait] impl DirectoryReaderInfra for ForgeInfra { async fn list_directory_entries( &self, directory: &Path, ) -> anyhow::Result> { self.directory_reader_service .list_directory_entries(directory) .await } async fn read_directory_files( &self, directory: &Path, pattern: Option<&str>, ) -> anyhow::Result> { self.directory_reader_service .read_directory_files(directory, pattern) .await } } impl StrategyFactory for ForgeInfra { type Strategy = AnyAuthStrategy; fn create_auth_strategy( &self, provider_id: ProviderId, method: AuthMethod, required_params: Vec, ) -> anyhow::Result { self.strategy_factory .create_auth_strategy(provider_id, method, required_params) } } impl GrpcInfra for ForgeInfra { fn channel(&self) -> anyhow::Result { self.grpc_client.channel() } fn hydrate(&self) { self.grpc_client.hydrate(); } } impl forge_domain::ConsoleWriter for ForgeInfra { fn write(&self, buf: &[u8]) -> std::io::Result { self.output_printer.write(buf) } fn write_err(&self, buf: &[u8]) -> std::io::Result { self.output_printer.write_err(buf) } fn flush(&self) -> std::io::Result<()> { self.output_printer.flush() } fn flush_err(&self) -> std::io::Result<()> { self.output_printer.flush_err() } }