@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.
Files changed (3) hide show
  1. package/README.md +42 -9
  2. package/dist/index.js +179 -87
  3. 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 switch <profile-name>
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 switch work
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
- if (!name || name.trim().length === 0) {
65
+ const trimmedName = name.trim();
66
+ if (!trimmedName) {
64
67
  return { valid: false, error: "Profile name cannot be empty" };
65
68
  }
66
- if (name.includes("..") || name.includes("/") || name.includes("\\")) {
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(name)) {
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 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) => {
211
+ var require2 = createRequire(import.meta.url);
212
+ function getCliVersion() {
210
213
  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);
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
- 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);
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
- 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);
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
- 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);
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
- 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
- }
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
- 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
- }
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
- 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();
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
- var rawArgs = process.argv.slice(2);
397
- var knownCommands = /* @__PURE__ */ new Set(["switch", "create", "delete", "rename", "list"]);
398
- var firstArg = rawArgs[0];
399
- var isFlag = firstArg?.startsWith("-");
400
- if (rawArgs.length === 1 && firstArg && !knownCommands.has(firstArg) && !isFlag) {
401
- (async () => {
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(firstArg);
404
- console.log(`Switched to profile "${firstArg}"`);
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
- } else {
412
- program.parse();
413
- if (!rawArgs.length) {
414
- interactiveMode();
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aravhawk/cc-switch",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "Profile manager for Claude Code settings",
5
5
  "type": "module",
6
6
  "bin": {