@guanmu/ccprofile 0.1.4 → 0.1.5

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