1use crate::infra::git::{resolve_git_common_dir, resolve_toplevel};
4use crate::infra::registry::SlotRegistry;
5use crate::workflow::error::WorkflowError;
6use std::collections::BTreeMap;
7use std::fs;
8use std::path::{Path, PathBuf};
9use std::process::Command;
10
11pub struct RunOptions {
12 pub name: Option<String>,
13 pub command: Vec<String>,
14 pub repo_root: Option<PathBuf>,
15}
16
17pub fn parse_env_file(path: &Path) -> BTreeMap<String, String> {
19 let mut map = BTreeMap::new();
20 if let Ok(content) = fs::read_to_string(path) {
21 for line in content.lines() {
22 let trimmed = line.trim();
23 if trimmed.is_empty() || trimmed.starts_with('#') {
24 continue;
25 }
26 if let Some((k, v)) = trimmed.split_once('=') {
27 map.insert(k.trim().to_string(), v.trim().to_string());
28 }
29 }
30 }
31 map
32}
33
34pub fn run_command(options: RunOptions) -> Result<i32, WorkflowError> {
36 let toplevel = resolve_toplevel(options.repo_root.as_deref())?;
37 let common_dir = resolve_git_common_dir(Some(&toplevel))?;
38
39 let registry_path = common_dir.join("claw/slots.json");
40 let registry = SlotRegistry::load_or_empty(®istry_path)?;
41
42 let (slot, cmd_args) = match options.name {
44 Some(name_str) => {
45 if let Some(s) = registry.find_by_name(&name_str) {
46 (s.clone(), options.command)
48 } else {
49 let cwd = std::env::current_dir().unwrap_or_default();
51 let canonical_cwd = cwd.canonicalize().unwrap_or(cwd);
52 let found = registry.slots.iter().find(|s| {
53 let p = PathBuf::from(&s.path);
54 let canonical_p = p.canonicalize().unwrap_or(p);
55 canonical_cwd.starts_with(&canonical_p)
56 });
57
58 if let Some(s) = found {
59 let mut full_cmd = vec![name_str];
60 full_cmd.extend(options.command);
61 (s.clone(), full_cmd)
62 } else {
63 return Err(WorkflowError::WorktreeNotFound(name_str));
64 }
65 }
66 }
67 None => {
68 let cwd = std::env::current_dir().unwrap_or_default();
69 let canonical_cwd = cwd.canonicalize().unwrap_or(cwd);
70 let found = registry.slots.iter().find(|s| {
71 let p = PathBuf::from(&s.path);
72 let canonical_p = p.canonicalize().unwrap_or(p);
73 canonical_cwd.starts_with(&canonical_p)
74 });
75
76 match found {
77 Some(s) => (s.clone(), options.command),
78 None => {
79 return Err(WorkflowError::CannotInferWorktree(
80 canonical_cwd.to_string_lossy().to_string(),
81 ));
82 }
83 }
84 }
85 };
86
87 if cmd_args.is_empty() {
88 return Err(WorkflowError::NoCommandSpecified);
89 }
90
91 let worktree_path = PathBuf::from(&slot.path);
92 let env_file_path = worktree_path.join(".env.worktree");
93 let env_vars = parse_env_file(&env_file_path);
94
95 let mut cmd = Command::new(&cmd_args[0]);
96 cmd.args(&cmd_args[1..]);
97 cmd.current_dir(&worktree_path);
98
99 for (k, v) in env_vars {
100 cmd.env(k, v);
101 }
102
103 let status = cmd.status().map_err(|e| {
104 WorkflowError::CommandExecutionFailed(format!("failed to execute '{}': {}", cmd_args[0], e))
105 })?;
106
107 Ok(status.code().unwrap_or(1))
108}