Skip to content

Commit fa79d22

Browse files
dk19ykkashilk
authored andcommitted
refactor: consolidate paths
1 parent cc40034 commit fa79d22

File tree

10 files changed

+214
-33
lines changed

10 files changed

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

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)