ai-profile-switch 0.0.0-stage → 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.
@@ -0,0 +1,152 @@
1
+ import * as prompt from '../lib/prompt.mjs';
2
+ import { readMeta } from '../lib/config.mjs';
3
+ import { color, displayWidth, error, intro, sym, warn } from '../lib/ui.mjs';
4
+ import { accountInfo } 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';
8
+ import { add } from './add.mjs';
9
+ import { list } from './list.mjs';
10
+ import { login } from './login.mjs';
11
+ import { accountSummary, profileChoices, usageLine } from './shell.mjs';
12
+ import { use } from './use.mjs';
13
+ import { remove } from './remove.mjs';
14
+ import { rename } from './rename.mjs';
15
+ import { share } from './share.mjs';
16
+ import { language } from './language.mjs';
17
+
18
+ function help() {
19
+ console.log(t('manage.help'));
20
+ }
21
+
22
+ export async function manage(args = []) {
23
+ if (args.some(arg => ['--help', '-h'].includes(arg))) {
24
+ help();
25
+ return;
26
+ }
27
+ if (args.length === 1 && args[0] === '--list') return list();
28
+ if (args[0] === 'share') return share(args.slice(1));
29
+ if (args[0] === 'language') return language(args.slice(1));
30
+ if (args.length === 1 && !args[0].startsWith('-')) return use(args[0]);
31
+ if (args.length > 0) {
32
+ const [command, ...names] = args;
33
+ const operations = { add: [1, add], remove: [1, remove], rename: [2, rename], login: [1, login] };
34
+ const operation = Object.hasOwn(operations, command) ? operations[command] : undefined;
35
+ if (!operation || names.length !== operation[0] || names.some(name => name.startsWith('-'))) {
36
+ throw new Error(t('manage.invalid'));
37
+ }
38
+ return operation[1](...names);
39
+ }
40
+ if (!process.stdin.isTTY) {
41
+ help();
42
+ return;
43
+ }
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(' · ') }) : '');
48
+ while (true) {
49
+ const meta = readMeta();
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);
56
+ if (!action || action === 'exit') return;
57
+ try {
58
+ if (action === 'share') {
59
+ await share();
60
+ } else if (action === 'language') {
61
+ await language();
62
+ } else if (action === 'add') {
63
+ const name = await prompt.text(t('menu.addName'));
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);
68
+ } else {
69
+ await manageProfiles();
70
+ }
71
+ } catch (err) {
72
+ error(err.message, err.hints);
73
+ }
74
+ }
75
+ }
76
+
77
+ function pickProfile(question, meta) {
78
+ return prompt.select(question, profileChoices(meta.profiles, meta.activeProfile), {
79
+ initial: Math.max(0, meta.profiles.indexOf(meta.activeProfile)), numbered: true, back: true,
80
+ });
81
+ }
82
+
83
+ // Pick a profile, then act on it from its detail screen until Back.
84
+ async function manageProfiles() {
85
+ while (true) {
86
+ const meta = readMeta();
87
+ if (!meta.profiles.length) return;
88
+ const name = await pickProfile(t('menu.managePick'), meta);
89
+ if (!name) return;
90
+ let current = name;
91
+ while (current) {
92
+ try {
93
+ current = await profileDetail(current);
94
+ } catch (err) {
95
+ error(err.message, err.hints);
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ function detailLines(name) {
102
+ const account = accountInfo(name);
103
+ const rows = [
104
+ [t('detail.account'), accountSummary(account) || color.dim(t('account.notSignedIn'))],
105
+ ...(account.email ? [[t('detail.usage'), account.usage ? usageLine(account) : color.dim(t('detail.noUsage'))]] : []),
106
+ [t('detail.lastUsed'), account.lastUsed ? relativeTime(account.lastUsed) : color.dim(t('detail.never'))],
107
+ [t('detail.folder'), color.dim(shortPath(profileDir(name)))],
108
+ ];
109
+ const width = Math.max(...rows.map(([label]) => displayWidth(label)));
110
+ return [...rows.map(([label, value]) => `${color.dim(label)}${' '.repeat(width - displayWidth(label) + 2)}${value}`), ''];
111
+ }
112
+
113
+ function shortPath(path) {
114
+ return path.startsWith(HOME) ? `~${path.slice(HOME.length)}` : path;
115
+ }
116
+
117
+ // Returns the profile to show again, or null to go back to the list.
118
+ async function profileDetail(name) {
119
+ const title = t('detail.title', { name });
120
+ const action = await prompt.select(color.bold(title), [
121
+ { label: t('detail.use'), value: 'use' },
122
+ { label: t('detail.rename'), value: 'rename' },
123
+ { label: t('detail.login'), value: 'login', hint: color.dim(t('detail.loginHint')) },
124
+ { label: t('detail.remove'), value: 'remove', hint: color.dim(t('menu.removeHint')) },
125
+ { label: t('detail.back'), value: 'back' },
126
+ ], { body: detailLines(name), back: true });
127
+ if (!action || action === 'back') return null;
128
+ if (action === 'use') {
129
+ await use(name);
130
+ return name;
131
+ }
132
+ if (action === 'remove') {
133
+ await remove(name);
134
+ return readMeta().profiles.includes(name) ? name : null;
135
+ }
136
+ if (action === 'rename') {
137
+ warn(t('rename.closeSessions', { name }));
138
+ const newName = await prompt.text(t('rename.newName'), undefined, t('rename.newNameHint', { name }));
139
+ if (!newName || newName === name) return name;
140
+ checkRename(name, newName);
141
+ if (!await prompt.confirm(t('rename.confirm', { old: name, new: newName }), false)) return name;
142
+ await rename(name, newName, { warned: true });
143
+ return newName;
144
+ }
145
+ const account = accountSummary(accountInfo(name));
146
+ console.log(`${color.dim(sym.rail)} ${color.dim(t('login.current'))} ${account || color.dim(t('account.notSignedIn'))}`);
147
+ console.log(`${color.dim(sym.rail)} ${color.dim(t('login.onlyThis'))}`);
148
+ console.log(`${color.dim(sym.rail)} ${color.dim(t('login.browser'))}`);
149
+ console.log(`${color.dim(sym.rail)} ${color.dim(t('login.closeSessions', { name }))}`);
150
+ if (await prompt.confirm(t('login.confirm', { name }), false)) await login(name, { explained: true });
151
+ return name;
152
+ }
@@ -0,0 +1,28 @@
1
+ import { join } from 'node:path';
2
+ import { PROFILES_DIR, SHARED_DIR } from '../../lib/constants.mjs';
3
+ import { readMeta } from '../../lib/config.mjs';
4
+ import { readJsonFile } from '../../lib/json-fs.mjs';
5
+ import { info, error } from '../../lib/ui.mjs';
6
+ import { t } from '../../lib/i18n.mjs';
7
+
8
+ export async function mcp(args = []) {
9
+ if (args.length > 1 || (args[0] && args[0] !== 'list')) {
10
+ error(t('mcp.noWrites'));
11
+ process.exitCode = 1;
12
+ return;
13
+ }
14
+ info(t('mcp.preserved'));
15
+ const files = [join(SHARED_DIR, 'settings.json'),
16
+ ...readMeta().profiles.map(name => join(PROFILES_DIR, name, 'settings.local.json'))];
17
+ for (const file of files) {
18
+ const data = readJsonFile(file);
19
+ const names = Object.keys(data.mcpServers ?? {});
20
+ const disabled = Array.isArray(data.disabledMcpServers) ? data.disabledMcpServers : [];
21
+ if (names.length || disabled.length) {
22
+ console.log(`${file}: ${names.join(', ') || t('mcp.noServers')}`);
23
+ if (disabled.length) console.log(` ${t('mcp.disabled', { list: disabled.join(', ') })}`);
24
+ }
25
+ }
26
+ info(t('mcp.where'));
27
+ info(t('mcp.see'));
28
+ }
@@ -0,0 +1,57 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { resolve } from 'node:path';
3
+ import { color, success, warn } from '../lib/ui.mjs';
4
+ import * as prompt from '../lib/prompt.mjs';
5
+ import { readMeta } from '../lib/config.mjs';
6
+ import { migrateDir, profileExists, validateProfileName } from '../lib/profile.mjs';
7
+ import { t } from '../lib/i18n.mjs';
8
+ import { existingClaudeSources } from './init.mjs';
9
+
10
+ export async function migrate(profileName, argv = []) {
11
+ const { values, positionals } = parseArgs({
12
+ args: argv,
13
+ options: { from: { type: 'string' } },
14
+ allowPositionals: true,
15
+ });
16
+ const meta = readMeta();
17
+ if (meta.profiles.length === 0) {
18
+ throw new Error(t('migrate.noProfiles'));
19
+ }
20
+ if (positionals.length > 1) throw new Error('Usage: ai-profile-switch migrate [profile] --from <path>');
21
+ let target = positionals[0] || (profileName && !profileName.startsWith('-') ? profileName : undefined);
22
+ if (!target) target = await prompt.select(t('migrate.target'), meta.profiles);
23
+ if (!target) return;
24
+ const error = validateProfileName(target);
25
+ if (error) throw new Error(error);
26
+ if (!profileExists(target) || !meta.profiles.includes(target)) {
27
+ throw new Error(t('profile.notFound', { name: target }));
28
+ }
29
+
30
+ let source = values.from;
31
+ if (source !== undefined && !source.trim()) throw new Error(t('migrate.emptySource'));
32
+ if (!source) {
33
+ const choices = existingClaudeSources().map(path => ({ label: path, value: path }));
34
+ choices.push({ label: t('migrate.customPath'), value: '__custom__' });
35
+ source = await prompt.select(t('migrate.which'), choices);
36
+ if (!source) return;
37
+ if (source === '__custom__') source = await prompt.text(t('migrate.enterPath'), '');
38
+ if (!source.trim()) throw new Error(t('migrate.emptySource'));
39
+ }
40
+ source = resolve(source);
41
+ console.log();
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);
47
+ if (!confirmed) {
48
+ console.log(` ${t('common.cancelled')}`);
49
+ return;
50
+ }
51
+ const { backupPath } = migrateDir(source, target, meta.shareSettings !== false);
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')}`);
56
+ console.log();
57
+ }
@@ -0,0 +1,34 @@
1
+ import { success, warn } from '../lib/ui.mjs';
2
+ import { confirm } from '../lib/prompt.mjs';
3
+ import { assertRegisteredProfile, removeProfile } from '../lib/profile.mjs';
4
+ import { getActiveProfile } from '../lib/config.mjs';
5
+ import { t } from '../lib/i18n.mjs';
6
+
7
+ export async function remove(name) {
8
+ if (!name) throw new Error('Usage: cpf remove <name>');
9
+
10
+ assertRegisteredProfile(name);
11
+
12
+ const active = getActiveProfile();
13
+ if (name === active) {
14
+ warn(t('remove.activeWarn', { name }));
15
+ }
16
+
17
+ const yes = await confirm(t('remove.confirm', { name }), false);
18
+ if (!yes) {
19
+ console.log(` ${t('common.cancelled')}`);
20
+ return;
21
+ }
22
+
23
+ removeProfile(name);
24
+ success(t('remove.done', { name }));
25
+
26
+ if (name === active) {
27
+ const newActive = getActiveProfile();
28
+ if (newActive) {
29
+ success(t('remove.newActive', { name: newActive }));
30
+ } else {
31
+ warn(`${t('remove.none')} ${t('hint.add')} cpf add work · ${t('hint.setup')} npx ai-profile-switch@latest init`);
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,19 @@
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';
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 } = {}) {
7
+ if (!oldName || !newName) throw new Error('Usage: cpf rename <old-name> <new-name>');
8
+ checkRename(oldName, newName);
9
+ if (oldName === newName) {
10
+ info(t('rename.same', { name: oldName }));
11
+ return;
12
+ }
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) })}`);
19
+ }
@@ -0,0 +1,60 @@
1
+ import { color, info, success, warn } from '../lib/ui.mjs';
2
+ import { SHAREABLE_ENTRIES, SHARED_FILES, SHARED_DIRS } from '../lib/constants.mjs';
3
+ import { readMeta } from '../lib/config.mjs';
4
+ import { shareEntry, syncShared, unshareEntry } from '../lib/shared.mjs';
5
+ import { t } from '../lib/i18n.mjs';
6
+
7
+ export function reportConflicts(results) {
8
+ for (const { name, conflicts, missing } of results) {
9
+ for (const entry of conflicts) {
10
+ warn(t('share.keptOwn', { name, entry }));
11
+ }
12
+ for (const entry of missing) warn(t('share.missing', { name, entry }));
13
+ }
14
+ }
15
+
16
+ function status() {
17
+ const meta = readMeta();
18
+ const shared = meta.shared ?? [];
19
+ console.log();
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 }))}`);
23
+ console.log();
24
+ for (const result of syncShared(meta.profiles, { dryRun: true })) {
25
+ const problems = [
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 }))),
29
+ ];
30
+ console.log(` ${result.name.padEnd(12)} ${problems.length ? problems.join(', ') : color.green(t('share.inSync'))}`);
31
+ }
32
+ if (!shared.length) info(t('share.nothingExtra'));
33
+ console.log();
34
+ }
35
+
36
+ export async function share(args = []) {
37
+ const [command, entry, ...rest] = args;
38
+ if (rest.length || (['add', 'remove'].includes(command) ? !entry : entry !== undefined)) {
39
+ throw new Error(t('share.usage'));
40
+ }
41
+ if (!command || command === 'list') return status();
42
+ if (command === 'sync') {
43
+ const results = syncShared();
44
+ reportConflicts(results);
45
+ success(t('share.linked', { count: results.reduce((count, result) => count + result.linked.length, 0) }));
46
+ return;
47
+ }
48
+ if (command === 'add') {
49
+ const results = shareEntry(entry);
50
+ reportConflicts(results);
51
+ success(t('share.added', { entry }));
52
+ return;
53
+ }
54
+ if (command === 'remove') {
55
+ unshareEntry(entry);
56
+ success(t('share.removed', { entry }));
57
+ return;
58
+ }
59
+ throw new Error(t('share.usage'));
60
+ }
@@ -0,0 +1,220 @@
1
+ import { spawn, execFileSync } from 'node:child_process';
2
+ import { accessSync, constants, statSync } from 'node:fs';
3
+ import { constants as osConstants } from 'node:os';
4
+ import { join, delimiter } from 'node:path';
5
+ import { getActiveProfile, setActiveProfile } from '../lib/config.mjs';
6
+ import { HOME, IS_WINDOWS } from '../lib/constants.mjs';
7
+ import { installAllShells } from '../lib/shell.mjs';
8
+ import { listProfiles, profileDir, assertRegisteredProfile } from '../lib/profile.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';
12
+ import { syncShared } from '../lib/shared.mjs';
13
+ import { reportConflicts } from './share.mjs';
14
+ import * as prompt from '../lib/prompt.mjs';
15
+ import { scheduleSelfUpdate } from '../lib/self-update.mjs';
16
+
17
+ export async function shell(argv = []) {
18
+ const [subcommand, ...args] = argv;
19
+ switch (subcommand) {
20
+ case 'launch': {
21
+ let profile;
22
+ let forwarded = args;
23
+ if (args[0] === '--profile') {
24
+ if (!args[1] || args[2] !== '--') throw new Error('Expected shell launch --profile <name> -- <args>');
25
+ profile = args[1];
26
+ forwarded = args.slice(3);
27
+ }
28
+ await launch(forwarded, profile);
29
+ return;
30
+ }
31
+ case 'use':
32
+ if (args.length !== 1) throw new Error('Usage: cpf <profile-name>');
33
+ useProfile(args[0]);
34
+ return;
35
+ case 'pick':
36
+ if (args.length && (args.length !== 1 || args[0] !== '--print')) throw new Error('Usage: shell pick [--print]');
37
+ await pickProfile(args[0] === '--print');
38
+ return;
39
+ case 'refresh':
40
+ if (args.length) throw new Error('Usage: shell refresh');
41
+ installAllShells();
42
+ success(t('shell.refreshed'));
43
+ return;
44
+ default:
45
+ throw new Error('Usage: shell <launch|use|pick|refresh>');
46
+ }
47
+ }
48
+
49
+ async function launch(args, selected) {
50
+ const profile = selected ?? await selectProfile({ launching: true });
51
+ if (!profile) {
52
+ process.exitCode = 130;
53
+ return;
54
+ }
55
+ assertRegisteredProfile(profile);
56
+ const bin = resolveClaudeBinary();
57
+ if (!bin) {
58
+ console.error(`${t('launch.notFound')} ${t('launch.install')} https://code.claude.com/docs/en/setup`);
59
+ process.exitCode = 127;
60
+ return;
61
+ }
62
+ scheduleSelfUpdate();
63
+ if (getActiveProfile() !== profile) setActiveProfile(profile);
64
+ // Sharing is a convenience; a broken link must not stop Claude from starting.
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`);
68
+ const cmd = bin.endsWith('.js') ? process.execPath : bin;
69
+ const childArgs = bin.endsWith('.js') ? [bin, ...args] : args;
70
+ const result = await new Promise(resolve => {
71
+ const child = spawn(cmd, childArgs, {
72
+ stdio: 'inherit', shell: false,
73
+ env: { ...process.env, CLAUDE_CONFIG_DIR: profileDir(profile) },
74
+ });
75
+ let interrupted;
76
+ const onInt = () => { interrupted = 'SIGINT'; child.kill('SIGINT'); };
77
+ const onTerm = () => { interrupted = 'SIGTERM'; child.kill('SIGTERM'); };
78
+ process.on('SIGINT', onInt);
79
+ process.on('SIGTERM', onTerm);
80
+ const cleanup = () => {
81
+ process.removeListener('SIGINT', onInt);
82
+ process.removeListener('SIGTERM', onTerm);
83
+ };
84
+ child.once('error', err => {
85
+ cleanup();
86
+ console.error(t('launch.startFailed', { message: err.message }));
87
+ resolve(1);
88
+ });
89
+ child.once('close', (code, signal) => {
90
+ cleanup();
91
+ resolve(interrupted ? 128 + osConstants.signals[interrupted] : code ?? (128 + (osConstants.signals[signal] ?? 1)));
92
+ });
93
+ });
94
+ process.exitCode = result;
95
+ }
96
+
97
+ function useProfile(name) {
98
+ assertRegisteredProfile(name);
99
+ if (getActiveProfile() !== name) setActiveProfile(name);
100
+ success(t('use.switched', { profile: name }));
101
+ }
102
+
103
+ // The claude shell function runs `pick --print` and then launches the printed profile.
104
+ async function pickProfile(printOnly) {
105
+ const profile = await selectProfile({ launching: printOnly });
106
+ if (!profile) {
107
+ process.exitCode = 130;
108
+ return;
109
+ }
110
+ if (getActiveProfile() !== profile) setActiveProfile(profile);
111
+ if (printOnly) process.stdout.write(profile + '\n');
112
+ }
113
+
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 } = {}) {
146
+ const profiles = listProfiles();
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
+ }
153
+ const active = getActiveProfile();
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);
176
+ if (selected) assertRegisteredProfile(selected);
177
+ return selected;
178
+ }
179
+
180
+ // Declared below; the Windows resolver test runs that part on its own.
181
+ export { resolveClaudeBinary };
182
+
183
+ function executable(file) {
184
+ try {
185
+ accessSync(file, IS_WINDOWS ? constants.F_OK : constants.X_OK);
186
+ return statSync(file).isFile();
187
+ } catch {
188
+ return false;
189
+ }
190
+ }
191
+
192
+ function resolveClaudeBinary() {
193
+ const names = IS_WINDOWS ? ['claude.exe'] : ['claude'];
194
+ for (const directory of (process.env.PATH ?? '').split(delimiter).filter(Boolean)) {
195
+ for (const name of names) {
196
+ const file = join(directory, name);
197
+ if (executable(file)) return file;
198
+ }
199
+ }
200
+ const binName = IS_WINDOWS ? 'claude.exe' : 'claude';
201
+ const native = [join(HOME, '.local/bin', binName), join(HOME, '.claude/local', binName)];
202
+ if (!IS_WINDOWS) native.push('/opt/homebrew/bin/claude', '/usr/local/bin/claude');
203
+ const found = native.find(executable);
204
+ if (found) return found;
205
+ try {
206
+ const npmRoot = execFileSync(IS_WINDOWS ? 'npm.cmd' : 'npm', ['root', '-g'], {
207
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000, shell: IS_WINDOWS,
208
+ }).trim();
209
+ const candidates = IS_WINDOWS ? ['bin/claude.exe', 'cli.js'] : ['bin/claude', 'cli.js'];
210
+ return candidates
211
+ .map(file => join(npmRoot, '@anthropic-ai/claude-code', file))
212
+ .find(file => executable(file) || (file.endsWith('.js') && executable(process.execPath) && exists(file))) ?? null;
213
+ } catch {
214
+ return null;
215
+ }
216
+ }
217
+
218
+ function exists(file) {
219
+ try { accessSync(file); return statSync(file).isFile(); } catch { return false; }
220
+ }
@@ -0,0 +1,53 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { computeUpdatePlan, compareSemver } from '../lib/updater.mjs';
3
+ import { readFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { installedRuntimeDirectory, runSelfUpdate } from '../lib/self-update.mjs';
6
+ import { displayWidth, info, warn, error } from '../lib/ui.mjs';
7
+ import { t } from '../lib/i18n.mjs';
8
+
9
+ export async function update(args = []) {
10
+ const { values, positionals } = parseArgs({ args, allowPositionals: true, options: {
11
+ check: { type: 'boolean', short: 'n' },
12
+ self: { type: 'boolean' },
13
+ 'claude-code': { type: 'boolean' },
14
+ yes: { type: 'boolean', short: 'y' },
15
+ } });
16
+ if (positionals.length) throw new Error('Usage: update [--check] [--self]');
17
+ if (values['claude-code']) {
18
+ info(t('update.claudeManaged'));
19
+ info(t('update.seeSetup'));
20
+ if (values.check) {
21
+ warn(t('update.notChecked'));
22
+ process.exitCode = 2;
23
+ }
24
+ if (!values.self) return;
25
+ }
26
+ let self;
27
+ try {
28
+ ({ self } = await computeUpdatePlan());
29
+ const runtime = installedRuntimeDirectory();
30
+ if (runtime) {
31
+ self.installed = JSON.parse(readFileSync(join(runtime, 'package.json'), 'utf8')).version;
32
+ self.hasUpdate = compareSemver(self.installed, self.latest) < 0;
33
+ }
34
+ } catch (err) {
35
+ error(err.message);
36
+ process.exitCode = 2;
37
+ return;
38
+ }
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'));
44
+ else {
45
+ const result = await runSelfUpdate({ force: true, computePlan: async () => ({ self }) });
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; }
50
+ }
51
+ if (values.check && !values['claude-code']) process.exitCode = self.hasUpdate ? 1 : 0;
52
+ }
53
+
@@ -0,0 +1,16 @@
1
+ import { color, success, info } from '../lib/ui.mjs';
2
+ import { assertRegisteredProfile } from '../lib/profile.mjs';
3
+ import { setActiveProfile, getActiveProfile } from '../lib/config.mjs';
4
+ import { t } from '../lib/i18n.mjs';
5
+
6
+ export async function use(name) {
7
+ assertRegisteredProfile(name);
8
+ const current = getActiveProfile();
9
+ if (current === name) {
10
+ info(t('use.already', { profile: name }));
11
+ return;
12
+ }
13
+ setActiveProfile(name);
14
+ success(t('use.switched', { profile: name }));
15
+ console.log(` ${color.dim(t('use.note'))}`);
16
+ }