Skip to main content

agent_procs/
config.rs

1use serde::Deserialize;
2use std::collections::HashMap;
3use std::path::{Path, PathBuf};
4
5pub const DEFAULT_SESSION: &str = "default";
6
7#[derive(Debug, Deserialize)]
8pub struct ProjectConfig {
9    pub session: Option<String>,
10    pub processes: HashMap<String, ProcessDef>,
11}
12
13#[derive(Debug, Deserialize)]
14pub struct ProcessDef {
15    pub cmd: String,
16    pub cwd: Option<String>,
17    #[serde(default)]
18    pub env: HashMap<String, String>,
19    pub ready: Option<String>,
20    #[serde(default)]
21    pub depends_on: Vec<String>,
22}
23
24impl ProjectConfig {
25    pub fn startup_order(&self) -> Result<Vec<Vec<String>>, String> {
26        let mut in_degree: HashMap<&str, usize> = HashMap::new();
27        let mut dependents: HashMap<&str, Vec<&str>> = HashMap::new();
28
29        for name in self.processes.keys() {
30            in_degree.entry(name.as_str()).or_insert(0);
31        }
32        for (name, def) in &self.processes {
33            for dep in &def.depends_on {
34                if !self.processes.contains_key(dep) {
35                    return Err(format!("unknown dependency: {} depends on {}", name, dep));
36                }
37                dependents.entry(dep.as_str()).or_default().push(name.as_str());
38                *in_degree.entry(name.as_str()).or_insert(0) += 1;
39            }
40        }
41
42        let mut groups = Vec::new();
43        let mut remaining = in_degree.clone();
44
45        loop {
46            let mut ready: Vec<String> = remaining.iter()
47                .filter(|(_, &deg)| deg == 0)
48                .map(|(&name, _)| name.to_string())
49                .collect();
50
51            if ready.is_empty() {
52                if remaining.is_empty() { break; }
53                else { return Err("dependency cycle detected".into()); }
54            }
55
56            for name in &ready {
57                remaining.remove(name.as_str());
58                if let Some(deps) = dependents.get(name.as_str()) {
59                    for dep in deps {
60                        if let Some(deg) = remaining.get_mut(dep) { *deg -= 1; }
61                    }
62                }
63            }
64            ready.sort();
65            groups.push(ready);
66        }
67        Ok(groups)
68    }
69}
70
71pub fn discover_config(start: &Path) -> Option<PathBuf> {
72    let mut dir = start.to_path_buf();
73    loop {
74        let candidate = dir.join("agent-procs.yaml");
75        if candidate.exists() { return Some(candidate); }
76        if !dir.pop() { return None; }
77    }
78}
79
80pub fn load_config(config_path: Option<&str>) -> Result<(PathBuf, ProjectConfig), String> {
81    let path = match config_path {
82        Some(p) => PathBuf::from(p),
83        None => discover_config(&std::env::current_dir().map_err(|e| format!("cannot get cwd: {}", e))?)
84            .ok_or_else(|| "no agent-procs.yaml found".to_string())?,
85    };
86    let content = std::fs::read_to_string(&path)
87        .map_err(|e| format!("cannot read config: {}", e))?;
88    let config: ProjectConfig = serde_yaml::from_str(&content)
89        .map_err(|e| format!("invalid config: {}", e))?;
90    Ok((path, config))
91}
92
93pub fn resolve_session<'a>(cli_session: Option<&'a str>, config_session: Option<&'a str>) -> &'a str {
94    cli_session.or(config_session).unwrap_or(DEFAULT_SESSION)
95}