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::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-example"];
90
91 for contract in example_contracts {
92 self.update_oz_example(&absolute_project_path, contract, global_args)
93 .await?;
94 }
95
96 printer.checkln(format!("Project successfully created at {project_str}"));
97 Ok(())
98 }
99
100 async fn update_oz_example(
105 &self,
106 absolute_project_path: &PathBuf,
107 contract_path: &str,
108 global_args: &global::Args,
109 ) -> Result<(), Error> {
110 let printer = Print::new(global_args.quiet);
111 let original_dir = env::current_dir()?;
112 env::set_current_dir(absolute_project_path)?;
113
114 let contracts_path = absolute_project_path.join("contracts");
115 let existing_contract_path = contracts_path.join(contract_path);
116
117 if existing_contract_path.exists() {
118 remove_dir_all(&existing_contract_path)?;
119 }
120
121 let mut quiet_global_args = global_args.clone();
122 quiet_global_args.quiet = true;
123
124 let result = generate::contract::Cmd {
125 from: Some(contract_path.to_owned()),
126 ls: false,
127 from_wizard: false,
128 output: Some(
129 contracts_path
130 .join(contract_path)
131 .to_string_lossy()
132 .into_owned(),
133 ),
134 }
135 .run(&quiet_global_args)
136 .await;
137
138 let _ = env::set_current_dir(original_dir);
140
141 match result {
142 Ok(()) => {
143 printer.infoln(format!(
144 "Successfully added OpenZeppelin example contract: {contract_path}"
145 ));
146 }
147 Err(generate::contract::Error::ExampleNotFound(_)) => {
148 printer.infoln(format!(
149 "Skipped missing OpenZeppelin example contract: {contract_path}"
150 ));
151 }
152 Err(e) => {
153 printer.warnln(format!(
154 "Failed to generate example contract: {contract_path}\n{e}"
155 ));
156 }
157 }
158
159 Ok(())
160 }
161}
162
163fn git_exists() -> bool {
165 Command::new("git").arg("--version").output().is_err()
166}
167
168fn git_init(path: &PathBuf) {
170 let _ = Command::new("git").arg("init").current_dir(path).output();
171}
172
173fn git_add(path: &PathBuf, rest: &[&str]) {
175 let mut args = vec!["add"];
176 args.extend_from_slice(rest);
177 let _ = Command::new("git").args(args).current_dir(path).output();
178}
179
180fn git_commit(path: &PathBuf, message: &str) {
182 let _ = Command::new("git")
183 .args(["commit", "-m", message])
184 .current_dir(path)
185 .output();
186}