Skip to main content

chronicle/cli/
init.rs

1use std::path::PathBuf;
2
3use crate::error::chronicle_error::{GitSnafu, IoSnafu};
4use crate::error::Result;
5use crate::git::CliOps;
6use crate::git::GitOps;
7use crate::hooks::install_hooks;
8use crate::sync::enable_sync;
9use snafu::ResultExt;
10
11use super::util::find_git_dir;
12
13pub fn run(
14    no_sync: bool,
15    no_hooks: bool,
16    provider: Option<String>,
17    model: Option<String>,
18    backfill: bool,
19) -> Result<()> {
20    // Find the git directory
21    let git_dir = find_git_dir()?;
22
23    // Create .git/chronicle/ directory
24    let chronicle_dir = git_dir.join("chronicle");
25    std::fs::create_dir_all(&chronicle_dir).context(IoSnafu)?;
26
27    // Set up git config
28    let repo_dir = git_dir.parent().unwrap_or(&git_dir).to_path_buf();
29    let ops = CliOps::new(repo_dir.clone());
30
31    ops.config_set("chronicle.enabled", "true")
32        .context(GitSnafu)?;
33
34    if let Some(ref p) = provider {
35        ops.config_set("chronicle.provider", p).context(GitSnafu)?;
36    }
37
38    if let Some(ref m) = model {
39        ops.config_set("chronicle.model", m).context(GitSnafu)?;
40    }
41
42    // Install hooks unless --no-hooks
43    if !no_hooks {
44        install_hooks(&git_dir)?;
45        eprintln!("installed post-commit hook");
46    }
47
48    // Enable notes sync by default (push/fetch refspecs on origin)
49    if !no_sync {
50        ops.config_set("chronicle.sync", "true").context(GitSnafu)?;
51        let remote = "origin";
52        match enable_sync(&repo_dir, remote) {
53            Ok(()) => eprintln!("notes sync enabled for remote '{remote}'"),
54            Err(e) => eprintln!("warning: could not enable notes sync: {e}"),
55        }
56    }
57
58    eprintln!("chronicle initialized in {}", chronicle_dir.display());
59
60    // --- Enhanced post-init checks ---
61
62    // Count unannotated commits
63    let unannotated = count_unannotated(&ops);
64    if unannotated > 0 {
65        eprintln!();
66        eprintln!(
67            "Found {} unannotated commits (of last 100). Run `git chronicle backfill --limit 20` to annotate recent history.",
68            unannotated
69        );
70    }
71
72    // Check if global skills are installed
73    if let Ok(home) = std::env::var("HOME") {
74        let skills_dir = PathBuf::from(&home)
75            .join(".claude")
76            .join("skills")
77            .join("chronicle");
78        if !skills_dir.exists() {
79            eprintln!();
80            eprintln!("TIP: Run `git chronicle setup` to install Claude Code skills globally.");
81        }
82    }
83
84    // Run backfill if requested
85    if backfill {
86        eprintln!();
87        eprintln!("Running backfill (limit 20)...");
88        if let Err(e) = crate::cli::backfill::run(20, false) {
89            eprintln!("warning: backfill failed: {e}");
90        }
91    }
92
93    Ok(())
94}
95
96/// Count unannotated commits in the last 100.
97fn count_unannotated(ops: &dyn GitOps) -> usize {
98    let output = match std::process::Command::new("git")
99        .args(["log", "--format=%H", "-100"])
100        .output()
101    {
102        Ok(o) if o.status.success() => o,
103        _ => return 0,
104    };
105
106    let shas: Vec<String> = String::from_utf8_lossy(&output.stdout)
107        .lines()
108        .map(|s| s.to_string())
109        .filter(|s| !s.is_empty())
110        .collect();
111
112    let mut unannotated = 0;
113    for sha in &shas {
114        if let Ok(false) = ops.note_exists(sha) {
115            unannotated += 1;
116        }
117    }
118    unannotated
119}