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.
@@ -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
+ }