/**
 * olam skills source — manage registered skill sources.
 *
 *   olam skills source list
 *   olam skills source add --name <n> --git-url <url> [--branch <b>]
 *   olam skills source remove <id>
 *   olam skills source pull <id>
 *   olam skills source show <id>
 */
import * as readline from 'node:readline';
import pc from 'picocolors';
import { listSkillSources, getSkillSource, getSkillSourceByName, addSkillSource, removeSkillSource, reorderSkillSource, updateSkillSource, cloneSkillSource, pullSkillSource, removeSkillSourceClone, skillSourceClonePath, appendTrustAudit, redactUrl, installSkillsHookToFile, isSkillsHookInstalled, skillsHookSettingsPathFor, syncSkills, readSourceConfig, resolveEffectivePrefix, } from '@olam/core/src/skill-sources/index.js';
import { printError, printSuccess, printInfo, printHeader } from '../output.js';
import { refreshShims } from './flywheel/install-shims.js';
function asMessage(err) {
    return err instanceof Error ? err.message : String(err);
}
/**
 * Capability-class trust gate (C4 / T6 mitigation). Registering a source
 * grants symlink-into-`~/.claude/` permission; auditable + gated:
 *  - `--trust` → grant (audit: method=flag)
 *  - non-TTY + no flag → refuse (audit: action=auto-rejected)
 *  - TTY + no flag → prompt; ack → grant, cancel → refuse
 *
 * Injectable `promptFn` lets tests skip readline by passing a function that
 * returns 'y' / 'n'. Production code uses the readline-based default.
 */
async function decideTrust(opts) {
    if (opts.trustFlag) {
        return { granted: true, method: 'flag' };
    }
    if (!opts.isTTY) {
        return {
            granted: false,
            method: 'auto-reject-tty',
            reason: 'non-interactive context (no TTY) requires explicit `--trust` flag. ' +
                'Adding a skill source grants it permission to symlink into ~/.claude/ — ' +
                'pass `--trust` to acknowledge, or re-run interactively.',
        };
    }
    const fn = opts.promptFn ?? (() => defaultTrustPrompt(opts.gitUrl));
    const answer = (await fn()).trim().toLowerCase();
    const yes = answer === 'y' || answer === 'yes';
    return { granted: yes, method: 'interactive' };
}
function defaultTrustPrompt(gitUrl) {
    return new Promise((resolve) => {
        const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
        process.stdout.write(`${pc.yellow('Trust gate:')} register "${gitUrl}" as a skill source?\n` +
            '  This grants the source permission to symlink artifacts into\n' +
            '  ~/.claude/ on every `olam skills sync`.\n' +
            `Type ${pc.bold('y')} to add + trust, anything else to cancel: `);
        rl.question('', (a) => {
            rl.close();
            resolve(a);
        });
    });
}
function defaultSourcePrefixPrompt(input) {
    return new Promise((resolve) => {
        const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
        const cfg = input.newConfig;
        const prefixLabel = cfg.prefix !== undefined ? `'${cfg.prefix}:'` : '(no prefix)';
        const scopeLabel = cfg.prefixScope !== undefined && cfg.prefixScope.length > 0
            ? ` (scope: ${cfg.prefixScope.join(',')})`
            : '';
        process.stdout.write(`\n${pc.cyan('Source-config:')} ${input.sourceName} suggests prefix ${pc.bold(prefixLabel)}${scopeLabel}.\n` +
            `Adopt for this host? ${pc.dim('[Y/n] ')}`);
        rl.question('', (a) => {
            rl.close();
            resolve(!/^n(o)?$/i.test(a.trim()));
        });
    });
}
function defaultPostAddPrompt(input) {
    return new Promise((resolve) => {
        const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
        process.stdout.write(`\n${pc.bold('Sync now?')} ${pc.dim('[Y/n] ')}`);
        rl.question('', (syncAnswer) => {
            const sync = !/^n(o)?$/i.test(syncAnswer.trim());
            if (input.hookAlreadyInstalled) {
                rl.close();
                resolve({ syncNow: sync, installHook: false });
                return;
            }
            process.stdout.write(`${pc.bold('Install SessionStart hook so future sessions auto-sync?')} ${pc.dim('[Y/n] ')}`);
            rl.question('', (hookAnswer) => {
                rl.close();
                const installHook = !/^n(o)?$/i.test(hookAnswer.trim());
                resolve({ syncNow: sync, installHook });
            });
        });
    });
}
/**
 * Parse a comma-separated prefix-scope string like "skill", "agent", or "skill,agent".
 * Returns the parsed array, or null if any token is invalid.
 */
function parsePrefixScope(raw) {
    const tokens = raw.split(',').map((t) => t.trim()).filter((t) => t.length > 0);
    if (tokens.length === 0)
        return [];
    const valid = new Set(['skill', 'agent']);
    for (const t of tokens) {
        if (!valid.has(t))
            return null;
    }
    // De-dup while preserving order.
    return [...new Set(tokens)];
}
export function registerSkillsSource(program) {
    const skills = program.command('skills').description('Manage skill sources and synchronization');
    const source = skills.command('source').description('Manage registered skill sources');
    source
        .command('list')
        .description('List all registered skill sources')
        .option('--json', 'Emit a machine-readable JSON array instead of the human table')
        .action((opts) => {
        const all = listSkillSources();
        // --json: stable, scriptable contract. Always an array (empty when no
        // sources). Computes the effective-prefix view per source so consumers
        // don't have to re-implement the operator+source merge.
        if (opts.json === true) {
            const payload = all.map((s, i) => {
                const clonePath = skillSourceClonePath(s.id);
                let srcCfg;
                try {
                    srcCfg = readSourceConfig(clonePath);
                }
                catch {
                    srcCfg = undefined;
                }
                const eff = resolveEffectivePrefix(s, srcCfg);
                return {
                    ordinal: i + 1,
                    id: s.id,
                    name: s.name,
                    gitUrl: s.gitUrl,
                    branch: s.branch,
                    addedAt: s.addedAt,
                    lastPulledSha: s.lastPulledSha ?? null,
                    clonePath,
                    prefix: s.prefix ?? null,
                    prefixScope: s.prefixScope ? [...s.prefixScope] : null,
                    effective: {
                        prefix: eff.prefix ?? null,
                        prefixScope: eff.prefixScope ? [...eff.prefixScope] : null,
                        origin: eff.origin,
                    },
                };
            });
            console.log(JSON.stringify(payload, null, 2));
            return;
        }
        if (all.length === 0) {
            console.log(pc.dim('0 skill source(s) registered. Add one with: olam skills source add --name <n> --git-url <url>'));
            return;
        }
        printHeader(`${all.length} skill source(s)`);
        // Show scope column when any source has a non-default prefixScope.
        const DEFAULT_SCOPE_REPR = 'skill,agent';
        const showScopeCol = all.some((s) => s.prefixScope !== undefined && s.prefixScope.slice().sort().join(',') !== 'agent,skill');
        // A8 (ADR-021): compute per-source effective-prefix origin AT LIST TIME.
        // Read-only — does NOT run a sync, does NOT mutate state. Origin column
        // is shown only when at least one source ships a source-config (keeps
        // default output unchanged for operators not using the feature).
        const sourceConfigs = all.map((s) => {
            const clonePath = skillSourceClonePath(s.id);
            // Quiet read: pass undefined sourceId so malformed-warnings don't
            // spam `list` output (the warning fires on sync where it's actionable).
            try {
                return readSourceConfig(clonePath);
            }
            catch {
                return undefined;
            }
        });
        const showOriginCol = sourceConfigs.some((c) => c !== undefined);
        // Ordinal prefix exposes the multi-source precedence order
        // (first-registered wins). Mutate via `olam skills source reorder`.
        for (let i = 0; i < all.length; i += 1) {
            const s = all[i];
            const when = new Date(s.addedAt).toISOString().slice(0, 10);
            const sha = s.lastPulledSha ? s.lastPulledSha.slice(0, 8) : pc.dim('(unpulled)');
            const ord = pc.dim(`[${i + 1}]`);
            const prefixCol = s.prefix ? pc.cyan(s.prefix.padEnd(12)) : pc.dim('—'.padEnd(12));
            const scopeDisplay = s.prefixScope !== undefined ? s.prefixScope.join(',') : DEFAULT_SCOPE_REPR;
            const scopeCol = showScopeCol ? ` ${pc.yellow(scopeDisplay.padEnd(12))}` : '';
            let originCol = '';
            if (showOriginCol) {
                const eff = resolveEffectivePrefix(s, sourceConfigs[i]);
                const tag = eff.origin === 'operator'
                    ? pc.dim('(operator)')
                    : eff.origin === 'source'
                        ? pc.dim('(source)')
                        : '';
                originCol = ` ${tag.padEnd(10)}`;
            }
            console.log(`  ${ord} ${pc.bold(s.id.padEnd(14))} ${s.name.padEnd(24)} ${s.branch.padEnd(12)} ${sha.padEnd(12)} ${prefixCol}${originCol}${scopeCol} ${pc.dim(when)} ${pc.dim(s.gitUrl)}`);
        }
    });
    source
        .command('add')
        .description('Register and clone a skill source (T6 capability-class: requires --trust OR interactive ack)')
        .requiredOption('--name <name>', 'Display name (lowercase, digits, dash)')
        .requiredOption('--git-url <url>', 'git URL (https://, git@, ssh://, file://, or absolute path)')
        .option('--branch <branch>', 'Branch to track', 'main')
        .option('--prefix <prefix>', 'Deploy prefix: skills+agents from this source deploy as <prefix>:<canonical-name> (e.g. "atl", "pln"). Must match /^[a-z0-9][a-z0-9_-]{0,38}$/')
        .option('--prefix-scope <scope>', 'Comma-separated list of artifact kinds to rename when prefix is set. Valid values: skill, agent. Default: skill,agent. E.g. "--prefix-scope=skill" to rename only skills.')
        .option('--trust', 'Acknowledge that registering grants symlink-into-~/.claude permission (skips interactive picker)')
        .option('--auto-accept-source-prefix', "ADR-021: when the source ships shared/source-config.yaml with a prefix suggestion, adopt it silently (no prompt). Useful for CI / scripted onboarding.")
        .option('--accept-source-prefix-changes', 'ADR-021: forwarded to the post-add sync (if --sync-now) — accept future source-prefix CHANGES silently.')
        // Post-add follow-up flags. In TTY context the operator is prompted;
        // these flags let scripts override or skip the prompt non-interactively.
        .option('--sync-now', 'Run `olam skills sync` immediately after add (skips interactive prompt)')
        .option('--no-sync-now', 'Skip the post-add sync (skips interactive prompt)')
        .option('--install-hook', 'Install the SessionStart hook after add (skips interactive prompt)')
        .option('--no-install-hook', 'Skip installing the SessionStart hook (skips interactive prompt)')
        .option('--hook-scope <scope>', 'When installing the hook (post-add): project (<cwd>/.claude) or user (~/.claude)', 'project')
        .action(async (opts) => {
        // Validate --prefix early before any I/O (regex: same shape as schema).
        const PREFIX_PATTERN = /^[a-z0-9][a-z0-9_-]{0,38}$/;
        if (opts.prefix !== undefined && !PREFIX_PATTERN.test(opts.prefix)) {
            printError(`prefix must match /^[a-z0-9][a-z0-9_-]{0,38}$/ — got "${opts.prefix}"`);
            process.exitCode = 1;
            return;
        }
        // Validate --prefix-scope early.
        let parsedPrefixScope;
        if (opts.prefixScope !== undefined) {
            const parsed = parsePrefixScope(opts.prefixScope);
            if (parsed === null) {
                printError(`--prefix-scope must be a comma-separated list of "skill" and/or "agent" — got "${opts.prefixScope}"`);
                process.exitCode = 1;
                return;
            }
            parsedPrefixScope = parsed;
        }
        // T6 / C4 — capability-class trust gate. Audits every decision.
        const isTTY = Boolean(process.stdout.isTTY);
        const testPromptFn = globalThis.__olamTestTrustPrompt;
        let decision;
        try {
            decision = await decideTrust({
                trustFlag: opts.trust === true,
                gitUrl: opts.gitUrl,
                isTTY,
                promptFn: testPromptFn,
            });
        }
        catch (err) {
            printError(`trust gate errored: ${asMessage(err)}`);
            process.exitCode = 1;
            return;
        }
        if (!decision.granted) {
            try {
                appendTrustAudit({
                    gitUrl: redactUrl(opts.gitUrl),
                    action: decision.method === 'auto-reject-tty' ? 'auto-rejected' : 'rejected',
                    trustMethod: decision.method,
                    ...(decision.reason ? { note: decision.reason } : {}),
                });
            }
            catch {
                // audit-log failure must not block the user-facing error path
            }
            printError(decision.reason ?? 'trust gate declined; skill source NOT added');
            process.exitCode = 1;
            return;
        }
        let entry;
        try {
            // `trustMethod` is required at the store layer — CP3 trust-coverage
            // fix. addSkillSource() writes the 'added' audit entry internally;
            // CLI no longer duplicates the audit append on success.
            entry = addSkillSource({
                name: opts.name,
                gitUrl: opts.gitUrl,
                branch: opts.branch,
                trustMethod: decision.method,
            });
            // Apply --prefix / --prefix-scope if provided.
            if (opts.prefix !== undefined || parsedPrefixScope !== undefined) {
                updateSkillSource(entry.id, {
                    ...(opts.prefix !== undefined ? { prefix: opts.prefix } : {}),
                    ...(parsedPrefixScope !== undefined ? { prefixScope: parsedPrefixScope } : {}),
                });
            }
        }
        catch (err) {
            try {
                appendTrustAudit({
                    gitUrl: redactUrl(opts.gitUrl),
                    action: 'failed',
                    trustMethod: decision.method,
                    note: `addSkillSource: ${asMessage(err)}`,
                });
            }
            catch {
                // best-effort
            }
            printError(asMessage(err));
            process.exitCode = 1;
            return;
        }
        try {
            const result = cloneSkillSource({
                gitUrl: entry.gitUrl,
                branch: entry.branch,
                id: entry.id,
            });
            updateSkillSource(entry.id, { lastPulledSha: result.headSha });
            // 'added' audit entry already written by addSkillSource() itself.
            printSuccess(`registered skill source "${entry.name}" (${entry.id}) at ${result.clonePath} @ ${result.headSha.slice(0, 8)}`);
        }
        catch (err) {
            // Roll back the state-store entry so add stays atomic
            try {
                removeSkillSource(entry.id);
            }
            catch {
                // best-effort rollback
            }
            try {
                appendTrustAudit({
                    gitUrl: redactUrl(entry.gitUrl),
                    action: 'failed',
                    trustMethod: decision.method,
                    sourceId: entry.id,
                    note: `clone: ${asMessage(err)}`,
                });
            }
            catch {
                // best-effort
            }
            printError(asMessage(err));
            process.exitCode = 1;
            return;
        }
        // -------------------------------------------------------------
        // ADR-021 source-config prefix gate (A6). Fires after clone +
        // before the post-add picker. Only when:
        //   - source ships shared/source-config.yaml that parses cleanly
        //   - source-config has at least one prefix field set
        //   - operator did NOT pass --prefix (explicit operator intent wins)
        // Persists lastSeenSourcePrefix on BOTH accept and reject so the
        // sync-time gate doesn't re-prompt for the same content. Rejection
        // is silent — operator gets canonical names from this source.
        // -------------------------------------------------------------
        if (opts.prefix === undefined) {
            const clonePath = skillSourceClonePath(entry.id);
            const srcCfg = readSourceConfig(clonePath, entry.id);
            const hasPrefixField = srcCfg !== undefined &&
                (srcCfg.prefix !== undefined ||
                    (srcCfg.prefixScope !== undefined && srcCfg.prefixScope.length > 0));
            if (srcCfg !== undefined && hasPrefixField) {
                const testSrcPrefixPrompt = globalThis.__olamTestSourcePrefixPrompt;
                const sourceConfigIsTTY = Boolean(process.stdout.isTTY) || testSrcPrefixPrompt !== undefined;
                let consented = false;
                let method;
                if (opts.autoAcceptSourcePrefix === true) {
                    consented = true;
                    method = 'flag';
                }
                else if (sourceConfigIsTTY) {
                    try {
                        const promptFn = testSrcPrefixPrompt ?? defaultSourcePrefixPrompt;
                        consented = await promptFn({ sourceName: entry.name, newConfig: srcCfg });
                        method = 'interactive';
                    }
                    catch (err) {
                        printError(`source-prefix prompt errored: ${asMessage(err)}`);
                        consented = false;
                    }
                }
                // else: non-TTY without flag → silent skip; let sync-time gate handle.
                if (consented) {
                    try {
                        updateSkillSource(entry.id, {
                            ...(srcCfg.prefix !== undefined ? { prefix: srcCfg.prefix } : {}),
                            ...(srcCfg.prefixScope !== undefined ? { prefixScope: srcCfg.prefixScope } : {}),
                            lastSeenSourcePrefix: srcCfg,
                        });
                        appendTrustAudit({
                            gitUrl: 'internal:source-prefix-adopted',
                            action: 'source-prefix-adopted',
                            trustMethod: method ?? 'flag',
                            sourceId: entry.id,
                            note: `snapshot=${JSON.stringify(srcCfg)}`,
                        });
                        printInfo('source-config', `adopted prefix '${srcCfg.prefix ?? '(none)'}'${srcCfg.prefixScope ? ` scope=${srcCfg.prefixScope.join(',')}` : ''} from ${entry.name}`);
                    }
                    catch (err) {
                        printError(`source-prefix adopt failed: ${asMessage(err)}`);
                    }
                }
                else if (method === 'interactive') {
                    // TTY reject: persist snapshot only so sync gate stays quiet
                    // until source-config CHANGES. Audit-log records the decline.
                    try {
                        updateSkillSource(entry.id, { lastSeenSourcePrefix: srcCfg });
                        appendTrustAudit({
                            gitUrl: 'internal:source-prefix-adopted',
                            action: 'rejected',
                            trustMethod: 'interactive',
                            sourceId: entry.id,
                            note: `declined-snapshot=${JSON.stringify(srcCfg)}`,
                        });
                    }
                    catch {
                        // best-effort
                    }
                }
            }
        }
        // -------------------------------------------------------------
        // Post-add follow-ups (#4): offer sync + hook install to the
        // operator. Flag-driven in scripts; prompt-driven in TTY.
        // -------------------------------------------------------------
        const hookScope = opts.hookScope === 'user' ? 'user' : 'project';
        const hookFilePath = skillsHookSettingsPathFor(hookScope);
        const hookAlreadyInstalled = isSkillsHookInstalled(hookFilePath);
        const flagSyncSet = opts.syncNow !== undefined;
        const flagHookSet = opts.installHook !== undefined;
        const decideOutcome = async () => {
            // Explicit flags always win, even in TTY: scripts that pass
            // `--sync-now` shouldn't unexpectedly hit an interactive prompt
            // when run from a terminal.
            if (flagSyncSet && (flagHookSet || hookAlreadyInstalled)) {
                return {
                    syncNow: opts.syncNow === true,
                    installHook: hookAlreadyInstalled ? false : opts.installHook === true,
                };
            }
            if (!process.stdout.isTTY) {
                // Non-interactive: use whichever flags are set, default false
                // for the rest. Preserves the pre-#4 silent behavior.
                return {
                    syncNow: opts.syncNow === true,
                    installHook: hookAlreadyInstalled ? false : opts.installHook === true,
                };
            }
            const testPostAddPrompt = globalThis
                .__olamTestPostAddPrompt;
            const promptFn = testPostAddPrompt ?? defaultPostAddPrompt;
            const fromPrompt = await promptFn({ hookAlreadyInstalled });
            // Flag-overrides on top of the prompt's defaults (a script using
            // --sync-now in a TTY still wants that to stick).
            return {
                syncNow: flagSyncSet ? opts.syncNow === true : fromPrompt.syncNow,
                installHook: hookAlreadyInstalled
                    ? false
                    : flagHookSet
                        ? opts.installHook === true
                        : fromPrompt.installHook,
            };
        };
        let outcome;
        try {
            outcome = await decideOutcome();
        }
        catch (err) {
            printError(`post-add picker errored: ${asMessage(err)}`);
            process.exitCode = 1;
            return;
        }
        if (outcome.installHook) {
            try {
                const hookResult = installSkillsHookToFile(hookFilePath);
                switch (hookResult.status) {
                    case 'installed':
                        printInfo('hook', `installed (${hookScope} scope) at ${hookFilePath}`);
                        if (hookResult.backupPath)
                            printInfo('backup', hookResult.backupPath);
                        break;
                    case 'already-present':
                        printInfo('hook', `already installed at ${hookFilePath}`);
                        break;
                    case 'no-op':
                        printInfo('hook', `no change at ${hookFilePath}`);
                        break;
                }
            }
            catch (err) {
                // Hook install failure must NOT roll back the source — the
                // source registration is still valid + useful. Surface a
                // warning + audit row instead.
                printError(`install-hook failed: ${asMessage(err)}`);
                try {
                    appendTrustAudit({
                        gitUrl: redactUrl(entry.gitUrl),
                        action: 'failed',
                        trustMethod: decision.method,
                        sourceId: entry.id,
                        note: `post-add install-hook: ${asMessage(err)}`,
                    });
                }
                catch {
                    // best-effort
                }
            }
        }
        if (outcome.syncNow) {
            try {
                const summary = await syncSkills({
                    autoAcceptSourcePrefix: opts.autoAcceptSourcePrefix === true,
                    acceptSourcePrefixChanges: opts.acceptSourcePrefixChanges === true,
                });
                printInfo('sync', `${summary.artifactCount} artifact(s) deployed`);
            }
            catch (err) {
                printError(`sync failed: ${asMessage(err)}`);
                try {
                    appendTrustAudit({
                        gitUrl: redactUrl(entry.gitUrl),
                        action: 'failed',
                        trustMethod: decision.method,
                        sourceId: entry.id,
                        note: `post-add sync: ${asMessage(err)}`,
                    });
                }
                catch {
                    // best-effort
                }
            }
        }
        // Audit the picker outcome ONLY when the picker actually ran (TTY
        // context) or when post-add flags were explicitly set. Skipping
        // the row in non-TTY + no-flag context preserves the pre-Phase-D
        // single-row-per-add invariant existing audit tests rely on. When
        // emitted, the row carries `action: 'added'` (passthrough-extended
        // with a post-add note) so audit:trust-coverage stays satisfied
        // even if a future caller checks counts beyond first match.
        const pickerRan = flagSyncSet || flagHookSet || Boolean(process.stdout.isTTY);
        if (pickerRan) {
            try {
                const syncNote = outcome.syncNow ? 'yes' : 'no';
                const hookNote = hookAlreadyInstalled
                    ? 'already-installed'
                    : outcome.installHook
                        ? 'yes'
                        : 'no';
                appendTrustAudit({
                    gitUrl: redactUrl(entry.gitUrl),
                    action: 'added',
                    trustMethod: decision.method,
                    sourceId: entry.id,
                    note: `post-add: sync=${syncNote} hook=${hookNote}`,
                });
            }
            catch {
                // best-effort
            }
        }
    });
    source
        .command('remove')
        .description('Remove a registered skill source (deletes its clone)')
        .argument('<id>', 'Skill source id (from "olam skills source list")')
        .action((id) => {
        try {
            removeSkillSourceClone(id);
            removeSkillSource(id);
            printSuccess(`removed skill source "${id}"`);
        }
        catch (err) {
            printError(asMessage(err));
            process.exitCode = 1;
        }
    });
    source
        .command('pull')
        .description('Fetch + reset the clone to upstream HEAD')
        .argument('<id>', 'Skill source id')
        .action((id) => {
        try {
            const entry = getSkillSource(id);
            if (!entry) {
                printError(`skill source "${id}" is not registered`);
                process.exitCode = 1;
                return;
            }
            const result = pullSkillSource({
                gitUrl: entry.gitUrl,
                branch: entry.branch,
                id: entry.id,
            });
            updateSkillSource(entry.id, { lastPulledSha: result.headSha });
            printSuccess(`pulled "${entry.name}" → ${result.headSha.slice(0, 8)}`);
            // B2 — closes K1 OQ20: refresh ~/.claude/scripts/<shim-target> after
            // every pull. syncSkills already owns the ~/.claude/scripts/ write
            // access; layering the flywheel-shim refresh here keeps the contract
            // close to the path-touch that necessitates it (atlas-toolbox pull
            // could have changed shipped scripts).
            const shimSummary = refreshShims({});
            if (shimSummary.written + shimSummary.overwritten > 0) {
                printInfo('flywheel shims refreshed', `${shimSummary.written} written, ${shimSummary.overwritten} overwritten, ${shimSummary.unchanged} unchanged${shimSummary.preserved > 0 ? `, ${shimSummary.preserved} PRESERVED (operator action needed — run \`olam flywheel install-shims --force\`)` : ''}`);
            }
        }
        catch (err) {
            printError(asMessage(err));
            process.exitCode = 1;
        }
    });
    source
        .command('reorder')
        .description('Move a registered source to a new ordinal (1-indexed; mutates precedence)')
        .argument('<id>', 'Skill source id (from "olam skills source list")')
        .argument('<position>', 'New 1-indexed ordinal (1 = first / highest precedence)')
        .action((id, positionRaw) => {
        const position = Number.parseInt(positionRaw, 10);
        if (!Number.isInteger(position)) {
            printError(`position must be an integer (got "${positionRaw}")`);
            process.exitCode = 1;
            return;
        }
        try {
            const next = reorderSkillSource(id, position);
            const idx = next.findIndex((s) => s.id === id);
            printSuccess(`reordered "${id}" → position ${idx + 1} of ${next.length}`);
        }
        catch (err) {
            printError(asMessage(err));
            process.exitCode = 1;
        }
    });
    source
        .command('show')
        .description('Show details for a single skill source (lookup by id OR name)')
        .argument('<idOrName>', 'Skill source id or display name')
        .option('--json', 'Emit a machine-readable JSON object instead of the four human sections')
        .action((idOrName, opts) => {
        // A9 (ADR-021): lookup tries id first, then name. Preserves existing
        // `show <id>` behavior while adding `show <name>` per ADR-021.
        const entry = getSkillSource(idOrName) ?? getSkillSourceByName(idOrName);
        if (!entry) {
            printError(`skill source "${idOrName}" is not registered. Run \`olam skills source list\` to see registered sources.`);
            process.exitCode = 1;
            return;
        }
        const when = new Date(entry.addedAt).toISOString();
        const clonePath = skillSourceClonePath(entry.id);
        // --json: same four logical sections, nested as one object. Mirrors the
        // human render below so consumers and operators see identical data.
        if (opts.json === true) {
            let srcCfg;
            try {
                srcCfg = readSourceConfig(clonePath, entry.id);
            }
            catch {
                srcCfg = undefined;
            }
            const effJson = resolveEffectivePrefix(entry, srcCfg);
            const payload = {
                registered: {
                    id: entry.id,
                    name: entry.name,
                    gitUrl: entry.gitUrl,
                    branch: entry.branch,
                    addedAt: entry.addedAt,
                    lastPulledSha: entry.lastPulledSha ?? null,
                    clonePath,
                },
                sourceConfig: srcCfg === undefined
                    ? null
                    : { prefix: srcCfg.prefix ?? null, prefixScope: srcCfg.prefixScope ? [...srcCfg.prefixScope] : null },
                operator: {
                    prefix: entry.prefix ?? null,
                    prefixScope: entry.prefixScope ? [...entry.prefixScope] : null,
                    lastSeenSourcePrefix: entry.lastSeenSourcePrefix ?? null,
                },
                effective: {
                    prefix: effJson.prefix ?? null,
                    prefixScope: effJson.prefixScope ? [...effJson.prefixScope] : null,
                    origin: effJson.origin,
                },
            };
            console.log(JSON.stringify(payload, null, 2));
            return;
        }
        // Section 1: registered (operator-config) fields.
        console.log(pc.bold('▸ registered'));
        console.log(`  ${pc.bold('id:')}            ${entry.id}`);
        console.log(`  ${pc.bold('name:')}          ${entry.name}`);
        console.log(`  ${pc.bold('gitUrl:')}        ${entry.gitUrl}`);
        console.log(`  ${pc.bold('branch:')}        ${entry.branch}`);
        console.log(`  ${pc.bold('addedAt:')}       ${when}`);
        console.log(`  ${pc.bold('lastPulledSha:')} ${entry.lastPulledSha ?? pc.dim('(unpulled)')}`);
        console.log(`  ${pc.bold('clonePath:')}     ${clonePath}`);
        // Section 2: source-config snapshot (from <clone>/shared/source-config.yaml).
        let sourceConfig;
        try {
            sourceConfig = readSourceConfig(clonePath, entry.id);
        }
        catch {
            sourceConfig = undefined;
        }
        console.log(pc.bold('▸ source-config (from clone)'));
        if (sourceConfig === undefined) {
            console.log(`  ${pc.dim('(no shared/source-config.yaml in clone)')}`);
        }
        else {
            console.log(`  ${pc.bold('prefix:')}        ${sourceConfig.prefix ?? pc.dim('—')}`);
            console.log(`  ${pc.bold('prefixScope:')}   ${sourceConfig.prefixScope?.join(',') ?? pc.dim('—')}`);
        }
        // Section 3: operator overrides (from ~/.olam/config.json skillSources[]).
        console.log(pc.bold('▸ operator overrides'));
        console.log(`  ${pc.bold('prefix:')}        ${entry.prefix ?? pc.dim('—')}`);
        console.log(`  ${pc.bold('prefixScope:')}   ${entry.prefixScope?.join(',') ?? pc.dim('—')}`);
        console.log(`  ${pc.bold('lastSeen:')}      ${entry.lastSeenSourcePrefix ? JSON.stringify(entry.lastSeenSourcePrefix) : pc.dim('—')}`);
        // Section 4: effective (merged) — what this source actually deploys as.
        const eff = resolveEffectivePrefix(entry, sourceConfig);
        console.log(pc.bold(`▸ effective ${pc.dim(`(origin: ${eff.origin})`)}`));
        console.log(`  ${pc.bold('prefix:')}        ${eff.prefix ?? pc.dim('—')}`);
        console.log(`  ${pc.bold('prefixScope:')}   ${eff.prefixScope?.join(',') ?? pc.dim('—')}`);
    });
    // Phase A skill-prefix-rules: set-prefix / unset-prefix subcommands.
    source
        .command('set-prefix')
        .description('Set the deploy prefix for a registered skill source (skills+agents deploy as <prefix>:<canonical-name>)')
        .argument('<name>', 'Display name of the skill source (from "olam skills source list")')
        .argument('<prefix>', 'Prefix value (must match /^[a-z0-9][a-z0-9_-]{0,38}$/)')
        .action((name, prefix) => {
        const entry = getSkillSourceByName(name);
        if (entry === null) {
            printError(`skill source "${name}" is not registered. Run "olam skills source list" to see registered sources.`);
            process.exitCode = 1;
            return;
        }
        try {
            updateSkillSource(entry.id, { prefix });
            printSuccess(`set prefix "${prefix}" on source "${name}" (${entry.id})`);
        }
        catch (err) {
            printError(asMessage(err));
            process.exitCode = 1;
        }
    });
    source
        .command('unset-prefix')
        .description('Remove the deploy prefix from a registered skill source (reverts to canonical deploy names)')
        .argument('<name>', 'Display name of the skill source (from "olam skills source list")')
        .action((name) => {
        const entry = getSkillSourceByName(name);
        if (entry === null) {
            printError(`skill source "${name}" is not registered. Run "olam skills source list" to see registered sources.`);
            process.exitCode = 1;
            return;
        }
        try {
            updateSkillSource(entry.id, { prefix: null });
            printSuccess(`unset prefix on source "${name}" (${entry.id})`);
        }
        catch (err) {
            printError(asMessage(err));
            process.exitCode = 1;
        }
    });
    // Phase A+: set-prefix-scope / unset-prefix-scope subcommands.
    source
        .command('set-prefix-scope')
        .description('Set which artifact kinds are renamed by the prefix (comma-separated: skill, agent, or skill,agent)')
        .argument('<name>', 'Display name of the skill source (from "olam skills source list")')
        .argument('<scope>', 'Comma-separated artifact kinds: "skill", "agent", or "skill,agent". Empty string = no renaming.')
        .action((name, scopeRaw) => {
        const entry = getSkillSourceByName(name);
        if (entry === null) {
            printError(`skill source "${name}" is not registered. Run "olam skills source list" to see registered sources.`);
            process.exitCode = 1;
            return;
        }
        const parsed = parsePrefixScope(scopeRaw);
        if (parsed === null) {
            printError(`scope must be a comma-separated list of "skill" and/or "agent" — got "${scopeRaw}"`);
            process.exitCode = 1;
            return;
        }
        try {
            updateSkillSource(entry.id, { prefixScope: parsed });
            const display = parsed.length === 0 ? '(none — prefix disabled)' : parsed.join(',');
            printSuccess(`set prefix-scope "${display}" on source "${name}" (${entry.id})`);
        }
        catch (err) {
            printError(asMessage(err));
            process.exitCode = 1;
        }
    });
    source
        .command('unset-prefix-scope')
        .description('Remove the prefix-scope override from a registered skill source (reverts to default: both skill and agent are renamed)')
        .argument('<name>', 'Display name of the skill source (from "olam skills source list")')
        .action((name) => {
        const entry = getSkillSourceByName(name);
        if (entry === null) {
            printError(`skill source "${name}" is not registered. Run "olam skills source list" to see registered sources.`);
            process.exitCode = 1;
            return;
        }
        try {
            updateSkillSource(entry.id, { prefixScope: null });
            printSuccess(`unset prefix-scope on source "${name}" (${entry.id}) — reverted to default (skill,agent)`);
        }
        catch (err) {
            printError(asMessage(err));
            process.exitCode = 1;
        }
    });
}
//# sourceMappingURL=skills-source.js.map