git_worktree_manager/cli.rs
1/// CLI definitions using clap derive.
2///
3/// Mirrors the Typer-based CLI in src/git_worktree_manager/cli.py.
4pub mod completions;
5
6use clap::{Parser, Subcommand, ValueEnum, ValueHint};
7use std::path::PathBuf;
8
9use crate::operations::config_ops::ConfigKey;
10
11/// Output format for `gw new --emit`.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
13pub enum EmitFormat {
14 /// Human-readable styled output (default).
15 #[default]
16 Text,
17 /// Single-line JSON object with worktree metadata; suppresses AI-tool launch.
18 Json,
19}
20
21/// Git worktree manager CLI.
22#[derive(Parser, Debug)]
23#[command(
24 name = "gw",
25 version,
26 about = "git worktree manager — AI coding assistant integration",
27 long_about = None,
28 arg_required_else_help = true,
29)]
30pub struct Cli {
31 /// Generate shell completions for the given shell
32 #[arg(long, value_name = "SHELL", value_parser = clap::builder::PossibleValuesParser::new(["bash", "zsh", "fish", "powershell", "elvish"]))]
33 pub generate_completion: Option<String>,
34
35 #[command(subcommand)]
36 pub command: Option<Commands>,
37}
38
39#[derive(Subcommand, Debug)]
40pub enum Commands {
41 /// Create new worktree for feature branch.
42 ///
43 /// Trailing positional args are forwarded verbatim to the underlying AI
44 /// tool (claude/codex/gemini). Use `--` to disambiguate when forwarded
45 /// args start with a hyphen, e.g. `gw new feat-x -- --model opus`.
46 #[command(group(
47 clap::ArgGroup::new("prompt_source")
48 .args(["prompt", "prompt_file"])
49 .multiple(false)
50 .required(false)
51 ))]
52 New {
53 /// Branch name for the new worktree
54 name: String,
55
56 /// Custom worktree path (default: ../<repo>-<branch>)
57 #[arg(long, value_hint = ValueHint::DirPath)]
58 path: Option<String>,
59
60 /// Base branch to create from (default: from config)
61 #[arg(long = "base")]
62 base: Option<String>,
63
64 /// Terminal launch method for THIS invocation. Overrides config
65 /// and CW_LAUNCH_METHOD. Accepts canonical names (e.g.,
66 /// `wezterm-tab`) or aliases (e.g., `w-t`, `w-t-b`). Supports
67 /// `method:session-name` for tmux/zellij. Use `noop`, `none`, or
68 /// `skip` to skip the AI tool launch entirely.
69 #[arg(short = 'T', long = "term", value_name = "METHOD")]
70 term: Option<String>,
71
72 /// Initial prompt to pass to the AI tool. Use `-` to read the
73 /// prompt from standard input (e.g., `--prompt -` with a heredoc
74 /// or pipe).
75 #[arg(long)]
76 prompt: Option<String>,
77
78 /// Read the initial prompt from a file (recommended for multi-line prompts)
79 #[arg(long = "prompt-file", value_hint = ValueHint::FilePath)]
80 prompt_file: Option<PathBuf>,
81
82 /// Disable auto-forwarding of `<TOOL>_*` environment variables
83 /// (e.g. `CLAUDE_*` when ai-tool is claude). Without this flag, gw
84 /// snapshots the parent shell's matching env at invocation time
85 /// and re-injects it inside the spawned terminal so launchers like
86 /// wezterm/iterm/tmux/zellij behave like a normal child process.
87 #[arg(long = "no-env-forward")]
88 no_env_forward: bool,
89
90 /// Output format for stdout. `text` (default) for human-readable styled
91 /// output; `json` emits a single-line JSON object with worktree metadata
92 /// and suppresses the AI-tool launch. Useful for scripts and the Claude
93 /// Code `WorktreeCreate` hook integration.
94 #[arg(long, value_enum, default_value_t = EmitFormat::Text)]
95 emit: EmitFormat,
96
97 /// Extra arguments forwarded verbatim to the AI tool (claude/codex/
98 /// gemini). Mutually exclusive with `--prompt`/`--prompt-file`
99 /// because both ultimately set the AI tool's prompt — use one or
100 /// the other.
101 #[arg(
102 trailing_var_arg = true,
103 allow_hyphen_values = true,
104 value_name = "AI_TOOL_ARGS"
105 )]
106 forward_args: Vec<String>,
107 },
108
109 /// Resume AI work in a worktree.
110 ///
111 /// Trailing positional args are forwarded verbatim to the AI tool. The
112 /// tool's own resume flag (`--continue`/`--resume`) is always injected
113 /// alongside, so `gw resume foo -- --model opus` resumes with opus.
114 Resume {
115 /// Branch name, worktree name, or path to resume (default: current worktree)
116 branch: Option<String>,
117
118 /// Terminal launch method for THIS invocation. Overrides config
119 /// and CW_LAUNCH_METHOD. Accepts canonical names or aliases.
120 /// Supports `method:session-name` for tmux/zellij. Use `noop`,
121 /// `none`, or `skip` to skip the AI tool launch.
122 #[arg(short = 'T', long = "term", value_name = "METHOD")]
123 term: Option<String>,
124
125 /// Disable auto-forwarding of `<TOOL>_*` environment variables.
126 #[arg(long = "no-env-forward")]
127 no_env_forward: bool,
128
129 /// Extra arguments forwarded verbatim to the AI tool (claude/codex/
130 /// gemini). The tool's resume flag is still injected automatically.
131 #[arg(
132 trailing_var_arg = true,
133 allow_hyphen_values = true,
134 value_name = "AI_TOOL_ARGS"
135 )]
136 forward_args: Vec<String>,
137 },
138
139 /// Launch AI tool in an existing worktree (default: current).
140 ///
141 /// Trailing positional args are forwarded verbatim to the AI tool.
142 #[command(group(
143 clap::ArgGroup::new("prompt_source")
144 .args(["prompt", "prompt_file"])
145 .multiple(false)
146 .required(false)
147 ))]
148 Spawn {
149 /// Worktree target — exact worktree name, branch name, or path.
150 /// Default: current worktree.
151 target: Option<String>,
152
153 /// Terminal launch method for THIS invocation. Overrides config
154 /// and CW_LAUNCH_METHOD. Accepts canonical names or aliases.
155 /// Supports `method:session-name` for tmux/zellij. Use `noop`,
156 /// `none`, or `skip` to skip the AI tool launch.
157 #[arg(short = 'T', long = "term", value_name = "METHOD")]
158 term: Option<String>,
159
160 /// Initial prompt to pass to the AI tool. Use `-` to read from stdin.
161 #[arg(long)]
162 prompt: Option<String>,
163
164 /// Read the initial prompt from a file.
165 #[arg(long = "prompt-file", value_hint = ValueHint::FilePath)]
166 prompt_file: Option<PathBuf>,
167
168 /// Disable auto-forwarding of `<TOOL>_*` environment variables.
169 #[arg(long = "no-env-forward")]
170 no_env_forward: bool,
171
172 /// Extra arguments forwarded verbatim to the AI tool.
173 #[arg(
174 trailing_var_arg = true,
175 allow_hyphen_values = true,
176 value_name = "AI_TOOL_ARGS"
177 )]
178 forward_args: Vec<String>,
179 },
180
181 /// Remove one or more worktrees.
182 ///
183 /// With no arguments: removes the current worktree (must be inside one).
184 /// With one or more positional targets: removes each of them; flags apply
185 /// to every target.
186 /// With `-i`: opens a multi-select UI.
187 ///
188 /// Exits 0 on full success, 1 if the user cancelled at the confirmation
189 /// prompt or in the interactive UI, 2 if any target could not be removed
190 /// (not found, busy, or an error).
191 Rm {
192 /// Branch names or paths of worktrees to remove.
193 /// If empty and --interactive is not set, removes the current worktree.
194 #[arg(conflicts_with = "interactive")]
195 targets: Vec<String>,
196
197 /// Interactive multi-select UI (mutually exclusive with positional targets)
198 #[arg(short, long, conflicts_with = "targets")]
199 interactive: bool,
200
201 /// Show what would be removed without removing
202 #[arg(long)]
203 dry_run: bool,
204
205 /// Keep the branch (only remove worktree)
206 #[arg(short = 'k', long)]
207 keep_branch: bool,
208
209 /// Also delete the remote branch
210 #[arg(short = 'r', long)]
211 delete_remote: bool,
212
213 /// Force remove: also bypasses the busy-detection gate (skips the
214 /// "worktree is in use" check and removes anyway)
215 #[arg(short, long, conflicts_with = "no_force")]
216 force: bool,
217
218 /// Don't use --force flag
219 #[arg(long)]
220 no_force: bool,
221 },
222
223 /// List all worktrees (rich, human-readable)
224 List,
225
226 /// Print all worktrees as TSV (for scripts)
227 ///
228 /// Columns: worktree_id, branch, status, age, repo_root, path
229 Ls,
230
231 /// Run diagnostics
232 Doctor {
233 /// Hook-friendly mode: emit a single-line summary and exit 0.
234 #[arg(long)]
235 session_start: bool,
236 /// Suppress informational chatter; keep only the summary.
237 #[arg(long)]
238 quiet: bool,
239 },
240
241 /// Check for updates / upgrade
242 Upgrade {
243 /// Skip the confirmation prompt; required for non-TTY environments.
244 #[arg(short, long)]
245 yes: bool,
246 },
247
248 /// Install Claude Code skills and hooks into the current repo's `.claude/`.
249 ///
250 /// Writes skill files into `.claude/skills/gw-delegate/` and
251 /// `.claude/skills/gw-manage/`, then registers three Claude Code hooks
252 /// (PreToolUse Bash guard, WorktreeCreate, WorktreeRemove) into
253 /// `.claude/settings.json`. Idempotent — re-running only writes files
254 /// whose content changed.
255 #[command(name = "setup-claude")]
256 SetupClaude,
257
258 /// View or edit gw configuration.
259 ///
260 /// Configuration lives in two scopes:
261 /// - `global`: `~/.config/git-worktree-manager/config.json`
262 /// - `repo`: `<repo-root>/.cwconfig.json` (overrides global)
263 ///
264 /// `set` writes to `global` by default; pass `--repo` for a repo
265 /// override. `edit` opens an interactive TUI that lets you toggle
266 /// between scopes with Tab.
267 Config {
268 #[command(subcommand)]
269 action: ConfigAction,
270 },
271
272 /// Interactive shell integration setup
273 ShellSetup,
274
275 /// Run cmd in one specific worktree.
276 Exec {
277 /// Worktree name, branch name, or path.
278 target: String,
279
280 /// Command and args.
281 #[arg(trailing_var_arg = true, allow_hyphen_values = true, required = true)]
282 cmd: Vec<String>,
283 },
284
285 /// Run cmd in every worktree in scope.
286 Run {
287 /// Glob filter on branch name or worktree directory basename
288 /// (e.g. 'feat-*'). Matches if either side does.
289 #[arg(long)]
290 only: Option<String>,
291 /// Skip the main worktree.
292 #[arg(long = "no-main")]
293 no_main: bool,
294 /// Parallel worktrees.
295 #[arg(short = 'j', long, default_value = "1")]
296 jobs: usize,
297 /// Continue past per-worktree failures.
298 #[arg(long)]
299 continue_on_error: bool,
300 /// Command and args.
301 #[arg(trailing_var_arg = true, allow_hyphen_values = true, required = true)]
302 cmd: Vec<String>,
303 },
304
305 /// Hook helper: read a Claude Code hook payload from stdin (or a file)
306 /// and decide whether to allow or block the inbound tool use. Exits 0
307 /// to allow; non-zero with stderr message to block.
308 Guard {
309 /// Path to read the hook payload from, or "-" for stdin.
310 #[arg(long, value_name = "PATH")]
311 tool_input: String,
312 },
313
314 /// [Internal] Read a Claude Code WorktreeCreate hook payload from stdin
315 /// and create a worktree via `gw new --emit json --term skip` under the hood.
316 /// Prints only the worktree path (plain text, one line) to stdout.
317 #[command(name = "_claude-worktree-create", hide = true)]
318 ClaudeWorktreeCreate,
319
320 /// [Internal] Read a Claude Code WorktreeRemove hook payload from stdin
321 /// and run the configured `pre_rm` hook as a best-effort advisory step.
322 /// Always exits 0; Claude Code owns the actual removal.
323 #[command(name = "_claude-worktree-remove", hide = true)]
324 ClaudeWorktreeRemove,
325
326 /// [Internal] Get worktree path for a branch
327 #[command(name = "_path", hide = true)]
328 Path {
329 /// Branch name
330 branch: Option<String>,
331
332 /// List branch names (for tab completion)
333 #[arg(long)]
334 list_branches: bool,
335
336 /// Interactive worktree selection
337 #[arg(short, long)]
338 interactive: bool,
339 },
340
341 /// Generate shell function for gw-cd
342 #[command(name = "_shell-function", hide = true)]
343 ShellFunction {
344 /// Shell type: bash, zsh, fish, or powershell
345 shell: String,
346 },
347
348 /// Refresh update cache (background process)
349 #[command(name = "_update-cache", hide = true)]
350 UpdateCache,
351
352 /// [Internal] Print completion targets for the current repo (one per line)
353 #[command(name = "_complete-targets", hide = true)]
354 CompleteTargets,
355
356 /// [Internal] Execute an AI tool spawn spec file
357 #[command(name = "_spawn-ai", hide = true)]
358 SpawnAi {
359 /// Path to the JSON spawn spec. If omitted, resolves the most recent
360 /// spec for the current worktree from `<git-dir>/gw-spawn-last.json`.
361 #[arg(value_hint = ValueHint::FilePath)]
362 spec: Option<PathBuf>,
363 },
364}
365
366/// Subcommands under `gw config`.
367#[derive(Subcommand, Debug)]
368pub enum ConfigAction {
369 /// List every known key with its current value and scope.
370 List,
371
372 /// Print the resolved value of a single key (repo > global > default).
373 Get {
374 /// Key to look up (e.g. `ai-tool.command`).
375 key: ConfigKey,
376 },
377
378 /// Set a key. Writes to global config by default; `--repo` writes to
379 /// `<repo-root>/.cwconfig.json` as an override.
380 Set {
381 /// Key to set.
382 key: ConfigKey,
383 /// New value. Strings are taken as-is; booleans accept
384 /// true/false/1/0/yes/no/on/off; `ai-tool.args` accepts either
385 /// whitespace-separated tokens or a JSON array literal.
386 value: String,
387 /// Write to repo scope instead of global.
388 #[arg(long)]
389 repo: bool,
390 },
391
392 /// Open an interactive TUI that lets you browse/edit both scopes.
393 Edit,
394}