use std::sync::Arc;
use serde_json::{Value, json};
use trusty_common::console_metrics::{ConsoleMetricsReport, ServiceHealth, make_report};
use crate::core::auto_resume;
use crate::core::trusty_tools_config::{self, TrustyToolsConfig};
use crate::daemon::state::DaemonState;
use crate::supervisor::metrics::FleetMetrics;
const METRICS_SCHEMA_VERSION: u32 = 1;
const SERVICE_VERSION: &str = env!("CARGO_PKG_VERSION");
async fn fleet_snapshot(state: &Arc<DaemonState>) -> Value {
let mgr = state.session_manager().await;
let records = mgr.list().await;
let fleet = FleetMetrics::from_records(&records);
let desired = auto_resume::read_desired().unwrap_or(false);
let env = auto_resume::effective_from_env();
json!({
"fleet": fleet,
"auto_resume": {
"desired": desired,
"env": env,
"pending_restart": desired != env,
},
})
}
pub async fn console_metrics(state: &Arc<DaemonState>) -> Result<Value, String> {
let snapshot = fleet_snapshot(state).await;
let errored = snapshot
.get("fleet")
.and_then(|f| f.get("errored"))
.and_then(Value::as_u64)
.unwrap_or(0);
let status = if errored > 0 {
ServiceHealth::Degraded
} else {
ServiceHealth::Ok
};
let report: ConsoleMetricsReport = make_report(
"trusty-mpm",
"Trusty MPM",
SERVICE_VERSION,
status,
snapshot,
METRICS_SCHEMA_VERSION,
);
serde_json::to_value(&report).map_err(|e| format!("serialising console_metrics report: {e}"))
}
pub async fn supervisor_status(state: &Arc<DaemonState>) -> Result<Value, String> {
Ok(fleet_snapshot(state).await)
}
pub async fn auto_resume_set(enabled: bool) -> Result<Value, String> {
auto_resume::write_desired(enabled)
.map_err(|e| format!("persisting auto_resume desired state: {e}"))?;
let desired = enabled;
let env = auto_resume::effective_from_env();
Ok(json!({
"desired": desired,
"env": env,
"pending_restart": desired != env,
}))
}
fn config_to_json(config: &TrustyToolsConfig) -> Value {
json!({
"workspace_root_template": config.workspace_root_template,
"auto_resume": config.auto_resume,
"default_model": config.default_model,
"github": config.github,
"untracked_sync": config.untracked_sync,
"projects": config.projects,
"workspace_root": trusty_tools_config::workspace_root(config).to_string_lossy(),
})
}
pub fn config_read() -> Result<Value, String> {
Ok(config_to_json(&TrustyToolsConfig::load()))
}
#[allow(clippy::too_many_arguments)]
pub fn config_write(
workspace_root_template: Option<&str>,
auto_resume: Option<bool>,
default_model: Option<&str>,
project_name: Option<&str>,
github_config_dir: Option<&str>,
github_token_env: Option<&str>,
github_account: Option<&str>,
github_host: Option<&str>,
commit_name: Option<&str>,
commit_email: Option<&str>,
untracked_sync_patterns: Option<Vec<String>>,
untracked_sync_enabled: Option<bool>,
) -> Result<Value, String> {
let mut config = TrustyToolsConfig::load();
apply_config_write(
&mut config,
workspace_root_template,
auto_resume,
default_model,
project_name,
github_config_dir,
github_token_env,
github_account,
github_host,
commit_name,
commit_email,
untracked_sync_patterns,
untracked_sync_enabled,
)?;
trusty_common::crate_config::save(trusty_tools_config::CRATE_NAME, &config)
.map_err(|e| format!("persisting trusty-mpm config: {e}"))?;
Ok(config_to_json(&config))
}
#[allow(clippy::too_many_arguments)]
fn apply_config_write(
config: &mut TrustyToolsConfig,
workspace_root_template: Option<&str>,
auto_resume: Option<bool>,
default_model: Option<&str>,
project_name: Option<&str>,
github_config_dir: Option<&str>,
github_token_env: Option<&str>,
github_account: Option<&str>,
github_host: Option<&str>,
commit_name: Option<&str>,
commit_email: Option<&str>,
untracked_sync_patterns: Option<Vec<String>>,
untracked_sync_enabled: Option<bool>,
) -> Result<(), String> {
if let Some(t) = workspace_root_template {
config.workspace_root_template = Some(t.to_string());
}
if let Some(a) = auto_resume {
config.auto_resume = Some(a);
}
if let Some(m) = default_model {
config.default_model = Some(m.to_string());
}
let has_github_edit = github_config_dir.is_some()
|| github_token_env.is_some()
|| github_account.is_some()
|| github_host.is_some();
let has_commit_edit = commit_name.is_some() || commit_email.is_some();
let has_untracked_sync_edit =
untracked_sync_patterns.is_some() || untracked_sync_enabled.is_some();
if !has_github_edit && !has_commit_edit && !has_untracked_sync_edit {
return Ok(());
}
match project_name {
None => {
if has_github_edit {
config.github = Some(merge_github_config(
config.github.take(),
github_config_dir,
github_token_env,
github_account,
github_host,
));
}
if has_untracked_sync_edit {
config.untracked_sync = Some(merge_untracked_sync_config(
config.untracked_sync.take(),
untracked_sync_patterns,
untracked_sync_enabled,
));
}
if has_commit_edit {
return Err(
"commit_name/commit_email require project_name (commit identity is \
project-scoped; there is no global commit identity tier)"
.to_string(),
);
}
}
Some(name) => {
let entry = config
.projects
.iter_mut()
.find(|p| p.name == name)
.ok_or_else(|| {
format!(
"project '{name}' not found in config.projects; register it first \
(e.g. via `project_register` or a static config.yaml entry) before \
setting its github/commit identity"
)
})?;
if has_github_edit {
entry.github = Some(merge_github_config(
entry.github.take(),
github_config_dir,
github_token_env,
github_account,
github_host,
));
}
if has_untracked_sync_edit {
entry.untracked_sync = Some(merge_untracked_sync_config(
entry.untracked_sync.take(),
untracked_sync_patterns,
untracked_sync_enabled,
));
}
if let Some(n) = commit_name {
entry.commit_name = Some(n.to_string());
}
if let Some(e) = commit_email {
entry.commit_email = Some(e.to_string());
}
}
}
Ok(())
}
fn merge_github_config(
existing: Option<trusty_tools_config::GithubConfig>,
config_dir: Option<&str>,
token_env: Option<&str>,
account: Option<&str>,
host: Option<&str>,
) -> trusty_tools_config::GithubConfig {
let mut cfg = existing.unwrap_or_default();
if let Some(d) = config_dir {
cfg.config_dir = Some(d.into());
}
if let Some(t) = token_env {
cfg.token_env = Some(t.to_string());
}
if let Some(a) = account {
cfg.account = Some(a.to_string());
}
if let Some(h) = host {
cfg.host = Some(h.to_string());
}
cfg
}
fn merge_untracked_sync_config(
existing: Option<trusty_tools_config::UntrackedSyncConfig>,
patterns: Option<Vec<String>>,
enabled: Option<bool>,
) -> trusty_tools_config::UntrackedSyncConfig {
let mut cfg = existing.unwrap_or_default();
if let Some(p) = patterns {
cfg.patterns = Some(p);
}
if let Some(e) = enabled {
cfg.enabled = Some(e);
}
cfg
}
#[cfg(test)]
mod tests;