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