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 argv0_routes: BTreeMap<String, Argv0Route>,
248}
249
250impl CliConfig {
251 #[must_use]
253 pub fn new(
254 name: impl Into<String>,
255 short: impl Into<String>,
256 app_id: impl Into<String>,
257 ) -> Self {
258 Self {
259 name: name.into(),
260 short: short.into(),
261 app_id: app_id.into(),
262 ..Self::default()
263 }
264 }
265
266 #[must_use]
268 pub fn with_long(mut self, long: impl Into<String>) -> Self {
269 self.long = Some(long.into());
270 self
271 }
272
273 #[must_use]
275 pub fn with_build(mut self, build: BuildInfo) -> Self {
276 self.build = build;
277 self
278 }
279
280 #[must_use]
282 pub fn with_default_auth_provider(mut self, provider: impl Into<String>) -> Self {
283 self.default_auth_provider = Some(provider.into());
284 self
285 }
286
287 #[must_use]
289 pub fn with_module(mut self, module: Module) -> Self {
290 self.modules.push(module);
291 self
292 }
293
294 #[must_use]
296 pub fn with_modules(mut self, modules: impl IntoIterator<Item = Module>) -> Self {
297 self.modules.extend(modules);
298 self
299 }
300
301 #[must_use]
303 pub fn with_command(mut self, command: RuntimeCommandSpec) -> Self {
304 self.commands.push(command);
305 self
306 }
307
308 #[must_use]
310 pub fn with_guide(mut self, guide: GuideEntry) -> Self {
311 self.guides.push(guide);
312 self
313 }
314
315 #[must_use]
317 pub fn with_guides(mut self, guides: impl IntoIterator<Item = GuideEntry>) -> Self {
318 self.guides.extend(guides);
319 self
320 }
321
322 #[must_use]
324 pub fn with_view(mut self, view: HumanViewDef) -> Self {
325 self.views.push(view);
326 self
327 }
328
329 #[must_use]
331 pub fn with_auth_provider(mut self, provider: Arc<dyn AuthProvider>) -> Self {
332 self.auth_providers.push(provider);
333 self
334 }
335
336 #[must_use]
338 pub fn with_authz(mut self, authz: Arc<dyn Authorizer>) -> Self {
339 self.authz = Some(authz);
340 self
341 }
342
343 #[must_use]
345 pub fn with_auditor(mut self, auditor: Arc<dyn Auditor>) -> Self {
346 self.auditor = Some(auditor);
347 self
348 }
349
350 #[must_use]
352 pub fn with_activity(mut self, activity: Arc<dyn ActivityEmitter>) -> Self {
353 self.activity = Some(activity);
354 self
355 }
356
357 #[must_use]
359 pub fn with_init_deps(mut self, init_deps: InitDeps) -> Self {
360 self.init_deps = Some(init_deps);
361 self
362 }
363
364 #[must_use]
366 pub fn with_register_flags(mut self, register_flags: RegisterFlags) -> Self {
367 self.register_flags = Some(register_flags);
368 self
369 }
370
371 #[must_use]
373 pub fn with_apply_flags(mut self, apply_flags: ApplyFlags) -> Self {
374 self.apply_flags = Some(apply_flags);
375 self
376 }
377
378 #[must_use]
380 pub fn with_pre_run(mut self, pre_run: PreRun) -> Self {
381 self.pre_run = Some(pre_run);
382 self
383 }
384
385 #[must_use]
387 pub fn with_meta_resolver(mut self, meta_resolver: ResolveMeta) -> Self {
388 self.meta_resolver = Some(meta_resolver);
389 self
390 }
391
392 #[must_use]
394 pub fn with_on_shutdown(mut self, on_shutdown: OnShutdown) -> Self {
395 self.on_shutdown = Some(on_shutdown);
396 self
397 }
398
399 #[must_use]
401 pub fn with_extra_search_docs(mut self, extra_search_docs: ExtraSearchDocs) -> Self {
402 self.extra_search_docs = Some(extra_search_docs);
403 self
404 }
405
406 #[must_use]
408 pub fn with_root_next_actions(mut self, root_next_actions: RootNextActions) -> Self {
409 self.root_next_actions = Some(root_next_actions);
410 self
411 }
412
413 #[must_use]
417 pub fn with_admin_category(mut self, category: impl Into<String>) -> Self {
418 self.admin_category = Some(category.into());
419 self
420 }
421
422 #[must_use]
445 pub fn with_argv0_alias(
446 mut self,
447 name: impl Into<String>,
448 command_path: impl IntoIterator<Item = impl Into<String>>,
449 ) -> Self {
450 let name = name.into();
451 debug_assert!(
452 is_valid_argv0_name(&name),
453 "argv0 route name {name:?} must be non-empty and contain only ASCII letters, digits, '-', or '_'"
454 );
455 debug_assert!(
456 name != self.name,
457 "argv0 route name {name:?} must differ from the CLI's own name {:?}",
458 self.name
459 );
460 let tokens = command_path.into_iter().map(Into::into).collect();
461 self.argv0_routes.insert(name, Argv0Route::Alias(tokens));
462 self
463 }
464
465 #[must_use]
487 pub fn with_argv0_personality(
488 mut self,
489 name: impl Into<String>,
490 build: impl Fn() -> CliConfig + Send + Sync + 'static,
491 ) -> Self {
492 let name = name.into();
493 debug_assert!(
494 is_valid_argv0_name(&name),
495 "argv0 route name {name:?} must be non-empty and contain only ASCII letters, digits, '-', or '_'"
496 );
497 debug_assert!(
498 name != self.name,
499 "argv0 route name {name:?} must differ from the CLI's own name {:?}",
500 self.name
501 );
502 self.argv0_routes
503 .insert(name, Argv0Route::Personality(Arc::new(build)));
504 self
505 }
506}
507
508impl std::fmt::Debug for CliConfig {
509 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
510 formatter
511 .debug_struct("CliConfig")
512 .field("name", &self.name)
513 .field("short", &self.short)
514 .field("long", &self.long)
515 .field("build", &self.build)
516 .field("app_id", &self.app_id)
517 .field("default_auth_provider", &self.default_auth_provider)
518 .field("modules", &self.modules)
519 .field("commands", &self.commands)
520 .field("guides", &self.guides)
521 .field("views", &self.views)
522 .field("auth_providers_len", &self.auth_providers.len())
523 .field("has_authz", &self.authz.is_some())
524 .field("has_auditor", &self.auditor.is_some())
525 .field("has_activity", &self.activity.is_some())
526 .field("has_init_deps", &self.init_deps.is_some())
527 .field("has_register_flags", &self.register_flags.is_some())
528 .field("has_apply_flags", &self.apply_flags.is_some())
529 .field("has_pre_run", &self.pre_run.is_some())
530 .field("has_meta_resolver", &self.meta_resolver.is_some())
531 .field("has_on_shutdown", &self.on_shutdown.is_some())
532 .field("has_extra_search_docs", &self.extra_search_docs.is_some())
533 .field("has_root_next_actions", &self.root_next_actions.is_some())
534 .field("admin_category", &self.admin_category)
535 .field(
536 "argv0_routes",
537 &self.argv0_routes.keys().collect::<Vec<_>>(),
538 )
539 .finish()
540 }
541}
542
543#[derive(Clone, Debug, PartialEq)]
545pub struct CliRunOutput {
546 pub exit_code: i32,
548 pub rendered: String,
550}
551
552impl From<crate::middleware::MiddlewareOutput> for CliRunOutput {
553 fn from(o: crate::middleware::MiddlewareOutput) -> Self {
554 Self {
555 exit_code: o.exit_code,
556 rendered: o.rendered,
557 }
558 }
559}
560
561#[derive(Clone)]
567pub struct Cli {
568 config: CliConfig,
569 middleware: Middleware,
570 root: Command,
571 commands: BTreeMap<String, RuntimeCommandSpec>,
572 module_entries: Vec<ModuleHelpEntry>,
573 guide_entries: Vec<GuideEntry>,
574 init_deps: Option<InitDeps>,
575 apply_flags: Option<ApplyFlags>,
576 pre_run: Option<PreRun>,
577 meta_resolver: Option<ResolveMeta>,
578 on_shutdown: Option<OnShutdown>,
579 extra_search_docs: Option<ExtraSearchDocs>,
580 root_next_actions: Option<RootNextActions>,
581 init_state: Arc<Mutex<Option<std::result::Result<Middleware, InitFailure>>>>,
582}
583
584#[derive(Clone, Debug, Eq, PartialEq)]
585struct InitFailure {
586 message: String,
587 code: String,
588 system: String,
589 request_id: String,
590 exit_code: i32,
591}
592
593impl InitFailure {
594 fn capture(err: &CliCoreError) -> Self {
595 let envelope = crate::output::build_error_envelope(err, "");
596 let (code, system, request_id) = envelope.error.map_or_else(
597 || ("ERROR".to_owned(), String::new(), String::new()),
598 |error| (error.code, error.system, error.request_id),
599 );
600 Self {
601 message: err.to_string(),
602 code,
603 system,
604 request_id,
605 exit_code: exit_code_for_error(err),
606 }
607 }
608
609 fn into_error(self) -> CliCoreError {
610 CliCoreError::with_exit_code(
611 self.exit_code,
612 CliCoreError::SystemMessage {
613 message: self.message,
614 system: self.system,
615 code: self.code,
616 request_id: self.request_id,
617 },
618 )
619 }
620}
621
622impl std::fmt::Debug for Cli {
623 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
624 formatter
625 .debug_struct("Cli")
626 .field("config", &self.config)
627 .field("middleware", &self.middleware)
628 .field("root", &self.root)
629 .field("commands", &self.commands)
630 .field("module_entries", &self.module_entries)
631 .field("guide_entries", &self.guide_entries)
632 .field("has_init_deps", &self.init_deps.is_some())
633 .field("has_apply_flags", &self.apply_flags.is_some())
634 .field("has_pre_run", &self.pre_run.is_some())
635 .field("has_meta_resolver", &self.meta_resolver.is_some())
636 .field("has_on_shutdown", &self.on_shutdown.is_some())
637 .field("has_extra_search_docs", &self.extra_search_docs.is_some())
638 .field("has_root_next_actions", &self.root_next_actions.is_some())
639 .finish()
640 }
641}
642
643impl Cli {
644 #[must_use]
646 pub fn new(config: CliConfig) -> Self {
647 let auth_providers = config.auth_providers.clone();
648 let guides = config.guides.clone();
649 let views = config.views.clone();
650 let modules = config.modules.clone();
651 let commands = config.commands.clone();
652 let init_deps = config.init_deps.clone();
653 let apply_flags = config.apply_flags.clone();
654 let pre_run = config.pre_run.clone();
655 let meta_resolver = config.meta_resolver.clone();
656 let on_shutdown = config.on_shutdown.clone();
657 let extra_search_docs = config.extra_search_docs.clone();
658 let root_next_actions = config.root_next_actions.clone();
659 let mut root = Command::new(config.name.clone())
660 .about(config.short.clone())
661 .disable_help_subcommand(true)
662 .version(config.build.version_string());
663 if let Some(long) = &config.long
664 && !long.is_empty()
665 {
666 root = root.long_about(long.clone());
667 }
668 root = register_global_flags(root)
669 .subcommand(help_command())
670 .subcommand(guide_command())
671 .subcommand(Command::new("tree").about("Display full command tree"));
672 if let Some(register_flags) = &config.register_flags {
673 root = register_flags(root);
674 }
675 let intro = config
676 .long
677 .as_deref()
678 .filter(|long| !long.is_empty())
679 .unwrap_or(config.short.as_str());
680 root = root
681 .long_about(build_root_long(intro, &[], false))
682 .help_template(ROOT_HELP_TEMPLATE);
683
684 let mut middleware = Middleware::new();
685 middleware.app_id = config.app_id.clone();
686 middleware.default_auth_provider = config.default_auth_provider.clone().unwrap_or_default();
687 middleware.authz = config.authz.clone();
688 middleware.auditor = config.auditor.clone();
689 middleware.activity = config.activity.clone();
690 middleware
691 .schema_registry
692 .merge(&global_schema_registry_snapshot());
693 middleware
694 .human_views
695 .merge(&global_human_view_registry_snapshot());
696
697 let mut cli = Self {
698 config,
699 middleware,
700 root,
701 commands: BTreeMap::new(),
702 module_entries: Vec::new(),
703 guide_entries: Vec::new(),
704 init_deps,
705 apply_flags,
706 pre_run,
707 meta_resolver,
708 on_shutdown,
709 extra_search_docs,
710 root_next_actions,
711 init_state: Arc::new(Mutex::new(None)),
712 };
713 for provider in auth_providers {
714 cli.register_auth_provider(provider);
715 }
716 if cli.middleware.default_auth_provider.is_empty()
717 && let Some(provider) = cli.middleware.auth.registered_names().first()
718 {
719 cli.middleware.default_auth_provider = provider.clone();
720 }
721 if !cli.middleware.default_auth_provider.is_empty() {
722 cli.ensure_auth_command();
723 }
724 for view in views {
725 cli.middleware.human_views.register(view);
726 }
727 cli.add_guides(guides);
728 for module in modules {
729 cli.add_module(module);
730 }
731 for command in commands {
732 cli.add_command(command);
733 }
734 cli
735 }
736
737 fn register_auth_help_entry(&mut self) {
742 let category = self
743 .config
744 .admin_category
745 .clone()
746 .unwrap_or_else(|| DEFAULT_ADMIN_CATEGORY.to_owned());
747 let already_listed = self.module_entries.iter().any(|entry| entry.name == "auth");
748 let short = self
749 .root
750 .find_subcommand("auth")
751 .filter(|auth| !auth.is_hide_set())
752 .map(|auth| {
753 auth.get_about()
754 .map(ToString::to_string)
755 .unwrap_or_default()
756 });
757 if !already_listed && let Some(short) = short {
758 self.module_entries.push(ModuleHelpEntry {
759 category,
760 name: "auth".to_owned(),
761 short,
762 });
763 }
764 self.refresh_root_long();
765 }
766
767 #[must_use]
769 pub fn middleware(&self) -> &Middleware {
770 &self.middleware
771 }
772
773 pub fn middleware_mut(&mut self) -> &mut Middleware {
775 &mut self.middleware
776 }
777
778 pub async fn execute(&self) -> ExitCode {
780 let mut stdout = std::io::stdout().lock();
781 let mut stderr = std::io::stderr().lock();
782 match self
783 .execute_from(std::env::args_os(), &mut stdout, &mut stderr)
784 .await
785 {
786 Ok(code) => code,
787 Err(err) => {
788 drop(writeln!(stderr, "{err}"));
789 ExitCode::from(1)
790 }
791 }
792 }
793
794 pub async fn execute_from<I, S, O, E>(
796 &self,
797 args: I,
798 stdout: &mut O,
799 stderr: &mut E,
800 ) -> std::io::Result<ExitCode>
801 where
802 I: IntoIterator<Item = S>,
803 S: Into<std::ffi::OsString> + Clone,
804 O: Write,
805 E: Write,
806 {
807 self.execute_from_until_signal(args, stdout, stderr, shutdown_signal())
808 .await
809 }
810
811 pub async fn execute_from_until_signal<I, S, O, E, Shutdown>(
813 &self,
814 args: I,
815 stdout: &mut O,
816 stderr: &mut E,
817 shutdown: Shutdown,
818 ) -> std::io::Result<ExitCode>
819 where
820 I: IntoIterator<Item = S>,
821 S: Into<std::ffi::OsString> + Clone,
822 O: Write,
823 E: Write,
824 Shutdown: Future<Output = ()>,
825 {
826 let output = run_until_signal(self.run(args), shutdown).await;
827 if output.exit_code == 130
828 && output.rendered == "command interrupted\n"
829 && let Some(on_shutdown) = &self.on_shutdown
830 {
831 on_shutdown();
832 }
833 if output.exit_code == 0 {
834 stdout.write_all(output.rendered.as_bytes())?;
835 } else {
836 stderr.write_all(output.rendered.as_bytes())?;
837 }
838 Ok(process_exit_code(output.exit_code))
839 }
840
841 pub fn register_auth_provider(&mut self, provider: Arc<dyn AuthProvider>) -> &mut Self {
843 self.middleware.auth.register(provider);
844 self.ensure_auth_command();
845 self.refresh_root_long();
846 self
847 }
848
849 #[must_use]
851 pub fn root_command(&self) -> &Command {
852 &self.root
853 }
854
855 pub fn add_module_group(
857 &mut self,
858 category: impl Into<String>,
859 group: RuntimeGroupSpec,
860 ) -> &mut Self {
861 let category = category.into();
862 if !group.group.hidden {
863 self.module_entries.push(ModuleHelpEntry {
864 category,
865 name: group.group.name.clone(),
866 short: group.group.short.clone(),
867 });
868 }
869
870 let mut prefix = Vec::new();
871 register_runtime_group_schemas(&group, &mut prefix, &mut self.middleware.schema_registry);
872 let mut prefix = Vec::new();
873 group.register_commands(&mut prefix, &mut self.commands);
874 let mut prefix = Vec::new();
875 let clap_group = runtime_group_clap_command_with_schema_help(
876 &group,
877 &mut prefix,
878 &self.middleware.schema_registry,
879 );
880 self.root = self.root.clone().subcommand(clap_group);
881 self.refresh_root_long();
882 self
883 }
884
885 pub fn add_module(&mut self, module: Module) -> &mut Self {
887 for view in module.views.clone() {
888 self.middleware.human_views.register(view);
889 }
890 self.add_guides(module.guides.clone());
891 let mut context = ModuleContext::new(&mut self.middleware);
892 let group = (module.register)(&mut context);
893 let (guides, views) = context.into_parts();
894 for view in views {
895 self.middleware.human_views.register(view);
896 }
897 self.add_guides(guides);
898 self.add_module_group(module.category, group)
899 }
900
901 pub fn add_command(&mut self, command: RuntimeCommandSpec) -> &mut Self {
903 let name = command.spec.name.clone();
904 register_command_schema(&command.spec, &name, &mut self.middleware.schema_registry);
905 self.commands.insert(name, command.clone());
906 self.root = self
907 .root
908 .clone()
909 .subcommand(command_clap_command_with_schema_help(
910 &command.spec,
911 &command.spec.name,
912 &self.middleware.schema_registry,
913 ));
914 self
915 }
916
917 pub fn set_has_guide(&mut self, has_guide: bool) -> &mut Self {
919 if has_guide && self.guide_entries.is_empty() && !has_subcommand(&self.root, "guide") {
920 self.root = self.root.clone().subcommand(guide_command());
921 }
922 self.refresh_root_long();
923 self
924 }
925
926 pub fn add_guides(&mut self, entries: impl IntoIterator<Item = GuideEntry>) -> &mut Self {
928 let mut seen = self
929 .guide_entries
930 .iter()
931 .map(|entry| entry.name.clone())
932 .collect::<BTreeSet<_>>();
933 for entry in entries {
934 if seen.insert(entry.name.clone()) {
935 self.guide_entries.push(entry);
936 }
937 }
938 if !self.guide_entries.is_empty() && !has_subcommand(&self.root, "guide") {
939 self.root = self.root.clone().subcommand(guide_command());
940 }
941 self.refresh_root_long();
942 self
943 }
944
945 async fn resolve_argv0(&self, text_args: Vec<String>, depth: usize) -> Argv0Outcome {
954 if self.config.argv0_routes.is_empty() {
955 return Argv0Outcome::Proceed(text_args);
956 }
957
958 if depth > MAX_ARGV0_DEPTH {
959 return Argv0Outcome::Handled(
960 self.render_argv0_error(&text_args, "argv0 dispatch recursion limit exceeded"),
961 );
962 }
963
964 let explicit = text_args.get(1).map(String::as_str) == Some("argv0");
969 let (name, rest) = if explicit {
970 match text_args.get(2) {
971 None => {
972 return Argv0Outcome::Handled(self.render_argv0_error(
973 &text_args,
974 "the argv0 command requires a name to dispatch as",
975 ));
976 }
977 Some(name) => (
981 program_basename(name),
982 text_args
983 .get(3..)
984 .map(<[String]>::to_vec)
985 .unwrap_or_default(),
986 ),
987 }
988 } else {
989 let name = text_args
990 .first()
991 .map(|arg| program_basename(arg))
992 .unwrap_or_default();
993 let rest = text_args
994 .get(1..)
995 .map(<[String]>::to_vec)
996 .unwrap_or_default();
997 (name, rest)
998 };
999
1000 match self.config.argv0_routes.get(&name) {
1001 Some(Argv0Route::Alias(tokens)) => {
1002 let mut rewritten = Vec::with_capacity(1 + tokens.len() + rest.len());
1005 rewritten.push(self.config.name.clone());
1006 rewritten.extend(tokens.iter().cloned());
1007 rewritten.extend(rest);
1008 Argv0Outcome::Proceed(rewritten)
1009 }
1010 Some(Argv0Route::Personality(build)) => {
1011 let config = build();
1016 let bin = config.name.clone();
1017 let alt = Self::new(config);
1018 let mut alt_args = Vec::with_capacity(1 + rest.len());
1019 alt_args.push(bin);
1020 alt_args.extend(rest);
1021 Argv0Outcome::Handled(Box::pin(alt.run_with_depth(alt_args, depth + 1)).await)
1022 }
1023 None if explicit => Argv0Outcome::Handled(self.render_argv0_error(
1024 &text_args,
1025 format!(
1026 "{name:?} is not a registered argv0 name; known names: {}",
1027 self.known_argv0_names()
1028 ),
1029 )),
1030 None => {
1031 let mut rewritten = Vec::with_capacity(1 + rest.len());
1036 rewritten.push(self.config.name.clone());
1037 rewritten.extend(rest);
1038 Argv0Outcome::Proceed(rewritten)
1039 }
1040 }
1041 }
1042
1043 fn known_argv0_names(&self) -> String {
1046 self.config
1047 .argv0_routes
1048 .keys()
1049 .cloned()
1050 .collect::<Vec<_>>()
1051 .join(", ")
1052 }
1053
1054 fn render_argv0_error(&self, text_args: &[String], message: impl Into<String>) -> CliRunOutput {
1059 let mut middleware = self.middleware.clone();
1060 middleware.output_format =
1061 extract_output_format(text_args, &default_output_format(&self.config.app_id));
1062 let err = CliCoreError::message(message);
1063 self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id))
1064 }
1065
1066 #[must_use]
1071 pub fn argv0_names(&self) -> Vec<&str> {
1072 self.config
1073 .argv0_routes
1074 .keys()
1075 .map(String::as_str)
1076 .collect()
1077 }
1078
1079 pub fn create_link(
1104 &self,
1105 name: &str,
1106 dir: impl AsRef<Path>,
1107 target: Option<&Path>,
1108 method: Argv0LinkMethod,
1109 ) -> std::io::Result<PathBuf> {
1110 if !self.config.argv0_routes.contains_key(name) {
1111 return Err(std::io::Error::new(
1112 std::io::ErrorKind::InvalidInput,
1113 format!("{name:?} is not a registered argv0 name"),
1114 ));
1115 }
1116
1117 let dir = dir.as_ref();
1118 std::fs::create_dir_all(dir)?;
1119 let link = dir.join(argv0_link_file_name(name, method));
1120
1121 let resolved_target;
1123 let target = match target {
1124 Some(target) => target,
1125 None => {
1126 resolved_target = std::env::current_exe()?;
1127 resolved_target.as_path()
1128 }
1129 };
1130
1131 if std::fs::symlink_metadata(&link).is_ok() {
1135 if argv0_link_matches(&link, target, name, method)? {
1136 return Ok(link);
1137 }
1138 std::fs::remove_file(&link)?;
1139 }
1140
1141 match method {
1142 Argv0LinkMethod::SoftLink => create_symlink(target, &link)?,
1143 Argv0LinkMethod::HardLink => std::fs::hard_link(target, &link)?,
1144 Argv0LinkMethod::Script => {
1145 std::fs::write(&link, argv0_script_contents(target, name))?;
1146 make_executable(&link)?;
1147 }
1148 }
1149 Ok(link)
1150 }
1151
1152 pub async fn run<I, S>(&self, args: I) -> CliRunOutput
1154 where
1155 I: IntoIterator<Item = S>,
1156 S: Into<std::ffi::OsString> + Clone,
1157 {
1158 self.run_with_depth(args, 0).await
1159 }
1160
1161 async fn run_with_depth<I, S>(&self, args: I, depth: usize) -> CliRunOutput
1164 where
1165 I: IntoIterator<Item = S>,
1166 S: Into<std::ffi::OsString> + Clone,
1167 {
1168 let raw_args = args
1169 .into_iter()
1170 .map(Into::into)
1171 .collect::<Vec<std::ffi::OsString>>();
1172 let text_args = raw_args
1173 .iter()
1174 .map(|arg| arg.to_string_lossy().into_owned())
1175 .collect::<Vec<_>>();
1176 let text_args = match self.resolve_argv0(text_args, depth).await {
1177 Argv0Outcome::Handled(output) => return output,
1178 Argv0Outcome::Proceed(args) => args,
1179 };
1180 let mut clap_args = normalize_optional_global_flags_before_command(&self.root, &text_args);
1181 if has_root_version_flag(&text_args, &self.root, &self.config.name) {
1182 return self.finish_run(CliRunOutput {
1183 exit_code: 0,
1184 rendered: format!(
1185 "{} version {}\n",
1186 self.config.name,
1187 self.config.build.version_string()
1188 ),
1189 });
1190 }
1191 if let Some(output) = self.try_run_schema_bypass(&text_args) {
1192 return output;
1193 }
1194 if let Some(output) = self.try_run_search_bypass(&text_args) {
1195 return output;
1196 }
1197 let bool_flags = derive_bool_flags(&self.root);
1200 let value_flags = derive_value_flags(&self.root);
1201 let positionals =
1202 positional_command_tokens(&text_args, &self.config.name, &bool_flags, &value_flags);
1203 let command_keyword_count = match text_args.iter().position(|arg| arg == "--") {
1208 Some(end) => positional_command_tokens(
1209 &text_args[..end],
1210 &self.config.name,
1211 &bool_flags,
1212 &value_flags,
1213 )
1214 .len(),
1215 None => positionals.len(),
1216 };
1217 if let Some(parts) =
1218 group_help_target_parts(&self.root, &positionals, command_keyword_count)
1219 {
1220 clap_args = rewrite_group_help_args(
1227 &clap_args,
1228 &self.config.name,
1229 &bool_flags,
1230 &value_flags,
1231 &parts,
1232 );
1233 } else if let Some(message) = unknown_group_command_message(&self.root, &positionals) {
1234 return self.finish_run(CliRunOutput {
1235 exit_code: 1,
1236 rendered: message,
1237 });
1238 }
1239
1240 let matches = match self.root.clone().try_get_matches_from(clap_args) {
1241 Ok(matches) => matches,
1242 Err(err) => {
1243 return self.finish_run(CliRunOutput {
1244 exit_code: err.exit_code(),
1245 rendered: err.to_string(),
1246 });
1247 }
1248 };
1249
1250 let default_format = default_output_format(&self.config.app_id);
1251 let flags = global_flags_from_matches(&matches, &default_format);
1252 let command_timeout = match parse_command_timeout(&flags.timeout) {
1253 Ok(timeout) => timeout,
1254 Err(err) => {
1255 return self.finish_run(render_cli_error(
1256 &self.middleware,
1257 &err,
1258 &self.config.app_id,
1259 ));
1260 }
1261 };
1262 let mut middleware = self.middleware.clone();
1263 apply_global_flags(&mut middleware, &flags, command_timeout);
1264 if let Err(err) = self.apply_config_flags(&matches, &mut middleware) {
1265 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
1266 }
1267
1268 let command_path = command_path_from_matches(&self.config.name, &matches);
1269 if command_path == "help" {
1270 if let Err(err) = self.run_pre_run(&mut middleware, &command_path, &help_args(&matches))
1271 {
1272 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
1273 }
1274 return self.finish_run(self.render_help_command(&matches));
1275 }
1276 if command_path == "tree" {
1277 if let Err(err) = self.run_pre_run(
1278 &mut middleware,
1279 &command_path,
1280 &crate::middleware::ValueMap::new(),
1281 ) {
1282 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
1283 }
1284 return self.finish_run(tree_render::render_tree(
1285 &self.root,
1286 &self.config.app_id,
1287 &middleware,
1288 ));
1289 }
1290 if command_path == "guide" {
1291 if let Err(err) =
1292 self.run_pre_run(&mut middleware, &command_path, &guide_args(&matches))
1293 {
1294 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
1295 }
1296 return self.finish_run(self.render_guide(&matches));
1297 }
1298 let Some(command) = self.commands.get(&command_path) else {
1299 if !command_path.is_empty()
1300 && let Some(group) = find_command_by_colon_path(&self.root, &command_path)
1301 && group.get_subcommands().next().is_some()
1302 {
1303 if let Err(err) = self.run_pre_run(
1304 &mut middleware,
1305 &command_path,
1306 &crate::middleware::ValueMap::new(),
1307 ) {
1308 return self.finish_run(render_cli_error(
1309 &middleware,
1310 &err,
1311 &self.config.app_id,
1312 ));
1313 }
1314 return self.finish_run(CliRunOutput {
1315 exit_code: 0,
1316 rendered: group.clone().render_long_help().to_string(),
1317 });
1318 }
1319 if command_path.is_empty()
1320 && let Some(root_next_actions) = &self.root_next_actions
1321 {
1322 let actions = root_next_actions();
1327 return self.finish_run(self.render_root(&middleware, actions));
1328 }
1329 return self.finish_run(CliRunOutput {
1330 exit_code: if command_path.is_empty() { 0 } else { 1 },
1331 rendered: if command_path.is_empty() {
1332 self.root.clone().render_long_help().to_string()
1333 } else {
1334 format!("unknown command {command_path:?}")
1335 },
1336 });
1337 };
1338
1339 let mut middleware = match self.initialized_middleware() {
1340 Ok(middleware) => middleware,
1341 Err(err) => {
1342 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
1343 }
1344 };
1345 apply_global_flags(&mut middleware, &flags, command_timeout);
1346 if let Err(err) = self.apply_config_flags(&matches, &mut middleware) {
1347 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
1348 }
1349
1350 let leaf = leaf_matches(&matches);
1351 let args = command_args_from_matches(leaf, &command.spec, false);
1352 let user_args = command_args_from_matches(leaf, &command.spec, true);
1353 if let Err(err) = self.run_pre_run(&mut middleware, &command_path, &args) {
1354 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
1355 }
1356 let meta = self.resolve_meta(&command_path, command.spec.metadata());
1357 let default_fields = command.spec.default_fields.clone().unwrap_or_default();
1358 let system = command.spec.system.clone().unwrap_or_default();
1359
1360 if let Some(streaming_handler) = command.streaming_handler.clone() {
1361 let result = run_with_timeout(
1362 command_timeout,
1363 &flags.timeout,
1364 run_streaming_command(
1365 &middleware,
1366 MiddlewareRequest {
1367 meta,
1368 command_path: &command_path,
1369 system: &system,
1370 user_args,
1371 args,
1372 default_fields: &default_fields,
1373 auth: command.spec.auth,
1374 },
1375 Arc::new(leaf.clone()),
1376 streaming_handler,
1377 ),
1378 )
1379 .await;
1380 return self.finish_run(match result {
1381 Ok(output) => output,
1382 Err(err) => render_cli_error(&middleware, &err, &self.config.app_id),
1383 });
1384 }
1385
1386 let handler = command.handler.clone();
1387 let args_for_handler = args.clone();
1388 let user_args_for_handler = user_args.clone();
1389 let handler_path = command_path.clone();
1390 let middleware_for_handler = middleware.clone();
1391 let raw_matches_for_handler = Arc::new(leaf.clone());
1392 let result = run_with_timeout(
1393 command_timeout,
1394 &flags.timeout,
1395 middleware.run(
1396 MiddlewareRequest {
1397 meta,
1398 command_path: &command_path,
1399 system: &system,
1400 user_args,
1401 args,
1402 default_fields: &default_fields,
1403 auth: command.spec.auth,
1404 },
1405 async move |credential| {
1406 handler(CommandContext {
1407 credential,
1408 args: args_for_handler,
1409 user_args: user_args_for_handler,
1410 command_path: handler_path,
1411 middleware: middleware_for_handler,
1412 raw_matches: raw_matches_for_handler,
1413 })
1414 .await
1415 },
1416 ),
1417 )
1418 .await;
1419
1420 match result {
1421 Ok(output) => self.finish_run(output.into()),
1422 Err(err) => self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id)),
1423 }
1424 }
1425
1426 fn try_run_search_bypass(&self, args: &[String]) -> Option<CliRunOutput> {
1427 let query = extract_search_query(args);
1428 if query.is_empty() {
1429 return None;
1430 }
1431 let scope = self.search_scope(args);
1432 let output_format =
1433 extract_output_format(args, &default_output_format(&self.config.app_id));
1434 Some(self.render_search(&query, &scope, &output_format))
1435 }
1436
1437 fn try_run_schema_bypass(&self, args: &[String]) -> Option<CliRunOutput> {
1438 if !has_true_schema_flag(args) {
1439 return None;
1440 }
1441 let bool_flags = derive_bool_flags(&self.root);
1442 let value_flags = derive_value_flags(&self.root);
1443 let command_path =
1444 self.canonical_command_path(&extract_command_path(args, &bool_flags, &value_flags));
1445 let schema = self.middleware.schema_registry.get_by_path(&command_path)?;
1446 let output_format =
1447 extract_output_format(args, &default_output_format(&self.config.app_id));
1448 Some(self.render_schema(schema, &output_format))
1449 }
1450
1451 fn render_schema(
1452 &self,
1453 schema: crate::output::SchemaInfo,
1454 output_format: &str,
1455 ) -> CliRunOutput {
1456 let format: crate::output::OutputFormat = match output_format.parse() {
1457 Ok(format) => format,
1458 Err(err) => {
1459 return CliRunOutput {
1460 exit_code: exit_code_for_error(&err),
1461 rendered: err.to_string(),
1462 };
1463 }
1464 };
1465 let envelope =
1466 crate::Envelope::success(schema, self.config.app_id.clone()).prepare_for_render("");
1467 match crate::output::render(format, &envelope) {
1468 Ok(rendered) => CliRunOutput {
1469 exit_code: 0,
1470 rendered,
1471 },
1472 Err(err) => CliRunOutput {
1473 exit_code: exit_code_for_error(&err),
1474 rendered: err.to_string(),
1475 },
1476 }
1477 }
1478
1479 fn render_search(&self, query: &str, scope: &str, output_format: &str) -> CliRunOutput {
1480 let format: crate::output::OutputFormat = match output_format.parse() {
1481 Ok(format) => format,
1482 Err(err) => {
1483 return CliRunOutput {
1484 exit_code: exit_code_for_error(&err),
1485 rendered: err.to_string(),
1486 };
1487 }
1488 };
1489 let docs = self.search_documents(scope);
1490 let results = SearchIndex::new(docs).search(query, 10);
1491 let envelope =
1492 crate::Envelope::success(results, self.config.app_id.clone()).prepare_for_render("");
1493 match crate::output::render(format, &envelope) {
1494 Ok(rendered) => CliRunOutput {
1495 exit_code: 0,
1496 rendered,
1497 },
1498 Err(err) => CliRunOutput {
1499 exit_code: exit_code_for_error(&err),
1500 rendered: err.to_string(),
1501 },
1502 }
1503 }
1504
1505 fn render_root(&self, middleware: &Middleware, actions: Vec<NextAction>) -> CliRunOutput {
1511 if !crate::output::is_valid_output_format(&middleware.output_format) {
1516 let err = CliCoreError::InvalidOutputFormat(middleware.output_format.clone());
1517 return CliRunOutput {
1518 exit_code: exit_code_for_error(&err),
1519 rendered: err.to_string(),
1520 };
1521 }
1522 let format = middleware
1523 .output_format
1524 .parse()
1525 .unwrap_or(crate::output::OutputFormat::Json);
1526 if format == crate::output::OutputFormat::Human {
1527 let base_long = self
1531 .root
1532 .get_long_about()
1533 .map(ToString::to_string)
1534 .unwrap_or_default();
1535 let long = format!("{base_long}{}", render_next_actions_human(&actions));
1536 let rendered = self
1537 .root
1538 .clone()
1539 .long_about(long)
1540 .render_long_help()
1541 .to_string();
1542 return CliRunOutput {
1543 exit_code: 0,
1544 rendered,
1545 };
1546 }
1547 let description = self
1548 .config
1549 .long
1550 .as_deref()
1551 .filter(|long| !long.is_empty())
1552 .unwrap_or(self.config.short.as_str());
1553 let data = serde_json::json!({
1554 "description": description,
1555 "version": self.config.build.version,
1556 });
1557 let envelope = crate::Envelope::success(data, self.config.app_id.clone())
1558 .with_next_actions(actions)
1559 .prepare_for_render(&middleware.verbose);
1560 match crate::output::render(format, &envelope) {
1561 Ok(rendered) => CliRunOutput {
1562 exit_code: 0,
1563 rendered,
1564 },
1565 Err(err) => CliRunOutput {
1566 exit_code: exit_code_for_error(&err),
1567 rendered: err.to_string(),
1568 },
1569 }
1570 }
1571
1572 fn search_documents(&self, scope: &str) -> Vec<SearchDocument> {
1573 let (scoped, mut prefix) = find_command_and_canonical_path_by_colon_path(&self.root, scope)
1574 .unwrap_or((&self.root, Vec::new()));
1575 let mut docs = Vec::new();
1576 let mut aliases = Vec::new();
1577 append_command_alias_terms(scoped, &mut aliases);
1578 collect_command_search_documents(scoped, &mut prefix, &mut aliases, &mut docs);
1579 if scope.is_empty() {
1580 for entry in &self.guide_entries {
1581 docs.push(SearchDocument {
1582 id: format!("guide:{}", entry.name),
1583 kind: "guide".to_owned(),
1584 title: format!("guide {}", entry.name),
1585 summary: entry.summary.clone(),
1586 content: format!("{} {}", entry.summary, entry.content),
1587 });
1588 }
1589 if let Some(extra_search_docs) = &self.extra_search_docs {
1590 docs.extend(extra_search_docs());
1591 }
1592 }
1593 docs
1594 }
1595
1596 fn search_scope(&self, args: &[String]) -> String {
1597 let parts = extract_search_scope_parts(args);
1598 canonical_path_from_parts(&self.root, &parts).unwrap_or_default()
1599 }
1600
1601 fn canonical_command_path(&self, command_path: &str) -> String {
1602 find_command_and_canonical_path_by_colon_path(&self.root, command_path).map_or_else(
1603 || command_path.to_owned(),
1604 |(_, canonical)| canonical.join(":"),
1605 )
1606 }
1607
1608 fn render_guide(&self, matches: &ArgMatches) -> CliRunOutput {
1609 let leaf = leaf_matches(matches);
1610 let topic = leaf.get_one::<String>("topic").map(String::as_str);
1611 match guide_content(&self.guide_entries, topic) {
1612 Ok(rendered) => CliRunOutput {
1613 exit_code: 0,
1614 rendered,
1615 },
1616 Err(err) => CliRunOutput {
1617 exit_code: 1,
1618 rendered: err,
1619 },
1620 }
1621 }
1622
1623 fn render_help_command(&self, matches: &ArgMatches) -> CliRunOutput {
1624 let leaf = leaf_matches(matches);
1625 let parts = leaf
1626 .get_many::<String>("command")
1627 .map(|values| values.map(String::as_str).collect::<Vec<_>>())
1628 .unwrap_or_default();
1629 self.render_help_for_parts(&parts)
1630 }
1631
1632 fn render_help_for_parts(&self, parts: &[&str]) -> CliRunOutput {
1639 if parts.is_empty() {
1640 return CliRunOutput {
1641 exit_code: 0,
1642 rendered: self.root.clone().render_long_help().to_string(),
1643 };
1644 }
1645 let Some(command) = find_help_target(&self.root, parts) else {
1646 return CliRunOutput {
1647 exit_code: 1,
1648 rendered: format!(
1649 "unknown command {:?} — run '{} help' for available commands",
1650 parts.join(" "),
1651 self.config.name
1652 ),
1653 };
1654 };
1655 CliRunOutput {
1656 exit_code: 0,
1657 rendered: command.clone().render_long_help().to_string(),
1658 }
1659 }
1660
1661 fn refresh_root_long(&mut self) {
1662 const BUILTINS: [&str; 4] = ["help", "guide", "tree", "completion"];
1667 let categorized: BTreeSet<&str> = self
1668 .module_entries
1669 .iter()
1670 .map(|entry| entry.name.as_str())
1671 .collect();
1672 let mut generic: Vec<ModuleHelpEntry> = self
1673 .root
1674 .get_subcommands()
1675 .filter(|command| !command.is_hide_set())
1676 .filter(|command| !BUILTINS.contains(&command.get_name()))
1677 .filter(|command| !categorized.contains(command.get_name()))
1678 .map(|command| ModuleHelpEntry {
1679 category: "Commands".to_owned(),
1680 name: command.get_name().to_owned(),
1681 short: command
1682 .get_about()
1683 .map(ToString::to_string)
1684 .unwrap_or_default(),
1685 })
1686 .collect();
1687 generic.sort_by(|left, right| left.name.cmp(&right.name));
1688
1689 let mut entries = self.module_entries.clone();
1690 entries.extend(generic);
1691 let has_guide = !self.guide_entries.is_empty() || has_subcommand(&self.root, "guide");
1692 let intro = self
1693 .config
1694 .long
1695 .as_deref()
1696 .filter(|long| !long.is_empty())
1697 .unwrap_or(self.config.short.as_str());
1698 self.root = self
1699 .root
1700 .clone()
1701 .long_about(build_root_long(intro, &entries, has_guide));
1702 }
1703
1704 fn ensure_auth_command(&mut self) {
1705 let default_provider = self.default_auth_provider();
1706 let registered_names = self.middleware.auth.registered_names();
1707 if default_provider.is_empty() && registered_names.is_empty() {
1708 return;
1709 }
1710 let replacing_builtin = self.commands.contains_key("auth:login");
1711 if has_subcommand(&self.root, "auth") && !replacing_builtin {
1712 return;
1713 }
1714 let group = auth_command_group(&default_provider, ®istered_names);
1715 let mut prefix = Vec::new();
1716 group.register_commands(&mut prefix, &mut self.commands);
1717 let mut prefix = Vec::new();
1718 let clap_group = runtime_group_clap_command_with_schema_help(
1719 &group,
1720 &mut prefix,
1721 &self.middleware.schema_registry,
1722 );
1723 self.root = if replacing_builtin {
1724 self.root.clone().mut_subcommand("auth", |_| clap_group)
1725 } else {
1726 self.root.clone().subcommand(clap_group)
1727 };
1728 self.register_auth_help_entry();
1732 }
1733
1734 fn default_auth_provider(&self) -> String {
1735 if !self.middleware.default_auth_provider.is_empty() {
1736 return self.middleware.default_auth_provider.clone();
1737 }
1738 self.middleware
1739 .auth
1740 .registered_names()
1741 .into_iter()
1742 .next()
1743 .unwrap_or_default()
1744 }
1745
1746 fn initialized_middleware(&self) -> Result<Middleware> {
1747 let Some(init_deps) = &self.init_deps else {
1748 return Ok(self.middleware.clone());
1749 };
1750 let mut guard = self
1751 .init_state
1752 .lock()
1753 .map_err(|_| CliCoreError::message("init deps lock poisoned"))?;
1754 if let Some(result) = guard.as_ref() {
1755 return result.clone().map_err(InitFailure::into_error);
1756 }
1757 let mut middleware = self.middleware.clone();
1758 let result = init_deps(&mut middleware)
1759 .map(|()| middleware)
1760 .map_err(|err| InitFailure::capture(&err));
1761 *guard = Some(result.clone());
1762 result.map_err(InitFailure::into_error)
1763 }
1764
1765 fn apply_config_flags(&self, matches: &ArgMatches, middleware: &mut Middleware) -> Result<()> {
1766 if let Some(apply_flags) = &self.apply_flags {
1767 apply_flags(matches, middleware)?;
1768 }
1769 Ok(())
1770 }
1771
1772 fn run_pre_run(
1773 &self,
1774 middleware: &mut Middleware,
1775 command_path: &str,
1776 args: &crate::middleware::ValueMap,
1777 ) -> Result<()> {
1778 if let Some(pre_run) = &self.pre_run {
1779 pre_run(middleware, command_path, args)?;
1780 }
1781 Ok(())
1782 }
1783
1784 fn resolve_meta(&self, command_path: &str, meta: CommandMeta) -> CommandMeta {
1785 if let Some(resolver) = &self.meta_resolver {
1786 resolver(command_path, meta)
1787 } else {
1788 meta
1789 }
1790 }
1791
1792 fn finish_run(&self, output: CliRunOutput) -> CliRunOutput {
1793 if let Some(on_shutdown) = &self.on_shutdown {
1794 on_shutdown();
1795 }
1796 output
1797 }
1798}
1799
1800fn apply_global_flags(middleware: &mut Middleware, flags: &GlobalFlags, timeout: Option<Duration>) {
1801 middleware.output_format = flags.output_format.clone();
1802 middleware.verbose = flags.verbose.clone();
1803 middleware.dry_run = flags.dry_run;
1804 middleware.fields = flags.fields.clone();
1805 middleware.filter = flags.filter.clone();
1806 middleware.expr = flags.expr.clone();
1807 middleware.limit = flags.limit;
1808 middleware.offset = flags.offset;
1809 middleware.reason = flags.reason.clone();
1810 middleware.schema = flags.schema;
1811 middleware.timeout = timeout;
1812 middleware.debug = flags.debug.clone();
1813 middleware.search = flags.search.clone();
1814}
1815
1816async fn run_with_timeout<F, T>(
1817 timeout: Option<Duration>,
1818 timeout_label: &str,
1819 future: F,
1820) -> Result<T>
1821where
1822 F: Future<Output = Result<T>>,
1823{
1824 let Some(timeout) = timeout else {
1825 return future.await;
1826 };
1827 match tokio::time::timeout(timeout, future).await {
1828 Ok(result) => result,
1829 Err(_) => Err(CliCoreError::message(format!(
1830 "command timed out after {timeout_label}"
1831 ))),
1832 }
1833}
1834
1835async fn run_until_signal<Run, Shutdown>(run: Run, shutdown: Shutdown) -> CliRunOutput
1836where
1837 Run: Future<Output = CliRunOutput>,
1838 Shutdown: Future<Output = ()>,
1839{
1840 tokio::pin!(run);
1841 tokio::pin!(shutdown);
1842 tokio::select! {
1843 output = &mut run => output,
1844 () = &mut shutdown => CliRunOutput {
1845 exit_code: 130,
1846 rendered: "command interrupted\n".to_owned(),
1847 },
1848 }
1849}
1850
1851#[cfg(unix)]
1852async fn shutdown_signal() {
1853 let ctrl_c = tokio::signal::ctrl_c();
1854 match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
1855 Ok(mut sigterm) => {
1856 tokio::select! {
1857 _ = ctrl_c => {},
1858 _ = sigterm.recv() => {},
1859 }
1860 }
1861 Err(_) => {
1862 drop(ctrl_c.await);
1863 }
1864 }
1865}
1866
1867#[cfg(not(unix))]
1868async fn shutdown_signal() {
1869 drop(tokio::signal::ctrl_c().await);
1870}
1871
1872fn parse_command_timeout(raw: &str) -> Result<Option<Duration>> {
1873 let raw = raw.trim();
1874 if raw.is_empty() {
1875 return Ok(Some(Duration::from_secs(60)));
1876 }
1877 let Some(seconds) = parse_duration_seconds(raw) else {
1878 return Err(CliCoreError::message(format!(
1879 "invalid timeout {raw:?}: expected duration like 60s, 5m, or 0s"
1880 )));
1881 };
1882 if seconds <= 0.0 {
1883 Ok(None)
1884 } else {
1885 Ok(Some(Duration::from_secs_f64(seconds)))
1886 }
1887}
1888
1889fn parse_duration_seconds(raw: &str) -> Option<f64> {
1890 for (suffix, seconds) in [
1891 ("ns", 0.000_000_001_f64),
1892 ("us", 0.000_001_f64),
1893 ("µs", 0.000_001_f64),
1894 ("ms", 0.001_f64),
1895 ("s", 1.0_f64),
1896 ("m", 60.0_f64),
1897 ("h", 3600.0_f64),
1898 ] {
1899 if let Some(number) = raw.strip_suffix(suffix) {
1900 let value = number.parse::<f64>().ok()?;
1901 if !value.is_finite() {
1902 return None;
1903 }
1904 return Some(value * seconds);
1905 }
1906 }
1907 None
1908}
1909
1910fn render_cli_error(
1911 middleware: &Middleware,
1912 err: &(dyn std::error::Error + 'static),
1913 system: &str,
1914) -> CliRunOutput {
1915 let format = middleware
1916 .output_format
1917 .parse::<crate::output::OutputFormat>()
1918 .unwrap_or(crate::output::OutputFormat::Json);
1919 let envelope =
1920 crate::output::build_error_envelope(err, system).prepare_for_render(&middleware.verbose);
1921 match crate::output::render(format, &envelope) {
1922 Ok(rendered) => CliRunOutput {
1923 exit_code: exit_code_for_error(err),
1924 rendered,
1925 },
1926 Err(render_err) => CliRunOutput {
1927 exit_code: exit_code_for_error(err),
1928 rendered: render_err.to_string(),
1929 },
1930 }
1931}
1932
1933fn find_command_by_colon_path<'command>(
1934 root: &'command Command,
1935 path: &str,
1936) -> Option<&'command Command> {
1937 find_command_and_canonical_path_by_colon_path(root, path).map(|(command, _)| command)
1938}
1939
1940fn find_help_target<'command>(
1941 root: &'command Command,
1942 parts: &[&str],
1943) -> Option<&'command Command> {
1944 let mut current = root;
1945 let mut matched_any = false;
1946 for part in parts {
1947 let Some(next) = current.find_subcommand(part) else {
1948 break;
1949 };
1950 current = next;
1951 matched_any = true;
1952 }
1953 matched_any.then_some(current)
1954}
1955
1956fn find_command_and_canonical_path_by_colon_path<'command>(
1957 root: &'command Command,
1958 path: &str,
1959) -> Option<(&'command Command, Vec<String>)> {
1960 if path.is_empty() {
1961 return Some((root, Vec::new()));
1962 }
1963 let mut current = root;
1964 let mut canonical = Vec::new();
1965 for part in path.split(':') {
1966 current = current.find_subcommand(part)?;
1967 canonical.push(current.get_name().to_owned());
1968 }
1969 Some((current, canonical))
1970}
1971
1972fn canonical_path_from_parts(root: &Command, parts: &[String]) -> Option<String> {
1973 if parts.is_empty() {
1974 return Some(String::new());
1975 }
1976 let mut current = root;
1977 let mut canonical = Vec::new();
1978 for part in parts {
1979 current = current.find_subcommand(part)?;
1980 canonical.push(current.get_name().to_owned());
1981 }
1982 Some(canonical.join(":"))
1983}
1984
1985fn extract_search_scope_parts(args: &[String]) -> Vec<String> {
1986 let mut parts = Vec::new();
1987 let mut index = 1;
1988 while index < args.len() {
1989 let arg = &args[index];
1990 if arg == "--search" || arg.starts_with("--search=") {
1991 break;
1992 }
1993 if arg.starts_with('-') {
1994 if !arg.contains('=') && index + 1 < args.len() && !args[index + 1].starts_with('-') {
1995 index += 2;
1996 } else {
1997 index += 1;
1998 }
1999 continue;
2000 }
2001 parts.push(arg.clone());
2002 index += 1;
2003 }
2004 parts
2005}
2006
2007fn collect_command_search_documents(
2008 command: &Command,
2009 prefix: &mut Vec<String>,
2010 aliases: &mut Vec<String>,
2011 docs: &mut Vec<SearchDocument>,
2012) {
2013 if command.is_hide_set() || command.get_name() == "completion" {
2014 return;
2015 }
2016 if command.get_subcommands().next().is_some() {
2017 for child in command.get_subcommands() {
2018 prefix.push(child.get_name().to_owned());
2019 let alias_len = aliases.len();
2020 append_command_alias_terms(child, aliases);
2021 collect_command_search_documents(child, prefix, aliases, docs);
2022 aliases.truncate(alias_len);
2023 prefix.pop();
2024 }
2025 return;
2026 }
2027 if prefix.is_empty() {
2028 prefix.push(command.get_name().to_owned());
2029 append_command_alias_terms(command, aliases);
2030 }
2031 let path = prefix.join(" ");
2032 let alias_text = aliases.join(" ");
2033 docs.push(SearchDocument {
2034 id: format!("cmd:{path}"),
2035 kind: "command".to_owned(),
2036 title: path,
2037 summary: command
2038 .get_about()
2039 .map(ToString::to_string)
2040 .unwrap_or_default(),
2041 content: format!(
2042 "{} {} {} {}",
2043 command
2044 .get_about()
2045 .map(ToString::to_string)
2046 .unwrap_or_default(),
2047 command
2048 .get_long_about()
2049 .map(ToString::to_string)
2050 .unwrap_or_default(),
2051 command_flag_text(command),
2052 alias_text
2053 ),
2054 });
2055 if prefix.len() == 1 && prefix[0] == command.get_name() {
2056 prefix.pop();
2057 }
2058}
2059
2060fn append_command_alias_terms(command: &Command, aliases: &mut Vec<String>) {
2061 aliases.extend(command.get_all_aliases().map(str::to_owned));
2062 aliases.extend(
2063 command
2064 .get_all_short_flag_aliases()
2065 .map(|alias| alias.to_string()),
2066 );
2067 aliases.extend(command.get_all_long_flag_aliases().map(str::to_owned));
2068}
2069
2070fn command_flag_text(command: &Command) -> String {
2071 command
2072 .get_arguments()
2073 .filter_map(|arg| {
2074 let mut names = Vec::new();
2075 if let Some(short) = arg.get_short() {
2076 names.push(format!("-{short}"));
2077 }
2078 if let Some(long) = arg.get_long() {
2079 names.push(format!("--{long}"));
2080 }
2081 if let Some(short_aliases) = arg.get_all_short_aliases() {
2082 names.extend(
2083 short_aliases
2084 .into_iter()
2085 .map(|short_alias| format!("-{short_alias}")),
2086 );
2087 }
2088 if let Some(aliases) = arg.get_all_aliases() {
2089 names.extend(aliases.into_iter().map(|alias| format!("--{alias}")));
2090 }
2091 (!names.is_empty()).then(|| names.join(" "))
2092 })
2093 .collect::<Vec<_>>()
2094 .join(" ")
2095}
2096
2097fn has_subcommand(command: &Command, name: &str) -> bool {
2098 command
2099 .get_subcommands()
2100 .any(|child| child.get_name() == name)
2101}
2102
2103fn has_root_version_flag(args: &[String], root: &Command, root_name: &str) -> bool {
2104 let bool_flags = derive_bool_flags(root);
2105 let value_flags = derive_value_flags(root);
2106 let mut iter = args.iter().peekable();
2107 if iter
2108 .peek()
2109 .is_some_and(|arg| arg_matches_root_name(arg, root_name))
2110 {
2111 iter.next();
2112 }
2113
2114 while let Some(arg) = iter.next() {
2115 match arg.as_str() {
2116 "--version" | "-v" => return true,
2117 "--" => return false,
2118 value if value.contains('=') || bool_flags.contains(value) => continue,
2119 value
2120 if value_flags.contains(value)
2121 || unknown_flag_consumes_value(value, iter.peek()) =>
2122 {
2123 iter.next();
2124 }
2125 value if value.starts_with('-') => {}
2126 _ => return false,
2127 }
2128 }
2129 false
2130}
2131
2132fn normalize_optional_global_flags_before_command(root: &Command, args: &[String]) -> Vec<String> {
2133 let optional_string_defaults = BTreeMap::from([("--verbose", "all"), ("--debug", "*")]);
2134 let optional_bool_defaults = BTreeMap::from([("--dry-run", "true"), ("--schema", "true")]);
2135 let mut normalized = Vec::with_capacity(args.len());
2136 let mut index = 0;
2137 let mut current = root;
2138 while index < args.len() {
2139 let arg = &args[index];
2140 if index == 0 && arg_matches_root_name(arg, root.get_name()) {
2141 normalized.push(arg.clone());
2142 index += 1;
2143 continue;
2144 }
2145
2146 if let Some(default) = optional_bool_defaults.get(arg.as_str()) {
2147 normalized.push(format!("{arg}={default}"));
2148 index += 1;
2149 continue;
2150 }
2151
2152 if let Some(default) = optional_string_defaults.get(arg.as_str()) {
2153 match args.get(index + 1) {
2154 None => {
2155 normalized.push(format!("{arg}={default}"));
2156 index += 1;
2157 continue;
2158 }
2159 Some(next)
2160 if current.get_name() == root.get_name()
2161 || next.starts_with('-')
2162 || direct_subcommand(current, next).is_some() =>
2163 {
2164 normalized.push(format!("{arg}={default}"));
2165 index += 1;
2166 continue;
2167 }
2168 Some(next) => {
2169 normalized.push(arg.clone());
2170 normalized.push(next.clone());
2171 index += 2;
2172 continue;
2173 }
2174 }
2175 }
2176
2177 normalized.push(arg.clone());
2178 if !arg.starts_with('-')
2179 && let Some(next_command) = direct_subcommand(current, arg)
2180 {
2181 current = next_command;
2182 }
2183 index += 1;
2184 }
2185 normalized
2186}
2187
2188fn direct_subcommand<'command>(
2189 command: &'command Command,
2190 token: &str,
2191) -> Option<&'command Command> {
2192 command.get_subcommands().find(|child| {
2193 child.get_name() == token || child.get_all_aliases().any(|alias| alias == token)
2194 })
2195}
2196
2197fn unknown_group_command_message(root: &Command, positionals: &[String]) -> Option<String> {
2198 if positionals.is_empty() {
2199 return None;
2200 }
2201
2202 let mut current = root;
2203 let mut path = vec![root.get_name().to_owned()];
2204 for token in positionals {
2205 if let Some(next) = current.find_subcommand(token) {
2206 current = next;
2207 path.push(next.get_name().to_owned());
2208 continue;
2209 }
2210 if current.get_subcommands().next().is_some() {
2211 return Some(format!(
2212 "unknown command {token:?} for {:?}",
2213 path.join(" ")
2214 ));
2215 }
2216 return None;
2217 }
2218 None
2219}
2220
2221fn group_help_target_parts(
2244 root: &Command,
2245 positionals: &[String],
2246 command_keyword_count: usize,
2247) -> Option<Vec<String>> {
2248 let help_index = positionals.iter().position(|token| token == "help")?;
2249 if help_index == 0 {
2251 return None;
2252 }
2253 if help_index >= command_keyword_count {
2255 return None;
2256 }
2257 let prefix = &positionals[..help_index];
2258 let mut current = root;
2259 for token in prefix {
2260 current = current.find_subcommand(token)?;
2261 }
2262 current.get_subcommands().next()?;
2264 if current.find_subcommand("help").is_some() {
2266 return None;
2267 }
2268 let suffix = &positionals[help_index + 1..];
2270 Some(prefix.iter().chain(suffix).cloned().collect())
2271}
2272
2273fn rewrite_group_help_args(
2284 clap_args: &[String],
2285 root_name: &str,
2286 bool_flags: &BTreeSet<String>,
2287 value_flags: &BTreeSet<String>,
2288 parts: &[String],
2289) -> Vec<String> {
2290 let mut next_positional = std::iter::once("help".to_owned())
2292 .chain(parts.iter().cloned())
2293 .peekable();
2294 let mut out = Vec::with_capacity(clap_args.len());
2295 let mut iter = clap_args.iter().peekable();
2296 if iter
2297 .peek()
2298 .is_some_and(|arg| arg_matches_root_name(arg, root_name))
2299 && let Some(program) = iter.next()
2300 {
2301 out.push(program.clone());
2302 }
2303
2304 let mut take_positional =
2305 |fallback: &String| next_positional.next().unwrap_or(fallback.clone());
2306
2307 while let Some(arg) = iter.next() {
2308 if arg == "--" {
2309 out.push(arg.clone());
2310 for rest in iter.by_ref() {
2312 out.push(take_positional(rest));
2313 }
2314 break;
2315 }
2316 if arg.contains('=') || bool_flags.contains(arg) {
2317 out.push(arg.clone());
2318 continue;
2319 }
2320 if value_flags.contains(arg) || unknown_flag_consumes_value(arg, iter.peek()) {
2321 out.push(arg.clone());
2322 if let Some(value) = iter.next() {
2323 out.push(value.clone());
2324 }
2325 continue;
2326 }
2327 if arg.starts_with('-') {
2328 out.push(arg.clone());
2329 continue;
2330 }
2331 out.push(take_positional(arg));
2332 }
2333 out.extend(next_positional);
2335 out
2336}
2337
2338fn positional_command_tokens(
2339 args: &[String],
2340 root_name: &str,
2341 bool_flags: &BTreeSet<String>,
2342 value_flags: &BTreeSet<String>,
2343) -> Vec<String> {
2344 let mut tokens = Vec::new();
2345 let mut iter = args.iter().peekable();
2346 if iter
2347 .peek()
2348 .is_some_and(|arg| arg_matches_root_name(arg, root_name))
2349 {
2350 iter.next();
2351 }
2352
2353 while let Some(arg) = iter.next() {
2354 if arg == "--" {
2355 tokens.extend(iter.cloned());
2356 break;
2357 }
2358 if arg.contains('=') {
2359 continue;
2360 }
2361 if bool_flags.contains(arg) {
2362 continue;
2363 }
2364 if value_flags.contains(arg) || unknown_flag_consumes_value(arg, iter.peek()) {
2365 iter.next();
2366 continue;
2367 }
2368 if arg.starts_with('-') {
2369 continue;
2370 }
2371 tokens.push(arg.clone());
2372 }
2373 tokens
2374}
2375
2376fn unknown_flag_consumes_value(arg: &str, next: Option<&&String>) -> bool {
2377 arg.starts_with('-') && next.is_some_and(|value| !value.starts_with('-'))
2378}
2379
2380fn arg_matches_root_name(arg: &str, root_name: &str) -> bool {
2381 arg == root_name
2382 || Path::new(arg)
2383 .file_stem()
2384 .and_then(|n| n.to_str())
2385 .is_some_and(|n| n == root_name)
2386}
2387
2388enum Argv0Outcome {
2391 Proceed(Vec<String>),
2393 Handled(CliRunOutput),
2395}
2396
2397fn program_basename(arg: &str) -> String {
2401 Path::new(arg)
2402 .file_stem()
2403 .and_then(|stem| stem.to_str())
2404 .map_or_else(|| arg.to_owned(), ToOwned::to_owned)
2405}
2406
2407fn is_valid_argv0_name(name: &str) -> bool {
2412 !name.is_empty()
2413 && name.chars().all(|character| {
2414 character.is_ascii_alphanumeric() || character == '-' || character == '_'
2415 })
2416}
2417
2418fn argv0_link_matches(
2423 link: &Path,
2424 target: &Path,
2425 name: &str,
2426 method: Argv0LinkMethod,
2427) -> std::io::Result<bool> {
2428 let metadata = std::fs::symlink_metadata(link)?;
2429 match method {
2430 Argv0LinkMethod::SoftLink => {
2431 Ok(metadata.file_type().is_symlink() && std::fs::read_link(link)? == target)
2432 }
2433 Argv0LinkMethod::HardLink => {
2434 if metadata.file_type().is_symlink() {
2435 return Ok(false);
2436 }
2437 Ok(std::fs::read(link)? == std::fs::read(target)?)
2440 }
2441 Argv0LinkMethod::Script => {
2442 if metadata.file_type().is_symlink() {
2443 return Ok(false);
2444 }
2445 Ok(std::fs::read_to_string(link).ok() == Some(argv0_script_contents(target, name)))
2446 }
2447 }
2448}
2449
2450fn argv0_link_file_name(name: &str, method: Argv0LinkMethod) -> String {
2452 let extension = match method {
2453 Argv0LinkMethod::Script if cfg!(windows) => ".cmd",
2454 Argv0LinkMethod::Script => "",
2456 _ if cfg!(windows) => ".exe",
2457 _ => "",
2458 };
2459 format!("{name}{extension}")
2460}
2461
2462fn argv0_script_contents(target: &Path, name: &str) -> String {
2466 let target = target.display();
2467 if cfg!(windows) {
2468 format!("@\"{target}\" argv0 {name} %*\r\n")
2469 } else {
2470 format!("#!/bin/sh\nexec \"{target}\" argv0 {name} \"$@\"\n")
2471 }
2472}
2473
2474#[cfg(unix)]
2475fn create_symlink(target: &Path, link: &Path) -> std::io::Result<()> {
2476 std::os::unix::fs::symlink(target, link)
2477}
2478
2479#[cfg(windows)]
2480fn create_symlink(target: &Path, link: &Path) -> std::io::Result<()> {
2481 std::os::windows::fs::symlink_file(target, link)
2482}
2483
2484#[cfg(not(any(unix, windows)))]
2485fn create_symlink(_target: &Path, _link: &Path) -> std::io::Result<()> {
2486 Err(std::io::Error::new(
2487 std::io::ErrorKind::Unsupported,
2488 "symlink creation is not supported on this platform",
2489 ))
2490}
2491
2492#[cfg(unix)]
2494fn make_executable(path: &Path) -> std::io::Result<()> {
2495 use std::os::unix::fs::PermissionsExt;
2496 let mut permissions = std::fs::metadata(path)?.permissions();
2497 permissions.set_mode(0o755);
2498 std::fs::set_permissions(path, permissions)
2499}
2500
2501#[cfg(not(unix))]
2502fn make_executable(_path: &Path) -> std::io::Result<()> {
2503 Ok(())
2504}
2505
2506fn register_runtime_group_schemas(
2507 group: &RuntimeGroupSpec,
2508 prefix: &mut Vec<String>,
2509 schemas: &mut SchemaRegistry,
2510) {
2511 prefix.push(group.group.name.clone());
2512 for child_group in &group.groups {
2513 register_runtime_group_schemas(child_group, prefix, schemas);
2514 }
2515 for child in &group.commands {
2516 prefix.push(child.spec.name.clone());
2517 register_command_schema(&child.spec, &prefix.join(":"), schemas);
2518 prefix.pop();
2519 }
2520 prefix.pop();
2521}
2522
2523fn register_command_schema(spec: &CommandSpec, command_path: &str, schemas: &mut SchemaRegistry) {
2524 if let Some(schema) = &spec.output_schema {
2525 schemas.register_info(command_path.to_owned(), schema.clone());
2526 }
2527}
2528
2529fn runtime_group_clap_command_with_schema_help(
2530 group: &RuntimeGroupSpec,
2531 prefix: &mut Vec<String>,
2532 schemas: &SchemaRegistry,
2533) -> Command {
2534 let mut command = group_clap_command_without_children(&group.group);
2535 prefix.push(group.group.name.clone());
2536 for child_group in &group.groups {
2537 command = command.subcommand(runtime_group_clap_command_with_schema_help(
2538 child_group,
2539 prefix,
2540 schemas,
2541 ));
2542 }
2543 for child in &group.commands {
2544 prefix.push(child.spec.name.clone());
2545 let command_path = prefix.join(":");
2546 command = command.subcommand(command_clap_command_with_schema_help(
2547 &child.spec,
2548 &command_path,
2549 schemas,
2550 ));
2551 prefix.pop();
2552 }
2553 prefix.pop();
2554 command
2555}
2556
2557fn group_clap_command_without_children(group: &GroupSpec) -> Command {
2558 let mut command = Command::new(group.name.clone())
2559 .about(group.short.clone())
2560 .help_template(GROUP_HELP_TEMPLATE);
2561 if let Some(long) = &group.long
2562 && !long.is_empty()
2563 {
2564 command = command.long_about(long.clone());
2565 }
2566 for alias in &group.aliases {
2567 command = command.alias(alias.clone());
2568 }
2569 if group.hidden {
2570 command = command.hide(true);
2571 }
2572 command
2573}
2574
2575fn command_clap_command_with_schema_help(
2576 spec: &CommandSpec,
2577 command_path: &str,
2578 schemas: &SchemaRegistry,
2579) -> Command {
2580 let mut command = spec.clap_command();
2581 let Some(schema) = schemas.get_by_path(command_path) else {
2582 return command;
2583 };
2584 let schema_help = format_help_section(&schema.fields);
2585 if schema_help.is_empty() {
2586 return command;
2587 }
2588 let base = spec
2589 .long
2590 .as_ref()
2591 .filter(|long| !long.is_empty())
2592 .cloned()
2593 .unwrap_or_else(|| spec.short.clone());
2594 let long = if base.is_empty() {
2595 schema_help
2596 } else {
2597 format!("{base}\n\n{schema_help}")
2598 };
2599 command = command.long_about(long);
2600 command
2601}
2602
2603fn process_exit_code(code: i32) -> ExitCode {
2604 if code == 0 {
2605 return ExitCode::SUCCESS;
2606 }
2607 match u8::try_from(code) {
2608 Ok(code) if code != 0 => ExitCode::from(code),
2609 Ok(_) | Err(_) => ExitCode::from(1),
2610 }
2611}
2612
2613async fn run_streaming_command(
2614 middleware: &Middleware,
2615 request: MiddlewareRequest<'_>,
2616 raw_matches: Arc<ArgMatches>,
2617 streaming_handler: crate::command::StreamingCommandHandler,
2618) -> Result<CliRunOutput> {
2619 use tokio::{io::AsyncWriteExt, sync::mpsc};
2620
2621 let args_for_handler = request.args.clone();
2622 let user_args_for_handler = request.user_args.clone();
2623 let handler_path = request.command_path.to_owned();
2624 let middleware_for_handler = middleware.clone();
2625 let raw_matches_for_handler = raw_matches;
2626
2627 let (tx, mut rx) = mpsc::channel::<serde_json::Value>(64);
2628 let sender = StreamSender(tx);
2629
2630 let writer = tokio::spawn(async move {
2634 let mut stdout = tokio::io::stdout();
2635 while let Some(event) = rx.recv().await {
2636 let Ok(line) = serde_json::to_string(&event) else {
2637 continue;
2638 };
2639 if stdout.write_all(line.as_bytes()).await.is_err()
2640 || stdout.write_all(b"\n").await.is_err()
2641 || stdout.flush().await.is_err()
2642 {
2643 break;
2644 }
2645 }
2646 });
2647
2648 let output = middleware
2649 .run(request, async move |credential| {
2650 streaming_handler(
2651 CommandContext {
2652 credential,
2653 args: args_for_handler,
2654 user_args: user_args_for_handler,
2655 command_path: handler_path,
2656 middleware: middleware_for_handler,
2657 raw_matches: raw_matches_for_handler,
2658 },
2659 sender,
2660 )
2661 .await?;
2662 Ok(crate::CommandResult::new(serde_json::Value::Null))
2663 })
2664 .await;
2665
2666 let _write_result = writer.await;
2669
2670 match output {
2671 Ok(out) if out.exit_code == 0 => Ok(CliRunOutput {
2672 exit_code: 0,
2673 rendered: String::new(),
2674 }),
2675 Ok(out) => Ok(out.into()),
2676 Err(err) => Ok(CliRunOutput {
2677 exit_code: exit_code_for_error(&err),
2678 rendered: render_cli_error(middleware, &err, middleware.app_id.as_str()).rendered,
2679 }),
2680 }
2681}