/**
 * Update Command
 *
 * Refreshes OpenSpec skills and commands for configured tools.
 * Supports profile-aware updates, delivery changes, migration, and smart update detection.
 */
import path from 'path';
import chalk from 'chalk';
import ora from 'ora';
import * as fs from 'fs';
import { createRequire } from 'module';
import { FileSystemUtils } from '../utils/file-system.js';
import { getSkillReferenceTransformer, getTransformerForTool, transformToSkillReferences } from '../utils/command-references.js';
import { AI_TOOLS, OPENSPEC_DIR_NAME } from './config.js';
import { generateCommands, CommandAdapterRegistry, } from './command-generation/index.js';
import { getToolVersionStatus, getSkillTemplates, getCommandContents, generateSkillContent, getToolsWithSkillsDir, hasGlobalSkillTarget, resolveToolSkillsDir, toolSupportsSkills, } from './shared/index.js';
import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatDeferredGlobalPromptSummary, formatCleanupSummary, formatDetectionSummary, getLegacyGlobalPromptMatches, getLegacyWorkflowIdsForTool, getToolsFromLegacyArtifacts, omitGlobalLegacyPromptFiles, omitToolLegacyArtifacts, pickGlobalLegacyPromptFiles, } from './legacy-cleanup.js';
import { isInteractive } from '../utils/interactive.js';
import { getGlobalConfig } from './global-config.js';
import { getProfileWorkflows, ALL_WORKFLOWS, CORE_WORKFLOWS } from './profiles.js';
import { getOnboardingCommands } from './onboarding-commands.js';
import { getAvailableTools } from './available-tools.js';
import { WORKFLOW_TO_SKILL_DIR, getConfiguredToolsForProfileSync, getToolsNeedingProfileSync, } from './profile-sync-drift.js';
import { scanInstalledWorkflows as scanInstalledWorkflowsShared, migrateIfNeeded as migrateIfNeededShared, findLegacyToolMigrations, migrateLegacyToolDirs, describeLegacyMigration, legacyMigrationNotice, keptInPlaceNotice, hasMovableContent, } from './migration.js';
import { resolveCommandSurfaceCapability, resolveCommandInvocation, shouldGenerateCommandsForTool, shouldGenerateSkillsForTool, shouldReconcileCommandFilesForTool, shouldRemoveSkillsForTool, } from './command-surface.js';
import { resolveSharedSkillWriters, writeSharedSkillTarget, } from './shared-skill-target.js';
import { includesGitHubCopilot, writeCopilotCloudFiles, removeCopilotCloudFiles, isCopilotCloudEnabled, readCopilotCloudOptIn, findUnmanagedCloudFiles } from './github-copilot/cloud-agent.js';
const require = createRequire(import.meta.url);
const { version: OPENSPEC_VERSION } = require('../../package.json');
/**
 * Scans installed workflow artifacts (skills and managed commands) across all configured tools.
 * Returns the union of detected workflow IDs that match ALL_WORKFLOWS.
 *
 * Wrapper around the shared migration module's scanInstalledWorkflows that accepts tool IDs.
 */
export function scanInstalledWorkflows(projectPath, toolIds) {
    const tools = toolIds
        .map((id) => AI_TOOLS.find((t) => t.value === id))
        .filter((t) => t != null);
    return scanInstalledWorkflowsShared(projectPath, tools);
}
export class UpdateCommand {
    force;
    constructor(options = {}) {
        this.force = options.force ?? false;
    }
    /**
     * Refreshes OpenSpec skills and commands for all configured tools,
     * regenerating artifacts according to the effective profile and delivery mode.
     *
     * @param projectPath - Path to the project root containing the openspec directory
     */
    async execute(projectPath) {
        const resolvedProjectPath = path.resolve(projectPath);
        const openspecPath = path.join(resolvedProjectPath, OPENSPEC_DIR_NAME);
        // 1. Check openspec directory exists
        if (!await FileSystemUtils.directoryExists(openspecPath)) {
            throw new Error(`No OpenSpec directory found. Run 'openspec init' first.`);
        }
        // 2. Migrate OpenSpec-managed skills left in renamed tool directories
        // (e.g. .kimi -> .kimi-code) so they stay detected and get refreshed,
        // then perform the one-time profile migration if needed before any
        // legacy upgrade generation.
        for (const migration of migrateLegacyToolDirs(resolvedProjectPath)) {
            if (hasMovableContent(migration)) {
                console.log(chalk.dim(`Migrated ${describeLegacyMigration(migration)}: ${migration.from} → ${migration.to}`));
            }
            this.reportKeptInPlace(migration);
        }
        const declinedMigrations = await this.offerConsentedLegacyMigrations(resolvedProjectPath);
        // Use detected tool directories to preserve existing opsx skills/commands.
        const detectedTools = getAvailableTools(resolvedProjectPath);
        migrateIfNeededShared(resolvedProjectPath, detectedTools);
        // 3. Read global config for profile/delivery
        const globalConfig = getGlobalConfig();
        const profile = globalConfig.profile ?? 'core';
        const delivery = globalConfig.delivery ?? 'both';
        const profileWorkflows = getProfileWorkflows(profile, globalConfig.workflows);
        const desiredWorkflows = profileWorkflows.filter((workflow) => ALL_WORKFLOWS.includes(workflow));
        // 4. Detect and handle legacy artifacts + upgrade legacy tools using effective config
        const legacyUpgrade = await this.handleLegacyCleanup(resolvedProjectPath, desiredWorkflows, delivery);
        const { newlyConfiguredTools, workflowOverrides: legacyWorkflowOverrides, deferredGlobalCleanup, } = legacyUpgrade;
        // 5. Find configured tools
        const configuredTools = getConfiguredToolsForProfileSync(resolvedProjectPath);
        const configuredAndNewTools = [...new Set([...configuredTools, ...newlyConfiguredTools])];
        if (configuredTools.length === 0 && newlyConfiguredTools.length === 0) {
            if (deferredGlobalCleanup) {
                await this.performDeferredGlobalPromptCleanup(resolvedProjectPath, deferredGlobalCleanup);
            }
            if (declinedMigrations.length > 0) {
                // Not an unconfigured project — a configured one the user chose to
                // leave in its former directory. Saying "run init" would be wrong.
                for (const migration of declinedMigrations) {
                    console.log(chalk.yellow(`Nothing to update: this project's OpenSpec files are still in ${migration.from}/, ` +
                        `which OpenSpec no longer writes.`));
                    console.log(chalk.dim(`Re-run "openspec update" and accept the move to ${migration.to}/ to resume updates.`));
                }
                return;
            }
            await this.syncCopilotCloudFiles(resolvedProjectPath, configuredAndNewTools);
            console.log(chalk.yellow('No configured tools found.'));
            console.log(chalk.dim('Run "openspec init" to set up tools.'));
            return;
        }
        // 6. Check version status for all configured tools, against the same workflow set
        //    the generation loop below writes — otherwise a legacy-upgraded tool would be
        //    fingerprinted against commands it was never given.
        const toolStatuses = configuredTools.map((toolId) => getToolVersionStatus(resolvedProjectPath, toolId, OPENSPEC_VERSION, {
            workflows: legacyWorkflowOverrides[toolId] ?? desiredWorkflows,
        }));
        const statusByTool = new Map(toolStatuses.map((status) => [status.toolId, status]));
        // 7. Smart update detection
        const toolsNeedingVersionUpdate = toolStatuses
            .filter((s) => {
            if (!s.needsUpdate || delivery !== 'commands') {
                return s.needsUpdate;
            }
            const tool = AI_TOOLS.find((candidate) => candidate.value === s.toolId);
            return !tool || !hasGlobalSkillTarget(tool);
        })
            .map((s) => s.toolId);
        const toolsNeedingConfigSync = getToolsNeedingProfileSync(resolvedProjectPath, desiredWorkflows, delivery, configuredTools);
        const toolsToUpdateSet = new Set([
            ...toolsNeedingVersionUpdate,
            ...toolsNeedingConfigSync,
        ]);
        const toolsUpToDate = toolStatuses.filter((s) => !toolsToUpdateSet.has(s.toolId));
        if (!this.force && toolsToUpdateSet.size === 0 && newlyConfiguredTools.length === 0) {
            if (deferredGlobalCleanup) {
                await this.performDeferredGlobalPromptCleanup(resolvedProjectPath, deferredGlobalCleanup);
            }
            // All tools are up to date
            this.displayUpToDateMessage(toolStatuses);
            await this.syncCopilotCloudFiles(resolvedProjectPath, configuredAndNewTools);
            // Still check for new tool directories and extra workflows
            this.detectNewTools(resolvedProjectPath, configuredTools);
            this.displayExtraWorkflowsNote(resolvedProjectPath, configuredTools, desiredWorkflows);
            this.displayMissingCoreWorkflowsNote(profile, desiredWorkflows);
            this.displaySetupNotes(configuredTools);
            return;
        }
        // 8. Display update plan
        if (this.force) {
            console.log(`Force updating ${configuredTools.length} tool(s): ${configuredTools.join(', ')}`);
        }
        else if (toolsToUpdateSet.size === 0) {
            console.log('No additional refresh needed after legacy migration.');
        }
        else {
            this.displayUpdatePlan([...toolsToUpdateSet], statusByTool, toolsUpToDate);
        }
        console.log();
        // 9. Determine what to generate based on delivery
        const deliveryIncludesCommands = delivery !== 'skills';
        // 10. Update tools (all if force, otherwise only those needing update)
        const toolsToUpdate = this.force ? configuredTools : [...toolsToUpdateSet];
        const sharedSkillWriters = resolveSharedSkillWriters(resolvedProjectPath, configuredAndNewTools
            .map((toolId) => AI_TOOLS.find((tool) => tool.value === toolId))
            .filter((tool) => tool !== undefined));
        const updatedTools = [];
        const updatedToolIds = [];
        const failedTools = [];
        const skillsInvocableCommandSkips = [];
        const zeroArtifactTools = [];
        let removedCommandCount = 0;
        let removedSkillCount = 0;
        let removedDeselectedCommandCount = 0;
        let removedDeselectedSkillCount = 0;
        for (const toolId of toolsToUpdate) {
            const tool = AI_TOOLS.find((t) => t.value === toolId);
            if (!tool || !toolSupportsSkills(tool))
                continue;
            const spinner = ora(`Updating ${tool.name}...`).start();
            try {
                const skillsDir = resolveToolSkillsDir(resolvedProjectPath, tool);
                const skillsRoot = hasGlobalSkillTarget(tool) ? skillsDir : resolvedProjectPath;
                const shouldGenerateSkills = shouldGenerateSkillsForTool(tool.value, delivery);
                const shouldGenerateCommands = shouldGenerateCommandsForTool(tool.value, delivery);
                const writesSkills = !tool.skillsDir || sharedSkillWriters.has(tool.value);
                const toolWorkflows = legacyWorkflowOverrides[tool.value] ?? desiredWorkflows;
                const skillTemplates = getSkillTemplates(toolWorkflows);
                const commandContents = getCommandContents(toolWorkflows);
                // Generate skill files if delivery includes skills
                if (shouldGenerateSkills && writesSkills) {
                    for (const { template, dirName } of skillTemplates) {
                        const skillDir = path.join(skillsDir, dirName);
                        const skillFile = path.join(skillDir, 'SKILL.md');
                        const transformer = getTransformerForTool(tool.value, delivery, resolveCommandSurfaceCapability(tool.value), resolveCommandInvocation(tool.value));
                        const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
                        FileSystemUtils.assertPathWithin(skillsRoot, skillFile);
                        await FileSystemUtils.writeFile(skillFile, skillContent);
                    }
                    writeSharedSkillTarget(resolvedProjectPath, tool.value);
                    removedDeselectedSkillCount += await this.removeUnselectedSkillDirs(skillsRoot, skillsDir, toolWorkflows);
                }
                // Delete skill directories if delivery is commands-only
                if (shouldRemoveSkillsForTool(tool.value, delivery) &&
                    writesSkills &&
                    !hasGlobalSkillTarget(tool)) {
                    removedSkillCount += await this.removeSkillDirs(skillsRoot, skillsDir);
                    // Persist the selected owner even when commands-only delivery leaves
                    // this target with no generated skills.
                    writeSharedSkillTarget(resolvedProjectPath, tool.value);
                    // A tool with no command adapter now has zero OpenSpec artifacts;
                    // say so like init does, rather than deleting its skills silently
                    // and letting tool detection re-suggest an init that would also
                    // generate nothing under this delivery setting.
                    if (!shouldGenerateCommandsForTool(tool.value, delivery)) {
                        zeroArtifactTools.push(tool.name);
                    }
                }
                // Generate commands if delivery includes commands
                if (shouldGenerateCommands) {
                    const adapter = CommandAdapterRegistry.get(tool.value);
                    if (adapter) {
                        const generatedCommands = generateCommands(commandContents, adapter);
                        for (const cmd of generatedCommands) {
                            const commandFile = FileSystemUtils.resolveProjectArtifactPath(resolvedProjectPath, cmd.path);
                            await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
                        }
                        removedDeselectedCommandCount += await this.removeUnselectedCommandFiles(resolvedProjectPath, toolId, toolWorkflows);
                    }
                }
                else if (deliveryIncludesCommands && resolveCommandSurfaceCapability(tool.value) === 'skills-invocable') {
                    skillsInvocableCommandSkips.push(tool.value);
                }
                // Delete command files if delivery is skills-only
                if (shouldReconcileCommandFilesForTool(tool.value, delivery)) {
                    removedCommandCount += await this.removeCommandFiles(resolvedProjectPath, toolId);
                }
                spinner.succeed(`Updated ${tool.name}`);
                updatedTools.push(tool.name);
                updatedToolIds.push(tool.value);
                for (const migration of migrateLegacyToolDirs(resolvedProjectPath, [tool.value], 'after-generation')) {
                    if (hasMovableContent(migration)) {
                        console.log(chalk.dim(`Migrated ${describeLegacyMigration(migration)}: ${migration.from} → ${migration.to}`));
                    }
                    this.reportKeptInPlace(migration);
                }
            }
            catch (error) {
                spinner.fail(`Failed to update ${tool.name}`);
                failedTools.push({
                    name: tool.name,
                    error: error instanceof Error ? error.message : String(error)
                });
            }
        }
        if (deferredGlobalCleanup) {
            await this.performDeferredGlobalPromptCleanup(resolvedProjectPath, deferredGlobalCleanup);
        }
        // 11. Summary
        console.log();
        if (updatedTools.length > 0) {
            console.log(chalk.green(`✓ Updated: ${updatedTools.join(', ')} (v${OPENSPEC_VERSION})`));
        }
        if (failedTools.length > 0) {
            console.log(chalk.red(`✗ Failed: ${failedTools.map(f => `${f.name} (${f.error})`).join(', ')}`));
        }
        if (skillsInvocableCommandSkips.length > 0) {
            console.log(chalk.dim(`Commands skipped for: ${skillsInvocableCommandSkips.join(', ')} (uses skills)`));
        }
        if (removedCommandCount > 0) {
            console.log(chalk.dim(`Removed: ${removedCommandCount} command files (delivery: skills)`));
        }
        if (removedSkillCount > 0) {
            console.log(chalk.dim(`Removed: ${removedSkillCount} skill directories (delivery: commands)`));
        }
        if (zeroArtifactTools.length > 0) {
            const names = zeroArtifactTools.join(', ');
            console.log(chalk.yellow(`No skills or commands remain for ${names}: delivery is set to 'commands' but ` +
                `${zeroArtifactTools.length === 1 ? 'it supports' : 'they support'} only skills. ` +
                `Run 'openspec config set delivery both' to generate skills.`));
        }
        if (removedDeselectedCommandCount > 0) {
            console.log(chalk.dim(`Removed: ${removedDeselectedCommandCount} command files (deselected workflows)`));
        }
        if (removedDeselectedSkillCount > 0) {
            console.log(chalk.dim(`Removed: ${removedDeselectedSkillCount} skill directories (deselected workflows)`));
        }
        // 12. Show onboarding message for newly configured tools from legacy upgrade.
        // Command tools get the command name their files answer to, skill-only
        // tools their documented skill invocation, and disagreements fall back to
        // naming the skill.
        if (newlyConfiguredTools.length > 0) {
            const referenceFor = (command) => {
                const neutralForm = `the ${transformToSkillReferences(command).slice(1)} skill`;
                const forms = new Set(newlyConfiguredTools.map((toolId) => {
                    if (shouldGenerateCommandsForTool(toolId, delivery)) {
                        // Name the command the tool's files actually answer to:
                        // /opsx-<id> where the filename is the command name.
                        const transformer = getTransformerForTool(toolId, delivery, resolveCommandSurfaceCapability(toolId), resolveCommandInvocation(toolId));
                        return transformer ? transformer(command) : command;
                    }
                    return getSkillReferenceTransformer(toolId)(command);
                }));
                return forms.size === 1 ? [...forms][0] : neutralForm;
            };
            // Only hint at workflows these tools actually received. A legacy upgrade
            // can install a narrower set than the profile (inferred Codex prompts).
            const installedWorkflows = [
                ...new Set(newlyConfiguredTools.flatMap((toolId) => legacyWorkflowOverrides[toolId] ?? desiredWorkflows)),
            ];
            const entries = getOnboardingCommands(installedWorkflows).map(({ command, description }) => [referenceFor(command), description]);
            console.log();
            if (entries.length > 0) {
                const width = Math.max(...entries.map(([reference]) => reference.length));
                console.log(chalk.bold('Getting started:'));
                for (const [reference, description] of entries) {
                    console.log(`  ${reference.padEnd(width)}  ${description}`);
                }
                console.log();
            }
            console.log(`Learn more: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec')}`);
        }
        await this.syncCopilotCloudFiles(resolvedProjectPath, configuredAndNewTools);
        // 13. Detect new tool directories not currently configured
        this.detectNewTools(resolvedProjectPath, configuredAndNewTools);
        // 14. Display note about extra workflows not in profile
        this.displayExtraWorkflowsNote(resolvedProjectPath, configuredAndNewTools, desiredWorkflows);
        this.displayMissingCoreWorkflowsNote(profile, desiredWorkflows);
        this.displaySetupNotes(configuredAndNewTools);
        // 15. List affected tools
        if (updatedTools.length > 0) {
            const toolDisplayNames = updatedTools;
            console.log(chalk.dim(`Tools: ${toolDisplayNames.join(', ')}`));
        }
        console.log();
        const affectedToolIds = [...new Set([...newlyConfiguredTools, ...updatedToolIds])];
        const shouldRestartIde = affectedToolIds.some((toolId) => {
            const tool = AI_TOOLS.find((candidate) => candidate.value === toolId);
            return Boolean(tool?.requiresIdeRestart &&
                (shouldGenerateCommandsForTool(toolId, delivery) ||
                    shouldGenerateSkillsForTool(toolId, delivery)));
        });
        if (shouldRestartIde) {
            console.log(chalk.dim('Restart your IDE for changes to take effect.'));
        }
        if (failedTools.length > 0) {
            throw new Error(`OpenSpec update failed for: ${failedTools.map((tool) => tool.name).join(', ')}`);
        }
    }
    async syncCopilotCloudFiles(projectPath, configuredTools) {
        try {
            if (includesGitHubCopilot(configuredTools)) {
                // Cloud files are opt-in (see cloud-agent.ts). `update` never prompts,
                // so it only refreshes files the user has already opted into (via
                // `openspec init` or a `githubCopilot.cloudAgent: true` config), or that
                // a pre-opt-in project already has. Opting in is a deliberate init/config
                // step, never a silent side effect of running update.
                if (await isCopilotCloudEnabled(projectPath)) {
                    await writeCopilotCloudFiles(projectPath);
                    const collisions = await findUnmanagedCloudFiles(projectPath);
                    if (collisions.length > 0) {
                        console.log(chalk.dim(`Left your existing ${collisions.join(' and ')} untouched — add the OpenSpec ` +
                            `install step by hand so the Copilot cloud agent can run openspec.`));
                    }
                    return;
                }
                // Explicit opt-out (githubCopilot.cloudAgent: false) means "not here":
                // remove any managed files a prior opt-in left behind (customized files
                // are preserved). If the user simply never decided, stay quiet unless
                // we're at an interactive terminal, where a one-line hint aids discovery.
                if (readCopilotCloudOptIn(projectPath) === false) {
                    const removed = await removeCopilotCloudFiles(projectPath);
                    if (removed > 0) {
                        console.log(chalk.dim(`Removed: ${removed} Copilot cloud agent file(s) (opted out of cloud files)`));
                    }
                }
                else if (isInteractive()) {
                    console.log(chalk.dim("GitHub Copilot cloud coding-agent files are available (opt-in). Enable with 'openspec init --copilot-cloud'."));
                }
                return;
            }
            const removed = await removeCopilotCloudFiles(projectPath);
            if (removed > 0) {
                console.log(chalk.dim(`Removed: ${removed} Copilot cloud agent file(s) (github-copilot not configured)`));
            }
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            console.warn(`Warning: failed to sync Copilot cloud agent files: ${message}`);
        }
    }
    /**
     * Display message when all tools are up to date.
     */
    displayUpToDateMessage(toolStatuses) {
        const toolNames = toolStatuses.map((s) => s.toolId);
        console.log(chalk.green(`✓ All ${toolStatuses.length} tool(s) up to date (v${OPENSPEC_VERSION})`));
        console.log(chalk.dim(`  Tools: ${toolNames.join(', ')}`));
        console.log();
        console.log(chalk.dim('Use --force to refresh files anyway.'));
    }
    /**
     * Display the update plan showing which tools need updating.
     */
    displayUpdatePlan(toolsToUpdate, statusByTool, upToDate) {
        const updates = toolsToUpdate.map((toolId) => {
            const status = statusByTool.get(toolId);
            if (status?.needsUpdate) {
                const fromVersion = status.generatedByVersion ?? 'unknown';
                return `${status.toolId} (${fromVersion} → ${OPENSPEC_VERSION})`;
            }
            return `${toolId} (config sync)`;
        });
        console.log(`Updating ${toolsToUpdate.length} tool(s): ${updates.join(', ')}`);
        if (upToDate.length > 0) {
            const upToDateNames = upToDate.map((s) => s.toolId);
            console.log(chalk.dim(`Already up to date: ${upToDateNames.join(', ')}`));
        }
    }
    /**
     * Shows manual setup notes for configured tools that need extra
     * configuration before they pick up generated files.
     */
    displaySetupNotes(toolIds) {
        for (const toolId of toolIds) {
            const tool = AI_TOOLS.find((t) => t.value === toolId);
            if (tool?.setupNote) {
                console.log(chalk.yellow(`Setup required for ${tool.name}: ${tool.setupNote}`));
            }
        }
    }
    /**
     * Detects new tool directories that aren't currently configured and displays a hint.
     */
    detectNewTools(projectPath, configuredTools) {
        const availableTools = getAvailableTools(projectPath);
        const configuredSet = new Set(configuredTools);
        const newTools = availableTools.filter((t) => !configuredSet.has(t.value));
        if (newTools.length > 0) {
            const newToolNames = newTools.map((tool) => tool.name);
            const isSingleTool = newToolNames.length === 1;
            const toolNoun = isSingleTool ? 'tool' : 'tools';
            const pronoun = isSingleTool ? 'it' : 'them';
            console.log();
            console.log(chalk.yellow(`Detected new ${toolNoun}: ${newToolNames.join(', ')}. Run 'openspec init' to add ${pronoun}.`));
        }
    }
    /**
     * Displays a note about extra workflows installed that aren't in the current profile.
     */
    displayExtraWorkflowsNote(projectPath, configuredTools, profileWorkflows) {
        const installedWorkflows = scanInstalledWorkflows(projectPath, configuredTools);
        const profileSet = new Set(profileWorkflows);
        const extraWorkflows = installedWorkflows.filter((w) => !profileSet.has(w));
        if (extraWorkflows.length > 0) {
            console.log(chalk.dim(`Note: ${extraWorkflows.length} extra workflows not in profile (use \`openspec config profile\` to manage)`));
        }
    }
    /**
     * Point out core workflows a custom profile is missing, so releases that
     * grow CORE_WORKFLOWS stay discoverable. Keep custom profiles user-owned;
     * do not mutate them.
     */
    displayMissingCoreWorkflowsNote(profile, workflows) {
        if (profile !== 'custom' || !workflows) {
            return;
        }
        const workflowSet = new Set(workflows);
        const missing = CORE_WORKFLOWS.filter((workflow) => !workflowSet.has(workflow));
        if (missing.length === 0) {
            return;
        }
        const label = missing.length === 1 ? 'workflow' : 'workflows';
        const pronoun = missing.length === 1 ? 'it' : 'them';
        console.log(chalk.dim(`Note: Your custom profile is missing ${missing.length} core ${label}: ${missing.join(', ')}`));
        console.log(chalk.dim(`Run \`openspec config profile\` to add ${pronoun}, or \`openspec config profile core\` to use the core set.`));
    }
    /**
     * Removes skill directories for workflows when delivery changed to commands-only.
     * Returns the number of directories removed.
     */
    async removeSkillDirs(skillsRoot, skillsDir) {
        let removed = 0;
        for (const workflow of ALL_WORKFLOWS) {
            const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
            if (!dirName)
                continue;
            const skillDir = path.join(skillsDir, dirName);
            if (!fs.existsSync(skillDir))
                continue;
            FileSystemUtils.assertPathWithin(skillsRoot, skillDir);
            try {
                await fs.promises.rm(skillDir, { recursive: true, force: true });
                removed++;
            }
            catch {
                // Ignore errors
            }
        }
        return removed;
    }
    /**
     * Removes skill directories for workflows that are no longer selected in the active profile.
     * Returns the number of directories removed.
     */
    async removeUnselectedSkillDirs(skillsRoot, skillsDir, desiredWorkflows) {
        const desiredSet = new Set(desiredWorkflows);
        let removed = 0;
        for (const workflow of ALL_WORKFLOWS) {
            if (desiredSet.has(workflow))
                continue;
            const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
            if (!dirName)
                continue;
            const skillDir = path.join(skillsDir, dirName);
            if (!fs.existsSync(skillDir))
                continue;
            FileSystemUtils.assertPathWithin(skillsRoot, skillDir);
            try {
                await fs.promises.rm(skillDir, { recursive: true, force: true });
                removed++;
            }
            catch {
                // Ignore errors
            }
        }
        return removed;
    }
    /**
     * Removes command files for workflows when delivery changed to skills-only.
     * Returns the number of files removed.
     */
    async removeCommandFiles(projectPath, toolId) {
        let removed = 0;
        const adapter = CommandAdapterRegistry.get(toolId);
        if (!adapter)
            return 0;
        for (const workflow of ALL_WORKFLOWS) {
            const cmdPath = adapter.getFilePath(workflow);
            const fullPath = FileSystemUtils.resolveProjectArtifactPath(projectPath, cmdPath);
            try {
                if (fs.existsSync(fullPath)) {
                    await fs.promises.unlink(fullPath);
                    removed++;
                }
            }
            catch {
                // Ignore errors
            }
        }
        return removed;
    }
    /**
     * Removes command files for workflows that are no longer selected in the active profile.
     * Returns the number of files removed.
     */
    async removeUnselectedCommandFiles(projectPath, toolId, desiredWorkflows) {
        let removed = 0;
        const adapter = CommandAdapterRegistry.get(toolId);
        if (!adapter)
            return 0;
        const desiredSet = new Set(desiredWorkflows);
        for (const workflow of ALL_WORKFLOWS) {
            if (desiredSet.has(workflow))
                continue;
            const cmdPath = adapter.getFilePath(workflow);
            const fullPath = FileSystemUtils.resolveProjectArtifactPath(projectPath, cmdPath);
            try {
                if (fs.existsSync(fullPath)) {
                    await fs.promises.unlink(fullPath);
                    removed++;
                }
            }
            catch {
                // Ignore errors
            }
        }
        return removed;
    }
    /**
     * Offers to move OpenSpec content out of a renamed tool's former directory
     * when the old location might still be the live one — today, Windsurf's
     * `.windsurf/` after the Devin Desktop rebrand.
     *
     * Interactive runs are asked, because nothing on disk distinguishes a user
     * who took the rebrand from one still on a pre-rebrand Windsurf build that
     * reads only `.windsurf/`. `--force` and non-interactive runs migrate, which
     * is what an unattended upgrade wants.
     */
    /** Surfaces files the move left behind rather than overwriting. */
    reportKeptInPlace(migration) {
        const notice = keptInPlaceNotice(migration);
        if (notice)
            console.log(chalk.dim(notice));
    }
    async offerConsentedLegacyMigrations(projectPath) {
        const pending = findLegacyToolMigrations(projectPath).filter((m) => m.needsConsent);
        const declined = [];
        if (pending.length === 0)
            return declined;
        for (const migration of pending) {
            // Nothing movable: every legacy file differs from its counterpart, so
            // there is no move to offer. Still say so — silence would leave two
            // divergent copies the user never hears about.
            if (!hasMovableContent(migration)) {
                this.reportKeptInPlace(migration);
                console.log();
                continue;
            }
            console.log(chalk.yellow(legacyMigrationNotice(migration)));
            if (!this.force && isInteractive()) {
                const { confirm } = await import('@inquirer/prompts');
                let shouldMigrate;
                try {
                    shouldMigrate = await confirm({
                        message: `Move ${describeLegacyMigration(migration)} from ${migration.from}/ to ${migration.to}/?`,
                        default: true,
                    });
                }
                catch {
                    // Closed stdin is not consent, and it must not abort the update.
                    shouldMigrate = false;
                }
                if (!shouldMigrate) {
                    // Say what declining costs. OpenSpec writes the current root now, so
                    // the files keep working where they are, but OpenSpec stops managing
                    // them — it no longer looks in the former directory.
                    console.log(chalk.dim(`Left in place. OpenSpec writes ${migration.to}/ now and will not manage ` +
                        `${migration.from}/, so those files stay as they are until you move them. ` +
                        `You will be asked again next run.`));
                    console.log();
                    declined.push(migration);
                    continue;
                }
            }
            for (const applied of migrateLegacyToolDirs(projectPath, [migration.toolId])) {
                if (hasMovableContent(applied)) {
                    console.log(chalk.dim(`Migrated ${describeLegacyMigration(applied)}: ${applied.from} → ${applied.to}`));
                }
                this.reportKeptInPlace(applied);
            }
            console.log();
        }
        return declined;
    }
    /**
     * Detect and handle legacy OpenSpec artifacts.
     * Unlike init, update warns but continues if legacy files found in non-interactive mode.
     * Returns array of tool IDs that were newly configured during legacy upgrade.
     */
    async handleLegacyCleanup(projectPath, desiredWorkflows, delivery) {
        // Detect legacy artifacts
        const detection = await detectLegacyArtifacts(projectPath);
        if (!detection.hasLegacyArtifacts) {
            return { newlyConfiguredTools: [], workflowOverrides: {} }; // No legacy artifacts found
        }
        // Show what was detected
        const immediateSummary = formatDetectionSummary(omitGlobalLegacyPromptFiles(detection));
        const deferredSummary = formatDeferredGlobalPromptSummary(detection);
        if (immediateSummary || deferredSummary) {
            console.log();
            if (immediateSummary) {
                console.log(immediateSummary);
                console.log();
            }
            if (deferredSummary) {
                console.log(deferredSummary);
                console.log();
            }
        }
        const canPrompt = isInteractive();
        if (this.force) {
            const legacyUpgrade = await this.upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery);
            await this.performImmediateLegacyCleanup(projectPath, detection, legacyUpgrade.skippedSharedSkillTools);
            return {
                ...legacyUpgrade,
                deferredGlobalCleanup: pickGlobalLegacyPromptFiles(detection, detection.globalSlashCommandFiles),
            };
        }
        if (!canPrompt) {
            // Non-interactive mode without --force: warn and continue
            // (Unlike init, update doesn't abort - user may just want to update skills)
            console.log(chalk.yellow('⚠ Run with --force to auto-cleanup legacy files, or run interactively.'));
            console.log();
            return { newlyConfiguredTools: [], workflowOverrides: {} };
        }
        // Interactive mode: prompt for confirmation
        const { confirm } = await import('@inquirer/prompts');
        const shouldCleanup = await confirm({
            message: 'Upgrade and clean up legacy files?',
            default: true,
        });
        if (shouldCleanup) {
            const legacyUpgrade = await this.upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery);
            await this.performImmediateLegacyCleanup(projectPath, detection, legacyUpgrade.skippedSharedSkillTools);
            return {
                ...legacyUpgrade,
                deferredGlobalCleanup: pickGlobalLegacyPromptFiles(detection, detection.globalSlashCommandFiles),
            };
        }
        else {
            console.log(chalk.dim('Skipping legacy cleanup. Continuing with skill update...'));
            console.log();
            return { newlyConfiguredTools: [], workflowOverrides: {} };
        }
    }
    /**
     * Cleans approved repo-local legacy artifacts before configured tools refresh.
     */
    async performImmediateLegacyCleanup(projectPath, detection, skippedSharedSkillTools = []) {
        // Tools whose upgrade was skipped (shared root owned by another) had no
        // replacement written, so their repo-local legacy files must be preserved.
        const immediateDetection = omitToolLegacyArtifacts(omitGlobalLegacyPromptFiles(detection), skippedSharedSkillTools);
        if (immediateDetection.hasLegacyArtifacts) {
            await this.performLegacyCleanup(projectPath, immediateDetection);
        }
    }
    /**
     * Cleans approved global Codex prompts after configured tools refresh so newly
     * installed replacement skills can retire their prompts in the same run.
     */
    async performDeferredGlobalPromptCleanup(projectPath, detection) {
        const availableCodexWorkflows = new Set(scanInstalledWorkflows(projectPath, ['codex']));
        const removableMatches = getLegacyGlobalPromptMatches(detection)
            .filter((prompt) => prompt.workflowIds.every((workflowId) => availableCodexWorkflows.has(workflowId)));
        if (removableMatches.length > 0) {
            await this.performLegacyCleanup(projectPath, pickGlobalLegacyPromptFiles(detection, removableMatches.map((prompt) => prompt.path)));
        }
        const blockedMatches = getLegacyGlobalPromptMatches(detection)
            .filter((prompt) => !removableMatches.some((match) => match.path === prompt.path));
        if (blockedMatches.length > 0) {
            console.log(chalk.yellow('Preserved deferred global prompts without replacement skills:'));
            for (const prompt of blockedMatches) {
                console.log(chalk.dim(`  - ${prompt.toolId}: ${prompt.path}`));
            }
            console.log();
        }
    }
    /**
     * Perform cleanup of legacy artifacts.
     */
    async performLegacyCleanup(projectPath, detection) {
        const spinner = ora('Cleaning up legacy files...').start();
        const result = await cleanupLegacyArtifacts(projectPath, detection);
        spinner.succeed('Legacy files cleaned up');
        const summary = formatCleanupSummary(result);
        if (summary) {
            console.log();
            console.log(summary);
        }
        console.log();
    }
    /**
     * Upgrades unconfigured legacy tools into the skills-based setup and carries
     * workflow overrides for migrations that should mirror legacy Codex prompts.
     */
    async upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery) {
        // Get tools that had legacy artifacts
        const legacyTools = getToolsFromLegacyArtifacts(detection);
        if (legacyTools.length === 0) {
            return { newlyConfiguredTools: [], workflowOverrides: {} };
        }
        // Get currently configured tools
        const configuredTools = getConfiguredToolsForProfileSync(projectPath);
        const configuredSet = new Set(configuredTools);
        // Filter to tools that aren't already configured
        const unconfiguredLegacyTools = legacyTools.filter((t) => !configuredSet.has(t));
        if (unconfiguredLegacyTools.length === 0) {
            return { newlyConfiguredTools: [], workflowOverrides: {} };
        }
        // Get valid tools (those with skillsDir)
        const validToolIds = new Set(getToolsWithSkillsDir());
        const validUnconfiguredTools = unconfiguredLegacyTools.filter((t) => validToolIds.has(t));
        if (validUnconfiguredTools.length === 0) {
            return { newlyConfiguredTools: [], workflowOverrides: {} };
        }
        // Show what tools were detected from legacy artifacts
        console.log(chalk.bold('Tools detected from legacy artifacts:'));
        for (const toolId of validUnconfiguredTools) {
            const tool = AI_TOOLS.find((t) => t.value === toolId);
            console.log(`  • ${tool?.name || toolId}`);
        }
        console.log();
        let selectedTools;
        if (this.force || !canPrompt) {
            // Non-interactive with --force: auto-select detected tools
            selectedTools = validUnconfiguredTools;
            console.log(`Setting up skills for: ${selectedTools.join(', ')}`);
        }
        else {
            // Interactive mode: prompt for tool selection with detected tools pre-selected
            const { searchableMultiSelect } = await import('../prompts/searchable-multi-select.js');
            const sortedChoices = validUnconfiguredTools.map((toolId) => {
                const tool = AI_TOOLS.find((t) => t.value === toolId);
                return {
                    name: tool?.name || toolId,
                    value: toolId,
                    configured: false,
                    preSelected: true, // Pre-select all detected legacy tools
                };
            });
            selectedTools = await searchableMultiSelect({
                message: 'Select tools to set up with the new skill system:',
                pageSize: 15,
                choices: sortedChoices,
                validate: (_selected) => true, // Allow empty selection (user can skip)
            });
            if (selectedTools.length === 0) {
                console.log(chalk.dim('Skipping tool setup.'));
                console.log();
                return { newlyConfiguredTools: [], workflowOverrides: {} };
            }
        }
        const inferredCodexWorkflows = getProfileWorkflows('custom', getLegacyWorkflowIdsForTool(detection, 'codex')).filter((workflow) => ALL_WORKFLOWS.includes(workflow));
        // Create skills/commands for selected tools using effective profile+delivery.
        const newlyConfigured = [];
        const skippedSharedSkillTools = [];
        const workflowOverrides = {};
        const arbitrationTools = [...new Set([...configuredTools, ...selectedTools])]
            .map((toolId) => AI_TOOLS.find((tool) => tool.value === toolId))
            .filter((tool) => tool !== undefined);
        const sharedSkillWriters = resolveSharedSkillWriters(projectPath, arbitrationTools);
        for (const toolId of selectedTools) {
            const tool = AI_TOOLS.find((t) => t.value === toolId);
            if (!tool || !toolSupportsSkills(tool))
                continue;
            const spinner = ora(`Setting up ${tool.name}...`).start();
            try {
                const skillsDir = resolveToolSkillsDir(projectPath, tool);
                const skillsRoot = hasGlobalSkillTarget(tool) ? skillsDir : projectPath;
                const shouldGenerateSkills = shouldGenerateSkillsForTool(tool.value, delivery);
                const shouldGenerateCommands = shouldGenerateCommandsForTool(tool.value, delivery);
                const writesSkills = !tool.skillsDir || sharedSkillWriters.has(tool.value);
                const toolWorkflows = (tool.value === 'codex' && inferredCodexWorkflows.length > 0
                    ? inferredCodexWorkflows
                    : desiredWorkflows);
                if (tool.value === 'codex' && inferredCodexWorkflows.length > 0) {
                    workflowOverrides[tool.value] = inferredCodexWorkflows;
                }
                const skillTemplates = getSkillTemplates(toolWorkflows);
                const commandContents = getCommandContents(toolWorkflows);
                // Never overwrite a shared skills root owned by another tool. A tool
                // with its own command surface can still install those commands: this
                // is how a legacy Antigravity install gains `.agents/workflows` beside
                // Codex-owned `.agents/skills`. A skills-only tool has no safe artifact
                // to install, so preserve its legacy files and re-offer it later.
                const sharedOwner = shouldGenerateSkills && !writesSkills
                    ? arbitrationTools.find((candidate) => candidate.skillsDir === tool.skillsDir &&
                        sharedSkillWriters.has(candidate.value))?.value
                    : undefined;
                if (sharedOwner && !shouldGenerateCommands) {
                    const ownerName = AI_TOOLS.find((candidate) => candidate.value === sharedOwner)?.name ?? sharedOwner;
                    spinner.info(`Skipped ${tool.name}: ${tool.skillsDir}/skills is already managed by another tool (${ownerName}).`);
                    skippedSharedSkillTools.push(tool.value);
                    continue;
                }
                // Create skill files when delivery includes skills
                if (shouldGenerateSkills && writesSkills) {
                    for (const { template, dirName } of skillTemplates) {
                        const skillDir = path.join(skillsDir, dirName);
                        const skillFile = path.join(skillDir, 'SKILL.md');
                        const transformer = getTransformerForTool(tool.value, delivery, resolveCommandSurfaceCapability(tool.value), resolveCommandInvocation(tool.value));
                        const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
                        FileSystemUtils.assertPathWithin(skillsRoot, skillFile);
                        await FileSystemUtils.writeFile(skillFile, skillContent);
                    }
                    writeSharedSkillTarget(projectPath, tool.value);
                }
                // Create commands when delivery includes commands
                if (shouldGenerateCommands) {
                    const adapter = CommandAdapterRegistry.get(tool.value);
                    if (adapter) {
                        const generatedCommands = generateCommands(commandContents, adapter);
                        for (const cmd of generatedCommands) {
                            const commandFile = FileSystemUtils.resolveProjectArtifactPath(projectPath, cmd.path);
                            await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
                        }
                    }
                }
                spinner.succeed(`Setup complete for ${tool.name}`);
                newlyConfigured.push(toolId);
                for (const migration of migrateLegacyToolDirs(projectPath, [tool.value], 'after-generation')) {
                    if (hasMovableContent(migration)) {
                        console.log(chalk.dim(`Migrated ${describeLegacyMigration(migration)}: ${migration.from} → ${migration.to}`));
                    }
                    this.reportKeptInPlace(migration);
                }
            }
            catch (error) {
                spinner.fail(`Failed to set up ${tool.name}`);
                console.log(chalk.red(`  ${error instanceof Error ? error.message : String(error)}`));
            }
        }
        if (newlyConfigured.length > 0) {
            console.log();
        }
        return { newlyConfiguredTools: newlyConfigured, workflowOverrides, skippedSharedSkillTools };
    }
}
//# sourceMappingURL=update.js.map