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 == 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}