use std::fs;
use std::path::Path;
use anyhow::Result;
use serde::Serialize;
const TOML_TEMPLATE: &str = r##"# .cartog.toml — project-level configuration for cartog
#
# All sections are commented out by default; defaults apply. Uncomment a
# section and set the keys you want to override. Run `cartog config` to
# print the active configuration. See https://github.com/jrollin/cartog
# for the schema reference.
# [database]
# path = ".cartog/db.sqlite"
# [embedding]
# provider = "local" # "local" (default) | "ollama" | "openai"
#
# For an OpenAI-compatible /v1 endpoint (OpenAI, Mistral, Voyage, Jina, OVHcloud,
# or a local server like Ollama /v1, LM Studio, vLLM), set provider = "openai"
# and uncomment the [embedding.openai] block below. The API key is read from an
# env var, never stored here; leave the var unset for keyless local endpoints.
# model = "text-embedding-3-small"
#
# Watcher auto-embed under `serve --watch` / `watch`. Omit for auto-detect (embed
# only if the repo already has embeddings). Precedence: CARTOG_WATCH_RAG > this > --rag.
# auto_embed = true
# [embedding.openai] # only used when provider = "openai"
# base_url = "https://api.openai.com/v1" # or http://localhost:11434/v1 (Ollama), etc.
# api_key_env = "OPENAI_API_KEY" # env var NAME, not the key itself
# [reranker]
# enabled = true
# [rag]
# fts_weight = 0.5
# vector_weight = 0.5
# [security]
# Redact common secret patterns (API keys, tokens, JWTs) from stored symbol
# text. On by default. Sensitive files (.env, *.pem, id_rsa, ...) are always
# excluded regardless of this setting.
# redact_secrets = true
"##;
#[derive(Debug, Serialize)]
struct InitReport {
toml: TomlStep,
dry_run: bool,
}
#[derive(Debug, Serialize)]
struct TomlStep {
path: String,
status: TomlStatus,
message: String,
}
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
enum TomlStatus {
Created,
Unchanged,
Skipped,
}
pub fn cmd_init(dry_run: bool, json: bool) -> Result<()> {
let cwd = std::env::current_dir()?;
let toml = scaffold_toml(&cwd, dry_run)?;
let report = InitReport { toml, dry_run };
if json {
println!("{}", serde_json::to_string_pretty(&report)?);
} else {
print!("{}", render_human(&report));
print_next_steps(&report);
}
if matches!(report.toml.status, TomlStatus::Skipped) {
std::process::exit(1);
}
Ok(())
}
fn render_human(report: &InitReport) -> String {
let icon = match report.toml.status {
TomlStatus::Created | TomlStatus::Unchanged => "+",
TomlStatus::Skipped => "!",
};
format!(
"{} .cartog.toml ({}): {}\n",
icon, report.toml.path, report.toml.message
)
}
fn print_next_steps(report: &InitReport) {
if report.dry_run {
println!("\nDry run only. Re-run without --dry-run to apply.");
return;
}
println!("\nNext steps:");
if matches!(report.toml.status, TomlStatus::Created) {
println!(
" 1. Edit .cartog.toml if you want to change defaults (DB path, embedding provider)."
);
println!(" 2. Run `cartog ide` to wire cartog into your editor(s).");
println!(" 3. Run `cartog index` to build the code graph.");
} else {
println!(" 1. Run `cartog ide` to wire (or re-wire) cartog into your editor(s).");
println!(" 2. Run `cartog index` to build (or refresh) the code graph.");
}
}
fn scaffold_toml(cwd: &Path, dry_run: bool) -> Result<TomlStep> {
let path = cwd.join(".cartog.toml");
let path_str = path.display().to_string();
if path.exists() {
return Ok(TomlStep {
path: path_str,
status: TomlStatus::Unchanged,
message: "already present, left untouched".into(),
});
}
if dry_run {
return Ok(TomlStep {
path: path_str,
status: TomlStatus::Created,
message: "would create from template".into(),
});
}
match fs::write(&path, TOML_TEMPLATE) {
Ok(()) => Ok(TomlStep {
path: path_str,
status: TomlStatus::Created,
message: "created from template".into(),
}),
Err(e) => Ok(TomlStep {
path: path_str,
status: TomlStatus::Skipped,
message: format!("could not create: {e}"),
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn toml_scaffold_writes_template_when_absent() {
let tmp = TempDir::new().unwrap();
let step = scaffold_toml(tmp.path(), false).unwrap();
assert_eq!(step.status, TomlStatus::Created);
let body = fs::read_to_string(tmp.path().join(".cartog.toml")).unwrap();
assert!(body.contains("[database]"));
assert!(body.contains("[embedding]"));
}
#[test]
fn toml_template_has_no_duplicate_table_headers() {
let mut headers = Vec::new();
for line in TOML_TEMPLATE.lines() {
let t = line.trim_start_matches(['#', ' ']);
let t = t.split('#').next().unwrap_or(t).trim();
if t.starts_with('[') && t.ends_with(']') {
headers.push(t);
}
}
let mut seen = std::collections::HashSet::new();
for h in &headers {
assert!(seen.insert(*h), "duplicate table header in template: {h}");
}
}
#[test]
fn toml_scaffold_preserves_existing_file() {
let tmp = TempDir::new().unwrap();
fs::write(tmp.path().join(".cartog.toml"), "# pre-existing\n").unwrap();
let step = scaffold_toml(tmp.path(), false).unwrap();
assert_eq!(step.status, TomlStatus::Unchanged);
let body = fs::read_to_string(tmp.path().join(".cartog.toml")).unwrap();
assert_eq!(body, "# pre-existing\n");
}
#[test]
fn toml_scaffold_dry_run_does_not_write() {
let tmp = TempDir::new().unwrap();
let step = scaffold_toml(tmp.path(), true).unwrap();
assert_eq!(step.status, TomlStatus::Created);
assert!(!tmp.path().join(".cartog.toml").exists());
}
}