diff --git a/CHANGELOG.md b/CHANGELOG.md
index a595db7..9e52b20 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -29,6 +29,12 @@ that may never merge. They are not releases and are not listed here.
set in. One setting today: `update-check`, which `mapbox config set
update-check off` turns off for good, mirroring `MAPBOX_NO_UPDATE_CHECK`.
+- `mapbox config list`/`unset`, alongside `get`/`set`. `list` reports every
+ setting in one call rather than one key at a time; `unset` clears a
+ setting back to "never set" rather than writing its current default value
+ explicitly — the difference that lets a later default change reach a
+ cleared key but not one a caller pinned to the old default on purpose.
+
- The README now documents installing without the install script: the
archives are plain HTTP downloads, `manifest.json` lists every target with
its checksum, and the commands to verify and extract one are written out.
diff --git a/docs/commands.md b/docs/commands.md
index d20a63d..972f0c5 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -61,7 +61,8 @@ nests, and is typed `mapbox styles draft get`.
**[Uninstall](#uninstall)** — [uninstall](#mapbox-uninstall)
**[Config](#config)** — [config.get](#mapbox-config-get) ·
-[config.set](#mapbox-config-set)
+[config.set](#mapbox-config-set) · [config.list](#mapbox-config-list) ·
+[config.unset](#mapbox-config-unset)
**[Usage](#usage)** — [usage](#mapbox-usage)
@@ -3196,6 +3197,87 @@ update-check set to off.
+### `mapbox config list`
+
+Lists every setting and its current value — `get` answers one key at a
+time, this answers all of them in one call, falling back to each one's
+default the same way `get` does.
+
+#### Parameters
+
+None.
+
+#### Examples
+
+```sh
+mapbox config list
+```
+
+#### Outputs
+
+
+text | json |
+|
+
+```
+update-check on
+```
+
+ |
+
+```json
+[
+ {
+ "key": "update-check",
+ "value": true
+ }
+]
+```
+
+ |
+
+
+### `mapbox config unset`
+
+Clears a setting back to its default, rather than setting it to that
+default value explicitly. The difference matters the next time this CLI
+changes what a setting's default is: a cleared key picks up the new
+default, a key explicitly set to the old default value does not.
+
+#### Parameters
+
+| Parameter | Effect |
+| --- | --- |
+| `` | Which setting to clear. Only `update-check` exists today. |
+
+#### Examples
+
+```sh
+mapbox config unset update-check
+```
+
+#### Outputs
+
+
+text | json |
+|
+
+```
+update-check cleared, now on (default).
+```
+
+ |
+
+```json
+{
+ "key": "update-check",
+ "value": true
+}
+```
+
+ |
+
+
---
## Usage
diff --git a/src/config.rs b/src/config.rs
index 0a4a5f6..af955ea 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -6,9 +6,13 @@
//! file beside the credentials, written through the same
//! [`crate::auth::write_private`] so it gets the same `0600` treatment.
//!
-//! One setting today — `update-check` — with room for more: `get`/`set` take
-//! a `key`, restricted by clap to [`KEYS`], so adding a second setting is a
-//! new key and a new match arm rather than a new pair of subcommands.
+//! One setting today — `update-check` — with room for more: `get`/`set`/
+//! `unset` take a `key`, restricted by clap to [`KEYS`], so adding a second
+//! setting is a new key and a new match arm rather than a new subcommand.
+//! `list` needs no key at all: it walks [`KEYS`] and reports every setting's
+//! current value in one call, which `get` cannot — the whole reason it
+//! exists alongside `get`/`set` rather than waiting for a second setting to
+//! make the gap visible.
use std::path::PathBuf;
@@ -16,7 +20,7 @@ use anyhow::{Context, Result};
use clap::builder::PossibleValuesParser;
use clap::{Arg, ArgMatches, Command};
use serde::{Deserialize, Serialize};
-use serde_json::json;
+use serde_json::{json, Value};
use crate::auth;
use crate::output::{self, Mode};
@@ -87,6 +91,28 @@ fn on_off(enabled: bool) -> &'static str {
}
}
+/// `key`'s current value in `config`, resolved to its default the same way
+/// every getter here does. Shared by [`get`] and [`list`] so the two cannot
+/// answer a key differently.
+fn resolve(config: &Config, key: &str) -> bool {
+ match key {
+ UPDATE_CHECK_KEY => update_check_setting(config),
+ _ => unreachable!("clap's value_parser restricts `key` to {KEYS:?}"),
+ }
+}
+
+/// Clears `key` back to "never set" in `config`, in place. The counterpart
+/// to `set`'s `Some(enabled)` — distinct from setting a key to its default
+/// value, which [`resolve`] would read identically but which
+/// `serde(skip_serializing_if)` would not write identically: a later default
+/// change reaches only a key that was actually cleared.
+fn clear(config: &mut Config, key: &str) {
+ match key {
+ UPDATE_CHECK_KEY => config.update_check = None,
+ _ => unreachable!("clap's value_parser restricts `key` to {KEYS:?}"),
+ }
+}
+
pub fn command() -> Command {
let key_arg = || {
Arg::new("key")
@@ -113,23 +139,30 @@ pub fn command() -> Command {
.arg(key_arg())
.arg(Arg::new("value").required(true).value_parser([ON, OFF])),
)
+ .subcommand(Command::new("list").about("List every setting and its current value"))
+ .subcommand(
+ Command::new("unset")
+ .about("Clear a setting back to its default")
+ .long_about(
+ "Clear a setting back to its default, rather than setting it to that \
+ default value explicitly — the difference matters the next time this \
+ CLI changes what the default is: a cleared key picks up the new default, \
+ a key explicitly set to the old default value does not.",
+ )
+ .arg(key_arg()),
+ )
}
pub fn get(matches: &ArgMatches, mode: Mode) -> Result<()> {
let key = matches.get_one::("key").expect("required");
let config = read_config();
+ let enabled = resolve(&config, key);
- match key.as_str() {
- UPDATE_CHECK_KEY => {
- let enabled = update_check_setting(&config);
- output::emit(
- mode,
- on_off(enabled),
- json!({ "key": key, "value": enabled }),
- )
- }
- _ => unreachable!("clap's value_parser restricts `key` to {KEYS:?}"),
- }
+ output::emit(
+ mode,
+ on_off(enabled),
+ json!({ "key": key, "value": enabled }),
+ )
}
pub fn set(matches: &ArgMatches, mode: Mode) -> Result<()> {
@@ -151,6 +184,37 @@ pub fn set(matches: &ArgMatches, mode: Mode) -> Result<()> {
)
}
+pub fn list(mode: Mode) -> Result<()> {
+ let config = read_config();
+ let entries: Vec = KEYS
+ .iter()
+ .map(|key| json!({ "key": key, "value": resolve(&config, key) }))
+ .collect();
+
+ let text = KEYS
+ .iter()
+ .map(|key| format!("{key}\t{}", on_off(resolve(&config, key))))
+ .collect::>()
+ .join("\n");
+
+ output::emit(mode, &text, Value::Array(entries))
+}
+
+pub fn unset(matches: &ArgMatches, mode: Mode) -> Result<()> {
+ let key = matches.get_one::("key").expect("required");
+
+ let mut config = read_config();
+ clear(&mut config, key);
+ write_config(&config)?;
+
+ let enabled = resolve(&config, key);
+ output::emit(
+ mode,
+ &format!("{key} cleared, now {} (default).", on_off(enabled)),
+ json!({ "key": key, "value": enabled }),
+ )
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -181,4 +245,35 @@ mod tests {
assert_eq!(on_off(true), ON);
assert_eq!(on_off(false), OFF);
}
+
+ #[test]
+ fn resolve_matches_update_check_setting_at_every_state() {
+ for update_check in [None, Some(true), Some(false)] {
+ let config = Config { update_check };
+ assert_eq!(
+ resolve(&config, UPDATE_CHECK_KEY),
+ update_check_setting(&config)
+ );
+ }
+ }
+
+ /// `clear` and `set` leave different bytes on disk even when they leave
+ /// the same *value*: this is the difference `unset` exists to offer, so
+ /// it is worth pinning rather than only exercising through `resolve`,
+ /// which cannot tell the two states apart by design.
+ #[test]
+ fn clear_removes_the_key_rather_than_writing_the_default() {
+ let mut explicit_default = Config {
+ update_check: Some(true),
+ };
+ clear(&mut explicit_default, UPDATE_CHECK_KEY);
+ assert_eq!(explicit_default, Config::default());
+ assert_eq!(explicit_default.update_check, None);
+
+ let mut explicit_off = Config {
+ update_check: Some(false),
+ };
+ clear(&mut explicit_off, UPDATE_CHECK_KEY);
+ assert_eq!(explicit_off.update_check, None);
+ }
}
diff --git a/src/main.rs b/src/main.rs
index 035dcea..a3a7961 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1052,6 +1052,8 @@ fn run(app: &Command, specs: &[ServiceSpec], matches: &ArgMatches, mode: Mode) -
Some((config::COMMAND, config_matches)) => match config_matches.subcommand() {
Some(("get", get_matches)) => config::get(get_matches, mode)?,
Some(("set", set_matches)) => config::set(set_matches, mode)?,
+ Some(("list", _)) => config::list(mode)?,
+ Some(("unset", unset_matches)) => config::unset(unset_matches, mode)?,
_ => unreachable!("`config` sets subcommand_required(true)"),
},
// Token resolution mirrors the service arm below, minus path
diff --git a/tests/config.rs b/tests/config.rs
index 36dc19f..c6e2558 100644
--- a/tests/config.rs
+++ b/tests/config.rs
@@ -123,6 +123,70 @@ fn an_unknown_key_or_value_is_a_usage_error_not_a_panic() {
);
}
+#[test]
+fn list_reports_every_setting_including_an_unset_one() {
+ let home = scratch("list");
+
+ // Nothing set yet: list still names the one known key, at its default.
+ let empty = command(&home)
+ .args(["-o", "json", "config", "list"])
+ .output()
+ .expect("run mapbox config list");
+ assert!(empty.status.success());
+ assert_eq!(stdout(&empty), r#"[{"key":"update-check","value":true}]"#);
+
+ let set = command(&home)
+ .args(["config", "set", "update-check", "off"])
+ .output()
+ .expect("run mapbox config set");
+ assert!(set.status.success());
+
+ let after = command(&home)
+ .args(["-o", "json", "config", "list"])
+ .output()
+ .expect("run mapbox config list");
+ assert!(after.status.success());
+ assert_eq!(stdout(&after), r#"[{"key":"update-check","value":false}]"#);
+
+ let text = command(&home)
+ .args(["-o", "text", "config", "list"])
+ .output()
+ .expect("run mapbox config list");
+ assert!(text.status.success());
+ assert_eq!(stdout(&text), "update-check\toff");
+}
+
+#[test]
+fn unset_clears_the_key_rather_than_writing_the_default() {
+ let home = scratch("unset");
+ let path = config_dir(&home).join("config.json");
+
+ // Set to `on` explicitly — the default value, but written, not absent.
+ let set = command(&home)
+ .args(["config", "set", "update-check", "on"])
+ .output()
+ .expect("run mapbox config set");
+ assert!(set.status.success());
+ let written = std::fs::read_to_string(&path).expect("config.json exists");
+ assert!(
+ written.contains("update_check"),
+ "an explicit `on` should still be written: {written:?}"
+ );
+
+ let unset = command(&home)
+ .args(["-o", "json", "config", "unset", "update-check"])
+ .output()
+ .expect("run mapbox config unset");
+ assert!(unset.status.success());
+ assert_eq!(stdout(&unset), r#"{"key":"update-check","value":true}"#);
+
+ let cleared = std::fs::read_to_string(&path).expect("config.json still exists");
+ assert_eq!(
+ cleared, "{}",
+ "unset should remove the key, not merely write back its default"
+ );
+}
+
#[cfg(unix)]
#[test]
fn the_config_file_is_written_private() {