stellar_scaffold_cli/commands/
init.rs1use clap::{Args, Parser};
2use degit::degit;
3use std::fs::{copy, metadata, read_dir, remove_dir_all};
4use std::path::PathBuf;
5use std::process::Command;
6use std::{env, io};
7
8use super::{build, generate};
9use stellar_cli::{commands::global, print::Print};
10
11pub const FRONTEND_TEMPLATE: &str = "theahaco/scaffold-stellar-frontend";
12const TUTORIAL_BRANCH: &str = "tutorial";
13
14#[derive(Parser, Debug, Clone)]
16pub struct Cmd {
17 pub project_path: PathBuf,
19
20 #[command(flatten)]
21 vers: Vers,
22}
23
24#[derive(Args, Debug, Clone)]
25#[group(multiple = false)]
26struct Vers {
27 #[arg(long, default_value_t = false)]
29 pub tutorial: bool,
30
31 #[arg(long)]
33 pub tag: Option<String>,
34}
35
36#[derive(thiserror::Error, Debug)]
38pub enum Error {
39 #[error("Failed to clone template: {0}")]
40 DegitError(String),
41 #[error("Project path contains invalid UTF-8 characters and cannot be converted to a string")]
42 InvalidProjectPathEncoding,
43 #[error("IO error: {0}")]
44 IoError(#[from] io::Error),
45 #[error(transparent)]
46 GenerateError(#[from] generate::contract::Error),
47}
48
49impl Cmd {
50 pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
59 let printer: Print = Print::new(global_args.quiet);
60
61 let absolute_project_path = self.project_path.canonicalize().unwrap_or_else(|_| {
63 if self.project_path.is_absolute() {
65 self.project_path.clone()
66 } else {
67 env::current_dir()
68 .unwrap_or_default()
69 .join(&self.project_path)
70 }
71 });
72
73 printer.infoln(format!(
74 "Creating new Stellar project in {}",
75 absolute_project_path.display()
76 ));
77
78 let project_str = absolute_project_path
79 .to_str()
80 .ok_or(Error::InvalidProjectPathEncoding)?
81 .to_owned();
82
83 let mut repo = FRONTEND_TEMPLATE.to_string();
84 if let Some(tag) = self.vers.tag.as_deref() {
85 repo = format!("{repo}#{tag}");
86 } else if self.vers.tutorial {
87 repo = format!("{repo}#{TUTORIAL_BRANCH}");
88 }
89 tokio::task::spawn_blocking(move || {
90 degit(repo.as_str(), &project_str);
91 })
92 .await
93 .expect("Blocking task panicked");
94
95 if metadata(&absolute_project_path).is_err()
96 || read_dir(&absolute_project_path)?.next().is_none()
97 {
98 return Err(Error::DegitError(format!(
99 "Failed to clone template into {}: directory is empty or missing",
100 absolute_project_path.display()
101 )));
102 }
103
104 let example_path = absolute_project_path.join(".env.example");
106 let env_path = absolute_project_path.join(".env");
107 copy(example_path, env_path)?;
108
109 if !self.vers.tutorial {
111 let example_contracts = ["oz/nft-enumerable", "oz/fungible-allowlist"];
112
113 for contract in example_contracts {
114 self.update_oz_example(&absolute_project_path, contract, global_args)
115 .await?;
116 }
117 }
118
119 let npm_status = npm_install(&absolute_project_path, &printer);
121
122 printer.infoln("Building contracts and generating client code...");
124 let mut build_command = build::Command::parse_from(["build", "--build-clients"]);
126 build_command.build.manifest_path = Some(absolute_project_path.join("Cargo.toml"));
127 build_command.build_clients_args.env = Some(build::clients::ScaffoldEnv::Development);
128 build_command.build_clients_args.workspace_root = Some(absolute_project_path.clone());
129 let mut build_args = global_args.clone();
130 if !(global_args.verbose && global_args.very_verbose) {
131 build_args.quiet = true;
132 }
133
134 if let Err(e) = build_command.run(&build_args).await {
135 printer.warnln(format!("Failed to build contract clients: {e}"));
136 }
137
138 if git_exists() {
140 git_init(&absolute_project_path);
141 git_add(&absolute_project_path, &["-A"]);
142 git_commit(&absolute_project_path, "initial commit");
143 }
144
145 printer.blankln("\n\n");
146 printer.checkln(format!(
147 "Project successfully created at {}!",
148 absolute_project_path.display()
149 ));
150 printer.blankln(" You can now run the application with:\n");
151 printer.blankln(format!("\tcd {}", self.project_path.display()));
152 if !npm_status {
153 printer.blankln("\tnpm install");
154 }
155 printer.blankln("\tnpm start\n");
156 printer.blankln(" Happy hacking! 🚀");
157 Ok(())
158 }
159
160 async fn update_oz_example(
165 &self,
166 absolute_project_path: &PathBuf,
167 example_name: &str,
168 global_args: &global::Args,
169 ) -> Result<(), Error> {
170 let mut example_path = example_name;
171 if example_name.starts_with("oz/") {
172 (_, example_path) = example_name.split_at(3);
173 }
174
175 let printer = Print::new(global_args.quiet);
176 let original_dir = env::current_dir()?;
177 env::set_current_dir(absolute_project_path)?;
178
179 let all_contracts_path = absolute_project_path.join("contracts");
180 let existing_contract_path = all_contracts_path.join(example_path);
181
182 if existing_contract_path.exists() {
183 remove_dir_all(&existing_contract_path)?;
184 }
185
186 let mut quiet_global_args = global_args.clone();
187 quiet_global_args.quiet = false;
188
189 let result = generate::contract::Cmd {
190 from: Some(example_name.to_owned()),
191 ls: false,
192 from_wizard: false,
193 output: Some(all_contracts_path.join(example_path)),
194 force: false,
195 }
196 .run(&quiet_global_args)
197 .await;
198
199 let _ = env::set_current_dir(original_dir);
201
202 match result {
203 Ok(()) => {
204 printer.infoln(format!(
205 "Successfully added OpenZeppelin example contract: {example_path}"
206 ));
207 }
208 Err(generate::contract::Error::OzExampleNotFound(_)) => {
209 printer.infoln(format!(
210 "Skipped missing OpenZeppelin example contract: {example_path}"
211 ));
212 }
213 Err(e) => {
214 printer.warnln(format!(
215 "Failed to generate example contract: {example_path}\n{e}"
216 ));
217 }
218 }
219
220 Ok(())
221 }
222}
223
224fn npm_exists() -> bool {
226 Command::new("npm").arg("--version").output().is_ok()
227}
228
229fn npm_install(path: &PathBuf, printer: &Print) -> bool {
231 if !npm_exists() {
232 printer.warnln("Failed to install dependencies, npm is not installed");
233 return false;
234 }
235
236 printer.infoln("Installing npm dependencies...");
237 match Command::new("npm")
238 .arg("install")
239 .current_dir(path)
240 .output()
241 {
242 Ok(output) if output.status.success() => true,
243 Ok(output) => {
244 printer.warnln("Failed to install dependencies: Please run 'npm install' manually");
247 if !output.stderr.is_empty()
248 && let Ok(stderr) = String::from_utf8(output.stderr)
249 {
250 printer.warnln(format!("Error: {}", stderr.trim()));
251 }
252 false
253 }
254 Err(e) => {
255 printer.warnln(format!("Failed to run npm install: {e}"));
256 false
257 }
258 }
259}
260
261fn git_exists() -> bool {
263 Command::new("git").arg("--version").output().is_ok()
264}
265
266fn git_init(path: &PathBuf) {
268 let _ = Command::new("git").arg("init").current_dir(path).output();
269}
270
271fn git_add(path: &PathBuf, rest: &[&str]) {
273 let mut args = vec!["add"];
274 args.extend_from_slice(rest);
275 let _ = Command::new("git").args(args).current_dir(path).output();
276}
277
278fn git_commit(path: &PathBuf, message: &str) {
280 let _ = Command::new("git")
281 .args(["commit", "-m", message])
282 .current_dir(path)
283 .output();
284}