@neuxsbotzz/cc-switch 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +18 -0
  2. package/cli.mjs +358 -0
  3. package/package.json +14 -0
package/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # cc-switch
2
+
3
+ Switch Claude Code and Codex CLI between official endpoints and Token.Me.Uk.
4
+
5
+ ```bash
6
+ npm install -g @neuxsbotzz/cc-switch
7
+
8
+ cc-switch use token-me-uk --key sk-your-api-key
9
+ cc-switch status
10
+ cc-switch use official --key sk-your-official-key
11
+ ```
12
+
13
+ Custom provider:
14
+
15
+ ```bash
16
+ cc-switch add work --anthropic-url https://example.com --openai-url https://example.com/v1 --key sk-your-api-key
17
+ cc-switch use work
18
+ ```
package/cli.mjs ADDED
@@ -0,0 +1,358 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from 'node:child_process'
3
+ import fs from 'node:fs'
4
+ import os from 'node:os'
5
+ import path from 'node:path'
6
+ import readline from 'node:readline/promises'
7
+ import { stdin as input, stdout as output } from 'node:process'
8
+
9
+ const markerStart = '# >>> cc-switch config >>>'
10
+ const markerEnd = '# <<< cc-switch config <<<'
11
+
12
+ const builtInProviders = {
13
+ official: {
14
+ name: 'official',
15
+ anthropicUrl: 'https://api.anthropic.com',
16
+ openaiUrl: 'https://api.openai.com/v1',
17
+ key: '',
18
+ },
19
+ 'token-me-uk': {
20
+ name: 'token-me-uk',
21
+ anthropicUrl: 'https://www.token.me.uk',
22
+ openaiUrl: 'https://www.token.me.uk/v1',
23
+ key: '',
24
+ },
25
+ }
26
+
27
+ function printHelp() {
28
+ console.log(`cc-switch - Claude Code / Codex CLI 线路切换工具
29
+
30
+ 用法:
31
+ cc-switch use token-me-uk --key sk-your-api-key
32
+ cc-switch use official --key sk-your-official-key
33
+ cc-switch add <name> --anthropic-url <url> --openai-url <url> --key <key>
34
+ cc-switch list
35
+ cc-switch status
36
+ cc-switch remove <name>
37
+
38
+ 参数:
39
+ --key <key> API Key;use 时传入会保存到该线路
40
+ --anthropic-url <url> Claude Code 使用的地址,不带 /v1
41
+ --openai-url <url> Codex CLI / OpenAI SDK 使用的地址,通常带 /v1
42
+ --target <all|claude|codex>
43
+ --profile <path> macOS/Linux 写入的 shell 配置文件
44
+ --dry-run 只显示将写入的内容,不修改系统
45
+ `)
46
+ }
47
+
48
+ function parseOptions(argv) {
49
+ const positional = []
50
+ const options = {
51
+ key: '',
52
+ anthropicUrl: '',
53
+ openaiUrl: '',
54
+ target: 'all',
55
+ profile: '',
56
+ dryRun: false,
57
+ }
58
+
59
+ for (let i = 0; i < argv.length; i += 1) {
60
+ const arg = argv[i]
61
+ const next = argv[i + 1]
62
+ if (arg === '--key' && next) {
63
+ options.key = next
64
+ i += 1
65
+ } else if (arg === '--anthropic-url' && next) {
66
+ options.anthropicUrl = normalizeUrl(next)
67
+ i += 1
68
+ } else if (arg === '--openai-url' && next) {
69
+ options.openaiUrl = normalizeUrl(next)
70
+ i += 1
71
+ } else if (arg === '--target' && next) {
72
+ options.target = next
73
+ i += 1
74
+ } else if (arg === '--profile' && next) {
75
+ options.profile = next
76
+ i += 1
77
+ } else if (arg === '--dry-run') {
78
+ options.dryRun = true
79
+ } else if (arg === '-h' || arg === '--help') {
80
+ printHelp()
81
+ process.exit(0)
82
+ } else {
83
+ positional.push(arg)
84
+ }
85
+ }
86
+
87
+ return { positional, options }
88
+ }
89
+
90
+ function normalizeUrl(value) {
91
+ return String(value || '').trim().replace(/\/+$/, '')
92
+ }
93
+
94
+ function configPath() {
95
+ if (process.env.CC_SWITCH_CONFIG) return process.env.CC_SWITCH_CONFIG
96
+ if (process.platform === 'win32' && process.env.APPDATA) {
97
+ return path.join(process.env.APPDATA, 'cc-switch', 'config.json')
98
+ }
99
+ const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config')
100
+ return path.join(base, 'cc-switch', 'config.json')
101
+ }
102
+
103
+ function loadConfig() {
104
+ const file = configPath()
105
+ if (!fs.existsSync(file)) return { active: '', providers: {} }
106
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'))
107
+ return {
108
+ active: parsed.active || '',
109
+ providers: parsed.providers || {},
110
+ }
111
+ }
112
+
113
+ function saveConfig(config) {
114
+ const file = configPath()
115
+ fs.mkdirSync(path.dirname(file), { recursive: true })
116
+ fs.writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`, 'utf8')
117
+ }
118
+
119
+ function allProviders(config) {
120
+ return {
121
+ ...builtInProviders,
122
+ ...config.providers,
123
+ }
124
+ }
125
+
126
+ function assertProviderName(name) {
127
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(name || '')) {
128
+ throw new Error('线路名称只能使用字母、数字、点、下划线和短横线')
129
+ }
130
+ }
131
+
132
+ function deriveOpenAiUrl(anthropicUrl) {
133
+ return `${normalizeUrl(anthropicUrl)}/v1`
134
+ }
135
+
136
+ async function askForKey(provider, current) {
137
+ if (current) return current.trim()
138
+ if (provider.key) return provider.key.trim()
139
+ const envKey = process.env.TOKEN_ME_UK_API_KEY || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY || ''
140
+ if (envKey) return envKey.trim()
141
+
142
+ const rl = readline.createInterface({ input, output })
143
+ try {
144
+ const answer = await rl.question('请输入 API Key: ')
145
+ return answer.trim()
146
+ } finally {
147
+ rl.close()
148
+ }
149
+ }
150
+
151
+ function buildEnv(provider, target) {
152
+ if (!['all', 'claude', 'codex'].includes(target)) {
153
+ throw new Error('--target 只能是 all、claude 或 codex')
154
+ }
155
+ const env = {}
156
+ if (target === 'all' || target === 'claude') {
157
+ env.ANTHROPIC_BASE_URL = provider.anthropicUrl
158
+ env.ANTHROPIC_API_KEY = provider.key
159
+ }
160
+ if (target === 'all' || target === 'codex') {
161
+ env.OPENAI_BASE_URL = provider.openaiUrl
162
+ env.OPENAI_API_KEY = provider.key
163
+ }
164
+ return env
165
+ }
166
+
167
+ function defaultProfilePath() {
168
+ if (process.env.CC_SWITCH_PROFILE) return process.env.CC_SWITCH_PROFILE
169
+ const home = os.homedir()
170
+ const shell = process.env.SHELL || ''
171
+ if (shell.includes('zsh')) return path.join(home, '.zshrc')
172
+ if (shell.includes('bash')) return path.join(home, '.bashrc')
173
+ const zshrc = path.join(home, '.zshrc')
174
+ const bashrc = path.join(home, '.bashrc')
175
+ if (fs.existsSync(zshrc)) return zshrc
176
+ if (fs.existsSync(bashrc)) return bashrc
177
+ return bashrc
178
+ }
179
+
180
+ function shellQuote(value) {
181
+ return `'${String(value).replace(/'/g, `'\\''`)}'`
182
+ }
183
+
184
+ function writeUnixProfile(profile, env, dryRun) {
185
+ const block = [
186
+ markerStart,
187
+ ...Object.entries(env).map(([key, value]) => `export ${key}=${shellQuote(value)}`),
188
+ markerEnd,
189
+ '',
190
+ ].join('\n')
191
+
192
+ if (dryRun) {
193
+ console.log(`将写入 ${profile}:\n\n${block}`)
194
+ return
195
+ }
196
+
197
+ fs.mkdirSync(path.dirname(profile), { recursive: true })
198
+ const current = fs.existsSync(profile) ? fs.readFileSync(profile, 'utf8') : ''
199
+ const pattern = new RegExp(`${escapeRegExp(markerStart)}[\\s\\S]*?${escapeRegExp(markerEnd)}\\n?`, 'm')
200
+ const next = pattern.test(current)
201
+ ? current.replace(pattern, block)
202
+ : `${current.replace(/\s*$/, '')}\n\n${block}`
203
+ fs.writeFileSync(profile, next, 'utf8')
204
+ console.log(`已写入 ${profile}`)
205
+ }
206
+
207
+ function writeWindowsEnv(env, dryRun) {
208
+ for (const [key, value] of Object.entries(env)) {
209
+ if (dryRun) {
210
+ console.log(`setx ${key} ${value}`)
211
+ continue
212
+ }
213
+ execFileSync('setx', [key, value], { stdio: 'inherit' })
214
+ }
215
+ }
216
+
217
+ function writeEnv(env, options) {
218
+ const profile = options.profile || process.env.CC_SWITCH_PROFILE || ''
219
+ if (profile || process.platform !== 'win32') {
220
+ writeUnixProfile(profile || defaultProfilePath(), env, options.dryRun)
221
+ return
222
+ }
223
+ writeWindowsEnv(env, options.dryRun)
224
+ }
225
+
226
+ function escapeRegExp(value) {
227
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
228
+ }
229
+
230
+ function maskKey(value) {
231
+ if (!value) return '(未设置)'
232
+ if (value.length <= 12) return '***'
233
+ return `${value.slice(0, 7)}...${value.slice(-4)}`
234
+ }
235
+
236
+ function printProvider(name, provider, active) {
237
+ const mark = active === name ? '*' : ' '
238
+ console.log(`${mark} ${name}`)
239
+ console.log(` Claude: ${provider.anthropicUrl}`)
240
+ console.log(` OpenAI: ${provider.openaiUrl}`)
241
+ console.log(` Key: ${maskKey(provider.key || '')}`)
242
+ }
243
+
244
+ async function addProvider(name, options) {
245
+ assertProviderName(name)
246
+ const anthropicUrl = normalizeUrl(options.anthropicUrl)
247
+ const openaiUrl = normalizeUrl(options.openaiUrl || deriveOpenAiUrl(anthropicUrl))
248
+ if (!anthropicUrl) throw new Error('请提供 --anthropic-url')
249
+ if (!openaiUrl) throw new Error('请提供 --openai-url')
250
+
251
+ const config = loadConfig()
252
+ config.providers[name] = {
253
+ name,
254
+ anthropicUrl,
255
+ openaiUrl,
256
+ key: options.key || config.providers[name]?.key || '',
257
+ }
258
+ saveConfig(config)
259
+ console.log(`已保存线路: ${name}`)
260
+ }
261
+
262
+ async function useProvider(name, options) {
263
+ assertProviderName(name)
264
+ const config = loadConfig()
265
+ const providers = allProviders(config)
266
+ const provider = providers[name]
267
+ if (!provider) throw new Error(`找不到线路: ${name}。可先运行 cc-switch add ${name} ...`)
268
+
269
+ const key = await askForKey(provider, options.key)
270
+ if (!key) throw new Error('API Key 不能为空')
271
+
272
+ const resolved = { ...provider, key }
273
+ const env = buildEnv(resolved, options.target)
274
+ writeEnv(env, options)
275
+
276
+ if (!builtInProviders[name]) {
277
+ config.providers[name] = resolved
278
+ } else if (options.key || key) {
279
+ config.providers[name] = resolved
280
+ }
281
+ config.active = name
282
+ if (!options.dryRun) saveConfig(config)
283
+
284
+ console.log(`\n已切换到: ${name}`)
285
+ console.log(`Claude: ${resolved.anthropicUrl}`)
286
+ console.log(`OpenAI: ${resolved.openaiUrl}`)
287
+ console.log(`Key: ${maskKey(resolved.key)}`)
288
+ if (process.platform === 'win32' && !options.profile && !process.env.CC_SWITCH_PROFILE) {
289
+ console.log('请重新打开 PowerShell / CMD 后再运行 claude 或 codex。')
290
+ } else {
291
+ console.log(`执行 source ${options.profile || process.env.CC_SWITCH_PROFILE || defaultProfilePath()},或重新打开终端后生效。`)
292
+ }
293
+ }
294
+
295
+ function listProviders() {
296
+ const config = loadConfig()
297
+ const providers = allProviders(config)
298
+ for (const [name, provider] of Object.entries(providers)) {
299
+ printProvider(name, provider, config.active)
300
+ }
301
+ }
302
+
303
+ function status() {
304
+ const config = loadConfig()
305
+ const providers = allProviders(config)
306
+ const active = config.active
307
+ if (!active || !providers[active]) {
308
+ console.log('当前没有启用线路。')
309
+ console.log(`配置文件: ${configPath()}`)
310
+ return
311
+ }
312
+ const provider = providers[active]
313
+ console.log(`当前线路: ${active}`)
314
+ console.log(`Claude: ${provider.anthropicUrl}`)
315
+ console.log(`OpenAI: ${provider.openaiUrl}`)
316
+ console.log(`Key: ${maskKey(provider.key || '')}`)
317
+ console.log(`配置文件: ${configPath()}`)
318
+ }
319
+
320
+ function removeProvider(name) {
321
+ assertProviderName(name)
322
+ if (builtInProviders[name]) throw new Error('内置线路不能删除')
323
+ const config = loadConfig()
324
+ if (!config.providers[name]) throw new Error(`找不到线路: ${name}`)
325
+ delete config.providers[name]
326
+ if (config.active === name) config.active = ''
327
+ saveConfig(config)
328
+ console.log(`已删除线路: ${name}`)
329
+ }
330
+
331
+ async function main() {
332
+ const { positional, options } = parseOptions(process.argv.slice(2))
333
+ const [command, name] = positional
334
+
335
+ if (!command) {
336
+ printHelp()
337
+ return
338
+ }
339
+
340
+ if (command === 'add') {
341
+ await addProvider(name, options)
342
+ } else if (command === 'use') {
343
+ await useProvider(name, options)
344
+ } else if (command === 'list') {
345
+ listProviders()
346
+ } else if (command === 'status') {
347
+ status()
348
+ } else if (command === 'remove' || command === 'rm') {
349
+ removeProvider(name)
350
+ } else {
351
+ throw new Error(`未知命令: ${command}`)
352
+ }
353
+ }
354
+
355
+ main().catch((error) => {
356
+ console.error(`\ncc-switch 失败: ${error.message}`)
357
+ process.exit(1)
358
+ })
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@neuxsbotzz/cc-switch",
3
+ "version": "1.0.0",
4
+ "description": "Switch Claude Code and Codex CLI between official and Token.Me.Uk API providers.",
5
+ "type": "module",
6
+ "bin": {
7
+ "cc-switch": "./cli.mjs"
8
+ },
9
+ "files": [
10
+ "cli.mjs",
11
+ "README.md"
12
+ ],
13
+ "license": "UNLICENSED"
14
+ }