Skip to content
Open
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
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,26 @@ 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
`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.
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name>` 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`
Expand Down
116 changes: 116 additions & 0 deletions scripts/install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
148 changes: 124 additions & 24 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<EOF

To set it up later — the same instructions mapbox tilesets-cli prints when it
cannot find it:

pipx install mapbox-tilesets # recommended, keeps it isolated
python3 -m pip install --user mapbox-tilesets

If it lands somewhere not on your PATH, point the CLI straight at it:

export MAPBOX_TILESETS_CLI=/path/to/tilesets

Docs: https://github.com/mapbox/tilesets-cli
EOF
}

# Under `curl ... | sh` stdin is the script, so a prompt has to come from the
# terminal directly. /dev/tty exists as a device inside a container with no
# terminal attached and fails only on open, so open it to find out.
# terminal attached and fails only on open, so open it to find out. Shared by
# both optional steps below, Tilesets and the coding agent skill — neither
# owns it.
have_tty() {
[ -c /dev/tty ] || return 1
(exec 3</dev/tty) 2>/dev/null
Expand Down Expand Up @@ -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 "<Agent label>, 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 <json> : 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); then
agent_names="$(agent_names_from "$agent_check")"
echo ""
echo "A coding agent was detected on this machine: ${agent_names}."
do_agent_setup=no
if have_tty; then
echo ""
answer="$(ask 'Set up the mapbox CLI skill and the Mapbox Agent Skills library for it? [y/N] ')" || answer=''
is_yes "$answer" && do_agent_setup=yes
fi
if [ "$do_agent_setup" = yes ]; then
# 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.
if "${INSTALL_DIR}/mapbox" generate-skills --global >/dev/null 2>&1 </dev/null; then
echo "Wrote the mapbox CLI skill for: ${agent_names}."
fi

if agent_out=$("${INSTALL_DIR}/mapbox" agent-skills install --global -o json 2>&1 </dev/null); then
echo "Installed the Mapbox Agent Skills library for: ${agent_names}."
else
case "$agent_out" in
# A reinstall/upgrade: the skill directory is already
# there from a previous run of this same script.
# `update` is the one safe to repeat, but only once it
# is 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.
*'"code":"already_installed"'*)
if update_check=$("${INSTALL_DIR}/mapbox" agent-skills update --global --dry-run -o json 2>/dev/null </dev/null); then
case "$update_check" in
*'"updated":[]'*)
"${INSTALL_DIR}/mapbox" agent-skills update --global >/dev/null 2>&1 </dev/null || true
echo "Updated the Mapbox Agent Skills library for: ${agent_names}."
;;
*)
echo "Some Mapbox Agent Skills have local changes and were left alone."
echo "Run 'mapbox agent-skills update --global' to review and replace them."
;;
esac
fi
;;
esac
fi
else
echo "Not set up. Run these any time:"
echo " mapbox generate-skills --global"
echo " mapbox agent-skills install --global"
fi
fi
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 <<EOF

To set it up later — the same instructions mapbox tilesets-cli prints when it
cannot find it:

pipx install mapbox-tilesets # recommended, keeps it isolated
python3 -m pip install --user mapbox-tilesets

If it lands somewhere not on your PATH, point the CLI straight at it:

export MAPBOX_TILESETS_CLI=/path/to/tilesets

Docs: https://github.com/mapbox/tilesets-cli
EOF
}

# `src/tilesets_cli.rs`'s `launch_failed` names this same requirement when
# `tilesets` can't be found; change one and change the other.
python3_at_least_310() {
Expand Down
Loading
Loading