agpm_cli/config/mod.rs
1//! Configuration management for AGPM
2//!
3//! This module provides comprehensive configuration management for the `AGent` Package Manager (AGPM).
4//! It handles project manifests, global user configuration, and resource metadata with a focus on
5//! security, cross-platform compatibility, and reproducible builds.
6//!
7//! # Architecture Overview
8//!
9//! AGPM uses a multi-layered configuration architecture:
10//!
11//! 1. **Global Configuration** (`~/.agpm/config.toml`) - User-wide settings including authentication
12//! 2. **Project Manifest** (`agpm.toml`) - Project dependencies and sources
13//! 3. **Lockfile** (`agpm.lock`) - Resolved versions for reproducible builds
14//! 4. **Resource Metadata** - Agent and snippet configurations embedded in `.md` files
15//!
16//! # Modules
17//!
18//! - `global` - Global configuration management with authentication token support
19//! - `parser` - Generic TOML parsing utilities with error context
20//!
21//! # Configuration Files
22//!
23//! ## Global Configuration (`~/.agpm/config.toml`)
24//!
25//! **Location:**
26//! - Unix/macOS: `~/.agpm/config.toml`
27//! - Windows: `%LOCALAPPDATA%\agpm\config.toml`
28//!
29//! **Purpose:** Store user-wide settings including private repository access tokens.
30//! This file is never committed to version control.
31//!
32//! ```toml
33//! # Global sources with authentication tokens
34//! [sources]
35//! private = "https://oauth2:ghp_xxxxxxxxxxxx@github.com/company/private-agpm.git"
36//! enterprise = "https://token:abc123@gitlab.company.com/ai/resources.git"
37//! ```
38//!
39//! ## Project Manifest (`agpm.toml`)
40//!
41//! **Purpose:** Define project dependencies and public sources. Safe for version control.
42//!
43//! ```toml
44//! [sources]
45//! community = "https://github.com/aig787/agpm-community.git"
46//!
47//! [agents]
48//! code-reviewer = { source = "community", path = "agents/code-reviewer.md", version = "v1.2.0" }
49//! local-helper = { path = "../local-agents/helper.md" }
50//!
51//! [snippets]
52//! rust-patterns = { source = "community", path = "snippets/rust.md", version = "^2.0" }
53//! ```
54//!
55//! ## Lockfile (`agpm.lock`)
56//!
57//! **Purpose:** Pin exact versions for reproducible installations. Auto-generated.
58//!
59//! ```toml
60//! # Auto-generated lockfile - DO NOT EDIT
61//! version = 1
62//!
63//! [[sources]]
64//! name = "community"
65//! url = "https://github.com/aig787/agpm-community.git"
66//! commit = "abc123..."
67//!
68//! [[agents]]
69//! name = "code-reviewer"
70//! source = "community"
71//! version = "v1.2.0"
72//! resolved_commit = "def456..."
73//! checksum = "sha256:..."
74//! installed_at = "agents/code-reviewer.md"
75//! ```
76//!
77//! # Security Model
78//!
79//! ## Credential Isolation
80//!
81//! - **Global Config**: Contains authentication tokens, never committed
82//! - **Project Manifest**: Public sources only, safe for version control
83//! - **Source Merging**: Global sources loaded first, project sources can override
84//!
85//! ## Configuration Priority
86//!
87//! 1. Environment variables (`AGPM_CONFIG_PATH`, `AGPM_CACHE_DIR`)
88//! 2. Global configuration (`~/.agpm/config.toml`)
89//! 3. Project manifest (`agpm.toml`)
90//! 4. Default values
91//!
92//! # Resource Metadata
93//!
94//! Agent and snippet files can include TOML frontmatter for metadata:
95//!
96//! ```markdown
97//! +++
98//! [metadata]
99//! name = "rust-expert"
100//! description = "Expert Rust development agent"
101//! author = "AGPM Community"
102//! license = "MIT"
103//! keywords = ["rust", "programming", "expert"]
104//!
105//! [requirements]
106//! agpm_version = ">=0.1.0"
107//! claude_version = "latest"
108//! platforms = ["windows", "macos", "linux"]
109//!
110//! [[requirements.dependencies]]
111//! name = "code-formatter"
112//! version = "^1.0"
113//! type = "snippet"
114//! +++
115//!
116//! # Rust Expert Agent
117//!
118//! You are an expert Rust developer...
119//! ```
120//!
121//! # Platform Support
122//!
123//! This module handles cross-platform configuration paths:
124//!
125//! - **Windows**: Uses `%LOCALAPPDATA%` for configuration
126//! - **macOS/Linux**: Uses `$HOME/.agpm` directory
127//! - **Path Separators**: Normalized automatically
128//! - **File Permissions**: Handles Windows vs Unix differences
129//!
130//! # Examples
131//!
132//! ## Loading Global Configuration
133//!
134//! ```rust,no_run
135//! use agpm_cli::config::{GlobalConfig, GlobalConfigManager};
136//!
137//! # async fn example() -> anyhow::Result<()> {
138//! // Simple load
139//! let global = GlobalConfig::load().await?;
140//! println!("Found {} global sources", global.sources.len());
141//!
142//! // Using manager for caching
143//! let mut manager = GlobalConfigManager::new()?;
144//! let config = manager.get().await?;
145//!
146//! // Add authenticated source
147//! let config = manager.get_mut().await?;
148//! config.add_source(
149//! "private".to_string(),
150//! "https://oauth2:token@github.com/company/repo.git".to_string()
151//! );
152//! manager.save().await?;
153//! # Ok(())
154//! # }
155//! ```
156//!
157//! ## Source Resolution with Authentication
158//!
159//! ```rust,no_run
160//! use agpm_cli::config::GlobalConfig;
161//! use std::collections::HashMap;
162//!
163//! # async fn example() -> anyhow::Result<()> {
164//! let global = GlobalConfig::load().await?;
165//!
166//! // Project manifest sources (public)
167//! let mut local_sources = HashMap::new();
168//! local_sources.insert(
169//! "community".to_string(),
170//! "https://github.com/aig787/agpm-community.git".to_string()
171//! );
172//!
173//! // Merge with global sources (may include auth tokens)
174//! let merged = global.merge_sources(&local_sources);
175//!
176//! // Use merged sources for git operations
177//! for (name, url) in &merged {
178//! println!("Source {}: {}", name,
179//! if url.contains("@") { "[authenticated]" } else { url });
180//! }
181//! # Ok(())
182//! # }
183//! ```
184
185mod global;
186mod parser;
187
188pub use global::{GlobalConfig, GlobalConfigManager};
189pub use parser::parse_config;
190
191use crate::core::file_error::{FileOperation, FileResultExt};
192use anyhow::Result;
193use std::path::PathBuf;
194
195/// Get the cache directory for AGPM.
196///
197/// Returns the directory where AGPM stores cached Git repositories and temporary files.
198/// The location follows platform conventions and can be overridden with environment variables.
199///
200/// # Location Priority
201///
202/// 1. `AGPM_CACHE_DIR` environment variable (if set)
203/// 2. Platform-specific cache directory:
204/// - Windows: `%LOCALAPPDATA%\agpm\cache`
205/// - macOS/Linux: `~/.agpm/cache`
206///
207/// # Directory Creation
208///
209/// The directory is automatically created if it doesn't exist.
210///
211/// # Examples
212///
213/// ```rust,no_run
214/// use agpm_cli::config::get_cache_dir;
215///
216/// # fn example() -> anyhow::Result<()> {
217/// let cache = get_cache_dir()?;
218/// println!("Cache directory: {}", cache.display());
219/// # Ok(())
220/// # }
221/// ```
222///
223/// # Errors
224///
225/// Returns an error if:
226/// - The system cache directory cannot be determined
227/// - The cache directory cannot be created
228/// - Insufficient permissions for directory creation
229pub fn get_cache_dir() -> Result<PathBuf> {
230 // Check for environment variable override first (essential for testing)
231 if let Ok(dir) = std::env::var("AGPM_CACHE_DIR") {
232 return Ok(PathBuf::from(dir));
233 }
234
235 // Use consistent directory structure with rest of AGPM
236 let cache_dir = if cfg!(target_os = "windows") {
237 dirs::data_local_dir()
238 .ok_or_else(|| anyhow::anyhow!("Unable to determine local data directory"))?
239 .join("agpm")
240 .join("cache")
241 } else {
242 dirs::home_dir()
243 .ok_or_else(|| anyhow::anyhow!("Unable to determine home directory"))?
244 .join(".agpm")
245 .join("cache")
246 };
247
248 if !cache_dir.exists() {
249 std::fs::create_dir_all(&cache_dir).with_file_context(
250 FileOperation::CreateDir,
251 &cache_dir,
252 "creating cache directory",
253 "config::get_cache_dir",
254 )?;
255 }
256
257 Ok(cache_dir)
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 #[test]
265 fn test_get_cache_dir() {
266 // Test that we get a valid cache dir
267 let dir = get_cache_dir().unwrap();
268 assert!(dir.to_string_lossy().contains("agpm"));
269 }
270}