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