1use std::fmt::Display;
4use std::fs;
5use std::io::{Write, stdin, stdout};
6use std::path::Path;
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!("{prompt} [y/N]: ");
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
349fn elvish_quote(value: &str) -> String {
354 value.replace('\\', "\\\\").replace('"', "\\\"")
355}
356
357pub fn setup_path(scope: Scope) -> anyhow::Result<()> {
369 if scope == Scope::Project {
370 return Ok(());
371 }
372
373 let zoi_bin_dir = match scope {
374 Scope::User => crate::pkg::utils::get_user_bin_dir()?,
375 Scope::System => crate::pkg::utils::get_system_bin_dir(),
376 Scope::Project => return Ok(())
377 };
378
379 if !zoi_bin_dir.exists() {
380 fs::create_dir_all(&zoi_bin_dir)?;
381 }
382
383 if scope == Scope::System && cfg!(unix) {
384 println!(
385 "{}",
386 "System-wide installation complete. Binaries are in the system \
387 PATH."
388 .green()
389 );
390 return Ok(());
391 }
392
393 #[cfg(unix)]
394 {
395 use std::fs::{File, OpenOptions};
396 let home = crate::pkg::utils::get_user_home()
397 .ok_or_else(|| anyhow!("Could not find home directory."))?;
398 let zoi_bin_str = zoi_bin_dir.to_string_lossy();
399
400 let shell_name = std::env::var("SHELL").unwrap_or_default();
401 let (profile_file_path, cmd_to_write) = if shell_name.contains("bash") {
402 let path = if cfg!(target_os = "macos") {
403 home.join(".bash_profile")
404 } else {
405 home.join(".bashrc")
406 };
407 let cmd = format!(
408 "\n# Added by Zoi\nexport PATH=\"{}:{}\"\n",
409 zoi_bin_str, "$PATH"
410 );
411 (path, cmd)
412 } else if shell_name.contains("zsh") {
413 let path = home.join(".zshrc");
414 let cmd = format!(
415 "\n# Added by Zoi\nexport PATH=\"{}:{}\"\n",
416 zoi_bin_str, "$PATH"
417 );
418 (path, cmd)
419 } else if shell_name.contains("fish") {
420 let path = home.join(".config/fish/config.fish");
421 let cmd =
422 format!("\n# Added by Zoi\nfish_add_path \"{zoi_bin_str}\"\n");
423 (path, cmd)
424 } else if shell_name.contains("elvish") {
425 let path = home.join(".config/elvish/rc.elv");
426 let cmd = format!(
429 "\n# Added by Zoi\nset paths = [ \"{}\" $paths... ]\n",
430 elvish_quote(&zoi_bin_str)
431 );
432 (path, cmd)
433 } else if shell_name.contains("csh") || shell_name.contains("tcsh") {
434 let path = home.join(".cshrc");
435 let cmd = format!(
436 "\n# Added by Zoi\nsetenv PATH=\"{}:{}\"\n",
437 zoi_bin_str, "$PATH"
438 );
439 (path, cmd)
440 } else {
441 let path = home.join(".profile");
442 let cmd = format!(
443 "\n# Added by Zoi\nexport PATH=\"{}:{}\"\n",
444 zoi_bin_str, "$PATH"
445 );
446 (path, cmd)
447 };
448
449 if !profile_file_path.exists() {
450 if let Some(parent) = profile_file_path.parent() {
451 fs::create_dir_all(parent)?;
452 }
453 File::create(&profile_file_path)?;
454 }
455
456 let content = fs::read_to_string(&profile_file_path)?;
457 if content.contains(zoi_bin_str.as_ref()) {
458 println!("Zoi bin directory is already in your shell's config.");
459 return Ok(());
460 }
461
462 let mut file =
463 OpenOptions::new().append(true).open(&profile_file_path)?;
464
465 file.write_all(cmd_to_write.as_bytes())?;
466
467 println!(
468 "{} Zoi bin directory has been added to your PATH in '{}'.",
469 "Success:".green(),
470 profile_file_path.display()
471 );
472 println!(
473 "Please restart your shell or run `source {}` for the changes to \
474 take effect.",
475 profile_file_path.display()
476 );
477 }
478
479 #[cfg(windows)]
480 {
481 use winreg::RegKey;
482 use winreg::enums::*;
483
484 let zoi_bin_path_str = zoi_bin_dir
485 .to_str()
486 .ok_or_else(|| anyhow!("Invalid path string"))?;
487
488 let (root, subkey, scope_name) = if scope == Scope::System {
489 if !is_admin() {
490 return Err(anyhow!(
491 "Administrator privileges required to modify system PATH."
492 ));
493 }
494 (
495 HKEY_LOCAL_MACHINE,
496 "System\\CurrentControlSet\\Control\\Session \
497 Manager\\Environment",
498 "system"
499 )
500 } else {
501 (HKEY_CURRENT_USER, "Environment", "user")
502 };
503
504 let key = RegKey::predef(root);
505 let env = key.open_subkey_with_flags(subkey, KEY_READ | KEY_WRITE)?;
506 let current_path: String = env.get_value("Path")?;
507
508 if current_path
509 .split(';')
510 .any(|p| p.eq_ignore_ascii_case(zoi_bin_path_str))
511 {
512 println!("Zoi bin directory is already in your PATH.");
513 return Ok(());
514 }
515
516 let new_path = if current_path.is_empty() {
517 zoi_bin_path_str.to_string()
518 } else {
519 format!("{};{}", current_path, zoi_bin_path_str)
520 };
521 env.set_value("Path", &new_path)?;
522
523 println!(
524 "{} Zoi bin directory has been added to your {} PATH environment \
525 variable.",
526 "Success:".green(),
527 scope_name
528 );
529 println!(
530 "Please restart your shell or log out and log back in for the \
531 changes to take effect."
532 );
533 }
534
535 Ok(())
536}
537
538pub fn check_path() {
541 let Ok(zoi_bin_dir) = crate::pkg::utils::get_user_bin_dir() else {
542 return;
543 };
544 if !zoi_bin_dir.exists() {
545 return;
546 }
547
548 let command_output = if cfg!(target_os = "windows") {
549 Command::new("pwsh")
550 .arg("-Command")
551 .arg("echo $env:Path")
552 .output()
553 } else {
554 Command::new("bash").arg("-c").arg("echo $PATH").output()
555 };
556
557 let is_in_path = match command_output {
558 Ok(output) => {
559 if output.status.success() {
560 let path_var = String::from_utf8_lossy(&output.stdout);
561 std::env::split_paths(path_var.as_ref())
562 .any(|path| path == zoi_bin_dir)
563 } else {
564 false
565 }
566 }
567 Err(_) => false
568 };
569
570 if !is_in_path {
571 eprintln!(
572 "Please run 'zoi shell <shell>' or add it to your PATH manually \
573 for commands to be available."
574 );
575 }
576}