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}