233 lines
7.5 KiB
Rust
233 lines
7.5 KiB
Rust
//! Application context: configuration, catalog, state, paths, logger and
|
|
//! shared flags.
|
|
|
|
use crate::catalog::Catalog;
|
|
use crate::cli::Cli;
|
|
use crate::config::{self, Config, Settings};
|
|
use crate::output::Logger;
|
|
use crate::state::StateStore;
|
|
use crate::theme::{self, Theme};
|
|
use anyhow::{anyhow, Context, Result};
|
|
use std::cell::Cell;
|
|
use std::io::{IsTerminal, Write};
|
|
use std::path::PathBuf;
|
|
|
|
#[derive(Debug)]
|
|
pub struct App {
|
|
pub cli: Cli,
|
|
pub config: Config,
|
|
pub config_origin: Option<PathBuf>,
|
|
pub paths: Paths,
|
|
pub catalog: Catalog,
|
|
pub state: StateStore,
|
|
pub log: Logger,
|
|
/// Active output language (issue #74); mirrored in i18n::active() so the
|
|
/// REPL 'lang' command can switch it at runtime.
|
|
lang: Cell<crate::i18n::Lang>,
|
|
/// Active output color theme (switchable at runtime with the REPL
|
|
/// 'theme <name>' command).
|
|
theme: Cell<&'static Theme>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Paths {
|
|
pub install_dir: PathBuf,
|
|
pub bin_dir: PathBuf,
|
|
pub log_dir: PathBuf,
|
|
pub state_file: PathBuf,
|
|
pub probe_cache_file: PathBuf,
|
|
pub config_dir: Option<PathBuf>,
|
|
}
|
|
|
|
impl Paths {
|
|
pub fn from_settings(settings: &Settings) -> Self {
|
|
let install_dir = settings
|
|
.install_dir
|
|
.as_deref()
|
|
.map(config::expand_path)
|
|
.unwrap_or_else(|| data_dir().join("agents"));
|
|
let log_dir = settings
|
|
.log_dir
|
|
.as_deref()
|
|
.map(config::expand_path)
|
|
.unwrap_or_else(|| state_dir().join("logs"));
|
|
let bin_dir = install_dir.join("bin");
|
|
let state_file = state_dir().join("state.json");
|
|
let probe_cache_file = state_dir().join("probe-cache.json");
|
|
let config_dir = config::user_config_dir();
|
|
Self {
|
|
install_dir,
|
|
bin_dir,
|
|
log_dir,
|
|
state_file,
|
|
probe_cache_file,
|
|
config_dir,
|
|
}
|
|
}
|
|
|
|
/// Create every directory the manager needs.
|
|
pub fn ensure(&self) -> Result<()> {
|
|
std::fs::create_dir_all(&self.install_dir)
|
|
.with_context(|| format!("cannot create {}", self.install_dir.display()))?;
|
|
std::fs::create_dir_all(&self.bin_dir)?;
|
|
std::fs::create_dir_all(&self.log_dir)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl App {
|
|
pub fn from_cli(cli: Cli) -> Result<Self> {
|
|
let loaded = config::load(&cli)?;
|
|
let paths = Paths::from_settings(&loaded.config.settings);
|
|
let log = Logger::new(paths.log_dir.clone(), cli.verbose, cli.quiet, cli.no_color);
|
|
let state = StateStore::new(paths.state_file.clone());
|
|
let catalog = Catalog::new(
|
|
loaded.config.agents.clone(),
|
|
loaded.config.aliases.clone(),
|
|
loaded.config.groups.clone(),
|
|
);
|
|
let theme = resolve_theme(cli.theme.as_deref(), loaded.config.settings.theme.as_deref())?;
|
|
let lang = crate::i18n::resolve(cli.lang);
|
|
crate::i18n::set_lang(lang);
|
|
Ok(Self {
|
|
cli,
|
|
config: loaded.config,
|
|
config_origin: loaded.origin,
|
|
paths,
|
|
catalog,
|
|
state,
|
|
log,
|
|
lang: Cell::new(lang),
|
|
theme: Cell::new(theme),
|
|
})
|
|
}
|
|
|
|
pub fn dry_run(&self) -> bool {
|
|
self.cli.dry_run
|
|
}
|
|
|
|
pub fn json(&self) -> bool {
|
|
self.cli.json
|
|
}
|
|
|
|
pub fn yes(&self) -> bool {
|
|
self.cli.yes
|
|
}
|
|
|
|
pub fn color(&self) -> bool {
|
|
!self.cli.no_color && std::io::stdout().is_terminal()
|
|
}
|
|
|
|
pub fn theme(&self) -> &'static Theme {
|
|
self.theme.get()
|
|
}
|
|
|
|
/// Active output language (issue #74).
|
|
pub fn lang(&self) -> crate::i18n::Lang {
|
|
self.lang.get()
|
|
}
|
|
|
|
/// Switch the active language (REPL 'lang' command); the i18n global is
|
|
/// kept in sync so tr()/tr_fmt! follow immediately.
|
|
pub fn set_lang(&self, lang: crate::i18n::Lang) {
|
|
self.lang.set(lang);
|
|
crate::i18n::set_lang(lang);
|
|
}
|
|
|
|
/// Switch the active theme (used by the REPL 'theme <name>' command).
|
|
pub fn set_theme(&self, theme: &'static Theme) {
|
|
self.theme.set(theme);
|
|
}
|
|
|
|
/// Directory of the event journal (the state directory).
|
|
pub fn events_dir(&self) -> PathBuf {
|
|
self.paths
|
|
.state_file
|
|
.parent()
|
|
.map(|p| p.to_path_buf())
|
|
.unwrap_or_else(|| self.paths.state_file.clone())
|
|
}
|
|
|
|
/// Append an event to the journal; failures are never fatal (logged at
|
|
/// verbose level only). The plugins subscribed to the event kind are
|
|
/// then fired (issue #75) — non-blocking, never fatal either.
|
|
pub fn emit(&self, event: &crate::events::Event) {
|
|
if let Err(e) = crate::events::append(&self.events_dir(), event) {
|
|
self.log.verbose(&format!("cannot write event journal: {e:#}"));
|
|
}
|
|
crate::plugins::dispatch(self, event);
|
|
}
|
|
|
|
/// Ask the user for confirmation on stderr. Accepts y/yes/o/oui.
|
|
pub fn confirm(&self, question: &str) -> Result<bool> {
|
|
if self.cli.yes {
|
|
return Ok(true);
|
|
}
|
|
eprint!("{question} [y/N] ");
|
|
std::io::stderr().flush().ok();
|
|
let mut line = String::new();
|
|
std::io::stdin().read_line(&mut line).ok();
|
|
let answer = line.trim().to_lowercase();
|
|
Ok(matches!(answer.as_str(), "y" | "yes" | "o" | "oui" | "true"))
|
|
}
|
|
}
|
|
|
|
/// Resolve the output color theme: the --theme flag wins, then the
|
|
/// configuration entry, then the default theme. An unknown --theme value is
|
|
/// an error; an unknown configured value silently falls back to the default.
|
|
fn resolve_theme(flag: Option<&str>, configured: Option<&str>) -> Result<&'static Theme> {
|
|
if let Some(name) = flag {
|
|
return theme::find(name).ok_or_else(|| {
|
|
anyhow!(
|
|
"unknown theme '{name}' — available: {}",
|
|
theme::names().join(", ")
|
|
)
|
|
});
|
|
}
|
|
Ok(configured
|
|
.and_then(theme::find)
|
|
.unwrap_or_else(theme::default_theme))
|
|
}
|
|
|
|
/// Root directory for agent data (overridable with AGENT_MANAGER_DATA,
|
|
/// mostly used by tests).
|
|
pub fn data_dir() -> PathBuf {
|
|
if let Some(d) = std::env::var_os("AGENT_MANAGER_DATA") {
|
|
return PathBuf::from(d);
|
|
}
|
|
if cfg!(windows) {
|
|
let base = std::env::var_os("LOCALAPPDATA")
|
|
.map(PathBuf::from)
|
|
.or_else(|| config::home_dir().map(|h| h.join(".local").join("share")));
|
|
return base
|
|
.map(|b| b.join("agent-manager"))
|
|
.unwrap_or_else(|| PathBuf::from(".agent-manager"));
|
|
}
|
|
if cfg!(target_os = "macos") {
|
|
return config::home_dir()
|
|
.map(|h| h.join("Library").join("Application Support").join("agent-manager"))
|
|
.unwrap_or_else(|| PathBuf::from(".agent-manager"));
|
|
}
|
|
let base = std::env::var_os("XDG_DATA_HOME")
|
|
.map(PathBuf::from)
|
|
.or_else(|| config::home_dir().map(|h| h.join(".local").join("share")));
|
|
base.map(|b| b.join("agent-manager"))
|
|
.unwrap_or_else(|| PathBuf::from(".agent-manager"))
|
|
}
|
|
|
|
/// Root directory for runtime state (logs, state file).
|
|
pub fn state_dir() -> PathBuf {
|
|
if let Some(d) = std::env::var_os("AGENT_MANAGER_STATE") {
|
|
return PathBuf::from(d);
|
|
}
|
|
if cfg!(target_os = "linux") {
|
|
let base = std::env::var_os("XDG_STATE_HOME")
|
|
.map(PathBuf::from)
|
|
.or_else(|| config::home_dir().map(|h| h.join(".local").join("state")));
|
|
if let Some(b) = base {
|
|
return b.join("agent-manager");
|
|
}
|
|
}
|
|
data_dir()
|
|
}
|