Skip to content

Commit 9cf31dc

Browse files
committed
refactor: migrate from directories to hierarchical paths module
- 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 a275492 commit 9cf31dc

File tree

28 files changed

+480
-698
lines changed

28 files changed

+480
-698
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: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ pub mod hooks;
33

44
use std::collections::HashMap;
55

6+
use crate::util::paths::PathResolver;
7+
68
use dialoguer::Select;
79
use eyre::bail;
810
use tracing::{
@@ -19,7 +21,7 @@ use crate::cli::agent::hook::Hook;
1921
use crate::cli::agent::legacy::context::LegacyContextConfig;
2022
use crate::os::Os;
2123
use crate::theme::StyledText;
22-
use crate::util::directories;
24+
2325

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

35-
let legacy_global_context_path = directories::chat_global_context_path(os)?;
37+
let legacy_global_context_path = PathResolver::new(os).global().global_context()?;
3638
let legacy_global_context: Option<LegacyContextConfig> = 'global: {
3739
let Ok(content) = os.fs.read(&legacy_global_context_path).await else {
3840
break 'global None;
3941
};
4042
serde_json::from_slice::<LegacyContextConfig>(&content).ok()
4143
};
4244

43-
let legacy_profile_path = directories::chat_profiles_dir(os)?;
45+
let legacy_profile_path = PathResolver::new(os).global().profiles_dir()?;
4446
let mut legacy_profiles: HashMap<String, LegacyContextConfig> = 'profiles: {
4547
let mut profiles = HashMap::<String, LegacyContextConfig>::new();
4648
let Ok(mut read_dir) = os.fs.read_dir(&legacy_profile_path).await else {
@@ -78,7 +80,7 @@ pub async fn migrate(os: &mut Os, force: bool) -> eyre::Result<Option<Vec<Agent>
7880
};
7981

8082
let mcp_servers = {
81-
let config_path = directories::chat_legacy_global_mcp_config(os)?;
83+
let config_path = PathResolver::new(os).global().mcp_config()?;
8284
if os.fs.exists(&config_path) {
8385
match McpServerConfig::load_from_file(os, config_path).await {
8486
Ok(mut config) => {
@@ -145,7 +147,7 @@ pub async fn migrate(os: &mut Os, force: bool) -> eyre::Result<Option<Vec<Agent>
145147
new_agents.push(Agent {
146148
name: LEGACY_GLOBAL_AGENT_NAME.to_string(),
147149
description: Some(DEFAULT_DESC.to_string()),
148-
path: Some(directories::chat_global_agent_path(os)?.join(format!("{LEGACY_GLOBAL_AGENT_NAME}.json"))),
150+
path: Some(PathResolver::new(os).global().agents_dir()?.join(format!("{LEGACY_GLOBAL_AGENT_NAME}.json"))),
149151
resources: context.paths.iter().map(|p| format!("file://{p}").into()).collect(),
150152
hooks: HashMap::from([
151153
(
@@ -168,7 +170,7 @@ pub async fn migrate(os: &mut Os, force: bool) -> eyre::Result<Option<Vec<Agent>
168170
});
169171
}
170172

171-
let global_agent_path = directories::chat_global_agent_path(os)?;
173+
let global_agent_path = PathResolver::new(os).global().ensure_agents_dir().await?;
172174

173175
// Migration of profile context
174176
for (profile_name, context) in legacy_profiles.drain() {
@@ -205,10 +207,6 @@ pub async fn migrate(os: &mut Os, force: bool) -> eyre::Result<Option<Vec<Agent>
205207
});
206208
}
207209

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

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

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ use std::path::{
1919
PathBuf,
2020
};
2121

22+
use crate::util::{paths, paths::PathResolver};
23+
2224
use crossterm::style::Stylize as _;
2325
use crossterm::{
2426
execute,
@@ -66,7 +68,6 @@ use crate::theme::StyledText;
6668
use crate::util::{
6769
self,
6870
MCP_SERVER_TOOL_DELIMITER,
69-
directories,
7071
file_uri,
7172
};
7273

@@ -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_DOC_FILES.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,
@@ -344,12 +342,12 @@ impl Agent {
344342
pub async fn get_agent_by_name(os: &Os, agent_name: &str) -> eyre::Result<(Agent, PathBuf)> {
345343
let config_path: Result<PathBuf, PathBuf> = 'config: {
346344
// 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"));
345+
let local_config_dir = PathResolver::new(os).workspace().agents_dir()?.join(format!("{agent_name}.json"));
348346
if os.fs.exists(&local_config_dir) {
349347
break 'config Ok(local_config_dir);
350348
}
351349

352-
let global_config_dir = directories::chat_global_agent_path(os)?.join(format!("{agent_name}.json"));
350+
let global_config_dir = PathResolver::new(os).global().agents_dir()?.join(format!("{agent_name}.json"));
353351
if os.fs.exists(&global_config_dir) {
354352
break 'config Ok(global_config_dir);
355353
}
@@ -547,15 +545,15 @@ impl Agents {
547545
let mut local_agents = 'local: {
548546
// We could be launching from the home dir, in which case the global and local agents
549547
// 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)) {
548+
match (std::env::current_dir(), paths::home_dir(os)) {
551549
(Ok(cwd), Ok(home_dir)) if cwd == home_dir => break 'local Vec::<Agent>::new(),
552550
_ => {
553551
// noop, we keep going with the extraction of local agents (even if we have an
554552
// error retrieving cwd or home_dir)
555553
},
556554
}
557555

558-
let Ok(path) = directories::chat_local_agent_dir(os) else {
556+
let Ok(path) = PathResolver::new(os).workspace().agents_dir() else {
559557
break 'local Vec::<Agent>::new();
560558
};
561559
let Ok(files) = os.fs.read_dir(path).await else {
@@ -585,7 +583,7 @@ impl Agents {
585583
};
586584

587585
let mut global_agents = 'global: {
588-
let Ok(path) = directories::chat_global_agent_path(os) else {
586+
let Ok(path) = PathResolver::new(os).global().agents_dir() else {
589587
break 'global Vec::<Agent>::new();
590588
};
591589
let files = match os.fs.read_dir(&path).await {
@@ -629,10 +627,11 @@ impl Agents {
629627
// there.
630628
// Note that this config is not what q chat uses. It merely serves as an example.
631629
'example_config: {
632-
let Ok(path) = directories::example_agent_config(os) else {
630+
let Ok(agents_dir) = PathResolver::new(os).global().agents_dir() else {
633631
error!("Error obtaining example agent path.");
634632
break 'example_config;
635633
};
634+
let path = agents_dir.join("agent_config.json.example");
636635
if os.fs.exists(&path) {
637636
break 'example_config;
638637
}
@@ -744,7 +743,7 @@ impl Agents {
744743
if mcp_enabled {
745744
'load_legacy_mcp_json: {
746745
if global_mcp_config.is_none() {
747-
let Ok(global_mcp_path) = directories::chat_legacy_global_mcp_config(os) else {
746+
let Ok(global_mcp_path) = PathResolver::new(os).global().mcp_config() else {
748747
tracing::error!("Error obtaining legacy mcp json path. Skipping");
749748
break 'load_legacy_mcp_json;
750749
};
@@ -906,7 +905,7 @@ async fn load_agents_from_entries(
906905
/// Loads legacy mcp config by combining workspace and global config.
907906
/// In case of a server naming conflict, the workspace config is prioritized.
908907
async fn load_legacy_mcp_config(os: &Os) -> eyre::Result<Option<McpServerConfig>> {
909-
let global_mcp_path = directories::chat_legacy_global_mcp_config(os)?;
908+
let global_mcp_path = PathResolver::new(os).global().mcp_config()?;
910909
let global_mcp_config = match McpServerConfig::load_from_file(os, global_mcp_path).await {
911910
Ok(config) => Some(config),
912911
Err(e) => {
@@ -915,7 +914,7 @@ async fn load_legacy_mcp_config(os: &Os) -> eyre::Result<Option<McpServerConfig>
915914
},
916915
};
917916

918-
let workspace_mcp_path = directories::chat_legacy_workspace_mcp_config(os)?;
917+
let workspace_mcp_path = PathResolver::new(os).workspace().mcp_config()?;
919918
let workspace_mcp_config = match McpServerConfig::load_from_file(os, workspace_mcp_path).await {
920919
Ok(config) => Some(config),
921920
Err(e) => {

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ 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, paths::PathResolver};
2929

3030
#[derive(Clone, Debug, Subcommand, PartialEq, Eq)]
3131
pub enum AgentSubcommands {
@@ -336,9 +336,9 @@ pub async fn create_agent(
336336
bail!("Path must be a directory");
337337
}
338338

339-
directories::agent_config_dir(path)?
339+
path.join(paths::workspace::AGENTS_DIR)
340340
} else {
341-
directories::chat_global_agent_path(os)?
341+
PathResolver::new(os).global().agents_dir()?
342342
};
343343

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

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ 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;
32+
3233

3334
#[derive(Debug, PartialEq, Subcommand)]
3435
pub enum CheckpointSubcommand {
@@ -134,8 +135,9 @@ impl CheckpointSubcommand {
134135
StyledText::reset(),
135136
)?;
136137
} 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()))?;
138+
let path = PathResolver::new(os).global().shadow_repo_dir()
139+
.map_err(|e| ChatError::Custom(e.to_string().into()))?
140+
.join(session.conversation.conversation_id().to_string());
139141

140142
let start = std::time::Instant::now();
141143
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: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,9 @@ 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;
44+
use crate::util::paths::PathResolver;
4545
use crate::util::{
4646
NullWriter,
47-
directories,
4847
};
4948

5049
#[deny(missing_docs)]
@@ -54,7 +53,7 @@ use crate::util::{
5453
5554
Notes
5655
• 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)
56+
• Construct an agent in the global agents directory (accessible globally) or workspace agents directory (accessible in workspace)
5857
• See example config under global directory
5958
• Set default agent to assume with settings by running \"q settings chat.defaultAgent agent_name\"
6059
• Each agent maintains its own set of context and customizations"
@@ -395,7 +394,7 @@ impl AgentSubcommand {
395394
// switch / create profile after a session has started.
396395
// TODO: perhaps revive this after we have a decision on profile create /
397396
// switch
398-
let global_path = if let Ok(path) = chat_global_agent_path(os) {
397+
let global_path = if let Ok(path) = PathResolver::new(os).global().agents_dir() {
399398
path.to_str().unwrap_or("default global agent path").to_string()
400399
} else {
401400
"default global agent path".to_string()
@@ -538,7 +537,7 @@ pub async fn get_all_available_mcp_servers(os: &mut Os) -> Result<Vec<McpServerI
538537
}
539538

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) = PathResolver::new(os).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) = PathResolver::new(os).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)