@aravhawk/cc-switch 1.1.1 → 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 +31 -12
  2. package/dist/index.js +165 -102
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -34,18 +34,20 @@ This will present a menu with options to:
34
34
 
35
35
  ### Command Line Mode
36
36
 
37
+ Actions use flags. Use a positional profile name for quick switching.
38
+
37
39
  #### Switch Profile
38
40
 
39
41
  ```bash
40
- cc-switch switch <profile-name>
42
+ cc-switch <profile-name>
41
43
  ```
42
44
 
43
- Example:
45
+ Or explicit:
44
46
  ```bash
45
- cc-switch switch work
47
+ cc-switch --switch <profile-name>
46
48
  ```
47
49
 
48
- Shorthand:
50
+ Example:
49
51
  ```bash
50
52
  cc-switch work
51
53
  ```
@@ -55,12 +57,12 @@ cc-switch work
55
57
  Create a new profile from your current `~/.claude/settings.json`:
56
58
 
57
59
  ```bash
58
- cc-switch create <profile-name>
60
+ cc-switch --create <profile-name>
59
61
  ```
60
62
 
61
63
  Example:
62
64
  ```bash
63
- cc-switch create personal
65
+ cc-switch --create personal
64
66
  ```
65
67
 
66
68
  #### Delete Profile
@@ -68,12 +70,12 @@ cc-switch create personal
68
70
  Delete an existing profile (cannot delete the active profile):
69
71
 
70
72
  ```bash
71
- cc-switch delete <profile-name>
73
+ cc-switch --delete <profile-name>
72
74
  ```
73
75
 
74
76
  Example:
75
77
  ```bash
76
- cc-switch delete old-config
78
+ cc-switch --delete old-config
77
79
  ```
78
80
 
79
81
  #### Rename Profile
@@ -81,12 +83,17 @@ cc-switch delete old-config
81
83
  Rename an existing profile:
82
84
 
83
85
  ```bash
84
- 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>
85
92
  ```
86
93
 
87
94
  Example:
88
95
  ```bash
89
- cc-switch rename work work-2024
96
+ cc-switch --rename work work-2024
90
97
  ```
91
98
 
92
99
  #### List Profiles
@@ -94,7 +101,7 @@ cc-switch rename work work-2024
94
101
  List all available profiles:
95
102
 
96
103
  ```bash
97
- cc-switch list
104
+ cc-switch --list
98
105
  ```
99
106
 
100
107
  #### Current Profile
@@ -102,9 +109,20 @@ cc-switch list
102
109
  Show the active profile:
103
110
 
104
111
  ```bash
105
- cc-switch current
112
+ cc-switch --current
106
113
  ```
107
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
+
108
126
  ## How It Works
109
127
 
110
128
  `cc-switch` manages multiple Claude Code profiles by storing copies of your `settings.json` file in separate profile directories.
@@ -166,6 +184,7 @@ Profile names must:
166
184
  - Not be empty
167
185
  - Only contain letters, numbers, hyphens, and underscores
168
186
  - Not contain path separators (`/`, `\`, `..`)
187
+ - Not be reserved words (`help`, `version`)
169
188
 
170
189
  ## Error Handling
171
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 };
@@ -199,89 +208,53 @@ async function renameProfile(oldName, newName) {
199
208
  }
200
209
 
201
210
  // src/index.ts
211
+ var require2 = createRequire(import.meta.url);
212
+ function getCliVersion() {
213
+ try {
214
+ const pkg = require2("../package.json");
215
+ if (pkg && typeof pkg.version === "string") {
216
+ return pkg.version;
217
+ }
218
+ } catch {
219
+ return "unknown";
220
+ }
221
+ return "unknown";
222
+ }
223
+ var cliVersion = getCliVersion();
202
224
  var program = new Command();
203
225
  function formatActiveProfileLine(status) {
204
226
  const missingSuffix = status.exists ? "" : " (missing)";
205
227
  return `Current profile: "${status.name}"${missingSuffix}`;
206
228
  }
207
- program.name("cc-switch").description("Profile manager for Claude Code settings").version("1.0.0");
208
- program.command("switch <name>").description("Switch to a different profile").action(async (name) => {
209
- try {
210
- await switchProfile(name);
211
- console.log(`Switched to profile "${name}"`);
212
- process.exit(0);
213
- } catch (error) {
214
- console.error(`Error: ${error.message}`);
215
- process.exit(1);
216
- }
217
- });
218
- program.command("create <name>").description("Create a new profile from current settings").action(async (name) => {
219
- try {
220
- await createProfile(name);
221
- console.log(`Created profile "${name}"`);
222
- process.exit(0);
223
- } catch (error) {
224
- console.error(`Error: ${error.message}`);
225
- process.exit(1);
226
- }
227
- });
228
- program.command("delete <name>").description("Delete a profile").action(async (name) => {
229
- try {
230
- await deleteProfile(name);
231
- console.log(`Deleted profile "${name}"`);
232
- process.exit(0);
233
- } catch (error) {
234
- console.error(`Error: ${error.message}`);
235
- process.exit(1);
236
- }
237
- });
238
- program.command("rename <oldName> <newName>").description("Rename a profile").action(async (oldName, newName) => {
239
- try {
240
- await renameProfile(oldName, newName);
241
- console.log(`Renamed profile "${oldName}" to "${newName}"`);
242
- process.exit(0);
243
- } catch (error) {
244
- console.error(`Error: ${error.message}`);
245
- process.exit(1);
229
+ function getProfileValidationMessage(value) {
230
+ const validation = validateProfileName(value ?? "");
231
+ if (!validation.valid) {
232
+ return validation.error ?? "Profile name is invalid";
246
233
  }
247
- });
248
- program.command("current").description("Show the current active profile").action(async () => {
249
- try {
250
- const activeStatus = await getActiveProfileStatus();
251
- console.log(formatActiveProfileLine(activeStatus));
252
- process.exit(0);
253
- } catch (error) {
254
- console.error(`Error: ${error.message}`);
255
- 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;
256
243
  }
257
- });
258
- program.command("list").description("List all profiles").action(async () => {
259
- try {
260
- const profiles = await listProfiles();
261
- const activeStatus = await getActiveProfileStatus();
262
- console.log(formatActiveProfileLine(activeStatus));
263
- if (profiles.length === 0) {
264
- console.log("No profiles found");
265
- process.exit(0);
266
- }
267
- console.log("\nProfiles:");
268
- for (const profile of profiles) {
269
- const marker = profile.isActive ? " (active)" : "";
270
- console.log(` ${profile.name}${marker}`);
271
- }
272
- console.log();
273
- process.exit(0);
274
- } catch (error) {
275
- console.error(`Error: ${error.message}`);
276
- 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}`);
277
248
  }
278
- });
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");
279
252
  async function interactiveMode() {
280
253
  clack.intro("cc-switch - Claude Code Profile Manager");
281
254
  try {
282
255
  const profiles = await listProfiles();
283
256
  if (profiles.length === 0) {
284
- 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>");
285
258
  process.exit(0);
286
259
  }
287
260
  const action = await clack.select({
@@ -326,10 +299,7 @@ async function interactiveMode() {
326
299
  const profileName = await clack.text({
327
300
  message: "Enter new profile name:",
328
301
  validate: (value) => {
329
- if (!value) return "Profile name is required";
330
- if (!/^[A-Za-z0-9-_]+$/.test(value)) {
331
- return "Profile name can only contain letters, numbers, hyphens, and underscores";
332
- }
302
+ return getProfileValidationMessage(value);
333
303
  }
334
304
  });
335
305
  if (clack.isCancel(profileName)) {
@@ -389,10 +359,7 @@ async function interactiveMode() {
389
359
  const newName = await clack.text({
390
360
  message: "Enter new profile name:",
391
361
  validate: (value) => {
392
- if (!value) return "Profile name is required";
393
- if (!/^[A-Za-z0-9-_]+$/.test(value)) {
394
- return "Profile name can only contain letters, numbers, hyphens, and underscores";
395
- }
362
+ return getProfileValidationMessage(value);
396
363
  }
397
364
  });
398
365
  if (clack.isCancel(newName)) {
@@ -404,14 +371,7 @@ async function interactiveMode() {
404
371
  break;
405
372
  }
406
373
  case "list": {
407
- const activeStatus = await getActiveProfileStatus();
408
- console.log(formatActiveProfileLine(activeStatus));
409
- console.log("\nProfiles:");
410
- for (const profile of profiles) {
411
- const marker = profile.isActive ? " (active)" : "";
412
- console.log(` ${profile.name}${marker}`);
413
- }
414
- console.log();
374
+ await showProfileList(profiles);
415
375
  clack.outro("Profile list complete");
416
376
  break;
417
377
  }
@@ -422,24 +382,127 @@ async function interactiveMode() {
422
382
  process.exit(1);
423
383
  }
424
384
  }
425
- var rawArgs = process.argv.slice(2);
426
- var knownCommands = /* @__PURE__ */ new Set(["switch", "create", "delete", "rename", "list", "current"]);
427
- var firstArg = rawArgs[0];
428
- var isFlag = firstArg?.startsWith("-");
429
- if (rawArgs.length === 1 && firstArg && !knownCommands.has(firstArg) && !isFlag) {
430
- (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
+ }
431
426
  try {
432
- await switchProfile(firstArg);
433
- console.log(`Switched to profile "${firstArg}"`);
427
+ await switchProfile(profileArg);
428
+ console.log(`Switched to profile "${profileArg}"`);
434
429
  process.exit(0);
435
430
  } catch (error) {
436
431
  console.error(`Error: ${error.message}`);
437
432
  process.exit(1);
438
433
  }
439
- })();
440
- } else {
441
- program.parse();
442
- if (!rawArgs.length) {
443
- 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);
444
506
  }
445
507
  }
508
+ void runCli();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aravhawk/cc-switch",
3
- "version": "1.1.1",
3
+ "version": "2.0.0",
4
4
  "description": "Profile manager for Claude Code settings",
5
5
  "type": "module",
6
6
  "bin": {