Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion crates/chat-cli/src/cli/chat/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -628,9 +628,11 @@ async fn load_global_config(os: &Os) -> Result<ContextConfig> {
Ok(config)
} else {
// Return default global configuration with predefined paths
use crate::util::paths::workspace;

Ok(ContextConfig {
paths: vec![
".amazonq/rules/**/*.md".to_string(),
workspace::RULES_PATTERN.to_string(),
"README.md".to_string(),
AMAZONQ_FILENAME.to_string(),
],
Expand Down
17 changes: 9 additions & 8 deletions crates/chat-cli/src/cli/chat/tool_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ use crate::mcp_client::{
};
use crate::os::Os;
use crate::telemetry::TelemetryThread;
use crate::util::directories::home_dir;
use crate::util::paths::PathResolver;

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

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

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

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

impl McpServerConfig {
pub async fn load_config(stderr: &mut impl Write) -> eyre::Result<Self> {
let mut cwd = std::env::current_dir()?;
cwd.push(".amazonq/mcp.json");
let expanded_path = shellexpand::tilde("~/.aws/amazonq/mcp.json");
let global_path = PathBuf::from(expanded_path.as_ref() as &str);
let os = Os::new().await?;
let resolver = PathResolver::new(&os);
let workspace_path = resolver.workspace().mcp_config()?;
let global_path = resolver.global().mcp_config()?;

let global_buf = tokio::fs::read(global_path).await.ok();
let local_buf = tokio::fs::read(cwd).await.ok();
let local_buf = tokio::fs::read(workspace_path).await.ok();
let conf = match (global_buf, local_buf) {
(Some(global_buf), Some(local_buf)) => {
let mut global_conf = Self::from_slice(&global_buf, stderr, "global")?;
Expand Down
40 changes: 22 additions & 18 deletions crates/chat-cli/src/util/directories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ use std::path::PathBuf;
use thiserror::Error;

use crate::os::Os;
use crate::util::paths::PathResolver;

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

/// The q data directory
///
/// - Linux: `$XDG_DATA_HOME/amazon-q` or `$HOME/.local/share/amazon-q`
/// - MacOS: `$HOME/Library/Application Support/amazon-q`
pub fn fig_data_dir() -> Result<PathBuf> {
Ok(dirs::data_local_dir()
.ok_or(DirectoryError::NoHomeDirectory)?
.join("amazon-q"))
}

/// Get the macos tempdir from the `confstr` function
///
/// See: <https://man7.org/linux/man-pages/man3/confstr.3.html>
Expand Down Expand Up @@ -124,29 +117,38 @@ pub fn logs_dir() -> Result<PathBuf> {
if #[cfg(unix)] {
Ok(runtime_dir()?.join("qlog"))
} else if #[cfg(windows)] {
Ok(std::env::temp_dir().join("amazon-q").join("logs"))
use crate::util::paths::application::DATA_DIR_NAME;
Ok(std::env::temp_dir().join(DATA_DIR_NAME).join("logs"))
}
}
}

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

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

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

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

#[cfg(test)]
Expand Down Expand Up @@ -237,9 +239,11 @@ mod tests {

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

#[test]
Expand Down
1 change: 1 addition & 0 deletions crates/chat-cli/src/util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub mod consts;
pub mod directories;
pub mod knowledge_store;
pub mod open;
pub mod paths;
pub mod process;
pub mod spinner;
pub mod system_info;
Expand Down
156 changes: 156 additions & 0 deletions crates/chat-cli/src/util/paths.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
//! Hierarchical path management for the application

use std::path::PathBuf;

use crate::os::Os;

#[derive(Debug, thiserror::Error)]
pub enum DirectoryError {
#[error("home directory not found")]
NoHomeDirectory,
#[error("IO Error: {0}")]
Io(#[from] std::io::Error),
}

pub mod workspace {
//! Project-level paths (relative to current working directory)
pub const MCP_CONFIG: &str = ".amazonq/mcp.json";
pub const RULES_PATTERN: &str = ".amazonq/rules/**/*.md";
}

pub mod global {
//! User-level paths (relative to home directory)
pub const MCP_CONFIG: &str = ".aws/amazonq/mcp.json";
pub const GLOBAL_CONTEXT: &str = ".aws/amazonq/global_context.json";
pub const PROFILES_DIR: &str = ".aws/amazonq/profiles";
}

pub mod application {
//! Application data paths (system-specific)
#[cfg(unix)]
pub const DATA_DIR_NAME: &str = "amazon-q";
#[cfg(windows)]
pub const DATA_DIR_NAME: &str = "AmazonQ";
pub const SETTINGS_FILE: &str = "settings.json";
pub const DATABASE_FILE: &str = "data.sqlite3";
}

type Result<T, E = DirectoryError> = std::result::Result<T, E>;

/// The directory of the users home
/// - Linux: /home/Alice
/// - MacOS: /Users/Alice
/// - Windows: C:\Users\Alice
pub fn home_dir(#[cfg_attr(windows, allow(unused_variables))] os: &Os) -> Result<PathBuf> {
#[cfg(unix)]
match cfg!(test) {
true => os
.env
.get("HOME")
.map_err(|_err| DirectoryError::NoHomeDirectory)
.and_then(|h| {
if h.is_empty() {
Err(DirectoryError::NoHomeDirectory)
} else {
Ok(h)
}
})
.map(PathBuf::from)
.map(|p| os.fs.chroot_path(p)),
false => dirs::home_dir().ok_or(DirectoryError::NoHomeDirectory),
}

#[cfg(windows)]
match cfg!(test) {
true => os
.env
.get("USERPROFILE")
.map_err(|_err| DirectoryError::NoHomeDirectory)
.and_then(|h| {
if h.is_empty() {
Err(DirectoryError::NoHomeDirectory)
} else {
Ok(h)
}
})
.map(PathBuf::from)
.map(|p| os.fs.chroot_path(p)),
false => dirs::home_dir().ok_or(DirectoryError::NoHomeDirectory),
}
}

/// The application data directory
/// - Linux: `$XDG_DATA_HOME/{data_dir}` or `$HOME/.local/share/{data_dir}`
/// - MacOS: `$HOME/Library/Application Support/{data_dir}`
/// - Windows: `%LOCALAPPDATA%\{data_dir}`
pub fn app_data_dir() -> Result<PathBuf> {
Ok(dirs::data_local_dir()
.ok_or(DirectoryError::NoHomeDirectory)?
.join(application::DATA_DIR_NAME))
}

/// Path resolver with hierarchy-aware methods
pub struct PathResolver<'a> {
os: &'a Os,
}

impl<'a> PathResolver<'a> {
pub fn new(os: &'a Os) -> Self {
Self { os }
}

/// Get workspace-scoped path resolver
pub fn workspace(&self) -> WorkspacePaths<'_> {
WorkspacePaths { os: self.os }
}

/// Get global-scoped path resolver
pub fn global(&self) -> GlobalPaths<'_> {
GlobalPaths { os: self.os }
}
}

/// Workspace-scoped path methods
pub struct WorkspacePaths<'a> {
os: &'a Os,
}

impl<'a> WorkspacePaths<'a> {
pub fn mcp_config(&self) -> Result<PathBuf> {
Ok(self.os.env.current_dir()?.join(workspace::MCP_CONFIG))
}
}

/// Global-scoped path methods
pub struct GlobalPaths<'a> {
os: &'a Os,
}

impl<'a> GlobalPaths<'a> {
pub fn mcp_config(&self) -> Result<PathBuf> {
Ok(home_dir(self.os)?.join(global::MCP_CONFIG))
}

pub fn global_context(&self) -> Result<PathBuf> {
Ok(home_dir(self.os)?.join(global::GLOBAL_CONTEXT))
}

pub fn profiles_dir(&self) -> Result<PathBuf> {
Ok(home_dir(self.os)?.join(global::PROFILES_DIR))
}
}

/// Application path static methods
pub struct ApplicationPaths;

impl ApplicationPaths {
/// Static method for settings path (to avoid circular dependency)
pub fn settings_path_static() -> Result<PathBuf> {
Ok(app_data_dir()?.join(application::SETTINGS_FILE))
}

/// Static method for database path (to avoid circular dependency)
pub fn database_path_static() -> Result<PathBuf> {
Ok(app_data_dir()?.join(application::DATABASE_FILE))
}
}
3 changes: 2 additions & 1 deletion crates/fig_install/src/macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use std::path::{
use fig_util::consts::{
APP_BUNDLE_ID,
CLI_BINARY_NAME,
system_paths,
};
use fig_util::macos::BUNDLE_CONTENTS_MACOS_PATH;
use fig_util::{
Expand Down Expand Up @@ -187,7 +188,7 @@ pub(crate) async fn update(
let installed_app_path = if same_bundle_name {
fig_util::app_bundle_path()
} else {
Path::new("/Applications").join(app_name)
Path::new(system_paths::APPLICATIONS_DIR).join(app_name)
};

let installed_app_path_cstr = CString::new(installed_app_path.as_os_str().as_bytes())?;
Expand Down
17 changes: 17 additions & 0 deletions crates/fig_util/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ pub const PRODUCT_NAME: &str = "Amazon Q";

pub const RUNTIME_DIR_NAME: &str = "cwrun";

/// Data directory name used in paths like ~/.local/share/{DATA_DIR_NAME}
#[cfg(unix)]
pub const DATA_DIR_NAME: &str = "amazon-q";
#[cfg(windows)]
pub const DATA_DIR_NAME: &str = "AmazonQ";

/// Backup directory name
pub const BACKUP_DIR_NAME: &str = ".amazon-q.dotfiles.bak";

// These are the old "CodeWhisperer" branding, used anywhere we will not update to Amazon Q
pub const OLD_PRODUCT_NAME: &str = "CodeWhisperer";
pub const OLD_CLI_BINARY_NAMES: &[&str] = &["cw"];
Expand Down Expand Up @@ -134,6 +143,14 @@ pub mod env_var {
}
}

pub mod system_paths {
/// System installation paths
pub const APPLICATIONS_DIR: &str = "/Applications";
pub const USR_LOCAL_BIN: &str = "/usr/local/bin";
pub const USR_SHARE: &str = "/usr/share";
pub const OPT_HOMEBREW_BIN: &str = "/opt/homebrew/bin";
}

#[cfg(test)]
mod tests {
use time::OffsetDateTime;
Expand Down
Loading
Loading