1use std::io::{self, Write};
2use std::path::{Path, PathBuf};
3use std::process::{Command, Stdio};
4use std::str::FromStr;
5
6use anyhow::{bail, format_err};
7use clap::Parser;
8use fs_err as fs;
9use fs_err::OpenOptions;
10
11use super::resolve_path;
12
13static MODEL_PROJECT: &str =
14 include_str!("../../assets/default-model-project/default.project.json");
15static MODEL_README: &str = include_str!("../../assets/default-model-project/README.md");
16static MODEL_INIT: &str = include_str!("../../assets/default-model-project/src-init.luau");
17static MODEL_GIT_IGNORE: &str = include_str!("../../assets/default-model-project/gitignore.txt");
18
19static PLACE_PROJECT: &str =
20 include_str!("../../assets/default-place-project/default.project.json");
21static PLACE_README: &str = include_str!("../../assets/default-place-project/README.md");
22static PLACE_GIT_IGNORE: &str = include_str!("../../assets/default-place-project/gitignore.txt");
23
24static PLUGIN_PROJECT: &str =
25 include_str!("../../assets/default-plugin-project/default.project.json");
26static PLUGIN_README: &str = include_str!("../../assets/default-plugin-project/README.md");
27static PLUGIN_GIT_IGNORE: &str = include_str!("../../assets/default-plugin-project/gitignore.txt");
28
29#[derive(Debug, Parser)]
31pub struct InitCommand {
32 #[clap(default_value = "")]
34 pub path: PathBuf,
35
36 #[clap(long, default_value = "place")]
38 pub kind: InitKind,
39}
40
41impl InitCommand {
42 pub fn run(self) -> anyhow::Result<()> {
43 let base_path = resolve_path(&self.path);
44 fs::create_dir_all(&base_path)?;
45
46 let canonical = fs::canonicalize(&base_path)?;
47 let project_name = canonical
48 .file_name()
49 .and_then(|name| name.to_str())
50 .unwrap_or("new-project");
51
52 let project_params = ProjectParams {
53 name: project_name.to_owned(),
54 };
55
56 match self.kind {
57 InitKind::Place => init_place(&base_path, project_params)?,
58 InitKind::Model => init_model(&base_path, project_params)?,
59 InitKind::Plugin => init_plugin(&base_path, project_params)?,
60 }
61
62 println!("Created project successfully.");
63
64 Ok(())
65 }
66}
67
68#[derive(Debug, Clone, Copy)]
70pub enum InitKind {
71 Place,
73
74 Model,
76
77 Plugin,
79}
80
81impl FromStr for InitKind {
82 type Err = anyhow::Error;
83
84 fn from_str(source: &str) -> Result<Self, Self::Err> {
85 match source {
86 "place" => Ok(InitKind::Place),
87 "model" => Ok(InitKind::Model),
88 "plugin" => Ok(InitKind::Plugin),
89 _ => Err(format_err!(
90 "Invalid init kind '{}'. Valid kinds are: place, model, plugin",
91 source
92 )),
93 }
94 }
95}
96
97fn init_place(base_path: &Path, project_params: ProjectParams) -> anyhow::Result<()> {
98 println!("Creating new place project '{}'", project_params.name);
99
100 let project_file = project_params.render_template(PLACE_PROJECT);
101 try_create_project(base_path, &project_file)?;
102
103 let readme = project_params.render_template(PLACE_README);
104 write_if_not_exists(&base_path.join("README.md"), &readme)?;
105
106 let src = base_path.join("src");
107 fs::create_dir_all(&src)?;
108
109 let src_shared = src.join("shared");
110 fs::create_dir_all(src.join(&src_shared))?;
111
112 let src_server = src.join("server");
113 fs::create_dir_all(src.join(&src_server))?;
114
115 let src_client = src.join("client");
116 fs::create_dir_all(src.join(&src_client))?;
117
118 write_if_not_exists(
119 &src_shared.join("Hello.luau"),
120 "return function()\n\tprint(\"Hello, world!\")\nend",
121 )?;
122
123 write_if_not_exists(
124 &src_server.join("init.server.luau"),
125 "print(\"Hello world, from server!\")",
126 )?;
127
128 write_if_not_exists(
129 &src_client.join("init.client.luau"),
130 "print(\"Hello world, from client!\")",
131 )?;
132
133 let git_ignore = project_params.render_template(PLACE_GIT_IGNORE);
134 try_git_init(base_path, &git_ignore)?;
135
136 Ok(())
137}
138
139fn init_model(base_path: &Path, project_params: ProjectParams) -> anyhow::Result<()> {
140 println!("Creating new model project '{}'", project_params.name);
141
142 let project_file = project_params.render_template(MODEL_PROJECT);
143 try_create_project(base_path, &project_file)?;
144
145 let readme = project_params.render_template(MODEL_README);
146 write_if_not_exists(&base_path.join("README.md"), &readme)?;
147
148 let src = base_path.join("src");
149 fs::create_dir_all(&src)?;
150
151 let init = project_params.render_template(MODEL_INIT);
152 write_if_not_exists(&src.join("init.luau"), &init)?;
153
154 let git_ignore = project_params.render_template(MODEL_GIT_IGNORE);
155 try_git_init(base_path, &git_ignore)?;
156
157 Ok(())
158}
159
160fn init_plugin(base_path: &Path, project_params: ProjectParams) -> anyhow::Result<()> {
161 println!("Creating new plugin project '{}'", project_params.name);
162
163 let project_file = project_params.render_template(PLUGIN_PROJECT);
164 try_create_project(base_path, &project_file)?;
165
166 let readme = project_params.render_template(PLUGIN_README);
167 write_if_not_exists(&base_path.join("README.md"), &readme)?;
168
169 let src = base_path.join("src");
170 fs::create_dir_all(&src)?;
171
172 write_if_not_exists(
173 &src.join("init.server.luau"),
174 "print(\"Hello world, from plugin!\")\n",
175 )?;
176
177 let git_ignore = project_params.render_template(PLUGIN_GIT_IGNORE);
178 try_git_init(base_path, &git_ignore)?;
179
180 Ok(())
181}
182
183struct ProjectParams {
185 name: String,
186}
187
188impl ProjectParams {
189 fn render_template(&self, template: &str) -> String {
191 template
192 .replace("{project_name}", &self.name)
193 .replace("{rojo_version}", env!("CARGO_PKG_VERSION"))
194 }
195}
196
197fn try_git_init(path: &Path, git_ignore: &str) -> Result<(), anyhow::Error> {
199 if should_git_init(path) {
200 log::debug!("Initializing Git repository...");
201
202 let status = Command::new("git").arg("init").current_dir(path).status()?;
203
204 if !status.success() {
205 bail!("git init failed: status code {:?}", status.code());
206 }
207 }
208
209 write_if_not_exists(&path.join(".gitignore"), git_ignore)?;
210
211 Ok(())
212}
213
214fn should_git_init(path: &Path) -> bool {
219 let result = Command::new("git")
220 .args(["rev-parse", "--is-inside-work-tree"])
221 .stdout(Stdio::null())
222 .stderr(Stdio::null())
223 .current_dir(path)
224 .status();
225
226 match result {
227 Ok(status) => !status.success(),
230
231 Err(_) => false,
233 }
234}
235
236fn write_if_not_exists(path: &Path, contents: &str) -> Result<(), anyhow::Error> {
238 let file_res = OpenOptions::new().write(true).create_new(true).open(path);
239
240 let mut file = match file_res {
241 Ok(file) => file,
242 Err(err) => {
243 return match err.kind() {
244 io::ErrorKind::AlreadyExists => return Ok(()),
245 _ => Err(err.into()),
246 }
247 }
248 };
249
250 file.write_all(contents.as_bytes())?;
251
252 Ok(())
253}
254
255fn try_create_project(base_path: &Path, contents: &str) -> Result<(), anyhow::Error> {
257 let project_path = base_path.join("default.project.json");
258
259 let file_res = OpenOptions::new()
260 .write(true)
261 .create_new(true)
262 .open(&project_path);
263
264 let mut file = match file_res {
265 Ok(file) => file,
266 Err(err) => {
267 return match err.kind() {
268 io::ErrorKind::AlreadyExists => {
269 bail!("Project file already exists: {}", project_path.display())
270 }
271 _ => Err(err.into()),
272 }
273 }
274 };
275
276 file.write_all(contents.as_bytes())?;
277
278 Ok(())
279}