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