// Pure CLI core: argument normalization, option parsing and the
// `run_cli` dispatcher. Everything here is deterministic and testable
// without spawning a process.
///|
/// The parsed command-line options shared by all commands.
pub struct CliOptions {
mut resource : String?
rels : Array[String]
mut origin : String?
mut input : String?
mut input_file : String?
mut media_type : String?
}
///|
/// Drop leading program/runtime paths from the argument list. The argv
/// shape differs per backend:
///
/// native / wasm-gc: [program, args...]
/// js: [node, program.js, args...]
///
/// Program paths are recognised by shape (a path ending in a program
/// extension, or a known runtime binary name) and dropped until the
/// first known command or the user's first argument is reached, so the
/// backend never leaks into `run_cli`.
pub fn normalize_cli_args(args : Array[String]) -> Array[String] {
let known = [
"request", "parse", "validate", "query", "canonicalize", "audit", "stats", "version",
"help",
]
let mut i = 0
while i < args.length() {
let a = args[i]
let mut is_command = false
for k in known {
if a == k || a == k.to_upper() {
is_command = true
}
}
if is_command {
break
}
if !is_program_path(a) {
break
}
i = i + 1
}
args[i:].to_owned()
}
///|
/// Whether a leading argument is the running program (or its runtime
/// binary) rather than a user-supplied command. Program paths carry a
/// directory separator and a known program extension; runtime binaries
/// are recognised by their bare names.
fn is_program_path(arg : String) -> Bool {
let lower = arg.to_lower()
if lower == "node" ||
lower == "deno" ||
lower == "bun" ||
lower.has_suffix("/node") ||
lower.has_suffix("/deno") ||
lower.has_suffix("/bun") {
return true
}
if !(arg.contains("/") || arg.contains("\\")) {
return false
}
lower.has_suffix(".exe") ||
lower.has_suffix(".js") ||
lower.has_suffix(".cjs") ||
lower.has_suffix(".mjs") ||
lower.has_suffix(".wasm")
}
///|
/// Parse options after the command word. Fails with a message on unknown
/// options or missing values.
fn parse_options(args : Array[String]) -> Result[CliOptions, String] {
let opts : CliOptions = {
resource: None,
rels: [],
origin: None,
input: None,
input_file: None,
media_type: None,
}
let mut i = 0
while i < args.length() {
let a = args[i]
if i + 1 >= args.length() {
return Err("option \{a} requires a value")
}
let value = args[i + 1]
if a == "--resource" {
opts.resource = Some(value)
} else if a == "--rel" {
opts.rels.push(value)
} else if a == "--origin" {
opts.origin = Some(value)
} else if a == "--input" {
opts.input = Some(value)
} else if a == "--input-file" {
opts.input_file = Some(value)
} else if a == "--type" {
opts.media_type = Some(value)
} else {
return Err("unknown option: \{a}")
}
i = i + 2
}
Ok(opts)
}
///|
/// Run the CLI on already-normalized arguments and return the complete
/// JSON output line. Pure: never exits, never panics.
pub fn run_cli(args : Array[String]) -> String {
let args = normalize_cli_args(args)
if args.length() == 0 {
return cli_err_json(
"", "MissingCommand", "no command given; run 'webfinger-tool help'",
)
}
let command = args[0].to_lower()
let rest : Array[String] = args[1:].to_owned()
match parse_options(rest) {
Err(msg) => cli_err_json(command, "BadOption", msg)
Ok(opts) => dispatch(command, opts)
}
}
///|
/// Dispatch to a command implementation.
fn dispatch(command : String, opts : CliOptions) -> String {
match command {
"request" => cmd_request(opts)
"parse" => cmd_parse(opts)
"validate" => cmd_validate(opts)
"query" => cmd_query(opts)
"canonicalize" => cmd_canonicalize(opts)
"audit" => cmd_audit(opts)
"stats" => cmd_stats(opts)
"version" => cmd_version()
"help" => cmd_help()
_ =>
cli_err_json(
command,
"UnknownCommand",
"unknown command '\{command}'; run 'webfinger-tool help'",
)
}
}