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.
@@ -1,11 +1,12 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { join, resolve } from 'node:path';
3
- import { color, box, success, warn } from '../lib/ui.mjs';
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('ai-profile-switch is already initialized.');
27
- console.log(` Profiles: ${current.profiles.join(', ')}`);
28
- console.log(` Active: ${current.activeProfile || 'none'}`);
29
- console.log(` Add an account: ${color.cyan('cpf add <name>')}`);
30
- console.log(` Repair shell integration: ${color.cyan('npx ai-profile-switch@latest install-shell')}`);
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
- console.log();
35
- console.log(box([
36
- `Welcome to ${color.bold('Claude Switch!')}`,
37
- 'Multi-account manager for Claude Code',
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('Which profile should be active by default?', names);
42
+ : await prompt.select(t('init.activeQuestion'), names);
53
43
  if (!activeProfile) return;
54
- const shareSettings = await prompt.confirm('Share settings.json and commands across profiles?', true);
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('Copy an existing Claude configuration?', [
48
+ const source = await prompt.select(t('init.copyQuestion'), [
59
49
  ...sources.map(path => ({ label: path, value: path })),
60
- { label: 'Skip', value: '__skip__' },
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('Copy into which profile?', names);
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(`Profile directory "${name}" already exists. Choose another name.`);
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(`Copied ${migration.source} → profile: ${migration.target}`);
75
- if (backupPath) warn(`Previous destination data backup: ${backupPath}`);
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(`Profile ready: ${name}`);
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(`Shell integration installed (${shells.join(', ')})`);
85
- success(`Active profile: ${activeProfile}`);
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(` Open a new terminal and run ${color.cyan('claude')}. Claude will check authentication.`);
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(` To use another profile: ${color.cyan(command)}`);
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
- async function askProfileName(message, defaultName, names) {
88
+ // One prompt takes every name, such as "work, personal".
89
+ async function askProfileNames() {
100
90
  while (true) {
101
- const name = await prompt.text(message, defaultName);
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 || names.includes(name) || profileExists(name)) {
104
- console.log(` ${color.red(error || `"${name}" is already used — choose a different name`)}`);
105
- continue;
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('Local shell runtime installed.');
11
- if (shells.length) success(`Shell integration ready (${shells.join(', ')})`);
12
- info('Open a new terminal. Run claude to launch or cpf to manage profiles.');
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
+ }
@@ -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('No profiles configured. Run cpf to add one, or npx ai-profile-switch@latest init for first setup.');
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('Profiles:')}`);
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 dir = color.dim(profileDir(name));
23
- console.log(` ${marker}${label} ${dir}`);
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(` Active: ${color.green(active || 'none')}`);
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
+ }
@@ -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(`Usage:
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('Invalid arguments. Run cpf --help for usage.');
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 actions = meta.profiles.length ? [
55
- { label: 'Switch profile', value: 'use' },
56
- { label: 'List profiles', value: 'list' },
57
- { label: 'Add profile', value: 'add' },
58
- { label: 'Rename profile', value: 'rename' },
59
- { label: 'Delete profile', value: 'remove' },
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 === 'list') {
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('New profile name (empty to cancel):');
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
- const name = await prompt.select('Choose a profile:', meta.profiles.map(profile => ({
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('MCP writes are no longer supported here. Use claude mcp add/remove and /mcp.');
10
+ error(t('mcp.noWrites'));
10
11
  process.exitCode = 1;
11
12
  return;
12
13
  }
13
- info('Legacy MCP data is preserved. Register servers using claude mcp for the selected profile.');
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(', ') || '(no servers)'}`);
22
- if (disabled.length) console.log(` Legacy disabled: ${disabled.join(', ')}`);
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('User servers belong in the profile .claude.json; project servers belong in .mcp.json.');
26
- info('See https://code.claude.com/docs/en/mcp');
26
+ info(t('mcp.where'));
27
+ info(t('mcp.see'));
27
28
  }
@@ -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('No profiles found. Run npx ai-profile-switch@latest init first.');
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('Copy into which profile?', meta.profiles);
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(`Profile "${target}" does not exist.`);
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('Source path cannot be empty.');
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: 'Enter a custom path', value: '__custom__' });
34
- source = await prompt.select('Which configuration do you want to copy?', choices);
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('Enter the full path to the directory:', '');
37
- if (!source.trim()) throw new Error('Source path cannot be empty.');
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(` Source : ${color.cyan(source)}`);
42
- console.log(` Profile: ${color.cyan(target)}`);
43
- console.log(' Existing conflicting data will stop migration. The source will be preserved.');
44
- warn('Close Claude sessions using the source or destination before continuing. Keep them closed until migration finishes.');
45
- const confirmed = await prompt.confirm(`Copy configuration into "${target}"?`, true);
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(' Cancelled.');
48
+ console.log(` ${t('common.cancelled')}`);
48
49
  return;
49
50
  }
50
51
  const { backupPath } = migrateDir(source, target, meta.shareSettings !== false);
51
- success(`Copied ${source} → profile: ${target}`);
52
- if (backupPath) warn(`Previous destination data backup: ${backupPath}`);
53
- warn('Original configuration was preserved. macOS Keychain credentials are not copied.');
54
- console.log(' Claude will check authentication when you launch this profile.');
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
  }
@@ -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(`"${name}" is the currently active profile`);
14
+ warn(t('remove.activeWarn', { name }));
14
15
  }
15
16
 
16
- const yes = await confirm(`Delete profile "${name}"? This cannot be undone`, false);
17
+ const yes = await confirm(t('remove.confirm', { name }), false);
17
18
  if (!yes) {
18
- console.log(' Cancelled.');
19
+ console.log(` ${t('common.cancelled')}`);
19
20
  return;
20
21
  }
21
22
 
22
23
  removeProfile(name);
23
- success(`Removed profile: ${name}`);
24
+ success(t('remove.done', { name }));
24
25
 
25
26
  if (name === active) {
26
27
  const newActive = getActiveProfile();
27
28
  if (newActive) {
28
- success(`Active profile switched to: ${newActive}`);
29
+ success(t('remove.newActive', { name: newActive }));
29
30
  } else {
30
- warn('No profiles remaining. Run cpf to add a profile, or npx ai-profile-switch@latest init to set up again.');
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
  }