This repository has been archived on 2026-10-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
maestro/crates/maestro-daemon/src/config.rs
T
Miguel PalhasandClaude Opus 5 6596104789
ci / web (pull_request) Successful in 28s
ci / rust (pull_request) Successful in 1m49s
ci / web (push) Successful in 39s
ci / rust (push) Successful in 2m48s
refactor(maestro): delete the config projections and the dead knobs
maestro-cli/src/config.rs was two pass-throughs, a newtype error, a
two-field projection of MaestroConfigFile, and one four-line or_else with
a single caller. `resolve_daemon_url` moves onto MaestroConfigFile, next
to the schema it reads, and the module goes; its tests move with it.

`[web].dev_proxy_target` is deleted end to end — schema, env var,
normalisation, three tests, README line, and the startup warning that
admitted the server ignores it.

MaestroConfigFile::save had no callers outside its own tests. Deleting it
also removes every skip_serializing_if, the five is_empty impls, and the
Serialize derives that existed only to make its output tidy.

BroadcastTuning was a struct that was never instantiated: all three uses
were `::DEFAULT.field` in const position and there is no [broadcast]
section. Now two plain consts.

$MAESTRO_CONFIG was honoured twice, by resolve_config_path and by clap's
`env =`. Every binary already gets it through clap, so resolve_config_path
and CONFIG_PATH_ENV go and the TUI falls back to default_config_path like
everything else.

Also collapses three layers of 1:1 forwarding: the umbrella's cli_dispatch
shim, and four near-identical 8-line functions in cli/dispatch.rs that
differed only in which run_* they called.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 14:39:01 +01:00

342 lines
11 KiB
Rust

//! Daemon view over the unified Maestro config.
//!
//! Reads `[daemon]` (hostname, bind_addr, state_dir, worktree_root, …) and
//! `[spawn]` from `~/.config/maestro/config.toml`.
//!
//! Precedence per field: **env > file > default**. Every field has a default,
//! so a missing config file is fine — the daemon starts with defaults.
//!
//! Default config path: `~/.config/maestro/config.toml` (override via
//! `$MAESTRO_CONFIG`).
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use maestro_config::MaestroConfigFile;
pub use maestro_config::default_config_path;
/// Default bind address when neither env nor file specify one.
pub const DEFAULT_BIND_ADDR: &str = "127.0.0.1:7070";
/// Default recency cap (days) for the durable spawn history (#24) when
/// `[daemon].spawn_history_days` is unset. `0` in config disables history.
pub const DEFAULT_SPAWN_HISTORY_DAYS: u32 = 30;
/// Env var that overrides `[daemon].bind_addr`.
pub const ENV_DAEMON_BIND_ADDR: &str = "MAESTRO_DAEMON_BIND_ADDR";
/// Env var that overrides `[daemon].tmux_tmpdir`.
pub const ENV_DAEMON_TMUX_TMPDIR: &str = "MAESTRO_DAEMON_TMUX_TMPDIR";
fn default_bind_addr() -> SocketAddr {
DEFAULT_BIND_ADDR.parse().expect("valid default bind_addr")
}
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error(transparent)]
Inner(#[from] maestro_config::ConfigError),
#[error("io: {0}")]
Io(#[from] std::io::Error),
#[error("invalid [spawn].presets: {0}")]
SpawnPresets(#[from] maestro_config::SpawnConfigError),
}
/// Resolved, validated daemon configuration ready to drive the binary.
#[derive(Debug, Clone)]
pub struct DaemonConfig {
/// Hostname advertised to clients, for display only.
pub hostname: String,
/// Home directory of the OS user the daemon runs as, advertised to clients
/// so they can prefill a spawn form's working directory. Empty when `$HOME`
/// can't be resolved (NAP-328).
pub home_dir: String,
pub bind_addr: SocketAddr,
pub state_dir: PathBuf,
pub worktree_root: PathBuf,
/// Per-session PTY replay buffer cap in bytes (NAP-229). Each new attach
/// receives this much of the prior PTY output before the live stream.
/// Default `pty::DEFAULT_REPLAY_BYTES`; `0` disables.
pub pty_replay_bytes: usize,
/// `TMUX_TMPDIR` exported for every `tmux` invocation (NAP-234).
/// `None` lets tmux pick its own default (`/tmp/tmux-$UID`); `Some(p)`
/// forces sockets under `p` so bare `tmux ls` in a matching user shell
/// sees maestro sessions.
pub tmux_tmpdir: Option<PathBuf>,
/// Recency cap (days) for the durable spawn history that backs the spawn
/// form's "recent dirs" picker (#24). `0` disables history. Default
/// [`DEFAULT_SPAWN_HISTORY_DAYS`].
pub spawn_history_days: u32,
/// Resolved spawn-form command presets (#22/#26): `[spawn].presets` (or the
/// built-ins) with the implicit `custom` tile appended. Advertised to
/// clients in `GET /api/info`.
pub spawn_presets: Vec<maestro_config::SpawnPreset>,
}
impl DaemonConfig {
pub fn sidecar_dir(&self) -> PathBuf {
self.state_dir.join("sessions")
}
pub fn fifo_dir(&self) -> PathBuf {
self.state_dir.join("fifos")
}
/// JSONL spawn-history log path (#24): `<state_dir>/spawn-history.jsonl`.
pub fn spawn_history_path(&self) -> PathBuf {
self.state_dir.join("spawn-history.jsonl")
}
}
/// Env-var overrides applied by [`load_config`]. Tests pass explicit
/// `Option`s; production reads from real env vars in `main`.
#[derive(Debug, Clone, Default)]
pub struct EnvOverrides {
pub bind_addr: Option<String>,
pub tmux_tmpdir: Option<String>,
}
impl EnvOverrides {
/// Read every supported env var from the process environment.
pub fn from_env() -> Self {
Self {
bind_addr: std::env::var(ENV_DAEMON_BIND_ADDR).ok(),
tmux_tmpdir: std::env::var(ENV_DAEMON_TMUX_TMPDIR).ok(),
}
}
}
/// Resolve the runtime configuration: env overrides on top of the unified
/// file, then defaults. A missing file is not an error.
pub fn load_config(path: &Path, env: EnvOverrides) -> Result<DaemonConfig, ConfigError> {
let file = MaestroConfigFile::load(path)?;
let daemon = file.daemon;
// Resolve spawn presets up front so a bad `[spawn].presets` (empty/dup id,
// empty command) refuses startup with a clear error, like other config
// validation (#22/#26).
let spawn_presets = file.spawn.resolve()?;
let hostname = daemon.hostname.unwrap_or_else(system_hostname);
let home_dir = dirs::home_dir()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_default();
let bind_addr = match env.bind_addr {
Some(raw) => maestro_config::parse_bind_addr(&raw, ENV_DAEMON_BIND_ADDR)?,
None => daemon.bind_addr.unwrap_or_else(default_bind_addr),
};
let state_dir = match daemon.state_dir {
Some(p) => p,
None => default_state_dir()?,
};
let worktree_root = daemon
.worktree_root
.unwrap_or_else(|| state_dir.join("worktrees"));
let pty_replay_bytes = daemon
.pty_replay_bytes
.unwrap_or(crate::pty::DEFAULT_REPLAY_BYTES);
let tmux_tmpdir = env
.tmux_tmpdir
.map(PathBuf::from)
.or(daemon.tmux_tmpdir)
.or_else(dirs::runtime_dir);
let spawn_history_days = daemon
.spawn_history_days
.unwrap_or(DEFAULT_SPAWN_HISTORY_DAYS);
Ok(DaemonConfig {
hostname,
home_dir,
bind_addr,
state_dir,
worktree_root,
pty_replay_bytes,
tmux_tmpdir,
spawn_history_days,
spawn_presets,
})
}
pub fn default_state_dir() -> Result<PathBuf, ConfigError> {
Ok(maestro_config::default_state_dir("maestro")?)
}
/// Read the kernel hostname via `gethostname(2)`. Falls back to `"unknown"`
/// on the (very unlikely) syscall failure.
pub fn system_hostname() -> String {
let mut buf = [0u8; 256];
let r = unsafe { libc::gethostname(buf.as_mut_ptr().cast(), buf.len()) };
if r != 0 {
return "unknown".into();
}
let nul = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
String::from_utf8_lossy(&buf[..nul]).into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_file_defaults_everything() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
std::fs::write(&path, "").unwrap();
let cfg = load_config(&path, EnvOverrides::default()).unwrap();
assert_eq!(cfg.bind_addr, default_bind_addr());
assert!(cfg.state_dir.ends_with("maestro"));
assert!(cfg.worktree_root.ends_with("worktrees"));
assert!(!cfg.hostname.is_empty());
}
#[test]
fn missing_file_is_not_an_error() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("does-not-exist.toml");
let cfg = load_config(&path, EnvOverrides::default()).unwrap();
assert_eq!(cfg.bind_addr, default_bind_addr());
assert!(!path.exists(), "load_config must not create the file");
}
#[test]
fn env_overrides_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
std::fs::write(
&path,
r#"
[daemon]
bind_addr = "127.0.0.1:1"
"#,
)
.unwrap();
let env = EnvOverrides {
bind_addr: Some("0.0.0.0:9999".into()),
..EnvOverrides::default()
};
let cfg = load_config(&path, env).unwrap();
assert_eq!(cfg.bind_addr, "0.0.0.0:9999".parse().unwrap());
}
#[test]
fn invalid_env_bind_addr_returns_diagnostic() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
std::fs::write(&path, "").unwrap();
let env = EnvOverrides {
bind_addr: Some("not-an-addr".into()),
..EnvOverrides::default()
};
let err = load_config(&path, env).unwrap_err();
match err {
ConfigError::Inner(maestro_config::ConfigError::InvalidBindAddr {
input, env, ..
}) => {
assert_eq!(input, "not-an-addr");
assert_eq!(env, ENV_DAEMON_BIND_ADDR);
}
other => panic!("expected InvalidBindAddr, got {other:?}"),
}
}
#[test]
fn hostname_from_file_wins_over_gethostname() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
std::fs::write(
&path,
r#"
[daemon]
hostname = "vm-42"
"#,
)
.unwrap();
let cfg = load_config(&path, EnvOverrides::default()).unwrap();
assert_eq!(cfg.hostname, "vm-42");
}
#[test]
fn spawn_history_days_defaults_and_overrides() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
std::fs::write(&path, "").unwrap();
let cfg = load_config(&path, EnvOverrides::default()).unwrap();
assert_eq!(cfg.spawn_history_days, DEFAULT_SPAWN_HISTORY_DAYS);
assert!(cfg.spawn_history_path().ends_with("spawn-history.jsonl"));
std::fs::write(
&path,
r#"
[daemon]
spawn_history_days = 0
"#,
)
.unwrap();
let cfg = load_config(&path, EnvOverrides::default()).unwrap();
assert_eq!(cfg.spawn_history_days, 0);
}
#[test]
fn system_hostname_is_nonempty() {
let h = system_hostname();
assert!(!h.is_empty(), "gethostname returned empty string");
}
// NAP-234: tmux_tmpdir precedence is env > file > dirs::runtime_dir().
// Tested without mutating the process env (which races other tests) by
// feeding `EnvOverrides` directly.
#[test]
fn tmux_tmpdir_env_beats_file_and_runtime_dir() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
std::fs::write(
&path,
r#"
[daemon]
tmux_tmpdir = "/from-file"
"#,
)
.unwrap();
let env = EnvOverrides {
tmux_tmpdir: Some("/from-env".into()),
..EnvOverrides::default()
};
let cfg = load_config(&path, env).unwrap();
assert_eq!(cfg.tmux_tmpdir.as_deref(), Some(Path::new("/from-env")));
}
#[test]
fn tmux_tmpdir_file_beats_runtime_dir() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
std::fs::write(
&path,
r#"
[daemon]
tmux_tmpdir = "/from-file"
"#,
)
.unwrap();
let cfg = load_config(&path, EnvOverrides::default()).unwrap();
assert_eq!(cfg.tmux_tmpdir.as_deref(), Some(Path::new("/from-file")));
}
#[test]
fn tmux_tmpdir_defaults_to_xdg_runtime_dir_when_unset() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
std::fs::write(&path, "").unwrap();
let cfg = load_config(&path, EnvOverrides::default()).unwrap();
// dirs::runtime_dir() is `Some(_)` on any Linux host with
// $XDG_RUNTIME_DIR set (CI included). When it's `None`, the daemon
// falls through to None and tmux uses /tmp; either is acceptable —
// but on this test runner the env var is always present.
assert_eq!(cfg.tmux_tmpdir, dirs::runtime_dir());
}
}