mod account; mod ca; mod proxy; mod settings; use std::{net::SocketAddr, sync::Arc}; use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use tokio::sync::Mutex; use crate::{ store::{Store, TabMode, TabSettings}, Error, Result, }; use self::{ca::CaManager, proxy::ProxyRuntime}; pub(crate) fn proxy_host_allowed(host: &str) -> bool { proxy::is_cursor_host(host) } fn integration_prerequisites_ready(ca: &CaState, backend_ready: bool) -> bool { matches!(ca, CaState::Ready) && backend_ready } #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "snake_case")] pub enum CaState { Missing, Untrusted, Ready, Invalid, Unsupported, } #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "snake_case")] pub enum IntegrationState { Disabled, Enabled, Degraded, } #[derive(Clone, Debug, Serialize)] pub struct CursorHarnessStatus { pub platform: &'static str, pub ca: CaState, pub configured_models: usize, pub enabled_models: usize, pub integration: IntegrationState, pub proxy_url: Option, pub ca_install_command: Option, } #[derive(Clone, Copy, Debug, Deserialize)] pub struct SetEnabled { pub enabled: bool, } #[derive(Clone)] pub struct CursorHarness { inner: Arc, } struct Inner { store: Store, ca: CaManager, ca_initialization: Mutex<()>, backend_addr: RwLock>, tab_mode: Arc>, proxy: Mutex, } impl CursorHarness { pub fn new(store: Store) -> Result { Ok(Self { inner: Arc::new(Inner { store, ca: CaManager::managed()?, ca_initialization: Mutex::new(()), backend_addr: RwLock::new(None), tab_mode: Arc::new(RwLock::new(TabMode::default())), proxy: Mutex::new(ProxyRuntime::default()), }), }) } pub fn set_backend_addr(&self, addr: SocketAddr) { *self.inner.backend_addr.write() = Some(addr); } pub async fn cleanup_stale_settings(&self) -> Result<()> { settings::clear_stale_managed_settings() } pub async fn status(&self) -> Result { let models = self.inner.store.provider_models(false).await?; let configured_models = models.len(); let enabled_models = models.iter().filter(|model| model.enabled).count(); let ca = self.inner.ca.state()?; if integration_prerequisites_ready(&ca, self.inner.backend_addr.read().is_some()) { self.enable().await?; } let proxy = self.inner.proxy.lock().await; let proxy_url = proxy.url(); let settings_applied = proxy_url .as_deref() .map(settings::settings_match) .transpose()? .unwrap_or(false); let integration = match (proxy.running(), settings_applied) { (false, false) => IntegrationState::Disabled, (true, true) => IntegrationState::Enabled, _ => IntegrationState::Degraded, }; Ok(CursorHarnessStatus { platform: std::env::consts::OS, ca, configured_models, enabled_models, integration, proxy_url, ca_install_command: self.inner.ca.install_command(), }) } pub async fn initialize_ca(&self) -> Result { let _initialization = self.inner.ca_initialization.lock().await; let manager = self.inner.ca.clone(); tokio::task::spawn_blocking(move || manager.initialize_local()) .await .map_err(|error| Error::Store(format!("CA initialization task failed: {error}")))??; self.status().await } pub async fn set_enabled(&self, enabled: bool) -> Result { if enabled { self.enable().await?; } else { self.disable().await?; } self.status().await } pub async fn set_tab_settings(&self, settings: TabSettings) -> Result { let saved = self.inner.store.set_tab_settings(settings).await?; *self.inner.tab_mode.write() = saved.mode; Ok(saved) } async fn enable(&self) -> Result<()> { if !matches!(self.inner.ca.state()?, CaState::Ready) { return Err(Error::Config( "initialize and trust the CA before enabling Cursor".into(), )); } let backend_addr = self .inner .backend_addr .read() .ok_or_else(|| Error::Config("desktop management server is not ready".into()))?; let mut proxy = self.inner.proxy.lock().await; if proxy.running() { if let Some(url) = proxy.url() { apply_cursor_configuration(&url).await?; } return Ok(()); } let ca = self.inner.ca.load()?; let requested_port = self.inner.store.port_settings().await?.proxy_port; *self.inner.tab_mode.write() = self.inner.store.tab_settings().await?.mode; let (url, actual_port) = proxy .start( backend_addr, ca, requested_port, self.inner.tab_mode.clone(), ) .await?; if let Err(error) = self.inner.store.set_proxy_port(actual_port).await { proxy.stop().await; return Err(error); } if let Err(error) = apply_cursor_configuration(&url).await { proxy.stop().await; return Err(error); } Ok(()) } pub async fn disable(&self) -> Result<()> { settings::clear_proxy_settings()?; self.inner.proxy.lock().await.stop().await; Ok(()) } } async fn apply_cursor_configuration(proxy_url: &str) -> Result<()> { account::inject_if_missing().await?; settings::write_proxy_settings(proxy_url) } #[cfg(test)] mod tests { use super::*; #[test] fn automatic_integration_requires_only_a_ready_ca_and_backend() { assert!(integration_prerequisites_ready(&CaState::Ready, true)); assert!(!integration_prerequisites_ready(&CaState::Ready, false)); assert!(!integration_prerequisites_ready(&CaState::Missing, true)); } }