nils-codex-cli 0.7.3

CLI crate for nils-codex-cli in the nils-cli workspace.
Documentation
use nils_test_support::bin;
use nils_test_support::cmd::{self, CmdOutput};
use pretty_assertions::assert_eq;
use std::path::PathBuf;

fn codex_cli_bin() -> PathBuf {
    bin::resolve("codex-cli")
}

fn run(args: &[&str]) -> CmdOutput {
    let bin = codex_cli_bin();
    cmd::run(&bin, args, &[], None)
}

fn stdout(output: &CmdOutput) -> String {
    output.stdout_text()
}

fn assert_exit(output: &CmdOutput, code: i32) {
    assert_eq!(output.code, code, "stderr: {}", output.stderr_text());
}

fn parse_help_flag_tokens(help_text: &str) -> Vec<String> {
    let mut in_options = false;
    let mut flags: Vec<String> = Vec::new();

    for raw_line in help_text.lines() {
        let line = raw_line.trim();
        if line == "Options:" {
            in_options = true;
            continue;
        }
        if !in_options {
            continue;
        }
        if line.is_empty() {
            break;
        }

        let spec_end = line.find("  ").unwrap_or(line.len());
        let spec = &line[..spec_end];

        for token in spec
            .split(|ch: char| ch == ',' || ch.is_whitespace())
            .filter(|piece| !piece.is_empty())
        {
            if !token.starts_with('-') {
                continue;
            }
            if token == "-h" || token == "--help" {
                continue;
            }
            if !flags.iter().any(|seen| seen == token) {
                flags.push(token.to_string());
            }
        }
    }

    flags
}

fn bash_case_opts(script: &str, label: &str) -> String {
    let case_marker = format!("{label})");
    let mut in_case = false;

    for line in script.lines() {
        let trimmed = line.trim();
        if trimmed == case_marker {
            in_case = true;
            continue;
        }

        if !in_case {
            continue;
        }

        if let Some(rest) = trimmed.strip_prefix("opts=\"") {
            let end = rest
                .find('"')
                .unwrap_or_else(|| panic!("missing closing quote for opts in case {label}"));
            return rest[..end].to_string();
        }

        if trimmed.ends_with(")") {
            break;
        }
    }

    panic!("missing bash opts case: {label}");
}

fn bash_case_label(path: &[&str]) -> String {
    let mut parts = vec!["codex__cli".to_string()];
    parts.extend(path.iter().map(|segment| segment.replace('-', "__")));
    parts.join("__")
}

fn zsh_context_marker(parents: &[&str]) -> String {
    if parents.is_empty() {
        "curcontext=\"${curcontext%:*:*}:codex-cli-command-$line[1]:\"".to_string()
    } else {
        format!(
            "curcontext=\"${{curcontext%:*:*}}:codex-cli-{}-command-$line[1]:\"",
            parents.join("-")
        )
    }
}

fn zsh_leaf_arguments_block(script: &str, path: &[&str]) -> String {
    let (leaf, parents) = path
        .split_last()
        .unwrap_or_else(|| panic!("expected non-empty command path"));
    let marker = zsh_context_marker(parents);
    let marker_idx = script
        .find(&marker)
        .unwrap_or_else(|| panic!("missing zsh context marker: {marker}"));
    let from_marker = &script[marker_idx..];

    let leaf_marker = format!("({leaf})");
    let leaf_idx = from_marker
        .find(&leaf_marker)
        .unwrap_or_else(|| panic!("missing leaf marker {leaf_marker} after marker {marker}"));
    let from_leaf = &from_marker[leaf_idx..];

    let args_marker = "_arguments \"${_arguments_options[@]}\" : \\";
    let args_idx = from_leaf
        .find(args_marker)
        .unwrap_or_else(|| panic!("missing _arguments block for path {}", path.join(" ")));
    let from_args = &from_leaf[args_idx..];

    let end_idx = from_args
        .find("&& ret=0")
        .unwrap_or_else(|| panic!("missing _arguments terminator for path {}", path.join(" ")));

    from_args[..end_idx].to_string()
}

fn contains_option_token(haystack: &str, token: &str) -> bool {
    let mut search_start = 0;
    while let Some(relative_idx) = haystack[search_start..].find(token) {
        let start = search_start + relative_idx;
        let end = start + token.len();
        let before = haystack[..start].chars().next_back();
        let after = haystack[end..].chars().next();
        let before_ok = before.is_none_or(|ch| !ch.is_ascii_alphanumeric() && ch != '-');
        let after_ok = after.is_none_or(|ch| !ch.is_ascii_alphanumeric() && ch != '-');
        if before_ok && after_ok {
            return true;
        }
        search_start = end;
    }
    false
}

fn leaf_paths() -> Vec<Vec<&'static str>> {
    vec![
        vec!["agent", "prompt"],
        vec!["agent", "advice"],
        vec!["agent", "knowledge"],
        vec!["agent", "commit"],
        vec!["auth", "login"],
        vec!["auth", "use"],
        vec!["auth", "save"],
        vec!["auth", "remove"],
        vec!["auth", "refresh"],
        vec!["auth", "auto-refresh"],
        vec!["auth", "current"],
        vec!["auth", "sync"],
        vec!["diag", "rate-limits"],
        vec!["config", "show"],
        vec!["config", "set"],
        vec!["prompt-segment"],
        vec!["completion"],
    ]
}

#[test]
fn completion_flags_contract_leaf_help_matches_bash_and_zsh_candidates() {
    let bash_output = run(&["completion", "bash"]);
    assert_exit(&bash_output, 0);
    let bash_script = stdout(&bash_output);

    let zsh_output = run(&["completion", "zsh"]);
    assert_exit(&zsh_output, 0);
    let zsh_script = stdout(&zsh_output);

    for path in leaf_paths() {
        let mut help_args = path.clone();
        help_args.push("--help");
        let help_output = run(&help_args);
        assert_exit(&help_output, 0);
        let help_text = stdout(&help_output);
        let expected_flags = parse_help_flag_tokens(&help_text);
        if expected_flags.is_empty() {
            continue;
        }

        let label = bash_case_label(&path);
        let bash_opts = bash_case_opts(&bash_script, &label);
        let zsh_block = zsh_leaf_arguments_block(&zsh_script, &path);

        for flag in expected_flags {
            assert!(
                contains_option_token(&bash_opts, &flag),
                "missing bash completion flag for `{}`: {}",
                path.join(" "),
                flag
            );
            assert!(
                contains_option_token(&zsh_block, &flag),
                "missing zsh completion flag for `{}`: {}\nblock:\n{}",
                path.join(" "),
                flag,
                zsh_block
            );
        }
    }
}