Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -47,8 +52,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<version>` banner, as a command run does.
Expand Down Expand Up @@ -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.

Expand Down
55 changes: 29 additions & 26 deletions docs/commands.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -629,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 <id>.`, 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
Expand Down Expand Up @@ -658,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
Expand All @@ -667,12 +671,11 @@ 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`.

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:

<table>
<tr><th width="50%">Terminal — refuses</th><th width="50%">Redirected — raw bytes</th></tr>
Expand All @@ -687,7 +690,7 @@ it to a file, e.g. `... > out.png`.
</td><td>

```
$ 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
```
Expand Down
19 changes: 16 additions & 3 deletions src/help_layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"]),
(
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/output/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ fn encode(value: &Value, pretty: bool) -> Result<String> {
/// 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
Expand Down
52 changes: 41 additions & 11 deletions src/output/render.rs
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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<Rendered> {
match service {
Some("search") => match search_feature_rows(value) {
Expand All @@ -25,7 +32,7 @@ pub(super) fn list_rendering(value: &Value, service: Option<&str>) -> Option<Ren
Some("geocoder") => {
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,
}
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)");
}
Expand All @@ -1765,14 +1772,37 @@ 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"
);
}
}

/// 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<String> = 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 =
Expand Down Expand Up @@ -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`.
Expand Down
1 change: 1 addition & 0 deletions tests/output_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,7 @@ fn top_level_help_groups_commands_and_options() {
let headings = [
"Maps and data:",
"Search:",
"Navigation:",
"Account:",
"Coding agents:",
"CLI:",
Expand Down
Loading