1use crate::config::CobbleConfig;
2use std::fs;
3use std::path::PathBuf;
4
5pub struct InitOptions {
6 pub name: Option<String>,
7 pub description: Option<String>,
8 pub pack_format: Option<String>,
9 pub template: String,
10}
11
12pub fn init(options: InitOptions) -> Result<(), String> {
13 let sample_code = sample_code_for_template(&options.template)?;
14 let requested_name = options.name.clone();
15 let has_name = requested_name.is_some();
16 let project_name = requested_name
17 .as_ref()
18 .and_then(|name| {
19 PathBuf::from(name)
20 .file_name()
21 .map(|name| name.to_string_lossy().to_string())
22 })
23 .filter(|name| !name.is_empty())
24 .or_else(|| {
25 std::env::current_dir()
26 .ok()
27 .and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string()))
28 })
29 .unwrap_or_else(|| "my-datapack".to_string());
30
31 println!("Initializing Cobble project: {}", project_name);
32
33 let project_dir = if has_name {
35 let dir = PathBuf::from(requested_name.as_ref().unwrap());
36 fs::create_dir_all(&dir)
37 .map_err(|e| format!("Failed to create project directory: {}", e))?;
38 dir
39 } else {
40 std::env::current_dir().map_err(|e| format!("Failed to get current directory: {}", e))?
41 };
42
43 let mut config = CobbleConfig::default_with_name(project_name);
45
46 if let Some(desc) = options.description {
48 config.project.description = desc;
49 }
50 if let Some(format_str) = options.pack_format {
51 use crate::pack_format::{
52 PackFormat, COBBLE_VERSION, SUPPORTED_MINECRAFT_VERSION, SUPPORTED_PACK_FORMAT,
53 };
54 let pack_fmt = PackFormat::parse_format(&format_str)?;
55
56 if !pack_fmt.is_supported() {
57 return Err(format!(
58 "pack_format must be {} (Minecraft Java Edition {}), got {}.\n\
59 Cobble v{} exclusively supports Minecraft Java Edition {}.\n\
60 See https://minecraft.wiki/w/Pack_format for version compatibility.",
61 SUPPORTED_PACK_FORMAT,
62 SUPPORTED_MINECRAFT_VERSION,
63 pack_fmt,
64 COBBLE_VERSION,
65 SUPPORTED_MINECRAFT_VERSION
66 ));
67 }
68
69 config.project.pack_format = format_str;
70 }
71
72 let config_path = project_dir.join("cobble.toml");
73
74 if config_path.exists() {
75 return Err("cobble.toml already exists".to_string());
76 }
77
78 config.save(&config_path)?;
79
80 let src_dir = project_dir.join("src");
82 fs::create_dir_all(&src_dir).map_err(|e| format!("Failed to create src directory: {}", e))?;
83
84 let main_file = src_dir.join("main.cbl");
86
87 fs::write(&main_file, sample_code).map_err(|e| format!("Failed to create main.cbl: {}", e))?;
88
89 let gitignore = project_dir.join(".gitignore");
91 let gitignore_content = r#"# Cobble output
92output/
93*.zip
94
95# Editor files
96.vscode/
97.idea/
98*.swp
99*.swo
100*~
101
102# OS files
103.DS_Store
104Thumbs.db
105"#;
106
107 fs::write(&gitignore, gitignore_content)
108 .map_err(|e| format!("Failed to create .gitignore: {}", e))?;
109
110 println!("✓ Created cobble.toml");
111 println!("✓ Created src/main.cbl");
112 println!("✓ Created .gitignore");
113 println!();
114 println!("Project initialized successfully!");
115 println!("Next steps:");
116 if has_name {
117 println!(" cd {}", project_dir.display());
118 }
119 println!(" cobble build --dry-run");
120 println!(" cobble build --validate");
121 println!(" cobble watch");
122
123 Ok(())
124}
125
126fn sample_code_for_template(template: &str) -> Result<&'static str, String> {
127 match template {
128 "minimal" => Ok(r#"def main():
129 /say Hello from Cobble
130"#),
131 "stdlib" => Ok(r#"import stdlib
132from stdlib import event
133
134def init():
135 """Initialize the data pack"""
136 /scoreboard objectives add game_score dummy "Game Score"
137 /tellraw @a {"text":"Data pack initialized!", "color":"green"}
138
139def tick():
140 """Called every game tick"""
141 pass
142
143def hello(player):
144 """Greet a player"""
145 /tellraw @a {"text":"Hello, World!", "color":"gold"}
146
147# Register event handlers
148stdlib.addEventListener(event.LOAD, init)
149stdlib.addEventListener(event.TICK, tick)
150"#),
151 "validation" => Ok(r#"import stdlib
152from stdlib import event
153
154def init():
155 /tellraw @a {"text":"Cobble validation-ready pack loaded","color":"green"}
156 score.objective.add("points", "dummy", "Points")
157 score.set("points", 0)
158 bossbar.add("progress", "Progress")
159 bossbar.set_max("progress", 100)
160 bossbar.set_players("progress", "@a")
161
162def tick():
163 score.add("points", 1)
164 bossbar.set_value("progress", 50)
165
166stdlib.addEventListener(event.LOAD, init)
167stdlib.addEventListener(event.TICK, tick)
168"#),
169 other => Err(format!(
170 "Unknown template '{}'. Expected one of: minimal, stdlib, validation",
171 other
172 )),
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn init_minimal_template_creates_minimal_source() {
182 let temp_dir = tempfile::TempDir::new().unwrap();
183 let project_dir = temp_dir.path().join("minimal_pack");
184
185 init(InitOptions {
186 name: Some(project_dir.display().to_string()),
187 description: None,
188 pack_format: None,
189 template: "minimal".to_string(),
190 })
191 .unwrap();
192
193 let source = fs::read_to_string(project_dir.join("src/main.cbl")).unwrap();
194 assert!(source.contains("def main():"));
195 assert!(!source.contains("import stdlib"));
196 }
197
198 #[test]
199 fn init_rejects_unknown_template() {
200 let temp_dir = tempfile::TempDir::new().unwrap();
201 let project_dir = temp_dir.path().join("bad_template");
202
203 let error = init(InitOptions {
204 name: Some(project_dir.display().to_string()),
205 description: None,
206 pack_format: None,
207 template: "unknown".to_string(),
208 })
209 .unwrap_err();
210
211 assert!(error.contains("Unknown template"));
212 assert!(!project_dir.exists());
213 }
214
215 #[test]
216 fn init_uses_basename_for_project_name_when_name_is_a_path() {
217 let temp_dir = tempfile::TempDir::new().unwrap();
218 let project_dir = temp_dir.path().join("nested").join("pack_name");
219
220 init(InitOptions {
221 name: Some(project_dir.display().to_string()),
222 description: None,
223 pack_format: None,
224 template: "minimal".to_string(),
225 })
226 .unwrap();
227
228 let config = fs::read_to_string(project_dir.join("cobble.toml")).unwrap();
229 assert!(config.contains(r#"name = "pack_name""#));
230 assert!(config.contains(r#"namespace = "pack_name""#));
231 }
232}