Skip to main content

cobble/commands/
init.rs

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    // Create project directory if a name was provided
34    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    // Create cobble.toml
44    let mut config = CobbleConfig::default_with_name(project_name);
45
46    // Apply custom description and pack_format if provided
47    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    // Create src directory
81    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    // Create main.cbl with sample code
85    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    // Create .gitignore
90    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}