claude-switch-profile 1.4.21 → 1.4.23

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.
@@ -3,8 +3,8 @@ require "language/node"
3
3
  class ClaudeSwitchProfile < Formula
4
4
  desc "CLI tool for managing multiple Claude Code profiles"
5
5
  homepage "https://github.com/ThanhThi2895/claude-switch-profile"
6
- url "https://registry.npmjs.org/claude-switch-profile/-/claude-switch-profile-1.4.20.tgz"
7
- sha256 "5cb22677386f5220f92ccfe714e968e8121e27943daed2e816f6b1128cf8aba9"
6
+ url "https://registry.npmjs.org/claude-switch-profile/-/claude-switch-profile-1.4.22.tgz"
7
+ sha256 "4d828740a0d3c6e77093ca074686ab71412589c6eaa987a0d2e92002294e1a95"
8
8
  license "MIT"
9
9
 
10
10
  depends_on "node"
package/README.md CHANGED
@@ -39,7 +39,15 @@ curl -fsSL https://raw.githubusercontent.com/ThanhThi2895/claude-switch-profile/
39
39
  Leverages Homebrew's own managed cellars to isolate the Node engine:
40
40
 
41
41
  ```bash
42
- brew tap ThanhThi2895/claude-switch-profile
42
+ brew tap ThanhThi2895/claude-switch-profile https://github.com/ThanhThi2895/claude-switch-profile
43
+ brew install claude-switch-profile
44
+ ```
45
+
46
+ If you previously hit a tap resolution error, reset then tap again:
47
+
48
+ ```bash
49
+ brew untap ThanhThi2895/claude-switch-profile 2>/dev/null || true
50
+ brew tap ThanhThi2895/claude-switch-profile https://github.com/ThanhThi2895/claude-switch-profile
43
51
  brew install claude-switch-profile
44
52
  ```
45
53
 
@@ -101,6 +109,7 @@ csp
101
109
  | Command | Description |
102
110
  |---|---|
103
111
  | `csp` / `csp select` | Interactive profile selector (default) |
112
+ | `csp -v` / `csp --version` | Print the current CSP version |
104
113
  | `csp init` | Initialize and capture current state as `default` profile |
105
114
  | `csp create <name>` | Create a new profile (`--from`, `--source`, `-d` options) |
106
115
  | `csp launch <name>` | Launch isolated Claude session for a profile |
@@ -116,8 +125,13 @@ csp
116
125
  | `csp import <file>` | Import profile from archive |
117
126
  | `csp delete <name>` | Delete a profile |
118
127
  | `csp deactivate` | Switch back to `default` profile |
128
+ | `csp update [--method <npm\|brew\|standalone>]` | Update CSP (auto-detect install method by default) |
119
129
  | `csp uninstall --method <npm\|brew\|standalone>` | Uninstall csp CLI and keep all profiles |
120
130
 
131
+ > `csp -v` is a short alias for `csp --version` and prints the same semver.
132
+ >
133
+ > `csp update` auto-detects install method (npm/brew/standalone). Use `--method` to override explicitly.
134
+ >
121
135
  > 📖 **Full command reference with all options and detailed behavior:** [docs/commands-reference.md](docs/commands-reference.md)
122
136
 
123
137
  ## Two Launch Modes
@@ -143,6 +157,8 @@ csp use work
143
157
  ```
144
158
 
145
159
  - Mutates `~/.claude` directly
160
+ - Saves current active profile snapshot by copy (unless `--no-save`)
161
+ - Restores target profile snapshot into `~/.claude` by copy (profile snapshot is not consumed)
146
162
  - Updates `.active` marker
147
163
  - Requires Claude restart
148
164
 
package/bin/csp.js CHANGED
@@ -16,6 +16,7 @@ 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
18
  import { uninstallCommand } from '../src/commands/uninstall.js';
19
+ import { updateCommand } from '../src/commands/update.js';
19
20
  import { launchCommand, execCommand } from '../src/commands/launch.js';
20
21
  import { deactivateCommand } from '../src/commands/deactivate.js';
21
22
  import { toggleCommand } from '../src/commands/toggle.js';
@@ -30,7 +31,7 @@ const program = new Command();
30
31
  program
31
32
  .name('csp')
32
33
  .description('Claude Switch Profile — manage multiple Claude Code configurations')
33
- .version(pkg.version)
34
+ .version(pkg.version, '-v, --version')
34
35
  .enablePositionalOptions();
35
36
 
36
37
  program
@@ -157,4 +158,11 @@ program
157
158
  .option('--method <method>', 'Install method: npm | brew | standalone')
158
159
  .action(uninstallCommand);
159
160
 
161
+ program
162
+ .command('update')
163
+ .description('Update csp CLI while keeping all profile data intact')
164
+ .option('-f, --force', 'Skip confirmation prompt')
165
+ .option('--method <method>', 'Install method: npm | brew | standalone')
166
+ .action(updateCommand);
167
+
160
168
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-switch-profile",
3
- "version": "1.4.21",
3
+ "version": "1.4.23",
4
4
  "description": "CLI tool for managing multiple Claude Code profiles",
5
5
  "type": "module",
6
6
  "bin": {
@@ -81,7 +81,7 @@ export const createCommand = (name, options) => {
81
81
  }
82
82
 
83
83
  // MANAGED_FILES → empty file stubs (CLAUDE.md, statusline.*, .luna.json)
84
- // These must exist so moveItemsToClaude can overwrite the old profile's copies
84
+ // These must exist so restoreItems can copy profile snapshots into ~/.claude
85
85
  const managedFiles = MANAGED_ITEMS.filter((item) => !MANAGED_DIRS.includes(item));
86
86
  for (const item of managedFiles) {
87
87
  const dest = join(profileDir, item);
@@ -90,7 +90,7 @@ export const createCommand = (name, options) => {
90
90
  }
91
91
 
92
92
  // COPY_DIRS → empty directories (commands, plugins, workflows, scripts, …)
93
- // These must exist so moveDirsToClaude can overwrite the old profile's dirs
93
+ // These must exist so restoreFiles can copy profile directories into ~/.claude
94
94
  for (const dir of COPY_DIRS) {
95
95
  mkdirSync(join(profileDir, dir), { recursive: true });
96
96
  }
@@ -0,0 +1,116 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { createInterface } from 'node:readline';
7
+ import { success, info, warn, error } from '../output-helpers.js';
8
+
9
+ const METHODS = ['npm', 'brew', 'standalone'];
10
+ const STANDALONE_INSTALL_URL = 'https://raw.githubusercontent.com/ThanhThi2895/claude-switch-profile/main/install.sh';
11
+ const modulePath = fileURLToPath(import.meta.url);
12
+ const __dirname = dirname(modulePath);
13
+ const INSTALL_SCRIPT = join(__dirname, '..', '..', 'install.sh');
14
+ const TEST_DRY_RUN = process.env.NODE_ENV === 'test' || process.env.CSP_UPDATE_DRY_RUN === '1';
15
+
16
+ const confirm = (question) => new Promise((resolve) => {
17
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
18
+ rl.question(question, (answer) => {
19
+ rl.close();
20
+ resolve(answer.toLowerCase().startsWith('y'));
21
+ });
22
+ });
23
+
24
+ const normalizeMethod = (method) => {
25
+ const normalized = (method || '').toLowerCase();
26
+ if (!normalized) return null;
27
+ return METHODS.includes(normalized) ? normalized : null;
28
+ };
29
+
30
+ const detectInstallMethod = () => {
31
+ const normalizedPath = modulePath.replaceAll('\\', '/').toLowerCase();
32
+ const normalizedHome = homedir().replaceAll('\\', '/').toLowerCase();
33
+
34
+ if (normalizedPath.includes(`${normalizedHome}/.csp-cli/`)) return 'standalone';
35
+ if (normalizedPath.includes('/cellar/claude-switch-profile/')) return 'brew';
36
+ return 'npm';
37
+ };
38
+
39
+ const runCommand = ({ command, args, label }) => {
40
+ const rendered = [command, ...args].join(' ');
41
+
42
+ if (TEST_DRY_RUN) {
43
+ info(`${label} (dry run in test mode):`);
44
+ info(` ${rendered}`);
45
+ return true;
46
+ }
47
+
48
+ const result = spawnSync(command, args, { stdio: 'inherit' });
49
+
50
+ if (result.error) {
51
+ error(`${label} failed: ${result.error.message}`);
52
+ process.exitCode = 1;
53
+ return false;
54
+ }
55
+
56
+ if (result.status !== 0) {
57
+ error(`${label} failed.`);
58
+ process.exitCode = result.status || 1;
59
+ return false;
60
+ }
61
+
62
+ return true;
63
+ };
64
+
65
+ const runStandaloneUpdate = () => {
66
+ if (existsSync(INSTALL_SCRIPT)) {
67
+ return runCommand({ command: 'bash', args: [INSTALL_SCRIPT], label: 'Standalone update' });
68
+ }
69
+
70
+ return runCommand({
71
+ command: 'bash',
72
+ args: ['-lc', `curl -fsSL ${STANDALONE_INSTALL_URL} | bash`],
73
+ label: 'Standalone update',
74
+ });
75
+ };
76
+
77
+ export const updateCommand = async (options = {}) => {
78
+ const hasExplicitMethod = typeof options.method === 'string' && options.method.length > 0;
79
+ const method = hasExplicitMethod ? normalizeMethod(options.method) : detectInstallMethod();
80
+
81
+ if (hasExplicitMethod && !method) {
82
+ warn('Missing or invalid --method. Use one of: npm, brew, standalone');
83
+ process.exitCode = 1;
84
+ return;
85
+ }
86
+
87
+ console.log('');
88
+ info('This updates the csp CLI only.');
89
+ info('Profiles are kept at ~/.claude-profiles (no data is removed).');
90
+ info(`Method: ${method}${hasExplicitMethod ? '' : ' (auto-detected; override with --method)'}`);
91
+ console.log('');
92
+
93
+ if (!options.force) {
94
+ const confirmed = await confirm(`Proceed update for method "${method}"? (y/N) `);
95
+ if (!confirmed) {
96
+ warn('Cancelled.');
97
+ return;
98
+ }
99
+ }
100
+
101
+ if (method === 'standalone') {
102
+ if (runStandaloneUpdate()) success('csp update completed.');
103
+ return;
104
+ }
105
+
106
+ if (method === 'brew') {
107
+ if (runCommand({ command: 'brew', args: ['upgrade', 'claude-switch-profile'], label: 'Homebrew update' })) {
108
+ success('csp update completed.');
109
+ }
110
+ return;
111
+ }
112
+
113
+ if (runCommand({ command: 'npm', args: ['install', '-g', 'claude-switch-profile@latest'], label: 'npm update' })) {
114
+ success('csp update completed.');
115
+ }
116
+ };
@@ -7,15 +7,8 @@ import {
7
7
  setPrevious,
8
8
  ensureDefaultProfileSnapshot,
9
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';
10
+ import { copyItems, restoreItems, removeItems } from '../item-manager.js';
11
+ import { saveFiles, removeFiles, restoreFiles, updateSettingsPaths } from '../file-operations.js';
19
12
  import { validateProfile } from '../profile-validator.js';
20
13
  import { withLock, assertClaudeNotRunning } from '../safety.js';
21
14
  import { success, error, info, warn } from '../output-helpers.js';
@@ -94,9 +87,8 @@ export const useCommand = async (name, options = {}) => {
94
87
  await withLock(async () => {
95
88
  if (active && profileExists(active) && options.save !== false) {
96
89
  const activeDir = getProfileDir(active);
97
- moveItemsToProfile(activeDir);
90
+ copyItems(activeDir);
98
91
  saveFiles(activeDir);
99
- moveDirsToProfile(activeDir);
100
92
  updateSettingsPaths(activeDir, 'save');
101
93
  info(`Saved current state to "${active}"`);
102
94
  }
@@ -105,9 +97,8 @@ export const useCommand = async (name, options = {}) => {
105
97
  removeFiles();
106
98
 
107
99
  try {
108
- moveItemsToClaude(profileDir);
100
+ restoreItems(profileDir);
109
101
  restoreFiles(profileDir);
110
- moveDirsToClaude(profileDir);
111
102
  updateSettingsPaths(CLAUDE_DIR, 'restore', profileDir);
112
103
  } catch (err) {
113
104
  warn(`Switch failed: ${err.message}`);
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 by snapshot/move flows; file/dir/symlink shape is preserved
22
+ // Items managed by snapshot/copy flows; file/dir/symlink shape is preserved
23
23
  export const MANAGED_ITEMS = [
24
24
  'CLAUDE.md',
25
25
  'rules',
@@ -1,20 +1,7 @@
1
- import { existsSync, cpSync, unlinkSync, rmSync, mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
1
+ import { existsSync, cpSync, unlinkSync, rmSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join, dirname } 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, verbatimSymlinks: true });
13
- rmSync(src, { recursive: true, force: true });
14
- } else throw err;
15
- }
16
- };
17
-
18
5
  const copyPathPreservingSymlink = (src, dest) => {
19
6
  mkdirSync(dirname(dest), { recursive: true });
20
7
  rmSync(dest, { recursive: true, force: true });
@@ -78,28 +65,6 @@ export const removeFiles = () => {
78
65
  }
79
66
  };
80
67
 
81
- // Move COPY_DIRS from ~/.claude → profileDir (destructive, for use command)
82
- // COPY_ITEMS always copied (tiny files, not worth move complexity)
83
- export const moveDirsToProfile = (profileDir) => {
84
- mkdirSync(profileDir, { recursive: true });
85
- for (const dir of COPY_DIRS) {
86
- const src = join(CLAUDE_DIR, dir);
87
- if (existsSync(src)) {
88
- moveDir(src, join(profileDir, dir));
89
- }
90
- }
91
- };
92
-
93
- // Move COPY_DIRS from profileDir → ~/.claude (destructive, for use command)
94
- export const moveDirsToClaude = (profileDir) => {
95
- for (const dir of COPY_DIRS) {
96
- const src = join(profileDir, dir);
97
- if (existsSync(src)) {
98
- moveDir(src, join(CLAUDE_DIR, dir));
99
- }
100
- }
101
- };
102
-
103
68
  /**
104
69
  * Update absolute paths in settings.json.
105
70
  * Direction: 'save' replaces CLAUDE_DIR → profileDir, 'restore' replaces profileDir → CLAUDE_DIR.
@@ -1,23 +1,7 @@
1
- import { existsSync, readFileSync, writeFileSync, cpSync, rmSync, mkdirSync, lstatSync, renameSync } from 'node:fs';
2
- import { join, dirname, basename } from 'node:path';
1
+ import { existsSync, readFileSync, writeFileSync, cpSync, rmSync, mkdirSync, lstatSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
3
  import { CLAUDE_DIR, MANAGED_ITEMS, SOURCE_FILE } from './constants.js';
4
4
 
5
- const SKIP_PATTERNS = ['.venv', 'node_modules', '__pycache__', '.git'];
6
- const skipHeavyDirs = (src) => !SKIP_PATTERNS.includes(basename(src));
7
-
8
- // Rename-based move with EXDEV fallback (filtered copy + delete)
9
- const moveItem = (src, dest) => {
10
- mkdirSync(dirname(dest), { recursive: true });
11
- if (existsSync(dest)) rmSync(dest, { recursive: true, force: true });
12
- try {
13
- renameSync(src, dest);
14
- } catch (err) {
15
- if (err.code === 'EXDEV') {
16
- cpSync(src, dest, { recursive: true, filter: skipHeavyDirs, verbatimSymlinks: true });
17
- rmSync(src, { recursive: true, force: true });
18
- } else throw err;
19
- }
20
- };
21
5
 
22
6
  // Read current managed items from ~/.claude — returns map of {item: claudeDir/item}
23
7
  export const readCurrentItems = () => {
@@ -88,6 +72,8 @@ export const restoreItems = (profileDir) => {
88
72
  if (!MANAGED_ITEMS.includes(item)) continue;
89
73
 
90
74
  const dest = join(CLAUDE_DIR, item);
75
+ const localSrc = join(profileDir, item);
76
+ const restoreSrc = existsSync(localSrc) ? localSrc : srcPath;
91
77
 
92
78
  try {
93
79
  if (existsSync(dest)) rmSync(dest, { recursive: true, force: true });
@@ -95,10 +81,10 @@ export const restoreItems = (profileDir) => {
95
81
  // fine
96
82
  }
97
83
 
98
- // Copy from profile (or external legacy path) into ~/.claude
99
- if (existsSync(srcPath)) {
84
+ // Prefer profile-local snapshot; fallback to legacy external source path
85
+ if (restoreSrc && existsSync(restoreSrc)) {
100
86
  try {
101
- copyItemPreservingSymlink(srcPath, dest);
87
+ copyItemPreservingSymlink(restoreSrc, dest);
102
88
  } catch {
103
89
  // skip unreadable
104
90
  }
@@ -108,42 +94,3 @@ export const restoreItems = (profileDir) => {
108
94
  return sourceMap;
109
95
  };
110
96
 
111
- // Move items from ~/.claude → profileDir (destructive — items leave ~/.claude)
112
- export const moveItemsToProfile = (profileDir) => {
113
- mkdirSync(profileDir, { recursive: true });
114
- const sourceMap = {};
115
- for (const item of MANAGED_ITEMS) {
116
- const itemPath = join(CLAUDE_DIR, item);
117
- if (!existsSync(itemPath)) continue;
118
- try {
119
- const dest = join(profileDir, item);
120
- moveItem(itemPath, dest);
121
- sourceMap[item] = dest;
122
- } catch {
123
- // skip
124
- }
125
- }
126
- writeFileSync(join(profileDir, SOURCE_FILE), JSON.stringify(sourceMap, null, 2) + '\n');
127
- return sourceMap;
128
- };
129
-
130
- // Move items from profileDir → ~/.claude (destructive — items leave profileDir)
131
- export const moveItemsToClaude = (profileDir) => {
132
- const sourcePath = join(profileDir, SOURCE_FILE);
133
- if (!existsSync(sourcePath)) return {};
134
- const sourceMap = JSON.parse(readFileSync(sourcePath, 'utf-8'));
135
-
136
- for (const [item] of Object.entries(sourceMap)) {
137
- if (!MANAGED_ITEMS.includes(item)) continue;
138
- const src = join(profileDir, item);
139
- const dest = join(CLAUDE_DIR, item);
140
- if (!existsSync(src)) continue;
141
- try {
142
- if (existsSync(dest)) rmSync(dest, { recursive: true, force: true });
143
- moveItem(src, dest);
144
- } catch {
145
- // skip
146
- }
147
- }
148
- return sourceMap;
149
- };