claude-profile-manager 1.0.0

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.
@@ -0,0 +1,259 @@
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
+ } from '../utils/snapshot.js';
12
+ import { getConfig, claudeDirExists, getProfilePath } from '../utils/config.js';
13
+
14
+ /**
15
+ * Save current .claude folder as a profile
16
+ */
17
+ export async function saveProfile(name, options) {
18
+ // Validate name
19
+ if (!/^[a-z0-9][a-z0-9-_]*[a-z0-9]$|^[a-z0-9]$/i.test(name)) {
20
+ console.log(chalk.red('✗ Invalid profile name. Use alphanumeric characters, hyphens, and underscores.'));
21
+ process.exit(1);
22
+ }
23
+
24
+ if (!claudeDirExists()) {
25
+ console.log(chalk.red('✗ Claude directory (.claude) not found.'));
26
+ console.log(chalk.dim(' Make sure Claude CLI is installed and configured.'));
27
+ process.exit(1);
28
+ }
29
+
30
+ const spinner = ora('Creating profile snapshot...').start();
31
+
32
+ try {
33
+ const result = await createSnapshot(name, options);
34
+ spinner.succeed(chalk.green(`Profile saved: ${chalk.bold(name)}`));
35
+
36
+ console.log('');
37
+ console.log(chalk.dim(' Location: ') + result.profileDir);
38
+ console.log(chalk.dim(' Files: ') + result.metadata.files.length);
39
+
40
+ if (options.description) {
41
+ console.log(chalk.dim(' Desc: ') + options.description);
42
+ }
43
+
44
+ console.log('');
45
+ console.log(chalk.dim('Load this profile anytime with:'));
46
+ console.log(chalk.cyan(` cpm load ${name}`));
47
+
48
+ } catch (error) {
49
+ spinner.fail(chalk.red(`Failed to save profile: ${error.message}`));
50
+ process.exit(1);
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Load a local profile
56
+ */
57
+ export async function loadProfile(name, options) {
58
+ const profilePath = getProfilePath(name);
59
+
60
+ if (!existsSync(profilePath)) {
61
+ console.log(chalk.red(`✗ Profile not found: ${name}`));
62
+ console.log(chalk.dim(' List local profiles with: cpm local'));
63
+ console.log(chalk.dim(' Browse marketplace with: cpm list'));
64
+ process.exit(1);
65
+ }
66
+
67
+ const metadata = readProfileMetadata(name);
68
+
69
+ // Show profile info
70
+ console.log('');
71
+ console.log(chalk.bold('Profile: ') + chalk.cyan(name));
72
+ if (metadata?.description) {
73
+ console.log(chalk.dim(metadata.description));
74
+ }
75
+ console.log('');
76
+
77
+ // Confirm if .claude exists and no force flag
78
+ if (claudeDirExists() && !options.force) {
79
+ const { confirm } = await inquirer.prompt([{
80
+ type: 'confirm',
81
+ name: 'confirm',
82
+ message: 'This will replace your current .claude configuration. Continue?',
83
+ default: false
84
+ }]);
85
+
86
+ if (!confirm) {
87
+ console.log(chalk.yellow('Aborted.'));
88
+ process.exit(0);
89
+ }
90
+
91
+ // Ask about backup
92
+ if (!options.backup) {
93
+ const { backup } = await inquirer.prompt([{
94
+ type: 'confirm',
95
+ name: 'backup',
96
+ message: 'Backup current .claude folder first?',
97
+ default: true
98
+ }]);
99
+ options.backup = backup;
100
+ }
101
+
102
+ options.force = true;
103
+ }
104
+
105
+ const spinner = ora('Loading profile...').start();
106
+
107
+ try {
108
+ const result = await extractSnapshot(name, options);
109
+ spinner.succeed(chalk.green(`Profile loaded: ${chalk.bold(name)}`));
110
+
111
+ if (options.backup) {
112
+ console.log(chalk.dim(' Previous config backed up'));
113
+ }
114
+
115
+ console.log('');
116
+ console.log(chalk.green('✓ Your Claude CLI is now configured with this profile.'));
117
+
118
+ } catch (error) {
119
+ spinner.fail(chalk.red(`Failed to load profile: ${error.message}`));
120
+ process.exit(1);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * List all locally saved profiles
126
+ */
127
+ export async function listLocalProfiles() {
128
+ const profiles = listLocalProfileNames();
129
+
130
+ console.log('');
131
+ console.log(chalk.bold('Local Profiles'));
132
+ console.log(chalk.dim('─'.repeat(50)));
133
+
134
+ if (profiles.length === 0) {
135
+ console.log('');
136
+ console.log(chalk.dim(' No local profiles found.'));
137
+ console.log('');
138
+ console.log(chalk.dim(' Save your current config: ') + chalk.cyan('cpm save <n>'));
139
+ console.log(chalk.dim(' Browse marketplace: ') + chalk.cyan('cpm list'));
140
+ console.log('');
141
+ return;
142
+ }
143
+
144
+ console.log('');
145
+
146
+ for (const name of profiles) {
147
+ const metadata = readProfileMetadata(name);
148
+
149
+ console.log(chalk.cyan(' ' + name));
150
+
151
+ if (metadata) {
152
+ if (metadata.description) {
153
+ console.log(chalk.dim(` ${metadata.description}`));
154
+ }
155
+
156
+ const info = [];
157
+ if (metadata.tags?.length) {
158
+ info.push(chalk.yellow(metadata.tags.join(', ')));
159
+ }
160
+ if (metadata.createdAt) {
161
+ const date = new Date(metadata.createdAt).toLocaleDateString();
162
+ info.push(chalk.dim(date));
163
+ }
164
+ if (metadata.files?.length) {
165
+ info.push(chalk.dim(`${metadata.files.length} files`));
166
+ }
167
+
168
+ if (info.length) {
169
+ console.log(` ${info.join(' • ')}`);
170
+ }
171
+ }
172
+
173
+ console.log('');
174
+ }
175
+
176
+ console.log(chalk.dim('Commands:'));
177
+ console.log(chalk.dim(' Load: ') + chalk.cyan('cpm load <n>'));
178
+ console.log(chalk.dim(' Info: ') + chalk.cyan('cpm info <n>'));
179
+ console.log(chalk.dim(' Delete: ') + chalk.cyan('cpm delete <n>'));
180
+ console.log('');
181
+ }
182
+
183
+ /**
184
+ * Delete a local profile
185
+ */
186
+ export async function deleteLocalProfile(name, options) {
187
+ const profilePath = getProfilePath(name);
188
+
189
+ if (!existsSync(profilePath)) {
190
+ console.log(chalk.red(`✗ Profile not found: ${name}`));
191
+ process.exit(1);
192
+ }
193
+
194
+ if (!options.force) {
195
+ const { confirm } = await inquirer.prompt([{
196
+ type: 'confirm',
197
+ name: 'confirm',
198
+ message: `Delete profile "${name}"? This cannot be undone.`,
199
+ default: false
200
+ }]);
201
+
202
+ if (!confirm) {
203
+ console.log(chalk.yellow('Aborted.'));
204
+ process.exit(0);
205
+ }
206
+ }
207
+
208
+ rmSync(profilePath, { recursive: true, force: true });
209
+ console.log(chalk.green(`✓ Deleted profile: ${name}`));
210
+ }
211
+
212
+ /**
213
+ * Show detailed profile info
214
+ */
215
+ export async function showProfileInfo(name) {
216
+ const profilePath = getProfilePath(name);
217
+
218
+ if (!existsSync(profilePath)) {
219
+ console.log(chalk.red(`✗ Profile not found: ${name}`));
220
+ process.exit(1);
221
+ }
222
+
223
+ const metadata = readProfileMetadata(name);
224
+
225
+ console.log('');
226
+ console.log(chalk.bold('Profile Information'));
227
+ console.log(chalk.dim('─'.repeat(50)));
228
+ console.log('');
229
+
230
+ console.log(chalk.cyan('Name: ') + name);
231
+ console.log(chalk.cyan('Version: ') + (metadata?.version || '1.0.0'));
232
+ console.log(chalk.cyan('Description: ') + (metadata?.description || chalk.dim('No description')));
233
+ console.log(chalk.cyan('Tags: ') + (metadata?.tags?.join(', ') || chalk.dim('None')));
234
+ console.log(chalk.cyan('Created: ') + (metadata?.createdAt ? new Date(metadata.createdAt).toLocaleString() : chalk.dim('Unknown')));
235
+ console.log(chalk.cyan('Platform: ') + (metadata?.platform || chalk.dim('Unknown')));
236
+ console.log(chalk.cyan('Claude Ver: ') + (metadata?.claudeVersion || chalk.dim('Unknown')));
237
+
238
+ console.log('');
239
+ console.log(chalk.bold('Files Included:'));
240
+
241
+ if (metadata?.files?.length) {
242
+ const maxShow = 15;
243
+ const files = metadata.files.slice(0, maxShow);
244
+
245
+ for (const file of files) {
246
+ console.log(chalk.dim(' • ') + file);
247
+ }
248
+
249
+ if (metadata.files.length > maxShow) {
250
+ console.log(chalk.dim(` ... and ${metadata.files.length - maxShow} more files`));
251
+ }
252
+ } else {
253
+ console.log(chalk.dim(' No file information available'));
254
+ }
255
+
256
+ console.log('');
257
+ console.log(chalk.dim('Location: ') + profilePath);
258
+ console.log('');
259
+ }
@@ -0,0 +1,346 @@
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 } from 'fs';
6
+ import { join } from 'path';
7
+ import { getConfig, claudeDirExists, DEFAULTS } from '../utils/config.js';
8
+ import { extractDownloadedSnapshot } from '../utils/snapshot.js';
9
+
10
+ const INDEX_CACHE_TIME = 60 * 60 * 1000; // 1 hour
11
+
12
+ /**
13
+ * Get the marketplace index (list of all profiles)
14
+ */
15
+ async function fetchMarketplaceIndex(forceRefresh = false) {
16
+ const config = await getConfig();
17
+ const cacheFile = join(config.cacheDir, 'marketplace-index.json');
18
+
19
+ // Check cache
20
+ if (!forceRefresh && existsSync(cacheFile)) {
21
+ try {
22
+ const cached = JSON.parse(readFileSync(cacheFile, 'utf-8'));
23
+ const age = Date.now() - (cached._cachedAt || 0);
24
+
25
+ if (age < INDEX_CACHE_TIME) {
26
+ return cached;
27
+ }
28
+ } catch {
29
+ // Ignore cache errors
30
+ }
31
+ }
32
+
33
+ // Fetch from GitHub
34
+ const indexUrl = `https://raw.githubusercontent.com/${config.marketplaceRepo}/main/index.json`;
35
+
36
+ try {
37
+ const response = await fetch(indexUrl);
38
+
39
+ if (!response.ok) {
40
+ throw new Error(`Failed to fetch marketplace index: ${response.status}`);
41
+ }
42
+
43
+ const index = await response.json();
44
+ index._cachedAt = Date.now();
45
+
46
+ // Cache it
47
+ mkdirSync(config.cacheDir, { recursive: true });
48
+ writeFileSync(cacheFile, JSON.stringify(index, null, 2));
49
+
50
+ return index;
51
+ } catch (error) {
52
+ // Try to use stale cache if available
53
+ if (existsSync(cacheFile)) {
54
+ console.log(chalk.yellow('⚠ Could not refresh marketplace. Using cached data.'));
55
+ return JSON.parse(readFileSync(cacheFile, 'utf-8'));
56
+ }
57
+ throw error;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * List profiles in the marketplace
63
+ */
64
+ export async function listMarketplace(options) {
65
+ const spinner = ora('Fetching marketplace...').start();
66
+
67
+ try {
68
+ const index = await fetchMarketplaceIndex(options.refresh);
69
+ spinner.stop();
70
+
71
+ console.log('');
72
+ console.log(chalk.bold('🛒 Profile Marketplace'));
73
+ console.log(chalk.dim('─'.repeat(50)));
74
+ console.log('');
75
+
76
+ if (!index.profiles || index.profiles.length === 0) {
77
+ console.log(chalk.dim(' No profiles available yet.'));
78
+ console.log('');
79
+ console.log(chalk.dim(' Be the first to publish: ') + chalk.cyan('cpm publish <n>'));
80
+ return;
81
+ }
82
+
83
+ // Group by category
84
+ const byCategory = {};
85
+
86
+ for (const profile of index.profiles) {
87
+ const cats = profile.tags || ['uncategorized'];
88
+ for (const cat of cats) {
89
+ if (options.category && cat !== options.category) continue;
90
+
91
+ if (!byCategory[cat]) {
92
+ byCategory[cat] = [];
93
+ }
94
+ byCategory[cat].push(profile);
95
+ }
96
+ }
97
+
98
+ for (const [category, profiles] of Object.entries(byCategory)) {
99
+ console.log(chalk.magenta(`[${category}]`));
100
+ console.log('');
101
+
102
+ for (const profile of profiles) {
103
+ const fullName = `${profile.author}/${profile.name}`;
104
+ console.log(` ${chalk.cyan(fullName)} ${chalk.dim('v' + (profile.version || '1.0.0'))}`);
105
+
106
+ if (profile.description) {
107
+ console.log(` ${chalk.dim(profile.description.slice(0, 60))}${profile.description.length > 60 ? '...' : ''}`);
108
+ }
109
+
110
+ const stats = [];
111
+ if (profile.downloads) stats.push(`↓${profile.downloads}`);
112
+ if (profile.stars) stats.push(`★${profile.stars}`);
113
+ if (stats.length) {
114
+ console.log(` ${chalk.yellow(stats.join(' • '))}`);
115
+ }
116
+
117
+ console.log('');
118
+ }
119
+ }
120
+
121
+ console.log(chalk.dim('Install a profile:'));
122
+ console.log(chalk.cyan(' cpm install author/profile-name'));
123
+ console.log('');
124
+
125
+ } catch (error) {
126
+ spinner.fail(chalk.red(`Failed to fetch marketplace: ${error.message}`));
127
+ process.exit(1);
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Search the marketplace
133
+ */
134
+ export async function searchMarketplace(query) {
135
+ const spinner = ora('Searching marketplace...').start();
136
+
137
+ try {
138
+ const index = await fetchMarketplaceIndex();
139
+ spinner.stop();
140
+
141
+ const queryLower = query.toLowerCase();
142
+
143
+ const results = (index.profiles || []).filter(profile => {
144
+ const searchable = [
145
+ profile.name,
146
+ profile.author,
147
+ profile.description,
148
+ ...(profile.tags || [])
149
+ ].join(' ').toLowerCase();
150
+
151
+ return searchable.includes(queryLower);
152
+ });
153
+
154
+ console.log('');
155
+ console.log(chalk.bold(`Search Results for "${query}"`));
156
+ console.log(chalk.dim('─'.repeat(50)));
157
+ console.log('');
158
+
159
+ if (results.length === 0) {
160
+ console.log(chalk.dim(` No profiles found matching "${query}"`));
161
+ console.log('');
162
+ return;
163
+ }
164
+
165
+ for (const profile of results) {
166
+ const fullName = `${profile.author}/${profile.name}`;
167
+ console.log(` ${chalk.cyan(fullName)} ${chalk.dim('v' + (profile.version || '1.0.0'))}`);
168
+
169
+ if (profile.description) {
170
+ console.log(` ${chalk.dim(profile.description.slice(0, 60))}${profile.description.length > 60 ? '...' : ''}`);
171
+ }
172
+
173
+ if (profile.tags?.length) {
174
+ console.log(` ${chalk.yellow(profile.tags.join(', '))}`);
175
+ }
176
+
177
+ console.log('');
178
+ }
179
+
180
+ console.log(chalk.dim(`Found ${results.length} profile(s)`));
181
+ console.log('');
182
+
183
+ } catch (error) {
184
+ spinner.fail(chalk.red(`Search failed: ${error.message}`));
185
+ process.exit(1);
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Show detailed info about a marketplace profile
191
+ */
192
+ export async function showMarketplaceInfo(profilePath) {
193
+ const [author, name] = profilePath.includes('/')
194
+ ? profilePath.split('/')
195
+ : [null, profilePath];
196
+
197
+ if (!author || !name) {
198
+ console.log(chalk.red('✗ Invalid profile format. Use: author/profile-name'));
199
+ process.exit(1);
200
+ }
201
+
202
+ const spinner = ora('Fetching profile info...').start();
203
+
204
+ try {
205
+ const index = await fetchMarketplaceIndex();
206
+ const profile = (index.profiles || []).find(
207
+ p => p.author === author && p.name === name
208
+ );
209
+
210
+ if (!profile) {
211
+ spinner.fail(chalk.red(`Profile not found: ${profilePath}`));
212
+ process.exit(1);
213
+ }
214
+
215
+ // Fetch full profile metadata
216
+ const config = await getConfig();
217
+ const metadataUrl = `https://raw.githubusercontent.com/${config.marketplaceRepo}/main/profiles/${author}/${name}/profile.json`;
218
+
219
+ let metadata = profile;
220
+ try {
221
+ const response = await fetch(metadataUrl);
222
+ if (response.ok) {
223
+ metadata = { ...profile, ...await response.json() };
224
+ }
225
+ } catch {
226
+ // Use index data
227
+ }
228
+
229
+ spinner.stop();
230
+
231
+ console.log('');
232
+ console.log(chalk.bold('Profile Information'));
233
+ console.log(chalk.dim('─'.repeat(50)));
234
+ console.log('');
235
+
236
+ console.log(chalk.cyan('Name: ') + `${author}/${name}`);
237
+ console.log(chalk.cyan('Version: ') + (metadata.version || '1.0.0'));
238
+ console.log(chalk.cyan('Author: ') + author);
239
+ console.log(chalk.cyan('Description: ') + (metadata.description || chalk.dim('No description')));
240
+ console.log(chalk.cyan('Tags: ') + (metadata.tags?.join(', ') || chalk.dim('None')));
241
+
242
+ if (metadata.downloads) {
243
+ console.log(chalk.cyan('Downloads: ') + metadata.downloads);
244
+ }
245
+ if (metadata.stars) {
246
+ console.log(chalk.cyan('Stars: ') + metadata.stars);
247
+ }
248
+ if (metadata.createdAt) {
249
+ console.log(chalk.cyan('Created: ') + new Date(metadata.createdAt).toLocaleDateString());
250
+ }
251
+ if (metadata.updatedAt) {
252
+ console.log(chalk.cyan('Updated: ') + new Date(metadata.updatedAt).toLocaleDateString());
253
+ }
254
+
255
+ console.log('');
256
+ console.log(chalk.dim('Install with:'));
257
+ console.log(chalk.cyan(` cpm install ${author}/${name}`));
258
+ console.log('');
259
+
260
+ } catch (error) {
261
+ spinner.fail(chalk.red(`Failed to fetch profile: ${error.message}`));
262
+ process.exit(1);
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Install a profile from the marketplace
268
+ */
269
+ export async function installFromMarketplace(profilePath, options) {
270
+ const [author, name] = profilePath.includes('/')
271
+ ? profilePath.split('/')
272
+ : [null, profilePath];
273
+
274
+ if (!author || !name) {
275
+ console.log(chalk.red('✗ Invalid profile format. Use: author/profile-name'));
276
+ process.exit(1);
277
+ }
278
+
279
+ console.log('');
280
+ console.log(chalk.bold(`Installing: ${chalk.cyan(profilePath)}`));
281
+ console.log('');
282
+
283
+ // Confirm if .claude exists
284
+ if (claudeDirExists() && !options.force) {
285
+ const { confirm } = await inquirer.prompt([{
286
+ type: 'confirm',
287
+ name: 'confirm',
288
+ message: 'This will replace your current .claude configuration. Continue?',
289
+ default: false
290
+ }]);
291
+
292
+ if (!confirm) {
293
+ console.log(chalk.yellow('Aborted.'));
294
+ process.exit(0);
295
+ }
296
+
297
+ if (!options.backup) {
298
+ const { backup } = await inquirer.prompt([{
299
+ type: 'confirm',
300
+ name: 'backup',
301
+ message: 'Backup current .claude folder first?',
302
+ default: true
303
+ }]);
304
+ options.backup = backup;
305
+ }
306
+
307
+ options.force = true;
308
+ }
309
+
310
+ const spinner = ora('Downloading profile...').start();
311
+
312
+ try {
313
+ const config = await getConfig();
314
+
315
+ // Download the snapshot zip
316
+ const zipUrl = `https://raw.githubusercontent.com/${config.marketplaceRepo}/main/profiles/${author}/${name}/snapshot.zip`;
317
+
318
+ const response = await fetch(zipUrl);
319
+
320
+ if (!response.ok) {
321
+ if (response.status === 404) {
322
+ throw new Error(`Profile not found: ${profilePath}`);
323
+ }
324
+ throw new Error(`Download failed: ${response.status}`);
325
+ }
326
+
327
+ spinner.text = 'Extracting profile...';
328
+
329
+ const buffer = Buffer.from(await response.arrayBuffer());
330
+ await extractDownloadedSnapshot(buffer, options);
331
+
332
+ spinner.succeed(chalk.green(`Installed: ${chalk.bold(profilePath)}`));
333
+
334
+ if (options.backup) {
335
+ console.log(chalk.dim(' Previous config backed up'));
336
+ }
337
+
338
+ console.log('');
339
+ console.log(chalk.green('✓ Your Claude CLI is now configured with this profile.'));
340
+ console.log('');
341
+
342
+ } catch (error) {
343
+ spinner.fail(chalk.red(`Installation failed: ${error.message}`));
344
+ process.exit(1);
345
+ }
346
+ }