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.
@@ -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
+ };
@@ -1,14 +1,18 @@
1
- import { getActive, setActive, profileExists, getProfileDir } from '../profile-store.js';
2
- import { saveSymlinks, removeSymlinks, restoreSymlinks, createSymlinks } from '../symlink-manager.js';
3
- import { saveFiles, removeFiles, restoreFiles } from '../file-operations.js';
4
- import { validateProfile, validateSourceTargets } from '../profile-validator.js';
5
- import { withLock, warnIfClaudeRunning, createBackup } from '../safety.js';
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 { readFileSync, existsSync } from 'node:fs';
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
- // Validate target profile
26
- const validation = validateProfile(profileDir);
27
- if (!validation.valid) {
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 symlink targets exist
34
- const sourcePath = join(profileDir, SOURCE_FILE);
35
- try {
36
- const sourceMap = JSON.parse(readFileSync(sourcePath, 'utf-8'));
37
- const targetValidation = validateSourceTargets(sourceMap);
38
- if (!targetValidation.valid) {
39
- warn('Some symlink targets are missing:');
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
- warnIfClaudeRunning();
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 to active profile (if any)
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
- saveSymlinks(activeDir);
63
- saveFiles(activeDir);
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. Auto-backup
68
- const backupPath = createBackup();
69
- info(`Backup created at ${backupPath}`);
70
-
71
- // 3. Remove managed items + restore target — with rollback on failure
72
- removeSymlinks();
73
- removeFiles();
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
- try {
76
- restoreSymlinks(profileDir);
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
- const backupSource = join(backupPath, SOURCE_FILE);
83
- if (existsSync(backupSource)) {
84
- const backupMap = JSON.parse(readFileSync(backupSource, 'utf-8'));
85
- createSymlinks(backupMap);
86
- }
87
- restoreFiles(backupPath);
88
- warn('Rollback complete. Previous config restored.');
89
- } catch (rollbackErr) {
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
- info('Restart your Claude Code session to apply changes.');
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 — these point to external dirs/files
17
- export const SYMLINK_ITEMS = [
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 symlink items (auto-created in new profiles)
28
- export const SYMLINK_DIRS = ['rules', 'agents', 'skills', 'hooks'];
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
- // Never touch these runtime/session data
45
- export const NEVER_TOUCH = [
60
+ // Items to NEVER clone (runtime/cache/tracking)
61
+ export const NEVER_CLONE = [
46
62
  '.credentials.json',
47
63
  'projects',
48
- 'backups',
64
+ 'sessions',
65
+ 'session-env',
66
+ 'ide',
49
67
  'cache',
50
- 'debug',
51
- 'telemetry',
52
- 'shell-snapshots',
53
68
  'paste-cache',
54
- 'file-history',
55
- 'ide',
56
- 'session-env',
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
- // All managed items (symlinks + copies + copy dirs)
66
- export const ALL_MANAGED = [...SYMLINK_ITEMS, ...COPY_ITEMS, ...COPY_DIRS];
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];
@@ -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
+ };