chronicle/cli/mod.rs
1pub mod annotate;
2pub mod backfill;
3pub mod context;
4pub mod contracts;
5pub mod correct;
6pub mod decisions;
7pub mod deps;
8pub mod doctor;
9pub mod export;
10pub mod flag;
11pub mod history;
12pub mod import;
13pub mod init;
14pub mod knowledge;
15pub mod lookup;
16pub mod note;
17pub mod read;
18pub mod reconfigure;
19pub mod schema;
20pub mod setup;
21pub mod show;
22pub mod status;
23pub mod summary;
24pub mod sync;
25pub(crate) mod util;
26
27use clap::{Parser, Subcommand};
28
29#[derive(Parser)]
30#[command(
31 name = "git-chronicle",
32 version,
33 about = "AI-powered commit annotation"
34)]
35pub struct Cli {
36 #[command(subcommand)]
37 pub command: Commands,
38}
39
40#[derive(Subcommand)]
41pub enum Commands {
42 /// One-time machine-wide setup (provider, skills, hooks, CLAUDE.md)
43 Setup {
44 /// Overwrite existing files without prompting
45 #[arg(long)]
46 force: bool,
47
48 /// Print what would be done without writing
49 #[arg(long)]
50 dry_run: bool,
51
52 /// Skip installing Claude Code skills
53 #[arg(long)]
54 skip_skills: bool,
55
56 /// Skip installing Claude Code hooks
57 #[arg(long)]
58 skip_hooks: bool,
59
60 /// Skip modifying ~/.claude/CLAUDE.md
61 #[arg(long)]
62 skip_claude_md: bool,
63 },
64
65 /// Rerun the LLM provider selection prompt
66 Reconfigure,
67
68 /// Annotate historical commits that lack Chronicle annotations
69 Backfill {
70 /// Maximum number of commits to annotate
71 #[arg(long, default_value = "20")]
72 limit: usize,
73
74 /// List commits that would be annotated without calling the LLM
75 #[arg(long)]
76 dry_run: bool,
77 },
78
79 /// Initialize chronicle in the current repository
80 Init {
81 /// Disable notes sync (sync is enabled by default)
82 #[arg(long)]
83 no_sync: bool,
84
85 /// Skip hook installation
86 #[arg(long)]
87 no_hooks: bool,
88
89 /// LLM provider to use
90 #[arg(long)]
91 provider: Option<String>,
92
93 /// LLM model to use
94 #[arg(long)]
95 model: Option<String>,
96
97 /// Run backfill after init (annotate last 20 commits)
98 #[arg(long)]
99 backfill: bool,
100 },
101
102 /// Manage annotation context
103 Context {
104 #[command(subcommand)]
105 action: ContextAction,
106 },
107
108 /// Read annotations for a file
109 Read {
110 /// File path to read annotations for
111 path: String,
112
113 /// Filter by AST anchor name
114 #[arg(long)]
115 anchor: Option<String>,
116
117 /// Filter by line range (format: start:end)
118 #[arg(long)]
119 lines: Option<String>,
120 },
121
122 /// Annotate a specific commit
123 Annotate {
124 /// Commit to annotate (default: HEAD)
125 #[arg(long, default_value = "HEAD")]
126 commit: String,
127
128 /// Read AnnotateInput JSON from stdin (live annotation path, zero LLM cost)
129 #[arg(long)]
130 live: bool,
131
132 /// Comma-separated source commit SHAs for squash synthesis (CI usage)
133 #[arg(long)]
134 squash_sources: Option<String>,
135
136 /// Old commit SHA to migrate annotation from (amend re-annotation)
137 #[arg(long)]
138 amend_source: Option<String>,
139
140 /// Quick annotation: provide summary directly on command line
141 #[arg(long, conflicts_with_all = ["live", "json_input", "squash_sources", "amend_source"])]
142 summary: Option<String>,
143
144 /// Provide full annotation JSON on command line
145 #[arg(long = "json", conflicts_with_all = ["live", "summary", "squash_sources", "amend_source"])]
146 json_input: Option<String>,
147
148 /// Auto-annotate using the commit message as summary
149 #[arg(long, conflicts_with_all = ["live", "summary", "json_input", "squash_sources", "amend_source"])]
150 auto: bool,
151 },
152
153 /// Flag a region annotation as potentially inaccurate
154 Flag {
155 /// File path relative to repository root
156 path: String,
157
158 /// Optional AST anchor name to scope the flag to a specific region
159 anchor: Option<String>,
160
161 /// Reason for flagging this annotation
162 #[arg(long)]
163 reason: String,
164 },
165
166 /// Apply a precise correction to a specific annotation field
167 Correct {
168 /// Commit SHA of the annotation to correct
169 sha: String,
170
171 /// AST anchor name of the region within the annotation
172 #[arg(long)]
173 region: String,
174
175 /// Annotation field to correct (intent, reasoning, constraints, risk_notes, semantic_dependencies, tags)
176 #[arg(long)]
177 field: String,
178
179 /// Specific value to remove or mark as incorrect
180 #[arg(long)]
181 remove: Option<String>,
182
183 /// Replacement or amendment text
184 #[arg(long)]
185 amend: Option<String>,
186 },
187
188 /// Manage notes sync with remotes
189 Sync {
190 #[command(subcommand)]
191 action: SyncAction,
192 },
193
194 /// Export annotations as JSONL
195 Export {
196 /// Write to file instead of stdout
197 #[arg(short, long)]
198 output: Option<String>,
199 },
200
201 /// Import annotations from a JSONL file
202 Import {
203 /// JSONL file to import
204 file: String,
205
206 /// Overwrite existing annotations
207 #[arg(long)]
208 force: bool,
209
210 /// Show what would be imported without writing
211 #[arg(long)]
212 dry_run: bool,
213 },
214
215 /// Run diagnostic checks on the chronicle setup
216 Doctor {
217 /// Output as JSON
218 #[arg(long)]
219 json: bool,
220
221 /// Include annotation staleness check (scans recent annotations)
222 #[arg(long)]
223 staleness: bool,
224 },
225
226 /// Find code that depends on a given file/anchor (dependency inversion)
227 Deps {
228 /// File path to query
229 path: String,
230
231 /// AST anchor name to query
232 anchor: Option<String>,
233
234 /// Output format (json or pretty)
235 #[arg(long, default_value = "json")]
236 format: String,
237
238 /// Maximum number of results to return
239 #[arg(long, default_value = "50")]
240 max_results: u32,
241
242 /// Maximum number of commits to scan
243 #[arg(long, default_value = "500")]
244 scan_limit: u32,
245
246 /// Omit metadata (schema, query echo, stats) from JSON output
247 #[arg(long)]
248 compact: bool,
249 },
250
251 /// Show annotation timeline for a file/anchor across commits
252 History {
253 /// File path to query
254 path: String,
255
256 /// AST anchor name to query
257 anchor: Option<String>,
258
259 /// Maximum number of timeline entries
260 #[arg(long, default_value = "10")]
261 limit: u32,
262
263 /// Output format (json or pretty)
264 #[arg(long, default_value = "json")]
265 format: String,
266
267 /// Omit metadata (schema, query echo, stats) from JSON output
268 #[arg(long)]
269 compact: bool,
270 },
271
272 /// Interactive TUI explorer for annotated source code
273 Show {
274 /// File path to show
275 path: String,
276
277 /// Focus on a specific AST anchor
278 anchor: Option<String>,
279
280 /// Commit to show file at
281 #[arg(long, default_value = "HEAD")]
282 commit: String,
283
284 /// Force non-interactive plain-text output
285 #[arg(long)]
286 no_tui: bool,
287 },
288
289 /// Print JSON Schema for annotation types (self-documenting for AI agents)
290 Schema {
291 /// Schema name: annotate-input, annotation
292 name: String,
293 },
294
295 /// Query contracts and dependencies for a file/anchor ("What must I not break?")
296 Contracts {
297 /// File path to query
298 path: String,
299
300 /// AST anchor name to query
301 #[arg(long)]
302 anchor: Option<String>,
303
304 /// Output format (json or pretty)
305 #[arg(long, default_value = "json")]
306 format: String,
307
308 /// Omit metadata (schema, query echo, stats) from JSON output
309 #[arg(long)]
310 compact: bool,
311 },
312
313 /// Query design decisions and rejected alternatives ("What was decided?")
314 Decisions {
315 /// File path to scope decisions to (omit for all)
316 path: Option<String>,
317
318 /// Output format (json or pretty)
319 #[arg(long, default_value = "json")]
320 format: String,
321
322 /// Omit metadata (schema, query echo) from JSON output
323 #[arg(long)]
324 compact: bool,
325 },
326
327 /// Show condensed annotation summary for a file
328 Summary {
329 /// File path to query
330 path: String,
331
332 /// Filter to a specific AST anchor
333 #[arg(long)]
334 anchor: Option<String>,
335
336 /// Output format (json or pretty)
337 #[arg(long, default_value = "json")]
338 format: String,
339
340 /// Omit metadata (schema, query echo, stats) from JSON output
341 #[arg(long)]
342 compact: bool,
343 },
344
345 /// One-stop context lookup for a file (contracts + decisions + history)
346 Lookup {
347 /// File path to query
348 path: String,
349
350 /// AST anchor name
351 #[arg(long)]
352 anchor: Option<String>,
353
354 /// Output format (json or pretty)
355 #[arg(long, default_value = "json")]
356 format: String,
357
358 /// Compact output (payload only)
359 #[arg(long)]
360 compact: bool,
361 },
362
363 /// Show annotation status and coverage for the repository
364 Status {
365 /// Output format
366 #[arg(long, default_value = "json")]
367 format: String,
368 },
369
370 /// Stage a note for the next annotation (captured context during work)
371 Note {
372 /// The note text to stage (omit to list or clear)
373 text: Option<String>,
374
375 /// List current staged notes
376 #[arg(long)]
377 list: bool,
378
379 /// Clear all staged notes
380 #[arg(long)]
381 clear: bool,
382 },
383
384 /// Manage repo-level knowledge (conventions, boundaries, anti-patterns)
385 Knowledge {
386 #[command(subcommand)]
387 action: KnowledgeAction,
388 },
389}
390
391#[derive(Subcommand)]
392#[allow(clippy::large_enum_variant)]
393pub enum KnowledgeAction {
394 /// List all knowledge entries
395 List {
396 /// Output as JSON
397 #[arg(long)]
398 json: bool,
399 },
400
401 /// Add a new knowledge entry
402 Add {
403 /// Type of entry: convention, boundary, anti-pattern
404 #[arg(long = "type")]
405 entry_type: String,
406
407 /// Stable ID (auto-generated if omitted)
408 #[arg(long)]
409 id: Option<String>,
410
411 /// File/directory scope (for conventions)
412 #[arg(long)]
413 scope: Option<String>,
414
415 /// The rule text (for conventions)
416 #[arg(long)]
417 rule: Option<String>,
418
419 /// Module directory (for boundaries)
420 #[arg(long)]
421 module: Option<String>,
422
423 /// What the module owns (for boundaries)
424 #[arg(long)]
425 owns: Option<String>,
426
427 /// What must not cross the boundary (for boundaries)
428 #[arg(long)]
429 boundary: Option<String>,
430
431 /// The anti-pattern to avoid
432 #[arg(long)]
433 pattern: Option<String>,
434
435 /// What to do instead (for anti-patterns)
436 #[arg(long)]
437 instead: Option<String>,
438
439 /// Stability level: permanent, provisional, experimental
440 #[arg(long)]
441 stability: Option<String>,
442
443 /// Commit SHA where this was decided
444 #[arg(long)]
445 decided_in: Option<String>,
446
447 /// Where this was learned (for anti-patterns)
448 #[arg(long)]
449 learned_from: Option<String>,
450 },
451
452 /// Remove a knowledge entry by ID
453 Remove {
454 /// ID of the entry to remove
455 id: String,
456 },
457}
458
459#[derive(Subcommand)]
460pub enum SyncAction {
461 /// Enable notes sync for a remote
462 Enable {
463 /// Remote name (default: origin)
464 #[arg(long, default_value = "origin")]
465 remote: String,
466 },
467
468 /// Show sync status
469 Status {
470 /// Remote name (default: origin)
471 #[arg(long, default_value = "origin")]
472 remote: String,
473 },
474
475 /// Fetch and merge remote notes
476 Pull {
477 /// Remote name (default: origin)
478 #[arg(long, default_value = "origin")]
479 remote: String,
480 },
481}
482
483#[derive(Subcommand)]
484pub enum ContextAction {
485 /// Set pending context for the next commit
486 Set {
487 #[arg(long)]
488 task: Option<String>,
489
490 #[arg(long)]
491 reasoning: Option<String>,
492
493 #[arg(long)]
494 dependencies: Option<String>,
495
496 #[arg(long)]
497 tags: Vec<String>,
498 },
499
500 /// Show current pending context
501 Show,
502
503 /// Clear pending context
504 Clear,
505}