@wayfarer35/ccs 0.2.2
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/LICENSE +21 -0
- package/README.md +94 -0
- package/dist/cli.d.ts +24 -0
- package/dist/cli.js +490 -0
- package/dist/completion.d.ts +27 -0
- package/dist/completion.js +116 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.js +73 -0
- package/dist/form.d.ts +66 -0
- package/dist/form.js +269 -0
- package/dist/formUi.d.ts +30 -0
- package/dist/formUi.js +608 -0
- package/dist/i18n.d.ts +24 -0
- package/dist/i18n.js +195 -0
- package/dist/inkPrompts.d.ts +25 -0
- package/dist/inkPrompts.js +176 -0
- package/dist/launch.d.ts +27 -0
- package/dist/launch.js +108 -0
- package/dist/picker.d.ts +24 -0
- package/dist/picker.js +153 -0
- package/dist/presets.d.ts +13 -0
- package/dist/presets.js +29 -0
- package/dist/presets.json +314 -0
- package/dist/screen.d.ts +12 -0
- package/dist/screen.js +14 -0
- package/dist/tui.d.ts +24 -0
- package/dist/tui.js +25 -0
- package/dist/types.d.ts +68 -0
- package/dist/types.js +9 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +35 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 evan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# ccs — Claude Code Switch
|
|
2
|
+
|
|
3
|
+
支持 [Claude Code](https://docs.claude.com/en/docs/claude-code) 可以使用不同的配置
|
|
4
|
+
|
|
5
|
+
ccs 将供应商配置单独存储,启动把「供应商配置」通过 `claude --settings` 添加到启动参数。由 claude 会将 `~/.claude/settings.json` 与供应商配置进行合并(`--settings` 优先级最高,同 key 覆盖下层)。
|
|
6
|
+
|
|
7
|
+
> Bilingual: ccs 自动感知系统语言(中文/English),可用 `ccs config locale en|zh-CN` 手动设置。未设置时按 `LANG`/`LC_ALL` 自动判断,识别不到则回退English。
|
|
8
|
+
|
|
9
|
+
## 为什么需要
|
|
10
|
+
|
|
11
|
+
Claude Code 的供应商接入全靠 `env`(`ANTHROPIC_BASE_URL` / `ANTHROPIC_AUTH_TOKEN` / `ANTHROPIC_MODEL` / 各档位别名)。多供应商时只能手动改来改去,没法同时维护几套。`ccs` 把供应商配置拆成独立文件,按需启动;**通用配置直接复用 `~/.claude/settings.json`**,不重复维护 hooks/statusLine/theme 等。
|
|
12
|
+
|
|
13
|
+
## 安装
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g @wayfarer35/ccs
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
安装后即可使用 `ccs` 命令:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
ccs --help
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 配置存放
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
~/.ccs/
|
|
29
|
+
providers/
|
|
30
|
+
deepseek-api.settings.json 供应商配置(env: BASE_URL/AUTH_TOKEN/MODEL/别名 + model)
|
|
31
|
+
myprov.settings.json
|
|
32
|
+
config.json ccs 自身设置(如语言 locale)
|
|
33
|
+
presets.json 自定义/覆盖预设(可选)
|
|
34
|
+
.lastused 记录上次选择(仅用于交互高亮)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
每个 provider 文件都是标准的 Claude Code settings 片段。启动时 ccs 直接执行 `claude --settings ~/.ccs/providers/<name>.settings.json`。
|
|
38
|
+
|
|
39
|
+
## 用法
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
ccs # 交互选择 供应商 / default / create 并启动(有配置时另含 edit / remove)
|
|
43
|
+
ccs use # 同上:交互选择并启动
|
|
44
|
+
ccs provider-name # 直接用 供应商配置名称
|
|
45
|
+
ccs provider-name --print "hi" # 透传参数给 claude
|
|
46
|
+
ccs provider-name --dry-run # 打印片段与命令,不启动
|
|
47
|
+
ccs list # 列出供应商
|
|
48
|
+
ccs presets # 列出可用预设
|
|
49
|
+
ccs create # 引导式创建(先选 内置/自定义,再填 Key)
|
|
50
|
+
ccs create myprov # 直接以自定义名称创建
|
|
51
|
+
ccs edit provider-name # 引导式编辑(预填当前值,回车保留)
|
|
52
|
+
ccs edit provider-name --raw # 用编辑器改原始 JSON
|
|
53
|
+
ccs remove provider-name # 删除
|
|
54
|
+
ccs common # 在编辑器中打开 ~/.claude/settings.json
|
|
55
|
+
ccs show provider-name # 查看供应商片段(密钥已遮蔽)
|
|
56
|
+
ccs config locale zh-CN # 设置语言(en | zh-CN)
|
|
57
|
+
ccs -h # 帮助
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
### 自定义预设
|
|
62
|
+
|
|
63
|
+
在 `~/.ccs/presets.json` 中按相同结构写入,可覆盖内置预设或新增:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"myprov": {
|
|
68
|
+
"label": "我的供应商",
|
|
69
|
+
"baseUrl": "https://my.example.com/anthropic",
|
|
70
|
+
"model": "my-model",
|
|
71
|
+
"models": { "haiku": "my-fast", "sonnet": "my-model", "opus": "my-model", "fable": "my-model" },
|
|
72
|
+
"options": {
|
|
73
|
+
"attributionHeader": false,
|
|
74
|
+
"disableNonEssentialTraffic": true,
|
|
75
|
+
"autoCompactWindow": 200000
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
- `model`:默认模型(`ANTHROPIC_MODEL`)。
|
|
82
|
+
- `models`:各档位别名映射(`haiku/sonnet/opus/fable`,小写)。没有 `models` 的预设按「只预填 Base URL」处理,模型由用户填写。
|
|
83
|
+
- `options`:可选,预填四项 `CLAUDE_CODE_*` 配置;省略时用默认值。
|
|
84
|
+
|
|
85
|
+
## 环境变量
|
|
86
|
+
|
|
87
|
+
- `CCS_CLAUDE_BIN`:指定 `claude` 路径(默认 `claude`)。
|
|
88
|
+
- `EDITOR` / `VISUAL`:`--raw` 与 `ccs common` 使用的编辑器(默认 `vi`)。
|
|
89
|
+
- `LANG` / `LC_ALL`:未通过 `ccs config locale` 设置语言时,据此自动感知。
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
## 许可
|
|
93
|
+
|
|
94
|
+
MIT
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import type { SpawnSyncReturns } from 'node:child_process';
|
|
3
|
+
export { main, cmdCreate, cmdEdit, cmdRemove, cmdCommon, cmdShow, cmdConfig, cmdConfigLocale, cmdList, cmdPresets, cmdUse, cmdLaunch, printHelp, validateName, nameValidator, };
|
|
4
|
+
declare function main(): Promise<void>;
|
|
5
|
+
declare function cmdLaunch(name: string, rest: string[]): void;
|
|
6
|
+
declare function cmdUse(rest: string[]): Promise<void>;
|
|
7
|
+
declare function cmdList(): void;
|
|
8
|
+
declare function cmdPresets(): void;
|
|
9
|
+
declare function validateName(name: string): void;
|
|
10
|
+
/**
|
|
11
|
+
* 配置名校验器(供 inkText 的 validate 内联使用)。
|
|
12
|
+
* 检查非空、非法字符、重名——重名即时提示,引导用户改名而非直接失败。
|
|
13
|
+
* 返回错误消息字符串,或 undefined 表示通过。
|
|
14
|
+
*/
|
|
15
|
+
declare function nameValidator(v: string): string | undefined;
|
|
16
|
+
declare function cmdCreate(rest: string[]): Promise<string | undefined>;
|
|
17
|
+
declare function cmdEdit(rest: string[]): Promise<string | undefined>;
|
|
18
|
+
declare function cmdRemove(rest: string[]): Promise<string | undefined>;
|
|
19
|
+
declare function cmdCommon(): void;
|
|
20
|
+
declare function cmdShow(rest: string[]): void;
|
|
21
|
+
declare function cmdConfig(rest: string[]): Promise<void>;
|
|
22
|
+
declare function cmdConfigLocale(val: string | undefined): Promise<void>;
|
|
23
|
+
declare function printHelp(): void;
|
|
24
|
+
export type { SpawnSyncReturns };
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { listProviders, providerExists, providerFile, readProvider, writeJSON, removeProvider, getLastUsed, writeFileSyncSafe, CLAUDE_SETTINGS_FILE, } from './config.js';
|
|
6
|
+
import { presetList } from './presets.js';
|
|
7
|
+
import { providerFormWithPreview, chooseCreateMode, pickBuiltinPreset } from './form.js';
|
|
8
|
+
import { launch, dryRun, launchDirect, dryRunDirect, redactSettings } from './launch.js';
|
|
9
|
+
import { ui, Cancel } from './tui.js';
|
|
10
|
+
import { t, detectLocale, setConfig, LOCALES } from './i18n.js';
|
|
11
|
+
import { clearScreen } from './screen.js';
|
|
12
|
+
import { getVersion } from './version.js';
|
|
13
|
+
import { completeCandidates, completionScript, completionHelp } from './completion.js';
|
|
14
|
+
// 版本号单一真源:运行时从 package.json 读取,不再硬编码。
|
|
15
|
+
const VERSION = getVersion();
|
|
16
|
+
const helpEn = `ccs ${VERSION} — Claude Code Switch
|
|
17
|
+
|
|
18
|
+
Switch between multiple provider configs and launch Claude Code.
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
ccs Pick a provider / direct / create interactively, then launch
|
|
22
|
+
ccs <name> [args...] Launch with the named provider; args forwarded to claude
|
|
23
|
+
ccs <name> --dry-run Print provider config + command without launching
|
|
24
|
+
ccs use [args...] Pick a provider interactively and launch
|
|
25
|
+
ccs list List provider configs
|
|
26
|
+
ccs presets List built-in presets
|
|
27
|
+
ccs create [name] Create a new config (duplicate names rejected; built-in prompts for a name, default = preset key)
|
|
28
|
+
ccs edit <name> [--raw] Guided edit (--raw: edit raw JSON in $EDITOR)
|
|
29
|
+
ccs remove <name> Remove a provider config
|
|
30
|
+
ccs common Edit common config (~/.claude/settings.json) in $EDITOR
|
|
31
|
+
ccs show <name> Show provider config (secrets redacted)
|
|
32
|
+
ccs config [locale [en|zh-CN]] Show/set language
|
|
33
|
+
ccs completion <bash|zsh> Print shell completion script
|
|
34
|
+
ccs -h | --help Help
|
|
35
|
+
ccs -v | --version Version
|
|
36
|
+
|
|
37
|
+
Storage:
|
|
38
|
+
~/.claude/settings.json Common config (provider-agnostic); ccs reads, never writes
|
|
39
|
+
~/.ccs/providers/<name>.settings.json Per-provider config
|
|
40
|
+
~/.ccs/config.json ccs settings (e.g. locale)
|
|
41
|
+
~/.ccs/presets.json Custom/override presets (optional)
|
|
42
|
+
|
|
43
|
+
Environment:
|
|
44
|
+
CCS_CLAUDE_BIN Path to claude binary (default: claude)
|
|
45
|
+
EDITOR / VISUAL Editor for --raw / common (default: vi)
|
|
46
|
+
LANG / LC_ALL Auto-detected for locale before ccs config locale is set
|
|
47
|
+
|
|
48
|
+
Examples:
|
|
49
|
+
ccs create Pick built-in (e.g. deepseek-api) → only API key needed
|
|
50
|
+
ccs create myprov Create a custom provider named myprov
|
|
51
|
+
ccs deepseek-api Launch with deepseek-api
|
|
52
|
+
ccs deepseek-api --print "hi" Forward args to claude
|
|
53
|
+
ccs show deepseek-api Inspect provider config (secrets redacted)`;
|
|
54
|
+
const helpZh = `ccs ${VERSION} — Claude Code Switch
|
|
55
|
+
|
|
56
|
+
在多套供应商配置间切换并启动 Claude Code。
|
|
57
|
+
|
|
58
|
+
用法:
|
|
59
|
+
ccs 交互选择 供应商 / direct / create 并启动
|
|
60
|
+
ccs <name> [args...] 用指定供应商启动;args 透传给 claude
|
|
61
|
+
ccs <name> --dry-run 打印 provider 配置与命令,不启动
|
|
62
|
+
ccs use [args...] 交互选择供应商并启动
|
|
63
|
+
ccs list 列出供应商配置
|
|
64
|
+
ccs presets 列出可用预设
|
|
65
|
+
ccs create [name] 创建新配置(重名将被拒绝;内置会询问配置名,默认取预设 key)
|
|
66
|
+
ccs edit <name> [--raw] 引导式编辑(--raw 用编辑器改原始 JSON)
|
|
67
|
+
ccs remove <name> 删除供应商配置
|
|
68
|
+
ccs common 编辑通用配置(~/.claude/settings.json)
|
|
69
|
+
ccs show <name> 查看 provider 配置(密钥已遮蔽)
|
|
70
|
+
ccs config [locale [en|zh-CN]] 查看/设置语言
|
|
71
|
+
ccs completion <bash|zsh> 输出 shell 补全脚本
|
|
72
|
+
ccs -h | --help 帮助
|
|
73
|
+
ccs -v | --version 版本
|
|
74
|
+
|
|
75
|
+
配置存放:
|
|
76
|
+
~/.claude/settings.json 通用配置(与供应商无关);ccs 只读不写
|
|
77
|
+
~/.ccs/providers/<name>.settings.json 各供应商配置
|
|
78
|
+
~/.ccs/config.json ccs 设置(如语言)
|
|
79
|
+
~/.ccs/presets.json 自定义/覆盖预设(可选)
|
|
80
|
+
|
|
81
|
+
环境变量:
|
|
82
|
+
CCS_CLAUDE_BIN 指定 claude 可执行文件路径(默认 claude)
|
|
83
|
+
EDITOR / VISUAL --raw / common 使用的编辑器(默认 vi)
|
|
84
|
+
LANG / LC_ALL 未设置语言时自动感知系统语言
|
|
85
|
+
|
|
86
|
+
示例:
|
|
87
|
+
ccs create 选内置(如 deepseek-api)→ 通常只需填 API Key
|
|
88
|
+
ccs create myprov 创建名为 myprov 的自定义供应商
|
|
89
|
+
ccs deepseek-api 直接用 deepseek-api 启动
|
|
90
|
+
ccs deepseek-api --print "hi" 透传参数给 claude
|
|
91
|
+
ccs show deepseek-api 查看 provider 配置(密钥已遮蔽)`;
|
|
92
|
+
// 直接作为入口执行时才跑 main;被 import(如测试)时不自动运行。
|
|
93
|
+
const isMain = (() => {
|
|
94
|
+
try {
|
|
95
|
+
return realpathSync(process.argv[1] ?? '') === fileURLToPath(import.meta.url);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
})();
|
|
101
|
+
if (isMain)
|
|
102
|
+
main();
|
|
103
|
+
export { main, cmdCreate, cmdEdit, cmdRemove, cmdCommon, cmdShow, cmdConfig, cmdConfigLocale, cmdList, cmdPresets, cmdUse, cmdLaunch, printHelp, validateName, nameValidator, };
|
|
104
|
+
async function main() {
|
|
105
|
+
const argv = process.argv.slice(2);
|
|
106
|
+
const [cmd, ...rest] = argv;
|
|
107
|
+
try {
|
|
108
|
+
if (cmd === '-h' || cmd === '--help' || cmd === 'help')
|
|
109
|
+
return printHelp();
|
|
110
|
+
if (!cmd)
|
|
111
|
+
return await cmdUse(rest);
|
|
112
|
+
if (cmd === '-v' || cmd === '--version') {
|
|
113
|
+
console.log(VERSION);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// 隐藏命令:供 shell 补全脚本回调,输出候选项(每行一个)。
|
|
117
|
+
if (cmd === '__complete') {
|
|
118
|
+
printCandidates(rest);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
switch (cmd) {
|
|
122
|
+
case 'list':
|
|
123
|
+
case 'ls': return cmdList();
|
|
124
|
+
case 'presets': return cmdPresets();
|
|
125
|
+
case 'create': {
|
|
126
|
+
await cmdCreate(rest);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
case 'edit': {
|
|
130
|
+
finishStandalone(await cmdEdit(rest));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
case 'remove':
|
|
134
|
+
case 'rm': {
|
|
135
|
+
finishStandalone(await cmdRemove(rest));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
case 'common': return cmdCommon();
|
|
139
|
+
case 'show': return cmdShow(rest);
|
|
140
|
+
case 'config': return await cmdConfig(rest);
|
|
141
|
+
case 'use': return await cmdUse(rest);
|
|
142
|
+
case 'completion': return cmdCompletion(rest);
|
|
143
|
+
default:
|
|
144
|
+
if (cmd.startsWith('-')) {
|
|
145
|
+
// 裸 ccs 直接跟参数(如 `ccs --dangerously-skip-permissions`):
|
|
146
|
+
// 当作 `ccs use <args>` 处理——交互选供应商后透传给 claude。
|
|
147
|
+
// (-h/--help/-v/--version 已在上面先行处理。)
|
|
148
|
+
return cmdUse([cmd, ...rest]);
|
|
149
|
+
}
|
|
150
|
+
return cmdLaunch(cmd, rest);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
if (e instanceof Cancel) {
|
|
155
|
+
ui.cancel(t('common.cancelled'));
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
console.error(`\x1b[31m${t('error.generic', { msg: e.message })}\x1b[0m`);
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// ---------- launch ----------
|
|
163
|
+
/**
|
|
164
|
+
* 独立命令(ccs edit/remove)的终态输出:清屏后打印一行结果消息。
|
|
165
|
+
* 菜单循环(cmdUse)路径不走这里——那里由 picker 顶部横幅显示结果。
|
|
166
|
+
*/
|
|
167
|
+
function finishStandalone(msg) {
|
|
168
|
+
if (!msg)
|
|
169
|
+
return;
|
|
170
|
+
clearScreen();
|
|
171
|
+
console.log(msg);
|
|
172
|
+
}
|
|
173
|
+
function cmdLaunch(name, rest) {
|
|
174
|
+
const dry = rest.includes('--dry-run');
|
|
175
|
+
const forwarded = rest.filter((a) => a !== '--dry-run');
|
|
176
|
+
if (dry)
|
|
177
|
+
return dryRun(name, forwarded);
|
|
178
|
+
launch(name, forwarded);
|
|
179
|
+
}
|
|
180
|
+
function cmdLaunchDirect(rest) {
|
|
181
|
+
const dry = rest.includes('--dry-run');
|
|
182
|
+
const forwarded = rest.filter((a) => a !== '--dry-run');
|
|
183
|
+
if (dry)
|
|
184
|
+
return dryRunDirect(forwarded);
|
|
185
|
+
launchDirect(forwarded);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* 让用户从现有配置中挑一个(用于菜单里的 edit / remove)。
|
|
189
|
+
* 无配置时提示并返回 null;取消(Esc)抛 Cancel 由调用方决定是否回菜单。
|
|
190
|
+
*/
|
|
191
|
+
async function pickExistingProvider(message) {
|
|
192
|
+
const names = listProviders();
|
|
193
|
+
if (!names.length) {
|
|
194
|
+
ui.log.message(t('list.empty'));
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
const last = getLastUsed();
|
|
198
|
+
const items = names.map((n) => ({
|
|
199
|
+
value: n,
|
|
200
|
+
label: n,
|
|
201
|
+
hint: n === last ? t('list.lastUsed') : '',
|
|
202
|
+
}));
|
|
203
|
+
return ui.picker({ message, items, initialValue: items[0].value });
|
|
204
|
+
}
|
|
205
|
+
async function cmdUse(rest) {
|
|
206
|
+
// 循环菜单:选供应商/direct/create 会启动并返回;
|
|
207
|
+
// 选 edit/remove 完成后回到菜单继续,子流程取消(Esc)也回菜单而非退出。
|
|
208
|
+
// statusMessage:edit/remove 完成后由下一轮 picker 顶部横幅显示一个周期。
|
|
209
|
+
let statusMessage;
|
|
210
|
+
for (;;) {
|
|
211
|
+
const names = listProviders();
|
|
212
|
+
const last = getLastUsed();
|
|
213
|
+
// 两个版块:供应商区(可过滤/可滚动)+ 操作区(固定不过滤)。
|
|
214
|
+
const items = [];
|
|
215
|
+
let initial;
|
|
216
|
+
for (const n of names) {
|
|
217
|
+
const v = { kind: 'provider', name: n };
|
|
218
|
+
items.push({ value: v, label: n, hint: n === last ? t('list.lastUsed') : '' });
|
|
219
|
+
if (n === last)
|
|
220
|
+
initial = v;
|
|
221
|
+
}
|
|
222
|
+
const actions = [
|
|
223
|
+
{ value: { kind: 'direct' }, label: t('use.direct'), hint: t('use.directHint') },
|
|
224
|
+
{ value: { kind: 'create' }, label: t('use.create'), hint: t('use.createHint') },
|
|
225
|
+
];
|
|
226
|
+
if (names.length) {
|
|
227
|
+
actions.push({ value: { kind: 'edit' }, label: t('use.edit'), hint: t('use.editHint') }, { value: { kind: 'remove' }, label: t('use.remove'), hint: t('use.removeHint') });
|
|
228
|
+
}
|
|
229
|
+
// 默认高亮上次使用的供应商,否则第一项。
|
|
230
|
+
if (!initial)
|
|
231
|
+
initial = (items[0] ?? actions[0])?.value;
|
|
232
|
+
const pickerOpts = { message: t('use.select'), items, actions };
|
|
233
|
+
if (initial)
|
|
234
|
+
pickerOpts.initialValue = initial;
|
|
235
|
+
if (statusMessage)
|
|
236
|
+
pickerOpts.statusMessage = statusMessage;
|
|
237
|
+
const picked = await ui.picker(pickerOpts);
|
|
238
|
+
statusMessage = undefined; // 横幅只显示一个周期
|
|
239
|
+
if (picked.kind === 'provider')
|
|
240
|
+
return cmdLaunch(picked.name, rest);
|
|
241
|
+
if (picked.kind === 'direct')
|
|
242
|
+
return cmdLaunchDirect(rest);
|
|
243
|
+
if (picked.kind === 'create') {
|
|
244
|
+
// 创建完成后立即用新供应商启动;创建被取消则回菜单。
|
|
245
|
+
const created = await cmdCreate(rest);
|
|
246
|
+
if (created)
|
|
247
|
+
return cmdLaunch(created, rest);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (picked.kind === 'edit') {
|
|
251
|
+
try {
|
|
252
|
+
const name = await pickExistingProvider(t('use.editSelect'));
|
|
253
|
+
if (name) {
|
|
254
|
+
const msg = await cmdEdit([name]);
|
|
255
|
+
if (msg)
|
|
256
|
+
statusMessage = msg;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch (e) {
|
|
260
|
+
if (e instanceof Cancel) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
throw e;
|
|
264
|
+
}
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (picked.kind === 'remove') {
|
|
268
|
+
try {
|
|
269
|
+
const name = await pickExistingProvider(t('use.removeSelect'));
|
|
270
|
+
if (name) {
|
|
271
|
+
const msg = await cmdRemove([name]);
|
|
272
|
+
if (msg)
|
|
273
|
+
statusMessage = msg;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch (e) {
|
|
277
|
+
if (e instanceof Cancel) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
throw e;
|
|
281
|
+
}
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// ---------- listing ----------
|
|
287
|
+
function cmdList() {
|
|
288
|
+
const names = listProviders();
|
|
289
|
+
if (!names.length) {
|
|
290
|
+
console.log(t('list.empty'));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const last = getLastUsed();
|
|
294
|
+
console.log(t('list.header'));
|
|
295
|
+
for (const n of names)
|
|
296
|
+
console.log(` ${n === last ? '*' : ' '} ${n}`);
|
|
297
|
+
console.log(`\n${t('list.summary', { count: names.length })}`);
|
|
298
|
+
}
|
|
299
|
+
function cmdPresets() {
|
|
300
|
+
const list = presetList();
|
|
301
|
+
console.log(t('presets.header'));
|
|
302
|
+
for (const p of list) {
|
|
303
|
+
console.log(` ${p.key.padEnd(20)} ${p.label} ${p.baseUrl || t('presets.fillUrl')}`);
|
|
304
|
+
}
|
|
305
|
+
console.log(`\n${t('presets.footer')}`);
|
|
306
|
+
console.log(t('presets.userFile'));
|
|
307
|
+
}
|
|
308
|
+
// ---------- completion ----------
|
|
309
|
+
/** 隐藏命令 `ccs __complete <words...>`:逐行打印候选。 */
|
|
310
|
+
function printCandidates(words) {
|
|
311
|
+
for (const c of completeCandidates(words))
|
|
312
|
+
console.log(c);
|
|
313
|
+
}
|
|
314
|
+
/** `ccs completion <shell>`:输出补全脚本或提示。 */
|
|
315
|
+
function cmdCompletion(rest) {
|
|
316
|
+
const shell = rest[0];
|
|
317
|
+
if (!shell) {
|
|
318
|
+
console.log(completionHelp());
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const script = completionScript(shell);
|
|
322
|
+
if (!script) {
|
|
323
|
+
console.log(completionHelp(shell));
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
process.stdout.write(script);
|
|
327
|
+
}
|
|
328
|
+
// ---------- create / edit / remove ----------
|
|
329
|
+
function validateName(name) {
|
|
330
|
+
if (!name || /[\\/\s]/.test(name) || name.includes('..')) {
|
|
331
|
+
throw new Error(t('error.invalidName', { name }));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* 配置名校验器(供 inkText 的 validate 内联使用)。
|
|
336
|
+
* 检查非空、非法字符、重名——重名即时提示,引导用户改名而非直接失败。
|
|
337
|
+
* 返回错误消息字符串,或 undefined 表示通过。
|
|
338
|
+
*/
|
|
339
|
+
function nameValidator(v) {
|
|
340
|
+
const name = (v || '').trim();
|
|
341
|
+
if (!name)
|
|
342
|
+
return t('create.customNameValidate');
|
|
343
|
+
if (/[\\/\s]/.test(name) || name.includes('..'))
|
|
344
|
+
return t('error.invalidName', { name });
|
|
345
|
+
if (providerExists(name))
|
|
346
|
+
return t('error.exists', { name });
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
async function cmdCreate(rest) {
|
|
350
|
+
const nameArg = rest[0];
|
|
351
|
+
let name;
|
|
352
|
+
let preset = null;
|
|
353
|
+
if (nameArg) {
|
|
354
|
+
// ccs create <name> → 用给定名称创建(自定义空白表单)
|
|
355
|
+
validateName(nameArg);
|
|
356
|
+
name = nameArg;
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
// 交互:内置 or 自定义
|
|
360
|
+
const mode = await chooseCreateMode();
|
|
361
|
+
if (mode === 'builtin') {
|
|
362
|
+
const picked = await pickBuiltinPreset();
|
|
363
|
+
// 配置名默认取预设 key,可改——同一供应商可建多个账号配置
|
|
364
|
+
// (如 deepseek-api / deepseek-work),不再固定为预设名。
|
|
365
|
+
name = (await ui.inkText({
|
|
366
|
+
message: t('create.namePrompt', { default: picked.key }),
|
|
367
|
+
initialValue: picked.key,
|
|
368
|
+
validate: nameValidator,
|
|
369
|
+
})).trim();
|
|
370
|
+
preset = picked.preset;
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
name = (await ui.inkText({
|
|
374
|
+
message: t('create.customNamePrompt'),
|
|
375
|
+
validate: nameValidator,
|
|
376
|
+
})).trim();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// create 永远新建:重名直接拒绝。修改既有配置请用 ccs edit,删除用 ccs remove。
|
|
380
|
+
// (交互流程已内联检测;此处兜底覆盖 ccs create <name> 直传名称的情况。)
|
|
381
|
+
if (providerExists(name)) {
|
|
382
|
+
throw new Error(t('error.exists', { name }));
|
|
383
|
+
}
|
|
384
|
+
const result = await providerFormWithPreview({ initial: {}, preset, title: t('create.kindTitle', { name }) });
|
|
385
|
+
writeJSON(providerFile(name), result);
|
|
386
|
+
clearScreen();
|
|
387
|
+
console.log(t('create.created', { file: providerFile(name) }));
|
|
388
|
+
return name;
|
|
389
|
+
}
|
|
390
|
+
async function cmdEdit(rest) {
|
|
391
|
+
const name = rest[0];
|
|
392
|
+
const raw = rest.includes('--raw');
|
|
393
|
+
if (!name) {
|
|
394
|
+
console.error(t('usage.editName'));
|
|
395
|
+
process.exit(1);
|
|
396
|
+
}
|
|
397
|
+
if (!providerExists(name)) {
|
|
398
|
+
console.error(t('error.notFound', { name }));
|
|
399
|
+
process.exit(1);
|
|
400
|
+
}
|
|
401
|
+
if (raw) {
|
|
402
|
+
editRaw(providerFile(name));
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
405
|
+
const initial = readProvider(name) || {};
|
|
406
|
+
const result = await providerFormWithPreview({ initial, title: t('edit.title', { name }) });
|
|
407
|
+
writeJSON(providerFile(name), result);
|
|
408
|
+
return t('edit.updated', { file: providerFile(name) });
|
|
409
|
+
}
|
|
410
|
+
async function cmdRemove(rest) {
|
|
411
|
+
const name = rest[0];
|
|
412
|
+
if (!name) {
|
|
413
|
+
console.error(t('usage.removeName'));
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
if (!providerExists(name)) {
|
|
417
|
+
console.error(t('error.notFound', { name }));
|
|
418
|
+
process.exit(1);
|
|
419
|
+
}
|
|
420
|
+
const ok = await ui.inkConfirm({ message: t('remove.confirm', { name }), initialValue: false });
|
|
421
|
+
if (!ok)
|
|
422
|
+
return undefined;
|
|
423
|
+
removeProvider(name);
|
|
424
|
+
return t('remove.done', { name });
|
|
425
|
+
}
|
|
426
|
+
function editRaw(file) {
|
|
427
|
+
const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
|
|
428
|
+
spawnSync(editor, [file], { stdio: 'inherit' });
|
|
429
|
+
}
|
|
430
|
+
// ---------- common / show / config ----------
|
|
431
|
+
function cmdCommon() {
|
|
432
|
+
// 通用配置直接用 ~/.claude/settings.json,ccs 只读不写。
|
|
433
|
+
if (!existsSync(CLAUDE_SETTINGS_FILE)) {
|
|
434
|
+
writeFileSyncSafe(CLAUDE_SETTINGS_FILE, '{}\n');
|
|
435
|
+
console.log(t('common.createdEmpty', { file: CLAUDE_SETTINGS_FILE }));
|
|
436
|
+
}
|
|
437
|
+
console.log(t('common.openEditor'));
|
|
438
|
+
const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
|
|
439
|
+
spawnSync(editor, [CLAUDE_SETTINGS_FILE], { stdio: 'inherit' });
|
|
440
|
+
}
|
|
441
|
+
function cmdShow(rest) {
|
|
442
|
+
const name = rest[0];
|
|
443
|
+
if (!name) {
|
|
444
|
+
console.error(t('usage.showName'));
|
|
445
|
+
process.exit(1);
|
|
446
|
+
}
|
|
447
|
+
const settings = readProvider(name);
|
|
448
|
+
if (!settings) {
|
|
449
|
+
console.error(t('error.providerMissing', { name }));
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
console.log(JSON.stringify(redactSettings(settings), null, 2));
|
|
453
|
+
}
|
|
454
|
+
async function cmdConfig(rest) {
|
|
455
|
+
const [key, val] = rest;
|
|
456
|
+
if (key === 'locale')
|
|
457
|
+
return cmdConfigLocale(val);
|
|
458
|
+
if (!key) {
|
|
459
|
+
console.log(t('config.localeCurrent', { locale: detectLocale() }));
|
|
460
|
+
console.log(` ccs config locale [${LOCALES.map((l) => l.value).join(' | ')}]`);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
console.error(t('config.unknownKey', { key }));
|
|
464
|
+
process.exit(1);
|
|
465
|
+
}
|
|
466
|
+
async function cmdConfigLocale(val) {
|
|
467
|
+
const valid = LOCALES.map((l) => l.value);
|
|
468
|
+
if (val) {
|
|
469
|
+
if (!valid.includes(val)) {
|
|
470
|
+
console.error(t('config.localeInvalid', { opts: valid.join(', ') }));
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
473
|
+
setConfig({ locale: val });
|
|
474
|
+
console.log(t('config.localeSet', { locale: val }));
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const current = detectLocale();
|
|
478
|
+
const picked = await ui.inkSelect({
|
|
479
|
+
message: t('config.localePrompt'),
|
|
480
|
+
options: LOCALES.map((l) => ({ value: l.value, label: l.label, hint: l.value === current ? t('list.lastUsed') : '' })),
|
|
481
|
+
initialValue: valid.includes(current) ? current : 'en',
|
|
482
|
+
});
|
|
483
|
+
setConfig({ locale: picked });
|
|
484
|
+
console.log(t('config.localeSet', { locale: picked }));
|
|
485
|
+
}
|
|
486
|
+
// ---------- help ----------
|
|
487
|
+
function printHelp() {
|
|
488
|
+
const locale = detectLocale();
|
|
489
|
+
console.log(locale === 'zh-CN' ? helpZh : helpEn);
|
|
490
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 根据已输入词序列返回补全候选(已按当前光标词前缀过滤)。
|
|
3
|
+
*
|
|
4
|
+
* argv = 去掉 `ccs` 与 `__complete` 后的词,最后一个元素是当前光标词 cur(可为空串)。
|
|
5
|
+
*
|
|
6
|
+
* 注意:第一位置词同时容纳子命令与具名 provider(`ccs <name>` 直接启动),
|
|
7
|
+
* 这是该 CLI「子命令与 provider 名共享第一位置词空间」特性的自然映射。
|
|
8
|
+
*/
|
|
9
|
+
export declare function completeCandidates(argv: string[]): string[];
|
|
10
|
+
/**
|
|
11
|
+
* bash 补全脚本。`eval "$(ccs completion bash)"` 后生效。
|
|
12
|
+
* ccs 不在 PATH 时静默不注册;__complete 出错时静默返回。
|
|
13
|
+
* 注意:模板字面量中所有 shell 的 ${...} 必须转义为 \${...},避免被 JS 当成插值。
|
|
14
|
+
*/
|
|
15
|
+
export declare const bashCompletionScript = "# ccs bash completion\nif command -v ccs >/dev/null 2>&1; then\n _ccs_bash_complete() {\n local cur cands\n cur=\"${COMP_WORDS[COMP_CWORD]}\"\n # COMP_WORDS[1:] \u5DF2\u542B\u672B\u5C3E cur \u8BCD\uFF0C\u76F4\u63A5\u4F20\u7ED9 ccs __complete\n cands=$(ccs __complete \"${COMP_WORDS[@]:1}\" 2>/dev/null) || return\n COMPREPLY=($(compgen -W \"$cands\" -- \"$cur\"))\n }\n complete -F _ccs_bash_complete ccs\nfi\n";
|
|
16
|
+
/**
|
|
17
|
+
* zsh 补全脚本。`eval "$(ccs completion zsh)"` 后生效(需 compinit 已加载)。
|
|
18
|
+
* ccs 不在 PATH 时静默不注册。
|
|
19
|
+
* 注意:模板字面量中所有 shell 的 ${...} 必须转义为 \${...},避免被 JS 当成插值。
|
|
20
|
+
*/
|
|
21
|
+
export declare const zshCompletionScript = "# ccs zsh completion\nif command -v ccs >/dev/null 2>&1; then\n _ccs_zsh_complete() {\n local -a cands\n # ${words[@]:1}\uFF1A\u53BB\u6389 ccs \u672C\u8EAB\uFF0C\u672B\u5C3E\u5373\u5F53\u524D\u8BCD\n cands=(\"${(@f)$(ccs __complete \"${words[@]:1}\" 2>/dev/null)}\")\n compadd -- \"$@\" \"${cands[@]}\"\n }\n compdef _ccs_zsh_complete ccs\nfi\n";
|
|
22
|
+
/** 支持的 shell。 */
|
|
23
|
+
export declare const SUPPORTED_SHELLS: readonly ["bash", "zsh"];
|
|
24
|
+
/** 补全脚本按 shell 取;不支持时返回 null。 */
|
|
25
|
+
export declare function completionScript(shell: string): string | null;
|
|
26
|
+
/** `ccs completion` 无参/不支持的 shell 时的提示文本。 */
|
|
27
|
+
export declare function completionHelp(shell?: string): string;
|