Skip to main content

oseda_cli/cmd/
init.rs

1use std::{collections::HashMap, error::Error, process::Command, str::FromStr};
2
3use clap::Args;
4use spinners::{Spinner, Spinners};
5use strum::IntoEnumIterator;
6
7use crate::{
8    config,
9    templates::{templater::Templater, CLITemplate, Renderer},
10};
11
12/// Options for the `oseda init` command
13#[derive(Args, Debug)]
14pub struct InitOptions {
15    // claps 'value_name' does not change the argument name, basically just the value in the help menu,
16    // e.g  --template <FORMAT>
17    /// Project Title
18    #[arg(long, value_name = "TITLE")]
19    pub title: Option<String>,
20
21    /// Project Tags [e.g: ComputerScience,Engineering,...]
22    #[arg(long, value_delimiter = ',', value_name = "TAG1,TAG2,...")]
23    pub tags: Option<Vec<String>>,
24
25    /// Project Color [e.g: Red]
26    #[arg(long, value_name = "COLOR")]
27    pub color: Option<String>,
28
29    /// Project Template Format [HTML | Markdown]
30    #[arg(long, value_name = "FORMAT")]
31    pub template: Option<String>,
32
33    /// OSI License SPDX identifier [e.g: MIT]
34    #[arg(long, value_name = "SPDX_ID")]
35    pub license: Option<String>,
36
37    /// Project Description
38    #[arg(long, value_name = "TEXT")]
39    pub description: Option<String>,
40}
41
42/// Initialize an Oseda project with the provided options
43///
44/// This command will:
45/// - Run `npm init`
46/// - Install required dependencies (Vite, Reveal.js, etc)
47/// - Write config and boilerplate files
48///
49/// # Arguments
50/// * `_opts` - command-line options (this is unused rn, used later I hope)
51///
52/// # Returns
53/// * `Ok(())` if project initialization is suceeded
54/// * `Err` if any step (npm, file write, config generation etc) fails
55pub fn init(opts: InitOptions) -> Result<(), Box<dyn Error>> {
56    let template = match opts.template {
57        Some(ref arg_template) => {
58            CLITemplate::from_str(arg_template).map_err(|_| "Invalid template".to_string())?
59        }
60        None => prompt_template()?,
61    };
62
63    let conf = config::create_conf(opts)?;
64
65    std::fs::create_dir_all(&conf.title)?;
66
67    let output = Command::new("npm")
68        .args(["init", "-y", "--prefix", &conf.title])
69        .current_dir(&conf.title)
70        .output()?;
71
72    // swapped to explicit check so it doesn't hang after
73    if !output.status.success() {
74        eprintln!(
75            "npm init failed: {}",
76            String::from_utf8_lossy(&output.stderr)
77        );
78        return Err("npm init failed".into());
79    }
80
81    let npm_commands = vec![
82        format!("install --save-dev vite@5.4.21 http-server@14.1.1"),
83        format!("install reveal.js@5.2.1 serve@14.2.6 highlight.js@11.12.0"),
84        format!("install patch-package@8.0.1"),
85    ];
86
87    for c in npm_commands {
88        let mut spinner = Spinner::new(Spinners::Dots9, "Initializing...".into());
89
90        let args: Vec<&str> = c.split(' ').collect();
91        let output = Command::new("npm")
92            .args(&args)
93            .current_dir(&conf.title)
94            .output()?;
95
96        if !output.status.success() {
97            eprintln!(
98                "npm {} failed: {}",
99                c,
100                String::from_utf8_lossy(&output.stderr)
101            );
102            return Err(format!("npm {} failed", c).into());
103        }
104        spinner.stop();
105
106        println!("Bootstrapped npm {}", c);
107    }
108
109    println!("Saving config file...");
110
111    config::write_config(&conf.title, &conf)?;
112
113    // empty hashmap for now
114    let mut params: HashMap<String, String> = HashMap::new();
115    params.insert(String::from("TITLE"), conf.title.clone());
116
117    let templater = Templater::new(template, params);
118    templater.write_to_fs(&conf.title)?;
119
120    Ok(())
121}
122
123fn prompt_template() -> Result<CLITemplate, Box<dyn Error>> {
124    let template_opts: Vec<CLITemplate> = CLITemplate::iter().collect();
125
126    let chosen_template = inquire::Select::new("Select a template:", template_opts).prompt()?;
127
128    Ok(chosen_template)
129}