forklaunch 1.15.0

Launch faster with forklaunch
use std::{collections::HashMap, io::Write, path::Path};

use anyhow::{Context, Result, bail};
use clap::ArgMatches;
use rustyline::{Editor, history::DefaultHistory};
use termcolor::{ColorChoice, StandardStream, WriteColor};

use crate::{
    CliCommand,
    constants::ERROR_FAILED_TO_PARSE_MANIFEST,
    core::{
        base_path::{RequiredLocation, find_app_root_path},
        command::command,
        manifest::{ProjectType, application::ApplicationManifestData},
        rendered_template::{RenderedTemplate, RenderedTemplatesCache, write_rendered_templates},
        sync::{
            artifacts::{
                ArtifactType, ProjectSyncMetadata, remove_project_from_artifacts,
                sync_project_to_artifacts,
            },
            resolvers::resolve_description,
        },
    },
    prompt::{ArrayCompleter, prompt_for_confirmation},
};

#[derive(Debug)]
pub(crate) struct LibrarySyncCommand;

impl LibrarySyncCommand {
    pub(crate) fn new() -> Self {
        Self {}
    }
}

pub(crate) fn sync_library_with_cache(
    library_name: &str,
    app_root_path: &Path,
    manifest_data: &mut ApplicationManifestData,
    matches: &ArgMatches,
    prompts_map: &HashMap<String, HashMap<String, String>>,
    rendered_templates_cache: &mut RenderedTemplatesCache,
    stdout: &mut StandardStream,
) -> Result<()> {
    let modules_path = app_root_path.join(&manifest_data.modules_path);
    let library_path = modules_path.join(library_name);

    if !library_path.exists() {
        if let Some(project) = manifest_data
            .projects
            .iter()
            .find(|p| p.name == library_name)
        {
            log_warn!(stdout, "Library directory not found, but exists in manifest");

            let mut line_editor = Editor::<ArrayCompleter, DefaultHistory>::new()?;
            let should_cleanup = prompt_for_confirmation(
                &mut line_editor,
                &format!(
                    "Remove library '{}' from all artifacts? (y/N) ",
                    library_name
                ),
            )?;

            if !should_cleanup {
                log_warn!(stdout, "Skipping cleanup");
                bail!("Library directory not found: {}", library_path.display());
            }

            writeln!(
                stdout,
                "[INFO] Removing '{}' from artifacts...",
                library_name
            )?;

            remove_project_from_artifacts(
                rendered_templates_cache,
                manifest_data,
                library_name,
                project.r#type.clone(),
                &[ArtifactType::Manifest, ArtifactType::Runtime],
                app_root_path,
                &modules_path,
                stdout,
            )?;

            log_ok!(stdout, "Removed orphaned library '{}'", library_name);
            return Ok(());
        } else {
            log_error!(stdout, "Library directory not found: {}", library_path.display());
            bail!("Library directory not found: {}", library_path.display());
        }
    }

    if manifest_data
        .projects
        .iter()
        .any(|p| p.name == library_name)
    {
        log_ok!(stdout, "Library '{}' already synced", library_name);
        return Ok(());
    }

    let description =
        resolve_description(library_name, &library_path, matches, prompts_map, stdout)?;

    let sync_metadata = ProjectSyncMetadata {
        project_type: ProjectType::Library,
        project_name: library_name.to_string(),
        description,
        database: None,
        infrastructure: vec![],
        worker_type: None,
    };

    sync_project_to_artifacts(
        rendered_templates_cache,
        manifest_data,
        &sync_metadata,
        &[
            ArtifactType::Manifest,
            ArtifactType::Runtime,
            ArtifactType::ModulesTsconfig,
        ],
        &app_root_path,
        &modules_path,
        stdout,
    )?;

    log_ok!(stdout, "Library '{}' synced successfully", library_name);

    Ok(())
}

impl CliCommand for LibrarySyncCommand {
    fn command(&self) -> clap::Command {
        use clap::Arg;
        command("library", "Sync a specific library to application artifacts").arg(
            Arg::new("name")
                .help("The name of the library")
                .required(true),
        )
        .arg(
            Arg::new("base_path")
                .short('p')
                .long("path")
                .help("The application path"),
        )
        .arg(
            Arg::new("prompts")
                .short('P')
                .long("prompts")
                .help("JSON object with pre-provided answers")
                .value_name("JSON"),
        )
    }

    fn handler(&self, matches: &ArgMatches) -> Result<()> {
        let library_name = matches.get_one::<String>("name").unwrap();

        let prompts_map: HashMap<String, HashMap<String, String>> =
            if let Some(prompts_json) = matches.get_one::<String>("prompts") {
                serde_json::from_str(prompts_json).context("Failed to parse prompts JSON")?
            } else {
                HashMap::new()
            };

        let (app_root_path, _) = find_app_root_path(matches, RequiredLocation::Application)?;

        let mut stdout = StandardStream::stdout(ColorChoice::Always);
        let mut rendered_templates_cache = RenderedTemplatesCache::new();
        let manifest_path = app_root_path.join(".forklaunch").join("manifest.toml");
        rendered_templates_cache.get(&manifest_path)?;

        let manifest_template = rendered_templates_cache.get(&manifest_path)?.unwrap();
        let mut manifest_data: ApplicationManifestData =
            toml::from_str(&manifest_template.content).context(ERROR_FAILED_TO_PARSE_MANIFEST)?;

        sync_library_with_cache(
            library_name,
            &app_root_path,
            &mut manifest_data,
            matches,
            &prompts_map,
            &mut rendered_templates_cache,
            &mut stdout,
        )?;

        rendered_templates_cache.insert(
            manifest_path.to_string_lossy().to_string(),
            RenderedTemplate {
                path: manifest_path.clone(),
                content: toml::to_string_pretty(&manifest_data)
                    .context("Failed to serialize manifest")?,
                context: Some("Failed to write manifest".to_string()),
            },
        );

        let rendered_templates: Vec<_> = rendered_templates_cache
            .drain()
            .map(|(_, template)| template)
            .collect();

        write_rendered_templates(&rendered_templates, false, &mut stdout)?;

        Ok(())
    }
}