1use clap::Parser;
9
10use crate::cli::{BackupAction, Cli, Commands, ConfigAction, HookAction, StashAction};
11use crate::config;
12use crate::console as cwconsole;
13use crate::constants;
14use crate::cwshare_setup;
15use crate::error::{CwError, Result};
16use crate::hooks;
17use crate::operations::{
18 ai_tools, backup, clean, config_ops, diagnostics, display, git_ops, global_ops, guard, helpers,
19 path_cmd, setup_claude, shell, spawn_spec, stash, worktree,
20};
21use crate::resolve_prompt;
22use crate::shell_functions;
23use crate::tui;
24use crate::update;
25use std::io::Read;
26
27pub fn run() {
28 tui::install_panic_hook();
29 let cli = Cli::parse();
30
31 if let Some(ref shell_name) = cli.generate_completion {
32 generate_completions(shell_name);
33 return;
34 }
35
36 let is_internal = matches!(
41 &cli.command,
42 Some(
43 Commands::UpdateCache
44 | Commands::ConfigKeys
45 | Commands::TermValues
46 | Commands::PresetNames
47 | Commands::HookEvents
48 | Commands::Path { .. }
49 | Commands::ShellFunction { .. }
50 | Commands::SpawnAi { .. }
51 | Commands::Guard { .. }
52 )
53 );
54
55 if !is_internal {
56 crate::operations::spawn_spec::sweep_stale();
57 update::check_for_update_if_needed();
58 }
59
60 if !is_internal {
61 config::prompt_shell_completion_setup();
62 }
63
64 helpers::set_global_mode(cli.global);
65
66 let result = match cli.command {
67 Some(Commands::List { cache }) => {
68 let no_cache = cache.no_cache;
69 if cli.global {
70 global_ops::global_list_worktrees(no_cache)
71 } else {
72 display::list_worktrees(no_cache)
73 }
74 }
75 Some(Commands::Status { cache }) => display::show_status(cache.no_cache),
76 Some(Commands::Tree { cache }) => display::show_tree(cache.no_cache),
77 Some(Commands::Stats { cache }) => display::show_stats(cache.no_cache),
78 Some(Commands::Diff {
79 branch1,
80 branch2,
81 summary,
82 files,
83 }) => display::diff_worktrees(&branch1, &branch2, summary, files),
84
85 Some(Commands::Config { action }) => match action {
86 ConfigAction::Show => config::show_config().map(|output| println!("{}", output)),
87 ConfigAction::List => config::list_config(),
88 ConfigAction::Get { key } => config::get_config_value(&key),
89 ConfigAction::Set { key, value } => config::set_config_value(&key, &value),
90 ConfigAction::UsePreset { name } => config::use_preset(&name),
91 ConfigAction::ListPresets => {
92 println!("{}", config::list_presets());
93 Ok(())
94 }
95 ConfigAction::Reset => config::reset_config(),
96 },
97
98 Some(Commands::New {
99 name,
100 path,
101 base,
102 no_term,
103 term,
104 bg,
105 fg,
106 prompt,
107 prompt_file,
108 prompt_stdin,
109 }) => (|| -> Result<()> {
110 let resolved = resolve_prompt(prompt, prompt_file.as_deref(), prompt_stdin, || {
114 let mut buf = String::new();
115 std::io::stdin().read_to_string(&mut buf)?;
116 Ok(buf)
117 })?;
118
119 cwshare_setup::prompt_cwshare_setup();
120
121 worktree::create_worktree(
122 &name,
123 base.as_deref(),
124 path.as_deref(),
125 term.as_deref(),
126 no_term,
127 resolved.as_deref(),
128 bg,
129 fg,
130 )?;
131 Ok(())
132 })(),
133
134 Some(Commands::Pr {
135 branch,
136 title,
137 body,
138 draft,
139 no_push,
140 worktree: is_worktree,
141 by_branch,
142 }) => {
143 let lookup_mode = resolve_lookup_mode(is_worktree, by_branch);
144 git_ops::create_pr_worktree(
145 branch.as_deref(),
146 !no_push,
147 title.as_deref(),
148 body.as_deref(),
149 draft,
150 lookup_mode,
151 )
152 }
153
154 Some(Commands::Merge {
155 branch,
156 interactive,
157 dry_run,
158 push,
159 ai_merge,
160 worktree: is_worktree,
161 }) => {
162 let lookup_mode = if is_worktree { Some("worktree") } else { None };
163 git_ops::merge_worktree(
164 branch.as_deref(),
165 push,
166 interactive,
167 dry_run,
168 ai_merge,
169 lookup_mode,
170 )
171 }
172
173 Some(Commands::Resume {
174 branch,
175 term,
176 bg,
177 fg,
178 worktree: is_worktree,
179 by_branch,
180 }) => {
181 let lookup_mode = resolve_lookup_mode(is_worktree, by_branch);
182 ai_tools::resume_worktree(branch.as_deref(), term.as_deref(), lookup_mode, bg, fg)
183 }
184
185 Some(Commands::Shell { worktree, args }) => {
186 let cmd = if args.is_empty() { None } else { Some(args) };
187 shell::shell_worktree(worktree.as_deref(), cmd)
188 }
189
190 Some(Commands::Delete {
191 targets,
192 interactive,
193 dry_run,
194 keep_branch,
195 delete_remote,
196 force,
197 no_force,
198 worktree: is_worktree,
199 branch: is_branch,
200 }) => {
201 let lookup_mode = resolve_lookup_mode(is_worktree, is_branch);
202 let flags = crate::operations::worktree::DeleteFlags {
203 keep_branch,
204 delete_remote,
205 git_force: !no_force,
206 allow_busy: force,
207 };
208 match crate::operations::delete_batch::delete_worktrees(
209 targets,
210 interactive,
211 dry_run,
212 flags,
213 lookup_mode,
214 ) {
215 Ok(0) => Ok(()),
216 Ok(code) => Err(crate::error::CwError::ExitCode(code)),
217 Err(e) => Err(e),
218 }
219 }
220
221 Some(Commands::Clean {
222 cache,
223 merged,
224 older_than,
225 interactive,
226 dry_run,
227 force,
228 }) => clean::clean_worktrees(
229 cache.no_cache,
230 merged,
231 older_than,
232 interactive,
233 dry_run,
234 force,
235 ),
236
237 Some(Commands::Sync {
238 branch,
239 all,
240 fetch_only,
241 ai_merge,
242 worktree: is_worktree,
243 by_branch,
244 }) => {
245 let lookup_mode = resolve_lookup_mode(is_worktree, by_branch);
246 worktree::sync_worktree(branch.as_deref(), all, fetch_only, ai_merge, lookup_mode)
247 }
248
249 Some(Commands::ChangeBase {
250 new_base,
251 branch,
252 dry_run,
253 interactive,
254 worktree: is_worktree,
255 by_branch,
256 }) => {
257 let lookup_mode = resolve_lookup_mode(is_worktree, by_branch);
258 config_ops::change_base_branch(
259 &new_base,
260 branch.as_deref(),
261 dry_run,
262 interactive,
263 lookup_mode,
264 )
265 }
266
267 Some(Commands::Backup { action }) => match action {
268 BackupAction::Create {
269 branch,
270 all,
271 output,
272 } => backup::backup_worktree(
273 branch.as_deref(),
274 all,
275 output.as_deref().map(std::path::Path::new),
276 ),
277 BackupAction::List { branch, all } => backup::list_backups(branch.as_deref(), all),
278 BackupAction::Restore { branch, path, id } => {
279 backup::restore_worktree(&branch, path.as_deref(), id.as_deref())
280 }
281 },
282
283 Some(Commands::Stash { action }) => match action {
284 StashAction::Save { message } => stash::stash_save(message.as_deref()),
285 StashAction::List => stash::stash_list(),
286 StashAction::Apply {
287 target_branch,
288 stash: stash_ref,
289 } => stash::stash_apply(&target_branch, &stash_ref),
290 },
291
292 Some(Commands::Hook { action }) => match action {
293 HookAction::Add {
294 event,
295 command,
296 id,
297 description,
298 } => hooks::add_hook(&event, &command, id.as_deref(), description.as_deref()).map(
299 |hook_id| {
300 println!("* Added hook '{}' for {}", hook_id, event);
301 },
302 ),
303 HookAction::Remove { event, hook_id } => hooks::remove_hook(&event, &hook_id),
304 HookAction::List { event } => {
305 list_hooks(event.as_deref());
306 Ok(())
307 }
308 HookAction::Enable { event, hook_id } => {
309 hooks::set_hook_enabled(&event, &hook_id, true)
310 }
311 HookAction::Disable { event, hook_id } => {
312 hooks::set_hook_enabled(&event, &hook_id, false)
313 }
314 HookAction::Run { event, dry_run } => run_hooks_manual(&event, dry_run),
315 },
316
317 Some(Commands::Export { output }) => config_ops::export_config(output.as_deref()),
318 Some(Commands::Import { import_file, apply }) => {
319 config_ops::import_config(&import_file, apply)
320 }
321
322 Some(Commands::Scan { dir }) => global_ops::global_scan(dir.as_deref()),
323 Some(Commands::Prune) => global_ops::global_prune(),
324 Some(Commands::Doctor {
325 session_start,
326 quiet,
327 }) => diagnostics::doctor(session_start, quiet),
328 Some(Commands::Guard { tool_input }) => guard::run(&tool_input),
329 Some(Commands::SetupClaude) => setup_claude::setup_claude(),
330
331 Some(Commands::Upgrade { yes }) => {
332 update::upgrade(yes);
333 Ok(())
334 }
335
336 Some(Commands::ShellSetup) => {
337 shell_setup();
338 Ok(())
339 }
340
341 Some(Commands::Path {
342 branch,
343 list_branches,
344 interactive,
345 }) => path_cmd::worktree_path(branch.as_deref(), cli.global, list_branches, interactive),
346
347 Some(Commands::ShellFunction { shell }) => match shell_functions::generate(&shell) {
348 Some(output) => {
349 print!("{}", output);
350 Ok(())
351 }
352 None => Err(CwError::Config(format!(
353 "Unsupported shell: {}. Use bash, zsh, fish, or powershell.",
354 shell
355 ))),
356 },
357
358 Some(Commands::UpdateCache) => {
359 update::refresh_cache();
360 Ok(())
361 }
362
363 Some(Commands::ConfigKeys) => {
364 for (key, _desc) in config::CONFIG_KEYS {
365 println!("{}", key);
366 }
367 Ok(())
368 }
369
370 Some(Commands::TermValues) => {
371 for v in constants::all_term_values() {
372 println!("{}", v);
373 }
374 Ok(())
375 }
376
377 Some(Commands::PresetNames) => {
378 for name in constants::PRESET_NAMES {
379 println!("{}", name);
380 }
381 Ok(())
382 }
383
384 Some(Commands::HookEvents) => {
385 for evt in constants::HOOK_EVENTS {
386 println!("{}", evt);
387 }
388 Ok(())
389 }
390
391 Some(Commands::SpawnAi { spec }) => {
392 if let Err(e) = spawn_spec::execute(&spec) {
398 eprintln!("{}", e);
399 std::process::exit(127);
400 }
401 Ok(())
402 }
403
404 None => Ok(()),
405 };
406
407 if let Err(e) = result {
408 if let CwError::ExitCode(code) = e {
413 std::process::exit(code);
414 }
415 cwconsole::print_error(&format!("Error: {}", e));
416 std::process::exit(1);
417 }
418}
419
420fn resolve_lookup_mode(is_worktree: bool, is_branch: bool) -> Option<&'static str> {
421 if is_worktree {
422 Some("worktree")
423 } else if is_branch {
424 Some("branch")
425 } else {
426 None
427 }
428}
429
430fn generate_completions(shell_name: &str) {
431 use clap::CommandFactory;
432 use clap_complete::{generate, Shell};
433
434 let shell = match shell_name.to_lowercase().as_str() {
435 "bash" => Shell::Bash,
436 "zsh" => Shell::Zsh,
437 "fish" => Shell::Fish,
438 "powershell" | "pwsh" => Shell::PowerShell,
439 "elvish" => Shell::Elvish,
440 _ => {
441 eprintln!(
442 "Unsupported shell: {}. Use bash, zsh, fish, powershell, or elvish.",
443 shell_name
444 );
445 std::process::exit(1);
446 }
447 };
448
449 let mut cmd = Cli::command();
450 generate(shell, &mut cmd, "gw", &mut std::io::stdout());
451}
452
453fn list_hooks(event: Option<&str>) {
454 let events: Vec<&str> = if let Some(e) = event {
455 vec![e]
456 } else {
457 hooks::HOOK_EVENTS.to_vec()
458 };
459
460 let mut has_any = false;
461 for evt in &events {
462 let hook_list = hooks::get_hooks(evt, None);
463 if hook_list.is_empty() && event.is_none() {
464 continue;
465 }
466 if !hook_list.is_empty() {
467 has_any = true;
468 println!("\n{}:", evt);
469 for h in &hook_list {
470 let status = if h.enabled { "enabled" } else { "disabled" };
471 let desc = if h.description.is_empty() {
472 String::new()
473 } else {
474 format!(" - {}", h.description)
475 };
476 println!(" {} [{}]: {}{}", h.id, status, h.command, desc);
477 }
478 } else {
479 println!("\n{}:", evt);
480 println!(" (no hooks)");
481 }
482 }
483
484 if event.is_none() && !has_any {
485 println!("No hooks configured. Use 'gw hook add' to add one.");
486 }
487}
488
489fn run_hooks_manual(event: &str, dry_run: bool) -> Result<()> {
490 let hook_list = hooks::get_hooks(event, None);
491 if hook_list.is_empty() {
492 println!("No hooks configured for {}", event);
493 return Ok(());
494 }
495
496 let enabled: Vec<_> = hook_list.iter().filter(|h| h.enabled).collect();
497 if enabled.is_empty() {
498 println!("All hooks for {} are disabled", event);
499 return Ok(());
500 }
501
502 if dry_run {
503 println!("Would run {} hook(s) for {}:", enabled.len(), event);
504 for h in &hook_list {
505 let status = if h.enabled {
506 "enabled"
507 } else {
508 "disabled (skipped)"
509 };
510 let desc = if h.description.is_empty() {
511 String::new()
512 } else {
513 format!(" - {}", h.description)
514 };
515 println!(" {} [{}]: {}{}", h.id, status, h.command, desc);
516 }
517 return Ok(());
518 }
519
520 let cwd = std::env::current_dir().unwrap_or_default();
521 let context = helpers::build_hook_context("", "", &cwd, &cwd, event, "manual");
522
523 hooks::run_hooks(event, &context, Some(&cwd), None)?;
524 Ok(())
525}
526
527fn shell_setup() {
528 let shell_env = std::env::var("SHELL").unwrap_or_default();
529 let is_powershell = cfg!(target_os = "windows") || std::env::var("PSModulePath").is_ok();
530
531 let home = constants::home_dir_or_fallback();
532 let (shell_name, profile_path) = if shell_env.contains("zsh") {
533 ("zsh", Some(home.join(".zshrc")))
534 } else if shell_env.contains("bash") {
535 ("bash", Some(home.join(".bashrc")))
536 } else if shell_env.contains("fish") {
537 (
538 "fish",
539 Some(home.join(".config").join("fish").join("config.fish")),
540 )
541 } else if is_powershell {
542 ("powershell", None::<std::path::PathBuf>)
543 } else {
544 println!("Could not detect your shell automatically.\n");
545 println!("Please manually add the gw-cd function to your shell:\n");
546 println!(" bash/zsh: source <(gw _shell-function bash)");
547 println!(" fish: gw _shell-function fish | source");
548 println!(" PowerShell: gw _shell-function powershell | Out-String | Invoke-Expression");
549 return;
550 };
551
552 println!("Detected shell: {}\n", shell_name);
553
554 if shell_name == "powershell" {
555 println!("To enable gw-cd in PowerShell, add the following to your $PROFILE:\n");
556 println!(" gw _shell-function powershell | Out-String | Invoke-Expression\n");
557 println!("To find your PowerShell profile location, run: $PROFILE");
558 println!(
559 "\nIf the profile file doesn't exist, create it with: New-Item -Path $PROFILE -ItemType File -Force"
560 );
561 return;
562 }
563
564 let shell_function_line = match shell_name {
565 "fish" => "gw _shell-function fish | source".to_string(),
566 _ => format!("source <(gw _shell-function {})", shell_name),
567 };
568
569 if let Some(ref path) = profile_path {
570 if path.exists() {
571 if let Ok(content) = std::fs::read_to_string(path) {
572 if content.contains("gw _shell-function") || content.contains("gw-cd") {
573 println!(
574 "{}",
575 console::style("Shell integration is already installed.").green()
576 );
577 println!(" Found in: {}\n", path.display());
578
579 refresh_shell_cache(shell_name);
580
581 println!("\nRestart your shell or run: source {}", path.display());
582 return;
583 }
584 }
585 }
586 }
587
588 println!("Setup shell integration?\n");
589 println!(
590 "This will add the following to {}:",
591 profile_path
592 .as_ref()
593 .map(|p| p.display().to_string())
594 .unwrap_or("your profile".to_string())
595 );
596
597 println!(
598 "\n # git-worktree-manager shell integration{}",
599 if matches!(shell_name, "zsh" | "bash") {
600 " (gw-cd + tab completion)"
601 } else {
602 ""
603 }
604 );
605 println!(" {}\n", shell_function_line);
606
607 print!("Add to your shell profile? [Y/n]: ");
608 use std::io::Write;
609 let _ = std::io::stdout().flush();
610
611 let mut input = String::new();
612 let _ = std::io::stdin().read_line(&mut input);
613 let input = input.trim().to_lowercase();
614
615 if !input.is_empty() && input != "y" && input != "yes" {
616 println!("\nSetup cancelled.");
617 return;
618 }
619
620 let Some(ref path) = profile_path else {
621 return;
622 };
623
624 if let Some(parent) = path.parent() {
625 let _ = std::fs::create_dir_all(parent);
626 }
627
628 let comment_suffix = if matches!(shell_name, "zsh" | "bash") {
629 " (gw-cd + tab completion)"
630 } else {
631 ""
632 };
633 let append = format!(
634 "\n# git-worktree-manager shell integration{}\n{}\n",
635 comment_suffix, shell_function_line
636 );
637
638 match std::fs::OpenOptions::new()
639 .create(true)
640 .append(true)
641 .open(path)
642 {
643 Ok(mut f) => {
644 let _ = f.write_all(append.as_bytes());
645
646 if let Ok(mut cfg) = config::load_config() {
647 cfg.shell_completion.installed = true;
648 cfg.shell_completion.prompted = true;
649 let _ = config::save_config(&cfg);
650 }
651
652 println!("\n* Successfully added to {}", path.display());
653
654 refresh_shell_cache(shell_name);
655
656 println!("\nNext steps:");
657 println!(" 1. Restart your shell or run: source {}", path.display());
658 println!(" 2. Try directory navigation: gw-cd <branch-name>");
659 println!(" 3. Try tab completion: gw <TAB> or gw new <TAB>");
660 }
661 Err(e) => {
662 println!("\nError: Failed to update {}: {}", path.display(), e);
663 println!("\nTo install manually, add the lines shown above to your profile");
664 }
665 }
666}
667
668fn refresh_shell_cache(shell_name: &str) {
670 let home = constants::home_dir_or_fallback();
671
672 let cache_paths = [
673 home.join(".cache").join("gw-shell-function.zsh"),
674 home.join(".cache").join("gw-shell-function.bash"),
675 home.join(".cache").join("gw-shell-function.fish"),
676 ];
677
678 let mut refreshed = false;
679 for cache_path in &cache_paths {
680 if !cache_path.exists() {
681 continue;
682 }
683 let cache_shell = cache_path
684 .extension()
685 .and_then(|e| e.to_str())
686 .unwrap_or("");
687 if let Some(content) = shell_functions::generate(cache_shell) {
688 if std::fs::write(cache_path, content).is_ok() {
689 println!(
690 " {} {}",
691 console::style("Refreshed cache:").dim(),
692 cache_path.display()
693 );
694 refreshed = true;
695 }
696 }
697 }
698
699 if refreshed {
700 return;
701 }
702
703 let cache_path = home
704 .join(".cache")
705 .join(format!("gw-shell-function.{}", shell_name));
706 if let Some(content) = shell_functions::generate(shell_name) {
707 let _ = std::fs::create_dir_all(cache_path.parent().unwrap_or(&home));
708 if std::fs::write(&cache_path, &content).is_ok() {
709 println!(
710 " {} {}",
711 console::style("Created cache:").dim(),
712 cache_path.display()
713 );
714 }
715 }
716}