1mod activate;
2mod env;
3mod package;
4mod store;
5mod term;
6
7use slox_cli::{Commands, EnvCommand, PkgCommand};
8use store::StorePaths;
9
10pub fn report_error(error: &str) {
11 term::report_error(error);
12}
13
14pub fn run(command: Commands) -> Result<(), String> {
15 run_with_store(command, &StorePaths::default())
16}
17
18fn run_with_store(command: Commands, store: &StorePaths) -> Result<(), String> {
19 match command {
20 Commands::Env { command } => handle_env(store, command),
21 Commands::Activate { command } => {
22 println!("{}", activate::activation_script(store, command)?);
23 Ok(())
24 }
25 Commands::Pkg { command } => handle_pkg(store, command),
26 }
27}
28
29fn handle_env(store: &StorePaths, cmd: EnvCommand) -> Result<(), String> {
30 match cmd {
31 EnvCommand::Add { path } => env::add_env(store, &path),
32 EnvCommand::Remove { path } => env::remove_env(store, &path),
33 EnvCommand::Set { path } => env::set_env(store, &path),
34 }
35}
36
37fn handle_pkg(store: &StorePaths, cmd: PkgCommand) -> Result<(), String> {
38 match cmd {
39 PkgCommand::Add { path } => package::download(store, &package::parse_pkg(&path)?),
40 PkgCommand::Remove { path } => {
41 term::print_status(
42 "WARN",
43 term::YELLOW,
44 &format!("Package removal is not implemented yet for `{path}`."),
45 );
46 Ok(())
47 }
48 }
49}
50
51#[cfg(test)]
52mod tests {
53 use super::{
54 activate::activation_script,
55 env::{active_bin_dir, active_env_name, add_env, remove_env, set_env},
56 package::{Pkg, install_built_binary, parse_pkg, run_build_script},
57 store::StorePaths,
58 };
59 use crate::activate;
60 use slox_cli::ActivateCommand;
61 use std::fs;
62 use std::path::PathBuf;
63 use std::time::{SystemTime, UNIX_EPOCH};
64
65 fn make_temp_dir(name: &str) -> PathBuf {
66 let unique = SystemTime::now()
67 .duration_since(UNIX_EPOCH)
68 .expect("time went backwards")
69 .as_nanos();
70 let dir = std::env::temp_dir().join(format!("slox-{name}-{unique}"));
71 fs::create_dir_all(&dir).expect("failed to create temp dir");
72 dir
73 }
74
75 fn make_store() -> StorePaths {
76 StorePaths {
77 base: make_temp_dir("store"),
78 }
79 }
80
81 #[test]
82 fn parse_github_package() {
83 let pkg = parse_pkg("devwon/slox@github").expect("github package should parse");
84 match pkg {
85 Pkg::GitHub { user, repo } => {
86 assert_eq!(user, "devwon");
87 assert_eq!(repo, "slox");
88 }
89 Pkg::Regit { .. } => panic!("expected github package"),
90 }
91 }
92
93 #[test]
94 fn parse_regit_package() {
95 let pkg = parse_pkg("plur@sloxpkgs").expect("sloxpkgs package should parse");
96 match pkg {
97 Pkg::Regit { pkg } => assert_eq!(pkg, "plur"),
98 Pkg::GitHub { .. } => panic!("expected sloxpkgs package"),
99 }
100 }
101
102 #[test]
103 fn reject_invalid_package_format() {
104 let error = parse_pkg("invalid-package").expect_err("package parse should fail");
105 assert!(error.contains("user/repo@github"));
106 }
107
108 #[test]
109 fn activation_script_uses_root_bin_path() {
110 let store = StorePaths {
111 base: PathBuf::from("/tmp/slox-store"),
112 };
113
114 let script = activation_script(&store, ActivateCommand::Zsh)
115 .expect("root activation script should render");
116 assert_eq!(
117 script,
118 concat!(
119 "_slox_store='/tmp/slox-store'\n",
120 "_slox_bin='/tmp/slox-store/bin'\n",
121 "_slox_filtered_path=\n",
122 "_slox_old_ifs=$IFS\n",
123 "IFS=:\n",
124 "for _slox_entry in $PATH; do\n",
125 " case \"$_slox_entry\" in\n",
126 " \"$_slox_store\"/bin|\"$_slox_store\"/bin/)\n",
127 " ;;\n",
128 " '')\n",
129 " ;;\n",
130 " *)\n",
131 " if [ -n \"$_slox_filtered_path\" ]; then\n",
132 " _slox_filtered_path=\"$_slox_filtered_path:$_slox_entry\"\n",
133 " else\n",
134 " _slox_filtered_path=\"$_slox_entry\"\n",
135 " fi\n",
136 " ;;\n",
137 " esac\n",
138 "done\n",
139 "IFS=$_slox_old_ifs\n",
140 "if [ -n \"$_slox_filtered_path\" ]; then\n",
141 " export PATH=\"$_slox_bin:$_slox_filtered_path\"\n",
142 "else\n",
143 " export PATH=\"$_slox_bin\"\n",
144 "fi\n",
145 "unset _slox_store _slox_bin _slox_filtered_path _slox_old_ifs _slox_entry"
146 )
147 );
148 }
149
150 #[test]
151 fn add_and_remove_env_under_store() {
152 let store = make_store();
153 let env_dir = store.env_dir("demo");
154
155 add_env(&store, "demo").expect("env should be created");
156 assert!(env_dir.is_dir());
157 assert!(store.env_bin_dir("demo").is_dir());
158
159 remove_env(&store, "demo").expect("env should be removed");
160 assert!(!env_dir.exists());
161
162 fs::remove_dir_all(store.base).expect("failed to clean store dir");
163 }
164
165 #[test]
166 fn set_env_changes_active_bin_dir_and_activation_script() {
167 let store = make_store();
168 add_env(&store, "demo").expect("env should be created");
169
170 set_env(&store, "demo").expect("env should be set");
171 assert_eq!(
172 active_env_name(&store).expect("active env should be readable"),
173 Some("demo".to_string())
174 );
175 assert_eq!(
176 active_bin_dir(&store).expect("active bin dir should resolve"),
177 store.env_bin_dir("demo")
178 );
179
180 let script = activation_script(&store, ActivateCommand::Bash)
181 .expect("env activation script should render");
182 assert_eq!(
183 script,
184 format!(
185 concat!(
186 "_slox_store={store}\n",
187 "_slox_bin={bin}\n",
188 "_slox_filtered_path=\n",
189 "_slox_old_ifs=$IFS\n",
190 "IFS=:\n",
191 "for _slox_entry in $PATH; do\n",
192 " case \"$_slox_entry\" in\n",
193 " \"$_slox_store\"/bin|\"$_slox_store\"/bin/)\n",
194 " ;;\n",
195 " '')\n",
196 " ;;\n",
197 " *)\n",
198 " if [ -n \"$_slox_filtered_path\" ]; then\n",
199 " _slox_filtered_path=\"$_slox_filtered_path:$_slox_entry\"\n",
200 " else\n",
201 " _slox_filtered_path=\"$_slox_entry\"\n",
202 " fi\n",
203 " ;;\n",
204 " esac\n",
205 "done\n",
206 "IFS=$_slox_old_ifs\n",
207 "if [ -n \"$_slox_filtered_path\" ]; then\n",
208 " export PATH=\"$_slox_bin:$_slox_filtered_path\"\n",
209 "else\n",
210 " export PATH=\"$_slox_bin\"\n",
211 "fi\n",
212 "unset _slox_store _slox_bin _slox_filtered_path _slox_old_ifs _slox_entry"
213 ),
214 store = activate::shell_single_quote(&store.base.display().to_string()),
215 bin = activate::shell_single_quote(&store.shim_bin_dir().display().to_string())
216 )
217 );
218
219 fs::remove_dir_all(store.base).expect("failed to clean store dir");
220 }
221
222 #[test]
223 fn removing_active_env_clears_active_env_file() {
224 let store = make_store();
225 add_env(&store, "demo").expect("env should be created");
226 set_env(&store, "demo").expect("env should be set");
227
228 remove_env(&store, "demo").expect("env should be removed");
229 assert_eq!(
230 active_env_name(&store).expect("active env should be readable"),
231 None
232 );
233 assert_eq!(
234 active_bin_dir(&store).expect("root bin should be used"),
235 store.root_bin_dir()
236 );
237
238 fs::remove_dir_all(store.base).expect("failed to clean store dir");
239 }
240
241 #[test]
242 fn build_and_install_github_package_layout() {
243 let repo_name = "demo";
244 let repo_dir = make_temp_dir("repo");
245 let root_bin_dir = make_temp_dir("root-bin");
246
247 fs::write(
248 repo_dir.join("build"),
249 format!(
250 "#!/bin/sh\nmkdir -p bin\nprintf '#!/bin/sh\\necho installed\\n' > bin/{repo_name}\nchmod +x bin/{repo_name}\n"
251 ),
252 )
253 .expect("failed to write build script");
254
255 run_build_script(&repo_dir).expect("build script should succeed");
256 let installed_binary = install_built_binary(&repo_dir, repo_name, &root_bin_dir)
257 .expect("install should succeed");
258
259 assert!(installed_binary.is_file());
260 let contents =
261 fs::read_to_string(installed_binary).expect("failed to read installed binary");
262 assert!(contents.contains("installed"));
263
264 fs::remove_dir_all(repo_dir).expect("failed to clean repo dir");
265 fs::remove_dir_all(root_bin_dir).expect("failed to clean root bin dir");
266 }
267
268 #[test]
269 fn install_target_follows_active_env_bin() {
270 let store = make_store();
271 let repo_name = "demo";
272 let repo_dir = make_temp_dir("repo");
273
274 add_env(&store, "demo-env").expect("env should be created");
275 set_env(&store, "demo-env").expect("env should be set");
276
277 fs::write(
278 repo_dir.join("build"),
279 format!(
280 "#!/bin/sh\nmkdir -p bin\nprintf '#!/bin/sh\\necho active-env\\n' > bin/{repo_name}\nchmod +x bin/{repo_name}\n"
281 ),
282 )
283 .expect("failed to write build script");
284
285 run_build_script(&repo_dir).expect("build script should succeed");
286 let install_dir = active_bin_dir(&store).expect("active bin dir should resolve");
287 let installed_binary = install_built_binary(&repo_dir, repo_name, &install_dir)
288 .expect("install should succeed");
289
290 assert_eq!(
291 installed_binary,
292 store.env_bin_dir("demo-env").join(repo_name)
293 );
294 assert!(installed_binary.is_file());
295
296 fs::remove_dir_all(repo_dir).expect("failed to clean repo dir");
297 fs::remove_dir_all(store.base).expect("failed to clean store dir");
298 }
299
300 #[test]
301 fn sync_active_shims_points_to_active_env_bins() {
302 let store = make_store();
303 add_env(&store, "demo").expect("env should be created");
304
305 fs::create_dir_all(store.env_bin_dir("demo")).expect("env bin dir should exist");
306 fs::write(
307 store.env_bin_dir("demo").join("demo"),
308 "#!/bin/sh\necho env\n",
309 )
310 .expect("binary should be written");
311
312 set_env(&store, "demo").expect("env should be set");
313
314 let shim_path = store.shim_bin_dir().join("demo");
315 assert!(shim_path.is_file());
316 let shim_contents = fs::read_to_string(&shim_path).expect("shim should be readable");
317 assert!(
318 shim_contents.contains(&store.env_bin_dir("demo").join("demo").display().to_string())
319 );
320
321 fs::remove_dir_all(store.base).expect("failed to clean store dir");
322 }
323
324 #[test]
325 fn clearing_active_env_refreshes_shims_to_root_bin() {
326 let store = make_store();
327 add_env(&store, "demo").expect("env should be created");
328 fs::create_dir_all(store.root_bin_dir()).expect("root bin dir should exist");
329 fs::write(
330 store.root_bin_dir().join("root-tool"),
331 "#!/bin/sh\necho root\n",
332 )
333 .expect("root binary should be written");
334 fs::write(
335 store.env_bin_dir("demo").join("env-tool"),
336 "#!/bin/sh\necho env\n",
337 )
338 .expect("env binary should be written");
339
340 set_env(&store, "demo").expect("env should be set");
341 remove_env(&store, "demo").expect("env should be removed");
342
343 let shim_dir = store.shim_bin_dir();
344 assert!(shim_dir.join("root-tool").is_file());
345 assert!(!shim_dir.join("env-tool").exists());
346
347 fs::remove_dir_all(store.base).expect("failed to clean store dir");
348 }
349}