@hepheastus-devkit/claude-switch 1.0.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/README.md +162 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +331 -0
- package/dist/cli.js.map +1 -0
- package/dist/profileManager.d.ts +33 -0
- package/dist/profileManager.js +997 -0
- package/dist/profileManager.js.map +1 -0
- package/dist/registry.d.ts +57 -0
- package/dist/registry.js +118 -0
- package/dist/registry.js.map +1 -0
- package/dist/secureStore.d.ts +9 -0
- package/dist/secureStore.js +118 -0
- package/dist/secureStore.js.map +1 -0
- package/dist/utils.d.ts +66 -0
- package/dist/utils.js +239 -0
- package/dist/utils.js.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,997 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.status = status;
|
|
7
|
+
exports.whoami = whoami;
|
|
8
|
+
exports.listProfiles = listProfiles;
|
|
9
|
+
exports.addProfile = addProfile;
|
|
10
|
+
exports.switchProfile = switchProfile;
|
|
11
|
+
exports.printEnv = printEnv;
|
|
12
|
+
exports.loginAndCapture = loginAndCapture;
|
|
13
|
+
exports.runProfile = runProfile;
|
|
14
|
+
exports.switchByQuery = switchByQuery;
|
|
15
|
+
exports.interactiveSwitch = interactiveSwitch;
|
|
16
|
+
exports.removeProfile = removeProfile;
|
|
17
|
+
exports.getCurrentProfile = getCurrentProfile;
|
|
18
|
+
exports.getPreviousProfile = getPreviousProfile;
|
|
19
|
+
exports.getAllProfileNames = getAllProfileNames;
|
|
20
|
+
exports.getProfileMetadata = getProfileMetadata;
|
|
21
|
+
exports.setAlias = setAlias;
|
|
22
|
+
exports.clearAlias = clearAlias;
|
|
23
|
+
exports.listAliases = listAliases;
|
|
24
|
+
exports.resolveProfileQuery = resolveProfileQuery;
|
|
25
|
+
exports.exportProfiles = exportProfiles;
|
|
26
|
+
exports.importProfiles = importProfiles;
|
|
27
|
+
exports.cleanProfiles = cleanProfiles;
|
|
28
|
+
const path_1 = __importDefault(require("path"));
|
|
29
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
30
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
31
|
+
const ora_1 = __importDefault(require("ora"));
|
|
32
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
33
|
+
const child_process_1 = require("child_process");
|
|
34
|
+
const utils_1 = require("./utils");
|
|
35
|
+
const registry_1 = require("./registry");
|
|
36
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
37
|
+
// status
|
|
38
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
39
|
+
async function status() {
|
|
40
|
+
const activeDir = (0, utils_1.getActiveClaudeDir)();
|
|
41
|
+
const isOverridden = !!process.env.CLAUDE_CONFIG_DIR;
|
|
42
|
+
console.log(chalk_1.default.bold('Claude Code config dir:'), activeDir);
|
|
43
|
+
if (isOverridden) {
|
|
44
|
+
(0, utils_1.logInfo)('Using CLAUDE_CONFIG_DIR (full isolation mode)');
|
|
45
|
+
}
|
|
46
|
+
const credPath = path_1.default.join(activeDir, utils_1.CREDENTIALS_FILE);
|
|
47
|
+
const hasCreds = await fs_extra_1.default.pathExists(credPath);
|
|
48
|
+
if (hasCreds) {
|
|
49
|
+
(0, utils_1.logSuccess)('Credentials file found (.credentials.json) — OAuth session active');
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
(0, utils_1.logWarn)('No .credentials.json found. Using API key or not yet logged in.');
|
|
53
|
+
}
|
|
54
|
+
// Check env for API key
|
|
55
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
56
|
+
const key = process.env.ANTHROPIC_API_KEY;
|
|
57
|
+
const masked = key.slice(0, 10) + '...' + key.slice(-4);
|
|
58
|
+
(0, utils_1.logSuccess)(`ANTHROPIC_API_KEY is set in env: ${masked}`);
|
|
59
|
+
}
|
|
60
|
+
const current = await getCurrentProfile();
|
|
61
|
+
if (current) {
|
|
62
|
+
console.log(chalk_1.default.bold('Tracked current profile:'), chalk_1.default.green(current));
|
|
63
|
+
// Show registry info for current
|
|
64
|
+
const registry = await (0, registry_1.loadRegistry)();
|
|
65
|
+
const entry = (0, registry_1.findEntry)(registry, current);
|
|
66
|
+
if (entry) {
|
|
67
|
+
if (entry.email)
|
|
68
|
+
console.log(chalk_1.default.gray(` Email: ${entry.email}`));
|
|
69
|
+
if (entry.plan)
|
|
70
|
+
console.log(chalk_1.default.gray(` Plan: ${(0, registry_1.formatPlan)(entry.plan)}`));
|
|
71
|
+
console.log(chalk_1.default.gray(` Type: ${entry.accountType}`));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
console.log(chalk_1.default.gray('No active profile tracked (default or unmanaged)'));
|
|
76
|
+
}
|
|
77
|
+
console.log(chalk_1.default.gray(`Profile storage: ${utils_1.PROFILES_DIR}`));
|
|
78
|
+
}
|
|
79
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
80
|
+
// whoami
|
|
81
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
82
|
+
async function whoami() {
|
|
83
|
+
const current = await getCurrentProfile();
|
|
84
|
+
if (!current) {
|
|
85
|
+
(0, utils_1.logWarn)('No active profile tracked. Use "claude-switch add <name>" to save the current account.');
|
|
86
|
+
// Still show env key if present
|
|
87
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
88
|
+
const key = process.env.ANTHROPIC_API_KEY;
|
|
89
|
+
const masked = key.slice(0, 10) + '...' + key.slice(-4);
|
|
90
|
+
(0, utils_1.logInfo)(`Current env ANTHROPIC_API_KEY: ${masked}`);
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const registry = await (0, registry_1.loadRegistry)();
|
|
95
|
+
const entry = (0, registry_1.findEntry)(registry, current);
|
|
96
|
+
console.log('');
|
|
97
|
+
console.log(chalk_1.default.bold('Currently active account'));
|
|
98
|
+
console.log(chalk_1.default.bold('────────────────────────'));
|
|
99
|
+
const metaPath = path_1.default.join((0, utils_1.getProfileDir)(current), utils_1.METADATA_FILE);
|
|
100
|
+
const meta = await fs_extra_1.default.readJson(metaPath).catch(() => ({ name: current, dirName: current, createdAt: '' }));
|
|
101
|
+
console.log(` Name: ${chalk_1.default.green(meta.name || current)}`);
|
|
102
|
+
if (entry?.email) {
|
|
103
|
+
console.log(` Email: ${chalk_1.default.cyan(entry.email)}`);
|
|
104
|
+
}
|
|
105
|
+
if (entry?.plan) {
|
|
106
|
+
const planColors = {
|
|
107
|
+
pro: chalk_1.default.blue, max: chalk_1.default.magenta, free: chalk_1.default.gray, api: chalk_1.default.yellow, unknown: chalk_1.default.gray,
|
|
108
|
+
};
|
|
109
|
+
const color = planColors[entry.plan] || chalk_1.default.white;
|
|
110
|
+
console.log(` Plan: ${color((0, registry_1.formatPlan)(entry.plan))}`);
|
|
111
|
+
}
|
|
112
|
+
const type = entry?.accountType || meta.accountType || 'unknown';
|
|
113
|
+
console.log(` Type: ${chalk_1.default.gray(type === 'apikey' ? 'API Key' : type === 'oauth' ? 'OAuth (claude.ai login)' : 'Unknown')}`);
|
|
114
|
+
if (entry?.lastUsed) {
|
|
115
|
+
console.log(` Used: ${chalk_1.default.gray((0, registry_1.formatRelativeTime)(entry.lastUsed))}`);
|
|
116
|
+
}
|
|
117
|
+
// Show current API key if apikey type
|
|
118
|
+
if (type === 'apikey') {
|
|
119
|
+
const key = await (0, utils_1.readApiKeyFromProfile)(current);
|
|
120
|
+
if (key) {
|
|
121
|
+
const masked = key.slice(0, 10) + '...' + key.slice(-4);
|
|
122
|
+
console.log(` Key: ${chalk_1.default.gray(masked)}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
console.log('');
|
|
126
|
+
}
|
|
127
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
128
|
+
// list
|
|
129
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
130
|
+
async function listProfiles() {
|
|
131
|
+
await (0, utils_1.ensureDir)(utils_1.PROFILES_DIR);
|
|
132
|
+
const names = await getAllProfileNames();
|
|
133
|
+
if (names.length === 0) {
|
|
134
|
+
(0, utils_1.logInfo)('No profiles saved yet.');
|
|
135
|
+
console.log('Run: claude-switch add <name> (saves current credentials)');
|
|
136
|
+
console.log('Or: claude-switch add <name> --api-key (save an API key account)');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const current = await getCurrentProfile();
|
|
140
|
+
const registry = await (0, registry_1.loadRegistry)();
|
|
141
|
+
console.log(chalk_1.default.bold('\nClaude accounts:\n'));
|
|
142
|
+
const metaList = await Promise.all(names.map(async (name, index) => {
|
|
143
|
+
const metaPath = path_1.default.join(utils_1.PROFILES_DIR, name, utils_1.METADATA_FILE);
|
|
144
|
+
const meta = (await fs_extra_1.default.readJson(metaPath).catch(() => null)) || {
|
|
145
|
+
name,
|
|
146
|
+
dirName: name,
|
|
147
|
+
createdAt: '',
|
|
148
|
+
};
|
|
149
|
+
const entry = (0, registry_1.findEntry)(registry, name);
|
|
150
|
+
return { name, index, meta, entry };
|
|
151
|
+
}));
|
|
152
|
+
for (const { name, index, meta, entry } of metaList) {
|
|
153
|
+
const displayName = meta.name || name;
|
|
154
|
+
const num = String(index + 1).padStart(2, '0');
|
|
155
|
+
const isCurrent = current === name;
|
|
156
|
+
const prefix = isCurrent ? chalk_1.default.green('→') : ' ';
|
|
157
|
+
const namePart = isCurrent ? chalk_1.default.green.bold(displayName) : chalk_1.default.bold(displayName);
|
|
158
|
+
// Plan badge
|
|
159
|
+
const plan = entry?.plan || meta.plan;
|
|
160
|
+
const planBadge = plan ? ` ${chalk_1.default.bgBlue.white(` ${(0, registry_1.formatPlan)(plan)} `)}` : '';
|
|
161
|
+
// Account type badge
|
|
162
|
+
const accType = entry?.accountType || meta.accountType || 'unknown';
|
|
163
|
+
const typeBadge = accType === 'apikey'
|
|
164
|
+
? chalk_1.default.yellow(' [API]')
|
|
165
|
+
: accType === 'oauth'
|
|
166
|
+
? chalk_1.default.blue(' [OAuth]')
|
|
167
|
+
: '';
|
|
168
|
+
const email = entry?.email || meta.email || '';
|
|
169
|
+
console.log(`${prefix} ${chalk_1.default.gray(num)}. ${namePart}${planBadge}${typeBadge}${email ? ' ' + chalk_1.default.gray(email) : ''}`);
|
|
170
|
+
const lastUsed = entry?.lastUsed || meta.lastUsed;
|
|
171
|
+
if (lastUsed) {
|
|
172
|
+
console.log(` ${chalk_1.default.gray('last used: ' + (0, registry_1.formatRelativeTime)(lastUsed))}`);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
const created = meta.createdAt ? new Date(meta.createdAt).toLocaleDateString() : 'unknown';
|
|
176
|
+
console.log(` ${chalk_1.default.gray('added: ' + created)}`);
|
|
177
|
+
}
|
|
178
|
+
console.log('');
|
|
179
|
+
}
|
|
180
|
+
console.log(chalk_1.default.gray('Quick switch:'));
|
|
181
|
+
console.log(chalk_1.default.gray(' claude-switch 1 # by row number'));
|
|
182
|
+
console.log(chalk_1.default.gray(' claude-switch work # by name fragment'));
|
|
183
|
+
console.log(chalk_1.default.gray(' claude-switch - # back to previous account'));
|
|
184
|
+
console.log(chalk_1.default.gray(' claude-switch # open interactive picker\n'));
|
|
185
|
+
if (current) {
|
|
186
|
+
console.log(chalk_1.default.gray(`Currently active: ${current}`));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
190
|
+
// add
|
|
191
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
192
|
+
async function addProfile(name, options = {}) {
|
|
193
|
+
const profileDir = (0, utils_1.getProfileDir)(name);
|
|
194
|
+
const dirName = (0, utils_1.sanitizeProfileName)(name);
|
|
195
|
+
if (await (0, utils_1.profileExists)(name) && !options.force) {
|
|
196
|
+
const { overwrite } = await inquirer_1.default.prompt([{
|
|
197
|
+
type: 'confirm',
|
|
198
|
+
name: 'overwrite',
|
|
199
|
+
message: `Profile "${name}" already exists. Overwrite?`,
|
|
200
|
+
default: false,
|
|
201
|
+
}]);
|
|
202
|
+
if (!overwrite) {
|
|
203
|
+
(0, utils_1.logWarn)('Aborted.');
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
await (0, utils_1.ensureDir)(profileDir);
|
|
208
|
+
// ── Determine account type ──────────────────────────────────────────────────
|
|
209
|
+
let apiKey = options.apiKey;
|
|
210
|
+
// --api-key-env: pick up key from environment
|
|
211
|
+
if (!apiKey && options.apiKeyEnv) {
|
|
212
|
+
apiKey = process.env.ANTHROPIC_API_KEY;
|
|
213
|
+
if (!apiKey) {
|
|
214
|
+
(0, utils_1.logError)('--api-key-env was set but ANTHROPIC_API_KEY is not in the current environment.');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Interactive: if no OAuth creds and no key passed, ask what mode
|
|
219
|
+
const activeDir = (0, utils_1.getActiveClaudeDir)();
|
|
220
|
+
const hasOAuthCreds = await fs_extra_1.default.pathExists(path_1.default.join(activeDir, utils_1.CREDENTIALS_FILE));
|
|
221
|
+
const hasEnvKey = !!process.env.ANTHROPIC_API_KEY;
|
|
222
|
+
if (!apiKey && !hasOAuthCreds) {
|
|
223
|
+
if (hasEnvKey) {
|
|
224
|
+
// Auto-suggest picking up the env key
|
|
225
|
+
const { useEnv } = await inquirer_1.default.prompt([{
|
|
226
|
+
type: 'confirm',
|
|
227
|
+
name: 'useEnv',
|
|
228
|
+
message: `No OAuth credentials found. Use current ANTHROPIC_API_KEY from env? (${process.env.ANTHROPIC_API_KEY.slice(0, 12)}...)`,
|
|
229
|
+
default: true,
|
|
230
|
+
}]);
|
|
231
|
+
if (useEnv) {
|
|
232
|
+
apiKey = process.env.ANTHROPIC_API_KEY;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
const { mode } = await inquirer_1.default.prompt([{
|
|
237
|
+
type: 'list',
|
|
238
|
+
name: 'mode',
|
|
239
|
+
message: 'No credentials detected. How will this account authenticate?',
|
|
240
|
+
choices: [
|
|
241
|
+
{ name: 'Enter API key manually', value: 'manual' },
|
|
242
|
+
{ name: 'I will log in via "claude auth login" first', value: 'oauth' },
|
|
243
|
+
{ name: 'Cancel', value: 'cancel' },
|
|
244
|
+
],
|
|
245
|
+
}]);
|
|
246
|
+
if (mode === 'cancel') {
|
|
247
|
+
(0, utils_1.logWarn)('Aborted.');
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (mode === 'oauth') {
|
|
251
|
+
(0, utils_1.logInfo)('Please run "claude auth login" in another terminal, then re-run this command.');
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (mode === 'manual') {
|
|
255
|
+
const { key } = await inquirer_1.default.prompt([{
|
|
256
|
+
type: 'password',
|
|
257
|
+
name: 'key',
|
|
258
|
+
message: 'Paste your ANTHROPIC_API_KEY:',
|
|
259
|
+
validate: (v) => v.startsWith('sk-ant-') ? true : 'Key should start with sk-ant-',
|
|
260
|
+
}]);
|
|
261
|
+
apiKey = key;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const accountType = apiKey ? 'apikey' : hasOAuthCreds ? 'oauth' : 'unknown';
|
|
266
|
+
const mode = options.full ? 'full config snapshot' : apiKey ? 'API key' : 'credentials only';
|
|
267
|
+
const spinner = (0, ora_1.default)(`Saving account "${name}" (${mode})...`).start();
|
|
268
|
+
try {
|
|
269
|
+
if (apiKey) {
|
|
270
|
+
// ── API Key mode ──────────────────────────────────────────────────────
|
|
271
|
+
await (0, utils_1.saveApiKeyToProfile)(dirName, apiKey, options.note);
|
|
272
|
+
}
|
|
273
|
+
else if (options.full) {
|
|
274
|
+
// ── Full snapshot mode ────────────────────────────────────────────────
|
|
275
|
+
const configTarget = path_1.default.join(profileDir, utils_1.CONFIG_SUBDIR);
|
|
276
|
+
await fs_extra_1.default.copy(activeDir, configTarget, { overwrite: true, dereference: true });
|
|
277
|
+
await (0, utils_1.copyCredentials)(activeDir, profileDir);
|
|
278
|
+
await fs_extra_1.default.writeJson(path_1.default.join(profileDir, 'full.json'), { full: true }, { spaces: 2 });
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
// ── Light OAuth mode ──────────────────────────────────────────────────
|
|
282
|
+
await (0, utils_1.copyCredentials)(activeDir, profileDir);
|
|
283
|
+
const activeCred = path_1.default.join(activeDir, utils_1.CREDENTIALS_FILE);
|
|
284
|
+
if (await fs_extra_1.default.pathExists(activeCred)) {
|
|
285
|
+
await fs_extra_1.default.copy(activeCred, path_1.default.join(profileDir, 'credentials.snapshot.json'), { overwrite: true });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// ── Write profile metadata ────────────────────────────────────────────────
|
|
289
|
+
const meta = {
|
|
290
|
+
name,
|
|
291
|
+
dirName,
|
|
292
|
+
createdAt: new Date().toISOString(),
|
|
293
|
+
accountType,
|
|
294
|
+
...(options.email ? { email: options.email } : {}),
|
|
295
|
+
...(options.plan ? { plan: options.plan } : {}),
|
|
296
|
+
...(options.note ? { note: options.note } : {}),
|
|
297
|
+
...(options.apiUrl ? { apiUrl: options.apiUrl } : {}),
|
|
298
|
+
...(options.proxy ? { proxy: options.proxy } : {}),
|
|
299
|
+
};
|
|
300
|
+
await fs_extra_1.default.writeJson(path_1.default.join(profileDir, utils_1.METADATA_FILE), meta, { spaces: 2 });
|
|
301
|
+
// ── Register in registry.json ─────────────────────────────────────────────
|
|
302
|
+
await (0, registry_1.upsertAccount)({
|
|
303
|
+
id: dirName,
|
|
304
|
+
name,
|
|
305
|
+
accountType,
|
|
306
|
+
addedAt: new Date().toISOString(),
|
|
307
|
+
...(options.email ? { email: options.email } : {}),
|
|
308
|
+
...(options.plan ? { plan: options.plan } : {}),
|
|
309
|
+
...(options.apiUrl ? { apiUrl: options.apiUrl } : {}),
|
|
310
|
+
...(options.proxy ? { proxy: options.proxy } : {}),
|
|
311
|
+
});
|
|
312
|
+
spinner.succeed(`Profile "${name}" saved (${mode}).`);
|
|
313
|
+
if (apiKey) {
|
|
314
|
+
const masked = apiKey.slice(0, 10) + '...' + apiKey.slice(-4);
|
|
315
|
+
(0, utils_1.logSuccess)(`API key saved: ${masked}`);
|
|
316
|
+
(0, utils_1.logInfo)('To activate in this shell, run:');
|
|
317
|
+
console.log(chalk_1.default.bold(` eval $(claude-switch env) # bash/zsh`));
|
|
318
|
+
console.log(chalk_1.default.bold(` claude-switch env | Invoke-Expression # PowerShell`));
|
|
319
|
+
}
|
|
320
|
+
else if (options.full) {
|
|
321
|
+
(0, utils_1.logSuccess)('Full config directory snapshotted. Use "claude-switch run ' + name + '" for isolated sessions.');
|
|
322
|
+
}
|
|
323
|
+
else if (accountType === 'oauth') {
|
|
324
|
+
(0, utils_1.logSuccess)('OAuth credentials captured. You can now switch to other accounts and come back.');
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
(0, utils_1.logWarn)('No credentials found. Profile created but may be empty.');
|
|
328
|
+
}
|
|
329
|
+
await setCurrentProfile(dirName);
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
spinner.fail('Failed to save profile');
|
|
333
|
+
(0, utils_1.logError)(err.message || String(err));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
337
|
+
// switch
|
|
338
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
339
|
+
async function switchProfile(name) {
|
|
340
|
+
const profileDir = (0, utils_1.getProfileDir)(name);
|
|
341
|
+
if (!(await (0, utils_1.profileExists)(name))) {
|
|
342
|
+
(0, utils_1.logError)(`Profile "${name}" does not exist.`);
|
|
343
|
+
console.log('Available profiles: run "claude-switch list"');
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const activeDir = (0, utils_1.getActiveClaudeDir)();
|
|
347
|
+
const spinner = (0, ora_1.default)(`Switching to "${name}"...`).start();
|
|
348
|
+
try {
|
|
349
|
+
// Determine account type
|
|
350
|
+
const metaPath = path_1.default.join(profileDir, utils_1.METADATA_FILE);
|
|
351
|
+
const meta = await fs_extra_1.default.readJson(metaPath).catch(() => ({
|
|
352
|
+
name,
|
|
353
|
+
dirName: (0, utils_1.sanitizeProfileName)(name),
|
|
354
|
+
createdAt: new Date().toISOString(),
|
|
355
|
+
}));
|
|
356
|
+
const registry = await (0, registry_1.loadRegistry)();
|
|
357
|
+
const entry = (0, registry_1.findEntry)(registry, (0, utils_1.sanitizeProfileName)(name));
|
|
358
|
+
const accountType = entry?.accountType || meta.accountType || 'unknown';
|
|
359
|
+
if (accountType === 'apikey') {
|
|
360
|
+
// ── API Key switch ──────────────────────────────────────────────────────
|
|
361
|
+
const key = await (0, utils_1.readApiKeyFromProfile)((0, utils_1.sanitizeProfileName)(name));
|
|
362
|
+
if (!key) {
|
|
363
|
+
spinner.fail('No API key found in this profile.');
|
|
364
|
+
(0, utils_1.logInfo)('Re-add it with: claude-switch add ' + name + ' --api-key-env');
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
const apiUrl = entry?.apiUrl || meta.apiUrl;
|
|
368
|
+
const proxy = entry?.proxy || meta.proxy;
|
|
369
|
+
await (0, utils_1.writeShellEnvFiles)(key, apiUrl, proxy);
|
|
370
|
+
spinner.succeed(`Switched to "${name}" (API key mode).`);
|
|
371
|
+
console.log('');
|
|
372
|
+
(0, utils_1.logInfo)('To apply in this shell, run one of:');
|
|
373
|
+
console.log(chalk_1.default.bold(' eval $(claude-switch env) # bash/zsh'));
|
|
374
|
+
console.log(chalk_1.default.bold(' claude-switch env | Invoke-Expression # PowerShell'));
|
|
375
|
+
console.log('');
|
|
376
|
+
(0, utils_1.logInfo)('Or start a new terminal — new sessions will pick it up automatically.');
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
// ── OAuth credential swap ───────────────────────────────────────────────
|
|
380
|
+
// Clear env keys so OAuth credentials can take precedence
|
|
381
|
+
await (0, utils_1.writeShellEnvFiles)(null, null, null);
|
|
382
|
+
// 1. Backup current active credentials
|
|
383
|
+
const backupDir = path_1.default.join(utils_1.SWITCH_DIR, 'current-backup');
|
|
384
|
+
await (0, utils_1.ensureDir)(backupDir);
|
|
385
|
+
await (0, utils_1.copyCredentials)(activeDir, backupDir);
|
|
386
|
+
// 2. Atomically restore profile credentials
|
|
387
|
+
const tempCred = path_1.default.join(activeDir, '.credentials.tmp.json');
|
|
388
|
+
const sourceCred = path_1.default.join(profileDir, utils_1.CREDENTIALS_FILE);
|
|
389
|
+
let restored = false;
|
|
390
|
+
if (await fs_extra_1.default.pathExists(sourceCred)) {
|
|
391
|
+
await fs_extra_1.default.copy(sourceCred, tempCred, { overwrite: true });
|
|
392
|
+
await fs_extra_1.default.move(tempCred, path_1.default.join(activeDir, utils_1.CREDENTIALS_FILE), { overwrite: true });
|
|
393
|
+
restored = true;
|
|
394
|
+
}
|
|
395
|
+
spinner.succeed(`Switched to "${name}".`);
|
|
396
|
+
if (restored) {
|
|
397
|
+
(0, utils_1.logSuccess)('OAuth credentials restored to active Claude directory.');
|
|
398
|
+
(0, utils_1.logInfo)('Restart Claude Code or start a new terminal session for changes to take effect.');
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
(0, utils_1.logWarn)('No credentials file in profile. You may need to log in first.');
|
|
402
|
+
}
|
|
403
|
+
console.log(chalk_1.default.gray(`Active config dir: ${activeDir}`));
|
|
404
|
+
}
|
|
405
|
+
// 3. Update registry (single source of truth for lastUsed)
|
|
406
|
+
await (0, registry_1.touchAccount)((0, utils_1.sanitizeProfileName)(name));
|
|
407
|
+
await setCurrentProfile((0, utils_1.sanitizeProfileName)(name));
|
|
408
|
+
}
|
|
409
|
+
catch (err) {
|
|
410
|
+
spinner.fail('Switch failed');
|
|
411
|
+
(0, utils_1.logError)(err.message || String(err));
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
416
|
+
// env — output shell-sourceable env vars for current profile
|
|
417
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
418
|
+
async function printEnv(shell) {
|
|
419
|
+
const current = await getCurrentProfile();
|
|
420
|
+
if (!current) {
|
|
421
|
+
// No profile — just show whatever is in env
|
|
422
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
423
|
+
console.log((0, utils_1.formatApiKeyExport)(process.env.ANTHROPIC_API_KEY));
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
(0, utils_1.logWarn)('No active profile and no ANTHROPIC_API_KEY in env.');
|
|
427
|
+
process.exit(1);
|
|
428
|
+
}
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const registry = await (0, registry_1.loadRegistry)();
|
|
432
|
+
const entry = (0, registry_1.findEntry)(registry, current);
|
|
433
|
+
const meta = await getProfileMetadata(current);
|
|
434
|
+
const key = await (0, utils_1.readApiKeyFromProfile)(current);
|
|
435
|
+
const apiUrl = entry?.apiUrl || meta?.apiUrl;
|
|
436
|
+
const proxy = entry?.proxy || meta?.proxy;
|
|
437
|
+
const isPowerShell = shell === 'powershell' || (process.platform === 'win32' && !shell);
|
|
438
|
+
const outputs = [];
|
|
439
|
+
if (key) {
|
|
440
|
+
outputs.push(isPowerShell ? `$env:ANTHROPIC_API_KEY="${key}"` : `export ANTHROPIC_API_KEY="${key}"`);
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
outputs.push(isPowerShell ? `Remove-Item Env:\\ANTHROPIC_API_KEY -ErrorAction SilentlyContinue` : `unset ANTHROPIC_API_KEY`);
|
|
444
|
+
}
|
|
445
|
+
if (apiUrl) {
|
|
446
|
+
outputs.push(isPowerShell ? `$env:ANTHROPIC_BASE_URL="${apiUrl}"` : `export ANTHROPIC_BASE_URL="${apiUrl}"`);
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
outputs.push(isPowerShell ? `Remove-Item Env:\\ANTHROPIC_BASE_URL -ErrorAction SilentlyContinue` : `unset ANTHROPIC_BASE_URL`);
|
|
450
|
+
}
|
|
451
|
+
if (proxy) {
|
|
452
|
+
outputs.push(isPowerShell ? `$env:HTTPS_PROXY="${proxy}"` : `export HTTPS_PROXY="${proxy}"`);
|
|
453
|
+
outputs.push(isPowerShell ? `$env:HTTP_PROXY="${proxy}"` : `export HTTP_PROXY="${proxy}"`);
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
outputs.push(isPowerShell ? `Remove-Item Env:\\HTTPS_PROXY -ErrorAction SilentlyContinue` : `unset HTTPS_PROXY`);
|
|
457
|
+
outputs.push(isPowerShell ? `Remove-Item Env:\\HTTP_PROXY -ErrorAction SilentlyContinue` : `unset HTTP_PROXY`);
|
|
458
|
+
}
|
|
459
|
+
console.log(outputs.join('\n'));
|
|
460
|
+
}
|
|
461
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
462
|
+
// login — guided login + auto capture
|
|
463
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
464
|
+
async function loginAndCapture() {
|
|
465
|
+
console.log(chalk_1.default.cyan('\n=== claude-switch login (guided) ===\n'));
|
|
466
|
+
const { mode } = await inquirer_1.default.prompt([{
|
|
467
|
+
type: 'list',
|
|
468
|
+
name: 'mode',
|
|
469
|
+
message: 'What type of account do you want to add?',
|
|
470
|
+
choices: [
|
|
471
|
+
{ name: 'OAuth / claude.ai subscription (runs claude auth login)', value: 'oauth' },
|
|
472
|
+
{ name: 'API key (paste key directly)', value: 'apikey' },
|
|
473
|
+
],
|
|
474
|
+
}]);
|
|
475
|
+
const { profileName } = await inquirer_1.default.prompt([{
|
|
476
|
+
type: 'input',
|
|
477
|
+
name: 'profileName',
|
|
478
|
+
message: 'Name this account (e.g. work, personal):',
|
|
479
|
+
validate: (v) => v.trim().length > 0 ? true : 'Name cannot be empty',
|
|
480
|
+
}]);
|
|
481
|
+
if (mode === 'apikey') {
|
|
482
|
+
const { key } = await inquirer_1.default.prompt([{
|
|
483
|
+
type: 'password',
|
|
484
|
+
name: 'key',
|
|
485
|
+
message: 'Paste your ANTHROPIC_API_KEY:',
|
|
486
|
+
validate: (v) => v.startsWith('sk-ant-') ? true : 'Key should start with sk-ant-',
|
|
487
|
+
}]);
|
|
488
|
+
const { email } = await inquirer_1.default.prompt([{
|
|
489
|
+
type: 'input',
|
|
490
|
+
name: 'email',
|
|
491
|
+
message: 'Account email (optional, for display):',
|
|
492
|
+
}]);
|
|
493
|
+
const { plan } = await inquirer_1.default.prompt([{
|
|
494
|
+
type: 'list',
|
|
495
|
+
name: 'plan',
|
|
496
|
+
message: 'Plan type (for display):',
|
|
497
|
+
choices: ['api', 'pro', 'max', 'free', 'unknown'],
|
|
498
|
+
default: 'api',
|
|
499
|
+
}]);
|
|
500
|
+
await addProfile(profileName, {
|
|
501
|
+
apiKey: key,
|
|
502
|
+
email: email || undefined,
|
|
503
|
+
plan,
|
|
504
|
+
});
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
// OAuth mode: record mtime before, launch claude auth login, detect change
|
|
508
|
+
const activeDir = (0, utils_1.getActiveClaudeDir)();
|
|
509
|
+
const credFile = path_1.default.join(activeDir, utils_1.CREDENTIALS_FILE);
|
|
510
|
+
const mtimeBefore = await fs_extra_1.default.pathExists(credFile)
|
|
511
|
+
? (await fs_extra_1.default.stat(credFile)).mtimeMs
|
|
512
|
+
: 0;
|
|
513
|
+
console.log('\n' + chalk_1.default.bold('Step 1:') + ' Launching "claude auth login"...');
|
|
514
|
+
console.log(chalk_1.default.gray('Complete the browser OAuth flow, then return here.\n'));
|
|
515
|
+
await new Promise((resolve, reject) => {
|
|
516
|
+
const claudeBin = (0, utils_1.resolveClaudeBin)();
|
|
517
|
+
const child = (0, child_process_1.spawn)(claudeBin, ['auth', 'login'], {
|
|
518
|
+
stdio: 'inherit',
|
|
519
|
+
shell: true,
|
|
520
|
+
});
|
|
521
|
+
child.on('error', reject);
|
|
522
|
+
child.on('close', (code) => {
|
|
523
|
+
if (code === 0)
|
|
524
|
+
resolve();
|
|
525
|
+
else
|
|
526
|
+
reject(new Error(`claude auth login exited with code ${code}`));
|
|
527
|
+
});
|
|
528
|
+
}).catch((err) => {
|
|
529
|
+
(0, utils_1.logError)(`Login failed: ${err.message}`);
|
|
530
|
+
(0, utils_1.logInfo)('You can still manually run "claude auth login" then "claude-switch add <name>".');
|
|
531
|
+
});
|
|
532
|
+
// Check if credentials changed
|
|
533
|
+
const credExists = await fs_extra_1.default.pathExists(credFile);
|
|
534
|
+
const mtimeAfter = credExists ? (await fs_extra_1.default.stat(credFile)).mtimeMs : 0;
|
|
535
|
+
if (!credExists || mtimeAfter <= mtimeBefore) {
|
|
536
|
+
(0, utils_1.logWarn)('No new credentials detected after login.');
|
|
537
|
+
(0, utils_1.logInfo)('If you completed the OAuth flow, run: claude-switch add ' + profileName);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
(0, utils_1.logSuccess)('New credentials detected! Saving as profile "' + profileName + '"...');
|
|
541
|
+
const { email } = await inquirer_1.default.prompt([{
|
|
542
|
+
type: 'input',
|
|
543
|
+
name: 'email',
|
|
544
|
+
message: 'Account email (optional, for display):',
|
|
545
|
+
}]);
|
|
546
|
+
const { plan } = await inquirer_1.default.prompt([{
|
|
547
|
+
type: 'list',
|
|
548
|
+
name: 'plan',
|
|
549
|
+
message: 'Plan type (for display):',
|
|
550
|
+
choices: ['pro', 'max', 'free', 'unknown'],
|
|
551
|
+
default: 'unknown',
|
|
552
|
+
}]);
|
|
553
|
+
await addProfile(profileName, {
|
|
554
|
+
email: email || undefined,
|
|
555
|
+
plan,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
559
|
+
// run
|
|
560
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
561
|
+
async function runProfile(name, extraArgs = []) {
|
|
562
|
+
const profileDir = (0, utils_1.getProfileDir)(name);
|
|
563
|
+
if (!(await (0, utils_1.profileExists)(name))) {
|
|
564
|
+
(0, utils_1.logError)(`Profile "${name}" does not exist.`);
|
|
565
|
+
process.exit(1);
|
|
566
|
+
}
|
|
567
|
+
// Check if API key profile
|
|
568
|
+
const key = await (0, utils_1.readApiKeyFromProfile)((0, utils_1.sanitizeProfileName)(name));
|
|
569
|
+
if (key) {
|
|
570
|
+
const env = { ...process.env, ANTHROPIC_API_KEY: key };
|
|
571
|
+
(0, utils_1.logInfo)(`Launching claude with API key from profile "${name}"...`);
|
|
572
|
+
const claudeBin = (0, utils_1.resolveClaudeBin)();
|
|
573
|
+
const child = (0, child_process_1.spawn)(claudeBin, extraArgs, {
|
|
574
|
+
stdio: 'inherit',
|
|
575
|
+
env,
|
|
576
|
+
shell: true,
|
|
577
|
+
});
|
|
578
|
+
child.on('error', (err) => (0, utils_1.logError)(`Failed to launch claude: ${err.message}`));
|
|
579
|
+
child.on('close', (code) => { if (code !== 0)
|
|
580
|
+
(0, utils_1.logWarn)(`Claude exited with code ${code}`); });
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
const fullMarker = path_1.default.join(profileDir, 'full.json');
|
|
584
|
+
const isFull = await fs_extra_1.default.pathExists(fullMarker);
|
|
585
|
+
if (isFull) {
|
|
586
|
+
const configDir = path_1.default.join(profileDir, utils_1.CONFIG_SUBDIR);
|
|
587
|
+
if (await fs_extra_1.default.pathExists(configDir)) {
|
|
588
|
+
(0, utils_1.launchClaudeWithConfigDir)(configDir, extraArgs);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
// Fallback: credential swap then launch
|
|
593
|
+
(0, utils_1.logWarn)('Profile does not have a full snapshot. Performing credential swap then launching...');
|
|
594
|
+
await switchProfile(name);
|
|
595
|
+
const activeDir = (0, utils_1.getActiveClaudeDir)();
|
|
596
|
+
(0, utils_1.logInfo)(`Launching claude (current credentials are now from "${name}")`);
|
|
597
|
+
(0, utils_1.launchClaudeWithConfigDir)(activeDir, extraArgs);
|
|
598
|
+
}
|
|
599
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
600
|
+
// switchByQuery / interactiveSwitch
|
|
601
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
602
|
+
async function switchByQuery(query) {
|
|
603
|
+
const resolved = await resolveProfileQuery(query);
|
|
604
|
+
if (resolved) {
|
|
605
|
+
await switchProfile(resolved);
|
|
606
|
+
return true;
|
|
607
|
+
}
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
async function interactiveSwitch() {
|
|
611
|
+
const names = await getAllProfileNames();
|
|
612
|
+
if (names.length === 0) {
|
|
613
|
+
(0, utils_1.logInfo)('No profiles saved yet. Use "claude-switch login" to add your first account.');
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
const current = await getCurrentProfile();
|
|
617
|
+
const previous = await getPreviousProfile();
|
|
618
|
+
const registry = await (0, registry_1.loadRegistry)();
|
|
619
|
+
const choices = names.map((name, idx) => {
|
|
620
|
+
const num = String(idx + 1).padStart(2, '0');
|
|
621
|
+
const isCurrent = current === name;
|
|
622
|
+
const isPrevious = previous === name && !isCurrent;
|
|
623
|
+
const entry = (0, registry_1.findEntry)(registry, name);
|
|
624
|
+
const planBadge = entry?.plan ? ` [${(0, registry_1.formatPlan)(entry.plan)}]` : '';
|
|
625
|
+
const typeBadge = entry?.accountType === 'apikey' ? ' (API)' : entry?.accountType === 'oauth' ? ' (OAuth)' : '';
|
|
626
|
+
const emailPart = entry?.email ? ` ${entry.email}` : '';
|
|
627
|
+
let label = `${name}${planBadge}${typeBadge}${emailPart}`;
|
|
628
|
+
if (isCurrent)
|
|
629
|
+
label += ` ${chalk_1.default.green('← current')}`;
|
|
630
|
+
if (isPrevious)
|
|
631
|
+
label += ` ${chalk_1.default.yellow('← previous')}`;
|
|
632
|
+
return {
|
|
633
|
+
name: `${num}. ${label}`,
|
|
634
|
+
value: name,
|
|
635
|
+
short: name,
|
|
636
|
+
};
|
|
637
|
+
});
|
|
638
|
+
const { selected } = await inquirer_1.default.prompt([
|
|
639
|
+
{
|
|
640
|
+
type: 'list',
|
|
641
|
+
name: 'selected',
|
|
642
|
+
message: 'Select account to switch to:',
|
|
643
|
+
choices: [
|
|
644
|
+
...choices,
|
|
645
|
+
new inquirer_1.default.Separator(),
|
|
646
|
+
{ name: 'Cancel', value: null },
|
|
647
|
+
],
|
|
648
|
+
pageSize: 14,
|
|
649
|
+
},
|
|
650
|
+
]);
|
|
651
|
+
if (!selected) {
|
|
652
|
+
(0, utils_1.logInfo)('Cancelled.');
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
await switchProfile(selected);
|
|
656
|
+
}
|
|
657
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
658
|
+
// remove
|
|
659
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
660
|
+
async function removeProfile(name) {
|
|
661
|
+
const profileDir = (0, utils_1.getProfileDir)(name);
|
|
662
|
+
if (!(await (0, utils_1.profileExists)(name))) {
|
|
663
|
+
(0, utils_1.logError)(`Profile "${name}" not found.`);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const { confirm } = await inquirer_1.default.prompt([{
|
|
667
|
+
type: 'confirm',
|
|
668
|
+
name: 'confirm',
|
|
669
|
+
message: `Delete profile "${name}" and all its data?`,
|
|
670
|
+
default: false,
|
|
671
|
+
}]);
|
|
672
|
+
if (!confirm) {
|
|
673
|
+
(0, utils_1.logWarn)('Cancelled.');
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
await fs_extra_1.default.remove(profileDir);
|
|
677
|
+
const current = await getCurrentProfile();
|
|
678
|
+
if (current === (0, utils_1.sanitizeProfileName)(name)) {
|
|
679
|
+
await clearCurrentProfile();
|
|
680
|
+
}
|
|
681
|
+
// Remove from registry
|
|
682
|
+
await (0, registry_1.removeAccount)((0, utils_1.sanitizeProfileName)(name));
|
|
683
|
+
(0, utils_1.logSuccess)(`Profile "${name}" removed.`);
|
|
684
|
+
}
|
|
685
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
686
|
+
// Profile state helpers
|
|
687
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
688
|
+
async function getCurrentProfile() {
|
|
689
|
+
const marker = path_1.default.join(utils_1.SWITCH_DIR, 'current-profile.txt');
|
|
690
|
+
if (await fs_extra_1.default.pathExists(marker)) {
|
|
691
|
+
const name = (await fs_extra_1.default.readFile(marker, 'utf8')).trim();
|
|
692
|
+
if (name && await (0, utils_1.profileExists)(name)) {
|
|
693
|
+
return name;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
async function setCurrentProfile(dirName) {
|
|
699
|
+
const current = await getCurrentProfile();
|
|
700
|
+
if (current && current !== dirName) {
|
|
701
|
+
const prevMarker = path_1.default.join(utils_1.SWITCH_DIR, 'previous-profile.txt');
|
|
702
|
+
await fs_extra_1.default.writeFile(prevMarker, current, 'utf8');
|
|
703
|
+
}
|
|
704
|
+
const marker = path_1.default.join(utils_1.SWITCH_DIR, 'current-profile.txt');
|
|
705
|
+
await fs_extra_1.default.writeFile(marker, dirName, 'utf8');
|
|
706
|
+
}
|
|
707
|
+
async function clearCurrentProfile() {
|
|
708
|
+
const marker = path_1.default.join(utils_1.SWITCH_DIR, 'current-profile.txt');
|
|
709
|
+
if (await fs_extra_1.default.pathExists(marker)) {
|
|
710
|
+
await fs_extra_1.default.remove(marker);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
async function getPreviousProfile() {
|
|
714
|
+
const prevMarker = path_1.default.join(utils_1.SWITCH_DIR, 'previous-profile.txt');
|
|
715
|
+
if (await fs_extra_1.default.pathExists(prevMarker)) {
|
|
716
|
+
const name = (await fs_extra_1.default.readFile(prevMarker, 'utf8')).trim();
|
|
717
|
+
if (name && await (0, utils_1.profileExists)(name)) {
|
|
718
|
+
return name;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
async function getAllProfileNames() {
|
|
724
|
+
await (0, utils_1.ensureDir)(utils_1.PROFILES_DIR);
|
|
725
|
+
const entries = await fs_extra_1.default.readdir(utils_1.PROFILES_DIR);
|
|
726
|
+
const checks = await Promise.all(entries.map(async (entry) => {
|
|
727
|
+
const metaPath = path_1.default.join(utils_1.PROFILES_DIR, entry, utils_1.METADATA_FILE);
|
|
728
|
+
return (await fs_extra_1.default.pathExists(metaPath)) ? entry : null;
|
|
729
|
+
}));
|
|
730
|
+
return checks.filter((e) => e !== null);
|
|
731
|
+
}
|
|
732
|
+
async function getProfileMetadata(name) {
|
|
733
|
+
const p = path_1.default.join((0, utils_1.getProfileDir)(name), utils_1.METADATA_FILE);
|
|
734
|
+
if (await fs_extra_1.default.pathExists(p)) {
|
|
735
|
+
return fs_extra_1.default.readJson(p);
|
|
736
|
+
}
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
739
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
740
|
+
// Alias support
|
|
741
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
742
|
+
async function setAlias(nameOrQuery, alias) {
|
|
743
|
+
const resolved = await resolveProfileQuery(nameOrQuery);
|
|
744
|
+
if (!resolved) {
|
|
745
|
+
(0, utils_1.logError)(`Could not resolve profile for "${nameOrQuery}". Use exact name or run list first.`);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const aliases = await (0, utils_1.loadAliases)();
|
|
749
|
+
aliases[alias] = resolved;
|
|
750
|
+
await (0, utils_1.saveAliases)(aliases);
|
|
751
|
+
(0, utils_1.logSuccess)(`Alias "${alias}" set to profile "${resolved}".`);
|
|
752
|
+
(0, utils_1.logInfo)(`You can now use: claude-switch ${alias} or claude-switch switch ${alias}`);
|
|
753
|
+
}
|
|
754
|
+
async function clearAlias(alias) {
|
|
755
|
+
const aliases = await (0, utils_1.loadAliases)();
|
|
756
|
+
if (aliases[alias]) {
|
|
757
|
+
delete aliases[alias];
|
|
758
|
+
await (0, utils_1.saveAliases)(aliases);
|
|
759
|
+
(0, utils_1.logSuccess)(`Alias "${alias}" cleared.`);
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
(0, utils_1.logWarn)(`No alias named "${alias}".`);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
async function listAliases() {
|
|
766
|
+
const aliases = await (0, utils_1.loadAliases)();
|
|
767
|
+
const entries = Object.entries(aliases);
|
|
768
|
+
if (entries.length === 0) {
|
|
769
|
+
(0, utils_1.logInfo)('No aliases set yet. Use "claude-switch alias set <name> <short>"');
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
console.log(chalk_1.default.bold('\nAliases:\n'));
|
|
773
|
+
for (const [alias, target] of entries) {
|
|
774
|
+
console.log(` ${chalk_1.default.cyan(alias)} → ${target}`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
778
|
+
// Profile query resolver (supports aliases, numbers, fragments)
|
|
779
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
780
|
+
async function resolveProfileQuery(query) {
|
|
781
|
+
const trimmed = query.trim().toLowerCase();
|
|
782
|
+
// Special: switch back to previous
|
|
783
|
+
if (trimmed === '-' || trimmed === 'previous' || trimmed === 'prev') {
|
|
784
|
+
return await getPreviousProfile();
|
|
785
|
+
}
|
|
786
|
+
const aliases = await (0, utils_1.loadAliases)();
|
|
787
|
+
if (aliases[query] || aliases[trimmed]) {
|
|
788
|
+
const target = aliases[query] || aliases[trimmed];
|
|
789
|
+
if (await (0, utils_1.profileExists)(target))
|
|
790
|
+
return target;
|
|
791
|
+
}
|
|
792
|
+
const allProfiles = await getAllProfileNames();
|
|
793
|
+
// Exact match
|
|
794
|
+
const exact = allProfiles.find(p => p.toLowerCase() === trimmed);
|
|
795
|
+
if (exact)
|
|
796
|
+
return exact;
|
|
797
|
+
// Numeric row
|
|
798
|
+
if (/^\d+$/.test(trimmed)) {
|
|
799
|
+
const index = parseInt(trimmed, 10) - 1;
|
|
800
|
+
if (index >= 0 && index < allProfiles.length) {
|
|
801
|
+
return allProfiles[index];
|
|
802
|
+
}
|
|
803
|
+
return null;
|
|
804
|
+
}
|
|
805
|
+
// Partial name match
|
|
806
|
+
const matches = allProfiles.filter(name => name.toLowerCase().includes(trimmed));
|
|
807
|
+
if (matches.length === 1) {
|
|
808
|
+
return matches[0];
|
|
809
|
+
}
|
|
810
|
+
// Also try matching against registry email / display name
|
|
811
|
+
const registry = await (0, registry_1.loadRegistry)();
|
|
812
|
+
const emailMatches = allProfiles.filter(name => {
|
|
813
|
+
const e = (0, registry_1.findEntry)(registry, name);
|
|
814
|
+
return e?.email?.toLowerCase().includes(trimmed) || e?.name?.toLowerCase().includes(trimmed);
|
|
815
|
+
});
|
|
816
|
+
if (emailMatches.length === 1)
|
|
817
|
+
return emailMatches[0];
|
|
818
|
+
return null;
|
|
819
|
+
}
|
|
820
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
821
|
+
// Export / Import
|
|
822
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
823
|
+
async function exportProfiles(outputDir, safe = false) {
|
|
824
|
+
await (0, utils_1.ensureDir)(outputDir);
|
|
825
|
+
const modeLabel = safe ? ' (safe mode)' : '';
|
|
826
|
+
const spinner = (0, ora_1.default)(`Exporting profiles to ${outputDir}${modeLabel}...`).start();
|
|
827
|
+
try {
|
|
828
|
+
const names = await getAllProfileNames();
|
|
829
|
+
if (names.length === 0) {
|
|
830
|
+
spinner.warn('No profiles to export.');
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
for (const name of names) {
|
|
834
|
+
const src = (0, utils_1.getProfileDir)(name);
|
|
835
|
+
const dest = path_1.default.join(outputDir, name);
|
|
836
|
+
await (0, utils_1.ensureDir)(dest);
|
|
837
|
+
// Always copy metadata
|
|
838
|
+
const metaSrc = path_1.default.join(src, utils_1.METADATA_FILE);
|
|
839
|
+
if (await fs_extra_1.default.pathExists(metaSrc)) {
|
|
840
|
+
await fs_extra_1.default.copy(metaSrc, path_1.default.join(dest, utils_1.METADATA_FILE));
|
|
841
|
+
}
|
|
842
|
+
if (!safe) {
|
|
843
|
+
// Copy credentials and config in non-safe mode
|
|
844
|
+
const apikeySrc = path_1.default.join(src, utils_1.APIKEY_FILE);
|
|
845
|
+
if (await fs_extra_1.default.pathExists(apikeySrc)) {
|
|
846
|
+
await fs_extra_1.default.copy(apikeySrc, path_1.default.join(dest, utils_1.APIKEY_FILE));
|
|
847
|
+
}
|
|
848
|
+
const oauthSrc = path_1.default.join(src, utils_1.CREDENTIALS_FILE);
|
|
849
|
+
if (await fs_extra_1.default.pathExists(oauthSrc)) {
|
|
850
|
+
await fs_extra_1.default.copy(oauthSrc, path_1.default.join(dest, utils_1.CREDENTIALS_FILE));
|
|
851
|
+
}
|
|
852
|
+
const snapSrc = path_1.default.join(src, 'credentials.snapshot.json');
|
|
853
|
+
if (await fs_extra_1.default.pathExists(snapSrc)) {
|
|
854
|
+
await fs_extra_1.default.copy(snapSrc, path_1.default.join(dest, 'credentials.snapshot.json'));
|
|
855
|
+
}
|
|
856
|
+
const fullMarker = path_1.default.join(src, 'full.json');
|
|
857
|
+
if (await fs_extra_1.default.pathExists(fullMarker)) {
|
|
858
|
+
await fs_extra_1.default.copy(fullMarker, path_1.default.join(dest, 'full.json'));
|
|
859
|
+
}
|
|
860
|
+
const configSrc = path_1.default.join(src, utils_1.CONFIG_SUBDIR);
|
|
861
|
+
if (await fs_extra_1.default.pathExists(configSrc)) {
|
|
862
|
+
await fs_extra_1.default.copy(configSrc, path_1.default.join(dest, utils_1.CONFIG_SUBDIR));
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
// Export aliases and registry
|
|
867
|
+
const aliases = await (0, utils_1.loadAliases)();
|
|
868
|
+
await fs_extra_1.default.writeJson(path_1.default.join(outputDir, 'aliases.json'), aliases, { spaces: 2 });
|
|
869
|
+
const registry = await (0, registry_1.loadRegistry)();
|
|
870
|
+
await fs_extra_1.default.writeJson(path_1.default.join(outputDir, 'registry.json'), registry, { spaces: 2 });
|
|
871
|
+
spinner.succeed(`Exported ${names.length} profiles to ${outputDir}${modeLabel}`);
|
|
872
|
+
if (safe) {
|
|
873
|
+
(0, utils_1.logSuccess)('Safe export completed: API keys and credentials were excluded.');
|
|
874
|
+
}
|
|
875
|
+
else {
|
|
876
|
+
(0, utils_1.logInfo)('You can zip this folder or move it to another machine.');
|
|
877
|
+
}
|
|
878
|
+
(0, utils_1.logInfo)('To import later: claude-switch import ' + outputDir);
|
|
879
|
+
}
|
|
880
|
+
catch (err) {
|
|
881
|
+
spinner.fail('Export failed');
|
|
882
|
+
(0, utils_1.logError)(err.message);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
async function importProfiles(inputDir, force = false) {
|
|
886
|
+
if (!(await fs_extra_1.default.pathExists(inputDir))) {
|
|
887
|
+
(0, utils_1.logError)(`Import path does not exist: ${inputDir}`);
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
const spinner = (0, ora_1.default)(`Importing profiles from ${inputDir}...`).start();
|
|
891
|
+
try {
|
|
892
|
+
const entries = await fs_extra_1.default.readdir(inputDir);
|
|
893
|
+
let imported = 0;
|
|
894
|
+
for (const entry of entries) {
|
|
895
|
+
const srcProfile = path_1.default.join(inputDir, entry);
|
|
896
|
+
if (!(await fs_extra_1.default.stat(srcProfile)).isDirectory())
|
|
897
|
+
continue;
|
|
898
|
+
const metaPath = path_1.default.join(srcProfile, utils_1.METADATA_FILE);
|
|
899
|
+
if (!(await fs_extra_1.default.pathExists(metaPath)))
|
|
900
|
+
continue;
|
|
901
|
+
const dest = (0, utils_1.getProfileDir)(entry);
|
|
902
|
+
if (await (0, utils_1.profileExists)(entry) && !force) {
|
|
903
|
+
(0, utils_1.logWarn)(`Skipping existing profile "${entry}" (use --force to overwrite)`);
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
await fs_extra_1.default.copy(srcProfile, dest, { overwrite: true });
|
|
907
|
+
imported++;
|
|
908
|
+
// Re-register in local registry
|
|
909
|
+
const meta = await fs_extra_1.default.readJson(metaPath).catch(() => ({}));
|
|
910
|
+
if (meta.name) {
|
|
911
|
+
await (0, registry_1.upsertAccount)({
|
|
912
|
+
id: (0, utils_1.sanitizeProfileName)(entry),
|
|
913
|
+
name: meta.name,
|
|
914
|
+
accountType: meta.accountType || 'unknown',
|
|
915
|
+
addedAt: meta.createdAt || new Date().toISOString(),
|
|
916
|
+
email: meta.email,
|
|
917
|
+
plan: meta.plan || undefined,
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
// Import aliases
|
|
922
|
+
const aliasSrc = path_1.default.join(inputDir, 'aliases.json');
|
|
923
|
+
if (await fs_extra_1.default.pathExists(aliasSrc)) {
|
|
924
|
+
const importedAliases = await fs_extra_1.default.readJson(aliasSrc);
|
|
925
|
+
const current = await (0, utils_1.loadAliases)();
|
|
926
|
+
await (0, utils_1.saveAliases)({ ...current, ...importedAliases });
|
|
927
|
+
}
|
|
928
|
+
// Import registry (merge)
|
|
929
|
+
const regSrc = path_1.default.join(inputDir, 'registry.json');
|
|
930
|
+
if (await fs_extra_1.default.pathExists(regSrc)) {
|
|
931
|
+
const importedReg = await fs_extra_1.default.readJson(regSrc).catch(() => ({ accounts: [] }));
|
|
932
|
+
const localReg = await (0, registry_1.loadRegistry)();
|
|
933
|
+
for (const acc of (importedReg.accounts || [])) {
|
|
934
|
+
if (!(0, registry_1.findEntry)(localReg, acc.id)) {
|
|
935
|
+
localReg.accounts.push(acc);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
await fs_extra_1.default.writeJson(registry_1.REGISTRY_FILE, localReg, { spaces: 2 });
|
|
939
|
+
}
|
|
940
|
+
spinner.succeed(`Imported ${imported} profiles.`);
|
|
941
|
+
if (imported > 0)
|
|
942
|
+
(0, utils_1.logSuccess)('Run "claude-switch list" to see them.');
|
|
943
|
+
}
|
|
944
|
+
catch (err) {
|
|
945
|
+
spinner.fail('Import failed');
|
|
946
|
+
(0, utils_1.logError)(err.message);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
950
|
+
// Clean temporary files and caches
|
|
951
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
952
|
+
async function cleanProfiles() {
|
|
953
|
+
const spinner = (0, ora_1.default)('Cleaning temporary files...').start();
|
|
954
|
+
try {
|
|
955
|
+
const backupDir = path_1.default.join(utils_1.SWITCH_DIR, 'current-backup');
|
|
956
|
+
const psFile = path_1.default.join(utils_1.SWITCH_DIR, 'current-apikey.ps1');
|
|
957
|
+
const envFile = path_1.default.join(utils_1.SWITCH_DIR, 'current-apikey.env');
|
|
958
|
+
let cleanedCount = 0;
|
|
959
|
+
if (await fs_extra_1.default.pathExists(backupDir)) {
|
|
960
|
+
await fs_extra_1.default.remove(backupDir);
|
|
961
|
+
cleanedCount++;
|
|
962
|
+
}
|
|
963
|
+
if (await fs_extra_1.default.pathExists(psFile)) {
|
|
964
|
+
await fs_extra_1.default.remove(psFile);
|
|
965
|
+
cleanedCount++;
|
|
966
|
+
}
|
|
967
|
+
if (await fs_extra_1.default.pathExists(envFile)) {
|
|
968
|
+
await fs_extra_1.default.remove(envFile);
|
|
969
|
+
cleanedCount++;
|
|
970
|
+
}
|
|
971
|
+
spinner.succeed(`Cleaned ${cleanedCount} temporary files/directories.`);
|
|
972
|
+
const { cleanIndexedDB } = await inquirer_1.default.prompt([{
|
|
973
|
+
type: 'confirm',
|
|
974
|
+
name: 'cleanIndexedDB',
|
|
975
|
+
message: 'Do you want to clean Claude Code session databases and history (IndexedDB)?',
|
|
976
|
+
default: false,
|
|
977
|
+
}]);
|
|
978
|
+
if (cleanIndexedDB) {
|
|
979
|
+
const dbDir = path_1.default.join(utils_1.APPDATA_CLAUDE_DIR, 'IndexedDB');
|
|
980
|
+
if (await fs_extra_1.default.pathExists(dbDir)) {
|
|
981
|
+
const dbSpinner = (0, ora_1.default)('Clearing Claude IndexedDB...').start();
|
|
982
|
+
await fs_extra_1.default.remove(dbDir).catch((e) => {
|
|
983
|
+
dbSpinner.warn(`Could not clear IndexedDB fully (it might be in use): ${e.message}`);
|
|
984
|
+
});
|
|
985
|
+
dbSpinner.succeed('Claude IndexedDB directories cleared.');
|
|
986
|
+
}
|
|
987
|
+
else {
|
|
988
|
+
(0, utils_1.logInfo)('No Claude IndexedDB directories found.');
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
catch (err) {
|
|
993
|
+
spinner.fail('Cleanup failed');
|
|
994
|
+
(0, utils_1.logError)(err.message);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
//# sourceMappingURL=profileManager.js.map
|