diff --git a/CHANGELOG.md b/CHANGELOG.md index 33a4be7..4bcbbd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,23 @@ that may never merge. They are not releases and are not listed here. ## Unreleased +## 0.2.2 - 2026-09-15 + +### Changed + +- `mapbox usage` is no longer described as a private preview: the Statistics + API it calls is generally available, so `mapbox auth login` now requests + `statistics:read` unconditionally instead of through a feature flag. + Nothing about who can run the command or what it prints changed — the flag + it used to go through was already on for everyone. + +- The 403 a missing `statistics:read` scope gets back now leads with "run + `mapbox auth login` again", the fix that actually applies, before falling + back to "contact Mapbox support" for the rarer case of an account with no + access at all. It used to jump straight to support, which was written for + the old private-preview gate and never distinguished the two — the API + answers both with 403, not the 401/403 split the docs previously assumed. + ## 0.2.1 - 2026-09-15 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 8407554..ce69239 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -751,7 +751,7 @@ checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" [[package]] name = "mapbox-cli" -version = "0.2.1" +version = "0.2.2" dependencies = [ "anyhow", "base64", diff --git a/Cargo.toml b/Cargo.toml index c7b78d2..7c54919 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mapbox-cli" -version = "0.2.1" +version = "0.2.2" edition = "2021" description = "A command-line interface for Mapbox APIs, with commands generated at build time from OpenAPI specs." repository = "https://github.com/mapbox/cli" diff --git a/README.md b/README.md index 56e42b5..fe18867 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,6 @@ curl -fsSL https://cli.mapbox.com/install.sh | sh irm https://cli.mapbox.com/install.ps1 | iex ``` -That channel is not serving yet. Until it is, build from source above. - `scripts/install.sh` and `scripts/install.ps1` here are those installers' sources; `scripts/test-install.sh` and `scripts/test-install.ps1` exercise them end to end without touching the network. diff --git a/custom-openapi/statistics/openapi/statistics.yaml b/custom-openapi/statistics/openapi/statistics.yaml index a3d7e99..c98bbed 100644 --- a/custom-openapi/statistics/openapi/statistics.yaml +++ b/custom-openapi/statistics/openapi/statistics.yaml @@ -4,14 +4,13 @@ # generically. openapi: "3.0.0" info: - title: "Mapbox Statistics API (private preview)" + title: "Mapbox Statistics API" description: >- The one endpoint `mapbox usage` calls: usage per Mapbox product, by day, - for the whole account or for a single token. A private preview gated two - ways — the account has to be enabled for it by Mapbox support, and the - token needs the `statistics:read` scope. Written by hand rather than trimmed - from an upstream spec because openapi-specs publishes none for this API; - the parameter names are the ones the API answers to, as documented in + for the whole account or for a single token. Gated by the token's + `statistics:read` scope. Written by hand rather than trimmed from an + upstream spec because openapi-specs publishes none for this API; the + parameter names are the ones the API answers to, as documented in `src/account_usage.rs`. version: "0.0.0" servers: @@ -63,9 +62,12 @@ paths: "200": description: Usage per product, per day, over the period. "401": - description: Unauthorized — the token does not carry the `statistics:read` scope. + description: Unauthorized — the token is missing or invalid. "403": - description: Forbidden — the account is not enabled for the private preview. + description: >- + Forbidden — the token doesn't carry the `statistics:read` scope, or (less + commonly) it does and the account itself doesn't have access to the Statistics + API. "422": description: Unprocessable — the period is malformed, backwards, or longer than 31 days. "429": diff --git a/docs/commands.md b/docs/commands.md index fa9a534..cd536e2 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -3158,17 +3158,11 @@ Removed /home/user/.local/bin/mapbox. ### `mapbox usage` Usage per Mapbox product, by day, for the account or one token. Calls the -Statistics API (`GET /statistics/v1`), a private preview gated two ways -this CLI cannot get around: the account has to be enabled for it by Mapbox -support (a 403 here means it isn't), and the token needs the -`statistics:read` scope. `mapbox auth login` requests it by default now; -log in again if your stored token predates that. Also takes -`--token-id ` for one token's usage instead of the whole account — -see `mapbox accounts list-tokens` for ids. - -Gated behind a feature flag, flipped on: an official release ships this -command the same as a build from source does. See `feature_flags` for what -the gate is for. +Statistics API (`GET /statistics/v1`), which needs the `statistics:read` +scope. `mapbox auth login` requests it by default now; log in again if +your stored token predates that. Also takes `--token-id ` for one +token's usage instead of the whole account — see `mapbox accounts +list-tokens` for ids. #### Parameters @@ -3193,8 +3187,7 @@ mapbox usage --product "Vector Tiles API" --daily #### Outputs -Run live 2026-09-08 against an account enabled for the private preview. -Numbers below are made up — the real response carries actual traffic +Run live. Numbers below are made up — the real response carries actual traffic figures, which do not belong in a page committed to the repo — but the shape, including the sort order (busiest product first), the sparkline, and every line `-o text` prints around the table, is exactly what came back. @@ -3260,9 +3253,12 @@ without it, a sparkline's solid glyphs sitting flush against the next row's read as cramped rather than dense, on an account with more than a couple of products. Exact per-day numbers and the per-browser/country/host breakdown are left to `-o json`; `--product` narrows the whole response, -both columns, to one product's row. A 403 answers `Statistics API feature -is not enabled for this account`; a 401 means the token's missing -`statistics:read` — a login from before the scope was added, most likely. +both columns, to one product's row. A 403 covers two different causes the +API doesn't otherwise distinguish: the token missing `statistics:read` — a +login from before the scope was added, most likely, and fixed by logging in +again — or, less commonly, an account with no access to the Statistics API +at all, which needs Mapbox support. A 401 means the token itself is missing +or invalid. `--daily` swaps every product's sparkline row for its own day-by-day listing — same total, same period, newest day first, no `DAILY TREND` diff --git a/src/account_usage.rs b/src/account_usage.rs index d5d308b..461c3b9 100644 --- a/src/account_usage.rs +++ b/src/account_usage.rs @@ -1,15 +1,9 @@ //! `mapbox usage` — account/token usage by product and day. //! -//! Calls the Statistics API (`GET /statistics/v1`), a private preview: -//! the account needs Mapbox support to enable it (403 otherwise), and the -//! token needs the `statistics:read` scope. `mapbox auth login` requests it -//! by default now that [`crate::feature_flags::flags::ACCOUNT_USAGE`] is on -//! (see `auth::requested_scopes`); a token from before that flip won't -//! carry it until logged in again. -//! -//! Gated by [`crate::feature_flags::flags::ACCOUNT_USAGE`]; see that module. -//! The gate stays while the API itself is a preview: the switch is what lets -//! an official binary stop shipping the command if the preview is withdrawn. +//! Calls the Statistics API (`GET /statistics/v1`); the token needs the +//! `statistics:read` scope. `mapbox auth login` requests it by default; a +//! token from before that scope was added won't carry it until logged in +//! again. use std::sync::OnceLock; use std::time::Duration; @@ -62,7 +56,7 @@ const DAILY_ARG: &str = "daily"; /// this crate, not a state a shipped binary can drift into; the pinning test /// below calls this function, so CI fails first; and nothing else — not /// `--help`, not `--schema`, not startup — calls it, so a broken spec can -/// only ever break `usage` itself, and only behind its feature flag. +/// only ever break `usage` itself. fn operation() -> &'static Operation { static PARSED: OnceLock = OnceLock::new(); PARSED.get_or_init(|| { @@ -79,13 +73,12 @@ fn operation() -> &'static Operation { pub fn command() -> Command { Command::new(COMMAND) - .about("Show account/token usage by product and day (Statistics API, private preview)") + .about("Show account/token usage by product and day (Statistics API)") .long_about(format!( "Show usage per Mapbox product, by day, for the account or one token.\n\n\ - Calls the Statistics API, a private preview gated two ways: the account has to \ - be enabled for it by Mapbox support first — a 403 here means it isn't — and the \ - token needs the `statistics:read` scope. `mapbox auth login` requests it by \ - default; log in again if your stored token predates that.\n\n\ + Calls the Statistics API; the token needs the `statistics:read` scope. \ + `mapbox auth login` requests it by default; log in again if your stored \ + token predates that.\n\n\ See {REPO_URL}/issues." )) .arg( @@ -655,12 +648,13 @@ fn redacted_url(url: &str, query: &[(String, String)]) -> String { fn remedy_for(status: u16) -> Remedy { match status { 401 => Remedy::default().with_fix( - "Check the token has the `statistics:read` scope — run `mapbox auth login` again \ - if it predates that scope, or pass one from account.mapbox.com with --token.", + "The token is missing or invalid — run `mapbox auth login` again, or pass one \ + from account.mapbox.com with --token.", ), 403 => Remedy::default().with_fix( - "The Statistics API is a private preview: ask Mapbox support to enable it for \ - this account before this can return anything.", + "Check the token has the `statistics:read` scope — run `mapbox auth login` again \ + if it predates that scope. If it already has the scope, this account doesn't \ + have access to the Statistics API; contact Mapbox support.", ), 422 => Remedy::default().with_fix( "--period-start/--period-end take YYYY-MM-DD, the end can't be before the start, \ @@ -1285,11 +1279,16 @@ mod tests { } } + /// The API answers 403, not 401, when the token itself is fine but is + /// missing `statistics:read` — confirmed live against a real account. A + /// re-login fixes that case, so the fix has to lead with it rather than + /// jump straight to Mapbox support, which is only the answer once the + /// scope is already there. #[test] - fn a_403_carries_the_private_preview_explanation() { + fn a_403_checks_the_scope_before_pointing_at_mapbox_support() { let (server, base_url) = serve_once( "403 Forbidden", - r#"{"message":"Statistics API feature is not enabled for this account"}"#, + r#"{"message":"This API requires a token with statistics:read scope."}"#, ); let matches = command().get_matches_from(["usage"]); @@ -1301,22 +1300,21 @@ mod tests { .downcast::() .expect("an HTTP failure is a CliError"); assert_eq!(cli.status, Some(403)); - assert_eq!( - cli.message, - "Statistics API feature is not enabled for this account" - ); + let fix = cli.fix.as_deref().unwrap_or_default(); + assert!(fix.contains("statistics:read"), "{fix:?}"); + assert!(fix.contains("Mapbox support"), "{fix:?}"); assert!( - cli.fix - .as_deref() - .unwrap_or_default() - .contains("Mapbox support"), - "{:?}", - cli.fix + fix.find("statistics:read").unwrap() < fix.find("Mapbox support").unwrap(), + "the self-serviceable fix should come before the support fallback: {fix:?}" ); } + /// A 401 here means the token itself is missing or invalid — not a scope + /// problem, which this endpoint answers with 403 instead. Naming the + /// scope on a 401 would send the reader chasing something a bad token + /// can't have anyway. #[test] - fn a_401_points_at_the_scope_rather_than_just_login() { + fn a_401_says_the_token_is_invalid_rather_than_naming_a_scope() { let (server, base_url) = serve_once( "401 Unauthorized", r#"{"message":"Not Authorized - Invalid Token"}"#, @@ -1330,13 +1328,8 @@ mod tests { let cli = err .downcast::() .expect("an HTTP failure is a CliError"); - assert!( - cli.fix - .as_deref() - .unwrap_or_default() - .contains("statistics:read"), - "{:?}", - cli.fix - ); + let fix = cli.fix.as_deref().unwrap_or_default(); + assert!(fix.contains("auth login"), "{fix:?}"); + assert!(!fix.contains("statistics:read"), "{fix:?}"); } } diff --git a/src/auth.rs b/src/auth.rs index 164dfe4..fd577b7 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -5,6 +5,7 @@ use sha2::{Digest, Sha256}; use std::io::{IsTerminal, Read, Write}; use std::net::TcpListener; use std::path::{Path, PathBuf}; +use std::sync::OnceLock; use std::time::{Duration, Instant}; use crate::output::{self, CliError, Mode}; @@ -62,38 +63,34 @@ const VALIDATION_ENDPOINT: &str = "https://api.mapbox.com/tokens/v2"; // succeeded, so it ships alongside the other three rather than staying in // `spec::WITHHELD_OPERATIONS`. Anyone logged in before this lands needs // `mapbox auth login` again, for the same reason `scopes:list` above does. -const DEFAULT_SCOPES: &str = "styles:tiles styles:read styles:write styles:list fonts:read fonts:list fonts:write datasets:read datasets:write tokens:read scopes:list tilesets:read tilesets:write tilesets:list user-feedback:read"; - -/// [`DEFAULT_SCOPES`] plus every enabled flag's `oauth_scopes` — what -/// `mapbox auth login` actually requests. -/// -/// `statistics:read` (`ACCOUNT_USAGE`'s scope, now on) has **not** been -/// confirmed against the Accounts API's registration allowlist, unlike -/// everything in `DEFAULT_SCOPES`. If DCR drops it silently (as it does for -/// `tokens:write` and others in `spec::UNSUPPORTED_OPERATIONS`), a login -/// done with the flag on would still leave `mapbox usage` unable to -/// authenticate. Confirm this against a real `mapbox auth login` before -/// this reaches a staging or production release. -fn requested_scopes() -> String { - let gated: Vec<&'static [&'static str]> = crate::feature_flags::flags::ALL - .iter() - .filter(|flag| flag.is_enabled()) - .map(|flag| flag.oauth_scopes) - .collect(); - scopes_with(&gated) -} - -/// [`requested_scopes`], parameterised for testing — a build's own flags -/// can't be flipped from a test. -fn scopes_with(gated: &[&[&str]]) -> String { - let mut scopes = DEFAULT_SCOPES.to_string(); - for scopes_list in gated { - for scope in *scopes_list { - scopes.push(' '); - scopes.push_str(scope); - } - } - scopes +// `statistics:read` (needed by `mapbox usage`) registers fine — confirmed +// live against a real `mapbox auth login` — so it rides along unconditionally +// rather than through the now-removed `ACCOUNT_USAGE` flag. +const DEFAULT_SCOPES_LIST: &[&str] = &[ + "styles:tiles", + "styles:read", + "styles:write", + "styles:list", + "fonts:read", + "fonts:list", + "fonts:write", + "datasets:read", + "datasets:write", + "tokens:read", + "scopes:list", + "tilesets:read", + "tilesets:write", + "tilesets:list", + "user-feedback:read", + "statistics:read", +]; + +/// [`DEFAULT_SCOPES_LIST`], space-joined the way the OAuth `scope` parameter +/// takes it. Computed once and cached — every caller wants the joined form, +/// and none of it changes at runtime. +fn default_scopes() -> &'static str { + static JOINED: OnceLock = OnceLock::new(); + JOINED.get_or_init(|| DEFAULT_SCOPES_LIST.join(" ")) } #[derive(serde::Serialize, serde::Deserialize, Debug)] @@ -1223,7 +1220,7 @@ pub fn describe_plan(action: &str, profile: Option<&str>, mode: Mode) -> Result< "would_replace_existing": stored.is_some(), "authorization_endpoint": AUTHORIZATION_ENDPOINT, "token_endpoint": TOKEN_ENDPOINT, - "scopes": requested_scopes().split(' ').map(str::to_string).collect::>(), + "scopes": DEFAULT_SCOPES_LIST, }), ) } @@ -1760,10 +1757,10 @@ pub fn login(debug: bool, profile: Option<&str>, mode: Mode) -> Result<()> { let redirect_uri = format!("http://localhost:{}/callback", port); // Computed once: register_client's ceiling and the authorize scope must agree. - let scopes = requested_scopes(); + let scopes = default_scopes(); output::progress("Registering OAuth client with Mapbox..."); - let registration = register_client(&redirect_uri, debug, &scopes)?; + let registration = register_client(&redirect_uri, debug, scopes)?; let (code_verifier, code_challenge) = generate_pkce(); // Same generator as the verifier above, and for the same reason: `state` @@ -1776,7 +1773,7 @@ pub fn login(debug: bool, profile: Option<&str>, mode: Mode) -> Result<()> { AUTHORIZATION_ENDPOINT, percent_encode(®istration.client_id), percent_encode(&redirect_uri), - percent_encode(&scopes), + percent_encode(scopes), state, code_challenge, ); @@ -1837,35 +1834,10 @@ pub fn login(debug: bool, profile: Option<&str>, mode: Mode) -> Result<()> { #[cfg(test)] mod tests { use super::{ - fix_for, remedy_for, scopes_with, time_until, with_auth_fix, TokenSource, DEFAULT_SCOPES, + default_scopes, fix_for, remedy_for, time_until, with_auth_fix, TokenSource, + DEFAULT_SCOPES_LIST, }; - #[test] - fn a_flags_scopes_ride_along_only_while_it_is_gated_on() { - let without = scopes_with(&[]); - let off: Vec<&str> = without.split(' ').collect(); - assert!(!off.contains(&"statistics:read"), "{off:?}"); - assert_eq!(off, DEFAULT_SCOPES.split(' ').collect::>()); - - let scopes = scopes_with(&[&["statistics:read"]]); - let on: Vec<&str> = scopes.split(' ').collect(); - assert!(on.contains(&"statistics:read"), "{on:?}"); - for scope in DEFAULT_SCOPES.split(' ') { - assert!(on.contains(&scope), "lost {scope} when a flag gated on"); - } - } - - /// Goes through `feature_flags::flags::ALL`, unlike `scopes_with` - /// above, so a flag whose scopes never reach `ALL` fails here too. - #[test] - fn requested_scopes_is_never_narrower_than_the_default_set() { - let requested = super::requested_scopes(); - let requested: Vec<&str> = requested.split(' ').collect(); - for scope in DEFAULT_SCOPES.split(' ') { - assert!(requested.contains(&scope), "lost {scope}"); - } - } - /// Every scope the CLI's live operations need, that Mapbox will actually /// grant. The two the specs ask for and the platform still does not have /// — `fonts:metadata`, `tokens:write` — are deliberately absent; their @@ -1875,7 +1847,7 @@ mod tests { /// became registrable (2026-09-08). #[test] fn the_requested_scopes_cover_what_the_commands_need() { - let requested: Vec<&str> = DEFAULT_SCOPES.split(' ').collect(); + let requested = DEFAULT_SCOPES_LIST; for needed in [ "styles:read", @@ -1895,6 +1867,7 @@ mod tests { "tilesets:read", "tilesets:write", "tilesets:list", + "statistics:read", ] { assert!(requested.contains(&needed), "{needed} is not requested"); } @@ -1922,7 +1895,7 @@ mod tests { /// client holding whatever prefix got through. #[test] fn the_scope_query_is_encoded_by_hand_so_it_has_to_be_exact() { - let encoded = percent_encode(DEFAULT_SCOPES); + let encoded = percent_encode(default_scopes()); assert!( !encoded.contains(' '), @@ -1930,7 +1903,7 @@ mod tests { ); assert_eq!( encoded.matches("%20").count(), - DEFAULT_SCOPES.split(' ').count() - 1, + DEFAULT_SCOPES_LIST.len() - 1, "every separator has to survive as one: {encoded}" ); assert!(encoded.contains("styles%3Atiles"), "{encoded}"); diff --git a/src/completion.rs b/src/completion.rs index 0d7446f..f70076c 100644 --- a/src/completion.rs +++ b/src/completion.rs @@ -2,9 +2,8 @@ //! //! The script is generated by `clap_complete` from the same `Command` this //! process assembled, so it cannot describe a different CLI than the binary -//! that printed it: an operation `Operation::is_exposed` withholds, a -//! flag-gated command a release build leaves out (`feature_flags`), and a -//! service a spec sync added yesterday are all handled by not being special +//! that printed it: an operation `Operation::is_exposed` withholds and a +//! service a spec sync added yesterday are both handled by not being special //! cases at all. `a_withheld_operation_is_absent_from_every_shell` in //! `tests/completion.rs` holds the first of those. //! diff --git a/src/feature_flags.rs b/src/feature_flags.rs deleted file mode 100644 index ca61323..0000000 --- a/src/feature_flags.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! Feature flags for commands implemented ahead of a public-rollout decision. -//! -//! The gate is narrower than it reads. It withholds a command from the -//! binaries Mapbox publishes to its staging and production channels, and from -//! nothing else: `MAPBOX_CLI_BUILD_ENV` is what tells those two apart, read at -//! compile time via `option_env!`, and only Mapbox's own release pipeline sets -//! it. Anything else — `cargo build` here, a fork, a distribution packaging -//! this from source — leaves it unset, so `is_dev_build_for` answers true -//! and every flag is on whatever its `switch` says. -//! -//! So a `false` switch is a statement about what an official release ships, -//! not about what the code does. Flipping it to `true` and cutting a release -//! is the only thing that puts a flagged command in front of someone who -//! installed one. -//! -//! A [`Flag`] holds the switch and the OAuth scopes `auth login` should -//! request while it's on, so a new flag is one declaration in [`flags`]. -//! `build_app` uses `.is_enabled()` to decide whether to register a flag's -//! subcommand at all — disabled means it's absent from the `clap` tree, not -//! just hidden from `--help`. `auth::requested_scopes` folds every flag's -//! `.oauth_scopes` into what `login` asks for. - -/// One flag-gated feature: the switch, and the OAuth scopes `auth login` -/// should request only while it's active. -pub struct Flag { - /// Read through [`is_enabled`](Flag::is_enabled), which folds in the - /// dev-build override. - switch: bool, - /// Scopes `auth login` requests only while this flag is enabled. - pub oauth_scopes: &'static [&'static str], -} - -impl Flag { - pub fn is_enabled(&self) -> bool { - enabled_for(self.switch, option_env!("MAPBOX_CLI_BUILD_ENV")) - } -} - -/// True unless `build_env` names the release pipeline building for staging -/// or production. -fn is_dev_build_for(build_env: Option<&str>) -> bool { - !matches!(build_env, Some("staging") | Some("production")) -} - -fn enabled_for(switch: bool, build_env: Option<&str>) -> bool { - switch || is_dev_build_for(build_env) -} - -/// Every flag-gated feature. Flip a switch to `true`, then cut a release, -/// once that feature is ready for staging and production. -/// -/// Nothing is gated today: `ACCOUNT_USAGE` is the only entry here and its -/// switch is on. The mechanism stays for the next command that needs a -/// staged rollout. -pub mod flags { - use super::Flag; - - /// Gates `account_usage::COMMAND` (`mapbox usage`), which calls a - /// private-preview API. `statistics:read` is what that call needs; - /// `mapbox auth login`'s default scopes don't request it. - pub const ACCOUNT_USAGE: Flag = Flag { - switch: true, - oauth_scopes: &["statistics:read"], - }; - - /// Every flag, so a caller (`auth::requested_scopes`) can fold them all - /// in without naming each by hand. A flag left out of this list - /// silently contributes no scopes. - pub const ALL: &[&Flag] = &[&ACCOUNT_USAGE]; -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn dev_and_an_unset_build_env_ignore_a_false_switch() { - assert!(enabled_for(false, None)); - assert!(enabled_for(false, Some("dev"))); - } - - #[test] - fn staging_and_production_hide_a_false_switch() { - assert!(!enabled_for(false, Some("staging"))); - assert!(!enabled_for(false, Some("production"))); - } - - #[test] - fn a_true_switch_is_enabled_everywhere() { - for build_env in [None, Some("dev"), Some("staging"), Some("production")] { - assert!(enabled_for(true, build_env)); - } - } - - #[test] - fn every_flag_with_oauth_scopes_is_reachable_through_all() { - assert!( - !flags::ACCOUNT_USAGE.oauth_scopes.is_empty(), - "update this test if a scope-less flag is ever added on purpose" - ); - let scopes_in_all: Vec<&str> = flags::ALL - .iter() - .flat_map(|f| f.oauth_scopes.iter().copied()) - .collect(); - for scope in flags::ACCOUNT_USAGE.oauth_scopes { - assert!( - scopes_in_all.contains(scope), - "{scope} is not reachable through ALL, so it never reaches a login" - ); - } - } -} diff --git a/src/main.rs b/src/main.rs index 89712ca..fb6f480 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,7 +20,6 @@ mod completion; mod confirm; mod deprecation; mod executor; -mod feature_flags; mod generate_skills; mod http; mod link; @@ -586,17 +585,13 @@ fn build_app(specs: &[ServiceSpec]) -> Command { // Between the other two hand-written leaves, so the help lists the three // that make no request together and in the order someone meets them. // What it prints is built from `app` itself, which is why nothing here - // has to know about it: a service added by a spec sync, or a command a - // feature flag left out, is completed or not completed by having been - // registered above or not. + // has to know about it: a service added by a spec sync is completed or + // not by having been registered above or not. app = app.subcommand(completion::command()); app = app.subcommand(uninstall::command()); - // See `feature_flags`: absent from the tree, not merely hidden, when disabled. - if feature_flags::flags::ACCOUNT_USAGE.is_enabled() { - app = app.subcommand(account_usage::command()); - } + app = app.subcommand(account_usage::command()); app.subcommand(tilesets_cli::command()) } diff --git a/src/update_check.rs b/src/update_check.rs index 1a0bc69..57bf679 100644 --- a/src/update_check.rs +++ b/src/update_check.rs @@ -35,8 +35,9 @@ //! `MAPBOX_CLI_AUTH`, and this never sends a credential on a request the //! user did not type. So only a production build checks — a dev build, a //! staging build and `cargo build` make no request at all. The channel is -//! compiled in from `MAPBOX_CLI_BUILD_ENV`, the same variable -//! [`crate::feature_flags`] reads. +//! compiled in from `MAPBOX_CLI_BUILD_ENV`, read at compile time via +//! `option_env!`, the same way an official release pipeline distinguishes +//! staging from production. //! - **Someone watching.** stderr must be a terminal. A notice nobody reads //! is noise in a CI log, and the request behind it is the one a scripted //! environment has the least reason to make. This is what keeps the check