Skip to main content

chronicle/cli/
mod.rs

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