stellar_scaffold_cli/commands/
init.rs1use clap::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
11const FRONTEND_TEMPLATE: &str = "https://github.com/theahaco/scaffold-stellar-frontend";
12
13#[derive(Parser, Debug, Clone)]
15pub struct Cmd {
16 pub project_path: PathBuf,
18}
19
20#[derive(thiserror::Error, Debug)]
22pub enum Error {
23 #[error("Failed to clone template: {0}")]
24 DegitError(String),
25 #[error("Project path contains invalid UTF-8 characters and cannot be converted to a string")]
26 InvalidProjectPathEncoding,
27 #[error("IO error: {0}")]
28 IoError(#[from] io::Error),
29 #[error(transparent)]
30 GenerateError(#[from] generate::contract::Error),
31}
32
33impl Cmd {
34 pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
43 let printer: Print = Print::new(global_args.quiet);
44
45 let absolute_project_path = self.project_path.canonicalize().unwrap_or_else(|_| {
47 if self.project_path.is_absolute() {
49 self.project_path.clone()
50 } else {
51 env::current_dir()
52 .unwrap_or_default()
53 .join(&self.project_path)
54 }
55 });
56
57 printer.infoln(format!(
58 "Creating new Stellar project in {}",
59 absolute_project_path.display()
60 ));
61
62 let project_str = absolute_project_path
63 .to_str()
64 .ok_or(Error::InvalidProjectPathEncoding)?;
65
66 degit(FRONTEND_TEMPLATE, project_str);
67
68 if metadata(&absolute_project_path).is_err()
69 || read_dir(&absolute_project_path)?.next().is_none()
70 {
71 return Err(Error::DegitError(format!(
72 "Failed to clone template into {project_str}: directory is empty or missing",
73 )));
74 }
75
76 let example_path = absolute_project_path.join(".env.example");
78 let env_path = absolute_project_path.join(".env");
79 copy(example_path, env_path)?;
80
81 if git_exists() {
83 git_init(&absolute_project_path);
84 git_add(&absolute_project_path, &["-A"]);
85 git_commit(&absolute_project_path, "initial commit");
86 }
87
88 let example_contracts = ["nft-enumerable", "fungible-allowlist"];
90
91 for contract in example_contracts {
92 self.update_oz_example(&absolute_project_path, contract, global_args)
93 .await?;
94 }
95
96 printer.infoln("Installing npm dependencies...");
98 let npm_install_command = Command::new("npm")
99 .arg("install")
100 .current_dir(&absolute_project_path)
101 .output()?;
102 if !npm_install_command.status.success() {
103 printer.warnln(
104 "Failed to install dependencies, run 'npm install' in the project directory",
105 );
106 }
107
108 printer.infoln("Building contracts and generating client code...");
110 let mut build_command = build::Command::parse_from(["build", "--build-clients"]);
112 build_command.build.manifest_path = Some(absolute_project_path.join("Cargo.toml"));
113 build_command.build_clients_args.env = Some(build::clients::ScaffoldEnv::Development);
114 build_command.build_clients_args.workspace_root = Some(absolute_project_path.clone());
115 let mut build_args = global_args.clone();
116 if !(global_args.verbose && global_args.very_verbose) {
117 build_args.quiet = true;
118 }
119
120 if let Err(e) = build_command.run(&build_args).await {
121 printer.warnln(format!("Failed to build contract clients: {e}"));
122 }
123
124 printer.blankln("\n\n");
125 printer.checkln(format!("Project successfully created at {project_str}"));
126 printer.blankln(" You can now run the application with:\n");
127 printer.blankln(format!("\tcd {}", self.project_path.display()));
128 if !npm_install_command.status.success() {
129 printer.blankln("\tnpm install");
130 }
131 printer.blankln("\tnpm start\n");
132 printer.blankln(" Happy hacking! 🚀");
133 Ok(())
134 }
135
136 async fn update_oz_example(
141 &self,
142 absolute_project_path: &PathBuf,
143 contract_path: &str,
144 global_args: &global::Args,
145 ) -> Result<(), Error> {
146 let printer = Print::new(global_args.quiet);
147 let original_dir = env::current_dir()?;
148 env::set_current_dir(absolute_project_path)?;
149
150 let contracts_path = absolute_project_path.join("contracts");
151 let existing_contract_path = contracts_path.join(contract_path);
152
153 if existing_contract_path.exists() {
154 remove_dir_all(&existing_contract_path)?;
155 }
156
157 let mut quiet_global_args = global_args.clone();
158 quiet_global_args.quiet = true;
159
160 let result = generate::contract::Cmd {
161 from: Some(contract_path.to_owned()),
162 ls: false,
163 from_wizard: false,
164 output: Some(
165 contracts_path
166 .join(contract_path)
167 .to_string_lossy()
168 .into_owned(),
169 ),
170 }
171 .run(&quiet_global_args)
172 .await;
173
174 let _ = env::set_current_dir(original_dir);
176
177 match result {
178 Ok(()) => {
179 printer.infoln(format!(
180 "Successfully added OpenZeppelin example contract: {contract_path}"
181 ));
182 }
183 Err(generate::contract::Error::ExampleNotFound(_)) => {
184 printer.infoln(format!(
185 "Skipped missing OpenZeppelin example contract: {contract_path}"
186 ));
187 }
188 Err(e) => {
189 printer.warnln(format!(
190 "Failed to generate example contract: {contract_path}\n{e}"
191 ));
192 }
193 }
194
195 Ok(())
196 }
197}
198
199fn git_exists() -> bool {
201 Command::new("git").arg("--version").output().is_err()
202}
203
204fn git_init(path: &PathBuf) {
206 let _ = Command::new("git").arg("init").current_dir(path).output();
207}
208
209fn git_add(path: &PathBuf, rest: &[&str]) {
211 let mut args = vec!["add"];
212 args.extend_from_slice(rest);
213 let _ = Command::new("git").args(args).current_dir(path).output();
214}
215
216fn git_commit(path: &PathBuf, message: &str) {
218 let _ = Command::new("git")
219 .args(["commit", "-m", message])
220 .current_dir(path)
221 .output();
222}