Skip to main content

chronicle/cli/
init.rs

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