use super::defaults::scaffold_defaults;
use crate::common::Result;
use std::path::PathBuf;
pub fn collet_home(override_path: Option<&str>) -> PathBuf {
if let Ok(env) = std::env::var("COLLET_HOME") {
return expand_tilde(&env);
}
if let Some(p) = override_path {
return expand_tilde(p);
}
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".collet")
}
pub fn config_file_path() -> PathBuf {
collet_home(None).join("config.toml")
}
pub fn secrets_file_path() -> PathBuf {
collet_home(None).join(".secrets")
}
pub fn logs_dir() -> PathBuf {
collet_home(None).join("logs")
}
pub fn bench_log_path() -> PathBuf {
logs_dir().join("bench.jsonl")
}
pub fn dated_log_path(dir: &std::path::Path, prefix: &str, keep_days: u64) -> PathBuf {
let today = chrono::Local::now().format("%Y-%m-%d").to_string();
let current_name = format!("{prefix}.{today}.log");
let cutoff = std::time::Duration::from_secs(keep_days * 24 * 60 * 60);
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if name.starts_with(&format!("{prefix}."))
&& name.ends_with(".log")
&& *name != current_name
{
let is_old = entry
.metadata()
.ok()
.and_then(|m| m.modified().ok())
.and_then(|t| t.elapsed().ok())
.map(|d| d > cutoff)
.unwrap_or(false);
if is_old {
let _ = std::fs::remove_file(entry.path());
}
}
}
}
dir.join(current_name)
}
pub(super) fn expand_tilde(path: &str) -> PathBuf {
if let Some(rest) = path.strip_prefix("~/") {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(rest)
} else if path == "~" {
dirs::home_dir().unwrap_or_else(|| PathBuf::from("."))
} else {
PathBuf::from(path)
}
}
pub fn project_data_dir(working_dir: &str) -> PathBuf {
project_data_dir_with(working_dir, None)
}
pub fn project_data_dir_with(working_dir: &str, home_override: Option<&str>) -> PathBuf {
let canonical =
std::fs::canonicalize(working_dir).unwrap_or_else(|_| PathBuf::from(working_dir));
let hash = blake3::hash(canonical.to_string_lossy().as_bytes());
let hex = hash.to_hex();
let short = &hex.as_str()[..12];
collet_home(home_override).join("projects").join(short)
}
pub fn ensure_collet_home() -> Result<PathBuf> {
let dir = collet_home(None);
if !dir.exists() {
std::fs::create_dir_all(&dir).map_err(|e| {
crate::common::AgentError::Config(format!(
"Failed to create collet home: {} - {}",
dir.display(),
e
))
})?;
}
let example = dir.join("mcp.example.json");
if !example.exists() {
let _ = std::fs::write(&example, MCP_EXAMPLE_JSON);
}
let _ = scaffold_defaults(&dir);
Ok(dir)
}
pub const MCP_EXAMPLE_JSON: &str = r##"{
"mcpServers": {
"context7": {
"command": "npx",
"args": ["-y", "@upstash/context7-mcp@latest"],
"description": "Library documentation lookup via Context7"
},
"playwright": {
"command": "npx",
"args": ["-y", "@playwright/mcp@latest"],
"env": {
"DISPLAY": ":0"
},
"description": "Browser automation and E2E testing"
},
"example-http": {
"url": "https://api.example.com/mcp",
"headers": {
"Authorization": "Bearer ${EXAMPLE_API_KEY}"
},
"enabled": false,
"description": "Example HTTP MCP server (disabled by default)"
}
}
}
"##;
#[inline]
pub fn config_dir() -> PathBuf {
collet_home(None)
}
#[inline]
pub fn ensure_config_dir() -> Result<PathBuf> {
ensure_collet_home()
}