agpm_cli/manifest/
tool_config.rs

1//! Tool configuration types for multi-tool support.
2//!
3//! This module defines the types and structures used to configure different AI coding
4//! assistant tools (Claude Code, OpenCode, AGPM, and custom tools) in AGPM manifests.
5//!
6//! # Overview
7//!
8//! AGPM supports multiple AI coding tools through a flexible configuration system:
9//! - **Claude Code**: The primary AI coding assistant (enabled by default)
10//! - **OpenCode**: Alternative AI coding assistant (enabled by default for consistency)
11//! - **AGPM**: Internal tool for shared infrastructure like snippets (enabled by default)
12//! - **Custom Tools**: User-defined tools with custom configurations (enabled by default)
13//!
14//! # Tool Configuration
15//!
16//! Each tool defines:
17//! - A base directory (e.g., `.claude`, `.opencode`, `.agpm`)
18//! - Resource type mappings (agents, commands, snippets, etc.)
19//! - Installation paths or merge targets for each resource type
20//! - Default flatten behavior for directory structure preservation
21//! - An enabled/disabled state
22//!
23//! # Key Types
24//!
25//! - [`WellKnownTool`]: Enum representing officially supported tools
26//! - [`ResourceConfig`]: Configuration for a specific resource type within a tool
27//! - [`ArtifactTypeConfig`]: Complete configuration for a tool
28//! - [`ToolsConfig`]: Top-level configuration mapping tool names to their configs
29//!
30//! # Examples
31//!
32//! ```toml
33//! [tools.claude-code]
34//! path = ".claude"
35//! enabled = true
36//!
37//! [tools.claude-code.resources.agents]
38//! path = "agents"
39//! flatten = true
40//!
41//! [tools.opencode]
42//! path = ".opencode"
43//! enabled = true   # Enabled by default
44//!
45//! [tools.opencode.resources.agents]
46//! path = "agent"  # Singular in OpenCode
47//! flatten = true
48//! ```
49
50use serde::{Deserialize, Serialize};
51use std::collections::HashMap;
52use std::path::PathBuf;
53
54/// Resource configuration within a tool.
55///
56/// Defines the installation path for a specific resource type within a tool.
57/// Resources can either:
58/// - Install to a subdirectory (via `path`)
59/// - Merge into a configuration file (via `merge_target`)
60///
61/// At least one of `path` or `merge_target` should be set for a resource type
62/// to be considered supported by a tool.
63#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
64pub struct ResourceConfig {
65    /// Subdirectory path for this resource type relative to the tool's base directory.
66    ///
67    /// Used for resources that install as separate files (agents, snippets, commands, scripts).
68    /// When None, this resource type either uses merge_target or is not supported.
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub path: Option<String>,
71
72    /// Target configuration file for merging this resource type.
73    ///
74    /// Used for resources that merge into configuration files (hooks, MCP servers).
75    /// The path is relative to the project root.
76    ///
77    /// # Examples
78    ///
79    /// - Hooks: `.claude/settings.local.json`
80    /// - MCP servers: `.mcp.json` or `.opencode/opencode.json`
81    #[serde(skip_serializing_if = "Option::is_none", rename = "merge-target")]
82    pub merge_target: Option<String>,
83
84    /// Default flatten behavior for this resource type.
85    ///
86    /// When `true`: Only the filename is used for installation (e.g., `nested/dir/file.md` → `file.md`)
87    /// When `false`: Full relative path is preserved (e.g., `nested/dir/file.md` → `nested/dir/file.md`)
88    ///
89    /// This default can be overridden per-dependency using the `flatten` field.
90    /// If not specified, defaults to `false` (preserve directory structure).
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub flatten: Option<bool>,
93}
94
95/// Well-known tool types with specific default behaviors.
96///
97/// This enum represents the officially supported tools and their
98/// specific default configurations, particularly for the `enabled` field.
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum WellKnownTool {
101    /// Claude Code - the primary AI coding assistant tool.
102    /// Enabled by default since most users rely on Claude Code.
103    ClaudeCode,
104
105    /// OpenCode - an alternative AI coding assistant tool.
106    /// Enabled by default for consistency with other tools.
107    OpenCode,
108
109    /// AGPM - internal tool for shared infrastructure (snippets).
110    /// Enabled by default for backward compatibility and shared resources.
111    Agpm,
112
113    /// Generic/custom tools not in the well-known set.
114    /// Enabled by default for backward compatibility.
115    Generic,
116}
117
118impl WellKnownTool {
119    /// Identifies a well-known tool from its string name.
120    ///
121    /// # Arguments
122    ///
123    /// * `tool_name` - The name of the tool (e.g., "claude-code", "opencode", "agpm")
124    ///
125    /// # Returns
126    ///
127    /// The corresponding `WellKnownTool` variant, or `Generic` for custom tools.
128    pub fn from_name(tool_name: &str) -> Self {
129        match tool_name {
130            "claude-code" => WellKnownTool::ClaudeCode,
131            "opencode" => WellKnownTool::OpenCode,
132            "agpm" => WellKnownTool::Agpm,
133            _ => WellKnownTool::Generic,
134        }
135    }
136
137    /// Returns the default `enabled` value for this tool.
138    ///
139    /// # Default Values
140    ///
141    /// - **Claude Code**: `true` (most users rely on it)
142    /// - **OpenCode**: `true` (enabled by default for consistency)
143    /// - **AGPM**: `true` (shared infrastructure)
144    /// - **Generic**: `true` (backward compatibility)
145    pub const fn default_enabled(self) -> bool {
146        match self {
147            WellKnownTool::ClaudeCode => true,
148            WellKnownTool::OpenCode => true,
149            WellKnownTool::Agpm => true,
150            WellKnownTool::Generic => true,
151        }
152    }
153}
154
155/// Tool configuration (internal deserialization structure).
156///
157/// This is used during deserialization to capture optional fields.
158/// The public API uses `ArtifactTypeConfig` with required `enabled` field.
159#[derive(Debug, Clone, Deserialize)]
160struct ArtifactTypeConfigRaw {
161    /// Base directory for this tool (e.g., ".claude", ".opencode", ".agpm")
162    path: PathBuf,
163
164    /// Map of resource type -> configuration
165    #[serde(default)]
166    resources: HashMap<String, ResourceConfig>,
167
168    /// Whether this tool is enabled (optional during deserialization)
169    ///
170    /// When None, the tool-specific default will be applied based on the tool name.
171    #[serde(default)]
172    enabled: Option<bool>,
173}
174
175/// Tool configuration.
176///
177/// Defines how a specific tool (e.g., claude-code, opencode, agpm)
178/// organizes its resources. Each tool has a base directory and
179/// a map of resource types to their subdirectory configurations.
180#[derive(Debug, Clone, Serialize)]
181pub struct ArtifactTypeConfig {
182    /// Base directory for this tool (e.g., ".claude", ".opencode", ".agpm")
183    pub path: PathBuf,
184
185    /// Map of resource type -> configuration
186    pub resources: HashMap<String, ResourceConfig>,
187
188    /// Whether this tool is enabled.
189    ///
190    /// When disabled, dependencies for this tool will not be resolved,
191    /// installed, or included in the lockfile.
192    ///
193    /// # Defaults
194    ///
195    /// - **claude-code**: `true` (most users rely on it)
196    /// - **opencode**: `true` (enabled by default for consistency)
197    /// - **agpm**: `true` (shared infrastructure)
198    /// - **custom tools**: `true` (backward compatibility)
199    pub enabled: bool,
200}
201
202/// Top-level tools configuration.
203///
204/// Maps tool type names to their configurations. This replaces the old
205/// `[target]` section and enables multi-tool support.
206#[derive(Debug, Clone, Serialize)]
207pub struct ToolsConfig {
208    /// Map of tool type name -> configuration
209    #[serde(flatten)]
210    pub types: HashMap<String, ArtifactTypeConfig>,
211}
212
213impl<'de> serde::Deserialize<'de> for ToolsConfig {
214    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
215    where
216        D: serde::Deserializer<'de>,
217    {
218        // First deserialize into the raw structure with Option<bool> for enabled
219        let raw_types: HashMap<String, ArtifactTypeConfigRaw> = HashMap::deserialize(deserializer)?;
220
221        // Convert to the final structure, applying tool-specific defaults
222        let types = raw_types
223            .into_iter()
224            .map(|(tool_name, raw_config)| {
225                // Determine the enabled value:
226                // - If explicitly set in TOML, use that value
227                // - Otherwise, use the tool-specific default
228                let well_known_tool = WellKnownTool::from_name(&tool_name);
229                let enabled =
230                    raw_config.enabled.unwrap_or_else(|| well_known_tool.default_enabled());
231
232                let config = ArtifactTypeConfig {
233                    path: raw_config.path,
234                    resources: raw_config.resources,
235                    enabled,
236                };
237
238                (tool_name, config)
239            })
240            .collect();
241
242        Ok(ToolsConfig {
243            types,
244        })
245    }
246}
247
248impl Default for ToolsConfig {
249    fn default() -> Self {
250        use crate::core::ResourceType;
251        let mut types = HashMap::new();
252
253        // Claude Code configuration
254        let mut claude_resources = HashMap::new();
255        claude_resources.insert(
256            ResourceType::Agent.to_plural().to_string(),
257            ResourceConfig {
258                path: Some("agents".to_string()),
259                merge_target: None,
260                flatten: Some(true), // Agents flatten by default
261            },
262        );
263        claude_resources.insert(
264            ResourceType::Snippet.to_plural().to_string(),
265            ResourceConfig {
266                path: Some("snippets".to_string()),
267                merge_target: None,
268                flatten: Some(false), // Snippets preserve directory structure
269            },
270        );
271        claude_resources.insert(
272            ResourceType::Command.to_plural().to_string(),
273            ResourceConfig {
274                path: Some("commands".to_string()),
275                merge_target: None,
276                flatten: Some(true), // Commands flatten by default
277            },
278        );
279        claude_resources.insert(
280            ResourceType::Script.to_plural().to_string(),
281            ResourceConfig {
282                path: Some("scripts".to_string()),
283                merge_target: None,
284                flatten: Some(false), // Scripts preserve directory structure
285            },
286        );
287        claude_resources.insert(
288            ResourceType::Hook.to_plural().to_string(),
289            ResourceConfig {
290                path: None, // Hooks are merged into configuration file
291                merge_target: Some(".claude/settings.local.json".to_string()),
292                flatten: None, // N/A for merge targets
293            },
294        );
295        claude_resources.insert(
296            ResourceType::McpServer.to_plural().to_string(),
297            ResourceConfig {
298                path: None, // MCP servers are merged into configuration file
299                merge_target: Some(".mcp.json".to_string()),
300                flatten: None, // N/A for merge targets
301            },
302        );
303
304        types.insert(
305            "claude-code".to_string(),
306            ArtifactTypeConfig {
307                path: PathBuf::from(".claude"),
308                resources: claude_resources,
309                enabled: WellKnownTool::ClaudeCode.default_enabled(),
310            },
311        );
312
313        // OpenCode configuration
314        let mut opencode_resources = HashMap::new();
315        opencode_resources.insert(
316            ResourceType::Agent.to_plural().to_string(),
317            ResourceConfig {
318                path: Some("agent".to_string()), // Singular
319                merge_target: None,
320                flatten: Some(true), // Agents flatten by default
321            },
322        );
323        opencode_resources.insert(
324            ResourceType::Snippet.to_plural().to_string(),
325            ResourceConfig {
326                path: Some("snippet".to_string()), // Singular
327                merge_target: None,
328                flatten: Some(false), // Snippets preserve directory structure
329            },
330        );
331        opencode_resources.insert(
332            ResourceType::Command.to_plural().to_string(),
333            ResourceConfig {
334                path: Some("command".to_string()), // Singular
335                merge_target: None,
336                flatten: Some(true), // Commands flatten by default
337            },
338        );
339        opencode_resources.insert(
340            ResourceType::McpServer.to_plural().to_string(),
341            ResourceConfig {
342                path: None, // MCP servers are merged into configuration file
343                merge_target: Some(".opencode/opencode.json".to_string()),
344                flatten: None, // N/A for merge targets
345            },
346        );
347
348        types.insert(
349            "opencode".to_string(),
350            ArtifactTypeConfig {
351                path: PathBuf::from(".opencode"),
352                resources: opencode_resources,
353                enabled: WellKnownTool::OpenCode.default_enabled(),
354            },
355        );
356
357        // AGPM configuration (snippets only)
358        let mut agpm_resources = HashMap::new();
359        agpm_resources.insert(
360            ResourceType::Snippet.to_plural().to_string(),
361            ResourceConfig {
362                path: Some("snippets".to_string()),
363                merge_target: None,
364                flatten: Some(false), // Snippets preserve directory structure
365            },
366        );
367
368        types.insert(
369            "agpm".to_string(),
370            ArtifactTypeConfig {
371                path: PathBuf::from(".agpm"),
372                resources: agpm_resources,
373                enabled: WellKnownTool::Agpm.default_enabled(),
374            },
375        );
376
377        Self {
378            types,
379        }
380    }
381}