claude-switch-profile 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cocoindex_code/cocoindex.db/mdb/data.mdb +0 -0
- package/.cocoindex_code/cocoindex.db/mdb/lock.mdb +0 -0
- package/.cocoindex_code/settings.yml +41 -0
- package/.cocoindex_code/target_sqlite.db +0 -0
- package/CHANGELOG.md +19 -0
- package/README.md +112 -50
- package/package.json +1 -1
- package/src/commands/create.js +9 -26
- package/src/commands/current.js +13 -2
- package/src/commands/deactivate.js +6 -8
- package/src/commands/diff.js +6 -4
- package/src/commands/export.js +7 -4
- package/src/commands/import.js +47 -2
- package/src/commands/init.js +29 -9
- package/src/commands/launch.js +10 -5
- package/src/commands/save.js +8 -4
- package/src/commands/uninstall.js +19 -22
- package/src/commands/use.js +55 -50
- package/src/constants.js +1 -1
- package/src/file-operations.js +13 -11
- package/src/item-manager.js +10 -16
- package/src/profile-store.js +37 -14
- package/src/runtime-instance-manager.js +29 -29
package/src/commands/import.js
CHANGED
|
@@ -1,11 +1,47 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import { existsSync, mkdirSync } from 'node:fs';
|
|
3
|
-
import { resolve, basename } from 'node:path';
|
|
2
|
+
import { existsSync, mkdirSync, lstatSync, readlinkSync, readdirSync, rmSync } from 'node:fs';
|
|
3
|
+
import { resolve, basename, dirname, join } from 'node:path';
|
|
4
4
|
import { addProfile, profileExists, getProfileDir, ensureProfilesDir } from '../profile-store.js';
|
|
5
5
|
import { isWindows } from '../platform.js';
|
|
6
6
|
import { validateProfile } from '../profile-validator.js';
|
|
7
|
+
import { MANAGED_ITEMS, COPY_ITEMS, COPY_DIRS } from '../constants.js';
|
|
7
8
|
import { success, error, warn } from '../output-helpers.js';
|
|
8
9
|
|
|
10
|
+
const IMPORT_SAFE_ITEMS = [...new Set([...MANAGED_ITEMS, ...COPY_ITEMS, ...COPY_DIRS])];
|
|
11
|
+
|
|
12
|
+
const isWithinDir = (baseDir, targetPath) => {
|
|
13
|
+
const normalizedBase = resolve(baseDir);
|
|
14
|
+
const normalizedTarget = resolve(targetPath);
|
|
15
|
+
return normalizedTarget === normalizedBase || normalizedTarget.startsWith(normalizedBase + '/');
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const findUnsafeManagedSymlink = (profileDir) => {
|
|
19
|
+
const pending = IMPORT_SAFE_ITEMS.map((item) => join(profileDir, item)).filter((itemPath) => existsSync(itemPath));
|
|
20
|
+
|
|
21
|
+
while (pending.length > 0) {
|
|
22
|
+
const currentPath = pending.pop();
|
|
23
|
+
const stat = lstatSync(currentPath);
|
|
24
|
+
|
|
25
|
+
if (stat.isSymbolicLink()) {
|
|
26
|
+
const linkTarget = readlinkSync(currentPath);
|
|
27
|
+
const resolvedTarget = resolve(dirname(currentPath), linkTarget);
|
|
28
|
+
if (!isWithinDir(profileDir, resolvedTarget)) {
|
|
29
|
+
return { path: currentPath, target: linkTarget };
|
|
30
|
+
}
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!stat.isDirectory()) continue;
|
|
35
|
+
|
|
36
|
+
const entries = readdirSync(currentPath, { withFileTypes: true });
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
pending.push(join(currentPath, entry.name));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return null;
|
|
43
|
+
};
|
|
44
|
+
|
|
9
45
|
export const importCommand = (file, options) => {
|
|
10
46
|
const filePath = resolve(file);
|
|
11
47
|
if (!existsSync(filePath)) {
|
|
@@ -47,6 +83,15 @@ export const importCommand = (file, options) => {
|
|
|
47
83
|
validation.errors.forEach((e) => warn(` ${e}`));
|
|
48
84
|
}
|
|
49
85
|
|
|
86
|
+
const unsafeSymlink = findUnsafeManagedSymlink(profileDir);
|
|
87
|
+
if (unsafeSymlink) {
|
|
88
|
+
rmSync(profileDir, { recursive: true, force: true });
|
|
89
|
+
error(
|
|
90
|
+
`Imported profile contains an unsafe symlink outside the profile tree: ${unsafeSymlink.path} -> ${unsafeSymlink.target}`,
|
|
91
|
+
);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
50
95
|
addProfile(name, { description: options.description || `Imported from ${basename(file)}` });
|
|
51
96
|
success(`Profile "${name}" imported from ${filePath}`);
|
|
52
97
|
};
|
package/src/commands/init.js
CHANGED
|
@@ -1,23 +1,43 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import {
|
|
3
|
+
ensureProfilesDir,
|
|
4
|
+
ensureDefaultProfileSnapshot,
|
|
5
|
+
getActive,
|
|
6
|
+
readProfiles,
|
|
7
|
+
addProfile,
|
|
8
|
+
setActive,
|
|
9
|
+
getProfileDir,
|
|
10
|
+
} from '../profile-store.js';
|
|
11
|
+
import { success, info, error } from '../output-helpers.js';
|
|
12
|
+
import { PROFILES_DIR, DEFAULT_PROFILE } from '../constants.js';
|
|
5
13
|
|
|
6
14
|
export const initCommand = () => {
|
|
7
15
|
const active = getActive();
|
|
16
|
+
|
|
17
|
+
ensureProfilesDir();
|
|
18
|
+
info('Initializing Claude Switch Profile...');
|
|
19
|
+
|
|
20
|
+
const profiles = readProfiles();
|
|
21
|
+
if (!profiles[DEFAULT_PROFILE]) {
|
|
22
|
+
addProfile(DEFAULT_PROFILE, { description: 'Vanilla Claude defaults', mode: 'legacy' });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
ensureDefaultProfileSnapshot();
|
|
27
|
+
} catch (err) {
|
|
28
|
+
error(err.message);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
8
32
|
if (existsSync(PROFILES_DIR) && active) {
|
|
9
33
|
info(`Already initialized. Active profile: "${active}"`);
|
|
10
34
|
info(`Profiles directory: ${PROFILES_DIR}`);
|
|
11
35
|
return;
|
|
12
36
|
}
|
|
13
37
|
|
|
14
|
-
|
|
15
|
-
info('Initializing Claude Switch Profile...');
|
|
16
|
-
|
|
17
|
-
addProfile('default', { description: 'Vanilla Claude defaults', mode: 'legacy' });
|
|
18
|
-
setActive('default');
|
|
38
|
+
setActive(DEFAULT_PROFILE);
|
|
19
39
|
|
|
20
40
|
success('Initialization complete.');
|
|
21
|
-
info(
|
|
41
|
+
info(`Created physical default profile at ${getProfileDir(DEFAULT_PROFILE)}`);
|
|
22
42
|
info('Run "csp create <name>" to capture your current setup into a new profile.');
|
|
23
43
|
};
|
package/src/commands/launch.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFileSync, spawn } from 'node:child_process';
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { profileExists } from '../profile-store.js';
|
|
4
|
+
import { profileExists, ensureDefaultProfileSnapshot } from '../profile-store.js';
|
|
5
5
|
import { useCommand } from './use.js';
|
|
6
6
|
import { ensureRuntimeInstance } from '../runtime-instance-manager.js';
|
|
7
7
|
import { withRuntimeLock } from '../safety.js';
|
|
@@ -128,6 +128,15 @@ export const stripInheritedLaunchEnv = (env = process.env) => {
|
|
|
128
128
|
};
|
|
129
129
|
|
|
130
130
|
export const launchCommand = async (name, claudeArgs, options = {}) => {
|
|
131
|
+
if (name === DEFAULT_PROFILE) {
|
|
132
|
+
try {
|
|
133
|
+
ensureDefaultProfileSnapshot();
|
|
134
|
+
} catch (err) {
|
|
135
|
+
error(err.message);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
131
140
|
if (!profileExists(name)) {
|
|
132
141
|
error(`Profile "${name}" does not exist. Run "csp list" to see available profiles.`);
|
|
133
142
|
process.exit(1);
|
|
@@ -142,12 +151,8 @@ export const launchCommand = async (name, claudeArgs, options = {}) => {
|
|
|
142
151
|
let launchEnv = { ...process.env };
|
|
143
152
|
|
|
144
153
|
if (options.legacyGlobal || legacyFromArgs) {
|
|
145
|
-
// Legacy path: keep historical behavior for compatibility.
|
|
146
154
|
await useCommand(name, { save: true, skipClaudeCheck: true });
|
|
147
155
|
info(`Launching legacy/global mode: claude ${args.join(' ')}`.trim());
|
|
148
|
-
} else if (name === DEFAULT_PROFILE) {
|
|
149
|
-
// Default profile: launch Claude using ~/.claude natively — no runtime override
|
|
150
|
-
info(`Launching with default profile (using ~/.claude directly): claude ${args.join(' ')}`.trim());
|
|
151
156
|
} else {
|
|
152
157
|
const runtimeDir = await withRuntimeLock(name, async () => ensureRuntimeInstance(name));
|
|
153
158
|
const { profileSettingsEnv, profileDotEnvEnv } = readProfileLaunchEnvSources(runtimeDir);
|
package/src/commands/save.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { getActive, profileExists, getProfileDir } from '../profile-store.js';
|
|
1
|
+
import { getActive, profileExists, getProfileDir, ensureDefaultProfileSnapshot } from '../profile-store.js';
|
|
2
2
|
import { saveItems } from '../item-manager.js';
|
|
3
3
|
import { saveFiles, updateSettingsPaths } from '../file-operations.js';
|
|
4
|
-
import { success, error
|
|
4
|
+
import { success, error } from '../output-helpers.js';
|
|
5
5
|
import { DEFAULT_PROFILE } from '../constants.js';
|
|
6
6
|
|
|
7
7
|
export const saveCommand = () => {
|
|
@@ -12,8 +12,12 @@ export const saveCommand = () => {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
if (active === DEFAULT_PROFILE) {
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
try {
|
|
16
|
+
ensureDefaultProfileSnapshot();
|
|
17
|
+
} catch (err) {
|
|
18
|
+
error(err.message);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
if (!profileExists(active)) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, rmSync } from 'node:fs';
|
|
2
2
|
import { createInterface } from 'node:readline';
|
|
3
|
-
import { getActive, profileExists, getProfileDir } from '../profile-store.js';
|
|
3
|
+
import { getActive, profileExists, getProfileDir, ensureDefaultProfileSnapshot } from '../profile-store.js';
|
|
4
4
|
import { removeItems, restoreItems } from '../item-manager.js';
|
|
5
5
|
import { removeFiles, restoreFiles } from '../file-operations.js';
|
|
6
6
|
import { withLock, createBackup, warnIfClaudeRunning } from '../safety.js';
|
|
@@ -26,15 +26,23 @@ export const uninstallCommand = async (options) => {
|
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
const restoreProfile = options.profile || active;
|
|
30
|
+
if (restoreProfile === DEFAULT_PROFILE) {
|
|
31
|
+
try {
|
|
32
|
+
ensureDefaultProfileSnapshot();
|
|
33
|
+
} catch (err) {
|
|
34
|
+
error(err.message);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
// Show what will happen
|
|
30
40
|
console.log('');
|
|
31
41
|
info('This will:');
|
|
32
42
|
if (options.profile && profileExists(options.profile)) {
|
|
33
43
|
info(` 1. Restore profile "${options.profile}" to ~/.claude`);
|
|
34
|
-
} else if (active &&
|
|
44
|
+
} else if (active && profileExists(active)) {
|
|
35
45
|
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
46
|
} else {
|
|
39
47
|
warn(' 1. No profile to restore (managed items will be removed)');
|
|
40
48
|
}
|
|
@@ -61,26 +69,15 @@ export const uninstallCommand = async (options) => {
|
|
|
61
69
|
// Non-critical — profiles dir may be empty
|
|
62
70
|
}
|
|
63
71
|
|
|
64
|
-
// 2.
|
|
65
|
-
|
|
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
|
+
// 2. Remove current managed items from ~/.claude
|
|
73
|
+
removeItems();
|
|
74
|
+
removeFiles();
|
|
72
75
|
|
|
73
|
-
//
|
|
74
|
-
if (restoreProfile
|
|
75
|
-
info('Default profile — ~/.claude already in correct state.');
|
|
76
|
-
} else if (restoreProfile && profileExists(restoreProfile)) {
|
|
76
|
+
// 3. Restore the chosen profile's config
|
|
77
|
+
if (restoreProfile && profileExists(restoreProfile)) {
|
|
77
78
|
const profileDir = getProfileDir(restoreProfile);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (restoreProfile !== active) {
|
|
81
|
-
restoreItems(profileDir);
|
|
82
|
-
restoreFiles(profileDir);
|
|
83
|
-
}
|
|
79
|
+
restoreItems(profileDir);
|
|
80
|
+
restoreFiles(profileDir);
|
|
84
81
|
success(`Restored "${restoreProfile}" profile to ~/.claude`);
|
|
85
82
|
} else {
|
|
86
83
|
warn('No profile restored. ~/.claude managed items have been cleared.');
|
package/src/commands/use.js
CHANGED
|
@@ -1,24 +1,53 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
getActive,
|
|
3
|
+
setActive,
|
|
4
|
+
profileExists,
|
|
5
|
+
getProfileDir,
|
|
6
|
+
getProfileMeta,
|
|
7
|
+
setPrevious,
|
|
8
|
+
ensureDefaultProfileSnapshot,
|
|
9
|
+
} from '../profile-store.js';
|
|
10
|
+
import { moveItemsToProfile, moveItemsToClaude, removeItems } from '../item-manager.js';
|
|
11
|
+
import {
|
|
12
|
+
saveFiles,
|
|
13
|
+
removeFiles,
|
|
14
|
+
restoreFiles,
|
|
15
|
+
updateSettingsPaths,
|
|
16
|
+
moveDirsToProfile,
|
|
17
|
+
moveDirsToClaude,
|
|
18
|
+
} from '../file-operations.js';
|
|
4
19
|
import { validateProfile } from '../profile-validator.js';
|
|
5
20
|
import { withLock, assertClaudeNotRunning } from '../safety.js';
|
|
6
21
|
import { success, error, info, warn } from '../output-helpers.js';
|
|
7
22
|
import { CLAUDE_DIR, DEFAULT_PROFILE } from '../constants.js';
|
|
8
23
|
|
|
9
|
-
|
|
10
|
-
|
|
24
|
+
const ensureDefaultProfileIfNeeded = (name) => {
|
|
25
|
+
if (name !== DEFAULT_PROFILE) return;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
ensureDefaultProfileSnapshot();
|
|
29
|
+
} catch (err) {
|
|
30
|
+
error(err.message);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const useCommand = async (name, options = {}) => {
|
|
11
36
|
if (!options.skipClaudeCheck) {
|
|
12
37
|
warn('Note: "csp use" modifies ~/.claude directly (legacy mode).');
|
|
13
38
|
info('Prefer "csp launch <name>" for isolated sessions that never touch ~/.claude.');
|
|
14
39
|
}
|
|
15
40
|
|
|
41
|
+
ensureDefaultProfileIfNeeded(name);
|
|
42
|
+
|
|
16
43
|
if (!profileExists(name)) {
|
|
17
44
|
error(`Profile "${name}" does not exist. Run "csp list" to see available profiles.`);
|
|
18
45
|
process.exit(1);
|
|
19
46
|
}
|
|
20
47
|
|
|
21
48
|
const active = getActive();
|
|
49
|
+
ensureDefaultProfileIfNeeded(active);
|
|
50
|
+
|
|
22
51
|
if (active === name) {
|
|
23
52
|
info(`Profile "${name}" is already active.`);
|
|
24
53
|
return;
|
|
@@ -32,14 +61,11 @@ export const useCommand = async (name, options) => {
|
|
|
32
61
|
info('Legacy "csp use" mutates global ~/.claude state.');
|
|
33
62
|
}
|
|
34
63
|
|
|
35
|
-
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
validation.errors.forEach((e) => error(` ${e}`));
|
|
41
|
-
process.exit(1);
|
|
42
|
-
}
|
|
64
|
+
const validation = validateProfile(profileDir);
|
|
65
|
+
if (!validation.valid) {
|
|
66
|
+
error(`Profile "${name}" is invalid:`);
|
|
67
|
+
validation.errors.forEach((validationError) => error(` ${validationError}`));
|
|
68
|
+
process.exit(1);
|
|
43
69
|
}
|
|
44
70
|
|
|
45
71
|
if (options.dryRun) {
|
|
@@ -58,54 +84,33 @@ export const useCommand = async (name, options) => {
|
|
|
58
84
|
}
|
|
59
85
|
|
|
60
86
|
await withLock(async () => {
|
|
61
|
-
|
|
62
|
-
if (active && active !== DEFAULT_PROFILE && profileExists(active) && options.save !== false) {
|
|
87
|
+
if (active && profileExists(active) && options.save !== false) {
|
|
63
88
|
const activeDir = getProfileDir(active);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
} else {
|
|
68
|
-
moveItemsToProfile(activeDir);
|
|
69
|
-
saveFiles(activeDir);
|
|
70
|
-
moveDirsToProfile(activeDir);
|
|
71
|
-
}
|
|
89
|
+
moveItemsToProfile(activeDir);
|
|
90
|
+
saveFiles(activeDir);
|
|
91
|
+
moveDirsToProfile(activeDir);
|
|
72
92
|
updateSettingsPaths(activeDir, 'save');
|
|
73
93
|
info(`Saved current state to "${active}"`);
|
|
74
94
|
}
|
|
75
95
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (active !== DEFAULT_PROFILE) {
|
|
79
|
-
removeItems();
|
|
80
|
-
}
|
|
81
|
-
removeFiles();
|
|
82
|
-
}
|
|
96
|
+
removeItems();
|
|
97
|
+
removeFiles();
|
|
83
98
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
error('Manual recovery: re-run "csp use <profile>" or restore from profile directory.');
|
|
94
|
-
throw err;
|
|
95
|
-
}
|
|
99
|
+
try {
|
|
100
|
+
moveItemsToClaude(profileDir);
|
|
101
|
+
restoreFiles(profileDir);
|
|
102
|
+
moveDirsToClaude(profileDir);
|
|
103
|
+
updateSettingsPaths(CLAUDE_DIR, 'restore', profileDir);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
warn(`Switch failed: ${err.message}`);
|
|
106
|
+
error('Manual recovery: re-run "csp use <profile>" or restore from profile directory.');
|
|
107
|
+
throw err;
|
|
96
108
|
}
|
|
97
109
|
|
|
98
|
-
// Track previous profile for toggle
|
|
99
110
|
if (active) setPrevious(active);
|
|
100
|
-
|
|
101
|
-
// 4. Update active marker
|
|
102
111
|
setActive(name);
|
|
103
112
|
|
|
104
113
|
success(`Switched to profile "${name}"`);
|
|
105
|
-
|
|
106
|
-
info('Using ~/.claude directly (default profile).');
|
|
107
|
-
} else {
|
|
108
|
-
info('Restart your Claude Code session to apply changes.');
|
|
109
|
-
}
|
|
114
|
+
info('Restart your Claude Code session to apply changes.');
|
|
110
115
|
});
|
|
111
116
|
};
|
package/src/constants.js
CHANGED
|
@@ -19,7 +19,7 @@ export const RUNTIMES_DIR = join(PROFILES_DIR, RUNTIME_DIR_NAME);
|
|
|
19
19
|
export const LAUNCH_CONFIG_ENV = 'CLAUDE_CONFIG_DIR';
|
|
20
20
|
export const LAUNCH_ANTHROPIC_ENV_KEYS = ['ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL', 'ANTHROPIC_MODEL'];
|
|
21
21
|
|
|
22
|
-
// Items managed
|
|
22
|
+
// Items managed by snapshot/move flows; file/dir/symlink shape is preserved
|
|
23
23
|
export const MANAGED_ITEMS = [
|
|
24
24
|
'CLAUDE.md',
|
|
25
25
|
'rules',
|
package/src/file-operations.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync,
|
|
2
|
-
import { join } from 'node:path';
|
|
1
|
+
import { existsSync, cpSync, unlinkSync, rmSync, mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
3
|
import { CLAUDE_DIR, COPY_ITEMS, COPY_DIRS } from './constants.js';
|
|
4
4
|
|
|
5
5
|
// Rename-based dir move with EXDEV fallback
|
|
@@ -9,12 +9,18 @@ const moveDir = (src, dest) => {
|
|
|
9
9
|
renameSync(src, dest);
|
|
10
10
|
} catch (err) {
|
|
11
11
|
if (err.code === 'EXDEV') {
|
|
12
|
-
cpSync(src, dest, { recursive: true });
|
|
12
|
+
cpSync(src, dest, { recursive: true, verbatimSymlinks: true });
|
|
13
13
|
rmSync(src, { recursive: true, force: true });
|
|
14
14
|
} else throw err;
|
|
15
15
|
}
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
const copyPathPreservingSymlink = (src, dest) => {
|
|
19
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
20
|
+
rmSync(dest, { recursive: true, force: true });
|
|
21
|
+
cpSync(src, dest, { recursive: true, verbatimSymlinks: true });
|
|
22
|
+
};
|
|
23
|
+
|
|
18
24
|
// Copy COPY_ITEMS files + COPY_DIRS dirs from ~/.claude to profileDir
|
|
19
25
|
export const saveFiles = (profileDir) => {
|
|
20
26
|
mkdirSync(profileDir, { recursive: true });
|
|
@@ -22,16 +28,14 @@ export const saveFiles = (profileDir) => {
|
|
|
22
28
|
for (const item of COPY_ITEMS) {
|
|
23
29
|
const src = join(CLAUDE_DIR, item);
|
|
24
30
|
if (existsSync(src)) {
|
|
25
|
-
|
|
31
|
+
copyPathPreservingSymlink(src, join(profileDir, item));
|
|
26
32
|
}
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
for (const dir of COPY_DIRS) {
|
|
30
36
|
const src = join(CLAUDE_DIR, dir);
|
|
31
37
|
if (existsSync(src)) {
|
|
32
|
-
|
|
33
|
-
rmSync(dest, { recursive: true, force: true });
|
|
34
|
-
cpSync(src, dest, { recursive: true });
|
|
38
|
+
copyPathPreservingSymlink(src, join(profileDir, dir));
|
|
35
39
|
}
|
|
36
40
|
}
|
|
37
41
|
};
|
|
@@ -41,16 +45,14 @@ export const restoreFiles = (profileDir) => {
|
|
|
41
45
|
for (const item of COPY_ITEMS) {
|
|
42
46
|
const src = join(profileDir, item);
|
|
43
47
|
if (existsSync(src)) {
|
|
44
|
-
|
|
48
|
+
copyPathPreservingSymlink(src, join(CLAUDE_DIR, item));
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
for (const dir of COPY_DIRS) {
|
|
49
53
|
const src = join(profileDir, dir);
|
|
50
54
|
if (existsSync(src)) {
|
|
51
|
-
|
|
52
|
-
rmSync(dest, { recursive: true, force: true });
|
|
53
|
-
cpSync(src, dest, { recursive: true });
|
|
55
|
+
copyPathPreservingSymlink(src, join(CLAUDE_DIR, dir));
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
};
|
package/src/item-manager.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync, cpSync, rmSync, mkdirSync, lstatSync,
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, cpSync, rmSync, mkdirSync, lstatSync, renameSync } from 'node:fs';
|
|
2
2
|
import { join, dirname, basename } from 'node:path';
|
|
3
3
|
import { CLAUDE_DIR, MANAGED_ITEMS, SOURCE_FILE } from './constants.js';
|
|
4
4
|
|
|
@@ -13,7 +13,7 @@ const moveItem = (src, dest) => {
|
|
|
13
13
|
renameSync(src, dest);
|
|
14
14
|
} catch (err) {
|
|
15
15
|
if (err.code === 'EXDEV') {
|
|
16
|
-
cpSync(src, dest, { recursive: true, filter: skipHeavyDirs });
|
|
16
|
+
cpSync(src, dest, { recursive: true, filter: skipHeavyDirs, verbatimSymlinks: true });
|
|
17
17
|
rmSync(src, { recursive: true, force: true });
|
|
18
18
|
} else throw err;
|
|
19
19
|
}
|
|
@@ -49,6 +49,12 @@ export const removeItems = () => {
|
|
|
49
49
|
}
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
+
const copyItemPreservingSymlink = (src, dest) => {
|
|
53
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
54
|
+
rmSync(dest, { recursive: true, force: true });
|
|
55
|
+
cpSync(src, dest, { recursive: true, verbatimSymlinks: true });
|
|
56
|
+
};
|
|
57
|
+
|
|
52
58
|
// Copy managed items from ~/.claude into profileDir, write source.json manifest (non-destructive)
|
|
53
59
|
export const copyItems = (profileDir) => {
|
|
54
60
|
const sourceMap = {};
|
|
@@ -58,14 +64,7 @@ export const copyItems = (profileDir) => {
|
|
|
58
64
|
if (!existsSync(itemPath)) continue;
|
|
59
65
|
|
|
60
66
|
const dest = join(profileDir, item);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (stat.isDirectory()) {
|
|
64
|
-
rmSync(dest, { recursive: true, force: true });
|
|
65
|
-
cpSync(itemPath, dest, { recursive: true });
|
|
66
|
-
} else if (stat.isFile()) {
|
|
67
|
-
copyFileSync(itemPath, dest);
|
|
68
|
-
}
|
|
67
|
+
copyItemPreservingSymlink(itemPath, dest);
|
|
69
68
|
sourceMap[item] = dest;
|
|
70
69
|
} catch {
|
|
71
70
|
// Not readable — skip
|
|
@@ -99,12 +98,7 @@ export const restoreItems = (profileDir) => {
|
|
|
99
98
|
// Copy from profile (or external legacy path) into ~/.claude
|
|
100
99
|
if (existsSync(srcPath)) {
|
|
101
100
|
try {
|
|
102
|
-
|
|
103
|
-
if (stat.isDirectory()) {
|
|
104
|
-
cpSync(srcPath, dest, { recursive: true });
|
|
105
|
-
} else if (stat.isFile()) {
|
|
106
|
-
copyFileSync(srcPath, dest);
|
|
107
|
-
}
|
|
101
|
+
copyItemPreservingSymlink(srcPath, dest);
|
|
108
102
|
} catch {
|
|
109
103
|
// skip unreadable
|
|
110
104
|
}
|
package/src/profile-store.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { saveFiles, updateSettingsPaths } from './file-operations.js';
|
|
4
|
+
import { copyItems } from './item-manager.js';
|
|
3
5
|
import {
|
|
4
6
|
PROFILES_DIR,
|
|
5
7
|
ACTIVE_FILE,
|
|
@@ -53,6 +55,15 @@ const normalizeProfiles = (raw) => {
|
|
|
53
55
|
return normalized;
|
|
54
56
|
};
|
|
55
57
|
|
|
58
|
+
const backfillDefaultProfileSnapshot = () => {
|
|
59
|
+
const profileDir = getProfileDir(DEFAULT_PROFILE);
|
|
60
|
+
mkdirSync(profileDir, { recursive: true });
|
|
61
|
+
copyItems(profileDir);
|
|
62
|
+
saveFiles(profileDir);
|
|
63
|
+
updateSettingsPaths(profileDir, 'save');
|
|
64
|
+
return profileDir;
|
|
65
|
+
};
|
|
66
|
+
|
|
56
67
|
export const readProfiles = () => {
|
|
57
68
|
const metaPath = join(PROFILES_DIR, PROFILES_META);
|
|
58
69
|
if (!existsSync(metaPath)) return {};
|
|
@@ -130,12 +141,27 @@ export const removeProfile = (name) => {
|
|
|
130
141
|
writeProfiles(profiles);
|
|
131
142
|
};
|
|
132
143
|
|
|
144
|
+
export const ensureDefaultProfileSnapshot = () => {
|
|
145
|
+
const profileDir = getProfileDir(DEFAULT_PROFILE);
|
|
146
|
+
if (existsSync(profileDir)) return profileDir;
|
|
147
|
+
|
|
148
|
+
const profiles = readProfiles();
|
|
149
|
+
if (!profiles[DEFAULT_PROFILE]) return profileDir;
|
|
150
|
+
|
|
151
|
+
const active = getActive();
|
|
152
|
+
if (active && active !== DEFAULT_PROFILE) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`Default profile snapshot is missing for this legacy install while "${active}" is active. CSP cannot safely recreate "default" from the current ~/.claude state. Switch back to your intended default baseline, then retry.`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return backfillDefaultProfileSnapshot();
|
|
159
|
+
};
|
|
160
|
+
|
|
133
161
|
export const profileExists = (name) => {
|
|
134
|
-
if (name === DEFAULT_PROFILE) return true;
|
|
135
162
|
return existsSync(getProfileDir(name));
|
|
136
163
|
};
|
|
137
164
|
|
|
138
|
-
// Validate profile name — prevent path traversal and injection
|
|
139
165
|
const SAFE_NAME = /^[a-zA-Z0-9_-]+$/;
|
|
140
166
|
export const validateName = (name) => {
|
|
141
167
|
if (!name || !SAFE_NAME.test(name)) {
|
|
@@ -170,31 +196,28 @@ export const updateProfileMeta = (name, patch) => {
|
|
|
170
196
|
|
|
171
197
|
export const markRuntimeInitialized = (name, runtimeDir) => {
|
|
172
198
|
const now = new Date().toISOString();
|
|
173
|
-
return updateProfileMeta(name, {
|
|
199
|
+
return updateProfileMeta(name, (meta) => ({
|
|
200
|
+
...meta,
|
|
174
201
|
runtimeDir,
|
|
175
202
|
runtimeInitializedAt: now,
|
|
176
203
|
lastLaunchAt: now,
|
|
177
|
-
mode: 'account-session',
|
|
178
|
-
});
|
|
204
|
+
mode: name === DEFAULT_PROFILE ? meta.mode : 'account-session',
|
|
205
|
+
}));
|
|
179
206
|
};
|
|
180
207
|
|
|
181
208
|
export const markProfileLaunched = (name) => {
|
|
182
|
-
return updateProfileMeta(name, {
|
|
209
|
+
return updateProfileMeta(name, (meta) => ({
|
|
210
|
+
...meta,
|
|
183
211
|
lastLaunchAt: new Date().toISOString(),
|
|
184
|
-
mode: 'account-session',
|
|
185
|
-
});
|
|
212
|
+
mode: name === DEFAULT_PROFILE ? meta.mode : 'account-session',
|
|
213
|
+
}));
|
|
186
214
|
};
|
|
187
215
|
|
|
188
216
|
export const listProfileNames = () => {
|
|
189
217
|
return Object.keys(readProfiles());
|
|
190
218
|
};
|
|
191
219
|
|
|
192
|
-
// Returns the directory containing a profile's actual files.
|
|
193
|
-
// If profile is active, items were moved to CLAUDE_DIR during switch.
|
|
194
|
-
// If profile is not active, items are in profileDir.
|
|
195
220
|
export const getEffectiveDir = (name) => {
|
|
196
|
-
if (name === DEFAULT_PROFILE) return CLAUDE_DIR;
|
|
197
221
|
const active = getActive();
|
|
198
|
-
|
|
199
|
-
return getProfileDir(name);
|
|
222
|
+
return active === name ? CLAUDE_DIR : getProfileDir(name);
|
|
200
223
|
};
|