@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.
- package/dist/index.js +278 -85
- 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
|
-
|
|
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
|
|
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
|
-
|
|
28
|
-
|
|
65
|
+
p.log.error(`Profile "${name}" already exists.`);
|
|
66
|
+
return;
|
|
29
67
|
}
|
|
30
68
|
writeProfile(name, { name, plugins: [] });
|
|
31
|
-
|
|
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
|
-
|
|
37
|
-
|
|
87
|
+
p.log.error(`Profile "${name}" not found.`);
|
|
88
|
+
return;
|
|
38
89
|
}
|
|
39
90
|
fs.unlinkSync(file);
|
|
40
|
-
|
|
91
|
+
p.log.success(`Removed profile "${name}".`);
|
|
41
92
|
}
|
|
42
|
-
function listProfiles() {
|
|
93
|
+
async function listProfiles() {
|
|
43
94
|
ensureProfilesDir();
|
|
44
|
-
const files = fs
|
|
95
|
+
const files = fs
|
|
96
|
+
.readdirSync(PROFILES_DIR)
|
|
97
|
+
.filter((f) => f.endsWith(".json"));
|
|
45
98
|
if (files.length === 0) {
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
222
|
+
p.log.warn(`No plugins to install in profile "${profileName}".`);
|
|
89
223
|
return;
|
|
90
224
|
}
|
|
91
|
-
|
|
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
|
-
|
|
230
|
+
s.message(`Installing ${plugin}...`);
|
|
94
231
|
try {
|
|
95
232
|
execSync(`claude plugin install ${plugin} --scope project`, {
|
|
96
|
-
stdio: "
|
|
233
|
+
stdio: "pipe",
|
|
97
234
|
});
|
|
235
|
+
installed++;
|
|
98
236
|
}
|
|
99
237
|
catch {
|
|
100
|
-
|
|
238
|
+
failed++;
|
|
239
|
+
p.log.error(`Failed to install ${plugin}`);
|
|
101
240
|
}
|
|
102
241
|
}
|
|
103
|
-
|
|
242
|
+
s.stop(`${installed} installed${failed > 0 ? `, ${failed} failed` : ""}`);
|
|
243
|
+
p.log.success("Done.");
|
|
104
244
|
}
|
|
105
|
-
function
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
129
|
-
console.log(`No plugins matching "${keyword}".`);
|
|
130
|
-
}
|
|
336
|
+
p.outro("Done.");
|
|
131
337
|
}
|
|
132
338
|
function printHelp() {
|
|
133
|
-
console.log(`
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
ccx
|
|
137
|
-
ccx
|
|
138
|
-
ccx <
|
|
139
|
-
ccx
|
|
140
|
-
ccx
|
|
141
|
-
ccx <
|
|
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
|
|
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.
|
|
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"
|