@leeguoo/wrangler-accounts 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 leeguoo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # wrangler-accounts
2
+
3
+ Local CLI to manage multiple Cloudflare Wrangler login profiles by saving and swapping the Wrangler config file.
4
+
5
+ ## What it does
6
+
7
+ - Save the current Wrangler config as a named profile
8
+ - Switch between profiles by copying a saved config into place
9
+ - List or inspect status (active profile and matching profile)
10
+ - Optional automatic backups when switching
11
+
12
+ ## Install (npm)
13
+
14
+ ```bash
15
+ npm i -g @leeguoo/wrangler-accounts
16
+ ```
17
+
18
+ ## Install (local)
19
+
20
+ From this repo:
21
+
22
+ ```bash
23
+ npm link
24
+ ```
25
+
26
+ Or run directly:
27
+
28
+ ```bash
29
+ node bin/wrangler-accounts.js <command>
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ```bash
35
+ wrangler-accounts list
36
+ wrangler-accounts status
37
+ wrangler-accounts save work
38
+ wrangler-accounts use personal
39
+ wrangler-accounts remove old
40
+ ```
41
+
42
+ ## Options
43
+
44
+ ```text
45
+ -c, --config <path> Wrangler config path
46
+ -p, --profiles <path> Profiles directory
47
+ --json JSON output for list/status
48
+ -f, --force Overwrite existing profile on save
49
+ --backup Backup current config on use (default)
50
+ --no-backup Disable backup on use
51
+ ```
52
+
53
+ ## Environment variables
54
+
55
+ - WRANGLER_CONFIG_PATH
56
+ - WRANGLER_ACCOUNTS_DIR
57
+ - XDG_CONFIG_HOME
58
+
59
+ ## Defaults
60
+
61
+ If you do not specify a config path, the CLI checks for these and uses the first existing path:
62
+
63
+ - ~/.wrangler/config/default.toml
64
+ - ~/.config/.wrangler/config/default.toml
65
+ - ~/.config/wrangler/config/default.toml
66
+
67
+ The profiles directory defaults to:
68
+
69
+ - $XDG_CONFIG_HOME/wrangler-accounts (if set)
70
+ - ~/.config/wrangler-accounts
71
+
72
+ ## Notes
73
+
74
+ - Profile names accept only letters, numbers, dot, underscore, and dash.
75
+ - On `use`, the current config is backed up into `__backup-YYYYMMDD-HHMMSS` unless you pass `--no-backup`.
@@ -0,0 +1,353 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const os = require("os");
7
+ const crypto = require("crypto");
8
+
9
+ function die(message) {
10
+ console.error(`Error: ${message}`);
11
+ process.exit(1);
12
+ }
13
+
14
+ function printHelp(exitCode = 0) {
15
+ const text = `wrangler-accounts - manage multiple Wrangler login profiles
16
+
17
+ Usage:
18
+ wrangler-accounts <command> [options]
19
+
20
+ Commands:
21
+ list
22
+ status
23
+ save <name>
24
+ use <name>
25
+ remove <name>
26
+
27
+ Options:
28
+ -c, --config <path> Wrangler config path
29
+ -p, --profiles <path> Profiles directory
30
+ --json JSON output for list/status
31
+ -f, --force Overwrite existing profile on save
32
+ --backup Backup current config on use (default)
33
+ --no-backup Disable backup on use
34
+ -h, --help Show help
35
+
36
+ Env:
37
+ WRANGLER_CONFIG_PATH
38
+ WRANGLER_ACCOUNTS_DIR
39
+ XDG_CONFIG_HOME
40
+
41
+ Examples:
42
+ wrangler-accounts save work
43
+ wrangler-accounts use personal
44
+ `;
45
+ console.log(text);
46
+ process.exit(exitCode);
47
+ }
48
+
49
+ function expandHome(p) {
50
+ if (!p) return p;
51
+ if (p === "~") return os.homedir();
52
+ if (p.startsWith("~/")) return path.join(os.homedir(), p.slice(2));
53
+ return p;
54
+ }
55
+
56
+ function resolvePath(p) {
57
+ if (!p) return p;
58
+ return path.resolve(expandHome(p));
59
+ }
60
+
61
+ function parseArgs(argv) {
62
+ const opts = {
63
+ json: false,
64
+ force: false,
65
+ backup: true,
66
+ };
67
+ const rest = [];
68
+ for (let i = 0; i < argv.length; i += 1) {
69
+ const arg = argv[i];
70
+ if (arg === "--help" || arg === "-h") {
71
+ opts.help = true;
72
+ } else if (arg === "--json") {
73
+ opts.json = true;
74
+ } else if (arg === "--force" || arg === "-f") {
75
+ opts.force = true;
76
+ } else if (arg === "--backup") {
77
+ opts.backup = true;
78
+ } else if (arg === "--no-backup") {
79
+ opts.backup = false;
80
+ } else if (arg === "--config" || arg === "-c") {
81
+ opts.config = argv[i + 1];
82
+ if (!opts.config) die("Missing value for --config");
83
+ i += 1;
84
+ } else if (arg === "--profiles" || arg === "-p") {
85
+ opts.profiles = argv[i + 1];
86
+ if (!opts.profiles) die("Missing value for --profiles");
87
+ i += 1;
88
+ } else {
89
+ rest.push(arg);
90
+ }
91
+ }
92
+ return { opts, rest };
93
+ }
94
+
95
+ function detectConfigPath(cliPath) {
96
+ if (cliPath) return resolvePath(cliPath);
97
+ if (process.env.WRANGLER_CONFIG_PATH) {
98
+ return resolvePath(process.env.WRANGLER_CONFIG_PATH);
99
+ }
100
+
101
+ const home = os.homedir();
102
+ const candidates = [
103
+ path.join(home, ".wrangler", "config", "default.toml"),
104
+ path.join(home, ".config", ".wrangler", "config", "default.toml"),
105
+ path.join(home, ".config", "wrangler", "config", "default.toml"),
106
+ ];
107
+
108
+ for (const candidate of candidates) {
109
+ if (fs.existsSync(candidate)) return candidate;
110
+ }
111
+
112
+ return candidates[0];
113
+ }
114
+
115
+ function detectProfilesDir(cliPath) {
116
+ if (cliPath) return resolvePath(cliPath);
117
+ if (process.env.WRANGLER_ACCOUNTS_DIR) {
118
+ return resolvePath(process.env.WRANGLER_ACCOUNTS_DIR);
119
+ }
120
+
121
+ const xdg = process.env.XDG_CONFIG_HOME;
122
+ if (xdg) return path.join(resolvePath(xdg), "wrangler-accounts");
123
+
124
+ return path.join(os.homedir(), ".config", "wrangler-accounts");
125
+ }
126
+
127
+ function ensureDir(dirPath) {
128
+ fs.mkdirSync(dirPath, { recursive: true });
129
+ }
130
+
131
+ function isValidName(name) {
132
+ return /^[A-Za-z0-9._-]+$/.test(name);
133
+ }
134
+
135
+ function listProfiles(profilesDir) {
136
+ if (!fs.existsSync(profilesDir)) return [];
137
+ const entries = fs.readdirSync(profilesDir, { withFileTypes: true });
138
+ return entries
139
+ .filter((entry) => entry.isDirectory())
140
+ .map((entry) => entry.name)
141
+ .filter((name) => fs.existsSync(path.join(profilesDir, name, "config.toml")))
142
+ .sort();
143
+ }
144
+
145
+ function fileHash(filePath) {
146
+ const data = fs.readFileSync(filePath);
147
+ return crypto.createHash("sha256").update(data).digest("hex");
148
+ }
149
+
150
+ function filesEqual(pathA, pathB) {
151
+ if (!fs.existsSync(pathA) || !fs.existsSync(pathB)) return false;
152
+ const statA = fs.statSync(pathA);
153
+ const statB = fs.statSync(pathB);
154
+ if (statA.size !== statB.size) return false;
155
+ return fileHash(pathA) === fileHash(pathB);
156
+ }
157
+
158
+ function writeMeta(profileDir, name, sourcePath) {
159
+ const configPath = path.join(profileDir, "config.toml");
160
+ const stat = fs.statSync(configPath);
161
+ const meta = {
162
+ name,
163
+ savedAt: new Date().toISOString(),
164
+ sourcePath,
165
+ bytes: stat.size,
166
+ sha256: fileHash(configPath),
167
+ };
168
+ fs.writeFileSync(path.join(profileDir, "meta.json"), JSON.stringify(meta, null, 2));
169
+ }
170
+
171
+ function setActiveProfile(profilesDir, name) {
172
+ ensureDir(profilesDir);
173
+ fs.writeFileSync(path.join(profilesDir, "active"), `${name}\n`);
174
+ }
175
+
176
+ function getActiveProfile(profilesDir) {
177
+ const activePath = path.join(profilesDir, "active");
178
+ if (!fs.existsSync(activePath)) return null;
179
+ const value = fs.readFileSync(activePath, "utf8").trim();
180
+ return value.length ? value : null;
181
+ }
182
+
183
+ function timestampForFile() {
184
+ const now = new Date();
185
+ const pad = (n) => String(n).padStart(2, "0");
186
+ return [
187
+ now.getFullYear(),
188
+ pad(now.getMonth() + 1),
189
+ pad(now.getDate()),
190
+ "-",
191
+ pad(now.getHours()),
192
+ pad(now.getMinutes()),
193
+ pad(now.getSeconds()),
194
+ ].join("");
195
+ }
196
+
197
+ function backupCurrentConfig(configPath, profilesDir) {
198
+ const backupName = `__backup-${timestampForFile()}`;
199
+ const backupDir = path.join(profilesDir, backupName);
200
+ ensureDir(backupDir);
201
+ fs.copyFileSync(configPath, path.join(backupDir, "config.toml"));
202
+ writeMeta(backupDir, backupName, configPath);
203
+ return backupName;
204
+ }
205
+
206
+ function findMatchingProfile(profilesDir, configPath) {
207
+ if (!fs.existsSync(configPath)) return null;
208
+ const configHash = fileHash(configPath);
209
+ const profiles = listProfiles(profilesDir);
210
+ for (const name of profiles) {
211
+ const profileConfig = path.join(profilesDir, name, "config.toml");
212
+ if (fileHash(profileConfig) === configHash) return name;
213
+ }
214
+ return null;
215
+ }
216
+
217
+ function saveProfile(name, configPath, profilesDir, force) {
218
+ if (!isValidName(name)) {
219
+ die(`Invalid profile name: ${name}`);
220
+ }
221
+ if (!fs.existsSync(configPath)) {
222
+ die(`Config file not found: ${configPath}`);
223
+ }
224
+
225
+ const profileDir = path.join(profilesDir, name);
226
+ if (fs.existsSync(profileDir) && !force) {
227
+ die(`Profile exists: ${name} (use --force to overwrite)`);
228
+ }
229
+
230
+ ensureDir(profileDir);
231
+ fs.copyFileSync(configPath, path.join(profileDir, "config.toml"));
232
+ writeMeta(profileDir, name, configPath);
233
+ }
234
+
235
+ function useProfile(name, configPath, profilesDir, backup) {
236
+ if (!isValidName(name)) {
237
+ die(`Invalid profile name: ${name}`);
238
+ }
239
+
240
+ const profileDir = path.join(profilesDir, name);
241
+ const profileConfig = path.join(profileDir, "config.toml");
242
+ if (!fs.existsSync(profileConfig)) {
243
+ die(`Profile not found: ${name}`);
244
+ }
245
+
246
+ let backupName = null;
247
+ if (backup && fs.existsSync(configPath) && !filesEqual(configPath, profileConfig)) {
248
+ backupName = backupCurrentConfig(configPath, profilesDir);
249
+ }
250
+
251
+ ensureDir(path.dirname(configPath));
252
+ fs.copyFileSync(profileConfig, configPath);
253
+ setActiveProfile(profilesDir, name);
254
+
255
+ return backupName;
256
+ }
257
+
258
+ function removeProfile(name, profilesDir) {
259
+ if (!isValidName(name)) {
260
+ die(`Invalid profile name: ${name}`);
261
+ }
262
+ const profileDir = path.join(profilesDir, name);
263
+ if (!fs.existsSync(profileDir)) {
264
+ die(`Profile not found: ${name}`);
265
+ }
266
+
267
+ fs.rmSync(profileDir, { recursive: true, force: true });
268
+
269
+ const active = getActiveProfile(profilesDir);
270
+ if (active === name) {
271
+ const activePath = path.join(profilesDir, "active");
272
+ if (fs.existsSync(activePath)) fs.unlinkSync(activePath);
273
+ }
274
+ }
275
+
276
+ function main() {
277
+ const { opts, rest } = parseArgs(process.argv.slice(2));
278
+ if (opts.help) printHelp(0);
279
+
280
+ const command = rest[0];
281
+ if (!command) printHelp(1);
282
+
283
+ const configPath = detectConfigPath(opts.config);
284
+ const profilesDir = detectProfilesDir(opts.profiles);
285
+
286
+ if (command === "list") {
287
+ const profiles = listProfiles(profilesDir);
288
+ if (opts.json) {
289
+ console.log(JSON.stringify(profiles, null, 2));
290
+ } else if (profiles.length === 0) {
291
+ console.log("No profiles found.");
292
+ } else {
293
+ console.log(profiles.join("\n"));
294
+ }
295
+ return;
296
+ }
297
+
298
+ if (command === "status") {
299
+ const profiles = listProfiles(profilesDir);
300
+ const active = getActiveProfile(profilesDir);
301
+ const match = findMatchingProfile(profilesDir, configPath);
302
+ const payload = {
303
+ configPath,
304
+ configExists: fs.existsSync(configPath),
305
+ profilesDir,
306
+ profileCount: profiles.length,
307
+ profiles,
308
+ activeProfile: active,
309
+ matchingProfile: match,
310
+ };
311
+
312
+ if (opts.json) {
313
+ console.log(JSON.stringify(payload, null, 2));
314
+ } else {
315
+ console.log(`Config: ${payload.configPath} (${payload.configExists ? "exists" : "missing"})`);
316
+ console.log(`Profiles: ${payload.profilesDir} (${payload.profileCount})`);
317
+ console.log(`Active: ${payload.activeProfile || "-"}`);
318
+ console.log(`Match: ${payload.matchingProfile || "-"}`);
319
+ }
320
+ return;
321
+ }
322
+
323
+ if (command === "save") {
324
+ const name = rest[1];
325
+ if (!name) die("Missing profile name for save");
326
+ ensureDir(profilesDir);
327
+ saveProfile(name, configPath, profilesDir, opts.force);
328
+ console.log(`Saved profile '${name}' from ${configPath}`);
329
+ return;
330
+ }
331
+
332
+ if (command === "use") {
333
+ const name = rest[1];
334
+ if (!name) die("Missing profile name for use");
335
+ ensureDir(profilesDir);
336
+ const backupName = useProfile(name, configPath, profilesDir, opts.backup);
337
+ const backupNote = backupName ? ` (backup: ${backupName})` : "";
338
+ console.log(`Switched to profile '${name}'${backupNote}`);
339
+ return;
340
+ }
341
+
342
+ if (command === "remove") {
343
+ const name = rest[1];
344
+ if (!name) die("Missing profile name for remove");
345
+ removeProfile(name, profilesDir);
346
+ console.log(`Removed profile '${name}'`);
347
+ return;
348
+ }
349
+
350
+ die(`Unknown command: ${command}`);
351
+ }
352
+
353
+ main();
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@leeguoo/wrangler-accounts",
3
+ "version": "0.1.0",
4
+ "description": "Local CLI to manage multiple Cloudflare Wrangler login profiles.",
5
+ "license": "MIT",
6
+ "bin": {
7
+ "wrangler-accounts": "bin/wrangler-accounts.js"
8
+ },
9
+ "files": [
10
+ "bin"
11
+ ],
12
+ "keywords": [
13
+ "cloudflare",
14
+ "wrangler",
15
+ "accounts",
16
+ "cli"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/leeguooooo/wrangler-accounts.git"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/leeguooooo/wrangler-accounts/issues"
24
+ },
25
+ "homepage": "https://github.com/leeguooooo/wrangler-accounts#readme",
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "engines": {
30
+ "node": ">=16"
31
+ }
32
+ }