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, default_output_format, derive_bool_flags, derive_value_flags,
28 extract_command_path, extract_output_format, extract_search_query,
29 global_flags_from_matches, has_true_schema_flag, register_global_flags,
30 },
31 guide::guide_content,
32 module::{Module, ModuleContext},
33 output::{
34 HumanViewDef, NextAction, SchemaRegistry, format_help_section,
35 global_human_view_registry_snapshot, global_schema_registry_snapshot,
36 },
37 search::{SearchDocument, SearchIndex},
38};
39
40use builtins::{guide_args, guide_command, help_args, help_command};
41use help::{GROUP_HELP_TEMPLATE, ROOT_HELP_TEMPLATE};
42pub use help::{ModuleHelpEntry, build_root_long, render_next_actions_human};
43
44#[derive(Clone, Debug, Default, Eq, PartialEq)]
46pub struct BuildInfo {
47 pub version: String,
49 pub commit: Option<String>,
51 pub date: Option<String>,
53}
54
55impl BuildInfo {
56 #[must_use]
58 pub fn new(version: impl Into<String>) -> Self {
59 Self {
60 version: version.into(),
61 commit: None,
62 date: None,
63 }
64 }
65
66 #[must_use]
68 pub fn with_commit(mut self, commit: impl Into<String>) -> Self {
69 self.commit = Some(commit.into());
70 self
71 }
72
73 #[must_use]
75 pub fn with_date(mut self, date: impl Into<String>) -> Self {
76 self.date = Some(date.into());
77 self
78 }
79
80 #[must_use]
82 pub fn version_string(&self) -> String {
83 let commit = self.commit.as_deref().unwrap_or_default();
84 let date = self.date.as_deref().unwrap_or_default();
85
86 if commit.is_empty() && date.is_empty() {
87 self.version.clone()
88 } else {
89 format!("{} (commit {commit}, built {date})", self.version)
90 }
91 }
92}
93
94pub type InitDeps = Arc<dyn Fn(&mut Middleware) -> Result<()> + Send + Sync>;
96pub type RegisterFlags = Arc<dyn Fn(Command) -> Command + Send + Sync>;
98pub type ApplyFlags = Arc<dyn Fn(&ArgMatches, &mut Middleware) -> Result<()> + Send + Sync>;
100pub type PreRun =
102 Arc<dyn Fn(&mut Middleware, &str, &crate::middleware::ValueMap) -> Result<()> + Send + Sync>;
103pub type ResolveMeta = Arc<dyn Fn(&str, CommandMeta) -> CommandMeta + Send + Sync>;
105pub type OnShutdown = Arc<dyn Fn() + Send + Sync>;
107pub type ExtraSearchDocs = Arc<dyn Fn() -> Vec<SearchDocument> + Send + Sync>;
109pub type RootNextActions = Arc<dyn Fn() -> Vec<NextAction> + Send + Sync>;
113
114const DEFAULT_ADMIN_CATEGORY: &str = "Admin";
118
119#[derive(Clone, Default)]
125pub struct CliConfig {
126 pub name: String,
128 pub short: String,
130 pub long: Option<String>,
132 pub build: BuildInfo,
134 pub app_id: String,
136 pub default_auth_provider: Option<String>,
138 pub modules: Vec<Module>,
140 pub commands: Vec<RuntimeCommandSpec>,
142 pub guides: Vec<GuideEntry>,
144 pub views: Vec<HumanViewDef>,
146 pub auth_providers: Vec<Arc<dyn AuthProvider>>,
148 pub authz: Option<Arc<dyn Authorizer>>,
150 pub auditor: Option<Arc<dyn Auditor>>,
152 pub activity: Option<Arc<dyn ActivityEmitter>>,
154 pub init_deps: Option<InitDeps>,
156 pub register_flags: Option<RegisterFlags>,
158 pub apply_flags: Option<ApplyFlags>,
160 pub pre_run: Option<PreRun>,
162 pub meta_resolver: Option<ResolveMeta>,
164 pub on_shutdown: Option<OnShutdown>,
166 pub extra_search_docs: Option<ExtraSearchDocs>,
168 pub root_next_actions: Option<RootNextActions>,
170 pub admin_category: Option<String>,
175}
176
177impl CliConfig {
178 #[must_use]
180 pub fn new(
181 name: impl Into<String>,
182 short: impl Into<String>,
183 app_id: impl Into<String>,
184 ) -> Self {
185 Self {
186 name: name.into(),
187 short: short.into(),
188 app_id: app_id.into(),
189 ..Self::default()
190 }
191 }
192
193 #[must_use]
195 pub fn with_long(mut self, long: impl Into<String>) -> Self {
196 self.long = Some(long.into());
197 self
198 }
199
200 #[must_use]
202 pub fn with_build(mut self, build: BuildInfo) -> Self {
203 self.build = build;
204 self
205 }
206
207 #[must_use]
209 pub fn with_default_auth_provider(mut self, provider: impl Into<String>) -> Self {
210 self.default_auth_provider = Some(provider.into());
211 self
212 }
213
214 #[must_use]
216 pub fn with_module(mut self, module: Module) -> Self {
217 self.modules.push(module);
218 self
219 }
220
221 #[must_use]
223 pub fn with_modules(mut self, modules: impl IntoIterator<Item = Module>) -> Self {
224 self.modules.extend(modules);
225 self
226 }
227
228 #[must_use]
230 pub fn with_command(mut self, command: RuntimeCommandSpec) -> Self {
231 self.commands.push(command);
232 self
233 }
234
235 #[must_use]
237 pub fn with_guide(mut self, guide: GuideEntry) -> Self {
238 self.guides.push(guide);
239 self
240 }
241
242 #[must_use]
244 pub fn with_guides(mut self, guides: impl IntoIterator<Item = GuideEntry>) -> Self {
245 self.guides.extend(guides);
246 self
247 }
248
249 #[must_use]
251 pub fn with_view(mut self, view: HumanViewDef) -> Self {
252 self.views.push(view);
253 self
254 }
255
256 #[must_use]
258 pub fn with_auth_provider(mut self, provider: Arc<dyn AuthProvider>) -> Self {
259 self.auth_providers.push(provider);
260 self
261 }
262
263 #[must_use]
265 pub fn with_authz(mut self, authz: Arc<dyn Authorizer>) -> Self {
266 self.authz = Some(authz);
267 self
268 }
269
270 #[must_use]
272 pub fn with_auditor(mut self, auditor: Arc<dyn Auditor>) -> Self {
273 self.auditor = Some(auditor);
274 self
275 }
276
277 #[must_use]
279 pub fn with_activity(mut self, activity: Arc<dyn ActivityEmitter>) -> Self {
280 self.activity = Some(activity);
281 self
282 }
283
284 #[must_use]
286 pub fn with_init_deps(mut self, init_deps: InitDeps) -> Self {
287 self.init_deps = Some(init_deps);
288 self
289 }
290
291 #[must_use]
293 pub fn with_register_flags(mut self, register_flags: RegisterFlags) -> Self {
294 self.register_flags = Some(register_flags);
295 self
296 }
297
298 #[must_use]
300 pub fn with_apply_flags(mut self, apply_flags: ApplyFlags) -> Self {
301 self.apply_flags = Some(apply_flags);
302 self
303 }
304
305 #[must_use]
307 pub fn with_pre_run(mut self, pre_run: PreRun) -> Self {
308 self.pre_run = Some(pre_run);
309 self
310 }
311
312 #[must_use]
314 pub fn with_meta_resolver(mut self, meta_resolver: ResolveMeta) -> Self {
315 self.meta_resolver = Some(meta_resolver);
316 self
317 }
318
319 #[must_use]
321 pub fn with_on_shutdown(mut self, on_shutdown: OnShutdown) -> Self {
322 self.on_shutdown = Some(on_shutdown);
323 self
324 }
325
326 #[must_use]
328 pub fn with_extra_search_docs(mut self, extra_search_docs: ExtraSearchDocs) -> Self {
329 self.extra_search_docs = Some(extra_search_docs);
330 self
331 }
332
333 #[must_use]
335 pub fn with_root_next_actions(mut self, root_next_actions: RootNextActions) -> Self {
336 self.root_next_actions = Some(root_next_actions);
337 self
338 }
339
340 #[must_use]
344 pub fn with_admin_category(mut self, category: impl Into<String>) -> Self {
345 self.admin_category = Some(category.into());
346 self
347 }
348}
349
350impl std::fmt::Debug for CliConfig {
351 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
352 formatter
353 .debug_struct("CliConfig")
354 .field("name", &self.name)
355 .field("short", &self.short)
356 .field("long", &self.long)
357 .field("build", &self.build)
358 .field("app_id", &self.app_id)
359 .field("default_auth_provider", &self.default_auth_provider)
360 .field("modules", &self.modules)
361 .field("commands", &self.commands)
362 .field("guides", &self.guides)
363 .field("views", &self.views)
364 .field("auth_providers_len", &self.auth_providers.len())
365 .field("has_authz", &self.authz.is_some())
366 .field("has_auditor", &self.auditor.is_some())
367 .field("has_activity", &self.activity.is_some())
368 .field("has_init_deps", &self.init_deps.is_some())
369 .field("has_register_flags", &self.register_flags.is_some())
370 .field("has_apply_flags", &self.apply_flags.is_some())
371 .field("has_pre_run", &self.pre_run.is_some())
372 .field("has_meta_resolver", &self.meta_resolver.is_some())
373 .field("has_on_shutdown", &self.on_shutdown.is_some())
374 .field("has_extra_search_docs", &self.extra_search_docs.is_some())
375 .field("has_root_next_actions", &self.root_next_actions.is_some())
376 .field("admin_category", &self.admin_category)
377 .finish()
378 }
379}
380
381#[derive(Clone, Debug, PartialEq)]
383pub struct CliRunOutput {
384 pub exit_code: i32,
386 pub rendered: String,
388}
389
390impl From<crate::middleware::MiddlewareOutput> for CliRunOutput {
391 fn from(o: crate::middleware::MiddlewareOutput) -> Self {
392 Self {
393 exit_code: o.exit_code,
394 rendered: o.rendered,
395 }
396 }
397}
398
399#[derive(Clone)]
405pub struct Cli {
406 config: CliConfig,
407 middleware: Middleware,
408 root: Command,
409 commands: BTreeMap<String, RuntimeCommandSpec>,
410 module_entries: Vec<ModuleHelpEntry>,
411 guide_entries: Vec<GuideEntry>,
412 init_deps: Option<InitDeps>,
413 apply_flags: Option<ApplyFlags>,
414 pre_run: Option<PreRun>,
415 meta_resolver: Option<ResolveMeta>,
416 on_shutdown: Option<OnShutdown>,
417 extra_search_docs: Option<ExtraSearchDocs>,
418 root_next_actions: Option<RootNextActions>,
419 init_state: Arc<Mutex<Option<std::result::Result<Middleware, InitFailure>>>>,
420}
421
422#[derive(Clone, Debug, Eq, PartialEq)]
423struct InitFailure {
424 message: String,
425 code: String,
426 system: String,
427 request_id: String,
428 exit_code: i32,
429}
430
431impl InitFailure {
432 fn capture(err: &CliCoreError) -> Self {
433 let envelope = crate::output::build_error_envelope(err, "");
434 let (code, system, request_id) = envelope.error.map_or_else(
435 || ("ERROR".to_owned(), String::new(), String::new()),
436 |error| (error.code, error.system, error.request_id),
437 );
438 Self {
439 message: err.to_string(),
440 code,
441 system,
442 request_id,
443 exit_code: exit_code_for_error(err),
444 }
445 }
446
447 fn into_error(self) -> CliCoreError {
448 CliCoreError::with_exit_code(
449 self.exit_code,
450 CliCoreError::SystemMessage {
451 message: self.message,
452 system: self.system,
453 code: self.code,
454 request_id: self.request_id,
455 },
456 )
457 }
458}
459
460impl std::fmt::Debug for Cli {
461 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
462 formatter
463 .debug_struct("Cli")
464 .field("config", &self.config)
465 .field("middleware", &self.middleware)
466 .field("root", &self.root)
467 .field("commands", &self.commands)
468 .field("module_entries", &self.module_entries)
469 .field("guide_entries", &self.guide_entries)
470 .field("has_init_deps", &self.init_deps.is_some())
471 .field("has_apply_flags", &self.apply_flags.is_some())
472 .field("has_pre_run", &self.pre_run.is_some())
473 .field("has_meta_resolver", &self.meta_resolver.is_some())
474 .field("has_on_shutdown", &self.on_shutdown.is_some())
475 .field("has_extra_search_docs", &self.extra_search_docs.is_some())
476 .field("has_root_next_actions", &self.root_next_actions.is_some())
477 .finish()
478 }
479}
480
481impl Cli {
482 #[must_use]
484 pub fn new(config: CliConfig) -> Self {
485 let auth_providers = config.auth_providers.clone();
486 let guides = config.guides.clone();
487 let views = config.views.clone();
488 let modules = config.modules.clone();
489 let commands = config.commands.clone();
490 let init_deps = config.init_deps.clone();
491 let apply_flags = config.apply_flags.clone();
492 let pre_run = config.pre_run.clone();
493 let meta_resolver = config.meta_resolver.clone();
494 let on_shutdown = config.on_shutdown.clone();
495 let extra_search_docs = config.extra_search_docs.clone();
496 let root_next_actions = config.root_next_actions.clone();
497 let mut root = Command::new(config.name.clone())
498 .about(config.short.clone())
499 .disable_help_subcommand(true)
500 .version(config.build.version_string());
501 if let Some(long) = &config.long
502 && !long.is_empty()
503 {
504 root = root.long_about(long.clone());
505 }
506 root = register_global_flags(root)
507 .subcommand(help_command())
508 .subcommand(guide_command())
509 .subcommand(Command::new("tree").about("Display full command tree"));
510 if let Some(register_flags) = &config.register_flags {
511 root = register_flags(root);
512 }
513 let intro = config
514 .long
515 .as_deref()
516 .filter(|long| !long.is_empty())
517 .unwrap_or(config.short.as_str());
518 root = root
519 .long_about(build_root_long(intro, &[], false))
520 .help_template(ROOT_HELP_TEMPLATE);
521
522 let mut middleware = Middleware::new();
523 middleware.app_id = config.app_id.clone();
524 middleware.default_auth_provider = config.default_auth_provider.clone().unwrap_or_default();
525 middleware.authz = config.authz.clone();
526 middleware.auditor = config.auditor.clone();
527 middleware.activity = config.activity.clone();
528 middleware
529 .schema_registry
530 .merge(&global_schema_registry_snapshot());
531 middleware
532 .human_views
533 .merge(&global_human_view_registry_snapshot());
534
535 let mut cli = Self {
536 config,
537 middleware,
538 root,
539 commands: BTreeMap::new(),
540 module_entries: Vec::new(),
541 guide_entries: Vec::new(),
542 init_deps,
543 apply_flags,
544 pre_run,
545 meta_resolver,
546 on_shutdown,
547 extra_search_docs,
548 root_next_actions,
549 init_state: Arc::new(Mutex::new(None)),
550 };
551 for provider in auth_providers {
552 cli.register_auth_provider(provider);
553 }
554 if cli.middleware.default_auth_provider.is_empty()
555 && let Some(provider) = cli.middleware.auth.registered_names().first()
556 {
557 cli.middleware.default_auth_provider = provider.clone();
558 }
559 if !cli.middleware.default_auth_provider.is_empty() {
560 cli.ensure_auth_command();
561 }
562 for view in views {
563 cli.middleware.human_views.register(view);
564 }
565 cli.add_guides(guides);
566 for module in modules {
567 cli.add_module(module);
568 }
569 for command in commands {
570 cli.add_command(command);
571 }
572 cli
573 }
574
575 fn register_auth_help_entry(&mut self) {
580 let category = self
581 .config
582 .admin_category
583 .clone()
584 .unwrap_or_else(|| DEFAULT_ADMIN_CATEGORY.to_owned());
585 let already_listed = self.module_entries.iter().any(|entry| entry.name == "auth");
586 let short = self
587 .root
588 .find_subcommand("auth")
589 .filter(|auth| !auth.is_hide_set())
590 .map(|auth| {
591 auth.get_about()
592 .map(ToString::to_string)
593 .unwrap_or_default()
594 });
595 if !already_listed && let Some(short) = short {
596 self.module_entries.push(ModuleHelpEntry {
597 category,
598 name: "auth".to_owned(),
599 short,
600 });
601 }
602 self.refresh_root_long();
603 }
604
605 #[must_use]
607 pub fn middleware(&self) -> &Middleware {
608 &self.middleware
609 }
610
611 pub fn middleware_mut(&mut self) -> &mut Middleware {
613 &mut self.middleware
614 }
615
616 pub async fn execute(&self) -> ExitCode {
618 let mut stdout = std::io::stdout().lock();
619 let mut stderr = std::io::stderr().lock();
620 match self
621 .execute_from(std::env::args_os(), &mut stdout, &mut stderr)
622 .await
623 {
624 Ok(code) => code,
625 Err(err) => {
626 drop(writeln!(stderr, "{err}"));
627 ExitCode::from(1)
628 }
629 }
630 }
631
632 pub async fn execute_from<I, S, O, E>(
634 &self,
635 args: I,
636 stdout: &mut O,
637 stderr: &mut E,
638 ) -> std::io::Result<ExitCode>
639 where
640 I: IntoIterator<Item = S>,
641 S: Into<std::ffi::OsString> + Clone,
642 O: Write,
643 E: Write,
644 {
645 self.execute_from_until_signal(args, stdout, stderr, shutdown_signal())
646 .await
647 }
648
649 pub async fn execute_from_until_signal<I, S, O, E, Shutdown>(
651 &self,
652 args: I,
653 stdout: &mut O,
654 stderr: &mut E,
655 shutdown: Shutdown,
656 ) -> std::io::Result<ExitCode>
657 where
658 I: IntoIterator<Item = S>,
659 S: Into<std::ffi::OsString> + Clone,
660 O: Write,
661 E: Write,
662 Shutdown: Future<Output = ()>,
663 {
664 let output = run_until_signal(self.run(args), shutdown).await;
665 if output.exit_code == 130
666 && output.rendered == "command interrupted\n"
667 && let Some(on_shutdown) = &self.on_shutdown
668 {
669 on_shutdown();
670 }
671 if output.exit_code == 0 {
672 stdout.write_all(output.rendered.as_bytes())?;
673 } else {
674 stderr.write_all(output.rendered.as_bytes())?;
675 }
676 Ok(process_exit_code(output.exit_code))
677 }
678
679 pub fn register_auth_provider(&mut self, provider: Arc<dyn AuthProvider>) -> &mut Self {
681 self.middleware.auth.register(provider);
682 self.ensure_auth_command();
683 self.refresh_root_long();
684 self
685 }
686
687 #[must_use]
689 pub fn root_command(&self) -> &Command {
690 &self.root
691 }
692
693 pub fn add_module_group(
695 &mut self,
696 category: impl Into<String>,
697 group: RuntimeGroupSpec,
698 ) -> &mut Self {
699 let category = category.into();
700 if !group.group.hidden {
701 self.module_entries.push(ModuleHelpEntry {
702 category,
703 name: group.group.name.clone(),
704 short: group.group.short.clone(),
705 });
706 }
707
708 let mut prefix = Vec::new();
709 register_runtime_group_schemas(&group, &mut prefix, &mut self.middleware.schema_registry);
710 let mut prefix = Vec::new();
711 group.register_commands(&mut prefix, &mut self.commands);
712 let mut prefix = Vec::new();
713 let clap_group = runtime_group_clap_command_with_schema_help(
714 &group,
715 &mut prefix,
716 &self.middleware.schema_registry,
717 );
718 self.root = self.root.clone().subcommand(clap_group);
719 self.refresh_root_long();
720 self
721 }
722
723 pub fn add_module(&mut self, module: Module) -> &mut Self {
725 for view in module.views.clone() {
726 self.middleware.human_views.register(view);
727 }
728 self.add_guides(module.guides.clone());
729 let mut context = ModuleContext::new(&mut self.middleware);
730 let group = (module.register)(&mut context);
731 let (guides, views) = context.into_parts();
732 for view in views {
733 self.middleware.human_views.register(view);
734 }
735 self.add_guides(guides);
736 self.add_module_group(module.category, group)
737 }
738
739 pub fn add_command(&mut self, command: RuntimeCommandSpec) -> &mut Self {
741 let name = command.spec.name.clone();
742 register_command_schema(&command.spec, &name, &mut self.middleware.schema_registry);
743 self.commands.insert(name, command.clone());
744 self.root = self
745 .root
746 .clone()
747 .subcommand(command_clap_command_with_schema_help(
748 &command.spec,
749 &command.spec.name,
750 &self.middleware.schema_registry,
751 ));
752 self
753 }
754
755 pub fn set_has_guide(&mut self, has_guide: bool) -> &mut Self {
757 if has_guide && self.guide_entries.is_empty() && !has_subcommand(&self.root, "guide") {
758 self.root = self.root.clone().subcommand(guide_command());
759 }
760 self.refresh_root_long();
761 self
762 }
763
764 pub fn add_guides(&mut self, entries: impl IntoIterator<Item = GuideEntry>) -> &mut Self {
766 let mut seen = self
767 .guide_entries
768 .iter()
769 .map(|entry| entry.name.clone())
770 .collect::<BTreeSet<_>>();
771 for entry in entries {
772 if seen.insert(entry.name.clone()) {
773 self.guide_entries.push(entry);
774 }
775 }
776 if !self.guide_entries.is_empty() && !has_subcommand(&self.root, "guide") {
777 self.root = self.root.clone().subcommand(guide_command());
778 }
779 self.refresh_root_long();
780 self
781 }
782
783 pub async fn run<I, S>(&self, args: I) -> CliRunOutput
785 where
786 I: IntoIterator<Item = S>,
787 S: Into<std::ffi::OsString> + Clone,
788 {
789 let raw_args = args
790 .into_iter()
791 .map(Into::into)
792 .collect::<Vec<std::ffi::OsString>>();
793 let text_args = raw_args
794 .iter()
795 .map(|arg| arg.to_string_lossy().into_owned())
796 .collect::<Vec<_>>();
797 let mut clap_args = normalize_optional_global_flags_before_command(&self.root, &text_args);
798 if has_root_version_flag(&text_args, &self.root, &self.config.name) {
799 return self.finish_run(CliRunOutput {
800 exit_code: 0,
801 rendered: format!(
802 "{} version {}\n",
803 self.config.name,
804 self.config.build.version_string()
805 ),
806 });
807 }
808 if let Some(output) = self.try_run_schema_bypass(&text_args) {
809 return output;
810 }
811 if let Some(output) = self.try_run_search_bypass(&text_args) {
812 return output;
813 }
814 let bool_flags = derive_bool_flags(&self.root);
817 let value_flags = derive_value_flags(&self.root);
818 let positionals =
819 positional_command_tokens(&text_args, &self.config.name, &bool_flags, &value_flags);
820 let command_keyword_count = match text_args.iter().position(|arg| arg == "--") {
825 Some(end) => positional_command_tokens(
826 &text_args[..end],
827 &self.config.name,
828 &bool_flags,
829 &value_flags,
830 )
831 .len(),
832 None => positionals.len(),
833 };
834 if let Some(parts) =
835 group_help_target_parts(&self.root, &positionals, command_keyword_count)
836 {
837 clap_args = rewrite_group_help_args(
844 &clap_args,
845 &self.config.name,
846 &bool_flags,
847 &value_flags,
848 &parts,
849 );
850 } else if let Some(message) = unknown_group_command_message(&self.root, &positionals) {
851 return self.finish_run(CliRunOutput {
852 exit_code: 1,
853 rendered: message,
854 });
855 }
856
857 let matches = match self.root.clone().try_get_matches_from(clap_args) {
858 Ok(matches) => matches,
859 Err(err) => {
860 return self.finish_run(CliRunOutput {
861 exit_code: err.exit_code(),
862 rendered: err.to_string(),
863 });
864 }
865 };
866
867 let default_format = default_output_format(&self.config.app_id);
868 let flags = global_flags_from_matches(&matches, &default_format);
869 let command_timeout = match parse_command_timeout(&flags.timeout) {
870 Ok(timeout) => timeout,
871 Err(err) => {
872 return self.finish_run(render_cli_error(
873 &self.middleware,
874 &err,
875 &self.config.app_id,
876 ));
877 }
878 };
879 let mut middleware = self.middleware.clone();
880 apply_global_flags(&mut middleware, &flags, command_timeout);
881 if let Err(err) = self.apply_config_flags(&matches, &mut middleware) {
882 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
883 }
884
885 let command_path = command_path_from_matches(&self.config.name, &matches);
886 if command_path == "help" {
887 if let Err(err) = self.run_pre_run(&mut middleware, &command_path, &help_args(&matches))
888 {
889 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
890 }
891 return self.finish_run(self.render_help_command(&matches));
892 }
893 if command_path == "tree" {
894 if let Err(err) = self.run_pre_run(
895 &mut middleware,
896 &command_path,
897 &crate::middleware::ValueMap::new(),
898 ) {
899 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
900 }
901 return self.finish_run(tree_render::render_tree(
902 &self.root,
903 &self.config.app_id,
904 &middleware,
905 ));
906 }
907 if command_path == "guide" {
908 if let Err(err) =
909 self.run_pre_run(&mut middleware, &command_path, &guide_args(&matches))
910 {
911 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
912 }
913 return self.finish_run(self.render_guide(&matches));
914 }
915 let Some(command) = self.commands.get(&command_path) else {
916 if !command_path.is_empty()
917 && let Some(group) = find_command_by_colon_path(&self.root, &command_path)
918 && group.get_subcommands().next().is_some()
919 {
920 if let Err(err) = self.run_pre_run(
921 &mut middleware,
922 &command_path,
923 &crate::middleware::ValueMap::new(),
924 ) {
925 return self.finish_run(render_cli_error(
926 &middleware,
927 &err,
928 &self.config.app_id,
929 ));
930 }
931 return self.finish_run(CliRunOutput {
932 exit_code: 0,
933 rendered: group.clone().render_long_help().to_string(),
934 });
935 }
936 if command_path.is_empty()
937 && let Some(root_next_actions) = &self.root_next_actions
938 {
939 let actions = root_next_actions();
944 return self.finish_run(self.render_root(&middleware, actions));
945 }
946 return self.finish_run(CliRunOutput {
947 exit_code: if command_path.is_empty() { 0 } else { 1 },
948 rendered: if command_path.is_empty() {
949 self.root.clone().render_long_help().to_string()
950 } else {
951 format!("unknown command {command_path:?}")
952 },
953 });
954 };
955
956 let mut middleware = match self.initialized_middleware() {
957 Ok(middleware) => middleware,
958 Err(err) => {
959 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
960 }
961 };
962 apply_global_flags(&mut middleware, &flags, command_timeout);
963 if let Err(err) = self.apply_config_flags(&matches, &mut middleware) {
964 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
965 }
966
967 let leaf = leaf_matches(&matches);
968 let args = command_args_from_matches(leaf, &command.spec, false);
969 let user_args = command_args_from_matches(leaf, &command.spec, true);
970 if let Err(err) = self.run_pre_run(&mut middleware, &command_path, &args) {
971 return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id));
972 }
973 let meta = self.resolve_meta(&command_path, command.spec.metadata());
974 let default_fields = command.spec.default_fields.clone().unwrap_or_default();
975 let system = command.spec.system.clone().unwrap_or_default();
976
977 if let Some(streaming_handler) = command.streaming_handler.clone() {
978 let result = run_with_timeout(
979 command_timeout,
980 &flags.timeout,
981 run_streaming_command(
982 &middleware,
983 MiddlewareRequest {
984 meta,
985 command_path: &command_path,
986 system: &system,
987 user_args,
988 args,
989 default_fields: &default_fields,
990 auth: command.spec.auth,
991 },
992 Arc::new(leaf.clone()),
993 streaming_handler,
994 ),
995 )
996 .await;
997 return self.finish_run(match result {
998 Ok(output) => output,
999 Err(err) => render_cli_error(&middleware, &err, &self.config.app_id),
1000 });
1001 }
1002
1003 let handler = command.handler.clone();
1004 let args_for_handler = args.clone();
1005 let user_args_for_handler = user_args.clone();
1006 let handler_path = command_path.clone();
1007 let middleware_for_handler = middleware.clone();
1008 let raw_matches_for_handler = Arc::new(leaf.clone());
1009 let result = run_with_timeout(
1010 command_timeout,
1011 &flags.timeout,
1012 middleware.run(
1013 MiddlewareRequest {
1014 meta,
1015 command_path: &command_path,
1016 system: &system,
1017 user_args,
1018 args,
1019 default_fields: &default_fields,
1020 auth: command.spec.auth,
1021 },
1022 async move |credential| {
1023 handler(CommandContext {
1024 credential,
1025 args: args_for_handler,
1026 user_args: user_args_for_handler,
1027 command_path: handler_path,
1028 middleware: middleware_for_handler,
1029 raw_matches: raw_matches_for_handler,
1030 })
1031 .await
1032 },
1033 ),
1034 )
1035 .await;
1036
1037 match result {
1038 Ok(output) => self.finish_run(output.into()),
1039 Err(err) => self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id)),
1040 }
1041 }
1042
1043 fn try_run_search_bypass(&self, args: &[String]) -> Option<CliRunOutput> {
1044 let query = extract_search_query(args);
1045 if query.is_empty() {
1046 return None;
1047 }
1048 let scope = self.search_scope(args);
1049 let output_format =
1050 extract_output_format(args, &default_output_format(&self.config.app_id));
1051 Some(self.render_search(&query, &scope, &output_format))
1052 }
1053
1054 fn try_run_schema_bypass(&self, args: &[String]) -> Option<CliRunOutput> {
1055 if !has_true_schema_flag(args) {
1056 return None;
1057 }
1058 let bool_flags = derive_bool_flags(&self.root);
1059 let value_flags = derive_value_flags(&self.root);
1060 let command_path =
1061 self.canonical_command_path(&extract_command_path(args, &bool_flags, &value_flags));
1062 let schema = self.middleware.schema_registry.get_by_path(&command_path)?;
1063 let output_format =
1064 extract_output_format(args, &default_output_format(&self.config.app_id));
1065 Some(self.render_schema(schema, &output_format))
1066 }
1067
1068 fn render_schema(
1069 &self,
1070 schema: crate::output::SchemaInfo,
1071 output_format: &str,
1072 ) -> CliRunOutput {
1073 let format: crate::output::OutputFormat = match output_format.parse() {
1074 Ok(format) => format,
1075 Err(err) => {
1076 return CliRunOutput {
1077 exit_code: exit_code_for_error(&err),
1078 rendered: err.to_string(),
1079 };
1080 }
1081 };
1082 let envelope =
1083 crate::Envelope::success(schema, self.config.app_id.clone()).prepare_for_render("");
1084 match crate::output::render(format, &envelope) {
1085 Ok(rendered) => CliRunOutput {
1086 exit_code: 0,
1087 rendered,
1088 },
1089 Err(err) => CliRunOutput {
1090 exit_code: exit_code_for_error(&err),
1091 rendered: err.to_string(),
1092 },
1093 }
1094 }
1095
1096 fn render_search(&self, query: &str, scope: &str, output_format: &str) -> CliRunOutput {
1097 let format: crate::output::OutputFormat = match output_format.parse() {
1098 Ok(format) => format,
1099 Err(err) => {
1100 return CliRunOutput {
1101 exit_code: exit_code_for_error(&err),
1102 rendered: err.to_string(),
1103 };
1104 }
1105 };
1106 let docs = self.search_documents(scope);
1107 let results = SearchIndex::new(docs).search(query, 10);
1108 let envelope =
1109 crate::Envelope::success(results, self.config.app_id.clone()).prepare_for_render("");
1110 match crate::output::render(format, &envelope) {
1111 Ok(rendered) => CliRunOutput {
1112 exit_code: 0,
1113 rendered,
1114 },
1115 Err(err) => CliRunOutput {
1116 exit_code: exit_code_for_error(&err),
1117 rendered: err.to_string(),
1118 },
1119 }
1120 }
1121
1122 fn render_root(&self, middleware: &Middleware, actions: Vec<NextAction>) -> CliRunOutput {
1128 if !crate::output::is_valid_output_format(&middleware.output_format) {
1133 let err = CliCoreError::InvalidOutputFormat(middleware.output_format.clone());
1134 return CliRunOutput {
1135 exit_code: exit_code_for_error(&err),
1136 rendered: err.to_string(),
1137 };
1138 }
1139 let format = middleware
1140 .output_format
1141 .parse()
1142 .unwrap_or(crate::output::OutputFormat::Json);
1143 if format == crate::output::OutputFormat::Human {
1144 let base_long = self
1148 .root
1149 .get_long_about()
1150 .map(ToString::to_string)
1151 .unwrap_or_default();
1152 let long = format!("{base_long}{}", render_next_actions_human(&actions));
1153 let rendered = self
1154 .root
1155 .clone()
1156 .long_about(long)
1157 .render_long_help()
1158 .to_string();
1159 return CliRunOutput {
1160 exit_code: 0,
1161 rendered,
1162 };
1163 }
1164 let description = self
1165 .config
1166 .long
1167 .as_deref()
1168 .filter(|long| !long.is_empty())
1169 .unwrap_or(self.config.short.as_str());
1170 let data = serde_json::json!({
1171 "description": description,
1172 "version": self.config.build.version,
1173 });
1174 let envelope = crate::Envelope::success(data, self.config.app_id.clone())
1175 .with_next_actions(actions)
1176 .prepare_for_render(&middleware.verbose);
1177 match crate::output::render(format, &envelope) {
1178 Ok(rendered) => CliRunOutput {
1179 exit_code: 0,
1180 rendered,
1181 },
1182 Err(err) => CliRunOutput {
1183 exit_code: exit_code_for_error(&err),
1184 rendered: err.to_string(),
1185 },
1186 }
1187 }
1188
1189 fn search_documents(&self, scope: &str) -> Vec<SearchDocument> {
1190 let (scoped, mut prefix) = find_command_and_canonical_path_by_colon_path(&self.root, scope)
1191 .unwrap_or((&self.root, Vec::new()));
1192 let mut docs = Vec::new();
1193 let mut aliases = Vec::new();
1194 append_command_alias_terms(scoped, &mut aliases);
1195 collect_command_search_documents(scoped, &mut prefix, &mut aliases, &mut docs);
1196 if scope.is_empty() {
1197 for entry in &self.guide_entries {
1198 docs.push(SearchDocument {
1199 id: format!("guide:{}", entry.name),
1200 kind: "guide".to_owned(),
1201 title: format!("guide {}", entry.name),
1202 summary: entry.summary.clone(),
1203 content: format!("{} {}", entry.summary, entry.content),
1204 });
1205 }
1206 if let Some(extra_search_docs) = &self.extra_search_docs {
1207 docs.extend(extra_search_docs());
1208 }
1209 }
1210 docs
1211 }
1212
1213 fn search_scope(&self, args: &[String]) -> String {
1214 let parts = extract_search_scope_parts(args);
1215 canonical_path_from_parts(&self.root, &parts).unwrap_or_default()
1216 }
1217
1218 fn canonical_command_path(&self, command_path: &str) -> String {
1219 find_command_and_canonical_path_by_colon_path(&self.root, command_path).map_or_else(
1220 || command_path.to_owned(),
1221 |(_, canonical)| canonical.join(":"),
1222 )
1223 }
1224
1225 fn render_guide(&self, matches: &ArgMatches) -> CliRunOutput {
1226 let leaf = leaf_matches(matches);
1227 let topic = leaf.get_one::<String>("topic").map(String::as_str);
1228 match guide_content(&self.guide_entries, topic) {
1229 Ok(rendered) => CliRunOutput {
1230 exit_code: 0,
1231 rendered,
1232 },
1233 Err(err) => CliRunOutput {
1234 exit_code: 1,
1235 rendered: err,
1236 },
1237 }
1238 }
1239
1240 fn render_help_command(&self, matches: &ArgMatches) -> CliRunOutput {
1241 let leaf = leaf_matches(matches);
1242 let parts = leaf
1243 .get_many::<String>("command")
1244 .map(|values| values.map(String::as_str).collect::<Vec<_>>())
1245 .unwrap_or_default();
1246 self.render_help_for_parts(&parts)
1247 }
1248
1249 fn render_help_for_parts(&self, parts: &[&str]) -> CliRunOutput {
1256 if parts.is_empty() {
1257 return CliRunOutput {
1258 exit_code: 0,
1259 rendered: self.root.clone().render_long_help().to_string(),
1260 };
1261 }
1262 let Some(command) = find_help_target(&self.root, parts) else {
1263 return CliRunOutput {
1264 exit_code: 1,
1265 rendered: format!(
1266 "unknown command {:?} — run '{} help' for available commands",
1267 parts.join(" "),
1268 self.config.name
1269 ),
1270 };
1271 };
1272 CliRunOutput {
1273 exit_code: 0,
1274 rendered: command.clone().render_long_help().to_string(),
1275 }
1276 }
1277
1278 fn refresh_root_long(&mut self) {
1279 const BUILTINS: [&str; 4] = ["help", "guide", "tree", "completion"];
1284 let categorized: BTreeSet<&str> = self
1285 .module_entries
1286 .iter()
1287 .map(|entry| entry.name.as_str())
1288 .collect();
1289 let mut generic: Vec<ModuleHelpEntry> = self
1290 .root
1291 .get_subcommands()
1292 .filter(|command| !command.is_hide_set())
1293 .filter(|command| !BUILTINS.contains(&command.get_name()))
1294 .filter(|command| !categorized.contains(command.get_name()))
1295 .map(|command| ModuleHelpEntry {
1296 category: "Commands".to_owned(),
1297 name: command.get_name().to_owned(),
1298 short: command
1299 .get_about()
1300 .map(ToString::to_string)
1301 .unwrap_or_default(),
1302 })
1303 .collect();
1304 generic.sort_by(|left, right| left.name.cmp(&right.name));
1305
1306 let mut entries = self.module_entries.clone();
1307 entries.extend(generic);
1308 let has_guide = !self.guide_entries.is_empty() || has_subcommand(&self.root, "guide");
1309 let intro = self
1310 .config
1311 .long
1312 .as_deref()
1313 .filter(|long| !long.is_empty())
1314 .unwrap_or(self.config.short.as_str());
1315 self.root = self
1316 .root
1317 .clone()
1318 .long_about(build_root_long(intro, &entries, has_guide));
1319 }
1320
1321 fn ensure_auth_command(&mut self) {
1322 let default_provider = self.default_auth_provider();
1323 let registered_names = self.middleware.auth.registered_names();
1324 if default_provider.is_empty() && registered_names.is_empty() {
1325 return;
1326 }
1327 let replacing_builtin = self.commands.contains_key("auth:login");
1328 if has_subcommand(&self.root, "auth") && !replacing_builtin {
1329 return;
1330 }
1331 let group = auth_command_group(&default_provider, ®istered_names);
1332 let mut prefix = Vec::new();
1333 group.register_commands(&mut prefix, &mut self.commands);
1334 let mut prefix = Vec::new();
1335 let clap_group = runtime_group_clap_command_with_schema_help(
1336 &group,
1337 &mut prefix,
1338 &self.middleware.schema_registry,
1339 );
1340 self.root = if replacing_builtin {
1341 self.root.clone().mut_subcommand("auth", |_| clap_group)
1342 } else {
1343 self.root.clone().subcommand(clap_group)
1344 };
1345 self.register_auth_help_entry();
1349 }
1350
1351 fn default_auth_provider(&self) -> String {
1352 if !self.middleware.default_auth_provider.is_empty() {
1353 return self.middleware.default_auth_provider.clone();
1354 }
1355 self.middleware
1356 .auth
1357 .registered_names()
1358 .into_iter()
1359 .next()
1360 .unwrap_or_default()
1361 }
1362
1363 fn initialized_middleware(&self) -> Result<Middleware> {
1364 let Some(init_deps) = &self.init_deps else {
1365 return Ok(self.middleware.clone());
1366 };
1367 let mut guard = self
1368 .init_state
1369 .lock()
1370 .map_err(|_| CliCoreError::message("init deps lock poisoned"))?;
1371 if let Some(result) = guard.as_ref() {
1372 return result.clone().map_err(InitFailure::into_error);
1373 }
1374 let mut middleware = self.middleware.clone();
1375 let result = init_deps(&mut middleware)
1376 .map(|()| middleware)
1377 .map_err(|err| InitFailure::capture(&err));
1378 *guard = Some(result.clone());
1379 result.map_err(InitFailure::into_error)
1380 }
1381
1382 fn apply_config_flags(&self, matches: &ArgMatches, middleware: &mut Middleware) -> Result<()> {
1383 if let Some(apply_flags) = &self.apply_flags {
1384 apply_flags(matches, middleware)?;
1385 }
1386 Ok(())
1387 }
1388
1389 fn run_pre_run(
1390 &self,
1391 middleware: &mut Middleware,
1392 command_path: &str,
1393 args: &crate::middleware::ValueMap,
1394 ) -> Result<()> {
1395 if let Some(pre_run) = &self.pre_run {
1396 pre_run(middleware, command_path, args)?;
1397 }
1398 Ok(())
1399 }
1400
1401 fn resolve_meta(&self, command_path: &str, meta: CommandMeta) -> CommandMeta {
1402 if let Some(resolver) = &self.meta_resolver {
1403 resolver(command_path, meta)
1404 } else {
1405 meta
1406 }
1407 }
1408
1409 fn finish_run(&self, output: CliRunOutput) -> CliRunOutput {
1410 if let Some(on_shutdown) = &self.on_shutdown {
1411 on_shutdown();
1412 }
1413 output
1414 }
1415}
1416
1417fn apply_global_flags(middleware: &mut Middleware, flags: &GlobalFlags, timeout: Option<Duration>) {
1418 middleware.output_format = flags.output_format.clone();
1419 middleware.verbose = flags.verbose.clone();
1420 middleware.dry_run = flags.dry_run;
1421 middleware.fields = flags.fields.clone();
1422 middleware.filter = flags.filter.clone();
1423 middleware.expr = flags.expr.clone();
1424 middleware.limit = flags.limit;
1425 middleware.offset = flags.offset;
1426 middleware.reason = flags.reason.clone();
1427 middleware.schema = flags.schema;
1428 middleware.timeout = timeout;
1429 middleware.debug = flags.debug.clone();
1430 middleware.search = flags.search.clone();
1431}
1432
1433async fn run_with_timeout<F, T>(
1434 timeout: Option<Duration>,
1435 timeout_label: &str,
1436 future: F,
1437) -> Result<T>
1438where
1439 F: Future<Output = Result<T>>,
1440{
1441 let Some(timeout) = timeout else {
1442 return future.await;
1443 };
1444 match tokio::time::timeout(timeout, future).await {
1445 Ok(result) => result,
1446 Err(_) => Err(CliCoreError::message(format!(
1447 "command timed out after {timeout_label}"
1448 ))),
1449 }
1450}
1451
1452async fn run_until_signal<Run, Shutdown>(run: Run, shutdown: Shutdown) -> CliRunOutput
1453where
1454 Run: Future<Output = CliRunOutput>,
1455 Shutdown: Future<Output = ()>,
1456{
1457 tokio::pin!(run);
1458 tokio::pin!(shutdown);
1459 tokio::select! {
1460 output = &mut run => output,
1461 () = &mut shutdown => CliRunOutput {
1462 exit_code: 130,
1463 rendered: "command interrupted\n".to_owned(),
1464 },
1465 }
1466}
1467
1468#[cfg(unix)]
1469async fn shutdown_signal() {
1470 let ctrl_c = tokio::signal::ctrl_c();
1471 match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
1472 Ok(mut sigterm) => {
1473 tokio::select! {
1474 _ = ctrl_c => {},
1475 _ = sigterm.recv() => {},
1476 }
1477 }
1478 Err(_) => {
1479 drop(ctrl_c.await);
1480 }
1481 }
1482}
1483
1484#[cfg(not(unix))]
1485async fn shutdown_signal() {
1486 drop(tokio::signal::ctrl_c().await);
1487}
1488
1489fn parse_command_timeout(raw: &str) -> Result<Option<Duration>> {
1490 let raw = raw.trim();
1491 if raw.is_empty() {
1492 return Ok(Some(Duration::from_secs(60)));
1493 }
1494 let Some(seconds) = parse_duration_seconds(raw) else {
1495 return Err(CliCoreError::message(format!(
1496 "invalid timeout {raw:?}: expected duration like 60s, 5m, or 0s"
1497 )));
1498 };
1499 if seconds <= 0.0 {
1500 Ok(None)
1501 } else {
1502 Ok(Some(Duration::from_secs_f64(seconds)))
1503 }
1504}
1505
1506fn parse_duration_seconds(raw: &str) -> Option<f64> {
1507 for (suffix, seconds) in [
1508 ("ns", 0.000_000_001_f64),
1509 ("us", 0.000_001_f64),
1510 ("µs", 0.000_001_f64),
1511 ("ms", 0.001_f64),
1512 ("s", 1.0_f64),
1513 ("m", 60.0_f64),
1514 ("h", 3600.0_f64),
1515 ] {
1516 if let Some(number) = raw.strip_suffix(suffix) {
1517 let value = number.parse::<f64>().ok()?;
1518 if !value.is_finite() {
1519 return None;
1520 }
1521 return Some(value * seconds);
1522 }
1523 }
1524 None
1525}
1526
1527fn render_cli_error(
1528 middleware: &Middleware,
1529 err: &(dyn std::error::Error + 'static),
1530 system: &str,
1531) -> CliRunOutput {
1532 let format = middleware
1533 .output_format
1534 .parse::<crate::output::OutputFormat>()
1535 .unwrap_or(crate::output::OutputFormat::Json);
1536 let envelope =
1537 crate::output::build_error_envelope(err, system).prepare_for_render(&middleware.verbose);
1538 match crate::output::render(format, &envelope) {
1539 Ok(rendered) => CliRunOutput {
1540 exit_code: exit_code_for_error(err),
1541 rendered,
1542 },
1543 Err(render_err) => CliRunOutput {
1544 exit_code: exit_code_for_error(err),
1545 rendered: render_err.to_string(),
1546 },
1547 }
1548}
1549
1550fn find_command_by_colon_path<'command>(
1551 root: &'command Command,
1552 path: &str,
1553) -> Option<&'command Command> {
1554 find_command_and_canonical_path_by_colon_path(root, path).map(|(command, _)| command)
1555}
1556
1557fn find_help_target<'command>(
1558 root: &'command Command,
1559 parts: &[&str],
1560) -> Option<&'command Command> {
1561 let mut current = root;
1562 let mut matched_any = false;
1563 for part in parts {
1564 let Some(next) = current.find_subcommand(part) else {
1565 break;
1566 };
1567 current = next;
1568 matched_any = true;
1569 }
1570 matched_any.then_some(current)
1571}
1572
1573fn find_command_and_canonical_path_by_colon_path<'command>(
1574 root: &'command Command,
1575 path: &str,
1576) -> Option<(&'command Command, Vec<String>)> {
1577 if path.is_empty() {
1578 return Some((root, Vec::new()));
1579 }
1580 let mut current = root;
1581 let mut canonical = Vec::new();
1582 for part in path.split(':') {
1583 current = current.find_subcommand(part)?;
1584 canonical.push(current.get_name().to_owned());
1585 }
1586 Some((current, canonical))
1587}
1588
1589fn canonical_path_from_parts(root: &Command, parts: &[String]) -> Option<String> {
1590 if parts.is_empty() {
1591 return Some(String::new());
1592 }
1593 let mut current = root;
1594 let mut canonical = Vec::new();
1595 for part in parts {
1596 current = current.find_subcommand(part)?;
1597 canonical.push(current.get_name().to_owned());
1598 }
1599 Some(canonical.join(":"))
1600}
1601
1602fn extract_search_scope_parts(args: &[String]) -> Vec<String> {
1603 let mut parts = Vec::new();
1604 let mut index = 1;
1605 while index < args.len() {
1606 let arg = &args[index];
1607 if arg == "--search" || arg.starts_with("--search=") {
1608 break;
1609 }
1610 if arg.starts_with('-') {
1611 if !arg.contains('=') && index + 1 < args.len() && !args[index + 1].starts_with('-') {
1612 index += 2;
1613 } else {
1614 index += 1;
1615 }
1616 continue;
1617 }
1618 parts.push(arg.clone());
1619 index += 1;
1620 }
1621 parts
1622}
1623
1624fn collect_command_search_documents(
1625 command: &Command,
1626 prefix: &mut Vec<String>,
1627 aliases: &mut Vec<String>,
1628 docs: &mut Vec<SearchDocument>,
1629) {
1630 if command.is_hide_set() || command.get_name() == "completion" {
1631 return;
1632 }
1633 if command.get_subcommands().next().is_some() {
1634 for child in command.get_subcommands() {
1635 prefix.push(child.get_name().to_owned());
1636 let alias_len = aliases.len();
1637 append_command_alias_terms(child, aliases);
1638 collect_command_search_documents(child, prefix, aliases, docs);
1639 aliases.truncate(alias_len);
1640 prefix.pop();
1641 }
1642 return;
1643 }
1644 if prefix.is_empty() {
1645 prefix.push(command.get_name().to_owned());
1646 append_command_alias_terms(command, aliases);
1647 }
1648 let path = prefix.join(" ");
1649 let alias_text = aliases.join(" ");
1650 docs.push(SearchDocument {
1651 id: format!("cmd:{path}"),
1652 kind: "command".to_owned(),
1653 title: path,
1654 summary: command
1655 .get_about()
1656 .map(ToString::to_string)
1657 .unwrap_or_default(),
1658 content: format!(
1659 "{} {} {} {}",
1660 command
1661 .get_about()
1662 .map(ToString::to_string)
1663 .unwrap_or_default(),
1664 command
1665 .get_long_about()
1666 .map(ToString::to_string)
1667 .unwrap_or_default(),
1668 command_flag_text(command),
1669 alias_text
1670 ),
1671 });
1672 if prefix.len() == 1 && prefix[0] == command.get_name() {
1673 prefix.pop();
1674 }
1675}
1676
1677fn append_command_alias_terms(command: &Command, aliases: &mut Vec<String>) {
1678 aliases.extend(command.get_all_aliases().map(str::to_owned));
1679 aliases.extend(
1680 command
1681 .get_all_short_flag_aliases()
1682 .map(|alias| alias.to_string()),
1683 );
1684 aliases.extend(command.get_all_long_flag_aliases().map(str::to_owned));
1685}
1686
1687fn command_flag_text(command: &Command) -> String {
1688 command
1689 .get_arguments()
1690 .filter_map(|arg| {
1691 let mut names = Vec::new();
1692 if let Some(short) = arg.get_short() {
1693 names.push(format!("-{short}"));
1694 }
1695 if let Some(long) = arg.get_long() {
1696 names.push(format!("--{long}"));
1697 }
1698 if let Some(short_aliases) = arg.get_all_short_aliases() {
1699 names.extend(
1700 short_aliases
1701 .into_iter()
1702 .map(|short_alias| format!("-{short_alias}")),
1703 );
1704 }
1705 if let Some(aliases) = arg.get_all_aliases() {
1706 names.extend(aliases.into_iter().map(|alias| format!("--{alias}")));
1707 }
1708 (!names.is_empty()).then(|| names.join(" "))
1709 })
1710 .collect::<Vec<_>>()
1711 .join(" ")
1712}
1713
1714fn has_subcommand(command: &Command, name: &str) -> bool {
1715 command
1716 .get_subcommands()
1717 .any(|child| child.get_name() == name)
1718}
1719
1720fn has_root_version_flag(args: &[String], root: &Command, root_name: &str) -> bool {
1721 let bool_flags = derive_bool_flags(root);
1722 let value_flags = derive_value_flags(root);
1723 let mut iter = args.iter().peekable();
1724 if iter
1725 .peek()
1726 .is_some_and(|arg| arg_matches_root_name(arg, root_name))
1727 {
1728 iter.next();
1729 }
1730
1731 while let Some(arg) = iter.next() {
1732 match arg.as_str() {
1733 "--version" | "-v" => return true,
1734 "--" => return false,
1735 value if value.contains('=') || bool_flags.contains(value) => continue,
1736 value
1737 if value_flags.contains(value)
1738 || unknown_flag_consumes_value(value, iter.peek()) =>
1739 {
1740 iter.next();
1741 }
1742 value if value.starts_with('-') => {}
1743 _ => return false,
1744 }
1745 }
1746 false
1747}
1748
1749fn normalize_optional_global_flags_before_command(root: &Command, args: &[String]) -> Vec<String> {
1750 let optional_string_defaults = BTreeMap::from([("--verbose", "all"), ("--debug", "*")]);
1751 let optional_bool_defaults = BTreeMap::from([("--dry-run", "true"), ("--schema", "true")]);
1752 let mut normalized = Vec::with_capacity(args.len());
1753 let mut index = 0;
1754 let mut current = root;
1755 while index < args.len() {
1756 let arg = &args[index];
1757 if index == 0 && arg_matches_root_name(arg, root.get_name()) {
1758 normalized.push(arg.clone());
1759 index += 1;
1760 continue;
1761 }
1762
1763 if let Some(default) = optional_bool_defaults.get(arg.as_str()) {
1764 normalized.push(format!("{arg}={default}"));
1765 index += 1;
1766 continue;
1767 }
1768
1769 if let Some(default) = optional_string_defaults.get(arg.as_str()) {
1770 match args.get(index + 1) {
1771 None => {
1772 normalized.push(format!("{arg}={default}"));
1773 index += 1;
1774 continue;
1775 }
1776 Some(next)
1777 if current.get_name() == root.get_name()
1778 || next.starts_with('-')
1779 || direct_subcommand(current, next).is_some() =>
1780 {
1781 normalized.push(format!("{arg}={default}"));
1782 index += 1;
1783 continue;
1784 }
1785 Some(next) => {
1786 normalized.push(arg.clone());
1787 normalized.push(next.clone());
1788 index += 2;
1789 continue;
1790 }
1791 }
1792 }
1793
1794 normalized.push(arg.clone());
1795 if !arg.starts_with('-')
1796 && let Some(next_command) = direct_subcommand(current, arg)
1797 {
1798 current = next_command;
1799 }
1800 index += 1;
1801 }
1802 normalized
1803}
1804
1805fn direct_subcommand<'command>(
1806 command: &'command Command,
1807 token: &str,
1808) -> Option<&'command Command> {
1809 command.get_subcommands().find(|child| {
1810 child.get_name() == token || child.get_all_aliases().any(|alias| alias == token)
1811 })
1812}
1813
1814fn unknown_group_command_message(root: &Command, positionals: &[String]) -> Option<String> {
1815 if positionals.is_empty() {
1816 return None;
1817 }
1818
1819 let mut current = root;
1820 let mut path = vec![root.get_name().to_owned()];
1821 for token in positionals {
1822 if let Some(next) = current.find_subcommand(token) {
1823 current = next;
1824 path.push(next.get_name().to_owned());
1825 continue;
1826 }
1827 if current.get_subcommands().next().is_some() {
1828 return Some(format!(
1829 "unknown command {token:?} for {:?}",
1830 path.join(" ")
1831 ));
1832 }
1833 return None;
1834 }
1835 None
1836}
1837
1838fn group_help_target_parts(
1861 root: &Command,
1862 positionals: &[String],
1863 command_keyword_count: usize,
1864) -> Option<Vec<String>> {
1865 let help_index = positionals.iter().position(|token| token == "help")?;
1866 if help_index == 0 {
1868 return None;
1869 }
1870 if help_index >= command_keyword_count {
1872 return None;
1873 }
1874 let prefix = &positionals[..help_index];
1875 let mut current = root;
1876 for token in prefix {
1877 current = current.find_subcommand(token)?;
1878 }
1879 current.get_subcommands().next()?;
1881 if current.find_subcommand("help").is_some() {
1883 return None;
1884 }
1885 let suffix = &positionals[help_index + 1..];
1887 Some(prefix.iter().chain(suffix).cloned().collect())
1888}
1889
1890fn rewrite_group_help_args(
1901 clap_args: &[String],
1902 root_name: &str,
1903 bool_flags: &BTreeSet<String>,
1904 value_flags: &BTreeSet<String>,
1905 parts: &[String],
1906) -> Vec<String> {
1907 let mut next_positional = std::iter::once("help".to_owned())
1909 .chain(parts.iter().cloned())
1910 .peekable();
1911 let mut out = Vec::with_capacity(clap_args.len());
1912 let mut iter = clap_args.iter().peekable();
1913 if iter
1914 .peek()
1915 .is_some_and(|arg| arg_matches_root_name(arg, root_name))
1916 && let Some(program) = iter.next()
1917 {
1918 out.push(program.clone());
1919 }
1920
1921 let mut take_positional =
1922 |fallback: &String| next_positional.next().unwrap_or(fallback.clone());
1923
1924 while let Some(arg) = iter.next() {
1925 if arg == "--" {
1926 out.push(arg.clone());
1927 for rest in iter.by_ref() {
1929 out.push(take_positional(rest));
1930 }
1931 break;
1932 }
1933 if arg.contains('=') || bool_flags.contains(arg) {
1934 out.push(arg.clone());
1935 continue;
1936 }
1937 if value_flags.contains(arg) || unknown_flag_consumes_value(arg, iter.peek()) {
1938 out.push(arg.clone());
1939 if let Some(value) = iter.next() {
1940 out.push(value.clone());
1941 }
1942 continue;
1943 }
1944 if arg.starts_with('-') {
1945 out.push(arg.clone());
1946 continue;
1947 }
1948 out.push(take_positional(arg));
1949 }
1950 out.extend(next_positional);
1952 out
1953}
1954
1955fn positional_command_tokens(
1956 args: &[String],
1957 root_name: &str,
1958 bool_flags: &BTreeSet<String>,
1959 value_flags: &BTreeSet<String>,
1960) -> Vec<String> {
1961 let mut tokens = Vec::new();
1962 let mut iter = args.iter().peekable();
1963 if iter
1964 .peek()
1965 .is_some_and(|arg| arg_matches_root_name(arg, root_name))
1966 {
1967 iter.next();
1968 }
1969
1970 while let Some(arg) = iter.next() {
1971 if arg == "--" {
1972 tokens.extend(iter.cloned());
1973 break;
1974 }
1975 if arg.contains('=') {
1976 continue;
1977 }
1978 if bool_flags.contains(arg) {
1979 continue;
1980 }
1981 if value_flags.contains(arg) || unknown_flag_consumes_value(arg, iter.peek()) {
1982 iter.next();
1983 continue;
1984 }
1985 if arg.starts_with('-') {
1986 continue;
1987 }
1988 tokens.push(arg.clone());
1989 }
1990 tokens
1991}
1992
1993fn unknown_flag_consumes_value(arg: &str, next: Option<&&String>) -> bool {
1994 arg.starts_with('-') && next.is_some_and(|value| !value.starts_with('-'))
1995}
1996
1997fn arg_matches_root_name(arg: &str, root_name: &str) -> bool {
1998 arg == root_name
1999 || std::path::Path::new(arg)
2000 .file_stem()
2001 .and_then(|n| n.to_str())
2002 .is_some_and(|n| n == root_name)
2003}
2004
2005fn register_runtime_group_schemas(
2006 group: &RuntimeGroupSpec,
2007 prefix: &mut Vec<String>,
2008 schemas: &mut SchemaRegistry,
2009) {
2010 prefix.push(group.group.name.clone());
2011 for child_group in &group.groups {
2012 register_runtime_group_schemas(child_group, prefix, schemas);
2013 }
2014 for child in &group.commands {
2015 prefix.push(child.spec.name.clone());
2016 register_command_schema(&child.spec, &prefix.join(":"), schemas);
2017 prefix.pop();
2018 }
2019 prefix.pop();
2020}
2021
2022fn register_command_schema(spec: &CommandSpec, command_path: &str, schemas: &mut SchemaRegistry) {
2023 if let Some(schema) = &spec.output_schema {
2024 schemas.register_info(command_path.to_owned(), schema.clone());
2025 }
2026}
2027
2028fn runtime_group_clap_command_with_schema_help(
2029 group: &RuntimeGroupSpec,
2030 prefix: &mut Vec<String>,
2031 schemas: &SchemaRegistry,
2032) -> Command {
2033 let mut command = group_clap_command_without_children(&group.group);
2034 prefix.push(group.group.name.clone());
2035 for child_group in &group.groups {
2036 command = command.subcommand(runtime_group_clap_command_with_schema_help(
2037 child_group,
2038 prefix,
2039 schemas,
2040 ));
2041 }
2042 for child in &group.commands {
2043 prefix.push(child.spec.name.clone());
2044 let command_path = prefix.join(":");
2045 command = command.subcommand(command_clap_command_with_schema_help(
2046 &child.spec,
2047 &command_path,
2048 schemas,
2049 ));
2050 prefix.pop();
2051 }
2052 prefix.pop();
2053 command
2054}
2055
2056fn group_clap_command_without_children(group: &GroupSpec) -> Command {
2057 let mut command = Command::new(group.name.clone())
2058 .about(group.short.clone())
2059 .help_template(GROUP_HELP_TEMPLATE);
2060 if let Some(long) = &group.long
2061 && !long.is_empty()
2062 {
2063 command = command.long_about(long.clone());
2064 }
2065 for alias in &group.aliases {
2066 command = command.alias(alias.clone());
2067 }
2068 if group.hidden {
2069 command = command.hide(true);
2070 }
2071 command
2072}
2073
2074fn command_clap_command_with_schema_help(
2075 spec: &CommandSpec,
2076 command_path: &str,
2077 schemas: &SchemaRegistry,
2078) -> Command {
2079 let mut command = spec.clap_command();
2080 let Some(schema) = schemas.get_by_path(command_path) else {
2081 return command;
2082 };
2083 let schema_help = format_help_section(&schema.fields);
2084 if schema_help.is_empty() {
2085 return command;
2086 }
2087 let base = spec
2088 .long
2089 .as_ref()
2090 .filter(|long| !long.is_empty())
2091 .cloned()
2092 .unwrap_or_else(|| spec.short.clone());
2093 let long = if base.is_empty() {
2094 schema_help
2095 } else {
2096 format!("{base}\n\n{schema_help}")
2097 };
2098 command = command.long_about(long);
2099 command
2100}
2101
2102fn process_exit_code(code: i32) -> ExitCode {
2103 if code == 0 {
2104 return ExitCode::SUCCESS;
2105 }
2106 match u8::try_from(code) {
2107 Ok(code) if code != 0 => ExitCode::from(code),
2108 Ok(_) | Err(_) => ExitCode::from(1),
2109 }
2110}
2111
2112async fn run_streaming_command(
2113 middleware: &Middleware,
2114 request: MiddlewareRequest<'_>,
2115 raw_matches: Arc<ArgMatches>,
2116 streaming_handler: crate::command::StreamingCommandHandler,
2117) -> Result<CliRunOutput> {
2118 use tokio::{io::AsyncWriteExt, sync::mpsc};
2119
2120 let args_for_handler = request.args.clone();
2121 let user_args_for_handler = request.user_args.clone();
2122 let handler_path = request.command_path.to_owned();
2123 let middleware_for_handler = middleware.clone();
2124 let raw_matches_for_handler = raw_matches;
2125
2126 let (tx, mut rx) = mpsc::channel::<serde_json::Value>(64);
2127 let sender = StreamSender(tx);
2128
2129 let writer = tokio::spawn(async move {
2133 let mut stdout = tokio::io::stdout();
2134 while let Some(event) = rx.recv().await {
2135 let Ok(line) = serde_json::to_string(&event) else {
2136 continue;
2137 };
2138 if stdout.write_all(line.as_bytes()).await.is_err()
2139 || stdout.write_all(b"\n").await.is_err()
2140 || stdout.flush().await.is_err()
2141 {
2142 break;
2143 }
2144 }
2145 });
2146
2147 let output = middleware
2148 .run(request, async move |credential| {
2149 streaming_handler(
2150 CommandContext {
2151 credential,
2152 args: args_for_handler,
2153 user_args: user_args_for_handler,
2154 command_path: handler_path,
2155 middleware: middleware_for_handler,
2156 raw_matches: raw_matches_for_handler,
2157 },
2158 sender,
2159 )
2160 .await?;
2161 Ok(crate::CommandResult::new(serde_json::Value::Null))
2162 })
2163 .await;
2164
2165 let _write_result = writer.await;
2168
2169 match output {
2170 Ok(out) if out.exit_code == 0 => Ok(CliRunOutput {
2171 exit_code: 0,
2172 rendered: String::new(),
2173 }),
2174 Ok(out) => Ok(out.into()),
2175 Err(err) => Ok(CliRunOutput {
2176 exit_code: exit_code_for_error(&err),
2177 rendered: render_cli_error(middleware, &err, middleware.app_id.as_str()).rendered,
2178 }),
2179 }
2180}