Skip to main content

bitrouter_runtime/
paths.rs

1use std::path::{Path, PathBuf};
2
3/// All resolved paths for a bitrouter instance.
4///
5/// The canonical derivation is from a **home directory**:
6///   `<home>/bitrouter.yaml`, `<home>/.env`, `<home>/run/`, `<home>/logs/`
7///
8/// Individual paths can be overridden via CLI flags.
9#[derive(Debug, Clone)]
10pub struct RuntimePaths {
11    pub home_dir: PathBuf,
12    pub config_file: PathBuf,
13    pub env_file: PathBuf,
14    pub runtime_dir: PathBuf,
15    pub log_dir: PathBuf,
16}
17
18impl RuntimePaths {
19    /// Derive all paths from a resolved home directory.
20    pub fn from_home(home: impl Into<PathBuf>) -> Self {
21        let home = home.into();
22        Self {
23            config_file: home.join("bitrouter.yaml"),
24            env_file: home.join(".env"),
25            runtime_dir: home.join("run"),
26            log_dir: home.join("logs"),
27            home_dir: home,
28        }
29    }
30}
31
32/// Overrides that can be applied to individual paths after resolution.
33#[derive(Debug, Clone, Default)]
34pub struct PathOverrides {
35    pub config_file: Option<PathBuf>,
36    pub env_file: Option<PathBuf>,
37    pub runtime_dir: Option<PathBuf>,
38    pub log_dir: Option<PathBuf>,
39}
40
41impl PathOverrides {
42    pub fn apply(self, mut paths: RuntimePaths) -> RuntimePaths {
43        if let Some(v) = self.config_file {
44            paths.config_file = v;
45        }
46        if let Some(v) = self.env_file {
47            paths.env_file = v;
48        }
49        if let Some(v) = self.runtime_dir {
50            paths.runtime_dir = v;
51        }
52        if let Some(v) = self.log_dir {
53            paths.log_dir = v;
54        }
55        paths
56    }
57}
58
59/// Resolve the bitrouter home directory.
60///
61/// Priority:
62/// 1. Explicit `--home-dir` override
63/// 2. CWD, if `./bitrouter.yaml` exists
64/// 3. `$BITROUTER_HOME` environment variable
65/// 4. `~/.bitrouter` (scaffolded if missing)
66pub fn resolve_home(explicit: Option<&Path>) -> RuntimePaths {
67    if let Some(dir) = explicit {
68        let abs = std::path::absolute(dir).unwrap_or_else(|_| dir.to_path_buf());
69        return RuntimePaths::from_home(abs);
70    }
71
72    // CWD check
73    let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
74    if cwd.join("bitrouter.yaml").exists() {
75        return RuntimePaths::from_home(cwd);
76    }
77
78    // $BITROUTER_HOME
79    if let Ok(home_env) = std::env::var("BITROUTER_HOME") {
80        let p = PathBuf::from(home_env);
81        if p.is_dir() {
82            return RuntimePaths::from_home(p);
83        }
84    }
85
86    // ~/.bitrouter (scaffold if needed)
87    let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
88    let default_home = home.join(".bitrouter");
89    if !default_home.exists() {
90        let _ = scaffold_home(&default_home);
91    }
92    RuntimePaths::from_home(default_home)
93}
94
95/// Create the default home directory with placeholder files.
96fn scaffold_home(home: &Path) -> std::io::Result<()> {
97    std::fs::create_dir_all(home)?;
98    std::fs::create_dir_all(home.join("run"))?;
99    std::fs::create_dir_all(home.join("logs"))?;
100
101    let config_path = home.join("bitrouter.yaml");
102    if !config_path.exists() {
103        std::fs::write(
104            &config_path,
105            "\
106# BitRouter configuration
107# See https://github.com/bitrouter/bitrouter for documentation.
108#
109# server:
110#   listen: \"127.0.0.1:8787\"
111#
112# providers:
113#   openai:
114#     api_key: \"${OPENAI_API_KEY}\"
115#
116# models:
117#   my-model:
118#     strategy: priority
119#     endpoints:
120#       - provider: openai
121#         model_id: gpt-4o
122",
123        )?;
124    }
125
126    let env_path = home.join(".env");
127    if !env_path.exists() {
128        std::fs::write(
129            &env_path,
130            "\
131# BitRouter environment variables
132# Place your API keys here. This file is ignored by git.
133#
134# OPENAI_API_KEY=sk-...
135# ANTHROPIC_API_KEY=sk-ant-...
136# GOOGLE_API_KEY=...
137",
138        )?;
139    }
140
141    let gitignore_path = home.join(".gitignore");
142    if !gitignore_path.exists() {
143        std::fs::write(
144            &gitignore_path,
145            "\
146logs/
147run/
148.env
149",
150        )?;
151    }
152
153    Ok(())
154}