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