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