1use crate::cli::InitArgs;
6use crate::commands::{templates, utils};
7use crate::error::{CliError, Result};
8use crate::ui;
9use std::fs;
10use std::path::Path;
11
12pub async fn execute(args: InitArgs) -> Result<()> {
34 let project_name = determine_project_name(&args)?;
36 validate_project_name(&project_name)?;
37
38 ui::info(&format!("Creating project: {}", project_name));
39
40 let template = select_template(&args)?;
42 ui::info(&format!("Using template: {}", template.name()));
43
44 let project_dir = Path::new(&project_name);
46 if project_dir.exists() {
47 return Err(CliError::InvalidArgument(format!(
48 "Directory '{}' already exists",
49 project_name
50 )));
51 }
52
53 fs::create_dir(project_dir)?;
54 ui::success(&format!("Created directory: {}", project_name));
55
56 generate_project_files(project_dir, &project_name, template)?;
58
59 let pkg_mgr = if args.use_pnpm {
61 utils::PackageManager::Pnpm
62 } else if args.use_yarn {
63 utils::PackageManager::Yarn
64 } else {
65 utils::PackageManager::Npm
67 };
68
69 print_next_steps(&project_name, pkg_mgr);
71
72 ui::success("Project created successfully!");
73 Ok(())
74}
75
76fn determine_project_name(args: &InitArgs) -> Result<String> {
78 if let Some(ref name) = args.name {
79 Ok(name.clone())
80 } else {
81 let cwd = utils::get_cwd()?;
83 let dir_name = cwd
84 .file_name()
85 .and_then(|n| n.to_str())
86 .ok_or_else(|| CliError::InvalidArgument("Invalid directory name".to_string()))?;
87 Ok(dir_name.to_string())
88 }
89}
90
91const RESERVED_NAMES: &[&str] = &[
93 "node_modules",
94 "favicon.ico",
95 "index.js",
96 "index.html",
97 "package.json",
98 "tsconfig.json",
99 "fob.config.json",
100 ".git",
101 ".gitignore",
102 ".DS_Store",
103];
104
105fn validate_project_name(name: &str) -> Result<()> {
107 if name.is_empty() {
108 return Err(CliError::InvalidArgument(
109 "Project name cannot be empty".to_string(),
110 ));
111 }
112
113 if RESERVED_NAMES.contains(&name) {
115 return Err(CliError::InvalidArgument(format!(
116 "Project name '{}' is reserved and cannot be used",
117 name
118 )));
119 }
120
121 if !name
123 .chars()
124 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
125 {
126 return Err(CliError::InvalidArgument(
127 "Project name can only contain letters, numbers, hyphens, and underscores".to_string(),
128 ));
129 }
130
131 if name.starts_with('.') || name.as_bytes()[0].is_ascii_digit() {
134 return Err(CliError::InvalidArgument(
135 "Project name cannot start with a dot or number".to_string(),
136 ));
137 }
138
139 Ok(())
140}
141
142fn select_template(args: &InitArgs) -> Result<templates::Template> {
148 if let Some(ref template_name) = args.template {
149 templates::Template::from_str(template_name).ok_or_else(|| {
150 CliError::InvalidArgument(format!(
151 "Invalid template '{}'. Available: library, app, component-library, meta-framework",
152 template_name
153 ))
154 })
155 } else if args.yes {
156 Ok(templates::Template::Library)
158 } else {
159 ui::info("Defaulting to 'library' template (use --template to specify)");
162 Ok(templates::Template::Library)
163 }
164}
165
166fn generate_project_files(
168 project_dir: &Path,
169 project_name: &str,
170 template: templates::Template,
171) -> Result<()> {
172 ui::info("Generating files...");
173
174 let src_dir = project_dir.join("src");
176 fs::create_dir(&src_dir)?;
177
178 let package_json = templates::package_json(project_name, template);
180 fs::write(project_dir.join("package.json"), package_json)?;
181 ui::success(" Created package.json");
182
183 let tsconfig = templates::tsconfig_json(template);
185 fs::write(project_dir.join("tsconfig.json"), tsconfig)?;
186 ui::success(" Created tsconfig.json");
187
188 let joy_config = templates::joy_config_json(template);
190 fs::write(project_dir.join("fob.config.json"), joy_config)?;
191 ui::success(" Created fob.config.json");
192
193 let source_file = templates::source_file(template);
195 let source_filename = match template {
196 templates::Template::Library => "index.ts",
197 templates::Template::App => "main.ts",
198 templates::Template::ComponentLibrary => "index.ts",
199 templates::Template::MetaFramework => "index.ts",
200 };
201 fs::write(src_dir.join(source_filename), source_file)?;
202 ui::success(&format!(" Created src/{}", source_filename));
203
204 match template {
206 templates::Template::App => {
207 let index_html = templates::index_html(project_name);
209 fs::write(project_dir.join("index.html"), index_html)?;
210 ui::success(" Created index.html");
211
212 let app_css = templates::app_css();
214 fs::write(src_dir.join("app.css"), app_css)?;
215 ui::success(" Created src/app.css");
216 }
217 templates::Template::Library => {
218 }
220 templates::Template::ComponentLibrary => {
221 let button_content = templates::button_component();
223 fs::write(src_dir.join("Button.tsx"), button_content)?;
224 ui::success(" Created src/Button.tsx");
225 }
226 templates::Template::MetaFramework => {
227 let router_content = templates::router_module();
229 let server_content = templates::server_module();
230 fs::write(src_dir.join("router.ts"), router_content)?;
231 ui::success(" Created src/router.ts");
232 fs::write(src_dir.join("server.ts"), server_content)?;
233 ui::success(" Created src/server.ts");
234 }
235 }
236
237 let gitignore = templates::gitignore();
239 fs::write(project_dir.join(".gitignore"), gitignore)?;
240 ui::success(" Created .gitignore");
241
242 let readme = templates::readme(project_name, template);
244 fs::write(project_dir.join("README.md"), readme)?;
245 ui::success(" Created README.md");
246
247 Ok(())
248}
249
250fn print_next_steps(project_name: &str, pkg_mgr: utils::PackageManager) {
252 eprintln!();
253 ui::info("Next steps:");
254 eprintln!();
255 eprintln!(" cd {}", project_name);
256 eprintln!(" {}", pkg_mgr.install_cmd());
257 eprintln!(" {} run dev", pkg_mgr.command());
258 eprintln!();
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn test_validate_project_name_valid() {
267 assert!(validate_project_name("my-project").is_ok());
268 assert!(validate_project_name("my_project").is_ok());
269 assert!(validate_project_name("project123").is_ok());
270 }
271
272 #[test]
273 fn test_validate_project_name_empty() {
274 assert!(validate_project_name("").is_err());
275 }
276
277 #[test]
278 fn test_validate_project_name_starts_with_dot() {
279 assert!(validate_project_name(".hidden").is_err());
280 }
281
282 #[test]
283 fn test_validate_project_name_starts_with_number() {
284 assert!(validate_project_name("123project").is_err());
285 }
286
287 #[test]
288 fn test_validate_project_name_invalid_chars() {
289 assert!(validate_project_name("my@project").is_err());
290 assert!(validate_project_name("my project").is_err());
291 assert!(validate_project_name("my/project").is_err());
292 }
293
294 #[test]
295 fn test_validate_project_name_reserved() {
296 assert!(validate_project_name("node_modules").is_err());
297 assert!(validate_project_name("favicon.ico").is_err());
298 assert!(validate_project_name("index.js").is_err());
299 assert!(validate_project_name("package.json").is_err());
300 }
301
302 #[test]
303 fn test_select_template_from_args() {
304 let args = InitArgs {
305 name: None,
306 template: Some("library".to_string()),
307 yes: false,
308 use_npm: false,
309 use_yarn: false,
310 use_pnpm: false,
311 };
312
313 assert_eq!(
314 select_template(&args).unwrap(),
315 templates::Template::Library
316 );
317 }
318
319 #[test]
320 fn test_select_template_with_yes_flag() {
321 let args = InitArgs {
322 name: None,
323 template: None,
324 yes: true,
325 use_npm: false,
326 use_yarn: false,
327 use_pnpm: false,
328 };
329
330 assert_eq!(
331 select_template(&args).unwrap(),
332 templates::Template::Library
333 );
334 }
335
336 #[test]
337 fn test_select_template_invalid() {
338 let args = InitArgs {
339 name: None,
340 template: Some("invalid".to_string()),
341 yes: false,
342 use_npm: false,
343 use_yarn: false,
344 use_pnpm: false,
345 };
346
347 assert!(select_template(&args).is_err());
348 }
349}