Skip to main content

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, fish)
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        /// Automatically navigate (cd) to the worktree directory after creation
117        #[arg(long)]
118        cd: bool,
119    },
120    /// Publish the worktree branch to remote with upstream tracking
121    Publish {
122        /// Worktree name (inferred from cwd if omitted)
123        name: Option<String>,
124        /// Remote repository name (defaults to origin)
125        #[arg(long)]
126        remote: Option<String>,
127    },
128}
129
130#[derive(Subcommand, Debug, Clone, PartialEq, Eq)]
131pub enum SpikeAction {
132    /// Create and start a new spike worktree
133    Start {
134        /// Name of the spike branch/worktree (kebab-case)
135        name: String,
136        /// Skip shared cache directory symlinking
137        #[arg(long)]
138        isolated: bool,
139        /// Automatically navigate (cd) to the worktree directory after creation
140        #[arg(long)]
141        cd: bool,
142    },
143    /// Drop a spike worktree without merging
144    Drop {
145        /// Name of the spike branch/worktree
146        name: String,
147    },
148}
149
150/// Normalizes CLI arguments to transparently support both direct `git-claw`
151/// and git subcommand `git claw` (or `git-claw claw`) execution.
152pub fn sanitize_args<I, T>(args: I) -> Vec<T>
153where
154    I: IntoIterator<Item = T>,
155    T: AsRef<std::ffi::OsStr> + Clone,
156{
157    let mut vec: Vec<T> = args.into_iter().collect();
158    if vec.len() > 1 && vec[1].as_ref() == "claw" {
159        vec.remove(1);
160    }
161    vec
162}
163
164impl Cli {
165    /// Parse arguments from process environment after normalizing leading 'claw' subcommand.
166    pub fn parse_from_env() -> Self {
167        let args = sanitize_args(std::env::args_os());
168        Self::parse_from(args)
169    }
170
171    /// Try parsing arguments from an iterator of OsString.
172    #[allow(dead_code)]
173    pub fn try_parse_from_args<I, T>(args: I) -> Result<Self, clap::Error>
174    where
175        I: IntoIterator<Item = T>,
176        T: Into<OsString> + Clone,
177    {
178        let raw: Vec<OsString> = args.into_iter().map(Into::into).collect();
179        let sanitized = sanitize_args(raw);
180        Self::try_parse_from(sanitized)
181    }
182}