code-kb-cli 2.5.1

CLI and MCP server for code-kb
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::{EnvFilter, fmt};

/// Returns the log directory for a root: `<root>/.code-kb/logs` when the tools accept the
/// root as a project, otherwise `logs` in the global telemetry directory, so a launch in a
/// folder that is not a project leaves no files there.
pub fn get_log_dir(root: &Path, explicit_db: Option<&Path>) -> PathBuf {
    if crate::mcp::server::accepts_root(root, explicit_db) {
        code_kb_core::workspace::log_dir(root)
    } else {
        code_kb_core::get_global_telemetry_dir().join("logs")
    }
}

/// Initializes structured logging to `code-kb.log` in `log_dir` and optionally stderr.
///
/// NOTE: When `is_serve` is true (MCP mode over stdio), all stdout logging is strictly
/// suppressed to prevent protocol stream corruption.
pub fn init_logging(log_dir: &Path, is_serve: bool, verbose: bool) -> Option<WorkerGuard> {
    if let Err(e) = fs::create_dir_all(log_dir) {
        eprintln!(
            "Warning: Failed to create log directory '{}': {e}",
            log_dir.display()
        );
        return None;
    }

    if let Some(code_kb_dir) = log_dir.parent().filter(|dir| dir.ends_with(".code-kb")) {
        let gitignore = code_kb_dir.join(".gitignore");
        if let Err(e) = fs::OpenOptions::new()
            .write(true)
            .create_new(true)
            .open(&gitignore)
            .and_then(|mut file| file.write_all(b"*\n"))
            && e.kind() != std::io::ErrorKind::AlreadyExists
        {
            eprintln!(
                "Warning: Failed to create ignore file '{}': {e}",
                gitignore.display()
            );
        }
    }

    // Rolling daily log appender in .code-kb/logs/ capped at 7 files
    let file_appender = match tracing_appender::rolling::RollingFileAppender::builder()
        .rotation(tracing_appender::rolling::Rotation::DAILY)
        .filename_prefix("code-kb.log")
        .max_log_files(7)
        .build(log_dir)
    {
        Ok(appender) => appender,
        Err(e) => {
            eprintln!("Warning: Failed to initialize rolling file appender: {e}");
            return None;
        }
    };
    let (non_blocking_file, guard) = tracing_appender::non_blocking(file_appender);

    // Build default EnvFilter
    let default_filter = if verbose {
        "code_kb_core=debug,code_kb_cli=debug,code_kb=debug,info"
    } else {
        "code_kb_core=info,code_kb_cli=info,code_kb=info,warn"
    };

    let env_filter =
        EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_filter));

    let file_layer = fmt::layer()
        .with_writer(non_blocking_file)
        .with_ansi(false)
        .with_target(true)
        .with_thread_ids(true)
        .with_file(true)
        .with_line_number(true);

    // Stderr layer only in CLI mode when verbose or RUST_LOG is set
    let stderr_layer = if !is_serve && (verbose || std::env::var_os("RUST_LOG").is_some()) {
        Some(
            fmt::layer()
                .with_writer(std::io::stderr)
                .with_ansi(true)
                .with_target(false),
        )
    } else {
        None
    };

    let subscriber = tracing_subscriber::registry()
        .with(env_filter)
        .with(file_layer)
        .with(stderr_layer);

    let _ = subscriber.try_init();

    Some(guard)
}