// @crash-shield-ignore-file — config/cache reader for Planu-controlled JSON; writer is this codebase, shape guaranteed by build/seed.
// cli/commands/install.ts — planu install: detect AI tools and install MCP entry (SPEC-236)
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
import { homedir } from 'node:os';
import { dirname, join } from 'node:path';
import { bold, cyan, green, yellow, dim } from '../colors.js';
import { PLANU_VERSION } from '../../config/version.js';
import { confirm, select, ask } from '../interactive.js';
import { activateLicense } from '../../engine/license-validator.js';
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const PLANU_KEY = 'planu';
// ---------------------------------------------------------------------------
// Locale helpers
// ---------------------------------------------------------------------------
const CLAUDE_CODE_LOCALES = new Set(['en', 'es', 'pt', 'fr', 'de', 'ja', 'ko', 'zh']);
function detectLocale() {
    const raw = process.env.LANG ?? process.env.LC_ALL ?? process.env.LC_MESSAGES ?? '';
    const code = raw.split(/[_.-]/)[0]?.toLowerCase() ?? 'en';
    return CLAUDE_CODE_LOCALES.has(code) ? code : 'en';
}
function claudeCodeDocsUrl() {
    return `https://code.claude.com/docs/${detectLocale()}/overview`;
}
const TOOLS = [
    {
        name: 'Claude Code',
        userScopePath: join(homedir(), '.claude.json'),
        projectScopePath: join(process.cwd(), '.claude.json'),
        format: 'standard',
    },
    {
        name: 'Cursor',
        userScopePath: join(homedir(), '.cursor', 'mcp.json'),
        projectScopePath: join(process.cwd(), '.cursor', 'mcp.json'),
        format: 'standard',
    },
    {
        name: 'Windsurf',
        userScopePath: join(homedir(), '.codeium', 'windsurf', 'mcp_config.json'),
        projectScopePath: join(process.cwd(), '.windsurf', 'mcp.json'),
        format: 'standard',
    },
    {
        name: 'Zed',
        userScopePath: join(homedir(), '.config', 'zed', 'settings.json'),
        projectScopePath: join(process.cwd(), '.zed', 'settings.json'),
        format: 'zed',
    },
    {
        name: 'Cline',
        userScopePath: join(homedir(), '.vscode', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'),
        projectScopePath: null,
        format: 'standard',
    },
    {
        name: 'Continue',
        userScopePath: join(homedir(), '.continue', 'config.json'),
        projectScopePath: join(process.cwd(), '.continue', 'config.json'),
        format: 'standard',
    },
];
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function buildMcpEntry(licenseKey) {
    const entry = {
        command: 'npx',
        args: ['--prefer-online', '-y', '@planu/cli@latest'],
    };
    const ownerToken = licenseKey ?? process.env.SDD_OWNER_TOKEN;
    if (ownerToken) {
        entry.env = { SDD_OWNER_TOKEN: ownerToken };
    }
    return entry;
}
function readJsonFile(filePath) {
    if (!existsSync(filePath)) {
        return {};
    }
    const raw = readFileSync(filePath, 'utf-8').trim();
    if (!raw) {
        return {};
    }
    return JSON.parse(raw);
}
function hasPlanuEntry(config, format) {
    if (format === 'zed') {
        const zedConfig = config;
        return Boolean(zedConfig.context_servers?.[PLANU_KEY]);
    }
    const stdConfig = config;
    return Boolean(stdConfig.mcpServers?.[PLANU_KEY]);
}
function ensureParentDir(filePath) {
    const dir = dirname(filePath);
    if (!existsSync(dir)) {
        mkdirSync(dir, { recursive: true });
    }
}
function writeStandardEntry(filePath, entry) {
    let config = {};
    try {
        config = readJsonFile(filePath);
    }
    catch {
        // invalid JSON — start fresh
    }
    config.mcpServers ??= {};
    config.mcpServers[PLANU_KEY] = entry;
    ensureParentDir(filePath);
    writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
}
function writeZedEntry(filePath, entry) {
    let config = {};
    try {
        config = readJsonFile(filePath);
    }
    catch {
        // invalid JSON — start fresh
    }
    config.context_servers ??= {};
    const zedEntry = {
        command: { path: entry.command, args: entry.args },
    };
    if (entry.env) {
        zedEntry.env = entry.env;
    }
    config.context_servers[PLANU_KEY] = zedEntry;
    ensureParentDir(filePath);
    writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
}
function removePlanuFromStandard(config) {
    if (!config.mcpServers) {
        return config;
    }
    const filtered = {};
    for (const [key, val] of Object.entries(config.mcpServers)) {
        if (key !== PLANU_KEY) {
            filtered[key] = val;
        }
    }
    return { ...config, mcpServers: filtered };
}
function removePlanuFromZed(config) {
    if (!config.context_servers) {
        return config;
    }
    const filtered = {};
    for (const [key, val] of Object.entries(config.context_servers)) {
        if (key !== PLANU_KEY) {
            filtered[key] = val;
        }
    }
    return { ...config, context_servers: filtered };
}
function removeEntry(filePath, format) {
    let config;
    try {
        config = readJsonFile(filePath);
    }
    catch {
        return; // nothing to do
    }
    const updated = format === 'zed'
        ? removePlanuFromZed(config)
        : removePlanuFromStandard(config);
    writeFileSync(filePath, JSON.stringify(updated, null, 2) + '\n', 'utf-8');
}
// ---------------------------------------------------------------------------
// License step helper (exported for tests)
// ---------------------------------------------------------------------------
/**
 * Prompt the user for a license key and activate it.
 * Returns the activation result or null if the user skips.
 * In CI/headless mode (SDD_OWNER_TOKEN already set), returns a synthetic result immediately.
 */
export async function promptLicense() {
    // Headless / CI mode: use token from environment directly
    const envToken = process.env.SDD_OWNER_TOKEN;
    if (envToken) {
        return { key: envToken, customerEmail: '', variantName: 'Pro' };
    }
    const wantsLicense = await confirm('Do you have a Pro license key?', false);
    if (!wantsLicense) {
        return null;
    }
    const key = await ask('Enter your license key:');
    if (!key) {
        return null;
    }
    process.stdout.write(`${dim('Validating license key...')}\n`);
    try {
        const result = await activateLicense(key, 'planu-cli');
        if (!result.activated) {
            const errMsg = result.error ?? 'Invalid license key';
            process.stdout.write(`${yellow('!')} License validation failed: ${errMsg}\n`);
            const continueWithout = await confirm('Continue without license?', true);
            if (!continueWithout) {
                throw new Error('Aborted by user after license validation failure');
            }
            return null;
        }
        const email = result.meta.customer_email;
        const variant = result.meta.variant_name;
        process.stdout.write(`${green('+')} Pro license — ${email}\n`);
        return { key, customerEmail: email, variantName: variant };
    }
    catch (err) {
        if (err instanceof Error && err.message.startsWith('Aborted')) {
            throw err;
        }
        const msg = err instanceof Error ? err.message : String(err);
        process.stdout.write(`${yellow('!')} License API error: ${msg}\n`);
        const continueWithout = await confirm('Continue without license?', true);
        if (!continueWithout) {
            throw new Error('Aborted by user after license API error', { cause: err });
        }
        return null;
    }
}
// Exported so tests can use them without going through the interactive CLI
export { buildMcpEntry, readJsonFile, hasPlanuEntry, writeStandardEntry, writeZedEntry, removeEntry, };
// ---------------------------------------------------------------------------
// Banner
// ---------------------------------------------------------------------------
function printBanner() {
    const teal = (s) => `\x1b[38;5;43m${s}\x1b[0m`;
    const art = [
        '  ██████╗ ██╗      █████╗ ███╗   ██╗██╗   ██╗',
        '  ██╔══██╗██║     ██╔══██╗████╗  ██║██║   ██║',
        '  ██████╔╝██║     ███████║██╔██╗ ██║██║   ██║',
        '  ██╔═══╝ ██║     ██╔══██║██║╚██╗██║██║   ██║',
        '  ██║     ███████╗██║  ██║██║ ╚████║╚██████╔╝',
        '  ╚═╝     ╚══════╝╚═╝  ╚═╝╚═╝  ╚═══╝ ╚═════╝',
    ];
    process.stdout.write('\n');
    for (const line of art) {
        process.stdout.write(`${teal(line)}\n`);
    }
    process.stdout.write(`\n  ${dim(`Spec Driven Development MCP Server  ·  v${PLANU_VERSION}`)}\n`);
    process.stdout.write(`\n  ${bold('🔒 Privacy guarantee')}\n`);
    process.stdout.write(`     ${dim('·')} Everything runs ${bold('locally')} — no cloud, no servers\n`);
    process.stdout.write(`     ${dim('·')} Your code never leaves your machine. Planu is local-first.\n`);
    process.stdout.write(`     ${dim('·')} No API key required. Your specs stay on your machine.\n`);
    process.stdout.write('\n');
}
// ---------------------------------------------------------------------------
// Welcome message (shown after successful installation)
// ---------------------------------------------------------------------------
function printWelcome() {
    const docsUrl = claudeCodeDocsUrl();
    process.stdout.write(`${bold("🚀 You're all set — welcome to Planu!")}\n\n`);
    process.stdout.write(`   ${dim('How to get started:')}\n\n`);
    process.stdout.write(`   ${bold('1.')} Open your AI tool and describe what you want to build\n` +
        `      ${dim('→')} Planu will create a spec automatically\n\n` +
        `   ${bold('2.')} Review, refine, approve the spec\n` +
        `      ${dim('→')} Then let your AI implement it, validated against the spec\n\n` +
        `   ${bold('3.')} Iterate with confidence\n` +
        `      ${dim('→')} Full audit trail, no hallucinated features, zero tech debt\n\n`);
    process.stdout.write(`   ${dim('Docs:')} ${cyan(docsUrl)}\n`);
    process.stdout.write(`   ${dim('Website:')} ${cyan('https://planu.dev')}\n\n`);
    process.stdout.write(`   ${dim('🔒')} ${dim('Your code never leaves your machine.')} ` +
        `${dim('Planu runs 100% locally — no cloud, no telemetry.')}\n\n`);
    process.stdout.write(`   ${dim('Happy building! ✨')}\n\n`);
}
// ---------------------------------------------------------------------------
// Command implementation helpers (split to reduce run() complexity)
// ---------------------------------------------------------------------------
function printDetectionResults(detected, notFound) {
    for (const tool of detected) {
        process.stdout.write(`  ${green('+')} ${tool.name} ${dim('(detected)')}\n`);
    }
    for (const tool of notFound) {
        process.stdout.write(`  ${dim('-')} ${tool.name} ${dim('(not found)')}\n`);
    }
}
async function resolveScope(scopeArg) {
    if (scopeArg === 'user' || scopeArg === 'project') {
        return scopeArg;
    }
    const choice = await select('Install scope?', ['user (global)', 'project (this directory)']);
    return choice.startsWith('user') ? 'user' : 'project';
}
async function resolveTargets(detected, installAll) {
    if (installAll || detected.length === 1) {
        return detected;
    }
    const toolNames = detected.map((t) => t.name);
    const choice = await select(`Install in all ${String(detected.length)} detected tools, or select one?`, ['All detected tools', ...toolNames]);
    if (choice === 'All detected tools') {
        return detected;
    }
    return detected.filter((t) => t.name === choice);
}
async function installToPath(tool, targetPath, quiet, licenseKey) {
    const entry = buildMcpEntry(licenseKey);
    let existingConfig = {};
    let jsonError = false;
    try {
        if (existsSync(targetPath)) {
            existingConfig = readJsonFile(targetPath);
        }
    }
    catch {
        jsonError = true;
        process.stdout.write(`  ${yellow('!')} ${tool.name}: config file has invalid JSON — will create a new one\n`);
    }
    if (!jsonError && hasPlanuEntry(existingConfig, tool.format)) {
        if (quiet) {
            return false;
        }
        const overwrite = await confirm(`  Planu is already installed in ${tool.name}. Update it?`, false);
        if (!overwrite) {
            process.stdout.write(`  ${dim('Skipped')} ${tool.name}\n`);
            return false;
        }
    }
    try {
        if (tool.format === 'zed') {
            writeZedEntry(targetPath, entry);
        }
        else {
            writeStandardEntry(targetPath, entry);
        }
        process.stdout.write(`  ${green('+')} Installed in ${bold(tool.name)} → ${dim(targetPath)}\n`);
        return true;
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        process.stdout.write(`  ${yellow('!')} ${tool.name}: failed to write — ${msg}\n`);
        return false;
    }
}
// ---------------------------------------------------------------------------
// Command
// ---------------------------------------------------------------------------
export const installCommand = {
    name: 'install',
    description: 'Install Planu MCP in detected AI tools',
    usage: 'planu install [--scope user|project] [--all]',
    async run(args, flags) {
        const quiet = Boolean(flags?.quiet);
        // Parse flags
        let scopeArg;
        let installAll = false;
        for (let i = 0; i < args.length; i++) {
            if (args[i] === '--scope' && args[i + 1]) {
                scopeArg = args[++i];
            }
            else if (args[i] === '--all') {
                installAll = true;
            }
        }
        printBanner();
        // Detect installed tools (user-scope paths)
        const detected = [];
        const notFound = [];
        for (const tool of TOOLS) {
            if (existsSync(tool.userScopePath)) {
                detected.push(tool);
            }
            else {
                notFound.push(tool);
            }
        }
        printDetectionResults(detected, notFound);
        process.stdout.write('\n');
        if (detected.length === 0) {
            const docsUrl = claudeCodeDocsUrl();
            process.stdout.write(`${yellow('⚠  No AI tools detected on this machine.')}\n\n` +
                `   Planu works with Claude Code, Cursor, Windsurf, OpenCode, Zed and more.\n` +
                `   Install at least one to get started:\n\n` +
                `   ${bold('★  Claude Code')}  ${dim('(recommended)')}\n` +
                `      ${dim('→')} https://claude.ai/download\n` +
                `      ${dim('→ npx @anthropic-ai/claude-code@latest')}\n` +
                `      ${dim('→ Docs:')} ${cyan(docsUrl)}\n\n` +
                `   ${bold('Cursor')}       https://cursor.com\n` +
                `   ${bold('Windsurf')}     https://windsurf.com\n` +
                `   ${bold('OpenCode')}     https://opencode.ai\n\n` +
                `   Once installed, run ${cyan('npx @planu/cli install')} again.\n\n`);
            return;
        }
        const scope = await resolveScope(scopeArg);
        const targets = await resolveTargets(detected, installAll);
        // License step — prompt or read from env
        process.stdout.write('\n');
        const license = await promptLicense();
        const licenseKey = license?.key;
        process.stdout.write(`\n${cyan('Installing...')}\n`);
        let installed = 0;
        for (const tool of targets) {
            let targetPath;
            if (scope === 'project') {
                if (!tool.projectScopePath) {
                    process.stdout.write(`  ${dim('Skip')} ${tool.name} — no project-scope config supported\n`);
                    continue;
                }
                targetPath = tool.projectScopePath;
            }
            else {
                targetPath = tool.userScopePath;
            }
            const ok = await installToPath(tool, targetPath, quiet, licenseKey);
            if (ok) {
                installed++;
            }
        }
        process.stdout.write(`\n${bold('Done.')} Installed in ${String(installed)} tool(s).\n`);
        if (installed > 0) {
            process.stdout.write(`${dim('Restart your AI tool to pick up the new MCP server.')}\n\n`);
            printWelcome();
        }
    },
};
//# sourceMappingURL=install.js.map