Skip to main content

oseda_cli/cmd/
init.rs

1use std::{
2    error::Error,
3    fs::{self},
4    process::Command,
5    str::FromStr,
6};
7
8use clap::Args;
9use spinners::{Spinner, Spinners};
10use strum::IntoEnumIterator;
11
12use crate::{config, template::Template};
13
14/// Options for the `oseda init` command
15#[derive(Args, Debug)]
16pub struct InitOptions {
17    // claps 'value_name' does not change the argument name, basically just the value in the help menu,
18    // e.g  --template <FORMAT>
19    /// Project Title
20    #[arg(long, value_name = "TITLE")]
21    pub title: Option<String>,
22
23    /// Project Tags [e.g: ComputerScience,Engineering,...]
24    #[arg(long, value_delimiter = ',', value_name = "TAG1,TAG2,...")]
25    pub tags: Option<Vec<String>>,
26
27    /// Project Color [e.g: Red]
28    #[arg(long, value_name = "COLOR")]
29    pub color: Option<String>,
30
31    /// Project Template Format [HTML | Markdown]
32    #[arg(long, value_name = "FORMAT")]
33    pub template: Option<String>,
34
35    /// OSI License SPDX identifier [e.g: MIT]
36    #[arg(long, value_name = "SPDX_ID")]
37    pub license: Option<String>,
38
39    /// Project Description
40    #[arg(long, value_name = "TEXT")]
41    pub description: Option<String>,
42}
43
44// embed all the static markdown template files into binary
45const MD_VITE_CONFIG_JS: &str = include_str!("../static/md-templates/vite.config.js");
46const MD_INDEX_HTML: &str = include_str!("../static/md-templates/index.html");
47const MD_MAIN_JS: &str = include_str!("../static/md-templates/main.js");
48const MD_SLIDES: &str = include_str!("../static/md-templates/slides.md");
49const MD_CUSTOM_CSS: &str = include_str!("../static/md-templates/custom.css");
50const MD_FERRIS: &[u8] = include_bytes!("../static/md-templates/ferris.png");
51const MD_GITIGNORE: &str = include_str!("../static/md-templates/.gitignore");
52const MD_FAVICON: &[u8] = include_bytes!("../static/md-templates/favicon.png");
53
54// do the same with the html templates
55const HTML_VITE_CONFIG_JS: &str = include_str!("../static/html-templates/vite.config.js");
56const HTML_INDEX_HTML: &str = include_str!("../static/html-templates/index.html");
57const HTML_MAIN_JS: &str = include_str!("../static/html-templates/main.js");
58const HTML_SLIDES: &str = include_str!("../static/html-templates/slides.html");
59const HTML_CUSTOM_CSS: &str = include_str!("../static/html-templates/custom.css");
60const HTML_FERRIS: &[u8] = include_bytes!("../static/html-templates/ferris.png");
61const HTML_GITIGNORE: &str = include_str!("../static/html-templates/.gitignore");
62const HTML_FAVICON: &[u8] = include_bytes!("../static/html-templates/favicon.png");
63
64/// Initialize an Oseda project with the provided options
65///
66/// This command will:
67/// - Run `npm init`
68/// - Install required dependencies (Vite, Reveal.js, etc)
69/// - Write config and boilerplate files
70///
71/// # Arguments
72/// * `_opts` - command-line options (this is unused rn, used later I hope)
73///
74/// # Returns
75/// * `Ok(())` if project initialization is suceeded
76/// * `Err` if any step (npm, file write, config generation etc) fails
77pub fn init(opts: InitOptions) -> Result<(), Box<dyn Error>> {
78    let template = match opts.template {
79        Some(ref arg_template) => {
80            Template::from_str(arg_template).map_err(|_| "Invalid template".to_string())?
81        }
82        None => prompt_template()?,
83    };
84
85    let conf = config::create_conf(opts)?;
86
87    std::fs::create_dir_all(&conf.title)?;
88
89    let output = Command::new("npm")
90        .args(["init", "-y", "--prefix", &conf.title])
91        .current_dir(&conf.title)
92        .output()?;
93
94    // swapped to explicit check so it doesn't hang after
95    if !output.status.success() {
96        eprintln!(
97            "npm init failed: {}",
98            String::from_utf8_lossy(&output.stderr)
99        );
100        return Err("npm init failed".into());
101    }
102
103    let npm_commands = vec![
104        format!("install --save-dev vite@5.4.21 http-server@14.1.1"),
105        format!("install reveal.js@5.2.1 serve@14.2.6 highlight.js@11.12.0"),
106        format!("install patch-package@8.0.1"),
107    ];
108
109    for c in npm_commands {
110        let mut spinner = Spinner::new(Spinners::Dots9, "Initializing...".into());
111
112        let args: Vec<&str> = c.split(' ').collect();
113        let output = Command::new("npm")
114            .args(&args)
115            .current_dir(&conf.title)
116            .output()?;
117
118        if !output.status.success() {
119            eprintln!(
120                "npm {} failed: {}",
121                c,
122                String::from_utf8_lossy(&output.stderr)
123            );
124            return Err(format!("npm {} failed", c).into());
125        }
126        spinner.stop();
127
128        println!("Bootstrapped npm {}", c);
129    }
130
131    println!("Saving config file...");
132
133    config::write_config(&conf.title, &conf)?;
134
135    // 99% sure we'll only ever have to maintain these two template schemas
136    match template {
137        Template::Markdown => {
138            // fs::write(format!("{}/package.json", &conf.title), MD_PACKAGE_JSON)?;
139            fs::write(format!("{}/vite.config.js", &conf.title), MD_VITE_CONFIG_JS)?;
140            fs::write(format!("{}/index.html", &conf.title), MD_INDEX_HTML)?;
141            fs::write(format!("{}/.gitignore", &conf.title), MD_GITIGNORE)?;
142
143            std::fs::create_dir_all(format!("{}/src", &conf.title))?;
144            fs::write(format!("{}/src/main.js", &conf.title), MD_MAIN_JS)?;
145
146            std::fs::create_dir_all(format!("{}/slides", &conf.title))?;
147            fs::write(format!("{}/slides/slides.md", &conf.title), MD_SLIDES)?;
148
149            std::fs::create_dir_all(format!("{}/css", &conf.title))?;
150            fs::write(format!("{}/css/custom.css", &conf.title), MD_CUSTOM_CSS)?;
151
152            std::fs::create_dir_all(format!("{}/public", &conf.title))?;
153            fs::write(format!("{}/public/ferris.png", &conf.title), MD_FERRIS)?;
154            fs::write(format!("{}/public/favicon.png", &conf.title), MD_FAVICON)?;
155        }
156        Template::HTML => {
157            // fs::write(format!("{}/package.json", &conf.title), HTML_PACKAGE_JSON)?;
158            fs::write(
159                format!("{}/vite.config.js", &conf.title),
160                HTML_VITE_CONFIG_JS,
161            )?;
162            fs::write(format!("{}/index.html", &conf.title), HTML_INDEX_HTML)?;
163            fs::write(format!("{}/.gitignore", &conf.title), HTML_GITIGNORE)?;
164
165            std::fs::create_dir_all(format!("{}/src", &conf.title))?;
166            fs::write(format!("{}/src/main.js", &conf.title), HTML_MAIN_JS)?;
167
168            std::fs::create_dir_all(format!("{}/slides", &conf.title))?;
169            fs::write(format!("{}/slides/slides.html", &conf.title), HTML_SLIDES)?;
170
171            std::fs::create_dir_all(format!("{}/css", &conf.title))?;
172            fs::write(format!("{}/css/custom.css", &conf.title), HTML_CUSTOM_CSS)?;
173
174            std::fs::create_dir_all(format!("{}/public", &conf.title))?;
175            fs::write(format!("{}/public/ferris.png", &conf.title), HTML_FERRIS)?;
176            fs::write(format!("{}/public/favicon.png", &conf.title), HTML_FAVICON)?;
177        }
178    }
179
180    Ok(())
181}
182
183fn prompt_template() -> Result<Template, Box<dyn Error>> {
184    let template_opts: Vec<Template> = Template::iter().collect();
185
186    let chosen_template = inquire::Select::new("Select a template:", template_opts).prompt()?;
187
188    Ok(chosen_template)
189}