ai-profile-switch 0.0.0-stage → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,96 @@
1
+ import * as prompt from '../lib/prompt.mjs';
2
+ import { readMeta } from '../lib/config.mjs';
3
+ import { error, warn } from '../lib/ui.mjs';
4
+ import { add } from './add.mjs';
5
+ import { list } from './list.mjs';
6
+ import { use } from './use.mjs';
7
+ import { remove } from './remove.mjs';
8
+ import { rename } from './rename.mjs';
9
+ import { share } from './share.mjs';
10
+
11
+ 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]`);
28
+ }
29
+
30
+ export async function manage(args = []) {
31
+ if (args.some(arg => ['--help', '-h'].includes(arg))) {
32
+ help();
33
+ return;
34
+ }
35
+ if (args.length === 1 && args[0] === '--list') return list();
36
+ if (args[0] === 'share') return share(args.slice(1));
37
+ if (args.length === 1 && !args[0].startsWith('-')) return use(args[0]);
38
+ if (args.length > 0) {
39
+ const [command, ...names] = args;
40
+ const operations = { add: [1, add], remove: [1, remove], rename: [2, rename] };
41
+ const operation = Object.hasOwn(operations, command) ? operations[command] : undefined;
42
+ if (!operation || names.length !== operation[0] || names.some(name => name.startsWith('-'))) {
43
+ throw new Error('Invalid arguments. Run cpf --help for usage.');
44
+ }
45
+ return operation[1](...names);
46
+ }
47
+ if (!process.stdin.isTTY) {
48
+ help();
49
+ return;
50
+ }
51
+
52
+ while (true) {
53
+ 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);
67
+ if (!action || action === 'exit') return;
68
+ try {
69
+ if (action === 'list') {
70
+ await list();
71
+ } else if (action === 'share') {
72
+ await share();
73
+ } else if (action === 'add') {
74
+ const name = await prompt.text('New profile name (empty to cancel):');
75
+ if (name) await add(name);
76
+ } 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
+ }
91
+ }
92
+ } catch (err) {
93
+ error(err.message);
94
+ }
95
+ }
96
+ }
@@ -0,0 +1,27 @@
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
+
7
+ export async function mcp(args = []) {
8
+ 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
+ process.exitCode = 1;
11
+ return;
12
+ }
13
+ info('Legacy MCP data is preserved. Register servers using claude mcp for the selected profile.');
14
+ const files = [join(SHARED_DIR, 'settings.json'),
15
+ ...readMeta().profiles.map(name => join(PROFILES_DIR, name, 'settings.local.json'))];
16
+ for (const file of files) {
17
+ const data = readJsonFile(file);
18
+ const names = Object.keys(data.mcpServers ?? {});
19
+ const disabled = Array.isArray(data.disabledMcpServers) ? data.disabledMcpServers : [];
20
+ if (names.length || disabled.length) {
21
+ console.log(`${file}: ${names.join(', ') || '(no servers)'}`);
22
+ if (disabled.length) console.log(` Legacy disabled: ${disabled.join(', ')}`);
23
+ }
24
+ }
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');
27
+ }
@@ -0,0 +1,56 @@
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 { existingClaudeSources } from './init.mjs';
8
+
9
+ export async function migrate(profileName, argv = []) {
10
+ const { values, positionals } = parseArgs({
11
+ args: argv,
12
+ options: { from: { type: 'string' } },
13
+ allowPositionals: true,
14
+ });
15
+ const meta = readMeta();
16
+ if (meta.profiles.length === 0) {
17
+ throw new Error('No profiles found. Run npx ai-profile-switch@latest init first.');
18
+ }
19
+ if (positionals.length > 1) throw new Error('Usage: ai-profile-switch migrate [profile] --from <path>');
20
+ 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) return;
23
+ const error = validateProfileName(target);
24
+ if (error) throw new Error(error);
25
+ if (!profileExists(target) || !meta.profiles.includes(target)) {
26
+ throw new Error(`Profile "${target}" does not exist.`);
27
+ }
28
+
29
+ let source = values.from;
30
+ if (source !== undefined && !source.trim()) throw new Error('Source path cannot be empty.');
31
+ if (!source) {
32
+ 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);
35
+ 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.');
38
+ }
39
+ source = resolve(source);
40
+ 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);
46
+ if (!confirmed) {
47
+ console.log(' Cancelled.');
48
+ return;
49
+ }
50
+ 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.');
55
+ console.log();
56
+ }
@@ -0,0 +1,33 @@
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
+
6
+ export async function remove(name) {
7
+ if (!name) throw new Error('Usage: cpf remove <name>');
8
+
9
+ assertRegisteredProfile(name);
10
+
11
+ const active = getActiveProfile();
12
+ if (name === active) {
13
+ warn(`"${name}" is the currently active profile`);
14
+ }
15
+
16
+ const yes = await confirm(`Delete profile "${name}"? This cannot be undone`, false);
17
+ if (!yes) {
18
+ console.log(' Cancelled.');
19
+ return;
20
+ }
21
+
22
+ removeProfile(name);
23
+ success(`Removed profile: ${name}`);
24
+
25
+ if (name === active) {
26
+ const newActive = getActiveProfile();
27
+ if (newActive) {
28
+ success(`Active profile switched to: ${newActive}`);
29
+ } else {
30
+ warn('No profiles remaining. Run cpf to add a profile, or npx ai-profile-switch@latest init to set up again.');
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,14 @@
1
+ import { info, success, warn } from '../lib/ui.mjs';
2
+ import { renameProfile } from '../lib/profile.mjs';
3
+
4
+ export async function rename(oldName, newName) {
5
+ if (!oldName || !newName) throw new Error('Usage: cpf rename <old-name> <new-name>');
6
+ if (oldName !== newName) {
7
+ warn(`Close running Claude sessions using "${oldName}" before renaming; they still use the old configuration path.`);
8
+ if (process.platform === 'darwin') {
9
+ warn('macOS may require signing in again after the configuration path changes.');
10
+ }
11
+ }
12
+ if (renameProfile(oldName, newName)) success(`Renamed profile: ${oldName} → ${newName}`);
13
+ else info(`"${oldName}" already has that name`);
14
+ }
@@ -0,0 +1,58 @@
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
+
6
+ export function reportConflicts(results) {
7
+ for (const { name, conflicts, missing } of results) {
8
+ for (const entry of conflicts) {
9
+ warn(`${name}: kept its own ${entry} because it differs from the shared copy. Move or delete it, then run cpf share sync.`);
10
+ }
11
+ for (const entry of missing) warn(`${name}: shared ${entry} is missing from _shared.`);
12
+ }
13
+ }
14
+
15
+ function status() {
16
+ const meta = readMeta();
17
+ const shared = meta.shared ?? [];
18
+ console.log();
19
+ console.log(` ${color.bold('Shared by every profile:')} ${[...SHARED_FILES, ...SHARED_DIRS, ...shared].join(', ')}`);
20
+ console.log(` ${color.dim(`Can also share: ${SHAREABLE_ENTRIES.filter(entry => !shared.includes(entry)).join(', ') || 'nothing left'}`)}`);
21
+ console.log();
22
+ for (const result of syncShared(meta.profiles, { dryRun: true })) {
23
+ const problems = [
24
+ ...result.conflicts.map(entry => color.yellow(`own ${entry}`)),
25
+ ...result.linked.map(entry => `${entry} not linked yet`),
26
+ ...result.missing.map(entry => color.red(`${entry} missing`)),
27
+ ];
28
+ console.log(` ${result.name.padEnd(12)} ${problems.length ? problems.join(', ') : color.green('in sync')}`);
29
+ }
30
+ if (!shared.length) info('Nothing extra is shared yet. Example: cpf share add CLAUDE.md');
31
+ console.log();
32
+ }
33
+
34
+ export async function share(args = []) {
35
+ const [command, entry, ...rest] = args;
36
+ if (rest.length || (['add', 'remove'].includes(command) ? !entry : entry !== undefined)) {
37
+ throw new Error('Usage: cpf share [list | add <entry> | remove <entry> | sync]');
38
+ }
39
+ if (!command || command === 'list') return status();
40
+ if (command === 'sync') {
41
+ const results = syncShared();
42
+ reportConflicts(results);
43
+ success(`Linked ${results.reduce((count, result) => count + result.linked.length, 0)} entries.`);
44
+ return;
45
+ }
46
+ if (command === 'add') {
47
+ const results = shareEntry(entry);
48
+ reportConflicts(results);
49
+ success(`Sharing ${entry} through ~/.claude-profiles/_shared/${entry}`);
50
+ return;
51
+ }
52
+ if (command === 'remove') {
53
+ unshareEntry(entry);
54
+ success(`Stopped sharing ${entry}. Each profile kept its own copy.`);
55
+ return;
56
+ }
57
+ throw new Error('Usage: cpf share [list | add <entry> | remove <entry> | sync]');
58
+ }
@@ -0,0 +1,159 @@
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 { success, warn } from '../lib/ui.mjs';
10
+ import { syncShared } from '../lib/shared.mjs';
11
+ import { reportConflicts } from './share.mjs';
12
+ import * as prompt from '../lib/prompt.mjs';
13
+ import { scheduleSelfUpdate } from '../lib/self-update.mjs';
14
+
15
+ export async function shell(argv = []) {
16
+ const [subcommand, ...args] = argv;
17
+ switch (subcommand) {
18
+ case 'launch': {
19
+ let profile;
20
+ let forwarded = args;
21
+ if (args[0] === '--profile') {
22
+ if (!args[1] || args[2] !== '--') throw new Error('Expected shell launch --profile <name> -- <args>');
23
+ profile = args[1];
24
+ forwarded = args.slice(3);
25
+ }
26
+ await launch(forwarded, profile);
27
+ return;
28
+ }
29
+ case 'use':
30
+ if (args.length !== 1) throw new Error('Usage: cpf <profile-name>');
31
+ useProfile(args[0]);
32
+ return;
33
+ case 'pick':
34
+ if (args.length && (args.length !== 1 || args[0] !== '--print')) throw new Error('Usage: shell pick [--print]');
35
+ await pickProfile(args[0] === '--print');
36
+ return;
37
+ case 'refresh':
38
+ if (args.length) throw new Error('Usage: shell refresh');
39
+ installAllShells();
40
+ success('Shell integration refreshed.');
41
+ return;
42
+ default:
43
+ throw new Error('Usage: shell <launch|use|pick|refresh>');
44
+ }
45
+ }
46
+
47
+ async function launch(args, selected) {
48
+ const profile = selected ?? await selectProfile();
49
+ if (!profile) {
50
+ process.exitCode = 130;
51
+ return;
52
+ }
53
+ assertRegisteredProfile(profile);
54
+ const bin = resolveClaudeBinary();
55
+ if (!bin) {
56
+ console.error('Claude Code executable not found. See https://code.claude.com/docs/en/setup');
57
+ process.exitCode = 127;
58
+ return;
59
+ }
60
+ scheduleSelfUpdate();
61
+ if (getActiveProfile() !== profile) setActiveProfile(profile);
62
+ // Sharing is a convenience; a broken link must not stop Claude from starting.
63
+ try { reportConflicts(syncShared([profile])); } catch (err) { warn(`Shared items not linked: ${err.message}`); }
64
+ process.stderr.write(`[ai-profile-switch] Profile: ${profile}\n`);
65
+ const cmd = bin.endsWith('.js') ? process.execPath : bin;
66
+ const childArgs = bin.endsWith('.js') ? [bin, ...args] : args;
67
+ const result = await new Promise(resolve => {
68
+ const child = spawn(cmd, childArgs, {
69
+ stdio: 'inherit', shell: false,
70
+ env: { ...process.env, CLAUDE_CONFIG_DIR: profileDir(profile) },
71
+ });
72
+ let interrupted;
73
+ const onInt = () => { interrupted = 'SIGINT'; child.kill('SIGINT'); };
74
+ const onTerm = () => { interrupted = 'SIGTERM'; child.kill('SIGTERM'); };
75
+ process.on('SIGINT', onInt);
76
+ process.on('SIGTERM', onTerm);
77
+ const cleanup = () => {
78
+ process.removeListener('SIGINT', onInt);
79
+ process.removeListener('SIGTERM', onTerm);
80
+ };
81
+ child.once('error', err => {
82
+ cleanup();
83
+ console.error(`Could not start Claude Code: ${err.message}`);
84
+ resolve(1);
85
+ });
86
+ child.once('close', (code, signal) => {
87
+ cleanup();
88
+ resolve(interrupted ? 128 + osConstants.signals[interrupted] : code ?? (128 + (osConstants.signals[signal] ?? 1)));
89
+ });
90
+ });
91
+ process.exitCode = result;
92
+ }
93
+
94
+ function useProfile(name) {
95
+ assertRegisteredProfile(name);
96
+ if (getActiveProfile() !== name) setActiveProfile(name);
97
+ success(`Switched to profile: ${name}`);
98
+ }
99
+
100
+ async function pickProfile(printOnly) {
101
+ const profile = await selectProfile();
102
+ if (!profile) {
103
+ process.exitCode = 130;
104
+ return;
105
+ }
106
+ if (getActiveProfile() !== profile) setActiveProfile(profile);
107
+ if (printOnly) process.stdout.write(profile + '\n');
108
+ else success(`Switched to profile: ${profile}`);
109
+ }
110
+
111
+ async function selectProfile() {
112
+ const profiles = listProfiles();
113
+ if (!profiles.length) throw new Error('No profiles found. Run cpf to add one, or npx ai-profile-switch@latest init to import existing Claude settings.');
114
+ const active = getActiveProfile();
115
+ const selected = !process.stdin.isTTY || profiles.length === 1
116
+ ? (active ?? profiles[0])
117
+ : await prompt.select('Select a profile:', profiles, { output: process.stderr, initial: profiles.indexOf(active) });
118
+ if (selected) assertRegisteredProfile(selected);
119
+ return selected;
120
+ }
121
+
122
+ function executable(file) {
123
+ try {
124
+ accessSync(file, IS_WINDOWS ? constants.F_OK : constants.X_OK);
125
+ return statSync(file).isFile();
126
+ } catch {
127
+ return false;
128
+ }
129
+ }
130
+
131
+ function resolveClaudeBinary() {
132
+ const names = IS_WINDOWS ? ['claude.exe'] : ['claude'];
133
+ for (const directory of (process.env.PATH ?? '').split(delimiter).filter(Boolean)) {
134
+ for (const name of names) {
135
+ const file = join(directory, name);
136
+ if (executable(file)) return file;
137
+ }
138
+ }
139
+ const binName = IS_WINDOWS ? 'claude.exe' : 'claude';
140
+ const native = [join(HOME, '.local/bin', binName), join(HOME, '.claude/local', binName)];
141
+ if (!IS_WINDOWS) native.push('/opt/homebrew/bin/claude', '/usr/local/bin/claude');
142
+ const found = native.find(executable);
143
+ if (found) return found;
144
+ try {
145
+ const npmRoot = execFileSync(IS_WINDOWS ? 'npm.cmd' : 'npm', ['root', '-g'], {
146
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000, shell: IS_WINDOWS,
147
+ }).trim();
148
+ const candidates = IS_WINDOWS ? ['bin/claude.exe', 'cli.js'] : ['bin/claude', 'cli.js'];
149
+ return candidates
150
+ .map(file => join(npmRoot, '@anthropic-ai/claude-code', file))
151
+ .find(file => executable(file) || (file.endsWith('.js') && executable(process.execPath) && exists(file))) ?? null;
152
+ } catch {
153
+ return null;
154
+ }
155
+ }
156
+
157
+ function exists(file) {
158
+ try { accessSync(file); return statSync(file).isFile(); } catch { return false; }
159
+ }
@@ -0,0 +1,50 @@
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 { info, warn, error } from '../lib/ui.mjs';
7
+
8
+ export async function update(args = []) {
9
+ const { values, positionals } = parseArgs({ args, allowPositionals: true, options: {
10
+ check: { type: 'boolean', short: 'n' },
11
+ self: { type: 'boolean' },
12
+ 'claude-code': { type: 'boolean' },
13
+ yes: { type: 'boolean', short: 'y' },
14
+ } });
15
+ if (positionals.length) throw new Error('Usage: update [--check] [--self]');
16
+ if (values['claude-code']) {
17
+ info('Claude Code manages its own updates. Run claude update (or your package manager).');
18
+ info('See https://code.claude.com/docs/en/setup');
19
+ if (values.check) {
20
+ warn('Claude Code update status was not checked.');
21
+ process.exitCode = 2;
22
+ }
23
+ if (!values.self) return;
24
+ }
25
+ let self;
26
+ try {
27
+ ({ self } = await computeUpdatePlan());
28
+ const runtime = installedRuntimeDirectory();
29
+ if (runtime) {
30
+ self.installed = JSON.parse(readFileSync(join(runtime, 'package.json'), 'utf8')).version;
31
+ self.hasUpdate = compareSemver(self.installed, self.latest) < 0;
32
+ }
33
+ } catch (err) {
34
+ error(err.message);
35
+ process.exitCode = 2;
36
+ return;
37
+ }
38
+ console.log('Package Installed Latest');
39
+ console.log(`${self.pkg} ${self.installed} ${self.latest}`);
40
+ if (!self.hasUpdate) info('This package is up to date.');
41
+ else if (values.check) info('Run npx ai-profile-switch@latest update to install.');
42
+ else {
43
+ const result = await runSelfUpdate({ force: true, computePlan: async () => ({ self }) });
44
+ if (result.action === 'updated') info(`Updated to ${result.version}. New launches use this version; active sessions keep their files.`);
45
+ else if (result.reason === 'not-installed') info('Local runtime is not installed; no installation was run. Run: npx ai-profile-switch@latest install-shell');
46
+ else if (result.reason === 'busy') { warn('An update is already running.'); process.exitCode = 2; }
47
+ else if (result.action === 'failed') { error(`Update failed; existing runtime retained: ${result.error}`); process.exitCode = 2; }
48
+ }
49
+ if (values.check && !values['claude-code']) process.exitCode = self.hasUpdate ? 1 : 0;
50
+ }
@@ -0,0 +1,14 @@
1
+ import { success, info } from '../lib/ui.mjs';
2
+ import { assertRegisteredProfile } from '../lib/profile.mjs';
3
+ import { setActiveProfile, getActiveProfile } from '../lib/config.mjs';
4
+
5
+ export async function use(name) {
6
+ assertRegisteredProfile(name);
7
+ const current = getActiveProfile();
8
+ if (current === name) {
9
+ info(`"${name}" is already the active profile`);
10
+ return;
11
+ }
12
+ setActiveProfile(name);
13
+ success(`Switched to profile: ${name}`);
14
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,114 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { dirname, join } from 'node:path';
4
+ import { error, color } from './lib/ui.mjs';
5
+
6
+ const pkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'));
7
+
8
+ const COMMANDS = {
9
+ init: () => import('./commands/init.mjs').then(m => m.init()),
10
+ add: args => import('./commands/add.mjs').then(m => m.add(args[0])),
11
+ remove: args => import('./commands/remove.mjs').then(m => m.remove(args[0])),
12
+ rename: args => import('./commands/rename.mjs').then(m => m.rename(...args)),
13
+ list: () => import('./commands/list.mjs').then(m => m.list()),
14
+ use: args => import('./commands/use.mjs').then(m => m.use(args[0])),
15
+ manage: args => import('./commands/manage.mjs').then(m => m.manage(args)),
16
+ 'install-shell': () => import('./commands/install-shell.mjs').then(m => m.installShell()),
17
+ migrate: args => import('./commands/migrate.mjs').then(m => m.migrate(undefined, args)),
18
+ share: args => import('./commands/share.mjs').then(m => m.share(args)),
19
+ mcp: args => import('./commands/mcp/index.mjs').then(m => m.mcp(args)),
20
+ update: args => import('./commands/update.mjs').then(m => m.update(args)),
21
+ shell: args => import('./commands/shell.mjs').then(m => m.shell(args)),
22
+ };
23
+
24
+ const USAGE = {
25
+ init: 'init',
26
+ add: 'add <name>',
27
+ remove: 'remove <name>',
28
+ rename: 'rename <old-name> <new-name>',
29
+ list: 'list',
30
+ use: 'use <name>',
31
+ manage: 'manage [profile | add <name> | rename <old-name> <new-name> | remove <name> | --list]',
32
+ 'install-shell': 'install-shell',
33
+ migrate: 'migrate [profile] [--from <path>]',
34
+ share: 'share [list | add <entry> | remove <entry> | sync]',
35
+ mcp: 'mcp [list]',
36
+ update: 'update [--check] [--self] [--claude-code] [--yes]',
37
+ shell: 'shell <launch|use|pick|refresh>',
38
+ };
39
+ const POSITIONAL_COUNTS = { init: 0, add: 1, remove: 1, rename: 2, list: 0, use: 1, 'install-shell': 0 };
40
+
41
+ function showHelp(command) {
42
+ if (command) {
43
+ console.log(`Usage: npx ai-profile-switch@latest ${USAGE[command]}`);
44
+ if (command === 'rename') console.log('Close running sessions for this profile first. Its history and settings are kept; macOS may require sign-in again.');
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.');
48
+ return;
49
+ }
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
+ `);
77
+ }
78
+
79
+ export async function run(argv) {
80
+ const [command, ...args] = argv;
81
+ if (!command || ['help', '--help', '-h'].includes(command)) {
82
+ if (command === 'help' && args.length === 1 && Object.hasOwn(COMMANDS, args[0])) showHelp(args[0]);
83
+ else showHelp();
84
+ return;
85
+ }
86
+ if (['--version', '-v'].includes(command)) {
87
+ console.log(pkg.version);
88
+ return;
89
+ }
90
+ if (!Object.hasOwn(COMMANDS, command)) {
91
+ error(`Unknown command: ${command}`);
92
+ showHelp();
93
+ process.exitCode = 1;
94
+ return;
95
+ }
96
+ try {
97
+ // Claude's forwarded arguments must reach it unchanged.
98
+ const wantsHelp = command === 'shell'
99
+ ? ['--help', '-h'].includes(args[0])
100
+ : command !== 'manage' && args.some(arg => ['--help', '-h'].includes(arg));
101
+ if (wantsHelp) {
102
+ showHelp(command);
103
+ return;
104
+ }
105
+ if (Object.hasOwn(POSITIONAL_COUNTS, command)
106
+ && (args.length !== POSITIONAL_COUNTS[command] || args.some(arg => arg.startsWith('-')))) {
107
+ throw new Error(`Usage: npx ai-profile-switch@latest ${USAGE[command]}`);
108
+ }
109
+ await COMMANDS[command](args);
110
+ } catch (err) {
111
+ error(err.message);
112
+ process.exitCode = 1;
113
+ }
114
+ }