1use std::path::PathBuf;
2
3use crate::error::TuiError;
4use crate::security::validate::validate_argument;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum ExportSelection {
9 All,
11 Role(String),
13 Provider(String),
15 Agents(Vec<String>),
17}
18
19#[derive(Debug, Clone)]
21pub struct ExportCommand {
22 pub platform: String,
23 pub selection: ExportSelection,
24 pub target_repo: PathBuf,
25 pub dry_run: bool,
26 pub force: bool,
27 pub no_skills: bool,
28}
29
30impl ExportCommand {
31 pub fn new(platform: String, selection: ExportSelection, target_repo: PathBuf) -> Self {
33 Self {
34 platform,
35 selection,
36 target_repo,
37 dry_run: true,
38 force: false,
39 no_skills: false,
40 }
41 }
42
43 pub fn validate(&self) -> Result<(), TuiError> {
46 validate_argument(&self.platform)?;
47
48 match &self.selection {
49 ExportSelection::All => {}
50 ExportSelection::Role(role) => {
51 validate_argument(role)?;
52 }
53 ExportSelection::Provider(provider) => {
54 validate_argument(provider)?;
55 }
56 ExportSelection::Agents(ids) => {
57 for id in ids {
58 validate_argument(id)?;
59 }
60 }
61 }
62
63 let target_str = self.target_repo.to_string_lossy();
64 validate_argument(&target_str)?;
65
66 Ok(())
67 }
68
69 pub fn to_args(&self) -> Vec<String> {
73 let mut args = Vec::new();
74
75 args.push("--platform".to_string());
76 args.push(self.platform.clone());
77
78 match &self.selection {
79 ExportSelection::All => {
80 args.push("--all".to_string());
81 }
82 ExportSelection::Role(role) => {
83 args.push("--role".to_string());
84 args.push(role.clone());
85 }
86 ExportSelection::Provider(provider) => {
87 args.push("--provider".to_string());
88 args.push(provider.clone());
89 }
90 ExportSelection::Agents(ids) => {
91 args.push(format!("--agents={}", ids.join(",")));
92 }
93 }
94
95 args.push("--repo".to_string());
96 args.push(self.target_repo.to_string_lossy().to_string());
97
98 if self.dry_run {
99 args.push("--dry-run".to_string());
100 }
101
102 if self.force {
103 args.push("--force".to_string());
104 }
105
106 if self.no_skills {
107 args.push("--no-skills".to_string());
108 }
109
110 args
111 }
112
113 pub fn display_command(&self) -> String {
115 format!(
116 "node scripts/export-marketplace-agents.mjs {}",
117 self.to_args().join(" ")
118 )
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn to_args_all_selection() {
128 let cmd = ExportCommand::new(
129 "kiro".to_string(),
130 ExportSelection::All,
131 PathBuf::from("/tmp/target"),
132 );
133 let args = cmd.to_args();
134 assert!(args.contains(&"--platform".to_string()));
135 assert!(args.contains(&"kiro".to_string()));
136 assert!(args.contains(&"--all".to_string()));
137 assert!(args.contains(&"--repo".to_string()));
138 assert!(args.contains(&"/tmp/target".to_string()));
139 assert!(args.contains(&"--dry-run".to_string()));
140 assert!(!args.contains(&"--force".to_string()));
141 assert!(!args.contains(&"--no-skills".to_string()));
142 }
143
144 #[test]
145 fn to_args_role_selection() {
146 let cmd = ExportCommand::new(
147 "cursor".to_string(),
148 ExportSelection::Role("devops-engineer".to_string()),
149 PathBuf::from("/tmp/target"),
150 );
151 let args = cmd.to_args();
152 assert!(args.contains(&"--role".to_string()));
153 assert!(args.contains(&"devops-engineer".to_string()));
154 }
155
156 #[test]
157 fn to_args_provider_selection() {
158 let cmd = ExportCommand::new(
159 "claude".to_string(),
160 ExportSelection::Provider("aws".to_string()),
161 PathBuf::from("/tmp/target"),
162 );
163 let args = cmd.to_args();
164 assert!(args.contains(&"--provider".to_string()));
165 assert!(args.contains(&"aws".to_string()));
166 }
167
168 #[test]
169 fn to_args_agents_selection() {
170 let cmd = ExportCommand::new(
171 "kiro".to_string(),
172 ExportSelection::Agents(vec!["agent-a".to_string(), "agent-b".to_string()]),
173 PathBuf::from("/tmp/target"),
174 );
175 let args = cmd.to_args();
176 assert!(args.iter().any(|a| a == "--agents=agent-a,agent-b"));
177 assert!(!args.contains(&"--all".to_string()));
178 assert!(!args.contains(&"--role".to_string()));
179 assert!(!args.contains(&"--provider".to_string()));
180 }
181
182 #[test]
183 fn to_args_with_force_and_no_skills() {
184 let mut cmd = ExportCommand::new(
185 "kiro".to_string(),
186 ExportSelection::All,
187 PathBuf::from("/tmp/target"),
188 );
189 cmd.dry_run = false;
190 cmd.force = true;
191 cmd.no_skills = true;
192 let args = cmd.to_args();
193 assert!(!args.contains(&"--dry-run".to_string()));
194 assert!(args.contains(&"--force".to_string()));
195 assert!(args.contains(&"--no-skills".to_string()));
196 }
197
198 #[test]
199 fn display_command_format() {
200 let cmd = ExportCommand::new(
201 "kiro".to_string(),
202 ExportSelection::All,
203 PathBuf::from("/tmp/target"),
204 );
205 let display = cmd.display_command();
206 assert!(display.starts_with("node scripts/export-marketplace-agents.mjs"));
207 assert!(display.contains("--platform"));
208 assert!(display.contains("kiro"));
209 }
210
211 #[test]
212 fn no_empty_strings_in_args() {
213 let cmd = ExportCommand::new(
214 "kiro".to_string(),
215 ExportSelection::All,
216 PathBuf::from("/tmp/target"),
217 );
218 for arg in cmd.to_args() {
219 assert!(!arg.is_empty(), "found empty string in args");
220 }
221 }
222
223 #[test]
224 fn validate_accepts_safe_args() {
225 let cmd = ExportCommand::new(
226 "kiro".to_string(),
227 ExportSelection::Role("devops-engineer".to_string()),
228 PathBuf::from("/tmp/target"),
229 );
230 assert!(cmd.validate().is_ok());
231 }
232
233 #[test]
234 fn validate_rejects_platform_with_metachar() {
235 let cmd = ExportCommand::new(
236 "kiro; rm -rf /".to_string(),
237 ExportSelection::All,
238 PathBuf::from("/tmp/target"),
239 );
240 assert!(cmd.validate().is_err());
241 }
242
243 #[test]
244 fn validate_rejects_role_with_metachar() {
245 let cmd = ExportCommand::new(
246 "kiro".to_string(),
247 ExportSelection::Role("role|cat /etc/passwd".to_string()),
248 PathBuf::from("/tmp/target"),
249 );
250 assert!(cmd.validate().is_err());
251 }
252
253 #[test]
254 fn validate_rejects_agent_id_with_metachar() {
255 let cmd = ExportCommand::new(
256 "kiro".to_string(),
257 ExportSelection::Agents(vec!["good-agent".to_string(), "bad$(cmd)".to_string()]),
258 PathBuf::from("/tmp/target"),
259 );
260 assert!(cmd.validate().is_err());
261 }
262
263 #[test]
264 fn to_args_agents_selection_uses_agents_flag() {
265 let cmd = ExportCommand::new(
266 "kiro".to_string(),
267 ExportSelection::Agents(vec!["agent-a".to_string(), "agent-b".to_string()]),
268 PathBuf::from("/tmp/target"),
269 );
270 let args = cmd.to_args();
271 assert!(args.iter().any(|arg| arg == "--agents=agent-a,agent-b"));
272 assert!(!args.iter().any(|arg| arg == "agent-a"));
273 }
274}