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
1 change: 1 addition & 0 deletions docs/commands/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This directory documents command behavior by command. Use `codex-auth <command>
| `export` | [docs/commands/export.md](./export.md) |
| `switch` | [docs/commands/switch.md](./switch.md) |
| `remove` | [docs/commands/remove.md](./remove.md) |
| `alias` | [docs/commands/alias.md](./alias.md) |
| `clean` | [docs/commands/clean.md](./clean.md) |
| `config` | [docs/commands/config.md](./config.md) |

Expand Down
36 changes: 36 additions & 0 deletions docs/commands/alias.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# `codex-auth alias`

## Usage

```shell
codex-auth alias set <query> <alias>
codex-auth alias clear <query>
```

## Selector Rules

`<query>` resolves from stored local data only. It does not trigger API refresh.

Selectors can match:

- displayed row number,
- alias fragment,
- email fragment, or
- account name fragment.

If one account matches, the command updates that account immediately. If multiple accounts match, the command falls back to interactive selection in a TTY.

## Set Alias

`codex-auth alias set <query> <alias>` stores an alias in `registry.json` for the matched account.

- Empty aliases are rejected.
- All-digit aliases are rejected because numeric selectors already refer to displayed row numbers.
- Alias comparison is case-insensitive for duplicate detection.
- Changing an alias updates only stored registry metadata.

## Clear Alias

`codex-auth alias clear <query>` removes the stored alias for the matched account.

If the alias is already empty, the command reports that state and leaves the registry unchanged.
4 changes: 3 additions & 1 deletion docs/commands/list.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ When local-only refresh is active, only the active account can be updated from l

## Output Notes

- Alias labels render before the email when an alias exists.
- Singleton rows with aliases render as `alias(email)`.
- Singleton rows with both alias and account name render as `alias(account name, email)`.
- Grouped rows keep the shared email in the header; child rows with both alias and account name render as `alias(account name)`.
- Usage cells show remaining percent and reset time when that data is known.
- Remote refresh failures can render row overlays such as `401`, `403`, `TimedOut`, or `MissingAuth`.
- `LAST ACTIVITY` is based on the last stored usage update time.
Expand Down
1 change: 1 addition & 0 deletions docs/commands/switch.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@ When switching succeeds:
1. `auth.json` is backed up when its contents would change.
2. The selected account snapshot is copied to `~/.codex/auth.json`.
3. `active_account_key` is updated in `registry.json`.
4. The success message uses the same identity label as singleton rows, for example `Switched to me(test@example.com)`.
40 changes: 40 additions & 0 deletions src/cli/commands/alias.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const std = @import("std");
const types = @import("../types.zig");
const common = @import("common.zig");

pub fn parse(allocator: std.mem.Allocator, args: []const [:0]const u8) !types.ParseResult {
if (args.len == 1 and common.isHelpFlag(std.mem.sliceTo(args[0], 0))) {
return .{ .command = .{ .help = .alias } };
}
if (args.len == 0) {
return common.usageErrorResult(allocator, .alias, "`alias` requires `set` or `clear`.", .{});
}

const subcommand = std.mem.sliceTo(args[0], 0);
if (std.mem.eql(u8, subcommand, "set")) {
if (args.len < 3) return common.usageErrorResult(allocator, .alias, "`alias set` requires a selector and alias.", .{});
if (args.len > 3) return common.usageErrorResult(allocator, .alias, "unexpected extra argument `{s}` for `alias set`.", .{std.mem.sliceTo(args[3], 0)});

const selector = try allocator.dupe(u8, std.mem.sliceTo(args[1], 0));
errdefer allocator.free(selector);
const alias_value = try allocator.dupe(u8, std.mem.sliceTo(args[2], 0));
return .{ .command = .{ .alias = .{ .set = .{
.selector = selector,
.alias = alias_value,
} } } };
}
if (std.mem.eql(u8, subcommand, "clear")) {
if (args.len < 2) return common.usageErrorResult(allocator, .alias, "`alias clear` requires a selector.", .{});
if (args.len > 2) return common.usageErrorResult(allocator, .alias, "unexpected extra argument `{s}` for `alias clear`.", .{std.mem.sliceTo(args[2], 0)});
return .{ .command = .{ .alias = .{ .clear = .{
.selector = try allocator.dupe(u8, std.mem.sliceTo(args[1], 0)),
} } } };
}
if (common.isHelpFlag(subcommand)) {
return common.usageErrorResult(allocator, .alias, "`--help` must be used by itself for `alias`.", .{});
}
if (std.mem.startsWith(u8, subcommand, "-")) {
return common.usageErrorResult(allocator, .alias, "unknown flag `{s}` for `alias`.", .{subcommand});
}
return common.usageErrorResult(allocator, .alias, "unknown alias subcommand `{s}`.", .{subcommand});
}
10 changes: 10 additions & 0 deletions src/cli/commands/root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const std = @import("std");
const types = @import("../types.zig");
const common = @import("common.zig");

const alias = @import("alias.zig");
const clean = @import("clean.zig");
const config = @import("config.zig");
const export_auth = @import("export.zig");
Expand Down Expand Up @@ -43,6 +44,7 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: []const [:0]const u8) !type
if (std.mem.eql(u8, cmd, "export")) return export_auth.parse(allocator, args[2..]);
if (std.mem.eql(u8, cmd, "switch")) return switch_account.parse(allocator, args[2..]);
if (std.mem.eql(u8, cmd, "remove")) return remove.parse(allocator, args[2..]);
if (std.mem.eql(u8, cmd, "alias")) return alias.parse(allocator, args[2..]);
if (std.mem.eql(u8, cmd, "clean")) return clean.parse(allocator, args[2..]);
if (std.mem.eql(u8, cmd, "config")) return config.parse(allocator, args[2..]);

Expand Down Expand Up @@ -70,6 +72,13 @@ fn freeCommand(allocator: std.mem.Allocator, cmd: *types.Command) void {
common.freeOwnedStringList(allocator, opts.selectors);
allocator.free(opts.selectors);
},
.alias => |opts| switch (opts) {
.set => |set_opts| {
allocator.free(set_opts.selector);
allocator.free(set_opts.alias);
},
.clear => |clear_opts| allocator.free(clear_opts.selector),
},
else => {},
}
cmd.* = undefined;
Expand All @@ -95,6 +104,7 @@ fn helpTopicForName(name: []const u8) ?types.HelpTopic {
if (std.mem.eql(u8, name, "export")) return .export_auth;
if (std.mem.eql(u8, name, "switch")) return .switch_account;
if (std.mem.eql(u8, name, "remove")) return .remove_account;
if (std.mem.eql(u8, name, "alias")) return .alias;
if (std.mem.eql(u8, name, "clean")) return .clean;
if (std.mem.eql(u8, name, "config")) return .config;
return null;
Expand Down
31 changes: 29 additions & 2 deletions src/cli/help.zig
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ pub fn writeHelp(
try writeCommandDetail(out, use_color, "remove [--live] [--api|--skip-api]");
try writeCommandDetail(out, use_color, "remove <alias|email|display-number|query>...");
try writeCommandDetail(out, use_color, "remove --all");
try writeCommandSummary(out, use_color, "alias", "Set or clear account aliases");
try writeCommandDetail(out, use_color, "alias set <alias|email|display-number|query> <alias>");
try writeCommandDetail(out, use_color, "alias clear <alias|email|display-number|query>");
try writeCommandSummary(out, use_color, "clean", "Delete backup and stale files under accounts/");
try writeCommandDetail(out, use_color, "clean background");
try writeCommandSummary(out, use_color, "config", "Manage configuration");
Expand Down Expand Up @@ -125,6 +128,7 @@ fn commandNameForTopic(topic: HelpTopic) []const u8 {
.export_auth => "export",
.switch_account => "switch",
.remove_account => "remove",
.alias => "alias",
.clean => "clean",
.config => "config",
};
Expand All @@ -139,27 +143,29 @@ fn commandDescriptionForTopic(topic: HelpTopic) []const u8 {
.export_auth => "Export stored account auth files.",
.switch_account => "Switch the active account by alias, email, display number, or partial query.",
.remove_account => "Remove one or more accounts by alias, email, display number, or partial query.",
.alias => "Set or clear an account alias by alias, email, display number, or partial query.",
.clean => "Delete backup and stale files under accounts/.",
.config => "Manage live refresh configuration.",
};
}

fn commandHelpHasExamples(topic: HelpTopic) bool {
return switch (topic) {
.import_auth, .export_auth, .switch_account, .remove_account, .config => true,
.import_auth, .export_auth, .switch_account, .remove_account, .alias, .config => true,
else => false,
};
}

fn commandHelpHasOptions(topic: HelpTopic) bool {
return switch (topic) {
.list, .login, .import_auth, .export_auth, .switch_account, .remove_account, .config => true,
.list, .login, .import_auth, .export_auth, .switch_account, .remove_account, .alias, .config => true,
else => false,
};
}

fn commandHelpHasNotes(topic: HelpTopic) bool {
return switch (topic) {
.switch_account, .alias => true,
else => false,
};
}
Expand Down Expand Up @@ -204,6 +210,10 @@ fn writeUsageLines(out: *std.Io.Writer, topic: HelpTopic) !void {
try out.writeAll(" codex-auth remove <alias|email|display-number|query>...\n");
try out.writeAll(" codex-auth remove --all\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");
},
.clean => {
try out.writeAll(" codex-auth clean\n");
try out.writeAll(" codex-auth clean background\n");
Expand All @@ -223,6 +233,7 @@ pub fn helpCommandForTopic(topic: HelpTopic) []const u8 {
.export_auth => "codex-auth export --help",
.switch_account => "codex-auth switch --help",
.remove_account => "codex-auth remove --help",
.alias => "codex-auth alias --help",
.clean => "codex-auth clean --help",
.config => "codex-auth config --help",
};
Expand Down Expand Up @@ -270,6 +281,12 @@ fn writeOptionLines(out: *std.Io.Writer, topic: HelpTopic) !void {
try out.writeAll(" <alias|email|display-number|query>...\n");
try out.writeAll(" Remove one or more matching accounts.\n");
},
.alias => {
try out.writeAll(" set <selector> <alias>\n");
try out.writeAll(" Set one stored account alias without remote refresh.\n");
try out.writeAll(" clear <selector>\n");
try out.writeAll(" Remove one stored account alias without remote refresh.\n");
},
.config => {
try out.writeAll(" live --interval <seconds>\n");
try out.writeAll(" Set the live TUI refresh interval from 5 to 3600 seconds.\n");
Expand Down Expand Up @@ -332,6 +349,12 @@ fn writeExampleLines(out: *std.Io.Writer, topic: HelpTopic) !void {
try out.writeAll(" codex-auth remove john@example.com jane@example.com\n");
try out.writeAll(" codex-auth remove --all\n");
},
.alias => {
try out.writeAll(" codex-auth alias set 02 work\n");
try out.writeAll(" codex-auth alias set john@example.com personal\n");
try out.writeAll(" codex-auth alias set old-name new-name\n");
try out.writeAll(" codex-auth alias clear work\n");
},
.clean => {
try out.writeAll(" codex-auth clean\n");
try out.writeAll(" codex-auth clean background\n");
Expand All @@ -349,6 +372,10 @@ fn writeNotesSectionStyled(out: *std.Io.Writer, use_color: bool, topic: HelpTopi
.switch_account => {
try out.writeAll(" Targets can be aliases, emails, display numbers, or partial queries.\n");
},
.alias => {
try out.writeAll(" Alias targets can be aliases, emails, display numbers, or partial queries.\n");
try out.writeAll(" New aliases cannot be empty or only digits.\n");
},
else => {},
}
}
Expand Down
82 changes: 72 additions & 10 deletions src/cli/output.zig
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,18 @@ pub fn printSwitchAccountNotFoundError(query: []const u8) !void {
try out.flush();
}

pub fn printAliasAccountNotFoundError(query: []const u8) !void {
var buffer: [768]u8 = undefined;
var writer = std.Io.File.stderr().writer(app_runtime.io(), &buffer);
const out = &writer.interface;
const use_color = style.stderrColorEnabled();
try writeErrorPrefixTo(out, use_color);
try out.print(" no alias target matches '{s}'.\n", .{query});
try writeHintPrefixTo(out, use_color);
try out.writeAll(" Alias targets accept one account: alias, email, display number, or partial query.\n");
try out.flush();
}

pub fn printAccountNotFoundErrors(queries: []const []const u8) !void {
if (queries.len == 0) return;
if (queries.len == 1) {
Expand Down Expand Up @@ -196,6 +208,64 @@ pub fn printRemoveRequiresTtyError() !void {
try out.flush();
}

pub fn printAliasRequiresTtyError() !void {
var buffer: [512]u8 = undefined;
var writer = std.Io.File.stderr().writer(app_runtime.io(), &buffer);
const out = &writer.interface;
const use_color = style.stderrColorEnabled();
try writeErrorPrefixTo(out, use_color);
try out.writeAll(" multiple alias targets require a TTY.\n");
try writeHintPrefixTo(out, use_color);
try out.writeAll(" Narrow the selector or use a displayed row number.\n");
try out.flush();
}

pub fn printInvalidAliasError(reason: []const u8) !void {
var buffer: [768]u8 = undefined;
var writer = std.Io.File.stderr().writer(app_runtime.io(), &buffer);
const out = &writer.interface;
const use_color = style.stderrColorEnabled();
try writeErrorPrefixTo(out, use_color);
try out.print(" invalid alias: {s}\n", .{reason});
try out.flush();
}

pub fn printDuplicateAliasError(alias_value: []const u8, email: []const u8) !void {
var buffer: [768]u8 = undefined;
var writer = std.Io.File.stderr().writer(app_runtime.io(), &buffer);
const out = &writer.interface;
const use_color = style.stderrColorEnabled();
try writeErrorPrefixTo(out, use_color);
try out.print(" alias '{s}' is already used by {s}.\n", .{ alias_value, email });
try out.flush();
}

pub fn printAliasSet(rec: *const registry.AccountRecord, old_alias: []const u8) !void {
var stdout: io_util.Stdout = undefined;
stdout.init();
const out = stdout.out();
if (old_alias.len == 0) {
try out.print("Set alias for {s}: {s}\n", .{ rec.email, rec.alias });
} else if (std.mem.eql(u8, old_alias, rec.alias)) {
try out.print("Alias already set for {s}: {s}\n", .{ rec.email, rec.alias });
} else {
try out.print("Updated alias for {s}: {s} -> {s}\n", .{ rec.email, old_alias, rec.alias });
}
try out.flush();
}

pub fn printAliasCleared(rec: *const registry.AccountRecord, old_alias: []const u8) !void {
var stdout: io_util.Stdout = undefined;
stdout.init();
const out = stdout.out();
if (old_alias.len == 0) {
try out.print("Alias already empty for {s}.\n", .{rec.email});
} else {
try out.print("Cleared alias for {s}: {s}\n", .{ rec.email, old_alias });
}
try out.flush();
}

pub fn printInvalidRemoveSelectionError() !void {
var buffer: [512]u8 = undefined;
var writer = std.Io.File.stderr().writer(app_runtime.io(), &buffer);
Expand Down Expand Up @@ -231,15 +301,7 @@ pub fn buildRemoveLabels(

const label = if (row.depth == 0 or current_header == null) blk: {
const rec = &reg.accounts.items[row.account_index.?];
if (std.mem.eql(u8, row.account_cell, rec.email)) {
const preferred = try display_rows.buildPreferredAccountLabelAlloc(allocator, rec, rec.email);
defer allocator.free(preferred);
if (std.mem.eql(u8, preferred, rec.email)) {
break :blk try allocator.dupe(u8, row.account_cell);
}
break :blk try std.fmt.allocPrint(allocator, "{s} / {s}", .{ rec.email, preferred });
}
break :blk try std.fmt.allocPrint(allocator, "{s} / {s}", .{ rec.email, row.account_cell });
break :blk try display_rows.buildAccountIdentityLabelAlloc(allocator, rec);
} else try std.fmt.allocPrint(allocator, "{s} / {s}", .{ current_header.?, row.account_cell });
try labels.append(allocator, label);
}
Expand Down Expand Up @@ -307,7 +369,7 @@ pub fn printSwitchedAccount(
account_key: []const u8,
) !void {
const label = if (registry.findAccountIndexByAccountKey(reg, account_key)) |idx|
try display_rows.buildPreferredAccountLabelAlloc(allocator, &reg.accounts.items[idx], reg.accounts.items[idx].email)
try display_rows.buildAccountIdentityLabelAlloc(allocator, &reg.accounts.items[idx])
else
try allocator.dupe(u8, account_key);
defer allocator.free(label);
Expand Down
13 changes: 13 additions & 0 deletions src/cli/types.zig
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@ pub const RemoveOptions = struct {
live: bool = false,
api_mode: ApiMode = .default,
};
pub const AliasSetOptions = struct {
selector: []u8,
alias: []u8,
};
pub const AliasClearOptions = struct {
selector: []u8,
};
pub const AliasOptions = union(enum) {
set: AliasSetOptions,
clear: AliasClearOptions,
};
pub const CleanTarget = enum { accounts, background };
pub const CleanOptions = struct {
target: CleanTarget = .accounts,
Expand All @@ -51,6 +62,7 @@ pub const HelpTopic = enum {
export_auth,
switch_account,
remove_account,
alias,
clean,
config,
};
Expand All @@ -62,6 +74,7 @@ pub const Command = union(enum) {
export_auth: ExportOptions,
switch_account: SwitchOptions,
remove_account: RemoveOptions,
alias: AliasOptions,
clean: CleanOptions,
config: ConfigOptions,
version: void,
Expand Down
Loading
Loading