@cc-x/cc-x 0.3.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.
@@ -0,0 +1,235 @@
1
+ /**
2
+ * 配置存取层:读写 ~/.cc-mini/providers.json,生成默认配置,构造 env map。
3
+ *
4
+ * 铁律:这里写的是工具自己的数据文件(providers.json),**绝不**碰 ~/.claude/*。
5
+ * 格式与现版 PowerShell 完全兼容;写入 UTF-8 无 BOM、2 空格缩进。
6
+ */
7
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
8
+ import { homedir } from 'node:os';
9
+ import { join } from 'node:path';
10
+ import { KNOWN_KEYS } from './types.js';
11
+ export { KNOWN_KEYS } from './types.js';
12
+ /** 解析存储路径。`storeDir` 来自 --store-dir(测试用),默认 ~/.cc-mini。 */
13
+ export function resolveStorePaths(storeDir) {
14
+ const dir = storeDir && storeDir.trim() ? storeDir : join(homedir(), '.cc-mini');
15
+ return { dir, file: join(dir, 'providers.json') };
16
+ }
17
+ const nonEmpty = (v) => typeof v === 'string' && v.trim() !== '';
18
+ /**
19
+ * 默认配置:官方 + DeepSeek + 智谱GLM + 小米MiMo(密钥空)。
20
+ * 官方档带 `builtin: 'official'`(评审①),其它供应商是专有名词、不翻译。
21
+ */
22
+ export function defaultStore() {
23
+ return {
24
+ current: '官方',
25
+ lang: 'zh',
26
+ providers: [
27
+ { name: '官方', note: '', builtin: 'official', env: {} },
28
+ {
29
+ name: 'DeepSeek',
30
+ note: '',
31
+ env: {
32
+ ANTHROPIC_BASE_URL: 'https://api.deepseek.com/anthropic',
33
+ ANTHROPIC_DEFAULT_OPUS_MODEL: 'deepseek-v4-pro',
34
+ ANTHROPIC_DEFAULT_SONNET_MODEL: 'deepseek-v4-pro',
35
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: 'deepseek-v4-flash',
36
+ CLAUDE_CODE_EFFORT_LEVEL: 'max',
37
+ },
38
+ },
39
+ {
40
+ name: '智谱GLM',
41
+ note: '',
42
+ env: {
43
+ ANTHROPIC_BASE_URL: 'https://open.bigmodel.cn/api/anthropic',
44
+ ANTHROPIC_DEFAULT_OPUS_MODEL: 'GLM-4.7',
45
+ ANTHROPIC_DEFAULT_SONNET_MODEL: 'GLM-4.7',
46
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: 'glm-4.5-air',
47
+ },
48
+ },
49
+ {
50
+ name: '小米MiMo',
51
+ note: '',
52
+ env: {
53
+ ANTHROPIC_BASE_URL: 'https://api.xiaomimimo.com/anthropic',
54
+ ANTHROPIC_DEFAULT_OPUS_MODEL: 'mimo-v2.5-pro',
55
+ ANTHROPIC_DEFAULT_SONNET_MODEL: 'mimo-v2.5-pro',
56
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: 'mimo-v2.5-pro',
57
+ },
58
+ },
59
+ ],
60
+ };
61
+ }
62
+ /**
63
+ * 把任意解析结果规整为合法 Store。
64
+ *
65
+ * 字段级**宽松容错**(缺 lang / 缺 note / 缺 builtin / 缺 env 都不报错),保证老用户文件零迁移;
66
+ * 但结构级**严格校验**:顶层必须是对象、`providers` 必须是数组、每个配置的 name/env 结构必须合法
67
+ * —— 否则抛 StoreError('format')。
68
+ * 这是为了堵住「语法合法但结构损坏的 JSON 被静默规整成空 providers、用户一保存就覆盖丢数据」的坑([P1])。
69
+ */
70
+ function normalizeStore(raw, file) {
71
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
72
+ throw new StoreError('format', file);
73
+ const obj = raw;
74
+ if (!Array.isArray(obj.providers))
75
+ throw new StoreError('format', file);
76
+ const providers = obj.providers.map((p) => normalizeProvider(p, file));
77
+ const lang = obj.lang === 'en' ? 'en' : obj.lang === 'zh' ? 'zh' : undefined;
78
+ const current = typeof obj.current === 'string' ? obj.current : (providers[0]?.name ?? '');
79
+ return { current, ...(lang ? { lang } : {}), providers };
80
+ }
81
+ function normalizeProvider(raw, file) {
82
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
83
+ throw new StoreError('format', file);
84
+ const p = raw;
85
+ if (typeof p.name !== 'string')
86
+ throw new StoreError('format', file);
87
+ if (p.note !== undefined && typeof p.note !== 'string')
88
+ throw new StoreError('format', file);
89
+ if (p.builtin !== undefined && typeof p.builtin !== 'string')
90
+ throw new StoreError('format', file);
91
+ if (p.env !== undefined && (!p.env || typeof p.env !== 'object' || Array.isArray(p.env))) {
92
+ throw new StoreError('format', file);
93
+ }
94
+ const name = p.name;
95
+ const note = typeof p.note === 'string' ? p.note : '';
96
+ const builtin = typeof p.builtin === 'string' ? p.builtin : undefined;
97
+ const env = {};
98
+ if (p.env) {
99
+ for (const [k, v] of Object.entries(p.env)) {
100
+ if (typeof v !== 'string')
101
+ throw new StoreError('format', file);
102
+ env[k] = v;
103
+ }
104
+ }
105
+ return { name, note, ...(builtin ? { builtin } : {}), env };
106
+ }
107
+ export class StoreError extends Error {
108
+ kind;
109
+ file;
110
+ constructor(kind, file) {
111
+ super(`store ${kind} error: ${file}`);
112
+ this.kind = kind;
113
+ this.file = file;
114
+ this.name = 'StoreError';
115
+ }
116
+ }
117
+ /** 读配置;文件不存在则生成默认并落盘后返回;文件不可用则抛 StoreError(绝不覆盖)。 */
118
+ export function loadStore(paths) {
119
+ let text;
120
+ try {
121
+ text = readFileSync(paths.file, 'utf-8'); // [P2] 权限/磁盘/EISDIR 等
122
+ }
123
+ catch (e) {
124
+ if (e.code !== 'ENOENT')
125
+ throw new StoreError('read', paths.file);
126
+ const store = defaultStore();
127
+ saveStore(paths, store);
128
+ return store;
129
+ }
130
+ let parsed;
131
+ try {
132
+ parsed = JSON.parse(text);
133
+ }
134
+ catch {
135
+ throw new StoreError('parse', paths.file);
136
+ }
137
+ return normalizeStore(parsed, paths.file); // [P1] 结构校验在内部,失败抛 'format'
138
+ }
139
+ /**
140
+ * 只读探测 lang,**不生成文件**(用于 --help/--version 在 parse 前定语言,避免副作用)。
141
+ * 文件不存在/解析失败都返回 undefined。
142
+ */
143
+ export function peekStoreLang(paths) {
144
+ try {
145
+ if (!existsSync(paths.file))
146
+ return undefined;
147
+ const raw = JSON.parse(readFileSync(paths.file, 'utf-8'));
148
+ return raw.lang === 'en' ? 'en' : raw.lang === 'zh' ? 'zh' : undefined;
149
+ }
150
+ catch {
151
+ return undefined;
152
+ }
153
+ }
154
+ /** 写配置:UTF-8 无 BOM、2 空格缩进(Node 默认 utf-8 不带 BOM,与现版一致)。 */
155
+ export function saveStore(paths, store) {
156
+ if (!existsSync(paths.dir))
157
+ mkdirSync(paths.dir, { recursive: true });
158
+ writeFileSync(paths.file, `${JSON.stringify(store, null, 2)}\n`, 'utf-8');
159
+ }
160
+ /**
161
+ * 是否官方档。优先认稳定标识 `builtin === 'official'`(评审①);
162
+ * 老文件没有 builtin 时,仅将「中文名为官方 + env 为空」视为官方档。
163
+ * 这样旧数据仍兼容,而手动填了第三方地址/密钥的同名配置不会被误判为登录态。
164
+ */
165
+ export function isOfficial(p) {
166
+ if (p.builtin)
167
+ return p.builtin === 'official';
168
+ return p.name === '官方' && Object.keys(p.env).length === 0;
169
+ }
170
+ /**
171
+ * [P1] 编辑保存后修正身份:官方档(builtin='official' = 登录态、空 env)一旦被配成真实第三方
172
+ * (env 非空,有了 base/key 等),就清掉 builtin —— 否则会继续被当登录态、跳过缺密钥警告。
173
+ */
174
+ export function reconcileBuiltin(p) {
175
+ if (p.builtin === 'official' && Object.keys(p.env).length > 0) {
176
+ delete p.builtin;
177
+ }
178
+ }
179
+ /** 删除配置等操作后修正默认指向:优先剩余官方档,其次第一项;没有配置则置空。 */
180
+ export function reconcileCurrent(store) {
181
+ if (store.providers.some((p) => p.name === store.current))
182
+ return;
183
+ store.current = (store.providers.find(isOfficial) ?? store.providers[0])?.name ?? '';
184
+ }
185
+ /** 取配置的 env map(即 provider.env,保证非空对象)。 */
186
+ export function getProviderEnvMap(p) {
187
+ return p.env ?? {};
188
+ }
189
+ /**
190
+ * 由一组字段构造 provider.env:按 KNOWN_KEYS 顺序、丢弃空白值。
191
+ * 对齐现版 Build-ProviderEnv。`[1m]` 等后缀属于自由文本,原样保留(见 plan §3.1.1)。
192
+ */
193
+ export function buildProviderEnv(fields) {
194
+ const env = {};
195
+ for (const key of KNOWN_KEYS) {
196
+ const v = fields[key];
197
+ if (nonEmpty(v))
198
+ env[key] = v.trim();
199
+ }
200
+ return env;
201
+ }
202
+ export function getProviderState(p) {
203
+ const map = getProviderEnvMap(p);
204
+ const effort = nonEmpty(map.CLAUDE_CODE_EFFORT_LEVEL) ? map.CLAUDE_CODE_EFFORT_LEVEL : undefined;
205
+ if (isOfficial(p))
206
+ return { key: 'official', ...(effort ? { effort } : {}) };
207
+ const hasTok = nonEmpty(map.ANTHROPIC_AUTH_TOKEN);
208
+ const hasKey = nonEmpty(map.ANTHROPIC_API_KEY);
209
+ const key = !hasTok && !hasKey ? 'noKey' : hasKey ? 'apiKey' : 'hasToken';
210
+ return { key, ...(effort ? { effort } : {}) };
211
+ }
212
+ /** 按 name 找配置。 */
213
+ export function findProvider(store, name) {
214
+ return store.providers.find((p) => p.name === name);
215
+ }
216
+ /**
217
+ * 名称去重:同名被【其它】配置占用时追加 “ 2/3/…”。`exclude` 是正在编辑的本条(排除自身)。
218
+ * 对齐现版 Resolve-UniqueName。
219
+ */
220
+ export function resolveUniqueName(store, name, exclude) {
221
+ const existing = store.providers.filter((p) => p !== exclude).map((p) => p.name);
222
+ if (!existing.includes(name))
223
+ return name;
224
+ let i = 2;
225
+ while (existing.includes(`${name} ${i}`))
226
+ i += 1;
227
+ return `${name} ${i}`;
228
+ }
229
+ /** 语言:providers.json 的 lang 字段,缺省视为 zh。 */
230
+ export function getLang(store) {
231
+ return store.lang === 'en' ? 'en' : 'zh';
232
+ }
233
+ export function setLang(store, lang) {
234
+ store.lang = lang;
235
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * 数据层共享类型与常量。
3
+ *
4
+ * 数据格式必须与现版 PowerShell 完全兼容(老用户的 ~/.cc-mini/providers.json 能直接读)。
5
+ * 详见 docs/npm-rewrite-plan.md §3。
6
+ */
7
+ /** 受管的 7 个环境变量(工具只动这些,其它一律不碰)。详见 plan §2。 */
8
+ export const KNOWN_KEYS = [
9
+ 'ANTHROPIC_BASE_URL',
10
+ 'ANTHROPIC_AUTH_TOKEN',
11
+ 'ANTHROPIC_API_KEY',
12
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL',
13
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL',
14
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
15
+ 'CLAUDE_CODE_EFFORT_LEVEL',
16
+ ];
@@ -0,0 +1,42 @@
1
+ /**
2
+ * 设为默认(Set-Default)—— 持久化用户环境变量,仅影响新开终端、不动运行中会话。
3
+ *
4
+ * 对 7 个受管键:目标配置有值的写值,没值的清除(vals 里记 null)。然后 store.current=name 并存盘。
5
+ * 平台分叉是唯一有平台差异的地方:Windows 走注册表+广播,Unix 走 rc 文件 marker 块。
6
+ * `--default-scope process` = 不落盘 dry-run(评审⑥):照常算 vals、更新 store,但跳过系统持久化。
7
+ */
8
+ import { KNOWN_KEYS, getProviderEnvMap, saveStore } from '../config/store.js';
9
+ import { persistUnix } from './persist-unix.js';
10
+ import { persistWindows } from './persist-windows.js';
11
+ /** 每个受管键 → 值或 null(清除)。对齐现版 Set-Default 里的 $vals 构造。 */
12
+ export function computeManagedVals(p) {
13
+ const map = getProviderEnvMap(p);
14
+ const vals = {};
15
+ for (const k of KNOWN_KEYS) {
16
+ const v = map[k];
17
+ vals[k] = typeof v === 'string' && v.trim() !== '' ? v : null;
18
+ }
19
+ return vals;
20
+ }
21
+ export function setDefault(paths, store, p, scope) {
22
+ const vals = computeManagedVals(p);
23
+ const dryRun = scope === 'process';
24
+ const result = { scope, dryRun };
25
+ let persisted = true;
26
+ if (!dryRun) {
27
+ if (process.platform === 'win32') {
28
+ result.windows = persistWindows(vals);
29
+ persisted = result.windows.ok;
30
+ }
31
+ else {
32
+ result.unix = persistUnix(vals);
33
+ persisted = !result.unix.unsupported; // fish 未写入 → 不算持久化成功
34
+ }
35
+ }
36
+ // [P1] 仅当持久化成功(或 dry-run)才改默认指向并落盘,避免「报失败却已改默认」的不一致。
37
+ if (dryRun || persisted) {
38
+ store.current = p.name;
39
+ saveStore(paths, store);
40
+ }
41
+ return result;
42
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * macOS / Linux 持久化:在 shell 启动文件里维护一个 marker 块(幂等,可重复重写)。
3
+ *
4
+ * # >>> xx >>>
5
+ * export ANTHROPIC_BASE_URL='https://...'
6
+ * ...只导出当前配置用到的受管键...
7
+ * # <<< xx <<<
8
+ *
9
+ * 每次「设为默认」整体重写该块 —— 自动清除上个默认里多余的 export。语义与 Windows 一致:
10
+ * 只影响新开终端、不动运行中会话(rc 文件仅在新交互 shell 启动时加载)。
11
+ * fish 语法不同(set -gx),v1 不支持,由调用方据 `kind==='fish'` 给提示(评审/plan §4.2)。
12
+ */
13
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
14
+ import { homedir } from 'node:os';
15
+ import { dirname } from 'node:path';
16
+ import { join as posixJoin } from 'node:path/posix';
17
+ const BEGIN = '# >>> xx >>>';
18
+ const END = '# <<< xx <<<';
19
+ /** 单引号包裹值,按 shell 规则转义内部单引号('\'' 收尾再起)。 */
20
+ function shQuote(v) {
21
+ return `'${v.split("'").join("'\\''")}'`;
22
+ }
23
+ /** 生成 marker 块文本(只含非空键,按传入顺序)。 */
24
+ export function buildBlock(vals) {
25
+ const lines = [BEGIN];
26
+ for (const [k, v] of Object.entries(vals)) {
27
+ if (v !== null && v !== '')
28
+ lines.push(`export ${k}=${shQuote(v)}`);
29
+ }
30
+ lines.push(END);
31
+ return lines.join('\n');
32
+ }
33
+ const BLOCK_RE = /# >>> xx >>>[\s\S]*?# <<< xx <<</;
34
+ /** 把 marker 块写进/替换进指定 rc 文件(纯 fs,可测)。返回写入的文件路径。 */
35
+ export function writeMarkerBlock(file, vals) {
36
+ const block = buildBlock(vals);
37
+ let text = existsSync(file) ? readFileSync(file, 'utf-8') : '';
38
+ if (BLOCK_RE.test(text)) {
39
+ text = text.replace(BLOCK_RE, block);
40
+ }
41
+ else {
42
+ const sep = text === '' || text.endsWith('\n') ? '' : '\n';
43
+ text = `${text}${sep}${text === '' ? '' : '\n'}${block}\n`;
44
+ }
45
+ const dir = dirname(file);
46
+ if (!existsSync(dir))
47
+ mkdirSync(dir, { recursive: true });
48
+ writeFileSync(file, text, 'utf-8');
49
+ return file;
50
+ }
51
+ /** 据 $SHELL basename + 平台选 rc 文件。 */
52
+ export function rcTargetFor(shellPath, platform, home) {
53
+ // 这些是 Unix-destined 路径,用 posix join 保证宿主无关(生产仅在 Unix 上运行)。
54
+ const base = (shellPath ?? '').split('/').pop() ?? '';
55
+ if (base === 'zsh')
56
+ return { file: posixJoin(home, '.zshrc'), kind: 'zsh' };
57
+ if (base === 'fish')
58
+ return { file: posixJoin(home, '.config', 'fish', 'config.fish'), kind: 'fish' };
59
+ if (base === 'bash') {
60
+ // macOS 登录 shell 读 .bash_profile;Linux 交互非登录读 .bashrc。
61
+ return { file: posixJoin(home, platform === 'darwin' ? '.bash_profile' : '.bashrc'), kind: 'bash' };
62
+ }
63
+ return { file: posixJoin(home, '.profile'), kind: 'sh' };
64
+ }
65
+ /** 设为默认的 Unix 实现:选 rc 文件并重写 marker 块(fish 跳过)。 */
66
+ export function persistUnix(vals) {
67
+ const target = rcTargetFor(process.env.SHELL, process.platform, homedir());
68
+ if (target.kind === 'fish')
69
+ return { kind: 'fish', unsupported: true, file: target.file };
70
+ writeMarkerBlock(target.file, vals);
71
+ return { kind: target.kind, file: target.file };
72
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Windows 持久化:直写 HKCU\Environment + 单次 WM_SETTINGCHANGE 广播。
3
+ *
4
+ * 评审③:钉 `powershell.exe`(5.1,Windows 必然存在),不用 `pwsh`(7+,可能没装)。
5
+ * 逻辑搬自现版 Set-UserEnv-Fast + Invoke-EnvBroadcast:注册表瞬时写入、最后只广播一次(100ms 短超时、
6
+ * SMTO_ABORTIFHUNG 跳过挂死窗口),避免「逐个 setx 广播 7 次、每窗口等 1s」的拖慢。
7
+ *
8
+ * 键值通过环境变量传 JSON 给子进程、用 ConvertFrom-Json 解析 —— 彻底避开命令行注入/引号转义。
9
+ */
10
+ import { spawnSync } from 'node:child_process';
11
+ const PS_SCRIPT = String.raw `
12
+ $ErrorActionPreference = 'Stop'
13
+ $m = $env:CCX_ENV_PAYLOAD | ConvertFrom-Json
14
+ $reg = 'HKCU:\Environment'
15
+ foreach ($p in $m.PSObject.Properties) {
16
+ if ($null -eq $p.Value -or $p.Value -eq '') {
17
+ Remove-ItemProperty -Path $reg -Name $p.Name -ErrorAction SilentlyContinue
18
+ } else {
19
+ Set-ItemProperty -Path $reg -Name $p.Name -Value $p.Value -Type String
20
+ }
21
+ }
22
+ $sig = @'
23
+ using System;
24
+ using System.Runtime.InteropServices;
25
+ public static class CcxNative {
26
+ [DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
27
+ static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam, uint fuFlags, uint uTimeout, out UIntPtr lpdwResult);
28
+ public static void Notify() {
29
+ UIntPtr r;
30
+ SendMessageTimeout((IntPtr)0xffff, 0x001A, UIntPtr.Zero, "Environment", 0x0002, 100, out r);
31
+ }
32
+ }
33
+ '@
34
+ Add-Type -TypeDefinition $sig
35
+ [CcxNative]::Notify()
36
+ `;
37
+ /** 把一组受管键写进/删出 HKCU\Environment,并广播一次环境变更。 */
38
+ export function persistWindows(vals) {
39
+ const res = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', PS_SCRIPT], {
40
+ env: { ...process.env, CCX_ENV_PAYLOAD: JSON.stringify(vals) },
41
+ stdio: ['ignore', 'ignore', 'pipe'],
42
+ windowsHide: true,
43
+ });
44
+ if (res.error)
45
+ return { ok: false, error: res.error.message };
46
+ if (res.status !== 0)
47
+ return { ok: false, error: (res.stderr?.toString() || '').trim() || `exit ${res.status}` };
48
+ return { ok: true };
49
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * 本次启用(Session-Launch)—— 进程级、阅后即焚。
3
+ *
4
+ * 1) 对 7 个受管键:有值 set,没值 delete(只动这 7 个)。
5
+ * 2) 找到 claude(which;Windows 上是 claude.cmd,评审②)。
6
+ * 3) spawn 且 stdio:inherit —— 子进程继承真实控制台句柄,天然没有现版 PowerShell 的
7
+ * 「stdin 被包成管道 → claude 误判非交互」问题。
8
+ * 4) Windows 不能直接 spawn .cmd(Node 的 EINVAL 防护),用 shell:true 经 cmd.exe 启动并对路径加引号。
9
+ */
10
+ import { spawnSync } from 'node:child_process';
11
+ import which from 'which';
12
+ import { KNOWN_KEYS, getProviderEnvMap } from '../config/store.js';
13
+ /** 把目标配置的受管环境变量套到当前进程(有值 set、没值 delete,只动这 7 个)。 */
14
+ export function applyManagedEnv(p) {
15
+ const map = getProviderEnvMap(p);
16
+ for (const key of KNOWN_KEYS) {
17
+ const v = map[key];
18
+ if (typeof v === 'string' && v.trim() !== '')
19
+ process.env[key] = v;
20
+ else
21
+ delete process.env[key];
22
+ }
23
+ }
24
+ /** 在 PATH 中定位 claude;找不到返回 null。Windows 下通常解析到 claude.cmd。 */
25
+ export function resolveClaude() {
26
+ return which.sync('claude', { nothrow: true });
27
+ }
28
+ /**
29
+ * 套环境 + 启动 claude,阻塞直到其退出。调用方负责在前后打印 banner / 处理 claudeMissing。
30
+ * @param claudePath 可注入(测试用);缺省走 resolveClaude()。
31
+ */
32
+ export function sessionLaunch(p, claudePath) {
33
+ applyManagedEnv(p);
34
+ const bin = claudePath ?? resolveClaude();
35
+ if (!bin)
36
+ return { claudeMissing: true, status: null };
37
+ const isWin = process.platform === 'win32';
38
+ // Windows:经 cmd.exe 启动以兼容 .cmd 包装,路径加引号防空格;Unix:直接 exec 解析后的真实路径。
39
+ const file = isWin ? `"${bin}"` : bin;
40
+ const res = spawnSync(file, [], { stdio: 'inherit', shell: isWin });
41
+ if (res.error)
42
+ return { spawnError: res.error, status: null };
43
+ return { status: res.status };
44
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * i18n 运行时:T() 翻译、当前语言、语言解析、供应商显示名。
3
+ *
4
+ * 语言来源优先级(plan §5):
5
+ * --lang 参数 > providers.json 的 lang 字段 > 环境 LC_ALL/LANG/LANGUAGE > 默认 zh。
6
+ */
7
+ import { isOfficial } from '../config/store.js';
8
+ import { messages } from './messages.js';
9
+ let current = 'zh';
10
+ /** 设置本次进程的界面语言(启动时按 resolveLang 的结果调用一次)。 */
11
+ export function setLang(lang) {
12
+ current = lang;
13
+ }
14
+ export function getLang() {
15
+ return current;
16
+ }
17
+ /**
18
+ * 翻译:查目录取当前语言文案,按序替换 `{0}` `{1}` …。
19
+ * 缺 key 时返回 key 本身(便于一眼发现漏翻),缺当前语言时回退 zh。
20
+ */
21
+ export function T(key, ...args) {
22
+ const m = messages[key];
23
+ if (!m)
24
+ return key;
25
+ let s = m[current] || m.zh || key;
26
+ args.forEach((a, i) => {
27
+ s = s.split(`{${i}}`).join(String(a));
28
+ });
29
+ return s;
30
+ }
31
+ /**
32
+ * 解析本次界面语言。`explicit` 来自 --lang,`storeLang` 来自 providers.json。
33
+ * 环境变量:含 `zh` → 中文;以 `en` 开头(如 en_US)→ 英文;其余默认 zh。
34
+ */
35
+ export function resolveLang(explicit, storeLang) {
36
+ if (explicit)
37
+ return explicit;
38
+ if (storeLang)
39
+ return storeLang;
40
+ const env = (process.env.LC_ALL || process.env.LANG || process.env.LANGUAGE || '').toLowerCase();
41
+ if (env.includes('zh'))
42
+ return 'zh';
43
+ if (env.startsWith('en'))
44
+ return 'en';
45
+ return 'zh';
46
+ }
47
+ /**
48
+ * 配置的显示名:官方档显示翻译后的「官方/Official」(评审①:显示名与数据主键 `name` 解耦);
49
+ * 其余是专有名词,原样显示 `name`。
50
+ */
51
+ export function providerDisplayName(p) {
52
+ return isOfficial(p) ? T('provider.official') : p.name;
53
+ }
@@ -0,0 +1,143 @@
1
+ export const messages = {
2
+ // —— CLI ——
3
+ 'cli.desc': {
4
+ zh: 'Claude Code API 切换器:在官方账号与第三方 Anthropic 兼容 API 间切换。',
5
+ en: 'Claude Code API switcher: switch between the official account and third-party Anthropic-compatible APIs.',
6
+ },
7
+ 'cli.arg.name': { zh: '目标配置名;省略则打开交互菜单', en: 'target profile name; omit to open the menu' },
8
+ 'cli.opt.session': {
9
+ zh: '本次启用:仅当前终端设环境变量并启动 claude(阅后即焚)',
10
+ en: 'session: set env for this terminal only and launch claude',
11
+ },
12
+ 'cli.opt.list': { zh: '列出所有配置及状态', en: 'list all profiles and their state' },
13
+ 'cli.opt.storeDir': { zh: '覆盖配置存储目录(测试用,默认 ~/.cc-mini)', en: 'override store dir (testing; default ~/.cc-mini)' },
14
+ 'cli.opt.defaultScope': {
15
+ zh: '设为默认写到哪:user(持久) / process(不落盘 dry-run,测试用)',
16
+ en: 'set-default scope: user (persist) / process (dry-run, testing)',
17
+ },
18
+ 'cli.opt.lang': { zh: '本次界面语言:zh / en', en: 'UI language for this run: zh / en' },
19
+ 'cli.opt.version': { zh: '显示版本号', en: 'show version' },
20
+ 'cli.opt.help': { zh: '显示帮助', en: 'show help' },
21
+ // —— 列表 / 状态 ——
22
+ 'list.default': { zh: '默认配置:{0}', en: 'Default: {0}' },
23
+ 'state.login': { zh: '登录态', en: 'Logged in' },
24
+ 'state.noKey': { zh: '密钥未填', en: 'No key' },
25
+ 'state.apiKey': { zh: '密钥·API_KEY', en: 'Key · API_KEY' },
26
+ 'state.hasKey': { zh: '密钥已设', en: 'Key set' },
27
+ // —— 供应商显示名(仅官方这种普通名词需要翻译;DeepSeek/GLM/MiMo 是专有名词不翻)——
28
+ 'provider.official': { zh: '官方', en: 'Official' },
29
+ // —— 菜单通用 ——
30
+ 'menu.prompt': { zh: '输入序号 (q 取消): ', en: 'Enter number (q to cancel): ' },
31
+ 'menu.mainTitle': {
32
+ zh: 'cc-x v{0} · Claude Code API 切换器 (默认 = 新终端裸敲 claude 用的)',
33
+ en: 'cc-x v{0} · Claude Code API switcher (default = used by bare `claude` in new terminals)',
34
+ },
35
+ 'menu.mainHint': {
36
+ zh: '↑↓ 选择 · Enter 进入 · Shift+↑↓(或 PgUp/PgDn)排序 · q 退出',
37
+ en: '↑↓ move · Enter open · Shift+↑↓ (or PgUp/PgDn) reorder · q quit',
38
+ },
39
+ 'menu.newProfile': { zh: '+ 新增配置', en: '+ New profile' },
40
+ 'menu.exit': { zh: '退出', en: 'Exit' },
41
+ 'menu.default': { zh: '(默认)', en: '(default)' },
42
+ 'menu.comingSoon': { zh: '(该功能下一步实现)', en: '(coming in the next step)' },
43
+ // —— 动作菜单 ——
44
+ 'action.titlePrefix': { zh: '配置:', en: 'Profile: ' },
45
+ 'action.session': {
46
+ zh: '本次启用 — 仅当前终端,立即启动 Claude(并行多终端推荐)',
47
+ en: 'Session — this terminal only, launches Claude now (great for parallel terminals)',
48
+ },
49
+ 'action.setDefault': {
50
+ zh: '设为默认 — 新终端裸敲 claude 默认用它(不影响运行中会话)',
51
+ en: 'Set default — used by bare claude in new terminals (running sessions unaffected)',
52
+ },
53
+ 'action.edit': { zh: '编辑', en: 'Edit' },
54
+ 'action.delete': { zh: '删除', en: 'Delete' },
55
+ 'action.back': { zh: '返回', en: 'Back' },
56
+ 'action.hint': { zh: '↑↓ 选择 · Enter 确认 · q 返回', en: '↑↓ move · Enter select · q back' },
57
+ 'action.deleteConfirm': { zh: '确认删除 [{0}]? (y/N): ', en: 'Delete [{0}]? (y/N): ' },
58
+ 'action.deleteOfficialWarn': { zh: '建议保留『官方』。', en: 'Keeping "Official" is recommended.' },
59
+ 'menu.language': { zh: '🌐 切换到 English', en: '🌐 切换到中文' },
60
+ // —— 通用占位 ——
61
+ 'empty.paren': { zh: '(空)', en: '(empty)' },
62
+ // —— 编辑表单 ——
63
+ 'edit.title': { zh: '编辑配置 (↑↓ 选要改的项,Enter 进入;↓到底可选保存/放弃)', en: 'Edit profile (↑↓ pick a field, Enter to edit; save/discard at bottom)' },
64
+ 'edit.hint': {
65
+ zh: '供应商:选后自动填地址/模型 · 备注随便写 · 回车=不改 · 输入 - =清空',
66
+ en: 'Provider: auto-fills url/models · Enter=keep · type "-" to clear',
67
+ },
68
+ 'edit.current': { zh: '当前:{0}', en: 'current: {0}' },
69
+ 'edit.inputHint': { zh: '回车=不改,输入=替换,- =清空', en: 'Enter=keep, type=replace, "-"=clear' },
70
+ 'edit.field.provider': { zh: '供应商 ', en: 'Provider ' },
71
+ 'edit.field.note': { zh: '备注 ', en: 'Note ' },
72
+ 'edit.field.base': { zh: 'API 地址 ', en: 'API URL ' },
73
+ 'edit.field.auth': { zh: '认证字段 ', en: 'Auth field ' },
74
+ 'edit.field.key': { zh: 'API 密钥 ', en: 'API key ' },
75
+ 'edit.field.opus': { zh: 'opus → 模型 ', en: 'opus → model ' },
76
+ 'edit.field.sonnet': { zh: 'sonnet→ 模型 ', en: 'sonnet→ model ' },
77
+ 'edit.field.haiku': { zh: 'haiku → 模型 ', en: 'haiku → model ' },
78
+ 'edit.field.effort': { zh: 'effort 思考档 ', en: 'effort level ' },
79
+ 'edit.toggleSecretShow': { zh: '👁 显示密钥明文(当前隐藏)', en: '👁 Show key in plaintext (now hidden)' },
80
+ 'edit.toggleSecretHide': { zh: '🙈 隐藏密钥(当前明文)', en: '🙈 Hide key (now shown)' },
81
+ 'edit.save': { zh: '保存并返回', en: 'Save & back' },
82
+ 'edit.discard': { zh: '放弃修改', en: 'Discard' },
83
+ 'edit.nameEmpty': { zh: '还没选供应商(或自定义名称),未保存。', en: 'No provider/name chosen yet; not saved.' },
84
+ 'edit.customName': { zh: '自定义供应商名称(回车=不改): ', en: 'Custom provider name (Enter=keep): ' },
85
+ 'edit.noteInput': { zh: '备注(回车=不改,- =清空): ', en: 'Note (Enter=keep, "-" clear): ' },
86
+ // —— Picker ——
87
+ 'pick.hint': { zh: '↑↓ 选择 · Enter 确认 · q 不改', en: '↑↓ move · Enter select · q keep' },
88
+ 'pick.noChange': { zh: '不修改', en: '(no change)' },
89
+ 'pick.manual': { zh: '手动输入…', en: 'Enter manually…' },
90
+ 'pick.provider.title': { zh: '供应商(当前:{0})', en: 'Provider (current: {0})' },
91
+ 'pick.provider.none': { zh: '(未选)', en: '(none)' },
92
+ 'pick.provider.custom': { zh: '自定义(手动填名字)', en: 'Custom (type a name)' },
93
+ 'pick.providerUrl.title': { zh: '{0} 有多个 API 地址,选一个', en: '{0} has multiple URLs, pick one' },
94
+ 'pick.base.title': { zh: 'API 地址(当前:{0})', en: 'API URL (current: {0})' },
95
+ 'pick.base.existing': { zh: '(已有:{0})', en: '(used:{0})' },
96
+ 'pick.base.manualInput': { zh: '手动输入 API 地址(回车=不改,- =清空): ', en: 'Type API URL (Enter=keep, "-" clear): ' },
97
+ 'pick.auth.title': { zh: '认证字段(当前:{0})', en: 'Auth field (current: {0})' },
98
+ 'pick.auth.token': { zh: 'AUTH_TOKEN (Bearer,多数第三方中转)', en: 'AUTH_TOKEN (Bearer, most 3rd-party relays)' },
99
+ 'pick.auth.apikey': { zh: 'API_KEY (x-api-key,官方/少数)', en: 'API_KEY (x-api-key, official/few)' },
100
+ 'pick.effort.title': { zh: 'effort 思考档(当前:{0})', en: 'effort level (current: {0})' },
101
+ 'pick.effort.empty': { zh: '留空(不设)', en: 'Leave empty' },
102
+ 'pick.effort.hint': { zh: '越往后越深入;auto=模型默认 · q 不改', en: 'deeper to the right; auto=model default · q keep' },
103
+ // —— 错误 ——
104
+ 'error.notFound': { zh: '找不到配置:{0}', en: 'Profile not found: {0}' },
105
+ 'error.existing': { zh: '现有:{0}', en: 'Existing: {0}' },
106
+ 'error.storeRead': { zh: '配置文件读取失败:{0}', en: 'Failed to read config file: {0}' },
107
+ 'error.storeCorrupt': { zh: '配置文件解析失败(JSON 语法错误):{0}', en: 'Failed to parse config file (invalid JSON): {0}' },
108
+ 'error.storeFormat': { zh: '配置文件结构不正确(顶层须为对象、providers 须为数组且条目结构合法):{0}', en: 'Config file has invalid structure (top-level must be an object, providers must be an array with valid profile entries): {0}' },
109
+ 'error.storeCorruptHint': {
110
+ zh: '为避免误删,未对它做任何改动。请修复后重试;或删除该文件以重新生成默认配置(会丢失已填的密钥)。',
111
+ en: 'Left untouched to avoid data loss. Fix it and retry; or delete the file to regenerate defaults (loses any saved keys).',
112
+ },
113
+ // —— 本次启用(session)——
114
+ 'session.noKey': { zh: '⚠ 配置 [{0}] 还没填密钥。', en: '⚠ Profile [{0}] has no key set.' },
115
+ 'session.launch': {
116
+ zh: '▶ 本次启用:{0}(仅当前终端,不影响其它终端)',
117
+ en: '▶ Session: {0} (this terminal only; others unaffected)',
118
+ },
119
+ 'session.starting': {
120
+ zh: '正在启动 Claude…(退出 Claude 后回到命令行)',
121
+ en: 'Launching Claude… (returns here after Claude exits)',
122
+ },
123
+ 'session.noClaude': { zh: '未找到 claude 命令,请确认它在 PATH 中。', en: 'claude not found on PATH.' },
124
+ // —— 设为默认(default)——
125
+ 'default.writing': { zh: '正在写入用户环境变量…', en: 'Writing user environment variables…' },
126
+ 'default.done': {
127
+ zh: '✓ 已设为默认:{0} · 新开终端裸敲 claude 生效(不影响运行中会话)',
128
+ en: '✓ Default set: {0} · effective in newly opened terminals (running sessions unaffected)',
129
+ },
130
+ 'default.dryRun': {
131
+ zh: '(dry-run:--default-scope process,未改系统,仅更新存储)',
132
+ en: '(dry-run: --default-scope process; system untouched, store only)',
133
+ },
134
+ 'default.unixWrote': {
135
+ zh: '已写入 {0}(新开终端生效;或 source 它立即生效)',
136
+ en: 'Wrote {0} (effective in new terminals; or source it now)',
137
+ },
138
+ 'default.failed': { zh: '设为默认失败:{0}', en: 'Failed to set default: {0}' },
139
+ 'default.fishUnsupported': {
140
+ zh: '⚠ 检测到 fish:v1 暂不支持「设为默认」,请手动设置或改用「本次启用」。',
141
+ en: '⚠ fish detected: "set default" is unsupported in v1; set manually or use session launch.',
142
+ },
143
+ };