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