agpm_cli/models/mod.rs
1//! Shared data models for AGPM operations
2//!
3//! This module provides reusable data structures that are used across
4//! different CLI commands and core operations, ensuring consistency
5//! and reducing code duplication.
6
7use clap::Args;
8use serde::{Deserialize, Serialize};
9
10/// Common dependency specification used across commands
11#[derive(Debug, Clone, Args)]
12pub struct DependencySpec {
13 /// Dependency specification string
14 ///
15 /// Format: `source:path[@version]` for Git sources or path for local files
16 ///
17 /// GIT DEPENDENCIES (from a repository source defined in `[sources]`):
18 /// source:path@version - Specific version (tag/branch/commit)
19 /// source:path - Defaults to "main" branch
20 ///
21 /// Examples:
22 /// official:agents/code-reviewer.md@v1.0.0 - Specific version tag
23 /// community:snippets/python-utils.md@main - Branch name
24 /// myrepo:commands/deploy.md@abc123f - Commit SHA
25 /// community:hooks/pre-commit.json - Defaults to "main"
26 ///
27 /// LOCAL FILE DEPENDENCIES:
28 /// ./path/file.md - Relative to current directory
29 /// ../path/file.md - Parent directory
30 /// /absolute/path/file.md - Absolute path (Unix/macOS)
31 /// C:\path\file.md - Absolute path (Windows)
32 ///
33 /// Examples:
34 /// ./agents/my-agent.md - Project agent
35 /// ../shared-resources/common-snippet.md - Shared resource
36 /// /usr/local/share/agpm/hooks/lint.json - System-wide hook
37 ///
38 /// PATTERN DEPENDENCIES (glob patterns for multiple files):
39 /// source:dir/*.md@version - All .md files in directory
40 /// source:dir/**/*.md - All .md files recursively
41 ///
42 /// Examples:
43 /// community:agents/ai/*.md@v2.0.0 - All AI agents
44 /// official:agents/**/review*.md@v1.5.0 - All review agents (recursive)
45 /// ./local-agents/*.md - All local agents
46 ///
47 /// Notes:
48 /// - Version is optional for Git sources (defaults to "main")
49 /// - Version is not applicable for local file paths
50 /// - Use --name to specify a custom dependency name
51 /// - Patterns require --name to provide a meaningful dependency name
52 #[arg(
53 value_name = "SPEC",
54 help = "Dependency spec: 'source:path@version' for Git (e.g., community:agents/helper.md@v1.0.0) or './path' for local files. Use --help for more examples"
55 )]
56 pub spec: String,
57
58 /// Custom name for the dependency
59 ///
60 /// If not provided, the name will be derived from the file path.
61 /// This allows for more descriptive or shorter names in the manifest.
62 #[arg(long)]
63 pub name: Option<String>,
64
65 /// Target tool for the dependency
66 ///
67 /// Specifies which AI coding tool this resource is for.
68 /// Supported values: claude-code, opencode, agpm
69 ///
70 /// Examples:
71 /// --tool claude-code - Install to .claude/ (default for agents, commands, scripts, hooks)
72 /// --tool opencode - Install to .opencode/
73 /// --tool agpm - Install to .agpm/ (default for snippets)
74 #[arg(long)]
75 pub tool: Option<String>,
76
77 /// Custom installation target path (relative to resource directory)
78 ///
79 /// Override the default installation path. The path is relative to the
80 /// resource type's default directory (e.g., .claude/agents/).
81 ///
82 /// IMPORTANT: Since v0.3.18+, custom targets are relative to the resource
83 /// directory, not the project root.
84 ///
85 /// Examples:
86 /// --target custom/special.md - Install to .claude/agents/custom/special.md
87 /// --target experimental/test.md - Install to .claude/commands/experimental/test.md
88 #[arg(long)]
89 pub target: Option<String>,
90
91 /// Custom filename for the installed resource
92 ///
93 /// Override the default filename derived from the source path.
94 /// Use this to rename resources during installation.
95 ///
96 /// Examples:
97 /// --filename my-reviewer.md - Install as my-reviewer.md instead of original name
98 /// --filename helper.json - Rename JSON file during installation
99 #[arg(long)]
100 pub filename: Option<String>,
101
102 /// Force overwrite if dependency exists
103 ///
104 /// By default, adding a duplicate dependency will fail.
105 /// Use this flag to replace existing dependencies.
106 #[arg(long, short = 'f')]
107 pub force: bool,
108
109 /// Skip automatic installation after adding dependency
110 ///
111 /// By default, the dependency is automatically installed after being added
112 /// to the manifest. Use this flag to only update the manifest without
113 /// installing the dependency files.
114 ///
115 /// Examples:
116 /// --no-install - Add to manifest only, skip installation
117 #[arg(long)]
118 pub no_install: bool,
119}
120
121/// Arguments for adding an agent dependency
122#[derive(Debug, Clone, Args)]
123pub struct AgentDependency {
124 /// Common dependency specification fields
125 #[command(flatten)]
126 pub common: DependencySpec,
127}
128
129/// Arguments for adding a snippet dependency
130#[derive(Debug, Clone, Args)]
131pub struct SnippetDependency {
132 /// Common dependency specification fields
133 #[command(flatten)]
134 pub common: DependencySpec,
135}
136
137/// Arguments for adding a command dependency
138#[derive(Debug, Clone, Args)]
139pub struct CommandDependency {
140 /// Common dependency specification fields
141 #[command(flatten)]
142 pub common: DependencySpec,
143}
144
145/// Arguments for adding an MCP server dependency
146#[derive(Debug, Clone, Args)]
147pub struct McpServerDependency {
148 /// Common dependency specification fields
149 #[command(flatten)]
150 pub common: DependencySpec,
151}
152
153/// Enum representing all possible dependency types
154#[derive(Debug, Clone)]
155pub enum DependencyType {
156 /// An agent dependency
157 Agent(AgentDependency),
158 /// A snippet dependency
159 Snippet(SnippetDependency),
160 /// A command dependency
161 Command(CommandDependency),
162 /// A script dependency
163 Script(ScriptDependency),
164 /// A hook dependency
165 Hook(HookDependency),
166 /// An MCP server dependency
167 McpServer(McpServerDependency),
168}
169
170impl DependencyType {
171 /// Get the common dependency specification
172 #[must_use]
173 pub const fn common(&self) -> &DependencySpec {
174 match self {
175 Self::Agent(dep) => &dep.common,
176 Self::Snippet(dep) => &dep.common,
177 Self::Command(dep) => &dep.common,
178 Self::Script(dep) => &dep.common,
179 Self::Hook(dep) => &dep.common,
180 Self::McpServer(dep) => &dep.common,
181 }
182 }
183
184 /// Get the resource type as a string
185 #[must_use]
186 pub const fn resource_type(&self) -> &'static str {
187 match self {
188 Self::Agent(_) => "agent",
189 Self::Snippet(_) => "snippet",
190 Self::Command(_) => "command",
191 Self::Script(_) => "script",
192 Self::Hook(_) => "hook",
193 Self::McpServer(_) => "mcp-server",
194 }
195 }
196}
197
198/// Source repository specification
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct SourceSpec {
201 /// Name for the source
202 pub name: String,
203
204 /// Git repository URL
205 pub url: String,
206}
207
208/// Resource installation options
209#[derive(Debug, Clone, Default)]
210pub struct InstallOptions {
211 /// Skip installation, only update lockfile
212 pub no_install: bool,
213
214 /// Force reinstallation even if up to date
215 pub force: bool,
216
217 /// Suppress progress indicators
218 pub quiet: bool,
219
220 /// Use cached data only, don't fetch updates
221 pub offline: bool,
222}
223
224/// Resource update options
225#[derive(Debug, Clone, Default)]
226pub struct UpdateOptions {
227 /// Update all dependencies
228 pub all: bool,
229
230 /// Specific dependencies to update
231 pub dependencies: Vec<String>,
232
233 /// Allow updating to incompatible versions
234 pub breaking: bool,
235
236 /// Suppress progress indicators
237 pub quiet: bool,
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn test_dependency_type_common() {
246 let agent = DependencyType::Agent(AgentDependency {
247 common: DependencySpec {
248 spec: "test:agent.md".to_string(),
249 name: None,
250 tool: None,
251 target: None,
252 filename: None,
253 force: false,
254 no_install: false,
255 },
256 });
257
258 assert_eq!(agent.common().spec, "test:agent.md");
259 assert_eq!(agent.resource_type(), "agent");
260 }
261
262 #[test]
263 fn test_mcp_server_dependency() {
264 let mcp = DependencyType::McpServer(McpServerDependency {
265 common: DependencySpec {
266 spec: "test:mcp.toml".to_string(),
267 name: Some("test-server".to_string()),
268 tool: None,
269 target: None,
270 filename: None,
271 force: true,
272 no_install: false,
273 },
274 });
275
276 assert_eq!(mcp.common().spec, "test:mcp.toml");
277 assert_eq!(mcp.common().name, Some("test-server".to_string()));
278 assert!(mcp.common().force);
279 assert_eq!(mcp.resource_type(), "mcp-server");
280 }
281}
282
283/// Arguments for adding a script dependency
284#[derive(Debug, Clone, Args)]
285pub struct ScriptDependency {
286 /// Common dependency specification fields
287 #[command(flatten)]
288 pub common: DependencySpec,
289}
290
291/// Arguments for adding a hook dependency
292#[derive(Debug, Clone, Args)]
293pub struct HookDependency {
294 /// Common dependency specification fields
295 #[command(flatten)]
296 pub common: DependencySpec,
297}