1use serde::Deserialize;
2use std::collections::HashMap;
3use std::path::{Path, PathBuf};
4
5#[derive(Debug, Deserialize)]
6pub struct ProjectConfig {
7 pub processes: HashMap<String, ProcessDef>,
8}
9
10#[derive(Debug, Deserialize)]
11pub struct ProcessDef {
12 pub cmd: String,
13 pub cwd: Option<String>,
14 #[serde(default)]
15 pub env: HashMap<String, String>,
16 pub ready: Option<String>,
17 #[serde(default)]
18 pub depends_on: Vec<String>,
19}
20
21impl ProjectConfig {
22 pub fn startup_order(&self) -> Result<Vec<Vec<String>>, String> {
23 let mut in_degree: HashMap<&str, usize> = HashMap::new();
24 let mut dependents: HashMap<&str, Vec<&str>> = HashMap::new();
25
26 for name in self.processes.keys() {
27 in_degree.entry(name.as_str()).or_insert(0);
28 }
29 for (name, def) in &self.processes {
30 for dep in &def.depends_on {
31 if !self.processes.contains_key(dep) {
32 return Err(format!("unknown dependency: {} depends on {}", name, dep));
33 }
34 dependents.entry(dep.as_str()).or_default().push(name.as_str());
35 *in_degree.entry(name.as_str()).or_insert(0) += 1;
36 }
37 }
38
39 let mut groups = Vec::new();
40 let mut remaining = in_degree.clone();
41
42 loop {
43 let mut ready: Vec<String> = remaining.iter()
44 .filter(|(_, °)| deg == 0)
45 .map(|(&name, _)| name.to_string())
46 .collect();
47
48 if ready.is_empty() {
49 if remaining.is_empty() { break; }
50 else { return Err("dependency cycle detected".into()); }
51 }
52
53 for name in &ready {
54 remaining.remove(name.as_str());
55 if let Some(deps) = dependents.get(name.as_str()) {
56 for dep in deps {
57 if let Some(deg) = remaining.get_mut(dep) { *deg -= 1; }
58 }
59 }
60 }
61 ready.sort();
62 groups.push(ready);
63 }
64 Ok(groups)
65 }
66}
67
68pub fn discover_config(start: &Path) -> Option<PathBuf> {
69 let mut dir = start.to_path_buf();
70 loop {
71 let candidate = dir.join("agent-procs.yaml");
72 if candidate.exists() { return Some(candidate); }
73 if !dir.pop() { return None; }
74 }
75}