ai-profile-switch 2.2.0 → 2.2.1
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 +51 -121
- package/README.md +47 -117
- package/package.json +1 -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 +107 -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 +76 -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 +47 -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 +30 -19
- package/src/lib/prompt.mjs +77 -38
- package/src/lib/shared.mjs +5 -4
- package/src/lib/ui.mjs +54 -29
- package/src/locales/en.mjs +265 -0
- package/src/locales/ko.mjs +265 -0
package/src/commands/rename.mjs
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
import { info, success, warn } from '../lib/ui.mjs';
|
|
2
|
-
import { renameProfile } from '../lib/profile.mjs';
|
|
1
|
+
import { color, info, success, warn } from '../lib/ui.mjs';
|
|
2
|
+
import { checkRename, renameProfile } from '../lib/profile.mjs';
|
|
3
|
+
import { t } from '../lib/i18n.mjs';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
// warned: the caller already asked to close sessions, so do not say it twice.
|
|
6
|
+
export async function rename(oldName, newName, { warned = false } = {}) {
|
|
5
7
|
if (!oldName || !newName) throw new Error('Usage: cpf rename <old-name> <new-name>');
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
8
|
+
checkRename(oldName, newName);
|
|
9
|
+
if (oldName === newName) {
|
|
10
|
+
info(t('rename.same', { name: oldName }));
|
|
11
|
+
return;
|
|
11
12
|
}
|
|
12
|
-
if (
|
|
13
|
-
|
|
13
|
+
if (!warned) warn(t('rename.closeSessions', { name: oldName }));
|
|
14
|
+
renameProfile(oldName, newName);
|
|
15
|
+
success(t('rename.done', { old: oldName, new: color.bold(newName) }));
|
|
16
|
+
const kept = process.platform === 'darwin' ? `${t('rename.kept')} ${t('rename.macSignIn')}` : t('rename.kept');
|
|
17
|
+
console.log(` ${color.dim(kept)}`);
|
|
18
|
+
console.log(` ${t('rename.next', { command: color.cyan('claude'), name: color.bold(newName) })}`);
|
|
14
19
|
}
|
package/src/commands/share.mjs
CHANGED
|
@@ -2,13 +2,14 @@ import { color, info, success, warn } from '../lib/ui.mjs';
|
|
|
2
2
|
import { SHAREABLE_ENTRIES, SHARED_FILES, SHARED_DIRS } from '../lib/constants.mjs';
|
|
3
3
|
import { readMeta } from '../lib/config.mjs';
|
|
4
4
|
import { shareEntry, syncShared, unshareEntry } from '../lib/shared.mjs';
|
|
5
|
+
import { t } from '../lib/i18n.mjs';
|
|
5
6
|
|
|
6
7
|
export function reportConflicts(results) {
|
|
7
8
|
for (const { name, conflicts, missing } of results) {
|
|
8
9
|
for (const entry of conflicts) {
|
|
9
|
-
warn(
|
|
10
|
+
warn(t('share.keptOwn', { name, entry }));
|
|
10
11
|
}
|
|
11
|
-
for (const entry of missing) warn(
|
|
12
|
+
for (const entry of missing) warn(t('share.missing', { name, entry }));
|
|
12
13
|
}
|
|
13
14
|
}
|
|
14
15
|
|
|
@@ -16,43 +17,44 @@ function status() {
|
|
|
16
17
|
const meta = readMeta();
|
|
17
18
|
const shared = meta.shared ?? [];
|
|
18
19
|
console.log();
|
|
19
|
-
console.log(` ${color.bold('
|
|
20
|
-
|
|
20
|
+
console.log(` ${color.bold(t('share.title'))} ${[...SHARED_FILES, ...SHARED_DIRS, ...shared].join(', ')}`);
|
|
21
|
+
const more = SHAREABLE_ENTRIES.filter(entry => !shared.includes(entry)).join(', ') || t('share.nothingLeft');
|
|
22
|
+
console.log(` ${color.dim(t('share.canAlso', { list: more }))}`);
|
|
21
23
|
console.log();
|
|
22
24
|
for (const result of syncShared(meta.profiles, { dryRun: true })) {
|
|
23
25
|
const problems = [
|
|
24
|
-
...result.conflicts.map(entry => color.yellow(
|
|
25
|
-
...result.linked.map(entry =>
|
|
26
|
-
...result.missing.map(entry => color.red(
|
|
26
|
+
...result.conflicts.map(entry => color.yellow(t('share.own', { entry }))),
|
|
27
|
+
...result.linked.map(entry => t('share.notLinked', { entry })),
|
|
28
|
+
...result.missing.map(entry => color.red(t('share.missingShort', { entry }))),
|
|
27
29
|
];
|
|
28
|
-
console.log(` ${result.name.padEnd(12)} ${problems.length ? problems.join(', ') : color.green('
|
|
30
|
+
console.log(` ${result.name.padEnd(12)} ${problems.length ? problems.join(', ') : color.green(t('share.inSync'))}`);
|
|
29
31
|
}
|
|
30
|
-
if (!shared.length) info('
|
|
32
|
+
if (!shared.length) info(t('share.nothingExtra'));
|
|
31
33
|
console.log();
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
export async function share(args = []) {
|
|
35
37
|
const [command, entry, ...rest] = args;
|
|
36
38
|
if (rest.length || (['add', 'remove'].includes(command) ? !entry : entry !== undefined)) {
|
|
37
|
-
throw new Error('
|
|
39
|
+
throw new Error(t('share.usage'));
|
|
38
40
|
}
|
|
39
41
|
if (!command || command === 'list') return status();
|
|
40
42
|
if (command === 'sync') {
|
|
41
43
|
const results = syncShared();
|
|
42
44
|
reportConflicts(results);
|
|
43
|
-
success(
|
|
45
|
+
success(t('share.linked', { count: results.reduce((count, result) => count + result.linked.length, 0) }));
|
|
44
46
|
return;
|
|
45
47
|
}
|
|
46
48
|
if (command === 'add') {
|
|
47
49
|
const results = shareEntry(entry);
|
|
48
50
|
reportConflicts(results);
|
|
49
|
-
success(
|
|
51
|
+
success(t('share.added', { entry }));
|
|
50
52
|
return;
|
|
51
53
|
}
|
|
52
54
|
if (command === 'remove') {
|
|
53
55
|
unshareEntry(entry);
|
|
54
|
-
success(
|
|
56
|
+
success(t('share.removed', { entry }));
|
|
55
57
|
return;
|
|
56
58
|
}
|
|
57
|
-
throw new Error('
|
|
59
|
+
throw new Error(t('share.usage'));
|
|
58
60
|
}
|
package/src/commands/shell.mjs
CHANGED
|
@@ -6,7 +6,9 @@ import { getActiveProfile, setActiveProfile } from '../lib/config.mjs';
|
|
|
6
6
|
import { HOME, IS_WINDOWS } from '../lib/constants.mjs';
|
|
7
7
|
import { installAllShells } from '../lib/shell.mjs';
|
|
8
8
|
import { listProfiles, profileDir, assertRegisteredProfile } from '../lib/profile.mjs';
|
|
9
|
-
import { success, warn } from '../lib/ui.mjs';
|
|
9
|
+
import { color, colorFor, intro, meter, outro, success, sym, userError, warn } from '../lib/ui.mjs';
|
|
10
|
+
import { accountInfo } from '../lib/account.mjs';
|
|
11
|
+
import { relativeTime, t } from '../lib/i18n.mjs';
|
|
10
12
|
import { syncShared } from '../lib/shared.mjs';
|
|
11
13
|
import { reportConflicts } from './share.mjs';
|
|
12
14
|
import * as prompt from '../lib/prompt.mjs';
|
|
@@ -37,7 +39,7 @@ export async function shell(argv = []) {
|
|
|
37
39
|
case 'refresh':
|
|
38
40
|
if (args.length) throw new Error('Usage: shell refresh');
|
|
39
41
|
installAllShells();
|
|
40
|
-
success('
|
|
42
|
+
success(t('shell.refreshed'));
|
|
41
43
|
return;
|
|
42
44
|
default:
|
|
43
45
|
throw new Error('Usage: shell <launch|use|pick|refresh>');
|
|
@@ -45,7 +47,7 @@ export async function shell(argv = []) {
|
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
async function launch(args, selected) {
|
|
48
|
-
const profile = selected ?? await selectProfile();
|
|
50
|
+
const profile = selected ?? await selectProfile({ launching: true });
|
|
49
51
|
if (!profile) {
|
|
50
52
|
process.exitCode = 130;
|
|
51
53
|
return;
|
|
@@ -53,15 +55,16 @@ async function launch(args, selected) {
|
|
|
53
55
|
assertRegisteredProfile(profile);
|
|
54
56
|
const bin = resolveClaudeBinary();
|
|
55
57
|
if (!bin) {
|
|
56
|
-
console.error('
|
|
58
|
+
console.error(`${t('launch.notFound')} ${t('launch.install')} https://code.claude.com/docs/en/setup`);
|
|
57
59
|
process.exitCode = 127;
|
|
58
60
|
return;
|
|
59
61
|
}
|
|
60
62
|
scheduleSelfUpdate();
|
|
61
63
|
if (getActiveProfile() !== profile) setActiveProfile(profile);
|
|
62
64
|
// Sharing is a convenience; a broken link must not stop Claude from starting.
|
|
63
|
-
try { reportConflicts(syncShared([profile])); } catch (err) { warn(
|
|
64
|
-
|
|
65
|
+
try { reportConflicts(syncShared([profile])); } catch (err) { warn(t('launch.sharedFailed', { message: err.message })); }
|
|
66
|
+
// The picker already told a person which account starts; logs still get a plain line.
|
|
67
|
+
if (!process.stderr.isTTY) process.stderr.write(`[ai-profile-switch] Profile: ${profile}\n`);
|
|
65
68
|
const cmd = bin.endsWith('.js') ? process.execPath : bin;
|
|
66
69
|
const childArgs = bin.endsWith('.js') ? [bin, ...args] : args;
|
|
67
70
|
const result = await new Promise(resolve => {
|
|
@@ -80,7 +83,7 @@ async function launch(args, selected) {
|
|
|
80
83
|
};
|
|
81
84
|
child.once('error', err => {
|
|
82
85
|
cleanup();
|
|
83
|
-
console.error(
|
|
86
|
+
console.error(t('launch.startFailed', { message: err.message }));
|
|
84
87
|
resolve(1);
|
|
85
88
|
});
|
|
86
89
|
child.once('close', (code, signal) => {
|
|
@@ -94,31 +97,89 @@ async function launch(args, selected) {
|
|
|
94
97
|
function useProfile(name) {
|
|
95
98
|
assertRegisteredProfile(name);
|
|
96
99
|
if (getActiveProfile() !== name) setActiveProfile(name);
|
|
97
|
-
success(
|
|
100
|
+
success(t('use.switched', { profile: name }));
|
|
98
101
|
}
|
|
99
102
|
|
|
103
|
+
// The claude shell function runs `pick --print` and then launches the printed profile.
|
|
100
104
|
async function pickProfile(printOnly) {
|
|
101
|
-
const profile = await selectProfile();
|
|
105
|
+
const profile = await selectProfile({ launching: printOnly });
|
|
102
106
|
if (!profile) {
|
|
103
107
|
process.exitCode = 130;
|
|
104
108
|
return;
|
|
105
109
|
}
|
|
106
110
|
if (getActiveProfile() !== profile) setActiveProfile(profile);
|
|
107
111
|
if (printOnly) process.stdout.write(profile + '\n');
|
|
108
|
-
else success(`Switched to profile: ${profile}`);
|
|
109
112
|
}
|
|
110
113
|
|
|
111
|
-
|
|
114
|
+
export function accountSummary({ email, plan }) {
|
|
115
|
+
return [email, plan].filter(Boolean).join(' · ');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function usageLine({ usage }) {
|
|
119
|
+
if (!usage) return color.dim(t('account.noUsage'));
|
|
120
|
+
const parts = [];
|
|
121
|
+
if (usage.fiveHour !== null) parts.push(`${t('account.fiveHour')} ${meter(usage.fiveHour)}`);
|
|
122
|
+
if (usage.weekly !== null) parts.push(`${t('account.weekly')} ${meter(usage.weekly)}`);
|
|
123
|
+
if (usage.savedAt) parts.push(color.dim(t('account.savedAt', { time: relativeTime(usage.savedAt) })));
|
|
124
|
+
return parts.join(' ');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// One picker row per profile: name, in use or last used, then account and usage lines.
|
|
128
|
+
export function profileChoices(profiles, active) {
|
|
129
|
+
const width = Math.max(...profiles.map(name => name.length));
|
|
130
|
+
return profiles.map(name => profileChoice(name, active, width));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function profileChoice(name, active, width) {
|
|
134
|
+
const account = accountInfo(name);
|
|
135
|
+
const hint = name === active
|
|
136
|
+
? color.green(`${sym.bulletOn} ${t('picker.inUse')}`)
|
|
137
|
+
: account.lastUsed ? color.dim(relativeTime(account.lastUsed)) : '';
|
|
138
|
+
const summary = accountSummary(account);
|
|
139
|
+
const details = summary
|
|
140
|
+
? [color.dim(summary), usageLine(account)]
|
|
141
|
+
: [color.dim(t('account.notSignedIn'))];
|
|
142
|
+
return { label: name.padEnd(width), value: name, hint, details, summary };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function selectProfile({ launching = false } = {}) {
|
|
112
146
|
const profiles = listProfiles();
|
|
113
|
-
if (!profiles.length)
|
|
147
|
+
if (!profiles.length) {
|
|
148
|
+
throw userError(t('profiles.none'), [
|
|
149
|
+
{ label: t('hint.add'), command: 'cpf add work' },
|
|
150
|
+
{ label: t('hint.setup'), command: 'npx ai-profile-switch@latest init' },
|
|
151
|
+
]);
|
|
152
|
+
}
|
|
114
153
|
const active = getActiveProfile();
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
154
|
+
const output = process.stderr;
|
|
155
|
+
colorFor(output);
|
|
156
|
+
if (!process.stdin.isTTY || profiles.length === 1) {
|
|
157
|
+
const selected = active ?? profiles[0];
|
|
158
|
+
assertRegisteredProfile(selected);
|
|
159
|
+
if (launching && output.isTTY) {
|
|
160
|
+
const summary = accountSummary(accountInfo(selected));
|
|
161
|
+
output.write(`${color.green(sym.stepDone)} ${t('picker.starting', { profile: color.bold(selected) })}${summary ? ` ${color.dim(summary)}` : ''}\n`);
|
|
162
|
+
}
|
|
163
|
+
return selected;
|
|
164
|
+
}
|
|
165
|
+
const choices = profileChoices(profiles, active);
|
|
166
|
+
intro('ai-profile-switch', '', output);
|
|
167
|
+
const selected = await prompt.select(t('picker.question'), choices, {
|
|
168
|
+
output, initial: profiles.indexOf(active), numbered: true,
|
|
169
|
+
done: name => {
|
|
170
|
+
const { summary } = choices.find(choice => choice.value === name);
|
|
171
|
+
const message = t(launching ? 'picker.starting' : 'picker.switched', { profile: color.bold(name) });
|
|
172
|
+
return `${message}${summary ? ` ${color.dim(summary)}` : ''}`;
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
outro(selected && launching ? t('launch.opening') : '', output);
|
|
118
176
|
if (selected) assertRegisteredProfile(selected);
|
|
119
177
|
return selected;
|
|
120
178
|
}
|
|
121
179
|
|
|
180
|
+
// Declared below; the Windows resolver test runs that part on its own.
|
|
181
|
+
export { resolveClaudeBinary };
|
|
182
|
+
|
|
122
183
|
function executable(file) {
|
|
123
184
|
try {
|
|
124
185
|
accessSync(file, IS_WINDOWS ? constants.F_OK : constants.X_OK);
|
package/src/commands/update.mjs
CHANGED
|
@@ -3,7 +3,8 @@ import { computeUpdatePlan, compareSemver } from '../lib/updater.mjs';
|
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { installedRuntimeDirectory, runSelfUpdate } from '../lib/self-update.mjs';
|
|
6
|
-
import { info, warn, error } from '../lib/ui.mjs';
|
|
6
|
+
import { displayWidth, info, warn, error } from '../lib/ui.mjs';
|
|
7
|
+
import { t } from '../lib/i18n.mjs';
|
|
7
8
|
|
|
8
9
|
export async function update(args = []) {
|
|
9
10
|
const { values, positionals } = parseArgs({ args, allowPositionals: true, options: {
|
|
@@ -14,10 +15,10 @@ export async function update(args = []) {
|
|
|
14
15
|
} });
|
|
15
16
|
if (positionals.length) throw new Error('Usage: update [--check] [--self]');
|
|
16
17
|
if (values['claude-code']) {
|
|
17
|
-
info('
|
|
18
|
-
info('
|
|
18
|
+
info(t('update.claudeManaged'));
|
|
19
|
+
info(t('update.seeSetup'));
|
|
19
20
|
if (values.check) {
|
|
20
|
-
warn('
|
|
21
|
+
warn(t('update.notChecked'));
|
|
21
22
|
process.exitCode = 2;
|
|
22
23
|
}
|
|
23
24
|
if (!values.self) return;
|
|
@@ -35,16 +36,18 @@ export async function update(args = []) {
|
|
|
35
36
|
process.exitCode = 2;
|
|
36
37
|
return;
|
|
37
38
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
const table = [[t('update.package'), t('update.installed'), t('update.latest')], [self.pkg, self.installed, self.latest]];
|
|
40
|
+
const widths = [0, 1].map(column => Math.max(...table.map(row => displayWidth(row[column]))) + 3);
|
|
41
|
+
for (const row of table) console.log(row.map((cell, column) => cell + ' '.repeat(Math.max(0, (widths[column] ?? 0) - displayWidth(cell)))).join('').trimEnd());
|
|
42
|
+
if (!self.hasUpdate) info(t('update.upToDate'));
|
|
43
|
+
else if (values.check) info(t('update.runToInstall'));
|
|
42
44
|
else {
|
|
43
45
|
const result = await runSelfUpdate({ force: true, computePlan: async () => ({ self }) });
|
|
44
|
-
if (result.action === 'updated') info(
|
|
45
|
-
else if (result.reason === 'not-installed') info('
|
|
46
|
-
else if (result.reason === 'busy') { warn('
|
|
47
|
-
else if (result.action === 'failed') { error(
|
|
46
|
+
if (result.action === 'updated') info(t('update.updated', { version: result.version }));
|
|
47
|
+
else if (result.reason === 'not-installed') info(t('update.notInstalled'));
|
|
48
|
+
else if (result.reason === 'busy') { warn(t('update.busy')); process.exitCode = 2; }
|
|
49
|
+
else if (result.action === 'failed') { error(t('update.failed', { error: result.error })); process.exitCode = 2; }
|
|
48
50
|
}
|
|
49
51
|
if (values.check && !values['claude-code']) process.exitCode = self.hasUpdate ? 1 : 0;
|
|
50
52
|
}
|
|
53
|
+
|
package/src/commands/use.mjs
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import { success, info } from '../lib/ui.mjs';
|
|
1
|
+
import { color, success, info } from '../lib/ui.mjs';
|
|
2
2
|
import { assertRegisteredProfile } from '../lib/profile.mjs';
|
|
3
3
|
import { setActiveProfile, getActiveProfile } from '../lib/config.mjs';
|
|
4
|
+
import { t } from '../lib/i18n.mjs';
|
|
4
5
|
|
|
5
6
|
export async function use(name) {
|
|
6
7
|
assertRegisteredProfile(name);
|
|
7
8
|
const current = getActiveProfile();
|
|
8
9
|
if (current === name) {
|
|
9
|
-
info(
|
|
10
|
+
info(t('use.already', { profile: name }));
|
|
10
11
|
return;
|
|
11
12
|
}
|
|
12
13
|
setActiveProfile(name);
|
|
13
|
-
success(
|
|
14
|
+
success(t('use.switched', { profile: name }));
|
|
15
|
+
console.log(` ${color.dim(t('use.note'))}`);
|
|
14
16
|
}
|
package/src/index.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync } from 'node:fs';
|
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { error, color } from './lib/ui.mjs';
|
|
5
|
+
import { t } from './lib/i18n.mjs';
|
|
5
6
|
|
|
6
7
|
const pkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'));
|
|
7
8
|
|
|
@@ -10,12 +11,14 @@ const COMMANDS = {
|
|
|
10
11
|
add: args => import('./commands/add.mjs').then(m => m.add(args[0])),
|
|
11
12
|
remove: args => import('./commands/remove.mjs').then(m => m.remove(args[0])),
|
|
12
13
|
rename: args => import('./commands/rename.mjs').then(m => m.rename(...args)),
|
|
14
|
+
login: args => import('./commands/login.mjs').then(m => m.login(args[0])),
|
|
13
15
|
list: () => import('./commands/list.mjs').then(m => m.list()),
|
|
14
16
|
use: args => import('./commands/use.mjs').then(m => m.use(args[0])),
|
|
15
17
|
manage: args => import('./commands/manage.mjs').then(m => m.manage(args)),
|
|
16
18
|
'install-shell': () => import('./commands/install-shell.mjs').then(m => m.installShell()),
|
|
17
19
|
migrate: args => import('./commands/migrate.mjs').then(m => m.migrate(undefined, args)),
|
|
18
20
|
share: args => import('./commands/share.mjs').then(m => m.share(args)),
|
|
21
|
+
language: args => import('./commands/language.mjs').then(m => m.language(args)),
|
|
19
22
|
mcp: args => import('./commands/mcp/index.mjs').then(m => m.mcp(args)),
|
|
20
23
|
update: args => import('./commands/update.mjs').then(m => m.update(args)),
|
|
21
24
|
shell: args => import('./commands/shell.mjs').then(m => m.shell(args)),
|
|
@@ -26,54 +29,29 @@ const USAGE = {
|
|
|
26
29
|
add: 'add <name>',
|
|
27
30
|
remove: 'remove <name>',
|
|
28
31
|
rename: 'rename <old-name> <new-name>',
|
|
32
|
+
login: 'login <name>',
|
|
29
33
|
list: 'list',
|
|
30
34
|
use: 'use <name>',
|
|
31
35
|
manage: 'manage [profile | add <name> | rename <old-name> <new-name> | remove <name> | --list]',
|
|
32
36
|
'install-shell': 'install-shell',
|
|
33
37
|
migrate: 'migrate [profile] [--from <path>]',
|
|
34
38
|
share: 'share [list | add <entry> | remove <entry> | sync]',
|
|
39
|
+
language: 'language [auto | en | ko]',
|
|
35
40
|
mcp: 'mcp [list]',
|
|
36
41
|
update: 'update [--check] [--self] [--claude-code] [--yes]',
|
|
37
42
|
shell: 'shell <launch|use|pick|refresh>',
|
|
38
43
|
};
|
|
39
|
-
const POSITIONAL_COUNTS = { init: 0, add: 1, remove: 1, rename: 2, list: 0, use: 1, 'install-shell': 0 };
|
|
44
|
+
const POSITIONAL_COUNTS = { init: 0, add: 1, remove: 1, rename: 2, login: 1, list: 0, use: 1, 'install-shell': 0 };
|
|
45
|
+
|
|
46
|
+
const COMMAND_NOTES = { login: 'index.helpLogin', rename: 'index.helpRename', remove: 'index.helpRemove', add: 'index.helpAdd', use: 'index.helpUse' };
|
|
40
47
|
|
|
41
48
|
function showHelp(command) {
|
|
42
49
|
if (command) {
|
|
43
|
-
console.log(
|
|
44
|
-
if (command
|
|
45
|
-
if (command === 'remove') console.log('Deletes a registered profile after confirmation. This cannot be undone.');
|
|
46
|
-
if (command === 'add') console.log('Creates a profile. Run claude, choose it, and sign in to the intended account.');
|
|
47
|
-
if (command === 'use') console.log('Changes the active profile for future launches; existing sessions keep their profile.');
|
|
50
|
+
console.log(t('index.usage', { usage: USAGE[command] }));
|
|
51
|
+
if (Object.hasOwn(COMMAND_NOTES, command)) console.log(t(COMMAND_NOTES[command]));
|
|
48
52
|
return;
|
|
49
53
|
}
|
|
50
|
-
console.log(
|
|
51
|
-
${color.bold('ai-profile-switch')} — Profile switching for Claude Code
|
|
52
|
-
|
|
53
|
-
Everyday commands (in your terminal, after shell setup):
|
|
54
|
-
claude Select a profile and launch Claude Code
|
|
55
|
-
cpf Open the profile management menu
|
|
56
|
-
cpf work Switch to an existing profile
|
|
57
|
-
cpf --help Show short management commands
|
|
58
|
-
|
|
59
|
-
Usage: npx ai-profile-switch@latest <command> [options]
|
|
60
|
-
|
|
61
|
-
Commands:
|
|
62
|
-
init First setup; show existing setup on repeated runs
|
|
63
|
-
add <name> Create a profile
|
|
64
|
-
remove <name> Remove a profile after confirmation
|
|
65
|
-
rename <old> <new> Rename a profile, keeping its data
|
|
66
|
-
list List profiles
|
|
67
|
-
use <name> Change the active profile
|
|
68
|
-
manage Open the same menu as cpf
|
|
69
|
-
install-shell Install or repair shell integration
|
|
70
|
-
migrate [name] Import existing settings (--from <path>)
|
|
71
|
-
update [--check] Update this tool; --check only checks
|
|
72
|
-
share [add|remove] Share CLAUDE.md, skills, agents, … across profiles
|
|
73
|
-
mcp [list] Show legacy MCP migration guidance
|
|
74
|
-
|
|
75
|
-
Run any command with --help for its usage.
|
|
76
|
-
`);
|
|
54
|
+
console.log(t('index.help', { title: color.bold('ai-profile-switch') }));
|
|
77
55
|
}
|
|
78
56
|
|
|
79
57
|
export async function run(argv) {
|
|
@@ -88,7 +66,7 @@ export async function run(argv) {
|
|
|
88
66
|
return;
|
|
89
67
|
}
|
|
90
68
|
if (!Object.hasOwn(COMMANDS, command)) {
|
|
91
|
-
error(
|
|
69
|
+
error(t('index.unknown', { command }));
|
|
92
70
|
showHelp();
|
|
93
71
|
process.exitCode = 1;
|
|
94
72
|
return;
|
|
@@ -104,11 +82,11 @@ export async function run(argv) {
|
|
|
104
82
|
}
|
|
105
83
|
if (Object.hasOwn(POSITIONAL_COUNTS, command)
|
|
106
84
|
&& (args.length !== POSITIONAL_COUNTS[command] || args.some(arg => arg.startsWith('-')))) {
|
|
107
|
-
throw new Error(
|
|
85
|
+
throw new Error(t('index.usage', { usage: USAGE[command] }));
|
|
108
86
|
}
|
|
109
87
|
await COMMANDS[command](args);
|
|
110
88
|
} catch (err) {
|
|
111
|
-
error(err.message);
|
|
89
|
+
error(err.message, err.hints);
|
|
112
90
|
process.exitCode = 1;
|
|
113
91
|
}
|
|
114
92
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Reads what Claude Code saved in a profile, for display only. Nothing is written,
|
|
2
|
+
// and any field with an unexpected shape is left out instead of failing.
|
|
3
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { PROFILES_DIR } from './constants.mjs';
|
|
6
|
+
|
|
7
|
+
const PLANS = { claude_max: 'Max', claude_pro: 'Pro', claude_team: 'Team', claude_enterprise: 'Enterprise' };
|
|
8
|
+
|
|
9
|
+
function readConfig(dir) {
|
|
10
|
+
try {
|
|
11
|
+
const config = JSON.parse(readFileSync(join(dir, '.claude.json'), 'utf8'));
|
|
12
|
+
return config && typeof config === 'object' ? config : {};
|
|
13
|
+
} catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function percent(window) {
|
|
19
|
+
const value = window?.utilization;
|
|
20
|
+
return typeof value === 'number' && Number.isFinite(value) ? Math.min(100, Math.max(0, Math.round(value))) : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Claude Code keeps usage in an undocumented cache; it may change or disappear.
|
|
24
|
+
function usage(cached, account) {
|
|
25
|
+
if (!cached || typeof cached !== 'object') return null;
|
|
26
|
+
if (typeof cached.accountUuid === 'string' && typeof account.accountUuid === 'string'
|
|
27
|
+
&& cached.accountUuid !== account.accountUuid) return null;
|
|
28
|
+
const fiveHour = percent(cached.utilization?.five_hour);
|
|
29
|
+
const weekly = percent(cached.utilization?.seven_day);
|
|
30
|
+
if (fiveHour === null && weekly === null) return null;
|
|
31
|
+
const savedAt = Number.isFinite(cached.fetchedAtMs) ? cached.fetchedAtMs : null;
|
|
32
|
+
return { fiveHour, weekly, savedAt };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function accountInfo(name) {
|
|
36
|
+
const dir = join(PROFILES_DIR, name);
|
|
37
|
+
const config = readConfig(dir);
|
|
38
|
+
const account = config.oauthAccount && typeof config.oauthAccount === 'object' ? config.oauthAccount : {};
|
|
39
|
+
let lastUsed = null;
|
|
40
|
+
try { lastUsed = statSync(join(dir, 'history.jsonl')).mtimeMs; } catch { /* never used */ }
|
|
41
|
+
return {
|
|
42
|
+
email: typeof account.emailAddress === 'string' && account.emailAddress ? account.emailAddress : null,
|
|
43
|
+
plan: Object.hasOwn(PLANS, account.organizationType) ? PLANS[account.organizationType] : null,
|
|
44
|
+
usage: usage(config.cachedUsageUtilization, account),
|
|
45
|
+
lastUsed,
|
|
46
|
+
};
|
|
47
|
+
}
|
package/src/lib/config.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
-
META_FILE, PROFILE_NAME_REGEX, PROFILE_NAME_MAX_LENGTH, RESERVED_NAMES, SHAREABLE_ENTRIES,
|
|
2
|
+
LANGUAGES, META_FILE, PROFILE_NAME_REGEX, PROFILE_NAME_MAX_LENGTH, RESERVED_NAMES, SHAREABLE_ENTRIES,
|
|
3
3
|
} from './constants.mjs';
|
|
4
4
|
import { readJsonFile, writeJsonFile } from './json-fs.mjs';
|
|
5
5
|
|
|
@@ -19,6 +19,7 @@ export function readMeta() {
|
|
|
19
19
|
|| new Set(meta.profiles).size !== meta.profiles.length
|
|
20
20
|
|| (meta.activeProfile !== null && !meta.profiles.includes(meta.activeProfile))
|
|
21
21
|
|| typeof meta.shareSettings !== 'boolean'
|
|
22
|
+
|| (meta.language !== undefined && !LANGUAGES.includes(meta.language))
|
|
22
23
|
|| (meta.shared !== undefined && (!Array.isArray(meta.shared)
|
|
23
24
|
|| meta.shared.some(entry => !SHAREABLE_ENTRIES.includes(entry))
|
|
24
25
|
|| new Set(meta.shared).size !== meta.shared.length))) {
|
|
@@ -61,6 +62,12 @@ export function setSharedEntries(entries) {
|
|
|
61
62
|
writeMeta({ ...meta, shared: entries });
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
// null clears the choice so the system language is used again.
|
|
66
|
+
export function setLanguagePreference(language) {
|
|
67
|
+
const { language: _previous, ...meta } = readMeta();
|
|
68
|
+
writeMeta(language ? { ...meta, language } : meta);
|
|
69
|
+
}
|
|
70
|
+
|
|
64
71
|
export function removeProfileFromMeta(name) {
|
|
65
72
|
const meta = readMeta();
|
|
66
73
|
meta.profiles = meta.profiles.filter(p => p !== name);
|
package/src/lib/constants.mjs
CHANGED
|
@@ -38,3 +38,7 @@ export const SHAREABLE_FILES = ['CLAUDE.md', 'keybindings.json'];
|
|
|
38
38
|
export const ACCOUNT_SKILL_ENTRIES = ['synced'];
|
|
39
39
|
|
|
40
40
|
export const SELF_PKG = 'ai-profile-switch';
|
|
41
|
+
|
|
42
|
+
// Screen languages; English is the fallback for anything missing.
|
|
43
|
+
export const LANGUAGES = ['en', 'ko'];
|
|
44
|
+
export const LANGUAGE_ENV = 'AI_PROFILE_SWITCH_LANG';
|
package/src/lib/i18n.mjs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Chooses the screen language and looks up messages. English fills any gap.
|
|
2
|
+
import { LANGUAGES, LANGUAGE_ENV, META_FILE } from './constants.mjs';
|
|
3
|
+
import { readJsonFile } from './json-fs.mjs';
|
|
4
|
+
import en from '../locales/en.mjs';
|
|
5
|
+
import ko from '../locales/ko.mjs';
|
|
6
|
+
|
|
7
|
+
const MESSAGES = { en, ko };
|
|
8
|
+
export const LANGUAGE_NAMES = { en: 'English', ko: '한국어' };
|
|
9
|
+
|
|
10
|
+
function languageCode(value) {
|
|
11
|
+
const code = String(value ?? '').trim().split(/[-_.@]/)[0].toLowerCase();
|
|
12
|
+
return code || null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Terminal locale first (LC_ALL → LC_MESSAGES → LANG), then the OS locale.
|
|
16
|
+
// A set but unknown locale such as C means English rather than a fallthrough.
|
|
17
|
+
export function systemLanguage(env = process.env, intlLocale) {
|
|
18
|
+
const code = languageCode(env.LC_ALL || env.LC_MESSAGES || env.LANG)
|
|
19
|
+
?? languageCode(intlLocale ?? Intl.DateTimeFormat().resolvedOptions().locale);
|
|
20
|
+
return LANGUAGES.includes(code) ? code : 'en';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function savedLanguage() {
|
|
24
|
+
try {
|
|
25
|
+
const { language } = readJsonFile(META_FILE, {}) ?? {};
|
|
26
|
+
return LANGUAGES.includes(language) ? language : null;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function detectLanguage({ saved = savedLanguage(), env = process.env, intlLocale } = {}) {
|
|
33
|
+
if (saved) return saved;
|
|
34
|
+
const forced = languageCode(env[LANGUAGE_ENV]);
|
|
35
|
+
if (LANGUAGES.includes(forced)) return forced;
|
|
36
|
+
return systemLanguage(env, intlLocale);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let current;
|
|
40
|
+
|
|
41
|
+
export function language() {
|
|
42
|
+
current ??= detectLanguage();
|
|
43
|
+
return current;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function setLanguage(code) {
|
|
47
|
+
current = code;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function t(key, vars = {}) {
|
|
51
|
+
const message = MESSAGES[language()][key] ?? en[key];
|
|
52
|
+
if (message === undefined) throw new Error(`Missing message: ${key}`);
|
|
53
|
+
return message.replace(/\{(\w+)\}/g, (match, name) => (Object.hasOwn(vars, name) ? String(vars[name]) : match));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const TIME_UNITS = [['year', 31536000], ['month', 2592000], ['week', 604800], ['day', 86400], ['hour', 3600], ['minute', 60]];
|
|
57
|
+
|
|
58
|
+
export function relativeTime(ms, now = Date.now()) {
|
|
59
|
+
const seconds = Math.round((ms - now) / 1000);
|
|
60
|
+
const format = new Intl.RelativeTimeFormat(language(), { numeric: 'auto' });
|
|
61
|
+
for (const [unit, size] of TIME_UNITS) {
|
|
62
|
+
if (Math.abs(seconds) >= size) return format.format(Math.round(seconds / size), unit);
|
|
63
|
+
}
|
|
64
|
+
return t('time.justNow');
|
|
65
|
+
}
|