// engine/skill-registry/installer.ts — SPEC-150: Skill installer
// Installs skills from a registry into a project's .claude/skills/ directory.
// Manages the local manifest (manifest.json) tracking all installed skills.
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import * as os from 'node:os';
import { fetchAnthropicSkillContent } from './anthropic-adapter.js';
import { getBuiltInSkillEntry } from './builtin-catalog-adapter.js';
import { scanSkillSecurity } from './skill-security-scanner.js';
const SKILLS_DIR = '.claude/skills';
const MANIFEST_FILE = 'manifest.json';
const BLOCKING_SEVERITIES = new Set(['HIGH', 'CRITICAL']);
/** Return the absolute path to the project's skills directory. */
function skillsRoot(projectPath) {
    return path.join(projectPath, SKILLS_DIR);
}
/** Return the absolute path to the manifest file. */
function manifestPath(projectPath) {
    return path.join(skillsRoot(projectPath), MANIFEST_FILE);
}
/**
 * Read the skill manifest from disk.
 * Returns an empty manifest if the file does not exist or cannot be parsed.
 */
export async function readSkillManifest(projectPath) {
    try {
        const raw = await fs.readFile(manifestPath(projectPath), 'utf-8');
        return JSON.parse(raw);
    }
    catch {
        return { installedAt: new Date().toISOString(), skills: [] };
    }
}
/** Write the manifest to disk (creates the directory if needed). */
async function writeSkillManifest(projectPath, manifest) {
    await fs.mkdir(skillsRoot(projectPath), { recursive: true });
    await fs.writeFile(manifestPath(projectPath), JSON.stringify(manifest, null, 2), 'utf-8');
}
/**
 * Check whether a skill is already installed in the project.
 */
export async function isSkillInstalled(projectPath, skillName) {
    const manifest = await readSkillManifest(projectPath);
    return manifest.skills.some((s) => s.name === skillName);
}
/** Update or add a skill entry in the manifest. */
function upsertManifestEntry(manifest, entry) {
    const existing = manifest.skills.findIndex((s) => s.name === entry.name);
    const skills = existing >= 0
        ? manifest.skills.map((s, i) => i === existing ? { ...s, ...entry, updatedAt: new Date().toISOString() } : s)
        : [...manifest.skills, entry];
    return { ...manifest, skills };
}
/** Prepare a skill from the Anthropic (GitHub) source. */
async function prepareFromAnthropic(skillName) {
    const content = await fetchAnthropicSkillContent(skillName);
    if (!content) {
        throw new Error(`Skill "${skillName}" not found in the Anthropic skills repository. ` +
            'Check the skill name and try again.');
    }
    const hasScripts = content.files.some((f) => f.startsWith('scripts/') || f === 'scripts');
    return { content: content.skillMd, hasScripts };
}
/** Build a YAML frontmatter block for a skill entry if model/effort are defined. */
function buildFrontmatter(entry) {
    if (!entry.model && !entry.effort) {
        return '';
    }
    const lines = ['---'];
    if (entry.model) {
        lines.push(`model: ${entry.model}`);
    }
    if (entry.effort) {
        lines.push(`effort: ${entry.effort}`);
    }
    lines.push('---', '');
    return lines.join('\n');
}
/** Prepare a built-in skill from the local catalog, generating a SKILL.md. */
function prepareFromBuiltIn(skillName) {
    const entry = getBuiltInSkillEntry(skillName);
    let content;
    if (entry) {
        const frontmatter = buildFrontmatter(entry);
        content = [
            frontmatter,
            `# ${entry.name}`,
            ``,
            entry.description,
            ``,
            `## When to use`,
            ``,
            entry.trigger,
            ``,
            `## Tags`,
            ``,
            entry.tags.join(', '),
        ].join('\n');
    }
    else {
        content = [
            `# ${skillName}`,
            ``,
            `Built-in Planu skill.`,
            ``,
            `## When to use`,
            ``,
            `Consult the Planu documentation.`,
        ].join('\n');
    }
    return { content, hasScripts: false };
}
/** Prepare a skill placeholder for skillssh/agentskill sources (no direct file API yet). */
function prepareFromExternal(skillName, source) {
    const placeholder = [
        `# ${skillName}`,
        '',
        `> Installed from: ${source}`,
        `> Installation date: ${new Date().toISOString()}`,
        '',
        'Run `planu registry install` again to refresh this skill.',
    ].join('\n');
    return { content: placeholder, hasScripts: false };
}
function shouldScanSource(source) {
    return source !== 'builtin';
}
async function writePreparedSkill(prepared, installDir) {
    await fs.mkdir(installDir, { recursive: true });
    const skillMdPath = path.join(installDir, 'SKILL.md');
    await fs.writeFile(skillMdPath, prepared.content, 'utf-8');
    return {
        filesWritten: [skillMdPath],
        hasScripts: prepared.hasScripts,
        version: prepared.version,
    };
}
async function scanPreparedSkill(prepared, skillName, source, options) {
    if (!shouldScanSource(source)) {
        return undefined;
    }
    const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'planu-skill-scan-'));
    try {
        await fs.writeFile(path.join(tempDir, 'SKILL.md'), prepared.content, 'utf-8');
        const scan = await scanSkillSecurity(tempDir, skillName);
        if (BLOCKING_SEVERITIES.has(scan.severity) || scan.recommendation === 'DO_NOT_INSTALL') {
            throw new Error(`Skill "${skillName}" blocked by security scan: ${scan.severity} risk, recommendation ${scan.recommendation}.`);
        }
        return scan;
    }
    catch (err) {
        if (options.scannerUnavailablePolicy === 'allow' &&
            err instanceof Error &&
            err.name === 'SkillSecurityScannerUnavailableError') {
            return {
                scanner: 'skillspector',
                scannedAt: new Date().toISOString(),
                score: 100,
                severity: 'UNKNOWN',
                recommendation: 'UNKNOWN',
                issuesCount: 0,
                command: 'skillspector scan <skillDir> --no-llm --format json',
                available: false,
                error: err.message,
            };
        }
        throw err;
    }
    finally {
        await fs.rm(tempDir, { recursive: true, force: true });
    }
}
/**
 * Install a skill from the specified source into the project's .claude/skills/ directory.
 *
 * Steps:
 *  1. Resolve the target installation directory.
 *  2. Fetch skill content from the appropriate adapter.
 *  3. Write files to disk.
 *  4. Update the local manifest.
 *  5. Return the install result with security flags.
 */
export async function installSkill(skillName, source, projectPath, options = {}) {
    const installDir = path.join(skillsRoot(projectPath), skillName);
    const prepared = source === 'anthropic'
        ? await prepareFromAnthropic(skillName)
        : source === 'builtin'
            ? prepareFromBuiltIn(skillName)
            : prepareFromExternal(skillName, source);
    const securityScan = await scanPreparedSkill(prepared, skillName, source, options);
    const { filesWritten, hasScripts, version } = await writePreparedSkill(prepared, installDir);
    const manifest = await readSkillManifest(projectPath);
    const entry = {
        name: skillName,
        source,
        version,
        installedAt: new Date().toISOString(),
        path: installDir,
        securityScan,
    };
    await writeSkillManifest(projectPath, upsertManifestEntry(manifest, entry));
    return {
        name: skillName,
        source,
        installedPath: installDir,
        filesWritten,
        hasScripts,
        version,
        securityScan,
    };
}
//# sourceMappingURL=installer.js.map