From cd72c61421605c2d020c43d64771b9651a566123 Mon Sep 17 00:00:00 2001 From: Mofei Zhu Date: Thu, 8 Oct 2026 14:33:00 +0300 Subject: [PATCH] Point agents at --schema from top-level help --- CHANGELOG.md | 4 ++++ src/main.rs | 14 ++++++++++++- tests/output_contract.rs | 43 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db1b9e8..a8a5742 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,10 @@ that may never merge. They are not releases and are not listed here. ## Unreleased +- When a coding agent runs `mapbox --help` (detected the same way as for + the `User-Agent`), the help opens with a two-line hint pointing at + `mapbox --schema`. Help for people, and for subcommands, is unchanged. + - **Breaking**: `--eta-type`, `--navigation-profile` and `--origin` removed from `mapbox search category`. The category endpoint does not currently return an ETA, so the flags were accepted but had no effect. A script diff --git a/src/main.rs b/src/main.rs index 952e32b..9ad81ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -53,6 +53,13 @@ use spec::ServiceSpec; /// single source for the Rust half rather than for all three. const REPO_URL: &str = "https://github.com/mapbox/cli"; +/// Opens the top-level help when an agent is driving the CLI. Agents explore +/// by chaining `--help` calls, one command at a time; `--schema` answers the +/// same question for every command in one document. +const AGENT_HELP_HINT: &str = "\ +Agents: `mapbox --schema` describes every command, its arguments and the request it makes +as one JSON document. Use it instead of chaining --help calls."; + /// Rejects a value the spec says is a number, while still yielding a /// `String`. /// @@ -770,7 +777,12 @@ fn cli() -> u8 { } }; - let app = build_app(&specs); + let mut app = build_app(&specs); + // Here rather than in `build_app`, which tests call directly and which + // should not depend on who happens to be running them. + if agent_detect::detect_agent().is_some() { + app = app.before_help(AGENT_HELP_HINT); + } let argv = tilesets_cli::escape_passthrough_args(&app, raw_argv.clone()); // Parsing consumes the tree, and `--schema` still has to read it // afterwards — so the parse gets the copy and `app` stays whole. diff --git a/tests/output_contract.rs b/tests/output_contract.rs index 62b5048..ebc8869 100644 --- a/tests/output_contract.rs +++ b/tests/output_contract.rs @@ -363,6 +363,49 @@ fn help_is_never_wrapped() { assert!(text.contains("--output"), "--output should be documented"); } +const AGENT_HINT: &str = "Agents: `mapbox --schema`"; + +/// An agent reading top-level help is pointed at `--schema` rather than left +/// to chain `--help` calls. Only there: subcommand help stays as it was. +#[test] +fn an_agent_is_pointed_at_the_schema() { + let out = command() + .env("CLAUDECODE", "1") + .arg("--help") + .output() + .expect("run mapbox"); + assert!(out.status.success()); + assert!(stdout(&out).contains(AGENT_HINT), "{}", stdout(&out)); + + let sub = command() + .env("CLAUDECODE", "1") + .args(["styles", "--help"]) + .output() + .expect("run mapbox"); + assert!(!stdout(&sub).contains(AGENT_HINT), "{}", stdout(&sub)); +} + +/// A person gets the help they always did. The environment is cleared rather +/// than having agent variables removed one by one: the suite itself often +/// runs under an agent, and `agent_detect` knows more variables than this +/// file should have to list. +#[test] +fn without_an_agent_the_help_has_no_hint() { + let home = sandbox_home(); + let mut cmd = Command::new(env!("CARGO_BIN_EXE_mapbox")); + cmd.env_clear() + .env("HOME", &home) + .env("MAPBOX_CONFIG_DIR", home.join(".mapbox")); + // Windows processes expect this one; nothing reads it as an agent. + if let Some(root) = std::env::var_os("SystemRoot") { + cmd.env("SystemRoot", root); + } + let out = cmd.arg("--help").output().expect("run mapbox"); + + assert!(out.status.success()); + assert!(!stdout(&out).contains(AGENT_HINT), "{}", stdout(&out)); +} + /// An operation that can never succeed is not in the command surface, so it /// answers exactly as a mistyped name does — same code, same shape, same /// exit. Anything else would tell a caller which scopes exist while still