@leeguoo/wrangler-accounts 1.0.0 → 1.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.
@@ -408,15 +408,66 @@ function main() {
408
408
 
409
409
  if (command === "list") {
410
410
  const profiles = listProfiles(profilesDir, { includeBackups });
411
+ const defaultName = getDefaultProfile(profilesDir);
412
+ const activeName = getActiveProfile(profilesDir);
413
+
414
+ const entries = profiles.map((name) => {
415
+ const profileDir = path.join(profilesDir, name);
416
+ const cfgPath = path.join(profileDir, "config.toml");
417
+ const session = readSessionState(cfgPath);
418
+ const meta = readMeta(profileDir);
419
+ const identity = getMetaIdentity(meta);
420
+ let status;
421
+ if (session.expired === true) status = "expired";
422
+ else if (session.expired === false) status = "valid";
423
+ else status = "unknown";
424
+ return {
425
+ name,
426
+ isDefault: name === defaultName,
427
+ isActive: name === activeName,
428
+ status,
429
+ expirationTime: session.expirationTime,
430
+ identity,
431
+ };
432
+ });
433
+
434
+ if (opts.plain) {
435
+ // --plain keeps the v1.0 contract: one name per line, scriptable.
436
+ if (entries.length) console.log(entries.map((e) => e.name).join("\n"));
437
+ return;
438
+ }
439
+
411
440
  if (opts.json) {
412
- console.log(JSON.stringify(profiles, null, 2));
413
- } else if (opts.plain) {
414
- if (profiles.length) console.log(profiles.join("\n"));
415
- } else if (profiles.length === 0) {
441
+ console.log(JSON.stringify(entries, null, 2));
442
+ return;
443
+ }
444
+
445
+ // Text output: human-friendly table with status markers.
446
+ if (entries.length === 0) {
416
447
  console.log("No profiles found.");
417
- } else {
418
- console.log(profiles.join("\n"));
448
+ return;
419
449
  }
450
+ if (defaultName) console.log(`Default: ${defaultName}\n`);
451
+ const nameW = Math.max(4, ...entries.map((e) => e.name.length));
452
+ const statusW = 8; // fits 'expired', 'valid', 'unknown'
453
+ const header = ` ${"NAME".padEnd(nameW)} ${"STATUS".padEnd(statusW)} IDENTITY`;
454
+ console.log(header);
455
+ for (const e of entries) {
456
+ const marker = e.isDefault ? "*" : " ";
457
+ const statusLabel =
458
+ e.status === "expired" ? "EXPIRED"
459
+ : e.status === "valid" ? "valid"
460
+ : "unknown";
461
+ const idStr = e.identity ? describeIdentity(e.identity) : "(no identity)";
462
+ const exp = e.expirationTime ? ` (${e.expirationTime})` : "";
463
+ console.log(
464
+ `${marker} ${e.name.padEnd(nameW)} ${statusLabel.padEnd(statusW)} ${idStr}${e.status === "expired" ? exp : ""}`,
465
+ );
466
+ }
467
+ console.log();
468
+ console.log(
469
+ `Legend: * = default profile, EXPIRED = OAuth session needs 'wrangler-accounts login <name>'`,
470
+ );
420
471
  return;
421
472
  }
422
473
 
@@ -0,0 +1,105 @@
1
+ 'use strict';
2
+
3
+ const { spawnSync: defaultSpawnSync } = require('node:child_process');
4
+ const path = require('node:path');
5
+
6
+ const { resolvePath, detectConfigPath } = require('./paths');
7
+ const { listProfiles, readMeta } = require('./profile-store');
8
+
9
+ function parseWranglerWhoamiOutput(output) {
10
+ const text = output || '';
11
+ const emailMatch = text.match(/associated with the email ([^\n]+?)\.\s*$/m);
12
+ const rowRegex = /│\s*(.+?)\s*│\s*([a-f0-9]{32})\s*│/g;
13
+ const rows = [...text.matchAll(rowRegex)];
14
+ const row = rows[0];
15
+
16
+ if (!emailMatch && !row) return null;
17
+
18
+ return {
19
+ email: emailMatch ? emailMatch[1].trim() : null,
20
+ accountName: row ? row[1].trim() : null,
21
+ accountId: row ? row[2].trim() : null,
22
+ };
23
+ }
24
+
25
+ function getWranglerAuthPath(env = process.env) {
26
+ return detectConfigPath(undefined, env);
27
+ }
28
+
29
+ function canInspectIdentity(configPath, env = process.env) {
30
+ return resolvePath(configPath) === resolvePath(getWranglerAuthPath(env));
31
+ }
32
+
33
+ function getCurrentIdentity(configPath, { env = process.env, spawn = defaultSpawnSync } = {}) {
34
+ if (!canInspectIdentity(configPath, env)) {
35
+ return {
36
+ identity: null,
37
+ error: `Identity lookup only works for the active Wrangler auth file: ${getWranglerAuthPath(env)}`,
38
+ };
39
+ }
40
+
41
+ const result = spawn('wrangler', ['whoami'], { encoding: 'utf8' });
42
+
43
+ if (result.error) {
44
+ return { identity: null, error: result.error.message };
45
+ }
46
+
47
+ const output = `${result.stdout || ''}\n${result.stderr || ''}`;
48
+ if (result.status !== 0) {
49
+ return {
50
+ identity: null,
51
+ error: output.includes('Not logged in')
52
+ ? 'Not logged in'
53
+ : `wrangler whoami exited with code ${result.status}`,
54
+ };
55
+ }
56
+
57
+ const identity = parseWranglerWhoamiOutput(output);
58
+ if (!identity) {
59
+ return { identity: null, error: "Failed to parse 'wrangler whoami' output" };
60
+ }
61
+
62
+ return { identity, error: null };
63
+ }
64
+
65
+ function getMetaIdentity(meta) {
66
+ if (!meta || !meta.identity) return null;
67
+ const { email = null, accountName = null, accountId = null } = meta.identity;
68
+ if (!email && !accountName && !accountId) return null;
69
+ return { email, accountName, accountId };
70
+ }
71
+
72
+ function identitiesMatch(left, right) {
73
+ if (!left || !right) return false;
74
+ if (left.accountId && right.accountId) return left.accountId === right.accountId;
75
+ if (left.email && right.email) return left.email === right.email;
76
+ return false;
77
+ }
78
+
79
+ function describeIdentity(identity) {
80
+ if (!identity) return 'unknown';
81
+ const parts = [];
82
+ if (identity.email) parts.push(identity.email);
83
+ if (identity.accountId) parts.push(identity.accountId);
84
+ return parts.join(' / ') || 'unknown';
85
+ }
86
+
87
+ function findProfilesByIdentity(profilesDir, identity, { includeBackups = false } = {}) {
88
+ if (!identity) return [];
89
+ const profiles = listProfiles(profilesDir, { includeBackups });
90
+ return profiles.filter((name) => {
91
+ const meta = readMeta(path.join(profilesDir, name));
92
+ return identitiesMatch(identity, getMetaIdentity(meta));
93
+ });
94
+ }
95
+
96
+ module.exports = {
97
+ parseWranglerWhoamiOutput,
98
+ getWranglerAuthPath,
99
+ canInspectIdentity,
100
+ getCurrentIdentity,
101
+ getMetaIdentity,
102
+ identitiesMatch,
103
+ describeIdentity,
104
+ findProfilesByIdentity,
105
+ };
@@ -0,0 +1,186 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const { spawnSync } = require('node:child_process');
7
+
8
+ /**
9
+ * Create a per-invocation shadow HOME directory.
10
+ *
11
+ * Layout:
12
+ * $shadow/
13
+ * .wrangler/config/default.toml → symlink to profileCfg (token refresh
14
+ * syncs back)
15
+ * .npmrc → symlink to $realHome/.npmrc
16
+ * .ssh → symlink to $realHome/.ssh
17
+ * Library → symlink to $realHome/Library
18
+ * ... every other top-level entry in realHome except `.wrangler`
19
+ *
20
+ * Caller MUST call cleanupShadow(shadow) when done.
21
+ *
22
+ * @param {object} args
23
+ * @param {string} args.realHome
24
+ * @param {string} args.profileCfg - path to the profile's config.toml file
25
+ * @param {string} [args.label] - optional label for the tmpdir name
26
+ * @returns {string} path to the shadow HOME
27
+ */
28
+ function createShadowHome({ realHome, profileCfg, label = 'wa' }) {
29
+ if (!realHome || !fs.existsSync(realHome)) {
30
+ throw new Error(`real HOME does not exist: ${realHome}`);
31
+ }
32
+ if (!profileCfg || !fs.existsSync(profileCfg)) {
33
+ throw new Error(`profile config does not exist: ${profileCfg}`);
34
+ }
35
+
36
+ const shadow = fs.mkdtempSync(path.join(os.tmpdir(), `${label}-`));
37
+ fs.chmodSync(shadow, 0o700);
38
+
39
+ // Mirror every top-level entry from real HOME except .wrangler.
40
+ for (const entry of fs.readdirSync(realHome)) {
41
+ if (entry === '.wrangler') continue;
42
+ try {
43
+ fs.symlinkSync(
44
+ path.join(realHome, entry),
45
+ path.join(shadow, entry),
46
+ );
47
+ } catch (err) {
48
+ // If symlinking a specific entry fails (permissions, weird file type),
49
+ // log to stderr and continue. Missing entries are a UX problem, not a
50
+ // correctness one — the subprocess will just not find that file.
51
+ process.stderr.write(
52
+ `[wrangler-accounts] skip symlink ${entry}: ${err.message}\n`,
53
+ );
54
+ }
55
+ }
56
+
57
+ // The one file that matters — a real symlink to the profile file so
58
+ // Wrangler's in-place writeFileSync token refreshes flow back into the
59
+ // saved profile automatically.
60
+ const shadowWranglerConfig = path.join(shadow, '.wrangler', 'config');
61
+ fs.mkdirSync(shadowWranglerConfig, { recursive: true });
62
+ fs.symlinkSync(
63
+ profileCfg,
64
+ path.join(shadowWranglerConfig, 'default.toml'),
65
+ );
66
+
67
+ return shadow;
68
+ }
69
+
70
+ /**
71
+ * Remove a shadow HOME. Safe because the shadow contains only symlinks and
72
+ * small directories owned by this process. fs.rmSync does not follow
73
+ * symlinks (it unlinks them), so files in real HOME are never at risk.
74
+ */
75
+ function cleanupShadow(shadow) {
76
+ if (!shadow) return;
77
+ try {
78
+ fs.rmSync(shadow, { recursive: true, force: true });
79
+ } catch (err) {
80
+ process.stderr.write(
81
+ `[wrangler-accounts] cleanup warning: ${err.message}\n`,
82
+ );
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Build the environment variable set that every isolated child process gets.
88
+ * This is a pure function — no I/O.
89
+ */
90
+ function buildIsolatedEnv({
91
+ shadow,
92
+ realHome,
93
+ profile,
94
+ baseEnv = process.env,
95
+ cloudflaredPath = null,
96
+ }) {
97
+ const env = { ...baseEnv };
98
+ env.HOME = shadow;
99
+ env.WRANGLER_PROFILE = profile;
100
+ env.WRANGLER_ACCOUNT = profile;
101
+ env.WRANGLER_ACCOUNT_REAL_HOME = realHome;
102
+ env.WRANGLER_REGISTRY_PATH = path.join(realHome, '.wrangler', 'registry');
103
+ env.WRANGLER_CACHE_DIR = path.join(realHome, '.wrangler', 'cache');
104
+ env.WRANGLER_LOG_PATH = path.join(realHome, '.wrangler', 'logs');
105
+ env.WRANGLER_SEND_METRICS = 'false';
106
+ if (cloudflaredPath) {
107
+ env.CLOUDFLARED_PATH = cloudflaredPath;
108
+ }
109
+ return env;
110
+ }
111
+
112
+ /**
113
+ * Spawn a command inside a shadow HOME for the given profile.
114
+ * Handles setup, spawning, and cleanup in a try/finally so cleanup runs
115
+ * even on unexpected errors.
116
+ *
117
+ * @param {object} args
118
+ * @param {string} args.profile
119
+ * @param {string} args.profileCfg
120
+ * @param {string} args.realHome
121
+ * @param {string} args.command
122
+ * @param {string[]} args.args
123
+ * @param {object} [args.baseEnv]
124
+ * @param {boolean} [args.captureStdout]
125
+ * @param {string|null} [args.cloudflaredPath]
126
+ * @returns {{exitCode: number, stdout?: string, stderr?: string}}
127
+ */
128
+ function runIsolated({
129
+ profile,
130
+ profileCfg,
131
+ realHome,
132
+ command,
133
+ args,
134
+ baseEnv = process.env,
135
+ captureStdout = false,
136
+ cloudflaredPath = null,
137
+ }) {
138
+ const shadow = createShadowHome({
139
+ realHome,
140
+ profileCfg,
141
+ label: `wa-${profile}`,
142
+ });
143
+ const env = buildIsolatedEnv({
144
+ shadow,
145
+ realHome,
146
+ profile,
147
+ baseEnv,
148
+ cloudflaredPath,
149
+ });
150
+
151
+ let result;
152
+ try {
153
+ result = spawnSync(command, args, {
154
+ stdio: captureStdout ? ['inherit', 'pipe', 'pipe'] : 'inherit',
155
+ env,
156
+ encoding: 'utf8',
157
+ });
158
+ } finally {
159
+ cleanupShadow(shadow);
160
+ }
161
+
162
+ // spawnSync surfaces spawn-time errors (ENOENT, EACCES, etc.) via
163
+ // result.error. Forward the message to stderr so the user can diagnose
164
+ // "command not found" scenarios instead of seeing a silent exit 1.
165
+ if (result.error) {
166
+ process.stderr.write(
167
+ `[wrangler-accounts] failed to spawn '${command}': ${result.error.message}\n`,
168
+ );
169
+ }
170
+
171
+ const exitCode =
172
+ result.status == null ? (result.signal ? 128 : 1) : result.status;
173
+
174
+ return {
175
+ exitCode,
176
+ stdout: captureStdout ? result.stdout || '' : undefined,
177
+ stderr: captureStdout ? result.stderr || '' : undefined,
178
+ };
179
+ }
180
+
181
+ module.exports = {
182
+ createShadowHome,
183
+ cleanupShadow,
184
+ buildIsolatedEnv,
185
+ runIsolated,
186
+ };
package/lib/paths.js ADDED
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+
7
+ function expandHome(p) {
8
+ if (!p) return p;
9
+ if (p === '~') return os.homedir();
10
+ if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2));
11
+ return p;
12
+ }
13
+
14
+ function resolvePath(p) {
15
+ if (!p) return p;
16
+ return path.resolve(expandHome(p));
17
+ }
18
+
19
+ function detectConfigPath(cliPath, env = process.env) {
20
+ if (cliPath) return resolvePath(cliPath);
21
+ if (env.WRANGLER_CONFIG_PATH) {
22
+ return resolvePath(env.WRANGLER_CONFIG_PATH);
23
+ }
24
+
25
+ const home = os.homedir();
26
+ const candidates = [
27
+ path.join(home, '.wrangler', 'config', 'default.toml'),
28
+ path.join(home, 'Library', 'Preferences', '.wrangler', 'config', 'default.toml'),
29
+ path.join(home, '.config', '.wrangler', 'config', 'default.toml'),
30
+ path.join(home, '.config', 'wrangler', 'config', 'default.toml'),
31
+ ];
32
+
33
+ for (const candidate of candidates) {
34
+ if (fs.existsSync(candidate)) return candidate;
35
+ }
36
+
37
+ return candidates[0];
38
+ }
39
+
40
+ function detectProfilesDir(cliPath, env = process.env) {
41
+ if (cliPath) return resolvePath(cliPath);
42
+ if (env.WRANGLER_ACCOUNTS_DIR) {
43
+ return resolvePath(env.WRANGLER_ACCOUNTS_DIR);
44
+ }
45
+
46
+ const xdg = env.XDG_CONFIG_HOME;
47
+ if (xdg) return path.join(resolvePath(xdg), 'wrangler-accounts');
48
+
49
+ return path.join(os.homedir(), '.config', 'wrangler-accounts');
50
+ }
51
+
52
+ module.exports = {
53
+ expandHome,
54
+ resolvePath,
55
+ detectConfigPath,
56
+ detectProfilesDir,
57
+ };
@@ -0,0 +1,213 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+
7
+ function ensureDir(dirPath) {
8
+ fs.mkdirSync(dirPath, { recursive: true });
9
+ }
10
+
11
+ function isValidName(name) {
12
+ return /^[A-Za-z0-9._-]+$/.test(name);
13
+ }
14
+
15
+ function isBackupName(name) {
16
+ return name.startsWith('__backup-');
17
+ }
18
+
19
+ function listProfiles(profilesDir, { includeBackups = false } = {}) {
20
+ if (!fs.existsSync(profilesDir)) return [];
21
+ const entries = fs.readdirSync(profilesDir, { withFileTypes: true });
22
+ return entries
23
+ .filter((entry) => entry.isDirectory())
24
+ .map((entry) => entry.name)
25
+ .filter((name) => includeBackups || !isBackupName(name))
26
+ .filter((name) => fs.existsSync(path.join(profilesDir, name, 'config.toml')))
27
+ .sort();
28
+ }
29
+
30
+ function fileHash(filePath) {
31
+ const data = fs.readFileSync(filePath);
32
+ return crypto.createHash('sha256').update(data).digest('hex');
33
+ }
34
+
35
+ function readExpirationTime(filePath) {
36
+ if (!fs.existsSync(filePath)) return null;
37
+ const text = fs.readFileSync(filePath, 'utf8');
38
+ const match = text.match(/^\s*expiration_time\s*=\s*"([^"]+)"/m);
39
+ return match ? match[1] : null;
40
+ }
41
+
42
+ function readSessionState(filePath) {
43
+ const expirationTime = readExpirationTime(filePath);
44
+ if (!expirationTime) {
45
+ return { expirationTime: null, expired: null };
46
+ }
47
+
48
+ const expiresAt = new Date(expirationTime);
49
+ if (Number.isNaN(expiresAt.getTime())) {
50
+ return { expirationTime, expired: null };
51
+ }
52
+
53
+ return {
54
+ expirationTime,
55
+ expired: expiresAt.getTime() <= Date.now(),
56
+ };
57
+ }
58
+
59
+ function filesEqual(pathA, pathB) {
60
+ if (!fs.existsSync(pathA) || !fs.existsSync(pathB)) return false;
61
+ const statA = fs.statSync(pathA);
62
+ const statB = fs.statSync(pathB);
63
+ if (statA.size !== statB.size) return false;
64
+ return fileHash(pathA) === fileHash(pathB);
65
+ }
66
+
67
+ function writeMeta(profileDir, name, sourcePath, identity = null) {
68
+ const configPath = path.join(profileDir, 'config.toml');
69
+ const stat = fs.statSync(configPath);
70
+ const meta = {
71
+ name,
72
+ savedAt: new Date().toISOString(),
73
+ sourcePath,
74
+ bytes: stat.size,
75
+ sha256: fileHash(configPath),
76
+ };
77
+ if (identity) {
78
+ meta.identity = identity;
79
+ }
80
+ fs.writeFileSync(path.join(profileDir, 'meta.json'), JSON.stringify(meta, null, 2));
81
+ }
82
+
83
+ function readMeta(profileDir) {
84
+ const metaPath = path.join(profileDir, 'meta.json');
85
+ if (!fs.existsSync(metaPath)) return null;
86
+ try {
87
+ return JSON.parse(fs.readFileSync(metaPath, 'utf8'));
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+
93
+ function setActiveProfile(profilesDir, name) {
94
+ ensureDir(profilesDir);
95
+ fs.writeFileSync(path.join(profilesDir, 'active'), `${name}\n`);
96
+ }
97
+
98
+ function getActiveProfile(profilesDir) {
99
+ const activePath = path.join(profilesDir, 'active');
100
+ if (!fs.existsSync(activePath)) return null;
101
+ const value = fs.readFileSync(activePath, 'utf8').trim();
102
+ return value.length ? value : null;
103
+ }
104
+
105
+ function getDefaultProfile(profilesDir) {
106
+ const p = path.join(profilesDir, 'default');
107
+ if (!fs.existsSync(p)) return null;
108
+ const raw = fs.readFileSync(p, 'utf8').trim();
109
+ return raw.length ? raw : null;
110
+ }
111
+
112
+ function setDefaultProfile(profilesDir, name) {
113
+ ensureDir(profilesDir);
114
+ fs.writeFileSync(path.join(profilesDir, 'default'), `${name}\n`);
115
+ }
116
+
117
+ function unsetDefaultProfile(profilesDir) {
118
+ const p = path.join(profilesDir, 'default');
119
+ if (fs.existsSync(p)) fs.unlinkSync(p);
120
+ }
121
+
122
+ function timestampForFile() {
123
+ const now = new Date();
124
+ const pad = (n) => String(n).padStart(2, '0');
125
+ return [
126
+ now.getFullYear(),
127
+ pad(now.getMonth() + 1),
128
+ pad(now.getDate()),
129
+ '-',
130
+ pad(now.getHours()),
131
+ pad(now.getMinutes()),
132
+ pad(now.getSeconds()),
133
+ ].join('');
134
+ }
135
+
136
+ function backupCurrentConfig(configPath, profilesDir) {
137
+ const backupName = `__backup-${timestampForFile()}`;
138
+ const backupDir = path.join(profilesDir, backupName);
139
+ ensureDir(backupDir);
140
+ fs.copyFileSync(configPath, path.join(backupDir, 'config.toml'));
141
+ writeMeta(backupDir, backupName, configPath);
142
+ return backupName;
143
+ }
144
+
145
+ function findMatchingProfile(profilesDir, configPath, { includeBackups = false } = {}) {
146
+ if (!fs.existsSync(configPath)) return null;
147
+ const configHash = fileHash(configPath);
148
+ const profiles = listProfiles(profilesDir, { includeBackups });
149
+ for (const name of profiles) {
150
+ const profileConfig = path.join(profilesDir, name, 'config.toml');
151
+ if (fileHash(profileConfig) === configHash) return name;
152
+ }
153
+ return null;
154
+ }
155
+
156
+ function saveProfile(name, configPath, profilesDir, force, identity = null) {
157
+ if (!isValidName(name)) {
158
+ throw new Error(`Invalid profile name: ${name}`);
159
+ }
160
+ if (!fs.existsSync(configPath)) {
161
+ throw new Error(`Config file not found: ${configPath}`);
162
+ }
163
+
164
+ const profileDir = path.join(profilesDir, name);
165
+ if (fs.existsSync(profileDir) && !force) {
166
+ throw new Error(`Profile exists: ${name} (use --force to overwrite)`);
167
+ }
168
+
169
+ ensureDir(profileDir);
170
+ fs.copyFileSync(configPath, path.join(profileDir, 'config.toml'));
171
+ writeMeta(profileDir, name, configPath, identity);
172
+ }
173
+
174
+ function removeProfile(name, profilesDir) {
175
+ if (!isValidName(name)) {
176
+ throw new Error(`Invalid profile name: ${name}`);
177
+ }
178
+ const profileDir = path.join(profilesDir, name);
179
+ if (!fs.existsSync(profileDir)) {
180
+ throw new Error(`Profile not found: ${name}`);
181
+ }
182
+
183
+ fs.rmSync(profileDir, { recursive: true, force: true });
184
+
185
+ const active = getActiveProfile(profilesDir);
186
+ if (active === name) {
187
+ const activePath = path.join(profilesDir, 'active');
188
+ if (fs.existsSync(activePath)) fs.unlinkSync(activePath);
189
+ }
190
+ }
191
+
192
+ module.exports = {
193
+ ensureDir,
194
+ isValidName,
195
+ isBackupName,
196
+ listProfiles,
197
+ fileHash,
198
+ readExpirationTime,
199
+ readSessionState,
200
+ filesEqual,
201
+ writeMeta,
202
+ readMeta,
203
+ setActiveProfile,
204
+ getActiveProfile,
205
+ getDefaultProfile,
206
+ setDefaultProfile,
207
+ unsetDefaultProfile,
208
+ timestampForFile,
209
+ backupCurrentConfig,
210
+ findMatchingProfile,
211
+ saveProfile,
212
+ removeProfile,
213
+ };
package/lib/resolve.js ADDED
@@ -0,0 +1,101 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const { isValidName, getDefaultProfile } = require('./profile-store');
7
+
8
+ class ResolveError extends Error {
9
+ constructor(message, code = 'PROFILE_NOT_FOUND') {
10
+ super(message);
11
+ this.name = 'ResolveError';
12
+ this.code = code;
13
+ }
14
+ }
15
+
16
+ function profileExists(name, profilesDir) {
17
+ return fs.existsSync(path.join(profilesDir, name, 'config.toml'));
18
+ }
19
+
20
+ function assertValid(name) {
21
+ if (!isValidName(name)) {
22
+ throw new ResolveError(`Invalid profile name: ${name}`, 'INVALID_NAME');
23
+ }
24
+ }
25
+
26
+ function assertExists(name, profilesDir) {
27
+ if (!profileExists(name, profilesDir)) {
28
+ throw new ResolveError(`Profile not found: ${name}`, 'PROFILE_NOT_FOUND');
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Resolve a profile name using AWS-style precedence.
34
+ *
35
+ * Order:
36
+ * 1. Explicit `--profile <name>` / `-p <name>` (cliProfile)
37
+ * 2. Positional shorthand — first positional arg, only when it matches a
38
+ * saved profile AND is not a management subcommand name
39
+ * 3. `$WRANGLER_PROFILE`
40
+ * 4. Persistent default from profilesDir/default
41
+ * 5. Hard error with actionable hint
42
+ *
43
+ * @param {object} args
44
+ * @param {string|null} args.cliProfile
45
+ * @param {string|null} args.positional
46
+ * @param {object} args.env
47
+ * @param {string} args.profilesDir
48
+ * @param {Set<string>} [args.managementSubcommands]
49
+ * @returns {{ name: string, source: 'cli' | 'positional' | 'env' | 'default' }}
50
+ * @throws ResolveError
51
+ */
52
+ function resolveProfile({
53
+ cliProfile,
54
+ positional,
55
+ env,
56
+ profilesDir,
57
+ managementSubcommands = new Set(),
58
+ }) {
59
+ // 1. Explicit --profile / -p
60
+ if (cliProfile) {
61
+ assertValid(cliProfile);
62
+ assertExists(cliProfile, profilesDir);
63
+ return { name: cliProfile, source: 'cli' };
64
+ }
65
+
66
+ // 2. Positional shorthand
67
+ if (positional && !managementSubcommands.has(positional)) {
68
+ if (isValidName(positional) && profileExists(positional, profilesDir)) {
69
+ return { name: positional, source: 'positional' };
70
+ }
71
+ }
72
+
73
+ // 3. Env var
74
+ const envProfile = env && env.WRANGLER_PROFILE;
75
+ if (envProfile && envProfile.length) {
76
+ assertValid(envProfile);
77
+ assertExists(envProfile, profilesDir);
78
+ return { name: envProfile, source: 'env' };
79
+ }
80
+
81
+ // 4. Persistent default
82
+ const def = getDefaultProfile(profilesDir);
83
+ if (def) {
84
+ assertValid(def);
85
+ assertExists(def, profilesDir);
86
+ return { name: def, source: 'default' };
87
+ }
88
+
89
+ // 5. Error
90
+ throw new ResolveError(
91
+ [
92
+ 'No profile specified. Options:',
93
+ ' - wrangler-accounts --profile <name> ...',
94
+ ' - WRANGLER_PROFILE=<name> wrangler-accounts ...',
95
+ ' - wrangler-accounts default <name> (set a persistent default)',
96
+ ].join('\n'),
97
+ 'NO_PROFILE',
98
+ );
99
+ }
100
+
101
+ module.exports = { resolveProfile, ResolveError };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leeguoo/wrangler-accounts",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "AWS-style multi-account convenience for Cloudflare Wrangler — per-invocation OAuth isolation via shadow HOME.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "files": [
10
10
  "bin",
11
+ "lib",
11
12
  "completions",
12
13
  "skills"
13
14
  ],
@@ -33,7 +34,9 @@
33
34
  "access": "public"
34
35
  },
35
36
  "scripts": {
36
- "test": "node --test test/*.test.js"
37
+ "test": "node --test test/*.test.js",
38
+ "verify-package": "node scripts/verify-package.js",
39
+ "prepublishOnly": "npm test && npm run verify-package"
37
40
  },
38
41
  "engines": {
39
42
  "node": ">=16"