@leeguoo/wrangler-accounts 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "leeguoo-tools",
3
+ "owner": {
4
+ "name": "Lee Guo"
5
+ },
6
+ "metadata": {
7
+ "description": "Claude Code plugins for practical developer workflows from the wrangler-accounts author.",
8
+ "version": "1.5.0"
9
+ },
10
+ "plugins": [
11
+ {
12
+ "name": "wrangler-accounts",
13
+ "source": "./plugins/wrangler-accounts",
14
+ "description": "Cloudflare Wrangler multi-account skill plus a Bash hook that blocks raw wrangler calls when local profiles are configured.",
15
+ "version": "1.5.0",
16
+ "author": {
17
+ "name": "Lee Guo"
18
+ },
19
+ "homepage": "https://github.com/leeguooooo/wrangler-accounts#readme",
20
+ "repository": "https://github.com/leeguooooo/wrangler-accounts",
21
+ "license": "MIT",
22
+ "keywords": [
23
+ "cloudflare",
24
+ "wrangler",
25
+ "accounts",
26
+ "multi-account",
27
+ "claude-code",
28
+ "hook",
29
+ "skill"
30
+ ]
31
+ }
32
+ ]
33
+ }
package/README.md CHANGED
@@ -20,19 +20,48 @@ wrangler-accounts --profile work tail my-worker
20
20
  wrangler-accounts --profile personal dev
21
21
  ```
22
22
 
23
+ ## Claude Code users (recommended)
24
+
25
+ Install the marketplace and plugin first:
26
+
27
+ ```text
28
+ /plugin marketplace add leeguooooo/wrangler-accounts
29
+ /plugin install wrangler-accounts@leeguoo-tools
30
+ ```
31
+
32
+ The plugin bundles the `wrangler-accounts` skill plus a Bash `PreToolUse` hook, so Claude Code can learn the workflow and block raw `wrangler` calls before they bypass your local profile isolation. No manual `settings.json` editing is required.
33
+
34
+ The plugin does **not** install the CLI binary. Keep the npm package on your `PATH`:
35
+
36
+ ```bash
37
+ npm i -g @leeguoo/wrangler-accounts
38
+ ```
39
+
40
+ The guard only blocks direct `wrangler ...` Bash commands when all of these are true:
41
+
42
+ - `wrangler-accounts` is installed on `PATH`
43
+ - at least one local profile is configured
44
+ - the current directory looks like a Cloudflare project (`wrangler.toml`, `wrangler.json`, or `wrangler.jsonc`) or Cloudflare API env vars are set
45
+
46
+ If you explicitly want raw `wrangler`, bypass the hook for that command:
47
+
48
+ ```bash
49
+ NOWRANGLER_ACCOUNTS_GUARD=1 wrangler deploy
50
+ ```
51
+
23
52
  ## What it does
24
53
 
25
54
  Every execution runs `wrangler` inside a per-invocation **shadow HOME** — a temporary directory that mirrors most of your real home, except `.wrangler/config/default.toml` is a symlink pointing at the saved profile's config. Token refreshes flow back to the profile automatically. Nothing touches your real `~/.wrangler`. Two parallel invocations get two independent shadow HOMEs.
26
55
 
27
56
  ## Install
28
57
 
29
- You need **two** things to get the full experience — the CLI binary (what actually runs) and the AI agent skill (what teaches your AI coding agent how to use it). Install both:
58
+ If you do **not** use Claude Code plugins, install the CLI binary and the mirrored `skills.sh` skill separately:
30
59
 
31
60
  ```bash
32
61
  # 1. The CLI (always required)
33
62
  npm i -g @leeguoo/wrangler-accounts
34
63
 
35
- # 2. The AI agent skill (recommended if you use Claude Code / Cursor / Codex / etc.)
64
+ # 2. The AI agent skill mirror (recommended if you use Cursor / Codex / Gemini CLI / etc.)
36
65
  npx skills add leeguooooo/wrangler-accounts -g -y
37
66
  ```
38
67
 
@@ -47,9 +76,9 @@ The CLI works standalone — you can skip step 2 if you don't use an AI coding a
47
76
 
48
77
  The AI will tell you to run commands like `wrangler-accounts --profile work deploy`; without step 1 those commands fail with "command not found".
49
78
 
50
- ### AI agent skill — more options
79
+ ### `skills.sh` mirror — more options
51
80
 
52
- The repo ships an Agent Skill (`skills/wrangler-accounts/SKILL.md`) with multi-account deploy recipes, troubleshooting, CI guidance, and invariants AI can rely on. [skills.sh](https://skills.sh)'s CLI auto-detects which agent you use (Claude Code, Cursor, Codex, Gemini CLI, OpenCode, and 40+ others) and installs to the right directory.
81
+ The repo keeps a mirrored Agent Skill at `skills/wrangler-accounts/SKILL.md` for [skills.sh](https://skills.sh). Claude Code users should prefer the plugin marketplace flow above; other agents can keep using `skills.sh` to install the same guidance markdown into their own skill directories.
53
82
 
54
83
  ```bash
55
84
  npx skills add leeguooooo/wrangler-accounts -g -y # user-global, non-interactive
package/package.json CHANGED
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "name": "@leeguoo/wrangler-accounts",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Cloudflare Wrangler multi-account manager — save, switch, and run wrangler against multiple Cloudflare Workers accounts with AWS-style --profile and per-invocation shadow HOME isolation.",
5
5
  "license": "MIT",
6
6
  "bin": {
7
7
  "wrangler-accounts": "bin/wrangler-accounts.js"
8
8
  },
9
9
  "files": [
10
+ ".claude-plugin",
10
11
  "bin",
11
12
  "lib",
12
13
  "completions",
14
+ "plugins",
13
15
  "skills"
14
16
  ],
15
17
  "keywords": [
@@ -55,7 +57,8 @@
55
57
  "scripts": {
56
58
  "test": "node --test test/*.test.js",
57
59
  "verify-package": "node scripts/verify-package.js",
58
- "prepublishOnly": "npm test && npm run verify-package"
60
+ "check-skill-sync": "node -e \"const fs=require('fs');const a='skills/wrangler-accounts/SKILL.md';const b='plugins/wrangler-accounts/skills/wrangler-accounts/SKILL.md';if(fs.readFileSync(a,'utf8')!==fs.readFileSync(b,'utf8')){console.error('SKILL.md sync check failed: keep skills/wrangler-accounts/SKILL.md and plugins/wrangler-accounts/skills/wrangler-accounts/SKILL.md identical.');process.exit(1)}\"",
61
+ "prepublishOnly": "npm test && npm run verify-package && npm run check-skill-sync"
59
62
  },
60
63
  "engines": {
61
64
  "node": ">=16"
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "wrangler-accounts",
3
+ "description": "Cloudflare Wrangler multi-account helper with a bundled skill and Bash guard hook for Claude Code.",
4
+ "version": "1.5.0",
5
+ "author": {
6
+ "name": "Lee Guo"
7
+ },
8
+ "homepage": "https://github.com/leeguooooo/wrangler-accounts#readme",
9
+ "repository": "https://github.com/leeguooooo/wrangler-accounts",
10
+ "license": "MIT"
11
+ }
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ INPUT_JSON="$(cat)"
5
+
6
+ parse_with_jq() {
7
+ TOOL_NAME="$(printf '%s' "$INPUT_JSON" | jq -r '.tool_name // empty' 2>/dev/null || true)"
8
+ TOOL_COMMAND="$(printf '%s' "$INPUT_JSON" | jq -r '.tool_input.command // empty' 2>/dev/null || true)"
9
+ }
10
+
11
+ parse_with_python() {
12
+ local parsed
13
+ parsed="$(
14
+ printf '%s' "$INPUT_JSON" | python3 -c '
15
+ import json
16
+ import sys
17
+
18
+ try:
19
+ payload = json.load(sys.stdin)
20
+ except Exception:
21
+ print("")
22
+ print("")
23
+ raise SystemExit(0)
24
+
25
+ print(payload.get("tool_name", ""))
26
+ print(payload.get("tool_input", {}).get("command", ""))
27
+ ' 2>/dev/null || true
28
+ )"
29
+ TOOL_NAME="$(printf '%s\n' "$parsed" | sed -n '1p')"
30
+ TOOL_COMMAND="$(printf '%s\n' "$parsed" | sed -n '2,$p')"
31
+ }
32
+
33
+ TOOL_NAME=""
34
+ TOOL_COMMAND=""
35
+
36
+ if command -v jq >/dev/null 2>&1; then
37
+ parse_with_jq
38
+ elif command -v python3 >/dev/null 2>&1; then
39
+ parse_with_python
40
+ else
41
+ exit 0
42
+ fi
43
+
44
+ [ "${TOOL_NAME:-}" = "Bash" ] || exit 0
45
+ [ -n "${TOOL_COMMAND:-}" ] || exit 0
46
+
47
+ if [ "${NOWRANGLER_ACCOUNTS_GUARD:-}" = "1" ]; then
48
+ exit 0
49
+ fi
50
+
51
+ case "$TOOL_COMMAND" in
52
+ *NOWRANGLER_ACCOUNTS_GUARD=1*)
53
+ exit 0
54
+ ;;
55
+ esac
56
+
57
+ if [ -n "${CLOUDFLARE_API_TOKEN:-}" ] || [ -n "${CLOUDFLARE_ACCOUNT_ID:-}" ]; then
58
+ :
59
+ elif [ -f "wrangler.toml" ] || [ -f "wrangler.jsonc" ] || [ -f "wrangler.json" ]; then
60
+ :
61
+ else
62
+ exit 0
63
+ fi
64
+
65
+ if printf '%s' "$TOOL_COMMAND" | grep -Eq '(^|[[:space:];(|&])(npx|pnpm|yarn|bunx)[[:space:]]+wrangler[[:space:]]'; then
66
+ exit 0
67
+ fi
68
+
69
+ if ! printf '%s' "$TOOL_COMMAND" | grep -Eq '(^|[[:space:];(|&])wrangler[[:space:]]'; then
70
+ exit 0
71
+ fi
72
+
73
+ if ! command -v wrangler-accounts >/dev/null 2>&1; then
74
+ exit 0
75
+ fi
76
+
77
+ PROFILES="$(wrangler-accounts list --plain 2>/dev/null || true)"
78
+ [ -n "$PROFILES" ] || exit 0
79
+
80
+ DEFAULT_PROFILE="$(wrangler-accounts default 2>/dev/null || true)"
81
+
82
+ {
83
+ echo "wrangler-accounts guard: blocked a direct \`wrangler\` command."
84
+ echo
85
+ echo "Direct \`wrangler\` calls bypass wrangler-accounts profile isolation when local profiles are configured."
86
+ echo "Retry with one of these forms instead:"
87
+ echo
88
+ echo " wrangler-accounts --profile <name> <wrangler-args...>"
89
+ echo " wrangler-accounts exec <name> -- <your-original-command>"
90
+ echo
91
+ echo "Configured profiles:"
92
+ printf '%s\n' "$PROFILES" | sed 's/^/ - /'
93
+ echo "Default profile: ${DEFAULT_PROFILE:-"(none)"}"
94
+ echo
95
+ echo "If the user explicitly wants raw wrangler, prepend NOWRANGLER_ACCOUNTS_GUARD=1."
96
+ } >&2
97
+
98
+ exit 2
@@ -0,0 +1,15 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "Bash",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/guard-wrangler.sh"
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }
@@ -0,0 +1,452 @@
1
+ ---
2
+ name: wrangler-accounts
3
+ description: AWS-style multi-account convenience for Cloudflare Wrangler. Use when you need to run wrangler commands against a specific Cloudflare account, manage saved OAuth profiles, set or switch the persistent default profile, or open an isolated subshell for a profile. Prefer --json for machine-readable output.
4
+ ---
5
+
6
+ # Wrangler Accounts
7
+
8
+ ## Overview
9
+
10
+ `wrangler-accounts` runs `wrangler` under per-invocation **shadow HOME** isolation, so multiple shells can use different Cloudflare accounts in parallel without any global switching. Profile resolution order: `--profile` / `-p` > positional shorthand > `$WRANGLER_PROFILE` > `profilesDir/default` > hard error.
11
+
12
+ ## Installation
13
+
14
+ For Claude Code users, prefer the plugin marketplace install path because it ships this skill and the raw-`wrangler` guard hook together:
15
+
16
+ ```text
17
+ /plugin marketplace add leeguooooo/wrangler-accounts
18
+ /plugin install wrangler-accounts@leeguoo-tools
19
+ ```
20
+
21
+ The plugin does **not** replace the CLI binary. The actual `wrangler-accounts` executable still must be installed on `PATH`:
22
+
23
+ ```bash
24
+ npm i -g @leeguoo/wrangler-accounts
25
+ ```
26
+
27
+ Non-Claude-Code users can keep using the `skills.sh` distribution path for this same `SKILL.md` mirror:
28
+
29
+ ```bash
30
+ npx skills add leeguooooo/wrangler-accounts -g -y
31
+ ```
32
+
33
+ ## Prerequisites (check before running any recipe below)
34
+
35
+ This skill is only documentation — the actual `wrangler-accounts` binary must also be installed on the user's `PATH`. Before running any command below, verify:
36
+
37
+ ```bash
38
+ command -v wrangler-accounts && wrangler-accounts --version
39
+ ```
40
+
41
+ If the command is missing, tell the user to install the CLI first:
42
+
43
+ ```bash
44
+ npm i -g @leeguoo/wrangler-accounts
45
+ ```
46
+
47
+ `wrangler` itself (the Cloudflare CLI) must also be on `PATH`. If missing:
48
+
49
+ ```bash
50
+ npm i -g wrangler
51
+ ```
52
+
53
+ ### Minimum versions you should ask the user to upgrade past
54
+
55
+ If `wrangler-accounts --version` is below any of these, **upgrade first** before debugging anything else — older versions have real bugs that will misdirect you:
56
+
57
+ | Version | What it fixed | Symptom on older versions |
58
+ |---|---|---|
59
+ | **≥ 1.2.2** | per-profile `WRANGLER_CACHE_DIR`, fixes account-id leak across profiles | `d1`/`r2 object` commands return `7403 not authorized` even though OAuth is valid; deploys silently land in the wrong account |
60
+ | **≥ 1.3.0** | STATUS column distinguishes `valid` / `valid*` / `EXPIRED` (refresh-token-aware) | `list` shows `EXPIRED` for healthy profiles, scaring you into running `login` for no reason |
61
+ | **≥ 1.4.0** | `login` refuses non-TTY contexts and accidental overwrites | `login <name>` hangs forever in non-interactive contexts; reflexive `login` overwrites a healthy profile |
62
+
63
+ ```bash
64
+ npm i -g @leeguoo/wrangler-accounts@latest # always-safe upgrade
65
+ ```
66
+
67
+ ## Triage flow — when something looks wrong
68
+
69
+ **Run these in order before reaching for `login` or any destructive command.** The agent who skipped this step in a recent incident ended up overwriting a perfectly healthy profile in a sub-shell where the browser couldn't even open.
70
+
71
+ ```bash
72
+ # 1. Verify your binary is recent enough (see version table above)
73
+ wrangler-accounts --version
74
+
75
+ # 2. Authoritative state of every profile (hits Cloudflare API per profile)
76
+ wrangler-accounts list --deep
77
+ ```
78
+
79
+ How to read `list --deep` output:
80
+
81
+ | You see | Meaning | What to do |
82
+ |---|---|---|
83
+ | `STATUS valid` + `VERIFIED ✓ ok` | profile is fine | nothing — proceed with whatever the user actually asked |
84
+ | `STATUS valid*` + `VERIFIED ✓ ok` | profile is fine; access token will auto-refresh on next use | nothing — **`valid*` is healthy, do NOT re-login** |
85
+ | `STATUS EXPIRED` + `VERIFIED ✓ ok` | rare; only on `< 1.3.0` binaries — STATUS is lying | upgrade the CLI; profile is fine |
86
+ | `STATUS EXPIRED` + `VERIFIED ✗ Not logged in` | profile is genuinely broken | `wrangler-accounts login <name>` (interactive only) |
87
+ | `STATUS valid` + `VERIFIED ✗` of any kind | refresh token revoked server-side | `wrangler-accounts login <name>` (interactive only) |
88
+ | `STATUS unknown` | profile file lacks `expiration_time` | run `--deep` (already did) — trust VERIFIED |
89
+
90
+ **Rule**: only suggest `wrangler-accounts login <name>` when at least one of:
91
+ 1. `list --deep` showed `VERIFIED ✗` for that profile, OR
92
+ 2. The profile doesn't exist at all yet, OR
93
+ 3. The user explicitly says "re-authenticate" / "log me in again"
94
+
95
+ **If `list --deep` shows everything ✓ but a wrangler command still fails**, the root cause is somewhere other than wrangler-accounts — most likely:
96
+ - Project-local `./.wrangler/state/` is sharing data across profiles (see "What is and isn't isolated")
97
+ - The project's `wrangler.toml` has the wrong `account_id` hardcoded
98
+ - The user is on a CLI version below 1.2.2 (account-id cache leak)
99
+ - The wrangler subcommand actually needs `--remote` or `--local` and you forgot
100
+
101
+ ## Quick Start
102
+
103
+ - `wrangler-accounts login <name>` — interactive OAuth login into a new profile (never touches real `~/.wrangler`)
104
+ - `wrangler-accounts default <name>` — set the persistent default profile
105
+ - `wrangler-accounts deploy` — run `wrangler deploy` under the default profile
106
+ - `wrangler-accounts --profile personal deploy` — one-shot override
107
+ - `wrangler-accounts exec work -- npm run release` — run a command in an isolated subshell for the `work` profile
108
+
109
+ > 💡 **Reading the STATUS column**: `valid` = healthy, `valid*` = healthy (will auto-refresh on next use, **don't re-login**), `EXPIRED` = truly broken (need login), `unknown` = run `--deep` to find out. Full table below in "List and inspect profiles".
110
+
111
+ ## Tasks
112
+
113
+ ### Run wrangler against a profile
114
+
115
+ Per-invocation (preferred for scripts):
116
+
117
+ `wrangler-accounts --profile <name> <wrangler-args...>`
118
+
119
+ Or with env var:
120
+
121
+ `WRANGLER_PROFILE=<name> wrangler-accounts <wrangler-args...>`
122
+
123
+ Or positional shorthand (only when `<name>` is a saved profile name, not a management subcommand):
124
+
125
+ `wrangler-accounts <name> <wrangler-args...>`
126
+
127
+ ### Open a subshell for a profile
128
+
129
+ `wrangler-accounts exec <name>` — launches `$SHELL -i` with isolated `HOME` and `WRANGLER_PROFILE` set. Everything inside the subshell sees the profile, including nested `npm run` scripts, Makefiles, and `npx wrangler`.
130
+
131
+ Run a single command instead:
132
+
133
+ `wrangler-accounts exec <name> -- <cmd> [args]`
134
+
135
+ ### Manage the persistent default profile
136
+
137
+ - `wrangler-accounts default` — print current default (exit 1 if none set)
138
+ - `wrangler-accounts default <name>` — set the default
139
+ - `wrangler-accounts default --unset` — clear the default
140
+ - `wrangler-accounts default --json` — JSON output
141
+
142
+ ### Show the resolved identity for a profile
143
+
144
+ `wrangler-accounts whoami [--profile <name>]` — reports the profile name, source tier (cli / positional / env / default), and identity from `meta.json`. Does not spawn wrangler.
145
+
146
+ Use `--json` for structured output.
147
+
148
+ ### List and inspect profiles
149
+
150
+ - `wrangler-accounts list` — text table with NAME / STATUS / EXPIRES / IDENTITY columns
151
+ - `wrangler-accounts list --json` — structured: array of `{name, isDefault, isActive, status, expirationTime, hasRefreshToken, identity, verified, verifyError}`
152
+ - `wrangler-accounts list --plain` — one profile name per line (scriptable)
153
+ - `wrangler-accounts list --deep` — **authoritative** check: spawns `wrangler whoami` in a shadow HOME for every profile and reports whether Cloudflare actually accepts the credentials. Slower (makes network calls), but the only way to catch revoked refresh tokens or broken profile files.
154
+ - `wrangler-accounts status` / `status --json`
155
+ - Pass `--include-backups` to show hidden backup profiles.
156
+
157
+ **STATUS values (1.3.0+):**
158
+
159
+ | value | meaning | user action |
160
+ |---|---|---|
161
+ | `valid` | access_token is currently valid | none |
162
+ | `valid*` / `refreshable` | access_token past expiry **BUT** refresh_token present; wrangler will auto-refresh on next use | **none** — this is fine, don't scare the user |
163
+ | `EXPIRED` / `expired` | access_token expired **AND** no refresh_token saved; profile is genuinely broken | `wrangler-accounts login <name>` |
164
+ | `unknown` | profile file has no `expiration_time` field | run `list --deep` to verify live |
165
+
166
+ **Cloudflare OAuth lifecycle reference:** access tokens are short-lived (~1 hour) by design. Every profile with `offline_access` in its scopes also has a long-lived refresh_token (~30 days, silently extended on use). Wrangler refreshes access tokens automatically whenever it runs a command and the current one is past expiry. **Do not tell the user to re-login just because `list` shows an expired access token** — check `hasRefreshToken` first. If the profile's STATUS is `valid*` / `refreshable`, nothing is wrong.
167
+
168
+ The only time a user actually needs `wrangler-accounts login <name>` again is:
169
+ 1. STATUS is `EXPIRED` (no refresh_token at all — profile was saved without `offline_access` scope)
170
+ 2. OR `list --deep` returns `✗` with "Not logged in" / "refresh token may be revoked" (refresh token itself got invalidated)
171
+
172
+ ### Save, sync, login, remove
173
+
174
+ - `wrangler-accounts save <name>` — snapshot current Wrangler config as a profile
175
+ - `wrangler-accounts sync <name>` — refresh a specific profile from the current login
176
+ - `wrangler-accounts sync-default` — refresh the default profile
177
+ - `wrangler-accounts login <name>` — fresh isolated OAuth login
178
+ - `wrangler-accounts remove <name>` — delete a profile
179
+
180
+ ### Clean up stale shadow HOMEs
181
+
182
+ `wrangler-accounts gc [--older-than 1h]` — removes `wa-*` directories under `$TMPDIR` older than the threshold (default 1h). Safe to run at any time.
183
+
184
+ ## Common Recipes
185
+
186
+ These are the patterns the user is most likely asking about when they mention "Cloudflare accounts", "wrangler", or "multi-account deploys". Pick the one that matches intent.
187
+
188
+ ### User wants: deploy a worker to a specific account
189
+
190
+ ```bash
191
+ wrangler-accounts --profile work deploy
192
+ ```
193
+
194
+ Or, when the user will be on this account for a while:
195
+
196
+ ```bash
197
+ wrangler-accounts default work # set once
198
+ wrangler-accounts deploy # uses work from now on
199
+ wrangler-accounts deploy --env staging # wrangler flags pass through unchanged
200
+ ```
201
+
202
+ ### User wants: tail production logs on one account while developing on another
203
+
204
+ ```bash
205
+ # shell 1
206
+ wrangler-accounts --profile work tail my-worker --format pretty
207
+
208
+ # shell 2 (simultaneously, zero interference)
209
+ wrangler-accounts --profile personal dev
210
+ ```
211
+
212
+ The two shells each get their own shadow `HOME`, so there's no global state for the other to clobber.
213
+
214
+ ### User wants: run a deploy script / npm script / Makefile against a specific account
215
+
216
+ ```bash
217
+ wrangler-accounts exec work -- npm run deploy
218
+ wrangler-accounts exec work -- make release
219
+ wrangler-accounts exec work -- bash scripts/deploy.sh
220
+ ```
221
+
222
+ Anything inside the subshell that calls `wrangler` (directly, via `npx`, via `pnpm`, via a package.json script) automatically uses the `work` profile.
223
+
224
+ ### User wants: set up a new Cloudflare account from scratch
225
+
226
+ ```bash
227
+ wrangler-accounts login new-account # opens the browser OAuth flow
228
+ wrangler-accounts whoami --profile new-account # verify identity
229
+ wrangler-accounts list # confirm the profile is saved
230
+ ```
231
+
232
+ The login flow runs inside an isolated shadow `HOME`, so the user's real `~/.wrangler/config/default.toml` is never touched.
233
+
234
+ > ⚠️ **`login` is destructive.** It opens a browser, requires the user to click "Authorize" interactively, and **OVERWRITES** the named profile. As of 1.4.0, `wrangler-accounts login <name>` refuses to run if (a) stdin is not a TTY, or (b) the profile already exists and looks healthy — both unless you pass `--force`. **Never run `login` to "verify" or "refresh" a profile** — see the antipattern below.
235
+
236
+ ### ❌ Antipattern: running `login` to verify a profile works
237
+
238
+ This is wrong:
239
+ ```bash
240
+ wrangler-accounts login Xdreamstar2025 # ❌ DON'T do this just to check
241
+ ```
242
+
243
+ Reasons:
244
+ 1. `login` is **destructive** — it overwrites the saved profile with a brand new OAuth flow.
245
+ 2. `login` requires a **browser and an interactive terminal** — it cannot complete in a Bash sub-shell, CI runner, or sub-agent context. The command will hang waiting for the user.
246
+ 3. The Cloudflare access token in a healthy profile auto-refreshes via `refresh_token` — there is **nothing to "log in to"** when the profile already works.
247
+
248
+ Use one of these instead:
249
+
250
+ ```bash
251
+ wrangler-accounts whoami --profile Xdreamstar2025 # fast, reads meta.json, no network
252
+ wrangler-accounts list --deep # authoritative, runs wrangler whoami per profile
253
+ wrangler-accounts list # quick STATUS overview (valid / valid* / EXPIRED / unknown)
254
+ ```
255
+
256
+ Only fall back to `wrangler-accounts login <name>` when:
257
+ - The profile **does not exist yet** (creating a new account profile from scratch)
258
+ - The profile shows `EXPIRED` (truly expired, no refresh_token left) — see STATUS table above
259
+ - `list --deep` returns `✗` with "Not logged in" / "refresh token may be revoked" (server-side revocation)
260
+ - The user **explicitly says** "re-authenticate this profile" / "log me in again"
261
+
262
+ ### User wants: check which account a profile is tied to, without running wrangler
263
+
264
+ ```bash
265
+ wrangler-accounts whoami --profile <name> # text
266
+ wrangler-accounts whoami --profile <name> --json # structured
267
+ ```
268
+
269
+ Returns the email + account ID from the saved `meta.json`. No network call.
270
+
271
+ ### User wants: swap between many accounts quickly in one shell
272
+
273
+ Use `default` as a "current" setting:
274
+
275
+ ```bash
276
+ wrangler-accounts default work
277
+ wrangler-accounts deploy # work
278
+ wrangler-accounts default personal
279
+ wrangler-accounts dev # personal
280
+ ```
281
+
282
+ Or use positional shorthand inline:
283
+
284
+ ```bash
285
+ wrangler-accounts work deploy # one-shot, no persistent default
286
+ wrangler-accounts personal dev
287
+ ```
288
+
289
+ ### User wants: run wrangler in CI with multiple accounts
290
+
291
+ **Don't use `wrangler-accounts` in CI.** Use native env vars:
292
+
293
+ ```bash
294
+ # In CI secrets:
295
+ CLOUDFLARE_API_TOKEN=<token-with-workers-deploy-scope>
296
+ CLOUDFLARE_ACCOUNT_ID=<account-id>
297
+
298
+ # In the pipeline:
299
+ wrangler deploy
300
+ ```
301
+
302
+ `wrangler-accounts` is for **local developer** OAuth sessions. CI should use long-lived API tokens directly with plain `wrangler`. Recommend this even if the user asks to use `wrangler-accounts` in CI.
303
+
304
+ ## Troubleshooting
305
+
306
+ ### "Profile 'X' has expired Wrangler OAuth credentials"
307
+
308
+ The saved OAuth access token is past its `expiration_time` and the refresh didn't happen (or Wrangler is older than 4.x). Refresh it interactively:
309
+
310
+ ```bash
311
+ wrangler-accounts login <name>
312
+ ```
313
+
314
+ This overwrites the existing profile with a fresh OAuth session. Any saved metadata (identity, etc.) is re-verified.
315
+
316
+ ### "No profile specified. Options: ..."
317
+
318
+ The user ran `wrangler-accounts <wrangler-args>` without a resolvable profile. Fix one of:
319
+
320
+ ```bash
321
+ wrangler-accounts --profile <name> <args> # one-shot
322
+ WRANGLER_PROFILE=<name> wrangler-accounts <args> # env var
323
+ wrangler-accounts default <name> # persistent default
324
+ ```
325
+
326
+ ### "Profile not found: X"
327
+
328
+ The profile name doesn't exist in `profilesDir`. Check what's saved:
329
+
330
+ ```bash
331
+ wrangler-accounts list
332
+ ```
333
+
334
+ Create it with `wrangler-accounts login <name>` or copy an existing Wrangler login with `wrangler-accounts save <name>`.
335
+
336
+ ### Inside `wrangler-accounts exec`, `cd ~` lands in a weird tmpdir
337
+
338
+ Expected. Inside an `exec` session, `$HOME` is the shadow HOME. The real home is still accessible:
339
+
340
+ ```bash
341
+ cd "$WRANGLER_ACCOUNT_REAL_HOME"
342
+ ```
343
+
344
+ Or users can add a shell alias: `alias realhome='cd "$WRANGLER_ACCOUNT_REAL_HOME"'`.
345
+
346
+ ### Deprecated `use` command warning
347
+
348
+ `wrangler-accounts use <name>` still works but prints a deprecation warning. Suggest the replacement based on intent:
349
+
350
+ - "I want this account to stick for a while" → `wrangler-accounts default <name>`
351
+ - "Just this one command" → `wrangler-accounts --profile <name> <wrangler-args>`
352
+
353
+ ### `[ERROR] A request to the Cloudflare API ... Authentication error [code: 10000]` with `code: 7403` ("not authorized to access this service")
354
+
355
+ The OAuth token is fine but **the URL contains the wrong account ID**. wrangler caches the user's selected account in `wrangler-account.json`. If that cache file is shared across profiles, profile A's OAuth token gets paired with profile B's cached account ID, sending API calls to the wrong account. Symptoms:
356
+
357
+ - `deploy` and `secret put` succeed (they don't put account ID in the URL path)
358
+ - `d1 execute --remote`, `r2 object get/put`, anything else with `/accounts/<id>/...` in the URL fails with 7403
359
+
360
+ **Fix path** (in order):
361
+
362
+ 1. **Are you on wrangler-accounts ≥ 1.2.2?** Run `wrangler-accounts --version`. If `< 1.2.2`, upgrade — earlier versions pointed `WRANGLER_CACHE_DIR` at a shared global path. 1.2.2 isolates the cache per profile.
363
+ 2. **Clear the polluted shared cache** (one-time, even after upgrading):
364
+ ```bash
365
+ rm -f ~/.wrangler/cache/wrangler-account.json
366
+ rm -f ~/Library/Preferences/.wrangler/cache/wrangler-account.json # macOS env-paths fallback
367
+ ```
368
+ 3. **Verify with `wrangler-accounts list --deep`** — the VERIFIED column for each profile should be `✓ ok`. If `✗`, the underlying OAuth session itself is broken; run `wrangler-accounts login <name>`.
369
+ 4. **Defense in depth**: set `CLOUDFLARE_ACCOUNT_ID=<correct-id>` in the calling environment. wrangler reads this env var directly and bypasses the cache entirely. Useful for scripts or one-off recovery commands:
370
+ ```bash
371
+ CLOUDFLARE_ACCOUNT_ID=<id> wrangler-accounts <profile> r2 object put ...
372
+ ```
373
+
374
+ **What to tell the user**: "wrangler returned 7403 because it cached the wrong account ID alongside your OAuth token. This was a real bug in wrangler-accounts ≤ 1.2.1 (shared cache directory across profiles). Upgrade to 1.2.2 and clear the polluted cache."
375
+
376
+ ### "The OAuth config seems right, but the wrong account is being used"
377
+
378
+ Same root cause as the 7403 above. Default to the same fix path.
379
+
380
+ ### `wrangler dev` (or any `--local` command) shows stale data after switching profiles
381
+
382
+ Project-local state at `<project>/.wrangler/state/` is **NOT** isolated per profile — wrangler's `getLocalPersistencePath` (cli.js:149025) hardcodes the path next to `wrangler.toml` and the only override is the `--persist-to` CLI flag (no env var hook). So if profile A's `wrangler dev` populated a local D1 emulation, then you switch to profile B and run `wrangler dev` in the same directory, B sees A's emulated rows.
383
+
384
+ This only affects `--local` simulations. **`--remote` commands hit Cloudflare directly and are unaffected** — that's the common case for d1/r2 work in a multi-account setup.
385
+
386
+ Two clean fixes:
387
+
388
+ 1. **Use git worktrees** (recommended for any serious multi-profile dev workflow):
389
+ ```bash
390
+ git worktree add ../my-project-work main
391
+ git worktree add ../my-project-personal main
392
+ cd ../my-project-work && wrangler-accounts exec work # isolated .wrangler/state/
393
+ cd ../my-project-personal && wrangler-accounts exec personal
394
+ ```
395
+ 2. **Clear state manually before switching**:
396
+ ```bash
397
+ rm -rf .wrangler/state
398
+ wrangler-accounts --profile <new> dev
399
+ ```
400
+
401
+ `wrangler-accounts` does not auto-isolate `.wrangler/state/` because the only mechanism would be argv injection of `--persist-to`, which has too many failure modes (different subcommands accept persistTo at different positions, can't override user-supplied flags, path selection is ambiguous between per-profile and per-profile-per-project). The honest tradeoff is documented in the "What is and isn't isolated" table above — partial isolation with hidden gotchas would be worse than honest sharing.
402
+
403
+ ### Shell history / `.zsh_history` seems to grow when running `exec`
404
+
405
+ Intentional. By design the shadow HOME symlinks all top-level entries of real HOME except `.wrangler`, so shell history writes pass through to the real file. This is a **convenience** bias, not a clean-room sandbox — the goal is that `exec` subshells feel like a normal terminal with a different Cloudflare account, not a jail.
406
+
407
+ ## Invariants the AI should rely on
408
+
409
+ - **Real `~/.wrangler/config/default.toml` is never written to by `wrangler-accounts`.** If a user reports that it changed, something else touched it (e.g. a direct `wrangler login` outside `wrangler-accounts`).
410
+ - **Two `wrangler-accounts --profile A` and `wrangler-accounts --profile B` running in parallel never clobber each other on credentials OR account-id cache.** Each gets its own `mkdtemp` shadow HOME, and each gets its own per-profile `WRANGLER_CACHE_DIR` (next to the profile's `config.toml`) so that wrangler's `wrangler-account.json` (which stores the selected Cloudflare account ID) is naturally isolated.
411
+ - **OAuth token refresh inside a profile is automatic.** The shadow HOME contains a symlink from `.wrangler/config/default.toml` to the saved profile file, so Wrangler's in-place `fs.writeFileSync` during `refreshToken()` flows straight back to the profile.
412
+ - **`wrangler-accounts <args>` without a management subcommand forwards everything to wrangler verbatim**, including `--env`, `--dry-run`, `--json`, and any wrangler-native flags. The only flags consumed by `wrangler-accounts` itself are the ones listed in "Paths and environment" below.
413
+
414
+ ## What is and isn't isolated
415
+
416
+ | State | Location | Isolated? |
417
+ |---|---|---|
418
+ | OAuth credentials (`config.toml`) | shadow `$HOME/.wrangler/config/default.toml` → symlink to per-profile file | ✅ per profile |
419
+ | Account-id cache (`wrangler-account.json`) | per-profile `WRANGLER_CACHE_DIR` (= `<profilesDir>/<name>/cache/`) | ✅ per profile |
420
+ | Pages config cache (`pages-config-cache.json`) | same as above | ✅ per profile |
421
+ | Miniflare dev registry | `WRANGLER_REGISTRY_PATH` = `$realHome/.wrangler/registry` | ❌ shared on purpose (cross-profile worker discovery during local dev) |
422
+ | Wrangler debug logs | `WRANGLER_LOG_PATH` = `$realHome/.wrangler/logs` | ❌ shared (append-only, harmless) |
423
+ | Project-local state (`./.wrangler/state/`, `./node_modules/.cache/wrangler`) | inside the project directory | ❌ shared at project level (per-project, but not per-profile) |
424
+ | `cloudflared` binary | `CLOUDFLARED_PATH` or `~/.wrangler/cloudflared/` | ❌ shared (binary, not account-scoped) |
425
+ | Shell history, npm cache, git config, ssh keys | symlinked through to real `$HOME` | ❌ shared by design (so `exec` subshells feel like a normal terminal) |
426
+
427
+ If a user is hitting a "wrong account" symptom and the credentials look right, the most likely culprit is **project-local state** in `./.wrangler/state/` — clear that and re-run.
428
+
429
+ ## CI guidance
430
+
431
+ For CI and deploy pipelines, **use `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` with plain `wrangler`**, not saved OAuth profiles. `wrangler-accounts` is a local developer convenience for juggling OAuth sessions on your workstation, not a CI primitive.
432
+
433
+ ## Paths and environment
434
+
435
+ - `--profile <name>` / `-p <name>` — profile for this invocation (v1.0: `-p` means `--profile`)
436
+ - `--profiles <path>` — profiles directory (long form only since v1.0)
437
+ - `-c, --config <path>` — Wrangler config path
438
+ - `WRANGLER_PROFILE` — profile to use when no `--profile` flag is given
439
+ - `WRANGLER_CONFIG_PATH`, `WRANGLER_ACCOUNTS_DIR`, `XDG_CONFIG_HOME` — path overrides
440
+
441
+ ## Output conventions
442
+
443
+ Use `--json` when another tool needs to parse results. All v1.0 commands that produce structured data support `--json`.
444
+
445
+ ## Naming rules
446
+
447
+ Profile names: letters, numbers, dot, underscore, dash only. Names matching management subcommand names (`exec`, `default`, `whoami`, `gc`, `login`, `list`, `status`, `save`, `sync`, `sync-default`, `remove`, `use`, `sync-active`) cannot be reached via positional shorthand — use `--profile <name>` for those.
448
+
449
+ ## Deprecated
450
+
451
+ - `wrangler-accounts use <name>` — deprecated, prints warning. Use `default <name>` for persistence or `--profile <name>` for one-shot.
452
+ - `wrangler-accounts sync-active` — deprecated alias for `sync-default`.
@@ -9,12 +9,33 @@ description: AWS-style multi-account convenience for Cloudflare Wrangler. Use wh
9
9
 
10
10
  `wrangler-accounts` runs `wrangler` under per-invocation **shadow HOME** isolation, so multiple shells can use different Cloudflare accounts in parallel without any global switching. Profile resolution order: `--profile` / `-p` > positional shorthand > `$WRANGLER_PROFILE` > `profilesDir/default` > hard error.
11
11
 
12
+ ## Installation
13
+
14
+ For Claude Code users, prefer the plugin marketplace install path because it ships this skill and the raw-`wrangler` guard hook together:
15
+
16
+ ```text
17
+ /plugin marketplace add leeguooooo/wrangler-accounts
18
+ /plugin install wrangler-accounts@leeguoo-tools
19
+ ```
20
+
21
+ The plugin does **not** replace the CLI binary. The actual `wrangler-accounts` executable still must be installed on `PATH`:
22
+
23
+ ```bash
24
+ npm i -g @leeguoo/wrangler-accounts
25
+ ```
26
+
27
+ Non-Claude-Code users can keep using the `skills.sh` distribution path for this same `SKILL.md` mirror:
28
+
29
+ ```bash
30
+ npx skills add leeguooooo/wrangler-accounts -g -y
31
+ ```
32
+
12
33
  ## Prerequisites (check before running any recipe below)
13
34
 
14
35
  This skill is only documentation — the actual `wrangler-accounts` binary must also be installed on the user's `PATH`. Before running any command below, verify:
15
36
 
16
37
  ```bash
17
- command -v wrangler-accounts
38
+ command -v wrangler-accounts && wrangler-accounts --version
18
39
  ```
19
40
 
20
41
  If the command is missing, tell the user to install the CLI first:
@@ -29,6 +50,54 @@ npm i -g @leeguoo/wrangler-accounts
29
50
  npm i -g wrangler
30
51
  ```
31
52
 
53
+ ### Minimum versions you should ask the user to upgrade past
54
+
55
+ If `wrangler-accounts --version` is below any of these, **upgrade first** before debugging anything else — older versions have real bugs that will misdirect you:
56
+
57
+ | Version | What it fixed | Symptom on older versions |
58
+ |---|---|---|
59
+ | **≥ 1.2.2** | per-profile `WRANGLER_CACHE_DIR`, fixes account-id leak across profiles | `d1`/`r2 object` commands return `7403 not authorized` even though OAuth is valid; deploys silently land in the wrong account |
60
+ | **≥ 1.3.0** | STATUS column distinguishes `valid` / `valid*` / `EXPIRED` (refresh-token-aware) | `list` shows `EXPIRED` for healthy profiles, scaring you into running `login` for no reason |
61
+ | **≥ 1.4.0** | `login` refuses non-TTY contexts and accidental overwrites | `login <name>` hangs forever in non-interactive contexts; reflexive `login` overwrites a healthy profile |
62
+
63
+ ```bash
64
+ npm i -g @leeguoo/wrangler-accounts@latest # always-safe upgrade
65
+ ```
66
+
67
+ ## Triage flow — when something looks wrong
68
+
69
+ **Run these in order before reaching for `login` or any destructive command.** The agent who skipped this step in a recent incident ended up overwriting a perfectly healthy profile in a sub-shell where the browser couldn't even open.
70
+
71
+ ```bash
72
+ # 1. Verify your binary is recent enough (see version table above)
73
+ wrangler-accounts --version
74
+
75
+ # 2. Authoritative state of every profile (hits Cloudflare API per profile)
76
+ wrangler-accounts list --deep
77
+ ```
78
+
79
+ How to read `list --deep` output:
80
+
81
+ | You see | Meaning | What to do |
82
+ |---|---|---|
83
+ | `STATUS valid` + `VERIFIED ✓ ok` | profile is fine | nothing — proceed with whatever the user actually asked |
84
+ | `STATUS valid*` + `VERIFIED ✓ ok` | profile is fine; access token will auto-refresh on next use | nothing — **`valid*` is healthy, do NOT re-login** |
85
+ | `STATUS EXPIRED` + `VERIFIED ✓ ok` | rare; only on `< 1.3.0` binaries — STATUS is lying | upgrade the CLI; profile is fine |
86
+ | `STATUS EXPIRED` + `VERIFIED ✗ Not logged in` | profile is genuinely broken | `wrangler-accounts login <name>` (interactive only) |
87
+ | `STATUS valid` + `VERIFIED ✗` of any kind | refresh token revoked server-side | `wrangler-accounts login <name>` (interactive only) |
88
+ | `STATUS unknown` | profile file lacks `expiration_time` | run `--deep` (already did) — trust VERIFIED |
89
+
90
+ **Rule**: only suggest `wrangler-accounts login <name>` when at least one of:
91
+ 1. `list --deep` showed `VERIFIED ✗` for that profile, OR
92
+ 2. The profile doesn't exist at all yet, OR
93
+ 3. The user explicitly says "re-authenticate" / "log me in again"
94
+
95
+ **If `list --deep` shows everything ✓ but a wrangler command still fails**, the root cause is somewhere other than wrangler-accounts — most likely:
96
+ - Project-local `./.wrangler/state/` is sharing data across profiles (see "What is and isn't isolated")
97
+ - The project's `wrangler.toml` has the wrong `account_id` hardcoded
98
+ - The user is on a CLI version below 1.2.2 (account-id cache leak)
99
+ - The wrangler subcommand actually needs `--remote` or `--local` and you forgot
100
+
32
101
  ## Quick Start
33
102
 
34
103
  - `wrangler-accounts login <name>` — interactive OAuth login into a new profile (never touches real `~/.wrangler`)
@@ -37,6 +106,8 @@ npm i -g wrangler
37
106
  - `wrangler-accounts --profile personal deploy` — one-shot override
38
107
  - `wrangler-accounts exec work -- npm run release` — run a command in an isolated subshell for the `work` profile
39
108
 
109
+ > 💡 **Reading the STATUS column**: `valid` = healthy, `valid*` = healthy (will auto-refresh on next use, **don't re-login**), `EXPIRED` = truly broken (need login), `unknown` = run `--deep` to find out. Full table below in "List and inspect profiles".
110
+
40
111
  ## Tasks
41
112
 
42
113
  ### Run wrangler against a profile