1use std::{
2 collections::{BTreeMap, BTreeSet},
3 future::Future,
4 io::Write,
5 process::ExitCode,
6 sync::{Arc, Mutex},
7 time::Duration,
8};
9
10mod builtins;
11mod help;
12mod tree_render;
13
14use clap::{ArgMatches, Command};
15
16use crate::{
17 AuthProvider, CliCoreError, CommandMeta, CommandSpec, GroupSpec, GuideEntry, Middleware,
18 MiddlewareRequest, Result, RuntimeCommandSpec, RuntimeGroupSpec,
19 auth::commands::auth_command_group,
20 command::{CommandContext, command_args_from_matches, command_path_from_matches, leaf_matches},
21 error::exit_code_for_error,
22 flags::{
23 GlobalFlags, derive_bool_flags, derive_value_flags, extract_command_path,
24 extract_output_format, extract_search_query, global_flags_from_matches,
25 has_true_schema_flag, register_global_flags,
26 },
27 guide::guide_content,
28 module::{Module, ModuleContext},
29 output::{
30 HumanViewDef, SchemaRegistry, format_help_section, global_human_view_registry_snapshot,
31 global_schema_registry_snapshot,
32 },
33 search::{SearchDocument, SearchIndex},
34};
35
36use builtins::{guide_args, guide_command, help_args, help_command};
37pub use help::{ModuleHelpEntry, build_root_long};
38
39#[derive(Clone, Debug, Default, Eq, PartialEq)]
41pub struct BuildInfo {
42 pub version: String,
44 pub commit: Option<String>,
46 pub date: Option<String>,
48}
49
50impl BuildInfo {
51 #[must_use]
53 pub fn new(version: impl Into<String>) -> Self {
54 Self {
55 version: version.into(),
56 commit: None,
57 date: None,
58 }
59 }
60
61 #[must_use]
63 pub fn with_commit(mut self, commit: impl Into<String>) -> Self {
64 self.commit = Some(commit.into());
65 self
66 }
67
68 #[must_use]
70 pub fn with_date(mut self, date: impl Into<String>) -> Self {
71 self.date = Some(date.into());
72 self
73 }
74
75 #[must_use]
77 pub fn version_string(&self) -> String {
78 let commit = self.commit.as_deref().unwrap_or_default();
79 let date = self.date.as_deref().unwrap_or_default();
80
81 if commit.is_empty() && date.is_empty() {
82 self.version.clone()
83 } else {
84 format!("{} (commit {commit}, built {date})", self.version)
85 }
86 }
87}
88
89pub type InitDeps = Arc<dyn Fn(&mut Middleware) -> Result<()> + Send + Sync>;
91pub type RegisterFlags = Arc<dyn Fn(Command) -> Command + Send + Sync>;
93pub type ApplyFlags = Arc<dyn Fn(&ArgMatches, &mut Middleware) -> Result<()> + Send + Sync>;
95pub type PreRun =
97 Arc<dyn Fn(&mut Middleware, &str, &crate::middleware::ValueMap) -> Result<()> + Send + Sync>;
98pub type ResolveMeta = Arc<dyn Fn(&str, CommandMeta) -> CommandMeta + Send + Sync>;
100pub type OnShutdown = Arc<dyn Fn() + Send + Sync>;
102pub type ExtraSearchDocs = Arc<dyn Fn() -> Vec<SearchDocument> + Send + Sync>;
104
105#[derive(Clone, Default)]
111pub struct CliConfig {
112 pub name: String,
114 pub short: String,
116 pub long: Option<String>,
118 pub build: BuildInfo,
120 pub app_id: String,
122 pub default_auth_provider: Option<String>,
124 pub modules: Vec<Module>,
126 pub commands: Vec<RuntimeCommandSpec>,
128 pub guides: Vec<GuideEntry>,
130 pub views: Vec<HumanViewDef>,
132 pub auth_providers: Vec<Arc<dyn AuthProvider>>,
134 pub init_deps: Option<InitDeps>,
136 pub register_flags: Option<RegisterFlags>,
138 pub apply_flags: Option<ApplyFlags>,
140 pub pre_run: Option<PreRun>,
142 pub meta_resolver: Option<ResolveMeta>,
144 pub on_shutdown: Option<OnShutdown>,
146 pub extra_search_docs: Option<ExtraSearchDocs>,
148}
149
150impl CliConfig {
151 #[must_use]
153 pub fn new(
154 name: impl Into<String>,
155 short: impl Into<String>,
156 app_id: impl Into<String>,
157 ) -> Self {
158 Self {
159 name: name.into(),
160 short: short.into(),
161 app_id: app_id.into(),
162 ..Self::default()
163 }
164 }
165
166 #[must_use]
168 pub fn with_long(mut self, long: impl Into<String>) -> Self {
169 self.long = Some(long.into());
170 self
171 }
172
173 #[must_use]
175 pub fn with_build(mut self, build: BuildInfo) -> Self {
176 self.build = build;
177 self
178 }
179
180 #[must_use]
182 pub fn with_default_auth_provider(mut self, provider: impl Into<String>) -> Self {
183 self.default_auth_provider = Some(provider.into());
184 self
185 }
186
187 #[must_use]
189 pub fn with_module(mut self, module: Module) -> Self {
190 self.modules.push(module);
191 self
192 }
193
194 #[must_use]
196 pub fn with_modules(mut self, modules: impl IntoIterator<Item = Module>) -> Self {
197 self.modules.extend(modules);
198 self
199 }
200
201 #[must_use]
203 pub fn with_command(mut self, command: RuntimeCommandSpec) -> Self {
204 self.commands.push(command);
205 self
206 }
207
208 #[must_use]
210 pub fn with_guide(mut self, guide: GuideEntry) -> Self {
211 self.guides.push(guide);
212 self
213 }
214
215 #[must_use]
217 pub fn with_guides(mut self, guides: impl IntoIterator<Item = GuideEntry>) -> Self {
218 self.guides.extend(guides);
219 self
220 }
221
222 #[must_use]
224 pub fn with_view(mut self, view: HumanViewDef) -> Self {
225 self.views.push(view);
226 self
227 }
228
229 #[must_use]
231 pub fn with_auth_provider(mut self, provider: Arc<dyn AuthProvider>) -> Self {
232 self.auth_providers.push(provider);
233 self
234 }
235
236 #[must_use]
238 pub fn with_init_deps(mut self, init_deps: InitDeps) -> Self {
239 self.init_deps = Some(init_deps);
240 self
241 }
242
243 #[must_use]
245 pub fn with_register_flags(mut self, register_flags: RegisterFlags) -> Self {
246 self.register_flags = Some(register_flags);
247 self
248 }
249
250 #[must_use]
252 pub fn with_apply_flags(mut self, apply_flags: ApplyFlags) -> Self {
253 self.apply_flags = Some(apply_flags);
254 self
255 }
256
257 #[must_use]
259 pub fn with_pre_run(mut self, pre_run: PreRun) -> Self {
260 self.pre_run = Some(pre_run);
261 self
262 }
263
264 #[must_use]
266 pub fn with_meta_resolver(mut self, meta_resolver: ResolveMeta) -> Self {
267 self.meta_resolver = Some(meta_resolver);
268 self
269 }
270
271 #[must_use]
273 pub fn with_on_shutdown(mut self, on_shutdown: OnShutdown) -> Self {
274 self.on_shutdown = Some(on_shutdown);
275 self
276 }
277
278 #[must_use]
280 pub fn with_extra_search_docs(mut self, extra_search_docs: ExtraSearchDocs) -> Self {
281 self.extra_search_docs = Some(extra_search_docs);
282 self
283 }
284}
285
286impl std::fmt::Debug for CliConfig {
287 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288 formatter
289 .debug_struct("CliConfig")
290 .field("name", &self.name)
291 .field("short", &self.short)
292 .field("long", &self.long)
293 .field("build", &self.build)
294 .field("app_id", &self.app_id)
295 .field("default_auth_provider", &self.default_auth_provider)
296 .field("modules", &self.modules)
297 .field("commands", &self.commands)
298 .field("guides", &self.guides)
299 .field("views", &self.views)
300 .field("auth_providers_len", &self.auth_providers.len())
301 .field("has_init_deps", &self.init_deps.is_some())
302 .field("has_register_flags", &self.register_flags.is_some())
303 .field("has_apply_flags", &self.apply_flags.is_some())
304 .field("has_pre_run", &self.pre_run.is_some())
305 .field("has_meta_resolver", &self.meta_resolver.is_some())
306 .field("has_on_shutdown", &self.on_shutdown.is_some())
307 .field("has_extra_search_docs", &self.extra_search_docs.is_some())
308 .finish()
309 }
310}
311
312#[derive(Clone, Debug, PartialEq)]
314pub struct CliRunOutput {
315 pub exit_code: i32,
317 pub rendered: String,
319}
320
321#[derive(Clone)]
327pub struct Cli {
328 config: CliConfig,
329 middleware: Middleware,
330 root: Command,
331 commands: BTreeMap<String, RuntimeCommandSpec>,
332 module_entries: Vec<ModuleHelpEntry>,
333 guide_entries: Vec<GuideEntry>,
334 init_deps: Option<InitDeps>,
335 apply_flags: Option<ApplyFlags>,
336 pre_run: Option<PreRun>,
337 meta_resolver: Option<ResolveMeta>,
338 on_shutdown: Option<OnShutdown>,
339 extra_search_docs: Option<ExtraSearchDocs>,
340 init_state: Arc<Mutex<Option<std::result::Result<Middleware, InitFailure>>>>,
341}
342
343#[derive(Clone, Debug, Eq, PartialEq)]
344struct InitFailure {
345 message: String,
346 code: String,
347 system: String,
348 request_id: String,
349 exit_code: i32,
350}
351
352impl InitFailure {
353 fn capture(err: &CliCoreError) -> Self {
354 let envelope = crate::output::build_error_envelope(err, "");
355 let (code, system, request_id) = envelope.error.map_or_else(
356 || ("ERROR".to_owned(), String::new(), String::new()),
357 |error| (error.code, error.system, error.request_id),
358 );
359 Self {
360 message: err.to_string(),
361 code,
362 system,
363 request_id,
364 exit_code: exit_code_for_error(err),
365 }
366 }
367
368 fn into_error(self) -> CliCoreError {
369 CliCoreError::with_exit_code(
370 self.exit_code,
371 CliCoreError::SystemMessage {
372 message: self.message,
373 system: self.system,
374 code: self.code,
375 request_id: self.request_id,
376 },
377 )
378 }
379}
380
381impl std::fmt::Debug for Cli {
382 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
383 formatter
384 .debug_struct("Cli")
385 .field("config", &self.config)
386 .field("middleware", &self.middleware)
387 .field("root", &self.root)
388 .field("commands", &self.commands)
389 .field("module_entries", &self.module_entries)
390 .field("guide_entries", &self.guide_entries)
391 .field("has_init_deps", &self.init_deps.is_some())
392 .field("has_apply_flags", &self.apply_flags.is_some())
393 .field("has_pre_run", &self.pre_run.is_some())
394 .field("has_meta_resolver", &self.meta_resolver.is_some())
395 .field("has_on_shutdown", &self.on_shutdown.is_some())
396 .field("has_extra_search_docs", &self.extra_search_docs.is_some())
397 .finish()
398 }
399}
400
401impl Cli {
402 #[must_use]
404 pub fn new(config: CliConfig) -> Self {
405 let auth_providers = config.auth_providers.clone();
406 let guides = config.guides.clone();
407 let views = config.views.clone();
408 let modules = config.modules.clone();
409 let commands = config.commands.clone();
410 let init_deps = config.init_deps.clone();
411 let apply_flags = config.apply_flags.clone();
412 let pre_run = config.pre_run.clone();
413 let meta_resolver = config.meta_resolver.clone();
414 let on_shutdown = config.on_shutdown.clone();
415 let extra_search_docs = config.extra_search_docs.clone();
416 let mut root = Command::new(config.name.clone())
417 .about(config.short.clone())
418 .disable_help_subcommand(true)
419 .version(config.build.version_string());
420 if let Some(long) = &config.long
421 && !long.is_empty()
422 {
423 root = root.long_about(long.clone());
424 }
425 root = register_global_flags(root)
426 .subcommand(help_command())
427 .subcommand(Command::new("tree").about("Display full command tree"));
428 if let Some(register_flags) = &config.register_flags {
429 root = register_flags(root);
430 }
431 let intro = config
432 .long
433 .as_deref()
434 .filter(|long| !long.is_empty())
435 .unwrap_or(config.short.as_str());
436 root = root.long_about(build_root_long(intro, &[], false));
437
438 let mut middleware = Middleware::new();
439 middleware.app_id = config.app_id.clone();
440 middleware.default_auth_provider = config.default_auth_provider.clone().unwrap_or_default();
441 middleware
442 .schema_registry
443 .merge(&global_schema_registry_snapshot());
444 middleware
445 .human_views
446 .merge(&global_human_view_registry_snapshot());
447
448 let mut cli = Self {
449 config,
450 middleware,
451 root,
452 commands: BTreeMap::new(),
453 module_entries: Vec::new(),
454 guide_entries: Vec::new(),
455 init_deps,
456 apply_flags,
457 pre_run,
458 meta_resolver,
459 on_shutdown,
460 extra_search_docs,
461 init_state: Arc::new(Mutex::new(None)),
462 };
463 for provider in auth_providers {
464 cli.register_auth_provider(provider);
465 }
466 if cli.middleware.default_auth_provider.is_empty()
467 && let Some(provider) = cli.middleware.auth.registered_names().first()
468 {
469 cli.middleware.default_auth_provider = provider.clone();
470 }
471 if !cli.middleware.default_auth_provider.is_empty() {
472 cli.ensure_auth_command();
473 }
474 for view in views {
475 cli.middleware.human_views.register(view);
476 }
477 cli.add_guides(guides);
478 for module in modules {
479 cli.add_module(module);
480 }
481 for command in commands {
482 cli.add_command(command);
483 }
484 cli
485 }
486
487 #[must_use]
489 pub fn middleware(&self) -> &Middleware {
490 &self.middleware
491 }
492
493 pub fn middleware_mut(&mut self) -> &mut Middleware {
495 &mut self.middleware
496 }
497
498 pub async fn execute(&self) -> ExitCode {
500 let mut stdout = std::io::stdout().lock();
501 let mut stderr = std::io::stderr().lock();
502 match self
503 .execute_from(std::env::args_os(), &mut stdout, &mut stderr)
504 .await
505 {
506 Ok(code) => code,
507 Err(err) => {
508 drop(writeln!(stderr, "{err}"));
509 ExitCode::from(1)
510 }
511 }
512 }
513
514 pub async fn execute_from<I, S, O, E>(
516 &self,
517 args: I,
518 stdout: &mut O,
519 stderr: &mut E,
520 ) -> std::io::Result<ExitCode>
521 where
522 I: IntoIterator<Item = S>,
523 S: Into<std::ffi::OsString> + Clone,
524 O: Write,
525 E: Write,
526 {
527 self.execute_from_until_signal(args, stdout, stderr, shutdown_signal())
528 .await
529 }
530
531 pub async fn execute_from_until_signal<I, S, O, E, Shutdown>(
533 &self,
534 args: I,
535 stdout: &mut O,
536 stderr: &mut E,
537 shutdown: Shutdown,
538 ) -> std::io::Result<ExitCode>
539 where
540 I: IntoIterator<Item = S>,
541 S: Into<std::ffi::OsString> + Clone,
542 O: Write,
543 E: Write,
544 Shutdown: Future<Output = ()>,
545 {
546 let output = run_until_signal(self.run(args), shutdown).await;
547 if output.exit_code == 130
548 && output.rendered == "command interrupted\n"
549 && let Some(on_shutdown) = &self.on_shutdown
550 {
551 on_shutdown();
552 }
553 if output.exit_code == 0 {
554 stdout.write_all(output.rendered.as_bytes())?;
555 } else {
556 stderr.write_all(output.rendered.as_bytes())?;
557 }
558 Ok(process_exit_code(output.exit_code))
559 }
560
561 pub fn register_auth_provider(&mut self, provider: Arc<dyn AuthProvider>) -> &mut Self {
563 self.middleware.auth.register(provider);
564 self.ensure_auth_command();
565 self.refresh_root_long();
566 self
567 }
568
569 #[must_use]
571 pub fn root_command(&self) -> &Command {
572 &self.root
573 }
574
575 pub fn add_module_group(
577 &mut self,
578 category: impl Into<String>,
579 group: RuntimeGroupSpec,
580 ) -> &mut Self {
581 let category = category.into();
582 if !group.group.hidden {
583 self.module_entries.push(ModuleHelpEntry {
584 category,
585 name: group.group.name.clone(),
586 short: group.group.short.clone(),
587 });
588 }
589
590 let mut prefix = Vec::new();
591 register_runtime_group_schemas(&group, &mut prefix, &mut self.middleware.schema_registry);
592 let mut prefix = Vec::new();
593 group.register_commands(&mut prefix, &mut self.commands);
594 let mut prefix = Vec::new();
595 let clap_group = runtime_group_clap_command_with_schema_help(
596 &group,
597 &mut prefix,
598 &self.middleware.schema_registry,
599 );
600 self.root = self.root.clone().subcommand(clap_group);
601 self.refresh_root_long();
602 self
603 }
604
605 pub fn add_module(&mut self, module: Module) -> &mut Self {
607 for view in module.views.clone() {
608 self.middleware.human_views.register(view);
609 }
610 self.add_guides(module.guides.clone());
611 let mut context = ModuleContext::new(&mut self.middleware);
612 let group = (module.register)(&mut context);
613 let (guides, views) = context.into_parts();
614 for view in views {
615 self.middleware.human_views.register(view);
616 }
617 self.add_guides(guides);
618 self.add_module_group(module.category, group)
619 }
620
621 pub fn add_command(&mut self, command: RuntimeCommandSpec) -> &mut Self {
623 let name = command.spec.name.clone();
624 register_command_schema(&command.spec, &name, &mut self.middleware.schema_registry);
625 self.commands.insert(name, command.clone());
626 self.root = self
627 .root
628 .clone()
629 .subcommand(command_clap_command_with_schema_help(
630 &command.spec,
631 &command.spec.name,
632 &self.middleware.schema_registry,
633 ));
634 self
635 }
636
637 pub fn set_has_guide(&mut self, has_guide: bool) -> &mut Self {
639 if has_guide && self.guide_entries.is_empty() && !has_subcommand(&self.root, "guide") {
640 self.root = self.root.clone().subcommand(guide_command());
641 }
642 self.refresh_root_long();
643 self
644 }
645
646 pub fn add_guides(&mut self, entries: impl IntoIterator<Item = GuideEntry>) -> &mut Self {
648 let mut seen = self
649 .guide_entries
650 .iter()
651 .map(|entry| entry.name.clone())
652 .collect::<BTreeSet<_>>();
653 for entry in entries {
654 if seen.insert(entry.name.clone()) {
655 self.guide_entries.push(entry);
656 }
657 }
658 if !self.guide_entries.is_empty() && !has_subcommand(&self.root, "guide") {
659 self.root = self.root.clone().subcommand(guide_command());
660 }
661 self.refresh_root_long();
662 self
663 }
664
665 pub async fn run<I, S>(&self, args: I) -> CliRunOutput
667 where
668 I: IntoIterator<Item = S>,
669 S: Into<std::ffi::OsString> + Clone,
670 {
671 let raw_args = args
672 .into_iter()
673 .map(Into::into)
674 .collect::<Vec<std::ffi::OsString>>();
675 let text_args = raw_args
676 .iter()
677 .map(|arg| arg.to_string_lossy().into_owned())
678 .collect::<Vec<_>>();
679 let clap_args = normalize_optional_global_flags_before_command(&self.root, &text_args);
680 if has_root_version_flag(&text_args, &self.root, &self.config.name) {
681 return self.finish_run(CliRunOutput {
682 exit_code: 0,
683 rendered: format!(
684 "{} version {}\n",
685 self.config.name,
686 self.config.build.version_string()
687 ),
688 });
689 }
690 if let Some(output) = self.try_run_schema_bypass(&text_args) {
691 return output;
692 }
693 if let Some(output) = self.try_run_search_bypass(&text_args) {
694 return output;
695 }
696 if let Some(message) =
697 unknown_group_command_message(&self.root, &text_args, &self.config.name)
698 {
699 return self.finish_run(CliRunOutput {
700 exit_code: 1,
701 rendered: message,
702 });
703 }
704
705 let matches = match self.root.clone().try_get_matches_from(clap_args) {
706 Ok(matches) => matches,
707 Err(err) => {
708 return self.finish_run(CliRunOutput {
709 exit_code: err.exit_code(),
710 rendered: err.to_string(),
711 });
712 }
713 };
714
715 let flags = global_flags_from_matches(&matches);
716 let command_timeout = match parse_command_timeout(&flags.timeout) {
717 Ok(timeout) => timeout,
718 Err(err) => {
719 return self.finish_run(render_cli_error(
720 &self.middleware,
721 &err,
722 &self.config.app_id,
723 ));
724 }
725 };
726 let mut middleware = self.middleware.clone();
727 apply_global_flags(&mut middleware, &flags, command_timeout);
728 if let Err(err) = self.apply_config_flags(&matches, &mut middleware) {
729 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
730 }
731
732 let command_path = command_path_from_matches(&self.config.name, &matches);
733 if command_path == "help" {
734 if let Err(err) = self.run_pre_run(&mut middleware, &command_path, &help_args(&matches))
735 {
736 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
737 }
738 return self.finish_run(self.render_help_command(&matches));
739 }
740 if command_path == "tree" {
741 if let Err(err) = self.run_pre_run(
742 &mut middleware,
743 &command_path,
744 &crate::middleware::ValueMap::new(),
745 ) {
746 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
747 }
748 return self.finish_run(tree_render::render_tree(
749 &self.root,
750 &self.config.app_id,
751 &middleware,
752 ));
753 }
754 if command_path == "guide" {
755 if let Err(err) =
756 self.run_pre_run(&mut middleware, &command_path, &guide_args(&matches))
757 {
758 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
759 }
760 return self.finish_run(self.render_guide(&matches));
761 }
762 let Some(command) = self.commands.get(&command_path) else {
763 if !command_path.is_empty()
764 && let Some(group) = find_command_by_colon_path(&self.root, &command_path)
765 && group.get_subcommands().next().is_some()
766 {
767 if let Err(err) = self.run_pre_run(
768 &mut middleware,
769 &command_path,
770 &crate::middleware::ValueMap::new(),
771 ) {
772 return self.finish_run(render_cli_error(
773 &middleware,
774 &err,
775 &self.config.app_id,
776 ));
777 }
778 return self.finish_run(CliRunOutput {
779 exit_code: 0,
780 rendered: group.clone().render_long_help().to_string(),
781 });
782 }
783 return self.finish_run(CliRunOutput {
784 exit_code: if command_path.is_empty() { 0 } else { 1 },
785 rendered: if command_path.is_empty() {
786 self.root.clone().render_long_help().to_string()
787 } else {
788 format!("unknown command {command_path:?}")
789 },
790 });
791 };
792
793 let mut middleware = match self.initialized_middleware() {
794 Ok(middleware) => middleware,
795 Err(err) => {
796 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
797 }
798 };
799 apply_global_flags(&mut middleware, &flags, command_timeout);
800 if let Err(err) = self.apply_config_flags(&matches, &mut middleware) {
801 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
802 }
803
804 let leaf = leaf_matches(&matches);
805 let args = command_args_from_matches(leaf, &command.spec, false);
806 let user_args = command_args_from_matches(leaf, &command.spec, true);
807 if let Err(err) = self.run_pre_run(&mut middleware, &command_path, &args) {
808 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
809 }
810 let meta = self.resolve_meta(&command_path, command.spec.metadata());
811 let default_fields = command.spec.default_fields.clone().unwrap_or_default();
812 let system = command.spec.system.clone().unwrap_or_default();
813 let handler = command.handler.clone();
814 let args_for_handler = args.clone();
815 let user_args_for_handler = user_args.clone();
816 let handler_path = command_path.clone();
817 let middleware_for_handler = middleware.clone();
818 let result = run_with_timeout(
819 command_timeout,
820 &flags.timeout,
821 middleware.run(
822 MiddlewareRequest {
823 meta,
824 command_path: &command_path,
825 system: &system,
826 user_args,
827 args,
828 default_fields: &default_fields,
829 no_auth: command.spec.no_auth,
830 },
831 async move |credential| {
832 handler(CommandContext {
833 credential,
834 args: args_for_handler,
835 user_args: user_args_for_handler,
836 command_path: handler_path,
837 middleware: middleware_for_handler,
838 })
839 .await
840 },
841 ),
842 )
843 .await;
844
845 match result {
846 Ok(output) => self.finish_run(CliRunOutput {
847 exit_code: output.exit_code,
848 rendered: output.rendered,
849 }),
850 Err(err) => self.finish_run(CliRunOutput {
851 exit_code: exit_code_for_error(&err),
852 rendered: render_cli_error(&middleware, &err, &self.config.app_id).rendered,
853 }),
854 }
855 }
856
857 fn try_run_search_bypass(&self, args: &[String]) -> Option<CliRunOutput> {
858 let query = extract_search_query(args);
859 if query.is_empty() {
860 return None;
861 }
862 let scope = self.search_scope(args);
863 let output_format = extract_output_format(args);
864 Some(self.render_search(&query, &scope, &output_format))
865 }
866
867 fn try_run_schema_bypass(&self, args: &[String]) -> Option<CliRunOutput> {
868 if !has_true_schema_flag(args) {
869 return None;
870 }
871 let bool_flags = derive_bool_flags(&self.root);
872 let value_flags = derive_value_flags(&self.root);
873 let command_path =
874 self.canonical_command_path(&extract_command_path(args, &bool_flags, &value_flags));
875 let schema = self.middleware.schema_registry.get_by_path(&command_path)?;
876 let output_format = extract_output_format(args);
877 Some(self.render_schema(schema, &output_format))
878 }
879
880 fn render_schema(
881 &self,
882 schema: crate::output::SchemaInfo,
883 output_format: &str,
884 ) -> CliRunOutput {
885 let format: crate::output::OutputFormat = match output_format.parse() {
886 Ok(format) => format,
887 Err(err) => {
888 return CliRunOutput {
889 exit_code: exit_code_for_error(&err),
890 rendered: err.to_string(),
891 };
892 }
893 };
894 let envelope =
895 crate::Envelope::success(schema, self.config.app_id.clone()).prepare_for_render("");
896 match crate::output::render(format, &envelope) {
897 Ok(rendered) => CliRunOutput {
898 exit_code: 0,
899 rendered,
900 },
901 Err(err) => CliRunOutput {
902 exit_code: exit_code_for_error(&err),
903 rendered: err.to_string(),
904 },
905 }
906 }
907
908 fn render_search(&self, query: &str, scope: &str, output_format: &str) -> CliRunOutput {
909 let format: crate::output::OutputFormat = match output_format.parse() {
910 Ok(format) => format,
911 Err(err) => {
912 return CliRunOutput {
913 exit_code: exit_code_for_error(&err),
914 rendered: err.to_string(),
915 };
916 }
917 };
918 let docs = self.search_documents(scope);
919 let results = SearchIndex::new(docs).search(query, 10);
920 let envelope =
921 crate::Envelope::success(results, self.config.app_id.clone()).prepare_for_render("");
922 match crate::output::render(format, &envelope) {
923 Ok(rendered) => CliRunOutput {
924 exit_code: 0,
925 rendered,
926 },
927 Err(err) => CliRunOutput {
928 exit_code: exit_code_for_error(&err),
929 rendered: err.to_string(),
930 },
931 }
932 }
933
934 fn search_documents(&self, scope: &str) -> Vec<SearchDocument> {
935 let (scoped, mut prefix) = find_command_and_canonical_path_by_colon_path(&self.root, scope)
936 .unwrap_or((&self.root, Vec::new()));
937 let mut docs = Vec::new();
938 let mut aliases = Vec::new();
939 append_command_alias_terms(scoped, &mut aliases);
940 collect_command_search_documents(scoped, &mut prefix, &mut aliases, &mut docs);
941 if scope.is_empty() {
942 for entry in &self.guide_entries {
943 docs.push(SearchDocument {
944 id: format!("guide:{}", entry.name),
945 kind: "guide".to_owned(),
946 title: format!("guide {}", entry.name),
947 summary: entry.summary.clone(),
948 content: format!("{} {}", entry.summary, entry.content),
949 });
950 }
951 if let Some(extra_search_docs) = &self.extra_search_docs {
952 docs.extend(extra_search_docs());
953 }
954 }
955 docs
956 }
957
958 fn search_scope(&self, args: &[String]) -> String {
959 let parts = extract_search_scope_parts(args);
960 canonical_path_from_parts(&self.root, &parts).unwrap_or_default()
961 }
962
963 fn canonical_command_path(&self, command_path: &str) -> String {
964 find_command_and_canonical_path_by_colon_path(&self.root, command_path).map_or_else(
965 || command_path.to_owned(),
966 |(_, canonical)| canonical.join(":"),
967 )
968 }
969
970 fn render_guide(&self, matches: &ArgMatches) -> CliRunOutput {
971 let leaf = leaf_matches(matches);
972 let topic = leaf.get_one::<String>("topic").map(String::as_str);
973 match guide_content(&self.guide_entries, topic) {
974 Ok(rendered) => CliRunOutput {
975 exit_code: 0,
976 rendered,
977 },
978 Err(err) => CliRunOutput {
979 exit_code: 1,
980 rendered: err,
981 },
982 }
983 }
984
985 fn render_help_command(&self, matches: &ArgMatches) -> CliRunOutput {
986 let leaf = leaf_matches(matches);
987 let parts = leaf
988 .get_many::<String>("command")
989 .map(|values| values.map(String::as_str).collect::<Vec<_>>())
990 .unwrap_or_default();
991 if parts.is_empty() {
992 return CliRunOutput {
993 exit_code: 0,
994 rendered: self.root.clone().render_long_help().to_string(),
995 };
996 }
997 let Some(command) = find_help_target(&self.root, &parts) else {
998 return CliRunOutput {
999 exit_code: 1,
1000 rendered: format!(
1001 "unknown command {:?} — run '{} help' for available commands",
1002 parts.join(" "),
1003 self.config.name
1004 ),
1005 };
1006 };
1007 CliRunOutput {
1008 exit_code: 0,
1009 rendered: command.clone().render_long_help().to_string(),
1010 }
1011 }
1012
1013 fn refresh_root_long(&mut self) {
1014 let intro = self
1015 .config
1016 .long
1017 .as_deref()
1018 .filter(|long| !long.is_empty())
1019 .unwrap_or(self.config.short.as_str());
1020 self.root = self.root.clone().long_about(build_root_long(
1021 intro,
1022 &self.module_entries,
1023 !self.guide_entries.is_empty() || has_subcommand(&self.root, "guide"),
1024 ));
1025 }
1026
1027 fn ensure_auth_command(&mut self) {
1028 let default_provider = self.default_auth_provider();
1029 let registered_names = self.middleware.auth.registered_names();
1030 if default_provider.is_empty() && registered_names.is_empty() {
1031 return;
1032 }
1033 let replacing_builtin = self.commands.contains_key("auth:login");
1034 if has_subcommand(&self.root, "auth") && !replacing_builtin {
1035 return;
1036 }
1037 let group = auth_command_group(&default_provider, ®istered_names);
1038 let mut prefix = Vec::new();
1039 group.register_commands(&mut prefix, &mut self.commands);
1040 let mut prefix = Vec::new();
1041 let clap_group = runtime_group_clap_command_with_schema_help(
1042 &group,
1043 &mut prefix,
1044 &self.middleware.schema_registry,
1045 );
1046 self.root = if replacing_builtin {
1047 self.root.clone().mut_subcommand("auth", |_| clap_group)
1048 } else {
1049 self.root.clone().subcommand(clap_group)
1050 };
1051 }
1052
1053 fn default_auth_provider(&self) -> String {
1054 if !self.middleware.default_auth_provider.is_empty() {
1055 return self.middleware.default_auth_provider.clone();
1056 }
1057 self.middleware
1058 .auth
1059 .registered_names()
1060 .into_iter()
1061 .next()
1062 .unwrap_or_default()
1063 }
1064
1065 fn initialized_middleware(&self) -> Result<Middleware> {
1066 let Some(init_deps) = &self.init_deps else {
1067 return Ok(self.middleware.clone());
1068 };
1069 let mut guard = self
1070 .init_state
1071 .lock()
1072 .map_err(|_| CliCoreError::message("init deps lock poisoned"))?;
1073 if let Some(result) = guard.as_ref() {
1074 return result.clone().map_err(InitFailure::into_error);
1075 }
1076 let mut middleware = self.middleware.clone();
1077 let result = init_deps(&mut middleware)
1078 .map(|()| middleware)
1079 .map_err(|err| InitFailure::capture(&err));
1080 *guard = Some(result.clone());
1081 result.map_err(InitFailure::into_error)
1082 }
1083
1084 fn apply_config_flags(&self, matches: &ArgMatches, middleware: &mut Middleware) -> Result<()> {
1085 if let Some(apply_flags) = &self.apply_flags {
1086 apply_flags(matches, middleware)?;
1087 }
1088 Ok(())
1089 }
1090
1091 fn run_pre_run(
1092 &self,
1093 middleware: &mut Middleware,
1094 command_path: &str,
1095 args: &crate::middleware::ValueMap,
1096 ) -> Result<()> {
1097 if let Some(pre_run) = &self.pre_run {
1098 pre_run(middleware, command_path, args)?;
1099 }
1100 Ok(())
1101 }
1102
1103 fn resolve_meta(&self, command_path: &str, meta: CommandMeta) -> CommandMeta {
1104 if let Some(resolver) = &self.meta_resolver {
1105 resolver(command_path, meta)
1106 } else {
1107 meta
1108 }
1109 }
1110
1111 fn finish_run(&self, output: CliRunOutput) -> CliRunOutput {
1112 if let Some(on_shutdown) = &self.on_shutdown {
1113 on_shutdown();
1114 }
1115 output
1116 }
1117}
1118
1119fn apply_global_flags(middleware: &mut Middleware, flags: &GlobalFlags, timeout: Option<Duration>) {
1120 middleware.output_format = flags.output_format.clone();
1121 middleware.verbose = flags.verbose.clone();
1122 middleware.dry_run = flags.dry_run;
1123 middleware.fields = flags.fields.clone();
1124 middleware.filter = flags.filter.clone();
1125 middleware.expr = flags.expr.clone();
1126 middleware.limit = flags.limit;
1127 middleware.offset = flags.offset;
1128 middleware.reason = flags.reason.clone();
1129 middleware.schema = flags.schema;
1130 middleware.timeout = timeout;
1131 middleware.debug = flags.debug.clone();
1132 middleware.search = flags.search.clone();
1133}
1134
1135async fn run_with_timeout<F, T>(
1136 timeout: Option<Duration>,
1137 timeout_label: &str,
1138 future: F,
1139) -> Result<T>
1140where
1141 F: Future<Output = Result<T>>,
1142{
1143 let Some(timeout) = timeout else {
1144 return future.await;
1145 };
1146 match tokio::time::timeout(timeout, future).await {
1147 Ok(result) => result,
1148 Err(_) => Err(CliCoreError::message(format!(
1149 "command timed out after {timeout_label}"
1150 ))),
1151 }
1152}
1153
1154async fn run_until_signal<Run, Shutdown>(run: Run, shutdown: Shutdown) -> CliRunOutput
1155where
1156 Run: Future<Output = CliRunOutput>,
1157 Shutdown: Future<Output = ()>,
1158{
1159 tokio::pin!(run);
1160 tokio::pin!(shutdown);
1161 tokio::select! {
1162 output = &mut run => output,
1163 () = &mut shutdown => CliRunOutput {
1164 exit_code: 130,
1165 rendered: "command interrupted\n".to_owned(),
1166 },
1167 }
1168}
1169
1170#[cfg(unix)]
1171async fn shutdown_signal() {
1172 let ctrl_c = tokio::signal::ctrl_c();
1173 match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
1174 Ok(mut sigterm) => {
1175 tokio::select! {
1176 _ = ctrl_c => {},
1177 _ = sigterm.recv() => {},
1178 }
1179 }
1180 Err(_) => {
1181 drop(ctrl_c.await);
1182 }
1183 }
1184}
1185
1186#[cfg(not(unix))]
1187async fn shutdown_signal() {
1188 drop(tokio::signal::ctrl_c().await);
1189}
1190
1191fn parse_command_timeout(raw: &str) -> Result<Option<Duration>> {
1192 let raw = raw.trim();
1193 if raw.is_empty() {
1194 return Ok(Some(Duration::from_secs(60)));
1195 }
1196 let Some(seconds) = parse_duration_seconds(raw) else {
1197 return Err(CliCoreError::message(format!(
1198 "invalid timeout {raw:?}: expected duration like 60s, 5m, or 0s"
1199 )));
1200 };
1201 if seconds <= 0.0 {
1202 Ok(None)
1203 } else {
1204 Ok(Some(Duration::from_secs_f64(seconds)))
1205 }
1206}
1207
1208fn parse_duration_seconds(raw: &str) -> Option<f64> {
1209 for (suffix, seconds) in [
1210 ("ns", 0.000_000_001_f64),
1211 ("us", 0.000_001_f64),
1212 ("µs", 0.000_001_f64),
1213 ("ms", 0.001_f64),
1214 ("s", 1.0_f64),
1215 ("m", 60.0_f64),
1216 ("h", 3600.0_f64),
1217 ] {
1218 if let Some(number) = raw.strip_suffix(suffix) {
1219 let value = number.parse::<f64>().ok()?;
1220 if !value.is_finite() {
1221 return None;
1222 }
1223 return Some(value * seconds);
1224 }
1225 }
1226 None
1227}
1228
1229fn render_cli_error(
1230 middleware: &Middleware,
1231 err: &(dyn std::error::Error + 'static),
1232 system: &str,
1233) -> CliRunOutput {
1234 let format = middleware
1235 .output_format
1236 .parse::<crate::output::OutputFormat>()
1237 .unwrap_or(crate::output::OutputFormat::Json);
1238 let envelope =
1239 crate::output::build_error_envelope(err, system).prepare_for_render(&middleware.verbose);
1240 match crate::output::render(format, &envelope) {
1241 Ok(rendered) => CliRunOutput {
1242 exit_code: exit_code_for_error(err),
1243 rendered,
1244 },
1245 Err(render_err) => CliRunOutput {
1246 exit_code: exit_code_for_error(err),
1247 rendered: render_err.to_string(),
1248 },
1249 }
1250}
1251
1252fn find_command_by_colon_path<'command>(
1253 root: &'command Command,
1254 path: &str,
1255) -> Option<&'command Command> {
1256 find_command_and_canonical_path_by_colon_path(root, path).map(|(command, _)| command)
1257}
1258
1259fn find_help_target<'command>(
1260 root: &'command Command,
1261 parts: &[&str],
1262) -> Option<&'command Command> {
1263 let mut current = root;
1264 let mut matched_any = false;
1265 for part in parts {
1266 let Some(next) = current.find_subcommand(part) else {
1267 break;
1268 };
1269 current = next;
1270 matched_any = true;
1271 }
1272 matched_any.then_some(current)
1273}
1274
1275fn find_command_and_canonical_path_by_colon_path<'command>(
1276 root: &'command Command,
1277 path: &str,
1278) -> Option<(&'command Command, Vec<String>)> {
1279 if path.is_empty() {
1280 return Some((root, Vec::new()));
1281 }
1282 let mut current = root;
1283 let mut canonical = Vec::new();
1284 for part in path.split(':') {
1285 current = current.find_subcommand(part)?;
1286 canonical.push(current.get_name().to_owned());
1287 }
1288 Some((current, canonical))
1289}
1290
1291fn canonical_path_from_parts(root: &Command, parts: &[String]) -> Option<String> {
1292 if parts.is_empty() {
1293 return Some(String::new());
1294 }
1295 let mut current = root;
1296 let mut canonical = Vec::new();
1297 for part in parts {
1298 current = current.find_subcommand(part)?;
1299 canonical.push(current.get_name().to_owned());
1300 }
1301 Some(canonical.join(":"))
1302}
1303
1304fn extract_search_scope_parts(args: &[String]) -> Vec<String> {
1305 let mut parts = Vec::new();
1306 let mut index = 1;
1307 while index < args.len() {
1308 let arg = &args[index];
1309 if arg == "--search" || arg.starts_with("--search=") {
1310 break;
1311 }
1312 if arg.starts_with('-') {
1313 if !arg.contains('=') && index + 1 < args.len() && !args[index + 1].starts_with('-') {
1314 index += 2;
1315 } else {
1316 index += 1;
1317 }
1318 continue;
1319 }
1320 parts.push(arg.clone());
1321 index += 1;
1322 }
1323 parts
1324}
1325
1326fn collect_command_search_documents(
1327 command: &Command,
1328 prefix: &mut Vec<String>,
1329 aliases: &mut Vec<String>,
1330 docs: &mut Vec<SearchDocument>,
1331) {
1332 if command.is_hide_set() || command.get_name() == "completion" {
1333 return;
1334 }
1335 if command.get_subcommands().next().is_some() {
1336 for child in command.get_subcommands() {
1337 prefix.push(child.get_name().to_owned());
1338 let alias_len = aliases.len();
1339 append_command_alias_terms(child, aliases);
1340 collect_command_search_documents(child, prefix, aliases, docs);
1341 aliases.truncate(alias_len);
1342 prefix.pop();
1343 }
1344 return;
1345 }
1346 if prefix.is_empty() {
1347 prefix.push(command.get_name().to_owned());
1348 append_command_alias_terms(command, aliases);
1349 }
1350 let path = prefix.join(" ");
1351 let alias_text = aliases.join(" ");
1352 docs.push(SearchDocument {
1353 id: format!("cmd:{path}"),
1354 kind: "command".to_owned(),
1355 title: path,
1356 summary: command
1357 .get_about()
1358 .map(ToString::to_string)
1359 .unwrap_or_default(),
1360 content: format!(
1361 "{} {} {} {}",
1362 command
1363 .get_about()
1364 .map(ToString::to_string)
1365 .unwrap_or_default(),
1366 command
1367 .get_long_about()
1368 .map(ToString::to_string)
1369 .unwrap_or_default(),
1370 command_flag_text(command),
1371 alias_text
1372 ),
1373 });
1374 if prefix.len() == 1 && prefix[0] == command.get_name() {
1375 prefix.pop();
1376 }
1377}
1378
1379fn append_command_alias_terms(command: &Command, aliases: &mut Vec<String>) {
1380 aliases.extend(command.get_all_aliases().map(str::to_owned));
1381 aliases.extend(
1382 command
1383 .get_all_short_flag_aliases()
1384 .map(|alias| alias.to_string()),
1385 );
1386 aliases.extend(command.get_all_long_flag_aliases().map(str::to_owned));
1387}
1388
1389fn command_flag_text(command: &Command) -> String {
1390 command
1391 .get_arguments()
1392 .filter_map(|arg| {
1393 let mut names = Vec::new();
1394 if let Some(short) = arg.get_short() {
1395 names.push(format!("-{short}"));
1396 }
1397 if let Some(long) = arg.get_long() {
1398 names.push(format!("--{long}"));
1399 }
1400 if let Some(short_aliases) = arg.get_all_short_aliases() {
1401 names.extend(
1402 short_aliases
1403 .into_iter()
1404 .map(|short_alias| format!("-{short_alias}")),
1405 );
1406 }
1407 if let Some(aliases) = arg.get_all_aliases() {
1408 names.extend(aliases.into_iter().map(|alias| format!("--{alias}")));
1409 }
1410 (!names.is_empty()).then(|| names.join(" "))
1411 })
1412 .collect::<Vec<_>>()
1413 .join(" ")
1414}
1415
1416fn has_subcommand(command: &Command, name: &str) -> bool {
1417 command
1418 .get_subcommands()
1419 .any(|child| child.get_name() == name)
1420}
1421
1422fn has_root_version_flag(args: &[String], root: &Command, root_name: &str) -> bool {
1423 let bool_flags = derive_bool_flags(root);
1424 let value_flags = derive_value_flags(root);
1425 let mut iter = args.iter().peekable();
1426 if iter.peek().is_some_and(|arg| arg.as_str() == root_name) {
1427 iter.next();
1428 }
1429
1430 while let Some(arg) = iter.next() {
1431 match arg.as_str() {
1432 "--version" | "-v" => return true,
1433 "--" => return false,
1434 value if value.contains('=') || bool_flags.contains(value) => continue,
1435 value
1436 if value_flags.contains(value)
1437 || unknown_flag_consumes_value(value, iter.peek()) =>
1438 {
1439 iter.next();
1440 }
1441 value if value.starts_with('-') => {}
1442 _ => return false,
1443 }
1444 }
1445 false
1446}
1447
1448fn normalize_optional_global_flags_before_command(root: &Command, args: &[String]) -> Vec<String> {
1449 let optional_string_defaults = BTreeMap::from([("--verbose", "all"), ("--debug", "*")]);
1450 let optional_bool_defaults = BTreeMap::from([("--dry-run", "true"), ("--schema", "true")]);
1451 let mut normalized = Vec::with_capacity(args.len());
1452 let mut index = 0;
1453 let mut current = root;
1454 while index < args.len() {
1455 let arg = &args[index];
1456 if index == 0 && arg == root.get_name() {
1457 normalized.push(arg.clone());
1458 index += 1;
1459 continue;
1460 }
1461
1462 if let Some(default) = optional_bool_defaults.get(arg.as_str()) {
1463 normalized.push(format!("{arg}={default}"));
1464 index += 1;
1465 continue;
1466 }
1467
1468 if let Some(default) = optional_string_defaults.get(arg.as_str()) {
1469 match args.get(index + 1) {
1470 None => {
1471 normalized.push(format!("{arg}={default}"));
1472 index += 1;
1473 continue;
1474 }
1475 Some(next)
1476 if current.get_name() == root.get_name()
1477 || next.starts_with('-')
1478 || direct_subcommand(current, next).is_some() =>
1479 {
1480 normalized.push(format!("{arg}={default}"));
1481 index += 1;
1482 continue;
1483 }
1484 Some(next) => {
1485 normalized.push(arg.clone());
1486 normalized.push(next.clone());
1487 index += 2;
1488 continue;
1489 }
1490 }
1491 }
1492
1493 normalized.push(arg.clone());
1494 if !arg.starts_with('-')
1495 && let Some(next_command) = direct_subcommand(current, arg)
1496 {
1497 current = next_command;
1498 }
1499 index += 1;
1500 }
1501 normalized
1502}
1503
1504fn direct_subcommand<'command>(
1505 command: &'command Command,
1506 token: &str,
1507) -> Option<&'command Command> {
1508 command.get_subcommands().find(|child| {
1509 child.get_name() == token || child.get_all_aliases().any(|alias| alias == token)
1510 })
1511}
1512
1513fn unknown_group_command_message(
1514 root: &Command,
1515 args: &[String],
1516 root_name: &str,
1517) -> Option<String> {
1518 let bool_flags = derive_bool_flags(root);
1519 let value_flags = derive_value_flags(root);
1520 let positionals = positional_command_tokens(args, root_name, &bool_flags, &value_flags);
1521 if positionals.is_empty() {
1522 return None;
1523 }
1524
1525 let mut current = root;
1526 let mut path = vec![root.get_name().to_owned()];
1527 for token in positionals {
1528 if let Some(next) = current.find_subcommand(&token) {
1529 current = next;
1530 path.push(next.get_name().to_owned());
1531 continue;
1532 }
1533 if current.get_subcommands().next().is_some() {
1534 return Some(format!(
1535 "unknown command {token:?} for {:?}",
1536 path.join(" ")
1537 ));
1538 }
1539 return None;
1540 }
1541 None
1542}
1543
1544fn positional_command_tokens(
1545 args: &[String],
1546 root_name: &str,
1547 bool_flags: &BTreeSet<String>,
1548 value_flags: &BTreeSet<String>,
1549) -> Vec<String> {
1550 let mut tokens = Vec::new();
1551 let mut iter = args.iter().peekable();
1552 if iter.peek().is_some_and(|arg| arg.as_str() == root_name) {
1553 iter.next();
1554 }
1555
1556 while let Some(arg) = iter.next() {
1557 if arg == "--" {
1558 tokens.extend(iter.cloned());
1559 break;
1560 }
1561 if arg.contains('=') {
1562 continue;
1563 }
1564 if bool_flags.contains(arg) {
1565 continue;
1566 }
1567 if value_flags.contains(arg) || unknown_flag_consumes_value(arg, iter.peek()) {
1568 iter.next();
1569 continue;
1570 }
1571 if arg.starts_with('-') {
1572 continue;
1573 }
1574 tokens.push(arg.clone());
1575 }
1576 tokens
1577}
1578
1579fn unknown_flag_consumes_value(arg: &str, next: Option<&&String>) -> bool {
1580 arg.starts_with('-') && next.is_some_and(|value| !value.starts_with('-'))
1581}
1582
1583fn register_runtime_group_schemas(
1584 group: &RuntimeGroupSpec,
1585 prefix: &mut Vec<String>,
1586 schemas: &mut SchemaRegistry,
1587) {
1588 prefix.push(group.group.name.clone());
1589 for child_group in &group.groups {
1590 register_runtime_group_schemas(child_group, prefix, schemas);
1591 }
1592 for child in &group.commands {
1593 prefix.push(child.spec.name.clone());
1594 register_command_schema(&child.spec, &prefix.join(":"), schemas);
1595 prefix.pop();
1596 }
1597 prefix.pop();
1598}
1599
1600fn register_command_schema(spec: &CommandSpec, command_path: &str, schemas: &mut SchemaRegistry) {
1601 if let Some(schema) = &spec.output_schema {
1602 schemas.register_info(command_path.to_owned(), schema.clone());
1603 }
1604}
1605
1606fn runtime_group_clap_command_with_schema_help(
1607 group: &RuntimeGroupSpec,
1608 prefix: &mut Vec<String>,
1609 schemas: &SchemaRegistry,
1610) -> Command {
1611 let mut command = group_clap_command_without_children(&group.group);
1612 prefix.push(group.group.name.clone());
1613 for child_group in &group.groups {
1614 command = command.subcommand(runtime_group_clap_command_with_schema_help(
1615 child_group,
1616 prefix,
1617 schemas,
1618 ));
1619 }
1620 for child in &group.commands {
1621 prefix.push(child.spec.name.clone());
1622 let command_path = prefix.join(":");
1623 command = command.subcommand(command_clap_command_with_schema_help(
1624 &child.spec,
1625 &command_path,
1626 schemas,
1627 ));
1628 prefix.pop();
1629 }
1630 prefix.pop();
1631 command
1632}
1633
1634fn group_clap_command_without_children(group: &GroupSpec) -> Command {
1635 let mut command = Command::new(group.name.clone()).about(group.short.clone());
1636 if let Some(long) = &group.long
1637 && !long.is_empty()
1638 {
1639 command = command.long_about(long.clone());
1640 }
1641 for alias in &group.aliases {
1642 command = command.alias(alias.clone());
1643 }
1644 if group.hidden {
1645 command = command.hide(true);
1646 }
1647 command
1648}
1649
1650fn command_clap_command_with_schema_help(
1651 spec: &CommandSpec,
1652 command_path: &str,
1653 schemas: &SchemaRegistry,
1654) -> Command {
1655 let mut command = spec.clap_command();
1656 let Some(schema) = schemas.get_by_path(command_path) else {
1657 return command;
1658 };
1659 let schema_help = format_help_section(&schema.fields);
1660 if schema_help.is_empty() {
1661 return command;
1662 }
1663 let base = spec
1664 .long
1665 .as_ref()
1666 .filter(|long| !long.is_empty())
1667 .cloned()
1668 .unwrap_or_else(|| spec.short.clone());
1669 let long = if base.is_empty() {
1670 schema_help
1671 } else {
1672 format!("{base}\n\n{schema_help}")
1673 };
1674 command = command.long_about(long);
1675 command
1676}
1677
1678fn process_exit_code(code: i32) -> ExitCode {
1679 if code == 0 {
1680 return ExitCode::SUCCESS;
1681 }
1682 match u8::try_from(code) {
1683 Ok(code) if code != 0 => ExitCode::from(code),
1684 Ok(_) | Err(_) => ExitCode::from(1),
1685 }
1686}