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.
- package/LICENSE +21 -0
- package/README.md +233 -0
- package/index.json +56 -0
- package/package.json +40 -0
- package/src/cli.js +166 -0
- package/src/commands/local.js +259 -0
- package/src/commands/marketplace.js +346 -0
- package/src/commands/publish.js +184 -0
- package/src/utils/config.js +83 -0
- package/src/utils/snapshot.js +280 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import { existsSync, readFileSync, mkdirSync, cpSync, writeFileSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import { getConfig, updateConfig, getProfilePath } from '../utils/config.js';
|
|
8
|
+
import { readProfileMetadata } from '../utils/snapshot.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Publish a local profile to the marketplace
|
|
12
|
+
*/
|
|
13
|
+
export async function publishProfile(name, options) {
|
|
14
|
+
const profilePath = getProfilePath(name);
|
|
15
|
+
|
|
16
|
+
if (!existsSync(profilePath)) {
|
|
17
|
+
console.log(chalk.red(`✗ Profile not found: ${name}`));
|
|
18
|
+
console.log(chalk.dim(' List local profiles with: cpm local'));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const metadata = readProfileMetadata(name);
|
|
23
|
+
|
|
24
|
+
if (!metadata) {
|
|
25
|
+
console.log(chalk.red('✗ Invalid profile: missing metadata'));
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log('');
|
|
30
|
+
console.log(chalk.bold('Publish Profile to Marketplace'));
|
|
31
|
+
console.log(chalk.dim('─'.repeat(50)));
|
|
32
|
+
console.log('');
|
|
33
|
+
|
|
34
|
+
// Get GitHub username
|
|
35
|
+
let gitUsername;
|
|
36
|
+
try {
|
|
37
|
+
gitUsername = execSync('git config user.name', { encoding: 'utf-8' }).trim();
|
|
38
|
+
} catch {
|
|
39
|
+
gitUsername = '';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Gather publication info
|
|
43
|
+
const answers = await inquirer.prompt([
|
|
44
|
+
{
|
|
45
|
+
type: 'input',
|
|
46
|
+
name: 'author',
|
|
47
|
+
message: 'Your GitHub username:',
|
|
48
|
+
default: gitUsername,
|
|
49
|
+
validate: (input) => {
|
|
50
|
+
if (!input || !/^[a-z0-9-]+$/i.test(input)) {
|
|
51
|
+
return 'Please enter a valid GitHub username';
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
type: 'input',
|
|
58
|
+
name: 'description',
|
|
59
|
+
message: 'Profile description:',
|
|
60
|
+
default: metadata.description || '',
|
|
61
|
+
validate: (input) => input.length >= 10 || 'Description must be at least 10 characters'
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
type: 'input',
|
|
65
|
+
name: 'tags',
|
|
66
|
+
message: 'Tags (comma-separated):',
|
|
67
|
+
default: metadata.tags?.join(', ') || '',
|
|
68
|
+
filter: (input) => input.split(',').map(t => t.trim().toLowerCase()).filter(Boolean)
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
type: 'confirm',
|
|
72
|
+
name: 'confirm',
|
|
73
|
+
message: 'Ready to publish?',
|
|
74
|
+
default: true
|
|
75
|
+
}
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
if (!answers.confirm) {
|
|
79
|
+
console.log(chalk.yellow('Aborted.'));
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const config = await getConfig();
|
|
84
|
+
|
|
85
|
+
console.log('');
|
|
86
|
+
console.log(chalk.bold('📦 Publishing Profile'));
|
|
87
|
+
console.log('');
|
|
88
|
+
console.log(chalk.dim('To publish your profile, you need to:'));
|
|
89
|
+
console.log('');
|
|
90
|
+
console.log(chalk.cyan('1.') + ' Fork the marketplace repository:');
|
|
91
|
+
console.log(chalk.dim(` https://github.com/${config.marketplaceRepo}`));
|
|
92
|
+
console.log('');
|
|
93
|
+
console.log(chalk.cyan('2.') + ' Clone your fork locally:');
|
|
94
|
+
console.log(chalk.dim(` git clone https://github.com/${answers.author}/claude-profile-marketplace`));
|
|
95
|
+
console.log('');
|
|
96
|
+
console.log(chalk.cyan('3.') + ' Copy your profile to the profiles directory:');
|
|
97
|
+
|
|
98
|
+
const targetDir = `profiles/${answers.author}/${name}`;
|
|
99
|
+
console.log(chalk.dim(` mkdir -p ${targetDir}`));
|
|
100
|
+
console.log(chalk.dim(` cp -r ~/.claude-profiles/${name}/* ${targetDir}/`));
|
|
101
|
+
console.log('');
|
|
102
|
+
console.log(chalk.cyan('4.') + ' Update the profile metadata:');
|
|
103
|
+
console.log(chalk.dim(` Edit ${targetDir}/profile.json`));
|
|
104
|
+
console.log('');
|
|
105
|
+
console.log(chalk.cyan('5.') + ' Commit and push:');
|
|
106
|
+
console.log(chalk.dim(` git add .`));
|
|
107
|
+
console.log(chalk.dim(` git commit -m "Add profile: ${answers.author}/${name}"`));
|
|
108
|
+
console.log(chalk.dim(` git push origin main`));
|
|
109
|
+
console.log('');
|
|
110
|
+
console.log(chalk.cyan('6.') + ' Open a Pull Request on GitHub');
|
|
111
|
+
console.log('');
|
|
112
|
+
|
|
113
|
+
// Offer to prepare the files
|
|
114
|
+
const { prepare } = await inquirer.prompt([{
|
|
115
|
+
type: 'confirm',
|
|
116
|
+
name: 'prepare',
|
|
117
|
+
message: 'Would you like me to prepare the files for you?',
|
|
118
|
+
default: true
|
|
119
|
+
}]);
|
|
120
|
+
|
|
121
|
+
if (prepare) {
|
|
122
|
+
const exportDir = join(process.cwd(), 'publish-ready', answers.author, name);
|
|
123
|
+
mkdirSync(exportDir, { recursive: true });
|
|
124
|
+
|
|
125
|
+
// Copy profile files
|
|
126
|
+
cpSync(profilePath, exportDir, { recursive: true });
|
|
127
|
+
|
|
128
|
+
// Update metadata with author info
|
|
129
|
+
const updatedMetadata = {
|
|
130
|
+
...metadata,
|
|
131
|
+
author: answers.author,
|
|
132
|
+
description: answers.description,
|
|
133
|
+
tags: answers.tags,
|
|
134
|
+
publishedAt: new Date().toISOString()
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
writeFileSync(
|
|
138
|
+
join(exportDir, 'profile.json'),
|
|
139
|
+
JSON.stringify(updatedMetadata, null, 2)
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
console.log('');
|
|
143
|
+
console.log(chalk.green('✓ Files prepared at:'));
|
|
144
|
+
console.log(chalk.cyan(` ${exportDir}`));
|
|
145
|
+
console.log('');
|
|
146
|
+
console.log(chalk.dim('Copy these files to your fork of the marketplace repository.'));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
console.log('');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Set a custom marketplace repository
|
|
154
|
+
*/
|
|
155
|
+
export async function setRepository(repository) {
|
|
156
|
+
// Validate format
|
|
157
|
+
if (!/^[a-z0-9-]+\/[a-z0-9-]+$/i.test(repository)) {
|
|
158
|
+
console.log(chalk.red('✗ Invalid repository format. Use: owner/repo'));
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const spinner = ora('Validating repository...').start();
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
// Try to fetch the index to validate
|
|
166
|
+
const response = await fetch(
|
|
167
|
+
`https://raw.githubusercontent.com/${repository}/main/index.json`
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
if (!response.ok && response.status !== 404) {
|
|
171
|
+
throw new Error(`Repository not accessible: ${response.status}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
await updateConfig({ marketplaceRepo: repository });
|
|
175
|
+
|
|
176
|
+
spinner.succeed(chalk.green(`Repository set to: ${chalk.bold(repository)}`));
|
|
177
|
+
console.log('');
|
|
178
|
+
console.log(chalk.dim('Browse profiles with: ') + chalk.cyan('cpm list'));
|
|
179
|
+
|
|
180
|
+
} catch (error) {
|
|
181
|
+
spinner.fail(chalk.red(`Failed to set repository: ${error.message}`));
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
4
|
+
|
|
5
|
+
const HOME = homedir();
|
|
6
|
+
|
|
7
|
+
// Default paths
|
|
8
|
+
const DEFAULTS = {
|
|
9
|
+
claudeDir: join(HOME, '.claude'),
|
|
10
|
+
profilesDir: join(HOME, '.claude-profiles'),
|
|
11
|
+
cacheDir: join(HOME, '.claude-profiles', '.cache'),
|
|
12
|
+
configFile: join(HOME, '.claude-profiles', 'config.json'),
|
|
13
|
+
marketplaceRepo: 'YOUR_USERNAME/claude-profile-marketplace'
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Ensure required directories exist
|
|
18
|
+
*/
|
|
19
|
+
export function ensureDirs() {
|
|
20
|
+
const dirs = [DEFAULTS.profilesDir, DEFAULTS.cacheDir];
|
|
21
|
+
for (const dir of dirs) {
|
|
22
|
+
if (!existsSync(dir)) {
|
|
23
|
+
mkdirSync(dir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get current configuration
|
|
30
|
+
*/
|
|
31
|
+
export async function getConfig() {
|
|
32
|
+
ensureDirs();
|
|
33
|
+
|
|
34
|
+
let userConfig = {};
|
|
35
|
+
|
|
36
|
+
if (existsSync(DEFAULTS.configFile)) {
|
|
37
|
+
try {
|
|
38
|
+
userConfig = JSON.parse(readFileSync(DEFAULTS.configFile, 'utf-8'));
|
|
39
|
+
} catch (e) {
|
|
40
|
+
// Ignore invalid config
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
...DEFAULTS,
|
|
46
|
+
...userConfig
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Update configuration
|
|
52
|
+
*/
|
|
53
|
+
export async function updateConfig(updates) {
|
|
54
|
+
ensureDirs();
|
|
55
|
+
|
|
56
|
+
const current = await getConfig();
|
|
57
|
+
const newConfig = { ...current, ...updates };
|
|
58
|
+
|
|
59
|
+
// Only save user-configurable options
|
|
60
|
+
const toSave = {
|
|
61
|
+
marketplaceRepo: newConfig.marketplaceRepo
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
writeFileSync(DEFAULTS.configFile, JSON.stringify(toSave, null, 2));
|
|
65
|
+
|
|
66
|
+
return newConfig;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get the path for a local profile
|
|
71
|
+
*/
|
|
72
|
+
export function getProfilePath(name) {
|
|
73
|
+
return join(DEFAULTS.profilesDir, name);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if Claude directory exists
|
|
78
|
+
*/
|
|
79
|
+
export function claudeDirExists() {
|
|
80
|
+
return existsSync(DEFAULTS.claudeDir);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export { DEFAULTS };
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { createWriteStream, createReadStream, existsSync, mkdirSync, readdirSync, statSync, rmSync, cpSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { join, basename, relative } from 'path';
|
|
3
|
+
import archiver from 'archiver';
|
|
4
|
+
import extractZip from 'extract-zip';
|
|
5
|
+
import { getConfig, DEFAULTS } from './config.js';
|
|
6
|
+
|
|
7
|
+
// Files/patterns to exclude by default (secrets, caches, etc.)
|
|
8
|
+
const DEFAULT_EXCLUDES = [
|
|
9
|
+
'.credentials',
|
|
10
|
+
'.auth',
|
|
11
|
+
'*.key',
|
|
12
|
+
'*.pem',
|
|
13
|
+
'*.secret',
|
|
14
|
+
'oauth_token*',
|
|
15
|
+
'.cache',
|
|
16
|
+
'node_modules',
|
|
17
|
+
'.git'
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
// Files that are safe to include
|
|
21
|
+
const SAFE_INCLUDES = [
|
|
22
|
+
'settings.json',
|
|
23
|
+
'settings.local.json',
|
|
24
|
+
'CLAUDE.md',
|
|
25
|
+
'commands',
|
|
26
|
+
'commands/**',
|
|
27
|
+
'projects',
|
|
28
|
+
'projects/**',
|
|
29
|
+
'templates',
|
|
30
|
+
'templates/**',
|
|
31
|
+
'hooks',
|
|
32
|
+
'hooks/**',
|
|
33
|
+
'mcp.json',
|
|
34
|
+
'mcp_servers',
|
|
35
|
+
'mcp_servers/**'
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create a snapshot of the .claude folder
|
|
40
|
+
*/
|
|
41
|
+
export async function createSnapshot(profileName, options = {}) {
|
|
42
|
+
const config = await getConfig();
|
|
43
|
+
const claudeDir = config.claudeDir;
|
|
44
|
+
const profileDir = join(config.profilesDir, profileName);
|
|
45
|
+
|
|
46
|
+
if (!existsSync(claudeDir)) {
|
|
47
|
+
throw new Error(`Claude directory not found: ${claudeDir}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Create profile directory
|
|
51
|
+
if (existsSync(profileDir)) {
|
|
52
|
+
throw new Error(`Profile "${profileName}" already exists. Use a different name or delete the existing one.`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
mkdirSync(profileDir, { recursive: true });
|
|
56
|
+
|
|
57
|
+
const zipPath = join(profileDir, 'snapshot.zip');
|
|
58
|
+
const metadataPath = join(profileDir, 'profile.json');
|
|
59
|
+
|
|
60
|
+
// Create metadata
|
|
61
|
+
const metadata = {
|
|
62
|
+
name: profileName,
|
|
63
|
+
version: '1.0.0',
|
|
64
|
+
description: options.description || '',
|
|
65
|
+
tags: options.tags ? options.tags.split(',').map(t => t.trim()) : [],
|
|
66
|
+
createdAt: new Date().toISOString(),
|
|
67
|
+
claudeVersion: await getClaudeVersion(),
|
|
68
|
+
platform: process.platform,
|
|
69
|
+
includesSecrets: options.includeSecrets || false,
|
|
70
|
+
files: []
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Create zip archive
|
|
74
|
+
const output = createWriteStream(zipPath);
|
|
75
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
76
|
+
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
output.on('close', async () => {
|
|
79
|
+
// Save metadata
|
|
80
|
+
writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
|
|
81
|
+
resolve({ profileDir, metadata });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
archive.on('error', reject);
|
|
85
|
+
archive.on('entry', (entry) => {
|
|
86
|
+
metadata.files.push(entry.name);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
archive.pipe(output);
|
|
90
|
+
|
|
91
|
+
// Add files from .claude directory
|
|
92
|
+
const files = getFilesToArchive(claudeDir, options.includeSecrets);
|
|
93
|
+
|
|
94
|
+
for (const file of files) {
|
|
95
|
+
const fullPath = join(claudeDir, file);
|
|
96
|
+
const stat = statSync(fullPath);
|
|
97
|
+
|
|
98
|
+
if (stat.isDirectory()) {
|
|
99
|
+
archive.directory(fullPath, file);
|
|
100
|
+
} else {
|
|
101
|
+
archive.file(fullPath, { name: file });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
archive.finalize();
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get list of files to archive (respecting excludes)
|
|
111
|
+
*/
|
|
112
|
+
function getFilesToArchive(dir, includeSecrets = false) {
|
|
113
|
+
const files = [];
|
|
114
|
+
const excludes = includeSecrets ? [] : DEFAULT_EXCLUDES;
|
|
115
|
+
|
|
116
|
+
function walk(currentDir, relativePath = '') {
|
|
117
|
+
const entries = readdirSync(currentDir);
|
|
118
|
+
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
const fullPath = join(currentDir, entry);
|
|
121
|
+
const relPath = relativePath ? join(relativePath, entry) : entry;
|
|
122
|
+
|
|
123
|
+
// Check exclusions
|
|
124
|
+
if (shouldExclude(entry, relPath, excludes)) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const stat = statSync(fullPath);
|
|
129
|
+
|
|
130
|
+
if (stat.isDirectory()) {
|
|
131
|
+
walk(fullPath, relPath);
|
|
132
|
+
} else {
|
|
133
|
+
files.push(relPath);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
walk(dir);
|
|
139
|
+
return files;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check if a file/folder should be excluded
|
|
144
|
+
*/
|
|
145
|
+
function shouldExclude(name, path, excludes) {
|
|
146
|
+
for (const pattern of excludes) {
|
|
147
|
+
if (pattern.startsWith('*.')) {
|
|
148
|
+
// Extension pattern
|
|
149
|
+
const ext = pattern.slice(1);
|
|
150
|
+
if (name.endsWith(ext)) return true;
|
|
151
|
+
} else if (name === pattern || path === pattern) {
|
|
152
|
+
return true;
|
|
153
|
+
} else if (pattern.endsWith('*') && name.startsWith(pattern.slice(0, -1))) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Extract a snapshot to the .claude folder
|
|
162
|
+
*/
|
|
163
|
+
export async function extractSnapshot(profileName, options = {}) {
|
|
164
|
+
const config = await getConfig();
|
|
165
|
+
const profileDir = join(config.profilesDir, profileName);
|
|
166
|
+
const zipPath = join(profileDir, 'snapshot.zip');
|
|
167
|
+
const claudeDir = config.claudeDir;
|
|
168
|
+
|
|
169
|
+
if (!existsSync(zipPath)) {
|
|
170
|
+
throw new Error(`Profile "${profileName}" not found or corrupted`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Backup existing .claude if requested
|
|
174
|
+
if (options.backup && existsSync(claudeDir)) {
|
|
175
|
+
const backupName = `.claude-backup-${Date.now()}`;
|
|
176
|
+
const backupPath = join(DEFAULTS.profilesDir, backupName);
|
|
177
|
+
cpSync(claudeDir, backupPath, { recursive: true });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Clear existing .claude directory (if force or confirmed)
|
|
181
|
+
if (existsSync(claudeDir)) {
|
|
182
|
+
if (!options.force) {
|
|
183
|
+
throw new Error('Claude directory exists. Use --force to overwrite or --backup to save current config.');
|
|
184
|
+
}
|
|
185
|
+
rmSync(claudeDir, { recursive: true, force: true });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Create fresh .claude directory
|
|
189
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
190
|
+
|
|
191
|
+
// Extract snapshot
|
|
192
|
+
await extractZip(zipPath, { dir: claudeDir });
|
|
193
|
+
|
|
194
|
+
return { claudeDir };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Extract a downloaded snapshot (from marketplace)
|
|
199
|
+
*/
|
|
200
|
+
export async function extractDownloadedSnapshot(zipBuffer, options = {}) {
|
|
201
|
+
const config = await getConfig();
|
|
202
|
+
const claudeDir = config.claudeDir;
|
|
203
|
+
const tempZip = join(config.cacheDir, `temp-${Date.now()}.zip`);
|
|
204
|
+
|
|
205
|
+
// Write buffer to temp file
|
|
206
|
+
writeFileSync(tempZip, zipBuffer);
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
// Backup existing .claude if requested
|
|
210
|
+
if (options.backup && existsSync(claudeDir)) {
|
|
211
|
+
const backupName = `.claude-backup-${Date.now()}`;
|
|
212
|
+
const backupPath = join(DEFAULTS.profilesDir, backupName);
|
|
213
|
+
cpSync(claudeDir, backupPath, { recursive: true });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Clear existing .claude directory
|
|
217
|
+
if (existsSync(claudeDir)) {
|
|
218
|
+
if (!options.force) {
|
|
219
|
+
throw new Error('Claude directory exists. Use --force to overwrite or --backup to save current config.');
|
|
220
|
+
}
|
|
221
|
+
rmSync(claudeDir, { recursive: true, force: true });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
225
|
+
await extractZip(tempZip, { dir: claudeDir });
|
|
226
|
+
|
|
227
|
+
return { claudeDir };
|
|
228
|
+
} finally {
|
|
229
|
+
// Clean up temp file
|
|
230
|
+
if (existsSync(tempZip)) {
|
|
231
|
+
rmSync(tempZip);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Get Claude CLI version if installed
|
|
238
|
+
*/
|
|
239
|
+
async function getClaudeVersion() {
|
|
240
|
+
try {
|
|
241
|
+
const { execSync } = await import('child_process');
|
|
242
|
+
const version = execSync('claude --version', { encoding: 'utf-8' }).trim();
|
|
243
|
+
return version;
|
|
244
|
+
} catch {
|
|
245
|
+
return 'unknown';
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Read profile metadata
|
|
251
|
+
*/
|
|
252
|
+
export function readProfileMetadata(profileName) {
|
|
253
|
+
const config = DEFAULTS;
|
|
254
|
+
const metadataPath = join(config.profilesDir, profileName, 'profile.json');
|
|
255
|
+
|
|
256
|
+
if (!existsSync(metadataPath)) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return JSON.parse(readFileSync(metadataPath, 'utf-8'));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* List all local profiles
|
|
265
|
+
*/
|
|
266
|
+
export function listLocalProfileNames() {
|
|
267
|
+
const profilesDir = DEFAULTS.profilesDir;
|
|
268
|
+
|
|
269
|
+
if (!existsSync(profilesDir)) {
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return readdirSync(profilesDir)
|
|
274
|
+
.filter(name => {
|
|
275
|
+
if (name.startsWith('.')) return false;
|
|
276
|
+
const profilePath = join(profilesDir, name);
|
|
277
|
+
const stat = statSync(profilePath);
|
|
278
|
+
return stat.isDirectory() && existsSync(join(profilePath, 'profile.json'));
|
|
279
|
+
});
|
|
280
|
+
}
|