1use anyhow::{Context, Result};
4
5use crate::core::{foundry, validation};
6use crate::types::project::ProjectConfig;
7use crate::types::responses::{CreateProjectResponse, FoundryResponse};
8use crate::utils::response::{build_incomplete_response, build_success_response};
9
10#[derive(Debug, Clone)]
11pub struct Input {
12 pub project_name: String,
13 pub vision: String,
14 pub tech_stack: String,
15 pub summary: String,
16}
17
18pub async fn run(input: Input) -> Result<FoundryResponse<CreateProjectResponse>> {
19 let foundry = foundry::get_default_foundry()?;
20
21 validate_project_preconditions(&foundry, &input.project_name).await?;
22
23 let suggestions = process_content_validation(&input)?;
24
25 let project_config = build_project_config(input);
26 let created_project = foundry
27 .create_project(project_config)
28 .await
29 .context("Failed to create project structure")?;
30
31 Ok(build_response(created_project, suggestions))
32}
33
34async fn validate_project_preconditions(
35 foundry: &foundry::Foundry<crate::core::backends::filesystem::FilesystemBackend>,
36 project_name: &str,
37) -> Result<()> {
38 validate_project_name(project_name)?;
39
40 if foundry.project_exists(project_name).await? {
41 return Err(anyhow::anyhow!("Project '{}' already exists", project_name));
42 }
43
44 Ok(())
45}
46
47fn process_content_validation(input: &Input) -> Result<Vec<String>> {
48 let validation_results = validate_content(input)?;
49
50 let (validation_errors, suggestions): (Vec<String>, Vec<String>) =
51 validation_results.into_iter().fold(
52 (Vec::new(), Vec::new()),
53 |(mut errors, mut suggestions), (content_type, result)| {
54 if !result.is_valid {
55 errors.extend(
56 result
57 .errors
58 .into_iter()
59 .map(|e| format!("{}: {}", content_type, e)),
60 );
61 }
62 suggestions.extend(
63 result
64 .suggestions
65 .into_iter()
66 .map(|s| format!("{}: {}", content_type, s)),
67 );
68 (errors, suggestions)
69 },
70 );
71
72 if !validation_errors.is_empty() {
73 return Err(anyhow::anyhow!(
74 "Content validation failed:\n{}",
75 validation_errors.join("\n")
76 ));
77 }
78
79 Ok(suggestions)
80}
81
82fn build_project_config(input: Input) -> ProjectConfig {
83 ProjectConfig {
84 name: input.project_name,
85 vision: input.vision,
86 tech_stack: input.tech_stack,
87 summary: input.summary,
88 }
89}
90
91fn build_response(
92 created_project: crate::types::project::Project,
93 suggestions: Vec<String>,
94) -> FoundryResponse<CreateProjectResponse> {
95 let files_created = vec![
96 "vision.md".to_string(),
97 "tech-stack.md".to_string(),
98 "summary.md".to_string(),
99 "specs/".to_string(),
100 ];
101
102 let response_data = CreateProjectResponse {
103 project_name: created_project.name.clone(),
104 created_at: created_project.created_at,
105 project_path: created_project.path.to_string_lossy().to_string(),
106 files_created,
107 };
108
109 let next_steps = vec![
110 format!("Project '{}' created successfully", created_project.name),
111 "Project structure is ready for development".to_string(),
112 format!(
113 "Next → create a spec: {{\"name\": \"create_spec\", \"arguments\": {{\"project_name\": \"{}\", \"feature_name\": \"<feature>\", \"spec\": \"...\", \"tasks\": \"...\", \"notes\": \"...\"}}}}; load project: {{\"name\": \"load_project\", \"arguments\": {{\"project_name\": \"{}\"}}}}; list projects: {{\"name\": \"list_projects\", \"arguments\": {{}}}}",
114 created_project.name, created_project.name
115 ),
116 ];
117
118 let workflow_hints = if !suggestions.is_empty() {
119 let mut enhanced_suggestions = vec![
120 "📋 DOCUMENT PURPOSE: Your content serves as COMPLETE CONTEXT for future implementation".to_string(),
121 "🎯 CONTEXT TEST: Could someone with no prior knowledge implement this using only your documents?".to_string(),
122 ];
123 enhanced_suggestions.extend(suggestions.clone());
124 enhanced_suggestions
125 } else {
126 vec![
127 "📋 DOCUMENT PURPOSE: Your content serves as COMPLETE CONTEXT for future implementation".to_string(),
128 "🎯 CONTEXT TEST: Could someone with no prior knowledge implement this using only your documents?".to_string(),
129 "Consider what you want to work on next".to_string(),
130 format!("Create a spec: {{\"name\": \"create_spec\", \"arguments\": {{\"project_name\": \"{}\", \"feature_name\": \"<feature>\", \"spec\": \"...\", \"tasks\": \"...\", \"notes\": \"...\"}}}}", created_project.name),
137 format!("Load project: {{\"name\": \"load_project\", \"arguments\": {{\"project_name\": \"{}\"}}}}", created_project.name),
138 "Tool selection guidance: {\"name\": \"get_foundry_help\", {\"topic\": \"decision-points\"}}".to_string(),
139 ]
140 };
141
142 if suggestions.is_empty() {
143 build_success_response(response_data, next_steps, workflow_hints)
144 } else {
145 build_incomplete_response(response_data, next_steps, workflow_hints)
146 }
147}
148
149fn validate_project_name(name: &str) -> Result<()> {
150 if name.is_empty() {
151 return Err(anyhow::anyhow!("Project name cannot be empty"));
152 }
153
154 if !name
155 .chars()
156 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
157 {
158 return Err(anyhow::anyhow!(
159 "Project name must be in kebab-case format (lowercase letters, numbers, and hyphens only)"
160 ));
161 }
162
163 if name.starts_with('-') || name.ends_with('-') {
164 return Err(anyhow::anyhow!(
165 "Project name cannot start or end with a hyphen"
166 ));
167 }
168
169 if name.contains("--") {
170 return Err(anyhow::anyhow!(
171 "Project name cannot contain consecutive hyphens"
172 ));
173 }
174
175 Ok(())
176}
177
178fn validate_content(input: &Input) -> Result<Vec<(&'static str, validation::ValidationResult)>> {
179 let validations = vec![
180 (
181 "Vision",
182 validation::validate_content(validation::ContentType::Vision, &input.vision),
183 ),
184 (
185 "Tech Stack",
186 validation::validate_content(validation::ContentType::TechStack, &input.tech_stack),
187 ),
188 (
189 "Summary",
190 validation::validate_content(validation::ContentType::Summary, &input.summary),
191 ),
192 ];
193
194 Ok(validations)
195}