@leeguoo/wrangler-accounts 1.1.0 → 1.2.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.
- package/bin/wrangler-accounts.js +123 -15
- package/lib/isolation.js +6 -1
- package/package.json +1 -1
- package/skills/wrangler-accounts/SKILL.md +6 -1
package/bin/wrangler-accounts.js
CHANGED
|
@@ -96,6 +96,29 @@ function parseDuration(s) {
|
|
|
96
96
|
return n * mult;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
// Format an ISO expiration timestamp as a compact relative + absolute
|
|
100
|
+
// string, e.g. "in 14d (2026-04-24)" or "30d ago (2026-03-11)".
|
|
101
|
+
function formatExpiry(iso, now = Date.now()) {
|
|
102
|
+
if (!iso) return "(unknown)";
|
|
103
|
+
const then = new Date(iso).getTime();
|
|
104
|
+
if (Number.isNaN(then)) return "(unknown)";
|
|
105
|
+
const delta = then - now;
|
|
106
|
+
const abs = Math.abs(delta);
|
|
107
|
+
const day = 86400000;
|
|
108
|
+
const hour = 3600000;
|
|
109
|
+
const minute = 60000;
|
|
110
|
+
let relative;
|
|
111
|
+
if (abs >= day) {
|
|
112
|
+
relative = `${Math.floor(abs / day)}d`;
|
|
113
|
+
} else if (abs >= hour) {
|
|
114
|
+
relative = `${Math.floor(abs / hour)}h`;
|
|
115
|
+
} else {
|
|
116
|
+
relative = `${Math.max(1, Math.floor(abs / minute))}m`;
|
|
117
|
+
}
|
|
118
|
+
const date = iso.slice(0, 10); // YYYY-MM-DD
|
|
119
|
+
return delta >= 0 ? `in ${relative} (${date})` : `${relative} ago (${date})`;
|
|
120
|
+
}
|
|
121
|
+
|
|
99
122
|
// Thin wrappers that turn thrown errors into die() calls, so the lib
|
|
100
123
|
// functions remain pure / testable without depending on process.exit.
|
|
101
124
|
function saveProfile(...args) {
|
|
@@ -213,6 +236,8 @@ function parseArgs(argv) {
|
|
|
213
236
|
opts.backup = false;
|
|
214
237
|
} else if (arg === "--unset") {
|
|
215
238
|
opts.unset = true;
|
|
239
|
+
} else if (arg === "--deep" || arg === "--verify") {
|
|
240
|
+
opts.deep = true;
|
|
216
241
|
} else if (arg === "--config" || arg === "-c") {
|
|
217
242
|
opts.config = argv[i + 1];
|
|
218
243
|
if (!opts.config) die("Missing value for --config");
|
|
@@ -428,9 +453,59 @@ function main() {
|
|
|
428
453
|
status,
|
|
429
454
|
expirationTime: session.expirationTime,
|
|
430
455
|
identity,
|
|
456
|
+
verified: null,
|
|
457
|
+
verifyError: null,
|
|
431
458
|
};
|
|
432
459
|
});
|
|
433
460
|
|
|
461
|
+
// --deep: actually run `wrangler whoami` inside a shadow HOME for
|
|
462
|
+
// each profile. This is the only authoritative check — the fast
|
|
463
|
+
// status column above is derived purely from the saved
|
|
464
|
+
// expiration_time, which does not tell us whether the refresh token
|
|
465
|
+
// still works or whether Cloudflare has revoked the session.
|
|
466
|
+
if (opts.deep) {
|
|
467
|
+
if (entries.length > 0 && !opts.json) {
|
|
468
|
+
process.stderr.write(
|
|
469
|
+
`[wrangler-accounts] running deep check (wrangler whoami) for ${entries.length} profile(s)...\n`,
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
const cloudflaredPath = findCloudflared();
|
|
473
|
+
for (const e of entries) {
|
|
474
|
+
const profileCfg = path.join(profilesDir, e.name, "config.toml");
|
|
475
|
+
try {
|
|
476
|
+
const r = runIsolated({
|
|
477
|
+
profile: e.name,
|
|
478
|
+
profileCfg,
|
|
479
|
+
realHome: os.homedir(),
|
|
480
|
+
command: "wrangler",
|
|
481
|
+
args: ["whoami"],
|
|
482
|
+
baseEnv: process.env,
|
|
483
|
+
captureStdout: true,
|
|
484
|
+
cloudflaredPath,
|
|
485
|
+
});
|
|
486
|
+
const output = `${r.stdout || ""}\n${r.stderr || ""}`;
|
|
487
|
+
if (r.exitCode === 0) {
|
|
488
|
+
const live = parseWranglerWhoamiOutput(output);
|
|
489
|
+
if (live) {
|
|
490
|
+
e.verified = true;
|
|
491
|
+
e.liveIdentity = live;
|
|
492
|
+
} else {
|
|
493
|
+
e.verified = false;
|
|
494
|
+
e.verifyError = "could not parse wrangler whoami output";
|
|
495
|
+
}
|
|
496
|
+
} else {
|
|
497
|
+
e.verified = false;
|
|
498
|
+
e.verifyError = /not logged in/i.test(output)
|
|
499
|
+
? "not logged in (refresh token may be revoked)"
|
|
500
|
+
: `wrangler whoami exit ${r.exitCode}`;
|
|
501
|
+
}
|
|
502
|
+
} catch (err) {
|
|
503
|
+
e.verified = false;
|
|
504
|
+
e.verifyError = err.message;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
434
509
|
if (opts.plain) {
|
|
435
510
|
// --plain keeps the v1.0 contract: one name per line, scriptable.
|
|
436
511
|
if (entries.length) console.log(entries.map((e) => e.name).join("\n"));
|
|
@@ -448,26 +523,59 @@ function main() {
|
|
|
448
523
|
return;
|
|
449
524
|
}
|
|
450
525
|
if (defaultName) console.log(`Default: ${defaultName}\n`);
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
for (const e of entries) {
|
|
456
|
-
const marker = e.isDefault ? "*" : " ";
|
|
457
|
-
const statusLabel =
|
|
526
|
+
const rows = entries.map((e) => ({
|
|
527
|
+
marker: e.isDefault ? "*" : " ",
|
|
528
|
+
name: e.name,
|
|
529
|
+
status:
|
|
458
530
|
e.status === "expired" ? "EXPIRED"
|
|
459
531
|
: e.status === "valid" ? "valid"
|
|
460
|
-
: "unknown"
|
|
461
|
-
|
|
462
|
-
|
|
532
|
+
: "unknown",
|
|
533
|
+
expires: formatExpiry(e.expirationTime),
|
|
534
|
+
verified:
|
|
535
|
+
e.verified === true ? "✓ ok"
|
|
536
|
+
: e.verified === false ? `✗ ${e.verifyError || "failed"}`
|
|
537
|
+
: "—",
|
|
538
|
+
identity: e.identity ? describeIdentity(e.identity) : "(no identity)",
|
|
539
|
+
}));
|
|
540
|
+
const nameW = Math.max(4, ...rows.map((r) => r.name.length));
|
|
541
|
+
const statusW = Math.max(6, ...rows.map((r) => r.status.length));
|
|
542
|
+
const expiresW = Math.max(7, ...rows.map((r) => r.expires.length));
|
|
543
|
+
const verifiedW = Math.max(8, ...rows.map((r) => r.verified.length));
|
|
544
|
+
|
|
545
|
+
let header;
|
|
546
|
+
if (opts.deep) {
|
|
547
|
+
header = ` ${"NAME".padEnd(nameW)} ${"STATUS".padEnd(statusW)} ${"EXPIRES".padEnd(expiresW)} ${"VERIFIED".padEnd(verifiedW)} IDENTITY`;
|
|
548
|
+
} else {
|
|
549
|
+
header = ` ${"NAME".padEnd(nameW)} ${"STATUS".padEnd(statusW)} ${"EXPIRES".padEnd(expiresW)} IDENTITY`;
|
|
550
|
+
}
|
|
551
|
+
console.log(header);
|
|
552
|
+
for (const r of rows) {
|
|
553
|
+
if (opts.deep) {
|
|
554
|
+
console.log(
|
|
555
|
+
`${r.marker} ${r.name.padEnd(nameW)} ${r.status.padEnd(statusW)} ${r.expires.padEnd(expiresW)} ${r.verified.padEnd(verifiedW)} ${r.identity}`,
|
|
556
|
+
);
|
|
557
|
+
} else {
|
|
558
|
+
console.log(
|
|
559
|
+
`${r.marker} ${r.name.padEnd(nameW)} ${r.status.padEnd(statusW)} ${r.expires.padEnd(expiresW)} ${r.identity}`,
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
console.log();
|
|
564
|
+
if (opts.deep) {
|
|
565
|
+
console.log(
|
|
566
|
+
"Legend: * = default profile, VERIFIED ✓ = wrangler whoami succeeded in shadow HOME, ✗ = authoritative failure",
|
|
567
|
+
);
|
|
568
|
+
} else {
|
|
463
569
|
console.log(
|
|
464
|
-
|
|
570
|
+
"Legend: * = default profile, EXPIRED = access token past expiration_time (wrangler may still auto-refresh)",
|
|
571
|
+
);
|
|
572
|
+
console.log(
|
|
573
|
+
" STATUS is derived from the saved file only. For a live check that runs 'wrangler whoami' against Cloudflare,",
|
|
574
|
+
);
|
|
575
|
+
console.log(
|
|
576
|
+
" pass --deep (slower, makes network calls).",
|
|
465
577
|
);
|
|
466
578
|
}
|
|
467
|
-
console.log();
|
|
468
|
-
console.log(
|
|
469
|
-
`Legend: * = default profile, EXPIRED = OAuth session needs 'wrangler-accounts login <name>'`,
|
|
470
|
-
);
|
|
471
579
|
return;
|
|
472
580
|
}
|
|
473
581
|
|
package/lib/isolation.js
CHANGED
|
@@ -150,8 +150,13 @@ function runIsolated({
|
|
|
150
150
|
|
|
151
151
|
let result;
|
|
152
152
|
try {
|
|
153
|
+
// captureStdout mode is used for non-interactive checks (e.g.
|
|
154
|
+
// background `wrangler whoami` during `list --deep`), so we close
|
|
155
|
+
// stdin on the child rather than letting it read from the user's
|
|
156
|
+
// terminal. Normal inherit mode keeps stdin attached for interactive
|
|
157
|
+
// flows like `login` and `exec`.
|
|
153
158
|
result = spawnSync(command, args, {
|
|
154
|
-
stdio: captureStdout ? ['
|
|
159
|
+
stdio: captureStdout ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
155
160
|
env,
|
|
156
161
|
encoding: 'utf8',
|
|
157
162
|
});
|
package/package.json
CHANGED
|
@@ -76,10 +76,15 @@ Use `--json` for structured output.
|
|
|
76
76
|
|
|
77
77
|
### List and inspect profiles
|
|
78
78
|
|
|
79
|
-
- `wrangler-accounts list` /
|
|
79
|
+
- `wrangler-accounts list` — text table with NAME / STATUS / EXPIRES / IDENTITY columns
|
|
80
|
+
- `wrangler-accounts list --json` — structured: array of `{name, isDefault, isActive, status, expirationTime, identity}`
|
|
81
|
+
- `wrangler-accounts list --plain` — one profile name per line (scriptable)
|
|
82
|
+
- `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.
|
|
80
83
|
- `wrangler-accounts status` / `status --json`
|
|
81
84
|
- Pass `--include-backups` to show hidden backup profiles.
|
|
82
85
|
|
|
86
|
+
**Accuracy note:** the `STATUS` column without `--deep` is derived purely from the saved `expiration_time` and tells you *when the access token will expire*, not whether the profile is actually usable. Wrangler auto-refreshes access tokens via the refresh token, so an `EXPIRED` access token is often fine in practice. When the user needs a real verdict, suggest `wrangler-accounts list --deep`.
|
|
87
|
+
|
|
83
88
|
### Save, sync, login, remove
|
|
84
89
|
|
|
85
90
|
- `wrangler-accounts save <name>` — snapshot current Wrangler config as a profile
|