@guanmu/ccprofile 0.1.16 → 0.1.18
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/README.md +8 -0
- package/dist/index.js +196 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# ccx
|
|
2
2
|
|
|
3
|
+
```text
|
|
4
|
+
______ ______ __ __
|
|
5
|
+
/ ____// ____/ \ \ / /
|
|
6
|
+
| | | | \ V /
|
|
7
|
+
| |___ | |___ / . \
|
|
8
|
+
\____/ \____/ /_/ \_\
|
|
9
|
+
```
|
|
10
|
+
|
|
3
11
|
Agent Profile Manager for Claude Code.
|
|
4
12
|
|
|
5
13
|
`ccx` lets you save named Claude Code plugin profiles and install a whole profile into the current project with one command. It is designed as a command-first CLI, with a lightweight interactive wizard for manual use.
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,17 @@ import * as p from "@clack/prompts";
|
|
|
7
7
|
import pc from "picocolors";
|
|
8
8
|
const PROFILES_DIR = path.join(process.env.HOME, ".ccx", "profiles");
|
|
9
9
|
const MARKETPLACES_DIR = path.join(process.env.HOME, ".claude", "plugins", "marketplaces");
|
|
10
|
+
const PROJECT_CONFIG_FILE = ".ccx.json";
|
|
11
|
+
const LOGO_LINES = [
|
|
12
|
+
" ______ ______ __ __",
|
|
13
|
+
" / ____// ____/ \\ \\ / /",
|
|
14
|
+
" | | | | \\ V /",
|
|
15
|
+
" | |___ | |___ / . \\",
|
|
16
|
+
" \\____/ \\____/ /_/ \\_\\",
|
|
17
|
+
];
|
|
18
|
+
function renderLogo() {
|
|
19
|
+
return LOGO_LINES.map((line, index) => index < 2 ? pc.cyan(line) : pc.magenta(line)).join("\n");
|
|
20
|
+
}
|
|
10
21
|
function ensureProfilesDir() {
|
|
11
22
|
fs.mkdirSync(PROFILES_DIR, { recursive: true });
|
|
12
23
|
}
|
|
@@ -78,6 +89,36 @@ function writeProfile(name, data) {
|
|
|
78
89
|
ensureProfilesDir();
|
|
79
90
|
fs.writeFileSync(profilePath(normalized), JSON.stringify({ ...data, name: normalized }, null, 2) + "\n");
|
|
80
91
|
}
|
|
92
|
+
function readProjectConfig() {
|
|
93
|
+
const file = path.join(process.cwd(), PROJECT_CONFIG_FILE);
|
|
94
|
+
if (!fs.existsSync(file)) {
|
|
95
|
+
p.log.error(`No ${PROJECT_CONFIG_FILE} found. Run \`ccx init\` to create one.`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
let data;
|
|
99
|
+
try {
|
|
100
|
+
data = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
p.log.error(`Invalid ${PROJECT_CONFIG_FILE}. Expected valid JSON.`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
if (!data ||
|
|
107
|
+
typeof data !== "object" ||
|
|
108
|
+
!Array.isArray(data.plugins)) {
|
|
109
|
+
p.log.error(`Invalid ${PROJECT_CONFIG_FILE}. Expected a plugins array.`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
const plugins = data.plugins.map((plugin) => typeof plugin === "string" ? plugin.trim() : undefined);
|
|
113
|
+
if (plugins.some((plugin) => !plugin)) {
|
|
114
|
+
p.log.error(`Invalid ${PROJECT_CONFIG_FILE}. Plugin entries must be non-empty strings.`);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
return plugins;
|
|
118
|
+
}
|
|
119
|
+
function writeProjectConfig(plugins) {
|
|
120
|
+
fs.writeFileSync(path.join(process.cwd(), PROJECT_CONFIG_FILE), JSON.stringify({ plugins }, null, 2) + "\n");
|
|
121
|
+
}
|
|
81
122
|
function getProfileNames() {
|
|
82
123
|
ensureProfilesDir();
|
|
83
124
|
return fs
|
|
@@ -411,20 +452,12 @@ async function browsePlugins(currentProfile) {
|
|
|
411
452
|
}
|
|
412
453
|
return [];
|
|
413
454
|
}
|
|
414
|
-
async function
|
|
415
|
-
const normalizedProfileName = normalizeProfileName(profileName);
|
|
416
|
-
if (!normalizedProfileName)
|
|
417
|
-
return;
|
|
418
|
-
const data = readProfile(normalizedProfileName);
|
|
419
|
-
if (data.plugins.length === 0) {
|
|
420
|
-
p.log.warn(`No plugins to install in profile "${normalizedProfileName}".`);
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
455
|
+
async function installPlugins(plugins, label) {
|
|
423
456
|
const s = p.spinner();
|
|
424
|
-
s.start(`Installing ${
|
|
457
|
+
s.start(`Installing ${plugins.length} plugin(s) from "${label}"...`);
|
|
425
458
|
let installed = 0;
|
|
426
459
|
let failed = 0;
|
|
427
|
-
for (const plugin of
|
|
460
|
+
for (const plugin of plugins) {
|
|
428
461
|
s.message(`Installing ${plugin}...`);
|
|
429
462
|
try {
|
|
430
463
|
execFileSync("claude", ["plugin", "install", plugin, "--scope", "project"], {
|
|
@@ -440,10 +473,144 @@ async function executeProfile(profileName) {
|
|
|
440
473
|
s.stop(`${installed} installed${failed > 0 ? `, ${failed} failed` : ""}`);
|
|
441
474
|
p.log.success("Done.");
|
|
442
475
|
}
|
|
476
|
+
async function executeProfile(profileName) {
|
|
477
|
+
const normalizedProfileName = normalizeProfileName(profileName);
|
|
478
|
+
if (!normalizedProfileName)
|
|
479
|
+
return;
|
|
480
|
+
const data = readProfile(normalizedProfileName);
|
|
481
|
+
if (data.plugins.length === 0) {
|
|
482
|
+
p.log.warn(`No plugins to install in profile "${normalizedProfileName}".`);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
await installPlugins(data.plugins, normalizedProfileName);
|
|
486
|
+
}
|
|
487
|
+
async function executeProjectConfig() {
|
|
488
|
+
const file = path.join(process.cwd(), PROJECT_CONFIG_FILE);
|
|
489
|
+
if (!fs.existsSync(file)) {
|
|
490
|
+
p.log.error(`No ${PROJECT_CONFIG_FILE} found. Run \`ccx init\` to create one.`);
|
|
491
|
+
process.exitCode = 1;
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const plugins = readProjectConfig();
|
|
495
|
+
if (plugins.length === 0) {
|
|
496
|
+
p.log.warn(`No plugins in ${PROJECT_CONFIG_FILE}.`);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
await installPlugins(plugins, PROJECT_CONFIG_FILE);
|
|
500
|
+
}
|
|
501
|
+
async function initProjectConfig() {
|
|
502
|
+
if (!canPrompt()) {
|
|
503
|
+
console.error("ccx init requires a TTY.");
|
|
504
|
+
process.exitCode = 1;
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const file = path.join(process.cwd(), PROJECT_CONFIG_FILE);
|
|
508
|
+
if (fs.existsSync(file)) {
|
|
509
|
+
const overwrite = await p.confirm({
|
|
510
|
+
message: `${PROJECT_CONFIG_FILE} already exists. Overwrite?`,
|
|
511
|
+
initialValue: false,
|
|
512
|
+
});
|
|
513
|
+
if (p.isCancel(overwrite) || !overwrite)
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const allPlugins = getAllPlugins();
|
|
517
|
+
let selected;
|
|
518
|
+
if (allPlugins.length === 0) {
|
|
519
|
+
const input = (await p.text({
|
|
520
|
+
message: "No marketplace plugins found. Enter plugin names (comma-separated):",
|
|
521
|
+
placeholder: "plugin-a, plugin-b",
|
|
522
|
+
}));
|
|
523
|
+
if (p.isCancel(input))
|
|
524
|
+
return;
|
|
525
|
+
selected = input.split(",").map((s) => s.trim()).filter(Boolean);
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
let filtered = allPlugins;
|
|
529
|
+
if (allPlugins.length > 10) {
|
|
530
|
+
const query = (await p.text({
|
|
531
|
+
message: "Search plugins (leave empty to list all):",
|
|
532
|
+
}));
|
|
533
|
+
if (p.isCancel(query))
|
|
534
|
+
return;
|
|
535
|
+
const q = query.trim().toLowerCase();
|
|
536
|
+
if (q) {
|
|
537
|
+
filtered = allPlugins.filter((pl) => pl.name.toLowerCase().includes(q) ||
|
|
538
|
+
pl.description.toLowerCase().includes(q) ||
|
|
539
|
+
(pl.category && pl.category.toLowerCase().includes(q)));
|
|
540
|
+
if (filtered.length === 0) {
|
|
541
|
+
p.log.warn(`No plugins matching "${query.trim()}".`);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const picked = await p.multiselect({
|
|
547
|
+
message: "Select plugins for this project:",
|
|
548
|
+
options: filtered.map((pl) => ({
|
|
549
|
+
value: pl.name,
|
|
550
|
+
label: pl.name,
|
|
551
|
+
hint: pl.description.slice(0, 50),
|
|
552
|
+
})),
|
|
553
|
+
required: false,
|
|
554
|
+
});
|
|
555
|
+
if (p.isCancel(picked))
|
|
556
|
+
return;
|
|
557
|
+
selected = picked;
|
|
558
|
+
}
|
|
559
|
+
writeProjectConfig(selected);
|
|
560
|
+
p.log.success(`Created ${PROJECT_CONFIG_FILE} with ${selected.length} plugin(s).`);
|
|
561
|
+
}
|
|
562
|
+
function syncProjectConfig() {
|
|
563
|
+
const pluginsDir = path.join(process.cwd(), ".claude", "plugins");
|
|
564
|
+
if (!fs.existsSync(pluginsDir)) {
|
|
565
|
+
p.log.warn("No project plugins found (.claude/plugins/).");
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
const entries = fs.readdirSync(pluginsDir, { withFileTypes: true });
|
|
569
|
+
const plugins = entries
|
|
570
|
+
.filter((e) => e.isDirectory())
|
|
571
|
+
.map((e) => e.name)
|
|
572
|
+
.filter((name) => !name.startsWith("."))
|
|
573
|
+
.sort();
|
|
574
|
+
if (plugins.length === 0) {
|
|
575
|
+
p.log.warn("No project plugins found (.claude/plugins/).");
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
writeProjectConfig(plugins);
|
|
579
|
+
p.log.success(`Synced ${plugins.length} plugin(s) to ${PROJECT_CONFIG_FILE}.`);
|
|
580
|
+
}
|
|
581
|
+
async function saveToProfile(name) {
|
|
582
|
+
const plugins = readProjectConfig();
|
|
583
|
+
if (!name) {
|
|
584
|
+
if (!canPrompt()) {
|
|
585
|
+
missingArg("Profile name is required.", "ccx save <name>");
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
name = (await p.text({
|
|
589
|
+
message: "Save as profile:",
|
|
590
|
+
}));
|
|
591
|
+
if (p.isCancel(name))
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
name = normalizeProfileName(name);
|
|
595
|
+
if (!name)
|
|
596
|
+
return;
|
|
597
|
+
const file = profilePath(name);
|
|
598
|
+
if (fs.existsSync(file)) {
|
|
599
|
+
const overwrite = await p.confirm({
|
|
600
|
+
message: `Profile "${name}" already exists. Overwrite?`,
|
|
601
|
+
initialValue: false,
|
|
602
|
+
});
|
|
603
|
+
if (p.isCancel(overwrite) || !overwrite)
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
writeProfile(name, { name, plugins });
|
|
607
|
+
p.log.success(`Saved ${plugins.length} plugin(s) to profile "${name}".`);
|
|
608
|
+
}
|
|
443
609
|
function printBanner() {
|
|
444
610
|
const require = createRequire(import.meta.url);
|
|
445
611
|
const pkg = require("../package.json");
|
|
446
|
-
|
|
612
|
+
console.log(`${renderLogo()}\n`);
|
|
613
|
+
p.note(pc.bold("ccx") + pc.dim(" - Agent Profile Manager ") + pc.gray(`v${pkg.version}`));
|
|
447
614
|
}
|
|
448
615
|
async function interactiveMode() {
|
|
449
616
|
if (!canPrompt()) {
|
|
@@ -517,11 +684,17 @@ async function interactiveMode() {
|
|
|
517
684
|
p.outro("Done.");
|
|
518
685
|
}
|
|
519
686
|
function printHelp() {
|
|
520
|
-
console.log(
|
|
687
|
+
console.log(`${renderLogo()}
|
|
688
|
+
|
|
689
|
+
ccx - Agent Profile Manager for Claude Code
|
|
521
690
|
|
|
522
691
|
Usage:
|
|
523
692
|
ccx Interactive mode (TTY only)
|
|
524
693
|
ccx ui Interactive mode (TTY only)
|
|
694
|
+
ccx init Create .ccx.json for current project
|
|
695
|
+
ccx sync Sync installed plugins to .ccx.json
|
|
696
|
+
ccx save [name] Save .ccx.json plugins as a profile
|
|
697
|
+
ccx install Install plugins from .ccx.json
|
|
525
698
|
ccx install <profile> Install all plugins from profile
|
|
526
699
|
ccx create <name> Create a new profile
|
|
527
700
|
ccx delete <name> Remove a profile
|
|
@@ -557,6 +730,15 @@ async function main(args) {
|
|
|
557
730
|
}
|
|
558
731
|
const cmd = args[0];
|
|
559
732
|
switch (cmd) {
|
|
733
|
+
case "init":
|
|
734
|
+
await initProjectConfig();
|
|
735
|
+
break;
|
|
736
|
+
case "sync":
|
|
737
|
+
syncProjectConfig();
|
|
738
|
+
break;
|
|
739
|
+
case "save":
|
|
740
|
+
await saveToProfile(args[1]);
|
|
741
|
+
break;
|
|
560
742
|
case "ui":
|
|
561
743
|
case "tui":
|
|
562
744
|
case "interactive":
|
|
@@ -564,7 +746,7 @@ async function main(args) {
|
|
|
564
746
|
break;
|
|
565
747
|
case "install":
|
|
566
748
|
if (!args[1]) {
|
|
567
|
-
|
|
749
|
+
await executeProjectConfig();
|
|
568
750
|
return;
|
|
569
751
|
}
|
|
570
752
|
await executeProfile(args[1]);
|