cqs 1.25.0

Code intelligence and RAG for AI agents. Semantic search, call graphs, impact analysis, type dependencies, and smart context assembly — in single tool calls. 54 languages + L5X/L5K PLC exports, 91.2% Recall@1 (BGE-large), 0.951 MRR (296 queries). Local ML, GPU-accelerated.
Documentation
//! CLI handler for `cqs convert`.

use std::path::PathBuf;

use anyhow::Result;

pub fn cmd_convert(
    path: &str,
    output: Option<&str>,
    overwrite: bool,
    dry_run: bool,
    clean_tags: Option<&str>,
) -> Result<()> {
    let _span = tracing::info_span!("cmd_convert").entered();

    let source = PathBuf::from(path);
    if !source.exists() {
        anyhow::bail!("Path not found: {}", path);
    }

    // Default output dir: same directory as input (or input dir itself)
    // SEC-4: Canonicalize to normalize symlinks and warn if outside source tree
    let output_dir = match output {
        Some(dir) => {
            let raw = PathBuf::from(dir);
            let canonical = dunce::canonicalize(&raw).unwrap_or(raw);
            if let Ok(source_parent) = dunce::canonicalize(source.parent().unwrap_or(&source)) {
                if !canonical.starts_with(&source_parent) {
                    tracing::warn!(
                        output = %canonical.display(),
                        source = %source_parent.display(),
                        "Output directory is outside source tree"
                    );
                }
            }
            canonical
        }
        None => {
            if source.is_dir() {
                source.clone()
            } else {
                source
                    .parent()
                    .map(|p| p.to_path_buf())
                    .unwrap_or_else(|| PathBuf::from("."))
            }
        }
    };

    let tags: Vec<String> = clean_tags
        .map(|s| s.split(',').map(|t| t.trim().to_string()).collect())
        .unwrap_or_default();

    let opts = cqs::convert::ConvertOptions {
        output_dir,
        overwrite,
        dry_run,
        clean_tags: tags,
    };

    let results = cqs::convert::convert_path(&source, &opts)?;

    if results.is_empty() {
        println!("No supported documents found.");
        return Ok(());
    }

    if dry_run {
        println!(
            "Dry run — {} document(s) would be converted:\n",
            results.len()
        );
    } else {
        println!("Converted {} document(s):\n", results.len());
    }

    for r in &results {
        println!(
            "  {}{}",
            r.source.display(),
            r.output.file_name().unwrap_or_default().to_string_lossy()
        );
        println!(
            "    Format: {} | Title: {} | Sections: {}",
            r.format, r.title, r.sections
        );
    }

    Ok(())
}