#![cfg_attr(
not(test),
deny(clippy::unwrap_used, clippy::panic, clippy::indexing_slicing)
)]
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,
));
}
#[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 } => {
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))
}
}
},
}
}