claude-switch-profile 1.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/README.md +744 -0
- package/bin/csp.js +91 -0
- package/package.json +33 -0
- package/scripts/release.js +110 -0
- package/src/commands/create.js +72 -0
- package/src/commands/current.js +13 -0
- package/src/commands/delete.js +40 -0
- package/src/commands/diff.js +119 -0
- package/src/commands/export.js +24 -0
- package/src/commands/import.js +43 -0
- package/src/commands/init.js +21 -0
- package/src/commands/list.js +26 -0
- package/src/commands/save.js +22 -0
- package/src/commands/use.js +102 -0
- package/src/constants.js +63 -0
- package/src/file-operations.js +64 -0
- package/src/output-helpers.js +27 -0
- package/src/profile-store.js +68 -0
- package/src/profile-validator.js +50 -0
- package/src/safety.js +116 -0
- package/src/symlink-manager.js +69 -0
package/bin/csp.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
import { currentCommand } from '../src/commands/current.js';
|
|
9
|
+
import { listCommand } from '../src/commands/list.js';
|
|
10
|
+
import { createCommand } from '../src/commands/create.js';
|
|
11
|
+
import { saveCommand } from '../src/commands/save.js';
|
|
12
|
+
import { useCommand } from '../src/commands/use.js';
|
|
13
|
+
import { deleteCommand } from '../src/commands/delete.js';
|
|
14
|
+
import { exportCommand } from '../src/commands/export.js';
|
|
15
|
+
import { importCommand } from '../src/commands/import.js';
|
|
16
|
+
import { diffCommand } from '../src/commands/diff.js';
|
|
17
|
+
import { initCommand } from '../src/commands/init.js';
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
21
|
+
|
|
22
|
+
const program = new Command();
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.name('csp')
|
|
26
|
+
.description('Claude Switch Profile — manage multiple Claude Code configurations')
|
|
27
|
+
.version(pkg.version);
|
|
28
|
+
|
|
29
|
+
program
|
|
30
|
+
.command('init')
|
|
31
|
+
.description('Initialize profiles directory and create default profile')
|
|
32
|
+
.action(initCommand);
|
|
33
|
+
|
|
34
|
+
program
|
|
35
|
+
.command('current')
|
|
36
|
+
.description('Show the active profile')
|
|
37
|
+
.action(currentCommand);
|
|
38
|
+
|
|
39
|
+
program
|
|
40
|
+
.command('list')
|
|
41
|
+
.alias('ls')
|
|
42
|
+
.description('List all profiles')
|
|
43
|
+
.action(listCommand);
|
|
44
|
+
|
|
45
|
+
program
|
|
46
|
+
.command('create <name>')
|
|
47
|
+
.description('Create a new profile from current Claude Code state')
|
|
48
|
+
.option('--from <profile>', 'Clone from an existing profile')
|
|
49
|
+
.option('-s, --source <path>', 'Path to .agents/ or kit directory to link')
|
|
50
|
+
.option('-d, --description <text>', 'Profile description')
|
|
51
|
+
.action(createCommand);
|
|
52
|
+
|
|
53
|
+
program
|
|
54
|
+
.command('save')
|
|
55
|
+
.description('Save current state to the active profile')
|
|
56
|
+
.action(saveCommand);
|
|
57
|
+
|
|
58
|
+
program
|
|
59
|
+
.command('use <name>')
|
|
60
|
+
.description('Switch to a different profile')
|
|
61
|
+
.option('--dry-run', 'Show what would change without executing')
|
|
62
|
+
.option('--no-save', 'Skip saving current profile before switching')
|
|
63
|
+
.option('--force', 'Switch even if symlink targets are missing')
|
|
64
|
+
.action(useCommand);
|
|
65
|
+
|
|
66
|
+
program
|
|
67
|
+
.command('delete <name>')
|
|
68
|
+
.alias('rm')
|
|
69
|
+
.description('Delete a profile')
|
|
70
|
+
.option('-f, --force', 'Skip confirmation prompt')
|
|
71
|
+
.action(deleteCommand);
|
|
72
|
+
|
|
73
|
+
program
|
|
74
|
+
.command('export <name>')
|
|
75
|
+
.description('Export a profile as tar.gz archive')
|
|
76
|
+
.option('-o, --output <path>', 'Output file path')
|
|
77
|
+
.action(exportCommand);
|
|
78
|
+
|
|
79
|
+
program
|
|
80
|
+
.command('import <file>')
|
|
81
|
+
.description('Import a profile from tar.gz archive')
|
|
82
|
+
.option('-n, --name <name>', 'Profile name (defaults to filename)')
|
|
83
|
+
.option('-d, --description <text>', 'Profile description')
|
|
84
|
+
.action(importCommand);
|
|
85
|
+
|
|
86
|
+
program
|
|
87
|
+
.command('diff <profileA> <profileB>')
|
|
88
|
+
.description('Compare two profiles (use "current" for active profile)')
|
|
89
|
+
.action(diffCommand);
|
|
90
|
+
|
|
91
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-switch-profile",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "CLI tool for managing multiple Claude Code profiles",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"csp": "./bin/csp.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test tests/*.test.js",
|
|
11
|
+
"test:core": "node --test tests/core-library.test.js",
|
|
12
|
+
"test:cli": "node --test tests/cli-integration.test.js",
|
|
13
|
+
"test:safety": "node --test tests/safety.test.js",
|
|
14
|
+
"release": "node scripts/release.js",
|
|
15
|
+
"release:patch": "node scripts/release.js patch",
|
|
16
|
+
"release:minor": "node scripts/release.js minor",
|
|
17
|
+
"release:major": "node scripts/release.js major"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"claude",
|
|
21
|
+
"profile",
|
|
22
|
+
"switcher",
|
|
23
|
+
"cli"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18.0.0"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"chalk": "^5.6.2",
|
|
31
|
+
"commander": "^14.0.3"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Release script for claude-switch-profile (csp)
|
|
5
|
+
* Usage: node scripts/release.js [patch|minor|major]
|
|
6
|
+
* Default: patch
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execSync } from 'node:child_process';
|
|
10
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { join, dirname } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const ROOT = join(__dirname, '..');
|
|
16
|
+
const PKG_PATH = join(ROOT, 'package.json');
|
|
17
|
+
|
|
18
|
+
const run = (cmd) => execSync(cmd, { cwd: ROOT, stdio: 'inherit' });
|
|
19
|
+
const runQuiet = (cmd) => execSync(cmd, { cwd: ROOT, encoding: 'utf-8' }).trim();
|
|
20
|
+
|
|
21
|
+
// --- Helpers ---
|
|
22
|
+
|
|
23
|
+
const bumpVersion = (current, type) => {
|
|
24
|
+
const [major, minor, patch] = current.split('.').map(Number);
|
|
25
|
+
const bumps = {
|
|
26
|
+
major: `${major + 1}.0.0`,
|
|
27
|
+
minor: `${major}.${minor + 1}.0`,
|
|
28
|
+
patch: `${major}.${minor}.${patch + 1}`,
|
|
29
|
+
};
|
|
30
|
+
return bumps[type] || bumps.patch;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const checkCleanWorkingTree = () => {
|
|
34
|
+
const status = runQuiet('git status --porcelain');
|
|
35
|
+
if (status) {
|
|
36
|
+
console.error('✗ Working tree is not clean. Commit or stash changes first.');
|
|
37
|
+
console.error(status);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const checkOnMainBranch = () => {
|
|
43
|
+
const branch = runQuiet('git branch --show-current');
|
|
44
|
+
if (branch !== 'main' && branch !== 'master') {
|
|
45
|
+
console.error(`✗ Must be on main/master branch. Current: ${branch}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const checkNpmAuth = () => {
|
|
51
|
+
try {
|
|
52
|
+
runQuiet('npm whoami');
|
|
53
|
+
} catch {
|
|
54
|
+
console.error('✗ Not logged in to npm. Run "npm login" first.');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// --- Main ---
|
|
60
|
+
|
|
61
|
+
const main = () => {
|
|
62
|
+
const type = process.argv[2] || 'patch';
|
|
63
|
+
if (!['patch', 'minor', 'major'].includes(type)) {
|
|
64
|
+
console.error(`✗ Invalid bump type: "${type}". Use patch, minor, or major.`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Pre-flight checks
|
|
69
|
+
console.log('Pre-flight checks...');
|
|
70
|
+
checkCleanWorkingTree();
|
|
71
|
+
checkOnMainBranch();
|
|
72
|
+
checkNpmAuth();
|
|
73
|
+
console.log('✓ All checks passed\n');
|
|
74
|
+
|
|
75
|
+
// Read current version
|
|
76
|
+
const pkg = JSON.parse(readFileSync(PKG_PATH, 'utf-8'));
|
|
77
|
+
const oldVersion = pkg.version;
|
|
78
|
+
const newVersion = bumpVersion(oldVersion, type);
|
|
79
|
+
|
|
80
|
+
console.log(`Bumping version: ${oldVersion} → ${newVersion} (${type})\n`);
|
|
81
|
+
|
|
82
|
+
// Run tests
|
|
83
|
+
console.log('Running tests...');
|
|
84
|
+
run('npm test');
|
|
85
|
+
console.log('✓ Tests passed\n');
|
|
86
|
+
|
|
87
|
+
// Update package.json version
|
|
88
|
+
pkg.version = newVersion;
|
|
89
|
+
writeFileSync(PKG_PATH, JSON.stringify(pkg, null, 2) + '\n');
|
|
90
|
+
console.log(`✓ Updated package.json to ${newVersion}`);
|
|
91
|
+
|
|
92
|
+
// Git commit + tag
|
|
93
|
+
run('git add package.json');
|
|
94
|
+
run(`git commit -m "chore(release): v${newVersion}"`);
|
|
95
|
+
run(`git tag -a v${newVersion} -m "v${newVersion}"`);
|
|
96
|
+
console.log(`✓ Created tag v${newVersion}`);
|
|
97
|
+
|
|
98
|
+
// Publish to npm
|
|
99
|
+
console.log('\nPublishing to npm...');
|
|
100
|
+
run('npm publish');
|
|
101
|
+
console.log(`✓ Published claude-switch-profile@${newVersion}`);
|
|
102
|
+
|
|
103
|
+
// Push to remote
|
|
104
|
+
run('git push && git push --tags');
|
|
105
|
+
console.log(`✓ Pushed to remote with tags\n`);
|
|
106
|
+
|
|
107
|
+
console.log(`🎉 Released v${newVersion} successfully!`);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
main();
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { mkdirSync, cpSync, existsSync, writeFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { addProfile, getActive, setActive, profileExists, getProfileDir } from '../profile-store.js';
|
|
4
|
+
import { saveSymlinks } from '../symlink-manager.js';
|
|
5
|
+
import { saveFiles } from '../file-operations.js';
|
|
6
|
+
import { SYMLINK_ITEMS, SOURCE_FILE } from '../constants.js';
|
|
7
|
+
import { success, error, info, warn } from '../output-helpers.js';
|
|
8
|
+
|
|
9
|
+
export const createCommand = (name, options) => {
|
|
10
|
+
if (profileExists(name)) {
|
|
11
|
+
error(`Profile "${name}" already exists.`);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const profileDir = getProfileDir(name);
|
|
16
|
+
|
|
17
|
+
if (options.from) {
|
|
18
|
+
// Clone from existing profile
|
|
19
|
+
if (!profileExists(options.from)) {
|
|
20
|
+
error(`Source profile "${options.from}" does not exist.`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
const sourceDir = getProfileDir(options.from);
|
|
24
|
+
cpSync(sourceDir, profileDir, { recursive: true });
|
|
25
|
+
info(`Cloned from profile "${options.from}"`);
|
|
26
|
+
} else if (options.source) {
|
|
27
|
+
// Create from a specific .agents/ directory (or any kit directory)
|
|
28
|
+
const sourcePath = resolve(options.source);
|
|
29
|
+
if (!existsSync(sourcePath)) {
|
|
30
|
+
error(`Source directory "${options.source}" does not exist.`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
mkdirSync(profileDir, { recursive: true });
|
|
35
|
+
|
|
36
|
+
// Build source.json from the kit directory
|
|
37
|
+
const sourceMap = {};
|
|
38
|
+
for (const item of SYMLINK_ITEMS) {
|
|
39
|
+
const target = join(sourcePath, item);
|
|
40
|
+
if (existsSync(target)) {
|
|
41
|
+
sourceMap[item] = target;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (Object.keys(sourceMap).length === 0) {
|
|
46
|
+
warn(`No recognized items found in "${sourcePath}". Expected: ${SYMLINK_ITEMS.join(', ')}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
writeFileSync(join(profileDir, SOURCE_FILE), JSON.stringify(sourceMap, null, 2) + '\n');
|
|
50
|
+
info(`Linked to kit at ${sourcePath}`);
|
|
51
|
+
info(`Items found: ${Object.keys(sourceMap).join(', ') || 'none'}`);
|
|
52
|
+
|
|
53
|
+
// Also copy current mutable files
|
|
54
|
+
saveFiles(profileDir);
|
|
55
|
+
} else {
|
|
56
|
+
// Create from current ~/.claude state
|
|
57
|
+
mkdirSync(profileDir, { recursive: true });
|
|
58
|
+
saveSymlinks(profileDir);
|
|
59
|
+
saveFiles(profileDir);
|
|
60
|
+
info('Captured current Claude Code configuration');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
addProfile(name, { description: options.description || '' });
|
|
64
|
+
|
|
65
|
+
// Set as active if first profile
|
|
66
|
+
if (!getActive()) {
|
|
67
|
+
setActive(name);
|
|
68
|
+
info(`Set "${name}" as active profile`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
success(`Profile "${name}" created at ${profileDir}`);
|
|
72
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getActive } from '../profile-store.js';
|
|
2
|
+
import { getProfileDir } from '../profile-store.js';
|
|
3
|
+
import { success, info, warn } from '../output-helpers.js';
|
|
4
|
+
|
|
5
|
+
export const currentCommand = () => {
|
|
6
|
+
const active = getActive();
|
|
7
|
+
if (!active) {
|
|
8
|
+
warn('No active profile. Run "csp create <name>" to create one.');
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
success(`Active profile: ${active}`);
|
|
12
|
+
info(`Location: ${getProfileDir(active)}`);
|
|
13
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { rmSync } from 'node:fs';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import { getActive, removeProfile, profileExists, getProfileDir } from '../profile-store.js';
|
|
4
|
+
import { success, error, warn } from '../output-helpers.js';
|
|
5
|
+
|
|
6
|
+
const confirm = (question) => {
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
9
|
+
rl.question(question, (answer) => {
|
|
10
|
+
rl.close();
|
|
11
|
+
resolve(answer.toLowerCase().startsWith('y'));
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const deleteCommand = async (name, options) => {
|
|
17
|
+
if (!profileExists(name)) {
|
|
18
|
+
error(`Profile "${name}" does not exist.`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const active = getActive();
|
|
23
|
+
if (active === name) {
|
|
24
|
+
error(`Cannot delete active profile "${name}". Switch to another profile first.`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!options.force) {
|
|
29
|
+
const confirmed = await confirm(`Delete profile "${name}"? This cannot be undone. (y/N) `);
|
|
30
|
+
if (!confirmed) {
|
|
31
|
+
warn('Cancelled.');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const profileDir = getProfileDir(name);
|
|
37
|
+
rmSync(profileDir, { recursive: true, force: true });
|
|
38
|
+
removeProfile(name);
|
|
39
|
+
success(`Profile "${name}" deleted.`);
|
|
40
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { profileExists, getProfileDir, getActive } from '../profile-store.js';
|
|
5
|
+
import { SOURCE_FILE } from '../constants.js';
|
|
6
|
+
import { error, info } from '../output-helpers.js';
|
|
7
|
+
|
|
8
|
+
export const diffCommand = (profileA, profileB) => {
|
|
9
|
+
// Resolve "current" alias
|
|
10
|
+
const resolveProfile = (name) => {
|
|
11
|
+
if (name === 'current') {
|
|
12
|
+
const active = getActive();
|
|
13
|
+
if (!active) {
|
|
14
|
+
error('No active profile to use as "current".');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
return active;
|
|
18
|
+
}
|
|
19
|
+
return name;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const nameA = resolveProfile(profileA);
|
|
23
|
+
const nameB = resolveProfile(profileB);
|
|
24
|
+
|
|
25
|
+
if (!profileExists(nameA)) {
|
|
26
|
+
error(`Profile "${nameA}" does not exist.`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
if (!profileExists(nameB)) {
|
|
30
|
+
error(`Profile "${nameB}" does not exist.`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const dirA = getProfileDir(nameA);
|
|
35
|
+
const dirB = getProfileDir(nameB);
|
|
36
|
+
|
|
37
|
+
console.log(`\n${chalk.bold('Comparing:')} ${chalk.cyan(nameA)} ↔ ${chalk.cyan(nameB)}\n`);
|
|
38
|
+
|
|
39
|
+
// Compare source.json (symlink targets)
|
|
40
|
+
const sourceA = readJsonSafe(join(dirA, SOURCE_FILE));
|
|
41
|
+
const sourceB = readJsonSafe(join(dirB, SOURCE_FILE));
|
|
42
|
+
diffObject('Symlink targets (source.json)', sourceA, sourceB, nameA, nameB);
|
|
43
|
+
|
|
44
|
+
// Compare files that exist in either profile
|
|
45
|
+
const filesA = new Set(readdirSync(dirA).filter((f) => f !== SOURCE_FILE));
|
|
46
|
+
const filesB = new Set(readdirSync(dirB).filter((f) => f !== SOURCE_FILE));
|
|
47
|
+
|
|
48
|
+
const allFiles = new Set([...filesA, ...filesB]);
|
|
49
|
+
const diffs = [];
|
|
50
|
+
|
|
51
|
+
for (const file of allFiles) {
|
|
52
|
+
const inA = filesA.has(file);
|
|
53
|
+
const inB = filesB.has(file);
|
|
54
|
+
|
|
55
|
+
if (inA && !inB) {
|
|
56
|
+
diffs.push({ file, status: `only in ${nameA}` });
|
|
57
|
+
} else if (!inA && inB) {
|
|
58
|
+
diffs.push({ file, status: `only in ${nameB}` });
|
|
59
|
+
} else {
|
|
60
|
+
// Both exist — compare content for regular files
|
|
61
|
+
const pathA = join(dirA, file);
|
|
62
|
+
const pathB = join(dirB, file);
|
|
63
|
+
try {
|
|
64
|
+
const contentA = readFileSync(pathA, 'utf-8');
|
|
65
|
+
const contentB = readFileSync(pathB, 'utf-8');
|
|
66
|
+
if (contentA !== contentB) {
|
|
67
|
+
diffs.push({ file, status: 'different' });
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
diffs.push({ file, status: 'could not compare (directory?)' });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (diffs.length === 0) {
|
|
76
|
+
info('Profiles are identical (excluding symlink targets).');
|
|
77
|
+
} else {
|
|
78
|
+
console.log(chalk.bold('File differences:'));
|
|
79
|
+
for (const d of diffs) {
|
|
80
|
+
const color = d.status === 'different' ? chalk.yellow : chalk.dim;
|
|
81
|
+
console.log(` ${color(d.file)} — ${d.status}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log('');
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const readJsonSafe = (path) => {
|
|
89
|
+
try {
|
|
90
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
91
|
+
} catch {
|
|
92
|
+
return {};
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const diffObject = (label, objA, objB, nameA, nameB) => {
|
|
97
|
+
const allKeys = new Set([...Object.keys(objA), ...Object.keys(objB)]);
|
|
98
|
+
const diffs = [];
|
|
99
|
+
|
|
100
|
+
for (const key of allKeys) {
|
|
101
|
+
const valA = objA[key];
|
|
102
|
+
const valB = objB[key];
|
|
103
|
+
if (valA !== valB) {
|
|
104
|
+
diffs.push({ key, a: valA || '(none)', b: valB || '(none)' });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (diffs.length === 0) {
|
|
109
|
+
info(`${label}: identical`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log(chalk.bold(`${label}:`));
|
|
114
|
+
for (const d of diffs) {
|
|
115
|
+
console.log(` ${chalk.dim(d.key)}:`);
|
|
116
|
+
console.log(` ${chalk.red(`${nameA}:`)} ${d.a}`);
|
|
117
|
+
console.log(` ${chalk.green(`${nameB}:`)} ${d.b}`);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { profileExists, getProfileDir } from '../profile-store.js';
|
|
5
|
+
import { success, error } from '../output-helpers.js';
|
|
6
|
+
|
|
7
|
+
export const exportCommand = (name, options) => {
|
|
8
|
+
if (!profileExists(name)) {
|
|
9
|
+
error(`Profile "${name}" does not exist.`);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const profileDir = getProfileDir(name);
|
|
14
|
+
const output = options.output || `./${name}.csp.tar.gz`;
|
|
15
|
+
const outputPath = resolve(output);
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
execFileSync('tar', ['-czf', outputPath, '-C', profileDir, '.'], { stdio: 'pipe' });
|
|
19
|
+
success(`Profile "${name}" exported to ${outputPath}`);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
error(`Failed to export: ${err.message}`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { resolve, basename } from 'node:path';
|
|
4
|
+
import { addProfile, profileExists, getProfileDir, ensureProfilesDir } from '../profile-store.js';
|
|
5
|
+
import { validateProfile } from '../profile-validator.js';
|
|
6
|
+
import { success, error, warn } from '../output-helpers.js';
|
|
7
|
+
|
|
8
|
+
export const importCommand = (file, options) => {
|
|
9
|
+
const filePath = resolve(file);
|
|
10
|
+
if (!existsSync(filePath)) {
|
|
11
|
+
error(`File "${file}" does not exist.`);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Derive name from filename if not provided
|
|
16
|
+
const name = options.name || basename(file).replace(/\.csp\.tar\.gz$/, '').replace(/\.tar\.gz$/, '');
|
|
17
|
+
|
|
18
|
+
if (profileExists(name)) {
|
|
19
|
+
error(`Profile "${name}" already exists. Use --name to specify a different name.`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
ensureProfilesDir();
|
|
24
|
+
const profileDir = getProfileDir(name);
|
|
25
|
+
mkdirSync(profileDir, { recursive: true });
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
execFileSync('tar', ['-xzf', filePath, '-C', profileDir], { stdio: 'pipe' });
|
|
29
|
+
} catch (err) {
|
|
30
|
+
error(`Failed to extract: ${err.message}`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Validate imported profile
|
|
35
|
+
const validation = validateProfile(profileDir);
|
|
36
|
+
if (!validation.valid) {
|
|
37
|
+
warn('Imported profile has issues:');
|
|
38
|
+
validation.errors.forEach((e) => warn(` ${e}`));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
addProfile(name, { description: options.description || `Imported from ${basename(file)}` });
|
|
42
|
+
success(`Profile "${name}" imported from ${filePath}`);
|
|
43
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { ensureProfilesDir, getActive, profileExists } from '../profile-store.js';
|
|
3
|
+
import { createCommand } from './create.js';
|
|
4
|
+
import { success, info, warn } from '../output-helpers.js';
|
|
5
|
+
import { PROFILES_DIR } from '../constants.js';
|
|
6
|
+
|
|
7
|
+
export const initCommand = () => {
|
|
8
|
+
if (existsSync(PROFILES_DIR) && getActive()) {
|
|
9
|
+
const active = getActive();
|
|
10
|
+
info(`Already initialized. Active profile: "${active}"`);
|
|
11
|
+
info(`Profiles directory: ${PROFILES_DIR}`);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
ensureProfilesDir();
|
|
16
|
+
info('Initializing Claude Switch Profile...');
|
|
17
|
+
|
|
18
|
+
// Create default profile from current state
|
|
19
|
+
createCommand('default', { description: 'Default profile (initial capture)' });
|
|
20
|
+
success('Initialization complete. Your current setup is saved as "default".');
|
|
21
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { readProfiles, getActive } from '../profile-store.js';
|
|
3
|
+
import { info } from '../output-helpers.js';
|
|
4
|
+
|
|
5
|
+
export const listCommand = () => {
|
|
6
|
+
const profiles = readProfiles();
|
|
7
|
+
const active = getActive();
|
|
8
|
+
const names = Object.keys(profiles);
|
|
9
|
+
|
|
10
|
+
if (names.length === 0) {
|
|
11
|
+
info('No profiles found. Run "csp create <name>" to create one.');
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
console.log('');
|
|
16
|
+
for (const name of names) {
|
|
17
|
+
const meta = profiles[name];
|
|
18
|
+
const isActive = name === active;
|
|
19
|
+
const marker = isActive ? chalk.green(' * ') : ' ';
|
|
20
|
+
const label = isActive ? chalk.green.bold(name) : name;
|
|
21
|
+
const desc = meta.description ? chalk.dim(` — ${meta.description}`) : '';
|
|
22
|
+
const date = meta.created ? chalk.dim(` (${meta.created.split('T')[0]})`) : '';
|
|
23
|
+
console.log(`${marker}${label}${desc}${date}`);
|
|
24
|
+
}
|
|
25
|
+
console.log('');
|
|
26
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getActive, profileExists, getProfileDir } from '../profile-store.js';
|
|
2
|
+
import { saveSymlinks } from '../symlink-manager.js';
|
|
3
|
+
import { saveFiles } from '../file-operations.js';
|
|
4
|
+
import { success, error } from '../output-helpers.js';
|
|
5
|
+
|
|
6
|
+
export const saveCommand = () => {
|
|
7
|
+
const active = getActive();
|
|
8
|
+
if (!active) {
|
|
9
|
+
error('No active profile. Run "csp create <name>" first.');
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!profileExists(active)) {
|
|
14
|
+
error(`Active profile "${active}" directory is missing.`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const profileDir = getProfileDir(active);
|
|
19
|
+
saveSymlinks(profileDir);
|
|
20
|
+
saveFiles(profileDir);
|
|
21
|
+
success(`Saved current state to profile "${active}"`);
|
|
22
|
+
};
|