use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct RuntimePaths {
pub home_dir: PathBuf,
pub config_file: PathBuf,
pub env_file: PathBuf,
pub runtime_dir: PathBuf,
pub log_dir: PathBuf,
pub token_store_file: PathBuf,
#[cfg(feature = "tui")]
pub cache_dir: PathBuf,
#[cfg(feature = "tui")]
pub agents_dir: PathBuf,
#[cfg(feature = "tui")]
pub agent_state_file: PathBuf,
}
impl RuntimePaths {
pub fn from_home(home: impl Into<PathBuf>) -> Self {
let home = home.into();
#[cfg(feature = "tui")]
let agents_dir = home.join("agents");
Self {
config_file: home.join("bitrouter.yaml"),
env_file: home.join(".env"),
runtime_dir: home.join("run"),
log_dir: home.join("logs"),
token_store_file: home.join("tokens.json"),
#[cfg(feature = "tui")]
cache_dir: home.join("cache"),
#[cfg(feature = "tui")]
agent_state_file: agents_dir.join("state.json"),
#[cfg(feature = "tui")]
agents_dir,
home_dir: home,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct PathOverrides {
pub config_file: Option<PathBuf>,
pub env_file: Option<PathBuf>,
pub runtime_dir: Option<PathBuf>,
pub log_dir: Option<PathBuf>,
}
impl PathOverrides {
pub fn apply(self, mut paths: RuntimePaths) -> RuntimePaths {
if let Some(v) = self.config_file {
paths.config_file = v;
}
if let Some(v) = self.env_file {
paths.env_file = v;
}
if let Some(v) = self.runtime_dir {
paths.runtime_dir = v;
}
if let Some(v) = self.log_dir {
paths.log_dir = v;
}
paths
}
}
pub fn resolve_home(explicit: Option<&Path>) -> RuntimePaths {
if let Some(dir) = explicit {
let abs = std::path::absolute(dir).unwrap_or_else(|_| dir.to_path_buf());
return RuntimePaths::from_home(abs);
}
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
if cwd.join("bitrouter.yaml").exists() {
return RuntimePaths::from_home(cwd);
}
if let Ok(home_env) = std::env::var("BITROUTER_HOME") {
let p = PathBuf::from(home_env);
if p.is_dir() {
return RuntimePaths::from_home(p);
}
}
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
let default_home = home.join(".bitrouter");
if !default_home.exists() {
let _ = scaffold_home(&default_home);
}
RuntimePaths::from_home(default_home)
}
fn scaffold_home(home: &Path) -> std::io::Result<()> {
std::fs::create_dir_all(home)?;
std::fs::create_dir_all(home.join("run"))?;
std::fs::create_dir_all(home.join("logs"))?;
std::fs::create_dir_all(home.join("cache"))?;
std::fs::create_dir_all(home.join("agents"))?;
let config_path = home.join("bitrouter.yaml");
if !config_path.exists() {
std::fs::write(&config_path, include_str!("../../templates/minimal.yaml"))?;
}
let env_path = home.join(".env");
if !env_path.exists() {
std::fs::write(
&env_path,
"\
# BitRouter environment variables
# Place your API keys here. This file is ignored by git.
#
# OPENAI_API_KEY=sk-...
# ANTHROPIC_API_KEY=sk-ant-...
# GOOGLE_API_KEY=...
",
)?;
}
let gitignore_path = home.join(".gitignore");
if !gitignore_path.exists() {
std::fs::write(
&gitignore_path,
"\
# Secrets and credentials
.env
.keys/
tokens.json
# Runtime state
bitrouter.db
logs/
run/
",
)?;
}
let readme_path = home.join("README.md");
if !readme_path.exists() {
std::fs::write(&readme_path, include_str!("../../templates/home_readme.md"))?;
}
Ok(())
}