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