1use crate::pkg::resolve::SourceType;
2use anyhow::anyhow;
3use colored::*;
4use std::fmt::Display;
5use std::fs;
6use std::io::{Write, stdin, stdout};
7use std::process::Command;
8use std::time::Duration;
9use walkdir::WalkDir;
10
11pub fn format_bytes(bytes: u64) -> String {
12 const KIB: u64 = 1024;
13 const MIB: u64 = 1024 * KIB;
14 const GIB: u64 = 1024 * MIB;
15
16 if bytes >= GIB {
17 format!("{:.2} GiB", bytes as f64 / GIB as f64)
18 } else if bytes >= MIB {
19 format!("{:.2} MiB", bytes as f64 / MIB as f64)
20 } else if bytes >= KIB {
21 format!("{:.2} KiB", bytes as f64 / KIB as f64)
22 } else {
23 format!("{} B", bytes)
24 }
25}
26
27pub fn format_size_diff(diff: i64) -> String {
28 if diff == 0 {
29 return "0 B".to_string();
30 }
31
32 let sign = if diff > 0 { "+" } else { "-" };
33 let bytes = diff.unsigned_abs();
34
35 format!("{} {}", sign, format_bytes(bytes))
36}
37
38use crate::pkg::types::Scope;
39use clap_complete::Shell;
40use std::path::{Path, PathBuf};
41
42pub fn copy_dir_all(src: &Path, dst: &Path) -> std::io::Result<()> {
43 fs::create_dir_all(dst)?;
44 for entry in fs::read_dir(src)? {
45 let entry = entry?;
46 let ty = entry.file_type()?;
47 if ty.is_dir() {
48 copy_dir_all(&entry.path(), &dst.join(entry.file_name()))?;
49 } else {
50 fs::copy(entry.path(), dst.join(entry.file_name()))?;
51 }
52 }
53 Ok(())
54}
55
56pub fn is_admin() -> bool {
57 #[cfg(windows)]
58 {
59 use std::mem;
60 use std::ptr;
61 use winapi::um::handleapi::CloseHandle;
62 use winapi::um::processthreadsapi::GetCurrentProcess;
63 use winapi::um::processthreadsapi::OpenProcessToken;
64 use winapi::um::securitybaseapi::CheckTokenMembership;
65 use winapi::um::winnt::{PSID, TOKEN_QUERY};
66
67 let mut token = ptr::null_mut();
68 let process = unsafe { GetCurrentProcess() };
69 if unsafe { OpenProcessToken(process, TOKEN_QUERY, &mut token) } == 0 {
70 return false;
71 }
72
73 let mut sid: [u8; 8] = [0; 8];
74 let mut sid_size = mem::size_of_val(&sid) as u32;
75 if unsafe {
76 winapi::um::securitybaseapi::CreateWellKnownSid(
77 winapi::um::winnt::WinBuiltinAdministratorsSid,
78 ptr::null_mut(),
79 sid.as_mut_ptr() as PSID,
80 &mut sid_size,
81 )
82 } == 0
83 {
84 unsafe { CloseHandle(token) };
85 return false;
86 }
87
88 let mut is_member = 0;
89 let result =
90 unsafe { CheckTokenMembership(token, sid.as_mut_ptr() as PSID, &mut is_member) };
91 unsafe { CloseHandle(token) };
92
93 result != 0 && is_member != 0
94 }
95 #[cfg(unix)]
96 {
97 nix::unistd::getuid().is_root()
98 }
99}
100
101pub fn print_info<T: Display>(key: &str, value: T) {
102 println!("{}: {}", key, value);
103}
104
105pub fn format_version_summary(branch: &str, status: &str, number: &str) -> String {
106 let branch_short = if branch == "Production" {
107 "Prod."
108 } else if branch == "Development" {
109 "Dev."
110 } else if branch == "Public" {
111 "Pub."
112 } else if branch == "Special" {
113 "Spec."
114 } else {
115 branch
116 };
117 format!(
118 "{} {} {}",
119 branch_short.blue().bold().italic(),
120 status,
121 number,
122 )
123}
124
125pub fn format_version_full(branch: &str, status: &str, number: &str, commit: &str) -> String {
126 format!(
127 "{} {}",
128 format_version_summary(branch, status, number),
129 commit.green()
130 )
131}
132
133pub fn print_aligned_info(key: &str, value: &str) {
134 let key_with_colon = format!("{}:", key);
135 println!("{:<18}{}", key_with_colon.cyan(), value);
136}
137
138pub fn command_exists(command: &str) -> bool {
139 if cfg!(target_os = "windows") {
140 Command::new("where")
141 .arg(command)
142 .stdout(std::process::Stdio::null())
143 .stderr(std::process::Stdio::null())
144 .status()
145 .is_ok_and(|status| status.success())
146 } else {
147 Command::new("bash")
148 .arg("-c")
149 .arg(format!("command -v {}", command))
150 .stdout(std::process::Stdio::null())
151 .stderr(std::process::Stdio::null())
152 .status()
153 .is_ok_and(|status| status.success())
154 }
155}
156
157pub fn ask_for_confirmation(prompt: &str, yes: bool) -> bool {
158 if yes {
159 return true;
160 }
161 print!("{} [y/N]: ", prompt.yellow());
162 let _ = stdout().flush();
163 let mut input = String::new();
164 if stdin().read_line(&mut input).is_err() {
165 return false;
166 }
167 input.trim().eq_ignore_ascii_case("y")
168}
169
170pub fn set_path_read_only(path: &Path) -> anyhow::Result<()> {
171 if !path.exists() {
172 return Ok(());
173 }
174 for entry in WalkDir::new(path) {
175 let entry = entry?;
176 let mut perms = fs::metadata(entry.path())?.permissions();
177 if !perms.readonly() {
178 perms.set_readonly(true);
179 fs::set_permissions(entry.path(), perms)?;
180 }
181 }
182 Ok(())
183}
184
185pub fn set_path_writable(path: &Path) -> anyhow::Result<()> {
186 if !path.exists() {
187 return Ok(());
188 }
189 for entry in WalkDir::new(path) {
190 let entry = entry?;
191 let mut perms = fs::metadata(entry.path())?.permissions();
192 if perms.readonly() {
193 #[cfg(unix)]
194 {
195 use std::os::unix::fs::PermissionsExt;
196 let mode = perms.mode();
197 perms.set_mode(mode | 0o200);
198 }
199 #[cfg(not(unix))]
200 {
201 perms.set_readonly(false);
202 }
203 fs::set_permissions(entry.path(), perms)?;
204 }
205 }
206 Ok(())
207}
208
209use std::collections::HashMap;
210
211pub fn get_linux_distribution_info() -> Option<HashMap<String, String>> {
212 if let Ok(contents) = fs::read_to_string("/etc/os-release") {
213 let info: HashMap<String, String> = contents
214 .lines()
215 .filter_map(|line| {
216 let mut parts = line.splitn(2, '=');
217 let key = parts.next()?;
218 let value = parts.next()?.trim_matches('"').to_string();
219 if key.is_empty() {
220 None
221 } else {
222 Some((key.to_string(), value))
223 }
224 })
225 .collect();
226 if info.is_empty() { None } else { Some(info) }
227 } else {
228 None
229 }
230}
231
232pub fn get_linux_distro_family() -> Option<String> {
233 if let Some(info) = get_linux_distribution_info() {
234 if let Some(id_like) = info.get("ID_LIKE") {
235 let families: Vec<&str> = id_like.split_whitespace().collect();
236 if families.contains(&"debian") {
237 return Some("debian".to_string());
238 }
239 if families.contains(&"arch") {
240 return Some("arch".to_string());
241 }
242 if families.contains(&"fedora") {
243 return Some("fedora".to_string());
244 }
245 if families.contains(&"rhel") {
246 return Some("fedora".to_string());
247 }
248 if families.contains(&"suse") {
249 return Some("suse".to_string());
250 }
251 if families.contains(&"gentoo") {
252 return Some("gentoo".to_string());
253 }
254 }
255 if let Some(id) = info.get("ID") {
256 return match id.as_str() {
257 "debian" | "ubuntu" | "linuxmint" | "pop" | "kali" | "kubuntu" | "lubuntu"
258 | "xubuntu" | "zorin" | "elementary" => Some("debian".to_string()),
259 "arch" | "manjaro" | "cachyos" | "endeavouros" | "garuda" => {
260 Some("arch".to_string())
261 }
262 "fedora" | "centos" | "rhel" | "rocky" | "almalinux" => Some("fedora".to_string()),
263 "opensuse" | "opensuse-tumbleweed" | "opensuse-leap" => Some("suse".to_string()),
264 "gentoo" => Some("gentoo".to_string()),
265 "alpine" => Some("alpine".to_string()),
266 "void" => Some("void".to_string()),
267 "solus" => Some("solus".to_string()),
268 "guix" => Some("guix".to_string()),
269 _ => None,
270 };
271 }
272 }
273 None
274}
275
276pub fn get_linux_distribution() -> Option<String> {
277 get_linux_distribution_info().and_then(|info| info.get("ID").cloned())
278}
279
280pub fn get_native_package_manager() -> Option<String> {
281 let os = std::env::consts::OS;
282 match os {
283 "linux" => get_linux_distro_family()
284 .map(|family| {
285 match family.as_str() {
286 "debian" => "apt",
287 "arch" => "pacman",
288 "fedora" => "dnf",
289 "suse" => "zypper",
290 "gentoo" => "portage",
291 "alpine" => "apk",
292 "void" => "xbps-install",
293 "solus" => "eopkg",
294 "guix" => "guix",
295 _ => "unknown",
296 }
297 .to_string()
298 })
299 .filter(|s| s != "unknown"),
300 "macos" => {
301 if command_exists("brew") {
302 Some("brew".to_string())
303 } else if command_exists("port") {
304 Some("macports".to_string())
305 } else {
306 None
307 }
308 }
309 "windows" => {
310 if command_exists("scoop") {
311 Some("scoop".to_string())
312 } else if command_exists("choco") {
313 Some("choco".to_string())
314 } else if command_exists("winget") {
315 Some("winget".to_string())
316 } else {
317 None
318 }
319 }
320 "freebsd" => Some("pkg".to_string()),
321 "openbsd" => Some("pkg_add".to_string()),
322 _ => None,
323 }
324}
325
326pub fn print_repo_warning(repo_name: &str) {
327 if let Ok(db_path) = crate::pkg::resolve::get_db_root()
328 && let Ok(repo_config) = crate::pkg::config::read_repo_config(&db_path)
329 {
330 let major_repo = repo_name.split('/').next().unwrap_or("");
331 if let Some(repo_entry) = repo_config.repos.iter().find(|r| r.name == major_repo) {
332 let warning_message = match repo_entry.repo_type.as_str() {
333 "unoffical" => {
334 Some("This package is from an unofficial repository and is not trusted.")
335 }
336 "community" => {
337 Some("This package is from a community repository. Use with caution.")
338 }
339 "test" => Some(
340 "This package is from a testing repository and may not function correctly.",
341 ),
342 "archive" => {
343 Some("This package is from an archive repository and is no longer maintained.")
344 }
345 _ => None,
346 };
347
348 if let Some(message) = warning_message {
349 println!("\n{}: {}", "NOTE".yellow().bold(), message.yellow());
350 }
351 }
352 }
353}
354
355pub fn confirm_untrusted_source(source_type: &SourceType, yes: bool) -> anyhow::Result<()> {
356 if source_type == &SourceType::OfficialRepo {
357 return Ok(());
358 }
359
360 let warning_message = match source_type {
361 SourceType::UntrustedRepo(repo) => {
362 format!(
363 "The package from repository '@{}' is not an official Zoi repository.",
364 repo
365 )
366 }
367 SourceType::LocalFile => "You are installing from a local file.".to_string(),
368 SourceType::Url => "You are installing from a remote URL.".to_string(),
369 _ => return Ok(()),
370 };
371
372 println!(
373 "\n{}: {}",
374 "SECURITY WARNING".yellow().bold(),
375 warning_message
376 );
377
378 if ask_for_confirmation(
379 "This source is not trusted. Are you sure you want to continue?",
380 yes,
381 ) {
382 Ok(())
383 } else {
384 Err(anyhow!("Operation aborted by user."))
385 }
386}
387
388pub fn is_platform_compatible(current_platform: &str, allowed_platforms: &[String]) -> bool {
389 let os = match std::env::consts::OS {
390 "darwin" => "macos",
391 other => other,
392 };
393 allowed_platforms
394 .iter()
395 .any(|p| p == "all" || p == os || p == current_platform)
396}
397
398pub fn setup_path(scope: Scope) -> anyhow::Result<()> {
399 if scope == Scope::Project {
400 return Ok(());
401 }
402
403 let zoi_bin_dir = match scope {
404 Scope::User => home::home_dir()
405 .ok_or_else(|| anyhow!("Could not find home directory."))?
406 .join(".zoi")
407 .join("pkgs")
408 .join("bin"),
409 Scope::System => {
410 if cfg!(target_os = "windows") {
411 PathBuf::from("C:\\ProgramData\\zoi\\pkgs\\bin")
412 } else {
413 PathBuf::from("/usr/local/bin")
414 }
415 }
416 Scope::Project => return Ok(()),
417 };
418
419 if !zoi_bin_dir.exists() {
420 fs::create_dir_all(&zoi_bin_dir)?;
421 }
422
423 if scope == Scope::System {
424 println!(
425 "{}",
426 "System-wide installation complete. Binaries are in the system PATH.".green()
427 );
428 return Ok(());
429 }
430
431 println!("{}", "Ensuring Zoi bin directory is in your PATH...".bold());
432
433 #[cfg(unix)]
434 {
435 use std::fs::{File, OpenOptions};
436 let home = home::home_dir().ok_or_else(|| anyhow!("Could not find home directory."))?;
437 let zoi_bin_str = "$HOME/.zoi/pkgs/bin";
438
439 let shell_name = std::env::var("SHELL").unwrap_or_default();
440 let (profile_file_path, cmd_to_write) = if shell_name.contains("bash") {
441 let path = if cfg!(target_os = "macos") {
442 home.join(".bash_profile")
443 } else {
444 home.join(".bashrc")
445 };
446 let cmd = format!(
447 "\n# Added by Zoi\nexport PATH=\"{}:{}\"\n",
448 zoi_bin_str, "$PATH"
449 );
450 (path, cmd)
451 } else if shell_name.contains("zsh") {
452 let path = home.join(".zshrc");
453 let cmd = format!(
454 "\n# Added by Zoi\nexport PATH=\"{}:{}\"\n",
455 zoi_bin_str, "$PATH"
456 );
457 (path, cmd)
458 } else if shell_name.contains("fish") {
459 let path = home.join(".config/fish/config.fish");
460 let cmd = format!("\n# Added by Zoi\nset -gx PATH \"{}\" $PATH\n", zoi_bin_str);
461
462 (path, cmd)
463 } else if shell_name.contains("elvish") {
464 let path = home.join(".config/elvish/rc.elv");
465 let cmd = "
466# Added by Zoi
467set paths = [ ~/.zoi/pkgs/bin $paths... ]
468"
469 .to_string();
470 (path, cmd)
471 } else if shell_name.contains("csh") || shell_name.contains("tcsh") {
472 let path = home.join(".cshrc");
473 let cmd = format!(
474 "\n# Added by Zoi\nsetenv PATH=\"{}:{}\"\n",
475 zoi_bin_str, "$PATH"
476 );
477 (path, cmd)
478 } else {
479 let path = home.join(".profile");
480 let cmd = format!(
481 "\n# Added by Zoi\nexport PATH=\"{}:{}\"\n",
482 zoi_bin_str, "$PATH"
483 );
484 (path, cmd)
485 };
486
487 if !profile_file_path.exists() {
488 if let Some(parent) = profile_file_path.parent() {
489 fs::create_dir_all(parent)?;
490 }
491 File::create(&profile_file_path)?;
492 }
493
494 let content = fs::read_to_string(&profile_file_path)?;
495 if content.contains(zoi_bin_str) {
496 println!("Zoi bin directory is already in your shell's config.");
497 return Ok(());
498 }
499
500 let mut file = OpenOptions::new().append(true).open(&profile_file_path)?;
501
502 file.write_all(cmd_to_write.as_bytes())?;
503
504 println!(
505 "{} Zoi bin directory has been added to your PATH in '{}'.",
506 "Success:".green(),
507 profile_file_path.display()
508 );
509 println!(
510 "Please restart your shell or run `source {}` for the changes to take effect.",
511 profile_file_path.display()
512 );
513 }
514
515 #[cfg(windows)]
516 {
517 use winreg::RegKey;
518 use winreg::enums::*;
519
520 let zoi_bin_path_str = zoi_bin_dir
521 .to_str()
522 .ok_or_else(|| anyhow!("Invalid path string"))?;
523
524 let hkcu = RegKey::predef(HKEY_CURRENT_USER);
525 let env = hkcu.open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)?;
526 let current_path: String = env.get_value("Path")?;
527
528 if current_path
529 .split(';')
530 .any(|p| p.eq_ignore_ascii_case(zoi_bin_path_str))
531 {
532 println!("Zoi bin directory is already in your PATH.");
533 return Ok(());
534 }
535
536 let new_path = if current_path.is_empty() {
537 zoi_bin_path_str.to_string()
538 } else {
539 format!("{};{}", current_path, zoi_bin_path_str)
540 };
541 env.set_value("Path", &new_path)?;
542
543 println!(
544 "{} Zoi bin directory has been added to your user PATH environment variable.",
545 "Success:".green()
546 );
547 println!(
548 "Please restart your shell or log out and log back in for the changes to take effect."
549 );
550 }
551
552 Ok(())
553}
554
555pub fn check_path() {
556 if let Some(home) = home::home_dir() {
557 let zoi_bin_dir = home.join(".zoi/pkgs/bin");
558 if !zoi_bin_dir.exists() {
559 return;
560 }
561 } else {
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 path_var.contains(".zoi/pkgs/bin")
579 } else {
580 false
581 }
582 }
583 Err(_) => false,
584 };
585
586 if !is_in_path {
587 eprintln!(
588 "Please run 'zoi setup --scope user' or add it to your PATH manually for commands to be available."
589 );
590 }
591}
592
593pub fn get_platform() -> anyhow::Result<String> {
594 let os = match std::env::consts::OS {
595 "linux" => "linux",
596 "macos" | "darwin" => "macos",
597 "windows" => "windows",
598 "freebsd" => "freebsd",
599 "openbsd" => "openbsd",
600 unsupported_os => return Err(anyhow!("Unsupported operating system: {}", unsupported_os)),
601 };
602
603 let arch = match std::env::consts::ARCH {
604 "x86_64" => "amd64",
605 "aarch64" => "arm64",
606 unsupported_arch => return Err(anyhow!("Unsupported architecture: {}", unsupported_arch)),
607 };
608
609 Ok(format!("{}-{}", os, arch))
610}
611
612pub fn get_all_available_package_managers() -> Vec<String> {
613 let mut managers = Vec::new();
614 let all_possible_managers = [
615 "apt",
616 "apt-get",
617 "pacman",
618 "yay",
619 "paru",
620 "pikaur",
621 "trizen",
622 "dnf",
623 "yum",
624 "zypper",
625 "portage",
626 "apk",
627 "snap",
628 "flatpak",
629 "nix",
630 "brew",
631 "port",
632 "scoop",
633 "choco",
634 "winget",
635 "pkg",
636 "pkg_add",
637 "xbps-install",
638 "eopkg",
639 "guix",
640 "mas",
641 ];
642
643 for manager in &all_possible_managers {
644 if command_exists(manager) {
645 managers.push(manager.to_string());
646 }
647 }
648 managers.sort();
649 managers.dedup();
650 managers
651}
652
653pub fn build_blocking_http_client(timeout_secs: u64) -> anyhow::Result<reqwest::blocking::Client> {
654 let client = reqwest::blocking::Client::builder()
655 .timeout(Duration::from_secs(timeout_secs))
656 .build()?;
657 Ok(client)
658}
659
660pub fn retry_backoff_sleep(attempt: u32) {
661 let base_ms = 500u64.saturating_mul(1u64 << (attempt.saturating_sub(1)));
662 let jitter = (std::time::SystemTime::now()
663 .duration_since(std::time::UNIX_EPOCH)
664 .unwrap_or(Duration::from_secs(0))
665 .subsec_millis()
666 % 200) as u64;
667 let sleep_ms = (base_ms + jitter).min(8000);
668 std::thread::sleep(Duration::from_millis(sleep_ms));
669}
670
671pub fn check_license(license: &str) {
672 if license.is_empty() {
673 println!(
674 "{}",
675 "Warning: Package does not have a license specified.".yellow()
676 );
677 return;
678 }
679
680 if license.eq_ignore_ascii_case("Proprietary") {
681 println!(
682 "{}",
683 "Warning: Package is using a proprietary license.".red()
684 );
685 return;
686 }
687
688 if license.eq_ignore_ascii_case("Unkown") {
689 println!("{}", "Warning: Package license is unkown.".red());
690 return;
691 }
692
693 match spdx::Expression::parse(license) {
694 Ok(expr) => {
695 if !expr.evaluate(|req| match req.license {
696 spdx::LicenseItem::Spdx { id, .. } => id.is_osi_approved(),
697 spdx::LicenseItem::Other { .. } => false,
698 }) {
699 println!(
700 "{}{}{}",
701 "Warning: License '".yellow(),
702 license.yellow().bold(),
703 "' is not an OSI approved license.".yellow()
704 );
705 }
706 }
707 Err(_) => {
708 println!(
709 "{}{}{}",
710 "Warning: Could not parse license expression '".yellow(),
711 license.yellow().bold(),
712 "' It may not be a valid SPDX identifier.".yellow()
713 );
714 }
715 }
716}
717
718#[derive(serde::Deserialize)]
719struct PackageForCompletion {
720 description: Option<String>,
721}
722
723pub struct PackageCompletion {
724 pub display: String,
725 pub repo: String,
726 pub description: String,
727}
728
729pub fn get_all_packages_for_completion() -> Vec<PackageCompletion> {
730 let db_root = if let Ok(path) = crate::pkg::resolve::get_db_root() {
731 path
732 } else {
733 return Vec::new();
734 };
735
736 let active_repos = if let Ok(config) = crate::pkg::config::read_config() {
737 config.repos
738 } else {
739 return Vec::new();
740 };
741
742 if !db_root.exists() {
743 return Vec::new();
744 }
745
746 let mut packages = Vec::new();
747 for repo_name in &active_repos {
748 let repo_path = db_root.join(repo_name);
749 if !repo_path.is_dir() {
750 continue;
751 }
752 for entry in WalkDir::new(&repo_path)
753 .into_iter()
754 .filter_map(|e| e.ok())
755 .filter(|e| e.file_type().is_dir())
756 {
757 let pkg_name = entry.file_name().to_string_lossy();
758 let pkg_file_path = entry.path().join(format!("{}.pkg.lua", pkg_name));
759
760 if pkg_file_path.is_file() {
761 let pkg_info: anyhow::Result<PackageForCompletion> = (|| -> anyhow::Result<_> {
762 let pkg = crate::pkg::lua::parser::parse_lua_package(
763 pkg_file_path.to_str().unwrap(),
764 None,
765 true,
766 )?;
767 Ok(PackageForCompletion {
768 description: Some(pkg.description),
769 })
770 })();
771
772 let description = match pkg_info {
773 Ok(pi) => pi.description.unwrap_or_default(),
774 Err(_) => String::new(),
775 };
776
777 let relative_path = entry.path().strip_prefix(&db_root).unwrap();
778 let full_pkg_id =
779 format!("@{}", relative_path.to_string_lossy().replace('\\', "/"));
780
781 packages.push(PackageCompletion {
782 display: full_pkg_id,
783 repo: repo_name.clone(),
784 description,
785 });
786 }
787 }
788 }
789 packages.sort_by(|a, b| a.display.cmp(&b.display));
790 packages
791}
792
793pub fn get_current_shell() -> Option<Shell> {
794 if cfg!(windows) {
795 return Some(Shell::PowerShell);
796 }
797
798 if let Ok(shell_path) = std::env::var("SHELL") {
799 let shell_name = Path::new(&shell_path).file_name()?.to_str()?;
800 match shell_name {
801 "bash" => Some(Shell::Bash),
802 "zsh" => Some(Shell::Zsh),
803 "fish" => Some(Shell::Fish),
804 "elvish" => Some(Shell::Elvish),
805 "pwsh" => Some(Shell::PowerShell),
806 _ => None,
807 }
808 } else {
809 None
810 }
811}