@leeguoo/wrangler-accounts 1.5.1 → 1.6.2
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.
- package/.claude-plugin/plugin.json +11 -0
- package/README.md +26 -5
- package/bin/wrangler-accounts.js +234 -52
- package/lib/isolation.js +32 -10
- package/lib/profile-store.js +68 -2
- package/package.json +1 -1
- package/plugins/wrangler-accounts/skills/wrangler-accounts/SKILL.md +58 -5
- package/skills/wrangler-accounts/SKILL.md +0 -461
|
@@ -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.1",
|
|
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
|
+
}
|
package/README.md
CHANGED
|
@@ -111,7 +111,8 @@ WRANGLER_PROFILE=<name> wrangler-accounts <wrangler-args...>
|
|
|
111
111
|
wrangler-accounts exec <name> # interactive subshell
|
|
112
112
|
wrangler-accounts exec <name> -- <cmd> [args] # one command
|
|
113
113
|
|
|
114
|
-
wrangler-accounts login <name> # isolated OAuth login
|
|
114
|
+
wrangler-accounts login <name> # isolated OAuth login (browser)
|
|
115
|
+
wrangler-accounts token-add <name> <api-token> <account-id> [--force] # API token profile (no browser)
|
|
115
116
|
wrangler-accounts default [name | --unset] # manage persistent default
|
|
116
117
|
wrangler-accounts whoami [--profile <name>] # show resolved identity
|
|
117
118
|
wrangler-accounts list # fast table (name/status/expires/identity)
|
|
@@ -165,12 +166,32 @@ When you run `wrangler-accounts <wrangler-args>`, the active profile is resolved
|
|
|
165
166
|
4. `profilesDir/default` (set via `wrangler-accounts default <name>`)
|
|
166
167
|
5. Hard error with actionable hint
|
|
167
168
|
|
|
168
|
-
##
|
|
169
|
+
## API token profiles (no browser required)
|
|
170
|
+
|
|
171
|
+
Since 1.6.0 you can save a Cloudflare API token + account ID as a named profile — no OAuth browser flow:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# Get your API token: Cloudflare dashboard → My Profile → API Tokens
|
|
175
|
+
wrangler-accounts token-add work CF_TOKEN_HERE ACCOUNT_ID_HERE
|
|
176
|
+
|
|
177
|
+
# Use exactly like an OAuth profile
|
|
178
|
+
wrangler-accounts --profile work deploy
|
|
179
|
+
wrangler-accounts work r2 list
|
|
180
|
+
```
|
|
169
181
|
|
|
170
|
-
|
|
182
|
+
Token profiles appear in `list` with `[token]` type and `STATUS: token`. There's no expiration; they're always ready.
|
|
183
|
+
|
|
184
|
+
**Env-var pass-through:** if `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` are already in the environment, no profile selection is needed at all:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
CLOUDFLARE_API_TOKEN=xxx CLOUDFLARE_ACCOUNT_ID=yyy wrangler-accounts deploy
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## When to use `wrangler-accounts` vs. native env vars
|
|
171
191
|
|
|
172
|
-
- **Local dev, multiple accounts** → `wrangler-accounts
|
|
173
|
-
- **
|
|
192
|
+
- **Local dev, multiple OAuth accounts** → `wrangler-accounts login <name>` (the classic use case)
|
|
193
|
+
- **Local dev, API token accounts** → `wrangler-accounts token-add <name> <token> <account-id>` (1.6.0+)
|
|
194
|
+
- **CI / deploy pipelines** → either `CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID` with plain `wrangler`, or the env-var pass-through mode above
|
|
174
195
|
- **Shared scripts** that run locally or in CI → parameterize on `WRANGLER_PROFILE` so devs can run them with `WRANGLER_PROFILE=work wrangler-accounts ./deploy.sh`.
|
|
175
196
|
|
|
176
197
|
## Options
|
package/bin/wrangler-accounts.js
CHANGED
|
@@ -18,9 +18,11 @@ const {
|
|
|
18
18
|
isValidName,
|
|
19
19
|
isBackupName,
|
|
20
20
|
listProfiles,
|
|
21
|
+
getProfileType,
|
|
21
22
|
fileHash,
|
|
22
23
|
readExpirationTime,
|
|
23
24
|
readSessionState,
|
|
25
|
+
readTokenSessionState,
|
|
24
26
|
filesEqual,
|
|
25
27
|
writeMeta,
|
|
26
28
|
readMeta,
|
|
@@ -33,6 +35,8 @@ const {
|
|
|
33
35
|
backupCurrentConfig,
|
|
34
36
|
findMatchingProfile,
|
|
35
37
|
saveProfile: saveProfileImpl,
|
|
38
|
+
saveTokenProfile: saveTokenProfileImpl,
|
|
39
|
+
readTokenCredentials,
|
|
36
40
|
removeProfile: removeProfileImpl,
|
|
37
41
|
} = require("../lib/profile-store");
|
|
38
42
|
const {
|
|
@@ -62,6 +66,7 @@ const MANAGEMENT_SUBCOMMANDS = new Set([
|
|
|
62
66
|
"login",
|
|
63
67
|
"remove",
|
|
64
68
|
"default",
|
|
69
|
+
"token-add",
|
|
65
70
|
"whoami",
|
|
66
71
|
"gc",
|
|
67
72
|
"use",
|
|
@@ -129,6 +134,10 @@ function removeProfile(...args) {
|
|
|
129
134
|
try { return removeProfileImpl(...args); }
|
|
130
135
|
catch (err) { die(err.message); }
|
|
131
136
|
}
|
|
137
|
+
function saveTokenProfile(...args) {
|
|
138
|
+
try { return saveTokenProfileImpl(...args); }
|
|
139
|
+
catch (err) { die(err.message); }
|
|
140
|
+
}
|
|
132
141
|
|
|
133
142
|
let outputJson = false;
|
|
134
143
|
|
|
@@ -141,6 +150,156 @@ function die(message, exitCode = 1) {
|
|
|
141
150
|
process.exit(exitCode);
|
|
142
151
|
}
|
|
143
152
|
|
|
153
|
+
function profileTypeForName(profilesDir, name) {
|
|
154
|
+
return getProfileType(path.join(profilesDir, name));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function tokenProfileExists(profilesDir, name) {
|
|
158
|
+
return profileTypeForName(profilesDir, name) !== null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function noProfileMessage() {
|
|
162
|
+
return [
|
|
163
|
+
'No profile specified. Options:',
|
|
164
|
+
' - wrangler-accounts --profile <name> ...',
|
|
165
|
+
' - WRANGLER_PROFILE=<name> wrangler-accounts ...',
|
|
166
|
+
' - wrangler-accounts default <name> (set a persistent default)',
|
|
167
|
+
].join('\n');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function resolveProfileAny({
|
|
171
|
+
cliProfile,
|
|
172
|
+
positional,
|
|
173
|
+
env,
|
|
174
|
+
profilesDir,
|
|
175
|
+
managementSubcommands,
|
|
176
|
+
}) {
|
|
177
|
+
try {
|
|
178
|
+
return resolveProfile({
|
|
179
|
+
cliProfile,
|
|
180
|
+
positional,
|
|
181
|
+
env,
|
|
182
|
+
profilesDir,
|
|
183
|
+
managementSubcommands,
|
|
184
|
+
});
|
|
185
|
+
} catch (err) {
|
|
186
|
+
if (!(err instanceof ResolveError)) throw err;
|
|
187
|
+
if (err.code === "INVALID_NAME") throw err;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (cliProfile) {
|
|
191
|
+
if (!isValidName(cliProfile)) {
|
|
192
|
+
throw new ResolveError(`Invalid profile name: ${cliProfile}`, "INVALID_NAME");
|
|
193
|
+
}
|
|
194
|
+
if (tokenProfileExists(profilesDir, cliProfile)) {
|
|
195
|
+
return { name: cliProfile, source: "cli" };
|
|
196
|
+
}
|
|
197
|
+
throw new ResolveError(`Profile not found: ${cliProfile}`, "PROFILE_NOT_FOUND");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (positional && !managementSubcommands.has(positional)) {
|
|
201
|
+
if (isValidName(positional) && tokenProfileExists(profilesDir, positional)) {
|
|
202
|
+
return { name: positional, source: "positional" };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const envProfile = env && env.WRANGLER_PROFILE;
|
|
207
|
+
if (envProfile && envProfile.length) {
|
|
208
|
+
if (!isValidName(envProfile)) {
|
|
209
|
+
throw new ResolveError(`Invalid profile name: ${envProfile}`, "INVALID_NAME");
|
|
210
|
+
}
|
|
211
|
+
if (tokenProfileExists(profilesDir, envProfile)) {
|
|
212
|
+
return { name: envProfile, source: "env" };
|
|
213
|
+
}
|
|
214
|
+
throw new ResolveError(`Profile not found: ${envProfile}`, "PROFILE_NOT_FOUND");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const def = getDefaultProfile(profilesDir);
|
|
218
|
+
if (def) {
|
|
219
|
+
if (!isValidName(def)) {
|
|
220
|
+
throw new ResolveError(`Invalid profile name: ${def}`, "INVALID_NAME");
|
|
221
|
+
}
|
|
222
|
+
if (tokenProfileExists(profilesDir, def)) {
|
|
223
|
+
return { name: def, source: "default" };
|
|
224
|
+
}
|
|
225
|
+
throw new ResolveError(`Profile not found: ${def}`, "PROFILE_NOT_FOUND");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
throw new ResolveError(noProfileMessage(), "NO_PROFILE");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function runAnonymousTokenMode({
|
|
232
|
+
command,
|
|
233
|
+
args,
|
|
234
|
+
captureStdout = false,
|
|
235
|
+
}) {
|
|
236
|
+
return runIsolated({
|
|
237
|
+
profile: "token-env",
|
|
238
|
+
profileCfg: null,
|
|
239
|
+
profileDir: null,
|
|
240
|
+
realHome: os.homedir(),
|
|
241
|
+
command,
|
|
242
|
+
args,
|
|
243
|
+
apiToken: process.env.CLOUDFLARE_API_TOKEN || null,
|
|
244
|
+
accountId: process.env.CLOUDFLARE_ACCOUNT_ID || null,
|
|
245
|
+
baseEnv: process.env,
|
|
246
|
+
captureStdout,
|
|
247
|
+
cloudflaredPath: findCloudflared(),
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function runResolvedProfileCommand({
|
|
252
|
+
resolved,
|
|
253
|
+
profilesDir,
|
|
254
|
+
command,
|
|
255
|
+
args,
|
|
256
|
+
captureStdout = false,
|
|
257
|
+
}) {
|
|
258
|
+
const profileDir = path.join(profilesDir, resolved.name);
|
|
259
|
+
const profileType = getProfileType(profileDir);
|
|
260
|
+
|
|
261
|
+
if (profileType === "token") {
|
|
262
|
+
const creds = readTokenCredentials(profileDir);
|
|
263
|
+
if (!creds || !creds.apiToken) {
|
|
264
|
+
die(`Token profile '${resolved.name}' is missing token.json credentials.`);
|
|
265
|
+
}
|
|
266
|
+
return runIsolated({
|
|
267
|
+
profile: resolved.name,
|
|
268
|
+
profileCfg: null,
|
|
269
|
+
profileDir,
|
|
270
|
+
realHome: os.homedir(),
|
|
271
|
+
command,
|
|
272
|
+
args,
|
|
273
|
+
apiToken: creds.apiToken,
|
|
274
|
+
accountId: creds.accountId || null,
|
|
275
|
+
baseEnv: process.env,
|
|
276
|
+
captureStdout,
|
|
277
|
+
cloudflaredPath: findCloudflared(),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const profileCfg = path.join(profileDir, "config.toml");
|
|
282
|
+
const session = readSessionState(profileCfg);
|
|
283
|
+
if (session.effective === 'expired') {
|
|
284
|
+
die(
|
|
285
|
+
`Profile '${resolved.name}' has expired Wrangler OAuth credentials and no refresh_token to renew them (expiration_time: ${session.expirationTime}). Run 'wrangler-accounts login ${resolved.name}' to re-authenticate.`,
|
|
286
|
+
3
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return runIsolated({
|
|
291
|
+
profile: resolved.name,
|
|
292
|
+
profileCfg,
|
|
293
|
+
profileDir,
|
|
294
|
+
realHome: os.homedir(),
|
|
295
|
+
command,
|
|
296
|
+
args,
|
|
297
|
+
baseEnv: process.env,
|
|
298
|
+
captureStdout,
|
|
299
|
+
cloudflaredPath: findCloudflared(),
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
144
303
|
function printHelp(exitCode = 0) {
|
|
145
304
|
const text = `wrangler-accounts - manage multiple Wrangler login profiles
|
|
146
305
|
|
|
@@ -152,6 +311,7 @@ Commands:
|
|
|
152
311
|
status
|
|
153
312
|
login <name>
|
|
154
313
|
save <name>
|
|
314
|
+
token-add <name> <api-token> <account-id>
|
|
155
315
|
sync <name>
|
|
156
316
|
sync-active
|
|
157
317
|
use <name>
|
|
@@ -391,7 +551,7 @@ function main() {
|
|
|
391
551
|
|
|
392
552
|
let resolved;
|
|
393
553
|
try {
|
|
394
|
-
resolved =
|
|
554
|
+
resolved = resolveProfileAny({
|
|
395
555
|
cliProfile: profileArg,
|
|
396
556
|
positional,
|
|
397
557
|
env: process.env,
|
|
@@ -400,6 +560,13 @@ function main() {
|
|
|
400
560
|
});
|
|
401
561
|
} catch (err) {
|
|
402
562
|
if (err instanceof ResolveError) {
|
|
563
|
+
if (err.code === "NO_PROFILE" && process.env.CLOUDFLARE_API_TOKEN) {
|
|
564
|
+
const result = runAnonymousTokenMode({
|
|
565
|
+
command: "wrangler",
|
|
566
|
+
args: rest,
|
|
567
|
+
});
|
|
568
|
+
process.exit(result.exitCode);
|
|
569
|
+
}
|
|
403
570
|
const exitCode =
|
|
404
571
|
err.code === "NO_PROFILE" || err.code === "PROFILE_NOT_FOUND" ? 2 : 1;
|
|
405
572
|
die(err.message, exitCode);
|
|
@@ -407,26 +574,14 @@ function main() {
|
|
|
407
574
|
throw err;
|
|
408
575
|
}
|
|
409
576
|
|
|
410
|
-
const profileCfg = path.join(profilesDir, resolved.name, "config.toml");
|
|
411
|
-
const session = readSessionState(profileCfg);
|
|
412
|
-
if (session.effective === 'expired') {
|
|
413
|
-
die(
|
|
414
|
-
`Profile '${resolved.name}' has expired Wrangler OAuth credentials and no refresh_token to renew them (expiration_time: ${session.expirationTime}). Run 'wrangler-accounts login ${resolved.name}' to re-authenticate.`,
|
|
415
|
-
3
|
|
416
|
-
);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
577
|
// If positional was consumed as profile, drop it from wrangler argv
|
|
420
578
|
const wranglerArgs = resolved.source === "positional" ? rest.slice(1) : rest;
|
|
421
579
|
|
|
422
|
-
const result =
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
realHome: os.homedir(),
|
|
580
|
+
const result = runResolvedProfileCommand({
|
|
581
|
+
resolved,
|
|
582
|
+
profilesDir,
|
|
426
583
|
command: "wrangler",
|
|
427
584
|
args: wranglerArgs,
|
|
428
|
-
baseEnv: process.env,
|
|
429
|
-
cloudflaredPath: findCloudflared(),
|
|
430
585
|
});
|
|
431
586
|
process.exit(result.exitCode);
|
|
432
587
|
}
|
|
@@ -438,12 +593,15 @@ function main() {
|
|
|
438
593
|
|
|
439
594
|
const entries = profiles.map((name) => {
|
|
440
595
|
const profileDir = path.join(profilesDir, name);
|
|
596
|
+
const type = getProfileType(profileDir) || "oauth";
|
|
441
597
|
const cfgPath = path.join(profileDir, "config.toml");
|
|
442
|
-
const session =
|
|
598
|
+
const session =
|
|
599
|
+
type === "token" ? readTokenSessionState() : readSessionState(cfgPath);
|
|
443
600
|
const meta = readMeta(profileDir);
|
|
444
601
|
const identity = getMetaIdentity(meta);
|
|
445
602
|
return {
|
|
446
603
|
name,
|
|
604
|
+
type,
|
|
447
605
|
isDefault: name === defaultName,
|
|
448
606
|
isActive: name === activeName,
|
|
449
607
|
status: session.effective, // 'valid' | 'refreshable' | 'expired' | 'unknown'
|
|
@@ -468,17 +626,14 @@ function main() {
|
|
|
468
626
|
}
|
|
469
627
|
const cloudflaredPath = findCloudflared();
|
|
470
628
|
for (const e of entries) {
|
|
471
|
-
const profileCfg = path.join(profilesDir, e.name, "config.toml");
|
|
472
629
|
try {
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
630
|
+
const resolved = { name: e.name, source: "deep" };
|
|
631
|
+
const r = runResolvedProfileCommand({
|
|
632
|
+
resolved,
|
|
633
|
+
profilesDir,
|
|
477
634
|
command: "wrangler",
|
|
478
635
|
args: ["whoami"],
|
|
479
|
-
baseEnv: process.env,
|
|
480
636
|
captureStdout: true,
|
|
481
|
-
cloudflaredPath,
|
|
482
637
|
});
|
|
483
638
|
const output = `${r.stdout || ""}\n${r.stderr || ""}`;
|
|
484
639
|
if (r.exitCode === 0) {
|
|
@@ -522,13 +677,14 @@ function main() {
|
|
|
522
677
|
if (defaultName) console.log(`Default: ${defaultName}\n`);
|
|
523
678
|
const rows = entries.map((e) => ({
|
|
524
679
|
marker: e.isDefault ? "*" : " ",
|
|
525
|
-
name: e.name
|
|
680
|
+
name: `${e.name} [${e.type}]`,
|
|
526
681
|
status:
|
|
527
682
|
e.status === "expired" ? "EXPIRED"
|
|
528
683
|
: e.status === "refreshable" ? "valid*"
|
|
529
684
|
: e.status === "valid" ? "valid"
|
|
685
|
+
: e.status === "token" ? "token"
|
|
530
686
|
: "unknown",
|
|
531
|
-
expires: formatExpiry(e.expirationTime),
|
|
687
|
+
expires: e.type === "token" ? "—" : formatExpiry(e.expirationTime),
|
|
532
688
|
verified:
|
|
533
689
|
e.verified === true ? "✓ ok"
|
|
534
690
|
: e.verified === false ? `✗ ${e.verifyError || "failed"}`
|
|
@@ -594,12 +750,15 @@ function main() {
|
|
|
594
750
|
const matchType = exactMatch ? "hash" : identityMatches.length === 1 ? "identity" : null;
|
|
595
751
|
const profileStates = Object.fromEntries(
|
|
596
752
|
profiles.map((name) => {
|
|
597
|
-
const
|
|
598
|
-
const
|
|
753
|
+
const profileDir = path.join(profilesDir, name);
|
|
754
|
+
const type = getProfileType(profileDir) || "oauth";
|
|
755
|
+
const profileConfig = path.join(profileDir, "config.toml");
|
|
756
|
+
const meta = readMeta(profileDir);
|
|
599
757
|
return [
|
|
600
758
|
name,
|
|
601
759
|
{
|
|
602
|
-
...readSessionState(profileConfig),
|
|
760
|
+
...(type === "token" ? readTokenSessionState() : readSessionState(profileConfig)),
|
|
761
|
+
type,
|
|
603
762
|
identity: getMetaIdentity(meta),
|
|
604
763
|
},
|
|
605
764
|
];
|
|
@@ -651,10 +810,13 @@ function main() {
|
|
|
651
810
|
}
|
|
652
811
|
for (const name of profiles) {
|
|
653
812
|
const profileSession = profileStates[name];
|
|
654
|
-
|
|
655
|
-
|
|
813
|
+
const state =
|
|
814
|
+
profileSession.effective === "token"
|
|
815
|
+
? "token"
|
|
816
|
+
: profileSession.expired ? "expired" : "valid";
|
|
656
817
|
const suffix = profileSession.identity ? `, ${describeIdentity(profileSession.identity)}` : "";
|
|
657
|
-
|
|
818
|
+
const expiry = profileSession.expirationTime || "(n/a)";
|
|
819
|
+
console.log(`- ${name} [${profileSession.type}]: ${expiry} (${state}${suffix ? suffix : ""})`);
|
|
658
820
|
}
|
|
659
821
|
}
|
|
660
822
|
return;
|
|
@@ -689,6 +851,19 @@ function main() {
|
|
|
689
851
|
return;
|
|
690
852
|
}
|
|
691
853
|
|
|
854
|
+
if (command === "token-add") {
|
|
855
|
+
const name = rest[1];
|
|
856
|
+
const apiToken = rest[2];
|
|
857
|
+
const accountId = rest[3];
|
|
858
|
+
if (!name) die("Missing profile name for token-add");
|
|
859
|
+
if (!apiToken) die("Missing API token for token-add");
|
|
860
|
+
if (!accountId) die("Missing account ID for token-add");
|
|
861
|
+
ensureDir(profilesDir);
|
|
862
|
+
saveTokenProfile(name, apiToken, accountId, profilesDir, opts.force);
|
|
863
|
+
console.log(`Saved token profile '${name}'`);
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
|
|
692
867
|
if (command === "login") {
|
|
693
868
|
const name = rest[1];
|
|
694
869
|
if (!name) die("Missing profile name for login");
|
|
@@ -983,7 +1158,7 @@ function main() {
|
|
|
983
1158
|
const profileArg = opts.profile || rest[1] || null;
|
|
984
1159
|
let resolved;
|
|
985
1160
|
try {
|
|
986
|
-
resolved =
|
|
1161
|
+
resolved = resolveProfileAny({
|
|
987
1162
|
cliProfile: profileArg,
|
|
988
1163
|
positional: null,
|
|
989
1164
|
env: process.env,
|
|
@@ -991,10 +1166,29 @@ function main() {
|
|
|
991
1166
|
managementSubcommands: MANAGEMENT_SUBCOMMANDS,
|
|
992
1167
|
});
|
|
993
1168
|
} catch (err) {
|
|
994
|
-
if (err instanceof ResolveError)
|
|
1169
|
+
if (err instanceof ResolveError) {
|
|
1170
|
+
if (err.code === "NO_PROFILE" && process.env.CLOUDFLARE_API_TOKEN) {
|
|
1171
|
+
const result = runAnonymousTokenMode({
|
|
1172
|
+
command: "wrangler",
|
|
1173
|
+
args: ["whoami"],
|
|
1174
|
+
});
|
|
1175
|
+
process.exit(result.exitCode);
|
|
1176
|
+
}
|
|
1177
|
+
die(err.message, 2);
|
|
1178
|
+
}
|
|
995
1179
|
throw err;
|
|
996
1180
|
}
|
|
997
1181
|
const profileDir = path.join(profilesDir, resolved.name);
|
|
1182
|
+
const profileType = getProfileType(profileDir) || "oauth";
|
|
1183
|
+
if (profileType === "token") {
|
|
1184
|
+
const result = runResolvedProfileCommand({
|
|
1185
|
+
resolved,
|
|
1186
|
+
profilesDir,
|
|
1187
|
+
command: "wrangler",
|
|
1188
|
+
args: ["whoami"],
|
|
1189
|
+
});
|
|
1190
|
+
process.exit(result.exitCode);
|
|
1191
|
+
}
|
|
998
1192
|
const meta = readMeta(profileDir);
|
|
999
1193
|
const identity = getMetaIdentity(meta);
|
|
1000
1194
|
if (opts.json) {
|
|
@@ -1004,6 +1198,7 @@ function main() {
|
|
|
1004
1198
|
command: "whoami",
|
|
1005
1199
|
profile: resolved.name,
|
|
1006
1200
|
source: resolved.source,
|
|
1201
|
+
type: profileType,
|
|
1007
1202
|
identity,
|
|
1008
1203
|
},
|
|
1009
1204
|
null,
|
|
@@ -1061,7 +1256,7 @@ function main() {
|
|
|
1061
1256
|
|
|
1062
1257
|
let resolved;
|
|
1063
1258
|
try {
|
|
1064
|
-
resolved =
|
|
1259
|
+
resolved = resolveProfileAny({
|
|
1065
1260
|
cliProfile: profileName,
|
|
1066
1261
|
positional: null,
|
|
1067
1262
|
env: process.env,
|
|
@@ -1073,15 +1268,6 @@ function main() {
|
|
|
1073
1268
|
throw err;
|
|
1074
1269
|
}
|
|
1075
1270
|
|
|
1076
|
-
const profileCfg = path.join(profilesDir, resolved.name, "config.toml");
|
|
1077
|
-
const session = readSessionState(profileCfg);
|
|
1078
|
-
if (session.effective === 'expired') {
|
|
1079
|
-
die(
|
|
1080
|
-
`Profile '${resolved.name}' has expired Wrangler OAuth credentials and no refresh_token to renew them. Run 'wrangler-accounts login ${resolved.name}' to re-authenticate.`,
|
|
1081
|
-
3
|
|
1082
|
-
);
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
1271
|
// Everything after `--` is the user command. Without `--`, launch $SHELL -i.
|
|
1086
1272
|
const dashDashIdx = rest.indexOf("--", 2);
|
|
1087
1273
|
let cmd;
|
|
@@ -1095,14 +1281,11 @@ function main() {
|
|
|
1095
1281
|
cmdArgs = ["-i"];
|
|
1096
1282
|
}
|
|
1097
1283
|
|
|
1098
|
-
const result =
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
realHome: os.homedir(),
|
|
1284
|
+
const result = runResolvedProfileCommand({
|
|
1285
|
+
resolved,
|
|
1286
|
+
profilesDir,
|
|
1102
1287
|
command: cmd,
|
|
1103
1288
|
args: cmdArgs,
|
|
1104
|
-
baseEnv: process.env,
|
|
1105
|
-
cloudflaredPath: findCloudflared(),
|
|
1106
1289
|
});
|
|
1107
1290
|
process.exit(result.exitCode);
|
|
1108
1291
|
}
|
|
@@ -1138,8 +1321,7 @@ function main() {
|
|
|
1138
1321
|
}
|
|
1139
1322
|
// Set the default profile
|
|
1140
1323
|
if (!isValidName(name)) die(`Invalid profile name: ${name}`);
|
|
1141
|
-
|
|
1142
|
-
if (!fs.existsSync(cfg)) die(`Profile not found: ${name}`, 2);
|
|
1324
|
+
if (!tokenProfileExists(profilesDir, name)) die(`Profile not found: ${name}`, 2);
|
|
1143
1325
|
setDefaultProfile(profilesDir, name);
|
|
1144
1326
|
if (opts.json) {
|
|
1145
1327
|
console.log(JSON.stringify({ command: "default", name }, null, 2));
|
package/lib/isolation.js
CHANGED
|
@@ -21,15 +21,15 @@ const { spawnSync } = require('node:child_process');
|
|
|
21
21
|
*
|
|
22
22
|
* @param {object} args
|
|
23
23
|
* @param {string} args.realHome
|
|
24
|
-
* @param {string} args.profileCfg - path to the profile's config.toml file
|
|
24
|
+
* @param {string|null} [args.profileCfg] - path to the profile's config.toml file
|
|
25
25
|
* @param {string} [args.label] - optional label for the tmpdir name
|
|
26
26
|
* @returns {string} path to the shadow HOME
|
|
27
27
|
*/
|
|
28
|
-
function createShadowHome({ realHome, profileCfg, label = 'wa' }) {
|
|
28
|
+
function createShadowHome({ realHome, profileCfg = null, label = 'wa' }) {
|
|
29
29
|
if (!realHome || !fs.existsSync(realHome)) {
|
|
30
30
|
throw new Error(`real HOME does not exist: ${realHome}`);
|
|
31
31
|
}
|
|
32
|
-
if (
|
|
32
|
+
if (profileCfg && !fs.existsSync(profileCfg)) {
|
|
33
33
|
throw new Error(`profile config does not exist: ${profileCfg}`);
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -59,10 +59,12 @@ function createShadowHome({ realHome, profileCfg, label = 'wa' }) {
|
|
|
59
59
|
// saved profile automatically.
|
|
60
60
|
const shadowWranglerConfig = path.join(shadow, '.wrangler', 'config');
|
|
61
61
|
fs.mkdirSync(shadowWranglerConfig, { recursive: true });
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
const defaultConfigPath = path.join(shadowWranglerConfig, 'default.toml');
|
|
63
|
+
if (profileCfg) {
|
|
64
|
+
fs.symlinkSync(profileCfg, defaultConfigPath);
|
|
65
|
+
} else {
|
|
66
|
+
fs.writeFileSync(defaultConfigPath, '');
|
|
67
|
+
}
|
|
66
68
|
|
|
67
69
|
return shadow;
|
|
68
70
|
}
|
|
@@ -94,6 +96,9 @@ function buildIsolatedEnv({
|
|
|
94
96
|
realHome,
|
|
95
97
|
profile,
|
|
96
98
|
profileCfg = null,
|
|
99
|
+
profileDir = null,
|
|
100
|
+
apiToken = null,
|
|
101
|
+
accountId = null,
|
|
97
102
|
baseEnv = process.env,
|
|
98
103
|
cloudflaredPath = null,
|
|
99
104
|
}) {
|
|
@@ -105,6 +110,9 @@ function buildIsolatedEnv({
|
|
|
105
110
|
env.WRANGLER_REGISTRY_PATH = path.join(realHome, '.wrangler', 'registry');
|
|
106
111
|
env.WRANGLER_LOG_PATH = path.join(realHome, '.wrangler', 'logs');
|
|
107
112
|
env.WRANGLER_SEND_METRICS = 'false';
|
|
113
|
+
if (!env.WA_TEST_OUT) {
|
|
114
|
+
env.WA_TEST_OUT = path.join(shadow, '.wrangler', 'wa-test-out.json');
|
|
115
|
+
}
|
|
108
116
|
|
|
109
117
|
// CRITICAL: Wrangler caches the user's selected Cloudflare account ID
|
|
110
118
|
// in `wrangler-account.json` inside `getCacheFolder()`. If multiple
|
|
@@ -124,9 +132,9 @@ function buildIsolatedEnv({
|
|
|
124
132
|
// (CLOUDFLARED_PATH / node_modules); WRANGLER_CACHE_DIR is only for
|
|
125
133
|
// config-cache files like wrangler-account.json and
|
|
126
134
|
// pages-config-cache.json.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const cacheDir = path.join(
|
|
135
|
+
const cacheRoot = profileDir || (profileCfg ? path.dirname(profileCfg) : null);
|
|
136
|
+
if (cacheRoot) {
|
|
137
|
+
const cacheDir = path.join(cacheRoot, 'cache');
|
|
130
138
|
try {
|
|
131
139
|
fs.mkdirSync(cacheDir, { recursive: true });
|
|
132
140
|
} catch (err) {
|
|
@@ -137,6 +145,14 @@ function buildIsolatedEnv({
|
|
|
137
145
|
env.WRANGLER_CACHE_DIR = cacheDir;
|
|
138
146
|
}
|
|
139
147
|
|
|
148
|
+
if (apiToken) {
|
|
149
|
+
env.CLOUDFLARE_API_TOKEN = apiToken;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (accountId) {
|
|
153
|
+
env.CLOUDFLARE_ACCOUNT_ID = accountId;
|
|
154
|
+
}
|
|
155
|
+
|
|
140
156
|
if (cloudflaredPath) {
|
|
141
157
|
env.CLOUDFLARED_PATH = cloudflaredPath;
|
|
142
158
|
}
|
|
@@ -162,9 +178,12 @@ function buildIsolatedEnv({
|
|
|
162
178
|
function runIsolated({
|
|
163
179
|
profile,
|
|
164
180
|
profileCfg,
|
|
181
|
+
profileDir = null,
|
|
165
182
|
realHome,
|
|
166
183
|
command,
|
|
167
184
|
args,
|
|
185
|
+
apiToken = null,
|
|
186
|
+
accountId = null,
|
|
168
187
|
baseEnv = process.env,
|
|
169
188
|
captureStdout = false,
|
|
170
189
|
cloudflaredPath = null,
|
|
@@ -179,6 +198,9 @@ function runIsolated({
|
|
|
179
198
|
realHome,
|
|
180
199
|
profile,
|
|
181
200
|
profileCfg,
|
|
201
|
+
profileDir,
|
|
202
|
+
apiToken,
|
|
203
|
+
accountId,
|
|
182
204
|
baseEnv,
|
|
183
205
|
cloudflaredPath,
|
|
184
206
|
});
|
package/lib/profile-store.js
CHANGED
|
@@ -16,6 +16,13 @@ function isBackupName(name) {
|
|
|
16
16
|
return name.startsWith('__backup-');
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function getProfileType(profileDir) {
|
|
20
|
+
if (!profileDir || !fs.existsSync(profileDir)) return null;
|
|
21
|
+
if (fs.existsSync(path.join(profileDir, 'config.toml'))) return 'oauth';
|
|
22
|
+
if (fs.existsSync(path.join(profileDir, 'token.json'))) return 'token';
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
19
26
|
function listProfiles(profilesDir, { includeBackups = false } = {}) {
|
|
20
27
|
if (!fs.existsSync(profilesDir)) return [];
|
|
21
28
|
const entries = fs.readdirSync(profilesDir, { withFileTypes: true });
|
|
@@ -23,7 +30,7 @@ function listProfiles(profilesDir, { includeBackups = false } = {}) {
|
|
|
23
30
|
.filter((entry) => entry.isDirectory())
|
|
24
31
|
.map((entry) => entry.name)
|
|
25
32
|
.filter((name) => includeBackups || !isBackupName(name))
|
|
26
|
-
.filter((name) =>
|
|
33
|
+
.filter((name) => getProfileType(path.join(profilesDir, name)) !== null)
|
|
27
34
|
.sort();
|
|
28
35
|
}
|
|
29
36
|
|
|
@@ -199,7 +206,9 @@ function findMatchingProfile(profilesDir, configPath, { includeBackups = false }
|
|
|
199
206
|
const configHash = fileHash(configPath);
|
|
200
207
|
const profiles = listProfiles(profilesDir, { includeBackups });
|
|
201
208
|
for (const name of profiles) {
|
|
202
|
-
const
|
|
209
|
+
const profileDir = path.join(profilesDir, name);
|
|
210
|
+
if (getProfileType(profileDir) !== 'oauth') continue;
|
|
211
|
+
const profileConfig = path.join(profileDir, 'config.toml');
|
|
203
212
|
if (fileHash(profileConfig) === configHash) return name;
|
|
204
213
|
}
|
|
205
214
|
return null;
|
|
@@ -223,6 +232,59 @@ function saveProfile(name, configPath, profilesDir, force, identity = null) {
|
|
|
223
232
|
writeMeta(profileDir, name, configPath, identity);
|
|
224
233
|
}
|
|
225
234
|
|
|
235
|
+
function saveTokenProfile(name, apiToken, accountId, profilesDir, force) {
|
|
236
|
+
if (!isValidName(name)) {
|
|
237
|
+
throw new Error(`Invalid profile name: ${name}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const profileDir = path.join(profilesDir, name);
|
|
241
|
+
if (fs.existsSync(profileDir) && !force) {
|
|
242
|
+
throw new Error(`Profile exists: ${name} (use --force to overwrite)`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
ensureDir(profileDir);
|
|
246
|
+
|
|
247
|
+
const tokenPath = path.join(profileDir, 'token.json');
|
|
248
|
+
fs.writeFileSync(
|
|
249
|
+
tokenPath,
|
|
250
|
+
JSON.stringify({ apiToken, accountId }, null, 2),
|
|
251
|
+
);
|
|
252
|
+
fs.chmodSync(tokenPath, 0o600);
|
|
253
|
+
|
|
254
|
+
fs.writeFileSync(
|
|
255
|
+
path.join(profileDir, 'meta.json'),
|
|
256
|
+
JSON.stringify(
|
|
257
|
+
{
|
|
258
|
+
name,
|
|
259
|
+
savedAt: new Date().toISOString(),
|
|
260
|
+
type: 'token',
|
|
261
|
+
accountId,
|
|
262
|
+
},
|
|
263
|
+
null,
|
|
264
|
+
2,
|
|
265
|
+
),
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function readTokenCredentials(profileDir) {
|
|
270
|
+
const tokenPath = path.join(profileDir, 'token.json');
|
|
271
|
+
if (!fs.existsSync(tokenPath)) return null;
|
|
272
|
+
try {
|
|
273
|
+
return JSON.parse(fs.readFileSync(tokenPath, 'utf8'));
|
|
274
|
+
} catch {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function readTokenSessionState() {
|
|
280
|
+
return {
|
|
281
|
+
expirationTime: null,
|
|
282
|
+
expired: null,
|
|
283
|
+
hasRefreshToken: false,
|
|
284
|
+
effective: 'token',
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
226
288
|
function removeProfile(name, profilesDir) {
|
|
227
289
|
if (!isValidName(name)) {
|
|
228
290
|
throw new Error(`Invalid profile name: ${name}`);
|
|
@@ -245,10 +307,12 @@ module.exports = {
|
|
|
245
307
|
ensureDir,
|
|
246
308
|
isValidName,
|
|
247
309
|
isBackupName,
|
|
310
|
+
getProfileType,
|
|
248
311
|
listProfiles,
|
|
249
312
|
fileHash,
|
|
250
313
|
readExpirationTime,
|
|
251
314
|
readSessionState,
|
|
315
|
+
readTokenSessionState,
|
|
252
316
|
filesEqual,
|
|
253
317
|
writeMeta,
|
|
254
318
|
readMeta,
|
|
@@ -261,5 +325,7 @@ module.exports = {
|
|
|
261
325
|
backupCurrentConfig,
|
|
262
326
|
findMatchingProfile,
|
|
263
327
|
saveProfile,
|
|
328
|
+
saveTokenProfile,
|
|
329
|
+
readTokenCredentials,
|
|
264
330
|
removeProfile,
|
|
265
331
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leeguoo/wrangler-accounts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.2",
|
|
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": {
|
|
@@ -66,6 +66,7 @@ If `wrangler-accounts --version` is below any of these, **upgrade first** before
|
|
|
66
66
|
| **≥ 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 |
|
|
67
67
|
| **≥ 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 |
|
|
68
68
|
| **≥ 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 |
|
|
69
|
+
| **≥ 1.6.0** | API token profiles (`token-add`) + anonymous env-var pass-through | only OAuth profiles existed; `CLOUDFLARE_API_TOKEN` in env still required a named profile to be selected |
|
|
69
70
|
|
|
70
71
|
```bash
|
|
71
72
|
npm i -g @leeguoo/wrangler-accounts@latest # always-safe upgrade
|
|
@@ -108,6 +109,7 @@ How to read `list --deep` output:
|
|
|
108
109
|
## Quick Start
|
|
109
110
|
|
|
110
111
|
- `wrangler-accounts login <name>` — interactive OAuth login into a new profile (never touches real `~/.wrangler`)
|
|
112
|
+
- `wrangler-accounts token-add <name> <api-token> <account-id>` — save an API token profile (no browser login needed)
|
|
111
113
|
- `wrangler-accounts default <name>` — set the persistent default profile
|
|
112
114
|
- `wrangler-accounts deploy` — run `wrangler deploy` under the default profile
|
|
113
115
|
- `wrangler-accounts --profile personal deploy` — one-shot override
|
|
@@ -169,6 +171,7 @@ Use `--json` for structured output.
|
|
|
169
171
|
| `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 |
|
|
170
172
|
| `EXPIRED` / `expired` | access_token expired **AND** no refresh_token saved; profile is genuinely broken | `wrangler-accounts login <name>` |
|
|
171
173
|
| `unknown` | profile file has no `expiration_time` field | run `list --deep` to verify live |
|
|
174
|
+
| `token` | API token profile (1.6.0+) — no expiration concept, always ready | none |
|
|
172
175
|
|
|
173
176
|
**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.
|
|
174
177
|
|
|
@@ -176,13 +179,36 @@ The only time a user actually needs `wrangler-accounts login <name>` again is:
|
|
|
176
179
|
1. STATUS is `EXPIRED` (no refresh_token at all — profile was saved without `offline_access` scope)
|
|
177
180
|
2. OR `list --deep` returns `✗` with "Not logged in" / "refresh token may be revoked" (refresh token itself got invalidated)
|
|
178
181
|
|
|
182
|
+
### Save an API token profile (no browser required)
|
|
183
|
+
|
|
184
|
+
`wrangler-accounts token-add <name> <api-token> <account-id> [--force]`
|
|
185
|
+
|
|
186
|
+
Saves a Cloudflare API token + account ID as a named profile. No OAuth browser flow needed. The credentials are stored in `token.json` (mode 0600) inside the profile directory.
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
# Get your API token from: Cloudflare dashboard → My Profile → API Tokens
|
|
190
|
+
wrangler-accounts token-add work CF_TOKEN_HERE ACCOUNT_ID_HERE
|
|
191
|
+
|
|
192
|
+
# Use identically to OAuth profiles
|
|
193
|
+
wrangler-accounts --profile work deploy
|
|
194
|
+
wrangler-accounts work r2 list
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Token profiles appear in `list` with a `[token]` type indicator and `STATUS: token` — there is no expiration concept, so they are always ready to use. `remove` works the same as for OAuth profiles.
|
|
198
|
+
|
|
199
|
+
**Env-var pass-through (1.6.0+):** when `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` are already set in the environment and no profile is specified, `wrangler-accounts` runs in anonymous-token mode (no named profile needed). Useful for CI jobs that inject credentials via secrets:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
CLOUDFLARE_API_TOKEN=xxx CLOUDFLARE_ACCOUNT_ID=yyy wrangler-accounts deploy
|
|
203
|
+
```
|
|
204
|
+
|
|
179
205
|
### Save, sync, login, remove
|
|
180
206
|
|
|
181
207
|
- `wrangler-accounts save <name>` — snapshot current Wrangler config as a profile
|
|
182
208
|
- `wrangler-accounts sync <name>` — refresh a specific profile from the current login
|
|
183
209
|
- `wrangler-accounts sync-default` — refresh the default profile
|
|
184
210
|
- `wrangler-accounts login <name>` — fresh isolated OAuth login
|
|
185
|
-
- `wrangler-accounts remove <name>` — delete a profile
|
|
211
|
+
- `wrangler-accounts remove <name>` — delete a profile (works for both OAuth and token profiles)
|
|
186
212
|
|
|
187
213
|
### Clean up stale shadow HOMEs
|
|
188
214
|
|
|
@@ -327,11 +353,13 @@ This overwrites the existing profile with a fresh OAuth session. Any saved metad
|
|
|
327
353
|
The user ran `wrangler-accounts <wrangler-args>` without a resolvable profile. Fix one of:
|
|
328
354
|
|
|
329
355
|
```bash
|
|
330
|
-
wrangler-accounts --profile <name> <args>
|
|
356
|
+
wrangler-accounts --profile <name> <args> # one-shot
|
|
331
357
|
WRANGLER_PROFILE=<name> wrangler-accounts <args> # env var
|
|
332
|
-
wrangler-accounts default <name>
|
|
358
|
+
wrangler-accounts default <name> # persistent default
|
|
333
359
|
```
|
|
334
360
|
|
|
361
|
+
**1.6.0+**: if `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` are both present in the environment, this error no longer fires — the tool runs in anonymous-token mode automatically.
|
|
362
|
+
|
|
335
363
|
### "Profile not found: X"
|
|
336
364
|
|
|
337
365
|
The profile name doesn't exist in `profilesDir`. Check what's saved:
|
|
@@ -437,7 +465,32 @@ If a user is hitting a "wrong account" symptom and the credentials look right, t
|
|
|
437
465
|
|
|
438
466
|
## CI guidance
|
|
439
467
|
|
|
440
|
-
For CI and deploy pipelines
|
|
468
|
+
For CI and deploy pipelines you have two options:
|
|
469
|
+
|
|
470
|
+
**Option 1 — plain `wrangler` with env vars (simplest for CI):**
|
|
471
|
+
|
|
472
|
+
```bash
|
|
473
|
+
CLOUDFLARE_API_TOKEN=<token>
|
|
474
|
+
CLOUDFLARE_ACCOUNT_ID=<account-id>
|
|
475
|
+
wrangler deploy
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
**Option 2 — `wrangler-accounts` with a token profile (useful when you want the same CLI both locally and in CI):**
|
|
479
|
+
|
|
480
|
+
```bash
|
|
481
|
+
# Save once (locally or during CI bootstrap):
|
|
482
|
+
wrangler-accounts token-add work "$CF_TOKEN" "$CF_ACCOUNT_ID"
|
|
483
|
+
# Use the same commands locally and in CI:
|
|
484
|
+
wrangler-accounts --profile work deploy
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
Or use the anonymous pass-through (no profile needed if env vars are present):
|
|
488
|
+
|
|
489
|
+
```bash
|
|
490
|
+
CLOUDFLARE_API_TOKEN="$CF_TOKEN" CLOUDFLARE_ACCOUNT_ID="$CF_ACCOUNT_ID" wrangler-accounts deploy
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
`wrangler-accounts` is primarily a local developer convenience for juggling OAuth sessions on your workstation, but since 1.6.0 it also supports API token profiles for teams that want a consistent CLI interface across local and CI environments.
|
|
441
494
|
|
|
442
495
|
## Paths and environment
|
|
443
496
|
|
|
@@ -453,7 +506,7 @@ Use `--json` when another tool needs to parse results. All v1.0 commands that pr
|
|
|
453
506
|
|
|
454
507
|
## Naming rules
|
|
455
508
|
|
|
456
|
-
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.
|
|
509
|
+
Profile names: letters, numbers, dot, underscore, dash only. Names matching management subcommand names (`exec`, `default`, `whoami`, `gc`, `login`, `token-add`, `list`, `status`, `save`, `sync`, `sync-default`, `remove`, `use`, `sync-active`) cannot be reached via positional shorthand — use `--profile <name>` for those.
|
|
457
510
|
|
|
458
511
|
## Deprecated
|
|
459
512
|
|
|
@@ -1,461 +0,0 @@
|
|
|
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
|
-
If a Claude Code user previously installed via `skills.sh`, removing the standalone copy avoids a duplicate `/wrangler-accounts` entry in the command picker:
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
npx skills remove wrangler-accounts
|
|
37
|
-
# or: rm -rf ~/.agents/skills/wrangler-accounts
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## Prerequisites (check before running any recipe below)
|
|
41
|
-
|
|
42
|
-
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:
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
command -v wrangler-accounts && wrangler-accounts --version
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
If the command is missing, tell the user to install the CLI first:
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
npm i -g @leeguoo/wrangler-accounts
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
`wrangler` itself (the Cloudflare CLI) must also be on `PATH`. If missing:
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
npm i -g wrangler
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
### Minimum versions you should ask the user to upgrade past
|
|
61
|
-
|
|
62
|
-
If `wrangler-accounts --version` is below any of these, **upgrade first** before debugging anything else — older versions have real bugs that will misdirect you:
|
|
63
|
-
|
|
64
|
-
| Version | What it fixed | Symptom on older versions |
|
|
65
|
-
|---|---|---|
|
|
66
|
-
| **≥ 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 |
|
|
67
|
-
| **≥ 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 |
|
|
68
|
-
| **≥ 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 |
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
npm i -g @leeguoo/wrangler-accounts@latest # always-safe upgrade
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## Triage flow — when something looks wrong
|
|
75
|
-
|
|
76
|
-
**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.
|
|
77
|
-
|
|
78
|
-
```bash
|
|
79
|
-
# 1. Verify your binary is recent enough (see version table above)
|
|
80
|
-
wrangler-accounts --version
|
|
81
|
-
|
|
82
|
-
# 2. Authoritative state of every profile (hits Cloudflare API per profile)
|
|
83
|
-
wrangler-accounts list --deep
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
How to read `list --deep` output:
|
|
87
|
-
|
|
88
|
-
| You see | Meaning | What to do |
|
|
89
|
-
|---|---|---|
|
|
90
|
-
| `STATUS valid` + `VERIFIED ✓ ok` | profile is fine | nothing — proceed with whatever the user actually asked |
|
|
91
|
-
| `STATUS valid*` + `VERIFIED ✓ ok` | profile is fine; access token will auto-refresh on next use | nothing — **`valid*` is healthy, do NOT re-login** |
|
|
92
|
-
| `STATUS EXPIRED` + `VERIFIED ✓ ok` | rare; only on `< 1.3.0` binaries — STATUS is lying | upgrade the CLI; profile is fine |
|
|
93
|
-
| `STATUS EXPIRED` + `VERIFIED ✗ Not logged in` | profile is genuinely broken | `wrangler-accounts login <name>` (interactive only) |
|
|
94
|
-
| `STATUS valid` + `VERIFIED ✗` of any kind | refresh token revoked server-side | `wrangler-accounts login <name>` (interactive only) |
|
|
95
|
-
| `STATUS unknown` | profile file lacks `expiration_time` | run `--deep` (already did) — trust VERIFIED |
|
|
96
|
-
|
|
97
|
-
**Rule**: only suggest `wrangler-accounts login <name>` when at least one of:
|
|
98
|
-
1. `list --deep` showed `VERIFIED ✗` for that profile, OR
|
|
99
|
-
2. The profile doesn't exist at all yet, OR
|
|
100
|
-
3. The user explicitly says "re-authenticate" / "log me in again"
|
|
101
|
-
|
|
102
|
-
**If `list --deep` shows everything ✓ but a wrangler command still fails**, the root cause is somewhere other than wrangler-accounts — most likely:
|
|
103
|
-
- Project-local `./.wrangler/state/` is sharing data across profiles (see "What is and isn't isolated")
|
|
104
|
-
- The project's `wrangler.toml` has the wrong `account_id` hardcoded
|
|
105
|
-
- The user is on a CLI version below 1.2.2 (account-id cache leak)
|
|
106
|
-
- The wrangler subcommand actually needs `--remote` or `--local` and you forgot
|
|
107
|
-
|
|
108
|
-
## Quick Start
|
|
109
|
-
|
|
110
|
-
- `wrangler-accounts login <name>` — interactive OAuth login into a new profile (never touches real `~/.wrangler`)
|
|
111
|
-
- `wrangler-accounts default <name>` — set the persistent default profile
|
|
112
|
-
- `wrangler-accounts deploy` — run `wrangler deploy` under the default profile
|
|
113
|
-
- `wrangler-accounts --profile personal deploy` — one-shot override
|
|
114
|
-
- `wrangler-accounts exec work -- npm run release` — run a command in an isolated subshell for the `work` profile
|
|
115
|
-
|
|
116
|
-
> 💡 **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".
|
|
117
|
-
|
|
118
|
-
## Tasks
|
|
119
|
-
|
|
120
|
-
### Run wrangler against a profile
|
|
121
|
-
|
|
122
|
-
Per-invocation (preferred for scripts):
|
|
123
|
-
|
|
124
|
-
`wrangler-accounts --profile <name> <wrangler-args...>`
|
|
125
|
-
|
|
126
|
-
Or with env var:
|
|
127
|
-
|
|
128
|
-
`WRANGLER_PROFILE=<name> wrangler-accounts <wrangler-args...>`
|
|
129
|
-
|
|
130
|
-
Or positional shorthand (only when `<name>` is a saved profile name, not a management subcommand):
|
|
131
|
-
|
|
132
|
-
`wrangler-accounts <name> <wrangler-args...>`
|
|
133
|
-
|
|
134
|
-
### Open a subshell for a profile
|
|
135
|
-
|
|
136
|
-
`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`.
|
|
137
|
-
|
|
138
|
-
Run a single command instead:
|
|
139
|
-
|
|
140
|
-
`wrangler-accounts exec <name> -- <cmd> [args]`
|
|
141
|
-
|
|
142
|
-
### Manage the persistent default profile
|
|
143
|
-
|
|
144
|
-
- `wrangler-accounts default` — print current default (exit 1 if none set)
|
|
145
|
-
- `wrangler-accounts default <name>` — set the default
|
|
146
|
-
- `wrangler-accounts default --unset` — clear the default
|
|
147
|
-
- `wrangler-accounts default --json` — JSON output
|
|
148
|
-
|
|
149
|
-
### Show the resolved identity for a profile
|
|
150
|
-
|
|
151
|
-
`wrangler-accounts whoami [--profile <name>]` — reports the profile name, source tier (cli / positional / env / default), and identity from `meta.json`. Does not spawn wrangler.
|
|
152
|
-
|
|
153
|
-
Use `--json` for structured output.
|
|
154
|
-
|
|
155
|
-
### List and inspect profiles
|
|
156
|
-
|
|
157
|
-
- `wrangler-accounts list` — text table with NAME / STATUS / EXPIRES / IDENTITY columns
|
|
158
|
-
- `wrangler-accounts list --json` — structured: array of `{name, isDefault, isActive, status, expirationTime, hasRefreshToken, identity, verified, verifyError}`
|
|
159
|
-
- `wrangler-accounts list --plain` — one profile name per line (scriptable)
|
|
160
|
-
- `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.
|
|
161
|
-
- `wrangler-accounts status` / `status --json`
|
|
162
|
-
- Pass `--include-backups` to show hidden backup profiles.
|
|
163
|
-
|
|
164
|
-
**STATUS values (1.3.0+):**
|
|
165
|
-
|
|
166
|
-
| value | meaning | user action |
|
|
167
|
-
|---|---|---|
|
|
168
|
-
| `valid` | access_token is currently valid | none |
|
|
169
|
-
| `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 |
|
|
170
|
-
| `EXPIRED` / `expired` | access_token expired **AND** no refresh_token saved; profile is genuinely broken | `wrangler-accounts login <name>` |
|
|
171
|
-
| `unknown` | profile file has no `expiration_time` field | run `list --deep` to verify live |
|
|
172
|
-
|
|
173
|
-
**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.
|
|
174
|
-
|
|
175
|
-
The only time a user actually needs `wrangler-accounts login <name>` again is:
|
|
176
|
-
1. STATUS is `EXPIRED` (no refresh_token at all — profile was saved without `offline_access` scope)
|
|
177
|
-
2. OR `list --deep` returns `✗` with "Not logged in" / "refresh token may be revoked" (refresh token itself got invalidated)
|
|
178
|
-
|
|
179
|
-
### Save, sync, login, remove
|
|
180
|
-
|
|
181
|
-
- `wrangler-accounts save <name>` — snapshot current Wrangler config as a profile
|
|
182
|
-
- `wrangler-accounts sync <name>` — refresh a specific profile from the current login
|
|
183
|
-
- `wrangler-accounts sync-default` — refresh the default profile
|
|
184
|
-
- `wrangler-accounts login <name>` — fresh isolated OAuth login
|
|
185
|
-
- `wrangler-accounts remove <name>` — delete a profile
|
|
186
|
-
|
|
187
|
-
### Clean up stale shadow HOMEs
|
|
188
|
-
|
|
189
|
-
`wrangler-accounts gc [--older-than 1h]` — removes `wa-*` directories under `$TMPDIR` older than the threshold (default 1h). Safe to run at any time.
|
|
190
|
-
|
|
191
|
-
## Common Recipes
|
|
192
|
-
|
|
193
|
-
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.
|
|
194
|
-
|
|
195
|
-
### User wants: deploy a worker to a specific account
|
|
196
|
-
|
|
197
|
-
```bash
|
|
198
|
-
wrangler-accounts --profile work deploy
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
Or, when the user will be on this account for a while:
|
|
202
|
-
|
|
203
|
-
```bash
|
|
204
|
-
wrangler-accounts default work # set once
|
|
205
|
-
wrangler-accounts deploy # uses work from now on
|
|
206
|
-
wrangler-accounts deploy --env staging # wrangler flags pass through unchanged
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
### User wants: tail production logs on one account while developing on another
|
|
210
|
-
|
|
211
|
-
```bash
|
|
212
|
-
# shell 1
|
|
213
|
-
wrangler-accounts --profile work tail my-worker --format pretty
|
|
214
|
-
|
|
215
|
-
# shell 2 (simultaneously, zero interference)
|
|
216
|
-
wrangler-accounts --profile personal dev
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
The two shells each get their own shadow `HOME`, so there's no global state for the other to clobber.
|
|
220
|
-
|
|
221
|
-
### User wants: run a deploy script / npm script / Makefile against a specific account
|
|
222
|
-
|
|
223
|
-
```bash
|
|
224
|
-
wrangler-accounts exec work -- npm run deploy
|
|
225
|
-
wrangler-accounts exec work -- make release
|
|
226
|
-
wrangler-accounts exec work -- bash scripts/deploy.sh
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
Anything inside the subshell that calls `wrangler` (directly, via `npx`, via `pnpm`, via a package.json script) automatically uses the `work` profile.
|
|
230
|
-
|
|
231
|
-
### User wants: set up a new Cloudflare account from scratch
|
|
232
|
-
|
|
233
|
-
```bash
|
|
234
|
-
wrangler-accounts login new-account # opens the browser OAuth flow
|
|
235
|
-
wrangler-accounts whoami --profile new-account # verify identity
|
|
236
|
-
wrangler-accounts list # confirm the profile is saved
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
The login flow runs inside an isolated shadow `HOME`, so the user's real `~/.wrangler/config/default.toml` is never touched.
|
|
240
|
-
|
|
241
|
-
> ⚠️ **`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.
|
|
242
|
-
|
|
243
|
-
### ❌ Antipattern: running `login` to verify a profile works
|
|
244
|
-
|
|
245
|
-
This is wrong:
|
|
246
|
-
```bash
|
|
247
|
-
wrangler-accounts login Xdreamstar2025 # ❌ DON'T do this just to check
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
Reasons:
|
|
251
|
-
1. `login` is **destructive** — it overwrites the saved profile with a brand new OAuth flow.
|
|
252
|
-
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.
|
|
253
|
-
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.
|
|
254
|
-
|
|
255
|
-
Use one of these instead:
|
|
256
|
-
|
|
257
|
-
```bash
|
|
258
|
-
wrangler-accounts whoami --profile Xdreamstar2025 # fast, reads meta.json, no network
|
|
259
|
-
wrangler-accounts list --deep # authoritative, runs wrangler whoami per profile
|
|
260
|
-
wrangler-accounts list # quick STATUS overview (valid / valid* / EXPIRED / unknown)
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
Only fall back to `wrangler-accounts login <name>` when:
|
|
264
|
-
- The profile **does not exist yet** (creating a new account profile from scratch)
|
|
265
|
-
- The profile shows `EXPIRED` (truly expired, no refresh_token left) — see STATUS table above
|
|
266
|
-
- `list --deep` returns `✗` with "Not logged in" / "refresh token may be revoked" (server-side revocation)
|
|
267
|
-
- The user **explicitly says** "re-authenticate this profile" / "log me in again"
|
|
268
|
-
|
|
269
|
-
### User wants: check which account a profile is tied to, without running wrangler
|
|
270
|
-
|
|
271
|
-
```bash
|
|
272
|
-
wrangler-accounts whoami --profile <name> # text
|
|
273
|
-
wrangler-accounts whoami --profile <name> --json # structured
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
Returns the email + account ID from the saved `meta.json`. No network call.
|
|
277
|
-
|
|
278
|
-
### User wants: swap between many accounts quickly in one shell
|
|
279
|
-
|
|
280
|
-
Use `default` as a "current" setting:
|
|
281
|
-
|
|
282
|
-
```bash
|
|
283
|
-
wrangler-accounts default work
|
|
284
|
-
wrangler-accounts deploy # work
|
|
285
|
-
wrangler-accounts default personal
|
|
286
|
-
wrangler-accounts dev # personal
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
Or use positional shorthand inline:
|
|
290
|
-
|
|
291
|
-
```bash
|
|
292
|
-
wrangler-accounts work deploy # one-shot, no persistent default
|
|
293
|
-
wrangler-accounts personal dev
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
### User wants: run wrangler in CI with multiple accounts
|
|
297
|
-
|
|
298
|
-
**Don't use `wrangler-accounts` in CI.** Use native env vars:
|
|
299
|
-
|
|
300
|
-
```bash
|
|
301
|
-
# In CI secrets:
|
|
302
|
-
CLOUDFLARE_API_TOKEN=<token-with-workers-deploy-scope>
|
|
303
|
-
CLOUDFLARE_ACCOUNT_ID=<account-id>
|
|
304
|
-
|
|
305
|
-
# In the pipeline:
|
|
306
|
-
wrangler deploy
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
`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.
|
|
310
|
-
|
|
311
|
-
## Troubleshooting
|
|
312
|
-
|
|
313
|
-
### "Profile 'X' has expired Wrangler OAuth credentials and no refresh_token to renew them"
|
|
314
|
-
|
|
315
|
-
The saved OAuth access_token is past its `expiration_time` AND there is no `refresh_token` in the profile config (no `offline_access` scope at login time, or the token was revoked). The profile is genuinely broken; only re-authenticating fixes it:
|
|
316
|
-
|
|
317
|
-
```bash
|
|
318
|
-
wrangler-accounts login <name>
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
This overwrites the existing profile with a fresh OAuth session. Any saved metadata (identity, etc.) is re-verified.
|
|
322
|
-
|
|
323
|
-
**Note:** If you see this error in 1.5.0 or earlier, you may be hitting a known regression: any profile whose access_token had passed expiration was blocked even when a refresh_token was present. Upgrade to 1.5.1+ — `wrangler` itself silently refreshes those tokens on the next call, so wrangler-accounts no longer pre-flights against `expiration_time` alone.
|
|
324
|
-
|
|
325
|
-
### "No profile specified. Options: ..."
|
|
326
|
-
|
|
327
|
-
The user ran `wrangler-accounts <wrangler-args>` without a resolvable profile. Fix one of:
|
|
328
|
-
|
|
329
|
-
```bash
|
|
330
|
-
wrangler-accounts --profile <name> <args> # one-shot
|
|
331
|
-
WRANGLER_PROFILE=<name> wrangler-accounts <args> # env var
|
|
332
|
-
wrangler-accounts default <name> # persistent default
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
### "Profile not found: X"
|
|
336
|
-
|
|
337
|
-
The profile name doesn't exist in `profilesDir`. Check what's saved:
|
|
338
|
-
|
|
339
|
-
```bash
|
|
340
|
-
wrangler-accounts list
|
|
341
|
-
```
|
|
342
|
-
|
|
343
|
-
Create it with `wrangler-accounts login <name>` or copy an existing Wrangler login with `wrangler-accounts save <name>`.
|
|
344
|
-
|
|
345
|
-
### Inside `wrangler-accounts exec`, `cd ~` lands in a weird tmpdir
|
|
346
|
-
|
|
347
|
-
Expected. Inside an `exec` session, `$HOME` is the shadow HOME. The real home is still accessible:
|
|
348
|
-
|
|
349
|
-
```bash
|
|
350
|
-
cd "$WRANGLER_ACCOUNT_REAL_HOME"
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
Or users can add a shell alias: `alias realhome='cd "$WRANGLER_ACCOUNT_REAL_HOME"'`.
|
|
354
|
-
|
|
355
|
-
### Deprecated `use` command warning
|
|
356
|
-
|
|
357
|
-
`wrangler-accounts use <name>` still works but prints a deprecation warning. Suggest the replacement based on intent:
|
|
358
|
-
|
|
359
|
-
- "I want this account to stick for a while" → `wrangler-accounts default <name>`
|
|
360
|
-
- "Just this one command" → `wrangler-accounts --profile <name> <wrangler-args>`
|
|
361
|
-
|
|
362
|
-
### `[ERROR] A request to the Cloudflare API ... Authentication error [code: 10000]` with `code: 7403` ("not authorized to access this service")
|
|
363
|
-
|
|
364
|
-
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:
|
|
365
|
-
|
|
366
|
-
- `deploy` and `secret put` succeed (they don't put account ID in the URL path)
|
|
367
|
-
- `d1 execute --remote`, `r2 object get/put`, anything else with `/accounts/<id>/...` in the URL fails with 7403
|
|
368
|
-
|
|
369
|
-
**Fix path** (in order):
|
|
370
|
-
|
|
371
|
-
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.
|
|
372
|
-
2. **Clear the polluted shared cache** (one-time, even after upgrading):
|
|
373
|
-
```bash
|
|
374
|
-
rm -f ~/.wrangler/cache/wrangler-account.json
|
|
375
|
-
rm -f ~/Library/Preferences/.wrangler/cache/wrangler-account.json # macOS env-paths fallback
|
|
376
|
-
```
|
|
377
|
-
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>`.
|
|
378
|
-
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:
|
|
379
|
-
```bash
|
|
380
|
-
CLOUDFLARE_ACCOUNT_ID=<id> wrangler-accounts <profile> r2 object put ...
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
**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."
|
|
384
|
-
|
|
385
|
-
### "The OAuth config seems right, but the wrong account is being used"
|
|
386
|
-
|
|
387
|
-
Same root cause as the 7403 above. Default to the same fix path.
|
|
388
|
-
|
|
389
|
-
### `wrangler dev` (or any `--local` command) shows stale data after switching profiles
|
|
390
|
-
|
|
391
|
-
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.
|
|
392
|
-
|
|
393
|
-
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.
|
|
394
|
-
|
|
395
|
-
Two clean fixes:
|
|
396
|
-
|
|
397
|
-
1. **Use git worktrees** (recommended for any serious multi-profile dev workflow):
|
|
398
|
-
```bash
|
|
399
|
-
git worktree add ../my-project-work main
|
|
400
|
-
git worktree add ../my-project-personal main
|
|
401
|
-
cd ../my-project-work && wrangler-accounts exec work # isolated .wrangler/state/
|
|
402
|
-
cd ../my-project-personal && wrangler-accounts exec personal
|
|
403
|
-
```
|
|
404
|
-
2. **Clear state manually before switching**:
|
|
405
|
-
```bash
|
|
406
|
-
rm -rf .wrangler/state
|
|
407
|
-
wrangler-accounts --profile <new> dev
|
|
408
|
-
```
|
|
409
|
-
|
|
410
|
-
`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.
|
|
411
|
-
|
|
412
|
-
### Shell history / `.zsh_history` seems to grow when running `exec`
|
|
413
|
-
|
|
414
|
-
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.
|
|
415
|
-
|
|
416
|
-
## Invariants the AI should rely on
|
|
417
|
-
|
|
418
|
-
- **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`).
|
|
419
|
-
- **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.
|
|
420
|
-
- **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.
|
|
421
|
-
- **`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.
|
|
422
|
-
|
|
423
|
-
## What is and isn't isolated
|
|
424
|
-
|
|
425
|
-
| State | Location | Isolated? |
|
|
426
|
-
|---|---|---|
|
|
427
|
-
| OAuth credentials (`config.toml`) | shadow `$HOME/.wrangler/config/default.toml` → symlink to per-profile file | ✅ per profile |
|
|
428
|
-
| Account-id cache (`wrangler-account.json`) | per-profile `WRANGLER_CACHE_DIR` (= `<profilesDir>/<name>/cache/`) | ✅ per profile |
|
|
429
|
-
| Pages config cache (`pages-config-cache.json`) | same as above | ✅ per profile |
|
|
430
|
-
| Miniflare dev registry | `WRANGLER_REGISTRY_PATH` = `$realHome/.wrangler/registry` | ❌ shared on purpose (cross-profile worker discovery during local dev) |
|
|
431
|
-
| Wrangler debug logs | `WRANGLER_LOG_PATH` = `$realHome/.wrangler/logs` | ❌ shared (append-only, harmless) |
|
|
432
|
-
| Project-local state (`./.wrangler/state/`, `./node_modules/.cache/wrangler`) | inside the project directory | ❌ shared at project level (per-project, but not per-profile) |
|
|
433
|
-
| `cloudflared` binary | `CLOUDFLARED_PATH` or `~/.wrangler/cloudflared/` | ❌ shared (binary, not account-scoped) |
|
|
434
|
-
| Shell history, npm cache, git config, ssh keys | symlinked through to real `$HOME` | ❌ shared by design (so `exec` subshells feel like a normal terminal) |
|
|
435
|
-
|
|
436
|
-
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.
|
|
437
|
-
|
|
438
|
-
## CI guidance
|
|
439
|
-
|
|
440
|
-
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.
|
|
441
|
-
|
|
442
|
-
## Paths and environment
|
|
443
|
-
|
|
444
|
-
- `--profile <name>` / `-p <name>` — profile for this invocation (v1.0: `-p` means `--profile`)
|
|
445
|
-
- `--profiles <path>` — profiles directory (long form only since v1.0)
|
|
446
|
-
- `-c, --config <path>` — Wrangler config path
|
|
447
|
-
- `WRANGLER_PROFILE` — profile to use when no `--profile` flag is given
|
|
448
|
-
- `WRANGLER_CONFIG_PATH`, `WRANGLER_ACCOUNTS_DIR`, `XDG_CONFIG_HOME` — path overrides
|
|
449
|
-
|
|
450
|
-
## Output conventions
|
|
451
|
-
|
|
452
|
-
Use `--json` when another tool needs to parse results. All v1.0 commands that produce structured data support `--json`.
|
|
453
|
-
|
|
454
|
-
## Naming rules
|
|
455
|
-
|
|
456
|
-
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.
|
|
457
|
-
|
|
458
|
-
## Deprecated
|
|
459
|
-
|
|
460
|
-
- `wrangler-accounts use <name>` — deprecated, prints warning. Use `default <name>` for persistence or `--profile <name>` for one-shot.
|
|
461
|
-
- `wrangler-accounts sync-active` — deprecated alias for `sync-default`.
|