ai-profile-switch 2.2.0 → 2.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +54 -119
- package/README.md +50 -115
- package/package.json +6 -1
- package/src/commands/add.mjs +19 -6
- package/src/commands/init.mjs +44 -45
- package/src/commands/install-shell.mjs +4 -3
- package/src/commands/language.mjs +38 -0
- package/src/commands/list.mjs +13 -6
- package/src/commands/login.mjs +43 -0
- package/src/commands/manage.mjs +108 -51
- package/src/commands/mcp/index.mjs +7 -6
- package/src/commands/migrate.mjs +19 -18
- package/src/commands/remove.mjs +7 -6
- package/src/commands/rename.mjs +15 -10
- package/src/commands/share.mjs +16 -14
- package/src/commands/shell.mjs +78 -15
- package/src/commands/update.mjs +15 -12
- package/src/commands/use.mjs +5 -3
- package/src/index.mjs +14 -36
- package/src/lib/account.mjs +122 -0
- package/src/lib/config.mjs +8 -1
- package/src/lib/constants.mjs +4 -0
- package/src/lib/i18n.mjs +65 -0
- package/src/lib/profile.mjs +62 -19
- package/src/lib/prompt.mjs +105 -38
- package/src/lib/shared.mjs +5 -4
- package/src/lib/ui.mjs +54 -29
- package/src/locales/en.mjs +269 -0
- package/src/locales/ko.mjs +269 -0
package/src/commands/init.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
|
-
import { color,
|
|
3
|
+
import { color, intro, outro, success, sym, warn } from '../lib/ui.mjs';
|
|
4
4
|
import * as prompt from '../lib/prompt.mjs';
|
|
5
5
|
import { readMeta, writeMeta } from '../lib/config.mjs';
|
|
6
6
|
import { createProfile, profileExists, validateProfileName, migrateDir, migrationSource } from '../lib/profile.mjs';
|
|
7
7
|
import { HOME, DEFAULT_CLAUDE_DIR, IS_WINDOWS } from '../lib/constants.mjs';
|
|
8
8
|
import { installAllShells } from '../lib/shell.mjs';
|
|
9
|
+
import { t } from '../lib/i18n.mjs';
|
|
9
10
|
|
|
10
11
|
export function existingClaudeSources() {
|
|
11
12
|
const candidates = [
|
|
@@ -23,87 +24,85 @@ export function existingClaudeSources() {
|
|
|
23
24
|
export async function init() {
|
|
24
25
|
const current = readMeta();
|
|
25
26
|
if (current.profiles.length > 0) {
|
|
26
|
-
warn('
|
|
27
|
-
console.log(`
|
|
28
|
-
console.log(`
|
|
29
|
-
console.log(`
|
|
30
|
-
console.log(`
|
|
27
|
+
warn(t('init.already'));
|
|
28
|
+
console.log(` ${color.dim(t('init.profiles'))} ${current.profiles.join(', ')}`);
|
|
29
|
+
console.log(` ${color.dim(t('init.active'))} ${current.activeProfile || t('common.none')}`);
|
|
30
|
+
console.log(` ${color.dim(t('init.addHint'))} ${color.cyan('cpf add <name>')}`);
|
|
31
|
+
console.log(` ${color.dim(t('init.repairHint'))} ${color.cyan('npx ai-profile-switch@latest install-shell')}`);
|
|
31
32
|
return;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
console.log(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
console.log();
|
|
40
|
-
warn('Close Claude sessions using the configuration you will import. Keep source and destination sessions closed until migration finishes.');
|
|
41
|
-
const count = await prompt.number('How many profiles do you want to set up?', 2);
|
|
42
|
-
console.log();
|
|
43
|
-
const names = [];
|
|
44
|
-
for (let index = 0; index < count; index++) {
|
|
45
|
-
const defaultName = count === 1 ? 'main' : index === 0 ? 'work' : index === 1 ? 'personal' : '';
|
|
46
|
-
const message = count === 1 ? 'Profile name:' : `Profile ${index + 1} name:`;
|
|
47
|
-
names.push(await askProfileName(message, defaultName, names));
|
|
48
|
-
}
|
|
49
|
-
console.log();
|
|
35
|
+
intro(t('init.title'));
|
|
36
|
+
console.log(`${color.dim(sym.rail)} ${color.dim(t('init.tagline'))}`);
|
|
37
|
+
console.log(`${color.dim(sym.rail)} ${color.dim(t('init.closeSessions'))}`);
|
|
38
|
+
console.log(color.dim(sym.rail));
|
|
39
|
+
const names = await askProfileNames();
|
|
50
40
|
const activeProfile = names.length === 1
|
|
51
41
|
? names[0]
|
|
52
|
-
: await prompt.select('
|
|
42
|
+
: await prompt.select(t('init.activeQuestion'), names);
|
|
53
43
|
if (!activeProfile) return;
|
|
54
|
-
const shareSettings = await prompt.confirm('
|
|
44
|
+
const shareSettings = await prompt.confirm(t('init.shareQuestion'), true);
|
|
55
45
|
const sources = existingClaudeSources();
|
|
56
46
|
let migration;
|
|
57
47
|
if (sources.length > 0) {
|
|
58
|
-
const source = await prompt.select('
|
|
48
|
+
const source = await prompt.select(t('init.copyQuestion'), [
|
|
59
49
|
...sources.map(path => ({ label: path, value: path })),
|
|
60
|
-
{ label: '
|
|
50
|
+
{ label: t('init.skip'), value: '__skip__' },
|
|
61
51
|
]);
|
|
62
52
|
if (!source) return;
|
|
63
53
|
if (source !== '__skip__') {
|
|
64
|
-
const target = names.length === 1 ? names[0] : await prompt.select('
|
|
54
|
+
const target = names.length === 1 ? names[0] : await prompt.select(t('migrate.target'), names);
|
|
65
55
|
if (!target) return;
|
|
66
56
|
migration = { source, target };
|
|
67
57
|
}
|
|
68
58
|
}
|
|
69
59
|
for (const name of names) {
|
|
70
|
-
if (profileExists(name)) throw new Error(
|
|
60
|
+
if (profileExists(name)) throw new Error(t('init.dirExists', { name }));
|
|
71
61
|
}
|
|
62
|
+
outro('');
|
|
72
63
|
if (migration) {
|
|
73
64
|
const { backupPath } = migrateDir(migration.source, migration.target, shareSettings);
|
|
74
|
-
success(
|
|
75
|
-
if (backupPath) warn(
|
|
65
|
+
success(t('migrate.copied', { source: migration.source, name: migration.target }));
|
|
66
|
+
if (backupPath) warn(t('migrate.backup', { path: backupPath }));
|
|
76
67
|
}
|
|
77
68
|
for (const name of names) {
|
|
78
69
|
if (migration?.target !== name) createProfile(name, shareSettings);
|
|
79
|
-
success(
|
|
70
|
+
success(t('init.ready', { name }));
|
|
80
71
|
}
|
|
81
72
|
writeMeta({ ...readMeta(), activeProfile, shareSettings });
|
|
82
73
|
const { newlyInstalled, alreadyInstalled } = installAllShells();
|
|
83
74
|
const shells = [...newlyInstalled, ...alreadyInstalled];
|
|
84
|
-
if (shells.length > 0) success(
|
|
85
|
-
success(
|
|
75
|
+
if (shells.length > 0) success(t('init.shells', { shells: shells.join(', ') }));
|
|
76
|
+
success(t('init.activeDone', { name: activeProfile }));
|
|
86
77
|
console.log();
|
|
87
|
-
console.log(`
|
|
78
|
+
console.log(` ${t('init.openTerminal', { command: color.cyan('claude') })}`);
|
|
88
79
|
if (names.length > 1) {
|
|
89
80
|
const other = names.find(name => name !== activeProfile);
|
|
90
81
|
const command = IS_WINDOWS ? `cpf ${other}; claude` : `cpf ${other} && claude`;
|
|
91
|
-
console.log(`
|
|
92
|
-
}
|
|
93
|
-
if (migration) {
|
|
94
|
-
warn('Original configuration was preserved. macOS Keychain credentials are not copied.');
|
|
82
|
+
console.log(` ${t('init.otherProfile')} ${color.cyan(command)}`);
|
|
95
83
|
}
|
|
84
|
+
if (migration) warn(t('migrate.preserved'));
|
|
96
85
|
console.log();
|
|
97
86
|
}
|
|
98
87
|
|
|
99
|
-
|
|
88
|
+
// One prompt takes every name, such as "work, personal".
|
|
89
|
+
async function askProfileNames() {
|
|
100
90
|
while (true) {
|
|
101
|
-
const
|
|
91
|
+
const names = (await prompt.text(t('init.names'), 'work, personal'))
|
|
92
|
+
.split(',').map(name => name.trim()).filter(Boolean);
|
|
93
|
+
const problem = profileNamesProblem(names);
|
|
94
|
+
if (!problem) return names;
|
|
95
|
+
console.log(` ${color.red(problem)}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function profileNamesProblem(names) {
|
|
100
|
+
if (!names.length) return validateProfileName('');
|
|
101
|
+
for (const [index, name] of names.entries()) {
|
|
102
102
|
const error = validateProfileName(name);
|
|
103
|
-
if (error
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
return name;
|
|
103
|
+
if (error) return `${name}: ${error}`;
|
|
104
|
+
if (names.indexOf(name) !== index) return t('init.duplicate', { name });
|
|
105
|
+
if (profileExists(name)) return t('init.used', { name });
|
|
108
106
|
}
|
|
107
|
+
return null;
|
|
109
108
|
}
|
|
@@ -2,12 +2,13 @@ import { mkdirSync } from 'node:fs';
|
|
|
2
2
|
import { PROFILES_DIR } from '../lib/constants.mjs';
|
|
3
3
|
import { installAllShells } from '../lib/shell.mjs';
|
|
4
4
|
import { success, info } from '../lib/ui.mjs';
|
|
5
|
+
import { t } from '../lib/i18n.mjs';
|
|
5
6
|
|
|
6
7
|
export async function installShell() {
|
|
7
8
|
mkdirSync(PROFILES_DIR, { recursive: true });
|
|
8
9
|
const { newlyInstalled, alreadyInstalled } = installAllShells();
|
|
9
10
|
const shells = [...newlyInstalled, ...alreadyInstalled];
|
|
10
|
-
success('
|
|
11
|
-
if (shells.length) success(
|
|
12
|
-
info('
|
|
11
|
+
success(t('install.done'));
|
|
12
|
+
if (shells.length) success(t('install.shells', { shells: shells.join(', ') }));
|
|
13
|
+
info(t('install.next'));
|
|
13
14
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as prompt from '../lib/prompt.mjs';
|
|
2
|
+
import { setLanguagePreference } from '../lib/config.mjs';
|
|
3
|
+
import { LANGUAGES } from '../lib/constants.mjs';
|
|
4
|
+
import { LANGUAGE_NAMES, detectLanguage, language as currentLanguage, savedLanguage, setLanguage, t } from '../lib/i18n.mjs';
|
|
5
|
+
import { color, info, success } from '../lib/ui.mjs';
|
|
6
|
+
|
|
7
|
+
const AUTO = 'auto';
|
|
8
|
+
|
|
9
|
+
function save(choice) {
|
|
10
|
+
setLanguagePreference(choice === AUTO ? null : choice);
|
|
11
|
+
const effective = choice === AUTO ? detectLanguage({ saved: null }) : choice;
|
|
12
|
+
setLanguage(effective);
|
|
13
|
+
success(choice === AUTO
|
|
14
|
+
? t('language.savedAuto', { name: LANGUAGE_NAMES[effective] })
|
|
15
|
+
: t('language.saved', { name: LANGUAGE_NAMES[effective] }));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function language(args = []) {
|
|
19
|
+
if (args.length > 1) throw new Error(t('language.usage'));
|
|
20
|
+
const [choice] = args;
|
|
21
|
+
if (choice !== undefined) {
|
|
22
|
+
if (choice !== AUTO && !LANGUAGES.includes(choice)) throw new Error(t('language.usage'));
|
|
23
|
+
save(choice);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const saved = savedLanguage();
|
|
27
|
+
if (!process.stdin.isTTY) {
|
|
28
|
+
const source = t(saved ? 'language.sourceSaved' : 'language.sourceAuto');
|
|
29
|
+
info(`${t('language.current', { name: LANGUAGE_NAMES[currentLanguage()] })} ${color.dim(`(${source})`)}`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const system = LANGUAGE_NAMES[detectLanguage({ saved: null })];
|
|
33
|
+
const selected = await prompt.select(t('language.title'), [
|
|
34
|
+
{ label: t('language.auto'), value: AUTO, hint: color.dim(t('language.autoHint', { name: system })) },
|
|
35
|
+
...LANGUAGES.map(code => ({ label: LANGUAGE_NAMES[code], value: code })),
|
|
36
|
+
], { initial: saved ? LANGUAGES.indexOf(saved) + 1 : 0 });
|
|
37
|
+
if (selected) save(selected);
|
|
38
|
+
}
|
package/src/commands/list.mjs
CHANGED
|
@@ -1,28 +1,35 @@
|
|
|
1
1
|
import { color, info } from '../lib/ui.mjs';
|
|
2
2
|
import { listProfiles, profileDir } from '../lib/profile.mjs';
|
|
3
3
|
import { getActiveProfile } from '../lib/config.mjs';
|
|
4
|
+
import { HOME } from '../lib/constants.mjs';
|
|
5
|
+
import { accountInfo } from '../lib/account.mjs';
|
|
6
|
+
import { t } from '../lib/i18n.mjs';
|
|
4
7
|
|
|
5
8
|
export async function list() {
|
|
6
9
|
const profiles = listProfiles();
|
|
7
10
|
|
|
8
11
|
if (profiles.length === 0) {
|
|
9
|
-
info('
|
|
12
|
+
info(`${t('profiles.none')} ${t('hint.add')} cpf add work · ${t('hint.setup')} npx ai-profile-switch@latest init`);
|
|
10
13
|
return;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
const active = getActiveProfile();
|
|
17
|
+
const width = Math.max(...profiles.map(name => name.length));
|
|
14
18
|
|
|
15
19
|
console.log();
|
|
16
|
-
console.log(` ${color.bold('
|
|
20
|
+
console.log(` ${color.bold(t('list.title'))}`);
|
|
17
21
|
console.log();
|
|
18
22
|
for (const name of profiles) {
|
|
19
23
|
const isActive = name === active;
|
|
20
24
|
const marker = isActive ? color.green(' * ') : ' ';
|
|
21
|
-
const label = isActive ? color.bold(name) : name;
|
|
22
|
-
const
|
|
23
|
-
|
|
25
|
+
const label = isActive ? color.bold(name.padEnd(width)) : name.padEnd(width);
|
|
26
|
+
const { email, plan } = accountInfo(name);
|
|
27
|
+
const account = [email, plan].filter(Boolean).join(' · ');
|
|
28
|
+
const dir = profileDir(name);
|
|
29
|
+
const shown = dir.startsWith(HOME) ? `~${dir.slice(HOME.length)}` : dir;
|
|
30
|
+
console.log(` ${marker}${label} ${account ? `${account} ` : ''}${color.dim(shown)}`);
|
|
24
31
|
}
|
|
25
32
|
console.log();
|
|
26
|
-
console.log(`
|
|
33
|
+
console.log(` ${t('list.active', { name: color.green(active || t('common.none')) })}`);
|
|
27
34
|
console.log();
|
|
28
35
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { accountInfo } from '../lib/account.mjs';
|
|
3
|
+
import { t } from '../lib/i18n.mjs';
|
|
4
|
+
import { assertRegisteredProfile, profileDir } from '../lib/profile.mjs';
|
|
5
|
+
import { color, info, success, userError, warn } from '../lib/ui.mjs';
|
|
6
|
+
import { accountSummary, resolveClaudeBinary } from './shell.mjs';
|
|
7
|
+
|
|
8
|
+
function runAuthLogin(bin, name) {
|
|
9
|
+
const cmd = bin.endsWith('.js') ? process.execPath : bin;
|
|
10
|
+
const args = bin.endsWith('.js') ? [bin, 'auth', 'login'] : ['auth', 'login'];
|
|
11
|
+
return new Promise(resolve => {
|
|
12
|
+
const child = spawn(cmd, args, {
|
|
13
|
+
stdio: 'inherit', shell: false,
|
|
14
|
+
env: { ...process.env, CLAUDE_CONFIG_DIR: profileDir(name) },
|
|
15
|
+
});
|
|
16
|
+
child.once('error', () => resolve(1));
|
|
17
|
+
child.once('close', code => resolve(code ?? 1));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Signs one profile in again with Claude Code's own login; other profiles keep theirs.
|
|
22
|
+
// explained: the menu already showed the current sign-in and what will happen.
|
|
23
|
+
export async function login(name, { explained = false } = {}) {
|
|
24
|
+
if (!name) throw new Error(t('login.usage'));
|
|
25
|
+
assertRegisteredProfile(name);
|
|
26
|
+
const bin = resolveClaudeBinary();
|
|
27
|
+
if (!bin) throw userError(t('launch.notFound'), [{ label: t('launch.install'), text: 'https://code.claude.com/docs/en/setup' }]);
|
|
28
|
+
const before = accountSummary(accountInfo(name));
|
|
29
|
+
if (!explained) {
|
|
30
|
+
console.log(` ${color.dim(t('login.current'))} ${before || color.dim(t('account.notSignedIn'))}`);
|
|
31
|
+
warn(t('login.closeSessions', { name }));
|
|
32
|
+
}
|
|
33
|
+
console.log(` ${color.dim(t('login.opening'))}`);
|
|
34
|
+
const code = await runAuthLogin(bin, name);
|
|
35
|
+
if (code !== 0) throw new Error(t('login.failed', { code }));
|
|
36
|
+
const after = accountSummary(accountInfo(name));
|
|
37
|
+
if (!after) warn(t('login.notSaved', { name }));
|
|
38
|
+
else if (after === before) info(t('login.same', { name, account: after }));
|
|
39
|
+
else {
|
|
40
|
+
success(t('login.done', { name, account: color.bold(after) }));
|
|
41
|
+
console.log(` ${color.dim(t('login.usageLater'))}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/commands/manage.mjs
CHANGED
|
@@ -1,30 +1,22 @@
|
|
|
1
1
|
import * as prompt from '../lib/prompt.mjs';
|
|
2
2
|
import { readMeta } from '../lib/config.mjs';
|
|
3
|
-
import { error, warn } from '../lib/ui.mjs';
|
|
3
|
+
import { color, displayWidth, error, intro, sym, warn } from '../lib/ui.mjs';
|
|
4
|
+
import { accountInfo, refreshUsage } from '../lib/account.mjs';
|
|
5
|
+
import { HOME } from '../lib/constants.mjs';
|
|
6
|
+
import { LANGUAGE_NAMES, language as currentLanguage, relativeTime, t } from '../lib/i18n.mjs';
|
|
7
|
+
import { checkRename, profileDir } from '../lib/profile.mjs';
|
|
4
8
|
import { add } from './add.mjs';
|
|
5
9
|
import { list } from './list.mjs';
|
|
10
|
+
import { login } from './login.mjs';
|
|
11
|
+
import { accountSummary, profileChoices, usageLine } from './shell.mjs';
|
|
6
12
|
import { use } from './use.mjs';
|
|
7
13
|
import { remove } from './remove.mjs';
|
|
8
14
|
import { rename } from './rename.mjs';
|
|
9
15
|
import { share } from './share.mjs';
|
|
16
|
+
import { language } from './language.mjs';
|
|
10
17
|
|
|
11
18
|
function help() {
|
|
12
|
-
console.log(
|
|
13
|
-
cpf Open the profile management menu
|
|
14
|
-
cpf <name> Switch to an existing profile
|
|
15
|
-
cpf --list List profiles and the active one
|
|
16
|
-
cpf add <name> Create a profile
|
|
17
|
-
cpf rename <old> <new> Rename a profile, keeping its data
|
|
18
|
-
cpf remove <name> Delete a profile after confirmation
|
|
19
|
-
cpf share Show what every profile shares
|
|
20
|
-
cpf share add <entry> Share CLAUDE.md, skills, agents, … across profiles
|
|
21
|
-
cpf share remove <entry> Stop sharing; each profile keeps a copy
|
|
22
|
-
cpf --help Show this help
|
|
23
|
-
|
|
24
|
-
Enter these commands in your terminal, outside the Claude Code chat. Use /exit to leave the chat.
|
|
25
|
-
Run claude to choose a profile and launch Claude Code.
|
|
26
|
-
Before renaming, close sessions using that profile; macOS may require sign-in again.
|
|
27
|
-
Without shell setup, use: npx ai-profile-switch@latest manage [arguments]`);
|
|
19
|
+
console.log(t('manage.help'));
|
|
28
20
|
}
|
|
29
21
|
|
|
30
22
|
export async function manage(args = []) {
|
|
@@ -34,13 +26,14 @@ export async function manage(args = []) {
|
|
|
34
26
|
}
|
|
35
27
|
if (args.length === 1 && args[0] === '--list') return list();
|
|
36
28
|
if (args[0] === 'share') return share(args.slice(1));
|
|
29
|
+
if (args[0] === 'language') return language(args.slice(1));
|
|
37
30
|
if (args.length === 1 && !args[0].startsWith('-')) return use(args[0]);
|
|
38
31
|
if (args.length > 0) {
|
|
39
32
|
const [command, ...names] = args;
|
|
40
|
-
const operations = { add: [1, add], remove: [1, remove], rename: [2, rename] };
|
|
33
|
+
const operations = { add: [1, add], remove: [1, remove], rename: [2, rename], login: [1, login] };
|
|
41
34
|
const operation = Object.hasOwn(operations, command) ? operations[command] : undefined;
|
|
42
35
|
if (!operation || names.length !== operation[0] || names.some(name => name.startsWith('-'))) {
|
|
43
|
-
throw new Error('
|
|
36
|
+
throw new Error(t('manage.invalid'));
|
|
44
37
|
}
|
|
45
38
|
return operation[1](...names);
|
|
46
39
|
}
|
|
@@ -49,48 +42,112 @@ export async function manage(args = []) {
|
|
|
49
42
|
return;
|
|
50
43
|
}
|
|
51
44
|
|
|
45
|
+
const start = readMeta().activeProfile;
|
|
46
|
+
const email = start ? accountInfo(start).email : null;
|
|
47
|
+
intro('ai-profile-switch', start ? t('menu.current', { name: [start, email].filter(Boolean).join(' · ') }) : '');
|
|
52
48
|
while (true) {
|
|
53
49
|
const meta = readMeta();
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
{ label: 'Show shared items', value: 'share' },
|
|
61
|
-
{ label: 'Exit', value: 'exit' },
|
|
62
|
-
] : [
|
|
63
|
-
{ label: 'Add profile', value: 'add' },
|
|
64
|
-
{ label: 'Exit', value: 'exit' },
|
|
65
|
-
];
|
|
66
|
-
const action = await prompt.select(`Manage profiles (active: ${meta.activeProfile || 'none'})`, actions);
|
|
50
|
+
const item = (value, hint) => ({ label: t(`menu.${value}`), value, hint: hint && color.dim(hint) });
|
|
51
|
+
const languageItem = item('language', LANGUAGE_NAMES[currentLanguage()]);
|
|
52
|
+
const actions = meta.profiles.length
|
|
53
|
+
? [item('use'), item('manage', t('menu.manageHint')), item('add'), item('share'), languageItem, item('exit')]
|
|
54
|
+
: [item('add'), languageItem, item('exit')];
|
|
55
|
+
const action = await prompt.select(t('menu.question'), actions);
|
|
67
56
|
if (!action || action === 'exit') return;
|
|
68
57
|
try {
|
|
69
|
-
if (action === '
|
|
70
|
-
await list();
|
|
71
|
-
} else if (action === 'share') {
|
|
58
|
+
if (action === 'share') {
|
|
72
59
|
await share();
|
|
60
|
+
} else if (action === 'language') {
|
|
61
|
+
await language();
|
|
73
62
|
} else if (action === 'add') {
|
|
74
|
-
const name = await prompt.text(
|
|
63
|
+
const name = await prompt.text(t('menu.addName'));
|
|
75
64
|
if (name) await add(name);
|
|
65
|
+
} else if (action === 'use') {
|
|
66
|
+
const name = await pickProfile(t('menu.usePick'), meta);
|
|
67
|
+
if (name) await use(name);
|
|
76
68
|
} else {
|
|
77
|
-
|
|
78
|
-
label: profile === meta.activeProfile ? `${profile} (active)` : profile,
|
|
79
|
-
value: profile,
|
|
80
|
-
})), { initial: meta.profiles.indexOf(meta.activeProfile) });
|
|
81
|
-
if (!name) return;
|
|
82
|
-
if (action === 'use') await use(name);
|
|
83
|
-
else if (action === 'remove') await remove(name);
|
|
84
|
-
else {
|
|
85
|
-
warn('Close running sessions for this profile before continuing. macOS may require sign-in again.');
|
|
86
|
-
const newName = await prompt.text('New name (empty to cancel):');
|
|
87
|
-
if (newName && await prompt.confirm(`Rename "${name}" to "${newName}"?`, false)) {
|
|
88
|
-
await rename(name, newName);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
69
|
+
await manageProfiles();
|
|
91
70
|
}
|
|
92
71
|
} catch (err) {
|
|
93
|
-
error(err.message);
|
|
72
|
+
error(err.message, err.hints);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function pickProfile(question, meta) {
|
|
78
|
+
await refreshUsage(meta.profiles);
|
|
79
|
+
return prompt.select(question, profileChoices(meta.profiles, meta.activeProfile), {
|
|
80
|
+
initial: Math.max(0, meta.profiles.indexOf(meta.activeProfile)), numbered: true, back: true,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Pick a profile, then act on it from its detail screen until Back.
|
|
85
|
+
async function manageProfiles() {
|
|
86
|
+
while (true) {
|
|
87
|
+
const meta = readMeta();
|
|
88
|
+
if (!meta.profiles.length) return;
|
|
89
|
+
const name = await pickProfile(t('menu.managePick'), meta);
|
|
90
|
+
if (!name) return;
|
|
91
|
+
let current = name;
|
|
92
|
+
while (current) {
|
|
93
|
+
try {
|
|
94
|
+
current = await profileDetail(current);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
error(err.message, err.hints);
|
|
97
|
+
}
|
|
94
98
|
}
|
|
95
99
|
}
|
|
96
100
|
}
|
|
101
|
+
|
|
102
|
+
function detailLines(name) {
|
|
103
|
+
const account = accountInfo(name);
|
|
104
|
+
const rows = [
|
|
105
|
+
[t('detail.account'), accountSummary(account) || color.dim(t('account.notSignedIn'))],
|
|
106
|
+
...(account.email ? [[t('detail.usage'), account.usage ? usageLine(account) : color.dim(t('detail.noUsage'))]] : []),
|
|
107
|
+
[t('detail.lastUsed'), account.lastUsed ? relativeTime(account.lastUsed) : color.dim(t('detail.never'))],
|
|
108
|
+
[t('detail.folder'), color.dim(shortPath(profileDir(name)))],
|
|
109
|
+
];
|
|
110
|
+
const width = Math.max(...rows.map(([label]) => displayWidth(label)));
|
|
111
|
+
return [...rows.map(([label, value]) => `${color.dim(label)}${' '.repeat(width - displayWidth(label) + 2)}${value}`), ''];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function shortPath(path) {
|
|
115
|
+
return path.startsWith(HOME) ? `~${path.slice(HOME.length)}` : path;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Returns the profile to show again, or null to go back to the list.
|
|
119
|
+
async function profileDetail(name) {
|
|
120
|
+
const title = t('detail.title', { name });
|
|
121
|
+
const action = await prompt.select(color.bold(title), [
|
|
122
|
+
{ label: t('detail.use'), value: 'use' },
|
|
123
|
+
{ label: t('detail.rename'), value: 'rename' },
|
|
124
|
+
{ label: t('detail.login'), value: 'login', hint: color.dim(t('detail.loginHint')) },
|
|
125
|
+
{ label: t('detail.remove'), value: 'remove', hint: color.dim(t('menu.removeHint')) },
|
|
126
|
+
{ label: t('detail.back'), value: 'back' },
|
|
127
|
+
], { body: detailLines(name), back: true });
|
|
128
|
+
if (!action || action === 'back') return null;
|
|
129
|
+
if (action === 'use') {
|
|
130
|
+
await use(name);
|
|
131
|
+
return name;
|
|
132
|
+
}
|
|
133
|
+
if (action === 'remove') {
|
|
134
|
+
await remove(name);
|
|
135
|
+
return readMeta().profiles.includes(name) ? name : null;
|
|
136
|
+
}
|
|
137
|
+
if (action === 'rename') {
|
|
138
|
+
warn(t('rename.closeSessions', { name }));
|
|
139
|
+
const newName = await prompt.text(t('rename.newName'), undefined, t('rename.newNameHint', { name }));
|
|
140
|
+
if (!newName || newName === name) return name;
|
|
141
|
+
checkRename(name, newName);
|
|
142
|
+
if (!await prompt.confirm(t('rename.confirm', { old: name, new: newName }), false)) return name;
|
|
143
|
+
await rename(name, newName, { warned: true });
|
|
144
|
+
return newName;
|
|
145
|
+
}
|
|
146
|
+
const account = accountSummary(accountInfo(name));
|
|
147
|
+
console.log(`${color.dim(sym.rail)} ${color.dim(t('login.current'))} ${account || color.dim(t('account.notSignedIn'))}`);
|
|
148
|
+
console.log(`${color.dim(sym.rail)} ${color.dim(t('login.onlyThis'))}`);
|
|
149
|
+
console.log(`${color.dim(sym.rail)} ${color.dim(t('login.browser'))}`);
|
|
150
|
+
console.log(`${color.dim(sym.rail)} ${color.dim(t('login.closeSessions', { name }))}`);
|
|
151
|
+
if (await prompt.confirm(t('login.confirm', { name }), false)) await login(name, { explained: true });
|
|
152
|
+
return name;
|
|
153
|
+
}
|
|
@@ -3,14 +3,15 @@ import { PROFILES_DIR, SHARED_DIR } from '../../lib/constants.mjs';
|
|
|
3
3
|
import { readMeta } from '../../lib/config.mjs';
|
|
4
4
|
import { readJsonFile } from '../../lib/json-fs.mjs';
|
|
5
5
|
import { info, error } from '../../lib/ui.mjs';
|
|
6
|
+
import { t } from '../../lib/i18n.mjs';
|
|
6
7
|
|
|
7
8
|
export async function mcp(args = []) {
|
|
8
9
|
if (args.length > 1 || (args[0] && args[0] !== 'list')) {
|
|
9
|
-
error('
|
|
10
|
+
error(t('mcp.noWrites'));
|
|
10
11
|
process.exitCode = 1;
|
|
11
12
|
return;
|
|
12
13
|
}
|
|
13
|
-
info('
|
|
14
|
+
info(t('mcp.preserved'));
|
|
14
15
|
const files = [join(SHARED_DIR, 'settings.json'),
|
|
15
16
|
...readMeta().profiles.map(name => join(PROFILES_DIR, name, 'settings.local.json'))];
|
|
16
17
|
for (const file of files) {
|
|
@@ -18,10 +19,10 @@ export async function mcp(args = []) {
|
|
|
18
19
|
const names = Object.keys(data.mcpServers ?? {});
|
|
19
20
|
const disabled = Array.isArray(data.disabledMcpServers) ? data.disabledMcpServers : [];
|
|
20
21
|
if (names.length || disabled.length) {
|
|
21
|
-
console.log(`${file}: ${names.join(', ') || '
|
|
22
|
-
if (disabled.length) console.log(`
|
|
22
|
+
console.log(`${file}: ${names.join(', ') || t('mcp.noServers')}`);
|
|
23
|
+
if (disabled.length) console.log(` ${t('mcp.disabled', { list: disabled.join(', ') })}`);
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
|
-
info('
|
|
26
|
-
info('
|
|
26
|
+
info(t('mcp.where'));
|
|
27
|
+
info(t('mcp.see'));
|
|
27
28
|
}
|
package/src/commands/migrate.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { color, success, warn } from '../lib/ui.mjs';
|
|
|
4
4
|
import * as prompt from '../lib/prompt.mjs';
|
|
5
5
|
import { readMeta } from '../lib/config.mjs';
|
|
6
6
|
import { migrateDir, profileExists, validateProfileName } from '../lib/profile.mjs';
|
|
7
|
+
import { t } from '../lib/i18n.mjs';
|
|
7
8
|
import { existingClaudeSources } from './init.mjs';
|
|
8
9
|
|
|
9
10
|
export async function migrate(profileName, argv = []) {
|
|
@@ -14,43 +15,43 @@ export async function migrate(profileName, argv = []) {
|
|
|
14
15
|
});
|
|
15
16
|
const meta = readMeta();
|
|
16
17
|
if (meta.profiles.length === 0) {
|
|
17
|
-
throw new Error('
|
|
18
|
+
throw new Error(t('migrate.noProfiles'));
|
|
18
19
|
}
|
|
19
20
|
if (positionals.length > 1) throw new Error('Usage: ai-profile-switch migrate [profile] --from <path>');
|
|
20
21
|
let target = positionals[0] || (profileName && !profileName.startsWith('-') ? profileName : undefined);
|
|
21
|
-
if (!target) target = await prompt.select('
|
|
22
|
+
if (!target) target = await prompt.select(t('migrate.target'), meta.profiles);
|
|
22
23
|
if (!target) return;
|
|
23
24
|
const error = validateProfileName(target);
|
|
24
25
|
if (error) throw new Error(error);
|
|
25
26
|
if (!profileExists(target) || !meta.profiles.includes(target)) {
|
|
26
|
-
throw new Error(
|
|
27
|
+
throw new Error(t('profile.notFound', { name: target }));
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
let source = values.from;
|
|
30
|
-
if (source !== undefined && !source.trim()) throw new Error('
|
|
31
|
+
if (source !== undefined && !source.trim()) throw new Error(t('migrate.emptySource'));
|
|
31
32
|
if (!source) {
|
|
32
33
|
const choices = existingClaudeSources().map(path => ({ label: path, value: path }));
|
|
33
|
-
choices.push({ label: '
|
|
34
|
-
source = await prompt.select('
|
|
34
|
+
choices.push({ label: t('migrate.customPath'), value: '__custom__' });
|
|
35
|
+
source = await prompt.select(t('migrate.which'), choices);
|
|
35
36
|
if (!source) return;
|
|
36
|
-
if (source === '__custom__') source = await prompt.text('
|
|
37
|
-
if (!source.trim()) throw new Error('
|
|
37
|
+
if (source === '__custom__') source = await prompt.text(t('migrate.enterPath'), '');
|
|
38
|
+
if (!source.trim()) throw new Error(t('migrate.emptySource'));
|
|
38
39
|
}
|
|
39
40
|
source = resolve(source);
|
|
40
41
|
console.log();
|
|
41
|
-
console.log(`
|
|
42
|
-
console.log(`
|
|
43
|
-
console.log('
|
|
44
|
-
warn('
|
|
45
|
-
const confirmed = await prompt.confirm(
|
|
42
|
+
console.log(` ${color.dim(t('migrate.source'))} ${color.cyan(source)}`);
|
|
43
|
+
console.log(` ${color.dim(t('migrate.profile'))} ${color.cyan(target)}`);
|
|
44
|
+
console.log(` ${t('migrate.conflictNote')}`);
|
|
45
|
+
warn(t('migrate.closeSessions'));
|
|
46
|
+
const confirmed = await prompt.confirm(t('migrate.confirm', { name: target }), true);
|
|
46
47
|
if (!confirmed) {
|
|
47
|
-
console.log('
|
|
48
|
+
console.log(` ${t('common.cancelled')}`);
|
|
48
49
|
return;
|
|
49
50
|
}
|
|
50
51
|
const { backupPath } = migrateDir(source, target, meta.shareSettings !== false);
|
|
51
|
-
success(
|
|
52
|
-
if (backupPath) warn(
|
|
53
|
-
warn('
|
|
54
|
-
console.log('
|
|
52
|
+
success(t('migrate.copied', { source, name: target }));
|
|
53
|
+
if (backupPath) warn(t('migrate.backup', { path: backupPath }));
|
|
54
|
+
warn(t('migrate.preserved'));
|
|
55
|
+
console.log(` ${t('migrate.signIn')}`);
|
|
55
56
|
console.log();
|
|
56
57
|
}
|
package/src/commands/remove.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { success, warn } from '../lib/ui.mjs';
|
|
|
2
2
|
import { confirm } from '../lib/prompt.mjs';
|
|
3
3
|
import { assertRegisteredProfile, removeProfile } from '../lib/profile.mjs';
|
|
4
4
|
import { getActiveProfile } from '../lib/config.mjs';
|
|
5
|
+
import { t } from '../lib/i18n.mjs';
|
|
5
6
|
|
|
6
7
|
export async function remove(name) {
|
|
7
8
|
if (!name) throw new Error('Usage: cpf remove <name>');
|
|
@@ -10,24 +11,24 @@ export async function remove(name) {
|
|
|
10
11
|
|
|
11
12
|
const active = getActiveProfile();
|
|
12
13
|
if (name === active) {
|
|
13
|
-
warn(
|
|
14
|
+
warn(t('remove.activeWarn', { name }));
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
const yes = await confirm(
|
|
17
|
+
const yes = await confirm(t('remove.confirm', { name }), false);
|
|
17
18
|
if (!yes) {
|
|
18
|
-
console.log('
|
|
19
|
+
console.log(` ${t('common.cancelled')}`);
|
|
19
20
|
return;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
removeProfile(name);
|
|
23
|
-
success(
|
|
24
|
+
success(t('remove.done', { name }));
|
|
24
25
|
|
|
25
26
|
if (name === active) {
|
|
26
27
|
const newActive = getActiveProfile();
|
|
27
28
|
if (newActive) {
|
|
28
|
-
success(
|
|
29
|
+
success(t('remove.newActive', { name: newActive }));
|
|
29
30
|
} else {
|
|
30
|
-
warn('
|
|
31
|
+
warn(`${t('remove.none')} ${t('hint.add')} cpf add work · ${t('hint.setup')} npx ai-profile-switch@latest init`);
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
}
|