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