@carjms/codexswitch 0.3.2 → 0.3.4
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/README.ko.md +1 -0
- package/README.md +1 -0
- package/bin/codex-switch.js +2 -1
- package/package.json +1 -1
- package/src/cli.js +44 -26
- package/src/ui.js +35 -0
- package/src/util.js +5 -3
package/README.ko.md
CHANGED
|
@@ -217,6 +217,7 @@ cxs list # 계정별 5h/week 사용량 % 확인
|
|
|
217
217
|
| `cxs export <파일>` | 전체 계정·설정 백업 (⚠️ 토큰 포함 — 비밀번호처럼 취급) |
|
|
218
218
|
| `cxs restore <파일>` | 백업 파일에서 계정 복원 (다른 PC 이전용) |
|
|
219
219
|
| `cxs completion <bash\|zsh>` | 셸 자동완성 스크립트 출력 |
|
|
220
|
+
| `cxs <그 외 명령>` | codex로 그대로 전달 — `cxs resume`, `cxs goal ...`, `cxs apply` 등 codex의 모든 명령을 관리 계정으로 바로 사용 가능 |
|
|
220
221
|
| `cxs remove <이름>` | 계정 삭제 |
|
|
221
222
|
| `cxs rename <옛이름> <새이름>` | 계정 이름 변경 |
|
|
222
223
|
| `cxs disable / enable <이름>` | 로테이션에서 임시 제외 / 복귀 |
|
package/README.md
CHANGED
|
@@ -219,6 +219,7 @@ cxs list # per-account 5h/week usage columns
|
|
|
219
219
|
| `cxs export <file>` | Back up all accounts + settings (⚠️ contains tokens — treat like a password) |
|
|
220
220
|
| `cxs restore <file>` | Restore accounts from a backup (for moving machines) |
|
|
221
221
|
| `cxs completion <bash\|zsh>` | Print a shell completion script |
|
|
222
|
+
| `cxs <anything else>` | Forwarded to codex under the managed account — `cxs resume`, `cxs goal ...`, `cxs apply` all work like their codex counterparts |
|
|
222
223
|
| `cxs remove <name>` | Delete an account |
|
|
223
224
|
| `cxs rename <old> <new>` | Rename an account |
|
|
224
225
|
| `cxs disable / enable <name>` | Temporarily exclude from / restore to rotation |
|
package/bin/codex-switch.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
require('../src/cli.js').main(process.argv.slice(2)).then(
|
|
5
5
|
(code) => process.exit(code || 0),
|
|
6
6
|
(err) => {
|
|
7
|
-
|
|
7
|
+
const ui = require('../src/ui.js');
|
|
8
|
+
console.error(ui.fail(`${ui.red('error:')} ${err && err.message ? err.message : err}`));
|
|
8
9
|
process.exit(1);
|
|
9
10
|
}
|
|
10
11
|
);
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ const path = require('path');
|
|
|
6
6
|
const { readJSONSafe, writeJSONAtomic, authInfo, ensureDir, fmtDate, fmtRemaining, table } = require('./util.js');
|
|
7
7
|
const store = require('./store.js');
|
|
8
8
|
const runner = require('./runner.js');
|
|
9
|
+
const ui = require('./ui.js');
|
|
9
10
|
|
|
10
11
|
const HELP = `codexswitch — multi-account manager for the OpenAI Codex CLI
|
|
11
12
|
|
|
@@ -48,6 +49,10 @@ Maintenance
|
|
|
48
49
|
completion <bash|zsh> print a shell completion script
|
|
49
50
|
help show this help
|
|
50
51
|
|
|
52
|
+
Anything else is forwarded to codex under the managed account, so commands
|
|
53
|
+
like "codexswitch resume", "codexswitch goal ..." or "codexswitch apply"
|
|
54
|
+
work exactly like their codex counterparts.
|
|
55
|
+
|
|
51
56
|
Environment
|
|
52
57
|
CODEX_SWITCH_HOME data dir (default ~/.codex-switch)
|
|
53
58
|
CODEX_HOME codex config dir codexswitch manages (default ~/.codex)
|
|
@@ -84,7 +89,7 @@ function storeAccount(name, auth) {
|
|
|
84
89
|
delete meta.accounts[name].disabled;
|
|
85
90
|
delete meta.accounts[name].limitedUntil;
|
|
86
91
|
store.saveMeta(meta);
|
|
87
|
-
out(`${existed ? 'updated' : 'added'} account "${name}"${info.email ? ` (${info.email}, ${info.plan || 'unknown plan'})` : ''}`);
|
|
92
|
+
out(ui.ok(`${existed ? 'updated' : 'added'} account ${ui.bold(`"${name}"`)}${info.email ? ui.dim(` (${info.email}, ${info.plan || 'unknown plan'})`) : ''}`));
|
|
88
93
|
return name;
|
|
89
94
|
}
|
|
90
95
|
|
|
@@ -107,7 +112,7 @@ function cmdLogin(args) {
|
|
|
107
112
|
store.ensureDirs();
|
|
108
113
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'codexswitch-login-'));
|
|
109
114
|
try {
|
|
110
|
-
out('opening codex login in an isolated profile (your current account is untouched)...');
|
|
115
|
+
out(ui.info('opening codex login in an isolated profile (your current account is untouched)...'));
|
|
111
116
|
const r = runner.spawnCodexSync(['login'], {
|
|
112
117
|
env: { ...process.env, CODEX_HOME: tmp },
|
|
113
118
|
stdio: 'inherit',
|
|
@@ -136,30 +141,34 @@ function cmdList() {
|
|
|
136
141
|
}
|
|
137
142
|
const now = Date.now();
|
|
138
143
|
const meta = store.loadMeta();
|
|
139
|
-
const pct = (win) =>
|
|
144
|
+
const pct = (win, threshold) => {
|
|
145
|
+
if (!win || typeof win.pct !== 'number') return ui.dim('-');
|
|
146
|
+
const v = `${Math.round(win.pct)}%`;
|
|
147
|
+
return win.pct >= threshold ? ui.red(v) : win.pct >= 70 ? ui.yellow(v) : ui.green(v);
|
|
148
|
+
};
|
|
140
149
|
const rows = accounts.map((a) => {
|
|
141
150
|
const over = store.overThreshold(a, meta, now);
|
|
142
151
|
const status = a.disabled
|
|
143
|
-
? 'disabled'
|
|
152
|
+
? ui.red('disabled')
|
|
144
153
|
: a.limitedUntil && a.limitedUntil > now
|
|
145
|
-
? `limited ${fmtRemaining(a.limitedUntil)}`
|
|
154
|
+
? ui.yellow(`limited ${fmtRemaining(a.limitedUntil)}`)
|
|
146
155
|
: over
|
|
147
|
-
? `over-${over}`
|
|
148
|
-
: 'ok';
|
|
156
|
+
? ui.yellow(`over-${over}`)
|
|
157
|
+
: ui.green('ok');
|
|
149
158
|
return [
|
|
150
|
-
a.active ? '
|
|
151
|
-
a.name,
|
|
152
|
-
a.email,
|
|
159
|
+
a.active ? ui.green('●') : '',
|
|
160
|
+
a.active ? ui.bold(a.name) : a.name,
|
|
161
|
+
ui.dim(a.email || '-'),
|
|
153
162
|
a.plan,
|
|
154
163
|
a.priority,
|
|
155
164
|
status,
|
|
156
|
-
pct(a.usage && a.usage.p5h),
|
|
157
|
-
pct(a.usage && a.usage.weekly),
|
|
158
|
-
fmtDate(a.lastRefresh),
|
|
165
|
+
pct(a.usage && a.usage.p5h, meta.threshold5h),
|
|
166
|
+
pct(a.usage && a.usage.weekly, meta.thresholdWeekly),
|
|
167
|
+
ui.dim(fmtDate(a.lastRefresh)),
|
|
159
168
|
];
|
|
160
169
|
});
|
|
161
170
|
out(table(rows, ['', 'name', 'email', 'plan', 'prio', 'status', '5h', 'week', 'token refreshed']));
|
|
162
|
-
out(`(rotate threshold: 5h ${meta.threshold5h}% / weekly ${meta.thresholdWeekly}% — change with "codexswitch threshold")`);
|
|
171
|
+
out(ui.dim(`(rotate threshold: 5h ${meta.threshold5h}% / weekly ${meta.thresholdWeekly}% — change with "codexswitch threshold")`));
|
|
163
172
|
}
|
|
164
173
|
|
|
165
174
|
function cmdUse(args) {
|
|
@@ -175,7 +184,7 @@ function cmdUse(args) {
|
|
|
175
184
|
meta.accounts[name].lastUsed = Date.now();
|
|
176
185
|
store.saveMeta(meta);
|
|
177
186
|
const info = authInfo(auth);
|
|
178
|
-
out(`now using "${name}"${info.email ? ` (${info.email}, ${info.plan || 'unknown plan'})` : ''}`);
|
|
187
|
+
out(ui.ok(`now using ${ui.bold(`"${name}"`)}${info.email ? ui.dim(` (${info.email}, ${info.plan || 'unknown plan'})`) : ''}`));
|
|
179
188
|
}
|
|
180
189
|
|
|
181
190
|
function cmdCurrent() {
|
|
@@ -192,9 +201,9 @@ function cmdCurrent() {
|
|
|
192
201
|
(info.email && accounts.find((a) => a.email === info.email)) ||
|
|
193
202
|
accounts.find((a) => a.accountId === info.accountId);
|
|
194
203
|
const name = match ? match.name : '(not stored — run "codexswitch import")';
|
|
195
|
-
const note = meta.active && match && meta.active !== match.name ? ` (meta says "${meta.active}" — out of sync)` : '';
|
|
196
|
-
out(`active: ${name}${note}`);
|
|
197
|
-
out(` email: ${info.email || '-'}\n plan: ${info.plan || '-'}\n token refreshed: ${fmtDate(info.lastRefresh)}`);
|
|
204
|
+
const note = meta.active && match && meta.active !== match.name ? ui.yellow(` (meta says "${meta.active}" — out of sync)`) : '';
|
|
205
|
+
out(`active: ${ui.bold(name)}${note}`);
|
|
206
|
+
out(ui.dim(` email: ${info.email || '-'}\n plan: ${info.plan || '-'}\n token refreshed: ${fmtDate(info.lastRefresh)}`));
|
|
198
207
|
}
|
|
199
208
|
|
|
200
209
|
function cmdNext() {
|
|
@@ -468,7 +477,7 @@ async function cmdRun(args) {
|
|
|
468
477
|
} else if (meta.model && rest.length === 0) {
|
|
469
478
|
rest = ['-m', meta.model];
|
|
470
479
|
}
|
|
471
|
-
out(`
|
|
480
|
+
out(ui.info(`running codex as ${ui.bold(`"${name}"`)}`));
|
|
472
481
|
const startTs = Date.now();
|
|
473
482
|
const res = await runner.runCodex(name, rest);
|
|
474
483
|
runner.recordUsage(name, res.profile, startTs);
|
|
@@ -511,7 +520,7 @@ async function cmdExec(args) {
|
|
|
511
520
|
}
|
|
512
521
|
tried.push(name);
|
|
513
522
|
console.error(
|
|
514
|
-
`
|
|
523
|
+
ui.info(`exec as ${ui.bold(`"${name}"`)}${ui.dim(attempt > 0 ? ` (attempt ${attempt + 1}${useResume ? ', resuming session' : ''})` : '')}`)
|
|
515
524
|
);
|
|
516
525
|
// On rotation, continue the same session with the next account instead
|
|
517
526
|
// of restarting the whole prompt — the session files are shared.
|
|
@@ -523,6 +532,7 @@ async function cmdExec(args) {
|
|
|
523
532
|
runner.recordUsage(name, res.profile, startTs);
|
|
524
533
|
if (res.code === 0) {
|
|
525
534
|
store.clearLimited(name);
|
|
535
|
+
console.error(ui.ok(`done as ${ui.bold(`"${name}"`)}`));
|
|
526
536
|
warnIfOverThreshold(name);
|
|
527
537
|
return 0;
|
|
528
538
|
}
|
|
@@ -533,7 +543,7 @@ async function cmdExec(args) {
|
|
|
533
543
|
useResume = true;
|
|
534
544
|
}
|
|
535
545
|
console.error(
|
|
536
|
-
`
|
|
546
|
+
ui.warn(`"${name}" hit a usage/rate limit (paused until ${fmtDate(until)}) — rotating${useResume ? ' and resuming the session' : ''}`)
|
|
537
547
|
);
|
|
538
548
|
continue;
|
|
539
549
|
}
|
|
@@ -542,13 +552,13 @@ async function cmdExec(args) {
|
|
|
542
552
|
// rotation and keep the task going on the next one.
|
|
543
553
|
setFlag(name, { disabled: true });
|
|
544
554
|
console.error(
|
|
545
|
-
`
|
|
555
|
+
ui.fail(`"${name}" has a revoked/invalid login — disabled. Fix it with: ${ui.bold(`codexswitch login ${name}`)}`)
|
|
546
556
|
);
|
|
547
557
|
continue;
|
|
548
558
|
}
|
|
549
559
|
return res.code; // real failure, don't burn other accounts on it
|
|
550
560
|
}
|
|
551
|
-
console.error('
|
|
561
|
+
console.error(ui.fail('all accounts are rate-limited, over threshold, or disabled'));
|
|
552
562
|
return 2;
|
|
553
563
|
}
|
|
554
564
|
|
|
@@ -560,7 +570,7 @@ function warnIfOverThreshold(name) {
|
|
|
560
570
|
if (blocked) {
|
|
561
571
|
const pct = blocked === '5h' ? account.usage.p5h.pct : account.usage.weekly.pct;
|
|
562
572
|
console.error(
|
|
563
|
-
`
|
|
573
|
+
ui.warn(`"${name}" is at ${Math.round(pct)}% of its ${blocked} limit (threshold ${blocked === '5h' ? meta.threshold5h : meta.thresholdWeekly}%) — the next exec will rotate to another account`)
|
|
564
574
|
);
|
|
565
575
|
}
|
|
566
576
|
}
|
|
@@ -572,7 +582,12 @@ async function main(argv) {
|
|
|
572
582
|
case 'help':
|
|
573
583
|
case '--help':
|
|
574
584
|
case '-h':
|
|
575
|
-
out(
|
|
585
|
+
out(
|
|
586
|
+
HELP.replace(/^(codexswitch)(?= —)/, ui.bold('$1')).replace(
|
|
587
|
+
/^(Accounts|Running codex|Settings|Maintenance|Environment)$/gm,
|
|
588
|
+
(s) => ui.bold(ui.cyan(s))
|
|
589
|
+
)
|
|
590
|
+
);
|
|
576
591
|
return 0;
|
|
577
592
|
case 'login':
|
|
578
593
|
return cmdLogin(args), 0;
|
|
@@ -638,7 +653,10 @@ async function main(argv) {
|
|
|
638
653
|
case 'exec':
|
|
639
654
|
return cmdExec(args);
|
|
640
655
|
default:
|
|
641
|
-
|
|
656
|
+
// Forward everything else to codex under the managed account, so
|
|
657
|
+
// codexswitch is a drop-in replacement: "cxs goal", "cxs resume", ...
|
|
658
|
+
console.error(ui.info(ui.dim(`forwarding to codex: codex ${argv.join(' ')}`)));
|
|
659
|
+
return cmdRun(argv);
|
|
642
660
|
}
|
|
643
661
|
}
|
|
644
662
|
|
package/src/ui.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Tiny ANSI color layer (zero deps). Colors turn off automatically when the
|
|
4
|
+
// output is piped, or when NO_COLOR is set — so scripts and tests always see
|
|
5
|
+
// plain text (https://no-color.org).
|
|
6
|
+
const enabled =
|
|
7
|
+
process.env.NO_COLOR == null &&
|
|
8
|
+
process.env.FORCE_COLOR !== '0' &&
|
|
9
|
+
(Boolean(process.stdout.isTTY) || Boolean(process.env.FORCE_COLOR));
|
|
10
|
+
|
|
11
|
+
const wrap = (open, close) => (s) => (enabled ? `[${open}m${s}[${close}m` : String(s));
|
|
12
|
+
|
|
13
|
+
const green = wrap(32, 39);
|
|
14
|
+
const red = wrap(31, 39);
|
|
15
|
+
const yellow = wrap(33, 39);
|
|
16
|
+
const cyan = wrap(36, 39);
|
|
17
|
+
const dim = wrap(2, 22);
|
|
18
|
+
const bold = wrap(1, 22);
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
enabled,
|
|
22
|
+
green,
|
|
23
|
+
red,
|
|
24
|
+
yellow,
|
|
25
|
+
cyan,
|
|
26
|
+
dim,
|
|
27
|
+
bold,
|
|
28
|
+
// status line prefixes — one glyph per kind keeps progress scannable
|
|
29
|
+
ok: (s) => `${green('✓')} ${s}`,
|
|
30
|
+
info: (s) => `${cyan('›')} ${s}`,
|
|
31
|
+
warn: (s) => `${yellow('⚠')} ${s}`,
|
|
32
|
+
fail: (s) => `${red('✗')} ${s}`,
|
|
33
|
+
// strip ANSI codes (for width calculations)
|
|
34
|
+
visible: (s) => String(s).replace(/\[[0-9;]*m/g, ''),
|
|
35
|
+
};
|
package/src/util.js
CHANGED
|
@@ -89,10 +89,12 @@ function fmtRemaining(untilTs) {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
function table(rows, headers) {
|
|
92
|
+
const { visible, dim } = require('./ui.js');
|
|
92
93
|
const all = [headers, ...rows].map((r) => r.map((c) => String(c == null ? '-' : c)));
|
|
93
|
-
const widths = headers.map((_, i) => Math.max(...all.map((r) => r[i].length)));
|
|
94
|
-
const
|
|
95
|
-
|
|
94
|
+
const widths = headers.map((_, i) => Math.max(...all.map((r) => visible(r[i]).length)));
|
|
95
|
+
const pad = (c, w) => c + ' '.repeat(Math.max(0, w - visible(c).length));
|
|
96
|
+
const line = (r) => r.map((c, i) => pad(c, widths[i])).join(' ').trimEnd();
|
|
97
|
+
return [dim(line(all[0])), dim(line(widths.map((w) => '-'.repeat(w)))), ...all.slice(1).map(line)].join('\n');
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
module.exports = {
|