1use anyhow::Context;
2use clap::Parser;
3use std::{ffi::OsString, fs, process::exit};
4use utils::prompts;
5
6use crate::{
7 package_manager::PackageManager,
8 template::{ElectronSubTemplate, TauriSubTemplate, Template},
9 utils::colors::*,
10};
11
12mod args;
13mod package_manager;
14mod template;
15pub mod utils;
16
17pub fn run<I, A>(args: I, bin_name: Option<String>, detected_manager: Option<String>)
18where
19 I: IntoIterator<Item = A>,
20 A: Into<OsString> + Clone,
21{
22 if let Err(e) = run_cli(args, bin_name, detected_manager) {
23 println!();
24 eprintln!("\n {BOLD}{RED}error{RESET}: {e:#}\n");
25 exit(1);
26 }
27}
28
29fn run_cli<I, A>(
30 args: I,
31 bin_name: Option<String>,
32 detected_manager: Option<String>,
33) -> anyhow::Result<()>
34where
35 I: IntoIterator<Item = A>,
36 A: Into<OsString> + Clone,
37{
38 let detected_manager = detected_manager.and_then(|p| p.parse::<PackageManager>().ok());
39 let args = args::Args::parse_from(
41 std::iter::once(OsString::from(bin_name.unwrap_or_default()))
42 .chain(args.into_iter().map(Into::into)),
43 );
44
45 handle_brand_text("\n ⚡ Welcome To Farm ! \n");
46 let defaults = args::Args::default();
47 let args::Args {
48 manager,
49 project_name,
50 template,
51 force,
52 } = args;
53
54 let cwd = std::env::current_dir()?;
55 let mut default_project_name = "farm-project";
56 let project_name = match project_name {
57 Some(name) => to_valid_pkg_name(&name),
58 None => loop {
59 let input = prompts::input("Project name", Some(default_project_name), false)?
60 .trim()
61 .to_string();
62 if !is_valid_pkg_name(&input) {
63 eprintln!(
64 "{BOLD}{RED}✘{RESET} Invalid project name: {BOLD}{YELLOW}{input}{RESET}, package name should only include lowercase alphanumeric character and hyphens \"-\" and doesn't start with numbers",
65 );
66 default_project_name = to_valid_pkg_name(&input).leak();
67 continue;
68 };
69 break input;
70 },
71 };
72 let target_dir = cwd.join(&project_name);
73
74 if target_dir.exists() && target_dir.read_dir()?.next().is_some() {
75 let overwrite = force
76 || prompts::confirm(
77 &format!(
78 "{} directory is not empty, do you want to overwrite?",
79 if target_dir == cwd {
80 "Current".to_string()
81 } else {
82 target_dir
83 .file_name()
84 .unwrap()
85 .to_string_lossy()
86 .to_string()
87 }
88 ),
89 false,
90 )?;
91 if !overwrite {
92 eprintln!("{BOLD}{RED}✘{RESET} Directory is not empty, Operation Cancelled");
93 exit(1);
94 }
95 };
96
97 let pkg_manager = manager.unwrap_or(match detected_manager {
98 Some(manager) => manager,
99 None => defaults.manager.context("default manager not set")?,
100 });
101
102 let templates_no_flavors = pkg_manager.templates_no_flavors();
103
104 let template = match template {
105 Some(template) => template,
106 None => {
107 let selected_template =
108 prompts::select("Select a framework:", &templates_no_flavors, Some(0))?.unwrap();
109
110 match selected_template {
111 Template::Tauri(None) => {
112 let sub_templates = vec![
113 TauriSubTemplate::React,
114 TauriSubTemplate::Vue,
115 TauriSubTemplate::Svelte,
116 TauriSubTemplate::Vanilla,
117 TauriSubTemplate::Solid,
118 TauriSubTemplate::Preact,
119 ];
120
121 let sub_template =
122 prompts::select("Select a Tauri template:", &sub_templates, Some(0))?.unwrap();
123
124 Template::Tauri(Some(*sub_template))
125 }
126 Template::Tauri2(None) => {
127 let sub_templates = vec![
128 TauriSubTemplate::React,
129 TauriSubTemplate::Vue,
130 TauriSubTemplate::Svelte,
131 TauriSubTemplate::Vanilla,
132 TauriSubTemplate::Solid,
133 TauriSubTemplate::Preact,
134 ];
135
136 let sub_template =
137 prompts::select("Select a Tauri2 template:", &sub_templates, Some(0))?.unwrap();
138
139 Template::Tauri2(Some(*sub_template))
140 }
141 Template::Electron(None) => {
142 let sub_templates = vec![
143 ElectronSubTemplate::React,
144 ElectronSubTemplate::Vue,
145 ElectronSubTemplate::Svelte,
146 ElectronSubTemplate::Vanilla,
147 ElectronSubTemplate::Solid,
148 ElectronSubTemplate::Preact,
149 ];
150
151 let sub_template =
152 prompts::select("Select an Electron template:", &sub_templates, Some(0))?.unwrap();
153
154 Template::Electron(Some(*sub_template))
155 }
156 _ => *selected_template,
157 }
158 }
159 };
160
161 if target_dir.exists() {
162 #[inline(always)]
163 fn clean_dir(dir: &std::path::PathBuf) -> anyhow::Result<()> {
164 for entry in fs::read_dir(dir)?.flatten() {
165 let path = entry.path();
166 if entry.file_type()?.is_dir() {
167 if entry.file_name() != ".git" {
168 clean_dir(&path)?;
169 std::fs::remove_dir(path)?;
170 }
171 } else {
172 fs::remove_file(path)?;
173 }
174 }
175 Ok(())
176 }
177 clean_dir(&target_dir)?;
178 } else {
179 fs::create_dir_all(&target_dir)?;
180 }
181
182 template.render(&target_dir, pkg_manager, &project_name, &project_name)?;
184
185 handle_brand_text("\n > Initial Farm Project created successfully ✨ ✨ \n");
186
187 if target_dir != cwd {
188 handle_brand_text(&format!(
189 " cd {} \n",
190 if project_name.contains(' ') {
191 format!("\"{project_name}\"")
192 } else {
193 project_name.to_string()
194 }
195 ));
196 }
197 if let Some(cmd) = pkg_manager.install_cmd() {
198 handle_brand_text(&format!(" {cmd} \n"));
199 }
200 handle_brand_text(&format!(" {} \n", get_run_cmd(&pkg_manager, &template)));
201
202 Ok(())
203}
204
205fn is_valid_pkg_name(project_name: &str) -> bool {
206 let mut chars = project_name.chars().peekable();
207 !project_name.is_empty()
208 && !chars.peek().map(|c| c.is_ascii_digit()).unwrap_or_default()
209 && !chars.any(|ch| !(ch.is_alphanumeric() || ch == '-' || ch == '_') || ch.is_uppercase())
210}
211
212fn to_valid_pkg_name(project_name: &str) -> String {
213 let ret = project_name
214 .trim()
215 .to_lowercase()
216 .replace([':', ';', ' ', '~'], "-")
217 .replace(['.', '\\', '/'], "");
218
219 let ret = ret
220 .chars()
221 .skip_while(|ch| ch.is_ascii_digit() || *ch == '-')
222 .collect::<String>();
223
224 if ret.is_empty() || !is_valid_pkg_name(&ret) {
225 "farm-project".to_string()
226 } else {
227 ret
228 }
229}
230
231fn get_run_cmd(pkg_manager: &PackageManager, template: &Template) -> &'static str {
232 match template {
233 Template::Tauri(_) => match pkg_manager {
234 PackageManager::Pnpm => "pnpm tauri dev",
235 PackageManager::Yarn => "yarn tauri dev",
236 PackageManager::Npm => "npm run tauri dev",
237 PackageManager::Bun => "bun tauri dev",
238 },
239 _ => pkg_manager.default_cmd(),
240 }
241}