@aravhawk/cc-switch 2.1.1 → 3.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 (2) hide show
  1. package/dist/index.js +216 -33
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import * as clack from "@clack/prompts";
6
6
  import { createRequire } from "module";
7
7
 
8
8
  // src/profiles.ts
9
- import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, rm, readdir, rename as fsRename, access } from "fs/promises";
9
+ import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, rm, readdir, rename as fsRename, access, cp, copyFile, stat as fsStat } from "fs/promises";
10
10
  import { dirname as dirname2 } from "path";
11
11
 
12
12
  // src/paths.ts
@@ -14,15 +14,38 @@ import { homedir } from "os";
14
14
  import { join } from "path";
15
15
  var CLAUDE_DIR = join(homedir(), ".claude");
16
16
  var CLAUDE_SETTINGS = join(CLAUDE_DIR, "settings.json");
17
+ var CLAUDE_MD = join(CLAUDE_DIR, "CLAUDE.md");
18
+ var CLAUDE_AGENTS_DIR = join(CLAUDE_DIR, "agents");
19
+ var CLAUDE_SKILLS_DIR = join(CLAUDE_DIR, "skills");
20
+ var CLAUDE_PLUGINS_DIR = join(CLAUDE_DIR, "plugins");
21
+ var CLAUDE_TEAMS_DIR = join(CLAUDE_DIR, "teams");
17
22
  var CC_SWITCH_DIR = join(homedir(), ".cc-switch");
18
23
  var PROFILES_DIR = join(CC_SWITCH_DIR, "profiles");
19
24
  var STATE_FILE = join(CC_SWITCH_DIR, "state.json");
25
+ var AVAILABLE_MANAGED_PATHS = [
26
+ { name: "CLAUDE.md", label: "Instructions (CLAUDE.md)", claudePath: CLAUDE_MD },
27
+ { name: "agents", label: "Agents", claudePath: CLAUDE_AGENTS_DIR },
28
+ { name: "skills", label: "Skills", claudePath: CLAUDE_SKILLS_DIR },
29
+ { name: "plugins", label: "Plugins", claudePath: CLAUDE_PLUGINS_DIR },
30
+ { name: "teams", label: "Teams", claudePath: CLAUDE_TEAMS_DIR }
31
+ ];
32
+ var DEFAULT_MANAGED_PATHS = ["CLAUDE.md", "agents", "skills"];
20
33
  function getProfileDir(profileName) {
21
34
  return join(PROFILES_DIR, profileName);
22
35
  }
23
36
  function getProfileSettings(profileName) {
24
37
  return join(getProfileDir(profileName), "settings.json");
25
38
  }
39
+ function getProfileMetaPath(profileName) {
40
+ return join(getProfileDir(profileName), "profile.json");
41
+ }
42
+ function getProfileManagedPath(profileName, managedName) {
43
+ const valid = AVAILABLE_MANAGED_PATHS.some((p) => p.name === managedName);
44
+ if (!valid) {
45
+ throw new Error(`Invalid managed path name: "${managedName}"`);
46
+ }
47
+ return join(getProfileDir(profileName), managedName);
48
+ }
26
49
 
27
50
  // src/state.ts
28
51
  import { readFile, writeFile, mkdir } from "fs/promises";
@@ -114,9 +137,9 @@ var PROVIDER_TEMPLATES = [
114
137
  label: "MiniMax",
115
138
  requiresApiKey: true,
116
139
  baseUrl: "https://api.minimax.io/anthropic",
117
- haikuModel: "MiniMax-M2.5",
118
- sonnetModel: "MiniMax-M2.5",
119
- opusModel: "MiniMax-M2.5"
140
+ haikuModel: "MiniMax-M2.7",
141
+ sonnetModel: "MiniMax-M2.7",
142
+ opusModel: "MiniMax-M2.7"
120
143
  }
121
144
  ];
122
145
  var TEMPLATE_ALIASES = /* @__PURE__ */ new Map();
@@ -129,6 +152,67 @@ for (const template of PROVIDER_TEMPLATES) {
129
152
  function getTemplateConfig(name) {
130
153
  return PROVIDER_TEMPLATES.find((template) => template.name === name);
131
154
  }
155
+ async function getProfileType(profileName) {
156
+ const meta = await readProfileMeta(profileName);
157
+ return meta?.type === "full" ? "full" : "settings";
158
+ }
159
+ async function readProfileMeta(profileName) {
160
+ let data;
161
+ try {
162
+ data = await readFile2(getProfileMetaPath(profileName), "utf-8");
163
+ } catch (e) {
164
+ if (e.code === "ENOENT") return null;
165
+ throw e;
166
+ }
167
+ try {
168
+ return JSON.parse(data);
169
+ } catch {
170
+ throw new Error(`Profile metadata for "${profileName}" is corrupt (profile.json). Delete and recreate the profile.`);
171
+ }
172
+ }
173
+ async function writeProfileMeta(profileName, type, managedPaths) {
174
+ const metaPath = getProfileMetaPath(profileName);
175
+ const meta = { type, version: 1, managedPaths };
176
+ const tempFile = `${metaPath}.tmp`;
177
+ await writeFile2(tempFile, JSON.stringify(meta, null, 2), "utf-8");
178
+ await fsRename(tempFile, metaPath);
179
+ }
180
+ async function syncManagedPath(source, dest) {
181
+ let sourceStat;
182
+ try {
183
+ sourceStat = await fsStat(source);
184
+ } catch (e) {
185
+ if (e.code === "ENOENT") return;
186
+ throw e;
187
+ }
188
+ await rm(dest, { recursive: true, force: true });
189
+ if (sourceStat.isDirectory()) {
190
+ await cp(source, dest, { recursive: true, verbatimSymlinks: true });
191
+ } else {
192
+ await mkdir2(dirname2(dest), { recursive: true });
193
+ await copyFile(source, dest);
194
+ }
195
+ }
196
+ function resolveManagedClaudePath(managedName) {
197
+ const entry = AVAILABLE_MANAGED_PATHS.find((p) => p.name === managedName);
198
+ return entry?.claudePath;
199
+ }
200
+ async function mirrorFullProfile(profileName, managedPaths) {
201
+ for (const name of managedPaths) {
202
+ const claudePath = resolveManagedClaudePath(name);
203
+ if (!claudePath) continue;
204
+ const destPath = getProfileManagedPath(profileName, name);
205
+ await syncManagedPath(claudePath, destPath);
206
+ }
207
+ }
208
+ async function loadFullProfile(profileName, managedPaths) {
209
+ for (const name of managedPaths) {
210
+ const claudePath = resolveManagedClaudePath(name);
211
+ if (!claudePath) continue;
212
+ const sourcePath = getProfileManagedPath(profileName, name);
213
+ await syncManagedPath(sourcePath, claudePath);
214
+ }
215
+ }
132
216
  function getProviderTemplates() {
133
217
  return PROVIDER_TEMPLATES.map((template) => ({
134
218
  name: template.name,
@@ -156,10 +240,16 @@ async function listProfiles() {
156
240
  try {
157
241
  await mkdir2(PROFILES_DIR, { recursive: true });
158
242
  const entries = await readdir(PROFILES_DIR, { withFileTypes: true });
159
- const profiles = entries.filter((entry) => entry.isDirectory()).map((entry) => ({
160
- name: entry.name,
161
- isActive: entry.name === state.activeProfile
162
- }));
243
+ const profiles = [];
244
+ for (const entry of entries) {
245
+ if (!entry.isDirectory()) continue;
246
+ const type = await getProfileType(entry.name);
247
+ profiles.push({
248
+ name: entry.name,
249
+ isActive: entry.name === state.activeProfile,
250
+ type
251
+ });
252
+ }
163
253
  return profiles.sort((a, b) => {
164
254
  if (a.isActive) return -1;
165
255
  if (b.isActive) return 1;
@@ -209,13 +299,17 @@ function applyTemplateOverrides(settingsContent, template, apiKey) {
209
299
  parsed.env = env;
210
300
  return JSON.stringify(parsed, null, 2);
211
301
  }
212
- async function mirrorSettings(profileName) {
302
+ async function mirrorActiveProfile(profileName) {
213
303
  const profileSettings = getProfileSettings(profileName);
214
304
  await mkdir2(dirname2(profileSettings), { recursive: true });
215
305
  const currentSettings = await readFile2(CLAUDE_SETTINGS, "utf-8");
216
306
  const tempFile = `${profileSettings}.tmp`;
217
307
  await writeFile2(tempFile, currentSettings, "utf-8");
218
308
  await fsRename(tempFile, profileSettings);
309
+ const meta = await readProfileMeta(profileName);
310
+ if (meta?.type === "full" && meta.managedPaths.length > 0) {
311
+ await mirrorFullProfile(profileName, meta.managedPaths);
312
+ }
219
313
  }
220
314
  async function switchProfile(targetProfile) {
221
315
  const validation = validateProfileName(targetProfile);
@@ -228,15 +322,25 @@ async function switchProfile(targetProfile) {
228
322
  }
229
323
  const state = await readState();
230
324
  const activeProfile = state.activeProfile;
231
- await mirrorSettings(activeProfile);
232
325
  if (targetProfile === activeProfile) {
233
326
  throw new Error(`Profile "${targetProfile}" is already active`);
234
327
  }
328
+ await mirrorActiveProfile(activeProfile);
235
329
  const targetSettings = getProfileSettings(targetProfile);
236
330
  const newSettings = await readFile2(targetSettings, "utf-8");
237
331
  const tempFile = `${CLAUDE_SETTINGS}.tmp`;
238
332
  await writeFile2(tempFile, newSettings, "utf-8");
239
333
  await fsRename(tempFile, CLAUDE_SETTINGS);
334
+ const activeMeta = await readProfileMeta(activeProfile);
335
+ const targetMeta = await readProfileMeta(targetProfile);
336
+ if (targetMeta?.type === "full" && targetMeta.managedPaths.length > 0) {
337
+ await loadFullProfile(targetProfile, targetMeta.managedPaths);
338
+ } else if (activeMeta?.type === "full" && activeMeta.managedPaths.length > 0) {
339
+ for (const name of activeMeta.managedPaths) {
340
+ const claudePath = resolveManagedClaudePath(name);
341
+ if (claudePath) await rm(claudePath, { recursive: true, force: true });
342
+ }
343
+ }
240
344
  await updateState({ activeProfile: targetProfile });
241
345
  }
242
346
  async function createProfile(profileName, options) {
@@ -269,7 +373,7 @@ async function createProfile(profileName, options) {
269
373
  }
270
374
  let settingsToWrite = currentSettings;
271
375
  if (templateConfig) {
272
- await mirrorSettings(activeProfileForTemplate ?? "default");
376
+ await mirrorActiveProfile(activeProfileForTemplate ?? "default");
273
377
  settingsToWrite = applyTemplateOverrides(currentSettings, templateConfig, apiKey);
274
378
  }
275
379
  const tempFile = `${profileSettings}.tmp`;
@@ -280,6 +384,11 @@ async function createProfile(profileName, options) {
280
384
  await writeFile2(tempCurrentSettings, settingsToWrite, "utf-8");
281
385
  await fsRename(tempCurrentSettings, CLAUDE_SETTINGS);
282
386
  }
387
+ if (options?.full) {
388
+ const managedPaths = options.managedPaths ?? [];
389
+ await writeProfileMeta(profileName, "full", managedPaths);
390
+ await mirrorFullProfile(profileName, managedPaths);
391
+ }
283
392
  await updateState({ activeProfile: profileName });
284
393
  }
285
394
  async function deleteProfile(profileName) {
@@ -322,6 +431,7 @@ async function renameProfile(oldName, newName) {
322
431
  }
323
432
 
324
433
  // src/index.ts
434
+ var VALID_MANAGED_PATH_NAMES = AVAILABLE_MANAGED_PATHS.map((p) => p.name);
325
435
  var require2 = createRequire(import.meta.url);
326
436
  function getCliVersion() {
327
437
  try {
@@ -399,30 +509,37 @@ async function showProfileList(profiles) {
399
509
  }
400
510
  console.log("\nProfiles:");
401
511
  for (const profile of resolvedProfiles) {
402
- const marker = profile.isActive ? " (active)" : "";
403
- console.log(` ${profile.name}${marker}`);
512
+ const markers = [];
513
+ if (profile.isActive) markers.push("active");
514
+ markers.push(profile.type === "full" ? "full" : "settings");
515
+ const suffix = ` (${markers.join(", ")})`;
516
+ console.log(` ${profile.name}${suffix}`);
404
517
  }
405
518
  console.log();
406
519
  }
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");
520
+ 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").option("--full", "Create a full profile (settings, instructions, agents, skills)").option("--include <paths>", "Comma-separated paths to include in full profile (default: CLAUDE.md,agents,skills)");
408
521
  async function interactiveMode() {
409
522
  clack.intro("cc-switch - Claude Code Profile Manager");
410
523
  try {
411
524
  const profiles = await listProfiles();
412
- if (profiles.length === 0) {
413
- clack.outro("No profiles found. Create one with: cc-switch --create <name>");
414
- process.exit(0);
525
+ const hasProfiles = profiles.length > 0;
526
+ const menuOptions = hasProfiles ? [
527
+ { value: "current", label: "Show current profile" },
528
+ { value: "switch", label: "Switch profile" },
529
+ { value: "create", label: "Create new profile" },
530
+ { value: "delete", label: "Delete profile" },
531
+ { value: "rename", label: "Rename profile" },
532
+ { value: "list", label: "List profiles" }
533
+ ] : [
534
+ { value: "create", label: "Create your first profile" },
535
+ { value: "current", label: "Show current profile" }
536
+ ];
537
+ if (!hasProfiles) {
538
+ clack.log.info("No profiles found yet. Create one to get started.");
415
539
  }
416
540
  const action = await clack.select({
417
541
  message: "What would you like to do?",
418
- options: [
419
- { value: "current", label: "Show current profile" },
420
- { value: "switch", label: "Switch profile" },
421
- { value: "create", label: "Create new profile" },
422
- { value: "delete", label: "Delete profile" },
423
- { value: "rename", label: "Rename profile" },
424
- { value: "list", label: "List profiles" }
425
- ]
542
+ options: menuOptions
426
543
  });
427
544
  if (clack.isCancel(action)) {
428
545
  clack.cancel("Operation cancelled");
@@ -430,10 +547,13 @@ async function interactiveMode() {
430
547
  }
431
548
  switch (action) {
432
549
  case "switch": {
433
- const profileChoices = profiles.map((p) => ({
434
- value: p.name,
435
- label: p.isActive ? `${p.name} (active)` : p.name
436
- }));
550
+ const profileChoices = profiles.map((p) => {
551
+ const markers = [];
552
+ if (p.isActive) markers.push("active");
553
+ if (p.type === "full") markers.push("full");
554
+ const suffix = markers.length > 0 ? ` (${markers.join(", ")})` : "";
555
+ return { value: p.name, label: `${p.name}${suffix}` };
556
+ });
437
557
  const selectedProfile = await clack.select({
438
558
  message: "Select profile to switch to:",
439
559
  options: profileChoices
@@ -462,10 +582,43 @@ async function interactiveMode() {
462
582
  clack.cancel("Operation cancelled");
463
583
  process.exit(0);
464
584
  }
585
+ const profileType = await clack.select({
586
+ message: "Profile type:",
587
+ options: [
588
+ { value: "settings", label: "Settings only (swap settings.json)" },
589
+ { value: "full", label: "Full (swap settings, instructions, agents, skills)" }
590
+ ]
591
+ });
592
+ if (clack.isCancel(profileType)) {
593
+ clack.cancel("Operation cancelled");
594
+ process.exit(0);
595
+ }
596
+ const isFull = profileType === "full";
597
+ let selectedManagedPaths = [];
598
+ if (isFull) {
599
+ const pathSelection = await clack.multiselect({
600
+ message: "Which paths to include in this profile?",
601
+ options: AVAILABLE_MANAGED_PATHS.map((p) => ({
602
+ value: p.name,
603
+ label: p.label
604
+ })),
605
+ initialValues: [...DEFAULT_MANAGED_PATHS]
606
+ });
607
+ if (clack.isCancel(pathSelection)) {
608
+ clack.cancel("Operation cancelled");
609
+ process.exit(0);
610
+ }
611
+ selectedManagedPaths = pathSelection;
612
+ }
465
613
  const templateSelection = await promptForTemplateSelection();
466
- await createProfile(profileName, templateSelection);
614
+ await createProfile(profileName, {
615
+ ...templateSelection,
616
+ full: isFull,
617
+ managedPaths: isFull ? selectedManagedPaths : void 0
618
+ });
467
619
  const templateSuffix = templateSelection.template ? ` with template "${templateSelection.template}"` : "";
468
- clack.outro(`Created profile "${profileName}"${templateSuffix}`);
620
+ const fullSuffix = isFull ? " (full profile)" : "";
621
+ clack.outro(`Created profile "${profileName}"${templateSuffix}${fullSuffix}`);
469
622
  break;
470
623
  }
471
624
  case "delete": {
@@ -589,6 +742,33 @@ async function runCli() {
589
742
  console.error(`Error: API key is required for template "${templateMeta.label}".`);
590
743
  process.exit(1);
591
744
  }
745
+ if (opts.full && !opts.create) {
746
+ console.error("Error: --full can only be used with --create.");
747
+ process.exit(1);
748
+ }
749
+ if (opts.include && !opts.full) {
750
+ console.error("Error: --include can only be used with --full.");
751
+ process.exit(1);
752
+ }
753
+ let parsedManagedPaths;
754
+ if (opts.full) {
755
+ if (opts.include) {
756
+ parsedManagedPaths = opts.include.split(",").map((s) => s.trim()).filter(Boolean);
757
+ if (parsedManagedPaths.length === 0) {
758
+ console.error("Error: --include must specify at least one valid path.");
759
+ console.error(`Valid paths: ${VALID_MANAGED_PATH_NAMES.join(", ")}`);
760
+ process.exit(1);
761
+ }
762
+ const invalid = parsedManagedPaths.filter((p) => !VALID_MANAGED_PATH_NAMES.includes(p));
763
+ if (invalid.length > 0) {
764
+ console.error(`Error: Invalid managed path(s): ${invalid.join(", ")}`);
765
+ console.error(`Valid paths: ${VALID_MANAGED_PATH_NAMES.join(", ")}`);
766
+ process.exit(1);
767
+ }
768
+ } else {
769
+ parsedManagedPaths = [...DEFAULT_MANAGED_PATHS];
770
+ }
771
+ }
592
772
  if (actionFlags.length > 1) {
593
773
  console.error("Error: Please provide only one action flag at a time.");
594
774
  process.exit(1);
@@ -631,10 +811,13 @@ async function runCli() {
631
811
  }
632
812
  await createProfile(opts.create, {
633
813
  template: resolvedTemplate,
634
- apiKey: opts.apiKey
814
+ apiKey: opts.apiKey,
815
+ full: opts.full,
816
+ managedPaths: parsedManagedPaths
635
817
  });
636
818
  const templateSuffix = resolvedTemplate ? ` with template "${resolvedTemplate}"` : "";
637
- console.log(`Created profile "${opts.create}"${templateSuffix}`);
819
+ const fullSuffix = opts.full ? " (full profile)" : "";
820
+ console.log(`Created profile "${opts.create}"${templateSuffix}${fullSuffix}`);
638
821
  process.exit(0);
639
822
  break;
640
823
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aravhawk/cc-switch",
3
- "version": "2.1.1",
3
+ "version": "3.1.0",
4
4
  "description": "Profile manager for Claude Code settings",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,7 @@
21
21
  "author": "Arav",
22
22
  "license": "MIT",
23
23
  "engines": {
24
- "node": ">=18.0.0"
24
+ "node": ">=20.7.0"
25
25
  },
26
26
  "dependencies": {
27
27
  "@clack/prompts": "^1.0.0",