@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/dist/i18n.js ADDED
@@ -0,0 +1,195 @@
1
+ import { readJSON, writeJSON, CONFIG_FILE } from './config.js';
2
+ export const LOCALES = [
3
+ { value: 'en', label: 'English' },
4
+ { value: 'zh-CN', label: '简体中文' },
5
+ ];
6
+ const SUPPORTED = LOCALES.map((l) => l.value);
7
+ /** 读取 ~/.ccs/config.json(不存在则空对象)。 */
8
+ export function getConfig() {
9
+ return readJSON(CONFIG_FILE, {}) || {};
10
+ }
11
+ /** 合并写入 ~/.ccs/config.json。 */
12
+ export function setConfig(patch) {
13
+ const next = { ...getConfig(), ...patch };
14
+ writeJSON(CONFIG_FILE, next);
15
+ return next;
16
+ }
17
+ /**
18
+ * 语言探测顺序:config.locale → LC_ALL → LC_MESSAGES → LANG → 'en'。
19
+ * 形如 zh_CN / zh_TW / zh-Hans 均归为 zh-CN,其余归 en。
20
+ */
21
+ export function detectLocale() {
22
+ const cfg = getConfig();
23
+ if (cfg.locale && SUPPORTED.includes(cfg.locale))
24
+ return cfg.locale;
25
+ const envVal = process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || '';
26
+ return /^zh/i.test(envVal) ? 'zh-CN' : 'en';
27
+ }
28
+ const DICT = {
29
+ // ---------- generic ----------
30
+ 'common.cancelled': { en: 'Cancelled', 'zh-CN': '已取消' },
31
+ 'common.yes': { en: 'Yes', 'zh-CN': '是' },
32
+ 'common.no': { en: 'No', 'zh-CN': '否' },
33
+ // ---------- errors ----------
34
+ 'error.unknownArg': { en: 'Unknown argument: {arg}', 'zh-CN': '未知参数: {arg}' },
35
+ 'error.generic': { en: 'Error: {msg}', 'zh-CN': '错误: {msg}' },
36
+ 'error.invalidName': { en: 'Invalid config name: {name}', 'zh-CN': '非法配置名: {name}' },
37
+ 'error.exists': { en: 'Already exists: {name}. Use `ccs edit {name}` to modify, or pick another name.', 'zh-CN': '已存在: {name}。用 ccs edit {name} 修改,或换个名字。' },
38
+ 'error.notFound': { en: 'Not found: {name}', 'zh-CN': '未找到: {name}' },
39
+ 'error.providerMissing': {
40
+ en: 'Provider config not found: {name}\nRun `ccs list` to see available configs, or `ccs create` to create one.',
41
+ 'zh-CN': '未找到供应商配置: {name}\n运行 ccs list 查看可用配置,或 ccs create 创建。',
42
+ },
43
+ 'error.claudeBin': {
44
+ en: 'claude executable not found: {bin}\nMake sure Claude Code is installed, or set CCS_CLAUDE_BIN.',
45
+ 'zh-CN': '未找到 claude 可执行文件: {bin}\n请确认 Claude Code 已安装,或用 CCS_CLAUDE_BIN 指定路径。',
46
+ },
47
+ 'error.jsonParse': { en: 'Failed to parse JSON: {file}\n{msg}', 'zh-CN': '解析 JSON 失败: {file}\n{msg}' },
48
+ // ---------- usage ----------
49
+ 'usage.createName': { en: 'Usage: ccs create [name]', 'zh-CN': '用法: ccs create [name]' },
50
+ 'usage.editName': { en: 'Usage: ccs edit <name> [--raw]', 'zh-CN': '用法: ccs edit <name> [--raw]' },
51
+ 'usage.removeName': { en: 'Usage: ccs remove <name>', 'zh-CN': '用法: ccs remove <name>' },
52
+ 'usage.showName': { en: 'Usage: ccs show <name>', 'zh-CN': '用法: ccs show <name>' },
53
+ // ---------- list ----------
54
+ 'list.empty': {
55
+ en: 'No provider configs yet. Run `ccs create` or `ccs init` to create one.',
56
+ 'zh-CN': '暂无供应商配置。运行 ccs create 或 ccs init 创建。',
57
+ },
58
+ 'list.header': { en: 'Available provider configs:', 'zh-CN': '可用供应商配置:' },
59
+ 'list.summary': {
60
+ en: '{count} total. Use `ccs <name>` to launch, or just `ccs` to pick interactively.',
61
+ 'zh-CN': '共 {count} 个。用 ccs <name> 启动,或直接 ccs 交互选择。',
62
+ },
63
+ 'list.lastUsed': { en: 'last used', 'zh-CN': '上次使用' },
64
+ // ---------- presets ----------
65
+ 'presets.header': { en: 'Available presets:', 'zh-CN': '可用预设:' },
66
+ 'presets.fillUrl': { en: '(Base URL required)', 'zh-CN': '(需填写 Base URL)' },
67
+ 'presets.footer': {
68
+ en: 'Use `ccs create` and pick a built-in provider to create from a preset.',
69
+ 'zh-CN': '用 ccs create 并选择内置供应商来快速创建。',
70
+ },
71
+ 'presets.userFile': { en: 'Custom/override presets: ~/.ccs/presets.json', 'zh-CN': '自定义/覆盖预设: ~/.ccs/presets.json' },
72
+ // ---------- create flow ----------
73
+ 'create.kindTitle': { en: 'Create provider config', 'zh-CN': '创建供应商配置' },
74
+ 'create.kindPrompt': { en: 'Built-in provider or custom?', 'zh-CN': '选择内置供应商或自定义' },
75
+ 'create.kindBuiltin': { en: 'Built-in provider (preset prefilled, name customizable)', 'zh-CN': '内置供应商(预填预设,名称可自定义)' },
76
+ 'create.kindCustom': { en: 'Custom (your own name, blank form)', 'zh-CN': '自定义(自定义名称,空白表单)' },
77
+ 'create.builtinPrompt': { en: 'Select a built-in provider', 'zh-CN': '选择内置供应商' },
78
+ 'create.namePrompt': { en: 'Config name (default: {default})', 'zh-CN': '配置名(默认: {default})' },
79
+ 'create.customNamePrompt': { en: 'Config name (e.g. glm / deepseek / myprov)', 'zh-CN': '配置名 (如 glm / deepseek / myprov)' },
80
+ 'create.customNameValidate': { en: 'Name cannot be empty', 'zh-CN': '名称不能为空' },
81
+ 'create.created': { en: 'Created: {file}', 'zh-CN': '已创建: {file}' },
82
+ // ---------- edit / remove ----------
83
+ 'edit.title': { en: 'Edit provider config: {name}', 'zh-CN': '编辑供应商配置: {name}' },
84
+ 'edit.updated': { en: 'Updated: {file}', 'zh-CN': '已更新: {file}' },
85
+ 'remove.title': { en: 'Remove provider config: {name}', 'zh-CN': '删除供应商配置: {name}' },
86
+ 'remove.confirm': { en: 'Confirm remove {name}?', 'zh-CN': '确认删除 {name}?' },
87
+ 'remove.done': { en: 'Removed: {name}', 'zh-CN': '已删除: {name}' },
88
+ // ---------- form ----------
89
+ 'form.baseUrl': { en: 'Base URL (ANTHROPIC_BASE_URL)', 'zh-CN': 'Base URL (ANTHROPIC_BASE_URL)' },
90
+ 'form.baseUrlValidate': { en: 'Base URL cannot be empty', 'zh-CN': 'Base URL 不能为空' },
91
+ 'form.placeholderUrl': { en: 'https://...', 'zh-CN': 'https://...' },
92
+ 'form.apiKeyKeep': { en: 'API Key (leave empty to keep current)', 'zh-CN': 'API Key (留空保留原值)' },
93
+ 'form.apiKey': { en: 'API Key', 'zh-CN': 'API Key' },
94
+ 'form.model': { en: 'Default model (ANTHROPIC_MODEL)', 'zh-CN': '默认模型 (ANTHROPIC_MODEL)' },
95
+ 'form.modelValidate': { en: 'Model cannot be empty', 'zh-CN': '模型不能为空' },
96
+ 'form.tier': { en: 'Default tier (settings.model)', 'zh-CN': '默认档位 (settings.model)' },
97
+ 'form.aliasesPrompt': { en: 'Use tier-alias mode? (Yes: settings.model=opus + per-tier ANTHROPIC_DEFAULT_*_MODEL, /model switches between them. No: single ANTHROPIC_MODEL.)', 'zh-CN': '使用档位别名模式?(是:settings.model=opus + 各档位 ANTHROPIC_DEFAULT_*_MODEL,可用 /model 切换;否:单一 ANTHROPIC_MODEL)' },
98
+ 'form.aliasModel': { en: '{tier} model ({var})', 'zh-CN': '{tier} 模型 ({var})' },
99
+ // ---------- options (tab 3) ----------
100
+ 'form.optionsAttribution': { en: 'Enable CLAUDE_CODE_ATTRIBUTION_HEADER? (adds attribution to git commits)', 'zh-CN': '启用 CLAUDE_CODE_ATTRIBUTION_HEADER?(向 git 提交追加归属信息)' },
101
+ 'form.optionsNonEssential': { en: 'Enable CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC? (disables telemetry/analytics traffic)', 'zh-CN': '启用 CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC?(禁用遥测/分析流量)' },
102
+ 'form.optionsAutoCompact': { en: 'Auto-compact window in tokens (CLAUDE_CODE_AUTO_COMPACT_WINDOW)', 'zh-CN': '自动压缩窗口(token 数,CLAUDE_CODE_AUTO_COMPACT_WINDOW)' },
103
+ 'form.autoCompactValidate': { en: 'Enter a positive integer', 'zh-CN': '请输入正整数' },
104
+ 'form.autoCompactPlaceholder': { en: '200000', 'zh-CN': '200000' },
105
+ // ---------- tab navigation ----------
106
+ 'tab.apikey': { en: 'API Key', 'zh-CN': 'API Key' },
107
+ 'tab.models': { en: 'Models', 'zh-CN': '模型' },
108
+ 'tab.options': { en: 'Options', 'zh-CN': '其他配置' },
109
+ 'tab.review': { en: 'Review', 'zh-CN': '预览/提交' },
110
+ 'tab.preview': { en: 'Preview', 'zh-CN': '预览' },
111
+ 'tab.submit': { en: 'Submit', 'zh-CN': '提交保存' },
112
+ 'tab.cancel': { en: 'Cancel', 'zh-CN': '取消' },
113
+ 'tab.filled': { en: 'filled', 'zh-CN': '已填' },
114
+ 'tab.unfilled': { en: 'not filled', 'zh-CN': '未填' },
115
+ 'tab.previewTitle': { en: 'Preview (secrets redacted)', 'zh-CN': '配置预览(密钥已遮蔽)' },
116
+ 'tab.submitConfirm': { en: 'Save this provider config?', 'zh-CN': '保存该供应商配置?' },
117
+ // ---------- inline form field labels & help ----------
118
+ 'form.fBaseUrl': { en: 'Base URL', 'zh-CN': 'Base URL' },
119
+ 'form.fToken': { en: 'API Key', 'zh-CN': 'API Key' },
120
+ 'form.fTokenKeep': { en: 'API Key (leave empty to keep current)', 'zh-CN': 'API Key (留空保留原值)' },
121
+ 'form.fTokenKept': { en: '(keeping current)', 'zh-CN': '(保留原值)' },
122
+ 'form.fAliases': { en: 'Tier-alias mode', 'zh-CN': '档位别名模式' },
123
+ 'form.fModel': { en: 'Default model', 'zh-CN': '默认模型' },
124
+ 'form.fTier': { en: 'Default tier', 'zh-CN': '默认档位' },
125
+ 'form.fAliasShort': { en: '{tier} model', 'zh-CN': '{tier} 模型' },
126
+ 'form.fAttr': { en: 'Attribution header (CLAUDE_CODE_ATTRIBUTION_HEADER)', 'zh-CN': '归属信息 (CLAUDE_CODE_ATTRIBUTION_HEADER)' },
127
+ 'form.fNonEss': { en: 'Disable non-essential traffic (CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC)', 'zh-CN': '禁用非必要流量 (CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC)' },
128
+ 'form.fAutoCompact': { en: 'Auto-compact window (CLAUDE_CODE_AUTO_COMPACT_WINDOW)', 'zh-CN': '自动压缩窗口 (CLAUDE_CODE_AUTO_COMPACT_WINDOW)' },
129
+ 'form.fEffort': { en: 'Reasoning effort (CLAUDE_CODE_EFFORT_LEVEL)', 'zh-CN': '推理强度 (CLAUDE_CODE_EFFORT_LEVEL)' },
130
+ 'form.fAuthMethod': { en: 'Auth Method', 'zh-CN': '认证方式' },
131
+ 'form.authToken': { en: 'Auth Token (ANTHROPIC_AUTH_TOKEN)', 'zh-CN': 'Auth Token (ANTHROPIC_AUTH_TOKEN)' },
132
+ 'form.apiUrlKey': { en: 'API Key (ANTHROPIC_API_KEY)', 'zh-CN': 'API Key (ANTHROPIC_API_KEY)' },
133
+ 'form.fCustomParams': { en: 'Custom Params (JSON)', 'zh-CN': '自定义参数 (JSON)' },
134
+ 'form.customParamsValidate': { en: 'Custom params JSON is invalid', 'zh-CN': '自定义参数 JSON 格式无效' },
135
+ 'form.customParamsHint': { en: 'Extra env key-value pairs as JSON, e.g. {"allowed_openai_params": {"max_tokens": 8192}}', 'zh-CN': '额外 env 键值对 JSON,例如 {"allowed_openai_params": {"max_tokens": 8192}}' },
136
+ 'form.spaceHint': { en: '(Space to change)', 'zh-CN': '(空格切换)' },
137
+ 'tab.next': { en: 'Next →', 'zh-CN': '下一个 →' },
138
+ 'form.acHint': { en: '↑↓=pick Enter=use', 'zh-CN': '↑↓=选 Enter=用' },
139
+ 'form.help': {
140
+ en: 'Tab=switch tab ↑↓=field (↑↓=pick model on model fields) ←→=cursor (in text) Space=toggle/cycle n=next tab Enter=next/accept/submit Esc=cancel',
141
+ 'zh-CN': 'Tab=切标签 ↑↓=切字段(模型字段上 ↑↓=选模型) ←→=光标(文本内) Space=开关/切换 n=下一标签 Enter=下一步/采纳/提交 Esc=取消',
142
+ },
143
+ // ---------- preview / edit loop ----------
144
+ 'preview.title': { en: 'Preview (secrets redacted)', 'zh-CN': '配置预览(密钥已遮蔽)' },
145
+ 'preview.prompt': { en: 'Save, re-edit the form, or edit raw JSON?', 'zh-CN': '保存、重新编辑表单,还是编辑原始 JSON?' },
146
+ 'preview.save': { en: 'Save', 'zh-CN': '保存' },
147
+ 'preview.reedit': { en: 'Re-edit form', 'zh-CN': '重新编辑表单' },
148
+ 'preview.editRaw': { en: 'Edit raw JSON in $EDITOR', 'zh-CN': '在 $EDITOR 中编辑原始 JSON' },
149
+ 'preview.rawParseError': { en: 'Raw JSON invalid, keeping previous result. {msg}', 'zh-CN': '原始 JSON 解析失败,保留上一次结果。{msg}' },
150
+ // ---------- use ----------
151
+ 'use.select': { en: 'Select a provider', 'zh-CN': '选择供应商' },
152
+ 'use.direct': { en: 'direct', 'zh-CN': 'direct' },
153
+ 'use.directHint': { en: 'Claude Code default config (no provider merge)', 'zh-CN': 'Claude Code 默认配置(不走供应商)' },
154
+ 'use.create': { en: 'create', 'zh-CN': 'create' },
155
+ 'use.createHint': { en: 'Create a new provider', 'zh-CN': '创建新供应商' },
156
+ 'use.edit': { en: 'edit', 'zh-CN': 'edit' },
157
+ 'use.editHint': { en: 'Edit an existing config', 'zh-CN': '修改已有配置' },
158
+ 'use.remove': { en: 'remove', 'zh-CN': 'remove' },
159
+ 'use.removeHint': { en: 'Remove a config', 'zh-CN': '删除配置' },
160
+ 'use.editSelect': { en: 'Select a config to edit', 'zh-CN': '选择要修改的配置' },
161
+ 'use.removeSelect': { en: 'Select a config to remove', 'zh-CN': '选择要删除的配置' },
162
+ // ---------- picker (ink search-select) ----------
163
+ 'picker.placeholder': { en: 'type to filter providers...', 'zh-CN': '输入过滤供应商...' },
164
+ 'picker.noMatch': { en: 'no match', 'zh-CN': '无匹配' },
165
+ 'picker.providers': { en: 'Providers ({count})', 'zh-CN': '供应商配置 ({count})' },
166
+ 'picker.actions': { en: 'Actions', 'zh-CN': '操作' },
167
+ 'picker.help': { en: 'type=filter providers ↑↓=select Tab=jump region Enter=confirm Esc=cancel', 'zh-CN': '输入=过滤供应商 ↑↓=选择 Tab=跳转区域 Enter=确认 Esc=取消' },
168
+ // ---------- common / show / init ----------
169
+ 'common.openEditor': { en: 'Opening ~/.claude/settings.json (common config) in editor...', 'zh-CN': '在编辑器中打开 ~/.claude/settings.json (通用配置)...' },
170
+ 'common.createdEmpty': { en: 'Created empty settings file: {file}', 'zh-CN': '已创建空配置文件: {file}' },
171
+ // ---------- launch / dry-run ----------
172
+ 'launch.willRun': { en: 'Will run: {cmd}', 'zh-CN': '将执行: {cmd}' },
173
+ 'launch.dryTmp': { en: '(settings file: {file} — the provider config itself, passed directly to --settings; dry-run does not launch or create any file)', 'zh-CN': '(配置文件: {file},即 provider 配置本身,直接作为 --settings 目标;dry-run 不启动、不创建文件)' },
174
+ 'launch.dryDirect': { en: '(direct mode: no merge, no temp file — uses ~/.claude/settings.json as-is)', 'zh-CN': '(direct 模式:不合并、无临时文件,直接用 ~/.claude/settings.json)' },
175
+ // ---------- config ----------
176
+ 'config.localeSet': { en: 'Locale set to {locale}', 'zh-CN': '语言已设为 {locale}' },
177
+ 'config.localeCurrent': { en: 'Current locale: {locale}', 'zh-CN': '当前语言: {locale}' },
178
+ 'config.localeInvalid': { en: 'Invalid locale. Choose from: {opts}', 'zh-CN': '无效语言。可选: {opts}' },
179
+ 'config.localePrompt': { en: 'Select language', 'zh-CN': '选择语言' },
180
+ 'config.unknownKey': { en: 'Unknown config key: {key}. Try `ccs config locale`.', 'zh-CN': '未知配置项: {key}。试试 ccs config locale。' },
181
+ };
182
+ /**
183
+ * 翻译。t(key, { name: 'foo' }) 替换 {name}。
184
+ */
185
+ export function t(key, vars) {
186
+ const entry = DICT[key];
187
+ const locale = detectLocale();
188
+ let s = entry ? (entry[locale] ?? entry.en ?? key) : key;
189
+ if (vars) {
190
+ for (const [k, v] of Object.entries(vars)) {
191
+ s = s.replaceAll(`{${k}}`, String(v));
192
+ }
193
+ }
194
+ return s;
195
+ }
@@ -0,0 +1,25 @@
1
+ export interface InkSelectOption<T> {
2
+ value: T;
3
+ label: string;
4
+ hint?: string;
5
+ }
6
+ export interface InkSelectParams<T> {
7
+ message: string;
8
+ options: ReadonlyArray<InkSelectOption<T>>;
9
+ initialValue?: T;
10
+ }
11
+ export interface InkTextParams {
12
+ message: string;
13
+ placeholder?: string;
14
+ initialValue?: string;
15
+ validate?: (value: string) => string | Error | undefined;
16
+ }
17
+ export interface InkConfirmParams {
18
+ message: string;
19
+ active?: string;
20
+ inactive?: string;
21
+ initialValue?: boolean;
22
+ }
23
+ export declare function inkSelect<T>(opts: InkSelectParams<T>): Promise<T>;
24
+ export declare function inkText(opts: InkTextParams): Promise<string>;
25
+ export declare function inkConfirm(opts: InkConfirmParams): Promise<boolean>;
@@ -0,0 +1,176 @@
1
+ import { Box, render, Text, useInput, useStdout } from 'ink';
2
+ import React, { useEffect, useState } from 'react';
3
+ import { Cancel } from './tui.js';
4
+ import { clearScreen } from './screen.js';
5
+ const h = React.createElement;
6
+ // ---------- shared helpers ----------
7
+ function insertAt(s, idx, ch) { return s.slice(0, idx) + ch + s.slice(idx); }
8
+ function eraseBack(s, idx) { return idx > 0 ? s.slice(0, idx - 1) + s.slice(idx) : s; }
9
+ function useBlink(deps) {
10
+ const [on, setOn] = useState(true);
11
+ useEffect(() => {
12
+ const id = setInterval(() => setOn((b) => !b), 530);
13
+ return () => clearInterval(id);
14
+ }, []);
15
+ useEffect(() => { setOn(true); }, deps);
16
+ return on;
17
+ }
18
+ // ---------- inkSelect ----------
19
+ function SelectApp({ message, options, initialIndex, onDone, onCancel }) {
20
+ const [index, setIndex] = useState(initialIndex);
21
+ useInput((input, key) => {
22
+ if (key.escape) {
23
+ onCancel();
24
+ return;
25
+ }
26
+ if (key.return || input === ' ') {
27
+ onDone(options[index].value);
28
+ return;
29
+ }
30
+ if (key.upArrow) {
31
+ setIndex((i) => (i - 1 + options.length) % options.length);
32
+ return;
33
+ }
34
+ if (key.downArrow) {
35
+ setIndex((i) => (i + 1) % options.length);
36
+ return;
37
+ }
38
+ });
39
+ return h(Box, { flexDirection: 'column' }, h(Text, { color: 'cyan', bold: true }, message), ...options.map((opt, i) => {
40
+ const sel = i === index;
41
+ const style = sel ? { backgroundColor: 'cyan', color: 'black' } : {};
42
+ return h(Box, { key: i, flexDirection: 'row' }, h(Text, style, `${sel ? '▸ ' : ' '}${opt.label}`), opt.hint ? h(Text, sel ? { backgroundColor: 'cyan', color: 'black' } : { dimColor: true }, ` ${opt.hint}`) : null);
43
+ }), h(Text, { dimColor: true }, '↑↓=select Enter=confirm Esc=cancel'));
44
+ }
45
+ export async function inkSelect(opts) {
46
+ const { message, options } = opts;
47
+ const arr = [...options];
48
+ if (!arr.length)
49
+ throw new Cancel();
50
+ let initialIndex = 0;
51
+ if (opts.initialValue !== undefined) {
52
+ const i = arr.findIndex((o) => o.value === opts.initialValue);
53
+ if (i >= 0)
54
+ initialIndex = i;
55
+ }
56
+ return new Promise((resolve, reject) => {
57
+ let inst;
58
+ const onDone = (v) => { inst.unmount(); resolve(v); };
59
+ const onCancel = () => { inst.unmount(); reject(new Cancel()); };
60
+ const App = SelectApp;
61
+ clearScreen();
62
+ inst = render(h(App, { message, options: arr, initialIndex, onDone, onCancel }));
63
+ });
64
+ }
65
+ // ---------- inkText ----------
66
+ function TextApp({ message, placeholder, initial, validate, onDone, onCancel }) {
67
+ const [value, setValue] = useState(initial);
68
+ const [cursor, setCursor] = useState(initial.length);
69
+ const [error, setError] = useState(null);
70
+ const { stdout } = useStdout();
71
+ const cols = stdout && stdout.columns ? stdout.columns : 60;
72
+ const blinkOn = useBlink([cursor]);
73
+ useInput((input, key) => {
74
+ if (key.escape) {
75
+ onCancel();
76
+ return;
77
+ }
78
+ if (key.return) {
79
+ if (validate) {
80
+ const r = validate(value);
81
+ if (typeof r === 'string') {
82
+ setError(r);
83
+ return;
84
+ }
85
+ if (r instanceof Error) {
86
+ setError(r.message);
87
+ return;
88
+ }
89
+ }
90
+ onDone(value);
91
+ return;
92
+ }
93
+ setError(null);
94
+ if (key.leftArrow) {
95
+ setCursor((c) => Math.max(0, c - 1));
96
+ return;
97
+ }
98
+ if (key.rightArrow) {
99
+ setCursor((c) => Math.min(value.length, c + 1));
100
+ return;
101
+ }
102
+ if (key.backspace || key.delete) {
103
+ setValue((v) => { const nv = eraseBack(v, cursor); setCursor((c) => Math.max(0, c - 1)); return nv; });
104
+ return;
105
+ }
106
+ if (input && !key.ctrl && !key.meta) {
107
+ let s = '';
108
+ for (const ch of input) {
109
+ if (ch.charCodeAt(0) >= 32)
110
+ s += ch;
111
+ }
112
+ if (s) {
113
+ setValue((v) => insertAt(v, cursor, s));
114
+ setCursor((c) => c + s.length);
115
+ }
116
+ }
117
+ });
118
+ const cur = Math.min(cursor, value.length);
119
+ const charAt = value.slice(cur, cur + 1);
120
+ const cursorNode = blinkOn ? h(Text, { color: 'cyan' }, '▏') : h(Text, null, charAt || ' ');
121
+ return h(Box, { flexDirection: 'column' }, h(Text, { color: 'cyan', bold: true }, message), h(Box, { flexDirection: 'row' }, value
122
+ ? h(Text, null, h(Text, { color: 'cyan' }, value.slice(0, cur)), cursorNode, h(Text, { color: 'cyan' }, value.slice(cur + 1)))
123
+ : h(Text, { dimColor: true }, placeholder ?? '')), error ? h(Text, { color: 'red' }, '✗ ' + error) : null, h(Text, { dimColor: true }, 'type value Enter=confirm Esc=cancel'));
124
+ }
125
+ export async function inkText(opts) {
126
+ const { message, initialValue = '' } = opts;
127
+ return new Promise((resolve, reject) => {
128
+ let inst;
129
+ const onDone = (v) => { inst.unmount(); resolve(v); };
130
+ const onCancel = () => { inst.unmount(); reject(new Cancel()); };
131
+ const props = { message, initial: initialValue, onDone, onCancel };
132
+ if (opts.placeholder !== undefined)
133
+ props.placeholder = opts.placeholder;
134
+ if (opts.validate !== undefined)
135
+ props.validate = opts.validate;
136
+ clearScreen();
137
+ inst = render(h(TextApp, props));
138
+ });
139
+ }
140
+ // ---------- inkConfirm ----------
141
+ function ConfirmApp({ message, active, inactive, initial, onDone, onCancel }) {
142
+ const [val, setVal] = useState(initial);
143
+ useInput((input, key) => {
144
+ if (key.escape) {
145
+ onCancel();
146
+ return;
147
+ }
148
+ if (key.return) {
149
+ onDone(val);
150
+ return;
151
+ }
152
+ if (key.leftArrow || key.rightArrow || input === ' ' || input === 'y' || input === 'n') {
153
+ if (input === 'y') {
154
+ setVal(true);
155
+ return;
156
+ }
157
+ if (input === 'n') {
158
+ setVal(false);
159
+ return;
160
+ }
161
+ setVal((v) => !v);
162
+ return;
163
+ }
164
+ });
165
+ return h(Box, { flexDirection: 'column' }, h(Box, { flexDirection: 'row' }, h(Text, { color: 'cyan', bold: true }, message + ' '), h(Text, val ? { backgroundColor: 'green', color: 'black' } : { dimColor: true }, val ? `[Y] ${active}` : `[ ] ${active}`), h(Text, null, ' '), h(Text, !val ? { backgroundColor: 'red', color: 'black' } : { dimColor: true }, !val ? `[N] ${inactive}` : `[ ] ${inactive}`)), h(Text, { dimColor: true }, 'y/n or ←→=toggle Enter=confirm Esc=cancel'));
166
+ }
167
+ export async function inkConfirm(opts) {
168
+ const { message, active = 'Yes', inactive = 'No', initialValue = false } = opts;
169
+ return new Promise((resolve, reject) => {
170
+ let inst;
171
+ const onDone = (v) => { inst.unmount(); resolve(v); };
172
+ const onCancel = () => { inst.unmount(); reject(new Cancel()); };
173
+ clearScreen();
174
+ inst = render(h(ConfirmApp, { message, active, inactive, initial: initialValue, onDone, onCancel }));
175
+ });
176
+ }
@@ -0,0 +1,27 @@
1
+ import type { ProviderSettings } from './types.js';
2
+ /** 展示用:遮蔽疑似密钥的 env 值(仅保留末 4 位)。 */
3
+ export declare function redactSettings(obj: ProviderSettings): ProviderSettings;
4
+ export declare function whichClaude(): string;
5
+ /**
6
+ * 打印 provider 配置片段与将要执行的命令,不启动 claude。
7
+ * `--settings` 直接指向 provider 配置文件本身——ccs 不再产出中间文件。
8
+ * 注意:展示的是 provider 片段,非 claude 最终加载的完整 settings(后者还会与
9
+ * ~/.claude/settings.json 深合,片段里未出现的 key 由下层继承)。
10
+ */
11
+ export declare function dryRun(name: string, forwardedArgs?: string[]): void;
12
+ /**
13
+ * 直接启动 claude,使用 Claude Code 默认配置(~/.claude/settings.json)。
14
+ * 不做合并、不写临时文件、不记录 lastUsed。
15
+ */
16
+ export declare function launchDirect(forwardedArgs?: string[]): void;
17
+ /**
18
+ * 打印 direct 模式将执行的命令,不启动。
19
+ */
20
+ export declare function dryRunDirect(forwardedArgs?: string[]): void;
21
+ /**
22
+ * 以 provider 配置文件作为 --settings 目标直接启动 claude(claude 自行与 ~/.claude/settings.json 深合)。
23
+ * 不解析 provider 对象、不写中间文件、不注入子进程 env——claude 加载 --settings 时自行读取 env。
24
+ * @param name 供应商名
25
+ * @param forwardedArgs 透传给 claude 的参数
26
+ */
27
+ export declare function launch(name: string, forwardedArgs?: string[]): void;
package/dist/launch.js ADDED
@@ -0,0 +1,108 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { readProvider, providerFile, providerExists, setLastUsed, } from './config.js';
3
+ import { t } from './i18n.js';
4
+ /** 展示用:遮蔽疑似密钥的 env 值(仅保留末 4 位)。 */
5
+ export function redactSettings(obj) {
6
+ const clone = JSON.parse(JSON.stringify(obj));
7
+ if (clone && typeof clone === 'object' && clone.env) {
8
+ for (const k of Object.keys(clone.env)) {
9
+ if (/TOKEN|KEY|SECRET|PASSWORD/i.test(k) && typeof clone.env[k] === 'string') {
10
+ const v = clone.env[k];
11
+ clone.env[k] = v.length > 4 ? `****${v.slice(-4)}` : '****';
12
+ }
13
+ }
14
+ }
15
+ return clone;
16
+ }
17
+ export function whichClaude() {
18
+ return process.env.CCS_CLAUDE_BIN || 'claude';
19
+ }
20
+ /**
21
+ * 清屏并擦除滚动缓冲,让 claude 接管一个干净窗口——视觉上与直接在命令行运行 claude 一致。
22
+ * 仅在交互式 TTY 下生效;非 TTY(管道/重定向)时跳过,避免向非终端写入转义序列。
23
+ */
24
+ function clearScreen() {
25
+ if (process.stdout.isTTY) {
26
+ // \x1b[3J 清滚动缓冲,\x1b[H 光标归位,\x1b[2J 清可见屏幕。
27
+ process.stdout.write('\x1b[3J\x1b[H\x1b[2J');
28
+ }
29
+ }
30
+ /**
31
+ * 打印 provider 配置片段与将要执行的命令,不启动 claude。
32
+ * `--settings` 直接指向 provider 配置文件本身——ccs 不再产出中间文件。
33
+ * 注意:展示的是 provider 片段,非 claude 最终加载的完整 settings(后者还会与
34
+ * ~/.claude/settings.json 深合,片段里未出现的 key 由下层继承)。
35
+ */
36
+ export function dryRun(name, forwardedArgs = []) {
37
+ const provider = readProvider(name);
38
+ if (!provider) {
39
+ throw new Error(t('error.providerMissing', { name }));
40
+ }
41
+ const file = providerFile(name);
42
+ const args = ['--settings', file, ...forwardedArgs];
43
+ console.log(JSON.stringify(redactSettings(provider), null, 2));
44
+ console.log(`\n${t('launch.willRun', { cmd: `${whichClaude()} ${args.join(' ')}` })}`);
45
+ console.log(t('launch.dryTmp', { file }));
46
+ }
47
+ /**
48
+ * 直接启动 claude,使用 Claude Code 默认配置(~/.claude/settings.json)。
49
+ * 不做合并、不写临时文件、不记录 lastUsed。
50
+ */
51
+ export function launchDirect(forwardedArgs = []) {
52
+ const bin = whichClaude();
53
+ const args = [...forwardedArgs];
54
+ // 擦除 ccs 自身 TUI 残留,给 claude 一个干净窗口(等同命令行直接启动)。
55
+ clearScreen();
56
+ let res;
57
+ try {
58
+ res = spawnSync(bin, args, { stdio: 'inherit', env: process.env });
59
+ }
60
+ catch (e) {
61
+ if (e.code === 'ENOENT') {
62
+ throw new Error(t('error.claudeBin', { bin }));
63
+ }
64
+ throw e;
65
+ }
66
+ if (res.status !== null && res.status !== 0) {
67
+ process.exitCode = res.status;
68
+ }
69
+ }
70
+ /**
71
+ * 打印 direct 模式将执行的命令,不启动。
72
+ */
73
+ export function dryRunDirect(forwardedArgs = []) {
74
+ const bin = whichClaude();
75
+ const args = [...forwardedArgs];
76
+ console.log(t('launch.willRun', { cmd: `${bin} ${args.join(' ')}` }));
77
+ console.log(t('launch.dryDirect'));
78
+ }
79
+ /**
80
+ * 以 provider 配置文件作为 --settings 目标直接启动 claude(claude 自行与 ~/.claude/settings.json 深合)。
81
+ * 不解析 provider 对象、不写中间文件、不注入子进程 env——claude 加载 --settings 时自行读取 env。
82
+ * @param name 供应商名
83
+ * @param forwardedArgs 透传给 claude 的参数
84
+ */
85
+ export function launch(name, forwardedArgs = []) {
86
+ if (!providerExists(name)) {
87
+ throw new Error(t('error.providerMissing', { name }));
88
+ }
89
+ const file = providerFile(name);
90
+ const bin = whichClaude();
91
+ const args = ['--settings', file, ...forwardedArgs];
92
+ setLastUsed(name);
93
+ // 擦除 ccs 自身 TUI 残留,给 claude 一个干净窗口(等同命令行直接启动)。
94
+ clearScreen();
95
+ let res;
96
+ try {
97
+ res = spawnSync(bin, args, { stdio: 'inherit', env: process.env });
98
+ }
99
+ catch (e) {
100
+ if (e.code === 'ENOENT') {
101
+ throw new Error(t('error.claudeBin', { bin }));
102
+ }
103
+ throw e;
104
+ }
105
+ if (res.status !== null && res.status !== 0) {
106
+ process.exitCode = res.status;
107
+ }
108
+ }
@@ -0,0 +1,24 @@
1
+ /** picker 的一项。 */
2
+ export interface PickerItem<T> {
3
+ value: T;
4
+ label: string;
5
+ hint?: string;
6
+ }
7
+ export interface PickerOpts<T> {
8
+ message: string;
9
+ /** 可过滤、可滚动的列表(如供应商配置)。 */
10
+ items: ReadonlyArray<PickerItem<T>>;
11
+ /** 固定在下方、不过滤的操作项(如 direct/create/edit/remove)。 */
12
+ actions?: ReadonlyArray<PickerItem<T>>;
13
+ /** 初始高亮项(按值相等定位)。 */
14
+ initialValue?: T;
15
+ /** items 区最多可见行数,默认 5。 */
16
+ maxItems?: number;
17
+ /** 顶部状态横幅(如 edit/remove 后的 ✓ Updated xxx);仅显示一个菜单周期。 */
18
+ statusMessage?: string;
19
+ }
20
+ /**
21
+ * 搜索选择器(ink):items 区可过滤可滚动,actions 区固定不过滤。
22
+ * ↑↓ 选择、Enter 确认、Esc 取消(抛 Cancel)。
23
+ */
24
+ export declare function runPicker<T>(opts: PickerOpts<T>): Promise<T>;