1use std::{
2 collections::{BTreeMap, BTreeSet},
3 future::Future,
4 io::Write,
5 process::ExitCode,
6 sync::{Arc, Mutex},
7 time::Duration,
8};
9
10mod builtins;
11mod help;
12mod tree_render;
13
14use clap::{ArgMatches, Command};
15
16use crate::{
17 ActivityEmitter, Auditor, AuthProvider, Authorizer, CliCoreError, CommandMeta, CommandSpec,
18 GroupSpec, GuideEntry, Middleware, MiddlewareRequest, Result, RuntimeCommandSpec,
19 RuntimeGroupSpec,
20 auth::commands::auth_command_group,
21 command::{
22 CommandContext, StreamSender, command_args_from_matches, command_path_from_matches,
23 leaf_matches,
24 },
25 error::exit_code_for_error,
26 flags::{
27 GlobalFlags, derive_bool_flags, derive_value_flags, extract_command_path,
28 extract_output_format, extract_search_query, global_flags_from_matches,
29 has_true_schema_flag, register_global_flags,
30 },
31 guide::guide_content,
32 module::{Module, ModuleContext},
33 output::{
34 HumanViewDef, SchemaRegistry, format_help_section, global_human_view_registry_snapshot,
35 global_schema_registry_snapshot,
36 },
37 search::{SearchDocument, SearchIndex},
38};
39
40use builtins::{guide_args, guide_command, help_args, help_command};
41pub use help::{ModuleHelpEntry, build_root_long};
42
43#[derive(Clone, Debug, Default, Eq, PartialEq)]
45pub struct BuildInfo {
46 pub version: String,
48 pub commit: Option<String>,
50 pub date: Option<String>,
52}
53
54impl BuildInfo {
55 #[must_use]
57 pub fn new(version: impl Into<String>) -> Self {
58 Self {
59 version: version.into(),
60 commit: None,
61 date: None,
62 }
63 }
64
65 #[must_use]
67 pub fn with_commit(mut self, commit: impl Into<String>) -> Self {
68 self.commit = Some(commit.into());
69 self
70 }
71
72 #[must_use]
74 pub fn with_date(mut self, date: impl Into<String>) -> Self {
75 self.date = Some(date.into());
76 self
77 }
78
79 #[must_use]
81 pub fn version_string(&self) -> String {
82 let commit = self.commit.as_deref().unwrap_or_default();
83 let date = self.date.as_deref().unwrap_or_default();
84
85 if commit.is_empty() && date.is_empty() {
86 self.version.clone()
87 } else {
88 format!("{} (commit {commit}, built {date})", self.version)
89 }
90 }
91}
92
93pub type InitDeps = Arc<dyn Fn(&mut Middleware) -> Result<()> + Send + Sync>;
95pub type RegisterFlags = Arc<dyn Fn(Command) -> Command + Send + Sync>;
97pub type ApplyFlags = Arc<dyn Fn(&ArgMatches, &mut Middleware) -> Result<()> + Send + Sync>;
99pub type PreRun =
101 Arc<dyn Fn(&mut Middleware, &str, &crate::middleware::ValueMap) -> Result<()> + Send + Sync>;
102pub type ResolveMeta = Arc<dyn Fn(&str, CommandMeta) -> CommandMeta + Send + Sync>;
104pub type OnShutdown = Arc<dyn Fn() + Send + Sync>;
106pub type ExtraSearchDocs = Arc<dyn Fn() -> Vec<SearchDocument> + Send + Sync>;
108
109#[derive(Clone, Default)]
115pub struct CliConfig {
116 pub name: String,
118 pub short: String,
120 pub long: Option<String>,
122 pub build: BuildInfo,
124 pub app_id: String,
126 pub default_auth_provider: Option<String>,
128 pub modules: Vec<Module>,
130 pub commands: Vec<RuntimeCommandSpec>,
132 pub guides: Vec<GuideEntry>,
134 pub views: Vec<HumanViewDef>,
136 pub auth_providers: Vec<Arc<dyn AuthProvider>>,
138 pub authz: Option<Arc<dyn Authorizer>>,
140 pub auditor: Option<Arc<dyn Auditor>>,
142 pub activity: Option<Arc<dyn ActivityEmitter>>,
144 pub init_deps: Option<InitDeps>,
146 pub register_flags: Option<RegisterFlags>,
148 pub apply_flags: Option<ApplyFlags>,
150 pub pre_run: Option<PreRun>,
152 pub meta_resolver: Option<ResolveMeta>,
154 pub on_shutdown: Option<OnShutdown>,
156 pub extra_search_docs: Option<ExtraSearchDocs>,
158}
159
160impl CliConfig {
161 #[must_use]
163 pub fn new(
164 name: impl Into<String>,
165 short: impl Into<String>,
166 app_id: impl Into<String>,
167 ) -> Self {
168 Self {
169 name: name.into(),
170 short: short.into(),
171 app_id: app_id.into(),
172 ..Self::default()
173 }
174 }
175
176 #[must_use]
178 pub fn with_long(mut self, long: impl Into<String>) -> Self {
179 self.long = Some(long.into());
180 self
181 }
182
183 #[must_use]
185 pub fn with_build(mut self, build: BuildInfo) -> Self {
186 self.build = build;
187 self
188 }
189
190 #[must_use]
192 pub fn with_default_auth_provider(mut self, provider: impl Into<String>) -> Self {
193 self.default_auth_provider = Some(provider.into());
194 self
195 }
196
197 #[must_use]
199 pub fn with_module(mut self, module: Module) -> Self {
200 self.modules.push(module);
201 self
202 }
203
204 #[must_use]
206 pub fn with_modules(mut self, modules: impl IntoIterator<Item = Module>) -> Self {
207 self.modules.extend(modules);
208 self
209 }
210
211 #[must_use]
213 pub fn with_command(mut self, command: RuntimeCommandSpec) -> Self {
214 self.commands.push(command);
215 self
216 }
217
218 #[must_use]
220 pub fn with_guide(mut self, guide: GuideEntry) -> Self {
221 self.guides.push(guide);
222 self
223 }
224
225 #[must_use]
227 pub fn with_guides(mut self, guides: impl IntoIterator<Item = GuideEntry>) -> Self {
228 self.guides.extend(guides);
229 self
230 }
231
232 #[must_use]
234 pub fn with_view(mut self, view: HumanViewDef) -> Self {
235 self.views.push(view);
236 self
237 }
238
239 #[must_use]
241 pub fn with_auth_provider(mut self, provider: Arc<dyn AuthProvider>) -> Self {
242 self.auth_providers.push(provider);
243 self
244 }
245
246 #[must_use]
248 pub fn with_authz(mut self, authz: Arc<dyn Authorizer>) -> Self {
249 self.authz = Some(authz);
250 self
251 }
252
253 #[must_use]
255 pub fn with_auditor(mut self, auditor: Arc<dyn Auditor>) -> Self {
256 self.auditor = Some(auditor);
257 self
258 }
259
260 #[must_use]
262 pub fn with_activity(mut self, activity: Arc<dyn ActivityEmitter>) -> Self {
263 self.activity = Some(activity);
264 self
265 }
266
267 #[must_use]
269 pub fn with_init_deps(mut self, init_deps: InitDeps) -> Self {
270 self.init_deps = Some(init_deps);
271 self
272 }
273
274 #[must_use]
276 pub fn with_register_flags(mut self, register_flags: RegisterFlags) -> Self {
277 self.register_flags = Some(register_flags);
278 self
279 }
280
281 #[must_use]
283 pub fn with_apply_flags(mut self, apply_flags: ApplyFlags) -> Self {
284 self.apply_flags = Some(apply_flags);
285 self
286 }
287
288 #[must_use]
290 pub fn with_pre_run(mut self, pre_run: PreRun) -> Self {
291 self.pre_run = Some(pre_run);
292 self
293 }
294
295 #[must_use]
297 pub fn with_meta_resolver(mut self, meta_resolver: ResolveMeta) -> Self {
298 self.meta_resolver = Some(meta_resolver);
299 self
300 }
301
302 #[must_use]
304 pub fn with_on_shutdown(mut self, on_shutdown: OnShutdown) -> Self {
305 self.on_shutdown = Some(on_shutdown);
306 self
307 }
308
309 #[must_use]
311 pub fn with_extra_search_docs(mut self, extra_search_docs: ExtraSearchDocs) -> Self {
312 self.extra_search_docs = Some(extra_search_docs);
313 self
314 }
315}
316
317impl std::fmt::Debug for CliConfig {
318 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319 formatter
320 .debug_struct("CliConfig")
321 .field("name", &self.name)
322 .field("short", &self.short)
323 .field("long", &self.long)
324 .field("build", &self.build)
325 .field("app_id", &self.app_id)
326 .field("default_auth_provider", &self.default_auth_provider)
327 .field("modules", &self.modules)
328 .field("commands", &self.commands)
329 .field("guides", &self.guides)
330 .field("views", &self.views)
331 .field("auth_providers_len", &self.auth_providers.len())
332 .field("has_authz", &self.authz.is_some())
333 .field("has_auditor", &self.auditor.is_some())
334 .field("has_activity", &self.activity.is_some())
335 .field("has_init_deps", &self.init_deps.is_some())
336 .field("has_register_flags", &self.register_flags.is_some())
337 .field("has_apply_flags", &self.apply_flags.is_some())
338 .field("has_pre_run", &self.pre_run.is_some())
339 .field("has_meta_resolver", &self.meta_resolver.is_some())
340 .field("has_on_shutdown", &self.on_shutdown.is_some())
341 .field("has_extra_search_docs", &self.extra_search_docs.is_some())
342 .finish()
343 }
344}
345
346#[derive(Clone, Debug, PartialEq)]
348pub struct CliRunOutput {
349 pub exit_code: i32,
351 pub rendered: String,
353}
354
355impl From<crate::middleware::MiddlewareOutput> for CliRunOutput {
356 fn from(o: crate::middleware::MiddlewareOutput) -> Self {
357 Self {
358 exit_code: o.exit_code,
359 rendered: o.rendered,
360 }
361 }
362}
363
364#[derive(Clone)]
370pub struct Cli {
371 config: CliConfig,
372 middleware: Middleware,
373 root: Command,
374 commands: BTreeMap<String, RuntimeCommandSpec>,
375 module_entries: Vec<ModuleHelpEntry>,
376 guide_entries: Vec<GuideEntry>,
377 init_deps: Option<InitDeps>,
378 apply_flags: Option<ApplyFlags>,
379 pre_run: Option<PreRun>,
380 meta_resolver: Option<ResolveMeta>,
381 on_shutdown: Option<OnShutdown>,
382 extra_search_docs: Option<ExtraSearchDocs>,
383 init_state: Arc<Mutex<Option<std::result::Result<Middleware, InitFailure>>>>,
384}
385
386#[derive(Clone, Debug, Eq, PartialEq)]
387struct InitFailure {
388 message: String,
389 code: String,
390 system: String,
391 request_id: String,
392 exit_code: i32,
393}
394
395impl InitFailure {
396 fn capture(err: &CliCoreError) -> Self {
397 let envelope = crate::output::build_error_envelope(err, "");
398 let (code, system, request_id) = envelope.error.map_or_else(
399 || ("ERROR".to_owned(), String::new(), String::new()),
400 |error| (error.code, error.system, error.request_id),
401 );
402 Self {
403 message: err.to_string(),
404 code,
405 system,
406 request_id,
407 exit_code: exit_code_for_error(err),
408 }
409 }
410
411 fn into_error(self) -> CliCoreError {
412 CliCoreError::with_exit_code(
413 self.exit_code,
414 CliCoreError::SystemMessage {
415 message: self.message,
416 system: self.system,
417 code: self.code,
418 request_id: self.request_id,
419 },
420 )
421 }
422}
423
424impl std::fmt::Debug for Cli {
425 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
426 formatter
427 .debug_struct("Cli")
428 .field("config", &self.config)
429 .field("middleware", &self.middleware)
430 .field("root", &self.root)
431 .field("commands", &self.commands)
432 .field("module_entries", &self.module_entries)
433 .field("guide_entries", &self.guide_entries)
434 .field("has_init_deps", &self.init_deps.is_some())
435 .field("has_apply_flags", &self.apply_flags.is_some())
436 .field("has_pre_run", &self.pre_run.is_some())
437 .field("has_meta_resolver", &self.meta_resolver.is_some())
438 .field("has_on_shutdown", &self.on_shutdown.is_some())
439 .field("has_extra_search_docs", &self.extra_search_docs.is_some())
440 .finish()
441 }
442}
443
444impl Cli {
445 #[must_use]
447 pub fn new(config: CliConfig) -> Self {
448 let auth_providers = config.auth_providers.clone();
449 let guides = config.guides.clone();
450 let views = config.views.clone();
451 let modules = config.modules.clone();
452 let commands = config.commands.clone();
453 let init_deps = config.init_deps.clone();
454 let apply_flags = config.apply_flags.clone();
455 let pre_run = config.pre_run.clone();
456 let meta_resolver = config.meta_resolver.clone();
457 let on_shutdown = config.on_shutdown.clone();
458 let extra_search_docs = config.extra_search_docs.clone();
459 let mut root = Command::new(config.name.clone())
460 .about(config.short.clone())
461 .disable_help_subcommand(true)
462 .version(config.build.version_string());
463 if let Some(long) = &config.long
464 && !long.is_empty()
465 {
466 root = root.long_about(long.clone());
467 }
468 root = register_global_flags(root)
469 .subcommand(help_command())
470 .subcommand(guide_command())
471 .subcommand(Command::new("tree").about("Display full command tree"));
472 if let Some(register_flags) = &config.register_flags {
473 root = register_flags(root);
474 }
475 let intro = config
476 .long
477 .as_deref()
478 .filter(|long| !long.is_empty())
479 .unwrap_or(config.short.as_str());
480 root = root.long_about(build_root_long(intro, &[], false));
481
482 let mut middleware = Middleware::new();
483 middleware.app_id = config.app_id.clone();
484 middleware.default_auth_provider = config.default_auth_provider.clone().unwrap_or_default();
485 middleware.authz = config.authz.clone();
486 middleware.auditor = config.auditor.clone();
487 middleware.activity = config.activity.clone();
488 middleware
489 .schema_registry
490 .merge(&global_schema_registry_snapshot());
491 middleware
492 .human_views
493 .merge(&global_human_view_registry_snapshot());
494
495 let mut cli = Self {
496 config,
497 middleware,
498 root,
499 commands: BTreeMap::new(),
500 module_entries: Vec::new(),
501 guide_entries: Vec::new(),
502 init_deps,
503 apply_flags,
504 pre_run,
505 meta_resolver,
506 on_shutdown,
507 extra_search_docs,
508 init_state: Arc::new(Mutex::new(None)),
509 };
510 for provider in auth_providers {
511 cli.register_auth_provider(provider);
512 }
513 if cli.middleware.default_auth_provider.is_empty()
514 && let Some(provider) = cli.middleware.auth.registered_names().first()
515 {
516 cli.middleware.default_auth_provider = provider.clone();
517 }
518 if !cli.middleware.default_auth_provider.is_empty() {
519 cli.ensure_auth_command();
520 }
521 for view in views {
522 cli.middleware.human_views.register(view);
523 }
524 cli.add_guides(guides);
525 for module in modules {
526 cli.add_module(module);
527 }
528 for command in commands {
529 cli.add_command(command);
530 }
531 cli
532 }
533
534 #[must_use]
536 pub fn middleware(&self) -> &Middleware {
537 &self.middleware
538 }
539
540 pub fn middleware_mut(&mut self) -> &mut Middleware {
542 &mut self.middleware
543 }
544
545 pub async fn execute(&self) -> ExitCode {
547 let mut stdout = std::io::stdout().lock();
548 let mut stderr = std::io::stderr().lock();
549 match self
550 .execute_from(std::env::args_os(), &mut stdout, &mut stderr)
551 .await
552 {
553 Ok(code) => code,
554 Err(err) => {
555 drop(writeln!(stderr, "{err}"));
556 ExitCode::from(1)
557 }
558 }
559 }
560
561 pub async fn execute_from<I, S, O, E>(
563 &self,
564 args: I,
565 stdout: &mut O,
566 stderr: &mut E,
567 ) -> std::io::Result<ExitCode>
568 where
569 I: IntoIterator<Item = S>,
570 S: Into<std::ffi::OsString> + Clone,
571 O: Write,
572 E: Write,
573 {
574 self.execute_from_until_signal(args, stdout, stderr, shutdown_signal())
575 .await
576 }
577
578 pub async fn execute_from_until_signal<I, S, O, E, Shutdown>(
580 &self,
581 args: I,
582 stdout: &mut O,
583 stderr: &mut E,
584 shutdown: Shutdown,
585 ) -> std::io::Result<ExitCode>
586 where
587 I: IntoIterator<Item = S>,
588 S: Into<std::ffi::OsString> + Clone,
589 O: Write,
590 E: Write,
591 Shutdown: Future<Output = ()>,
592 {
593 let output = run_until_signal(self.run(args), shutdown).await;
594 if output.exit_code == 130
595 && output.rendered == "command interrupted\n"
596 && let Some(on_shutdown) = &self.on_shutdown
597 {
598 on_shutdown();
599 }
600 if output.exit_code == 0 {
601 stdout.write_all(output.rendered.as_bytes())?;
602 } else {
603 stderr.write_all(output.rendered.as_bytes())?;
604 }
605 Ok(process_exit_code(output.exit_code))
606 }
607
608 pub fn register_auth_provider(&mut self, provider: Arc<dyn AuthProvider>) -> &mut Self {
610 self.middleware.auth.register(provider);
611 self.ensure_auth_command();
612 self.refresh_root_long();
613 self
614 }
615
616 #[must_use]
618 pub fn root_command(&self) -> &Command {
619 &self.root
620 }
621
622 pub fn add_module_group(
624 &mut self,
625 category: impl Into<String>,
626 group: RuntimeGroupSpec,
627 ) -> &mut Self {
628 let category = category.into();
629 if !group.group.hidden {
630 self.module_entries.push(ModuleHelpEntry {
631 category,
632 name: group.group.name.clone(),
633 short: group.group.short.clone(),
634 });
635 }
636
637 let mut prefix = Vec::new();
638 register_runtime_group_schemas(&group, &mut prefix, &mut self.middleware.schema_registry);
639 let mut prefix = Vec::new();
640 group.register_commands(&mut prefix, &mut self.commands);
641 let mut prefix = Vec::new();
642 let clap_group = runtime_group_clap_command_with_schema_help(
643 &group,
644 &mut prefix,
645 &self.middleware.schema_registry,
646 );
647 self.root = self.root.clone().subcommand(clap_group);
648 self.refresh_root_long();
649 self
650 }
651
652 pub fn add_module(&mut self, module: Module) -> &mut Self {
654 for view in module.views.clone() {
655 self.middleware.human_views.register(view);
656 }
657 self.add_guides(module.guides.clone());
658 let mut context = ModuleContext::new(&mut self.middleware);
659 let group = (module.register)(&mut context);
660 let (guides, views) = context.into_parts();
661 for view in views {
662 self.middleware.human_views.register(view);
663 }
664 self.add_guides(guides);
665 self.add_module_group(module.category, group)
666 }
667
668 pub fn add_command(&mut self, command: RuntimeCommandSpec) -> &mut Self {
670 let name = command.spec.name.clone();
671 register_command_schema(&command.spec, &name, &mut self.middleware.schema_registry);
672 self.commands.insert(name, command.clone());
673 self.root = self
674 .root
675 .clone()
676 .subcommand(command_clap_command_with_schema_help(
677 &command.spec,
678 &command.spec.name,
679 &self.middleware.schema_registry,
680 ));
681 self
682 }
683
684 pub fn set_has_guide(&mut self, has_guide: bool) -> &mut Self {
686 if has_guide && self.guide_entries.is_empty() && !has_subcommand(&self.root, "guide") {
687 self.root = self.root.clone().subcommand(guide_command());
688 }
689 self.refresh_root_long();
690 self
691 }
692
693 pub fn add_guides(&mut self, entries: impl IntoIterator<Item = GuideEntry>) -> &mut Self {
695 let mut seen = self
696 .guide_entries
697 .iter()
698 .map(|entry| entry.name.clone())
699 .collect::<BTreeSet<_>>();
700 for entry in entries {
701 if seen.insert(entry.name.clone()) {
702 self.guide_entries.push(entry);
703 }
704 }
705 if !self.guide_entries.is_empty() && !has_subcommand(&self.root, "guide") {
706 self.root = self.root.clone().subcommand(guide_command());
707 }
708 self.refresh_root_long();
709 self
710 }
711
712 pub async fn run<I, S>(&self, args: I) -> CliRunOutput
714 where
715 I: IntoIterator<Item = S>,
716 S: Into<std::ffi::OsString> + Clone,
717 {
718 let raw_args = args
719 .into_iter()
720 .map(Into::into)
721 .collect::<Vec<std::ffi::OsString>>();
722 let text_args = raw_args
723 .iter()
724 .map(|arg| arg.to_string_lossy().into_owned())
725 .collect::<Vec<_>>();
726 let clap_args = normalize_optional_global_flags_before_command(&self.root, &text_args);
727 if has_root_version_flag(&text_args, &self.root, &self.config.name) {
728 return self.finish_run(CliRunOutput {
729 exit_code: 0,
730 rendered: format!(
731 "{} version {}\n",
732 self.config.name,
733 self.config.build.version_string()
734 ),
735 });
736 }
737 if let Some(output) = self.try_run_schema_bypass(&text_args) {
738 return output;
739 }
740 if let Some(output) = self.try_run_search_bypass(&text_args) {
741 return output;
742 }
743 if let Some(message) =
744 unknown_group_command_message(&self.root, &text_args, &self.config.name)
745 {
746 return self.finish_run(CliRunOutput {
747 exit_code: 1,
748 rendered: message,
749 });
750 }
751
752 let matches = match self.root.clone().try_get_matches_from(clap_args) {
753 Ok(matches) => matches,
754 Err(err) => {
755 return self.finish_run(CliRunOutput {
756 exit_code: err.exit_code(),
757 rendered: err.to_string(),
758 });
759 }
760 };
761
762 let flags = global_flags_from_matches(&matches);
763 let command_timeout = match parse_command_timeout(&flags.timeout) {
764 Ok(timeout) => timeout,
765 Err(err) => {
766 return self.finish_run(render_cli_error(
767 &self.middleware,
768 &err,
769 &self.config.app_id,
770 ));
771 }
772 };
773 let mut middleware = self.middleware.clone();
774 apply_global_flags(&mut middleware, &flags, command_timeout);
775 if let Err(err) = self.apply_config_flags(&matches, &mut middleware) {
776 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
777 }
778
779 let command_path = command_path_from_matches(&self.config.name, &matches);
780 if command_path == "help" {
781 if let Err(err) = self.run_pre_run(&mut middleware, &command_path, &help_args(&matches))
782 {
783 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
784 }
785 return self.finish_run(self.render_help_command(&matches));
786 }
787 if command_path == "tree" {
788 if let Err(err) = self.run_pre_run(
789 &mut middleware,
790 &command_path,
791 &crate::middleware::ValueMap::new(),
792 ) {
793 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
794 }
795 return self.finish_run(tree_render::render_tree(
796 &self.root,
797 &self.config.app_id,
798 &middleware,
799 ));
800 }
801 if command_path == "guide" {
802 if let Err(err) =
803 self.run_pre_run(&mut middleware, &command_path, &guide_args(&matches))
804 {
805 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
806 }
807 return self.finish_run(self.render_guide(&matches));
808 }
809 let Some(command) = self.commands.get(&command_path) else {
810 if !command_path.is_empty()
811 && let Some(group) = find_command_by_colon_path(&self.root, &command_path)
812 && group.get_subcommands().next().is_some()
813 {
814 if let Err(err) = self.run_pre_run(
815 &mut middleware,
816 &command_path,
817 &crate::middleware::ValueMap::new(),
818 ) {
819 return self.finish_run(render_cli_error(
820 &middleware,
821 &err,
822 &self.config.app_id,
823 ));
824 }
825 return self.finish_run(CliRunOutput {
826 exit_code: 0,
827 rendered: group.clone().render_long_help().to_string(),
828 });
829 }
830 return self.finish_run(CliRunOutput {
831 exit_code: if command_path.is_empty() { 0 } else { 1 },
832 rendered: if command_path.is_empty() {
833 self.root.clone().render_long_help().to_string()
834 } else {
835 format!("unknown command {command_path:?}")
836 },
837 });
838 };
839
840 let mut middleware = match self.initialized_middleware() {
841 Ok(middleware) => middleware,
842 Err(err) => {
843 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
844 }
845 };
846 apply_global_flags(&mut middleware, &flags, command_timeout);
847 if let Err(err) = self.apply_config_flags(&matches, &mut middleware) {
848 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
849 }
850
851 let leaf = leaf_matches(&matches);
852 let args = command_args_from_matches(leaf, &command.spec, false);
853 let user_args = command_args_from_matches(leaf, &command.spec, true);
854 if let Err(err) = self.run_pre_run(&mut middleware, &command_path, &args) {
855 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
856 }
857 let meta = self.resolve_meta(&command_path, command.spec.metadata());
858 let default_fields = command.spec.default_fields.clone().unwrap_or_default();
859 let system = command.spec.system.clone().unwrap_or_default();
860
861 if let Some(streaming_handler) = command.streaming_handler.clone() {
862 let result = run_with_timeout(
863 command_timeout,
864 &flags.timeout,
865 run_streaming_command(
866 &middleware,
867 MiddlewareRequest {
868 meta,
869 command_path: &command_path,
870 system: &system,
871 user_args,
872 args,
873 default_fields: &default_fields,
874 no_auth: command.spec.no_auth,
875 },
876 Arc::new(leaf.clone()),
877 streaming_handler,
878 ),
879 )
880 .await;
881 return self.finish_run(match result {
882 Ok(output) => output,
883 Err(err) => render_cli_error(&middleware, &err, &self.config.app_id),
884 });
885 }
886
887 let handler = command.handler.clone();
888 let args_for_handler = args.clone();
889 let user_args_for_handler = user_args.clone();
890 let handler_path = command_path.clone();
891 let middleware_for_handler = middleware.clone();
892 let raw_matches_for_handler = Arc::new(leaf.clone());
893 let result = run_with_timeout(
894 command_timeout,
895 &flags.timeout,
896 middleware.run(
897 MiddlewareRequest {
898 meta,
899 command_path: &command_path,
900 system: &system,
901 user_args,
902 args,
903 default_fields: &default_fields,
904 no_auth: command.spec.no_auth,
905 },
906 async move |credential| {
907 handler(CommandContext {
908 credential,
909 args: args_for_handler,
910 user_args: user_args_for_handler,
911 command_path: handler_path,
912 middleware: middleware_for_handler,
913 raw_matches: raw_matches_for_handler,
914 })
915 .await
916 },
917 ),
918 )
919 .await;
920
921 match result {
922 Ok(output) => self.finish_run(output.into()),
923 Err(err) => self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id)),
924 }
925 }
926
927 fn try_run_search_bypass(&self, args: &[String]) -> Option<CliRunOutput> {
928 let query = extract_search_query(args);
929 if query.is_empty() {
930 return None;
931 }
932 let scope = self.search_scope(args);
933 let output_format = extract_output_format(args);
934 Some(self.render_search(&query, &scope, &output_format))
935 }
936
937 fn try_run_schema_bypass(&self, args: &[String]) -> Option<CliRunOutput> {
938 if !has_true_schema_flag(args) {
939 return None;
940 }
941 let bool_flags = derive_bool_flags(&self.root);
942 let value_flags = derive_value_flags(&self.root);
943 let command_path =
944 self.canonical_command_path(&extract_command_path(args, &bool_flags, &value_flags));
945 let schema = self.middleware.schema_registry.get_by_path(&command_path)?;
946 let output_format = extract_output_format(args);
947 Some(self.render_schema(schema, &output_format))
948 }
949
950 fn render_schema(
951 &self,
952 schema: crate::output::SchemaInfo,
953 output_format: &str,
954 ) -> CliRunOutput {
955 let format: crate::output::OutputFormat = match output_format.parse() {
956 Ok(format) => format,
957 Err(err) => {
958 return CliRunOutput {
959 exit_code: exit_code_for_error(&err),
960 rendered: err.to_string(),
961 };
962 }
963 };
964 let envelope =
965 crate::Envelope::success(schema, self.config.app_id.clone()).prepare_for_render("");
966 match crate::output::render(format, &envelope) {
967 Ok(rendered) => CliRunOutput {
968 exit_code: 0,
969 rendered,
970 },
971 Err(err) => CliRunOutput {
972 exit_code: exit_code_for_error(&err),
973 rendered: err.to_string(),
974 },
975 }
976 }
977
978 fn render_search(&self, query: &str, scope: &str, output_format: &str) -> CliRunOutput {
979 let format: crate::output::OutputFormat = match output_format.parse() {
980 Ok(format) => format,
981 Err(err) => {
982 return CliRunOutput {
983 exit_code: exit_code_for_error(&err),
984 rendered: err.to_string(),
985 };
986 }
987 };
988 let docs = self.search_documents(scope);
989 let results = SearchIndex::new(docs).search(query, 10);
990 let envelope =
991 crate::Envelope::success(results, self.config.app_id.clone()).prepare_for_render("");
992 match crate::output::render(format, &envelope) {
993 Ok(rendered) => CliRunOutput {
994 exit_code: 0,
995 rendered,
996 },
997 Err(err) => CliRunOutput {
998 exit_code: exit_code_for_error(&err),
999 rendered: err.to_string(),
1000 },
1001 }
1002 }
1003
1004 fn search_documents(&self, scope: &str) -> Vec<SearchDocument> {
1005 let (scoped, mut prefix) = find_command_and_canonical_path_by_colon_path(&self.root, scope)
1006 .unwrap_or((&self.root, Vec::new()));
1007 let mut docs = Vec::new();
1008 let mut aliases = Vec::new();
1009 append_command_alias_terms(scoped, &mut aliases);
1010 collect_command_search_documents(scoped, &mut prefix, &mut aliases, &mut docs);
1011 if scope.is_empty() {
1012 for entry in &self.guide_entries {
1013 docs.push(SearchDocument {
1014 id: format!("guide:{}", entry.name),
1015 kind: "guide".to_owned(),
1016 title: format!("guide {}", entry.name),
1017 summary: entry.summary.clone(),
1018 content: format!("{} {}", entry.summary, entry.content),
1019 });
1020 }
1021 if let Some(extra_search_docs) = &self.extra_search_docs {
1022 docs.extend(extra_search_docs());
1023 }
1024 }
1025 docs
1026 }
1027
1028 fn search_scope(&self, args: &[String]) -> String {
1029 let parts = extract_search_scope_parts(args);
1030 canonical_path_from_parts(&self.root, &parts).unwrap_or_default()
1031 }
1032
1033 fn canonical_command_path(&self, command_path: &str) -> String {
1034 find_command_and_canonical_path_by_colon_path(&self.root, command_path).map_or_else(
1035 || command_path.to_owned(),
1036 |(_, canonical)| canonical.join(":"),
1037 )
1038 }
1039
1040 fn render_guide(&self, matches: &ArgMatches) -> CliRunOutput {
1041 let leaf = leaf_matches(matches);
1042 let topic = leaf.get_one::<String>("topic").map(String::as_str);
1043 match guide_content(&self.guide_entries, topic) {
1044 Ok(rendered) => CliRunOutput {
1045 exit_code: 0,
1046 rendered,
1047 },
1048 Err(err) => CliRunOutput {
1049 exit_code: 1,
1050 rendered: err,
1051 },
1052 }
1053 }
1054
1055 fn render_help_command(&self, matches: &ArgMatches) -> CliRunOutput {
1056 let leaf = leaf_matches(matches);
1057 let parts = leaf
1058 .get_many::<String>("command")
1059 .map(|values| values.map(String::as_str).collect::<Vec<_>>())
1060 .unwrap_or_default();
1061 if parts.is_empty() {
1062 return CliRunOutput {
1063 exit_code: 0,
1064 rendered: self.root.clone().render_long_help().to_string(),
1065 };
1066 }
1067 let Some(command) = find_help_target(&self.root, &parts) else {
1068 return CliRunOutput {
1069 exit_code: 1,
1070 rendered: format!(
1071 "unknown command {:?} — run '{} help' for available commands",
1072 parts.join(" "),
1073 self.config.name
1074 ),
1075 };
1076 };
1077 CliRunOutput {
1078 exit_code: 0,
1079 rendered: command.clone().render_long_help().to_string(),
1080 }
1081 }
1082
1083 fn refresh_root_long(&mut self) {
1084 let intro = self
1085 .config
1086 .long
1087 .as_deref()
1088 .filter(|long| !long.is_empty())
1089 .unwrap_or(self.config.short.as_str());
1090 self.root = self.root.clone().long_about(build_root_long(
1091 intro,
1092 &self.module_entries,
1093 !self.guide_entries.is_empty() || has_subcommand(&self.root, "guide"),
1094 ));
1095 }
1096
1097 fn ensure_auth_command(&mut self) {
1098 let default_provider = self.default_auth_provider();
1099 let registered_names = self.middleware.auth.registered_names();
1100 if default_provider.is_empty() && registered_names.is_empty() {
1101 return;
1102 }
1103 let replacing_builtin = self.commands.contains_key("auth:login");
1104 if has_subcommand(&self.root, "auth") && !replacing_builtin {
1105 return;
1106 }
1107 let group = auth_command_group(&default_provider, ®istered_names);
1108 let mut prefix = Vec::new();
1109 group.register_commands(&mut prefix, &mut self.commands);
1110 let mut prefix = Vec::new();
1111 let clap_group = runtime_group_clap_command_with_schema_help(
1112 &group,
1113 &mut prefix,
1114 &self.middleware.schema_registry,
1115 );
1116 self.root = if replacing_builtin {
1117 self.root.clone().mut_subcommand("auth", |_| clap_group)
1118 } else {
1119 self.root.clone().subcommand(clap_group)
1120 };
1121 }
1122
1123 fn default_auth_provider(&self) -> String {
1124 if !self.middleware.default_auth_provider.is_empty() {
1125 return self.middleware.default_auth_provider.clone();
1126 }
1127 self.middleware
1128 .auth
1129 .registered_names()
1130 .into_iter()
1131 .next()
1132 .unwrap_or_default()
1133 }
1134
1135 fn initialized_middleware(&self) -> Result<Middleware> {
1136 let Some(init_deps) = &self.init_deps else {
1137 return Ok(self.middleware.clone());
1138 };
1139 let mut guard = self
1140 .init_state
1141 .lock()
1142 .map_err(|_| CliCoreError::message("init deps lock poisoned"))?;
1143 if let Some(result) = guard.as_ref() {
1144 return result.clone().map_err(InitFailure::into_error);
1145 }
1146 let mut middleware = self.middleware.clone();
1147 let result = init_deps(&mut middleware)
1148 .map(|()| middleware)
1149 .map_err(|err| InitFailure::capture(&err));
1150 *guard = Some(result.clone());
1151 result.map_err(InitFailure::into_error)
1152 }
1153
1154 fn apply_config_flags(&self, matches: &ArgMatches, middleware: &mut Middleware) -> Result<()> {
1155 if let Some(apply_flags) = &self.apply_flags {
1156 apply_flags(matches, middleware)?;
1157 }
1158 Ok(())
1159 }
1160
1161 fn run_pre_run(
1162 &self,
1163 middleware: &mut Middleware,
1164 command_path: &str,
1165 args: &crate::middleware::ValueMap,
1166 ) -> Result<()> {
1167 if let Some(pre_run) = &self.pre_run {
1168 pre_run(middleware, command_path, args)?;
1169 }
1170 Ok(())
1171 }
1172
1173 fn resolve_meta(&self, command_path: &str, meta: CommandMeta) -> CommandMeta {
1174 if let Some(resolver) = &self.meta_resolver {
1175 resolver(command_path, meta)
1176 } else {
1177 meta
1178 }
1179 }
1180
1181 fn finish_run(&self, output: CliRunOutput) -> CliRunOutput {
1182 if let Some(on_shutdown) = &self.on_shutdown {
1183 on_shutdown();
1184 }
1185 output
1186 }
1187}
1188
1189fn apply_global_flags(middleware: &mut Middleware, flags: &GlobalFlags, timeout: Option<Duration>) {
1190 middleware.output_format = flags.output_format.clone();
1191 middleware.verbose = flags.verbose.clone();
1192 middleware.dry_run = flags.dry_run;
1193 middleware.fields = flags.fields.clone();
1194 middleware.filter = flags.filter.clone();
1195 middleware.expr = flags.expr.clone();
1196 middleware.limit = flags.limit;
1197 middleware.offset = flags.offset;
1198 middleware.reason = flags.reason.clone();
1199 middleware.schema = flags.schema;
1200 middleware.timeout = timeout;
1201 middleware.debug = flags.debug.clone();
1202 middleware.search = flags.search.clone();
1203}
1204
1205async fn run_with_timeout<F, T>(
1206 timeout: Option<Duration>,
1207 timeout_label: &str,
1208 future: F,
1209) -> Result<T>
1210where
1211 F: Future<Output = Result<T>>,
1212{
1213 let Some(timeout) = timeout else {
1214 return future.await;
1215 };
1216 match tokio::time::timeout(timeout, future).await {
1217 Ok(result) => result,
1218 Err(_) => Err(CliCoreError::message(format!(
1219 "command timed out after {timeout_label}"
1220 ))),
1221 }
1222}
1223
1224async fn run_until_signal<Run, Shutdown>(run: Run, shutdown: Shutdown) -> CliRunOutput
1225where
1226 Run: Future<Output = CliRunOutput>,
1227 Shutdown: Future<Output = ()>,
1228{
1229 tokio::pin!(run);
1230 tokio::pin!(shutdown);
1231 tokio::select! {
1232 output = &mut run => output,
1233 () = &mut shutdown => CliRunOutput {
1234 exit_code: 130,
1235 rendered: "command interrupted\n".to_owned(),
1236 },
1237 }
1238}
1239
1240#[cfg(unix)]
1241async fn shutdown_signal() {
1242 let ctrl_c = tokio::signal::ctrl_c();
1243 match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
1244 Ok(mut sigterm) => {
1245 tokio::select! {
1246 _ = ctrl_c => {},
1247 _ = sigterm.recv() => {},
1248 }
1249 }
1250 Err(_) => {
1251 drop(ctrl_c.await);
1252 }
1253 }
1254}
1255
1256#[cfg(not(unix))]
1257async fn shutdown_signal() {
1258 drop(tokio::signal::ctrl_c().await);
1259}
1260
1261fn parse_command_timeout(raw: &str) -> Result<Option<Duration>> {
1262 let raw = raw.trim();
1263 if raw.is_empty() {
1264 return Ok(Some(Duration::from_secs(60)));
1265 }
1266 let Some(seconds) = parse_duration_seconds(raw) else {
1267 return Err(CliCoreError::message(format!(
1268 "invalid timeout {raw:?}: expected duration like 60s, 5m, or 0s"
1269 )));
1270 };
1271 if seconds <= 0.0 {
1272 Ok(None)
1273 } else {
1274 Ok(Some(Duration::from_secs_f64(seconds)))
1275 }
1276}
1277
1278fn parse_duration_seconds(raw: &str) -> Option<f64> {
1279 for (suffix, seconds) in [
1280 ("ns", 0.000_000_001_f64),
1281 ("us", 0.000_001_f64),
1282 ("µs", 0.000_001_f64),
1283 ("ms", 0.001_f64),
1284 ("s", 1.0_f64),
1285 ("m", 60.0_f64),
1286 ("h", 3600.0_f64),
1287 ] {
1288 if let Some(number) = raw.strip_suffix(suffix) {
1289 let value = number.parse::<f64>().ok()?;
1290 if !value.is_finite() {
1291 return None;
1292 }
1293 return Some(value * seconds);
1294 }
1295 }
1296 None
1297}
1298
1299fn render_cli_error(
1300 middleware: &Middleware,
1301 err: &(dyn std::error::Error + 'static),
1302 system: &str,
1303) -> CliRunOutput {
1304 let format = middleware
1305 .output_format
1306 .parse::<crate::output::OutputFormat>()
1307 .unwrap_or(crate::output::OutputFormat::Json);
1308 let envelope =
1309 crate::output::build_error_envelope(err, system).prepare_for_render(&middleware.verbose);
1310 match crate::output::render(format, &envelope) {
1311 Ok(rendered) => CliRunOutput {
1312 exit_code: exit_code_for_error(err),
1313 rendered,
1314 },
1315 Err(render_err) => CliRunOutput {
1316 exit_code: exit_code_for_error(err),
1317 rendered: render_err.to_string(),
1318 },
1319 }
1320}
1321
1322fn find_command_by_colon_path<'command>(
1323 root: &'command Command,
1324 path: &str,
1325) -> Option<&'command Command> {
1326 find_command_and_canonical_path_by_colon_path(root, path).map(|(command, _)| command)
1327}
1328
1329fn find_help_target<'command>(
1330 root: &'command Command,
1331 parts: &[&str],
1332) -> Option<&'command Command> {
1333 let mut current = root;
1334 let mut matched_any = false;
1335 for part in parts {
1336 let Some(next) = current.find_subcommand(part) else {
1337 break;
1338 };
1339 current = next;
1340 matched_any = true;
1341 }
1342 matched_any.then_some(current)
1343}
1344
1345fn find_command_and_canonical_path_by_colon_path<'command>(
1346 root: &'command Command,
1347 path: &str,
1348) -> Option<(&'command Command, Vec<String>)> {
1349 if path.is_empty() {
1350 return Some((root, Vec::new()));
1351 }
1352 let mut current = root;
1353 let mut canonical = Vec::new();
1354 for part in path.split(':') {
1355 current = current.find_subcommand(part)?;
1356 canonical.push(current.get_name().to_owned());
1357 }
1358 Some((current, canonical))
1359}
1360
1361fn canonical_path_from_parts(root: &Command, parts: &[String]) -> Option<String> {
1362 if parts.is_empty() {
1363 return Some(String::new());
1364 }
1365 let mut current = root;
1366 let mut canonical = Vec::new();
1367 for part in parts {
1368 current = current.find_subcommand(part)?;
1369 canonical.push(current.get_name().to_owned());
1370 }
1371 Some(canonical.join(":"))
1372}
1373
1374fn extract_search_scope_parts(args: &[String]) -> Vec<String> {
1375 let mut parts = Vec::new();
1376 let mut index = 1;
1377 while index < args.len() {
1378 let arg = &args[index];
1379 if arg == "--search" || arg.starts_with("--search=") {
1380 break;
1381 }
1382 if arg.starts_with('-') {
1383 if !arg.contains('=') && index + 1 < args.len() && !args[index + 1].starts_with('-') {
1384 index += 2;
1385 } else {
1386 index += 1;
1387 }
1388 continue;
1389 }
1390 parts.push(arg.clone());
1391 index += 1;
1392 }
1393 parts
1394}
1395
1396fn collect_command_search_documents(
1397 command: &Command,
1398 prefix: &mut Vec<String>,
1399 aliases: &mut Vec<String>,
1400 docs: &mut Vec<SearchDocument>,
1401) {
1402 if command.is_hide_set() || command.get_name() == "completion" {
1403 return;
1404 }
1405 if command.get_subcommands().next().is_some() {
1406 for child in command.get_subcommands() {
1407 prefix.push(child.get_name().to_owned());
1408 let alias_len = aliases.len();
1409 append_command_alias_terms(child, aliases);
1410 collect_command_search_documents(child, prefix, aliases, docs);
1411 aliases.truncate(alias_len);
1412 prefix.pop();
1413 }
1414 return;
1415 }
1416 if prefix.is_empty() {
1417 prefix.push(command.get_name().to_owned());
1418 append_command_alias_terms(command, aliases);
1419 }
1420 let path = prefix.join(" ");
1421 let alias_text = aliases.join(" ");
1422 docs.push(SearchDocument {
1423 id: format!("cmd:{path}"),
1424 kind: "command".to_owned(),
1425 title: path,
1426 summary: command
1427 .get_about()
1428 .map(ToString::to_string)
1429 .unwrap_or_default(),
1430 content: format!(
1431 "{} {} {} {}",
1432 command
1433 .get_about()
1434 .map(ToString::to_string)
1435 .unwrap_or_default(),
1436 command
1437 .get_long_about()
1438 .map(ToString::to_string)
1439 .unwrap_or_default(),
1440 command_flag_text(command),
1441 alias_text
1442 ),
1443 });
1444 if prefix.len() == 1 && prefix[0] == command.get_name() {
1445 prefix.pop();
1446 }
1447}
1448
1449fn append_command_alias_terms(command: &Command, aliases: &mut Vec<String>) {
1450 aliases.extend(command.get_all_aliases().map(str::to_owned));
1451 aliases.extend(
1452 command
1453 .get_all_short_flag_aliases()
1454 .map(|alias| alias.to_string()),
1455 );
1456 aliases.extend(command.get_all_long_flag_aliases().map(str::to_owned));
1457}
1458
1459fn command_flag_text(command: &Command) -> String {
1460 command
1461 .get_arguments()
1462 .filter_map(|arg| {
1463 let mut names = Vec::new();
1464 if let Some(short) = arg.get_short() {
1465 names.push(format!("-{short}"));
1466 }
1467 if let Some(long) = arg.get_long() {
1468 names.push(format!("--{long}"));
1469 }
1470 if let Some(short_aliases) = arg.get_all_short_aliases() {
1471 names.extend(
1472 short_aliases
1473 .into_iter()
1474 .map(|short_alias| format!("-{short_alias}")),
1475 );
1476 }
1477 if let Some(aliases) = arg.get_all_aliases() {
1478 names.extend(aliases.into_iter().map(|alias| format!("--{alias}")));
1479 }
1480 (!names.is_empty()).then(|| names.join(" "))
1481 })
1482 .collect::<Vec<_>>()
1483 .join(" ")
1484}
1485
1486fn has_subcommand(command: &Command, name: &str) -> bool {
1487 command
1488 .get_subcommands()
1489 .any(|child| child.get_name() == name)
1490}
1491
1492fn has_root_version_flag(args: &[String], root: &Command, root_name: &str) -> bool {
1493 let bool_flags = derive_bool_flags(root);
1494 let value_flags = derive_value_flags(root);
1495 let mut iter = args.iter().peekable();
1496 if iter
1497 .peek()
1498 .is_some_and(|arg| arg_matches_root_name(arg, root_name))
1499 {
1500 iter.next();
1501 }
1502
1503 while let Some(arg) = iter.next() {
1504 match arg.as_str() {
1505 "--version" | "-v" => return true,
1506 "--" => return false,
1507 value if value.contains('=') || bool_flags.contains(value) => continue,
1508 value
1509 if value_flags.contains(value)
1510 || unknown_flag_consumes_value(value, iter.peek()) =>
1511 {
1512 iter.next();
1513 }
1514 value if value.starts_with('-') => {}
1515 _ => return false,
1516 }
1517 }
1518 false
1519}
1520
1521fn normalize_optional_global_flags_before_command(root: &Command, args: &[String]) -> Vec<String> {
1522 let optional_string_defaults = BTreeMap::from([("--verbose", "all"), ("--debug", "*")]);
1523 let optional_bool_defaults = BTreeMap::from([("--dry-run", "true"), ("--schema", "true")]);
1524 let mut normalized = Vec::with_capacity(args.len());
1525 let mut index = 0;
1526 let mut current = root;
1527 while index < args.len() {
1528 let arg = &args[index];
1529 if index == 0 && arg_matches_root_name(arg, root.get_name()) {
1530 normalized.push(arg.clone());
1531 index += 1;
1532 continue;
1533 }
1534
1535 if let Some(default) = optional_bool_defaults.get(arg.as_str()) {
1536 normalized.push(format!("{arg}={default}"));
1537 index += 1;
1538 continue;
1539 }
1540
1541 if let Some(default) = optional_string_defaults.get(arg.as_str()) {
1542 match args.get(index + 1) {
1543 None => {
1544 normalized.push(format!("{arg}={default}"));
1545 index += 1;
1546 continue;
1547 }
1548 Some(next)
1549 if current.get_name() == root.get_name()
1550 || next.starts_with('-')
1551 || direct_subcommand(current, next).is_some() =>
1552 {
1553 normalized.push(format!("{arg}={default}"));
1554 index += 1;
1555 continue;
1556 }
1557 Some(next) => {
1558 normalized.push(arg.clone());
1559 normalized.push(next.clone());
1560 index += 2;
1561 continue;
1562 }
1563 }
1564 }
1565
1566 normalized.push(arg.clone());
1567 if !arg.starts_with('-')
1568 && let Some(next_command) = direct_subcommand(current, arg)
1569 {
1570 current = next_command;
1571 }
1572 index += 1;
1573 }
1574 normalized
1575}
1576
1577fn direct_subcommand<'command>(
1578 command: &'command Command,
1579 token: &str,
1580) -> Option<&'command Command> {
1581 command.get_subcommands().find(|child| {
1582 child.get_name() == token || child.get_all_aliases().any(|alias| alias == token)
1583 })
1584}
1585
1586fn unknown_group_command_message(
1587 root: &Command,
1588 args: &[String],
1589 root_name: &str,
1590) -> Option<String> {
1591 let bool_flags = derive_bool_flags(root);
1592 let value_flags = derive_value_flags(root);
1593 let positionals = positional_command_tokens(args, root_name, &bool_flags, &value_flags);
1594 if positionals.is_empty() {
1595 return None;
1596 }
1597
1598 let mut current = root;
1599 let mut path = vec![root.get_name().to_owned()];
1600 for token in positionals {
1601 if let Some(next) = current.find_subcommand(&token) {
1602 current = next;
1603 path.push(next.get_name().to_owned());
1604 continue;
1605 }
1606 if current.get_subcommands().next().is_some() {
1607 return Some(format!(
1608 "unknown command {token:?} for {:?}",
1609 path.join(" ")
1610 ));
1611 }
1612 return None;
1613 }
1614 None
1615}
1616
1617fn positional_command_tokens(
1618 args: &[String],
1619 root_name: &str,
1620 bool_flags: &BTreeSet<String>,
1621 value_flags: &BTreeSet<String>,
1622) -> Vec<String> {
1623 let mut tokens = Vec::new();
1624 let mut iter = args.iter().peekable();
1625 if iter
1626 .peek()
1627 .is_some_and(|arg| arg_matches_root_name(arg, root_name))
1628 {
1629 iter.next();
1630 }
1631
1632 while let Some(arg) = iter.next() {
1633 if arg == "--" {
1634 tokens.extend(iter.cloned());
1635 break;
1636 }
1637 if arg.contains('=') {
1638 continue;
1639 }
1640 if bool_flags.contains(arg) {
1641 continue;
1642 }
1643 if value_flags.contains(arg) || unknown_flag_consumes_value(arg, iter.peek()) {
1644 iter.next();
1645 continue;
1646 }
1647 if arg.starts_with('-') {
1648 continue;
1649 }
1650 tokens.push(arg.clone());
1651 }
1652 tokens
1653}
1654
1655fn unknown_flag_consumes_value(arg: &str, next: Option<&&String>) -> bool {
1656 arg.starts_with('-') && next.is_some_and(|value| !value.starts_with('-'))
1657}
1658
1659fn arg_matches_root_name(arg: &str, root_name: &str) -> bool {
1660 arg == root_name
1661 || std::path::Path::new(arg)
1662 .file_stem()
1663 .and_then(|n| n.to_str())
1664 .is_some_and(|n| n == root_name)
1665}
1666
1667fn register_runtime_group_schemas(
1668 group: &RuntimeGroupSpec,
1669 prefix: &mut Vec<String>,
1670 schemas: &mut SchemaRegistry,
1671) {
1672 prefix.push(group.group.name.clone());
1673 for child_group in &group.groups {
1674 register_runtime_group_schemas(child_group, prefix, schemas);
1675 }
1676 for child in &group.commands {
1677 prefix.push(child.spec.name.clone());
1678 register_command_schema(&child.spec, &prefix.join(":"), schemas);
1679 prefix.pop();
1680 }
1681 prefix.pop();
1682}
1683
1684fn register_command_schema(spec: &CommandSpec, command_path: &str, schemas: &mut SchemaRegistry) {
1685 if let Some(schema) = &spec.output_schema {
1686 schemas.register_info(command_path.to_owned(), schema.clone());
1687 }
1688}
1689
1690fn runtime_group_clap_command_with_schema_help(
1691 group: &RuntimeGroupSpec,
1692 prefix: &mut Vec<String>,
1693 schemas: &SchemaRegistry,
1694) -> Command {
1695 let mut command = group_clap_command_without_children(&group.group);
1696 prefix.push(group.group.name.clone());
1697 for child_group in &group.groups {
1698 command = command.subcommand(runtime_group_clap_command_with_schema_help(
1699 child_group,
1700 prefix,
1701 schemas,
1702 ));
1703 }
1704 for child in &group.commands {
1705 prefix.push(child.spec.name.clone());
1706 let command_path = prefix.join(":");
1707 command = command.subcommand(command_clap_command_with_schema_help(
1708 &child.spec,
1709 &command_path,
1710 schemas,
1711 ));
1712 prefix.pop();
1713 }
1714 prefix.pop();
1715 command
1716}
1717
1718fn group_clap_command_without_children(group: &GroupSpec) -> Command {
1719 let mut command = Command::new(group.name.clone()).about(group.short.clone());
1720 if let Some(long) = &group.long
1721 && !long.is_empty()
1722 {
1723 command = command.long_about(long.clone());
1724 }
1725 for alias in &group.aliases {
1726 command = command.alias(alias.clone());
1727 }
1728 if group.hidden {
1729 command = command.hide(true);
1730 }
1731 command
1732}
1733
1734fn command_clap_command_with_schema_help(
1735 spec: &CommandSpec,
1736 command_path: &str,
1737 schemas: &SchemaRegistry,
1738) -> Command {
1739 let mut command = spec.clap_command();
1740 let Some(schema) = schemas.get_by_path(command_path) else {
1741 return command;
1742 };
1743 let schema_help = format_help_section(&schema.fields);
1744 if schema_help.is_empty() {
1745 return command;
1746 }
1747 let base = spec
1748 .long
1749 .as_ref()
1750 .filter(|long| !long.is_empty())
1751 .cloned()
1752 .unwrap_or_else(|| spec.short.clone());
1753 let long = if base.is_empty() {
1754 schema_help
1755 } else {
1756 format!("{base}\n\n{schema_help}")
1757 };
1758 command = command.long_about(long);
1759 command
1760}
1761
1762fn process_exit_code(code: i32) -> ExitCode {
1763 if code == 0 {
1764 return ExitCode::SUCCESS;
1765 }
1766 match u8::try_from(code) {
1767 Ok(code) if code != 0 => ExitCode::from(code),
1768 Ok(_) | Err(_) => ExitCode::from(1),
1769 }
1770}
1771
1772async fn run_streaming_command(
1773 middleware: &Middleware,
1774 request: MiddlewareRequest<'_>,
1775 raw_matches: Arc<ArgMatches>,
1776 streaming_handler: crate::command::StreamingCommandHandler,
1777) -> Result<CliRunOutput> {
1778 use tokio::{io::AsyncWriteExt, sync::mpsc};
1779
1780 let args_for_handler = request.args.clone();
1781 let user_args_for_handler = request.user_args.clone();
1782 let handler_path = request.command_path.to_owned();
1783 let middleware_for_handler = middleware.clone();
1784 let raw_matches_for_handler = raw_matches;
1785
1786 let (tx, mut rx) = mpsc::channel::<serde_json::Value>(64);
1787 let sender = StreamSender(tx);
1788
1789 let writer = tokio::spawn(async move {
1793 let mut stdout = tokio::io::stdout();
1794 while let Some(event) = rx.recv().await {
1795 let Ok(line) = serde_json::to_string(&event) else {
1796 continue;
1797 };
1798 if stdout.write_all(line.as_bytes()).await.is_err()
1799 || stdout.write_all(b"\n").await.is_err()
1800 || stdout.flush().await.is_err()
1801 {
1802 break;
1803 }
1804 }
1805 });
1806
1807 let output = middleware
1808 .run(request, async move |credential| {
1809 streaming_handler(
1810 CommandContext {
1811 credential,
1812 args: args_for_handler,
1813 user_args: user_args_for_handler,
1814 command_path: handler_path,
1815 middleware: middleware_for_handler,
1816 raw_matches: raw_matches_for_handler,
1817 },
1818 sender,
1819 )
1820 .await?;
1821 Ok(crate::CommandResult::new(serde_json::Value::Null))
1822 })
1823 .await;
1824
1825 let _write_result = writer.await;
1828
1829 match output {
1830 Ok(out) if out.exit_code == 0 => Ok(CliRunOutput {
1831 exit_code: 0,
1832 rendered: String::new(),
1833 }),
1834 Ok(out) => Ok(out.into()),
1835 Err(err) => Ok(CliRunOutput {
1836 exit_code: exit_code_for_error(&err),
1837 rendered: render_cli_error(middleware, &err, middleware.app_id.as_str()).rendered,
1838 }),
1839 }
1840}