foundry_mcp/cli/commands/
create_project.rs1use crate::cli::args::CreateProjectArgs;
4use crate::core::{project, validation};
5use crate::types::project::ProjectConfig;
6use crate::types::responses::{CreateProjectResponse, FoundryResponse};
7use crate::utils::response::{build_incomplete_response, build_success_response};
8use anyhow::{Context, Result};
9
10pub async fn execute(args: CreateProjectArgs) -> Result<FoundryResponse<CreateProjectResponse>> {
11 validate_project_preconditions(&args.project_name)?;
13
14 let suggestions = process_content_validation(&args)?;
16
17 let project_config = build_project_config(args);
19 let created_project =
20 project::create_project(project_config).context("Failed to create project structure")?;
21
22 Ok(build_response(created_project, suggestions))
24}
25
26fn validate_project_preconditions(project_name: &str) -> Result<()> {
28 validate_project_name(project_name)?;
29
30 if project::project_exists(project_name)? {
31 return Err(anyhow::anyhow!("Project '{}' already exists", project_name));
32 }
33
34 Ok(())
35}
36
37fn process_content_validation(args: &CreateProjectArgs) -> Result<Vec<String>> {
39 let validation_results = validate_content(args)?;
40
41 let (validation_errors, suggestions): (Vec<String>, Vec<String>) =
42 validation_results.into_iter().fold(
43 (Vec::new(), Vec::new()),
44 |(mut errors, mut suggestions), (content_type, result)| {
45 if !result.is_valid {
46 errors.extend(
47 result
48 .errors
49 .into_iter()
50 .map(|e| format!("{}: {}", content_type, e)),
51 );
52 }
53 suggestions.extend(
54 result
55 .suggestions
56 .into_iter()
57 .map(|s| format!("{}: {}", content_type, s)),
58 );
59 (errors, suggestions)
60 },
61 );
62
63 if !validation_errors.is_empty() {
65 return Err(anyhow::anyhow!(
66 "Content validation failed:\n{}",
67 validation_errors.join("\n")
68 ));
69 }
70
71 Ok(suggestions)
72}
73
74fn build_project_config(args: CreateProjectArgs) -> ProjectConfig {
76 ProjectConfig {
77 name: args.project_name,
78 vision: args.vision,
79 tech_stack: args.tech_stack,
80 summary: args.summary,
81 }
82}
83
84fn build_response(
86 created_project: crate::types::project::Project,
87 suggestions: Vec<String>,
88) -> FoundryResponse<CreateProjectResponse> {
89 let files_created = vec![
90 "vision.md".to_string(),
91 "tech-stack.md".to_string(),
92 "summary.md".to_string(),
93 "specs/".to_string(),
94 ];
95
96 let response_data = CreateProjectResponse {
97 project_name: created_project.name.clone(),
98 created_at: created_project.created_at,
99 project_path: created_project.path.to_string_lossy().to_string(),
100 files_created,
101 };
102
103 let next_steps = vec![
104 format!("Project '{}' created successfully", created_project.name),
105 "Project structure is ready for development".to_string(),
106 "Available next steps: foundry create_spec (if you have a specific feature), foundry load_project (to see context), or foundry list_projects (to explore other projects)".to_string(),
107 ];
108
109 let workflow_hints = if !suggestions.is_empty() {
110 let mut enhanced_suggestions = vec![
111 "📋 DOCUMENT PURPOSE: Your content serves as COMPLETE CONTEXT for future implementation".to_string(),
112 "🎯 CONTEXT TEST: Could someone with no prior knowledge implement this using only your documents?".to_string(),
113 ];
114 enhanced_suggestions.extend(suggestions.clone());
115 enhanced_suggestions
116 } else {
117 vec![
118 "📋 DOCUMENT PURPOSE: Your content serves as COMPLETE CONTEXT for future implementation".to_string(),
119 "🎯 CONTEXT TEST: Could someone with no prior knowledge implement this using only your documents?".to_string(),
120 "Consider what you want to work on next".to_string(),
121 "foundry create_spec: Use when you have a specific feature to implement".to_string(),
122 "foundry load_project: Use to see full project context and available specs".to_string(),
123 "foundry get_foundry_help decision-points: Use to understand tool selection"
124 .to_string(),
125 ]
126 };
127
128 if suggestions.is_empty() {
129 build_success_response(response_data, next_steps, workflow_hints)
130 } else {
131 build_incomplete_response(response_data, next_steps, workflow_hints)
132 }
133}
134
135fn validate_project_name(name: &str) -> Result<()> {
137 if name.is_empty() {
138 return Err(anyhow::anyhow!("Project name cannot be empty"));
139 }
140
141 if !name
143 .chars()
144 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
145 {
146 return Err(anyhow::anyhow!(
147 "Project name must be in kebab-case format (lowercase letters, numbers, and hyphens only)"
148 ));
149 }
150
151 if name.starts_with('-') || name.ends_with('-') {
153 return Err(anyhow::anyhow!(
154 "Project name cannot start or end with a hyphen"
155 ));
156 }
157
158 if name.contains("--") {
160 return Err(anyhow::anyhow!(
161 "Project name cannot contain consecutive hyphens"
162 ));
163 }
164
165 Ok(())
166}
167
168fn validate_content(
170 args: &CreateProjectArgs,
171) -> Result<Vec<(&'static str, validation::ValidationResult)>> {
172 let validations = vec![
173 (
174 "Vision",
175 validation::validate_content(validation::ContentType::Vision, &args.vision),
176 ),
177 (
178 "Tech Stack",
179 validation::validate_content(validation::ContentType::TechStack, &args.tech_stack),
180 ),
181 (
182 "Summary",
183 validation::validate_content(validation::ContentType::Summary, &args.summary),
184 ),
185 ];
186
187 Ok(validations)
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 fn create_test_args() -> CreateProjectArgs {
196 CreateProjectArgs {
197 project_name: "test-project".to_string(),
198 vision: "This is a test vision that is long enough to meet the minimum requirements. It should contain at least 200 characters to pass validation. This includes multiple sentences and provides comprehensive coverage of what the project aims to achieve.".to_string(),
199 tech_stack: "This project uses Rust as the primary language with tokio for async runtime, serde for serialization, and clap for command line argument parsing. It follows functional programming principles and modern Rust 2024 practices.".to_string(),
200 summary: "A comprehensive CLI tool for deterministic project management and AI coding assistant integration with modern Rust patterns.".to_string(),
201 }
202 }
203
204 #[test]
205 fn test_validate_project_name_valid() {
206 let valid_names = vec!["my-project", "project123", "my-awesome-project", "test"];
207
208 for name in valid_names {
209 assert!(
210 validate_project_name(name).is_ok(),
211 "Name '{}' should be valid",
212 name
213 );
214 }
215 }
216
217 #[test]
218 fn test_validate_project_name_invalid() {
219 let invalid_names = vec![
220 "", "-project", "project-", "my--project", "MyProject", "my project", "my.project", ];
228
229 for name in invalid_names {
230 assert!(
231 validate_project_name(name).is_err(),
232 "Name '{}' should be invalid",
233 name
234 );
235 }
236 }
237
238 #[test]
239 fn test_validate_content_structure() {
240 let args = create_test_args();
241 let validations = validate_content(&args).unwrap();
242
243 assert_eq!(validations.len(), 3);
244
245 let content_types: Vec<&str> = validations.iter().map(|(t, _)| *t).collect();
247 assert!(content_types.contains(&"Vision"));
248 assert!(content_types.contains(&"Tech Stack"));
249 assert!(content_types.contains(&"Summary"));
250 }
251}