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