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"
# [reranker]
# enabled = true
# [rag]
# fts_weight = 0.5
# vector_weight = 0.5
"##;
#[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_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());
}
}