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