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