Skip to main content

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}