1use crate::rust_template::{create_anchor_toml, ProgramTemplate};
2use anyhow::{anyhow, Result};
3use clap::Parser;
4use heck::{ToKebabCase, ToSnakeCase};
5use solana_sdk::signature::Keypair;
6use std::fs::{self, File};
7use std::io::prelude::*;
8use std::path::{Path, PathBuf};
9use std::process::Stdio;
10use std::string::ToString;
11
12pub mod rust_template;
13const VERSION: &str = env!("CARGO_PKG_VERSION");
14#[derive(Debug, Parser)]
15#[clap(version = VERSION)]
16pub struct Opts {
17 #[clap(subcommand)]
18 pub command: Command,
19}
20
21#[derive(Debug, Parser)]
22pub enum Command {
23 Init {
24 name: String,
26 #[clap(long)]
28 no_install: bool,
29 #[clap(long)]
31 no_git: bool,
32 #[clap(value_enum, short, long, default_value = "basic")]
34 template: ProgramTemplate,
35 #[clap(long, action)]
37 force: bool,
38 },
39}
40
41pub fn entry(opts: Opts) -> Result<()> {
42 let result = process_command(opts);
43
44 result
45}
46
47fn process_command(opts: Opts) -> Result<()> {
48 match opts.command {
49 Command::Init {
50 name,
51 no_install,
52 no_git,
53 template,
54 force,
55 } => init(name, no_install, no_git, template, force),
56 }
57}
58
59#[allow(clippy::too_many_arguments)]
60fn init(
61 name: String,
62 no_install: bool,
63 no_git: bool,
64 template: ProgramTemplate,
65 force: bool,
66) -> Result<()> {
67 let rust_name = name.to_snake_case();
69 let project_name = if name == rust_name {
70 rust_name.clone()
71 } else {
72 name.to_kebab_case()
73 };
74
75 let extra_keywords = ["async", "await", "try"];
78 if syn::parse_str::<syn::Ident>(&rust_name).is_err()
80 || extra_keywords.contains(&rust_name.as_str())
81 {
82 return Err(anyhow!(
83 "Anchor workspace name must be a valid Rust identifier. It may not be a Rust reserved word, start with a digit, or include certain disallowed characters. See https://doc.rust-lang.org/reference/identifiers.html for more detail.",
84 ));
85 }
86
87 if force {
88 fs::create_dir_all(&project_name)?;
89 } else {
90 fs::create_dir(&project_name)?;
91 }
92 std::env::set_current_dir(&project_name)?;
93 fs::create_dir_all("app")?;
94
95 let test_script = rust_template::get_test_script();
96 let program_id = rust_template::get_or_create_program_id(&rust_name);
97 let toml = create_anchor_toml(program_id.to_string(), test_script.to_string(), template);
98 fs::write("Anchor.toml", toml)?;
99
100 fs::write(".gitignore", rust_template::git_ignore())?;
102
103 fs::write(".prettierignore", rust_template::prettier_ignore())?;
105
106 fs::write("wallet.json", create_keypair())?;
108
109 fs::write("README.md", rust_template::readme(template))?;
111
112 fs::write("devbox.json", rust_template::devbox_json())?;
114
115 if force {
117 fs::remove_dir_all(
118 std::env::current_dir()?
119 .join("programs")
120 .join(&project_name),
121 )?;
122 }
123
124 rust_template::create_program(&project_name, template)?;
126
127 fs::create_dir_all("migrations")?;
129
130 let license = get_npm_init_license()?;
131
132 let mut ts_config = File::create("tsconfig.json")?;
134 ts_config.write_all(rust_template::ts_config().as_bytes())?;
135
136 let mut ts_package_json = File::create("package.json")?;
137 ts_package_json.write_all(rust_template::ts_package_json(license, template).as_bytes())?;
138
139 let mut deploy = File::create("migrations/deploy.ts")?;
140 deploy.write_all(rust_template::ts_deploy_script().as_bytes())?;
141
142 rust_template::create_test_files(&project_name, template)?;
143
144 if !no_install {
145 let yarn_result = install_node_modules("yarn")?;
146 if !yarn_result.status.success() {
147 println!("Failed yarn install will attempt to npm install");
148 install_node_modules("npm")?;
149 }
150 }
151
152 if !no_git {
153 let git_result = std::process::Command::new("git")
154 .arg("init")
155 .stdout(Stdio::inherit())
156 .stderr(Stdio::inherit())
157 .output()
158 .map_err(|e| anyhow::format_err!("git init failed: {}", e.to_string()))?;
159 if !git_result.status.success() {
160 eprintln!("Failed to automatically initialize a new git repository");
161 }
162 }
163
164 println!("{project_name} initialized");
165
166 Ok(())
167}
168
169pub type Files = Vec<(PathBuf, String)>;
171
172pub fn create_files(files: &Files) -> Result<()> {
180 for (path, content) in files {
181 let path = Path::new(path);
182 if path.exists() {
183 continue;
184 }
185
186 match path.extension() {
187 Some(_) => {
188 fs::create_dir_all(path.parent().unwrap())?;
189 fs::write(path, content)?;
190 }
191 None => fs::create_dir_all(path)?,
192 }
193 }
194
195 Ok(())
196}
197
198pub fn override_or_create_files(files: &Files) -> Result<()> {
206 for (path, content) in files {
207 let path = Path::new(path);
208 if path.exists() {
209 let mut f = fs::OpenOptions::new()
210 .write(true)
211 .truncate(true)
212 .open(path)?;
213 f.write_all(content.as_bytes())?;
214 f.flush()?;
215 } else {
216 fs::create_dir_all(path.parent().unwrap())?;
217 fs::write(path, content)?;
218 }
219 }
220
221 Ok(())
222}
223
224fn install_node_modules(cmd: &str) -> Result<std::process::Output> {
225 if cfg!(target_os = "windows") {
226 std::process::Command::new("cmd")
227 .arg(format!("/C {cmd} install"))
228 .stdout(Stdio::inherit())
229 .stderr(Stdio::inherit())
230 .output()
231 .map_err(|e| anyhow::format_err!("{} install failed: {}", cmd, e.to_string()))
232 } else {
233 std::process::Command::new(cmd)
234 .arg("install")
235 .stdout(Stdio::inherit())
236 .stderr(Stdio::inherit())
237 .output()
238 .map_err(|e| anyhow::format_err!("{} install failed: {}", cmd, e.to_string()))
239 }
240}
241
242fn get_npm_init_license() -> Result<String> {
244 let npm_init_license_output = std::process::Command::new("npm")
245 .arg("config")
246 .arg("get")
247 .arg("init-license")
248 .output()?;
249
250 if !npm_init_license_output.status.success() {
251 return Err(anyhow!("Failed to get npm init license"));
252 }
253
254 let license = String::from_utf8(npm_init_license_output.stdout)?;
255 Ok(license.trim().to_string())
256}
257
258fn create_keypair() -> String {
259 let keypair = Keypair::new();
260 let keypair_bytes = keypair.to_bytes();
261 let serialized = serde_json::to_string(&keypair_bytes.to_vec());
263
264 match serialized {
265 Ok(v) => return v,
266 Err(_e) => return "".parse().unwrap(),
267 }
268}