1use clap::Parser;
8
9use crate::cli::{Cli, Commands};
10use crate::config;
11use crate::console as cwconsole;
12use crate::constants;
13use crate::cwshare_setup;
14use crate::error::{CwError, Result};
15use crate::operations::ai_tools::LaunchOptions;
16use crate::operations::{
17 ai_tools, diagnostics, display, exec, guard, helpers, path_cmd, run, setup_claude, spawn_spec,
18 worktree,
19};
20use crate::resolve_prompt;
21use crate::shell_functions;
22use crate::tui;
23use crate::update;
24use std::io::{IsTerminal, Read};
25
26pub fn run() {
27 tui::install_panic_hook();
28 let cli = Cli::parse();
29
30 if let Some(ref shell_name) = cli.generate_completion {
31 generate_completions(shell_name);
32 return;
33 }
34
35 let is_internal = matches!(
40 &cli.command,
41 Some(
42 Commands::UpdateCache
43 | Commands::CompleteTargets
44 | Commands::Path { .. }
45 | Commands::ShellFunction { .. }
46 | Commands::SpawnAi { .. }
47 | Commands::Guard { .. }
48 )
49 );
50
51 if !is_internal {
52 crate::operations::spawn_spec::sweep_stale();
53 update::check_for_update_if_needed();
54 }
55
56 if !is_internal {
57 config::prompt_shell_completion_setup();
58 }
59
60 let result = match cli.command {
61 Some(Commands::List) => display::list_worktrees(),
62 Some(Commands::Ls) => display::list_worktrees_tsv(),
63 Some(Commands::New {
64 name,
65 path,
66 base,
67 term,
68 prompt,
69 prompt_file,
70 no_env_forward,
71 forward_args,
72 }) => (|| -> Result<()> {
73 if (prompt.is_some() || prompt_file.is_some()) && !forward_args.is_empty() {
78 return Err(CwError::Other(
79 "--prompt / --prompt-file cannot be combined with trailing AI tool args; \
80 pick one or the other"
81 .to_string(),
82 ));
83 }
84 let resolved = resolve_prompt(
88 prompt,
89 prompt_file.as_deref(),
90 || std::io::stdin().is_terminal(),
91 || {
92 let mut buf = String::new();
93 std::io::stdin().read_to_string(&mut buf)?;
94 Ok(buf)
95 },
96 )?;
97 let _ = config::parse_term_option(term.as_deref())?;
103 cwshare_setup::prompt_cwshare_setup();
104
105 let opts = LaunchOptions {
106 term_override: term.as_deref(),
107 forward_args: &forward_args,
108 no_env_forward,
109 };
110 worktree::create_worktree(
111 &name,
112 base.as_deref(),
113 path.as_deref(),
114 resolved.as_deref(),
115 &opts,
116 )?;
117 Ok(())
118 })(),
119
120 Some(Commands::Resume {
121 branch,
122 term,
123 no_env_forward,
124 forward_args,
125 }) => {
126 let (branch, forward_args) = lift_dash_target(branch, forward_args);
131 let opts = LaunchOptions {
132 term_override: term.as_deref(),
133 forward_args: &forward_args,
134 no_env_forward,
135 };
136 ai_tools::resume_worktree(branch.as_deref(), &opts)
137 }
138
139 Some(Commands::Spawn {
140 target,
141 term,
142 prompt,
143 prompt_file,
144 no_env_forward,
145 forward_args,
146 }) => (|| -> Result<()> {
147 let (target, forward_args) = lift_dash_target(target, forward_args);
149 if (prompt.is_some() || prompt_file.is_some()) && !forward_args.is_empty() {
151 return Err(CwError::Other(
152 "--prompt / --prompt-file cannot be combined with trailing AI tool args; \
153 pick one or the other"
154 .to_string(),
155 ));
156 }
157 let resolved_prompt = resolve_prompt(
158 prompt,
159 prompt_file.as_deref(),
160 || std::io::stdin().is_terminal(),
161 || {
162 let mut buf = String::new();
163 std::io::stdin().read_to_string(&mut buf)?;
164 Ok(buf)
165 },
166 )?;
167 let cwd = std::env::current_dir()?;
168 let target_path = match target {
169 Some(t) => {
170 let main_repo = crate::git::get_main_repo_root(Some(&cwd))?;
171 helpers::resolve_target_strict(&main_repo, &t)?.path
172 }
173 None => crate::git::get_repo_root(Some(&cwd))?,
174 };
175 let opts = LaunchOptions {
176 term_override: term.as_deref(),
177 forward_args: &forward_args,
178 no_env_forward,
179 };
180 ai_tools::spawn_in_worktree(&target_path, resolved_prompt.as_deref(), &opts)
181 })(),
182
183 Some(Commands::Rm {
184 targets,
185 interactive,
186 dry_run,
187 keep_branch,
188 delete_remote,
189 force,
190 no_force,
191 }) => {
192 let flags = crate::operations::worktree::RmFlags {
193 keep_branch,
194 delete_remote,
195 git_force: !no_force,
196 allow_busy: force,
197 };
198 match crate::operations::rm_batch::rm_worktrees(targets, interactive, dry_run, flags) {
199 Ok(0) => Ok(()),
200 Ok(code) => Err(crate::error::CwError::ExitCode(code)),
201 Err(e) => Err(e),
202 }
203 }
204
205 Some(Commands::Doctor {
206 session_start,
207 quiet,
208 }) => diagnostics::doctor(session_start, quiet),
209 Some(Commands::Run {
210 only,
211 no_main,
212 jobs,
213 continue_on_error,
214 cmd,
215 }) => (|| -> Result<()> {
216 let cwd = std::env::current_dir()?;
217 let code = run::run_in_scope(
218 &cwd,
219 &cmd,
220 only.as_deref(),
221 no_main,
222 jobs,
223 continue_on_error,
224 )?;
225 if code != 0 {
226 return Err(crate::error::CwError::ExitCode(code));
227 }
228 Ok(())
229 })(),
230
231 Some(Commands::Exec { target, cmd }) => (|| -> Result<()> {
232 let cwd = std::env::current_dir()?;
233 let mut out = std::io::stdout().lock();
234 let code = exec::exec_in_target(&cwd, &target, &cmd, &mut out)?;
235 if code != 0 {
236 return Err(crate::error::CwError::ExitCode(code));
237 }
238 Ok(())
239 })(),
240
241 Some(Commands::Guard { tool_input }) => guard::run(&tool_input),
242 Some(Commands::SetupClaude) => setup_claude::setup_claude(),
243
244 Some(Commands::Upgrade { yes }) => {
245 update::upgrade(yes);
246 Ok(())
247 }
248
249 Some(Commands::ShellSetup) => {
250 shell_setup();
251 Ok(())
252 }
253
254 Some(Commands::Path {
255 branch,
256 list_branches,
257 interactive,
258 }) => path_cmd::worktree_path(branch.as_deref(), list_branches, interactive),
259
260 Some(Commands::ShellFunction { shell }) => match shell_functions::generate(&shell) {
261 Some(output) => {
262 print!("{}", output);
263 Ok(())
264 }
265 None => Err(CwError::Config(format!(
266 "Unsupported shell: {}. Use bash, zsh, fish, or powershell.",
267 shell
268 ))),
269 },
270
271 Some(Commands::UpdateCache) => {
272 update::refresh_cache();
273 Ok(())
274 }
275
276 Some(Commands::CompleteTargets) => crate::operations::complete::print_completion_targets(),
277
278 Some(Commands::SpawnAi { spec }) => {
279 let resolved = match spec {
285 Some(p) => p,
286 None => match spawn_spec::resolve_last_for_cwd() {
287 Ok(p) => p,
288 Err(e) => {
289 eprintln!("{}", e);
290 std::process::exit(127);
291 }
292 },
293 };
294 if let Err(e) = spawn_spec::execute(&resolved) {
295 eprintln!("{}", e);
296 std::process::exit(127);
297 }
298 Ok(())
299 }
300
301 None => Ok(()),
302 };
303
304 if let Err(e) = result {
305 if let CwError::ExitCode(code) = e {
310 std::process::exit(code);
311 }
312 cwconsole::print_error(&format!("Error: {}", e));
313 std::process::exit(1);
314 }
315}
316
317fn generate_completions(shell_name: &str) {
318 use clap::CommandFactory;
319 use clap_complete::{generate, Shell};
320
321 let shell = match shell_name.to_lowercase().as_str() {
322 "bash" => Shell::Bash,
323 "zsh" => Shell::Zsh,
324 "fish" => Shell::Fish,
325 "powershell" | "pwsh" => Shell::PowerShell,
326 "elvish" => Shell::Elvish,
327 _ => {
328 eprintln!(
329 "Unsupported shell: {}. Use bash, zsh, fish, powershell, or elvish.",
330 shell_name
331 );
332 std::process::exit(1);
333 }
334 };
335
336 let mut cmd = Cli::command();
337 generate(shell, &mut cmd, "gw", &mut std::io::stdout());
338}
339
340fn shell_setup() {
341 let shell_env = std::env::var("SHELL").unwrap_or_default();
342 let is_powershell = cfg!(target_os = "windows") || std::env::var("PSModulePath").is_ok();
343
344 let home = constants::home_dir_or_fallback();
345 let (shell_name, profile_path) = if shell_env.contains("zsh") {
346 ("zsh", Some(home.join(".zshrc")))
347 } else if shell_env.contains("bash") {
348 ("bash", Some(home.join(".bashrc")))
349 } else if shell_env.contains("fish") {
350 (
351 "fish",
352 Some(home.join(".config").join("fish").join("config.fish")),
353 )
354 } else if is_powershell {
355 ("powershell", None::<std::path::PathBuf>)
356 } else {
357 println!("Could not detect your shell automatically.\n");
358 println!("Please manually add the gw-cd function to your shell:\n");
359 println!(" bash/zsh: source <(gw _shell-function bash)");
360 println!(" fish: gw _shell-function fish | source");
361 println!(" PowerShell: gw _shell-function powershell | Out-String | Invoke-Expression");
362 return;
363 };
364
365 println!("Detected shell: {}\n", shell_name);
366
367 if shell_name == "powershell" {
368 println!("To enable gw-cd in PowerShell, add the following to your $PROFILE:\n");
369 println!(" gw _shell-function powershell | Out-String | Invoke-Expression\n");
370 println!("To find your PowerShell profile location, run: $PROFILE");
371 println!(
372 "\nIf the profile file doesn't exist, create it with: New-Item -Path $PROFILE -ItemType File -Force"
373 );
374 return;
375 }
376
377 let shell_function_line = match shell_name {
378 "fish" => "gw _shell-function fish | source".to_string(),
379 _ => format!("source <(gw _shell-function {})", shell_name),
380 };
381
382 if let Some(ref path) = profile_path {
383 if path.exists() {
384 if let Ok(content) = std::fs::read_to_string(path) {
385 if content.contains("gw _shell-function") || content.contains("gw-cd") {
386 println!(
387 "{}",
388 console::style("Shell integration is already installed.").green()
389 );
390 println!(" Found in: {}\n", path.display());
391
392 refresh_shell_cache(shell_name);
393
394 println!("\nRestart your shell or run: source {}", path.display());
395 return;
396 }
397 }
398 }
399 }
400
401 println!("Setup shell integration?\n");
402 println!(
403 "This will add the following to {}:",
404 profile_path
405 .as_ref()
406 .map(|p| p.display().to_string())
407 .unwrap_or("your profile".to_string())
408 );
409
410 println!(
411 "\n # git-worktree-manager shell integration{}",
412 if matches!(shell_name, "zsh" | "bash") {
413 " (gw-cd + tab completion)"
414 } else {
415 ""
416 }
417 );
418 println!(" {}\n", shell_function_line);
419
420 print!("Add to your shell profile? [Y/n]: ");
421 use std::io::Write;
422 let _ = std::io::stdout().flush();
423
424 let mut input = String::new();
425 let _ = std::io::stdin().read_line(&mut input);
426 let input = input.trim().to_lowercase();
427
428 if !input.is_empty() && input != "y" && input != "yes" {
429 println!("\nSetup cancelled.");
430 return;
431 }
432
433 let Some(ref path) = profile_path else {
434 return;
435 };
436
437 if let Some(parent) = path.parent() {
438 let _ = std::fs::create_dir_all(parent);
439 }
440
441 let comment_suffix = if matches!(shell_name, "zsh" | "bash") {
442 " (gw-cd + tab completion)"
443 } else {
444 ""
445 };
446 let append = format!(
447 "\n# git-worktree-manager shell integration{}\n{}\n",
448 comment_suffix, shell_function_line
449 );
450
451 match std::fs::OpenOptions::new()
452 .create(true)
453 .append(true)
454 .open(path)
455 {
456 Ok(mut f) => {
457 let _ = f.write_all(append.as_bytes());
458
459 if let Ok(mut cfg) = config::load_config() {
460 cfg.shell_completion.installed = true;
461 cfg.shell_completion.prompted = true;
462 let _ = config::save_config(&cfg);
463 }
464
465 println!("\n* Successfully added to {}", path.display());
466
467 refresh_shell_cache(shell_name);
468
469 println!("\nNext steps:");
470 println!(" 1. Restart your shell or run: source {}", path.display());
471 println!(" 2. Try directory navigation: gw-cd <branch-name>");
472 println!(" 3. Try tab completion: gw <TAB> or gw new <TAB>");
473 }
474 Err(e) => {
475 println!("\nError: Failed to update {}: {}", path.display(), e);
476 println!("\nTo install manually, add the lines shown above to your profile");
477 }
478 }
479}
480
481fn refresh_shell_cache(shell_name: &str) {
483 let home = constants::home_dir_or_fallback();
484
485 let cache_paths = [
486 home.join(".cache").join("gw-shell-function.zsh"),
487 home.join(".cache").join("gw-shell-function.bash"),
488 home.join(".cache").join("gw-shell-function.fish"),
489 ];
490
491 let mut refreshed = false;
492 for cache_path in &cache_paths {
493 if !cache_path.exists() {
494 continue;
495 }
496 let cache_shell = cache_path
497 .extension()
498 .and_then(|e| e.to_str())
499 .unwrap_or("");
500 if let Some(content) = shell_functions::generate(cache_shell) {
501 if std::fs::write(cache_path, content).is_ok() {
502 println!(
503 " {} {}",
504 console::style("Refreshed cache:").dim(),
505 cache_path.display()
506 );
507 refreshed = true;
508 }
509 }
510 }
511
512 if refreshed {
513 return;
514 }
515
516 let cache_path = home
517 .join(".cache")
518 .join(format!("gw-shell-function.{}", shell_name));
519 if let Some(content) = shell_functions::generate(shell_name) {
520 let _ = std::fs::create_dir_all(cache_path.parent().unwrap_or(&home));
521 if std::fs::write(&cache_path, &content).is_ok() {
522 println!(
523 " {} {}",
524 console::style("Created cache:").dim(),
525 cache_path.display()
526 );
527 }
528 }
529}
530
531fn lift_dash_target(
540 target: Option<String>,
541 forward_args: Vec<String>,
542) -> (Option<String>, Vec<String>) {
543 match target {
544 Some(t) if t.starts_with('-') => {
545 let mut lifted = Vec::with_capacity(forward_args.len() + 1);
546 lifted.push(t);
547 lifted.extend(forward_args);
548 (None, lifted)
549 }
550 other => (other, forward_args),
551 }
552}
553
554#[cfg(test)]
555mod tests {
556 use super::lift_dash_target;
557
558 #[test]
559 fn lift_dash_target_lifts_hyphen_target() {
560 let (target, fwd) = lift_dash_target(
561 Some("--model".to_string()),
562 vec!["opus".to_string(), "--resume".to_string()],
563 );
564 assert_eq!(target, None);
565 assert_eq!(fwd, vec!["--model", "opus", "--resume"]);
566 }
567
568 #[test]
569 fn lift_dash_target_passes_through_normal_target() {
570 let (target, fwd) = lift_dash_target(
571 Some("feat-x".to_string()),
572 vec!["--model".to_string(), "opus".to_string()],
573 );
574 assert_eq!(target.as_deref(), Some("feat-x"));
575 assert_eq!(fwd, vec!["--model", "opus"]);
576 }
577
578 #[test]
579 fn lift_dash_target_handles_none_target() {
580 let (target, fwd) = lift_dash_target(None, vec![]);
581 assert_eq!(target, None);
582 assert!(fwd.is_empty());
583 }
584
585 #[test]
586 fn lift_dash_target_lifts_with_no_forward_args() {
587 let (target, fwd) = lift_dash_target(Some("--model".to_string()), vec![]);
589 assert_eq!(target, None);
590 assert_eq!(fwd, vec!["--model"]);
591 }
592}