git_claw/cli/args.rs
1//! Clap CLI argument definitions and Git subcommand normalization.
2
3use clap::{Parser, Subcommand};
4use std::ffi::OsString;
5
6/// Deterministic git worktree orchestration engine for developers and AI agents.
7#[derive(Parser, Debug, Clone, PartialEq, Eq)]
8#[command(
9 name = "git-claw",
10 bin_name = "git-claw",
11 version,
12 about = "Deterministic git worktree orchestration engine for developers and AI agents"
13)]
14pub struct Cli {
15 #[command(subcommand)]
16 pub command: Option<Commands>,
17}
18
19#[derive(Subcommand, Debug, Clone, PartialEq, Eq)]
20pub enum Commands {
21 /// Manage feature worktrees
22 Feature {
23 #[command(subcommand)]
24 action: WorktreeAction,
25 },
26 /// Manage bugfix worktrees
27 Bugfix {
28 #[command(subcommand)]
29 action: WorktreeAction,
30 },
31 /// Manage hotfix worktrees
32 Hotfix {
33 #[command(subcommand)]
34 action: WorktreeAction,
35 },
36 /// Manage spike worktrees
37 Spike {
38 #[command(subcommand)]
39 action: SpikeAction,
40 },
41 /// Complete a worktree, run tests, merge into main, and clean up
42 Finish {
43 /// Worktree name (inferred from cwd if omitted)
44 name: Option<String>,
45 /// Force completion even if pre-finish hooks fail
46 #[arg(long)]
47 force: bool,
48 /// Keep remote branch on remote repository instead of deleting it
49 #[arg(long, short = 'k')]
50 keep_remote: bool,
51 },
52 /// Publish a worktree branch to remote with upstream tracking
53 Publish {
54 /// Worktree name (inferred from cwd if omitted)
55 name: Option<String>,
56 /// Remote repository name (defaults to origin)
57 #[arg(long)]
58 remote: Option<String>,
59 },
60 /// List all active worktree slots
61 List,
62 /// Run an arbitrary command inside a worktree with environment injected
63 Run {
64 /// Worktree name (inferred from cwd if omitted)
65 name: Option<String>,
66 /// Shell command to execute
67 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
68 command: Vec<String>,
69 },
70 /// Open the worktree in configured editor ($EDITOR or code/cursor)
71 Open {
72 /// Worktree name (inferred from cwd if omitted)
73 name: Option<String>,
74 },
75 /// Print the worktree path (usable with cd $(git claw cd <name>))
76 Cd {
77 /// Worktree name (inferred from cwd if omitted)
78 name: Option<String>,
79 },
80 /// Create an annotated SemVer release tag on main (auto-infers version and changelog from Conventional Commits if omitted)
81 Tag {
82 /// SemVer version tag (e.g., v1.2.0; if omitted, automatically inferred from Conventional Commits)
83 version: Option<String>,
84 },
85 /// Initialize git-claw configuration for the current repository
86 Init {
87 /// Automatically accept all inferred defaults without prompting
88 #[arg(long, short = 'y')]
89 yes: bool,
90 },
91 /// Output shell integration script for auto-cd navigation
92 #[command(name = "shell-hook")]
93 ShellHook {
94 /// Target shell (bash, zsh)
95 #[arg(default_value = "bash")]
96 shell: String,
97 },
98 /// Generate shell auto-completion script
99 #[command(alias = "completions")]
100 Completion {
101 /// Target shell
102 #[arg(value_enum)]
103 shell: clap_complete::Shell,
104 },
105}
106
107#[derive(Subcommand, Debug, Clone, PartialEq, Eq)]
108pub enum WorktreeAction {
109 /// Create and start a new worktree
110 Start {
111 /// Name of the branch/worktree (kebab-case)
112 name: String,
113 /// Skip shared cache directory symlinking
114 #[arg(long)]
115 isolated: bool,
116 },
117 /// Publish the worktree branch to remote with upstream tracking
118 Publish {
119 /// Worktree name (inferred from cwd if omitted)
120 name: Option<String>,
121 /// Remote repository name (defaults to origin)
122 #[arg(long)]
123 remote: Option<String>,
124 },
125}
126
127#[derive(Subcommand, Debug, Clone, PartialEq, Eq)]
128pub enum SpikeAction {
129 /// Create and start a new spike worktree
130 Start {
131 /// Name of the spike branch/worktree (kebab-case)
132 name: String,
133 /// Skip shared cache directory symlinking
134 #[arg(long)]
135 isolated: bool,
136 },
137 /// Drop a spike worktree without merging
138 Drop {
139 /// Name of the spike branch/worktree
140 name: String,
141 },
142}
143
144/// Normalizes CLI arguments to transparently support both direct `git-claw`
145/// and git subcommand `git claw` (or `git-claw claw`) execution.
146pub fn sanitize_args<I, T>(args: I) -> Vec<T>
147where
148 I: IntoIterator<Item = T>,
149 T: AsRef<std::ffi::OsStr> + Clone,
150{
151 let mut vec: Vec<T> = args.into_iter().collect();
152 if vec.len() > 1 && vec[1].as_ref() == "claw" {
153 vec.remove(1);
154 }
155 vec
156}
157
158impl Cli {
159 /// Parse arguments from process environment after normalizing leading 'claw' subcommand.
160 pub fn parse_from_env() -> Self {
161 let args = sanitize_args(std::env::args_os());
162 Self::parse_from(args)
163 }
164
165 /// Try parsing arguments from an iterator of OsString.
166 #[allow(dead_code)]
167 pub fn try_parse_from_args<I, T>(args: I) -> Result<Self, clap::Error>
168 where
169 I: IntoIterator<Item = T>,
170 T: Into<OsString> + Clone,
171 {
172 let raw: Vec<OsString> = args.into_iter().map(Into::into).collect();
173 let sanitized = sanitize_args(raw);
174 Self::try_parse_from(sanitized)
175 }
176}