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 git_exists() {
111 git_init(&absolute_project_path);
112 git_add(&absolute_project_path, &["-A"]);
113 git_commit(&absolute_project_path, "initial commit");
114 }
115
116 if !self.vers.tutorial {
118 let example_contracts = ["nft-enumerable", "fungible-allowlist"];
119
120 for contract in example_contracts {
121 self.update_oz_example(&absolute_project_path, contract, global_args)
122 .await?;
123 }
124 }
125
126 printer.infoln("Installing npm dependencies...");
128 let npm_install_command = Command::new("npm")
129 .arg("install")
130 .current_dir(&absolute_project_path)
131 .output()?;
132 if !npm_install_command.status.success() {
133 printer.warnln(
134 "Failed to install dependencies, run 'npm install' in the project directory",
135 );
136 }
137
138 printer.infoln("Building contracts and generating client code...");
140 let mut build_command = build::Command::parse_from(["build", "--build-clients"]);
142 build_command.build.manifest_path = Some(absolute_project_path.join("Cargo.toml"));
143 build_command.build_clients_args.env = Some(build::clients::ScaffoldEnv::Development);
144 build_command.build_clients_args.workspace_root = Some(absolute_project_path.clone());
145 let mut build_args = global_args.clone();
146 if !(global_args.verbose && global_args.very_verbose) {
147 build_args.quiet = true;
148 }
149
150 if let Err(e) = build_command.run(&build_args).await {
151 printer.warnln(format!("Failed to build contract clients: {e}"));
152 }
153
154 printer.blankln("\n\n");
155 printer.checkln(format!(
156 "Project successfully created at {}!",
157 absolute_project_path.display()
158 ));
159 printer.blankln(" You can now run the application with:\n");
160 printer.blankln(format!("\tcd {}", self.project_path.display()));
161 if !npm_install_command.status.success() {
162 printer.blankln("\tnpm install");
163 }
164 printer.blankln("\tnpm start\n");
165 printer.blankln(" Happy hacking! 🚀");
166 Ok(())
167 }
168
169 async fn update_oz_example(
174 &self,
175 absolute_project_path: &PathBuf,
176 contract_path: &str,
177 global_args: &global::Args,
178 ) -> Result<(), Error> {
179 let printer = Print::new(global_args.quiet);
180 let original_dir = env::current_dir()?;
181 env::set_current_dir(absolute_project_path)?;
182
183 let contracts_path = absolute_project_path.join("contracts");
184 let existing_contract_path = contracts_path.join(contract_path);
185
186 if existing_contract_path.exists() {
187 remove_dir_all(&existing_contract_path)?;
188 }
189
190 let mut quiet_global_args = global_args.clone();
191 quiet_global_args.quiet = true;
192
193 let result = generate::contract::Cmd {
194 from: Some(contract_path.to_owned()),
195 ls: false,
196 from_wizard: false,
197 output: Some(
198 contracts_path
199 .join(contract_path)
200 .to_string_lossy()
201 .into_owned(),
202 ),
203 }
204 .run(&quiet_global_args)
205 .await;
206
207 let _ = env::set_current_dir(original_dir);
209
210 match result {
211 Ok(()) => {
212 printer.infoln(format!(
213 "Successfully added OpenZeppelin example contract: {contract_path}"
214 ));
215 }
216 Err(generate::contract::Error::ExampleNotFound(_)) => {
217 printer.infoln(format!(
218 "Skipped missing OpenZeppelin example contract: {contract_path}"
219 ));
220 }
221 Err(e) => {
222 printer.warnln(format!(
223 "Failed to generate example contract: {contract_path}\n{e}"
224 ));
225 }
226 }
227
228 Ok(())
229 }
230}
231
232fn git_exists() -> bool {
234 Command::new("git").arg("--version").output().is_err()
235}
236
237fn git_init(path: &PathBuf) {
239 let _ = Command::new("git").arg("init").current_dir(path).output();
240}
241
242fn git_add(path: &PathBuf, rest: &[&str]) {
244 let mut args = vec!["add"];
245 args.extend_from_slice(rest);
246 let _ = Command::new("git").args(args).current_dir(path).output();
247}
248
249fn git_commit(path: &PathBuf, message: &str) {
251 let _ = Command::new("git")
252 .args(["commit", "-m", message])
253 .current_dir(path)
254 .output();
255}