Ctrl K
ComunidadGuías…

src/plugins.js

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

Archivo original ↗Preguntar a KiBot
// SPDX-License-Identifier: GPL-3.0-or-later
/**
 * Plugin checks: any .js file in the plugin directory is loaded as a check.
 * This lets sysadmins add checks without forking the project — the killer
 * feature for custom environments. A plugin file must export (default or
 * named) an object shaped like a built-in check: { id, title, category?,
 * appliesTo?, async run(ctx) }.
 *
 * The directory is `~/.config/linux-doctor/checks/` (override with
 * LINUX_DOCTOR_PLUGINS). A broken plugin is reported to stderr and skipped —
 * it must never take down a run.
 *
 * `loadPluginEntries` keeps the source file next to each check so `--plugins`
 * can answer "which file does this check come from?" without re-reading the
 * directory. Loading stays one pass: `loadPlugins` is the check-only view.
 */
import { readdirSync } from "node:fs";
import { join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { ALL_KINDS } from "./checks/define.js";
import { pluginDir } from "./paths.js";

export { pluginDir };

/**
 * Load every plugin file and keep the provenance. Returns one entry per file:
 *
 *   { kind: "ok", name, file, check }        a usable check
 *   { kind: "invalid", name, file, warning } wrong export shape, skipped
 *   { kind: "error", name, file, warning }   threw while importing, skipped
 *
 * Never throws. A missing directory is an empty list.
 */
export async function loadPluginEntries(dir = pluginDir()) {
  const root = resolve(dir);
  let files;
  try {
    files = readdirSync(root).filter((f) => f.endsWith(".js"));
  } catch {
    return []; // no plugin directory — nothing to load
  }

  const entries = [];
  for (const name of files.sort()) {
    const file = join(root, name);
    try {
      const mod = await import(pathToFileURL(file).href);
      const check = mod.default ?? Object.values(mod)[0];
      if (check && typeof check.id === "string" && check.id.trim() !== "" && typeof check.run === "function") {
        // Normalize to the defineCheck shape so downstream code (gating,
        // --list) can assume category and appliesTo always exist.
        entries.push({
          kind: "ok",
          name,
          file,
          check: {
            ...check,
            category: check.category ?? "custom",
            appliesTo: Array.isArray(check.appliesTo) ? check.appliesTo : ALL_KINDS,
          },
        });
      } else {
        entries.push({
          kind: "invalid",
          name,
          file,
          warning: `linux-doctor: plugin ${name} does not export { id, run } — skipping`,
        });
      }
    } catch (err) {
      entries.push({
        kind: "error",
        name,
        file,
        warning: `linux-doctor: could not load plugin ${name}: ${err.message}`,
      });
    }
  }
  return entries;
}

/** Load every plugin in the directory as a check. Never throws. */
export async function loadPlugins(dir = pluginDir()) {
  const checks = [];
  for (const entry of await loadPluginEntries(dir)) {
    if (entry.check) checks.push(entry.check);
    else console.error(entry.warning);
  }
  return checks;
}