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