@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 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 |
@@ -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
- console.error(`error: ${err && err.message ? err.message : err}`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carjms/codexswitch",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Multi-account manager for OpenAI Codex CLI with quota-based rotation (inspired by KarpelesLab/teamclaude)",
5
5
  "license": "MIT",
6
6
  "repository": {
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) => (win && typeof win.pct === 'number' ? `${Math.round(win.pct)}%` : '-');
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(`[codexswitch] running codex as "${name}"`);
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
- `[codexswitch] exec as "${name}"${attempt > 0 ? ` (attempt ${attempt + 1}${useResume ? ', resuming session' : ''})` : ''}`
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
- `[codexswitch] "${name}" hit a usage/rate limit (paused until ${fmtDate(until)}) — rotating${useResume ? ' and resuming the session' : ''}`
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
- `[codexswitch] "${name}" has a revoked/invalid login — disabled. Fix it with: codexswitch login ${name}`
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('[codexswitch] all accounts are rate-limited, over threshold, or disabled');
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
- `[codexswitch] "${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`
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(HELP);
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
- throw new Error(`unknown command "${cmd}" (see "codexswitch help")`);
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 line = (r) => r.map((c, i) => c.padEnd(widths[i])).join(' ').trimEnd();
95
- return [line(all[0]), line(widths.map((w) => '-'.repeat(w))), ...all.slice(1).map(line)].join('\n');
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 = {