claude-switch-profile 1.0.2 → 1.1.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 +45 -0
- package/README.md +66 -1
- package/bin/csp.js +8 -0
- package/package.json +1 -1
- package/src/commands/create.js +53 -10
- package/src/commands/init.js +30 -7
- package/src/commands/uninstall.js +89 -0
- package/src/symlink-manager.js +40 -7
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `claude-switch-profile` are documented here.
|
|
4
|
+
|
|
5
|
+
## [1.1.0] - 2026-03-13
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- ✨ **`csp uninstall` command** — Cleanly remove CSP and restore a chosen profile to `~/.claude` before wiping all profile data. Supports `--force` and `--profile <name>` flags.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- 🔄 **`saveSymlinks` now handles real dirs/files** — When a managed item in `~/.claude` is a real directory or file (not yet a symlink), `saveSymlinks` moves it into the profile directory and replaces it with a symlink in-place. Fixes the case where switching from a fresh `~/.claude` setup left the default profile empty.
|
|
12
|
+
- 🔄 **`removeSymlinks` now removes real dirs/files** — Previously only removed symlinks; now also removes real directories and files for managed items so `createSymlinks` is never blocked.
|
|
13
|
+
- 🔄 **`createSymlinks` uses `rmSync` instead of `unlinkSync`** — Correctly handles pre-existing directories (not just files/symlinks) when restoring a profile.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- 🐛 **`statusline.cjs` broken after switch to default profile** — `saveSymlinks` was copying the file into the profile directory, causing `require('./lib/...')` to fail because `lib/` does not exist there. Now the file stays as a symlink pointing to the source project.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## [1.0.2] - 2026-03-12
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- 🐛 **`csp create` produces clean profiles** — New profiles no longer inherit the current session state; they start empty and isolated.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- 🔄 **Max backups reduced to 2** — Keeps `.backup/` lean.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## [1.0.1] - 2026-03-11
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- 🐛 Initial bug fixes after first release.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## [1.0.0] - 2026-03-11
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
- ✨ Initial release of `claude-switch-profile`
|
|
41
|
+
- `csp init`, `csp create`, `csp use`, `csp save`, `csp list`, `csp current`
|
|
42
|
+
- `csp delete`, `csp export`, `csp import`, `csp diff`
|
|
43
|
+
- Symlink + copy strategy for isolating Claude Code profiles
|
|
44
|
+
- Lock file, automatic backups, Claude process detection
|
|
45
|
+
- Environment variable overrides (`CSP_HOME`, `CSP_CLAUDE_DIR`, `CSP_PROFILES_DIR`)
|
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Claude Switch Profile (CSP)
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/claude-switch-profile)
|
|
4
|
+
[](https://nodejs.org)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
3
7
|
A CLI tool for managing multiple Claude Code configurations and profiles. Effortlessly switch between different development setups, each with their own rules, agents, skills, and settings.
|
|
4
8
|
|
|
5
9
|
## Overview
|
|
@@ -104,13 +108,22 @@ csp list
|
|
|
104
108
|
Output example:
|
|
105
109
|
|
|
106
110
|
```
|
|
107
|
-
* default —
|
|
111
|
+
* default — Vanilla Claude defaults (2026-03-11)
|
|
108
112
|
work — Work setup (2026-03-11)
|
|
109
113
|
experimental (2026-03-11)
|
|
110
114
|
```
|
|
111
115
|
|
|
112
116
|
The `*` marks the active profile.
|
|
113
117
|
|
|
118
|
+
### 6. Uninstall CSP
|
|
119
|
+
|
|
120
|
+
Remove CSP and restore your Claude Code to its original state:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
csp uninstall
|
|
124
|
+
# Uninstall CSP and remove all profiles? This cannot be undone. (y/N)
|
|
125
|
+
```
|
|
126
|
+
|
|
114
127
|
## Commands Reference
|
|
115
128
|
|
|
116
129
|
### init
|
|
@@ -403,6 +416,44 @@ File differences:
|
|
|
403
416
|
- Lists file presence and content differences
|
|
404
417
|
- Shows which files differ and in which profile they exist
|
|
405
418
|
|
|
419
|
+
### uninstall
|
|
420
|
+
|
|
421
|
+
Remove all profiles and restore Claude Code to its pre-CSP state.
|
|
422
|
+
|
|
423
|
+
```bash
|
|
424
|
+
csp uninstall
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
**Options:**
|
|
428
|
+
- `-f, --force` — Skip confirmation prompt
|
|
429
|
+
- `--profile <name>` — Restore a specific profile instead of the active one
|
|
430
|
+
|
|
431
|
+
**Examples:**
|
|
432
|
+
|
|
433
|
+
Uninstall with confirmation:
|
|
434
|
+
```bash
|
|
435
|
+
csp uninstall
|
|
436
|
+
# Uninstall CSP and remove all profiles? This cannot be undone. (y/N)
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
Restore a specific profile during uninstall:
|
|
440
|
+
```bash
|
|
441
|
+
csp uninstall --profile production
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
Force uninstall without prompt:
|
|
445
|
+
```bash
|
|
446
|
+
csp uninstall --force
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
**Behavior:**
|
|
450
|
+
1. Creates a final backup at `~/.claude-profiles/.backup/`
|
|
451
|
+
2. Restores the active profile (or `--profile` choice) to `~/.claude`
|
|
452
|
+
3. Removes `~/.claude-profiles/` entirely
|
|
453
|
+
4. Prints reminder to run `npm uninstall -g claude-switch-profile`
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
406
457
|
## How Profiles Work
|
|
407
458
|
|
|
408
459
|
### Profile Storage
|
|
@@ -470,6 +521,16 @@ Profiles are stored in `~/.claude-profiles/`:
|
|
|
470
521
|
- Each profile needs its own independent configuration
|
|
471
522
|
- Prevents accidental modifications from affecting other profiles
|
|
472
523
|
|
|
524
|
+
### Real Directory Handling
|
|
525
|
+
|
|
526
|
+
When `csp save` (or `csp use`) encounters a **real directory/file** in `~/.claude` for a managed item (instead of a symlink), it automatically:
|
|
527
|
+
|
|
528
|
+
1. **Moves** the real item into the profile directory (`~/.claude-profiles/<name>/<item>`)
|
|
529
|
+
2. **Replaces** it with a symlink at the original location
|
|
530
|
+
3. **Records** the new location in `source.json`
|
|
531
|
+
|
|
532
|
+
This ensures that profiles created from a fresh `~/.claude` setup (before any symlinks exist) work correctly on first use.
|
|
533
|
+
|
|
473
534
|
## Safety Features
|
|
474
535
|
|
|
475
536
|
### Lock File
|
|
@@ -699,6 +760,10 @@ npm run test:cli # CLI integration tests
|
|
|
699
760
|
npm run test:safety # Safety feature tests
|
|
700
761
|
```
|
|
701
762
|
|
|
763
|
+
### Changelog
|
|
764
|
+
|
|
765
|
+
See [CHANGELOG.md](CHANGELOG.md) for version history and migration guidance.
|
|
766
|
+
|
|
702
767
|
### Project Structure
|
|
703
768
|
|
|
704
769
|
```
|
package/bin/csp.js
CHANGED
|
@@ -15,6 +15,7 @@ import { exportCommand } from '../src/commands/export.js';
|
|
|
15
15
|
import { importCommand } from '../src/commands/import.js';
|
|
16
16
|
import { diffCommand } from '../src/commands/diff.js';
|
|
17
17
|
import { initCommand } from '../src/commands/init.js';
|
|
18
|
+
import { uninstallCommand } from '../src/commands/uninstall.js';
|
|
18
19
|
|
|
19
20
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
21
|
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
@@ -88,4 +89,11 @@ program
|
|
|
88
89
|
.description('Compare two profiles (use "current" for active profile)')
|
|
89
90
|
.action(diffCommand);
|
|
90
91
|
|
|
92
|
+
program
|
|
93
|
+
.command('uninstall')
|
|
94
|
+
.description('Remove all profiles and restore Claude Code to pre-CSP state')
|
|
95
|
+
.option('-f, --force', 'Skip confirmation prompt')
|
|
96
|
+
.option('--profile <name>', 'Restore specific profile instead of active one')
|
|
97
|
+
.action(uninstallCommand);
|
|
98
|
+
|
|
91
99
|
program.parse();
|
package/package.json
CHANGED
package/src/commands/create.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { mkdirSync, cpSync, existsSync, writeFileSync,
|
|
1
|
+
import { mkdirSync, cpSync, existsSync, statSync, writeFileSync, readFileSync, copyFileSync } from 'node:fs';
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
import { addProfile, getActive, setActive, profileExists, getProfileDir } from '../profile-store.js';
|
|
4
4
|
import { saveFiles } from '../file-operations.js';
|
|
5
|
-
import { SYMLINK_ITEMS, SYMLINK_DIRS, SOURCE_FILE } from '../constants.js';
|
|
5
|
+
import { CLAUDE_DIR, SYMLINK_ITEMS, SYMLINK_DIRS, SOURCE_FILE } from '../constants.js';
|
|
6
6
|
import { success, error, info, warn } from '../output-helpers.js';
|
|
7
7
|
|
|
8
8
|
export const createCommand = (name, options) => {
|
|
@@ -45,6 +45,22 @@ export const createCommand = (name, options) => {
|
|
|
45
45
|
warn(`No recognized items found in "${sourcePath}". Expected: ${SYMLINK_ITEMS.join(', ')}`);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// Inherit missing items from current state (hooks, statusline, etc.)
|
|
49
|
+
for (const item of SYMLINK_ITEMS) {
|
|
50
|
+
if (sourceMap[item]) continue;
|
|
51
|
+
const src = join(CLAUDE_DIR, item);
|
|
52
|
+
if (!existsSync(src)) continue;
|
|
53
|
+
try {
|
|
54
|
+
const dest = join(profileDir, item);
|
|
55
|
+
if (statSync(src).isDirectory()) {
|
|
56
|
+
cpSync(src, dest, { recursive: true, dereference: true });
|
|
57
|
+
} else {
|
|
58
|
+
copyFileSync(src, dest);
|
|
59
|
+
}
|
|
60
|
+
sourceMap[item] = dest;
|
|
61
|
+
} catch { /* skip */ }
|
|
62
|
+
}
|
|
63
|
+
|
|
48
64
|
writeFileSync(join(profileDir, SOURCE_FILE), JSON.stringify(sourceMap, null, 2) + '\n');
|
|
49
65
|
info(`Linked to kit at ${sourcePath}`);
|
|
50
66
|
info(`Items found: ${Object.keys(sourceMap).join(', ') || 'none'}`);
|
|
@@ -52,21 +68,48 @@ export const createCommand = (name, options) => {
|
|
|
52
68
|
// Also copy current mutable files
|
|
53
69
|
saveFiles(profileDir);
|
|
54
70
|
} else {
|
|
55
|
-
// Create
|
|
71
|
+
// Create new independent profile — inherit infrastructure from current state
|
|
56
72
|
mkdirSync(profileDir, { recursive: true });
|
|
57
73
|
|
|
58
|
-
//
|
|
74
|
+
// Copy all current symlink items (dirs + files) into profile — dereference symlinks
|
|
59
75
|
const sourceMap = {};
|
|
76
|
+
for (const item of SYMLINK_ITEMS) {
|
|
77
|
+
const src = join(CLAUDE_DIR, item);
|
|
78
|
+
const dest = join(profileDir, item);
|
|
79
|
+
try {
|
|
80
|
+
if (!existsSync(src)) continue;
|
|
81
|
+
if (statSync(src).isDirectory()) {
|
|
82
|
+
cpSync(src, dest, { recursive: true, dereference: true });
|
|
83
|
+
} else {
|
|
84
|
+
copyFileSync(src, dest);
|
|
85
|
+
}
|
|
86
|
+
sourceMap[item] = dest;
|
|
87
|
+
} catch { /* skip unreadable items */ }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Fallback: ensure empty dirs for any missing SYMLINK_DIRS
|
|
60
91
|
for (const item of SYMLINK_DIRS) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
92
|
+
if (!sourceMap[item]) {
|
|
93
|
+
const itemDir = join(profileDir, item);
|
|
94
|
+
mkdirSync(itemDir, { recursive: true });
|
|
95
|
+
sourceMap[item] = itemDir;
|
|
96
|
+
}
|
|
64
97
|
}
|
|
65
98
|
|
|
66
99
|
writeFileSync(join(profileDir, SOURCE_FILE), JSON.stringify(sourceMap, null, 2) + '\n');
|
|
67
|
-
|
|
68
|
-
//
|
|
69
|
-
|
|
100
|
+
|
|
101
|
+
// Copy hooks config from settings.json (hooks won't run without it)
|
|
102
|
+
const settingsPath = join(CLAUDE_DIR, 'settings.json');
|
|
103
|
+
if (existsSync(settingsPath)) {
|
|
104
|
+
try {
|
|
105
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
106
|
+
if (settings.hooks) {
|
|
107
|
+
writeFileSync(join(profileDir, 'settings.json'), JSON.stringify({ hooks: settings.hooks }, null, 2) + '\n');
|
|
108
|
+
}
|
|
109
|
+
} catch { /* parse error — skip */ }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
info('Created new independent profile (infrastructure inherited)');
|
|
70
113
|
}
|
|
71
114
|
|
|
72
115
|
addProfile(name, { description: options.description || '' });
|
package/src/commands/init.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, lstatSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { ensureProfilesDir, getActive, addProfile, setActive, getProfileDir } from '../profile-store.js';
|
|
4
4
|
import { success, info, warn } from '../output-helpers.js';
|
|
5
|
-
import { PROFILES_DIR } from '../constants.js';
|
|
5
|
+
import { PROFILES_DIR, SOURCE_FILE, CLAUDE_DIR, SYMLINK_ITEMS } from '../constants.js';
|
|
6
6
|
|
|
7
7
|
export const initCommand = () => {
|
|
8
8
|
if (existsSync(PROFILES_DIR) && getActive()) {
|
|
@@ -15,7 +15,30 @@ export const initCommand = () => {
|
|
|
15
15
|
ensureProfilesDir();
|
|
16
16
|
info('Initializing Claude Switch Profile...');
|
|
17
17
|
|
|
18
|
-
// Create default profile
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
// Create "default" as vanilla Claude — empty profile with no symlinks or copy files.
|
|
19
|
+
// Switching to this profile removes all managed items, restoring Claude's built-in defaults.
|
|
20
|
+
const profileDir = getProfileDir('default');
|
|
21
|
+
mkdirSync(profileDir, { recursive: true });
|
|
22
|
+
writeFileSync(join(profileDir, SOURCE_FILE), '{}\n');
|
|
23
|
+
|
|
24
|
+
addProfile('default', { description: 'Vanilla Claude defaults' });
|
|
25
|
+
setActive('default');
|
|
26
|
+
|
|
27
|
+
success('Initialization complete. "default" profile uses vanilla Claude defaults.');
|
|
28
|
+
|
|
29
|
+
// Warn if real directories exist that should be captured into a profile
|
|
30
|
+
const realItems = SYMLINK_ITEMS.filter((item) => {
|
|
31
|
+
const itemPath = join(CLAUDE_DIR, item);
|
|
32
|
+
try {
|
|
33
|
+
const stat = lstatSync(itemPath);
|
|
34
|
+
return !stat.isSymbolicLink() && (stat.isDirectory() || stat.isFile());
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (realItems.length) {
|
|
41
|
+
warn(`Real directories/files detected: ${realItems.join(', ')}`);
|
|
42
|
+
info('Run "csp create <name>" to capture them into a profile.');
|
|
43
|
+
}
|
|
21
44
|
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { existsSync, rmSync } from 'node:fs';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import { getActive, profileExists, getProfileDir } from '../profile-store.js';
|
|
4
|
+
import { removeSymlinks, restoreSymlinks } from '../symlink-manager.js';
|
|
5
|
+
import { removeFiles, restoreFiles } from '../file-operations.js';
|
|
6
|
+
import { withLock, createBackup, warnIfClaudeRunning } from '../safety.js';
|
|
7
|
+
import { PROFILES_DIR } 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 && profileExists(active)) {
|
|
35
|
+
info(` 1. Restore active profile "${active}" to ~/.claude (use --profile <name> to choose)`);
|
|
36
|
+
} else {
|
|
37
|
+
warn(' 1. No profile to restore (managed items will be removed)');
|
|
38
|
+
}
|
|
39
|
+
info(` 2. Remove all profiles: ${PROFILES_DIR}`);
|
|
40
|
+
info(' 3. You can then run: npm uninstall -g claude-switch-profile');
|
|
41
|
+
console.log('');
|
|
42
|
+
|
|
43
|
+
if (!options.force) {
|
|
44
|
+
const confirmed = await confirm('Uninstall CSP and remove all profiles? This cannot be undone. (y/N) ');
|
|
45
|
+
if (!confirmed) {
|
|
46
|
+
warn('Cancelled.');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
warnIfClaudeRunning();
|
|
52
|
+
|
|
53
|
+
await withLock(async () => {
|
|
54
|
+
// 1. Create final backup before uninstall
|
|
55
|
+
try {
|
|
56
|
+
const backupPath = createBackup();
|
|
57
|
+
info(`Final backup created at ${backupPath}`);
|
|
58
|
+
} catch {
|
|
59
|
+
// Non-critical — profiles dir may be empty
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 2. Determine which profile to restore
|
|
63
|
+
const restoreProfile = options.profile || active;
|
|
64
|
+
|
|
65
|
+
// 3. Remove current managed items from ~/.claude
|
|
66
|
+
removeSymlinks();
|
|
67
|
+
removeFiles();
|
|
68
|
+
|
|
69
|
+
// 4. Restore the chosen profile's config
|
|
70
|
+
if (restoreProfile && profileExists(restoreProfile)) {
|
|
71
|
+
const profileDir = getProfileDir(restoreProfile);
|
|
72
|
+
restoreSymlinks(profileDir);
|
|
73
|
+
restoreFiles(profileDir);
|
|
74
|
+
success(`Restored "${restoreProfile}" profile to ~/.claude`);
|
|
75
|
+
} else {
|
|
76
|
+
warn('No profile restored. ~/.claude managed items have been cleared.');
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// 5. Remove profiles directory (after lock is released)
|
|
81
|
+
rmSync(PROFILES_DIR, { recursive: true, force: true });
|
|
82
|
+
success('Removed all profiles and CSP data.');
|
|
83
|
+
|
|
84
|
+
console.log('');
|
|
85
|
+
info('To complete uninstall, run:');
|
|
86
|
+
info(' npm uninstall -g claude-switch-profile');
|
|
87
|
+
info('');
|
|
88
|
+
info('Restart your Claude Code session to apply changes.');
|
|
89
|
+
};
|
package/src/symlink-manager.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readlinkSync, symlinkSync, unlinkSync, lstatSync, readFileSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readlinkSync, symlinkSync, unlinkSync, lstatSync, readFileSync, writeFileSync, cpSync, rmSync } from 'node:fs';
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
import { CLAUDE_DIR, SYMLINK_ITEMS, SOURCE_FILE } from './constants.js';
|
|
4
4
|
|
|
@@ -18,16 +18,24 @@ export const readCurrentSymlinks = () => {
|
|
|
18
18
|
return sourceMap;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
// Remove all managed symlinks from ~/.claude
|
|
21
|
+
// Remove all managed symlinks (and real dirs/files) from ~/.claude
|
|
22
22
|
export const removeSymlinks = () => {
|
|
23
23
|
for (const item of SYMLINK_ITEMS) {
|
|
24
24
|
const itemPath = join(CLAUDE_DIR, item);
|
|
25
25
|
try {
|
|
26
|
-
if (existsSync(itemPath) && lstatSync(itemPath).isSymbolicLink())
|
|
26
|
+
if (!existsSync(itemPath) && !lstatSync(itemPath).isSymbolicLink()) continue;
|
|
27
|
+
} catch {
|
|
28
|
+
continue; // Doesn't exist at all
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const stat = lstatSync(itemPath);
|
|
32
|
+
if (stat.isSymbolicLink()) {
|
|
27
33
|
unlinkSync(itemPath);
|
|
34
|
+
} else if (stat.isDirectory() || stat.isFile()) {
|
|
35
|
+
rmSync(itemPath, { recursive: true });
|
|
28
36
|
}
|
|
29
37
|
} catch {
|
|
30
|
-
// Already gone
|
|
38
|
+
// Already gone — skip
|
|
31
39
|
}
|
|
32
40
|
}
|
|
33
41
|
};
|
|
@@ -38,9 +46,9 @@ export const createSymlinks = (sourceMap) => {
|
|
|
38
46
|
if (!SYMLINK_ITEMS.includes(item)) continue;
|
|
39
47
|
const itemPath = join(CLAUDE_DIR, item);
|
|
40
48
|
|
|
41
|
-
// Remove existing
|
|
49
|
+
// Remove existing (symlink, file, or directory)
|
|
42
50
|
try {
|
|
43
|
-
if (lstatSync(itemPath))
|
|
51
|
+
if (lstatSync(itemPath)) rmSync(itemPath, { recursive: true, force: true });
|
|
44
52
|
} catch {
|
|
45
53
|
// Doesn't exist — fine
|
|
46
54
|
}
|
|
@@ -53,8 +61,33 @@ export const createSymlinks = (sourceMap) => {
|
|
|
53
61
|
};
|
|
54
62
|
|
|
55
63
|
// Save current symlink targets to profileDir/source.json
|
|
64
|
+
// For real dirs/files: move them into profileDir and create symlinks in their place
|
|
56
65
|
export const saveSymlinks = (profileDir) => {
|
|
57
|
-
const sourceMap =
|
|
66
|
+
const sourceMap = {};
|
|
67
|
+
for (const item of SYMLINK_ITEMS) {
|
|
68
|
+
const itemPath = join(CLAUDE_DIR, item);
|
|
69
|
+
try {
|
|
70
|
+
if (!existsSync(itemPath) && !lstatSync(itemPath).isSymbolicLink()) continue;
|
|
71
|
+
} catch {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const stat = lstatSync(itemPath);
|
|
76
|
+
if (stat.isSymbolicLink()) {
|
|
77
|
+
// Already a symlink — just record its target
|
|
78
|
+
sourceMap[item] = resolve(CLAUDE_DIR, readlinkSync(itemPath));
|
|
79
|
+
} else if (stat.isDirectory() || stat.isFile()) {
|
|
80
|
+
// Real dir/file — move into profileDir, replace with symlink
|
|
81
|
+
const dest = join(profileDir, item);
|
|
82
|
+
cpSync(itemPath, dest, { recursive: true });
|
|
83
|
+
rmSync(itemPath, { recursive: true });
|
|
84
|
+
symlinkSync(dest, itemPath);
|
|
85
|
+
sourceMap[item] = dest;
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// Not readable — skip
|
|
89
|
+
}
|
|
90
|
+
}
|
|
58
91
|
writeFileSync(join(profileDir, SOURCE_FILE), JSON.stringify(sourceMap, null, 2) + '\n');
|
|
59
92
|
return sourceMap;
|
|
60
93
|
};
|