#[cfg(all(feature = "git-overlay", feature = "ingest"))]
use super::DryRunArgs;
use super::{AuthoredMessageArgs, CodeScopeArgs, HistoricalRevisionArgs};
#[derive(Clone, Debug, clap::Subcommand)]
pub enum ContextCommands {
Set(ContextSetArgs),
Get(ContextGetArgs),
List(ContextListArgs),
History(ContextHistoryArgs),
Edit(ContextEditArgs),
Supersede(ContextSupersedeArgs),
Rm(ContextRmArgs),
Check(ContextCheckArgs),
Suggest(ContextSuggestArgs),
Audit(ContextAuditArgs),
#[cfg(all(feature = "git-overlay", feature = "ingest"))]
Reason {
#[command(subcommand)]
command: ContextReasonCommands,
},
}
#[cfg(all(feature = "git-overlay", feature = "ingest"))]
#[derive(Clone, Debug, clap::Subcommand)]
pub enum ContextReasonCommands {
Git(ContextReasonGitArgs),
}
#[cfg(all(feature = "git-overlay", feature = "ingest"))]
#[derive(Clone, Debug, clap::Args)]
pub struct ContextReasonGitArgs {
#[arg(long)]
pub path: std::path::PathBuf,
#[arg(long, default_value_t = 5)]
pub max_sessions_per_commit: usize,
#[arg(long, default_value_t = 0.20)]
pub min_match_confidence: f32,
#[arg(long)]
pub limit: Option<usize>,
#[arg(long = "claude-home")]
pub claude_home: Option<String>,
#[arg(long = "codex-home")]
pub codex_home: Option<String>,
#[arg(long = "opencode-home")]
pub opencode_home: Option<String>,
#[command(flatten)]
pub dry_run: DryRunArgs,
}
#[derive(Clone, Debug, clap::Args)]
pub struct ContextSetArgs {
#[arg(value_name = "PATH", conflicts_with = "path")]
pub path_positional: Option<String>,
#[command(flatten)]
pub scope: CodeScopeArgs,
#[command(flatten)]
pub revision: HistoricalRevisionArgs,
#[arg(
long,
default_value = "rationale",
value_parser = ["constraint", "invariant", "rationale"]
)]
pub kind: String,
#[arg(long)]
pub tag: Vec<String>,
#[command(flatten)]
pub message: AuthoredMessageArgs,
}
impl ContextSetArgs {
pub fn resolved_path(&self) -> Option<&str> {
self.path_positional
.as_deref()
.or(self.scope.path.as_deref())
}
}
#[derive(Clone, Debug, clap::Args)]
pub struct ContextGetArgs {
#[command(flatten)]
pub scope: CodeScopeArgs,
#[command(flatten)]
pub revision: HistoricalRevisionArgs,
#[arg(long)]
pub tag: Option<String>,
}
#[derive(Clone, Debug, clap::Args)]
pub struct ContextListArgs {
#[arg(long)]
pub prefix: Option<String>,
#[arg(long)]
pub tag: Option<String>,
#[command(flatten)]
pub revision: HistoricalRevisionArgs,
#[arg(long)]
pub include_superseded: bool,
}
#[derive(Clone, Debug, clap::Args)]
pub struct ContextHistoryArgs {
#[arg(required_unless_present_any = ["path", "state"])]
pub annotation_id: Option<String>,
#[command(flatten)]
pub scope: CodeScopeArgs,
#[command(flatten)]
pub revision: HistoricalRevisionArgs,
}
#[derive(Clone, Debug, clap::Args)]
pub struct ContextEditArgs {
#[arg(required_unless_present_any = ["path", "state"])]
pub annotation_id: Option<String>,
#[command(flatten)]
pub scope: CodeScopeArgs,
#[command(flatten)]
pub revision: HistoricalRevisionArgs,
#[arg(long, value_parser = ["constraint", "invariant", "rationale"])]
pub kind: Option<String>,
#[arg(long)]
pub tag: Vec<String>,
#[command(flatten)]
pub message: AuthoredMessageArgs,
}
#[derive(Clone, Debug, clap::Args)]
pub struct ContextSupersedeArgs {
pub annotation_id: String,
#[command(flatten)]
pub scope: CodeScopeArgs,
#[command(flatten)]
pub revision: HistoricalRevisionArgs,
#[arg(
long,
default_value = "rationale",
value_parser = ["constraint", "invariant", "rationale"]
)]
pub kind: String,
#[arg(long)]
pub tag: Vec<String>,
#[command(flatten)]
pub message: AuthoredMessageArgs,
}
#[derive(Clone, Debug, clap::Args)]
pub struct ContextRmArgs {
#[command(flatten)]
pub scope: CodeScopeArgs,
#[command(flatten)]
pub revision: HistoricalRevisionArgs,
#[arg(long)]
pub all: bool,
}
#[derive(Clone, Debug, clap::Args)]
pub struct ContextCheckArgs {
#[command(flatten)]
pub scope: CodeScopeArgs,
#[command(flatten)]
pub revision: HistoricalRevisionArgs,
#[arg(long)]
pub tag: Option<String>,
}
#[derive(Clone, Debug, clap::Args)]
pub struct ContextSuggestArgs {
#[command(flatten)]
pub revision: HistoricalRevisionArgs,
#[arg(short = 'n', long, default_value = "10")]
pub limit: usize,
}
#[derive(Clone, Debug, clap::Args)]
pub struct ContextAuditArgs {
#[command(flatten)]
pub revision: HistoricalRevisionArgs,
}
#[cfg(test)]
mod tests {
use clap::Parser;
use crate::cli::{Cli, Commands, ContextCommands};
#[test]
fn history_and_edit_accept_path_or_id() {
match Cli::try_parse_from(["heddle", "context", "history", "ann-1"])
.expect("history id")
.command
{
Commands::Context {
for_thread: None,
command: Some(ContextCommands::History(args)),
} => {
assert_eq!(args.annotation_id.as_deref(), Some("ann-1"));
assert!(args.scope.path.is_none());
}
_ => panic!("expected context history"),
}
match Cli::try_parse_from(["heddle", "context", "history", "--path", "src/auth.rs"])
.expect("history path")
.command
{
Commands::Context {
for_thread: None,
command: Some(ContextCommands::History(args)),
} => {
assert!(args.annotation_id.is_none());
assert_eq!(args.scope.path.as_deref(), Some("src/auth.rs"));
}
_ => panic!("expected context history"),
}
match Cli::try_parse_from([
"heddle",
"context",
"edit",
"--path",
"src/auth.rs",
"-m",
"revised",
])
.expect("edit path")
.command
{
Commands::Context {
for_thread: None,
command: Some(ContextCommands::Edit(args)),
} => {
assert!(args.annotation_id.is_none());
assert_eq!(args.scope.path.as_deref(), Some("src/auth.rs"));
assert_eq!(args.message.body.as_deref(), Some("revised"));
}
_ => panic!("expected context edit"),
}
}
#[test]
fn context_set_accepts_positional_path_body_and_file() {
match Cli::try_parse_from([
"heddle",
"context",
"set",
"src/auth.rs",
"--body",
"keep timing constant",
])
.expect("positional path + body")
.command
{
Commands::Context {
for_thread: None,
command: Some(ContextCommands::Set(args)),
} => {
assert_eq!(args.resolved_path(), Some("src/auth.rs"));
assert_eq!(args.message.body.as_deref(), Some("keep timing constant"));
}
_ => panic!("expected context set"),
}
match Cli::try_parse_from([
"heddle",
"context",
"set",
"--path",
"src/auth.rs",
"--file",
"note.md",
])
.expect("file body")
.command
{
Commands::Context {
for_thread: None,
command: Some(ContextCommands::Set(args)),
} => {
assert_eq!(args.resolved_path(), Some("src/auth.rs"));
assert_eq!(
args.message.file.as_deref(),
Some(std::path::Path::new("note.md"))
);
}
_ => panic!("expected context set"),
}
assert!(
Cli::try_parse_from([
"heddle",
"context",
"set",
"--path",
"src/auth.rs",
"--from-file",
"note.md",
])
.is_err(),
"--from-file is not an alias of --file"
);
}
#[test]
fn context_set_accepts_path_symbol_and_line_together() {
match Cli::try_parse_from([
"heddle",
"context",
"set",
"--path",
"src/auth.rs",
"--symbol",
"verify",
"-m",
"keep timing constant",
])
.expect("set --path --symbol")
.command
{
Commands::Context {
for_thread: None,
command: Some(ContextCommands::Set(args)),
} => {
assert_eq!(args.resolved_path(), Some("src/auth.rs"));
assert_eq!(args.scope.symbol.as_deref(), Some("verify"));
assert!(args.scope.line.is_none());
}
_ => panic!("expected context set"),
}
match Cli::try_parse_from([
"heddle",
"context",
"set",
"--path",
"src/auth.rs",
"--line",
"12",
"-m",
"guard this row",
])
.expect("set --path --line")
.command
{
Commands::Context {
for_thread: None,
command: Some(ContextCommands::Set(args)),
} => {
assert_eq!(args.scope.line, Some(12));
assert!(args.scope.symbol.is_none());
}
_ => panic!("expected context set"),
}
match Cli::try_parse_from([
"heddle",
"context",
"set",
"--path",
"src/auth.rs",
"--symbol",
"verify",
"--line",
"12",
"-m",
"both",
])
.expect("symbol and line together")
.command
{
Commands::Context {
for_thread: None,
command: Some(ContextCommands::Set(args)),
} => {
assert_eq!(args.scope.symbol.as_deref(), Some("verify"));
assert_eq!(args.scope.line, Some(12));
}
_ => panic!("expected context set"),
}
assert!(
Cli::try_parse_from([
"heddle",
"context",
"set",
"--path",
"src/auth.rs",
"--scope",
"symbol:verify",
"-m",
"legacy",
])
.is_err(),
"hidden --scope alias is removed"
);
}
}