@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.
- package/dist/index.js +244 -83
- 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
|
|
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
|
|
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}
|
|
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
|
|
58
|
-
|
|
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
|
|
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}
|
|
210
|
+
console.log(pc.bold(`Installing ${data.plugins.length} plugin(s) from profile "${profileName}"...\n`));
|
|
92
211
|
for (const plugin of data.plugins) {
|
|
93
|
-
|
|
212
|
+
const spinner = ora(`Installing ${pc.cyan(plugin)}...`).start();
|
|
94
213
|
try {
|
|
95
214
|
execSync(`claude plugin install ${plugin} --scope project`, {
|
|
96
|
-
stdio: "
|
|
215
|
+
stdio: "pipe",
|
|
97
216
|
});
|
|
217
|
+
spinner.succeed(`Installed ${pc.cyan(plugin)}`);
|
|
98
218
|
}
|
|
99
219
|
catch {
|
|
100
|
-
|
|
220
|
+
spinner.fail(`Failed to install ${pc.red(plugin)}`);
|
|
101
221
|
}
|
|
102
222
|
}
|
|
103
|
-
console.log("
|
|
223
|
+
console.log(pc.green("\nDone."));
|
|
104
224
|
}
|
|
105
|
-
function
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
found
|
|
124
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
ccx
|
|
137
|
-
ccx
|
|
138
|
-
ccx <
|
|
139
|
-
ccx
|
|
140
|
-
ccx
|
|
141
|
-
ccx <
|
|
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
|
|
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.
|
|
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"
|