#!/usr/bin/env node
/**
 * SpecWeave Update Command
 *
 * Unified update command for keeping SpecWeave up-to-date.
 * This is the recommended command for users to run when updating.
 *
 * What it does (by default):
 * 1. Self-updates SpecWeave CLI via npm (npm i -g specweave@latest)
 * 2. Migrates config.json (adds missing sections like 'auto')
 * 3. Updates instruction files (CLAUDE.md, AGENTS.md)
 * 4. Validates project health
 * 5. Refreshes ALL marketplace plugins (DEFAULT - keeps all skills up to date)
 *
 * Usage:
 *   specweave update                  # Full update: CLI + instructions + config + plugins
 *   specweave update --no-plugins     # Skip marketplace plugins refresh
 *   specweave update --all            # Install ALL plugins (default, kept for compat)
 *   specweave update --minimal        # Clean /plugin output (removes marketplace)
 *   specweave update --no-self        # Skip CLI update, only project files
 *   specweave update --check          # Dry run - show what would change
 *
 * @since 1.0.131
 * @updated 1.0.138 - Plugins refresh is now DEFAULT (use --no-plugins to skip)
 */
import chalk from 'chalk';
import * as fs from 'fs';
import * as path from 'path';
import ora from 'ora';
import { execSync } from 'child_process';
import { updateInstructionsCommand } from './update-instructions.js';
import { refreshPluginsCommand } from './refresh-plugins.js';
import { getPackageVersion } from '../helpers/init/instruction-file-merger.js';
import { pruneSkillMemories, listSkillMemoryFiles } from '../../core/reflection/skill-memories.js';
import { npmRegistryFlag } from '../../utils/npm-constants.js';
import { hasSpecweaveIncrements } from '../../utils/find-project-root.js';
/**
 * Main update command
 */
export async function updateCommand(options = {}) {
    const projectPath = process.cwd();
    const version = getPackageVersion();
    let isSpecWeaveProject = fs.existsSync(path.join(projectPath, '.specweave', 'config.json'));
    console.log(chalk.blue.bold('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
    console.log(chalk.blue.bold('  SpecWeave Update'));
    console.log(chalk.blue.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
    console.log(chalk.gray(`  Version: ${version}`));
    console.log(chalk.gray(`  Project: ${projectPath}`));
    if (options.check) {
        console.log(chalk.yellow('\n  Mode: DRY RUN (no changes will be made)\n'));
    }
    else {
        console.log('');
    }
    const result = {
        configMigrated: false,
        instructionsUpdated: false,
        pluginsRefreshed: false,
        selfUpdated: false,
        stateFilesCleaned: 0,
        errors: [],
        warnings: [],
    };
    const spinner = ora();
    // Step 0: Self-update SpecWeave CLI (default: ON, skip with --no-self)
    // Commander maps --no-self to options.self=false (not options.noSelf=true)
    // Also respect env guard to prevent infinite recursion from spawned child
    const skipSelf = options.self === false || options.noSelf === true || process.env.SPECWEAVE_UPDATE_NO_SELF === '1';
    if (!skipSelf) {
        const selfUpdateResult = await selfUpdateSpecWeave(version, options, spinner);
        result.selfUpdated = selfUpdateResult.updated;
        result.newVersion = selfUpdateResult.newVersion;
        if (selfUpdateResult.error) {
            result.errors.push(selfUpdateResult.error);
        }
        // If self-update happened, spawn NEW CLI to complete remaining steps
        if (result.selfUpdated) {
            console.log(chalk.green(`\n✓ SpecWeave updated to ${result.newVersion}`));
            console.log(chalk.blue('  Running post-update tasks with new version...\n'));
            // Build command with same flags, but add --no-self to skip re-updating
            const flags = ['--no-self'];
            if (options.noPlugins)
                flags.push('--no-plugins');
            if (options.all)
                flags.push('--all');
            if (options.minimal)
                flags.push('--minimal');
            if (options.verbose)
                flags.push('--verbose');
            if (options.force)
                flags.push('--force');
            if (options.check)
                flags.push('--check');
            try {
                // Spawn NEW binary to run instructions/config/plugins update
                // Guard against infinite recursion with env variable
                // 120s timeout prevents indefinite hang (e.g. plugin refresh outside a project dir)
                execSync(`specweave update ${flags.join(' ')}`, {
                    stdio: 'inherit',
                    cwd: projectPath,
                    timeout: 120000,
                    env: { ...process.env, SPECWEAVE_UPDATE_NO_SELF: '1' },
                });
            }
            catch (error) {
                // Non-zero exit from spawned process - already printed output
                if (error.status) {
                    process.exit(error.status);
                }
                // Timeout — the child process hung (e.g. plugin refresh stalled)
                if (error.signal === 'SIGTERM') {
                    console.log(chalk.yellow('\n⚠️  Post-update tasks timed out. Run `specweave update --no-self` from your project directory to retry.\n'));
                    return;
                }
                throw error;
            }
            return;
        }
    }
    // Check if this is a SpecWeave project
    if (!isSpecWeaveProject) {
        const specweaveDir = path.join(projectPath, '.specweave');
        const hasDir = fs.existsSync(specweaveDir);
        // Recovery: .specweave/ exists with real content but config.json is missing.
        // Restore a minimal config so the rest of the update (config migration,
        // instruction updates, plugin refresh) can proceed normally.
        if (hasDir && hasSpecweaveIncrements(projectPath)) {
            const configPath = path.join(specweaveDir, 'config.json');
            const projectName = path.basename(projectPath);
            if (!options.check) {
                fs.writeFileSync(configPath, JSON.stringify({
                    version: '2.0',
                    project: { name: projectName },
                }, null, 2) + '\n');
                isSpecWeaveProject = true;
                console.log(chalk.green(`  ✓ Restored missing config.json (project: ${projectName})`));
                console.log(chalk.gray('    .specweave/ directory had content but config.json was missing\n'));
            }
            else {
                console.log(chalk.yellow(`  ⚠️  config.json is missing but .specweave/ has content — would restore on update`));
            }
        }
        if (!isSpecWeaveProject) {
            if (hasDir) {
                console.log(chalk.yellow('⚠️  Not a SpecWeave project (.specweave directory exists but config.json is missing)'));
                console.log(chalk.gray('   Run: specweave init\n'));
            }
            else {
                console.log(chalk.yellow('⚠️  Not a SpecWeave project (no .specweave directory found)'));
                console.log(chalk.gray('   Run: specweave init\n'));
            }
            // Nothing to do outside a SpecWeave project — plugin refresh requires
            // a project context and can hang when run from an arbitrary directory.
            if (result.selfUpdated) {
                console.log(chalk.green('✓ CLI updated successfully. Run `specweave update` from a project directory to refresh plugins.\n'));
            }
            return;
        }
    }
    // Step 1: Update instructions & migrate config
    if (isSpecWeaveProject) {
        spinner.start('Updating instructions and config...');
        try {
            await updateInstructionsCommand({
                dryRun: options.check,
                verbose: options.verbose,
            });
            result.instructionsUpdated = true;
            result.configMigrated = true; // updateInstructionsCommand now handles this
            spinner.succeed('Instructions and config updated');
        }
        catch (error) {
            spinner.fail('Failed to update instructions');
            result.errors.push(`Instructions update failed: ${error}`);
        }
    }
    // Step 2: Cleanup stale auto state files (prevents infinite stop hook loops!)
    if (isSpecWeaveProject) {
        const cleanupResult = await cleanupStaleAutoState(projectPath, options.verbose, options.check);
        result.stateFilesCleaned = cleanupResult.cleaned;
        if (cleanupResult.cleaned > 0) {
            if (options.check) {
                console.log(chalk.yellow(`  ⚠️  ${cleanupResult.cleaned} stale auto state file(s) found (will be cleaned on update)`));
            }
            else {
                console.log(chalk.green(`  ✓ Cleaned ${cleanupResult.cleaned} stale auto state file(s)`));
            }
            if (options.verbose || options.check) {
                cleanupResult.files.forEach(file => {
                    console.log(chalk.gray(`    - ${file}`));
                });
            }
        }
    }
    // Step 2.4: Migrate legacy ~/.claude/commands/ to native plugin cache (v1.0.356)
    // Prior to 1.0.356, plugins were manually copied to ~/.claude/commands/<name>/.
    // Now they're installed via `claude plugin install` to ~/.claude/plugins/cache/.
    {
        const home = process.env.HOME || process.env.USERPROFILE || '';
        const legacyCommandsDir = path.join(home, '.claude', 'commands');
        if (fs.existsSync(legacyCommandsDir)) {
            const legacyPluginDirs = ['sw', 'sw-github', 'sw-jira', 'sw-ado'];
            let migratedCount = 0;
            for (const pluginName of legacyPluginDirs) {
                const legacyDir = path.join(legacyCommandsDir, pluginName);
                if (fs.existsSync(legacyDir)) {
                    if (options.check) {
                        console.log(chalk.yellow(`  \u26a0\ufe0f  Legacy ~/.claude/commands/${pluginName}/ will be removed (migrated to native plugin cache)`));
                        migratedCount++;
                    }
                    else {
                        try {
                            fs.rmSync(legacyDir, { recursive: true, force: true });
                            migratedCount++;
                        }
                        catch {
                            // Best effort
                        }
                    }
                }
            }
            if (migratedCount > 0 && !options.check) {
                console.log(chalk.green(`  \u2713 Migrated ${migratedCount} legacy plugin(s) from ~/.claude/commands/ to native cache`));
            }
        }
    }
    // Step 2.5: Remove deprecated .specweave/memory/ directory
    // No migration needed - just delete. Learnings now go to CLAUDE.md
    if (isSpecWeaveProject) {
        const memoryDir = path.join(projectPath, '.specweave', 'memory');
        if (fs.existsSync(memoryDir)) {
            if (options.check) {
                console.log(chalk.yellow(`  ⚠️  Deprecated .specweave/memory/ will be deleted`));
            }
            else {
                fs.rmSync(memoryDir, { recursive: true, force: true });
                console.log(chalk.green(`  ✓ Removed deprecated .specweave/memory/`));
            }
        }
    }
    // Step 2.5b: Clean up invalid folders in .specweave/increments/ (v1.0.257+)
    // Removes: unrecognized underscore folders (_analysis, etc.) and nested .specweave
    if (isSpecWeaveProject) {
        const { RECOGNIZED_LIFECYCLE_FOLDERS } = await import('../../core/increment/increment-utils.js');
        const incrementsDir = path.join(projectPath, '.specweave', 'increments');
        if (fs.existsSync(incrementsDir)) {
            const recognizedSet = new Set(RECOGNIZED_LIFECYCLE_FOLDERS);
            const entries = fs.readdirSync(incrementsDir, { withFileTypes: true });
            for (const entry of entries) {
                if (!entry.isDirectory())
                    continue;
                const fullPath = path.join(incrementsDir, entry.name);
                // Remove nested .specweave folder (accidental init artifact)
                if (entry.name === '.specweave') {
                    if (options.check) {
                        console.log(chalk.yellow(`  ⚠️  Nested .specweave/ inside increments will be removed`));
                    }
                    else {
                        fs.rmSync(fullPath, { recursive: true, force: true });
                        console.log(chalk.green(`  ✓ Removed nested .specweave/ from increments/`));
                    }
                    continue;
                }
                // Remove unrecognized underscore folders
                if (entry.name.startsWith('_') && !recognizedSet.has(entry.name)) {
                    if (options.check) {
                        console.log(chalk.yellow(`  ⚠️  Unrecognized folder ${entry.name} will be removed`));
                    }
                    else {
                        fs.rmSync(fullPath, { recursive: true, force: true });
                        console.log(chalk.green(`  ✓ Removed unrecognized folder: increments/${entry.name}`));
                    }
                }
            }
        }
    }
    // Step 2.6: Migrate reflect-config.json to enable autoReflect by default (v1.0.173+)
    // Projects initialized before v1.0.96 may have autoReflect: false
    if (isSpecWeaveProject && !options.check) {
        const reflectMigrated = migrateReflectConfig(projectPath, options.verbose);
        if (reflectMigrated) {
            console.log(chalk.green(`  ✓ Enabled autoReflect in reflect-config.json (self-improving AI)`));
        }
    }
    // Step 2.7: Prune old skill memories (v1.0.228+)
    // Removes learnings older than 90 days or exceeding 10 per skill
    if (isSpecWeaveProject) {
        const skillMemoryFiles = listSkillMemoryFiles(projectPath);
        if (skillMemoryFiles.length > 0) {
            if (options.check) {
                console.log(chalk.yellow(`  ⚠️  ${skillMemoryFiles.length} skill memory file(s) will be checked for pruning`));
            }
            else {
                const pruneResult = pruneSkillMemories(projectPath, {
                    retentionDays: 90,
                    maxLearningsPerSkill: 10,
                });
                if (pruneResult.prunedCount > 0) {
                    console.log(chalk.green(`  ✓ Pruned ${pruneResult.prunedCount} old learning(s) from ${pruneResult.skillsAffected.length} skill(s)`));
                    if (options.verbose) {
                        pruneResult.skillsAffected.forEach(skill => {
                            console.log(chalk.gray(`    - ${skill}`));
                        });
                    }
                }
                if (pruneResult.errors.length > 0 && options.verbose) {
                    pruneResult.errors.forEach(err => {
                        console.log(chalk.yellow(`    ⚠️ ${err}`));
                    });
                }
            }
        }
    }
    // LSP is OPT-IN only (v1.0.210+)
    // Users who want LSP should run: specweave lsp enable
    // Removed: forced ensureLspSettingsOnUpdate, setupLspEnvVar, migrateLspConfig
    // Step 2.10: Ensure Deep Interview config in .specweave/config.json (v1.0.195+)
    // Projects initialized before v1.0.195 may not have the planning.deepInterview section
    // This enables all existing clients to use deep interview for spec design
    if (isSpecWeaveProject && !options.check) {
        const deepInterviewMigrated = migrateDeepInterviewConfig(projectPath, options.verbose);
        if (deepInterviewMigrated) {
            console.log(chalk.green(`  ✓ Added Deep Interview config (disabled by default, enable in config.json)`));
        }
    }
    // Step 2.11: Clean up stale .specweave/ folders in parent directories (v1.0.262+)
    // These are created by bugs in hooks that use process.cwd() without config.json validation
    if (isSpecWeaveProject) {
        const staleFolders = findStaleSpecweaveFolders(projectPath);
        if (staleFolders.length > 0) {
            for (const staleDir of staleFolders) {
                if (options.check) {
                    console.log(chalk.yellow(`  ⚠️  Stale .specweave/ found at ${staleDir} (will be removed)`));
                }
                else {
                    try {
                        fs.rmSync(staleDir, { recursive: true, force: true });
                        console.log(chalk.green(`  ✓ Removed stale .specweave/ at ${staleDir}`));
                    }
                    catch {
                        console.log(chalk.yellow(`  ⚠️  Could not remove stale .specweave/ at ${staleDir}`));
                    }
                }
            }
        }
    }
    // Step 3: Validate project health (quick checks)
    if (isSpecWeaveProject && !options.check) {
        spinner.start('Validating project health...');
        const healthIssues = await validateProjectHealth(projectPath);
        if (healthIssues.length > 0) {
            spinner.warn(`Found ${healthIssues.length} issue(s)`);
            result.warnings.push(...healthIssues);
            healthIssues.forEach(issue => {
                console.log(chalk.yellow(`   - ${issue}`));
            });
        }
        else {
            spinner.succeed('Project health OK');
        }
    }
    // Step 3b: Installation health scan (ghost commands, stale cache, etc.)
    // Runs BEFORE plugin refresh so issues are detected, then refresh fixes them
    {
        const { InstallationHealthChecker } = await import('../../core/doctor/checkers/installation-health-checker.js');
        const installChecker = new InstallationHealthChecker();
        const installResult = await installChecker.check(projectPath, { fix: !options.check });
        const installIssues = installResult.checks.filter(c => c.status === 'warn' || c.status === 'fail');
        if (installIssues.length > 0) {
            for (const issue of installIssues) {
                if (options.check) {
                    console.log(chalk.yellow(`  ⚠️  ${issue.name}: ${issue.message}`));
                }
                else {
                    console.log(chalk.green(`  ✓ Fixed: ${issue.name} (${issue.message})`));
                }
            }
        }
    }
    // Step 4: Refresh plugins (DEFAULT - unless --no-plugins specified)
    if (!options.noPlugins) {
        console.log('');
        spinner.start('Refreshing marketplace plugins...');
        spinner.stop();
        try {
            await refreshPluginsCommand({
                all: options.all ?? true,
                force: options.force,
                verbose: options.verbose,
            });
            result.pluginsRefreshed = true;
        }
        catch (error) {
            result.errors.push(`Plugin refresh failed: ${error}`);
        }
        // Step 4a-fix: Sync plugin cache hooks (v1.0.306)
        // Claude Code's plugin cache (~/.claude/plugins/cache/) may have stale hooks
        // if the source was updated but `claude plugin install` didn't refresh the cache.
        // Directly patch the cache to ensure hooks match the source.
        try {
            const { InstallationHealthChecker } = await import('../../core/doctor/checkers/installation-health-checker.js');
            const cacheChecker = new InstallationHealthChecker();
            const cacheResult = await cacheChecker.check(projectPath, { fix: !options.check });
            const cacheFixed = cacheResult.checks.find(c => c.name === 'Plugin cache hook freshness');
            if (cacheFixed && cacheFixed.status !== 'pass') {
                console.log(chalk.green(`  ✓ Plugin cache hooks updated`));
            }
        }
        catch {
            // Non-critical — plugin cache freshness is best-effort
        }
        // NOTE: sw@specweave is enabled at USER level by plugin-installer.ts
        // Do NOT enable it at project level — that would make the core framework
        // plugin appear as "Project" scope instead of "User" scope in Claude Code UI.
    }
    // Summary
    console.log(chalk.blue.bold('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
    console.log(chalk.blue.bold('  Update Summary'));
    console.log(chalk.blue.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
    if (isSpecWeaveProject) {
        console.log(`  Config:       ${result.configMigrated ? chalk.green('✓ Updated') : chalk.gray('No changes')}`);
        console.log(`  Instructions: ${result.instructionsUpdated ? chalk.green('✓ Updated') : chalk.gray('No changes')}`);
        console.log(`  State cleanup: ${result.stateFilesCleaned > 0 ? chalk.green(`✓ Cleaned ${result.stateFilesCleaned} file(s)`) : chalk.gray('No stale files')}`);
    }
    if (!options.noPlugins) {
        console.log(`  Plugins:      ${result.pluginsRefreshed ? chalk.green('✓ Refreshed') : chalk.red('Failed')}`);
    }
    else {
        console.log(chalk.gray(`  Plugins:      Skipped (--no-plugins specified)`));
    }
    if (result.warnings.length > 0) {
        console.log(chalk.yellow(`\n  Warnings: ${result.warnings.length}`));
        result.warnings.forEach(warn => {
            console.log(chalk.yellow(`    - ${warn}`));
        });
    }
    if (result.errors.length > 0) {
        console.log(chalk.red(`\n  Errors: ${result.errors.length}`));
        result.errors.forEach(err => {
            console.log(chalk.red(`    - ${err}`));
        });
    }
    // Next steps
    console.log(chalk.blue('\n  Next steps:'));
    if (!options.noPlugins) {
        console.log(chalk.gray('    1. Restart Claude Code for plugin changes'));
        if (result.warnings.length > 0) {
            console.log(chalk.gray('    2. Review warnings above'));
        }
    }
    else {
        console.log(chalk.gray('    1. Plugins were skipped. Run `specweave update` to refresh them.'));
        if (result.warnings.length > 0) {
            console.log(chalk.gray('    2. Review warnings above'));
        }
    }
    console.log('');
    // Set non-zero exit code if any errors occurred (CI/CD friendly)
    if (result.errors.length > 0) {
        process.exitCode = 1;
    }
}
/**
 * Cleanup stale auto state files that can cause infinite stop hook loops
 * Only cleans files older than the threshold (default: 4 hours for safety)
 *
 * @param projectPath - Path to the project root
 * @param verbose - Show detailed output
 * @param dryRun - If true, only report what would be cleaned (don't actually delete)
 */
async function cleanupStaleAutoState(projectPath, verbose, dryRun) {
    const stateDir = path.join(projectPath, '.specweave', 'state');
    const result = { cleaned: 0, files: [] };
    if (!fs.existsSync(stateDir)) {
        return result;
    }
    // All auto-related state files that should be session-scoped
    const autoStateFiles = [
        'auto-mode.json',
        'auto-session.json',
        '.stop-auto-dedup',
        '.stop-auto-dedup-prev',
        '.stop-auto-turns',
    ];
    // Threshold: 4 hours (sessions rarely last this long, and if they do, state should be fresh)
    const staleThresholdHours = 4;
    for (const file of autoStateFiles) {
        const filePath = path.join(stateDir, file);
        if (fs.existsSync(filePath)) {
            try {
                const stats = fs.statSync(filePath);
                const ageHours = (Date.now() - stats.mtimeMs) / (1000 * 60 * 60);
                // Only clean if stale (older than threshold)
                if (ageHours > staleThresholdHours) {
                    if (dryRun) {
                        // Dry run - just report
                        result.cleaned++;
                        result.files.push(`${file} (${Math.floor(ageHours)}h old)`);
                    }
                    else {
                        // Actually delete
                        try {
                            fs.unlinkSync(filePath);
                            result.cleaned++;
                            result.files.push(`${file} (was ${Math.floor(ageHours)}h old)`);
                        }
                        catch {
                            // Ignore cleanup errors silently
                        }
                    }
                }
            }
            catch {
                // Ignore stat errors
            }
        }
    }
    return result;
}
/**
 * Find stale .specweave/ folders in parent directories (no config.json = stale)
 *
 * These are typically created by bugs in hooks that use process.cwd() or
 * ${SW_PROJECT_ROOT:-.} before project root detection runs. They contain
 * only logs/ and state/ subdirectories but no config.json.
 *
 * Only scans UP to 3 levels above the project, plus $HOME/.specweave.
 */
function findStaleSpecweaveFolders(projectPath) {
    const staleFolders = [];
    const projectResolved = path.resolve(projectPath);
    // Scan parent directories (up to 3 levels)
    let current = path.dirname(projectResolved);
    const root = path.parse(current).root;
    let depth = 0;
    while (current !== root && depth < 3) {
        const candidate = path.join(current, '.specweave');
        if (fs.existsSync(candidate) &&
            fs.statSync(candidate).isDirectory() &&
            !fs.existsSync(path.join(candidate, 'config.json'))) {
            staleFolders.push(candidate);
        }
        current = path.dirname(current);
        depth++;
    }
    // Also check $HOME/.specweave (created by hooks using $HOME paths)
    const homeSpecweave = path.join(process.env.HOME || process.env.USERPROFILE || '', '.specweave');
    if (homeSpecweave &&
        fs.existsSync(homeSpecweave) &&
        fs.statSync(homeSpecweave).isDirectory() &&
        !fs.existsSync(path.join(homeSpecweave, 'config.json'))) {
        // Don't remove ~/.specweave if it's the user-level config dir
        // Only remove if it just has logs/state (no meaningful content)
        const entries = fs.readdirSync(homeSpecweave);
        const onlyRuntimeDirs = entries.every(e => ['logs', 'state', 'cache'].includes(e));
        if (onlyRuntimeDirs && entries.length > 0) {
            staleFolders.push(homeSpecweave);
        }
    }
    return staleFolders;
}
/**
 * Quick project health validation (no cleanup - that's done separately)
 */
async function validateProjectHealth(projectPath) {
    const issues = [];
    // Check config.json exists and is valid
    const configPath = path.join(projectPath, '.specweave', 'config.json');
    if (fs.existsSync(configPath)) {
        try {
            const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
            // Check for missing recommended sections
            if (!config.auto) {
                issues.push('Missing "auto" section in config.json (will be added)');
            }
            if (!config.project?.name) {
                issues.push('Missing project.name in config.json');
            }
        }
        catch {
            issues.push('config.json is invalid JSON');
        }
    }
    else {
        issues.push('config.json not found');
    }
    // Check for too many active increments
    const incrementsDir = path.join(projectPath, '.specweave', 'increments');
    if (fs.existsSync(incrementsDir)) {
        try {
            const dirs = fs.readdirSync(incrementsDir).filter(name => /^\d{4}/.test(name));
            let activeCount = 0;
            for (const dir of dirs) {
                const metadataPath = path.join(incrementsDir, dir, 'metadata.json');
                if (fs.existsSync(metadataPath)) {
                    try {
                        const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
                        if (metadata.status === 'active' || metadata.status === 'in-progress') {
                            activeCount++;
                        }
                    }
                    catch {
                        // Ignore parse errors
                    }
                }
            }
            if (activeCount > 5) {
                issues.push(`High number of active increments: ${activeCount} (consider closing some)`);
            }
        }
        catch {
            // Ignore directory read errors
        }
    }
    return issues;
}
/**
 * Fetch what's new from npm package changelog
 */
async function fetchWhatsNew(currentVersion, latestVersion) {
    try {
        // Try to get changelog from GitHub raw content
        const https = await import('https');
        return new Promise((resolve) => {
            const url = 'https://raw.githubusercontent.com/anthropics/specweave/main/CHANGELOG.md';
            const req = https.get(url, { timeout: 5000 }, (res) => {
                if (res.statusCode !== 200) {
                    resolve([]);
                    return;
                }
                let data = '';
                res.on('data', (chunk) => { data += chunk.toString(); });
                res.on('end', () => {
                    const changes = parseChangelogBetweenVersions(data, currentVersion, latestVersion);
                    resolve(changes);
                });
            });
            req.on('error', () => resolve([]));
            req.on('timeout', () => {
                req.destroy();
                resolve([]);
            });
        });
    }
    catch {
        return [];
    }
}
/**
 * Parse changelog entries between two versions
 */
function parseChangelogBetweenVersions(changelog, fromVersion, toVersion) {
    const changes = [];
    const lines = changelog.split('\n');
    let inRelevantSection = false;
    let currentSection = '';
    for (const line of lines) {
        // Match version headers like ## [1.0.132] or ## [1.0.132] - 2026-01-20
        const versionMatch = line.match(/^## \[(\d+\.\d+\.\d+)\]/);
        if (versionMatch) {
            const version = versionMatch[1];
            const versionNum = versionToNumber(version);
            const fromNum = versionToNumber(fromVersion);
            const toNum = versionToNumber(toVersion);
            // Include versions > fromVersion and <= toVersion
            if (versionNum > fromNum && versionNum <= toNum) {
                inRelevantSection = true;
                changes.push(`\n${chalk.cyan.bold(`v${version}`)}`);
            }
            else if (versionNum <= fromNum) {
                inRelevantSection = false;
            }
            continue;
        }
        if (inRelevantSection) {
            // Match section headers like ### ✨ Features
            if (line.startsWith('### ')) {
                currentSection = line.replace('### ', '').trim();
                changes.push(chalk.yellow(`  ${currentSection}`));
            }
            // Match bullet points
            else if (line.startsWith('- ')) {
                const item = line.replace('- ', '').trim();
                // Truncate long items
                const truncated = item.length > 80 ? item.substring(0, 77) + '...' : item;
                changes.push(chalk.gray(`    • ${truncated}`));
            }
        }
    }
    return changes.slice(0, 20); // Limit to 20 lines
}
/**
 * Convert version string to number for comparison
 */
function versionToNumber(version) {
    const parts = version.split('.').map(Number);
    return parts[0] * 1000000 + parts[1] * 1000 + parts[2];
}
/**
 * Install specweave with ETARGET fallback for npm CDN propagation delays.
 *
 * 1. Try `npm install -g specweave@{version}` (pinned to resolved version)
 * 2. If ETARGET: query `npm view specweave versions --json`, pick highest available
 * 3. Retry with that version
 * 4. If still fails: throw with actionable suggestion
 */
function installWithFallback(targetVersion) {
    try {
        npmPublicInstall(`npm install -g specweave@${targetVersion}`, 120000);
        return { installedVersion: targetVersion };
    }
    catch (error) {
        const stderr = error.stderr?.toString() || error.message || '';
        // Only attempt fallback for ETARGET/notarget errors (CDN propagation)
        if (!stderr.includes('ETARGET') && !stderr.includes('notarget')) {
            throw error;
        }
        // Fallback: get all published versions, pick highest (read-only, no sudo needed)
        let allVersions;
        try {
            const versionsJson = npmPublicExec('npm view specweave versions --json', 30000);
            const parsed = JSON.parse(versionsJson);
            allVersions = Array.isArray(parsed) ? parsed : [parsed];
        }
        catch {
            throw new Error(`ETARGET: version ${targetVersion} not yet available on npm CDN. ` +
                `Try: npm cache clean --force && specweave update`);
        }
        if (allVersions.length === 0) {
            throw new Error(`ETARGET: version ${targetVersion} not available and no versions found. ` +
                `Try: npm cache clean --force && specweave update`);
        }
        // Pick the highest available version
        const fallbackVersion = allVersions[allVersions.length - 1];
        try {
            npmPublicInstall(`npm install -g specweave@${fallbackVersion}`, 120000);
            return { installedVersion: fallbackVersion };
        }
        catch {
            throw new Error(`ETARGET: neither ${targetVersion} nor ${fallbackVersion} could be installed. ` +
                `Try: npm cache clean --force && specweave update`);
        }
    }
}
/**
 * Build a clean environment for npm commands targeting the public registry.
 * Bypasses ALL auth config sources that can cause E401 on public packages:
 *   - ~/.npmrc (user config) — stale tokens from npm login, GitHub Packages
 *   - $PREFIX/etc/npmrc (global config) — nvm/Homebrew-installed node configs
 *   - NPM_TOKEN env var — CI environments
 *   - npm_config_* env vars — AWS CodeArtifact, Azure DevOps, Artifactory, etc.
 *
 * Since specweave self-update ALWAYS targets the public npm registry,
 * we ALWAYS use this clean env — no fallback/retry needed.
 */
function buildPublicRegistryEnv() {
    const env = { ...process.env };
    // Strip auth-related env vars
    delete env['NPM_TOKEN'];
    for (const key of Object.keys(env)) {
        if (key.startsWith('npm_config_') &&
            (key.includes('authToken') || key.includes('_auth') || key.includes('_password'))) {
            delete env[key];
        }
    }
    // Point user and global config to non-existent files to bypass all npmrc auth
    const emptyConfig = process.platform === 'win32' ? 'NUL' : '/dev/null';
    env['npm_config_userconfig'] = emptyConfig;
    // Use a distinct path for globalconfig to avoid npm's "double-loading" error
    env['npm_config_globalconfig'] = path.join(process.platform === 'win32' ? (process.env['TEMP'] || 'C:\\Temp') : '/tmp', '.specweave-npm-noop');
    return env;
}
/**
 * Run an npm command against the public registry with a clean environment.
 * Always bypasses user auth config to prevent E401 from stale tokens.
 * Used for all specweave self-update operations (view, install).
 */
function npmPublicExec(command, timeout) {
    return execSync(`${command} ${npmRegistryFlag()}`, {
        encoding: 'utf-8',
        stdio: ['pipe', 'pipe', 'pipe'],
        timeout,
        env: buildPublicRegistryEnv(),
    }).trim();
}
/**
 * Check whether the current user can write to the global npm node_modules directory.
 * Returns true on Windows (sudo not applicable), or when the directory is writable.
 * Returns false when npm was installed with sudo and the global prefix is root-owned.
 */
function isGlobalNpmWritable() {
    if (process.platform === 'win32')
        return true;
    try {
        const prefix = execSync('npm prefix -g', {
            encoding: 'utf-8',
            stdio: ['pipe', 'pipe', 'pipe'],
            timeout: 5000,
            env: buildPublicRegistryEnv(),
        }).trim();
        const modulesDir = path.join(prefix, 'lib', 'node_modules');
        fs.accessSync(modulesDir, fs.constants.W_OK);
        return true;
    }
    catch {
        return false;
    }
}
/**
 * Check if sudo is available (not on Windows, not in some containers).
 */
function isSudoAvailable() {
    if (process.platform === 'win32')
        return false;
    try {
        execSync('which sudo', {
            encoding: 'utf-8',
            stdio: ['pipe', 'pipe', 'pipe'],
            timeout: 3000,
        });
        return true;
    }
    catch {
        return false;
    }
}
/**
 * Run npm install for self-update, with automatic sudo elevation when needed.
 *
 * Unlike npmPublicExec (which captures output), this uses stdio: 'inherit'
 * when sudo is required so the user can enter their password.
 * Falls back to non-sudo if sudo is not available.
 */
function npmPublicInstall(installCommand, timeout) {
    const writable = isGlobalNpmWritable();
    if (writable) {
        // Normal install — no sudo needed
        execSync(`${installCommand} ${npmRegistryFlag()}`, {
            encoding: 'utf-8',
            stdio: ['pipe', 'pipe', 'pipe'],
            timeout,
            env: buildPublicRegistryEnv(),
        });
        return;
    }
    // Not writable — try with sudo if available
    if (isSudoAvailable()) {
        execSync(`sudo ${installCommand} ${npmRegistryFlag()}`, {
            stdio: 'inherit',
            timeout,
            env: buildPublicRegistryEnv(),
        });
        return;
    }
    // Not writable and no sudo — try anyway (will fail with EACCES, caught upstream)
    execSync(`${installCommand} ${npmRegistryFlag()}`, {
        encoding: 'utf-8',
        stdio: ['pipe', 'pipe', 'pipe'],
        timeout,
        env: buildPublicRegistryEnv(),
    });
}
/**
 * Self-update SpecWeave CLI via npm
 */
async function selfUpdateSpecWeave(currentVersion, options, spinner) {
    spinner.start('Checking for SpecWeave updates...');
    try {
        // Get latest version from npm (30s timeout to prevent indefinite hang)
        const latestVersion = npmPublicExec('npm view specweave version', 30000);
        // Compare versions
        if (latestVersion === currentVersion) {
            spinner.succeed(`SpecWeave is up to date (v${currentVersion})`);
            if (options.check) {
                console.log(chalk.green('\n  ✓ You have the latest version. No update needed.'));
            }
            return { updated: false };
        }
        // Check if newer version available
        const currentParts = currentVersion.split('.').map(Number);
        const latestParts = latestVersion.split('.').map(Number);
        let isNewer = false;
        for (let i = 0; i < 3; i++) {
            if (latestParts[i] > currentParts[i]) {
                isNewer = true;
                break;
            }
            else if (latestParts[i] < currentParts[i]) {
                break;
            }
        }
        if (!isNewer) {
            spinner.succeed(`SpecWeave is up to date (v${currentVersion})`);
            return { updated: false };
        }
        spinner.info(`New version available: v${currentVersion} → v${latestVersion}`);
        // Fetch and show what's new
        spinner.start('Fetching changelog...');
        const whatsNew = await fetchWhatsNew(currentVersion, latestVersion);
        spinner.stop();
        if (whatsNew.length > 0) {
            console.log(chalk.blue.bold('\n  📋 What\'s New:'));
            whatsNew.forEach(line => console.log(line));
            console.log('');
        }
        // Dry run - show recommendation
        if (options.check) {
            console.log(chalk.green.bold('  ✅ Update recommended!'));
            console.log('');
            console.log(chalk.gray('  To update, run:'));
            console.log(chalk.cyan('    specweave update'));
            console.log('');
            return { updated: false, newVersion: latestVersion };
        }
        // Perform update — pin to the resolved version instead of @latest
        // to avoid CDN propagation race conditions where the dist-tag points
        // to a version whose tarball hasn't propagated to all CDN edges yet
        spinner.start(`Updating SpecWeave: v${currentVersion} → v${latestVersion}...`);
        const installResult = installWithFallback(latestVersion);
        const expectedVersion = installResult.installedVersion;
        // Verify the new binary is functional before declaring success
        try {
            const installedVersion = execSync('specweave --version', {
                encoding: 'utf-8',
                stdio: ['pipe', 'pipe', 'pipe'],
                timeout: 10000,
            }).trim();
            // Extract version number (output may include prefix like "specweave/1.0.233")
            const versionMatch = installedVersion.match(/(\d+\.\d+\.\d+)/);
            const actualVersion = versionMatch ? versionMatch[1] : installedVersion;
            if (actualVersion !== expectedVersion) {
                spinner.warn(`SpecWeave installed but version mismatch: expected v${expectedVersion}, got v${actualVersion}`);
                return { updated: false, error: `Version mismatch after install: expected ${expectedVersion}, got ${actualVersion}` };
            }
        }
        catch {
            spinner.warn('SpecWeave installed but could not verify new binary');
            return { updated: true, newVersion: expectedVersion };
        }
        spinner.succeed(`SpecWeave updated: v${currentVersion} → v${expectedVersion}`);
        return { updated: true, newVersion: expectedVersion };
    }
    catch (error) {
        spinner.fail('Failed to check/update SpecWeave');
        // Provide helpful error messages
        if (error.message?.includes('EACCES') || error.message?.includes('permission denied')) {
            const suggestion = process.platform === 'win32'
                ? 'Permission denied. Run your terminal as Administrator.'
                : 'Permission denied. Try: sudo specweave update';
            return {
                updated: false,
                error: suggestion,
            };
        }
        if (error.message?.includes('ENOENT') || error.message?.includes('npm: not found')) {
            return {
                updated: false,
                error: 'npm not found. Make sure Node.js is installed.',
            };
        }
        if (error.message?.includes('ETIMEDOUT') || error.signal === 'SIGTERM' || error.killed) {
            return {
                updated: false,
                error: 'npm registry timed out. Check your network connection and try again.',
            };
        }
        if (error.message?.includes('ETARGET') || error.message?.includes('notarget')) {
            return {
                updated: false,
                error: 'npm CDN propagation delay. Try: npm cache clean --force && specweave update',
            };
        }
        return {
            updated: false,
            error: `Self-update failed: ${error.message || error}`,
        };
    }
}
/**
 * Migrate reflect-config.json to enable autoReflect by default
 * Projects initialized before v1.0.96 may have autoReflect: false or missing
 *
 * @param projectPath - Path to the project root
 * @param verbose - Show detailed output
 * @returns true if migration was performed
 */
function migrateReflectConfig(projectPath, verbose) {
    const reflectConfigPath = path.join(projectPath, '.specweave', 'state', 'reflect-config.json');
    // Create state directory if missing
    const stateDir = path.join(projectPath, '.specweave', 'state');
    if (!fs.existsSync(stateDir)) {
        fs.mkdirSync(stateDir, { recursive: true });
    }
    // If reflect-config.json doesn't exist, create it with defaults
    if (!fs.existsSync(reflectConfigPath)) {
        const defaultConfig = {
            enabled: true,
            autoReflect: true,
            enabledAt: new Date().toISOString(),
            confidenceThreshold: 'medium',
            maxLearningsPerSession: 10,
            gitCommit: false,
            gitPush: false,
        };
        fs.writeFileSync(reflectConfigPath, JSON.stringify(defaultConfig, null, 2));
        if (verbose) {
            console.log(chalk.gray(`    Created reflect-config.json with autoReflect: true`));
        }
        return true;
    }
    // If it exists, check if autoReflect needs to be enabled
    try {
        const config = JSON.parse(fs.readFileSync(reflectConfigPath, 'utf-8'));
        // Only migrate if autoReflect is explicitly false or missing
        if (config.autoReflect === false || config.autoReflect === undefined) {
            config.autoReflect = true;
            // Also ensure other defaults are present
            if (config.enabled === undefined)
                config.enabled = true;
            if (config.confidenceThreshold === undefined)
                config.confidenceThreshold = 'medium';
            if (config.maxLearningsPerSession === undefined)
                config.maxLearningsPerSession = 10;
            fs.writeFileSync(reflectConfigPath, JSON.stringify(config, null, 2));
            if (verbose) {
                console.log(chalk.gray(`    Updated reflect-config.json: autoReflect: false → true`));
            }
            return true;
        }
        // autoReflect is already true, no migration needed
        return false;
    }
    catch (error) {
        // If parsing fails, recreate with defaults
        if (verbose) {
            console.log(chalk.yellow(`    ⚠ reflect-config.json was invalid, recreating with defaults`));
        }
        const defaultConfig = {
            enabled: true,
            autoReflect: true,
            enabledAt: new Date().toISOString(),
            confidenceThreshold: 'medium',
            maxLearningsPerSession: 10,
            gitCommit: false,
            gitPush: false,
        };
        fs.writeFileSync(reflectConfigPath, JSON.stringify(defaultConfig, null, 2));
        return true;
    }
}
/**
 * Migrate config.json to add Deep Interview section if missing (v1.0.195+)
 * Projects initialized before v1.0.195 may not have the planning.deepInterview section
 * This enables all clients to use deep interview for spec design after running update
 *
 * @param projectPath - Path to the project root
 * @param verbose - Show detailed output
 * @returns true if migration was performed
 */
export function migrateDeepInterviewConfig(projectPath, verbose) {
    const configPath = path.join(projectPath, '.specweave', 'config.json');
    if (!fs.existsSync(configPath)) {
        return false;
    }
    try {
        const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
        // Only migrate if planning.deepInterview is missing
        if (config.planning?.deepInterview) {
            // Already exists, don't overwrite user's settings
            return false;
        }
        // Initialize planning object if missing
        if (!config.planning) {
            config.planning = {};
        }
        // Add deep interview with defaults (disabled by default per ADR-0232)
        config.planning.deepInterview = {
            enabled: false, // Opt-in, users can enable in config.json or during sw:increment
            minQuestions: 5, // Soft guideline - LLM should assess complexity
            categories: [
                'architecture',
                'integrations',
                'ui-ux',
                'performance',
                'security',
                'edge-cases',
            ],
        };
        fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
        if (verbose) {
            console.log(chalk.gray(`    Added planning.deepInterview section to config.json`));
        }
        return true;
    }
    catch (error) {
        // If parsing fails, don't modify
        if (verbose) {
            console.log(chalk.yellow(`    ⚠ Could not migrate Deep Interview config: ${error}`));
        }
        return false;
    }
}
// migrateLspConfig removed (v1.0.210) - LSP is opt-in only, not forced during update
/**
 * Register command with Commander
 */
export function registerUpdateCommand(program) {
    program
        .command('update')
        .description('Update SpecWeave: CLI, instructions, config, AND plugins (default)')
        .option('--no-self', 'Skip CLI self-update via npm')
        .option('--no-plugins', 'Skip marketplace plugins refresh')
        .option('--all', 'Install ALL plugins (not just router)')
        .option('--minimal', 'Clean /plugin output (removes marketplace, no lazy loading)')
        .option('--check', 'Dry run - show what would change without making changes')
        .option('-v, --verbose', 'Show detailed output')
        .option('-f, --force', 'Force refresh even if up to date')
        .action(async (options) => {
        await updateCommand(options);
    });
}
//# sourceMappingURL=update.js.map