claude-switch-profile 1.0.2 → 1.4.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.
- package/CHANGELOG.md +69 -0
- package/README.md +205 -55
- package/bin/csp.js +52 -3
- package/package.json +1 -1
- package/src/commands/create.js +76 -18
- package/src/commands/current.js +8 -3
- package/src/commands/deactivate.js +22 -0
- package/src/commands/delete.js +19 -7
- package/src/commands/diff.js +19 -9
- package/src/commands/export.js +30 -5
- package/src/commands/import.js +11 -2
- package/src/commands/init.js +10 -8
- package/src/commands/launch.js +216 -0
- package/src/commands/list.js +2 -1
- package/src/commands/save.js +11 -4
- package/src/commands/select.js +126 -0
- package/src/commands/status.js +48 -0
- package/src/commands/toggle.js +25 -0
- package/src/commands/uninstall.js +99 -0
- package/src/commands/use.js +67 -58
- package/src/constants.js +43 -16
- package/src/file-operations.js +80 -1
- package/src/item-manager.js +155 -0
- package/src/launch-effective-env-resolver.js +192 -0
- package/src/platform.js +24 -0
- package/src/profile-store.js +139 -7
- package/src/profile-validator.js +7 -9
- package/src/runtime-instance-manager.js +190 -0
- package/src/safety.js +72 -35
- package/src/symlink-manager.js +0 -69
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { readProfiles, getActive } from '../profile-store.js';
|
|
4
|
+
import { PROFILES_DIR } from '../constants.js';
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { launchCommand } from './launch.js';
|
|
7
|
+
import { info, error } from '../output-helpers.js';
|
|
8
|
+
|
|
9
|
+
const renderMenu = (names, profiles, active, selected) => {
|
|
10
|
+
// Move cursor up to overwrite previous render (except first render)
|
|
11
|
+
const lines = [];
|
|
12
|
+
for (let i = 0; i < names.length; i++) {
|
|
13
|
+
const name = names[i];
|
|
14
|
+
const meta = profiles[name];
|
|
15
|
+
const isActive = name === active;
|
|
16
|
+
const isSelected = i === selected;
|
|
17
|
+
|
|
18
|
+
const cursor = isSelected ? chalk.cyan('❯ ') : ' ';
|
|
19
|
+
const label = isSelected ? chalk.cyan.bold(name) : isActive ? chalk.green(name) : name;
|
|
20
|
+
const activeTag = isActive ? chalk.green(' (active)') : '';
|
|
21
|
+
const desc = meta.description ? chalk.dim(` — ${meta.description}`) : '';
|
|
22
|
+
lines.push(`${cursor}${label}${activeTag}${desc}`);
|
|
23
|
+
}
|
|
24
|
+
return lines;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const selectCommand = async () => {
|
|
28
|
+
if (!existsSync(PROFILES_DIR)) {
|
|
29
|
+
error('CSP not initialized. Run "csp init" to start.');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const profiles = readProfiles();
|
|
34
|
+
const active = getActive();
|
|
35
|
+
const names = Object.keys(profiles);
|
|
36
|
+
|
|
37
|
+
if (names.length === 0) {
|
|
38
|
+
info('No profiles found. Run "csp create <name>" to create one.');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (names.length === 1) {
|
|
43
|
+
info(`Only one profile: "${names[0]}". Nothing to switch to.`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Non-TTY: fall back to list
|
|
48
|
+
if (!process.stdin.isTTY) {
|
|
49
|
+
info('Non-interactive terminal. Use "csp launch <name>" or "csp use <name>" directly.');
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let selected = active ? Math.max(names.indexOf(active), 0) : 0;
|
|
54
|
+
|
|
55
|
+
console.log('');
|
|
56
|
+
console.log(chalk.bold('Select profile to launch:'));
|
|
57
|
+
console.log(chalk.dim('(↑/↓ navigate, Enter select, Esc cancel)'));
|
|
58
|
+
console.log('');
|
|
59
|
+
|
|
60
|
+
// Initial render
|
|
61
|
+
const menuLines = renderMenu(names, profiles, active, selected);
|
|
62
|
+
for (const line of menuLines) {
|
|
63
|
+
process.stdout.write(line + '\n');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
const rl = createInterface({ input: process.stdin });
|
|
68
|
+
|
|
69
|
+
if (process.stdin.setRawMode) {
|
|
70
|
+
process.stdin.setRawMode(true);
|
|
71
|
+
}
|
|
72
|
+
process.stdin.resume();
|
|
73
|
+
|
|
74
|
+
const redraw = () => {
|
|
75
|
+
// Move up N lines and clear
|
|
76
|
+
process.stdout.write(`\x1b[${names.length}A`);
|
|
77
|
+
const lines = renderMenu(names, profiles, active, selected);
|
|
78
|
+
for (const line of lines) {
|
|
79
|
+
process.stdout.write(`\x1b[2K${line}\n`);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const cleanup = () => {
|
|
84
|
+
if (process.stdin.setRawMode) {
|
|
85
|
+
process.stdin.setRawMode(false);
|
|
86
|
+
}
|
|
87
|
+
rl.close();
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
process.stdin.on('data', async (data) => {
|
|
91
|
+
const key = data.toString();
|
|
92
|
+
|
|
93
|
+
// Escape or Ctrl+C
|
|
94
|
+
if (key === '\x1b' || key === '\x03') {
|
|
95
|
+
cleanup();
|
|
96
|
+
console.log(chalk.dim('\nCancelled.'));
|
|
97
|
+
resolve();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Up arrow
|
|
102
|
+
if (key === '\x1b[A' || key === 'k') {
|
|
103
|
+
selected = (selected - 1 + names.length) % names.length;
|
|
104
|
+
redraw();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Down arrow
|
|
109
|
+
if (key === '\x1b[B' || key === 'j') {
|
|
110
|
+
selected = (selected + 1) % names.length;
|
|
111
|
+
redraw();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Enter
|
|
116
|
+
if (key === '\r' || key === '\n') {
|
|
117
|
+
cleanup();
|
|
118
|
+
const chosen = names[selected];
|
|
119
|
+
console.log('');
|
|
120
|
+
await launchCommand(chosen, [], {});
|
|
121
|
+
resolve();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { readProfiles, getActive } from '../profile-store.js';
|
|
3
|
+
import { isClaudeRunning } from '../safety.js';
|
|
4
|
+
import { PROFILES_DIR } from '../constants.js';
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
|
|
7
|
+
export const statusCommand = () => {
|
|
8
|
+
if (!existsSync(PROFILES_DIR)) {
|
|
9
|
+
console.log(chalk.yellow('CSP not initialized. Run "csp init" to start.'));
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const profiles = readProfiles();
|
|
14
|
+
const active = getActive();
|
|
15
|
+
const names = Object.keys(profiles);
|
|
16
|
+
const claudeRunning = isClaudeRunning();
|
|
17
|
+
|
|
18
|
+
console.log('');
|
|
19
|
+
console.log(chalk.bold('Claude Switch Profile'));
|
|
20
|
+
console.log(chalk.dim('─'.repeat(40)));
|
|
21
|
+
|
|
22
|
+
// Active profile
|
|
23
|
+
if (active) {
|
|
24
|
+
console.log(` ${chalk.bold('Active:')} ${chalk.green(active)}`);
|
|
25
|
+
} else {
|
|
26
|
+
console.log(` ${chalk.bold('Active:')} ${chalk.dim('none')}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Profile count
|
|
30
|
+
console.log(` ${chalk.bold('Profiles:')} ${names.length} (${names.join(', ')})`);
|
|
31
|
+
|
|
32
|
+
// Last launch info
|
|
33
|
+
const launched = names
|
|
34
|
+
.filter((n) => profiles[n].lastLaunchAt)
|
|
35
|
+
.sort((a, b) => (profiles[b].lastLaunchAt || '').localeCompare(profiles[a].lastLaunchAt || ''));
|
|
36
|
+
if (launched.length > 0) {
|
|
37
|
+
const last = launched[0];
|
|
38
|
+
const ts = profiles[last].lastLaunchAt.split('T')[0];
|
|
39
|
+
console.log(` ${chalk.bold('Last launch:')} ${last} (${ts})`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Claude running
|
|
43
|
+
const runIcon = claudeRunning ? chalk.yellow('⚠ running') : chalk.green('not running');
|
|
44
|
+
console.log(` ${chalk.bold('Claude:')} ${runIcon}`);
|
|
45
|
+
|
|
46
|
+
console.log(chalk.dim('─'.repeat(40)));
|
|
47
|
+
console.log('');
|
|
48
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { getPrevious, getActive, profileExists } from '../profile-store.js';
|
|
2
|
+
import { launchCommand } from './launch.js';
|
|
3
|
+
import { error, info } from '../output-helpers.js';
|
|
4
|
+
|
|
5
|
+
export const toggleCommand = async () => {
|
|
6
|
+
const previous = getPrevious();
|
|
7
|
+
const active = getActive();
|
|
8
|
+
|
|
9
|
+
if (!previous) {
|
|
10
|
+
error('No previous profile to toggle to. Switch profiles first.');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (previous === active) {
|
|
15
|
+
info(`Already on "${active}" — no previous profile to toggle to.`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!profileExists(previous)) {
|
|
20
|
+
error(`Previous profile "${previous}" no longer exists.`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
await launchCommand(previous, [], {});
|
|
25
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { existsSync, rmSync } from 'node:fs';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import { getActive, profileExists, getProfileDir } from '../profile-store.js';
|
|
4
|
+
import { removeItems, restoreItems } from '../item-manager.js';
|
|
5
|
+
import { removeFiles, restoreFiles } from '../file-operations.js';
|
|
6
|
+
import { withLock, createBackup, warnIfClaudeRunning } from '../safety.js';
|
|
7
|
+
import { PROFILES_DIR, DEFAULT_PROFILE } from '../constants.js';
|
|
8
|
+
import { success, error, info, warn } from '../output-helpers.js';
|
|
9
|
+
|
|
10
|
+
const confirm = (question) => {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
13
|
+
rl.question(question, (answer) => {
|
|
14
|
+
rl.close();
|
|
15
|
+
resolve(answer.toLowerCase().startsWith('y'));
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const uninstallCommand = async (options) => {
|
|
21
|
+
const active = getActive();
|
|
22
|
+
|
|
23
|
+
if (!existsSync(PROFILES_DIR)) {
|
|
24
|
+
info('No profiles directory found. CSP is not initialized.');
|
|
25
|
+
info('To remove the CLI: npm uninstall -g claude-switch-profile');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Show what will happen
|
|
30
|
+
console.log('');
|
|
31
|
+
info('This will:');
|
|
32
|
+
if (options.profile && profileExists(options.profile)) {
|
|
33
|
+
info(` 1. Restore profile "${options.profile}" to ~/.claude`);
|
|
34
|
+
} else if (active && active !== DEFAULT_PROFILE && profileExists(active)) {
|
|
35
|
+
info(` 1. Restore active profile "${active}" to ~/.claude (use --profile <name> to choose)`);
|
|
36
|
+
} else if (active === DEFAULT_PROFILE) {
|
|
37
|
+
info(' 1. Default profile — ~/.claude already in correct state');
|
|
38
|
+
} else {
|
|
39
|
+
warn(' 1. No profile to restore (managed items will be removed)');
|
|
40
|
+
}
|
|
41
|
+
info(` 2. Remove all profiles: ${PROFILES_DIR}`);
|
|
42
|
+
info(' 3. You can then run: npm uninstall -g claude-switch-profile');
|
|
43
|
+
console.log('');
|
|
44
|
+
|
|
45
|
+
if (!options.force) {
|
|
46
|
+
const confirmed = await confirm('Uninstall CSP and remove all profiles? This cannot be undone. (y/N) ');
|
|
47
|
+
if (!confirmed) {
|
|
48
|
+
warn('Cancelled.');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
warnIfClaudeRunning();
|
|
54
|
+
|
|
55
|
+
await withLock(async () => {
|
|
56
|
+
// 1. Create final backup before uninstall
|
|
57
|
+
try {
|
|
58
|
+
const backupPath = createBackup();
|
|
59
|
+
info(`Final backup created at ${backupPath}`);
|
|
60
|
+
} catch {
|
|
61
|
+
// Non-critical — profiles dir may be empty
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 2. Determine which profile to restore
|
|
65
|
+
const restoreProfile = options.profile || active;
|
|
66
|
+
|
|
67
|
+
// 3. Remove current managed items from ~/.claude (skip for default — items live there)
|
|
68
|
+
if (restoreProfile !== DEFAULT_PROFILE) {
|
|
69
|
+
removeItems();
|
|
70
|
+
removeFiles();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 4. Restore the chosen profile's config
|
|
74
|
+
if (restoreProfile === DEFAULT_PROFILE) {
|
|
75
|
+
info('Default profile — ~/.claude already in correct state.');
|
|
76
|
+
} else if (restoreProfile && profileExists(restoreProfile)) {
|
|
77
|
+
const profileDir = getProfileDir(restoreProfile);
|
|
78
|
+
// If restoring the active profile: items already in ~/.claude (moved there on switch)
|
|
79
|
+
// If restoring a different profile: need to restore from profileDir
|
|
80
|
+
if (restoreProfile !== active) {
|
|
81
|
+
restoreItems(profileDir);
|
|
82
|
+
restoreFiles(profileDir);
|
|
83
|
+
}
|
|
84
|
+
success(`Restored "${restoreProfile}" profile to ~/.claude`);
|
|
85
|
+
} else {
|
|
86
|
+
warn('No profile restored. ~/.claude managed items have been cleared.');
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// 5. Remove profiles directory (after lock is released)
|
|
91
|
+
rmSync(PROFILES_DIR, { recursive: true, force: true });
|
|
92
|
+
success('Removed all profiles and CSP data.');
|
|
93
|
+
|
|
94
|
+
console.log('');
|
|
95
|
+
info('To complete uninstall, run:');
|
|
96
|
+
info(' npm uninstall -g claude-switch-profile');
|
|
97
|
+
info('');
|
|
98
|
+
info('Restart your Claude Code session to apply changes.');
|
|
99
|
+
};
|
package/src/commands/use.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import { getActive, setActive, profileExists, getProfileDir } from '../profile-store.js';
|
|
2
|
-
import {
|
|
3
|
-
import { saveFiles, removeFiles, restoreFiles } from '../file-operations.js';
|
|
4
|
-
import { validateProfile
|
|
5
|
-
import { withLock,
|
|
1
|
+
import { getActive, setActive, profileExists, getProfileDir, getProfileMeta, setPrevious } from '../profile-store.js';
|
|
2
|
+
import { moveItemsToProfile, moveItemsToClaude, saveItems, removeItems } from '../item-manager.js';
|
|
3
|
+
import { saveFiles, removeFiles, restoreFiles, updateSettingsPaths, moveDirsToProfile, moveDirsToClaude } from '../file-operations.js';
|
|
4
|
+
import { validateProfile } from '../profile-validator.js';
|
|
5
|
+
import { withLock, assertClaudeNotRunning } from '../safety.js';
|
|
6
6
|
import { success, error, info, warn } from '../output-helpers.js';
|
|
7
|
-
import {
|
|
8
|
-
import { join } from 'node:path';
|
|
9
|
-
import { SOURCE_FILE } from '../constants.js';
|
|
7
|
+
import { CLAUDE_DIR, DEFAULT_PROFILE } from '../constants.js';
|
|
10
8
|
|
|
11
9
|
export const useCommand = async (name, options) => {
|
|
10
|
+
// Warn about legacy behavior (skip if called internally from launch --legacy-global)
|
|
11
|
+
if (!options.skipClaudeCheck) {
|
|
12
|
+
warn('Note: "csp use" modifies ~/.claude directly (legacy mode).');
|
|
13
|
+
info('Prefer "csp launch <name>" for isolated sessions that never touch ~/.claude.');
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
if (!profileExists(name)) {
|
|
13
17
|
error(`Profile "${name}" does not exist. Run "csp list" to see available profiles.`);
|
|
14
18
|
process.exit(1);
|
|
@@ -21,30 +25,21 @@ export const useCommand = async (name, options) => {
|
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
const profileDir = getProfileDir(name);
|
|
28
|
+
const meta = getProfileMeta(name);
|
|
24
29
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
error(`Profile "${name}" is invalid:`);
|
|
29
|
-
validation.errors.forEach((e) => error(` ${e}`));
|
|
30
|
-
process.exit(1);
|
|
30
|
+
if (meta?.lastLaunchAt) {
|
|
31
|
+
warn(`Profile "${name}" was used in isolated launch mode recently (${meta.lastLaunchAt}).`);
|
|
32
|
+
info('Legacy "csp use" mutates global ~/.claude state.');
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
// Validate
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
targetValidation.errors.forEach((e) => warn(` ${e}`));
|
|
41
|
-
if (!options.force) {
|
|
42
|
-
error('Use --force to switch anyway.');
|
|
43
|
-
process.exit(1);
|
|
44
|
-
}
|
|
35
|
+
// Validate target profile (skip for default — it's a pass-through)
|
|
36
|
+
if (name !== DEFAULT_PROFILE) {
|
|
37
|
+
const validation = validateProfile(profileDir);
|
|
38
|
+
if (!validation.valid) {
|
|
39
|
+
error(`Profile "${name}" is invalid:`);
|
|
40
|
+
validation.errors.forEach((e) => error(` ${e}`));
|
|
41
|
+
process.exit(1);
|
|
45
42
|
}
|
|
46
|
-
} catch {
|
|
47
|
-
// source.json parse error — will be caught during restore
|
|
48
43
|
}
|
|
49
44
|
|
|
50
45
|
if (options.dryRun) {
|
|
@@ -53,50 +48,64 @@ export const useCommand = async (name, options) => {
|
|
|
53
48
|
return;
|
|
54
49
|
}
|
|
55
50
|
|
|
56
|
-
|
|
51
|
+
if (!options.skipClaudeCheck) {
|
|
52
|
+
try {
|
|
53
|
+
assertClaudeNotRunning();
|
|
54
|
+
} catch (err) {
|
|
55
|
+
error(err.message);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
57
59
|
|
|
58
60
|
await withLock(async () => {
|
|
59
|
-
// 1. Save current state
|
|
60
|
-
if (active && profileExists(active) && options.save !== false) {
|
|
61
|
+
// 1. Save current state (skip if from=default — ~/.claude IS default)
|
|
62
|
+
if (active && active !== DEFAULT_PROFILE && profileExists(active) && options.save !== false) {
|
|
61
63
|
const activeDir = getProfileDir(active);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
if (name === DEFAULT_PROFILE) {
|
|
65
|
+
saveItems(activeDir);
|
|
66
|
+
saveFiles(activeDir);
|
|
67
|
+
} else {
|
|
68
|
+
moveItemsToProfile(activeDir);
|
|
69
|
+
saveFiles(activeDir);
|
|
70
|
+
moveDirsToProfile(activeDir);
|
|
71
|
+
}
|
|
72
|
+
updateSettingsPaths(activeDir, 'save');
|
|
64
73
|
info(`Saved current state to "${active}"`);
|
|
65
74
|
}
|
|
66
75
|
|
|
67
|
-
// 2.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
// 2. Remove leftovers only when switching to non-default
|
|
77
|
+
if (name !== DEFAULT_PROFILE) {
|
|
78
|
+
if (active !== DEFAULT_PROFILE) {
|
|
79
|
+
removeItems();
|
|
80
|
+
}
|
|
81
|
+
removeFiles();
|
|
82
|
+
}
|
|
74
83
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
restoreFiles(profileDir);
|
|
78
|
-
} catch (err) {
|
|
79
|
-
// Rollback: restore from backup
|
|
80
|
-
warn('Switch failed — rolling back from backup...');
|
|
84
|
+
// 3. Restore target (skip if to=default — ~/.claude already correct)
|
|
85
|
+
if (name !== DEFAULT_PROFILE) {
|
|
81
86
|
try {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
error(`Rollback also failed: ${rollbackErr.message}`);
|
|
91
|
-
error(`Manual recovery: restore from ${backupPath}`);
|
|
87
|
+
moveItemsToClaude(profileDir);
|
|
88
|
+
restoreFiles(profileDir);
|
|
89
|
+
moveDirsToClaude(profileDir);
|
|
90
|
+
updateSettingsPaths(CLAUDE_DIR, 'restore', profileDir);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
warn(`Switch failed: ${err.message}`);
|
|
93
|
+
error('Manual recovery: re-run "csp use <profile>" or restore from profile directory.');
|
|
94
|
+
throw err;
|
|
92
95
|
}
|
|
93
|
-
throw err;
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
// Track previous profile for toggle
|
|
99
|
+
if (active) setPrevious(active);
|
|
100
|
+
|
|
96
101
|
// 4. Update active marker
|
|
97
102
|
setActive(name);
|
|
98
103
|
|
|
99
104
|
success(`Switched to profile "${name}"`);
|
|
100
|
-
|
|
105
|
+
if (name === DEFAULT_PROFILE) {
|
|
106
|
+
info('Using ~/.claude directly (default profile).');
|
|
107
|
+
} else {
|
|
108
|
+
info('Restart your Claude Code session to apply changes.');
|
|
109
|
+
}
|
|
101
110
|
});
|
|
102
111
|
};
|
package/src/constants.js
CHANGED
|
@@ -12,20 +12,28 @@ export const PROFILES_META = 'profiles.json';
|
|
|
12
12
|
export const SOURCE_FILE = 'source.json';
|
|
13
13
|
export const LOCK_FILE = '.lock';
|
|
14
14
|
export const BACKUP_DIR = '.backup';
|
|
15
|
+
export const DEFAULT_PROFILE = 'default';
|
|
16
|
+
export const PROFILES_SCHEMA_VERSION = 2;
|
|
17
|
+
export const RUNTIME_DIR_NAME = '.runtime';
|
|
18
|
+
export const RUNTIMES_DIR = join(PROFILES_DIR, RUNTIME_DIR_NAME);
|
|
19
|
+
export const LAUNCH_CONFIG_ENV = 'CLAUDE_CONFIG_DIR';
|
|
20
|
+
export const LAUNCH_ANTHROPIC_ENV_KEYS = ['ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL', 'ANTHROPIC_MODEL'];
|
|
15
21
|
|
|
16
|
-
// Items managed via symlinks —
|
|
17
|
-
export const
|
|
22
|
+
// Items managed via copy (formerly symlinks — now always copied)
|
|
23
|
+
export const MANAGED_ITEMS = [
|
|
18
24
|
'CLAUDE.md',
|
|
19
25
|
'rules',
|
|
20
26
|
'agents',
|
|
21
27
|
'skills',
|
|
22
28
|
'hooks',
|
|
23
29
|
'statusline.cjs',
|
|
30
|
+
'statusline.sh',
|
|
31
|
+
'statusline.ps1',
|
|
24
32
|
'.luna.json',
|
|
25
33
|
];
|
|
26
34
|
|
|
27
|
-
// Directory-type
|
|
28
|
-
export const
|
|
35
|
+
// Directory-type managed items (auto-created in new profiles)
|
|
36
|
+
export const MANAGED_DIRS = ['rules', 'agents', 'skills', 'hooks'];
|
|
29
37
|
|
|
30
38
|
// Mutable files managed via copy
|
|
31
39
|
export const COPY_ITEMS = [
|
|
@@ -33,34 +41,53 @@ export const COPY_ITEMS = [
|
|
|
33
41
|
'.env',
|
|
34
42
|
'.ck.json',
|
|
35
43
|
'.ckignore',
|
|
44
|
+
'.mcp.json',
|
|
45
|
+
'.mcp.json.example',
|
|
46
|
+
'.env.example',
|
|
47
|
+
'.gitignore',
|
|
36
48
|
];
|
|
37
49
|
|
|
38
50
|
// Directories managed via copy
|
|
39
51
|
export const COPY_DIRS = [
|
|
40
52
|
'commands',
|
|
41
53
|
'plugins',
|
|
54
|
+
'workflows',
|
|
55
|
+
'scripts',
|
|
56
|
+
'output-styles',
|
|
57
|
+
'schemas',
|
|
42
58
|
];
|
|
43
59
|
|
|
44
|
-
//
|
|
45
|
-
export const
|
|
60
|
+
// Items to NEVER clone (runtime/cache/tracking)
|
|
61
|
+
export const NEVER_CLONE = [
|
|
46
62
|
'.credentials.json',
|
|
47
63
|
'projects',
|
|
48
|
-
'
|
|
64
|
+
'sessions',
|
|
65
|
+
'session-env',
|
|
66
|
+
'ide',
|
|
49
67
|
'cache',
|
|
50
|
-
'debug',
|
|
51
|
-
'telemetry',
|
|
52
|
-
'shell-snapshots',
|
|
53
68
|
'paste-cache',
|
|
54
|
-
'
|
|
55
|
-
'
|
|
56
|
-
'
|
|
69
|
+
'downloads',
|
|
70
|
+
'stats-cache.json',
|
|
71
|
+
'active-plan',
|
|
72
|
+
'history.jsonl',
|
|
73
|
+
'metadata.json',
|
|
74
|
+
'telemetry',
|
|
75
|
+
'debug',
|
|
76
|
+
'statsig',
|
|
77
|
+
'backups',
|
|
78
|
+
'command-archive',
|
|
79
|
+
'commands-archived',
|
|
57
80
|
'todos',
|
|
58
81
|
'tasks',
|
|
59
82
|
'teams',
|
|
60
83
|
'agent-memory',
|
|
61
|
-
'history.jsonl',
|
|
62
84
|
'plans',
|
|
85
|
+
'file-history',
|
|
86
|
+
'shell-snapshots',
|
|
63
87
|
];
|
|
64
88
|
|
|
65
|
-
//
|
|
66
|
-
export const
|
|
89
|
+
// Never touch these — runtime/session data (same scope as NEVER_CLONE)
|
|
90
|
+
export const NEVER_TOUCH = [...NEVER_CLONE];
|
|
91
|
+
|
|
92
|
+
// All managed items (managed + copies + copy dirs)
|
|
93
|
+
export const ALL_MANAGED = [...MANAGED_ITEMS, ...COPY_ITEMS, ...COPY_DIRS];
|
package/src/file-operations.js
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
|
-
import { existsSync, copyFileSync, cpSync, unlinkSync, rmSync, mkdirSync } from 'node:fs';
|
|
1
|
+
import { existsSync, copyFileSync, cpSync, unlinkSync, rmSync, mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { CLAUDE_DIR, COPY_ITEMS, COPY_DIRS } from './constants.js';
|
|
4
4
|
|
|
5
|
+
// Rename-based dir move with EXDEV fallback
|
|
6
|
+
const moveDir = (src, dest) => {
|
|
7
|
+
if (existsSync(dest)) rmSync(dest, { recursive: true, force: true });
|
|
8
|
+
try {
|
|
9
|
+
renameSync(src, dest);
|
|
10
|
+
} catch (err) {
|
|
11
|
+
if (err.code === 'EXDEV') {
|
|
12
|
+
cpSync(src, dest, { recursive: true });
|
|
13
|
+
rmSync(src, { recursive: true, force: true });
|
|
14
|
+
} else throw err;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
5
18
|
// Copy COPY_ITEMS files + COPY_DIRS dirs from ~/.claude to profileDir
|
|
6
19
|
export const saveFiles = (profileDir) => {
|
|
7
20
|
mkdirSync(profileDir, { recursive: true });
|
|
@@ -62,3 +75,69 @@ export const removeFiles = () => {
|
|
|
62
75
|
}
|
|
63
76
|
}
|
|
64
77
|
};
|
|
78
|
+
|
|
79
|
+
// Move COPY_DIRS from ~/.claude → profileDir (destructive, for use command)
|
|
80
|
+
// COPY_ITEMS always copied (tiny files, not worth move complexity)
|
|
81
|
+
export const moveDirsToProfile = (profileDir) => {
|
|
82
|
+
mkdirSync(profileDir, { recursive: true });
|
|
83
|
+
for (const dir of COPY_DIRS) {
|
|
84
|
+
const src = join(CLAUDE_DIR, dir);
|
|
85
|
+
if (existsSync(src)) {
|
|
86
|
+
moveDir(src, join(profileDir, dir));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Move COPY_DIRS from profileDir → ~/.claude (destructive, for use command)
|
|
92
|
+
export const moveDirsToClaude = (profileDir) => {
|
|
93
|
+
for (const dir of COPY_DIRS) {
|
|
94
|
+
const src = join(profileDir, dir);
|
|
95
|
+
if (existsSync(src)) {
|
|
96
|
+
moveDir(src, join(CLAUDE_DIR, dir));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Update absolute paths in settings.json.
|
|
103
|
+
* Direction: 'save' replaces CLAUDE_DIR → profileDir, 'restore' replaces profileDir → CLAUDE_DIR.
|
|
104
|
+
* @param {string} targetDir - The directory containing settings.json to update
|
|
105
|
+
* @param {'save'|'restore'} direction - Direction of path replacement
|
|
106
|
+
* @param {string} [profileDir] - Profile directory (required for 'restore' mode)
|
|
107
|
+
*/
|
|
108
|
+
export const updateSettingsPaths = (targetDir, direction = 'save', profileDir = null) => {
|
|
109
|
+
const settingsPath = join(targetDir, 'settings.json');
|
|
110
|
+
if (!existsSync(settingsPath)) return;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const raw = readFileSync(settingsPath, 'utf-8');
|
|
114
|
+
let updated = raw;
|
|
115
|
+
|
|
116
|
+
let fromPath, toPath;
|
|
117
|
+
if (direction === 'save') {
|
|
118
|
+
fromPath = CLAUDE_DIR;
|
|
119
|
+
toPath = targetDir;
|
|
120
|
+
} else {
|
|
121
|
+
// restore: replace profileDir refs with CLAUDE_DIR
|
|
122
|
+
fromPath = profileDir || targetDir;
|
|
123
|
+
toPath = CLAUDE_DIR;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Handle both backslash (Windows JSON-escaped) and forward-slash variants
|
|
127
|
+
const fromEscaped = fromPath.replaceAll('\\', '\\\\');
|
|
128
|
+
const toEscaped = toPath.replaceAll('\\', '\\\\');
|
|
129
|
+
const fromFwd = fromPath.replaceAll('\\', '/');
|
|
130
|
+
const toFwd = toPath.replaceAll('\\', '/');
|
|
131
|
+
|
|
132
|
+
updated = updated.replaceAll(fromEscaped, toEscaped);
|
|
133
|
+
updated = updated.replaceAll(fromFwd, toFwd);
|
|
134
|
+
// Also handle raw backslash paths (single backslash in non-JSON context)
|
|
135
|
+
if (fromPath !== fromEscaped) {
|
|
136
|
+
updated = updated.replaceAll(fromPath, toPath);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (updated !== raw) {
|
|
140
|
+
writeFileSync(settingsPath, updated);
|
|
141
|
+
}
|
|
142
|
+
} catch { /* parse error — skip */ }
|
|
143
|
+
};
|