From f57c41381bf926cf8772b5dd3774b6ce98177f42 Mon Sep 17 00:00:00 2001 From: zipg Date: Sun, 28 Jun 2026 19:00:40 +0800 Subject: [PATCH] fix(portable): prevent stale portable marker from wiping history data --- crates/dbx-core/src/storage.rs | 228 ++++++++++++++++++++++++- src-tauri/src/commands/app_settings.rs | 8 +- src-tauri/src/data_dir.rs | 204 ++++++++++++++++------ src-tauri/src/lib.rs | 12 +- 4 files changed, 389 insertions(+), 63 deletions(-) diff --git a/crates/dbx-core/src/storage.rs b/crates/dbx-core/src/storage.rs index e2b86ec9ae..d25dd20602 100644 --- a/crates/dbx-core/src/storage.rs +++ b/crates/dbx-core/src/storage.rs @@ -1,7 +1,7 @@ use std::collections::{HashMap, HashSet}; use std::path::Path; -use rusqlite::{params, params_from_iter, Connection, OptionalExtension, ToSql}; +use rusqlite::{params, params_from_iter, Connection, DatabaseName, OpenFlags, OptionalExtension, ToSql}; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -17,6 +17,80 @@ use crate::saved_sql::{SavedSqlFile, SavedSqlFolder, SavedSqlLibrary}; const SSH_TUNNEL_SECRET_PREFIX: &str = "ssh_tunnels."; const TRANSPORT_LAYER_SECRET_PREFIX: &str = "transport_layers."; +const STORAGE_DB_FILE_NAME: &str = "dbx.db"; +const USER_DATA_TABLES: &[&str] = &[ + "connections", + "connection_secrets", + "history", + "ai_conversations", + "mq_token_records", + "saved_sql_folders", + "saved_sql_files", +]; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DataDbImportResult { + Imported, + SkippedNoSource, + SkippedInvalidSource, + SkippedInvalidTarget, + SkippedSourceEmpty, + SkippedTargetHasData, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum SqliteDbFileState { + Missing, + Empty, + Valid, + Invalid, +} + +pub fn maybe_import_user_data_db( + target_data_dir: &Path, + source_data_dir: Option<&Path>, +) -> Result { + let Some(source_data_dir) = source_data_dir else { + return Ok(DataDbImportResult::SkippedNoSource); + }; + + let source_db_path = source_data_dir.join(STORAGE_DB_FILE_NAME); + if !source_db_path.is_file() { + return Ok(DataDbImportResult::SkippedNoSource); + } + if inspect_sqlite_db_file(&source_db_path)? != SqliteDbFileState::Valid { + return Ok(DataDbImportResult::SkippedInvalidSource); + } + + let source_conn = open_read_only_sqlite(&source_db_path)?; + if !sqlite_db_has_user_data(&source_conn)? { + return Ok(DataDbImportResult::SkippedSourceEmpty); + } + + let target_db_path = target_data_dir.join(STORAGE_DB_FILE_NAME); + match inspect_sqlite_db_file(&target_db_path)? { + SqliteDbFileState::Missing => {} + SqliteDbFileState::Empty => { + remove_sqlite_db_files(&target_db_path)?; + } + SqliteDbFileState::Valid => { + let target_conn = open_read_only_sqlite(&target_db_path)?; + if sqlite_db_has_user_data(&target_conn)? { + return Ok(DataDbImportResult::SkippedTargetHasData); + } + drop(target_conn); + remove_sqlite_db_files(&target_db_path)?; + } + SqliteDbFileState::Invalid => return Ok(DataDbImportResult::SkippedInvalidTarget), + } + + std::fs::create_dir_all(target_data_dir).map_err(|e| format!("Failed to create data dir: {e}"))?; + source_conn + .backup(DatabaseName::Main, &target_db_path, None) + .map_err(|e| format!("Failed to import user data db: {e}"))?; + + Ok(DataDbImportResult::Imported) +} pub struct Storage { db: SqliteHandle, @@ -224,6 +298,64 @@ impl Storage { } } +fn inspect_sqlite_db_file(path: &Path) -> Result { + if !path.exists() { + return Ok(SqliteDbFileState::Missing); + } + + let metadata = path.metadata().map_err(|e| format!("Failed to inspect db file: {e}"))?; + if metadata.len() == 0 { + return Ok(SqliteDbFileState::Empty); + } + + if crate::db::sqlite::path_has_sqlite_header(path)? { + Ok(SqliteDbFileState::Valid) + } else { + Ok(SqliteDbFileState::Invalid) + } +} + +fn open_read_only_sqlite(path: &Path) -> Result { + Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY) + .map_err(|e| format!("Failed to open db read-only: {e}")) +} + +fn sqlite_db_has_user_data(conn: &Connection) -> Result { + for table_name in USER_DATA_TABLES { + if sqlite_table_has_rows(conn, table_name)? { + return Ok(true); + } + } + Ok(false) +} + +fn sqlite_table_has_rows(conn: &Connection, table_name: &str) -> Result { + let exists: bool = conn + .query_row( + "SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?1)", + [table_name], + |row| row.get(0), + ) + .map_err(|e| e.to_string())?; + if !exists { + return Ok(false); + } + + let sql = format!("SELECT EXISTS(SELECT 1 FROM {table_name} LIMIT 1)"); + conn.query_row(&sql, [], |row| row.get(0)).map_err(|e| e.to_string()) +} + +fn remove_sqlite_db_files(db_path: &Path) -> Result<(), String> { + for path in [db_path.to_path_buf(), db_path.with_extension("db-wal"), db_path.with_extension("db-shm")] { + match std::fs::remove_file(&path) { + Ok(()) => {} + Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} + Err(err) => return Err(format!("Failed to remove empty target db file {}: {err}", path.display())), + } + } + Ok(()) +} + fn ensure_history_columns_sync(conn: &Connection) -> Result<(), String> { const COLUMNS: &[(&str, &str)] = &[ ("activity_kind", "TEXT NOT NULL DEFAULT 'query'"), @@ -1956,7 +2088,7 @@ fn map_from_sql_err(err: serde_json::Error) -> rusqlite::Error { #[cfg(test)] mod tests { - use super::{DesktopIconTheme, DesktopSettings, Storage}; + use super::{maybe_import_user_data_db, DataDbImportResult, DesktopIconTheme, DesktopSettings, Storage}; use crate::connection_secrets::{MQ_AUTH_PASSWORD_KEY, MQ_AUTH_TOKEN_KEY, MQ_TOKEN_SIGNING_KEY}; use crate::models::connection::{ConnectionConfig, DatabaseType}; use crate::saved_sql::SavedSqlFile; @@ -1967,6 +2099,11 @@ mod tests { std::env::temp_dir().join(format!("dbx-storage-{name}-{}-{stamp}.db", std::process::id())) } + fn temp_data_dir(name: &str) -> std::path::PathBuf { + let stamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos(); + std::env::temp_dir().join(format!("dbx-storage-{name}-{}-{stamp}", std::process::id())) + } + fn mq_connection(id: &str, token: &str) -> ConnectionConfig { ConnectionConfig { id: id.to_string(), @@ -2055,6 +2192,93 @@ mod tests { config.external_config.as_ref()?.get("tokenSigning")?.get("key")?.as_str() } + async fn create_data_dir_with_connection(name: &str, connection_id: &str, token: &str) -> std::path::PathBuf { + let data_dir = temp_data_dir(name); + let storage = Storage::open(&data_dir.join("dbx.db")).await.unwrap(); + storage.save_connections(&[mq_connection(connection_id, token)]).await.unwrap(); + drop(storage); + data_dir + } + + #[tokio::test] + async fn import_user_data_db_copies_source_when_target_is_missing() { + let source_dir = create_data_dir_with_connection("import-source", "source-connection", "source-token").await; + let target_dir = temp_data_dir("import-target"); + + let result = maybe_import_user_data_db(&target_dir, Some(&source_dir)).unwrap(); + + assert_eq!(result, DataDbImportResult::Imported); + let storage = Storage::open(&target_dir.join("dbx.db")).await.unwrap(); + let connections = storage.load_connections().await.unwrap(); + assert_eq!(connections.len(), 1); + assert_eq!(connections[0].id, "source-connection"); + assert_eq!(mq_token(&connections[0]), Some("source-token")); + } + + #[tokio::test] + async fn import_user_data_db_does_not_overwrite_target_with_user_data() { + let source_dir = + create_data_dir_with_connection("import-source-existing", "source-connection", "source-token").await; + let target_dir = + create_data_dir_with_connection("import-target-existing", "target-connection", "target-token").await; + + let result = maybe_import_user_data_db(&target_dir, Some(&source_dir)).unwrap(); + + assert_eq!(result, DataDbImportResult::SkippedTargetHasData); + let storage = Storage::open(&target_dir.join("dbx.db")).await.unwrap(); + let connections = storage.load_connections().await.unwrap(); + assert_eq!(connections.len(), 1); + assert_eq!(connections[0].id, "target-connection"); + assert_eq!(mq_token(&connections[0]), Some("target-token")); + } + + #[tokio::test] + async fn import_user_data_db_replaces_empty_target_schema() { + let source_dir = + create_data_dir_with_connection("import-source-empty-target", "source-connection", "source-token").await; + let target_dir = temp_data_dir("import-empty-target"); + let target_storage = Storage::open(&target_dir.join("dbx.db")).await.unwrap(); + target_storage + .save_desktop_settings(&DesktopSettings { debug_logging_enabled: true, ..DesktopSettings::default() }) + .await + .unwrap(); + drop(target_storage); + + let result = maybe_import_user_data_db(&target_dir, Some(&source_dir)).unwrap(); + + assert_eq!(result, DataDbImportResult::Imported); + let storage = Storage::open(&target_dir.join("dbx.db")).await.unwrap(); + let connections = storage.load_connections().await.unwrap(); + assert_eq!(connections.len(), 1); + assert_eq!(connections[0].id, "source-connection"); + } + + #[tokio::test] + async fn import_user_data_db_skips_empty_source_schema() { + let source_dir = temp_data_dir("import-empty-source"); + let source_storage = Storage::open(&source_dir.join("dbx.db")).await.unwrap(); + drop(source_storage); + let target_dir = temp_data_dir("import-empty-source-target"); + + let result = maybe_import_user_data_db(&target_dir, Some(&source_dir)).unwrap(); + + assert_eq!(result, DataDbImportResult::SkippedSourceEmpty); + assert!(!target_dir.join("dbx.db").exists()); + } + + #[test] + fn import_user_data_db_skips_invalid_source_file() { + let source_dir = temp_data_dir("import-invalid-source"); + std::fs::create_dir_all(&source_dir).unwrap(); + std::fs::write(source_dir.join("dbx.db"), b"not sqlite").unwrap(); + let target_dir = temp_data_dir("import-invalid-source-target"); + + let result = maybe_import_user_data_db(&target_dir, Some(&source_dir)).unwrap(); + + assert_eq!(result, DataDbImportResult::SkippedInvalidSource); + assert!(!target_dir.join("dbx.db").exists()); + } + #[tokio::test] async fn save_connections_moves_mq_auth_token_to_secret_table_and_restores_it() { let path = temp_db_path("mq-token-secrets"); diff --git a/src-tauri/src/commands/app_settings.rs b/src-tauri/src/commands/app_settings.rs index be5297b65f..bdc756c66a 100644 --- a/src-tauri/src/commands/app_settings.rs +++ b/src-tauri/src/commands/app_settings.rs @@ -176,14 +176,14 @@ fn default_store_dirs(app: &AppHandle) -> Result<(PathBuf, PathBuf), String> { fn default_plugin_store_dir(app: &AppHandle) -> Result { let default_data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?; - Ok(crate::data_dir::resolve_data_dir(default_data_dir).join("plugins")) + Ok(crate::data_dir::resolve_data_dir_with_mode(default_data_dir).data_dir.join("plugins")) } fn default_agent_store_dir(app: &AppHandle) -> Result { let default_data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?; - let data_dir = crate::data_dir::resolve_data_dir(default_data_dir); - Ok(if crate::data_dir::uses_custom_data_dir() { - data_dir.join("agents") + let data_dir_resolution = crate::data_dir::resolve_data_dir_with_mode(default_data_dir); + Ok(if data_dir_resolution.uses_custom_data_dir() { + data_dir_resolution.data_dir.join("agents") } else { dbx_core::connection::default_agent_dir() }) diff --git a/src-tauri/src/data_dir.rs b/src-tauri/src/data_dir.rs index 29205ee111..599bc5b88d 100644 --- a/src-tauri/src/data_dir.rs +++ b/src-tauri/src/data_dir.rs @@ -1,107 +1,203 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; #[cfg(target_os = "windows")] const PORTABLE_MARKER: &str = "portable.dbx"; +#[cfg(target_os = "windows")] +const INSTALLER_MARKER: &str = "uninstall.exe"; -pub fn resolve_data_dir(default_app_data_dir: PathBuf) -> PathBuf { - if let Some(env_dir) = std::env::var_os("DBX_DATA_DIR").filter(|value| !value.is_empty()) { - return PathBuf::from(env_dir); +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DataDirMode { + Default, + EnvOverride, + Portable { exe_dir: PathBuf }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DataDirResolution { + pub data_dir: PathBuf, + pub default_data_dir: PathBuf, + pub mode: DataDirMode, + portable_data_dir: Option, +} + +impl DataDirResolution { + pub fn uses_custom_data_dir(&self) -> bool { + matches!(self.mode, DataDirMode::EnvOverride | DataDirMode::Portable { .. }) } + pub fn is_portable_mode(&self) -> bool { + matches!(self.mode, DataDirMode::Portable { .. }) + } +} + +pub fn resolve_data_dir_with_mode(default_app_data_dir: PathBuf) -> DataDirResolution { + let env_data_dir = std::env::var_os("DBX_DATA_DIR").filter(|value| !value.is_empty()).map(PathBuf::from); + #[cfg(target_os = "windows")] - if let Some(exe_dir) = current_exe_dir() { - if portable_marker_exists(&exe_dir) { - return exe_dir.join("data"); - } + let exe_dir = current_exe_dir(); + #[cfg(not(target_os = "windows"))] + let exe_dir = None; + + let portable_marker_exists = exe_dir.as_deref().is_some_and(portable_marker_exists); + let installer_marker_exists = exe_dir.as_deref().is_some_and(installer_marker_exists); + + resolve_data_dir_from_inputs( + default_app_data_dir, + exe_dir, + portable_marker_exists, + installer_marker_exists, + env_data_dir, + ) +} + +pub fn alternative_data_dir(resolution: &DataDirResolution) -> Option { + match &resolution.mode { + DataDirMode::Portable { .. } => Some(resolution.default_data_dir.clone()), + DataDirMode::Default => resolution.portable_data_dir.clone(), + DataDirMode::EnvOverride => None, } - - default_app_data_dir } -pub fn uses_custom_data_dir() -> bool { - std::env::var_os("DBX_DATA_DIR").filter(|value| !value.is_empty()).is_some() || is_portable_mode() -} - -#[cfg(target_os = "windows")] pub fn is_portable_mode() -> bool { - current_exe_dir().is_some_and(|dir| portable_marker_exists(&dir)) -} - -#[cfg(not(target_os = "windows"))] -pub fn is_portable_mode() -> bool { - false + resolve_data_dir_with_mode(PathBuf::new()).is_portable_mode() } #[cfg(target_os = "windows")] fn current_exe_dir() -> Option { - std::env::current_exe().ok().and_then(|path| path.parent().map(std::path::Path::to_path_buf)) + std::env::current_exe().ok().and_then(|path| path.parent().map(Path::to_path_buf)) } #[cfg(target_os = "windows")] -fn portable_marker_exists(exe_dir: &std::path::Path) -> bool { +fn portable_marker_exists(exe_dir: &Path) -> bool { exe_dir.join(PORTABLE_MARKER).is_file() } -#[cfg(test)] +#[cfg(not(target_os = "windows"))] +fn portable_marker_exists(_exe_dir: &Path) -> bool { + false +} + +#[cfg(target_os = "windows")] +fn installer_marker_exists(exe_dir: &Path) -> bool { + exe_dir.join(INSTALLER_MARKER).is_file() +} + +#[cfg(not(target_os = "windows"))] +fn installer_marker_exists(_exe_dir: &Path) -> bool { + false +} + fn resolve_data_dir_from_inputs( default_app_data_dir: PathBuf, exe_dir: Option, portable_marker_exists: bool, + installer_marker_exists: bool, env_data_dir: Option, -) -> PathBuf { +) -> DataDirResolution { + let portable_data_dir = exe_dir.as_ref().filter(|_| portable_marker_exists).map(|dir| dir.join("data")); + if let Some(env_dir) = env_data_dir { - return env_dir; + return DataDirResolution { + data_dir: env_dir, + default_data_dir: default_app_data_dir, + mode: DataDirMode::EnvOverride, + portable_data_dir, + }; } - match (exe_dir, portable_marker_exists) { - (Some(dir), true) => dir.join("data"), - _ => default_app_data_dir, + if portable_marker_exists && !installer_marker_exists { + if let Some(exe_dir) = exe_dir { + return DataDirResolution { + data_dir: exe_dir.join("data"), + default_data_dir: default_app_data_dir, + mode: DataDirMode::Portable { exe_dir }, + portable_data_dir, + }; + } } -} -#[cfg(test)] -fn is_portable_mode_from_inputs(exe_dir: Option, portable_marker_exists: bool) -> bool { - exe_dir.is_some() && portable_marker_exists -} - -#[cfg(test)] -fn uses_custom_data_dir_from_inputs( - env_data_dir: Option, - exe_dir: Option, - portable_marker_exists: bool, -) -> bool { - env_data_dir.is_some() || is_portable_mode_from_inputs(exe_dir, portable_marker_exists) + DataDirResolution { + data_dir: default_app_data_dir.clone(), + default_data_dir: default_app_data_dir, + mode: DataDirMode::Default, + portable_data_dir, + } } #[cfg(test)] mod tests { use std::path::PathBuf; - use super::{is_portable_mode_from_inputs, resolve_data_dir_from_inputs, uses_custom_data_dir_from_inputs}; + use super::{alternative_data_dir, resolve_data_dir_from_inputs, DataDirMode}; #[test] - fn uses_portable_data_dir_when_marker_exists() { + fn uses_portable_data_dir_when_marker_exists_without_installer_marker() { let default_dir = PathBuf::from(r"C:\Users\Administrator\AppData\Roaming\com.dbx.app"); let exe_dir = PathBuf::from(r"D:\Apps\DBX"); - let data_dir = resolve_data_dir_from_inputs(default_dir, Some(exe_dir.clone()), true, None); + let resolution = resolve_data_dir_from_inputs(default_dir, Some(exe_dir.clone()), true, false, None); - assert_eq!(data_dir, exe_dir.join("data")); + assert_eq!(resolution.data_dir, exe_dir.join("data")); + assert_eq!(resolution.mode, DataDirMode::Portable { exe_dir }); + assert!(resolution.uses_custom_data_dir()); + assert!(resolution.is_portable_mode()); } #[test] - fn detects_portable_mode_only_when_marker_exists_next_to_exe() { + fn installer_marker_keeps_installed_mode_even_when_portable_marker_exists() { + let default_dir = PathBuf::from(r"C:\Users\Administrator\AppData\Roaming\com.dbx.app"); + let exe_dir = PathBuf::from(r"C:\Program Files\DBX"); + + let resolution = resolve_data_dir_from_inputs(default_dir.clone(), Some(exe_dir), true, true, None); + + assert_eq!(resolution.data_dir, default_dir); + assert_eq!(resolution.mode, DataDirMode::Default); + assert!(!resolution.uses_custom_data_dir()); + assert!(!resolution.is_portable_mode()); + } + + #[test] + fn env_override_wins_over_installer_and_portable_markers() { + let default_dir = PathBuf::from(r"C:\Users\Administrator\AppData\Roaming\com.dbx.app"); + let exe_dir = PathBuf::from(r"C:\Program Files\DBX"); + let env_dir = PathBuf::from(r"E:\DBXData"); + + let resolution = resolve_data_dir_from_inputs(default_dir, Some(exe_dir), true, true, Some(env_dir.clone())); + + assert_eq!(resolution.data_dir, env_dir); + assert_eq!(resolution.mode, DataDirMode::EnvOverride); + assert!(resolution.uses_custom_data_dir()); + assert!(!resolution.is_portable_mode()); + } + + #[test] + fn portable_mode_can_import_from_default_data_dir() { + let default_dir = PathBuf::from(r"C:\Users\Administrator\AppData\Roaming\com.dbx.app"); let exe_dir = PathBuf::from(r"D:\Apps\DBX"); - assert!(is_portable_mode_from_inputs(Some(exe_dir), true)); - assert!(!is_portable_mode_from_inputs(Some(PathBuf::from(r"D:\Apps\DBX")), false)); - assert!(!is_portable_mode_from_inputs(None, true)); + let resolution = resolve_data_dir_from_inputs(default_dir.clone(), Some(exe_dir), true, false, None); + + assert_eq!(alternative_data_dir(&resolution), Some(default_dir)); } #[test] - fn custom_data_dir_is_used_for_env_override_or_portable_mode() { - assert!(uses_custom_data_dir_from_inputs(Some(PathBuf::from(r"D:\DBXData")), None, false)); - assert!(uses_custom_data_dir_from_inputs(None, Some(PathBuf::from(r"D:\Apps\DBX")), true)); - assert!(!uses_custom_data_dir_from_inputs(None, Some(PathBuf::from(r"D:\Apps\DBX")), false)); + fn installed_mode_can_import_from_leftover_portable_data_dir() { + let default_dir = PathBuf::from(r"C:\Users\Administrator\AppData\Roaming\com.dbx.app"); + let exe_dir = PathBuf::from(r"C:\Program Files\DBX"); + + let resolution = resolve_data_dir_from_inputs(default_dir, Some(exe_dir.clone()), true, true, None); + + assert_eq!(alternative_data_dir(&resolution), Some(exe_dir.join("data"))); + } + + #[test] + fn env_override_does_not_import_from_implicit_alternative_dir() { + let default_dir = PathBuf::from(r"C:\Users\Administrator\AppData\Roaming\com.dbx.app"); + let exe_dir = PathBuf::from(r"D:\Apps\DBX"); + + let resolution = + resolve_data_dir_from_inputs(default_dir, Some(exe_dir), true, false, Some(PathBuf::from(r"E:\DBXData"))); + + assert_eq!(alternative_data_dir(&resolution), None); } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e6ec6473c9..77ede2b840 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -5,7 +5,7 @@ mod models; mod window_state_guard; use commands::connection::AppState; -use dbx_core::storage::{DesktopIconTheme, DesktopSettings, Storage}; +use dbx_core::storage::{maybe_import_user_data_db, DesktopIconTheme, DesktopSettings, Storage}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Instant; @@ -530,8 +530,14 @@ pub fn run() { let default_data_dir = app.path().app_data_dir().map_err(|e| e.to_string()).expect("Failed to resolve app data dir"); - let data_dir = data_dir::resolve_data_dir(default_data_dir); + let data_dir_resolution = data_dir::resolve_data_dir_with_mode(default_data_dir); + let data_dir = data_dir_resolution.data_dir.clone(); std::fs::create_dir_all(&data_dir).expect("Failed to create data dir"); + let alternative_data_dir = data_dir::alternative_data_dir(&data_dir_resolution); + match maybe_import_user_data_db(&data_dir, alternative_data_dir.as_deref()) { + Ok(result) => eprintln!("[STARTUP] data db fallback import: {result:?}"), + Err(err) => eprintln!("[STARTUP] data db fallback import failed: {err}"), + } let db_path = data_dir.join("dbx.db"); let t = Instant::now(); @@ -548,7 +554,7 @@ pub fn run() { apply_debug_log_level(desktop_settings.debug_logging_enabled); eprintln!("[STARTUP] storage ready in {:?}", t.elapsed()); - let default_agent_dir = data_dir::uses_custom_data_dir().then(|| data_dir.join("agents")); + let default_agent_dir = data_dir_resolution.uses_custom_data_dir().then(|| data_dir.join("agents")); let (plugin_dir, agent_dir) = commands::app_settings::resolve_driver_store_dirs_from_settings( &desktop_settings, &data_dir,