@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.
- package/LICENSE +21 -0
- package/README.md +170 -0
- package/dist/index.js +399 -0
- 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
|
+
}
|