use super::*;
use std::collections::HashMap;
use std::io::IsTerminal;
use std::path::{Path, PathBuf};
#[allow(clippy::large_enum_variant)]
pub enum ConfigLoad {
Loaded { config: CartogConfig, path: PathBuf },
Missing,
Rejected { path: PathBuf },
}
impl ConfigLoad {
pub fn config_or_default(self) -> CartogConfig {
match self {
ConfigLoad::Loaded { config, .. } => config,
_ => CartogConfig::default(),
}
}
pub fn path(&self) -> Option<&Path> {
match self {
ConfigLoad::Loaded { path, .. } | ConfigLoad::Rejected { path } => Some(path),
ConfigLoad::Missing => None,
}
}
pub fn is_rejected(&self) -> bool {
matches!(self, ConfigLoad::Rejected { .. })
}
}
pub fn load_config() -> ConfigLoad {
match local_config_path() {
Some(p) => match read_config(&p) {
Some(config) => ConfigLoad::Loaded { config, path: p },
None => ConfigLoad::Rejected { path: p },
},
None => ConfigLoad::Missing,
}
}
fn local_config_path() -> Option<PathBuf> {
let mut dir = std::env::current_dir().ok()?;
loop {
let candidate = dir.join(".cartog.toml");
if candidate.exists() {
return Some(candidate);
}
if dir.join(".git").exists() {
return None;
}
if !dir.pop() {
break;
}
}
None
}
const KNOWN_CONFIG_SECTIONS: &[&str] = &[
"database",
"embedding",
"reranker",
"rag",
"remote",
"security",
"lsp",
"index",
];
pub(crate) fn unknown_sections(raw: &toml::value::Table) -> Vec<&str> {
raw.keys()
.map(String::as_str)
.filter(|k| !KNOWN_CONFIG_SECTIONS.contains(k))
.collect()
}
pub(crate) fn config_diagnostics_visible() -> bool {
std::io::stderr().is_terminal()
}
fn warn_unknown_sections(raw: &toml::value::Table, path: &Path) {
if !config_diagnostics_visible() {
return;
}
for key in unknown_sections(raw) {
eprintln!(
"cartog: warning: unknown config key '{key}' in {} (ignored)",
path.display()
);
}
}
pub(crate) fn read_config(path: &Path) -> Option<CartogConfig> {
let text = match std::fs::read_to_string(path) {
Ok(t) => t,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return None,
Err(e) => {
eprintln!("cartog: error reading {}: {e}", path.display());
return None;
}
};
if let Ok(raw) = toml::from_str::<toml::value::Table>(&text) {
if let Some(toml::Value::Table(remote)) = raw.get("remote") {
if let Err(msg) = validate_remote_no_credentials(remote) {
eprintln!("cartog: error in {}: {msg}", path.display());
return None;
}
}
warn_unknown_sections(&raw, path);
}
let parsed = match toml::from_str::<CartogConfig>(&text) {
Ok(cfg) => cfg,
Err(e) => {
eprintln!("cartog: warning: failed to parse {}: {e}", path.display());
return None;
}
};
if let Some(remote) = parsed.remote.as_ref() {
if let Err(msg) = validate_endpoint(remote.endpoint.as_deref()) {
eprintln!("cartog: error in {}: {msg}", path.display());
return None;
}
}
if let Err(msg) = validate_providers(&parsed) {
eprintln!("cartog: error in {}: {msg}", path.display());
return None;
}
if let Err(msg) = validate_lsp_overrides(&parsed) {
eprintln!("cartog: error in {}: {msg}", path.display());
return None;
}
if let Err(msg) = to_walk_filter(&parsed) {
eprintln!("cartog: error in {}: {msg}", path.display());
return None;
}
Some(parsed)
}
fn validate_lsp_overrides(config: &CartogConfig) -> Result<(), String> {
if let Some(lsp) = config.lsp.as_ref() {
for (lang, cfg) in &lsp.langs {
if cfg.command.is_empty() {
return Err(format!(
"[lsp.{lang}] command is empty; provide at least the executable, \
e.g. command = [\"some-lsp\", \"--stdio\"]"
));
}
#[cfg(feature = "lsp")]
if !cartog_lsp::servers::has_server_spec(lang) {
return Err(format!(
"[lsp.{lang}] is not a recognized cartog language; \
overrides are keyed by language (rust, python, go, dart, ...)"
));
}
}
}
Ok(())
}
#[must_use]
pub fn to_lsp_overrides(config: &CartogConfig) -> HashMap<String, Vec<String>> {
config
.lsp
.as_ref()
.map(|lsp| {
lsp.langs
.iter()
.map(|(lang, cfg)| (lang.clone(), cfg.command.clone()))
.collect()
})
.unwrap_or_default()
}
pub(crate) fn validate_providers(config: &CartogConfig) -> Result<(), String> {
const EMBEDDING_PROVIDERS: &[&str] = &["local", "ollama", "openai"];
const RERANKER_PROVIDERS: &[&str] = &["local", "none"];
if let Some(p) = config
.embedding
.as_ref()
.and_then(|e| e.provider.as_deref())
{
if !EMBEDDING_PROVIDERS.contains(&p) {
return Err(format!(
"unknown embedding provider '{p}'; supported: {}",
EMBEDDING_PROVIDERS.join(", ")
));
}
}
if let Some(p) = config.reranker.as_ref().and_then(|r| r.provider.as_deref()) {
if !RERANKER_PROVIDERS.contains(&p) {
return Err(format!(
"unknown reranker provider '{p}'; supported: {}",
RERANKER_PROVIDERS.join(", ")
));
}
}
Ok(())
}
fn validate_endpoint(endpoint: Option<&str>) -> Result<(), String> {
let ep = match endpoint {
Some(s) if !s.is_empty() => s,
_ => return Ok(()),
};
let after_scheme = ep.split_once("://").map(|x| x.1).unwrap_or(ep);
let authority = after_scheme
.split('/')
.next()
.unwrap_or(after_scheme)
.split('?')
.next()
.unwrap_or(after_scheme)
.split('#')
.next()
.unwrap_or(after_scheme);
if authority.contains('@') {
return Err(format!(
"[remote].endpoint embeds credentials in its URL ({ep:?}) — cartog \
does not accept credentials in config. Move them to the AWS \
environment chain (AWS_ACCESS_KEY_ID / AWS_PROFILE / IMDS) and \
use a plain endpoint URL."
));
}
Ok(())
}
pub const AUTO_INIT_ENV: &str = "CARTOG_AUTO_INIT";
#[must_use]
pub fn allow_index_creation(db_path: &Path, config_present: bool) -> bool {
config_present || db_path.exists() || auto_init_enabled()
}
fn auto_init_enabled() -> bool {
std::env::var(AUTO_INIT_ENV)
.map(|v| !v.is_empty())
.unwrap_or(false)
}
pub fn resolve_db_path(explicit: Option<PathBuf>, config: &CartogConfig) -> PathBuf {
if let Some(p) = explicit {
return expand_tilde(p);
}
if let Some(path_str) = config.database.as_ref().and_then(|d| d.path.as_deref()) {
return expand_tilde(PathBuf::from(path_str));
}
if let Ok(mut dir) = std::env::current_dir() {
loop {
if dir.join(".git").exists() {
return resolve_root_db_path(&dir);
}
if !dir.pop() {
break;
}
}
}
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
resolve_root_db_path(&cwd)
}
fn resolve_root_db_path(root: &Path) -> PathBuf {
let new_path = root.join(cartog_db::DB_DIR).join(cartog_db::DB_FILENAME);
let legacy = root.join(cartog_db::LEGACY_DB_FILE);
if new_path.exists() {
if legacy.exists() {
warn_orphan_legacy_once(&legacy);
}
return new_path;
}
if legacy.exists() {
warn_legacy_db_once(&legacy);
return legacy;
}
new_path
}
fn warn_legacy_db_once(path: &Path) {
use std::sync::atomic::{AtomicBool, Ordering};
static WARNED: AtomicBool = AtomicBool::new(false);
if WARNED.swap(true, Ordering::Relaxed) {
return;
}
if !config_diagnostics_visible() {
return;
}
eprintln!(
"cartog: using legacy database at {}; run `cartog self migrate-db` to move it into .cartog/",
path.display()
);
}
fn warn_orphan_legacy_once(path: &Path) {
use std::sync::atomic::{AtomicBool, Ordering};
static WARNED: AtomicBool = AtomicBool::new(false);
if WARNED.swap(true, Ordering::Relaxed) {
return;
}
if !config_diagnostics_visible() {
return;
}
eprintln!(
"cartog: found legacy database at {} alongside the new layout; the legacy file is ignored",
path.display()
);
}
pub fn expand_tilde(p: PathBuf) -> PathBuf {
let s = p.to_string_lossy();
if let Some(rest) = s.strip_prefix("~/") {
if let Ok(home) = std::env::var("HOME").or_else(|_| std::env::var("USERPROFILE")) {
return PathBuf::from(home).join(rest);
}
}
p
}