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")]
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
406#[derive(Subcommand, Debug)]
407pub enum ConfigAction {
408 /// Show current configuration summary
409 Show,
410 /// List all configuration keys, values, and descriptions
411 #[command(alias = "ls")]
412 List,
413 /// Get a configuration value
414 Get {
415 /// Dot-separated config key (e.g., ai_tool.command)
416 #[arg(value_parser = parse_config_key)]
417 key: String,
418 },
419 /// Set a configuration value
420 Set {
421 /// Dot-separated config key (e.g., ai_tool.command)
422 #[arg(value_parser = parse_config_key)]
423 key: String,
424 /// Value to set
425 value: String,
426 },
427 /// Use a predefined AI tool preset
428 UsePreset {
429 /// Preset name (e.g., claude, codex, no-op)
430 name: String,
431 },
432 /// List available presets
433 ListPresets,
434 /// Reset configuration to defaults
435 Reset,
436}
437
438#[derive(Subcommand, Debug)]
439pub enum BackupAction {
440 /// Create backup of worktree(s) using git bundle
441 Create {
442 /// Branch name to backup (default: current worktree)
443 branch: Option<String>,
444
445 /// Backup all worktrees
446 #[arg(long)]
447 all: bool,
448
449 /// Output directory for backups
450 #[arg(short, long)]
451 output: Option<String>,
452 },
453 /// List available backups
454 List {
455 /// Filter by branch name
456 branch: Option<String>,
457
458 /// Show all backups (not just current repo)
459 #[arg(short, long)]
460 all: bool,
461 },
462 /// Restore worktree from backup
463 Restore {
464 /// Branch name to restore
465 branch: String,
466
467 /// Custom path for restored worktree
468 #[arg(short, long)]
469 path: Option<String>,
470
471 /// Backup ID (timestamp) to restore (default: latest)
472 #[arg(long)]
473 id: Option<String>,
474 },
475}
476
477#[derive(Subcommand, Debug)]
478pub enum StashAction {
479 /// Save changes in current worktree to stash
480 Save {
481 /// Optional message to describe the stash
482 message: Option<String>,
483 },
484 /// List all stashes organized by worktree/branch
485 List,
486 /// Apply a stash to a different worktree
487 Apply {
488 /// Branch name of worktree to apply stash to
489 target_branch: String,
490
491 /// Stash reference (default: stash@{0})
492 #[arg(short, long, default_value = "stash@{0}")]
493 stash: String,
494 },
495}
496
497#[derive(Subcommand, Debug)]
498pub enum HookAction {
499 /// Add a new hook for an event
500 Add {
501 /// Hook event (e.g., worktree.post_create, merge.pre)
502 event: String,
503 /// Shell command to execute
504 command: String,
505 /// Custom hook identifier
506 #[arg(long)]
507 id: Option<String>,
508 /// Human-readable description
509 #[arg(short, long)]
510 description: Option<String>,
511 },
512 /// Remove a hook
513 Remove {
514 /// Hook event
515 event: String,
516 /// Hook identifier to remove
517 hook_id: String,
518 },
519 /// List all hooks
520 List {
521 /// Filter by event
522 event: Option<String>,
523 },
524 /// Enable a disabled hook
525 Enable {
526 /// Hook event
527 event: String,
528 /// Hook identifier
529 hook_id: String,
530 },
531 /// Disable a hook without removing it
532 Disable {
533 /// Hook event
534 event: String,
535 /// Hook identifier
536 hook_id: String,
537 },
538 /// Manually run all hooks for an event
539 Run {
540 /// Hook event to run
541 event: String,
542 /// Show what would be executed without running
543 #[arg(long)]
544 dry_run: bool,
545 },
546}