"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SkillInstallSelectionError = exports.NoInstallableSkillError = exports.SKILL_INSTALL_SCHEMA_VERSION = exports.SKILL_INSTALL_SELECTION_REQUIRED_KIND = exports.SKILL_INSTALL_RESULT_KIND = void 0;
exports.installSkill = installSkill;
// biome-ignore lint/nursery/noExcessiveLinesPerFile: Skill install keeps source, search, provider plugin, verification, and result materialization in one CLI flow.
const promises_1 = require("node:fs/promises");
const node_os_1 = __importDefault(require("node:os"));
const node_path_1 = __importDefault(require("node:path"));
const claude_cli_1 = require("../auth-profiles/claude-cli");
const environment_1 = require("../auth-profiles/environment");
const store_1 = require("../auth-profiles/store");
const app_server_client_1 = require("../codex/app-server-client");
const github_http_1 = require("../catalog/github-http");
const requirement_exclusions_1 = require("../catalog/requirement-exclusions");
const command_1 = require("../core/command");
const paths_1 = require("../core/paths");
const skill_inventory_1 = require("../workflows/skill-inventory");
const search_1 = require("./search");
const source_uri_1 = require("./source-uri");
exports.SKILL_INSTALL_RESULT_KIND = "alpaca.skillInstallResult";
exports.SKILL_INSTALL_SELECTION_REQUIRED_KIND = "alpaca.skillInstallSelectionRequired";
exports.SKILL_INSTALL_SCHEMA_VERSION = 3;
class NoInstallableSkillError extends Error {
    constructor(query) {
        super(`No installable skill found for query: ${query}`);
        this.name = "NoInstallableSkillError";
    }
}
exports.NoInstallableSkillError = NoInstallableSkillError;
async function installSkill(input) {
    const env = input.env ?? process.env;
    const homeDirectory = input.homeDirectory ?? node_os_1.default.homedir();
    const source = await resolveInstallSource({ ...input, env, homeDirectory });
    switch (source.parsed.kind) {
        case "file":
            return installDirectorySkill({
                ...input,
                env,
                homeDirectory,
                source: source.source,
                sourcePath: source.parsed.path,
                ...(source.installId === undefined
                    ? {}
                    : { installId: source.installId }),
            });
        case "github":
            return installGithubSkill({
                ...input,
                env,
                homeDirectory,
                source: source.source,
                parsed: source.parsed,
                ...(source.installId === undefined
                    ? {}
                    : { installId: source.installId }),
            });
        case "claude-plugin":
            return installClaudePlugin({
                ...input,
                env,
                homeDirectory,
                source: source.source,
                marketplace: source.parsed.marketplace,
                plugin: source.parsed.plugin,
            });
        case "codex-plugin":
            return installCodexPlugin({
                ...input,
                env,
                homeDirectory,
                source: source.source,
                marketplace: source.parsed.marketplace,
                marketplacePath: source.parsed.marketplacePath,
                plugin: source.parsed.plugin,
            });
    }
}
async function resolveInstallSource(input) {
    if (input.source !== undefined) {
        return {
            source: input.source,
            parsed: (0, source_uri_1.parseSkillSourceUri)(input.source),
            ...(input.installId === undefined ? {} : { installId: input.installId }),
        };
    }
    if (input.query === undefined) {
        throw new Error("Specify --source or --search.");
    }
    const query = input.query;
    const result = await (0, search_1.searchSkills)({
        repoRoot: input.repoRoot,
        query,
        targets: [input.target],
        configuredWorkers: input.configuredWorkers,
        includeExcluded: input.includeExcluded === true,
        ...(input.authProfiles === undefined
            ? {}
            : { authProfiles: input.authProfiles }),
        env: input.env,
        homeDirectory: input.homeDirectory,
    });
    const candidates = result.items.filter((item) => item.installable && item.targets.includes(input.target));
    const exactCandidates = candidates.filter((item) => normalizeSearchId(item.id) === normalizeSearchId(query));
    const selectedCandidates = exactCandidates.length > 0 ? exactCandidates : candidates;
    if (selectedCandidates.length === 0) {
        throw new NoInstallableSkillError(query);
    }
    if (selectedCandidates.length !== 1) {
        throw new SkillInstallSelectionError({
            query,
            target: input.target,
            items: selectedCandidates.map(searchItemToSelectionItem),
        });
    }
    const selected = selectedCandidates[0];
    if (selected === undefined) {
        throw new NoInstallableSkillError(query);
    }
    return {
        source: selected.source,
        parsed: (0, source_uri_1.parseSkillSourceUri)(selected.source),
        installId: selected.id,
    };
}
class SkillInstallSelectionError extends Error {
    result;
    constructor(input) {
        super(`Multiple installable skills match query: ${input.query}`);
        this.name = "SkillInstallSelectionError";
        this.result = {
            kind: exports.SKILL_INSTALL_SELECTION_REQUIRED_KIND,
            schemaVersion: exports.SKILL_INSTALL_SCHEMA_VERSION,
            query: input.query,
            target: input.target,
            items: input.items,
        };
    }
}
exports.SkillInstallSelectionError = SkillInstallSelectionError;
async function installDirectorySkill(input) {
    const sourceDirectory = await resolveSourceSkillDirectory(input.sourcePath);
    const plan = buildLocalInstallPlan({
        repoRoot: input.repoRoot,
        homeDirectory: input.homeDirectory,
        target: input.target,
        sourceDirectory,
        ...(input.installId === undefined
            ? {}
            : { fallbackSkillId: input.installId }),
    });
    return installLocalSkillPlan({ ...input, plan });
}
async function installGithubSkill(input) {
    const tempRoot = await (0, promises_1.mkdtemp)(node_path_1.default.join(node_os_1.default.tmpdir(), "alpaca-skill-"));
    try {
        const sourceDirectory = node_path_1.default.join(tempRoot, "skill");
        await downloadGithubDirectory(input.parsed, sourceDirectory, input.env);
        const plan = buildLocalInstallPlan({
            repoRoot: input.repoRoot,
            homeDirectory: input.homeDirectory,
            target: input.target,
            sourceDirectory,
            fallbackSkillId: input.installId ?? githubDirectoryFallbackId(input.parsed),
        });
        return await installLocalSkillPlan({ ...input, plan });
    }
    finally {
        await (0, promises_1.rm)(tempRoot, { recursive: true, force: true });
    }
}
async function installLocalSkillPlan(input) {
    const status = await copySkillDirectory(input.plan, input.overwrite);
    return materializeInstallResult({
        ...input,
        status,
        source: input.source,
        skillId: input.plan.skillId,
        installPath: input.plan.targetDirectory,
    });
}
async function installClaudePlugin(input) {
    if (input.target !== "claude") {
        throw new Error("Claude plugin sources can only be installed for claude.");
    }
    await (0, command_1.runCommand)({
        command: "claude",
        args: [
            "plugin",
            "install",
            `${input.plugin}@${input.marketplace}`,
            "--scope",
            input.pluginScope,
        ],
        cwd: input.repoRoot,
        env: buildClaudePluginInstallEnvironment(input),
        timeoutMs: 60_000,
    });
    return materializePluginInstallResult({
        ...input,
        status: "installed",
        plugin: input.plugin,
    });
}
async function installCodexPlugin(input) {
    if (input.target !== "codex") {
        throw new Error("Codex plugin sources can only be installed for codex.");
    }
    if (input.pluginScope !== "project") {
        throw new Error("--plugin-scope is only supported for Claude plugin installs.");
    }
    await (0, app_server_client_1.installCodexPlugin)({
        cwd: input.repoRoot,
        env: buildCodexPluginInstallEnvironment(input),
        pluginName: input.plugin,
        marketplacePath: input.marketplacePath,
        remoteMarketplaceName: input.marketplacePath === null ? input.marketplace : null,
    });
    return materializePluginInstallResult({
        ...input,
        status: "installed",
        plugin: input.plugin,
    });
}
function buildClaudePluginInstallEnvironment(input) {
    const cliProfile = input.authProfiles?.find((profile) => profile.kind === "claude-cli");
    return cliProfile === undefined
        ? input.env
        : (0, claude_cli_1.buildClaudeCliAuthEnvironment)(input.env, cliProfile.claudeConfigPath);
}
function buildCodexPluginInstallEnvironment(input) {
    return collectCodexPluginInstallEnvironments(input)[0] ?? input.env;
}
function collectCodexPluginInstallEnvironments(input) {
    return (input.authProfiles
        ?.filter((candidate) => (0, store_1.getAuthProfileProvider)(candidate) === "codex")
        .map((profile) => (0, environment_1.buildAuthProfileEnvironment)(profile, input.env))
        .filter((env) => env !== null) ?? []);
}
async function materializeInstallResult(input) {
    const installed = await findInstalledSkill({
        repoRoot: input.repoRoot,
        target: input.target,
        configuredWorkers: input.configuredWorkers,
        ...(input.authProfiles === undefined
            ? {}
            : { authProfiles: input.authProfiles }),
        env: input.env,
        homeDirectory: input.homeDirectory,
        skillId: input.skillId,
        installPath: input.installPath,
    });
    const excludedFromRequires = installed?.excludedFromRequires ??
        (await findSkillInstallRequirementExclusion({
            repoRoot: input.repoRoot,
            homeDirectory: input.homeDirectory,
            ref: installed?.ref ?? input.skillId,
            id: input.skillId,
            source: input.source,
        }));
    const registeredRequirementExclusion = await maybeRecordSkillRequirementExclusion({
        existingExclusion: excludedFromRequires,
        homeDirectory: input.homeDirectory,
        ref: installed?.ref ?? input.skillId,
        id: input.skillId,
        source: input.source,
        installed,
    });
    return {
        kind: exports.SKILL_INSTALL_RESULT_KIND,
        schemaVersion: exports.SKILL_INSTALL_SCHEMA_VERSION,
        status: input.status,
        target: input.target,
        id: input.skillId,
        ref: installed?.ref ?? null,
        usableForRequiredSkills: installed?.usableForRequiredSkills ?? false,
        source: input.source,
        installPath: input.installPath,
        nextCommands: buildNextCommands(input.target),
        ...(excludedFromRequires === undefined
            ? {}
            : {
                excludedFromRequires,
                excludeReason: `Installed successfully, but this skill is excluded from workflow requires: ${excludedFromRequires.reason}`,
            }),
        ...(registeredRequirementExclusion === undefined
            ? {}
            : { registeredRequirementExclusion }),
    };
}
async function materializePluginInstallResult(input) {
    const installed = await findInstalledPluginSkill({
        repoRoot: input.repoRoot,
        target: input.target,
        configuredWorkers: input.configuredWorkers,
        ...(input.authProfiles === undefined
            ? {}
            : { authProfiles: input.authProfiles }),
        env: input.env,
        homeDirectory: input.homeDirectory,
        plugin: input.plugin,
    });
    const excludedFromRequires = installed?.excludedFromRequires ??
        (await findSkillInstallRequirementExclusion({
            repoRoot: input.repoRoot,
            homeDirectory: input.homeDirectory,
            ref: installed?.ref ?? `${input.target}:${input.plugin}`,
            id: input.plugin,
            source: input.source,
        }));
    const registeredRequirementExclusion = await maybeRecordSkillRequirementExclusion({
        existingExclusion: excludedFromRequires,
        homeDirectory: input.homeDirectory,
        ref: installed?.ref ?? `${input.target}:${input.plugin}`,
        id: input.plugin,
        source: input.source,
        installed,
    });
    return {
        kind: exports.SKILL_INSTALL_RESULT_KIND,
        schemaVersion: exports.SKILL_INSTALL_SCHEMA_VERSION,
        status: input.status,
        target: input.target,
        id: input.plugin,
        ref: installed?.ref ?? null,
        usableForRequiredSkills: installed?.usableForRequiredSkills ?? false,
        source: input.source,
        installPath: installed === undefined ? null : node_path_1.default.dirname(installed.path),
        nextCommands: buildNextCommands(input.target),
        ...(excludedFromRequires === undefined
            ? {}
            : {
                excludedFromRequires,
                excludeReason: `Installed successfully, but this skill is excluded from workflow requires: ${excludedFromRequires.reason}`,
            }),
        ...(registeredRequirementExclusion === undefined
            ? {}
            : { registeredRequirementExclusion }),
    };
}
async function maybeRecordSkillRequirementExclusion(input) {
    if (input.installed?.usableForRequiredSkills === true ||
        input.existingExclusion !== undefined) {
        return undefined;
    }
    const reason = "Installed skill did not pass the basic requires.skills usability check after install.";
    const result = await (0, requirement_exclusions_1.addMachineRequirementExclusion)({
        homeDirectory: input.homeDirectory,
        target: "skill",
        entry: {
            ref: input.ref,
            id: input.id,
            source: input.source,
            reason,
            addedAt: new Date().toISOString(),
        },
    });
    return {
        ...result,
        reason,
        message: `${input.ref} was excluded from workflow requires. Remove the entry from ${result.path} after fixing the local setup.`,
    };
}
async function findSkillInstallRequirementExclusion(input) {
    const exclusions = await (0, requirement_exclusions_1.loadEffectiveRequirementExclusions)({
        repoRoot: input.repoRoot,
        homeDirectory: input.homeDirectory,
    });
    return ((0, requirement_exclusions_1.findRequirementExclusionMatch)(exclusions, "skill", {
        ref: input.ref,
        id: input.id,
        source: input.source,
    }) ?? undefined);
}
async function findInstalledSkill(input) {
    const result = await (0, skill_inventory_1.listAvailableSkills)({
        repoRoot: input.repoRoot,
        workers: [input.target],
        configuredWorkers: input.configuredWorkers,
        ...(input.authProfiles === undefined
            ? {}
            : { authProfiles: input.authProfiles }),
        env: input.env,
        homeDirectory: input.homeDirectory,
        includeExcluded: true,
    });
    const expectedSkillPath = node_path_1.default.join(input.installPath, "SKILL.md");
    return result.items.find((item) => item.skillId === input.skillId &&
        node_path_1.default.resolve(item.path) === node_path_1.default.resolve(expectedSkillPath));
}
async function findInstalledPluginSkill(input) {
    const inventory = await (0, skill_inventory_1.listAvailableSkills)({
        repoRoot: input.repoRoot,
        workers: [input.target],
        configuredWorkers: input.configuredWorkers,
        ...(input.authProfiles === undefined
            ? {}
            : { authProfiles: input.authProfiles }),
        env: input.env,
        homeDirectory: input.homeDirectory,
        includeExcluded: true,
    });
    const candidates = inventory.items.filter((item) => item.worker === input.target && pluginSkillMatches(item, input.plugin));
    if (candidates.length === 1) {
        return candidates[0];
    }
    if (input.target !== "codex") {
        return undefined;
    }
    const codexRefs = await listCodexPluginSkillRefs({
        repoRoot: input.repoRoot,
        env: buildCodexPluginInstallEnvironment(input),
        plugin: input.plugin,
    });
    const codexCandidates = inventory.items.filter((item) => codexRefs.has(item.ref));
    return codexCandidates.length === 1 ? codexCandidates[0] : undefined;
}
function pluginSkillMatches(item, plugin) {
    if (item.source !== "plugin") {
        return false;
    }
    return (item.ref === `${item.worker}:${plugin}` ||
        item.ref.startsWith(`${item.worker}:${plugin}:`));
}
async function listCodexPluginSkillRefs(input) {
    try {
        const result = await (0, app_server_client_1.listCodexSkills)({
            cwd: input.repoRoot,
            env: input.env,
            forceReload: true,
        });
        return new Set(result.data
            .flatMap((entry) => entry.skills)
            .filter((skill) => skill.enabled)
            .map((skill) => `codex:${skill.name}`)
            .filter((ref) => (0, skill_inventory_1.isValidRequiredSkillRef)(ref) &&
            (ref === `codex:${input.plugin}` ||
                ref.startsWith(`codex:${input.plugin}:`))));
    }
    catch {
        return new Set();
    }
}
async function resolveSourceSkillDirectory(sourcePath) {
    const resolved = node_path_1.default.resolve(sourcePath);
    const sourceStat = await (0, promises_1.stat)(resolved);
    const directory = sourceStat.isDirectory()
        ? resolved
        : node_path_1.default.dirname(resolved);
    await assertSkillMarkdownExists(directory);
    return directory;
}
function buildLocalInstallPlan(input) {
    const skillId = input.fallbackSkillId ?? node_path_1.default.basename(input.sourceDirectory);
    if (!(0, skill_inventory_1.isValidBareSkillId)(skillId)) {
        throw new Error(`Invalid skill id for install path: ${skillId}`);
    }
    if (input.target !== "codex" && input.target !== "claude") {
        return {
            skillId,
            sourceDirectory: input.sourceDirectory,
            targetDirectory: node_path_1.default.join((0, paths_1.getControlPlaneHomePath)(input.homeDirectory), "pi-workers", input.target, "skills", skillId),
        };
    }
    const workerDirectory = input.target === "codex" ? ".agents" : ".claude";
    return {
        skillId,
        sourceDirectory: input.sourceDirectory,
        targetDirectory: node_path_1.default.join(input.repoRoot, workerDirectory, "skills", skillId),
    };
}
async function copySkillDirectory(plan, overwrite) {
    if (sameResolvedPath(plan.sourceDirectory, plan.targetDirectory)) {
        await assertSkillMarkdownExists(plan.targetDirectory);
        return "alreadyInstalled";
    }
    if (await pathExists(plan.targetDirectory)) {
        if (!overwrite) {
            await assertSkillMarkdownExists(plan.targetDirectory);
            return "alreadyInstalled";
        }
        await (0, promises_1.rm)(plan.targetDirectory, { recursive: true, force: true });
    }
    await (0, promises_1.mkdir)(node_path_1.default.dirname(plan.targetDirectory), { recursive: true });
    await (0, promises_1.cp)(plan.sourceDirectory, plan.targetDirectory, { recursive: true });
    await assertSkillMarkdownExists(plan.targetDirectory);
    return "installed";
}
async function downloadGithubDirectory(source, targetDirectory, env) {
    await (0, promises_1.mkdir)(targetDirectory, { recursive: true });
    await downloadGithubDirectoryContents({
        source,
        sourceDirectoryPath: source.directoryPath,
        targetDirectory,
        env,
    });
    await assertSkillMarkdownExists(targetDirectory);
}
async function downloadGithubDirectoryContents(input) {
    const entries = await fetchGithubContents(input.source, input.sourceDirectoryPath, input.env);
    for (const entry of entries) {
        if (entry.type === "dir" && entry.path !== undefined) {
            await downloadGithubDirectoryContents({
                source: input.source,
                sourceDirectoryPath: entry.path,
                targetDirectory: node_path_1.default.join(input.targetDirectory, entry.name ?? ""),
                env: input.env,
            });
            continue;
        }
        const downloadUrl = readGithubDownloadUrl(entry);
        if (entry.type === "file" &&
            downloadUrl !== null &&
            entry.name !== undefined) {
            const response = await fetch(downloadUrl, {
                headers: (0, github_http_1.buildGithubHeaders)(input.env),
            });
            if (!response.ok) {
                throw new Error(`GitHub skill file request failed: HTTP ${response.status}`);
            }
            await (0, promises_1.mkdir)(input.targetDirectory, { recursive: true });
            await (0, promises_1.writeFile)(node_path_1.default.join(input.targetDirectory, entry.name), Buffer.from(await response.arrayBuffer()));
        }
    }
}
function sameResolvedPath(left, right) {
    return node_path_1.default.resolve(left) === node_path_1.default.resolve(right);
}
function readGithubDownloadUrl(entry) {
    const value = entry.download_url;
    return typeof value === "string" && value.length > 0 ? value : null;
}
async function fetchGithubContents(source, directoryPath, env) {
    const encodedDirectoryPath = encodeGithubContentPath(directoryPath);
    const url = `https://api.github.com/repos/${encodeURIComponent(source.owner)}/${encodeURIComponent(source.repo)}/contents${encodedDirectoryPath === "" ? "" : `/${encodedDirectoryPath}`}?ref=${encodeURIComponent(source.ref)}`;
    const response = await fetch(url, { headers: (0, github_http_1.buildGithubHeaders)(env) });
    if (!response.ok) {
        throw new Error(`GitHub skill directory request failed: HTTP ${response.status}`);
    }
    const parsed = (await response.json());
    return Array.isArray(parsed) ? parsed : [parsed];
}
function encodeGithubContentPath(value) {
    if (value === ".") {
        return "";
    }
    return value.split("/").map(encodeURIComponent).join("/");
}
function githubDirectoryFallbackId(source) {
    return source.directoryPath === "."
        ? source.repo
        : node_path_1.default.posix.basename(source.directoryPath);
}
async function assertSkillMarkdownExists(directory) {
    const skillPath = node_path_1.default.join(directory, "SKILL.md");
    await (0, promises_1.readFile)(skillPath, "utf8");
}
async function pathExists(filePath) {
    try {
        await (0, promises_1.stat)(filePath);
        return true;
    }
    catch {
        return false;
    }
}
function searchItemToSelectionItem(item) {
    return {
        id: item.id,
        title: item.title,
        description: item.description,
        sourceKind: item.sourceKind,
        source: item.source,
        ...(item.stars === undefined ? {} : { stars: item.stars }),
        ...(item.updatedAt === undefined ? {} : { updatedAt: item.updatedAt }),
        ...(item.createdAt === undefined ? {} : { createdAt: item.createdAt }),
        ...(item.sourceUrl === undefined ? {} : { sourceUrl: item.sourceUrl }),
    };
}
function buildNextCommands(target) {
    return [`alpaca skill list --for ${target}`, "alpaca workflow lint"];
}
function normalizeSearchId(value) {
    return value.toLowerCase().trim();
}
