@guanmu/ccprofile 0.1.15 → 0.1.17
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 +209 -78
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ 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";
|
|
10
11
|
function ensureProfilesDir() {
|
|
11
12
|
fs.mkdirSync(PROFILES_DIR, { recursive: true });
|
|
12
13
|
}
|
|
@@ -78,6 +79,36 @@ function writeProfile(name, data) {
|
|
|
78
79
|
ensureProfilesDir();
|
|
79
80
|
fs.writeFileSync(profilePath(normalized), JSON.stringify({ ...data, name: normalized }, null, 2) + "\n");
|
|
80
81
|
}
|
|
82
|
+
function readProjectConfig() {
|
|
83
|
+
const file = path.join(process.cwd(), PROJECT_CONFIG_FILE);
|
|
84
|
+
if (!fs.existsSync(file)) {
|
|
85
|
+
p.log.error(`No ${PROJECT_CONFIG_FILE} found. Run \`ccx init\` to create one.`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
let data;
|
|
89
|
+
try {
|
|
90
|
+
data = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
p.log.error(`Invalid ${PROJECT_CONFIG_FILE}. Expected valid JSON.`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
if (!data ||
|
|
97
|
+
typeof data !== "object" ||
|
|
98
|
+
!Array.isArray(data.plugins)) {
|
|
99
|
+
p.log.error(`Invalid ${PROJECT_CONFIG_FILE}. Expected a plugins array.`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
const plugins = data.plugins.map((plugin) => typeof plugin === "string" ? plugin.trim() : undefined);
|
|
103
|
+
if (plugins.some((plugin) => !plugin)) {
|
|
104
|
+
p.log.error(`Invalid ${PROJECT_CONFIG_FILE}. Plugin entries must be non-empty strings.`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
return plugins;
|
|
108
|
+
}
|
|
109
|
+
function writeProjectConfig(plugins) {
|
|
110
|
+
fs.writeFileSync(path.join(process.cwd(), PROJECT_CONFIG_FILE), JSON.stringify({ plugins }, null, 2) + "\n");
|
|
111
|
+
}
|
|
81
112
|
function getProfileNames() {
|
|
82
113
|
ensureProfilesDir();
|
|
83
114
|
return fs
|
|
@@ -271,29 +302,27 @@ async function addPlugin(profileName, plugin) {
|
|
|
271
302
|
}
|
|
272
303
|
}
|
|
273
304
|
}
|
|
274
|
-
const selected = await p.
|
|
275
|
-
message: `
|
|
305
|
+
const selected = await p.multiselect({
|
|
306
|
+
message: `Select plugins to add to "${profileName}":`,
|
|
276
307
|
options: [
|
|
277
308
|
...filtered.map((pl) => ({
|
|
278
309
|
value: pl.name,
|
|
279
310
|
label: pl.name,
|
|
280
|
-
hint: pl.description.slice(0,
|
|
311
|
+
hint: pl.description.slice(0, 50),
|
|
281
312
|
})),
|
|
282
|
-
{ value: "__url__", label: "Enter URL manually...", hint: "Input a GitHub URL or plugin name" },
|
|
283
313
|
],
|
|
314
|
+
required: false,
|
|
284
315
|
});
|
|
285
316
|
if (p.isCancel(selected))
|
|
286
317
|
return;
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
message: "Plugin URL or name:",
|
|
290
|
-
}));
|
|
291
|
-
if (p.isCancel(plugin))
|
|
292
|
-
return;
|
|
318
|
+
for (const name of selected) {
|
|
319
|
+
data.plugins.push(name);
|
|
293
320
|
}
|
|
294
|
-
|
|
295
|
-
|
|
321
|
+
writeProfile(normalizedProfileName, data);
|
|
322
|
+
if (selected.length > 0) {
|
|
323
|
+
p.log.success(`Added ${selected.length} plugin(s) to profile "${normalizedProfileName}".`);
|
|
296
324
|
}
|
|
325
|
+
return;
|
|
297
326
|
}
|
|
298
327
|
plugin = normalizePluginName(plugin);
|
|
299
328
|
if (!plugin)
|
|
@@ -355,54 +384,55 @@ async function listPlugins(profileName) {
|
|
|
355
384
|
p.log.success(pl);
|
|
356
385
|
}
|
|
357
386
|
}
|
|
358
|
-
async function
|
|
359
|
-
if (!keyword) {
|
|
360
|
-
if (!canPrompt()) {
|
|
361
|
-
missingArg("Search keyword is required.", "ccx search <keyword>");
|
|
362
|
-
return undefined;
|
|
363
|
-
}
|
|
364
|
-
keyword = (await p.text({
|
|
365
|
-
message: "Search plugins:",
|
|
366
|
-
}));
|
|
367
|
-
if (p.isCancel(keyword))
|
|
368
|
-
return undefined;
|
|
369
|
-
}
|
|
387
|
+
async function browsePlugins(currentProfile) {
|
|
370
388
|
const allPlugins = getAllPlugins();
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
389
|
+
if (allPlugins.length === 0) {
|
|
390
|
+
p.log.warn("No plugins available in marketplaces.");
|
|
391
|
+
return [];
|
|
392
|
+
}
|
|
393
|
+
let plugins = allPlugins;
|
|
394
|
+
if (allPlugins.length > 15) {
|
|
395
|
+
const query = (await p.text({
|
|
396
|
+
message: "Search plugins (leave empty to list all):",
|
|
397
|
+
}));
|
|
398
|
+
if (p.isCancel(query))
|
|
399
|
+
return [];
|
|
400
|
+
const q = query.trim().toLowerCase();
|
|
401
|
+
if (q) {
|
|
402
|
+
plugins = allPlugins.filter((pl) => pl.name.toLowerCase().includes(q) ||
|
|
403
|
+
pl.description.toLowerCase().includes(q) ||
|
|
404
|
+
(pl.category || "").toLowerCase().includes(q));
|
|
405
|
+
if (plugins.length === 0) {
|
|
406
|
+
p.log.warn(`No plugins matching "${query.trim()}".`);
|
|
407
|
+
return [];
|
|
408
|
+
}
|
|
409
|
+
}
|
|
378
410
|
}
|
|
379
411
|
if (currentProfile) {
|
|
380
412
|
const data = readProfile(currentProfile);
|
|
381
|
-
const selected = await p.
|
|
382
|
-
message:
|
|
383
|
-
options:
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
{ value: "__back__", label: "Back", hint: "Return to menu" },
|
|
390
|
-
],
|
|
413
|
+
const selected = await p.multiselect({
|
|
414
|
+
message: "Select plugins to add:",
|
|
415
|
+
options: plugins.map((pl) => ({
|
|
416
|
+
value: pl.name,
|
|
417
|
+
label: pl.name,
|
|
418
|
+
hint: pl.description.slice(0, 50) + (data.plugins.includes(pl.name) ? " (installed)" : ""),
|
|
419
|
+
})),
|
|
420
|
+
required: false,
|
|
391
421
|
});
|
|
392
|
-
if (p.isCancel(selected)
|
|
393
|
-
return
|
|
394
|
-
return selected;
|
|
422
|
+
if (p.isCancel(selected))
|
|
423
|
+
return [];
|
|
424
|
+
return selected.filter((name) => !data.plugins.includes(name));
|
|
395
425
|
}
|
|
396
426
|
const grouped = new Map();
|
|
397
|
-
for (const pl of
|
|
427
|
+
for (const pl of plugins) {
|
|
398
428
|
const list = grouped.get(pl.marketplace) || [];
|
|
399
429
|
list.push(pl);
|
|
400
430
|
grouped.set(pl.marketplace, list);
|
|
401
431
|
}
|
|
402
|
-
console.log(`\n
|
|
403
|
-
for (const [marketplace,
|
|
432
|
+
console.log(`\n ${plugins.length} plugin(s) available:\n`);
|
|
433
|
+
for (const [marketplace, mPlugins] of grouped) {
|
|
404
434
|
console.log(` [${marketplace}]`);
|
|
405
|
-
for (const pl of
|
|
435
|
+
for (const pl of mPlugins) {
|
|
406
436
|
const desc = pl.description.length > 70
|
|
407
437
|
? pl.description.slice(0, 67) + "..."
|
|
408
438
|
: pl.description;
|
|
@@ -410,22 +440,14 @@ async function searchPlugins(keyword, currentProfile) {
|
|
|
410
440
|
}
|
|
411
441
|
console.log();
|
|
412
442
|
}
|
|
413
|
-
return
|
|
443
|
+
return [];
|
|
414
444
|
}
|
|
415
|
-
async function
|
|
416
|
-
const normalizedProfileName = normalizeProfileName(profileName);
|
|
417
|
-
if (!normalizedProfileName)
|
|
418
|
-
return;
|
|
419
|
-
const data = readProfile(normalizedProfileName);
|
|
420
|
-
if (data.plugins.length === 0) {
|
|
421
|
-
p.log.warn(`No plugins to install in profile "${normalizedProfileName}".`);
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
445
|
+
async function installPlugins(plugins, label) {
|
|
424
446
|
const s = p.spinner();
|
|
425
|
-
s.start(`Installing ${
|
|
447
|
+
s.start(`Installing ${plugins.length} plugin(s) from "${label}"...`);
|
|
426
448
|
let installed = 0;
|
|
427
449
|
let failed = 0;
|
|
428
|
-
for (const plugin of
|
|
450
|
+
for (const plugin of plugins) {
|
|
429
451
|
s.message(`Installing ${plugin}...`);
|
|
430
452
|
try {
|
|
431
453
|
execFileSync("claude", ["plugin", "install", plugin, "--scope", "project"], {
|
|
@@ -441,6 +463,111 @@ async function executeProfile(profileName) {
|
|
|
441
463
|
s.stop(`${installed} installed${failed > 0 ? `, ${failed} failed` : ""}`);
|
|
442
464
|
p.log.success("Done.");
|
|
443
465
|
}
|
|
466
|
+
async function executeProfile(profileName) {
|
|
467
|
+
const normalizedProfileName = normalizeProfileName(profileName);
|
|
468
|
+
if (!normalizedProfileName)
|
|
469
|
+
return;
|
|
470
|
+
const data = readProfile(normalizedProfileName);
|
|
471
|
+
if (data.plugins.length === 0) {
|
|
472
|
+
p.log.warn(`No plugins to install in profile "${normalizedProfileName}".`);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
await installPlugins(data.plugins, normalizedProfileName);
|
|
476
|
+
}
|
|
477
|
+
async function executeProjectConfig() {
|
|
478
|
+
const file = path.join(process.cwd(), PROJECT_CONFIG_FILE);
|
|
479
|
+
if (!fs.existsSync(file)) {
|
|
480
|
+
p.log.error(`No ${PROJECT_CONFIG_FILE} found. Run \`ccx init\` to create one.`);
|
|
481
|
+
process.exitCode = 1;
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const plugins = readProjectConfig();
|
|
485
|
+
if (plugins.length === 0) {
|
|
486
|
+
p.log.warn(`No plugins in ${PROJECT_CONFIG_FILE}.`);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
await installPlugins(plugins, PROJECT_CONFIG_FILE);
|
|
490
|
+
}
|
|
491
|
+
async function initProjectConfig() {
|
|
492
|
+
if (!canPrompt()) {
|
|
493
|
+
console.error("ccx init requires a TTY.");
|
|
494
|
+
process.exitCode = 1;
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const file = path.join(process.cwd(), PROJECT_CONFIG_FILE);
|
|
498
|
+
if (fs.existsSync(file)) {
|
|
499
|
+
const overwrite = await p.confirm({
|
|
500
|
+
message: `${PROJECT_CONFIG_FILE} already exists. Overwrite?`,
|
|
501
|
+
initialValue: false,
|
|
502
|
+
});
|
|
503
|
+
if (p.isCancel(overwrite) || !overwrite)
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const allPlugins = getAllPlugins();
|
|
507
|
+
let selected;
|
|
508
|
+
if (allPlugins.length === 0) {
|
|
509
|
+
const input = (await p.text({
|
|
510
|
+
message: "No marketplace plugins found. Enter plugin names (comma-separated):",
|
|
511
|
+
placeholder: "plugin-a, plugin-b",
|
|
512
|
+
}));
|
|
513
|
+
if (p.isCancel(input))
|
|
514
|
+
return;
|
|
515
|
+
selected = input.split(",").map((s) => s.trim()).filter(Boolean);
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
let filtered = allPlugins;
|
|
519
|
+
if (allPlugins.length > 10) {
|
|
520
|
+
const query = (await p.text({
|
|
521
|
+
message: "Search plugins (leave empty to list all):",
|
|
522
|
+
}));
|
|
523
|
+
if (p.isCancel(query))
|
|
524
|
+
return;
|
|
525
|
+
const q = query.trim().toLowerCase();
|
|
526
|
+
if (q) {
|
|
527
|
+
filtered = allPlugins.filter((pl) => pl.name.toLowerCase().includes(q) ||
|
|
528
|
+
pl.description.toLowerCase().includes(q) ||
|
|
529
|
+
(pl.category && pl.category.toLowerCase().includes(q)));
|
|
530
|
+
if (filtered.length === 0) {
|
|
531
|
+
p.log.warn(`No plugins matching "${query.trim()}".`);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const picked = await p.multiselect({
|
|
537
|
+
message: "Select plugins for this project:",
|
|
538
|
+
options: filtered.map((pl) => ({
|
|
539
|
+
value: pl.name,
|
|
540
|
+
label: pl.name,
|
|
541
|
+
hint: pl.description.slice(0, 50),
|
|
542
|
+
})),
|
|
543
|
+
required: false,
|
|
544
|
+
});
|
|
545
|
+
if (p.isCancel(picked))
|
|
546
|
+
return;
|
|
547
|
+
selected = picked;
|
|
548
|
+
}
|
|
549
|
+
writeProjectConfig(selected);
|
|
550
|
+
p.log.success(`Created ${PROJECT_CONFIG_FILE} with ${selected.length} plugin(s).`);
|
|
551
|
+
}
|
|
552
|
+
function syncProjectConfig() {
|
|
553
|
+
const pluginsDir = path.join(process.cwd(), ".claude", "plugins");
|
|
554
|
+
if (!fs.existsSync(pluginsDir)) {
|
|
555
|
+
p.log.warn("No project plugins found (.claude/plugins/).");
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
const entries = fs.readdirSync(pluginsDir, { withFileTypes: true });
|
|
559
|
+
const plugins = entries
|
|
560
|
+
.filter((e) => e.isDirectory())
|
|
561
|
+
.map((e) => e.name)
|
|
562
|
+
.filter((name) => !name.startsWith("."))
|
|
563
|
+
.sort();
|
|
564
|
+
if (plugins.length === 0) {
|
|
565
|
+
p.log.warn("No project plugins found (.claude/plugins/).");
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
writeProjectConfig(plugins);
|
|
569
|
+
p.log.success(`Synced ${plugins.length} plugin(s) to ${PROJECT_CONFIG_FILE}.`);
|
|
570
|
+
}
|
|
444
571
|
function printBanner() {
|
|
445
572
|
const require = createRequire(import.meta.url);
|
|
446
573
|
const pkg = require("../package.json");
|
|
@@ -463,13 +590,16 @@ async function interactiveMode() {
|
|
|
463
590
|
let stayInProfile = true;
|
|
464
591
|
while (stayInProfile && currentProfile) {
|
|
465
592
|
const data = readProfile(currentProfile);
|
|
593
|
+
const pluginList = data.plugins.length > 0
|
|
594
|
+
? data.plugins.map((pl) => ` ${pc.dim("•")} ${pl}`).join("\n")
|
|
595
|
+
: pc.dim(" (empty)");
|
|
596
|
+
p.note(`${pc.bold(pc.cyan(currentProfile))}\n${pluginList}`);
|
|
466
597
|
const action = await p.select({
|
|
467
|
-
message:
|
|
598
|
+
message: "Choose action:",
|
|
468
599
|
options: [
|
|
469
600
|
{ value: "install", label: "Install", hint: "Apply to current project" },
|
|
470
|
-
{ value: "add", label: "Add plugin", hint: "Search and add
|
|
601
|
+
{ value: "add", label: "Add plugin", hint: "Search and add plugins" },
|
|
471
602
|
{ value: "remove", label: "Remove plugin", hint: "Remove a plugin" },
|
|
472
|
-
{ value: "list", label: "List plugins", hint: "Show all plugins" },
|
|
473
603
|
{ value: "search", label: "Search marketplace", hint: "Find new plugins" },
|
|
474
604
|
{ value: "switch", label: "Switch profile", hint: "Choose a different profile" },
|
|
475
605
|
{ value: "delete", label: "Delete profile", hint: "Remove this profile" },
|
|
@@ -491,21 +621,13 @@ async function interactiveMode() {
|
|
|
491
621
|
case "remove":
|
|
492
622
|
await removePlugin(currentProfile);
|
|
493
623
|
break;
|
|
494
|
-
case "list":
|
|
495
|
-
await listPlugins(currentProfile);
|
|
496
|
-
break;
|
|
497
624
|
case "search": {
|
|
498
|
-
const found = await
|
|
499
|
-
if (found) {
|
|
625
|
+
const found = await browsePlugins(currentProfile);
|
|
626
|
+
if (found.length > 0) {
|
|
500
627
|
const d = readProfile(currentProfile);
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
}
|
|
504
|
-
else {
|
|
505
|
-
d.plugins.push(found);
|
|
506
|
-
writeProfile(currentProfile, d);
|
|
507
|
-
p.log.success(`Added "${found}" to profile "${currentProfile}".`);
|
|
508
|
-
}
|
|
628
|
+
d.plugins.push(...found);
|
|
629
|
+
writeProfile(currentProfile, d);
|
|
630
|
+
p.log.success(`Added ${found.length} plugin(s) to profile "${currentProfile}".`);
|
|
509
631
|
}
|
|
510
632
|
break;
|
|
511
633
|
}
|
|
@@ -528,6 +650,9 @@ function printHelp() {
|
|
|
528
650
|
Usage:
|
|
529
651
|
ccx Interactive mode (TTY only)
|
|
530
652
|
ccx ui Interactive mode (TTY only)
|
|
653
|
+
ccx init Create .ccx.json for current project
|
|
654
|
+
ccx sync Sync installed plugins to .ccx.json
|
|
655
|
+
ccx install Install plugins from .ccx.json
|
|
531
656
|
ccx install <profile> Install all plugins from profile
|
|
532
657
|
ccx create <name> Create a new profile
|
|
533
658
|
ccx delete <name> Remove a profile
|
|
@@ -563,6 +688,12 @@ async function main(args) {
|
|
|
563
688
|
}
|
|
564
689
|
const cmd = args[0];
|
|
565
690
|
switch (cmd) {
|
|
691
|
+
case "init":
|
|
692
|
+
await initProjectConfig();
|
|
693
|
+
break;
|
|
694
|
+
case "sync":
|
|
695
|
+
syncProjectConfig();
|
|
696
|
+
break;
|
|
566
697
|
case "ui":
|
|
567
698
|
case "tui":
|
|
568
699
|
case "interactive":
|
|
@@ -570,7 +701,7 @@ async function main(args) {
|
|
|
570
701
|
break;
|
|
571
702
|
case "install":
|
|
572
703
|
if (!args[1]) {
|
|
573
|
-
|
|
704
|
+
await executeProjectConfig();
|
|
574
705
|
return;
|
|
575
706
|
}
|
|
576
707
|
await executeProfile(args[1]);
|
|
@@ -609,7 +740,7 @@ async function main(args) {
|
|
|
609
740
|
}
|
|
610
741
|
break;
|
|
611
742
|
case "search":
|
|
612
|
-
await
|
|
743
|
+
await browsePlugins();
|
|
613
744
|
break;
|
|
614
745
|
default: {
|
|
615
746
|
const profileName = cmd;
|