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)
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}