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