@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.
Files changed (2) hide show
  1. package/dist/index.js +209 -78
  2. 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.select({
275
- message: `Add plugin to "${profileName}":`,
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, 60),
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
- if (selected === "__url__") {
288
- plugin = (await p.text({
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
- else {
295
- plugin = selected;
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 searchPlugins(keyword, currentProfile) {
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
- const lower = keyword.toLowerCase();
372
- const results = allPlugins.filter((pl) => pl.name.toLowerCase().includes(lower) ||
373
- pl.description.toLowerCase().includes(lower) ||
374
- (pl.category || "").toLowerCase().includes(lower));
375
- if (results.length === 0) {
376
- p.log.warn(`No plugins matching "${keyword}".`);
377
- return undefined;
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.select({
382
- message: `Search results for "${keyword}":`,
383
- options: [
384
- ...results.map((pl) => ({
385
- value: pl.name,
386
- label: pl.name,
387
- hint: pl.description.slice(0, 60) + (data.plugins.includes(pl.name) ? " ✓" : ""),
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) || selected === "__back__")
393
- return undefined;
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 results) {
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 Found ${results.length} plugin(s) for "${keyword}":\n`);
403
- for (const [marketplace, plugins] of grouped) {
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 plugins) {
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 undefined;
443
+ return [];
414
444
  }
415
- async function executeProfile(profileName) {
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 ${data.plugins.length} plugin(s) from "${normalizedProfileName}"...`);
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 data.plugins) {
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: pc.bold(pc.cyan(`[${currentProfile}]`)) + ` ${data.plugins.length} plugin(s)`,
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 a plugin" },
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 searchPlugins(undefined, currentProfile);
499
- if (found) {
625
+ const found = await browsePlugins(currentProfile);
626
+ if (found.length > 0) {
500
627
  const d = readProfile(currentProfile);
501
- if (d.plugins.includes(found)) {
502
- p.log.warn(`"${found}" already in profile "${currentProfile}".`);
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
- missingArg("Profile name is required.", "ccx install <profile>");
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 searchPlugins(args[1]);
743
+ await browsePlugins();
613
744
  break;
614
745
  default: {
615
746
  const profileName = cmd;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanmu/ccprofile",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Agent Profile Manager for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {