use std::sync::Arc;
use argonauts_core::app::ConfigEntry;
use argonauts_core::settings::layer;
use argonauts_core::settings::{FILE_LAYERS, Settings};
use argonauts_registry::clis::agents_turned_on;
use argonauts_registry::{ResolvedSettings, SettingsError, SettingsSources, WarningAbout, resolve};
use tokio::sync::watch;
use crate::env::AppEnv;
use crate::trust::TrustStore;
pub(crate) struct AppSettings {
flags: Vec<String>,
session: Vec<(String, String)>,
interactive: bool,
trust: TrustStore,
resolved: ResolvedSettings,
live: watch::Sender<Arc<Settings>>,
turned_on: watch::Sender<Vec<String>>,
}
impl AppSettings {
pub(crate) fn load(
env: &AppEnv,
flags: Vec<String>,
interactive: bool,
) -> anyhow::Result<Self> {
let trust = TrustStore::load(&env.state_dir);
let sources = sources(env, &trust, flags.clone(), interactive);
let resolved = resolve(&sources)?;
let live = watch::Sender::new(Arc::new(resolved.settings.clone()));
let turned_on = watch::Sender::new(agents_turned_on(&resolved));
Ok(Self {
flags,
session: Vec::new(),
interactive,
trust,
resolved,
live,
turned_on,
})
}
pub(crate) fn settings(&self) -> &Settings {
&self.resolved.settings
}
pub(crate) fn shared(&self) -> Arc<Settings> {
Arc::clone(&self.live.borrow())
}
pub(crate) fn follow(&self) -> watch::Receiver<Arc<Settings>> {
self.live.subscribe()
}
pub(crate) fn follow_turned_on(&self) -> watch::Receiver<Vec<String>> {
self.turned_on.subscribe()
}
pub(crate) fn resolved(&self) -> &ResolvedSettings {
&self.resolved
}
pub(crate) fn sha256(&self) -> &str {
&self.resolved.sha256
}
pub(crate) fn entry(&self, key: &str) -> Option<&ConfigEntry> {
self.resolved.entries.iter().find(|entry| entry.key == key)
}
pub(crate) fn trust(&self) -> &TrustStore {
&self.trust
}
pub(crate) fn trust_mut(&mut self) -> &mut TrustStore {
&mut self.trust
}
pub(crate) fn session_overrides(&self) -> &[(String, String)] {
&self.session
}
pub(crate) fn run_overrides(&self) -> Vec<String> {
let mut overrides = self.flags.clone();
overrides.extend(
self.session
.iter()
.map(|(key, value)| format!("{key}={value}")),
);
overrides
}
pub(crate) fn check_trust(
&self,
env: &AppEnv,
file: &std::path::Path,
) -> Result<(), SettingsError> {
let mut sources = sources(env, &self.trust, self.run_overrides(), self.interactive);
sources.strict = true;
if *file == env.project_config() {
sources.project_trusted = true;
} else if *file == env.project_local_config() {
sources.project_local_trusted = true;
}
resolve(&sources).map(drop)
}
pub(crate) fn reload(&mut self, env: &AppEnv) -> anyhow::Result<()> {
self.trust = TrustStore::load(&env.state_dir);
let sources = sources(env, &self.trust, self.run_overrides(), self.interactive);
self.resolved = resolve(&sources)?;
self.turned_on
.send_replace(agents_turned_on(&self.resolved));
self.live
.send_replace(Arc::new(self.resolved.settings.clone()));
Ok(())
}
pub(crate) fn set_session(
&mut self,
env: &AppEnv,
key: &str,
value: &str,
) -> anyhow::Result<()> {
self.set_session_all(env, &[(key.to_owned(), value.to_owned())])
}
pub(crate) fn set_session_all(
&mut self,
env: &AppEnv,
pairs: &[(String, String)],
) -> anyhow::Result<()> {
let before = self.session.clone();
for (key, value) in pairs {
self.session.retain(|(existing, _)| existing != key);
self.session.push((key.clone(), value.clone()));
}
self.reload(env)
.inspect_err(|_| self.session = before.clone())?;
let keys: Vec<String> = pairs.iter().map(|(key, _)| key.clone()).collect();
if let Some(unknown) = self.unknown_among(&keys) {
self.session = before;
self.reload(env)?;
anyhow::bail!("{unknown} isn't a setting; /config lists them");
}
Ok(())
}
pub(crate) fn unknown_among(&self, keys: &[String]) -> Option<String> {
self.resolved.warnings.iter().find_map(|warning| {
let WarningAbout::Unknown { key } = &warning.about else {
return None;
};
keys.iter()
.any(|written| {
key == written
|| key
.strip_prefix(written.as_str())
.is_some_and(|rest| rest.starts_with('.'))
})
.then(|| key.to_owned())
})
}
pub(crate) fn remove_session(&mut self, env: &AppEnv, key: &str) -> anyhow::Result<bool> {
let before = self.session.clone();
self.session.retain(|(existing, _)| existing != key);
if self.session.len() == before.len() {
return Ok(false);
}
self.reload(env).inspect_err(|_| self.session = before)?;
Ok(true)
}
pub(crate) fn session_sets(&self, key: &str) -> bool {
self.session
.iter()
.any(|(set, value)| leaf_keys(set, value).iter().any(|leaf| leaf == key))
}
pub(crate) fn layer(&self, key: &str) -> String {
match self.entry(key) {
Some(entry) if entry.layer == layer::FLAG && self.session_sets(key) => {
layer::SESSION.to_owned()
}
Some(entry) => entry.layer.clone(),
None => layer::BUILT_IN.to_owned(),
}
}
pub(crate) fn workflow_from(&self, from: &str) -> Option<String> {
if from != layer::WORKFLOW {
return None;
}
let picked = self.layer("workflow");
FILE_LAYERS.contains(&picked.as_str()).then_some(picked)
}
pub(crate) fn entries(&self) -> Vec<ConfigEntry> {
self.resolved
.entries
.iter()
.map(|entry| ConfigEntry {
layer: self.layer(&entry.key),
..entry.clone()
})
.collect()
}
pub(crate) fn layer_without_session(&self, env: &AppEnv, key: &str) -> anyhow::Result<String> {
if !self.session_sets(key) {
return Ok(self.layer(key));
}
let sources = sources(env, &self.trust, self.flags.clone(), self.interactive);
let resolved = resolve(&sources)?;
Ok(resolved
.entries
.iter()
.find(|entry| entry.key == key)
.map_or_else(|| "built-in".to_owned(), |entry| entry.layer.clone()))
}
}
pub(crate) fn leaf_keys(key: &str, value: &str) -> Vec<String> {
match format!("value = {value}").parse::<toml::Table>() {
Ok(table) => match table.get("value") {
Some(toml::Value::Table(inner)) => {
let mut leaves = Vec::new();
walk_leaves(key, inner, &mut leaves);
leaves
}
_ => vec![key.to_owned()],
},
Err(_) => vec![key.to_owned()],
}
}
fn walk_leaves(prefix: &str, table: &toml::Table, leaves: &mut Vec<String>) {
for (name, value) in table {
let key = format!("{prefix}.{name}");
match value {
toml::Value::Table(inner) => walk_leaves(&key, inner, leaves),
_ => leaves.push(key),
}
}
}
pub(crate) fn sources(
env: &AppEnv,
trust: &TrustStore,
overrides: Vec<String>,
interactive: bool,
) -> SettingsSources {
let project = env.project_config();
let project_local = env.project_local_config();
SettingsSources {
global: Some(env.global_config()),
project_trusted: trust.is_trusted(&project),
project_local_trusted: trust.is_trusted(&project_local),
project: Some(project),
project_local: Some(project_local),
overrides,
interactive,
learned_workflow: None,
request: Vec::new(),
strict: false,
waiting: argonauts_crew::prompts::trust::waiting(&env.repo_root, &env.state_dir)
.all()
.collect(),
home: env.user_home.clone(),
}
}