1use super::{AuthoredMessageArgs, CodeScopeArgs, DryRunArgs, HistoricalRevisionArgs};
5
6#[derive(Clone, Debug, clap::Subcommand)]
8pub enum ContextCommands {
9 Set(ContextSetArgs),
11
12 Get(ContextGetArgs),
14
15 List(ContextListArgs),
17
18 History(ContextHistoryArgs),
20
21 Edit(ContextEditArgs),
23
24 Supersede(ContextSupersedeArgs),
26
27 Rm(ContextRmArgs),
29
30 Check(ContextCheckArgs),
32
33 Suggest(ContextSuggestArgs),
35
36 Audit(ContextAuditArgs),
38
39 #[cfg(all(feature = "git-overlay", feature = "ingest"))]
41 Reason {
42 #[command(subcommand)]
43 command: ContextReasonCommands,
44 },
45}
46
47#[cfg(all(feature = "git-overlay", feature = "ingest"))]
48#[derive(Clone, Debug, clap::Subcommand)]
49pub enum ContextReasonCommands {
50 Git(ContextReasonGitArgs),
52}
53
54#[cfg(all(feature = "git-overlay", feature = "ingest"))]
55#[derive(Clone, Debug, clap::Args)]
56pub struct ContextReasonGitArgs {
57 #[arg(long)]
59 pub path: std::path::PathBuf,
60
61 #[arg(long, default_value_t = 5)]
63 pub max_sessions_per_commit: usize,
64
65 #[arg(long, default_value_t = 0.20)]
67 pub min_match_confidence: f32,
68
69 #[arg(long)]
71 pub limit: Option<usize>,
72
73 #[arg(long = "claude-home")]
75 pub claude_home: Option<String>,
76
77 #[arg(long = "codex-home")]
79 pub codex_home: Option<String>,
80
81 #[arg(long = "opencode-home")]
83 pub opencode_home: Option<String>,
84
85 #[command(flatten)]
86 pub dry_run: DryRunArgs,
87}
88
89#[derive(Clone, Debug, clap::Args)]
91pub struct ContextSetArgs {
92 #[arg(value_name = "PATH", conflicts_with = "path")]
94 pub path_positional: Option<String>,
95
96 #[command(flatten)]
97 pub scope: CodeScopeArgs,
98
99 #[command(flatten)]
101 pub revision: HistoricalRevisionArgs,
102
103 #[arg(
105 long,
106 default_value = "rationale",
107 value_parser = ["constraint", "invariant", "rationale"]
108 )]
109 pub kind: String,
110
111 #[arg(long)]
113 pub tag: Vec<String>,
114
115 #[command(flatten)]
116 pub message: AuthoredMessageArgs,
117}
118
119impl ContextSetArgs {
120 pub fn resolved_path(&self) -> Option<&str> {
122 self.path_positional
123 .as_deref()
124 .or(self.scope.path.as_deref())
125 }
126}
127
128#[derive(Clone, Debug, clap::Args)]
130pub struct ContextGetArgs {
131 #[command(flatten)]
132 pub scope: CodeScopeArgs,
133
134 #[command(flatten)]
135 pub revision: HistoricalRevisionArgs,
136
137 #[arg(long)]
139 pub tag: Option<String>,
140}
141
142#[derive(Clone, Debug, clap::Args)]
144pub struct ContextListArgs {
145 #[arg(long)]
147 pub prefix: Option<String>,
148
149 #[arg(long)]
151 pub tag: Option<String>,
152
153 #[command(flatten)]
154 pub revision: HistoricalRevisionArgs,
155
156 #[arg(long)]
158 pub include_superseded: bool,
159}
160
161#[derive(Clone, Debug, clap::Args)]
162pub struct ContextHistoryArgs {
163 #[arg(required_unless_present_any = ["path", "state"])]
165 pub annotation_id: Option<String>,
166
167 #[command(flatten)]
168 pub scope: CodeScopeArgs,
169
170 #[command(flatten)]
171 pub revision: HistoricalRevisionArgs,
172}
173
174#[derive(Clone, Debug, clap::Args)]
175pub struct ContextEditArgs {
176 #[arg(required_unless_present_any = ["path", "state"])]
178 pub annotation_id: Option<String>,
179
180 #[command(flatten)]
181 pub scope: CodeScopeArgs,
182
183 #[command(flatten)]
184 pub revision: HistoricalRevisionArgs,
185
186 #[arg(long, value_parser = ["constraint", "invariant", "rationale"])]
188 pub kind: Option<String>,
189
190 #[arg(long)]
192 pub tag: Vec<String>,
193
194 #[command(flatten)]
195 pub message: AuthoredMessageArgs,
196}
197
198#[derive(Clone, Debug, clap::Args)]
199pub struct ContextSupersedeArgs {
200 pub annotation_id: String,
202
203 #[command(flatten)]
204 pub scope: CodeScopeArgs,
205
206 #[command(flatten)]
207 pub revision: HistoricalRevisionArgs,
208
209 #[arg(
211 long,
212 default_value = "rationale",
213 value_parser = ["constraint", "invariant", "rationale"]
214 )]
215 pub kind: String,
216
217 #[arg(long)]
219 pub tag: Vec<String>,
220
221 #[command(flatten)]
222 pub message: AuthoredMessageArgs,
223}
224
225#[derive(Clone, Debug, clap::Args)]
227pub struct ContextRmArgs {
228 #[command(flatten)]
229 pub scope: CodeScopeArgs,
230
231 #[command(flatten)]
232 pub revision: HistoricalRevisionArgs,
233
234 #[arg(long)]
236 pub all: bool,
237}
238
239#[derive(Clone, Debug, clap::Args)]
241pub struct ContextCheckArgs {
242 #[command(flatten)]
243 pub scope: CodeScopeArgs,
244
245 #[command(flatten)]
246 pub revision: HistoricalRevisionArgs,
247
248 #[arg(long)]
250 pub tag: Option<String>,
251}
252
253#[derive(Clone, Debug, clap::Args)]
254pub struct ContextSuggestArgs {
255 #[command(flatten)]
256 pub revision: HistoricalRevisionArgs,
257
258 #[arg(short = 'n', long, default_value = "10")]
260 pub limit: usize,
261}
262
263#[derive(Clone, Debug, clap::Args)]
264pub struct ContextAuditArgs {
265 #[command(flatten)]
266 pub revision: HistoricalRevisionArgs,
267}
268
269#[cfg(test)]
270mod tests {
271 use clap::Parser;
272
273 use crate::cli::{Cli, Commands, ContextCommands};
274
275 #[test]
276 fn history_and_edit_accept_path_or_id() {
277 match Cli::try_parse_from(["heddle", "context", "history", "ann-1"])
278 .expect("history id")
279 .command
280 {
281 Commands::Context {
282 command: ContextCommands::History(args),
283 } => {
284 assert_eq!(args.annotation_id.as_deref(), Some("ann-1"));
285 assert!(args.scope.path.is_none());
286 }
287 _ => panic!("expected context history"),
288 }
289 match Cli::try_parse_from(["heddle", "context", "history", "--path", "src/auth.rs"])
290 .expect("history path")
291 .command
292 {
293 Commands::Context {
294 command: ContextCommands::History(args),
295 } => {
296 assert!(args.annotation_id.is_none());
297 assert_eq!(args.scope.path.as_deref(), Some("src/auth.rs"));
298 }
299 _ => panic!("expected context history"),
300 }
301 match Cli::try_parse_from([
302 "heddle",
303 "context",
304 "edit",
305 "--path",
306 "src/auth.rs",
307 "-m",
308 "revised",
309 ])
310 .expect("edit path")
311 .command
312 {
313 Commands::Context {
314 command: ContextCommands::Edit(args),
315 } => {
316 assert!(args.annotation_id.is_none());
317 assert_eq!(args.scope.path.as_deref(), Some("src/auth.rs"));
318 assert_eq!(args.message.body.as_deref(), Some("revised"));
319 }
320 _ => panic!("expected context edit"),
321 }
322 }
323
324 #[test]
325 fn context_set_accepts_positional_path_body_and_file() {
326 match Cli::try_parse_from([
327 "heddle",
328 "context",
329 "set",
330 "src/auth.rs",
331 "--body",
332 "keep timing constant",
333 ])
334 .expect("positional path + body")
335 .command
336 {
337 Commands::Context {
338 command: ContextCommands::Set(args),
339 } => {
340 assert_eq!(args.resolved_path(), Some("src/auth.rs"));
341 assert_eq!(args.message.body.as_deref(), Some("keep timing constant"));
342 }
343 _ => panic!("expected context set"),
344 }
345 match Cli::try_parse_from([
346 "heddle",
347 "context",
348 "set",
349 "--path",
350 "src/auth.rs",
351 "--file",
352 "note.md",
353 ])
354 .expect("file body")
355 .command
356 {
357 Commands::Context {
358 command: ContextCommands::Set(args),
359 } => {
360 assert_eq!(args.resolved_path(), Some("src/auth.rs"));
361 assert_eq!(
362 args.message.file.as_deref(),
363 Some(std::path::Path::new("note.md"))
364 );
365 }
366 _ => panic!("expected context set"),
367 }
368 assert!(
369 Cli::try_parse_from([
370 "heddle",
371 "context",
372 "set",
373 "--path",
374 "src/auth.rs",
375 "--from-file",
376 "note.md",
377 ])
378 .is_err(),
379 "--from-file is not an alias of --file"
380 );
381 }
382
383 #[test]
384 fn context_set_accepts_path_symbol_and_line_together() {
385 match Cli::try_parse_from([
386 "heddle",
387 "context",
388 "set",
389 "--path",
390 "src/auth.rs",
391 "--symbol",
392 "verify",
393 "-m",
394 "keep timing constant",
395 ])
396 .expect("set --path --symbol")
397 .command
398 {
399 Commands::Context {
400 command: ContextCommands::Set(args),
401 } => {
402 assert_eq!(args.resolved_path(), Some("src/auth.rs"));
403 assert_eq!(args.scope.symbol.as_deref(), Some("verify"));
404 assert!(args.scope.line.is_none());
405 }
406 _ => panic!("expected context set"),
407 }
408 match Cli::try_parse_from([
409 "heddle",
410 "context",
411 "set",
412 "--path",
413 "src/auth.rs",
414 "--line",
415 "12",
416 "-m",
417 "guard this row",
418 ])
419 .expect("set --path --line")
420 .command
421 {
422 Commands::Context {
423 command: ContextCommands::Set(args),
424 } => {
425 assert_eq!(args.scope.line, Some(12));
426 assert!(args.scope.symbol.is_none());
427 }
428 _ => panic!("expected context set"),
429 }
430 match Cli::try_parse_from([
431 "heddle",
432 "context",
433 "set",
434 "--path",
435 "src/auth.rs",
436 "--symbol",
437 "verify",
438 "--line",
439 "12",
440 "-m",
441 "both",
442 ])
443 .expect("symbol and line together")
444 .command
445 {
446 Commands::Context {
447 command: ContextCommands::Set(args),
448 } => {
449 assert_eq!(args.scope.symbol.as_deref(), Some("verify"));
450 assert_eq!(args.scope.line, Some(12));
451 }
452 _ => panic!("expected context set"),
453 }
454 assert!(
455 Cli::try_parse_from([
456 "heddle",
457 "context",
458 "set",
459 "--path",
460 "src/auth.rs",
461 "--scope",
462 "symbol:verify",
463 "-m",
464 "legacy",
465 ])
466 .is_err(),
467 "hidden --scope alias is removed"
468 );
469 }
470}