@guanmu/ccprofile 0.1.4 → 0.1.6

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 +278 -85
  2. package/package.json +6 -1
package/dist/index.js CHANGED
@@ -2,7 +2,9 @@
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { execSync } from "node:child_process";
5
+ import * as p from "@clack/prompts";
5
6
  const PROFILES_DIR = path.join(process.env.HOME, ".ccx", "profiles");
7
+ const MARKETPLACES_DIR = path.join(process.env.HOME, ".claude", "plugins", "marketplaces");
6
8
  function ensureProfilesDir() {
7
9
  fs.mkdirSync(PROFILES_DIR, { recursive: true });
8
10
  }
@@ -12,7 +14,7 @@ function profilePath(name) {
12
14
  function readProfile(name) {
13
15
  const file = profilePath(name);
14
16
  if (!fs.existsSync(file)) {
15
- console.error(`Profile "${name}" not found.`);
17
+ p.log.error(`Profile "${name}" not found.`);
16
18
  process.exit(1);
17
19
  }
18
20
  return JSON.parse(fs.readFileSync(file, "utf-8"));
@@ -21,175 +23,366 @@ function writeProfile(name, data) {
21
23
  ensureProfilesDir();
22
24
  fs.writeFileSync(profilePath(name), JSON.stringify(data, null, 2) + "\n");
23
25
  }
24
- function addProfile(name) {
26
+ function getProfileNames() {
27
+ ensureProfilesDir();
28
+ return fs
29
+ .readdirSync(PROFILES_DIR)
30
+ .filter((f) => f.endsWith(".json"))
31
+ .map((f) => f.replace(".json", ""));
32
+ }
33
+ function getAllPlugins() {
34
+ if (!fs.existsSync(MARKETPLACES_DIR))
35
+ return [];
36
+ const plugins = [];
37
+ const dirs = fs.readdirSync(MARKETPLACES_DIR);
38
+ for (const dir of dirs) {
39
+ const file = path.join(MARKETPLACES_DIR, dir, ".claude-plugin", "marketplace.json");
40
+ if (!fs.existsSync(file))
41
+ continue;
42
+ const data = JSON.parse(fs.readFileSync(file, "utf-8"));
43
+ for (const pl of data.plugins || []) {
44
+ plugins.push({
45
+ name: pl.name,
46
+ description: pl.description || "",
47
+ category: pl.category,
48
+ marketplace: data.name,
49
+ });
50
+ }
51
+ }
52
+ return plugins;
53
+ }
54
+ // ── Commands ──────────────────────────────────────────────
55
+ async function addProfile(name) {
56
+ if (!name) {
57
+ name = (await p.text({
58
+ message: "Profile name:",
59
+ }));
60
+ if (p.isCancel(name))
61
+ return;
62
+ }
25
63
  const file = profilePath(name);
26
64
  if (fs.existsSync(file)) {
27
- console.error(`Profile "${name}" already exists.`);
28
- process.exit(1);
65
+ p.log.error(`Profile "${name}" already exists.`);
66
+ return;
29
67
  }
30
68
  writeProfile(name, { name, plugins: [] });
31
- console.log(`Created profile "${name}".`);
69
+ p.log.success(`Created profile "${name}".`);
32
70
  }
33
- function removeProfile(name) {
71
+ async function removeProfile(name) {
72
+ const names = getProfileNames();
73
+ if (names.length === 0) {
74
+ p.log.warn("No profiles found.");
75
+ return;
76
+ }
77
+ if (!name) {
78
+ name = (await p.select({
79
+ message: "Select profile to remove:",
80
+ options: names.map((n) => ({ value: n, label: n })),
81
+ }));
82
+ if (p.isCancel(name))
83
+ return;
84
+ }
34
85
  const file = profilePath(name);
35
86
  if (!fs.existsSync(file)) {
36
- console.error(`Profile "${name}" not found.`);
37
- process.exit(1);
87
+ p.log.error(`Profile "${name}" not found.`);
88
+ return;
38
89
  }
39
90
  fs.unlinkSync(file);
40
- console.log(`Removed profile "${name}".`);
91
+ p.log.success(`Removed profile "${name}".`);
41
92
  }
42
- function listProfiles() {
93
+ async function listProfiles() {
43
94
  ensureProfilesDir();
44
- const files = fs.readdirSync(PROFILES_DIR).filter((f) => f.endsWith(".json"));
95
+ const files = fs
96
+ .readdirSync(PROFILES_DIR)
97
+ .filter((f) => f.endsWith(".json"));
45
98
  if (files.length === 0) {
46
- console.log("No profiles found.");
99
+ p.log.warn("No profiles found.");
47
100
  return;
48
101
  }
49
102
  for (const f of files) {
50
103
  const data = JSON.parse(fs.readFileSync(path.join(PROFILES_DIR, f), "utf-8"));
51
- console.log(` ${data.name} (${data.plugins.length} plugins)`);
104
+ p.log.success(`${data.name} (${data.plugins.length} plugins)`);
52
105
  }
53
106
  }
54
- function addPlugin(profileName, plugin) {
107
+ async function addPlugin(profileName, plugin) {
55
108
  const data = readProfile(profileName);
109
+ if (!plugin) {
110
+ const allPlugins = getAllPlugins();
111
+ const available = allPlugins.filter((pl) => !data.plugins.includes(pl.name));
112
+ if (available.length === 0) {
113
+ p.log.warn("All available plugins already added.");
114
+ return;
115
+ }
116
+ const selected = await p.select({
117
+ message: `Add plugin to "${profileName}":`,
118
+ options: [
119
+ ...available.map((pl) => ({
120
+ value: pl.name,
121
+ label: pl.name,
122
+ hint: pl.description.slice(0, 60),
123
+ })),
124
+ { value: "__url__", label: "Enter URL manually...", hint: "Input a GitHub URL or plugin name" },
125
+ ],
126
+ });
127
+ if (p.isCancel(selected))
128
+ return;
129
+ if (selected === "__url__") {
130
+ plugin = (await p.text({
131
+ message: "Plugin URL or name:",
132
+ }));
133
+ if (p.isCancel(plugin))
134
+ return;
135
+ }
136
+ else {
137
+ plugin = selected;
138
+ }
139
+ }
56
140
  if (data.plugins.includes(plugin)) {
57
- console.error(`Plugin "${plugin}" already in profile "${profileName}".`);
58
- process.exit(1);
141
+ p.log.warn(`Plugin "${plugin}" already in profile.`);
142
+ return;
59
143
  }
60
144
  data.plugins.push(plugin);
61
145
  writeProfile(profileName, data);
62
- console.log(`Added "${plugin}" to profile "${profileName}".`);
146
+ p.log.success(`Added "${plugin}" to profile "${profileName}".`);
63
147
  }
64
- function removePlugin(profileName, plugin) {
148
+ async function removePlugin(profileName, plugin) {
65
149
  const data = readProfile(profileName);
150
+ if (data.plugins.length === 0) {
151
+ p.log.warn("No plugins in this profile.");
152
+ return;
153
+ }
154
+ if (!plugin) {
155
+ plugin = (await p.select({
156
+ message: `Remove plugin from "${profileName}":`,
157
+ options: data.plugins.map((pl) => ({
158
+ value: pl,
159
+ label: pl,
160
+ })),
161
+ }));
162
+ if (p.isCancel(plugin))
163
+ return;
164
+ }
66
165
  const idx = data.plugins.indexOf(plugin);
67
166
  if (idx === -1) {
68
- console.error(`Plugin "${plugin}" not found in profile "${profileName}".`);
69
- process.exit(1);
167
+ p.log.error(`Plugin "${plugin}" not found.`);
168
+ return;
70
169
  }
71
170
  data.plugins.splice(idx, 1);
72
171
  writeProfile(profileName, data);
73
- console.log(`Removed "${plugin}" from profile "${profileName}".`);
172
+ p.log.success(`Removed "${plugin}" from profile "${profileName}".`);
74
173
  }
75
- function listPlugins(profileName) {
174
+ async function listPlugins(profileName) {
76
175
  const data = readProfile(profileName);
77
176
  if (data.plugins.length === 0) {
78
- console.log(`No plugins in profile "${profileName}".`);
177
+ p.log.warn(`No plugins in profile "${profileName}".`);
178
+ return;
179
+ }
180
+ for (const pl of data.plugins) {
181
+ p.log.success(pl);
182
+ }
183
+ }
184
+ async function searchPlugins(keyword) {
185
+ if (!keyword) {
186
+ keyword = (await p.text({
187
+ message: "Search plugins:",
188
+ }));
189
+ if (p.isCancel(keyword))
190
+ return;
191
+ }
192
+ const allPlugins = getAllPlugins();
193
+ const lower = keyword.toLowerCase();
194
+ const results = allPlugins.filter((pl) => pl.name.toLowerCase().includes(lower) ||
195
+ pl.description.toLowerCase().includes(lower) ||
196
+ (pl.category || "").toLowerCase().includes(lower));
197
+ if (results.length === 0) {
198
+ p.log.warn(`No plugins matching "${keyword}".`);
79
199
  return;
80
200
  }
81
- for (const p of data.plugins) {
82
- console.log(` ${p}`);
201
+ const grouped = new Map();
202
+ for (const pl of results) {
203
+ const list = grouped.get(pl.marketplace) || [];
204
+ list.push(pl);
205
+ grouped.set(pl.marketplace, list);
206
+ }
207
+ console.log(`\n Found ${results.length} plugin(s) for "${keyword}":\n`);
208
+ for (const [marketplace, plugins] of grouped) {
209
+ console.log(` [${marketplace}]`);
210
+ for (const pl of plugins) {
211
+ const desc = pl.description.length > 70
212
+ ? pl.description.slice(0, 67) + "..."
213
+ : pl.description;
214
+ console.log(` ${pl.name.padEnd(24)} ${desc}`);
215
+ }
216
+ console.log();
83
217
  }
84
218
  }
85
- function executeProfile(profileName) {
219
+ async function executeProfile(profileName) {
86
220
  const data = readProfile(profileName);
87
221
  if (data.plugins.length === 0) {
88
- console.log(`No plugins to install in profile "${profileName}".`);
222
+ p.log.warn(`No plugins to install in profile "${profileName}".`);
89
223
  return;
90
224
  }
91
- console.log(`Installing ${data.plugins.length} plugins from profile "${profileName}"...`);
225
+ const s = p.spinner();
226
+ s.start(`Installing ${data.plugins.length} plugin(s) from "${profileName}"...`);
227
+ let installed = 0;
228
+ let failed = 0;
92
229
  for (const plugin of data.plugins) {
93
- console.log(` Installing ${plugin}...`);
230
+ s.message(`Installing ${plugin}...`);
94
231
  try {
95
232
  execSync(`claude plugin install ${plugin} --scope project`, {
96
- stdio: "inherit",
233
+ stdio: "pipe",
97
234
  });
235
+ installed++;
98
236
  }
99
237
  catch {
100
- console.error(` Failed to install ${plugin}.`);
238
+ failed++;
239
+ p.log.error(`Failed to install ${plugin}`);
101
240
  }
102
241
  }
103
- console.log("Done.");
242
+ s.stop(`${installed} installed${failed > 0 ? `, ${failed} failed` : ""}`);
243
+ p.log.success("Done.");
104
244
  }
105
- function searchPlugins(keyword) {
106
- const marketplacesDir = path.join(process.env.HOME, ".claude", "plugins", "marketplaces");
107
- if (!fs.existsSync(marketplacesDir)) {
108
- console.log("No marketplaces found.");
245
+ async function interactiveMode() {
246
+ p.intro("ccx Agent Profile Manager");
247
+ const action = await p.select({
248
+ message: "What do you want to do?",
249
+ options: [
250
+ { value: "install", label: "Install profile plugins", hint: "Run a profile to install its plugins" },
251
+ { value: "plugin-add", label: "Add plugin to profile", hint: "Add a plugin to an existing profile" },
252
+ { value: "plugin-remove", label: "Remove plugin from profile", hint: "Remove a plugin from a profile" },
253
+ { value: "plugin-list", label: "List profile plugins", hint: "Show plugins in a profile" },
254
+ { value: "add", label: "Create new profile", hint: "Create a new empty profile" },
255
+ { value: "remove", label: "Delete profile", hint: "Remove a profile" },
256
+ { value: "list", label: "List all profiles", hint: "Show all profiles" },
257
+ { value: "search", label: "Search plugins", hint: "Search plugins in marketplaces" },
258
+ ],
259
+ });
260
+ if (p.isCancel(action))
109
261
  return;
110
- }
111
- const lower = keyword.toLowerCase();
112
- const dirs = fs.readdirSync(marketplacesDir);
113
- let found = 0;
114
- for (const dir of dirs) {
115
- const file = path.join(marketplacesDir, dir, ".claude-plugin", "marketplace.json");
116
- if (!fs.existsSync(file))
117
- continue;
118
- const data = JSON.parse(fs.readFileSync(file, "utf-8"));
119
- for (const p of data.plugins || []) {
120
- if (p.name?.toLowerCase().includes(lower) ||
121
- p.description?.toLowerCase().includes(lower) ||
122
- p.category?.toLowerCase().includes(lower)) {
123
- found++;
124
- console.log(` ${p.name} (${data.name}) ${p.description?.slice(0, 80) || ""}`);
262
+ switch (action) {
263
+ case "install": {
264
+ const names = getProfileNames();
265
+ if (names.length === 0) {
266
+ p.log.warn("No profiles found.");
267
+ return;
268
+ }
269
+ const name = await p.select({
270
+ message: "Select profile to install:",
271
+ options: names.map((n) => ({ value: n, label: n })),
272
+ });
273
+ if (p.isCancel(name))
274
+ return;
275
+ await executeProfile(name);
276
+ break;
277
+ }
278
+ case "plugin-add": {
279
+ const names = getProfileNames();
280
+ if (names.length === 0) {
281
+ p.log.warn("No profiles found. Create one first.");
282
+ return;
125
283
  }
284
+ const name = await p.select({
285
+ message: "Select profile:",
286
+ options: names.map((n) => ({ value: n, label: n })),
287
+ });
288
+ if (p.isCancel(name))
289
+ return;
290
+ await addPlugin(name);
291
+ break;
126
292
  }
293
+ case "plugin-remove": {
294
+ const names = getProfileNames();
295
+ if (names.length === 0) {
296
+ p.log.warn("No profiles found.");
297
+ return;
298
+ }
299
+ const name = await p.select({
300
+ message: "Select profile:",
301
+ options: names.map((n) => ({ value: n, label: n })),
302
+ });
303
+ if (p.isCancel(name))
304
+ return;
305
+ await removePlugin(name);
306
+ break;
307
+ }
308
+ case "plugin-list": {
309
+ const names = getProfileNames();
310
+ if (names.length === 0) {
311
+ p.log.warn("No profiles found.");
312
+ return;
313
+ }
314
+ const name = await p.select({
315
+ message: "Select profile:",
316
+ options: names.map((n) => ({ value: n, label: n })),
317
+ });
318
+ if (p.isCancel(name))
319
+ return;
320
+ await listPlugins(name);
321
+ break;
322
+ }
323
+ case "add":
324
+ await addProfile();
325
+ break;
326
+ case "remove":
327
+ await removeProfile();
328
+ break;
329
+ case "list":
330
+ await listProfiles();
331
+ break;
332
+ case "search":
333
+ await searchPlugins();
334
+ break;
127
335
  }
128
- if (found === 0) {
129
- console.log(`No plugins matching "${keyword}".`);
130
- }
336
+ p.outro("Done.");
131
337
  }
132
338
  function printHelp() {
133
- console.log(`Usage:
134
- ccx add <profile> Create a new profile
135
- ccx remove <profile> Remove a profile
136
- ccx list List all profiles
137
- ccx search <keyword> Search plugins in all marketplaces
138
- ccx <profile> Install all plugins from profile
139
- ccx <profile> add <plugin|url> Add plugin to profile
140
- ccx <profile> remove <plugin|url> Remove plugin from profile
141
- ccx <profile> list List plugins in profile`);
339
+ console.log(`ccx — Agent Profile Manager for Claude Code
340
+
341
+ Usage:
342
+ ccx Interactive mode
343
+ ccx <profile> Install all plugins from profile
344
+ ccx add <name> Create a new profile
345
+ ccx remove <name> Remove a profile
346
+ ccx list List all profiles
347
+ ccx search <keyword> Search plugins in marketplaces
348
+ ccx <profile> add [plugin] Add plugin to profile
349
+ ccx <profile> remove [plugin] Remove plugin from profile
350
+ ccx <profile> list List plugins in profile`);
142
351
  }
352
+ // ── Main ──────────────────────────────────────────────────
143
353
  const args = process.argv.slice(2);
144
- if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
354
+ if (args.length === 0) {
355
+ interactiveMode();
356
+ process.exit(0);
357
+ }
358
+ if (args[0] === "--help" || args[0] === "-h") {
145
359
  printHelp();
146
360
  process.exit(0);
147
361
  }
148
362
  const cmd = args[0];
149
363
  switch (cmd) {
150
364
  case "add":
151
- if (!args[1]) {
152
- console.error("Usage: ccx add <profile>");
153
- process.exit(1);
154
- }
155
365
  addProfile(args[1]);
156
366
  break;
157
367
  case "remove":
158
- if (!args[1]) {
159
- console.error("Usage: ccx remove <profile>");
160
- process.exit(1);
161
- }
162
368
  removeProfile(args[1]);
163
369
  break;
164
370
  case "list":
165
371
  listProfiles();
166
372
  break;
167
373
  case "search":
168
- if (!args[1]) {
169
- console.error("Usage: ccx search <keyword>");
170
- process.exit(1);
171
- }
172
374
  searchPlugins(args[1]);
173
375
  break;
174
376
  default: {
175
- // cmd is a profile name
176
377
  const profileName = cmd;
177
378
  const sub = args[1];
178
379
  if (!sub) {
179
380
  executeProfile(profileName);
180
381
  }
181
382
  else if (sub === "add") {
182
- if (!args[2]) {
183
- console.error("Usage: ccx <profile> add <plugin|url>");
184
- process.exit(1);
185
- }
186
383
  addPlugin(profileName, args[2]);
187
384
  }
188
385
  else if (sub === "remove") {
189
- if (!args[2]) {
190
- console.error("Usage: ccx <profile> remove <plugin|url>");
191
- process.exit(1);
192
- }
193
386
  removePlugin(profileName, args[2]);
194
387
  }
195
388
  else if (sub === "list") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanmu/ccprofile",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Agent Profile Manager for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,6 +18,11 @@
18
18
  "dev": "bun run src/index.ts",
19
19
  "prepublishOnly": "npm run build"
20
20
  },
21
+ "dependencies": {
22
+ "@clack/prompts": "^1.4.0",
23
+ "ora": "^8.0.0",
24
+ "picocolors": "^1.1.0"
25
+ },
21
26
  "devDependencies": {
22
27
  "@types/node": "^22.0.0",
23
28
  "typescript": "^5.7.0"