@guanmu/ccprofile 0.1.7 → 0.1.10

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 +414 -170
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import fs from "node:fs";
3
3
  import { createRequire } from "node:module";
4
4
  import path from "node:path";
5
- import { execSync } from "node:child_process";
5
+ import { execFileSync } from "node:child_process";
6
6
  import * as p from "@clack/prompts";
7
7
  const PROFILES_DIR = path.join(process.env.HOME, ".ccx", "profiles");
8
8
  const MARKETPLACES_DIR = path.join(process.env.HOME, ".claude", "plugins", "marketplaces");
@@ -12,24 +12,77 @@ function ensureProfilesDir() {
12
12
  function profilePath(name) {
13
13
  return path.join(PROFILES_DIR, `${name}.json`);
14
14
  }
15
+ function isValidProfileName(name) {
16
+ return /^[A-Za-z0-9._-]+$/.test(name) && name !== "." && name !== "..";
17
+ }
18
+ function normalizeProfileName(name) {
19
+ if (!name)
20
+ return undefined;
21
+ const normalized = name.trim();
22
+ if (!isValidProfileName(normalized)) {
23
+ console.error("Invalid profile name. Use only letters, numbers, dots, underscores, and hyphens.");
24
+ process.exitCode = 1;
25
+ return undefined;
26
+ }
27
+ return normalized;
28
+ }
29
+ function normalizePluginName(plugin) {
30
+ if (!plugin)
31
+ return undefined;
32
+ const normalized = plugin.trim();
33
+ if (!normalized) {
34
+ console.error("Plugin name is required.");
35
+ process.exitCode = 1;
36
+ return undefined;
37
+ }
38
+ return normalized;
39
+ }
15
40
  function readProfile(name) {
16
- const file = profilePath(name);
41
+ const normalized = normalizeProfileName(name);
42
+ if (!normalized)
43
+ process.exit(1);
44
+ const file = profilePath(normalized);
17
45
  if (!fs.existsSync(file)) {
18
- p.log.error(`Profile "${name}" not found.`);
46
+ p.log.error(`Profile "${normalized}" not found.`);
47
+ process.exit(1);
48
+ }
49
+ let data;
50
+ try {
51
+ data = JSON.parse(fs.readFileSync(file, "utf-8"));
52
+ }
53
+ catch {
54
+ p.log.error(`Invalid profile file "${normalized}". Expected valid JSON.`);
19
55
  process.exit(1);
20
56
  }
21
- return JSON.parse(fs.readFileSync(file, "utf-8"));
57
+ if (!data ||
58
+ typeof data !== "object" ||
59
+ !Array.isArray(data.plugins)) {
60
+ p.log.error(`Invalid profile file "${normalized}". Expected a plugins array.`);
61
+ process.exit(1);
62
+ }
63
+ const plugins = data.plugins.map((plugin) => typeof plugin === "string" ? plugin.trim() : undefined);
64
+ if (plugins.some((plugin) => !plugin)) {
65
+ p.log.error(`Invalid profile file "${normalized}". Plugin entries must be non-empty strings.`);
66
+ process.exit(1);
67
+ }
68
+ return {
69
+ name: normalized,
70
+ plugins: plugins,
71
+ };
22
72
  }
23
73
  function writeProfile(name, data) {
74
+ const normalized = normalizeProfileName(name);
75
+ if (!normalized)
76
+ return;
24
77
  ensureProfilesDir();
25
- fs.writeFileSync(profilePath(name), JSON.stringify(data, null, 2) + "\n");
78
+ fs.writeFileSync(profilePath(normalized), JSON.stringify({ ...data, name: normalized }, null, 2) + "\n");
26
79
  }
27
80
  function getProfileNames() {
28
81
  ensureProfilesDir();
29
82
  return fs
30
83
  .readdirSync(PROFILES_DIR)
31
84
  .filter((f) => f.endsWith(".json"))
32
- .map((f) => f.replace(".json", ""));
85
+ .map((f) => f.slice(0, -".json".length));
33
86
  }
34
87
  function getAllPlugins() {
35
88
  if (!fs.existsSync(MARKETPLACES_DIR))
@@ -40,27 +93,83 @@ function getAllPlugins() {
40
93
  const file = path.join(MARKETPLACES_DIR, dir, ".claude-plugin", "marketplace.json");
41
94
  if (!fs.existsSync(file))
42
95
  continue;
43
- const data = JSON.parse(fs.readFileSync(file, "utf-8"));
44
- for (const pl of data.plugins || []) {
96
+ let data;
97
+ try {
98
+ data = JSON.parse(fs.readFileSync(file, "utf-8"));
99
+ }
100
+ catch {
101
+ p.log.warn(`Skipping invalid marketplace "${dir}".`);
102
+ continue;
103
+ }
104
+ if (!data ||
105
+ typeof data !== "object" ||
106
+ !Array.isArray(data.plugins)) {
107
+ p.log.warn(`Skipping invalid marketplace "${dir}".`);
108
+ continue;
109
+ }
110
+ const marketplace = typeof data.name === "string"
111
+ ? (data.name || dir)
112
+ : dir;
113
+ for (const pl of data.plugins) {
114
+ if (!pl || typeof pl !== "object")
115
+ continue;
116
+ const name = pl.name;
117
+ if (typeof name !== "string" || !name.trim())
118
+ continue;
119
+ const description = pl.description;
120
+ const category = pl.category;
45
121
  plugins.push({
46
- name: pl.name,
47
- description: pl.description || "",
48
- category: pl.category,
49
- marketplace: data.name,
122
+ name: name.trim(),
123
+ description: typeof description === "string" ? description : "",
124
+ category: typeof category === "string" ? category : undefined,
125
+ marketplace,
50
126
  });
51
127
  }
52
128
  }
53
129
  return plugins;
54
130
  }
131
+ function canPrompt() {
132
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
133
+ }
134
+ function printNonInteractiveHelp() {
135
+ console.error("Interactive mode requires a TTY. Run `ccx --help` for command usage.");
136
+ printHelp();
137
+ }
138
+ function missingArg(message, usage) {
139
+ console.error(message);
140
+ console.error(`Usage: ${usage}`);
141
+ process.exitCode = 1;
142
+ }
143
+ async function selectProfile(message) {
144
+ const names = getProfileNames();
145
+ if (names.length === 0) {
146
+ p.log.warn("No profiles found.");
147
+ return undefined;
148
+ }
149
+ const name = await p.select({
150
+ message,
151
+ options: names.map((n) => ({ value: n, label: n })),
152
+ });
153
+ if (p.isCancel(name))
154
+ return undefined;
155
+ return name;
156
+ }
55
157
  // ── Commands ──────────────────────────────────────────────
56
158
  async function addProfile(name) {
57
159
  if (!name) {
160
+ if (!canPrompt()) {
161
+ missingArg("Profile name is required.", "ccx create <name>");
162
+ return;
163
+ }
58
164
  name = (await p.text({
59
165
  message: "Profile name:",
60
166
  }));
61
167
  if (p.isCancel(name))
62
168
  return;
63
169
  }
170
+ name = normalizeProfileName(name);
171
+ if (!name)
172
+ return;
64
173
  const file = profilePath(name);
65
174
  if (fs.existsSync(file)) {
66
175
  p.log.error(`Profile "${name}" already exists.`);
@@ -70,12 +179,20 @@ async function addProfile(name) {
70
179
  p.log.success(`Created profile "${name}".`);
71
180
  }
72
181
  async function removeProfile(name) {
182
+ if (!name && !canPrompt()) {
183
+ missingArg("Profile name is required.", "ccx delete <name>");
184
+ return;
185
+ }
73
186
  const names = getProfileNames();
74
187
  if (names.length === 0) {
75
188
  p.log.warn("No profiles found.");
76
189
  return;
77
190
  }
78
191
  if (!name) {
192
+ if (!canPrompt()) {
193
+ missingArg("Profile name is required.", "ccx delete <name>");
194
+ return;
195
+ }
79
196
  name = (await p.select({
80
197
  message: "Select profile to remove:",
81
198
  options: names.map((n) => ({ value: n, label: n })),
@@ -83,6 +200,9 @@ async function removeProfile(name) {
83
200
  if (p.isCancel(name))
84
201
  return;
85
202
  }
203
+ name = normalizeProfileName(name);
204
+ if (!name)
205
+ return;
86
206
  const file = profilePath(name);
87
207
  if (!fs.existsSync(file)) {
88
208
  p.log.error(`Profile "${name}" not found.`);
@@ -92,22 +212,26 @@ async function removeProfile(name) {
92
212
  p.log.success(`Removed profile "${name}".`);
93
213
  }
94
214
  async function listProfiles() {
95
- ensureProfilesDir();
96
- const files = fs
97
- .readdirSync(PROFILES_DIR)
98
- .filter((f) => f.endsWith(".json"));
99
- if (files.length === 0) {
215
+ const names = getProfileNames();
216
+ if (names.length === 0) {
100
217
  p.log.warn("No profiles found.");
101
218
  return;
102
219
  }
103
- for (const f of files) {
104
- const data = JSON.parse(fs.readFileSync(path.join(PROFILES_DIR, f), "utf-8"));
220
+ for (const name of names) {
221
+ const data = readProfile(name);
105
222
  p.log.success(`${data.name} (${data.plugins.length} plugins)`);
106
223
  }
107
224
  }
108
225
  async function addPlugin(profileName, plugin) {
226
+ const normalizedProfileName = normalizeProfileName(profileName);
227
+ if (!normalizedProfileName)
228
+ return;
109
229
  const data = readProfile(profileName);
110
230
  if (!plugin) {
231
+ if (!canPrompt()) {
232
+ missingArg("Plugin name is required.", "ccx add <profile> <plugin>");
233
+ return;
234
+ }
111
235
  const allPlugins = getAllPlugins();
112
236
  const available = allPlugins.filter((pl) => !data.plugins.includes(pl.name));
113
237
  if (available.length === 0) {
@@ -138,21 +262,31 @@ async function addPlugin(profileName, plugin) {
138
262
  plugin = selected;
139
263
  }
140
264
  }
265
+ plugin = normalizePluginName(plugin);
266
+ if (!plugin)
267
+ return;
141
268
  if (data.plugins.includes(plugin)) {
142
269
  p.log.warn(`Plugin "${plugin}" already in profile.`);
143
270
  return;
144
271
  }
145
272
  data.plugins.push(plugin);
146
- writeProfile(profileName, data);
147
- p.log.success(`Added "${plugin}" to profile "${profileName}".`);
273
+ writeProfile(normalizedProfileName, data);
274
+ p.log.success(`Added "${plugin}" to profile "${normalizedProfileName}".`);
148
275
  }
149
276
  async function removePlugin(profileName, plugin) {
277
+ const normalizedProfileName = normalizeProfileName(profileName);
278
+ if (!normalizedProfileName)
279
+ return;
150
280
  const data = readProfile(profileName);
151
281
  if (data.plugins.length === 0) {
152
282
  p.log.warn("No plugins in this profile.");
153
283
  return;
154
284
  }
155
285
  if (!plugin) {
286
+ if (!canPrompt()) {
287
+ missingArg("Plugin name is required.", "ccx remove <profile> <plugin>");
288
+ return;
289
+ }
156
290
  plugin = (await p.select({
157
291
  message: `Remove plugin from "${profileName}":`,
158
292
  options: data.plugins.map((pl) => ({
@@ -163,19 +297,25 @@ async function removePlugin(profileName, plugin) {
163
297
  if (p.isCancel(plugin))
164
298
  return;
165
299
  }
300
+ plugin = normalizePluginName(plugin);
301
+ if (!plugin)
302
+ return;
166
303
  const idx = data.plugins.indexOf(plugin);
167
304
  if (idx === -1) {
168
305
  p.log.error(`Plugin "${plugin}" not found.`);
169
306
  return;
170
307
  }
171
308
  data.plugins.splice(idx, 1);
172
- writeProfile(profileName, data);
173
- p.log.success(`Removed "${plugin}" from profile "${profileName}".`);
309
+ writeProfile(normalizedProfileName, data);
310
+ p.log.success(`Removed "${plugin}" from profile "${normalizedProfileName}".`);
174
311
  }
175
312
  async function listPlugins(profileName) {
176
- const data = readProfile(profileName);
313
+ const normalizedProfileName = normalizeProfileName(profileName);
314
+ if (!normalizedProfileName)
315
+ return;
316
+ const data = readProfile(normalizedProfileName);
177
317
  if (data.plugins.length === 0) {
178
- p.log.warn(`No plugins in profile "${profileName}".`);
318
+ p.log.warn(`No plugins in profile "${normalizedProfileName}".`);
179
319
  return;
180
320
  }
181
321
  for (const pl of data.plugins) {
@@ -184,6 +324,10 @@ async function listPlugins(profileName) {
184
324
  }
185
325
  async function searchPlugins(keyword) {
186
326
  if (!keyword) {
327
+ if (!canPrompt()) {
328
+ missingArg("Search keyword is required.", "ccx search <keyword>");
329
+ return;
330
+ }
187
331
  keyword = (await p.text({
188
332
  message: "Search plugins:",
189
333
  }));
@@ -218,19 +362,22 @@ async function searchPlugins(keyword) {
218
362
  }
219
363
  }
220
364
  async function executeProfile(profileName) {
221
- const data = readProfile(profileName);
365
+ const normalizedProfileName = normalizeProfileName(profileName);
366
+ if (!normalizedProfileName)
367
+ return;
368
+ const data = readProfile(normalizedProfileName);
222
369
  if (data.plugins.length === 0) {
223
- p.log.warn(`No plugins to install in profile "${profileName}".`);
370
+ p.log.warn(`No plugins to install in profile "${normalizedProfileName}".`);
224
371
  return;
225
372
  }
226
373
  const s = p.spinner();
227
- s.start(`Installing ${data.plugins.length} plugin(s) from "${profileName}"...`);
374
+ s.start(`Installing ${data.plugins.length} plugin(s) from "${normalizedProfileName}"...`);
228
375
  let installed = 0;
229
376
  let failed = 0;
230
377
  for (const plugin of data.plugins) {
231
378
  s.message(`Installing ${plugin}...`);
232
379
  try {
233
- execSync(`claude plugin install ${plugin} --scope project`, {
380
+ execFileSync("claude", ["plugin", "install", plugin, "--scope", "project"], {
234
381
  stdio: "pipe",
235
382
  });
236
383
  installed++;
@@ -243,164 +390,261 @@ async function executeProfile(profileName) {
243
390
  s.stop(`${installed} installed${failed > 0 ? `, ${failed} failed` : ""}`);
244
391
  p.log.success("Done.");
245
392
  }
393
+ async function installWizard() {
394
+ while (true) {
395
+ const action = await p.select({
396
+ message: "Install",
397
+ options: [
398
+ { value: "install", label: "Install profile plugins", hint: "Run a profile" },
399
+ { value: "back", label: "Back" },
400
+ { value: "exit", label: "Exit" },
401
+ ],
402
+ });
403
+ if (p.isCancel(action) || action === "exit")
404
+ return "exit";
405
+ if (action === "back")
406
+ return "back";
407
+ const name = await selectProfile("Select profile to install:");
408
+ if (name)
409
+ await executeProfile(name);
410
+ }
411
+ }
412
+ async function profilesWizard() {
413
+ while (true) {
414
+ const action = await p.select({
415
+ message: "Profiles",
416
+ options: [
417
+ { value: "create", label: "Create profile", hint: "Create a new empty profile" },
418
+ { value: "list", label: "List profiles", hint: "Show all profiles" },
419
+ { value: "delete", label: "Delete profile", hint: "Remove a profile" },
420
+ { value: "back", label: "Back" },
421
+ { value: "exit", label: "Exit" },
422
+ ],
423
+ });
424
+ if (p.isCancel(action) || action === "exit")
425
+ return "exit";
426
+ if (action === "back")
427
+ return "back";
428
+ switch (action) {
429
+ case "create":
430
+ await addProfile();
431
+ break;
432
+ case "list":
433
+ await listProfiles();
434
+ break;
435
+ case "delete":
436
+ await removeProfile();
437
+ break;
438
+ }
439
+ }
440
+ }
441
+ async function pluginsWizard() {
442
+ while (true) {
443
+ const action = await p.select({
444
+ message: "Plugins",
445
+ options: [
446
+ { value: "add", label: "Add plugin to profile", hint: "Choose a profile, then a plugin" },
447
+ { value: "remove", label: "Remove plugin from profile", hint: "Choose a profile, then a plugin" },
448
+ { value: "list", label: "List profile plugins", hint: "Show plugins in a profile" },
449
+ { value: "back", label: "Back" },
450
+ { value: "exit", label: "Exit" },
451
+ ],
452
+ });
453
+ if (p.isCancel(action) || action === "exit")
454
+ return "exit";
455
+ if (action === "back")
456
+ return "back";
457
+ const name = await selectProfile("Select profile:");
458
+ if (!name)
459
+ continue;
460
+ switch (action) {
461
+ case "add":
462
+ await addPlugin(name);
463
+ break;
464
+ case "remove":
465
+ await removePlugin(name);
466
+ break;
467
+ case "list":
468
+ await listPlugins(name);
469
+ break;
470
+ }
471
+ }
472
+ }
473
+ async function marketplaceWizard() {
474
+ while (true) {
475
+ const action = await p.select({
476
+ message: "Marketplace",
477
+ options: [
478
+ { value: "search", label: "Search plugins", hint: "Search installed marketplaces" },
479
+ { value: "back", label: "Back" },
480
+ { value: "exit", label: "Exit" },
481
+ ],
482
+ });
483
+ if (p.isCancel(action) || action === "exit")
484
+ return "exit";
485
+ if (action === "back")
486
+ return "back";
487
+ await searchPlugins();
488
+ }
489
+ }
246
490
  async function interactiveMode() {
247
- p.intro("ccx — Agent Profile Manager");
248
- const action = await p.select({
249
- message: "What do you want to do?",
250
- options: [
251
- { value: "install", label: "Install profile plugins", hint: "Run a profile to install its plugins" },
252
- { value: "plugin-add", label: "Add plugin to profile", hint: "Add a plugin to an existing profile" },
253
- { value: "plugin-remove", label: "Remove plugin from profile", hint: "Remove a plugin from a profile" },
254
- { value: "plugin-list", label: "List profile plugins", hint: "Show plugins in a profile" },
255
- { value: "add", label: "Create new profile", hint: "Create a new empty profile" },
256
- { value: "remove", label: "Delete profile", hint: "Remove a profile" },
257
- { value: "list", label: "List all profiles", hint: "Show all profiles" },
258
- { value: "search", label: "Search plugins", hint: "Search plugins in marketplaces" },
259
- ],
260
- });
261
- if (p.isCancel(action))
491
+ if (!canPrompt()) {
492
+ printNonInteractiveHelp();
493
+ process.exitCode = 1;
262
494
  return;
263
- switch (action) {
264
- case "install": {
265
- const names = getProfileNames();
266
- if (names.length === 0) {
267
- p.log.warn("No profiles found.");
268
- return;
269
- }
270
- const name = await p.select({
271
- message: "Select profile to install:",
272
- options: names.map((n) => ({ value: n, label: n })),
273
- });
274
- if (p.isCancel(name))
275
- return;
276
- await executeProfile(name);
495
+ }
496
+ p.intro("ccx — Agent Profile Manager");
497
+ let shouldExit = false;
498
+ while (!shouldExit) {
499
+ const area = await p.select({
500
+ message: "Choose area:",
501
+ options: [
502
+ { value: "install", label: "Install", hint: "Run a profile" },
503
+ { value: "profiles", label: "Profiles", hint: "Create, list, or delete profiles" },
504
+ { value: "plugins", label: "Plugins", hint: "Manage plugins inside profiles" },
505
+ { value: "marketplace", label: "Marketplace", hint: "Search available plugins" },
506
+ { value: "help", label: "Help", hint: "Show command usage" },
507
+ { value: "exit", label: "Exit" },
508
+ ],
509
+ });
510
+ if (p.isCancel(area) || area === "exit")
277
511
  break;
512
+ let result = "back";
513
+ switch (area) {
514
+ case "install":
515
+ result = await installWizard();
516
+ break;
517
+ case "profiles":
518
+ result = await profilesWizard();
519
+ break;
520
+ case "plugins":
521
+ result = await pluginsWizard();
522
+ break;
523
+ case "marketplace":
524
+ result = await marketplaceWizard();
525
+ break;
526
+ case "help":
527
+ printHelp();
528
+ break;
278
529
  }
279
- case "plugin-add": {
280
- const names = getProfileNames();
281
- if (names.length === 0) {
282
- p.log.warn("No profiles found. Create one first.");
283
- return;
284
- }
285
- const name = await p.select({
286
- message: "Select profile:",
287
- options: names.map((n) => ({ value: n, label: n })),
288
- });
289
- if (p.isCancel(name))
290
- return;
291
- await addPlugin(name);
530
+ shouldExit = result === "exit";
531
+ }
532
+ p.outro("Done.");
533
+ }
534
+ function printHelp() {
535
+ console.log(`ccx — Agent Profile Manager for Claude Code
536
+
537
+ Usage:
538
+ ccx Interactive mode (TTY only)
539
+ ccx ui Interactive mode (TTY only)
540
+ ccx install <profile> Install all plugins from profile
541
+ ccx create <name> Create a new profile
542
+ ccx delete <name> Remove a profile
543
+ ccx profiles List all profiles
544
+ ccx add <profile> <plugin> Add plugin to profile
545
+ ccx remove <profile> <plugin> Remove plugin from profile
546
+ ccx list <profile> List plugins in profile
547
+ ccx search <keyword> Search plugins in marketplaces
548
+ ccx <profile> Install all plugins from profile
549
+ ccx <profile> add [plugin] Add plugin to profile (legacy)
550
+ ccx <profile> remove [plugin] Remove plugin from profile (legacy)
551
+ ccx <profile> list List plugins in profile (legacy)
552
+ ccx add <name> Create a new profile (legacy)
553
+ ccx remove <name> Remove a profile (legacy)
554
+ ccx list List all profiles (legacy)
555
+ ccx -v, --version Show version`);
556
+ }
557
+ // ── Main ──────────────────────────────────────────────────
558
+ async function main(args) {
559
+ if (args.length === 0) {
560
+ await interactiveMode();
561
+ return;
562
+ }
563
+ if (args[0] === "--help" || args[0] === "-h") {
564
+ printHelp();
565
+ return;
566
+ }
567
+ if (args[0] === "--version" || args[0] === "-v" || args[0] === "-V") {
568
+ const require = createRequire(import.meta.url);
569
+ const pkg = require("../package.json");
570
+ console.log(`ccx v${pkg.version}`);
571
+ return;
572
+ }
573
+ const cmd = args[0];
574
+ switch (cmd) {
575
+ case "ui":
576
+ case "tui":
577
+ case "interactive":
578
+ await interactiveMode();
292
579
  break;
293
- }
294
- case "plugin-remove": {
295
- const names = getProfileNames();
296
- if (names.length === 0) {
297
- p.log.warn("No profiles found.");
580
+ case "install":
581
+ if (!args[1]) {
582
+ missingArg("Profile name is required.", "ccx install <profile>");
298
583
  return;
299
584
  }
300
- const name = await p.select({
301
- message: "Select profile:",
302
- options: names.map((n) => ({ value: n, label: n })),
303
- });
304
- if (p.isCancel(name))
305
- return;
306
- await removePlugin(name);
585
+ await executeProfile(args[1]);
307
586
  break;
308
- }
309
- case "plugin-list": {
310
- const names = getProfileNames();
311
- if (names.length === 0) {
312
- p.log.warn("No profiles found.");
313
- return;
314
- }
315
- const name = await p.select({
316
- message: "Select profile:",
317
- options: names.map((n) => ({ value: n, label: n })),
318
- });
319
- if (p.isCancel(name))
320
- return;
321
- await listPlugins(name);
587
+ case "create":
588
+ await addProfile(args[1]);
589
+ break;
590
+ case "delete":
591
+ await removeProfile(args[1]);
592
+ break;
593
+ case "profiles":
594
+ await listProfiles();
322
595
  break;
323
- }
324
596
  case "add":
325
- await addProfile();
597
+ if (args[2]) {
598
+ await addPlugin(args[1], args[2]);
599
+ }
600
+ else {
601
+ await addProfile(args[1]);
602
+ }
326
603
  break;
327
604
  case "remove":
328
- await removeProfile();
605
+ if (args[2]) {
606
+ await removePlugin(args[1], args[2]);
607
+ }
608
+ else {
609
+ await removeProfile(args[1]);
610
+ }
329
611
  break;
330
612
  case "list":
331
- await listProfiles();
613
+ if (args[1]) {
614
+ await listPlugins(args[1]);
615
+ }
616
+ else {
617
+ await listProfiles();
618
+ }
332
619
  break;
333
620
  case "search":
334
- await searchPlugins();
621
+ await searchPlugins(args[1]);
622
+ break;
623
+ default: {
624
+ const profileName = cmd;
625
+ const sub = args[1];
626
+ if (!sub) {
627
+ await executeProfile(profileName);
628
+ }
629
+ else if (sub === "add") {
630
+ await addPlugin(profileName, args[2]);
631
+ }
632
+ else if (sub === "remove") {
633
+ await removePlugin(profileName, args[2]);
634
+ }
635
+ else if (sub === "list") {
636
+ await listPlugins(profileName);
637
+ }
638
+ else {
639
+ console.error(`Unknown command: ccx ${args.join(" ")}`);
640
+ printHelp();
641
+ process.exitCode = 1;
642
+ }
335
643
  break;
336
- }
337
- p.outro("Done.");
338
- }
339
- function printHelp() {
340
- console.log(`ccx — Agent Profile Manager for Claude Code
341
-
342
- Usage:
343
- ccx Interactive mode
344
- ccx <profile> Install all plugins from profile
345
- ccx add <name> Create a new profile
346
- ccx remove <name> Remove a profile
347
- ccx list List all profiles
348
- ccx search <keyword> Search plugins in marketplaces
349
- ccx <profile> add [plugin] Add plugin to profile
350
- ccx <profile> remove [plugin] Remove plugin from profile
351
- ccx <profile> list List plugins in profile
352
- ccx -v, --version Show version`);
353
- }
354
- // ── Main ──────────────────────────────────────────────────
355
- const args = process.argv.slice(2);
356
- if (args.length === 0) {
357
- interactiveMode();
358
- process.exit(0);
359
- }
360
- if (args[0] === "--help" || args[0] === "-h") {
361
- printHelp();
362
- process.exit(0);
363
- }
364
- if (args[0] === "--version" || args[0] === "-v" || args[0] === "-V") {
365
- const require = createRequire(import.meta.url);
366
- const pkg = require("../package.json");
367
- console.log(`ccx v${pkg.version}`);
368
- process.exit(0);
369
- }
370
- const cmd = args[0];
371
- switch (cmd) {
372
- case "add":
373
- addProfile(args[1]);
374
- break;
375
- case "remove":
376
- removeProfile(args[1]);
377
- break;
378
- case "list":
379
- listProfiles();
380
- break;
381
- case "search":
382
- searchPlugins(args[1]);
383
- break;
384
- default: {
385
- const profileName = cmd;
386
- const sub = args[1];
387
- if (!sub) {
388
- executeProfile(profileName);
389
- }
390
- else if (sub === "add") {
391
- addPlugin(profileName, args[2]);
392
- }
393
- else if (sub === "remove") {
394
- removePlugin(profileName, args[2]);
395
- }
396
- else if (sub === "list") {
397
- listPlugins(profileName);
398
- }
399
- else {
400
- console.error(`Unknown command: ccx ${args.join(" ")}`);
401
- printHelp();
402
- process.exit(1);
403
644
  }
404
- break;
405
645
  }
406
646
  }
647
+ main(process.argv.slice(2)).catch((err) => {
648
+ console.error(err instanceof Error ? err.message : String(err));
649
+ process.exitCode = 1;
650
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanmu/ccprofile",
3
- "version": "0.1.7",
3
+ "version": "0.1.10",
4
4
  "description": "Agent Profile Manager for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,6 +16,7 @@
16
16
  "scripts": {
17
17
  "build": "tsc",
18
18
  "dev": "bun run src/index.ts",
19
+ "test": "pnpm run build && node test/cli.test.mjs",
19
20
  "prepublishOnly": "npm run build"
20
21
  },
21
22
  "dependencies": {