@aravhawk/cc-switch 2.1.1 → 3.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/dist/index.js +198 -19
- 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";
|
|
@@ -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 =
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
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
|
|
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,12 +509,15 @@ async function showProfileList(profiles) {
|
|
|
399
509
|
}
|
|
400
510
|
console.log("\nProfiles:");
|
|
401
511
|
for (const profile of resolvedProfiles) {
|
|
402
|
-
const
|
|
403
|
-
|
|
512
|
+
const markers = [];
|
|
513
|
+
if (profile.isActive) markers.push("active");
|
|
514
|
+
if (profile.type === "full") markers.push("full");
|
|
515
|
+
const suffix = markers.length > 0 ? ` (${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 {
|
|
@@ -430,10 +543,13 @@ async function interactiveMode() {
|
|
|
430
543
|
}
|
|
431
544
|
switch (action) {
|
|
432
545
|
case "switch": {
|
|
433
|
-
const profileChoices = profiles.map((p) =>
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
546
|
+
const profileChoices = profiles.map((p) => {
|
|
547
|
+
const markers = [];
|
|
548
|
+
if (p.isActive) markers.push("active");
|
|
549
|
+
if (p.type === "full") markers.push("full");
|
|
550
|
+
const suffix = markers.length > 0 ? ` (${markers.join(", ")})` : "";
|
|
551
|
+
return { value: p.name, label: `${p.name}${suffix}` };
|
|
552
|
+
});
|
|
437
553
|
const selectedProfile = await clack.select({
|
|
438
554
|
message: "Select profile to switch to:",
|
|
439
555
|
options: profileChoices
|
|
@@ -462,10 +578,43 @@ async function interactiveMode() {
|
|
|
462
578
|
clack.cancel("Operation cancelled");
|
|
463
579
|
process.exit(0);
|
|
464
580
|
}
|
|
581
|
+
const profileType = await clack.select({
|
|
582
|
+
message: "Profile type:",
|
|
583
|
+
options: [
|
|
584
|
+
{ value: "settings", label: "Settings only (swap settings.json)" },
|
|
585
|
+
{ value: "full", label: "Full (swap settings, instructions, agents, skills)" }
|
|
586
|
+
]
|
|
587
|
+
});
|
|
588
|
+
if (clack.isCancel(profileType)) {
|
|
589
|
+
clack.cancel("Operation cancelled");
|
|
590
|
+
process.exit(0);
|
|
591
|
+
}
|
|
592
|
+
const isFull = profileType === "full";
|
|
593
|
+
let selectedManagedPaths = [];
|
|
594
|
+
if (isFull) {
|
|
595
|
+
const pathSelection = await clack.multiselect({
|
|
596
|
+
message: "Which paths to include in this profile?",
|
|
597
|
+
options: AVAILABLE_MANAGED_PATHS.map((p) => ({
|
|
598
|
+
value: p.name,
|
|
599
|
+
label: p.label
|
|
600
|
+
})),
|
|
601
|
+
initialValues: [...DEFAULT_MANAGED_PATHS]
|
|
602
|
+
});
|
|
603
|
+
if (clack.isCancel(pathSelection)) {
|
|
604
|
+
clack.cancel("Operation cancelled");
|
|
605
|
+
process.exit(0);
|
|
606
|
+
}
|
|
607
|
+
selectedManagedPaths = pathSelection;
|
|
608
|
+
}
|
|
465
609
|
const templateSelection = await promptForTemplateSelection();
|
|
466
|
-
await createProfile(profileName,
|
|
610
|
+
await createProfile(profileName, {
|
|
611
|
+
...templateSelection,
|
|
612
|
+
full: isFull,
|
|
613
|
+
managedPaths: isFull ? selectedManagedPaths : void 0
|
|
614
|
+
});
|
|
467
615
|
const templateSuffix = templateSelection.template ? ` with template "${templateSelection.template}"` : "";
|
|
468
|
-
|
|
616
|
+
const fullSuffix = isFull ? " (full profile)" : "";
|
|
617
|
+
clack.outro(`Created profile "${profileName}"${templateSuffix}${fullSuffix}`);
|
|
469
618
|
break;
|
|
470
619
|
}
|
|
471
620
|
case "delete": {
|
|
@@ -589,6 +738,33 @@ async function runCli() {
|
|
|
589
738
|
console.error(`Error: API key is required for template "${templateMeta.label}".`);
|
|
590
739
|
process.exit(1);
|
|
591
740
|
}
|
|
741
|
+
if (opts.full && !opts.create) {
|
|
742
|
+
console.error("Error: --full can only be used with --create.");
|
|
743
|
+
process.exit(1);
|
|
744
|
+
}
|
|
745
|
+
if (opts.include && !opts.full) {
|
|
746
|
+
console.error("Error: --include can only be used with --full.");
|
|
747
|
+
process.exit(1);
|
|
748
|
+
}
|
|
749
|
+
let parsedManagedPaths;
|
|
750
|
+
if (opts.full) {
|
|
751
|
+
if (opts.include) {
|
|
752
|
+
parsedManagedPaths = opts.include.split(",").map((s) => s.trim()).filter(Boolean);
|
|
753
|
+
if (parsedManagedPaths.length === 0) {
|
|
754
|
+
console.error("Error: --include must specify at least one valid path.");
|
|
755
|
+
console.error(`Valid paths: ${VALID_MANAGED_PATH_NAMES.join(", ")}`);
|
|
756
|
+
process.exit(1);
|
|
757
|
+
}
|
|
758
|
+
const invalid = parsedManagedPaths.filter((p) => !VALID_MANAGED_PATH_NAMES.includes(p));
|
|
759
|
+
if (invalid.length > 0) {
|
|
760
|
+
console.error(`Error: Invalid managed path(s): ${invalid.join(", ")}`);
|
|
761
|
+
console.error(`Valid paths: ${VALID_MANAGED_PATH_NAMES.join(", ")}`);
|
|
762
|
+
process.exit(1);
|
|
763
|
+
}
|
|
764
|
+
} else {
|
|
765
|
+
parsedManagedPaths = [...DEFAULT_MANAGED_PATHS];
|
|
766
|
+
}
|
|
767
|
+
}
|
|
592
768
|
if (actionFlags.length > 1) {
|
|
593
769
|
console.error("Error: Please provide only one action flag at a time.");
|
|
594
770
|
process.exit(1);
|
|
@@ -631,10 +807,13 @@ async function runCli() {
|
|
|
631
807
|
}
|
|
632
808
|
await createProfile(opts.create, {
|
|
633
809
|
template: resolvedTemplate,
|
|
634
|
-
apiKey: opts.apiKey
|
|
810
|
+
apiKey: opts.apiKey,
|
|
811
|
+
full: opts.full,
|
|
812
|
+
managedPaths: parsedManagedPaths
|
|
635
813
|
});
|
|
636
814
|
const templateSuffix = resolvedTemplate ? ` with template "${resolvedTemplate}"` : "";
|
|
637
|
-
|
|
815
|
+
const fullSuffix = opts.full ? " (full profile)" : "";
|
|
816
|
+
console.log(`Created profile "${opts.create}"${templateSuffix}${fullSuffix}`);
|
|
638
817
|
process.exit(0);
|
|
639
818
|
break;
|
|
640
819
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aravhawk/cc-switch",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.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": ">=
|
|
24
|
+
"node": ">=20.7.0"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@clack/prompts": "^1.0.0",
|