/**
 * PluginLoader — discovery, installation, and runtime activation of plugins.
 *
 * This service is the entry point for the plugin system's I/O boundary:
 *
 * 1. **Discovery** — Scans the local plugin directory
 *    (`~/.rudder/plugins/`) and `node_modules` for packages matching
 *    the `rudder-plugin-*` naming convention. Aggregates results with
 *    path-based deduplication.
 *
 * 2. **Installation** — `installPlugin()` downloads from npm (or reads a
 *    local path), validates the manifest, checks capability consistency,
 *    and persists the install record.
 *
 * 3. **Runtime activation** — `activatePlugin()` wires up a loaded plugin
 *    with all runtime services: resolves its entrypoint, builds
 *    capability-gated host handlers, spawns a worker process, syncs job
 *    declarations, registers event subscriptions, and discovers tools.
 *
 * 4. **Shutdown** — `shutdownAll()` gracefully stops all active workers
 *    and unregisters runtime hooks.
 *
 * @see PLUGIN_SPEC.md §8 — Plugin Discovery
 * @see PLUGIN_SPEC.md §10 — Package Contract
 * @see PLUGIN_SPEC.md §12 — Process Model
 */
import { existsSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { execFile } from "node:child_process";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
export const execFileAsync = promisify(execFile);
export const __dirname = path.dirname(fileURLToPath(import.meta.url));
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
/**
 * Naming convention for npm-published Rudder plugins.
 * Packages matching this pattern are considered Rudder plugins.
 *
 * @see PLUGIN_SPEC.md §10 — Package Contract
 */
export const NPM_PLUGIN_PACKAGE_PREFIX = "rudder-plugin-";
/**
 * Default local plugin directory.  The loader scans this directory for
 * locally-installed plugin packages.
 *
 * @see PLUGIN_SPEC.md §8.1 — On-Disk Layout
 */
export const DEFAULT_LOCAL_PLUGIN_DIR = path.join(os.homedir(), ".rudder", "plugins");
export const DEV_TSX_LOADER_PATH = path.resolve(__dirname, "../../../cli/node_modules/tsx/dist/loader.mjs");
export function getDeclaredPageRoutePaths(manifest) {
    return (manifest.ui?.slots ?? [])
        .filter((slot) => slot.type === "page" && typeof slot.routePath === "string" && slot.routePath.length > 0)
        .map((slot) => slot.routePath);
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
 * Check whether a package name matches the Rudder plugin naming convention.
 * Accepts both the "rudder-plugin-" prefix and scoped "@scope/plugin-" packages.
 *
 * @see PLUGIN_SPEC.md §10 — Package Contract
 */
export function isPluginPackageName(name) {
    if (name.startsWith(NPM_PLUGIN_PACKAGE_PREFIX))
        return true;
    // Also accept scoped packages like @acme/plugin-linear or @rudderhq/plugin-*
    if (name.includes("/")) {
        const localPart = name.split("/")[1] ?? "";
        return localPart.startsWith("plugin-");
    }
    return false;
}
/**
 * Read and parse a package.json from a directory path.
 * Returns null if no package.json exists.
 */
export async function readPackageJson(dir) {
    const pkgPath = path.join(dir, "package.json");
    if (!existsSync(pkgPath))
        return null;
    try {
        const raw = await readFile(pkgPath, "utf-8");
        return JSON.parse(raw);
    }
    catch {
        return null;
    }
}
/**
 * Resolve the manifest entrypoint from a package.json and package root.
 *
 * The spec defines a "rudderPlugin" key in package.json with a "manifest"
 * subkey pointing to the manifest module.  This helper resolves the path.
 *
 * @see PLUGIN_SPEC.md §10 — Package Contract
 */
export function resolveManifestPath(packageRoot, pkgJson) {
    const rudderPlugin = pkgJson["rudderPlugin"];
    if (rudderPlugin !== null &&
        typeof rudderPlugin === "object" &&
        !Array.isArray(rudderPlugin)) {
        const manifestRelPath = rudderPlugin["manifest"];
        if (typeof manifestRelPath === "string") {
            // NOTE: the resolved path is returned as-is even if the file does not yet
            // exist on disk (e.g. the package has not been built).  Callers MUST guard
            // with existsSync() before passing the path to loadManifestFromPath().
            return path.resolve(packageRoot, manifestRelPath);
        }
    }
    // Fallback: look for dist/manifest.js as a convention
    const conventionalPath = path.join(packageRoot, "dist", "manifest.js");
    if (existsSync(conventionalPath)) {
        return conventionalPath;
    }
    // Fallback: look for manifest.js at package root
    const rootManifestPath = path.join(packageRoot, "manifest.js");
    if (existsSync(rootManifestPath)) {
        return rootManifestPath;
    }
    return null;
}
export function parseSemver(version) {
    const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
    if (!match)
        return null;
    return {
        major: Number(match[1]),
        minor: Number(match[2]),
        patch: Number(match[3]),
        prerelease: match[4] ? match[4].split(".") : [],
    };
}
export function compareIdentifiers(left, right) {
    const leftIsNumeric = /^\d+$/.test(left);
    const rightIsNumeric = /^\d+$/.test(right);
    if (leftIsNumeric && rightIsNumeric) {
        return Number(left) - Number(right);
    }
    if (leftIsNumeric)
        return -1;
    if (rightIsNumeric)
        return 1;
    return left.localeCompare(right);
}
export function compareSemver(left, right) {
    const leftParsed = parseSemver(left);
    const rightParsed = parseSemver(right);
    if (!leftParsed || !rightParsed) {
        throw new Error(`Invalid semver comparison: '${left}' vs '${right}'`);
    }
    const coreOrder = ["major", "minor", "patch"].map((key) => leftParsed[key] - rightParsed[key]).find((delta) => delta !== 0);
    if (coreOrder) {
        return coreOrder;
    }
    if (leftParsed.prerelease.length === 0 && rightParsed.prerelease.length === 0) {
        return 0;
    }
    if (leftParsed.prerelease.length === 0)
        return 1;
    if (rightParsed.prerelease.length === 0)
        return -1;
    const maxLength = Math.max(leftParsed.prerelease.length, rightParsed.prerelease.length);
    for (let index = 0; index < maxLength; index += 1) {
        const leftId = leftParsed.prerelease[index];
        const rightId = rightParsed.prerelease[index];
        if (leftId === undefined)
            return -1;
        if (rightId === undefined)
            return 1;
        const diff = compareIdentifiers(leftId, rightId);
        if (diff !== 0)
            return diff;
    }
    return 0;
}
export function getMinimumHostVersion(manifest) {
    return manifest.minimumHostVersion ?? manifest.minimumPaperclipVersion;
}
/**
 * Extract UI contribution metadata from a manifest for route serialization.
 *
 * Returns `null` when the plugin does not declare any UI slots or launchers.
 * Launcher declarations are aggregated from both the legacy top-level
 * `launchers` field and the preferred `ui.launchers` field.
 */
export function getPluginUiContributionMetadata(manifest) {
    const slots = manifest.ui?.slots ?? [];
    const launchers = [
        ...(manifest.launchers ?? []),
        ...(manifest.ui?.launchers ?? []),
    ];
    if (slots.length === 0 && launchers.length === 0) {
        return null;
    }
    return {
        uiEntryFile: "index.js",
        slots,
        launchers,
    };
}
// ---------------------------------------------------------------------------
// Factory
// ---------------------------------------------------------------------------
/**
 * Create a PluginLoader service.
 *
 * The loader is responsible for plugin discovery, installation, and runtime
 * activation.  It reads plugin packages from the local filesystem and npm,
 * validates their manifests, registers them in the database, and — when
 * runtime services are provided — initialises worker processes, event
 * subscriptions, job schedules, webhook endpoints, and agent tools.
 *
 * Usage (discovery & install only):
 * ```ts
 * const loader = pluginLoader(db, { enableLocalFilesystem: true });
 *
 * // Discover all available plugins
 * const result = await loader.discoverAll();
 * for (const plugin of result.discovered) {
 *   console.log(plugin.packageName, plugin.manifest?.id);
 * }
 *
 * // Install a specific plugin
 * const discovered = await loader.installPlugin({
 *   packageName: "rudder-plugin-linear",
 *   version: "^1.0.0",
 * });
 * ```
 *
 * Usage (full runtime activation at server startup):
 * ```ts
 * const loader = pluginLoader(db, loaderOpts, {
 *   workerManager,
 *   eventBus,
 *   jobScheduler,
 *   jobStore,
 *   toolDispatcher,
 *   lifecycleManager,
 *   buildHostHandlers: (pluginId, manifest) => ({ ... }),
 *   instanceInfo: { instanceId: "inst-1", hostVersion: "1.0.0" },
 * });
 *
 * // Load all ready plugins at startup
 * const loadResult = await loader.loadAll();
 * console.log(`Loaded ${loadResult.succeeded}/${loadResult.total} plugins`);
 *
 * // Load a single plugin after install
 * const singleResult = await loader.loadSingle(pluginId);
 *
 * // Shutdown all plugin workers on server exit
 * await loader.shutdownAll();
 * ```
 *
 * @see PLUGIN_SPEC.md §8.1 — On-Disk Layout
 * @see PLUGIN_SPEC.md §8.3 — Install Process
 * @see PLUGIN_SPEC.md §12 — Process Model
 */
//# sourceMappingURL=plugin-loader.helpers.js.map