@aravhawk/cc-switch 1.0.1

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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +170 -0
  3. package/dist/index.js +399 -0
  4. package/package.json +40 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Arav Jain
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # cc-switch
2
+
3
+ Profile manager for Claude Code settings. Easily manage and switch between multiple Claude Code configurations.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @aravhawk/cc-switch
9
+ ```
10
+
11
+ Or with pnpm:
12
+
13
+ ```bash
14
+ pnpm add -g @aravhawk/cc-switch
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ### Interactive Mode
20
+
21
+ Run `cc-switch` without arguments to enter interactive mode:
22
+
23
+ ```bash
24
+ cc-switch
25
+ ```
26
+
27
+ This will present a menu with options to:
28
+ - Switch between profiles
29
+ - Create new profiles
30
+ - Delete profiles
31
+ - Rename profiles
32
+ - List all profiles
33
+
34
+ ### Command Line Mode
35
+
36
+ #### Switch Profile
37
+
38
+ ```bash
39
+ cc-switch switch <profile-name>
40
+ ```
41
+
42
+ Example:
43
+ ```bash
44
+ cc-switch switch work
45
+ ```
46
+
47
+ #### Create Profile
48
+
49
+ Create a new profile from your current `~/.claude/settings.json`:
50
+
51
+ ```bash
52
+ cc-switch create <profile-name>
53
+ ```
54
+
55
+ Example:
56
+ ```bash
57
+ cc-switch create personal
58
+ ```
59
+
60
+ #### Delete Profile
61
+
62
+ Delete an existing profile (cannot delete the active profile):
63
+
64
+ ```bash
65
+ cc-switch delete <profile-name>
66
+ ```
67
+
68
+ Example:
69
+ ```bash
70
+ cc-switch delete old-config
71
+ ```
72
+
73
+ #### Rename Profile
74
+
75
+ Rename an existing profile:
76
+
77
+ ```bash
78
+ cc-switch rename <old-name> <new-name>
79
+ ```
80
+
81
+ Example:
82
+ ```bash
83
+ cc-switch rename work work-2024
84
+ ```
85
+
86
+ #### List Profiles
87
+
88
+ List all available profiles:
89
+
90
+ ```bash
91
+ cc-switch list
92
+ ```
93
+
94
+ ## How It Works
95
+
96
+ `cc-switch` manages multiple Claude Code profiles by storing copies of your `settings.json` file in separate profile directories.
97
+
98
+ ### Data Layout
99
+
100
+ ```
101
+ ~/.cc-switch/
102
+ ├── profiles/
103
+ │ ├── default/
104
+ │ │ └── settings.json
105
+ │ ├── work/
106
+ │ │ └── settings.json
107
+ │ └── personal/
108
+ │ └── settings.json
109
+ └── state.json
110
+ ```
111
+
112
+ ### State File
113
+
114
+ The `state.json` file tracks the currently active profile:
115
+
116
+ ```json
117
+ {
118
+ "activeProfile": "default",
119
+ "lastSyncedAt": "2026-02-01T12:34:56.789Z"
120
+ }
121
+ ```
122
+
123
+ ### Switch Behavior
124
+
125
+ When you switch profiles:
126
+
127
+ 1. The current `~/.claude/settings.json` is mirrored back to the active profile's directory
128
+ 2. The target profile's `settings.json` is copied to `~/.claude/settings.json`
129
+ 3. The active profile is updated in `state.json`
130
+
131
+ This ensures you never lose your current settings when switching.
132
+
133
+ ## Requirements
134
+
135
+ - Node.js 18 or higher
136
+ - Claude Code installed (with `~/.claude/settings.json` present)
137
+
138
+ ## First-Time Setup
139
+
140
+ If you don't have a `~/.claude/settings.json` file, you'll see this error:
141
+
142
+ ```
143
+ No ~/.claude/settings.json found. Run Claude Code once to generate it,
144
+ or run the setup script provided by your provider (e.g., Z.ai).
145
+ ```
146
+
147
+ Simply run Claude Code once to generate the initial settings file, then you can start using `cc-switch`.
148
+
149
+ ## Profile Name Rules
150
+
151
+ Profile names must:
152
+ - Not be empty
153
+ - Only contain letters, numbers, hyphens, and underscores
154
+ - Not contain path separators (`/`, `\`, `..`)
155
+
156
+ ## Error Handling
157
+
158
+ `cc-switch` provides clear error messages for common issues:
159
+
160
+ - Missing Claude settings file
161
+ - Profile not found
162
+ - Attempting to delete the active profile
163
+ - Invalid profile names
164
+ - Permission errors
165
+
166
+ All operations use atomic file writes to prevent data loss.
167
+
168
+ ## License
169
+
170
+ [MIT](LICENSE)
package/dist/index.js ADDED
@@ -0,0 +1,399 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+ import * as clack from "@clack/prompts";
6
+
7
+ // src/profiles.ts
8
+ import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, rm, readdir, rename as fsRename, access } from "fs/promises";
9
+ import { dirname as dirname2 } from "path";
10
+
11
+ // src/paths.ts
12
+ import { homedir } from "os";
13
+ import { join } from "path";
14
+ var CLAUDE_DIR = join(homedir(), ".claude");
15
+ var CLAUDE_SETTINGS = join(CLAUDE_DIR, "settings.json");
16
+ var CC_SWITCH_DIR = join(homedir(), ".cc-switch");
17
+ var PROFILES_DIR = join(CC_SWITCH_DIR, "profiles");
18
+ var STATE_FILE = join(CC_SWITCH_DIR, "state.json");
19
+ function getProfileDir(profileName) {
20
+ return join(PROFILES_DIR, profileName);
21
+ }
22
+ function getProfileSettings(profileName) {
23
+ return join(getProfileDir(profileName), "settings.json");
24
+ }
25
+
26
+ // src/state.ts
27
+ import { readFile, writeFile, mkdir } from "fs/promises";
28
+ import { dirname } from "path";
29
+ var DEFAULT_STATE = {
30
+ activeProfile: "default",
31
+ lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString()
32
+ };
33
+ async function readState() {
34
+ try {
35
+ const data = await readFile(STATE_FILE, "utf-8");
36
+ return JSON.parse(data);
37
+ } catch (error) {
38
+ if (error.code === "ENOENT") {
39
+ return DEFAULT_STATE;
40
+ }
41
+ throw error;
42
+ }
43
+ }
44
+ async function writeState(state) {
45
+ await mkdir(dirname(STATE_FILE), { recursive: true });
46
+ const tempFile = `${STATE_FILE}.tmp`;
47
+ await writeFile(tempFile, JSON.stringify(state, null, 2), "utf-8");
48
+ await writeFile(STATE_FILE, JSON.stringify(state, null, 2), "utf-8");
49
+ }
50
+ async function updateState(updates) {
51
+ const currentState = await readState();
52
+ const newState = {
53
+ ...currentState,
54
+ ...updates,
55
+ lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString()
56
+ };
57
+ await writeState(newState);
58
+ return newState;
59
+ }
60
+
61
+ // src/validation.ts
62
+ function validateProfileName(name) {
63
+ if (!name || name.trim().length === 0) {
64
+ return { valid: false, error: "Profile name cannot be empty" };
65
+ }
66
+ if (name.includes("..") || name.includes("/") || name.includes("\\")) {
67
+ return { valid: false, error: 'Profile name cannot contain "..", "/", or "\\"' };
68
+ }
69
+ if (!/^[A-Za-z0-9-_]+$/.test(name)) {
70
+ return { valid: false, error: "Profile name can only contain letters, numbers, hyphens, and underscores" };
71
+ }
72
+ return { valid: true };
73
+ }
74
+
75
+ // src/profiles.ts
76
+ async function checkClaudeSettings() {
77
+ try {
78
+ await access(CLAUDE_SETTINGS);
79
+ } catch {
80
+ throw new Error(
81
+ "No ~/.claude/settings.json found. Run Claude Code once to generate it, or run the setup script provided by your provider (e.g., Z.ai)."
82
+ );
83
+ }
84
+ }
85
+ async function listProfiles() {
86
+ const state = await readState();
87
+ try {
88
+ await mkdir2(PROFILES_DIR, { recursive: true });
89
+ const entries = await readdir(PROFILES_DIR, { withFileTypes: true });
90
+ const profiles = entries.filter((entry) => entry.isDirectory()).map((entry) => ({
91
+ name: entry.name,
92
+ isActive: entry.name === state.activeProfile
93
+ }));
94
+ return profiles.sort((a, b) => {
95
+ if (a.isActive) return -1;
96
+ if (b.isActive) return 1;
97
+ return a.name.localeCompare(b.name);
98
+ });
99
+ } catch (error) {
100
+ if (error.code === "ENOENT") {
101
+ return [];
102
+ }
103
+ throw error;
104
+ }
105
+ }
106
+ async function profileExists(profileName) {
107
+ try {
108
+ await access(getProfileDir(profileName));
109
+ return true;
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
114
+ async function mirrorSettings(profileName) {
115
+ const profileSettings = getProfileSettings(profileName);
116
+ await mkdir2(dirname2(profileSettings), { recursive: true });
117
+ const currentSettings = await readFile2(CLAUDE_SETTINGS, "utf-8");
118
+ const tempFile = `${profileSettings}.tmp`;
119
+ await writeFile2(tempFile, currentSettings, "utf-8");
120
+ await fsRename(tempFile, profileSettings);
121
+ }
122
+ async function switchProfile(targetProfile) {
123
+ const validation = validateProfileName(targetProfile);
124
+ if (!validation.valid) {
125
+ throw new Error(validation.error);
126
+ }
127
+ await checkClaudeSettings();
128
+ if (!await profileExists(targetProfile)) {
129
+ throw new Error(`Profile "${targetProfile}" does not exist`);
130
+ }
131
+ const state = await readState();
132
+ const activeProfile = state.activeProfile;
133
+ await mirrorSettings(activeProfile);
134
+ if (targetProfile === activeProfile) {
135
+ throw new Error(`Profile "${targetProfile}" is already active`);
136
+ }
137
+ const targetSettings = getProfileSettings(targetProfile);
138
+ const newSettings = await readFile2(targetSettings, "utf-8");
139
+ const tempFile = `${CLAUDE_SETTINGS}.tmp`;
140
+ await writeFile2(tempFile, newSettings, "utf-8");
141
+ await fsRename(tempFile, CLAUDE_SETTINGS);
142
+ await updateState({ activeProfile: targetProfile });
143
+ }
144
+ async function createProfile(profileName) {
145
+ const validation = validateProfileName(profileName);
146
+ if (!validation.valid) {
147
+ throw new Error(validation.error);
148
+ }
149
+ await checkClaudeSettings();
150
+ if (await profileExists(profileName)) {
151
+ throw new Error(`Profile "${profileName}" already exists`);
152
+ }
153
+ const profileSettings = getProfileSettings(profileName);
154
+ await mkdir2(dirname2(profileSettings), { recursive: true });
155
+ const currentSettings = await readFile2(CLAUDE_SETTINGS, "utf-8");
156
+ const tempFile = `${profileSettings}.tmp`;
157
+ await writeFile2(tempFile, currentSettings, "utf-8");
158
+ await fsRename(tempFile, profileSettings);
159
+ }
160
+ async function deleteProfile(profileName) {
161
+ const validation = validateProfileName(profileName);
162
+ if (!validation.valid) {
163
+ throw new Error(validation.error);
164
+ }
165
+ if (!await profileExists(profileName)) {
166
+ throw new Error(`Profile "${profileName}" does not exist`);
167
+ }
168
+ const state = await readState();
169
+ if (profileName === state.activeProfile) {
170
+ throw new Error(`Cannot delete active profile "${profileName}". Switch to another profile first.`);
171
+ }
172
+ await rm(getProfileDir(profileName), { recursive: true, force: true });
173
+ }
174
+ async function renameProfile(oldName, newName) {
175
+ const oldValidation = validateProfileName(oldName);
176
+ if (!oldValidation.valid) {
177
+ throw new Error(`Invalid old profile name: ${oldValidation.error}`);
178
+ }
179
+ const newValidation = validateProfileName(newName);
180
+ if (!newValidation.valid) {
181
+ throw new Error(`Invalid new profile name: ${newValidation.error}`);
182
+ }
183
+ if (!await profileExists(oldName)) {
184
+ throw new Error(`Profile "${oldName}" does not exist`);
185
+ }
186
+ if (await profileExists(newName)) {
187
+ throw new Error(`Profile "${newName}" already exists`);
188
+ }
189
+ await fsRename(getProfileDir(oldName), getProfileDir(newName));
190
+ const state = await readState();
191
+ if (oldName === state.activeProfile) {
192
+ await updateState({ activeProfile: newName });
193
+ }
194
+ }
195
+
196
+ // src/index.ts
197
+ var program = new Command();
198
+ program.name("cc-switch").description("Profile manager for Claude Code settings").version("1.0.0");
199
+ program.command("switch <name>").description("Switch to a different profile").action(async (name) => {
200
+ try {
201
+ await switchProfile(name);
202
+ console.log(`Switched to profile "${name}"`);
203
+ process.exit(0);
204
+ } catch (error) {
205
+ console.error(`Error: ${error.message}`);
206
+ process.exit(1);
207
+ }
208
+ });
209
+ program.command("create <name>").description("Create a new profile from current settings").action(async (name) => {
210
+ try {
211
+ await createProfile(name);
212
+ console.log(`Created profile "${name}"`);
213
+ process.exit(0);
214
+ } catch (error) {
215
+ console.error(`Error: ${error.message}`);
216
+ process.exit(1);
217
+ }
218
+ });
219
+ program.command("delete <name>").description("Delete a profile").action(async (name) => {
220
+ try {
221
+ await deleteProfile(name);
222
+ console.log(`Deleted profile "${name}"`);
223
+ process.exit(0);
224
+ } catch (error) {
225
+ console.error(`Error: ${error.message}`);
226
+ process.exit(1);
227
+ }
228
+ });
229
+ program.command("rename <oldName> <newName>").description("Rename a profile").action(async (oldName, newName) => {
230
+ try {
231
+ await renameProfile(oldName, newName);
232
+ console.log(`Renamed profile "${oldName}" to "${newName}"`);
233
+ process.exit(0);
234
+ } catch (error) {
235
+ console.error(`Error: ${error.message}`);
236
+ process.exit(1);
237
+ }
238
+ });
239
+ program.command("list").description("List all profiles").action(async () => {
240
+ try {
241
+ const profiles = await listProfiles();
242
+ if (profiles.length === 0) {
243
+ console.log("No profiles found");
244
+ process.exit(0);
245
+ }
246
+ console.log("\nProfiles:");
247
+ for (const profile of profiles) {
248
+ const marker = profile.isActive ? " (active)" : "";
249
+ console.log(` ${profile.name}${marker}`);
250
+ }
251
+ console.log();
252
+ process.exit(0);
253
+ } catch (error) {
254
+ console.error(`Error: ${error.message}`);
255
+ process.exit(1);
256
+ }
257
+ });
258
+ async function interactiveMode() {
259
+ clack.intro("cc-switch - Claude Code Profile Manager");
260
+ try {
261
+ const profiles = await listProfiles();
262
+ if (profiles.length === 0) {
263
+ clack.outro("No profiles found. Create one with: cc-switch create <name>");
264
+ process.exit(0);
265
+ }
266
+ const action = await clack.select({
267
+ message: "What would you like to do?",
268
+ options: [
269
+ { value: "switch", label: "Switch profile" },
270
+ { value: "create", label: "Create new profile" },
271
+ { value: "delete", label: "Delete profile" },
272
+ { value: "rename", label: "Rename profile" },
273
+ { value: "list", label: "List profiles" }
274
+ ]
275
+ });
276
+ if (clack.isCancel(action)) {
277
+ clack.cancel("Operation cancelled");
278
+ process.exit(0);
279
+ }
280
+ switch (action) {
281
+ case "switch": {
282
+ const profileChoices = profiles.map((p) => ({
283
+ value: p.name,
284
+ label: p.isActive ? `${p.name} (active)` : p.name
285
+ }));
286
+ const selectedProfile = await clack.select({
287
+ message: "Select profile to switch to:",
288
+ options: profileChoices
289
+ });
290
+ if (clack.isCancel(selectedProfile)) {
291
+ clack.cancel("Operation cancelled");
292
+ process.exit(0);
293
+ }
294
+ await switchProfile(selectedProfile);
295
+ clack.outro(`Switched to profile "${selectedProfile}"`);
296
+ break;
297
+ }
298
+ case "create": {
299
+ const profileName = await clack.text({
300
+ message: "Enter new profile name:",
301
+ validate: (value) => {
302
+ if (!value) return "Profile name is required";
303
+ if (!/^[A-Za-z0-9-_]+$/.test(value)) {
304
+ return "Profile name can only contain letters, numbers, hyphens, and underscores";
305
+ }
306
+ }
307
+ });
308
+ if (clack.isCancel(profileName)) {
309
+ clack.cancel("Operation cancelled");
310
+ process.exit(0);
311
+ }
312
+ await createProfile(profileName);
313
+ clack.outro(`Created profile "${profileName}"`);
314
+ break;
315
+ }
316
+ case "delete": {
317
+ const nonActiveProfiles = profiles.filter((p) => !p.isActive);
318
+ if (nonActiveProfiles.length === 0) {
319
+ clack.outro("No profiles available to delete. Cannot delete the active profile.");
320
+ process.exit(0);
321
+ }
322
+ const profileChoices = nonActiveProfiles.map((p) => ({
323
+ value: p.name,
324
+ label: p.name
325
+ }));
326
+ const selectedProfile = await clack.select({
327
+ message: "Select profile to delete:",
328
+ options: profileChoices
329
+ });
330
+ if (clack.isCancel(selectedProfile)) {
331
+ clack.cancel("Operation cancelled");
332
+ process.exit(0);
333
+ }
334
+ const confirmDelete = await clack.confirm({
335
+ message: `Are you sure you want to delete profile "${selectedProfile}"?`
336
+ });
337
+ if (clack.isCancel(confirmDelete)) {
338
+ clack.cancel("Operation cancelled");
339
+ process.exit(0);
340
+ }
341
+ if (!confirmDelete) {
342
+ clack.outro("Deletion cancelled");
343
+ process.exit(0);
344
+ }
345
+ await deleteProfile(selectedProfile);
346
+ clack.outro(`Deleted profile "${selectedProfile}"`);
347
+ break;
348
+ }
349
+ case "rename": {
350
+ const profileChoices = profiles.map((p) => ({
351
+ value: p.name,
352
+ label: p.isActive ? `${p.name} (active)` : p.name
353
+ }));
354
+ const oldName = await clack.select({
355
+ message: "Select profile to rename:",
356
+ options: profileChoices
357
+ });
358
+ if (clack.isCancel(oldName)) {
359
+ clack.cancel("Operation cancelled");
360
+ process.exit(0);
361
+ }
362
+ const newName = await clack.text({
363
+ message: "Enter new profile name:",
364
+ validate: (value) => {
365
+ if (!value) return "Profile name is required";
366
+ if (!/^[A-Za-z0-9-_]+$/.test(value)) {
367
+ return "Profile name can only contain letters, numbers, hyphens, and underscores";
368
+ }
369
+ }
370
+ });
371
+ if (clack.isCancel(newName)) {
372
+ clack.cancel("Operation cancelled");
373
+ process.exit(0);
374
+ }
375
+ await renameProfile(oldName, newName);
376
+ clack.outro(`Renamed profile "${oldName}" to "${newName}"`);
377
+ break;
378
+ }
379
+ case "list": {
380
+ console.log("\nProfiles:");
381
+ for (const profile of profiles) {
382
+ const marker = profile.isActive ? " (active)" : "";
383
+ console.log(` ${profile.name}${marker}`);
384
+ }
385
+ console.log();
386
+ clack.outro("Profile list complete");
387
+ break;
388
+ }
389
+ }
390
+ process.exit(0);
391
+ } catch (error) {
392
+ clack.outro(`Error: ${error.message}`);
393
+ process.exit(1);
394
+ }
395
+ }
396
+ program.parse();
397
+ if (!process.argv.slice(2).length) {
398
+ interactiveMode();
399
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@aravhawk/cc-switch",
3
+ "version": "1.0.1",
4
+ "description": "Profile manager for Claude Code settings",
5
+ "type": "module",
6
+ "bin": {
7
+ "cc-switch": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "keywords": [
15
+ "claude",
16
+ "claude-code",
17
+ "cli",
18
+ "profile-manager",
19
+ "settings"
20
+ ],
21
+ "author": "Arav",
22
+ "license": "MIT",
23
+ "engines": {
24
+ "node": ">=18.0.0"
25
+ },
26
+ "dependencies": {
27
+ "@clack/prompts": "^1.0.0",
28
+ "commander": "^12.1.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^20.17.10",
32
+ "tsup": "^8.3.5",
33
+ "typescript": "^5.7.3"
34
+ },
35
+ "scripts": {
36
+ "build": "tsup",
37
+ "dev": "tsup --watch",
38
+ "typecheck": "tsc --noEmit"
39
+ }
40
+ }