From f4cd62989facdcd889d9347ba68230481a698328 Mon Sep 17 00:00:00 2001 From: Mofei Zhu Date: Fri, 9 Oct 2026 14:39:45 +0300 Subject: [PATCH 1/3] Place the navigation commands in a Navigation help group --- CHANGELOG.md | 4 ++-- src/help_layout.rs | 19 ++++++++++++++++--- tests/output_contract.rs | 1 + 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 071d480..301157a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,8 +47,8 @@ that may never merge. They are not releases and are not listed here. way off wins. - Top-level `mapbox --help` lists commands in groups (Maps and data, - Search, Account, Coding agents, CLI), each with a line saying what it - does rather than which API it wraps, and the global options under + Search, Navigation, Account, Coding agents, CLI), each with a line saying + what it does rather than which API it wraps, and the global options under Authentication, Output and Behavior, wrapped to the terminal. At a terminal, help — top-level or a command's — opens with the `mapbox · v` banner, as a command run does. diff --git a/src/help_layout.rs b/src/help_layout.rs index 8161e4a..d50f7c1 100644 --- a/src/help_layout.rs +++ b/src/help_layout.rs @@ -8,14 +8,13 @@ //! - **Maps and data**: create or fetch map content — styles, sprites, //! fonts, tiles, static images. //! - **Search**: find places and addresses, and report problems with them. +//! - **Navigation**: routes, travel times and reachable areas, and snapping +//! GPS traces to roads. //! - **Account**: credentials, tokens and usage — who you are and what you //! have used. //! - **Coding agents**: wire Mapbox into a coding agent. //! - **CLI**: manage the CLI itself. //! -//! Routing APIs (Directions, Matrix, Isochrone, Map Matching, Optimization) -//! are expected to land as a "Navigation" group of their own. -//! //! clap has no notion of command groups, and aligns each option heading on //! its own column, so this renders the whole top-level page itself — one //! column for every section — and hands it to clap as the root's help @@ -37,6 +36,10 @@ const GROUPS: &[(&str, &[&str])] = &[ &["styles", "sprites", "fonts", "tilesets", "static"], ), ("Search", &["search", "geocoder", "feedback"]), + ( + "Navigation", + &["directions", "matrix", "isochrone", "map-match"], + ), ("Account", &["auth", "accounts", "usage"]), ("Coding agents", &["mcp", "agent-skills", "generate-skills"]), ( @@ -73,6 +76,16 @@ const DESCRIPTIONS: &[(&str, &str)] = &[ ), ("geocoder", "Forward, reverse and batch geocoding"), ("feedback", "Submit and list feedback about Mapbox data"), + ("directions", "Route between waypoints"), + ( + "matrix", + "Travel times and distances between every pair of points", + ), + ( + "isochrone", + "Areas reachable from a point within a time or distance", + ), + ("map-match", "Snap a GPS trace to the road network"), ("accounts", "List access tokens and their scopes"), ( "generate-skills", diff --git a/tests/output_contract.rs b/tests/output_contract.rs index 69a0aa9..2d34129 100644 --- a/tests/output_contract.rs +++ b/tests/output_contract.rs @@ -484,6 +484,7 @@ fn top_level_help_groups_commands_and_options() { let headings = [ "Maps and data:", "Search:", + "Navigation:", "Account:", "Coding agents:", "CLI:", From 9656fc0cccde21399d90b58a6b11103e34734020 Mon Sep 17 00:00:00 2001 From: Mofei Zhu Date: Wed, 7 Oct 2026 12:49:27 +0300 Subject: [PATCH 2/3] Fix stale command and operation counts in docs/commands.md --- docs/commands.md | 49 +++++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index e317d84..d830b43 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -1,7 +1,8 @@ # Implemented commands -Every command the CLI ships: five auth commands, 36 API operations across 10 -command groups, the tilesets-cli proxy, `completion` and `generate-skills`. Each is +Every command the CLI ships: five auth commands, 39 API operations (35 across 9 +command groups, plus `directions`, `isochrone`, `map-match` and `matrix`), the +tilesets-cli proxy, `completion` and `generate-skills`. Each is shown in both of its renderings. Which one you get is decided by `--output`, whose default (`auto`) reads stdout: a terminal gets the left column, a pipe or redirect gets the right one. See @@ -10,11 +11,12 @@ gets the right one. See Account names, style ids and tokens in the examples are replaced; everything else is as the API sent it. -**32 of the 36 were run against the live API and show what came back:** 25 +**35 of the 39 were run against the live API and show what came back:** 25 on 2026-09-01, `fonts list`, `fonts upload` and `fonts delete` on 2026-09-08, once `fonts:list`/`fonts:write` became registrable, -`directions route` on 2026-09-23, `feedback list` and `feedback get` on -2026-09-24, and `feedback create` on 2026-10-02. +`directions`, `isochrone` and `map-match` on 2026-09-23, `matrix`, +`feedback list` and `feedback get` on 2026-09-24, and `feedback create` on +2026-10-02. The write operations were exercised as round trips on throwaway objects — a style created, updated, drafted and deleted; icons uploaded to a sprite and taken out again; a font uploaded and deleted — leaving the account as @@ -454,33 +456,35 @@ No stored profiles. Run `mapbox auth login` to create one. ## API command groups -33 operations across 10 command groups. Nine are generated from the OpenAPI specs -vendored in `openapi/`; `search` is the one exception — a hand-authored -spec versioned in this repo's own `custom-openapi/`, see +35 operations across 9 command groups, plus four single-operation commands +with no group: `directions`, `isochrone`, `map-match` and `matrix`. Seven +groups are generated from the OpenAPI specs vendored in `openapi/`; `search`, +`feedback` and the four single commands are the exceptions — hand-authored +specs versioned in this repo's own `custom-openapi/`, see `custom-openapi/README.md`. **A command group is not a spec file.** Which command group an operation belongs to is decided per operation, by an `x-mapbox-cli-command` extension the sync writes onto it, not by which file it was parsed from — so `sprites` is five operations out of the styles spec, and `tilesets` is one operation -each out of the raster-tiles and vector-tiles specs. Two names that used to -be command groups, `maps` and `vectortiles`, are gone because their last -operation moved to `tilesets`. +each out of the raster-tiles, vector-tiles and tilequery specs. Names that +used to be command groups — `maps`, `vectortiles`, `tilequery`, +`static-images` and `static-tiles` — are gone because their last operation +moved to `tilesets` or `static`. | Command group | Operations | | --- | --- | | [`accounts`](#accounts) | **3** | +| [`feedback`](#feedback) | **3** | | [`fonts`](#fonts) | **3** | | [`geocoder`](#geocoder) | **3** | | [`search`](#search) | **4** | | [`sprites`](#sprites) | **5** | -| [`static-images`](#static-images) | **3** | -| [`static-tiles`](#static-tiles) | **1** | -| [`styles`](#styles) | **8** | -| [`tilequery`](#tilequery) | **1** | -| [`tilesets`](#tilesets) | **2** | +| [`static`](#static) | **2** | +| [`styles`](#styles) | **9** | +| [`tilesets`](#tilesets) | **3** | -The [Contents](#contents) list above names every one of the 33. +The [Contents](#contents) list above names every one of the 39. **Everything else the Mapbox specs describe is not here at all.** Not hidden, not shipped as a command that refuses: absent from the spec content @@ -588,8 +592,8 @@ Three things worth knowing about the read forms: A command that changes something takes `--dry-run`, which prints the request it would send, on stdout, and sends nothing. Which commands those are is not -a list anyone keeps: it is every `POST`, `PUT`, `PATCH` and `DELETE` — 12 of -the 33 operations — plus `auth login`, `auth logout`, `auth refresh` and +a list anyone keeps: it is every `POST`, `PUT`, `PATCH` and `DELETE` — 13 of +the 39 operations — plus `auth login`, `auth logout`, `auth refresh` and `generate-skills`. A read-only `GET` does not take it, so `mapbox styles list --dry-run` is a usage error rather than a no-op. It rehearses rather than describes: `--data` is parsed and every `--file` is read, so a @@ -670,9 +674,8 @@ properties directly. A conforming response never reaches that case: `geocoder` requires `name`/`feature_type` on every feature, `tilequery` requires `tilequery.layer`. -Three of the ten command groups can answer with bytes — `static-images`, -`static-tiles` and `tilesets`. Those bypass `--output` in -both modes: +Two of the nine command groups can answer with bytes — `static` and +`tilesets`. Those bypass `--output` in both modes: @@ -687,7 +690,7 @@ it to a file, e.g. `... > out.png`.
Terminal — refusesRedirected — raw bytes
``` -$ mapbox static-images get-static-image … > map.png +$ mapbox static get-image … > map.png $ file map.png map.png: PNG image data, 600 x 400 ``` From 18ffd5f96f9ddfde3ab1a7d1cfb886ff4f08cbd5 Mon Sep 17 00:00:00 2001 From: Mofei Zhu Date: Wed, 7 Oct 2026 14:20:44 +0300 Subject: [PATCH 3/3] Restore the tilesets query list under -o text The renderer matched the service name tilequery, which #116 merged into tilesets, so tilesets query has printed pretty JSON since 0.2.0. Match tilesets, and check every listed name against the bundled specs so a future rename fails a test instead of dropping the list. --- CHANGELOG.md | 7 +++++- docs/commands.md | 6 ++--- src/output/mod.rs | 2 +- src/output/render.rs | 52 ++++++++++++++++++++++++++++++++++---------- 4 files changed, 51 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 301157a..da87b8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,11 @@ that may never merge. They are not releases and are not listed here. ## Unreleased +- `tilesets query` renders as a numbered list under `-o text` again, as + documented. It had printed pretty JSON since 0.2.0, when the command moved + from `tilequery get` and the renderer kept matching the old name. + `-o json` is unchanged. + - `mapbox usage` reads as a chart: one row per product with no blank lines between, every sparkline as wide as the period, bars scaled from zero so their height is proportional to the value, and the tallest @@ -230,7 +235,7 @@ that may never merge. They are not releases and are not listed here. terminal or for `mapbox completion`. `--quiet`/`-q` or `MAPBOX_QUIET=1` hides it. Table headers, the labels of key/value lists (`auth whoami`, `doctor`, `config list`, a single object's fields), the result lists of - `geocoder`, `search` and `tilequery`, and tips are styled at a terminal + `geocoder`, `search` and `tilesets query`, and tips are styled at a terminal too, and a result written to a file or pipe never is; `NO_COLOR` turns color off everywhere. diff --git a/docs/commands.md b/docs/commands.md index d830b43..b4012cd 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -633,7 +633,7 @@ rather than from the spec: | One key holding an array of like objects | The same table — `{"icons":[…]}` is still a listing | | A single object | A field list, one level of nesting flattened onto dotted keys | | No body at all | A confirmation naming what happened — `Deleted .`, or `{"ok":true,…}` | -| A `search`, `geocoder` or `tilequery` `FeatureCollection` | A numbered list, one entry per feature — see the paragraph below | +| A `search`, `geocoder` or `tilesets query` `FeatureCollection` | A numbered list, one entry per feature — see the paragraph below | | Anything else | Pretty-printed JSON — every other command group's GeoJSON, style documents and bare values lose their meaning in a table | A table shows the columns most rows have, that vary, and that do not repeat @@ -662,7 +662,7 @@ usually wants a result for), never clipped: 24.7454,59.437 ``` -`geocoder`'s and `tilequery`'s `FeatureCollection`s render as a numbered list +`geocoder`'s and `tilesets query`'s `FeatureCollection`s render as a numbered list for the same reason — see their own sections for the shape. The match is on those three command-group names exactly, so another command group that answers with GeoJSON keeps falling to pretty-printed JSON; its nesting is the information @@ -671,7 +671,7 @@ to show falls the whole collection back to JSON rather than print a blank numbered entry — a feature that also carries a geometry still keeps its coordinate line, since the guard checks what the row ended up with, not the properties directly. A conforming response never reaches that case: -`geocoder` requires `name`/`feature_type` on every feature, `tilequery` +`geocoder` requires `name`/`feature_type` on every feature, `tilesets query` requires `tilequery.layer`. Two of the nine command groups can answer with bytes — `static` and diff --git a/src/output/mod.rs b/src/output/mod.rs index facf799..dc84b18 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -239,7 +239,7 @@ fn encode(value: &Value, pretty: bool) -> Result { /// suits one, and pretty-printed otherwise. /// /// `service` gates the exception — see [`list_rendering`]: `search`'s, -/// `geocoder`'s and `tilequery`'s GeoJSON render as a list instead. Every +/// `geocoder`'s and `tilesets query`'s GeoJSON render as a list instead. Every /// other value takes the path it always has. /// /// `page` is the note that this response is one page of several. It goes to diff --git a/src/output/render.rs b/src/output/render.rs index f38664c..0a644af 100644 --- a/src/output/render.rs +++ b/src/output/render.rs @@ -1,7 +1,7 @@ //! Turning a response into something a person reads. //! //! Tables for lists, aligned field lists for single objects, and numbered -//! feature lists for the geocoding, search and tilequery services. Every +//! feature lists for `geocoder`, `search` and `tilesets query`. Every //! function here returns text rather than printing it, so the shape of each //! rendering is testable; `super::emit_value` decides where it goes. @@ -16,6 +16,13 @@ use super::style; /// somebody has looked at its features and decided what a line of them should /// say. `None` — an unlisted service, no service at all, or a value that is /// not a `FeatureCollection` — falls through to [`render_human`]. +/// +/// The names are `Operation::service`, which is the command group, not the +/// spec file: `tilesets query` comes from the tilequery spec but answers as +/// `tilesets`. An arm still named `tilequery` after #116 moved the command +/// silently dropped its list for every release since; see +/// `every_listed_service_is_a_real_command_group`. `tilesets`'s other +/// commands answer with tile bytes, which never reach here. pub(super) fn list_rendering(value: &Value, service: Option<&str>) -> Option { match service { Some("search") => match search_feature_rows(value) { @@ -25,7 +32,7 @@ pub(super) fn list_rendering(value: &Value, service: Option<&str>) -> Option { render_geocoder_list(value).or_else(|| render_batch_feature_list(value)) } - Some("tilequery") => tilequery_feature_rows(value).map(|rows| render_feature_list(&rows)), + Some("tilesets") => tilequery_feature_rows(value).map(|rows| render_feature_list(&rows)), _ => None, } } @@ -63,7 +70,7 @@ const FLOOR: usize = 8; /// `search forward`/`reverse`/`category` are asked for a list of POIs to /// scan, the way any other listing is scanned — but as /// [`render_feature_list`], not a table: see there for why. `geocoder` and -/// `tilequery` read the same way and reach the same renderer through their +/// `tilesets query` read the same way and reach the same renderer through their /// own row-builders; every other service's GeoJSON stays pretty-printed — see /// `shapes_a_table_would_misrepresent_are_left_alone` below — because it is a /// handful of features whose nesting *is* the content a table would throw @@ -241,7 +248,7 @@ fn search_result_row(feature: &Value) -> Value { /// A `FeatureCollection`'s features, as one row per result — named /// generically since `geocoder` is the first caller, not the last. /// -/// `search` and `tilequery` keep their own row-builders +/// `search` and `tilesets query` keep their own row-builders /// ([`search_feature_rows`], [`tilequery_feature_rows`]) because what they /// read out of a feature genuinely differs: a search result carries a /// distance and a POI category a geocoding result has no equivalent of, and @@ -365,7 +372,7 @@ fn feature_row(feature: &Value) -> Value { Value::Object(row) } -/// `tilequery`'s features, as one row per result — same `feature_collection_rows` +/// `tilesets query`'s features, as one row per result — same `feature_collection_rows` /// contract, different fields: labeled layers (`poi_label`, `place_label`, a /// named `road`, …) carry `properties.name`; unlabeled ones (`building`, /// `landuse`, …) don't, and where the tileset sends `properties.type` as a @@ -514,8 +521,8 @@ fn tilequery_row(feature: &Value) -> Value { /// /// One renderer for all three services that get a list. Which fields a row /// carries is its own row-builder's business, and every field here is skipped -/// when absent: `extra` is `tilequery`'s alone, and `distance` arrives -/// already formatted — `km` from `search`, `m` from `tilequery` — so the unit +/// when absent: `extra` is `tilesets query`'s alone, and `distance` arrives +/// already formatted — `km` from `search`, `m` from `tilesets query` — so the unit /// is the builder's decision rather than this function's. fn render_feature_list(rows: &[Value]) -> Rendered { if rows.is_empty() { @@ -572,7 +579,7 @@ fn feature_list_text(rows: &[Value], color: bool) -> String { if let Some(coordinates) = row.get("coordinates").and_then(Value::as_str) { out.push_str(&format!("\n {}", dim(coordinates))); } - // Only `tilequery` fills this in; a geocoding row never carries it. + // Only `tilesets query` fills this in; a geocoding row never carries it. if let Some(extra) = row.get("extra").and_then(Value::as_object) { for (key, value) in extra { let rendered = match value { @@ -1740,7 +1747,7 @@ mod tests { for rendered in [ list_rendering(&empty, Some("geocoder")), - list_rendering(&empty, Some("tilequery")), + list_rendering(&empty, Some("tilesets")), ] { assert_eq!(rendered.expect("a list").text, "(none)"); } @@ -1765,7 +1772,7 @@ mod tests { } assert!(render_human(&fc).is_none(), "and nothing else renders it"); - for service in ["search", "geocoder", "tilequery"] { + for service in LISTED_SERVICES { assert!( list_rendering(&fc, Some(service)).is_some(), "{service} should get a list" @@ -1773,6 +1780,29 @@ mod tests { } } + /// Every service `list_rendering` matches, by name. + const LISTED_SERVICES: [&str; 3] = ["search", "geocoder", "tilesets"]; + + /// The match is on a string, so a command group renamed or merged in the + /// specs leaves its arm unreachable without a warning — the tests above + /// pass that string straight in and keep passing. #116 moved + /// `tilequery get` to `tilesets query` exactly that way. Checking each + /// name against the bundled specs is what ties the arm to a real command. + #[test] + fn every_listed_service_is_a_real_command_group() { + let services: Vec = crate::spec::effective_services() + .expect("the bundled specs parse") + .into_iter() + .map(|service| service.name) + .collect(); + for listed in LISTED_SERVICES { + assert!( + services.iter().any(|name| name == listed), + "`{listed}` has a list rendering but no command group by that name: {services:?}" + ); + } + } + #[test] fn a_feature_list_never_clips_an_address() { let long_address = @@ -2160,7 +2190,7 @@ mod tests { ); } - /// Pins docs/commands.md's two `get-tilequery` examples to the real + /// Pins docs/commands.md's two `tilesets query` examples to the real /// render — the vector one, whose `height` has to reach the text column /// now that it reaches the JSON one, and the raster-array one, which has /// no name to show and carries its sample under `val`.