Skip to content

Commit 7233186

Browse files
authored
refactor: migrate from directories to hierarchical paths module (#3309)
- Replace directories module with new paths module using PathResolver pattern - Add workspace and global path scopes with clear separation of concerns - Implement static path methods to avoid circular dependencies in Database/Settings - Centralize all path constants and patterns in dedicated modules This refactoring improves path management consistency and provides a cleaner, more maintainable architecture for handling file system paths throughout the application.
1 parent 13babcc commit 7233186

File tree

28 files changed

+516
-697
lines changed

28 files changed

+516
-697
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,4 @@ book/
4949
.env*
5050

5151
run-build.sh
52+
.amazonq/

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ pub enum AuthError {
3131
#[error(transparent)]
3232
TimeComponentRange(#[from] time::error::ComponentRange),
3333
#[error(transparent)]
34-
Directories(#[from] crate::util::directories::DirectoryError),
34+
Directories(#[from] crate::util::paths::DirectoryError),
3535
#[error(transparent)]
3636
SerdeJson(#[from] serde_json::Error),
3737
#[error(transparent)]

crates/chat-cli/src/cli/agent/legacy/mod.rs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use crate::cli::agent::hook::Hook;
1919
use crate::cli::agent::legacy::context::LegacyContextConfig;
2020
use crate::os::Os;
2121
use crate::theme::StyledText;
22-
use crate::util::directories;
22+
use crate::util::paths::PathResolver;
2323

2424
/// Performs the migration from legacy profile configuration to agent configuration if it hasn't
2525
/// already been done.
@@ -32,15 +32,16 @@ pub async fn migrate(os: &mut Os, force: bool) -> eyre::Result<Option<Vec<Agent>
3232
return Ok(None);
3333
}
3434

35-
let legacy_global_context_path = directories::chat_global_context_path(os)?;
35+
let resolver = PathResolver::new(os);
36+
let legacy_global_context_path = resolver.global().global_context()?;
3637
let legacy_global_context: Option<LegacyContextConfig> = 'global: {
3738
let Ok(content) = os.fs.read(&legacy_global_context_path).await else {
3839
break 'global None;
3940
};
4041
serde_json::from_slice::<LegacyContextConfig>(&content).ok()
4142
};
4243

43-
let legacy_profile_path = directories::chat_profiles_dir(os)?;
44+
let legacy_profile_path = resolver.global().profiles_dir()?;
4445
let mut legacy_profiles: HashMap<String, LegacyContextConfig> = 'profiles: {
4546
let mut profiles = HashMap::<String, LegacyContextConfig>::new();
4647
let Ok(mut read_dir) = os.fs.read_dir(&legacy_profile_path).await else {
@@ -78,7 +79,7 @@ pub async fn migrate(os: &mut Os, force: bool) -> eyre::Result<Option<Vec<Agent>
7879
};
7980

8081
let mcp_servers = {
81-
let config_path = directories::chat_legacy_global_mcp_config(os)?;
82+
let config_path = resolver.global().mcp_config()?;
8283
if os.fs.exists(&config_path) {
8384
match McpServerConfig::load_from_file(os, config_path).await {
8485
Ok(mut config) => {
@@ -145,7 +146,12 @@ pub async fn migrate(os: &mut Os, force: bool) -> eyre::Result<Option<Vec<Agent>
145146
new_agents.push(Agent {
146147
name: LEGACY_GLOBAL_AGENT_NAME.to_string(),
147148
description: Some(DEFAULT_DESC.to_string()),
148-
path: Some(directories::chat_global_agent_path(os)?.join(format!("{LEGACY_GLOBAL_AGENT_NAME}.json"))),
149+
path: Some(
150+
resolver
151+
.global()
152+
.agents_dir()?
153+
.join(format!("{LEGACY_GLOBAL_AGENT_NAME}.json")),
154+
),
149155
resources: context.paths.iter().map(|p| format!("file://{p}").into()).collect(),
150156
hooks: HashMap::from([
151157
(
@@ -168,7 +174,7 @@ pub async fn migrate(os: &mut Os, force: bool) -> eyre::Result<Option<Vec<Agent>
168174
});
169175
}
170176

171-
let global_agent_path = directories::chat_global_agent_path(os)?;
177+
let global_agent_path = resolver.global().ensure_agents_dir().await?;
172178

173179
// Migration of profile context
174180
for (profile_name, context) in legacy_profiles.drain() {
@@ -205,10 +211,6 @@ pub async fn migrate(os: &mut Os, force: bool) -> eyre::Result<Option<Vec<Agent>
205211
});
206212
}
207213

208-
if !os.fs.exists(&global_agent_path) {
209-
os.fs.create_dir_all(&global_agent_path).await?;
210-
}
211-
212214
for agent in &mut new_agents {
213215
let content = agent.to_str_pretty()?;
214216
if let Some(path) = agent.path.as_ref() {

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

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,12 @@ use crate::cli::agent::hook::{
6363
use crate::database::settings::Setting;
6464
use crate::os::Os;
6565
use crate::theme::StyledText;
66+
use crate::util::paths::PathResolver;
6667
use crate::util::{
6768
self,
6869
MCP_SERVER_TOOL_DELIMITER,
69-
directories,
7070
file_uri,
71+
paths,
7172
};
7273

7374
pub const DEFAULT_AGENT_NAME: &str = "q_cli_default";
@@ -84,7 +85,7 @@ pub enum AgentConfigError {
8485
error: Box<jsonschema::ValidationError<'static>>,
8586
},
8687
#[error("Encountered directory error: {0}")]
87-
Directories(#[from] util::directories::DirectoryError),
88+
Directories(#[from] util::paths::DirectoryError),
8889
#[error("Encountered io error: {0}")]
8990
Io(#[from] std::io::Error),
9091
#[error("Failed to parse legacy mcp config: {0}")]
@@ -165,7 +166,7 @@ pub struct Agent {
165166
#[serde(default)]
166167
#[schemars(schema_with = "tool_settings_schema")]
167168
pub tools_settings: HashMap<ToolSettingTarget, serde_json::Value>,
168-
/// Whether or not to include the legacy ~/.aws/amazonq/mcp.json in the agent
169+
/// Whether or not to include the legacy global MCP configuration in the agent
169170
/// You can reference tools brought in by these servers as just as you would with the servers
170171
/// you configure in the mcpServers field in this config
171172
#[serde(default)]
@@ -193,15 +194,12 @@ impl Default for Agent {
193194
set.extend(default_approve);
194195
set
195196
},
196-
resources: vec![
197-
"file://AmazonQ.md",
198-
"file://AGENTS.md",
199-
"file://README.md",
200-
"file://.amazonq/rules/**/*.md",
201-
]
202-
.into_iter()
203-
.map(Into::into)
204-
.collect::<Vec<_>>(),
197+
resources: {
198+
let mut resources = Vec::new();
199+
resources.extend(paths::workspace::DEFAULT_AGENT_RESOURCES.iter().map(|&s| s.into()));
200+
resources.push(format!("file://{}", paths::workspace::RULES_PATTERN).into());
201+
resources
202+
},
205203
hooks: Default::default(),
206204
tools_settings: Default::default(),
207205
use_legacy_mcp_json: true,
@@ -342,14 +340,15 @@ impl Agent {
342340
/// Retrieves an agent by name. It does so via first seeking the given agent under local dir,
343341
/// and falling back to global dir if it does not exist in local.
344342
pub async fn get_agent_by_name(os: &Os, agent_name: &str) -> eyre::Result<(Agent, PathBuf)> {
343+
let resolver = PathResolver::new(os);
345344
let config_path: Result<PathBuf, PathBuf> = 'config: {
346345
// local first, and then fall back to looking at global
347-
let local_config_dir = directories::chat_local_agent_dir(os)?.join(format!("{agent_name}.json"));
346+
let local_config_dir = resolver.workspace().agents_dir()?.join(format!("{agent_name}.json"));
348347
if os.fs.exists(&local_config_dir) {
349348
break 'config Ok(local_config_dir);
350349
}
351350

352-
let global_config_dir = directories::chat_global_agent_path(os)?.join(format!("{agent_name}.json"));
351+
let global_config_dir = resolver.global().agents_dir()?.join(format!("{agent_name}.json"));
353352
if os.fs.exists(&global_config_dir) {
354353
break 'config Ok(global_config_dir);
355354
}
@@ -542,20 +541,21 @@ impl Agents {
542541
vec![]
543542
};
544543

544+
let resolver = PathResolver::new(os);
545545
let mut global_mcp_config = None::<McpServerConfig>;
546546

547547
let mut local_agents = 'local: {
548548
// We could be launching from the home dir, in which case the global and local agents
549549
// are the same set of agents. If that is the case, we simply skip this.
550-
match (std::env::current_dir(), directories::home_dir(os)) {
550+
match (std::env::current_dir(), paths::home_dir(os)) {
551551
(Ok(cwd), Ok(home_dir)) if cwd == home_dir => break 'local Vec::<Agent>::new(),
552552
_ => {
553553
// noop, we keep going with the extraction of local agents (even if we have an
554554
// error retrieving cwd or home_dir)
555555
},
556556
}
557557

558-
let Ok(path) = directories::chat_local_agent_dir(os) else {
558+
let Ok(path) = resolver.workspace().agents_dir() else {
559559
break 'local Vec::<Agent>::new();
560560
};
561561
let Ok(files) = os.fs.read_dir(path).await else {
@@ -585,7 +585,7 @@ impl Agents {
585585
};
586586

587587
let mut global_agents = 'global: {
588-
let Ok(path) = directories::chat_global_agent_path(os) else {
588+
let Ok(path) = resolver.global().agents_dir() else {
589589
break 'global Vec::<Agent>::new();
590590
};
591591
let files = match os.fs.read_dir(&path).await {
@@ -629,10 +629,11 @@ impl Agents {
629629
// there.
630630
// Note that this config is not what q chat uses. It merely serves as an example.
631631
'example_config: {
632-
let Ok(path) = directories::example_agent_config(os) else {
632+
let Ok(agents_dir) = resolver.global().agents_dir() else {
633633
error!("Error obtaining example agent path.");
634634
break 'example_config;
635635
};
636+
let path = agents_dir.join("agent_config.json.example");
636637
if os.fs.exists(&path) {
637638
break 'example_config;
638639
}
@@ -744,7 +745,7 @@ impl Agents {
744745
if mcp_enabled {
745746
'load_legacy_mcp_json: {
746747
if global_mcp_config.is_none() {
747-
let Ok(global_mcp_path) = directories::chat_legacy_global_mcp_config(os) else {
748+
let Ok(global_mcp_path) = resolver.global().mcp_config() else {
748749
tracing::error!("Error obtaining legacy mcp json path. Skipping");
749750
break 'load_legacy_mcp_json;
750751
};
@@ -906,7 +907,8 @@ async fn load_agents_from_entries(
906907
/// Loads legacy mcp config by combining workspace and global config.
907908
/// In case of a server naming conflict, the workspace config is prioritized.
908909
async fn load_legacy_mcp_config(os: &Os) -> eyre::Result<Option<McpServerConfig>> {
909-
let global_mcp_path = directories::chat_legacy_global_mcp_config(os)?;
910+
let resolver = PathResolver::new(os);
911+
let global_mcp_path = resolver.global().mcp_config()?;
910912
let global_mcp_config = match McpServerConfig::load_from_file(os, global_mcp_path).await {
911913
Ok(config) => Some(config),
912914
Err(e) => {
@@ -915,7 +917,7 @@ async fn load_legacy_mcp_config(os: &Os) -> eyre::Result<Option<McpServerConfig>
915917
},
916918
};
917919

918-
let workspace_mcp_path = directories::chat_legacy_workspace_mcp_config(os)?;
920+
let workspace_mcp_path = resolver.workspace().mcp_config()?;
919921
let workspace_mcp_config = match McpServerConfig::load_from_file(os, workspace_mcp_path).await {
920922
Ok(config) => Some(config),
921923
Err(e) => {

crates/chat-cli/src/cli/agent/root_command_args.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ use super::{
2525
use crate::database::settings::Setting;
2626
use crate::os::Os;
2727
use crate::theme::StyledText;
28-
use crate::util::directories;
28+
use crate::util::paths;
29+
use crate::util::paths::PathResolver;
2930

3031
#[derive(Clone, Debug, Subcommand, PartialEq, Eq)]
3132
pub enum AgentSubcommands {
@@ -336,9 +337,9 @@ pub async fn create_agent(
336337
bail!("Path must be a directory");
337338
}
338339

339-
directories::agent_config_dir(path)?
340+
path.join(paths::workspace::AGENTS_DIR)
340341
} else {
341-
directories::chat_global_agent_path(os)?
342+
PathResolver::new(os).global().agents_dir()?
342343
};
343344

344345
if let Some((name, _)) = agents.agents.iter().find(|(agent_name, agent)| {

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ use crate::cli::experiment::experiment_manager::{
2828
};
2929
use crate::os::Os;
3030
use crate::theme::StyledText;
31-
use crate::util::directories::get_shadow_repo_dir;
31+
use crate::util::paths::PathResolver;
3232

3333
#[derive(Debug, PartialEq, Subcommand)]
3434
pub enum CheckpointSubcommand {
@@ -134,8 +134,11 @@ impl CheckpointSubcommand {
134134
StyledText::reset(),
135135
)?;
136136
} else {
137-
let path = get_shadow_repo_dir(os, session.conversation.conversation_id().to_string())
138-
.map_err(|e| ChatError::Custom(e.to_string().into()))?;
137+
let path = PathResolver::new(os)
138+
.global()
139+
.shadow_repo_dir()
140+
.map_err(|e| ChatError::Custom(e.to_string().into()))?
141+
.join(session.conversation.conversation_id());
139142

140143
let start = std::time::Instant::now();
141144
session.conversation.checkpoint_manager = Some(

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use crate::cli::chat::{
1919
ChatState,
2020
};
2121
use crate::theme::StyledText;
22-
use crate::util::directories::logs_dir;
22+
use crate::util::paths::logs_dir;
2323

2424
/// Arguments for the logdump command that collects logs for support investigation
2525
#[derive(Debug, PartialEq, Args)]

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

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,8 @@ use crate::cli::chat::{
4141
use crate::database::settings::Setting;
4242
use crate::os::Os;
4343
use crate::theme::StyledText;
44-
use crate::util::directories::chat_global_agent_path;
45-
use crate::util::{
46-
NullWriter,
47-
directories,
48-
};
44+
use crate::util::NullWriter;
45+
use crate::util::paths::PathResolver;
4946

5047
#[deny(missing_docs)]
5148
#[derive(Debug, PartialEq, Subcommand)]
@@ -54,7 +51,7 @@ use crate::util::{
5451
5552
Notes
5653
• Launch q chat with a specific agent with --agent
57-
• Construct an agent under ~/.aws/amazonq/cli-agents/ (accessible globally) or cwd/.amazonq/cli-agents (accessible in workspace)
54+
• Construct an agent in the global agents directory (accessible globally) or workspace agents directory (accessible in workspace)
5855
• See example config under global directory
5956
• Set default agent to assume with settings by running \"q settings chat.defaultAgent agent_name\"
6057
• Each agent maintains its own set of context and customizations"
@@ -395,7 +392,7 @@ impl AgentSubcommand {
395392
// switch / create profile after a session has started.
396393
// TODO: perhaps revive this after we have a decision on profile create /
397394
// switch
398-
let global_path = if let Ok(path) = chat_global_agent_path(os) {
395+
let global_path = if let Ok(path) = PathResolver::new(os).global().agents_dir() {
399396
path.to_str().unwrap_or("default global agent path").to_string()
400397
} else {
401398
"default global agent path".to_string()
@@ -537,8 +534,10 @@ pub async fn get_all_available_mcp_servers(os: &mut Os) -> Result<Vec<McpServerI
537534
}
538535
}
539536

537+
let resolver = PathResolver::new(os);
538+
540539
// 2. Load from workspace legacy config (medium priority)
541-
if let Ok(workspace_path) = directories::chat_legacy_workspace_mcp_config(os) {
540+
if let Ok(workspace_path) = resolver.workspace().mcp_config() {
542541
if let Ok(workspace_config) = McpServerConfig::load_from_file(os, workspace_path).await {
543542
for (server_name, server_config) in workspace_config.mcp_servers {
544543
if !servers.values().any(|s| s.config.command == server_config.command) {
@@ -552,7 +551,7 @@ pub async fn get_all_available_mcp_servers(os: &mut Os) -> Result<Vec<McpServerI
552551
}
553552

554553
// 3. Load from global legacy config (lowest priority)
555-
if let Ok(global_path) = directories::chat_legacy_global_mcp_config(os) {
554+
if let Ok(global_path) = resolver.global().mcp_config() {
556555
if let Ok(global_config) = McpServerConfig::load_from_file(os, global_path).await {
557556
for (server_name, server_config) in global_config.mcp_servers {
558557
if !servers.values().any(|s| s.config.command == server_config.command) {

0 commit comments

Comments
 (0)