use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use crate::config_resolve::ConfigTier;
fn user_config_path_from(xdg_config_home: Option<&OsStr>, home: Option<&OsStr>) -> Option<PathBuf> {
let base = xdg_config_home
.map(PathBuf::from)
.filter(|p| p.is_absolute())
.or_else(|| home.map(|h| PathBuf::from(h).join(".config")))?;
Some(base.join("cortexkit").join("aft.jsonc"))
}
pub fn cortexkit_user_config_path() -> Option<PathBuf> {
let xdg = std::env::var_os("XDG_CONFIG_HOME");
let home = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE"));
user_config_path_from(xdg.as_deref(), home.as_deref())
}
fn cortexkit_project_config_path(project_root: &Path) -> PathBuf {
project_root.join(".cortexkit").join("aft.jsonc")
}
fn read_tiers_from(user_config_path: Option<&Path>, project_config_path: &Path) -> Vec<ConfigTier> {
let mut tiers = Vec::new();
if let Some(user_path) = user_config_path {
if let Ok(doc) = std::fs::read_to_string(user_path) {
tiers.push(ConfigTier {
tier: "user".to_string(),
source: user_path.to_string_lossy().into_owned(),
doc,
});
}
}
if let Ok(doc) = std::fs::read_to_string(project_config_path) {
tiers.push(ConfigTier {
tier: "project".to_string(),
source: project_config_path.to_string_lossy().into_owned(),
doc,
});
}
tiers
}
pub fn read_local_cortexkit_config_tiers(
user_config_path: Option<&Path>,
project_root: &Path,
) -> Vec<ConfigTier> {
read_tiers_from(
user_config_path,
&cortexkit_project_config_path(project_root),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
use crate::config_resolve::resolve_config_onto;
#[test]
fn user_path_prefers_absolute_xdg_config_home() {
let xdg = if cfg!(windows) {
r"C:\xdg\cfg"
} else {
"/xdg/cfg"
};
let home = if cfg!(windows) {
r"C:\home\u"
} else {
"/home/u"
};
let path = user_config_path_from(Some(OsStr::new(xdg)), Some(OsStr::new(home)));
let expected = PathBuf::from(xdg).join("cortexkit").join("aft.jsonc");
assert_eq!(path, Some(expected));
}
#[test]
fn user_path_falls_back_to_home_config_when_xdg_unset() {
let path = user_config_path_from(None, Some(OsStr::new("/home/u")));
assert_eq!(
path,
Some(PathBuf::from("/home/u/.config/cortexkit/aft.jsonc"))
);
}
#[test]
fn user_path_treats_empty_xdg_as_unset() {
let path = user_config_path_from(Some(OsStr::new("")), Some(OsStr::new("/home/u")));
assert_eq!(
path,
Some(PathBuf::from("/home/u/.config/cortexkit/aft.jsonc"))
);
}
#[test]
fn user_path_none_when_no_home_and_no_xdg() {
assert_eq!(user_config_path_from(None, None), None);
}
#[test]
fn reads_user_and_project_with_raw_jsonc_docs() {
let dir = tempfile::tempdir().unwrap();
let user = dir.path().join("user-aft.jsonc");
let project = dir.path().join("project-aft.jsonc");
std::fs::write(&user, "{\n // user\n \"search_index\": true\n}").unwrap();
std::fs::write(&project, "{ \"semantic_search\": false }").unwrap();
let tiers = read_tiers_from(Some(&user), &project);
assert_eq!(tiers.len(), 2);
assert_eq!(tiers[0].tier, "user");
assert!(tiers[0].doc.contains("// user"));
assert_eq!(tiers[1].tier, "project");
assert_eq!(tiers[1].source, project.to_string_lossy());
}
#[test]
fn missing_files_yield_no_tiers() {
let dir = tempfile::tempdir().unwrap();
let tiers = read_tiers_from(
Some(&dir.path().join("nope-user.jsonc")),
&dir.path().join("nope-project.jsonc"),
);
assert!(tiers.is_empty());
}
const PRIVILEGED_DOC: &str = r#"{ "semantic": { "api_key_env": "SECRET_KEY" } }"#;
#[test]
fn user_file_privileged_field_is_trusted_project_file_is_dropped() {
let dir = tempfile::tempdir().unwrap();
let user = dir.path().join("user-aft.jsonc");
std::fs::write(&user, PRIVILEGED_DOC).unwrap();
let project_root = dir.path();
let project_cfg_dir = project_root.join(".cortexkit");
std::fs::create_dir_all(&project_cfg_dir).unwrap();
std::fs::write(
project_cfg_dir.join("aft.jsonc"),
r#"{ "semantic": { "api_key_env": "PROJECT_INJECTED" } }"#,
)
.unwrap();
let tiers = read_local_cortexkit_config_tiers(Some(&user), project_root);
let mut base = Config::default();
let dropped = resolve_config_onto(&tiers, &mut base);
assert_eq!(
base.semantic.api_key_env.as_deref(),
Some("SECRET_KEY"),
"user-file privileged field must be trusted"
);
assert!(
dropped.iter().any(|d| d.key == "semantic.api_key_env"),
"project-file privileged field must be dropped by the resolver"
);
}
}