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 merged,
223 older_than,
224 dry_run,
225 force,
226 interactive,
227 }) => match clean::clean_worktrees(merged, older_than, dry_run, force, interactive) {
228 Ok(0) => Ok(()),
229 Ok(code) => Err(crate::error::CwError::ExitCode(code)),
230 Err(e) => Err(e),
231 },
232
233 Some(Commands::Sync {
234 branch,
235 all,
236 fetch_only,
237 ai_merge,
238 worktree: is_worktree,
239 by_branch,
240 }) => {
241 let lookup_mode = resolve_lookup_mode(is_worktree, by_branch);
242 worktree::sync_worktree(branch.as_deref(), all, fetch_only, ai_merge, lookup_mode)
243 }
244
245 Some(Commands::ChangeBase {
246 new_base,
247 branch,
248 dry_run,
249 interactive,
250 worktree: is_worktree,
251 by_branch,
252 }) => {
253 let lookup_mode = resolve_lookup_mode(is_worktree, by_branch);
254 config_ops::change_base_branch(
255 &new_base,
256 branch.as_deref(),
257 dry_run,
258 interactive,
259 lookup_mode,
260 )
261 }
262
263 Some(Commands::Backup { action }) => match action {
264 BackupAction::Create {
265 branch,
266 all,
267 output,
268 } => backup::backup_worktree(
269 branch.as_deref(),
270 all,
271 output.as_deref().map(std::path::Path::new),
272 ),
273 BackupAction::List { branch, all } => backup::list_backups(branch.as_deref(), all),
274 BackupAction::Restore { branch, path, id } => {
275 backup::restore_worktree(&branch, path.as_deref(), id.as_deref())
276 }
277 },
278
279 Some(Commands::Stash { action }) => match action {
280 StashAction::Save { message } => stash::stash_save(message.as_deref()),
281 StashAction::List => stash::stash_list(),
282 StashAction::Apply {
283 target_branch,
284 stash: stash_ref,
285 } => stash::stash_apply(&target_branch, &stash_ref),
286 },
287
288 Some(Commands::Hook { action }) => match action {
289 HookAction::Add {
290 event,
291 command,
292 id,
293 description,
294 } => hooks::add_hook(&event, &command, id.as_deref(), description.as_deref()).map(
295 |hook_id| {
296 println!("* Added hook '{}' for {}", hook_id, event);
297 },
298 ),
299 HookAction::Remove { event, hook_id } => hooks::remove_hook(&event, &hook_id),
300 HookAction::List { event } => {
301 list_hooks(event.as_deref());
302 Ok(())
303 }
304 HookAction::Enable { event, hook_id } => {
305 hooks::set_hook_enabled(&event, &hook_id, true)
306 }
307 HookAction::Disable { event, hook_id } => {
308 hooks::set_hook_enabled(&event, &hook_id, false)
309 }
310 HookAction::Run { event, dry_run } => run_hooks_manual(&event, dry_run),
311 },
312
313 Some(Commands::Export { output }) => config_ops::export_config(output.as_deref()),
314 Some(Commands::Import { import_file, apply }) => {
315 config_ops::import_config(&import_file, apply)
316 }
317
318 Some(Commands::Scan { dir }) => global_ops::global_scan(dir.as_deref()),
319 Some(Commands::Prune) => global_ops::global_prune(),
320 Some(Commands::Doctor {
321 session_start,
322 quiet,
323 }) => diagnostics::doctor(session_start, quiet),
324 Some(Commands::Guard { tool_input }) => guard::run(&tool_input),
325 Some(Commands::SetupClaude) => setup_claude::setup_claude(),
326
327 Some(Commands::Upgrade { yes }) => {
328 update::upgrade(yes);
329 Ok(())
330 }
331
332 Some(Commands::ShellSetup) => {
333 shell_setup();
334 Ok(())
335 }
336
337 Some(Commands::Path {
338 branch,
339 list_branches,
340 interactive,
341 }) => path_cmd::worktree_path(branch.as_deref(), cli.global, list_branches, interactive),
342
343 Some(Commands::ShellFunction { shell }) => match shell_functions::generate(&shell) {
344 Some(output) => {
345 print!("{}", output);
346 Ok(())
347 }
348 None => Err(CwError::Config(format!(
349 "Unsupported shell: {}. Use bash, zsh, fish, or powershell.",
350 shell
351 ))),
352 },
353
354 Some(Commands::UpdateCache) => {
355 update::refresh_cache();
356 Ok(())
357 }
358
359 Some(Commands::ConfigKeys) => {
360 for (key, _desc) in config::CONFIG_KEYS {
361 println!("{}", key);
362 }
363 Ok(())
364 }
365
366 Some(Commands::TermValues) => {
367 for v in constants::all_term_values() {
368 println!("{}", v);
369 }
370 Ok(())
371 }
372
373 Some(Commands::PresetNames) => {
374 for name in constants::PRESET_NAMES {
375 println!("{}", name);
376 }
377 Ok(())
378 }
379
380 Some(Commands::HookEvents) => {
381 for evt in constants::HOOK_EVENTS {
382 println!("{}", evt);
383 }
384 Ok(())
385 }
386
387 Some(Commands::SpawnAi { spec }) => {
388 let resolved = match spec {
394 Some(p) => p,
395 None => match spawn_spec::resolve_last_for_cwd() {
396 Ok(p) => p,
397 Err(e) => {
398 eprintln!("{}", e);
399 std::process::exit(127);
400 }
401 },
402 };
403 if let Err(e) = spawn_spec::execute(&resolved) {
404 eprintln!("{}", e);
405 std::process::exit(127);
406 }
407 Ok(())
408 }
409
410 None => Ok(()),
411 };
412
413 if let Err(e) = result {
414 if let CwError::ExitCode(code) = e {
419 std::process::exit(code);
420 }
421 cwconsole::print_error(&format!("Error: {}", e));
422 std::process::exit(1);
423 }
424}
425
426fn resolve_lookup_mode(is_worktree: bool, is_branch: bool) -> Option<&'static str> {
427 if is_worktree {
428 Some("worktree")
429 } else if is_branch {
430 Some("branch")
431 } else {
432 None
433 }
434}
435
436fn generate_completions(shell_name: &str) {
437 use clap::CommandFactory;
438 use clap_complete::{generate, Shell};
439
440 let shell = match shell_name.to_lowercase().as_str() {
441 "bash" => Shell::Bash,
442 "zsh" => Shell::Zsh,
443 "fish" => Shell::Fish,
444 "powershell" | "pwsh" => Shell::PowerShell,
445 "elvish" => Shell::Elvish,
446 _ => {
447 eprintln!(
448 "Unsupported shell: {}. Use bash, zsh, fish, powershell, or elvish.",
449 shell_name
450 );
451 std::process::exit(1);
452 }
453 };
454
455 let mut cmd = Cli::command();
456 generate(shell, &mut cmd, "gw", &mut std::io::stdout());
457}
458
459fn list_hooks(event: Option<&str>) {
460 let events: Vec<&str> = if let Some(e) = event {
461 vec![e]
462 } else {
463 hooks::HOOK_EVENTS.to_vec()
464 };
465
466 let mut has_any = false;
467 for evt in &events {
468 let hook_list = hooks::get_hooks(evt, None);
469 if hook_list.is_empty() && event.is_none() {
470 continue;
471 }
472 if !hook_list.is_empty() {
473 has_any = true;
474 println!("\n{}:", evt);
475 for h in &hook_list {
476 let status = if h.enabled { "enabled" } else { "disabled" };
477 let desc = if h.description.is_empty() {
478 String::new()
479 } else {
480 format!(" - {}", h.description)
481 };
482 println!(" {} [{}]: {}{}", h.id, status, h.command, desc);
483 }
484 } else {
485 println!("\n{}:", evt);
486 println!(" (no hooks)");
487 }
488 }
489
490 if event.is_none() && !has_any {
491 println!("No hooks configured. Use 'gw hook add' to add one.");
492 }
493}
494
495fn run_hooks_manual(event: &str, dry_run: bool) -> Result<()> {
496 let hook_list = hooks::get_hooks(event, None);
497 if hook_list.is_empty() {
498 println!("No hooks configured for {}", event);
499 return Ok(());
500 }
501
502 let enabled: Vec<_> = hook_list.iter().filter(|h| h.enabled).collect();
503 if enabled.is_empty() {
504 println!("All hooks for {} are disabled", event);
505 return Ok(());
506 }
507
508 if dry_run {
509 println!("Would run {} hook(s) for {}:", enabled.len(), event);
510 for h in &hook_list {
511 let status = if h.enabled {
512 "enabled"
513 } else {
514 "disabled (skipped)"
515 };
516 let desc = if h.description.is_empty() {
517 String::new()
518 } else {
519 format!(" - {}", h.description)
520 };
521 println!(" {} [{}]: {}{}", h.id, status, h.command, desc);
522 }
523 return Ok(());
524 }
525
526 let cwd = std::env::current_dir().unwrap_or_default();
527 let context = helpers::build_hook_context("", "", &cwd, &cwd, event, "manual");
528
529 hooks::run_hooks(event, &context, Some(&cwd), None)?;
530 Ok(())
531}
532
533fn shell_setup() {
534 let shell_env = std::env::var("SHELL").unwrap_or_default();
535 let is_powershell = cfg!(target_os = "windows") || std::env::var("PSModulePath").is_ok();
536
537 let home = constants::home_dir_or_fallback();
538 let (shell_name, profile_path) = if shell_env.contains("zsh") {
539 ("zsh", Some(home.join(".zshrc")))
540 } else if shell_env.contains("bash") {
541 ("bash", Some(home.join(".bashrc")))
542 } else if shell_env.contains("fish") {
543 (
544 "fish",
545 Some(home.join(".config").join("fish").join("config.fish")),
546 )
547 } else if is_powershell {
548 ("powershell", None::<std::path::PathBuf>)
549 } else {
550 println!("Could not detect your shell automatically.\n");
551 println!("Please manually add the gw-cd function to your shell:\n");
552 println!(" bash/zsh: source <(gw _shell-function bash)");
553 println!(" fish: gw _shell-function fish | source");
554 println!(" PowerShell: gw _shell-function powershell | Out-String | Invoke-Expression");
555 return;
556 };
557
558 println!("Detected shell: {}\n", shell_name);
559
560 if shell_name == "powershell" {
561 println!("To enable gw-cd in PowerShell, add the following to your $PROFILE:\n");
562 println!(" gw _shell-function powershell | Out-String | Invoke-Expression\n");
563 println!("To find your PowerShell profile location, run: $PROFILE");
564 println!(
565 "\nIf the profile file doesn't exist, create it with: New-Item -Path $PROFILE -ItemType File -Force"
566 );
567 return;
568 }
569
570 let shell_function_line = match shell_name {
571 "fish" => "gw _shell-function fish | source".to_string(),
572 _ => format!("source <(gw _shell-function {})", shell_name),
573 };
574
575 if let Some(ref path) = profile_path {
576 if path.exists() {
577 if let Ok(content) = std::fs::read_to_string(path) {
578 if content.contains("gw _shell-function") || content.contains("gw-cd") {
579 println!(
580 "{}",
581 console::style("Shell integration is already installed.").green()
582 );
583 println!(" Found in: {}\n", path.display());
584
585 refresh_shell_cache(shell_name);
586
587 println!("\nRestart your shell or run: source {}", path.display());
588 return;
589 }
590 }
591 }
592 }
593
594 println!("Setup shell integration?\n");
595 println!(
596 "This will add the following to {}:",
597 profile_path
598 .as_ref()
599 .map(|p| p.display().to_string())
600 .unwrap_or("your profile".to_string())
601 );
602
603 println!(
604 "\n # git-worktree-manager shell integration{}",
605 if matches!(shell_name, "zsh" | "bash") {
606 " (gw-cd + tab completion)"
607 } else {
608 ""
609 }
610 );
611 println!(" {}\n", shell_function_line);
612
613 print!("Add to your shell profile? [Y/n]: ");
614 use std::io::Write;
615 let _ = std::io::stdout().flush();
616
617 let mut input = String::new();
618 let _ = std::io::stdin().read_line(&mut input);
619 let input = input.trim().to_lowercase();
620
621 if !input.is_empty() && input != "y" && input != "yes" {
622 println!("\nSetup cancelled.");
623 return;
624 }
625
626 let Some(ref path) = profile_path else {
627 return;
628 };
629
630 if let Some(parent) = path.parent() {
631 let _ = std::fs::create_dir_all(parent);
632 }
633
634 let comment_suffix = if matches!(shell_name, "zsh" | "bash") {
635 " (gw-cd + tab completion)"
636 } else {
637 ""
638 };
639 let append = format!(
640 "\n# git-worktree-manager shell integration{}\n{}\n",
641 comment_suffix, shell_function_line
642 );
643
644 match std::fs::OpenOptions::new()
645 .create(true)
646 .append(true)
647 .open(path)
648 {
649 Ok(mut f) => {
650 let _ = f.write_all(append.as_bytes());
651
652 if let Ok(mut cfg) = config::load_config() {
653 cfg.shell_completion.installed = true;
654 cfg.shell_completion.prompted = true;
655 let _ = config::save_config(&cfg);
656 }
657
658 println!("\n* Successfully added to {}", path.display());
659
660 refresh_shell_cache(shell_name);
661
662 println!("\nNext steps:");
663 println!(" 1. Restart your shell or run: source {}", path.display());
664 println!(" 2. Try directory navigation: gw-cd <branch-name>");
665 println!(" 3. Try tab completion: gw <TAB> or gw new <TAB>");
666 }
667 Err(e) => {
668 println!("\nError: Failed to update {}: {}", path.display(), e);
669 println!("\nTo install manually, add the lines shown above to your profile");
670 }
671 }
672}
673
674fn refresh_shell_cache(shell_name: &str) {
676 let home = constants::home_dir_or_fallback();
677
678 let cache_paths = [
679 home.join(".cache").join("gw-shell-function.zsh"),
680 home.join(".cache").join("gw-shell-function.bash"),
681 home.join(".cache").join("gw-shell-function.fish"),
682 ];
683
684 let mut refreshed = false;
685 for cache_path in &cache_paths {
686 if !cache_path.exists() {
687 continue;
688 }
689 let cache_shell = cache_path
690 .extension()
691 .and_then(|e| e.to_str())
692 .unwrap_or("");
693 if let Some(content) = shell_functions::generate(cache_shell) {
694 if std::fs::write(cache_path, content).is_ok() {
695 println!(
696 " {} {}",
697 console::style("Refreshed cache:").dim(),
698 cache_path.display()
699 );
700 refreshed = true;
701 }
702 }
703 }
704
705 if refreshed {
706 return;
707 }
708
709 let cache_path = home
710 .join(".cache")
711 .join(format!("gw-shell-function.{}", shell_name));
712 if let Some(content) = shell_functions::generate(shell_name) {
713 let _ = std::fs::create_dir_all(cache_path.parent().unwrap_or(&home));
714 if std::fs::write(&cache_path, &content).is_ok() {
715 println!(
716 " {} {}",
717 console::style("Created cache:").dim(),
718 cache_path.display()
719 );
720 }
721 }
722}