bijux-cli 0.3.6

Command-line runtime for automation, plugin-driven tools, and interactive workflows with structured output.
Documentation
//! Help rendering for command paths.

use anyhow::Result;

use crate::routing::model::{
    CLI_CONFIG_SUBCOMMANDS, CLI_PLUGINS_SUBCOMMANDS, ROOT_INTERACTION_COMMANDS,
    ROOT_RUNTIME_COMMANDS, ROOT_STATE_COMMANDS,
};
use crate::routing::parser::root_command;

pub(crate) fn normalize_help_whitespace(raw: &str) -> String {
    let mut normalized = String::new();
    let mut previous_blank = false;
    let mut in_options_section = false;

    for line in raw.lines() {
        let trimmed = line.trim_end();
        let blank_line = trimmed.trim().is_empty();
        let section_header = !trimmed.starts_with(' ') && trimmed.ends_with(':');
        if trimmed == "Options:" {
            in_options_section = true;
        } else if in_options_section && section_header {
            in_options_section = false;
        }

        if blank_line {
            if in_options_section {
                continue;
            }
            if previous_blank {
                continue;
            }
            previous_blank = true;
            normalized.push('\n');
            continue;
        }

        previous_blank = false;
        normalized.push_str(trimmed);
        normalized.push('\n');
    }

    normalized
}

pub(crate) fn render_command_help(path: &[&str]) -> Result<String> {
    let mut argv = vec!["bijux".to_string()];
    argv.extend(path.iter().map(|segment| (*segment).to_string()));
    argv.push("--help".to_string());

    let rendered = match root_command().try_get_matches_from(argv) {
        Err(error)
            if matches!(
                error.kind(),
                clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion
            ) =>
        {
            error.to_string()
        }
        Ok(_) => return Err(anyhow::anyhow!("unknown help path")),
        Err(_) => return Err(anyhow::anyhow!("unknown help path")),
    };

    let normalized = normalize_help_whitespace(&rendered);
    Ok(decorate_help_text(normalized, path))
}

pub(crate) fn decorate_help_text(mut rendered: String, path: &[&str]) -> String {
    append_help_sections(&mut rendered, path);
    if path == ["history"] {
        rendered.push_str(
            "\nHistory integrity:\n  malformed JSON payloads fail closed\n  use `bijux history clear --force` to reset corrupted history files\n",
        );
    }
    if path == ["version"] {
        rendered.push_str(
            "\nVersion output:\n  text: one-line summary (`bijux version <value>`)\n  json/yaml: includes semver, source, commit, and build profile fields\n",
        );
    }
    if matches!(path, ["plugins", "inspect"] | ["cli", "plugins", "inspect"]) {
        rendered.push_str(
            "\nCompatibility note: inspect output includes plugin compatibility warnings when present.\n",
        );
    }
    rendered
}

fn append_help_sections(rendered: &mut String, path: &[&str]) {
    if !rendered.ends_with('\n') {
        rendered.push('\n');
    }

    let mut sections = Vec::new();

    if let Some(grouped) = help_grouped_guide(path) {
        sections.push(grouped);
    }
    if let Some(subcommands) = help_subcommand_guide(path) {
        sections.push(subcommands);
    }

    let examples = help_examples(path);
    if !examples.is_empty() {
        sections.push(render_examples(&examples));
    }

    if sections.is_empty() {
        return;
    }

    rendered.push('\n');
    rendered.push_str(&sections.join("\n\n"));
    if !rendered.ends_with('\n') {
        rendered.push('\n');
    }
}

fn format_command_group_row(name: &str, commands: &[&str]) -> String {
    format!("{name:<12} {}", commands.join(", "))
}

fn help_grouped_guide(path: &[&str]) -> Option<String> {
    match path {
        [] => Some(format!(
            "Management Commands:\n\
  {}\n\
  config       config\n\
  plugins      plugins\n\
  {}\n\
  {}\n\
\n\
Use `bijux help <command>` for command-specific help.",
            format_command_group_row("runtime", ROOT_RUNTIME_COMMANDS),
            format_command_group_row("state", ROOT_STATE_COMMANDS),
            format_command_group_row("interaction", ROOT_INTERACTION_COMMANDS),
        )),
        _ => None,
    }
}

fn render_subcommand_guide(rows: &[String]) -> String {
    let mut out = String::from("Subcommand guide:\n");
    for row in rows {
        out.push_str("  ");
        out.push_str(row);
        out.push('\n');
    }
    out.trim_end().to_string()
}

fn config_subcommand_help(command: &str) -> &'static str {
    match command {
        "list" => "Print all key/value pairs",
        "get" => "Read one key",
        "set" => "Write one key=value pair",
        "unset" => "Remove one key",
        "clear" => "Remove all keys",
        "reload" => "Validate and reload current file",
        "export" => "Write config to a target path",
        "load" => "Load config from a source path",
        _ => "Configuration command",
    }
}

fn plugin_subcommand_help(command: &str) -> &'static str {
    match command {
        "list" => "List discovered plugins",
        "info" => "Show plugin inventory details",
        "inspect" => "Inspect one plugin or the full inventory",
        "check" => "Validate a plugin namespace",
        "enable" => "Enable a plugin namespace",
        "disable" => "Disable a plugin namespace",
        "install" => "Install plugin from manifest/source",
        "uninstall" => "Remove a plugin namespace",
        "scaffold" => "Generate a plugin template",
        "doctor" => "Plugin health diagnostics",
        "reserved-names" => "Show reserved plugin namespace rules",
        "where" => "Show plugin state and install paths",
        "explain" => "Explain plugin resolution outcome",
        "schema" => "Show the current plugin manifest JSON schema",
        _ => "Plugin command",
    }
}

fn help_subcommand_guide(path: &[&str]) -> Option<String> {
    match path {
        ["cli"] => Some(render_subcommand_guide(&[
            "status     Runtime status summary".to_string(),
            "paths      Runtime state and filesystem paths".to_string(),
            "doctor     Runtime environment diagnostics".to_string(),
            "version    Runtime identity and provenance".to_string(),
            "repl       Interactive runtime shell".to_string(),
            "completion Shell completion output".to_string(),
            "config     Runtime configuration operations".to_string(),
            "self-test  Deterministic runtime self-checks".to_string(),
            "plugins    Canonical plugin lifecycle namespace".to_string(),
        ])),
        ["config"] | ["cli", "config"] => {
            let rows = CLI_CONFIG_SUBCOMMANDS
                .iter()
                .map(|command| format!("{command:<8} {}", config_subcommand_help(command)))
                .collect::<Vec<_>>();
            Some(render_subcommand_guide(&rows))
        }
        ["plugins"] | ["cli", "plugins"] => {
            let rows = CLI_PLUGINS_SUBCOMMANDS
                .iter()
                .map(|command| format!("{command:<14} {}", plugin_subcommand_help(command)))
                .collect::<Vec<_>>();
            Some(render_subcommand_guide(&rows))
        }
        _ => None,
    }
}

fn render_examples(examples: &[String]) -> String {
    let mut out = String::from("Examples:\n");
    for example in examples {
        out.push_str("  ");
        out.push_str(example);
        out.push('\n');
    }
    out.trim_end().to_string()
}

fn help_examples(path: &[&str]) -> Vec<String> {
    match path {
        [] => vec![
            "bijux status".to_string(),
            "bijux install atlas --dry-run".to_string(),
            "bijux config get foo".to_string(),
            "bijux config set foo=bar".to_string(),
            "bijux plugins list".to_string(),
        ],
        ["cli"] => vec![
            "bijux cli status".to_string(),
            "bijux cli paths".to_string(),
            "bijux cli config list".to_string(),
            "bijux cli plugins list".to_string(),
        ],
        ["status"] => vec!["bijux status".to_string(), "bijux status --format json".to_string()],
        ["audit"] => vec!["bijux audit".to_string(), "bijux audit --format json".to_string()],
        ["docs"] => vec!["bijux docs".to_string(), "bijux docs --format json".to_string()],
        ["install"] => vec![
            "bijux install cli --dry-run".to_string(),
            "bijux install dev-cli --dry-run".to_string(),
            "bijux install atlas --dry-run".to_string(),
        ],
        ["doctor"] => vec!["bijux doctor".to_string(), "bijux doctor --format json".to_string()],
        ["version"] => vec!["bijux version".to_string(), "bijux --version".to_string()],
        ["config"] => vec![
            "bijux config list".to_string(),
            "bijux config get foo".to_string(),
            "bijux config set foo=bar".to_string(),
            "bijux config export ./bijux.env".to_string(),
        ],
        ["plugins"] => vec![
            "bijux plugins list".to_string(),
            "bijux plugins inspect sample".to_string(),
            "bijux plugins check sample".to_string(),
            "bijux plugins install ./plugin.manifest.json".to_string(),
        ],
        ["plugins", "install"] => vec![
            "bijux plugins install ./plugin.manifest.json".to_string(),
            "bijux plugins install ./plugin.manifest.json --format json".to_string(),
            "bijux plugins install ./plugin.manifest.json --source community-catalog".to_string(),
        ],
        ["plugins", "scaffold"] => vec![
            "bijux plugins scaffold python sample-plugin".to_string(),
            "bijux plugins scaffold python sample-plugin --format json".to_string(),
            "bijux help plugins scaffold".to_string(),
        ],
        ["plugins", "inspect"] | ["cli", "plugins", "inspect"] => vec![
            "bijux cli plugins inspect sample".to_string(),
            "bijux cli plugins inspect sample --format json".to_string(),
            "bijux help cli plugins inspect".to_string(),
        ],
        ["repl"] => vec!["bijux repl".to_string(), "bijux repl --format text".to_string()],
        ["completion"] => vec![
            "bijux completion --shell bash".to_string(),
            "bijux completion --shell zsh --format json".to_string(),
        ],
        ["history"] => vec![
            "bijux history".to_string(),
            "bijux history clear".to_string(),
            "bijux history clear --force".to_string(),
        ],
        ["memory"] => vec![
            "bijux memory list".to_string(),
            "bijux memory set session.id=abc123".to_string(),
            "bijux memory get session.id".to_string(),
        ],
        ["cli", "config"] => vec![
            "bijux cli config list".to_string(),
            "bijux cli config get foo".to_string(),
            "bijux cli config set foo=bar".to_string(),
        ],
        ["cli", "plugins"] => vec![
            "bijux cli plugins list".to_string(),
            "bijux cli plugins inspect sample".to_string(),
            "bijux cli plugins check sample".to_string(),
        ],
        ["cli", "plugins", "install"] => vec![
            "bijux cli plugins install ./plugin.manifest.json".to_string(),
            "bijux cli plugins install ./plugin.manifest.json --format json".to_string(),
            "bijux cli plugins install ./plugin.manifest.json --source community-catalog"
                .to_string(),
        ],
        ["cli", "plugins", "scaffold"] => vec![
            "bijux cli plugins scaffold python sample-plugin".to_string(),
            "bijux cli plugins scaffold python sample-plugin --format json".to_string(),
            "bijux help cli plugins scaffold".to_string(),
        ],
        _ => default_examples(path),
    }
}

fn default_examples(path: &[&str]) -> Vec<String> {
    if path.is_empty() {
        return Vec::new();
    }

    let joined = path.join(" ");
    vec![
        format!("bijux {joined}"),
        format!("bijux {joined} --format json"),
        format!("bijux help {joined}"),
    ]
}