pub mod args;
mod ask;
mod commands;
mod template;
mod tools;
use std::io::{BufRead, IsTerminal, Write};
use std::path::PathBuf;
use balise_core::error_codes::{CLI0001, CLI0003, ErrorCode};
use balise_core::json::{self, Value};
use balise_core::text::closest;
use args::{GLOBAL, Opt, Parsed};
pub use tools::{Machine, System};
pub struct Command {
pub name: &'static str,
pub summary: &'static str,
pub arguments: &'static str,
pub options: &'static [Opt],
pub exclusive: &'static [&'static [&'static str]],
pub run: fn(&Parsed, &mut Context) -> Result<Report, Failure>,
}
pub const COMMANDS: &[&Command] = &[
&commands::new::COMMAND,
&commands::explain::COMMAND,
&commands::policy::COMMAND,
];
pub struct Context<'a> {
pub cwd: PathBuf,
pub interactive: bool,
pub input: &'a mut dyn BufRead,
pub output: &'a mut dyn Write,
pub machine: &'a dyn Machine,
}
pub struct Report {
pub text: String,
pub data: Value,
}
#[derive(Debug)]
pub struct Failure {
pub code: &'static ErrorCode,
pub message: String,
pub fix: String,
pub exit: u8,
}
impl Failure {
pub fn usage(code: &'static ErrorCode, args: &[(&str, &str)], fix: &[(&str, &str)]) -> Failure {
Failure {
code,
message: code.format_message(args),
fix: code.format_fix(fix),
exit: 2,
}
}
pub fn problem(
code: &'static ErrorCode,
args: &[(&str, &str)],
fix: &[(&str, &str)],
) -> Failure {
Failure {
exit: 1,
..Failure::usage(code, args, fix)
}
}
pub fn report(&self) -> String {
format!(
"{}: {}\n fix: {}\n more: balise explain {}",
self.code.code(),
self.message,
self.fix,
self.code.code()
)
}
fn json(&self) -> Value {
Value::Object(vec![(
"error".into(),
Value::Object(vec![
("code".into(), Value::String(self.code.code().into())),
("message".into(), Value::String(self.message.clone())),
("fix".into(), Value::String(self.fix.clone())),
]),
)])
}
}
pub fn person_at_terminal() -> bool {
const AGENTS: &[&str] = &["CLAUDECODE", "GEMINI_CLI", "CI"];
std::io::stdin().is_terminal()
&& std::io::stdout().is_terminal()
&& AGENTS.iter().all(|name| std::env::var_os(name).is_none())
}
pub fn run(args: &[String], context: &mut Context) -> (String, u8) {
let json = args.iter().any(|arg| arg == "--json");
let result = dispatch(args, context);
match (result, json) {
(Ok(report), false) => (report.text, 0),
(Ok(report), true) => (to_json(&report.data), 0),
(Err(failure), false) => (failure.report(), failure.exit),
(Err(failure), true) => (to_json(&failure.json()), failure.exit),
}
}
fn dispatch(args: &[String], context: &mut Context) -> Result<Report, Failure> {
let (name, rest) = match args.split_first() {
None => return Ok(help(None)),
Some((name, rest)) => (name.as_str(), rest),
};
match name {
"help" | "--help" | "-h" => return Ok(help(rest.first().map(String::as_str))),
"--version" | "-V" => {
return Ok(text(format!("balise {}", env!("CARGO_PKG_VERSION"))));
}
_ => {}
}
let Some(command) = COMMANDS.iter().find(|command| command.name == name) else {
let names: Vec<&str> = COMMANDS.iter().map(|command| command.name).collect();
let suggestion = closest(name, &names)
.map(|name| format!("; did you mean `{name}`?"))
.unwrap_or_default();
return Err(Failure::usage(
CLI0001,
&[("command", name), ("suggestion", &suggestion)],
&[],
));
};
let parsed = args::parse(command.name, rest, command.options, command.exclusive)?;
if parsed.has("--help") {
return Ok(help(Some(command.name)));
}
if parsed.has("--no-input") {
context.interactive = false;
}
(command.run)(&parsed, context)
}
pub(crate) fn text(text: String) -> Report {
Report {
data: Value::String(text.clone()),
text,
}
}
fn help(command: Option<&str>) -> Report {
let mut out = String::new();
match command.and_then(|name| COMMANDS.iter().find(|c| c.name == name)) {
Some(command) => {
out.push_str(&format!(
"balise {} {}\n\n{}\n",
command.name, command.arguments, command.summary
));
out.push_str("\nOptions:\n");
for opt in command.options.iter().chain(GLOBAL) {
let mut line = format!(" {:<26} {}", args::usage(opt), opt.help);
if !opt.values.is_empty() {
line.push_str(&format!(" ({})", opt.values.join(", ")));
}
out.push_str(&line);
out.push('\n');
}
}
None => {
out.push_str(&format!(
"balise {}: the Balise command-line tool\n\nCommands:\n",
env!("CARGO_PKG_VERSION")
));
for command in COMMANDS {
out.push_str(&format!(" {:<10} {}\n", command.name, command.summary));
}
out.push_str("\nRun `balise help <command>` for its options.\n");
}
}
text(out.trim_end().to_string())
}
pub(crate) fn missing(command: &str, missing: &str, example: &str) -> Failure {
Failure::usage(
CLI0003,
&[("command", command), ("missing", missing)],
&[("example", example)],
)
}
fn to_json(value: &Value) -> String {
let mut out = String::new();
json::write_value(&mut out, value);
out
}