diff --git a/.deepseek/trusted b/.deepseek/trusted deleted file mode 100644 index e69de29b..00000000 diff --git a/.gitignore b/.gitignore index 7eb9dbac..c1cef6b6 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,7 @@ AI_HANDOFF.md .codex/ docs/rlm-paper.txt .context/ + +# Local runtime state +.deepseek/ +session_*.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dafde80..ad171bb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.3.17] - 2026-02-16 + +### Fixed +- Config loading now expands `~` in `DEEPSEEK_CONFIG_PATH` and `--config` paths. +- When `DEEPSEEK_CONFIG_PATH` points to a missing file, config loading now falls back to `~/.deepseek/config.toml` if it exists. + +### Changed +- Removed committed transient runtime artifacts (`session_*.json`, `.deepseek/trusted`) and added ignore rules to prevent re-commit. + ## [0.3.16] - 2026-02-15 ### Added diff --git a/Cargo.lock b/Cargo.lock index 2d023f28..1c5ca08b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -726,7 +726,7 @@ dependencies = [ [[package]] name = "deepseek-tui" -version = "0.3.16" +version = "0.3.17" dependencies = [ "anyhow", "arboard", diff --git a/Cargo.toml b/Cargo.toml index 89d8c86b..f0ee13fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deepseek-tui" -version = "0.3.16" +version = "0.3.17" edition = "2024" description = "Unofficial DeepSeek CLI - Just run 'deepseek' to start chatting" license = "MIT" diff --git a/session_20260216_095242.json b/session_20260216_095242.json deleted file mode 100644 index 397ccbb6..00000000 --- a/session_20260216_095242.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "schema_version": 1, - "metadata": { - "id": "9f574f3d-c6f6-436e-9967-aaaba56148a2", - "title": "New Session", - "created_at": "2026-02-16T15:52:42.502684Z", - "updated_at": "2026-02-16T15:52:42.502684Z", - "message_count": 0, - "total_tokens": 0, - "model": "deepseek-v3.2", - "workspace": "/var/folders/gc/lw1tgpk97z51d30mcvbhyb400000gn/T/.tmpTtSsZw", - "mode": "AGENT" - }, - "messages": [], - "system_prompt": null -} \ No newline at end of file diff --git a/session_20260216_095908.json b/session_20260216_095908.json deleted file mode 100644 index 052859f0..00000000 --- a/session_20260216_095908.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "schema_version": 1, - "metadata": { - "id": "a9cab85f-7ea0-44f8-a6dc-88aee23462ab", - "title": "New Session", - "created_at": "2026-02-16T15:59:08.213118Z", - "updated_at": "2026-02-16T15:59:08.213118Z", - "message_count": 0, - "total_tokens": 0, - "model": "deepseek-v3.2", - "workspace": "/var/folders/gc/lw1tgpk97z51d30mcvbhyb400000gn/T/.tmpT6EfcG", - "mode": "AGENT" - }, - "messages": [], - "system_prompt": null -} \ No newline at end of file diff --git a/session_20260216_100056.json b/session_20260216_100056.json deleted file mode 100644 index 386e3508..00000000 --- a/session_20260216_100056.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "schema_version": 1, - "metadata": { - "id": "fa7764c1-15ba-4b5a-bce8-e4f939e1f838", - "title": "New Session", - "created_at": "2026-02-16T16:00:56.854336Z", - "updated_at": "2026-02-16T16:00:56.854336Z", - "message_count": 0, - "total_tokens": 0, - "model": "deepseek-v3.2", - "workspace": "/var/folders/gc/lw1tgpk97z51d30mcvbhyb400000gn/T/.tmpe0lg3x", - "mode": "AGENT" - }, - "messages": [], - "system_prompt": null -} \ No newline at end of file diff --git a/session_20260216_101655.json b/session_20260216_101655.json deleted file mode 100644 index a273bf71..00000000 --- a/session_20260216_101655.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "schema_version": 1, - "metadata": { - "id": "8d3f306c-70d3-46d9-8007-10c638966197", - "title": "New Session", - "created_at": "2026-02-16T16:16:55.890410Z", - "updated_at": "2026-02-16T16:16:55.890410Z", - "message_count": 0, - "total_tokens": 0, - "model": "deepseek-v3.2", - "workspace": "/var/folders/gc/lw1tgpk97z51d30mcvbhyb400000gn/T/.tmpUidsSQ", - "mode": "AGENT" - }, - "messages": [], - "system_prompt": null -} \ No newline at end of file diff --git a/session_20260216_102128.json b/session_20260216_102128.json deleted file mode 100644 index bcecc41a..00000000 --- a/session_20260216_102128.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "schema_version": 1, - "metadata": { - "id": "159c08f3-9aab-435c-9f2b-370a40667710", - "title": "New Session", - "created_at": "2026-02-16T16:21:28.631837Z", - "updated_at": "2026-02-16T16:21:28.631837Z", - "message_count": 0, - "total_tokens": 0, - "model": "deepseek-v3.2", - "workspace": "/var/folders/gc/lw1tgpk97z51d30mcvbhyb400000gn/T/.tmpeUxI8I", - "mode": "AGENT" - }, - "messages": [], - "system_prompt": null -} \ No newline at end of file diff --git a/session_20260216_103041.json b/session_20260216_103041.json deleted file mode 100644 index e114949f..00000000 --- a/session_20260216_103041.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "schema_version": 1, - "metadata": { - "id": "4e6f3623-30d7-431e-8b11-d8549dac8c13", - "title": "New Session", - "created_at": "2026-02-16T16:30:41.499125Z", - "updated_at": "2026-02-16T16:30:41.499125Z", - "message_count": 0, - "total_tokens": 0, - "model": "deepseek-v3.2", - "workspace": "/var/folders/gc/lw1tgpk97z51d30mcvbhyb400000gn/T/.tmpjykcjh", - "mode": "AGENT" - }, - "messages": [], - "system_prompt": null -} \ No newline at end of file diff --git a/session_20260216_103448.json b/session_20260216_103448.json deleted file mode 100644 index 9ef747b6..00000000 --- a/session_20260216_103448.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "schema_version": 1, - "metadata": { - "id": "0c1e5eb4-e426-4d8a-a622-80b1939e5083", - "title": "New Session", - "created_at": "2026-02-16T16:34:48.304398Z", - "updated_at": "2026-02-16T16:34:48.304398Z", - "message_count": 0, - "total_tokens": 0, - "model": "deepseek-v3.2", - "workspace": "/var/folders/gc/lw1tgpk97z51d30mcvbhyb400000gn/T/.tmp1bRsZy", - "mode": "AGENT" - }, - "messages": [], - "system_prompt": null -} \ No newline at end of file diff --git a/session_20260216_104548.json b/session_20260216_104548.json deleted file mode 100644 index 00a6142c..00000000 --- a/session_20260216_104548.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "schema_version": 1, - "metadata": { - "id": "ce96ab6c-6b3c-45fb-8c69-f7659e50cb03", - "title": "New Session", - "created_at": "2026-02-16T16:45:48.643707Z", - "updated_at": "2026-02-16T16:45:48.643707Z", - "message_count": 0, - "total_tokens": 0, - "model": "deepseek-v3.2", - "workspace": "/var/folders/gc/lw1tgpk97z51d30mcvbhyb400000gn/T/.tmpkQGlZi", - "mode": "AGENT" - }, - "messages": [], - "system_prompt": null -} \ No newline at end of file diff --git a/session_20260216_105657.json b/session_20260216_105657.json deleted file mode 100644 index dc045854..00000000 --- a/session_20260216_105657.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "schema_version": 1, - "metadata": { - "id": "3ea22ecd-cb0a-485b-ba2e-9f9e02d1ec74", - "title": "New Session", - "created_at": "2026-02-16T16:56:57.987857Z", - "updated_at": "2026-02-16T16:56:57.987857Z", - "message_count": 0, - "total_tokens": 0, - "model": "deepseek-v3.2", - "workspace": "/var/folders/gc/lw1tgpk97z51d30mcvbhyb400000gn/T/.tmpOWW4ty", - "mode": "AGENT" - }, - "messages": [], - "system_prompt": null -} \ No newline at end of file diff --git a/session_20260216_105739.json b/session_20260216_105739.json deleted file mode 100644 index 5f61cf1e..00000000 --- a/session_20260216_105739.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "schema_version": 1, - "metadata": { - "id": "206fc731-8181-4693-8c27-1fbc9cc8032e", - "title": "New Session", - "created_at": "2026-02-16T16:57:39.210126Z", - "updated_at": "2026-02-16T16:57:39.210126Z", - "message_count": 0, - "total_tokens": 0, - "model": "deepseek-v3.2", - "workspace": "/var/folders/gc/lw1tgpk97z51d30mcvbhyb400000gn/T/.tmpTNf6nn", - "mode": "AGENT" - }, - "messages": [], - "system_prompt": null -} \ No newline at end of file diff --git a/src/commands/config.rs b/src/commands/config.rs index da5c74cc..ca5b8c69 100644 --- a/src/commands/config.rs +++ b/src/commands/config.rs @@ -218,7 +218,88 @@ mod tests { use crate::config::Config; use crate::tui::app::{App, TuiOptions}; use crate::tui::approval::ApprovalMode; + use std::env; + use std::ffi::OsString; + use std::fs; + use std::path::Path; use std::path::PathBuf; + use std::sync::{Mutex, OnceLock}; + use std::time::{SystemTime, UNIX_EPOCH}; + + struct EnvGuard { + home: Option, + userprofile: Option, + deepseek_config_path: Option, + } + + impl EnvGuard { + fn new(home: &Path) -> Self { + let home_str = OsString::from(home.as_os_str()); + let config_path = home.join(".deepseek").join("config.toml"); + let config_str = OsString::from(config_path.as_os_str()); + let home_prev = env::var_os("HOME"); + let userprofile_prev = env::var_os("USERPROFILE"); + let deepseek_config_prev = env::var_os("DEEPSEEK_CONFIG_PATH"); + + // Safety: test-only environment mutation guarded by a global mutex. + unsafe { + env::set_var("HOME", &home_str); + env::set_var("USERPROFILE", &home_str); + env::set_var("DEEPSEEK_CONFIG_PATH", &config_str); + } + + Self { + home: home_prev, + userprofile: userprofile_prev, + deepseek_config_path: deepseek_config_prev, + } + } + } + + impl Drop for EnvGuard { + fn drop(&mut self) { + if let Some(value) = self.home.take() { + // Safety: test-only environment mutation guarded by a global mutex. + unsafe { + env::set_var("HOME", value); + } + } else { + // Safety: test-only environment mutation guarded by a global mutex. + unsafe { + env::remove_var("HOME"); + } + } + + if let Some(value) = self.userprofile.take() { + // Safety: test-only environment mutation guarded by a global mutex. + unsafe { + env::set_var("USERPROFILE", value); + } + } else { + // Safety: test-only environment mutation guarded by a global mutex. + unsafe { + env::remove_var("USERPROFILE"); + } + } + + if let Some(value) = self.deepseek_config_path.take() { + // Safety: test-only environment mutation guarded by a global mutex. + unsafe { + env::set_var("DEEPSEEK_CONFIG_PATH", value); + } + } else { + // Safety: test-only environment mutation guarded by a global mutex. + unsafe { + env::remove_var("DEEPSEEK_CONFIG_PATH"); + } + } + } + } + + fn env_lock() -> &'static Mutex<()> { + static LOCK: OnceLock> = OnceLock::new(); + LOCK.get_or_init(|| Mutex::new(())) + } fn create_test_app() -> App { let options = TuiOptions { @@ -364,15 +445,33 @@ mod tests { #[test] fn test_logout_clears_api_key_state() { + let _lock = env_lock().lock().unwrap(); + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let temp_root = env::temp_dir().join(format!( + "deepseek-cli-logout-test-{}-{}", + std::process::id(), + nanos + )); + fs::create_dir_all(&temp_root).unwrap(); + let _guard = EnvGuard::new(&temp_root); + + let config_path = temp_root.join(".deepseek").join("config.toml"); + fs::create_dir_all(config_path.parent().unwrap()).unwrap(); + fs::write(&config_path, "api_key = \"test-key\"\n").unwrap(); + let mut app = create_test_app(); - // Note: This test may fail if API key is not set in environment - // but the state changes should still occur let result = logout(&mut app); assert!(result.message.is_some()); assert_eq!(app.onboarding, OnboardingState::ApiKey); assert!(app.onboarding_needs_api_key); assert!(app.api_key_input.is_empty()); assert_eq!(app.api_key_cursor, 0); + + let updated = fs::read_to_string(config_path).unwrap(); + assert!(!updated.contains("api_key")); } #[test] diff --git a/src/config.rs b/src/config.rs index c4c2515b..1e49b631 100644 --- a/src/config.rs +++ b/src/config.rs @@ -123,7 +123,7 @@ impl Config { /// # Ok::<(), anyhow::Error>(()) /// ``` pub fn load(path: Option, profile: Option<&str>) -> Result { - let path = path.or_else(default_config_path); + let path = resolve_load_config_path(path); let mut config = if let Some(path) = path.as_ref() { if path.exists() { let contents = fs::read_to_string(path) @@ -337,14 +337,52 @@ impl Config { // === Defaults === fn default_config_path() -> Option { - if let Ok(path) = std::env::var("DEEPSEEK_CONFIG_PATH") - && !path.trim().is_empty() - { - return Some(PathBuf::from(path)); - } + env_config_path().or_else(home_config_path) +} + +fn home_config_path() -> Option { dirs::home_dir().map(|home| home.join(".deepseek").join("config.toml")) } +fn env_config_path() -> Option { + if let Ok(path) = std::env::var("DEEPSEEK_CONFIG_PATH") { + let trimmed = path.trim(); + if !trimmed.is_empty() { + return Some(expand_path(trimmed)); + } + } + None +} + +fn expand_pathbuf(path: PathBuf) -> PathBuf { + if let Some(raw) = path.to_str() { + return expand_path(raw); + } + path +} + +fn resolve_load_config_path(path: Option) -> Option { + if let Some(path) = path { + return Some(expand_pathbuf(path)); + } + + if let Some(path) = env_config_path() { + if path.exists() { + return Some(path); + } + + if let Some(home_path) = home_config_path() + && home_path.exists() + { + return Some(home_path); + } + + return Some(path); + } + + home_config_path() +} + fn default_managed_config_path() -> Option { #[cfg(unix)] { @@ -850,6 +888,67 @@ mod tests { Ok(()) } + #[test] + fn test_load_uses_tilde_expanded_deepseek_config_path() -> Result<()> { + let _lock = env_lock().lock().unwrap(); + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let temp_root = env::temp_dir().join(format!( + "deepseek-cli-load-tilde-test-{}-{}", + std::process::id(), + nanos + )); + fs::create_dir_all(&temp_root)?; + let _guard = EnvGuard::new(&temp_root); + + let config_path = temp_root.join(".custom-deepseek").join("config.toml"); + ensure_parent_dir(&config_path)?; + fs::write(&config_path, "api_key = \"test-key\"\n")?; + + // Safety: test-only environment mutation guarded by a global mutex. + unsafe { + env::set_var("DEEPSEEK_CONFIG_PATH", "~/.custom-deepseek/config.toml"); + } + + let config = Config::load(None, None)?; + assert_eq!(config.api_key.as_deref(), Some("test-key")); + Ok(()) + } + + #[test] + fn test_load_falls_back_to_home_config_when_env_path_missing() -> Result<()> { + let _lock = env_lock().lock().unwrap(); + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let temp_root = env::temp_dir().join(format!( + "deepseek-cli-load-fallback-test-{}-{}", + std::process::id(), + nanos + )); + fs::create_dir_all(&temp_root)?; + let _guard = EnvGuard::new(&temp_root); + + let home_config = temp_root.join(".deepseek").join("config.toml"); + ensure_parent_dir(&home_config)?; + fs::write(&home_config, "api_key = \"home-key\"\n")?; + + // Safety: test-only environment mutation guarded by a global mutex. + unsafe { + env::set_var( + "DEEPSEEK_CONFIG_PATH", + temp_root.join("missing-config.toml").as_os_str(), + ); + } + + let config = Config::load(None, None)?; + assert_eq!(config.api_key.as_deref(), Some("home-key")); + Ok(()) + } + #[test] fn test_nonexistent_profile_error() { let mut profiles = HashMap::new();