From 99f911f10ba71801b9251432acca5d808ff0c96e Mon Sep 17 00:00:00 2001 From: Pam <276951867@qq.com> Date: Fri, 27 Mar 2026 15:39:53 -0400 Subject: [PATCH 1/4] feat: support device auth login --- README.md | 5 ++- src/cli.zig | 66 ++++++++++++++++++++++++---- src/main.zig | 4 +- src/tests/cli_bdd_test.zig | 38 ++++++++++++++++ src/tests/e2e_cli_test.zig | 88 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 188 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 2c7b949..b918df9 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 []` | 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 | @@ -158,6 +158,7 @@ Add the currently logged-in Codex account: ```shell codex-auth login +codex-auth login --device-auth ``` ### Import diff --git a/src/cli.zig b/src/cli.zig index a8050c3..b59f471 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -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, @@ -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")) { @@ -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.", @@ -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 [--alias ]\n"); try out.writeAll(" codex-auth import --cpa [] [--alias ]\n"); @@ -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"); @@ -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 { diff --git a/src/main.zig b/src/main.zig index 9de688f..dd8ed8c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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; @@ -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); diff --git a/src/tests/cli_bdd_test.zig b/src/tests/cli_bdd_test.zig index 628b0fc..98d249a 100644 --- a/src/tests/cli_bdd_test.zig +++ b/src/tests/cli_bdd_test.zig @@ -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" }; @@ -177,6 +184,21 @@ 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 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" }; @@ -600,6 +622,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" }; diff --git a/src/tests/e2e_cli_test.zig b/src/tests/e2e_cli_test.zig index d42cb65..2473e4c 100644 --- a/src/tests/e2e_cli_test.zig +++ b/src/tests/e2e_cli_test.zig @@ -87,6 +87,38 @@ fn runCliWithIsolatedHome( }); } +fn runCliWithIsolatedHomeAndPath( + allocator: std.mem.Allocator, + project_root: []const u8, + home_root: []const u8, + path_override: []const u8, + args: []const []const u8, +) !std.process.Child.RunResult { + const exe_path = try builtCliPathAlloc(allocator, project_root); + defer allocator.free(exe_path); + + var argv = std.ArrayList([]const u8).empty; + defer argv.deinit(allocator); + try argv.append(allocator, exe_path); + try argv.appendSlice(allocator, args); + + var env_map = try std.process.getEnvMap(allocator); + defer env_map.deinit(); + try env_map.put("HOME", home_root); + try env_map.put("USERPROFILE", home_root); + try env_map.put("PATH", path_override); + try env_map.put("CODEX_AUTH_SKIP_SERVICE_RECONCILE", "1"); + try env_map.put("CODEX_AUTH_DISABLE_BACKGROUND_ACCOUNT_NAME_REFRESH", "1"); + + return try std.process.Child.run(.{ + .allocator = allocator, + .argv = argv.items, + .cwd = project_root, + .env_map = &env_map, + .max_output_bytes = 1024 * 1024, + }); +} + fn runCliWithIsolatedHomeAndStdin( allocator: std.mem.Allocator, project_root: []const u8, @@ -232,6 +264,62 @@ fn appendCustomAccount( }); } +test "Scenario: Given failed device auth login with existing auth json when running login then it forwards the flag and does not mutate the registry" { + const gpa = std.testing.allocator; + const project_root = try projectRootAlloc(gpa); + defer gpa.free(project_root); + try buildCliBinary(gpa, project_root); + + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + const home_root = try tmp.dir.realpathAlloc(gpa, "."); + defer gpa.free(home_root); + try tmp.dir.makePath(".codex"); + try tmp.dir.makePath("fake-bin"); + + const existing_auth = try bdd.authJsonWithEmailPlan(gpa, "existing@example.com", "plus"); + defer gpa.free(existing_auth); + try tmp.dir.writeFile(.{ .sub_path = ".codex/auth.json", .data = existing_auth }); + + try tmp.dir.writeFile(.{ .sub_path = "fake-bin/codex", .data = "#!/bin/sh\nexit 9\n" }); + { + var fake_codex = try tmp.dir.openFile("fake-bin/codex", .{ .mode = .read_write }); + defer fake_codex.close(); + try fake_codex.chmod(0o755); + } + + const fake_bin_path = try std.fs.path.join(gpa, &[_][]const u8{ home_root, "fake-bin" }); + defer gpa.free(fake_bin_path); + const path_override = try std.fmt.allocPrint( + gpa, + "{s}:/tmp/zig-0.15.1:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + .{fake_bin_path}, + ); + defer gpa.free(path_override); + + const result = try runCliWithIsolatedHomeAndPath( + gpa, + project_root, + home_root, + path_override, + &[_][]const u8{ "login", "--device-auth" }, + ); + defer gpa.free(result.stdout); + defer gpa.free(result.stderr); + + try expectFailure(result); + try std.testing.expectEqualStrings("", result.stderr); + + try std.testing.expectError(error.FileNotFound, tmp.dir.openFile(".codex/accounts/registry.json", .{})); + + var active_auth_file = try tmp.dir.openFile(".codex/auth.json", .{}); + defer active_auth_file.close(); + const active_auth = try active_auth_file.readToEndAlloc(gpa, 10 * 1024 * 1024); + defer gpa.free(active_auth); + try std.testing.expectEqualStrings(existing_auth, active_auth); +} + // This simulates first-time use on v0.2 when ~/.codex/auth.json already exists // but ~/.codex/accounts has not been created yet. test "Scenario: Given first-time use on v0.2 with an existing auth.json and no accounts directory when list runs then cli auto-imports and stays usable" { From a8e3da6d6c12b70446cba6194642d6b21a6539c7 Mon Sep 17 00:00:00 2001 From: Loongphy Date: Sat, 28 Mar 2026 16:32:00 +0800 Subject: [PATCH 2/4] test: harden device auth login coverage --- src/cli.zig | 4 +- src/tests/cli_bdd_test.zig | 9 +++ src/tests/e2e_cli_test.zig | 147 +++++++++++++++++++++++++++++++++---- 3 files changed, 143 insertions(+), 17 deletions(-) diff --git a/src/cli.zig b/src/cli.zig index b59f471..3bbdcaf 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -153,9 +153,9 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: []const [:0]const u8) !Pars 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, "unknown flag `{s}` for `login`.", .{arg}); } - return usageErrorResult(allocator, .login, "unexpected argument `{s}` for `login`.", .{ arg }); + return usageErrorResult(allocator, .login, "unexpected argument `{s}` for `login`.", .{arg}); } return .{ .command = .{ .login = opts } }; } diff --git a/src/tests/cli_bdd_test.zig b/src/tests/cli_bdd_test.zig index 98d249a..bc43c76 100644 --- a/src/tests/cli_bdd_test.zig +++ b/src/tests/cli_bdd_test.zig @@ -199,6 +199,15 @@ test "Scenario: Given login with device auth flag when parsing then device auth } } +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" }; diff --git a/src/tests/e2e_cli_test.zig b/src/tests/e2e_cli_test.zig index 2473e4c..58de7eb 100644 --- a/src/tests/e2e_cli_test.zig +++ b/src/tests/e2e_cli_test.zig @@ -57,6 +57,56 @@ fn builtCliPathAlloc(allocator: std.mem.Allocator, project_root: []const u8) ![] return std.fs.path.join(allocator, &[_][]const u8{ project_root, "zig-out", "bin", exe_name }); } +fn fakeCodexCommandPath() []const u8 { + return if (builtin.os.tag == .windows) "fake-bin/codex.cmd" else "fake-bin/codex"; +} + +fn writeFailingFakeCodex(dir: std.fs.Dir, exit_code: u8) !void { + var script_buf: [128]u8 = undefined; + const script = if (builtin.os.tag == .windows) + try std.fmt.bufPrint(&script_buf, "@echo off\r\n>\"%HOME%\\fake-codex-argv.txt\" echo %*\r\nexit /b {d}\r\n", .{exit_code}) + else + try std.fmt.bufPrint(&script_buf, "#!/bin/sh\nprintf '%s\\n' \"$*\" > \"$HOME/fake-codex-argv.txt\"\nexit {d}\n", .{exit_code}); + const sub_path = fakeCodexCommandPath(); + try dir.writeFile(.{ .sub_path = sub_path, .data = script }); + + if (builtin.os.tag != .windows) { + var file = try dir.openFile(sub_path, .{ .mode = .read_write }); + defer file.close(); + try file.chmod(0o755); + } +} + +fn writeSuccessfulFakeCodex(dir: std.fs.Dir) !void { + const script = + if (builtin.os.tag == .windows) + "@echo off\r\n" ++ + ">\"%HOME%\\fake-codex-argv.txt\" echo %*\r\n" ++ + "copy /Y \"%HOME%\\fake-auth.json\" \"%HOME%\\.codex\\auth.json\" >NUL\r\n" ++ + "exit /b 0\r\n" + else + "#!/bin/sh\n" ++ + "printf '%s\\n' \"$*\" > \"$HOME/fake-codex-argv.txt\"\n" ++ + "cp \"$HOME/fake-auth.json\" \"$HOME/.codex/auth.json\"\n" ++ + "exit 0\n"; + const sub_path = fakeCodexCommandPath(); + try dir.writeFile(.{ .sub_path = sub_path, .data = script }); + + if (builtin.os.tag != .windows) { + var file = try dir.openFile(sub_path, .{ .mode = .read_write }); + defer file.close(); + try file.chmod(0o755); + } +} + +fn prependPathEntryAlloc(allocator: std.mem.Allocator, entry: []const u8) ![]u8 { + var env_map = try std.process.getEnvMap(allocator); + defer env_map.deinit(); + + const inherited_path = env_map.get("PATH") orelse return allocator.dupe(u8, entry); + return try std.fmt.allocPrint(allocator, "{s}{c}{s}", .{ entry, std.fs.path.delimiter, inherited_path }); +} + fn runCliWithIsolatedHome( allocator: std.mem.Allocator, project_root: []const u8, @@ -264,6 +314,76 @@ fn appendCustomAccount( }); } +test "Scenario: Given device auth login when running login then it forwards the flag and imports the current account" { + const gpa = std.testing.allocator; + const project_root = try projectRootAlloc(gpa); + defer gpa.free(project_root); + try buildCliBinary(gpa, project_root); + + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + const home_root = try tmp.dir.realpathAlloc(gpa, "."); + defer gpa.free(home_root); + try tmp.dir.makePath(".codex"); + try tmp.dir.makePath("fake-bin"); + + const expected_email = "device-auth@example.com"; + const fake_auth = try bdd.authJsonWithEmailPlan(gpa, expected_email, "plus"); + defer gpa.free(fake_auth); + try tmp.dir.writeFile(.{ .sub_path = "fake-auth.json", .data = fake_auth }); + try writeSuccessfulFakeCodex(tmp.dir); + + const fake_bin_path = try std.fs.path.join(gpa, &[_][]const u8{ home_root, "fake-bin" }); + defer gpa.free(fake_bin_path); + const path_override = try prependPathEntryAlloc(gpa, fake_bin_path); + defer gpa.free(path_override); + + const result = try runCliWithIsolatedHomeAndPath( + gpa, + project_root, + home_root, + path_override, + &[_][]const u8{ "login", "--device-auth" }, + ); + defer gpa.free(result.stdout); + defer gpa.free(result.stderr); + + try expectSuccess(result); + try std.testing.expectEqualStrings("", result.stdout); + try std.testing.expectEqualStrings("", result.stderr); + + const argv_path = try std.fs.path.join(gpa, &[_][]const u8{ home_root, "fake-codex-argv.txt" }); + defer gpa.free(argv_path); + const argv_data = try bdd.readFileAlloc(gpa, argv_path); + defer gpa.free(argv_data); + try std.testing.expect(std.mem.indexOf(u8, argv_data, "login --device-auth") != null); + + const codex_home = try codexHomeAlloc(gpa, home_root); + defer gpa.free(codex_home); + var loaded = try registry.loadRegistry(gpa, codex_home); + defer loaded.deinit(gpa); + try std.testing.expectEqual(@as(usize, 1), loaded.accounts.items.len); + try std.testing.expect(loaded.active_account_key != null); + try std.testing.expect(std.mem.eql(u8, loaded.accounts.items[0].email, expected_email)); + + const expected_account_key = try bdd.accountKeyForEmailAlloc(gpa, expected_email); + defer gpa.free(expected_account_key); + try std.testing.expect(std.mem.eql(u8, loaded.active_account_key.?, expected_account_key)); + + const snapshot_path = try registry.accountAuthPath(gpa, codex_home, expected_account_key); + defer gpa.free(snapshot_path); + const snapshot_data = try bdd.readFileAlloc(gpa, snapshot_path); + defer gpa.free(snapshot_data); + try std.testing.expectEqualStrings(fake_auth, snapshot_data); + + const active_auth_path = try authJsonPathAlloc(gpa, home_root); + defer gpa.free(active_auth_path); + const active_auth = try bdd.readFileAlloc(gpa, active_auth_path); + defer gpa.free(active_auth); + try std.testing.expectEqualStrings(fake_auth, active_auth); +} + test "Scenario: Given failed device auth login with existing auth json when running login then it forwards the flag and does not mutate the registry" { const gpa = std.testing.allocator; const project_root = try projectRootAlloc(gpa); @@ -281,21 +401,11 @@ test "Scenario: Given failed device auth login with existing auth json when runn const existing_auth = try bdd.authJsonWithEmailPlan(gpa, "existing@example.com", "plus"); defer gpa.free(existing_auth); try tmp.dir.writeFile(.{ .sub_path = ".codex/auth.json", .data = existing_auth }); - - try tmp.dir.writeFile(.{ .sub_path = "fake-bin/codex", .data = "#!/bin/sh\nexit 9\n" }); - { - var fake_codex = try tmp.dir.openFile("fake-bin/codex", .{ .mode = .read_write }); - defer fake_codex.close(); - try fake_codex.chmod(0o755); - } + try writeFailingFakeCodex(tmp.dir, 9); const fake_bin_path = try std.fs.path.join(gpa, &[_][]const u8{ home_root, "fake-bin" }); defer gpa.free(fake_bin_path); - const path_override = try std.fmt.allocPrint( - gpa, - "{s}:/tmp/zig-0.15.1:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", - .{fake_bin_path}, - ); + const path_override = try prependPathEntryAlloc(gpa, fake_bin_path); defer gpa.free(path_override); const result = try runCliWithIsolatedHomeAndPath( @@ -309,13 +419,20 @@ test "Scenario: Given failed device auth login with existing auth json when runn defer gpa.free(result.stderr); try expectFailure(result); + try std.testing.expectEqualStrings("", result.stdout); try std.testing.expectEqualStrings("", result.stderr); + const argv_path = try std.fs.path.join(gpa, &[_][]const u8{ home_root, "fake-codex-argv.txt" }); + defer gpa.free(argv_path); + const argv_data = try bdd.readFileAlloc(gpa, argv_path); + defer gpa.free(argv_data); + try std.testing.expect(std.mem.indexOf(u8, argv_data, "login --device-auth") != null); + try std.testing.expectError(error.FileNotFound, tmp.dir.openFile(".codex/accounts/registry.json", .{})); - var active_auth_file = try tmp.dir.openFile(".codex/auth.json", .{}); - defer active_auth_file.close(); - const active_auth = try active_auth_file.readToEndAlloc(gpa, 10 * 1024 * 1024); + const active_auth_path = try authJsonPathAlloc(gpa, home_root); + defer gpa.free(active_auth_path); + const active_auth = try bdd.readFileAlloc(gpa, active_auth_path); defer gpa.free(active_auth); try std.testing.expectEqualStrings(existing_auth, active_auth); } From 0e638b7c7efc6703d5f677285e05906cdc9f757f Mon Sep 17 00:00:00 2001 From: Loongphy Date: Sat, 28 Mar 2026 16:34:30 +0800 Subject: [PATCH 3/4] docs: remove outdated CPA conversion instructions --- README.md | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index b918df9..23f874a 100644 --- a/README.md +++ b/README.md @@ -293,9 +293,7 @@ Upgrade notes: ### 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: +The CLI imports flat cli-proxy-api / CPA JSON files directly: ```shell codex-auth import --cpa # default source: ~/.cli-proxy-api @@ -304,21 +302,9 @@ 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 -``` - -Then import and switch: +After import, switch if needed: ```shell -codex-auth import /tmp/tokens/ -# or import the CPA files directly without a conversion step codex-auth import --cpa codex-auth switch ``` From a6538253dbee9094efeba4da52e575c38832a328 Mon Sep 17 00:00:00 2001 From: Loongphy Date: Sat, 28 Mar 2026 16:42:18 +0800 Subject: [PATCH 4/4] docs: remove duplicate CPA import section --- README.md | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/README.md b/README.md index 23f874a..2975397 100644 --- a/README.md +++ b/README.md @@ -291,24 +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? - -The CLI imports 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`. - -After import, switch if needed: - -```shell -codex-auth import --cpa -codex-auth switch -``` - Verify with: ```shell