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