tftio-prompter 4.0.0

A CLI tool for composing reusable prompt snippets from a library using TOML profiles
Documentation
#![cfg_attr(
    not(test),
    deny(clippy::unwrap_used, clippy::panic, clippy::indexing_slicing)
)]
//! Prompter CLI binary.
//!
//! Main entry point for the prompter command-line tool.

use prompter::{
    AppMode, Cli, Commands, MetaCommand, doctor, init_scaffold, resolve_app_mode, run_list_stdout,
    run_render_stdout, run_tree_stdout, run_validate_stdout,
};
use tftio_cli_common::{
    AgentCapability, AgentSurfaceSpec, CommandSelector, FatalCliError, JsonOutput, LicenseType,
    StandardCommand, ToolSpec, err_response, map_standard_command, print_error, run_cli_from,
    workspace_tool,
};

const TOOL_SPEC: ToolSpec = workspace_tool(
    "prompter",
    "Prompter",
    env!("CARGO_PKG_VERSION"),
    LicenseType::MIT,
    true,
    true,
)
.with_agent_surface(&AGENT_SURFACE);

const RENDER_PROMPTS_COMMAND: CommandSelector = CommandSelector::new(&["run"]);
const RENDER_SYSTEM_COMMAND: CommandSelector = CommandSelector::new(&["system"]);
const LIST_PROFILES_COMMAND: CommandSelector = CommandSelector::new(&["list"]);
const TREE_PROFILES_COMMAND: CommandSelector = CommandSelector::new(&["tree"]);
const VALIDATE_PROFILES_COMMAND: CommandSelector = CommandSelector::new(&["validate"]);

const RENDER_PROMPTS_CAPABILITY: AgentCapability = AgentCapability::minimal(
    "render-prompts",
    &[RENDER_PROMPTS_COMMAND],
    &[],
)
.with_when_to_use("the user wants to compose and emit one or more prompt profiles to stdout")
.with_when_not_to_use(
    "the user wants to inspect the profile library or validate configuration without rendering",
);
const RENDER_SYSTEM_CAPABILITY: AgentCapability = AgentCapability::minimal(
    "render-system-base",
    &[RENDER_SYSTEM_COMMAND],
    &[],
)
.with_when_to_use(
    "a harness or launcher needs the always-on invariant base (the agent system prompt), optionally followed by stack profiles, to stamp into a system-prompt site",
)
.with_when_not_to_use(
    "the user wants on-demand task standards for the current work (use render-prompts instead)",
);
const LIST_PROFILES_CAPABILITY: AgentCapability =
    AgentCapability::minimal("list-profiles", &[LIST_PROFILES_COMMAND], &[])
        .with_when_to_use(
            "the user wants to enumerate available prompt profiles from a prompter configuration",
        )
        .with_when_not_to_use(
            "the user wants the rendered contents of a profile rather than its name",
        );
const TREE_PROFILES_CAPABILITY: AgentCapability =
    AgentCapability::minimal("tree-profiles", &[TREE_PROFILES_COMMAND], &[])
        .with_when_to_use(
            "the user wants to see the dependency tree of a prompt profile and its included files",
        )
        .with_when_not_to_use(
            "the user wants flat profile listings or the rendered output of a profile",
        );
const VALIDATE_PROFILES_CAPABILITY: AgentCapability = AgentCapability::minimal(
    "validate-profiles",
    &[VALIDATE_PROFILES_COMMAND],
    &[],
)
.with_when_to_use(
    "the user wants to check that all profile dependencies resolve and configuration is consistent",
)
.with_when_not_to_use("the user wants to render a profile or list profiles by name");

const AGENT_SURFACE: AgentSurfaceSpec = AgentSurfaceSpec::new(&[
    RENDER_PROMPTS_CAPABILITY,
    RENDER_SYSTEM_CAPABILITY,
    LIST_PROFILES_CAPABILITY,
    TREE_PROFILES_CAPABILITY,
    VALIDATE_PROFILES_CAPABILITY,
]);

fn main() {
    let env = process_env();
    std::process::exit(run_cli_from::<Cli, _, doctor::PrompterDoctor, _, _>(
        &TOOL_SPEC,
        &env,
        std::env::args_os(),
        &doctor::PrompterDoctor,
        metadata_command,
        run,
    ));
}

/// Read process-edge environment values once at the binary edge.
#[allow(
    clippy::disallowed_methods,
    reason = "agent token / HOME read once at the process edge (REPO_INVARIANTS.md #5)"
)]
fn process_env() -> tftio_cli_common::ProcessEnv {
    tftio_cli_common::ProcessEnv {
        agent: tftio_cli_common::AgentModeContext::from_tokens(
            std::env::var(tftio_cli_common::AGENT_TOKEN_ENV).ok(),
            std::env::var(tftio_cli_common::AGENT_TOKEN_EXPECTED_ENV).ok(),
        ),
        home: std::env::var_os("HOME").map(std::path::PathBuf::from),
    }
}

fn metadata_command(cli: &Cli) -> Option<StandardCommand> {
    match &cli.command {
        Commands::Meta { command } => {
            // Completions: shell-specific dynamic augmentation (CLAUDE.md exception).
            match command {
                MetaCommand::Completions { .. } => None,
                _ => Some(map_standard_command(
                    command,
                    JsonOutput::from_flag(cli.json),
                )),
            }
        }
        _ => None,
    }
}

#[allow(
    clippy::unnecessary_wraps,
    reason = "signature is fixed by cli-common's command runner, which requires FnOnce(Cli) -> Result<i32, FatalCliError>; current arms all succeed"
)]
fn run(cli: Cli) -> Result<i32, FatalCliError> {
    let mode = resolve_app_mode(cli);

    match mode {
        AppMode::Help => unreachable!("help exits during clap parsing"),
        AppMode::Version { .. } | AppMode::License | AppMode::Agent { .. } => {
            unreachable!("routed by metadata_command")
        }
        AppMode::Doctor { .. } => {
            unreachable!("doctor is routed by metadata_command")
        }
        AppMode::Completions { shell } => match prompter::completions::generate(shell) {
            Ok(()) => Ok(0),
            Err(e) => Ok(print_error("completions", JsonOutput::Text, &e.to_string())),
        },
        AppMode::Init => match init_scaffold() {
            Ok(()) => Ok(0),
            Err(e) => Ok(print_error(
                "init",
                JsonOutput::Text,
                &format!("Init failed: {e}"),
            )),
        },
        AppMode::List { config, json } => {
            match run_list_stdout(config.as_deref(), JsonOutput::from_flag(json)) {
                Ok(()) => Ok(0),
                Err(e) => Ok(print_error("list", JsonOutput::Text, &e.to_string())),
            }
        }
        AppMode::Tree { config, json } => {
            match run_tree_stdout(config.as_deref(), JsonOutput::from_flag(json)) {
                Ok(()) => Ok(0),
                Err(e) => Ok(print_error("tree", JsonOutput::Text, &e.to_string())),
            }
        }
        AppMode::Validate { config, json } => {
            match run_validate_stdout(config.as_deref(), JsonOutput::from_flag(json)) {
                Ok(()) => {
                    if !json {
                        println!("All profiles valid");
                    }
                    Ok(0)
                }
                Err(errs) => {
                    if json {
                        eprintln!(
                            "{}",
                            err_response(
                                "validate",
                                "ERROR",
                                &errs.to_string(),
                                serde_json::Value::Null
                            )
                        );
                        Ok(1)
                    } else {
                        Ok(print_error("validate", JsonOutput::Text, &errs.to_string()))
                    }
                }
            }
        }
        AppMode::Run {
            profiles,
            family,
            separator,
            pre_prompt,
            post_prompt,
            framing,
            config,
            json,
        } => match run_render_stdout(
            &profiles,
            family.as_ref(),
            separator.as_deref(),
            pre_prompt.as_deref(),
            post_prompt.as_deref(),
            framing,
            config.as_deref(),
            JsonOutput::from_flag(json),
        ) {
            Ok(()) => Ok(0),
            Err(e) => {
                let message = e.to_string();
                if json {
                    eprintln!(
                        "{}",
                        err_response("run", "ERROR", &message, serde_json::Value::Null)
                    );
                    Ok(1)
                } else {
                    Ok(print_error("run", JsonOutput::Text, &message))
                }
            }
        },
    }
}