Skip to main content

cli_engine/
config_commands.rs

1//! Built-in `config` command group for reading and writing the per-application
2//! [config file](crate::config::ConfigFile).
3//!
4//! Mount it on a CLI with
5//! [`CliConfig::with_config_commands`](crate::cli::CliConfig::with_config_commands).
6//! The group exposes:
7//!
8//! - `config path` — print the resolved config file path.
9//! - `config get <key>` — print the value at a dotted key (e.g. `deploy.region`).
10//! - `config set <key> <value>` — set a value and persist (mutating; dry-run aware).
11//! - `config list` — print the full config file contents.
12
13use clap::Arg;
14use serde_json::{Value, json};
15
16use crate::config::{ConfigFile, config_file_path};
17use crate::{CommandResult, CommandSpec, GroupSpec, RuntimeCommandSpec, RuntimeGroupSpec, Tier};
18
19/// Builds the built-in runtime `config` command group.
20#[must_use]
21pub fn config_command_group() -> RuntimeGroupSpec {
22    RuntimeGroupSpec::new(GroupSpec::new(
23        "config",
24        "Read and write the CLI config file",
25    ))
26    .with_command(RuntimeCommandSpec::new_with_context(
27        CommandSpec::new("path", "Print the config file path")
28            .with_system("config")
29            .no_auth(true),
30        async |context| {
31            let path =
32                config_file_path(&context.middleware.app_id).map(|p| p.display().to_string());
33            Ok(CommandResult::new(json!({ "path": path })))
34        },
35    ))
36    .with_command(RuntimeCommandSpec::new_with_context(
37        CommandSpec::new("get", "Print a config value by dotted key")
38            .with_system("config")
39            .no_auth(true)
40            .with_arg(
41                Arg::new("key")
42                    .value_name("KEY")
43                    .required(true)
44                    .help("Dotted key, e.g. credentials.store or deploy.region"),
45            ),
46        async |context| {
47            let key = string_arg(&context.args, "key");
48            let value = context.config().get(&key);
49            Ok(CommandResult::new(json!({ "key": key, "value": value })))
50        },
51    ))
52    .with_command(RuntimeCommandSpec::new_with_context(
53        CommandSpec::new("set", "Set a config value and save")
54            .with_system("config")
55            .with_tier(Tier::Mutate)
56            .mutates(true)
57            .no_auth(true)
58            .with_arg(
59                Arg::new("key")
60                    .value_name("KEY")
61                    .required(true)
62                    .help("Dotted key, e.g. credentials.store or deploy.region"),
63            )
64            .with_arg(Arg::new("value").value_name("VALUE").required(true).help(
65                "Value to set. Type is inferred: \"true\"/\"false\" → bool, \
66                         digits → int, float syntax (\"1.5\", \"1e5\") → float, \
67                         everything else → string.",
68            )),
69        async |context| {
70            let key = string_arg(&context.args, "key");
71            let value = string_arg(&context.args, "value");
72            // Load fresh from disk (not the startup snapshot) to avoid
73            // clobbering a concurrent external edit. The startup snapshot in
74            // `context.config()` is NOT updated by this write; a subsequent
75            // `config get` in the same process will still read the old value
76            // until the CLI is restarted.
77            let mut config = ConfigFile::load(&context.middleware.app_id);
78            config.set(&key, &value)?;
79            config.save()?;
80            let path = config.path().map(|p| p.display().to_string());
81            Ok(CommandResult::new(
82                json!({ "key": key, "value": value, "path": path }),
83            ))
84        },
85    ))
86    .with_command(RuntimeCommandSpec::new_with_context(
87        CommandSpec::new("list", "Print the full config file contents")
88            .with_system("config")
89            .no_auth(true),
90        async |context| {
91            let path = context.config().path().map(|p| p.display().to_string());
92            Ok(CommandResult::new(json!({
93                "path": path,
94                "contents": context.config().to_toml_string(),
95            })))
96        },
97    ))
98}
99
100/// Reads a required string argument, defaulting to empty when absent.
101fn string_arg(args: &serde_json::Map<String, Value>, name: &str) -> String {
102    args.get(name)
103        .and_then(Value::as_str)
104        .unwrap_or_default()
105        .to_owned()
106}