mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-05 04:07:36 +08:00
215 lines
6.2 KiB
Rust
215 lines
6.2 KiB
Rust
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<String>,
|
|
pub ca_install_command: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Deserialize)]
|
|
pub struct SetEnabled {
|
|
pub enabled: bool,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct CursorHarness {
|
|
inner: Arc<Inner>,
|
|
}
|
|
|
|
struct Inner {
|
|
store: Store,
|
|
ca: CaManager,
|
|
ca_initialization: Mutex<()>,
|
|
backend_addr: RwLock<Option<SocketAddr>>,
|
|
tab_mode: Arc<RwLock<TabMode>>,
|
|
proxy: Mutex<ProxyRuntime>,
|
|
}
|
|
|
|
impl CursorHarness {
|
|
pub fn new(store: Store) -> Result<Self> {
|
|
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<CursorHarnessStatus> {
|
|
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<CursorHarnessStatus> {
|
|
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<CursorHarnessStatus> {
|
|
if enabled {
|
|
self.enable().await?;
|
|
} else {
|
|
self.disable().await?;
|
|
}
|
|
self.status().await
|
|
}
|
|
|
|
pub async fn set_tab_settings(&self, settings: TabSettings) -> Result<TabSettings> {
|
|
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));
|
|
}
|
|
}
|