cli-profile-manager 0.0.1
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/LICENSE +21 -0
- package/README.md +176 -0
- package/docs/devcontainer-setup.md +85 -0
- package/index.json +51 -0
- package/package.json +38 -0
- package/scripts/install-profile.mjs +87 -0
- package/src/cli.js +165 -0
- package/src/commands/local.js +307 -0
- package/src/commands/marketplace.js +403 -0
- package/src/commands/publish.js +252 -0
- package/src/utils/auth.js +403 -0
- package/src/utils/config.js +102 -0
- package/src/utils/snapshot.js +421 -0
- package/test/cli.test.js +22 -0
- package/test/config.test.js +15 -0
- package/test/references.test.js +39 -0
- package/test/snapshot.test.js +61 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import { existsSync, rmSync, readdirSync, statSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import {
|
|
7
|
+
createSnapshot,
|
|
8
|
+
extractSnapshot,
|
|
9
|
+
readProfileMetadata,
|
|
10
|
+
listLocalProfileNames,
|
|
11
|
+
deriveContents
|
|
12
|
+
} from '../utils/snapshot.js';
|
|
13
|
+
import { getConfig, claudeDirExists, getProfilePath } from '../utils/config.js';
|
|
14
|
+
|
|
15
|
+
// Display labels for content categories
|
|
16
|
+
const CATEGORY_LABELS = {
|
|
17
|
+
commands: 'Commands',
|
|
18
|
+
skills: 'Skills',
|
|
19
|
+
mcp: 'MCP Servers',
|
|
20
|
+
mcp_servers: 'MCP Servers',
|
|
21
|
+
agents: 'Agents',
|
|
22
|
+
plugins: 'Plugins',
|
|
23
|
+
hooks: 'Hooks',
|
|
24
|
+
instructions: 'Instructions'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get contents from metadata, falling back to deriving from files list
|
|
29
|
+
*/
|
|
30
|
+
function getContents(metadata) {
|
|
31
|
+
if (metadata?.contents) return metadata.contents;
|
|
32
|
+
if (metadata?.files) return deriveContents(metadata.files);
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Format a contents object into display lines
|
|
38
|
+
*/
|
|
39
|
+
function formatContentsLines(contents, indent = ' ') {
|
|
40
|
+
if (!contents || Object.keys(contents).length === 0) return [];
|
|
41
|
+
|
|
42
|
+
const lines = [];
|
|
43
|
+
for (const [category, items] of Object.entries(contents)) {
|
|
44
|
+
if (!items || items.length === 0) continue;
|
|
45
|
+
const label = CATEGORY_LABELS[category] || category;
|
|
46
|
+
const display = category === 'commands'
|
|
47
|
+
? items.map(i => `/${i}`).join(', ')
|
|
48
|
+
: items.join(', ');
|
|
49
|
+
lines.push(`${indent}${chalk.white(label + ':')} ${chalk.dim(display)}`);
|
|
50
|
+
}
|
|
51
|
+
return lines;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Save current .claude folder as a profile
|
|
56
|
+
*/
|
|
57
|
+
export async function saveProfile(name, options) {
|
|
58
|
+
if (!/^[a-z0-9][a-z0-9-_]*[a-z0-9]$|^[a-z0-9]$/i.test(name)) {
|
|
59
|
+
console.log(chalk.red('Invalid profile name. Use alphanumeric characters, hyphens, and underscores.'));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!claudeDirExists()) {
|
|
64
|
+
console.log(chalk.red('Claude directory (.claude) not found.'));
|
|
65
|
+
console.log(chalk.dim(' Make sure Claude CLI is installed and configured.'));
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const spinner = ora('Creating profile snapshot...').start();
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const result = await createSnapshot(name, options);
|
|
73
|
+
spinner.succeed(chalk.green(`Profile saved: ${chalk.bold(name)}`));
|
|
74
|
+
|
|
75
|
+
console.log('');
|
|
76
|
+
console.log(chalk.dim(' Location: ') + result.profileDir);
|
|
77
|
+
|
|
78
|
+
if (options.description) {
|
|
79
|
+
console.log(chalk.dim(' Desc: ') + options.description);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const contents = getContents(result.metadata);
|
|
83
|
+
const contentsLines = formatContentsLines(contents, ' ');
|
|
84
|
+
if (contentsLines.length > 0) {
|
|
85
|
+
for (const line of contentsLines) {
|
|
86
|
+
console.log(line);
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
console.log(chalk.dim(' Files: ') + result.metadata.files.length);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log('');
|
|
93
|
+
console.log(chalk.dim('Load this profile anytime with:'));
|
|
94
|
+
console.log(chalk.cyan(` cpm load ${name}`));
|
|
95
|
+
|
|
96
|
+
} catch (error) {
|
|
97
|
+
spinner.fail(chalk.red(`Failed to save profile: ${error.message}`));
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Load a local profile
|
|
104
|
+
*/
|
|
105
|
+
export async function loadProfile(name, options) {
|
|
106
|
+
const profilePath = getProfilePath(name);
|
|
107
|
+
|
|
108
|
+
if (!existsSync(profilePath)) {
|
|
109
|
+
console.log(chalk.red(`Profile not found: ${name}`));
|
|
110
|
+
console.log(chalk.dim(' List local profiles with: cpm local'));
|
|
111
|
+
console.log(chalk.dim(' Browse marketplace with: cpm list'));
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const metadata = readProfileMetadata(name);
|
|
116
|
+
|
|
117
|
+
console.log('');
|
|
118
|
+
console.log(chalk.bold('Profile: ') + chalk.cyan(name));
|
|
119
|
+
if (metadata?.description) {
|
|
120
|
+
console.log(chalk.dim(metadata.description));
|
|
121
|
+
}
|
|
122
|
+
console.log('');
|
|
123
|
+
|
|
124
|
+
if (claudeDirExists() && !options.force) {
|
|
125
|
+
const { confirm } = await inquirer.prompt([{
|
|
126
|
+
type: 'confirm',
|
|
127
|
+
name: 'confirm',
|
|
128
|
+
message: 'This will replace your current .claude configuration. Continue?',
|
|
129
|
+
default: false
|
|
130
|
+
}]);
|
|
131
|
+
|
|
132
|
+
if (!confirm) {
|
|
133
|
+
console.log(chalk.yellow('Aborted.'));
|
|
134
|
+
process.exit(0);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!options.backup) {
|
|
138
|
+
const { backup } = await inquirer.prompt([{
|
|
139
|
+
type: 'confirm',
|
|
140
|
+
name: 'backup',
|
|
141
|
+
message: 'Backup current .claude folder first?',
|
|
142
|
+
default: true
|
|
143
|
+
}]);
|
|
144
|
+
options.backup = backup;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
options.force = true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const spinner = ora('Loading profile...').start();
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const result = await extractSnapshot(name, options);
|
|
154
|
+
spinner.succeed(chalk.green(`Profile loaded: ${chalk.bold(name)}`));
|
|
155
|
+
|
|
156
|
+
if (options.backup) {
|
|
157
|
+
console.log(chalk.dim(' Previous config backed up'));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log('');
|
|
161
|
+
console.log(chalk.green('Your Claude CLI is now configured with this profile.'));
|
|
162
|
+
|
|
163
|
+
} catch (error) {
|
|
164
|
+
spinner.fail(chalk.red(`Failed to load profile: ${error.message}`));
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* List all locally saved profiles
|
|
171
|
+
*/
|
|
172
|
+
export async function listLocalProfiles() {
|
|
173
|
+
const profiles = listLocalProfileNames();
|
|
174
|
+
|
|
175
|
+
console.log('');
|
|
176
|
+
console.log(chalk.bold('Local Profiles'));
|
|
177
|
+
console.log(chalk.dim('-'.repeat(50)));
|
|
178
|
+
|
|
179
|
+
if (profiles.length === 0) {
|
|
180
|
+
console.log('');
|
|
181
|
+
console.log(chalk.dim(' No local profiles found.'));
|
|
182
|
+
console.log('');
|
|
183
|
+
console.log(chalk.dim(' Save your current config: ') + chalk.cyan('cpm save <n>'));
|
|
184
|
+
console.log(chalk.dim(' Browse marketplace: ') + chalk.cyan('cpm list'));
|
|
185
|
+
console.log('');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
console.log('');
|
|
190
|
+
|
|
191
|
+
for (const name of profiles) {
|
|
192
|
+
const metadata = readProfileMetadata(name);
|
|
193
|
+
|
|
194
|
+
console.log(chalk.cyan(' ' + name));
|
|
195
|
+
|
|
196
|
+
if (metadata) {
|
|
197
|
+
if (metadata.description) {
|
|
198
|
+
console.log(chalk.dim(` ${metadata.description}`));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const contents = getContents(metadata);
|
|
202
|
+
const contentsLines = formatContentsLines(contents);
|
|
203
|
+
for (const line of contentsLines) {
|
|
204
|
+
console.log(line);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const info = [];
|
|
208
|
+
if (metadata.tags?.length) {
|
|
209
|
+
info.push(chalk.yellow(metadata.tags.join(', ')));
|
|
210
|
+
}
|
|
211
|
+
if (metadata.createdAt) {
|
|
212
|
+
const date = new Date(metadata.createdAt).toLocaleDateString();
|
|
213
|
+
info.push(chalk.dim(date));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (info.length) {
|
|
217
|
+
console.log(` ${info.join(' | ')}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
console.log('');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
console.log(chalk.dim('Commands:'));
|
|
225
|
+
console.log(chalk.dim(' Load: ') + chalk.cyan('cpm load <n>'));
|
|
226
|
+
console.log(chalk.dim(' Info: ') + chalk.cyan('cpm info <n>'));
|
|
227
|
+
console.log(chalk.dim(' Delete: ') + chalk.cyan('cpm delete <n>'));
|
|
228
|
+
console.log('');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Delete a local profile
|
|
233
|
+
*/
|
|
234
|
+
export async function deleteLocalProfile(name, options) {
|
|
235
|
+
const profilePath = getProfilePath(name);
|
|
236
|
+
|
|
237
|
+
if (!existsSync(profilePath)) {
|
|
238
|
+
console.log(chalk.red(`Profile not found: ${name}`));
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!options.force) {
|
|
243
|
+
const { confirm } = await inquirer.prompt([{
|
|
244
|
+
type: 'confirm',
|
|
245
|
+
name: 'confirm',
|
|
246
|
+
message: `Delete profile "${name}"? This cannot be undone.`,
|
|
247
|
+
default: false
|
|
248
|
+
}]);
|
|
249
|
+
|
|
250
|
+
if (!confirm) {
|
|
251
|
+
console.log(chalk.yellow('Aborted.'));
|
|
252
|
+
process.exit(0);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
rmSync(profilePath, { recursive: true, force: true });
|
|
257
|
+
console.log(chalk.green(`Deleted profile: ${name}`));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Show detailed profile info
|
|
262
|
+
*/
|
|
263
|
+
export async function showProfileInfo(name) {
|
|
264
|
+
const profilePath = getProfilePath(name);
|
|
265
|
+
|
|
266
|
+
if (!existsSync(profilePath)) {
|
|
267
|
+
console.log(chalk.red(`Profile not found: ${name}`));
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const metadata = readProfileMetadata(name);
|
|
272
|
+
|
|
273
|
+
console.log('');
|
|
274
|
+
console.log(chalk.bold('Profile Information'));
|
|
275
|
+
console.log(chalk.dim('-'.repeat(50)));
|
|
276
|
+
console.log('');
|
|
277
|
+
|
|
278
|
+
console.log(chalk.cyan('Name: ') + name);
|
|
279
|
+
console.log(chalk.cyan('Version: ') + (metadata?.version || '1.0.0'));
|
|
280
|
+
console.log(chalk.cyan('Description: ') + (metadata?.description || chalk.dim('No description')));
|
|
281
|
+
console.log(chalk.cyan('Tags: ') + (metadata?.tags?.join(', ') || chalk.dim('None')));
|
|
282
|
+
console.log(chalk.cyan('Created: ') + (metadata?.createdAt ? new Date(metadata.createdAt).toLocaleString() : chalk.dim('Unknown')));
|
|
283
|
+
console.log(chalk.cyan('Platform: ') + (metadata?.platform || chalk.dim('Unknown')));
|
|
284
|
+
console.log(chalk.cyan('Claude Ver: ') + (metadata?.claudeVersion || chalk.dim('Unknown')));
|
|
285
|
+
|
|
286
|
+
const contents = getContents(metadata);
|
|
287
|
+
if (Object.keys(contents).length > 0) {
|
|
288
|
+
console.log('');
|
|
289
|
+
console.log(chalk.bold('Contents:'));
|
|
290
|
+
const contentsLines = formatContentsLines(contents, ' ');
|
|
291
|
+
for (const line of contentsLines) {
|
|
292
|
+
console.log(line);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (metadata?.files?.length) {
|
|
297
|
+
console.log('');
|
|
298
|
+
console.log(chalk.bold('Files:'));
|
|
299
|
+
for (const file of metadata.files) {
|
|
300
|
+
console.log(chalk.dim(' - ') + file);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
console.log('');
|
|
305
|
+
console.log(chalk.dim('Location: ') + profilePath);
|
|
306
|
+
console.log('');
|
|
307
|
+
}
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import fetch from 'node-fetch';
|
|
5
|
+
import { existsSync, writeFileSync, readFileSync, mkdirSync, cpSync } from 'fs';
|
|
6
|
+
import { join, dirname } from 'path';
|
|
7
|
+
import { getConfig, claudeDirExists, DEFAULTS } from '../utils/config.js';
|
|
8
|
+
import { cleanProfileContent } from '../utils/snapshot.js';
|
|
9
|
+
|
|
10
|
+
const INDEX_CACHE_TIME = 60 * 60 * 1000; // 1 hour
|
|
11
|
+
|
|
12
|
+
// Display labels for content categories
|
|
13
|
+
const CATEGORY_LABELS = {
|
|
14
|
+
commands: 'Commands',
|
|
15
|
+
skills: 'Skills',
|
|
16
|
+
mcp: 'MCP Servers',
|
|
17
|
+
mcp_servers: 'MCP Servers',
|
|
18
|
+
agents: 'Agents',
|
|
19
|
+
plugins: 'Plugins',
|
|
20
|
+
hooks: 'Hooks',
|
|
21
|
+
instructions: 'Instructions'
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Format a contents object into display lines
|
|
26
|
+
*/
|
|
27
|
+
function formatContentsLines(contents, indent = ' ') {
|
|
28
|
+
if (!contents || Object.keys(contents).length === 0) return [];
|
|
29
|
+
|
|
30
|
+
const lines = [];
|
|
31
|
+
for (const [category, items] of Object.entries(contents)) {
|
|
32
|
+
if (!items || items.length === 0) continue;
|
|
33
|
+
const label = CATEGORY_LABELS[category] || category;
|
|
34
|
+
const display = category === 'commands'
|
|
35
|
+
? items.map(i => `/${i}`).join(', ')
|
|
36
|
+
: items.join(', ');
|
|
37
|
+
lines.push(`${indent}${chalk.white(label + ':')} ${chalk.dim(display)}`);
|
|
38
|
+
}
|
|
39
|
+
return lines;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get the marketplace index (list of all profiles)
|
|
44
|
+
*/
|
|
45
|
+
async function fetchMarketplaceIndex(forceRefresh = false) {
|
|
46
|
+
const config = await getConfig();
|
|
47
|
+
const cacheFile = join(config.cacheDir, 'marketplace-index.json');
|
|
48
|
+
|
|
49
|
+
if (!forceRefresh && existsSync(cacheFile)) {
|
|
50
|
+
try {
|
|
51
|
+
const cached = JSON.parse(readFileSync(cacheFile, 'utf-8'));
|
|
52
|
+
const age = Date.now() - (cached._cachedAt || 0);
|
|
53
|
+
|
|
54
|
+
if (age < INDEX_CACHE_TIME) {
|
|
55
|
+
return cached;
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// Ignore cache errors
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const indexUrl = `https://raw.githubusercontent.com/${config.marketplaceRepo}/main/index.json`;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const response = await fetch(indexUrl);
|
|
66
|
+
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
throw new Error(`Failed to fetch marketplace index: ${response.status}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const index = await response.json();
|
|
72
|
+
index._cachedAt = Date.now();
|
|
73
|
+
|
|
74
|
+
mkdirSync(config.cacheDir, { recursive: true });
|
|
75
|
+
writeFileSync(cacheFile, JSON.stringify(index, null, 2));
|
|
76
|
+
|
|
77
|
+
return index;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (existsSync(cacheFile)) {
|
|
80
|
+
console.log(chalk.yellow('Could not refresh marketplace. Using cached data.'));
|
|
81
|
+
return JSON.parse(readFileSync(cacheFile, 'utf-8'));
|
|
82
|
+
}
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* List profiles in the marketplace
|
|
89
|
+
*/
|
|
90
|
+
export async function listMarketplace(options) {
|
|
91
|
+
const spinner = ora('Fetching marketplace...').start();
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const index = await fetchMarketplaceIndex(options.refresh);
|
|
95
|
+
spinner.stop();
|
|
96
|
+
|
|
97
|
+
console.log('');
|
|
98
|
+
console.log(chalk.bold('Profile Marketplace'));
|
|
99
|
+
console.log(chalk.dim('-'.repeat(50)));
|
|
100
|
+
console.log('');
|
|
101
|
+
|
|
102
|
+
if (!index.profiles || index.profiles.length === 0) {
|
|
103
|
+
console.log(chalk.dim(' No profiles available yet.'));
|
|
104
|
+
console.log('');
|
|
105
|
+
console.log(chalk.dim(' Be the first to publish: ') + chalk.cyan('cpm publish <n>'));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let profiles = index.profiles;
|
|
110
|
+
if (options.category) {
|
|
111
|
+
profiles = profiles.filter(p =>
|
|
112
|
+
(p.tags || []).includes(options.category)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const profile of profiles) {
|
|
117
|
+
const fullName = `${profile.author}/${profile.name}`;
|
|
118
|
+
console.log(` ${chalk.cyan(fullName)} ${chalk.dim('v' + (profile.version || '1.0.0'))}`);
|
|
119
|
+
|
|
120
|
+
if (profile.description) {
|
|
121
|
+
console.log(` ${chalk.dim(profile.description.slice(0, 60))}${profile.description.length > 60 ? '...' : ''}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const contentsLines = formatContentsLines(profile.contents);
|
|
125
|
+
for (const line of contentsLines) {
|
|
126
|
+
console.log(line);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (profile.tags?.length) {
|
|
130
|
+
console.log(` ${chalk.yellow(profile.tags.join(', '))}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const stats = [];
|
|
134
|
+
if (profile.downloads) stats.push(`${profile.downloads} downloads`);
|
|
135
|
+
if (profile.stars) stats.push(`${profile.stars} stars`);
|
|
136
|
+
if (stats.length) {
|
|
137
|
+
console.log(` ${chalk.yellow(stats.join(' | '))}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.log('');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
console.log(chalk.dim('Install a profile:'));
|
|
144
|
+
console.log(chalk.cyan(' cpm install author/profile-name'));
|
|
145
|
+
console.log('');
|
|
146
|
+
|
|
147
|
+
} catch (error) {
|
|
148
|
+
spinner.fail(chalk.red(`Failed to fetch marketplace: ${error.message}`));
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Search the marketplace
|
|
155
|
+
*/
|
|
156
|
+
export async function searchMarketplace(query) {
|
|
157
|
+
const spinner = ora('Searching marketplace...').start();
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const index = await fetchMarketplaceIndex();
|
|
161
|
+
spinner.stop();
|
|
162
|
+
|
|
163
|
+
const queryLower = query.toLowerCase();
|
|
164
|
+
|
|
165
|
+
const results = (index.profiles || []).filter(profile => {
|
|
166
|
+
const searchable = [
|
|
167
|
+
profile.name,
|
|
168
|
+
profile.author,
|
|
169
|
+
profile.description,
|
|
170
|
+
...(profile.tags || [])
|
|
171
|
+
].join(' ').toLowerCase();
|
|
172
|
+
|
|
173
|
+
return searchable.includes(queryLower);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
console.log('');
|
|
177
|
+
console.log(chalk.bold(`Search Results for "${query}"`));
|
|
178
|
+
console.log(chalk.dim('-'.repeat(50)));
|
|
179
|
+
console.log('');
|
|
180
|
+
|
|
181
|
+
if (results.length === 0) {
|
|
182
|
+
console.log(chalk.dim(` No profiles found matching "${query}"`));
|
|
183
|
+
console.log('');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for (const profile of results) {
|
|
188
|
+
const fullName = `${profile.author}/${profile.name}`;
|
|
189
|
+
console.log(` ${chalk.cyan(fullName)} ${chalk.dim('v' + (profile.version || '1.0.0'))}`);
|
|
190
|
+
|
|
191
|
+
if (profile.description) {
|
|
192
|
+
console.log(` ${chalk.dim(profile.description.slice(0, 60))}${profile.description.length > 60 ? '...' : ''}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (profile.tags?.length) {
|
|
196
|
+
console.log(` ${chalk.yellow(profile.tags.join(', '))}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log('');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
console.log(chalk.dim(`Found ${results.length} profile(s)`));
|
|
203
|
+
console.log('');
|
|
204
|
+
|
|
205
|
+
} catch (error) {
|
|
206
|
+
spinner.fail(chalk.red(`Search failed: ${error.message}`));
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Show detailed info about a marketplace profile
|
|
213
|
+
*/
|
|
214
|
+
export async function showMarketplaceInfo(profilePath) {
|
|
215
|
+
const [author, name] = profilePath.includes('/')
|
|
216
|
+
? profilePath.split('/')
|
|
217
|
+
: [null, profilePath];
|
|
218
|
+
|
|
219
|
+
if (!author || !name) {
|
|
220
|
+
console.log(chalk.red('Invalid profile format. Use: author/profile-name'));
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const spinner = ora('Fetching profile info...').start();
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const index = await fetchMarketplaceIndex();
|
|
228
|
+
const profile = (index.profiles || []).find(
|
|
229
|
+
p => p.author === author && p.name === name
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
if (!profile) {
|
|
233
|
+
spinner.fail(chalk.red(`Profile not found: ${profilePath}`));
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const config = await getConfig();
|
|
238
|
+
const metadataUrl = `https://raw.githubusercontent.com/${config.marketplaceRepo}/main/profiles/${author}/${name}/profile.json`;
|
|
239
|
+
|
|
240
|
+
let metadata = profile;
|
|
241
|
+
try {
|
|
242
|
+
const response = await fetch(metadataUrl);
|
|
243
|
+
if (response.ok) {
|
|
244
|
+
metadata = { ...profile, ...await response.json() };
|
|
245
|
+
}
|
|
246
|
+
} catch {
|
|
247
|
+
// Use index data
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
spinner.stop();
|
|
251
|
+
|
|
252
|
+
console.log('');
|
|
253
|
+
console.log(chalk.bold('Profile Information'));
|
|
254
|
+
console.log(chalk.dim('-'.repeat(50)));
|
|
255
|
+
console.log('');
|
|
256
|
+
|
|
257
|
+
console.log(chalk.cyan('Name: ') + `${author}/${name}`);
|
|
258
|
+
console.log(chalk.cyan('Version: ') + (metadata.version || '1.0.0'));
|
|
259
|
+
console.log(chalk.cyan('Author: ') + author);
|
|
260
|
+
console.log(chalk.cyan('Description: ') + (metadata.description || chalk.dim('No description')));
|
|
261
|
+
console.log(chalk.cyan('Tags: ') + (metadata.tags?.join(', ') || chalk.dim('None')));
|
|
262
|
+
|
|
263
|
+
if (metadata.downloads) {
|
|
264
|
+
console.log(chalk.cyan('Downloads: ') + metadata.downloads);
|
|
265
|
+
}
|
|
266
|
+
if (metadata.stars) {
|
|
267
|
+
console.log(chalk.cyan('Stars: ') + metadata.stars);
|
|
268
|
+
}
|
|
269
|
+
if (metadata.createdAt) {
|
|
270
|
+
console.log(chalk.cyan('Created: ') + new Date(metadata.createdAt).toLocaleDateString());
|
|
271
|
+
}
|
|
272
|
+
if (metadata.updatedAt) {
|
|
273
|
+
console.log(chalk.cyan('Updated: ') + new Date(metadata.updatedAt).toLocaleDateString());
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (metadata.contents && Object.keys(metadata.contents).length > 0) {
|
|
277
|
+
console.log('');
|
|
278
|
+
console.log(chalk.bold('Contents:'));
|
|
279
|
+
const contentsLines = formatContentsLines(metadata.contents, ' ');
|
|
280
|
+
for (const line of contentsLines) {
|
|
281
|
+
console.log(line);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
console.log('');
|
|
286
|
+
console.log(chalk.dim('Install with:'));
|
|
287
|
+
console.log(chalk.cyan(` cpm install ${author}/${name}`));
|
|
288
|
+
console.log('');
|
|
289
|
+
|
|
290
|
+
} catch (error) {
|
|
291
|
+
spinner.fail(chalk.red(`Failed to fetch profile: ${error.message}`));
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Install a profile from the marketplace
|
|
298
|
+
*/
|
|
299
|
+
export async function installFromMarketplace(profilePath, options) {
|
|
300
|
+
const [author, name] = profilePath.includes('/')
|
|
301
|
+
? profilePath.split('/')
|
|
302
|
+
: [null, profilePath];
|
|
303
|
+
|
|
304
|
+
if (!author || !name) {
|
|
305
|
+
console.log(chalk.red('Invalid profile format. Use: author/profile-name'));
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
console.log('');
|
|
310
|
+
console.log(chalk.bold(`Installing: ${chalk.cyan(profilePath)}`));
|
|
311
|
+
console.log('');
|
|
312
|
+
|
|
313
|
+
if (claudeDirExists() && !options.force) {
|
|
314
|
+
const { confirm } = await inquirer.prompt([{
|
|
315
|
+
type: 'confirm',
|
|
316
|
+
name: 'confirm',
|
|
317
|
+
message: 'This will replace your current .claude configuration. Continue?',
|
|
318
|
+
default: false
|
|
319
|
+
}]);
|
|
320
|
+
|
|
321
|
+
if (!confirm) {
|
|
322
|
+
console.log(chalk.yellow('Aborted.'));
|
|
323
|
+
process.exit(0);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (!options.backup) {
|
|
327
|
+
const { backup } = await inquirer.prompt([{
|
|
328
|
+
type: 'confirm',
|
|
329
|
+
name: 'backup',
|
|
330
|
+
message: 'Backup current .claude folder first?',
|
|
331
|
+
default: true
|
|
332
|
+
}]);
|
|
333
|
+
options.backup = backup;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
options.force = true;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const spinner = ora('Downloading profile...').start();
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
const config = await getConfig();
|
|
343
|
+
const claudeDir = config.claudeDir;
|
|
344
|
+
const baseUrl = `https://raw.githubusercontent.com/${config.marketplaceRepo}/main/profiles/${author}/${name}`;
|
|
345
|
+
|
|
346
|
+
const metaResponse = await fetch(`${baseUrl}/profile.json`);
|
|
347
|
+
|
|
348
|
+
if (!metaResponse.ok) {
|
|
349
|
+
if (metaResponse.status === 404) {
|
|
350
|
+
throw new Error(`Profile not found: ${profilePath}`);
|
|
351
|
+
}
|
|
352
|
+
throw new Error(`Download failed: ${metaResponse.status}`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const metadata = await metaResponse.json();
|
|
356
|
+
const files = (metadata.files || []).map(f => f.replace(/\\/g, '/'));
|
|
357
|
+
|
|
358
|
+
if (files.length === 0) {
|
|
359
|
+
throw new Error('Profile has no files to install');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (options.backup && existsSync(claudeDir)) {
|
|
363
|
+
const backupName = `.claude-backup-${Date.now()}`;
|
|
364
|
+
const backupPath = join(DEFAULTS.profilesDir, backupName);
|
|
365
|
+
cpSync(claudeDir, backupPath, { recursive: true });
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
369
|
+
|
|
370
|
+
cleanProfileContent(claudeDir);
|
|
371
|
+
|
|
372
|
+
spinner.text = 'Installing profile files...';
|
|
373
|
+
|
|
374
|
+
for (const filePath of files) {
|
|
375
|
+
const fileUrl = `${baseUrl}/${filePath}`;
|
|
376
|
+
const fileResponse = await fetch(fileUrl);
|
|
377
|
+
|
|
378
|
+
if (!fileResponse.ok) {
|
|
379
|
+
throw new Error(`Failed to download ${filePath}: ${fileResponse.status}`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const content = await fileResponse.text();
|
|
383
|
+
const destPath = join(claudeDir, filePath);
|
|
384
|
+
|
|
385
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
386
|
+
writeFileSync(destPath, content);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
spinner.succeed(chalk.green(`Installed: ${chalk.bold(profilePath)}`));
|
|
390
|
+
|
|
391
|
+
if (options.backup) {
|
|
392
|
+
console.log(chalk.dim(' Previous config backed up'));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
console.log('');
|
|
396
|
+
console.log(chalk.green('Your Claude CLI is now configured with this profile.'));
|
|
397
|
+
console.log('');
|
|
398
|
+
|
|
399
|
+
} catch (error) {
|
|
400
|
+
spinner.fail(chalk.red(`Installation failed: ${error.message}`));
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
}
|