use std::io::Write as _;
use crate::config::Config;
use crate::ctx::Ctx;
use crate::errors;
use crate::registry::Registry;
pub const fn cli_frontend_compiled() -> bool {
cfg!(feature = "cli")
}
pub const fn http_frontend_compiled() -> bool {
cfg!(feature = "http")
}
pub const fn mcp_frontend_compiled() -> bool {
cfg!(feature = "mcp")
}
pub fn main(cmds: &[&dyn crate::builder::Definable]) -> ! {
if !cmds.is_empty() {
for cmd in cmds {
if let Err(e) = cmd.register(Registry::default()) {
eprintln!("{e}");
std::process::exit(2);
}
}
}
let args: Vec<String> = std::env::args().skip(1).collect();
std::process::exit(run(Registry::default(), args));
}
pub fn main_config(cfg: Config) -> ! {
let args: Vec<String> = std::env::args().skip(1).collect();
std::process::exit(run_config(Registry::default(), args, cfg));
}
pub fn run(reg: &Registry, args: Vec<String>) -> i32 {
run_config(reg, args, Config::default())
}
pub fn run_config(reg: &Registry, args: Vec<String>, cfg: Config) -> i32 {
run_internal(reg, args, cfg, false).0
}
pub fn try_run(reg: &Registry, args: Vec<String>) -> (i32, bool) {
try_run_config(reg, args, Config::default())
}
pub fn try_run_config(reg: &Registry, args: Vec<String>, cfg: Config) -> (i32, bool) {
run_internal(reg, args, cfg, true)
}
fn cli_known_top(reg: &Registry, first: &str) -> bool {
if first.starts_with('-') {
return true; }
for e in reg.all() {
if e.cli.skip {
continue;
}
if e.name.split('.').next() == Some(first) {
return true;
}
if e.cli.aliases.iter().any(|a| a == first) {
return true;
}
if e.cli.default {
return true; }
}
false
}
fn run_internal(reg: &Registry, args: Vec<String>, cfg: Config, composable: bool) -> (i32, bool) {
let words = match resolve_modes(&cfg) {
Ok(m) => m,
Err(e) => {
eprintln!("{e}");
return (2, true);
}
};
if reg.names().is_empty() {
return (0, true);
}
for a in &args {
if a == "--" {
break;
}
if a == "-v" || a == "--version" {
let bin = crate::cli::app::bin_name();
let _ = writeln!(
std::io::stdout(),
"{bin} version {}",
crate::version::version()
);
return (0, true);
}
}
let mut cfg = cfg;
let args = match crate::builtins::strip_xyz_flags(args, &mut cfg) {
Ok(a) => a,
Err(e) => {
eprintln!("xyz: {e}");
return (2, true);
}
};
if cfg.log_level != crate::logx::Level::Unset {
crate::logx::set_level(cfg.log_level);
}
let resolved_lang = if !cfg.lang.is_empty() {
match crate::lang::XyzLang::parse(&cfg.lang) {
Some(l) => l,
None => {
eprintln!("xyz: invalid --xyz.lang {:?} (want en|zh-CN)", cfg.lang);
return (2, true);
}
}
} else {
crate::lang::XyzLang::detect()
};
crate::lang::set(
resolved_lang,
cfg.translations.get(resolved_lang.as_str()).cloned(),
);
let shadowed = shadowed_modes(reg, &words);
if args.is_empty() || args[0] == "--help" || args[0] == "-h" {
let mut stdout = std::io::stdout();
let _ = crate::overview::print_overview(
&mut stdout,
reg,
&words,
&shadowed,
cfg.capabilities,
&cfg.help_before,
&cfg.help_after,
);
return (0, true);
}
let kind = match_mode(&args[0], &words, &shadowed);
if kind != ModeKind::None {
let rest = &args[1..];
if kind == ModeKind::Help {
return (run_help(reg, rest, &words, &shadowed, cfg), true);
}
if has_help_flag(rest) {
return (print_mode_help(kind, &words), true);
}
let ctx = Ctx::new();
spawn_signal_watcher(ctx.clone());
crate::logx::debugf(format_args!(
"dispatch: mode word='{}' addr={} tokens={} timeout={:?} cors={}",
args[0],
cfg.addr,
cfg.bearer_tokens.len(),
cfg.timeout,
cfg.cors_origins.len()
));
match kind {
ModeKind::Serve => {
if cfg.capabilities.no_http {
crate::logx::warnf(format_args!(
"{}",
crate::lang::tf("warn.mode_disabled", &[&words.serve, "HTTP"])
));
return (1, true);
}
let mount = !cfg.capabilities.no_mcp;
return (run_serve(&ctx, reg, rest, cfg, mount), true);
}
ModeKind::Http => {
if cfg.capabilities.no_http {
crate::logx::warnf(format_args!(
"{}",
crate::lang::tf("warn.mode_disabled", &[&words.http, "HTTP"])
));
return (1, true);
}
return (run_serve(&ctx, reg, rest, cfg, false), true);
}
_ => {}
}
let mcp_word = words.mcp.clone();
if cfg.capabilities.no_mcp {
crate::logx::warnf(format_args!(
"{}",
crate::lang::tf("warn.mode_disabled", &[&mcp_word, "MCP"])
));
return (1, true);
}
return (run_mcp(&ctx, reg, rest, cfg), true);
}
if cfg.capabilities.no_cli {
crate::logx::warnf(format_args!(
"{}",
crate::lang::tf("warn.no_cli", &[&words.mcp, &words.serve])
));
return (1, true);
}
if composable && !args.is_empty() && !cli_known_top(reg, &args[0]) {
return (0, false);
}
let ctx = Ctx::new();
spawn_signal_watcher(ctx.clone());
(run_cli(&ctx, reg, &args, &cfg), true)
}
pub struct Words {
pub(crate) serve: String,
pub(crate) http: String,
pub(crate) mcp: String,
pub(crate) help: String,
}
impl Words {
fn all(&self) -> [&str; 4] {
[
self.serve.as_str(),
self.http.as_str(),
self.mcp.as_str(),
self.help.as_str(),
]
}
}
#[derive(PartialEq, Eq, Clone, Copy)]
pub(crate) enum ModeKind {
Serve,
Http,
Mcp,
Help,
None,
}
fn resolve_modes(cfg: &Config) -> errors::Result<Words> {
let pick = |v: &str, dflt: &str| {
if v.is_empty() {
dflt.to_string()
} else {
v.to_string()
}
};
let w = Words {
serve: pick(&cfg.modes.serve, "serve"),
http: pick(&cfg.modes.http, "http"),
mcp: pick(&cfg.modes.mcp, "mcp"),
help: pick(&cfg.modes.help, "help"),
};
for word in w.all() {
if word.starts_with('-') || word.chars().any(|c| c == ' ' || c == '\t') {
return Err(errors::Error::new(
errors::Kind::Internal,
format!("xyz: invalid mode word {word:?} (no leading dash, no whitespace)"),
));
}
}
let words = w.all();
for i in 0..words.len() {
for j in (i + 1)..words.len() {
if words[i] == words[j] {
return Err(errors::Error::new(
errors::Kind::Internal,
format!(
"xyz: mode words must be pairwise distinct (serve={:?} http={:?} mcp={:?} help={:?})",
w.serve, w.http, w.mcp, w.help
),
));
}
}
}
Ok(w)
}
fn shadowed_modes(reg: &Registry, w: &Words) -> std::collections::BTreeSet<String> {
let mut tops = std::collections::BTreeSet::new();
for name in reg.names() {
if let Some(e) = reg.get(&name)
&& e.cli.skip
{
continue;
}
if let Some(top) = name.split('.').next() {
tops.insert(top.to_string());
}
}
w.all()
.iter()
.filter(|word| tops.contains(**word))
.map(|word| word.to_string())
.collect()
}
fn match_mode(token: &str, w: &Words, shadowed: &std::collections::BTreeSet<String>) -> ModeKind {
if let Some(rest) = token.strip_prefix("xyz.") {
return match rest {
r if r == w.serve => ModeKind::Serve,
r if r == w.http => ModeKind::Http,
r if r == w.mcp => ModeKind::Mcp,
r if r == w.help => ModeKind::Help,
_ => ModeKind::None,
};
}
if shadowed.contains(token) {
return ModeKind::None;
}
match token {
t if t == w.serve => ModeKind::Serve,
t if t == w.http => ModeKind::Http,
t if t == w.mcp => ModeKind::Mcp,
t if t == w.help => ModeKind::Help,
_ => ModeKind::None,
}
}
fn has_help_flag(args: &[String]) -> bool {
for a in args {
if a == "--" {
break;
}
if a == "-h" || a == "--help" {
return true;
}
}
false
}
fn run_help(
reg: &Registry,
rest: &[String],
w: &Words,
shadowed: &std::collections::BTreeSet<String>,
cfg: Config,
) -> i32 {
if rest.is_empty() {
let mut stdout = std::io::stdout();
let _ = crate::overview::print_overview(
&mut stdout,
reg,
w,
shadowed,
cfg.capabilities,
&cfg.help_before,
&cfg.help_after,
);
return 0;
}
let kind = match_mode(&rest[0], w, shadowed);
match kind {
ModeKind::Help | ModeKind::None if kind == ModeKind::Help => {
let mut stdout = std::io::stdout();
let _ = crate::overview::print_overview(
&mut stdout,
reg,
w,
shadowed,
cfg.capabilities,
&cfg.help_before,
&cfg.help_after,
);
0
}
ModeKind::Serve | ModeKind::Http | ModeKind::Mcp => print_mode_help(kind, w),
_ => {
let mut path: Vec<String> = Vec::new();
for r in rest {
path.extend(r.split('.').map(|seg| seg.to_string()));
}
if cfg.capabilities.no_cli {
crate::logx::warnf(format_args!(
"{}",
crate::lang::tf("warn.no_cli", &[&w.mcp, &w.serve])
));
return 1;
}
path.push("-h".to_string());
run_cli(&Ctx::new(), reg, &path, &cfg)
}
}
}
pub(crate) fn mode_help_text(kind: ModeKind, w: &Words) -> String {
let key = match kind {
ModeKind::Serve => "mode_help.serve",
ModeKind::Http => "mode_help.http",
ModeKind::Mcp => "mode_help.mcp",
_ => "mode_help.help",
};
let word = match kind {
ModeKind::Serve => &w.serve,
ModeKind::Http => &w.http,
ModeKind::Mcp => &w.mcp,
_ => &w.help,
};
crate::lang::tf(key, &[word])
}
fn print_mode_help(kind: ModeKind, w: &Words) -> i32 {
let _ = writeln!(std::io::stdout(), "{}", mode_help_text(kind, w));
0
}
fn spawn_signal_watcher(ctx: Ctx) {
#[cfg(not(any(feature = "http-stack", feature = "cli")))]
let _ = &ctx;
#[cfg(feature = "http-stack")]
{
std::thread::spawn(move || {
let rt = match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
Ok(rt) => rt,
Err(_) => return,
};
rt.block_on(async move {
let _ = tokio::signal::ctrl_c().await;
ctx.cancel();
});
});
}
#[cfg(all(not(feature = "http-stack"), feature = "cli"))]
{
let _ = ctrlc::set_handler(move || {
ctx.cancel();
});
}
}
#[cfg(feature = "cli")]
fn run_cli(ctx: &Ctx, reg: &Registry, args: &[String], cfg: &Config) -> i32 {
crate::cli::run_context(
ctx,
reg,
args,
crate::cli::Options {
format: Some(cfg.format.clone()),
format_from_flag: cfg.format_from_flag,
format_interactive: Some(cfg.format_interactive.clone()),
format_piped: Some(cfg.format_piped.clone()),
..Default::default()
},
)
}
#[cfg(not(feature = "cli"))]
fn run_cli(_ctx: &Ctx, _reg: &Registry, _args: &[String], _cfg: &Config) -> i32 {
eprintln!("xyz: {}", crate::lang::tf("stub.not_compiled", &["CLI"]));
1
}
#[cfg(feature = "http")]
fn run_serve(ctx: &Ctx, reg: &Registry, args: &[String], cfg: Config, mount_mcp: bool) -> i32 {
crate::httpapi::serve(ctx, reg, args, cfg, mount_mcp)
}
#[cfg(not(feature = "http"))]
fn run_serve(_ctx: &Ctx, _reg: &Registry, _args: &[String], _cfg: Config, _mount_mcp: bool) -> i32 {
eprintln!("xyz: {}", crate::lang::tf("stub.not_compiled", &["HTTP"]));
1
}
#[cfg(feature = "mcp")]
fn run_mcp(ctx: &Ctx, reg: &Registry, args: &[String], cfg: Config) -> i32 {
crate::mcp::run_with_config(ctx, reg, args, cfg)
}
#[cfg(not(feature = "mcp"))]
fn run_mcp(_ctx: &Ctx, _reg: &Registry, _args: &[String], _cfg: Config) -> i32 {
eprintln!("xyz: {}", crate::lang::tf("stub.not_compiled", &["MCP"]));
1
}