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