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