1use anyhow::{Context, Result};
7use std::path::PathBuf;
8use std::process::Stdio;
9use std::sync::Arc;
10use std::time::Duration;
11use tokio::process::{Child, Command};
12use tokio::sync::RwLock;
13
14use super::client::OpenCodeClient;
15use super::events::EventStream;
16
17pub const DEFAULT_PORT: u16 = 4096;
19
20const STARTUP_TIMEOUT: Duration = Duration::from_secs(30);
22
23const HEALTH_CHECK_INTERVAL: Duration = Duration::from_millis(500);
25
26#[derive(Debug, Clone)]
28pub struct ServerConfig {
29 pub port: u16,
31 pub working_dir: Option<PathBuf>,
33 pub binary_path: Option<PathBuf>,
35}
36
37impl Default for ServerConfig {
38 fn default() -> Self {
39 Self {
40 port: DEFAULT_PORT,
41 working_dir: None,
42 binary_path: None,
43 }
44 }
45}
46
47pub struct OpenCodeManager {
49 config: ServerConfig,
50 client: OpenCodeClient,
51 server_process: Arc<RwLock<Option<Child>>>,
52}
53
54impl OpenCodeManager {
55 pub fn new() -> Self {
57 Self::with_config(ServerConfig::default())
58 }
59
60 pub fn with_config(config: ServerConfig) -> Self {
62 let client = OpenCodeClient::localhost(config.port);
63 Self {
64 config,
65 client,
66 server_process: Arc::new(RwLock::new(None)),
67 }
68 }
69
70 pub fn client(&self) -> &OpenCodeClient {
72 &self.client
73 }
74
75 pub async fn is_running(&self) -> bool {
77 self.client.health_check().await.unwrap_or(false)
78 }
79
80 pub async fn ensure_running(&self) -> Result<()> {
82 if self.is_running().await {
83 return Ok(());
84 }
85
86 self.start_server().await
87 }
88
89 pub async fn start_server(&self) -> Result<()> {
91 if self.is_running().await {
93 return Ok(());
94 }
95
96 let binary = self.find_binary()?;
98
99 let mut cmd = Command::new(&binary);
101 cmd.arg("serve");
102 cmd.arg("--port").arg(self.config.port.to_string());
103
104 if let Some(ref dir) = self.config.working_dir {
105 cmd.current_dir(dir);
106 }
107
108 cmd.stdout(Stdio::null());
110 cmd.stderr(Stdio::null());
111
112 let child = cmd
114 .spawn()
115 .with_context(|| format!("Failed to start opencode server: {}", binary.display()))?;
116
117 {
119 let mut process = self.server_process.write().await;
120 *process = Some(child);
121 }
122
123 self.wait_for_ready().await?;
125
126 Ok(())
127 }
128
129 async fn wait_for_ready(&self) -> Result<()> {
131 let start = std::time::Instant::now();
132
133 while start.elapsed() < STARTUP_TIMEOUT {
134 if self.client.health_check().await.unwrap_or(false) {
135 return Ok(());
136 }
137 tokio::time::sleep(HEALTH_CHECK_INTERVAL).await;
138 }
139
140 anyhow::bail!(
141 "OpenCode server failed to start within {:?}",
142 STARTUP_TIMEOUT
143 );
144 }
145
146 pub async fn stop_server(&self) -> Result<()> {
148 let mut process = self.server_process.write().await;
149
150 if let Some(mut child) = process.take() {
151 let _ = child.kill().await;
153 }
154
155 Ok(())
156 }
157
158 fn find_binary(&self) -> Result<PathBuf> {
160 if let Some(ref path) = self.config.binary_path {
162 if path.exists() {
163 return Ok(path.clone());
164 }
165 }
166
167 use crate::commands::spawn::terminal::{find_harness_binary, Harness};
169
170 find_harness_binary(Harness::OpenCode)
171 .map(PathBuf::from)
172 .context(
173 "Could not find opencode binary. Install with: npm install -g @anthropics/opencode",
174 )
175 }
176
177 pub async fn event_stream(&self) -> Result<EventStream> {
179 self.ensure_running().await?;
180 EventStream::connect(&self.client.event_stream_url()).await
181 }
182
183 pub fn port(&self) -> u16 {
185 self.config.port
186 }
187
188 pub fn config(&self) -> &ServerConfig {
190 &self.config
191 }
192}
193
194impl Default for OpenCodeManager {
195 fn default() -> Self {
196 Self::new()
197 }
198}
199
200impl Drop for OpenCodeManager {
201 fn drop(&mut self) {
202 }
206}
207
208static GLOBAL_MANAGER: std::sync::OnceLock<Arc<OpenCodeManager>> = std::sync::OnceLock::new();
210
211pub fn global_manager() -> Arc<OpenCodeManager> {
213 GLOBAL_MANAGER
214 .get_or_init(|| Arc::new(OpenCodeManager::new()))
215 .clone()
216}
217
218pub fn init_global_manager(config: ServerConfig) -> Arc<OpenCodeManager> {
220 GLOBAL_MANAGER
221 .get_or_init(|| Arc::new(OpenCodeManager::with_config(config)))
222 .clone()
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn test_default_config() {
231 let config = ServerConfig::default();
232 assert_eq!(config.port, DEFAULT_PORT);
233 assert!(config.working_dir.is_none());
234 assert!(config.binary_path.is_none());
235 }
236
237 #[test]
238 fn test_custom_config() {
239 let config = ServerConfig {
240 port: 8080,
241 working_dir: Some(PathBuf::from("/tmp")),
242 binary_path: Some(PathBuf::from("/usr/bin/opencode")),
243 };
244 assert_eq!(config.port, 8080);
245 assert_eq!(config.working_dir, Some(PathBuf::from("/tmp")));
246 }
247
248 #[test]
249 fn test_manager_creation() {
250 let manager = OpenCodeManager::new();
251 assert_eq!(manager.port(), DEFAULT_PORT);
252 }
253
254 #[test]
255 fn test_manager_with_config() {
256 let config = ServerConfig {
257 port: 9000,
258 ..Default::default()
259 };
260 let manager = OpenCodeManager::with_config(config);
261 assert_eq!(manager.port(), 9000);
262 }
263
264 #[test]
265 fn test_client_access() {
266 let manager = OpenCodeManager::new();
267 let client = manager.client();
268 assert_eq!(client.base_url(), "http://127.0.0.1:4096");
269 }
270}