From 15d5e9720c1b335fa436ee473d0ce56ac35172fb Mon Sep 17 00:00:00 2001 From: cyq <15000851237@163.com> Date: Tue, 2 Jun 2026 08:46:20 +0800 Subject: [PATCH 1/3] fix(config): report legacy config migration --- crates/config/src/lib.rs | 104 +++++++++++++++++++++++++++++++++++++-- crates/tui/src/main.rs | 8 ++- 2 files changed, 106 insertions(+), 6 deletions(-) diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index a09569a39..f80123a4f 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -1964,18 +1964,34 @@ pub fn default_config_path() -> Result { Ok(primary) } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ConfigMigration { + pub legacy_path: PathBuf, + pub primary_path: PathBuf, +} + +impl ConfigMigration { + pub fn user_notice(&self) -> String { + format!( + "Migrated legacy config from {} to {}. Use the .codewhale path for future edits; the .deepseek file remains only as a compatibility fallback.", + self.legacy_path.display(), + self.primary_path.display() + ) + } +} + /// v0.8.44: one-time migration from `~/.deepseek/config.toml` to /// `~/.codewhale/config.toml`. Called on first launch after the config /// is loaded; copies the legacy file if the primary doesn't exist yet. /// Never overwrites an existing primary config. -pub fn migrate_config_if_needed() -> Result<()> { +pub fn migrate_config_if_needed() -> Result> { let primary = codewhale_home()?.join(CONFIG_FILE_NAME); if primary.exists() { - return Ok(()); + return Ok(None); } let legacy = legacy_deepseek_home()?.join(CONFIG_FILE_NAME); if !legacy.exists() { - return Ok(()); + return Ok(None); } // Copy the config to the new home. if let Some(parent) = primary.parent() { @@ -1988,7 +2004,10 @@ pub fn migrate_config_if_needed() -> Result<()> { legacy.display(), primary.display() ); - Ok(()) + Ok(Some(ConfigMigration { + legacy_path: legacy, + primary_path: primary, + })) } fn parse_bool(raw: &str) -> Result { @@ -3112,6 +3131,83 @@ unix_socket_path = "/tmp/cw-hooks.sock" ); } + #[test] + fn migrate_config_reports_copied_legacy_path() { + let _lock = env_lock(); + struct HomeEnvGuard { + home: Option, + userprofile: Option, + codewhale_home: Option, + } + + impl Drop for HomeEnvGuard { + fn drop(&mut self) { + // Safety: test-only environment mutation is serialized by env_lock(). + unsafe { + match self.home.take() { + Some(value) => env::set_var("HOME", value), + None => env::remove_var("HOME"), + } + match self.userprofile.take() { + Some(value) => env::set_var("USERPROFILE", value), + None => env::remove_var("USERPROFILE"), + } + match self.codewhale_home.take() { + Some(value) => env::set_var("CODEWHALE_HOME", value), + None => env::remove_var("CODEWHALE_HOME"), + } + } + } + } + + let unique = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("clock") + .as_nanos(); + let home = std::env::temp_dir().join(format!( + "codewhale-config-migration-{}-{unique}", + std::process::id() + )); + let legacy_dir = home.join(LEGACY_APP_DIR); + let primary_dir = home.join(CODEWHALE_APP_DIR); + fs::create_dir_all(&legacy_dir).expect("legacy dir"); + fs::write( + legacy_dir.join(CONFIG_FILE_NAME), + "provider = \"deepseek\"\n", + ) + .expect("legacy config"); + + let _env = HomeEnvGuard { + home: env::var_os("HOME"), + userprofile: env::var_os("USERPROFILE"), + codewhale_home: env::var_os("CODEWHALE_HOME"), + }; + // Safety: test-only environment mutation is serialized by env_lock(). + unsafe { + env::set_var("HOME", &home); + env::set_var("USERPROFILE", &home); + env::remove_var("CODEWHALE_HOME"); + } + + let migration = migrate_config_if_needed() + .expect("migration") + .expect("legacy config should be copied"); + + assert_eq!(migration.legacy_path, legacy_dir.join(CONFIG_FILE_NAME)); + assert_eq!(migration.primary_path, primary_dir.join(CONFIG_FILE_NAME)); + let notice = migration.user_notice(); + assert!(notice.contains(&legacy_dir.join(CONFIG_FILE_NAME).display().to_string())); + assert!(notice.contains(&primary_dir.join(CONFIG_FILE_NAME).display().to_string())); + assert!(notice.contains(".codewhale path for future edits")); + assert!(notice.contains(".deepseek file remains only as a compatibility fallback")); + assert_eq!( + fs::read_to_string(primary_dir.join(CONFIG_FILE_NAME)).expect("primary config"), + "provider = \"deepseek\"\n" + ); + + let _ = fs::remove_dir_all(home); + } + #[test] fn normalize_config_file_path_rejects_traversal() { let err = normalize_config_file_path(PathBuf::from("../config.toml")) diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index 9feaaac46..70dc86b3d 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -4985,8 +4985,12 @@ async fn run_interactive( // v0.8.44: migrate config from ~/.deepseek/ to ~/.codewhale/ on first // launch. Non-fatal — existing installs keep working either way. - if let Err(err) = codewhale_config::migrate_config_if_needed() { - logging::warn(format!("Config migration skipped: {err}")); + match codewhale_config::migrate_config_if_needed() { + Ok(Some(migration)) => { + eprintln!("{}", migration.user_notice()); + } + Ok(None) => {} + Err(err) => logging::warn(format!("Config migration skipped: {err}")), } let model = config.default_model(); From 6a6fd7e5c9fd6dd3331ff78781faad4fa858d8f4 Mon Sep 17 00:00:00 2001 From: cyq <15000851237@163.com> Date: Tue, 2 Jun 2026 09:01:36 +0800 Subject: [PATCH 2/3] test(config): stabilize migration home on windows --- crates/config/src/lib.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index f80123a4f..e0dbbd3cc 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -3137,6 +3137,10 @@ unix_socket_path = "/tmp/cw-hooks.sock" struct HomeEnvGuard { home: Option, userprofile: Option, + #[cfg(windows)] + homedrive: Option, + #[cfg(windows)] + homepath: Option, codewhale_home: Option, } @@ -3152,6 +3156,16 @@ unix_socket_path = "/tmp/cw-hooks.sock" Some(value) => env::set_var("USERPROFILE", value), None => env::remove_var("USERPROFILE"), } + #[cfg(windows)] + match self.homedrive.take() { + Some(value) => env::set_var("HOMEDRIVE", value), + None => env::remove_var("HOMEDRIVE"), + } + #[cfg(windows)] + match self.homepath.take() { + Some(value) => env::set_var("HOMEPATH", value), + None => env::remove_var("HOMEPATH"), + } match self.codewhale_home.take() { Some(value) => env::set_var("CODEWHALE_HOME", value), None => env::remove_var("CODEWHALE_HOME"), @@ -3180,12 +3194,24 @@ unix_socket_path = "/tmp/cw-hooks.sock" let _env = HomeEnvGuard { home: env::var_os("HOME"), userprofile: env::var_os("USERPROFILE"), + #[cfg(windows)] + homedrive: env::var_os("HOMEDRIVE"), + #[cfg(windows)] + homepath: env::var_os("HOMEPATH"), codewhale_home: env::var_os("CODEWHALE_HOME"), }; // Safety: test-only environment mutation is serialized by env_lock(). unsafe { env::set_var("HOME", &home); env::set_var("USERPROFILE", &home); + #[cfg(windows)] + { + let mut components = home.components(); + if let Some(std::path::Component::Prefix(prefix)) = components.next() { + env::set_var("HOMEDRIVE", prefix.as_os_str()); + env::set_var("HOMEPATH", components.as_path()); + } + } env::remove_var("CODEWHALE_HOME"); } From 548cc7731a0c069fc1fa6a65b48d40ec602d5708 Mon Sep 17 00:00:00 2001 From: cyq <15000851237@163.com> Date: Tue, 2 Jun 2026 09:23:00 +0800 Subject: [PATCH 3/3] test(config): use real legacy home on windows --- crates/config/src/lib.rs | 70 +++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index e0dbbd3cc..20601cf42 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -3137,10 +3137,6 @@ unix_socket_path = "/tmp/cw-hooks.sock" struct HomeEnvGuard { home: Option, userprofile: Option, - #[cfg(windows)] - homedrive: Option, - #[cfg(windows)] - homepath: Option, codewhale_home: Option, } @@ -3156,16 +3152,6 @@ unix_socket_path = "/tmp/cw-hooks.sock" Some(value) => env::set_var("USERPROFILE", value), None => env::remove_var("USERPROFILE"), } - #[cfg(windows)] - match self.homedrive.take() { - Some(value) => env::set_var("HOMEDRIVE", value), - None => env::remove_var("HOMEDRIVE"), - } - #[cfg(windows)] - match self.homepath.take() { - Some(value) => env::set_var("HOMEPATH", value), - None => env::remove_var("HOMEPATH"), - } match self.codewhale_home.take() { Some(value) => env::set_var("CODEWHALE_HOME", value), None => env::remove_var("CODEWHALE_HOME"), @@ -3174,6 +3160,34 @@ unix_socket_path = "/tmp/cw-hooks.sock" } } + struct LegacyConfigGuard { + path: PathBuf, + original: Option>, + } + + impl LegacyConfigGuard { + fn install(path: PathBuf, contents: &[u8]) -> Self { + let original = fs::read(&path).ok(); + fs::create_dir_all(path.parent().expect("legacy config parent")) + .expect("legacy dir"); + fs::write(&path, contents).expect("legacy config"); + Self { path, original } + } + } + + impl Drop for LegacyConfigGuard { + fn drop(&mut self) { + if let Some(original) = self.original.take() { + let _ = fs::write(&self.path, original); + } else { + let _ = fs::remove_file(&self.path); + if let Some(parent) = self.path.parent() { + let _ = fs::remove_dir(parent); + } + } + } + } + let unique = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .expect("clock") @@ -3182,44 +3196,32 @@ unix_socket_path = "/tmp/cw-hooks.sock" "codewhale-config-migration-{}-{unique}", std::process::id() )); + #[cfg(windows)] + let legacy_dir = legacy_deepseek_home().expect("legacy home"); + #[cfg(not(windows))] let legacy_dir = home.join(LEGACY_APP_DIR); let primary_dir = home.join(CODEWHALE_APP_DIR); - fs::create_dir_all(&legacy_dir).expect("legacy dir"); - fs::write( - legacy_dir.join(CONFIG_FILE_NAME), - "provider = \"deepseek\"\n", - ) - .expect("legacy config"); + let legacy_config = legacy_dir.join(CONFIG_FILE_NAME); + let _legacy = + LegacyConfigGuard::install(legacy_config.clone(), b"provider = \"deepseek\"\n"); let _env = HomeEnvGuard { home: env::var_os("HOME"), userprofile: env::var_os("USERPROFILE"), - #[cfg(windows)] - homedrive: env::var_os("HOMEDRIVE"), - #[cfg(windows)] - homepath: env::var_os("HOMEPATH"), codewhale_home: env::var_os("CODEWHALE_HOME"), }; // Safety: test-only environment mutation is serialized by env_lock(). unsafe { env::set_var("HOME", &home); env::set_var("USERPROFILE", &home); - #[cfg(windows)] - { - let mut components = home.components(); - if let Some(std::path::Component::Prefix(prefix)) = components.next() { - env::set_var("HOMEDRIVE", prefix.as_os_str()); - env::set_var("HOMEPATH", components.as_path()); - } - } - env::remove_var("CODEWHALE_HOME"); + env::set_var("CODEWHALE_HOME", &primary_dir); } let migration = migrate_config_if_needed() .expect("migration") .expect("legacy config should be copied"); - assert_eq!(migration.legacy_path, legacy_dir.join(CONFIG_FILE_NAME)); + assert_eq!(migration.legacy_path, legacy_config); assert_eq!(migration.primary_path, primary_dir.join(CONFIG_FILE_NAME)); let notice = migration.user_notice(); assert!(notice.contains(&legacy_dir.join(CONFIG_FILE_NAME).display().to_string()));