1mod app;
8pub mod args;
9mod ask;
10pub mod check;
11mod commands;
12pub mod dev;
13mod journal;
14mod project;
15pub mod style;
16mod template;
17mod tools;
18mod value;
19
20use std::io::{BufRead, IsTerminal, Write};
21use std::path::PathBuf;
22
23use balise_core::error_codes::{CLI0001, CLI0003, ErrorCode};
24use balise_core::json::{self, Value};
25use balise_core::text::closest;
26
27use args::{GLOBAL, Opt, Parsed};
28pub use tools::{Machine, System};
29
30pub struct Command {
32 pub name: &'static str,
33 pub summary: &'static str,
34 pub arguments: &'static str,
36 pub options: &'static [Opt],
37 pub exclusive: &'static [&'static [&'static str]],
39 pub for_agents: bool,
42 pub run: fn(&Parsed, &mut Context) -> Result<Report, Failure>,
43}
44
45pub const COMMANDS: &[&Command] = &[
47 &commands::new::COMMAND,
48 &commands::dev::COMMAND,
49 &commands::check::COMMAND,
50 &commands::routes::COMMAND,
51 &commands::why::COMMAND,
52 &commands::logs::COMMAND,
53 &commands::trace::COMMAND,
54 &commands::config::COMMAND,
55 &commands::explain::COMMAND,
56 &commands::policy::COMMAND,
57];
58
59pub struct Context<'a> {
61 pub cwd: PathBuf,
62 pub interactive: bool,
64 pub input: &'a mut dyn BufRead,
65 pub output: &'a mut dyn Write,
67 pub color: bool,
69 pub machine: &'a dyn Machine,
70}
71
72pub struct Report {
74 pub text: String,
75 pub data: Value,
76 pub exit: u8,
78}
79
80impl Report {
81 pub fn new(text: String, data: Value) -> Report {
82 Report {
83 text,
84 data,
85 exit: 0,
86 }
87 }
88}
89
90pub struct Outcome {
92 pub text: String,
93 pub exit: u8,
94 pub failed: bool,
96}
97
98#[derive(Debug)]
101pub struct Failure {
102 pub code: &'static ErrorCode,
103 pub message: String,
104 pub fix: String,
105 pub exit: u8,
106}
107
108impl Failure {
109 pub fn usage(code: &'static ErrorCode, args: &[(&str, &str)], fix: &[(&str, &str)]) -> Failure {
111 Failure {
112 code,
113 message: code.format_message(args),
114 fix: code.format_fix(fix),
115 exit: 2,
116 }
117 }
118
119 pub fn problem(
121 code: &'static ErrorCode,
122 args: &[(&str, &str)],
123 fix: &[(&str, &str)],
124 ) -> Failure {
125 Failure {
126 exit: 1,
127 ..Failure::usage(code, args, fix)
128 }
129 }
130
131 pub fn report(&self) -> String {
133 format!(
134 "{}: {}\n fix: {}\n more: balise explain {}",
135 self.code.code(),
136 self.message,
137 self.fix,
138 self.code.code()
139 )
140 }
141
142 pub fn from_json(value: &Value) -> Option<Failure> {
144 let Value::Object(fields) = value else {
145 return None;
146 };
147 let (_, Value::Object(error)) = fields.iter().find(|(key, _)| key == "error")? else {
148 return None;
149 };
150 let text = |key: &str| {
151 error.iter().find_map(|(k, v)| match (k == key, v) {
152 (true, Value::String(text)) => Some(text.clone()),
153 _ => None,
154 })
155 };
156 Some(Failure {
157 code: balise_core::error_codes::find(&text("code")?)?,
158 message: text("message")?,
159 fix: text("fix")?,
160 exit: 1,
161 })
162 }
163
164 fn json(&self) -> Value {
165 Value::Object(vec![(
166 "error".into(),
167 Value::Object(vec![
168 ("code".into(), Value::String(self.code.code().into())),
169 ("message".into(), Value::String(self.message.clone())),
170 ("fix".into(), Value::String(self.fix.clone())),
171 ]),
172 )])
173 }
174}
175
176pub fn person_at_terminal() -> bool {
179 const AGENTS: &[&str] = &["CLAUDECODE", "GEMINI_CLI", "CI"];
180 std::io::stdin().is_terminal()
181 && std::io::stdout().is_terminal()
182 && AGENTS.iter().all(|name| std::env::var_os(name).is_none())
183}
184
185pub fn run(args: &[String], context: &mut Context) -> (String, u8) {
188 let outcome = execute(args, context);
189 (outcome.text, outcome.exit)
190}
191
192pub fn execute(args: &[String], context: &mut Context) -> Outcome {
194 let json = args.iter().any(|arg| arg == "--json");
195 let result = dispatch(args, context);
196 let (text, exit, failed) = match (result, json) {
197 (Ok(report), false) => (report.text, report.exit, false),
198 (Ok(report), true) => (to_json(&report.data), report.exit, false),
199 (Err(failure), false) => (failure.report(), failure.exit, true),
200 (Err(failure), true) => (to_json(&failure.json()), failure.exit, true),
201 };
202 Outcome { text, exit, failed }
203}
204
205fn dispatch(args: &[String], context: &mut Context) -> Result<Report, Failure> {
206 let (name, rest) = match args.split_first() {
207 None => return Ok(help(None)),
208 Some((name, rest)) => (name.as_str(), rest),
209 };
210 match name {
211 "help" | "--help" | "-h" => return Ok(help(rest.first().map(String::as_str))),
212 "--version" | "-V" => {
213 return Ok(text(format!("balise {}", env!("CARGO_PKG_VERSION"))));
214 }
215 _ => {}
216 }
217 let Some(command) = COMMANDS.iter().find(|command| command.name == name) else {
218 let names: Vec<&str> = COMMANDS.iter().map(|command| command.name).collect();
219 let suggestion = closest(name, &names)
220 .map(|name| format!("; did you mean `{name}`?"))
221 .unwrap_or_default();
222 return Err(Failure::usage(
223 CLI0001,
224 &[("command", name), ("suggestion", &suggestion)],
225 &[],
226 ));
227 };
228 let parsed = args::parse(command.name, rest, command.options, command.exclusive)?;
229 if parsed.has("--help") {
230 return Ok(help(Some(command.name)));
231 }
232 if parsed.has("--no-input") {
233 context.interactive = false;
234 }
235 (command.run)(&parsed, context)
236}
237
238pub(crate) fn text(text: String) -> Report {
240 Report::new(text.clone(), Value::String(text))
241}
242
243fn help(command: Option<&str>) -> Report {
245 let mut out = String::new();
246 match command.and_then(|name| COMMANDS.iter().find(|c| c.name == name)) {
247 Some(command) => {
248 out.push_str(&format!(
249 "balise {} {}\n\n{}\n",
250 command.name, command.arguments, command.summary
251 ));
252 out.push_str("\nOptions:\n");
253 for opt in command.options.iter().chain(GLOBAL) {
254 let mut line = format!(" {:<26} {}", args::usage(opt), opt.help);
255 if !opt.values.is_empty() {
256 line.push_str(&format!(" ({})", opt.values.join(", ")));
257 }
258 out.push_str(&line);
259 out.push('\n');
260 }
261 }
262 None => {
263 out.push_str(&format!(
264 "balise {}: the Balise command-line tool\n\nCommands:\n",
265 env!("CARGO_PKG_VERSION")
266 ));
267 for command in COMMANDS {
268 out.push_str(&format!(" {:<10} {}\n", command.name, command.summary));
269 }
270 out.push_str("\nRun `balise help <command>` for its options.\n");
271 }
272 }
273 text(out.trim_end().to_string())
274}
275
276pub(crate) fn missing(command: &str, missing: &str, example: &str) -> Failure {
278 Failure::usage(
279 CLI0003,
280 &[("command", command), ("missing", missing)],
281 &[("example", example)],
282 )
283}
284
285fn to_json(value: &Value) -> String {
286 let mut out = String::new();
287 json::write_value(&mut out, value);
288 out
289}