Skip to main content

zoi_cli/
utils.rs

1use crate::pkg::types::Scope;
2use anyhow::anyhow;
3use colored::*;
4use crossterm::tty::IsTty;
5use std::fmt::Display;
6use std::fs;
7use std::io::{Write, stdin, stdout};
8use std::path::{Path, PathBuf};
9use std::process::Command;
10
11pub fn print_info<T: Display>(key: &str, value: T) {
12    println!("{}: {}", key, value);
13}
14
15pub fn format_version_summary(branch: &str, status: &str, number: &str) -> String {
16    let branch_short = if branch == "Production" {
17        "Prod."
18    } else if branch == "Development" {
19        "Dev."
20    } else if branch == "Public" {
21        "Pub."
22    } else if branch == "Special" {
23        "Spec."
24    } else {
25        branch
26    };
27    format!(
28        "{} {} {}",
29        branch_short.blue().bold().italic(),
30        status,
31        number,
32    )
33}
34
35pub fn format_version_full(branch: &str, status: &str, number: &str, commit: &str) -> String {
36    format!(
37        "{} {}",
38        format_version_summary(branch, status, number),
39        commit.green()
40    )
41}
42
43pub fn print_aligned_info(key: &str, value: &str) {
44    let key_with_colon = format!("{}:", key);
45    println!("{:<18}{}", key_with_colon.cyan(), value);
46}
47
48pub fn print_repo_warning(repo_name: &str) {
49    if crate::pkg::utils::is_mini_mode() {
50        if let Ok(index) = crate::pkg::mini_resolve::fetch_registry_index()
51            && let Some(pkg_info) = index.packages.values().find(|p| p.repo == repo_name)
52        {
53            let warning_message = match pkg_info.repo_type.as_str() {
54                "unofficial" => {
55                    Some("This package is from an unofficial repository and is not trusted.")
56                }
57                "community" => {
58                    Some("This package is from a community repository. Use with caution.")
59                }
60                "test" => Some(
61                    "This package is from a testing repository and may not function correctly.",
62                ),
63                "archive" => {
64                    Some("This package is from an archive repository and is no longer maintained.")
65                }
66                _ => None,
67            };
68
69            if let Some(message) = warning_message {
70                println!("\n{}: {}", "NOTE".yellow().bold(), message.yellow());
71            }
72        }
73        return;
74    }
75
76    if let Ok(db_path) = crate::pkg::resolve::get_db_root()
77        && let Ok(repo_config) = crate::pkg::config::read_repo_config(&db_path)
78    {
79        let major_repo = repo_name.split('/').next().unwrap_or_default();
80        if let Some(repo_entry) = repo_config.repos.iter().find(|r| r.name == major_repo) {
81            let warning_message = match repo_entry.repo_type.as_str() {
82                "unofficial" => {
83                    Some("This package is from an unofficial repository and is not trusted.")
84                }
85                "community" => {
86                    Some("This package is from a community repository. Use with caution.")
87                }
88                "test" => Some(
89                    "This package is from a testing repository and may not function correctly.",
90                ),
91                "archive" => {
92                    Some("This package is from an archive repository and is no longer maintained.")
93                }
94                _ => None,
95            };
96
97            if let Some(message) = warning_message {
98                println!("\n{}: {}", "NOTE".yellow().bold(), message.yellow());
99            }
100        }
101    }
102}
103
104pub fn get_all_packages_for_completion() -> Vec<PackageCompletion> {
105    let mut completions = Vec::new();
106    let config = if let Ok(cfg) = crate::pkg::config::read_config() {
107        cfg
108    } else {
109        return completions;
110    };
111
112    let mut registries = Vec::new();
113    if let Some(default) = &config.default_registry {
114        registries.push(default.handle.clone());
115    }
116    for reg in &config.added_registries {
117        registries.push(reg.handle.clone());
118    }
119
120    let default_handle = config.default_registry.as_ref().map(|r| &r.handle);
121
122    for handle in registries {
123        if handle.is_empty() {
124            continue;
125        }
126        if let Ok(entries) = crate::pkg::db::get_packages_for_completion(&handle) {
127            let is_default = default_handle == Some(&handle);
128            for entry in entries {
129                let base_name = if is_default {
130                    format!("@{}/{}", entry.repo, entry.name)
131                } else {
132                    format!("#{}@{}/{}", handle, entry.repo, entry.name)
133                };
134
135                let display = if let Some(sub) = entry.sub_package {
136                    format!("{}:{}", base_name, sub)
137                } else {
138                    base_name
139                };
140
141                completions.push(PackageCompletion {
142                    display,
143                    repo: entry.repo,
144                    description: entry.description,
145                });
146            }
147        }
148    }
149
150    completions.sort_by(|a, b| a.display.cmp(&b.display));
151    completions
152}
153
154pub struct PackageCompletion {
155    pub display: String,
156    pub repo: String,
157    pub description: String,
158}
159
160pub fn symlink_file(target: &Path, link: &Path) -> std::io::Result<()> {
161    if link.exists() || link.is_symlink() {
162        fs::remove_file(link)?;
163    }
164
165    #[cfg(unix)]
166    {
167        std::os::unix::fs::symlink(target, link)
168    }
169    #[cfg(windows)]
170    {
171        if std::os::windows::fs::symlink_file(target, link).is_err() {
172            if fs::hard_link(target, link).is_err() {
173                fs::copy(target, link)?;
174            }
175        }
176        Ok(())
177    }
178}
179
180pub fn is_admin() -> bool {
181    #[cfg(windows)]
182    {
183        use std::mem;
184        use std::ptr;
185        use winapi::um::handleapi::CloseHandle;
186        use winapi::um::processthreadsapi::GetCurrentProcess;
187        use winapi::um::processthreadsapi::OpenProcessToken;
188        use winapi::um::securitybaseapi::CheckTokenMembership;
189        use winapi::um::winnt::{PSID, TOKEN_QUERY};
190
191        let mut token = ptr::null_mut();
192        let process = unsafe { GetCurrentProcess() };
193        if unsafe { OpenProcessToken(process, TOKEN_QUERY, &mut token) } == 0 {
194            return false;
195        }
196
197        let mut sid: [u8; 8] = [0; 8];
198        let mut sid_size = mem::size_of_val(&sid) as u32;
199        if unsafe {
200            winapi::um::securitybaseapi::CreateWellKnownSid(
201                winapi::um::winnt::WinBuiltinAdministratorsSid,
202                ptr::null_mut(),
203                sid.as_mut_ptr() as PSID,
204                &mut sid_size,
205            )
206        } == 0
207        {
208            unsafe { CloseHandle(token) };
209            return false;
210        }
211
212        let mut is_member = 0;
213        let result =
214            unsafe { CheckTokenMembership(token, sid.as_mut_ptr() as PSID, &mut is_member) };
215        unsafe { CloseHandle(token) };
216
217        result != 0 && is_member != 0
218    }
219    #[cfg(unix)]
220    {
221        nix::unistd::getuid().is_root()
222    }
223}
224
225pub fn check_license(license: &str) {
226    if license.is_empty() {
227        println!(
228            "{}",
229            "Warning: Package does not have a license specified.".yellow()
230        );
231        return;
232    }
233
234    if license.eq_ignore_ascii_case("Proprietary") {
235        println!(
236            "{}",
237            "Warning: Package is using a proprietary license.".red()
238        );
239        return;
240    }
241
242    if license.eq_ignore_ascii_case("Unknown") {
243        println!("{}", "Warning: Package license is unknown.".red());
244        return;
245    }
246
247    match spdx::Expression::parse(license) {
248        Ok(expr) => {
249            if !expr.evaluate(|req| match req.license {
250                spdx::LicenseItem::Spdx { id, .. } => id.is_osi_approved(),
251                spdx::LicenseItem::Other { .. } => false,
252            }) {
253                println!(
254                    "{}{}{}",
255                    "Warning: License '".yellow(),
256                    license.yellow().bold(),
257                    "' is not an OSI approved license.".yellow()
258                );
259            }
260        }
261        Err(_) => {
262            println!(
263                "{}{}{}",
264                "Warning: Could not parse license expression '".yellow(),
265                license.yellow().bold(),
266                "' It may not be a valid SPDX identifier.".yellow()
267            );
268        }
269    }
270}
271
272pub fn ask_for_confirmation(prompt: &str, yes: bool) -> bool {
273    if yes {
274        return true;
275    }
276
277    if std::env::var("ZOI_TEST").is_ok() || !stdin().is_tty() {
278        return false;
279    }
280
281    print!("{} [y/N]: ", prompt.yellow());
282    let _ = stdout().flush();
283    let mut input = String::new();
284    if stdin().read_line(&mut input).is_err() {
285        return false;
286    }
287    input.trim().eq_ignore_ascii_case("y")
288}
289
290pub fn setup_path(scope: Scope) -> anyhow::Result<()> {
291    if scope == Scope::Project {
292        return Ok(());
293    }
294
295    let zoi_bin_dir = match scope {
296        Scope::User => {
297            let home = crate::pkg::utils::get_user_home()
298                .ok_or_else(|| anyhow!("Could not find home directory."))?;
299            crate::pkg::sysroot::apply_sysroot(home.join(".zoi").join("pkgs").join("bin"))
300        }
301        Scope::System => {
302            if cfg!(target_os = "windows") {
303                crate::pkg::sysroot::apply_sysroot(PathBuf::from("C:\\ProgramData\\zoi\\pkgs\\bin"))
304            } else {
305                crate::pkg::sysroot::apply_sysroot(PathBuf::from("/usr/local/bin"))
306            }
307        }
308        Scope::Project => return Ok(()),
309    };
310
311    if !zoi_bin_dir.exists() {
312        fs::create_dir_all(&zoi_bin_dir)?;
313    }
314
315    if scope == Scope::System && cfg!(unix) {
316        println!(
317            "{}",
318            "System-wide installation complete. Binaries are in the system PATH.".green()
319        );
320        return Ok(());
321    }
322
323    #[cfg(unix)]
324    {
325        use std::fs::{File, OpenOptions};
326        let home = crate::pkg::utils::get_user_home()
327            .ok_or_else(|| anyhow!("Could not find home directory."))?;
328        let zoi_bin_str = "$HOME/.zoi/pkgs/bin";
329
330        let shell_name = std::env::var("SHELL").unwrap_or_default();
331        let (profile_file_path, cmd_to_write) = if shell_name.contains("bash") {
332            let path = if cfg!(target_os = "macos") {
333                home.join(".bash_profile")
334            } else {
335                home.join(".bashrc")
336            };
337            let cmd = format!(
338                "\n# Added by Zoi\nexport PATH=\"{}:{}\"\n",
339                zoi_bin_str, "$PATH"
340            );
341            (path, cmd)
342        } else if shell_name.contains("zsh") {
343            let path = home.join(".zshrc");
344            let cmd = format!(
345                "\n# Added by Zoi\nexport PATH=\"{}:{}\"\n",
346                zoi_bin_str, "$PATH"
347            );
348            (path, cmd)
349        } else if shell_name.contains("fish") {
350            let path = home.join(".config/fish/config.fish");
351            let cmd = format!("\n# Added by Zoi\nfish_add_path \"{}\"\n", zoi_bin_str);
352            (path, cmd)
353        } else if shell_name.contains("elvish") {
354            let path = home.join(".config/elvish/rc.elv");
355            let cmd = "
356# Added by Zoi
357set paths = [ ~/.zoi/pkgs/bin $paths... ]
358"
359            .to_string();
360            (path, cmd)
361        } else if shell_name.contains("csh") || shell_name.contains("tcsh") {
362            let path = home.join(".cshrc");
363            let cmd = format!(
364                "\n# Added by Zoi\nsetenv PATH=\"{}:{}\"\n",
365                zoi_bin_str, "$PATH"
366            );
367            (path, cmd)
368        } else {
369            let path = home.join(".profile");
370            let cmd = format!(
371                "\n# Added by Zoi\nexport PATH=\"{}:{}\"\n",
372                zoi_bin_str, "$PATH"
373            );
374            (path, cmd)
375        };
376
377        if !profile_file_path.exists() {
378            if let Some(parent) = profile_file_path.parent() {
379                fs::create_dir_all(parent)?;
380            }
381            File::create(&profile_file_path)?;
382        }
383
384        let content = fs::read_to_string(&profile_file_path)?;
385        if content.contains(zoi_bin_str) {
386            println!("Zoi bin directory is already in your shell's config.");
387            return Ok(());
388        }
389
390        let mut file = OpenOptions::new().append(true).open(&profile_file_path)?;
391
392        file.write_all(cmd_to_write.as_bytes())?;
393
394        println!(
395            "{} Zoi bin directory has been added to your PATH in '{}'.",
396            "Success:".green(),
397            profile_file_path.display()
398        );
399        println!(
400            "Please restart your shell or run `source {}` for the changes to take effect.",
401            profile_file_path.display()
402        );
403    }
404
405    #[cfg(windows)]
406    {
407        use winreg::RegKey;
408        use winreg::enums::*;
409
410        let zoi_bin_path_str = zoi_bin_dir
411            .to_str()
412            .ok_or_else(|| anyhow!("Invalid path string"))?;
413
414        let (root, subkey, scope_name) = if scope == Scope::System {
415            if !is_admin() {
416                return Err(anyhow!(
417                    "Administrator privileges required to modify system PATH."
418                ));
419            }
420            (
421                HKEY_LOCAL_MACHINE,
422                "System\\CurrentControlSet\\Control\\Session Manager\\Environment",
423                "system",
424            )
425        } else {
426            (HKEY_CURRENT_USER, "Environment", "user")
427        };
428
429        let key = RegKey::predef(root);
430        let env = key.open_subkey_with_flags(subkey, KEY_READ | KEY_WRITE)?;
431        let current_path: String = env.get_value("Path")?;
432
433        if current_path
434            .split(';')
435            .any(|p| p.eq_ignore_ascii_case(zoi_bin_path_str))
436        {
437            println!("Zoi bin directory is already in your PATH.");
438            return Ok(());
439        }
440
441        let new_path = if current_path.is_empty() {
442            zoi_bin_path_str.to_string()
443        } else {
444            format!("{};{}", current_path, zoi_bin_path_str)
445        };
446        env.set_value("Path", &new_path)?;
447
448        println!(
449            "{} Zoi bin directory has been added to your {} PATH environment variable.",
450            "Success:".green(),
451            scope_name
452        );
453        println!(
454            "Please restart your shell or log out and log back in for the changes to take effect."
455        );
456    }
457
458    Ok(())
459}
460
461pub fn check_path() {
462    if let Some(home) = crate::pkg::utils::get_user_home() {
463        let zoi_bin_dir = crate::pkg::sysroot::apply_sysroot(home.join(".zoi/pkgs/bin"));
464        if !zoi_bin_dir.exists() {
465            return;
466        }
467    } else {
468        return;
469    }
470
471    let command_output = if cfg!(target_os = "windows") {
472        Command::new("pwsh")
473            .arg("-Command")
474            .arg("echo $env:Path")
475            .output()
476    } else {
477        Command::new("bash").arg("-c").arg("echo $PATH").output()
478    };
479
480    let is_in_path = match command_output {
481        Ok(output) => {
482            if output.status.success() {
483                let path_var = String::from_utf8_lossy(&output.stdout);
484                path_var.contains(".zoi/pkgs/bin")
485            } else {
486                false
487            }
488        }
489        Err(_) => false,
490    };
491
492    if !is_in_path {
493        eprintln!(
494            "Please run 'zoi shell <shell>' or add it to your PATH manually for commands to be available."
495        );
496    }
497}