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 command: ContextCommands::History(args),
285 } => {
286 assert_eq!(args.annotation_id.as_deref(), Some("ann-1"));
287 assert!(args.scope.path.is_none());
288 }
289 _ => panic!("expected context history"),
290 }
291 match Cli::try_parse_from(["heddle", "context", "history", "--path", "src/auth.rs"])
292 .expect("history path")
293 .command
294 {
295 Commands::Context {
296 command: ContextCommands::History(args),
297 } => {
298 assert!(args.annotation_id.is_none());
299 assert_eq!(args.scope.path.as_deref(), Some("src/auth.rs"));
300 }
301 _ => panic!("expected context history"),
302 }
303 match Cli::try_parse_from([
304 "heddle",
305 "context",
306 "edit",
307 "--path",
308 "src/auth.rs",
309 "-m",
310 "revised",
311 ])
312 .expect("edit path")
313 .command
314 {
315 Commands::Context {
316 command: ContextCommands::Edit(args),
317 } => {
318 assert!(args.annotation_id.is_none());
319 assert_eq!(args.scope.path.as_deref(), Some("src/auth.rs"));
320 assert_eq!(args.message.body.as_deref(), Some("revised"));
321 }
322 _ => panic!("expected context edit"),
323 }
324 }
325
326 #[test]
327 fn context_set_accepts_positional_path_body_and_file() {
328 match Cli::try_parse_from([
329 "heddle",
330 "context",
331 "set",
332 "src/auth.rs",
333 "--body",
334 "keep timing constant",
335 ])
336 .expect("positional path + body")
337 .command
338 {
339 Commands::Context {
340 command: ContextCommands::Set(args),
341 } => {
342 assert_eq!(args.resolved_path(), Some("src/auth.rs"));
343 assert_eq!(args.message.body.as_deref(), Some("keep timing constant"));
344 }
345 _ => panic!("expected context set"),
346 }
347 match Cli::try_parse_from([
348 "heddle",
349 "context",
350 "set",
351 "--path",
352 "src/auth.rs",
353 "--file",
354 "note.md",
355 ])
356 .expect("file body")
357 .command
358 {
359 Commands::Context {
360 command: ContextCommands::Set(args),
361 } => {
362 assert_eq!(args.resolved_path(), Some("src/auth.rs"));
363 assert_eq!(
364 args.message.file.as_deref(),
365 Some(std::path::Path::new("note.md"))
366 );
367 }
368 _ => panic!("expected context set"),
369 }
370 assert!(
371 Cli::try_parse_from([
372 "heddle",
373 "context",
374 "set",
375 "--path",
376 "src/auth.rs",
377 "--from-file",
378 "note.md",
379 ])
380 .is_err(),
381 "--from-file is not an alias of --file"
382 );
383 }
384
385 #[test]
386 fn context_set_accepts_path_symbol_and_line_together() {
387 match Cli::try_parse_from([
388 "heddle",
389 "context",
390 "set",
391 "--path",
392 "src/auth.rs",
393 "--symbol",
394 "verify",
395 "-m",
396 "keep timing constant",
397 ])
398 .expect("set --path --symbol")
399 .command
400 {
401 Commands::Context {
402 command: ContextCommands::Set(args),
403 } => {
404 assert_eq!(args.resolved_path(), Some("src/auth.rs"));
405 assert_eq!(args.scope.symbol.as_deref(), Some("verify"));
406 assert!(args.scope.line.is_none());
407 }
408 _ => panic!("expected context set"),
409 }
410 match Cli::try_parse_from([
411 "heddle",
412 "context",
413 "set",
414 "--path",
415 "src/auth.rs",
416 "--line",
417 "12",
418 "-m",
419 "guard this row",
420 ])
421 .expect("set --path --line")
422 .command
423 {
424 Commands::Context {
425 command: ContextCommands::Set(args),
426 } => {
427 assert_eq!(args.scope.line, Some(12));
428 assert!(args.scope.symbol.is_none());
429 }
430 _ => panic!("expected context set"),
431 }
432 match Cli::try_parse_from([
433 "heddle",
434 "context",
435 "set",
436 "--path",
437 "src/auth.rs",
438 "--symbol",
439 "verify",
440 "--line",
441 "12",
442 "-m",
443 "both",
444 ])
445 .expect("symbol and line together")
446 .command
447 {
448 Commands::Context {
449 command: ContextCommands::Set(args),
450 } => {
451 assert_eq!(args.scope.symbol.as_deref(), Some("verify"));
452 assert_eq!(args.scope.line, Some(12));
453 }
454 _ => panic!("expected context set"),
455 }
456 assert!(
457 Cli::try_parse_from([
458 "heddle",
459 "context",
460 "set",
461 "--path",
462 "src/auth.rs",
463 "--scope",
464 "symbol:verify",
465 "-m",
466 "legacy",
467 ])
468 .is_err(),
469 "hidden --scope alias is removed"
470 );
471 }
472}