Skip to main content

cc_switch/daemon/
mod.rs

1//! Daemon subsystem: in-process supervision of one ccs-proxy per upstream URL.
2//!
3//! See docs/superpowers/specs/2026-05-28-cc-switch-daemon-design.md for design.
4
5pub mod aggregate;
6pub mod commands;
7pub mod pidfile;
8pub mod state;
9pub mod status;
10
11#[cfg(unix)]
12pub mod fork;
13#[cfg(unix)]
14pub mod lifecycle;
15#[cfg(unix)]
16pub mod logging;
17
18pub use commands::{DaemonAction, handle_daemon_command};
19
20use crate::daemon::pidfile::{Pidfile, process_alive};
21use crate::daemon::state::{CURRENT_VERSION, DaemonState};
22
23/// If the daemon is alive but was started by a `cc-switch` binary whose version
24/// differs from this one, return a warning message. Returns `None` when the
25/// daemon is stopped or its version matches the current binary.
26///
27/// A stale daemon may expose different proxy/api ports or capture semantics, so
28/// the user should restart it after upgrading `cc-switch`.
29pub fn version_mismatch_warning() -> Option<String> {
30    let home = dirs::home_dir()?;
31    let cc_switch_dir = home.join(".cc-switch");
32    let state = DaemonState::load(&cc_switch_dir.join("daemon-state.json")).ok()??;
33
34    // Only warn when a daemon is actually running.
35    let pidfile = Pidfile::new(cc_switch_dir.join("daemon.pid"));
36    let pid = pidfile.read().ok()??;
37    if !matches!(process_alive(pid), Ok(true)) {
38        return None;
39    }
40
41    if !state.version_mismatch() {
42        return None;
43    }
44
45    let running = if state.version.is_empty() {
46        "unknown (pre-version)".to_string()
47    } else {
48        state.version.clone()
49    };
50    Some(format!(
51        "cc daemon is running an outdated version (daemon {running}, CLI {CURRENT_VERSION}) — proxy ports/capture may be stale. Run `cc-switch daemon restart`."
52    ))
53}
54
55/// Print the version-mismatch warning in red, if one applies. No-op otherwise.
56pub fn print_version_mismatch_warning() {
57    use colored::Colorize;
58    if let Some(msg) = version_mismatch_warning() {
59        eprintln!("{}", format!("\u{26a0} {msg}").red().bold());
60    }
61}
62
63/// Result of attempting to resolve a proxy URL for a given upstream.
64pub enum ProxyResolution {
65    /// Daemon is running and has a matching proxy.
66    Proxied { proxy_url: String },
67    /// Daemon is not running or has no match; use direct URL.
68    Direct,
69}
70
71/// Check whether the daemon is alive and has a proxy for the given upstream URL.
72/// Returns the proxy URL (http://127.0.0.1:<port>) if available, otherwise Direct.
73pub fn try_resolve_proxy(upstream: &str) -> ProxyResolution {
74    let home = match dirs::home_dir() {
75        Some(h) => h,
76        None => return ProxyResolution::Direct,
77    };
78    let cc_switch_dir = home.join(".cc-switch");
79    let state_path = cc_switch_dir.join("daemon-state.json");
80    let pidfile_path = cc_switch_dir.join("daemon.pid");
81
82    try_resolve_proxy_from_paths(upstream, &state_path, &pidfile_path)
83}
84
85fn try_resolve_proxy_from_paths(
86    upstream: &str,
87    state_path: &std::path::Path,
88    pidfile_path: &std::path::Path,
89) -> ProxyResolution {
90    let state = match DaemonState::load(state_path) {
91        Ok(Some(s)) => s,
92        _ => return ProxyResolution::Direct,
93    };
94
95    let pidfile = Pidfile::new(pidfile_path.to_path_buf());
96    let pid = match pidfile.read() {
97        Ok(Some(pid)) => pid,
98        _ => return ProxyResolution::Direct,
99    };
100
101    match process_alive(pid) {
102        Ok(true) => {}
103        _ => return ProxyResolution::Direct,
104    }
105
106    match state.find_proxy("claude", upstream) {
107        Some(entry) => ProxyResolution::Proxied {
108            proxy_url: format!("http://127.0.0.1:{}", entry.proxy_port),
109        },
110        None => ProxyResolution::Direct,
111    }
112}