@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,152 @@
1
+ /**
2
+ * 自绘 ↑↓ 选择菜单(对齐现版 Select-Menu)。
3
+ *
4
+ * - ↑↓ 导航(跳过 '' 分隔空行)、数字键直选、Enter 确认、q/Esc 取消、Ctrl+C 退出。
5
+ * - 原地重绘(光标上移 + 清屏到底 + 隐藏光标)→ 不闪烁。
6
+ * - 可选就地排序:传 onMove + movableCount,Shift+↑↓ 或 PgUp/PgDn 在顶部前 N 项内移动选中项。
7
+ * - 非交互/无 TTY 时回退到「打印列表 + 读一行序号」。
8
+ *
9
+ * 注:文本输入(含中文)不在这里——那走 ui/text.ts(raw readValue / cooked readText,后者兼容输入法,评审④)。
10
+ */
11
+ import { createInterface, emitKeypressEvents } from 'node:readline';
12
+ import { T } from '../i18n/index.js';
13
+ import { CLEAR_DOWN, CLEAR_SCREEN, CR, cursorUp, HIDE_CURSOR, paint, SHOW_CURSOR } from '../utils/ansi.js';
14
+ import { truncateDisplay } from '../utils/display.js';
15
+ /** 返回选中索引;取消(q/Esc/非法)返回 -1。 */
16
+ export async function selectMenu(opts) {
17
+ const stdin = process.stdin;
18
+ const stdout = process.stdout;
19
+ let items = opts.items.slice();
20
+ if (!stdin.isTTY || !stdout.isTTY)
21
+ return fallbackSelect(opts, items);
22
+ const nextSel = (i, d) => {
23
+ const n = items.length;
24
+ do {
25
+ i = (i + d + n) % n;
26
+ } while (items[i] === '');
27
+ return i;
28
+ };
29
+ let idx = opts.start ?? 0;
30
+ if (idx < 0 || idx >= items.length || items[idx] === '')
31
+ idx = nextSel(Math.max(0, Math.min(idx, items.length - 1)), 1);
32
+ emitKeypressEvents(stdin);
33
+ const wasRaw = stdin.isRaw ?? false;
34
+ stdin.setRawMode(true);
35
+ stdin.resume();
36
+ stdout.write(CLEAR_SCREEN + HIDE_CURSOR); // 清屏归位,制造「整页」感(每进一级菜单都是新页)
37
+ let prevLines = 0;
38
+ const render = () => {
39
+ const cols = stdout.columns ?? 80;
40
+ const lines = [''];
41
+ if (opts.title) {
42
+ lines.push(` ${paint(opts.title, 'cyan')}`, '');
43
+ }
44
+ if (opts.status) {
45
+ lines.push(` ${paint(opts.status, 'green')}`, '');
46
+ }
47
+ for (let i = 0; i < items.length; i++) {
48
+ const it = items[i] ?? '';
49
+ if (it === '') {
50
+ lines.push('');
51
+ continue;
52
+ }
53
+ if (i === idx)
54
+ lines.push(paint(` ▶ ${it}`, 'green'));
55
+ else
56
+ lines.push(paint(` ${it}`, opts.colors?.[i] ?? 'none'));
57
+ }
58
+ lines.push('');
59
+ if (opts.hint)
60
+ lines.push(` ${paint(opts.hint, 'dim')}`);
61
+ const block = lines.map((l) => truncateDisplay(l, cols - 1)).join('\n');
62
+ const prefix = prevLines > 0 ? cursorUp(prevLines - 1) + CR + CLEAR_DOWN : '';
63
+ stdout.write(prefix + block);
64
+ prevLines = lines.length;
65
+ };
66
+ return new Promise((resolve) => {
67
+ const cleanup = (result) => {
68
+ stdin.off('keypress', onKey);
69
+ stdout.write(`${SHOW_CURSOR}\n`);
70
+ if (!wasRaw)
71
+ stdin.setRawMode(false);
72
+ stdin.pause();
73
+ resolve(result);
74
+ };
75
+ const onKey = (str, key) => {
76
+ if (key?.ctrl && key.name === 'c') {
77
+ cleanup(-1);
78
+ process.exit(130);
79
+ }
80
+ // 就地排序(仅可排序区内)
81
+ if (opts.onMove && opts.movableCount) {
82
+ const up = key?.name === 'pageup' || (Boolean(key?.shift) && key?.name === 'up');
83
+ const down = key?.name === 'pagedown' || (Boolean(key?.shift) && key?.name === 'down');
84
+ if (up && idx > 0 && idx < opts.movableCount) {
85
+ items = opts.onMove(idx, idx - 1);
86
+ idx -= 1;
87
+ render();
88
+ return;
89
+ }
90
+ if (down && idx < opts.movableCount - 1) {
91
+ items = opts.onMove(idx, idx + 1);
92
+ idx += 1;
93
+ render();
94
+ return;
95
+ }
96
+ }
97
+ switch (key?.name) {
98
+ case 'up':
99
+ idx = nextSel(idx, -1);
100
+ render();
101
+ return;
102
+ case 'down':
103
+ idx = nextSel(idx, 1);
104
+ render();
105
+ return;
106
+ case 'return':
107
+ case 'enter':
108
+ cleanup(idx);
109
+ return;
110
+ case 'escape':
111
+ cleanup(-1);
112
+ return;
113
+ default:
114
+ break;
115
+ }
116
+ const ch = str ?? '';
117
+ if (/^[0-9]$/.test(ch)) {
118
+ const n = Number.parseInt(ch, 10);
119
+ if (n >= 1 && n <= items.length && items[n - 1] !== '')
120
+ cleanup(n - 1);
121
+ return;
122
+ }
123
+ if (ch === 'q')
124
+ cleanup(-1);
125
+ };
126
+ stdin.on('keypress', onKey);
127
+ render();
128
+ });
129
+ }
130
+ /** 非交互回退:打印列表 + 读一行序号。 */
131
+ async function fallbackSelect(opts, items) {
132
+ const stdout = process.stdout;
133
+ stdout.write('\n');
134
+ if (opts.title)
135
+ stdout.write(` ${opts.title}\n\n`);
136
+ items.forEach((it, i) => {
137
+ if (it !== '')
138
+ stdout.write(` ${i + 1}. ${it}\n`);
139
+ });
140
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
141
+ const ans = await new Promise((res) => rl.question(` ${T('menu.prompt')}`, res));
142
+ rl.close();
143
+ const t = ans.trim();
144
+ if (t === 'q')
145
+ return -1;
146
+ if (/^\d+$/.test(t)) {
147
+ const n = Number.parseInt(t, 10);
148
+ if (n >= 1 && n <= items.length && items[n - 1] !== '')
149
+ return n - 1;
150
+ }
151
+ return -1;
152
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * 文本输入。**不用 inquirer**——它的 readline 会和自绘 selectMenu 的 raw 模式抢 stdin、收不到按键。
3
+ * 改回现版 PS 的两套机制(同一时刻只有一套在跑,互不干扰):
4
+ *
5
+ * · readValue:raw 逐键行编辑器(与 selectMenu 同机制,已验证可用),用于 ASCII 字段(密钥/模型/effort)。
6
+ * 密钥回显 `*`。语义对齐 Read-Value:回车空=不改、`-`=清空、Esc=取消、Ctrl+C 退出。
7
+ * · readText :cooked 模式 readline(兼容中文输入法,评审④),用于中文字段(备注/自定义名/手动地址)。
8
+ */
9
+ import { createInterface, emitKeypressEvents } from 'node:readline';
10
+ import { T } from '../i18n/index.js';
11
+ /** raw 逐键读 ASCII 字段:回车空=不改、`-`=清空、其它=替换、Esc/Ctrl+C=取消。 */
12
+ export async function readValue(label, current, secret = false) {
13
+ const stdin = process.stdin;
14
+ const stdout = process.stdout;
15
+ const cur = current === '' ? T('empty.paren') : secret ? '********' : current;
16
+ stdout.write(`\n ${label} [${T('edit.current', cur)}] ${T('edit.inputHint')}\n > `);
17
+ if (!stdin.isTTY) {
18
+ const line = await readText(''); // 非交互回退
19
+ if (line === undefined || line === '')
20
+ return { changed: false, value: current };
21
+ if (line === '-')
22
+ return { changed: true, value: '' };
23
+ return { changed: true, value: line };
24
+ }
25
+ emitKeypressEvents(stdin);
26
+ const wasRaw = stdin.isRaw ?? false;
27
+ stdin.setRawMode(true);
28
+ stdin.resume();
29
+ let buf = '';
30
+ return new Promise((resolve) => {
31
+ const cleanup = () => {
32
+ stdin.off('keypress', onKey);
33
+ if (!wasRaw)
34
+ stdin.setRawMode(false);
35
+ stdin.pause();
36
+ stdout.write('\n');
37
+ };
38
+ const onKey = (str, key) => {
39
+ if (key?.ctrl && key.name === 'c') {
40
+ cleanup();
41
+ resolve({ changed: false, value: current });
42
+ process.exit(130);
43
+ }
44
+ if (key?.name === 'return' || key?.name === 'enter') {
45
+ cleanup();
46
+ if (buf === '')
47
+ resolve({ changed: false, value: current });
48
+ else if (buf === '-')
49
+ resolve({ changed: true, value: '' });
50
+ else
51
+ resolve({ changed: true, value: buf });
52
+ return;
53
+ }
54
+ if (key?.name === 'escape') {
55
+ cleanup();
56
+ resolve({ changed: false, value: current });
57
+ return;
58
+ }
59
+ if (key?.name === 'backspace') {
60
+ if (buf.length > 0) {
61
+ buf = buf.slice(0, -1);
62
+ stdout.write('\b \b');
63
+ }
64
+ return;
65
+ }
66
+ // 普通字符(含粘贴的多字符),过滤控制字符
67
+ if (str && !key?.ctrl && !key?.meta) {
68
+ const printable = [...str].filter((c) => c >= ' ' && c !== '\x7f').join('');
69
+ if (printable) {
70
+ buf += printable;
71
+ stdout.write(secret ? '*'.repeat(printable.length) : printable);
72
+ }
73
+ }
74
+ };
75
+ stdin.on('keypress', onKey);
76
+ });
77
+ }
78
+ /** cooked 模式读一行(兼容中文输入法);Ctrl+C/中止返回 undefined。 */
79
+ export async function readText(message) {
80
+ const stdin = process.stdin;
81
+ if (stdin.isTTY)
82
+ stdin.setRawMode(false); // 确保 cooked,输入法才能组词
83
+ stdin.resume();
84
+ const rl = createInterface({ input: stdin, output: process.stdout });
85
+ return new Promise((resolve) => {
86
+ let done = false;
87
+ rl.on('SIGINT', () => {
88
+ if (done)
89
+ return;
90
+ done = true;
91
+ rl.close();
92
+ resolve(undefined);
93
+ });
94
+ rl.question(message ? `${message}` : ' > ', (ans) => {
95
+ if (done)
96
+ return;
97
+ done = true;
98
+ rl.close();
99
+ resolve(ans);
100
+ });
101
+ });
102
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * 极简 ANSI 颜色 + 光标控制(不引 chalk,零依赖)。
3
+ * 非 TTY 或设了 NO_COLOR 时自动退化为无色,方便管道/重定向。
4
+ */
5
+ const enabled = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
6
+ const wrap = (open, close) => (s) => enabled ? `\x1b[${open}m${s}\x1b[${close}m` : s;
7
+ export const green = wrap(32, 39);
8
+ export const yellow = wrap(33, 39);
9
+ export const cyan = wrap(36, 39);
10
+ export const red = wrap(31, 39);
11
+ export const dim = wrap(2, 22);
12
+ export const bold = wrap(1, 22);
13
+ export function paint(s, color) {
14
+ switch (color) {
15
+ case 'green':
16
+ return green(s);
17
+ case 'yellow':
18
+ return yellow(s);
19
+ case 'cyan':
20
+ return cyan(s);
21
+ case 'red':
22
+ return red(s);
23
+ case 'dim':
24
+ return dim(s);
25
+ case 'bold':
26
+ return bold(s);
27
+ default:
28
+ return s;
29
+ }
30
+ }
31
+ // —— 光标 / 屏幕控制 ——
32
+ export const HIDE_CURSOR = '\x1b[?25l';
33
+ export const SHOW_CURSOR = '\x1b[?25h';
34
+ /** 光标上移 n 行(n<=0 时为空串)。 */
35
+ export const cursorUp = (n) => (n > 0 ? `\x1b[${n}A` : '');
36
+ /** 从光标处清到屏幕末尾。 */
37
+ export const CLEAR_DOWN = '\x1b[0J';
38
+ /** 回到行首。 */
39
+ export const CR = '\r';
40
+ /** 清屏 + 清回滚 + 光标归位(等价于 PowerShell 的 Clear-Host,制造「整页」感)。 */
41
+ export const CLEAR_SCREEN = '\x1b[2J\x1b[3J\x1b[H';
@@ -0,0 +1,28 @@
1
+ /**
2
+ * 终端显示宽度工具:CJK/全角算 2、半角算 1。
3
+ * 用 `string-width`(封装 eastasianwidth)替代现版手写的码点区间判断;切英文后对齐照样成立。
4
+ */
5
+ import stringWidth from 'string-width';
6
+ export function displayWidth(s) {
7
+ return stringWidth(s);
8
+ }
9
+ /** 按显示宽度在右侧补空格到 `width`(不足才补,超出原样返回)。 */
10
+ export function padDisplay(s, width) {
11
+ const w = stringWidth(s);
12
+ return w < width ? s + ' '.repeat(width - w) : s;
13
+ }
14
+ /** 按显示宽度截断到 `max`(防止超宽行在终端换行打乱原地重绘的行数计算)。 */
15
+ export function truncateDisplay(s, max) {
16
+ if (stringWidth(s) <= max)
17
+ return s;
18
+ let w = 0;
19
+ let out = '';
20
+ for (const ch of s) {
21
+ const cw = stringWidth(ch);
22
+ if (w + cw > max)
23
+ break;
24
+ out += ch;
25
+ w += cw;
26
+ }
27
+ return out;
28
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@cc-x/cc-x",
3
+ "version": "0.3.0",
4
+ "description": "Claude Code API 切换器(命令 xx):在官方账号与第三方 Anthropic 兼容 API 间切换,纯环境变量、不碰 Claude Code 配置文件。",
5
+ "keywords": [
6
+ "claude",
7
+ "claude-code",
8
+ "anthropic",
9
+ "api",
10
+ "switcher",
11
+ "cli",
12
+ "deepseek",
13
+ "glm"
14
+ ],
15
+ "homepage": "https://github.com/becomeless/cc-x#readme",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/becomeless/cc-x.git"
19
+ },
20
+ "license": "MIT",
21
+ "author": "becomeless",
22
+ "type": "module",
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "bin": {
27
+ "xx": "dist/index.js"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "presets.json",
32
+ "README.md",
33
+ "README.en.md"
34
+ ],
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
38
+ "scripts": {
39
+ "build": "tsc",
40
+ "dev": "tsx src/index.ts",
41
+ "start": "node dist/index.js",
42
+ "typecheck": "tsc --noEmit",
43
+ "prepublishOnly": "npm run build"
44
+ },
45
+ "dependencies": {
46
+ "commander": "^13.0.0",
47
+ "string-width": "^7.2.0",
48
+ "which": "^5.0.0"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^22.10.0",
52
+ "@types/which": "^3.0.4",
53
+ "tsx": "^4.19.0",
54
+ "typescript": "^5.7.0"
55
+ }
56
+ }
package/presets.json ADDED
@@ -0,0 +1,36 @@
1
+ [
2
+ {
3
+ "name": "DeepSeek",
4
+ "auth": "AUTH_TOKEN",
5
+ "effort": "max",
6
+ "urls": [
7
+ { "label": "Anthropic 兼容", "url": "https://api.deepseek.com/anthropic" }
8
+ ],
9
+ "models": { "opus": "deepseek-v4-pro", "sonnet": "deepseek-v4-pro", "haiku": "deepseek-v4-flash" }
10
+ },
11
+ {
12
+ "name": "智谱GLM",
13
+ "auth": "AUTH_TOKEN",
14
+ "urls": [
15
+ { "label": "Anthropic 兼容", "url": "https://open.bigmodel.cn/api/anthropic" }
16
+ ],
17
+ "models": { "opus": "GLM-4.7", "sonnet": "GLM-4.7", "haiku": "glm-4.5-air" }
18
+ },
19
+ {
20
+ "name": "小米MiMo",
21
+ "auth": "AUTH_TOKEN",
22
+ "urls": [
23
+ { "label": "按量付费API", "url": "https://api.xiaomimimo.com/anthropic" },
24
+ { "label": "TokenPlan", "url": "https://token-plan-cn.xiaomimimo.com/anthropic" }
25
+ ],
26
+ "models": { "opus": "mimo-v2.5-pro", "sonnet": "mimo-v2.5-pro", "haiku": "mimo-v2.5-pro" }
27
+ },
28
+ {
29
+ "name": "官方Anthropic",
30
+ "auth": "API_KEY",
31
+ "urls": [
32
+ { "label": "(留空,用登录态)", "url": "" }
33
+ ],
34
+ "models": {}
35
+ }
36
+ ]