pesde 0.7.3

A package manager for the Luau programming language, supporting multiple runtimes including Roblox and Lune
Documentation
use crate::cli::{
	config::read_config,
	style::{ERROR_PREFIX, INFO_STYLE, SUCCESS_STYLE},
};
use anyhow::Context as _;
use clap::Args;
use inquire::validator::Validation;
use pesde::{
	errors::ManifestReadError,
	manifest::{target::TargetKind, DependencyType},
	names::PackageName,
	source::{
		git_index::GitBasedSource as _,
		pesde::{specifier::PesdeDependencySpecifier, PesdePackageSource},
		specifiers::DependencySpecifiers,
		traits::{PackageSource as _, RefreshOptions, ResolveOptions},
		PackageSources,
	},
	Project, RefreshedSources, DEFAULT_INDEX_NAME, SCRIPTS_LINK_FOLDER,
};
use semver::VersionReq;
use std::{fmt::Display, str::FromStr as _};

#[derive(Debug, Args)]
pub struct InitCommand;

#[derive(Debug)]
enum PackageNameOrCustom {
	PackageName(PackageName),
	Custom,
}

impl Display for PackageNameOrCustom {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		match self {
			PackageNameOrCustom::PackageName(n) => write!(f, "{n}"),
			PackageNameOrCustom::Custom => write!(f, "custom"),
		}
	}
}

impl InitCommand {
	pub async fn run(self, project: Project) -> anyhow::Result<()> {
		match project.read_manifest().await {
			Ok(_) => {
				anyhow::bail!("project already initialized");
			}
			Err(ManifestReadError::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => {}
			Err(e) => return Err(e.into()),
		}

		let mut manifest = toml_edit::DocumentMut::new();

		manifest["name"] = toml_edit::value(
			inquire::Text::new("what is the name of the project?")
				.with_validator(|name: &str| {
					Ok(match PackageName::from_str(name) {
						Ok(_) => Validation::Valid,
						Err(e) => Validation::Invalid(e.to_string().into()),
					})
				})
				.prompt()
				.unwrap(),
		);
		manifest["version"] = toml_edit::value("0.1.0");

		let description = inquire::Text::new("what is the description of the project?")
			.with_help_message("a short description of the project. leave empty for none")
			.prompt()
			.unwrap();

		if !description.is_empty() {
			manifest["description"] = toml_edit::value(description);
		}

		let authors = inquire::Text::new("who are the authors of this project?")
			.with_help_message("comma separated list. leave empty for none")
			.prompt()
			.unwrap();

		let authors = authors
			.split(',')
			.map(str::trim)
			.filter(|s| !s.is_empty())
			.collect::<toml_edit::Array>();

		if !authors.is_empty() {
			manifest["authors"] = toml_edit::value(authors);
		}

		let repo = inquire::Text::new("what is the repository URL of this project?")
			.with_validator(|repo: &str| {
				if repo.is_empty() {
					return Ok(Validation::Valid);
				}

				Ok(match url::Url::parse(repo) {
					Ok(_) => Validation::Valid,
					Err(e) => Validation::Invalid(e.to_string().into()),
				})
			})
			.with_help_message("leave empty for none")
			.prompt()
			.unwrap();
		if !repo.is_empty() {
			manifest["repository"] = toml_edit::value(repo);
		}

		let license = inquire::Text::new("what is the license of this project?")
			.with_initial_value("MIT")
			.with_help_message("an SPDX license identifier. leave empty for none")
			.prompt()
			.unwrap();
		if !license.is_empty() {
			manifest["license"] = toml_edit::value(license);
		}

		let target_env = inquire::Select::new(
			"what environment are you targeting for your package?",
			TargetKind::VARIANTS.to_vec(),
		)
		.prompt()
		.unwrap();

		manifest["target"].or_insert(toml_edit::Item::Table(toml_edit::Table::new()))
			["environment"] = toml_edit::value(target_env.to_string());

		let source = PesdePackageSource::new(read_config().await?.default_index);

		manifest["indices"].or_insert(toml_edit::Item::Table(toml_edit::Table::new()))
			[DEFAULT_INDEX_NAME] = toml_edit::value(source.repo_url().to_bstring().to_string());

		let refreshed_sources = RefreshedSources::new();

		if target_env.is_roblox()
			|| inquire::prompt_confirmation("would you like to setup Roblox compatibility scripts?")
				.unwrap()
		{
			refreshed_sources
				.refresh(
					&PackageSources::Pesde(source.clone()),
					&RefreshOptions {
						project: project.clone(),
					},
				)
				.await
				.context("failed to refresh package source")?;
			let config = source
				.config(&project)
				.await
				.context("failed to get source config")?;

			let scripts_package = if config.scripts_packages.is_empty() {
				PackageNameOrCustom::Custom
			} else {
				inquire::Select::new(
					"which scripts package do you want to use?",
					config
						.scripts_packages
						.into_iter()
						.map(PackageNameOrCustom::PackageName)
						.chain(std::iter::once(PackageNameOrCustom::Custom))
						.collect(),
				)
				.prompt()
				.unwrap()
			};

			let scripts_package = match scripts_package {
				PackageNameOrCustom::PackageName(p) => Some(p),
				PackageNameOrCustom::Custom => {
					let name = inquire::Text::new("which scripts package to use?")
						.with_validator(|name: &str| {
							if name.is_empty() {
								return Ok(Validation::Valid);
							}

							Ok(match PackageName::from_str(name) {
								Ok(_) => Validation::Valid,
								Err(e) => Validation::Invalid(e.to_string().into()),
							})
						})
						.with_help_message("leave empty for none")
						.prompt()
						.unwrap();

					if name.is_empty() {
						None
					} else {
						Some(PackageName::from_str(&name).unwrap())
					}
				}
			};

			if let Some(scripts_pkg_name) = scripts_package {
				let (v_id, pkg_ref) = source
					.resolve(
						&PesdeDependencySpecifier {
							name: scripts_pkg_name.clone(),
							version: VersionReq::STAR,
							index: DEFAULT_INDEX_NAME.into(),
							target: None,
						},
						&ResolveOptions {
							project: project.clone(),
							target: TargetKind::Luau,
							refreshed_sources,
							loose_target: true,
						},
					)
					.await
					.context("failed to resolve scripts package")?
					.1
					.pop_last()
					.context("scripts package not found")?;

				let mut file = source
					.read_index_file(&scripts_pkg_name, &project)
					.await
					.context("failed to read scripts package index file")?
					.context("scripts package not found in index")?;

				let entry = file
					.entries
					.remove(&v_id)
					.context("failed to remove scripts package entry")?;

				let Some(scripts) = entry.target.scripts().filter(|s| !s.is_empty()) else {
					anyhow::bail!("scripts package has no scripts.")
				};

				let scripts_field =
					manifest["scripts"].or_insert(toml_edit::Item::Table(toml_edit::Table::new()));

				for script_name in scripts.keys() {
					scripts_field[script_name] = toml_edit::value(format!(
						"{SCRIPTS_LINK_FOLDER}/scripts/{script_name}.luau"
					));
				}

				let dev_deps = manifest["dev_dependencies"]
					.or_insert(toml_edit::Item::Table(toml_edit::Table::new()));

				let field = &mut dev_deps["scripts"];
				field["name"] = toml_edit::value(scripts_pkg_name.to_string());
				field["version"] = toml_edit::value(format!("^{}", v_id.version()));
				field["target"] = toml_edit::value(v_id.target().to_string());

				for (alias, (spec, ty)) in pkg_ref.dependencies {
					if ty != DependencyType::Peer {
						continue;
					}

					let DependencySpecifiers::Pesde(spec) = spec else {
						continue;
					};

					let field = &mut dev_deps[alias.as_str()];
					field["name"] = toml_edit::value(spec.name.to_string());
					field["version"] = toml_edit::value(spec.version.to_string());
					field["target"] =
						toml_edit::value(spec.target.unwrap_or_else(|| v_id.target()).to_string());
				}

				if !entry.engines.is_empty() {
					let engines = manifest["engines"]
						.or_insert(toml_edit::Item::Table(toml_edit::Table::new()));

					for (engine, req) in entry.engines {
						engines[engine.to_string()] = toml_edit::value(req.to_string());
					}
				}
			} else {
				println!(
					"{ERROR_PREFIX}: no scripts package configured, this can cause issues with Roblox compatibility"
				);
				if !inquire::prompt_confirmation("initialize regardless?").unwrap() {
					return Ok(());
				}
			}
		}

		project.write_manifest(manifest.to_string()).await?;

		println!(
			"{}\n{}: run `install` to fully finish setup",
			SUCCESS_STYLE.apply_to("initialized project"),
			INFO_STYLE.apply_to("tip")
		);
		Ok(())
	}
}