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/// - `ALC_PROMPT_PREVIEW_CHARS`: char count for `alc_status(pending_filter="preview")`
43/// prompt truncation. Falls back to
44/// [`algocline_engine::DEFAULT_PROMPT_PREVIEW_CHARS`] when unset or
45/// unparseable. Setting `0` yields empty previews — if you want no
46/// prompt at all, use the `"meta"` preset instead.
47#[derive(Clone, Debug)]
48pub struct AppConfig {
49 /// Resolved log directory, or `None` if no writable directory is available.
50 pub log_dir: Option<PathBuf>,
51 pub log_dir_source: LogDirSource,
52 pub log_enabled: bool,
53 /// Char count for `alc_status` prompt_preview truncation.
54 pub prompt_preview_chars: usize,
55}
56
57impl AppConfig {
58 /// Build from environment variables (single resolution point).
59 pub fn from_env() -> Self {
60 let (log_dir, log_dir_source) = Self::resolve_log_dir();
61
62 let log_enabled = std::env::var("ALC_LOG_LEVEL")
63 .map(|v| v.to_lowercase() != "off")
64 .unwrap_or(true);
65
66 let prompt_preview_chars = std::env::var("ALC_PROMPT_PREVIEW_CHARS")
67 .ok()
68 .and_then(|s| s.parse::<usize>().ok())
69 .unwrap_or(algocline_engine::DEFAULT_PROMPT_PREVIEW_CHARS);
70
71 Self {
72 log_dir,
73 log_dir_source,
74 log_enabled,
75 prompt_preview_chars,
76 }
77 }
78
79 /// Resolve log directory with fallback chain.
80 ///
81 /// Tries each candidate in order, creating the directory if needed via
82 /// [`ensure_dir`](Self::ensure_dir). Returns `(Some(path), source)` on
83 /// the first writable candidate, or `(None, LogDirSource::None)` if every
84 /// candidate fails.
85 ///
86 /// ## Fallback order
87 ///
88 /// 1. `ALC_LOG_DIR` env var — explicit user/operator override.
89 /// 2. `~/.algocline/logs` — home-based default (most common).
90 /// 3. `$XDG_STATE_HOME/algocline/logs` (or `~/.local/state/…`).
91 /// 4. `<cwd>/algocline-logs` — **sandbox fallback**.
92 /// In containerised / sandbox environments (Docker, CI runners,
93 /// restricted shells) the home directory and XDG paths may not
94 /// exist or may be read-only. The current working directory is
95 /// often the only writable location available, so we fall back
96 /// to it to preserve file logging in those environments.
97 /// 5. `None` — no writable directory found; file logging is disabled
98 /// and the server operates in stderr-only tracing mode.
99 fn resolve_log_dir() -> (Option<PathBuf>, LogDirSource) {
100 // 1. ALC_LOG_DIR env (explicit override — highest priority)
101 if let Ok(dir) = std::env::var("ALC_LOG_DIR") {
102 let path = PathBuf::from(dir);
103 if Self::ensure_dir(&path) {
104 return (Some(path), LogDirSource::EnvVar);
105 }
106 }
107
108 // 2. ~/.algocline/logs (home-based default)
109 if let Some(home) = dirs::home_dir() {
110 let path = home.join(".algocline").join("logs");
111 if Self::ensure_dir(&path) {
112 return (Some(path), LogDirSource::Home);
113 }
114 }
115
116 // 3. state_dir (XDG_STATE_HOME or ~/.local/state)
117 if let Some(state) = dirs::state_dir() {
118 let path = state.join("algocline").join("logs");
119 if Self::ensure_dir(&path) {
120 return (Some(path), LogDirSource::StateDir);
121 }
122 }
123
124 // 4. Current working directory (sandbox fallback — see doc above)
125 if let Ok(cwd) = std::env::current_dir() {
126 let path = cwd.join("algocline-logs");
127 if Self::ensure_dir(&path) {
128 return (Some(path), LogDirSource::CurrentDir);
129 }
130 }
131
132 // 5. No writable directory — stderr-only
133 (None, LogDirSource::None)
134 }
135
136 /// Try to create the directory. Returns true if it exists and is writable.
137 fn ensure_dir(path: &Path) -> bool {
138 std::fs::create_dir_all(path).is_ok() && path.is_dir()
139 }
140}