@tkpdx01/ccc 1.2.7 → 1.3.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 CHANGED
@@ -45,12 +45,89 @@ ccc edit [profile] # Edit profile / 编辑配置
45
45
  ccc delete [profile] # Delete profile / 删除配置
46
46
  ```
47
47
 
48
+ ### WebDAV Cloud Sync / WebDAV 云同步
49
+
50
+ ```bash
51
+ ccc webdav setup # Configure WebDAV and sync password / 配置 WebDAV 和同步密码
52
+ ccc webdav push # Push profiles to cloud / 推送到云端
53
+ ccc webdav pull # Pull profiles from cloud / 从云端拉取
54
+ ccc webdav status # View sync status / 查看同步状态
55
+ ```
56
+
48
57
  ## Features / 功能
49
58
 
50
59
  - **Multiple Profiles / 多配置**: Manage different API configurations
51
60
  - **Template Support / 模板**: Based on `~/.claude/settings.json`
52
61
  - **Smart Import / 智能导入**: Auto-detect API URL and token
53
62
  - **Sync Settings / 同步**: Update from template, preserve credentials
63
+ - **WebDAV Cloud Sync / 云同步**: Encrypted sync across devices
64
+
65
+ ## Sync Command / 同步命令
66
+
67
+ The `sync` command updates profiles with the latest settings from `~/.claude/settings.json` while preserving each profile's API credentials (`ANTHROPIC_AUTH_TOKEN` and `ANTHROPIC_BASE_URL`).
68
+
69
+ `sync` 命令从 `~/.claude/settings.json` 同步最新设置到 profiles,同时保留每个 profile 的 API 凭证(`ANTHROPIC_AUTH_TOKEN` 和 `ANTHROPIC_BASE_URL`)。
70
+
71
+ ```bash
72
+ ccc sync [profile] # Sync single profile / 同步单个配置
73
+ ccc sync --all # Sync all profiles / 同步所有配置
74
+ ```
75
+
76
+ Use this when you've updated your main Claude settings (plugins, model, etc.) and want to apply those changes to all profiles.
77
+
78
+ 当你更新了主 Claude 设置(插件、模型等)并想将这些更改应用到所有 profiles 时使用此命令。
79
+
80
+ ## WebDAV Cloud Sync / WebDAV 云同步
81
+
82
+ Sync your profiles across multiple devices using any WebDAV service (Nutstore, Nextcloud, etc.).
83
+
84
+ 使用任意 WebDAV 服务(坚果云、Nextcloud 等)在多设备间同步配置。
85
+
86
+ ### Setup / 配置
87
+
88
+ ```bash
89
+ ccc webdav setup
90
+ ```
91
+
92
+ This will prompt for:
93
+ - WebDAV server URL
94
+ - Username / Password
95
+ - Remote storage path
96
+ - Sync password (for encryption)
97
+
98
+ ### Commands / 命令
99
+
100
+ ```bash
101
+ ccc webdav push # Upload encrypted profiles / 上传加密配置
102
+ ccc webdav pull # Download and decrypt / 下载并解密
103
+ ccc webdav status # View sync status / 查看同步状态
104
+ ccc webdav push -f # Force push (skip conflict prompts) / 强制推送
105
+ ccc webdav pull -f # Force pull (skip conflict prompts) / 强制拉取
106
+ ```
107
+
108
+ ### Security / 安全设计
109
+
110
+ - **End-to-end encryption**: All data is encrypted locally with AES-256-GCM before upload. Even if someone gains access to your WebDAV storage, they cannot read your API keys without the sync password.
111
+
112
+ - **Password-based protection**: Your sync password is never transmitted. It derives the encryption key using PBKDF2 (100,000 iterations).
113
+
114
+ - **Local password caching**: On trusted devices, the password is cached locally (encrypted with machine fingerprint), so you don't need to enter it every time.
115
+
116
+ - **Manual sync only**: Synchronization only happens when you explicitly run `push` or `pull`. No background processes, no automatic uploads. You always know when your data leaves your machine.
117
+
118
+ - **Non-destructive merge**: By default, conflicts preserve both versions instead of overwriting. Use `--force` only when you're certain.
119
+
120
+ **安全设计**:
121
+
122
+ - **端到端加密**:所有数据在上传前使用 AES-256-GCM 本地加密。即使他人获取了你的 WebDAV 存储访问权限,没有同步密码也无法读取你的 API Key。
123
+
124
+ - **密码保护**:同步密码永不传输,使用 PBKDF2(10万次迭代)派生加密密钥。
125
+
126
+ - **本机免密**:在可信设备上,密码使用机器指纹加密缓存在本地,无需每次输入。
127
+
128
+ - **手动同步**:同步仅在你显式执行 `push` 或 `pull` 时发生。无后台进程,无自动上传。你始终清楚数据何时离开本机。
129
+
130
+ - **无损合并**:默认情况下,冲突时保留两个版本而非覆盖。仅在确定时使用 `--force`。
54
131
 
55
132
  ## Storage / 存储
56
133
 
package/index.js CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  editCommand,
18
18
  deleteCommand,
19
19
  syncCommand,
20
+ webdavCommand,
20
21
  helpCommand
21
22
  } from './src/commands/index.js';
22
23
 
@@ -26,7 +27,7 @@ const program = new Command();
26
27
  program
27
28
  .name('ccc')
28
29
  .description('Claude Code Settings Launcher - 管理多个 Claude Code 配置文件')
29
- .version('1.2.7');
30
+ .version('1.3.1');
30
31
 
31
32
  // 注册所有命令
32
33
  listCommand(program);
@@ -37,6 +38,7 @@ newCommand(program);
37
38
  editCommand(program);
38
39
  deleteCommand(program);
39
40
  syncCommand(program);
41
+ webdavCommand(program);
40
42
  helpCommand(program);
41
43
 
42
44
  // ccc <profile> 或 ccc (无参数)
@@ -49,7 +51,7 @@ program
49
51
 
50
52
  if (profile) {
51
53
  // 检查是否是子命令
52
- if (['list', 'ls', 'use', 'show', 'import', 'if', 'new', 'edit', 'delete', 'rm', 'sync', 'help'].includes(profile)) {
54
+ if (['list', 'ls', 'use', 'show', 'import', 'if', 'new', 'edit', 'delete', 'rm', 'sync', 'webdav', 'help'].includes(profile)) {
53
55
  return; // 让子命令处理
54
56
  }
55
57
 
package/package.json CHANGED
@@ -1,39 +1,40 @@
1
- {
2
- "name": "@tkpdx01/ccc",
3
- "version": "1.2.7",
4
- "description": "Claude Code Settings Launcher - Manage multiple Claude Code profiles",
5
- "main": "index.js",
6
- "bin": {
7
- "ccc": "./index.js"
8
- },
9
- "type": "module",
10
- "scripts": {
11
- "test": "node index.js --help",
12
- "postinstall": "node postinstall.js"
13
- },
14
- "keywords": [
15
- "claude",
16
- "claude-code",
17
- "cli",
18
- "settings",
19
- "launcher",
20
- "profile",
21
- "anthropic"
22
- ],
23
- "author": "tkpdx01",
24
- "license": "MIT",
25
- "repository": {
26
- "type": "git",
27
- "url": "git+https://github.com/tkpdx01/claude-code-launcher.git"
28
- },
29
- "bugs": {
30
- "url": "https://github.com/tkpdx01/claude-code-launcher/issues"
31
- },
32
- "homepage": "https://github.com/tkpdx01/claude-code-launcher#readme",
33
- "dependencies": {
34
- "chalk": "^5.3.0",
35
- "cli-table3": "^0.6.5",
36
- "commander": "^12.0.0",
37
- "inquirer": "^9.2.0"
38
- }
39
- }
1
+ {
2
+ "name": "@tkpdx01/ccc",
3
+ "version": "1.3.1",
4
+ "description": "Claude Code Settings Launcher - Manage multiple Claude Code profiles",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "ccc": "./index.js"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "test": "node index.js --help",
12
+ "postinstall": "node postinstall.js"
13
+ },
14
+ "keywords": [
15
+ "claude",
16
+ "claude-code",
17
+ "cli",
18
+ "settings",
19
+ "launcher",
20
+ "profile",
21
+ "anthropic"
22
+ ],
23
+ "author": "tkpdx01",
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/tkpdx01/claude-code-launcher.git"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/tkpdx01/claude-code-launcher/issues"
31
+ },
32
+ "homepage": "https://github.com/tkpdx01/claude-code-launcher#readme",
33
+ "dependencies": {
34
+ "chalk": "^5.3.0",
35
+ "cli-table3": "^0.6.5",
36
+ "commander": "^12.0.0",
37
+ "inquirer": "^9.2.0",
38
+ "webdav": "^5.8.0"
39
+ }
40
+ }
@@ -1,66 +1,66 @@
1
- import chalk from 'chalk';
2
- import inquirer from 'inquirer';
3
- import {
4
- getProfiles,
5
- getDefaultProfile,
6
- profileExists,
7
- deleteProfile,
8
- clearDefaultProfile
9
- } from '../profiles.js';
10
-
11
- export function deleteCommand(program) {
12
- program
13
- .command('delete [profile]')
14
- .alias('rm')
15
- .description('删除 profile')
16
- .action(async (profile) => {
17
- const profiles = getProfiles();
18
-
19
- if (profiles.length === 0) {
20
- console.log(chalk.yellow('没有可用的 profiles'));
21
- process.exit(0);
22
- }
23
-
24
- // 如果没有指定 profile,交互选择
25
- if (!profile) {
26
- const { selectedProfile } = await inquirer.prompt([
27
- {
28
- type: 'list',
29
- name: 'selectedProfile',
30
- message: '选择要删除的配置:',
31
- choices: profiles
32
- }
33
- ]);
34
- profile = selectedProfile;
35
- }
36
-
37
- if (!profileExists(profile)) {
38
- console.log(chalk.red(`Profile "${profile}" 不存在`));
39
- process.exit(1);
40
- }
41
-
42
- const { confirm } = await inquirer.prompt([
43
- {
44
- type: 'confirm',
45
- name: 'confirm',
46
- message: `确定要删除 "${profile}" 吗?`,
47
- default: false
48
- }
49
- ]);
50
-
51
- if (!confirm) {
52
- console.log(chalk.yellow('已取消'));
53
- process.exit(0);
54
- }
55
-
56
- deleteProfile(profile);
57
-
58
- // 如果删除的是默认 profile,清除默认设置
59
- if (getDefaultProfile() === profile) {
60
- clearDefaultProfile();
61
- }
62
-
63
- console.log(chalk.green(`✓ Profile "${profile}" 已删除`));
64
- });
65
- }
66
-
1
+ import chalk from 'chalk';
2
+ import inquirer from 'inquirer';
3
+ import {
4
+ getProfiles,
5
+ getDefaultProfile,
6
+ profileExists,
7
+ deleteProfile,
8
+ clearDefaultProfile
9
+ } from '../profiles.js';
10
+
11
+ export function deleteCommand(program) {
12
+ program
13
+ .command('delete [profile]')
14
+ .alias('rm')
15
+ .description('删除 profile')
16
+ .action(async (profile) => {
17
+ const profiles = getProfiles();
18
+
19
+ if (profiles.length === 0) {
20
+ console.log(chalk.yellow('没有可用的 profiles'));
21
+ process.exit(0);
22
+ }
23
+
24
+ // 如果没有指定 profile,交互选择
25
+ if (!profile) {
26
+ const { selectedProfile } = await inquirer.prompt([
27
+ {
28
+ type: 'list',
29
+ name: 'selectedProfile',
30
+ message: '选择要删除的配置:',
31
+ choices: profiles
32
+ }
33
+ ]);
34
+ profile = selectedProfile;
35
+ }
36
+
37
+ if (!profileExists(profile)) {
38
+ console.log(chalk.red(`Profile "${profile}" 不存在`));
39
+ process.exit(1);
40
+ }
41
+
42
+ const { confirm } = await inquirer.prompt([
43
+ {
44
+ type: 'confirm',
45
+ name: 'confirm',
46
+ message: `确定要删除 "${profile}" 吗?`,
47
+ default: false
48
+ }
49
+ ]);
50
+
51
+ if (!confirm) {
52
+ console.log(chalk.yellow('已取消'));
53
+ process.exit(0);
54
+ }
55
+
56
+ deleteProfile(profile);
57
+
58
+ // 如果删除的是默认 profile,清除默认设置
59
+ if (getDefaultProfile() === profile) {
60
+ clearDefaultProfile();
61
+ }
62
+
63
+ console.log(chalk.green(`✓ Profile "${profile}" 已删除`));
64
+ });
65
+ }
66
+
@@ -1,120 +1,120 @@
1
- import fs from 'fs';
2
- import chalk from 'chalk';
3
- import inquirer from 'inquirer';
4
- import {
5
- getProfiles,
6
- getDefaultProfile,
7
- profileExists,
8
- getProfilePath,
9
- readProfile,
10
- saveProfile,
11
- setDefaultProfile,
12
- deleteProfile,
13
- resolveProfile,
14
- getProfileCredentials,
15
- getClaudeSettingsTemplate
16
- } from '../profiles.js';
17
-
18
- export function editCommand(program) {
19
- program
20
- .command('edit [profile]')
21
- .description('编辑 profile 配置')
22
- .action(async (profile) => {
23
- const profiles = getProfiles();
24
-
25
- if (profiles.length === 0) {
26
- console.log(chalk.yellow('没有可用的 profiles'));
27
- console.log(chalk.gray('使用 "ccc import" 导入配置'));
28
- process.exit(0);
29
- }
30
-
31
- // 如果没有指定 profile,交互选择
32
- if (!profile) {
33
- const { selectedProfile } = await inquirer.prompt([
34
- {
35
- type: 'list',
36
- name: 'selectedProfile',
37
- message: '选择要编辑的配置:',
38
- choices: profiles
39
- }
40
- ]);
41
- profile = selectedProfile;
42
- } else {
43
- // 支持序号或名称
44
- const resolved = resolveProfile(profile);
45
- if (!resolved) {
46
- console.log(chalk.red(`Profile "${profile}" 不存在`));
47
- process.exit(1);
48
- }
49
- profile = resolved;
50
- }
51
-
52
- // 使用新的 getProfileCredentials 函数获取凭证(支持新旧格式)
53
- const { apiKey: currentApiKey, apiUrl: currentApiUrl } = getProfileCredentials(profile);
54
-
55
- console.log(chalk.cyan(`\n当前配置 (${profile}):`));
56
- console.log(chalk.gray(` ANTHROPIC_BASE_URL: ${currentApiUrl || '未设置'}`));
57
- console.log(chalk.gray(` ANTHROPIC_AUTH_TOKEN: ${currentApiKey ? currentApiKey.substring(0, 10) + '...' : '未设置'}`));
58
- console.log();
59
-
60
- const { apiUrl, apiKey, newName } = await inquirer.prompt([
61
- {
62
- type: 'input',
63
- name: 'apiUrl',
64
- message: 'ANTHROPIC_BASE_URL:',
65
- default: currentApiUrl || ''
66
- },
67
- {
68
- type: 'input',
69
- name: 'apiKey',
70
- message: 'ANTHROPIC_AUTH_TOKEN:',
71
- default: currentApiKey || ''
72
- },
73
- {
74
- type: 'input',
75
- name: 'newName',
76
- message: 'Profile 名称:',
77
- default: profile
78
- }
79
- ]);
80
-
81
- // 读取当前 profile 或使用主配置模板
82
- let currentProfile = readProfile(profile);
83
- if (!currentProfile || !currentProfile.env) {
84
- // 如果是旧格式或空配置,基于主配置模板创建
85
- const template = getClaudeSettingsTemplate() || {};
86
- currentProfile = { ...template };
87
- }
88
-
89
- // 确保 env 对象存在
90
- if (!currentProfile.env) {
91
- currentProfile.env = {};
92
- }
93
-
94
- // 更新 env 中的 API 凭证
95
- currentProfile.env.ANTHROPIC_AUTH_TOKEN = apiKey;
96
- currentProfile.env.ANTHROPIC_BASE_URL = apiUrl;
97
-
98
- // 如果重命名
99
- if (newName && newName !== profile) {
100
- const newPath = getProfilePath(newName);
101
- if (fs.existsSync(newPath)) {
102
- console.log(chalk.red(`Profile "${newName}" 已存在`));
103
- process.exit(1);
104
- }
105
- saveProfile(newName, currentProfile);
106
- deleteProfile(profile);
107
-
108
- // 更新默认 profile
109
- if (getDefaultProfile() === profile) {
110
- setDefaultProfile(newName);
111
- }
112
-
113
- console.log(chalk.green(`\n✓ Profile 已重命名为 "${newName}" 并保存`));
114
- } else {
115
- saveProfile(profile, currentProfile);
116
- console.log(chalk.green(`\n✓ Profile "${profile}" 已更新`));
117
- }
118
- });
119
- }
120
-
1
+ import fs from 'fs';
2
+ import chalk from 'chalk';
3
+ import inquirer from 'inquirer';
4
+ import {
5
+ getProfiles,
6
+ getDefaultProfile,
7
+ profileExists,
8
+ getProfilePath,
9
+ readProfile,
10
+ saveProfile,
11
+ setDefaultProfile,
12
+ deleteProfile,
13
+ resolveProfile,
14
+ getProfileCredentials,
15
+ getClaudeSettingsTemplate
16
+ } from '../profiles.js';
17
+
18
+ export function editCommand(program) {
19
+ program
20
+ .command('edit [profile]')
21
+ .description('编辑 profile 配置')
22
+ .action(async (profile) => {
23
+ const profiles = getProfiles();
24
+
25
+ if (profiles.length === 0) {
26
+ console.log(chalk.yellow('没有可用的 profiles'));
27
+ console.log(chalk.gray('使用 "ccc import" 导入配置'));
28
+ process.exit(0);
29
+ }
30
+
31
+ // 如果没有指定 profile,交互选择
32
+ if (!profile) {
33
+ const { selectedProfile } = await inquirer.prompt([
34
+ {
35
+ type: 'list',
36
+ name: 'selectedProfile',
37
+ message: '选择要编辑的配置:',
38
+ choices: profiles
39
+ }
40
+ ]);
41
+ profile = selectedProfile;
42
+ } else {
43
+ // 支持序号或名称
44
+ const resolved = resolveProfile(profile);
45
+ if (!resolved) {
46
+ console.log(chalk.red(`Profile "${profile}" 不存在`));
47
+ process.exit(1);
48
+ }
49
+ profile = resolved;
50
+ }
51
+
52
+ // 使用新的 getProfileCredentials 函数获取凭证(支持新旧格式)
53
+ const { apiKey: currentApiKey, apiUrl: currentApiUrl } = getProfileCredentials(profile);
54
+
55
+ console.log(chalk.cyan(`\n当前配置 (${profile}):`));
56
+ console.log(chalk.gray(` ANTHROPIC_BASE_URL: ${currentApiUrl || '未设置'}`));
57
+ console.log(chalk.gray(` ANTHROPIC_AUTH_TOKEN: ${currentApiKey ? currentApiKey.substring(0, 10) + '...' : '未设置'}`));
58
+ console.log();
59
+
60
+ const { apiUrl, apiKey, newName } = await inquirer.prompt([
61
+ {
62
+ type: 'input',
63
+ name: 'apiUrl',
64
+ message: 'ANTHROPIC_BASE_URL:',
65
+ default: currentApiUrl || ''
66
+ },
67
+ {
68
+ type: 'input',
69
+ name: 'apiKey',
70
+ message: 'ANTHROPIC_AUTH_TOKEN:',
71
+ default: currentApiKey || ''
72
+ },
73
+ {
74
+ type: 'input',
75
+ name: 'newName',
76
+ message: 'Profile 名称:',
77
+ default: profile
78
+ }
79
+ ]);
80
+
81
+ // 读取当前 profile 或使用主配置模板
82
+ let currentProfile = readProfile(profile);
83
+ if (!currentProfile || !currentProfile.env) {
84
+ // 如果是旧格式或空配置,基于主配置模板创建
85
+ const template = getClaudeSettingsTemplate() || {};
86
+ currentProfile = { ...template };
87
+ }
88
+
89
+ // 确保 env 对象存在
90
+ if (!currentProfile.env) {
91
+ currentProfile.env = {};
92
+ }
93
+
94
+ // 更新 env 中的 API 凭证
95
+ currentProfile.env.ANTHROPIC_AUTH_TOKEN = apiKey;
96
+ currentProfile.env.ANTHROPIC_BASE_URL = apiUrl;
97
+
98
+ // 如果重命名
99
+ if (newName && newName !== profile) {
100
+ const newPath = getProfilePath(newName);
101
+ if (fs.existsSync(newPath)) {
102
+ console.log(chalk.red(`Profile "${newName}" 已存在`));
103
+ process.exit(1);
104
+ }
105
+ saveProfile(newName, currentProfile);
106
+ deleteProfile(profile);
107
+
108
+ // 更新默认 profile
109
+ if (getDefaultProfile() === profile) {
110
+ setDefaultProfile(newName);
111
+ }
112
+
113
+ console.log(chalk.green(`\n✓ Profile 已重命名为 "${newName}" 并保存`));
114
+ } else {
115
+ saveProfile(profile, currentProfile);
116
+ console.log(chalk.green(`\n✓ Profile "${profile}" 已更新`));
117
+ }
118
+ });
119
+ }
120
+