Skip to content

Commit b8b058a

Browse files
committed
Isolate dev/prod app data to prevent clashes
Running dev (`pnpm dev`) and prod (installed .app) on the same machine caused `database is locked` errors, MCP port bind failures, and shared state corruption because both used the same `~/Library/Application Support/com.veszelovszki.cmdr/`. - Add `resolved_app_data_dir()` helper that appends `-dev` to the data directory in debug builds (→ `com.veszelovszki.cmdr-dev/`). Replace all 6 `app_data_dir()` call sites: indexing, settings, AI, font metrics, known shares. - Use MCP port 9225 in dev (prod keeps 9224) to prevent port conflicts. - Update `.mcp.json` and `.claude/settings.json` to match the dev port. - Logs stay shared intentionally — useful for cross-environment debugging.
1 parent 3dbd5af commit b8b058a

13 files changed

Lines changed: 62 additions & 31 deletions

File tree

.claude/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"mcpServers": {
33
"cmdr": {
4-
"url": "http://localhost:9224/mcp"
4+
"url": "http://localhost:9225/mcp"
55
}
66
},
77
"hooks": {

.mcp.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"mcpServers": {
33
"cmdr": {
44
"type": "http",
5-
"url": "http://127.0.0.1:9224/mcp"
5+
"url": "http://127.0.0.1:9225/mcp"
66
},
77
"tauri": {
88
"type": "stdio",

apps/desktop/src-tauri/src/ai/manager.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use std::fs;
1818
use std::path::{Path, PathBuf};
1919
use std::sync::Mutex;
2020
use std::time::{SystemTime, UNIX_EPOCH};
21-
use tauri::{AppHandle, Emitter, Manager, Runtime};
21+
use tauri::{AppHandle, Emitter, Runtime};
2222

2323
/// Global manager state, accessible from Tauri commands.
2424
static MANAGER: Mutex<Option<ManagerState>> = Mutex::new(None);
@@ -341,8 +341,7 @@ fn get_current_model() -> &'static ModelInfo {
341341
}
342342

343343
fn get_ai_dir<R: Runtime>(app: &AppHandle<R>) -> PathBuf {
344-
app.path()
345-
.app_data_dir()
344+
crate::config::resolved_app_data_dir(app)
346345
.unwrap_or_else(|_| PathBuf::from("."))
347346
.join("ai")
348347
}

apps/desktop/src-tauri/src/config.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
//! Configuration constants.
1+
//! Configuration constants and path helpers.
22
//!
33
//! These can be extracted to environment variables or a config file in the future.
44
5+
use std::path::PathBuf;
6+
use tauri::{AppHandle, Manager, Runtime};
7+
58
/// Icon size in pixels (32x32 for retina display)
69
pub const ICON_SIZE: u32 = 32;
710

@@ -15,6 +18,36 @@ pub const ICON_SIZE: u32 = 32;
1518
/// TODO: Move this to a setting once we have a settings window in place
1619
pub const USE_APP_ICONS_AS_DOCUMENT_ICONS: bool = true;
1720

21+
/// Returns the app data directory, with a `-dev` suffix in debug builds to isolate
22+
/// dev and prod data (databases, caches, AI models). Creates the directory if needed.
23+
pub fn resolved_app_data_dir<R: Runtime>(app: &AppHandle<R>) -> Result<PathBuf, String> {
24+
let base = app
25+
.path()
26+
.app_data_dir()
27+
.map_err(|e| format!("Failed to get app data dir: {e}"))?;
28+
29+
let dir = if cfg!(debug_assertions) {
30+
let mut name = base.file_name().unwrap_or_default().to_os_string();
31+
name.push("-dev");
32+
base.with_file_name(name)
33+
} else {
34+
base
35+
};
36+
37+
std::fs::create_dir_all(&dir).map_err(|e| format!("Failed to create app data dir: {e}"))?;
38+
39+
Ok(dir)
40+
}
41+
42+
/// Logs the resolved data directory once at startup.
43+
pub fn log_app_data_dir<R: Runtime>(app: &AppHandle<R>) {
44+
if cfg!(debug_assertions)
45+
&& let Ok(dir) = resolved_app_data_dir(app)
46+
{
47+
log::debug!("Using dev app data dir: {}", dir.display());
48+
}
49+
}
50+
1851
// MCP Server Security Design:
1952
// --------------------------
2053
// The MCP (Model Context Protocol) bridge allows AI assistants to control the app.

apps/desktop/src-tauri/src/font_metrics/CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,6 @@ METRICS_CACHE: LazyLock<RwLock<HashMap<String, FontMetrics>>>
5555

5656
## Dependencies
5757

58-
External: `bincode2`, `tauri::Manager`
58+
External: `bincode2`
59+
Internal: `crate::config::resolved_app_data_dir`
5960
Internal: none

apps/desktop/src-tauri/src/font_metrics/mod.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ use serde::{Deserialize, Serialize};
88
use std::collections::HashMap;
99
use std::fs;
1010
use std::sync::{LazyLock, RwLock};
11-
use tauri::Manager;
1211

1312
/// Cache for font metrics, keyed by font ID (like "system-400-12")
1413
static METRICS_CACHE: LazyLock<RwLock<HashMap<String, FontMetrics>>> = LazyLock::new(|| RwLock::new(HashMap::new()));
@@ -95,7 +94,7 @@ pub fn calculate_max_width(texts: &[&str], font_id: &str) -> Option<f32> {
9594

9695
/// Loads font metrics from disk
9796
pub fn load_from_disk<R: tauri::Runtime>(app: &tauri::AppHandle<R>, font_id: &str) -> Option<FontMetrics> {
98-
let data_dir = app.path().app_data_dir().ok()?;
97+
let data_dir = crate::config::resolved_app_data_dir(app).ok()?;
9998
let metrics_dir = data_dir.join("font-metrics");
10099
let file_path = metrics_dir.join(format!("{}.bin", font_id));
101100

@@ -109,10 +108,7 @@ pub fn save_to_disk<R: tauri::Runtime>(
109108
font_id: &str,
110109
widths: &HashMap<u32, f32>,
111110
) -> Result<(), String> {
112-
let data_dir = app
113-
.path()
114-
.app_data_dir()
115-
.map_err(|e| format!("Failed to get app data dir: {}", e))?;
111+
let data_dir = crate::config::resolved_app_data_dir(app)?;
116112
let metrics_dir = data_dir.join("font-metrics");
117113

118114
// Create directory if it doesn't exist

apps/desktop/src-tauri/src/indexing/mod.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -307,13 +307,7 @@ impl IndexManager {
307307
/// Opens (or creates) the SQLite database, spawns the writer thread,
308308
/// and sets up the micro-scan manager.
309309
pub fn new(volume_id: String, volume_root: PathBuf, app: AppHandle) -> Result<Self, String> {
310-
let data_dir = app
311-
.path()
312-
.app_data_dir()
313-
.map_err(|e| format!("Failed to get app data dir: {e}"))?;
314-
315-
// Ensure the data directory exists
316-
std::fs::create_dir_all(&data_dir).map_err(|e| format!("Failed to create app data dir: {e}"))?;
310+
let data_dir = crate::config::resolved_app_data_dir(&app)?;
317311

318312
let db_path = data_dir.join(format!("index-{volume_id}.db"));
319313

apps/desktop/src-tauri/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,9 @@ pub fn run() {
253253
log::set_max_level(log::LevelFilter::Info);
254254
}
255255

256+
// Log the resolved app data directory (shows -dev suffix in debug builds)
257+
config::log_app_data_dir(app.handle());
258+
256259
// Initialize benchmarking (enabled by RUSTY_COMMANDER_BENCHMARK=1)
257260
benchmark::init_benchmarking();
258261

apps/desktop/src-tauri/src/mcp/CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Expose Cmdr functionality to AI agents via the Model Context Protocol (MCP). Age
99
### Server (`server.rs`)
1010

1111
- Runs in a background tokio task spawned at app startup
12-
- Binds to `127.0.0.1:9224` (localhost only for security)
12+
- Binds to `127.0.0.1:9225` in dev, `127.0.0.1:9224` in release (localhost only for security)
1313
- Streamable HTTP transport (MCP spec 2025-11-25)
1414
- Endpoints: `POST /mcp` (JSON-RPC), `GET /mcp` (optional SSE), `GET /mcp/health`
1515

apps/desktop/src-tauri/src/mcp/config.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,13 @@ impl McpConfig {
3636
// Priority for port:
3737
// 1. CMDR_MCP_PORT env var (explicit dev override)
3838
// 2. User setting (developer.mcpPort)
39-
// 3. Default: 9224
39+
// 3. Default: 9225 in debug builds, 9224 in release
40+
let default_port: u16 = if cfg!(debug_assertions) { 9225 } else { 9224 };
4041
let port = env::var("CMDR_MCP_PORT")
4142
.ok()
4243
.and_then(|v| v.parse().ok())
4344
.or(setting_port)
44-
.unwrap_or(9224);
45+
.unwrap_or(default_port);
4546

4647
Self { enabled, port }
4748
}
@@ -61,10 +62,10 @@ mod tests {
6162
fn test_direct_construction() {
6263
let config = McpConfig {
6364
enabled: true,
64-
port: 9224,
65+
port: 9225,
6566
};
6667

67-
assert_eq!(config.port, 9224);
68+
assert_eq!(config.port, 9225);
6869
assert!(config.enabled);
6970
}
7071

@@ -84,6 +85,10 @@ mod tests {
8485
fn test_from_settings_with_no_settings() {
8586
// When no settings are provided, should use defaults
8687
let config = McpConfig::from_settings_and_env(None, None);
88+
// Debug builds use port 9225 to avoid clashing with a running release build
89+
#[cfg(debug_assertions)]
90+
assert_eq!(config.port, 9225);
91+
#[cfg(not(debug_assertions))]
8792
assert_eq!(config.port, 9224);
8893
// In debug builds, enabled is true by default
8994
#[cfg(debug_assertions)]

0 commit comments

Comments
 (0)