@aravhawk/cc-switch 1.1.0 → 2.0.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/README.md +42 -9
- package/dist/index.js +179 -87
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ cc-switch
|
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
This will present a menu with options to:
|
|
28
|
+
- Show the current profile
|
|
28
29
|
- Switch between profiles
|
|
29
30
|
- Create new profiles
|
|
30
31
|
- Delete profiles
|
|
@@ -33,15 +34,22 @@ This will present a menu with options to:
|
|
|
33
34
|
|
|
34
35
|
### Command Line Mode
|
|
35
36
|
|
|
37
|
+
Actions use flags. Use a positional profile name for quick switching.
|
|
38
|
+
|
|
36
39
|
#### Switch Profile
|
|
37
40
|
|
|
38
41
|
```bash
|
|
39
|
-
cc-switch
|
|
42
|
+
cc-switch <profile-name>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Or explicit:
|
|
46
|
+
```bash
|
|
47
|
+
cc-switch --switch <profile-name>
|
|
40
48
|
```
|
|
41
49
|
|
|
42
50
|
Example:
|
|
43
51
|
```bash
|
|
44
|
-
cc-switch
|
|
52
|
+
cc-switch work
|
|
45
53
|
```
|
|
46
54
|
|
|
47
55
|
#### Create Profile
|
|
@@ -49,12 +57,12 @@ cc-switch switch work
|
|
|
49
57
|
Create a new profile from your current `~/.claude/settings.json`:
|
|
50
58
|
|
|
51
59
|
```bash
|
|
52
|
-
cc-switch create <profile-name>
|
|
60
|
+
cc-switch --create <profile-name>
|
|
53
61
|
```
|
|
54
62
|
|
|
55
63
|
Example:
|
|
56
64
|
```bash
|
|
57
|
-
cc-switch create personal
|
|
65
|
+
cc-switch --create personal
|
|
58
66
|
```
|
|
59
67
|
|
|
60
68
|
#### Delete Profile
|
|
@@ -62,12 +70,12 @@ cc-switch create personal
|
|
|
62
70
|
Delete an existing profile (cannot delete the active profile):
|
|
63
71
|
|
|
64
72
|
```bash
|
|
65
|
-
cc-switch delete <profile-name>
|
|
73
|
+
cc-switch --delete <profile-name>
|
|
66
74
|
```
|
|
67
75
|
|
|
68
76
|
Example:
|
|
69
77
|
```bash
|
|
70
|
-
cc-switch delete old-config
|
|
78
|
+
cc-switch --delete old-config
|
|
71
79
|
```
|
|
72
80
|
|
|
73
81
|
#### Rename Profile
|
|
@@ -75,12 +83,17 @@ cc-switch delete old-config
|
|
|
75
83
|
Rename an existing profile:
|
|
76
84
|
|
|
77
85
|
```bash
|
|
78
|
-
cc-switch rename <old-name> <new-name>
|
|
86
|
+
cc-switch --rename <old-name> <new-name>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Or:
|
|
90
|
+
```bash
|
|
91
|
+
cc-switch --rename <old-name> --to <new-name>
|
|
79
92
|
```
|
|
80
93
|
|
|
81
94
|
Example:
|
|
82
95
|
```bash
|
|
83
|
-
cc-switch rename work work-2024
|
|
96
|
+
cc-switch --rename work work-2024
|
|
84
97
|
```
|
|
85
98
|
|
|
86
99
|
#### List Profiles
|
|
@@ -88,9 +101,28 @@ cc-switch rename work work-2024
|
|
|
88
101
|
List all available profiles:
|
|
89
102
|
|
|
90
103
|
```bash
|
|
91
|
-
cc-switch list
|
|
104
|
+
cc-switch --list
|
|
92
105
|
```
|
|
93
106
|
|
|
107
|
+
#### Current Profile
|
|
108
|
+
|
|
109
|
+
Show the active profile:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
cc-switch --current
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
#### Help and Version
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
cc-switch help
|
|
119
|
+
cc-switch --help
|
|
120
|
+
cc-switch version
|
|
121
|
+
cc-switch --version
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Reserved profile names: `help` and `version`.
|
|
125
|
+
|
|
94
126
|
## How It Works
|
|
95
127
|
|
|
96
128
|
`cc-switch` manages multiple Claude Code profiles by storing copies of your `settings.json` file in separate profile directories.
|
|
@@ -152,6 +184,7 @@ Profile names must:
|
|
|
152
184
|
- Not be empty
|
|
153
185
|
- Only contain letters, numbers, hyphens, and underscores
|
|
154
186
|
- Not contain path separators (`/`, `\`, `..`)
|
|
187
|
+
- Not be reserved words (`help`, `version`)
|
|
155
188
|
|
|
156
189
|
## Error Handling
|
|
157
190
|
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import * as clack from "@clack/prompts";
|
|
6
|
+
import { createRequire } from "module";
|
|
6
7
|
|
|
7
8
|
// src/profiles.ts
|
|
8
9
|
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, rm, readdir, rename as fsRename, access } from "fs/promises";
|
|
@@ -59,14 +60,22 @@ async function updateState(updates) {
|
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
// src/validation.ts
|
|
63
|
+
var RESERVED_PROFILE_NAMES = /* @__PURE__ */ new Set(["help", "version"]);
|
|
62
64
|
function validateProfileName(name) {
|
|
63
|
-
|
|
65
|
+
const trimmedName = name.trim();
|
|
66
|
+
if (!trimmedName) {
|
|
64
67
|
return { valid: false, error: "Profile name cannot be empty" };
|
|
65
68
|
}
|
|
66
|
-
if (name
|
|
69
|
+
if (name !== trimmedName) {
|
|
70
|
+
return { valid: false, error: "Profile name cannot contain leading or trailing spaces" };
|
|
71
|
+
}
|
|
72
|
+
if (RESERVED_PROFILE_NAMES.has(trimmedName.toLowerCase())) {
|
|
73
|
+
return { valid: false, error: `Profile name "${trimmedName}" is reserved. Choose a different name.` };
|
|
74
|
+
}
|
|
75
|
+
if (trimmedName.includes("..") || trimmedName.includes("/") || trimmedName.includes("\\")) {
|
|
67
76
|
return { valid: false, error: 'Profile name cannot contain "..", "/", or "\\"' };
|
|
68
77
|
}
|
|
69
|
-
if (!/^[A-Za-z0-9-_]+$/.test(
|
|
78
|
+
if (!/^[A-Za-z0-9-_]+$/.test(trimmedName)) {
|
|
70
79
|
return { valid: false, error: "Profile name can only contain letters, numbers, hyphens, and underscores" };
|
|
71
80
|
}
|
|
72
81
|
return { valid: true };
|
|
@@ -111,6 +120,11 @@ async function profileExists(profileName) {
|
|
|
111
120
|
return false;
|
|
112
121
|
}
|
|
113
122
|
}
|
|
123
|
+
async function getActiveProfileStatus() {
|
|
124
|
+
const state = await readState();
|
|
125
|
+
const exists = await profileExists(state.activeProfile);
|
|
126
|
+
return { name: state.activeProfile, exists };
|
|
127
|
+
}
|
|
114
128
|
async function mirrorSettings(profileName) {
|
|
115
129
|
const profileSettings = getProfileSettings(profileName);
|
|
116
130
|
await mkdir2(dirname2(profileSettings), { recursive: true });
|
|
@@ -194,78 +208,59 @@ async function renameProfile(oldName, newName) {
|
|
|
194
208
|
}
|
|
195
209
|
|
|
196
210
|
// src/index.ts
|
|
197
|
-
var
|
|
198
|
-
|
|
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) => {
|
|
211
|
+
var require2 = createRequire(import.meta.url);
|
|
212
|
+
function getCliVersion() {
|
|
210
213
|
try {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
const pkg = require2("../package.json");
|
|
215
|
+
if (pkg && typeof pkg.version === "string") {
|
|
216
|
+
return pkg.version;
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
return "unknown";
|
|
217
220
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
221
|
+
return "unknown";
|
|
222
|
+
}
|
|
223
|
+
var cliVersion = getCliVersion();
|
|
224
|
+
var program = new Command();
|
|
225
|
+
function formatActiveProfileLine(status) {
|
|
226
|
+
const missingSuffix = status.exists ? "" : " (missing)";
|
|
227
|
+
return `Current profile: "${status.name}"${missingSuffix}`;
|
|
228
|
+
}
|
|
229
|
+
function getProfileValidationMessage(value) {
|
|
230
|
+
const validation = validateProfileName(value ?? "");
|
|
231
|
+
if (!validation.valid) {
|
|
232
|
+
return validation.error ?? "Profile name is invalid";
|
|
227
233
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
console.
|
|
236
|
-
|
|
234
|
+
return void 0;
|
|
235
|
+
}
|
|
236
|
+
async function showProfileList(profiles) {
|
|
237
|
+
const activeStatus = await getActiveProfileStatus();
|
|
238
|
+
const resolvedProfiles = profiles ?? await listProfiles();
|
|
239
|
+
console.log(formatActiveProfileLine(activeStatus));
|
|
240
|
+
if (resolvedProfiles.length === 0) {
|
|
241
|
+
console.log("No profiles found");
|
|
242
|
+
return;
|
|
237
243
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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);
|
|
244
|
+
console.log("\nProfiles:");
|
|
245
|
+
for (const profile of resolvedProfiles) {
|
|
246
|
+
const marker = profile.isActive ? " (active)" : "";
|
|
247
|
+
console.log(` ${profile.name}${marker}`);
|
|
256
248
|
}
|
|
257
|
-
|
|
249
|
+
console.log();
|
|
250
|
+
}
|
|
251
|
+
program.name("cc-switch").description("Profile manager for Claude Code settings").version(cliVersion, "-V, --version", "output the version number").argument("[profile]", "Profile name to switch to").option("--switch <name>", "Switch to a different profile").option("--create <name>", "Create a new profile from current settings").option("--delete <name>", "Delete a profile").option("--rename <names...>", "Rename a profile").option("--to <name>", "New name for rename").option("--current", "Show the current active profile").option("--list", "List all profiles");
|
|
258
252
|
async function interactiveMode() {
|
|
259
253
|
clack.intro("cc-switch - Claude Code Profile Manager");
|
|
260
254
|
try {
|
|
261
255
|
const profiles = await listProfiles();
|
|
262
256
|
if (profiles.length === 0) {
|
|
263
|
-
clack.outro("No profiles found. Create one with: cc-switch create <name>");
|
|
257
|
+
clack.outro("No profiles found. Create one with: cc-switch --create <name>");
|
|
264
258
|
process.exit(0);
|
|
265
259
|
}
|
|
266
260
|
const action = await clack.select({
|
|
267
261
|
message: "What would you like to do?",
|
|
268
262
|
options: [
|
|
263
|
+
{ value: "current", label: "Show current profile" },
|
|
269
264
|
{ value: "switch", label: "Switch profile" },
|
|
270
265
|
{ value: "create", label: "Create new profile" },
|
|
271
266
|
{ value: "delete", label: "Delete profile" },
|
|
@@ -295,14 +290,16 @@ async function interactiveMode() {
|
|
|
295
290
|
clack.outro(`Switched to profile "${selectedProfile}"`);
|
|
296
291
|
break;
|
|
297
292
|
}
|
|
293
|
+
case "current": {
|
|
294
|
+
const activeStatus = await getActiveProfileStatus();
|
|
295
|
+
clack.outro(formatActiveProfileLine(activeStatus));
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
298
|
case "create": {
|
|
299
299
|
const profileName = await clack.text({
|
|
300
300
|
message: "Enter new profile name:",
|
|
301
301
|
validate: (value) => {
|
|
302
|
-
|
|
303
|
-
if (!/^[A-Za-z0-9-_]+$/.test(value)) {
|
|
304
|
-
return "Profile name can only contain letters, numbers, hyphens, and underscores";
|
|
305
|
-
}
|
|
302
|
+
return getProfileValidationMessage(value);
|
|
306
303
|
}
|
|
307
304
|
});
|
|
308
305
|
if (clack.isCancel(profileName)) {
|
|
@@ -362,10 +359,7 @@ async function interactiveMode() {
|
|
|
362
359
|
const newName = await clack.text({
|
|
363
360
|
message: "Enter new profile name:",
|
|
364
361
|
validate: (value) => {
|
|
365
|
-
|
|
366
|
-
if (!/^[A-Za-z0-9-_]+$/.test(value)) {
|
|
367
|
-
return "Profile name can only contain letters, numbers, hyphens, and underscores";
|
|
368
|
-
}
|
|
362
|
+
return getProfileValidationMessage(value);
|
|
369
363
|
}
|
|
370
364
|
});
|
|
371
365
|
if (clack.isCancel(newName)) {
|
|
@@ -377,12 +371,7 @@ async function interactiveMode() {
|
|
|
377
371
|
break;
|
|
378
372
|
}
|
|
379
373
|
case "list": {
|
|
380
|
-
|
|
381
|
-
for (const profile of profiles) {
|
|
382
|
-
const marker = profile.isActive ? " (active)" : "";
|
|
383
|
-
console.log(` ${profile.name}${marker}`);
|
|
384
|
-
}
|
|
385
|
-
console.log();
|
|
374
|
+
await showProfileList(profiles);
|
|
386
375
|
clack.outro("Profile list complete");
|
|
387
376
|
break;
|
|
388
377
|
}
|
|
@@ -393,24 +382,127 @@ async function interactiveMode() {
|
|
|
393
382
|
process.exit(1);
|
|
394
383
|
}
|
|
395
384
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
385
|
+
async function runCli() {
|
|
386
|
+
const rawArgs = process.argv.slice(2);
|
|
387
|
+
if (!rawArgs.length) {
|
|
388
|
+
await interactiveMode();
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (rawArgs.length === 1) {
|
|
392
|
+
const singleArg = rawArgs[0];
|
|
393
|
+
if (singleArg === "help") {
|
|
394
|
+
program.outputHelp();
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (singleArg === "version") {
|
|
398
|
+
console.log(cliVersion);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
program.parse();
|
|
403
|
+
const opts = program.opts();
|
|
404
|
+
const profileArg = program.args[0];
|
|
405
|
+
const actionFlags = [];
|
|
406
|
+
if (opts.switch) actionFlags.push("switch");
|
|
407
|
+
if (opts.create) actionFlags.push("create");
|
|
408
|
+
if (opts.delete) actionFlags.push("delete");
|
|
409
|
+
if (opts.rename || opts.to) actionFlags.push("rename");
|
|
410
|
+
if (opts.current) actionFlags.push("current");
|
|
411
|
+
if (opts.list) actionFlags.push("list");
|
|
412
|
+
if (actionFlags.length > 1) {
|
|
413
|
+
console.error("Error: Please provide only one action flag at a time.");
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
if (profileArg && actionFlags.length > 0) {
|
|
417
|
+
console.error("Error: Do not combine a profile name with action flags.");
|
|
418
|
+
process.exit(1);
|
|
419
|
+
}
|
|
420
|
+
if (actionFlags.length === 0) {
|
|
421
|
+
if (!profileArg) {
|
|
422
|
+
program.outputHelp();
|
|
423
|
+
process.exit(0);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
402
426
|
try {
|
|
403
|
-
await switchProfile(
|
|
404
|
-
console.log(`Switched to profile "${
|
|
427
|
+
await switchProfile(profileArg);
|
|
428
|
+
console.log(`Switched to profile "${profileArg}"`);
|
|
405
429
|
process.exit(0);
|
|
406
430
|
} catch (error) {
|
|
407
431
|
console.error(`Error: ${error.message}`);
|
|
408
432
|
process.exit(1);
|
|
409
433
|
}
|
|
410
|
-
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const action = actionFlags[0];
|
|
437
|
+
try {
|
|
438
|
+
switch (action) {
|
|
439
|
+
case "switch": {
|
|
440
|
+
if (!opts.switch) {
|
|
441
|
+
throw new Error("Missing profile name for --switch");
|
|
442
|
+
}
|
|
443
|
+
await switchProfile(opts.switch);
|
|
444
|
+
console.log(`Switched to profile "${opts.switch}"`);
|
|
445
|
+
process.exit(0);
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
case "create": {
|
|
449
|
+
if (!opts.create) {
|
|
450
|
+
throw new Error("Missing profile name for --create");
|
|
451
|
+
}
|
|
452
|
+
await createProfile(opts.create);
|
|
453
|
+
console.log(`Created profile "${opts.create}"`);
|
|
454
|
+
process.exit(0);
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
case "delete": {
|
|
458
|
+
if (!opts.delete) {
|
|
459
|
+
throw new Error("Missing profile name for --delete");
|
|
460
|
+
}
|
|
461
|
+
await deleteProfile(opts.delete);
|
|
462
|
+
console.log(`Deleted profile "${opts.delete}"`);
|
|
463
|
+
process.exit(0);
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
case "rename": {
|
|
467
|
+
const renameArgs = opts.rename ?? [];
|
|
468
|
+
const renameTarget = opts.to;
|
|
469
|
+
if (!renameArgs.length && renameTarget) {
|
|
470
|
+
throw new Error("Missing old profile name for --rename");
|
|
471
|
+
}
|
|
472
|
+
if (!renameArgs.length) {
|
|
473
|
+
throw new Error("Missing profile name for --rename");
|
|
474
|
+
}
|
|
475
|
+
if (renameArgs.length > 2) {
|
|
476
|
+
throw new Error("Provide only two names for --rename");
|
|
477
|
+
}
|
|
478
|
+
if (renameArgs.length === 2 && renameTarget) {
|
|
479
|
+
throw new Error('Use either "--rename <old> <new>" or "--rename <old> --to <new>"');
|
|
480
|
+
}
|
|
481
|
+
const oldName = renameArgs[0];
|
|
482
|
+
const newName = renameArgs.length === 2 ? renameArgs[1] : renameTarget;
|
|
483
|
+
if (!newName) {
|
|
484
|
+
throw new Error("Missing new profile name for --rename");
|
|
485
|
+
}
|
|
486
|
+
await renameProfile(oldName, newName);
|
|
487
|
+
console.log(`Renamed profile "${oldName}" to "${newName}"`);
|
|
488
|
+
process.exit(0);
|
|
489
|
+
break;
|
|
490
|
+
}
|
|
491
|
+
case "current": {
|
|
492
|
+
const activeStatus = await getActiveProfileStatus();
|
|
493
|
+
console.log(formatActiveProfileLine(activeStatus));
|
|
494
|
+
process.exit(0);
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
case "list": {
|
|
498
|
+
await showProfileList();
|
|
499
|
+
process.exit(0);
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
} catch (error) {
|
|
504
|
+
console.error(`Error: ${error.message}`);
|
|
505
|
+
process.exit(1);
|
|
415
506
|
}
|
|
416
507
|
}
|
|
508
|
+
void runCli();
|