1use crate::infra::git::{resolve_git_common_dir, resolve_toplevel};
4use crate::infra::registry::SlotRegistry;
5use crate::workflow::error::WorkflowError;
6use std::path::{Path, PathBuf};
7use std::process::Command;
8
9pub struct CdOptions<'a> {
10 pub name: Option<&'a str>,
11 pub repo_root: Option<&'a Path>,
12}
13
14pub struct OpenOptions<'a> {
15 pub name: Option<&'a str>,
16 pub editor: Option<&'a str>,
17 pub repo_root: Option<&'a Path>,
18}
19
20fn resolve_target_worktree_path(
22 name: Option<&str>,
23 repo_root: Option<&Path>,
24) -> Result<PathBuf, WorkflowError> {
25 let toplevel = resolve_toplevel(repo_root)?;
26 let common_dir = resolve_git_common_dir(Some(&toplevel))?;
27
28 let registry_path = common_dir.join("claw/slots.json");
29 let registry = SlotRegistry::load_or_empty(®istry_path)?;
30
31 match name {
32 Some(target_name) => {
33 let slot = registry
34 .find_by_name(target_name)
35 .ok_or_else(|| WorkflowError::WorktreeNotFound(target_name.to_string()))?;
36 Ok(PathBuf::from(&slot.path))
37 }
38 None => {
39 let cwd = std::env::current_dir().unwrap_or_default();
40 let canonical_cwd = cwd.canonicalize().unwrap_or(cwd);
41 let found = registry.slots.iter().find(|s| {
42 let p = PathBuf::from(&s.path);
43 let canonical_p = p.canonicalize().unwrap_or(p);
44 canonical_cwd.starts_with(&canonical_p)
45 });
46
47 match found {
48 Some(s) => Ok(PathBuf::from(&s.path)),
49 None => Err(WorkflowError::CannotInferWorktree(
50 canonical_cwd.to_string_lossy().to_string(),
51 )),
52 }
53 }
54 }
55}
56
57pub fn cd_worktree(options: CdOptions<'_>) -> Result<PathBuf, WorkflowError> {
59 let path = resolve_target_worktree_path(options.name, options.repo_root)?;
60 println!("{}", path.display());
61 Ok(path)
62}
63
64pub fn open_worktree(options: OpenOptions<'_>) -> Result<(), WorkflowError> {
66 let path = resolve_target_worktree_path(options.name, options.repo_root)?;
67
68 let editor_cmd = match options.editor {
69 Some(e) => e.to_string(),
70 None => std::env::var("EDITOR")
71 .or_else(|_| std::env::var("VISUAL"))
72 .unwrap_or_else(|_| "code".to_string()),
73 };
74
75 let status = Command::new(&editor_cmd).arg(&path).status().map_err(|e| {
76 WorkflowError::CommandExecutionFailed(format!(
77 "failed to launch editor '{}': {}",
78 editor_cmd, e
79 ))
80 })?;
81
82 if !status.success() {
83 return Err(WorkflowError::CommandExecutionFailed(format!(
84 "editor '{}' exited with non-zero status",
85 editor_cmd
86 )));
87 }
88
89 Ok(())
90}