1use anyhow::{Context, Result};
4
5use crate::core::foundry;
6use crate::core::validation::{ContentType, validate_content};
7use crate::types::project::ProjectConfig;
8use crate::types::responses::{AnalyzeProjectResponse, FoundryResponse, ValidationStatus};
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<AnalyzeProjectResponse>> {
19 let foundry = foundry::get_default_foundry()?;
20
21 validate_project_name(&input.project_name).with_context(|| "Project name validation failed")?;
22
23 validate_content_sizes(&input.vision, &input.tech_stack, &input.summary)
24 .with_context(|| "Content size validation failed")?;
25
26 if foundry
27 .project_exists(&input.project_name)
28 .await
29 .with_context(|| format!("Failed to check if project '{}' exists", input.project_name))?
30 {
31 return Err(anyhow::anyhow!(
32 "Project '{}' already exists. Use MCP to discover existing projects: {{\"name\": \"list_projects\", \"arguments\": {{}}}} or choose a different name.",
33 input.project_name
34 ));
35 }
36
37 let vision_validation = validate_content(ContentType::Vision, &input.vision);
38 let tech_stack_validation = validate_content(ContentType::TechStack, &input.tech_stack);
39 let summary_validation = validate_content(ContentType::Summary, &input.summary);
40
41 let mut validation_errors = Vec::new();
42 if !vision_validation.is_valid {
43 validation_errors.extend(vision_validation.errors);
44 }
45 if !tech_stack_validation.is_valid {
46 validation_errors.extend(tech_stack_validation.errors);
47 }
48 if !summary_validation.is_valid {
49 validation_errors.extend(summary_validation.errors);
50 }
51
52 if !validation_errors.is_empty() {
53 let error_count = validation_errors.len();
54 return Err(anyhow::anyhow!(
55 "Content validation failed with {} error(s):\n{}",
56 error_count,
57 validation_errors.join("\n")
58 ));
59 }
60
61 let project_config = ProjectConfig {
63 name: input.project_name.clone(),
64 vision: input.vision,
65 tech_stack: input.tech_stack,
66 summary: input.summary,
67 };
68
69 foundry
70 .create_project(project_config)
71 .await
72 .with_context(|| format!("Failed to create project '{}'", input.project_name))?;
73
74 let files_created = vec![
75 "vision.md".to_string(),
76 "tech-stack.md".to_string(),
77 "summary.md".to_string(),
78 "specs/".to_string(),
79 ];
80
81 let response_data = AnalyzeProjectResponse {
82 project_name: input.project_name.clone(),
83 files_created,
84 };
85
86 let next_steps = vec![
87 format!(
88 "Project '{}' analyzed and structure created from your codebase analysis",
89 input.project_name
90 ),
91 "Your analyzed content has been structured and is ready for development work".to_string(),
92 format!(
93 "Create a spec: {{\"name\": \"create_spec\", \"arguments\": {{\"project_name\": \"{}\", \"feature_name\": \"<feature>\", \"spec\": \"...\", \"tasks\": \"...\", \"notes\": \"...\"}}}}; Load project: {{\"name\": \"load_project\", \"arguments\": {{\"project_name\": \"{}\"}}}}; Or continue codebase analysis",
94 input.project_name, input.project_name
95 ),
96 ];
97
98 let mut workflow_hints: Vec<String> = vec![
99 "Project structure created based on your codebase analysis".to_string(),
100 format!("Create a spec when you identify a feature: {{\"name\": \"create_spec\", \"arguments\": {{\"project_name\": \"{}\", \"feature_name\": \"<feature>\", \"spec\": \"...\", \"tasks\": \"...\", \"notes\": \"...\"}}}}", input.project_name),
101 format!("Load full project context: {{\"name\": \"load_project\", \"arguments\": {{\"project_name\": \"{}\"}}}}", input.project_name),
102 "You can continue using codebase_search, grep_search, read_file for deeper analysis".to_string(),
103 "Tool selection guidance: {\"name\": \"get_foundry_help\", \"arguments\": {\"topic\": \"decision-points\"}}".to_string(),
104 ];
105
106 workflow_hints.extend(vision_validation.suggestions);
107 workflow_hints.extend(tech_stack_validation.suggestions);
108 workflow_hints.extend(summary_validation.suggestions);
109
110 Ok(FoundryResponse {
111 data: response_data,
112 next_steps,
113 validation_status: ValidationStatus::Complete,
114 workflow_hints,
115 })
116}
117
118fn validate_project_name(name: &str) -> Result<()> {
119 if name.trim().is_empty() {
120 return Err(anyhow::anyhow!(
121 "Project name cannot be empty. Please provide a descriptive project name."
122 ));
123 }
124 if name.len() > 100 {
125 return Err(anyhow::anyhow!(
126 "Project name too long ({} characters). Please keep it under 100 characters for filesystem compatibility.",
127 name.len()
128 ));
129 }
130 let invalid_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|'];
131 if let Some(invalid_char) = name.chars().find(|c| invalid_chars.contains(c)) {
132 return Err(anyhow::anyhow!(
133 "Project name contains invalid character '{}'. Please use only letters, numbers, hyphens, and underscores.",
134 invalid_char
135 ));
136 }
137 let reserved_names = ["CON", "PRN", "AUX", "NUL"];
138 let upper_name = name.to_uppercase();
139 if reserved_names.contains(&upper_name.as_str()) {
140 return Err(anyhow::anyhow!(
141 "Project name '{}' is reserved by the operating system. Please choose a different name.",
142 name
143 ));
144 }
145 Ok(())
146}
147
148fn validate_content_sizes(vision: &str, tech_stack: &str, summary: &str) -> Result<()> {
149 const MAX_CONTENT_SIZE: usize = 50_000;
150 if vision.len() > MAX_CONTENT_SIZE {
151 return Err(anyhow::anyhow!(
152 "Vision content too large ({} characters). Please keep it under {} characters for optimal performance.",
153 vision.len(),
154 MAX_CONTENT_SIZE
155 ));
156 }
157 if tech_stack.len() > MAX_CONTENT_SIZE {
158 return Err(anyhow::anyhow!(
159 "Tech stack content too large ({} characters). Please keep it under {} characters for optimal performance.",
160 tech_stack.len(),
161 MAX_CONTENT_SIZE
162 ));
163 }
164 if summary.len() > MAX_CONTENT_SIZE {
165 return Err(anyhow::anyhow!(
166 "Summary content too large ({} characters). Please keep it under {} characters for optimal performance.",
167 summary.len(),
168 MAX_CONTENT_SIZE
169 ));
170 }
171 Ok(())
172}