crtx 0.1.1

CLI for the Cortex supervisory memory substrate.
//! `cortex completions <shell>` — emit a shell completion script.
//!
//! Phase 4.A polish item. Operators pipe the script to wherever their shell
//! expects (e.g. `cortex completions bash > ~/.bash_completion.d/cortex`).
//! The set of supported shells is whatever [`clap_complete::Shell`] supports
//! today: `bash`, `zsh`, `fish`, `powershell`, `elvish`. We name the four
//! POSIX/Windows-relevant variants explicitly in the docs and runbook; the
//! `elvish` variant is supported transitively because `clap_complete::Shell`
//! exposes it and there is no operator-visible cost to leaving it on the
//! `ValueEnum` surface.
//!
//! ## Why `--json` is not wired
//!
//! Shell completion scripts are *executable shell code*, not data. There is
//! no meaningful way to fold a bash function definition into the
//! `cortex.<cmd>` envelope (`command` / `exit_code` / `outcome` / `report`)
//! without forcing the operator to base64-decode or jq-extract before piping
//! to their shell — that defeats the whole point of `cortex completions
//! bash > ~/.bash_completion.d/cortex`. If `--json` is set we therefore
//! emit a refusal envelope (`Outcome::Usage`, `Exit::Usage`) explaining the
//! limitation rather than silently producing prose-shaped JSON or a JSON
//! envelope with a shell script as a string payload.
//!
//! This refusal pattern matches the rest of the CLI's truth-ceiling
//! contract: when `--json` cannot honour its rendering contract, the
//! command surfaces a typed refusal rather than degrading.

use std::io;

use clap::{Args, CommandFactory};
use clap_complete::{generate, Shell};
use serde::Serialize;

use crate::exit::Exit;
use crate::output::{self, Envelope, Outcome};
use crate::Cli;

/// `cortex completions` flags.
#[derive(Debug, Args)]
pub struct CompletionsArgs {
    /// Target shell. One of: `bash`, `zsh`, `fish`, `powershell`, `elvish`.
    #[arg(value_enum)]
    pub shell: Shell,
}

/// Structured `cortex completions` refusal payload.
///
/// Only emitted on the `--json` refusal path. The success path writes a
/// shell script directly to stdout because shell completion scripts are
/// not JSON-encodable in any operator-useful way (see module docstring).
#[derive(Debug, Serialize)]
struct CompletionsRefusalReport {
    shell: &'static str,
    reason: &'static str,
}

/// Run the `completions` command. Writes the completion script to stdout
/// on the happy path, or a typed refusal envelope when `--json` is set.
pub fn run(args: CompletionsArgs) -> Exit {
    if output::json_enabled() {
        let envelope = Envelope::new(
            "cortex.completions",
            Exit::Usage,
            CompletionsRefusalReport {
                shell: shell_name(args.shell),
                reason: "cortex completions emits a shell script to stdout; \
                         it cannot be wrapped in the --json envelope. Re-run \
                         without --json.",
            },
        )
        .with_outcome(Outcome::Usage);
        return output::emit(&envelope, Exit::Usage);
    }

    let mut cmd = Cli::command();
    let bin_name = cmd.get_name().to_string();
    let mut stdout = io::stdout().lock();
    generate(args.shell, &mut cmd, bin_name, &mut stdout);
    Exit::Ok
}

/// Canonical token for the shell name. Mirrors `clap_complete::Shell`'s
/// `Display` rendering so the JSON refusal payload stays stable across
/// `clap_complete` upgrades.
fn shell_name(shell: Shell) -> &'static str {
    match shell {
        Shell::Bash => "bash",
        Shell::Elvish => "elvish",
        Shell::Fish => "fish",
        Shell::PowerShell => "powershell",
        Shell::Zsh => "zsh",
        _ => "unknown",
    }
}

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

    #[test]
    fn shell_name_covers_documented_set() {
        assert_eq!(shell_name(Shell::Bash), "bash");
        assert_eq!(shell_name(Shell::Zsh), "zsh");
        assert_eq!(shell_name(Shell::Fish), "fish");
        assert_eq!(shell_name(Shell::PowerShell), "powershell");
        assert_eq!(shell_name(Shell::Elvish), "elvish");
    }
}