Skip to main content

argot_cmd/model/
command.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use serde::{Deserialize, Serialize};
5
6use super::{Argument, BuildError, Example, Flag};
7
8/// A handler function that can be registered on a [`Command`].
9///
10/// The function is stored in an [`Arc`] so that [`Command::clone`] only
11/// increments a reference count rather than copying the closure. The
12/// higher-ranked trait bound (`for<'a>`) allows the handler to be called with
13/// a [`ParsedCommand`] of any lifetime, which is required because the parsed
14/// command borrows from the command tree at call time.
15///
16/// # Examples
17///
18/// ```
19/// # use std::sync::Arc;
20/// # use argot_cmd::HandlerFn;
21/// let handler: HandlerFn = Arc::new(|parsed| {
22///     println!("running command: {}", parsed.command.canonical);
23///     Ok(())
24/// });
25/// ```
26pub type HandlerFn =
27    Arc<dyn for<'a> Fn(&ParsedCommand<'a>) -> Result<(), Box<dyn std::error::Error>> + Send + Sync>;
28
29/// An async handler function that can be registered on a [`Command`].
30///
31/// The function receives a [`ParsedCommand`] reference and returns a
32/// pinned, boxed future. Use [`Command::builder`] → [`CommandBuilder::async_handler`]
33/// to register one.
34///
35/// # Feature
36///
37/// Requires the `async` feature flag.
38///
39/// # Examples
40///
41/// ```
42/// # #[cfg(feature = "async")] {
43/// use std::sync::Arc;
44/// use argot_cmd::AsyncHandlerFn;
45///
46/// let handler: AsyncHandlerFn = Arc::new(|parsed| Box::pin(async move {
47///     println!("async command: {}", parsed.command.canonical);
48///     Ok(())
49/// }));
50/// # }
51/// ```
52#[cfg(feature = "async")]
53pub type AsyncHandlerFn = std::sync::Arc<
54    dyn for<'a> Fn(
55            &'a ParsedCommand<'a>,
56        ) -> std::pin::Pin<
57            Box<
58                dyn std::future::Future<
59                        Output = Result<(), Box<dyn std::error::Error + Send + Sync>>,
60                    > + Send
61                    + 'a,
62            >,
63        > + Send
64        + Sync,
65>;
66
67/// The result of successfully parsing an invocation against a [`Command`].
68///
69/// `ParsedCommand` borrows the matched [`Command`] from the registry (lifetime
70/// `'a`) and owns the resolved argument and flag value maps. Keys in both maps
71/// are the canonical names of the argument/flag definitions.
72///
73/// # Examples
74///
75/// ```
76/// # use argot_cmd::{Command, Argument, Parser};
77/// let cmd = Command::builder("get")
78///     .argument(
79///         Argument::builder("id")
80///             .required()
81///             .build()
82///             .unwrap(),
83///     )
84///     .build()
85///     .unwrap();
86///
87/// let cmds = vec![cmd];
88/// let parser = Parser::new(&cmds);
89/// let parsed = parser.parse(&["get", "42"]).unwrap();
90///
91/// assert_eq!(parsed.command.canonical, "get");
92/// assert_eq!(parsed.args["id"], "42");
93/// ```
94#[derive(Debug)]
95pub struct ParsedCommand<'a> {
96    /// The matched [`Command`] definition, borrowed from the registry.
97    pub command: &'a Command,
98    /// Resolved positional argument values, keyed by argument name.
99    pub args: HashMap<String, String>,
100    /// Resolved flag values, keyed by flag name.
101    ///
102    /// Boolean flags (those without `takes_value`) are stored as `"true"`.
103    /// Flags that were not provided but have a `default` value are included
104    /// with that default.
105    pub flags: HashMap<String, String>,
106}
107
108impl<'a> ParsedCommand<'a> {
109    /// Return a positional argument value by name, or `None` if absent.
110    ///
111    /// # Examples
112    ///
113    /// ```
114    /// # use argot_cmd::{Argument, Command, Parser};
115    /// let cmd = Command::builder("get")
116    ///     .argument(Argument::builder("id").required().build().unwrap())
117    ///     .build().unwrap();
118    /// let cmds = vec![cmd];
119    /// let parsed = Parser::new(&cmds).parse(&["get", "42"]).unwrap();
120    /// assert_eq!(parsed.arg("id"), Some("42"));
121    /// assert_eq!(parsed.arg("missing"), None);
122    /// ```
123    pub fn arg(&self, name: &str) -> Option<&str> {
124        self.args.get(name).map(String::as_str)
125    }
126
127    /// Return a flag value by name, or `None` if absent.
128    ///
129    /// # Examples
130    ///
131    /// ```
132    /// # use argot_cmd::{Command, Flag, Parser};
133    /// let cmd = Command::builder("run")
134    ///     .flag(Flag::builder("output").takes_value().default_value("text").build().unwrap())
135    ///     .build().unwrap();
136    /// let cmds = vec![cmd];
137    /// let parsed = Parser::new(&cmds).parse(&["run", "--output=json"]).unwrap();
138    /// assert_eq!(parsed.flag("output"), Some("json"));
139    /// ```
140    pub fn flag(&self, name: &str) -> Option<&str> {
141        self.flags.get(name).map(String::as_str)
142    }
143
144    /// Return `true` if a boolean flag is present and set to `"true"`, `false` otherwise.
145    ///
146    /// # Examples
147    ///
148    /// ```
149    /// # use argot_cmd::{Command, Flag, Parser};
150    /// let cmd = Command::builder("build")
151    ///     .flag(Flag::builder("release").build().unwrap())
152    ///     .build().unwrap();
153    /// let cmds = vec![cmd];
154    /// let parsed = Parser::new(&cmds).parse(&["build", "--release"]).unwrap();
155    /// assert!(parsed.flag_bool("release"));
156    /// assert!(!parsed.flag_bool("missing"));
157    /// ```
158    pub fn flag_bool(&self, name: &str) -> bool {
159        self.flags.get(name).map(|v| v == "true").unwrap_or(false)
160    }
161
162    /// Return the occurrence count for a repeatable boolean flag (stored as a numeric string).
163    /// Returns `0` if the flag was not provided.
164    ///
165    /// For flags stored as `"true"` (non-repeatable), returns `1`.
166    ///
167    /// # Examples
168    ///
169    /// ```
170    /// # use argot_cmd::{Command, Flag, Parser};
171    /// // With a repeatable flag (see Flag::repeatable), -v -v -v → flag_count("verbose") == 3
172    /// // With a normal flag, --verbose → flag_count("verbose") == 1 (stored as "true")
173    /// let cmd = Command::builder("run")
174    ///     .flag(Flag::builder("verbose").short('v').build().unwrap())
175    ///     .build().unwrap();
176    /// let cmds = vec![cmd];
177    /// let parsed = Parser::new(&cmds).parse(&["run", "-v"]).unwrap();
178    /// // Non-repeatable flag stores "true"; flag_count returns 1
179    /// assert_eq!(parsed.flag_count("verbose"), 1);
180    /// assert_eq!(parsed.flag_count("missing"), 0);
181    /// ```
182    pub fn flag_count(&self, name: &str) -> u64 {
183        match self.flags.get(name) {
184            None => 0,
185            Some(v) if v == "true" => 1,
186            Some(v) if v == "false" => 0,
187            Some(v) => v.parse().unwrap_or(0),
188        }
189    }
190
191    /// Return all values for a repeatable value-taking flag as a `Vec<String>`.
192    ///
193    /// - If the flag was provided multiple times (repeatable), the stored JSON array is
194    ///   deserialized into a `Vec`.
195    /// - If the flag was provided once (non-repeatable), returns a single-element `Vec`.
196    /// - If the flag was not provided, returns an empty `Vec`.
197    ///
198    /// # Examples
199    ///
200    /// ```
201    /// # use argot_cmd::{Command, Flag, Parser};
202    /// let cmd = Command::builder("run")
203    ///     .flag(Flag::builder("output").takes_value().default_value("text").build().unwrap())
204    ///     .build().unwrap();
205    /// let cmds = vec![cmd];
206    /// let parsed = Parser::new(&cmds).parse(&["run", "--output=json"]).unwrap();
207    /// assert_eq!(parsed.flag_values("output"), vec!["json"]);
208    /// assert!(parsed.flag_values("missing").is_empty());
209    /// ```
210    pub fn flag_values(&self, name: &str) -> Vec<String> {
211        match self.flags.get(name) {
212            None => vec![],
213            Some(v) => serde_json::from_str::<Vec<String>>(v).unwrap_or_else(|_| vec![v.clone()]),
214        }
215    }
216
217    /// Return `true` if `name` is present in the parsed flag map.
218    ///
219    /// A flag is present when it was explicitly provided on the command line
220    /// **or** when the parser inserted a default or env-var value.
221    /// To distinguish explicit from default, use [`ParsedCommand::flag`] and
222    /// compare with the flag definition's default.
223    ///
224    /// # Examples
225    ///
226    /// ```
227    /// # use argot_cmd::{Command, Flag, Parser};
228    /// let cmd = Command::builder("run")
229    ///     .flag(Flag::builder("verbose").build().unwrap())
230    ///     .flag(Flag::builder("output").takes_value().default_value("text").build().unwrap())
231    ///     .build().unwrap();
232    /// let cmds = vec![cmd];
233    /// let parsed = Parser::new(&cmds).parse(&["run", "--verbose"]).unwrap();
234    /// assert!(parsed.has_flag("verbose"));
235    /// assert!(parsed.has_flag("output"));  // present via default
236    /// assert!(!parsed.has_flag("missing"));
237    /// ```
238    pub fn has_flag(&self, name: &str) -> bool {
239        self.flags.contains_key(name)
240    }
241
242    /// Parse a positional argument as type `T` using [`std::str::FromStr`].
243    ///
244    /// Returns:
245    /// - `None` if the argument was not provided (absent from the map)
246    /// - `Some(Ok(value))` if parsing succeeded
247    /// - `Some(Err(e))` if the string was present but could not be parsed
248    ///
249    /// # Examples
250    ///
251    /// ```
252    /// # use argot_cmd::{Argument, Command, Parser};
253    /// let cmd = Command::builder("resize")
254    ///     .argument(Argument::builder("width").required().build().unwrap())
255    ///     .build().unwrap();
256    /// let cmds = vec![cmd];
257    /// let parsed = Parser::new(&cmds).parse(&["resize", "1920"]).unwrap();
258    ///
259    /// let width: u32 = parsed.arg_as("width").unwrap().unwrap();
260    /// assert_eq!(width, 1920u32);
261    ///
262    /// assert!(parsed.arg_as::<u32>("missing").is_none());
263    /// ```
264    pub fn arg_as<T: std::str::FromStr>(&self, name: &str) -> Option<Result<T, T::Err>> {
265        self.args.get(name).map(|v| v.parse())
266    }
267
268    /// Parse a flag value as type `T` using [`std::str::FromStr`].
269    ///
270    /// Returns:
271    /// - `None` if the flag was not set (and has no default)
272    /// - `Some(Ok(value))` if parsing succeeded
273    /// - `Some(Err(e))` if the string was present but could not be parsed
274    ///
275    /// # Examples
276    ///
277    /// ```
278    /// # use argot_cmd::{Command, Flag, Parser};
279    /// let cmd = Command::builder("serve")
280    ///     .flag(Flag::builder("port").takes_value().default_value("8080").build().unwrap())
281    ///     .build().unwrap();
282    /// let cmds = vec![cmd];
283    /// let parsed = Parser::new(&cmds).parse(&["serve"]).unwrap();
284    ///
285    /// let port: u16 = parsed.flag_as("port").unwrap().unwrap();
286    /// assert_eq!(port, 8080u16);
287    /// ```
288    pub fn flag_as<T: std::str::FromStr>(&self, name: &str) -> Option<Result<T, T::Err>> {
289        self.flags.get(name).map(|v| v.parse())
290    }
291
292    /// Parse a positional argument as type `T`, returning a default if absent or unparseable.
293    ///
294    /// # Examples
295    ///
296    /// ```
297    /// # use argot_cmd::{Argument, Command, Parser};
298    /// let cmd = Command::builder("run")
299    ///     .argument(Argument::builder("count").build().unwrap())
300    ///     .build().unwrap();
301    /// let cmds = vec![cmd];
302    /// let parsed = Parser::new(&cmds).parse(&["run"]).unwrap();
303    ///
304    /// assert_eq!(parsed.arg_as_or("count", 1u32), 1u32);
305    /// ```
306    pub fn arg_as_or<T: std::str::FromStr>(&self, name: &str, default: T) -> T {
307        self.arg_as(name).and_then(|r| r.ok()).unwrap_or(default)
308    }
309
310    /// Parse a flag value as type `T`, returning a default if absent or unparseable.
311    ///
312    /// # Examples
313    ///
314    /// ```
315    /// # use argot_cmd::{Command, Flag, Parser};
316    /// let cmd = Command::builder("serve")
317    ///     .flag(Flag::builder("workers").takes_value().build().unwrap())
318    ///     .build().unwrap();
319    /// let cmds = vec![cmd];
320    /// let parsed = Parser::new(&cmds).parse(&["serve"]).unwrap();
321    ///
322    /// assert_eq!(parsed.flag_as_or("workers", 4u32), 4u32);
323    /// ```
324    pub fn flag_as_or<T: std::str::FromStr>(&self, name: &str, default: T) -> T {
325        self.flag_as(name).and_then(|r| r.ok()).unwrap_or(default)
326    }
327}
328
329/// A command in the registry, potentially with subcommands.
330///
331/// Commands are the central unit of argot. Each command has a canonical name,
332/// optional aliases and alternate spellings, human-readable documentation,
333/// typed positional arguments, named flags, usage examples, and an optional
334/// handler closure. Commands can be nested arbitrarily deep via
335/// [`Command::subcommands`].
336///
337/// Use [`Command::builder`] to construct instances — direct struct
338/// construction is intentionally not exposed.
339///
340/// # Serialization
341///
342/// `Command` implements `serde::Serialize` / `Deserialize`. The [`handler`]
343/// field is skipped during serialization (it cannot be represented in JSON)
344/// and will be `None` after deserialization.
345///
346/// # Examples
347///
348/// ```
349/// # use argot_cmd::{Command, Argument, Flag, Example};
350/// let cmd = Command::builder("deploy")
351///     .alias("d")
352///     .summary("Deploy the app")
353///     .description("Deploys to the specified environment.")
354///     .argument(
355///         Argument::builder("env")
356///             .description("Target environment")
357///             .required()
358///             .build()
359///             .unwrap(),
360///     )
361///     .flag(
362///         Flag::builder("dry-run")
363///             .short('n')
364///             .description("Simulate only")
365///             .build()
366///             .unwrap(),
367///     )
368///     .example(Example::new("deploy to prod", "myapp deploy production"))
369///     .build()
370///     .unwrap();
371///
372/// assert_eq!(cmd.canonical, "deploy");
373/// assert_eq!(cmd.aliases, vec!["d"]);
374/// ```
375///
376/// [`handler`]: Command::handler
377#[derive(Clone, Serialize, Deserialize)]
378pub struct Command {
379    /// The primary, canonical name used to invoke this command.
380    pub canonical: String,
381    /// Alternative names that resolve to this command (e.g. `"ls"` for `"list"`).
382    pub aliases: Vec<String>,
383    /// Alternate capitalizations or spellings (e.g. `"LIST"` for `"list"`).
384    ///
385    /// Spellings differ from aliases semantically: they represent the same
386    /// word written differently rather than a short-form abbreviation.
387    pub spellings: Vec<String>,
388    /// One-line description shown in command listings.
389    pub summary: String,
390    /// Full prose description shown in detailed help output.
391    pub description: String,
392    /// Ordered list of positional arguments accepted by this command.
393    pub arguments: Vec<Argument>,
394    /// Named flags (long and/or short) accepted by this command.
395    pub flags: Vec<Flag>,
396    /// Usage examples shown in help and Markdown documentation.
397    pub examples: Vec<Example>,
398    /// Nested sub-commands (e.g. `remote add`, `remote remove`).
399    pub subcommands: Vec<Command>,
400    /// Prose tips about correct usage, surfaced to AI agents.
401    pub best_practices: Vec<String>,
402    /// Prose warnings about incorrect usage, surfaced to AI agents.
403    pub anti_patterns: Vec<String>,
404    /// Natural-language phrases describing what this command does.
405    ///
406    /// Used for intent-based discovery (e.g. [`crate::query::Registry::match_intent`])
407    /// but intentionally excluded from normal help output.
408    pub semantic_aliases: Vec<String>,
409    /// Optional runtime handler invoked by [`crate::cli::Cli::run`].
410    ///
411    /// Skipped during JSON serialization/deserialization.
412    #[serde(skip)]
413    pub handler: Option<HandlerFn>,
414    /// Optional async runtime handler (feature: `async`).
415    ///
416    /// Skipped during JSON serialization.
417    #[cfg(feature = "async")]
418    #[serde(skip)]
419    pub async_handler: Option<AsyncHandlerFn>,
420    /// Arbitrary application-defined metadata.
421    ///
422    /// Use this to attach structured data that is not covered by the built-in
423    /// fields (e.g., permission requirements, category tags, telemetry labels).
424    ///
425    /// Serialized to JSON as an object; absent from output when empty.
426    #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
427    pub extra: HashMap<String, serde_json::Value>,
428    /// Groups of mutually exclusive flag names.
429    ///
430    /// At most one flag in each group may be provided in a single invocation.
431    /// Validated at build time (flags must exist) and enforced at parse time.
432    #[serde(default, skip_serializing_if = "Vec::is_empty")]
433    pub exclusive_groups: Vec<Vec<String>>,
434    /// Whether this command mutates state (creates, updates, or deletes resources).
435    ///
436    /// Set to `true` for commands that are not safe to run freely without
437    /// a `--dry-run` preview first. AI agents use this field to decide
438    /// whether they need explicit user confirmation before dispatching.
439    ///
440    /// Use [`CommandBuilder::mutating`] to set this field.
441    ///
442    /// # Examples
443    ///
444    /// ```
445    /// # use argot_cmd::Command;
446    /// let cmd = Command::builder("delete")
447    ///     .summary("Delete a resource")
448    ///     .mutating()
449    ///     .build()
450    ///     .unwrap();
451    ///
452    /// assert!(cmd.mutating);
453    /// ```
454    #[serde(default)]
455    pub mutating: bool,
456}
457
458impl std::fmt::Debug for Command {
459    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
460        let mut ds = f.debug_struct("Command");
461        ds.field("canonical", &self.canonical)
462            .field("aliases", &self.aliases)
463            .field("spellings", &self.spellings)
464            .field("summary", &self.summary)
465            .field("description", &self.description)
466            .field("arguments", &self.arguments)
467            .field("flags", &self.flags)
468            .field("examples", &self.examples)
469            .field("subcommands", &self.subcommands)
470            .field("best_practices", &self.best_practices)
471            .field("anti_patterns", &self.anti_patterns)
472            .field("semantic_aliases", &self.semantic_aliases)
473            .field("handler", &self.handler.as_ref().map(|_| "<handler>"));
474        #[cfg(feature = "async")]
475        ds.field(
476            "async_handler",
477            &self.async_handler.as_ref().map(|_| "<async_handler>"),
478        );
479        ds.field("extra", &self.extra)
480            .field("exclusive_groups", &self.exclusive_groups)
481            .field("mutating", &self.mutating)
482            .finish()
483    }
484}
485
486impl PartialEq for Command {
487    fn eq(&self, other: &Self) -> bool {
488        self.canonical == other.canonical
489            && self.aliases == other.aliases
490            && self.spellings == other.spellings
491            && self.summary == other.summary
492            && self.description == other.description
493            && self.arguments == other.arguments
494            && self.flags == other.flags
495            && self.examples == other.examples
496            && self.subcommands == other.subcommands
497            && self.best_practices == other.best_practices
498            && self.anti_patterns == other.anti_patterns
499            && self.semantic_aliases == other.semantic_aliases
500            && self.extra == other.extra
501            && self.exclusive_groups == other.exclusive_groups
502            && self.mutating == other.mutating
503    }
504}
505
506impl Eq for Command {}
507
508impl std::hash::Hash for Command {
509    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
510        self.canonical.hash(state);
511        self.aliases.hash(state);
512        self.spellings.hash(state);
513        self.summary.hash(state);
514        self.description.hash(state);
515        self.arguments.hash(state);
516        self.flags.hash(state);
517        self.examples.hash(state);
518        self.subcommands.hash(state);
519        self.best_practices.hash(state);
520        self.anti_patterns.hash(state);
521        self.semantic_aliases.hash(state);
522        // handler is intentionally excluded (not hashable)
523        // extra: hash keys in sorted order, with their JSON string representation
524        {
525            let mut keys: Vec<&String> = self.extra.keys().collect();
526            keys.sort();
527            for k in keys {
528                k.hash(state);
529                self.extra[k].to_string().hash(state);
530            }
531        }
532        self.exclusive_groups.hash(state);
533        self.mutating.hash(state);
534    }
535}
536
537impl PartialOrd for Command {
538    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
539        Some(self.cmp(other))
540    }
541}
542
543/// Commands are ordered by canonical name, then by their full field contents.
544impl Ord for Command {
545    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
546        self.canonical
547            .cmp(&other.canonical)
548            .then_with(|| self.summary.cmp(&other.summary))
549            .then_with(|| self.aliases.cmp(&other.aliases))
550    }
551}
552
553impl Command {
554    /// Create a new [`CommandBuilder`] with the given canonical name.
555    ///
556    /// # Arguments
557    ///
558    /// - `canonical` — The primary command name. Must be non-empty after
559    ///   trimming (enforced by [`CommandBuilder::build`]).
560    ///
561    /// # Examples
562    ///
563    /// ```
564    /// # use argot_cmd::Command;
565    /// let cmd = Command::builder("list").build().unwrap();
566    /// assert_eq!(cmd.canonical, "list");
567    /// ```
568    pub fn builder(canonical: impl Into<String>) -> CommandBuilder {
569        CommandBuilder {
570            canonical: canonical.into(),
571            aliases: Vec::new(),
572            spellings: Vec::new(),
573            summary: String::new(),
574            description: String::new(),
575            arguments: Vec::new(),
576            flags: Vec::new(),
577            examples: Vec::new(),
578            subcommands: Vec::new(),
579            best_practices: Vec::new(),
580            anti_patterns: Vec::new(),
581            semantic_aliases: Vec::new(),
582            handler: None,
583            #[cfg(feature = "async")]
584            async_handler: None,
585            extra: HashMap::new(),
586            exclusive_groups: Vec::new(),
587            mutating: false,
588        }
589    }
590
591    /// All strings this command can be matched by for exact lookup
592    /// (canonical + aliases + spellings), lowercased.
593    pub(crate) fn matchable_strings(&self) -> Vec<String> {
594        let mut v = vec![self.canonical.to_lowercase()];
595        v.extend(self.aliases.iter().map(|s| s.to_lowercase()));
596        v.extend(self.spellings.iter().map(|s| s.to_lowercase()));
597        v
598    }
599
600    /// Strings eligible for prefix matching (canonical + aliases only), lowercased.
601    ///
602    /// Spellings are intentionally excluded: they are exact-match-only so they
603    /// do not contribute to prefix ambiguity resolution and never appear in
604    /// "did you mean?" / ambiguous-candidate output.
605    pub(crate) fn prefix_matchable_strings(&self) -> Vec<String> {
606        let mut v = vec![self.canonical.to_lowercase()];
607        v.extend(self.aliases.iter().map(|s| s.to_lowercase()));
608        v
609    }
610}
611
612/// Consuming builder for [`Command`].
613///
614/// Obtain an instance via [`Command::builder`]. All setter methods consume and
615/// return `self` to allow method chaining. Call [`CommandBuilder::build`] to
616/// produce a [`Command`].
617///
618/// # Examples
619///
620/// ```
621/// # use argot_cmd::{Command, Flag};
622/// let cmd = Command::builder("run")
623///     .alias("r")
624///     .summary("Run the pipeline")
625///     .flag(Flag::builder("verbose").short('v').build().unwrap())
626///     .build()
627///     .unwrap();
628///
629/// assert_eq!(cmd.canonical, "run");
630/// assert_eq!(cmd.aliases, vec!["r"]);
631/// ```
632pub struct CommandBuilder {
633    canonical: String,
634    aliases: Vec<String>,
635    spellings: Vec<String>,
636    summary: String,
637    description: String,
638    arguments: Vec<Argument>,
639    flags: Vec<Flag>,
640    examples: Vec<Example>,
641    subcommands: Vec<Command>,
642    best_practices: Vec<String>,
643    anti_patterns: Vec<String>,
644    semantic_aliases: Vec<String>,
645    handler: Option<HandlerFn>,
646    #[cfg(feature = "async")]
647    async_handler: Option<AsyncHandlerFn>,
648    extra: HashMap<String, serde_json::Value>,
649    exclusive_groups: Vec<Vec<String>>,
650    mutating: bool,
651}
652
653impl CommandBuilder {
654    /// Replace the entire alias list with the given collection.
655    ///
656    /// To add a single alias use [`CommandBuilder::alias`].
657    pub fn aliases(mut self, aliases: impl IntoIterator<Item = impl Into<String>>) -> Self {
658        self.aliases = aliases.into_iter().map(Into::into).collect();
659        self
660    }
661
662    /// Append a single alias.
663    pub fn alias(mut self, alias: impl Into<String>) -> Self {
664        self.aliases.push(alias.into());
665        self
666    }
667
668    /// Replace the entire spelling list with the given collection.
669    ///
670    /// Spellings are silent typo-corrections or abbreviated forms that the
671    /// resolver accepts but does **not** advertise in help text. They differ
672    /// from aliases in that they will never appear in the `--help` aliases
673    /// line, and they are excluded from prefix-match ambiguity candidates.
674    ///
675    /// To add a single spelling use [`CommandBuilder::spelling`].
676    pub fn spellings(mut self, spellings: impl IntoIterator<Item = impl Into<String>>) -> Self {
677        self.spellings = spellings.into_iter().map(Into::into).collect();
678        self
679    }
680
681    /// Register a common misspelling or abbreviated form that resolves to this command.
682    ///
683    /// Unlike [`CommandBuilder::alias`], spellings are **not shown in help text**.
684    /// They serve as silent typo-correction or shorthand that resolves
685    /// to the canonical command name without advertising the alternative.
686    ///
687    /// # Difference from `alias`
688    ///
689    /// | Feature | `alias` | `spelling` |
690    /// |---------|---------|------------|
691    /// | Shown in `--help` | Yes | No |
692    /// | Resolver exact match | Yes | Yes |
693    /// | Resolver prefix match | Yes | No |
694    /// | Appears in `aliases` field | Yes | No (goes in `spellings`) |
695    ///
696    /// # Examples
697    ///
698    /// ```
699    /// # use argot_cmd::Command;
700    /// let cmd = Command::builder("deploy")
701    ///     .alias("release")           // shown in help: "deploy (release)"
702    ///     .alias("ship")              // shown in help
703    ///     .spelling("deply")          // silent typo correction
704    ///     .spelling("delpoy")         // silent typo correction
705    ///     .build().unwrap();
706    ///
707    /// assert!(cmd.aliases.contains(&"release".to_string()));
708    /// assert!(cmd.spellings.contains(&"deply".to_string()));
709    /// ```
710    pub fn spelling(mut self, s: impl Into<String>) -> Self {
711        self.spellings.push(s.into());
712        self
713    }
714
715    /// Set the one-line summary shown in command listings.
716    pub fn summary(mut self, s: impl Into<String>) -> Self {
717        self.summary = s.into();
718        self
719    }
720
721    /// Set the full prose description shown in detailed help output.
722    pub fn description(mut self, d: impl Into<String>) -> Self {
723        self.description = d.into();
724        self
725    }
726
727    /// Append a positional [`Argument`] definition.
728    ///
729    /// Arguments are bound in declaration order when the command is parsed.
730    pub fn argument(mut self, arg: Argument) -> Self {
731        self.arguments.push(arg);
732        self
733    }
734
735    /// Append a [`Flag`] definition.
736    pub fn flag(mut self, flag: Flag) -> Self {
737        self.flags.push(flag);
738        self
739    }
740
741    /// Append a usage [`Example`].
742    pub fn example(mut self, example: Example) -> Self {
743        self.examples.push(example);
744        self
745    }
746
747    /// Append a subcommand.
748    ///
749    /// Subcommands are resolved before positional arguments during parsing, so
750    /// a token that matches a subcommand name is consumed as the subcommand
751    /// selector, not as a positional value.
752    pub fn subcommand(mut self, cmd: Command) -> Self {
753        self.subcommands.push(cmd);
754        self
755    }
756
757    /// Append a best-practice tip surfaced to AI agents.
758    pub fn best_practice(mut self, bp: impl Into<String>) -> Self {
759        self.best_practices.push(bp.into());
760        self
761    }
762
763    /// Append an anti-pattern warning surfaced to AI agents.
764    pub fn anti_pattern(mut self, ap: impl Into<String>) -> Self {
765        self.anti_patterns.push(ap.into());
766        self
767    }
768
769    /// Replace the entire semantic alias list with the given collection.
770    ///
771    /// Semantic aliases are natural-language phrases used for intent-based
772    /// discovery but are **not shown in help text**.
773    ///
774    /// To add a single semantic alias use [`CommandBuilder::semantic_alias`].
775    pub fn semantic_aliases(
776        mut self,
777        aliases: impl IntoIterator<Item = impl Into<String>>,
778    ) -> Self {
779        self.semantic_aliases = aliases.into_iter().map(Into::into).collect();
780        self
781    }
782
783    /// Append a single natural-language phrase for intent-based discovery.
784    ///
785    /// Unlike [`CommandBuilder::alias`], semantic aliases are natural-language
786    /// phrases describing what the command does (e.g. `"release to production"`,
787    /// `"push to environment"`). They are used by
788    /// [`crate::query::Registry::match_intent`] but are **not shown in help text**.
789    ///
790    /// # Examples
791    ///
792    /// ```
793    /// # use argot_cmd::Command;
794    /// let cmd = Command::builder("deploy")
795    ///     .semantic_alias("release to production")
796    ///     .semantic_alias("push to environment")
797    ///     .build().unwrap();
798    ///
799    /// assert_eq!(cmd.semantic_aliases, vec!["release to production", "push to environment"]);
800    /// ```
801    pub fn semantic_alias(mut self, s: impl Into<String>) -> Self {
802        self.semantic_aliases.push(s.into());
803        self
804    }
805
806    /// Set the runtime handler invoked when this command is dispatched.
807    ///
808    /// The handler receives a [`ParsedCommand`] and should return `Ok(())`
809    /// on success or a boxed error on failure.
810    pub fn handler(mut self, h: HandlerFn) -> Self {
811        self.handler = Some(h);
812        self
813    }
814
815    /// Register an async handler for this command (feature: `async`).
816    ///
817    /// The handler receives a [`ParsedCommand`] and returns a boxed future.
818    /// Use [`crate::Cli::run_async`] to dispatch async handlers.
819    ///
820    /// # Examples
821    ///
822    /// ```
823    /// # #[cfg(feature = "async")] {
824    /// use std::sync::Arc;
825    /// use argot_cmd::Command;
826    ///
827    /// let cmd = Command::builder("deploy")
828    ///     .async_handler(Arc::new(|parsed| Box::pin(async move {
829    ///         println!("deployed!");
830    ///         Ok(())
831    ///     })))
832    ///     .build()
833    ///     .unwrap();
834    /// # }
835    /// ```
836    #[cfg(feature = "async")]
837    pub fn async_handler(mut self, h: AsyncHandlerFn) -> Self {
838        self.async_handler = Some(h);
839        self
840    }
841
842    /// Set an arbitrary metadata entry on this command.
843    ///
844    /// # Examples
845    ///
846    /// ```
847    /// # use argot_cmd::Command;
848    /// # use serde_json::json;
849    /// let cmd = Command::builder("deploy")
850    ///     .meta("category", json!("infrastructure"))
851    ///     .meta("min_role", json!("ops"))
852    ///     .build()
853    ///     .unwrap();
854    ///
855    /// assert_eq!(cmd.extra["category"], json!("infrastructure"));
856    /// ```
857    pub fn meta(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
858        self.extra.insert(key.into(), value);
859        self
860    }
861
862    /// Declare a group of mutually exclusive flags.
863    ///
864    /// At most one flag from `flags` may be provided in a single invocation.
865    /// The parser returns [`crate::ParseError::MutuallyExclusive`] if two or
866    /// more flags from the same group are present.
867    ///
868    /// # Panics (at build time via [`BuildError`])
869    ///
870    /// [`CommandBuilder::build`] returns an error if:
871    /// - The group has fewer than 2 flags.
872    /// - Any flag name in the group is not defined on this command.
873    ///
874    /// # Examples
875    ///
876    /// ```
877    /// # use argot_cmd::{Command, Flag};
878    /// let cmd = Command::builder("export")
879    ///     .flag(Flag::builder("json").build().unwrap())
880    ///     .flag(Flag::builder("yaml").build().unwrap())
881    ///     .flag(Flag::builder("csv").build().unwrap())
882    ///     .exclusive(["json", "yaml", "csv"])
883    ///     .build()
884    ///     .unwrap();
885    /// assert_eq!(cmd.exclusive_groups.len(), 1);
886    /// ```
887    pub fn exclusive(mut self, flags: impl IntoIterator<Item = impl Into<String>>) -> Self {
888        self.exclusive_groups
889            .push(flags.into_iter().map(Into::into).collect());
890        self
891    }
892
893    /// Mark this command as mutating (i.e., it modifies state).
894    ///
895    /// Mutating commands are flagged in help output and JSON schema so that
896    /// AI agents know they should exercise caution — typically by running a
897    /// `--dry-run` first — before dispatching the command.
898    ///
899    /// # Examples
900    ///
901    /// ```
902    /// # use argot_cmd::Command;
903    /// let cmd = Command::builder("delete")
904    ///     .summary("Delete a resource")
905    ///     .mutating()
906    ///     .build()
907    ///     .unwrap();
908    ///
909    /// assert!(cmd.mutating);
910    /// ```
911    pub fn mutating(mut self) -> Self {
912        self.mutating = true;
913        self
914    }
915
916    /// Consume the builder and return a [`Command`].
917    ///
918    /// # Errors
919    ///
920    /// - [`BuildError::EmptyCanonical`] — canonical name is empty or whitespace.
921    /// - [`BuildError::AliasEqualsCanonical`] — an alias is identical to the
922    ///   canonical name (case-insensitive).
923    /// - [`BuildError::DuplicateAlias`] — two aliases share the same string
924    ///   (case-insensitive comparison).
925    /// - [`BuildError::DuplicateFlagName`] — two flags share the same long name.
926    /// - [`BuildError::DuplicateShortFlag`] — two flags share the same short
927    ///   character.
928    /// - [`BuildError::DuplicateArgumentName`] — two positional arguments share
929    ///   the same name.
930    /// - [`BuildError::DuplicateSubcommandName`] — two subcommands share the
931    ///   same canonical name.
932    /// - [`BuildError::VariadicNotLast`] — a variadic argument is not the last
933    ///   argument in the list.
934    ///
935    /// # Examples
936    ///
937    /// ```
938    /// # use argot_cmd::{Command, BuildError};
939    /// assert!(Command::builder("ok").build().is_ok());
940    /// assert_eq!(Command::builder("").build().unwrap_err(), BuildError::EmptyCanonical);
941    /// ```
942    pub fn build(self) -> Result<Command, BuildError> {
943        if self.canonical.trim().is_empty() {
944            return Err(BuildError::EmptyCanonical);
945        }
946
947        // 1. Alias equals canonical
948        let canonical_lower = self.canonical.to_lowercase();
949        for alias in &self.aliases {
950            if alias.to_lowercase() == canonical_lower {
951                return Err(BuildError::AliasEqualsCanonical(alias.clone()));
952            }
953        }
954
955        // 2. Duplicate aliases
956        let mut seen_aliases = std::collections::HashSet::new();
957        for alias in &self.aliases {
958            let key = alias.to_lowercase();
959            if !seen_aliases.insert(key) {
960                return Err(BuildError::DuplicateAlias(alias.clone()));
961            }
962        }
963
964        // 3. Duplicate flag names (long)
965        let mut seen_flag_names = std::collections::HashSet::new();
966        for flag in &self.flags {
967            if !seen_flag_names.insert(flag.name.clone()) {
968                return Err(BuildError::DuplicateFlagName(flag.name.clone()));
969            }
970        }
971
972        // 4. Duplicate short flags
973        let mut seen_short_flags = std::collections::HashSet::new();
974        for flag in &self.flags {
975            if let Some(c) = flag.short {
976                if !seen_short_flags.insert(c) {
977                    return Err(BuildError::DuplicateShortFlag(c));
978                }
979            }
980        }
981
982        // 5. Duplicate argument names
983        let mut seen_arg_names = std::collections::HashSet::new();
984        for arg in &self.arguments {
985            if !seen_arg_names.insert(arg.name.clone()) {
986                return Err(BuildError::DuplicateArgumentName(arg.name.clone()));
987            }
988        }
989
990        // 6. Duplicate subcommand canonical names
991        let mut seen_sub_names = std::collections::HashSet::new();
992        for sub in &self.subcommands {
993            if !seen_sub_names.insert(sub.canonical.clone()) {
994                return Err(BuildError::DuplicateSubcommandName(sub.canonical.clone()));
995            }
996        }
997
998        // 7. Variadic argument must be last
999        for (i, arg) in self.arguments.iter().enumerate() {
1000            if arg.variadic && i != self.arguments.len() - 1 {
1001                return Err(BuildError::VariadicNotLast(arg.name.clone()));
1002            }
1003        }
1004
1005        // 8. Flags with choices must have a non-empty choices list
1006        for flag in &self.flags {
1007            if let Some(choices) = &flag.choices {
1008                if choices.is_empty() {
1009                    return Err(BuildError::EmptyChoices(flag.name.clone()));
1010                }
1011            }
1012        }
1013
1014        // 9. Mutual exclusivity group validation
1015        for group in &self.exclusive_groups {
1016            if group.len() < 2 {
1017                return Err(BuildError::ExclusiveGroupTooSmall);
1018            }
1019            for flag_name in group {
1020                if !self.flags.iter().any(|f| &f.name == flag_name) {
1021                    return Err(BuildError::ExclusiveGroupUnknownFlag(flag_name.clone()));
1022                }
1023            }
1024        }
1025
1026        Ok(Command {
1027            canonical: self.canonical,
1028            aliases: self.aliases,
1029            spellings: self.spellings,
1030            summary: self.summary,
1031            description: self.description,
1032            arguments: self.arguments,
1033            flags: self.flags,
1034            examples: self.examples,
1035            subcommands: self.subcommands,
1036            best_practices: self.best_practices,
1037            anti_patterns: self.anti_patterns,
1038            semantic_aliases: self.semantic_aliases,
1039            handler: self.handler,
1040            #[cfg(feature = "async")]
1041            async_handler: self.async_handler,
1042            extra: self.extra,
1043            exclusive_groups: self.exclusive_groups,
1044            mutating: self.mutating,
1045        })
1046    }
1047}
1048
1049#[cfg(test)]
1050mod tests {
1051    use super::*;
1052    use crate::model::{Argument, Flag};
1053
1054    fn make_simple_cmd() -> Command {
1055        Command::builder("run")
1056            .alias("r")
1057            .spelling("RUN")
1058            .summary("Run something")
1059            .description("Runs the thing.")
1060            .build()
1061            .unwrap()
1062    }
1063
1064    #[test]
1065    fn test_builder_happy_path() {
1066        let cmd = make_simple_cmd();
1067        assert_eq!(cmd.canonical, "run");
1068        assert_eq!(cmd.aliases, vec!["r"]);
1069        assert_eq!(cmd.spellings, vec!["RUN"]);
1070    }
1071
1072    #[test]
1073    fn test_builder_empty_canonical() {
1074        assert_eq!(
1075            Command::builder("").build().unwrap_err(),
1076            BuildError::EmptyCanonical
1077        );
1078        assert_eq!(
1079            Command::builder("   ").build().unwrap_err(),
1080            BuildError::EmptyCanonical
1081        );
1082    }
1083
1084    #[test]
1085    fn test_partial_eq_ignores_handler() {
1086        let cmd1 = Command::builder("run").build().unwrap();
1087        let mut cmd2 = cmd1.clone();
1088        cmd2.handler = Some(Arc::new(|_| Ok(())));
1089        assert_eq!(cmd1, cmd2);
1090    }
1091
1092    #[test]
1093    fn test_serde_round_trip_skips_handler() {
1094        let cmd = Command::builder("deploy")
1095            .summary("Deploy the app")
1096            .argument(
1097                Argument::builder("env")
1098                    .description("target env")
1099                    .required()
1100                    .build()
1101                    .unwrap(),
1102            )
1103            .flag(
1104                Flag::builder("dry-run")
1105                    .description("dry run mode")
1106                    .build()
1107                    .unwrap(),
1108            )
1109            .handler(Arc::new(|_| Ok(())))
1110            .build()
1111            .unwrap();
1112
1113        let json = serde_json::to_string(&cmd).unwrap();
1114        let de: Command = serde_json::from_str(&json).unwrap();
1115        assert_eq!(cmd, de);
1116        assert!(de.handler.is_none());
1117    }
1118
1119    #[test]
1120    fn test_matchable_strings() {
1121        let cmd = Command::builder("Git")
1122            .alias("g")
1123            .spelling("GIT")
1124            .build()
1125            .unwrap();
1126        let matchables = cmd.matchable_strings();
1127        assert!(matchables.contains(&"git".to_string()));
1128        assert!(matchables.contains(&"g".to_string()));
1129        assert!(matchables.contains(&"git".to_string())); // spelling lowercased
1130    }
1131
1132    #[test]
1133    fn test_clone_shares_handler() {
1134        let called = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
1135        let called_clone = called.clone();
1136        let cmd = Command::builder("x")
1137            .handler(Arc::new(move |_| {
1138                called_clone.store(true, std::sync::atomic::Ordering::SeqCst);
1139                Ok(())
1140            }))
1141            .build()
1142            .unwrap();
1143        let cmd2 = cmd.clone();
1144        assert!(std::sync::Arc::ptr_eq(
1145            cmd.handler.as_ref().unwrap(),
1146            cmd2.handler.as_ref().unwrap()
1147        ));
1148    }
1149
1150    #[test]
1151    fn test_duplicate_alias_rejected() {
1152        let err = Command::builder("cmd")
1153            .alias("a")
1154            .alias("a")
1155            .build()
1156            .unwrap_err();
1157        assert!(matches!(err, BuildError::DuplicateAlias(_)));
1158    }
1159
1160    #[test]
1161    fn test_alias_equals_canonical_rejected() {
1162        let err = Command::builder("deploy")
1163            .alias("deploy")
1164            .build()
1165            .unwrap_err();
1166        assert!(matches!(err, BuildError::AliasEqualsCanonical(_)));
1167    }
1168
1169    #[test]
1170    fn test_duplicate_flag_name_rejected() {
1171        let flag = Flag::builder("verbose").build().unwrap();
1172        let err = Command::builder("cmd")
1173            .flag(flag.clone())
1174            .flag(flag)
1175            .build()
1176            .unwrap_err();
1177        assert!(matches!(err, BuildError::DuplicateFlagName(_)));
1178    }
1179
1180    #[test]
1181    fn test_duplicate_short_flag_rejected() {
1182        let f1 = Flag::builder("verbose").short('v').build().unwrap();
1183        let f2 = Flag::builder("version").short('v').build().unwrap();
1184        let err = Command::builder("cmd")
1185            .flag(f1)
1186            .flag(f2)
1187            .build()
1188            .unwrap_err();
1189        assert!(matches!(err, BuildError::DuplicateShortFlag('v')));
1190    }
1191
1192    #[test]
1193    fn test_duplicate_argument_name_rejected() {
1194        let arg = Argument::builder("env").build().unwrap();
1195        let err = Command::builder("cmd")
1196            .argument(arg.clone())
1197            .argument(arg)
1198            .build()
1199            .unwrap_err();
1200        assert!(matches!(err, BuildError::DuplicateArgumentName(_)));
1201    }
1202
1203    #[test]
1204    fn test_duplicate_subcommand_name_rejected() {
1205        let sub = Command::builder("add").build().unwrap();
1206        let err = Command::builder("remote")
1207            .subcommand(sub.clone())
1208            .subcommand(sub)
1209            .build()
1210            .unwrap_err();
1211        assert!(matches!(err, BuildError::DuplicateSubcommandName(_)));
1212    }
1213
1214    #[test]
1215    fn test_variadic_must_be_last() {
1216        let variadic = Argument::builder("files").variadic().build().unwrap();
1217        let after = Argument::builder("extra").build().unwrap();
1218        let err = Command::builder("cmd")
1219            .argument(variadic)
1220            .argument(after)
1221            .build()
1222            .unwrap_err();
1223        assert!(matches!(err, BuildError::VariadicNotLast(_)));
1224    }
1225
1226    #[test]
1227    fn test_meta_field_serde() {
1228        let cmd = Command::builder("x")
1229            .meta("role", serde_json::json!("admin"))
1230            .build()
1231            .unwrap();
1232        let json = serde_json::to_string(&cmd).unwrap();
1233        assert!(json.contains("admin"));
1234        let de: Command = serde_json::from_str(&json).unwrap();
1235        assert_eq!(de.extra["role"], serde_json::json!("admin"));
1236    }
1237
1238    #[test]
1239    fn test_meta_empty_not_serialized() {
1240        let cmd = Command::builder("x").build().unwrap();
1241        let json = serde_json::to_string(&cmd).unwrap();
1242        assert!(!json.contains("extra"));
1243    }
1244
1245    #[test]
1246    fn test_semantic_alias_builder() {
1247        let cmd = Command::builder("deploy")
1248            .semantic_alias("release to production")
1249            .semantic_alias("push to environment")
1250            .build()
1251            .unwrap();
1252        assert_eq!(
1253            cmd.semantic_aliases,
1254            vec!["release to production", "push to environment"]
1255        );
1256    }
1257
1258    #[test]
1259    fn test_semantic_aliases_bulk_builder() {
1260        let cmd = Command::builder("deploy")
1261            .semantic_aliases(["release to production", "push to environment"])
1262            .build()
1263            .unwrap();
1264        assert_eq!(
1265            cmd.semantic_aliases,
1266            vec!["release to production", "push to environment"]
1267        );
1268    }
1269
1270    #[test]
1271    fn test_semantic_alias_not_in_canonical_aliases() {
1272        let cmd = Command::builder("deploy")
1273            .alias("d")
1274            .semantic_alias("release to production")
1275            .build()
1276            .unwrap();
1277        // semantic_aliases and aliases are distinct fields
1278        assert_eq!(cmd.aliases, vec!["d"]);
1279        assert_eq!(cmd.semantic_aliases, vec!["release to production"]);
1280        assert!(!cmd.aliases.contains(&"release to production".to_string()));
1281        assert!(!cmd.semantic_aliases.contains(&"d".to_string()));
1282    }
1283
1284    #[test]
1285    fn test_debug_impl_includes_fields() {
1286        let cmd = Command::builder("debug-test")
1287            .alias("dt")
1288            .spelling("DEBUGTEST")
1289            .summary("Test debug")
1290            .description("A debug test command")
1291            .handler(Arc::new(|_| Ok(())))
1292            .build()
1293            .unwrap();
1294        let s = format!("{:?}", cmd);
1295        assert!(s.contains("debug-test"));
1296        assert!(s.contains("dt"));
1297        assert!(s.contains("Test debug"));
1298        // Handler should appear as "<handler>" not a raw pointer
1299        assert!(s.contains("<handler>"));
1300        // spellings should also be shown
1301        assert!(s.contains("DEBUGTEST"));
1302    }
1303
1304    #[test]
1305    fn test_hash_deterministic() {
1306        use std::collections::hash_map::DefaultHasher;
1307        use std::hash::{Hash, Hasher};
1308
1309        let cmd1 = Command::builder("hash-test")
1310            .summary("Hash")
1311            .meta("key", serde_json::json!("value"))
1312            .build()
1313            .unwrap();
1314        let cmd2 = Command::builder("hash-test")
1315            .summary("Hash")
1316            .meta("key", serde_json::json!("value"))
1317            .build()
1318            .unwrap();
1319
1320        let mut h1 = DefaultHasher::new();
1321        cmd1.hash(&mut h1);
1322        let mut h2 = DefaultHasher::new();
1323        cmd2.hash(&mut h2);
1324        assert_eq!(h1.finish(), h2.finish());
1325    }
1326
1327    #[test]
1328    fn test_ord_by_canonical() {
1329        let a = Command::builder("alpha").build().unwrap();
1330        let b = Command::builder("beta").build().unwrap();
1331        assert!(a < b);
1332        assert!(b > a);
1333        assert_eq!(a.partial_cmp(&a), Some(std::cmp::Ordering::Equal));
1334    }
1335
1336    #[test]
1337    fn test_ord_same_canonical_by_summary() {
1338        let a = Command::builder("cmd").summary("Alpha").build().unwrap();
1339        let b = Command::builder("cmd").summary("Beta").build().unwrap();
1340        assert!(a < b);
1341    }
1342
1343    #[test]
1344    fn test_aliases_builder_method() {
1345        let cmd = Command::builder("cmd")
1346            .aliases(["a", "b", "c"])
1347            .build()
1348            .unwrap();
1349        assert_eq!(cmd.aliases, vec!["a", "b", "c"]);
1350    }
1351
1352    #[test]
1353    fn test_spellings_builder_method() {
1354        let cmd = Command::builder("deploy")
1355            .spellings(["DEPLOY", "dploy"])
1356            .build()
1357            .unwrap();
1358        assert_eq!(cmd.spellings, vec!["DEPLOY", "dploy"]);
1359    }
1360
1361    #[test]
1362    fn test_best_practice_and_anti_pattern_builder() {
1363        let cmd = Command::builder("cmd")
1364            .best_practice("Always dry-run first")
1365            .anti_pattern("Deploy on Fridays")
1366            .build()
1367            .unwrap();
1368        assert_eq!(cmd.best_practices, vec!["Always dry-run first"]);
1369        assert_eq!(cmd.anti_patterns, vec!["Deploy on Fridays"]);
1370    }
1371
1372    #[test]
1373    fn test_exclusive_group_too_small() {
1374        use crate::model::BuildError;
1375        let result = Command::builder("cmd")
1376            .flag(Flag::builder("json").build().unwrap())
1377            .exclusive(["json"])
1378            .build();
1379        assert!(matches!(result, Err(BuildError::ExclusiveGroupTooSmall)));
1380    }
1381
1382    #[test]
1383    fn test_exclusive_group_unknown_flag() {
1384        use crate::model::BuildError;
1385        let result = Command::builder("cmd")
1386            .flag(Flag::builder("json").build().unwrap())
1387            .exclusive(["json", "nonexistent"])
1388            .build();
1389        assert!(matches!(
1390            result,
1391            Err(BuildError::ExclusiveGroupUnknownFlag(_))
1392        ));
1393    }
1394
1395    #[test]
1396    fn test_parsed_command_helpers() {
1397        use crate::{Argument, Flag, Parser};
1398        let cmd = Command::builder("serve")
1399            .argument(Argument::builder("host").required().build().unwrap())
1400            .flag(
1401                Flag::builder("port")
1402                    .takes_value()
1403                    .default_value("8080")
1404                    .build()
1405                    .unwrap(),
1406            )
1407            .flag(Flag::builder("verbose").build().unwrap())
1408            .build()
1409            .unwrap();
1410        let cmds = vec![cmd];
1411        let parser = Parser::new(&cmds);
1412        let parsed = parser.parse(&["serve", "localhost", "--verbose"]).unwrap();
1413
1414        assert_eq!(parsed.arg("host"), Some("localhost"));
1415        assert_eq!(parsed.arg("missing"), None);
1416        assert_eq!(parsed.flag("port"), Some("8080"));
1417        assert_eq!(parsed.flag("missing"), None);
1418        assert!(parsed.flag_bool("verbose"));
1419        assert!(!parsed.flag_bool("missing"));
1420        assert_eq!(parsed.flag_count("verbose"), 1);
1421        assert_eq!(parsed.flag_count("missing"), 0);
1422        assert_eq!(parsed.flag_values("port"), vec!["8080"]);
1423        assert!(parsed.flag_values("missing").is_empty());
1424        assert!(parsed.has_flag("port"));
1425        assert!(!parsed.has_flag("missing"));
1426
1427        let port: u16 = parsed.flag_as("port").unwrap().unwrap();
1428        assert_eq!(port, 8080);
1429
1430        let port_or: u16 = parsed.flag_as_or("port", 9000);
1431        assert_eq!(port_or, 8080);
1432
1433        let missing_or: u16 = parsed.flag_as_or("missing", 9000);
1434        assert_eq!(missing_or, 9000);
1435
1436        let host: String = parsed.arg_as("host").unwrap().unwrap();
1437        assert_eq!(host, "localhost");
1438
1439        let missing_as: Option<Result<u32, _>> = parsed.arg_as("missing");
1440        assert!(missing_as.is_none());
1441
1442        let arg_or: String = parsed.arg_as_or("host", "default".to_string());
1443        assert_eq!(arg_or, "localhost");
1444
1445        let missing_arg_or: String = parsed.arg_as_or("missing", "default".to_string());
1446        assert_eq!(missing_arg_or, "default");
1447    }
1448
1449    #[test]
1450    fn test_flag_count_false_returns_zero() {
1451        use crate::{Flag, Parser};
1452        let cmd = Command::builder("cmd")
1453            .flag(Flag::builder("verbose").build().unwrap())
1454            .build()
1455            .unwrap();
1456        let cmds = vec![cmd];
1457        let parser = Parser::new(&cmds);
1458        let parsed = parser.parse(&["cmd", "--no-verbose"]).unwrap();
1459        assert_eq!(parsed.flag_count("verbose"), 0);
1460    }
1461
1462    #[test]
1463    fn test_flag_values_json_array() {
1464        use crate::{Flag, Parser};
1465        let cmd = Command::builder("cmd")
1466            .flag(
1467                Flag::builder("tag")
1468                    .takes_value()
1469                    .repeatable()
1470                    .build()
1471                    .unwrap(),
1472            )
1473            .build()
1474            .unwrap();
1475        let cmds = vec![cmd];
1476        let parser = Parser::new(&cmds);
1477        let parsed = parser.parse(&["cmd", "--tag=alpha", "--tag=beta"]).unwrap();
1478        let values = parsed.flag_values("tag");
1479        assert_eq!(values, vec!["alpha", "beta"]);
1480    }
1481
1482    #[test]
1483    fn test_mutating_builder_sets_field() {
1484        let non_mutating = Command::builder("list").build().unwrap();
1485        assert!(!non_mutating.mutating, "default should be non-mutating");
1486
1487        let mutating = Command::builder("delete")
1488            .summary("Delete a resource")
1489            .mutating()
1490            .build()
1491            .unwrap();
1492        assert!(mutating.mutating, "mutating() should set mutating to true");
1493    }
1494
1495    #[test]
1496    fn test_mutating_serde_round_trip() {
1497        let cmd = Command::builder("delete")
1498            .summary("Delete a resource")
1499            .mutating()
1500            .build()
1501            .unwrap();
1502        let json = serde_json::to_string(&cmd).unwrap();
1503        assert!(json.contains("\"mutating\":true"), "JSON should include mutating:true");
1504        let de: Command = serde_json::from_str(&json).unwrap();
1505        assert!(de.mutating, "deserialized command should have mutating=true");
1506    }
1507
1508    #[test]
1509    fn test_non_mutating_serde_default() {
1510        let cmd = Command::builder("list").build().unwrap();
1511        let json = serde_json::to_string(&cmd).unwrap();
1512        // With #[serde(default)], false is still serialized (no skip_serializing_if)
1513        let de: Command = serde_json::from_str(&json).unwrap();
1514        assert!(!de.mutating, "deserialized non-mutating command should have mutating=false");
1515    }
1516}