Skip to content

Commit 1d0b974

Browse files
committed
refactor(paths): Consolodates with hierarchical path util
Replace hardcoded path strings with a structured path management system that organizes paths by scope (workspace, global, application) and provides type-safe path resolution. Key changes: - Add new paths.rs module with PathResolver and scoped path builders - Replace hardcoded strings with constants in workspace/global/application modules - Migrate existing path functions to use new system while maintaining compatibility - Add system_paths module for common system directories - Update MCP config loading to use new path resolution - Maintain backward compatibility with existing directory functions
1 parent 058bc6e commit 1d0b974

File tree

10 files changed

+213
-33
lines changed

10 files changed

+213
-33
lines changed

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

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

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: 20 additions & 17 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>
@@ -131,22 +124,30 @@ pub fn logs_dir() -> Result<PathBuf> {
131124

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

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

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

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

152153
#[cfg(test)]
@@ -237,9 +238,11 @@ mod tests {
237238

238239
#[test]
239240
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");
241+
let app_data_dir =
242+
|| crate::util::paths::app_data_dir().map_err(|e| DirectoryError::Io(std::io::Error::other(e)));
243+
linux!(app_data_dir(), @"$HOME/.local/share/amazon-q");
244+
macos!(app_data_dir(), @"$HOME/Library/Application Support/amazon-q");
245+
windows!(app_data_dir(), @r"C:\Users\$USER\AppData\Local\AmazonQ");
243246
}
244247

245248
#[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: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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+
pub const DATA_DIR_NAME: &str = "amazon-q";
31+
pub const SETTINGS_FILE: &str = "settings.json";
32+
pub const DATABASE_FILE: &str = "data.sqlite3";
33+
}
34+
35+
type Result<T, E = DirectoryError> = std::result::Result<T, E>;
36+
37+
/// The directory of the users home
38+
/// - Linux: /home/Alice
39+
/// - MacOS: /Users/Alice
40+
/// - Windows: C:\Users\Alice
41+
pub fn home_dir(#[cfg_attr(windows, allow(unused_variables))] os: &Os) -> Result<PathBuf> {
42+
#[cfg(unix)]
43+
match cfg!(test) {
44+
true => os
45+
.env
46+
.get("HOME")
47+
.map_err(|_err| DirectoryError::NoHomeDirectory)
48+
.and_then(|h| {
49+
if h.is_empty() {
50+
Err(DirectoryError::NoHomeDirectory)
51+
} else {
52+
Ok(h)
53+
}
54+
})
55+
.map(PathBuf::from)
56+
.map(|p| os.fs.chroot_path(p)),
57+
false => dirs::home_dir().ok_or(DirectoryError::NoHomeDirectory),
58+
}
59+
60+
#[cfg(windows)]
61+
match cfg!(test) {
62+
true => os
63+
.env
64+
.get("USERPROFILE")
65+
.map_err(|_err| DirectoryError::NoHomeDirectory)
66+
.and_then(|h| {
67+
if h.is_empty() {
68+
Err(DirectoryError::NoHomeDirectory)
69+
} else {
70+
Ok(h)
71+
}
72+
})
73+
.map(PathBuf::from)
74+
.map(|p| os.fs.chroot_path(p)),
75+
false => dirs::home_dir().ok_or(DirectoryError::NoHomeDirectory),
76+
}
77+
}
78+
79+
/// The application data directory
80+
/// - Linux: `$XDG_DATA_HOME/amazon-q` or `$HOME/.local/share/amazon-q`
81+
/// - MacOS: `$HOME/Library/Application Support/amazon-q`
82+
/// - Windows: `%LOCALAPPDATA%\AmazonQ`
83+
pub fn app_data_dir() -> Result<PathBuf> {
84+
#[cfg(unix)]
85+
{
86+
Ok(dirs::data_local_dir()
87+
.ok_or(DirectoryError::NoHomeDirectory)?
88+
.join(application::DATA_DIR_NAME))
89+
}
90+
#[cfg(windows)]
91+
{
92+
Ok(dirs::data_local_dir()
93+
.ok_or(DirectoryError::NoHomeDirectory)?
94+
.join("AmazonQ"))
95+
}
96+
}
97+
98+
/// Path resolver with hierarchy-aware methods
99+
pub struct PathResolver<'a> {
100+
os: &'a Os,
101+
}
102+
103+
impl<'a> PathResolver<'a> {
104+
pub fn new(os: &'a Os) -> Self {
105+
Self { os }
106+
}
107+
108+
/// Get workspace-scoped path resolver
109+
pub fn workspace(&self) -> WorkspacePaths<'_> {
110+
WorkspacePaths { os: self.os }
111+
}
112+
113+
/// Get global-scoped path resolver
114+
pub fn global(&self) -> GlobalPaths<'_> {
115+
GlobalPaths { os: self.os }
116+
}
117+
}
118+
119+
/// Workspace-scoped path methods
120+
pub struct WorkspacePaths<'a> {
121+
os: &'a Os,
122+
}
123+
124+
impl<'a> WorkspacePaths<'a> {
125+
pub fn mcp_config(&self) -> Result<PathBuf> {
126+
Ok(self.os.env.current_dir()?.join(workspace::MCP_CONFIG))
127+
}
128+
}
129+
130+
/// Global-scoped path methods
131+
pub struct GlobalPaths<'a> {
132+
os: &'a Os,
133+
}
134+
135+
impl<'a> GlobalPaths<'a> {
136+
pub fn mcp_config(&self) -> Result<PathBuf> {
137+
Ok(home_dir(self.os)?.join(global::MCP_CONFIG))
138+
}
139+
140+
pub fn global_context(&self) -> Result<PathBuf> {
141+
Ok(home_dir(self.os)?.join(global::GLOBAL_CONTEXT))
142+
}
143+
144+
pub fn profiles_dir(&self) -> Result<PathBuf> {
145+
Ok(home_dir(self.os)?.join(global::PROFILES_DIR))
146+
}
147+
}
148+
149+
/// Application path static methods
150+
pub struct ApplicationPaths;
151+
152+
impl ApplicationPaths {
153+
/// Static method for settings path (to avoid circular dependency)
154+
pub fn settings_path_static() -> Result<PathBuf> {
155+
Ok(app_data_dir()?.join(application::SETTINGS_FILE))
156+
}
157+
158+
/// Static method for database path (to avoid circular dependency)
159+
pub fn database_path_static() -> Result<PathBuf> {
160+
Ok(app_data_dir()?.join(application::DATABASE_FILE))
161+
}
162+
}

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: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,14 @@ pub mod env_var {
134134
}
135135
}
136136

137+
pub mod system_paths {
138+
/// System installation paths
139+
pub const APPLICATIONS_DIR: &str = "/Applications";
140+
pub const USR_LOCAL_BIN: &str = "/usr/local/bin";
141+
pub const USR_SHARE: &str = "/usr/share";
142+
pub const OPT_HOMEBREW_BIN: &str = "/opt/homebrew/bin";
143+
}
144+
137145
#[cfg(test)]
138146
mod tests {
139147
use time::OffsetDateTime;

crates/fig_util/src/directories.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,7 @@ pub fn figterm_socket_path(session_id: impl Display) -> Result<PathBuf> {
403403
pub fn resources_path() -> Result<PathBuf> {
404404
cfg_if::cfg_if! {
405405
if #[cfg(all(unix, not(target_os = "macos")))] {
406-
Ok(std::path::Path::new("/usr/share/fig").into())
406+
Ok(std::path::Path::new("/usr/share").join(PACKAGE_NAME))
407407
} else if #[cfg(target_os = "macos")] {
408408
Ok(crate::app_bundle_path().join(crate::macos::BUNDLE_CONTENTS_RESOURCE_PATH))
409409
} else if #[cfg(windows)] {

crates/fig_util/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ fn app_bundle_path_opt() -> Option<PathBuf> {
126126

127127
#[must_use]
128128
pub fn app_bundle_path() -> PathBuf {
129-
app_bundle_path_opt().unwrap_or_else(|| Path::new("/Applications").join(APP_BUNDLE_NAME))
129+
app_bundle_path_opt().unwrap_or_else(|| Path::new(consts::system_paths::APPLICATIONS_DIR).join(APP_BUNDLE_NAME))
130130
}
131131

132132
pub fn partitioned_compare(lhs: &str, rhs: &str, by: char) -> Ordering {

crates/q_cli/src/cli/doctor/mod.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ use fig_util::{
9393
Shell,
9494
Terminal,
9595
directories,
96+
system_paths,
9697
};
9798
use futures::FutureExt;
9899
use futures::future::BoxFuture;
@@ -1144,7 +1145,8 @@ impl DoctorCheck<DiagnosticsResponse> for BundlePathCheck {
11441145

11451146
async fn check(&self, diagnostics: &DiagnosticsResponse) -> Result<(), DoctorError> {
11461147
let path = diagnostics.path_to_bundle.clone();
1147-
if path.contains(&format!("/Applications/{APP_BUNDLE_NAME}")) || path.contains(".toolbox") {
1148+
if path.contains(&format!("{}/{APP_BUNDLE_NAME}", system_paths::APPLICATIONS_DIR)) || path.contains(".toolbox")
1149+
{
11481150
Ok(())
11491151
} else if path.contains(&format!("/Build/Products/Debug/{APP_BUNDLE_NAME}")) {
11501152
Err(DoctorError::Warning(
@@ -1154,7 +1156,7 @@ impl DoctorCheck<DiagnosticsResponse> for BundlePathCheck {
11541156
Err(DoctorError::Error {
11551157
reason: format!("App is installed in {}", path.bold()).into(),
11561158
info: vec![
1157-
"You need to install the app into /Applications.".into(),
1159+
format!("You need to install the app into {}.", system_paths::APPLICATIONS_DIR).into(),
11581160
"To fix: uninstall and reinstall in the correct location.".into(),
11591161
"Remember to drag the installed app into the Applications folder.".into(),
11601162
],
@@ -1251,8 +1253,8 @@ impl DoctorCheck<DiagnosticsResponse> for CliPathCheck {
12511253
.join(CLI_BINARY_NAME);
12521254

12531255
if path == local_bin_path
1254-
|| path == Path::new("/usr/local/bin").join(CLI_BINARY_NAME)
1255-
|| path == Path::new("/opt/homebrew/bin").join(CLI_BINARY_NAME)
1256+
|| path == Path::new(system_paths::USR_LOCAL_BIN).join(CLI_BINARY_NAME)
1257+
|| path == Path::new(system_paths::OPT_HOMEBREW_BIN).join(CLI_BINARY_NAME)
12561258
{
12571259
Ok(())
12581260
} else if path.ends_with(Path::new("target/debug").join(CLI_BINARY_NAME))

0 commit comments

Comments
 (0)