diff --git a/src/cli.zig b/src/cli.zig index 840fbfda..c2e84d2c 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -1176,6 +1176,23 @@ pub fn runCodexLogin(opts: LoginOptions) !void { try ensureCodexLoginSucceeded(term); } +pub fn runCodexLoginWithCodexHome(allocator: std.mem.Allocator, opts: LoginOptions, codex_home: []const u8) !void { + var env_map = try std.process.getEnvMap(allocator); + defer env_map.deinit(); + try env_map.put("CODEX_HOME", codex_home); + + var child = std.process.Child.init(codexLoginArgs(opts), allocator); + child.env_map = &env_map; + child.stdin_behavior = .Inherit; + child.stdout_behavior = .Inherit; + child.stderr_behavior = .Inherit; + 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 { return selectAccountWithUsageOverrides(allocator, reg, null); } diff --git a/src/main.zig b/src/main.zig index c9cd45d2..caf4481f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const builtin = @import("builtin"); const account_api = @import("account_api.zig"); const account_name_refresh = @import("account_name_refresh.zig"); const cli = @import("cli.zig"); @@ -1530,8 +1531,16 @@ 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 { - try cli.runCodexLogin(opts); - const auth_path = try registry.activeAuthPath(allocator, codex_home); + const login_home = try createTempLoginCodexHome(allocator); + defer { + std.fs.cwd().deleteTree(login_home) catch |err| { + std.log.warn("failed to remove temporary Codex login home `{s}`: {s}", .{ login_home, @errorName(err) }); + }; + allocator.free(login_home); + } + + try cli.runCodexLoginWithCodexHome(allocator, opts, login_home); + const auth_path = try registry.activeAuthPath(allocator, login_home); defer allocator.free(auth_path); const info = try auth.parseAuthInfo(allocator, auth_path); @@ -1540,6 +1549,8 @@ fn handleLogin(allocator: std.mem.Allocator, codex_home: []const u8, opts: cli.L var reg = try registry.loadRegistry(allocator, codex_home); defer reg.deinit(allocator); + _ = try registry.syncActiveAccountFromAuth(allocator, codex_home, ®); + const email = info.email orelse return error.MissingEmail; _ = email; const record_key = info.record_key orelse return error.MissingChatgptUserId; @@ -1548,6 +1559,9 @@ fn handleLogin(allocator: std.mem.Allocator, codex_home: []const u8, opts: cli.L try registry.ensureAccountsDir(allocator, codex_home); try registry.copyFile(auth_path, dest); + const active_auth_path = try registry.activeAuthPath(allocator, codex_home); + defer allocator.free(active_auth_path); + try registry.copyFile(auth_path, active_auth_path); const record = try registry.accountFromAuth(allocator, "", &info); try registry.upsertAccount(allocator, ®, record); @@ -1556,6 +1570,56 @@ fn handleLogin(allocator: std.mem.Allocator, codex_home: []const u8, opts: cli.L try registry.saveRegistry(allocator, codex_home, ®); } +fn createTempLoginCodexHome(allocator: std.mem.Allocator) ![]u8 { + const base = try tempBasePathAlloc(allocator); + defer allocator.free(base); + var counter: usize = 0; + while (counter < 100) : (counter += 1) { + const path = try std.fmt.allocPrint( + allocator, + "{s}{c}codex-auth-login-{d}-{d}", + .{ base, std.fs.path.sep, std.time.nanoTimestamp(), counter }, + ); + std.fs.cwd().makePath(path) catch |err| switch (err) { + error.PathAlreadyExists => { + allocator.free(path); + continue; + }, + else => { + allocator.free(path); + return err; + }, + }; + return path; + } + return error.PathAlreadyExists; +} + +fn tempBasePathAlloc(allocator: std.mem.Allocator) ![]u8 { + if (builtin.os.tag == .windows) { + if (try getNonEmptyEnvVarOwned(allocator, "TEMP")) |path| return path; + if (try getNonEmptyEnvVarOwned(allocator, "TMP")) |path| return path; + if (try getNonEmptyEnvVarOwned(allocator, "TMPDIR")) |path| return path; + return allocator.dupe(u8, "C:\\Temp"); + } + if (try getNonEmptyEnvVarOwned(allocator, "TMPDIR")) |path| return path; + if (try getNonEmptyEnvVarOwned(allocator, "TMP")) |path| return path; + if (try getNonEmptyEnvVarOwned(allocator, "TEMP")) |path| return path; + return allocator.dupe(u8, "/tmp"); +} + +fn getNonEmptyEnvVarOwned(allocator: std.mem.Allocator, name: []const u8) !?[]u8 { + const value = std.process.getEnvVarOwned(allocator, name) catch |err| switch (err) { + error.EnvironmentVariableNotFound => return null, + else => return err, + }; + if (value.len == 0) { + allocator.free(value); + return null; + } + return value; +} + fn handleImport(allocator: std.mem.Allocator, codex_home: []const u8, opts: cli.ImportOptions) !void { if (opts.purge) { var report = try registry.purgeRegistryFromImportSource(allocator, codex_home, opts.auth_path, opts.alias); diff --git a/src/tests/e2e_cli_test.zig b/src/tests/e2e_cli_test.zig index f2af6b67..ccd485d8 100644 --- a/src/tests/e2e_cli_test.zig +++ b/src/tests/e2e_cli_test.zig @@ -540,6 +540,79 @@ test "Scenario: Given CODEX_HOME override when running login then it stores auth try std.testing.expect(std.mem.eql(u8, loaded.accounts.items[0].email, expected_email)); } +test "Scenario: Given existing active auth when login succeeds then upstream login uses a temporary Codex home" { + 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 }); + + const expected_email = "new-login@example.com"; + const fake_auth = try bdd.authJsonWithEmailPlan(gpa, expected_email, "pro"); + 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); + + 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); + + 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, 2), loaded.accounts.items.len); + try std.testing.expect(loaded.active_account_key != null); + + const expected_account_key = try bdd.accountKeyForEmailAlloc(gpa, expected_email); + defer gpa.free(expected_account_key); + try std.testing.expectEqualStrings(expected_account_key, loaded.active_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 existing_account_key = try bdd.accountKeyForEmailAlloc(gpa, "existing@example.com"); + defer gpa.free(existing_account_key); + const existing_snapshot_path = try registry.accountAuthPath(gpa, codex_home, existing_account_key); + defer gpa.free(existing_snapshot_path); + const existing_snapshot_data = try bdd.readFileAlloc(gpa, existing_snapshot_path); + defer gpa.free(existing_snapshot_data); + try std.testing.expectEqualStrings(existing_auth, existing_snapshot_data); +} + 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);