1use std::fs;
2use std::path::{Path, PathBuf};
3
4use crate::cli::InitKind;
5use crate::error::{DealerError, DealerResult};
6
7pub(crate) fn init_project(kind: InitKind, path: &Path) -> DealerResult<PathBuf> {
8 fs::create_dir_all(path).map_err(|error| DealerError::io(path, error))?;
9
10 let app = path.join("app.x");
11 let package = path.join("package.x");
12 if app.exists() || package.exists() {
13 return Err(DealerError::Backend(format!(
14 "project root '{}' already contains app.x or package.x",
15 path.display()
16 )));
17 }
18
19 let name = project_name(path);
20 let (root_file, content) = match kind {
21 InitKind::App => (
22 app,
23 format!("app {name}\n\tterminal.log\n\t\tmessage: \"Hello from Xtazy\"\n"),
24 ),
25 InitKind::Package => (package, format!("package {name}\n\tdeliver\n")),
26 };
27
28 fs::write(&root_file, content).map_err(|error| DealerError::io(&root_file, error))?;
29 Ok(root_file)
30}
31
32fn project_name(path: &Path) -> String {
33 path.file_name()
34 .and_then(|name| name.to_str())
35 .filter(|name| !name.is_empty() && *name != ".")
36 .map(sanitize_xtazy_ident)
37 .unwrap_or_else(|| "NewProject".to_string())
38}
39
40fn sanitize_xtazy_ident(value: &str) -> String {
41 let mut ident = String::new();
42 let mut upper_next = true;
43 for ch in value.chars() {
44 if ch.is_ascii_alphanumeric() {
45 if upper_next {
46 ident.push(ch.to_ascii_uppercase());
47 upper_next = false;
48 } else {
49 ident.push(ch);
50 }
51 } else {
52 upper_next = true;
53 }
54 }
55 if ident.is_empty() || ident.chars().next().is_some_and(|ch| ch.is_ascii_digit()) {
56 format!("Xtazy{ident}")
57 } else {
58 ident
59 }
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65 use crate::test_support::TempProject;
66
67 #[test]
68 fn init_app_creates_app_root_only() {
69 let temp = TempProject::new("init-app");
70
71 let root = init_project(InitKind::App, temp.path()).expect("app init should pass");
72
73 assert_eq!(root, temp.path().join("app.x"));
74 assert!(temp.path().join("app.x").is_file());
75 assert!(!temp.path().join("package.x").exists());
76 }
77
78 #[test]
79 fn init_refuses_existing_root() {
80 let temp = TempProject::new("init-existing");
81 fs::write(temp.path().join("app.x"), "app Existing\n").expect("app.x should be written");
82
83 let error =
84 init_project(InitKind::Package, temp.path()).expect_err("existing root should fail");
85
86 assert!(
87 error
88 .to_string()
89 .contains("already contains app.x or package.x")
90 );
91 }
92}