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
38                    .entry(dep.as_str())
39                    .or_default()
40                    .push(name.as_str());
41                *in_degree.entry(name.as_str()).or_insert(0) += 1;
42            }
43        }
44
45        let mut groups = Vec::new();
46        let mut remaining = in_degree.clone();
47
48        loop {
49            let mut ready: Vec<String> = remaining
50                .iter()
51                .filter(|(_, &deg)| deg == 0)
52                .map(|(&name, _)| name.to_string())
53                .collect();
54
55            if ready.is_empty() {
56                if remaining.is_empty() {
57                    break;
58                } else {
59                    return Err("dependency cycle detected".into());
60                }
61            }
62
63            for name in &ready {
64                remaining.remove(name.as_str());
65                if let Some(deps) = dependents.get(name.as_str()) {
66                    for dep in deps {
67                        if let Some(deg) = remaining.get_mut(dep) {
68                            *deg -= 1;
69                        }
70                    }
71                }
72            }
73            ready.sort();
74            groups.push(ready);
75        }
76        Ok(groups)
77    }
78}
79
80pub fn discover_config(start: &Path) -> Option<PathBuf> {
81    let mut dir = start.to_path_buf();
82    loop {
83        let candidate = dir.join("agent-procs.yaml");
84        if candidate.exists() {
85            return Some(candidate);
86        }
87        if !dir.pop() {
88            return None;
89        }
90    }
91}
92
93pub fn load_config(config_path: Option<&str>) -> Result<(PathBuf, ProjectConfig), String> {
94    let path = match config_path {
95        Some(p) => PathBuf::from(p),
96        None => {
97            discover_config(&std::env::current_dir().map_err(|e| format!("cannot get cwd: {}", e))?)
98                .ok_or_else(|| "no agent-procs.yaml found".to_string())?
99        }
100    };
101    let content =
102        std::fs::read_to_string(&path).map_err(|e| format!("cannot read config: {}", e))?;
103    let config: ProjectConfig =
104        serde_yaml::from_str(&content).map_err(|e| format!("invalid config: {}", e))?;
105    Ok((path, config))
106}
107
108pub fn resolve_session<'a>(
109    cli_session: Option<&'a str>,
110    config_session: Option<&'a str>,
111) -> &'a str {
112    cli_session.or(config_session).unwrap_or(DEFAULT_SESSION)
113}