refactor(cli): 🎨 use static variables

This commit is contained in:
daimond113 2024-03-17 01:19:53 +01:00
parent 9e1ffc41b6
commit 5c77d8db76
No known key found for this signature in database
GPG key ID: 3A8ECE51328B513C
12 changed files with 520 additions and 387 deletions

57
Cargo.lock generated
View file

@ -92,7 +92,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb"
dependencies = [
"quote",
"syn 2.0.52",
"syn 2.0.53",
]
[[package]]
@ -130,7 +130,7 @@ dependencies = [
"parse-size",
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.53",
]
[[package]]
@ -243,7 +243,7 @@ dependencies = [
"actix-router",
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.53",
]
[[package]]
@ -571,7 +571,7 @@ checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.53",
]
[[package]]
@ -600,13 +600,13 @@ checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799"
[[package]]
name = "async-trait"
version = "0.1.77"
version = "0.1.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9"
checksum = "461abc97219de0eaaf81fe3ef974a540158f3d079c2ab200f891f1a2ef201e85"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.53",
]
[[package]]
@ -925,7 +925,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.53",
]
[[package]]
@ -1146,7 +1146,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn 2.0.52",
"syn 2.0.53",
]
[[package]]
@ -1157,7 +1157,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
dependencies = [
"darling_core",
"quote",
"syn 2.0.52",
"syn 2.0.53",
]
[[package]]
@ -1363,7 +1363,7 @@ checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.53",
]
[[package]]
@ -1711,7 +1711,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.53",
]
[[package]]
@ -2604,9 +2604,9 @@ dependencies = [
[[package]]
name = "luau0-src"
version = "0.8.4+luau616"
version = "0.8.5+luau617"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74f34c6c8e52606273c5d689b722e03383e58fed85840868885301fe9038fbd9"
checksum = "652e36b8c35d807ec76a4931fe7c62883c62cc93311fb49bf5b76084647078ea"
dependencies = [
"cc",
]
@ -3103,7 +3103,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.53",
]
[[package]]
@ -3263,6 +3263,7 @@ dependencies = [
"keyring",
"log",
"lune",
"once_cell",
"pathdiff",
"pretty_env_logger",
"relative-path",
@ -3280,7 +3281,7 @@ dependencies = [
[[package]]
name = "pesde-registry"
version = "0.4.0"
version = "0.5.0"
dependencies = [
"actix-cors",
"actix-governor",
@ -3325,7 +3326,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.53",
]
[[package]]
@ -3470,7 +3471,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd"
dependencies = [
"quote",
"syn 2.0.52",
"syn 2.0.53",
]
[[package]]
@ -4320,7 +4321,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.53",
]
[[package]]
@ -4352,7 +4353,7 @@ checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.53",
]
[[package]]
@ -4653,9 +4654,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.52"
version = "2.0.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07"
checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032"
dependencies = [
"proc-macro2",
"quote",
@ -4888,7 +4889,7 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.53",
]
[[package]]
@ -5022,7 +5023,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.53",
]
[[package]]
@ -5180,7 +5181,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.53",
]
[[package]]
@ -5474,7 +5475,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.53",
"wasm-bindgen-shared",
]
@ -5508,7 +5509,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.53",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -5872,7 +5873,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.53",
]
[[package]]

View file

@ -9,7 +9,7 @@ homepage = "https://pesde.daimond113.com"
include = ["src/**/*", "Cargo.toml", "Cargo.lock", "README.md", "LICENSE", "CHANGELOG.md"]
[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", "inquire"]
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", "once_cell"]
[[bin]]
name = "pesde"
@ -48,6 +48,7 @@ indicatif = { version = "0.17.8", optional = true }
auth-git2 = { version = "0.5.4", optional = true }
indicatif-log-bridge = { version = "0.2.2", optional = true }
inquire = { version = "0.7.1", optional = true }
once_cell = { version = "1.19.0", optional = true }
[dev-dependencies]
tempfile = "3.10.1"

View file

@ -174,10 +174,7 @@ pub async fn get_package_version(
match index.package(&package_name)? {
Some(package) => {
if version == "latest" {
version = package
.last()
.map(|v| v.version.to_string())
.unwrap();
version = package.last().map(|v| v.version.to_string()).unwrap();
} else if !package.iter().any(|v| v.version.to_string() == version) {
return Ok(HttpResponse::NotFound().finish());
}

147
src/cli/api_token.rs Normal file
View file

@ -0,0 +1,147 @@
use std::path::PathBuf;
use keyring::Entry;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use crate::cli::INDEX_DIR;
pub trait ApiTokenSource: Send + Sync {
fn get_api_token(&self) -> anyhow::Result<Option<String>>;
fn set_api_token(&self, api_token: &str) -> anyhow::Result<()>;
fn delete_api_token(&self) -> anyhow::Result<()>;
fn persists(&self) -> bool {
true
}
}
pub struct EnvVarApiTokenSource;
const API_TOKEN_ENV_VAR: &str = "PESDE_API_TOKEN";
impl ApiTokenSource for EnvVarApiTokenSource {
fn get_api_token(&self) -> anyhow::Result<Option<String>> {
match std::env::var(API_TOKEN_ENV_VAR) {
Ok(token) => Ok(Some(token)),
Err(std::env::VarError::NotPresent) => Ok(None),
Err(e) => Err(e.into()),
}
}
// don't need to implement set_api_token or delete_api_token
fn set_api_token(&self, _api_token: &str) -> anyhow::Result<()> {
Ok(())
}
fn delete_api_token(&self) -> anyhow::Result<()> {
Ok(())
}
fn persists(&self) -> bool {
false
}
}
static KEYRING_ENTRY: Lazy<Entry> =
Lazy::new(|| Entry::new(env!("CARGO_BIN_NAME"), "api_token").unwrap());
pub struct KeyringApiTokenSource;
impl ApiTokenSource for KeyringApiTokenSource {
fn get_api_token(&self) -> anyhow::Result<Option<String>> {
match KEYRING_ENTRY.get_password() {
Ok(api_token) => Ok(Some(api_token)),
Err(err) => match err {
keyring::Error::NoEntry | keyring::Error::PlatformFailure(_) => Ok(None),
_ => Err(err.into()),
},
}
}
fn set_api_token(&self, api_token: &str) -> anyhow::Result<()> {
KEYRING_ENTRY.set_password(api_token)?;
Ok(())
}
fn delete_api_token(&self) -> anyhow::Result<()> {
KEYRING_ENTRY.delete_password()?;
Ok(())
}
}
static AUTH_FILE_PATH: Lazy<PathBuf> = Lazy::new(|| INDEX_DIR.join("auth.yaml"));
static AUTH_FILE: Lazy<AuthFile> =
Lazy::new(
|| match std::fs::read_to_string(AUTH_FILE_PATH.to_path_buf()) {
Ok(config) => serde_yaml::from_str(&config).unwrap(),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => AuthFile::default(),
Err(e) => panic!("{:?}", e),
},
);
#[derive(Serialize, Deserialize, Default, Clone)]
struct AuthFile {
#[serde(default)]
api_token: Option<String>,
}
pub struct ConfigFileApiTokenSource;
impl ApiTokenSource for ConfigFileApiTokenSource {
fn get_api_token(&self) -> anyhow::Result<Option<String>> {
Ok(AUTH_FILE.api_token.clone())
}
fn set_api_token(&self, api_token: &str) -> anyhow::Result<()> {
let mut config = AUTH_FILE.clone();
config.api_token = Some(api_token.to_string());
serde_yaml::to_writer(
&mut std::fs::File::create(AUTH_FILE_PATH.to_path_buf())?,
&config,
)?;
Ok(())
}
fn delete_api_token(&self) -> anyhow::Result<()> {
let mut config = AUTH_FILE.clone();
config.api_token = None;
serde_yaml::to_writer(
&mut std::fs::File::create(AUTH_FILE_PATH.to_path_buf())?,
&config,
)?;
Ok(())
}
}
pub static API_TOKEN_SOURCE: Lazy<Box<dyn ApiTokenSource>> = Lazy::new(|| {
let sources: Vec<Box<dyn ApiTokenSource>> = vec![
Box::new(EnvVarApiTokenSource),
Box::new(KeyringApiTokenSource),
Box::new(ConfigFileApiTokenSource),
];
let mut valid_sources = vec![];
for source in sources {
match source.get_api_token() {
Ok(Some(_)) => return source,
Ok(None) => {
if source.persists() {
valid_sources.push(source);
}
}
Err(e) => {
log::error!("error getting api token: {e}");
}
}
}
valid_sources.pop().unwrap()
});

View file

@ -2,9 +2,9 @@ use clap::Subcommand;
use pesde::index::Index;
use reqwest::{header::AUTHORIZATION, Url};
use crate::{send_request, CliParams};
use crate::cli::{api_token::API_TOKEN_SOURCE, send_request, INDEX, REQWEST_CLIENT};
#[derive(Subcommand)]
#[derive(Subcommand, Clone)]
pub enum AuthCommand {
/// Logs in to the registry
Login,
@ -12,14 +12,14 @@ pub enum AuthCommand {
Logout,
}
pub fn auth_command(cmd: AuthCommand, params: CliParams) -> anyhow::Result<()> {
let index_config = params.index.config()?;
pub fn auth_command(cmd: AuthCommand) -> anyhow::Result<()> {
match cmd {
AuthCommand::Login => {
let response = send_request(params.reqwest_client.post(Url::parse_with_params(
let github_oauth_client_id = INDEX.config()?.github_oauth_client_id;
let response = send_request(REQWEST_CLIENT.post(Url::parse_with_params(
"https://github.com/login/device/code",
&[("client_id", index_config.github_oauth_client_id.as_str())],
&[("client_id", &github_oauth_client_id)],
)?))?
.json::<serde_json::Value>()?;
@ -43,10 +43,10 @@ pub fn auth_command(cmd: AuthCommand, params: CliParams) -> anyhow::Result<()> {
while time_left > 0 {
std::thread::sleep(interval);
time_left -= interval.as_secs() as i64;
let response = send_request(params.reqwest_client.post(Url::parse_with_params(
let response = send_request(REQWEST_CLIENT.post(Url::parse_with_params(
"https://github.com/login/oauth/access_token",
&[
("client_id", index_config.github_oauth_client_id.as_str()),
("client_id", github_oauth_client_id.as_str()),
("device_code", device_code),
("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
],
@ -80,11 +80,10 @@ pub fn auth_command(cmd: AuthCommand, params: CliParams) -> anyhow::Result<()> {
.as_str()
.ok_or(anyhow::anyhow!("couldn't get access_token"))?;
params.api_token_entry.set_password(access_token)?;
API_TOKEN_SOURCE.set_api_token(access_token)?;
let response = send_request(
params
.reqwest_client
REQWEST_CLIENT
.get("https://api.github.com/user")
.header(AUTHORIZATION, format!("Bearer {access_token}")),
)?
@ -102,7 +101,7 @@ pub fn auth_command(cmd: AuthCommand, params: CliParams) -> anyhow::Result<()> {
anyhow::bail!("code expired, please re-run the login command");
}
AuthCommand::Logout => {
params.api_token_entry.delete_password()?;
API_TOKEN_SOURCE.delete_api_token()?;
println!("you're now logged out");
}

View file

@ -2,9 +2,9 @@ use std::path::PathBuf;
use clap::Subcommand;
use crate::{CliConfig, CliParams};
use crate::{cli::CLI_CONFIG, CliConfig};
#[derive(Subcommand)]
#[derive(Subcommand, Clone)]
pub enum ConfigCommand {
/// Sets the index repository URL
SetIndexRepo {
@ -25,41 +25,41 @@ pub enum ConfigCommand {
GetCacheDir,
}
pub fn config_command(cmd: ConfigCommand, params: CliParams) -> anyhow::Result<()> {
pub fn config_command(cmd: ConfigCommand) -> anyhow::Result<()> {
match cmd {
ConfigCommand::SetIndexRepo { url } => {
let cli_config = CliConfig {
index_repo_url: url.clone(),
..params.cli_config
..CLI_CONFIG.clone()
};
cli_config.write(&params.directories)?;
cli_config.write()?;
println!("index repository url set to: `{url}`");
}
ConfigCommand::GetIndexRepo => {
println!(
"current index repository url: `{}`",
params.cli_config.index_repo_url
CLI_CONFIG.index_repo_url
);
}
ConfigCommand::SetCacheDir { directory } => {
let cli_config = CliConfig {
cache_dir: directory,
..params.cli_config
..CLI_CONFIG.clone()
};
cli_config.write(&params.directories)?;
cli_config.write()?;
println!(
"cache directory set to: `{}`",
cli_config.cache_dir(&params.directories).display()
cli_config.cache_dir().display()
);
}
ConfigCommand::GetCacheDir => {
println!(
"current cache directory: `{}`",
params.cli_config.cache_dir(&params.directories).display()
CLI_CONFIG.cache_dir().display()
);
}
}

View file

@ -1,3 +1,277 @@
use crate::cli::{api_token::API_TOKEN_SOURCE, auth::AuthCommand, config::ConfigCommand};
use auth_git2::GitAuthenticator;
use clap::{Parser, Subcommand};
use directories::ProjectDirs;
use indicatif::MultiProgress;
use indicatif_log_bridge::LogWrapper;
use log::error;
use once_cell::sync::Lazy;
use pesde::{index::GitIndex, manifest::Realm, package_name::PackageName};
use pretty_env_logger::env_logger::Env;
use reqwest::{
blocking::{RequestBuilder, Response},
header::ACCEPT,
};
use semver::{Version, VersionReq};
use serde::{Deserialize, Serialize};
use std::{
hash::{DefaultHasher, Hash, Hasher},
path::PathBuf,
str::FromStr,
};
pub mod api_token;
pub mod auth;
pub mod config;
pub mod root;
#[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, Clone)]
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
Install {
/// Whether to use the lockfile for resolving dependencies
#[clap(long, short)]
locked: bool,
},
/// Runs the `bin` export of the specified package
Run {
/// The package to run
#[clap(value_name = "PACKAGE")]
package: PackageName,
/// The arguments to pass to the package
#[clap(last = true)]
args: Vec<String>,
},
/// Searches for a package on the registry
Search {
/// The query to search for
#[clap(value_name = "QUERY")]
query: Option<String>,
},
/// Publishes the project to the registry
Publish,
/// Converts a `wally.toml` file to a `pesde.yaml` file
Convert,
/// Begins a new patch
Patch {
/// The package to patch
#[clap(value_name = "PACKAGE")]
package: VersionedPackageName<Version>,
},
/// Commits (finishes) the patch
PatchCommit {
/// The package's changed directory
#[clap(value_name = "DIRECTORY")]
dir: PathBuf,
},
/// Auth-related commands
Auth {
#[clap(subcommand)]
command: AuthCommand,
},
/// Config-related commands
Config {
#[clap(subcommand)]
command: ConfigCommand,
},
}
#[derive(Parser, Clone)]
pub struct Cli {
#[clap(subcommand)]
pub command: Command,
/// The directory to run the command in
#[arg(short, long, value_name = "DIRECTORY")]
pub directory: Option<PathBuf>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct CliConfig {
pub index_repo_url: String,
pub cache_dir: Option<PathBuf>,
}
impl Default for CliConfig {
fn default() -> Self {
Self {
index_repo_url: "https://github.com/daimond113/pesde-index".to_string(),
cache_dir: None,
}
}
}
impl CliConfig {
pub fn cache_dir(&self) -> PathBuf {
self.cache_dir
.clone()
.unwrap_or_else(|| DIRS.cache_dir().to_path_buf())
}
pub fn open() -> anyhow::Result<Self> {
let cli_config_path = DIRS.config_dir().join("config.yaml");
if cli_config_path.exists() {
Ok(serde_yaml::from_slice(&std::fs::read(cli_config_path)?)?)
} else {
let config = CliConfig::default();
config.write()?;
Ok(config)
}
}
pub fn write(&self) -> anyhow::Result<()> {
let cli_config_path = DIRS.config_dir().join("config.yaml");
serde_yaml::to_writer(
&mut std::fs::File::create(cli_config_path.as_path())?,
&self,
)?;
Ok(())
}
}
pub fn send_request(request_builder: RequestBuilder) -> anyhow::Result<Response> {
let res = request_builder.send()?;
match res.error_for_status_ref() {
Ok(_) => Ok(res),
Err(e) => {
error!("request failed: {e}\nbody: {}", res.text()?);
Err(e.into())
}
}
}
pub static CLI: Lazy<Cli> = Lazy::new(Cli::parse);
pub static DIRS: Lazy<ProjectDirs> = Lazy::new(|| {
ProjectDirs::from("com", env!("CARGO_BIN_NAME"), env!("CARGO_BIN_NAME"))
.expect("couldn't get home directory")
});
pub static CLI_CONFIG: Lazy<CliConfig> = Lazy::new(|| CliConfig::open().unwrap());
pub static INDEX_DIR: Lazy<PathBuf> = Lazy::new(|| {
let mut hasher = DefaultHasher::new();
CLI_CONFIG.index_repo_url.hash(&mut hasher);
let hash = hasher.finish().to_string();
CLI_CONFIG.cache_dir().join("indices").join(hash)
});
pub static INDEX: Lazy<GitIndex> = Lazy::new(|| {
let index = GitIndex::new(
INDEX_DIR.join("index"),
&CLI_CONFIG.index_repo_url,
Some(Box::new(|| {
Box::new(|a, b, c| {
let git_authenticator = GitAuthenticator::new();
let config = git2::Config::open_default().unwrap();
let mut cred = git_authenticator.credentials(&config);
cred(a, b, c)
})
})),
);
index.refresh().unwrap();
index
});
pub static CWD: Lazy<PathBuf> = Lazy::new(|| {
CLI.directory
.clone()
.or(std::env::current_dir().ok())
.expect("couldn't get current directory")
});
pub static REQWEST_CLIENT: Lazy<reqwest::blocking::Client> = Lazy::new(|| {
let mut header_map = reqwest::header::HeaderMap::new();
header_map.insert(ACCEPT, "application/json".parse().unwrap());
header_map.insert("X-GitHub-Api-Version", "2022-11-28".parse().unwrap());
if let Ok(Some(token)) = API_TOKEN_SOURCE.get_api_token() {
header_map.insert(
reqwest::header::AUTHORIZATION,
format!("Bearer {token}").parse().unwrap(),
);
}
reqwest::blocking::Client::builder()
.user_agent(concat!(
env!("CARGO_PKG_NAME"),
"/",
env!("CARGO_PKG_VERSION")
))
.default_headers(header_map)
.build()
.unwrap()
});
pub static MULTI: Lazy<MultiProgress> = Lazy::new(|| {
let logger = pretty_env_logger::formatted_builder()
.parse_env(Env::default().default_filter_or("info"))
.build();
let multi = MultiProgress::new();
LogWrapper::new(multi.clone(), logger).try_init().unwrap();
multi
});

View file

@ -10,6 +10,7 @@ use ignore::{overrides::OverrideBuilder, WalkBuilder};
use inquire::{validator::Validation, Select, Text};
use log::debug;
use lune::Runtime;
use once_cell::sync::Lazy;
use reqwest::{header::AUTHORIZATION, Url};
use semver::Version;
use serde_json::Value;
@ -27,27 +28,19 @@ use pesde::{
SERVER_PACKAGES_FOLDER,
};
use crate::{send_request, CliParams, Command};
use crate::cli::{
api_token::API_TOKEN_SOURCE, send_request, Command, CLI_CONFIG, CWD, DIRS, INDEX, MULTI,
REQWEST_CLIENT,
};
pub const MAX_ARCHIVE_SIZE: usize = 4 * 1024 * 1024;
fn get_project(params: &CliParams) -> anyhow::Result<Project<GitIndex>> {
Project::from_path(
&params.cwd,
params.cli_config.cache_dir(&params.directories),
params.index.clone(),
params.api_token_entry.get_password().ok(),
)
.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(
let bar = MULTI.add(
indicatif::ProgressBar::new(len)
.with_style(
indicatif::ProgressStyle::default_bar()
@ -77,13 +70,21 @@ macro_rules! none_if_empty {
};
}
pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> {
pub fn root_command(cmd: Command) -> anyhow::Result<()> {
let project: Lazy<Project<GitIndex>> = Lazy::new(|| {
Project::from_path(
CWD.to_path_buf(),
CLI_CONFIG.cache_dir(),
INDEX.clone(),
API_TOKEN_SOURCE.get_api_token().ok().flatten(),
)
.unwrap()
});
match cmd {
Command::Install { locked } => {
let project = get_project(&params)?;
for packages_folder in &[PACKAGES_FOLDER, DEV_PACKAGES_FOLDER, SERVER_PACKAGES_FOLDER] {
if let Err(e) = remove_dir_all(&params.cwd.join(packages_folder)) {
if let Err(e) = remove_dir_all(CWD.join(packages_folder)) {
if e.kind() != std::io::ErrorKind::NotFound {
return Err(e.into());
} else {
@ -97,7 +98,6 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> {
let download_job = project.download(&resolved_versions_map)?;
multithreaded_bar(
&params,
download_job,
resolved_versions_map.len() as u64,
"Downloading packages".to_string(),
@ -111,8 +111,6 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> {
)?;
}
Command::Run { package, args } => {
let project = get_project(&params)?;
let lockfile = project
.lockfile()?
.ok_or(anyhow::anyhow!("lockfile not found"))?;
@ -145,10 +143,10 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> {
))?;
}
Command::Search { query } => {
let config = params.index.config()?;
let config = INDEX.config()?;
let api_url = config.api();
let response = send_request(params.reqwest_client.get(Url::parse_with_params(
let response = send_request(REQWEST_CLIENT.get(Url::parse_with_params(
&format!("{}/v0/search", api_url),
&query.map(|q| vec![("query", q)]).unwrap_or_default(),
)?))?
@ -171,8 +169,6 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> {
}
}
Command::Publish => {
let project = get_project(&params)?;
if project.manifest().private {
anyhow::bail!("package is private, cannot publish");
}
@ -180,9 +176,11 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> {
let encoder = GzEncoder::new(vec![], Compression::default());
let mut archive = TarBuilder::new(encoder);
let mut walk_builder = WalkBuilder::new(&params.cwd);
let cwd = &CWD.to_path_buf();
let mut walk_builder = WalkBuilder::new(cwd);
walk_builder.add_custom_ignore_filename(".pesdeignore");
let mut overrides = OverrideBuilder::new(&params.cwd);
let mut overrides = OverrideBuilder::new(cwd);
for packages_folder in IGNORED_FOLDERS {
overrides.add(&format!("!{}", packages_folder))?;
@ -193,7 +191,7 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> {
for entry in walk_builder.build() {
let entry = entry?;
let path = entry.path();
let relative_path = path.strip_prefix(&params.cwd)?;
let relative_path = path.strip_prefix(cwd)?;
let entry_type = entry
.file_type()
.ok_or(anyhow::anyhow!("failed to get file type"))?;
@ -222,8 +220,7 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> {
.file_name("tarball.tar.gz")
.mime_str("application/gzip")?;
let mut request = params
.reqwest_client
let mut request = REQWEST_CLIENT
.post(format!("{}/v0/packages", project.index().config()?.api()))
.multipart(reqwest::blocking::multipart::Form::new().part("tarball", part));
@ -236,8 +233,6 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> {
println!("{}", send_request(request)?.text()?);
}
Command::Patch { package } => {
let project = get_project(&params)?;
let lockfile = project
.lockfile()?
.ok_or(anyhow::anyhow!("lockfile not found"))?;
@ -247,7 +242,7 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> {
.and_then(|versions| versions.get(&package.1))
.ok_or(anyhow::anyhow!("package not found in lockfile"))?;
let dir = params.directories.data_dir().join("patches").join(format!(
let dir = DIRS.data_dir().join("patches").join(format!(
"{}_{}",
package.0.escaped(),
package.1
@ -273,8 +268,6 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> {
println!("done! modify the files in {} and run `{} patch-commit <DIRECTORY>` to commit the changes", dir.display(), env!("CARGO_BIN_NAME"));
}
Command::PatchCommit { dir } => {
let project = get_project(&params)?;
let manifest = Manifest::from_path(&dir)?;
let patch_path = project.path().join(PATCHES_FOLDER).join(format!(
"{}@{}.patch",
@ -301,13 +294,13 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> {
);
}
Command::Init => {
let manifest_path = params.cwd.join(MANIFEST_FILE_NAME);
let manifest_path = CWD.join(MANIFEST_FILE_NAME);
if manifest_path.exists() {
anyhow::bail!("manifest already exists");
}
let default_name = params.cwd.file_name().and_then(|s| s.to_str());
let default_name = CWD.file_name().and_then(|s| s.to_str());
let mut name =
Text::new("What is the name of the package?").with_validator(|name: &str| {
@ -380,8 +373,6 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> {
realm,
peer,
} => {
let project = get_project(&params)?;
let mut manifest = project.manifest().clone();
let specifier = DependencySpecifier::Registry(RegistryDependencySpecifier {
@ -402,8 +393,6 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> {
)?;
}
Command::Remove { package } => {
let project = get_project(&params)?;
let mut manifest = project.manifest().clone();
for dependencies in [&mut manifest.dependencies, &mut manifest.peer_dependencies] {
@ -422,8 +411,6 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> {
)?;
}
Command::Outdated => {
let project = get_project(&params)?;
let manifest = project.manifest();
let dependency_tree = manifest.dependency_tree(&project, false)?;
@ -434,7 +421,7 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> {
}
if let PackageRef::Registry(registry) = resolved_pkg.pkg_ref {
let latest_version = send_request(params.reqwest_client.get(format!(
let latest_version = send_request(REQWEST_CLIENT.get(format!(
"{}/v0/packages/{}/{}/versions",
project.index().config()?.api(),
registry.name.scope(),
@ -459,7 +446,7 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> {
}
}
Command::Convert => {
Manifest::from_path_or_convert(&params.cwd)?;
Manifest::from_path_or_convert(CWD.to_path_buf())?;
}
_ => unreachable!(),
}

View file

@ -152,7 +152,7 @@ impl GitPackageRef {
}
repo.reset(&obj, git2::ResetType::Hard, None)?;
Manifest::from_path_or_convert(dest)?;
Ok(())

View file

@ -163,7 +163,9 @@ impl<I: Index> Project<I> {
let project = self.clone();
job.execute(&tx, move || resolved_package.pkg_ref.download(&project, source));
job.execute(&tx, move || {
resolved_package.pkg_ref.download(&project, source)
});
}
}

View file

@ -1,289 +1,17 @@
use anyhow::bail;
use std::{
fs::{create_dir_all, read},
hash::{DefaultHasher, Hash, Hasher},
path::PathBuf,
str::FromStr,
};
use once_cell::sync::Lazy;
use auth_git2::GitAuthenticator;
use clap::{Parser, Subcommand};
use directories::ProjectDirs;
use indicatif::MultiProgress;
use indicatif_log_bridge::LogWrapper;
use keyring::Entry;
use log::error;
use pretty_env_logger::env_logger::Env;
use reqwest::{
blocking::{RequestBuilder, Response},
header::{ACCEPT, AUTHORIZATION},
};
use semver::{Version, VersionReq};
use serde::{Deserialize, Serialize};
use cli::{auth::auth_command, config::config_command, root::root_command};
use cli::{
auth::{auth_command, AuthCommand},
config::{config_command, ConfigCommand},
root::root_command,
};
use pesde::{index::GitIndex, manifest::Realm, package_name::PackageName};
use crate::cli::{CliConfig, Command, CLI, MULTI};
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)]
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
Install {
/// Whether to use the lockfile for resolving dependencies
#[clap(long, short)]
locked: bool,
},
/// Runs the `bin` export of the specified package
Run {
/// The package to run
#[clap(value_name = "PACKAGE")]
package: PackageName,
/// The arguments to pass to the package
#[clap(last = true)]
args: Vec<String>,
},
/// Searches for a package on the registry
Search {
/// The query to search for
#[clap(value_name = "QUERY")]
query: Option<String>,
},
/// Publishes the project to the registry
Publish,
/// Converts a `wally.toml` file to a `pesde.yaml` file
Convert,
/// Begins a new patch
Patch {
/// The package to patch
#[clap(value_name = "PACKAGE")]
package: VersionedPackageName<Version>,
},
/// Commits (finishes) the patch
PatchCommit {
/// The package's changed directory
#[clap(value_name = "DIRECTORY")]
dir: PathBuf,
},
/// Auth-related commands
Auth {
#[clap(subcommand)]
command: AuthCommand,
},
/// Config-related commands
Config {
#[clap(subcommand)]
command: ConfigCommand,
},
}
#[derive(Parser)]
struct Cli {
#[clap(subcommand)]
command: Command,
/// The directory to run the command in
#[arg(short, long, value_name = "DIRECTORY")]
directory: Option<PathBuf>,
}
#[derive(Serialize, Deserialize, Clone)]
struct CliConfig {
index_repo_url: String,
cache_dir: Option<PathBuf>,
}
impl CliConfig {
fn cache_dir(&self, directories: &ProjectDirs) -> PathBuf {
self.cache_dir
.clone()
.unwrap_or_else(|| directories.cache_dir().to_path_buf())
}
}
struct CliParams {
index: GitIndex,
api_token_entry: Entry,
reqwest_client: reqwest::blocking::Client,
cli_config: CliConfig,
cwd: PathBuf,
multi: MultiProgress,
directories: ProjectDirs,
}
impl CliConfig {
fn write(&self, directories: &ProjectDirs) -> anyhow::Result<()> {
let cli_config_path = directories.config_dir().join("config.yaml");
serde_yaml::to_writer(
&mut std::fs::File::create(cli_config_path.as_path())?,
&self,
)?;
Ok(())
}
}
pub fn send_request(request_builder: RequestBuilder) -> anyhow::Result<Response> {
let res = request_builder.send()?;
match res.error_for_status_ref() {
Ok(_) => Ok(res),
Err(e) => {
error!("request failed: {e}\nbody: {}", res.text()?);
Err(e.into())
}
}
}
fn main() -> anyhow::Result<()> {
let logger = pretty_env_logger::formatted_builder()
.parse_env(Env::default().default_filter_or("info"))
.build();
let multi = MultiProgress::new();
Lazy::force(&MULTI);
LogWrapper::new(multi.clone(), logger).try_init().unwrap();
let cli = Cli::parse();
let directories = ProjectDirs::from("com", env!("CARGO_BIN_NAME"), env!("CARGO_BIN_NAME"))
.expect("couldn't get home directory");
let cli_config_path = directories.config_dir().join("config.yaml");
let cli_config = if cli_config_path.exists() {
serde_yaml::from_slice(&read(cli_config_path.as_path())?)?
} else {
let config = CliConfig {
index_repo_url: "https://github.com/daimond113/pesde-index".to_string(),
cache_dir: None,
};
create_dir_all(directories.config_dir())?;
config.write(&directories)?;
config
};
let cwd_buf = cli
.directory
.or(std::env::current_dir().ok())
.ok_or(anyhow::anyhow!("couldn't get current directory"))?;
let api_token_entry = Entry::new(env!("CARGO_BIN_NAME"), "api_token")?;
let mut hasher = DefaultHasher::new();
cli_config.index_repo_url.hash(&mut hasher);
let hash = hasher.finish().to_string();
let index = GitIndex::new(
cli_config.cache_dir(&directories).join("index").join(hash),
&cli_config.index_repo_url,
Some(Box::new(|| {
Box::new(|a, b, c| {
let git_authenticator = GitAuthenticator::new();
let config = git2::Config::open_default().unwrap();
let mut cred = git_authenticator.credentials(&config);
cred(a, b, c)
})
})),
);
index.refresh()?;
let mut header_map = reqwest::header::HeaderMap::new();
header_map.insert(ACCEPT, "application/json".parse()?);
header_map.insert("X-GitHub-Api-Version", "2022-11-28".parse()?);
match api_token_entry.get_password() {
Ok(api_token) => {
header_map.insert(AUTHORIZATION, format!("Bearer {api_token}").parse()?);
}
Err(err) => match err {
keyring::Error::NoEntry => {}
_ => {
bail!("error getting api token from keyring: {err}")
}
},
};
let reqwest_client = reqwest::blocking::Client::builder()
.user_agent(concat!(
env!("CARGO_PKG_NAME"),
"/",
env!("CARGO_PKG_VERSION")
))
.default_headers(header_map)
.build()?;
let params = CliParams {
index,
api_token_entry,
reqwest_client,
cli_config,
cwd: cwd_buf,
multi,
directories,
};
match cli.command {
Command::Auth { command } => auth_command(command, params),
Command::Config { command } => config_command(command, params),
cmd => root_command(cmd, params),
match CLI.command.clone() {
Command::Auth { command } => auth_command(command),
Command::Config { command } => config_command(command),
cmd => root_command(cmd),
}
}

View file

@ -13,10 +13,7 @@ impl<E: Send + Sync + 'static> MultithreadedJob<E> {
let (tx, rx) = std::sync::mpsc::channel();
let pool = ThreadPool::new(6);
(Self {
progress: rx,
pool,
}, tx)
(Self { progress: rx, pool }, tx)
}
/// Returns the progress of the job
@ -41,7 +38,7 @@ impl<E: Send + Sync + 'static> MultithreadedJob<E> {
F: (FnOnce() -> Result<(), E>) + Send + 'static,
{
let sender = tx.clone();
self.pool.execute(move || {
let result = f();
sender.send(result).unwrap();