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, HumanViewRegistry, 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 user_agent: Option<String>,
217 pub redacted_debug_headers: Vec<String>,
223 pub authz: Option<Arc<dyn Authorizer>>,
225 pub auditor: Option<Arc<dyn Auditor>>,
227 pub activity: Option<Arc<dyn ActivityEmitter>>,
229 pub init_deps: Option<InitDeps>,
231 pub register_flags: Option<RegisterFlags>,
233 pub apply_flags: Option<ApplyFlags>,
235 pub pre_run: Option<PreRun>,
237 pub meta_resolver: Option<ResolveMeta>,
239 pub on_shutdown: Option<OnShutdown>,
241 pub extra_search_docs: Option<ExtraSearchDocs>,
243 pub root_next_actions: Option<RootNextActions>,
245 pub admin_category: Option<String>,
250 pub config_commands: bool,
255 pub argv0_routes: BTreeMap<String, Argv0Route>,
263 pub environments: Option<Arc<crate::environments::Environments>>,
270}
271
272impl CliConfig {
273 #[must_use]
275 pub fn new(
276 name: impl Into<String>,
277 short: impl Into<String>,
278 app_id: impl Into<String>,
279 ) -> Self {
280 Self {
281 name: name.into(),
282 short: short.into(),
283 app_id: app_id.into(),
284 ..Self::default()
285 }
286 }
287
288 #[must_use]
290 pub fn with_long(mut self, long: impl Into<String>) -> Self {
291 self.long = Some(long.into());
292 self
293 }
294
295 #[must_use]
297 pub fn with_build(mut self, build: BuildInfo) -> Self {
298 self.build = build;
299 self
300 }
301
302 #[must_use]
304 pub fn with_default_auth_provider(mut self, provider: impl Into<String>) -> Self {
305 self.default_auth_provider = Some(provider.into());
306 self
307 }
308
309 #[must_use]
338 pub fn with_environments(
339 mut self,
340 environments: Arc<crate::environments::Environments>,
341 ) -> Self {
342 self.environments = Some(environments);
343 self
344 }
345
346 #[must_use]
355 pub fn with_user_agent(mut self, user_agent: impl Into<String>) -> Self {
356 self.user_agent = Some(user_agent.into());
357 self
358 }
359
360 #[must_use]
370 pub fn with_redacted_debug_headers(
371 mut self,
372 names: impl IntoIterator<Item = impl Into<String>>,
373 ) -> Self {
374 self.redacted_debug_headers
375 .extend(names.into_iter().filter_map(|name| {
376 let name = name.into().trim().to_owned();
377 (!name.is_empty()).then_some(name)
378 }));
379 self
380 }
381
382 #[must_use]
389 pub fn user_agent_string(&self) -> String {
390 if let Some(user_agent) = &self.user_agent {
391 return user_agent.clone();
392 }
393 if self.build.version.is_empty() {
394 self.name.clone()
395 } else {
396 format!("{}/{}", self.name, self.build.version)
397 }
398 }
399
400 #[must_use]
402 pub fn with_module(mut self, module: Module) -> Self {
403 self.modules.push(module);
404 self
405 }
406
407 #[must_use]
409 pub fn with_modules(mut self, modules: impl IntoIterator<Item = Module>) -> Self {
410 self.modules.extend(modules);
411 self
412 }
413
414 #[must_use]
416 pub fn with_command(mut self, command: RuntimeCommandSpec) -> Self {
417 self.commands.push(command);
418 self
419 }
420
421 #[must_use]
423 pub fn with_guide(mut self, guide: GuideEntry) -> Self {
424 self.guides.push(guide);
425 self
426 }
427
428 #[must_use]
430 pub fn with_guides(mut self, guides: impl IntoIterator<Item = GuideEntry>) -> Self {
431 self.guides.extend(guides);
432 self
433 }
434
435 #[must_use]
437 pub fn with_view(mut self, view: HumanViewDef) -> Self {
438 self.views.push(view);
439 self
440 }
441
442 #[must_use]
444 pub fn with_auth_provider(mut self, provider: Arc<dyn AuthProvider>) -> Self {
445 self.auth_providers.push(provider);
446 self
447 }
448
449 #[must_use]
451 pub fn with_authz(mut self, authz: Arc<dyn Authorizer>) -> Self {
452 self.authz = Some(authz);
453 self
454 }
455
456 #[must_use]
458 pub fn with_auditor(mut self, auditor: Arc<dyn Auditor>) -> Self {
459 self.auditor = Some(auditor);
460 self
461 }
462
463 #[must_use]
465 pub fn with_activity(mut self, activity: Arc<dyn ActivityEmitter>) -> Self {
466 self.activity = Some(activity);
467 self
468 }
469
470 #[must_use]
472 pub fn with_init_deps(mut self, init_deps: InitDeps) -> Self {
473 self.init_deps = Some(init_deps);
474 self
475 }
476
477 #[must_use]
479 pub fn with_register_flags(mut self, register_flags: RegisterFlags) -> Self {
480 self.register_flags = Some(register_flags);
481 self
482 }
483
484 #[must_use]
486 pub fn with_apply_flags(mut self, apply_flags: ApplyFlags) -> Self {
487 self.apply_flags = Some(apply_flags);
488 self
489 }
490
491 #[must_use]
493 pub fn with_pre_run(mut self, pre_run: PreRun) -> Self {
494 self.pre_run = Some(pre_run);
495 self
496 }
497
498 #[must_use]
500 pub fn with_meta_resolver(mut self, meta_resolver: ResolveMeta) -> Self {
501 self.meta_resolver = Some(meta_resolver);
502 self
503 }
504
505 #[must_use]
507 pub fn with_on_shutdown(mut self, on_shutdown: OnShutdown) -> Self {
508 self.on_shutdown = Some(on_shutdown);
509 self
510 }
511
512 #[must_use]
514 pub fn with_extra_search_docs(mut self, extra_search_docs: ExtraSearchDocs) -> Self {
515 self.extra_search_docs = Some(extra_search_docs);
516 self
517 }
518
519 #[must_use]
521 pub fn with_root_next_actions(mut self, root_next_actions: RootNextActions) -> Self {
522 self.root_next_actions = Some(root_next_actions);
523 self
524 }
525
526 #[must_use]
530 pub fn with_admin_category(mut self, category: impl Into<String>) -> Self {
531 self.admin_category = Some(category.into());
532 self
533 }
534
535 #[must_use]
541 pub fn with_config_commands(mut self) -> Self {
542 self.config_commands = true;
543 self
544 }
545
546 #[must_use]
569 pub fn with_argv0_alias(
570 mut self,
571 name: impl Into<String>,
572 command_path: impl IntoIterator<Item = impl Into<String>>,
573 ) -> Self {
574 let name = name.into();
575 debug_assert!(
576 is_valid_argv0_name(&name),
577 "argv0 route name {name:?} must be non-empty and contain only ASCII letters, digits, '-', or '_'"
578 );
579 debug_assert!(
580 name != self.name,
581 "argv0 route name {name:?} must differ from the CLI's own name {:?}",
582 self.name
583 );
584 let tokens = command_path.into_iter().map(Into::into).collect();
585 self.argv0_routes.insert(name, Argv0Route::Alias(tokens));
586 self
587 }
588
589 #[must_use]
611 pub fn with_argv0_personality(
612 mut self,
613 name: impl Into<String>,
614 build: impl Fn() -> CliConfig + Send + Sync + 'static,
615 ) -> Self {
616 let name = name.into();
617 debug_assert!(
618 is_valid_argv0_name(&name),
619 "argv0 route name {name:?} must be non-empty and contain only ASCII letters, digits, '-', or '_'"
620 );
621 debug_assert!(
622 name != self.name,
623 "argv0 route name {name:?} must differ from the CLI's own name {:?}",
624 self.name
625 );
626 self.argv0_routes
627 .insert(name, Argv0Route::Personality(Arc::new(build)));
628 self
629 }
630}
631
632impl std::fmt::Debug for CliConfig {
633 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
634 formatter
635 .debug_struct("CliConfig")
636 .field("name", &self.name)
637 .field("short", &self.short)
638 .field("long", &self.long)
639 .field("build", &self.build)
640 .field("app_id", &self.app_id)
641 .field("default_auth_provider", &self.default_auth_provider)
642 .field("modules", &self.modules)
643 .field("commands", &self.commands)
644 .field("guides", &self.guides)
645 .field("views", &self.views)
646 .field("auth_providers_len", &self.auth_providers.len())
647 .field("has_authz", &self.authz.is_some())
648 .field("has_auditor", &self.auditor.is_some())
649 .field("has_activity", &self.activity.is_some())
650 .field("has_init_deps", &self.init_deps.is_some())
651 .field("has_register_flags", &self.register_flags.is_some())
652 .field("has_apply_flags", &self.apply_flags.is_some())
653 .field("has_pre_run", &self.pre_run.is_some())
654 .field("has_meta_resolver", &self.meta_resolver.is_some())
655 .field("has_on_shutdown", &self.on_shutdown.is_some())
656 .field("has_extra_search_docs", &self.extra_search_docs.is_some())
657 .field("has_root_next_actions", &self.root_next_actions.is_some())
658 .field("admin_category", &self.admin_category)
659 .field(
660 "argv0_routes",
661 &self.argv0_routes.keys().collect::<Vec<_>>(),
662 )
663 .finish()
664 }
665}
666
667#[derive(Clone, Debug, PartialEq)]
669pub struct CliRunOutput {
670 pub exit_code: i32,
672 pub rendered: String,
674}
675
676impl From<crate::middleware::MiddlewareOutput> for CliRunOutput {
677 fn from(o: crate::middleware::MiddlewareOutput) -> Self {
678 Self {
679 exit_code: o.exit_code,
680 rendered: o.rendered,
681 }
682 }
683}
684
685#[derive(Clone)]
691pub struct Cli {
692 config: CliConfig,
693 middleware: Middleware,
694 root: Command,
695 commands: BTreeMap<String, RuntimeCommandSpec>,
696 module_entries: Vec<ModuleHelpEntry>,
697 guide_entries: Vec<GuideEntry>,
698 init_deps: Option<InitDeps>,
699 apply_flags: Option<ApplyFlags>,
700 pre_run: Option<PreRun>,
701 meta_resolver: Option<ResolveMeta>,
702 on_shutdown: Option<OnShutdown>,
703 extra_search_docs: Option<ExtraSearchDocs>,
704 root_next_actions: Option<RootNextActions>,
705 init_state: Arc<Mutex<Option<std::result::Result<Middleware, InitFailure>>>>,
706}
707
708#[derive(Clone, Debug, Eq, PartialEq)]
709struct InitFailure {
710 message: String,
711 code: String,
712 system: String,
713 request_id: String,
714 exit_code: i32,
715}
716
717impl InitFailure {
718 fn capture(err: &CliCoreError) -> Self {
719 let envelope = crate::output::build_error_envelope(err, "");
720 let (code, system, request_id) = envelope.error.map_or_else(
721 || ("ERROR".to_owned(), String::new(), String::new()),
722 |error| (error.code, error.system, error.request_id),
723 );
724 Self {
725 message: err.to_string(),
726 code,
727 system,
728 request_id,
729 exit_code: exit_code_for_error(err),
730 }
731 }
732
733 fn into_error(self) -> CliCoreError {
734 CliCoreError::with_exit_code(
735 self.exit_code,
736 CliCoreError::SystemMessage {
737 message: self.message,
738 system: self.system,
739 code: self.code,
740 request_id: self.request_id,
741 },
742 )
743 }
744}
745
746impl std::fmt::Debug for Cli {
747 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
748 formatter
749 .debug_struct("Cli")
750 .field("config", &self.config)
751 .field("middleware", &self.middleware)
752 .field("root", &self.root)
753 .field("commands", &self.commands)
754 .field("module_entries", &self.module_entries)
755 .field("guide_entries", &self.guide_entries)
756 .field("has_init_deps", &self.init_deps.is_some())
757 .field("has_apply_flags", &self.apply_flags.is_some())
758 .field("has_pre_run", &self.pre_run.is_some())
759 .field("has_meta_resolver", &self.meta_resolver.is_some())
760 .field("has_on_shutdown", &self.on_shutdown.is_some())
761 .field("has_extra_search_docs", &self.extra_search_docs.is_some())
762 .field("has_root_next_actions", &self.root_next_actions.is_some())
763 .finish()
764 }
765}
766
767impl Cli {
768 #[must_use]
770 pub fn new(config: CliConfig) -> Self {
771 let auth_providers = config.auth_providers.clone();
772 let guides = config.guides.clone();
773 let views = config.views.clone();
774 let modules = config.modules.clone();
775 let commands = config.commands.clone();
776 let init_deps = config.init_deps.clone();
777 let apply_flags = config.apply_flags.clone();
778 let pre_run = config.pre_run.clone();
779 let meta_resolver = config.meta_resolver.clone();
780 let on_shutdown = config.on_shutdown.clone();
781 let extra_search_docs = config.extra_search_docs.clone();
782 let root_next_actions = config.root_next_actions.clone();
783 let mut root = Command::new(config.name.clone())
784 .about(config.short.clone())
785 .disable_help_subcommand(true)
786 .version(config.build.version_string());
787 if let Some(long) = &config.long
788 && !long.is_empty()
789 {
790 root = root.long_about(long.clone());
791 }
792 root = register_global_flags(root)
793 .subcommand(help_command())
794 .subcommand(guide_command())
795 .subcommand(Command::new("tree").about("Display full command tree"));
796 if let Some(register_flags) = &config.register_flags {
797 root = register_flags(root);
798 }
799 if config.environments.is_some() {
800 root = root.arg(
801 clap::Arg::new("env")
802 .long("env")
803 .global(true)
804 .value_name("ENV")
805 .help("Override the active environment (see: env list)"),
806 );
807 }
808 let intro = config
809 .long
810 .as_deref()
811 .filter(|long| !long.is_empty())
812 .unwrap_or(config.short.as_str());
813 root = root
814 .long_about(build_root_long(intro, &[], false))
815 .help_template(ROOT_HELP_TEMPLATE);
816
817 let mut middleware = Middleware::new();
818 middleware.app_id = config.app_id.clone();
819 middleware.config = Arc::new(crate::config::ConfigFile::load(&config.app_id));
822 middleware.default_auth_provider = config.default_auth_provider.clone().unwrap_or_default();
823 middleware.authz = config.authz.clone();
824 middleware.auditor = config.auditor.clone();
825 middleware.activity = config.activity.clone();
826 middleware
827 .schema_registry
828 .merge(&global_schema_registry_snapshot());
829 middleware
830 .human_views
831 .merge(&global_human_view_registry_snapshot());
832 if let Some(environments) = &config.environments {
833 middleware.env = environments.effective_active(None, &middleware.config);
838 middleware.environments = Some(Arc::clone(environments));
839 }
840
841 let mut cli = Self {
842 config,
843 middleware,
844 root,
845 commands: BTreeMap::new(),
846 module_entries: Vec::new(),
847 guide_entries: Vec::new(),
848 init_deps,
849 apply_flags,
850 pre_run,
851 meta_resolver,
852 on_shutdown,
853 extra_search_docs,
854 root_next_actions,
855 init_state: Arc::new(Mutex::new(None)),
856 };
857 for provider in auth_providers {
858 cli.register_auth_provider(provider);
859 }
860 if cli.middleware.default_auth_provider.is_empty()
861 && let Some(provider) = cli.middleware.auth.registered_names().first()
862 {
863 cli.middleware.default_auth_provider = provider.clone();
864 }
865 if !cli.middleware.default_auth_provider.is_empty() {
866 cli.ensure_auth_command();
867 }
868 for view in views {
869 cli.middleware.human_views.register(view);
870 }
871 cli.add_guides(guides);
872 for module in modules {
873 cli.add_module(module);
874 }
875 for command in commands {
876 cli.add_command(command);
877 }
878 if cli.config.config_commands {
879 cli.ensure_config_command();
880 }
881 if cli.config.environments.is_some() {
882 cli.ensure_env_command();
883 }
884 cli
885 }
886
887 fn register_auth_help_entry(&mut self) {
892 let category = self
893 .config
894 .admin_category
895 .clone()
896 .unwrap_or_else(|| DEFAULT_ADMIN_CATEGORY.to_owned());
897 let already_listed = self.module_entries.iter().any(|entry| entry.name == "auth");
898 let short = self
899 .root
900 .find_subcommand("auth")
901 .filter(|auth| !auth.is_hide_set())
902 .map(|auth| {
903 auth.get_about()
904 .map(ToString::to_string)
905 .unwrap_or_default()
906 });
907 if !already_listed && let Some(short) = short {
908 self.module_entries.push(ModuleHelpEntry {
909 category,
910 name: "auth".to_owned(),
911 short,
912 });
913 }
914 self.refresh_root_long();
915 }
916
917 #[must_use]
919 pub fn middleware(&self) -> &Middleware {
920 &self.middleware
921 }
922
923 pub fn middleware_mut(&mut self) -> &mut Middleware {
925 &mut self.middleware
926 }
927
928 pub async fn execute(&self) -> ExitCode {
930 let mut stdout = std::io::stdout().lock();
931 let mut stderr = std::io::stderr().lock();
932 match self
933 .execute_from(std::env::args_os(), &mut stdout, &mut stderr)
934 .await
935 {
936 Ok(code) => code,
937 Err(err) => {
938 drop(writeln!(stderr, "{err}"));
939 ExitCode::from(1)
940 }
941 }
942 }
943
944 pub async fn execute_from<I, S, O, E>(
946 &self,
947 args: I,
948 stdout: &mut O,
949 stderr: &mut E,
950 ) -> std::io::Result<ExitCode>
951 where
952 I: IntoIterator<Item = S>,
953 S: Into<std::ffi::OsString> + Clone,
954 O: Write,
955 E: Write,
956 {
957 self.execute_from_until_signal(args, stdout, stderr, shutdown_signal())
958 .await
959 }
960
961 pub async fn execute_from_until_signal<I, S, O, E, Shutdown>(
963 &self,
964 args: I,
965 stdout: &mut O,
966 stderr: &mut E,
967 shutdown: Shutdown,
968 ) -> std::io::Result<ExitCode>
969 where
970 I: IntoIterator<Item = S>,
971 S: Into<std::ffi::OsString> + Clone,
972 O: Write,
973 E: Write,
974 Shutdown: Future<Output = ()>,
975 {
976 self.install_default_user_agent();
977 let output = run_until_signal(self.run(args), shutdown).await;
978 if output.exit_code == 130
979 && output.rendered == "command interrupted\n"
980 && let Some(on_shutdown) = &self.on_shutdown
981 {
982 on_shutdown();
983 }
984 if output.exit_code == 0 {
985 stdout.write_all(output.rendered.as_bytes())?;
986 } else {
987 stderr.write_all(output.rendered.as_bytes())?;
988 }
989 Ok(process_exit_code(output.exit_code))
990 }
991
992 fn install_default_user_agent(&self) {
1000 crate::transport::set_default_user_agent(self.config.user_agent_string());
1001 }
1002
1003 pub fn register_auth_provider(&mut self, provider: Arc<dyn AuthProvider>) -> &mut Self {
1005 self.middleware.auth.register(provider);
1006 self.ensure_auth_command();
1007 self.refresh_root_long();
1008 self
1009 }
1010
1011 #[must_use]
1013 pub fn root_command(&self) -> &Command {
1014 &self.root
1015 }
1016
1017 pub fn add_module_group(
1019 &mut self,
1020 category: impl Into<String>,
1021 group: RuntimeGroupSpec,
1022 ) -> &mut Self {
1023 let category = category.into();
1024 if !group.group.hidden {
1025 self.module_entries.push(ModuleHelpEntry {
1026 category,
1027 name: group.group.name.clone(),
1028 short: group.group.short.clone(),
1029 });
1030 }
1031
1032 let mut prefix = Vec::new();
1033 register_runtime_group_metadata(
1034 &group,
1035 &mut prefix,
1036 &mut self.middleware.schema_registry,
1037 &mut self.middleware.human_views,
1038 );
1039 let mut prefix = Vec::new();
1040 group.register_commands(&mut prefix, &mut self.commands);
1041 let mut prefix = Vec::new();
1042 let clap_group = runtime_group_clap_command_with_schema_help(
1043 &group,
1044 &mut prefix,
1045 &self.middleware.schema_registry,
1046 );
1047 self.root = self.root.clone().subcommand(clap_group);
1048 self.refresh_root_long();
1049 self
1050 }
1051
1052 pub fn add_module(&mut self, module: Module) -> &mut Self {
1054 for view in module.views.clone() {
1055 self.middleware.human_views.register(view);
1056 }
1057 self.add_guides(module.guides.clone());
1058 let mut context = ModuleContext::new(&mut self.middleware);
1059 let group = (module.register)(&mut context);
1060 let (guides, views) = context.into_parts();
1061 for view in views {
1062 self.middleware.human_views.register(view);
1063 }
1064 self.add_guides(guides);
1065 self.add_module_group(module.category, group)
1066 }
1067
1068 pub fn add_command(&mut self, command: RuntimeCommandSpec) -> &mut Self {
1070 let name = command.spec.name.clone();
1071 register_command_schema(&command.spec, &name, &mut self.middleware.schema_registry);
1072 self.commands.insert(name, command.clone());
1073 self.root = self
1074 .root
1075 .clone()
1076 .subcommand(command_clap_command_with_schema_help(
1077 &command.spec,
1078 &command.spec.name,
1079 &self.middleware.schema_registry,
1080 ));
1081 self
1082 }
1083
1084 pub fn set_has_guide(&mut self, has_guide: bool) -> &mut Self {
1086 if has_guide && self.guide_entries.is_empty() && !has_subcommand(&self.root, "guide") {
1087 self.root = self.root.clone().subcommand(guide_command());
1088 }
1089 self.refresh_root_long();
1090 self
1091 }
1092
1093 pub fn add_guides(&mut self, entries: impl IntoIterator<Item = GuideEntry>) -> &mut Self {
1095 let mut seen = self
1096 .guide_entries
1097 .iter()
1098 .map(|entry| entry.name.clone())
1099 .collect::<BTreeSet<_>>();
1100 for entry in entries {
1101 if seen.insert(entry.name.clone()) {
1102 self.guide_entries.push(entry);
1103 }
1104 }
1105 if !self.guide_entries.is_empty() && !has_subcommand(&self.root, "guide") {
1106 self.root = self.root.clone().subcommand(guide_command());
1107 }
1108 self.refresh_root_long();
1109 self
1110 }
1111
1112 async fn resolve_argv0(&self, text_args: Vec<String>, depth: usize) -> Argv0Outcome {
1121 if self.config.argv0_routes.is_empty() {
1122 return Argv0Outcome::Proceed(text_args);
1123 }
1124
1125 if depth > MAX_ARGV0_DEPTH {
1126 return Argv0Outcome::Handled(
1127 self.render_argv0_error(&text_args, "argv0 dispatch recursion limit exceeded"),
1128 );
1129 }
1130
1131 let explicit = text_args.get(1).map(String::as_str) == Some("argv0");
1136 let (name, rest) = if explicit {
1137 match text_args.get(2) {
1138 None => {
1139 return Argv0Outcome::Handled(self.render_argv0_error(
1140 &text_args,
1141 "the argv0 command requires a name to dispatch as",
1142 ));
1143 }
1144 Some(name) => (
1148 program_basename(name),
1149 text_args
1150 .get(3..)
1151 .map(<[String]>::to_vec)
1152 .unwrap_or_default(),
1153 ),
1154 }
1155 } else {
1156 let name = text_args
1157 .first()
1158 .map(|arg| program_basename(arg))
1159 .unwrap_or_default();
1160 let rest = text_args
1161 .get(1..)
1162 .map(<[String]>::to_vec)
1163 .unwrap_or_default();
1164 (name, rest)
1165 };
1166
1167 match self.config.argv0_routes.get(&name) {
1168 Some(Argv0Route::Alias(tokens)) => {
1169 let mut rewritten = Vec::with_capacity(1 + tokens.len() + rest.len());
1172 rewritten.push(self.config.name.clone());
1173 rewritten.extend(tokens.iter().cloned());
1174 rewritten.extend(rest);
1175 Argv0Outcome::Proceed(rewritten)
1176 }
1177 Some(Argv0Route::Personality(build)) => {
1178 let config = build();
1183 let bin = config.name.clone();
1184 let alt = Self::new(config);
1185 let mut alt_args = Vec::with_capacity(1 + rest.len());
1186 alt_args.push(bin);
1187 alt_args.extend(rest);
1188 Argv0Outcome::Handled(Box::pin(alt.run_with_depth(alt_args, depth + 1)).await)
1189 }
1190 None if explicit => Argv0Outcome::Handled(self.render_argv0_error(
1191 &text_args,
1192 format!(
1193 "{name:?} is not a registered argv0 name; known names: {}",
1194 self.known_argv0_names()
1195 ),
1196 )),
1197 None => {
1198 let mut rewritten = Vec::with_capacity(1 + rest.len());
1203 rewritten.push(self.config.name.clone());
1204 rewritten.extend(rest);
1205 Argv0Outcome::Proceed(rewritten)
1206 }
1207 }
1208 }
1209
1210 fn known_argv0_names(&self) -> String {
1213 self.config
1214 .argv0_routes
1215 .keys()
1216 .cloned()
1217 .collect::<Vec<_>>()
1218 .join(", ")
1219 }
1220
1221 fn render_argv0_error(&self, text_args: &[String], message: impl Into<String>) -> CliRunOutput {
1226 let mut middleware = self.middleware.clone();
1227 middleware.output_format =
1228 extract_output_format(text_args, &default_output_format(&self.config.app_id));
1229 let err = CliCoreError::message(message);
1230 self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id))
1231 }
1232
1233 #[must_use]
1238 pub fn argv0_names(&self) -> Vec<&str> {
1239 self.config
1240 .argv0_routes
1241 .keys()
1242 .map(String::as_str)
1243 .collect()
1244 }
1245
1246 pub fn create_link(
1271 &self,
1272 name: &str,
1273 dir: impl AsRef<Path>,
1274 target: Option<&Path>,
1275 method: Argv0LinkMethod,
1276 ) -> std::io::Result<PathBuf> {
1277 if !self.config.argv0_routes.contains_key(name) {
1278 return Err(std::io::Error::new(
1279 std::io::ErrorKind::InvalidInput,
1280 format!("{name:?} is not a registered argv0 name"),
1281 ));
1282 }
1283
1284 let dir = dir.as_ref();
1285 std::fs::create_dir_all(dir)?;
1286 let link = dir.join(argv0_link_file_name(name, method));
1287
1288 let resolved_target;
1290 let target = match target {
1291 Some(target) => target,
1292 None => {
1293 resolved_target = std::env::current_exe()?;
1294 resolved_target.as_path()
1295 }
1296 };
1297
1298 if std::fs::symlink_metadata(&link).is_ok() {
1302 if argv0_link_matches(&link, target, name, method)? {
1303 return Ok(link);
1304 }
1305 std::fs::remove_file(&link)?;
1306 }
1307
1308 match method {
1309 Argv0LinkMethod::SoftLink => create_symlink(target, &link)?,
1310 Argv0LinkMethod::HardLink => std::fs::hard_link(target, &link)?,
1311 Argv0LinkMethod::Script => {
1312 std::fs::write(&link, argv0_script_contents(target, name))?;
1313 make_executable(&link)?;
1314 }
1315 }
1316 Ok(link)
1317 }
1318
1319 pub async fn run<I, S>(&self, args: I) -> CliRunOutput
1321 where
1322 I: IntoIterator<Item = S>,
1323 S: Into<std::ffi::OsString> + Clone,
1324 {
1325 self.run_with_depth(args, 0).await
1326 }
1327
1328 async fn run_with_depth<I, S>(&self, args: I, depth: usize) -> CliRunOutput
1331 where
1332 I: IntoIterator<Item = S>,
1333 S: Into<std::ffi::OsString> + Clone,
1334 {
1335 let raw_args = args
1336 .into_iter()
1337 .map(Into::into)
1338 .collect::<Vec<std::ffi::OsString>>();
1339 let text_args = raw_args
1340 .iter()
1341 .map(|arg| arg.to_string_lossy().into_owned())
1342 .collect::<Vec<_>>();
1343 let text_args = match self.resolve_argv0(text_args, depth).await {
1344 Argv0Outcome::Handled(output) => return output,
1345 Argv0Outcome::Proceed(args) => args,
1346 };
1347 let mut clap_args = normalize_optional_global_flags_before_command(&self.root, &text_args);
1348 if has_root_version_flag(&text_args, &self.root, &self.config.name) {
1349 return self.finish_run(CliRunOutput {
1350 exit_code: 0,
1351 rendered: format!(
1352 "{} version {}\n",
1353 self.config.name,
1354 self.config.build.version_string()
1355 ),
1356 });
1357 }
1358 if let Some(output) = self.try_run_schema_bypass(&text_args) {
1359 return output;
1360 }
1361 if let Some(output) = self.try_run_search_bypass(&text_args) {
1362 return output;
1363 }
1364 let bool_flags = derive_bool_flags(&self.root);
1367 let value_flags = derive_value_flags(&self.root);
1368 let positionals =
1369 positional_command_tokens(&text_args, &self.config.name, &bool_flags, &value_flags);
1370 let command_keyword_count = match text_args.iter().position(|arg| arg == "--") {
1375 Some(end) => positional_command_tokens(
1376 &text_args[..end],
1377 &self.config.name,
1378 &bool_flags,
1379 &value_flags,
1380 )
1381 .len(),
1382 None => positionals.len(),
1383 };
1384 if let Some(parts) =
1385 group_help_target_parts(&self.root, &positionals, command_keyword_count)
1386 {
1387 clap_args = rewrite_group_help_args(
1394 &clap_args,
1395 &self.config.name,
1396 &bool_flags,
1397 &value_flags,
1398 &parts,
1399 );
1400 } else if let Some(message) = unknown_group_command_message(&self.root, &positionals) {
1401 return self.finish_run(CliRunOutput {
1402 exit_code: 1,
1403 rendered: message,
1404 });
1405 }
1406
1407 let matches = match self.root.clone().try_get_matches_from(clap_args) {
1408 Ok(matches) => matches,
1409 Err(err) => {
1410 return self.finish_run(CliRunOutput {
1411 exit_code: err.exit_code(),
1412 rendered: err.to_string(),
1413 });
1414 }
1415 };
1416
1417 let default_format = default_output_format(&self.config.app_id);
1418 let flags = global_flags_from_matches(&matches, &default_format);
1419 crate::config::set_credential_store_flag(flags.credential_store);
1422 let command_timeout = match parse_command_timeout(&flags.timeout) {
1423 Ok(timeout) => timeout,
1424 Err(err) => {
1425 return self.finish_run(render_cli_error(
1426 &self.middleware,
1427 &err,
1428 &self.config.app_id,
1429 ));
1430 }
1431 };
1432 let mut middleware = self.middleware.clone();
1433 apply_global_flags(&mut middleware, &flags, command_timeout);
1434 install_debug_transport_logger(&flags.debug, &self.config.redacted_debug_headers);
1435 if let Err(err) = self.apply_config_flags(&matches, &mut middleware) {
1436 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
1437 }
1438 if let Err(err) = self.apply_env_flag(&matches, &mut middleware) {
1441 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
1442 }
1443
1444 let command_path = command_path_from_matches(&self.config.name, &matches);
1445 if command_path == "help" {
1446 if let Err(err) = self.run_pre_run(&mut middleware, &command_path, &help_args(&matches))
1447 {
1448 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
1449 }
1450 return self.finish_run(self.render_help_command(&matches));
1451 }
1452 if command_path == "tree" {
1453 if let Err(err) = self.run_pre_run(
1454 &mut middleware,
1455 &command_path,
1456 &crate::middleware::ValueMap::new(),
1457 ) {
1458 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
1459 }
1460 return self.finish_run(tree_render::render_tree(
1461 &self.root,
1462 &self.config.app_id,
1463 &middleware,
1464 ));
1465 }
1466 if command_path == "guide" {
1467 if let Err(err) =
1468 self.run_pre_run(&mut middleware, &command_path, &guide_args(&matches))
1469 {
1470 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
1471 }
1472 return self.finish_run(self.render_guide(&matches));
1473 }
1474 let Some(command) = self.commands.get(&command_path) else {
1475 if !command_path.is_empty()
1476 && let Some(group) = find_command_by_colon_path(&self.root, &command_path)
1477 && group.get_subcommands().next().is_some()
1478 {
1479 if let Err(err) = self.run_pre_run(
1480 &mut middleware,
1481 &command_path,
1482 &crate::middleware::ValueMap::new(),
1483 ) {
1484 return self.finish_run(render_cli_error(
1485 &middleware,
1486 &err,
1487 &self.config.app_id,
1488 ));
1489 }
1490 return self.finish_run(CliRunOutput {
1491 exit_code: 0,
1492 rendered: group.clone().render_long_help().to_string(),
1493 });
1494 }
1495 if command_path.is_empty()
1496 && let Some(root_next_actions) = &self.root_next_actions
1497 {
1498 let actions = root_next_actions();
1503 return self.finish_run(self.render_root(&middleware, actions));
1504 }
1505 return self.finish_run(CliRunOutput {
1506 exit_code: if command_path.is_empty() { 0 } else { 1 },
1507 rendered: if command_path.is_empty() {
1508 self.root.clone().render_long_help().to_string()
1509 } else {
1510 format!("unknown command {command_path:?}")
1511 },
1512 });
1513 };
1514
1515 let mut middleware = match self.initialized_middleware() {
1516 Ok(middleware) => middleware,
1517 Err(err) => {
1518 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
1519 }
1520 };
1521 apply_global_flags(&mut middleware, &flags, command_timeout);
1522 install_debug_transport_logger(&flags.debug, &self.config.redacted_debug_headers);
1523 if let Err(err) = self.apply_config_flags(&matches, &mut middleware) {
1524 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
1525 }
1526 if let Err(err) = self.apply_env_flag(&matches, &mut middleware) {
1529 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
1530 }
1531
1532 let leaf = leaf_matches(&matches);
1533 let args = command_args_from_matches(leaf, &command.spec, false);
1534 let user_args = command_args_from_matches(leaf, &command.spec, true);
1535 if let Err(err) = self.run_pre_run(&mut middleware, &command_path, &args) {
1536 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
1537 }
1538 let meta = self.resolve_meta(&command_path, command.spec.metadata());
1539 let default_fields = command.spec.default_fields.clone().unwrap_or_default();
1540 let system = command.spec.system.clone().unwrap_or_default();
1541 let view_id = command
1546 .spec
1547 .view_id
1548 .clone()
1549 .or_else(|| (!command.spec.view_columns.is_empty()).then(|| command_path.clone()));
1550
1551 if let Some(streaming_handler) = command.streaming_handler.clone() {
1552 let result = run_with_timeout(
1553 command_timeout,
1554 &flags.timeout,
1555 run_streaming_command(
1556 &middleware,
1557 MiddlewareRequest {
1558 meta,
1559 command_path: &command_path,
1560 system: &system,
1561 user_args,
1562 args,
1563 default_fields: &default_fields,
1564 view_id: view_id.as_deref(),
1565 auth: command.spec.auth,
1566 },
1567 Arc::new(leaf.clone()),
1568 streaming_handler,
1569 ),
1570 )
1571 .await;
1572 return self.finish_run(match result {
1573 Ok(output) => output,
1574 Err(err) => render_cli_error(&middleware, &err, &self.config.app_id),
1575 });
1576 }
1577
1578 let handler = command.handler.clone();
1579 let args_for_handler = args.clone();
1580 let user_args_for_handler = user_args.clone();
1581 let handler_path = command_path.clone();
1582 let middleware_for_handler = middleware.clone();
1583 let raw_matches_for_handler = Arc::new(leaf.clone());
1584 let result = run_with_timeout(
1585 command_timeout,
1586 &flags.timeout,
1587 middleware.run(
1588 MiddlewareRequest {
1589 meta,
1590 command_path: &command_path,
1591 system: &system,
1592 user_args,
1593 args,
1594 default_fields: &default_fields,
1595 view_id: view_id.as_deref(),
1596 auth: command.spec.auth,
1597 },
1598 async move |credential| {
1599 handler(CommandContext {
1600 credential,
1601 args: args_for_handler,
1602 user_args: user_args_for_handler,
1603 command_path: handler_path,
1604 middleware: middleware_for_handler,
1605 raw_matches: raw_matches_for_handler,
1606 })
1607 .await
1608 },
1609 ),
1610 )
1611 .await;
1612
1613 match result {
1614 Ok(output) => self.finish_run(output.into()),
1615 Err(err) => self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id)),
1616 }
1617 }
1618
1619 fn try_run_search_bypass(&self, args: &[String]) -> Option<CliRunOutput> {
1620 let query = extract_search_query(args);
1621 if query.is_empty() {
1622 return None;
1623 }
1624 let scope = self.search_scope(args);
1625 let output_format =
1626 extract_output_format(args, &default_output_format(&self.config.app_id));
1627 Some(self.render_search(&query, &scope, &output_format))
1628 }
1629
1630 fn try_run_schema_bypass(&self, args: &[String]) -> Option<CliRunOutput> {
1631 if !has_true_schema_flag(args) {
1632 return None;
1633 }
1634 let bool_flags = derive_bool_flags(&self.root);
1635 let value_flags = derive_value_flags(&self.root);
1636 let command_path =
1637 self.canonical_command_path(&extract_command_path(args, &bool_flags, &value_flags));
1638 let command = find_command_by_colon_path(&self.root, &command_path)?;
1643 if command.get_subcommands().next().is_some() {
1644 return None;
1645 }
1646 let output_format =
1647 extract_output_format(args, &default_output_format(&self.config.app_id));
1648 match self.middleware.schema_registry.get_by_path(&command_path) {
1652 Some(schema) => Some(self.render_schema(schema, &output_format)),
1653 None => Some(self.render_schema(
1654 crate::output::no_schema_response(&command_path),
1655 &output_format,
1656 )),
1657 }
1658 }
1659
1660 fn render_schema(&self, data: impl serde::Serialize, output_format: &str) -> CliRunOutput {
1661 let format: crate::output::OutputFormat = match output_format.parse() {
1662 Ok(format) => format,
1663 Err(err) => {
1664 return CliRunOutput {
1665 exit_code: exit_code_for_error(&err),
1666 rendered: err.to_string(),
1667 };
1668 }
1669 };
1670 let envelope =
1671 crate::Envelope::success(data, self.config.app_id.clone()).prepare_for_render("");
1672 match crate::output::render(format, &envelope) {
1673 Ok(rendered) => CliRunOutput {
1674 exit_code: 0,
1675 rendered,
1676 },
1677 Err(err) => CliRunOutput {
1678 exit_code: exit_code_for_error(&err),
1679 rendered: err.to_string(),
1680 },
1681 }
1682 }
1683
1684 fn render_search(&self, query: &str, scope: &str, output_format: &str) -> CliRunOutput {
1685 let format: crate::output::OutputFormat = match output_format.parse() {
1686 Ok(format) => format,
1687 Err(err) => {
1688 return CliRunOutput {
1689 exit_code: exit_code_for_error(&err),
1690 rendered: err.to_string(),
1691 };
1692 }
1693 };
1694 let docs = self.search_documents(scope);
1695 let results = SearchIndex::new(docs).search(query, 10);
1696 let envelope =
1697 crate::Envelope::success(results, self.config.app_id.clone()).prepare_for_render("");
1698 match crate::output::render(format, &envelope) {
1699 Ok(rendered) => CliRunOutput {
1700 exit_code: 0,
1701 rendered,
1702 },
1703 Err(err) => CliRunOutput {
1704 exit_code: exit_code_for_error(&err),
1705 rendered: err.to_string(),
1706 },
1707 }
1708 }
1709
1710 fn render_root(&self, middleware: &Middleware, actions: Vec<NextAction>) -> CliRunOutput {
1716 if !crate::output::is_valid_output_format(&middleware.output_format) {
1721 let err = CliCoreError::InvalidOutputFormat(middleware.output_format.clone());
1722 return CliRunOutput {
1723 exit_code: exit_code_for_error(&err),
1724 rendered: err.to_string(),
1725 };
1726 }
1727 let format = middleware
1728 .output_format
1729 .parse()
1730 .unwrap_or(crate::output::OutputFormat::Json);
1731 if format == crate::output::OutputFormat::Human {
1732 let base_long = self
1736 .root
1737 .get_long_about()
1738 .map(ToString::to_string)
1739 .unwrap_or_default();
1740 let long = format!("{base_long}{}", render_next_actions_human(&actions));
1741 let rendered = self
1742 .root
1743 .clone()
1744 .long_about(long)
1745 .render_long_help()
1746 .to_string();
1747 return CliRunOutput {
1748 exit_code: 0,
1749 rendered,
1750 };
1751 }
1752 let description = self
1753 .config
1754 .long
1755 .as_deref()
1756 .filter(|long| !long.is_empty())
1757 .unwrap_or(self.config.short.as_str());
1758 let data = serde_json::json!({
1759 "description": description,
1760 "version": self.config.build.version,
1761 });
1762 let envelope = crate::Envelope::success(data, self.config.app_id.clone())
1763 .with_next_actions(actions)
1764 .prepare_for_render(&middleware.verbose);
1765 match crate::output::render(format, &envelope) {
1766 Ok(rendered) => CliRunOutput {
1767 exit_code: 0,
1768 rendered,
1769 },
1770 Err(err) => CliRunOutput {
1771 exit_code: exit_code_for_error(&err),
1772 rendered: err.to_string(),
1773 },
1774 }
1775 }
1776
1777 fn search_documents(&self, scope: &str) -> Vec<SearchDocument> {
1778 let (scoped, mut prefix) = find_command_and_canonical_path_by_colon_path(&self.root, scope)
1779 .unwrap_or((&self.root, Vec::new()));
1780 let mut docs = Vec::new();
1781 let mut aliases = Vec::new();
1782 append_command_alias_terms(scoped, &mut aliases);
1783 collect_command_search_documents(scoped, &mut prefix, &mut aliases, &mut docs);
1784 if scope.is_empty() {
1785 for entry in &self.guide_entries {
1786 docs.push(SearchDocument {
1787 id: format!("guide:{}", entry.name),
1788 kind: "guide".to_owned(),
1789 title: format!("guide {}", entry.name),
1790 summary: entry.summary.clone(),
1791 content: format!("{} {}", entry.summary, entry.content),
1792 });
1793 }
1794 if let Some(extra_search_docs) = &self.extra_search_docs {
1795 docs.extend(extra_search_docs());
1796 }
1797 }
1798 docs
1799 }
1800
1801 fn search_scope(&self, args: &[String]) -> String {
1802 let parts = extract_search_scope_parts(args);
1803 canonical_path_from_parts(&self.root, &parts).unwrap_or_default()
1804 }
1805
1806 fn canonical_command_path(&self, command_path: &str) -> String {
1807 find_command_and_canonical_path_by_colon_path(&self.root, command_path).map_or_else(
1808 || command_path.to_owned(),
1809 |(_, canonical)| canonical.join(":"),
1810 )
1811 }
1812
1813 fn render_guide(&self, matches: &ArgMatches) -> CliRunOutput {
1814 let leaf = leaf_matches(matches);
1815 let topic = leaf.get_one::<String>("topic").map(String::as_str);
1816 match guide_content(&self.guide_entries, topic) {
1817 Ok(rendered) => CliRunOutput {
1818 exit_code: 0,
1819 rendered,
1820 },
1821 Err(err) => CliRunOutput {
1822 exit_code: 1,
1823 rendered: err,
1824 },
1825 }
1826 }
1827
1828 fn render_help_command(&self, matches: &ArgMatches) -> CliRunOutput {
1829 let leaf = leaf_matches(matches);
1830 let parts = leaf
1831 .get_many::<String>("command")
1832 .map(|values| values.map(String::as_str).collect::<Vec<_>>())
1833 .unwrap_or_default();
1834 self.render_help_for_parts(&parts)
1835 }
1836
1837 fn render_help_for_parts(&self, parts: &[&str]) -> CliRunOutput {
1844 if parts.is_empty() {
1845 return CliRunOutput {
1846 exit_code: 0,
1847 rendered: self.root.clone().render_long_help().to_string(),
1848 };
1849 }
1850 let Some(command) = find_help_target(&self.root, parts) else {
1851 return CliRunOutput {
1852 exit_code: 1,
1853 rendered: format!(
1854 "unknown command {:?} — run '{} help' for available commands",
1855 parts.join(" "),
1856 self.config.name
1857 ),
1858 };
1859 };
1860 CliRunOutput {
1861 exit_code: 0,
1862 rendered: command.clone().render_long_help().to_string(),
1863 }
1864 }
1865
1866 fn refresh_root_long(&mut self) {
1867 const BUILTINS: [&str; 4] = ["help", "guide", "tree", "completion"];
1872 let categorized: BTreeSet<&str> = self
1873 .module_entries
1874 .iter()
1875 .map(|entry| entry.name.as_str())
1876 .collect();
1877 let mut generic: Vec<ModuleHelpEntry> = self
1878 .root
1879 .get_subcommands()
1880 .filter(|command| !command.is_hide_set())
1881 .filter(|command| !BUILTINS.contains(&command.get_name()))
1882 .filter(|command| !categorized.contains(command.get_name()))
1883 .map(|command| ModuleHelpEntry {
1884 category: "Commands".to_owned(),
1885 name: command.get_name().to_owned(),
1886 short: command
1887 .get_about()
1888 .map(ToString::to_string)
1889 .unwrap_or_default(),
1890 })
1891 .collect();
1892 generic.sort_by(|left, right| left.name.cmp(&right.name));
1893
1894 let mut entries = self.module_entries.clone();
1895 entries.extend(generic);
1896 let has_guide = !self.guide_entries.is_empty() || has_subcommand(&self.root, "guide");
1897 let intro = self
1898 .config
1899 .long
1900 .as_deref()
1901 .filter(|long| !long.is_empty())
1902 .unwrap_or(self.config.short.as_str());
1903 self.root = self
1904 .root
1905 .clone()
1906 .long_about(build_root_long(intro, &entries, has_guide));
1907 }
1908
1909 fn ensure_auth_command(&mut self) {
1910 let default_provider = self.default_auth_provider();
1911 let registered_names = self.middleware.auth.registered_names();
1912 if default_provider.is_empty() && registered_names.is_empty() {
1913 return;
1914 }
1915 let replacing_builtin = self.commands.contains_key("auth:login");
1916 if has_subcommand(&self.root, "auth") && !replacing_builtin {
1917 return;
1918 }
1919 let group = auth_command_group(&default_provider, ®istered_names);
1920 let mut prefix = Vec::new();
1921 group.register_commands(&mut prefix, &mut self.commands);
1922 let mut prefix = Vec::new();
1923 let clap_group = runtime_group_clap_command_with_schema_help(
1924 &group,
1925 &mut prefix,
1926 &self.middleware.schema_registry,
1927 );
1928 self.root = if replacing_builtin {
1929 self.root.clone().mut_subcommand("auth", |_| clap_group)
1930 } else {
1931 self.root.clone().subcommand(clap_group)
1932 };
1933 self.register_auth_help_entry();
1937 }
1938
1939 fn ensure_config_command(&mut self) {
1943 if has_subcommand(&self.root, "config") {
1944 return;
1945 }
1946 let group = crate::config_commands::config_command_group();
1947 let mut prefix = Vec::new();
1948 group.register_commands(&mut prefix, &mut self.commands);
1949 let mut prefix = Vec::new();
1950 let clap_group = runtime_group_clap_command_with_schema_help(
1951 &group,
1952 &mut prefix,
1953 &self.middleware.schema_registry,
1954 );
1955 self.root = self.root.clone().subcommand(clap_group);
1956 let category = self
1957 .config
1958 .admin_category
1959 .clone()
1960 .unwrap_or_else(|| DEFAULT_ADMIN_CATEGORY.to_owned());
1961 if !self
1962 .module_entries
1963 .iter()
1964 .any(|entry| entry.name == "config")
1965 {
1966 self.module_entries.push(ModuleHelpEntry {
1967 category,
1968 name: "config".to_owned(),
1969 short: "Read and write the CLI config file".to_owned(),
1970 });
1971 }
1972 self.refresh_root_long();
1973 }
1974
1975 fn ensure_env_command(&mut self) {
1979 if has_subcommand(&self.root, "env") {
1980 return;
1981 }
1982 let group = crate::env_commands::env_command_group();
1983 let mut prefix = Vec::new();
1984 group.register_commands(&mut prefix, &mut self.commands);
1985 let mut prefix = Vec::new();
1986 let clap_group = runtime_group_clap_command_with_schema_help(
1987 &group,
1988 &mut prefix,
1989 &self.middleware.schema_registry,
1990 );
1991 self.root = self.root.clone().subcommand(clap_group);
1992 let category = self
1993 .config
1994 .admin_category
1995 .clone()
1996 .unwrap_or_else(|| DEFAULT_ADMIN_CATEGORY.to_owned());
1997 if !self.module_entries.iter().any(|e| e.name == "env") {
1998 self.module_entries.push(ModuleHelpEntry {
1999 category,
2000 name: "env".to_owned(),
2001 short: "Manage the active environment".to_owned(),
2002 });
2003 }
2004 self.refresh_root_long();
2005 }
2006
2007 fn default_auth_provider(&self) -> String {
2008 if !self.middleware.default_auth_provider.is_empty() {
2009 return self.middleware.default_auth_provider.clone();
2010 }
2011 self.middleware
2012 .auth
2013 .registered_names()
2014 .into_iter()
2015 .next()
2016 .unwrap_or_default()
2017 }
2018
2019 fn initialized_middleware(&self) -> Result<Middleware> {
2020 let Some(init_deps) = &self.init_deps else {
2021 return Ok(self.middleware.clone());
2022 };
2023 let mut guard = self
2024 .init_state
2025 .lock()
2026 .map_err(|_| CliCoreError::message("init deps lock poisoned"))?;
2027 if let Some(result) = guard.as_ref() {
2028 return result.clone().map_err(InitFailure::into_error);
2029 }
2030 let mut middleware = self.middleware.clone();
2031 let result = init_deps(&mut middleware)
2032 .map(|()| middleware)
2033 .map_err(|err| InitFailure::capture(&err));
2034 *guard = Some(result.clone());
2035 result.map_err(InitFailure::into_error)
2036 }
2037
2038 fn apply_config_flags(&self, matches: &ArgMatches, middleware: &mut Middleware) -> Result<()> {
2039 if let Some(apply_flags) = &self.apply_flags {
2040 apply_flags(matches, middleware)?;
2041 }
2042 Ok(())
2043 }
2044
2045 fn apply_env_flag(&self, matches: &ArgMatches, middleware: &mut Middleware) -> Result<()> {
2052 let Some(environments) = middleware.environments.as_ref() else {
2058 return Ok(());
2059 };
2060 if let Some(env) = matches.get_one::<String>("env") {
2061 environments.resolve(env)?;
2062 middleware.env = env.clone();
2063 }
2064 Ok(())
2065 }
2066
2067 fn run_pre_run(
2068 &self,
2069 middleware: &mut Middleware,
2070 command_path: &str,
2071 args: &crate::middleware::ValueMap,
2072 ) -> Result<()> {
2073 if let Some(pre_run) = &self.pre_run {
2074 pre_run(middleware, command_path, args)?;
2075 }
2076 Ok(())
2077 }
2078
2079 fn resolve_meta(&self, command_path: &str, meta: CommandMeta) -> CommandMeta {
2080 if let Some(resolver) = &self.meta_resolver {
2081 resolver(command_path, meta)
2082 } else {
2083 meta
2084 }
2085 }
2086
2087 fn finish_run(&self, output: CliRunOutput) -> CliRunOutput {
2088 crate::config::clear_credential_store_flag();
2091 if let Some(on_shutdown) = &self.on_shutdown {
2092 on_shutdown();
2093 }
2094 output
2095 }
2096}
2097
2098fn apply_global_flags(middleware: &mut Middleware, flags: &GlobalFlags, timeout: Option<Duration>) {
2099 middleware.output_format = flags.output_format.clone();
2100 middleware.verbose = flags.verbose.clone();
2101 middleware.dry_run = flags.dry_run;
2102 middleware.fields = flags.fields.clone();
2103 middleware.filter = flags.filter.clone();
2104 middleware.expr = flags.expr.clone();
2105 middleware.limit = flags.limit;
2106 middleware.offset = flags.offset;
2107 middleware.reason = flags.reason.clone();
2108 middleware.schema = flags.schema;
2109 middleware.timeout = timeout;
2110 middleware.debug = flags.debug.clone();
2111 middleware.search = flags.search.clone();
2112}
2113
2114fn install_debug_transport_logger(debug: &str, extra_redacted: &[String]) {
2125 let logger: Arc<dyn crate::transport::TransportLogger> =
2126 if crate::debug_component_enabled(debug, "transport") {
2127 Arc::new(
2128 crate::transport::StderrTransportLogger::new()
2129 .with_redacted_headers(extra_redacted.iter().cloned()),
2130 )
2131 } else {
2132 Arc::new(crate::transport::NoopTransportLogger)
2133 };
2134 crate::transport::set_default_transport_logger(logger);
2135}
2136
2137async fn run_with_timeout<F, T>(
2138 timeout: Option<Duration>,
2139 timeout_label: &str,
2140 future: F,
2141) -> Result<T>
2142where
2143 F: Future<Output = Result<T>>,
2144{
2145 let Some(timeout) = timeout else {
2146 return future.await;
2147 };
2148 match tokio::time::timeout(timeout, future).await {
2149 Ok(result) => result,
2150 Err(_) => Err(CliCoreError::message(format!(
2151 "command timed out after {timeout_label}"
2152 ))),
2153 }
2154}
2155
2156async fn run_until_signal<Run, Shutdown>(run: Run, shutdown: Shutdown) -> CliRunOutput
2157where
2158 Run: Future<Output = CliRunOutput>,
2159 Shutdown: Future<Output = ()>,
2160{
2161 tokio::pin!(run);
2162 tokio::pin!(shutdown);
2163 tokio::select! {
2164 output = &mut run => output,
2165 () = &mut shutdown => CliRunOutput {
2166 exit_code: 130,
2167 rendered: "command interrupted\n".to_owned(),
2168 },
2169 }
2170}
2171
2172#[cfg(unix)]
2173async fn shutdown_signal() {
2174 let ctrl_c = tokio::signal::ctrl_c();
2175 match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
2176 Ok(mut sigterm) => {
2177 tokio::select! {
2178 _ = ctrl_c => {},
2179 _ = sigterm.recv() => {},
2180 }
2181 }
2182 Err(_) => {
2183 drop(ctrl_c.await);
2184 }
2185 }
2186}
2187
2188#[cfg(not(unix))]
2189async fn shutdown_signal() {
2190 drop(tokio::signal::ctrl_c().await);
2191}
2192
2193fn parse_command_timeout(raw: &str) -> Result<Option<Duration>> {
2194 let raw = raw.trim();
2195 if raw.is_empty() {
2196 return Ok(Some(Duration::from_secs(60)));
2197 }
2198 let Some(seconds) = parse_duration_seconds(raw) else {
2199 return Err(CliCoreError::message(format!(
2200 "invalid timeout {raw:?}: expected duration like 60s, 5m, or 0s"
2201 )));
2202 };
2203 if seconds <= 0.0 {
2204 Ok(None)
2205 } else {
2206 Ok(Some(Duration::from_secs_f64(seconds)))
2207 }
2208}
2209
2210fn parse_duration_seconds(raw: &str) -> Option<f64> {
2211 for (suffix, seconds) in [
2212 ("ns", 0.000_000_001_f64),
2213 ("us", 0.000_001_f64),
2214 ("µs", 0.000_001_f64),
2215 ("ms", 0.001_f64),
2216 ("s", 1.0_f64),
2217 ("m", 60.0_f64),
2218 ("h", 3600.0_f64),
2219 ] {
2220 if let Some(number) = raw.strip_suffix(suffix) {
2221 let value = number.parse::<f64>().ok()?;
2222 if !value.is_finite() {
2223 return None;
2224 }
2225 return Some(value * seconds);
2226 }
2227 }
2228 None
2229}
2230
2231fn render_cli_error(
2232 middleware: &Middleware,
2233 err: &(dyn std::error::Error + 'static),
2234 system: &str,
2235) -> CliRunOutput {
2236 let format = middleware
2237 .output_format
2238 .parse::<crate::output::OutputFormat>()
2239 .unwrap_or(crate::output::OutputFormat::Json);
2240 let envelope =
2241 crate::output::build_error_envelope(err, system).prepare_for_render(&middleware.verbose);
2242 match crate::output::render(format, &envelope) {
2243 Ok(rendered) => CliRunOutput {
2244 exit_code: exit_code_for_error(err),
2245 rendered,
2246 },
2247 Err(render_err) => CliRunOutput {
2248 exit_code: exit_code_for_error(err),
2249 rendered: render_err.to_string(),
2250 },
2251 }
2252}
2253
2254fn find_command_by_colon_path<'command>(
2255 root: &'command Command,
2256 path: &str,
2257) -> Option<&'command Command> {
2258 find_command_and_canonical_path_by_colon_path(root, path).map(|(command, _)| command)
2259}
2260
2261fn find_help_target<'command>(
2262 root: &'command Command,
2263 parts: &[&str],
2264) -> Option<&'command Command> {
2265 let mut current = root;
2266 let mut matched_any = false;
2267 for part in parts {
2268 let Some(next) = current.find_subcommand(part) else {
2269 break;
2270 };
2271 current = next;
2272 matched_any = true;
2273 }
2274 matched_any.then_some(current)
2275}
2276
2277fn find_command_and_canonical_path_by_colon_path<'command>(
2278 root: &'command Command,
2279 path: &str,
2280) -> Option<(&'command Command, Vec<String>)> {
2281 if path.is_empty() {
2282 return Some((root, Vec::new()));
2283 }
2284 let mut current = root;
2285 let mut canonical = Vec::new();
2286 for part in path.split(':') {
2287 current = current.find_subcommand(part)?;
2288 canonical.push(current.get_name().to_owned());
2289 }
2290 Some((current, canonical))
2291}
2292
2293fn canonical_path_from_parts(root: &Command, parts: &[String]) -> Option<String> {
2294 if parts.is_empty() {
2295 return Some(String::new());
2296 }
2297 let mut current = root;
2298 let mut canonical = Vec::new();
2299 for part in parts {
2300 current = current.find_subcommand(part)?;
2301 canonical.push(current.get_name().to_owned());
2302 }
2303 Some(canonical.join(":"))
2304}
2305
2306fn extract_search_scope_parts(args: &[String]) -> Vec<String> {
2307 let mut parts = Vec::new();
2308 let mut index = 1;
2309 while index < args.len() {
2310 let arg = &args[index];
2311 if arg == "--search" || arg.starts_with("--search=") {
2312 break;
2313 }
2314 if arg.starts_with('-') {
2315 if !arg.contains('=') && index + 1 < args.len() && !args[index + 1].starts_with('-') {
2316 index += 2;
2317 } else {
2318 index += 1;
2319 }
2320 continue;
2321 }
2322 parts.push(arg.clone());
2323 index += 1;
2324 }
2325 parts
2326}
2327
2328fn collect_command_search_documents(
2329 command: &Command,
2330 prefix: &mut Vec<String>,
2331 aliases: &mut Vec<String>,
2332 docs: &mut Vec<SearchDocument>,
2333) {
2334 if command.is_hide_set() || command.get_name() == "completion" {
2335 return;
2336 }
2337 if command.get_subcommands().next().is_some() {
2338 for child in command.get_subcommands() {
2339 prefix.push(child.get_name().to_owned());
2340 let alias_len = aliases.len();
2341 append_command_alias_terms(child, aliases);
2342 collect_command_search_documents(child, prefix, aliases, docs);
2343 aliases.truncate(alias_len);
2344 prefix.pop();
2345 }
2346 return;
2347 }
2348 if prefix.is_empty() {
2349 prefix.push(command.get_name().to_owned());
2350 append_command_alias_terms(command, aliases);
2351 }
2352 let path = prefix.join(" ");
2353 let alias_text = aliases.join(" ");
2354 docs.push(SearchDocument {
2355 id: format!("cmd:{path}"),
2356 kind: "command".to_owned(),
2357 title: path,
2358 summary: command
2359 .get_about()
2360 .map(ToString::to_string)
2361 .unwrap_or_default(),
2362 content: format!(
2363 "{} {} {} {}",
2364 command
2365 .get_about()
2366 .map(ToString::to_string)
2367 .unwrap_or_default(),
2368 command
2369 .get_long_about()
2370 .map(ToString::to_string)
2371 .unwrap_or_default(),
2372 command_flag_text(command),
2373 alias_text
2374 ),
2375 });
2376 if prefix.len() == 1 && prefix[0] == command.get_name() {
2377 prefix.pop();
2378 }
2379}
2380
2381fn append_command_alias_terms(command: &Command, aliases: &mut Vec<String>) {
2382 aliases.extend(command.get_all_aliases().map(str::to_owned));
2383 aliases.extend(
2384 command
2385 .get_all_short_flag_aliases()
2386 .map(|alias| alias.to_string()),
2387 );
2388 aliases.extend(command.get_all_long_flag_aliases().map(str::to_owned));
2389}
2390
2391fn command_flag_text(command: &Command) -> String {
2392 command
2393 .get_arguments()
2394 .filter_map(|arg| {
2395 let mut names = Vec::new();
2396 if let Some(short) = arg.get_short() {
2397 names.push(format!("-{short}"));
2398 }
2399 if let Some(long) = arg.get_long() {
2400 names.push(format!("--{long}"));
2401 }
2402 if let Some(short_aliases) = arg.get_all_short_aliases() {
2403 names.extend(
2404 short_aliases
2405 .into_iter()
2406 .map(|short_alias| format!("-{short_alias}")),
2407 );
2408 }
2409 if let Some(aliases) = arg.get_all_aliases() {
2410 names.extend(aliases.into_iter().map(|alias| format!("--{alias}")));
2411 }
2412 (!names.is_empty()).then(|| names.join(" "))
2413 })
2414 .collect::<Vec<_>>()
2415 .join(" ")
2416}
2417
2418fn has_subcommand(command: &Command, name: &str) -> bool {
2419 command
2420 .get_subcommands()
2421 .any(|child| child.get_name() == name)
2422}
2423
2424fn has_root_version_flag(args: &[String], root: &Command, root_name: &str) -> bool {
2425 let bool_flags = derive_bool_flags(root);
2426 let value_flags = derive_value_flags(root);
2427 let mut iter = args.iter().peekable();
2428 if iter
2429 .peek()
2430 .is_some_and(|arg| arg_matches_root_name(arg, root_name))
2431 {
2432 iter.next();
2433 }
2434
2435 while let Some(arg) = iter.next() {
2436 match arg.as_str() {
2437 "--version" | "-v" => return true,
2438 "--" => return false,
2439 value if value.contains('=') || bool_flags.contains(value) => continue,
2440 value
2441 if value_flags.contains(value)
2442 || unknown_flag_consumes_value(value, iter.peek()) =>
2443 {
2444 iter.next();
2445 }
2446 value if value.starts_with('-') => {}
2447 _ => return false,
2448 }
2449 }
2450 false
2451}
2452
2453fn normalize_optional_global_flags_before_command(root: &Command, args: &[String]) -> Vec<String> {
2454 let optional_string_defaults = BTreeMap::from([("--verbose", "all"), ("--debug", "*")]);
2455 let optional_bool_defaults = BTreeMap::from([("--dry-run", "true"), ("--schema", "true")]);
2456 let mut normalized = Vec::with_capacity(args.len());
2457 let mut index = 0;
2458 let mut current = root;
2459 while index < args.len() {
2460 let arg = &args[index];
2461 if index == 0 && arg_matches_root_name(arg, root.get_name()) {
2462 normalized.push(arg.clone());
2463 index += 1;
2464 continue;
2465 }
2466
2467 if let Some(default) = optional_bool_defaults.get(arg.as_str()) {
2468 normalized.push(format!("{arg}={default}"));
2469 index += 1;
2470 continue;
2471 }
2472
2473 if let Some(default) = optional_string_defaults.get(arg.as_str()) {
2474 match args.get(index + 1) {
2475 None => {
2476 normalized.push(format!("{arg}={default}"));
2477 index += 1;
2478 continue;
2479 }
2480 Some(next)
2481 if current.get_name() == root.get_name()
2482 || next.starts_with('-')
2483 || direct_subcommand(current, next).is_some() =>
2484 {
2485 normalized.push(format!("{arg}={default}"));
2486 index += 1;
2487 continue;
2488 }
2489 Some(next) => {
2490 normalized.push(arg.clone());
2491 normalized.push(next.clone());
2492 index += 2;
2493 continue;
2494 }
2495 }
2496 }
2497
2498 normalized.push(arg.clone());
2499 if !arg.starts_with('-')
2500 && let Some(next_command) = direct_subcommand(current, arg)
2501 {
2502 current = next_command;
2503 }
2504 index += 1;
2505 }
2506 normalized
2507}
2508
2509fn direct_subcommand<'command>(
2510 command: &'command Command,
2511 token: &str,
2512) -> Option<&'command Command> {
2513 command.get_subcommands().find(|child| {
2514 child.get_name() == token || child.get_all_aliases().any(|alias| alias == token)
2515 })
2516}
2517
2518fn unknown_group_command_message(root: &Command, positionals: &[String]) -> Option<String> {
2519 if positionals.is_empty() {
2520 return None;
2521 }
2522
2523 let mut current = root;
2524 let mut path = vec![root.get_name().to_owned()];
2525 for token in positionals {
2526 if let Some(next) = current.find_subcommand(token) {
2527 current = next;
2528 path.push(next.get_name().to_owned());
2529 continue;
2530 }
2531 if current.get_subcommands().next().is_some() {
2532 return Some(format!(
2533 "unknown command {token:?} for {:?}",
2534 path.join(" ")
2535 ));
2536 }
2537 return None;
2538 }
2539 None
2540}
2541
2542fn group_help_target_parts(
2565 root: &Command,
2566 positionals: &[String],
2567 command_keyword_count: usize,
2568) -> Option<Vec<String>> {
2569 let help_index = positionals.iter().position(|token| token == "help")?;
2570 if help_index == 0 {
2572 return None;
2573 }
2574 if help_index >= command_keyword_count {
2576 return None;
2577 }
2578 let prefix = &positionals[..help_index];
2579 let mut current = root;
2580 for token in prefix {
2581 current = current.find_subcommand(token)?;
2582 }
2583 current.get_subcommands().next()?;
2585 if current.find_subcommand("help").is_some() {
2587 return None;
2588 }
2589 let suffix = &positionals[help_index + 1..];
2591 Some(prefix.iter().chain(suffix).cloned().collect())
2592}
2593
2594fn rewrite_group_help_args(
2605 clap_args: &[String],
2606 root_name: &str,
2607 bool_flags: &BTreeSet<String>,
2608 value_flags: &BTreeSet<String>,
2609 parts: &[String],
2610) -> Vec<String> {
2611 let mut next_positional = std::iter::once("help".to_owned())
2613 .chain(parts.iter().cloned())
2614 .peekable();
2615 let mut out = Vec::with_capacity(clap_args.len());
2616 let mut iter = clap_args.iter().peekable();
2617 if iter
2618 .peek()
2619 .is_some_and(|arg| arg_matches_root_name(arg, root_name))
2620 && let Some(program) = iter.next()
2621 {
2622 out.push(program.clone());
2623 }
2624
2625 let mut take_positional =
2626 |fallback: &String| next_positional.next().unwrap_or(fallback.clone());
2627
2628 while let Some(arg) = iter.next() {
2629 if arg == "--" {
2630 out.push(arg.clone());
2631 for rest in iter.by_ref() {
2633 out.push(take_positional(rest));
2634 }
2635 break;
2636 }
2637 if arg.contains('=') || bool_flags.contains(arg) {
2638 out.push(arg.clone());
2639 continue;
2640 }
2641 if value_flags.contains(arg) || unknown_flag_consumes_value(arg, iter.peek()) {
2642 out.push(arg.clone());
2643 if let Some(value) = iter.next() {
2644 out.push(value.clone());
2645 }
2646 continue;
2647 }
2648 if arg.starts_with('-') {
2649 out.push(arg.clone());
2650 continue;
2651 }
2652 out.push(take_positional(arg));
2653 }
2654 out.extend(next_positional);
2656 out
2657}
2658
2659fn positional_command_tokens(
2660 args: &[String],
2661 root_name: &str,
2662 bool_flags: &BTreeSet<String>,
2663 value_flags: &BTreeSet<String>,
2664) -> Vec<String> {
2665 let mut tokens = Vec::new();
2666 let mut iter = args.iter().peekable();
2667 if iter
2668 .peek()
2669 .is_some_and(|arg| arg_matches_root_name(arg, root_name))
2670 {
2671 iter.next();
2672 }
2673
2674 while let Some(arg) = iter.next() {
2675 if arg == "--" {
2676 tokens.extend(iter.cloned());
2677 break;
2678 }
2679 if arg.contains('=') {
2680 continue;
2681 }
2682 if bool_flags.contains(arg) {
2683 continue;
2684 }
2685 if value_flags.contains(arg) || unknown_flag_consumes_value(arg, iter.peek()) {
2686 iter.next();
2687 continue;
2688 }
2689 if arg.starts_with('-') {
2690 continue;
2691 }
2692 tokens.push(arg.clone());
2693 }
2694 tokens
2695}
2696
2697fn unknown_flag_consumes_value(arg: &str, next: Option<&&String>) -> bool {
2698 arg.starts_with('-') && next.is_some_and(|value| !value.starts_with('-'))
2699}
2700
2701fn arg_matches_root_name(arg: &str, root_name: &str) -> bool {
2702 arg == root_name
2703 || Path::new(arg)
2704 .file_stem()
2705 .and_then(|n| n.to_str())
2706 .is_some_and(|n| n == root_name)
2707}
2708
2709enum Argv0Outcome {
2712 Proceed(Vec<String>),
2714 Handled(CliRunOutput),
2716}
2717
2718fn program_basename(arg: &str) -> String {
2722 Path::new(arg)
2723 .file_stem()
2724 .and_then(|stem| stem.to_str())
2725 .map_or_else(|| arg.to_owned(), ToOwned::to_owned)
2726}
2727
2728fn is_valid_argv0_name(name: &str) -> bool {
2733 !name.is_empty()
2734 && name.chars().all(|character| {
2735 character.is_ascii_alphanumeric() || character == '-' || character == '_'
2736 })
2737}
2738
2739fn argv0_link_matches(
2744 link: &Path,
2745 target: &Path,
2746 name: &str,
2747 method: Argv0LinkMethod,
2748) -> std::io::Result<bool> {
2749 let metadata = std::fs::symlink_metadata(link)?;
2750 match method {
2751 Argv0LinkMethod::SoftLink => {
2752 Ok(metadata.file_type().is_symlink() && std::fs::read_link(link)? == target)
2753 }
2754 Argv0LinkMethod::HardLink => {
2755 if metadata.file_type().is_symlink() {
2756 return Ok(false);
2757 }
2758 Ok(std::fs::read(link)? == std::fs::read(target)?)
2761 }
2762 Argv0LinkMethod::Script => {
2763 if metadata.file_type().is_symlink() {
2764 return Ok(false);
2765 }
2766 Ok(std::fs::read_to_string(link).ok() == Some(argv0_script_contents(target, name)))
2767 }
2768 }
2769}
2770
2771fn argv0_link_file_name(name: &str, method: Argv0LinkMethod) -> String {
2773 let extension = match method {
2774 Argv0LinkMethod::Script if cfg!(windows) => ".cmd",
2775 Argv0LinkMethod::Script => "",
2777 _ if cfg!(windows) => ".exe",
2778 _ => "",
2779 };
2780 format!("{name}{extension}")
2781}
2782
2783fn argv0_script_contents(target: &Path, name: &str) -> String {
2787 let target = target.display();
2788 if cfg!(windows) {
2789 format!("@\"{target}\" argv0 {name} %*\r\n")
2790 } else {
2791 format!("#!/bin/sh\nexec \"{target}\" argv0 {name} \"$@\"\n")
2792 }
2793}
2794
2795#[cfg(unix)]
2796fn create_symlink(target: &Path, link: &Path) -> std::io::Result<()> {
2797 std::os::unix::fs::symlink(target, link)
2798}
2799
2800#[cfg(windows)]
2801fn create_symlink(target: &Path, link: &Path) -> std::io::Result<()> {
2802 std::os::windows::fs::symlink_file(target, link)
2803}
2804
2805#[cfg(not(any(unix, windows)))]
2806fn create_symlink(_target: &Path, _link: &Path) -> std::io::Result<()> {
2807 Err(std::io::Error::new(
2808 std::io::ErrorKind::Unsupported,
2809 "symlink creation is not supported on this platform",
2810 ))
2811}
2812
2813#[cfg(unix)]
2815fn make_executable(path: &Path) -> std::io::Result<()> {
2816 use std::os::unix::fs::PermissionsExt;
2817 let mut permissions = std::fs::metadata(path)?.permissions();
2818 permissions.set_mode(0o755);
2819 std::fs::set_permissions(path, permissions)
2820}
2821
2822#[cfg(not(unix))]
2823fn make_executable(_path: &Path) -> std::io::Result<()> {
2824 Ok(())
2825}
2826
2827fn register_runtime_group_metadata(
2828 group: &RuntimeGroupSpec,
2829 prefix: &mut Vec<String>,
2830 schemas: &mut SchemaRegistry,
2831 views: &mut HumanViewRegistry,
2832) {
2833 prefix.push(group.group.name.clone());
2834 for child_group in &group.groups {
2835 register_runtime_group_metadata(child_group, prefix, schemas, views);
2836 }
2837 for child in &group.commands {
2838 prefix.push(child.spec.name.clone());
2839 let command_path = prefix.join(":");
2840 register_command_schema(&child.spec, &command_path, schemas);
2841 if child.spec.view_id.is_none() && !child.spec.view_columns.is_empty() {
2847 views.register(HumanViewDef::new(
2848 command_path,
2849 child.spec.view_columns.clone(),
2850 ));
2851 }
2852 prefix.pop();
2853 }
2854 prefix.pop();
2855}
2856
2857fn register_command_schema(spec: &CommandSpec, command_path: &str, schemas: &mut SchemaRegistry) {
2858 if let Some(schema) = &spec.output_schema {
2859 schemas.register_info(command_path.to_owned(), schema.clone());
2860 }
2861}
2862
2863fn runtime_group_clap_command_with_schema_help(
2864 group: &RuntimeGroupSpec,
2865 prefix: &mut Vec<String>,
2866 schemas: &SchemaRegistry,
2867) -> Command {
2868 let mut command = group_clap_command_without_children(&group.group);
2869 prefix.push(group.group.name.clone());
2870 for child_group in &group.groups {
2871 command = command.subcommand(runtime_group_clap_command_with_schema_help(
2872 child_group,
2873 prefix,
2874 schemas,
2875 ));
2876 }
2877 for child in &group.commands {
2878 prefix.push(child.spec.name.clone());
2879 let command_path = prefix.join(":");
2880 command = command.subcommand(command_clap_command_with_schema_help(
2881 &child.spec,
2882 &command_path,
2883 schemas,
2884 ));
2885 prefix.pop();
2886 }
2887 prefix.pop();
2888 command
2889}
2890
2891fn group_clap_command_without_children(group: &GroupSpec) -> Command {
2892 let mut command = Command::new(group.name.clone())
2893 .about(group.short.clone())
2894 .help_template(GROUP_HELP_TEMPLATE);
2895 if let Some(long) = &group.long
2896 && !long.is_empty()
2897 {
2898 command = command.long_about(long.clone());
2899 }
2900 for alias in &group.aliases {
2901 command = command.alias(alias.clone());
2902 }
2903 if group.hidden {
2904 command = command.hide(true);
2905 }
2906 command
2907}
2908
2909fn command_clap_command_with_schema_help(
2910 spec: &CommandSpec,
2911 command_path: &str,
2912 schemas: &SchemaRegistry,
2913) -> Command {
2914 let mut command = spec.clap_command();
2915 let Some(schema) = schemas.get_by_path(command_path) else {
2916 return command;
2917 };
2918 let schema_help = format_help_section(&schema.fields);
2919 if schema_help.is_empty() {
2920 return command;
2921 }
2922 let base = spec
2923 .long
2924 .as_ref()
2925 .filter(|long| !long.is_empty())
2926 .cloned()
2927 .unwrap_or_else(|| spec.short.clone());
2928 let long = if base.is_empty() {
2929 schema_help
2930 } else {
2931 format!("{base}\n\n{schema_help}")
2932 };
2933 command = command.long_about(long);
2934 command
2935}
2936
2937fn process_exit_code(code: i32) -> ExitCode {
2938 if code == 0 {
2939 return ExitCode::SUCCESS;
2940 }
2941 match u8::try_from(code) {
2942 Ok(code) if code != 0 => ExitCode::from(code),
2943 Ok(_) | Err(_) => ExitCode::from(1),
2944 }
2945}
2946
2947async fn run_streaming_command(
2948 middleware: &Middleware,
2949 request: MiddlewareRequest<'_>,
2950 raw_matches: Arc<ArgMatches>,
2951 streaming_handler: crate::command::StreamingCommandHandler,
2952) -> Result<CliRunOutput> {
2953 use tokio::{io::AsyncWriteExt, sync::mpsc};
2954
2955 let args_for_handler = request.args.clone();
2956 let user_args_for_handler = request.user_args.clone();
2957 let handler_path = request.command_path.to_owned();
2958 let middleware_for_handler = middleware.clone();
2959 let raw_matches_for_handler = raw_matches;
2960
2961 let (tx, mut rx) = mpsc::channel::<serde_json::Value>(64);
2962 let sender = StreamSender(tx);
2963
2964 let writer = tokio::spawn(async move {
2968 let mut stdout = tokio::io::stdout();
2969 while let Some(event) = rx.recv().await {
2970 let Ok(line) = serde_json::to_string(&event) else {
2971 continue;
2972 };
2973 if stdout.write_all(line.as_bytes()).await.is_err()
2974 || stdout.write_all(b"\n").await.is_err()
2975 || stdout.flush().await.is_err()
2976 {
2977 break;
2978 }
2979 }
2980 });
2981
2982 let output = middleware
2983 .run(request, async move |credential| {
2984 streaming_handler(
2985 CommandContext {
2986 credential,
2987 args: args_for_handler,
2988 user_args: user_args_for_handler,
2989 command_path: handler_path,
2990 middleware: middleware_for_handler,
2991 raw_matches: raw_matches_for_handler,
2992 },
2993 sender,
2994 )
2995 .await?;
2996 Ok(crate::CommandResult::new(serde_json::Value::Null))
2997 })
2998 .await;
2999
3000 let _write_result = writer.await;
3003
3004 match output {
3005 Ok(out) if out.exit_code == 0 => Ok(CliRunOutput {
3006 exit_code: 0,
3007 rendered: String::new(),
3008 }),
3009 Ok(out) => Ok(out.into()),
3010 Err(err) => Ok(CliRunOutput {
3011 exit_code: exit_code_for_error(&err),
3012 rendered: render_cli_error(middleware, &err, middleware.app_id.as_str()).rendered,
3013 }),
3014 }
3015}
3016
3017#[cfg(test)]
3018mod user_agent_tests {
3019 use super::*;
3020
3021 #[test]
3022 fn user_agent_string_derives_name_and_version_by_default() {
3023 let config =
3024 CliConfig::new("gdx", "GoDaddy CLI", "gdx").with_build(BuildInfo::new("1.2.3"));
3025 assert_eq!(config.user_agent_string(), "gdx/1.2.3");
3026 }
3027
3028 #[test]
3029 fn user_agent_string_prefers_explicit_override() {
3030 let config = CliConfig::new("gdx", "GoDaddy CLI", "gdx")
3031 .with_build(BuildInfo::new("1.2.3"))
3032 .with_user_agent("gdx-cli/9.9 (custom)");
3033 assert_eq!(config.user_agent_string(), "gdx-cli/9.9 (custom)");
3034 }
3035
3036 #[test]
3037 fn user_agent_string_omits_version_when_absent() {
3038 let config = CliConfig::new("gdx", "GoDaddy CLI", "gdx");
3039 assert_eq!(config.user_agent_string(), "gdx");
3040 }
3041
3042 #[test]
3043 fn install_default_user_agent_publishes_config_value() {
3044 let _guard = crate::transport::client::UA_TEST_LOCK
3045 .lock()
3046 .unwrap_or_else(std::sync::PoisonError::into_inner);
3047 let _restore = crate::transport::client::RestoreDefaultUserAgent;
3048 crate::transport::set_default_user_agent("cli/dev");
3049 let cli = Cli::new(
3050 CliConfig::new("uatest", "UA test", "uatest").with_build(BuildInfo::new("4.5.6")),
3051 );
3052 cli.install_default_user_agent();
3053 assert_eq!(
3054 crate::transport::client::default_user_agent(),
3055 "uatest/4.5.6"
3056 );
3057 }
3058
3059 #[test]
3060 fn install_debug_transport_logger_tracks_the_debug_pattern() {
3061 let _guard = crate::transport::client::TRANSPORT_LOGGER_TEST_LOCK
3062 .lock()
3063 .unwrap_or_else(std::sync::PoisonError::into_inner);
3064 let _restore = crate::transport::client::RestoreDefaultTransportLogger;
3065
3066 install_debug_transport_logger("transport", &[]);
3068 assert!(crate::transport::default_transport_logger().enabled());
3069
3070 install_debug_transport_logger("*,-transport", &[]);
3072 assert!(!crate::transport::default_transport_logger().enabled());
3073
3074 install_debug_transport_logger("transport", &[]);
3076 install_debug_transport_logger("", &[]);
3077 assert!(!crate::transport::default_transport_logger().enabled());
3078 }
3079}
3080
3081#[cfg(test)]
3082mod env_config_tests {
3083 use super::*;
3084
3085 #[test]
3086 fn with_environments_stores_shared_arc_with_consumer_app_id() {
3087 let cfg = CliConfig::new("gddy", "GoDaddy CLI", "gddy").with_environments(Arc::new(
3091 crate::environments::Environments::new("prod")
3092 .with_app_id("gddy")
3093 .with_config_file(true),
3094 ));
3095 let envs = cfg.environments.as_ref().expect("environments set");
3096 assert!(envs.config_file_path().is_some());
3097 }
3098
3099 #[tokio::test]
3100 async fn env_flag_overrides_default_and_reaches_middleware_env() {
3101 use crate::{CommandResult, CommandSpec, RuntimeCommandSpec};
3102 use serde_json::json;
3103 let mut cli = Cli::new(
3104 CliConfig::new("envtest", "Env test", "envtest").with_environments(Arc::new(
3105 crate::environments::Environments::new("prod")
3106 .with_environment("prod", crate::environments::EnvironmentDef::new())
3107 .with_environment("ote", crate::environments::EnvironmentDef::new()),
3108 )),
3109 );
3110 cli.add_command(RuntimeCommandSpec::new_with_context(
3111 CommandSpec::new("whichenv", "echo env").no_auth(true),
3112 async |ctx| {
3113 Ok(CommandResult::new(
3114 json!({ "env": ctx.environment()?.name }),
3115 ))
3116 },
3117 ));
3118 let out = cli
3119 .run(["envtest", "whichenv", "--env", "ote", "--output", "json"])
3120 .await;
3121 assert_eq!(out.exit_code, 0, "rendered: {}", out.rendered);
3122 assert!(out.rendered.contains("\"env\""));
3123 assert!(out.rendered.contains("ote"));
3124 }
3125
3126 #[tokio::test]
3127 async fn unknown_env_flag_produces_error_envelope() {
3128 let cli = Cli::new(
3129 CliConfig::new("envtest2", "Env test", "envtest2").with_environments(Arc::new(
3130 crate::environments::Environments::new("prod")
3131 .with_environment("prod", crate::environments::EnvironmentDef::new()),
3132 )),
3133 );
3134 let out = cli.run(["envtest2", "tree", "--env", "nope"]).await;
3135 assert_ne!(out.exit_code, 0);
3136 assert!(out.rendered.contains("nope"));
3137 }
3138}