"use strict";
/**
 * CLI Command Executor (non-interactive mode)
 * Issue #294: Executes CLI tool commands for scheduled executions
 * Issue #379: Added OpenCode support (opencode run)
 *
 * Supported tools: claude, codex, gemini, vibe-local, opencode
 *
 * Security:
 * - Uses execFile (not exec) to prevent shell injection
 * - Sanitizes environment variables via env-sanitizer.ts
 * - Limits output size to prevent memory exhaustion
 * - Enforces execution timeout
 * - Validates cliToolId against ALLOWED_CLI_TOOLS whitelist [SEC-001]
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.ALLOWED_CLI_TOOLS = exports.MAX_MESSAGE_LENGTH = exports.EXECUTION_TIMEOUT_MS = exports.MAX_STORED_OUTPUT_SIZE = exports.MAX_OUTPUT_SIZE = void 0;
exports.truncateOutput = truncateOutput;
exports.buildCliArgs = buildCliArgs;
exports.executeClaudeCommand = executeClaudeCommand;
exports.getActiveProcesses = getActiveProcesses;
const child_process_1 = require("child_process");
const env_sanitizer_1 = require("../../lib/security/env-sanitizer");
const cli_patterns_1 = require("../../lib/detection/cli-patterns");
// =============================================================================
// Constants
// =============================================================================
/** Maximum output buffer size for execFile (1MB) */
exports.MAX_OUTPUT_SIZE = 1 * 1024 * 1024;
/** Maximum output size stored in DB (100KB) */
exports.MAX_STORED_OUTPUT_SIZE = 100 * 1024;
/** Execution timeout in milliseconds (15 minutes) */
exports.EXECUTION_TIMEOUT_MS = 15 * 60 * 1000;
/** Maximum message length sent to claude -p */
exports.MAX_MESSAGE_LENGTH = 10000;
/** Allowed CLI tool identifiers for scheduled execution */
exports.ALLOWED_CLI_TOOLS = new Set(['claude', 'codex', 'gemini', 'vibe-local', 'opencode']);
// =============================================================================
// Executor
// =============================================================================
/**
 * Truncate output to MAX_STORED_OUTPUT_SIZE bytes.
 * Appends a truncation notice if truncated.
 *
 * @param output - Raw output string
 * @returns Truncated output string
 */
function truncateOutput(output) {
    if (Buffer.byteLength(output, 'utf-8') <= exports.MAX_STORED_OUTPUT_SIZE) {
        return output;
    }
    // Truncate to MAX_STORED_OUTPUT_SIZE bytes
    const buffer = Buffer.from(output, 'utf-8');
    const truncated = buffer.subarray(0, exports.MAX_STORED_OUTPUT_SIZE).toString('utf-8');
    return truncated + '\n\n--- Output truncated (exceeded 100KB limit) ---';
}
/**
 * Build CLI arguments for non-interactive execution based on CLI tool type.
 *
 * - claude: -p <message> --output-format text --permission-mode <permission>
 * - codex: exec <message> --sandbox <permission>
 * - gemini: -p <message>
 * - vibe-local: [-p <message> -y] or [--model <model> -p <message> -y]
 * - opencode: [run <message>] or [run -m ollama/<model> <message>]
 * - others: -p <message> (fallback)
 *
 * @param message - Prompt message
 * @param cliToolId - CLI tool identifier
 * @param permission - Permission mode (claude: --permission-mode, codex: --sandbox)
 * @param options - Additional options (e.g., model for vibe-local)
 * @returns Array of CLI arguments
 */
function buildCliArgs(message, cliToolId, permission, options) {
    switch (cliToolId) {
        case 'codex':
            return ['exec', message, '--sandbox', permission ?? 'workspace-write'];
        case 'gemini':
            return ['-p', message];
        case 'vibe-local':
            if (options?.model) {
                return ['--model', options.model, '-p', message, '-y'];
            }
            return ['-p', message, '-y'];
        case 'opencode':
            // [D2-007] When model is not specified, OpenCode uses opencode.json default model
            if (options?.model) {
                return ['run', '-m', `ollama/${options.model}`, message];
            }
            return ['run', message];
        case 'claude':
        default:
            return ['-p', message, '--output-format', 'text', '--permission-mode', permission ?? 'acceptEdits'];
    }
}
/**
 * Execute a CLI command in a worktree directory.
 *
 * @param message - Prompt message to send
 * @param cwd - Working directory (worktree path from DB)
 * @param cliToolId - CLI tool to use (default: 'claude')
 * @param permission - Permission mode (claude: --permission-mode, codex: --sandbox)
 * @param options - Additional options (e.g., model for vibe-local)
 * @returns Execution result with output and status
 */
async function executeClaudeCommand(message, cwd, cliToolId = 'claude', permission, options) {
    // Validate cliToolId against whitelist [SEC-001]
    if (!exports.ALLOWED_CLI_TOOLS.has(cliToolId)) {
        return {
            output: '',
            exitCode: null,
            status: 'failed',
            error: `Invalid CLI tool: ${cliToolId}`,
        };
    }
    // Validate message length
    const truncatedMessage = message.length > exports.MAX_MESSAGE_LENGTH
        ? message.substring(0, exports.MAX_MESSAGE_LENGTH)
        : message;
    const args = buildCliArgs(truncatedMessage, cliToolId, permission, options);
    return new Promise((resolve) => {
        const child = (0, child_process_1.execFile)(cliToolId, args, {
            cwd,
            env: (0, env_sanitizer_1.sanitizeEnvForChildProcess)(),
            maxBuffer: exports.MAX_OUTPUT_SIZE,
            timeout: exports.EXECUTION_TIMEOUT_MS,
        }, (error, stdout, stderr) => {
            if (error) {
                const isTimeout = error.killed || error.code === 'ETIMEDOUT';
                const rawOutput = (0, cli_patterns_1.stripAnsi)(stdout || stderr || error.message);
                const output = truncateOutput(rawOutput);
                resolve({
                    output,
                    exitCode: error.code ? parseInt(String(error.code), 10) || null : null,
                    status: isTimeout ? 'timeout' : 'failed',
                    error: error.message,
                });
                return;
            }
            const rawOutput = (0, cli_patterns_1.stripAnsi)(stdout || '');
            const output = truncateOutput(rawOutput);
            resolve({
                output,
                exitCode: 0,
                status: 'completed',
            });
        });
        // Close stdin immediately to prevent hanging on yes/no prompts
        child.stdin?.end();
        // Return the child process PID for tracking
        if (child.pid) {
            // Store PID in global active processes for cleanup on shutdown
            const activeProcesses = getActiveProcesses();
            activeProcesses.set(child.pid, child);
            child.on('exit', () => {
                activeProcesses.delete(child.pid);
            });
        }
    });
}
/**
 * Get the global active processes map.
 * Uses globalThis for hot reload persistence.
 */
function getActiveProcesses() {
    if (!globalThis.__scheduleActiveProcesses) {
        globalThis.__scheduleActiveProcesses = new Map();
    }
    return globalThis.__scheduleActiveProcesses;
}
