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 if is_external_license_link(license) {
306 println!(
307 "{} The license is available at: {}",
308 "Warning:".yellow(),
309 license.cyan().bold()
310 );
311 return;
312 }
313
314 match spdx::Expression::parse(license) {
315 Ok(expr) => {
316 if !expr.evaluate(|req| match req.license {
317 spdx::LicenseItem::Spdx { id, .. } => id.is_osi_approved(),
318 spdx::LicenseItem::Other { .. } => false
319 }) {
320 println!(
321 "{} License expression '{}' does not evaluate to an OSI \
322 approved license.",
323 "Warning:".yellow(),
324 license.yellow().bold()
325 );
326 }
327 }
328 Err(_) => {
329 println!(
330 "{} Could not parse license expression '{}'. It may not be a \
331 valid SPDX identifier.",
332 "Warning:".yellow(),
333 license.yellow().bold()
334 );
335 }
336 }
337}
338
339fn is_external_license_link(license: &str) -> bool {
342 let trimmed = license.trim();
343 trimmed.starts_with("http://") || trimmed.starts_with("https://")
344}
345
346pub fn ask_for_confirmation(prompt: &str, yes: bool) -> bool {
348 if yes {
349 return true;
350 }
351
352 if std::env::var("ZOI_TEST").is_ok() || !stdin().is_tty() {
353 return false;
354 }
355
356 print!("{prompt} [y/N]: ");
357 let _ = stdout().flush();
358 let mut input = String::new();
359 if stdin().read_line(&mut input).is_err() {
360 return false;
361 }
362 input.trim().eq_ignore_ascii_case("y")
363}
364
365#[cfg(any(unix, test))]
370fn elvish_quote(value: &str) -> String {
371 value.replace('\\', "\\\\").replace('"', "\\\"")
372}
373
374pub fn setup_path(scope: Scope) -> anyhow::Result<()> {
386 if scope == Scope::Project {
387 return Ok(());
388 }
389
390 let zoi_bin_dir = match scope {
391 Scope::User => crate::pkg::utils::get_user_bin_dir()?,
392 Scope::System => crate::pkg::utils::get_system_bin_dir(),
393 Scope::Project => return Ok(())
394 };
395
396 if !zoi_bin_dir.exists() {
397 fs::create_dir_all(&zoi_bin_dir)?;
398 }
399
400 if scope == Scope::System && cfg!(unix) {
401 println!(
402 "{}",
403 "System-wide installation complete. Binaries are in the system \
404 PATH."
405 .green()
406 );
407 return Ok(());
408 }
409
410 #[cfg(unix)]
411 {
412 use std::fs::{File, OpenOptions};
413 let home = crate::pkg::utils::get_user_home()
414 .ok_or_else(|| anyhow!("Could not find home directory."))?;
415 let zoi_bin_str = zoi_bin_dir.to_string_lossy();
416
417 let shell_name = std::env::var("SHELL").unwrap_or_default();
418 let (profile_file_path, cmd_to_write) = if shell_name.contains("bash") {
419 let path = if cfg!(target_os = "macos") {
420 home.join(".bash_profile")
421 } else {
422 home.join(".bashrc")
423 };
424 let cmd = format!(
425 "\n# Added by Zoi\nexport PATH=\"{}:{}\"\n",
426 zoi_bin_str, "$PATH"
427 );
428 (path, cmd)
429 } else if shell_name.contains("zsh") {
430 let path = home.join(".zshrc");
431 let cmd = format!(
432 "\n# Added by Zoi\nexport PATH=\"{}:{}\"\n",
433 zoi_bin_str, "$PATH"
434 );
435 (path, cmd)
436 } else if shell_name.contains("fish") {
437 let path = home.join(".config/fish/config.fish");
438 let cmd =
439 format!("\n# Added by Zoi\nfish_add_path \"{zoi_bin_str}\"\n");
440 (path, cmd)
441 } else if shell_name.contains("elvish") {
442 let path = home.join(".config/elvish/rc.elv");
443 let cmd = format!(
446 "\n# Added by Zoi\nset paths = [ \"{}\" $paths... ]\n",
447 elvish_quote(&zoi_bin_str)
448 );
449 (path, cmd)
450 } else if shell_name.contains("csh") || shell_name.contains("tcsh") {
451 let path = home.join(".cshrc");
452 let cmd = format!(
453 "\n# Added by Zoi\nsetenv PATH=\"{}:{}\"\n",
454 zoi_bin_str, "$PATH"
455 );
456 (path, cmd)
457 } else {
458 let path = home.join(".profile");
459 let cmd = format!(
460 "\n# Added by Zoi\nexport PATH=\"{}:{}\"\n",
461 zoi_bin_str, "$PATH"
462 );
463 (path, cmd)
464 };
465
466 if !profile_file_path.exists() {
467 if let Some(parent) = profile_file_path.parent() {
468 fs::create_dir_all(parent)?;
469 }
470 File::create(&profile_file_path)?;
471 }
472
473 let content = fs::read_to_string(&profile_file_path)?;
474 if content.contains(zoi_bin_str.as_ref()) {
475 println!("Zoi bin directory is already in your shell's config.");
476 return Ok(());
477 }
478
479 let mut file =
480 OpenOptions::new().append(true).open(&profile_file_path)?;
481
482 file.write_all(cmd_to_write.as_bytes())?;
483
484 println!(
485 "{} Zoi bin directory has been added to your PATH in '{}'.",
486 "Success:".green(),
487 profile_file_path.display()
488 );
489 println!(
490 "Please restart your shell or run `source {}` for the changes to \
491 take effect.",
492 profile_file_path.display()
493 );
494 }
495
496 #[cfg(windows)]
497 {
498 use winreg::RegKey;
499 use winreg::enums::*;
500
501 let zoi_bin_path_str = zoi_bin_dir
502 .to_str()
503 .ok_or_else(|| anyhow!("Invalid path string"))?;
504
505 let (root, subkey, scope_name) = if scope == Scope::System {
506 if !is_admin() {
507 return Err(anyhow!(
508 "Administrator privileges required to modify system PATH."
509 ));
510 }
511 (
512 HKEY_LOCAL_MACHINE,
513 "System\\CurrentControlSet\\Control\\Session \
514 Manager\\Environment",
515 "system"
516 )
517 } else {
518 (HKEY_CURRENT_USER, "Environment", "user")
519 };
520
521 let key = RegKey::predef(root);
522 let env = key.open_subkey_with_flags(subkey, KEY_READ | KEY_WRITE)?;
523 let current_path: String = env.get_value("Path")?;
524
525 if current_path
526 .split(';')
527 .any(|p| p.eq_ignore_ascii_case(zoi_bin_path_str))
528 {
529 println!("Zoi bin directory is already in your PATH.");
530 return Ok(());
531 }
532
533 let new_path = if current_path.is_empty() {
534 zoi_bin_path_str.to_string()
535 } else {
536 format!("{};{}", current_path, zoi_bin_path_str)
537 };
538 env.set_value("Path", &new_path)?;
539
540 println!(
541 "{} Zoi bin directory has been added to your {} PATH environment \
542 variable.",
543 "Success:".green(),
544 scope_name
545 );
546 println!(
547 "Please restart your shell or log out and log back in for the \
548 changes to take effect."
549 );
550 }
551
552 Ok(())
553}
554
555pub fn check_path() {
558 let Ok(zoi_bin_dir) = crate::pkg::utils::get_user_bin_dir() else {
559 return;
560 };
561 if !zoi_bin_dir.exists() {
562 return;
563 }
564
565 let command_output = if cfg!(target_os = "windows") {
566 Command::new("pwsh")
567 .arg("-Command")
568 .arg("echo $env:Path")
569 .output()
570 } else {
571 Command::new("bash").arg("-c").arg("echo $PATH").output()
572 };
573
574 let is_in_path = match command_output {
575 Ok(output) => {
576 if output.status.success() {
577 let path_var = String::from_utf8_lossy(&output.stdout);
578 std::env::split_paths(path_var.as_ref())
579 .any(|path| path == zoi_bin_dir)
580 } else {
581 false
582 }
583 }
584 Err(_) => false
585 };
586
587 if !is_in_path {
588 eprintln!(
589 "Please run 'zoi shell <shell>' or add it to your PATH manually \
590 for commands to be available."
591 );
592 }
593}