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 (auto-selects an open port if not specified)
355 #[arg(long)]
356 port: Option<u16>,
357
358 /// Don't open browser automatically
359 #[arg(long)]
360 no_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}