@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
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell 补全:候选生成 + bash/zsh 脚本。
|
|
3
|
+
*
|
|
4
|
+
* 模式(cobra/oclif 风格):
|
|
5
|
+
* - `ccs __complete <words...>` 隐藏命令,按已输入词序列输出候选项(每行一个)。
|
|
6
|
+
* - `ccs completion <shell>` 输出对应 shell 的补全脚本,脚本运行时回调 `ccs __complete`。
|
|
7
|
+
*
|
|
8
|
+
* 候选数据复用 listProviders() / presetList(),单一真源,不重复维护配置名表。
|
|
9
|
+
* 本模块为纯函数(IO 委托 config/presets),便于单测。
|
|
10
|
+
*/
|
|
11
|
+
import { listProviders } from './config.js';
|
|
12
|
+
import { presetList } from './presets.js';
|
|
13
|
+
/** 子命令(含别名)。 */
|
|
14
|
+
const SUBCOMMANDS = ['list', 'ls', 'presets', 'create', 'edit', 'remove', 'rm', 'common', 'show', 'config', 'use'];
|
|
15
|
+
/** 全局 flag。 */
|
|
16
|
+
const GLOBAL_FLAGS = ['-h', '--help', '-v', '--version'];
|
|
17
|
+
/** 第一个参数位补 provider 名的子命令。 */
|
|
18
|
+
const PROVIDER_CMDS = new Set(['edit', 'remove', 'rm', 'show']);
|
|
19
|
+
/**
|
|
20
|
+
* 根据已输入词序列返回补全候选(已按当前光标词前缀过滤)。
|
|
21
|
+
*
|
|
22
|
+
* argv = 去掉 `ccs` 与 `__complete` 后的词,最后一个元素是当前光标词 cur(可为空串)。
|
|
23
|
+
*
|
|
24
|
+
* 注意:第一位置词同时容纳子命令与具名 provider(`ccs <name>` 直接启动),
|
|
25
|
+
* 这是该 CLI「子命令与 provider 名共享第一位置词空间」特性的自然映射。
|
|
26
|
+
*/
|
|
27
|
+
export function completeCandidates(argv) {
|
|
28
|
+
const cur = argv[argv.length - 1] ?? '';
|
|
29
|
+
const prev = argv.slice(0, -1);
|
|
30
|
+
// 第一个位置词未输入(prev 为空)→ 子命令 + provider 名 + 全局 flag
|
|
31
|
+
if (prev.length === 0) {
|
|
32
|
+
return filterByPrefix([...SUBCOMMANDS, ...listProviders(), ...GLOBAL_FLAGS], cur);
|
|
33
|
+
}
|
|
34
|
+
const head = prev[0]; // noUncheckedIndexedAccess:prev 非空,[0] 必存在
|
|
35
|
+
// 子命令的第一个参数位
|
|
36
|
+
if (prev.length === 1) {
|
|
37
|
+
if (PROVIDER_CMDS.has(head))
|
|
38
|
+
return filterByPrefix(listProviders(), cur);
|
|
39
|
+
if (head === 'create')
|
|
40
|
+
return filterByPrefix(presetList().map((p) => p.key), cur);
|
|
41
|
+
if (head === 'config')
|
|
42
|
+
return filterByPrefix(['locale'], cur);
|
|
43
|
+
// list/presets/common/use 无位置参数补全;ccs <name> 已在 prev.length===0 覆盖
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
// config locale <cur>(仅当 cur 处于 locale 取值位,即 prev 恰为 ['config','locale'])
|
|
47
|
+
if (head === 'config' && prev.length === 2 && prev[1] === 'locale') {
|
|
48
|
+
return filterByPrefix(['en', 'zh-CN'], cur);
|
|
49
|
+
}
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
/** 按前缀过滤候选;空前缀返回全部。 */
|
|
53
|
+
function filterByPrefix(cands, prefix) {
|
|
54
|
+
if (!prefix)
|
|
55
|
+
return cands;
|
|
56
|
+
return cands.filter((c) => c.startsWith(prefix));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* bash 补全脚本。`eval "$(ccs completion bash)"` 后生效。
|
|
60
|
+
* ccs 不在 PATH 时静默不注册;__complete 出错时静默返回。
|
|
61
|
+
* 注意:模板字面量中所有 shell 的 ${...} 必须转义为 \${...},避免被 JS 当成插值。
|
|
62
|
+
*/
|
|
63
|
+
export const bashCompletionScript = `# ccs bash completion
|
|
64
|
+
if command -v ccs >/dev/null 2>&1; then
|
|
65
|
+
_ccs_bash_complete() {
|
|
66
|
+
local cur cands
|
|
67
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
68
|
+
# COMP_WORDS[1:] 已含末尾 cur 词,直接传给 ccs __complete
|
|
69
|
+
cands=$(ccs __complete "\${COMP_WORDS[@]:1}" 2>/dev/null) || return
|
|
70
|
+
COMPREPLY=($(compgen -W "$cands" -- "$cur"))
|
|
71
|
+
}
|
|
72
|
+
complete -F _ccs_bash_complete ccs
|
|
73
|
+
fi
|
|
74
|
+
`;
|
|
75
|
+
/**
|
|
76
|
+
* zsh 补全脚本。`eval "$(ccs completion zsh)"` 后生效(需 compinit 已加载)。
|
|
77
|
+
* ccs 不在 PATH 时静默不注册。
|
|
78
|
+
* 注意:模板字面量中所有 shell 的 ${...} 必须转义为 \${...},避免被 JS 当成插值。
|
|
79
|
+
*/
|
|
80
|
+
export const zshCompletionScript = `# ccs zsh completion
|
|
81
|
+
if command -v ccs >/dev/null 2>&1; then
|
|
82
|
+
_ccs_zsh_complete() {
|
|
83
|
+
local -a cands
|
|
84
|
+
# \${words[@]:1}:去掉 ccs 本身,末尾即当前词
|
|
85
|
+
cands=("\${(@f)\$(ccs __complete "\${words[@]:1}" 2>/dev/null)}")
|
|
86
|
+
compadd -- "\$@" "\${cands[@]}"
|
|
87
|
+
}
|
|
88
|
+
compdef _ccs_zsh_complete ccs
|
|
89
|
+
fi
|
|
90
|
+
`;
|
|
91
|
+
/** 支持的 shell。 */
|
|
92
|
+
export const SUPPORTED_SHELLS = ['bash', 'zsh'];
|
|
93
|
+
/** 补全脚本按 shell 取;不支持时返回 null。 */
|
|
94
|
+
export function completionScript(shell) {
|
|
95
|
+
if (shell === 'bash')
|
|
96
|
+
return bashCompletionScript;
|
|
97
|
+
if (shell === 'zsh')
|
|
98
|
+
return zshCompletionScript;
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
/** `ccs completion` 无参/不支持的 shell 时的提示文本。 */
|
|
102
|
+
export function completionHelp(shell) {
|
|
103
|
+
const supported = SUPPORTED_SHELLS.join(', ');
|
|
104
|
+
if (shell && !SUPPORTED_SHELLS.includes(shell)) {
|
|
105
|
+
return `Unsupported shell: ${shell}. Supported: ${supported}.`;
|
|
106
|
+
}
|
|
107
|
+
return [
|
|
108
|
+
'Usage: ccs completion <shell>',
|
|
109
|
+
` Outputs a shell completion script. Supported: ${supported}.`,
|
|
110
|
+
'',
|
|
111
|
+
'Install (bash): eval "$(ccs completion bash)" # or append to ~/.bashrc',
|
|
112
|
+
'Install (zsh): eval "$(ccs completion zsh)" # or append to ~/.zshrc',
|
|
113
|
+
'',
|
|
114
|
+
'Global install (npm i -g @wayfarer35/ccs) auto-writes the eval line into your rc.',
|
|
115
|
+
].join('\n');
|
|
116
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const CCS_DIR: string;
|
|
2
|
+
export declare const PROVIDERS_DIR: string;
|
|
3
|
+
export declare const CONFIG_FILE: string;
|
|
4
|
+
export declare const LASTUSED_FILE: string;
|
|
5
|
+
export declare const CACHE_DIR: string;
|
|
6
|
+
/** 通用配置来源:直接用 Claude Code 自身的 settings.json,ccs 只读不写。 */
|
|
7
|
+
export declare const CLAUDE_SETTINGS_FILE: string;
|
|
8
|
+
export declare const PROVIDER_SUFFIX = ".settings.json";
|
|
9
|
+
export declare function ensureDirs(): void;
|
|
10
|
+
/** 读取并解析 JSON 文件;ENOENT 返回 fallback,其余解析错误抛出。 */
|
|
11
|
+
export declare function readJSON<T>(file: string, fallback?: T | null): T | null;
|
|
12
|
+
export declare function writeJSON(file: string, obj: unknown): void;
|
|
13
|
+
/** 写入原始字符串内容(自动创建父目录),用于编辑 ~/.claude/settings.json 等已有文件。 */
|
|
14
|
+
export declare function writeFileSyncSafe(file: string, content: string): void;
|
|
15
|
+
export declare function providerFile(name: string): string;
|
|
16
|
+
export declare function readProvider<T = unknown>(name: string): T | null;
|
|
17
|
+
export declare function providerExists(name: string): boolean;
|
|
18
|
+
export declare function listProviders(): string[];
|
|
19
|
+
export declare function getLastUsed(): string | null;
|
|
20
|
+
export declare function setLastUsed(name: string): void;
|
|
21
|
+
export declare function removeProvider(name: string): void;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { mkdirSync, readFileSync, writeFileSync, readdirSync, existsSync, rmSync, } from 'node:fs';
|
|
4
|
+
export const CCS_DIR = join(homedir(), '.ccs');
|
|
5
|
+
export const PROVIDERS_DIR = join(CCS_DIR, 'providers');
|
|
6
|
+
export const CONFIG_FILE = join(CCS_DIR, 'config.json');
|
|
7
|
+
export const LASTUSED_FILE = join(CCS_DIR, '.lastused');
|
|
8
|
+
export const CACHE_DIR = join(CCS_DIR, '.cache');
|
|
9
|
+
/** 通用配置来源:直接用 Claude Code 自身的 settings.json,ccs 只读不写。 */
|
|
10
|
+
export const CLAUDE_SETTINGS_FILE = join(homedir(), '.claude', 'settings.json');
|
|
11
|
+
export const PROVIDER_SUFFIX = '.settings.json';
|
|
12
|
+
export function ensureDirs() {
|
|
13
|
+
mkdirSync(PROVIDERS_DIR, { recursive: true });
|
|
14
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
/** 读取并解析 JSON 文件;ENOENT 返回 fallback,其余解析错误抛出。 */
|
|
17
|
+
export function readJSON(file, fallback = null) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
if (e.code === 'ENOENT')
|
|
23
|
+
return fallback;
|
|
24
|
+
throw new Error(`Failed to parse JSON / 解析 JSON 失败: ${file}\n${e.message}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function writeJSON(file, obj) {
|
|
28
|
+
mkdirSync(join(file, '..'), { recursive: true });
|
|
29
|
+
writeFileSync(file, JSON.stringify(obj, null, 2) + '\n');
|
|
30
|
+
}
|
|
31
|
+
/** 写入原始字符串内容(自动创建父目录),用于编辑 ~/.claude/settings.json 等已有文件。 */
|
|
32
|
+
export function writeFileSyncSafe(file, content) {
|
|
33
|
+
mkdirSync(join(file, '..'), { recursive: true });
|
|
34
|
+
writeFileSync(file, content);
|
|
35
|
+
}
|
|
36
|
+
export function providerFile(name) {
|
|
37
|
+
return join(PROVIDERS_DIR, `${name}${PROVIDER_SUFFIX}`);
|
|
38
|
+
}
|
|
39
|
+
export function readProvider(name) {
|
|
40
|
+
return readJSON(providerFile(name), null);
|
|
41
|
+
}
|
|
42
|
+
export function providerExists(name) {
|
|
43
|
+
return existsSync(providerFile(name));
|
|
44
|
+
}
|
|
45
|
+
export function listProviders() {
|
|
46
|
+
ensureDirs();
|
|
47
|
+
let entries = [];
|
|
48
|
+
try {
|
|
49
|
+
entries = readdirSync(PROVIDERS_DIR);
|
|
50
|
+
}
|
|
51
|
+
catch { /* dir missing */ }
|
|
52
|
+
return entries
|
|
53
|
+
.filter((f) => f.endsWith(PROVIDER_SUFFIX))
|
|
54
|
+
.map((f) => f.slice(0, -PROVIDER_SUFFIX.length))
|
|
55
|
+
.sort((a, b) => a.localeCompare(b));
|
|
56
|
+
}
|
|
57
|
+
export function getLastUsed() {
|
|
58
|
+
try {
|
|
59
|
+
return readFileSync(LASTUSED_FILE, 'utf8').trim() || null;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function setLastUsed(name) {
|
|
66
|
+
try {
|
|
67
|
+
writeFileSync(LASTUSED_FILE, name);
|
|
68
|
+
}
|
|
69
|
+
catch { /* best-effort */ }
|
|
70
|
+
}
|
|
71
|
+
export function removeProvider(name) {
|
|
72
|
+
rmSync(providerFile(name), { force: true });
|
|
73
|
+
}
|
package/dist/form.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { EffortLevel, FormState, Preset, ProviderSettings, Tier } from './types.js';
|
|
2
|
+
export declare const TIERS: Array<{
|
|
3
|
+
value: Tier;
|
|
4
|
+
label: string;
|
|
5
|
+
}>;
|
|
6
|
+
export declare const ALIAS_TIERS: readonly ["FABLE", "OPUS", "SONNET", "HAIKU"];
|
|
7
|
+
export declare const EFFORT_LEVELS: readonly EffortLevel[];
|
|
8
|
+
/** 解析 env 里的布尔值('1'/'true' → true,'0'/'false' → false,空 → def)。 */
|
|
9
|
+
export declare function parseBoolEnv(v: string | undefined, def: boolean): boolean;
|
|
10
|
+
/** 解析 env 里的正整数(非法 → def)。 */
|
|
11
|
+
export declare function parseNumEnv(v: string | undefined, def: number): number;
|
|
12
|
+
/** 解析 env 里的 effort 档位(非法/空 → def)。大小写不敏感。 */
|
|
13
|
+
export declare function parseEffortEnv(v: string | undefined, def: EffortLevel): EffortLevel;
|
|
14
|
+
/** initState 输入:已有 settings(编辑时)与 preset(创建时预填)。 */
|
|
15
|
+
interface InitInput {
|
|
16
|
+
env?: Record<string, string>;
|
|
17
|
+
model?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 第一步:选内置供应商还是自定义。
|
|
21
|
+
* @returns {'builtin' | 'custom'}
|
|
22
|
+
*/
|
|
23
|
+
export declare function chooseCreateMode(): Promise<'builtin' | 'custom'>;
|
|
24
|
+
/**
|
|
25
|
+
* 选择内置供应商预设。返回 { key, preset }。
|
|
26
|
+
*/
|
|
27
|
+
export declare function pickBuiltinPreset(): Promise<{
|
|
28
|
+
key: string;
|
|
29
|
+
preset: Preset;
|
|
30
|
+
}>;
|
|
31
|
+
/** 向后兼容:旧 pickPreset 调用点(如有)。 */
|
|
32
|
+
export declare function pickPreset(): Promise<{
|
|
33
|
+
key: string;
|
|
34
|
+
preset: Preset | null;
|
|
35
|
+
}>;
|
|
36
|
+
/**
|
|
37
|
+
* 从 initial(编辑时已有 settings)与 preset(创建时预填)初始化表单状态。
|
|
38
|
+
* 三项 options 的取值优先级:existing env > preset.options > DEFAULT_OPTIONS。
|
|
39
|
+
* 档位别名从 preset.model.tiers 读取(取代旧的 preset.models)。
|
|
40
|
+
*/
|
|
41
|
+
export declare function initState(initial: InitInput, preset: Preset | null): FormState;
|
|
42
|
+
/**
|
|
43
|
+
* 由表单状态构建 settings 片段 { env, model? }。
|
|
44
|
+
* 三项 CLAUDE_CODE_* 始终写入 env(显式可见,每次启动生效)。
|
|
45
|
+
* authMethod 决定写入 ANTHROPIC_AUTH_TOKEN 还是 ANTHROPIC_API_KEY。
|
|
46
|
+
* customParams 解析为 JSON 后,顶层 key-value 合并到 env。
|
|
47
|
+
*/
|
|
48
|
+
export declare function buildResult(state: FormState): ProviderSettings;
|
|
49
|
+
/** 提交前校验:必填项是否齐全。返回错误消息或 null。 */
|
|
50
|
+
export declare function validateState(state: FormState): string | null;
|
|
51
|
+
/**
|
|
52
|
+
* 持久化 Tab 表单(ink):API Key / Models / Options / Review(预览+提交)。
|
|
53
|
+
* Tab 切换时下方内容跟随变化,字段直接原地编辑,无需 Enter 进入。
|
|
54
|
+
* create/edit 共用。委托给 formUi.runProviderForm。
|
|
55
|
+
*
|
|
56
|
+
* @param opts
|
|
57
|
+
* @param opts.initial 已有 settings(edit 时预填)
|
|
58
|
+
* @param opts.preset 预设(create 时预填,可为 null=自定义)
|
|
59
|
+
* @returns 最终 settings 片段
|
|
60
|
+
*/
|
|
61
|
+
export declare function providerFormWithPreview(opts?: {
|
|
62
|
+
initial?: InitInput;
|
|
63
|
+
preset?: Preset | null;
|
|
64
|
+
title?: string;
|
|
65
|
+
}): Promise<ProviderSettings>;
|
|
66
|
+
export {};
|
package/dist/form.js
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { ui } from './tui.js';
|
|
2
|
+
import { getPresets, CUSTOM_KEY } from './presets.js';
|
|
3
|
+
import { t } from './i18n.js';
|
|
4
|
+
import { runProviderForm } from './formUi.js';
|
|
5
|
+
export const TIERS = [
|
|
6
|
+
{ value: 'opus', label: 'opus' },
|
|
7
|
+
{ value: 'sonnet', label: 'sonnet' },
|
|
8
|
+
{ value: 'haiku', label: 'haiku' },
|
|
9
|
+
{ value: 'fable', label: 'fable' },
|
|
10
|
+
];
|
|
11
|
+
// 环境变量后缀的大小写档位(对应 ANTHROPIC_DEFAULT_<TIER>_MODEL)
|
|
12
|
+
export const ALIAS_TIERS = ['FABLE', 'OPUS', 'SONNET', 'HAIKU'];
|
|
13
|
+
// 推理强度档位(对应 CLAUDE_CODE_EFFORT_LEVEL),与 Claude Code 内置取值一致。
|
|
14
|
+
export const EFFORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
15
|
+
// 三项 CLAUDE_CODE_* 配置的默认值。
|
|
16
|
+
// - attributionHeader: 默认关闭(尊重用户全局已禁用 attribution 的设置)。
|
|
17
|
+
// - disableNonEssentialTraffic: 默认开启(减少遥测/分析流量)。
|
|
18
|
+
// - autoCompactWindow: 默认 200k tokens。
|
|
19
|
+
// - effort: 默认 max(最高推理强度),可在供应商上覆盖。
|
|
20
|
+
const DEFAULT_OPTIONS = {
|
|
21
|
+
attributionHeader: false,
|
|
22
|
+
disableNonEssentialTraffic: true,
|
|
23
|
+
autoCompactWindow: 200000,
|
|
24
|
+
effort: 'max',
|
|
25
|
+
};
|
|
26
|
+
const MODE_BUILTIN = 'builtin';
|
|
27
|
+
const MODE_CUSTOM = 'custom';
|
|
28
|
+
function aliasKey(tierUpper) {
|
|
29
|
+
return `ANTHROPIC_DEFAULT_${tierUpper}_MODEL`;
|
|
30
|
+
}
|
|
31
|
+
/** /model 选择器中的显示名 key(与 _MODEL 配套,仅显示用,不参与 API 请求)。 */
|
|
32
|
+
function nameKey(tierUpper) {
|
|
33
|
+
return `ANTHROPIC_DEFAULT_${tierUpper}_MODEL_NAME`;
|
|
34
|
+
}
|
|
35
|
+
/** 剥离 [1m] 上下文后缀,用于派生显示名(claude 发请求前会自行剥离 _MODEL 的后缀,显示名无需带)。 */
|
|
36
|
+
function strip1m(v) {
|
|
37
|
+
return v.replace(/\[1m\]$/i, '').trim();
|
|
38
|
+
}
|
|
39
|
+
/** 是否存在任一档位别名 env(用于判断当前配置处于档位别名模式)。 */
|
|
40
|
+
function hasAliasEnv(env) {
|
|
41
|
+
return ALIAS_TIERS.some((tier) => env[aliasKey(tier)]);
|
|
42
|
+
}
|
|
43
|
+
/** 解析 env 里的布尔值('1'/'true' → true,'0'/'false' → false,空 → def)。 */
|
|
44
|
+
export function parseBoolEnv(v, def) {
|
|
45
|
+
if (v === undefined || v === null || v === '')
|
|
46
|
+
return def;
|
|
47
|
+
const s = String(v).trim().toLowerCase();
|
|
48
|
+
if (s === '1' || s === 'true')
|
|
49
|
+
return true;
|
|
50
|
+
if (s === '0' || s === 'false')
|
|
51
|
+
return false;
|
|
52
|
+
return def;
|
|
53
|
+
}
|
|
54
|
+
/** 解析 env 里的正整数(非法 → def)。 */
|
|
55
|
+
export function parseNumEnv(v, def) {
|
|
56
|
+
if (v === undefined || v === null || v === '')
|
|
57
|
+
return def;
|
|
58
|
+
const n = Number(String(v).trim());
|
|
59
|
+
return Number.isInteger(n) && n > 0 ? n : def;
|
|
60
|
+
}
|
|
61
|
+
/** 解析 env 里的 effort 档位(非法/空 → def)。大小写不敏感。 */
|
|
62
|
+
export function parseEffortEnv(v, def) {
|
|
63
|
+
if (v === undefined || v === null || v === '')
|
|
64
|
+
return def;
|
|
65
|
+
const s = String(v).trim().toLowerCase();
|
|
66
|
+
return EFFORT_LEVELS.includes(s) ? s : def;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 第一步:选内置供应商还是自定义。
|
|
70
|
+
* @returns {'builtin' | 'custom'}
|
|
71
|
+
*/
|
|
72
|
+
export async function chooseCreateMode() {
|
|
73
|
+
return ui.inkSelect({
|
|
74
|
+
message: t('create.kindPrompt'),
|
|
75
|
+
options: [
|
|
76
|
+
{ value: MODE_BUILTIN, label: t('create.kindBuiltin') },
|
|
77
|
+
{ value: MODE_CUSTOM, label: t('create.kindCustom') },
|
|
78
|
+
],
|
|
79
|
+
initialValue: MODE_BUILTIN,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 选择内置供应商预设。返回 { key, preset }。
|
|
84
|
+
*/
|
|
85
|
+
export async function pickBuiltinPreset() {
|
|
86
|
+
const presets = getPresets();
|
|
87
|
+
const options = Object.entries(presets).map(([key, p]) => ({
|
|
88
|
+
value: key,
|
|
89
|
+
label: p.label,
|
|
90
|
+
hint: p.baseUrl || t('presets.fillUrl'),
|
|
91
|
+
}));
|
|
92
|
+
const key = await ui.inkSelect({
|
|
93
|
+
message: t('create.builtinPrompt'),
|
|
94
|
+
options,
|
|
95
|
+
initialValue: Object.keys(presets)[0],
|
|
96
|
+
});
|
|
97
|
+
return { key, preset: presets[key] };
|
|
98
|
+
}
|
|
99
|
+
/** 向后兼容:旧 pickPreset 调用点(如有)。 */
|
|
100
|
+
export async function pickPreset() {
|
|
101
|
+
const mode = await chooseCreateMode();
|
|
102
|
+
if (mode === MODE_BUILTIN)
|
|
103
|
+
return pickBuiltinPreset();
|
|
104
|
+
return { key: CUSTOM_KEY, preset: null };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 推断本次表单应默认进入哪种模式。
|
|
108
|
+
* 默认档位别名模式(档位全部显示,配不配置由用户决定,预设只负责预填),
|
|
109
|
+
* 仅在编辑已有「单一 ANTHROPIC_MODEL」配置时回退到单模型模式。
|
|
110
|
+
* - 已有档位别名 env → 别名模式
|
|
111
|
+
* - 已有 ANTHROPIC_MODEL(且无别名)→ 单模型模式
|
|
112
|
+
* - 其余(含创建/预设,无论 preset 是否带 models)→ 别名模式
|
|
113
|
+
*/
|
|
114
|
+
function detectAliasMode(initial, preset) {
|
|
115
|
+
const env = initial.env || {};
|
|
116
|
+
if (hasAliasEnv(env))
|
|
117
|
+
return true;
|
|
118
|
+
if (env.ANTHROPIC_MODEL)
|
|
119
|
+
return false;
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 从 initial(编辑时已有 settings)与 preset(创建时预填)初始化表单状态。
|
|
124
|
+
* 三项 options 的取值优先级:existing env > preset.options > DEFAULT_OPTIONS。
|
|
125
|
+
* 档位别名从 preset.model.tiers 读取(取代旧的 preset.models)。
|
|
126
|
+
*/
|
|
127
|
+
export function initState(initial, preset) {
|
|
128
|
+
const env = initial.env || {};
|
|
129
|
+
const existingKey = env.ANTHROPIC_AUTH_TOKEN || env.ANTHROPIC_API_KEY || '';
|
|
130
|
+
const presetOptions = preset?.options || {};
|
|
131
|
+
const options = {
|
|
132
|
+
attributionHeader: parseBoolEnv(env.CLAUDE_CODE_ATTRIBUTION_HEADER, presetOptions.attributionHeader ?? DEFAULT_OPTIONS.attributionHeader),
|
|
133
|
+
disableNonEssentialTraffic: parseBoolEnv(env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC, presetOptions.disableNonEssentialTraffic ?? DEFAULT_OPTIONS.disableNonEssentialTraffic),
|
|
134
|
+
autoCompactWindow: parseNumEnv(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW, presetOptions.autoCompactWindow ?? DEFAULT_OPTIONS.autoCompactWindow),
|
|
135
|
+
effort: parseEffortEnv(env.CLAUDE_CODE_EFFORT_LEVEL, presetOptions.effort ?? DEFAULT_OPTIONS.effort),
|
|
136
|
+
};
|
|
137
|
+
const aliases = {};
|
|
138
|
+
const presetTiers = preset?.model?.tiers || {};
|
|
139
|
+
for (const tierUpper of ALIAS_TIERS) {
|
|
140
|
+
const k = aliasKey(tierUpper);
|
|
141
|
+
const tierLower = tierUpper.toLowerCase();
|
|
142
|
+
aliases[tierUpper] = env[k] ?? presetTiers[tierLower] ?? '';
|
|
143
|
+
}
|
|
144
|
+
const mode = detectAliasMode(initial, preset) ? 'alias' : 'single';
|
|
145
|
+
// 认证方式:编辑时检测 ANTHROPIC_API_KEY 存在则选 'api_key',否则 'auth_token'
|
|
146
|
+
const authMethod = env.ANTHROPIC_API_KEY ? 'api_key' : 'auth_token';
|
|
147
|
+
// 默认档位:从 preset.model.tier 读取,否则 'opus'
|
|
148
|
+
const tier = initial.model || preset?.model?.tier || 'opus';
|
|
149
|
+
// 单模型:从 preset.model.default 读取
|
|
150
|
+
const singleModel = env.ANTHROPIC_MODEL ?? preset?.model?.default ?? '';
|
|
151
|
+
return {
|
|
152
|
+
baseUrl: env.ANTHROPIC_BASE_URL ?? preset?.baseUrl ?? '',
|
|
153
|
+
existingKey,
|
|
154
|
+
apiKey: '',
|
|
155
|
+
keepExistingKey: !!existingKey,
|
|
156
|
+
mode,
|
|
157
|
+
authMethod,
|
|
158
|
+
tier,
|
|
159
|
+
aliases,
|
|
160
|
+
singleModel,
|
|
161
|
+
options,
|
|
162
|
+
customParams: '',
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* 由表单状态构建 settings 片段 { env, model? }。
|
|
167
|
+
* 三项 CLAUDE_CODE_* 始终写入 env(显式可见,每次启动生效)。
|
|
168
|
+
* authMethod 决定写入 ANTHROPIC_AUTH_TOKEN 还是 ANTHROPIC_API_KEY。
|
|
169
|
+
* customParams 解析为 JSON 后,顶层 key-value 合并到 env。
|
|
170
|
+
*/
|
|
171
|
+
export function buildResult(state) {
|
|
172
|
+
const env = { ANTHROPIC_BASE_URL: state.baseUrl.trim() };
|
|
173
|
+
// Key:根据 authMethod 写入不同的环境变量
|
|
174
|
+
// 同时把另一个 key 置为空串占位,覆盖主配置可能存在的残留值。
|
|
175
|
+
const keyEnv = state.authMethod === 'api_key' ? 'ANTHROPIC_API_KEY' : 'ANTHROPIC_AUTH_TOKEN';
|
|
176
|
+
const otherKeyEnv = state.authMethod === 'api_key' ? 'ANTHROPIC_AUTH_TOKEN' : 'ANTHROPIC_API_KEY';
|
|
177
|
+
if (state.apiKey && state.apiKey.trim()) {
|
|
178
|
+
env[keyEnv] = state.apiKey.trim();
|
|
179
|
+
}
|
|
180
|
+
else if (state.keepExistingKey && state.existingKey) {
|
|
181
|
+
env[keyEnv] = state.existingKey;
|
|
182
|
+
}
|
|
183
|
+
// 另一个 key 始终写入空串占位(覆盖主配置残留)
|
|
184
|
+
env[otherKeyEnv] = '';
|
|
185
|
+
if (state.mode === 'single') {
|
|
186
|
+
env.ANTHROPIC_MODEL = state.singleModel.trim();
|
|
187
|
+
}
|
|
188
|
+
else if (state.mode === 'alias') {
|
|
189
|
+
// 别名模式:显式写 ANTHROPIC_MODEL = ''(空串)。
|
|
190
|
+
// 必须显式置空——省略会让 ~/.claude/settings.json 里残留的 ANTHROPIC_MODEL 透传,
|
|
191
|
+
// 导致看到错误模型;而空串会覆盖残留值,又不参与模型解析,档位别名
|
|
192
|
+
// (ANTHROPIC_DEFAULT_*_MODEL)继续生效,会话内 /model 切换可用。
|
|
193
|
+
env.ANTHROPIC_MODEL = '';
|
|
194
|
+
for (const tierUpper of ALIAS_TIERS) {
|
|
195
|
+
const v = (state.aliases[tierUpper] || '').trim();
|
|
196
|
+
// 始终写入所有档位别名(含空串)及其显示名,确保完美覆盖主配置的同名 key——
|
|
197
|
+
// 否则主配置里残留的同名别名会透传进来。
|
|
198
|
+
env[aliasKey(tierUpper)] = v;
|
|
199
|
+
env[nameKey(tierUpper)] = v ? strip1m(v) : '';
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
// exhaustive check:新增 mode 时编译期报错,防止漏处理分支。
|
|
204
|
+
const _exhaustive = state.mode;
|
|
205
|
+
throw new Error(`unhandled form mode: ${_exhaustive}`);
|
|
206
|
+
}
|
|
207
|
+
env.CLAUDE_CODE_ATTRIBUTION_HEADER = state.options.attributionHeader ? '1' : '0';
|
|
208
|
+
env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = state.options.disableNonEssentialTraffic ? '1' : '0';
|
|
209
|
+
env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = String(state.options.autoCompactWindow);
|
|
210
|
+
env.CLAUDE_CODE_EFFORT_LEVEL = state.options.effort;
|
|
211
|
+
// 解析 customParams JSON,将顶层 key-value 合并到 env
|
|
212
|
+
if (state.customParams && state.customParams.trim()) {
|
|
213
|
+
try {
|
|
214
|
+
const parsed = JSON.parse(state.customParams);
|
|
215
|
+
if (parsed && typeof parsed === 'object') {
|
|
216
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
217
|
+
if (v !== undefined && v !== null) {
|
|
218
|
+
env[k] = String(v);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
// 解析失败应在 validateState 阶段拦截,此处静默忽略
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const result = { env };
|
|
228
|
+
if (state.mode === 'alias') {
|
|
229
|
+
// model 仅 alias 模式记录初始档位(供 ccs 启动参数与菜单展示)。
|
|
230
|
+
result.model = state.tier;
|
|
231
|
+
}
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
/** 提交前校验:必填项是否齐全。返回错误消息或 null。 */
|
|
235
|
+
export function validateState(state) {
|
|
236
|
+
if (!state.baseUrl || !state.baseUrl.trim())
|
|
237
|
+
return t('form.baseUrlValidate');
|
|
238
|
+
if (state.mode === 'single' && (!state.singleModel || !state.singleModel.trim())) {
|
|
239
|
+
return t('form.modelValidate');
|
|
240
|
+
}
|
|
241
|
+
// customParams 非空时需合法 JSON
|
|
242
|
+
if (state.customParams && state.customParams.trim()) {
|
|
243
|
+
try {
|
|
244
|
+
JSON.parse(state.customParams);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return t('form.customParamsValidate');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
// ---------- form entry ----------
|
|
253
|
+
/**
|
|
254
|
+
* 持久化 Tab 表单(ink):API Key / Models / Options / Review(预览+提交)。
|
|
255
|
+
* Tab 切换时下方内容跟随变化,字段直接原地编辑,无需 Enter 进入。
|
|
256
|
+
* create/edit 共用。委托给 formUi.runProviderForm。
|
|
257
|
+
*
|
|
258
|
+
* @param opts
|
|
259
|
+
* @param opts.initial 已有 settings(edit 时预填)
|
|
260
|
+
* @param opts.preset 预设(create 时预填,可为 null=自定义)
|
|
261
|
+
* @returns 最终 settings 片段
|
|
262
|
+
*/
|
|
263
|
+
export async function providerFormWithPreview(opts = {}) {
|
|
264
|
+
const { initial = {}, preset = null, title } = opts;
|
|
265
|
+
const runOpts = { initial, preset };
|
|
266
|
+
if (title !== undefined)
|
|
267
|
+
runOpts.title = title;
|
|
268
|
+
return runProviderForm(runOpts);
|
|
269
|
+
}
|
package/dist/formUi.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ALIAS_TIERS } from './form.js';
|
|
2
|
+
import type { FormState, Preset, ProviderSettings } from './types.js';
|
|
3
|
+
type FieldKind = 'text' | 'password' | 'toggle' | 'select' | 'number' | 'button';
|
|
4
|
+
type FieldId = 'baseUrl' | 'token' | 'authMethod' | 'singleModel' | `alias_${typeof ALIAS_TIERS[number]}` | 'autoCompactWindow' | 'tier' | 'effort' | 'customParams' | 'attributionHeader' | 'disableNonEssentialTraffic' | 'aliases' | 'nextTab' | 'submit' | 'cancel';
|
|
5
|
+
interface Field {
|
|
6
|
+
id: FieldId;
|
|
7
|
+
kind: FieldKind;
|
|
8
|
+
/** 若提供,则该 text 字段聚焦时在下方展开可搜索下拉(输入即过滤)。 */
|
|
9
|
+
support?: string[] | undefined;
|
|
10
|
+
}
|
|
11
|
+
/** 表单运行态:autoCompactWindow 以字符串编辑,提交时校验/转换。 */
|
|
12
|
+
export interface RuntimeForm extends Omit<FormState, 'options'> {
|
|
13
|
+
options: Omit<FormState['options'], 'autoCompactWindow'> & {
|
|
14
|
+
autoCompactWindow: string;
|
|
15
|
+
};
|
|
16
|
+
/** 供应商支持的模型列表(用于模型字段的内联下拉)。 */
|
|
17
|
+
modelSupport?: string[] | undefined;
|
|
18
|
+
}
|
|
19
|
+
/** 当前 tab 的可聚焦字段列表(review 的 preview 是静态展示,不参与聚焦)。 */
|
|
20
|
+
export declare function tabFields(form: RuntimeForm, tabIndex: number): Field[];
|
|
21
|
+
interface RunOpts {
|
|
22
|
+
initial?: {
|
|
23
|
+
env?: Record<string, string>;
|
|
24
|
+
model?: string;
|
|
25
|
+
};
|
|
26
|
+
preset?: Preset | null;
|
|
27
|
+
title?: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function runProviderForm(opts?: RunOpts): Promise<ProviderSettings>;
|
|
30
|
+
export {};
|