oy-cli 0.12.0

OpenCode launcher and deterministic MCP helpers for repository audit and review workflows
Documentation
use anyhow::{Context, Result, anyhow, bail};
use serde_json::Value;
use std::ffi::OsString;
use std::path::Path;
use std::sync::LazyLock;
use std::time::Duration;

use super::super::ToolContext;
use super::super::args::OutlineArgs;
use super::super::external::{ExternalCommand, discover};
use super::output::OutlineOutput;
use super::paths::resolve_read_path;

const OUTLINE_TIMEOUT: Duration = Duration::from_secs(30);
const OUTLINE_OUTPUT_LIMIT: usize = 8 * 1024 * 1024;

static CTAGS: LazyLock<std::result::Result<ExternalCommand, String>> =
    LazyLock::new(|| discover_ctags().map_err(|err| format!("{err:#}")));

pub(crate) fn tool_outline(ctx: &ToolContext, args: OutlineArgs) -> Result<Value> {
    let path = resolve_read_path(ctx, &args.path)?;
    if path.is_dir() {
        bail!("outline path is a directory: {}", args.path);
    }

    let command = ctags_command()?;
    let output = run_ctags(ctx.root(), command, &path)?;

    Ok(serde_json::to_value(OutlineOutput {
        path: args.path,
        format: "universal-ctags-json",
        command: command.name().to_string(),
        output,
    })?)
}

pub(crate) fn has_universal_ctags() -> bool {
    ctags_command().is_ok()
}

fn ctags_command() -> Result<&'static ExternalCommand> {
    CTAGS.as_ref().map_err(|err| anyhow!(err.clone()))
}

fn discover_ctags() -> Result<ExternalCommand> {
    discover(
        "Universal Ctags with JSON support",
        "OY_CTAGS",
        &["u-ctags", "ctags"],
        |command| {
            let version = command.probe(&["--options=NONE", "--version"])?;
            version.require_success(command)?;
            if !String::from_utf8_lossy(&version.stdout).contains("Universal Ctags") {
                bail!("version output does not identify Universal Ctags");
            }

            let features = command.probe(&["--options=NONE", "--list-features"])?;
            features.require_success(command)?;
            if !supports_json_feature(&features.stdout) {
                bail!("Universal Ctags was built without JSON output support");
            }
            Ok(())
        },
    )
}

fn supports_json_feature(output: &[u8]) -> bool {
    String::from_utf8_lossy(output).lines().any(|line| {
        line.split_whitespace()
            .next()
            .is_some_and(|feature| feature.eq_ignore_ascii_case("json"))
    })
}

fn run_ctags(root: &Path, command: &ExternalCommand, path: &Path) -> Result<Value> {
    let args = [
        OsString::from("--options=NONE"),
        OsString::from("--output-format=json"),
        OsString::from("--fields=+nK"),
        OsString::from("--extras=-F"),
        OsString::from("-f"),
        OsString::from("-"),
        path.as_os_str().to_os_string(),
    ];
    let output = command.run(root, args, OUTLINE_TIMEOUT, OUTLINE_OUTPUT_LIMIT)?;
    output.require_success(command)?;
    let stdout = String::from_utf8(output.stdout).context("ctags output was not UTF-8")?;
    let mut records = Vec::new();
    for (index, line) in stdout.lines().enumerate() {
        if line.trim().is_empty() {
            continue;
        }
        records.push(
            serde_json::from_str(line)
                .with_context(|| format!("ctags output line {} was not valid JSON", index + 1))?,
        );
    }
    Ok(Value::Array(records))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ctags_json_feature_accepts_tabular_feature_output() {
        assert!(supports_json_feature(
            b"#NAME DESCRIPTION\njson supports json format output\n"
        ));
        assert!(!supports_json_feature(
            b"wildcards supports glob matching\n"
        ));
    }

    #[cfg(unix)]
    #[test]
    fn ctags_call_disables_options_and_parses_json_lines() {
        use std::fs;
        use std::os::unix::fs::PermissionsExt as _;

        let dir = tempfile::tempdir().unwrap();
        let executable = dir.path().join("ctags");
        fs::write(
            &executable,
            "#!/bin/sh\n[ \"$1\" = \"--options=NONE\" ] || exit 19\nprintf '%s\\n' '{\"name\":\"main\",\"line\":1}'\n",
        )
        .unwrap();
        let mut permissions = fs::metadata(&executable).unwrap().permissions();
        permissions.set_mode(0o755);
        fs::set_permissions(&executable, permissions).unwrap();
        let source = dir.path().join("main.rs");
        fs::write(&source, "fn main() {}\n").unwrap();
        let command = super::super::super::external::test_command(&executable);

        let output = run_ctags(dir.path(), &command, &source).unwrap();

        assert_eq!(output[0]["name"], "main");
    }
}