use anyhow::{Context, Result};
use std::path::PathBuf;
use std::process::Stdio;
use std::sync::Arc;
use std::time::Duration;
use tokio::process::{Child, Command};
use tokio::sync::RwLock;
use super::client::OpenCodeClient;
use super::events::EventStream;
pub const DEFAULT_PORT: u16 = 4096;
const STARTUP_TIMEOUT: Duration = Duration::from_secs(30);
const HEALTH_CHECK_INTERVAL: Duration = Duration::from_millis(500);
#[derive(Debug, Clone)]
pub struct ServerConfig {
pub port: u16,
pub working_dir: Option<PathBuf>,
pub binary_path: Option<PathBuf>,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
port: DEFAULT_PORT,
working_dir: None,
binary_path: None,
}
}
}
pub struct OpenCodeManager {
config: ServerConfig,
client: OpenCodeClient,
server_process: Arc<RwLock<Option<Child>>>,
}
impl OpenCodeManager {
pub fn new() -> Self {
Self::with_config(ServerConfig::default())
}
pub fn with_config(config: ServerConfig) -> Self {
let client = OpenCodeClient::localhost(config.port);
Self {
config,
client,
server_process: Arc::new(RwLock::new(None)),
}
}
pub fn client(&self) -> &OpenCodeClient {
&self.client
}
pub async fn is_running(&self) -> bool {
self.client.health_check().await.unwrap_or(false)
}
pub async fn ensure_running(&self) -> Result<()> {
if self.is_running().await {
return Ok(());
}
self.start_server().await
}
pub async fn start_server(&self) -> Result<()> {
if self.is_running().await {
return Ok(());
}
let binary = self.find_binary()?;
let mut cmd = Command::new(&binary);
cmd.arg("serve");
cmd.arg("--port").arg(self.config.port.to_string());
if let Some(ref dir) = self.config.working_dir {
cmd.current_dir(dir);
}
cmd.stdout(Stdio::null());
cmd.stderr(Stdio::null());
let child = cmd
.spawn()
.with_context(|| format!("Failed to start opencode server: {}", binary.display()))?;
{
let mut process = self.server_process.write().await;
*process = Some(child);
}
self.wait_for_ready().await?;
Ok(())
}
async fn wait_for_ready(&self) -> Result<()> {
let start = std::time::Instant::now();
while start.elapsed() < STARTUP_TIMEOUT {
if self.client.health_check().await.unwrap_or(false) {
return Ok(());
}
tokio::time::sleep(HEALTH_CHECK_INTERVAL).await;
}
anyhow::bail!(
"OpenCode server failed to start within {:?}",
STARTUP_TIMEOUT
);
}
pub async fn stop_server(&self) -> Result<()> {
let mut process = self.server_process.write().await;
if let Some(mut child) = process.take() {
let _ = child.kill().await;
}
Ok(())
}
fn find_binary(&self) -> Result<PathBuf> {
if let Some(ref path) = self.config.binary_path {
if path.exists() {
return Ok(path.clone());
}
}
use crate::commands::spawn::terminal::{find_harness_binary, Harness};
find_harness_binary(Harness::OpenCode)
.map(PathBuf::from)
.context(
"Could not find opencode binary. Install with: npm install -g @anthropics/opencode",
)
}
pub async fn event_stream(&self) -> Result<EventStream> {
self.ensure_running().await?;
EventStream::connect(&self.client.event_stream_url()).await
}
pub fn port(&self) -> u16 {
self.config.port
}
pub fn config(&self) -> &ServerConfig {
&self.config
}
}
impl Default for OpenCodeManager {
fn default() -> Self {
Self::new()
}
}
impl Drop for OpenCodeManager {
fn drop(&mut self) {
}
}
static GLOBAL_MANAGER: std::sync::OnceLock<Arc<OpenCodeManager>> = std::sync::OnceLock::new();
pub fn global_manager() -> Arc<OpenCodeManager> {
GLOBAL_MANAGER
.get_or_init(|| Arc::new(OpenCodeManager::new()))
.clone()
}
pub fn init_global_manager(config: ServerConfig) -> Arc<OpenCodeManager> {
GLOBAL_MANAGER
.get_or_init(|| Arc::new(OpenCodeManager::with_config(config)))
.clone()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = ServerConfig::default();
assert_eq!(config.port, DEFAULT_PORT);
assert!(config.working_dir.is_none());
assert!(config.binary_path.is_none());
}
#[test]
fn test_custom_config() {
let config = ServerConfig {
port: 8080,
working_dir: Some(PathBuf::from("/tmp")),
binary_path: Some(PathBuf::from("/usr/bin/opencode")),
};
assert_eq!(config.port, 8080);
assert_eq!(config.working_dir, Some(PathBuf::from("/tmp")));
}
#[test]
fn test_manager_creation() {
let manager = OpenCodeManager::new();
assert_eq!(manager.port(), DEFAULT_PORT);
}
#[test]
fn test_manager_with_config() {
let config = ServerConfig {
port: 9000,
..Default::default()
};
let manager = OpenCodeManager::with_config(config);
assert_eq!(manager.port(), 9000);
}
#[test]
fn test_client_access() {
let manager = OpenCodeManager::new();
let client = manager.client();
assert_eq!(client.base_url(), "http://127.0.0.1:4096");
}
}