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