Skip to main content

sequel_mcp/app/
paths.rs

1//! XDG-aware path resolution, byte-compatible with the legacy `vault/paths.ts`.
2
3use std::path::PathBuf;
4
5pub const APP_NAME: &str = "sequel-mcp";
6pub const KEYCHAIN_SERVICE_PREFIX: &str = "sequel-mcp";
7
8/// Legacy namespace retained only for one-time migration from
9/// sequel-ace-mcp <= 0.1.0. Read-only.
10pub const LEGACY_APP_NAME: &str = "sequel-ace-mcp";
11pub const LEGACY_KEYCHAIN_SERVICE_PREFIX: &str = "sequel-ace-mcp";
12
13fn home() -> PathBuf {
14    std::env::var_os("HOME")
15        .map(PathBuf::from)
16        .unwrap_or_else(|| {
17            // No HOME: fall back to the passwd entry rather than crash so
18            // diagnostics can still be produced.
19            PathBuf::from("/")
20        })
21}
22
23/// Public read of the effective home directory (test-mode path checks).
24pub fn home_dir() -> PathBuf {
25    home()
26}
27
28pub fn config_dir() -> PathBuf {
29    match std::env::var_os("XDG_CONFIG_HOME") {
30        Some(xdg) if !xdg.is_empty() => PathBuf::from(xdg).join(APP_NAME),
31        _ => home().join(".config").join(APP_NAME),
32    }
33}
34
35pub fn legacy_config_dir() -> PathBuf {
36    match std::env::var_os("XDG_CONFIG_HOME") {
37        Some(xdg) if !xdg.is_empty() => PathBuf::from(xdg).join(LEGACY_APP_NAME),
38        _ => home().join(".config").join(LEGACY_APP_NAME),
39    }
40}
41
42pub fn config_path() -> PathBuf {
43    config_dir().join("config.json")
44}
45
46pub fn legacy_config_path() -> PathBuf {
47    legacy_config_dir().join("config.json")
48}
49
50pub fn data_dir() -> PathBuf {
51    match std::env::var_os("XDG_DATA_HOME") {
52        Some(xdg) if !xdg.is_empty() => PathBuf::from(xdg).join(APP_NAME),
53        _ => home().join(".local").join("share").join(APP_NAME),
54    }
55}
56
57pub fn audit_db_path() -> PathBuf {
58    data_dir().join("audit.sqlite")
59}
60
61/// Runtime registry for live MCP sessions (approval IPC discovery).
62pub fn runtime_dir() -> PathBuf {
63    data_dir().join("runtime")
64}
65
66pub fn keychain_service_name(connection_name: &str) -> String {
67    format!("{KEYCHAIN_SERVICE_PREFIX} : {connection_name}")
68}
69
70pub fn legacy_keychain_service_name(connection_name: &str) -> String {
71    format!("{LEGACY_KEYCHAIN_SERVICE_PREFIX} : {connection_name}")
72}
73
74pub fn sequel_ace_data_dir() -> PathBuf {
75    home()
76        .join("Library")
77        .join("Containers")
78        .join("com.sequel-ace.sequel-ace")
79        .join("Data")
80        .join("Library")
81        .join("Application Support")
82        .join("Sequel Ace")
83        .join("Data")
84}
85
86pub fn sequel_ace_favorites_plist_path() -> PathBuf {
87    sequel_ace_data_dir().join("Favorites.plist")
88}
89
90pub fn sequel_ace_query_history_db_path() -> PathBuf {
91    sequel_ace_data_dir().join("queryHistory.db")
92}
93
94pub fn sequel_ace_legacy_keychain_service_name(favorite_name: &str, favorite_id: i64) -> String {
95    format!("Sequel Ace : {favorite_name} ({favorite_id})")
96}
97
98pub fn sequel_ace_legacy_ssh_keychain_service_name(
99    favorite_name: &str,
100    favorite_id: i64,
101) -> String {
102    format!("Sequel Ace SSHTunnel : {favorite_name} ({favorite_id})")
103}
104
105/// Expand a leading `~` like the legacy `expandSqlitePath`/`expandTilde`.
106pub fn expand_tilde(path: &str) -> PathBuf {
107    if path == ":memory:" {
108        return PathBuf::from(path);
109    }
110    if path == "~" {
111        return home();
112    }
113    if let Some(rest) = path.strip_prefix("~/") {
114        return home().join(rest);
115    }
116    PathBuf::from(path)
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn keychain_names_match_legacy() {
125        assert_eq!(keychain_service_name("prod"), "sequel-mcp : prod");
126        assert_eq!(
127            legacy_keychain_service_name("prod"),
128            "sequel-ace-mcp : prod"
129        );
130        assert_eq!(
131            sequel_ace_legacy_keychain_service_name("acme", 12),
132            "Sequel Ace : acme (12)"
133        );
134        assert_eq!(
135            sequel_ace_legacy_ssh_keychain_service_name("acme", 12),
136            "Sequel Ace SSHTunnel : acme (12)"
137        );
138    }
139
140    #[test]
141    fn expand_tilde_variants() {
142        assert_eq!(expand_tilde("~"), home());
143        assert_eq!(expand_tilde("~/x/y"), home().join("x/y"));
144        assert_eq!(expand_tilde("/abs"), PathBuf::from("/abs"));
145    }
146}