@haiyangj/ccs 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/CHANGELOG.md +47 -0
- package/DESIGN_SPEC.md +530 -0
- package/IMPLEMENTATION.md +340 -0
- package/README.md +365 -0
- package/UPDATE_NOTES.md +126 -0
- package/bin/ccm.js +96 -0
- package/package.json +28 -0
- package/src/commands/add.js +98 -0
- package/src/commands/delete.js +50 -0
- package/src/commands/export.js +46 -0
- package/src/commands/import.js +99 -0
- package/src/commands/init.js +55 -0
- package/src/commands/list.js +55 -0
- package/src/commands/rename.js +40 -0
- package/src/commands/set.js +90 -0
- package/src/commands/show.js +33 -0
- package/src/commands/use.js +61 -0
- package/src/core/config.js +123 -0
- package/src/core/profiles.js +221 -0
- package/src/core/validator.js +96 -0
- package/src/utils/logger.js +66 -0
- package/src/utils/paths.js +83 -0
package/UPDATE_NOTES.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# 更新说明 (v1.0.1)
|
|
2
|
+
|
|
3
|
+
## 主要变更
|
|
4
|
+
|
|
5
|
+
### 1. ✅ 修复 settings.json 结构
|
|
6
|
+
**问题**: 之前的实现更新的是顶层的 `apiUrl` 和 `apiKey` 字段,但实际 Claude Code 使用的是嵌套的 `env` 对象。
|
|
7
|
+
|
|
8
|
+
**修复内容**:
|
|
9
|
+
- 现在正确更新 `env.ANTHROPIC_BASE_URL`
|
|
10
|
+
- 现在正确更新 `env.ANTHROPIC_AUTH_TOKEN`
|
|
11
|
+
- 保留其他配置项(如 `enabledPlugins`)
|
|
12
|
+
|
|
13
|
+
**实际的 settings.json 格式**:
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"env": {
|
|
17
|
+
"ANTHROPIC_AUTH_TOKEN": "your-api-key",
|
|
18
|
+
"ANTHROPIC_BASE_URL": "https://api.url.com"
|
|
19
|
+
},
|
|
20
|
+
"enabledPlugins": {
|
|
21
|
+
"document-skills@anthropic-agent-skills": true,
|
|
22
|
+
"code-review@claude-plugins-official": true,
|
|
23
|
+
"superpowers@superpowers-marketplace": true
|
|
24
|
+
},
|
|
25
|
+
"apiUrl": "",
|
|
26
|
+
"apiKey": ""
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### 2. ✅ 移除 API Key 格式限制
|
|
31
|
+
**问题**: 之前要求 API key 必须以 `sk-ant-` 开头,不支持其他 API 提供商。
|
|
32
|
+
|
|
33
|
+
**修复内容**:
|
|
34
|
+
- 移除 `sk-ant-` 前缀要求
|
|
35
|
+
- 最小长度从 20 字符降低到 10 字符
|
|
36
|
+
- 支持任意格式的 API key
|
|
37
|
+
|
|
38
|
+
**示例**: 现在可以使用以下格式的 key:
|
|
39
|
+
- `sk-ant-xxxxx` (Anthropic 官方)
|
|
40
|
+
- `cr_xxxxx` (其他提供商)
|
|
41
|
+
- 任何自定义格式
|
|
42
|
+
|
|
43
|
+
## 修改的文件
|
|
44
|
+
|
|
45
|
+
### 核心代码
|
|
46
|
+
1. **src/core/config.js**
|
|
47
|
+
- `getApiConfig()` - 从嵌套的 env 对象读取配置
|
|
48
|
+
- `updateApiConfig()` - 更新嵌套的 env 对象
|
|
49
|
+
- `createSettings()` - 创建正确的初始结构
|
|
50
|
+
|
|
51
|
+
2. **src/core/validator.js**
|
|
52
|
+
- `validateApiKey()` - 放宽验证规则
|
|
53
|
+
|
|
54
|
+
### 文档
|
|
55
|
+
3. **DESIGN_SPEC.md** - 更新验证规则和示例
|
|
56
|
+
4. **README.md** - 更新 settings.json 示例
|
|
57
|
+
5. **IMPLEMENTATION.md** - 更新数据结构说明
|
|
58
|
+
6. **CHANGELOG.md** - 记录变更历史
|
|
59
|
+
7. **package.json** - 版本升级到 1.0.1
|
|
60
|
+
|
|
61
|
+
## 使用示例
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# 现在可以添加任意格式的 API key
|
|
65
|
+
ccm add aihezu https://cn.aihezu.dev/api cr_b31cbdea1b3fda0a5bfcd7ae11f3c6e5b01b5bc9702a394c522c45a54a4fc886
|
|
66
|
+
|
|
67
|
+
# 切换配置
|
|
68
|
+
ccm use aihezu
|
|
69
|
+
|
|
70
|
+
# 查看当前配置
|
|
71
|
+
ccm show
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## 验证测试
|
|
75
|
+
|
|
76
|
+
建议进行以下测试:
|
|
77
|
+
1. [ ] `ccm init` - 初始化
|
|
78
|
+
2. [ ] `ccm add` - 添加新配置(使用非 sk-ant- 格式的 key)
|
|
79
|
+
3. [ ] `ccm use` - 切换配置
|
|
80
|
+
4. [ ] 检查 settings.json 文件是否正确更新 env 字段
|
|
81
|
+
5. [ ] 验证其他配置(如 enabledPlugins)是否保留
|
|
82
|
+
|
|
83
|
+
## 向后兼容性
|
|
84
|
+
|
|
85
|
+
- ✅ 如果 settings.json 不存在,会创建正确格式的文件
|
|
86
|
+
- ✅ 如果 settings.json 已存在但缺少 env 对象,会自动创建
|
|
87
|
+
- ✅ 保留所有其他配置项,不会覆盖
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
**更新日期**: 2026-01-16
|
|
92
|
+
**版本**: 1.0.0 → 1.0.1
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 包名更新记录
|
|
97
|
+
|
|
98
|
+
### 最终包名: @haiyangj/ccs
|
|
99
|
+
|
|
100
|
+
**发布命名**: `@haiyangj/ccs` (Haiyangj's Claude Config Switcher)
|
|
101
|
+
|
|
102
|
+
**原因**: 原始包名 `claude-config-manager` 已被其他开发者占用
|
|
103
|
+
|
|
104
|
+
**命名历史**:
|
|
105
|
+
1. ❌ `claude-config-manager` - 已被占用
|
|
106
|
+
2. ❌ `claude-profile-switcher` - 已被占用
|
|
107
|
+
3. ✅ `@haiyangj/ccs` - 最终选择(作用域包)
|
|
108
|
+
|
|
109
|
+
**安装方式**:
|
|
110
|
+
```bash
|
|
111
|
+
# 全局安装(推荐)
|
|
112
|
+
npm install -g @haiyangj/ccs
|
|
113
|
+
|
|
114
|
+
# 从源码安装
|
|
115
|
+
git clone https://github.com/haiyangj/ccs.git
|
|
116
|
+
cd ccs
|
|
117
|
+
npm install
|
|
118
|
+
npm link
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**作用域包的优势**:
|
|
122
|
+
- ✅ 不会与他人的包冲突
|
|
123
|
+
- ✅ 明确的包归属权
|
|
124
|
+
- ✅ 可以发布为私有包(免费)
|
|
125
|
+
- ✅ 更好的组织结构
|
|
126
|
+
|
package/bin/ccm.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { initCommand } from '../src/commands/init.js';
|
|
5
|
+
import { listCommand } from '../src/commands/list.js';
|
|
6
|
+
import { showCommand } from '../src/commands/show.js';
|
|
7
|
+
import { addCommand } from '../src/commands/add.js';
|
|
8
|
+
import { useCommand } from '../src/commands/use.js';
|
|
9
|
+
import { setCommand } from '../src/commands/set.js';
|
|
10
|
+
import { deleteCommand } from '../src/commands/delete.js';
|
|
11
|
+
import { renameCommand } from '../src/commands/rename.js';
|
|
12
|
+
import { exportCommand } from '../src/commands/export.js';
|
|
13
|
+
import { importCommand } from '../src/commands/import.js';
|
|
14
|
+
|
|
15
|
+
const program = new Command();
|
|
16
|
+
|
|
17
|
+
program
|
|
18
|
+
.name('ccm')
|
|
19
|
+
.description('CLI tool for managing Claude Code configuration profiles')
|
|
20
|
+
.version('1.0.0');
|
|
21
|
+
|
|
22
|
+
// Init command
|
|
23
|
+
program
|
|
24
|
+
.command('init')
|
|
25
|
+
.description('Initialize profile management')
|
|
26
|
+
.action(initCommand);
|
|
27
|
+
|
|
28
|
+
// List command
|
|
29
|
+
program
|
|
30
|
+
.command('list')
|
|
31
|
+
.alias('ls')
|
|
32
|
+
.description('List all profiles')
|
|
33
|
+
.action(listCommand);
|
|
34
|
+
|
|
35
|
+
// Show command
|
|
36
|
+
program
|
|
37
|
+
.command('show')
|
|
38
|
+
.alias('current')
|
|
39
|
+
.description('Show current active profile')
|
|
40
|
+
.action(showCommand);
|
|
41
|
+
|
|
42
|
+
// Add command
|
|
43
|
+
program
|
|
44
|
+
.command('add [name] [url] [key]')
|
|
45
|
+
.description('Add a new profile')
|
|
46
|
+
.option('--no-switch', 'Do not prompt to switch to new profile')
|
|
47
|
+
.action(addCommand);
|
|
48
|
+
|
|
49
|
+
// Use command
|
|
50
|
+
program
|
|
51
|
+
.command('use <name>')
|
|
52
|
+
.description('Switch to a profile')
|
|
53
|
+
.action(useCommand);
|
|
54
|
+
|
|
55
|
+
// Set command
|
|
56
|
+
program
|
|
57
|
+
.command('set [name] [url] [key]')
|
|
58
|
+
.description('Update an existing profile')
|
|
59
|
+
.action(setCommand);
|
|
60
|
+
|
|
61
|
+
// Delete command
|
|
62
|
+
program
|
|
63
|
+
.command('delete <name>')
|
|
64
|
+
.alias('rm')
|
|
65
|
+
.description('Delete a profile')
|
|
66
|
+
.option('-f, --force', 'Skip confirmation prompt')
|
|
67
|
+
.action(deleteCommand);
|
|
68
|
+
|
|
69
|
+
// Rename command
|
|
70
|
+
program
|
|
71
|
+
.command('rename <old-name> <new-name>')
|
|
72
|
+
.alias('mv')
|
|
73
|
+
.description('Rename a profile')
|
|
74
|
+
.action(renameCommand);
|
|
75
|
+
|
|
76
|
+
// Export command
|
|
77
|
+
program
|
|
78
|
+
.command('export [file]')
|
|
79
|
+
.description('Export profiles to a file (default: ./claude-profiles-backup.json)')
|
|
80
|
+
.option('--no-keys', 'Export without API keys')
|
|
81
|
+
.action(exportCommand);
|
|
82
|
+
|
|
83
|
+
// Import command
|
|
84
|
+
program
|
|
85
|
+
.command('import <file>')
|
|
86
|
+
.description('Import profiles from a file')
|
|
87
|
+
.option('--merge', 'Merge with existing profiles (overwrite conflicts)')
|
|
88
|
+
.action(importCommand);
|
|
89
|
+
|
|
90
|
+
// Parse arguments
|
|
91
|
+
program.parse(process.argv);
|
|
92
|
+
|
|
93
|
+
// Show help if no command provided
|
|
94
|
+
if (!process.argv.slice(2).length) {
|
|
95
|
+
program.outputHelp();
|
|
96
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@haiyangj/ccs",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "CLI tool for managing Claude Code configuration profiles",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ccm": "./bin/ccm.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"claude",
|
|
14
|
+
"config",
|
|
15
|
+
"cli",
|
|
16
|
+
"profile",
|
|
17
|
+
"manager"
|
|
18
|
+
],
|
|
19
|
+
"author": "",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"chalk": "^5.6.2",
|
|
24
|
+
"commander": "^14.0.2",
|
|
25
|
+
"inquirer": "^13.2.0",
|
|
26
|
+
"ora": "^9.0.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import { addProfile } from '../core/profiles.js';
|
|
3
|
+
import { updateApiConfig } from '../core/config.js';
|
|
4
|
+
import { validateProfile } from '../core/validator.js';
|
|
5
|
+
import { success, error, maskApiKey } from '../utils/logger.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Add a new profile
|
|
9
|
+
*/
|
|
10
|
+
export async function addCommand(name, url, key, options = {}) {
|
|
11
|
+
try {
|
|
12
|
+
// Interactive mode if arguments not provided
|
|
13
|
+
if (!name || !url || !key) {
|
|
14
|
+
const answers = await inquirer.prompt([
|
|
15
|
+
{
|
|
16
|
+
type: 'input',
|
|
17
|
+
name: 'name',
|
|
18
|
+
message: 'Profile name:',
|
|
19
|
+
default: name,
|
|
20
|
+
validate: (input) => {
|
|
21
|
+
const result = validateProfile(input, 'https://example.com', 'sk-ant-12345678901234567890');
|
|
22
|
+
if (!result.valid && result.error.includes('name')) {
|
|
23
|
+
return result.error;
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
type: 'input',
|
|
30
|
+
name: 'url',
|
|
31
|
+
message: 'API URL:',
|
|
32
|
+
default: url || 'https://api.anthropic.com',
|
|
33
|
+
validate: (input) => {
|
|
34
|
+
const result = validateProfile('default', input, 'sk-ant-12345678901234567890');
|
|
35
|
+
if (!result.valid && result.error.includes('URL')) {
|
|
36
|
+
return result.error;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
type: 'password',
|
|
43
|
+
name: 'key',
|
|
44
|
+
message: 'API Key:',
|
|
45
|
+
mask: '*',
|
|
46
|
+
default: key,
|
|
47
|
+
validate: (input) => {
|
|
48
|
+
const result = validateProfile('default', 'https://api.anthropic.com', input);
|
|
49
|
+
if (!result.valid && result.error.includes('key')) {
|
|
50
|
+
return result.error;
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
name = answers.name;
|
|
58
|
+
url = answers.url;
|
|
59
|
+
key = answers.key;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Validate inputs
|
|
63
|
+
const validation = validateProfile(name, url, key);
|
|
64
|
+
if (!validation.valid) {
|
|
65
|
+
error(validation.error);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Add profile
|
|
70
|
+
const profile = addProfile(name, url, key);
|
|
71
|
+
|
|
72
|
+
success(`Profile '${name}' added successfully`);
|
|
73
|
+
console.log(` API URL: ${url}`);
|
|
74
|
+
console.log(` API Key: ${maskApiKey(key)}`);
|
|
75
|
+
console.log();
|
|
76
|
+
|
|
77
|
+
// Ask if user wants to switch to this profile
|
|
78
|
+
if (!options.noSwitch) {
|
|
79
|
+
const { shouldSwitch } = await inquirer.prompt([
|
|
80
|
+
{
|
|
81
|
+
type: 'confirm',
|
|
82
|
+
name: 'shouldSwitch',
|
|
83
|
+
message: 'Switch to this profile now?',
|
|
84
|
+
default: false
|
|
85
|
+
}
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
if (shouldSwitch) {
|
|
89
|
+
updateApiConfig(url, key);
|
|
90
|
+
success(`Switched to profile '${name}'`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
} catch (err) {
|
|
95
|
+
error(`Failed to add profile: ${err.message}`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import { deleteProfile, getProfile } from '../core/profiles.js';
|
|
3
|
+
import { success, error, info } from '../utils/logger.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Delete a profile
|
|
7
|
+
*/
|
|
8
|
+
export async function deleteCommand(name, options = {}) {
|
|
9
|
+
try {
|
|
10
|
+
if (!name) {
|
|
11
|
+
error('Profile name is required');
|
|
12
|
+
info('Usage: ccm delete <name>');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Check if profile exists
|
|
17
|
+
const profile = getProfile(name);
|
|
18
|
+
if (!profile) {
|
|
19
|
+
error(`Profile '${name}' does not exist`);
|
|
20
|
+
info('Use "ccm list" to see available profiles');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Confirmation prompt unless --force flag is used
|
|
25
|
+
if (!options.force) {
|
|
26
|
+
const { confirmed } = await inquirer.prompt([
|
|
27
|
+
{
|
|
28
|
+
type: 'confirm',
|
|
29
|
+
name: 'confirmed',
|
|
30
|
+
message: `Delete profile '${name}'? This cannot be undone.`,
|
|
31
|
+
default: false
|
|
32
|
+
}
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
if (!confirmed) {
|
|
36
|
+
info('Deletion cancelled');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Delete profile (this will throw if it's the current profile)
|
|
42
|
+
deleteProfile(name);
|
|
43
|
+
|
|
44
|
+
success(`Profile '${name}' deleted`);
|
|
45
|
+
|
|
46
|
+
} catch (err) {
|
|
47
|
+
error(`Failed to delete profile: ${err.message}`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { writeFileSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { readProfiles } from '../core/profiles.js';
|
|
4
|
+
import { success, error, info } from '../utils/logger.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Export profiles to a file
|
|
8
|
+
*/
|
|
9
|
+
export async function exportCommand(file, options = {}) {
|
|
10
|
+
try {
|
|
11
|
+
const profiles = readProfiles();
|
|
12
|
+
|
|
13
|
+
if (Object.keys(profiles.profiles).length === 0) {
|
|
14
|
+
info('No profiles to export');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Default file path
|
|
19
|
+
const outputFile = file || './claude-profiles-backup.json';
|
|
20
|
+
const outputPath = resolve(outputFile);
|
|
21
|
+
|
|
22
|
+
// Clone profiles to avoid modifying original
|
|
23
|
+
const exportData = JSON.parse(JSON.stringify(profiles));
|
|
24
|
+
|
|
25
|
+
// Remove API keys if --no-keys flag is set
|
|
26
|
+
if (options.noKeys) {
|
|
27
|
+
for (const name in exportData.profiles) {
|
|
28
|
+
delete exportData.profiles[name].apiKey;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Write to file
|
|
33
|
+
writeFileSync(outputPath, JSON.stringify(exportData, null, 2), 'utf-8');
|
|
34
|
+
|
|
35
|
+
const profileCount = Object.keys(profiles.profiles).length;
|
|
36
|
+
success(`Exported ${profileCount} profile${profileCount > 1 ? 's' : ''} to ${outputPath}`);
|
|
37
|
+
|
|
38
|
+
if (options.noKeys) {
|
|
39
|
+
info('API keys were excluded from export');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
} catch (err) {
|
|
43
|
+
error(`Failed to export profiles: ${err.message}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { readProfiles, writeProfiles } from '../core/profiles.js';
|
|
4
|
+
import { success, error, warning, info } from '../utils/logger.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Import profiles from a file
|
|
8
|
+
*/
|
|
9
|
+
export async function importCommand(file, options = {}) {
|
|
10
|
+
try {
|
|
11
|
+
if (!file) {
|
|
12
|
+
error('Import file path is required');
|
|
13
|
+
info('Usage: ccm import <file>');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const inputPath = resolve(file);
|
|
18
|
+
|
|
19
|
+
// Check if file exists
|
|
20
|
+
if (!existsSync(inputPath)) {
|
|
21
|
+
error(`File not found: ${inputPath}`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Read and parse import file
|
|
26
|
+
let importData;
|
|
27
|
+
try {
|
|
28
|
+
const fileContent = readFileSync(inputPath, 'utf-8');
|
|
29
|
+
importData = JSON.parse(fileContent);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
error(`Failed to parse import file: ${err.message}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Validate import data structure
|
|
36
|
+
if (!importData.profiles || typeof importData.profiles !== 'object') {
|
|
37
|
+
error('Invalid import file format: missing profiles object');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Get current profiles
|
|
42
|
+
const currentProfiles = readProfiles();
|
|
43
|
+
|
|
44
|
+
let imported = 0;
|
|
45
|
+
let skipped = 0;
|
|
46
|
+
const conflicts = [];
|
|
47
|
+
|
|
48
|
+
// Import profiles
|
|
49
|
+
for (const [name, profile] of Object.entries(importData.profiles)) {
|
|
50
|
+
// Check for conflicts
|
|
51
|
+
if (currentProfiles.profiles[name] && !options.merge) {
|
|
52
|
+
conflicts.push(name);
|
|
53
|
+
skipped++;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Validate required fields
|
|
58
|
+
if (!profile.apiUrl || !profile.apiKey) {
|
|
59
|
+
warning(`Skipping profile '${name}': missing apiUrl or apiKey`);
|
|
60
|
+
skipped++;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Add profile
|
|
65
|
+
currentProfiles.profiles[name] = {
|
|
66
|
+
name,
|
|
67
|
+
apiUrl: profile.apiUrl,
|
|
68
|
+
apiKey: profile.apiKey,
|
|
69
|
+
createdAt: profile.createdAt || new Date().toISOString(),
|
|
70
|
+
lastUsed: profile.lastUsed || null
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
imported++;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Write updated profiles
|
|
77
|
+
writeProfiles(currentProfiles);
|
|
78
|
+
|
|
79
|
+
// Report results
|
|
80
|
+
info(`Importing profiles from ${inputPath}...`);
|
|
81
|
+
console.log();
|
|
82
|
+
|
|
83
|
+
if (imported > 0) {
|
|
84
|
+
success(`${imported} profile${imported > 1 ? 's' : ''} imported`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (conflicts.length > 0) {
|
|
88
|
+
warning(`${conflicts.length} profile${conflicts.length > 1 ? 's' : ''} skipped (name conflict: ${conflicts.join(', ')})`);
|
|
89
|
+
info('Use --merge flag to overwrite existing profiles');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log();
|
|
93
|
+
info('Use "ccm list" to see all profiles');
|
|
94
|
+
|
|
95
|
+
} catch (err) {
|
|
96
|
+
error(`Failed to import profiles: ${err.message}`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { readSettings, createSettings, settingsFileExists } from '../core/config.js';
|
|
2
|
+
import { addProfile, hasProfiles, writeProfiles } from '../core/profiles.js';
|
|
3
|
+
import { success, error, warning, info, section } from '../utils/logger.js';
|
|
4
|
+
import { resolveClaudeDir, claudeDirExists } from '../utils/paths.js';
|
|
5
|
+
import { mkdirSync } from 'fs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Initialize profile management
|
|
9
|
+
*/
|
|
10
|
+
export async function initCommand() {
|
|
11
|
+
try {
|
|
12
|
+
section('Initializing Claude Config Manager...');
|
|
13
|
+
|
|
14
|
+
// Ensure .claude directory exists
|
|
15
|
+
const claudeDir = resolveClaudeDir();
|
|
16
|
+
if (!claudeDirExists()) {
|
|
17
|
+
mkdirSync(claudeDir, { recursive: true, mode: 0o700 });
|
|
18
|
+
success(`Created directory: ${claudeDir}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Check if profiles already exist
|
|
22
|
+
if (hasProfiles()) {
|
|
23
|
+
warning('Profile management is already initialized');
|
|
24
|
+
info('Use "ccm list" to see existing profiles');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check if settings.json exists
|
|
29
|
+
const settings = readSettings();
|
|
30
|
+
|
|
31
|
+
if (settings && settings.apiUrl && settings.apiKey) {
|
|
32
|
+
// Import existing settings as default profile
|
|
33
|
+
addProfile('default', settings.apiUrl, settings.apiKey);
|
|
34
|
+
success('Found existing settings.json');
|
|
35
|
+
success('Imported current config as "default" profile');
|
|
36
|
+
} else {
|
|
37
|
+
// Create empty profiles structure
|
|
38
|
+
writeProfiles({
|
|
39
|
+
version: '1.0.0',
|
|
40
|
+
currentProfile: null,
|
|
41
|
+
profiles: {}
|
|
42
|
+
});
|
|
43
|
+
success('Initialized empty profile storage');
|
|
44
|
+
info('Use "ccm add <name> <url> <key>" to create your first profile');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.log();
|
|
48
|
+
info('Profile management is ready!');
|
|
49
|
+
info('Run "ccm --help" to see available commands');
|
|
50
|
+
|
|
51
|
+
} catch (err) {
|
|
52
|
+
error(`Initialization failed: ${err.message}`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { getAllProfiles, getCurrentProfileName } from '../core/profiles.js';
|
|
2
|
+
import { section, formatProfileName, maskApiKey, dim, info } from '../utils/logger.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* List all profiles
|
|
6
|
+
*/
|
|
7
|
+
export async function listCommand() {
|
|
8
|
+
try {
|
|
9
|
+
const profiles = getAllProfiles();
|
|
10
|
+
const currentProfile = getCurrentProfileName();
|
|
11
|
+
|
|
12
|
+
if (Object.keys(profiles).length === 0) {
|
|
13
|
+
info('No profiles found. Use "ccm add <name> <url> <key>" to create one.');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
section('Available profiles:');
|
|
18
|
+
|
|
19
|
+
// Sort profiles: current first, then by last used, then alphabetically
|
|
20
|
+
const sortedEntries = Object.entries(profiles).sort(([nameA, profA], [nameB, profB]) => {
|
|
21
|
+
if (nameA === currentProfile) return -1;
|
|
22
|
+
if (nameB === currentProfile) return 1;
|
|
23
|
+
|
|
24
|
+
if (profA.lastUsed && !profB.lastUsed) return -1;
|
|
25
|
+
if (!profA.lastUsed && profB.lastUsed) return 1;
|
|
26
|
+
|
|
27
|
+
if (profA.lastUsed && profB.lastUsed) {
|
|
28
|
+
return new Date(profB.lastUsed) - new Date(profA.lastUsed);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return nameA.localeCompare(nameB);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Calculate max name length for alignment
|
|
35
|
+
const maxNameLength = Math.max(...sortedEntries.map(([name]) => name.length));
|
|
36
|
+
const padding = maxNameLength + 4;
|
|
37
|
+
|
|
38
|
+
for (const [name, profile] of sortedEntries) {
|
|
39
|
+
const isActive = name === currentProfile;
|
|
40
|
+
const displayName = formatProfileName(name, isActive);
|
|
41
|
+
const paddedName = displayName + ' '.repeat(padding - name.length);
|
|
42
|
+
const maskedKey = maskApiKey(profile.apiKey);
|
|
43
|
+
const activeTag = isActive ? '(active)' : '';
|
|
44
|
+
|
|
45
|
+
console.log(`${paddedName} ${profile.apiUrl.padEnd(40)} ${dim(maskedKey)} ${activeTag}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log();
|
|
49
|
+
info('Use "ccm use <name>" to switch profiles');
|
|
50
|
+
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.error(`Failed to list profiles: ${err.message}`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { renameProfile, getProfile } from '../core/profiles.js';
|
|
2
|
+
import { validateProfileName } from '../core/validator.js';
|
|
3
|
+
import { success, error, info } from '../utils/logger.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Rename a profile
|
|
7
|
+
*/
|
|
8
|
+
export async function renameCommand(oldName, newName) {
|
|
9
|
+
try {
|
|
10
|
+
if (!oldName || !newName) {
|
|
11
|
+
error('Both old and new profile names are required');
|
|
12
|
+
info('Usage: ccm rename <old-name> <new-name>');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Check if old profile exists
|
|
17
|
+
const profile = getProfile(oldName);
|
|
18
|
+
if (!profile) {
|
|
19
|
+
error(`Profile '${oldName}' does not exist`);
|
|
20
|
+
info('Use "ccm list" to see available profiles');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Validate new name
|
|
25
|
+
const validation = validateProfileName(newName);
|
|
26
|
+
if (!validation.valid) {
|
|
27
|
+
error(validation.error);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Rename profile
|
|
32
|
+
renameProfile(oldName, newName);
|
|
33
|
+
|
|
34
|
+
success(`Profile renamed: '${oldName}' → '${newName}'`);
|
|
35
|
+
|
|
36
|
+
} catch (err) {
|
|
37
|
+
error(`Failed to rename profile: ${err.message}`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
}
|