feat(cli): add init, add, remove, and outdated commands

This commit is contained in:
daimond113 2024-03-10 19:03:12 +01:00
parent f2758c6351
commit 0be8a520c3
No known key found for this signature in database
GPG key ID: 3A8ECE51328B513C
12 changed files with 476 additions and 117 deletions

131
Cargo.lock generated
View file

@ -509,9 +509,9 @@ dependencies = [
[[package]] [[package]]
name = "async-io" name = "async-io"
version = "2.3.1" version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f97ab0c5b00a7cdbe5a371b9a782ee7be1316095885c8a4ea1daf490eb0ef65" checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884"
dependencies = [ dependencies = [
"async-lock 3.3.0", "async-lock 3.3.0",
"cfg-if", "cfg-if",
@ -580,7 +580,7 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5" checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5"
dependencies = [ dependencies = [
"async-io 2.3.1", "async-io 2.3.2",
"async-lock 2.8.0", "async-lock 2.8.0",
"atomic-waker", "atomic-waker",
"cfg-if", "cfg-if",
@ -790,9 +790,9 @@ dependencies = [
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.15.3" version = "3.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa"
[[package]] [[package]]
name = "bytecount" name = "bytecount"
@ -829,9 +829,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.89" version = "1.0.90"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0ba8f7aaa012f30d5b2861462f6708eccd49c3c39863fe083a308035f63d723" checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5"
dependencies = [ dependencies = [
"jobserver", "jobserver",
"libc", "libc",
@ -1077,6 +1077,31 @@ version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
[[package]]
name = "crossterm"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
dependencies = [
"bitflags 1.3.2",
"crossterm_winapi",
"libc",
"mio",
"parking_lot",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "crunchy" name = "crunchy"
version = "0.2.2" version = "0.2.2"
@ -1280,6 +1305,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b"
[[package]]
name = "dyn-clone"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
[[package]] [[package]]
name = "either" name = "either"
version = "1.10.0" version = "1.10.0"
@ -1349,9 +1380,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]] [[package]]
name = "erased-serde" name = "erased-serde"
version = "0.4.3" version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388979d208a049ffdfb22fa33b9c81942215b940910bccfe258caeb25d125cb3" checksum = "2b73807008a3c7f171cc40312f37d95ef0396e048b5848d775f54b1a4dd4a0d3"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@ -1689,6 +1720,24 @@ dependencies = [
"slab", "slab",
] ]
[[package]]
name = "fuzzy-matcher"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
dependencies = [
"thread_local",
]
[[package]]
name = "fxhash"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
dependencies = [
"byteorder 1.5.0",
]
[[package]] [[package]]
name = "generator" name = "generator"
version = "0.7.5" version = "0.7.5"
@ -2119,6 +2168,23 @@ dependencies = [
"log", "log",
] ]
[[package]]
name = "inquire"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd05e4e63529f3c9c5f5c668c398217f72756ffe48c85266b49692c55accd1f7"
dependencies = [
"bitflags 2.4.2",
"crossterm",
"dyn-clone",
"fuzzy-matcher",
"fxhash",
"newline-converter",
"once_cell",
"unicode-segmentation",
"unicode-width",
]
[[package]] [[package]]
name = "instant" name = "instant"
version = "0.1.12" version = "0.1.12"
@ -2664,6 +2730,15 @@ dependencies = [
"tempfile", "tempfile",
] ]
[[package]]
name = "newline-converter"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f"
dependencies = [
"unicode-segmentation",
]
[[package]] [[package]]
name = "nibble_vec" name = "nibble_vec"
version = "0.1.0" version = "0.1.0"
@ -3034,6 +3109,7 @@ dependencies = [
"ignore", "ignore",
"indicatif", "indicatif",
"indicatif-log-bridge", "indicatif-log-bridge",
"inquire",
"keyring", "keyring",
"log", "log",
"lune", "lune",
@ -3053,7 +3129,7 @@ dependencies = [
[[package]] [[package]]
name = "pesde-registry" name = "pesde-registry"
version = "0.2.0" version = "0.3.0"
dependencies = [ dependencies = [
"actix-cors", "actix-cors",
"actix-governor", "actix-governor",
@ -3551,9 +3627,9 @@ dependencies = [
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.11.24" version = "0.11.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" checksum = "0eea5a9eb898d3783f17c6407670e3592fd174cb81a10e51d4c37f49450b9946"
dependencies = [ dependencies = [
"base64 0.21.7", "base64 0.21.7",
"bytes", "bytes",
@ -4183,6 +4259,27 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
[[package]]
name = "signal-hook"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-mio"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
dependencies = [
"libc",
"mio",
"signal-hook",
]
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.1" version = "1.4.1"
@ -4391,20 +4488,20 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]] [[package]]
name = "system-configuration" name = "system-configuration"
version = "0.5.1" version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" checksum = "658bc6ee10a9b4fcf576e9b0819d95ec16f4d2c02d39fd83ac1c8789785c4a42"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 2.4.2",
"core-foundation", "core-foundation",
"system-configuration-sys", "system-configuration-sys",
] ]
[[package]] [[package]]
name = "system-configuration-sys" name = "system-configuration-sys"
version = "0.5.0" version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
dependencies = [ dependencies = [
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",

View file

@ -9,7 +9,7 @@ homepage = "https://pesde.daimond113.com"
include = ["src/**/*", "Cargo.toml", "Cargo.lock", "README.md", "LICENSE", "CHANGELOG.md"] include = ["src/**/*", "Cargo.toml", "Cargo.lock", "README.md", "LICENSE", "CHANGELOG.md"]
[features] [features]
bin = ["clap", "directories", "keyring", "anyhow", "ignore", "pretty_env_logger", "serde_json", "reqwest/json", "reqwest/multipart", "lune", "futures-executor", "indicatif", "auth-git2", "indicatif-log-bridge"] bin = ["clap", "directories", "keyring", "anyhow", "ignore", "pretty_env_logger", "serde_json", "reqwest/json", "reqwest/multipart", "lune", "futures-executor", "indicatif", "auth-git2", "indicatif-log-bridge", "inquire"]
[[bin]] [[bin]]
name = "pesde" name = "pesde"
@ -46,6 +46,7 @@ futures-executor = { version = "0.3.30", optional = true }
indicatif = { version = "0.17.8", optional = true } indicatif = { version = "0.17.8", optional = true }
auth-git2 = { version = "0.5.3", optional = true } auth-git2 = { version = "0.5.3", optional = true }
indicatif-log-bridge = { version = "0.2.2", optional = true } indicatif-log-bridge = { version = "0.2.2", optional = true }
inquire = { version = "0.7.0", optional = true }
[dev-dependencies] [dev-dependencies]
tempfile = "3.10.1" tempfile = "3.10.1"

View file

@ -17,10 +17,10 @@ pub fn auth_command(cmd: AuthCommand, params: CliParams) -> anyhow::Result<()> {
match cmd { match cmd {
AuthCommand::Login => { AuthCommand::Login => {
let response = send_request!(params.reqwest_client.post(Url::parse_with_params( let response = send_request(params.reqwest_client.post(Url::parse_with_params(
"https://github.com/login/device/code", "https://github.com/login/device/code",
&[("client_id", index_config.github_oauth_client_id.as_str())], &[("client_id", index_config.github_oauth_client_id.as_str())],
)?)) )?))?
.json::<serde_json::Value>()?; .json::<serde_json::Value>()?;
println!( println!(
@ -43,14 +43,14 @@ pub fn auth_command(cmd: AuthCommand, params: CliParams) -> anyhow::Result<()> {
while time_left > 0 { while time_left > 0 {
std::thread::sleep(interval); std::thread::sleep(interval);
time_left -= interval.as_secs() as i64; time_left -= interval.as_secs() as i64;
let response = send_request!(params.reqwest_client.post(Url::parse_with_params( let response = send_request(params.reqwest_client.post(Url::parse_with_params(
"https://github.com/login/oauth/access_token", "https://github.com/login/oauth/access_token",
&[ &[
("client_id", index_config.github_oauth_client_id.as_str()), ("client_id", index_config.github_oauth_client_id.as_str()),
("device_code", device_code), ("device_code", device_code),
("grant_type", "urn:ietf:params:oauth:grant-type:device_code") ("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
], ],
)?)) )?))?
.json::<serde_json::Value>()?; .json::<serde_json::Value>()?;
match response match response
@ -82,10 +82,12 @@ pub fn auth_command(cmd: AuthCommand, params: CliParams) -> anyhow::Result<()> {
params.api_token_entry.set_password(access_token)?; params.api_token_entry.set_password(access_token)?;
let response = send_request!(params let response = send_request(
.reqwest_client params
.get("https://api.github.com/user") .reqwest_client
.header(AUTHORIZATION, format!("Bearer {access_token}"))) .get("https://api.github.com/user")
.header(AUTHORIZATION, format!("Bearer {access_token}")),
)?
.json::<serde_json::Value>()?; .json::<serde_json::Value>()?;
let login = response["login"] let login = response["login"]

View file

@ -8,6 +8,7 @@ use crate::{CliConfig, CliParams};
pub enum ConfigCommand { pub enum ConfigCommand {
/// Sets the index repository URL /// Sets the index repository URL
SetIndexRepo { SetIndexRepo {
/// The URL of the index repository
#[clap(value_name = "URL")] #[clap(value_name = "URL")]
url: String, url: String,
}, },
@ -16,6 +17,7 @@ pub enum ConfigCommand {
/// Sets the cache directory /// Sets the cache directory
SetCacheDir { SetCacheDir {
/// The directory to use as the cache directory
#[clap(value_name = "DIRECTORY")] #[clap(value_name = "DIRECTORY")]
directory: Option<PathBuf>, directory: Option<PathBuf>,
}, },

View file

@ -1,11 +1,13 @@
use std::{ use std::{
fs::{create_dir_all, read, remove_dir_all, write}, fs::{create_dir_all, read, remove_dir_all, write, File},
str::FromStr,
time::Duration, time::Duration,
}; };
use flate2::{write::GzEncoder, Compression}; use flate2::{write::GzEncoder, Compression};
use futures_executor::block_on; use futures_executor::block_on;
use ignore::{overrides::OverrideBuilder, WalkBuilder}; use ignore::{overrides::OverrideBuilder, WalkBuilder};
use inquire::{validator::Validation, Select, Text};
use log::debug; use log::debug;
use lune::Runtime; use lune::Runtime;
use reqwest::{header::AUTHORIZATION, Url}; use reqwest::{header::AUTHORIZATION, Url};
@ -14,13 +16,15 @@ use serde_json::Value;
use tar::Builder as TarBuilder; use tar::Builder as TarBuilder;
use pesde::{ use pesde::{
dependencies::PackageRef, dependencies::{registry::RegistryDependencySpecifier, DependencySpecifier, PackageRef},
index::{GitIndex, Index}, index::{GitIndex, Index},
manifest::Manifest, manifest::{Manifest, PathStyle, Realm},
multithread::MultithreadedJob,
package_name::PackageName, package_name::PackageName,
patches::{create_patch, setup_patches_repo}, patches::{create_patch, setup_patches_repo},
project::{InstallOptions, Project}, project::{InstallOptions, Project},
DEV_PACKAGES_FOLDER, IGNORED_FOLDERS, PACKAGES_FOLDER, PATCHES_FOLDER, SERVER_PACKAGES_FOLDER, DEV_PACKAGES_FOLDER, IGNORED_FOLDERS, MANIFEST_FILE_NAME, PACKAGES_FOLDER, PATCHES_FOLDER,
SERVER_PACKAGES_FOLDER,
}; };
use crate::{send_request, CliParams, Command}; use crate::{send_request, CliParams, Command};
@ -37,6 +41,32 @@ fn get_project(params: &CliParams) -> anyhow::Result<Project<GitIndex>> {
.map_err(Into::into) .map_err(Into::into)
} }
fn multithreaded_bar<E: Send + Sync + Into<anyhow::Error> + 'static>(
params: &CliParams,
job: MultithreadedJob<E>,
len: u64,
message: String,
) -> Result<(), anyhow::Error> {
let bar = params.multi.add(
indicatif::ProgressBar::new(len)
.with_style(
indicatif::ProgressStyle::default_bar()
.template("{msg} {bar:40.208/166} {pos}/{len} {percent}% {elapsed_precise}")?,
)
.with_message(message),
);
bar.enable_steady_tick(Duration::from_millis(100));
while let Ok(result) = job.progress().recv() {
result.map_err(Into::into)?;
bar.inc(1);
}
bar.finish_with_message("done");
Ok(())
}
pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> { pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> {
match cmd { match cmd {
Command::Install { locked } => { Command::Install { locked } => {
@ -55,21 +85,13 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> {
let resolved_versions_map = project.manifest().dependency_tree(&project, locked)?; let resolved_versions_map = project.manifest().dependency_tree(&project, locked)?;
let download_job = project.download(&resolved_versions_map)?; let download_job = project.download(&resolved_versions_map)?;
let bar = params.multi.add(
indicatif::ProgressBar::new(resolved_versions_map.len() as u64)
.with_style(indicatif::ProgressStyle::default_bar().template(
"{msg} {bar:40.208/166} {pos}/{len} {percent}% {elapsed_precise}",
)?)
.with_message("Downloading packages"),
);
bar.enable_steady_tick(Duration::from_millis(100));
while let Ok(result) = download_job.progress().recv() { multithreaded_bar(
result?; &params,
bar.inc(1); download_job,
} resolved_versions_map.len() as u64,
"Downloading packages".to_string(),
bar.finish_with_message("done"); )?;
project.install( project.install(
InstallOptions::new() InstallOptions::new()
@ -81,16 +103,16 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> {
Command::Run { package, args } => { Command::Run { package, args } => {
let project = get_project(&params)?; let project = get_project(&params)?;
let name: PackageName = package.parse()?;
let lockfile = project let lockfile = project
.lockfile()? .lockfile()?
.ok_or(anyhow::anyhow!("lockfile not found"))?; .ok_or(anyhow::anyhow!("lockfile not found"))?;
let (_, resolved_pkg) = lockfile let (_, resolved_pkg) = lockfile
.get(&name) .get(&package)
.and_then(|versions| versions.iter().find(|(_, pkg_ref)| pkg_ref.is_root)) .and_then(|versions| versions.iter().find(|(_, pkg_ref)| pkg_ref.is_root))
.ok_or(anyhow::anyhow!("package not found in lockfile"))?; .ok_or(anyhow::anyhow!(
"package not found in lockfile (or isn't root)"
))?;
if !resolved_pkg.is_root { if !resolved_pkg.is_root {
anyhow::bail!("package is not a root package"); anyhow::bail!("package is not a root package");
@ -116,10 +138,10 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> {
let config = params.index.config()?; let config = params.index.config()?;
let api_url = config.api(); let api_url = config.api();
let response = send_request!(params.reqwest_client.get(Url::parse_with_params( let response = send_request(params.reqwest_client.get(Url::parse_with_params(
&format!("{}/v0/search", api_url), &format!("{}/v0/search", api_url),
&query.map_or_else(Vec::new, |q| vec![("query", q)]) &query.map_or_else(Vec::new, |q| vec![("query", q)]),
)?)) )?))?
.json::<Value>()?; .json::<Value>()?;
for package in response.as_array().unwrap() { for package in response.as_array().unwrap() {
@ -201,30 +223,24 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> {
request = request.header(AUTHORIZATION, ""); request = request.header(AUTHORIZATION, "");
} }
println!("{}", send_request!(request).text()?); println!("{}", send_request(request)?.text()?);
} }
Command::Patch { package } => { Command::Patch { package } => {
let project = get_project(&params)?; let project = get_project(&params)?;
let (name, version) = package
.split_once('@')
.ok_or(anyhow::anyhow!("Malformed package name"))?;
let name: PackageName = name.parse()?;
let version = Version::parse(version)?;
let lockfile = project let lockfile = project
.lockfile()? .lockfile()?
.ok_or(anyhow::anyhow!("lockfile not found"))?; .ok_or(anyhow::anyhow!("lockfile not found"))?;
let resolved_pkg = lockfile let resolved_pkg = lockfile
.get(&name) .get(&package.0)
.and_then(|versions| versions.get(&version)) .and_then(|versions| versions.get(&package.1))
.ok_or(anyhow::anyhow!("package not found in lockfile"))?; .ok_or(anyhow::anyhow!("package not found in lockfile"))?;
let dir = params.directories.data_dir().join("patches").join(format!( let dir = params.directories.data_dir().join("patches").join(format!(
"{}_{}", "{}_{}",
name.escaped(), package.0.escaped(),
version package.1
)); ));
if dir.exists() { if dir.exists() {
@ -274,6 +290,158 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> {
env!("CARGO_BIN_NAME") env!("CARGO_BIN_NAME")
); );
} }
Command::Init => {
let manifest_path = params.cwd.join(MANIFEST_FILE_NAME);
if manifest_path.exists() {
anyhow::bail!("manifest already exists");
}
let default_name = params.cwd.file_name().unwrap().to_str().unwrap();
let name = Text::new("What is the name of the package?")
.with_initial_value(default_name)
.with_validator(|name: &str| {
Ok(match PackageName::from_str(name) {
Ok(_) => Validation::Valid,
Err(e) => Validation::Invalid(e.into()),
})
})
.prompt()?;
let path_style =
Select::new("What style of paths do you want to use?", vec!["roblox"]).prompt()?;
let path_style = match path_style {
"roblox" => PathStyle::Roblox {
place: Default::default(),
},
_ => unreachable!(),
};
let description = Text::new("What is the description of the package?").prompt()?;
let license = Text::new("What is the license of the package?").prompt()?;
let authors = Text::new("Who are the authors of the package? (split using ;)")
.prompt()?
.split(';')
.map(|s| s.trim().to_string())
.collect::<Vec<String>>();
let private = Select::new("Is this package private?", vec!["yes", "no"]).prompt()?;
let private = private == "yes";
let realm = Select::new(
"What is the realm of the package?",
vec!["shared", "server", "dev"],
)
.prompt()?;
let realm = match realm {
"shared" => Realm::Shared,
"server" => Realm::Server,
"dev" => Realm::Development,
_ => unreachable!(),
};
let manifest = Manifest {
name: name.parse()?,
version: Version::parse("0.1.0")?,
exports: Default::default(),
path_style,
private,
realm: Some(realm),
dependencies: Default::default(),
peer_dependencies: Default::default(),
description: Some(description),
license: Some(license),
authors: Some(authors),
};
serde_yaml::to_writer(File::create(manifest_path)?, &manifest)?;
}
Command::Add {
package,
realm,
peer,
} => {
let project = get_project(&params)?;
let mut manifest = project.manifest().clone();
let specifier = DependencySpecifier::Registry(RegistryDependencySpecifier {
name: package.0,
version: package.1,
realm,
});
if peer {
manifest.peer_dependencies.push(specifier);
} else {
manifest.dependencies.push(specifier);
}
serde_yaml::to_writer(
File::create(project.path().join(MANIFEST_FILE_NAME))?,
&manifest,
)?;
}
Command::Remove { package } => {
let project = get_project(&params)?;
let mut manifest = project.manifest().clone();
for dependencies in [&mut manifest.dependencies, &mut manifest.peer_dependencies] {
dependencies.retain(|d| {
if let DependencySpecifier::Registry(registry) = d {
registry.name != package
} else {
true
}
});
}
serde_yaml::to_writer(
File::create(project.path().join(MANIFEST_FILE_NAME))?,
&manifest,
)?;
}
Command::Outdated => {
let project = get_project(&params)?;
let manifest = project.manifest();
let dependency_tree = manifest.dependency_tree(&project, false)?;
for (name, versions) in dependency_tree {
for (version, resolved_pkg) in versions {
if !resolved_pkg.is_root {
continue;
}
if let PackageRef::Registry(registry) = resolved_pkg.pkg_ref {
let latest_version = send_request(params.reqwest_client.get(format!(
"{}/v0/packages/{}/{}/versions",
project.index().config()?.api(),
registry.name.scope(),
registry.name.name()
)))?
.json::<Value>()?
.as_array()
.unwrap()
.iter()
.map(|v| Version::parse(v.as_str().unwrap()))
.collect::<Result<Vec<Version>, semver::Error>>()?
.into_iter()
.max()
.unwrap();
if latest_version > version {
println!(
"{name}@{version} is outdated. latest version: {latest_version}"
);
}
}
}
}
}
_ => unreachable!(), _ => unreachable!(),
} }

View file

@ -22,6 +22,7 @@ pub struct GitDependencySpecifier {
/// The revision of the git repository to use /// The revision of the git repository to use
pub rev: String, pub rev: String,
/// The realm of the package /// The realm of the package
#[serde(skip_serializing_if = "Option::is_none")]
pub realm: Option<Realm>, pub realm: Option<Realm>,
} }
@ -65,17 +66,15 @@ impl GitDependencySpecifier {
// should also work with ssh urls // should also work with ssh urls
let is_url = self.repo.contains(':'); let is_url = self.repo.contains(':');
let repo_name = { let repo_name = if !is_url {
if !is_url { self.repo.to_string()
self.repo.to_string() } else {
} else { let parts: Vec<&str> = self.repo.split('/').collect();
let parts: Vec<&str> = self.repo.split('/').collect(); format!(
format!( "{}/{}",
"{}/{}", parts[parts.len() - 2],
parts[parts.len() - 2], parts[parts.len() - 1].trim_end_matches(".git")
parts[parts.len() - 1].trim_end_matches(".git") )
)
}
}; };
if is_url { if is_url {
@ -84,12 +83,10 @@ impl GitDependencySpecifier {
debug!("assuming git repository is a name: {}", &repo_name); debug!("assuming git repository is a name: {}", &repo_name);
} }
let repo_url = { let repo_url = if !is_url {
if !is_url { format!("https://github.com/{}.git", &self.repo)
format!("https://github.com/{}.git", &self.repo) } else {
} else { self.repo.to_string()
self.repo.to_string()
}
}; };
if is_url { if is_url {

View file

@ -143,7 +143,7 @@ impl<I: Index> Project<I> {
&self, &self,
map: &ResolvedVersionsMap, map: &ResolvedVersionsMap,
) -> Result<MultithreadedJob<DownloadError>, InstallProjectError> { ) -> Result<MultithreadedJob<DownloadError>, InstallProjectError> {
let (job, tx) = MultithreadedJob::new(); let job = MultithreadedJob::new();
for (name, versions) in map.clone() { for (name, versions) in map.clone() {
for (version, resolved_package) in versions { for (version, resolved_package) in versions {
@ -162,12 +162,8 @@ impl<I: Index> Project<I> {
create_dir_all(&source)?; create_dir_all(&source)?;
let project = self.clone(); let project = self.clone();
let tx = tx.clone();
job.pool.execute(move || { job.execute(move || resolved_package.pkg_ref.download(&project, source));
let result = resolved_package.pkg_ref.download(&project, source);
tx.send(result).unwrap();
});
} }
} }

View file

@ -22,6 +22,7 @@ pub struct RegistryDependencySpecifier {
// #[serde(skip_serializing_if = "Option::is_none")] // #[serde(skip_serializing_if = "Option::is_none")]
// pub registry: Option<String>, // pub registry: Option<String>,
/// The realm of the package /// The realm of the package
#[serde(skip_serializing_if = "Option::is_none")]
pub realm: Option<Realm>, pub realm: Option<Realm>,
} }

View file

@ -515,7 +515,6 @@ impl IndexConfig {
/// An entry in the index file /// An entry in the index file
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(deny_unknown_fields)]
pub struct IndexFileEntry { pub struct IndexFileEntry {
/// The version of the package /// The version of the package
pub version: Version, pub version: Version,
@ -525,12 +524,6 @@ pub struct IndexFileEntry {
/// A description of the package /// A description of the package
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>, pub description: Option<String>,
/// The license of the package
#[serde(default, skip_serializing_if = "Option::is_none")]
pub license: Option<String>,
/// The authors of the package
#[serde(default, skip_serializing_if = "Option::is_none")]
pub authors: Option<Vec<String>>,
/// The dependencies of the package /// The dependencies of the package
#[serde(default, skip_serializing_if = "Vec::is_empty")] #[serde(default, skip_serializing_if = "Vec::is_empty")]
@ -546,8 +539,6 @@ impl From<Manifest> for IndexFileEntry {
realm: manifest.realm, realm: manifest.realm,
description: manifest.description, description: manifest.description,
license: manifest.license,
authors: manifest.authors,
dependencies, dependencies,
} }

View file

@ -3,6 +3,7 @@ use std::{
fs::{create_dir_all, read}, fs::{create_dir_all, read},
hash::{DefaultHasher, Hash, Hasher}, hash::{DefaultHasher, Hash, Hasher},
path::PathBuf, path::PathBuf,
str::FromStr,
}; };
use auth_git2::GitAuthenticator; use auth_git2::GitAuthenticator;
@ -11,8 +12,13 @@ use directories::ProjectDirs;
use indicatif::MultiProgress; use indicatif::MultiProgress;
use indicatif_log_bridge::LogWrapper; use indicatif_log_bridge::LogWrapper;
use keyring::Entry; use keyring::Entry;
use log::error;
use pretty_env_logger::env_logger::Env; use pretty_env_logger::env_logger::Env;
use reqwest::header::{ACCEPT, AUTHORIZATION}; use reqwest::{
blocking::{RequestBuilder, Response},
header::{ACCEPT, AUTHORIZATION},
};
use semver::{Version, VersionReq};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use cli::{ use cli::{
@ -20,29 +26,79 @@ use cli::{
config::{config_command, ConfigCommand}, config::{config_command, ConfigCommand},
root::root_command, root::root_command,
}; };
use pesde::index::GitIndex; use pesde::{index::GitIndex, manifest::Realm, package_name::PackageName};
mod cli; mod cli;
#[derive(Debug, Clone)]
pub struct VersionedPackageName<V: FromStr<Err = semver::Error>>(PackageName, V);
impl<V: FromStr<Err = semver::Error>> FromStr for VersionedPackageName<V> {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (name, version) = s.split_once('@').ok_or_else(|| {
anyhow::anyhow!("invalid package name: {s}; expected format: name@version")
})?;
Ok(VersionedPackageName(
name.to_string().parse()?,
version.parse()?,
))
}
}
#[derive(Subcommand)] #[derive(Subcommand)]
pub enum Command { pub enum Command {
/// Initializes a manifest file
Init,
/// Adds a package to the manifest
Add {
/// The package to add
#[clap(value_name = "PACKAGE")]
package: VersionedPackageName<VersionReq>,
/// Whether the package is a peer dependency
#[clap(long, short)]
peer: bool,
/// The realm of the package
#[clap(long, short)]
realm: Option<Realm>,
},
/// Removes a package from the manifest
Remove {
/// The package to remove
#[clap(value_name = "PACKAGE")]
package: PackageName,
},
/// Lists outdated packages
Outdated,
/// Installs the dependencies of the project /// Installs the dependencies of the project
Install { Install {
/// Whether to use the lockfile for resolving dependencies
#[clap(long, short)] #[clap(long, short)]
locked: bool, locked: bool,
}, },
/// Runs the `bin` export of the specified package /// Runs the `bin` export of the specified package
Run { Run {
/// The package to run
#[clap(value_name = "PACKAGE")] #[clap(value_name = "PACKAGE")]
package: String, package: PackageName,
/// The arguments to pass to the package
#[clap(last = true)] #[clap(last = true)]
args: Vec<String>, args: Vec<String>,
}, },
/// Searches for a package on the registry /// Searches for a package on the registry
Search { Search {
/// The query to search for
#[clap(value_name = "QUERY")] #[clap(value_name = "QUERY")]
query: Option<String>, query: Option<String>,
}, },
@ -51,10 +107,15 @@ pub enum Command {
Publish, Publish,
/// Begins a new patch /// Begins a new patch
Patch { package: String }, Patch {
/// The package to patch
#[clap(value_name = "PACKAGE")]
package: VersionedPackageName<Version>,
},
/// Commits (finished) the patch /// Commits (finishes) the patch
PatchCommit { PatchCommit {
/// The package's changed directory
#[clap(value_name = "DIRECTORY")] #[clap(value_name = "DIRECTORY")]
dir: PathBuf, dir: PathBuf,
}, },
@ -77,6 +138,7 @@ struct Cli {
#[clap(subcommand)] #[clap(subcommand)]
command: Command, command: Command,
/// The directory to run the command in
#[arg(short, long, value_name = "DIRECTORY")] #[arg(short, long, value_name = "DIRECTORY")]
directory: Option<PathBuf>, directory: Option<PathBuf>,
} }
@ -117,18 +179,16 @@ impl CliConfig {
} }
} }
#[macro_export] pub fn send_request(request_builder: RequestBuilder) -> anyhow::Result<Response> {
macro_rules! send_request { let res = request_builder.send()?;
($req:expr) => {{
let res = $req.send()?;
match res.error_for_status_ref() { match res.error_for_status_ref() {
Ok(_) => res, Ok(_) => Ok(res),
Err(e) => { Err(e) => {
panic!("request failed: {e}\nbody: {}", res.text()?); error!("request failed: {e}\nbody: {}", res.text()?);
} Err(e.into())
} }
}}; }
} }
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {

View file

@ -1,3 +1,4 @@
use std::str::FromStr;
use std::{collections::BTreeMap, fmt::Display, fs::read}; use std::{collections::BTreeMap, fmt::Display, fs::read};
use relative_path::RelativePathBuf; use relative_path::RelativePathBuf;
@ -87,6 +88,24 @@ impl Display for Realm {
} }
} }
/// An error that occurred while parsing a realm from a string
#[derive(Debug, Error)]
#[error("invalid realm {0}")]
pub struct FromStrRealmError(String);
impl FromStr for Realm {
type Err = FromStrRealmError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"shared" => Ok(Realm::Shared),
"server" => Ok(Realm::Server),
"development" => Ok(Realm::Development),
_ => Err(FromStrRealmError(s.to_string())),
}
}
}
/// The manifest of a package /// The manifest of a package
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
// #[serde(deny_unknown_fields)] // #[serde(deny_unknown_fields)]

View file

@ -1,18 +1,30 @@
use std::sync::mpsc::Receiver; use std::sync::mpsc::{Receiver, Sender};
use threadpool::ThreadPool; use threadpool::ThreadPool;
/// A multithreaded job /// A multithreaded job
pub struct MultithreadedJob<E> { pub struct MultithreadedJob<E: Send + Sync + 'static> {
pub(crate) progress: Receiver<Result<(), E>>, progress: Receiver<Result<(), E>>,
pub(crate) pool: ThreadPool, sender: Sender<Result<(), E>>,
pool: ThreadPool,
} }
impl<E> MultithreadedJob<E> { impl<E: Send + Sync + 'static> Default for MultithreadedJob<E> {
pub(crate) fn new() -> (Self, std::sync::mpsc::Sender<Result<(), E>>) { fn default() -> Self {
let (tx, rx) = std::sync::mpsc::channel(); let (tx, rx) = std::sync::mpsc::channel();
let pool = ThreadPool::new(6); let pool = ThreadPool::new(6);
(Self { progress: rx, pool }, tx) Self {
progress: rx,
pool,
sender: tx.clone(),
}
}
}
impl<E: Send + Sync + 'static> MultithreadedJob<E> {
/// Creates a new multithreaded job
pub fn new() -> Self {
Self::default()
} }
/// Returns the progress of the job /// Returns the progress of the job
@ -30,4 +42,17 @@ impl<E> MultithreadedJob<E> {
Ok(()) Ok(())
} }
/// Executes a function on the thread pool
pub fn execute<F>(&self, f: F)
where
F: (FnOnce() -> Result<(), E>) + Send + 'static,
{
let sender = self.sender.clone();
self.pool.execute(move || {
let result = f();
sender.send(result).unwrap();
});
}
} }