1use clap::Parser;
9
10use crate::cli::{Cli, Commands};
11use crate::config;
12use crate::console as cwconsole;
13use crate::constants;
14use crate::cwshare_setup;
15use crate::error::{CwError, Result};
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::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 no_term,
68 term,
69 prompt,
70 prompt_file,
71 prompt_stdin,
72 }) => (|| -> Result<()> {
73 let resolved = resolve_prompt(prompt, prompt_file.as_deref(), prompt_stdin, || {
77 let mut buf = String::new();
78 std::io::stdin().read_to_string(&mut buf)?;
79 Ok(buf)
80 })?;
81 if !no_term {
86 let _ = config::parse_term_option(term.as_deref())?;
87 }
88 cwshare_setup::prompt_cwshare_setup();
89
90 worktree::create_worktree(
91 &name,
92 base.as_deref(),
93 path.as_deref(),
94 no_term,
95 resolved.as_deref(),
96 term.as_deref(),
97 )?;
98 Ok(())
99 })(),
100
101 Some(Commands::Resume { branch, term }) => {
102 ai_tools::resume_worktree(branch.as_deref(), term.as_deref())
103 }
104
105 Some(Commands::Spawn {
106 target,
107 term,
108 prompt,
109 prompt_file,
110 prompt_stdin,
111 }) => (|| -> Result<()> {
112 let resolved_prompt =
113 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 let cwd = std::env::current_dir()?;
119 let target_path = match target {
120 Some(t) => {
121 let main_repo = crate::git::get_main_repo_root(Some(&cwd))?;
122 helpers::resolve_target_strict(&main_repo, &t)?.path
123 }
124 None => crate::git::get_repo_root(Some(&cwd))?,
125 };
126 ai_tools::spawn_in_worktree(&target_path, resolved_prompt.as_deref(), term.as_deref())
127 })(),
128
129 Some(Commands::Rm {
130 targets,
131 interactive,
132 dry_run,
133 keep_branch,
134 delete_remote,
135 force,
136 no_force,
137 }) => {
138 let flags = crate::operations::worktree::RmFlags {
139 keep_branch,
140 delete_remote,
141 git_force: !no_force,
142 allow_busy: force,
143 };
144 match crate::operations::rm_batch::rm_worktrees(targets, interactive, dry_run, flags) {
145 Ok(0) => Ok(()),
146 Ok(code) => Err(crate::error::CwError::ExitCode(code)),
147 Err(e) => Err(e),
148 }
149 }
150
151 Some(Commands::Doctor {
152 session_start,
153 quiet,
154 }) => diagnostics::doctor(session_start, quiet),
155 Some(Commands::Run {
156 only,
157 no_main,
158 jobs,
159 continue_on_error,
160 cmd,
161 }) => (|| -> Result<()> {
162 let cwd = std::env::current_dir()?;
163 let code = run::run_in_scope(
164 &cwd,
165 &cmd,
166 only.as_deref(),
167 no_main,
168 jobs,
169 continue_on_error,
170 )?;
171 if code != 0 {
172 return Err(crate::error::CwError::ExitCode(code));
173 }
174 Ok(())
175 })(),
176
177 Some(Commands::Exec { target, cmd }) => (|| -> Result<()> {
178 let cwd = std::env::current_dir()?;
179 let mut out = std::io::stdout().lock();
180 let code = exec::exec_in_target(&cwd, &target, &cmd, &mut out)?;
181 if code != 0 {
182 return Err(crate::error::CwError::ExitCode(code));
183 }
184 Ok(())
185 })(),
186
187 Some(Commands::Guard { tool_input }) => guard::run(&tool_input),
188 Some(Commands::SetupClaude) => setup_claude::setup_claude(),
189
190 Some(Commands::Upgrade { yes }) => {
191 update::upgrade(yes);
192 Ok(())
193 }
194
195 Some(Commands::ShellSetup) => {
196 shell_setup();
197 Ok(())
198 }
199
200 Some(Commands::Path {
201 branch,
202 list_branches,
203 interactive,
204 }) => path_cmd::worktree_path(branch.as_deref(), list_branches, interactive),
205
206 Some(Commands::ShellFunction { shell }) => match shell_functions::generate(&shell) {
207 Some(output) => {
208 print!("{}", output);
209 Ok(())
210 }
211 None => Err(CwError::Config(format!(
212 "Unsupported shell: {}. Use bash, zsh, fish, or powershell.",
213 shell
214 ))),
215 },
216
217 Some(Commands::UpdateCache) => {
218 update::refresh_cache();
219 Ok(())
220 }
221
222 Some(Commands::CompleteTargets) => crate::operations::complete::print_completion_targets(),
223
224 Some(Commands::SpawnAi { spec }) => {
225 let resolved = match spec {
231 Some(p) => p,
232 None => match spawn_spec::resolve_last_for_cwd() {
233 Ok(p) => p,
234 Err(e) => {
235 eprintln!("{}", e);
236 std::process::exit(127);
237 }
238 },
239 };
240 if let Err(e) = spawn_spec::execute(&resolved) {
241 eprintln!("{}", e);
242 std::process::exit(127);
243 }
244 Ok(())
245 }
246
247 None => Ok(()),
248 };
249
250 if let Err(e) = result {
251 if let CwError::ExitCode(code) = e {
256 std::process::exit(code);
257 }
258 cwconsole::print_error(&format!("Error: {}", e));
259 std::process::exit(1);
260 }
261}
262
263fn generate_completions(shell_name: &str) {
264 use clap::CommandFactory;
265 use clap_complete::{generate, Shell};
266
267 let shell = match shell_name.to_lowercase().as_str() {
268 "bash" => Shell::Bash,
269 "zsh" => Shell::Zsh,
270 "fish" => Shell::Fish,
271 "powershell" | "pwsh" => Shell::PowerShell,
272 "elvish" => Shell::Elvish,
273 _ => {
274 eprintln!(
275 "Unsupported shell: {}. Use bash, zsh, fish, powershell, or elvish.",
276 shell_name
277 );
278 std::process::exit(1);
279 }
280 };
281
282 let mut cmd = Cli::command();
283 generate(shell, &mut cmd, "gw", &mut std::io::stdout());
284}
285
286fn shell_setup() {
287 let shell_env = std::env::var("SHELL").unwrap_or_default();
288 let is_powershell = cfg!(target_os = "windows") || std::env::var("PSModulePath").is_ok();
289
290 let home = constants::home_dir_or_fallback();
291 let (shell_name, profile_path) = if shell_env.contains("zsh") {
292 ("zsh", Some(home.join(".zshrc")))
293 } else if shell_env.contains("bash") {
294 ("bash", Some(home.join(".bashrc")))
295 } else if shell_env.contains("fish") {
296 (
297 "fish",
298 Some(home.join(".config").join("fish").join("config.fish")),
299 )
300 } else if is_powershell {
301 ("powershell", None::<std::path::PathBuf>)
302 } else {
303 println!("Could not detect your shell automatically.\n");
304 println!("Please manually add the gw-cd function to your shell:\n");
305 println!(" bash/zsh: source <(gw _shell-function bash)");
306 println!(" fish: gw _shell-function fish | source");
307 println!(" PowerShell: gw _shell-function powershell | Out-String | Invoke-Expression");
308 return;
309 };
310
311 println!("Detected shell: {}\n", shell_name);
312
313 if shell_name == "powershell" {
314 println!("To enable gw-cd in PowerShell, add the following to your $PROFILE:\n");
315 println!(" gw _shell-function powershell | Out-String | Invoke-Expression\n");
316 println!("To find your PowerShell profile location, run: $PROFILE");
317 println!(
318 "\nIf the profile file doesn't exist, create it with: New-Item -Path $PROFILE -ItemType File -Force"
319 );
320 return;
321 }
322
323 let shell_function_line = match shell_name {
324 "fish" => "gw _shell-function fish | source".to_string(),
325 _ => format!("source <(gw _shell-function {})", shell_name),
326 };
327
328 if let Some(ref path) = profile_path {
329 if path.exists() {
330 if let Ok(content) = std::fs::read_to_string(path) {
331 if content.contains("gw _shell-function") || content.contains("gw-cd") {
332 println!(
333 "{}",
334 console::style("Shell integration is already installed.").green()
335 );
336 println!(" Found in: {}\n", path.display());
337
338 refresh_shell_cache(shell_name);
339
340 println!("\nRestart your shell or run: source {}", path.display());
341 return;
342 }
343 }
344 }
345 }
346
347 println!("Setup shell integration?\n");
348 println!(
349 "This will add the following to {}:",
350 profile_path
351 .as_ref()
352 .map(|p| p.display().to_string())
353 .unwrap_or("your profile".to_string())
354 );
355
356 println!(
357 "\n # git-worktree-manager shell integration{}",
358 if matches!(shell_name, "zsh" | "bash") {
359 " (gw-cd + tab completion)"
360 } else {
361 ""
362 }
363 );
364 println!(" {}\n", shell_function_line);
365
366 print!("Add to your shell profile? [Y/n]: ");
367 use std::io::Write;
368 let _ = std::io::stdout().flush();
369
370 let mut input = String::new();
371 let _ = std::io::stdin().read_line(&mut input);
372 let input = input.trim().to_lowercase();
373
374 if !input.is_empty() && input != "y" && input != "yes" {
375 println!("\nSetup cancelled.");
376 return;
377 }
378
379 let Some(ref path) = profile_path else {
380 return;
381 };
382
383 if let Some(parent) = path.parent() {
384 let _ = std::fs::create_dir_all(parent);
385 }
386
387 let comment_suffix = if matches!(shell_name, "zsh" | "bash") {
388 " (gw-cd + tab completion)"
389 } else {
390 ""
391 };
392 let append = format!(
393 "\n# git-worktree-manager shell integration{}\n{}\n",
394 comment_suffix, shell_function_line
395 );
396
397 match std::fs::OpenOptions::new()
398 .create(true)
399 .append(true)
400 .open(path)
401 {
402 Ok(mut f) => {
403 let _ = f.write_all(append.as_bytes());
404
405 if let Ok(mut cfg) = config::load_config() {
406 cfg.shell_completion.installed = true;
407 cfg.shell_completion.prompted = true;
408 let _ = config::save_config(&cfg);
409 }
410
411 println!("\n* Successfully added to {}", path.display());
412
413 refresh_shell_cache(shell_name);
414
415 println!("\nNext steps:");
416 println!(" 1. Restart your shell or run: source {}", path.display());
417 println!(" 2. Try directory navigation: gw-cd <branch-name>");
418 println!(" 3. Try tab completion: gw <TAB> or gw new <TAB>");
419 }
420 Err(e) => {
421 println!("\nError: Failed to update {}: {}", path.display(), e);
422 println!("\nTo install manually, add the lines shown above to your profile");
423 }
424 }
425}
426
427fn refresh_shell_cache(shell_name: &str) {
429 let home = constants::home_dir_or_fallback();
430
431 let cache_paths = [
432 home.join(".cache").join("gw-shell-function.zsh"),
433 home.join(".cache").join("gw-shell-function.bash"),
434 home.join(".cache").join("gw-shell-function.fish"),
435 ];
436
437 let mut refreshed = false;
438 for cache_path in &cache_paths {
439 if !cache_path.exists() {
440 continue;
441 }
442 let cache_shell = cache_path
443 .extension()
444 .and_then(|e| e.to_str())
445 .unwrap_or("");
446 if let Some(content) = shell_functions::generate(cache_shell) {
447 if std::fs::write(cache_path, content).is_ok() {
448 println!(
449 " {} {}",
450 console::style("Refreshed cache:").dim(),
451 cache_path.display()
452 );
453 refreshed = true;
454 }
455 }
456 }
457
458 if refreshed {
459 return;
460 }
461
462 let cache_path = home
463 .join(".cache")
464 .join(format!("gw-shell-function.{}", shell_name));
465 if let Some(content) = shell_functions::generate(shell_name) {
466 let _ = std::fs::create_dir_all(cache_path.parent().unwrap_or(&home));
467 if std::fs::write(&cache_path, &content).is_ok() {
468 println!(
469 " {} {}",
470 console::style("Created cache:").dim(),
471 cache_path.display()
472 );
473 }
474 }
475}