Skip to main content

dynamic_cli/config/
schema.rs

1//! Configuration schema definitions
2//!
3//! This module defines all data structures for representing
4//! CLI/REPL configurations loaded from YAML or JSON files.
5//!
6//! # Main Components
7//!
8//! - [`CommandsConfig`]: Root configuration structure
9//! - [`CommandDefinition`]: Individual command specification
10//! - [`ArgumentType`]: Supported argument types
11//! - [`ValidationRule`]: Validation constraints
12
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15
16/// Complete configuration for CLI/REPL commands
17///
18/// This is the root structure deserialized from YAML/JSON files.
19/// It contains metadata about the interface and all command definitions.
20///
21/// # Example YAML
22///
23/// ```yaml
24/// metadata:
25///   version: "1.0.0"
26///   prompt: "myapp"
27///   prompt_suffix: " > "
28/// commands:
29///   - name: hello
30///     description: "Say hello"
31///     # ... more fields
32/// global_options: []
33/// ```
34#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
35pub struct CommandsConfig {
36    /// Metadata about the application interface
37    pub metadata: Metadata,
38
39    /// List of all available commands
40    pub commands: Vec<CommandDefinition>,
41
42    /// Global options available to all commands
43    #[serde(default)]
44    pub global_options: Vec<OptionDefinition>,
45}
46
47/// Metadata for the CLI/REPL interface
48///
49/// Contains information about the application version
50/// and prompt customization for REPL mode.
51///
52/// # Fields
53///
54/// - `version`: Application version string
55/// - `prompt`: Command prompt prefix (e.g., "myapp")
56/// - `prompt_suffix`: Suffix after prompt (e.g., " > ")
57#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
58pub struct Metadata {
59    /// Application version (e.g., "1.0.0")
60    pub version: String,
61
62    /// Prompt prefix displayed in REPL mode
63    ///
64    /// Example: "chrom-rs" will display as "chrom-rs > "
65    pub prompt: String,
66
67    /// Prompt suffix (typically " > " or ": ")
68    #[serde(default = "default_prompt_suffix")]
69    pub prompt_suffix: String,
70}
71
72/// Default prompt suffix
73fn default_prompt_suffix() -> String {
74    " > ".to_string()
75}
76
77/// Definition of a single command
78///
79/// Describes a command with its arguments, options, and validation rules.
80/// Each command must have a corresponding handler implementation.
81///
82/// # Example
83///
84/// ```yaml
85/// name: simulate
86/// aliases: [sim, run]
87/// description: "Run a simulation"
88/// required: true
89/// arguments:
90///   - name: input_file
91///     arg_type: path
92///     required: true
93///     description: "Input configuration file"
94///     validation:
95///       - must_exist: true
96///       - extensions: [yaml, json]
97/// options: []
98/// implementation: "simulate_handler"
99/// continue_on_failure: false
100/// requires_success: false
101/// ```
102#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
103pub struct CommandDefinition {
104    /// Command name (used for invocation)
105    pub name: String,
106
107    /// Alternative names for the command
108    #[serde(default)]
109    pub aliases: Vec<String>,
110
111    /// Human-readable description for help text
112    pub description: String,
113
114    /// Whether this command is required to be implemented
115    ///
116    /// If true, the application will fail to start if no handler is registered.
117    #[serde(default)]
118    pub required: bool,
119
120    /// Positional arguments
121    #[serde(default)]
122    pub arguments: Vec<ArgumentDefinition>,
123
124    /// Named options (flags)
125    #[serde(default)]
126    pub options: Vec<OptionDefinition>,
127
128    /// Name of the handler implementation
129    ///
130    /// This string is used to match the command with its
131    /// registered handler in the CommandRegistry.
132    pub implementation: String,
133
134    /// Whether the chain continues when *this* command fails (DD-026).
135    ///
136    /// Governs the chain's behaviour when this command is one segment of a
137    /// multi-command CLI invocation (or a chained `run_script()` line) and
138    /// its handler returns an error:
139    ///
140    /// - `false` (default): stop-on-first-error — identical to today's
141    ///   single-command exit behaviour, just applied per segment.
142    /// - `true`: the chain-position error is reported but execution
143    ///   proceeds to the next segment. Intended for genuinely optional
144    ///   steps (e.g. a non-critical secondary export).
145    ///
146    /// Has no effect outside a chain (a single, non-chained command
147    /// behaves exactly as it does today regardless of this value).
148    #[serde(default)]
149    pub continue_on_failure: bool,
150
151    /// Whether *this* command requires every earlier command in the chain
152    /// to have succeeded before it is allowed to run (DD-026).
153    ///
154    /// Not to be confused with [`Self::required`], which is an unrelated,
155    /// startup-time check ("a handler must be registered for this
156    /// command") — `requires_success` is a per-invocation, chain-position
157    /// concern evaluated at dispatch time, and only observable downstream
158    /// of an earlier command whose own `continue_on_failure` is `true`
159    /// (otherwise the chain has already stopped by the time this command
160    /// would run).
161    ///
162    /// - `false` (default): runs regardless of any earlier failure in the
163    ///   chain (moot under the default stop-on-first policy).
164    /// - `true`: if any earlier command in the chain has already failed
165    ///   (tracked via a running `chain_has_failure` flag, regardless of
166    ///   that failure's own `continue_on_failure` value), this command is
167    ///   skipped — not executed, not counted as an additional failure —
168    ///   and reported distinctly from a chain-position error.
169    #[serde(default)]
170    pub requires_success: bool,
171}
172
173/// Definition of a positional argument
174///
175/// Positional arguments are required in order and don't have
176/// a flag prefix (unlike options).
177///
178/// # Example
179///
180/// ```yaml
181/// name: input_file
182/// arg_type: path
183/// required: true
184/// description: "Path to input file"
185/// validation:
186///   - must_exist: true
187///   - extensions: [yaml, yml]
188/// ```
189///
190/// To prevent a sensitive argument value from being written to the REPL
191/// history file, set `secure: true`:
192///
193/// ```yaml
194/// name: password
195/// arg_type: string
196/// required: true
197/// description: "User password"
198/// secure: true
199/// ```
200///
201/// When a command line contains at least one argument marked `secure: true`,
202/// the entire line is silently omitted from the history file. The command
203/// name itself is not filtered — only lines with a secure argument value
204/// are suppressed.
205#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
206pub struct ArgumentDefinition {
207    /// Argument name (used in error messages and documentation)
208    pub name: String,
209
210    /// Expected type of the argument
211    pub arg_type: ArgumentType,
212
213    /// Whether the argument is mandatory
214    pub required: bool,
215
216    /// Human-readable description
217    pub description: String,
218
219    /// Validation rules to apply
220    #[serde(default)]
221    pub validation: Vec<ValidationRule>,
222
223    /// Whether this argument carries a sensitive value.
224    ///
225    /// When `true`, any REPL command line that provides a value for this
226    /// argument is not written to the history file. Defaults to `false`.
227    #[serde(default)]
228    pub secure: bool,
229}
230
231/// Definition of a named option (flag)
232///
233/// Options are optional (by default) and can be specified
234/// with short (`-o`) or long (`--option`) forms.
235///
236/// # Example
237///
238/// ```yaml
239/// name: output
240/// short: o
241/// long: output
242/// option_type: path
243/// required: false
244/// default: "output.txt"
245/// description: "Output file path"
246/// choices: []
247/// ```
248#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
249pub struct OptionDefinition {
250    /// Option name (internal identifier)
251    pub name: String,
252
253    /// Short form (single character, e.g., "o" for -o)
254    pub short: Option<String>,
255
256    /// Long form (e.g., "output" for --output)
257    pub long: Option<String>,
258
259    /// Expected type of the option value
260    pub option_type: ArgumentType,
261
262    /// Whether this option is mandatory
263    #[serde(default)]
264    pub required: bool,
265
266    /// Default value if not specified
267    pub default: Option<String>,
268
269    /// Human-readable description
270    pub description: String,
271
272    /// Restricted set of allowed values
273    ///
274    /// If non-empty, the value must be one of these choices. When
275    /// `repeatable` is `true`, this list also doubles as the set of
276    /// valid discriminants for each occurrence (see [`Self::option_parameters`]).
277    #[serde(default)]
278    pub choices: Vec<String>,
279
280    /// Whether this option can appear more than once on a single
281    /// command line, each occurrence carrying its own discriminant
282    /// (from `choices`) and `key=value` sub-parameters.
283    ///
284    /// Defaults to `false` — fully backward-compatible with existing
285    /// scalar options.
286    #[serde(default)]
287    pub repeatable: bool,
288
289    /// Per-discriminant shape of the `key=value` sub-parameters accepted
290    /// by a repeatable option.
291    ///
292    /// Keys must exactly match the entries in [`Self::choices`] (enforced
293    /// by the config validator, not at deserialization time). Each value
294    /// reuses [`ArgumentDefinition`] — the same vocabulary already used
295    /// for positional `arguments:` — rather than a dedicated mini-schema
296    /// type.
297    ///
298    /// Ignored (and should be empty) when `repeatable` is `false`.
299    #[serde(default)]
300    pub option_parameters: HashMap<String, Vec<ArgumentDefinition>>,
301}
302
303/// Supported argument and option types
304///
305/// These types are used for automatic parsing and validation
306/// of user input.
307///
308/// # Serialization
309///
310/// Types are serialized as lowercase strings in YAML/JSON:
311/// - `String` → "string"
312/// - `Integer` → "integer"
313/// - `Float` → "float"
314/// - `Bool` → "bool"
315/// - `Path` → "path"
316#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
317#[serde(rename_all = "lowercase")]
318pub enum ArgumentType {
319    /// UTF-8 string
320    String,
321
322    /// Signed integer (i64)
323    Integer,
324
325    /// Floating-point number (f64)
326    Float,
327
328    /// Boolean value (true/false, yes/no, 1/0)
329    Bool,
330
331    /// File system path
332    ///
333    /// Represents a path that may or may not exist,
334    /// depending on validation rules.
335    Path,
336}
337
338impl ArgumentType {
339    /// Get the type name as a string for error messages
340    ///
341    /// # Example
342    ///
343    /// ```
344    /// use dynamic_cli::config::schema::ArgumentType;
345    ///
346    /// assert_eq!(ArgumentType::Integer.as_str(), "integer");
347    /// assert_eq!(ArgumentType::Path.as_str(), "path");
348    /// ```
349    pub fn as_str(&self) -> &'static str {
350        match self {
351            ArgumentType::String => "string",
352            ArgumentType::Integer => "integer",
353            ArgumentType::Float => "float",
354            ArgumentType::Bool => "bool",
355            ArgumentType::Path => "path",
356        }
357    }
358}
359
360/// Validation rules for arguments and options
361///
362/// These rules are applied after type parsing to enforce
363/// additional constraints on values.
364///
365/// # Variants
366///
367/// - `MustExist`: For paths, require that the file/directory exists
368/// - `Extensions`: For paths, restrict to specific file extensions
369/// - `Range`: For numbers, enforce min/max bounds
370///
371/// # Serialization
372///
373/// Rules use untagged enum serialization:
374///
375/// ```yaml
376/// # MustExist
377/// - must_exist: true
378///
379/// # Extensions
380/// - extensions: [yaml, yml, json]
381///
382/// # Range
383/// - min: 0.0
384///   max: 100.0
385/// ```
386#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
387#[serde(untagged)]
388pub enum ValidationRule {
389    /// Require that a path exists on the file system
390    MustExist { must_exist: bool },
391
392    /// Restrict file extensions (for path arguments)
393    ///
394    /// Extensions should be specified without the leading dot.
395    /// Example: `["yaml", "yml"]` matches "config.yaml" and "data.yml"
396    Extensions { extensions: Vec<String> },
397
398    /// Enforce numeric range constraints
399    ///
400    /// Either or both bounds can be specified:
401    /// - `min: Some(0.0), max: None` → x ≥ 0
402    /// - `min: None, max: Some(100.0)` → x ≤ 100
403    /// - `min: Some(0.0), max: Some(100.0)` → 0 ≤ x ≤ 100
404    Range { min: Option<f64>, max: Option<f64> },
405}
406
407impl CommandsConfig {
408    /// Create a minimal valid configuration for testing
409    ///
410    /// This is useful for unit tests and examples.
411    ///
412    /// # Example
413    ///
414    /// ```
415    /// use dynamic_cli::config::schema::CommandsConfig;
416    ///
417    /// let config = CommandsConfig::minimal();
418    /// assert_eq!(config.metadata.version, "0.1.0");
419    /// assert!(config.commands.is_empty());
420    /// ```
421    #[cfg(test)]
422    pub fn minimal() -> Self {
423        Self {
424            metadata: Metadata {
425                version: "0.1.0".to_string(),
426                prompt: "test".to_string(),
427                prompt_suffix: " > ".to_string(),
428            },
429            commands: vec![],
430            global_options: vec![],
431        }
432    }
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438
439    #[test]
440    fn test_argument_type_as_str() {
441        assert_eq!(ArgumentType::String.as_str(), "string");
442        assert_eq!(ArgumentType::Integer.as_str(), "integer");
443        assert_eq!(ArgumentType::Float.as_str(), "float");
444        assert_eq!(ArgumentType::Bool.as_str(), "bool");
445        assert_eq!(ArgumentType::Path.as_str(), "path");
446    }
447
448    #[test]
449    fn test_default_prompt_suffix() {
450        assert_eq!(default_prompt_suffix(), " > ");
451    }
452
453    #[test]
454    fn test_minimal_config() {
455        let config = CommandsConfig::minimal();
456
457        assert_eq!(config.metadata.version, "0.1.0");
458        assert_eq!(config.metadata.prompt, "test");
459        assert_eq!(config.metadata.prompt_suffix, " > ");
460        assert!(config.commands.is_empty());
461        assert!(config.global_options.is_empty());
462    }
463
464    #[test]
465    fn test_deserialize_argument_type() {
466        // Test YAML deserialization of ArgumentType
467        let yaml = r#"
468            type: string
469        "#;
470
471        #[derive(Deserialize)]
472        struct TestStruct {
473            #[serde(rename = "type")]
474            type_field: ArgumentType,
475        }
476
477        let result: TestStruct = serde_yaml::from_str(yaml).unwrap();
478        assert_eq!(result.type_field, ArgumentType::String);
479    }
480
481    #[test]
482    fn test_deserialize_metadata() {
483        let yaml = r#"
484            version: "1.0.0"
485            prompt: "myapp"
486            prompt_suffix: " $ "
487        "#;
488
489        let metadata: Metadata = serde_yaml::from_str(yaml).unwrap();
490
491        assert_eq!(metadata.version, "1.0.0");
492        assert_eq!(metadata.prompt, "myapp");
493        assert_eq!(metadata.prompt_suffix, " $ ");
494    }
495
496    #[test]
497    fn test_deserialize_metadata_with_default() {
498        // Test that prompt_suffix gets default value if not specified
499        let yaml = r#"
500            version: "1.0.0"
501            prompt: "myapp"
502        "#;
503
504        let metadata: Metadata = serde_yaml::from_str(yaml).unwrap();
505
506        assert_eq!(metadata.prompt_suffix, " > ");
507    }
508
509    #[test]
510    fn test_deserialize_command_definition() {
511        let yaml = r#"
512            name: test_cmd
513            aliases: [tc, test]
514            description: "A test command"
515            required: true
516            arguments: []
517            options: []
518            implementation: "test_handler"
519        "#;
520
521        let cmd: CommandDefinition = serde_yaml::from_str(yaml).unwrap();
522
523        assert_eq!(cmd.name, "test_cmd");
524        assert_eq!(cmd.aliases, vec!["tc", "test"]);
525        assert_eq!(cmd.description, "A test command");
526        assert!(cmd.required);
527        assert_eq!(cmd.implementation, "test_handler");
528        // continue_on_failure / requires_success absent from YAML: both
529        // must default to false (DD-026, #53).
530        assert!(!cmd.continue_on_failure);
531        assert!(!cmd.requires_success);
532    }
533
534    #[test]
535    fn test_deserialize_command_definition_continue_on_failure_and_requires_success_default_false()
536    {
537        // Explicit companion to test_deserialize_command_definition,
538        // mirroring the secure-field default test below: confirms serde's
539        // #[serde(default)] applies even when the fields are wholly absent
540        // from an otherwise-complete config, not just incidentally true in
541        // the general fixture above.
542        let yaml = r#"
543            name: solve
544            description: "Run the solver"
545            implementation: "solve_handler"
546        "#;
547
548        let cmd: CommandDefinition = serde_yaml::from_str(yaml).unwrap();
549
550        assert!(!cmd.continue_on_failure);
551        assert!(!cmd.requires_success);
552    }
553
554    #[test]
555    fn test_deserialize_command_definition_continue_on_failure_and_requires_success_explicit() {
556        let yaml = r#"
557            name: export
558            description: "Optional secondary export"
559            implementation: "export_handler"
560            continue_on_failure: true
561            requires_success: true
562        "#;
563
564        let cmd: CommandDefinition = serde_yaml::from_str(yaml).unwrap();
565
566        assert!(cmd.continue_on_failure);
567        assert!(cmd.requires_success);
568    }
569
570    #[test]
571    fn test_command_definition_requires_success_distinct_from_required() {
572        // Guards against the two fields being confused with one another
573        // (DD-026): `required` is the pre-existing "handler must be
574        // registered at startup" check; `requires_success` is the new,
575        // unrelated per-invocation chain guard.
576        let yaml = r#"
577            name: cleanup
578            description: "Cleanup step"
579            required: true
580            implementation: "cleanup_handler"
581            requires_success: false
582        "#;
583
584        let cmd: CommandDefinition = serde_yaml::from_str(yaml).unwrap();
585
586        assert!(cmd.required);
587        assert!(!cmd.requires_success);
588    }
589
590    #[test]
591    fn test_deserialize_argument_definition() {
592        let yaml = r#"
593            name: input_file
594            arg_type: path
595            required: true
596            description: "Input file"
597            validation:
598              - must_exist: true
599              - extensions: [yaml, yml]
600        "#;
601
602        let arg: ArgumentDefinition = serde_yaml::from_str(yaml).unwrap();
603
604        assert_eq!(arg.name, "input_file");
605        assert_eq!(arg.arg_type, ArgumentType::Path);
606        assert!(arg.required);
607        assert_eq!(arg.description, "Input file");
608        assert_eq!(arg.validation.len(), 2);
609        // secure defaults to false when absent from YAML
610        assert!(!arg.secure);
611    }
612
613    #[test]
614    fn test_deserialize_argument_definition_secure_default_false() {
615        // When `secure` is absent from the YAML, serde must default to false.
616        let yaml = r#"
617            name: username
618            arg_type: string
619            required: true
620            description: "User name"
621        "#;
622
623        let arg: ArgumentDefinition = serde_yaml::from_str(yaml).unwrap();
624        assert!(!arg.secure, "secure must default to false when absent");
625    }
626
627    #[test]
628    fn test_deserialize_argument_definition_secure_true() {
629        // When `secure: true` is present, it must be deserialised correctly.
630        let yaml = r#"
631            name: password
632            arg_type: string
633            required: true
634            description: "User password"
635            secure: true
636        "#;
637
638        let arg: ArgumentDefinition = serde_yaml::from_str(yaml).unwrap();
639        assert!(arg.secure, "secure must be true when set in YAML");
640    }
641
642    #[test]
643    fn test_deserialize_argument_definition_secure_false_explicit() {
644        // Explicit `secure: false` must round-trip correctly.
645        let yaml = r#"
646            name: output
647            arg_type: path
648            required: false
649            description: "Output path"
650            secure: false
651        "#;
652
653        let arg: ArgumentDefinition = serde_yaml::from_str(yaml).unwrap();
654        assert!(!arg.secure);
655    }
656
657    #[test]
658    fn test_serialize_argument_definition_secure_roundtrip() {
659        let original = ArgumentDefinition {
660            name: "secret".to_string(),
661            arg_type: ArgumentType::String,
662            required: true,
663            description: "A secret value".to_string(),
664            validation: vec![],
665            secure: true,
666        };
667
668        let yaml = serde_yaml::to_string(&original).unwrap();
669        let deserialized: ArgumentDefinition = serde_yaml::from_str(&yaml).unwrap();
670
671        assert_eq!(original, deserialized);
672        assert!(deserialized.secure);
673    }
674
675    #[test]
676    fn test_deserialize_option_definition() {
677        let yaml = r#"
678            name: output
679            short: o
680            long: output
681            option_type: path
682            required: false
683            default: "out.txt"
684            description: "Output file"
685            choices: []
686        "#;
687
688        let opt: OptionDefinition = serde_yaml::from_str(yaml).unwrap();
689
690        assert_eq!(opt.name, "output");
691        assert_eq!(opt.short, Some("o".to_string()));
692        assert_eq!(opt.long, Some("output".to_string()));
693        assert_eq!(opt.option_type, ArgumentType::Path);
694        assert!(!opt.required);
695        assert_eq!(opt.default, Some("out.txt".to_string()));
696    }
697
698    #[test]
699    fn test_deserialize_validation_rule_must_exist() {
700        let yaml = r#"
701            must_exist: true
702        "#;
703
704        let rule: ValidationRule = serde_yaml::from_str(yaml).unwrap();
705
706        assert_eq!(rule, ValidationRule::MustExist { must_exist: true });
707    }
708
709    #[test]
710    fn test_deserialize_validation_rule_extensions() {
711        let yaml = r#"
712            extensions: [yaml, yml, json]
713        "#;
714
715        let rule: ValidationRule = serde_yaml::from_str(yaml).unwrap();
716
717        match rule {
718            ValidationRule::Extensions { extensions } => {
719                assert_eq!(extensions, vec!["yaml", "yml", "json"]);
720            }
721            _ => panic!("Wrong variant"),
722        }
723    }
724
725    #[test]
726    fn test_deserialize_validation_rule_range() {
727        let yaml = r#"
728            min: 0.0
729            max: 100.0
730        "#;
731
732        let rule: ValidationRule = serde_yaml::from_str(yaml).unwrap();
733
734        match rule {
735            ValidationRule::Range { min, max } => {
736                assert_eq!(min, Some(0.0));
737                assert_eq!(max, Some(100.0));
738            }
739            _ => panic!("Wrong variant"),
740        }
741    }
742
743    #[test]
744    fn test_deserialize_full_config() {
745        let yaml = r#"
746            metadata:
747              version: "1.0.0"
748              prompt: "test"
749              prompt_suffix: " > "
750            commands:
751              - name: hello
752                aliases: []
753                description: "Say hello"
754                required: false
755                arguments: []
756                options: []
757                implementation: "hello_handler"
758            global_options: []
759        "#;
760
761        let config: CommandsConfig = serde_yaml::from_str(yaml).unwrap();
762
763        assert_eq!(config.metadata.version, "1.0.0");
764        assert_eq!(config.commands.len(), 1);
765        assert_eq!(config.commands[0].name, "hello");
766    }
767
768    #[test]
769    fn test_serialize_and_deserialize_roundtrip() {
770        let original = CommandsConfig {
771            metadata: Metadata {
772                version: "1.0.0".to_string(),
773                prompt: "test".to_string(),
774                prompt_suffix: " > ".to_string(),
775            },
776            commands: vec![CommandDefinition {
777                name: "cmd1".to_string(),
778                aliases: vec!["c1".to_string()],
779                description: "Test command".to_string(),
780                required: true,
781                arguments: vec![],
782                options: vec![],
783                implementation: "handler1".to_string(),
784                continue_on_failure: false,
785                requires_success: false,
786            }],
787            global_options: vec![],
788        };
789
790        // Serialize to YAML
791        let yaml = serde_yaml::to_string(&original).unwrap();
792
793        // Deserialize back
794        let deserialized: CommandsConfig = serde_yaml::from_str(&yaml).unwrap();
795
796        assert_eq!(original, deserialized);
797    }
798
799    #[test]
800    fn test_json_deserialization() {
801        let json = r#"
802        {
803            "metadata": {
804                "version": "1.0.0",
805                "prompt": "test",
806                "prompt_suffix": " > "
807            },
808            "commands": [],
809            "global_options": []
810        }
811        "#;
812
813        let config: CommandsConfig = serde_json::from_str(json).unwrap();
814
815        assert_eq!(config.metadata.version, "1.0.0");
816        assert_eq!(config.commands.len(), 0);
817    }
818}