cprofile 0.4.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.
- package/LICENSE +21 -0
- package/README.md +56 -0
- package/bin/cprofile +3 -0
- package/bin/cprofile-cli +3267 -0
- package/docs/NAMING.zh-CN.md +24 -0
- package/docs/QUICKSTART.zh-CN.md +25 -0
- package/docs/README.zh-CN.md +39 -0
- package/docs/TROUBLESHOOTING.zh-CN.md +83 -0
- package/package.json +42 -0
- package/shell/cprofile.sh +46 -0
package/bin/cprofile-cli
ADDED
|
@@ -0,0 +1,3267 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const crypto = require('node:crypto');
|
|
8
|
+
const readline = require('node:readline');
|
|
9
|
+
const { spawnSync, spawn } = require('node:child_process');
|
|
10
|
+
|
|
11
|
+
const HOME = os.homedir();
|
|
12
|
+
const ROOT = path.join(HOME, '.cprofile');
|
|
13
|
+
const REGISTRY_PATH = path.join(ROOT, 'registry.json');
|
|
14
|
+
const STATE_DIR = path.join(ROOT, 'state');
|
|
15
|
+
const CURRENT_PATH = path.join(STATE_DIR, 'current.json');
|
|
16
|
+
const USAGE_CACHE_PATH = path.join(STATE_DIR, 'usage-cache.json');
|
|
17
|
+
const PROFILES_DIR = path.join(ROOT, 'profiles');
|
|
18
|
+
const LOCKS_DIR = path.join(ROOT, 'locks');
|
|
19
|
+
const DOCS_DIR = path.join(ROOT, 'docs');
|
|
20
|
+
const DEFAULT_USAGE_CACHE_SECONDS = 300;
|
|
21
|
+
const DEFAULT_CLAUDE_USAGE_EXPERIMENTAL = true;
|
|
22
|
+
|
|
23
|
+
const NAME_RE = /^[a-zA-Z0-9_-]{2,32}$/;
|
|
24
|
+
const TOOLS = new Set(['codex', 'claude']);
|
|
25
|
+
|
|
26
|
+
const DOC_TEMPLATES = {
|
|
27
|
+
'README.zh-CN.md': `# cprofile 使用说明(中文)\n\n## 什么是 cprofile\n\ncprofile 是一个“终端账号切换台”,用于本机管理和切换 Codex / Claude 多账号。\n\n## 3 步上手\n\n1. 输入 \`cprofile\` 直接进入账号列表。\n2. 方向键选择账号,回车立刻启动对应 CLI。\n3. 需要低频操作时,在列表页按快捷键(A/D/I/R/?)。\n\n## 使用特点\n\n- 默认主链路就是切换账号,不再先选功能菜单。\n- 列表只保留核心信息:账号 + 工作空间(Codex)+ 用量。\n- 用量口径统一为“已用/剩余”。\n\n## 列表快捷键\n\n- \`A\` 添加账号\n- \`D\` 诊断\n- \`I\` 导入旧目录\n- \`R\` 刷新用量\n- \`Tab\` 切换排序\n- \`Ctrl+P\` 收藏账号\n- \`Ctrl+A\` 显示/隐藏异常账号\n- \`?\` 查看帮助\n\n## 文档入口\n\n- \`cprofile guide\`:查看文档主题\n- \`cprofile guide quickstart\`\n- \`cprofile guide troubleshooting\`\n- \`cprofile guide naming\`\n\n## 高级命令入口\n\n- \`cprofile --advanced-help\`\n`,
|
|
28
|
+
'QUICKSTART.zh-CN.md': `# Quickstart(中文)\n\n## 步骤 1:打开账号列表\n\n运行:\n\n\`cprofile\`\n\n## 步骤 2:快速切换并启动\n\n- 输入关键词可过滤账号\n- 方向键选择,回车启动 codex/claude\n\n## 步骤 3:低频操作(仍在同一页)\n\n- \`A\`:添加账号\n- \`D\`:诊断\n- \`I\`:导入旧目录\n- \`R\`:刷新用量\n- \`?\`:查看快捷键帮助\n\n## 常见补充\n\n- 查看诊断:\`cprofile doctor\`\n- 查看全部命令:\`cprofile --advanced-help\`\n`,
|
|
29
|
+
'TROUBLESHOOTING.zh-CN.md': `# 故障排查(Troubleshooting)\n\n## 网络或代理问题\n\n现象:\n- 登录页打不开、卡住或超时\n\n一键动作:\n- 检查 \`HTTP_PROXY\` / \`HTTPS_PROXY\`\n- 重新执行首页“添加账号”向导\n\n预期结果:\n- 能正常弹出并完成登录\n\n## 登录失败\n\n现象:\n- 完成授权后仍提示未登录\n\n一键动作:\n- 执行 \`cprofile\` -> “添加账号”重新走向导登录\n\n预期结果:\n- \`快速切换\` 中状态显示“可用”\n\n## Token 失效\n\n现象:\n- 请求未授权、额度异常、突然掉线\n\n一键动作:\n- 用首页“添加账号”重新登录,覆盖旧凭证\n\n预期结果:\n- 同一账号恢复可用\n\n## 账号重复\n\n现象:\n- 列表里看起来是同一账号的重复项\n\n一键动作:\n- 先执行 \`cprofile doctor\`\n- 再执行 \`cprofile rm <slug> --tool <tool>\`\n\n预期结果:\n- 重复项被清理,\`doctor\` 不再报重复\n\n## 命名冲突\n\n现象:\n- 自定义短名提示已存在\n\n一键动作:\n- 在向导中换一个短名\n- 或使用 \`cprofile name suggest ...\` 预览建议名\n\n预期结果:\n- 新短名保存成功,可直接切换\n\n## 权限问题\n\n现象:\n- 报权限不足、文件写入失败\n\n一键动作:\n- 检查 \`~/.cprofile\` 权限应为 \`700\`\n- 敏感文件应为 \`600\`\n\n预期结果:\n- 命令恢复正常\n\n## 导入后仍显示旧账号\n\n现象:\n- 导入后分不清哪个是目标账号\n\n一键动作:\n- 执行 \`cprofile use\`,输入邮箱或空间名过滤\n- 必要时执行 \`cprofile rename <id> <new_slug> --tool <tool>\`\n\n预期结果:\n- 目标账号能被快速定位并切换\n`,
|
|
30
|
+
'NAMING.zh-CN.md': `# 命名规则(Naming V2)\n\n## 核心\n\n- 用户可见名:\`slug\`\n- 内部唯一:\`profile_id\`\n- 兼容旧名:\`aliases\`\n\n## 自动命名\n\n\`<email_short>_<space_short>_<h4>\`\n\n其中:\n- \`email_short\`:邮箱简写\n- \`space_short\`:空间名简写\n- \`h4\`:\`sha256(tool|account_key|space_key)\` 前 4 位\n\n## 多空间支持\n\n同一账号在不同空间会得到不同 \`space_key\`,因此 slug 天然可并存。\n\n## 兼容策略\n\n旧 \`name\` 会自动写入 \`aliases\`,旧命令可继续使用。\n`,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
class CliError extends Error {
|
|
34
|
+
constructor(message, code = 1) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.code = code;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function msg(zh, en) {
|
|
41
|
+
if (!en) return zh;
|
|
42
|
+
return `${zh} (${en})`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isInteractiveTTY() {
|
|
46
|
+
return Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function shouldUseColor() {
|
|
50
|
+
const mode = String(process.env.CPROFILE_UI_COLOR || 'auto').toLowerCase();
|
|
51
|
+
if (mode === 'always') return true;
|
|
52
|
+
if (mode === 'never') return false;
|
|
53
|
+
if (process.env.NO_COLOR) return false;
|
|
54
|
+
if (!isInteractiveTTY()) return false;
|
|
55
|
+
if (String(process.env.TERM || '').toLowerCase() === 'dumb') return false;
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isClaudeUsageExperimentalEnabled() {
|
|
60
|
+
const raw = String(process.env.CPROFILE_CLAUDE_USAGE_EXPERIMENTAL || '').trim().toLowerCase();
|
|
61
|
+
if (!raw) return DEFAULT_CLAUDE_USAGE_EXPERIMENTAL;
|
|
62
|
+
if (raw === '0' || raw === 'false' || raw === 'off' || raw === 'no') return false;
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isClaudeUsagePtyProbeEnabled() {
|
|
67
|
+
const raw = String(process.env.CPROFILE_CLAUDE_USAGE_PTY || '').trim().toLowerCase();
|
|
68
|
+
return raw === '1' || raw === 'true' || raw === 'on' || raw === 'yes';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const UI_COLORS = {
|
|
72
|
+
reset: '\x1b[0m',
|
|
73
|
+
title: '\x1b[36m',
|
|
74
|
+
selected: '\x1b[94m',
|
|
75
|
+
codex: '\x1b[96m',
|
|
76
|
+
claude: '\x1b[93m',
|
|
77
|
+
success: '\x1b[32m',
|
|
78
|
+
warning: '\x1b[33m',
|
|
79
|
+
danger: '\x1b[91m',
|
|
80
|
+
muted: '\x1b[90m',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
function colorize(text, color) {
|
|
84
|
+
if (!shouldUseColor()) return text;
|
|
85
|
+
const c = UI_COLORS[color] || '';
|
|
86
|
+
if (!c) return text;
|
|
87
|
+
return `${c}${text}${UI_COLORS.reset}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const UI_FRAME = {
|
|
91
|
+
lines: 0,
|
|
92
|
+
cursorHidden: false,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const UI_SORT_MODES = new Set(['recent', 'pin_recent', 'week_desc']);
|
|
96
|
+
|
|
97
|
+
function hideCursor(stream = process.stderr) {
|
|
98
|
+
if (UI_FRAME.cursorHidden) return;
|
|
99
|
+
stream.write('\x1b[?25l');
|
|
100
|
+
UI_FRAME.cursorHidden = true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function showCursor(stream = process.stderr) {
|
|
104
|
+
if (!UI_FRAME.cursorHidden) return;
|
|
105
|
+
stream.write('\x1b[?25h');
|
|
106
|
+
UI_FRAME.cursorHidden = false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function clearFrame(stream = process.stderr) {
|
|
110
|
+
if (UI_FRAME.lines > 0) {
|
|
111
|
+
stream.write(`\x1b[${UI_FRAME.lines}A\r`);
|
|
112
|
+
stream.write('\x1b[J');
|
|
113
|
+
UI_FRAME.lines = 0;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function renderFrame(lines, stream = process.stderr) {
|
|
118
|
+
clearFrame(stream);
|
|
119
|
+
hideCursor(stream);
|
|
120
|
+
stream.write(lines.join('\n') + '\n');
|
|
121
|
+
UI_FRAME.lines = lines.length;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function finalizeWithToast(text, options = {}) {
|
|
125
|
+
const stream = options.stream || process.stderr;
|
|
126
|
+
const tone = options.tone || 'success';
|
|
127
|
+
clearFrame(stream);
|
|
128
|
+
showCursor(stream);
|
|
129
|
+
const icon = tone === 'warning' ? '!' : '✓';
|
|
130
|
+
stream.write(`${colorize(icon, tone === 'warning' ? 'warning' : 'success')} ${text}\n`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function now() {
|
|
134
|
+
return new Date().toISOString();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function ensureDir(dir, mode = 0o700) {
|
|
138
|
+
fs.mkdirSync(dir, { recursive: true, mode });
|
|
139
|
+
try {
|
|
140
|
+
fs.chmodSync(dir, mode);
|
|
141
|
+
} catch {
|
|
142
|
+
// ignore
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function chmodSafe(file, mode) {
|
|
147
|
+
try {
|
|
148
|
+
fs.chmodSync(file, mode);
|
|
149
|
+
} catch {
|
|
150
|
+
// ignore
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function writeJsonAtomic(file, data, mode = 0o600) {
|
|
155
|
+
const dir = path.dirname(file);
|
|
156
|
+
ensureDir(dir, 0o700);
|
|
157
|
+
const tmp = path.join(dir, `.${path.basename(file)}.${process.pid}.${Date.now()}.tmp`);
|
|
158
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n', { mode });
|
|
159
|
+
chmodSafe(tmp, mode);
|
|
160
|
+
fs.renameSync(tmp, file);
|
|
161
|
+
chmodSafe(file, mode);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function readJsonSafe(file) {
|
|
165
|
+
if (!fs.existsSync(file)) return null;
|
|
166
|
+
try {
|
|
167
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function readJson(file, fallback) {
|
|
174
|
+
if (!fs.existsSync(file)) return fallback;
|
|
175
|
+
try {
|
|
176
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
177
|
+
} catch {
|
|
178
|
+
throw new CliError(msg(`JSON 文件无效: ${file}`, `Invalid JSON: ${file}`));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function sha(value) {
|
|
183
|
+
return crypto.createHash('sha256').update(String(value)).digest('hex');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function shortHash(value, len = 4) {
|
|
187
|
+
return sha(value).slice(0, len);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function shellQuote(value) {
|
|
191
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function normalizeSegment(input, maxLen = 12, fallback = 'x') {
|
|
195
|
+
const raw = (input || '').toString().trim().toLowerCase();
|
|
196
|
+
const normalized = raw
|
|
197
|
+
.replace(/@/g, '_')
|
|
198
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
199
|
+
.replace(/^_+|_+$/g, '')
|
|
200
|
+
.replace(/_+/g, '_');
|
|
201
|
+
const cut = normalized.slice(0, maxLen);
|
|
202
|
+
return cut || fallback;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function emailShort(email) {
|
|
206
|
+
const e = (email || '').toString().trim().toLowerCase();
|
|
207
|
+
if (!e.includes('@')) return normalizeSegment(e, 16, 'acct');
|
|
208
|
+
const [local, domainRaw] = e.split('@');
|
|
209
|
+
const domain = (domainRaw || '').split('.')[0] || 'mail';
|
|
210
|
+
return `${normalizeSegment(local, 10, 'acct')}_${normalizeSegment(domain, 8, 'mail')}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function spaceShort(spaceName) {
|
|
214
|
+
return normalizeSegment(spaceName || 'unknown', 12, 'unknown');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function normalizeSpaceText(value) {
|
|
218
|
+
return String(value || '').trim();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function isLikelyPersonalSpace(name) {
|
|
222
|
+
const raw = normalizeSpaceText(name).toLowerCase();
|
|
223
|
+
if (!raw) return true;
|
|
224
|
+
if (raw === 'personal' || raw === 'unknown') return true;
|
|
225
|
+
if (raw.includes('个人')) return true;
|
|
226
|
+
if (raw.includes("organization") && raw.includes('@')) return true;
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function normalizeOrgCandidates(orgs) {
|
|
231
|
+
if (!Array.isArray(orgs)) return [];
|
|
232
|
+
const rows = [];
|
|
233
|
+
for (const org of orgs) {
|
|
234
|
+
if (!org || typeof org !== 'object') continue;
|
|
235
|
+
const title = normalizeSpaceText(org.title) || 'unknown';
|
|
236
|
+
const id = normalizeSpaceText(org.id);
|
|
237
|
+
const role = normalizeSpaceText(org.role);
|
|
238
|
+
const isDefault = Boolean(org.is_default);
|
|
239
|
+
const display = [title, id ? `(${id})` : '', role ? `[${role}]` : '', isDefault ? '(default)' : ''].filter(Boolean).join(' ');
|
|
240
|
+
rows.push({ title, id, role, is_default: isDefault, display });
|
|
241
|
+
}
|
|
242
|
+
return rows;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function decodeJwtPayload(token) {
|
|
246
|
+
if (!token || typeof token !== 'string') return null;
|
|
247
|
+
const parts = token.split('.');
|
|
248
|
+
if (parts.length < 2) return null;
|
|
249
|
+
const body = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
|
250
|
+
const padded = body.padEnd(Math.ceil(body.length / 4) * 4, '=');
|
|
251
|
+
try {
|
|
252
|
+
return JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
|
|
253
|
+
} catch {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function ensureBaseLayout() {
|
|
259
|
+
ensureDir(ROOT, 0o700);
|
|
260
|
+
ensureDir(PROFILES_DIR, 0o700);
|
|
261
|
+
ensureDir(path.join(PROFILES_DIR, 'codex'), 0o700);
|
|
262
|
+
ensureDir(path.join(PROFILES_DIR, 'claude'), 0o700);
|
|
263
|
+
ensureDir(LOCKS_DIR, 0o700);
|
|
264
|
+
ensureDir(STATE_DIR, 0o700);
|
|
265
|
+
ensureDir(DOCS_DIR, 0o700);
|
|
266
|
+
|
|
267
|
+
if (!fs.existsSync(CURRENT_PATH)) {
|
|
268
|
+
writeJsonAtomic(CURRENT_PATH, {
|
|
269
|
+
active_tool: null,
|
|
270
|
+
active_name: null,
|
|
271
|
+
active_profile_id: null,
|
|
272
|
+
last_switched_at: null,
|
|
273
|
+
ui_sort_mode: 'recent',
|
|
274
|
+
ui_show_unready: false,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!fs.existsSync(REGISTRY_PATH)) {
|
|
279
|
+
writeJsonAtomic(REGISTRY_PATH, {
|
|
280
|
+
version: 2,
|
|
281
|
+
profiles: { codex: [], claude: [] },
|
|
282
|
+
updated_at: now(),
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
for (const [name, content] of Object.entries(DOC_TEMPLATES)) {
|
|
287
|
+
const file = path.join(DOCS_DIR, name);
|
|
288
|
+
if (!fs.existsSync(file)) {
|
|
289
|
+
fs.writeFileSync(file, content, { mode: 0o600 });
|
|
290
|
+
chmodSafe(file, 0o600);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function normalizeUiSortMode(mode) {
|
|
296
|
+
const raw = String(mode || '').trim();
|
|
297
|
+
if (UI_SORT_MODES.has(raw)) return raw;
|
|
298
|
+
return 'recent';
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function validateTool(tool, allowAll = false) {
|
|
302
|
+
if (allowAll && tool === 'all') return 'all';
|
|
303
|
+
if (!TOOLS.has(tool)) {
|
|
304
|
+
throw new CliError(msg(`无效的 --tool: ${tool}`, `Invalid --tool: ${tool}`));
|
|
305
|
+
}
|
|
306
|
+
return tool;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function validateName(name) {
|
|
310
|
+
if (!NAME_RE.test(name)) {
|
|
311
|
+
throw new CliError(msg(`profile 名称不合法: ${name}`, `Invalid profile name: ${name}`));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function parseToolOption(args, defaultTool, allowAll = false) {
|
|
316
|
+
let tool = defaultTool;
|
|
317
|
+
const rest = [];
|
|
318
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
319
|
+
const cur = args[i];
|
|
320
|
+
if (cur === '--tool') {
|
|
321
|
+
if (i + 1 >= args.length) {
|
|
322
|
+
throw new CliError(msg('缺少 --tool 的值', 'Missing value after --tool'));
|
|
323
|
+
}
|
|
324
|
+
tool = args[i + 1];
|
|
325
|
+
i += 1;
|
|
326
|
+
} else {
|
|
327
|
+
rest.push(cur);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return { tool: validateTool(tool, allowAll), rest };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function splitRawArgs(args) {
|
|
334
|
+
const idx = args.indexOf('--');
|
|
335
|
+
if (idx < 0) return { head: args.slice(), raw: [] };
|
|
336
|
+
return { head: args.slice(0, idx), raw: args.slice(idx + 1) };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function loadCurrent() {
|
|
340
|
+
ensureBaseLayout();
|
|
341
|
+
const cur = readJson(CURRENT_PATH, {
|
|
342
|
+
active_tool: null,
|
|
343
|
+
active_name: null,
|
|
344
|
+
active_profile_id: null,
|
|
345
|
+
last_switched_at: null,
|
|
346
|
+
ui_sort_mode: 'recent',
|
|
347
|
+
ui_show_unready: false,
|
|
348
|
+
});
|
|
349
|
+
if (!('active_profile_id' in cur)) {
|
|
350
|
+
cur.active_profile_id = null;
|
|
351
|
+
}
|
|
352
|
+
cur.ui_sort_mode = normalizeUiSortMode(cur.ui_sort_mode);
|
|
353
|
+
cur.ui_show_unready = Boolean(cur.ui_show_unready);
|
|
354
|
+
return cur;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function saveCurrent(cur) {
|
|
358
|
+
cur.ui_sort_mode = normalizeUiSortMode(cur.ui_sort_mode);
|
|
359
|
+
cur.ui_show_unready = Boolean(cur.ui_show_unready);
|
|
360
|
+
writeJsonAtomic(CURRENT_PATH, cur, 0o600);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function makeProfileId(seed = '') {
|
|
364
|
+
return `cp_${shortHash(`${seed}|${Date.now()}|${Math.random()}`, 10)}`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function profilePathById(tool, profileId) {
|
|
368
|
+
return path.join(PROFILES_DIR, tool, profileId);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function codexAuthInfo(profilePath) {
|
|
372
|
+
const authPath = path.join(profilePath, 'auth.json');
|
|
373
|
+
const auth = readJsonSafe(authPath);
|
|
374
|
+
if (!auth) {
|
|
375
|
+
return {
|
|
376
|
+
account_email: '',
|
|
377
|
+
account_key: `missing:${shortHash(profilePath, 8)}`,
|
|
378
|
+
space_name: 'unknown',
|
|
379
|
+
space_key: `unknown:${shortHash(profilePath, 8)}`,
|
|
380
|
+
space_display: 'unknown',
|
|
381
|
+
enterprise_space_name: '',
|
|
382
|
+
enterprise_space_id: '',
|
|
383
|
+
enterprise_space_display: '',
|
|
384
|
+
org_candidates: [],
|
|
385
|
+
plan: '',
|
|
386
|
+
status: 'missing_auth',
|
|
387
|
+
token_hashes: {},
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const tokens = auth.tokens || {};
|
|
392
|
+
const payload = decodeJwtPayload(tokens.id_token);
|
|
393
|
+
const ext = payload && payload['https://api.openai.com/auth'] ? payload['https://api.openai.com/auth'] : {};
|
|
394
|
+
const orgs = normalizeOrgCandidates(Array.isArray(ext.organizations) ? ext.organizations : []);
|
|
395
|
+
const org = orgs.find((o) => o && o.is_default) || orgs[0] || null;
|
|
396
|
+
const enterpriseOrg = orgs.find((o) => o.is_default && !isLikelyPersonalSpace(o.title))
|
|
397
|
+
|| orgs.find((o) => !isLikelyPersonalSpace(o.title))
|
|
398
|
+
|| null;
|
|
399
|
+
|
|
400
|
+
const email = payload && payload.email ? String(payload.email) : '';
|
|
401
|
+
const accountKey = tokens.account_id || ext.chatgpt_account_id || `email:${shortHash(email || profilePath, 8)}`;
|
|
402
|
+
const spaceName = (org && org.title) ? String(org.title) : 'unknown';
|
|
403
|
+
const spaceId = (org && org.id) ? String(org.id) : '';
|
|
404
|
+
const spaceKey = spaceId || `space:${shortHash(spaceName, 8)}`;
|
|
405
|
+
const plan = ext.chatgpt_plan_type ? String(ext.chatgpt_plan_type) : '';
|
|
406
|
+
const spaceDisplay = [spaceName, spaceId ? `(${spaceId})` : '', plan ? `[${plan}]` : ''].filter(Boolean).join(' ');
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
account_email: email,
|
|
410
|
+
account_key: String(accountKey),
|
|
411
|
+
space_name: spaceName,
|
|
412
|
+
space_key: String(spaceKey),
|
|
413
|
+
space_display: spaceDisplay || spaceName,
|
|
414
|
+
enterprise_space_name: enterpriseOrg ? enterpriseOrg.title : '',
|
|
415
|
+
enterprise_space_id: enterpriseOrg ? enterpriseOrg.id : '',
|
|
416
|
+
enterprise_space_display: enterpriseOrg ? enterpriseOrg.display : '',
|
|
417
|
+
org_candidates: orgs,
|
|
418
|
+
plan,
|
|
419
|
+
status: 'ready',
|
|
420
|
+
token_hashes: {
|
|
421
|
+
refresh: tokens.refresh_token ? shortHash(tokens.refresh_token, 16) : '',
|
|
422
|
+
access: tokens.access_token ? shortHash(tokens.access_token, 16) : '',
|
|
423
|
+
id: tokens.id_token ? shortHash(tokens.id_token, 16) : '',
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function claudeGlobalInfo() {
|
|
429
|
+
const cfg = readJsonSafe(path.join(HOME, '.claude.json')) || {};
|
|
430
|
+
const oa = cfg.oauthAccount || {};
|
|
431
|
+
const email = oa.emailAddress ? String(oa.emailAddress) : '';
|
|
432
|
+
const orgName = oa.organizationName ? String(oa.organizationName) : 'unknown';
|
|
433
|
+
const billing = oa.organizationBillingType ? String(oa.organizationBillingType) : '';
|
|
434
|
+
const role = oa.organizationRole ? String(oa.organizationRole) : '';
|
|
435
|
+
const spaceDisplay = [orgName, billing ? `[${billing}]` : '', role ? `(role:${role})` : ''].filter(Boolean).join(' ');
|
|
436
|
+
const accountKey = oa.accountUuid ? String(oa.accountUuid) : `email:${shortHash(email || 'unknown', 8)}`;
|
|
437
|
+
const spaceKey = oa.organizationUuid ? String(oa.organizationUuid) : `space:${shortHash(orgName || 'unknown', 8)}`;
|
|
438
|
+
return { email, orgName, spaceDisplay, accountKey, spaceKey };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function claudeAuthInfo(profilePath) {
|
|
442
|
+
const local = readJsonSafe(path.join(profilePath, 'account.json')) || {};
|
|
443
|
+
if (local && typeof local === 'object' && (local.email || local.space || local.account_key || local.space_key)) {
|
|
444
|
+
const email = local.email ? String(local.email) : '';
|
|
445
|
+
const spaceName = local.space ? String(local.space) : 'unknown';
|
|
446
|
+
const enterpriseName = local.enterprise_space_name ? String(local.enterprise_space_name) : '';
|
|
447
|
+
const enterpriseId = local.enterprise_space_id ? String(local.enterprise_space_id) : '';
|
|
448
|
+
const enterpriseDisplay = local.enterprise_space_display ? String(local.enterprise_space_display) : '';
|
|
449
|
+
return {
|
|
450
|
+
account_email: email,
|
|
451
|
+
account_key: local.account_key ? String(local.account_key) : `email:${shortHash(email || profilePath, 8)}`,
|
|
452
|
+
space_name: spaceName,
|
|
453
|
+
space_key: local.space_key ? String(local.space_key) : `space:${shortHash(spaceName, 8)}`,
|
|
454
|
+
space_display: spaceName,
|
|
455
|
+
enterprise_space_name: enterpriseName,
|
|
456
|
+
enterprise_space_id: enterpriseId,
|
|
457
|
+
enterprise_space_display: enterpriseDisplay,
|
|
458
|
+
org_candidates: [],
|
|
459
|
+
plan: '',
|
|
460
|
+
status: email ? 'ready' : 'needs_login',
|
|
461
|
+
token_hashes: {},
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const g = claudeGlobalInfo();
|
|
466
|
+
return {
|
|
467
|
+
account_email: g.email,
|
|
468
|
+
account_key: g.accountKey,
|
|
469
|
+
space_name: g.orgName || 'unknown',
|
|
470
|
+
space_key: g.spaceKey,
|
|
471
|
+
space_display: g.spaceDisplay || g.orgName || 'unknown',
|
|
472
|
+
enterprise_space_name: isLikelyPersonalSpace(g.orgName) ? '' : (g.orgName || ''),
|
|
473
|
+
enterprise_space_id: isLikelyPersonalSpace(g.orgName) ? '' : (g.spaceKey || ''),
|
|
474
|
+
enterprise_space_display: isLikelyPersonalSpace(g.orgName) ? '' : (g.spaceDisplay || g.orgName || ''),
|
|
475
|
+
org_candidates: [],
|
|
476
|
+
plan: '',
|
|
477
|
+
status: g.email ? 'ready' : 'needs_login',
|
|
478
|
+
token_hashes: {},
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function readAuthInfo(tool, profilePath) {
|
|
483
|
+
return tool === 'codex' ? codexAuthInfo(profilePath) : claudeAuthInfo(profilePath);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function buildDisplayName(accountEmail, spaceName) {
|
|
487
|
+
const mail = accountEmail || 'unknown';
|
|
488
|
+
const sp = spaceName || 'unknown';
|
|
489
|
+
return `${mail} | ${sp}`;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function buildSlug(tool, accountEmail, spaceName, accountKey, spaceKey) {
|
|
493
|
+
const base = `${emailShort(accountEmail)}_${spaceShort(spaceName)}`;
|
|
494
|
+
const h4 = shortHash(`${tool}|${accountKey}|${spaceKey}`, 4);
|
|
495
|
+
return `${base}_${h4}`;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function ensureUniqueSlug(tool, desiredSlug, profiles, excludeProfileId = null) {
|
|
499
|
+
let slug = desiredSlug;
|
|
500
|
+
let n = 2;
|
|
501
|
+
const exists = (s) => profiles.some((p) => p.tool === tool && p.slug === s && p.profile_id !== excludeProfileId);
|
|
502
|
+
while (exists(slug)) {
|
|
503
|
+
slug = `${desiredSlug}_d${n}`;
|
|
504
|
+
n += 1;
|
|
505
|
+
}
|
|
506
|
+
return slug;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function normalizeAliases(arr) {
|
|
510
|
+
const out = [];
|
|
511
|
+
for (const item of arr || []) {
|
|
512
|
+
const s = String(item || '').trim();
|
|
513
|
+
if (!s) continue;
|
|
514
|
+
if (!out.includes(s)) out.push(s);
|
|
515
|
+
}
|
|
516
|
+
return out;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function backupRegistry() {
|
|
520
|
+
if (!fs.existsSync(REGISTRY_PATH)) return null;
|
|
521
|
+
const backup = `${REGISTRY_PATH}.bak.${Date.now()}`;
|
|
522
|
+
fs.copyFileSync(REGISTRY_PATH, backup);
|
|
523
|
+
chmodSafe(backup, 0o600);
|
|
524
|
+
return backup;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function allProfiles(reg) {
|
|
528
|
+
const rows = [];
|
|
529
|
+
for (const tool of ['codex', 'claude']) {
|
|
530
|
+
for (const p of reg.profiles[tool]) {
|
|
531
|
+
rows.push({ ...p, tool });
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return rows;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function migrateRegistryToV2(oldReg, oldCurrent) {
|
|
538
|
+
const migrated = {
|
|
539
|
+
version: 2,
|
|
540
|
+
profiles: { codex: [], claude: [] },
|
|
541
|
+
updated_at: now(),
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const usedSlugs = [];
|
|
545
|
+
let converted = 0;
|
|
546
|
+
let missingMetadata = 0;
|
|
547
|
+
|
|
548
|
+
for (const tool of ['codex', 'claude']) {
|
|
549
|
+
const list = (oldReg.profiles && Array.isArray(oldReg.profiles[tool])) ? oldReg.profiles[tool] : [];
|
|
550
|
+
for (let i = 0; i < list.length; i += 1) {
|
|
551
|
+
const old = list[i] || {};
|
|
552
|
+
const oldName = old.name ? String(old.name) : `legacy_${tool}_${i + 1}`;
|
|
553
|
+
const profileId = old.profile_id ? String(old.profile_id) : makeProfileId(`${tool}|${old.path || oldName}|${i}`);
|
|
554
|
+
const pPath = old.path ? String(old.path) : profilePathById(tool, profileId);
|
|
555
|
+
ensureDir(pPath, 0o700);
|
|
556
|
+
|
|
557
|
+
const auth = readAuthInfo(tool, pPath);
|
|
558
|
+
if (!auth.account_email || auth.space_name === 'unknown') {
|
|
559
|
+
missingMetadata += 1;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const baseSlug = buildSlug(tool, auth.account_email, auth.space_name, auth.account_key, auth.space_key);
|
|
563
|
+
const slug = ensureUniqueSlug(tool, baseSlug, usedSlugs.concat(allProfiles(migrated)));
|
|
564
|
+
|
|
565
|
+
const aliases = normalizeAliases([oldName, ...(old.aliases || [])]);
|
|
566
|
+
|
|
567
|
+
const profile = {
|
|
568
|
+
profile_id: profileId,
|
|
569
|
+
slug,
|
|
570
|
+
display_name: buildDisplayName(auth.account_email, auth.space_name),
|
|
571
|
+
aliases,
|
|
572
|
+
account_key: auth.account_key,
|
|
573
|
+
space_key: auth.space_key,
|
|
574
|
+
account_email: auth.account_email,
|
|
575
|
+
space_name: auth.space_name,
|
|
576
|
+
space_display: auth.space_display,
|
|
577
|
+
enterprise_space_name: auth.enterprise_space_name || '',
|
|
578
|
+
enterprise_space_id: auth.enterprise_space_id || '',
|
|
579
|
+
enterprise_space_display: auth.enterprise_space_display || '',
|
|
580
|
+
org_candidates: Array.isArray(auth.org_candidates) ? auth.org_candidates : [],
|
|
581
|
+
plan: auth.plan || '',
|
|
582
|
+
status: auth.status,
|
|
583
|
+
tool,
|
|
584
|
+
path: pPath,
|
|
585
|
+
created_at: old.created_at || now(),
|
|
586
|
+
last_login_at: old.last_login_at || null,
|
|
587
|
+
last_used_at: old.last_used_at || null,
|
|
588
|
+
pinned: Boolean(old.pinned),
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
migrated.profiles[tool].push(profile);
|
|
592
|
+
usedSlugs.push(profile);
|
|
593
|
+
converted += 1;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const cur = {
|
|
598
|
+
active_tool: oldCurrent && oldCurrent.active_tool ? oldCurrent.active_tool : null,
|
|
599
|
+
active_name: oldCurrent && oldCurrent.active_name ? oldCurrent.active_name : null,
|
|
600
|
+
active_profile_id: null,
|
|
601
|
+
last_switched_at: oldCurrent && oldCurrent.last_switched_at ? oldCurrent.last_switched_at : null,
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
if (cur.active_tool && cur.active_name && TOOLS.has(cur.active_tool)) {
|
|
605
|
+
const candidates = migrated.profiles[cur.active_tool].filter((p) => p.slug === cur.active_name || (p.aliases || []).includes(cur.active_name));
|
|
606
|
+
if (candidates.length === 1) {
|
|
607
|
+
cur.active_profile_id = candidates[0].profile_id;
|
|
608
|
+
cur.active_name = candidates[0].slug;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return { registry: migrated, current: cur, converted, missingMetadata };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function normalizeV2Registry(reg) {
|
|
616
|
+
let changed = false;
|
|
617
|
+
if (!reg || typeof reg !== 'object') {
|
|
618
|
+
reg = { version: 2, profiles: { codex: [], claude: [] }, updated_at: now() };
|
|
619
|
+
changed = true;
|
|
620
|
+
}
|
|
621
|
+
if (!reg.profiles || typeof reg.profiles !== 'object') {
|
|
622
|
+
reg.profiles = { codex: [], claude: [] };
|
|
623
|
+
changed = true;
|
|
624
|
+
}
|
|
625
|
+
for (const tool of ['codex', 'claude']) {
|
|
626
|
+
if (!Array.isArray(reg.profiles[tool])) {
|
|
627
|
+
reg.profiles[tool] = [];
|
|
628
|
+
changed = true;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const rows = [];
|
|
633
|
+
for (const tool of ['codex', 'claude']) {
|
|
634
|
+
for (const p of reg.profiles[tool]) {
|
|
635
|
+
rows.push({ ...p, tool });
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
for (const tool of ['codex', 'claude']) {
|
|
640
|
+
for (let i = 0; i < reg.profiles[tool].length; i += 1) {
|
|
641
|
+
const p = reg.profiles[tool][i];
|
|
642
|
+
const before = JSON.stringify(p);
|
|
643
|
+
const profileId = p.profile_id || makeProfileId(`${tool}|${p.path || p.slug || i}`);
|
|
644
|
+
p.profile_id = profileId;
|
|
645
|
+
p.tool = tool;
|
|
646
|
+
p.path = p.path || profilePathById(tool, profileId);
|
|
647
|
+
ensureDir(p.path, 0o700);
|
|
648
|
+
|
|
649
|
+
const auth = readAuthInfo(tool, p.path);
|
|
650
|
+
p.account_key = p.account_key || auth.account_key;
|
|
651
|
+
p.space_key = p.space_key || auth.space_key;
|
|
652
|
+
p.account_email = p.account_email || auth.account_email;
|
|
653
|
+
p.space_name = p.space_name || auth.space_name;
|
|
654
|
+
p.space_display = auth.space_display || p.space_display || p.space_name || 'unknown';
|
|
655
|
+
if (!('enterprise_space_name' in p)) p.enterprise_space_name = '';
|
|
656
|
+
if (!('enterprise_space_id' in p)) p.enterprise_space_id = '';
|
|
657
|
+
if (!('enterprise_space_display' in p)) p.enterprise_space_display = '';
|
|
658
|
+
if (!Array.isArray(p.org_candidates)) p.org_candidates = [];
|
|
659
|
+
if (!p.enterprise_space_name && auth.enterprise_space_name) p.enterprise_space_name = auth.enterprise_space_name;
|
|
660
|
+
if (!p.enterprise_space_id && auth.enterprise_space_id) p.enterprise_space_id = auth.enterprise_space_id;
|
|
661
|
+
if (!p.enterprise_space_display && auth.enterprise_space_display) p.enterprise_space_display = auth.enterprise_space_display;
|
|
662
|
+
if (p.org_candidates.length === 0 && Array.isArray(auth.org_candidates) && auth.org_candidates.length > 0) {
|
|
663
|
+
p.org_candidates = auth.org_candidates;
|
|
664
|
+
}
|
|
665
|
+
p.plan = p.plan || auth.plan || '';
|
|
666
|
+
p.status = auth.status || p.status || 'unknown';
|
|
667
|
+
|
|
668
|
+
if (!p.slug) {
|
|
669
|
+
const baseSlug = buildSlug(tool, p.account_email, p.space_name, p.account_key, p.space_key);
|
|
670
|
+
p.slug = ensureUniqueSlug(tool, baseSlug, rows, p.profile_id);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
p.display_name = p.display_name || buildDisplayName(p.account_email, p.space_name);
|
|
674
|
+
p.aliases = normalizeAliases([...(p.aliases || []), ...(p.name ? [String(p.name)] : [])]);
|
|
675
|
+
p.created_at = p.created_at || now();
|
|
676
|
+
if (!('last_login_at' in p)) p.last_login_at = null;
|
|
677
|
+
if (!('last_used_at' in p)) p.last_used_at = null;
|
|
678
|
+
if (!('pinned' in p)) p.pinned = false;
|
|
679
|
+
p.pinned = Boolean(p.pinned);
|
|
680
|
+
|
|
681
|
+
if (before !== JSON.stringify(p)) changed = true;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
reg.version = 2;
|
|
686
|
+
if (changed) reg.updated_at = now();
|
|
687
|
+
return { reg, changed };
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function loadRegistry(options = {}) {
|
|
691
|
+
const { forceMigrate = false, printSummary = false } = options;
|
|
692
|
+
ensureBaseLayout();
|
|
693
|
+
|
|
694
|
+
const oldCurrent = loadCurrent();
|
|
695
|
+
const regRaw = readJson(REGISTRY_PATH, { version: 2, profiles: { codex: [], claude: [] }, updated_at: now() });
|
|
696
|
+
|
|
697
|
+
if (!regRaw.version || regRaw.version < 2) {
|
|
698
|
+
const backup = backupRegistry();
|
|
699
|
+
const { registry, current, converted, missingMetadata } = migrateRegistryToV2(regRaw, oldCurrent);
|
|
700
|
+
writeJsonAtomic(REGISTRY_PATH, registry, 0o600);
|
|
701
|
+
saveCurrent(current);
|
|
702
|
+
if (printSummary) {
|
|
703
|
+
process.stdout.write(`${msg('迁移完成', 'Migration completed')}\n`);
|
|
704
|
+
process.stdout.write(` ${msg('备份文件', 'Backup')}: ${backup || '-'}\n`);
|
|
705
|
+
process.stdout.write(` ${msg('转换条目', 'Converted')}: ${converted}\n`);
|
|
706
|
+
process.stdout.write(` ${msg('元数据缺失', 'Missing metadata')}: ${missingMetadata}\n`);
|
|
707
|
+
}
|
|
708
|
+
return registry;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const { reg, changed } = normalizeV2Registry(regRaw);
|
|
712
|
+
if (changed) {
|
|
713
|
+
writeJsonAtomic(REGISTRY_PATH, reg, 0o600);
|
|
714
|
+
}
|
|
715
|
+
if (forceMigrate && printSummary) {
|
|
716
|
+
process.stdout.write(`${msg('当前已是 V2,已完成规范化检查', 'Already V2, normalization check completed')}\n`);
|
|
717
|
+
process.stdout.write(` ${msg('变更写入', 'Changes written')}: ${changed ? 'yes' : 'no'}\n`);
|
|
718
|
+
}
|
|
719
|
+
return reg;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function saveRegistry(reg) {
|
|
723
|
+
reg.updated_at = now();
|
|
724
|
+
writeJsonAtomic(REGISTRY_PATH, reg, 0o600);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function lockPath(tool, slugOrId) {
|
|
728
|
+
return path.join(LOCKS_DIR, `${tool}-${slugOrId}.lock`);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function withLocks(lockFiles, fn) {
|
|
732
|
+
ensureDir(LOCKS_DIR, 0o700);
|
|
733
|
+
const files = [...new Set(lockFiles)];
|
|
734
|
+
const acquired = [];
|
|
735
|
+
try {
|
|
736
|
+
for (const file of files) {
|
|
737
|
+
try {
|
|
738
|
+
const fd = fs.openSync(file, 'wx', 0o600);
|
|
739
|
+
fs.writeFileSync(fd, `${process.pid}\n`);
|
|
740
|
+
fs.closeSync(fd);
|
|
741
|
+
acquired.push(file);
|
|
742
|
+
} catch (err) {
|
|
743
|
+
if (err && err.code === 'EEXIST') {
|
|
744
|
+
throw new CliError(msg(`profile 正在被其他操作占用: ${path.basename(file, '.lock')}`, `Profile is locked: ${path.basename(file, '.lock')}`));
|
|
745
|
+
}
|
|
746
|
+
throw err;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return fn();
|
|
750
|
+
} finally {
|
|
751
|
+
for (const file of acquired.reverse()) {
|
|
752
|
+
try { fs.unlinkSync(file); } catch {}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function statusLabel(status) {
|
|
758
|
+
switch (status) {
|
|
759
|
+
case 'ready': return '可用';
|
|
760
|
+
case 'missing_auth': return '缺少凭证';
|
|
761
|
+
case 'needs_login': return '需要登录';
|
|
762
|
+
default: return '未知';
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function statusIcon(status) {
|
|
767
|
+
return status === 'ready' ? '✓' : '!';
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function statusTone(status) {
|
|
771
|
+
return status === 'ready' ? 'success' : 'warning';
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function statusRank(status) {
|
|
775
|
+
if (status === 'ready') return 0;
|
|
776
|
+
if (status === 'needs_login') return 1;
|
|
777
|
+
if (status === 'missing_auth') return 2;
|
|
778
|
+
return 3;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function toolTone(tool) {
|
|
782
|
+
return tool === 'codex' ? 'codex' : 'claude';
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function toolBadge(tool) {
|
|
786
|
+
const icon = tool === 'codex' ? '◉' : '◎';
|
|
787
|
+
return `${colorize(icon, toolTone(tool))} ${tool}`;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function codexWorkspaceName(profile) {
|
|
791
|
+
return oneLineText(profile.enterprise_space_display || profile.enterprise_space_name || '', 24) || '未设置';
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function togglePinnedById(profileId) {
|
|
795
|
+
const reg = loadRegistry();
|
|
796
|
+
for (const tool of ['codex', 'claude']) {
|
|
797
|
+
const hit = reg.profiles[tool].find((p) => p.profile_id === profileId);
|
|
798
|
+
if (!hit) continue;
|
|
799
|
+
hit.pinned = !Boolean(hit.pinned);
|
|
800
|
+
saveRegistry(reg);
|
|
801
|
+
writeJsonAtomic(path.join(hit.path, 'meta.json'), hit, 0o600);
|
|
802
|
+
return Boolean(hit.pinned);
|
|
803
|
+
}
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function usageRiskLevel(profile, usageMap) {
|
|
808
|
+
if (profile.tool !== 'codex') return 'none';
|
|
809
|
+
const entry = usageMap.get(usageCacheKey(profile));
|
|
810
|
+
if (!entry || !entry.ok || !entry.snapshot) return 'none';
|
|
811
|
+
const week = clampPercent(entry.snapshot.weekPercent);
|
|
812
|
+
if (!Number.isFinite(week)) return 'none';
|
|
813
|
+
if (week >= 95) return 'critical';
|
|
814
|
+
if (week >= 85) return 'high';
|
|
815
|
+
if (week >= 70) return 'medium';
|
|
816
|
+
return 'none';
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function riskBadge(level) {
|
|
820
|
+
if (level === 'critical') return colorize('⛔', 'danger');
|
|
821
|
+
if (level === 'high') return colorize('⚠', 'warning');
|
|
822
|
+
if (level === 'medium') return colorize('▲', 'warning');
|
|
823
|
+
return '';
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function oneLineText(value, maxLen = 120) {
|
|
827
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
828
|
+
if (!text) return '';
|
|
829
|
+
return text.length > maxLen ? `${text.slice(0, maxLen - 1)}…` : text;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function formatPercent(value) {
|
|
833
|
+
const n = Number(value);
|
|
834
|
+
if (!Number.isFinite(n)) return '-';
|
|
835
|
+
const rounded = Math.round(n * 10) / 10;
|
|
836
|
+
return Number.isInteger(rounded) ? `${rounded}%` : `${rounded.toFixed(1)}%`;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function clampPercent(value) {
|
|
840
|
+
const n = Number(value);
|
|
841
|
+
if (!Number.isFinite(n)) return null;
|
|
842
|
+
if (n < 0) return 0;
|
|
843
|
+
if (n > 100) return 100;
|
|
844
|
+
return n;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function formatUsedRemain(value) {
|
|
848
|
+
const used = clampPercent(value);
|
|
849
|
+
if (!Number.isFinite(used)) {
|
|
850
|
+
return { used: '-', remain: '-' };
|
|
851
|
+
}
|
|
852
|
+
const remain = 100 - used;
|
|
853
|
+
return {
|
|
854
|
+
used: formatPercent(used),
|
|
855
|
+
remain: formatPercent(remain),
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function usageToneByPercent(value) {
|
|
860
|
+
const used = clampPercent(value);
|
|
861
|
+
if (!Number.isFinite(used)) return 'muted';
|
|
862
|
+
if (used >= 95) return 'danger';
|
|
863
|
+
if (used >= 85) return 'warning';
|
|
864
|
+
if (used >= 70) return 'title';
|
|
865
|
+
return 'success';
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function usageBar(value, width = 8) {
|
|
869
|
+
const used = clampPercent(value);
|
|
870
|
+
if (!Number.isFinite(used)) return colorize('·'.repeat(width), 'muted');
|
|
871
|
+
let filled = Math.max(0, Math.min(width, Math.round((used / 100) * width)));
|
|
872
|
+
if (used > 0 && filled === 0) filled = 1;
|
|
873
|
+
if (used < 100 && filled === width) filled = width - 1;
|
|
874
|
+
const bar = `${'█'.repeat(filled)}${'░'.repeat(width - filled)}`;
|
|
875
|
+
return colorize(bar, usageToneByPercent(used));
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function usageBarCell(value, ratioText, width = 8) {
|
|
879
|
+
return `${usageBar(value, width)} ${ratioText}`;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function stripAnsi(text) {
|
|
883
|
+
return String(text || '')
|
|
884
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '')
|
|
885
|
+
.replace(/\x1b[@-_]/g, '')
|
|
886
|
+
.replace(/\u0008/g, '')
|
|
887
|
+
.replace(/\r/g, '\n');
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function parseUsedPercentFromChunk(chunk) {
|
|
891
|
+
const text = stripAnsi(chunk).replace(/\s+/g, ' ').trim();
|
|
892
|
+
if (!text) return null;
|
|
893
|
+
|
|
894
|
+
let m = text.match(/(?:已用|used)\s*[::]?\s*(\d{1,3}(?:\.\d+)?)\s*%/i);
|
|
895
|
+
if (m) return clampPercent(Number(m[1]));
|
|
896
|
+
|
|
897
|
+
m = text.match(/(?:剩余|余|remaining|remain|left)\s*[::]?\s*(\d{1,3}(?:\.\d+)?)\s*%/i);
|
|
898
|
+
if (m) {
|
|
899
|
+
const remain = clampPercent(Number(m[1]));
|
|
900
|
+
if (Number.isFinite(remain)) return clampPercent(100 - remain);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
m = text.match(/(\d{1,3}(?:\.\d+)?)\s*%\s*[/|]\s*(\d{1,3}(?:\.\d+)?)\s*%/);
|
|
904
|
+
if (m) return clampPercent(Number(m[1]));
|
|
905
|
+
|
|
906
|
+
m = text.match(/(\d{1,3}(?:\.\d+)?)\s*%/);
|
|
907
|
+
if (m) return clampPercent(Number(m[1]));
|
|
908
|
+
return null;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function extractPercentNearKeywords(text, keywords) {
|
|
912
|
+
const raw = stripAnsi(text);
|
|
913
|
+
if (!raw) return null;
|
|
914
|
+
const lower = raw.toLowerCase();
|
|
915
|
+
|
|
916
|
+
for (const key of keywords) {
|
|
917
|
+
const idx = lower.indexOf(String(key || '').toLowerCase());
|
|
918
|
+
if (idx < 0) continue;
|
|
919
|
+
const chunk = raw.slice(Math.max(0, idx - 12), idx + 180);
|
|
920
|
+
const used = parseUsedPercentFromChunk(chunk);
|
|
921
|
+
if (Number.isFinite(used)) return used;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const lines = raw.split('\n');
|
|
925
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
926
|
+
const line = lines[i];
|
|
927
|
+
const ll = line.toLowerCase();
|
|
928
|
+
if (!keywords.some((k) => ll.includes(String(k).toLowerCase()))) continue;
|
|
929
|
+
const chunk = `${line} ${lines[i + 1] || ''} ${lines[i + 2] || ''}`;
|
|
930
|
+
const used = parseUsedPercentFromChunk(chunk);
|
|
931
|
+
if (Number.isFinite(used)) return used;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function parseClaudeStatusSnapshot(rawText) {
|
|
938
|
+
const text = stripAnsi(rawText);
|
|
939
|
+
if (!text || !text.trim()) return null;
|
|
940
|
+
const five = extractPercentNearKeywords(text, ['5h', '5 hour', '5-hour', '5hr', '5小时', '五小时']);
|
|
941
|
+
const week = extractPercentNearKeywords(text, ['week', 'weekly', '7d', '7-day', '7 day', '周', '每周']);
|
|
942
|
+
if (!Number.isFinite(five) && !Number.isFinite(week)) return null;
|
|
943
|
+
|
|
944
|
+
const fiveUsed = Number.isFinite(five) ? five : null;
|
|
945
|
+
const weekUsed = Number.isFinite(week) ? week : null;
|
|
946
|
+
return {
|
|
947
|
+
limitId: 'claude_experimental',
|
|
948
|
+
planType: 'claude',
|
|
949
|
+
fiveHourPercent: fiveUsed,
|
|
950
|
+
fiveHourResetAt: null,
|
|
951
|
+
weekPercent: weekUsed,
|
|
952
|
+
weekResetAt: null,
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function runClaudeStatusPrint(profilePath, outputFormat) {
|
|
957
|
+
const res = spawnSync('claude', ['-p', '/status', '--output-format', outputFormat], {
|
|
958
|
+
env: { ...process.env, CLAUDE_CONFIG_DIR: profilePath },
|
|
959
|
+
encoding: 'utf8',
|
|
960
|
+
timeout: 12000,
|
|
961
|
+
maxBuffer: 1024 * 1024,
|
|
962
|
+
});
|
|
963
|
+
const stdout = String(res.stdout || '');
|
|
964
|
+
const stderr = String(res.stderr || '');
|
|
965
|
+
const err = res.error ? oneLineText(res.error.message || String(res.error), 80) : '';
|
|
966
|
+
return {
|
|
967
|
+
stdout,
|
|
968
|
+
stderr,
|
|
969
|
+
status: typeof res.status === 'number' ? res.status : null,
|
|
970
|
+
signal: res.signal || '',
|
|
971
|
+
error: err,
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function runClaudeStatusPty(profilePath) {
|
|
976
|
+
const shellCommand = '({ sleep 3; printf \'/status \\r\'; sleep 4; printf \'\\003\'; sleep 1; } | script -q /dev/null claude)';
|
|
977
|
+
const res = spawnSync('/bin/zsh', ['-lc', shellCommand], {
|
|
978
|
+
env: { ...process.env, CLAUDE_CONFIG_DIR: profilePath },
|
|
979
|
+
encoding: 'utf8',
|
|
980
|
+
timeout: 18000,
|
|
981
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
982
|
+
});
|
|
983
|
+
const stdout = String(res.stdout || '');
|
|
984
|
+
const stderr = String(res.stderr || '');
|
|
985
|
+
const err = res.error ? oneLineText(res.error.message || String(res.error), 80) : '';
|
|
986
|
+
return {
|
|
987
|
+
stdout,
|
|
988
|
+
stderr,
|
|
989
|
+
status: typeof res.status === 'number' ? res.status : null,
|
|
990
|
+
signal: res.signal || '',
|
|
991
|
+
error: err,
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function normalizeClaudeProbeError(text) {
|
|
996
|
+
const raw = oneLineText(text || '', 100);
|
|
997
|
+
const lower = raw.toLowerCase();
|
|
998
|
+
if (!raw) return msg('Claude /status 未返回可解析用量', 'Claude /status did not return parseable usage');
|
|
999
|
+
if (lower.includes('etimedout') || lower.includes('timed out') || lower.includes('timeout')) {
|
|
1000
|
+
return msg('Claude 实验探测超时', 'Claude experimental probe timed out');
|
|
1001
|
+
}
|
|
1002
|
+
if (lower.includes('enoent') || lower.includes('command not found')) {
|
|
1003
|
+
return msg('Claude 命令不可用', 'claude command unavailable');
|
|
1004
|
+
}
|
|
1005
|
+
return raw;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function readClaudeRateLimitsLiveExperimental(profilePath) {
|
|
1009
|
+
if (!isClaudeUsageExperimentalEnabled()) {
|
|
1010
|
+
return {
|
|
1011
|
+
ok: false,
|
|
1012
|
+
error: msg('Claude 用量实验功能已关闭', 'Claude usage experimental mode disabled'),
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const textRun = runClaudeStatusPrint(profilePath, 'text');
|
|
1017
|
+
const textSnapshot = parseClaudeStatusSnapshot(textRun.stdout);
|
|
1018
|
+
if (textSnapshot) return { ok: true, snapshot: textSnapshot };
|
|
1019
|
+
|
|
1020
|
+
const jsonRun = runClaudeStatusPrint(profilePath, 'json');
|
|
1021
|
+
let jsonSnapshot = parseClaudeStatusSnapshot(jsonRun.stdout);
|
|
1022
|
+
if (!jsonSnapshot) {
|
|
1023
|
+
try {
|
|
1024
|
+
const obj = JSON.parse(jsonRun.stdout || '{}');
|
|
1025
|
+
const candidate = [obj.result, obj.message, obj.response, obj.output].filter(Boolean).join('\n');
|
|
1026
|
+
if (candidate) jsonSnapshot = parseClaudeStatusSnapshot(candidate);
|
|
1027
|
+
} catch {
|
|
1028
|
+
// ignore json parse failure
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
if (jsonSnapshot) return { ok: true, snapshot: jsonSnapshot };
|
|
1032
|
+
|
|
1033
|
+
if (isClaudeUsagePtyProbeEnabled()) {
|
|
1034
|
+
const ptyRun = runClaudeStatusPty(profilePath);
|
|
1035
|
+
const ptySnapshot = parseClaudeStatusSnapshot(ptyRun.stdout);
|
|
1036
|
+
if (ptySnapshot) return { ok: true, snapshot: ptySnapshot };
|
|
1037
|
+
const fallback = normalizeClaudeProbeError(ptyRun.stderr || ptyRun.error || ptyRun.stdout);
|
|
1038
|
+
return {
|
|
1039
|
+
ok: false,
|
|
1040
|
+
error: fallback,
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
const fallback = normalizeClaudeProbeError(textRun.error || textRun.stderr || jsonRun.error || jsonRun.stderr);
|
|
1045
|
+
return {
|
|
1046
|
+
ok: false,
|
|
1047
|
+
error: fallback,
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function formatEpochSeconds(value) {
|
|
1052
|
+
const n = Number(value);
|
|
1053
|
+
if (!Number.isFinite(n) || n <= 0) return '-';
|
|
1054
|
+
return new Date(n * 1000).toISOString();
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function usageCacheTtlMs() {
|
|
1058
|
+
const raw = Number(process.env.CPROFILE_USAGE_CACHE_SECONDS || DEFAULT_USAGE_CACHE_SECONDS);
|
|
1059
|
+
const sec = Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : DEFAULT_USAGE_CACHE_SECONDS;
|
|
1060
|
+
return sec * 1000;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function usageCacheKey(profile) {
|
|
1064
|
+
return `${profile.tool}|${profile.account_key || profile.profile_id}`;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function normalizeUsageCacheEntry(entry) {
|
|
1068
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
1069
|
+
const checkedAt = entry.checked_at ? String(entry.checked_at) : '';
|
|
1070
|
+
if (!checkedAt) return null;
|
|
1071
|
+
const ok = Boolean(entry.ok);
|
|
1072
|
+
if (ok && entry.snapshot && typeof entry.snapshot === 'object') {
|
|
1073
|
+
return {
|
|
1074
|
+
ok: true,
|
|
1075
|
+
checked_at: checkedAt,
|
|
1076
|
+
snapshot: entry.snapshot,
|
|
1077
|
+
error: '',
|
|
1078
|
+
source: entry.source ? String(entry.source) : 'cache',
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
return {
|
|
1082
|
+
ok: false,
|
|
1083
|
+
checked_at: checkedAt,
|
|
1084
|
+
snapshot: null,
|
|
1085
|
+
error: entry.error ? oneLineText(entry.error) : msg('读取失败', 'Read failed'),
|
|
1086
|
+
source: entry.source ? String(entry.source) : 'cache',
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function loadUsageCache() {
|
|
1091
|
+
const raw = readJsonSafe(USAGE_CACHE_PATH);
|
|
1092
|
+
if (!raw || typeof raw !== 'object' || !raw.entries || typeof raw.entries !== 'object') {
|
|
1093
|
+
return { version: 1, entries: {} };
|
|
1094
|
+
}
|
|
1095
|
+
return { version: 1, entries: raw.entries };
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function saveUsageCache(cache) {
|
|
1099
|
+
const entries = cache && cache.entries && typeof cache.entries === 'object' ? cache.entries : {};
|
|
1100
|
+
writeJsonAtomic(USAGE_CACHE_PATH, {
|
|
1101
|
+
version: 1,
|
|
1102
|
+
updated_at: now(),
|
|
1103
|
+
entries,
|
|
1104
|
+
}, 0o600);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function isUsageEntryFresh(entry, ttlMs) {
|
|
1108
|
+
if (!entry || !entry.checked_at) return false;
|
|
1109
|
+
const ts = Date.parse(entry.checked_at);
|
|
1110
|
+
if (!Number.isFinite(ts)) return false;
|
|
1111
|
+
return (Date.now() - ts) <= ttlMs;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function formatUsageAge(checkedAt) {
|
|
1115
|
+
const ts = Date.parse(String(checkedAt || ''));
|
|
1116
|
+
if (!Number.isFinite(ts)) return '-';
|
|
1117
|
+
const sec = Math.max(0, Math.floor((Date.now() - ts) / 1000));
|
|
1118
|
+
if (sec < 60) return `${sec}s`;
|
|
1119
|
+
const min = Math.floor(sec / 60);
|
|
1120
|
+
if (min < 60) return `${min}m`;
|
|
1121
|
+
const hour = Math.floor(min / 60);
|
|
1122
|
+
if (hour < 48) return `${hour}h`;
|
|
1123
|
+
const day = Math.floor(hour / 24);
|
|
1124
|
+
return `${day}d`;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function formatUsageInline(profile, usageMap) {
|
|
1128
|
+
if (profile.tool !== 'codex') return '用量:--';
|
|
1129
|
+
const entry = usageMap.get(usageCacheKey(profile));
|
|
1130
|
+
if (!entry) return '5h --/-- · 周 --/--';
|
|
1131
|
+
if (!entry.ok || !entry.snapshot) {
|
|
1132
|
+
if (entry.source === 'loading') return msg('用量更新中', 'refreshing');
|
|
1133
|
+
return '5h --/-- · 周 --/--';
|
|
1134
|
+
}
|
|
1135
|
+
const s = entry.snapshot;
|
|
1136
|
+
const five = formatUsedRemain(s.fiveHourPercent);
|
|
1137
|
+
const week = formatUsedRemain(s.weekPercent);
|
|
1138
|
+
return `5h ${five.used}/${five.remain} · 周 ${week.used}/${week.remain}`;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
function formatUsageDetail(profile, usageMap) {
|
|
1142
|
+
if (profile.tool !== 'codex') {
|
|
1143
|
+
return msg('用量: Claude 暂不提供该指标', 'Usage: Claude does not expose this metric');
|
|
1144
|
+
}
|
|
1145
|
+
const entry = usageMap.get(usageCacheKey(profile));
|
|
1146
|
+
if (!entry) return msg('用量: 暂无数据', 'Usage: no data');
|
|
1147
|
+
if (!entry.ok || !entry.snapshot) {
|
|
1148
|
+
if (entry.source === 'loading') return msg('用量: 正在刷新中...', 'Usage: refreshing...');
|
|
1149
|
+
return `用量: ${entry.error || msg('读取失败', 'read failed')}`;
|
|
1150
|
+
}
|
|
1151
|
+
const s = entry.snapshot;
|
|
1152
|
+
const five = formatUsedRemain(s.fiveHourPercent);
|
|
1153
|
+
const week = formatUsedRemain(s.weekPercent);
|
|
1154
|
+
const source = entry.source === 'live'
|
|
1155
|
+
? '实时'
|
|
1156
|
+
: `缓存${formatUsageAge(entry.checked_at)}`;
|
|
1157
|
+
return `用量: 5h 已用${five.used}/余${five.remain} · 周 已用${week.used}/余${week.remain} · ${source}`;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function usageSummary(profile, usageMap) {
|
|
1161
|
+
if (profile.tool !== 'codex' && profile.tool !== 'claude') {
|
|
1162
|
+
return {
|
|
1163
|
+
five: '--/--',
|
|
1164
|
+
week: '--/--',
|
|
1165
|
+
fiveUsed: null,
|
|
1166
|
+
weekUsed: null,
|
|
1167
|
+
note: '暂无数据',
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
if (profile.tool === 'claude' && !isClaudeUsageExperimentalEnabled()) {
|
|
1172
|
+
return {
|
|
1173
|
+
five: '--/--',
|
|
1174
|
+
week: '--/--',
|
|
1175
|
+
fiveUsed: null,
|
|
1176
|
+
weekUsed: null,
|
|
1177
|
+
note: 'Claude 无此指标',
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
const entry = usageMap.get(usageCacheKey(profile));
|
|
1182
|
+
if (!entry || !entry.ok || !entry.snapshot) {
|
|
1183
|
+
const fallbackNote = profile.tool === 'claude' ? 'Claude 无此指标' : '暂无数据';
|
|
1184
|
+
return {
|
|
1185
|
+
five: '--/--',
|
|
1186
|
+
week: '--/--',
|
|
1187
|
+
fiveUsed: null,
|
|
1188
|
+
weekUsed: null,
|
|
1189
|
+
note: entry && entry.error ? oneLineText(entry.error, 24) : fallbackNote,
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
const fiveUsed = clampPercent(entry.snapshot.fiveHourPercent);
|
|
1193
|
+
const weekUsed = clampPercent(entry.snapshot.weekPercent);
|
|
1194
|
+
const five = formatUsedRemain(fiveUsed);
|
|
1195
|
+
const week = formatUsedRemain(weekUsed);
|
|
1196
|
+
const liveLabel = profile.tool === 'claude' ? '实时(实验)' : '实时';
|
|
1197
|
+
return {
|
|
1198
|
+
five: `${five.used}/${five.remain}`,
|
|
1199
|
+
week: `${week.used}/${week.remain}`,
|
|
1200
|
+
fiveUsed,
|
|
1201
|
+
weekUsed,
|
|
1202
|
+
note: entry.source === 'live'
|
|
1203
|
+
? liveLabel
|
|
1204
|
+
: entry.source === 'stale_error'
|
|
1205
|
+
? `缓存${formatUsageAge(entry.checked_at)}(刷新失败)`
|
|
1206
|
+
: `缓存${formatUsageAge(entry.checked_at)}`,
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function buildProfileRows(profiles, usageMap, options = {}) {
|
|
1211
|
+
const includeUnready = Boolean(options.includeUnready);
|
|
1212
|
+
const readyRows = profiles.filter((p) => p.status === 'ready');
|
|
1213
|
+
const source = includeUnready ? profiles : (readyRows.length > 0 ? readyRows : profiles);
|
|
1214
|
+
return source.map((profile) => {
|
|
1215
|
+
const usage = usageSummary(profile, usageMap);
|
|
1216
|
+
const workspace = profile.tool === 'codex' ? codexWorkspaceName(profile) : '-';
|
|
1217
|
+
const account = profile.account_email || '未登录';
|
|
1218
|
+
const riskLevel = usageRiskLevel(profile, usageMap);
|
|
1219
|
+
const explain = profile.status === 'ready' ? usage.note : statusLabel(profile.status);
|
|
1220
|
+
const flag = `${profile.pinned ? '★' : ''}${riskLevel === 'critical' ? '⛔' : riskLevel === 'high' ? '⚠' : ''}` || '-';
|
|
1221
|
+
return {
|
|
1222
|
+
profile,
|
|
1223
|
+
tool: profile.tool,
|
|
1224
|
+
account,
|
|
1225
|
+
workspace,
|
|
1226
|
+
usageFive: usage.five,
|
|
1227
|
+
usageWeek: usage.week,
|
|
1228
|
+
usageFiveUsed: usage.fiveUsed,
|
|
1229
|
+
usageWeekUsed: usage.weekUsed,
|
|
1230
|
+
usageFiveMeter: usageBarCell(usage.fiveUsed, usage.five, 8),
|
|
1231
|
+
usageWeekMeter: usageBarCell(usage.weekUsed, usage.week, 8),
|
|
1232
|
+
usageNote: usage.note,
|
|
1233
|
+
explain,
|
|
1234
|
+
riskLevel,
|
|
1235
|
+
flag,
|
|
1236
|
+
pinned: Boolean(profile.pinned),
|
|
1237
|
+
status: profile.status,
|
|
1238
|
+
};
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function padRight(text, width) {
|
|
1243
|
+
const s = String(text || '');
|
|
1244
|
+
if (s.length >= width) return s;
|
|
1245
|
+
return `${s}${' '.repeat(width - s.length)}`;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function terminalWidth() {
|
|
1249
|
+
const cols = Number(process.stderr.columns || process.stdout.columns || 120);
|
|
1250
|
+
if (!Number.isFinite(cols) || cols <= 0) return 120;
|
|
1251
|
+
return cols;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
function shortSpaceName(value) {
|
|
1255
|
+
const raw = String(value || '').trim();
|
|
1256
|
+
if (!raw) return 'unknown';
|
|
1257
|
+
return raw.length > 20 ? `${raw.slice(0, 20)}...` : raw;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function profileSearchText(p) {
|
|
1261
|
+
return [
|
|
1262
|
+
p.tool,
|
|
1263
|
+
p.slug,
|
|
1264
|
+
p.display_name,
|
|
1265
|
+
p.account_email,
|
|
1266
|
+
p.space_name,
|
|
1267
|
+
p.space_display,
|
|
1268
|
+
p.enterprise_space_name,
|
|
1269
|
+
p.enterprise_space_display,
|
|
1270
|
+
...(p.aliases || []),
|
|
1271
|
+
].join(' ').toLowerCase();
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function sortProfilesForPicker(items) {
|
|
1275
|
+
return items.sort((a, b) => {
|
|
1276
|
+
const pin = Number(Boolean(b.pinned)) - Number(Boolean(a.pinned));
|
|
1277
|
+
if (pin !== 0) return pin;
|
|
1278
|
+
const sr = statusRank(a.status) - statusRank(b.status);
|
|
1279
|
+
if (sr !== 0) return sr;
|
|
1280
|
+
const aTs = a.last_used_at ? Date.parse(a.last_used_at) : NaN;
|
|
1281
|
+
const bTs = b.last_used_at ? Date.parse(b.last_used_at) : NaN;
|
|
1282
|
+
const aOk = Number.isFinite(aTs);
|
|
1283
|
+
const bOk = Number.isFinite(bTs);
|
|
1284
|
+
if (aOk && bOk && aTs !== bTs) return bTs - aTs;
|
|
1285
|
+
if (aOk !== bOk) return aOk ? -1 : 1;
|
|
1286
|
+
const t = a.tool.localeCompare(b.tool);
|
|
1287
|
+
if (t !== 0) return t;
|
|
1288
|
+
return a.slug.localeCompare(b.slug);
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
function usageSortValue(profile, usageMap, kind) {
|
|
1293
|
+
if (profile.tool !== 'codex') return -1;
|
|
1294
|
+
const entry = usageMap.get(usageCacheKey(profile));
|
|
1295
|
+
if (!entry || !entry.ok || !entry.snapshot) return -1;
|
|
1296
|
+
if (kind === 'week') return Number(entry.snapshot.weekPercent) || 0;
|
|
1297
|
+
return Number(entry.snapshot.fiveHourPercent) || 0;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function sortProfilesByMode(items, mode, usageMap) {
|
|
1301
|
+
const rows = items.slice();
|
|
1302
|
+
return rows.sort((a, b) => {
|
|
1303
|
+
if (mode !== 'recent' && mode !== 'week_desc') {
|
|
1304
|
+
const pin = Number(Boolean(b.pinned)) - Number(Boolean(a.pinned));
|
|
1305
|
+
if (pin !== 0) return pin;
|
|
1306
|
+
}
|
|
1307
|
+
const sr = statusRank(a.status) - statusRank(b.status);
|
|
1308
|
+
if (sr !== 0) return sr;
|
|
1309
|
+
|
|
1310
|
+
if (mode === 'week_desc') {
|
|
1311
|
+
const bw = usageSortValue(b, usageMap, 'week');
|
|
1312
|
+
const aw = usageSortValue(a, usageMap, 'week');
|
|
1313
|
+
if (bw !== aw) return bw - aw;
|
|
1314
|
+
const b5 = usageSortValue(b, usageMap, 'five');
|
|
1315
|
+
const a5 = usageSortValue(a, usageMap, 'five');
|
|
1316
|
+
if (b5 !== a5) return b5 - a5;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
const aTs = a.last_used_at ? Date.parse(a.last_used_at) : NaN;
|
|
1320
|
+
const bTs = b.last_used_at ? Date.parse(b.last_used_at) : NaN;
|
|
1321
|
+
const aOk = Number.isFinite(aTs);
|
|
1322
|
+
const bOk = Number.isFinite(bTs);
|
|
1323
|
+
if (aOk && bOk && aTs !== bTs) return bTs - aTs;
|
|
1324
|
+
if (aOk !== bOk) return aOk ? -1 : 1;
|
|
1325
|
+
const t = a.tool.localeCompare(b.tool);
|
|
1326
|
+
if (t !== 0) return t;
|
|
1327
|
+
return a.slug.localeCompare(b.slug);
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
function sortModeLabel(mode) {
|
|
1332
|
+
if (mode === 'pin_recent') return '收藏优先';
|
|
1333
|
+
if (mode === 'week_desc') return '周用量高→低';
|
|
1334
|
+
return '最近使用';
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
async function pickProfileInteractive(items, options = {}) {
|
|
1338
|
+
const title = options.title || '请选择账号';
|
|
1339
|
+
const allowSearch = options.allowSearch !== false;
|
|
1340
|
+
const showUsage = options.showUsage !== false;
|
|
1341
|
+
const allowActions = Boolean(options.allowActions);
|
|
1342
|
+
|
|
1343
|
+
if (!isInteractiveTTY()) {
|
|
1344
|
+
throw new CliError('当前不是交互终端,请传入具体名称 (TTY required, pass explicit profile)', 2);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
let source = sortProfilesForPicker(items.slice());
|
|
1348
|
+
let usageSeed = showUsage ? seedCodexUsageMap(source, usageCacheTtlMs(), { refreshClaude: false }) : { usageMap: new Map(), pending: [] };
|
|
1349
|
+
const usageMap = usageSeed.usageMap;
|
|
1350
|
+
let query = '';
|
|
1351
|
+
let selected = 0;
|
|
1352
|
+
const curUi = loadCurrent();
|
|
1353
|
+
let showAll = allowActions ? Boolean(curUi.ui_show_unready) : false;
|
|
1354
|
+
let sortMode = normalizeUiSortMode(curUi.ui_sort_mode || 'recent');
|
|
1355
|
+
let closed = false;
|
|
1356
|
+
let showHelp = false;
|
|
1357
|
+
const stdin = process.stdin;
|
|
1358
|
+
const sortModes = ['recent', 'pin_recent', 'week_desc'];
|
|
1359
|
+
|
|
1360
|
+
const getHiddenCount = () => source.filter((p) => p.status !== 'ready').length;
|
|
1361
|
+
const nextSortMode = () => {
|
|
1362
|
+
const idx = sortModes.indexOf(sortMode);
|
|
1363
|
+
sortMode = sortModes[(idx + 1) % sortModes.length];
|
|
1364
|
+
};
|
|
1365
|
+
const persistUi = () => {
|
|
1366
|
+
const cur = loadCurrent();
|
|
1367
|
+
let changed = false;
|
|
1368
|
+
if (cur.ui_sort_mode !== sortMode) {
|
|
1369
|
+
cur.ui_sort_mode = sortMode;
|
|
1370
|
+
changed = true;
|
|
1371
|
+
}
|
|
1372
|
+
if (cur.ui_show_unready !== showAll) {
|
|
1373
|
+
cur.ui_show_unready = showAll;
|
|
1374
|
+
changed = true;
|
|
1375
|
+
}
|
|
1376
|
+
if (changed) saveCurrent(cur);
|
|
1377
|
+
};
|
|
1378
|
+
|
|
1379
|
+
const getVisible = () => {
|
|
1380
|
+
const ranked = sortProfilesByMode(source, sortMode, usageMap);
|
|
1381
|
+
const base = showAll ? ranked : ranked.filter((p) => p.status === 'ready');
|
|
1382
|
+
if (!allowSearch || !query) return base;
|
|
1383
|
+
const q = query.toLowerCase();
|
|
1384
|
+
return base.filter((p) => profileSearchText(p).includes(q));
|
|
1385
|
+
};
|
|
1386
|
+
|
|
1387
|
+
const render = () => {
|
|
1388
|
+
const width = terminalWidth();
|
|
1389
|
+
const compact = width <= 120;
|
|
1390
|
+
const visibleProfiles = getVisible();
|
|
1391
|
+
const visible = buildProfileRows(visibleProfiles, usageMap, { includeUnready: true });
|
|
1392
|
+
const hiddenCount = getHiddenCount();
|
|
1393
|
+
const lines = [];
|
|
1394
|
+
lines.push(colorize(`${title} · ${sortModeLabel(sortMode)}${showUsage ? ` · 缓存 ${Math.round(usageCacheTtlMs() / 60000)}m` : ''}`, 'title'));
|
|
1395
|
+
lines.push(colorize(`↑↓ 选择 · Enter 启动 · Esc 退出${allowSearch ? ' · 输入搜索' : ''} · ?帮助`, 'muted'));
|
|
1396
|
+
if (allowSearch) lines.push(`搜索: ${query || colorize('全部', 'muted')}`);
|
|
1397
|
+
if (!showAll && hiddenCount > 0) {
|
|
1398
|
+
lines.push(colorize(`已隐藏异常账号 ${hiddenCount} 个(Ctrl+A 查看)`, 'muted'));
|
|
1399
|
+
}
|
|
1400
|
+
if (showHelp && allowActions) {
|
|
1401
|
+
lines.push(colorize('快捷键:A 添加 · D 诊断 · I 导入 · R 刷新用量', 'muted'));
|
|
1402
|
+
lines.push(colorize('快捷键:Tab 切排序 · Ctrl+P 收藏 · Ctrl+A 显示异常', 'muted'));
|
|
1403
|
+
renderFrame(lines);
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
if (visible.length === 0) {
|
|
1408
|
+
lines.push(colorize('! 没有可用账号,请先登录或按 Ctrl+A 查看异常账号', 'warning'));
|
|
1409
|
+
} else {
|
|
1410
|
+
if (selected >= visible.length) selected = visible.length - 1;
|
|
1411
|
+
if (selected < 0) selected = 0;
|
|
1412
|
+
for (let i = 0; i < visible.length; i += 1) {
|
|
1413
|
+
const rowInfo = visible[i];
|
|
1414
|
+
const p = rowInfo.profile;
|
|
1415
|
+
const mark = i === selected ? '>' : ' ';
|
|
1416
|
+
const pin = rowInfo.pinned ? '★ ' : '';
|
|
1417
|
+
const account = compact ? oneLineText(rowInfo.account, 30) : oneLineText(rowInfo.account, 38);
|
|
1418
|
+
const barWidth = compact ? 6 : 8;
|
|
1419
|
+
let row = '';
|
|
1420
|
+
if (p.tool === 'codex') {
|
|
1421
|
+
const ws = compact ? oneLineText(rowInfo.workspace, 12) : oneLineText(rowInfo.workspace, 18);
|
|
1422
|
+
row = `${pin}codex ${account} | ${ws}${showUsage ? ` | 5h ${usageBarCell(rowInfo.usageFiveUsed, rowInfo.usageFive, barWidth)} 周 ${usageBarCell(rowInfo.usageWeekUsed, rowInfo.usageWeek, barWidth)}` : ''}`;
|
|
1423
|
+
} else {
|
|
1424
|
+
const hasClaudeUsage = Number.isFinite(rowInfo.usageFiveUsed) || Number.isFinite(rowInfo.usageWeekUsed);
|
|
1425
|
+
row = `${pin}claude ${account}${showUsage && hasClaudeUsage
|
|
1426
|
+
? ` | 5h ${usageBarCell(rowInfo.usageFiveUsed, rowInfo.usageFive, barWidth)} 周 ${usageBarCell(rowInfo.usageWeekUsed, rowInfo.usageWeek, barWidth)}`
|
|
1427
|
+
: ' | 用量 --'}`;
|
|
1428
|
+
}
|
|
1429
|
+
if (rowInfo.status !== 'ready') {
|
|
1430
|
+
row += ` | ${colorize(statusLabel(rowInfo.status), 'warning')}`;
|
|
1431
|
+
}
|
|
1432
|
+
if (rowInfo.riskLevel === 'critical') row += ' | 危险';
|
|
1433
|
+
if (rowInfo.riskLevel === 'high') row += ' | 高压';
|
|
1434
|
+
lines.push(i === selected ? colorize(`${mark} ${row}`, 'selected') : `${mark} ${row}`);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
const current = visible[selected];
|
|
1438
|
+
let focus = `选中: ${current.account}`;
|
|
1439
|
+
if (current.tool === 'codex') {
|
|
1440
|
+
focus += ` | ${current.workspace}`;
|
|
1441
|
+
}
|
|
1442
|
+
if (showUsage && (current.tool === 'codex' || Number.isFinite(current.usageFiveUsed) || Number.isFinite(current.usageWeekUsed))) {
|
|
1443
|
+
focus += ` | 5h ${current.usageFive} 周 ${current.usageWeek}`;
|
|
1444
|
+
}
|
|
1445
|
+
if (current.explain && current.explain !== '实时' && current.explain !== '缓存') {
|
|
1446
|
+
focus += ` | ${current.explain}`;
|
|
1447
|
+
}
|
|
1448
|
+
lines.push(colorize(focus, 'muted'));
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
renderFrame(lines);
|
|
1452
|
+
};
|
|
1453
|
+
|
|
1454
|
+
return new Promise((resolve, reject) => {
|
|
1455
|
+
const cleanup = () => {
|
|
1456
|
+
closed = true;
|
|
1457
|
+
stdin.removeListener('keypress', onKey);
|
|
1458
|
+
if (typeof stdin.setRawMode === 'function') stdin.setRawMode(false);
|
|
1459
|
+
stdin.pause();
|
|
1460
|
+
clearFrame(process.stderr);
|
|
1461
|
+
showCursor(process.stderr);
|
|
1462
|
+
};
|
|
1463
|
+
|
|
1464
|
+
const onKey = (str, key = {}) => {
|
|
1465
|
+
if (key.ctrl && key.name === 'c') {
|
|
1466
|
+
cleanup();
|
|
1467
|
+
reject(new CliError('已取消', 130));
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
if (key.name === 'up') {
|
|
1471
|
+
selected -= 1;
|
|
1472
|
+
render();
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
if (key.name === 'down') {
|
|
1476
|
+
selected += 1;
|
|
1477
|
+
render();
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
1481
|
+
const visibleProfiles = getVisible();
|
|
1482
|
+
const visible = buildProfileRows(visibleProfiles, usageMap, { includeUnready: true });
|
|
1483
|
+
if (visible.length === 0) return;
|
|
1484
|
+
const chosen = visible[Math.max(0, Math.min(selected, visible.length - 1))].profile;
|
|
1485
|
+
cleanup();
|
|
1486
|
+
if (allowActions) {
|
|
1487
|
+
resolve({ kind: 'profile', profile: chosen });
|
|
1488
|
+
} else {
|
|
1489
|
+
resolve(chosen);
|
|
1490
|
+
}
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
if (key.name === 'escape') {
|
|
1494
|
+
if (showHelp) {
|
|
1495
|
+
showHelp = false;
|
|
1496
|
+
render();
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
cleanup();
|
|
1500
|
+
reject(new CliError('已取消', 130));
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
if (key.ctrl && key.name === 'a') {
|
|
1504
|
+
showAll = !showAll;
|
|
1505
|
+
persistUi();
|
|
1506
|
+
selected = 0;
|
|
1507
|
+
render();
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
if (key.name === 'tab') {
|
|
1511
|
+
nextSortMode();
|
|
1512
|
+
persistUi();
|
|
1513
|
+
selected = 0;
|
|
1514
|
+
render();
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
if (key.ctrl && key.name === 'p') {
|
|
1518
|
+
const visibleProfiles = getVisible();
|
|
1519
|
+
const visible = buildProfileRows(visibleProfiles, usageMap, { includeUnready: true });
|
|
1520
|
+
if (visible.length === 0) return;
|
|
1521
|
+
const current = visible[Math.max(0, Math.min(selected, visible.length - 1))].profile;
|
|
1522
|
+
const pinned = togglePinnedById(current.profile_id);
|
|
1523
|
+
source = source.map((p) => (p.profile_id === current.profile_id ? { ...p, pinned } : p));
|
|
1524
|
+
selected = 0;
|
|
1525
|
+
render();
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
if (allowActions && str === '?') {
|
|
1529
|
+
showHelp = !showHelp;
|
|
1530
|
+
render();
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
if (allowActions && showHelp) {
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
if (allowActions && (str === 'A' || str === 'D' || str === 'I')) {
|
|
1537
|
+
cleanup();
|
|
1538
|
+
if (str === 'A') resolve({ kind: 'action', action: 'add' });
|
|
1539
|
+
if (str === 'D') resolve({ kind: 'action', action: 'doctor' });
|
|
1540
|
+
if (str === 'I') resolve({ kind: 'action', action: 'import' });
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
if (allowActions && (str === 'R' || str === 'r') && showUsage) {
|
|
1544
|
+
usageSeed = seedCodexUsageMap(source, 0, { refreshClaude: true });
|
|
1545
|
+
usageMap.clear();
|
|
1546
|
+
for (const [k, v] of usageSeed.usageMap.entries()) {
|
|
1547
|
+
usageMap.set(k, v);
|
|
1548
|
+
}
|
|
1549
|
+
render();
|
|
1550
|
+
refreshCodexUsageMap(usageSeed, {
|
|
1551
|
+
onUpdate: (k, v) => {
|
|
1552
|
+
usageMap.set(k, v);
|
|
1553
|
+
if (!closed) render();
|
|
1554
|
+
},
|
|
1555
|
+
}).then(() => {
|
|
1556
|
+
if (!closed) render();
|
|
1557
|
+
}).catch(() => {
|
|
1558
|
+
if (!closed) render();
|
|
1559
|
+
});
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
if (allowSearch && key.name === 'backspace') {
|
|
1563
|
+
query = query.slice(0, -1);
|
|
1564
|
+
selected = 0;
|
|
1565
|
+
render();
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
if (allowSearch && key.ctrl && key.name === 'u') {
|
|
1569
|
+
query = '';
|
|
1570
|
+
selected = 0;
|
|
1571
|
+
render();
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
if (allowSearch && str && !key.ctrl && !key.meta) {
|
|
1575
|
+
const code = str.charCodeAt(0);
|
|
1576
|
+
if (code >= 32 && code !== 127) {
|
|
1577
|
+
query += str;
|
|
1578
|
+
selected = 0;
|
|
1579
|
+
render();
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
|
|
1584
|
+
readline.emitKeypressEvents(stdin);
|
|
1585
|
+
if (typeof stdin.setRawMode === 'function') stdin.setRawMode(true);
|
|
1586
|
+
stdin.resume();
|
|
1587
|
+
stdin.on('keypress', onKey);
|
|
1588
|
+
render();
|
|
1589
|
+
|
|
1590
|
+
if (showUsage && usageSeed.pending.length > 0) {
|
|
1591
|
+
refreshCodexUsageMap(usageSeed, {
|
|
1592
|
+
onUpdate: () => {
|
|
1593
|
+
if (!closed) render();
|
|
1594
|
+
},
|
|
1595
|
+
}).then(() => {
|
|
1596
|
+
if (!closed) render();
|
|
1597
|
+
}).catch(() => {
|
|
1598
|
+
if (!closed) render();
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
function printQuickHelp() {
|
|
1605
|
+
process.stdout.write([
|
|
1606
|
+
'🤖 cprofile',
|
|
1607
|
+
'',
|
|
1608
|
+
'新手入口:',
|
|
1609
|
+
' cprofile # 直接进入账号选择并回车启动',
|
|
1610
|
+
'',
|
|
1611
|
+
'列表内快捷键:',
|
|
1612
|
+
' A 添加账号 D 诊断 I 导入 R 刷新用量 ? 帮助',
|
|
1613
|
+
' Tab 排序 Ctrl+P 收藏 Ctrl+A 显示异常',
|
|
1614
|
+
'',
|
|
1615
|
+
'查看全部命令:',
|
|
1616
|
+
' cprofile --advanced-help',
|
|
1617
|
+
].join('\n') + '\n');
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
function printAdvancedHelp() {
|
|
1621
|
+
process.stdout.write([
|
|
1622
|
+
'cprofile 高级命令',
|
|
1623
|
+
'',
|
|
1624
|
+
' add <name> [--tool codex|claude]',
|
|
1625
|
+
' login <id> [--tool codex|claude]',
|
|
1626
|
+
' use [<id>] [--tool codex|claude|all]',
|
|
1627
|
+
' ls [--tool codex|claude|all]',
|
|
1628
|
+
' usage [--tool codex|claude|all]',
|
|
1629
|
+
' current',
|
|
1630
|
+
' run <id> [--tool codex|claude] -- <raw_args>',
|
|
1631
|
+
' rm <id> [--tool codex|claude]',
|
|
1632
|
+
' rename <id> <new_slug> [--tool codex|claude]',
|
|
1633
|
+
' env use [<id>] [--tool codex|claude|all]',
|
|
1634
|
+
' doctor [--tool codex|claude|all]',
|
|
1635
|
+
' guide [readme|quickstart|troubleshooting|naming]',
|
|
1636
|
+
' home',
|
|
1637
|
+
' import [--tool codex|claude|all] [--cleanup]',
|
|
1638
|
+
' migrate',
|
|
1639
|
+
' name suggest --tool <tool> --account <email> --space <space>',
|
|
1640
|
+
].join('\n') + '\n');
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
function safeMenuItem(item) {
|
|
1644
|
+
return {
|
|
1645
|
+
value: item.value,
|
|
1646
|
+
label: item.label || '',
|
|
1647
|
+
desc: item.desc || '',
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
async function chooseFromMenu(title, options) {
|
|
1652
|
+
if (!isInteractiveTTY()) {
|
|
1653
|
+
throw new CliError('当前不是交互终端 (TTY required)');
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
const menu = (options || []).map((x) => safeMenuItem(x));
|
|
1657
|
+
if (menu.length === 0) {
|
|
1658
|
+
throw new CliError('菜单为空');
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
let idx = 0;
|
|
1662
|
+
const stdin = process.stdin;
|
|
1663
|
+
|
|
1664
|
+
const render = () => {
|
|
1665
|
+
const lines = [];
|
|
1666
|
+
lines.push(colorize(title, 'title'));
|
|
1667
|
+
lines.push(colorize('↑↓ 选择 · Enter 确认 · Esc 返回', 'muted'));
|
|
1668
|
+
lines.push('');
|
|
1669
|
+
for (let i = 0; i < menu.length; i += 1) {
|
|
1670
|
+
const mark = i === idx ? '›' : ' ';
|
|
1671
|
+
const line = `${mark} ${menu[i].label}`;
|
|
1672
|
+
lines.push(i === idx ? colorize(line, 'selected') : line);
|
|
1673
|
+
if (menu[i].desc) lines.push(colorize(` ${menu[i].desc}`, 'muted'));
|
|
1674
|
+
}
|
|
1675
|
+
renderFrame(lines);
|
|
1676
|
+
};
|
|
1677
|
+
|
|
1678
|
+
return new Promise((resolve, reject) => {
|
|
1679
|
+
const cleanup = () => {
|
|
1680
|
+
stdin.removeListener('keypress', onKey);
|
|
1681
|
+
if (typeof stdin.setRawMode === 'function') stdin.setRawMode(false);
|
|
1682
|
+
stdin.pause();
|
|
1683
|
+
clearFrame(process.stderr);
|
|
1684
|
+
showCursor(process.stderr);
|
|
1685
|
+
};
|
|
1686
|
+
|
|
1687
|
+
const onKey = (_str, key = {}) => {
|
|
1688
|
+
if (key.ctrl && key.name === 'c') {
|
|
1689
|
+
cleanup();
|
|
1690
|
+
reject(new CliError('已取消', 130));
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
if (key.name === 'up') {
|
|
1694
|
+
idx = idx <= 0 ? menu.length - 1 : idx - 1;
|
|
1695
|
+
render();
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
if (key.name === 'down') {
|
|
1699
|
+
idx = idx >= menu.length - 1 ? 0 : idx + 1;
|
|
1700
|
+
render();
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
1704
|
+
const v = menu[idx];
|
|
1705
|
+
cleanup();
|
|
1706
|
+
resolve(v.value);
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
if (key.name === 'escape') {
|
|
1710
|
+
cleanup();
|
|
1711
|
+
resolve(null);
|
|
1712
|
+
}
|
|
1713
|
+
};
|
|
1714
|
+
|
|
1715
|
+
readline.emitKeypressEvents(stdin);
|
|
1716
|
+
if (typeof stdin.setRawMode === 'function') stdin.setRawMode(true);
|
|
1717
|
+
stdin.resume();
|
|
1718
|
+
stdin.on('keypress', onKey);
|
|
1719
|
+
render();
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
async function askLine(question) {
|
|
1724
|
+
if (!isInteractiveTTY()) {
|
|
1725
|
+
throw new CliError('当前不是交互终端 (TTY required)');
|
|
1726
|
+
}
|
|
1727
|
+
showCursor(process.stderr);
|
|
1728
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
1729
|
+
return new Promise((resolve) => {
|
|
1730
|
+
rl.question(question, (ans) => {
|
|
1731
|
+
rl.close();
|
|
1732
|
+
resolve(String(ans || '').trim());
|
|
1733
|
+
});
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
function resolveByTool(reg, toolOption) {
|
|
1738
|
+
if (toolOption === 'all') return allProfiles(reg);
|
|
1739
|
+
return reg.profiles[toolOption].map((p) => ({ ...p, tool: toolOption }));
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
function resolveProfilesByInput(reg, identifier, toolOption) {
|
|
1743
|
+
const pool = resolveByTool(reg, toolOption);
|
|
1744
|
+
|
|
1745
|
+
const stage1 = pool.filter((p) => p.slug === identifier);
|
|
1746
|
+
if (stage1.length) return { stage: 'slug', matches: stage1 };
|
|
1747
|
+
|
|
1748
|
+
const stage2 = pool.filter((p) => (p.aliases || []).includes(identifier));
|
|
1749
|
+
if (stage2.length) return { stage: 'alias', matches: stage2 };
|
|
1750
|
+
|
|
1751
|
+
const stage3 = pool.filter((p) => p.display_name === identifier);
|
|
1752
|
+
if (stage3.length) return { stage: 'display_name', matches: stage3 };
|
|
1753
|
+
|
|
1754
|
+
return { stage: 'none', matches: [] };
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
async function resolveUseTarget(reg, name, toolOption) {
|
|
1758
|
+
if (!name) {
|
|
1759
|
+
const pool = resolveByTool(reg, toolOption);
|
|
1760
|
+
if (pool.length === 0) {
|
|
1761
|
+
throw new CliError(msg('没有可用 profile,请先添加账号', 'No profiles found. Add one first'));
|
|
1762
|
+
}
|
|
1763
|
+
return pickProfileInteractive(pool, { title: '快速切换', allowSearch: true });
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
const { stage, matches } = resolveProfilesByInput(reg, name, toolOption);
|
|
1767
|
+
if (matches.length === 0) {
|
|
1768
|
+
throw new CliError(`${msg(`未找到 profile: ${name}`, `Profile not found: ${name}`)}\n${msg('建议:执行 cprofile ls 或 cprofile guide naming', 'Try: cprofile ls or cprofile guide naming')}`);
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
if (matches.length === 1) return matches[0];
|
|
1772
|
+
|
|
1773
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
1774
|
+
throw new CliError(`${msg(`名称命中多个 profile: ${name}`, `Multiple profiles matched: ${name}`)}\n${msg('请指定更精确 slug 或加 --tool', 'Use exact slug or --tool')}`);
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
return pickProfileInteractive(matches, {
|
|
1778
|
+
title: `命中多个结果(${stage}),请选择`,
|
|
1779
|
+
allowSearch: true,
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
function emitUseEnv(profile) {
|
|
1784
|
+
const lines = [];
|
|
1785
|
+
if (profile.tool === 'codex') {
|
|
1786
|
+
lines.push(`export CODEX_HOME=${shellQuote(profile.path)}`);
|
|
1787
|
+
lines.push('unset CLAUDE_CONFIG_DIR');
|
|
1788
|
+
} else {
|
|
1789
|
+
lines.push(`export CLAUDE_CONFIG_DIR=${shellQuote(profile.path)}`);
|
|
1790
|
+
lines.push('unset CODEX_HOME');
|
|
1791
|
+
}
|
|
1792
|
+
lines.push(`export CPROFILE_ACTIVE_TOOL=${shellQuote(profile.tool)}`);
|
|
1793
|
+
lines.push(`export CPROFILE_ACTIVE_PROFILE=${shellQuote(profile.slug)}`);
|
|
1794
|
+
lines.push(`export CPROFILE_ACTIVE_PROFILE_ID=${shellQuote(profile.profile_id)}`);
|
|
1795
|
+
return lines.join('\n');
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
function setActiveProfile(reg, profile) {
|
|
1799
|
+
const cur = loadCurrent();
|
|
1800
|
+
cur.active_tool = profile.tool;
|
|
1801
|
+
cur.active_name = profile.slug;
|
|
1802
|
+
cur.active_profile_id = profile.profile_id;
|
|
1803
|
+
cur.last_switched_at = now();
|
|
1804
|
+
saveCurrent(cur);
|
|
1805
|
+
|
|
1806
|
+
const list = reg.profiles[profile.tool];
|
|
1807
|
+
const p = list.find((x) => x.profile_id === profile.profile_id);
|
|
1808
|
+
if (p) {
|
|
1809
|
+
p.last_used_at = now();
|
|
1810
|
+
p.status = readAuthInfo(p.tool, p.path).status;
|
|
1811
|
+
saveRegistry(reg);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
function spawnInProfile(profile, args = []) {
|
|
1816
|
+
const cmd = profile.tool === 'codex' ? 'codex' : 'claude';
|
|
1817
|
+
const env = { ...process.env };
|
|
1818
|
+
if (profile.tool === 'codex') env.CODEX_HOME = profile.path;
|
|
1819
|
+
if (profile.tool === 'claude') env.CLAUDE_CONFIG_DIR = profile.path;
|
|
1820
|
+
|
|
1821
|
+
const res = spawnSync(cmd, args, { stdio: 'inherit', env });
|
|
1822
|
+
if (res.error) {
|
|
1823
|
+
if (res.error.code === 'ENOENT') {
|
|
1824
|
+
throw new CliError(msg(`命令不存在: ${cmd}`, `Command not found: ${cmd}`), 127);
|
|
1825
|
+
}
|
|
1826
|
+
throw new CliError(msg(`启动失败: ${res.error.message}`, `Launch failed: ${res.error.message}`));
|
|
1827
|
+
}
|
|
1828
|
+
if (typeof res.status === 'number' && res.status !== 0) {
|
|
1829
|
+
throw new CliError(msg(`${cmd} 退出码 ${res.status}`, `${cmd} exited with ${res.status}`), res.status);
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
function updateIdentityAfterAuth(reg, profile) {
|
|
1834
|
+
const auth = readAuthInfo(profile.tool, profile.path);
|
|
1835
|
+
profile.account_key = auth.account_key;
|
|
1836
|
+
profile.space_key = auth.space_key;
|
|
1837
|
+
profile.account_email = auth.account_email;
|
|
1838
|
+
profile.space_name = auth.space_name;
|
|
1839
|
+
profile.space_display = auth.space_display;
|
|
1840
|
+
if (!('enterprise_space_name' in profile)) profile.enterprise_space_name = '';
|
|
1841
|
+
if (!('enterprise_space_id' in profile)) profile.enterprise_space_id = '';
|
|
1842
|
+
if (!('enterprise_space_display' in profile)) profile.enterprise_space_display = '';
|
|
1843
|
+
if (!Array.isArray(profile.org_candidates)) profile.org_candidates = [];
|
|
1844
|
+
if (!profile.enterprise_space_name && auth.enterprise_space_name) profile.enterprise_space_name = auth.enterprise_space_name;
|
|
1845
|
+
if (!profile.enterprise_space_id && auth.enterprise_space_id) profile.enterprise_space_id = auth.enterprise_space_id;
|
|
1846
|
+
if (!profile.enterprise_space_display && auth.enterprise_space_display) profile.enterprise_space_display = auth.enterprise_space_display;
|
|
1847
|
+
if (profile.org_candidates.length === 0 && Array.isArray(auth.org_candidates) && auth.org_candidates.length > 0) {
|
|
1848
|
+
profile.org_candidates = auth.org_candidates;
|
|
1849
|
+
}
|
|
1850
|
+
profile.plan = auth.plan || '';
|
|
1851
|
+
profile.status = auth.status;
|
|
1852
|
+
profile.display_name = buildDisplayName(profile.account_email, profile.space_name);
|
|
1853
|
+
|
|
1854
|
+
const desiredBase = buildSlug(profile.tool, profile.account_email, profile.space_name, profile.account_key, profile.space_key);
|
|
1855
|
+
const desired = ensureUniqueSlug(profile.tool, desiredBase, allProfiles(reg), profile.profile_id);
|
|
1856
|
+
if (profile.slug !== desired) {
|
|
1857
|
+
profile.aliases = normalizeAliases([...(profile.aliases || []), profile.slug]);
|
|
1858
|
+
profile.slug = desired;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
function addProfile(name, tool, options = {}) {
|
|
1863
|
+
const silent = Boolean(options.silent);
|
|
1864
|
+
validateName(name);
|
|
1865
|
+
const reg = loadRegistry();
|
|
1866
|
+
const pool = reg.profiles[tool];
|
|
1867
|
+
|
|
1868
|
+
if (pool.some((p) => p.slug === name || (p.aliases || []).includes(name))) {
|
|
1869
|
+
throw new CliError(msg(`已存在同名 profile: ${name}`, `Profile already exists: ${name}`));
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
const profileId = makeProfileId(`${tool}|${name}`);
|
|
1873
|
+
const pPath = profilePathById(tool, profileId);
|
|
1874
|
+
ensureDir(pPath, 0o700);
|
|
1875
|
+
|
|
1876
|
+
const alias = name;
|
|
1877
|
+
const placeholderAccount = `${name}@local`;
|
|
1878
|
+
const placeholderSpace = 'unknown';
|
|
1879
|
+
const accountKey = `pending:${shortHash(`${tool}|${name}|acct`, 8)}`;
|
|
1880
|
+
const spaceKey = `pending:${shortHash(`${tool}|${name}|space`, 8)}`;
|
|
1881
|
+
const baseSlug = buildSlug(tool, placeholderAccount, placeholderSpace, accountKey, spaceKey);
|
|
1882
|
+
const slug = ensureUniqueSlug(tool, baseSlug, allProfiles(reg));
|
|
1883
|
+
|
|
1884
|
+
const profile = {
|
|
1885
|
+
profile_id: profileId,
|
|
1886
|
+
slug,
|
|
1887
|
+
display_name: buildDisplayName('', 'unknown'),
|
|
1888
|
+
aliases: normalizeAliases([alias]),
|
|
1889
|
+
account_key: accountKey,
|
|
1890
|
+
space_key: spaceKey,
|
|
1891
|
+
account_email: '',
|
|
1892
|
+
space_name: 'unknown',
|
|
1893
|
+
space_display: 'unknown',
|
|
1894
|
+
enterprise_space_name: '',
|
|
1895
|
+
enterprise_space_id: '',
|
|
1896
|
+
enterprise_space_display: '',
|
|
1897
|
+
org_candidates: [],
|
|
1898
|
+
plan: '',
|
|
1899
|
+
status: 'needs_login',
|
|
1900
|
+
tool,
|
|
1901
|
+
path: pPath,
|
|
1902
|
+
created_at: now(),
|
|
1903
|
+
last_login_at: null,
|
|
1904
|
+
last_used_at: null,
|
|
1905
|
+
pinned: false,
|
|
1906
|
+
};
|
|
1907
|
+
|
|
1908
|
+
pool.push(profile);
|
|
1909
|
+
saveRegistry(reg);
|
|
1910
|
+
writeJsonAtomic(path.join(pPath, 'meta.json'), profile, 0o600);
|
|
1911
|
+
|
|
1912
|
+
if (!silent) {
|
|
1913
|
+
process.stdout.write(`${msg('已添加 profile', 'Profile added')}: ${tool}/${profile.slug}\n`);
|
|
1914
|
+
if (profile.slug !== name) {
|
|
1915
|
+
process.stdout.write(`${msg('已保留别名', 'Alias added')}: ${name}\n`);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
return profile;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
function findSingleProfileOrThrow(reg, identifier, tool) {
|
|
1922
|
+
const { matches } = resolveProfilesByInput(reg, identifier, tool);
|
|
1923
|
+
if (matches.length === 0) {
|
|
1924
|
+
throw new CliError(msg(`未找到 profile: ${identifier}`, `Profile not found: ${identifier}`));
|
|
1925
|
+
}
|
|
1926
|
+
if (matches.length > 1) {
|
|
1927
|
+
throw new CliError(msg(`命中多个 profile: ${identifier},请使用 slug`, `Multiple profiles matched: ${identifier}, use slug`));
|
|
1928
|
+
}
|
|
1929
|
+
return matches[0];
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
function loginProfile(identifier, tool, options = {}) {
|
|
1933
|
+
const silent = Boolean(options.silent);
|
|
1934
|
+
const reg = loadRegistry();
|
|
1935
|
+
const profile = findSingleProfileOrThrow(reg, identifier, tool);
|
|
1936
|
+
|
|
1937
|
+
withLocks([lockPath(tool, profile.profile_id)], () => {
|
|
1938
|
+
ensureDir(profile.path, 0o700);
|
|
1939
|
+
if (tool === 'codex') {
|
|
1940
|
+
spawnInProfile(profile, ['login']);
|
|
1941
|
+
} else {
|
|
1942
|
+
spawnInProfile(profile, ['setup-token']);
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
profile.last_login_at = now();
|
|
1946
|
+
updateIdentityAfterAuth(reg, profile);
|
|
1947
|
+
saveRegistry(reg);
|
|
1948
|
+
writeJsonAtomic(path.join(profile.path, 'meta.json'), profile, 0o600);
|
|
1949
|
+
|
|
1950
|
+
if (!silent) {
|
|
1951
|
+
process.stdout.write(`${msg('登录完成', 'Login completed')}: ${tool}/${profile.slug}\n`);
|
|
1952
|
+
}
|
|
1953
|
+
});
|
|
1954
|
+
return profile;
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
async function useProfile(name, toolOption, outputMode = 'env') {
|
|
1958
|
+
const reg = loadRegistry();
|
|
1959
|
+
const profile = await resolveUseTarget(reg, name, toolOption);
|
|
1960
|
+
setActiveProfile(reg, profile);
|
|
1961
|
+
|
|
1962
|
+
if (outputMode === 'env') {
|
|
1963
|
+
process.stdout.write(emitUseEnv(profile) + '\n');
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
if (outputMode === 'spawn') {
|
|
1968
|
+
finalizeWithToast(`已切换并启动: ${profile.tool}/${profile.slug}`);
|
|
1969
|
+
spawnInProfile(profile, []);
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
throw new CliError(msg(`未知输出模式: ${outputMode}`, `Unknown output mode: ${outputMode}`));
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
function listProfiles(toolOption) {
|
|
1977
|
+
const reg = loadRegistry();
|
|
1978
|
+
const rows = resolveByTool(reg, toolOption);
|
|
1979
|
+
sortProfilesForPicker(rows);
|
|
1980
|
+
|
|
1981
|
+
if (rows.length === 0) {
|
|
1982
|
+
process.stdout.write(`${msg('暂无 profile', 'No profiles found')}\n`);
|
|
1983
|
+
process.stdout.write(`${msg('下一步:cprofile add <name> --tool codex|claude', 'Next: cprofile add <name> --tool codex|claude')}\n`);
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
const usageMap = seedCodexUsageMap(rows).usageMap;
|
|
1988
|
+
const displayRows = buildProfileRows(rows, usageMap, { includeUnready: false });
|
|
1989
|
+
const hidden = rows.length - displayRows.length;
|
|
1990
|
+
process.stdout.write(`🤖 账号总览(账号 + 工作空间 + 用量)\n`);
|
|
1991
|
+
if (hidden > 0) {
|
|
1992
|
+
process.stdout.write(`已隐藏异常账号 ${hidden} 个(仅展示可用账号)\n`);
|
|
1993
|
+
}
|
|
1994
|
+
process.stdout.write(`${padRight('标', 4)}${padRight('工具', 7)}${padRight('账号', 34)}${padRight('工作空间', 16)}${padRight('5h(进度+已用/余)', 20)}${padRight('周(进度+已用/余)', 20)}说明\n`);
|
|
1995
|
+
for (const row of displayRows) {
|
|
1996
|
+
process.stdout.write(`${padRight(row.flag, 4)}${padRight(row.tool, 7)}${padRight(row.account, 34)}${padRight(row.workspace, 16)}${padRight(row.usageFiveMeter, 20)}${padRight(row.usageWeekMeter, 20)}${row.explain}\n`);
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
function showCurrent() {
|
|
2001
|
+
const cur = loadCurrent();
|
|
2002
|
+
process.stdout.write(`active_tool=${cur.active_tool || ''}\n`);
|
|
2003
|
+
process.stdout.write(`active_name=${cur.active_name || ''}\n`);
|
|
2004
|
+
process.stdout.write(`active_profile_id=${cur.active_profile_id || ''}\n`);
|
|
2005
|
+
process.stdout.write(`last_switched_at=${cur.last_switched_at || ''}\n`);
|
|
2006
|
+
process.stdout.write(`ui_sort_mode=${cur.ui_sort_mode || 'recent'}\n`);
|
|
2007
|
+
process.stdout.write(`ui_show_unready=${cur.ui_show_unready ? '1' : '0'}\n`);
|
|
2008
|
+
process.stdout.write(`env_CODEX_HOME=${process.env.CODEX_HOME || ''}\n`);
|
|
2009
|
+
process.stdout.write(`env_CLAUDE_CONFIG_DIR=${process.env.CLAUDE_CONFIG_DIR || ''}\n`);
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
function normalizeRateWindow(windowRow) {
|
|
2013
|
+
if (!windowRow || typeof windowRow !== 'object') return null;
|
|
2014
|
+
const usedPercent = Number(windowRow.usedPercent);
|
|
2015
|
+
const windowDurationMins = Number(windowRow.windowDurationMins);
|
|
2016
|
+
const resetsAt = Number(windowRow.resetsAt);
|
|
2017
|
+
return {
|
|
2018
|
+
usedPercent: Number.isFinite(usedPercent) ? usedPercent : null,
|
|
2019
|
+
windowDurationMins: Number.isFinite(windowDurationMins) ? windowDurationMins : null,
|
|
2020
|
+
resetsAt: Number.isFinite(resetsAt) ? resetsAt : null,
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
function extractCodexRateSnapshot(rateResponse) {
|
|
2025
|
+
if (!rateResponse || typeof rateResponse !== 'object') return null;
|
|
2026
|
+
const byLimitId = rateResponse.rateLimitsByLimitId && typeof rateResponse.rateLimitsByLimitId === 'object'
|
|
2027
|
+
? rateResponse.rateLimitsByLimitId
|
|
2028
|
+
: null;
|
|
2029
|
+
|
|
2030
|
+
let rate = rateResponse.rateLimits || null;
|
|
2031
|
+
if (!rate && byLimitId) {
|
|
2032
|
+
rate = byLimitId.codex || Object.values(byLimitId).find((item) => item && typeof item === 'object') || null;
|
|
2033
|
+
}
|
|
2034
|
+
if (!rate || typeof rate !== 'object') return null;
|
|
2035
|
+
|
|
2036
|
+
const windows = [];
|
|
2037
|
+
const primary = normalizeRateWindow(rate.primary);
|
|
2038
|
+
const secondary = normalizeRateWindow(rate.secondary);
|
|
2039
|
+
if (primary) windows.push(primary);
|
|
2040
|
+
if (secondary) windows.push(secondary);
|
|
2041
|
+
|
|
2042
|
+
const byMinutes = (minutes) => windows.find((w) => w.windowDurationMins === minutes) || null;
|
|
2043
|
+
const withDuration = windows.filter((w) => Number.isFinite(w.windowDurationMins));
|
|
2044
|
+
const sortedAsc = withDuration.slice().sort((a, b) => a.windowDurationMins - b.windowDurationMins);
|
|
2045
|
+
const sortedDesc = withDuration.slice().sort((a, b) => b.windowDurationMins - a.windowDurationMins);
|
|
2046
|
+
|
|
2047
|
+
let fiveHour = byMinutes(300);
|
|
2048
|
+
let weekly = byMinutes(10080);
|
|
2049
|
+
if (!fiveHour && sortedAsc.length > 0) fiveHour = sortedAsc[0];
|
|
2050
|
+
if (!weekly && sortedDesc.length > 0) weekly = sortedDesc[0];
|
|
2051
|
+
if (!fiveHour && windows.length > 0) fiveHour = windows[0];
|
|
2052
|
+
if (!weekly && windows.length > 0) weekly = windows[windows.length - 1];
|
|
2053
|
+
|
|
2054
|
+
return {
|
|
2055
|
+
limitId: rate.limitId || '',
|
|
2056
|
+
planType: rate.planType || '',
|
|
2057
|
+
fiveHourPercent: fiveHour ? fiveHour.usedPercent : null,
|
|
2058
|
+
fiveHourResetAt: fiveHour ? fiveHour.resetsAt : null,
|
|
2059
|
+
weekPercent: weekly ? weekly.usedPercent : null,
|
|
2060
|
+
weekResetAt: weekly ? weekly.resetsAt : null,
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
async function readCodexRateLimitsLive(profilePath) {
|
|
2065
|
+
return new Promise((resolve) => {
|
|
2066
|
+
const initId = `init_${shortHash(`${Date.now()}|${Math.random()}`, 8)}`;
|
|
2067
|
+
const readId = `rate_${shortHash(`${Date.now()}|${Math.random()}`, 8)}`;
|
|
2068
|
+
const child = spawn('codex', ['app-server'], {
|
|
2069
|
+
env: { ...process.env, CODEX_HOME: profilePath },
|
|
2070
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
2071
|
+
});
|
|
2072
|
+
|
|
2073
|
+
let settled = false;
|
|
2074
|
+
let sentRead = false;
|
|
2075
|
+
let outBuf = '';
|
|
2076
|
+
let errBuf = '';
|
|
2077
|
+
let timer = null;
|
|
2078
|
+
|
|
2079
|
+
const finalize = (result) => {
|
|
2080
|
+
if (settled) return;
|
|
2081
|
+
settled = true;
|
|
2082
|
+
if (timer) clearTimeout(timer);
|
|
2083
|
+
try { child.stdin.end(); } catch {}
|
|
2084
|
+
try { child.kill('SIGTERM'); } catch {}
|
|
2085
|
+
setTimeout(() => {
|
|
2086
|
+
try { child.kill('SIGKILL'); } catch {}
|
|
2087
|
+
}, 120);
|
|
2088
|
+
resolve(result);
|
|
2089
|
+
};
|
|
2090
|
+
|
|
2091
|
+
const writeReq = (req) => {
|
|
2092
|
+
try {
|
|
2093
|
+
child.stdin.write(`${JSON.stringify(req)}\n`);
|
|
2094
|
+
} catch (err) {
|
|
2095
|
+
finalize({
|
|
2096
|
+
ok: false,
|
|
2097
|
+
error: msg(`写入 app-server 失败: ${err.message}`, `Failed to write app-server input: ${err.message}`),
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
};
|
|
2101
|
+
|
|
2102
|
+
const handleLine = (line) => {
|
|
2103
|
+
if (!line) return;
|
|
2104
|
+
let obj = null;
|
|
2105
|
+
try {
|
|
2106
|
+
obj = JSON.parse(line);
|
|
2107
|
+
} catch {
|
|
2108
|
+
return;
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
if (obj.id === initId) {
|
|
2112
|
+
if (obj.error) {
|
|
2113
|
+
finalize({
|
|
2114
|
+
ok: false,
|
|
2115
|
+
error: oneLineText(obj.error.message || msg('初始化失败', 'Initialize failed')),
|
|
2116
|
+
});
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
if (!sentRead) {
|
|
2120
|
+
sentRead = true;
|
|
2121
|
+
writeReq({
|
|
2122
|
+
method: 'account/rateLimits/read',
|
|
2123
|
+
id: readId,
|
|
2124
|
+
params: {},
|
|
2125
|
+
});
|
|
2126
|
+
}
|
|
2127
|
+
return;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
if (obj.id === readId) {
|
|
2131
|
+
if (obj.error) {
|
|
2132
|
+
finalize({
|
|
2133
|
+
ok: false,
|
|
2134
|
+
error: oneLineText(obj.error.message || msg('读取用量失败', 'Failed to read rate limits')),
|
|
2135
|
+
});
|
|
2136
|
+
return;
|
|
2137
|
+
}
|
|
2138
|
+
const snapshot = extractCodexRateSnapshot(obj.result);
|
|
2139
|
+
if (!snapshot) {
|
|
2140
|
+
finalize({
|
|
2141
|
+
ok: false,
|
|
2142
|
+
error: msg('返回中没有可识别的用量信息', 'No usable rate limits in response'),
|
|
2143
|
+
});
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
finalize({ ok: true, snapshot });
|
|
2147
|
+
}
|
|
2148
|
+
};
|
|
2149
|
+
|
|
2150
|
+
child.stdout.on('data', (chunk) => {
|
|
2151
|
+
outBuf += chunk.toString();
|
|
2152
|
+
let idx = outBuf.indexOf('\n');
|
|
2153
|
+
while (idx >= 0) {
|
|
2154
|
+
const line = outBuf.slice(0, idx).trim();
|
|
2155
|
+
outBuf = outBuf.slice(idx + 1);
|
|
2156
|
+
handleLine(line);
|
|
2157
|
+
idx = outBuf.indexOf('\n');
|
|
2158
|
+
}
|
|
2159
|
+
});
|
|
2160
|
+
|
|
2161
|
+
child.stderr.on('data', (chunk) => {
|
|
2162
|
+
errBuf += chunk.toString();
|
|
2163
|
+
if (errBuf.length > 4000) errBuf = errBuf.slice(-4000);
|
|
2164
|
+
});
|
|
2165
|
+
|
|
2166
|
+
child.on('error', (err) => {
|
|
2167
|
+
finalize({
|
|
2168
|
+
ok: false,
|
|
2169
|
+
error: msg(`启动 codex 失败: ${err.message}`, `Failed to launch codex: ${err.message}`),
|
|
2170
|
+
});
|
|
2171
|
+
});
|
|
2172
|
+
|
|
2173
|
+
child.on('exit', (code, signal) => {
|
|
2174
|
+
if (settled) return;
|
|
2175
|
+
const tip = oneLineText(errBuf) || `${code || ''}${signal ? `/${signal}` : ''}`;
|
|
2176
|
+
finalize({
|
|
2177
|
+
ok: false,
|
|
2178
|
+
error: msg(`app-server 提前退出: ${tip}`, `app-server exited early: ${tip}`),
|
|
2179
|
+
});
|
|
2180
|
+
});
|
|
2181
|
+
|
|
2182
|
+
timer = setTimeout(() => {
|
|
2183
|
+
finalize({
|
|
2184
|
+
ok: false,
|
|
2185
|
+
error: msg('实时用量查询超时', 'Live usage query timed out'),
|
|
2186
|
+
});
|
|
2187
|
+
}, 20000);
|
|
2188
|
+
|
|
2189
|
+
writeReq({
|
|
2190
|
+
method: 'initialize',
|
|
2191
|
+
id: initId,
|
|
2192
|
+
params: {
|
|
2193
|
+
clientInfo: { name: 'cprofile', version: '1.0' },
|
|
2194
|
+
capabilities: { experimentalApi: true },
|
|
2195
|
+
},
|
|
2196
|
+
});
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
function seedCodexUsageMap(rows, ttlMs = usageCacheTtlMs(), options = {}) {
|
|
2201
|
+
const refreshClaude = Boolean(options.refreshClaude);
|
|
2202
|
+
const allowClaudeExperimental = isClaudeUsageExperimentalEnabled();
|
|
2203
|
+
const store = loadUsageCache();
|
|
2204
|
+
const usageMap = new Map();
|
|
2205
|
+
const pending = [];
|
|
2206
|
+
const seen = new Set();
|
|
2207
|
+
|
|
2208
|
+
for (const row of rows) {
|
|
2209
|
+
if (row.tool !== 'codex' && row.tool !== 'claude') continue;
|
|
2210
|
+
if (row.tool === 'claude' && !allowClaudeExperimental) continue;
|
|
2211
|
+
const key = usageCacheKey(row);
|
|
2212
|
+
if (seen.has(key)) continue;
|
|
2213
|
+
seen.add(key);
|
|
2214
|
+
|
|
2215
|
+
const cached = normalizeUsageCacheEntry(store.entries[key]);
|
|
2216
|
+
const isFresh = cached ? isUsageEntryFresh(cached, ttlMs) : false;
|
|
2217
|
+
if (cached) {
|
|
2218
|
+
usageMap.set(key, {
|
|
2219
|
+
...cached,
|
|
2220
|
+
source: isFresh ? 'cache' : 'stale',
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
if (row.status !== 'ready') {
|
|
2225
|
+
usageMap.set(key, {
|
|
2226
|
+
ok: false,
|
|
2227
|
+
checked_at: cached && cached.checked_at ? cached.checked_at : now(),
|
|
2228
|
+
snapshot: null,
|
|
2229
|
+
error: msg('未登录或凭证不可用', 'Not logged in or credentials unavailable'),
|
|
2230
|
+
source: 'status',
|
|
2231
|
+
});
|
|
2232
|
+
continue;
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
if (row.tool === 'claude' && !refreshClaude) {
|
|
2236
|
+
continue;
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
if (cached && isFresh && cached.ok) continue;
|
|
2240
|
+
pending.push({ key, row });
|
|
2241
|
+
if (!usageMap.has(key)) {
|
|
2242
|
+
usageMap.set(key, {
|
|
2243
|
+
ok: false,
|
|
2244
|
+
checked_at: '',
|
|
2245
|
+
snapshot: null,
|
|
2246
|
+
error: '',
|
|
2247
|
+
source: 'loading',
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
return { usageMap, pending, store };
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
async function refreshCodexUsageMap(seed, options = {}) {
|
|
2256
|
+
const onUpdate = typeof options.onUpdate === 'function' ? options.onUpdate : null;
|
|
2257
|
+
const rawConcurrency = Number(options.concurrency || process.env.CPROFILE_USAGE_REFRESH_CONCURRENCY || 3);
|
|
2258
|
+
const concurrency = Number.isFinite(rawConcurrency) && rawConcurrency > 0 ? Math.floor(rawConcurrency) : 3;
|
|
2259
|
+
let changed = false;
|
|
2260
|
+
|
|
2261
|
+
if (!seed.pending || seed.pending.length === 0) return;
|
|
2262
|
+
|
|
2263
|
+
let cursor = 0;
|
|
2264
|
+
const runOne = async (item) => {
|
|
2265
|
+
const prev = seed.usageMap.get(item.key);
|
|
2266
|
+
const prevSnapshot = prev && prev.snapshot && typeof prev.snapshot === 'object' ? prev.snapshot : null;
|
|
2267
|
+
const live = item.row.tool === 'claude'
|
|
2268
|
+
? readClaudeRateLimitsLiveExperimental(item.row.path)
|
|
2269
|
+
: await readCodexRateLimitsLive(item.row.path);
|
|
2270
|
+
const checkedAt = now();
|
|
2271
|
+
const entry = live && live.ok
|
|
2272
|
+
? {
|
|
2273
|
+
ok: true,
|
|
2274
|
+
checked_at: checkedAt,
|
|
2275
|
+
snapshot: live.snapshot,
|
|
2276
|
+
error: '',
|
|
2277
|
+
source: 'live',
|
|
2278
|
+
}
|
|
2279
|
+
: prevSnapshot
|
|
2280
|
+
? {
|
|
2281
|
+
ok: true,
|
|
2282
|
+
checked_at: prev && prev.checked_at ? prev.checked_at : checkedAt,
|
|
2283
|
+
snapshot: prevSnapshot,
|
|
2284
|
+
error: live && live.error ? oneLineText(live.error) : msg('读取失败', 'Read failed'),
|
|
2285
|
+
source: 'stale_error',
|
|
2286
|
+
}
|
|
2287
|
+
: {
|
|
2288
|
+
ok: false,
|
|
2289
|
+
checked_at: checkedAt,
|
|
2290
|
+
snapshot: null,
|
|
2291
|
+
error: live && live.error ? oneLineText(live.error) : msg('读取失败', 'Read failed'),
|
|
2292
|
+
source: 'live',
|
|
2293
|
+
};
|
|
2294
|
+
|
|
2295
|
+
seed.usageMap.set(item.key, entry);
|
|
2296
|
+
seed.store.entries[item.key] = {
|
|
2297
|
+
ok: entry.ok,
|
|
2298
|
+
checked_at: entry.checked_at,
|
|
2299
|
+
snapshot: entry.snapshot,
|
|
2300
|
+
error: entry.error,
|
|
2301
|
+
};
|
|
2302
|
+
changed = true;
|
|
2303
|
+
if (onUpdate) onUpdate(item.key, entry);
|
|
2304
|
+
};
|
|
2305
|
+
|
|
2306
|
+
const workerCount = Math.max(1, Math.min(concurrency, seed.pending.length));
|
|
2307
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
2308
|
+
while (true) {
|
|
2309
|
+
const idx = cursor;
|
|
2310
|
+
cursor += 1;
|
|
2311
|
+
if (idx >= seed.pending.length) return;
|
|
2312
|
+
await runOne(seed.pending[idx]);
|
|
2313
|
+
}
|
|
2314
|
+
});
|
|
2315
|
+
await Promise.all(workers);
|
|
2316
|
+
|
|
2317
|
+
if (changed) saveUsageCache(seed.store);
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
async function collectCodexUsage(rows, options = {}) {
|
|
2321
|
+
const ttlMs = Number.isFinite(Number(options.ttlMs)) ? Number(options.ttlMs) : usageCacheTtlMs();
|
|
2322
|
+
const seed = seedCodexUsageMap(rows, ttlMs, { refreshClaude: Boolean(options.refreshClaude) });
|
|
2323
|
+
await refreshCodexUsageMap(seed, options);
|
|
2324
|
+
return seed.usageMap;
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
async function usageProfiles(toolOption) {
|
|
2328
|
+
const reg = loadRegistry();
|
|
2329
|
+
const rows = resolveByTool(reg, toolOption);
|
|
2330
|
+
sortProfilesForPicker(rows);
|
|
2331
|
+
|
|
2332
|
+
if (rows.length === 0) {
|
|
2333
|
+
process.stdout.write(`${msg('暂无 profile', 'No profiles found')}\n`);
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
process.stdout.write(`🚀 用量总览(实时+缓存)\n`);
|
|
2338
|
+
process.stdout.write(`口径: 已用/剩余 · 缓存 ${Math.floor(usageCacheTtlMs() / 60000)}m\n`);
|
|
2339
|
+
const usageMap = await collectCodexUsage(rows, { ttlMs: 0, refreshClaude: true });
|
|
2340
|
+
const displayRows = buildProfileRows(rows, usageMap, { includeUnready: false });
|
|
2341
|
+
const hidden = rows.length - displayRows.length;
|
|
2342
|
+
if (hidden > 0) {
|
|
2343
|
+
process.stdout.write(`已隐藏异常账号 ${hidden} 个(仅展示可用账号)\n`);
|
|
2344
|
+
}
|
|
2345
|
+
process.stdout.write(`${padRight('标', 4)}${padRight('工具', 7)}${padRight('账号', 34)}${padRight('工作空间', 16)}${padRight('5h(进度+已用/余)', 20)}${padRight('周(进度+已用/余)', 20)}说明\n`);
|
|
2346
|
+
|
|
2347
|
+
for (const row of displayRows) {
|
|
2348
|
+
process.stdout.write(`${padRight(row.flag, 4)}${padRight(row.tool, 7)}${padRight(row.account, 34)}${padRight(row.workspace, 16)}${padRight(row.usageFiveMeter, 20)}${padRight(row.usageWeekMeter, 20)}${row.explain}\n`);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
function runProfile(identifier, tool, rawArgs) {
|
|
2353
|
+
if (rawArgs.length === 0) {
|
|
2354
|
+
throw new CliError(msg('缺少原始参数,请使用 -- 分隔', 'Missing raw args, use -- separator'));
|
|
2355
|
+
}
|
|
2356
|
+
const reg = loadRegistry();
|
|
2357
|
+
const profile = findSingleProfileOrThrow(reg, identifier, tool);
|
|
2358
|
+
spawnInProfile(profile, rawArgs);
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
function removeProfile(identifier, tool, options = {}) {
|
|
2362
|
+
const silent = Boolean(options.silent);
|
|
2363
|
+
const reg = loadRegistry();
|
|
2364
|
+
const profile = findSingleProfileOrThrow(reg, identifier, tool);
|
|
2365
|
+
|
|
2366
|
+
withLocks([lockPath(tool, profile.profile_id)], () => {
|
|
2367
|
+
reg.profiles[tool] = reg.profiles[tool].filter((p) => p.profile_id !== profile.profile_id);
|
|
2368
|
+
saveRegistry(reg);
|
|
2369
|
+
fs.rmSync(profile.path, { recursive: true, force: true });
|
|
2370
|
+
|
|
2371
|
+
const cur = loadCurrent();
|
|
2372
|
+
if (cur.active_profile_id === profile.profile_id) {
|
|
2373
|
+
cur.active_profile_id = null;
|
|
2374
|
+
cur.active_name = null;
|
|
2375
|
+
cur.active_tool = null;
|
|
2376
|
+
cur.last_switched_at = now();
|
|
2377
|
+
saveCurrent(cur);
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
if (!silent) {
|
|
2381
|
+
process.stdout.write(`${msg('已删除 profile', 'Removed profile')}: ${tool}/${profile.slug}\n`);
|
|
2382
|
+
}
|
|
2383
|
+
});
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
function renameProfile(identifier, newName, tool, options = {}) {
|
|
2387
|
+
const silent = Boolean(options.silent);
|
|
2388
|
+
validateName(newName);
|
|
2389
|
+
const reg = loadRegistry();
|
|
2390
|
+
const profile = findSingleProfileOrThrow(reg, identifier, tool);
|
|
2391
|
+
|
|
2392
|
+
const exists = reg.profiles[tool].some((p) => p.profile_id !== profile.profile_id && (p.slug === newName || (p.aliases || []).includes(newName)));
|
|
2393
|
+
if (exists) {
|
|
2394
|
+
throw new CliError(msg(`名称已存在: ${newName}`, `Name already exists: ${newName}`));
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
withLocks([lockPath(tool, profile.profile_id)], () => {
|
|
2398
|
+
profile.aliases = normalizeAliases([...(profile.aliases || []), profile.slug]);
|
|
2399
|
+
profile.slug = newName;
|
|
2400
|
+
saveRegistry(reg);
|
|
2401
|
+
|
|
2402
|
+
const cur = loadCurrent();
|
|
2403
|
+
if (cur.active_profile_id === profile.profile_id) {
|
|
2404
|
+
cur.active_name = profile.slug;
|
|
2405
|
+
cur.last_switched_at = now();
|
|
2406
|
+
saveCurrent(cur);
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
if (!silent) {
|
|
2410
|
+
process.stdout.write(`${msg('已重命名', 'Renamed')}: ${tool}/${identifier} -> ${newName}\n`);
|
|
2411
|
+
}
|
|
2412
|
+
});
|
|
2413
|
+
return profile;
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
function reportDuplicates(map, labelZh, labelEn) {
|
|
2417
|
+
const groups = [];
|
|
2418
|
+
for (const [k, list] of map.entries()) {
|
|
2419
|
+
if (!k || list.length < 2) continue;
|
|
2420
|
+
groups.push({ key: k, list });
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
if (groups.length === 0) {
|
|
2424
|
+
process.stdout.write(`${msg(labelZh, labelEn)}: ${msg('无', 'none')}\n`);
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
process.stdout.write(`${msg(labelZh, labelEn)}: ${msg('发现', 'found')} ${groups.length}\n`);
|
|
2429
|
+
for (const g of groups) {
|
|
2430
|
+
process.stdout.write(` - ${g.key}\n`);
|
|
2431
|
+
process.stdout.write(` profiles: ${g.list.join(', ')}\n`);
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
function doctorProfiles(toolOption) {
|
|
2436
|
+
const reg = loadRegistry();
|
|
2437
|
+
const rows = resolveByTool(reg, toolOption);
|
|
2438
|
+
if (rows.length === 0) {
|
|
2439
|
+
process.stdout.write(`${msg('暂无 profile', 'No profiles found')}\n`);
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
const byIdentity = new Map();
|
|
2444
|
+
const byRefresh = new Map();
|
|
2445
|
+
const byAccess = new Map();
|
|
2446
|
+
const byId = new Map();
|
|
2447
|
+
|
|
2448
|
+
for (const p of rows) {
|
|
2449
|
+
const key = `${p.tool}|${p.account_key}|${p.space_key}`;
|
|
2450
|
+
const id = `${p.tool}/${p.slug}`;
|
|
2451
|
+
|
|
2452
|
+
if (!byIdentity.has(key)) byIdentity.set(key, []);
|
|
2453
|
+
byIdentity.get(key).push(id);
|
|
2454
|
+
|
|
2455
|
+
if (p.tool === 'codex') {
|
|
2456
|
+
const hashes = codexAuthInfo(p.path).token_hashes;
|
|
2457
|
+
if (hashes.refresh) {
|
|
2458
|
+
if (!byRefresh.has(hashes.refresh)) byRefresh.set(hashes.refresh, []);
|
|
2459
|
+
byRefresh.get(hashes.refresh).push(id);
|
|
2460
|
+
}
|
|
2461
|
+
if (hashes.access) {
|
|
2462
|
+
if (!byAccess.has(hashes.access)) byAccess.set(hashes.access, []);
|
|
2463
|
+
byAccess.get(hashes.access).push(id);
|
|
2464
|
+
}
|
|
2465
|
+
if (hashes.id) {
|
|
2466
|
+
if (!byId.has(hashes.id)) byId.set(hashes.id, []);
|
|
2467
|
+
byId.get(hashes.id).push(id);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
const countDup = (map) => {
|
|
2473
|
+
let count = 0;
|
|
2474
|
+
for (const list of map.values()) {
|
|
2475
|
+
if (Array.isArray(list) && list.length > 1) count += 1;
|
|
2476
|
+
}
|
|
2477
|
+
return count;
|
|
2478
|
+
};
|
|
2479
|
+
|
|
2480
|
+
const idDup = countDup(byIdentity);
|
|
2481
|
+
const refreshDup = countDup(byRefresh);
|
|
2482
|
+
const accessDup = countDup(byAccess);
|
|
2483
|
+
const idTokenDup = countDup(byId);
|
|
2484
|
+
const total = idDup + refreshDup + accessDup + idTokenDup;
|
|
2485
|
+
|
|
2486
|
+
if (total === 0) {
|
|
2487
|
+
process.stdout.write('✅ 未发现重复身份或重复 token\n');
|
|
2488
|
+
return;
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
process.stdout.write(`⚠️ 发现重复:身份 ${idDup} | refresh ${refreshDup} | access ${accessDup} | id_token ${idTokenDup}\n`);
|
|
2492
|
+
process.stdout.write('处理建议:先执行 cprofile ls,再用 cprofile rm <slug> --tool <tool> 删除多余账号\n');
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
function findIdentityMatch(reg, tool, accountKey, spaceKey) {
|
|
2496
|
+
return reg.profiles[tool].find((p) => p.account_key === accountKey && p.space_key === spaceKey) || null;
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
function makeProfileFromAuth(reg, tool, auth, aliases, seed = '') {
|
|
2500
|
+
const profileId = makeProfileId(`${tool}|${auth.account_key}|${auth.space_key}|${seed}`);
|
|
2501
|
+
const pPath = profilePathById(tool, profileId);
|
|
2502
|
+
ensureDir(pPath, 0o700);
|
|
2503
|
+
|
|
2504
|
+
const baseSlug = buildSlug(tool, auth.account_email, auth.space_name, auth.account_key, auth.space_key);
|
|
2505
|
+
const slug = ensureUniqueSlug(tool, baseSlug, allProfiles(reg));
|
|
2506
|
+
return {
|
|
2507
|
+
profile_id: profileId,
|
|
2508
|
+
slug,
|
|
2509
|
+
display_name: buildDisplayName(auth.account_email, auth.space_name),
|
|
2510
|
+
aliases: normalizeAliases(aliases || []),
|
|
2511
|
+
account_key: auth.account_key,
|
|
2512
|
+
space_key: auth.space_key,
|
|
2513
|
+
account_email: auth.account_email || '',
|
|
2514
|
+
space_name: auth.space_name || 'unknown',
|
|
2515
|
+
space_display: auth.space_display || auth.space_name || 'unknown',
|
|
2516
|
+
enterprise_space_name: auth.enterprise_space_name || '',
|
|
2517
|
+
enterprise_space_id: auth.enterprise_space_id || '',
|
|
2518
|
+
enterprise_space_display: auth.enterprise_space_display || '',
|
|
2519
|
+
org_candidates: Array.isArray(auth.org_candidates) ? auth.org_candidates : [],
|
|
2520
|
+
plan: auth.plan || '',
|
|
2521
|
+
status: auth.status || 'unknown',
|
|
2522
|
+
tool,
|
|
2523
|
+
path: pPath,
|
|
2524
|
+
created_at: now(),
|
|
2525
|
+
last_login_at: null,
|
|
2526
|
+
last_used_at: null,
|
|
2527
|
+
pinned: false,
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
function importLegacyCodexDirs(reg, cleanupCodexDash) {
|
|
2532
|
+
let changed = false;
|
|
2533
|
+
const summary = {
|
|
2534
|
+
imported: 0,
|
|
2535
|
+
merged: 0,
|
|
2536
|
+
skipped: 0,
|
|
2537
|
+
cleaned: 0,
|
|
2538
|
+
items: [],
|
|
2539
|
+
};
|
|
2540
|
+
|
|
2541
|
+
const entries = fs.readdirSync(HOME, { withFileTypes: true });
|
|
2542
|
+
const legacyDirs = entries
|
|
2543
|
+
.filter((e) => e.isDirectory() && /^\.codex-/.test(e.name))
|
|
2544
|
+
.map((e) => path.join(HOME, e.name))
|
|
2545
|
+
.sort();
|
|
2546
|
+
|
|
2547
|
+
for (const src of legacyDirs) {
|
|
2548
|
+
const alias = path.basename(src).replace(/^\./, '') || path.basename(src);
|
|
2549
|
+
const auth = codexAuthInfo(src);
|
|
2550
|
+
|
|
2551
|
+
if (auth.status === 'missing_auth') {
|
|
2552
|
+
summary.skipped += 1;
|
|
2553
|
+
summary.items.push(`${msg('跳过', 'skip')} ${src}: ${msg('缺少 auth.json', 'missing auth.json')}`);
|
|
2554
|
+
continue;
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
const matched = findIdentityMatch(reg, 'codex', auth.account_key, auth.space_key);
|
|
2558
|
+
if (matched) {
|
|
2559
|
+
const beforeAliases = JSON.stringify(matched.aliases || []);
|
|
2560
|
+
const beforeStatus = matched.status;
|
|
2561
|
+
const beforeSpace = matched.space_display;
|
|
2562
|
+
matched.aliases = normalizeAliases([...(matched.aliases || []), alias]);
|
|
2563
|
+
matched.status = auth.status || matched.status;
|
|
2564
|
+
matched.space_display = auth.space_display || matched.space_display;
|
|
2565
|
+
if (
|
|
2566
|
+
beforeAliases !== JSON.stringify(matched.aliases || []) ||
|
|
2567
|
+
beforeStatus !== matched.status ||
|
|
2568
|
+
beforeSpace !== matched.space_display
|
|
2569
|
+
) {
|
|
2570
|
+
changed = true;
|
|
2571
|
+
}
|
|
2572
|
+
summary.merged += 1;
|
|
2573
|
+
summary.items.push(`${msg('合并', 'merged')} ${src} -> codex/${matched.slug}`);
|
|
2574
|
+
|
|
2575
|
+
if (cleanupCodexDash) {
|
|
2576
|
+
fs.rmSync(src, { recursive: true, force: true });
|
|
2577
|
+
summary.cleaned += 1;
|
|
2578
|
+
}
|
|
2579
|
+
continue;
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
const profile = makeProfileFromAuth(reg, 'codex', auth, [alias], src);
|
|
2583
|
+
try {
|
|
2584
|
+
fs.cpSync(src, profile.path, { recursive: true, force: true });
|
|
2585
|
+
} catch (err) {
|
|
2586
|
+
fs.rmSync(profile.path, { recursive: true, force: true });
|
|
2587
|
+
summary.skipped += 1;
|
|
2588
|
+
summary.items.push(`${msg('跳过', 'skip')} ${src}: ${msg('复制失败', 'copy failed')} - ${err && err.message ? err.message : String(err)}`);
|
|
2589
|
+
continue;
|
|
2590
|
+
}
|
|
2591
|
+
chmodSafe(profile.path, 0o700);
|
|
2592
|
+
chmodSafe(path.join(profile.path, 'auth.json'), 0o600);
|
|
2593
|
+
|
|
2594
|
+
reg.profiles.codex.push(profile);
|
|
2595
|
+
writeJsonAtomic(path.join(profile.path, 'meta.json'), profile, 0o600);
|
|
2596
|
+
changed = true;
|
|
2597
|
+
summary.imported += 1;
|
|
2598
|
+
summary.items.push(`${msg('导入', 'imported')} ${src} -> codex/${profile.slug}`);
|
|
2599
|
+
|
|
2600
|
+
if (cleanupCodexDash) {
|
|
2601
|
+
fs.rmSync(src, { recursive: true, force: true });
|
|
2602
|
+
summary.cleaned += 1;
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
return { changed, summary };
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
function importLegacyClaudeDir(reg) {
|
|
2610
|
+
let changed = false;
|
|
2611
|
+
const summary = {
|
|
2612
|
+
imported: 0,
|
|
2613
|
+
merged: 0,
|
|
2614
|
+
skipped: 0,
|
|
2615
|
+
items: [],
|
|
2616
|
+
};
|
|
2617
|
+
|
|
2618
|
+
const src = path.join(HOME, '.claude');
|
|
2619
|
+
if (!fs.existsSync(src) || !fs.statSync(src).isDirectory()) {
|
|
2620
|
+
return { changed, summary };
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
const g = claudeGlobalInfo();
|
|
2624
|
+
if (!g.email) {
|
|
2625
|
+
summary.skipped += 1;
|
|
2626
|
+
summary.items.push(`${msg('跳过', 'skip')} ${src}: ${msg('缺少 ~/.claude.json 账号信息', 'missing account info in ~/.claude.json')}`);
|
|
2627
|
+
return { changed, summary };
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
const auth = {
|
|
2631
|
+
account_email: g.email,
|
|
2632
|
+
account_key: g.accountKey,
|
|
2633
|
+
space_name: g.orgName || 'unknown',
|
|
2634
|
+
space_key: g.spaceKey,
|
|
2635
|
+
space_display: g.spaceDisplay || g.orgName || 'unknown',
|
|
2636
|
+
plan: '',
|
|
2637
|
+
status: 'ready',
|
|
2638
|
+
};
|
|
2639
|
+
|
|
2640
|
+
const matched = findIdentityMatch(reg, 'claude', auth.account_key, auth.space_key);
|
|
2641
|
+
if (matched) {
|
|
2642
|
+
const beforeAliases = JSON.stringify(matched.aliases || []);
|
|
2643
|
+
const beforeStatus = matched.status;
|
|
2644
|
+
const beforeSpace = matched.space_display;
|
|
2645
|
+
matched.aliases = normalizeAliases([...(matched.aliases || []), 'claude_default']);
|
|
2646
|
+
matched.status = auth.status || matched.status;
|
|
2647
|
+
matched.space_display = auth.space_display || matched.space_display;
|
|
2648
|
+
if (
|
|
2649
|
+
beforeAliases !== JSON.stringify(matched.aliases || []) ||
|
|
2650
|
+
beforeStatus !== matched.status ||
|
|
2651
|
+
beforeSpace !== matched.space_display
|
|
2652
|
+
) {
|
|
2653
|
+
changed = true;
|
|
2654
|
+
}
|
|
2655
|
+
summary.merged += 1;
|
|
2656
|
+
summary.items.push(`${msg('合并', 'merged')} ${src} -> claude/${matched.slug}`);
|
|
2657
|
+
return { changed, summary };
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
const profile = makeProfileFromAuth(reg, 'claude', auth, ['claude_default'], src);
|
|
2661
|
+
try {
|
|
2662
|
+
fs.cpSync(src, profile.path, { recursive: true, force: true });
|
|
2663
|
+
} catch (err) {
|
|
2664
|
+
fs.rmSync(profile.path, { recursive: true, force: true });
|
|
2665
|
+
summary.skipped += 1;
|
|
2666
|
+
summary.items.push(`${msg('跳过', 'skip')} ${src}: ${msg('复制失败', 'copy failed')} - ${err && err.message ? err.message : String(err)}`);
|
|
2667
|
+
return { changed, summary };
|
|
2668
|
+
}
|
|
2669
|
+
chmodSafe(profile.path, 0o700);
|
|
2670
|
+
chmodSafe(path.join(profile.path, 'account.json'), 0o600);
|
|
2671
|
+
|
|
2672
|
+
reg.profiles.claude.push(profile);
|
|
2673
|
+
writeJsonAtomic(path.join(profile.path, 'meta.json'), profile, 0o600);
|
|
2674
|
+
changed = true;
|
|
2675
|
+
summary.imported += 1;
|
|
2676
|
+
summary.items.push(`${msg('导入', 'imported')} ${src} -> claude/${profile.slug}`);
|
|
2677
|
+
return { changed, summary };
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
function importLegacy(toolOption, cleanupCodexDash = false) {
|
|
2681
|
+
const reg = loadRegistry();
|
|
2682
|
+
let changed = false;
|
|
2683
|
+
const lines = [];
|
|
2684
|
+
|
|
2685
|
+
let codexImported = 0;
|
|
2686
|
+
let codexMerged = 0;
|
|
2687
|
+
let codexSkipped = 0;
|
|
2688
|
+
let codexCleaned = 0;
|
|
2689
|
+
let claudeImported = 0;
|
|
2690
|
+
let claudeMerged = 0;
|
|
2691
|
+
let claudeSkipped = 0;
|
|
2692
|
+
|
|
2693
|
+
if (toolOption === 'all' || toolOption === 'codex') {
|
|
2694
|
+
const codexRes = importLegacyCodexDirs(reg, cleanupCodexDash);
|
|
2695
|
+
changed = changed || codexRes.changed;
|
|
2696
|
+
codexImported += codexRes.summary.imported;
|
|
2697
|
+
codexMerged += codexRes.summary.merged;
|
|
2698
|
+
codexSkipped += codexRes.summary.skipped;
|
|
2699
|
+
codexCleaned += codexRes.summary.cleaned;
|
|
2700
|
+
lines.push(...codexRes.summary.items);
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
if (toolOption === 'all' || toolOption === 'claude') {
|
|
2704
|
+
const claudeRes = importLegacyClaudeDir(reg);
|
|
2705
|
+
changed = changed || claudeRes.changed;
|
|
2706
|
+
claudeImported += claudeRes.summary.imported;
|
|
2707
|
+
claudeMerged += claudeRes.summary.merged;
|
|
2708
|
+
claudeSkipped += claudeRes.summary.skipped;
|
|
2709
|
+
lines.push(...claudeRes.summary.items);
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
if (changed) saveRegistry(reg);
|
|
2713
|
+
|
|
2714
|
+
process.stdout.write(`${msg('导入完成', 'Import completed')}\n`);
|
|
2715
|
+
process.stdout.write(` codex: ${msg('新增', 'imported')}=${codexImported}, ${msg('合并', 'merged')}=${codexMerged}, ${msg('跳过', 'skipped')}=${codexSkipped}, ${msg('清理旧目录', 'cleaned')}=${codexCleaned}\n`);
|
|
2716
|
+
process.stdout.write(` claude: ${msg('新增', 'imported')}=${claudeImported}, ${msg('合并', 'merged')}=${claudeMerged}, ${msg('跳过', 'skipped')}=${claudeSkipped}\n`);
|
|
2717
|
+
if (lines.length > 0) {
|
|
2718
|
+
process.stdout.write(`${msg('明细', 'Details')}:\n`);
|
|
2719
|
+
for (const line of lines) process.stdout.write(` - ${line}\n`);
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
function suggestName(tool, account, space) {
|
|
2724
|
+
validateTool(tool, false);
|
|
2725
|
+
const accountEmail = account || '';
|
|
2726
|
+
const spaceName = space || 'unknown';
|
|
2727
|
+
const accountKey = `email:${shortHash(accountEmail || 'unknown', 8)}`;
|
|
2728
|
+
const spaceKey = `space:${shortHash(spaceName || 'unknown', 8)}`;
|
|
2729
|
+
const slug = buildSlug(tool, accountEmail, spaceName, accountKey, spaceKey);
|
|
2730
|
+
const display = buildDisplayName(accountEmail, spaceName);
|
|
2731
|
+
|
|
2732
|
+
process.stdout.write(`tool=${tool}\n`);
|
|
2733
|
+
process.stdout.write(`slug=${slug}\n`);
|
|
2734
|
+
process.stdout.write(`display_name=${display}\n`);
|
|
2735
|
+
process.stdout.write(`account_key=${accountKey}\n`);
|
|
2736
|
+
process.stdout.write(`space_key=${spaceKey}\n`);
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
function printGuide(topic) {
|
|
2740
|
+
ensureBaseLayout();
|
|
2741
|
+
const map = {
|
|
2742
|
+
readme: 'README.zh-CN.md',
|
|
2743
|
+
quickstart: 'QUICKSTART.zh-CN.md',
|
|
2744
|
+
troubleshooting: 'TROUBLESHOOTING.zh-CN.md',
|
|
2745
|
+
naming: 'NAMING.zh-CN.md',
|
|
2746
|
+
};
|
|
2747
|
+
|
|
2748
|
+
if (!topic) {
|
|
2749
|
+
process.stdout.write(`${msg('本地文档目录', 'Local docs')}: ${DOCS_DIR}\n`);
|
|
2750
|
+
process.stdout.write(`${msg('可用主题', 'Topics')}: readme, quickstart, troubleshooting, naming\n`);
|
|
2751
|
+
process.stdout.write(`${msg('示例', 'Examples')}:\n`);
|
|
2752
|
+
process.stdout.write(` cprofile guide quickstart\n`);
|
|
2753
|
+
process.stdout.write(` cprofile guide troubleshooting\n`);
|
|
2754
|
+
process.stdout.write(` cprofile guide naming\n`);
|
|
2755
|
+
return;
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
const key = String(topic).toLowerCase();
|
|
2759
|
+
const file = map[key];
|
|
2760
|
+
if (!file) {
|
|
2761
|
+
throw new CliError(msg(`未知 guide 主题: ${topic}`, `Unknown guide topic: ${topic}`));
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
const p = path.join(DOCS_DIR, file);
|
|
2765
|
+
if (!fs.existsSync(p)) {
|
|
2766
|
+
throw new CliError(msg(`文档不存在: ${p}`, `Doc not found: ${p}`));
|
|
2767
|
+
}
|
|
2768
|
+
process.stdout.write(fs.readFileSync(p, 'utf8'));
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
function getLegacyCandidates() {
|
|
2772
|
+
const entries = fs.readdirSync(HOME, { withFileTypes: true });
|
|
2773
|
+
const codexDirs = entries
|
|
2774
|
+
.filter((e) => e.isDirectory() && /^\.codex-/.test(e.name))
|
|
2775
|
+
.map((e) => path.join(HOME, e.name));
|
|
2776
|
+
const claudeDir = path.join(HOME, '.claude');
|
|
2777
|
+
const hasClaude = fs.existsSync(claudeDir) && fs.statSync(claudeDir).isDirectory();
|
|
2778
|
+
return { codexDirs, hasClaude };
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
function findProfileById(reg, tool, profileId) {
|
|
2782
|
+
return reg.profiles[tool].find((p) => p.profile_id === profileId) || null;
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2785
|
+
function existsSlug(reg, tool, slug, excludeId = null) {
|
|
2786
|
+
return reg.profiles[tool].some((p) => p.profile_id !== excludeId && (p.slug === slug || (p.aliases || []).includes(slug)));
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2789
|
+
function randomAlias(tool) {
|
|
2790
|
+
return `new_${tool}_${shortHash(`${Date.now()}|${Math.random()}`, 6)}`;
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
async function askCustomSlug(tool, currentSlug, profileId) {
|
|
2794
|
+
while (true) {
|
|
2795
|
+
const next = await askLine(`输入自定义短名(2-32,字母数字下划线)[当前 ${currentSlug}]:`);
|
|
2796
|
+
if (!next) return null;
|
|
2797
|
+
try {
|
|
2798
|
+
validateName(next);
|
|
2799
|
+
} catch {
|
|
2800
|
+
finalizeWithToast('短名不合法,请重试', { tone: 'warning' });
|
|
2801
|
+
continue;
|
|
2802
|
+
}
|
|
2803
|
+
const reg = loadRegistry();
|
|
2804
|
+
if (existsSlug(reg, tool, next, profileId)) {
|
|
2805
|
+
finalizeWithToast('短名已存在,请换一个', { tone: 'warning' });
|
|
2806
|
+
continue;
|
|
2807
|
+
}
|
|
2808
|
+
return next;
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
async function maybeFillEnterpriseSpace(profile) {
|
|
2813
|
+
if (!isInteractiveTTY()) return profile;
|
|
2814
|
+
const reg = loadRegistry();
|
|
2815
|
+
const latest = findProfileById(reg, profile.tool, profile.profile_id) || profile;
|
|
2816
|
+
const currentEnterprise = String(latest.enterprise_space_display || latest.enterprise_space_name || '').trim();
|
|
2817
|
+
if (currentEnterprise) return latest;
|
|
2818
|
+
|
|
2819
|
+
const choice = await chooseFromMenu('企业空间(可跳过)', [
|
|
2820
|
+
{ value: 'fill', label: '填写企业空间', desc: '例如:0208 / 0209' },
|
|
2821
|
+
{ value: 'skip', label: '跳过', desc: '稍后可再补充' },
|
|
2822
|
+
]);
|
|
2823
|
+
if (choice !== 'fill') return latest;
|
|
2824
|
+
|
|
2825
|
+
while (true) {
|
|
2826
|
+
const input = oneLineText(await askLine('请输入企业空间名称(留空即跳过):'), 64);
|
|
2827
|
+
if (!input) return latest;
|
|
2828
|
+
|
|
2829
|
+
const reg2 = loadRegistry();
|
|
2830
|
+
const target = findProfileById(reg2, profile.tool, profile.profile_id);
|
|
2831
|
+
if (!target) return latest;
|
|
2832
|
+
target.enterprise_space_name = input;
|
|
2833
|
+
target.enterprise_space_id = target.enterprise_space_id || input;
|
|
2834
|
+
target.enterprise_space_display = input;
|
|
2835
|
+
saveRegistry(reg2);
|
|
2836
|
+
writeJsonAtomic(path.join(target.path, 'meta.json'), target, 0o600);
|
|
2837
|
+
finalizeWithToast(`企业空间已记录: ${input}`);
|
|
2838
|
+
return target;
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
async function runAddWizard() {
|
|
2843
|
+
const tool = await chooseFromMenu('添加账号 · 选择工具', [
|
|
2844
|
+
{ value: 'codex', label: 'codex', desc: 'OpenAI Codex' },
|
|
2845
|
+
{ value: 'claude', label: 'claude', desc: 'Anthropic Claude' },
|
|
2846
|
+
]);
|
|
2847
|
+
if (!tool) return false;
|
|
2848
|
+
|
|
2849
|
+
const created = addProfile(randomAlias(tool), tool, { silent: true });
|
|
2850
|
+
finalizeWithToast(`已创建配置: ${tool}/${created.slug}`);
|
|
2851
|
+
finalizeWithToast(`将打开 ${tool} 登录,请按提示完成授权`);
|
|
2852
|
+
try {
|
|
2853
|
+
loginProfile(created.slug, tool, { silent: true });
|
|
2854
|
+
} catch (err) {
|
|
2855
|
+
try {
|
|
2856
|
+
removeProfile(created.slug, tool, { silent: true });
|
|
2857
|
+
} catch {
|
|
2858
|
+
// ignore cleanup failures
|
|
2859
|
+
}
|
|
2860
|
+
throw err;
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
let reg = loadRegistry();
|
|
2864
|
+
let profile = findProfileById(reg, tool, created.profile_id);
|
|
2865
|
+
if (!profile) {
|
|
2866
|
+
throw new CliError('登录完成后未找到 profile,请重试');
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
profile = await maybeFillEnterpriseSpace(profile);
|
|
2870
|
+
reg = loadRegistry();
|
|
2871
|
+
profile = findProfileById(reg, tool, created.profile_id) || profile;
|
|
2872
|
+
|
|
2873
|
+
const naming = await chooseFromMenu('命名确认', [
|
|
2874
|
+
{ value: 'recommended', label: `使用推荐短名:${profile.slug}`, desc: '推荐:邮箱+空间+哈希,冲突更少' },
|
|
2875
|
+
{ value: 'custom', label: '自定义短名', desc: '自己输入易记名称(如 work_main)' },
|
|
2876
|
+
]);
|
|
2877
|
+
if (naming === 'custom') {
|
|
2878
|
+
const custom = await askCustomSlug(tool, profile.slug, profile.profile_id);
|
|
2879
|
+
if (custom && custom !== profile.slug) {
|
|
2880
|
+
renameProfile(profile.slug, custom, tool, { silent: true });
|
|
2881
|
+
reg = loadRegistry();
|
|
2882
|
+
profile = findProfileById(reg, tool, profile.profile_id) || profile;
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
setActiveProfile(reg, profile);
|
|
2887
|
+
finalizeWithToast(`已切换并启动: ${tool}/${profile.slug}`);
|
|
2888
|
+
spawnInProfile(profile, []);
|
|
2889
|
+
return true;
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
async function runAdvancedToolsMenu() {
|
|
2893
|
+
while (true) {
|
|
2894
|
+
const choice = await chooseFromMenu('高级工具', [
|
|
2895
|
+
{ value: 'import', label: '导入旧目录', desc: '导入 ~/.codex-* 与 ~/.claude' },
|
|
2896
|
+
{ value: 'migrate', label: '执行迁移', desc: '检查并规范化 registry 到 V2' },
|
|
2897
|
+
{ value: 'list', label: '查看全部明细', desc: '显示完整账号明细(高级输出)' },
|
|
2898
|
+
]);
|
|
2899
|
+
if (!choice) return false;
|
|
2900
|
+
|
|
2901
|
+
if (choice === 'import') {
|
|
2902
|
+
const scope = await chooseFromMenu('选择导入范围', [
|
|
2903
|
+
{ value: 'all', label: '全部(Codex + Claude)', desc: '' },
|
|
2904
|
+
{ value: 'codex', label: '仅 Codex', desc: '' },
|
|
2905
|
+
{ value: 'claude', label: '仅 Claude', desc: '' },
|
|
2906
|
+
]);
|
|
2907
|
+
if (!scope) continue;
|
|
2908
|
+
const cleanup = await chooseFromMenu('是否清理 ~/.codex-* 旧目录', [
|
|
2909
|
+
{ value: 'yes', label: '是,清理', desc: '仅清理 ~/.codex-*,不会删除 ~/.claude' },
|
|
2910
|
+
{ value: 'no', label: '否,保留', desc: '' },
|
|
2911
|
+
]);
|
|
2912
|
+
if (!cleanup) continue;
|
|
2913
|
+
importLegacy(scope, cleanup === 'yes');
|
|
2914
|
+
return true;
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
if (choice === 'migrate') {
|
|
2918
|
+
loadRegistry({ forceMigrate: true, printSummary: true });
|
|
2919
|
+
return true;
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
if (choice === 'list') {
|
|
2923
|
+
listProfiles('all');
|
|
2924
|
+
return true;
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
async function runQuickImportFlow() {
|
|
2930
|
+
const scope = await chooseFromMenu('选择导入范围', [
|
|
2931
|
+
{ value: 'all', label: '全部(Codex + Claude)', desc: '' },
|
|
2932
|
+
{ value: 'codex', label: '仅 Codex', desc: '' },
|
|
2933
|
+
{ value: 'claude', label: '仅 Claude', desc: '' },
|
|
2934
|
+
]);
|
|
2935
|
+
if (!scope) return false;
|
|
2936
|
+
|
|
2937
|
+
const cleanup = await chooseFromMenu('是否清理 ~/.codex-* 旧目录', [
|
|
2938
|
+
{ value: 'yes', label: '是,清理', desc: '仅清理 ~/.codex-*,不会删除 ~/.claude' },
|
|
2939
|
+
{ value: 'no', label: '否,保留', desc: '' },
|
|
2940
|
+
]);
|
|
2941
|
+
if (!cleanup) return false;
|
|
2942
|
+
importLegacy(scope, cleanup === 'yes');
|
|
2943
|
+
return true;
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
async function waitBackToSwitch() {
|
|
2947
|
+
await askLine('按回车返回账号列表...');
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
async function runHome() {
|
|
2951
|
+
ensureBaseLayout();
|
|
2952
|
+
if (!isInteractiveTTY()) {
|
|
2953
|
+
printQuickHelp();
|
|
2954
|
+
return;
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2957
|
+
let reg = loadRegistry();
|
|
2958
|
+
if (allProfiles(reg).length === 0) {
|
|
2959
|
+
const legacy = getLegacyCandidates();
|
|
2960
|
+
if (legacy.codexDirs.length > 0 || legacy.hasClaude) {
|
|
2961
|
+
const importChoice = await chooseFromMenu('检测到旧账号目录,是否现在导入?', [
|
|
2962
|
+
{ value: 'import', label: '立即导入', desc: '推荐:自动识别并复用旧登录态' },
|
|
2963
|
+
{ value: 'skip', label: '暂不导入', desc: '稍后可在“高级工具”里导入' },
|
|
2964
|
+
]);
|
|
2965
|
+
if (importChoice === 'import') {
|
|
2966
|
+
const cleanup = await chooseFromMenu('导入后是否清理 ~/.codex-*', [
|
|
2967
|
+
{ value: 'yes', label: '是,清理', desc: '推荐:减少旧目录混淆' },
|
|
2968
|
+
{ value: 'no', label: '否,保留', desc: '保留旧目录做备份' },
|
|
2969
|
+
]);
|
|
2970
|
+
if (cleanup) {
|
|
2971
|
+
importLegacy('all', cleanup === 'yes');
|
|
2972
|
+
reg = loadRegistry();
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
while (true) {
|
|
2979
|
+
reg = loadRegistry();
|
|
2980
|
+
const pool = resolveByTool(reg, 'all');
|
|
2981
|
+
if (pool.length === 0) {
|
|
2982
|
+
const bootstrap = await chooseFromMenu('还没有可用账号', [
|
|
2983
|
+
{ value: 'add', label: '添加账号', desc: '推荐:立即登录一个账号' },
|
|
2984
|
+
{ value: 'import', label: '导入旧目录', desc: '从 ~/.codex-* / ~/.claude 导入' },
|
|
2985
|
+
{ value: 'exit', label: '退出', desc: '' },
|
|
2986
|
+
]);
|
|
2987
|
+
if (!bootstrap || bootstrap === 'exit') {
|
|
2988
|
+
throw new CliError('已取消', 130);
|
|
2989
|
+
}
|
|
2990
|
+
if (bootstrap === 'add') {
|
|
2991
|
+
const done = await runAddWizard();
|
|
2992
|
+
if (done) return;
|
|
2993
|
+
continue;
|
|
2994
|
+
}
|
|
2995
|
+
if (bootstrap === 'import') {
|
|
2996
|
+
await runQuickImportFlow();
|
|
2997
|
+
continue;
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
const picked = await pickProfileInteractive(pool, {
|
|
3002
|
+
title: '快速切换',
|
|
3003
|
+
allowSearch: true,
|
|
3004
|
+
showUsage: true,
|
|
3005
|
+
allowActions: true,
|
|
3006
|
+
});
|
|
3007
|
+
|
|
3008
|
+
if (picked && picked.kind === 'profile') {
|
|
3009
|
+
const latest = findProfileById(reg, picked.profile.tool, picked.profile.profile_id) || picked.profile;
|
|
3010
|
+
setActiveProfile(reg, latest);
|
|
3011
|
+
finalizeWithToast(`已切换并启动: ${latest.tool}/${latest.slug}`);
|
|
3012
|
+
spawnInProfile(latest, []);
|
|
3013
|
+
return;
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
if (picked && picked.kind === 'action' && picked.action === 'add') {
|
|
3017
|
+
const done = await runAddWizard();
|
|
3018
|
+
if (done) return;
|
|
3019
|
+
continue;
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
if (picked && picked.kind === 'action' && picked.action === 'doctor') {
|
|
3023
|
+
doctorProfiles('all');
|
|
3024
|
+
await waitBackToSwitch();
|
|
3025
|
+
continue;
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
if (picked && picked.kind === 'action' && picked.action === 'import') {
|
|
3029
|
+
await runQuickImportFlow();
|
|
3030
|
+
continue;
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
function migrateNow() {
|
|
3035
|
+
loadRegistry({ forceMigrate: true, printSummary: true });
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
function parseImportArgs(args) {
|
|
3039
|
+
let cleanup = false;
|
|
3040
|
+
const rest = [];
|
|
3041
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
3042
|
+
const cur = args[i];
|
|
3043
|
+
if (cur === '--cleanup') {
|
|
3044
|
+
cleanup = true;
|
|
3045
|
+
continue;
|
|
3046
|
+
}
|
|
3047
|
+
if (cur === '--no-cleanup') {
|
|
3048
|
+
cleanup = false;
|
|
3049
|
+
continue;
|
|
3050
|
+
}
|
|
3051
|
+
rest.push(cur);
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
const parsed = parseToolOption(rest, 'all', true);
|
|
3055
|
+
if (parsed.rest.length !== 0) {
|
|
3056
|
+
throw new CliError(msg('用法: cprofile import [--tool codex|claude|all] [--cleanup]', 'Usage: cprofile import [--tool codex|claude|all] [--cleanup]'));
|
|
3057
|
+
}
|
|
3058
|
+
return { tool: parsed.tool, cleanup };
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
function parseNameSuggestArgs(args) {
|
|
3062
|
+
let tool = null;
|
|
3063
|
+
let account = '';
|
|
3064
|
+
let space = '';
|
|
3065
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
3066
|
+
const a = args[i];
|
|
3067
|
+
if (a === '--tool') {
|
|
3068
|
+
tool = args[i + 1] || null;
|
|
3069
|
+
i += 1;
|
|
3070
|
+
continue;
|
|
3071
|
+
}
|
|
3072
|
+
if (a === '--account') {
|
|
3073
|
+
account = args[i + 1] || '';
|
|
3074
|
+
i += 1;
|
|
3075
|
+
continue;
|
|
3076
|
+
}
|
|
3077
|
+
if (a === '--space') {
|
|
3078
|
+
space = args[i + 1] || '';
|
|
3079
|
+
i += 1;
|
|
3080
|
+
continue;
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
if (!tool) {
|
|
3084
|
+
throw new CliError(msg('缺少 --tool', 'Missing --tool'));
|
|
3085
|
+
}
|
|
3086
|
+
return { tool: validateTool(tool, false), account, space };
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
async function main() {
|
|
3090
|
+
ensureBaseLayout();
|
|
3091
|
+
loadRegistry();
|
|
3092
|
+
|
|
3093
|
+
const argv = process.argv.slice(2);
|
|
3094
|
+
const cmd = argv[0];
|
|
3095
|
+
|
|
3096
|
+
if (!cmd) {
|
|
3097
|
+
await runHome();
|
|
3098
|
+
return;
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
if (cmd === '-h' || cmd === '--help' || cmd === 'help') {
|
|
3102
|
+
printQuickHelp();
|
|
3103
|
+
return;
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
if (cmd === '--advanced-help') {
|
|
3107
|
+
printAdvancedHelp();
|
|
3108
|
+
return;
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
if (cmd === 'home') {
|
|
3112
|
+
await runHome();
|
|
3113
|
+
return;
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
if (cmd === 'guide') {
|
|
3117
|
+
printGuide(argv[1] || null);
|
|
3118
|
+
return;
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
if (cmd === 'migrate') {
|
|
3122
|
+
migrateNow();
|
|
3123
|
+
return;
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
if (cmd === 'import') {
|
|
3127
|
+
const { tool, cleanup } = parseImportArgs(argv.slice(1));
|
|
3128
|
+
importLegacy(tool, cleanup);
|
|
3129
|
+
return;
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
if (cmd === 'name') {
|
|
3133
|
+
const sub = argv[1];
|
|
3134
|
+
if (sub !== 'suggest') {
|
|
3135
|
+
throw new CliError(msg('用法: cprofile name suggest --tool <tool> --account <email> --space <space>', 'Usage: cprofile name suggest --tool <tool> --account <email> --space <space>'));
|
|
3136
|
+
}
|
|
3137
|
+
const { tool, account, space } = parseNameSuggestArgs(argv.slice(2));
|
|
3138
|
+
suggestName(tool, account, space);
|
|
3139
|
+
return;
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
if (cmd === 'add') {
|
|
3143
|
+
const { tool, rest } = parseToolOption(argv.slice(1), 'codex', false);
|
|
3144
|
+
const name = rest[0];
|
|
3145
|
+
if (!name || rest.length !== 1) {
|
|
3146
|
+
throw new CliError(msg('用法: cprofile add <name> [--tool codex|claude]', 'Usage: cprofile add <name> [--tool codex|claude]'));
|
|
3147
|
+
}
|
|
3148
|
+
addProfile(name, tool);
|
|
3149
|
+
return;
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
if (cmd === 'login') {
|
|
3153
|
+
const { tool, rest } = parseToolOption(argv.slice(1), 'codex', false);
|
|
3154
|
+
const id = rest[0];
|
|
3155
|
+
if (!id || rest.length !== 1) {
|
|
3156
|
+
throw new CliError(msg('用法: cprofile login <id> [--tool codex|claude]', 'Usage: cprofile login <id> [--tool codex|claude]'));
|
|
3157
|
+
}
|
|
3158
|
+
loginProfile(id, tool);
|
|
3159
|
+
return;
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
if (cmd === 'use') {
|
|
3163
|
+
const { tool, rest } = parseToolOption(argv.slice(1), 'all', true);
|
|
3164
|
+
if (rest.length > 1) {
|
|
3165
|
+
throw new CliError(msg('用法: cprofile use [<id>] [--tool codex|claude|all]', 'Usage: cprofile use [<id>] [--tool codex|claude|all]'));
|
|
3166
|
+
}
|
|
3167
|
+
await useProfile(rest[0] || null, tool, 'env');
|
|
3168
|
+
return;
|
|
3169
|
+
}
|
|
3170
|
+
|
|
3171
|
+
if (cmd === 'env') {
|
|
3172
|
+
const sub = argv[1];
|
|
3173
|
+
if (sub !== 'use') {
|
|
3174
|
+
throw new CliError(msg('用法: cprofile env use [<id>] [--tool codex|claude|all]', 'Usage: cprofile env use [<id>] [--tool codex|claude|all]'));
|
|
3175
|
+
}
|
|
3176
|
+
const { tool, rest } = parseToolOption(argv.slice(2), 'all', true);
|
|
3177
|
+
if (rest.length > 1) {
|
|
3178
|
+
throw new CliError(msg('用法: cprofile env use [<id>] [--tool codex|claude|all]', 'Usage: cprofile env use [<id>] [--tool codex|claude|all]'));
|
|
3179
|
+
}
|
|
3180
|
+
await useProfile(rest[0] || null, tool, 'env');
|
|
3181
|
+
return;
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
if (cmd === 'ls') {
|
|
3185
|
+
const { tool, rest } = parseToolOption(argv.slice(1), 'all', true);
|
|
3186
|
+
if (rest.length !== 0) {
|
|
3187
|
+
throw new CliError(msg('用法: cprofile ls [--tool codex|claude|all]', 'Usage: cprofile ls [--tool codex|claude|all]'));
|
|
3188
|
+
}
|
|
3189
|
+
listProfiles(tool);
|
|
3190
|
+
return;
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
if (cmd === 'usage') {
|
|
3194
|
+
const { tool, rest } = parseToolOption(argv.slice(1), 'all', true);
|
|
3195
|
+
if (rest.length !== 0) {
|
|
3196
|
+
throw new CliError(msg('用法: cprofile usage [--tool codex|claude|all]', 'Usage: cprofile usage [--tool codex|claude|all]'));
|
|
3197
|
+
}
|
|
3198
|
+
await usageProfiles(tool);
|
|
3199
|
+
return;
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
if (cmd === 'current') {
|
|
3203
|
+
if (argv.length !== 1) {
|
|
3204
|
+
throw new CliError(msg('用法: cprofile current', 'Usage: cprofile current'));
|
|
3205
|
+
}
|
|
3206
|
+
showCurrent();
|
|
3207
|
+
return;
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
if (cmd === 'run') {
|
|
3211
|
+
const { head, raw } = splitRawArgs(argv.slice(1));
|
|
3212
|
+
const { tool, rest } = parseToolOption(head, 'codex', false);
|
|
3213
|
+
const id = rest[0];
|
|
3214
|
+
if (!id || rest.length !== 1) {
|
|
3215
|
+
throw new CliError(msg('用法: cprofile run <id> [--tool codex|claude] -- <raw_args>', 'Usage: cprofile run <id> [--tool codex|claude] -- <raw_args>'));
|
|
3216
|
+
}
|
|
3217
|
+
runProfile(id, tool, raw);
|
|
3218
|
+
return;
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
if (cmd === 'rm') {
|
|
3222
|
+
const { tool, rest } = parseToolOption(argv.slice(1), 'codex', false);
|
|
3223
|
+
const id = rest[0];
|
|
3224
|
+
if (!id || rest.length !== 1) {
|
|
3225
|
+
throw new CliError(msg('用法: cprofile rm <id> [--tool codex|claude]', 'Usage: cprofile rm <id> [--tool codex|claude]'));
|
|
3226
|
+
}
|
|
3227
|
+
removeProfile(id, tool);
|
|
3228
|
+
return;
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
if (cmd === 'rename') {
|
|
3232
|
+
const { tool, rest } = parseToolOption(argv.slice(1), 'codex', false);
|
|
3233
|
+
const id = rest[0];
|
|
3234
|
+
const newSlug = rest[1];
|
|
3235
|
+
if (!id || !newSlug || rest.length !== 2) {
|
|
3236
|
+
throw new CliError(msg('用法: cprofile rename <id> <new_slug> [--tool codex|claude]', 'Usage: cprofile rename <id> <new_slug> [--tool codex|claude]'));
|
|
3237
|
+
}
|
|
3238
|
+
renameProfile(id, newSlug, tool);
|
|
3239
|
+
return;
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3242
|
+
if (cmd === 'doctor') {
|
|
3243
|
+
const { tool, rest } = parseToolOption(argv.slice(1), 'all', true);
|
|
3244
|
+
if (rest.length !== 0) {
|
|
3245
|
+
throw new CliError(msg('用法: cprofile doctor [--tool codex|claude|all]', 'Usage: cprofile doctor [--tool codex|claude|all]'));
|
|
3246
|
+
}
|
|
3247
|
+
doctorProfiles(tool);
|
|
3248
|
+
return;
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
throw new CliError(msg(`未知命令: ${cmd},可执行 cprofile --advanced-help`, `Unknown command: ${cmd}. Run cprofile --advanced-help`));
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3254
|
+
main().catch((err) => {
|
|
3255
|
+
if (err instanceof CliError) {
|
|
3256
|
+
if (err.code === 130) {
|
|
3257
|
+
clearFrame(process.stderr);
|
|
3258
|
+
showCursor(process.stderr);
|
|
3259
|
+
process.stderr.write('已取消\n');
|
|
3260
|
+
process.exit(130);
|
|
3261
|
+
}
|
|
3262
|
+
if (err.message) process.stderr.write(`cprofile: ${err.message}\n`);
|
|
3263
|
+
process.exit(err.code || 1);
|
|
3264
|
+
}
|
|
3265
|
+
process.stderr.write(`cprofile: ${err && err.message ? err.message : String(err)}\n`);
|
|
3266
|
+
process.exit(1);
|
|
3267
|
+
});
|