use crate::hook::Host;
use std::path::PathBuf;
fn home() -> PathBuf {
PathBuf::from(std::env::var_os("HOME").unwrap_or_default())
}
pub fn data_dir() -> PathBuf {
std::env::var_os("XDG_DATA_HOME")
.map(PathBuf::from)
.unwrap_or_else(|| home().join(".local/share"))
.join("ski")
}
pub fn index_path(host: Host) -> PathBuf {
let name = match host {
Host::Claude => "index.json",
Host::Opencode => "index-opencode.json",
};
data_dir().join(name)
}
pub fn config_dir() -> PathBuf {
std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.unwrap_or_else(|| home().join(".config"))
.join("ski")
}
pub fn model_cache_dir() -> PathBuf {
config_dir().join("models")
}
pub fn config_path() -> PathBuf {
config_dir().join("config.toml")
}
pub fn claude_settings_path() -> PathBuf {
home().join(".claude").join("settings.json")
}
pub fn opencode_plugin_dir() -> PathBuf {
std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.unwrap_or_else(|| home().join(".config"))
.join("opencode")
.join("plugin")
}
pub fn state_dir() -> PathBuf {
std::env::var_os("XDG_STATE_HOME")
.map(PathBuf::from)
.unwrap_or_else(|| home().join(".local/state"))
.join("ski")
}
pub fn sessions_dir() -> PathBuf {
state_dir().join("sessions")
}
pub fn telemetry_path() -> PathBuf {
state_dir().join("telemetry.jsonl")
}
pub fn session_path(session_id: &str) -> PathBuf {
sessions_dir().join(format!("{}.json", sanitize(session_id)))
}
fn sanitize(id: &str) -> String {
let s: String = id
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
c
} else {
'_'
}
})
.collect();
if s.is_empty() {
"default".to_string()
} else {
s
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn session_path_sanitizes_traversal() {
let p = session_path("../../etc/passwd");
let name = p.file_name().unwrap().to_str().unwrap();
assert!(!name.contains('/'));
assert!(!name.contains('.') || name.ends_with(".json"));
assert_eq!(p.parent().unwrap(), sessions_dir());
}
#[test]
fn empty_id_falls_back() {
assert!(session_path("").ends_with("default.json"));
}
}