1use crate::LocalStarterGroupLookup;
2use glob::glob;
3use log::debug;
4use log::error;
5use serde::{Deserialize, Serialize};
6use serde_yaml;
7use std::collections::HashMap;
8use std::fs;
9use std::io;
10use std::path::Path;
11use std::path::PathBuf;
12use std::str::FromStr;
13
14#[derive(Debug, Serialize, Deserialize, Clone)]
15pub struct PreviewConfig {
16 pub template: Option<String>,
17 pub dependencies: Option<HashMap<String, String>>,
18}
19
20#[derive(Debug, Serialize, Deserialize, Clone)]
21pub struct StarterConfig {
22 pub description: Option<String>,
23 #[serde(rename = "defaultDir")]
24 pub default_dir: Option<PathBuf>,
25 #[serde(rename = "mainFile")]
26 pub main_file: Option<String>,
27 pub preview: Option<PreviewConfig>,
28}
29
30impl FromStr for StarterConfig {
31 type Err = serde_yaml::Error;
32
33 fn from_str(s: &str) -> Result<Self, Self::Err> {
34 serde_yaml::from_str(s)
35 }
36}
37
38#[derive(Debug, Serialize, Deserialize, Clone)]
39pub struct LocalStarterFile {
40 pub path: String,
41 pub contents: String,
42}
43
44#[derive(Debug, Serialize, Deserialize, Clone)]
48pub struct RemoteStarter {
49 pub github_username: String,
50 pub github_repo: String,
51 pub group: String,
52 pub name: String,
53}
54
55impl RemoteStarter {
56 pub fn new(github_username: &str, github_repo: &str, group: &str, name: &str) -> Self {
57 Self {
58 github_username: github_username.to_string(),
59 github_repo: github_repo.to_string(),
60 group: group.to_string(),
61 name: name.to_string(),
62 }
63 }
64
65 pub fn from_path(path: &str) -> Option<Self> {
100 let parts: Vec<&str> = path.split('/').collect();
101 let github_username = &parts[0][1..];
103
104 match parts.len() {
105 3 => {
106 let github_repo = "jump-start";
107 Some(Self::new(github_username, github_repo, parts[1], parts[2]))
108 }
109 4 => {
110 let github_repo = parts[1];
111 Some(Self::new(github_username, github_repo, parts[2], parts[3]))
112 }
113 _ => panic!("Could not parse remote starter from string {:?}", path),
114 }
115 }
116}
117
118#[derive(Debug, Serialize, Deserialize, Clone)]
119pub struct LocalStarter {
120 pub path: String,
122 pub group: String,
124 pub name: String,
126 pub config: Option<StarterConfig>,
128}
129
130impl LocalStarter {
131 pub fn new(group: &str, name: &str) -> Self {
132 let path = format!("{}/{}", group, name);
133
134 Self {
135 group: group.to_string(),
136 name: name.to_string(),
137 path,
138 config: None,
139 }
140 }
141
142 pub fn from_path(path: &str) -> Option<Self> {
144 let parts: Vec<&str> = path.split('/').collect();
145 if parts.len() != 2 {
146 return None;
147 }
148
149 Some(Self::new(parts[0], parts[1]))
150 }
151}
152
153pub fn parse_starters(path: &Path) -> io::Result<LocalStarterGroupLookup> {
154 let mut groups: LocalStarterGroupLookup = HashMap::new();
155
156 let pattern = format!("{}/**/*jump-start.yaml", path.display());
158
159 for entry in glob(&pattern).expect("Failed to read glob pattern") {
161 match entry {
162 Ok(path) => {
163 let path_str = path.to_string_lossy();
165 if path_str.contains("node_modules") || path_str.contains("jump-start-tools") {
166 continue;
167 }
168
169 let file_content = fs::read_to_string(&path)?;
171 debug!("Parsing YAML file: {}", path.display());
172 debug!("Content: {}", file_content);
173
174 let starter_config = match file_content.parse::<StarterConfig>() {
175 Ok(config) => config,
176 Err(e) => {
177 error!("Error parsing yaml for {}: {}", path.display(), e);
178 continue;
179 }
180 };
181
182 let current_dir = path.parent().unwrap();
183 let name = current_dir
184 .file_name()
185 .unwrap()
186 .to_string_lossy()
187 .to_string();
188 let group = current_dir
189 .parent()
190 .unwrap()
191 .file_name()
192 .unwrap()
193 .to_string_lossy()
194 .to_string();
195
196 let starter = LocalStarter {
197 name: name.clone(),
198 group: group.clone(),
199 path: format!("{}/{}", group, name),
200 config: Some(starter_config),
201 };
202
203 groups.entry(group).or_default().push(starter);
204 }
205 Err(e) => error!("Error processing glob entry: {}", e),
206 }
207 }
208
209 Ok(groups)
210}
211
212pub fn get_starter_files(
214 starter: &LocalStarter,
215 instance_dir: &Path,
216) -> io::Result<Vec<LocalStarterFile>> {
217 let mut out = Vec::new();
218 let excluded_files = ["jump-start.yaml", "degit.json"];
219
220 let starter_dir = instance_dir.join(&starter.group).join(&starter.name);
222
223 if starter_dir.exists() && starter_dir.is_dir() {
224 visit_dirs(
226 &starter_dir,
227 &mut out,
228 &excluded_files,
229 &starter_dir.to_string_lossy(),
230 )?;
231 debug!(
232 "Found {} files for starter {}/{}",
233 out.len(),
234 starter.group,
235 starter.name
236 );
237 } else {
238 error!("Warning: Starter directory not found: {:?}", starter_dir);
239 out.push(LocalStarterFile {
242 path: "example.file".to_string(),
243 contents: "// This is a sample file content\nconsole.log('Hello world');\n".to_string(),
244 });
245 }
246
247 Ok(out)
248}
249
250fn visit_dirs(
251 dir: &Path,
252 files: &mut Vec<LocalStarterFile>,
253 excluded_files: &[&str],
254 base_path: &str,
255) -> io::Result<()> {
256 if dir.is_dir() {
257 for entry in fs::read_dir(dir)? {
258 let entry = entry?;
259 let path = entry.path();
260
261 if path.is_dir() {
262 visit_dirs(&path, files, excluded_files, base_path)?;
263 } else if let Some(file_name) = path.file_name() {
264 let file_name_str = file_name.to_string_lossy();
265
266 if !excluded_files.contains(&file_name_str.as_ref()) {
267 let base = Path::new(base_path);
269 let rel_path = match path.strip_prefix(base) {
270 Ok(rel) => rel.to_string_lossy().to_string(),
271 Err(_) => path.to_string_lossy().to_string(),
272 };
273
274 match fs::read_to_string(&path) {
276 Ok(contents) => {
277 files.push(LocalStarterFile {
279 path: rel_path,
280 contents,
281 });
282 }
283 Err(e) => {
284 error!("Warning: Could not read file {:?}: {}", path, e);
285 }
288 }
289 }
290 }
291 }
292 }
293
294 Ok(())
295}
296
297pub fn get_starter_command(
298 starter: &LocalStarter,
299 github_username: &str,
300 github_repo: &str,
301) -> String {
302 if !github_username.is_empty() && !github_repo.is_empty() {
303 if github_repo == "jump-start" {
304 format!("jump-start use @{}/{}/{}", github_username, starter.group, starter.name)
305 } else {
306 format!("jump-start use @{}/{}/{}/{}", github_username, github_repo, starter.group, starter.name)
307 }
308 } else {
309 format!("jump-start use {}/{}", starter.group, starter.name)
310 }
311}