Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: create new steel projects with a template #33

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ clap = { features = ["derive", "env"], version = "4.4" }
clap_v3 = { version = "3", package = "clap" }
anyhow = "1"
colored = "2.0"
git2 = "0.16"
git2 = { version = "0.18", features = ["vendored-libgit2"] }
indicatif = "0.17"
num_enum = "0.7"
prettyplease = "0.2"
Expand All @@ -28,7 +28,9 @@ solana-cli-config = "^1.18"
solana-program = "^1.18"
solana-sdk = "^1.18"
spl-token = { features = ["no-entrypoint"], version = "^4" }
spl-associated-token-account = { features = [ "no-entrypoint" ], version = "^2.3" }
spl-associated-token-account = { features = [ "no-entrypoint" ], version = "^2.3" }
thiserror = "1.0.57"
tokio = "1.35"
quote = "1.0"
tempfile = "3.2"
walkdir = "2.4"
4 changes: 3 additions & 1 deletion cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ anyhow.workspace = true
clap.workspace = true
clap_v3.workspace = true
colored.workspace = true
indicatif.workspace = true
indicatif.workspace = true
git2.workspace = true
prettyplease.workspace = true
syn.workspace = true
Expand All @@ -32,3 +32,5 @@ quote.workspace = true
solana-sdk.workspace = true
solana-cli-config.workspace = true
solana-clap-v3-utils.workspace = true
tempfile.workspace = true
walkdir.workspace = true
7 changes: 7 additions & 0 deletions cli/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ use clap::{arg, Parser};
pub struct NewArgs {
#[arg(value_name = "NAME", help = "The name of the program")]
pub name: Option<String>,

#[arg(
long = "template",
value_name = "URL",
help = "Git repository URL containing program templates (optional)"
)]
pub template_url: Option<String>,
}

#[derive(Parser, Debug)]
Expand Down
121 changes: 116 additions & 5 deletions cli/src/new_project.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,117 @@
use std::{fs, io, path::Path};

use colored::*;
use git2::Repository;
use git2::{FetchOptions, RemoteCallbacks, Repository};

use anyhow::{Context, Result};
use walkdir::WalkDir;

use crate::{
utils::{prompt, to_camel_case, to_lib_case, to_type_case},
NewArgs,
};

pub fn new_project(args: NewArgs) -> anyhow::Result<()> {
pub struct TemplateHandler;

impl TemplateHandler {
pub fn new() -> Self {
Self
}

pub fn clone_and_process(
&self,
url: &str,
target_dir: &Path,
project_name: &str,
) -> Result<()> {
if target_dir.exists() {
return Err(anyhow::anyhow!(
"Directory '{}' already exists",
target_dir.display()
));
}

fs::create_dir_all(target_dir)
.with_context(|| format!("Failed to create directory: {}", target_dir.display()))?;

let temp_dir = tempfile::TempDir::new().context("Failed to create temporary directory")?;

let mut callbacks = RemoteCallbacks::new();
let mut last_progress = 0;
callbacks.transfer_progress(|progress| {
let current = progress.received_objects();
let total = progress.total_objects();
if current != last_progress && current == total {
println!("Template downloaded successfully!");
}
last_progress = current;
true
});

let mut fetch_options = FetchOptions::new();
fetch_options.remote_callbacks(callbacks);

let mut builder = git2::build::RepoBuilder::new();
builder.fetch_options(fetch_options);

println!("Downloading template...");
builder
.clone(url, temp_dir.path())
.with_context(|| format!("Failed to clone template repository from {}", url))?;

self.copy_and_process_directory(temp_dir.path(), target_dir, project_name)?;
Repository::init(target_dir)?;

Ok(())
}

fn copy_and_process_directory(
&self,
source: &Path,
target: &Path,
project_name: &str,
) -> Result<()> {
for entry in WalkDir::new(source).min_depth(1) {
let entry = entry?;
let path = entry.path();

if path.components().any(|c| c.as_os_str() == ".git") {
continue;
}

let relative_path = path.strip_prefix(source)?;
let target_path = target.join(relative_path);

if entry.file_type().is_dir() {
fs::create_dir_all(&target_path).with_context(|| {
format!("Failed to create directory: {}", target_path.display())
})?;
} else {
if let Some(parent) = target_path.parent() {
fs::create_dir_all(parent).with_context(|| {
format!("Failed to create parent directory: {}", parent.display())
})?;
}

let content = fs::read_to_string(path)
.with_context(|| format!("Failed to read file: {}", path.display()))?;

let processed_content = content
.replace("{name_lowercase}", &project_name.to_ascii_lowercase())
.replace("{name_uppercase}", &project_name.to_ascii_uppercase())
.replace("{name_camelcase}", &to_camel_case(&project_name))
.replace("{name_typecase}", &to_type_case(&project_name))
.replace("{name_libcase}", &to_lib_case(&project_name));

fs::write(&target_path, processed_content)
.with_context(|| format!("Failed to write file: {}", target_path.display()))?;
}
}
Ok(())
}
}

pub fn new_project(args: NewArgs) -> Result<()> {
// Get project name
let project_name = if let Some(name) = args.name {
name.to_ascii_lowercase()
Expand All @@ -34,9 +137,17 @@ pub fn new_project(args: NewArgs) -> anyhow::Result<()> {
// - Generate docs link

let base_path = Path::new(&project_name);
stub_workspace(base_path, &project_name)?;
stub_api(base_path, &project_name)?;
stub_program(base_path, &project_name)?;

if let Some(template_url) = args.template_url {
let handler = TemplateHandler::new();
handler.clone_and_process(&template_url, base_path, &project_name)?;
println!("✨ Project '{}' created successfully!", project_name);
} else {
stub_workspace(base_path, &project_name)?;
stub_api(base_path, &project_name)?;
stub_program(base_path, &project_name)?;
}

Ok(())
}

Expand Down