1use std::collections::BTreeMap;
4use std::path::{Path, PathBuf};
5
6use anyhow::{Context, Result};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Runtime {
11 pub binary: String,
13 #[serde(default)]
14 pub supports_mcp: bool,
15 #[serde(default)]
16 pub session_resume: Option<String>,
17 #[serde(default)]
18 pub default_model: Option<String>,
19 #[serde(default)]
20 pub env: BTreeMap<String, String>,
21
22 #[serde(default)]
25 pub rate_limit_patterns: Vec<RateLimitPattern>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct RateLimitPattern {
33 pub r#match: String,
35 #[serde(default)]
38 pub resets_at_capture: Option<String>,
39 #[serde(default)]
42 pub resets_in_capture: Option<String>,
43}
44
45pub fn load_all(root: &Path) -> Result<BTreeMap<String, Runtime>> {
48 let dir = root.join("runtimes");
49 let mut map = BTreeMap::new();
50 if !dir.exists() {
51 return Ok(map);
52 }
53 for entry in std::fs::read_dir(&dir).with_context(|| format!("read {}", dir.display()))? {
54 let entry = entry?;
55 let path: PathBuf = entry.path();
56 if path.extension().and_then(|s| s.to_str()) != Some("yaml") {
57 continue;
58 }
59 let stem = path
60 .file_stem()
61 .and_then(|s| s.to_str())
62 .unwrap_or_default()
63 .to_string();
64 let content =
65 std::fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?;
66 let r: Runtime =
67 serde_yaml::from_str(&content).with_context(|| format!("parse {}", path.display()))?;
68 map.insert(stem, r);
69 }
70 Ok(map)
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 #[test]
78 fn load_nonexistent_returns_empty() {
79 let tmp = tempfile::tempdir().unwrap();
80 let m = load_all(tmp.path()).unwrap();
81 assert!(m.is_empty());
82 }
83
84 #[test]
85 fn load_parses_runtimes() {
86 let tmp = tempfile::tempdir().unwrap();
87 let dir = tmp.path().join("runtimes");
88 std::fs::create_dir_all(&dir).unwrap();
89 std::fs::write(
90 dir.join("claude-code.yaml"),
91 "binary: claude\nsupports_mcp: true\ndefault_model: claude-opus-4-7\n",
92 )
93 .unwrap();
94 std::fs::write(
95 dir.join("codex.yaml"),
96 "binary: codex\nsupports_mcp: true\n",
97 )
98 .unwrap();
99 let m = load_all(tmp.path()).unwrap();
100 assert_eq!(m.len(), 2);
101 assert_eq!(m["claude-code"].binary, "claude");
102 assert!(m["claude-code"].supports_mcp);
103 assert_eq!(m["codex"].binary, "codex");
104 }
105}