Ctrl K
ComunidadGuías…

src/distro.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// SPDX-License-Identifier: GPL-3.0-or-later
/**
 * Distro detection, in one place. Parses /etc/os-release fields (uppercase on
 * real systems, lowercase in test stubs) into a normalized profile that every
 * check can rely on instead of doing its own string matching.
 */
export function detectDistro(osRelease = {}) {
  const get = (k) =>
    String(osRelease[k] || osRelease[k.toUpperCase()] || osRelease[k.toLowerCase()] || "").toLowerCase();
  const id = get("id");
  const idLike = get("id_like");
  const variant = get("variant");
  const all = `${id} ${idLike} ${variant}`;

  // Image-based (ostree/atomic) distros update atomically via rpm-ostree,
  // and their root filesystem is virtual. Checked before family detection.
  const imageBased = /bazzite|silverblue|kinoite|sericea|bluefin|atomic|ostree/.test(all);

  // The specific atomic variant, so messages can name the distro ("Bazzite"
  // instead of a generic "immutable system"). null on classic distros.
  const variantMatch = all.match(/(bazzite|silverblue|kinoite|sericea|bluefin|aurora|cosmic-atomic|ublue|bootc)/);
  const atomicVariant = variantMatch ? variantMatch[1] : (imageBased ? "atomic" : null);

  let family = "other";
  if (id === "alpine") family = "alpine";
  else if (id === "void") family = "void";
  else if (id === "gentoo") family = "gentoo";
  else if (id === "arch" || /manjaro|endeavouros/.test(id) || /arch/.test(idLike)) family = "arch";
  else if (/suse|opensuse/.test(`${id} ${idLike}`)) family = "suse";
  else if (id === "debian" || id === "ubuntu" || /debian|ubuntu/.test(idLike)) family = "debian";
  else if (/fedora|rhel|centos|rocky|almalinux|nobara|bazzite|silverblue|kinoite/.test(all)) family = "fedora";

  let pkg = "unknown";
  if (imageBased) pkg = "rpm-ostree";
  else if (family === "fedora") pkg = "dnf";
  else if (family === "debian") pkg = "apt";
  else if (family === "arch") pkg = "pacman";
  else if (family === "suse") pkg = "zypper";
  else if (family === "alpine") pkg = "apk";
  else if (family === "void") pkg = "xbps";
  else if (family === "gentoo") pkg = "emerge";

  // Init system — the commands a fix may suggest for services. Alpine and
  // Gentoo run OpenRC, Void runs runit, everything else here is systemd.
  let init = "systemd";
  if (family === "alpine" || family === "gentoo") init = "openrc";
  else if (family === "void") init = "runit";

  return { id, idLike, variant, family, pkg, imageBased, atomicVariant, init, all };
}

/**
 * Central distro-specific install helper — single source of truth for fix strings.
 * `pkgs` can be:
 *   - a string ("pipewire") or array (["podman","buildah"]) when the package
 *     has the same name everywhere, or
 *   - an object mapping package-manager/family → name for packages that are
 *     named differently across distros ({ fedora: "procps-ng", "*": "procps" }).
 *     Keys are matched against dist.pkg first (dnf/apt/pacman/zypper/apk/
 *     rpm-ostree), then dist.family, then "*" as the fallback.
 * Returns a copy-pasteable `sudo ... install` line for the current distro.
 * Keeps fix messages from drifting across 20+ checks.
 */
export function pkgInstall(dist, pkgs) {
  if (pkgs && typeof pkgs === "object" && !Array.isArray(pkgs)) {
    const pick = pkgs[dist?.pkg] ?? pkgs[dist?.family] ?? pkgs["*"];
    if (!pick) throw new Error(`pkgInstall: no package name for ${dist?.pkg || dist?.family || "unknown"} and no "*" fallback`);
    return pkgInstall(dist, pick);
  }
  const list = Array.isArray(pkgs) ? pkgs.join(" ") : String(pkgs);
  const p = dist?.pkg;
  // An unknown manager must never degrade to a wrong command — on NixOS or an
  // exotic system, "sudo dnf install" is bad advice, not a fallback.
  if (!p || p === "unknown") {
    return `Manual step: install ${list} with your distribution's package manager (not recognized by linux-doctor).`;
  }
  if (p === "apt") return `sudo apt install ${list}`;
  if (p === "apk") return `sudo apk add ${list}`;
  if (p === "pacman") return `sudo pacman -S ${list}`;
  if (p === "zypper") return `sudo zypper install ${list}`;
  if (p === "xbps") return `sudo xbps-install -Sy ${list}`;
  if (p === "emerge") return `sudo emerge ${list}`;
  // Image-based/atomic desktops (Bazzite, Silverblue, Kinoite…): layering is
  // the correct command for a genuinely missing system package, but it only
  // applies on the next boot.
  if (p === "rpm-ostree") return `sudo rpm-ostree install ${list}, then reboot`;
  return `sudo dnf install ${list}`;
}

/**
 * How to reclaim space held by the package manager's cache. "There is no
 * package cache" is a real answer here: an image-based system has no dnf or
 * apt cache at all — what exists is rpm-ostree's cached repo metadata and
 * transient deployment space.
 * Returns a phrase to embed in a fix sentence.
 */
export function pkgCacheClean(dist) {
  const p = dist?.pkg;
  if (p === "apt") return "`sudo apt clean`";
  if (p === "dnf") return "`sudo dnf clean all`";
  if (p === "pacman") return "`sudo pacman -Sc`";
  if (p === "zypper") return "`sudo zypper clean`";
  if (p === "apk") return "`sudo apk cache clean`";
  if (p === "xbps") return "`sudo xbps-remove -O`";
  if (p === "emerge") return "`sudo eclean-dist` (gentoolkit)";
  if (p === "rpm-ostree") {
    return "`sudo rpm-ostree cleanup -m` (cached repo metadata — there is no package cache to clear on an image-based system)";
  }
  return "your distribution's package-cache tool";
}

/**
 * How to get rid of old kernels. Returns a phrase, because on some systems the
 * honest answer is "there is nothing to remove": Arch ships one kernel version
 * at a time, and on an image-based system the kernel belongs to the image.
 */
export function oldKernelClean(dist) {
  const p = dist?.pkg;
  if (p === "apt") return "`sudo apt autoremove --purge`";
  if (p === "dnf") return "`sudo dnf remove --oldinstallonly`";
  if (p === "zypper") return "`sudo zypper purge-kernels`";
  if (p === "xbps") return "`sudo vkpurge rm all`";
  if (p === "emerge") return "`sudo eclean-kernel` (app-admin/eclean-kernel)";
  if (p === "pacman") return "nothing — Arch ships one kernel version at a time; old ones only linger in the package cache (`sudo pacman -Sc`)";
  if (p === "rpm-ostree") {
    return "nothing to remove by hand — the kernel is part of the image, and old kernels leave with their deployments (`sudo rpm-ostree cleanup -b` frees the space)";
  }
  return "remove old kernels with your distribution's tool";
}

/**
 * How to refresh the bootloader configuration. An image-based system is the
 * case that matters: boot entries are generated per deployment by rpm-ostree,
 * and running grub2-mkconfig there by hand is the classic wrong move.
 */
export function bootloaderRefresh(dist) {
  const p = dist?.pkg;
  if (p === "rpm-ostree") {
    return "do not regenerate GRUB by hand on an image-based system — boot entries are generated per deployment; use `sudo rpm-ostree kargs` for kernel arguments (and `ujust configure-grub` for menu visibility on Bazzite)";
  }
  if (dist?.family === "debian") return "`sudo update-grub`";
  if (dist?.family === "arch") return "`sudo grub-mkconfig -o /boot/grub/grub.cfg` (or `sudo bootctl update` for systemd-boot)";
  if (dist?.family === "suse") return "`sudo grub2-mkconfig -o /boot/grub2/grub.cfg` (note the `2` and `/boot/grub2/` on openSUSE)";
  if (dist?.family === "fedora") return "`sudo grub2-mkconfig -o /boot/grub2/grub.cfg`";
  return "refresh the bootloader with your distribution's tool";
}

/**
 * The service-manager command for an action on a unit. Returns a BARE command
 * string (callers wrap it in backticks), or null when the concept has no
 * equivalent on that init system — telling an OpenRC or runit user to run
 * `systemctl` is simply wrong.
 * Actions: status · start · stop · restart · reload · enable-now.
 */
export function svcCmd(dist, action, unit) {
  const init = dist?.init || "systemd";
  // systemd units carry a .service suffix; OpenRC and runit service names
  // do not, so `rc-service foo.service` would be wrong.
  const u = init === "systemd" ? unit : String(unit).replace(/\.service$/, "");
  if (init === "openrc") {
    if (action === "status") return `rc-service ${u} status`;
    if (action === "start") return `sudo rc-service ${u} start`;
    if (action === "stop") return `sudo rc-service ${u} stop`;
    if (action === "restart") return `sudo rc-service ${u} restart`;
    if (action === "reload") return `sudo rc-service ${u} reload`;
    if (action === "enable-now") return `sudo rc-update add ${u} && sudo rc-service ${u} start`;
    return null;
  }
  if (init === "runit") {
    if (action === "status") return `sv status ${u}`;
    if (action === "start") return `sudo sv start ${u}`;
    if (action === "stop") return `sudo sv stop ${u}`;
    if (action === "restart") return `sudo sv restart ${u}`;
    if (action === "reload") return `sudo sv reload ${u}`;
    if (action === "enable-now") return `sudo ln -s /etc/sv/${u} /var/service/`;
    return null;
  }
  if (action === "status") return `systemctl status ${u}`;
  if (action === "start") return `sudo systemctl start ${u}`;
  if (action === "stop") return `sudo systemctl stop ${u}`;
  if (action === "restart") return `sudo systemctl restart ${u}`;
  if (action === "reload") return `sudo systemctl reload ${u}`;
  if (action === "enable-now") return `sudo systemctl enable --now ${u}`;
  return null;
}

/**
 * Where to read a unit's log output. systemd has `journalctl -u`; OpenRC and
 * runit write to a system log instead, and it is a different file on each.
 */
export function svcLogs(dist, unit) {
  const init = dist?.init || "systemd";
  if (init === "openrc") return `grep ${unit} /var/log/messages | tail -n 50`;
  if (init === "runit") return `tail -n 50 /var/log/${unit}/current`;
  return `journalctl -u ${unit} -b`;
}

/**
 * Timer units exist on systemd only. A fix that says "enable fstrim.timer" is
 * meaningless on OpenRC and runit, where the equivalent is a cron job.
 */
export function hasTimers(dist) {
  return (dist?.init || "systemd") === "systemd";
}