#!/usr/bin/env node
"use strict";
/**
 * CodeGraph CLI
 *
 * Command-line interface for CodeGraph code intelligence.
 *
 * Usage:
 *   codegraph                    Run interactive installer (when no args)
 *   codegraph install            Run interactive installer
 *   codegraph uninstall          Remove CodeGraph from your agents
 *   codegraph init [path]        Initialize CodeGraph in a project
 *   codegraph uninit [path]      Remove CodeGraph from a project
 *   codegraph index [path]       Index all files in the project
 *   codegraph sync [path]        Sync changes since last index
 *   codegraph status [path]      Show index status
 *   codegraph query <search>     Search for symbols
 *   codegraph files [options]    Show project file structure
 *   codegraph context <task>     Build context for a task
 *   codegraph callers <symbol>   Find what calls a function/method
 *   codegraph callees <symbol>   Find what a function/method calls
 *   codegraph impact <symbol>    Analyze what code is affected by changing a symbol
 *   codegraph affected [files]   Find test files affected by changes
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
const commander_1 = require("commander");
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
const directory_1 = require("../directory");
const worktree_1 = require("../sync/worktree");
const shimmer_progress_1 = require("../ui/shimmer-progress");
const glyphs_1 = require("../ui/glyphs");
const node_version_check_1 = require("./node-version-check");
const wasm_runtime_flags_1 = require("../extraction/wasm-runtime-flags");
// Lazy-load heavy modules (CodeGraph, runInstaller) to keep CLI startup fast.
async function loadCodeGraph() {
    try {
        return await Promise.resolve().then(() => __importStar(require('../index')));
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        console.error(`\x1b[31m${(0, glyphs_1.getGlyphs)().err}\x1b[0m Failed to load CodeGraph modules.`);
        console.error(`\n  Node: ${process.version}  Platform: ${process.platform} ${process.arch}`);
        console.error(`\n  Error: ${msg}`);
        console.error('\n  Try reinstalling with: npm install -g @colbymchenry/codegraph\n');
        process.exit(1);
    }
}
// Dynamic import helper — tsc compiles import() to require() in CJS mode,
// which fails for ESM-only packages. This bypasses the transformation.
// eslint-disable-next-line @typescript-eslint/no-implied-eval
const importESM = new Function('specifier', 'return import(specifier)');
// Block CodeGraph on Node.js 25.x — V8's turboshaft WASM JIT has a Zone
// allocator bug that reliably crashes when compiling tree-sitter
// grammars (see #54, #81, #140). The previous behaviour was a soft
// console.warn that scrolls off-screen before the OOM crash 30 seconds
// later, leading to a steady stream of "what is this OOM" reports.
// Hard-exit before any WASM work; allow override via env var for users
// who patched V8 themselves or want to test a future fix.
const nodeVersion = process.versions.node;
const nodeMajor = parseInt(nodeVersion.split('.')[0] ?? '0', 10);
if (nodeMajor >= 25) {
    process.stderr.write((0, node_version_check_1.buildNode25BlockBanner)(nodeVersion) + '\n');
    if (!process.env.CODEGRAPH_ALLOW_UNSAFE_NODE) {
        process.exit(1);
    }
    // Override active — banner shown for visibility, continuing.
}
// Enforce the supported Node floor. `engines` in package.json only *warns* on
// install (unless engine-strict), so hard-block here to actually keep users off
// unsupported versions. Mirrors the 25+ block above. See package.json `engines`.
if (nodeMajor < node_version_check_1.MIN_NODE_MAJOR) {
    process.stderr.write((0, node_version_check_1.buildNodeTooOldBanner)(nodeVersion) + '\n');
    if (!process.env.CODEGRAPH_ALLOW_UNSAFE_NODE) {
        process.exit(1);
    }
    // Override active — banner shown for visibility, continuing.
}
// Re-exec with V8's `--liftoff-only` if it isn't already set, so tree-sitter's
// large WASM grammars never hit the turboshaft Zone OOM (`Fatal process out of
// memory: Zone`) on Node >= 22. No-op under the bundled launcher, which already
// passes the flag. Must run before any grammar (in the parse worker, which
// inherits this process's flags) is compiled. See ../extraction/wasm-runtime-flags.
(0, wasm_runtime_flags_1.relaunchWithWasmRuntimeFlagsIfNeeded)(__filename);
// Check if running with no arguments - run installer
if (process.argv.length === 2) {
    Promise.resolve().then(() => __importStar(require('../installer'))).then(({ runInstaller }) => runInstaller()).catch((err) => {
        console.error('Installation failed:', err instanceof Error ? err.message : String(err));
        process.exit(1);
    });
}
else {
    // Normal CLI flow
    main();
}
process.on('uncaughtException', (error) => {
    console.error('[CodeGraph] Uncaught exception:', error);
});
process.on('unhandledRejection', (reason) => {
    console.error('[CodeGraph] Unhandled rejection:', reason);
});
function main() {
    const program = new commander_1.Command();
    // Version from package.json
    const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf-8'));
    // =============================================================================
    // ANSI Color Helpers (avoid chalk ESM issues)
    // =============================================================================
    const colors = {
        reset: '\x1b[0m',
        bold: '\x1b[1m',
        dim: '\x1b[2m',
        red: '\x1b[31m',
        green: '\x1b[32m',
        yellow: '\x1b[33m',
        blue: '\x1b[34m',
        cyan: '\x1b[36m',
        white: '\x1b[37m',
        gray: '\x1b[90m',
    };
    const chalk = {
        bold: (s) => `${colors.bold}${s}${colors.reset}`,
        dim: (s) => `${colors.dim}${s}${colors.reset}`,
        red: (s) => `${colors.red}${s}${colors.reset}`,
        green: (s) => `${colors.green}${s}${colors.reset}`,
        yellow: (s) => `${colors.yellow}${s}${colors.reset}`,
        blue: (s) => `${colors.blue}${s}${colors.reset}`,
        cyan: (s) => `${colors.cyan}${s}${colors.reset}`,
        white: (s) => `${colors.white}${s}${colors.reset}`,
        gray: (s) => `${colors.gray}${s}${colors.reset}`,
    };
    program
        .name('codegraph')
        .description('Code intelligence and knowledge graph for any codebase')
        .version(packageJson.version);
    // =============================================================================
    // Helper Functions
    // =============================================================================
    /**
     * Resolve project path from argument or current directory
     * Walks up parent directories to find nearest initialized CodeGraph project
     * (must have .codegraph/codegraph.db, not just .codegraph/lessons.db)
     */
    function resolveProjectPath(pathArg) {
        const absolutePath = path.resolve(pathArg || process.cwd());
        // If exact path is initialized (has codegraph.db), use it
        if ((0, directory_1.isInitialized)(absolutePath)) {
            return absolutePath;
        }
        // Walk up to find nearest parent with CodeGraph initialized
        // Note: findNearestCodeGraphRoot finds any .codegraph folder, but we need one with codegraph.db
        let current = absolutePath;
        const root = path.parse(current).root;
        while (current !== root) {
            const parent = path.dirname(current);
            if (parent === current)
                break;
            current = parent;
            if ((0, directory_1.isInitialized)(current)) {
                return current;
            }
        }
        // Not found - return original path (will fail later with helpful error)
        return absolutePath;
    }
    /**
     * Format a number with commas
     */
    function formatNumber(n) {
        return n.toLocaleString();
    }
    /**
     * Format duration in milliseconds to human readable
     */
    function formatDuration(ms) {
        if (ms < 1000) {
            return `${ms}ms`;
        }
        const seconds = ms / 1000;
        if (seconds < 60) {
            return `${seconds.toFixed(1)}s`;
        }
        const minutes = Math.floor(seconds / 60);
        const remainingSeconds = seconds % 60;
        return `${minutes}m ${remainingSeconds.toFixed(0)}s`;
    }
    // Shimmer progress renderer (runs in a worker thread for smooth animation)
    // Imported at top of file from '../ui/shimmer-progress'
    /**
     * Create a plain-text progress callback for --verbose mode.
     * No animations, no ANSI tricks — just timestamped lines to stdout.
     */
    function createVerboseProgress() {
        let lastPhase = '';
        let lastPct = -1;
        const startTime = Date.now();
        return (progress) => {
            const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
            if (progress.phase !== lastPhase) {
                lastPhase = progress.phase;
                lastPct = -1;
                console.log(`[${elapsed}s] Phase: ${progress.phase}`);
            }
            if (progress.total > 0) {
                const pct = Math.floor((progress.current / progress.total) * 100);
                // Log every 5% to keep output manageable
                if (pct >= lastPct + 5 || progress.current === progress.total) {
                    lastPct = pct;
                    console.log(`[${elapsed}s]   ${progress.current}/${progress.total} (${pct}%)${progress.currentFile ? ` ${(0, glyphs_1.getGlyphs)().dash} ${progress.currentFile}` : ''}`);
                }
            }
            else if (progress.current > 0) {
                // Scanning phase (no total yet) — log periodically
                if (progress.current % 1000 === 0 || progress.current === 1) {
                    console.log(`[${elapsed}s]   ${formatNumber(progress.current)} files found`);
                }
            }
        };
    }
    /**
     * Print success message
     */
    function success(message) {
        console.log(chalk.green((0, glyphs_1.getGlyphs)().ok) + ' ' + message);
    }
    /**
     * Print error message
     */
    function error(message) {
        console.error(chalk.red((0, glyphs_1.getGlyphs)().err) + ' ' + message);
    }
    /**
     * Print info message
     */
    function info(message) {
        console.log(chalk.blue((0, glyphs_1.getGlyphs)().info) + ' ' + message);
    }
    /**
     * Print warning message
     */
    function warn(message) {
        console.log(chalk.yellow((0, glyphs_1.getGlyphs)().warn) + ' ' + message);
    }
    /**
     * Print indexing results using clack log methods
     */
    function printIndexResult(clack, result, projectPath) {
        const hasErrors = result.filesErrored > 0;
        // Surface non-file-level failures (e.g. lock-acquisition failure
        // when another indexer is running) before the file-count branches.
        // Without this the CLI falls through to "No files found to index",
        // which is actively misleading — the index DID run, it just couldn't
        // get the lock.
        //
        // If success is false but no severity:'error' entry exists in
        // `result.errors` (degenerate case — shouldn't happen in practice
        // but worth guarding because the result shape is plumbed through
        // multiple call sites), fall back to a generic message rather than
        // continuing to the misleading "No files found" branch or throwing.
        if (!result.success && !hasErrors && result.filesIndexed === 0) {
            const generic = result.errors.find((e) => e.severity === 'error');
            clack.log.error(generic?.message ?? `Indexing failed ${(0, glyphs_1.getGlyphs)().dash} no further details available`);
            return;
        }
        if (result.filesIndexed > 0) {
            if (hasErrors) {
                clack.log.success(`Indexed ${formatNumber(result.filesIndexed)} files (${formatNumber(result.filesErrored)} could not be parsed)`);
            }
            else {
                clack.log.success(`Indexed ${formatNumber(result.filesIndexed)} files`);
            }
            clack.log.info(`${formatNumber(result.nodesCreated)} nodes, ${formatNumber(result.edgesCreated)} edges in ${formatDuration(result.durationMs)}`);
        }
        else if (hasErrors) {
            clack.log.error(`Indexing failed ${(0, glyphs_1.getGlyphs)().dash} all ${formatNumber(result.filesErrored)} files had errors`);
        }
        else {
            clack.log.warn('No files found to index');
        }
        if (hasErrors) {
            const errorsByCode = new Map();
            for (const err of result.errors) {
                if (err.severity === 'error') {
                    const code = err.code || 'unknown';
                    errorsByCode.set(code, (errorsByCode.get(code) || 0) + 1);
                }
            }
            const codeLabels = {
                parse_error: 'files failed to parse',
                read_error: 'files could not be read',
                size_exceeded: 'files exceeded size limit',
                path_traversal: 'blocked paths',
                unsupported_language: 'unsupported language',
                parser_error: 'parser initialization failures',
            };
            const breakdown = Array.from(errorsByCode)
                .map(([code, count]) => `${formatNumber(count)} ${codeLabels[code] || code}`)
                .join('\n');
            clack.note(breakdown, 'Error breakdown');
            if (projectPath) {
                writeErrorLog(projectPath, result.errors);
                clack.log.info('See .codegraph/errors.log for details');
            }
            if (result.filesIndexed > 0) {
                clack.log.info(`The index is fully usable ${(0, glyphs_1.getGlyphs)().dash} only the failed files are missing.`);
            }
        }
        else if (projectPath) {
            const logPath = path.join(projectPath, '.codegraph', 'errors.log');
            if (fs.existsSync(logPath)) {
                fs.unlinkSync(logPath);
            }
        }
    }
    /**
     * Write detailed error log to .codegraph/errors.log
     */
    function writeErrorLog(projectPath, errors) {
        const cgDir = path.join(projectPath, '.codegraph');
        if (!fs.existsSync(cgDir))
            return;
        const logPath = path.join(cgDir, 'errors.log');
        // Group errors by file path
        const errorsByFile = new Map();
        const noFileErrors = [];
        for (const err of errors) {
            if (err.severity !== 'error')
                continue;
            if (err.filePath) {
                let list = errorsByFile.get(err.filePath);
                if (!list) {
                    list = [];
                    errorsByFile.set(err.filePath, list);
                }
                list.push({ message: err.message, code: err.code });
            }
            else {
                noFileErrors.push({ message: err.message, code: err.code });
            }
        }
        const lines = [
            `CodeGraph Error Log - ${new Date().toISOString()}`,
            `${errorsByFile.size} files with errors`,
            '',
        ];
        for (const [filePath, fileErrors] of errorsByFile) {
            for (const err of fileErrors) {
                lines.push(`${filePath}: ${err.message}`);
            }
        }
        for (const err of noFileErrors) {
            lines.push(err.message);
        }
        fs.writeFileSync(logPath, lines.join('\n') + '\n');
    }
    // =============================================================================
    // Commands
    // =============================================================================
    /**
     * codegraph init [path]
     */
    program
        .command('init [path]')
        .description('Initialize CodeGraph in a project directory and build the initial index')
        .option('-i, --index', 'Deprecated: indexing now runs by default; flag accepted for backward compatibility')
        .option('-v, --verbose', 'Show detailed worker lifecycle and memory info')
        .action(async (pathArg, options) => {
        const projectPath = path.resolve(pathArg || process.cwd());
        const clack = await importESM('@clack/prompts');
        clack.intro('Initializing CodeGraph');
        try {
            if ((0, directory_1.isInitialized)(projectPath)) {
                clack.log.warn(`Already initialized in ${projectPath}`);
                clack.log.info('Use "codegraph index" to re-index or "codegraph sync" to update');
                try {
                    const { offerWatchFallback } = await Promise.resolve().then(() => __importStar(require('../installer')));
                    await offerWatchFallback(clack, projectPath);
                }
                catch { /* non-fatal */ }
                clack.outro('');
                return;
            }
            const { default: CodeGraph } = await loadCodeGraph();
            const cg = await CodeGraph.init(projectPath, { index: false });
            clack.log.success(`Initialized in ${projectPath}`);
            // Indexing runs by default now. The legacy -i/--index flag is still
            // accepted (so existing muscle memory and scripts don't break) but is a
            // no-op — initializing always builds the initial index.
            let result;
            if (options.verbose) {
                result = await cg.indexAll({
                    onProgress: createVerboseProgress(),
                    verbose: true,
                });
            }
            else {
                process.stdout.write(`${colors.dim}${(0, glyphs_1.getGlyphs)().rail}${colors.reset}\n`);
                const progress = (0, shimmer_progress_1.createShimmerProgress)();
                result = await cg.indexAll({
                    onProgress: progress.onProgress,
                });
                await progress.stop();
            }
            printIndexResult(clack, result, projectPath);
            try {
                const { offerWatchFallback } = await Promise.resolve().then(() => __importStar(require('../installer')));
                await offerWatchFallback(clack, projectPath);
            }
            catch { /* non-fatal */ }
            clack.outro('Done');
            cg.destroy();
        }
        catch (err) {
            clack.log.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
            process.exit(1);
        }
    });
    /**
     * codegraph uninit [path]
     */
    program
        .command('uninit [path]')
        .description('Remove CodeGraph from a project (deletes .codegraph/ directory)')
        .option('-f, --force', 'Skip confirmation prompt')
        .action(async (pathArg, options) => {
        const projectPath = resolveProjectPath(pathArg);
        try {
            if (!(0, directory_1.isInitialized)(projectPath)) {
                warn(`CodeGraph is not initialized in ${projectPath}`);
                return;
            }
            if (!options.force) {
                // Confirm with user
                const readline = await Promise.resolve().then(() => __importStar(require('readline')));
                const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
                const answer = await new Promise((resolve) => {
                    rl.question(chalk.yellow(`${(0, glyphs_1.getGlyphs)().warn} This will permanently delete all CodeGraph data. Continue? (y/N) `), resolve);
                });
                rl.close();
                if (answer.toLowerCase() !== 'y') {
                    info('Cancelled');
                    return;
                }
            }
            const { default: CodeGraph } = await loadCodeGraph();
            const cg = CodeGraph.openSync(projectPath);
            cg.uninitialize();
            // Clean up any git sync hooks we installed (no-op if none / not a repo).
            try {
                const { removeGitSyncHook } = await Promise.resolve().then(() => __importStar(require('../sync/git-hooks')));
                const removed = removeGitSyncHook(projectPath);
                if (removed.installed.length > 0) {
                    info(`Removed git ${removed.installed.join(', ')} sync hook${removed.installed.length > 1 ? 's' : ''}`);
                }
            }
            catch { /* non-fatal */ }
            success(`Removed CodeGraph from ${projectPath}`);
        }
        catch (err) {
            error(`Failed to uninitialize: ${err instanceof Error ? err.message : String(err)}`);
            process.exit(1);
        }
    });
    /**
     * codegraph index [path]
     */
    program
        .command('index [path]')
        .description('Index all files in the project')
        .option('-f, --force', 'Force full re-index even if already indexed')
        .option('-q, --quiet', 'Suppress progress output')
        .option('-v, --verbose', 'Show detailed worker lifecycle and memory info')
        .action(async (pathArg, options) => {
        const projectPath = resolveProjectPath(pathArg);
        try {
            if (!(0, directory_1.isInitialized)(projectPath)) {
                error(`CodeGraph not initialized in ${projectPath}`);
                info('Run "codegraph init" first');
                process.exit(1);
            }
            const { default: CodeGraph } = await loadCodeGraph();
            const cg = await CodeGraph.open(projectPath);
            if (options.quiet) {
                // Quiet mode: no UI, just run
                if (options.force)
                    cg.clear();
                const result = await cg.indexAll();
                if (!result.success)
                    process.exit(1);
                cg.destroy();
                return;
            }
            const clack = await importESM('@clack/prompts');
            clack.intro('Indexing project');
            if (options.force) {
                cg.clear();
                clack.log.info('Cleared existing index');
            }
            let result;
            if (options.verbose) {
                result = await cg.indexAll({
                    onProgress: createVerboseProgress(),
                    verbose: true,
                });
            }
            else {
                process.stdout.write(`${colors.dim}${(0, glyphs_1.getGlyphs)().rail}${colors.reset}\n`);
                const progress = (0, shimmer_progress_1.createShimmerProgress)();
                result = await cg.indexAll({
                    onProgress: progress.onProgress,
                });
                await progress.stop();
            }
            printIndexResult(clack, result, projectPath);
            if (!result.success) {
                process.exit(1);
            }
            clack.outro('Done');
            cg.destroy();
        }
        catch (err) {
            error(`Failed to index: ${err instanceof Error ? err.message : String(err)}`);
            process.exit(1);
        }
    });
    /**
     * codegraph sync [path]
     */
    program
        .command('sync [path]')
        .description('Sync changes since last index')
        .option('-q, --quiet', 'Suppress output (for git hooks)')
        .action(async (pathArg, options) => {
        const projectPath = resolveProjectPath(pathArg);
        try {
            if (!(0, directory_1.isInitialized)(projectPath)) {
                if (!options.quiet) {
                    error(`CodeGraph not initialized in ${projectPath}`);
                }
                process.exit(1);
            }
            const { default: CodeGraph } = await loadCodeGraph();
            const cg = await CodeGraph.open(projectPath);
            if (options.quiet) {
                await cg.sync();
                cg.destroy();
                return;
            }
            const clack = await importESM('@clack/prompts');
            clack.intro('Syncing CodeGraph');
            process.stdout.write(`${colors.dim}${(0, glyphs_1.getGlyphs)().rail}${colors.reset}\n`);
            const progress = (0, shimmer_progress_1.createShimmerProgress)();
            const result = await cg.sync({
                onProgress: progress.onProgress,
            });
            await progress.stop();
            const totalChanges = result.filesAdded + result.filesModified + result.filesRemoved;
            if (totalChanges === 0) {
                clack.log.info('Already up to date');
            }
            else {
                clack.log.success(`Synced ${formatNumber(totalChanges)} changed files`);
                const details = [];
                if (result.filesAdded > 0)
                    details.push(`Added: ${result.filesAdded}`);
                if (result.filesModified > 0)
                    details.push(`Modified: ${result.filesModified}`);
                if (result.filesRemoved > 0)
                    details.push(`Removed: ${result.filesRemoved}`);
                clack.log.info(`${details.join(', ')} ${(0, glyphs_1.getGlyphs)().dash} ${formatNumber(result.nodesUpdated)} nodes in ${formatDuration(result.durationMs)}`);
            }
            clack.outro('Done');
            cg.destroy();
        }
        catch (err) {
            if (!options.quiet) {
                error(`Failed to sync: ${err instanceof Error ? err.message : String(err)}`);
            }
            process.exit(1);
        }
    });
    /**
     * codegraph status [path]
     */
    program
        .command('status [path]')
        .description('Show index status and statistics')
        .option('-j, --json', 'Output as JSON')
        .action(async (pathArg, options) => {
        const projectPath = resolveProjectPath(pathArg);
        // The directory the user actually ran from, before walking up to the index
        // root. Used to detect when the resolved index lives in a different git
        // working tree (e.g. a nested worktree borrowing the main checkout's index).
        const startPath = path.resolve(pathArg || process.cwd());
        const worktreeMismatch = (0, worktree_1.detectWorktreeIndexMismatch)(startPath, projectPath);
        try {
            if (!(0, directory_1.isInitialized)(projectPath)) {
                if (options.json) {
                    console.log(JSON.stringify({ initialized: false, projectPath }));
                    return;
                }
                console.log(chalk.bold('\nCodeGraph Status\n'));
                info(`Project: ${projectPath}`);
                warn('Not initialized');
                info('Run "codegraph init" to initialize');
                return;
            }
            const { default: CodeGraph } = await loadCodeGraph();
            const cg = await CodeGraph.open(projectPath);
            const stats = cg.getStats();
            const changes = cg.getChangedFiles();
            const backend = cg.getBackend();
            const journalMode = cg.getJournalMode();
            // JSON output mode
            if (options.json) {
                console.log(JSON.stringify({
                    initialized: true,
                    projectPath,
                    fileCount: stats.fileCount,
                    nodeCount: stats.nodeCount,
                    edgeCount: stats.edgeCount,
                    dbSizeBytes: stats.dbSizeBytes,
                    backend,
                    journalMode,
                    nodesByKind: stats.nodesByKind,
                    languages: Object.entries(stats.filesByLanguage).filter(([, count]) => count > 0).map(([lang]) => lang),
                    pendingChanges: {
                        added: changes.added.length,
                        modified: changes.modified.length,
                        removed: changes.removed.length,
                    },
                    worktreeMismatch: worktreeMismatch
                        ? { worktreeRoot: worktreeMismatch.worktreeRoot, indexRoot: worktreeMismatch.indexRoot }
                        : null,
                }));
                cg.destroy();
                return;
            }
            console.log(chalk.bold('\nCodeGraph Status\n'));
            // Project info
            console.log(chalk.cyan('Project:'), projectPath);
            if (worktreeMismatch) {
                warn((0, worktree_1.worktreeMismatchWarning)(worktreeMismatch));
            }
            console.log();
            // Index stats
            console.log(chalk.bold('Index Statistics:'));
            console.log(`  Files:     ${formatNumber(stats.fileCount)}`);
            console.log(`  Nodes:     ${formatNumber(stats.nodeCount)}`);
            console.log(`  Edges:     ${formatNumber(stats.edgeCount)}`);
            console.log(`  DB Size:   ${(stats.dbSizeBytes / 1024 / 1024).toFixed(2)} MB`);
            // Surface the active SQLite backend (node:sqlite — Node's built-in real
            // SQLite, full WAL + FTS5, no native build).
            const backendLabel = chalk.green(`node:sqlite ${(0, glyphs_1.getGlyphs)().dash} built-in (full WAL)`);
            console.log(`  Backend:   ${backendLabel}`);
            // Effective journal mode: 'wal' means concurrent reads never block on a
            // writer; anything else means they can ("database is locked"). node:sqlite
            // supports WAL everywhere, so a non-wal mode means the filesystem can't
            // (network mounts, WSL2 /mnt). See issue #238.
            const journalLabel = journalMode === 'wal'
                ? chalk.green('wal')
                : chalk.yellow(`${journalMode || 'unknown'} ${(0, glyphs_1.getGlyphs)().dash} WAL inactive; reads can block on writes`);
            console.log(`  Journal:   ${journalLabel}`);
            console.log();
            // Node breakdown
            console.log(chalk.bold('Nodes by Kind:'));
            const nodesByKind = Object.entries(stats.nodesByKind)
                .filter(([, count]) => count > 0)
                .sort((a, b) => b[1] - a[1]);
            for (const [kind, count] of nodesByKind) {
                console.log(`  ${kind.padEnd(15)} ${formatNumber(count)}`);
            }
            console.log();
            // Language breakdown
            console.log(chalk.bold('Files by Language:'));
            const filesByLang = Object.entries(stats.filesByLanguage)
                .filter(([, count]) => count > 0)
                .sort((a, b) => b[1] - a[1]);
            for (const [lang, count] of filesByLang) {
                console.log(`  ${lang.padEnd(15)} ${formatNumber(count)}`);
            }
            console.log();
            // Pending changes
            const totalChanges = changes.added.length + changes.modified.length + changes.removed.length;
            if (totalChanges > 0) {
                console.log(chalk.bold('Pending Changes:'));
                if (changes.added.length > 0) {
                    console.log(`  Added:     ${changes.added.length} files`);
                }
                if (changes.modified.length > 0) {
                    console.log(`  Modified:  ${changes.modified.length} files`);
                }
                if (changes.removed.length > 0) {
                    console.log(`  Removed:   ${changes.removed.length} files`);
                }
                info('Run "codegraph sync" to update the index');
            }
            else {
                success('Index is up to date');
            }
            console.log();
            cg.destroy();
        }
        catch (err) {
            error(`Failed to get status: ${err instanceof Error ? err.message : String(err)}`);
            process.exit(1);
        }
    });
    /**
     * codegraph query <search>
     */
    program
        .command('query <search>')
        .description('Search for symbols in the codebase')
        .option('-p, --path <path>', 'Project path')
        .option('-l, --limit <number>', 'Maximum results', '10')
        .option('-k, --kind <kind>', 'Filter by node kind (function, class, etc.)')
        .option('-j, --json', 'Output as JSON')
        .action(async (search, options) => {
        const projectPath = resolveProjectPath(options.path);
        try {
            if (!(0, directory_1.isInitialized)(projectPath)) {
                error(`CodeGraph not initialized in ${projectPath}`);
                process.exit(1);
            }
            const { default: CodeGraph } = await loadCodeGraph();
            const cg = await CodeGraph.open(projectPath);
            const limit = parseInt(options.limit || '10', 10);
            const rawResults = cg.searchNodes(search, {
                limit,
                kinds: options.kind ? [options.kind] : undefined,
            });
            // Mirror the MCP search down-rank so the CLI also surfaces the
            // hand-written implementation before protobuf/gRPC scaffolding
            // when both share a name. See extraction/generated-detection.ts.
            const { isGeneratedFile } = await Promise.resolve().then(() => __importStar(require('../extraction/generated-detection')));
            const results = [...rawResults].sort((a, b) => {
                const aGen = isGeneratedFile(a.node.filePath) ? 1 : 0;
                const bGen = isGeneratedFile(b.node.filePath) ? 1 : 0;
                return aGen - bGen;
            });
            if (options.json) {
                console.log(JSON.stringify(results, null, 2));
            }
            else {
                if (results.length === 0) {
                    info(`No results found for "${search}"`);
                }
                else {
                    console.log(chalk.bold(`\nSearch Results for "${search}":\n`));
                    for (const result of results) {
                        const node = result.node;
                        const location = `${node.filePath}:${node.startLine}`;
                        const score = chalk.dim(`(${(result.score * 100).toFixed(0)}%)`);
                        console.log(chalk.cyan(node.kind.padEnd(12)) +
                            chalk.white(node.name) +
                            ' ' + score);
                        console.log(chalk.dim(`  ${location}`));
                        if (node.signature) {
                            console.log(chalk.dim(`  ${node.signature}`));
                        }
                        console.log();
                    }
                }
            }
            cg.destroy();
        }
        catch (err) {
            error(`Search failed: ${err instanceof Error ? err.message : String(err)}`);
            process.exit(1);
        }
    });
    /**
     * codegraph files [path]
     */
    program
        .command('files')
        .description('Show project file structure from the index')
        .option('-p, --path <path>', 'Project path')
        .option('--filter <dir>', 'Filter to files under this directory')
        .option('--pattern <glob>', 'Filter files matching this glob pattern')
        .option('--format <format>', 'Output format (tree, flat, grouped)', 'tree')
        .option('--max-depth <number>', 'Maximum directory depth for tree format')
        .option('--no-metadata', 'Hide file metadata (language, symbol count)')
        .option('-j, --json', 'Output as JSON')
        .action(async (options) => {
        const projectPath = resolveProjectPath(options.path);
        try {
            if (!(0, directory_1.isInitialized)(projectPath)) {
                error(`CodeGraph not initialized in ${projectPath}`);
                process.exit(1);
            }
            const { default: CodeGraph } = await loadCodeGraph();
            const cg = await CodeGraph.open(projectPath);
            let files = cg.getFiles();
            if (files.length === 0) {
                info('No files indexed. Run "codegraph index" first.');
                cg.destroy();
                return;
            }
            // Filter by path prefix
            if (options.filter) {
                const filter = options.filter;
                files = files.filter(f => f.path.startsWith(filter) || f.path.startsWith('./' + filter));
            }
            // Filter by glob pattern
            if (options.pattern) {
                const regex = globToRegex(options.pattern);
                files = files.filter(f => regex.test(f.path));
            }
            if (files.length === 0) {
                info('No files found matching the criteria.');
                cg.destroy();
                return;
            }
            // JSON output
            if (options.json) {
                const output = files.map(f => ({
                    path: f.path,
                    language: f.language,
                    nodeCount: f.nodeCount,
                    size: f.size,
                }));
                console.log(JSON.stringify(output, null, 2));
                cg.destroy();
                return;
            }
            const includeMetadata = options.metadata !== false;
            const format = options.format || 'tree';
            const maxDepth = options.maxDepth ? parseInt(options.maxDepth, 10) : undefined;
            // Format output
            switch (format) {
                case 'flat':
                    console.log(chalk.bold(`\nFiles (${files.length}):\n`));
                    for (const file of files.sort((a, b) => a.path.localeCompare(b.path))) {
                        if (includeMetadata) {
                            console.log(`  ${file.path} ${chalk.dim(`(${file.language}, ${file.nodeCount} symbols)`)}`);
                        }
                        else {
                            console.log(`  ${file.path}`);
                        }
                    }
                    break;
                case 'grouped':
                    console.log(chalk.bold(`\nFiles by Language (${files.length} total):\n`));
                    const byLang = new Map();
                    for (const file of files) {
                        const existing = byLang.get(file.language) || [];
                        existing.push(file);
                        byLang.set(file.language, existing);
                    }
                    const sortedLangs = [...byLang.entries()].sort((a, b) => b[1].length - a[1].length);
                    for (const [lang, langFiles] of sortedLangs) {
                        console.log(chalk.cyan(`${lang} (${langFiles.length}):`));
                        for (const file of langFiles.sort((a, b) => a.path.localeCompare(b.path))) {
                            if (includeMetadata) {
                                console.log(`  ${file.path} ${chalk.dim(`(${file.nodeCount} symbols)`)}`);
                            }
                            else {
                                console.log(`  ${file.path}`);
                            }
                        }
                        console.log();
                    }
                    break;
                case 'tree':
                default:
                    console.log(chalk.bold(`\nProject Structure (${files.length} files):\n`));
                    printFileTree(files, includeMetadata, maxDepth, chalk);
                    break;
            }
            console.log();
            cg.destroy();
        }
        catch (err) {
            error(`Failed to list files: ${err instanceof Error ? err.message : String(err)}`);
            process.exit(1);
        }
    });
    /**
     * Convert glob pattern to regex
     */
    function globToRegex(pattern) {
        const escaped = pattern
            .replace(/[.+^${}()|[\]\\]/g, '\\$&')
            .replace(/\*\*/g, '{{GLOBSTAR}}')
            .replace(/\*/g, '[^/]*')
            .replace(/\?/g, '[^/]')
            .replace(/\{\{GLOBSTAR\}\}/g, '.*');
        return new RegExp(escaped);
    }
    /**
     * Print files as a tree
     */
    function printFileTree(files, includeMetadata, maxDepth, chalk) {
        const root = { name: '', children: new Map() };
        for (const file of files) {
            const parts = file.path.split('/');
            let current = root;
            for (let i = 0; i < parts.length; i++) {
                const part = parts[i];
                if (!part)
                    continue;
                if (!current.children.has(part)) {
                    current.children.set(part, { name: part, children: new Map() });
                }
                current = current.children.get(part);
                if (i === parts.length - 1) {
                    current.file = { language: file.language, nodeCount: file.nodeCount };
                }
            }
        }
        const renderNode = (node, prefix, isLast, depth) => {
            if (maxDepth !== undefined && depth > maxDepth)
                return;
            const glyphs = (0, glyphs_1.getGlyphs)();
            const connector = isLast ? glyphs.treeLast : glyphs.treeBranch;
            const childPrefix = isLast ? '    ' : glyphs.treePipe;
            if (node.name) {
                let line = prefix + connector + node.name;
                if (node.file && includeMetadata) {
                    line += chalk.dim(` (${node.file.language}, ${node.file.nodeCount} symbols)`);
                }
                console.log(line);
            }
            const children = [...node.children.values()];
            children.sort((a, b) => {
                const aIsDir = a.children.size > 0 && !a.file;
                const bIsDir = b.children.size > 0 && !b.file;
                if (aIsDir !== bIsDir)
                    return aIsDir ? -1 : 1;
                return a.name.localeCompare(b.name);
            });
            for (let i = 0; i < children.length; i++) {
                const child = children[i];
                const nextPrefix = node.name ? prefix + childPrefix : prefix;
                renderNode(child, nextPrefix, i === children.length - 1, depth + 1);
            }
        };
        renderNode(root, '', true, 0);
    }
    /**
     * codegraph context <task>
     */
    program
        .command('context <task>')
        .description('Build context for a task (outputs markdown)')
        .option('-p, --path <path>', 'Project path')
        .option('-n, --max-nodes <number>', 'Maximum nodes to include', '50')
        .option('-c, --max-code <number>', 'Maximum code blocks', '10')
        .option('--no-code', 'Exclude code blocks')
        .option('-f, --format <format>', 'Output format (markdown, json)', 'markdown')
        .action(async (task, options) => {
        const projectPath = resolveProjectPath(options.path);
        try {
            if (!(0, directory_1.isInitialized)(projectPath)) {
                error(`CodeGraph not initialized in ${projectPath}`);
                process.exit(1);
            }
            const { default: CodeGraph } = await loadCodeGraph();
            const cg = await CodeGraph.open(projectPath);
            const context = await cg.buildContext(task, {
                maxNodes: parseInt(options.maxNodes || '50', 10),
                maxCodeBlocks: parseInt(options.maxCode || '10', 10),
                includeCode: options.code !== false,
                format: options.format,
            });
            // Output the context
            console.log(context);
            cg.destroy();
        }
        catch (err) {
            error(`Failed to build context: ${err instanceof Error ? err.message : String(err)}`);
            process.exit(1);
        }
    });
    /**
     * codegraph serve
     */
    program
        .command('serve')
        .description('Start CodeGraph as an MCP server for AI assistants')
        .option('-p, --path <path>', 'Project path (optional for MCP mode, uses rootUri from client)')
        .option('--mcp', 'Run as MCP server (stdio transport)')
        .option('--no-watch', 'Disable the file watcher (no auto-sync; useful on slow filesystems like WSL2 /mnt drives)')
        .action(async (options) => {
        const projectPath = options.path ? resolveProjectPath(options.path) : undefined;
        // Commander sets watch=false when --no-watch is passed. Route it through
        // the same env-var chokepoint the watcher and MCP server already honor.
        if (options.watch === false) {
            process.env.CODEGRAPH_NO_WATCH = '1';
        }
        try {
            if (options.mcp) {
                // Start MCP server - it handles initialization lazily based on rootUri from client
                const { MCPServer } = await Promise.resolve().then(() => __importStar(require('../mcp/index')));
                const server = new MCPServer(projectPath);
                await server.start();
                // Server will run until terminated
            }
            else {
                // Default: show info about MCP mode.
                // Use stderr so stdout stays clean for any piped/stdio usage.
                console.error(chalk.bold('\nCodeGraph MCP Server\n'));
                console.error(chalk.blue((0, glyphs_1.getGlyphs)().info) + ' Use --mcp flag to start the MCP server');
                console.error('\nTo use with Claude Code, add to your MCP configuration:');
                console.error(chalk.dim(`
{
  "mcpServers": {
    "codegraph": {
      "command": "codegraph",
      "args": ["serve", "--mcp"]
    }
  }
}
`));
                console.error('Available tools:');
                console.error(chalk.cyan('  codegraph_search') + '    - Search for code symbols');
                console.error(chalk.cyan('  codegraph_context') + '   - Build context for a task');
                console.error(chalk.cyan('  codegraph_callers') + '   - Find callers of a symbol');
                console.error(chalk.cyan('  codegraph_callees') + '   - Find what a symbol calls');
                console.error(chalk.cyan('  codegraph_impact') + '    - Analyze impact of changes');
                console.error(chalk.cyan('  codegraph_node') + '      - Get symbol details');
                console.error(chalk.cyan('  codegraph_files') + '     - Get project file structure');
                console.error(chalk.cyan('  codegraph_status') + '    - Get index status');
            }
        }
        catch (err) {
            error(`Failed to start server: ${err instanceof Error ? err.message : String(err)}`);
            process.exit(1);
        }
    });
    /**
     * codegraph unlock [path]
     */
    program
        .command('unlock [path]')
        .description('Remove a stale lock file that is blocking indexing')
        .action(async (pathArg) => {
        const projectPath = resolveProjectPath(pathArg);
        try {
            if (!(0, directory_1.isInitialized)(projectPath)) {
                error(`CodeGraph not initialized in ${projectPath}`);
                return;
            }
            const lockPath = path.join((0, directory_1.getCodeGraphDir)(projectPath), 'codegraph.lock');
            if (!fs.existsSync(lockPath)) {
                info(`No lock file found ${(0, glyphs_1.getGlyphs)().dash} nothing to do`);
                return;
            }
            fs.unlinkSync(lockPath);
            success('Removed lock file. You can now run indexing again.');
        }
        catch (err) {
            error(`Failed to remove lock: ${err instanceof Error ? err.message : String(err)}`);
            process.exit(1);
        }
    });
    /**
     * codegraph callers <symbol>
     *
     * CLI parity with the MCP graph tools (codegraph_callers/callees/impact) so the
     * traversal queries work in scripts, CI, and git hooks without a running MCP
     * server.
     */
    program
        .command('callers <symbol>')
        .description('Find all functions/methods that call a specific symbol')
        .option('-p, --path <path>', 'Project path')
        .option('-l, --limit <number>', 'Maximum results', '20')
        .option('-j, --json', 'Output as JSON')
        .action(async (symbol, options) => {
        const projectPath = resolveProjectPath(options.path);
        try {
            if (!(0, directory_1.isInitialized)(projectPath)) {
                error(`CodeGraph not initialized in ${projectPath}`);
                process.exit(1);
            }
            const { default: CodeGraph } = await loadCodeGraph();
            const cg = await CodeGraph.open(projectPath);
            const limit = parseInt(options.limit || '20', 10);
            const matches = cg.searchNodes(symbol, { limit: 50 });
            if (matches.length === 0) {
                info(`Symbol "${symbol}" not found`);
                cg.destroy();
                return;
            }
            const seen = new Set();
            const allCallers = [];
            for (const match of matches) {
                const exactMatch = match.node.name === symbol || match.node.name.endsWith(`.${symbol}`) || match.node.name.endsWith(`::${symbol}`);
                if (!exactMatch && matches.length > 1)
                    continue;
                for (const c of cg.getCallers(match.node.id)) {
                    if (!seen.has(c.node.id)) {
                        seen.add(c.node.id);
                        allCallers.push({ name: c.node.name, kind: c.node.kind, filePath: c.node.filePath, startLine: c.node.startLine });
                    }
                }
            }
            // Fallback: if exact filter removed everything, use the top match
            if (allCallers.length === 0 && matches[0]) {
                for (const c of cg.getCallers(matches[0].node.id)) {
                    if (!seen.has(c.node.id)) {
                        seen.add(c.node.id);
                        allCallers.push({ name: c.node.name, kind: c.node.kind, filePath: c.node.filePath, startLine: c.node.startLine });
                    }
                }
            }
            const limited = allCallers.slice(0, limit);
            if (options.json) {
                console.log(JSON.stringify({ symbol, callers: limited }, null, 2));
            }
            else if (limited.length === 0) {
                info(`No callers found for "${symbol}"`);
            }
            else {
                console.log(chalk.bold(`\nCallers of "${symbol}" (${limited.length}):\n`));
                for (const node of limited) {
                    const loc = node.startLine ? `:${node.startLine}` : '';
                    console.log(chalk.cyan(node.kind.padEnd(12)) +
                        chalk.white(node.name));
                    console.log(chalk.dim(`  ${node.filePath}${loc}`));
                    console.log();
                }
            }
            cg.destroy();
        }
        catch (err) {
            error(`callers failed: ${err instanceof Error ? err.message : String(err)}`);
            process.exit(1);
        }
    });
    /**
     * codegraph callees <symbol>
     */
    program
        .command('callees <symbol>')
        .description('Find all functions/methods that a specific symbol calls')
        .option('-p, --path <path>', 'Project path')
        .option('-l, --limit <number>', 'Maximum results', '20')
        .option('-j, --json', 'Output as JSON')
        .action(async (symbol, options) => {
        const projectPath = resolveProjectPath(options.path);
        try {
            if (!(0, directory_1.isInitialized)(projectPath)) {
                error(`CodeGraph not initialized in ${projectPath}`);
                process.exit(1);
            }
            const { default: CodeGraph } = await loadCodeGraph();
            const cg = await CodeGraph.open(projectPath);
            const limit = parseInt(options.limit || '20', 10);
            const matches = cg.searchNodes(symbol, { limit: 50 });
            if (matches.length === 0) {
                info(`Symbol "${symbol}" not found`);
                cg.destroy();
                return;
            }
            const seen = new Set();
            const allCallees = [];
            for (const match of matches) {
                const exactMatch = match.node.name === symbol || match.node.name.endsWith(`.${symbol}`) || match.node.name.endsWith(`::${symbol}`);
                if (!exactMatch && matches.length > 1)
                    continue;
                for (const c of cg.getCallees(match.node.id)) {
                    if (!seen.has(c.node.id)) {
                        seen.add(c.node.id);
                        allCallees.push({ name: c.node.name, kind: c.node.kind, filePath: c.node.filePath, startLine: c.node.startLine });
                    }
                }
            }
            if (allCallees.length === 0 && matches[0]) {
                for (const c of cg.getCallees(matches[0].node.id)) {
                    if (!seen.has(c.node.id)) {
                        seen.add(c.node.id);
                        allCallees.push({ name: c.node.name, kind: c.node.kind, filePath: c.node.filePath, startLine: c.node.startLine });
                    }
                }
            }
            const limited = allCallees.slice(0, limit);
            if (options.json) {
                console.log(JSON.stringify({ symbol, callees: limited }, null, 2));
            }
            else if (limited.length === 0) {
                info(`No callees found for "${symbol}"`);
            }
            else {
                console.log(chalk.bold(`\nCallees of "${symbol}" (${limited.length}):\n`));
                for (const node of limited) {
                    const loc = node.startLine ? `:${node.startLine}` : '';
                    console.log(chalk.cyan(node.kind.padEnd(12)) +
                        chalk.white(node.name));
                    console.log(chalk.dim(`  ${node.filePath}${loc}`));
                    console.log();
                }
            }
            cg.destroy();
        }
        catch (err) {
            error(`callees failed: ${err instanceof Error ? err.message : String(err)}`);
            process.exit(1);
        }
    });
    /**
     * codegraph impact <symbol>
     */
    program
        .command('impact <symbol>')
        .description('Analyze what code is affected by changing a symbol')
        .option('-p, --path <path>', 'Project path')
        .option('-d, --depth <number>', 'Traversal depth', '2')
        .option('-j, --json', 'Output as JSON')
        .action(async (symbol, options) => {
        const projectPath = resolveProjectPath(options.path);
        try {
            if (!(0, directory_1.isInitialized)(projectPath)) {
                error(`CodeGraph not initialized in ${projectPath}`);
                process.exit(1);
            }
            const { default: CodeGraph } = await loadCodeGraph();
            const cg = await CodeGraph.open(projectPath);
            const depth = Math.min(Math.max(parseInt(options.depth || '2', 10), 1), 10);
            const matches = cg.searchNodes(symbol, { limit: 50 });
            if (matches.length === 0) {
                info(`Symbol "${symbol}" not found`);
                cg.destroy();
                return;
            }
            // Merge impact subgraphs across all exact-matching symbols
            const mergedNodes = new Map();
            const seenEdges = new Set();
            let edgeCount = 0;
            for (const match of matches) {
                const exactMatch = match.node.name === symbol || match.node.name.endsWith(`.${symbol}`) || match.node.name.endsWith(`::${symbol}`);
                if (!exactMatch && matches.length > 1)
                    continue;
                const impact = cg.getImpactRadius(match.node.id, depth);
                for (const [id, n] of impact.nodes) {
                    mergedNodes.set(id, { name: n.name, kind: n.kind, filePath: n.filePath, startLine: n.startLine });
                }
                for (const e of impact.edges) {
                    const key = `${e.source}->${e.target}:${e.kind}`;
                    if (!seenEdges.has(key)) {
                        seenEdges.add(key);
                        edgeCount++;
                    }
                }
            }
            // Fallback to top match if exact filter removed everything
            if (mergedNodes.size === 0 && matches[0]) {
                const impact = cg.getImpactRadius(matches[0].node.id, depth);
                for (const [id, n] of impact.nodes) {
                    mergedNodes.set(id, { name: n.name, kind: n.kind, filePath: n.filePath, startLine: n.startLine });
                }
                edgeCount = impact.edges.length;
            }
            if (options.json) {
                console.log(JSON.stringify({
                    symbol,
                    depth,
                    nodeCount: mergedNodes.size,
                    edgeCount,
                    affected: Array.from(mergedNodes.values()),
                }, null, 2));
            }
            else if (mergedNodes.size === 0) {
                info(`No affected symbols found for "${symbol}"`);
            }
            else {
                console.log(chalk.bold(`\nImpact of changing "${symbol}" — ${mergedNodes.size} affected symbols:\n`));
                // Group by file
                const byFile = new Map();
                for (const node of mergedNodes.values()) {
                    const list = byFile.get(node.filePath) || [];
                    list.push({ name: node.name, kind: node.kind, startLine: node.startLine });
                    byFile.set(node.filePath, list);
                }
                for (const [file, nodes] of byFile) {
                    console.log(chalk.cyan(file));
                    for (const node of nodes) {
                        const loc = node.startLine ? `:${node.startLine}` : '';
                        console.log(`  ${chalk.dim(node.kind.padEnd(12))}${node.name}${chalk.dim(loc)}`);
                    }
                    console.log();
                }
            }
            cg.destroy();
        }
        catch (err) {
            error(`impact failed: ${err instanceof Error ? err.message : String(err)}`);
            process.exit(1);
        }
    });
    /**
     * codegraph affected [files...]
     *
     * Find test files affected by the given source files.
     * Traces dependency edges transitively to find test files that depend on changed code.
     *
     * Usage:
     *   git diff --name-only | codegraph affected --stdin
     *   codegraph affected src/lib/components/Editor.svelte src/routes/+page.svelte
     */
    program
        .command('affected [files...]')
        .description('Find test files affected by changed source files')
        .option('-p, --path <path>', 'Project path')
        .option('--stdin', 'Read file list from stdin (one per line)')
        .option('-d, --depth <number>', 'Max dependency traversal depth', '5')
        .option('-f, --filter <glob>', 'Custom glob filter for test files (e.g. "e2e/*.spec.ts")')
        .option('-j, --json', 'Output as JSON')
        .option('-q, --quiet', 'Only output file paths, no decoration')
        .action(async (fileArgs, options) => {
        const projectPath = resolveProjectPath(options.path);
        try {
            if (!(0, directory_1.isInitialized)(projectPath)) {
                error(`CodeGraph not initialized in ${projectPath}`);
                process.exit(1);
            }
            // Collect changed files from args or stdin
            let changedFiles = [...(fileArgs || [])];
            if (options.stdin) {
                const stdinData = fs.readFileSync(0, 'utf-8');
                const stdinFiles = stdinData.split('\n').map(f => f.trim()).filter(Boolean);
                changedFiles.push(...stdinFiles);
            }
            if (changedFiles.length === 0) {
                if (!options.quiet)
                    info('No files provided. Use file arguments or --stdin.');
                process.exit(0);
            }
            const { default: CodeGraph } = await loadCodeGraph();
            const cg = await CodeGraph.open(projectPath);
            const maxDepth = parseInt(options.depth || '5', 10);
            // Common test file patterns
            const defaultTestPatterns = [
                /\.spec\./,
                /\.test\./,
                /\/__tests__\//,
                /\/tests?\//,
                /\/e2e\//,
                /\/spec\//,
            ];
            // Custom filter pattern
            let customFilter = null;
            if (options.filter) {
                // Convert glob to regex: ** → .+, * → [^/]*, . → \.
                const regex = options.filter
                    .replace(/[+[\]{}()^$|\\]/g, '\\$&')
                    .replace(/\./g, '\\.')
                    .replace(/\*\*/g, '.+')
                    .replace(/\*/g, '[^/]*');
                customFilter = new RegExp(regex);
            }
            function isTestFile(filePath) {
                if (customFilter)
                    return customFilter.test(filePath);
                return defaultTestPatterns.some(p => p.test(filePath));
            }
            // BFS to find all transitive dependents of changed files, filtered to test files
            const affectedTests = new Set();
            const allDependents = new Set();
            for (const file of changedFiles) {
                // If the changed file is itself a test file, include it
                if (isTestFile(file)) {
                    affectedTests.add(file);
                    continue;
                }
                // BFS through dependents
                const queue = [{ file, depth: 0 }];
                const visited = new Set();
                visited.add(file);
                while (queue.length > 0) {
                    const current = queue.shift();
                    if (current.depth >= maxDepth)
                        continue;
                    const dependents = cg.getFileDependents(current.file);
                    for (const dep of dependents) {
                        if (visited.has(dep))
                            continue;
                        visited.add(dep);
                        allDependents.add(dep);
                        if (isTestFile(dep)) {
                            affectedTests.add(dep);
                        }
                        else {
                            queue.push({ file: dep, depth: current.depth + 1 });
                        }
                    }
                }
            }
            const sortedTests = Array.from(affectedTests).sort();
            // Output
            if (options.json) {
                console.log(JSON.stringify({
                    changedFiles,
                    affectedTests: sortedTests,
                    totalDependentsTraversed: allDependents.size,
                }, null, 2));
            }
            else if (options.quiet) {
                for (const t of sortedTests)
                    console.log(t);
            }
            else {
                if (sortedTests.length === 0) {
                    info('No test files affected by the changed files.');
                }
                else {
                    console.log(chalk.bold(`\nAffected test files (${sortedTests.length}):\n`));
                    for (const t of sortedTests) {
                        console.log('  ' + chalk.cyan(t));
                    }
                    console.log();
                }
            }
            cg.destroy();
        }
        catch (err) {
            error(`Affected analysis failed: ${err instanceof Error ? err.message : String(err)}`);
            process.exit(1);
        }
    });
    /**
     * codegraph install
     */
    program
        .command('install')
        .description('Install codegraph MCP server into one or more agents (Claude Code, Cursor, Codex CLI, opencode, Hermes Agent)')
        .option('-t, --target <ids>', 'Target agent(s): comma-separated ids, or "auto"|"all"|"none". Default: prompt')
        .option('-l, --location <where>', 'Install location: "global" or "local". Default: prompt')
        .option('-y, --yes', 'Non-interactive: defaults to --location=global --target=auto, auto-allow on')
        .option('--no-permissions', 'Skip writing the auto-allow permissions list (Claude Code only)')
        .option('--print-config <id>', 'Print MCP config snippet for the named agent and exit (no file writes)')
        .action(async (opts) => {
        if (opts.printConfig) {
            const { getTarget, listTargetIds } = await Promise.resolve().then(() => __importStar(require('../installer/targets/registry')));
            const target = getTarget(opts.printConfig);
            if (!target) {
                const known = listTargetIds().join(', ');
                error(`Unknown target "${opts.printConfig}". Known: ${known}.`);
                process.exit(1);
            }
            const loc = (opts.location === 'local' ? 'local' : 'global');
            process.stdout.write(target.printConfig(loc));
            return;
        }
        const { runInstallerWithOptions } = await Promise.resolve().then(() => __importStar(require('../installer')));
        if (opts.location && opts.location !== 'global' && opts.location !== 'local') {
            error(`--location must be "global" or "local" (got "${opts.location}").`);
            process.exit(1);
        }
        try {
            // Commander's `--no-permissions` makes `opts.permissions === false`;
            // omitting the flag leaves it `true` (the positive-form default).
            // We MUST treat the default-true as "user did not override — let
            // the orchestrator prompt" and only forward an explicit `false`
            // (or `true` when --yes implies it). Otherwise the auto-allow
            // prompt is silently skipped on every interactive run.
            const explicitNoPermissions = opts.permissions === false;
            const autoAllow = explicitNoPermissions
                ? false
                : opts.yes
                    ? true
                    : undefined;
            await runInstallerWithOptions({
                target: opts.target,
                location: opts.location,
                autoAllow,
                yes: opts.yes,
            });
        }
        catch (err) {
            error(err instanceof Error ? err.message : String(err));
            process.exit(1);
        }
    });
    /**
     * codegraph uninstall
     *
     * Inverse of `install`. Removes the codegraph MCP server entry,
     * instructions block, and permissions from every agent (or a
     * `--target` subset). Prompts global-vs-local when not given. Does NOT
     * delete the `.codegraph/` index — that's `codegraph uninit`.
     */
    program
        .command('uninstall')
        .description('Remove codegraph from your agents (Claude Code, Cursor, Codex CLI, opencode, Hermes Agent)')
        .option('-t, --target <ids>', 'Target agent(s): comma-separated ids, or "all". Default: all')
        .option('-l, --location <where>', 'Uninstall location: "global" or "local". Default: prompt')
        .option('-y, --yes', 'Non-interactive: defaults to --location=global --target=all')
        .action(async (opts) => {
        const { runUninstaller } = await Promise.resolve().then(() => __importStar(require('../installer')));
        if (opts.location && opts.location !== 'global' && opts.location !== 'local') {
            error(`--location must be "global" or "local" (got "${opts.location}").`);
            process.exit(1);
        }
        try {
            await runUninstaller({
                target: opts.target,
                location: opts.location,
                yes: opts.yes,
            });
        }
        catch (err) {
            error(err instanceof Error ? err.message : String(err));
            process.exit(1);
        }
    });
    // Parse and run
    program.parse();
} // end main()
//# sourceMappingURL=codegraph.js.map