import { cp, lstat, mkdir, readFile, readdir, rm, symlink } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { Option } from 'commander';
import { z } from 'zod';
import { writeStream } from '../utils.js';
const AGENT_LABELS = {
    claude: 'Claude Code',
    codex: 'OpenAI Codex',
};
/** The directory each agent reads skills from, by scope. */
function agentSkillsDir(agent, scope, projectRoot) {
    const base = agent === 'claude' ? '.claude' : '.codex';
    const root = scope === 'user' ? os.homedir() : projectRoot;
    return path.join(root, base, 'skills');
}
/**
 * Resolve the bundled `skills/` directory at runtime.
 *
 * `skills/` ships as a sibling of `dist/`, so it sits one level higher than
 * `vendor/` (which is resolved relative to `dist/src/scc.js`). We probe a few
 * candidates and key off the presence of `occ/SKILL.md` so dev (`src/skills/`)
 * and built (`dist/src/skills/`) layouts both resolve deterministically.
 */
function getBundledSkillsDir() {
    const dir = path.dirname(fileURLToPath(import.meta.url));
    const candidates = [
        path.join(dir, '..', '..', 'skills'), // dev:   src/skills/      → <repo>/skills
        path.join(dir, '..', '..', '..', 'skills'), // built: dist/src/skills/ → <repo>/skills
        path.join(dir, '..', 'skills'), // defensive fallback
    ];
    for (const candidate of candidates) {
        if (existsSync(path.join(candidate, 'occ', 'SKILL.md')))
            return candidate;
    }
    throw new Error('occ: bundled skills directory not found. Reinstall @cesarandreslopez/occ.');
}
/** Names of bundled skills — directories under the skills root that hold a SKILL.md. */
async function listBundledSkillNames(skillsDir) {
    const entries = await readdir(skillsDir, { withFileTypes: true });
    const names = [];
    for (const entry of entries) {
        if (entry.isDirectory() && existsSync(path.join(skillsDir, entry.name, 'SKILL.md'))) {
            names.push(entry.name);
        }
    }
    return names.sort();
}
/**
 * Minimal YAML frontmatter reader — extracts top-level `name` and `description`
 * (including folded/literal block scalars) without pulling in a YAML dependency.
 */
function parseSkillFrontmatter(markdown) {
    if (!markdown.startsWith('---'))
        return {};
    const end = markdown.indexOf('\n---', 3);
    if (end === -1)
        return {};
    const lines = markdown.slice(3, end).split('\n');
    const result = {};
    for (let i = 0; i < lines.length; i++) {
        const match = lines[i].match(/^([A-Za-z_][\w-]*):\s*(.*)$/);
        if (!match)
            continue;
        const key = match[1];
        let value = match[2];
        if (['>', '>-', '>+', '|', '|-', '|+'].includes(value)) {
            const collected = [];
            let j = i + 1;
            while (j < lines.length && (lines[j].startsWith('  ') || lines[j].trim() === '')) {
                collected.push(lines[j].trim());
                j++;
            }
            i = j - 1;
            value = (value.startsWith('|') ? collected.join('\n') : collected.join(' ')).trim();
        }
        if (key === 'name')
            result.name = value;
        else if (key === 'description')
            result.description = value;
    }
    return result;
}
function truncate(text, max) {
    const collapsed = text.replace(/\s+/g, ' ').trim();
    return collapsed.length > max ? `${collapsed.slice(0, max - 1)}…` : collapsed;
}
/**
 * Probe a path with lstat so symlinks are seen regardless of their target.
 * existsSync follows symlinks and reports a dangling link as non-existent,
 * which would leave a broken link occupying the destination path.
 */
export async function pathExists(target) {
    try {
        await lstat(target);
        return true;
    }
    catch {
        return false;
    }
}
const InstallOptionsSchema = z
    .object({
    agent: z.enum(['claude', 'codex', 'all']).optional(),
    user: z.boolean().optional(),
    project: z.boolean().optional(),
    projectRoot: z.string().optional(),
    force: z.boolean().optional(),
    link: z.boolean().optional(),
})
    .passthrough();
const ListOptionsSchema = z.object({ format: z.string().optional() }).passthrough();
function resolveAgents(agent) {
    if (agent === 'claude')
        return ['claude'];
    if (agent === 'codex')
        return ['codex'];
    return ['claude', 'codex'];
}
export function registerSkillsCommands(program) {
    const skills = program.command('skills').description('Install and inspect the Agent Skills bundled with occ');
    // occ skills path — print the bundled skills directory
    skills
        .command('path')
        .description("Print the path to occ's bundled Agent Skills")
        .action(async () => {
        await writeStream(process.stdout, `${getBundledSkillsDir()}\n`);
    });
    // occ skills list — list bundled skills with their frontmatter
    skills
        .command('list')
        .description('List the Agent Skills bundled with occ')
        .option('--format <type>', 'output format: tabular or json', 'tabular')
        .action(async (_options, command) => {
        // optsWithGlobals: the root program also defines `--format`, so it is a
        // global option — opts() alone would not see a leaf-level `--format json`.
        const options = ListOptionsSchema.parse(command.optsWithGlobals());
        const skillsDir = getBundledSkillsDir();
        const names = await listBundledSkillNames(skillsDir);
        const entries = await Promise.all(names.map(async (name) => {
            const frontmatter = parseSkillFrontmatter(await readFile(path.join(skillsDir, name, 'SKILL.md'), 'utf8'));
            return { name: frontmatter.name ?? name, description: frontmatter.description ?? '' };
        }));
        if (options.format === 'json') {
            await writeStream(process.stdout, `${JSON.stringify({ skillsDir, skills: entries }, null, 2)}\n`);
            return;
        }
        const rendered = entries.map(entry => `${entry.name}\n  ${truncate(entry.description, 280)}`).join('\n\n');
        await writeStream(process.stdout, `${rendered || 'No bundled skills found.'}\n`);
    });
    // occ skills install — copy (or symlink) the skill into agent skill directories
    const install = skills.command('install').description('Install occ Agent Skills into one or more coding agents');
    install
        .addOption(new Option('--agent <agent>', 'target agent').choices(['claude', 'codex', 'all']).default('all'))
        .option('--user', 'install into the user-level skills directory (default)')
        .option('--project', 'install into the project-level skills directory')
        .option('--project-root <dir>', 'project root used with --project', process.cwd())
        .option('--force', 'overwrite an already-installed skill')
        .option('--link', 'symlink instead of copying (auto-updates with the package, not on Windows)')
        .action(async (_options, command) => {
        try {
            const options = InstallOptionsSchema.parse(command.optsWithGlobals());
            const skillsDir = getBundledSkillsDir();
            const names = await listBundledSkillNames(skillsDir);
            const scope = options.project ? 'project' : 'user';
            const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
            const agents = resolveAgents(options.agent);
            const lines = [];
            for (const agent of agents) {
                const targetDir = agentSkillsDir(agent, scope, projectRoot);
                await mkdir(targetDir, { recursive: true });
                for (const name of names) {
                    const source = path.join(skillsDir, name);
                    const dest = path.join(targetDir, name);
                    const destExists = await pathExists(dest);
                    if (destExists && !options.force) {
                        lines.push(`• ${AGENT_LABELS[agent]}: "${name}" already installed at ${dest} (use --force to overwrite)`);
                        continue;
                    }
                    if (destExists)
                        await rm(dest, { recursive: true, force: true });
                    if (options.link) {
                        await symlink(source, dest, 'dir');
                        lines.push(`✓ ${AGENT_LABELS[agent]}: linked "${name}" → ${dest}`);
                    }
                    else {
                        await cp(source, dest, { recursive: true });
                        lines.push(`✓ ${AGENT_LABELS[agent]}: installed "${name}" → ${dest}`);
                    }
                }
            }
            lines.push('', 'Restart your agent (or reload skills) to pick up the new skill.');
            await writeStream(process.stdout, `${lines.join('\n')}\n`);
        }
        catch (error) {
            await writeStream(process.stderr, `Error: ${error.message}\n`);
            process.exitCode = 1;
        }
    });
}
//# sourceMappingURL=command.js.map