From 6d19b73edf0f58e7ab2e5288d4048a640623eb45 Mon Sep 17 00:00:00 2001 From: Gabriel Peal Date: Thu, 7 Aug 2025 01:17:33 -0700 Subject: [PATCH] Add logout command to CLI and TUI (#1932) ## Summary - support `codex logout` via new subcommand and helper that removes the stored `auth.json` - expose a `logout` function in `codex-login` and test it - add `/logout` slash command in the TUI; command list is filtered when not logged in and the handler deletes `auth.json` then exits ## Testing - `just fix` *(fails: failed to get `diffy` from crates.io)* - `cargo test --all-features` *(fails: failed to get `diffy` from crates.io)* ------ https://chatgpt.com/codex/tasks/task_i_68945c3facac832ca83d48499716fb51 --- codex-rs/cli/src/login.rs | 20 ++++++++++++++++++++ codex-rs/cli/src/main.rs | 14 ++++++++++++++ codex-rs/login/src/lib.rs | 23 +++++++++++++++++++++++ codex-rs/tui/src/app.rs | 6 ++++++ codex-rs/tui/src/slash_command.rs | 2 ++ 5 files changed, 65 insertions(+) diff --git a/codex-rs/cli/src/login.rs b/codex-rs/cli/src/login.rs index 4fa13f0c..4291e068 100644 --- a/codex-rs/cli/src/login.rs +++ b/codex-rs/cli/src/login.rs @@ -8,6 +8,7 @@ use codex_login::OPENAI_API_KEY_ENV_VAR; use codex_login::load_auth; use codex_login::login_with_api_key; use codex_login::login_with_chatgpt; +use codex_login::logout; pub async fn run_login_with_chatgpt(cli_config_overrides: CliConfigOverrides) -> ! { let config = load_config_or_exit(cli_config_overrides); @@ -80,6 +81,25 @@ pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! { } } +pub async fn run_logout(cli_config_overrides: CliConfigOverrides) -> ! { + let config = load_config_or_exit(cli_config_overrides); + + match logout(&config.codex_home) { + Ok(true) => { + eprintln!("Successfully logged out"); + std::process::exit(0); + } + Ok(false) => { + eprintln!("Not logged in"); + std::process::exit(0); + } + Err(e) => { + eprintln!("Error logging out: {e}"); + std::process::exit(1); + } + } +} + fn load_config_or_exit(cli_config_overrides: CliConfigOverrides) -> Config { let cli_overrides = match cli_config_overrides.parse_overrides() { Ok(v) => v, diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index c43365c7..9aef22c0 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -10,6 +10,7 @@ use codex_cli::SeatbeltCommand; use codex_cli::login::run_login_status; use codex_cli::login::run_login_with_api_key; use codex_cli::login::run_login_with_chatgpt; +use codex_cli::login::run_logout; use codex_cli::proto; use codex_common::CliConfigOverrides; use codex_exec::Cli as ExecCli; @@ -48,6 +49,9 @@ enum Subcommand { /// Manage login. Login(LoginCommand), + /// Remove stored authentication credentials. + Logout(LogoutCommand), + /// Experimental: run Codex as an MCP server. Mcp, @@ -106,6 +110,12 @@ enum LoginSubcommand { Status, } +#[derive(Debug, Parser)] +struct LogoutCommand { + #[clap(skip)] + config_overrides: CliConfigOverrides, +} + fn main() -> anyhow::Result<()> { arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move { cli_main(codex_linux_sandbox_exe).await?; @@ -147,6 +157,10 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() } } } + Some(Subcommand::Logout(mut logout_cli)) => { + prepend_config_flags(&mut logout_cli.config_overrides, cli.config_overrides); + run_logout(logout_cli.config_overrides).await; + } Some(Subcommand::Proto(mut proto_cli)) => { prepend_config_flags(&mut proto_cli.config_overrides, cli.config_overrides); proto::run_main(proto_cli).await?; diff --git a/codex-rs/login/src/lib.rs b/codex-rs/login/src/lib.rs index 95bc119e..f35191ce 100644 --- a/codex-rs/login/src/lib.rs +++ b/codex-rs/login/src/lib.rs @@ -6,6 +6,7 @@ use serde::Serialize; use std::env; use std::fs::File; use std::fs::OpenOptions; +use std::fs::remove_file; use std::io::Read; use std::io::Write; #[cfg(unix)] @@ -185,6 +186,17 @@ fn get_auth_file(codex_home: &Path) -> PathBuf { codex_home.join("auth.json") } +/// Delete the auth.json file inside `codex_home` if it exists. Returns `Ok(true)` +/// if a file was removed, `Ok(false)` if no auth file was present. +pub fn logout(codex_home: &Path) -> std::io::Result { + let auth_file = get_auth_file(codex_home); + match remove_file(&auth_file) { + Ok(_) => Ok(true), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(false), + Err(err) => Err(err), + } +} + /// Represents a running login subprocess. The child can be killed by holding /// the mutex and calling `kill()`. #[derive(Debug, Clone)] @@ -494,4 +506,15 @@ mod tests { assert!(auth.get_token_data().await.is_err()); } + + #[test] + fn logout_removes_auth_file() -> Result<(), std::io::Error> { + let dir = tempdir()?; + login_with_api_key(dir.path(), "sk-test-key")?; + assert!(dir.path().join("auth.json").exists()); + let removed = logout(dir.path())?; + assert!(removed); + assert!(!dir.path().join("auth.json").exists()); + Ok(()) + } } diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 3ecb4d0f..1ba8883b 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -328,6 +328,12 @@ impl App<'_> { SlashCommand::Quit => { break; } + SlashCommand::Logout => { + if let Err(e) = codex_login::logout(&self.config.codex_home) { + tracing::error!("failed to logout: {e}"); + } + break; + } SlashCommand::Diff => { let (is_git_repo, diff_text) = match get_git_diff() { Ok(v) => v, diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index 75bca641..0513d644 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -17,6 +17,7 @@ pub enum SlashCommand { Compact, Diff, Status, + Logout, Quit, #[cfg(debug_assertions)] TestApproval, @@ -32,6 +33,7 @@ impl SlashCommand { SlashCommand::Quit => "Exit the application", SlashCommand::Diff => "Show git diff (including untracked files)", SlashCommand::Status => "Show current session configuration and token usage", + SlashCommand::Logout => "Log out of Codex", #[cfg(debug_assertions)] SlashCommand::TestApproval => "Test approval request", }