Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,6 @@ Remove-Item "$env:LOCALAPPDATA\codex-auth\bin\codex-auth-auto.exe" -Force -Error
| `codex-auth remove` | Remove accounts with interactive multi-select |
| `codex-auth status` | Show auto-switch, service, and usage status |

> `codex-auth add` is still accepted as a deprecated alias for `codex-auth login`.

### Import

| Command | Description |
Expand Down
1 change: 0 additions & 1 deletion docs/implement.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ This document describes how `codex-auth` stores accounts, synchronizes auth file

- If `registry.json` is empty and `~/.codex/auth.json` exists, the tool auto-imports it into `accounts/<account file key>.auth.json`.
- If the registry is empty and there is no `auth.json`, `list` shows no accounts; use `codex-auth login` or `codex-auth import`.
- `codex-auth add` is still accepted as a deprecated alias for `codex-auth login`.

## Registry Compatibility

Expand Down
486 changes: 391 additions & 95 deletions src/cli.zig

Large diffs are not rendered by default.

122 changes: 2 additions & 120 deletions src/format.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ const std = @import("std");
const builtin = @import("builtin");
const display_rows = @import("display_rows.zig");
const registry = @import("registry.zig");
const cli = @import("cli.zig");
const io_util = @import("io_util.zig");
const timefmt = @import("timefmt.zig");
const c = @cImport({
Expand All @@ -24,14 +23,8 @@ fn planDisplay(rec: *const registry.AccountRecord, missing: []const u8) []const
return missing;
}

pub fn printAccounts(allocator: std.mem.Allocator, reg: *registry.Registry, fmt: cli.OutputFormat) !void {
switch (fmt) {
.table => try printAccountsTable(reg),
.json => try printAccountsJson(reg),
.csv => try printAccountsCsv(reg),
.compact => try printAccountsCompact(reg),
}
_ = allocator;
pub fn printAccounts(reg: *registry.Registry) !void {
try printAccountsTable(reg);
}

fn printAccountsTable(reg: *registry.Registry) !void {
Expand Down Expand Up @@ -167,82 +160,6 @@ fn printAccountsTable(reg: *registry.Registry) !void {
try out.flush();
}

fn printAccountsJson(reg: *registry.Registry) !void {
var stdout: io_util.Stdout = undefined;
stdout.init();
const out = stdout.out();
const dump = RegistryOut{
.schema_version = reg.schema_version,
.active_account_key = reg.active_account_key,
.auto_switch = reg.auto_switch,
.api = reg.api,
.accounts = reg.accounts.items,
};
try std.json.Stringify.value(dump, .{ .whitespace = .indent_2 }, out);
try out.writeAll("\n");
try out.flush();
}

fn printAccountsCsv(reg: *registry.Registry) !void {
var stdout: io_util.Stdout = undefined;
stdout.init();
const out = stdout.out();
try out.writeAll("active,account_key,chatgpt_account_id,chatgpt_user_id,email,plan,limit_5h,limit_weekly,last_used\n");
for (reg.accounts.items) |rec| {
const active = if (reg.active_account_key) |k| std.mem.eql(u8, k, rec.account_key) else false;
const email = rec.email;
const account_key = rec.account_key;
const chatgpt_account_id = rec.chatgpt_account_id;
const chatgpt_user_id = rec.chatgpt_user_id;
const plan = planDisplay(&rec, "");
const rate_5h = resolveRateWindow(rec.last_usage, 300, true);
const rate_week = resolveRateWindow(rec.last_usage, 10080, false);
const rate_5h_str = try formatRateLimitStatusAlloc(rate_5h);
defer std.heap.page_allocator.free(rate_5h_str);
const rate_week_str = try formatRateLimitStatusAlloc(rate_week);
defer std.heap.page_allocator.free(rate_week_str);
const last = if (rec.last_used_at) |t| try std.fmt.allocPrint(std.heap.page_allocator, "{d}", .{t}) else "";
defer if (rec.last_used_at != null) std.heap.page_allocator.free(last) else {};
try out.print(
"{s},{s},{s},{s},{s},{s},{s},{s},{s}\n",
.{ if (active) "1" else "0", account_key, chatgpt_account_id, chatgpt_user_id, email, plan, rate_5h_str, rate_week_str, last },
);
}
try out.flush();
}

fn printAccountsCompact(reg: *registry.Registry) !void {
var stdout: io_util.Stdout = undefined;
stdout.init();
const out = stdout.out();
for (reg.accounts.items) |rec| {
const active = if (reg.active_account_key) |k| std.mem.eql(u8, k, rec.account_key) else false;
const email = rec.email;
const plan = planDisplay(&rec, "-");
const rate_5h = resolveRateWindow(rec.last_usage, 300, true);
const rate_week = resolveRateWindow(rec.last_usage, 10080, false);
const rate_5h_str = try formatRateLimitStatusAlloc(rate_5h);
defer std.heap.page_allocator.free(rate_5h_str);
const rate_week_str = try formatRateLimitStatusAlloc(rate_week);
defer std.heap.page_allocator.free(rate_week_str);
const last = if (rec.last_used_at) |t| try formatTimestampAlloc(t) else "-";
defer if (rec.last_used_at != null) std.heap.page_allocator.free(last) else {};
try out.print(
"{s}{s} ({s}) 5h:{s} week:{s} last:{s}\n",
.{ if (active) "* " else " ", email, plan, rate_5h_str, rate_week_str, last },
);
}
try out.flush();
}

const RegistryOut = struct {
schema_version: u32,
active_account_key: ?[]const u8,
auto_switch: registry.AutoSwitchConfig,
api: registry.ApiConfig,
accounts: []const registry.AccountRecord,
};

fn resolveRateWindow(usage: ?registry.RateLimitSnapshot, minutes: i64, fallback_primary: bool) ?registry.RateLimitWindow {
if (usage == null) return null;
if (usage.?.primary) |p| {
Expand All @@ -254,20 +171,6 @@ fn resolveRateWindow(usage: ?registry.RateLimitSnapshot, minutes: i64, fallback_
return if (fallback_primary) usage.?.primary else usage.?.secondary;
}

fn formatRateLimitStatusAlloc(window: ?registry.RateLimitWindow) ![]u8 {
if (window == null) return try std.fmt.allocPrint(std.heap.page_allocator, "-", .{});
if (window.?.resets_at == null) return try std.fmt.allocPrint(std.heap.page_allocator, "-", .{});
const now = std.time.timestamp();
const reset_at = window.?.resets_at.?;
if (now >= reset_at) {
return try std.fmt.allocPrint(std.heap.page_allocator, "100%", .{});
}
const remaining = remainingPercent(window.?.used_percent);
const time_str = try formatResetTimeAlloc(reset_at, now);
defer std.heap.page_allocator.free(time_str);
return std.fmt.allocPrint(std.heap.page_allocator, "{d}% {s}", .{ remaining, time_str });
}

const ResetParts = struct {
time: []u8,
date: []u8,
Expand Down Expand Up @@ -667,27 +570,6 @@ fn truncateAlloc(value: []const u8, max_len: usize) ![]u8 {
return std.fmt.allocPrint(std.heap.page_allocator, "{s}.", .{value[0 .. max_len - 1]});
}

fn formatTimestampAlloc(ts: i64) ![]u8 {
if (ts < 0) return try std.fmt.allocPrint(std.heap.page_allocator, "-", .{});
var tm: c.struct_tm = undefined;
if (!localtimeCompat(ts, &tm)) {
return try std.fmt.allocPrint(std.heap.page_allocator, "-", .{});
}

const year = @as(u32, @intCast(tm.tm_year + 1900));
const month = @as(u32, @intCast(tm.tm_mon + 1));
const day = @as(u32, @intCast(tm.tm_mday));
const hour = @as(u32, @intCast(tm.tm_hour));
const min = @as(u32, @intCast(tm.tm_min));
const sec = @as(u32, @intCast(tm.tm_sec));

return std.fmt.allocPrint(
std.heap.page_allocator,
"{d:0>4}-{d:0>2}-{d:0>2} {d:0>2}:{d:0>2}:{d:0>2}",
.{ year, month, day, hour, min, sec },
);
}

test "printTableRow handles long cells without underflow" {
var buffer: [256]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buffer);
Expand Down
58 changes: 38 additions & 20 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ const skip_service_reconcile_env = "CODEX_AUTH_SKIP_SERVICE_RECONCILE";
pub fn main() !void {
var exit_code: u8 = 0;
runMain() catch |err| {
if (isHandledCliError(err)) {
if (err == error.InvalidCliUsage) {
exit_code = 2;
} else if (isHandledCliError(err)) {
exit_code = 1;
} else {
return err;
Expand All @@ -27,31 +29,47 @@ fn runMain() !void {
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);

var cmd = try cli.parseArgs(allocator, args);
defer cli.freeCommand(allocator, &cmd);
var parsed = try cli.parseArgs(allocator, args);
defer cli.freeParseResult(allocator, &parsed);

const codex_home = try registry.resolveCodexHome(allocator);
defer allocator.free(codex_home);
const cmd = switch (parsed) {
.command => |command| command,
.usage_error => |usage_err| {
try cli.printUsageError(&usage_err);
return error.InvalidCliUsage;
},
};

const needs_codex_home = switch (cmd) {
.version => false,
.help => |topic| topic == .top_level,
else => true,
};
const codex_home = if (needs_codex_home) try registry.resolveCodexHome(allocator) else null;
defer if (codex_home) |path| allocator.free(path);

switch (cmd) {
.version => try cli.printVersion(),
.help => try handleHelp(allocator, codex_home),
.status => try auto.printStatus(allocator, codex_home),
.help => |topic| switch (topic) {
.top_level => try handleTopLevelHelp(allocator, codex_home.?),
else => try cli.printCommandHelp(topic),
},
.status => try auto.printStatus(allocator, codex_home.?),
.daemon => |opts| switch (opts.mode) {
.watch => try auto.runDaemon(allocator, codex_home),
.once => try auto.runDaemonOnce(allocator, codex_home),
.watch => try auto.runDaemon(allocator, codex_home.?),
.once => try auto.runDaemonOnce(allocator, codex_home.?),
},
.config => |opts| try handleConfig(allocator, codex_home, opts),
.list => |opts| try handleList(allocator, codex_home, opts),
.login => |opts| try handleLogin(allocator, codex_home, opts),
.import_auth => |opts| try handleImport(allocator, codex_home, opts),
.switch_account => |opts| try handleSwitch(allocator, codex_home, opts),
.remove_account => |opts| try handleRemove(allocator, codex_home, opts),
.clean => |_| try handleClean(allocator, codex_home),
.config => |opts| try handleConfig(allocator, codex_home.?, opts),
.list => |opts| try handleList(allocator, codex_home.?, opts),
.login => |opts| try handleLogin(allocator, codex_home.?, opts),
.import_auth => |opts| try handleImport(allocator, codex_home.?, opts),
.switch_account => |opts| try handleSwitch(allocator, codex_home.?, opts),
.remove_account => |opts| try handleRemove(allocator, codex_home.?, opts),
.clean => |_| try handleClean(allocator, codex_home.?),
}

if (shouldReconcileManagedService(cmd)) {
try auto.reconcileManagedService(allocator, codex_home);
try auto.reconcileManagedService(allocator, codex_home.?);
}
}

Expand Down Expand Up @@ -175,11 +193,11 @@ fn handleList(allocator: std.mem.Allocator, codex_home: []const u8, opts: cli.Li
}
}
try maybeRefreshForegroundUsage(allocator, codex_home, &reg, .list);
try format.printAccounts(allocator, &reg, .table);
try format.printAccounts(&reg);
}

fn handleLogin(allocator: std.mem.Allocator, codex_home: []const u8, opts: cli.LoginOptions) !void {
cli.warnDeprecatedLoginAlias(opts);
_ = opts;
try cli.runCodexLogin(allocator);
const auth_path = try registry.activeAuthPath(allocator, codex_home);
defer allocator.free(auth_path);
Expand Down Expand Up @@ -475,7 +493,7 @@ fn handleRemove(allocator: std.mem.Allocator, codex_home: []const u8, opts: cli.
try cli.printRemoveSummary(removed_labels.items);
}

fn handleHelp(allocator: std.mem.Allocator, codex_home: []const u8) !void {
fn handleTopLevelHelp(allocator: std.mem.Allocator, codex_home: []const u8) !void {
const help_cfg = loadHelpConfig(allocator, codex_home);
try cli.printHelp(&help_cfg.auto_switch, &help_cfg.api);
}
Expand Down
Loading
Loading