agentmux_srv\backend/
shellintegration.rs1use std::path::Path;
13
14const BASH_SCRIPT: &str = include_str!("shellintegration/bash.sh");
17const ZSH_SCRIPT: &str = include_str!("shellintegration/zsh.sh");
18const PWSH_SCRIPT: &str = include_str!("shellintegration/pwsh.ps1");
19const FISH_SCRIPT: &str = include_str!("shellintegration/fish.fish");
20const VERSION_MARKER: &str = env!("CARGO_PKG_VERSION");
21
22#[derive(Debug, Clone, Copy, PartialEq)]
25pub enum ShellType {
26 Bash,
27 Zsh,
28 Pwsh,
29 Fish,
30 Unknown,
31}
32
33pub fn detect_shell_type(shell_path: &str) -> ShellType {
35 let name = Path::new(shell_path)
36 .file_stem()
37 .and_then(|s| s.to_str())
38 .unwrap_or("")
39 .to_lowercase();
40
41 match name.as_str() {
42 "pwsh" | "powershell" => ShellType::Pwsh,
43 "bash" => ShellType::Bash,
44 "zsh" => ShellType::Zsh,
45 "fish" => ShellType::Fish,
46 _ => ShellType::Unknown,
47 }
48}
49
50pub fn deploy_scripts(wave_data_dir: &Path) {
56 let shell_base = wave_data_dir.join("shell");
57 let version_file = shell_base.join(".version");
58
59 if let Ok(existing) = std::fs::read_to_string(&version_file) {
61 if existing.trim() == VERSION_MARKER {
62 return;
63 }
64 }
65
66 tracing::info!("Deploying shell integration scripts (v{})", VERSION_MARKER);
67
68 let deploys: &[(&str, &str, &str)] = &[
69 ("bash", ".bashrc", BASH_SCRIPT),
70 ("zsh", ".zshrc", ZSH_SCRIPT),
71 ("pwsh", "wavepwsh.ps1", PWSH_SCRIPT),
72 ("fish", "wave.fish", FISH_SCRIPT),
73 ];
74
75 let mut all_ok = true;
76 for (dir_name, file_name, content) in deploys {
77 let dir = shell_base.join(dir_name);
78 if let Err(e) = std::fs::create_dir_all(&dir) {
79 tracing::warn!("shell integration: failed to create {}: {}", dir.display(), e);
80 all_ok = false;
81 continue;
82 }
83 let path = dir.join(file_name);
84 if let Err(e) = std::fs::write(&path, content) {
85 tracing::warn!("shell integration: failed to write {}: {}", path.display(), e);
86 all_ok = false;
87 }
88 }
89
90 if all_ok {
92 let _ = std::fs::write(&version_file, VERSION_MARKER);
93 }
94}
95
96pub struct ShellStartup {
100 pub extra_args: Vec<String>,
102 pub env_vars: Vec<(String, String)>,
104}
105
106pub fn get_shell_startup(
109 shell_type: ShellType,
110 wave_data_dir: &Path,
111) -> Option<ShellStartup> {
112 match shell_type {
113 ShellType::Bash => {
114 let rcfile = wave_data_dir.join("shell").join("bash").join(".bashrc");
115 Some(ShellStartup {
116 extra_args: vec![
117 "--rcfile".to_string(),
118 rcfile.to_string_lossy().into_owned(),
119 ],
120 env_vars: vec![],
121 })
122 }
123 ShellType::Zsh => {
124 let zdotdir = wave_data_dir.join("shell").join("zsh");
125 Some(ShellStartup {
126 extra_args: vec![],
127 env_vars: vec![
128 ("ZDOTDIR".to_string(), zdotdir.to_string_lossy().into_owned()),
129 ("AGENTMUX_ZDOTDIR".to_string(), zdotdir.to_string_lossy().into_owned()),
131 ],
132 })
133 }
134 ShellType::Pwsh => {
135 let script = wave_data_dir
136 .join("shell")
137 .join("pwsh")
138 .join("wavepwsh.ps1");
139 Some(ShellStartup {
140 extra_args: vec![
141 "-ExecutionPolicy".to_string(),
142 "Bypass".to_string(),
143 "-NoExit".to_string(),
144 "-File".to_string(),
145 script.to_string_lossy().into_owned(),
146 ],
147 env_vars: vec![],
148 })
149 }
150 ShellType::Fish => {
151 let script = wave_data_dir
152 .join("shell")
153 .join("fish")
154 .join("wave.fish");
155 Some(ShellStartup {
156 extra_args: vec![
157 "-C".to_string(),
158 format!("source {}", shell_quote(&script.to_string_lossy())),
159 ],
160 env_vars: vec![],
161 })
162 }
163 ShellType::Unknown => None,
164 }
165}
166
167fn shell_quote(s: &str) -> String {
174 format!("'{}'", s.replace('\'', "'\\''"))
175}
176
177#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn test_detect_shell_type() {
185 assert_eq!(detect_shell_type("bash"), ShellType::Bash);
186 assert_eq!(detect_shell_type("/bin/bash"), ShellType::Bash);
187 assert_eq!(detect_shell_type("zsh"), ShellType::Zsh);
188 assert_eq!(detect_shell_type("/usr/bin/zsh"), ShellType::Zsh);
189 assert_eq!(detect_shell_type("pwsh"), ShellType::Pwsh);
190 assert_eq!(detect_shell_type("powershell"), ShellType::Pwsh);
191 assert_eq!(detect_shell_type("fish"), ShellType::Fish);
192 assert_eq!(detect_shell_type("cmd.exe"), ShellType::Unknown);
193 assert_eq!(detect_shell_type("cmd"), ShellType::Unknown);
194 }
195
196 #[test]
197 fn test_bash_startup_args() {
198 let dir = Path::new("/home/user/.agentmux");
199 let startup = get_shell_startup(ShellType::Bash, dir).unwrap();
200 assert_eq!(startup.extra_args[0], "--rcfile");
201 assert!(startup.extra_args[1].contains("bash"));
202 assert!(startup.extra_args[1].ends_with(".bashrc"));
203 }
204
205 #[test]
206 fn test_pwsh_startup_args() {
207 let dir = Path::new("/home/user/.agentmux");
208 let startup = get_shell_startup(ShellType::Pwsh, dir).unwrap();
209 assert!(startup.extra_args.contains(&"-NoExit".to_string()));
210 assert!(startup.extra_args.contains(&"-File".to_string()));
211 }
212
213 #[test]
214 fn test_zsh_uses_zdotdir() {
215 let dir = Path::new("/home/user/.agentmux");
216 let startup = get_shell_startup(ShellType::Zsh, dir).unwrap();
217 assert!(startup.extra_args.is_empty());
218 assert!(startup.env_vars.iter().any(|(k, _)| k == "ZDOTDIR"));
219 }
220
221 #[test]
222 fn test_unknown_shell_returns_none() {
223 let dir = Path::new("/tmp");
224 assert!(get_shell_startup(ShellType::Unknown, dir).is_none());
225 }
226}