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