ordinary 0.6.0-pre.12

Ordinary CLI
// Copyright (C) 2026 Ordinary Labs, LLC.
//
// SPDX-License-Identifier: AGPL-3.0-only

use anyhow::bail;
use clap::Subcommand;
use ordinary_config::{
    AssetsConfig, CompressionAlgorithm, CompressionAlgorithms, ErrorConfig, HttpCache,
    HttpCacheControl, HttpCsp, OrdinaryConfig,
};
use std::io::Write;
use std::path::Path;

#[derive(Subcommand, Debug)]
pub enum Ssg {
    /// generate an `ordinary.json` config to deploy your SSG to a running instance of `ordinaryd`
    Init {
        /// project domain
        domain: String,
        /// assets `dir_path`
        dir_path: String,
        /// contacts for Let's Encrypt (auto-TLS) [source](https://docs.rs/rustls-acme/latest/rustls_acme/struct.AcmeConfig.html#method.contact_push)
        #[arg(long, short, value_delimiter = ',', num_args = 1..)]
        contacts: Option<Vec<String>>,

        /// include generated 404.html as error page
        #[arg(long, short, default_value_t = false)]
        error_page: bool,
        /// accommodate inline styles in the generated files
        #[arg(long, short('s'), default_value_t = false)]
        inline_styles: bool,
        /// accommodate inline scripts in the generated files
        #[arg(long, short('r'), default_value_t = false)]
        inline_scripts: bool,
        /// accommodate inline images in the generated files
        #[arg(long, short('m'), default_value_t = false)]
        inline_images: bool,
    },
}

impl Ssg {
    pub fn handle(&self, project: &str) -> anyhow::Result<()> {
        match self {
            Ssg::Init {
                domain,
                dir_path,
                contacts,
                error_page,
                inline_styles,
                inline_scripts,
                inline_images,
            } => {
                let error = if *error_page {
                    Some(ErrorConfig {
                        asset: Some("404.html".into()),
                        ..Default::default()
                    })
                } else {
                    None
                };

                let html_csp = if *inline_scripts || *inline_styles || *inline_images {
                    Some(HttpCsp {
                        script_src: if *inline_scripts {
                            Some("'self' 'unsafe-inline'".into())
                        } else {
                            None
                        },
                        style_src: if *inline_styles {
                            Some("'self' 'unsafe-inline'".into())
                        } else {
                            None
                        },
                        img_src: if *inline_images {
                            Some("'self' data: 'unsafe-eval'".into())
                        } else {
                            None
                        },
                        ..Default::default()
                    })
                } else {
                    None
                };

                let config = OrdinaryConfig {
                    domain: domain.to_owned(),
                    contacts: contacts.to_owned(),
                    version: "0.1.0".into(),
                    port: Some(4433),
                    storage_size: Some(10_000_000),

                    error,

                    assets: Some(AssetsConfig {
                        dir_path: Some(dir_path.to_owned()),
                        base_route: "/".into(),
                        append_index_html: Some(true),
                        html_csp,
                        http: Some(HttpCache {
                            cache_control: Some(HttpCacheControl {
                                max_age: Some(3600),
                                ..Default::default()
                            }),
                            expires: Some(3600),
                            ..Default::default()
                        }),
                        precompression: Some(CompressionAlgorithms(vec![
                            CompressionAlgorithm::All,
                        ])),
                        minify_html: Some(true),
                        ..Default::default()
                    }),
                    ..Default::default()
                };

                let oj_path = Path::new(project).join("ordinary.json");
                if oj_path.exists() {
                    bail!("ordinary.json already exists in the '{project}' directory")
                }

                let mut file = fs_err::File::create(oj_path)?;
                serde_json::to_writer_pretty(&mut file, &config)?;
                file.flush()?;
            }
        }

        Ok(())
    }
}