stellar_scaffold_cli/commands/
init.rs

1use clap::Parser;
2use degit::degit;
3use std::fs::{metadata, read_dir, remove_dir_all};
4use std::path::PathBuf;
5use std::{env, io};
6
7use super::generate;
8use stellar_cli::{commands::global, print::Print};
9
10const FRONTEND_TEMPLATE: &str = "https://github.com/AhaLabs/scaffold-stellar-frontend";
11
12/// A command to initialize a new project
13#[derive(Parser, Debug, Clone)]
14pub struct Cmd {
15    /// The path to the project must be provided
16    pub project_path: PathBuf,
17}
18
19/// Errors that can occur during initialization
20#[derive(thiserror::Error, Debug)]
21pub enum Error {
22    #[error("Failed to clone template: {0}")]
23    DegitError(String),
24    #[error("Project path contains invalid UTF-8 characters and cannot be converted to a string")]
25    InvalidProjectPathEncoding,
26    #[error("IO error: {0}")]
27    IoError(#[from] io::Error),
28    #[error(transparent)]
29    GenerateError(#[from] generate::contract::Error),
30}
31
32impl Cmd {
33    /// Run the initialization command
34    ///
35    /// # Example:
36    ///
37    /// ```
38    /// /// From the command line
39    /// stellar scaffold init /path/to/project
40    /// ```
41    pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
42        let printer: Print = Print::new(global_args.quiet);
43
44        // Convert to absolute path to avoid issues when changing directories
45        let absolute_project_path = self.project_path.canonicalize().unwrap_or_else(|_| {
46            // If canonicalize fails (path doesn't exist yet), manually create absolute path
47            if self.project_path.is_absolute() {
48                self.project_path.clone()
49            } else {
50                env::current_dir()
51                    .unwrap_or_default()
52                    .join(&self.project_path)
53            }
54        });
55
56        printer.infoln(format!(
57            "Creating new Stellar project in {absolute_project_path:?}"
58        ));
59
60        let project_str = absolute_project_path
61            .to_str()
62            .ok_or(Error::InvalidProjectPathEncoding)?;
63
64        degit(FRONTEND_TEMPLATE, project_str);
65
66        if metadata(&absolute_project_path).is_err()
67            || read_dir(&absolute_project_path)?.next().is_none()
68        {
69            return Err(Error::DegitError(format!(
70                "Failed to clone template into {project_str}: directory is empty or missing",
71            )));
72        }
73
74        // Update the project with the latest OpenZeppelin examples
75        self.update_oz_example(
76            &absolute_project_path,
77            "fungible-token-interface",
78            global_args,
79        )
80        .await?;
81        self.update_oz_example(&absolute_project_path, "nft-enumerable", global_args)
82            .await?;
83
84        printer.checkln(format!("Project successfully created at {project_str}"));
85        Ok(())
86    }
87
88    async fn update_oz_example(
89        &self,
90        absolute_project_path: &PathBuf,
91        contract_path: &'static str,
92        global_args: &global::Args,
93    ) -> Result<(), Error> {
94        let original_dir = env::current_dir()?;
95        env::set_current_dir(absolute_project_path)?;
96
97        let contracts_path = absolute_project_path.join("contracts");
98        let existing_contract_path = contracts_path.join(contract_path);
99
100        if existing_contract_path.exists() {
101            remove_dir_all(&existing_contract_path)?;
102        }
103
104        let mut quiet_global_args = global_args.clone();
105        quiet_global_args.quiet = true;
106
107        generate::contract::Cmd {
108            from: Some(contract_path.to_owned()),
109            ls: false,
110            from_wizard: false,
111            output: Some(
112                contracts_path
113                    .join(contract_path)
114                    .to_string_lossy()
115                    .into_owned(),
116            ),
117        }
118        .run(&quiet_global_args)
119        .await?;
120        env::set_current_dir(original_dir)?;
121        Ok(())
122    }
123}