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 let git_dir = find_git_dir()?;
20
21 let chronicle_dir = git_dir.join("chronicle");
23 std::fs::create_dir_all(&chronicle_dir).context(IoSnafu)?;
24
25 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 if !no_hooks {
42 install_hooks(&git_dir)?;
43 eprintln!("installed post-commit hook");
44 }
45
46 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 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 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 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
94fn 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
119fn 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 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}