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
7 changes: 4 additions & 3 deletions docs/commands/reset.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,21 @@
## Usage

```shell
codex-auth reset <alias|email|display-number|query>
codex-auth reset <alias|email|display-number|query> [--credit <id>] [--json]
```

## Behavior

`codex-auth reset` redeems one reset credit for a single stored account.

- The account selector accepts the same alias, email, display-number, and query forms as `switch`, and must resolve to exactly one account. Two differences from `switch`: a flag-like selector (a word longer than one character starting with `-`) is rejected as an unknown flag, and a query matching several accounts fails instead of opening the interactive picker.
- The command picks the available credit (`status == "available"`) with the earliest `expires_at` from the stored `last_usage.reset_credit` snapshot. Credits without an expiry are used only when no dated credit is available.
- By default the command picks the available credit (`status == "available"`) with the earliest `expires_at` from the stored `last_usage.reset_credit` snapshot. Credits without an expiry are used only when no dated credit is available. `--credit <id>` (or `--credit=<id>`) instead redeems the available card whose `id` matches; an id that names no available card fails without a request.
- The chosen credit is redeemed through `POST wham/rate-limit-reset-credits/consume` with a freshly generated UUID v4 `redeem_request_id`. See [docs/api.md](../api.md) for the full request.
- On success it prints one compact line reporting that a credit was redeemed, how many are left, and the next card's expiry — or that none are left when the redeemed card was the last one. The backend answering `already_redeemed` is reported in the same spot and counts as success. The redeemed card is neither named nor dated in the line; `list --json` and the stored snapshot keep the card detail. Exact wording is covered by tests and follows the shared separator rules in [docs/windows.md](../windows.md).
- `--json` emits one machine-readable success document instead of the summary line: `schema_version`, `command: "reset"`, `redeemed` (the account object shared with `list --json`), `outcome` (`"reset"` or `"already_redeemed"`), `remaining_credits`, and `next_expires_at` (the stored ISO 8601 expiry of the soonest-expiring card still available, or `null`). `remaining_credits` and `next_expires_at` describe the stored card list after the consume. See [docs/json-api.md](../json-api.md).
- The remaining count is derived from the stored card list, so it can be lower than the `RESETS` cell of `codex-auth list`, which prefers the server's `available_count`. Server card lists may be capped; run `codex-auth list --api` to reconcile them.
- The registry file is not rewritten by this command. The consumed credit disappears from the account list on the next `codex-auth list --api` refresh; run that refresh when the backend state changed elsewhere.
- Exit code is `0` after a successful consume (including the backend reporting `already_redeemed`), `1` when the account has no available credits, the backend reports `no_credit` or `nothing_to_reset`, or the request fails, and `2` for a usage error such as a missing selector or an unknown flag.
- Exit code is `0` after a successful consume (including the backend reporting `already_redeemed`), `1` when the account has no available credits, `--credit` names no available card, the backend reports `no_credit` or `nothing_to_reset`, or the request fails, and `2` for a usage error such as a missing selector, a missing `--credit` value, or an unknown flag. With `--json`, failures are reported as JSON error documents on stdout (codes `account_not_found`, `ambiguous_query`, `no_reset_credit`, `reset_credit_not_found`, `nothing_to_reset`, `reset_failed`, `registry_error`, `usage`); see [docs/json-api.md](../json-api.md).

## Notes

Expand Down
29 changes: 27 additions & 2 deletions docs/json-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ codex-auth switch <query> --json
codex-auth - --json
codex-auth remove <selector> [<selector>...] --json
codex-auth remove --all --json
codex-auth reset <selector> [--credit <id>] --json
```

Interactive and live paths are not supported. `codex-auth - --json` emits the
Expand Down Expand Up @@ -188,6 +189,26 @@ accompany exit code 1 when the daemon restart fails.
}
```

### Reset

```json
{
"schema_version": 1,
"command": "reset",
"redeemed": {},
"outcome": "reset",
"remaining_credits": 1,
"next_expires_at": "2026-11-01T00:00:00Z"
}
```

`redeemed` is the account object of the account the credit was redeemed for.
`outcome` is `"reset"` or `"already_redeemed"`; both count as success.
`remaining_credits` and `next_expires_at` describe the stored reset-credit
card list after the consume: `next_expires_at` is the stored ISO 8601 expiry
of the soonest-expiring card still available, or `null` when no dated card
remains.

## Error Documents

```json
Expand Down Expand Up @@ -247,10 +268,14 @@ Resolution status values are `resolved`, `ambiguous`, and `not_found`.

| Error code | Meaning |
|------------|---------|
| `account_not_found` | Switch query has no match |
| `ambiguous_query` | Switch query has multiple matches |
| `account_not_found` | Switch query or reset selector has no match |
| `ambiguous_query` | Switch query or reset selector has multiple matches |
| `no_previous_account` | `codex-auth -` ran with no stored previous account |
| `selector_resolution_failed` | Remove selector resolution failed atomically |
| `no_reset_credit` | Account has no available reset credit, or the backend reports the credit spent |
| `reset_credit_not_found` | `reset --credit` names no available reset-credit card |
| `nothing_to_reset` | Backend reports the account's usage does not need a reset |
| `reset_failed` | The reset-credit consume request failed |
| `curl_unavailable` | Required API refresh cannot find curl |
| `registry_error` | State failed before mutation began |
| `state_uncertain` | Persistence failed after mutation began; run `list --json` before retrying |
Expand Down
56 changes: 48 additions & 8 deletions src/cli/commands/reset.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,65 @@ const types = @import("../types.zig");
const common = @import("common.zig");

pub fn parse(allocator: std.mem.Allocator, args: []const [:0]const u8) !types.ParseResult {
const json_requested = common.argsContainFlag(args, "--json");
if (args.len == 1 and common.isHelpFlag(std.mem.sliceTo(args[0], 0))) {
return .{ .command = .{ .help = .reset_credits } };
}

var selector: ?[]u8 = null;
for (args) |raw_arg| {
const arg = std.mem.sliceTo(raw_arg, 0);
defer if (selector) |value| allocator.free(value);
var credit: ?[]u8 = null;
defer if (credit) |value| allocator.free(value);
var json = false;
var i: usize = 0;
while (i < args.len) : (i += 1) {
const arg = std.mem.sliceTo(args[i], 0);
if (std.mem.eql(u8, arg, "--json")) {
if (json) {
return common.usageErrorResultWithJson(allocator, .reset_credits, true, "duplicate `--json` for `reset`.", .{});
}
json = true;
continue;
}
if (std.mem.eql(u8, arg, "--credit") or common.inlineFlagValue(arg, "--credit") != null) {
const inline_value = common.inlineFlagValue(arg, "--credit");
if (inline_value == null and i + 1 >= args.len) {
return common.usageErrorResultWithJson(allocator, .reset_credits, json_requested, "missing value for `--credit`.", .{});
}
if (credit != null) {
return common.usageErrorResultWithJson(allocator, .reset_credits, json_requested, "duplicate `--credit` for `reset`.", .{});
}
const value = inline_value orelse blk: {
// A flag-like next word is not a card id: it is the value the
// user forgot, e.g. `--credit --json`.
const next = std.mem.sliceTo(args[i + 1], 0);
if (next.len > 1 and next[0] == '-') {
return common.usageErrorResultWithJson(allocator, .reset_credits, json_requested, "missing value for `--credit`.", .{});
}
i += 1;
break :blk std.mem.sliceTo(args[i], 0);
};
credit = try allocator.dupe(u8, value);
continue;
}
if (arg.len > 1 and std.mem.startsWith(u8, arg, "-")) {
if (selector) |value| allocator.free(value);
return common.usageErrorResult(allocator, .reset_credits, "unknown flag `{s}` for `reset`.", .{arg});
return common.usageErrorResultWithJson(allocator, .reset_credits, json_requested, "unknown flag `{s}` for `reset`.", .{arg});
}
if (selector != null) {
allocator.free(selector.?);
return common.usageErrorResult(allocator, .reset_credits, "unexpected extra argument `{s}` for `reset`.", .{arg});
return common.usageErrorResultWithJson(allocator, .reset_credits, json_requested, "unexpected extra argument `{s}` for `reset`.", .{arg});
}
selector = try allocator.dupe(u8, arg);
}
if (selector == null) {
return common.usageErrorResult(allocator, .reset_credits, "`reset` requires an account selector (alias, email, display number, or partial query).", .{});
return common.usageErrorResultWithJson(allocator, .reset_credits, json_requested, "`reset` requires an account selector (alias, email, display number, or partial query).", .{});
}
return .{ .command = .{ .reset_credits = .{ .selector = selector.? } } };
const opts: types.ResetOptions = .{
.selector = selector.?,
.credit = credit,
.json = json,
};
// Ownership moved into `opts`; the defers above must not release it.
selector = null;
credit = null;
return .{ .command = .{ .reset_credits = opts } };
}
5 changes: 4 additions & 1 deletion src/cli/commands/root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ fn freeCommand(allocator: std.mem.Allocator, cmd: *types.Command) void {
common.freeOwnedStringList(allocator, opts.selectors);
allocator.free(opts.selectors);
},
.reset_credits => |opts| allocator.free(opts.selector),
.reset_credits => |opts| {
allocator.free(opts.selector);
if (opts.credit) |credit| allocator.free(credit);
},
.alias => |opts| switch (opts) {
.set => |set_opts| {
allocator.free(set_opts.selector);
Expand Down
6 changes: 5 additions & 1 deletion src/cli/completion.zig
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,10 @@ const remove_flags = [_]FlagSpec{
.{ .name = "--all", .desc = "Remove every stored account" },
.{ .name = "--json", .desc = "Emit one machine-readable JSON document for non-interactive remove" },
};
const reset_flags = [_]FlagSpec{
.{ .name = "--credit", .desc = "Redeem the reset-credit card with this id", .value = .freeform },
.{ .name = "--json", .desc = "Emit one machine-readable JSON document" },
};
const app_flags = [_]FlagSpec{
.{ .name = "--id", .desc = "Set the Windows package/AUMID or macOS bundle identifier", .value = .freeform },
.{ .name = "--codex-cli-path", .desc = "Set CODEX_CLI_PATH for the app launch", .value = .path },
Expand Down Expand Up @@ -386,7 +390,7 @@ const command_specs = [_]CmdSpec{
.{ .name = "export", .desc = "Export stored account auth files", .flags = &export_flags, .positionals = &export_positionals },
.{ .name = "switch", .desc = "Switch the active account", .flags = &switch_flags, .positionals = &switch_positionals, .pos_blocks_flags = &.{ "--live", "--api", "--skip-api" } },
.{ .name = "remove", .desc = "Remove one or more accounts", .flags = &remove_flags, .positionals = &remove_positionals, .pos_blocks_flags = &.{ "--live", "--api", "--skip-api", "--all" } },
.{ .name = "reset", .desc = "Use the available reset credit that expires first", .positionals = &reset_positionals },
.{ .name = "reset", .desc = "Use the available reset credit that expires first", .flags = &reset_flags, .positionals = &reset_positionals },
.{ .name = "alias", .desc = "Set or clear account aliases", .subs = &alias_subs },
.{ .name = "clean", .desc = "Delete backup and stale files under accounts/", .positionals = &clean_positionals },
.{ .name = "completion", .desc = "Generate shell completion scripts", .positionals = &completion_positionals },
Expand Down
5 changes: 4 additions & 1 deletion src/cli/help.zig
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ fn writeUsageLines(out: *std.Io.Writer, topic: HelpTopic) !void {
try out.writeAll(" codex-auth remove --all\n");
try out.writeAll(" codex-auth remove (<selector>...|--all) --json\n");
},
.reset_credits => try out.writeAll(" codex-auth reset <alias|email|display-number|query>\n"),
.reset_credits => try out.writeAll(" codex-auth reset <alias|email|display-number|query> [--credit <id>] [--json]\n"),
.alias => {
try out.writeAll(" codex-auth alias set <alias|email|display-number|query> <alias>\n");
try out.writeAll(" codex-auth alias clear <alias|email|display-number|query>\n");
Expand Down Expand Up @@ -319,6 +319,9 @@ fn writeOptionLines(out: *std.Io.Writer, topic: HelpTopic) !void {
try out.writeAll(" Remove one or more matching accounts.\n");
},
.reset_credits => {
try out.writeAll(" --credit <id> Redeem the stored reset-credit card with this id instead of the\n");
try out.writeAll(" soonest-expiring one.\n");
try out.writeAll(" --json Emit one machine-readable JSON document.\n");
try out.writeAll(" <alias|email|display-number|query>\n");
try out.writeAll(" Select one stored account with an available reset credit.\n");
},
Expand Down
26 changes: 26 additions & 0 deletions src/cli/json_output.zig
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ pub fn printRemoveResult(result: *const results.RemoveResult) !void {
try out.flush();
}

pub fn printResetResult(result: *const results.ResetResult) !void {
var stdout: io_util.Stdout = undefined;
stdout.init();
const out = stdout.out();
try writeResetResult(out, result);
try out.writeAll("\n");
try out.flush();
}

pub fn printError(code: []const u8, message: []const u8, candidates: ?[]const results.AccountView) !void {
var stdout: io_util.Stdout = undefined;
stdout.init();
Expand Down Expand Up @@ -93,6 +102,23 @@ pub fn writeRemoveResult(out: *std.Io.Writer, result: *const results.RemoveResul
try jw.endObject();
}

pub fn writeResetResult(out: *std.Io.Writer, result: *const results.ResetResult) !void {
var jw: std.json.Stringify = .{ .writer = out, .options = .{} };
try jw.beginObject();
try writeSchemaVersion(&jw);
try jw.objectField("command");
try jw.write("reset");
try jw.objectField("redeemed");
try writeAccount(&jw, &result.redeemed);
try jw.objectField("outcome");
try jw.write(@tagName(result.outcome));
try jw.objectField("remaining_credits");
try jw.write(result.remaining_credits);
try jw.objectField("next_expires_at");
try jw.write(result.next_expires_at);
try jw.endObject();
}

pub fn writeError(
out: *std.Io.Writer,
code: []const u8,
Expand Down
2 changes: 2 additions & 0 deletions src/cli/types.zig
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ pub const RemoveOptions = struct {
};
pub const ResetOptions = struct {
selector: []u8,
credit: ?[]u8 = null,
json: bool = false,
};
pub const AliasSetOptions = struct {
selector: []u8,
Expand Down
Loading
Loading