systemprompt-generator 0.10.2

Static site generation, theme rendering, and asset bundling for systemprompt.io AI governance dashboards. Handlebars and Markdown pipeline for the MCP governance platform.
Documentation
//! Implementation of the `copy_extension_assets` job: walks the extension
//! registry for required asset declarations and copies them into the build
//! output directory.

use std::path::Path;
use systemprompt_extension::{AssetDefinition, ExtensionRegistry};
use systemprompt_models::AppPaths;
use systemprompt_traits::JobResult;

use crate::error::{GeneratorResult as Result, PublishError};

pub async fn execute_copy_extension_assets(paths: &AppPaths) -> Result<JobResult> {
    let start_time = std::time::Instant::now();

    tracing::info!("Copy extension assets job started");

    let registry = ExtensionRegistry::discover().map_err(PublishError::other)?;
    let assets = registry.all_required_assets(paths);

    if assets.is_empty() {
        let duration_ms = start_time.elapsed().as_millis() as u64;
        tracing::info!(duration_ms, "No extension assets to copy");
        return Ok(JobResult::success()
            .with_stats(0, 0)
            .with_duration(duration_ms));
    }

    let dist_dir = paths.web().dist();

    let mut copied = 0u64;
    let mut failed = 0u64;

    for (ext_id, asset) in assets {
        match copy_asset(dist_dir, ext_id, &asset).await {
            Ok(()) => copied += 1,
            Err(e) => {
                if asset.is_required() {
                    return Err(e);
                }
                tracing::warn!(
                    extension = %ext_id,
                    asset = %asset.source().display(),
                    error = %e,
                    "Optional asset copy failed"
                );
                failed += 1;
            },
        }
    }

    let duration_ms = start_time.elapsed().as_millis() as u64;

    tracing::info!(
        copied,
        failed,
        duration_ms,
        "Copy extension assets job completed"
    );

    Ok(JobResult::success()
        .with_stats(copied, failed)
        .with_duration(duration_ms))
}

async fn copy_asset(dist_dir: &Path, ext_id: &str, asset: &AssetDefinition) -> Result<()> {
    let dest_path = dist_dir.join(asset.destination());

    if let Some(parent) = dest_path.parent() {
        tokio::fs::create_dir_all(parent).await.map_err(|e| {
            PublishError::other(format!(
                "Failed to create directory {}: {e}",
                parent.display()
            ))
        })?;
    }

    tokio::fs::copy(asset.source(), &dest_path)
        .await
        .map_err(|e| {
            PublishError::other(format!(
                "Failed to copy asset from {} to {}: {e}",
                asset.source().display(),
                dest_path.display()
            ))
        })?;

    tracing::debug!(
        extension = %ext_id,
        source = %asset.source().display(),
        destination = %dest_path.display(),
        "Copied extension asset"
    );

    Ok(())
}