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