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 even if worktree has changes (default)
202 #[arg(short, long, conflicts_with = "no_force")]
203 force: bool,
204
205 /// Don't use --force flag
206 #[arg(long)]
207 no_force: bool,
208
209 /// Resolve target as worktree name (instead of branch)
210 #[arg(short, long)]
211 worktree: bool,
212
213 /// Resolve target as branch name (instead of worktree)
214 #[arg(short, long, conflicts_with = "worktree")]
215 branch: bool,
216 },
217
218 /// List all worktrees
219 #[command(alias = "ls")]
220 List,
221
222 /// Batch cleanup of worktrees
223 Clean {
224 /// Delete worktrees for branches already merged to base
225 #[arg(long)]
226 merged: bool,
227
228 /// Delete worktrees older than duration (e.g., 7, 30d, 2w, 1m)
229 #[arg(long, value_name = "DURATION", value_parser = parse_duration_days)]
230 older_than: Option<u64>,
231
232 /// Interactive selection UI
233 #[arg(short, long)]
234 interactive: bool,
235
236 /// Show what would be deleted without deleting
237 #[arg(long)]
238 dry_run: bool,
239 },
240
241 /// Display worktree hierarchy as a tree
242 Tree,
243
244 /// Show worktree statistics
245 Stats,
246
247 /// Compare two branches
248 Diff {
249 /// First branch
250 branch1: String,
251 /// Second branch
252 branch2: String,
253 /// Show statistics only
254 #[arg(short, long)]
255 summary: bool,
256 /// Show changed files only
257 #[arg(short, long)]
258 files: bool,
259 },
260
261 /// Sync worktree with base branch
262 Sync {
263 /// Branch name (default: current worktree)
264 branch: Option<String>,
265
266 /// Sync all worktrees
267 #[arg(long)]
268 all: bool,
269
270 /// Only fetch updates without rebasing
271 #[arg(long)]
272 fetch_only: bool,
273
274 /// Use AI to resolve merge conflicts
275 #[arg(long)]
276 ai_merge: bool,
277
278 /// Resolve target as worktree name (instead of branch)
279 #[arg(short, long)]
280 worktree: bool,
281
282 /// Resolve target as branch name (instead of worktree)
283 #[arg(short, long, conflicts_with = "worktree")]
284 by_branch: bool,
285 },
286
287 /// Change base branch for a worktree
288 ChangeBase {
289 /// New base branch
290 new_base: String,
291 /// Branch name (default: current worktree)
292 branch: Option<String>,
293
294 /// Dry run (show what would happen)
295 #[arg(long)]
296 dry_run: bool,
297
298 /// Interactive rebase
299 #[arg(short, long)]
300 interactive: bool,
301
302 /// Resolve target as worktree name (instead of branch)
303 #[arg(short, long)]
304 worktree: bool,
305
306 /// Resolve target as branch name (instead of worktree)
307 #[arg(short, long, conflicts_with = "worktree")]
308 by_branch: bool,
309 },
310
311 /// Configuration management
312 Config {
313 #[command(subcommand)]
314 action: ConfigAction,
315 },
316
317 /// Backup and restore worktrees
318 Backup {
319 #[command(subcommand)]
320 action: BackupAction,
321 },
322
323 /// Stash management (worktree-aware)
324 Stash {
325 #[command(subcommand)]
326 action: StashAction,
327 },
328
329 /// Manage lifecycle hooks
330 Hook {
331 #[command(subcommand)]
332 action: HookAction,
333 },
334
335 /// Export worktree configuration to a file
336 Export {
337 /// Output file path
338 #[arg(short, long)]
339 output: Option<String>,
340 },
341
342 /// Import worktree configuration from a file
343 Import {
344 /// Path to the configuration file to import
345 import_file: String,
346
347 /// Apply the imported configuration (default: preview only)
348 #[arg(long)]
349 apply: bool,
350 },
351
352 /// Scan for repositories (global mode)
353 Scan {
354 /// Base directory to scan (default: home directory)
355 #[arg(short, long, value_hint = ValueHint::DirPath)]
356 dir: Option<std::path::PathBuf>,
357 },
358
359 /// Clean up stale registry entries (global mode)
360 Prune,
361
362 /// Run diagnostics
363 Doctor,
364
365 /// Check for updates / upgrade
366 Upgrade,
367
368 /// Install Claude Code skill for worktree task delegation
369 #[command(name = "setup-claude")]
370 SetupClaude,
371
372 /// Interactive shell integration setup
373 ShellSetup,
374
375 /// [Internal] Get worktree path for a branch
376 #[command(name = "_path", hide = true)]
377 Path {
378 /// Branch name
379 branch: Option<String>,
380
381 /// List branch names (for tab completion)
382 #[arg(long)]
383 list_branches: bool,
384
385 /// Interactive worktree selection
386 #[arg(short, long)]
387 interactive: bool,
388 },
389
390 /// Generate shell function for gw-cd / cw-cd
391 #[command(name = "_shell-function", hide = true)]
392 ShellFunction {
393 /// Shell type: bash, zsh, fish, or powershell
394 shell: String,
395 },
396
397 /// List config keys (for tab completion)
398 #[command(name = "_config-keys", hide = true)]
399 ConfigKeys,
400
401 /// Refresh update cache (background process)
402 #[command(name = "_update-cache", hide = true)]
403 UpdateCache,
404
405 /// List terminal launch method values (for tab completion)
406 #[command(name = "_term-values", hide = true)]
407 TermValues,
408
409 /// List preset names (for tab completion)
410 #[command(name = "_preset-names", hide = true)]
411 PresetNames,
412
413 /// List hook event names (for tab completion)
414 #[command(name = "_hook-events", hide = true)]
415 HookEvents,
416}
417
418#[derive(Subcommand, Debug)]
419pub enum ConfigAction {
420 /// Show current configuration summary
421 Show,
422 /// List all configuration keys, values, and descriptions
423 #[command(alias = "ls")]
424 List,
425 /// Get a configuration value
426 Get {
427 /// Dot-separated config key (e.g., ai_tool.command)
428 #[arg(value_parser = parse_config_key)]
429 key: String,
430 },
431 /// Set a configuration value
432 Set {
433 /// Dot-separated config key (e.g., ai_tool.command)
434 #[arg(value_parser = parse_config_key)]
435 key: String,
436 /// Value to set
437 value: String,
438 },
439 /// Use a predefined AI tool preset
440 UsePreset {
441 /// Preset name (e.g., claude, codex, no-op)
442 #[arg(value_parser = clap::builder::PossibleValuesParser::new(crate::constants::PRESET_NAMES))]
443 name: String,
444 },
445 /// List available presets
446 ListPresets,
447 /// Reset configuration to defaults
448 Reset,
449}
450
451#[derive(Subcommand, Debug)]
452pub enum BackupAction {
453 /// Create backup of worktree(s) using git bundle
454 Create {
455 /// Branch name to backup (default: current worktree)
456 branch: Option<String>,
457
458 /// Backup all worktrees
459 #[arg(long)]
460 all: bool,
461
462 /// Output directory for backups
463 #[arg(short, long)]
464 output: Option<String>,
465 },
466 /// List available backups
467 List {
468 /// Filter by branch name
469 branch: Option<String>,
470
471 /// Show all backups (not just current repo)
472 #[arg(short, long)]
473 all: bool,
474 },
475 /// Restore worktree from backup
476 Restore {
477 /// Branch name to restore
478 branch: String,
479
480 /// Custom path for restored worktree
481 #[arg(short, long)]
482 path: Option<String>,
483
484 /// Backup ID (timestamp) to restore (default: latest)
485 #[arg(long)]
486 id: Option<String>,
487 },
488}
489
490#[derive(Subcommand, Debug)]
491pub enum StashAction {
492 /// Save changes in current worktree to stash
493 Save {
494 /// Optional message to describe the stash
495 message: Option<String>,
496 },
497 /// List all stashes organized by worktree/branch
498 List,
499 /// Apply a stash to a different worktree
500 Apply {
501 /// Branch name of worktree to apply stash to
502 target_branch: String,
503
504 /// Stash reference (default: stash@{0})
505 #[arg(short, long, default_value = "stash@{0}")]
506 stash: String,
507 },
508}
509
510#[derive(Subcommand, Debug)]
511pub enum HookAction {
512 /// Add a new hook for an event
513 Add {
514 /// Hook event (e.g., worktree.post_create, merge.pre)
515 #[arg(value_parser = clap::builder::PossibleValuesParser::new(crate::constants::HOOK_EVENTS))]
516 event: String,
517 /// Shell command to execute
518 command: String,
519 /// Custom hook identifier
520 #[arg(long)]
521 id: Option<String>,
522 /// Human-readable description
523 #[arg(short, long)]
524 description: Option<String>,
525 },
526 /// Remove a hook
527 Remove {
528 /// Hook event
529 #[arg(value_parser = clap::builder::PossibleValuesParser::new(crate::constants::HOOK_EVENTS))]
530 event: String,
531 /// Hook identifier to remove
532 hook_id: String,
533 },
534 /// List all hooks
535 List {
536 /// Filter by event
537 #[arg(value_parser = clap::builder::PossibleValuesParser::new(crate::constants::HOOK_EVENTS))]
538 event: Option<String>,
539 },
540 /// Enable a disabled hook
541 Enable {
542 /// Hook event
543 #[arg(value_parser = clap::builder::PossibleValuesParser::new(crate::constants::HOOK_EVENTS))]
544 event: String,
545 /// Hook identifier
546 hook_id: String,
547 },
548 /// Disable a hook without removing it
549 Disable {
550 /// Hook event
551 #[arg(value_parser = clap::builder::PossibleValuesParser::new(crate::constants::HOOK_EVENTS))]
552 event: String,
553 /// Hook identifier
554 hook_id: String,
555 },
556 /// Manually run all hooks for an event
557 Run {
558 /// Hook event to run
559 #[arg(value_parser = clap::builder::PossibleValuesParser::new(crate::constants::HOOK_EVENTS))]
560 event: String,
561 /// Show what would be executed without running
562 #[arg(long)]
563 dry_run: bool,
564 },
565}