From f0cfd390301d14d6374d115b11d131b96c6dfc37 Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Tue, 29 Sep 2026 09:41:58 -0400 Subject: [PATCH 1/2] Give "no coding agent detected" a stable error code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit skill_dest::resolve returned a plain anyhow error when no coding agent was detected and none was explicitly named with --agent/--global/--dir. That's the common case on most machines (no coding agent installed at all), and the error had no way to be told apart from an unrelated internal failure — both rendered with the same generic "error" code. Now a real CliError with a stable no_agent_detected code. Same message, same non-zero exit, for every existing caller — exit codes are a documented promise in this repo, so this had to be additive, not a behavior change. Documented in docs/commands.md's "Where they go" section (shared by generate-skills and agent-skills, since both go through the same resolve()), and the existing end-to-end test in tests/generate_skills.rs now asserts the code itself, not just that the message mentions the right flags. Split out of #61 per review — the installer work there can depend on this merging first rather than carrying it. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 9 +++++++++ docs/commands.md | 15 +++++++++++++++ src/skill_dest.rs | 31 +++++++++++++++++++++---------- tests/generate_skills.rs | 8 ++++++++ 4 files changed, 53 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c707676..392fccf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,15 @@ that may never merge. They are not releases and are not listed here. ## Unreleased +- `mapbox generate-skills`/`agent-skills install`/`agent-skills update`, + when no coding agent is detected and none was named with `--agent`, + `--global` or `--dir`: the failure now carries a stable + `no_agent_detected` error code (previously an unstructured message with + the generic `error` code, indistinguishable from an unrelated internal + failure). Same message, same non-zero exit — only the `code` a caller can + match on is new. Documented in docs/commands.md's "Where they go" + section. + - Diagnostic logs, off by default: `mapbox config set log on` (or `MAPBOX_LOG=1`) keeps, for each run in history, the command line, each request and the error message, tokens redacted, on your machine only. diff --git a/docs/commands.md b/docs/commands.md index b320671..cf8448e 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -2623,6 +2623,21 @@ not in the table is what `--dir` is for. An agent counts as installed when its home directory exists — the presence of somewhere to read a skill from, not a `PATH` lookup. +**With no `--agent`, no `--global`, and no `--dir`, and no agent detected:** +`no_agent_detected` — both `generate-skills` and `agent-skills +install`/`update`/`uninstall` refuse rather than silently writing nowhere. +`agent-skills list` is the exception: it reports what's published either +way, since it never writes anything. + +``` +Error: Nowhere to write skills: no --agent was named, and no agent's home directory was found. +Fix: Pass --agent to write for one anyway (claude-code, codex, amp, …), --global to write under its home directory, or --dir to write somewhere specific. +``` + +An explicit `--agent ` for an agent whose home directory genuinely +isn't there is a different case — that's a mistake worth a specific +message of its own, not this code, since something *was* asked for. + --- ### `mapbox agent-skills list` diff --git a/src/skill_dest.rs b/src/skill_dest.rs index d51dbc3..4da2f4b 100644 --- a/src/skill_dest.rs +++ b/src/skill_dest.rs @@ -51,6 +51,9 @@ use anyhow::{anyhow, Result}; use clap::builder::PossibleValuesParser; use clap::{Arg, ArgAction, ArgMatches}; +use crate::output::CliError; +use crate::remedy::Remedy; + /// `--agent`, `--global` and `--dir`: the arg ids, and their long spellings. pub const AGENT_ARG: &str = "agent"; pub const GLOBAL_ARG: &str = "global"; @@ -610,13 +613,17 @@ pub fn resolve( if require && out.is_empty() { let known: Vec<&str> = Agent::all().map(Agent::flag).collect(); - return Err(anyhow!( + return Err(CliError::new( + "no_agent_detected", "Nowhere to write skills: no --agent was named, and no agent's home \ - directory was found. Pass --agent to write for one anyway ({}), \ - --global to write under its home directory, or --dir to write \ - somewhere specific.", + directory was found.", + ) + .with_remedy(Remedy::default().with_fix(&format!( + "Pass --agent to write for one anyway ({}), --global to write under \ + its home directory, or --dir to write somewhere specific.", known.join(", ") - )); + ))) + .into()); } Ok(out) @@ -1187,15 +1194,19 @@ mod tests { fn nothing_to_write_to_is_an_error_that_names_the_flags() { let nowhere = AgentHomes::default(); let err = resolve(None, &[], false, &nowhere, true) - .expect_err("no agent, no --dir, and nothing detected"); - let message = err.to_string(); + .expect_err("no agent, no --dir, and nothing detected") + .downcast::() + .expect("a CliError, not some other failure"); + assert_eq!(err.code, "no_agent_detected"); + + let fix = err.fix.as_deref().unwrap_or_default(); for flag in ["--agent", "--dir"] { - assert!(message.contains(flag), "{message} does not mention {flag}"); + assert!(fix.contains(flag), "{fix} does not mention {flag}"); } for agent in Agent::all() { assert!( - message.contains(agent.flag()), - "{message} does not mention {}", + fix.contains(agent.flag()), + "{fix} does not mention {}", agent.flag() ); } diff --git a/tests/generate_skills.rs b/tests/generate_skills.rs index 1d9f12d..99ac164 100644 --- a/tests/generate_skills.rs +++ b/tests/generate_skills.rs @@ -359,6 +359,14 @@ fn nowhere_to_write_is_an_error_naming_the_flags() { assert!(!out.status.success(), "{}", stdout(&out)); let message = stderr(&out); + // A stable, machine-readable code — not just a message a script would + // have to pattern-match — so a caller (an installer, say) can tell + // "nothing detected" apart from an unrelated failure. See + // docs/commands.md's own section on this. + assert!( + message.contains("\"code\":\"no_agent_detected\""), + "{message} does not carry the no_agent_detected code" + ); for flag in ["--agent", "--global", "--dir"] { assert!(message.contains(flag), "{message} does not mention {flag}"); } From ab94b31b64908ab3691fc2d9edca289f191a30fe Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Tue, 29 Sep 2026 10:18:04 -0400 Subject: [PATCH 2/2] Ask before installing a coding agent skill, and never overwrite a local edit Reworks the install.sh/install.ps1 coding-agent-skill step per review on PR #61: it now asks explicit interactive consent (naming the agent and what will be written) before writing anything, skips with one line of manual instructions when there is no console to ask on, and checks with --dry-run before ever re-running agent-skills update so a locally edited skill is reported rather than silently overwritten. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 11 +++ README.md | 8 ++ scripts/install.ps1 | 116 ++++++++++++++++++++++++ scripts/install.sh | 148 +++++++++++++++++++++++++----- scripts/test-install.ps1 | 190 ++++++++++++++++++++++++++++++++++++++- scripts/test-install.sh | 155 ++++++++++++++++++++++++++++++++ 6 files changed, 601 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 392fccf..f5d0564 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,17 @@ that may never merge. They are not releases and are not listed here. ## Unreleased +- `install.sh`/`install.ps1`: when the install finds a coding agent on the + machine, it now asks, once, whether to write this CLI's own skill and + install the Mapbox Agent Skills library for it, naming the agent and what + will be written before doing either. Answered no, or asked somewhere with + no terminal to answer on (a CI job, a container), it does neither and + prints the two commands to run by hand instead. On a reinstall, it never + overwrites a skill file you edited: it checks first, and only replaces + what is unchanged from what was published, reporting local edits rather + than discarding them. `MAPBOX_CLI_NO_AGENT_SETUP=1` skips the question + entirely, same spelling convention as `MAPBOX_CLI_NO_TELEMETRY`. + - `mapbox generate-skills`/`agent-skills install`/`agent-skills update`, when no coding agent is detected and none was named with `--agent`, `--global` or `--dir`: the failure now carries a stable diff --git a/README.md b/README.md index 5867392..fb889c2 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,14 @@ them end to end without touching the network. Run `mapbox --help` once it's on your `PATH`. +If the script finds a coding agent on the machine ([Claude Code](#agent-skills) +and the rest of the fifteen `agent-skills` supports), it asks, once, whether +to set up this CLI's own [skill](#generate-skills) and the Mapbox Agent +Skills library for it, naming the agent before it writes anything. Answer no, +or run the script somewhere with no terminal to ask on (CI, a container), and +it does neither, printing the two commands to run by hand instead. Set +`MAPBOX_CLI_NO_AGENT_SETUP=1` to skip the question entirely. + ### Download the archive yourself Nothing about the install script is required. If piping one into a shell is diff --git a/scripts/install.ps1 b/scripts/install.ps1 index e5c7663..c7fa74d 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -665,6 +665,122 @@ Windows 11 on Arm has and Windows 10 on Arm does not. Write-Host '' Write-Host " `$env:MAPBOX_TILESETS_CLI = 'C:\path\to\tilesets.cmd'" } + + # --- Coding agent skill ---------------------------------------------- + # + # Most machines running this script have no coding agent installed at + # all, and generate-skills answers "nowhere to write" as + # no_agent_detected rather than writing anything when that is the + # case (src/skill_dest.rs) - checked first, silently, so nothing is + # offered when there is nothing to offer. -Global, not the + # project-scoped default: this script runs in whatever directory the + # shell happened to be in, which has no relation to a project. + # + # Past that point this asks: writing into a directory this CLI does + # not own and downloading a whole separate library is a bigger ask + # than a one-line telemetry notice, and an install nobody consented + # to is not a feature. [Console]::IsInputRedirected is false for a + # real interactive session even under `irm ... | iex` - that pipes an + # object through PowerShell's own pipeline into Invoke-Expression, + # which runs in this same process rather than redirecting a child + # process's stdin the way `curl ... | sh` does - so a person typing + # that at a real prompt still gets asked; only a session with no + # console at all (a scheduled task, an image build) does not. + function Get-AgentName([string]$Json) { + ([regex]::Matches($Json, '"source":"([^"]*)"') | ForEach-Object { + $_.Groups[1].Value -replace ', all projects$', '' + }) -join ', ' + } + + $AgentSetupAllowed = $true + $disableAgentSetup = $env:MAPBOX_CLI_NO_AGENT_SETUP + if ($null -ne $disableAgentSetup) { + $disableAgentSetup = $disableAgentSetup.Trim().ToLowerInvariant() + $AgentSetupAllowed = ( + $disableAgentSetup.Length -eq 0 -or + @('0', 'f', 'false', 'n', 'no', 'off') -contains $disableAgentSetup + ) + } + + if ($AgentSetupAllowed) { + # A child writing to stderr is a terminating NativeCommandError in + # Windows PowerShell under $ErrorActionPreference = 'Stop' (set + # once, at the top of this script) - the same reason + # Invoke-Installer in test-install.ps1 has to switch to + # 'Continue' around a call that does the same. Every command + # below writes its failures to stderr, on purpose, so this has to + # hold for all of them; restored in the finally below regardless + # of which path is reached. + $previousErrorAction = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { + $checkOut = (& $destination generate-skills --global --dry-run -o json 2>$null) -join '' + if ($LASTEXITCODE -eq 0) { + $agentNames = Get-AgentName $checkOut + Write-Host '' + Write-Host "A coding agent was detected on this machine: $agentNames." + $doAgentSetup = $false + if (-not [Console]::IsInputRedirected) { + Write-Host '' + $answer = Read-Host 'Set up the mapbox CLI skill and the Mapbox Agent Skills library for it? [y/N]' + if ($answer -match '^(?i:y|yes)$') { $doAgentSetup = $true } + } + if ($doAgentSetup) { + # Offline and safe to re-run: it replaces its own + # generated directory wholesale and refuses only if + # something else already lives there, which -Global + # keeps out of this script's way. + & $destination generate-skills --global *> $null + if ($LASTEXITCODE -eq 0) { + Write-Host "Wrote the mapbox CLI skill for: $agentNames." + } + + # `install` errors with already_installed on a + # reinstall, since the skill directory is already + # there from a previous run of this same script - + # `update` is the one safe to repeat, but only once + # checked first: some of what changed since the last + # run might be a local edit, not just an upstream + # refresh, and that is never overwritten without + # being named. + $agentStderr = [System.IO.Path]::GetTempFileName() + try { + & $destination agent-skills install --global -o json 1>$null 2>$agentStderr + if ($LASTEXITCODE -eq 0) { + Write-Host "Installed the Mapbox Agent Skills library for: $agentNames." + } else { + $agentOut = Get-Content -LiteralPath $agentStderr -Raw -ErrorAction SilentlyContinue + if ($agentOut -and $agentOut.Contains('"code":"already_installed"')) { + $updateStderr = [System.IO.Path]::GetTempFileName() + try { + $updateOut = (& $destination agent-skills update --global --dry-run -o json 2>$updateStderr) -join '' + if ($LASTEXITCODE -eq 0) { + if ($updateOut.Contains('"updated":[]')) { + & $destination agent-skills update --global *> $null + Write-Host "Updated the Mapbox Agent Skills library for: $agentNames." + } else { + Write-Host 'Some Mapbox Agent Skills have local changes and were left alone.' + Write-Host "Run 'mapbox agent-skills update --global' to review and replace them." + } + } + } finally { + Remove-Item -LiteralPath $updateStderr -Force -ErrorAction SilentlyContinue + } + } + } + } finally { + Remove-Item -LiteralPath $agentStderr -Force -ErrorAction SilentlyContinue + } + } else { + Write-Host 'Not set up. Run these any time:' + Write-Host ' mapbox generate-skills --global' + Write-Host ' mapbox agent-skills install --global' + } + } + } finally { + $ErrorActionPreference = $previousErrorAction + } + } } finally { if ($staged -and (Test-Path -LiteralPath $staged)) { Remove-Item -LiteralPath $staged -Force -ErrorAction SilentlyContinue diff --git a/scripts/install.sh b/scripts/install.sh index dbf91d8..6a78949 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -556,32 +556,11 @@ stops this happening again; this script did not touch it. EOF fi -# --- The Tilesets CLI ------------------------------------------------------ -# -# `mapbox tilesets-cli` execs a separately installed `tilesets`. Everything -# below says the same thing as `launch_failed` in src/tilesets_cli.rs, in -# another language; keep the two in step. - -tilesets_instructions() { - cat </dev/null @@ -610,6 +589,127 @@ is_no() { esac } +# --- Coding agent skill ------------------------------------------------------ +# +# Most machines running this script have no coding agent installed at all, +# and generate-skills answers "nowhere to write" as `no_agent_detected` +# rather than writing anything when that is the case (src/skill_dest.rs) — +# checked first, silently, so nothing is offered when there is nothing to +# offer. --global, not the project-scoped default: this script runs in +# whatever directory the shell happened to be in, which has no relation to +# a project. +# +# Ahead of the Tilesets CLI below, on purpose: several of its branches exit +# 0 partway through (already on PATH, MAPBOX_TILESETS_CLI set, declined), and +# this must run regardless of any of that. +# +# Past that point this asks, the same way the Tilesets CLI below does: +# writing into a directory this CLI does not own and downloading a whole +# separate library is a bigger ask than a one-line telemetry notice, and an +# install nobody consented to is not a feature. An interactive terminal +# asks and installs only on yes; no terminal says nothing was installed and +# how to do it by hand instead. +agent_setup_allowed() { + # Same boolean spellings as MAPBOX_CLI_NO_TELEMETRY above: unset, empty + # or one of clap's false spellings is allowed; anything else opts out. + # ASCII-only on purpose, the same reason telemetry_allowed is above. + # shellcheck disable=SC2018,SC2019 + case "$(printf '%s' "${MAPBOX_CLI_NO_AGENT_SETUP-}" | + tr 'A-Z' 'a-z' | + sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" in + '' | 0 | f | false | n | no | off) return 0 ;; + *) return 1 ;; + esac +} + +# The "source" field of each destination in a generate-skills/agent-skills +# --global JSON report is ", all projects" — this is the part +# worth putting in front of a person before asking them anything, not the +# full file listing the real report carries. +agent_names_from() { # agent_names_from : comma-joined agent labels + printf '%s' "$1" | grep -o '"source":"[^"]*"' | + sed 's/"source":"//;s/, all projects"$//' | + paste -sd, - | sed 's/,/, /g' +} + +if agent_setup_allowed; then + if agent_check=$("${INSTALL_DIR}/mapbox" generate-skills --global --dry-run -o json 2>/dev/null /dev/null 2>&1 &1 /dev/null /dev/null 2>&1 0 ? args[0] : ""; + if (a0 == "generate-skills" || a0 == "agent-skills") { + System.Console.Error.WriteLine("{\"code\":\"no_agent_detected\"}"); + return 1; + } + System.Console.WriteLine("$VersionText"); + return 0; + } +} "@ & $csc /nologo "/out:$Path" $source | Out-Null if ($LASTEXITCODE -ne 0) { throw "csc.exe could not build the stand-in binary" } Remove-Item -LiteralPath $source -Force } else { - Set-Content -LiteralPath $Path -Value "#!/bin/sh`necho '$VersionText'" + Set-Content -LiteralPath $Path -Value @" +#!/bin/sh +case "`${1:-}" in + generate-skills | agent-skills) + echo '{"code":"no_agent_detected"}' >&2 + exit 1 + ;; + *) echo '$VersionText' ;; +esac +"@ + & /bin/chmod '+x' $Path + } +} + +# A fake binary that reports one detected coding agent, "claude", for the +# cases below that exercise install.ps1's coding-agent-skill step - the +# stand-in above always answers no_agent_detected instead, which every other +# case in this file relies on. Driven at runtime by two environment +# variables so the same binary covers every branch: MAPBOX_TEST_INSTALL_MODE +# (ok | already_installed) and MAPBOX_TEST_UPDATE_MODE (clean | dirty, read +# only once install reports already_installed) - the same two knobs +# test-install.sh's agent-channel fixture uses. +function New-AgentFakeBinary([string]$Path, [string]$VersionText) { + $ErrorActionPreference = 'Continue' + if ($OnWindows) { + $csc = Join-Path $env:SystemRoot 'Microsoft.NET\Framework64\v4.0.30319\csc.exe' + if (-not (Test-Path -LiteralPath $csc)) { + $csc = Join-Path $env:SystemRoot 'Microsoft.NET\Framework\v4.0.30319\csc.exe' + } + if (-not (Test-Path -LiteralPath $csc)) { throw 'no csc.exe to build the stand-in binary with' } + $source = "$Path.cs" + Set-Content -LiteralPath $source -Encoding ASCII -Value @" +using System; +using System.Linq; +class Program { + static int Main(string[] args) { + string a0 = args.Length > 0 ? args[0] : ""; + if (a0 == "--version") { Console.WriteLine("$VersionText"); return 0; } + if (a0 == "generate-skills") { + if (args.Contains("--dry-run")) { + Console.WriteLine("{\"written\":true,\"skills\":[{\"source\":\"claude, all projects\"}]}"); + } else { + Console.WriteLine("{\"written\":true}"); + } + return 0; + } + if (a0 == "agent-skills") { + string sub = args.Length > 1 ? args[1] : ""; + string installMode = Environment.GetEnvironmentVariable("MAPBOX_TEST_INSTALL_MODE") ?? "ok"; + string updateMode = Environment.GetEnvironmentVariable("MAPBOX_TEST_UPDATE_MODE") ?? "clean"; + if (sub == "install") { + if (installMode == "already_installed") { + Console.Error.WriteLine("{\"code\":\"already_installed\"}"); + return 1; + } + Console.WriteLine("{\"installed\":true}"); + return 0; + } + if (sub == "update") { + if (args.Contains("--dry-run")) { + if (updateMode == "dirty") { + Console.WriteLine("{\"updated\":[\"some-skill\"],\"unchanged\":[]}"); + } else { + Console.WriteLine("{\"updated\":[],\"unchanged\":[\"some-skill\"]}"); + } + } else { + Console.WriteLine("{\"updated\":[\"some-skill\"]}"); + } + return 0; + } + } + Console.WriteLine("$VersionText"); + return 0; + } +} +"@ + & $csc /nologo "/out:$Path" $source | Out-Null + if ($LASTEXITCODE -ne 0) { throw "csc.exe could not build the agent-aware stand-in binary" } + Remove-Item -LiteralPath $source -Force + } else { + Set-Content -LiteralPath $Path -Value @" +#!/bin/sh +case "`${1:-}" in + --version) echo '$VersionText' ;; + generate-skills) + for a in "`$@"; do + if [ "`$a" = "--dry-run" ]; then + echo '{"written":true,"skills":[{"source":"claude, all projects"}]}' + exit 0 + fi + done + echo '{"written":true}' + ;; + agent-skills) + sub="`$2" + case "`$sub" in + install) + if [ "`${MAPBOX_TEST_INSTALL_MODE:-ok}" = already_installed ]; then + echo '{"code":"already_installed"}' >&2 + exit 1 + fi + echo '{"installed":true}' + ;; + update) + is_dry=no + for a in "`$@"; do [ "`$a" = "--dry-run" ] && is_dry=yes; done + if [ "`$is_dry" = yes ]; then + if [ "`${MAPBOX_TEST_UPDATE_MODE:-clean}" = dirty ]; then + echo '{"updated":["some-skill"],"unchanged":[]}' + else + echo '{"updated":[],"unchanged":["some-skill"]}' + fi + else + echo '{"updated":["some-skill"]}' + fi + ;; + esac + ;; + *) echo '$VersionText' ;; +esac +"@ & /bin/chmod '+x' $Path } } @@ -200,7 +336,8 @@ function New-ChannelVersion { [string]$ArtifactName = $Target, [switch]$BadSha, [switch]$Broken, - [switch]$Empty + [switch]$Empty, + [switch]$Agent ) $dir = Join-Path $Root $Dir @@ -225,6 +362,8 @@ function New-ChannelVersion { Set-Content -LiteralPath $payload -Value "#!/bin/sh`nexit 1" & /bin/chmod '+x' $payload } + } elseif ($Agent) { + New-AgentFakeBinary $payload $VersionText } else { New-FakeBinary $payload $VersionText } @@ -390,6 +529,9 @@ function New-CaseEnv([string]$Name) { # turn every marker case into a failure that looks like the marker broke. Clear-Env 'DISABLE_TELEMETRY' Clear-Env 'MAPBOX_CLI_NO_TELEMETRY' + Clear-Env 'MAPBOX_CLI_NO_AGENT_SETUP' + Clear-Env 'MAPBOX_TEST_INSTALL_MODE' + Clear-Env 'MAPBOX_TEST_UPDATE_MODE' # What install.ps1 reads to decide where it is running. Set explicitly so # the same case means the same thing on Windows and on the machine this is # written on. @@ -429,6 +571,7 @@ $armRoot = Join-Path $Root 'arm-channel' $emptyRoot = Join-Path $Root 'empty-channel' $badShaRoot = Join-Path $Root 'bad-sha-channel' +$agentRoot = Join-Path $Root 'agent-channel' # Beside the channels rather than in one, and nothing ever requests it: this is # where the open server writes the path and User-Agent of every request it is @@ -443,6 +586,7 @@ New-ChannelVersion -Root $foreignRoot -Dir 'latest' -ArtifactName 'aarch64-apple New-ChannelVersion -Root $armRoot -Dir 'latest' -ArtifactName $Arm64Target -VersionText 'mapbox 9.9.9-arm64' New-ChannelVersion -Root $emptyRoot -Dir 'latest' -Empty New-ChannelVersion -Root $badShaRoot -Dir 'latest' -BadSha +New-ChannelVersion -Root $agentRoot -Dir 'latest' -Agent $open = $null $gated = $null @@ -855,6 +999,46 @@ try { Expect-Status 0 'exits 0' Expect-Out 'Tilesets CLI: C:\tools\tilesets.cmd' 'reports what it is pointed at' Expect-NoOut 'does not work natively here' 'and does not explain it again' + + # --- the coding agent skill --------------------------------------------- + # + # Invoke-Installer runs with -NonInteractive, which is what lets it merge + # a child's stderr without Stop turning that into a terminating error - + # but it also makes Read-Host refuse outright ("PowerShell is in + # NonInteractive mode"), regardless of what is actually attached to + # stdin. So only the no-console branch of install.ps1's prompt is + # reachable through this harness; that happens to be exactly what + # Invoke-Installer already simulates for every other case here. The + # answered-yes/answered-no branches were verified by hand instead, + # against a real pseudoterminal (`[Console]::IsInputRedirected` reads + # False there), across all of: declined, fresh install, reinstall with + # nothing changed, and reinstall with a local edit preserved rather than + # overwritten - there is no cross-platform way to allocate a pty from + # pure PowerShell to automate that part here. + + Start-Case 'no coding agent detected: silent, nothing offered' + New-CaseEnv 'no-agent' + Invoke-Installer + Expect-Status 0 'exits 0' + Expect-NoOut 'coding agent' 'says nothing about a step that has nothing to do' + + Start-Case 'no console: the question is skipped, not answered yes' + New-CaseEnv 'agent-no-console' + $env:MAPBOX_CLI_BASE_URL = "$($open.BaseUrl)/agent-channel" + Invoke-Installer + Expect-Status 0 'exits 0' + Expect-Out 'A coding agent was detected on this machine: claude.' 'names the agent' + Expect-Out 'Not set up. Run these any time:' 'gives the manual commands instead of asking' + Expect-Out 'mapbox generate-skills --global' 'the first manual command' + Expect-Out 'mapbox agent-skills install --global' 'the second manual command' + + Start-Case 'MAPBOX_CLI_NO_AGENT_SETUP opts out even though an agent was detected' + New-CaseEnv 'agent-opt-out' + $env:MAPBOX_CLI_BASE_URL = "$($open.BaseUrl)/agent-channel" + $env:MAPBOX_CLI_NO_AGENT_SETUP = '1' + Invoke-Installer + Expect-Status 0 'exits 0' + Expect-NoOut 'coding agent' 'skips the question entirely' } finally { if ($open) { Stop-ChannelServer $open } if ($gated) { Stop-ChannelServer $gated } diff --git a/scripts/test-install.sh b/scripts/test-install.sh index 30ad9b1..4a05352 100755 --- a/scripts/test-install.sh +++ b/scripts/test-install.sh @@ -211,10 +211,20 @@ make_channel() { # dir version [bad-sha|broken] # for another platform. printf '#!/bin/sh\nexit 1\n' >"${channel_dir}/build/mapbox" else + # generate-skills/agent-skills default to "no agent detected" — + # exit 1, the real CLI's own no_agent_detected shape — so every + # scenario built on this fixture reaches the coding-agent-skill + # section of install.sh and finds nothing to do, same as a plain + # machine with no ~/.claude, ~/.codex, etc. Scenarios that need a + # detected agent use make_agent_channel below instead. cat >"${channel_dir}/build/mapbox" <&2 + exit 1 + ;; *) echo "fake mapbox: \$*" ;; esac EOF @@ -263,6 +273,76 @@ CHANNEL="${ROOT}/channel" make_channel "${CHANNEL}/latest" v9.9.9 make_channel "${CHANNEL}/v0.1.0-dev.abc1234" v0.1.0-dev.abc1234 +# A channel whose fake binary reports one detected coding agent and answers +# generate-skills/agent-skills accordingly, driven at runtime (not baked in, +# since the same tarball is reused across every case below) by two +# environment variables: MAPBOX_TEST_INSTALL_MODE (ok | already_installed) +# and MAPBOX_TEST_UPDATE_MODE (clean | dirty, read only once install +# reports already_installed). Defaults match the plain first-install case. +AGENT_CHANNEL="${ROOT}/agent-channel" +mkdir -p "${AGENT_CHANNEL}/latest/build" +cat >"${AGENT_CHANNEL}/latest/build/mapbox" <<'EOF' +#!/bin/sh +case "${1:-}" in + --version) echo "mapbox 9.9.9" ;; + generate-skills) + for a in "$@"; do + if [ "$a" = "--dry-run" ]; then + echo '{"written":true,"skills":[{"source":"claude, all projects"}]}' + exit 0 + fi + done + echo '{"written":true}' + ;; + agent-skills) + sub="$2" + case "$sub" in + install) + if [ "${MAPBOX_TEST_INSTALL_MODE:-ok}" = already_installed ]; then + echo '{"code":"already_installed"}' >&2 + exit 1 + fi + echo '{"installed":true}' + ;; + update) + is_dry=no + for a in "$@"; do [ "$a" = "--dry-run" ] && is_dry=yes; done + if [ "$is_dry" = yes ]; then + if [ "${MAPBOX_TEST_UPDATE_MODE:-clean}" = dirty ]; then + echo '{"updated":["some-skill"],"unchanged":[]}' + else + echo '{"updated":[],"unchanged":["some-skill"]}' + fi + else + echo '{"updated":["some-skill"]}' + fi + ;; + esac + ;; + *) echo "fake mapbox: $*" ;; +esac +EOF +chmod 755 "${AGENT_CHANNEL}/latest/build/mapbox" +tar czf "${AGENT_CHANNEL}/latest/mapbox-v9.9.9-${TARGET}.tar.gz" \ + -C "${AGENT_CHANNEL}/latest/build" mapbox +rm -rf "${AGENT_CHANNEL}/latest/build" +agent_real_sha="$(sha256_of "${AGENT_CHANNEL}/latest/mapbox-v9.9.9-${TARGET}.tar.gz")" +cat >"${AGENT_CHANNEL}/latest/manifest.json" <"${AGENT_CHANNEL}/latest/SHA256SUMS" + # A channel whose manifest advertises a checksum the tarball does not have. BAD_SHA="${ROOT}/bad-sha-channel" make_channel "${BAD_SHA}/latest" v9.9.9 \ @@ -534,6 +614,7 @@ new_case_env() { # case-name # A developer with either of these set in their own shell would otherwise # turn every marker case into a failure that looks like the marker broke. unset DISABLE_TELEMETRY MAPBOX_CLI_NO_TELEMETRY + unset MAPBOX_CLI_NO_AGENT_SETUP MAPBOX_TEST_INSTALL_MODE MAPBOX_TEST_UPDATE_MODE export MAPBOX_CLI_BASE_URL MAPBOX_INSTALL_DIR PATH } @@ -1067,6 +1148,80 @@ expect_status 0 "$status" 'a broken override is not an install failure' expect_out 'where there is no' 'says the override points nowhere' expect_no_out 'fake pipx' 'does not install over a deliberate override' +# --- the coding agent skill ------------------------------------------------- + +start 'no coding agent detected: silent, nothing offered' +new_case_env no-agent +export MAPBOX_INSTALL_TILESETS=no +run_piped && status=0 || status=$? +expect_status 0 "$status" 'exits 0' +expect_no_out 'coding agent' 'says nothing about a step that has nothing to do' + +start 'MAPBOX_CLI_NO_AGENT_SETUP opts out even though an agent was detected' +new_case_env agent-opt-out +export MAPBOX_CLI_BASE_URL="file://${AGENT_CHANNEL}" +export MAPBOX_INSTALL_TILESETS=no +export MAPBOX_CLI_NO_AGENT_SETUP=1 +run_piped && status=0 || status=$? +expect_status 0 "$status" 'exits 0' +expect_no_out 'coding agent' 'skips the question entirely' + +start 'no terminal: the question is skipped, not answered yes' +new_case_env agent-no-tty +export MAPBOX_CLI_BASE_URL="file://${AGENT_CHANNEL}" +export MAPBOX_INSTALL_TILESETS=no +run_piped && status=0 || status=$? +expect_status 0 "$status" 'exits 0 rather than blocking (124 would be a hang)' +expect_out 'A coding agent was detected on this machine: claude.' 'names the agent' +expect_out 'Not set up. Run these any time:' 'gives the manual commands instead of asking' +expect_out 'mapbox generate-skills --global' 'the first manual command' +expect_out 'mapbox agent-skills install --global' 'the second manual command' + +start 'a terminal, answered no' +new_case_env agent-tty-no +export MAPBOX_CLI_BASE_URL="file://${AGENT_CHANNEL}" +export MAPBOX_INSTALL_TILESETS=no +run_interactive n && status=0 || status=$? +expect_status 0 "$status" 'a declined setup is not an install failure' +expect_out 'A coding agent was detected on this machine: claude.' 'names the agent' +expect_out 'Set up the mapbox CLI skill' 'asks' +expect_out 'Not set up. Run these any time:' 'falls back to the manual commands' + +start 'a terminal, answered yes: fresh install' +new_case_env agent-tty-yes-fresh +export MAPBOX_CLI_BASE_URL="file://${AGENT_CHANNEL}" +export MAPBOX_INSTALL_TILESETS=no +export MAPBOX_TEST_INSTALL_MODE=ok +run_interactive y && status=0 || status=$? +expect_status 0 "$status" 'exits 0' +expect_out 'Wrote the mapbox CLI skill for: claude.' 'reports the skill written' +expect_out 'Installed the Mapbox Agent Skills library for: claude.' 'reports the library installed' + +start 'a terminal, answered yes: reinstall with nothing locally changed' +new_case_env agent-tty-yes-reinstall-clean +export MAPBOX_CLI_BASE_URL="file://${AGENT_CHANNEL}" +export MAPBOX_INSTALL_TILESETS=no +export MAPBOX_TEST_INSTALL_MODE=already_installed +export MAPBOX_TEST_UPDATE_MODE=clean +run_interactive y && status=0 || status=$? +expect_status 0 "$status" 'exits 0' +expect_out 'Updated the Mapbox Agent Skills library for: claude.' 'updates in place' +expect_no_out 'have local changes and were left alone' 'nothing was skipped' + +start 'a terminal, answered yes: reinstall with a local edit is never overwritten' +new_case_env agent-tty-yes-reinstall-dirty +export MAPBOX_CLI_BASE_URL="file://${AGENT_CHANNEL}" +export MAPBOX_INSTALL_TILESETS=no +export MAPBOX_TEST_INSTALL_MODE=already_installed +export MAPBOX_TEST_UPDATE_MODE=dirty +run_interactive y && status=0 || status=$? +expect_status 0 "$status" 'exits 0' +expect_out 'Some Mapbox Agent Skills have local changes and were left alone.' \ + 'reports the local edit rather than discarding it' +expect_out "Run 'mapbox agent-skills update --global' to review and replace them." \ + 'points at the command to review it by hand' +expect_no_out 'Updated the Mapbox Agent Skills library' 'does not claim to have updated it' + # --- result ---------------------------------------------------------------- printf '\n'