1use std::{
2 collections::{BTreeMap, BTreeSet},
3 future::Future,
4 io::Write,
5 path::{Path, PathBuf},
6 process::ExitCode,
7 sync::{Arc, Mutex},
8 time::Duration,
9};
10
11mod builtins;
12mod help;
13mod tree_render;
14
15use clap::{ArgMatches, Command};
16
17use crate::{
18 ActivityEmitter, Auditor, AuthProvider, Authorizer, CliCoreError, CommandMeta, CommandSpec,
19 GroupSpec, GuideEntry, Middleware, MiddlewareRequest, Result, RuntimeCommandSpec,
20 RuntimeGroupSpec,
21 auth::commands::auth_command_group,
22 command::{
23 CommandContext, StreamSender, command_args_from_matches, command_path_from_matches,
24 leaf_matches,
25 },
26 error::exit_code_for_error,
27 flags::{
28 GlobalFlags, default_output_format, derive_bool_flags, derive_value_flags,
29 extract_command_path, extract_output_format, extract_search_query,
30 global_flags_from_matches, has_true_schema_flag, register_global_flags,
31 },
32 guide::guide_content,
33 module::{Module, ModuleContext},
34 output::{
35 HumanViewDef, NextAction, SchemaRegistry, format_help_section,
36 global_human_view_registry_snapshot, global_schema_registry_snapshot,
37 },
38 search::{SearchDocument, SearchIndex},
39};
40
41use builtins::{guide_args, guide_command, help_args, help_command};
42use help::{GROUP_HELP_TEMPLATE, ROOT_HELP_TEMPLATE};
43pub use help::{ModuleHelpEntry, build_root_long, render_next_actions_human};
44
45#[derive(Clone, Debug, Default, Eq, PartialEq)]
47pub struct BuildInfo {
48 pub version: String,
50 pub commit: Option<String>,
52 pub date: Option<String>,
54}
55
56impl BuildInfo {
57 #[must_use]
59 pub fn new(version: impl Into<String>) -> Self {
60 Self {
61 version: version.into(),
62 commit: None,
63 date: None,
64 }
65 }
66
67 #[must_use]
69 pub fn with_commit(mut self, commit: impl Into<String>) -> Self {
70 self.commit = Some(commit.into());
71 self
72 }
73
74 #[must_use]
76 pub fn with_date(mut self, date: impl Into<String>) -> Self {
77 self.date = Some(date.into());
78 self
79 }
80
81 #[must_use]
83 pub fn version_string(&self) -> String {
84 let commit = self.commit.as_deref().unwrap_or_default();
85 let date = self.date.as_deref().unwrap_or_default();
86
87 if commit.is_empty() && date.is_empty() {
88 self.version.clone()
89 } else {
90 format!("{} (commit {commit}, built {date})", self.version)
91 }
92 }
93}
94
95pub type InitDeps = Arc<dyn Fn(&mut Middleware) -> Result<()> + Send + Sync>;
97pub type RegisterFlags = Arc<dyn Fn(Command) -> Command + Send + Sync>;
99pub type ApplyFlags = Arc<dyn Fn(&ArgMatches, &mut Middleware) -> Result<()> + Send + Sync>;
101pub type PreRun =
103 Arc<dyn Fn(&mut Middleware, &str, &crate::middleware::ValueMap) -> Result<()> + Send + Sync>;
104pub type ResolveMeta = Arc<dyn Fn(&str, CommandMeta) -> CommandMeta + Send + Sync>;
106pub type OnShutdown = Arc<dyn Fn() + Send + Sync>;
108pub type ExtraSearchDocs = Arc<dyn Fn() -> Vec<SearchDocument> + Send + Sync>;
110pub type RootNextActions = Arc<dyn Fn() -> Vec<NextAction> + Send + Sync>;
114
115const DEFAULT_ADMIN_CATEGORY: &str = "Admin";
119
120const MAX_ARGV0_DEPTH: usize = 16;
125
126#[derive(Clone)]
138#[non_exhaustive]
139pub enum Argv0Route {
140 Alias(Vec<String>),
146 Personality(Arc<dyn Fn() -> CliConfig + Send + Sync>),
151}
152
153impl std::fmt::Debug for Argv0Route {
154 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 match self {
156 Self::Alias(tokens) => formatter.debug_tuple("Alias").field(tokens).finish(),
157 Self::Personality(_) => formatter.write_str("Personality(..)"),
158 }
159 }
160}
161
162#[derive(Clone, Copy, Debug, Eq, PartialEq)]
170#[non_exhaustive]
171pub enum Argv0LinkMethod {
172 SoftLink,
175 HardLink,
178 Script,
182}
183
184#[derive(Clone, Default)]
190pub struct CliConfig {
191 pub name: String,
193 pub short: String,
195 pub long: Option<String>,
197 pub build: BuildInfo,
199 pub app_id: String,
201 pub default_auth_provider: Option<String>,
203 pub modules: Vec<Module>,
205 pub commands: Vec<RuntimeCommandSpec>,
207 pub guides: Vec<GuideEntry>,
209 pub views: Vec<HumanViewDef>,
211 pub auth_providers: Vec<Arc<dyn AuthProvider>>,
213 pub authz: Option<Arc<dyn Authorizer>>,
215 pub auditor: Option<Arc<dyn Auditor>>,
217 pub activity: Option<Arc<dyn ActivityEmitter>>,
219 pub init_deps: Option<InitDeps>,
221 pub register_flags: Option<RegisterFlags>,
223 pub apply_flags: Option<ApplyFlags>,
225 pub pre_run: Option<PreRun>,
227 pub meta_resolver: Option<ResolveMeta>,
229 pub on_shutdown: Option<OnShutdown>,
231 pub extra_search_docs: Option<ExtraSearchDocs>,
233 pub root_next_actions: Option<RootNextActions>,
235 pub admin_category: Option<String>,
240 pub config_commands: bool,
245 pub argv0_routes: BTreeMap<String, Argv0Route>,
253}
254
255impl CliConfig {
256 #[must_use]
258 pub fn new(
259 name: impl Into<String>,
260 short: impl Into<String>,
261 app_id: impl Into<String>,
262 ) -> Self {
263 Self {
264 name: name.into(),
265 short: short.into(),
266 app_id: app_id.into(),
267 ..Self::default()
268 }
269 }
270
271 #[must_use]
273 pub fn with_long(mut self, long: impl Into<String>) -> Self {
274 self.long = Some(long.into());
275 self
276 }
277
278 #[must_use]
280 pub fn with_build(mut self, build: BuildInfo) -> Self {
281 self.build = build;
282 self
283 }
284
285 #[must_use]
287 pub fn with_default_auth_provider(mut self, provider: impl Into<String>) -> Self {
288 self.default_auth_provider = Some(provider.into());
289 self
290 }
291
292 #[must_use]
294 pub fn with_module(mut self, module: Module) -> Self {
295 self.modules.push(module);
296 self
297 }
298
299 #[must_use]
301 pub fn with_modules(mut self, modules: impl IntoIterator<Item = Module>) -> Self {
302 self.modules.extend(modules);
303 self
304 }
305
306 #[must_use]
308 pub fn with_command(mut self, command: RuntimeCommandSpec) -> Self {
309 self.commands.push(command);
310 self
311 }
312
313 #[must_use]
315 pub fn with_guide(mut self, guide: GuideEntry) -> Self {
316 self.guides.push(guide);
317 self
318 }
319
320 #[must_use]
322 pub fn with_guides(mut self, guides: impl IntoIterator<Item = GuideEntry>) -> Self {
323 self.guides.extend(guides);
324 self
325 }
326
327 #[must_use]
329 pub fn with_view(mut self, view: HumanViewDef) -> Self {
330 self.views.push(view);
331 self
332 }
333
334 #[must_use]
336 pub fn with_auth_provider(mut self, provider: Arc<dyn AuthProvider>) -> Self {
337 self.auth_providers.push(provider);
338 self
339 }
340
341 #[must_use]
343 pub fn with_authz(mut self, authz: Arc<dyn Authorizer>) -> Self {
344 self.authz = Some(authz);
345 self
346 }
347
348 #[must_use]
350 pub fn with_auditor(mut self, auditor: Arc<dyn Auditor>) -> Self {
351 self.auditor = Some(auditor);
352 self
353 }
354
355 #[must_use]
357 pub fn with_activity(mut self, activity: Arc<dyn ActivityEmitter>) -> Self {
358 self.activity = Some(activity);
359 self
360 }
361
362 #[must_use]
364 pub fn with_init_deps(mut self, init_deps: InitDeps) -> Self {
365 self.init_deps = Some(init_deps);
366 self
367 }
368
369 #[must_use]
371 pub fn with_register_flags(mut self, register_flags: RegisterFlags) -> Self {
372 self.register_flags = Some(register_flags);
373 self
374 }
375
376 #[must_use]
378 pub fn with_apply_flags(mut self, apply_flags: ApplyFlags) -> Self {
379 self.apply_flags = Some(apply_flags);
380 self
381 }
382
383 #[must_use]
385 pub fn with_pre_run(mut self, pre_run: PreRun) -> Self {
386 self.pre_run = Some(pre_run);
387 self
388 }
389
390 #[must_use]
392 pub fn with_meta_resolver(mut self, meta_resolver: ResolveMeta) -> Self {
393 self.meta_resolver = Some(meta_resolver);
394 self
395 }
396
397 #[must_use]
399 pub fn with_on_shutdown(mut self, on_shutdown: OnShutdown) -> Self {
400 self.on_shutdown = Some(on_shutdown);
401 self
402 }
403
404 #[must_use]
406 pub fn with_extra_search_docs(mut self, extra_search_docs: ExtraSearchDocs) -> Self {
407 self.extra_search_docs = Some(extra_search_docs);
408 self
409 }
410
411 #[must_use]
413 pub fn with_root_next_actions(mut self, root_next_actions: RootNextActions) -> Self {
414 self.root_next_actions = Some(root_next_actions);
415 self
416 }
417
418 #[must_use]
422 pub fn with_admin_category(mut self, category: impl Into<String>) -> Self {
423 self.admin_category = Some(category.into());
424 self
425 }
426
427 #[must_use]
433 pub fn with_config_commands(mut self) -> Self {
434 self.config_commands = true;
435 self
436 }
437
438 #[must_use]
461 pub fn with_argv0_alias(
462 mut self,
463 name: impl Into<String>,
464 command_path: impl IntoIterator<Item = impl Into<String>>,
465 ) -> Self {
466 let name = name.into();
467 debug_assert!(
468 is_valid_argv0_name(&name),
469 "argv0 route name {name:?} must be non-empty and contain only ASCII letters, digits, '-', or '_'"
470 );
471 debug_assert!(
472 name != self.name,
473 "argv0 route name {name:?} must differ from the CLI's own name {:?}",
474 self.name
475 );
476 let tokens = command_path.into_iter().map(Into::into).collect();
477 self.argv0_routes.insert(name, Argv0Route::Alias(tokens));
478 self
479 }
480
481 #[must_use]
503 pub fn with_argv0_personality(
504 mut self,
505 name: impl Into<String>,
506 build: impl Fn() -> CliConfig + Send + Sync + 'static,
507 ) -> Self {
508 let name = name.into();
509 debug_assert!(
510 is_valid_argv0_name(&name),
511 "argv0 route name {name:?} must be non-empty and contain only ASCII letters, digits, '-', or '_'"
512 );
513 debug_assert!(
514 name != self.name,
515 "argv0 route name {name:?} must differ from the CLI's own name {:?}",
516 self.name
517 );
518 self.argv0_routes
519 .insert(name, Argv0Route::Personality(Arc::new(build)));
520 self
521 }
522}
523
524impl std::fmt::Debug for CliConfig {
525 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
526 formatter
527 .debug_struct("CliConfig")
528 .field("name", &self.name)
529 .field("short", &self.short)
530 .field("long", &self.long)
531 .field("build", &self.build)
532 .field("app_id", &self.app_id)
533 .field("default_auth_provider", &self.default_auth_provider)
534 .field("modules", &self.modules)
535 .field("commands", &self.commands)
536 .field("guides", &self.guides)
537 .field("views", &self.views)
538 .field("auth_providers_len", &self.auth_providers.len())
539 .field("has_authz", &self.authz.is_some())
540 .field("has_auditor", &self.auditor.is_some())
541 .field("has_activity", &self.activity.is_some())
542 .field("has_init_deps", &self.init_deps.is_some())
543 .field("has_register_flags", &self.register_flags.is_some())
544 .field("has_apply_flags", &self.apply_flags.is_some())
545 .field("has_pre_run", &self.pre_run.is_some())
546 .field("has_meta_resolver", &self.meta_resolver.is_some())
547 .field("has_on_shutdown", &self.on_shutdown.is_some())
548 .field("has_extra_search_docs", &self.extra_search_docs.is_some())
549 .field("has_root_next_actions", &self.root_next_actions.is_some())
550 .field("admin_category", &self.admin_category)
551 .field(
552 "argv0_routes",
553 &self.argv0_routes.keys().collect::<Vec<_>>(),
554 )
555 .finish()
556 }
557}
558
559#[derive(Clone, Debug, PartialEq)]
561pub struct CliRunOutput {
562 pub exit_code: i32,
564 pub rendered: String,
566}
567
568impl From<crate::middleware::MiddlewareOutput> for CliRunOutput {
569 fn from(o: crate::middleware::MiddlewareOutput) -> Self {
570 Self {
571 exit_code: o.exit_code,
572 rendered: o.rendered,
573 }
574 }
575}
576
577#[derive(Clone)]
583pub struct Cli {
584 config: CliConfig,
585 middleware: Middleware,
586 root: Command,
587 commands: BTreeMap<String, RuntimeCommandSpec>,
588 module_entries: Vec<ModuleHelpEntry>,
589 guide_entries: Vec<GuideEntry>,
590 init_deps: Option<InitDeps>,
591 apply_flags: Option<ApplyFlags>,
592 pre_run: Option<PreRun>,
593 meta_resolver: Option<ResolveMeta>,
594 on_shutdown: Option<OnShutdown>,
595 extra_search_docs: Option<ExtraSearchDocs>,
596 root_next_actions: Option<RootNextActions>,
597 init_state: Arc<Mutex<Option<std::result::Result<Middleware, InitFailure>>>>,
598}
599
600#[derive(Clone, Debug, Eq, PartialEq)]
601struct InitFailure {
602 message: String,
603 code: String,
604 system: String,
605 request_id: String,
606 exit_code: i32,
607}
608
609impl InitFailure {
610 fn capture(err: &CliCoreError) -> Self {
611 let envelope = crate::output::build_error_envelope(err, "");
612 let (code, system, request_id) = envelope.error.map_or_else(
613 || ("ERROR".to_owned(), String::new(), String::new()),
614 |error| (error.code, error.system, error.request_id),
615 );
616 Self {
617 message: err.to_string(),
618 code,
619 system,
620 request_id,
621 exit_code: exit_code_for_error(err),
622 }
623 }
624
625 fn into_error(self) -> CliCoreError {
626 CliCoreError::with_exit_code(
627 self.exit_code,
628 CliCoreError::SystemMessage {
629 message: self.message,
630 system: self.system,
631 code: self.code,
632 request_id: self.request_id,
633 },
634 )
635 }
636}
637
638impl std::fmt::Debug for Cli {
639 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
640 formatter
641 .debug_struct("Cli")
642 .field("config", &self.config)
643 .field("middleware", &self.middleware)
644 .field("root", &self.root)
645 .field("commands", &self.commands)
646 .field("module_entries", &self.module_entries)
647 .field("guide_entries", &self.guide_entries)
648 .field("has_init_deps", &self.init_deps.is_some())
649 .field("has_apply_flags", &self.apply_flags.is_some())
650 .field("has_pre_run", &self.pre_run.is_some())
651 .field("has_meta_resolver", &self.meta_resolver.is_some())
652 .field("has_on_shutdown", &self.on_shutdown.is_some())
653 .field("has_extra_search_docs", &self.extra_search_docs.is_some())
654 .field("has_root_next_actions", &self.root_next_actions.is_some())
655 .finish()
656 }
657}
658
659impl Cli {
660 #[must_use]
662 pub fn new(config: CliConfig) -> Self {
663 let auth_providers = config.auth_providers.clone();
664 let guides = config.guides.clone();
665 let views = config.views.clone();
666 let modules = config.modules.clone();
667 let commands = config.commands.clone();
668 let init_deps = config.init_deps.clone();
669 let apply_flags = config.apply_flags.clone();
670 let pre_run = config.pre_run.clone();
671 let meta_resolver = config.meta_resolver.clone();
672 let on_shutdown = config.on_shutdown.clone();
673 let extra_search_docs = config.extra_search_docs.clone();
674 let root_next_actions = config.root_next_actions.clone();
675 let mut root = Command::new(config.name.clone())
676 .about(config.short.clone())
677 .disable_help_subcommand(true)
678 .version(config.build.version_string());
679 if let Some(long) = &config.long
680 && !long.is_empty()
681 {
682 root = root.long_about(long.clone());
683 }
684 root = register_global_flags(root)
685 .subcommand(help_command())
686 .subcommand(guide_command())
687 .subcommand(Command::new("tree").about("Display full command tree"));
688 if let Some(register_flags) = &config.register_flags {
689 root = register_flags(root);
690 }
691 let intro = config
692 .long
693 .as_deref()
694 .filter(|long| !long.is_empty())
695 .unwrap_or(config.short.as_str());
696 root = root
697 .long_about(build_root_long(intro, &[], false))
698 .help_template(ROOT_HELP_TEMPLATE);
699
700 let mut middleware = Middleware::new();
701 middleware.app_id = config.app_id.clone();
702 middleware.config = Arc::new(crate::config::ConfigFile::load(&config.app_id));
705 middleware.default_auth_provider = config.default_auth_provider.clone().unwrap_or_default();
706 middleware.authz = config.authz.clone();
707 middleware.auditor = config.auditor.clone();
708 middleware.activity = config.activity.clone();
709 middleware
710 .schema_registry
711 .merge(&global_schema_registry_snapshot());
712 middleware
713 .human_views
714 .merge(&global_human_view_registry_snapshot());
715
716 let mut cli = Self {
717 config,
718 middleware,
719 root,
720 commands: BTreeMap::new(),
721 module_entries: Vec::new(),
722 guide_entries: Vec::new(),
723 init_deps,
724 apply_flags,
725 pre_run,
726 meta_resolver,
727 on_shutdown,
728 extra_search_docs,
729 root_next_actions,
730 init_state: Arc::new(Mutex::new(None)),
731 };
732 for provider in auth_providers {
733 cli.register_auth_provider(provider);
734 }
735 if cli.middleware.default_auth_provider.is_empty()
736 && let Some(provider) = cli.middleware.auth.registered_names().first()
737 {
738 cli.middleware.default_auth_provider = provider.clone();
739 }
740 if !cli.middleware.default_auth_provider.is_empty() {
741 cli.ensure_auth_command();
742 }
743 for view in views {
744 cli.middleware.human_views.register(view);
745 }
746 cli.add_guides(guides);
747 for module in modules {
748 cli.add_module(module);
749 }
750 for command in commands {
751 cli.add_command(command);
752 }
753 if cli.config.config_commands {
754 cli.ensure_config_command();
755 }
756 cli
757 }
758
759 fn register_auth_help_entry(&mut self) {
764 let category = self
765 .config
766 .admin_category
767 .clone()
768 .unwrap_or_else(|| DEFAULT_ADMIN_CATEGORY.to_owned());
769 let already_listed = self.module_entries.iter().any(|entry| entry.name == "auth");
770 let short = self
771 .root
772 .find_subcommand("auth")
773 .filter(|auth| !auth.is_hide_set())
774 .map(|auth| {
775 auth.get_about()
776 .map(ToString::to_string)
777 .unwrap_or_default()
778 });
779 if !already_listed && let Some(short) = short {
780 self.module_entries.push(ModuleHelpEntry {
781 category,
782 name: "auth".to_owned(),
783 short,
784 });
785 }
786 self.refresh_root_long();
787 }
788
789 #[must_use]
791 pub fn middleware(&self) -> &Middleware {
792 &self.middleware
793 }
794
795 pub fn middleware_mut(&mut self) -> &mut Middleware {
797 &mut self.middleware
798 }
799
800 pub async fn execute(&self) -> ExitCode {
802 let mut stdout = std::io::stdout().lock();
803 let mut stderr = std::io::stderr().lock();
804 match self
805 .execute_from(std::env::args_os(), &mut stdout, &mut stderr)
806 .await
807 {
808 Ok(code) => code,
809 Err(err) => {
810 drop(writeln!(stderr, "{err}"));
811 ExitCode::from(1)
812 }
813 }
814 }
815
816 pub async fn execute_from<I, S, O, E>(
818 &self,
819 args: I,
820 stdout: &mut O,
821 stderr: &mut E,
822 ) -> std::io::Result<ExitCode>
823 where
824 I: IntoIterator<Item = S>,
825 S: Into<std::ffi::OsString> + Clone,
826 O: Write,
827 E: Write,
828 {
829 self.execute_from_until_signal(args, stdout, stderr, shutdown_signal())
830 .await
831 }
832
833 pub async fn execute_from_until_signal<I, S, O, E, Shutdown>(
835 &self,
836 args: I,
837 stdout: &mut O,
838 stderr: &mut E,
839 shutdown: Shutdown,
840 ) -> std::io::Result<ExitCode>
841 where
842 I: IntoIterator<Item = S>,
843 S: Into<std::ffi::OsString> + Clone,
844 O: Write,
845 E: Write,
846 Shutdown: Future<Output = ()>,
847 {
848 let output = run_until_signal(self.run(args), shutdown).await;
849 if output.exit_code == 130
850 && output.rendered == "command interrupted\n"
851 && let Some(on_shutdown) = &self.on_shutdown
852 {
853 on_shutdown();
854 }
855 if output.exit_code == 0 {
856 stdout.write_all(output.rendered.as_bytes())?;
857 } else {
858 stderr.write_all(output.rendered.as_bytes())?;
859 }
860 Ok(process_exit_code(output.exit_code))
861 }
862
863 pub fn register_auth_provider(&mut self, provider: Arc<dyn AuthProvider>) -> &mut Self {
865 self.middleware.auth.register(provider);
866 self.ensure_auth_command();
867 self.refresh_root_long();
868 self
869 }
870
871 #[must_use]
873 pub fn root_command(&self) -> &Command {
874 &self.root
875 }
876
877 pub fn add_module_group(
879 &mut self,
880 category: impl Into<String>,
881 group: RuntimeGroupSpec,
882 ) -> &mut Self {
883 let category = category.into();
884 if !group.group.hidden {
885 self.module_entries.push(ModuleHelpEntry {
886 category,
887 name: group.group.name.clone(),
888 short: group.group.short.clone(),
889 });
890 }
891
892 let mut prefix = Vec::new();
893 register_runtime_group_schemas(&group, &mut prefix, &mut self.middleware.schema_registry);
894 let mut prefix = Vec::new();
895 group.register_commands(&mut prefix, &mut self.commands);
896 let mut prefix = Vec::new();
897 let clap_group = runtime_group_clap_command_with_schema_help(
898 &group,
899 &mut prefix,
900 &self.middleware.schema_registry,
901 );
902 self.root = self.root.clone().subcommand(clap_group);
903 self.refresh_root_long();
904 self
905 }
906
907 pub fn add_module(&mut self, module: Module) -> &mut Self {
909 for view in module.views.clone() {
910 self.middleware.human_views.register(view);
911 }
912 self.add_guides(module.guides.clone());
913 let mut context = ModuleContext::new(&mut self.middleware);
914 let group = (module.register)(&mut context);
915 let (guides, views) = context.into_parts();
916 for view in views {
917 self.middleware.human_views.register(view);
918 }
919 self.add_guides(guides);
920 self.add_module_group(module.category, group)
921 }
922
923 pub fn add_command(&mut self, command: RuntimeCommandSpec) -> &mut Self {
925 let name = command.spec.name.clone();
926 register_command_schema(&command.spec, &name, &mut self.middleware.schema_registry);
927 self.commands.insert(name, command.clone());
928 self.root = self
929 .root
930 .clone()
931 .subcommand(command_clap_command_with_schema_help(
932 &command.spec,
933 &command.spec.name,
934 &self.middleware.schema_registry,
935 ));
936 self
937 }
938
939 pub fn set_has_guide(&mut self, has_guide: bool) -> &mut Self {
941 if has_guide && self.guide_entries.is_empty() && !has_subcommand(&self.root, "guide") {
942 self.root = self.root.clone().subcommand(guide_command());
943 }
944 self.refresh_root_long();
945 self
946 }
947
948 pub fn add_guides(&mut self, entries: impl IntoIterator<Item = GuideEntry>) -> &mut Self {
950 let mut seen = self
951 .guide_entries
952 .iter()
953 .map(|entry| entry.name.clone())
954 .collect::<BTreeSet<_>>();
955 for entry in entries {
956 if seen.insert(entry.name.clone()) {
957 self.guide_entries.push(entry);
958 }
959 }
960 if !self.guide_entries.is_empty() && !has_subcommand(&self.root, "guide") {
961 self.root = self.root.clone().subcommand(guide_command());
962 }
963 self.refresh_root_long();
964 self
965 }
966
967 async fn resolve_argv0(&self, text_args: Vec<String>, depth: usize) -> Argv0Outcome {
976 if self.config.argv0_routes.is_empty() {
977 return Argv0Outcome::Proceed(text_args);
978 }
979
980 if depth > MAX_ARGV0_DEPTH {
981 return Argv0Outcome::Handled(
982 self.render_argv0_error(&text_args, "argv0 dispatch recursion limit exceeded"),
983 );
984 }
985
986 let explicit = text_args.get(1).map(String::as_str) == Some("argv0");
991 let (name, rest) = if explicit {
992 match text_args.get(2) {
993 None => {
994 return Argv0Outcome::Handled(self.render_argv0_error(
995 &text_args,
996 "the argv0 command requires a name to dispatch as",
997 ));
998 }
999 Some(name) => (
1003 program_basename(name),
1004 text_args
1005 .get(3..)
1006 .map(<[String]>::to_vec)
1007 .unwrap_or_default(),
1008 ),
1009 }
1010 } else {
1011 let name = text_args
1012 .first()
1013 .map(|arg| program_basename(arg))
1014 .unwrap_or_default();
1015 let rest = text_args
1016 .get(1..)
1017 .map(<[String]>::to_vec)
1018 .unwrap_or_default();
1019 (name, rest)
1020 };
1021
1022 match self.config.argv0_routes.get(&name) {
1023 Some(Argv0Route::Alias(tokens)) => {
1024 let mut rewritten = Vec::with_capacity(1 + tokens.len() + rest.len());
1027 rewritten.push(self.config.name.clone());
1028 rewritten.extend(tokens.iter().cloned());
1029 rewritten.extend(rest);
1030 Argv0Outcome::Proceed(rewritten)
1031 }
1032 Some(Argv0Route::Personality(build)) => {
1033 let config = build();
1038 let bin = config.name.clone();
1039 let alt = Self::new(config);
1040 let mut alt_args = Vec::with_capacity(1 + rest.len());
1041 alt_args.push(bin);
1042 alt_args.extend(rest);
1043 Argv0Outcome::Handled(Box::pin(alt.run_with_depth(alt_args, depth + 1)).await)
1044 }
1045 None if explicit => Argv0Outcome::Handled(self.render_argv0_error(
1046 &text_args,
1047 format!(
1048 "{name:?} is not a registered argv0 name; known names: {}",
1049 self.known_argv0_names()
1050 ),
1051 )),
1052 None => {
1053 let mut rewritten = Vec::with_capacity(1 + rest.len());
1058 rewritten.push(self.config.name.clone());
1059 rewritten.extend(rest);
1060 Argv0Outcome::Proceed(rewritten)
1061 }
1062 }
1063 }
1064
1065 fn known_argv0_names(&self) -> String {
1068 self.config
1069 .argv0_routes
1070 .keys()
1071 .cloned()
1072 .collect::<Vec<_>>()
1073 .join(", ")
1074 }
1075
1076 fn render_argv0_error(&self, text_args: &[String], message: impl Into<String>) -> CliRunOutput {
1081 let mut middleware = self.middleware.clone();
1082 middleware.output_format =
1083 extract_output_format(text_args, &default_output_format(&self.config.app_id));
1084 let err = CliCoreError::message(message);
1085 self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id))
1086 }
1087
1088 #[must_use]
1093 pub fn argv0_names(&self) -> Vec<&str> {
1094 self.config
1095 .argv0_routes
1096 .keys()
1097 .map(String::as_str)
1098 .collect()
1099 }
1100
1101 pub fn create_link(
1126 &self,
1127 name: &str,
1128 dir: impl AsRef<Path>,
1129 target: Option<&Path>,
1130 method: Argv0LinkMethod,
1131 ) -> std::io::Result<PathBuf> {
1132 if !self.config.argv0_routes.contains_key(name) {
1133 return Err(std::io::Error::new(
1134 std::io::ErrorKind::InvalidInput,
1135 format!("{name:?} is not a registered argv0 name"),
1136 ));
1137 }
1138
1139 let dir = dir.as_ref();
1140 std::fs::create_dir_all(dir)?;
1141 let link = dir.join(argv0_link_file_name(name, method));
1142
1143 let resolved_target;
1145 let target = match target {
1146 Some(target) => target,
1147 None => {
1148 resolved_target = std::env::current_exe()?;
1149 resolved_target.as_path()
1150 }
1151 };
1152
1153 if std::fs::symlink_metadata(&link).is_ok() {
1157 if argv0_link_matches(&link, target, name, method)? {
1158 return Ok(link);
1159 }
1160 std::fs::remove_file(&link)?;
1161 }
1162
1163 match method {
1164 Argv0LinkMethod::SoftLink => create_symlink(target, &link)?,
1165 Argv0LinkMethod::HardLink => std::fs::hard_link(target, &link)?,
1166 Argv0LinkMethod::Script => {
1167 std::fs::write(&link, argv0_script_contents(target, name))?;
1168 make_executable(&link)?;
1169 }
1170 }
1171 Ok(link)
1172 }
1173
1174 pub async fn run<I, S>(&self, args: I) -> CliRunOutput
1176 where
1177 I: IntoIterator<Item = S>,
1178 S: Into<std::ffi::OsString> + Clone,
1179 {
1180 self.run_with_depth(args, 0).await
1181 }
1182
1183 async fn run_with_depth<I, S>(&self, args: I, depth: usize) -> CliRunOutput
1186 where
1187 I: IntoIterator<Item = S>,
1188 S: Into<std::ffi::OsString> + Clone,
1189 {
1190 let raw_args = args
1191 .into_iter()
1192 .map(Into::into)
1193 .collect::<Vec<std::ffi::OsString>>();
1194 let text_args = raw_args
1195 .iter()
1196 .map(|arg| arg.to_string_lossy().into_owned())
1197 .collect::<Vec<_>>();
1198 let text_args = match self.resolve_argv0(text_args, depth).await {
1199 Argv0Outcome::Handled(output) => return output,
1200 Argv0Outcome::Proceed(args) => args,
1201 };
1202 let mut clap_args = normalize_optional_global_flags_before_command(&self.root, &text_args);
1203 if has_root_version_flag(&text_args, &self.root, &self.config.name) {
1204 return self.finish_run(CliRunOutput {
1205 exit_code: 0,
1206 rendered: format!(
1207 "{} version {}\n",
1208 self.config.name,
1209 self.config.build.version_string()
1210 ),
1211 });
1212 }
1213 if let Some(output) = self.try_run_schema_bypass(&text_args) {
1214 return output;
1215 }
1216 if let Some(output) = self.try_run_search_bypass(&text_args) {
1217 return output;
1218 }
1219 let bool_flags = derive_bool_flags(&self.root);
1222 let value_flags = derive_value_flags(&self.root);
1223 let positionals =
1224 positional_command_tokens(&text_args, &self.config.name, &bool_flags, &value_flags);
1225 let command_keyword_count = match text_args.iter().position(|arg| arg == "--") {
1230 Some(end) => positional_command_tokens(
1231 &text_args[..end],
1232 &self.config.name,
1233 &bool_flags,
1234 &value_flags,
1235 )
1236 .len(),
1237 None => positionals.len(),
1238 };
1239 if let Some(parts) =
1240 group_help_target_parts(&self.root, &positionals, command_keyword_count)
1241 {
1242 clap_args = rewrite_group_help_args(
1249 &clap_args,
1250 &self.config.name,
1251 &bool_flags,
1252 &value_flags,
1253 &parts,
1254 );
1255 } else if let Some(message) = unknown_group_command_message(&self.root, &positionals) {
1256 return self.finish_run(CliRunOutput {
1257 exit_code: 1,
1258 rendered: message,
1259 });
1260 }
1261
1262 let matches = match self.root.clone().try_get_matches_from(clap_args) {
1263 Ok(matches) => matches,
1264 Err(err) => {
1265 return self.finish_run(CliRunOutput {
1266 exit_code: err.exit_code(),
1267 rendered: err.to_string(),
1268 });
1269 }
1270 };
1271
1272 let default_format = default_output_format(&self.config.app_id);
1273 let flags = global_flags_from_matches(&matches, &default_format);
1274 crate::config::set_credential_store_flag(flags.credential_store);
1277 let command_timeout = match parse_command_timeout(&flags.timeout) {
1278 Ok(timeout) => timeout,
1279 Err(err) => {
1280 return self.finish_run(render_cli_error(
1281 &self.middleware,
1282 &err,
1283 &self.config.app_id,
1284 ));
1285 }
1286 };
1287 let mut middleware = self.middleware.clone();
1288 apply_global_flags(&mut middleware, &flags, command_timeout);
1289 if let Err(err) = self.apply_config_flags(&matches, &mut middleware) {
1290 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
1291 }
1292
1293 let command_path = command_path_from_matches(&self.config.name, &matches);
1294 if command_path == "help" {
1295 if let Err(err) = self.run_pre_run(&mut middleware, &command_path, &help_args(&matches))
1296 {
1297 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
1298 }
1299 return self.finish_run(self.render_help_command(&matches));
1300 }
1301 if command_path == "tree" {
1302 if let Err(err) = self.run_pre_run(
1303 &mut middleware,
1304 &command_path,
1305 &crate::middleware::ValueMap::new(),
1306 ) {
1307 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
1308 }
1309 return self.finish_run(tree_render::render_tree(
1310 &self.root,
1311 &self.config.app_id,
1312 &middleware,
1313 ));
1314 }
1315 if command_path == "guide" {
1316 if let Err(err) =
1317 self.run_pre_run(&mut middleware, &command_path, &guide_args(&matches))
1318 {
1319 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
1320 }
1321 return self.finish_run(self.render_guide(&matches));
1322 }
1323 let Some(command) = self.commands.get(&command_path) else {
1324 if !command_path.is_empty()
1325 && let Some(group) = find_command_by_colon_path(&self.root, &command_path)
1326 && group.get_subcommands().next().is_some()
1327 {
1328 if let Err(err) = self.run_pre_run(
1329 &mut middleware,
1330 &command_path,
1331 &crate::middleware::ValueMap::new(),
1332 ) {
1333 return self.finish_run(render_cli_error(
1334 &middleware,
1335 &err,
1336 &self.config.app_id,
1337 ));
1338 }
1339 return self.finish_run(CliRunOutput {
1340 exit_code: 0,
1341 rendered: group.clone().render_long_help().to_string(),
1342 });
1343 }
1344 if command_path.is_empty()
1345 && let Some(root_next_actions) = &self.root_next_actions
1346 {
1347 let actions = root_next_actions();
1352 return self.finish_run(self.render_root(&middleware, actions));
1353 }
1354 return self.finish_run(CliRunOutput {
1355 exit_code: if command_path.is_empty() { 0 } else { 1 },
1356 rendered: if command_path.is_empty() {
1357 self.root.clone().render_long_help().to_string()
1358 } else {
1359 format!("unknown command {command_path:?}")
1360 },
1361 });
1362 };
1363
1364 let mut middleware = match self.initialized_middleware() {
1365 Ok(middleware) => middleware,
1366 Err(err) => {
1367 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
1368 }
1369 };
1370 apply_global_flags(&mut middleware, &flags, command_timeout);
1371 if let Err(err) = self.apply_config_flags(&matches, &mut middleware) {
1372 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
1373 }
1374
1375 let leaf = leaf_matches(&matches);
1376 let args = command_args_from_matches(leaf, &command.spec, false);
1377 let user_args = command_args_from_matches(leaf, &command.spec, true);
1378 if let Err(err) = self.run_pre_run(&mut middleware, &command_path, &args) {
1379 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
1380 }
1381 let meta = self.resolve_meta(&command_path, command.spec.metadata());
1382 let default_fields = command.spec.default_fields.clone().unwrap_or_default();
1383 let system = command.spec.system.clone().unwrap_or_default();
1384
1385 if let Some(streaming_handler) = command.streaming_handler.clone() {
1386 let result = run_with_timeout(
1387 command_timeout,
1388 &flags.timeout,
1389 run_streaming_command(
1390 &middleware,
1391 MiddlewareRequest {
1392 meta,
1393 command_path: &command_path,
1394 system: &system,
1395 user_args,
1396 args,
1397 default_fields: &default_fields,
1398 auth: command.spec.auth,
1399 },
1400 Arc::new(leaf.clone()),
1401 streaming_handler,
1402 ),
1403 )
1404 .await;
1405 return self.finish_run(match result {
1406 Ok(output) => output,
1407 Err(err) => render_cli_error(&middleware, &err, &self.config.app_id),
1408 });
1409 }
1410
1411 let handler = command.handler.clone();
1412 let args_for_handler = args.clone();
1413 let user_args_for_handler = user_args.clone();
1414 let handler_path = command_path.clone();
1415 let middleware_for_handler = middleware.clone();
1416 let raw_matches_for_handler = Arc::new(leaf.clone());
1417 let result = run_with_timeout(
1418 command_timeout,
1419 &flags.timeout,
1420 middleware.run(
1421 MiddlewareRequest {
1422 meta,
1423 command_path: &command_path,
1424 system: &system,
1425 user_args,
1426 args,
1427 default_fields: &default_fields,
1428 auth: command.spec.auth,
1429 },
1430 async move |credential| {
1431 handler(CommandContext {
1432 credential,
1433 args: args_for_handler,
1434 user_args: user_args_for_handler,
1435 command_path: handler_path,
1436 middleware: middleware_for_handler,
1437 raw_matches: raw_matches_for_handler,
1438 })
1439 .await
1440 },
1441 ),
1442 )
1443 .await;
1444
1445 match result {
1446 Ok(output) => self.finish_run(output.into()),
1447 Err(err) => self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id)),
1448 }
1449 }
1450
1451 fn try_run_search_bypass(&self, args: &[String]) -> Option<CliRunOutput> {
1452 let query = extract_search_query(args);
1453 if query.is_empty() {
1454 return None;
1455 }
1456 let scope = self.search_scope(args);
1457 let output_format =
1458 extract_output_format(args, &default_output_format(&self.config.app_id));
1459 Some(self.render_search(&query, &scope, &output_format))
1460 }
1461
1462 fn try_run_schema_bypass(&self, args: &[String]) -> Option<CliRunOutput> {
1463 if !has_true_schema_flag(args) {
1464 return None;
1465 }
1466 let bool_flags = derive_bool_flags(&self.root);
1467 let value_flags = derive_value_flags(&self.root);
1468 let command_path =
1469 self.canonical_command_path(&extract_command_path(args, &bool_flags, &value_flags));
1470 let schema = self.middleware.schema_registry.get_by_path(&command_path)?;
1471 let output_format =
1472 extract_output_format(args, &default_output_format(&self.config.app_id));
1473 Some(self.render_schema(schema, &output_format))
1474 }
1475
1476 fn render_schema(
1477 &self,
1478 schema: crate::output::SchemaInfo,
1479 output_format: &str,
1480 ) -> CliRunOutput {
1481 let format: crate::output::OutputFormat = match output_format.parse() {
1482 Ok(format) => format,
1483 Err(err) => {
1484 return CliRunOutput {
1485 exit_code: exit_code_for_error(&err),
1486 rendered: err.to_string(),
1487 };
1488 }
1489 };
1490 let envelope =
1491 crate::Envelope::success(schema, self.config.app_id.clone()).prepare_for_render("");
1492 match crate::output::render(format, &envelope) {
1493 Ok(rendered) => CliRunOutput {
1494 exit_code: 0,
1495 rendered,
1496 },
1497 Err(err) => CliRunOutput {
1498 exit_code: exit_code_for_error(&err),
1499 rendered: err.to_string(),
1500 },
1501 }
1502 }
1503
1504 fn render_search(&self, query: &str, scope: &str, output_format: &str) -> CliRunOutput {
1505 let format: crate::output::OutputFormat = match output_format.parse() {
1506 Ok(format) => format,
1507 Err(err) => {
1508 return CliRunOutput {
1509 exit_code: exit_code_for_error(&err),
1510 rendered: err.to_string(),
1511 };
1512 }
1513 };
1514 let docs = self.search_documents(scope);
1515 let results = SearchIndex::new(docs).search(query, 10);
1516 let envelope =
1517 crate::Envelope::success(results, self.config.app_id.clone()).prepare_for_render("");
1518 match crate::output::render(format, &envelope) {
1519 Ok(rendered) => CliRunOutput {
1520 exit_code: 0,
1521 rendered,
1522 },
1523 Err(err) => CliRunOutput {
1524 exit_code: exit_code_for_error(&err),
1525 rendered: err.to_string(),
1526 },
1527 }
1528 }
1529
1530 fn render_root(&self, middleware: &Middleware, actions: Vec<NextAction>) -> CliRunOutput {
1536 if !crate::output::is_valid_output_format(&middleware.output_format) {
1541 let err = CliCoreError::InvalidOutputFormat(middleware.output_format.clone());
1542 return CliRunOutput {
1543 exit_code: exit_code_for_error(&err),
1544 rendered: err.to_string(),
1545 };
1546 }
1547 let format = middleware
1548 .output_format
1549 .parse()
1550 .unwrap_or(crate::output::OutputFormat::Json);
1551 if format == crate::output::OutputFormat::Human {
1552 let base_long = self
1556 .root
1557 .get_long_about()
1558 .map(ToString::to_string)
1559 .unwrap_or_default();
1560 let long = format!("{base_long}{}", render_next_actions_human(&actions));
1561 let rendered = self
1562 .root
1563 .clone()
1564 .long_about(long)
1565 .render_long_help()
1566 .to_string();
1567 return CliRunOutput {
1568 exit_code: 0,
1569 rendered,
1570 };
1571 }
1572 let description = self
1573 .config
1574 .long
1575 .as_deref()
1576 .filter(|long| !long.is_empty())
1577 .unwrap_or(self.config.short.as_str());
1578 let data = serde_json::json!({
1579 "description": description,
1580 "version": self.config.build.version,
1581 });
1582 let envelope = crate::Envelope::success(data, self.config.app_id.clone())
1583 .with_next_actions(actions)
1584 .prepare_for_render(&middleware.verbose);
1585 match crate::output::render(format, &envelope) {
1586 Ok(rendered) => CliRunOutput {
1587 exit_code: 0,
1588 rendered,
1589 },
1590 Err(err) => CliRunOutput {
1591 exit_code: exit_code_for_error(&err),
1592 rendered: err.to_string(),
1593 },
1594 }
1595 }
1596
1597 fn search_documents(&self, scope: &str) -> Vec<SearchDocument> {
1598 let (scoped, mut prefix) = find_command_and_canonical_path_by_colon_path(&self.root, scope)
1599 .unwrap_or((&self.root, Vec::new()));
1600 let mut docs = Vec::new();
1601 let mut aliases = Vec::new();
1602 append_command_alias_terms(scoped, &mut aliases);
1603 collect_command_search_documents(scoped, &mut prefix, &mut aliases, &mut docs);
1604 if scope.is_empty() {
1605 for entry in &self.guide_entries {
1606 docs.push(SearchDocument {
1607 id: format!("guide:{}", entry.name),
1608 kind: "guide".to_owned(),
1609 title: format!("guide {}", entry.name),
1610 summary: entry.summary.clone(),
1611 content: format!("{} {}", entry.summary, entry.content),
1612 });
1613 }
1614 if let Some(extra_search_docs) = &self.extra_search_docs {
1615 docs.extend(extra_search_docs());
1616 }
1617 }
1618 docs
1619 }
1620
1621 fn search_scope(&self, args: &[String]) -> String {
1622 let parts = extract_search_scope_parts(args);
1623 canonical_path_from_parts(&self.root, &parts).unwrap_or_default()
1624 }
1625
1626 fn canonical_command_path(&self, command_path: &str) -> String {
1627 find_command_and_canonical_path_by_colon_path(&self.root, command_path).map_or_else(
1628 || command_path.to_owned(),
1629 |(_, canonical)| canonical.join(":"),
1630 )
1631 }
1632
1633 fn render_guide(&self, matches: &ArgMatches) -> CliRunOutput {
1634 let leaf = leaf_matches(matches);
1635 let topic = leaf.get_one::<String>("topic").map(String::as_str);
1636 match guide_content(&self.guide_entries, topic) {
1637 Ok(rendered) => CliRunOutput {
1638 exit_code: 0,
1639 rendered,
1640 },
1641 Err(err) => CliRunOutput {
1642 exit_code: 1,
1643 rendered: err,
1644 },
1645 }
1646 }
1647
1648 fn render_help_command(&self, matches: &ArgMatches) -> CliRunOutput {
1649 let leaf = leaf_matches(matches);
1650 let parts = leaf
1651 .get_many::<String>("command")
1652 .map(|values| values.map(String::as_str).collect::<Vec<_>>())
1653 .unwrap_or_default();
1654 self.render_help_for_parts(&parts)
1655 }
1656
1657 fn render_help_for_parts(&self, parts: &[&str]) -> CliRunOutput {
1664 if parts.is_empty() {
1665 return CliRunOutput {
1666 exit_code: 0,
1667 rendered: self.root.clone().render_long_help().to_string(),
1668 };
1669 }
1670 let Some(command) = find_help_target(&self.root, parts) else {
1671 return CliRunOutput {
1672 exit_code: 1,
1673 rendered: format!(
1674 "unknown command {:?} — run '{} help' for available commands",
1675 parts.join(" "),
1676 self.config.name
1677 ),
1678 };
1679 };
1680 CliRunOutput {
1681 exit_code: 0,
1682 rendered: command.clone().render_long_help().to_string(),
1683 }
1684 }
1685
1686 fn refresh_root_long(&mut self) {
1687 const BUILTINS: [&str; 4] = ["help", "guide", "tree", "completion"];
1692 let categorized: BTreeSet<&str> = self
1693 .module_entries
1694 .iter()
1695 .map(|entry| entry.name.as_str())
1696 .collect();
1697 let mut generic: Vec<ModuleHelpEntry> = self
1698 .root
1699 .get_subcommands()
1700 .filter(|command| !command.is_hide_set())
1701 .filter(|command| !BUILTINS.contains(&command.get_name()))
1702 .filter(|command| !categorized.contains(command.get_name()))
1703 .map(|command| ModuleHelpEntry {
1704 category: "Commands".to_owned(),
1705 name: command.get_name().to_owned(),
1706 short: command
1707 .get_about()
1708 .map(ToString::to_string)
1709 .unwrap_or_default(),
1710 })
1711 .collect();
1712 generic.sort_by(|left, right| left.name.cmp(&right.name));
1713
1714 let mut entries = self.module_entries.clone();
1715 entries.extend(generic);
1716 let has_guide = !self.guide_entries.is_empty() || has_subcommand(&self.root, "guide");
1717 let intro = self
1718 .config
1719 .long
1720 .as_deref()
1721 .filter(|long| !long.is_empty())
1722 .unwrap_or(self.config.short.as_str());
1723 self.root = self
1724 .root
1725 .clone()
1726 .long_about(build_root_long(intro, &entries, has_guide));
1727 }
1728
1729 fn ensure_auth_command(&mut self) {
1730 let default_provider = self.default_auth_provider();
1731 let registered_names = self.middleware.auth.registered_names();
1732 if default_provider.is_empty() && registered_names.is_empty() {
1733 return;
1734 }
1735 let replacing_builtin = self.commands.contains_key("auth:login");
1736 if has_subcommand(&self.root, "auth") && !replacing_builtin {
1737 return;
1738 }
1739 let group = auth_command_group(&default_provider, ®istered_names);
1740 let mut prefix = Vec::new();
1741 group.register_commands(&mut prefix, &mut self.commands);
1742 let mut prefix = Vec::new();
1743 let clap_group = runtime_group_clap_command_with_schema_help(
1744 &group,
1745 &mut prefix,
1746 &self.middleware.schema_registry,
1747 );
1748 self.root = if replacing_builtin {
1749 self.root.clone().mut_subcommand("auth", |_| clap_group)
1750 } else {
1751 self.root.clone().subcommand(clap_group)
1752 };
1753 self.register_auth_help_entry();
1757 }
1758
1759 fn ensure_config_command(&mut self) {
1763 if has_subcommand(&self.root, "config") {
1764 return;
1765 }
1766 let group = crate::config_commands::config_command_group();
1767 let mut prefix = Vec::new();
1768 group.register_commands(&mut prefix, &mut self.commands);
1769 let mut prefix = Vec::new();
1770 let clap_group = runtime_group_clap_command_with_schema_help(
1771 &group,
1772 &mut prefix,
1773 &self.middleware.schema_registry,
1774 );
1775 self.root = self.root.clone().subcommand(clap_group);
1776 let category = self
1777 .config
1778 .admin_category
1779 .clone()
1780 .unwrap_or_else(|| DEFAULT_ADMIN_CATEGORY.to_owned());
1781 if !self
1782 .module_entries
1783 .iter()
1784 .any(|entry| entry.name == "config")
1785 {
1786 self.module_entries.push(ModuleHelpEntry {
1787 category,
1788 name: "config".to_owned(),
1789 short: "Read and write the CLI config file".to_owned(),
1790 });
1791 }
1792 self.refresh_root_long();
1793 }
1794
1795 fn default_auth_provider(&self) -> String {
1796 if !self.middleware.default_auth_provider.is_empty() {
1797 return self.middleware.default_auth_provider.clone();
1798 }
1799 self.middleware
1800 .auth
1801 .registered_names()
1802 .into_iter()
1803 .next()
1804 .unwrap_or_default()
1805 }
1806
1807 fn initialized_middleware(&self) -> Result<Middleware> {
1808 let Some(init_deps) = &self.init_deps else {
1809 return Ok(self.middleware.clone());
1810 };
1811 let mut guard = self
1812 .init_state
1813 .lock()
1814 .map_err(|_| CliCoreError::message("init deps lock poisoned"))?;
1815 if let Some(result) = guard.as_ref() {
1816 return result.clone().map_err(InitFailure::into_error);
1817 }
1818 let mut middleware = self.middleware.clone();
1819 let result = init_deps(&mut middleware)
1820 .map(|()| middleware)
1821 .map_err(|err| InitFailure::capture(&err));
1822 *guard = Some(result.clone());
1823 result.map_err(InitFailure::into_error)
1824 }
1825
1826 fn apply_config_flags(&self, matches: &ArgMatches, middleware: &mut Middleware) -> Result<()> {
1827 if let Some(apply_flags) = &self.apply_flags {
1828 apply_flags(matches, middleware)?;
1829 }
1830 Ok(())
1831 }
1832
1833 fn run_pre_run(
1834 &self,
1835 middleware: &mut Middleware,
1836 command_path: &str,
1837 args: &crate::middleware::ValueMap,
1838 ) -> Result<()> {
1839 if let Some(pre_run) = &self.pre_run {
1840 pre_run(middleware, command_path, args)?;
1841 }
1842 Ok(())
1843 }
1844
1845 fn resolve_meta(&self, command_path: &str, meta: CommandMeta) -> CommandMeta {
1846 if let Some(resolver) = &self.meta_resolver {
1847 resolver(command_path, meta)
1848 } else {
1849 meta
1850 }
1851 }
1852
1853 fn finish_run(&self, output: CliRunOutput) -> CliRunOutput {
1854 crate::config::clear_credential_store_flag();
1857 if let Some(on_shutdown) = &self.on_shutdown {
1858 on_shutdown();
1859 }
1860 output
1861 }
1862}
1863
1864fn apply_global_flags(middleware: &mut Middleware, flags: &GlobalFlags, timeout: Option<Duration>) {
1865 middleware.output_format = flags.output_format.clone();
1866 middleware.verbose = flags.verbose.clone();
1867 middleware.dry_run = flags.dry_run;
1868 middleware.fields = flags.fields.clone();
1869 middleware.filter = flags.filter.clone();
1870 middleware.expr = flags.expr.clone();
1871 middleware.limit = flags.limit;
1872 middleware.offset = flags.offset;
1873 middleware.reason = flags.reason.clone();
1874 middleware.schema = flags.schema;
1875 middleware.timeout = timeout;
1876 middleware.debug = flags.debug.clone();
1877 middleware.search = flags.search.clone();
1878}
1879
1880async fn run_with_timeout<F, T>(
1881 timeout: Option<Duration>,
1882 timeout_label: &str,
1883 future: F,
1884) -> Result<T>
1885where
1886 F: Future<Output = Result<T>>,
1887{
1888 let Some(timeout) = timeout else {
1889 return future.await;
1890 };
1891 match tokio::time::timeout(timeout, future).await {
1892 Ok(result) => result,
1893 Err(_) => Err(CliCoreError::message(format!(
1894 "command timed out after {timeout_label}"
1895 ))),
1896 }
1897}
1898
1899async fn run_until_signal<Run, Shutdown>(run: Run, shutdown: Shutdown) -> CliRunOutput
1900where
1901 Run: Future<Output = CliRunOutput>,
1902 Shutdown: Future<Output = ()>,
1903{
1904 tokio::pin!(run);
1905 tokio::pin!(shutdown);
1906 tokio::select! {
1907 output = &mut run => output,
1908 () = &mut shutdown => CliRunOutput {
1909 exit_code: 130,
1910 rendered: "command interrupted\n".to_owned(),
1911 },
1912 }
1913}
1914
1915#[cfg(unix)]
1916async fn shutdown_signal() {
1917 let ctrl_c = tokio::signal::ctrl_c();
1918 match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
1919 Ok(mut sigterm) => {
1920 tokio::select! {
1921 _ = ctrl_c => {},
1922 _ = sigterm.recv() => {},
1923 }
1924 }
1925 Err(_) => {
1926 drop(ctrl_c.await);
1927 }
1928 }
1929}
1930
1931#[cfg(not(unix))]
1932async fn shutdown_signal() {
1933 drop(tokio::signal::ctrl_c().await);
1934}
1935
1936fn parse_command_timeout(raw: &str) -> Result<Option<Duration>> {
1937 let raw = raw.trim();
1938 if raw.is_empty() {
1939 return Ok(Some(Duration::from_secs(60)));
1940 }
1941 let Some(seconds) = parse_duration_seconds(raw) else {
1942 return Err(CliCoreError::message(format!(
1943 "invalid timeout {raw:?}: expected duration like 60s, 5m, or 0s"
1944 )));
1945 };
1946 if seconds <= 0.0 {
1947 Ok(None)
1948 } else {
1949 Ok(Some(Duration::from_secs_f64(seconds)))
1950 }
1951}
1952
1953fn parse_duration_seconds(raw: &str) -> Option<f64> {
1954 for (suffix, seconds) in [
1955 ("ns", 0.000_000_001_f64),
1956 ("us", 0.000_001_f64),
1957 ("µs", 0.000_001_f64),
1958 ("ms", 0.001_f64),
1959 ("s", 1.0_f64),
1960 ("m", 60.0_f64),
1961 ("h", 3600.0_f64),
1962 ] {
1963 if let Some(number) = raw.strip_suffix(suffix) {
1964 let value = number.parse::<f64>().ok()?;
1965 if !value.is_finite() {
1966 return None;
1967 }
1968 return Some(value * seconds);
1969 }
1970 }
1971 None
1972}
1973
1974fn render_cli_error(
1975 middleware: &Middleware,
1976 err: &(dyn std::error::Error + 'static),
1977 system: &str,
1978) -> CliRunOutput {
1979 let format = middleware
1980 .output_format
1981 .parse::<crate::output::OutputFormat>()
1982 .unwrap_or(crate::output::OutputFormat::Json);
1983 let envelope =
1984 crate::output::build_error_envelope(err, system).prepare_for_render(&middleware.verbose);
1985 match crate::output::render(format, &envelope) {
1986 Ok(rendered) => CliRunOutput {
1987 exit_code: exit_code_for_error(err),
1988 rendered,
1989 },
1990 Err(render_err) => CliRunOutput {
1991 exit_code: exit_code_for_error(err),
1992 rendered: render_err.to_string(),
1993 },
1994 }
1995}
1996
1997fn find_command_by_colon_path<'command>(
1998 root: &'command Command,
1999 path: &str,
2000) -> Option<&'command Command> {
2001 find_command_and_canonical_path_by_colon_path(root, path).map(|(command, _)| command)
2002}
2003
2004fn find_help_target<'command>(
2005 root: &'command Command,
2006 parts: &[&str],
2007) -> Option<&'command Command> {
2008 let mut current = root;
2009 let mut matched_any = false;
2010 for part in parts {
2011 let Some(next) = current.find_subcommand(part) else {
2012 break;
2013 };
2014 current = next;
2015 matched_any = true;
2016 }
2017 matched_any.then_some(current)
2018}
2019
2020fn find_command_and_canonical_path_by_colon_path<'command>(
2021 root: &'command Command,
2022 path: &str,
2023) -> Option<(&'command Command, Vec<String>)> {
2024 if path.is_empty() {
2025 return Some((root, Vec::new()));
2026 }
2027 let mut current = root;
2028 let mut canonical = Vec::new();
2029 for part in path.split(':') {
2030 current = current.find_subcommand(part)?;
2031 canonical.push(current.get_name().to_owned());
2032 }
2033 Some((current, canonical))
2034}
2035
2036fn canonical_path_from_parts(root: &Command, parts: &[String]) -> Option<String> {
2037 if parts.is_empty() {
2038 return Some(String::new());
2039 }
2040 let mut current = root;
2041 let mut canonical = Vec::new();
2042 for part in parts {
2043 current = current.find_subcommand(part)?;
2044 canonical.push(current.get_name().to_owned());
2045 }
2046 Some(canonical.join(":"))
2047}
2048
2049fn extract_search_scope_parts(args: &[String]) -> Vec<String> {
2050 let mut parts = Vec::new();
2051 let mut index = 1;
2052 while index < args.len() {
2053 let arg = &args[index];
2054 if arg == "--search" || arg.starts_with("--search=") {
2055 break;
2056 }
2057 if arg.starts_with('-') {
2058 if !arg.contains('=') && index + 1 < args.len() && !args[index + 1].starts_with('-') {
2059 index += 2;
2060 } else {
2061 index += 1;
2062 }
2063 continue;
2064 }
2065 parts.push(arg.clone());
2066 index += 1;
2067 }
2068 parts
2069}
2070
2071fn collect_command_search_documents(
2072 command: &Command,
2073 prefix: &mut Vec<String>,
2074 aliases: &mut Vec<String>,
2075 docs: &mut Vec<SearchDocument>,
2076) {
2077 if command.is_hide_set() || command.get_name() == "completion" {
2078 return;
2079 }
2080 if command.get_subcommands().next().is_some() {
2081 for child in command.get_subcommands() {
2082 prefix.push(child.get_name().to_owned());
2083 let alias_len = aliases.len();
2084 append_command_alias_terms(child, aliases);
2085 collect_command_search_documents(child, prefix, aliases, docs);
2086 aliases.truncate(alias_len);
2087 prefix.pop();
2088 }
2089 return;
2090 }
2091 if prefix.is_empty() {
2092 prefix.push(command.get_name().to_owned());
2093 append_command_alias_terms(command, aliases);
2094 }
2095 let path = prefix.join(" ");
2096 let alias_text = aliases.join(" ");
2097 docs.push(SearchDocument {
2098 id: format!("cmd:{path}"),
2099 kind: "command".to_owned(),
2100 title: path,
2101 summary: command
2102 .get_about()
2103 .map(ToString::to_string)
2104 .unwrap_or_default(),
2105 content: format!(
2106 "{} {} {} {}",
2107 command
2108 .get_about()
2109 .map(ToString::to_string)
2110 .unwrap_or_default(),
2111 command
2112 .get_long_about()
2113 .map(ToString::to_string)
2114 .unwrap_or_default(),
2115 command_flag_text(command),
2116 alias_text
2117 ),
2118 });
2119 if prefix.len() == 1 && prefix[0] == command.get_name() {
2120 prefix.pop();
2121 }
2122}
2123
2124fn append_command_alias_terms(command: &Command, aliases: &mut Vec<String>) {
2125 aliases.extend(command.get_all_aliases().map(str::to_owned));
2126 aliases.extend(
2127 command
2128 .get_all_short_flag_aliases()
2129 .map(|alias| alias.to_string()),
2130 );
2131 aliases.extend(command.get_all_long_flag_aliases().map(str::to_owned));
2132}
2133
2134fn command_flag_text(command: &Command) -> String {
2135 command
2136 .get_arguments()
2137 .filter_map(|arg| {
2138 let mut names = Vec::new();
2139 if let Some(short) = arg.get_short() {
2140 names.push(format!("-{short}"));
2141 }
2142 if let Some(long) = arg.get_long() {
2143 names.push(format!("--{long}"));
2144 }
2145 if let Some(short_aliases) = arg.get_all_short_aliases() {
2146 names.extend(
2147 short_aliases
2148 .into_iter()
2149 .map(|short_alias| format!("-{short_alias}")),
2150 );
2151 }
2152 if let Some(aliases) = arg.get_all_aliases() {
2153 names.extend(aliases.into_iter().map(|alias| format!("--{alias}")));
2154 }
2155 (!names.is_empty()).then(|| names.join(" "))
2156 })
2157 .collect::<Vec<_>>()
2158 .join(" ")
2159}
2160
2161fn has_subcommand(command: &Command, name: &str) -> bool {
2162 command
2163 .get_subcommands()
2164 .any(|child| child.get_name() == name)
2165}
2166
2167fn has_root_version_flag(args: &[String], root: &Command, root_name: &str) -> bool {
2168 let bool_flags = derive_bool_flags(root);
2169 let value_flags = derive_value_flags(root);
2170 let mut iter = args.iter().peekable();
2171 if iter
2172 .peek()
2173 .is_some_and(|arg| arg_matches_root_name(arg, root_name))
2174 {
2175 iter.next();
2176 }
2177
2178 while let Some(arg) = iter.next() {
2179 match arg.as_str() {
2180 "--version" | "-v" => return true,
2181 "--" => return false,
2182 value if value.contains('=') || bool_flags.contains(value) => continue,
2183 value
2184 if value_flags.contains(value)
2185 || unknown_flag_consumes_value(value, iter.peek()) =>
2186 {
2187 iter.next();
2188 }
2189 value if value.starts_with('-') => {}
2190 _ => return false,
2191 }
2192 }
2193 false
2194}
2195
2196fn normalize_optional_global_flags_before_command(root: &Command, args: &[String]) -> Vec<String> {
2197 let optional_string_defaults = BTreeMap::from([("--verbose", "all"), ("--debug", "*")]);
2198 let optional_bool_defaults = BTreeMap::from([("--dry-run", "true"), ("--schema", "true")]);
2199 let mut normalized = Vec::with_capacity(args.len());
2200 let mut index = 0;
2201 let mut current = root;
2202 while index < args.len() {
2203 let arg = &args[index];
2204 if index == 0 && arg_matches_root_name(arg, root.get_name()) {
2205 normalized.push(arg.clone());
2206 index += 1;
2207 continue;
2208 }
2209
2210 if let Some(default) = optional_bool_defaults.get(arg.as_str()) {
2211 normalized.push(format!("{arg}={default}"));
2212 index += 1;
2213 continue;
2214 }
2215
2216 if let Some(default) = optional_string_defaults.get(arg.as_str()) {
2217 match args.get(index + 1) {
2218 None => {
2219 normalized.push(format!("{arg}={default}"));
2220 index += 1;
2221 continue;
2222 }
2223 Some(next)
2224 if current.get_name() == root.get_name()
2225 || next.starts_with('-')
2226 || direct_subcommand(current, next).is_some() =>
2227 {
2228 normalized.push(format!("{arg}={default}"));
2229 index += 1;
2230 continue;
2231 }
2232 Some(next) => {
2233 normalized.push(arg.clone());
2234 normalized.push(next.clone());
2235 index += 2;
2236 continue;
2237 }
2238 }
2239 }
2240
2241 normalized.push(arg.clone());
2242 if !arg.starts_with('-')
2243 && let Some(next_command) = direct_subcommand(current, arg)
2244 {
2245 current = next_command;
2246 }
2247 index += 1;
2248 }
2249 normalized
2250}
2251
2252fn direct_subcommand<'command>(
2253 command: &'command Command,
2254 token: &str,
2255) -> Option<&'command Command> {
2256 command.get_subcommands().find(|child| {
2257 child.get_name() == token || child.get_all_aliases().any(|alias| alias == token)
2258 })
2259}
2260
2261fn unknown_group_command_message(root: &Command, positionals: &[String]) -> Option<String> {
2262 if positionals.is_empty() {
2263 return None;
2264 }
2265
2266 let mut current = root;
2267 let mut path = vec![root.get_name().to_owned()];
2268 for token in positionals {
2269 if let Some(next) = current.find_subcommand(token) {
2270 current = next;
2271 path.push(next.get_name().to_owned());
2272 continue;
2273 }
2274 if current.get_subcommands().next().is_some() {
2275 return Some(format!(
2276 "unknown command {token:?} for {:?}",
2277 path.join(" ")
2278 ));
2279 }
2280 return None;
2281 }
2282 None
2283}
2284
2285fn group_help_target_parts(
2308 root: &Command,
2309 positionals: &[String],
2310 command_keyword_count: usize,
2311) -> Option<Vec<String>> {
2312 let help_index = positionals.iter().position(|token| token == "help")?;
2313 if help_index == 0 {
2315 return None;
2316 }
2317 if help_index >= command_keyword_count {
2319 return None;
2320 }
2321 let prefix = &positionals[..help_index];
2322 let mut current = root;
2323 for token in prefix {
2324 current = current.find_subcommand(token)?;
2325 }
2326 current.get_subcommands().next()?;
2328 if current.find_subcommand("help").is_some() {
2330 return None;
2331 }
2332 let suffix = &positionals[help_index + 1..];
2334 Some(prefix.iter().chain(suffix).cloned().collect())
2335}
2336
2337fn rewrite_group_help_args(
2348 clap_args: &[String],
2349 root_name: &str,
2350 bool_flags: &BTreeSet<String>,
2351 value_flags: &BTreeSet<String>,
2352 parts: &[String],
2353) -> Vec<String> {
2354 let mut next_positional = std::iter::once("help".to_owned())
2356 .chain(parts.iter().cloned())
2357 .peekable();
2358 let mut out = Vec::with_capacity(clap_args.len());
2359 let mut iter = clap_args.iter().peekable();
2360 if iter
2361 .peek()
2362 .is_some_and(|arg| arg_matches_root_name(arg, root_name))
2363 && let Some(program) = iter.next()
2364 {
2365 out.push(program.clone());
2366 }
2367
2368 let mut take_positional =
2369 |fallback: &String| next_positional.next().unwrap_or(fallback.clone());
2370
2371 while let Some(arg) = iter.next() {
2372 if arg == "--" {
2373 out.push(arg.clone());
2374 for rest in iter.by_ref() {
2376 out.push(take_positional(rest));
2377 }
2378 break;
2379 }
2380 if arg.contains('=') || bool_flags.contains(arg) {
2381 out.push(arg.clone());
2382 continue;
2383 }
2384 if value_flags.contains(arg) || unknown_flag_consumes_value(arg, iter.peek()) {
2385 out.push(arg.clone());
2386 if let Some(value) = iter.next() {
2387 out.push(value.clone());
2388 }
2389 continue;
2390 }
2391 if arg.starts_with('-') {
2392 out.push(arg.clone());
2393 continue;
2394 }
2395 out.push(take_positional(arg));
2396 }
2397 out.extend(next_positional);
2399 out
2400}
2401
2402fn positional_command_tokens(
2403 args: &[String],
2404 root_name: &str,
2405 bool_flags: &BTreeSet<String>,
2406 value_flags: &BTreeSet<String>,
2407) -> Vec<String> {
2408 let mut tokens = Vec::new();
2409 let mut iter = args.iter().peekable();
2410 if iter
2411 .peek()
2412 .is_some_and(|arg| arg_matches_root_name(arg, root_name))
2413 {
2414 iter.next();
2415 }
2416
2417 while let Some(arg) = iter.next() {
2418 if arg == "--" {
2419 tokens.extend(iter.cloned());
2420 break;
2421 }
2422 if arg.contains('=') {
2423 continue;
2424 }
2425 if bool_flags.contains(arg) {
2426 continue;
2427 }
2428 if value_flags.contains(arg) || unknown_flag_consumes_value(arg, iter.peek()) {
2429 iter.next();
2430 continue;
2431 }
2432 if arg.starts_with('-') {
2433 continue;
2434 }
2435 tokens.push(arg.clone());
2436 }
2437 tokens
2438}
2439
2440fn unknown_flag_consumes_value(arg: &str, next: Option<&&String>) -> bool {
2441 arg.starts_with('-') && next.is_some_and(|value| !value.starts_with('-'))
2442}
2443
2444fn arg_matches_root_name(arg: &str, root_name: &str) -> bool {
2445 arg == root_name
2446 || Path::new(arg)
2447 .file_stem()
2448 .and_then(|n| n.to_str())
2449 .is_some_and(|n| n == root_name)
2450}
2451
2452enum Argv0Outcome {
2455 Proceed(Vec<String>),
2457 Handled(CliRunOutput),
2459}
2460
2461fn program_basename(arg: &str) -> String {
2465 Path::new(arg)
2466 .file_stem()
2467 .and_then(|stem| stem.to_str())
2468 .map_or_else(|| arg.to_owned(), ToOwned::to_owned)
2469}
2470
2471fn is_valid_argv0_name(name: &str) -> bool {
2476 !name.is_empty()
2477 && name.chars().all(|character| {
2478 character.is_ascii_alphanumeric() || character == '-' || character == '_'
2479 })
2480}
2481
2482fn argv0_link_matches(
2487 link: &Path,
2488 target: &Path,
2489 name: &str,
2490 method: Argv0LinkMethod,
2491) -> std::io::Result<bool> {
2492 let metadata = std::fs::symlink_metadata(link)?;
2493 match method {
2494 Argv0LinkMethod::SoftLink => {
2495 Ok(metadata.file_type().is_symlink() && std::fs::read_link(link)? == target)
2496 }
2497 Argv0LinkMethod::HardLink => {
2498 if metadata.file_type().is_symlink() {
2499 return Ok(false);
2500 }
2501 Ok(std::fs::read(link)? == std::fs::read(target)?)
2504 }
2505 Argv0LinkMethod::Script => {
2506 if metadata.file_type().is_symlink() {
2507 return Ok(false);
2508 }
2509 Ok(std::fs::read_to_string(link).ok() == Some(argv0_script_contents(target, name)))
2510 }
2511 }
2512}
2513
2514fn argv0_link_file_name(name: &str, method: Argv0LinkMethod) -> String {
2516 let extension = match method {
2517 Argv0LinkMethod::Script if cfg!(windows) => ".cmd",
2518 Argv0LinkMethod::Script => "",
2520 _ if cfg!(windows) => ".exe",
2521 _ => "",
2522 };
2523 format!("{name}{extension}")
2524}
2525
2526fn argv0_script_contents(target: &Path, name: &str) -> String {
2530 let target = target.display();
2531 if cfg!(windows) {
2532 format!("@\"{target}\" argv0 {name} %*\r\n")
2533 } else {
2534 format!("#!/bin/sh\nexec \"{target}\" argv0 {name} \"$@\"\n")
2535 }
2536}
2537
2538#[cfg(unix)]
2539fn create_symlink(target: &Path, link: &Path) -> std::io::Result<()> {
2540 std::os::unix::fs::symlink(target, link)
2541}
2542
2543#[cfg(windows)]
2544fn create_symlink(target: &Path, link: &Path) -> std::io::Result<()> {
2545 std::os::windows::fs::symlink_file(target, link)
2546}
2547
2548#[cfg(not(any(unix, windows)))]
2549fn create_symlink(_target: &Path, _link: &Path) -> std::io::Result<()> {
2550 Err(std::io::Error::new(
2551 std::io::ErrorKind::Unsupported,
2552 "symlink creation is not supported on this platform",
2553 ))
2554}
2555
2556#[cfg(unix)]
2558fn make_executable(path: &Path) -> std::io::Result<()> {
2559 use std::os::unix::fs::PermissionsExt;
2560 let mut permissions = std::fs::metadata(path)?.permissions();
2561 permissions.set_mode(0o755);
2562 std::fs::set_permissions(path, permissions)
2563}
2564
2565#[cfg(not(unix))]
2566fn make_executable(_path: &Path) -> std::io::Result<()> {
2567 Ok(())
2568}
2569
2570fn register_runtime_group_schemas(
2571 group: &RuntimeGroupSpec,
2572 prefix: &mut Vec<String>,
2573 schemas: &mut SchemaRegistry,
2574) {
2575 prefix.push(group.group.name.clone());
2576 for child_group in &group.groups {
2577 register_runtime_group_schemas(child_group, prefix, schemas);
2578 }
2579 for child in &group.commands {
2580 prefix.push(child.spec.name.clone());
2581 register_command_schema(&child.spec, &prefix.join(":"), schemas);
2582 prefix.pop();
2583 }
2584 prefix.pop();
2585}
2586
2587fn register_command_schema(spec: &CommandSpec, command_path: &str, schemas: &mut SchemaRegistry) {
2588 if let Some(schema) = &spec.output_schema {
2589 schemas.register_info(command_path.to_owned(), schema.clone());
2590 }
2591}
2592
2593fn runtime_group_clap_command_with_schema_help(
2594 group: &RuntimeGroupSpec,
2595 prefix: &mut Vec<String>,
2596 schemas: &SchemaRegistry,
2597) -> Command {
2598 let mut command = group_clap_command_without_children(&group.group);
2599 prefix.push(group.group.name.clone());
2600 for child_group in &group.groups {
2601 command = command.subcommand(runtime_group_clap_command_with_schema_help(
2602 child_group,
2603 prefix,
2604 schemas,
2605 ));
2606 }
2607 for child in &group.commands {
2608 prefix.push(child.spec.name.clone());
2609 let command_path = prefix.join(":");
2610 command = command.subcommand(command_clap_command_with_schema_help(
2611 &child.spec,
2612 &command_path,
2613 schemas,
2614 ));
2615 prefix.pop();
2616 }
2617 prefix.pop();
2618 command
2619}
2620
2621fn group_clap_command_without_children(group: &GroupSpec) -> Command {
2622 let mut command = Command::new(group.name.clone())
2623 .about(group.short.clone())
2624 .help_template(GROUP_HELP_TEMPLATE);
2625 if let Some(long) = &group.long
2626 && !long.is_empty()
2627 {
2628 command = command.long_about(long.clone());
2629 }
2630 for alias in &group.aliases {
2631 command = command.alias(alias.clone());
2632 }
2633 if group.hidden {
2634 command = command.hide(true);
2635 }
2636 command
2637}
2638
2639fn command_clap_command_with_schema_help(
2640 spec: &CommandSpec,
2641 command_path: &str,
2642 schemas: &SchemaRegistry,
2643) -> Command {
2644 let mut command = spec.clap_command();
2645 let Some(schema) = schemas.get_by_path(command_path) else {
2646 return command;
2647 };
2648 let schema_help = format_help_section(&schema.fields);
2649 if schema_help.is_empty() {
2650 return command;
2651 }
2652 let base = spec
2653 .long
2654 .as_ref()
2655 .filter(|long| !long.is_empty())
2656 .cloned()
2657 .unwrap_or_else(|| spec.short.clone());
2658 let long = if base.is_empty() {
2659 schema_help
2660 } else {
2661 format!("{base}\n\n{schema_help}")
2662 };
2663 command = command.long_about(long);
2664 command
2665}
2666
2667fn process_exit_code(code: i32) -> ExitCode {
2668 if code == 0 {
2669 return ExitCode::SUCCESS;
2670 }
2671 match u8::try_from(code) {
2672 Ok(code) if code != 0 => ExitCode::from(code),
2673 Ok(_) | Err(_) => ExitCode::from(1),
2674 }
2675}
2676
2677async fn run_streaming_command(
2678 middleware: &Middleware,
2679 request: MiddlewareRequest<'_>,
2680 raw_matches: Arc<ArgMatches>,
2681 streaming_handler: crate::command::StreamingCommandHandler,
2682) -> Result<CliRunOutput> {
2683 use tokio::{io::AsyncWriteExt, sync::mpsc};
2684
2685 let args_for_handler = request.args.clone();
2686 let user_args_for_handler = request.user_args.clone();
2687 let handler_path = request.command_path.to_owned();
2688 let middleware_for_handler = middleware.clone();
2689 let raw_matches_for_handler = raw_matches;
2690
2691 let (tx, mut rx) = mpsc::channel::<serde_json::Value>(64);
2692 let sender = StreamSender(tx);
2693
2694 let writer = tokio::spawn(async move {
2698 let mut stdout = tokio::io::stdout();
2699 while let Some(event) = rx.recv().await {
2700 let Ok(line) = serde_json::to_string(&event) else {
2701 continue;
2702 };
2703 if stdout.write_all(line.as_bytes()).await.is_err()
2704 || stdout.write_all(b"\n").await.is_err()
2705 || stdout.flush().await.is_err()
2706 {
2707 break;
2708 }
2709 }
2710 });
2711
2712 let output = middleware
2713 .run(request, async move |credential| {
2714 streaming_handler(
2715 CommandContext {
2716 credential,
2717 args: args_for_handler,
2718 user_args: user_args_for_handler,
2719 command_path: handler_path,
2720 middleware: middleware_for_handler,
2721 raw_matches: raw_matches_for_handler,
2722 },
2723 sender,
2724 )
2725 .await?;
2726 Ok(crate::CommandResult::new(serde_json::Value::Null))
2727 })
2728 .await;
2729
2730 let _write_result = writer.await;
2733
2734 match output {
2735 Ok(out) if out.exit_code == 0 => Ok(CliRunOutput {
2736 exit_code: 0,
2737 rendered: String::new(),
2738 }),
2739 Ok(out) => Ok(out.into()),
2740 Err(err) => Ok(CliRunOutput {
2741 exit_code: exit_code_for_error(&err),
2742 rendered: render_cli_error(middleware, &err, middleware.app_id.as_str()).rendered,
2743 }),
2744 }
2745}