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;
5pub mod global;
6
7use clap::{Parser, Subcommand, ValueHint};
8
9/// Validate config key (accepts any string but provides completion hints).
10fn parse_config_key(s: &str) -> Result<String, String> {
11 Ok(s.to_string())
12}
13
14/// Parse duration strings like "30", "30d", "2w", "1m" into days.
15fn parse_duration_days(s: &str) -> Result<u64, String> {
16 let s = s.trim();
17 if s.is_empty() {
18 return Err("empty duration".into());
19 }
20
21 // Pure number = days
22 if let Ok(n) = s.parse::<u64>() {
23 return Ok(n);
24 }
25
26 let (num_str, suffix) = s.split_at(s.len() - 1);
27 let n: u64 = num_str
28 .parse()
29 .map_err(|_| format!("invalid duration: '{}'. Use e.g. 30, 7d, 2w, 1m", s))?;
30
31 match suffix {
32 "d" => Ok(n),
33 "w" => Ok(n * 7),
34 "m" => Ok(n * 30),
35 "y" => Ok(n * 365),
36 _ => Err(format!(
37 "unknown duration suffix '{}'. Use d (days), w (weeks), m (months), y (years)",
38 suffix
39 )),
40 }
41}
42
43/// Git worktree manager CLI.
44#[derive(Parser, Debug)]
45#[command(
46 name = "gw",
47 version,
48 about = "git worktree manager — AI coding assistant integration",
49 long_about = None,
50 arg_required_else_help = true,
51)]
52pub struct Cli {
53 /// Run in global mode (across all registered repositories)
54 #[arg(short = 'g', long = "global", global = true)]
55 pub global: bool,
56
57 /// Generate shell completions for the given shell
58 #[arg(long, value_name = "SHELL", value_parser = clap::builder::PossibleValuesParser::new(["bash", "zsh", "fish", "powershell", "elvish"]))]
59 pub generate_completion: Option<String>,
60
61 #[command(subcommand)]
62 pub command: Option<Commands>,
63}
64
65#[derive(Subcommand, Debug)]
66pub enum Commands {
67 /// Create new worktree for feature branch
68 New {
69 /// Branch name for the new worktree
70 name: String,
71
72 /// Custom worktree path (default: ../<repo>-<branch>)
73 #[arg(short, long, value_hint = ValueHint::DirPath)]
74 path: Option<String>,
75
76 /// Base branch to create from (default: from config)
77 #[arg(short = 'b', long = "base")]
78 base: Option<String>,
79
80 /// Skip AI tool launch
81 #[arg(long = "no-term")]
82 no_term: bool,
83
84 /// Terminal launch method (e.g., tmux, iterm-tab, zellij)
85 #[arg(short = 'T', long)]
86 term: Option<String>,
87
88 /// Launch AI tool in background
89 #[arg(long)]
90 bg: bool,
91
92 /// Initial prompt to pass to the AI tool (starts interactive session with task)
93 #[arg(long)]
94 prompt: Option<String>,
95 },
96
97 /// Create GitHub Pull Request from worktree
98 Pr {
99 /// Branch name (default: current worktree branch)
100 branch: Option<String>,
101
102 /// PR title
103 #[arg(short, long)]
104 title: Option<String>,
105
106 /// PR body
107 #[arg(short = 'B', long)]
108 body: Option<String>,
109
110 /// Create as draft PR
111 #[arg(short, long)]
112 draft: bool,
113
114 /// Skip pushing to remote
115 #[arg(long)]
116 no_push: bool,
117
118 /// Resolve target as worktree name (instead of branch)
119 #[arg(short, long)]
120 worktree: bool,
121
122 /// Resolve target as branch name (instead of worktree)
123 #[arg(short = 'b', long = "by-branch", conflicts_with = "worktree")]
124 by_branch: bool,
125 },
126
127 /// Merge feature branch into base branch
128 Merge {
129 /// Branch name (default: current worktree branch)
130 branch: Option<String>,
131
132 /// Interactive rebase
133 #[arg(short, long)]
134 interactive: bool,
135
136 /// Dry run (show what would happen)
137 #[arg(long)]
138 dry_run: bool,
139
140 /// Push to remote after merge
141 #[arg(long)]
142 push: bool,
143
144 /// Use AI to resolve merge conflicts
145 #[arg(long)]
146 ai_merge: bool,
147
148 /// Resolve target as worktree name (instead of branch)
149 #[arg(short, long)]
150 worktree: bool,
151 },
152
153 /// Resume AI work in a worktree
154 Resume {
155 /// Branch name to resume (default: current worktree)
156 branch: Option<String>,
157
158 /// Terminal launch method
159 #[arg(short = 'T', long)]
160 term: Option<String>,
161
162 /// Launch AI tool in background
163 #[arg(long)]
164 bg: bool,
165
166 /// Resolve target as worktree name (instead of branch)
167 #[arg(short, long)]
168 worktree: bool,
169
170 /// Resolve target as branch name (instead of worktree)
171 #[arg(short, long, conflicts_with = "worktree")]
172 by_branch: bool,
173 },
174
175 /// Open interactive shell or execute command in a worktree
176 Shell {
177 /// Worktree branch to shell into
178 worktree: Option<String>,
179
180 /// Command and arguments to execute
181 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
182 args: Vec<String>,
183 },
184
185 /// Show current worktree status
186 Status,
187
188 /// Delete a worktree
189 Delete {
190 /// Branch name or path of worktree to delete (default: current worktree)
191 target: Option<String>,
192
193 /// Keep the branch (only remove worktree)
194 #[arg(short = 'k', long)]
195 keep_branch: bool,
196
197 /// Also delete the remote branch
198 #[arg(short = 'r', long)]
199 delete_remote: bool,
200
201 /// Force remove: also bypasses the busy-detection gate (skips the
202 /// "worktree is in use" check and deletes anyway)
203 #[arg(short, long, conflicts_with = "no_force")]
204 force: bool,
205
206 /// Don't use --force flag
207 #[arg(long)]
208 no_force: bool,
209
210 /// Resolve target as worktree name (instead of branch)
211 #[arg(short, long)]
212 worktree: bool,
213
214 /// Resolve target as branch name (instead of worktree)
215 #[arg(short, long, conflicts_with = "worktree")]
216 branch: bool,
217 },
218
219 /// List all worktrees
220 #[command(alias = "ls")]
221 List,
222
223 /// Batch cleanup of worktrees
224 Clean {
225 /// Delete worktrees for branches already merged to base
226 #[arg(long)]
227 merged: bool,
228
229 /// Delete worktrees older than duration (e.g., 7, 30d, 2w, 1m)
230 #[arg(long, value_name = "DURATION", value_parser = parse_duration_days)]
231 older_than: Option<u64>,
232
233 /// Interactive selection UI
234 #[arg(short, long)]
235 interactive: bool,
236
237 /// Show what would be deleted without deleting
238 #[arg(long)]
239 dry_run: bool,
240
241 /// Bypass the busy-detection gate: delete busy worktrees too
242 /// (default: skip worktrees another session is using)
243 #[arg(short, long)]
244 force: bool,
245 },
246
247 /// Display worktree hierarchy as a tree
248 Tree,
249
250 /// Show worktree statistics
251 Stats,
252
253 /// Compare two branches
254 Diff {
255 /// First branch
256 branch1: String,
257 /// Second branch
258 branch2: String,
259 /// Show statistics only
260 #[arg(short, long)]
261 summary: bool,
262 /// Show changed files only
263 #[arg(short, long)]
264 files: bool,
265 },
266
267 /// Sync worktree with base branch
268 Sync {
269 /// Branch name (default: current worktree)
270 branch: Option<String>,
271
272 /// Sync all worktrees
273 #[arg(long)]
274 all: bool,
275
276 /// Only fetch updates without rebasing
277 #[arg(long)]
278 fetch_only: bool,
279
280 /// Use AI to resolve merge conflicts
281 #[arg(long)]
282 ai_merge: bool,
283
284 /// Resolve target as worktree name (instead of branch)
285 #[arg(short, long)]
286 worktree: bool,
287
288 /// Resolve target as branch name (instead of worktree)
289 #[arg(short, long, conflicts_with = "worktree")]
290 by_branch: bool,
291 },
292
293 /// Change base branch for a worktree
294 ChangeBase {
295 /// New base branch
296 new_base: String,
297 /// Branch name (default: current worktree)
298 branch: Option<String>,
299
300 /// Dry run (show what would happen)
301 #[arg(long)]
302 dry_run: bool,
303
304 /// Interactive rebase
305 #[arg(short, long)]
306 interactive: bool,
307
308 /// Resolve target as worktree name (instead of branch)
309 #[arg(short, long)]
310 worktree: bool,
311
312 /// Resolve target as branch name (instead of worktree)
313 #[arg(short, long, conflicts_with = "worktree")]
314 by_branch: bool,
315 },
316
317 /// Configuration management
318 Config {
319 #[command(subcommand)]
320 action: ConfigAction,
321 },
322
323 /// Backup and restore worktrees
324 Backup {
325 #[command(subcommand)]
326 action: BackupAction,
327 },
328
329 /// Stash management (worktree-aware)
330 Stash {
331 #[command(subcommand)]
332 action: StashAction,
333 },
334
335 /// Manage lifecycle hooks
336 Hook {
337 #[command(subcommand)]
338 action: HookAction,
339 },
340
341 /// Export worktree configuration to a file
342 Export {
343 /// Output file path
344 #[arg(short, long)]
345 output: Option<String>,
346 },
347
348 /// Import worktree configuration from a file
349 Import {
350 /// Path to the configuration file to import
351 import_file: String,
352
353 /// Apply the imported configuration (default: preview only)
354 #[arg(long)]
355 apply: bool,
356 },
357
358 /// Scan for repositories (global mode)
359 Scan {
360 /// Base directory to scan (default: home directory)
361 #[arg(short, long, value_hint = ValueHint::DirPath)]
362 dir: Option<std::path::PathBuf>,
363 },
364
365 /// Clean up stale registry entries (global mode)
366 Prune,
367
368 /// Run diagnostics
369 Doctor,
370
371 /// Check for updates / upgrade
372 Upgrade,
373
374 /// Install Claude Code skill for worktree task delegation
375 #[command(name = "setup-claude")]
376 SetupClaude,
377
378 /// Interactive shell integration setup
379 ShellSetup,
380
381 /// [Internal] Get worktree path for a branch
382 #[command(name = "_path", hide = true)]
383 Path {
384 /// Branch name
385 branch: Option<String>,
386
387 /// List branch names (for tab completion)
388 #[arg(long)]
389 list_branches: bool,
390
391 /// Interactive worktree selection
392 #[arg(short, long)]
393 interactive: bool,
394 },
395
396 /// Generate shell function for gw-cd / cw-cd
397 #[command(name = "_shell-function", hide = true)]
398 ShellFunction {
399 /// Shell type: bash, zsh, fish, or powershell
400 shell: String,
401 },
402
403 /// List config keys (for tab completion)
404 #[command(name = "_config-keys", hide = true)]
405 ConfigKeys,
406
407 /// Refresh update cache (background process)
408 #[command(name = "_update-cache", hide = true)]
409 UpdateCache,
410
411 /// List terminal launch method values (for tab completion)
412 #[command(name = "_term-values", hide = true)]
413 TermValues,
414
415 /// List preset names (for tab completion)
416 #[command(name = "_preset-names", hide = true)]
417 PresetNames,
418
419 /// List hook event names (for tab completion)
420 #[command(name = "_hook-events", hide = true)]
421 HookEvents,
422}
423
424#[derive(Subcommand, Debug)]
425pub enum ConfigAction {
426 /// Show current configuration summary
427 Show,
428 /// List all configuration keys, values, and descriptions
429 #[command(alias = "ls")]
430 List,
431 /// Get a configuration value
432 Get {
433 /// Dot-separated config key (e.g., ai_tool.command)
434 #[arg(value_parser = parse_config_key)]
435 key: String,
436 },
437 /// Set a configuration value
438 Set {
439 /// Dot-separated config key (e.g., ai_tool.command)
440 #[arg(value_parser = parse_config_key)]
441 key: String,
442 /// Value to set
443 value: String,
444 },
445 /// Use a predefined AI tool preset
446 UsePreset {
447 /// Preset name (e.g., claude, codex, no-op)
448 #[arg(value_parser = clap::builder::PossibleValuesParser::new(crate::constants::PRESET_NAMES))]
449 name: String,
450 },
451 /// List available presets
452 ListPresets,
453 /// Reset configuration to defaults
454 Reset,
455}
456
457#[derive(Subcommand, Debug)]
458pub enum BackupAction {
459 /// Create backup of worktree(s) using git bundle
460 Create {
461 /// Branch name to backup (default: current worktree)
462 branch: Option<String>,
463
464 /// Backup all worktrees
465 #[arg(long)]
466 all: bool,
467
468 /// Output directory for backups
469 #[arg(short, long)]
470 output: Option<String>,
471 },
472 /// List available backups
473 List {
474 /// Filter by branch name
475 branch: Option<String>,
476
477 /// Show all backups (not just current repo)
478 #[arg(short, long)]
479 all: bool,
480 },
481 /// Restore worktree from backup
482 Restore {
483 /// Branch name to restore
484 branch: String,
485
486 /// Custom path for restored worktree
487 #[arg(short, long)]
488 path: Option<String>,
489
490 /// Backup ID (timestamp) to restore (default: latest)
491 #[arg(long)]
492 id: Option<String>,
493 },
494}
495
496#[derive(Subcommand, Debug)]
497pub enum StashAction {
498 /// Save changes in current worktree to stash
499 Save {
500 /// Optional message to describe the stash
501 message: Option<String>,
502 },
503 /// List all stashes organized by worktree/branch
504 List,
505 /// Apply a stash to a different worktree
506 Apply {
507 /// Branch name of worktree to apply stash to
508 target_branch: String,
509
510 /// Stash reference (default: stash@{0})
511 #[arg(short, long, default_value = "stash@{0}")]
512 stash: String,
513 },
514}
515
516#[derive(Subcommand, Debug)]
517pub enum HookAction {
518 /// Add a new hook for an event
519 Add {
520 /// Hook event (e.g., worktree.post_create, merge.pre)
521 #[arg(value_parser = clap::builder::PossibleValuesParser::new(crate::constants::HOOK_EVENTS))]
522 event: String,
523 /// Shell command to execute
524 command: String,
525 /// Custom hook identifier
526 #[arg(long)]
527 id: Option<String>,
528 /// Human-readable description
529 #[arg(short, long)]
530 description: Option<String>,
531 },
532 /// Remove a hook
533 Remove {
534 /// Hook event
535 #[arg(value_parser = clap::builder::PossibleValuesParser::new(crate::constants::HOOK_EVENTS))]
536 event: String,
537 /// Hook identifier to remove
538 hook_id: String,
539 },
540 /// List all hooks
541 List {
542 /// Filter by event
543 #[arg(value_parser = clap::builder::PossibleValuesParser::new(crate::constants::HOOK_EVENTS))]
544 event: Option<String>,
545 },
546 /// Enable a disabled hook
547 Enable {
548 /// Hook event
549 #[arg(value_parser = clap::builder::PossibleValuesParser::new(crate::constants::HOOK_EVENTS))]
550 event: String,
551 /// Hook identifier
552 hook_id: String,
553 },
554 /// Disable a hook without removing it
555 Disable {
556 /// Hook event
557 #[arg(value_parser = clap::builder::PossibleValuesParser::new(crate::constants::HOOK_EVENTS))]
558 event: String,
559 /// Hook identifier
560 hook_id: String,
561 },
562 /// Manually run all hooks for an event
563 Run {
564 /// Hook event to run
565 #[arg(value_parser = clap::builder::PossibleValuesParser::new(crate::constants::HOOK_EVENTS))]
566 event: String,
567 /// Show what would be executed without running
568 #[arg(long)]
569 dry_run: bool,
570 },
571}