Skip to main content

cli_engine/
cli.rs

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