import { writeFile } from 'node:fs/promises';
import path from 'node:path';
import { Option } from 'commander';
import { z } from 'zod';
import { buildCodebaseIndex } from './build.js';
import { DEFAULT_EXCLUDE_DIRS, writeStream } from '../utils.js';
import { parsePositiveInt } from '../cli-validation.js';
import { checkCodeSyntax } from './check.js';
import { analyzeCallChain, analyzeCallers, analyzeCalls, analyzeDeps, analyzeModuleCoupling, analyzeTree, createPayload, findByName, findByPattern, findByType, findContent, fusedSearch, } from './query.js';
import { formatCodeDiff, formatCodeDiffJson, formatChains, formatClassTree, formatContentResults, formatDirectoryTree, formatDirectoryTreeJson, formatDependencies, formatFusedResults, formatModuleCoupling, formatPayloadJson, formatRelationResults, formatSearchResults, renderRepoMap, } from './output.js';
import { computeCodeDiff } from './diff.js';
import { buildDirectoryTree } from './tree.js';
import { buildRepoMap, stampRepoMapChanges, toRepoMapChangeContext, } from './map.js';
import { readCodeSource } from './source.js';
import { createTokenizer, resolveTokenizerName } from '../tokens.js';
import { getGitDiffSummary, getGitFileHistory } from '../git/api.js';
const CodeCommandOptionsSchema = z
    .object({
    path: z.string().optional(),
    format: z.string().optional(),
    output: z.string().optional(),
    ci: z.boolean().optional(),
    file: z.string().optional(),
    limit: z.string().optional(),
    depth: z.string().optional(),
    type: z.string().optional(),
    excludeDir: z.string().optional(),
    gitignore: z.boolean().optional(),
    ignorePattern: z.array(z.string()).optional(),
    contentMode: z.string().optional(),
    maxFileSizeBytes: z.string().optional(),
    maxLines: z.string().optional(),
    skipMinified: z.boolean().optional(),
    mapTokens: z.string().optional(),
    tokenBudget: z.string().optional(),
    mode: z.string().optional(),
    mapFormat: z.string().optional(),
    compress: z.boolean().optional(),
    biasExports: z.boolean().optional(),
    maxSymbols: z.string().optional(),
    maxEntries: z.string().optional(),
    tokenizer: z.string().optional(),
    query: z.string().optional(),
    focusPath: z.array(z.string()).optional(),
    focusDepth: z.string().optional(),
    base: z.string().optional(),
    head: z.string().optional(),
    staged: z.boolean().optional(),
    changedFrom: z.string().optional(),
    changedTo: z.string().optional(),
    nodeId: z.string().optional(),
})
    .passthrough();
function getOptions(command) {
    return CodeCommandOptionsSchema.parse(command.optsWithGlobals());
}
/**
 * Wrap a subcommand action so thrown validation/runtime errors print a clean
 * `Error: <message>` line and set a non-zero exit code, matching the default
 * scan command. parseAsync does not catch action-handler rejections, so each
 * action opts in explicitly.
 */
function runAction(handler) {
    return async (...args) => {
        try {
            await handler(...args);
        }
        catch (error) {
            await writeStream(process.stderr, `Error: ${error.message}\n`);
            process.exitCode = 1;
        }
    };
}
const FINDABLE_TYPES = [
    'file',
    'module',
    'function',
    'class',
    'interface',
    'type-alias',
    'enum',
    'variable',
];
const FindableTypeSchema = z.enum([
    'file',
    'module',
    'function',
    'class',
    'interface',
    'type-alias',
    'enum',
    'variable',
]);
const ContentModeSchema = z.enum(['none', 'excerpt', 'full']);
function collectOption(value, previous = []) {
    return [...previous, value];
}
function parseType(value) {
    if (!value)
        return undefined;
    const result = FindableTypeSchema.safeParse(value);
    if (!result.success) {
        throw new Error(`Invalid type "${value}". Expected one of: ${FINDABLE_TYPES.join(', ')}`);
    }
    return result.data;
}
/** Parse a CLI string option as a non-negative integer, returning fallback if undefined. */
function parseNonNegativeInt(value, fallback, label) {
    if (!value)
        return fallback;
    const parsed = Number.parseInt(value, 10);
    if (!Number.isFinite(parsed) || parsed < 0) {
        throw new Error(`Invalid ${label}: "${value}"`);
    }
    return parsed;
}
async function emit(output, options) {
    if (options.output) {
        await writeFile(options.output, output);
        return;
    }
    await writeStream(process.stdout, output);
}
function formatSourceResult(result) {
    if (result.status === 'resolved')
        return result.source;
    const candidates = 'candidates' in result && result.candidates.length > 0
        ? `\n${result.candidates
            .map(candidate => `- ${candidate.name} ${candidate.relativePath ?? candidate.path}:${candidate.line ?? '?'}`)
            .join('\n')}`
        : '';
    return `${result.status}: ${result.message}${candidates}`;
}
function formatCheckResult(result) {
    if (result.status === 'unsupported')
        return `unsupported: ${result.relativePath}`;
    if (result.status === 'error')
        return `error: ${result.message}`;
    if (result.valid)
        return `valid: ${result.relativePath}`;
    return [
        `invalid: ${result.relativePath}`,
        ...result.diagnostics.map(diagnostic => {
            const location = diagnostic.line ? `:${diagnostic.line}${diagnostic.column ? `:${diagnostic.column}` : ''}` : '';
            return `- ${diagnostic.source ?? 'syntax'}${location}: ${diagnostic.message}`;
        }),
    ].join('\n');
}
function resolveContentMode(value, fallback) {
    if (!value)
        return fallback;
    return ContentModeSchema.parse(value);
}
function buildIndexOptions(options, fallbackContentMode, forceContentMode) {
    const repoRoot = path.resolve(options.path ?? process.cwd());
    return {
        repoRoot,
        excludeDir: options.excludeDir
            ?.split(',')
            .map(value => value.trim())
            .filter(Boolean),
        noGitignore: options.gitignore === false,
        ignorePatterns: options.ignorePattern,
        contentMode: forceContentMode ?? resolveContentMode(options.contentMode, fallbackContentMode),
        maxFileSizeBytes: options.maxFileSizeBytes
            ? parsePositiveInt(options.maxFileSizeBytes, 2_097_152, 'max file size')
            : undefined,
        maxLines: options.maxLines ? parsePositiveInt(options.maxLines, 25_000, 'max line count') : undefined,
        skipMinified: options.skipMinified,
    };
}
async function withIndex(options, fallbackContentMode = 'excerpt') {
    return buildCodebaseIndex(buildIndexOptions(options, fallbackContentMode));
}
/**
 * Build an index with content forced to "full" for commands whose search cannot
 * run without full content. An explicit conflicting --content-mode is reported
 * and overridden rather than silently producing an index that would fail.
 */
async function withFullContentIndex(options, commandLabel) {
    if (options.contentMode && options.contentMode !== 'full') {
        await writeStream(process.stderr, `Warning: ${commandLabel} requires --content-mode full; ignoring "${options.contentMode}".\n`);
    }
    return buildCodebaseIndex(buildIndexOptions(options, 'full', 'full'));
}
function addSharedOptions(command, extra = {}) {
    command
        .option('--path <repo-root>', 'repository root to analyze', process.cwd())
        .option('--format <type>', 'output format: tabular or json', 'tabular')
        .option('-o, --output <file>', 'write output to file')
        .option('--ci', 'ASCII-only output, no colors')
        .option('--exclude-dir <dirs>', 'directories to skip (comma-separated)', DEFAULT_EXCLUDE_DIRS.join(','))
        .option('--no-gitignore', 'disable .gitignore respect')
        .option('--ignore-pattern <pattern>', 'gitignore-style pattern to ignore', collectOption, [])
        .option('--content-mode <mode>', 'indexed content mode: none, excerpt, or full')
        .option('--max-file-size-bytes <n>', 'skip code files larger than this many bytes')
        .option('--max-lines <n>', 'skip code files with more than this many lines')
        .option('--no-skip-minified', 'do not skip likely minified code files');
    if (extra.includeFile) {
        command.option('--file <path>', 'specific file path to disambiguate the target');
    }
    if (extra.includeLimit) {
        command.option('--limit <n>', 'maximum results to return', '50');
    }
    if (extra.includeDepth) {
        command.option('--depth <n>', 'maximum call-chain depth', '5');
    }
    if (extra.includeType) {
        command.addOption(new Option('--type <node-type>', 'filter by node type').choices(FINDABLE_TYPES));
    }
}
export function registerCodeCommands(program) {
    const code = program.command('code').description('Explore code structure with first-class JS/TS and Python support');
    const find = code.command('find').description('Find symbols and source content');
    const analyze = code.command('analyze').description('Analyze relationships between code elements');
    const diffCmd = code.command('diff').description('Show changed symbols for a git diff range');
    addSharedOptions(diffCmd);
    diffCmd.option('--base <ref>', 'base ref for the diff range');
    diffCmd.option('--head <ref>', 'head ref for the diff range');
    diffCmd.option('--staged', 'inspect staged changes');
    diffCmd.option('--map-tokens <n>', 'token budget for changed symbols', '4096');
    diffCmd.addOption(new Option('--tokenizer <name>', 'token counter for --map-tokens').choices([
        'heuristic',
        'o200k_base',
        'cl100k_base',
    ]));
    diffCmd.action(runAction(async (_options, command) => {
        const options = getOptions(command);
        const tokenizerName = resolveTokenizerName(options.tokenizer);
        const tokenizer = await createTokenizer(tokenizerName);
        const index = await withIndex(options, 'excerpt');
        const result = await computeCodeDiff(index, {
            base: options.base,
            head: options.head,
            staged: options.staged,
            mapTokens: parsePositiveInt(options.mapTokens ?? options.tokenBudget, 4096, 'map tokens'),
            tokenizer: tokenizerName,
            countTokens: text => tokenizer.count(text),
        });
        const output = options.format === 'json' ? formatCodeDiffJson(result) : formatCodeDiff(result, options.ci);
        await emit(output.endsWith('\n') ? output : `${output}\n`, options);
    }));
    const historyCmd = code.command('history <symbol>').description('Show git line history for a symbol');
    addSharedOptions(historyCmd, { includeFile: true, includeLimit: true, includeType: true });
    historyCmd.action(runAction(async (symbol, _options, command) => {
        const options = getOptions(command);
        const index = await withIndex(options, 'excerpt');
        const matches = findByName(index, symbol, parseType(options.type), options.file, 1);
        const match = matches[0]?.node;
        if (!match || match.line == null) {
            throw new Error(`Symbol "${symbol}" was not found with a line range.`);
        }
        const relativePath = match.relativePath ?? path.relative(index.repoRoot, match.path).split(path.sep).join('/');
        const result = await getGitFileHistory({
            repoRoot: index.repoRoot,
            path: relativePath,
            lineRange: { start: match.line, end: match.endLine ?? match.line },
            limit: parsePositiveInt(options.limit, 50, 'limit'),
        });
        const output = options.format === 'json'
            ? JSON.stringify(result, null, 2)
            : result.entries.map(entry => `${entry.shortHash} ${entry.authorDate} ${entry.subject}`).join('\n') + '\n';
        await emit(output.endsWith('\n') ? output : `${output}\n`, options);
    }));
    // occ code index — build and emit the full codebase index
    const indexCmd = code
        .command('index')
        .description('Build the full codebase index (files, symbols, edges, capabilities)');
    addSharedOptions(indexCmd);
    indexCmd.action(runAction(async (_options, command) => {
        const options = getOptions(command);
        const index = await buildCodebaseIndex(buildIndexOptions(options, 'full'), event => {
            if (options.format !== 'json') {
                process.stderr.write(`\r${event.phase}: ${event.completed}/${event.total}${event.detail ? ` ${event.detail}` : ''}   `);
            }
        });
        if (options.format !== 'json') {
            process.stderr.write('\n');
        }
        const summary = options.format === 'json'
            ? JSON.stringify(index, null, 2)
            : `${index.files.length} files, ${index.nodes.length} nodes, ${index.edges.length} edges, ${Object.keys(index.capabilities).length} languages\n`;
        await emit(summary.endsWith('\n') ? summary : `${summary}\n`, options);
    }));
    const findName = find.command('name <name>').description('Find code elements by exact name');
    addSharedOptions(findName, { includeFile: true, includeLimit: true, includeType: true });
    findName.action(runAction(async (name, _options, command) => {
        const options = getOptions(command);
        const index = await withIndex(options, 'excerpt');
        const results = findByName(index, name, parseType(options.type), options.file, parsePositiveInt(options.limit, 50, 'limit'));
        const payload = createPayload(index, { command: 'code.find.name', value: name, file: options.file, type: options.type }, results);
        const output = options.format === 'json'
            ? formatPayloadJson(payload)
            : formatSearchResults(`Code Search: ${name}`, results, options.ci);
        await emit(output.endsWith('\n') ? output : `${output}\n`, options);
    }));
    const findPatternCmd = find.command('pattern <pattern>').description('Find code elements by substring pattern');
    addSharedOptions(findPatternCmd, { includeLimit: true, includeType: true });
    findPatternCmd.action(runAction(async (pattern, _options, command) => {
        const options = getOptions(command);
        const index = await withIndex(options, 'excerpt');
        const results = findByPattern(index, pattern, parseType(options.type), parsePositiveInt(options.limit, 50, 'limit'));
        const payload = createPayload(index, { command: 'code.find.pattern', value: pattern, type: options.type }, results);
        const output = options.format === 'json'
            ? formatPayloadJson(payload)
            : formatSearchResults(`Code Pattern: ${pattern}`, results, options.ci);
        await emit(output.endsWith('\n') ? output : `${output}\n`, options);
    }));
    const findTypeCmd = find.command('type <nodeType>').description('List all nodes of a given type');
    addSharedOptions(findTypeCmd, { includeLimit: true });
    findTypeCmd.action(runAction(async (nodeType, _options, command) => {
        const options = getOptions(command);
        const index = await withIndex(options, 'excerpt');
        const results = findByType(index, parseType(nodeType) ?? 'function', parsePositiveInt(options.limit, 50, 'limit'));
        const payload = createPayload(index, { command: 'code.find.type', value: nodeType }, results);
        const output = options.format === 'json'
            ? formatPayloadJson(payload)
            : formatSearchResults(`Code Type: ${nodeType}`, results, options.ci);
        await emit(output.endsWith('\n') ? output : `${output}\n`, options);
    }));
    const findContentCmd = find.command('content <text>').description('Search code content');
    addSharedOptions(findContentCmd, { includeLimit: true });
    findContentCmd.action(runAction(async (text, _options, command) => {
        const options = getOptions(command);
        const index = await withFullContentIndex(options, 'find content');
        const results = findContent(index, text, parsePositiveInt(options.limit, 50, 'limit'));
        const payload = createPayload(index, { command: 'code.find.content', value: text }, results);
        const output = options.format === 'json' ? formatPayloadJson(payload) : formatContentResults(results, options.ci);
        await emit(output.endsWith('\n') ? output : `${output}\n`, options);
    }));
    const sourceCmd = code.command('source [name]').description('Read one symbol body by node id or disambiguated name');
    addSharedOptions(sourceCmd, { includeFile: true, includeType: true });
    sourceCmd.option('--node-id <id>', 'exact node id from code map/find/index output');
    sourceCmd.action(runAction(async (name, _options, command) => {
        const options = getOptions(command);
        if (options.nodeId && name) {
            throw new Error('Use either --node-id or a symbol name, not both.');
        }
        if (!options.nodeId && !name) {
            throw new Error('Provide --node-id <id> or a symbol name.');
        }
        const index = await withIndex(options, 'none');
        const query = options.nodeId
            ? { nodeId: options.nodeId }
            : { name: name ?? '', file: options.file, type: parseType(options.type) };
        const result = await readCodeSource(index, query);
        // Signal failure to scripts/agents when the symbol can't be uniquely resolved
        // (not_found / ambiguous / error), mirroring `check`'s valid===false exit gate.
        if (result.status !== 'resolved') {
            process.exitCode = 1;
        }
        const output = options.format === 'json' ? JSON.stringify(result, null, 2) : formatSourceResult(result);
        await emit(output.endsWith('\n') ? output : `${output}\n`, options);
    }));
    const checkCmd = code.command('check <file>').description('Check syntax for a source file without a language server');
    addSharedOptions(checkCmd);
    checkCmd.action(runAction(async (file, _options, command) => {
        const options = getOptions(command);
        const repoRoot = path.resolve(options.path ?? process.cwd());
        const result = await checkCodeSyntax({ repoRoot, file });
        if (result.valid === false) {
            process.exitCode = 1;
        }
        const output = options.format === 'json' ? JSON.stringify(result, null, 2) : formatCheckResult(result);
        await emit(output.endsWith('\n') ? output : `${output}\n`, options);
    }));
    const analyzeCallsCmd = analyze.command('calls <functionName>').description('Show what a function calls');
    addSharedOptions(analyzeCallsCmd, { includeFile: true });
    analyzeCallsCmd.action(runAction(async (functionName, _options, command) => {
        const options = getOptions(command);
        const index = await withIndex(options, 'excerpt');
        const results = analyzeCalls(index, functionName, options.file);
        const payload = createPayload(index, { command: 'code.analyze.calls', value: functionName, file: options.file }, results);
        const output = options.format === 'json'
            ? formatPayloadJson(payload)
            : formatRelationResults(`Outgoing Calls: ${functionName}`, 'Callee', results, options.ci);
        await emit(output.endsWith('\n') ? output : `${output}\n`, options);
    }));
    const analyzeCallersCmd = analyze.command('callers <functionName>').description('Show what calls a function');
    addSharedOptions(analyzeCallersCmd, { includeFile: true });
    analyzeCallersCmd.action(runAction(async (functionName, _options, command) => {
        const options = getOptions(command);
        const index = await withIndex(options, 'excerpt');
        const results = analyzeCallers(index, functionName, options.file);
        const payload = createPayload(index, { command: 'code.analyze.callers', value: functionName, file: options.file }, results);
        const output = options.format === 'json'
            ? formatPayloadJson(payload)
            : formatRelationResults(`Incoming Calls: ${functionName}`, 'Caller', results, options.ci, 'from');
        await emit(output.endsWith('\n') ? output : `${output}\n`, options);
    }));
    const analyzeChainCmd = analyze
        .command('chain <fromFunction> <toFunction>')
        .description('Find call paths between two functions');
    addSharedOptions(analyzeChainCmd, { includeDepth: true });
    analyzeChainCmd.action(runAction(async (fromFunction, toFunction, _options, command) => {
        const options = getOptions(command);
        const index = await withIndex(options, 'excerpt');
        const results = analyzeCallChain(index, fromFunction, toFunction, parsePositiveInt(options.depth, 5, 'depth'));
        const payload = createPayload(index, { command: 'code.analyze.chain', from: fromFunction, to: toFunction, depth: options.depth }, results);
        const output = options.format === 'json' ? formatPayloadJson(payload) : formatChains(results, options.ci);
        await emit(output.endsWith('\n') ? output : `${output}\n`, options);
    }));
    const analyzeDepsCmd = analyze.command('deps <target>').description('Show module/file import relationships');
    addSharedOptions(analyzeDepsCmd);
    analyzeDepsCmd.action(runAction(async (target, _options, command) => {
        const options = getOptions(command);
        const index = await withIndex(options, 'excerpt');
        const results = analyzeDeps(index, target);
        const payload = createPayload(index, { command: 'code.analyze.deps', value: target }, results);
        const output = options.format === 'json' ? formatPayloadJson(payload) : formatDependencies(results, options.ci);
        await emit(output.endsWith('\n') ? output : `${output}\n`, options);
    }));
    const analyzeCouplingCmd = analyze.command('coupling <target>').description('Show module-level coupling metrics');
    addSharedOptions(analyzeCouplingCmd);
    analyzeCouplingCmd.action(runAction(async (target, _options, command) => {
        const options = getOptions(command);
        const index = await withIndex(options, 'excerpt');
        const results = analyzeModuleCoupling(index, target);
        const payload = createPayload(index, { command: 'code.analyze.coupling', value: target }, results);
        const output = options.format === 'json' ? formatPayloadJson(payload) : formatModuleCoupling(results, options.ci);
        await emit(output.endsWith('\n') ? output : `${output}\n`, options);
    }));
    const analyzeTreeCmd = analyze
        .command('tree <className>')
        .description('Show class/interface inheritance relationships');
    addSharedOptions(analyzeTreeCmd, { includeFile: true });
    analyzeTreeCmd.action(runAction(async (className, _options, command) => {
        const options = getOptions(command);
        const index = await withIndex(options, 'excerpt');
        const results = analyzeTree(index, className, options.file);
        const payload = createPayload(index, { command: 'code.analyze.tree', value: className, file: options.file }, results);
        const output = options.format === 'json' ? formatPayloadJson(payload) : formatClassTree(results, options.ci);
        await emit(output.endsWith('\n') ? output : `${output}\n`, options);
    }));
    const findFusedCmd = find
        .command('fused <query>')
        .description('Fused search: combines name, pattern, and content search with rank fusion');
    addSharedOptions(findFusedCmd, { includeLimit: true });
    findFusedCmd.option('--weights <spec>', 'weights as name:N,pattern:N,content:N', 'name:3,pattern:2,content:1');
    findFusedCmd.action(runAction(async (query, _options, command) => {
        const options = getOptions(command);
        const weightsStr = options.weights ?? 'name:3,pattern:2,content:1';
        const weights = {};
        for (const pair of weightsStr.split(',')) {
            const [key, val] = pair.split(':');
            const weightKey = key?.trim();
            const weightValue = val?.trim();
            if (!weightKey || !weightValue)
                continue;
            const parsed = z.number().finite().nonnegative().safeParse(Number(weightValue));
            if (!parsed.success) {
                throw new Error(`Invalid --weights value "${weightValue}" for "${weightKey}" (expected a non-negative number).`);
            }
            weights[weightKey] = parsed.data;
        }
        const needsFull = (weights.content ?? 1) > 0;
        const index = needsFull ? await withFullContentIndex(options, 'find fused') : await withIndex(options, 'excerpt');
        const results = fusedSearch(index, query, {
            weights: { name: weights.name, pattern: weights.pattern, content: weights.content },
            limit: parsePositiveInt(options.limit, 50, 'limit'),
        });
        const payload = createPayload(index, { command: 'code.find.fused', value: query, weights }, results);
        const output = options.format === 'json' ? formatPayloadJson(payload) : formatFusedResults(results, options.ci);
        await emit(output.endsWith('\n') ? output : `${output}\n`, options);
    }));
    const dirtreeCmd = code.command('dirtree').description('Render a bounded directory tree from the built code index');
    addSharedOptions(dirtreeCmd);
    dirtreeCmd.option('--depth <n>', 'maximum rendered tree depth', '6');
    dirtreeCmd.option('--max-entries <n>', 'maximum entries per directory', '50');
    dirtreeCmd.addOption(new Option('--mode <mode>', 'tree mode').choices(['compact', 'verbose', 'map']).default('compact'));
    dirtreeCmd.option('--map-tokens <n>', 'token budget for the directory tree (default 4096)');
    dirtreeCmd.option('--token-budget <n>', 'alias for --map-tokens');
    dirtreeCmd.addOption(new Option('--tokenizer <name>', 'token counter for the budget').choices(['heuristic', 'o200k_base', 'cl100k_base']));
    dirtreeCmd.option('--focus-path <path>', 'subtree root to render', collectOption, []);
    dirtreeCmd.action(runAction(async (_options, command) => {
        const options = getOptions(command);
        const tokenizerName = resolveTokenizerName(options.tokenizer);
        const tokenizer = await createTokenizer(tokenizerName);
        const index = await withIndex(options, 'excerpt');
        const focusPath = (options.focusPath ?? []).at(-1);
        const result = buildDirectoryTree(index, {
            mode: options.mode === 'verbose' || options.mode === 'map' ? options.mode : 'compact',
            maxDepth: parseNonNegativeInt(options.depth, 6, 'depth'),
            maxEntriesPerDir: parsePositiveInt(options.maxEntries, 50, 'max entries'),
            tokenBudget: parsePositiveInt(options.mapTokens ?? options.tokenBudget, 4096, 'map tokens'),
            focusPath,
            countTokens: text => tokenizer.count(text),
        });
        const output = options.format === 'json' ? formatDirectoryTreeJson(result) : formatDirectoryTree(result);
        await emit(output.endsWith('\n') ? output : `${output}\n`, options);
    }));
    registerRepoMapCommand(code, 'map', 'map', 'Token-budgeted, importance-ranked repo map (top symbol signatures per file)');
    registerRepoMapCommand(code, 'pack', 'pack', 'Token-budgeted, importance-ranked pack (file content, optionally compressed)');
}
const REPO_MAP_FORMATS = ['markdown', 'xml', 'json', 'plain'];
function registerRepoMapCommand(code, name, defaultMode, description) {
    const cmd = code.command(name).description(description);
    addSharedOptions(cmd);
    cmd.option('--map-tokens <n>', 'token budget for the repo map (default 4096)');
    cmd.option('--token-budget <n>', 'alias for --map-tokens');
    cmd.addOption(new Option('--mode <mode>', 'map (signatures) or pack (content)').choices(['map', 'pack']));
    cmd.addOption(new Option('--map-format <fmt>', 'output format for the map').choices(REPO_MAP_FORMATS));
    cmd.option('--compress', 'pack mode: compress content to the architecturally key section');
    cmd.option('--no-bias-exports', 'do not boost exported symbols when ranking');
    cmd.option('--max-symbols <n>', 'maximum symbols included per file');
    cmd.addOption(new Option('--tokenizer <name>', 'token counter for the budget').choices(['heuristic', 'o200k_base', 'cl100k_base']));
    cmd.option('--query <text>', 'focus: boost files/symbols matching this text');
    cmd.option('--focus-path <path>', 'focus: boost this file/dir and its graph neighbors (repeatable)', collectOption, []);
    cmd.option('--focus-depth <n>', 'focus: graph-neighbor depth for --focus-path (default 1, 0 = seeds only)');
    cmd.option('--changed-from <ref>', 'focus: boost files changed from this base ref');
    cmd.option('--changed-to <ref>', 'focus: compare changed files to this head ref');
    cmd.option('--staged', 'focus: use staged changes for --changed-from/--changed-to');
    cmd.action(runAction(async (_options, command) => {
        const options = getOptions(command);
        const mode = options.mode ?? defaultMode;
        const format = options.mapFormat ?? (options.format === 'json' ? 'json' : 'markdown');
        const budget = parsePositiveInt(options.mapTokens ?? options.tokenBudget, 4096, 'map tokens');
        const maxSymbols = options.maxSymbols ? parsePositiveInt(options.maxSymbols, 0, 'max symbols') : undefined;
        const tokenizerName = resolveTokenizerName(options.tokenizer);
        const tokenizer = await createTokenizer(tokenizerName);
        const index = await withIndex(options, mode === 'pack' ? 'full' : 'excerpt');
        const focusQuery = options.query?.trim();
        const focusPaths = (options.focusPath ?? []).map(value => value.trim()).filter(Boolean);
        let changeContext;
        if (options.changedFrom || options.changedTo || options.staged) {
            const summary = await getGitDiffSummary({
                repoRoot: index.repoRoot,
                base: options.changedFrom,
                head: options.changedTo,
                staged: options.staged,
            });
            changeContext = toRepoMapChangeContext(index.repoRoot, summary);
        }
        const allFocusPaths = [...new Set([...focusPaths, ...(changeContext?.focusPaths ?? [])])];
        const focus = focusQuery || allFocusPaths.length > 0
            ? {
                query: focusQuery || undefined,
                paths: allFocusPaths,
                graphDepth: options.focusDepth ? parseNonNegativeInt(options.focusDepth, 1, 'focus depth') : undefined,
            }
            : undefined;
        const result = buildRepoMap(index, {
            tokenBudget: budget,
            mode,
            format,
            compress: options.compress,
            maxSymbolsPerFile: maxSymbols,
            rank: { biasExports: options.biasExports },
            countTokens: text => tokenizer.count(text),
            focus,
        });
        result.tokenizer = tokenizerName;
        if (changeContext)
            stampRepoMapChanges(result, changeContext);
        const output = renderRepoMap(result);
        await emit(output.endsWith('\n') ? output : `${output}\n`, options);
    }));
}
//# sourceMappingURL=command.js.map