Skip to main content

concinnity_dev/command/
new.rs

1// src/cli/new.rs
2
3use std::path::{Path, PathBuf};
4
5use concinnity_cook::authoring::world::WORLD_JSONL;
6use concinnity_cook::build_from_path;
7use concinnity_host::store::paths::StateTree;
8
9// Default starter world file. Everything else a running world needs (window,
10// renderer, debug HUD) is injected at build time and recorded in
11// world-lock.json; `cn list --expanded` shows the effective world.
12//
13// The label names no Font, so it draws with the engine's built-in face. It asks
14// for `centered` itself rather than leaning on a default: unset, the greeting
15// lands at the label's default x/y, under the HUD chips in the top-left corner.
16const INIT_WORLD_JSONL: &str = r#"{"name":"hello_world","type":"TextLabel","args":{"content":"Hello, world!","centered":true}}
17"#;
18
19/// Create a new project in a new directory at `path`.
20pub fn new(path: &str) -> std::io::Result<()> {
21    // A pre-existing empty directory is a valid target; one that already holds
22    // a world is not.
23    if let Some(world) = existing_world(Path::new(path)) {
24        return Err(std::io::Error::new(
25            std::io::ErrorKind::AlreadyExists,
26            format!("'{}' already contains a {}", path, world.display()),
27        ));
28    }
29    std::fs::create_dir_all(path)?;
30    println!("Created directory '{}'", path);
31    init_in_dir(path)
32}
33
34/// Create a new project in the working directory.
35pub fn init() -> std::io::Result<()> {
36    init_in_dir(".")
37}
38
39// Write the starter world into `dir/worlds/` and run an initial build
40fn init_in_dir(dir: &str) -> std::io::Result<()> {
41    let dir = Path::new(dir);
42    if let Some(world) = existing_world(dir) {
43        println!("{} already exists, skipping init", world.display());
44        return Ok(());
45    }
46
47    let world_path = worlds_dir(dir).join(WORLD_JSONL);
48    std::fs::create_dir_all(world_path.parent().expect("the world has a directory"))?;
49    std::fs::write(&world_path, INIT_WORLD_JSONL)?;
50    println!("Created {}", world_path.display());
51
52    let world_path_str = world_path.to_str().unwrap_or(WORLD_JSONL);
53    build_from_path(
54        &crate::project::require()?,
55        world_path_str,
56        crate::cook_platform(),
57    )
58}
59
60// The world already scaffolded in `dir`, if any: the one a new project writes,
61// or the legacy `world.jsonl` at the project root.
62fn existing_world(dir: &Path) -> Option<PathBuf> {
63    [worlds_dir(dir).join(WORLD_JSONL), dir.join(WORLD_JSONL)]
64        .into_iter()
65        .find(|p| p.exists())
66}
67
68// Where a project rooted at `dir` keeps its authored worlds.
69fn worlds_dir(dir: &Path) -> PathBuf {
70    StateTree::at(dir).worlds_dir()
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    // Only the paths that stop before the initial build are exercised here;
78    // a successful `cn new` runs the full compile pipeline.
79
80    #[test]
81    fn new_refuses_a_directory_that_already_has_a_world() {
82        let dir = tempfile::tempdir().unwrap();
83        let world = worlds_dir(dir.path()).join(WORLD_JSONL);
84        std::fs::create_dir_all(world.parent().unwrap()).unwrap();
85        std::fs::write(&world, "").unwrap();
86
87        let err = new(dir.path().to_str().unwrap()).unwrap_err();
88        assert_eq!(err.kind(), std::io::ErrorKind::AlreadyExists);
89        assert!(err.to_string().contains(WORLD_JSONL), "got: {err}");
90    }
91
92    // The legacy location counts too, so `cn new` over a project written before
93    // worlds moved still refuses rather than scaffolding a second world.
94    #[test]
95    fn new_refuses_a_directory_holding_only_a_legacy_world() {
96        let dir = tempfile::tempdir().unwrap();
97        std::fs::write(dir.path().join(WORLD_JSONL), "").unwrap();
98
99        let err = new(dir.path().to_str().unwrap()).unwrap_err();
100        assert_eq!(err.kind(), std::io::ErrorKind::AlreadyExists);
101    }
102
103    #[test]
104    fn init_in_dir_skips_when_a_world_exists() {
105        let dir = tempfile::tempdir().unwrap();
106        let world = worlds_dir(dir.path()).join(WORLD_JSONL);
107        std::fs::create_dir_all(world.parent().unwrap()).unwrap();
108        std::fs::write(
109            &world,
110            "{\"name\":\"keep\",\"type\":\"Logger\",\"args\":{}}\n",
111        )
112        .unwrap();
113
114        init_in_dir(dir.path().to_str().unwrap()).unwrap();
115        // The existing world is untouched, not overwritten by the starter.
116        let content = std::fs::read_to_string(&world).unwrap();
117        assert!(content.contains("\"keep\""), "got: {content}");
118    }
119
120    #[test]
121    fn init_in_dir_skips_a_legacy_world_at_the_project_root() {
122        let dir = tempfile::tempdir().unwrap();
123        let world = dir.path().join(WORLD_JSONL);
124        std::fs::write(
125            &world,
126            "{\"name\":\"keep\",\"type\":\"Logger\",\"args\":{}}\n",
127        )
128        .unwrap();
129
130        init_in_dir(dir.path().to_str().unwrap()).unwrap();
131        assert!(
132            !worlds_dir(dir.path()).exists(),
133            "a legacy world is left in place rather than duplicated into worlds/"
134        );
135    }
136}