@daylenjeez/ccm-switch 1.2.2 → 1.2.4

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
@@ -52,17 +52,17 @@ ccm add # Interactive wizard to add a provider
52
52
 
53
53
  ## 🔌 cc-switch Integration
54
54
 
55
- Already using [cc-switch](https://github.com/farion1231/cc-switch)? ccm reads its SQLite database directly:
55
+ Already using [cc-switch](https://github.com/farion1231/cc-switch)? ccm can sync its configurations into standalone storage:
56
56
 
57
57
  ```bash
58
58
  $ ccm init
59
59
  cc-switch detected. Import configurations from it? (Y/n)
60
- ✓ Initialized in cc-switch mode
61
- Imported 4 configurations
60
+ ✓ Initialized
61
+ Synced 4 configurations
62
62
  Active: OpenRouter
63
63
  ```
64
64
 
65
- All configs sync both ways add in ccm, see it in cc-switch UI, and vice versa.
65
+ You can also run `ccm sync` at any time to pull the latest cc-switch configurations into `~/.ccm/config.json`.
66
66
 
67
67
  ## ➕ Adding Configurations
68
68
 
@@ -132,7 +132,9 @@ Aliases are stored in `~/.ccm/rc.json`:
132
132
  | `ccm modify [name]` | `edit` | Edit existing configuration |
133
133
  | `ccm remove [name]` | `rm` | Interactive or named delete |
134
134
  | `ccm current` | | Show active configuration |
135
- | `ccm config` | | Switch storage mode |
135
+ | `ccm config` | | View data source mode (deprecated) |
136
+ | `ccm sync` | | Sync cc-switch configs into standalone |
137
+ | `ccm clear` | | Clean up data files |
136
138
 
137
139
  ### Aliases
138
140
 
@@ -190,6 +192,22 @@ Claude Code reads `~/.claude/settings.json` on startup. The `env` field controls
190
192
 
191
193
  `ccm use` writes the selected profile into `settings.json` while preserving personal settings (`language`, `permissions`, etc.). Restart Claude Code to apply.
192
194
 
195
+ ## 🗑️ Uninstall
196
+
197
+ Removing `ccm` only removes the CLI itself. Your data files are left behind.
198
+
199
+ Use `ccm clear` to delete them automatically, or clean them up manually:
200
+
201
+ - `~/.ccm/rc.json` — aliases and locale
202
+ - `~/.ccm/config.json` — profiles
203
+ - `~/.claude/settings.json` — may still contain an active `env` profile written by ccm
204
+
205
+ Then remove the CLI:
206
+
207
+ ```bash
208
+ npm uninstall -g @daylenjeez/ccm-switch
209
+ ```
210
+
193
211
  ## 📄 License
194
212
 
195
213
  [MIT](./LICENSE)
package/README.zh-CN.md CHANGED
@@ -10,7 +10,7 @@
10
10
  [![license](https://img.shields.io/npm/l/@daylenjeez/ccm-switch.svg?style=flat-square)](https://github.com/daylenjeez/ccm-switch/blob/main/LICENSE)
11
11
  [![node](https://img.shields.io/badge/node-%3E%3D18-brightgreen?style=flat-square)](https://nodejs.org)
12
12
 
13
- English | [中文文档](https://github.com/daylenjeez/ccm-switch/blob/main/README.zh-CN.md)
13
+ [English](https://github.com/daylenjeez/ccm-switch/blob/main/README.md) | 中文文档
14
14
 
15
15
  [安装](#-安装) · [快速开始](#-快速开始) · [命令一览](#-命令一览) · [工作原理](#%EF%B8%8F-工作原理)
16
16
 
@@ -20,12 +20,12 @@ English | [中文文档](https://github.com/daylenjeez/ccm-switch/blob/main/READ
20
20
 
21
21
  ## ✨ 亮点
22
22
 
23
- - 🔌 **cc-switch 无缝对接** — 直接读取 [cc-switch](https://github.com/farion1231/cc-switch) 数据库,无需迁移
24
- - 🧙 **交互式向导** — `ccm add` 逐步引导,输入 `<` 可返回上一步
25
- - ⚡ **一键切换** — `ccm use OpenRouter` 或 `ccm ls` 方向键选择
26
- - 🛡️ **安全切换** — 自动保留 `language`、`permissions` 等个人设置
27
- - 🚀 **零配置上手** — 直接 `ccm init`,跟着提示走,无需阅读文档
28
- - 🌍 **中英双语** — `ccm locale set zh/en` 切换界面语言
23
+ - 🔌 **cc-switch Integration** — 直接读取 `cc-switch` 数据库,无需迁移
24
+ - 🧙 **Interactive Wizard** — `ccm add` 逐步引导,输入 `<` 可返回上一步
25
+ - ⚡ **One-command Switch** — `ccm use OpenRouter` 或 `ccm ls` 方向键选择
26
+ - 🛡️ **Safe Switching** — 自动保留 `language`、`permissions` 等个人设置
27
+ - 🚀 **Zero Config** — 直接 `ccm init`,跟着提示走,无需阅读文档
28
+ - 🌍 **i18n** — `ccm locale set zh/en` 切换界面语言
29
29
 
30
30
  ## 📦 安装
31
31
 
@@ -52,17 +52,17 @@ ccm add # 交互式向导添加供应商
52
52
 
53
53
  ## 🔌 cc-switch 集成
54
54
 
55
- 已经在用 [cc-switch](https://github.com/farion1231/cc-switch)?ccm 直接读取它的 SQLite 数据库:
55
+ 已经在用 [cc-switch](https://github.com/farion1231/cc-switch)?ccm 可以将它的配置同步到本地:
56
56
 
57
57
  ```bash
58
58
  $ ccm init
59
59
  检测到 cc-switch 已安装,是否从中导入配置?(Y/n)
60
- 已初始化为 cc-switch 模式
61
- 已导入 4 个配置
60
+ 初始化完成
61
+ 已同步 4 个配置
62
62
  当前激活: OpenRouter
63
63
  ```
64
64
 
65
- 双向同步 ccm 中添加的配置在 cc-switch UI 中也能看到,反之亦然。
65
+ 你也可以随时运行 `ccm sync` 将最新的 cc-switch 配置同步到 `~/.ccm/config.json`。
66
66
 
67
67
  ## ➕ 添加配置
68
68
 
@@ -132,7 +132,9 @@ ANTHROPIC_DEFAULT_HAIKU_MODEL (可选):
132
132
  | `ccm modify [name]` | `edit` | 修改已有配置 |
133
133
  | `ccm remove [name]` | `rm` | 交互式或指定名称删除 |
134
134
  | `ccm current` | | 显示当前激活配置 |
135
- | `ccm config` | | 切换存储模式 |
135
+ | `ccm config` | | 查看数据源模式(已废弃) |
136
+ | `ccm sync` | | 从 cc-switch 同步配置到本地 |
137
+ | `ccm clear` | | 清理数据文件 |
136
138
 
137
139
  ### 别名管理
138
140
 
@@ -190,6 +192,22 @@ Claude Code 启动时读取 `~/.claude/settings.json`,`env` 字段控制 API
190
192
 
191
193
  `ccm use` 将选中配置写入 `settings.json`,同时保留 `language`、`permissions` 等个人设置。重启 Claude Code 后生效。
192
194
 
195
+ ## 🗑️ 卸载
196
+
197
+ 卸载 `ccm` 只会删除 CLI 程序本身,相关的数据文件仍然保留。
198
+
199
+ 你可以先运行 `ccm clear` 自动清理数据文件,也可以手动删除:
200
+
201
+ - `~/.ccm/rc.json` — 别名和语言设置
202
+ - `~/.ccm/config.json` — 配置方案
203
+ - `~/.claude/settings.json` — 可能仍包含 ccm 写入的 `env` 配置
204
+
205
+ 然后移除 CLI:
206
+
207
+ ```bash
208
+ npm uninstall -g @daylenjeez/ccm-switch
209
+ ```
210
+
193
211
  ## 📄 License
194
212
 
195
213
  [MIT](./LICENSE)
package/dist/i18n/en.js CHANGED
@@ -17,24 +17,15 @@ const en = {
17
17
  // init
18
18
  "init.description": "Initialize ccm",
19
19
  "init.cc_switch_found": "cc-switch detected. Import configurations from it? (Y/n) ",
20
- "init.imported": "✓ Imported {count} configurations",
21
- "init.current": "Active: {name}",
22
- "init.no_current": "No active configuration",
23
- "init.done_cc_switch": "✓ Initialized in cc-switch mode",
24
- "init.done_standalone": "✓ Initialized in standalone mode",
20
+ "init.done": "✓ Initialized",
25
21
  // config
26
- "config.description": "View or switch data source mode",
27
- "config.current_mode": "Current mode: {mode}",
28
- "config.switch_confirm": "Switch mode? (y/N) ",
29
- "config.cc_switch_not_installed": "cc-switch is not installed",
30
- "config.switched": "✓ Switched to {mode} mode",
22
+ "config.description": "View data source mode (deprecated)",
31
23
  // list
32
24
  "list.description": "List and select configurations",
33
25
  "list.empty": "No configurations yet. Use ccm save <name> to save current config",
34
26
  "list.header": "Available configurations:",
35
27
  "list.select": "Select configuration:",
36
28
  "list.current_marker": "(current)",
37
- "list.cancelled": "Cancelled",
38
29
  "list.choose_number": "Enter number to switch (Enter to skip): ",
39
30
  // current
40
31
  "current.description": "Show the currently active configuration",
@@ -118,6 +109,19 @@ const en = {
118
109
  "alias.rm_alias": "Alias {name}",
119
110
  "alias.rm_config": "Config {target}",
120
111
  "alias.rm_choose": "Choose (1/2): ",
112
+ // sync
113
+ "sync.description": "Sync configurations from cc-switch",
114
+ "sync.no_cc_switch": "cc-switch database not detected",
115
+ "sync.empty": "No Claude configurations found in cc-switch",
116
+ "sync.done": "✓ Synced {count} configurations",
117
+ "sync.current": "Active: {name}",
118
+ "sync.no_current": "No active configuration",
119
+ // clear
120
+ "clear.description": "Clean up ccm data files",
121
+ "clear.confirm": "Delete all ccm data files? (y/N) ",
122
+ "clear.cancelled": "Cancelled",
123
+ "clear.removed": "✓ Deleted {path}",
124
+ "clear.done": "✓ Cleanup complete",
121
125
  // store errors
122
126
  "store.db_not_found": "cc-switch database not found: {path}",
123
127
  };
package/dist/i18n/zh.d.ts CHANGED
@@ -12,22 +12,13 @@ declare const zh: {
12
12
  readonly "suggest.use_list": "使用 ccm list 查看所有可用配置";
13
13
  readonly "init.description": "初始化 ccm";
14
14
  readonly "init.cc_switch_found": "检测到 cc-switch 已安装,是否从中导入配置?(Y/n) ";
15
- readonly "init.imported": "✓ 已导入 {count} 个配置";
16
- readonly "init.current": "当前激活: {name}";
17
- readonly "init.no_current": "当前无激活配置";
18
- readonly "init.done_cc_switch": "✓ 已初始化为 cc-switch 模式";
19
- readonly "init.done_standalone": "✓ 已初始化为独立模式";
20
- readonly "config.description": "查看或切换数据源模式";
21
- readonly "config.current_mode": "当前模式: {mode}";
22
- readonly "config.switch_confirm": "是否切换模式?(y/N) ";
23
- readonly "config.cc_switch_not_installed": "cc-switch 未安装";
24
- readonly "config.switched": "✓ 已切换为 {mode} 模式";
15
+ readonly "init.done": "✓ 初始化完成";
16
+ readonly "config.description": "查看数据源模式(已废弃)";
25
17
  readonly "list.description": "列出并选择配置方案";
26
18
  readonly "list.empty": "暂无配置方案。使用 ccm save <name> 保存当前配置";
27
19
  readonly "list.header": "可用配置:";
28
20
  readonly "list.select": "选择配置:";
29
21
  readonly "list.current_marker": "(当前)";
30
- readonly "list.cancelled": "已取消";
31
22
  readonly "list.choose_number": "输入序号切换 (回车跳过): ";
32
23
  readonly "current.description": "显示当前生效的配置";
33
24
  readonly "current.none": "当前无激活配置";
@@ -101,6 +92,17 @@ declare const zh: {
101
92
  readonly "alias.rm_alias": "别名 {name}";
102
93
  readonly "alias.rm_config": "配置 {target}";
103
94
  readonly "alias.rm_choose": "请选择 (1/2): ";
95
+ readonly "sync.description": "从 cc-switch 同步配置";
96
+ readonly "sync.no_cc_switch": "未检测到 cc-switch 数据库";
97
+ readonly "sync.empty": "cc-switch 中没有找到 Claude 配置";
98
+ readonly "sync.done": "✓ 已同步 {count} 个配置";
99
+ readonly "sync.current": "当前激活: {name}";
100
+ readonly "sync.no_current": "当前无激活配置";
101
+ readonly "clear.description": "清理 ccm 数据文件";
102
+ readonly "clear.confirm": "确认删除所有 ccm 数据文件?(y/N) ";
103
+ readonly "clear.cancelled": "已取消清理";
104
+ readonly "clear.removed": "✓ 已删除 {path}";
105
+ readonly "clear.done": "✓ 清理完成";
104
106
  readonly "store.db_not_found": "cc-switch 数据库不存在: {path}";
105
107
  };
106
108
  export type TranslationKey = keyof typeof zh;
package/dist/i18n/zh.js CHANGED
@@ -17,24 +17,15 @@ const zh = {
17
17
  // init
18
18
  "init.description": "初始化 ccm",
19
19
  "init.cc_switch_found": "检测到 cc-switch 已安装,是否从中导入配置?(Y/n) ",
20
- "init.imported": "✓ 已导入 {count} 个配置",
21
- "init.current": "当前激活: {name}",
22
- "init.no_current": "当前无激活配置",
23
- "init.done_cc_switch": "✓ 已初始化为 cc-switch 模式",
24
- "init.done_standalone": "✓ 已初始化为独立模式",
20
+ "init.done": "✓ 初始化完成",
25
21
  // config
26
- "config.description": "查看或切换数据源模式",
27
- "config.current_mode": "当前模式: {mode}",
28
- "config.switch_confirm": "是否切换模式?(y/N) ",
29
- "config.cc_switch_not_installed": "cc-switch 未安装",
30
- "config.switched": "✓ 已切换为 {mode} 模式",
22
+ "config.description": "查看数据源模式(已废弃)",
31
23
  // list
32
24
  "list.description": "列出并选择配置方案",
33
25
  "list.empty": "暂无配置方案。使用 ccm save <name> 保存当前配置",
34
26
  "list.header": "可用配置:",
35
27
  "list.select": "选择配置:",
36
28
  "list.current_marker": "(当前)",
37
- "list.cancelled": "已取消",
38
29
  "list.choose_number": "输入序号切换 (回车跳过): ",
39
30
  // current
40
31
  "current.description": "显示当前生效的配置",
@@ -118,6 +109,19 @@ const zh = {
118
109
  "alias.rm_alias": "别名 {name}",
119
110
  "alias.rm_config": "配置 {target}",
120
111
  "alias.rm_choose": "请选择 (1/2): ",
112
+ // sync
113
+ "sync.description": "从 cc-switch 同步配置",
114
+ "sync.no_cc_switch": "未检测到 cc-switch 数据库",
115
+ "sync.empty": "cc-switch 中没有找到 Claude 配置",
116
+ "sync.done": "✓ 已同步 {count} 个配置",
117
+ "sync.current": "当前激活: {name}",
118
+ "sync.no_current": "当前无激活配置",
119
+ // clear
120
+ "clear.description": "清理 ccm 数据文件",
121
+ "clear.confirm": "确认删除所有 ccm 数据文件?(y/N) ",
122
+ "clear.cancelled": "已取消清理",
123
+ "clear.removed": "✓ 已删除 {path}",
124
+ "clear.done": "✓ 清理完成",
121
125
  // store errors
122
126
  "store.db_not_found": "cc-switch 数据库不存在: {path}",
123
127
  };
package/dist/index.js CHANGED
@@ -6,8 +6,8 @@ import { ccSwitchExists } from "./store/cc-switch.js";
6
6
  import { readClaudeSettings, applyProfile } from "./claude.js";
7
7
  import { createInterface } from "readline";
8
8
  import { spawnSync } from "child_process";
9
- import { writeFileSync, readFileSync, unlinkSync } from "fs";
10
- import { tmpdir } from "os";
9
+ import { writeFileSync, readFileSync, unlinkSync, existsSync } from "fs";
10
+ import { tmpdir, homedir } from "os";
11
11
  import { join } from "path";
12
12
  import { t, setLocale } from "./i18n/index.js";
13
13
  import * as clack from "@clack/prompts";
@@ -26,14 +26,9 @@ function ask(question) {
26
26
  });
27
27
  });
28
28
  }
29
- // Helper: ensure initialized
29
+ // Helper: ensure store ready
30
30
  function ensureStore() {
31
- const store = getStore();
32
- if (!store) {
33
- console.log(chalk.yellow(t("common.not_init")));
34
- process.exit(1);
35
- }
36
- return store;
31
+ return getStore();
37
32
  }
38
33
  // Helper: format env for display
39
34
  function formatEnv(env) {
@@ -154,49 +149,98 @@ program
154
149
  .command("init")
155
150
  .description(t("init.description"))
156
151
  .action(async () => {
152
+ const rc = readRc();
153
+ writeRc({ aliases: rc?.aliases, locale: rc?.locale });
154
+ console.log(chalk.green(t("init.done")));
157
155
  if (ccSwitchExists()) {
158
156
  const use = await ask(t("init.cc_switch_found"));
159
157
  if (use.toLowerCase() !== "n") {
160
- writeRc({ mode: "cc-switch" });
161
158
  const { CcSwitchStore } = await import("./store/cc-switch.js");
162
- const store = new CcSwitchStore();
163
- const profiles = store.list();
164
- const current = store.getCurrent();
165
- console.log(chalk.green(t("init.done_cc_switch")));
166
- console.log(chalk.green(t("init.imported", { count: String(profiles.length) })));
159
+ const { StandaloneStore } = await import("./store/standalone.js");
160
+ const ccStore = new CcSwitchStore();
161
+ const standaloneStore = new StandaloneStore();
162
+ const profiles = ccStore.list();
163
+ const current = ccStore.getCurrent();
164
+ for (const profile of profiles) {
165
+ standaloneStore.save(profile.name, profile.settingsConfig);
166
+ }
167
+ if (current) {
168
+ standaloneStore.setCurrent(current);
169
+ }
170
+ console.log(chalk.green(t("sync.done", { count: String(profiles.length) })));
167
171
  if (current) {
168
- console.log(chalk.gray(t("init.current", { name: current })));
172
+ console.log(chalk.gray(t("sync.current", { name: current })));
169
173
  }
170
174
  else {
171
- console.log(chalk.gray(t("init.no_current")));
175
+ console.log(chalk.gray(t("sync.no_current")));
172
176
  }
173
- return;
177
+ ccStore.close();
174
178
  }
175
179
  }
176
- writeRc({ mode: "standalone" });
177
- console.log(chalk.green(t("init.done_standalone")));
178
180
  });
179
- // ccm config
181
+ // ccm config (deprecated, kept for compatibility)
180
182
  program
181
183
  .command("config")
182
184
  .description(t("config.description"))
183
185
  .action(async () => {
184
- const rc = readRc();
185
- if (!rc) {
186
- console.log(chalk.yellow(t("common.not_init")));
186
+ console.log(chalk.gray("ccm now only uses standalone mode. No configuration needed."));
187
+ });
188
+ // ccm sync
189
+ program
190
+ .command("sync")
191
+ .description(t("sync.description"))
192
+ .action(async () => {
193
+ if (!ccSwitchExists()) {
194
+ console.log(chalk.red(t("sync.no_cc_switch")));
187
195
  return;
188
196
  }
189
- console.log(t("config.current_mode", { mode: chalk.cyan(rc.mode) }));
190
- const confirm = await ask(t("config.switch_confirm"));
191
- if (confirm.toLowerCase() === "y") {
192
- const newMode = rc.mode === "cc-switch" ? "standalone" : "cc-switch";
193
- if (newMode === "cc-switch" && !ccSwitchExists()) {
194
- console.log(chalk.red(t("config.cc_switch_not_installed")));
195
- return;
196
- }
197
- writeRc({ mode: newMode });
198
- console.log(chalk.green(t("config.switched", { mode: newMode })));
197
+ const { CcSwitchStore } = await import("./store/cc-switch.js");
198
+ const { StandaloneStore } = await import("./store/standalone.js");
199
+ const ccStore = new CcSwitchStore();
200
+ const standaloneStore = new StandaloneStore();
201
+ const profiles = ccStore.list();
202
+ if (profiles.length === 0) {
203
+ console.log(chalk.yellow(t("sync.empty")));
204
+ ccStore.close();
205
+ return;
206
+ }
207
+ for (const profile of profiles) {
208
+ standaloneStore.save(profile.name, profile.settingsConfig);
209
+ }
210
+ const current = ccStore.getCurrent();
211
+ if (current) {
212
+ standaloneStore.setCurrent(current);
199
213
  }
214
+ console.log(chalk.green(t("sync.done", { count: String(profiles.length) })));
215
+ if (current) {
216
+ console.log(chalk.gray(t("sync.current", { name: current })));
217
+ }
218
+ else {
219
+ console.log(chalk.gray(t("sync.no_current")));
220
+ }
221
+ ccStore.close();
222
+ });
223
+ // ccm clear
224
+ program
225
+ .command("clear")
226
+ .description(t("clear.description"))
227
+ .action(async () => {
228
+ const confirm = await ask(t("clear.confirm"));
229
+ if (confirm.toLowerCase() !== "y") {
230
+ console.log(chalk.gray(t("clear.cancelled")));
231
+ return;
232
+ }
233
+ const rcPath = join(homedir(), ".ccm", "rc.json");
234
+ const configPath = join(homedir(), ".ccm", "config.json");
235
+ if (existsSync(configPath)) {
236
+ unlinkSync(configPath);
237
+ console.log(chalk.green(t("clear.removed", { path: configPath })));
238
+ }
239
+ if (existsSync(rcPath)) {
240
+ unlinkSync(rcPath);
241
+ console.log(chalk.green(t("clear.removed", { path: rcPath })));
242
+ }
243
+ console.log(chalk.green(t("clear.done")));
200
244
  });
201
245
  // ccm list
202
246
  program
@@ -245,8 +289,7 @@ program
245
289
  initialValue: initial >= 0 ? profiles[initial].name : profiles[0].name,
246
290
  });
247
291
  if (clack.isCancel(selected)) {
248
- clack.cancel(t("list.cancelled"));
249
- return;
292
+ process.exit(0);
250
293
  }
251
294
  switchTo(selected);
252
295
  }
@@ -528,8 +571,7 @@ program
528
571
  options,
529
572
  });
530
573
  if (clack.isCancel(selected)) {
531
- clack.cancel(t("list.cancelled"));
532
- return;
574
+ process.exit(0);
533
575
  }
534
576
  name = selected;
535
577
  }
@@ -675,8 +717,7 @@ program
675
717
  options,
676
718
  });
677
719
  if (clack.isCancel(selected)) {
678
- clack.cancel(t("list.cancelled"));
679
- return;
720
+ process.exit(0);
680
721
  }
681
722
  name = selected;
682
723
  }
@@ -809,10 +850,6 @@ const SUPPORTED_LOCALES = [
809
850
  ];
810
851
  const switchLocale = (code) => {
811
852
  const rc = readRc();
812
- if (!rc) {
813
- console.log(chalk.yellow(t("common.not_init")));
814
- return;
815
- }
816
853
  rc.locale = code;
817
854
  writeRc(rc);
818
855
  setLocale(code);
@@ -837,7 +874,9 @@ localeCmd
837
874
  options,
838
875
  initialValue: current,
839
876
  });
840
- if (clack.isCancel(selected) || selected === current)
877
+ if (clack.isCancel(selected))
878
+ process.exit(0);
879
+ if (selected === current)
841
880
  return;
842
881
  switchLocale(selected);
843
882
  }
@@ -46,10 +46,17 @@ export class CcSwitchStore {
46
46
  .run(JSON.stringify(settingsConfig), name);
47
47
  }
48
48
  else {
49
+ const maxSort = this.db
50
+ .prepare(`SELECT COALESCE(MAX(sort_index), -1) as max_sort FROM providers WHERE app_type = 'claude'`)
51
+ .get();
52
+ const sortIndex = (maxSort?.max_sort ?? -1) + 1;
49
53
  const id = crypto.randomUUID();
50
54
  this.db
51
- .prepare(`INSERT INTO providers (id, app_type, name, settings_config, meta, created_at) VALUES (?, 'claude', ?, ?, '{}', ?)`)
52
- .run(id, name, JSON.stringify(settingsConfig), Date.now());
55
+ .prepare(`INSERT INTO providers (
56
+ id, app_type, name, settings_config, website_url, category,
57
+ created_at, sort_index, notes, icon, icon_color, meta, is_current, in_failover_queue
58
+ ) VALUES (?, 'claude', ?, ?, NULL, NULL, ?, ?, NULL, NULL, NULL, '{}', 0, 0)`)
59
+ .run(id, name, JSON.stringify(settingsConfig), Date.now(), sortIndex);
53
60
  }
54
61
  }
55
62
  remove(name) {
@@ -59,6 +66,13 @@ export class CcSwitchStore {
59
66
  return result.changes > 0;
60
67
  }
61
68
  getCurrent() {
69
+ // Prefer DB is_current so we stay in sync with cc-switch UI
70
+ const dbRow = this.db
71
+ .prepare(`SELECT name FROM providers WHERE app_type = 'claude' AND is_current = 1 LIMIT 1`)
72
+ .get();
73
+ if (dbRow)
74
+ return dbRow.name;
75
+ // Fallback to settings.json
62
76
  if (!existsSync(SETTINGS_PATH))
63
77
  return undefined;
64
78
  try {
@@ -79,6 +93,15 @@ export class CcSwitchStore {
79
93
  const profile = this.get(name);
80
94
  if (!profile)
81
95
  throw new Error(t("error.not_found", { name }));
96
+ const tx = this.db.transaction(() => {
97
+ this.db
98
+ .prepare(`UPDATE providers SET is_current = 0 WHERE app_type = 'claude'`)
99
+ .run();
100
+ this.db
101
+ .prepare(`UPDATE providers SET is_current = 1 WHERE app_type = 'claude' AND id = ?`)
102
+ .run(profile.id);
103
+ });
104
+ tx();
82
105
  if (existsSync(SETTINGS_PATH)) {
83
106
  const settings = JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
84
107
  settings.currentProviderClaude = profile.id;
package/dist/types.d.ts CHANGED
@@ -12,7 +12,6 @@ export interface DataStore {
12
12
  setCurrent(name: string): void;
13
13
  }
14
14
  export interface RcConfig {
15
- mode: "cc-switch" | "standalone";
16
15
  aliases?: Record<string, string>;
17
16
  locale?: "zh" | "en";
18
17
  }
package/dist/utils.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { RcConfig } from "./types.js";
2
2
  import type { DataStore } from "./store/interface.js";
3
- export declare function readRc(): RcConfig | undefined;
3
+ export declare function readRc(): RcConfig;
4
4
  export declare function writeRc(rc: RcConfig): void;
5
- export declare function getStore(): DataStore | null;
5
+ export declare function getStore(): DataStore;
package/dist/utils.js CHANGED
@@ -1,18 +1,20 @@
1
1
  import { homedir } from "os";
2
2
  import { join } from "path";
3
3
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
4
- import { CcSwitchStore } from "./store/cc-switch.js";
5
4
  import { StandaloneStore } from "./store/standalone.js";
6
5
  const CCM_DIR = join(homedir(), ".ccm");
7
6
  const RC_PATH = join(CCM_DIR, "rc.json");
8
7
  export function readRc() {
9
- if (!existsSync(RC_PATH))
10
- return undefined;
8
+ if (!existsSync(RC_PATH)) {
9
+ writeRc({});
10
+ return {};
11
+ }
11
12
  try {
12
13
  return JSON.parse(readFileSync(RC_PATH, "utf-8"));
13
14
  }
14
15
  catch {
15
- return undefined;
16
+ writeRc({});
17
+ return {};
16
18
  }
17
19
  }
18
20
  export function writeRc(rc) {
@@ -22,13 +24,5 @@ export function writeRc(rc) {
22
24
  writeFileSync(RC_PATH, JSON.stringify(rc, null, 2));
23
25
  }
24
26
  export function getStore() {
25
- const rc = readRc();
26
- if (!rc)
27
- return null;
28
- if (rc.mode === "cc-switch") {
29
- return new CcSwitchStore();
30
- }
31
- else {
32
- return new StandaloneStore();
33
- }
27
+ return new StandaloneStore();
34
28
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daylenjeez/ccm-switch",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "description": "Claude Code Model Switcher - 快速切换 Claude Code 自定义模型配置",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/i18n/en.ts CHANGED
@@ -23,18 +23,10 @@ const en: Record<TranslationKey, string> = {
23
23
  // init
24
24
  "init.description": "Initialize ccm",
25
25
  "init.cc_switch_found":"cc-switch detected. Import configurations from it? (Y/n) ",
26
- "init.imported": "✓ Imported {count} configurations",
27
- "init.current": "Active: {name}",
28
- "init.no_current": "No active configuration",
29
- "init.done_cc_switch": "✓ Initialized in cc-switch mode",
30
- "init.done_standalone": "✓ Initialized in standalone mode",
26
+ "init.done": "✓ Initialized",
31
27
 
32
28
  // config
33
- "config.description": "View or switch data source mode",
34
- "config.current_mode": "Current mode: {mode}",
35
- "config.switch_confirm": "Switch mode? (y/N) ",
36
- "config.cc_switch_not_installed": "cc-switch is not installed",
37
- "config.switched": "✓ Switched to {mode} mode",
29
+ "config.description": "View data source mode (deprecated)",
38
30
 
39
31
  // list
40
32
  "list.description": "List and select configurations",
@@ -42,7 +34,6 @@ const en: Record<TranslationKey, string> = {
42
34
  "list.header": "Available configurations:",
43
35
  "list.select": "Select configuration:",
44
36
  "list.current_marker": "(current)",
45
- "list.cancelled": "Cancelled",
46
37
  "list.choose_number": "Enter number to switch (Enter to skip): ",
47
38
 
48
39
  // current
@@ -137,6 +128,21 @@ const en: Record<TranslationKey, string> = {
137
128
  "alias.rm_config": "Config {target}",
138
129
  "alias.rm_choose": "Choose (1/2): ",
139
130
 
131
+ // sync
132
+ "sync.description": "Sync configurations from cc-switch",
133
+ "sync.no_cc_switch": "cc-switch database not detected",
134
+ "sync.empty": "No Claude configurations found in cc-switch",
135
+ "sync.done": "✓ Synced {count} configurations",
136
+ "sync.current": "Active: {name}",
137
+ "sync.no_current": "No active configuration",
138
+
139
+ // clear
140
+ "clear.description": "Clean up ccm data files",
141
+ "clear.confirm": "Delete all ccm data files? (y/N) ",
142
+ "clear.cancelled": "Cancelled",
143
+ "clear.removed": "✓ Deleted {path}",
144
+ "clear.done": "✓ Cleanup complete",
145
+
140
146
  // store errors
141
147
  "store.db_not_found": "cc-switch database not found: {path}",
142
148
  };
package/src/i18n/zh.ts CHANGED
@@ -21,18 +21,10 @@ const zh = {
21
21
  // init
22
22
  "init.description": "初始化 ccm",
23
23
  "init.cc_switch_found":"检测到 cc-switch 已安装,是否从中导入配置?(Y/n) ",
24
- "init.imported": "✓ 已导入 {count} 个配置",
25
- "init.current": "当前激活: {name}",
26
- "init.no_current": "当前无激活配置",
27
- "init.done_cc_switch": "✓ 已初始化为 cc-switch 模式",
28
- "init.done_standalone": "✓ 已初始化为独立模式",
24
+ "init.done": "✓ 初始化完成",
29
25
 
30
26
  // config
31
- "config.description": "查看或切换数据源模式",
32
- "config.current_mode": "当前模式: {mode}",
33
- "config.switch_confirm": "是否切换模式?(y/N) ",
34
- "config.cc_switch_not_installed": "cc-switch 未安装",
35
- "config.switched": "✓ 已切换为 {mode} 模式",
27
+ "config.description": "查看数据源模式(已废弃)",
36
28
 
37
29
  // list
38
30
  "list.description": "列出并选择配置方案",
@@ -40,7 +32,6 @@ const zh = {
40
32
  "list.header": "可用配置:",
41
33
  "list.select": "选择配置:",
42
34
  "list.current_marker": "(当前)",
43
- "list.cancelled": "已取消",
44
35
  "list.choose_number": "输入序号切换 (回车跳过): ",
45
36
 
46
37
  // current
@@ -135,6 +126,21 @@ const zh = {
135
126
  "alias.rm_config": "配置 {target}",
136
127
  "alias.rm_choose": "请选择 (1/2): ",
137
128
 
129
+ // sync
130
+ "sync.description": "从 cc-switch 同步配置",
131
+ "sync.no_cc_switch": "未检测到 cc-switch 数据库",
132
+ "sync.empty": "cc-switch 中没有找到 Claude 配置",
133
+ "sync.done": "✓ 已同步 {count} 个配置",
134
+ "sync.current": "当前激活: {name}",
135
+ "sync.no_current": "当前无激活配置",
136
+
137
+ // clear
138
+ "clear.description": "清理 ccm 数据文件",
139
+ "clear.confirm": "确认删除所有 ccm 数据文件?(y/N) ",
140
+ "clear.cancelled": "已取消清理",
141
+ "clear.removed": "✓ 已删除 {path}",
142
+ "clear.done": "✓ 清理完成",
143
+
138
144
  // store errors
139
145
  "store.db_not_found": "cc-switch 数据库不存在: {path}",
140
146
  } as const;
package/src/index.ts CHANGED
@@ -4,11 +4,11 @@ import { Command } from "commander";
4
4
  import chalk from "chalk";
5
5
  import { readRc, writeRc, getStore } from "./utils.js";
6
6
  import { ccSwitchExists } from "./store/cc-switch.js";
7
- import { readClaudeSettings, applyProfile, getSettingsPath } from "./claude.js";
7
+ import { readClaudeSettings, applyProfile } from "./claude.js";
8
8
  import { createInterface } from "readline";
9
9
  import { spawnSync } from "child_process";
10
- import { writeFileSync, readFileSync, unlinkSync } from "fs";
11
- import { tmpdir } from "os";
10
+ import { writeFileSync, readFileSync, unlinkSync, existsSync } from "fs";
11
+ import { tmpdir, homedir } from "os";
12
12
  import { join } from "path";
13
13
  import { t, setLocale } from "./i18n/index.js";
14
14
  import * as clack from "@clack/prompts";
@@ -31,14 +31,9 @@ function ask(question: string): Promise<string> {
31
31
  });
32
32
  }
33
33
 
34
- // Helper: ensure initialized
34
+ // Helper: ensure store ready
35
35
  function ensureStore() {
36
- const store = getStore();
37
- if (!store) {
38
- console.log(chalk.yellow(t("common.not_init")));
39
- process.exit(1);
40
- }
41
- return store;
36
+ return getStore();
42
37
  }
43
38
 
44
39
  // Helper: format env for display
@@ -172,50 +167,110 @@ program
172
167
  .command("init")
173
168
  .description(t("init.description"))
174
169
  .action(async () => {
170
+ const rc = readRc();
171
+ writeRc({ aliases: rc?.aliases, locale: rc?.locale });
172
+ console.log(chalk.green(t("init.done")));
173
+
175
174
  if (ccSwitchExists()) {
176
175
  const use = await ask(t("init.cc_switch_found"));
177
176
  if (use.toLowerCase() !== "n") {
178
- writeRc({ mode: "cc-switch" });
179
177
  const { CcSwitchStore } = await import("./store/cc-switch.js");
180
- const store = new CcSwitchStore();
181
- const profiles = store.list();
182
- const current = store.getCurrent();
183
- console.log(chalk.green(t("init.done_cc_switch")));
184
- console.log(chalk.green(t("init.imported", { count: String(profiles.length) })));
178
+ const { StandaloneStore } = await import("./store/standalone.js");
179
+ const ccStore = new CcSwitchStore();
180
+ const standaloneStore = new StandaloneStore();
181
+ const profiles = ccStore.list();
182
+ const current = ccStore.getCurrent();
183
+
184
+ for (const profile of profiles) {
185
+ standaloneStore.save(profile.name, profile.settingsConfig);
186
+ }
185
187
  if (current) {
186
- console.log(chalk.gray(t("init.current", { name: current })));
188
+ standaloneStore.setCurrent(current);
189
+ }
190
+
191
+ console.log(chalk.green(t("sync.done", { count: String(profiles.length) })));
192
+ if (current) {
193
+ console.log(chalk.gray(t("sync.current", { name: current })));
187
194
  } else {
188
- console.log(chalk.gray(t("init.no_current")));
195
+ console.log(chalk.gray(t("sync.no_current")));
189
196
  }
190
- return;
197
+ ccStore.close();
191
198
  }
192
199
  }
193
-
194
- writeRc({ mode: "standalone" });
195
- console.log(chalk.green(t("init.done_standalone")));
196
200
  });
197
201
 
198
- // ccm config
202
+ // ccm config (deprecated, kept for compatibility)
199
203
  program
200
204
  .command("config")
201
205
  .description(t("config.description"))
202
206
  .action(async () => {
203
- const rc = readRc();
204
- if (!rc) {
205
- console.log(chalk.yellow(t("common.not_init")));
207
+ console.log(chalk.gray("ccm now only uses standalone mode. No configuration needed."));
208
+ });
209
+
210
+ // ccm sync
211
+ program
212
+ .command("sync")
213
+ .description(t("sync.description"))
214
+ .action(async () => {
215
+ if (!ccSwitchExists()) {
216
+ console.log(chalk.red(t("sync.no_cc_switch")));
206
217
  return;
207
218
  }
208
- console.log(t("config.current_mode", { mode: chalk.cyan(rc.mode) }));
209
- const confirm = await ask(t("config.switch_confirm"));
210
- if (confirm.toLowerCase() === "y") {
211
- const newMode = rc.mode === "cc-switch" ? "standalone" : "cc-switch";
212
- if (newMode === "cc-switch" && !ccSwitchExists()) {
213
- console.log(chalk.red(t("config.cc_switch_not_installed")));
214
- return;
215
- }
216
- writeRc({ mode: newMode });
217
- console.log(chalk.green(t("config.switched", { mode: newMode })));
219
+ const { CcSwitchStore } = await import("./store/cc-switch.js");
220
+ const { StandaloneStore } = await import("./store/standalone.js");
221
+ const ccStore = new CcSwitchStore();
222
+ const standaloneStore = new StandaloneStore();
223
+
224
+ const profiles = ccStore.list();
225
+ if (profiles.length === 0) {
226
+ console.log(chalk.yellow(t("sync.empty")));
227
+ ccStore.close();
228
+ return;
229
+ }
230
+
231
+ for (const profile of profiles) {
232
+ standaloneStore.save(profile.name, profile.settingsConfig);
218
233
  }
234
+
235
+ const current = ccStore.getCurrent();
236
+ if (current) {
237
+ standaloneStore.setCurrent(current);
238
+ }
239
+
240
+ console.log(chalk.green(t("sync.done", { count: String(profiles.length) })));
241
+ if (current) {
242
+ console.log(chalk.gray(t("sync.current", { name: current })));
243
+ } else {
244
+ console.log(chalk.gray(t("sync.no_current")));
245
+ }
246
+
247
+ ccStore.close();
248
+ });
249
+
250
+ // ccm clear
251
+ program
252
+ .command("clear")
253
+ .description(t("clear.description"))
254
+ .action(async () => {
255
+ const confirm = await ask(t("clear.confirm"));
256
+ if (confirm.toLowerCase() !== "y") {
257
+ console.log(chalk.gray(t("clear.cancelled")));
258
+ return;
259
+ }
260
+
261
+ const rcPath = join(homedir(), ".ccm", "rc.json");
262
+ const configPath = join(homedir(), ".ccm", "config.json");
263
+
264
+ if (existsSync(configPath)) {
265
+ unlinkSync(configPath);
266
+ console.log(chalk.green(t("clear.removed", { path: configPath })));
267
+ }
268
+ if (existsSync(rcPath)) {
269
+ unlinkSync(rcPath);
270
+ console.log(chalk.green(t("clear.removed", { path: rcPath })));
271
+ }
272
+
273
+ console.log(chalk.green(t("clear.done")));
219
274
  });
220
275
 
221
276
  // ccm list
@@ -270,8 +325,7 @@ program
270
325
  });
271
326
 
272
327
  if (clack.isCancel(selected)) {
273
- clack.cancel(t("list.cancelled"));
274
- return;
328
+ process.exit(0);
275
329
  }
276
330
  switchTo(selected as string);
277
331
  } else {
@@ -583,8 +637,7 @@ program
583
637
  });
584
638
 
585
639
  if (clack.isCancel(selected)) {
586
- clack.cancel(t("list.cancelled"));
587
- return;
640
+ process.exit(0);
588
641
  }
589
642
  name = selected as string;
590
643
  } else {
@@ -741,8 +794,7 @@ program
741
794
  });
742
795
 
743
796
  if (clack.isCancel(selected)) {
744
- clack.cancel(t("list.cancelled"));
745
- return;
797
+ process.exit(0);
746
798
  }
747
799
  name = selected as string;
748
800
  } else {
@@ -887,7 +939,6 @@ const SUPPORTED_LOCALES: Array<{ code: string; label: string }> = [
887
939
 
888
940
  const switchLocale = (code: string) => {
889
941
  const rc = readRc();
890
- if (!rc) { console.log(chalk.yellow(t("common.not_init"))); return; }
891
942
  rc.locale = code as "zh" | "en";
892
943
  writeRc(rc);
893
944
  setLocale(code as "zh" | "en");
@@ -916,7 +967,8 @@ localeCmd
916
967
  initialValue: current,
917
968
  });
918
969
 
919
- if (clack.isCancel(selected) || selected === current) return;
970
+ if (clack.isCancel(selected)) process.exit(0);
971
+ if (selected === current) return;
920
972
  switchLocale(selected as string);
921
973
  } else {
922
974
  console.log(chalk.bold(`\n${t("locale.list_header")}\n`));
@@ -62,12 +62,22 @@ export class CcSwitchStore implements DataStore {
62
62
  )
63
63
  .run(JSON.stringify(settingsConfig), name);
64
64
  } else {
65
+ const maxSort = this.db
66
+ .prepare(
67
+ `SELECT COALESCE(MAX(sort_index), -1) as max_sort FROM providers WHERE app_type = 'claude'`
68
+ )
69
+ .get() as { max_sort: number } | undefined;
70
+ const sortIndex = (maxSort?.max_sort ?? -1) + 1;
71
+
65
72
  const id = crypto.randomUUID();
66
73
  this.db
67
74
  .prepare(
68
- `INSERT INTO providers (id, app_type, name, settings_config, meta, created_at) VALUES (?, 'claude', ?, ?, '{}', ?)`
75
+ `INSERT INTO providers (
76
+ id, app_type, name, settings_config, website_url, category,
77
+ created_at, sort_index, notes, icon, icon_color, meta, is_current, in_failover_queue
78
+ ) VALUES (?, 'claude', ?, ?, NULL, NULL, ?, ?, NULL, NULL, NULL, '{}', 0, 0)`
69
79
  )
70
- .run(id, name, JSON.stringify(settingsConfig), Date.now());
80
+ .run(id, name, JSON.stringify(settingsConfig), Date.now(), sortIndex);
71
81
  }
72
82
  }
73
83
 
@@ -81,6 +91,15 @@ export class CcSwitchStore implements DataStore {
81
91
  }
82
92
 
83
93
  getCurrent(): string | undefined {
94
+ // Prefer DB is_current so we stay in sync with cc-switch UI
95
+ const dbRow = this.db
96
+ .prepare(
97
+ `SELECT name FROM providers WHERE app_type = 'claude' AND is_current = 1 LIMIT 1`
98
+ )
99
+ .get() as { name: string } | undefined;
100
+ if (dbRow) return dbRow.name;
101
+
102
+ // Fallback to settings.json
84
103
  if (!existsSync(SETTINGS_PATH)) return undefined;
85
104
  try {
86
105
  const settings = JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
@@ -101,6 +120,18 @@ export class CcSwitchStore implements DataStore {
101
120
  const profile = this.get(name);
102
121
  if (!profile) throw new Error(t("error.not_found", { name }));
103
122
 
123
+ const tx = this.db.transaction(() => {
124
+ this.db
125
+ .prepare(`UPDATE providers SET is_current = 0 WHERE app_type = 'claude'`)
126
+ .run();
127
+ this.db
128
+ .prepare(
129
+ `UPDATE providers SET is_current = 1 WHERE app_type = 'claude' AND id = ?`
130
+ )
131
+ .run(profile.id);
132
+ });
133
+ tx();
134
+
104
135
  if (existsSync(SETTINGS_PATH)) {
105
136
  const settings = JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
106
137
  settings.currentProviderClaude = profile.id;
package/src/types.ts CHANGED
@@ -14,7 +14,6 @@ export interface DataStore {
14
14
  }
15
15
 
16
16
  export interface RcConfig {
17
- mode: "cc-switch" | "standalone";
18
17
  aliases?: Record<string, string>;
19
18
  locale?: "zh" | "en";
20
19
  }
package/src/utils.ts CHANGED
@@ -3,18 +3,21 @@ import { join } from "path";
3
3
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
4
4
  import type { RcConfig } from "./types.js";
5
5
  import type { DataStore } from "./store/interface.js";
6
- import { CcSwitchStore } from "./store/cc-switch.js";
7
6
  import { StandaloneStore } from "./store/standalone.js";
8
7
 
9
8
  const CCM_DIR = join(homedir(), ".ccm");
10
9
  const RC_PATH = join(CCM_DIR, "rc.json");
11
10
 
12
- export function readRc(): RcConfig | undefined {
13
- if (!existsSync(RC_PATH)) return undefined;
11
+ export function readRc(): RcConfig {
12
+ if (!existsSync(RC_PATH)) {
13
+ writeRc({});
14
+ return {};
15
+ }
14
16
  try {
15
17
  return JSON.parse(readFileSync(RC_PATH, "utf-8"));
16
18
  } catch {
17
- return undefined;
19
+ writeRc({});
20
+ return {};
18
21
  }
19
22
  }
20
23
 
@@ -25,13 +28,6 @@ export function writeRc(rc: RcConfig): void {
25
28
  writeFileSync(RC_PATH, JSON.stringify(rc, null, 2));
26
29
  }
27
30
 
28
- export function getStore(): DataStore | null {
29
- const rc = readRc();
30
- if (!rc) return null;
31
-
32
- if (rc.mode === "cc-switch") {
33
- return new CcSwitchStore();
34
- } else {
35
- return new StandaloneStore();
36
- }
31
+ export function getStore(): DataStore {
32
+ return new StandaloneStore();
37
33
  }