pub mod argv;
pub mod cmd;
pub mod commands;
pub mod context;
pub mod help;
pub mod output;
pub mod render;
pub mod seams;
pub mod shell;
use std::ffi::OsStr;
use std::path::Path;
use argv::{Globals, parse_globals};
use commands::{resolve, unknown_command_error};
use context::{Cli, NO_WORKSPACE_OK, REMOTE_OK, SKIP_FRESHNESS};
use output::{CliError, EXIT_OK, Io, Result, render_error};
use render::Style;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const SPEC_VERSION: &str = "4.5";
#[must_use]
pub fn build_info() -> omgbase_surface::BuildInfo {
omgbase_surface::BuildInfo {
version: VERSION.to_owned(),
cli_spec: Some(SPEC_VERSION.to_owned()),
commit: option_env!("OMGBASE_COMMIT").map(str::to_owned),
built: option_env!("OMGBASE_BUILT").map(str::to_owned),
rustc: option_env!("OMGBASE_RUSTC").map(str::to_owned),
}
}
pub fn prog_name(argv0: Option<&OsStr>) -> String {
let Some(a) = argv0 else {
return "omg".to_owned();
};
let base = Path::new(a)
.file_name()
.map(|b| b.to_string_lossy().into_owned())
.unwrap_or_default();
if base.is_empty() || base.ends_with(".js") || base.ends_with(".cjs") || base.ends_with(".mjs")
{
"omg".to_owned()
} else {
base
}
}
pub fn run(argv: &[String], prog: &str) -> i32 {
let parsed = match parse_globals(argv) {
Ok(p) => p,
Err((flags, err)) => {
let style = Style::detect(flags.no_color, Io.stdout_tty());
return render_error(&err, Io, &style, flags.machine());
}
};
let flags: Globals = parsed.flags;
let mut command = parsed.command;
if flags.version && command.is_none() {
Io.out(VERSION);
return EXIT_OK;
}
if command.is_none() {
command = Some("help".to_owned());
}
let command = command.unwrap_or_default();
let machine = flags.machine();
let seams = match seams::read_env_seams() {
Ok(s) => s,
Err(e) => {
let style = Style::detect(flags.no_color, Io.stdout_tty());
return render_error(&e, Io, &style, machine);
}
};
let mut cli = Cli::new(flags, prog, seams);
if resolve(&command).is_none() {
let err = unknown_command_error(prog, &command);
return render_error(&err, cli.io, &cli.style, machine);
}
let code = match run_command(&mut cli, &command, &parsed.rest) {
Ok(code) => code,
Err(e) => render_error(&e, cli.io, &cli.style, machine),
};
cli.close();
code
}
fn check_headers(flags: &Globals) -> Result<()> {
let Some(server) = flags.server.as_deref() else {
return Ok(());
};
if flags.headers.is_empty() {
return Ok(());
}
let is_http = server.starts_with("http://") || server.starts_with("https://");
if !is_http {
return Err(CliError::usage(
"-H/--header only applies to an http(s) --server url",
));
}
for h in &flags.headers {
if !h.contains(':') {
return Err(CliError::usage(format!(
"invalid header {} — expected \"Name: value\"",
output::js_string(h)
)));
}
}
Ok(())
}
fn asks_for_help(flag: bool, name: &str, rest: &[String]) -> bool {
if flag {
return true;
}
let first = rest.first().map(String::as_str);
match name {
"config" | "embed" => first == Some("help"),
"node" => first.is_none_or(|f| f == "help"),
"source" => first.is_none_or(|f| f == "help" || f == "--help"),
_ => false,
}
}
pub fn run_command(cli: &mut Cli, command: &str, rest: &[String]) -> Result<i32> {
let Some(resolved) = resolve(command) else {
return Err(unknown_command_error(&cli.prog, command));
};
let help = asks_for_help(cli.flags.help, resolved.name, rest);
let remote = cli.flags.server.is_some() && !help;
if remote {
if !REMOTE_OK.contains(&resolved.name) {
return Err(CliError::usage(format!(
"--server is not supported for '{}' — it needs local ref resolution or a working tree; run it against a local workspace",
resolved.name
)));
}
check_headers(&cli.flags)?;
}
let mut args: Vec<String> = Vec::with_capacity(rest.len() + 1);
if cli.flags.help {
args.push("--help".to_owned());
}
args.extend(rest.iter().cloned());
if !help
&& !remote
&& !cli.flags.stale
&& !SKIP_FRESHNESS.contains(&resolved.name)
&& !NO_WORKSPACE_OK.contains(&resolved.name)
{
cli.freshness()?;
}
match (resolved.run)(cli, &args) {
Err(CliError::Usage {
message,
hint: None,
}) if message.starts_with("unknown option '") => Err(CliError::usage_hint(
message,
format!(
"run '{} {} --help' for the options",
cli.prog, resolved.name
),
)),
other => other,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn spec_version_matches_the_spec() {
let version = std::fs::read_to_string(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../spec/cli/VERSION"
))
.expect("spec/cli/VERSION");
assert_eq!(SPEC_VERSION, version.trim());
}
#[test]
fn program_name() {
assert_eq!(prog_name(None), "omg");
assert_eq!(prog_name(Some(OsStr::new("/usr/bin/omgbase"))), "omgbase");
assert_eq!(prog_name(Some(OsStr::new("/x/bin/omg"))), "omg");
assert_eq!(prog_name(Some(OsStr::new("dist/src/main.js"))), "omg");
}
}