/**
 * Skill management -- discovery, installation, and syncing of SKILL.md bundles.
 *
 * Skills are knowledge packs stored in ~/.agents/skills/. Each skill directory
 * contains a SKILL.md (with frontmatter metadata) and optional supporting files
 * (rules/, examples/, etc.). This module handles parsing skill metadata,
 * installing skills into agent version homes, and tracking installation state.
 */
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import * as yaml from 'yaml';
import { normalizeAliases } from '../resource-aliases.js';
import { AGENTS, ensureSkillsDir, agentConfigDirName } from '../agents.js';
import { capableAgents, isCapable } from '../capabilities.js';
import { getUserSkillsDir, getSkillsDir as getSystemSkillsDir, getProjectAgentsDir, getEnabledExtraRepos, getTrashSkillsDir } from '../state.js';
import { getEffectiveHome, getVersionHomePath, listInstalledVersions } from '../installations/store.js';
import { listCommandSkillsInVersion } from '../command-skills.js';
import { pluginSkillDirs, listPluginSkillNames } from '../staleness/writers/sources.js';
import { emit } from '../feed/events.js';
const HOME = os.homedir();
/** User-scoped skills dir (~/.agents/skills/). Used for installs. */
export function getSkillsDir() {
    return getUserSkillsDir();
}
export function ensureCentralSkillsDir() {
    const dir = getUserSkillsDir();
    if (!fs.existsSync(dir)) {
        fs.mkdirSync(dir, { recursive: true });
    }
}
export function getAgentSkillsDir(agentId) {
    const home = getEffectiveHome(agentId);
    return path.join(home, agentConfigDirName(agentId), 'skills');
}
export function getProjectSkillsDir(agentId, cwd = process.cwd()) {
    const dirs = [];
    const projectAgentsDir = getProjectAgentsDir(cwd);
    if (projectAgentsDir) {
        dirs.push(path.join(projectAgentsDir, 'skills'));
    }
    dirs.push(path.join(cwd, `.${agentId}`, 'skills'));
    // Return the first existing dir, otherwise default to the first candidate
    for (const dir of dirs) {
        if (fs.existsSync(dir))
            return dir;
    }
    return dirs[0];
}
export function validateSkillMetadata(metadata, skillName) {
    const errors = [];
    const warnings = [];
    if (!metadata) {
        errors.push('SKILL.md not found or has no valid frontmatter');
        return { valid: false, errors, warnings };
    }
    // name is required
    if (!metadata.name || metadata.name.trim() === '') {
        errors.push('Missing required field: name');
    }
    else {
        if (metadata.name.length > 64) {
            errors.push(`name exceeds 64 characters (${metadata.name.length})`);
        }
        if (!/^[a-z0-9-]+$/.test(metadata.name)) {
            warnings.push('name should be lowercase with hyphens (e.g., my-skill-name)');
        }
    }
    // description is required
    if (!metadata.description || metadata.description.trim() === '') {
        errors.push('Missing required field: description');
    }
    else if (metadata.description.length > 1024) {
        warnings.push(`description exceeds 1024 characters (${metadata.description.length})`);
    }
    return { valid: errors.length === 0, errors, warnings };
}
/**
 * Parse skill metadata from SKILL.md, returning both result and any error.
 */
export function tryParseSkillMetadata(skillDir) {
    const skillMdPath = path.join(skillDir, 'SKILL.md');
    if (!fs.existsSync(skillMdPath)) {
        return { metadata: null, error: 'SKILL.md not found' };
    }
    try {
        const content = fs.readFileSync(skillMdPath, 'utf-8');
        const lines = content.split('\n');
        // Check for YAML frontmatter (required)
        if (lines[0] === '---') {
            const endIndex = lines.slice(1).findIndex((l) => l === '---');
            if (endIndex > 0) {
                const frontmatter = lines.slice(1, endIndex + 1).join('\n');
                const parsed = yaml.parse(frontmatter);
                return {
                    metadata: {
                        name: parsed.name || '',
                        description: parsed.description || '',
                        author: parsed.author,
                        version: parsed.version,
                        license: parsed.license,
                        keywords: parsed.keywords,
                        aliases: normalizeAliases(parsed.aliases),
                    },
                };
            }
        }
        // No valid frontmatter
        return { metadata: null, error: 'No valid YAML frontmatter found' };
    }
    catch (e) {
        const msg = e instanceof Error ? e.message : 'Unknown parse error';
        return { metadata: null, error: `Invalid YAML: ${msg}` };
    }
}
/**
 * Parse skill metadata from SKILL.md (backward-compatible, returns null on error).
 */
export function parseSkillMetadata(skillDir) {
    return tryParseSkillMetadata(skillDir).metadata;
}
export function countSkillRules(skillDir) {
    const rulesDir = path.join(skillDir, 'rules');
    if (!fs.existsSync(rulesDir)) {
        return 0;
    }
    try {
        const files = fs.readdirSync(rulesDir);
        return files.filter((f) => f.endsWith('.md')).length;
    }
    catch {
        return 0;
    }
}
/**
 * Count bundled resource files in a skill directory: every regular file
 * beyond SKILL.md itself (reference docs, scripts, assets, etc). Hidden
 * files and hidden directories are skipped.
 */
export function countSkillFiles(skillDir) {
    if (!fs.existsSync(skillDir))
        return 0;
    let count = 0;
    const walk = (dir) => {
        let entries;
        try {
            entries = fs.readdirSync(dir, { withFileTypes: true });
        }
        catch {
            return;
        }
        for (const entry of entries) {
            if (entry.name.startsWith('.'))
                continue;
            const full = path.join(dir, entry.name);
            if (entry.isDirectory()) {
                walk(full);
            }
            else if (entry.isFile()) {
                if (dir === skillDir && entry.name === 'SKILL.md')
                    continue;
                count++;
            }
        }
    };
    walk(skillDir);
    return count;
}
export function discoverSkillsFromRepo(repoPath) {
    const skills = [];
    // Look for skills in common locations
    const searchPaths = [
        path.join(repoPath, 'skills'),
        path.join(repoPath, 'agent-skills'),
        repoPath, // Root level skill directories
    ];
    for (const searchPath of searchPaths) {
        if (!fs.existsSync(searchPath))
            continue;
        try {
            const entries = fs.readdirSync(searchPath, { withFileTypes: true });
            for (const entry of entries) {
                if (!entry.isDirectory())
                    continue;
                // Skip hidden directories (e.g., .system)
                if (entry.name.startsWith('.'))
                    continue;
                const skillDir = path.join(searchPath, entry.name);
                const skillMdPath = path.join(skillDir, 'SKILL.md');
                if (fs.existsSync(skillMdPath)) {
                    const parseResult = tryParseSkillMetadata(skillDir);
                    const validation = validateSkillMetadata(parseResult.metadata, entry.name);
                    // Include even if invalid (for discovery/listing with warnings)
                    skills.push({
                        name: entry.name,
                        path: skillDir,
                        metadata: parseResult.metadata || { name: entry.name, description: '' },
                        ruleCount: countSkillRules(skillDir),
                        validation,
                        parseError: parseResult.error,
                    });
                }
            }
        }
        catch {
            // Skip inaccessible directories
        }
    }
    return skills;
}
export function installSkill(sourcePath, skillName, agents, method = 'symlink') {
    // Validate skill metadata before installation
    const metadata = parseSkillMetadata(sourcePath);
    const validation = validateSkillMetadata(metadata, skillName);
    if (!validation.valid) {
        return {
            success: false,
            error: `Invalid skill: ${validation.errors.join(', ')}`,
            warnings: validation.warnings,
        };
    }
    ensureCentralSkillsDir();
    const centralPath = path.join(getSkillsDir(), skillName);
    // Copy to central location if not already there
    if (!fs.existsSync(centralPath)) {
        try {
            fs.cpSync(sourcePath, centralPath, { recursive: true });
        }
        catch (err) {
            return { success: false, error: `Failed to copy skill: ${err.message}` };
        }
    }
    // Symlink to each agent
    for (const agentId of agents) {
        if (!isCapable(agentId, 'skills')) {
            continue;
        }
        ensureSkillsDir(agentId);
        const agentSkillPath = path.join(getAgentSkillsDir(agentId), skillName);
        // Remove existing if present
        if (fs.existsSync(agentSkillPath)) {
            try {
                fs.rmSync(agentSkillPath, { recursive: true, force: true });
            }
            catch {
                // Ignore removal errors
            }
        }
        try {
            if (method === 'symlink') {
                fs.symlinkSync(centralPath, agentSkillPath, 'dir');
            }
            else {
                fs.cpSync(centralPath, agentSkillPath, { recursive: true });
            }
        }
        catch (err) {
            return {
                success: false,
                error: `Failed to ${method} skill to ${agentId}: ${err.message}`,
            };
        }
    }
    emit('skill.install', { skill: skillName, agents });
    return { success: true };
}
/**
 * Check if a skill exists for an agent.
 */
export function skillExists(agentId, skillName) {
    const agentSkillPath = path.join(getAgentSkillsDir(agentId), skillName);
    return fs.existsSync(agentSkillPath);
}
/**
 * Normalize content for comparison (trim, normalize line endings).
 */
function normalizeContent(content) {
    return content.replace(/\r\n/g, '\n').trim();
}
/**
 * Compare two directories recursively for content equality.
 */
function directoriesMatch(dir1, dir2) {
    if (!fs.existsSync(dir1) || !fs.existsSync(dir2)) {
        return fs.existsSync(dir1) === fs.existsSync(dir2);
    }
    try {
        const files1 = fs.readdirSync(dir1).filter(f => f.endsWith('.md')).sort();
        const files2 = fs.readdirSync(dir2).filter(f => f.endsWith('.md')).sort();
        if (files1.length !== files2.length)
            return false;
        if (files1.join(',') !== files2.join(','))
            return false;
        for (const file of files1) {
            const content1 = fs.readFileSync(path.join(dir1, file), 'utf-8');
            const content2 = fs.readFileSync(path.join(dir2, file), 'utf-8');
            if (normalizeContent(content1) !== normalizeContent(content2)) {
                return false;
            }
        }
        return true;
    }
    catch {
        return false;
    }
}
/**
 * Check if installed skill content matches source content.
 * Compares SKILL.md and all rules/*.md files.
 */
export function skillContentMatches(agentId, skillName, sourcePath) {
    const installedPath = path.join(getAgentSkillsDir(agentId), skillName);
    if (!fs.existsSync(installedPath) || !fs.existsSync(sourcePath)) {
        return false;
    }
    try {
        const installedSkillMd = path.join(installedPath, 'SKILL.md');
        const sourceSkillMd = path.join(sourcePath, 'SKILL.md');
        if (!fs.existsSync(installedSkillMd) || !fs.existsSync(sourceSkillMd)) {
            return false;
        }
        const installedContent = fs.readFileSync(installedSkillMd, 'utf-8');
        const sourceContent = fs.readFileSync(sourceSkillMd, 'utf-8');
        if (normalizeContent(installedContent) !== normalizeContent(sourceContent)) {
            return false;
        }
        const installedRulesDir = path.join(installedPath, 'rules');
        const sourceRulesDir = path.join(sourcePath, 'rules');
        return directoriesMatch(installedRulesDir, sourceRulesDir);
    }
    catch {
        return false;
    }
}
/**
 * List skill names from user (~/.agents/skills/) and system (~/.agents/.system/skills/) dirs.
 * User dir takes priority; deduplication preserves first occurrence.
 */
export function listCentralSkills() {
    const seen = new Set();
    for (const dir of [getUserSkillsDir(), getSystemSkillsDir()]) {
        if (!fs.existsSync(dir))
            continue;
        for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
            if (!e.isDirectory() || e.name.startsWith('.'))
                continue;
            if (!fs.existsSync(path.join(dir, e.name, 'SKILL.md')))
                continue;
            seen.add(e.name);
        }
    }
    return Array.from(seen).sort();
}
/**
 * Resolve a skill name to its source directory. Searches user dir first,
 * then system dir, then extra repos, then plugin-bundled skills
 * (`plugins/<plugin>/skills/<name>` under any trusted base). Returns null if no
 * source has a SKILL.md.
 *
 * Crediting plugins here is load-bearing (PHNX-3185): a skill materialized from
 * `plugins/design/skills/design` has a real source, so it must resolve — else
 * `versionSkillMatches` finds no source and the skill reads as drifted, and
 * {@link diffVersionSkills} classes it an orphan `prune cleanup` would delete.
 */
export function resolveSkillSourcePath(skillName) {
    for (const dir of [getUserSkillsDir(), getSystemSkillsDir()]) {
        const candidate = path.join(dir, skillName);
        if (fs.existsSync(path.join(candidate, 'SKILL.md')))
            return candidate;
    }
    for (const extra of getEnabledExtraRepos()) {
        const candidate = path.join(extra.dir, 'skills', skillName);
        if (fs.existsSync(path.join(candidate, 'SKILL.md')))
            return candidate;
    }
    for (const skillsDir of pluginSkillDirs()) {
        const candidate = path.join(skillsDir, skillName);
        if (fs.existsSync(path.join(candidate, 'SKILL.md')))
            return candidate;
    }
    return null;
}
/**
 * Union of skill names available across the primary and all enabled extras.
 * Primary wins on name collision.
 */
export function listAllSkills() {
    const seen = new Set(listCentralSkills());
    for (const extra of getEnabledExtraRepos()) {
        const dir = path.join(extra.dir, 'skills');
        if (!fs.existsSync(dir))
            continue;
        for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
            if (!e.isDirectory() || e.name.startsWith('.'))
                continue;
            if (!fs.existsSync(path.join(dir, e.name, 'SKILL.md')))
                continue;
            seen.add(e.name);
        }
    }
    return Array.from(seen).sort();
}
/**
 * Path to the skills dir of a specific version home (not the active one).
 */
export function getVersionSkillsDir(agent, version) {
    const home = getVersionHomePath(agent, version);
    return path.join(home, agentConfigDirName(agent), 'skills');
}
/**
 * List real skill names installed in a specific version home.
 *
 * Command-as-skill WRAPPERS (a `skills/<name>/SKILL.md` carrying the
 * `agents_command` marker — how command-runtime agents like Codex/Kimi install
 * commands) are excluded: they are commands, reconciled by the commands diff
 * against the command source, not skills. Counting them here made the skill
 * orphan/extra detectors flag every command wrapper (tickets, prune, swarm-plan,
 * …) as an unmanaged skill — a false positive that `prune cleanup` would delete.
 */
export function listSkillsInVersionHome(agent, version) {
    const dir = getVersionSkillsDir(agent, version);
    if (!fs.existsSync(dir))
        return [];
    const commandWrappers = new Set(listCommandSkillsInVersion(path.join(getVersionHomePath(agent, version), agentConfigDirName(agent))));
    return fs.readdirSync(dir, { withFileTypes: true })
        .filter((e) => e.isDirectory() && !e.name.startsWith('.'))
        .filter((e) => fs.existsSync(path.join(dir, e.name, 'SKILL.md')))
        .map((e) => e.name)
        .filter((name) => !commandWrappers.has(name))
        .sort();
}
/**
 * Check if a skill installed in a specific version matches central content.
 */
function versionSkillMatches(agent, version, skillName) {
    const installedPath = path.join(getVersionSkillsDir(agent, version), skillName);
    const sourcePath = resolveSkillSourcePath(skillName);
    if (!fs.existsSync(installedPath) || !sourcePath)
        return false;
    const installedSkillMd = path.join(installedPath, 'SKILL.md');
    const sourceSkillMd = path.join(sourcePath, 'SKILL.md');
    if (!fs.existsSync(installedSkillMd) || !fs.existsSync(sourceSkillMd))
        return false;
    try {
        if (normalizeContent(fs.readFileSync(installedSkillMd, 'utf-8')) !==
            normalizeContent(fs.readFileSync(sourceSkillMd, 'utf-8'))) {
            return false;
        }
    }
    catch {
        return false;
    }
    return directoriesMatch(path.join(installedPath, 'rules'), path.join(sourcePath, 'rules'));
}
/**
 * Compare a version home's skills against central. Returns the reconciliation diff.
 */
export function diffVersionSkills(agent, version) {
    // Plugin-provided skills (`plugins/<plugin>/skills`) are legitimate sources too, so a
    // skill materialized from a plugin is NOT an orphan (PHNX-3185). Without this
    // the detector offered to delete the entire plugin-installed skill library.
    const available = new Set([...listAllSkills(), ...listPluginSkillNames({ agent })]);
    // Goose and other native ~/.agents/skills consumers read central storage
    // directly. They intentionally have no per-version copy to diff, so every
    // available central skill is already current for every supported version.
    if (AGENTS[agent].nativeAgentsSkillsDir) {
        return {
            agent,
            version,
            toAdd: [],
            toUpdate: [],
            matched: Array.from(available).sort(),
            orphans: [],
        };
    }
    const installed = new Set(listSkillsInVersionHome(agent, version));
    const toAdd = [];
    const toUpdate = [];
    const matched = [];
    const orphans = [];
    for (const name of available) {
        if (!installed.has(name)) {
            toAdd.push(name);
        }
        else if (!versionSkillMatches(agent, version, name)) {
            toUpdate.push(name);
        }
        else {
            matched.push(name);
        }
    }
    for (const name of installed) {
        if (!available.has(name))
            orphans.push(name);
    }
    return { agent, version, toAdd: toAdd.sort(), toUpdate: toUpdate.sort(), matched, orphans: orphans.sort() };
}
/**
 * Walk a directory and return every file's path relative to base.
 * Follows no symlinks. Returns empty set if the dir doesn't exist.
 */
function walkRelativeFiles(base) {
    const out = new Set();
    if (!fs.existsSync(base))
        return out;
    const stack = [''];
    while (stack.length > 0) {
        const rel = stack.pop();
        const abs = path.join(base, rel);
        let entries;
        try {
            entries = fs.readdirSync(abs, { withFileTypes: true });
        }
        catch {
            continue;
        }
        for (const entry of entries) {
            const childRel = rel ? path.join(rel, entry.name) : entry.name;
            if (entry.isDirectory()) {
                stack.push(childRel);
            }
            else if (entry.isFile()) {
                out.add(childRel);
            }
        }
    }
    return out;
}
/**
 * Install a single skill from central into a specific version home.
 *
 * Copy mode preserves any files that exist in the version home but NOT in
 * central — e.g. user-populated `.env` alongside the git-tracked `.env.example`.
 * Without this, re-running `sync` whenever central content changes would wipe
 * local secrets.
 */
export function installSkillToVersion(agent, version, skillName, method = 'copy') {
    const sourcePath = resolveSkillSourcePath(skillName);
    if (!sourcePath) {
        return { success: false, error: `Skill '${skillName}' not found in central or any extra repo` };
    }
    const skillsDir = getVersionSkillsDir(agent, version);
    if (!fs.existsSync(skillsDir)) {
        fs.mkdirSync(skillsDir, { recursive: true });
    }
    const target = path.join(skillsDir, skillName);
    // Snapshot files unique to the version home so we can restore them after
    // the fresh copy (copy mode only; symlink shares the source so there's
    // nothing local to preserve — and writing a .env into a symlinked skill
    // would pollute the git-tracked source dir anyway).
    const preserved = [];
    if (method === 'copy' && fs.existsSync(target) && !fs.lstatSync(target).isSymbolicLink()) {
        const sourceFiles = walkRelativeFiles(sourcePath);
        const installedFiles = walkRelativeFiles(target);
        for (const rel of installedFiles) {
            if (sourceFiles.has(rel))
                continue;
            try {
                const abs = path.join(target, rel);
                preserved.push({
                    rel,
                    buf: fs.readFileSync(abs),
                    mode: fs.statSync(abs).mode,
                });
            }
            catch {
                // Unreadable — skip rather than fail the whole install
            }
        }
    }
    if (fs.existsSync(target) || fs.lstatSync(target, { throwIfNoEntry: false })) {
        try {
            fs.rmSync(target, { recursive: true, force: true });
        }
        catch (err) {
            return { success: false, error: `Failed to remove existing: ${err.message}` };
        }
    }
    try {
        if (method === 'symlink') {
            fs.symlinkSync(sourcePath, target, 'dir');
        }
        else {
            fs.cpSync(sourcePath, target, { recursive: true });
        }
    }
    catch (err) {
        return { success: false, error: `Failed to ${method}: ${err.message}` };
    }
    // Restore preserved files on top of the fresh copy.
    for (const { rel, buf, mode } of preserved) {
        try {
            const dest = path.join(target, rel);
            fs.mkdirSync(path.dirname(dest), { recursive: true });
            fs.writeFileSync(dest, buf);
            fs.chmodSync(dest, mode);
        }
        catch {
            // Best-effort; failure here shouldn't unwind the install
        }
    }
    emit('skill.install', { skill: skillName, agent, version });
    return { success: true };
}
/**
 * Remove a single skill from a specific version home.
 */
export function removeSkillFromVersion(agent, version, skillName) {
    const target = path.join(getVersionSkillsDir(agent, version), skillName);
    if (!fs.existsSync(target) && !fs.lstatSync(target, { throwIfNoEntry: false })) {
        return { success: true };
    }
    try {
        const stamp = new Date().toISOString().replace(/[:.]/g, '-');
        const trashDir = path.join(getTrashSkillsDir(), agent, version, skillName);
        const trashDest = path.join(trashDir, stamp);
        fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
        fs.renameSync(target, trashDest);
    }
    catch (err) {
        return { success: false, error: err.message };
    }
    return { success: true };
}
/**
 * Iterate all (agent, version) pairs that support skills and are installed,
 * optionally scoped to a single agent/version.
 */
export function iterSkillsCapableVersions(filter) {
    const pairs = [];
    const agents = filter?.agent ? [filter.agent] : capableAgents('skills');
    for (const agent of agents) {
        if (!capableAgents('skills').includes(agent))
            continue;
        const versions = listInstalledVersions(agent);
        for (const version of versions) {
            if (filter?.version && filter.version !== version)
                continue;
            pairs.push({ agent, version });
        }
    }
    return pairs;
}
export function uninstallSkill(skillName) {
    // Remove from central location
    const centralPath = path.join(getSkillsDir(), skillName);
    if (!fs.existsSync(centralPath)) {
        return { success: false, error: `Skill '${skillName}' not found` };
    }
    // Remove from all agents
    for (const agentId of capableAgents('skills')) {
        const agentSkillPath = path.join(getAgentSkillsDir(agentId), skillName);
        if (fs.existsSync(agentSkillPath)) {
            try {
                fs.rmSync(agentSkillPath, { recursive: true, force: true });
            }
            catch {
                // Ignore removal errors
            }
        }
    }
    try {
        fs.rmSync(centralPath, { recursive: true, force: true });
    }
    catch {
        // Ignore removal errors
    }
    emit('skill.remove', { skill: skillName });
    return { success: true };
}
export function listInstalledSkills() {
    const skills = new Map();
    const scan = (dir, source) => {
        if (!fs.existsSync(dir))
            return;
        try {
            const entries = fs.readdirSync(dir, { withFileTypes: true });
            for (const entry of entries) {
                if (!entry.isDirectory())
                    continue;
                // Skip hidden directories (e.g., .system)
                if (entry.name.startsWith('.'))
                    continue;
                // Primary wins on name collisions.
                if (skills.has(entry.name))
                    continue;
                const skillDir = path.join(dir, entry.name);
                const metadata = parseSkillMetadata(skillDir);
                const validation = validateSkillMetadata(metadata, entry.name);
                if (metadata) {
                    skills.set(entry.name, {
                        name: entry.name,
                        path: skillDir,
                        metadata,
                        ruleCount: countSkillRules(skillDir),
                        validation,
                        source,
                    });
                }
            }
        }
        catch {
            // Ignore errors
        }
    };
    scan(getSkillsDir());
    for (const extra of getEnabledExtraRepos()) {
        scan(path.join(extra.dir, 'skills'), extra.alias);
    }
    return skills;
}
export function listInstalledSkillsWithScope(agentId, cwd = process.cwd(), options) {
    const results = [];
    const seen = new Set();
    // Project-scoped skills
    const projectCandidates = [];
    const projectAgentsDir = getProjectAgentsDir(cwd);
    if (projectAgentsDir) {
        projectCandidates.push(path.join(projectAgentsDir, 'skills'));
    }
    projectCandidates.push(path.join(cwd, `.${agentId}`, 'skills'));
    for (const projectSkillsDir of projectCandidates) {
        if (!fs.existsSync(projectSkillsDir))
            continue;
        try {
            const entries = fs.readdirSync(projectSkillsDir, { withFileTypes: true });
            for (const entry of entries) {
                if (entry.name.startsWith('.'))
                    continue;
                const skillDir = path.join(projectSkillsDir, entry.name);
                let isDir = entry.isDirectory();
                if (entry.isSymbolicLink()) {
                    try {
                        const stat = fs.statSync(skillDir);
                        isDir = stat.isDirectory();
                    }
                    catch {
                        continue;
                    }
                }
                if (!isDir)
                    continue;
                const result = tryParseSkillMetadata(skillDir);
                if (result.metadata && !seen.has(entry.name)) {
                    results.push({
                        name: entry.name,
                        path: skillDir,
                        metadata: result.metadata,
                        ruleCount: countSkillRules(skillDir),
                        scope: 'project',
                        agent: agentId,
                    });
                    seen.add(entry.name);
                }
                else if (result.error && options?.errors && !seen.has(entry.name)) {
                    options.errors.push({
                        name: entry.name,
                        path: skillDir,
                        error: result.error,
                        scope: 'project',
                    });
                    seen.add(entry.name);
                }
            }
        }
        catch {
            // Ignore errors
        }
    }
    // User-scoped skills (version-aware when home is provided)
    const userSkillsDir = options?.home
        ? path.join(options.home, agentConfigDirName(agentId), 'skills')
        : getAgentSkillsDir(agentId);
    if (fs.existsSync(userSkillsDir)) {
        try {
            const entries = fs.readdirSync(userSkillsDir, { withFileTypes: true });
            for (const entry of entries) {
                if (entry.name.startsWith('.'))
                    continue;
                const skillDir = path.join(userSkillsDir, entry.name);
                let isDir = entry.isDirectory();
                if (entry.isSymbolicLink()) {
                    try {
                        const stat = fs.statSync(skillDir);
                        isDir = stat.isDirectory();
                    }
                    catch {
                        continue;
                    }
                }
                if (!isDir)
                    continue;
                const result = tryParseSkillMetadata(skillDir);
                if (result.metadata && !seen.has(entry.name)) {
                    results.push({
                        name: entry.name,
                        path: skillDir,
                        metadata: result.metadata,
                        ruleCount: countSkillRules(skillDir),
                        scope: 'user',
                        agent: agentId,
                    });
                    seen.add(entry.name);
                }
                else if (result.error && options?.errors && !seen.has(entry.name)) {
                    options.errors.push({
                        name: entry.name,
                        path: skillDir,
                        error: result.error,
                        scope: 'user',
                    });
                    seen.add(entry.name);
                }
            }
        }
        catch {
            // Ignore errors
        }
    }
    return results;
}
export function getSkillInfo(skillName) {
    const sourcePath = resolveSkillSourcePath(skillName);
    if (!sourcePath) {
        return null;
    }
    const metadata = parseSkillMetadata(sourcePath);
    const validation = validateSkillMetadata(metadata, skillName);
    if (!metadata) {
        return null;
    }
    return {
        name: skillName,
        path: sourcePath,
        metadata,
        ruleCount: countSkillRules(sourcePath),
        validation,
    };
}
export function getSkillRules(skillName) {
    const sourcePath = resolveSkillSourcePath(skillName);
    if (!sourcePath) {
        return [];
    }
    const rulesDir = path.join(sourcePath, 'rules');
    if (!fs.existsSync(rulesDir)) {
        return [];
    }
    try {
        const files = fs.readdirSync(rulesDir);
        return files.filter((f) => f.endsWith('.md')).map((f) => f.replace(/\.md$/, ''));
    }
    catch {
        return [];
    }
}
/**
 * Install a skill to central ~/.agents/skills/ directory only.
 * Does not create per-agent symlinks (shims handle that for synced agents).
 */
export function installSkillCentrally(sourcePath, skillName) {
    // Validate skill metadata (warnings only, don't block installation)
    const metadata = parseSkillMetadata(sourcePath);
    const validation = validateSkillMetadata(metadata, skillName);
    const allWarnings = [...validation.warnings];
    // Convert validation errors to warnings instead of blocking
    if (!validation.valid) {
        allWarnings.push(...validation.errors.map(e => `Validation: ${e}`));
    }
    ensureCentralSkillsDir();
    const centralPath = path.join(getSkillsDir(), skillName);
    // Resolve to absolute paths for comparison
    const resolvedSource = path.resolve(sourcePath);
    const resolvedCentral = path.resolve(centralPath);
    // If source is already the central path, nothing to copy
    if (resolvedSource === resolvedCentral) {
        return { success: true, warnings: allWarnings.length > 0 ? allWarnings : undefined };
    }
    // Remove existing if present
    if (fs.existsSync(centralPath)) {
        try {
            fs.rmSync(centralPath, { recursive: true, force: true });
        }
        catch {
            // Ignore removal errors
        }
    }
    try {
        fs.cpSync(sourcePath, centralPath, { recursive: true });
        return { success: true, warnings: allWarnings.length > 0 ? allWarnings : undefined };
    }
    catch (err) {
        return { success: false, error: `Failed to copy skill: ${err.message}` };
    }
}
