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::{JanudocsSubTemplate, Template},
9 utils::colors::*,
10};
11
12pub mod utils;
13mod args;
14mod package_manager;
15mod template;
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("\nWelcome to Janustack\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 = "janustack-project";
56 let project_name = match project_name {
57 Some(name) => to_valid_pkg_name(&name),
58 None => loop {
59 let input = prompts::input(
60 "Enter the name for your new project (relative to current directory)\n",
61 Some(default_project_name),
62 false,
63 )?
64 .trim()
65 .to_string();
66 if !is_valid_pkg_name(&input) {
67 eprintln!(
68 "{BOLD}{RED}✘{RESET} Invalid project name: {BOLD}{YELLOW}{input}{RESET}, {}",
69 "package name should only include lowercase alphanumeric character and hyphens \"-\" and doesn't start with numbers"
70 );
71 default_project_name = to_valid_pkg_name(&input).leak();
72 continue;
73 };
74 break input;
75 },
76 };
77 let target_dir = cwd.join(&project_name);
78
79 if target_dir.exists() && target_dir.read_dir()?.next().is_some() {
80 let overwrite = force
81 || prompts::confirm(
82 &format!(
83 "{} directory is not empty, do you want to overwrite?",
84 if target_dir == cwd {
85 "Current".to_string()
86 } else {
87 target_dir
88 .file_name()
89 .unwrap()
90 .to_string_lossy()
91 .to_string()
92 }
93 ),
94 false,
95 )?;
96 if !overwrite {
97 eprintln!("{BOLD}{RED}✘{RESET} Directory is not empty, Operation Cancelled");
98 exit(1);
99 }
100 };
101
102 let pkg_manager = manager.unwrap_or(match detected_manager {
103 Some(manager) => manager,
104 None => defaults.manager.context("default manager not set")?,
105 });
106
107 let templates_no_flavors = pkg_manager.templates_no_flavors();
108
109 let template = match template {
110 Some(template) => template,
111 None => {
112 let selected_template =
113 prompts::select("Select a framework:", &templates_no_flavors, Some(0))?.unwrap();
114
115 match selected_template {
116 Template::Janudocs(None) => {
117 let sub_templates =
118 vec![JanudocsSubTemplate::React, JanudocsSubTemplate::Solid];
119
120 let sub_template =
121 prompts::select("Select an Janudocs template:", &sub_templates, Some(0))?
122 .unwrap();
123
124 Template::Janudocs(Some(*sub_template))
125 }
126 _ => *selected_template,
127 }
128 }
129 };
130
131 if target_dir.exists() {
132 #[inline(always)]
133 fn clean_dir(dir: &std::path::PathBuf) -> anyhow::Result<()> {
134 for entry in fs::read_dir(dir)?.flatten() {
135 let path = entry.path();
136 if entry.file_type()?.is_dir() {
137 if entry.file_name() != ".git" {
138 clean_dir(&path)?;
139 std::fs::remove_dir(path)?;
140 }
141 } else {
142 fs::remove_file(path)?;
143 }
144 }
145 Ok(())
146 }
147 clean_dir(&target_dir)?;
148 } else {
149 fs::create_dir_all(&target_dir)?;
150 }
151
152 template.render(&target_dir, pkg_manager, &project_name, &project_name)?;
154
155 handle_brand_text("\nNext steps:\n");
156
157 if target_dir != cwd {
158 handle_brand_text(&format!(
159 "1. cd {} \n",
160 if project_name.contains(' ') {
161 format!("\"{project_name}\"")
162 } else {
163 project_name.to_string()
164 }
165 ));
166 }
167 if let Some(cmd) = pkg_manager.install_cmd() {
168 handle_brand_text(&format!("2. {cmd}\n"));
169 }
170 handle_brand_text(&format!("3. {}\n", get_run_cmd(&pkg_manager, &template)));
171
172 handle_brand_text("\nUpdate all dependencies:\n");
173 handle_brand_text(&format!("{} pons -r\n", pkg_manager.update_cmd()));
174
175 handle_brand_text("\nLike create-janustack? Give a star on GitHub:\n");
176 handle_brand_text(&format!("https://github.com/janustack/create-janustack"));
177
178 Ok(())
179}
180
181fn is_valid_pkg_name(project_name: &str) -> bool {
182 let mut chars = project_name.chars().peekable();
183 !project_name.is_empty()
184 && !chars.peek().map(|c| c.is_ascii_digit()).unwrap_or_default()
185 && !chars.any(|ch| !(ch.is_alphanumeric() || ch == '-' || ch == '_') || ch.is_uppercase())
186}
187
188fn to_valid_pkg_name(project_name: &str) -> String {
189 let ret = project_name
190 .trim()
191 .to_lowercase()
192 .replace([':', ';', ' ', '~'], "-")
193 .replace(['.', '\\', '/'], "");
194
195 let ret = ret
196 .chars()
197 .skip_while(|ch| ch.is_ascii_digit() || *ch == '-')
198 .collect::<String>();
199
200 if ret.is_empty() || !is_valid_pkg_name(&ret) {
201 "janustack-project".to_string()
202 } else {
203 ret
204 }
205}
206
207fn get_run_cmd(pkg_manager: &PackageManager, template: &Template) -> &'static str {
208 match template {
209 _ => pkg_manager.default_cmd(),
210 }
211}