@aravhawk/cc-switch 2.0.0 → 2.1.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 +18 -0
  2. package/dist/index.js +197 -7
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -65,6 +65,24 @@ Example:
65
65
  cc-switch --create personal
66
66
  ```
67
67
 
68
+ New profiles are activated immediately after creation.
69
+
70
+ #### Create From Provider Template
71
+
72
+ Create and apply provider defaults in one step (prompts for API key if omitted):
73
+
74
+ ```bash
75
+ cc-switch --create <profile-name> --template moonshot --api-key <your-key>
76
+ ```
77
+
78
+ Available templates:
79
+ - anthropic (claude) – no API key needed; leaves defaults in place
80
+ - moonshot (kimi)
81
+ - zai (glm)
82
+ - minimax
83
+
84
+ Templates set `ANTHROPIC_BASE_URL`, `ANTHROPIC_AUTH_TOKEN`, and default Haiku/Sonnet/Opus models, and remove any existing `ANTHROPIC_API_KEY`.
85
+
68
86
  #### Delete Profile
69
87
 
70
88
  Delete an existing profile (cannot delete the active profile):
package/dist/index.js CHANGED
@@ -82,6 +82,66 @@ function validateProfileName(name) {
82
82
  }
83
83
 
84
84
  // src/profiles.ts
85
+ var PROVIDER_TEMPLATES = [
86
+ {
87
+ name: "anthropic",
88
+ label: "Anthropic (Claude default)",
89
+ requiresApiKey: false,
90
+ aliases: ["claude"]
91
+ },
92
+ {
93
+ name: "moonshot",
94
+ label: "Moonshot (Kimi)",
95
+ requiresApiKey: true,
96
+ baseUrl: "https://api.kimi.com/coding/",
97
+ haikuModel: "kimi-for-coding",
98
+ sonnetModel: "kimi-for-coding",
99
+ opusModel: "kimi-for-coding",
100
+ aliases: ["kimi"]
101
+ },
102
+ {
103
+ name: "zai",
104
+ label: "Z.ai (GLM)",
105
+ requiresApiKey: true,
106
+ baseUrl: "https://api.z.ai/api/anthropic",
107
+ haikuModel: "glm-4.5-air",
108
+ sonnetModel: "glm-4.7",
109
+ opusModel: "glm-4.7",
110
+ aliases: ["glm"]
111
+ },
112
+ {
113
+ name: "minimax",
114
+ label: "MiniMax",
115
+ requiresApiKey: true,
116
+ baseUrl: "https://api.minimax.io/anthropic",
117
+ haikuModel: "MiniMax-M2.1",
118
+ sonnetModel: "MiniMax-M2.1",
119
+ opusModel: "MiniMax-M2.1"
120
+ }
121
+ ];
122
+ var TEMPLATE_ALIASES = /* @__PURE__ */ new Map();
123
+ for (const template of PROVIDER_TEMPLATES) {
124
+ TEMPLATE_ALIASES.set(template.name.toLowerCase(), template.name);
125
+ for (const alias of template.aliases ?? []) {
126
+ TEMPLATE_ALIASES.set(alias.toLowerCase(), template.name);
127
+ }
128
+ }
129
+ function getTemplateConfig(name) {
130
+ return PROVIDER_TEMPLATES.find((template) => template.name === name);
131
+ }
132
+ function getProviderTemplates() {
133
+ return PROVIDER_TEMPLATES.map((template) => ({
134
+ name: template.name,
135
+ label: template.label,
136
+ requiresApiKey: template.requiresApiKey
137
+ }));
138
+ }
139
+ function resolveTemplateName(input) {
140
+ if (!input) {
141
+ return void 0;
142
+ }
143
+ return TEMPLATE_ALIASES.get(input.trim().toLowerCase());
144
+ }
85
145
  async function checkClaudeSettings() {
86
146
  try {
87
147
  await access(CLAUDE_SETTINGS);
@@ -125,6 +185,30 @@ async function getActiveProfileStatus() {
125
185
  const exists = await profileExists(state.activeProfile);
126
186
  return { name: state.activeProfile, exists };
127
187
  }
188
+ function applyTemplateOverrides(settingsContent, template, apiKey) {
189
+ const parsed = JSON.parse(settingsContent);
190
+ const env = { ...parsed.env ?? {} };
191
+ delete env.ANTHROPIC_API_KEY;
192
+ if (template.baseUrl) {
193
+ env.ANTHROPIC_BASE_URL = template.baseUrl;
194
+ }
195
+ if (template.requiresApiKey && apiKey) {
196
+ env.ANTHROPIC_AUTH_TOKEN = apiKey;
197
+ } else if (apiKey) {
198
+ env.ANTHROPIC_AUTH_TOKEN = apiKey;
199
+ }
200
+ if (template.haikuModel) {
201
+ env.ANTHROPIC_DEFAULT_HAIKU_MODEL = template.haikuModel;
202
+ }
203
+ if (template.sonnetModel) {
204
+ env.ANTHROPIC_DEFAULT_SONNET_MODEL = template.sonnetModel;
205
+ }
206
+ if (template.opusModel) {
207
+ env.ANTHROPIC_DEFAULT_OPUS_MODEL = template.opusModel;
208
+ }
209
+ parsed.env = env;
210
+ return JSON.stringify(parsed, null, 2);
211
+ }
128
212
  async function mirrorSettings(profileName) {
129
213
  const profileSettings = getProfileSettings(profileName);
130
214
  await mkdir2(dirname2(profileSettings), { recursive: true });
@@ -155,7 +239,7 @@ async function switchProfile(targetProfile) {
155
239
  await fsRename(tempFile, CLAUDE_SETTINGS);
156
240
  await updateState({ activeProfile: targetProfile });
157
241
  }
158
- async function createProfile(profileName) {
242
+ async function createProfile(profileName, options) {
159
243
  const validation = validateProfileName(profileName);
160
244
  if (!validation.valid) {
161
245
  throw new Error(validation.error);
@@ -164,12 +248,39 @@ async function createProfile(profileName) {
164
248
  if (await profileExists(profileName)) {
165
249
  throw new Error(`Profile "${profileName}" already exists`);
166
250
  }
251
+ const templateConfig = options?.template ? getTemplateConfig(options.template) : void 0;
252
+ if (options?.template && !templateConfig) {
253
+ throw new Error(`Unknown template "${options.template}"`);
254
+ }
255
+ const apiKey = options?.apiKey?.trim();
256
+ if (templateConfig?.requiresApiKey && !apiKey) {
257
+ throw new Error(`API key is required for template "${templateConfig.label}"`);
258
+ }
259
+ if (!templateConfig && options?.apiKey) {
260
+ throw new Error("API key can only be used with a template");
261
+ }
167
262
  const profileSettings = getProfileSettings(profileName);
168
263
  await mkdir2(dirname2(profileSettings), { recursive: true });
169
264
  const currentSettings = await readFile2(CLAUDE_SETTINGS, "utf-8");
265
+ let activeProfileForTemplate;
266
+ if (templateConfig) {
267
+ const state = await readState();
268
+ activeProfileForTemplate = state.activeProfile;
269
+ }
270
+ let settingsToWrite = currentSettings;
271
+ if (templateConfig) {
272
+ await mirrorSettings(activeProfileForTemplate ?? "default");
273
+ settingsToWrite = applyTemplateOverrides(currentSettings, templateConfig, apiKey);
274
+ }
170
275
  const tempFile = `${profileSettings}.tmp`;
171
- await writeFile2(tempFile, currentSettings, "utf-8");
276
+ await writeFile2(tempFile, settingsToWrite, "utf-8");
172
277
  await fsRename(tempFile, profileSettings);
278
+ if (templateConfig) {
279
+ const tempCurrentSettings = `${CLAUDE_SETTINGS}.tmp`;
280
+ await writeFile2(tempCurrentSettings, settingsToWrite, "utf-8");
281
+ await fsRename(tempCurrentSettings, CLAUDE_SETTINGS);
282
+ }
283
+ await updateState({ activeProfile: profileName });
173
284
  }
174
285
  async function deleteProfile(profileName) {
175
286
  const validation = validateProfileName(profileName);
@@ -197,6 +308,9 @@ async function renameProfile(oldName, newName) {
197
308
  if (!await profileExists(oldName)) {
198
309
  throw new Error(`Profile "${oldName}" does not exist`);
199
310
  }
311
+ if (oldName === newName) {
312
+ return;
313
+ }
200
314
  if (await profileExists(newName)) {
201
315
  throw new Error(`Profile "${newName}" already exists`);
202
316
  }
@@ -233,6 +347,48 @@ function getProfileValidationMessage(value) {
233
347
  }
234
348
  return void 0;
235
349
  }
350
+ async function promptForTemplateSelection() {
351
+ const templates = getProviderTemplates();
352
+ const selection = await clack.select({
353
+ message: "Choose a provider template (optional):",
354
+ options: [
355
+ { value: "none", label: "None (copy current settings)" },
356
+ ...templates.map((template) => ({
357
+ value: template.name,
358
+ label: template.label
359
+ }))
360
+ ]
361
+ });
362
+ if (clack.isCancel(selection)) {
363
+ clack.cancel("Operation cancelled");
364
+ process.exit(0);
365
+ }
366
+ if (selection === "none") {
367
+ return {};
368
+ }
369
+ const chosenTemplate = selection;
370
+ const meta = templates.find((template) => template.name === chosenTemplate);
371
+ if (!meta) {
372
+ throw new Error("Selected template is not available");
373
+ }
374
+ if (!meta.requiresApiKey) {
375
+ return { template: chosenTemplate };
376
+ }
377
+ const apiKey = await clack.password({
378
+ message: `Enter API key for ${meta.label}:`,
379
+ validate: (value) => {
380
+ if (!value || !String(value).trim()) {
381
+ return "API key cannot be empty";
382
+ }
383
+ return void 0;
384
+ }
385
+ });
386
+ if (clack.isCancel(apiKey)) {
387
+ clack.cancel("Operation cancelled");
388
+ process.exit(0);
389
+ }
390
+ return { template: chosenTemplate, apiKey };
391
+ }
236
392
  async function showProfileList(profiles) {
237
393
  const activeStatus = await getActiveProfileStatus();
238
394
  const resolvedProfiles = profiles ?? await listProfiles();
@@ -248,7 +404,7 @@ async function showProfileList(profiles) {
248
404
  }
249
405
  console.log();
250
406
  }
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");
407
+ 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").option("--template <provider>", "Create profile using a provider template").option("--api-key <key>", "API key for the provider template");
252
408
  async function interactiveMode() {
253
409
  clack.intro("cc-switch - Claude Code Profile Manager");
254
410
  try {
@@ -306,8 +462,10 @@ async function interactiveMode() {
306
462
  clack.cancel("Operation cancelled");
307
463
  process.exit(0);
308
464
  }
309
- await createProfile(profileName);
310
- clack.outro(`Created profile "${profileName}"`);
465
+ const templateSelection = await promptForTemplateSelection();
466
+ await createProfile(profileName, templateSelection);
467
+ const templateSuffix = templateSelection.template ? ` with template "${templateSelection.template}"` : "";
468
+ clack.outro(`Created profile "${profileName}"${templateSuffix}`);
311
469
  break;
312
470
  }
313
471
  case "delete": {
@@ -366,6 +524,10 @@ async function interactiveMode() {
366
524
  clack.cancel("Operation cancelled");
367
525
  process.exit(0);
368
526
  }
527
+ if (oldName === newName) {
528
+ clack.outro("Profile name unchanged");
529
+ process.exit(0);
530
+ }
369
531
  await renameProfile(oldName, newName);
370
532
  clack.outro(`Renamed profile "${oldName}" to "${newName}"`);
371
533
  break;
@@ -401,6 +563,8 @@ async function runCli() {
401
563
  }
402
564
  program.parse();
403
565
  const opts = program.opts();
566
+ const resolvedTemplate = opts.template ? resolveTemplateName(opts.template) : void 0;
567
+ const templateMeta = resolvedTemplate ? getProviderTemplates().find((template) => template.name === resolvedTemplate) : void 0;
404
568
  const profileArg = program.args[0];
405
569
  const actionFlags = [];
406
570
  if (opts.switch) actionFlags.push("switch");
@@ -409,6 +573,22 @@ async function runCli() {
409
573
  if (opts.rename || opts.to) actionFlags.push("rename");
410
574
  if (opts.current) actionFlags.push("current");
411
575
  if (opts.list) actionFlags.push("list");
576
+ if (opts.template && !opts.create) {
577
+ console.error("Error: --template can only be used with --create.");
578
+ process.exit(1);
579
+ }
580
+ if (opts.apiKey && !opts.template) {
581
+ console.error("Error: --api-key can only be used with --template.");
582
+ process.exit(1);
583
+ }
584
+ if (opts.template && !resolvedTemplate) {
585
+ console.error(`Error: Unknown template "${opts.template}".`);
586
+ process.exit(1);
587
+ }
588
+ if (templateMeta?.requiresApiKey && !opts.apiKey) {
589
+ console.error(`Error: API key is required for template "${templateMeta.label}".`);
590
+ process.exit(1);
591
+ }
412
592
  if (actionFlags.length > 1) {
413
593
  console.error("Error: Please provide only one action flag at a time.");
414
594
  process.exit(1);
@@ -449,8 +629,12 @@ async function runCli() {
449
629
  if (!opts.create) {
450
630
  throw new Error("Missing profile name for --create");
451
631
  }
452
- await createProfile(opts.create);
453
- console.log(`Created profile "${opts.create}"`);
632
+ await createProfile(opts.create, {
633
+ template: resolvedTemplate,
634
+ apiKey: opts.apiKey
635
+ });
636
+ const templateSuffix = resolvedTemplate ? ` with template "${resolvedTemplate}"` : "";
637
+ console.log(`Created profile "${opts.create}"${templateSuffix}`);
454
638
  process.exit(0);
455
639
  break;
456
640
  }
@@ -483,6 +667,12 @@ async function runCli() {
483
667
  if (!newName) {
484
668
  throw new Error("Missing new profile name for --rename");
485
669
  }
670
+ if (oldName === newName) {
671
+ await renameProfile(oldName, newName);
672
+ console.log("Profile name unchanged");
673
+ process.exit(0);
674
+ break;
675
+ }
486
676
  await renameProfile(oldName, newName);
487
677
  console.log(`Renamed profile "${oldName}" to "${newName}"`);
488
678
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aravhawk/cc-switch",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Profile manager for Claude Code settings",
5
5
  "type": "module",
6
6
  "bin": {