poet 0.7.0

Static site generator with optional MCP server and LLM SEO optimizations
Documentation
use std::env::consts::ARCH;
use std::os::unix::fs::PermissionsExt as _;
use std::path::PathBuf;
use std::sync::Arc;

use anyhow::Result;
use async_trait::async_trait;
use clap::Parser;
use indoc::formatdoc;
use log::info;
use tokio::fs;

use crate::app_dir_desktop_entry::AppDirDesktopEntry;
use crate::assert_valid_desktop_entry_string::assert_valid_desktop_entry_string;
use crate::cmd::builds_project::BuildsProject;
use crate::cmd::handler::Handler;
use crate::cmd::value_parser::validate_is_directory;
use crate::cmd::value_parser::validate_is_directory_or_create;
use crate::copy_esbuild_metafile_assets_to::copy_esbuild_metafile_assets_to;
use crate::filesystem::Filesystem;
use crate::filesystem::storage::Storage;
use crate::read_esbuild_metafile_or_default::read_esbuild_metafile_or_default;

const ICON: &str = r#"<svg viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
    <rect width="10" height="10" fill="black"/>
</svg>"#;

#[derive(Parser)]
pub struct AppDir {
    #[arg(long)]
    name: String,

    #[arg(long, value_parser = validate_is_directory_or_create)]
    output_directory: PathBuf,

    #[arg(value_parser = validate_is_directory)]
    source_directory: PathBuf,

    #[arg(long)]
    title: String,

    #[arg(long)]
    version: String,
}

impl AppDir {
    fn app_dir_path(&self) -> PathBuf {
        self.output_directory.join(format!("{}.AppDir", self.name))
    }

    fn render_app_run_file(&self) -> Result<String> {
        Ok(formatdoc! {
            r#"
                #!/usr/bin/env sh

                ADDR=""
                PUBLIC_PATH=""

                while [ $# -gt 0 ]; do
                    case $1 in
                        --addr)
                            ADDR="$2"
                            shift 2
                            ;;
                        --public-path)
                            PUBLIC_PATH="$2"
                            shift 2
                            ;;
                        *)
                            echo "Unknown argument: $1"
                            echo "Usage: $0 --addr ADDRESS --public-path PATH"
                            exit 1
                            ;;
                    esac
                done

                if [ -z "$ADDR" ]; then
                    echo "Error: --addr is required"
                    echo "Usage: $0 --addr ADDRESS --public-path PATH"
                    exit 1
                fi

                if [ -z "$PUBLIC_PATH" ]; then
                    echo "Error: --public-path is required"
                    echo "Usage: $0 --addr ADDRESS --public-path PATH"
                    exit 1
                fi

                exec $APPDIR/poet serve $APPDIR --addr "$ADDR" --app-name "{name}" --public-path "$PUBLIC_PATH"
            "#,
            name = self.name,
        })
    }

    fn render_desktop_file(&self) -> Result<String> {
        Ok(AppDirDesktopEntry {
            name: assert_valid_desktop_entry_string(&self.name)?,
            poet_version: env!("CARGO_PKG_VERSION").to_string(),
            site_version: assert_valid_desktop_entry_string(&self.version)?,
            title: assert_valid_desktop_entry_string(&self.title)?,
        }
        .to_string())
    }
}

impl BuildsProject for AppDir {
    fn source_directory(&self) -> PathBuf {
        self.source_directory.clone()
    }
}

#[async_trait(?Send)]
impl Handler for AppDir {
    async fn handle(&self) -> Result<()> {
        let app_dir_path = self.app_dir_path();
        let source_filesystem = self.source_filesystem();
        let name_lowercase = self.name.to_lowercase();

        let app_dir_filesystem = Arc::new(Storage {
            base_directory: app_dir_path.clone(),
        });

        info!("Copying project files to AppDir...");

        app_dir_filesystem
            .copy_project_files_from(source_filesystem.clone())
            .await?;
        app_dir_filesystem
            .copy_file_from(
                source_filesystem.clone(),
                &PathBuf::from("esbuild-meta.json"),
            )
            .await?;

        info!("Copying assets to AppDir...");

        let esbuild_metafile = read_esbuild_metafile_or_default(source_filesystem.clone()).await?;

        copy_esbuild_metafile_assets_to(esbuild_metafile, &app_dir_path).await?;

        info!("Creating AppDir-specific metafiles...");

        fs::create_dir_all(&app_dir_path).await?;

        let apprun_path = app_dir_path.join("AppRun");

        fs::write(&apprun_path, self.render_app_run_file()?).await?;

        let mut perms = fs::metadata(&apprun_path).await?.permissions();

        perms.set_mode(0o755);

        fs::set_permissions(&apprun_path, perms).await?;

        fs::write(
            app_dir_path.join(format!("{name_lowercase}.desktop")),
            self.render_desktop_file()?,
        )
        .await?;
        fs::write(app_dir_path.join(format!("{name_lowercase}.svg")), ICON).await?;

        info!(
            "AppDir is ready. You can now run `ARCH={} appimagetool {}` to finish the process (you need to have AppImageKit installed)",
            ARCH,
            app_dir_path.display()
        );

        Ok(())
    }
}