Skip to main content

algocline_app/service/
config.rs

1use std::path::{Path, PathBuf};
2
3// ─── Application Config ─────────────────────────────────────────
4
5/// How the log directory was resolved.
6#[derive(Clone, Debug, PartialEq, Eq)]
7pub enum LogDirSource {
8    /// `ALC_LOG_DIR` environment variable.
9    EnvVar,
10    /// `~/.algocline/logs` (home-based default).
11    Home,
12    /// `$XDG_STATE_HOME/algocline/logs` or `~/.local/state/algocline/logs`.
13    StateDir,
14    /// Current working directory fallback.
15    CurrentDir,
16    /// No writable directory found — file logging disabled.
17    None,
18}
19
20impl std::fmt::Display for LogDirSource {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        match self {
23            Self::EnvVar => write!(f, "ALC_LOG_DIR"),
24            Self::Home => write!(f, "~/.algocline/logs"),
25            Self::StateDir => write!(f, "state_dir"),
26            Self::CurrentDir => write!(f, "current_dir"),
27            Self::None => write!(f, "none (stderr only)"),
28        }
29    }
30}
31
32/// Application-wide configuration resolved from environment variables.
33///
34/// Log directory resolution order:
35/// 1. `ALC_LOG_DIR` env var (explicit override)
36/// 2. `~/.algocline/logs` (home-based default)
37/// 3. `$XDG_STATE_HOME/algocline/logs` or `~/.local/state/algocline/logs`
38/// 4. Current working directory (sandbox fallback)
39/// 5. `None` — stderr-only mode (no file logging)
40///
41/// - `ALC_LOG_LEVEL`: `full` (default) or `off`.
42#[derive(Clone, Debug)]
43pub struct AppConfig {
44    /// Resolved log directory, or `None` if no writable directory is available.
45    pub log_dir: Option<PathBuf>,
46    pub log_dir_source: LogDirSource,
47    pub log_enabled: bool,
48}
49
50impl AppConfig {
51    /// Build from environment variables (single resolution point).
52    pub fn from_env() -> Self {
53        let (log_dir, log_dir_source) = Self::resolve_log_dir();
54
55        let log_enabled = std::env::var("ALC_LOG_LEVEL")
56            .map(|v| v.to_lowercase() != "off")
57            .unwrap_or(true);
58
59        Self {
60            log_dir,
61            log_dir_source,
62            log_enabled,
63        }
64    }
65
66    /// Resolve log directory with fallback chain.
67    ///
68    /// Tries each candidate in order, creating the directory if needed via
69    /// [`ensure_dir`](Self::ensure_dir). Returns `(Some(path), source)` on
70    /// the first writable candidate, or `(None, LogDirSource::None)` if every
71    /// candidate fails.
72    ///
73    /// ## Fallback order
74    ///
75    /// 1. `ALC_LOG_DIR` env var — explicit user/operator override.
76    /// 2. `~/.algocline/logs` — home-based default (most common).
77    /// 3. `$XDG_STATE_HOME/algocline/logs` (or `~/.local/state/…`).
78    /// 4. `<cwd>/algocline-logs` — **sandbox fallback**.
79    ///    In containerised / sandbox environments (Docker, CI runners,
80    ///    restricted shells) the home directory and XDG paths may not
81    ///    exist or may be read-only. The current working directory is
82    ///    often the only writable location available, so we fall back
83    ///    to it to preserve file logging in those environments.
84    /// 5. `None` — no writable directory found; file logging is disabled
85    ///    and the server operates in stderr-only tracing mode.
86    fn resolve_log_dir() -> (Option<PathBuf>, LogDirSource) {
87        // 1. ALC_LOG_DIR env (explicit override — highest priority)
88        if let Ok(dir) = std::env::var("ALC_LOG_DIR") {
89            let path = PathBuf::from(dir);
90            if Self::ensure_dir(&path) {
91                return (Some(path), LogDirSource::EnvVar);
92            }
93        }
94
95        // 2. ~/.algocline/logs (home-based default)
96        if let Some(home) = dirs::home_dir() {
97            let path = home.join(".algocline").join("logs");
98            if Self::ensure_dir(&path) {
99                return (Some(path), LogDirSource::Home);
100            }
101        }
102
103        // 3. state_dir (XDG_STATE_HOME or ~/.local/state)
104        if let Some(state) = dirs::state_dir() {
105            let path = state.join("algocline").join("logs");
106            if Self::ensure_dir(&path) {
107                return (Some(path), LogDirSource::StateDir);
108            }
109        }
110
111        // 4. Current working directory (sandbox fallback — see doc above)
112        if let Ok(cwd) = std::env::current_dir() {
113            let path = cwd.join("algocline-logs");
114            if Self::ensure_dir(&path) {
115                return (Some(path), LogDirSource::CurrentDir);
116            }
117        }
118
119        // 5. No writable directory — stderr-only
120        (None, LogDirSource::None)
121    }
122
123    /// Try to create the directory. Returns true if it exists and is writable.
124    fn ensure_dir(path: &Path) -> bool {
125        std::fs::create_dir_all(path).is_ok() && path.is_dir()
126    }
127}