/**
 * CLI Command - Unified CLI tool executor command
 * Provides interface for executing Gemini, Qwen, and Codex
 */

import chalk from 'chalk';
import http from 'http';
import inquirer from 'inquirer';
import type { CliOutputUnit } from '../tools/cli-output-converter.js';
import { SmartContentFormatter } from '../tools/cli-output-converter.js';
import {
  cliExecutorTool,
  generateExecutionId,
  getCliToolsStatus,
  getExecutionHistory,
  getExecutionHistoryAsync,
  getExecutionDetail,
  getConversationDetail,
  killCurrentCliProcess
} from '../tools/cli-executor.js';
import {
  getStorageStats,
  getStorageConfig,
  cleanProjectStorage,
  cleanAllStorage,
  formatBytes,
  formatTimeAgo,
  resolveProjectId,
  projectExists,
  getStorageLocationInstructions
} from '../tools/storage-manager.js';
import { getHistoryStore, findProjectWithExecution, getRegisteredExecutionHistory } from '../tools/cli-history-store.js';
import { createSpinner } from '../utils/ui.js';
import { loadClaudeCliSettings } from '../tools/claude-cli-tools.js';

// Dashboard notification settings
const DASHBOARD_PORT = process.env.CCW_PORT || 3456;

/**
 * Notify dashboard of CLI execution events (fire and forget)
 */
function notifyDashboard(data: Record<string, unknown>): void {
  const payload = JSON.stringify({
    type: 'cli_execution',
    ...data,
    timestamp: new Date().toISOString()
  });

  const req = http.request({
    hostname: 'localhost',
    port: Number(DASHBOARD_PORT),
    path: '/api/hook',
    method: 'POST',
    timeout: 2000, // 2 second timeout to prevent hanging
    agent: false, // Disable Keep-Alive to allow process exit
    headers: {
      'Content-Type': 'application/json',
      'Content-Length': Buffer.byteLength(payload),
      'Connection': 'close' // Ensure connection closes after response
    }
  });

  // Fire and forget - don't block process exit
  req.on('socket', (socket) => {
    socket.unref(); // Allow process to exit even if socket is open
  });
  req.on('error', (err) => {
    if (process.env.DEBUG) console.error('[Dashboard] CLI notification failed:', err.message);
  });
  req.on('timeout', () => {
    req.destroy();
    if (process.env.DEBUG) console.error('[Dashboard] CLI notification timed out');
  });
  req.write(payload);
  req.end();
}

/**
 * Broadcast WebSocket event to Dashboard for real-time streaming
 * Uses specific event types that match frontend handlers
 */
function broadcastStreamEvent(eventType: string, payload: Record<string, unknown>): void {
  if (process.env.DEBUG_CLI_EVENTS) {
    console.error(`[CLI-BROADCAST] START ${eventType} at ${Date.now()}`);
  }
  
  const data = JSON.stringify({
    type: eventType,
    ...payload,
    timestamp: new Date().toISOString()
  });

  const req = http.request({
    hostname: 'localhost',
    port: Number(DASHBOARD_PORT),
    path: '/api/hook',
    method: 'POST',
    timeout: 1000, // Short timeout for streaming
    agent: false, // Disable Keep-Alive to allow process exit
    headers: {
      'Content-Type': 'application/json',
      'Content-Length': Buffer.byteLength(data),
      'Connection': 'close' // Ensure connection closes after response
    }
  });

  // Fire and forget - don't block streaming
  req.on('socket', (socket) => {
    socket.unref();
  });
  req.on('error', () => {
    // Silently ignore - dashboard may not be running
  });
  req.on('timeout', () => {
    req.destroy();
  });
  req.write(data);
  req.end();
  
  if (process.env.DEBUG_CLI_EVENTS) {
    console.error(`[CLI-BROADCAST] END ${eventType} at ${Date.now()}`);
  }
}

interface CliExecOptions {
  prompt?: string; // Prompt via --prompt/-p option (preferred for multi-line)
  file?: string; // Read prompt from file
  tool?: string;
  mode?: string;
  model?: string;
  cd?: string;
  includeDirs?: string;
  // timeout removed - controlled by external caller (bash timeout)
  stream?: boolean; // Enable streaming (default: false, caches output)
  resume?: string | boolean; // true = last, string = execution ID, comma-separated for merge
  id?: string; // Custom execution ID (e.g., IMPL-001-step1)
  noNative?: boolean; // Force prompt concatenation instead of native resume
  cache?: string | boolean; // Cache: true = auto from CONTEXT, string = comma-separated patterns/content
  injectMode?: 'none' | 'full' | 'progressive'; // Inject mode for cached content
  debug?: boolean; // Enable debug logging
  // Codex review options
  uncommitted?: boolean; // Review uncommitted changes (default for review mode)
  base?: string; // Review changes against base branch
  commit?: string; // Review changes from specific commit
  title?: string; // Optional title for review summary
  // Template/Rules options
  rule?: string; // Template name for auto-discovery (defines $PROTO and $TMPL env vars)
  // Claude-specific options
  effort?: string; // Effort level for claude: low, medium, high
  // Output options
  raw?: boolean; // Raw output only (best for piping)
  final?: boolean; // Final agent result only (best for piping)
  toFile?: string; // Save output to file
}

/** Cache configuration parsed from --cache */
interface CacheConfig {
  patterns?: string[];       // @patterns to pack (items starting with @)
  content?: string;          // Additional text content (items not starting with @)
}

interface HistoryOptions {
  limit?: string;
  tool?: string;
  status?: string;
}

interface StorageOptions {
  all?: boolean;
  project?: string;
  cliHistory?: boolean;
  memory?: boolean;
  storageCache?: boolean;
  config?: boolean;
  force?: boolean;
}

interface OutputViewOptions {
  offset?: string;
  limit?: string;
  outputType?: 'stdout' | 'stderr' | 'both';
  turn?: string;
  raw?: boolean;
  final?: boolean; // Explicit --final (strict final result, no parsed/stdout fallback)
  verbose?: boolean; // Show full metadata + raw stdout/stderr
  project?: string; // Optional project path for lookup
}

/**
 * Show storage information and management options
 */
async function storageAction(subAction: string | undefined, options: StorageOptions): Promise<void> {
  switch (subAction) {
    case 'info':
    case undefined:
      await showStorageInfo();
      break;
    case 'clean':
      await cleanStorage(options);
      break;
    case 'config':
      showStorageConfig();
      break;
    default:
      showStorageHelp();
  }
}

/**
 * Show storage information
 */
async function showStorageInfo(): Promise<void> {
  console.log(chalk.bold.cyan('\n  CCW Storage Information\n'));

  const config = getStorageConfig();
  const stats = getStorageStats();

  // Configuration
  console.log(chalk.bold.white('  Location:'));
  console.log(`    ${chalk.cyan(stats.rootPath)}`);
  if (config.isCustom) {
    console.log(chalk.gray(`    (Custom: CCW_DATA_DIR=${config.envVar})`));
  }
  console.log();

  // Summary
  console.log(chalk.bold.white('  Summary:'));
  console.log(`    Total Size:     ${chalk.yellow(formatBytes(stats.totalSize))}`);
  console.log(`    Projects:       ${chalk.yellow(stats.projectCount.toString())}`);
  console.log(`    Global DB:      ${stats.globalDb.exists ? chalk.green(formatBytes(stats.globalDb.size)) : chalk.gray('Not created')}`);
  console.log();

  // Projects breakdown
  if (stats.projects.length > 0) {
    console.log(chalk.bold.white('  Projects:'));
    console.log(chalk.gray('    ID               Size       History    Last Used'));
    console.log(chalk.gray('    ─────────────────────────────────────────────────────'));

    for (const project of stats.projects) {
      const historyInfo = project.cliHistory.recordCount !== undefined
        ? `${project.cliHistory.recordCount} records`
        : (project.cliHistory.exists ? 'Yes' : '-');

      console.log(
        `    ${chalk.dim(project.projectId)}  ` +
        `${formatBytes(project.totalSize).padStart(8)}   ` +
        `${historyInfo.padStart(10)}   ` +
        `${chalk.gray(formatTimeAgo(project.lastModified))}`
      );
    }
    console.log();
  }

  // Usage tips
  console.log(chalk.gray('  Commands:'));
  console.log(chalk.gray('    ccw cli storage clean              Clean all storage'));
  console.log(chalk.gray('    ccw cli storage clean --project <path>  Clean specific project'));
  console.log(chalk.gray('    ccw cli storage config             Show location config'));
  console.log();
}

/**
 * Clean storage
 */
async function cleanStorage(options: StorageOptions): Promise<void> {
  const { all, project, force, cliHistory, memory, storageCache, config } = options;

  // Determine what to clean
  const cleanTypes = {
    cliHistory: cliHistory || (!cliHistory && !memory && !storageCache && !config),
    memory: memory || (!cliHistory && !memory && !storageCache && !config),
    cache: storageCache || (!cliHistory && !memory && !storageCache && !config),
    config: config || false, // Config requires explicit flag
    all: !cliHistory && !memory && !storageCache && !config
  };

  if (project) {
    // Clean specific project
    const projectId = resolveProjectId(project);

    if (!projectExists(projectId)) {
      console.log(chalk.yellow(`\n  No storage found for project: ${project}`));
      console.log(chalk.gray(`  (Project ID: ${projectId})\n`));
      return;
    }

    if (!force) {
      console.log(chalk.bold.yellow('\n  Warning: This will delete storage for project:'));
      console.log(`    Path: ${project}`);
      console.log(`    ID:   ${projectId}`);
      console.log(chalk.gray('\n  Use --force to confirm deletion.\n'));
      return;
    }

    console.log(chalk.bold.cyan('\n  Cleaning project storage...\n'));
    const result = cleanProjectStorage(projectId, cleanTypes);

    if (result.success) {
      console.log(chalk.green(`  ✓ Cleaned ${formatBytes(result.freedBytes)}`));
    } else {
      console.log(chalk.red('  ✗ Cleanup completed with errors:'));
      for (const err of result.errors) {
        console.log(chalk.red(`    - ${err}`));
      }
    }
  } else {
    // Clean all storage
    const stats = getStorageStats();

    if (stats.projectCount === 0) {
      console.log(chalk.yellow('\n  No storage to clean.\n'));
      return;
    }

    if (!force) {
      const { proceed } = await inquirer.prompt([{
        type: 'confirm',
        name: 'proceed',
        message: `Delete ALL CCW storage? This will remove ${stats.projectCount} projects (${formatBytes(stats.totalSize)}). This action cannot be undone.`,
        default: false
      }]);

      if (!proceed) {
        console.log(chalk.yellow('\n  Storage clean cancelled.\n'));
        return;
      }
    }

    console.log(chalk.bold.cyan('\n  Cleaning all storage...\n'));
    const result = cleanAllStorage(cleanTypes);

    if (result.success) {
      console.log(chalk.green(`  ✓ Cleaned ${result.projectsCleaned} projects, freed ${formatBytes(result.freedBytes)}`));
    } else {
      console.log(chalk.yellow(`  ⚠ Cleaned ${result.projectsCleaned} projects with some errors:`));
      for (const err of result.errors) {
        console.log(chalk.red(`    - ${err}`));
      }
    }
  }
  console.log();
}

/**
 * Show storage configuration
 */
function showStorageConfig(): void {
  console.log(getStorageLocationInstructions());
}

/**
 * Show storage help
 */
function showStorageHelp(): void {
  console.log(chalk.bold.cyan('\n  CCW Storage Management\n'));
  console.log('  Subcommands:');
  console.log(chalk.gray('    info                Show storage information (default)'));
  console.log(chalk.gray('    clean               Clean storage'));
  console.log(chalk.gray('    config              Show configuration instructions'));
  console.log();
  console.log('  Clean Options:');
  console.log(chalk.gray('    --project <path>    Clean specific project storage'));
  console.log(chalk.gray('    --force             Confirm deletion'));
  console.log(chalk.gray('    --cli-history       Clean only CLI history'));
  console.log(chalk.gray('    --memory            Clean only memory store'));
  console.log(chalk.gray('    --cache             Clean only cache'));
  console.log(chalk.gray('    --config            Clean config (requires explicit flag)'));
  console.log();
  console.log('  Examples:');
  console.log(chalk.gray('    ccw cli storage                           # Show storage info'));
  console.log(chalk.gray('    ccw cli storage clean --force             # Clean all storage'));
  console.log(chalk.gray('    ccw cli storage clean --project . --force # Clean current project'));
  console.log(chalk.gray('    ccw cli storage config                    # Show config instructions'));
  console.log();
}

/**
 * Show cached output for a conversation with pagination
 * Supports automatic discovery of project path from current directory or parents
 */
async function outputAction(conversationId: string | undefined, options: OutputViewOptions): Promise<void> {
  if (!conversationId) {
    console.error(chalk.red('Error: Conversation ID is required'));
    console.error(chalk.gray('Usage: ccw cli output <conversation-id> [--offset N] [--limit N] [--project <path>]'));
    process.exit(1);
  }

  // Determine project path to use
  let projectPath = options.project || process.cwd();
  let store = getHistoryStore(projectPath);

  // Try to get result from specified/current directory
  let result = store.getCachedOutput(
    conversationId,
    options.turn ? parseInt(options.turn) : undefined,
    {
      offset: parseInt(options.offset || '0'),
      limit: parseInt(options.limit || '10000'),
      outputType: options.outputType || 'both'
    }
  );

  // If not found and no explicit project specified, try to find it
  if (!result && !options.project) {
    const found = findProjectWithExecution(conversationId, process.cwd());
    if (found) {
      projectPath = found.projectPath;
      store = getHistoryStore(projectPath);
      result = store.getCachedOutput(
        conversationId,
        options.turn ? parseInt(options.turn) : undefined,
        {
          offset: parseInt(options.offset || '0'),
          limit: parseInt(options.limit || '10000'),
          outputType: options.outputType || 'both'
        }
      );
    }
  }

  if (!result) {
    const hint = options.project
      ? `in project: ${options.project}`
      : 'in registered CCW project history';
    console.error(chalk.red(`Error: Execution not found: ${conversationId}`));
    console.error(chalk.gray(`  Searched ${hint}`));
    console.error(chalk.gray('  Tip: use the real CCW execution ID, not an outer task label.'));
    console.error(chalk.gray('  Capture [CCW_EXEC_ID=...] from stderr, or start with --id <your-id>.'));
    console.error(chalk.gray('  Discover IDs via: ccw cli show or ccw cli history'));
    console.error(chalk.gray('Usage: ccw cli output <conversation-id> [--project <path>]'));
    process.exit(1);
    return;
  }

  if (options.raw) {
    // Raw output only (for piping) — unprocessed stdout
    if (result.stdout) console.log(result.stdout.content);
    return;
  }

  if (options.verbose) {
    // Verbose: full metadata + raw stdout/stderr (for debugging)
    console.log(chalk.bold.cyan('Execution Output\n'));
    console.log(`  ${chalk.gray('ID:')}        ${result.conversationId}`);
    console.log(`  ${chalk.gray('Turn:')}      ${result.turnNumber}`);
    console.log(`  ${chalk.gray('Cached:')}    ${result.cached ? chalk.green('Yes') : chalk.yellow('No')}`);
    console.log(`  ${chalk.gray('Status:')}    ${result.status}`);
    console.log(`  ${chalk.gray('Time:')}      ${result.timestamp}`);
    console.log(`  ${chalk.gray('Project:')}   ${chalk.cyan(projectPath)}`);
    console.log();

    if (result.stdout) {
      console.log(`  ${chalk.gray('Stdout:')} (${result.stdout.totalBytes} bytes, offset ${result.stdout.offset})`);
      console.log(chalk.gray('  ' + '-'.repeat(60)));
      console.log(result.stdout.content);
      console.log(chalk.gray('  ' + '-'.repeat(60)));
      if (result.stdout.hasMore) {
        console.log(chalk.yellow(`  ... ${result.stdout.totalBytes - result.stdout.offset - result.stdout.content.length} more bytes available`));
        console.log(chalk.gray(`  Use --offset ${result.stdout.offset + result.stdout.content.length} to continue`));
      }
      console.log();
    }

    if (result.stderr && result.stderr.content) {
      console.log(`  ${chalk.gray('Stderr:')} (${result.stderr.totalBytes} bytes, offset ${result.stderr.offset})`);
      console.log(chalk.gray('  ' + '-'.repeat(60)));
      console.log(result.stderr.content);
      console.log(chalk.gray('  ' + '-'.repeat(60)));
      if (result.stderr.hasMore) {
        console.log(chalk.yellow(`  ... ${result.stderr.totalBytes - result.stderr.offset - result.stderr.content.length} more bytes available`));
      }
      console.log();
    }
    return;
  }

  const finalOutputContent = result.finalOutput?.content;

  if (options.final) {
    if (finalOutputContent !== undefined) {
      console.log(finalOutputContent);
      return;
    }

    console.error(chalk.yellow('No final agent result found in cached output.'));
    console.error(chalk.gray('  Try without --final for best-effort output, or use --verbose to inspect raw stdout/stderr.'));
    process.exit(1);
    return;
  }

  // Default output: prefer strict final result, then fall back to best-effort parsed/plain output.
  const outputContent = finalOutputContent ?? result.parsedOutput?.content ?? result.stdout?.content;
  if (outputContent !== undefined) {
    console.log(outputContent);
  }
}

/**
 * Test endpoint for debugging multi-line prompt parsing
 * Shows exactly how Commander.js parsed the arguments
 */
function testParseAction(args: string[], options: CliExecOptions): void {
  console.log(chalk.bold.cyan('\n  ═══════════════════════════════════════════════'));
  console.log(chalk.bold.cyan('  │       CLI PARSE TEST ENDPOINT              │'));
  console.log(chalk.bold.cyan('  ═══════════════════════════════════════════════\n'));

  // Debug: show raw options.prompt with JSON.stringify to reveal hidden characters
  console.log(chalk.bold.yellow('🔬 RAW OPTIONS.PROMPT (JSON):'));
  console.log(chalk.cyan('   ' + JSON.stringify(options.prompt)));
  console.log();

  // Show args array parsing
  console.log(chalk.bold.yellow('📦 Positional Arguments (args[]):'));
  console.log(chalk.gray('   Length: ') + chalk.white(args.length));
  if (args.length === 0) {
    console.log(chalk.gray('   (empty)'));
  } else {
    args.forEach((arg, i) => {
      console.log(chalk.gray(`   [${i}]: `) + chalk.green(`"${arg}"`));
      if (arg.includes('\n')) {
        console.log(chalk.yellow(`        ↳ Contains ${arg.split('\n').length} lines`));
      }
    });
  }

  console.log();

  // Show options parsing
  console.log(chalk.bold.yellow('⚙️  Options:'));
  const optionEntries = Object.entries(options).filter(([_, v]) => v !== undefined);
  if (optionEntries.length === 0) {
    console.log(chalk.gray('   (none)'));
  } else {
    optionEntries.forEach(([key, value]) => {
      const displayValue = typeof value === 'string' && value.includes('\n')
        ? `"${value.substring(0, 50)}..." (${value.split('\n').length} lines)`
        : JSON.stringify(value);
      console.log(chalk.gray(`   --${key}: `) + chalk.cyan(displayValue));
    });
  }

  console.log();

  // Show what would be used as prompt
  console.log(chalk.bold.yellow('🎯 Final Prompt Resolution:'));
  const { prompt: optionPrompt, file } = options;

  if (file) {
    console.log(chalk.gray('   Source: ') + chalk.magenta('--file/-f option'));
    console.log(chalk.gray('   File: ') + chalk.cyan(file));
  } else if (optionPrompt) {
    console.log(chalk.gray('   Source: ') + chalk.magenta('--prompt/-p option'));
    console.log(chalk.gray('   Value: ') + chalk.green(`"${optionPrompt.substring(0, 100)}${optionPrompt.length > 100 ? '...' : ''}"`));
    if (optionPrompt.includes('\n')) {
      console.log(chalk.yellow(`   ↳ Multiline: ${optionPrompt.split('\n').length} lines`));
    }
  } else if (args[0]) {
    console.log(chalk.gray('   Source: ') + chalk.magenta('positional argument (args[0])'));
    console.log(chalk.gray('   Value: ') + chalk.green(`"${args[0].substring(0, 100)}${args[0].length > 100 ? '...' : ''}"`));
    if (args[0].includes('\n')) {
      console.log(chalk.yellow(`   ↳ Multiline: ${args[0].split('\n').length} lines`));
    }
  } else {
    console.log(chalk.red('   No prompt found!'));
  }

  console.log();

  // Show raw debug info
  console.log(chalk.bold.yellow('🔍 Raw Debug Info:'));
  console.log(chalk.gray('   process.argv:'));
  process.argv.forEach((arg, i) => {
    console.log(chalk.gray(`     [${i}]: `) + chalk.dim(arg.length > 60 ? arg.substring(0, 60) + '...' : arg));
  });

  console.log(chalk.bold.cyan('\n  ═══════════════════════════════════════════════\n'));
}

/**
 * Show CLI tool status
 */
async function statusAction(debug?: boolean): Promise<void> {
  // Enable debug mode if --debug flag is set
  if (debug) {
    process.env.DEBUG = 'true';
    console.log(chalk.yellow('  Debug mode enabled\n'));
  }

  console.log(chalk.bold.cyan('\n  CLI Tools Status\n'));

  const status = await getCliToolsStatus();

  for (const [tool, info] of Object.entries(status)) {
    const statusIcon = info.available ? chalk.green('●') : chalk.red('○');
    const statusText = info.available ? chalk.green('Available') : chalk.red('Not Found');

    console.log(`  ${statusIcon} ${chalk.bold.white(tool.padEnd(10))} ${statusText}`);
    if (info.available && info.path) {
      console.log(chalk.gray(`      ${info.path}`));
    }
  }

  console.log();
}

/**
 * Execute a CLI tool
 * @param {string} prompt - Prompt to execute
 * @param {Object} options - CLI options
 */
async function execAction(positionalPrompt: string | undefined, options: CliExecOptions): Promise<void> {
  const {
    prompt: optionPrompt,
    file,
    tool: userTool,
    mode = 'analysis',
    model,
    cd,
    includeDirs,
    stream,
    resume,
    id,
    noNative,
    cache,
    injectMode,
    debug,
    uncommitted,
    base,
    commit,
    title,
    rule,
    effort,
    toFile,
    raw,
    final: finalOnly,
  } = options;

  // Determine the tool to use: explicit --tool option, or defaultTool from config
  let tool = userTool;
  if (!tool) {
    try {
      const settings = loadClaudeCliSettings(cd || process.cwd());
      tool = settings.defaultTool || 'gemini';
    } catch {
      // Fallback to gemini if config cannot be loaded
      tool = 'gemini';
    }
  }

  // Enable debug mode if --debug flag is set
  if (debug) {
    process.env.DEBUG = 'true';
    console.log(chalk.yellow('  Debug mode enabled\n'));
  }

  // Priority: 1. --file, 2. --prompt/-p option, 3. positional argument, 4. stdin (piped)
  // IMPORTANT: In host CLIs, stdin may be non-TTY and kept open. Reading fd 0 first can block.
  // Note: On Windows, quoted arguments like -p "say hello" may be split into
  // -p "say" and positional "hello". We merge them back together.
  let finalPrompt: string | undefined;

  if (file) {
    // Read from file
    const { readFileSync, existsSync } = await import('fs');
    const { resolve } = await import('path');
    const filePath = resolve(file);
    if (!existsSync(filePath)) {
      console.error(chalk.red(`Error: File not found: ${filePath}`));
      process.exit(1);
    }
    finalPrompt = readFileSync(filePath, 'utf8').trim();
    if (!finalPrompt) {
      console.error(chalk.red('Error: File is empty'));
      process.exit(1);
    }
  } else {
    if (optionPrompt) {
      // Use --prompt/-p option (preferred for multi-line)
      // Merge with positional argument if Windows split the quoted string
      finalPrompt = positionalPrompt ? `${optionPrompt} ${positionalPrompt}` : optionPrompt;
    } else if (positionalPrompt) {
      // Fall back to positional argument
      finalPrompt = positionalPrompt;
    } else if (!process.stdin.isTTY) {
      // Read from stdin only when no explicit prompt input is provided
      // (enables: echo "prompt" | ccw cli --tool gemini)
      const { readFileSync } = await import('fs');
      try {
        finalPrompt = readFileSync(0, 'utf8').trim(); // fd 0 = stdin
        if (debug) {
          console.log(chalk.gray(`  Read ${finalPrompt.length} chars from stdin`));
        }
      } catch {
        // stdin not available or empty, keep finalPrompt undefined
      }
    }
  }

  // Prompt is required unless resuming OR using review mode with target flags
  // codex review: --uncommitted, --base, --commit don't require a prompt
  const isReviewWithTarget = mode === 'review' && (uncommitted || base || commit);

  // Conflict detection: prompt + review target flags are mutually exclusive
  // When both provided, warn user and prioritize target flag (drop prompt)
  if (isReviewWithTarget && finalPrompt && tool === 'codex') {
    const targetFlag = uncommitted ? '--uncommitted' : (base ? `--base ${base}` : `--commit ${commit}`);
    console.log(chalk.yellow('Warning: Prompt is ignored when review target flag is specified'));
    console.log(chalk.gray(`  Using: ${targetFlag} (prompt will be dropped)`));
    console.log(chalk.gray('  Valid combinations:'));
    console.log(chalk.gray('    • ccw cli -p "..." --tool codex --mode review           # prompt only'));
    console.log(chalk.gray('    • ccw cli --tool codex --mode review --uncommitted       # target only'));
    console.log();
    // Drop prompt to avoid codex CLI argument conflict
    finalPrompt = undefined;
  }

  if (!finalPrompt && !resume && !isReviewWithTarget) {
    console.error(chalk.red('Error: Prompt is required'));
    console.error(chalk.gray('Usage: ccw cli -p "<prompt>" --tool gemini'));
    console.error(chalk.gray('   or: ccw cli -f prompt.txt --tool codex'));
    console.error(chalk.gray('   or: ccw cli --resume --tool gemini'));
    console.error(chalk.gray('   or: ccw cli --tool codex --mode review --uncommitted'));
    process.exit(1);
  }

  const prompt_to_use = finalPrompt || '';

  // Load rules templates (concatenation mode - directly append to prompt)
  // Skip template loading when using target flags with codex review
  // codex review: --uncommitted, --base, --commit are all mutually exclusive with [PROMPT]
  // Default to universal-rigorous-style if --rule not specified
  const skipTemplates = mode === 'review' && (uncommitted || base || commit);
  const effectiveRule = rule || 'universal-rigorous-style';
  let systemRules = '';  // Protocol content
  let roles = '';        // Template content

  if (skipTemplates) {
    if (debug) {
      console.log(chalk.gray(`  Skipping templates: --commit with review mode doesn't support prompt`));
    }
  } else {
    try {
      const { loadProtocol, loadTemplate } = await import('../tools/template-discovery.js');
      const proto = loadProtocol(mode);
      const tmpl = loadTemplate(effectiveRule);
      if (proto) systemRules = proto;
      if (tmpl) roles = tmpl;
      if (debug) {
        console.log(chalk.gray(`  Rule loaded: ${effectiveRule}${!rule ? ' (default)' : ''}`));
        console.log(chalk.gray(`  systemRules(${systemRules.length} chars) + roles(${roles.length} chars)`));
        console.log(chalk.gray(`  Rules will be appended to prompt automatically`));
      }
    } catch (error) {
      console.error(chalk.red(`Error loading rule template: ${error instanceof Error ? error.message : error}`));
      process.exit(1);
    }
  }

  // Handle cache option: pack @patterns and/or content
  let cacheSessionId: string | undefined;
  // When skipTemplates is true (review mode with target flags), don't pass prompt to codex CLI
  // because --uncommitted/--base/--commit and [PROMPT] are mutually exclusive
  let actualPrompt = skipTemplates ? '' : prompt_to_use;

  if (cache) {
    const { handler: contextCacheHandler } = await import('../tools/context-cache.js');

    // Parse cache config from comma-separated string
    // Items starting with @ are patterns, others are text content
    let cacheConfig: CacheConfig = {};

    if (cache === true) {
      // --cache without value: auto-extract from CONTEXT field
      const contextMatch = prompt_to_use.match(/CONTEXT:\s*([^\n]+)/i);
      if (contextMatch) {
        const contextLine = contextMatch[1];
        const patternMatches = contextLine.matchAll(/@[^\s|]+/g);
        cacheConfig.patterns = Array.from(patternMatches).map(m => m[0]);
      }
    } else if (typeof cache === 'string') {
      // Parse comma-separated items: @patterns and text content
      const items = cache.split(',').map(s => s.trim()).filter(Boolean);
      const patterns: string[] = [];
      const contentParts: string[] = [];

      for (const item of items) {
        if (item.startsWith('@')) {
          patterns.push(item);
        } else {
          contentParts.push(item);
        }
      }

      if (patterns.length > 0) {
        cacheConfig.patterns = patterns;
      }
      if (contentParts.length > 0) {
        cacheConfig.content = contentParts.join('\n');
      }
    }

    // Also extract patterns from CONTEXT if not provided
    if ((!cacheConfig.patterns || cacheConfig.patterns.length === 0) && prompt_to_use) {
      const contextMatch = prompt_to_use.match(/CONTEXT:\s*([^\n]+)/i);
      if (contextMatch) {
        const contextLine = contextMatch[1];
        const patternMatches = contextLine.matchAll(/@[^\s|]+/g);
        cacheConfig.patterns = Array.from(patternMatches).map(m => m[0]);
      }
    }

    // Pack if we have patterns or content
    if ((cacheConfig.patterns && cacheConfig.patterns.length > 0) || cacheConfig.content) {
      const patternCount = cacheConfig.patterns?.length || 0;
      const hasContent = !!cacheConfig.content;
      console.log(chalk.gray(`  Caching: ${patternCount} pattern(s)${hasContent ? ' + text content' : ''}...`));

      const cacheResult = await contextCacheHandler({
        operation: 'pack',
        patterns: cacheConfig.patterns,
        content: cacheConfig.content,
        cwd: cd || process.cwd(),
        include_dirs: includeDirs ? includeDirs.split(',') : undefined,
      });

      if (cacheResult.success && cacheResult.result) {
        const packResult = cacheResult.result as { session_id: string; files_packed: number; total_bytes: number };
        cacheSessionId = packResult.session_id;
        console.log(chalk.gray(`  Cached: ${packResult.files_packed} files, ${packResult.total_bytes} bytes`));
        console.log(chalk.gray(`  Session: ${cacheSessionId}`));

        // Determine inject mode:
        // --inject-mode explicitly set > tool default (codex=full, others=none)
        const effectiveInjectMode = injectMode ?? (tool === 'codex' ? 'full' : 'none');

        if (effectiveInjectMode !== 'none' && cacheSessionId) {
          if (effectiveInjectMode === 'full') {
            // Read full cache content
            const readResult = await contextCacheHandler({
              operation: 'read',
              session_id: cacheSessionId,
              offset: 0,
              limit: 1024 * 1024, // 1MB max
            });

            if (readResult.success && readResult.result) {
              const { content: cachedContent, total_bytes } = readResult.result as { content: string; total_bytes: number };
              console.log(chalk.gray(`  Injecting ${total_bytes} bytes (full mode)...`));
              actualPrompt = `=== CACHED CONTEXT (${packResult.files_packed} files) ===\n${cachedContent}\n\n=== USER PROMPT ===\n${prompt_to_use}`;
            }
          } else if (effectiveInjectMode === 'progressive') {
            // Progressive mode: read first page only (64KB default)
            const pageLimit = 65536;
            const readResult = await contextCacheHandler({
              operation: 'read',
              session_id: cacheSessionId,
              offset: 0,
              limit: pageLimit,
            });

            if (readResult.success && readResult.result) {
              const { content: cachedContent, total_bytes, has_more, next_offset } = readResult.result as {
                content: string; total_bytes: number; has_more: boolean; next_offset: number | null
              };
              console.log(chalk.gray(`  Injecting ${cachedContent.length}/${total_bytes} bytes (progressive mode)...`));

              const moreInfo = has_more
                ? `\n[... ${total_bytes - cachedContent.length} more bytes available via: context_cache(operation="read", session_id="${cacheSessionId}", offset=${next_offset}) ...]`
                : '';

              actualPrompt = `=== CACHED CONTEXT (${packResult.files_packed} files, progressive) ===\n${cachedContent}${moreInfo}\n\n=== USER PROMPT ===\n${prompt_to_use}`;
            }
          }
        }

        console.log();
      } else {
        console.log(chalk.yellow(`  Cache warning: ${cacheResult.error}`));
      }
    }
  }

  // Concatenate systemRules and roles to the end of prompt (if loaded)
  // Format: [USER_PROMPT]\n\nProtocol (mode): [content]\n\nTemplate (rule): [content]
  // Skip concatenation when using --commit with review mode (prompt not allowed)
  if (!skipTemplates && (systemRules || roles)) {
    const parts: string[] = [actualPrompt];
    if (systemRules) {
      parts.push(`Protocol (${mode}):\n${systemRules}`);
    }
    if (roles) {
      parts.push(`Template (${effectiveRule}):\n${roles}`);
    }
    actualPrompt = parts.join('\n\n');

    if (debug) {
      console.log(chalk.gray(`  Prompt structure: USER_PROMPT(${prompt_to_use.length}) + Protocol(${systemRules.length}) + Template(${roles.length})`));
      console.log(chalk.gray(`  Total prompt length: ${actualPrompt.length} chars`));
    }
  }

  // Parse resume IDs for merge scenario
  const resumeIds = resume && typeof resume === 'string' ? resume.split(',').map(s => s.trim()).filter(Boolean) : [];
  const isMerge = resumeIds.length > 1;

  // Show execution mode
  let resumeInfo = '';
  if (isMerge) {
    resumeInfo = ` merging ${resumeIds.length} conversations`;
  } else if (resume) {
    resumeInfo = typeof resume === 'string' ? ` resuming ${resume}` : ' resuming last';
  }
  const nativeMode = noNative ? ' (prompt-concat)' : '';
  const idInfo = id ? ` [${id}]` : '';

  const autoStream = !stream
    && !raw
    && !finalOnly
    && !process.stdout.isTTY
    && Boolean(process.env.CLAUDECODE);
  const effectiveStream = Boolean(stream || autoStream);
  const shouldPassthroughCodexJsonl = effectiveStream
    && tool === 'codex'
    && !raw
    && !finalOnly
    && !process.stdout.isTTY
    && Boolean(process.env.CLAUDECODE);

  // Programmatic output mode:
  // - `--raw`: stdout/stderr passthrough semantics (minimal noise)
  // - `--final`: agent-message only semantics (minimal noise)
  // - non-TTY stdout (e.g. called from another process): default to final-only unless streaming is enabled
  const programmaticOutput = shouldPassthroughCodexJsonl
    || Boolean(raw || finalOnly)
    || (!process.stdout.isTTY && !effectiveStream);
  const showUi = !programmaticOutput;
  const useRawOutput = Boolean(raw);
  const useFinalOnlyOutput = Boolean(finalOnly) || (!useRawOutput && !process.stdout.isTTY && !effectiveStream);

  // Show merge details
  if (isMerge && showUi) {
    console.log(chalk.gray('  Merging conversations:'));
    for (const rid of resumeIds) {
      console.log(chalk.gray(`    • ${rid}`));
    }
    console.log();
  }

  // Generate execution ID for streaming (use custom ID or auto-generated readable ID)
  if (!id) {
    console.error(chalk.yellow('[WARN] --id not provided. Use --id <id> for reliable tracking. Auto-generating ID.'));
  }
  const executionId = id || generateExecutionId(tool);
  const startTime = Date.now();
  const modelInfo = model ? ` @${model}` : '';
  const spinnerBaseText = `Executing ${tool}${modelInfo} (${mode} mode${resumeInfo}${nativeMode})${idInfo}...`;
  if (showUi) {
    console.log();
  }

  const spinner = (showUi && !effectiveStream) ? createSpinner(`  ${spinnerBaseText}`).start() : null;
  const elapsedInterval = spinner
    ? setInterval(() => {
      const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
      spinner.text = `  ${spinnerBaseText} (${elapsedSeconds}s elapsed)`;
    }, 1000)
    : null;
  elapsedInterval?.unref?.();

  if (showUi && !spinner) {
    console.log(chalk.cyan(`  ${spinnerBaseText}\n`));
  }

  // Handle process interruption (SIGINT/SIGTERM) to notify dashboard
  const handleInterrupt = (signal: string) => {
    const duration = Date.now() - startTime;
    if (elapsedInterval) clearInterval(elapsedInterval);
    if (spinner) {
      spinner.warn(`Interrupted by ${signal} (${Math.floor(duration / 1000)}s elapsed)`);
    } else if (showUi) {
      console.log(chalk.yellow(`\n  Interrupted by ${signal}`));
    }

    // Kill child process (gemini/codex/qwen CLI) if running
    killCurrentCliProcess();

    // Broadcast interruption to dashboard
    broadcastStreamEvent('CLI_EXECUTION_COMPLETED', {
      executionId,
      success: false,
      duration,
      interrupted: true
    });

    // Give time for the event to be sent before exiting
    setTimeout(() => process.exit(130), 100);
  };

  process.on('SIGINT', () => handleInterrupt('SIGINT'));
  process.on('SIGTERM', () => handleInterrupt('SIGTERM'));

  // Notify dashboard: execution started (legacy)
  notifyDashboard({
    event: 'started',
    tool,
    mode,
    prompt_preview: prompt_to_use.substring(0, 100) + (prompt_to_use.length > 100 ? '...' : ''),
    custom_id: id || null
  });

  // Broadcast CLI_EXECUTION_STARTED for real-time streaming viewer
  // Note: /api/hook wraps extraData into payload, so send fields directly
  broadcastStreamEvent('CLI_EXECUTION_STARTED', {
    executionId,
    tool,
    mode
  });

  // Always output execution ID to stderr for programmatic capture
  // Callers can: ccw cli -p "..." 2>&1 | grep CCW_EXEC_ID
  console.error(`[CCW_EXEC_ID=${executionId}]`);

  // Buffer to accumulate output when both --stream and --to-file are specified
  let streamBuffer = '';

  const shouldSkipCodexPassthroughLine = (rawLine: string): boolean => {
    try {
      const parsed = JSON.parse(rawLine) as {
        type?: string;
        item?: { type?: string };
        item_type?: string;
      };
      if (!parsed || typeof parsed !== 'object') return false;
      const eventType = parsed.type || '';
      const itemType = parsed.item?.type || parsed.item_type || '';
      return eventType.startsWith('item.') && itemType === 'command_execution';
    } catch {
      return false;
    }
  };

  // Streaming output handler - broadcasts to dashboard AND writes to stdout
  const onOutput = (unit: CliOutputUnit) => {
    // Always broadcast to dashboard for real-time viewing
    // Note: /api/hook wraps extraData into payload, so send fields directly
    // Maintain backward compatibility with frontend expecting { chunkType, data }
    // Use SmartContentFormatter for intelligent content formatting (never returns null)
    const content = SmartContentFormatter.format(unit.content, unit.type);
    broadcastStreamEvent('CLI_OUTPUT', {
      executionId,
      chunkType: unit.type,  // For backward compatibility
      data: content,         // For backward compatibility (now formatted)
      unit                   // New structured format
    });

    // Write to terminal when streaming is enabled (explicit --stream or auto-stream)
    if (effectiveStream) {
      if (shouldPassthroughCodexJsonl && unit.rawLine) {
        if (shouldSkipCodexPassthroughLine(unit.rawLine)) {
          return;
        }
        const line = `${unit.rawLine}\n`;
        process.stdout.write(line);
        if (toFile) streamBuffer += line;
        return;
      }
      switch (unit.type) {
        case 'stdout':
        case 'code':
        case 'streaming_content':  // Show streaming delta content in real-time
          const content1 = typeof unit.content === 'string' ? unit.content : JSON.stringify(unit.content);
          process.stdout.write(content1);
          if (toFile) streamBuffer += content1;
          break;
        case 'stderr':
          const content2 = typeof unit.content === 'string' ? unit.content : JSON.stringify(unit.content);
          process.stderr.write(content2);
          if (toFile) streamBuffer += content2;
          break;
        case 'thought':
          // Optional: display thinking process with different color
          // For now, skip to reduce noise
          break;
        case 'progress':
          // Optional: update progress bar
          // For now, skip
          break;
        default:
          // Other types: output content if available
          if (unit.content) {
            const content3 = typeof unit.content === 'string' ? unit.content : '';
            process.stdout.write(content3);
            if (toFile) streamBuffer += content3;
          }
      }
    }
  };

  // Use JSON-lines parsing by default to enable type badges (thought, code, file_diff, etc.)
  // All CLI tools may output structured JSON that can be parsed for richer UI
  const outputFormat = 'json-lines';

  try {
    const result = await cliExecutorTool.execute({
      tool,
      prompt: actualPrompt,
      mode,
      model,
      cd,
      includeDirs,
      // timeout removed - controlled by external caller (bash timeout)
      resume,
      id: executionId, // unified execution ID (matches broadcast events)
      noNative,
      stream: effectiveStream, // stream=true → streaming enabled (no cache), stream=false → cache output (default)
      outputFormat, // Enable JSONL parsing for tools that support it
      // Codex review options
      uncommitted,
      base,
      commit,
      title,
      effort
      // Rules are now concatenated directly into prompt (no env vars)
    }, onOutput); // Always pass onOutput for real-time dashboard streaming

    if (elapsedInterval) clearInterval(elapsedInterval);
    if (spinner) {
      const durationSeconds = (result.execution.duration_ms / 1000).toFixed(1);
      const turnInfo = result.success && result.conversation.turn_count > 1
        ? ` (turn ${result.conversation.turn_count})`
        : '';
      if (result.success) {
        spinner.succeed(`Completed in ${durationSeconds}s${turnInfo}`);
      } else {
        spinner.fail(`Failed after ${durationSeconds}s`);
      }
    }

    // If not streaming (default), print output now
    // Prefer parsedOutput (from stream parser) over raw stdout for better formatting
    if (!effectiveStream) {
      const output = useRawOutput
        ? result.stdout
        : (useFinalOnlyOutput ? (result.finalOutput || result.parsedOutput || result.stdout) : (result.parsedOutput || result.stdout));
      if (output) {
        if (programmaticOutput) {
          process.stdout.write(output);
        } else {
          console.log(output);
        }

        // Save to file if --to-file is specified
        if (toFile) {
          try {
            const { writeFileSync, mkdirSync } = await import('fs');
            const { dirname, resolve } = await import('path');
            const filePath = resolve(cd || process.cwd(), toFile);
            const dir = dirname(filePath);
            mkdirSync(dir, { recursive: true });
            writeFileSync(filePath, output, 'utf8');
            if (debug) {
              console.log(chalk.gray(`  Saved output to: ${filePath}`));
            }
          } catch (err) {
            console.error(chalk.red(`  Error saving to file: ${(err as Error).message}`));
          }
        }
      }
    }

    // Print summary with execution ID and turn info (interactive mode only)
    if (showUi) {
      console.log();
    }

    if (result.success) {
      // Save streaming output to file if needed
      if (effectiveStream && toFile && streamBuffer) {
        try {
          const { writeFileSync, mkdirSync } = await import('fs');
          const { dirname, resolve } = await import('path');
          const filePath = resolve(cd || process.cwd(), toFile);
          const dir = dirname(filePath);
          mkdirSync(dir, { recursive: true });
          writeFileSync(filePath, streamBuffer, 'utf8');
        } catch (err) {
          console.error(chalk.red(`  Error saving to file: ${(err as Error).message}`));
        }
      }

      if (showUi && !spinner) {
        const turnInfo = result.conversation.turn_count > 1
          ? ` (turn ${result.conversation.turn_count})`
          : '';
        console.log(chalk.green(`  ✓ Completed in ${(result.execution.duration_ms / 1000).toFixed(1)}s${turnInfo}`));
      }

      if (showUi) {
        console.log(chalk.gray(`  ID: ${result.execution.id}`));
        if (isMerge && !id) {
          // Merge without custom ID: updated all source conversations
          console.log(chalk.gray(`  Updated ${resumeIds.length} conversations: ${resumeIds.join(', ')}`));
        } else if (isMerge && id) {
          // Merge with custom ID: created new merged conversation
          console.log(chalk.gray(`  Created merged conversation from ${resumeIds.length} sources`));
        }
        if (result.conversation.turn_count > 1) {
          console.log(chalk.gray(`  Total: ${result.conversation.turn_count} turns, ${(result.conversation.total_duration_ms / 1000).toFixed(1)}s`));
        }
        console.log(chalk.dim(`  Continue: ccw cli -p "..." --resume ${result.execution.id}`));
        if (!effectiveStream) {
          console.log(chalk.dim(`  Output: ccw cli output ${result.execution.id}`));
        }
        if (toFile) {
          const { resolve } = await import('path');
          const filePath = resolve(cd || process.cwd(), toFile);
          console.log(chalk.green(`  Saved to: ${filePath}`));
        }
      }

      // Notify dashboard: execution completed (legacy)
      notifyDashboard({
        event: 'completed',
        tool,
        mode,
        execution_id: result.execution.id,
        success: true,
        duration_ms: result.execution.duration_ms,
        turn_count: result.conversation.turn_count
      });

      // Broadcast CLI_EXECUTION_COMPLETED for real-time streaming viewer
      if (process.env.DEBUG_CLI_EVENTS) {
        console.error(`[CLI-TIMING] Broadcasting CLI_EXECUTION_COMPLETED at ${Date.now()}`);
      }
      broadcastStreamEvent('CLI_EXECUTION_COMPLETED', {
        executionId,  // Use the same executionId as started event
        success: true,
        duration: result.execution.duration_ms
      });
      if (process.env.DEBUG_CLI_EVENTS) {
        console.error(`[CLI-TIMING] Broadcast returned, setting timeout at ${Date.now()}`);
      }

      // Ensure clean exit after successful execution
      // Delay to allow HTTP request to complete
      // FIX: Increased from 150ms to 500ms for long-running executions
      setTimeout(() => {
        if (process.env.DEBUG_CLI_EVENTS) {
          console.error(`[CLI-TIMING] process.exit(0) at ${Date.now()}`);
        }
        process.exit(0);
      }, 500);
    } else {
      if (!showUi) {
        // Programmatic mode: avoid banners/hints; write stderr only if available.
        if (result.stderr) {
          process.stderr.write(result.stderr);
        }
      } else {
        if (!spinner) {
          console.log(chalk.red(`  ✗ Failed (${result.execution.status})`));
        }
        console.log(chalk.gray(`  ID: ${result.execution.id}`));
        console.log(chalk.gray(`  Duration: ${(result.execution.duration_ms / 1000).toFixed(1)}s`));
        console.log(chalk.gray(`  Exit Code: ${result.execution.exit_code}`));

        // Show stderr with better formatting
        if (result.stderr) {
          console.log();
          console.log(chalk.red.bold('  Error Output:'));
          console.log(chalk.gray('  ' + '─'.repeat(60)));
          // Indent stderr for better readability
          const stderrLines = result.stderr.split('\n');
          for (const line of stderrLines.slice(0, 30)) { // Limit to 30 lines
            console.error(chalk.red(`  ${line}`));
          }
          if (stderrLines.length > 30) {
            console.log(chalk.yellow(`  ... ${stderrLines.length - 30} more lines`));
            console.log(chalk.cyan(`  💡 View full output: ccw cli output ${result.execution.id}`));
            console.log();
          }
          console.log(chalk.gray('  ' + '─'.repeat(60)));
        }

        // Show troubleshooting hints
        console.log();
        console.log(chalk.yellow.bold('  Troubleshooting:'));
        console.log(chalk.gray(`    • Check if ${tool} is properly installed: ccw cli status`));
        console.log(chalk.gray(`    • Enable debug mode: DEBUG=true ccw cli -p "..."  or  set DEBUG=true && ccw cli -p "..."`));
        if (result.stderr?.includes('API key') || result.stderr?.includes('Authentication')) {
          console.log(chalk.gray(`    • Check API key configuration for ${tool}`));
        }
        if (result.stderr?.includes('rate limit')) {
          console.log(chalk.gray(`    • Wait and retry - rate limit exceeded`));
        }
      }

      // Notify dashboard: execution failed (legacy)
      notifyDashboard({
        event: 'completed',
        tool,
        mode,
        execution_id: result.execution.id,
        success: false,
        status: result.execution.status,
        duration_ms: result.execution.duration_ms
      });

      // Broadcast CLI_EXECUTION_COMPLETED for real-time streaming viewer
      broadcastStreamEvent('CLI_EXECUTION_COMPLETED', {
        executionId,  // Use the same executionId as started event
        success: false,
        duration: result.execution.duration_ms
      });

      // Delay to allow HTTP request to complete
      setTimeout(() => process.exit(1), 150);
    }
  } catch (error) {
    const err = error as Error;
    if (elapsedInterval) clearInterval(elapsedInterval);
    if (spinner) spinner.fail('Execution error');
    console.error(chalk.red.bold(`\n  ✗ Execution Error\n`));
    console.error(chalk.red(`  ${err.message}`));

    // Parse error message for additional context
    if (err.message.includes('Failed to spawn')) {
      console.log();
      console.log(chalk.yellow.bold('  Troubleshooting:'));
      console.log(chalk.gray(`    • Check if ${tool} is installed: npm ls -g @google/gemini-cli (or qwen/codex)`));
      console.log(chalk.gray(`    • Verify PATH includes npm global bin directory`));
      console.log(chalk.gray(`    • Run: ccw cli status`));
      console.log(chalk.gray(`    • Enable debug mode: DEBUG=true ccw cli -p "..."`));
    } else if (err.message.includes('not available')) {
      console.log();
      console.log(chalk.yellow.bold('  Troubleshooting:'));
      console.log(chalk.gray(`    • Install the tool: npm install -g <package-name>`));
      console.log(chalk.gray(`    • Run: ccw cli status`));
    }

    // Notify dashboard: execution error (legacy)
    notifyDashboard({
      event: 'error',
      tool,
      mode,
      error: err.message
    });

    // Broadcast CLI_EXECUTION_ERROR for real-time streaming viewer
    broadcastStreamEvent('CLI_EXECUTION_ERROR', {
      executionId,
      error: err.message
    });

    // Delay to allow HTTP request to complete
    setTimeout(() => process.exit(1), 150);
  }
}

/**
 * Show all executions — active (running) + recent completed
 * Combines live dashboard state with SQLite history
 */
async function showAction(options: { all?: boolean }): Promise<void> {
  console.log(chalk.bold.cyan('\n  CLI Executions\n'));

  // 1. Try to fetch active executions from dashboard
  let activeExecs: Array<{
    id: string; tool: string; mode: string; status: string;
    prompt: string; startTime: number | string | Date; isComplete?: boolean;
  }> = [];

  try {
    const data = await new Promise<string>((resolve, reject) => {
      const req = http.request({
        hostname: 'localhost',
        port: Number(DASHBOARD_PORT),
        path: '/api/cli/active',
        method: 'GET',
        timeout: 2000,
        agent: false,
        headers: { 'Connection': 'close' }
      }, (res) => {
        let body = '';
        res.on('data', (chunk: Buffer) => { body += chunk.toString(); });
        res.on('end', () => resolve(body));
      });
      req.on('error', reject);
      req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
      req.end();
    });
    const parsed = JSON.parse(data);
    activeExecs = Array.isArray(parsed) ? parsed : (parsed.executions || []);
  } catch {
    // Dashboard not available — show only history
  }

  // 2. Get recent history from SQLite
  const historyLimit = options.all ? 100 : 20;
  const history = getRegisteredExecutionHistory({ limit: historyLimit });
  const historyById = new Map(history.executions.map(exec => [exec.id, exec]));

  // 3. Build unified list: active first, then history (de-duped)
  const seenIds = new Set<string>();
  const rows: Array<{
    id: string; tool: string; mode: string; status: string;
    prompt: string; time: string; duration: string;
  }> = [];

  // Active executions (running)
  for (const exec of activeExecs) {
    if (exec.status === 'running') {
      const normalizedStartTime = normalizeTimestampMs(exec.startTime);
      const matchingHistory = historyById.get(exec.id);
      const shouldSuppressActiveRow = matchingHistory !== undefined && isSavedExecutionNewerThanActive(
        normalizedStartTime,
        matchingHistory.updated_at || matchingHistory.timestamp
      );

      if (shouldSuppressActiveRow) {
        continue;
      }

      seenIds.add(exec.id);
      rows.push({
        id: exec.id,
        tool: exec.tool,
        mode: exec.mode,
        status: 'running',
        prompt: (exec.prompt || '').replace(/\n/g, ' ').substring(0, 50),
        time: normalizedStartTime !== undefined ? getTimeAgo(new Date(normalizedStartTime)) : 'unknown',
        duration: normalizedStartTime !== undefined ? formatRunningDuration(Date.now() - normalizedStartTime) : 'running',
      });
    }
  }

  // History executions
  for (const exec of history.executions) {
    if (seenIds.has(exec.id)) continue;
    seenIds.add(exec.id);
    const duration = exec.duration_ms >= 1000
      ? `${(exec.duration_ms / 1000).toFixed(1)}s`
      : `${exec.duration_ms}ms`;
    const timeAgo = getTimeAgo(new Date(exec.updated_at || exec.timestamp));
    rows.push({
      id: exec.id,
      tool: exec.tool,
      mode: '-',
      status: exec.status,
      prompt: exec.prompt_preview.replace(/\n/g, ' ').substring(0, 50),
      time: timeAgo,
      duration,
    });
  }

  if (rows.length === 0) {
    console.log(chalk.gray('  No executions found.\n'));
    return;
  }

  // 4. Render table
  console.log(chalk.gray('  Status  Tool      Mode       Duration   Time         ID'));
  console.log(chalk.gray('  ' + '\u2500'.repeat(80)));

  for (const row of rows) {
    const statusIcon = row.status === 'running' ? chalk.blue('\u25CF') :
                       row.status === 'success' || row.status === 'completed' ? chalk.green('\u25CF') :
                       row.status === 'timeout' ? chalk.yellow('\u25CF') : chalk.red('\u25CF');
    console.log(`  ${statusIcon}     ${chalk.bold.white(row.tool.padEnd(8))}  ${chalk.gray(row.mode.padEnd(9))}  ${chalk.gray(row.duration.padEnd(9))}  ${chalk.gray(row.time.padEnd(11))}  ${chalk.dim(row.id)}`);
    if (row.prompt) {
      console.log(chalk.gray(`        ${row.prompt}${row.prompt.length >= 50 ? '...' : ''}`));
    }
  }

  console.log();
  console.log(chalk.gray('  ' + '\u2500'.repeat(80)));
  console.log(chalk.dim('  Output: ccw cli output <id>'));
  console.log(chalk.dim('  Watch:  ccw cli watch <id>'));
  console.log(chalk.dim('  Detail: ccw cli detail <id>'));
  console.log();
}

/**
 * Watch a running execution — stream output to stderr until completion
 * Exits with code 0 (success), 1 (error), or 2 (timeout)
 */
async function watchAction(watchId: string | undefined, options: { timeout?: string }): Promise<void> {
  if (!watchId) {
    console.error(chalk.red('Error: Execution ID is required'));
    console.error(chalk.gray('Usage: ccw cli watch <id> [--timeout 120]'));
    process.exit(1);
  }

  const timeoutMs = options.timeout ? parseInt(options.timeout, 10) * 1000 : 0;
  const startTime = Date.now();

  process.stderr.write(chalk.cyan(`Watching execution: ${watchId}\n`));

  // Track output position for incremental display
  let lastOutputLen = 0;

  const poll = async (): Promise<number> => {
    // Check timeout
    if (timeoutMs > 0 && (Date.now() - startTime) > timeoutMs) {
      process.stderr.write(chalk.yellow('\nWatch timed out.\n'));
      return 2;
    }

    try {
      // Fetch active execution state from dashboard
      const data = await new Promise<string>((resolve, reject) => {
        const req = http.request({
          hostname: 'localhost',
          port: Number(DASHBOARD_PORT),
          path: '/api/cli/active',
          method: 'GET',
          timeout: 3000,
          agent: false,
          headers: { 'Connection': 'close' }
        }, (res) => {
          let body = '';
          res.on('data', (chunk: Buffer) => { body += chunk.toString(); });
          res.on('end', () => resolve(body));
        });
        req.on('error', reject);
        req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
        req.end();
      });

      const parsed = JSON.parse(data);
      const executions = Array.isArray(parsed) ? parsed : (parsed.executions || []);
      const exec = executions.find((e: { id: string }) => e.id === watchId);

      if (exec) {
        // Show incremental output
        const fullOutput = exec.output || '';
        if (fullOutput.length > lastOutputLen) {
          process.stderr.write(fullOutput.slice(lastOutputLen));
          lastOutputLen = fullOutput.length;
        }

        if (exec.status === 'running') {
          const savedConversation = getHistoryStore(process.cwd()).getConversation(watchId);
          const shouldPreferSavedConversation = !!savedConversation && isSavedExecutionNewerThanActive(
            normalizeTimestampMs((exec as { startTime?: unknown }).startTime),
            savedConversation.updated_at || savedConversation.created_at
          );

          if (shouldPreferSavedConversation) {
            process.stderr.write(chalk.gray(`\nExecution already completed (status: ${savedConversation.latest_status}).\n`));
            process.stderr.write(chalk.dim(`Use: ccw cli output ${watchId}\n`));
            return savedConversation.latest_status === 'success' ? 0 : 1;
          }

          // Still running — wait and poll again
          await new Promise(r => setTimeout(r, 1000));
          return poll();
        }

        // Completed
        process.stderr.write(chalk.green(`\nExecution ${exec.status === 'completed' || exec.status === 'success' ? 'completed' : 'failed'}.\n`));
        return (exec.status === 'completed' || exec.status === 'success') ? 0 : 1;
      }
    } catch {
      // Dashboard not available
    }

    // Not found in active — check SQLite history
    const store = getHistoryStore(process.cwd());
    const result = store.getCachedOutput(watchId);
    if (result) {
      process.stderr.write(chalk.gray(`\nExecution already completed (status: ${result.status}).\n`));
      process.stderr.write(chalk.dim(`Use: ccw cli output ${watchId}\n`));
      return result.status === 'success' ? 0 : 1;
    }

    // Not found anywhere — may still be starting, wait and retry a few times
    if ((Date.now() - startTime) < 10000) {
      await new Promise(r => setTimeout(r, 1000));
      return poll();
    }

    process.stderr.write(chalk.red(`\nExecution not found: ${watchId}\n`));
    return 1;
  };

  const exitCode = await poll();
  process.exit(exitCode);
}

/**
 * Show execution history
 * @param {Object} options - CLI options
 */
async function historyAction(options: HistoryOptions): Promise<void> {
  const { limit = '20', tool, status } = options;

  console.log(chalk.bold.cyan('\n  CLI Execution History\n'));

  // Use recursive: true to aggregate history from parent and child projects (matches Dashboard behavior)
  const history = getRegisteredExecutionHistory({ limit: parseInt(limit, 10), tool, status });

  if (history.executions.length === 0) {
    console.log(chalk.gray('  No executions found.\n'));
    return;
  }

  // Count by tool
  const toolCounts: Record<string, number> = {};
  for (const exec of history.executions) {
    toolCounts[exec.tool] = (toolCounts[exec.tool] || 0) + 1;
  }
  const toolSummary = Object.entries(toolCounts).map(([t, c]) => `${t}:${c}`).join(' ');

  // Compact table header with tool breakdown
  console.log(chalk.gray(`  Total: ${history.total} | Showing: ${history.executions.length} (${toolSummary})\n`));
  console.log(chalk.gray('  Status  Tool      Time         Duration   ID'));
  console.log(chalk.gray('  ' + '─'.repeat(70)));

  for (const exec of history.executions) {
    const statusIcon = exec.status === 'success' ? chalk.green('●') :
                       exec.status === 'timeout' ? chalk.yellow('●') : chalk.red('●');
    const duration = exec.duration_ms >= 1000
      ? `${(exec.duration_ms / 1000).toFixed(1)}s`
      : `${exec.duration_ms}ms`;

    const timeAgo = getTimeAgo(new Date(exec.updated_at || exec.timestamp));
    const turnInfo = exec.turn_count && exec.turn_count > 1 ? chalk.cyan(`[${exec.turn_count}t]`) : '    ';

    // Compact format: status tool time duration [turns] + id on same line (no truncation)
    // Truncate prompt preview to 50 chars for compact display
    const shortPrompt = exec.prompt_preview.replace(/\n/g, ' ').substring(0, 50).trim();
    console.log(`  ${statusIcon}     ${chalk.bold.white(exec.tool.padEnd(8))}  ${chalk.gray(timeAgo.padEnd(11))}  ${chalk.gray(duration.padEnd(8))} ${turnInfo} ${chalk.dim(exec.id)}`);
    console.log(chalk.gray(`        ${shortPrompt}${exec.prompt_preview.length > 50 ? '...' : ''}`));
  }

  // Usage hint
  console.log();
  console.log(chalk.gray('  ' + '─'.repeat(70)));
  console.log(chalk.dim('  Filter: ccw cli history --tool <gemini|codex|qwen> --limit <n>'));
  console.log(chalk.dim('  Output: ccw cli output <id>'));
  console.log();
}

/**
 * Show conversation detail with all turns
 * @param {string} conversationId - Conversation ID
 */
async function detailAction(conversationId: string | undefined): Promise<void> {
  if (!conversationId) {
    console.error(chalk.red('Error: Conversation ID is required'));
    console.error(chalk.gray('Usage: ccw cli detail <conversation-id>'));
    process.exit(1);
  }

  let conversation = getConversationDetail(process.cwd(), conversationId);

  if (!conversation) {
    const found = findProjectWithExecution(conversationId, process.cwd());
    if (found) {
      conversation = getConversationDetail(found.projectPath, conversationId);
    }
  }

  if (!conversation) {
    console.error(chalk.red(`Error: Conversation not found: ${conversationId}`));
    process.exit(1);
  }

  console.log(chalk.bold.cyan('\n  Conversation Detail\n'));
  console.log(`  ${chalk.gray('ID:')}         ${conversation.id}`);
  console.log(`  ${chalk.gray('Tool:')}       ${conversation.tool}`);
  console.log(`  ${chalk.gray('Model:')}      ${conversation.model}`);
  console.log(`  ${chalk.gray('Mode:')}       ${conversation.mode}`);
  console.log(`  ${chalk.gray('Status:')}     ${conversation.latest_status}`);
  console.log(`  ${chalk.gray('Turns:')}      ${conversation.turn_count}`);
  console.log(`  ${chalk.gray('Duration:')}   ${(conversation.total_duration_ms / 1000).toFixed(1)}s total`);
  console.log(`  ${chalk.gray('Created:')}    ${conversation.created_at}`);
  if (conversation.turn_count > 1) {
    console.log(`  ${chalk.gray('Updated:')}    ${conversation.updated_at}`);
  }

  // Show all turns
  for (const turn of conversation.turns) {
    console.log(chalk.bold.cyan(`\n  ═══ Turn ${turn.turn} ═══`));
    console.log(chalk.gray(`  ${turn.timestamp} | ${turn.status} | ${(turn.duration_ms / 1000).toFixed(1)}s`));

    console.log(chalk.bold.white('\n  Prompt:'));
    console.log(chalk.gray('  ' + turn.prompt.split('\n').join('\n  ')));

    if (turn.output.stdout) {
      console.log(chalk.bold.white('\n  Output:'));
      console.log(turn.output.stdout);
    }

    if (turn.output.stderr) {
      console.log(chalk.bold.red('\n  Errors:'));
      console.log(turn.output.stderr);
    }

    if (turn.output.truncated) {
      console.log(chalk.yellow('\n  Note: Output was truncated due to size.'));
    }
  }

  console.log(chalk.dim(`\n  Continue: ccw cli -p "..." --resume ${conversation.id}`));
  console.log();
}

/**
 * Get human-readable time ago string
 * @param {Date} date
 * @returns {string}
 */
function getTimeAgo(date: Date): string {
  const seconds = Math.floor((Date.now() - date.getTime()) / 1000);

  if (seconds < 60) return 'just now';
  if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
  if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
  if (seconds < 604800) return `${Math.floor(seconds / 86400)}d ago`;
  return date.toLocaleDateString();
}

function normalizeTimestampMs(value: unknown): number | undefined {
  if (value instanceof Date) {
    const time = value.getTime();
    return Number.isFinite(time) ? time : undefined;
  }

  if (typeof value === 'number' && Number.isFinite(value)) {
    return value > 0 && value < 1_000_000_000_000 ? value * 1000 : value;
  }

  if (typeof value === 'string') {
    const trimmed = value.trim();
    if (!trimmed) return undefined;

    const numericValue = Number(trimmed);
    if (Number.isFinite(numericValue)) {
      return numericValue > 0 && numericValue < 1_000_000_000_000 ? numericValue * 1000 : numericValue;
    }

    const parsed = Date.parse(trimmed);
    return Number.isNaN(parsed) ? undefined : parsed;
  }

  return undefined;
}

function formatRunningDuration(elapsedMs: number): string {
  const safeElapsedMs = Math.max(0, elapsedMs);
  const totalSeconds = Math.floor(safeElapsedMs / 1000);

  if (totalSeconds < 60) return `${totalSeconds}s...`;

  const minutes = Math.floor(totalSeconds / 60);
  const seconds = totalSeconds % 60;
  if (totalSeconds < 3600) {
    return seconds === 0 ? `${minutes}m...` : `${minutes}m ${seconds}s...`;
  }

  const hours = Math.floor(totalSeconds / 3600);
  const remainingMinutes = Math.floor((totalSeconds % 3600) / 60);
  if (totalSeconds < 86400) {
    return remainingMinutes === 0 ? `${hours}h...` : `${hours}h ${remainingMinutes}m...`;
  }

  const days = Math.floor(totalSeconds / 86400);
  const remainingHours = Math.floor((totalSeconds % 86400) / 3600);
  return remainingHours === 0 ? `${days}d...` : `${days}d ${remainingHours}h...`;
}

function isSavedExecutionNewerThanActive(
  activeStartTimeMs: number | undefined,
  savedTimestamp: unknown
): boolean {
  if (activeStartTimeMs === undefined) {
    return false;
  }

  const savedTimestampMs = normalizeTimestampMs(savedTimestamp);
  if (savedTimestampMs === undefined) {
    return false;
  }

  return savedTimestampMs >= activeStartTimeMs;
}

/**ccw cli -p
 * CLI command entry point
 * @param {string} subcommand - Subcommand (status, exec, history, detail)
 * @param {string[]} args - Arguments array
 * @param {Object} options - CLI options
 */
export async function cliCommand(
  subcommand: string,
  args: string | string[],
  options: CliExecOptions | HistoryOptions
): Promise<void> {
  const argsArray = Array.isArray(args) ? args : (args ? [args] : []);

  switch (subcommand) {
    case 'status':
      await statusAction((options as CliExecOptions).debug);
      break;

    case 'history':
      await historyAction(options as HistoryOptions);
      break;

    case 'show':
      await showAction(options as unknown as { all?: boolean });
      break;

    case 'watch':
      await watchAction(argsArray[0], options as unknown as { timeout?: string });
      break;

    case 'detail':
      await detailAction(argsArray[0]);
      break;

    case 'storage':
      await storageAction(argsArray[0], options as unknown as StorageOptions);
      break;

    case 'output':
      await outputAction(argsArray[0], options as unknown as OutputViewOptions);
      break;

    case 'test-parse':
      // Test endpoint to debug multi-line prompt parsing
      testParseAction(argsArray, options as CliExecOptions);
      break;

    default: {
      const execOptions = options as CliExecOptions;
      // Auto-exec if: has -p/--prompt, has -f/--file, has --resume, subcommand looks like a prompt,
      // review mode with target flags (--uncommitted, --base, --commit), or stdin has piped input
      const hasPromptOption = !!execOptions.prompt;
      const hasFileOption = !!execOptions.file;
      const hasResume = execOptions.resume !== undefined;
      const subcommandIsPrompt = subcommand && !subcommand.startsWith('-');
      const hasReviewTarget = execOptions.mode === 'review' &&
        (execOptions.uncommitted || execOptions.base || execOptions.commit);
      const hasStdinInput = !process.stdin.isTTY; // piped input detected

      if (hasPromptOption || hasFileOption || hasResume || subcommandIsPrompt || hasReviewTarget || hasStdinInput) {
        // Treat as exec: use subcommand as positional prompt if no -p/-f option
        let positionalPrompt = subcommandIsPrompt ? subcommand : undefined;

        // On Windows, quoted arguments like -p "a b c" may be split across argsArray
        // Merge them back together to reconstruct the full prompt
        if (argsArray.length > 0 && hasPromptOption) {
          const extraArgs = argsArray.join(' ');
          positionalPrompt = positionalPrompt ? `${positionalPrompt} ${extraArgs}` : extraArgs;
        }

        await execAction(positionalPrompt, execOptions);
      } else {
        // Show help
        console.log(chalk.bold.cyan('\n  CCW CLI Tool Executor\n'));
        console.log('  Unified interface for Gemini, Qwen, and Codex CLI tools.\n');
        console.log('  Usage:');
        console.log(chalk.gray('    ccw cli -f prompt.txt --tool <tool>     Execute from file (recommended for multi-line)'));
        console.log(chalk.gray('    ccw cli -p "<prompt>" --tool <tool>     Execute with prompt (single-line)'));
        console.log(chalk.gray('    echo "prompt" | ccw cli --tool <tool>   Execute from stdin (pipe)'));
        console.log();
        console.log('  Subcommands:');
        console.log(chalk.gray('    show                List all executions (active + recent)'));
        console.log(chalk.gray('    watch <id>          Stream execution output (stderr, exits on completion)'));
        console.log(chalk.gray('    output <id>         Get final execution result'));
        console.log(chalk.gray('    status              Check CLI tools availability'));
        console.log(chalk.gray('    storage [cmd]       Manage CCW storage (info/clean/config)'));
        console.log(chalk.gray('    history             Show execution history'));
        console.log(chalk.gray('    detail <id>         Show execution detail (legacy, use show/output)'));
        console.log(chalk.gray('    test-parse [args]   Debug CLI argument parsing'));
        console.log();
        console.log('  Options:');
        console.log(chalk.gray('    -f, --file <file>   Read prompt from file (recommended for multi-line prompts)'));
        console.log(chalk.gray('    -p, --prompt <text> Prompt text (single-line)'));
        console.log(chalk.gray('    --tool <tool>       Tool: gemini, qwen, codex (default: gemini)'));
        console.log(chalk.gray('    --mode <mode>       Mode: analysis, write, auto, review (default: analysis)'));
        console.log(chalk.gray('    -d, --debug         Enable debug logging for troubleshooting'));
        console.log(chalk.gray('    --model <model>     Model override (supports PRIMARY_MODEL, SECONDARY_MODEL aliases)'));
        console.log(chalk.gray('    --effort <level>    Effort level for claude (low, medium, high)'));
        console.log(chalk.gray('    --cd <path>         Working directory'));
        console.log(chalk.gray('    --includeDirs <dirs>  Additional directories'));
        console.log(chalk.gray('    --id <id>           Execution ID (recommended, auto-generated if omitted)'));
        console.log(chalk.gray('    --resume [id]       Resume previous session'));
        console.log(chalk.gray('    --cache <items>     Cache: comma-separated @patterns and text'));
        console.log(chalk.gray('    --inject-mode <m>   Inject mode: none, full, progressive'));
        console.log();
        console.log('  Examples:');
        console.log(chalk.gray('    ccw cli -f my-prompt.txt --tool gemini'));
        console.log();
        console.log(chalk.gray('    # Bash/Linux heredoc'));
        console.log(chalk.gray("    ccw cli -f <(cat <<'EOF'"));
        console.log(chalk.gray('    PURPOSE: Multi-line prompt'));
        console.log(chalk.gray('    TASK: Example task'));
        console.log(chalk.gray('    EOF'));
        console.log(chalk.gray('    ) --tool gemini'));
        console.log();
        console.log(chalk.gray('    # PowerShell multi-line'));
        console.log(chalk.gray("    @'"));
        console.log(chalk.gray('    PURPOSE: Multi-line prompt'));
        console.log(chalk.gray('    TASK: Example task'));
        console.log(chalk.gray("    '@ | Out-File -Encoding utf8 prompt.tmp; ccw cli -f prompt.tmp --tool gemini"));
        console.log();
        console.log(chalk.gray('    ccw cli --resume --tool gemini'));
        console.log(chalk.gray('    ccw cli -p "..." --cache "@src/**/*.ts" --tool codex'));
        console.log(chalk.gray('    ccw cli -p "..." --cache "@src/**/*" --inject-mode progressive --tool gemini'));
        console.log(chalk.gray('    ccw cli output <id>              # View final result (default)'));
        console.log();
        console.log('  Cache format:');
        console.log(chalk.gray('    --cache "@src/**/*.ts,@CLAUDE.md"     # @patterns to pack'));
        console.log(chalk.gray('    --cache "@src/**/*,extra context"     # patterns + text content'));
        console.log(chalk.gray('    --cache                               # auto from CONTEXT field'));
        console.log();
        console.log('  Inject modes:');
        console.log(chalk.gray('    none:        cache only, no injection (default for gemini/qwen)'));
        console.log(chalk.gray('    full:        inject all cached content (default for codex)'));
        console.log(chalk.gray('    progressive: inject first 64KB with MCP continuation hint'));
        console.log();
        console.log('  Output options (ccw cli output <id>):');
        console.log(chalk.gray('    (default)    Final result only (agent response text)'));
        console.log(chalk.gray('    --verbose    Full metadata + raw stdout/stderr'));
        console.log(chalk.gray('    --raw        Raw stdout only (no formatting, for piping)'));
        console.log(chalk.gray('    --offset <n> Start from byte offset'));
        console.log(chalk.gray('    --limit <n>  Limit output bytes'));
        console.log();
        console.log(chalk.dim('  Tip: For complex prompts, use --file to avoid shell escaping issues'));
        console.log();
      }
    }
  }
}
