1use std::io::Write as _;
19
20use crate::config::Config;
21use crate::ctx::Ctx;
22use crate::errors;
23use crate::registry::Registry;
24
25pub const fn cli_frontend_compiled() -> bool {
27 cfg!(feature = "cli")
28}
29
30pub const fn http_frontend_compiled() -> bool {
31 cfg!(feature = "http")
32}
33
34pub const fn mcp_frontend_compiled() -> bool {
35 cfg!(feature = "mcp")
36}
37
38pub fn main(cmds: &[&dyn crate::builder::Definable]) -> ! {
42 if !cmds.is_empty() {
43 for cmd in cmds {
44 if let Err(e) = cmd.register(Registry::default()) {
45 eprintln!("{e}");
46 std::process::exit(2);
47 }
48 }
49 }
50 let args: Vec<String> = std::env::args().skip(1).collect();
51 std::process::exit(run(Registry::default(), args));
52}
53
54pub fn main_config(cfg: Config) -> ! {
56 let args: Vec<String> = std::env::args().skip(1).collect();
57 std::process::exit(run_config(Registry::default(), args, cfg));
58}
59
60pub fn run(reg: &Registry, args: Vec<String>) -> i32 {
62 run_config(reg, args, Config::default())
63}
64
65pub fn run_config(reg: &Registry, args: Vec<String>, cfg: Config) -> i32 {
67 run_internal(reg, args, cfg, false).0
68}
69
70pub fn try_run(reg: &Registry, args: Vec<String>) -> (i32, bool) {
75 try_run_config(reg, args, Config::default())
76}
77
78pub fn try_run_config(reg: &Registry, args: Vec<String>, cfg: Config) -> (i32, bool) {
80 run_internal(reg, args, cfg, true)
81}
82
83fn cli_known_top(reg: &Registry, first: &str) -> bool {
85 if first.starts_with('-') {
86 return true; }
88 for e in reg.all() {
89 if e.cli.skip {
90 continue;
91 }
92 if e.name.split('.').next() == Some(first) {
93 return true;
94 }
95 if e.cli.aliases.iter().any(|a| a == first) {
96 return true;
97 }
98 if e.cli.default {
99 return true; }
101 }
102 false
103}
104
105fn run_internal(reg: &Registry, args: Vec<String>, cfg: Config, composable: bool) -> (i32, bool) {
106 let words = match resolve_modes(&cfg) {
107 Ok(m) => m,
108 Err(e) => {
109 eprintln!("{e}");
110 return (2, true);
111 }
112 };
113 if reg.names().is_empty() {
115 return (0, true);
116 }
117 for a in &args {
120 if a == "--" {
121 break;
122 }
123 if a == "-v" || a == "--version" {
124 let bin = crate::cli::app::bin_name();
125 let _ = writeln!(
127 std::io::stdout(),
128 "{bin} version {}",
129 crate::version::version()
130 );
131 return (0, true);
132 }
133 }
134 let mut cfg = cfg;
136 let args = match crate::builtins::strip_xyz_flags(args, &mut cfg) {
137 Ok(a) => a,
138 Err(e) => {
139 eprintln!("xyz: {e}");
140 return (2, true);
141 }
142 };
143 if cfg.log_level != crate::logx::Level::Unset {
144 crate::logx::set_level(cfg.log_level);
145 }
146 let resolved_lang = if !cfg.lang.is_empty() {
148 match crate::lang::XyzLang::parse(&cfg.lang) {
149 Some(l) => l,
150 None => {
151 eprintln!("xyz: invalid --xyz.lang {:?} (want en|zh-CN)", cfg.lang);
152 return (2, true);
153 }
154 }
155 } else {
156 crate::lang::XyzLang::detect()
157 };
158 crate::lang::set(
159 resolved_lang,
160 cfg.translations.get(resolved_lang.as_str()).cloned(),
161 );
162 let shadowed = shadowed_modes(reg, &words);
165 if args.is_empty() || args[0] == "--help" || args[0] == "-h" {
168 let mut stdout = std::io::stdout();
169 let _ = crate::overview::print_overview(
170 &mut stdout,
171 reg,
172 &words,
173 &shadowed,
174 cfg.capabilities,
175 &cfg.help_before,
176 &cfg.help_after,
177 );
178 return (0, true);
179 }
180 let kind = match_mode(&args[0], &words, &shadowed);
182 if kind != ModeKind::None {
183 let rest = &args[1..];
184 if kind == ModeKind::Help {
185 return (run_help(reg, rest, &words, &shadowed, cfg), true);
186 }
187 if has_help_flag(rest) {
189 return (print_mode_help(kind, &words), true);
190 }
191 let ctx = Ctx::new();
193 spawn_signal_watcher(ctx.clone());
194 crate::logx::debugf(format_args!(
195 "dispatch: mode word='{}' addr={} tokens={} timeout={:?} cors={}",
196 args[0],
197 cfg.addr,
198 cfg.bearer_tokens.len(),
199 cfg.timeout,
200 cfg.cors_origins.len()
201 ));
202 match kind {
203 ModeKind::Serve => {
204 if cfg.capabilities.no_http {
205 crate::logx::warnf(format_args!(
206 "{}",
207 crate::lang::tf("warn.mode_disabled", &[&words.serve, "HTTP"])
208 ));
209 return (1, true);
210 }
211 let mount = !cfg.capabilities.no_mcp;
213 return (run_serve(&ctx, reg, rest, cfg, mount), true);
214 }
215 ModeKind::Http => {
216 if cfg.capabilities.no_http {
217 crate::logx::warnf(format_args!(
218 "{}",
219 crate::lang::tf("warn.mode_disabled", &[&words.http, "HTTP"])
220 ));
221 return (1, true);
222 }
223 return (run_serve(&ctx, reg, rest, cfg, false), true);
225 }
226 _ => {}
227 }
228 let mcp_word = words.mcp.clone();
229 if cfg.capabilities.no_mcp {
230 crate::logx::warnf(format_args!(
231 "{}",
232 crate::lang::tf("warn.mode_disabled", &[&mcp_word, "MCP"])
233 ));
234 return (1, true);
235 }
236 return (run_mcp(&ctx, reg, rest, cfg), true);
237 }
238 if cfg.capabilities.no_cli {
239 crate::logx::warnf(format_args!(
240 "{}",
241 crate::lang::tf("warn.no_cli", &[&words.mcp, &words.serve])
242 ));
243 return (1, true);
244 }
245 if composable && !args.is_empty() && !cli_known_top(reg, &args[0]) {
246 return (0, false);
248 }
249 let ctx = Ctx::new();
250 spawn_signal_watcher(ctx.clone());
251 (run_cli(&ctx, reg, &args, &cfg), true)
252}
253
254pub struct Words {
256 pub(crate) serve: String,
257 pub(crate) http: String,
258 pub(crate) mcp: String,
259 pub(crate) help: String,
260}
261
262impl Words {
263 fn all(&self) -> [&str; 4] {
265 [
266 self.serve.as_str(),
267 self.http.as_str(),
268 self.mcp.as_str(),
269 self.help.as_str(),
270 ]
271 }
272}
273
274#[derive(PartialEq, Eq, Clone, Copy)]
276pub(crate) enum ModeKind {
277 Serve,
278 Http,
279 Mcp,
280 Help,
281 None,
282}
283
284fn resolve_modes(cfg: &Config) -> errors::Result<Words> {
287 let pick = |v: &str, dflt: &str| {
288 if v.is_empty() {
289 dflt.to_string()
290 } else {
291 v.to_string()
292 }
293 };
294 let w = Words {
295 serve: pick(&cfg.modes.serve, "serve"),
296 http: pick(&cfg.modes.http, "http"),
297 mcp: pick(&cfg.modes.mcp, "mcp"),
298 help: pick(&cfg.modes.help, "help"),
299 };
300 for word in w.all() {
301 if word.starts_with('-') || word.chars().any(|c| c == ' ' || c == '\t') {
302 return Err(errors::Error::new(
303 errors::Kind::Internal,
304 format!("xyz: invalid mode word {word:?} (no leading dash, no whitespace)"),
305 ));
306 }
307 }
308 let words = w.all();
309 for i in 0..words.len() {
310 for j in (i + 1)..words.len() {
311 if words[i] == words[j] {
312 return Err(errors::Error::new(
313 errors::Kind::Internal,
314 format!(
315 "xyz: mode words must be pairwise distinct (serve={:?} http={:?} mcp={:?} help={:?})",
316 w.serve, w.http, w.mcp, w.help
317 ),
318 ));
319 }
320 }
321 }
322 Ok(w)
323}
324
325fn shadowed_modes(reg: &Registry, w: &Words) -> std::collections::BTreeSet<String> {
328 let mut tops = std::collections::BTreeSet::new();
329 for name in reg.names() {
330 if let Some(e) = reg.get(&name)
331 && e.cli.skip
332 {
333 continue;
334 }
335 if let Some(top) = name.split('.').next() {
336 tops.insert(top.to_string());
337 }
338 }
339 w.all()
340 .iter()
341 .filter(|word| tops.contains(**word))
342 .map(|word| word.to_string())
343 .collect()
344}
345
346fn match_mode(token: &str, w: &Words, shadowed: &std::collections::BTreeSet<String>) -> ModeKind {
348 if let Some(rest) = token.strip_prefix("xyz.") {
349 return match rest {
350 r if r == w.serve => ModeKind::Serve,
351 r if r == w.http => ModeKind::Http,
352 r if r == w.mcp => ModeKind::Mcp,
353 r if r == w.help => ModeKind::Help,
354 _ => ModeKind::None,
355 };
356 }
357 if shadowed.contains(token) {
358 return ModeKind::None;
359 }
360 match token {
361 t if t == w.serve => ModeKind::Serve,
362 t if t == w.http => ModeKind::Http,
363 t if t == w.mcp => ModeKind::Mcp,
364 t if t == w.help => ModeKind::Help,
365 _ => ModeKind::None,
366 }
367}
368
369fn has_help_flag(args: &[String]) -> bool {
371 for a in args {
372 if a == "--" {
373 break;
374 }
375 if a == "-h" || a == "--help" {
376 return true;
377 }
378 }
379 false
380}
381
382fn run_help(
385 reg: &Registry,
386 rest: &[String],
387 w: &Words,
388 shadowed: &std::collections::BTreeSet<String>,
389 cfg: Config,
390) -> i32 {
391 if rest.is_empty() {
392 let mut stdout = std::io::stdout();
393 let _ = crate::overview::print_overview(
394 &mut stdout,
395 reg,
396 w,
397 shadowed,
398 cfg.capabilities,
399 &cfg.help_before,
400 &cfg.help_after,
401 );
402 return 0;
403 }
404 let kind = match_mode(&rest[0], w, shadowed);
405 match kind {
406 ModeKind::Help | ModeKind::None if kind == ModeKind::Help => {
407 let mut stdout = std::io::stdout();
408 let _ = crate::overview::print_overview(
409 &mut stdout,
410 reg,
411 w,
412 shadowed,
413 cfg.capabilities,
414 &cfg.help_before,
415 &cfg.help_after,
416 );
417 0
418 }
419 ModeKind::Serve | ModeKind::Http | ModeKind::Mcp => print_mode_help(kind, w),
420 _ => {
421 let mut path: Vec<String> = Vec::new();
423 for r in rest {
424 path.extend(r.split('.').map(|seg| seg.to_string()));
425 }
426 if cfg.capabilities.no_cli {
427 crate::logx::warnf(format_args!(
428 "{}",
429 crate::lang::tf("warn.no_cli", &[&w.mcp, &w.serve])
430 ));
431 return 1;
432 }
433 path.push("-h".to_string());
434 run_cli(&Ctx::new(), reg, &path, &cfg)
435 }
436 }
437}
438
439pub(crate) fn mode_help_text(kind: ModeKind, w: &Words) -> String {
441 let key = match kind {
442 ModeKind::Serve => "mode_help.serve",
443 ModeKind::Http => "mode_help.http",
444 ModeKind::Mcp => "mode_help.mcp",
445 _ => "mode_help.help",
446 };
447 let word = match kind {
448 ModeKind::Serve => &w.serve,
449 ModeKind::Http => &w.http,
450 ModeKind::Mcp => &w.mcp,
451 _ => &w.help,
452 };
453 crate::lang::tf(key, &[word])
454}
455
456fn print_mode_help(kind: ModeKind, w: &Words) -> i32 {
458 let _ = writeln!(std::io::stdout(), "{}", mode_help_text(kind, w));
460 0
461}
462
463fn spawn_signal_watcher(ctx: Ctx) {
465 #[cfg(not(any(feature = "http-stack", feature = "cli")))]
467 let _ = &ctx;
468
469 #[cfg(feature = "http-stack")]
470 {
471 std::thread::spawn(move || {
472 let rt = match tokio::runtime::Builder::new_current_thread()
473 .enable_all()
474 .build()
475 {
476 Ok(rt) => rt,
477 Err(_) => return,
478 };
479 rt.block_on(async move {
480 let _ = tokio::signal::ctrl_c().await;
481 ctx.cancel();
482 });
483 });
484 }
485 #[cfg(all(not(feature = "http-stack"), feature = "cli"))]
486 {
487 let _ = ctrlc::set_handler(move || {
488 ctx.cancel();
489 });
490 }
491}
492
493#[cfg(feature = "cli")]
496fn run_cli(ctx: &Ctx, reg: &Registry, args: &[String], cfg: &Config) -> i32 {
497 crate::cli::run_context(
499 ctx,
500 reg,
501 args,
502 crate::cli::Options {
503 format: Some(cfg.format.clone()),
504 format_from_flag: cfg.format_from_flag,
505 format_interactive: Some(cfg.format_interactive.clone()),
506 format_piped: Some(cfg.format_piped.clone()),
507 ..Default::default()
508 },
509 )
510}
511
512#[cfg(not(feature = "cli"))]
513fn run_cli(_ctx: &Ctx, _reg: &Registry, _args: &[String], _cfg: &Config) -> i32 {
514 eprintln!("xyz: {}", crate::lang::tf("stub.not_compiled", &["CLI"]));
515 1
516}
517
518#[cfg(feature = "http")]
519fn run_serve(ctx: &Ctx, reg: &Registry, args: &[String], cfg: Config, mount_mcp: bool) -> i32 {
520 crate::httpapi::serve(ctx, reg, args, cfg, mount_mcp)
521}
522
523#[cfg(not(feature = "http"))]
524fn run_serve(_ctx: &Ctx, _reg: &Registry, _args: &[String], _cfg: Config, _mount_mcp: bool) -> i32 {
525 eprintln!("xyz: {}", crate::lang::tf("stub.not_compiled", &["HTTP"]));
526 1
527}
528
529#[cfg(feature = "mcp")]
530fn run_mcp(ctx: &Ctx, reg: &Registry, args: &[String], cfg: Config) -> i32 {
531 crate::mcp::run_with_config(ctx, reg, args, cfg)
532}
533
534#[cfg(not(feature = "mcp"))]
535fn run_mcp(_ctx: &Ctx, _reg: &Registry, _args: &[String], _cfg: Config) -> i32 {
536 eprintln!("xyz: {}", crate::lang::tf("stub.not_compiled", &["MCP"]));
537 1
538}