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