Skip to main content

cli_engine/
command.rs

1use std::{collections::BTreeMap, future::Future, pin::Pin, sync::Arc};
2
3use clap::{Arg, ArgAction, ArgMatches, Command};
4use schemars::JsonSchema;
5use serde_json::{Number, Value};
6use tokio::sync::mpsc;
7
8use crate::{
9    CommandMeta, Credential, Middleware, OutputSchema, Result, SchemaInfo, Tier,
10    middleware::ValueMap, output::NextAction,
11};
12
13/// Sender half for streaming command output.
14///
15/// Streaming handlers call [`StreamSender::send`] for each progress event.
16/// The engine drains the channel and writes each event as an NDJSON line.
17#[derive(Clone, Debug)]
18pub struct StreamSender(pub(crate) mpsc::Sender<Value>);
19
20impl StreamSender {
21    /// Sends one event. Silently drops the event if the receiver is gone.
22    pub async fn send(&self, event: Value) {
23        drop(self.0.send(event).await);
24    }
25}
26
27/// Boxed future returned by runtime command handlers.
28pub type CommandFuture = Pin<Box<dyn Future<Output = Result<CommandResult>> + Send>>;
29/// Shared command handler used by [`RuntimeCommandSpec`].
30pub type CommandHandler = Arc<dyn Fn(CommandContext) -> CommandFuture + Send + Sync>;
31
32/// Boxed future returned by streaming command handlers.
33pub type StreamingCommandFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
34/// Shared streaming handler: receives context and an event sender; returns when the stream ends.
35pub type StreamingCommandHandler =
36    Arc<dyn Fn(CommandContext, StreamSender) -> StreamingCommandFuture + Send + Sync>;
37
38/// Data returned by a command handler.
39///
40/// Command handlers should return renderable data and keep output metadata on
41/// [`CommandSpec`]. The metadata field is reserved for future command-result
42/// extensions that are not known when the command is registered.
43#[derive(Clone, Debug, PartialEq)]
44pub struct CommandResult {
45    /// JSON data rendered by the configured output formatter.
46    pub data: Value,
47    /// Optional command-result extension metadata.
48    pub metadata: CommandResultMetadata,
49}
50
51impl CommandResult {
52    /// Creates a command result from renderable JSON data.
53    #[must_use]
54    pub fn new(data: Value) -> Self {
55        Self {
56            data,
57            metadata: CommandResultMetadata::default(),
58        }
59    }
60
61    /// Attaches suggested follow-up actions to this result.
62    #[must_use]
63    pub fn with_next_actions(mut self, actions: Vec<NextAction>) -> Self {
64        self.metadata.next_actions = actions;
65        self
66    }
67}
68
69impl From<Value> for CommandResult {
70    fn from(data: Value) -> Self {
71        Self::new(data)
72    }
73}
74
75/// Optional metadata a command can attach to its result.
76#[non_exhaustive]
77#[derive(Clone, Debug, Default, Eq, PartialEq)]
78pub struct CommandResultMetadata {
79    /// Suggested follow-up actions for the caller.
80    pub next_actions: Vec<NextAction>,
81}
82
83/// Runtime context passed to advanced command handlers.
84///
85/// Most commands can use [`RuntimeCommandSpec::new`] and receive just the
86/// credential and effective args. Use this context when a command needs the
87/// colon path, user-supplied args, or a snapshot of middleware state.
88///
89/// This struct is constructed by the framework during command dispatch.
90/// Consumer code receives it in handler closures and should not construct it
91/// directly.
92#[derive(Clone, Debug)]
93#[non_exhaustive]
94pub struct CommandContext {
95    /// Credential resolved by middleware. No-auth commands receive `None`.
96    pub credential: Option<Credential>,
97    /// Effective arguments, including defaults and framework-injected values.
98    pub args: ValueMap,
99    /// Arguments explicitly supplied by the user.
100    pub user_args: ValueMap,
101    /// Colon-separated command path such as `project:list`.
102    pub command_path: String,
103    /// Middleware snapshot for this invocation.
104    pub middleware: Middleware,
105    /// Raw `clap` matches for typed argument deserialization via derive.
106    pub raw_matches: Arc<ArgMatches>,
107}
108
109impl CommandContext {
110    /// Deserializes the raw argument matches into a typed args struct.
111    ///
112    /// Use this with `#[derive(clap::Args)]` structs to get type-safe access
113    /// to command arguments instead of working with the `ValueMap` directly.
114    ///
115    /// # Errors
116    ///
117    /// Returns an error if the matches cannot be deserialized into `T`.
118    pub fn typed_args<T: clap::FromArgMatches>(&self) -> Result<T> {
119        T::from_arg_matches(self.raw_matches.as_ref())
120            .map_err(|e| crate::CliCoreError::Message(format!("argument parse error: {e}")))
121    }
122}
123
124/// Declarative leaf command metadata and parser arguments.
125///
126/// `CommandSpec` intentionally keeps command metadata next to the command's
127/// handler. This is the primary copy/paste surface for teams adding commands.
128#[derive(Clone, Debug, Default)]
129pub struct CommandSpec {
130    /// Leaf command name.
131    pub name: String,
132    /// One-line command description.
133    pub short: String,
134    /// Optional long help text.
135    pub long: Option<String>,
136    /// Alternate command names accepted by the parser.
137    pub aliases: Vec<String>,
138    /// Whether the command runs but is hidden from help, tree, and search.
139    pub hidden: bool,
140    /// Backend/system id used in output metadata and generic error envelopes.
141    pub system: Option<String>,
142    /// Default comma-separated field projection.
143    pub default_fields: Option<String>,
144    /// Whether the command bypasses credential resolution.
145    pub no_auth: bool,
146    /// Auth provider name for this command.
147    pub auth_provider: Option<String>,
148    /// Risk tier used by authentication, authorization, and dry-run.
149    pub tier: Option<Tier>,
150    /// Explicit dry-run prompt marker for commands without a tier.
151    pub mutates: bool,
152    /// Provider-specific auth metadata.
153    pub auth_metadata: BTreeMap<String, String>,
154    /// Command-specific `clap` arguments.
155    pub args: Vec<Arg>,
156    /// Optional output schema published through `--schema` and help.
157    pub output_schema: Option<SchemaInfo>,
158}
159
160impl CommandSpec {
161    /// Creates a command spec with the required name and one-line help.
162    #[must_use]
163    pub fn new(name: impl Into<String>, short: impl Into<String>) -> Self {
164        Self {
165            name: name.into(),
166            short: short.into(),
167            ..Self::default()
168        }
169    }
170
171    /// Creates a command spec from a `#[derive(clap::Args)]` struct.
172    ///
173    /// Extracts the argument definitions from the derive type and populates the
174    /// spec's args list. The command name and help text are still required since
175    /// `Args` types do not carry those.
176    #[must_use]
177    pub fn from_args<T: clap::Args>(name: impl Into<String>, short: impl Into<String>) -> Self {
178        let placeholder = Command::new("__placeholder");
179        let augmented = T::augment_args(placeholder);
180        let args: Vec<Arg> = augmented
181            .get_arguments()
182            .filter(|a| !matches!(a.get_id().as_str(), "help" | "version"))
183            .cloned()
184            .collect();
185        Self {
186            name: name.into(),
187            short: short.into(),
188            args,
189            ..Self::default()
190        }
191    }
192
193    /// Sets expanded command help.
194    #[must_use]
195    pub fn with_long(mut self, long: impl Into<String>) -> Self {
196        self.long = Some(long.into());
197        self
198    }
199
200    /// Adds one command alias.
201    #[must_use]
202    pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
203        self.aliases.push(alias.into());
204        self
205    }
206
207    /// Hides or shows this command in discovery output.
208    #[must_use]
209    pub fn hidden(mut self, hidden: bool) -> Self {
210        self.hidden = hidden;
211        self
212    }
213
214    /// Sets the backend/system id for output metadata and error attribution.
215    #[must_use]
216    pub fn with_system(mut self, system: impl Into<String>) -> Self {
217        self.system = Some(system.into());
218        self
219    }
220
221    /// Sets the default field projection used when `--fields` is absent.
222    #[must_use]
223    pub fn with_default_fields(mut self, default_fields: impl Into<String>) -> Self {
224        self.default_fields = Some(default_fields.into());
225        self
226    }
227
228    /// Selects the auth provider for this command.
229    #[must_use]
230    pub fn with_auth_provider(mut self, provider: impl Into<String>) -> Self {
231        self.auth_provider = Some(provider.into());
232        self
233    }
234
235    /// Marks the command as no-auth.
236    #[must_use]
237    pub fn no_auth(mut self, no_auth: bool) -> Self {
238        self.no_auth = no_auth;
239        self
240    }
241
242    /// Sets the command risk tier.
243    #[must_use]
244    pub fn with_tier(mut self, tier: Tier) -> Self {
245        self.tier = Some(tier);
246        self
247    }
248
249    /// Adds provider-specific auth metadata.
250    #[must_use]
251    pub fn with_auth_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
252        self.auth_metadata.insert(key.into(), value.into());
253        self
254    }
255
256    /// Adds a `clap` argument or option to this command.
257    #[must_use]
258    pub fn with_arg(mut self, arg: Arg) -> Self {
259        self.args.push(arg);
260        self
261    }
262
263    /// Adds a `clap` flag or option to this command.
264    #[must_use]
265    pub fn with_flag(self, flag: Arg) -> Self {
266        self.with_arg(flag)
267    }
268
269    /// Registers a compact framework schema from an [`OutputSchema`] type.
270    #[must_use]
271    pub fn with_output_schema<T: OutputSchema>(mut self) -> Self {
272        self.output_schema = Some(SchemaInfo {
273            command: String::new(),
274            fields: crate::output::fields_for::<T>(),
275            schema: None,
276        });
277        self
278    }
279
280    /// Registers JSON Schema generated from a Rust type with `schemars`.
281    #[must_use]
282    pub fn with_json_schema<T: JsonSchema>(mut self) -> Self {
283        self.output_schema = Some(crate::output::json_schema_info::<T>(""));
284        self
285    }
286
287    /// Marks whether the command should short-circuit under `--dry-run`.
288    #[must_use]
289    pub fn mutates(mut self, mutates: bool) -> Self {
290        self.mutates = mutates;
291        self
292    }
293
294    /// Builds middleware metadata from the spec.
295    #[must_use]
296    pub fn metadata(&self) -> CommandMeta {
297        let mut auth_metadata = self.auth_metadata.clone();
298        if let Some(provider) = &self.auth_provider
299            && !provider.is_empty()
300        {
301            auth_metadata.insert("provider".to_owned(), provider.clone());
302        }
303        if let Some(tier) = self.tier
304            && !auth_metadata.contains_key("tier")
305        {
306            auth_metadata.insert("tier".to_owned(), tier.to_string());
307        }
308        let scopes = auth_metadata
309            .get("scopes")
310            .map(|scopes| {
311                scopes
312                    .split_whitespace()
313                    .map(str::to_owned)
314                    .collect::<Vec<_>>()
315            })
316            .unwrap_or_default();
317
318        CommandMeta {
319            dry_run_prompt: self.mutates || self.tier.is_some_and(Tier::is_mutating),
320            auth_metadata,
321            scopes,
322        }
323    }
324
325    /// Builds the `clap` command for parser registration.
326    #[must_use]
327    pub fn clap_command(&self) -> Command {
328        let mut command = Command::new(self.name.clone()).about(self.short.clone());
329        if let Some(long) = &self.long
330            && !long.is_empty()
331        {
332            command = command.long_about(long.clone());
333        }
334        for alias in &self.aliases {
335            command = command.alias(alias.clone());
336        }
337        if self.hidden {
338            command = command.hide(true);
339        }
340        for arg in &self.args {
341            command = command.arg(arg.clone());
342        }
343        command
344    }
345}
346
347/// Declarative command group metadata.
348///
349/// Groups are noun-based containers. They do not run business logic directly;
350/// when invoked bare, the CLI renders group help.
351#[derive(Clone, Debug, Default)]
352pub struct GroupSpec {
353    /// Group command name.
354    pub name: String,
355    /// One-line group description.
356    pub short: String,
357    /// Optional long help text.
358    pub long: Option<String>,
359    /// Alternate group names accepted by the parser.
360    pub aliases: Vec<String>,
361    /// Whether the group runs but is hidden from discovery output.
362    pub hidden: bool,
363    /// Declarative child commands used for static tree construction.
364    pub commands: Vec<CommandSpec>,
365    /// Declarative nested groups used for static tree construction.
366    pub groups: Vec<GroupSpec>,
367}
368
369impl GroupSpec {
370    /// Creates a command group with the required name and one-line help.
371    #[must_use]
372    pub fn new(name: impl Into<String>, short: impl Into<String>) -> Self {
373        Self {
374            name: name.into(),
375            short: short.into(),
376            ..Self::default()
377        }
378    }
379
380    /// Sets expanded group help.
381    #[must_use]
382    pub fn with_long(mut self, long: impl Into<String>) -> Self {
383        self.long = Some(long.into());
384        self
385    }
386
387    /// Adds one group alias.
388    #[must_use]
389    pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
390        self.aliases.push(alias.into());
391        self
392    }
393
394    /// Hides or shows this group in discovery output.
395    #[must_use]
396    pub fn hidden(mut self, hidden: bool) -> Self {
397        self.hidden = hidden;
398        self
399    }
400
401    /// Adds one declarative child command.
402    #[must_use]
403    pub fn with_command(mut self, command: CommandSpec) -> Self {
404        self.commands.push(command);
405        self
406    }
407
408    /// Adds one declarative nested group.
409    #[must_use]
410    pub fn with_group(mut self, group: GroupSpec) -> Self {
411        self.groups.push(group);
412        self
413    }
414
415    /// Builds the `clap` command for parser registration.
416    #[must_use]
417    pub fn clap_command(&self) -> Command {
418        let mut command = Command::new(self.name.clone()).about(self.short.clone());
419        if let Some(long) = &self.long
420            && !long.is_empty()
421        {
422            command = command.long_about(long.clone());
423        }
424        for alias in &self.aliases {
425            command = command.alias(alias.clone());
426        }
427        if self.hidden {
428            command = command.hide(true);
429        }
430        for group in &self.groups {
431            command = command.subcommand(group.clap_command());
432        }
433        for child in &self.commands {
434            command = command.subcommand(child.clap_command());
435        }
436        command
437    }
438}
439
440/// Executable leaf command.
441///
442/// `RuntimeCommandSpec` pairs a [`CommandSpec`] with async business logic.
443/// This split keeps metadata inspectable for help/search/schema generation
444/// before the handler ever runs.
445///
446/// Use [`RuntimeCommandSpec::new_streaming`] for commands that emit incremental
447/// NDJSON progress events (e.g. long-running deployments with `--follow`).
448#[derive(Clone)]
449pub struct RuntimeCommandSpec {
450    /// Declarative command metadata.
451    pub spec: CommandSpec,
452    /// Async command implementation.
453    pub handler: CommandHandler,
454    /// Optional streaming handler. When set, the engine writes NDJSON events
455    /// to stdout as they arrive instead of collecting a single envelope.
456    pub streaming_handler: Option<StreamingCommandHandler>,
457}
458
459impl std::fmt::Debug for RuntimeCommandSpec {
460    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
461        formatter
462            .debug_struct("RuntimeCommandSpec")
463            .field("spec", &self.spec)
464            .field("is_streaming", &self.streaming_handler.is_some())
465            .finish_non_exhaustive()
466    }
467}
468
469impl RuntimeCommandSpec {
470    /// Creates a runtime command with the common handler shape.
471    ///
472    /// The handler receives the optional credential and effective args. It
473    /// returns [`CommandResult`], where `data` must be JSON-serializable.
474    #[must_use]
475    pub fn new<F, Fut, Output>(spec: CommandSpec, handler: F) -> Self
476    where
477        F: Fn(Option<Credential>, ValueMap) -> Fut + Send + Sync + 'static,
478        Fut: Future<Output = Result<Output>> + Send + 'static,
479        Output: Into<CommandResult> + Send + 'static,
480    {
481        Self {
482            spec,
483            streaming_handler: None,
484            handler: Arc::new(move |context| {
485                let future = handler(context.credential, context.args);
486                Box::pin(async move { future.await.map(Into::into) })
487            }),
488        }
489    }
490
491    /// Creates a runtime command with the full invocation context.
492    #[must_use]
493    pub fn new_with_context<F, Fut, Output>(spec: CommandSpec, handler: F) -> Self
494    where
495        F: Fn(CommandContext) -> Fut + Send + Sync + 'static,
496        Fut: Future<Output = Result<Output>> + Send + 'static,
497        Output: Into<CommandResult> + Send + 'static,
498    {
499        Self {
500            spec,
501            streaming_handler: None,
502            handler: Arc::new(move |context| {
503                let future = handler(context);
504                Box::pin(async move { future.await.map(Into::into) })
505            }),
506        }
507    }
508
509    /// Creates a streaming command that emits NDJSON events to stdout.
510    ///
511    /// The handler receives context and a [`StreamSender`]. It should call
512    /// `sender.send(event).await` for each progress event, then return `Ok(())`.
513    /// The engine writes each event as a JSON line; stdout is flushed after each.
514    #[must_use]
515    pub fn new_streaming<F, Fut>(spec: CommandSpec, handler: F) -> Self
516    where
517        F: Fn(CommandContext, StreamSender) -> Fut + Send + Sync + 'static,
518        Fut: Future<Output = Result<()>> + Send + 'static,
519    {
520        let streaming: StreamingCommandHandler = Arc::new(move |context, sender| {
521            let future = handler(context, sender);
522            Box::pin(future)
523        });
524        Self {
525            spec,
526            streaming_handler: Some(streaming),
527            handler: Arc::new(|_context| Box::pin(async { Ok(CommandResult::new(Value::Null)) })),
528        }
529    }
530
531    /// Creates a runtime command with typed argument deserialization.
532    ///
533    /// The handler receives the optional credential and the deserialized args
534    /// struct. Use with `CommandSpec::from_args::<T>()` to get end-to-end type
535    /// safety from argument definition through handler consumption.
536    ///
537    /// If the handler also needs the command path, middleware, or user-supplied
538    /// args, use [`RuntimeCommandSpec::new_with_context`] with
539    /// [`CommandContext::typed_args`] instead.
540    #[must_use]
541    pub fn new_typed<T, F, Fut, Output>(spec: CommandSpec, handler: F) -> Self
542    where
543        T: clap::FromArgMatches + Send + 'static,
544        F: Fn(Option<Credential>, T) -> Fut + Send + Sync + 'static,
545        Fut: Future<Output = Result<Output>> + Send + 'static,
546        Output: Into<CommandResult> + Send + 'static,
547    {
548        let handler = Arc::new(handler);
549        Self {
550            spec,
551            handler: Arc::new(move |context| {
552                let credential = context.credential.clone();
553                let parsed = T::from_arg_matches(context.raw_matches.as_ref());
554                let handler = handler.clone();
555                Box::pin(async move {
556                    let args = parsed.map_err(|e| {
557                        crate::CliCoreError::Message(format!("argument parse error: {e}"))
558                    })?;
559                    handler(credential, args).await.map(Into::into)
560                })
561            }),
562            streaming_handler: None,
563        }
564    }
565}
566
567/// Executable command group with runtime children.
568#[derive(Clone, Debug, Default)]
569pub struct RuntimeGroupSpec {
570    /// Declarative group metadata.
571    pub group: GroupSpec,
572    /// Executable leaf commands under this group.
573    pub commands: Vec<RuntimeCommandSpec>,
574    /// Executable nested groups under this group.
575    pub groups: Vec<RuntimeGroupSpec>,
576}
577
578impl RuntimeGroupSpec {
579    /// Creates a runtime group from declarative group metadata.
580    #[must_use]
581    pub fn new(group: GroupSpec) -> Self {
582        Self {
583            group,
584            ..Self::default()
585        }
586    }
587
588    /// Adds one executable leaf command.
589    #[must_use]
590    pub fn with_command(mut self, command: RuntimeCommandSpec) -> Self {
591        self.commands.push(command);
592        self
593    }
594
595    /// Adds one executable nested group.
596    #[must_use]
597    pub fn with_group(mut self, group: RuntimeGroupSpec) -> Self {
598        self.groups.push(group);
599        self
600    }
601
602    /// Builds the `clap` command for parser registration.
603    #[must_use]
604    pub fn clap_command(&self) -> Command {
605        let mut command = Command::new(self.group.name.clone()).about(self.group.short.clone());
606        if let Some(long) = &self.group.long
607            && !long.is_empty()
608        {
609            command = command.long_about(long.clone());
610        }
611        for alias in &self.group.aliases {
612            command = command.alias(alias.clone());
613        }
614        if self.group.hidden {
615            command = command.hide(true);
616        }
617        for group in &self.groups {
618            command = command.subcommand(group.clap_command());
619        }
620        for child in &self.commands {
621            command = command.subcommand(child.spec.clap_command());
622        }
623        command
624    }
625
626    pub(crate) fn register_commands(
627        &self,
628        prefix: &mut Vec<String>,
629        out: &mut BTreeMap<String, RuntimeCommandSpec>,
630    ) {
631        prefix.push(self.group.name.clone());
632        for group in &self.groups {
633            group.register_commands(prefix, out);
634        }
635        for command in &self.commands {
636            prefix.push(command.spec.name.clone());
637            out.insert(prefix.join(":"), command.clone());
638            prefix.pop();
639        }
640        prefix.pop();
641    }
642}
643
644/// Extracts the colon-separated command path from parsed `clap` matches.
645#[must_use]
646pub fn command_path_from_matches(root_name: &str, matches: &ArgMatches) -> String {
647    let mut parts = Vec::new();
648    let mut current = matches;
649    while let Some((name, submatches)) = current.subcommand() {
650        if name != root_name {
651            parts.push(name.to_owned());
652        }
653        current = submatches;
654    }
655    parts.join(":")
656}
657
658/// Builds a colon-separated command path from path parts.
659///
660/// The optional annotation is used only for isolated single-command tests.
661#[must_use]
662pub fn command_path_from_parts(parts: &[impl AsRef<str>], path_annotation: Option<&str>) -> String {
663    if parts.is_empty() {
664        return String::new();
665    }
666    if parts.len() > 1 {
667        return parts[1..]
668            .iter()
669            .map(AsRef::as_ref)
670            .collect::<Vec<_>>()
671            .join(":");
672    }
673    path_annotation
674        .filter(|annotation| !annotation.is_empty())
675        .map_or_else(|| parts[0].as_ref().to_owned(), ToOwned::to_owned)
676}
677
678/// Returns the deepest subcommand matches.
679#[must_use]
680pub fn leaf_matches(matches: &ArgMatches) -> &ArgMatches {
681    let mut current = matches;
682    while let Some((_, submatches)) = current.subcommand() {
683        current = submatches;
684    }
685    current
686}
687
688/// Converts parsed command arguments into the JSON-ish map consumed by middleware.
689///
690/// When `changed_only` is true, only arguments that came from the command line
691/// are included. This is the user-args map used by authz and audit.
692#[must_use]
693pub fn command_args_from_matches(
694    matches: &ArgMatches,
695    spec: &CommandSpec,
696    changed_only: bool,
697) -> ValueMap {
698    let mut args = ValueMap::new();
699    for arg in &spec.args {
700        let id = arg.get_id().to_string();
701        let changed = matches
702            .value_source(&id)
703            .is_some_and(|source| source == clap::parser::ValueSource::CommandLine);
704        if changed_only && !changed {
705            continue;
706        }
707        if let Some(value) = arg_value_from_matches(matches, arg, &id) {
708            args.insert(id, value);
709        }
710    }
711    args
712}
713
714fn arg_value_from_matches(matches: &ArgMatches, flag: &Arg, id: &str) -> Option<Value> {
715    matches.value_source(id)?;
716
717    if matches!(flag.get_action(), ArgAction::SetTrue | ArgAction::SetFalse)
718        && let Some(value) = matches.get_one::<bool>(id)
719    {
720        return Some(Value::Bool(*value));
721    }
722
723    if let Some(value) = typed_arg_value_from_matches(matches, id) {
724        return Some(value);
725    }
726
727    if let Some(values) = matches.get_raw(id) {
728        let rendered = values
729            .map(|value| value.to_string_lossy().into_owned())
730            .collect::<Vec<_>>();
731        return match rendered.as_slice() {
732            [] => None,
733            [single] => Some(Value::String(single.clone())),
734            _ => Some(Value::Array(
735                rendered.into_iter().map(Value::String).collect(),
736            )),
737        };
738    }
739
740    if let Some(value) = matches.get_one::<String>(id) {
741        return Some(Value::String(value.clone()));
742    }
743    if let Some(value) = matches.get_one::<usize>(id) {
744        return Some(serde_json::json!(value));
745    }
746    if let Some(value) = matches.get_one::<u64>(id) {
747        return Some(serde_json::json!(value));
748    }
749    if let Some(value) = matches.get_one::<i64>(id) {
750        return Some(serde_json::json!(value));
751    }
752    None
753}
754
755fn typed_arg_value_from_matches(matches: &ArgMatches, id: &str) -> Option<Value> {
756    typed_values::<bool>(matches, id, Value::Bool)
757        .or_else(|| typed_values::<i8>(matches, id, |value| Value::Number(value.into())))
758        .or_else(|| typed_values::<i16>(matches, id, |value| Value::Number(value.into())))
759        .or_else(|| typed_values::<i64>(matches, id, |value| Value::Number(value.into())))
760        .or_else(|| typed_values::<i32>(matches, id, |value| Value::Number(value.into())))
761        .or_else(|| typed_values::<u8>(matches, id, |value| Value::Number(value.into())))
762        .or_else(|| typed_values::<u16>(matches, id, |value| Value::Number(value.into())))
763        .or_else(|| typed_values::<u64>(matches, id, |value| Value::Number(value.into())))
764        .or_else(|| typed_values::<u32>(matches, id, |value| Value::Number(value.into())))
765        .or_else(|| {
766            typed_values::<usize>(matches, id, |value| {
767                u64::try_from(value).map_or(Value::Null, |value| Value::Number(value.into()))
768            })
769        })
770        .or_else(|| {
771            typed_values::<f64>(matches, id, |value| {
772                Number::from_f64(value).map_or(Value::Null, Value::Number)
773            })
774        })
775        .or_else(|| {
776            typed_values::<f32>(matches, id, |value| {
777                Number::from_f64(f64::from(value)).map_or(Value::Null, Value::Number)
778            })
779        })
780        .or_else(|| typed_values::<String>(matches, id, Value::String))
781}
782
783fn typed_values<T>(matches: &ArgMatches, id: &str, to_value: impl Fn(T) -> Value) -> Option<Value>
784where
785    T: Clone + Send + Sync + 'static,
786{
787    let Ok(Some(values)) = matches.try_get_many::<T>(id) else {
788        return None;
789    };
790    let values = values.cloned().map(to_value).collect::<Vec<_>>();
791    match values.as_slice() {
792        [] => None,
793        [single] => Some(single.clone()),
794        _ => Some(Value::Array(values)),
795    }
796}