feat: roblox sync config script

This commit is contained in:
daimond113 2024-07-22 16:41:45 +02:00
parent 10ca24a0cc
commit d81f2350df
No known key found for this signature in database
GPG key ID: 3A8ECE51328B513C
22 changed files with 940 additions and 291 deletions

175
Cargo.lock generated
View file

@ -227,9 +227,9 @@ dependencies = [
[[package]] [[package]]
name = "async-signal" name = "async-signal"
version = "0.2.8" version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "794f185324c2f00e771cd9f1ae8b5ac68be2ca7abb129a87afd6e86d228bc54d" checksum = "dfb3634b73397aa844481f814fad23bbf07fdb0eabec10f2eb95e58944b1ec32"
dependencies = [ dependencies = [
"async-io", "async-io",
"async-lock", "async-lock",
@ -370,9 +370,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.6.0" version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952"
[[package]] [[package]]
name = "bzip2" name = "bzip2"
@ -406,13 +406,12 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.1.1" version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "907d8581360765417f8f2e0e7d602733bbed60156b4465b7617243689ef9b83d" checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f"
dependencies = [ dependencies = [
"jobserver", "jobserver",
"libc", "libc",
"once_cell",
] ]
[[package]] [[package]]
@ -597,25 +596,6 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "crossbeam-deque"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.20" version = "0.8.20"
@ -723,9 +703,9 @@ dependencies = [
[[package]] [[package]]
name = "deflate64" name = "deflate64"
version = "0.1.8" version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83ace6c86376be0b6cdcf3fb41882e81d94b31587573d1cfa9d01cd06bba210d" checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b"
[[package]] [[package]]
name = "deranged" name = "deranged"
@ -1152,6 +1132,7 @@ dependencies = [
"gix-utils", "gix-utils",
"gix-validate", "gix-validate",
"gix-worktree", "gix-worktree",
"gix-worktree-state",
"once_cell", "once_cell",
"parking_lot", "parking_lot",
"regex", "regex",
@ -1172,7 +1153,7 @@ dependencies = [
"itoa", "itoa",
"serde", "serde",
"thiserror", "thiserror",
"winnow 0.6.13", "winnow 0.6.14",
] ]
[[package]] [[package]]
@ -1256,7 +1237,7 @@ dependencies = [
"smallvec", "smallvec",
"thiserror", "thiserror",
"unicode-bom", "unicode-bom",
"winnow 0.6.13", "winnow 0.6.14",
] ]
[[package]] [[package]]
@ -1529,7 +1510,7 @@ dependencies = [
"serde", "serde",
"smallvec", "smallvec",
"thiserror", "thiserror",
"winnow 0.6.13", "winnow 0.6.14",
] ]
[[package]] [[package]]
@ -1600,9 +1581,9 @@ dependencies = [
[[package]] [[package]]
name = "gix-path" name = "gix-path"
version = "0.10.8" version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca987128ffb056d732bd545db5db3d8b103d252fbf083c2567bb0796876619a4" checksum = "8d23d5bbda31344d8abc8de7c075b3cf26e5873feba7c4a15d916bce67382bd9"
dependencies = [ dependencies = [
"bstr", "bstr",
"gix-trace", "gix-trace",
@ -1655,7 +1636,7 @@ dependencies = [
"maybe-async", "maybe-async",
"serde", "serde",
"thiserror", "thiserror",
"winnow 0.6.13", "winnow 0.6.14",
] ]
[[package]] [[package]]
@ -1689,7 +1670,7 @@ dependencies = [
"memmap2", "memmap2",
"serde", "serde",
"thiserror", "thiserror",
"winnow 0.6.13", "winnow 0.6.14",
] ]
[[package]] [[package]]
@ -1878,16 +1859,23 @@ dependencies = [
] ]
[[package]] [[package]]
name = "globset" name = "gix-worktree-state"
version = "0.4.14" version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" checksum = "e64b2835892ce553b15aef7f6f7bb1e39e146fdf71eb99609b86710a7786cf34"
dependencies = [ dependencies = [
"aho-corasick",
"bstr", "bstr",
"log", "gix-features",
"regex-automata", "gix-filter",
"regex-syntax", "gix-fs",
"gix-glob",
"gix-hash",
"gix-index",
"gix-object",
"gix-path",
"gix-worktree",
"io-close",
"thiserror",
] ]
[[package]] [[package]]
@ -2119,22 +2107,6 @@ dependencies = [
"unicode-normalization", "unicode-normalization",
] ]
[[package]]
name = "ignore"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1"
dependencies = [
"crossbeam-deque",
"globset",
"log",
"memchr",
"regex-automata",
"same-file",
"walkdir",
"winapi-util",
]
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.9.3" version = "1.9.3"
@ -2216,6 +2188,16 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "io-close"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cadcf447f06744f8ce713d2d6239bb5bde2c357a452397a9ed90c625da390bc"
dependencies = [
"libc",
"winapi",
]
[[package]] [[package]]
name = "ipnet" name = "ipnet"
version = "2.9.0" version = "2.9.0"
@ -2284,9 +2266,9 @@ dependencies = [
[[package]] [[package]]
name = "keyring" name = "keyring"
version = "3.0.1" version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f32930aef50aff920e88e6cf66b70a6d587a6f2bae6aeade5c1e583661a40f8e" checksum = "9961b98f55dc0b2737000132505bdafa249abab147ee9de43c50ae04a054aa6c"
dependencies = [ dependencies = [
"byteorder", "byteorder",
"dbus-secret-service", "dbus-secret-service",
@ -2728,7 +2710,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"redox_syscall 0.5.2", "redox_syscall 0.5.3",
"smallvec", "smallvec",
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
@ -2774,7 +2756,6 @@ dependencies = [
"flate2", "flate2",
"full_moon", "full_moon",
"gix", "gix",
"ignore",
"indicatif", "indicatif",
"indicatif-log-bridge", "indicatif-log-bridge",
"inquire", "inquire",
@ -2787,7 +2768,6 @@ dependencies = [
"pretty_env_logger", "pretty_env_logger",
"relative-path", "relative-path",
"reqwest", "reqwest",
"secrecy",
"semver", "semver",
"serde", "serde",
"serde_json", "serde_json",
@ -2867,9 +2847,9 @@ dependencies = [
[[package]] [[package]]
name = "portable-atomic" name = "portable-atomic"
version = "1.6.0" version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265"
[[package]] [[package]]
name = "powerfmt" name = "powerfmt"
@ -3014,9 +2994,9 @@ dependencies = [
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.2" version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
] ]
@ -3228,15 +3208,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "secrecy"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e"
dependencies = [
"zeroize",
]
[[package]] [[package]]
name = "secret-service" name = "secret-service"
version = "4.0.0" version = "4.0.0"
@ -3258,9 +3229,9 @@ dependencies = [
[[package]] [[package]]
name = "security-framework" name = "security-framework"
version = "2.11.0" version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"core-foundation", "core-foundation",
@ -3271,9 +3242,9 @@ dependencies = [
[[package]] [[package]]
name = "security-framework-sys" name = "security-framework-sys"
version = "2.11.0" version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf"
dependencies = [ dependencies = [
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@ -3353,9 +3324,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_with" name = "serde_with"
version = "3.8.3" version = "3.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e73139bc5ec2d45e6c5fd85be5a46949c1c39a4c18e56915f5eb4c12f975e377" checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857"
dependencies = [ dependencies = [
"base64", "base64",
"chrono", "chrono",
@ -3371,9 +3342,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_with_macros" name = "serde_with_macros"
version = "3.8.3" version = "3.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b80d3d6b56b64335c0180e5ffde23b3c5e08c14c585b51a15bd0e95393f46703" checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350"
dependencies = [ dependencies = [
"darling", "darling",
"proc-macro2", "proc-macro2",
@ -3407,9 +3378,9 @@ dependencies = [
[[package]] [[package]]
name = "sha1_smol" name = "sha1_smol"
version = "1.0.0" version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
[[package]] [[package]]
name = "sha2" name = "sha2"
@ -3608,18 +3579,18 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.62" version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.62" version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3695,9 +3666,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.38.0" version = "1.38.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
@ -3734,14 +3705,14 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.8.14" version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" checksum = "ac2caab0bf757388c6c0ae23b3293fdb463fee59434529014f85e3263b995c28"
dependencies = [ dependencies = [
"serde", "serde",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime",
"toml_edit 0.22.15", "toml_edit 0.22.16",
] ]
[[package]] [[package]]
@ -3766,15 +3737,15 @@ dependencies = [
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.22.15" version = "0.22.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d59a3a72298453f564e2b111fa896f8d07fabb36f51f06d7e875fc5e0b5a3ef1" checksum = "278f3d518e152219c994ce877758516bca5e118eaed6996192a774fb9fbf0788"
dependencies = [ dependencies = [
"indexmap 2.2.6", "indexmap 2.2.6",
"serde", "serde",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime",
"winnow 0.6.13", "winnow 0.6.14",
] ]
[[package]] [[package]]
@ -4253,9 +4224,9 @@ dependencies = [
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.6.13" version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" checksum = "374ec40a2d767a3c1b4972d9475ecd557356637be906f2cb3f7fe17a6eb5e22f"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@ -4389,9 +4360,9 @@ dependencies = [
[[package]] [[package]]
name = "zip" name = "zip"
version = "2.1.3" version = "2.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "775a2b471036342aa69bc5a602bc889cb0a06cda00477d0c69566757d5553d39" checksum = "b895748a3ebcb69b9d38dcfdf21760859a4b0d0b0015277640c2ef4c69640e6f"
dependencies = [ dependencies = [
"aes", "aes",
"arbitrary", "arbitrary",

View file

@ -10,7 +10,7 @@ repository = "https://github.com/daimond113/pesde"
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", "ignore", "pretty_env_logger", "reqwest/json", "reqwest/multipart", "indicatif", "indicatif-log-bridge", "inquire", "nondestructive", "colored", "anyhow", "keyring", "open"] bin = ["clap", "directories", "pretty_env_logger", "reqwest/json", "reqwest/multipart", "indicatif", "indicatif-log-bridge", "inquire", "nondestructive", "colored", "anyhow", "keyring", "open", "gix/worktree-mutation"]
wally-compat = ["toml", "zip"] wally-compat = ["toml", "zip"]
roblox = [] roblox = []
lune = [] lune = []
@ -21,11 +21,14 @@ name = "pesde"
path = "src/main.rs" path = "src/main.rs"
required-features = ["bin"] required-features = ["bin"]
[lints.clippy]
uninlined_format_args = "warn"
[dependencies] [dependencies]
serde = { version = "1.0.204", features = ["derive"] } serde = { version = "1.0.204", features = ["derive"] }
serde_yaml = "0.9.34" serde_yaml = "0.9.34"
serde_json = "1.0.120" serde_json = "1.0.120"
serde_with = "3.8.3" serde_with = "3.9.0"
gix = { version = "0.63.0", default-features = false, features = ["blocking-http-transport-reqwest-rust-tls", "revparse-regex", "credentials", "serde"] } gix = { version = "0.63.0", default-features = false, features = ["blocking-http-transport-reqwest-rust-tls", "revparse-regex", "credentials", "serde"] }
semver = { version = "1.0.23", features = ["serde"] } semver = { version = "1.0.23", features = ["serde"] }
reqwest = { version = "0.12.5", default-features = false, features = ["rustls-tls", "blocking"] } reqwest = { version = "0.12.5", default-features = false, features = ["rustls-tls", "blocking"] }
@ -34,26 +37,26 @@ flate2 = "1.0.30"
pathdiff = "0.2.1" pathdiff = "0.2.1"
relative-path = { version = "1.9.3", features = ["serde"] } relative-path = { version = "1.9.3", features = ["serde"] }
log = "0.4.22" log = "0.4.22"
thiserror = "1.0.62" thiserror = "1.0.63"
threadpool = "1.8.1" threadpool = "1.8.1"
full_moon = { version = "1.0.0-rc.5", features = ["luau"] } full_moon = { version = "1.0.0-rc.5", features = ["luau"] }
url = { version = "2.5.2", features = ["serde"] } url = { version = "2.5.2", features = ["serde"] }
cfg-if = "1.0.0" cfg-if = "1.0.0"
once_cell = "1.19.0" once_cell = "1.19.0"
secrecy = "0.8.0" # TODO: reevaluate whether to use this
# secrecy = "0.8.0"
chrono = { version = "0.4.38", features = ["serde"] } chrono = { version = "0.4.38", features = ["serde"] }
toml = { version = "0.8.14", optional = true } toml = { version = "0.8.15", optional = true }
zip = { version = "2.1.3", optional = true } zip = { version = "2.1.5", optional = true }
anyhow = { version = "1.0.86", optional = true } anyhow = { version = "1.0.86", optional = true }
open = { version = "5.3.0", optional = true } open = { version = "5.3.0", optional = true }
keyring = { version = "3.0.1", features = ["crypto-rust", "windows-native", "apple-native", "linux-native"], optional = true } keyring = { version = "3.0.3", features = ["crypto-rust", "windows-native", "apple-native", "linux-native"], optional = true }
colored = { version = "2.1.0", optional = true } colored = { version = "2.1.0", optional = true }
nondestructive = { version = "0.0.25", optional = true } nondestructive = { version = "0.0.25", optional = true }
clap = { version = "4.5.9", features = ["derive"], optional = true } clap = { version = "4.5.9", features = ["derive"], optional = true }
directories = { version = "5.0.1", optional = true } directories = { version = "5.0.1", optional = true }
ignore = { version = "0.4.22", optional = true }
pretty_env_logger = { version = "0.5.0", optional = true } pretty_env_logger = { version = "0.5.0", optional = true }
indicatif = { version = "0.17.8", optional = true } indicatif = { version = "0.17.8", optional = true }
indicatif-log-bridge = { version = "0.2.2", optional = true } indicatif-log-bridge = { version = "0.2.2", optional = true }

View file

@ -15,6 +15,10 @@ pub struct LoginCommand {
/// The index to use. Defaults to `default`, or the configured default index if current directory doesn't have a manifest /// The index to use. Defaults to `default`, or the configured default index if current directory doesn't have a manifest
#[arg(short, long)] #[arg(short, long)]
index: Option<String>, index: Option<String>,
/// The token to use for authentication, skipping login
#[arg(short, long, conflicts_with = "index")]
token: Option<String>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -44,7 +48,11 @@ enum AccessTokenResponse {
} }
impl LoginCommand { impl LoginCommand {
pub fn run(self, project: Project) -> anyhow::Result<()> { pub fn authenticate_device_flow(
&self,
project: &Project,
reqwest: &reqwest::blocking::Client,
) -> anyhow::Result<String> {
let manifest = match project.deser_manifest() { let manifest = match project.deser_manifest() {
Ok(manifest) => Some(manifest), Ok(manifest) => Some(manifest),
Err(e) => match e { Err(e) => match e {
@ -82,18 +90,13 @@ impl LoginCommand {
.try_into() .try_into()
.context("cannot parse URL to git URL")?, .context("cannot parse URL to git URL")?,
); );
source source.refresh(project).context("failed to refresh index")?;
.refresh(&project)
.context("failed to refresh index")?;
dbg!(source.all_packages(&project).unwrap());
let config = source let config = source
.config(&project) .config(project)
.context("failed to read index config")?; .context("failed to read index config")?;
let client_id = config.github_oauth_client_id; let client_id = config.github_oauth_client_id;
let reqwest = reqwest_client(project.data_dir())?;
let response = reqwest let response = reqwest
.post(Url::parse_with_params( .post(Url::parse_with_params(
"https://github.com/login/device/code", "https://github.com/login/device/code",
@ -150,13 +153,7 @@ impl LoginCommand {
match response { match response {
AccessTokenResponse::Success { access_token } => { AccessTokenResponse::Success { access_token } => {
set_token(project.data_dir(), Some(&access_token))?; return Ok(access_token);
println!(
"logged in as {}",
get_token_login(&reqwest, &access_token)?.bold()
);
return Ok(());
} }
AccessTokenResponse::Error(e) => match e { AccessTokenResponse::Error(e) => match e {
AccessTokenError::AuthorizationPending => continue, AccessTokenError::AuthorizationPending => continue,
@ -178,4 +175,19 @@ impl LoginCommand {
anyhow::bail!("code expired, please re-run the login command"); anyhow::bail!("code expired, please re-run the login command");
} }
pub fn run(self, project: Project) -> anyhow::Result<()> {
let reqwest = reqwest_client(project.data_dir())?;
let token = match self.token {
Some(token) => token,
None => self.authenticate_device_flow(&project, &reqwest)?,
};
println!("logged in as {}", get_token_login(&reqwest, &token)?.bold());
set_token(project.data_dir(), Some(&token))?;
Ok(())
}
} }

View file

@ -27,7 +27,7 @@ impl DefaultIndexCommand {
Some(index) => { Some(index) => {
config.default_index = index.clone(); config.default_index = index.clone();
write_config(project.data_dir(), &config)?; write_config(project.data_dir(), &config)?;
println!("default index set to: {}", index); println!("default index set to: {index}");
} }
None => { None => {
println!("current default index: {}", config.default_index); println!("current default index: {}", config.default_index);

View file

@ -2,17 +2,22 @@ use clap::Subcommand;
use pesde::Project; use pesde::Project;
mod default_index; mod default_index;
mod scripts_repo;
#[derive(Debug, Subcommand)] #[derive(Debug, Subcommand)]
pub enum ConfigCommands { pub enum ConfigCommands {
/// Configuration for the default index /// Configuration for the default index
DefaultIndex(default_index::DefaultIndexCommand), DefaultIndex(default_index::DefaultIndexCommand),
/// Configuration for the scripts repository
ScriptsRepo(scripts_repo::ScriptsRepoCommand),
} }
impl ConfigCommands { impl ConfigCommands {
pub fn run(self, project: Project) -> anyhow::Result<()> { pub fn run(self, project: Project) -> anyhow::Result<()> {
match self { match self {
ConfigCommands::DefaultIndex(default_index) => default_index.run(project), ConfigCommands::DefaultIndex(default_index) => default_index.run(project),
ConfigCommands::ScriptsRepo(scripts_repo) => scripts_repo.run(project),
} }
} }
} }

View file

@ -0,0 +1,39 @@
use crate::cli::{read_config, write_config, CliConfig};
use clap::Args;
use pesde::Project;
#[derive(Debug, Args)]
pub struct ScriptsRepoCommand {
/// The new repo URL to set as default, don't pass any value to check the current default repo
#[arg(index = 1)]
repo: Option<url::Url>,
/// Resets the default repo to the default value
#[arg(short, long, conflicts_with = "repo")]
reset: bool,
}
impl ScriptsRepoCommand {
pub fn run(self, project: Project) -> anyhow::Result<()> {
let mut config = read_config(project.data_dir())?;
let repo = if self.reset {
Some(CliConfig::default().scripts_repo)
} else {
self.repo
};
match repo {
Some(repo) => {
config.scripts_repo = repo.clone();
write_config(project.data_dir(), &config)?;
println!("scripts repo set to: {repo}");
}
None => {
println!("current scripts repo: {}", config.scripts_repo);
}
}
Ok(())
}
}

View file

@ -1,13 +1,32 @@
use crate::cli::read_config; use crate::cli::read_config;
use anyhow::Context;
use clap::Args; use clap::Args;
use colored::Colorize; use colored::Colorize;
use inquire::validator::Validation; use inquire::validator::Validation;
use pesde::{errors::ManifestReadError, names::PackageName, Project, DEFAULT_INDEX_NAME}; use pesde::{
use std::str::FromStr; errors::ManifestReadError, manifest::ScriptName, names::PackageName, Project,
DEFAULT_INDEX_NAME,
};
use std::{path::Path, str::FromStr};
#[derive(Debug, Args)] #[derive(Debug, Args)]
pub struct InitCommand {} pub struct InitCommand {}
fn script_contents(path: &Path) -> String {
format!(
concat!(
r#"local process = require("@lune/process")
local home_dir = if process.os == "windows" then process.env.userprofile else process.env.HOME
require(home_dir .. ""#,
"/.",
env!("CARGO_PKG_NAME"),
r#"/scripts/{}")"#,
),
path.display()
)
}
impl InitCommand { impl InitCommand {
pub fn run(self, project: Project) -> anyhow::Result<()> { pub fn run(self, project: Project) -> anyhow::Result<()> {
match project.read_manifest() { match project.read_manifest() {
@ -94,6 +113,57 @@ impl InitCommand {
mapping.insert_str("license", license); mapping.insert_str("license", license);
} }
let target_env = inquire::Select::new(
"What environment are you targeting for your package?",
vec![
#[cfg(feature = "roblox")]
"roblox",
#[cfg(feature = "lune")]
"lune",
#[cfg(feature = "luau")]
"luau",
],
)
.prompt()
.unwrap();
let mut target = mapping
.insert("target", nondestructive::yaml::Separator::Auto)
.make_mapping();
target.insert_str("environment", target_env);
if target_env == "roblox"
|| inquire::Confirm::new(&format!(
"Would you like to setup a default {} script?",
ScriptName::RobloxSyncConfigGenerator
))
.prompt()
.unwrap()
{
let folder = project.path().join(concat!(".", env!("CARGO_PKG_NAME")));
std::fs::create_dir_all(&folder).context("failed to create scripts folder")?;
std::fs::write(
folder.join(format!("{}.luau", ScriptName::RobloxSyncConfigGenerator)),
script_contents(Path::new(&format!(
"lune/rojo/{}.luau",
ScriptName::RobloxSyncConfigGenerator
))),
)
.context("failed to write script file")?;
mapping
.insert("scripts", nondestructive::yaml::Separator::Auto)
.make_mapping()
.insert_str(
ScriptName::RobloxSyncConfigGenerator.to_string(),
format!(
concat!(concat!(".", env!("CARGO_PKG_NAME")), "/{}.luau"),
ScriptName::RobloxSyncConfigGenerator
),
);
}
let mut indices = mapping let mut indices = mapping
.insert("indices", nondestructive::yaml::Separator::Auto) .insert("indices", nondestructive::yaml::Separator::Auto)
.make_mapping(); .make_mapping();

View file

@ -1,18 +1,23 @@
use crate::git::authenticate_conn;
use anyhow::Context; use anyhow::Context;
use gix::remote::Direction;
use keyring::Entry; use keyring::Entry;
use pesde::Project; use pesde::Project;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::Path; use std::{collections::HashSet, path::Path};
mod auth; mod auth;
mod config; mod config;
mod init; mod init;
mod install; mod install;
mod publish;
mod run; mod run;
mod self_install;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CliConfig { pub struct CliConfig {
pub default_index: url::Url, pub default_index: url::Url,
pub scripts_repo: url::Url,
pub token: Option<String>, pub token: Option<String>,
} }
@ -20,6 +25,9 @@ impl Default for CliConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
default_index: "https://github.com/daimond113/pesde-index".parse().unwrap(), default_index: "https://github.com/daimond113/pesde-index".parse().unwrap(),
scripts_repo: "https://github.com/daimond113/pesde-scripts"
.parse()
.unwrap(),
token: None, token: None,
} }
} }
@ -102,7 +110,7 @@ pub fn reqwest_client(data_dir: &Path) -> anyhow::Result<reqwest::blocking::Clie
if let Some(token) = get_token(data_dir)? { if let Some(token) = get_token(data_dir)? {
headers.insert( headers.insert(
reqwest::header::AUTHORIZATION, reqwest::header::AUTHORIZATION,
format!("Bearer {}", token) format!("Bearer {token}")
.parse() .parse()
.context("failed to create auth header")?, .context("failed to create auth header")?,
); );
@ -125,6 +133,139 @@ pub fn reqwest_client(data_dir: &Path) -> anyhow::Result<reqwest::blocking::Clie
.build()?) .build()?)
} }
pub fn update_scripts_folder(project: &Project) -> anyhow::Result<()> {
let home_dir = directories::UserDirs::new()
.context("failed to get home directory")?
.home_dir()
.to_owned();
let scripts_dir = home_dir
.join(concat!(".", env!("CARGO_PKG_NAME")))
.join("scripts");
if scripts_dir.exists() {
let repo = gix::open(&scripts_dir).context("failed to open scripts repository")?;
let remote = repo
.find_default_remote(Direction::Fetch)
.context("missing default remote of scripts repository")?
.context("failed to find default remote of scripts repository")?;
let mut connection = remote
.connect(Direction::Fetch)
.context("failed to connect to default remote of scripts repository")?;
authenticate_conn(&mut connection, project.auth_config());
let results = connection
.prepare_fetch(gix::progress::Discard, Default::default())
.context("failed to prepare scripts repository fetch")?
.receive(gix::progress::Discard, &false.into())
.context("failed to receive new scripts repository contents")?;
let remote_ref = results
.ref_map
.remote_refs
.first()
.context("failed to get remote refs of scripts repository")?;
let unpacked = remote_ref.unpack();
let oid = unpacked
.1
.or(unpacked.2)
.context("couldn't find oid in remote ref")?;
let tree = repo
.find_object(oid)
.context("failed to find scripts repository tree")?
.peel_to_tree()
.context("failed to peel scripts repository object to tree")?;
let mut index = gix::index::File::from_state(
gix::index::State::from_tree(&tree.id, &repo.objects, Default::default())
.context("failed to create index state from scripts repository tree")?,
repo.index_path(),
);
let opts = gix::worktree::state::checkout::Options {
overwrite_existing: true,
destination_is_initially_empty: false,
..Default::default()
};
gix::worktree::state::checkout(
&mut index,
repo.work_dir().context("scripts repo is bare")?,
repo.objects
.clone()
.into_arc()
.context("failed to clone objects")?,
&gix::progress::Discard,
&gix::progress::Discard,
&false.into(),
opts,
)
.context("failed to checkout scripts repository")?;
index
.write(gix::index::write::Options::default())
.context("failed to write index")?;
} else {
std::fs::create_dir_all(&scripts_dir).context("failed to create scripts directory")?;
let cli_config = read_config(project.data_dir())?;
gix::prepare_clone(cli_config.scripts_repo.as_str(), &scripts_dir)
.context("failed to prepare scripts repository clone")?
.fetch_then_checkout(gix::progress::Discard, &false.into())
.context("failed to fetch and checkout scripts repository")?
.0
.main_worktree(gix::progress::Discard, &false.into())
.context("failed to set scripts repository as main worktree")?;
};
Ok(())
}
pub trait IsUpToDate {
fn is_up_to_date(&self) -> anyhow::Result<bool>;
}
impl IsUpToDate for Project {
fn is_up_to_date(&self) -> anyhow::Result<bool> {
let manifest = self.deser_manifest()?;
let lockfile = match self.deser_lockfile() {
Ok(lockfile) => lockfile,
Err(pesde::errors::LockfileReadError::Io(e))
if e.kind() == std::io::ErrorKind::NotFound =>
{
return Ok(false);
}
Err(e) => return Err(e.into()),
};
if manifest.name != lockfile.name || manifest.version != lockfile.version {
return Ok(false);
}
let specs = lockfile
.graph
.into_iter()
.flat_map(|(_, versions)| versions)
.filter_map(|(_, node)| match node.node.direct {
Some((_, spec)) => Some((spec, node.node.ty)),
None => None,
})
.collect::<HashSet<_>>();
Ok(!manifest
.all_dependencies()
.context("failed to get all dependencies")?
.iter()
.all(|(_, (spec, ty))| specs.contains(&(spec.clone(), *ty))))
}
}
#[derive(Debug, clap::Subcommand)] #[derive(Debug, clap::Subcommand)]
pub enum Subcommand { pub enum Subcommand {
/// Authentication-related commands /// Authentication-related commands
@ -143,6 +284,12 @@ pub enum Subcommand {
/// Installs all dependencies for the project /// Installs all dependencies for the project
Install(install::InstallCommand), Install(install::InstallCommand),
/// Publishes the project to the registry
Publish(publish::PublishCommand),
/// Installs the pesde binary and scripts
SelfInstall(self_install::SelfInstallCommand),
} }
impl Subcommand { impl Subcommand {
@ -153,6 +300,8 @@ impl Subcommand {
Subcommand::Init(init) => init.run(project), Subcommand::Init(init) => init.run(project),
Subcommand::Run(run) => run.run(project), Subcommand::Run(run) => run.run(project),
Subcommand::Install(install) => install.run(project), Subcommand::Install(install) => install.run(project),
Subcommand::Publish(publish) => publish.run(project),
Subcommand::SelfInstall(self_install) => self_install.run(project),
} }
} }
} }

289
src/cli/publish.rs Normal file
View file

@ -0,0 +1,289 @@
use anyhow::Context;
use clap::Args;
use colored::Colorize;
use pesde::{manifest::Target, Project, MANIFEST_FILE_NAME, MAX_ARCHIVE_SIZE};
use std::{io::Seek, path::Component};
#[derive(Debug, Args)]
pub struct PublishCommand {
/// Whether to output a tarball instead of publishing
#[arg(short, long)]
dry_run: bool,
}
impl PublishCommand {
pub fn run(self, project: Project) -> anyhow::Result<()> {
let mut manifest = project
.deser_manifest()
.context("failed to read manifest")?;
if manifest.private {
println!("{}", "package is private, cannot publish".red().bold());
return Ok(());
}
manifest
.target
.validate_publish()
.context("manifest not fit for publishing")?;
let mut archive = tar::Builder::new(flate2::write::GzEncoder::new(
vec![],
flate2::Compression::best(),
));
let mut display_includes: Vec<String> = vec![MANIFEST_FILE_NAME.to_string()];
#[cfg(feature = "roblox")]
let mut display_build_files: Vec<String> = vec![];
let (lib_path, bin_path) = (
manifest.target.lib_path().cloned(),
manifest.target.bin_path().cloned(),
);
#[cfg(feature = "roblox")]
let mut roblox_target = match &mut manifest.target {
Target::Roblox { build_files, .. } => Some(build_files),
_ => None,
};
#[cfg(not(feature = "roblox"))]
let roblox_target = None::<()>;
if !manifest.includes.insert(MANIFEST_FILE_NAME.to_string()) {
display_includes.push(MANIFEST_FILE_NAME.to_string());
println!(
"{}: {MANIFEST_FILE_NAME} was not in includes, adding it",
"warn".yellow().bold()
);
}
for (name, path) in [("lib path", lib_path), ("bin path", bin_path)] {
let Some(export_path) = path else { continue };
let export_path = export_path.to_path(project.path());
if !export_path.exists() {
anyhow::bail!("{name} points to non-existent file");
}
if !export_path.is_file() {
anyhow::bail!("{name} must point to a file");
}
let contents =
std::fs::read_to_string(&export_path).context(format!("failed to read {name}"))?;
if let Err(err) = full_moon::parse(&contents).map_err(|errs| {
errs.into_iter()
.map(|err| err.to_string())
.collect::<Vec<_>>()
.join(", ")
}) {
anyhow::bail!("{name} is not a valid Luau file: {err}");
}
let first_part = export_path
.strip_prefix(project.path())
.context(format!("{name} not within project directory"))?
.components()
.next()
.context(format!("{name} must contain at least one part"))?;
let first_part = match first_part {
Component::Normal(part) => part,
_ => anyhow::bail!("{name} must be within project directory"),
};
let first_part_str = first_part.to_string_lossy();
if manifest.includes.insert(first_part_str.to_string()) {
println!(
"{}: {name} was not in includes, adding {first_part_str}",
"warn".yellow().bold()
);
}
if roblox_target.as_mut().map_or(false, |build_files| {
build_files.insert(first_part_str.to_string())
}) {
println!(
"{}: {name} was not in build files, adding {first_part_str}",
"warn".yellow().bold()
);
}
}
for included_name in &manifest.includes {
let included_path = project.path().join(included_name);
if !included_path.exists() {
anyhow::bail!("included file {included_name} does not exist");
}
// it'll be included later, with our mut modifications
if included_name.eq_ignore_ascii_case(MANIFEST_FILE_NAME) {
continue;
}
if included_path.is_file() {
display_includes.push(included_name.clone());
archive.append_file(
included_name,
&mut std::fs::File::open(&included_path)
.context(format!("failed to read {included_name}"))?,
)?;
} else {
display_includes.push(format!("{included_name}/*"));
archive
.append_dir_all(included_name, &included_path)
.context(format!("failed to include directory {included_name}"))?;
}
}
if let Some(build_files) = &roblox_target {
for build_file in build_files.iter() {
if build_file.eq_ignore_ascii_case(MANIFEST_FILE_NAME) {
println!(
"{}: {MANIFEST_FILE_NAME} is in build files, please remove it",
"warn".yellow().bold()
);
continue;
}
let build_file_path = project.path().join(build_file);
if !build_file_path.exists() {
anyhow::bail!("build file {build_file} does not exist");
}
if !manifest.includes.contains(build_file) {
anyhow::bail!("build file {build_file} is not in includes, please add it");
}
if build_file_path.is_file() {
display_build_files.push(build_file.clone());
} else {
display_build_files.push(format!("{build_file}/*"));
}
}
}
{
println!("\n{}", "please confirm the following information:".bold());
println!("name: {}", manifest.name);
println!("version: {}", manifest.version);
println!(
"description: {}",
manifest.description.as_deref().unwrap_or("(none)")
);
println!(
"license: {}",
manifest.license.as_deref().unwrap_or("(none)")
);
println!(
"authors: {}",
manifest
.authors
.as_ref()
.map_or("(none)".to_string(), |a| a.join(", "))
);
println!(
"repository: {}",
manifest.repository.as_deref().unwrap_or("(none)")
);
let roblox_target = roblox_target.is_some_and(|_| true);
println!("target: {}", manifest.target);
println!(
"\tlib path: {}",
manifest
.target
.lib_path()
.map_or("(none)".to_string(), |p| p.to_string())
);
match roblox_target {
#[cfg(feature = "roblox")]
true => {
println!("\tbuild files: {}", display_build_files.join(", "));
}
_ => {
println!(
"\tbin path: {}",
manifest
.target
.bin_path()
.map_or("(none)".to_string(), |p| p.to_string())
);
}
}
println!(
"includes: {}",
display_includes.into_iter().collect::<Vec<_>>().join(", ")
);
if !self.dry_run && !inquire::Confirm::new("is this information correct?").prompt()? {
println!("{}", "publish aborted".red().bold());
return Ok(());
}
}
let temp_manifest_path = project
.data_dir()
.join(format!("temp_manifest_{}", chrono::Utc::now().timestamp()));
let mut temp_manifest = std::fs::File::options()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(&temp_manifest_path)
.context("failed to create temp manifest file")?;
serde_yaml::to_writer(&mut temp_manifest, &manifest)
.context("failed to write temp manifest file")?;
temp_manifest
.rewind()
.context("failed to rewind temp manifest file")?;
archive.append_file(MANIFEST_FILE_NAME, &mut temp_manifest)?;
drop(temp_manifest);
std::fs::remove_file(temp_manifest_path)?;
let archive = archive
.into_inner()
.context("failed to encode archive")?
.finish()
.context("failed to get archive bytes")?;
if archive.len() > MAX_ARCHIVE_SIZE {
anyhow::bail!(
"archive size exceeds maximum size of {} bytes by {} bytes",
MAX_ARCHIVE_SIZE,
archive.len() - MAX_ARCHIVE_SIZE
);
}
if self.dry_run {
std::fs::write("package.tar.gz", archive)?;
println!(
"{}",
"(dry run) package written to package.tar.gz".green().bold()
);
return Ok(());
}
todo!("publishing to registry");
}
}

View file

@ -1,9 +1,10 @@
use crate::cli::IsUpToDate;
use anyhow::Context; use anyhow::Context;
use clap::Args; use clap::Args;
use pesde::{ use pesde::{
names::PackageName, names::{PackageName, PackageNames},
scripts::{execute_lune_script, execute_script}, scripts::execute_script,
Project, Project, PACKAGES_CONTAINER_NAME,
}; };
use relative_path::RelativePathBuf; use relative_path::RelativePathBuf;
@ -20,14 +21,59 @@ pub struct RunCommand {
impl RunCommand { impl RunCommand {
pub fn run(self, project: Project) -> anyhow::Result<()> { pub fn run(self, project: Project) -> anyhow::Result<()> {
if let Ok(_pkg_name) = self.package_or_script.parse::<PackageName>() { if let Ok(pkg_name) = self.package_or_script.parse::<PackageName>() {
todo!("implement binary package execution") let graph = if project.is_up_to_date()? {
project.deser_lockfile()?.graph
} else {
anyhow::bail!("outdated lockfile, please run the install command first")
};
let pkg_name = PackageNames::Pesde(pkg_name);
for (version, node) in graph.get(&pkg_name).context("package not found in graph")? {
if node.node.direct.is_none() {
continue;
}
let Some(bin_path) = node.target.bin_path() else {
anyhow::bail!("package has no bin path");
};
let base_folder = node
.node
.base_folder(project.deser_manifest()?.target.kind(), true);
let container_folder = node.node.container_folder(
&project
.path()
.join(base_folder)
.join(PACKAGES_CONTAINER_NAME),
&pkg_name,
version,
);
let path = bin_path.to_path(&container_folder);
execute_script(
Some(pkg_name.as_str().1),
&path,
&self.args,
project.path(),
false,
)
.context("failed to execute script")?;
}
} }
if let Ok(manifest) = project.deser_manifest() { if let Ok(manifest) = project.deser_manifest() {
if manifest.scripts.contains_key(&self.package_or_script) { if let Some(script_path) = manifest.scripts.get(&self.package_or_script) {
execute_script(&manifest, &self.package_or_script, &self.args) execute_script(
.context("failed to execute script")?; Some(&self.package_or_script),
&script_path.to_path(project.path()),
&self.args,
project.path(),
false,
)
.context("failed to execute script")?;
return Ok(()); return Ok(());
} }
@ -40,7 +86,7 @@ impl RunCommand {
anyhow::bail!("path does not exist: {}", path.display()); anyhow::bail!("path does not exist: {}", path.display());
} }
execute_lune_script(None, &relative_path, &self.args) execute_script(None, &path, &self.args, project.path(), false)
.context("failed to execute script")?; .context("failed to execute script")?;
Ok(()) Ok(())

14
src/cli/self_install.rs Normal file
View file

@ -0,0 +1,14 @@
use crate::cli::update_scripts_folder;
use clap::Args;
use pesde::Project;
#[derive(Debug, Args)]
pub struct SelfInstallCommand {}
impl SelfInstallCommand {
pub fn run(self, project: Project) -> anyhow::Result<()> {
update_scripts_folder(&project)?;
Ok(())
}
}

View file

@ -10,6 +10,7 @@ use crate::{
}; };
impl Project { impl Project {
// TODO: use threadpool for concurrent downloads
pub fn download_graph( pub fn download_graph(
&self, &self,
graph: &DependencyGraph, graph: &DependencyGraph,

23
src/git.rs Normal file
View file

@ -0,0 +1,23 @@
use crate::AuthConfig;
pub fn authenticate_conn(
conn: &mut gix::remote::Connection<
'_,
'_,
Box<dyn gix::protocol::transport::client::Transport + Send>,
>,
auth_config: &AuthConfig,
) {
if let Some(iden) = auth_config.git_credentials().cloned() {
conn.set_credentials(move |action| match action {
gix::credentials::helper::Action::Get(ctx) => {
Ok(Some(gix::credentials::protocol::Outcome {
identity: iden.clone(),
next: gix::credentials::helper::NextAction::from(ctx),
}))
}
gix::credentials::helper::Action::Store(_) => Ok(None),
gix::credentials::helper::Action::Erase(_) => Ok(None),
});
}
}

View file

@ -8,6 +8,7 @@ use once_cell::sync::Lazy;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
pub mod download; pub mod download;
mod git;
pub mod linking; pub mod linking;
pub mod lockfile; pub mod lockfile;
pub mod manifest; pub mod manifest;
@ -20,6 +21,7 @@ pub const MANIFEST_FILE_NAME: &str = "pesde.yaml";
pub const LOCKFILE_FILE_NAME: &str = "pesde.lock"; pub const LOCKFILE_FILE_NAME: &str = "pesde.lock";
pub const DEFAULT_INDEX_NAME: &str = "default"; pub const DEFAULT_INDEX_NAME: &str = "default";
pub const PACKAGES_CONTAINER_NAME: &str = ".pesde"; pub const PACKAGES_CONTAINER_NAME: &str = ".pesde";
pub const MAX_ARCHIVE_SIZE: usize = 4 * 1024 * 1024;
pub(crate) static REQWEST_CLIENT: Lazy<reqwest::blocking::Client> = Lazy::new(|| { pub(crate) static REQWEST_CLIENT: Lazy<reqwest::blocking::Client> = Lazy::new(|| {
reqwest::blocking::Client::builder() reqwest::blocking::Client::builder()
@ -32,43 +34,10 @@ pub(crate) static REQWEST_CLIENT: Lazy<reqwest::blocking::Client> = Lazy::new(||
.expect("failed to create reqwest client") .expect("failed to create reqwest client")
}); });
#[derive(Debug, Clone)]
pub struct GitAccount {
username: String,
password: secrecy::SecretString,
}
impl GitAccount {
pub fn new<S: Into<secrecy::SecretString>>(username: String, password: S) -> Self {
GitAccount {
username,
password: password.into(),
}
}
pub fn as_account(&self) -> gix::sec::identity::Account {
use secrecy::ExposeSecret;
gix::sec::identity::Account {
username: self.username.clone(),
password: self.password.expose_secret().to_string(),
}
}
}
impl From<gix::sec::identity::Account> for GitAccount {
fn from(account: gix::sec::identity::Account) -> Self {
GitAccount {
username: account.username,
password: account.password.into(),
}
}
}
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct AuthConfig { pub struct AuthConfig {
pesde_token: Option<secrecy::SecretString>, pesde_token: Option<String>,
git_credentials: Option<GitAccount>, git_credentials: Option<gix::sec::identity::Account>,
} }
impl AuthConfig { impl AuthConfig {
@ -76,39 +45,28 @@ impl AuthConfig {
AuthConfig::default() AuthConfig::default()
} }
pub fn with_pesde_token<S: Into<secrecy::SecretString>>(mut self, token: Option<S>) -> Self { pub fn pesde_token(&self) -> Option<&str> {
self.pesde_token = token.map(Into::into); self.pesde_token.as_deref()
}
pub fn git_credentials(&self) -> Option<&gix::sec::identity::Account> {
self.git_credentials.as_ref()
}
pub fn with_pesde_token<S: AsRef<str>>(mut self, token: Option<S>) -> Self {
self.pesde_token = token.map(|s| s.as_ref().to_string());
self self
} }
pub fn with_git_credentials(mut self, git_credentials: Option<GitAccount>) -> Self { pub fn with_git_credentials(
mut self,
git_credentials: Option<gix::sec::identity::Account>,
) -> Self {
self.git_credentials = git_credentials; self.git_credentials = git_credentials;
self self
} }
} }
pub(crate) fn authenticate_conn(
conn: &mut gix::remote::Connection<
'_,
'_,
Box<dyn gix::protocol::transport::client::Transport + Send>,
>,
auth_config: AuthConfig,
) {
if let Some(iden) = auth_config.git_credentials {
conn.set_credentials(move |action| match action {
gix::credentials::helper::Action::Get(ctx) => {
Ok(Some(gix::credentials::protocol::Outcome {
identity: iden.as_account(),
next: gix::credentials::helper::NextAction::from(ctx),
}))
}
gix::credentials::helper::Action::Store(_) => Ok(None),
gix::credentials::helper::Action::Erase(_) => Ok(None),
});
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct Project { pub struct Project {
path: PathBuf, path: PathBuf,
@ -137,6 +95,10 @@ impl Project {
&self.data_dir &self.data_dir
} }
pub fn auth_config(&self) -> &AuthConfig {
&self.auth_config
}
pub fn read_manifest(&self) -> Result<Vec<u8>, errors::ManifestReadError> { pub fn read_manifest(&self) -> Result<Vec<u8>, errors::ManifestReadError> {
let bytes = std::fs::read(self.path.join(MANIFEST_FILE_NAME))?; let bytes = std::fs::read(self.path.join(MANIFEST_FILE_NAME))?;
Ok(bytes) Ok(bytes)

View file

@ -1,6 +1,11 @@
use crate::{ use crate::{
linking::generator::get_file_types, lockfile::DownloadedGraph, manifest::Manifest, linking::generator::get_file_types,
names::PackageNames, source::PackageRef, Project, MANIFEST_FILE_NAME, PACKAGES_CONTAINER_NAME, lockfile::DownloadedGraph,
manifest::{Manifest, ScriptName, Target},
names::PackageNames,
scripts::execute_script,
source::PackageRef,
Project, MANIFEST_FILE_NAME, PACKAGES_CONTAINER_NAME,
}; };
use semver::Version; use semver::Version;
use std::{collections::BTreeMap, fs::create_dir_all}; use std::{collections::BTreeMap, fs::create_dir_all};
@ -34,7 +39,7 @@ impl Project {
version, version,
); );
let lib_file = lib_file.to_path(container_folder); let lib_file = lib_file.to_path(&container_folder);
let contents = match std::fs::read_to_string(&lib_file) { let contents = match std::fs::read_to_string(&lib_file) {
Ok(contents) => contents, Ok(contents) => contents,
@ -60,6 +65,30 @@ impl Project {
.entry(name) .entry(name)
.or_default() .or_default()
.insert(version, types); .insert(version, types);
#[cfg(feature = "roblox")]
if let Target::Roblox { build_files, .. } = &node.target {
let script_name = ScriptName::RobloxSyncConfigGenerator.to_string();
let Some(script_path) = manifest.scripts.get(&script_name) else {
log::warn!("not having a `{script_name}` script in the manifest might cause issues with Roblox linking");
continue;
};
execute_script(
Some(&script_name),
&script_path.to_path(self.path()),
build_files,
&container_folder,
false,
)
.map_err(|e| {
errors::LinkingError::GenerateRobloxSyncConfig(
container_folder.display().to_string(),
e,
)
})?;
}
} }
} }
@ -173,5 +202,9 @@ pub mod errors {
#[error("error generating require path")] #[error("error generating require path")]
GetRequirePath(#[from] crate::linking::generator::errors::GetRequirePathError), GetRequirePath(#[from] crate::linking::generator::errors::GetRequirePathError),
#[cfg(feature = "roblox")]
#[error("error generating roblox sync config for {0}")]
GenerateRobloxSyncConfig(String, #[source] std::io::Error),
} }
} }

View file

@ -91,5 +91,5 @@ pub struct Lockfile {
pub version: Version, pub version: Version,
pub overrides: BTreeMap<OverrideKey, DependencySpecifiers>, pub overrides: BTreeMap<OverrideKey, DependencySpecifiers>,
pub graph: DependencyGraph, pub graph: DownloadedGraph,
} }

View file

@ -5,6 +5,7 @@ use pesde::{AuthConfig, Project};
use std::fs::create_dir_all; use std::fs::create_dir_all;
mod cli; mod cli;
pub mod git;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[clap(version, about = "pesde is a feature-rich package manager for Luau")] #[clap(version, about = "pesde is a feature-rich package manager for Luau")]

View file

@ -1,15 +1,14 @@
use crate::{names::PackageName, source::DependencySpecifiers};
use relative_path::RelativePathBuf; use relative_path::RelativePathBuf;
use semver::Version; use semver::Version;
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Serialize};
use serde_with::{DeserializeFromStr, SerializeDisplay}; use serde_with::{DeserializeFromStr, SerializeDisplay};
use std::{ use std::{
collections::BTreeMap, collections::{BTreeMap, BTreeSet},
fmt::{Display, Formatter}, fmt::{Display, Formatter},
str::FromStr, str::FromStr,
}; };
use crate::{names::PackageName, source::DependencySpecifiers};
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[serde(rename_all = "snake_case", deny_unknown_fields)] #[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum TargetKind { pub enum TargetKind {
@ -54,23 +53,32 @@ impl TargetKind {
return "packages".to_string(); return "packages".to_string();
} }
format!("{}_packages", dependency) format!("{dependency}_packages")
} }
} }
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[serde(rename_all = "snake_case", tag = "environment", remote = "Self")] #[serde(rename_all = "snake_case", tag = "environment")]
pub enum Target { pub enum Target {
#[cfg(feature = "roblox")] #[cfg(feature = "roblox")]
Roblox { lib: RelativePathBuf }, Roblox {
#[serde(default)]
lib: Option<RelativePathBuf>,
#[serde(default)]
build_files: BTreeSet<String>,
},
#[cfg(feature = "lune")] #[cfg(feature = "lune")]
Lune { Lune {
#[serde(default)]
lib: Option<RelativePathBuf>, lib: Option<RelativePathBuf>,
#[serde(default)]
bin: Option<RelativePathBuf>, bin: Option<RelativePathBuf>,
}, },
#[cfg(feature = "luau")] #[cfg(feature = "luau")]
Luau { Luau {
#[serde(default)]
lib: Option<RelativePathBuf>, lib: Option<RelativePathBuf>,
#[serde(default)]
bin: Option<RelativePathBuf>, bin: Option<RelativePathBuf>,
}, },
} }
@ -90,55 +98,47 @@ impl Target {
pub fn lib_path(&self) -> Option<&RelativePathBuf> { pub fn lib_path(&self) -> Option<&RelativePathBuf> {
match self { match self {
#[cfg(feature = "roblox")] #[cfg(feature = "roblox")]
Target::Roblox { lib } => Some(lib), Target::Roblox { lib, .. } => lib.as_ref(),
#[cfg(feature = "lune")] #[cfg(feature = "lune")]
Target::Lune { lib, .. } => lib.as_ref(), Target::Lune { lib, .. } => lib.as_ref(),
#[cfg(feature = "luau")] #[cfg(feature = "luau")]
Target::Luau { lib, .. } => lib.as_ref(), Target::Luau { lib, .. } => lib.as_ref(),
} }
} }
}
impl Serialize for Target { pub fn bin_path(&self) -> Option<&RelativePathBuf> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> match self {
where #[cfg(feature = "roblox")]
S: Serializer, Target::Roblox { .. } => None,
{
Self::serialize(self, serializer)
}
}
impl<'de> Deserialize<'de> for Target {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let target = Self::deserialize(deserializer)?;
match &target {
#[cfg(feature = "lune")] #[cfg(feature = "lune")]
Target::Lune { lib, bin } => { Target::Lune { bin, .. } => bin.as_ref(),
if lib.is_none() && bin.is_none() {
return Err(serde::de::Error::custom(
"one of `lib` or `bin` exports must be defined",
));
}
}
#[cfg(feature = "luau")] #[cfg(feature = "luau")]
Target::Luau { lib, bin } => { Target::Luau { bin, .. } => bin.as_ref(),
if lib.is_none() && bin.is_none() { }
return Err(serde::de::Error::custom( }
"one of `lib` or `bin` exports must be defined",
));
}
}
#[allow(unreachable_patterns)] pub fn validate_publish(&self) -> Result<(), errors::TargetValidatePublishError> {
_ => {} let has_exports = match self {
#[cfg(feature = "roblox")]
Target::Roblox { lib, .. } => lib.is_some(),
#[cfg(feature = "lune")]
Target::Lune { lib, bin } => lib.is_some() || bin.is_some(),
#[cfg(feature = "luau")]
Target::Luau { lib, bin } => lib.is_some() || bin.is_some(),
}; };
Ok(target) if !has_exports {
return Err(errors::TargetValidatePublishError::NoExportedFiles);
}
match self {
#[cfg(feature = "roblox")]
Target::Roblox { build_files, .. } if build_files.is_empty() => {
Err(errors::TargetValidatePublishError::NoBuildFiles)
}
_ => Ok(()),
}
} }
} }
@ -190,39 +190,57 @@ impl Display for OverrideKey {
} }
} }
#[derive(Deserialize, Debug, Clone)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum ScriptName {
#[cfg(feature = "roblox")]
RobloxSyncConfigGenerator,
#[cfg(all(feature = "wally-compat", feature = "roblox"))]
SourcemapGenerator,
}
impl Display for ScriptName {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
#[cfg(feature = "roblox")]
ScriptName::RobloxSyncConfigGenerator => write!(f, "roblox_sync_config_generator"),
#[cfg(all(feature = "wally-compat", feature = "roblox"))]
ScriptName::SourcemapGenerator => write!(f, "sourcemap_generator"),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Manifest { pub struct Manifest {
pub name: PackageName, pub name: PackageName,
pub version: Version, pub version: Version,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>, pub description: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub license: Option<String>, pub license: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub authors: Option<Vec<String>>, pub authors: Option<Vec<String>>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub repository: Option<String>, pub repository: Option<String>,
pub target: Target, pub target: Target,
#[serde(default)] #[serde(default)]
pub private: bool, pub private: bool,
#[serde(default)] #[serde(default, skip_serializing)]
pub scripts: BTreeMap<String, RelativePathBuf>, pub scripts: BTreeMap<String, RelativePathBuf>,
#[serde(default)] #[serde(default)]
pub indices: BTreeMap<String, url::Url>, pub indices: BTreeMap<String, url::Url>,
#[cfg(feature = "wally-compat")] #[cfg(feature = "wally-compat")]
#[serde(default)] #[serde(default)]
pub wally_indices: BTreeMap<String, url::Url>, pub wally_indices: BTreeMap<String, url::Url>,
#[cfg(all(feature = "wally-compat", feature = "roblox"))] #[serde(default, skip_serializing)]
#[serde(default)]
pub sourcemap_generator: Option<String>,
#[serde(default)]
pub overrides: BTreeMap<OverrideKey, DependencySpecifiers>, pub overrides: BTreeMap<OverrideKey, DependencySpecifiers>,
#[serde(default)]
pub includes: BTreeSet<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub dependencies: BTreeMap<String, DependencySpecifiers>, pub dependencies: BTreeMap<String, DependencySpecifiers>,
#[serde(default)] #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub peer_dependencies: BTreeMap<String, DependencySpecifiers>, pub peer_dependencies: BTreeMap<String, DependencySpecifiers>,
#[serde(default)] #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub dev_dependencies: BTreeMap<String, DependencySpecifiers>, pub dev_dependencies: BTreeMap<String, DependencySpecifiers>,
} }
@ -275,4 +293,15 @@ pub mod errors {
#[error("another specifier is already using the alias {0}")] #[error("another specifier is already using the alias {0}")]
AliasConflict(String), AliasConflict(String),
} }
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum TargetValidatePublishError {
#[error("no exported files specified")]
NoExportedFiles,
#[cfg(feature = "roblox")]
#[error("roblox target must have at least one build file")]
NoBuildFiles,
}
} }

View file

@ -91,7 +91,7 @@ impl PackageNames {
impl Display for PackageNames { impl Display for PackageNames {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
PackageNames::Pesde(name) => write!(f, "{}", name), PackageNames::Pesde(name) => write!(f, "{name}"),
} }
} }
} }

View file

@ -1,21 +1,23 @@
use crate::manifest::Manifest;
use relative_path::RelativePathBuf;
use std::{ use std::{
ffi::OsStr, ffi::OsStr,
io::{BufRead, BufReader}, io::{BufRead, BufReader},
path::Path,
process::{Command, Stdio}, process::{Command, Stdio},
thread::spawn, thread::spawn,
}; };
pub fn execute_lune_script<A: IntoIterator<Item = S>, S: AsRef<OsStr>>( pub fn execute_script<A: IntoIterator<Item = S>, S: AsRef<OsStr>, P: AsRef<Path>>(
script_name: Option<&str>, script_name: Option<&str>,
script_path: &RelativePathBuf, script_path: &Path,
args: A, args: A,
) -> Result<(), std::io::Error> { cwd: P,
return_stdout: bool,
) -> Result<Option<String>, std::io::Error> {
match Command::new("lune") match Command::new("lune")
.arg("run") .arg("run")
.arg(script_path.as_str()) .arg(script_path.as_os_str())
.args(args) .args(args)
.current_dir(cwd)
.stdin(Stdio::null()) .stdin(Stdio::null())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()) .stderr(Stdio::piped())
@ -27,7 +29,7 @@ pub fn execute_lune_script<A: IntoIterator<Item = S>, S: AsRef<OsStr>>(
let script = match script_name { let script = match script_name {
Some(script) => script.to_string(), Some(script) => script.to_string(),
None => script_path.to_string(), None => script_path.to_string_lossy().to_string(),
}; };
let script_2 = script.to_string(); let script_2 = script.to_string();
@ -46,10 +48,17 @@ pub fn execute_lune_script<A: IntoIterator<Item = S>, S: AsRef<OsStr>>(
} }
}); });
let mut stdout_str = String::new();
for line in stdout.lines() { for line in stdout.lines() {
match line { match line {
Ok(line) => { Ok(line) => {
log::info!("[{script_2}]: {line}"); log::info!("[{script_2}]: {line}");
if return_stdout {
stdout_str.push_str(&line);
stdout_str.push('\n');
}
} }
Err(e) => { Err(e) => {
log::error!("ERROR IN READING STDOUT OF {script_2}: {e}"); log::error!("ERROR IN READING STDOUT OF {script_2}: {e}");
@ -57,24 +66,18 @@ pub fn execute_lune_script<A: IntoIterator<Item = S>, S: AsRef<OsStr>>(
} }
} }
} }
if return_stdout {
Ok(Some(stdout_str))
} else {
Ok(None)
}
} }
Err(e) if e.kind() == std::io::ErrorKind::NotFound => { Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
log::warn!("Lune could not be found in PATH: {e}") log::warn!("Lune could not be found in PATH: {e}");
Ok(None)
} }
Err(e) => return Err(e), Err(e) => Err(e),
};
Ok(())
}
pub fn execute_script<A: IntoIterator<Item = S>, S: AsRef<OsStr>>(
manifest: &Manifest,
script: &str,
args: A,
) -> Result<(), std::io::Error> {
if let Some(script_path) = manifest.scripts.get(script) {
return execute_lune_script(Some(script), script_path, args);
} }
Ok(())
} }

View file

@ -34,7 +34,7 @@ impl DependencySpecifier for DependencySpecifiers {}
impl Display for DependencySpecifiers { impl Display for DependencySpecifiers {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
DependencySpecifiers::Pesde(specifier) => write!(f, "{}", specifier), DependencySpecifiers::Pesde(specifier) => write!(f, "{specifier}"),
} }
} }
} }

View file

@ -8,7 +8,7 @@ use pkg_ref::PesdePackageRef;
use specifier::PesdeDependencySpecifier; use specifier::PesdeDependencySpecifier;
use crate::{ use crate::{
authenticate_conn, git::authenticate_conn,
manifest::{DependencyType, Target}, manifest::{DependencyType, Target},
names::{PackageName, PackageNames}, names::{PackageName, PackageNames},
source::{hash, DependencySpecifiers, PackageSource, ResolveResult}, source::{hash, DependencySpecifiers, PackageSource, ResolveResult},
@ -264,7 +264,7 @@ impl PackageSource for PesdePackageSource {
.connect(Direction::Fetch) .connect(Direction::Fetch)
.map_err(|e| Self::RefreshError::Connect(self.repo_url.clone(), e))?; .map_err(|e| Self::RefreshError::Connect(self.repo_url.clone(), e))?;
authenticate_conn(&mut connection, project.auth_config.clone()); authenticate_conn(&mut connection, &project.auth_config);
connection connection
.prepare_fetch(gix::progress::Discard, Default::default()) .prepare_fetch(gix::progress::Discard, Default::default())
@ -276,12 +276,13 @@ impl PackageSource for PesdePackageSource {
} }
std::fs::create_dir_all(&path)?; std::fs::create_dir_all(&path)?;
let auth_config = project.auth_config.clone(); let auth_config = project.auth_config.clone();
gix::prepare_clone_bare(self.repo_url.clone(), &path) gix::prepare_clone_bare(self.repo_url.clone(), &path)
.map_err(|e| Self::RefreshError::Clone(self.repo_url.clone(), e))? .map_err(|e| Self::RefreshError::Clone(self.repo_url.clone(), e))?
.configure_connection(move |c| { .configure_connection(move |c| {
authenticate_conn(c, auth_config.clone()); authenticate_conn(c, &auth_config);
Ok(()) Ok(())
}) })
.fetch_only(gix::progress::Discard, &false.into()) .fetch_only(gix::progress::Discard, &false.into())
@ -344,9 +345,7 @@ impl PackageSource for PesdePackageSource {
let mut response = REQWEST_CLIENT.get(url); let mut response = REQWEST_CLIENT.get(url);
if let Some(token) = &project.auth_config.pesde_token { if let Some(token) = &project.auth_config.pesde_token {
use secrecy::ExposeSecret; response = response.header("Authorization", format!("Bearer {token}"));
response =
response.header("Authorization", format!("Bearer {}", token.expose_secret()));
} }
let response = response.send()?; let response = response.send()?;