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