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
37 changes: 3 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ For the best experience, install the Codex CLI even if you mainly use the VS Cod
npm install -g @openai/codex
```

After that, you can use `codex login` or `codex-auth login` to sign in and add accounts more easily.
After that, you can use `codex login`, `codex login --device-auth`, `codex-auth login`, or `codex-auth login --device-auth` to sign in and add accounts more easily.

## Install

Expand Down Expand Up @@ -93,7 +93,7 @@ Remove-Item "$env:LOCALAPPDATA\codex-auth\bin\codex-auth-auto.exe" -Force -Error
| Command | Description |
|---------|-------------|
| `codex-auth list` | List all accounts |
| `codex-auth login` | Run `codex login`, then add the current account |
| `codex-auth login [--device-auth]` | Run `codex login` (optionally with `--device-auth`), then add the current account |
| `codex-auth switch [<email>]` | Switch active account interactively or by partial match |
| `codex-auth remove` | Remove accounts with interactive multi-select |
| `codex-auth status` | Show auto-switch, service, and usage status |
Expand Down Expand Up @@ -158,6 +158,7 @@ Add the currently logged-in Codex account:

```shell
codex-auth login
codex-auth login --device-auth
```

### Import
Expand Down Expand Up @@ -290,38 +291,6 @@ Upgrade notes:
- If you are upgrading from `v0.1.x` to the latest `v0.2.x`, API usage refresh is enabled by default.
- If you previously used an early `v0.2` prerelease/test build and `status` still shows `usage: local`, run `codex-auth config api enable` once to switch back to API mode.

### How to import tokens from cli-proxy-api?

If you have token files from `~/.cli-proxy-api/token*.json`, this repository includes a helper script that can convert them into a format codex-auth can read.

The CLI can also import the flat cli-proxy-api / CPA JSON files directly:

```shell
codex-auth import --cpa # default source: ~/.cli-proxy-api
codex-auth import --cpa /path/to/cpa-dir # scans direct child .json files
```

Each CPA file is converted in memory to the standard auth snapshot shape before it is written into `~/.codex/accounts/`. Missing or empty `refresh_token` values are skipped as `MissingRefreshToken`.

The script is not bundled in the published npm package, so run it from a clone of this repository:

```shell
# Convert: ~/.cli-proxy-api → /tmp/tokens
python3 scripts/convert_tokens.sh

# Or specify custom directories
python3 scripts/convert_tokens.sh <source_dir> <output_dir>
```

Then import and switch:

```shell
codex-auth import /tmp/tokens/
# or import the CPA files directly without a conversion step
codex-auth import --cpa
codex-auth switch
```

Verify with:

```shell
Expand Down
66 changes: 57 additions & 9 deletions src/cli.zig
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ fn stderrColorEnabled() bool {
}

pub const ListOptions = struct {};
pub const LoginOptions = struct {};
pub const LoginOptions = struct {
device_auth: bool = false,
};
pub const ImportSource = enum { standard, cpa };
pub const ImportOptions = struct {
auth_path: ?[]u8,
Expand Down Expand Up @@ -132,7 +134,30 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: []const [:0]const u8) !Pars
}

if (std.mem.eql(u8, cmd, "login")) {
return try parseSimpleCommandArgs(allocator, "login", .login, .{ .login = .{} }, args[2..]);
if (args.len == 3 and isHelpFlag(std.mem.sliceTo(args[2], 0))) {
return .{ .command = .{ .help = .login } };
}

var opts: LoginOptions = .{};
var i: usize = 2;
while (i < args.len) : (i += 1) {
const arg = std.mem.sliceTo(args[i], 0);
if (std.mem.eql(u8, arg, "--device-auth")) {
if (opts.device_auth) {
return usageErrorResult(allocator, .login, "duplicate `--device-auth` for `login`.", .{});
}
opts.device_auth = true;
continue;
}
if (isHelpFlag(arg)) {
return usageErrorResult(allocator, .login, "`--help` must be used by itself for `login`.", .{});
}
if (std.mem.startsWith(u8, arg, "-")) {
return usageErrorResult(allocator, .login, "unknown flag `{s}` for `login`.", .{arg});
}
return usageErrorResult(allocator, .login, "unexpected argument `{s}` for `login`.", .{arg});
}
return .{ .command = .{ .login = opts } };
}

if (std.mem.eql(u8, cmd, "import")) {
Expand Down Expand Up @@ -640,7 +665,7 @@ fn commandDescriptionForTopic(topic: HelpTopic) []const u8 {
.top_level => "Command-line account management for Codex.",
.list => "List available accounts.",
.status => "Show auto-switch, service, and usage API status.",
.login => "Run `codex login`, then add the current account.",
.login => "Run `codex login` or `codex login --device-auth`, then add the current account.",
.import_auth => "Import auth files or rebuild the registry.",
.switch_account => "Switch the active account interactively or by query.",
.remove_account => "Remove one or more accounts.",
Expand All @@ -667,7 +692,10 @@ fn writeUsageSection(out: *std.Io.Writer, topic: HelpTopic) !void {
},
.list => try out.writeAll(" codex-auth list\n"),
.status => try out.writeAll(" codex-auth status\n"),
.login => try out.writeAll(" codex-auth login\n"),
.login => {
try out.writeAll(" codex-auth login\n");
try out.writeAll(" codex-auth login --device-auth\n");
},
.import_auth => {
try out.writeAll(" codex-auth import <path> [--alias <alias>]\n");
try out.writeAll(" codex-auth import --cpa [<path>] [--alias <alias>]\n");
Expand Down Expand Up @@ -708,7 +736,10 @@ fn writeExamplesSection(out: *std.Io.Writer, topic: HelpTopic) !void {
},
.list => try out.writeAll(" codex-auth list\n"),
.status => try out.writeAll(" codex-auth status\n"),
.login => try out.writeAll(" codex-auth login\n"),
.login => {
try out.writeAll(" codex-auth login\n");
try out.writeAll(" codex-auth login --device-auth\n");
},
.import_auth => {
try out.writeAll(" codex-auth import /path/to/auth.json --alias personal\n");
try out.writeAll(" codex-auth import --cpa /path/to/token.json --alias work\n");
Expand Down Expand Up @@ -992,16 +1023,33 @@ pub fn writeCodexLoginLaunchFailureHintTo(out: *std.Io.Writer, err_name: []const
}
}

pub fn runCodexLogin(allocator: std.mem.Allocator) !void {
_ = allocator;
var child = std.process.Child.init(&[_][]const u8{ "codex", "login" }, std.heap.page_allocator);
pub fn codexLoginArgs(opts: LoginOptions) []const []const u8 {
return if (opts.device_auth)
&[_][]const u8{ "codex", "login", "--device-auth" }
else
&[_][]const u8{ "codex", "login" };
}

fn ensureCodexLoginSucceeded(term: std.process.Child.Term) !void {
switch (term) {
.Exited => |code| {
if (code == 0) return;
return error.CodexLoginFailed;
},
else => return error.CodexLoginFailed,
}
}

pub fn runCodexLogin(opts: LoginOptions) !void {
var child = std.process.Child.init(codexLoginArgs(opts), std.heap.page_allocator);
child.stdin_behavior = .Inherit;
child.stdout_behavior = .Inherit;
child.stderr_behavior = .Inherit;
_ = child.spawnAndWait() catch |err| {
const term = child.spawnAndWait() catch |err| {
writeCodexLoginLaunchFailureHint(@errorName(err), stderrColorEnabled()) catch {};
return err;
};
try ensureCodexLoginSucceeded(term);
}

pub fn selectAccount(allocator: std.mem.Allocator, reg: *registry.Registry) !?[]const u8 {
Expand Down
4 changes: 2 additions & 2 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ fn runMain() !void {

fn isHandledCliError(err: anyerror) bool {
return err == error.AccountNotFound or
err == error.CodexLoginFailed or
err == error.RemoveConfirmationUnavailable or
err == error.RemoveSelectionRequiresTty or
err == error.InvalidRemoveSelectionInput;
Expand Down Expand Up @@ -485,8 +486,7 @@ fn handleList(allocator: std.mem.Allocator, codex_home: []const u8, opts: cli.Li
}

fn handleLogin(allocator: std.mem.Allocator, codex_home: []const u8, opts: cli.LoginOptions) !void {
_ = opts;
try cli.runCodexLogin(allocator);
try cli.runCodexLogin(opts);
const auth_path = try registry.activeAuthPath(allocator, codex_home);
defer allocator.free(auth_path);

Expand Down
47 changes: 47 additions & 0 deletions src/tests/cli_bdd_test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ fn expectUsageError(result: cli.ParseResult, topic: cli.HelpTopic, contains: ?[]
}
}

fn expectArgv(actual: []const []const u8, expected: []const []const u8) !void {
try std.testing.expectEqual(expected.len, actual.len);
for (expected, actual) |expected_arg, actual_arg| {
try std.testing.expectEqualStrings(expected_arg, actual_arg);
}
}

test "Scenario: Given import path and alias when parsing then import options are preserved" {
const gpa = std.testing.allocator;
const args = [_][:0]const u8{ "codex-auth", "import", "/tmp/auth.json", "--alias", "personal" };
Expand Down Expand Up @@ -177,6 +184,30 @@ test "Scenario: Given login with unknown flag when parsing then usage error is r
try expectUsageError(result, .login, "unknown flag");
}

test "Scenario: Given login with device auth flag when parsing then device auth is preserved" {
const gpa = std.testing.allocator;
const args = [_][:0]const u8{ "codex-auth", "login", "--device-auth" };
var result = try cli.parseArgs(gpa, &args);
defer cli.freeParseResult(gpa, &result);

switch (result) {
.command => |cmd| switch (cmd) {
.login => |opts| try std.testing.expect(opts.device_auth),
else => return error.TestExpectedEqual,
},
else => return error.TestExpectedEqual,
}
}

test "Scenario: Given login with duplicate device auth flag when parsing then usage error is returned" {
const gpa = std.testing.allocator;
const args = [_][:0]const u8{ "codex-auth", "login", "--device-auth", "--device-auth" };
var result = try cli.parseArgs(gpa, &args);
defer cli.freeParseResult(gpa, &result);

try expectUsageError(result, .login, "duplicate `--device-auth`");
}

test "Scenario: Given command help selector when parsing then command-specific help is preserved" {
const gpa = std.testing.allocator;
const args = [_][:0]const u8{ "codex-auth", "help", "list" };
Expand Down Expand Up @@ -600,6 +631,22 @@ test "Scenario: Given codex login client missing when rendering then detection h
try std.testing.expect(std.mem.indexOf(u8, hint, "Ensure the Codex CLI is installed and available in your environment.") != null);
}

test "Scenario: Given login help when rendering then device auth usage is included" {
const gpa = std.testing.allocator;
var aw: std.Io.Writer.Allocating = .init(gpa);
defer aw.deinit();

try cli.writeCommandHelp(&aw.writer, false, .login);

const help = aw.written();
try std.testing.expect(std.mem.indexOf(u8, help, "codex-auth login --device-auth") != null);
}

test "Scenario: Given login options when building codex argv then device auth is forwarded" {
try expectArgv(cli.codexLoginArgs(.{}), &[_][]const u8{ "codex", "login" });
try expectArgv(cli.codexLoginArgs(.{ .device_auth = true }), &[_][]const u8{ "codex", "login", "--device-auth" });
}

test "Scenario: Given switch with positional query when parsing then non-interactive target is preserved" {
const gpa = std.testing.allocator;
const args = [_][:0]const u8{ "codex-auth", "switch", "user@example.com" };
Expand Down
Loading
Loading