Skip to content

Commit d8e77d7

Browse files
dk19ykkashilk
andauthored
refactor(paths): Consolodates with hierarchical path util (#866)
* refactor: consolidate paths * feat(directories): Parameterize data directory names and backup paths --------- Co-authored-by: kkashilk <[email protected]>
1 parent cc40034 commit d8e77d7

File tree

10 files changed

+252
-80
lines changed

10 files changed

+252
-80
lines changed

crates/chat-cli/src/cli/chat/context.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -628,9 +628,11 @@ async fn load_global_config(os: &Os) -> Result<ContextConfig> {
628628
Ok(config)
629629
} else {
630630
// Return default global configuration with predefined paths
631+
use crate::util::paths::workspace;
632+
631633
Ok(ContextConfig {
632634
paths: vec![
633-
".amazonq/rules/**/*.md".to_string(),
635+
workspace::RULES_PATTERN.to_string(),
634636
"README.md".to_string(),
635637
AMAZONQ_FILENAME.to_string(),
636638
],

crates/chat-cli/src/cli/chat/tool_manager.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ use crate::mcp_client::{
9595
};
9696
use crate::os::Os;
9797
use crate::telemetry::TelemetryThread;
98-
use crate::util::directories::home_dir;
98+
use crate::util::paths::PathResolver;
9999

100100
const NAMESPACE_DELIMITER: &str = "___";
101101
// This applies for both mcp server and tool name since in the end the tool name as seen by the
@@ -104,11 +104,11 @@ const VALID_TOOL_NAME: &str = "^[a-zA-Z][a-zA-Z0-9_]*$";
104104
const SPINNER_CHARS: [char; 10] = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
105105

106106
pub fn workspace_mcp_config_path(os: &Os) -> eyre::Result<PathBuf> {
107-
Ok(os.env.current_dir()?.join(".amazonq").join("mcp.json"))
107+
Ok(PathResolver::new(os).workspace().mcp_config()?)
108108
}
109109

110110
pub fn global_mcp_config_path(os: &Os) -> eyre::Result<PathBuf> {
111-
Ok(home_dir(os)?.join(".aws").join("amazonq").join("mcp.json"))
111+
Ok(PathResolver::new(os).global().mcp_config()?)
112112
}
113113

114114
/// Messages used for communication between the tool initialization thread and the loading
@@ -158,12 +158,13 @@ pub struct McpServerConfig {
158158

159159
impl McpServerConfig {
160160
pub async fn load_config(stderr: &mut impl Write) -> eyre::Result<Self> {
161-
let mut cwd = std::env::current_dir()?;
162-
cwd.push(".amazonq/mcp.json");
163-
let expanded_path = shellexpand::tilde("~/.aws/amazonq/mcp.json");
164-
let global_path = PathBuf::from(expanded_path.as_ref() as &str);
161+
let os = Os::new().await?;
162+
let resolver = PathResolver::new(&os);
163+
let workspace_path = resolver.workspace().mcp_config()?;
164+
let global_path = resolver.global().mcp_config()?;
165+
165166
let global_buf = tokio::fs::read(global_path).await.ok();
166-
let local_buf = tokio::fs::read(cwd).await.ok();
167+
let local_buf = tokio::fs::read(workspace_path).await.ok();
167168
let conf = match (global_buf, local_buf) {
168169
(Some(global_buf), Some(local_buf)) => {
169170
let mut global_conf = Self::from_slice(&global_buf, stderr, "global")?;

crates/chat-cli/src/util/directories.rs

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ use std::path::PathBuf;
33
use thiserror::Error;
44

55
use crate::os::Os;
6+
use crate::util::paths::PathResolver;
67

8+
#[allow(dead_code)] // Allow unused variants during migration
79
#[derive(Debug, Error)]
810
pub enum DirectoryError {
911
#[error("home directory not found")]
@@ -32,6 +34,7 @@ type Result<T, E = DirectoryError> = std::result::Result<T, E>;
3234
/// - Linux: /home/Alice
3335
/// - MacOS: /Users/Alice
3436
/// - Windows: C:\Users\Alice
37+
#[allow(dead_code)] // Allow unused function during migration
3538
pub fn home_dir(#[cfg_attr(windows, allow(unused_variables))] os: &Os) -> Result<PathBuf> {
3639
#[cfg(unix)]
3740
match cfg!(test) {
@@ -70,16 +73,6 @@ pub fn home_dir(#[cfg_attr(windows, allow(unused_variables))] os: &Os) -> Result
7073
}
7174
}
7275

73-
/// The q data directory
74-
///
75-
/// - Linux: `$XDG_DATA_HOME/amazon-q` or `$HOME/.local/share/amazon-q`
76-
/// - MacOS: `$HOME/Library/Application Support/amazon-q`
77-
pub fn fig_data_dir() -> Result<PathBuf> {
78-
Ok(dirs::data_local_dir()
79-
.ok_or(DirectoryError::NoHomeDirectory)?
80-
.join("amazon-q"))
81-
}
82-
8376
/// Get the macos tempdir from the `confstr` function
8477
///
8578
/// See: <https://man7.org/linux/man-pages/man3/confstr.3.html>
@@ -124,29 +117,38 @@ pub fn logs_dir() -> Result<PathBuf> {
124117
if #[cfg(unix)] {
125118
Ok(runtime_dir()?.join("qlog"))
126119
} else if #[cfg(windows)] {
127-
Ok(std::env::temp_dir().join("amazon-q").join("logs"))
120+
use crate::util::paths::application::DATA_DIR_NAME;
121+
Ok(std::env::temp_dir().join(DATA_DIR_NAME).join("logs"))
128122
}
129123
}
130124
}
131125

132126
/// The directory to the directory containing config for the `/context` feature in `q chat`.
133127
pub fn chat_global_context_path(os: &Os) -> Result<PathBuf> {
134-
Ok(home_dir(os)?.join(".aws").join("amazonq").join("global_context.json"))
128+
PathResolver::new(os)
129+
.global()
130+
.global_context()
131+
.map_err(|e| DirectoryError::Io(std::io::Error::other(e)))
135132
}
136133

137134
/// The directory to the directory containing config for the `/context` feature in `q chat`.
138135
pub fn chat_profiles_dir(os: &Os) -> Result<PathBuf> {
139-
Ok(home_dir(os)?.join(".aws").join("amazonq").join("profiles"))
136+
PathResolver::new(os)
137+
.global()
138+
.profiles_dir()
139+
.map_err(|e| DirectoryError::Io(std::io::Error::other(e)))
140140
}
141141

142142
/// The path to the fig settings file
143143
pub fn settings_path() -> Result<PathBuf> {
144-
Ok(fig_data_dir()?.join("settings.json"))
144+
crate::util::paths::ApplicationPaths::settings_path_static()
145+
.map_err(|e| DirectoryError::Io(std::io::Error::other(e)))
145146
}
146147

147148
/// The path to the local sqlite database
148149
pub fn database_path() -> Result<PathBuf> {
149-
Ok(fig_data_dir()?.join("data.sqlite3"))
150+
crate::util::paths::ApplicationPaths::database_path_static()
151+
.map_err(|e| DirectoryError::Io(std::io::Error::other(e)))
150152
}
151153

152154
#[cfg(test)]
@@ -237,9 +239,11 @@ mod tests {
237239

238240
#[test]
239241
fn snapshot_fig_data_dir() {
240-
linux!(fig_data_dir(), @"$HOME/.local/share/amazon-q");
241-
macos!(fig_data_dir(), @"$HOME/Library/Application Support/amazon-q");
242-
windows!(fig_data_dir(), @r"C:\Users\$USER\AppData\Local\amazon-q");
242+
let app_data_dir =
243+
|| crate::util::paths::app_data_dir().map_err(|e| DirectoryError::Io(std::io::Error::other(e)));
244+
linux!(app_data_dir(), @"$HOME/.local/share/amazon-q");
245+
macos!(app_data_dir(), @"$HOME/Library/Application Support/amazon-q");
246+
windows!(app_data_dir(), @r"C:\Users\$USER\AppData\Local\AmazonQ");
243247
}
244248

245249
#[test]

crates/chat-cli/src/util/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ pub mod consts;
22
pub mod directories;
33
pub mod knowledge_store;
44
pub mod open;
5+
pub mod paths;
56
pub mod process;
67
pub mod spinner;
78
pub mod system_info;

crates/chat-cli/src/util/paths.rs

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
//! Hierarchical path management for the application
2+
3+
use std::path::PathBuf;
4+
5+
use crate::os::Os;
6+
7+
#[derive(Debug, thiserror::Error)]
8+
pub enum DirectoryError {
9+
#[error("home directory not found")]
10+
NoHomeDirectory,
11+
#[error("IO Error: {0}")]
12+
Io(#[from] std::io::Error),
13+
}
14+
15+
pub mod workspace {
16+
//! Project-level paths (relative to current working directory)
17+
pub const MCP_CONFIG: &str = ".amazonq/mcp.json";
18+
pub const RULES_PATTERN: &str = ".amazonq/rules/**/*.md";
19+
}
20+
21+
pub mod global {
22+
//! User-level paths (relative to home directory)
23+
pub const MCP_CONFIG: &str = ".aws/amazonq/mcp.json";
24+
pub const GLOBAL_CONTEXT: &str = ".aws/amazonq/global_context.json";
25+
pub const PROFILES_DIR: &str = ".aws/amazonq/profiles";
26+
}
27+
28+
pub mod application {
29+
//! Application data paths (system-specific)
30+
#[cfg(unix)]
31+
pub const DATA_DIR_NAME: &str = "amazon-q";
32+
#[cfg(windows)]
33+
pub const DATA_DIR_NAME: &str = "AmazonQ";
34+
pub const SETTINGS_FILE: &str = "settings.json";
35+
pub const DATABASE_FILE: &str = "data.sqlite3";
36+
}
37+
38+
type Result<T, E = DirectoryError> = std::result::Result<T, E>;
39+
40+
/// The directory of the users home
41+
/// - Linux: /home/Alice
42+
/// - MacOS: /Users/Alice
43+
/// - Windows: C:\Users\Alice
44+
pub fn home_dir(#[cfg_attr(windows, allow(unused_variables))] os: &Os) -> Result<PathBuf> {
45+
#[cfg(unix)]
46+
match cfg!(test) {
47+
true => os
48+
.env
49+
.get("HOME")
50+
.map_err(|_err| DirectoryError::NoHomeDirectory)
51+
.and_then(|h| {
52+
if h.is_empty() {
53+
Err(DirectoryError::NoHomeDirectory)
54+
} else {
55+
Ok(h)
56+
}
57+
})
58+
.map(PathBuf::from)
59+
.map(|p| os.fs.chroot_path(p)),
60+
false => dirs::home_dir().ok_or(DirectoryError::NoHomeDirectory),
61+
}
62+
63+
#[cfg(windows)]
64+
match cfg!(test) {
65+
true => os
66+
.env
67+
.get("USERPROFILE")
68+
.map_err(|_err| DirectoryError::NoHomeDirectory)
69+
.and_then(|h| {
70+
if h.is_empty() {
71+
Err(DirectoryError::NoHomeDirectory)
72+
} else {
73+
Ok(h)
74+
}
75+
})
76+
.map(PathBuf::from)
77+
.map(|p| os.fs.chroot_path(p)),
78+
false => dirs::home_dir().ok_or(DirectoryError::NoHomeDirectory),
79+
}
80+
}
81+
82+
/// The application data directory
83+
/// - Linux: `$XDG_DATA_HOME/{data_dir}` or `$HOME/.local/share/{data_dir}`
84+
/// - MacOS: `$HOME/Library/Application Support/{data_dir}`
85+
/// - Windows: `%LOCALAPPDATA%\{data_dir}`
86+
pub fn app_data_dir() -> Result<PathBuf> {
87+
Ok(dirs::data_local_dir()
88+
.ok_or(DirectoryError::NoHomeDirectory)?
89+
.join(application::DATA_DIR_NAME))
90+
}
91+
92+
/// Path resolver with hierarchy-aware methods
93+
pub struct PathResolver<'a> {
94+
os: &'a Os,
95+
}
96+
97+
impl<'a> PathResolver<'a> {
98+
pub fn new(os: &'a Os) -> Self {
99+
Self { os }
100+
}
101+
102+
/// Get workspace-scoped path resolver
103+
pub fn workspace(&self) -> WorkspacePaths<'_> {
104+
WorkspacePaths { os: self.os }
105+
}
106+
107+
/// Get global-scoped path resolver
108+
pub fn global(&self) -> GlobalPaths<'_> {
109+
GlobalPaths { os: self.os }
110+
}
111+
}
112+
113+
/// Workspace-scoped path methods
114+
pub struct WorkspacePaths<'a> {
115+
os: &'a Os,
116+
}
117+
118+
impl<'a> WorkspacePaths<'a> {
119+
pub fn mcp_config(&self) -> Result<PathBuf> {
120+
Ok(self.os.env.current_dir()?.join(workspace::MCP_CONFIG))
121+
}
122+
}
123+
124+
/// Global-scoped path methods
125+
pub struct GlobalPaths<'a> {
126+
os: &'a Os,
127+
}
128+
129+
impl<'a> GlobalPaths<'a> {
130+
pub fn mcp_config(&self) -> Result<PathBuf> {
131+
Ok(home_dir(self.os)?.join(global::MCP_CONFIG))
132+
}
133+
134+
pub fn global_context(&self) -> Result<PathBuf> {
135+
Ok(home_dir(self.os)?.join(global::GLOBAL_CONTEXT))
136+
}
137+
138+
pub fn profiles_dir(&self) -> Result<PathBuf> {
139+
Ok(home_dir(self.os)?.join(global::PROFILES_DIR))
140+
}
141+
}
142+
143+
/// Application path static methods
144+
pub struct ApplicationPaths;
145+
146+
impl ApplicationPaths {
147+
/// Static method for settings path (to avoid circular dependency)
148+
pub fn settings_path_static() -> Result<PathBuf> {
149+
Ok(app_data_dir()?.join(application::SETTINGS_FILE))
150+
}
151+
152+
/// Static method for database path (to avoid circular dependency)
153+
pub fn database_path_static() -> Result<PathBuf> {
154+
Ok(app_data_dir()?.join(application::DATABASE_FILE))
155+
}
156+
}

crates/fig_install/src/macos.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use std::path::{
1616
use fig_util::consts::{
1717
APP_BUNDLE_ID,
1818
CLI_BINARY_NAME,
19+
system_paths,
1920
};
2021
use fig_util::macos::BUNDLE_CONTENTS_MACOS_PATH;
2122
use fig_util::{
@@ -187,7 +188,7 @@ pub(crate) async fn update(
187188
let installed_app_path = if same_bundle_name {
188189
fig_util::app_bundle_path()
189190
} else {
190-
Path::new("/Applications").join(app_name)
191+
Path::new(system_paths::APPLICATIONS_DIR).join(app_name)
191192
};
192193

193194
let installed_app_path_cstr = CString::new(installed_app_path.as_os_str().as_bytes())?;

crates/fig_util/src/consts.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ pub const PRODUCT_NAME: &str = "Amazon Q";
2525

2626
pub const RUNTIME_DIR_NAME: &str = "cwrun";
2727

28+
/// Data directory name used in paths like ~/.local/share/{DATA_DIR_NAME}
29+
#[cfg(unix)]
30+
pub const DATA_DIR_NAME: &str = "amazon-q";
31+
#[cfg(windows)]
32+
pub const DATA_DIR_NAME: &str = "AmazonQ";
33+
34+
/// Backup directory name
35+
pub const BACKUP_DIR_NAME: &str = ".amazon-q.dotfiles.bak";
36+
2837
// These are the old "CodeWhisperer" branding, used anywhere we will not update to Amazon Q
2938
pub const OLD_PRODUCT_NAME: &str = "CodeWhisperer";
3039
pub const OLD_CLI_BINARY_NAMES: &[&str] = &["cw"];
@@ -134,6 +143,14 @@ pub mod env_var {
134143
}
135144
}
136145

146+
pub mod system_paths {
147+
/// System installation paths
148+
pub const APPLICATIONS_DIR: &str = "/Applications";
149+
pub const USR_LOCAL_BIN: &str = "/usr/local/bin";
150+
pub const USR_SHARE: &str = "/usr/share";
151+
pub const OPT_HOMEBREW_BIN: &str = "/opt/homebrew/bin";
152+
}
153+
137154
#[cfg(test)]
138155
mod tests {
139156
use time::OffsetDateTime;

0 commit comments

Comments
 (0)