Compare commits
24 commits
dc1ceba1a4
...
83343bc3dd
| Author | SHA1 | Date | |
|---|---|---|---|
|
83343bc3dd |
|||
|
f2af2fbbe4 |
|||
|
a89184a358 |
|||
|
8478c914b5 |
|||
|
0b5882b1e1 |
|||
|
f4287de795 |
|||
|
b0a594e892 |
|||
|
d4938c4ae8 |
|||
|
0fecd1486b |
|||
|
c6d60b4459 |
|||
|
bc74c24dd5 |
|||
|
a1357b2501 |
|||
|
1adccf8587 |
|||
|
829d52c95b |
|||
|
28d1763aee |
|||
|
00f5442679 |
|||
|
885cbd5da6 |
|||
|
0e5fd41496 |
|||
|
0d64d4d1de |
|||
|
a2d4a21fec |
|||
|
74a81e7cfd |
|||
|
3584117eb8 |
|||
|
fb2c02d53d |
|||
|
e01313066d |
38 changed files with 1250 additions and 676 deletions
373
Cargo.lock
generated
373
Cargo.lock
generated
|
|
@ -16,7 +16,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cipher",
|
"cipher",
|
||||||
"cpufeatures",
|
"cpufeatures 0.2.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -80,9 +80,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.100"
|
version = "1.0.101"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "assert-json-diff"
|
name = "assert-json-diff"
|
||||||
|
|
@ -211,6 +211,17 @@ version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chacha20"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures 0.3.0",
|
||||||
|
"rand_core 0.10.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cipher"
|
name = "cipher"
|
||||||
version = "0.4.4"
|
version = "0.4.4"
|
||||||
|
|
@ -223,9 +234,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.54"
|
version = "4.5.58"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394"
|
checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
|
|
@ -233,9 +244,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.5.54"
|
version = "4.5.58"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00"
|
checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
|
|
@ -245,9 +256,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_derive"
|
name = "clap_derive"
|
||||||
version = "4.5.49"
|
version = "4.5.55"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
|
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
|
@ -257,9 +268,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_lex"
|
name = "clap_lex"
|
||||||
version = "0.7.6"
|
version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
|
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cmake"
|
name = "cmake"
|
||||||
|
|
@ -321,9 +332,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "constant_time_eq"
|
name = "constant_time_eq"
|
||||||
version = "0.3.1"
|
version = "0.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
|
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
|
|
@ -360,6 +371,15 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc"
|
name = "crc"
|
||||||
version = "3.3.0"
|
version = "3.3.0"
|
||||||
|
|
@ -498,9 +518,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "env_filter"
|
name = "env_filter"
|
||||||
version = "0.1.4"
|
version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2"
|
checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"regex",
|
"regex",
|
||||||
|
|
@ -508,9 +528,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "env_logger"
|
name = "env_logger"
|
||||||
version = "0.11.8"
|
version = "0.11.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
|
checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
|
|
@ -564,6 +584,12 @@ version = "1.0.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foldhash"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
|
|
@ -706,10 +732,26 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "git2"
|
name = "getrandom"
|
||||||
version = "0.20.3"
|
version = "0.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3e2b37e2f62729cdada11f0e6b3b6fe383c69c29fc619e391223e12856af308c"
|
checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
"rand_core 0.10.0",
|
||||||
|
"wasip2",
|
||||||
|
"wasip3",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "git2"
|
||||||
|
version = "0.20.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
"libc",
|
"libc",
|
||||||
|
|
@ -720,6 +762,12 @@ dependencies = [
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glob"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.4.13"
|
version = "0.4.13"
|
||||||
|
|
@ -739,6 +787,15 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.15.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||||
|
dependencies = [
|
||||||
|
"foldhash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.16.1"
|
version = "0.16.1"
|
||||||
|
|
@ -951,6 +1008,12 @@ dependencies = [
|
||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "id-arena"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
|
@ -979,7 +1042,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown 0.16.1",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1108,6 +1173,12 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leb128fmt"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libbz2-rs-sys"
|
name = "libbz2-rs-sys"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
|
|
@ -1116,9 +1187,9 @@ checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.180"
|
version = "0.2.181"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libgit2-sys"
|
name = "libgit2-sys"
|
||||||
|
|
@ -1263,9 +1334,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mockito"
|
name = "mockito"
|
||||||
version = "1.7.1"
|
version = "1.7.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7e0603425789b4a70fcc4ac4f5a46a566c116ee3e2a6b768dc623f7719c611de"
|
checksum = "90820618712cab19cfc46b274c6c22546a82affcb3c3bdf0f29e3db8e1bb92c0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"assert-json-diff",
|
"assert-json-diff",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
@ -1278,7 +1349,7 @@ dependencies = [
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"log",
|
"log",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rand",
|
"rand 0.9.2",
|
||||||
"regex",
|
"regex",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
|
|
@ -1288,9 +1359,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
|
|
@ -1340,16 +1411,17 @@ dependencies = [
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"futures",
|
"futures",
|
||||||
"git2",
|
"git2",
|
||||||
|
"glob",
|
||||||
"indicatif",
|
"indicatif",
|
||||||
"keyring",
|
"keyring",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"md-5",
|
"md-5",
|
||||||
"mockito",
|
"mockito",
|
||||||
"once_cell",
|
"rand 0.10.0",
|
||||||
"rand",
|
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha1",
|
"sha1",
|
||||||
|
|
@ -1357,7 +1429,7 @@ dependencies = [
|
||||||
"strsim",
|
"strsim",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"textwrap",
|
"textwrap",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
"yansi",
|
"yansi",
|
||||||
|
|
@ -1453,9 +1525,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppmd-rust"
|
name = "ppmd-rust"
|
||||||
version = "1.3.0"
|
version = "1.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d558c559f0450f16f2a27a1f017ef38468c1090c9ce63c8e51366232d53717b4"
|
checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
|
|
@ -1466,6 +1538,16 @@ dependencies = [
|
||||||
"zerocopy",
|
"zerocopy",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prettyplease"
|
||||||
|
version = "0.2.37"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.105"
|
version = "1.0.105"
|
||||||
|
|
@ -1489,7 +1571,7 @@ dependencies = [
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"rustls",
|
"rustls",
|
||||||
"socket2",
|
"socket2",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"web-time",
|
"web-time",
|
||||||
|
|
@ -1505,13 +1587,13 @@ dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
"lru-slab",
|
"lru-slab",
|
||||||
"rand",
|
"rand 0.9.2",
|
||||||
"ring",
|
"ring",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"slab",
|
"slab",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.18",
|
||||||
"tinyvec",
|
"tinyvec",
|
||||||
"tracing",
|
"tracing",
|
||||||
"web-time",
|
"web-time",
|
||||||
|
|
@ -1553,7 +1635,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand_chacha",
|
"rand_chacha",
|
||||||
"rand_core",
|
"rand_core 0.9.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
|
||||||
|
dependencies = [
|
||||||
|
"chacha20",
|
||||||
|
"getrandom 0.4.1",
|
||||||
|
"rand_core 0.10.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1563,7 +1656,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ppv-lite86",
|
"ppv-lite86",
|
||||||
"rand_core",
|
"rand_core 0.9.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1575,6 +1668,12 @@ dependencies = [
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.18"
|
version = "0.5.18"
|
||||||
|
|
@ -1586,9 +1685,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.12.2"
|
version = "1.12.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
|
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
|
@ -1615,9 +1714,9 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.13.1"
|
version = "0.13.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62"
|
checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
@ -1820,6 +1919,12 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "semver"
|
||||||
|
version = "1.0.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.228"
|
version = "1.0.228"
|
||||||
|
|
@ -1882,7 +1987,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures 0.2.17",
|
||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1893,7 +1998,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures 0.2.17",
|
||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2031,12 +2136,12 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.24.0"
|
version = "3.25.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
|
checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.4.1",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
|
|
@ -2064,11 +2169,11 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.17"
|
version = "2.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
|
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl 2.0.17",
|
"thiserror-impl 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2084,9 +2189,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "2.0.17"
|
version = "2.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -2095,22 +2200,23 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.44"
|
version = "0.3.47"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
|
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deranged",
|
"deranged",
|
||||||
|
"js-sys",
|
||||||
"num-conv",
|
"num-conv",
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
"serde",
|
"serde_core",
|
||||||
"time-core",
|
"time-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-core"
|
name = "time-core"
|
||||||
version = "0.1.6"
|
version = "0.1.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
|
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinystr"
|
name = "tinystr"
|
||||||
|
|
@ -2258,6 +2364,12 @@ version = "0.2.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typed-path"
|
||||||
|
version = "0.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.19.0"
|
version = "1.19.0"
|
||||||
|
|
@ -2288,6 +2400,12 @@ version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-xid"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unit-prefix"
|
name = "unit-prefix"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
|
|
@ -2367,7 +2485,16 @@ version = "1.0.1+wasi-0.2.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
|
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"wit-bindgen",
|
"wit-bindgen 0.46.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasip3"
|
||||||
|
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen 0.51.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2428,6 +2555,40 @@ dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-encoder"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
||||||
|
dependencies = [
|
||||||
|
"leb128fmt",
|
||||||
|
"wasmparser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-metadata"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"indexmap",
|
||||||
|
"wasm-encoder",
|
||||||
|
"wasmparser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasmparser"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.10.0",
|
||||||
|
"hashbrown 0.15.5",
|
||||||
|
"indexmap",
|
||||||
|
"semver",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.83"
|
version = "0.3.83"
|
||||||
|
|
@ -2760,6 +2921,94 @@ version = "0.46.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen-rust-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen-core"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"heck",
|
||||||
|
"wit-parser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen-rust"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"heck",
|
||||||
|
"indexmap",
|
||||||
|
"prettyplease",
|
||||||
|
"syn",
|
||||||
|
"wasm-metadata",
|
||||||
|
"wit-bindgen-core",
|
||||||
|
"wit-component",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen-rust-macro"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"prettyplease",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wit-bindgen-core",
|
||||||
|
"wit-bindgen-rust",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-component"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"bitflags 2.10.0",
|
||||||
|
"indexmap",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"wasm-encoder",
|
||||||
|
"wasm-metadata",
|
||||||
|
"wasmparser",
|
||||||
|
"wit-parser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-parser"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"id-arena",
|
||||||
|
"indexmap",
|
||||||
|
"log",
|
||||||
|
"semver",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"unicode-xid",
|
||||||
|
"wasmparser",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "writeable"
|
name = "writeable"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
|
|
@ -2891,9 +3140,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zip"
|
name = "zip"
|
||||||
version = "7.1.0"
|
version = "7.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9013f1222db8a6d680f13a7ccdc60a781199cd09c2fa4eff58e728bb181757fc"
|
checksum = "cc12baa6db2b15a140161ce53d72209dacea594230798c24774139b54ecaa980"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"bzip2",
|
"bzip2",
|
||||||
|
|
@ -2901,8 +3150,7 @@ dependencies = [
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
"deflate64",
|
"deflate64",
|
||||||
"flate2",
|
"flate2",
|
||||||
"generic-array",
|
"getrandom 0.4.1",
|
||||||
"getrandom 0.3.4",
|
|
||||||
"hmac",
|
"hmac",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"lzma-rust2",
|
"lzma-rust2",
|
||||||
|
|
@ -2911,6 +3159,7 @@ dependencies = [
|
||||||
"ppmd-rust",
|
"ppmd-rust",
|
||||||
"sha1",
|
"sha1",
|
||||||
"time",
|
"time",
|
||||||
|
"typed-path",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
"zopfli",
|
"zopfli",
|
||||||
"zstd",
|
"zstd",
|
||||||
|
|
|
||||||
50
Cargo.toml
50
Cargo.toml
|
|
@ -1,47 +1,47 @@
|
||||||
[package]
|
[package]
|
||||||
name = "pakker"
|
name = "pakker"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = [ "NotAShelf <raf@notashelf.dev" ]
|
authors = [ "NotAShelf <raf@notashelf.dev>" ]
|
||||||
|
rust-version = "1.91.0"
|
||||||
|
readme = true
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.100"
|
anyhow = "1.0.101"
|
||||||
async-trait = "0.1.89"
|
async-trait = "0.1.89"
|
||||||
clap = { version = "4.5.54", features = [ "derive" ] }
|
clap = { version = "4.5.58", features = [ "derive" ] }
|
||||||
comfy-table = "7.1"
|
comfy-table = "7.2.2"
|
||||||
dialoguer = "0.12.0"
|
dialoguer = "0.12.0"
|
||||||
env_logger = "0.11.8"
|
env_logger = "0.11.9"
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
git2 = "0.20.3"
|
git2 = "0.20.4"
|
||||||
|
glob = "0.3.3"
|
||||||
indicatif = "0.18.3"
|
indicatif = "0.18.3"
|
||||||
keyring = "3.6.3"
|
keyring = "3.6.3"
|
||||||
libc = "0.2.180"
|
libc = "0.2.181"
|
||||||
log = "0.4.29"
|
log = "0.4.29"
|
||||||
md-5 = "0.10.6"
|
md-5 = "0.10.6"
|
||||||
once_cell = "1.20"
|
rand = "0.10.0"
|
||||||
rand = "0.9.2"
|
regex = "1.12.3"
|
||||||
regex = "1.12"
|
reqwest = { version = "0.13.2", features = [ "json" ] }
|
||||||
reqwest = { version = "0.13.1", features = [ "json" ] }
|
semver = "1.0.27"
|
||||||
serde = { version = "1.0.228", features = [ "derive" ] }
|
serde = { version = "1.0.228", features = [ "derive" ] }
|
||||||
serde_json = "1.0.149"
|
serde_json = "1.0.149"
|
||||||
sha1 = "0.10.6"
|
sha1 = "0.10.6"
|
||||||
sha2 = "0.10.0"
|
sha2 = "0.10.9"
|
||||||
strsim = "0.11.1"
|
strsim = "0.11.1"
|
||||||
tempfile = "3.24.0"
|
tempfile = "3.25.0"
|
||||||
textwrap = "0.16"
|
textwrap = "0.16.2"
|
||||||
thiserror = "2.0.17"
|
thiserror = "2.0.18"
|
||||||
tokio = { version = "1.49.0", features = [ "full" ] }
|
tokio = { version = "1.49.0", features = [ "full" ] }
|
||||||
walkdir = "2.5.0"
|
walkdir = "2.5.0"
|
||||||
yansi = "1.0.1"
|
yansi = "1.0.1"
|
||||||
zip = "7.1.0"
|
zip = "7.4.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
mockito = "1.7.1"
|
mockito = "1.7.2"
|
||||||
tempfile = "3.24.0"
|
tempfile = "3.25.0"
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "pakker"
|
|
||||||
path = "src/main.rs"
|
|
||||||
|
|
||||||
# Optimize crypto stuff. Building them with optimizations makes that build script
|
# Optimize crypto stuff. Building them with optimizations makes that build script
|
||||||
# run ~5x faster, more than offsetting the additional build time added to the
|
# run ~5x faster, more than offsetting the additional build time added to the
|
||||||
|
|
|
||||||
50
src/cli.rs
50
src/cli.rs
|
|
@ -15,6 +15,10 @@ pub struct Cli {
|
||||||
#[clap(short, long, action = clap::ArgAction::Count)]
|
#[clap(short, long, action = clap::ArgAction::Count)]
|
||||||
pub verbose: u8,
|
pub verbose: u8,
|
||||||
|
|
||||||
|
/// Skip all confirmation prompts (assume yes)
|
||||||
|
#[clap(short, long, global = true)]
|
||||||
|
pub yes: bool,
|
||||||
|
|
||||||
#[clap(subcommand)]
|
#[clap(subcommand)]
|
||||||
pub command: Commands,
|
pub command: Commands,
|
||||||
}
|
}
|
||||||
|
|
@ -97,20 +101,16 @@ pub struct InitArgs {
|
||||||
pub version: Option<String>,
|
pub version: Option<String>,
|
||||||
|
|
||||||
/// Target platform
|
/// Target platform
|
||||||
#[clap(short, long, default_value = "multiplatform")]
|
#[clap(short, long)]
|
||||||
pub target: String,
|
pub target: Option<String>,
|
||||||
|
|
||||||
/// Minecraft version
|
/// Minecraft versions (space-separated)
|
||||||
#[clap(short, long, default_value = "1.20.1")]
|
#[clap(short, long = "mc-versions", value_delimiter = ' ', num_args = 1..)]
|
||||||
pub mc_version: String,
|
pub mc_versions: Option<Vec<String>>,
|
||||||
|
|
||||||
/// Mod loader
|
/// Mod loaders (format: name=version, can be specified multiple times)
|
||||||
#[clap(short, long, default_value = "fabric")]
|
#[clap(short, long = "loaders", value_delimiter = ',')]
|
||||||
pub loader: String,
|
pub loaders: Option<Vec<String>>,
|
||||||
|
|
||||||
/// Mod loader version
|
|
||||||
#[clap(short = 'v', long, default_value = "latest")]
|
|
||||||
pub loader_version: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
|
|
@ -121,10 +121,6 @@ pub struct ImportArgs {
|
||||||
/// Resolve dependencies
|
/// Resolve dependencies
|
||||||
#[clap(short = 'D', long = "deps")]
|
#[clap(short = 'D', long = "deps")]
|
||||||
pub deps: bool,
|
pub deps: bool,
|
||||||
|
|
||||||
/// Skip confirmation prompts
|
|
||||||
#[clap(short, long)]
|
|
||||||
pub yes: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
|
|
@ -144,10 +140,6 @@ pub struct AddArgs {
|
||||||
/// Update if already exists
|
/// Update if already exists
|
||||||
#[clap(short, long)]
|
#[clap(short, long)]
|
||||||
pub update: bool,
|
pub update: bool,
|
||||||
|
|
||||||
/// Skip confirmation prompts
|
|
||||||
#[clap(short, long)]
|
|
||||||
pub yes: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
|
|
@ -195,10 +187,6 @@ pub struct AddPrjArgs {
|
||||||
/// Skip resolving dependencies
|
/// Skip resolving dependencies
|
||||||
#[clap(short = 'D', long = "no-deps")]
|
#[clap(short = 'D', long = "no-deps")]
|
||||||
pub no_deps: bool,
|
pub no_deps: bool,
|
||||||
|
|
||||||
/// Skip confirmation prompts
|
|
||||||
#[clap(short, long)]
|
|
||||||
pub yes: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
|
|
@ -211,9 +199,9 @@ pub struct RmArgs {
|
||||||
#[clap(short = 'a', long)]
|
#[clap(short = 'a', long)]
|
||||||
pub all: bool,
|
pub all: bool,
|
||||||
|
|
||||||
/// Skip confirmation prompt
|
/// Skip removing dependent projects
|
||||||
#[clap(short, long)]
|
#[clap(short = 'D', long = "no-deps")]
|
||||||
pub yes: bool,
|
pub no_deps: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
|
|
@ -222,9 +210,9 @@ pub struct UpdateArgs {
|
||||||
#[arg(value_name = "PROJECT")]
|
#[arg(value_name = "PROJECT")]
|
||||||
pub inputs: Vec<String>,
|
pub inputs: Vec<String>,
|
||||||
|
|
||||||
/// Skip confirmation prompts
|
/// Update all projects
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
pub yes: bool,
|
pub all: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
|
|
@ -344,7 +332,7 @@ pub struct SyncArgs {
|
||||||
#[clap(short = 'R', long)]
|
#[clap(short = 'R', long)]
|
||||||
pub removals: bool,
|
pub removals: bool,
|
||||||
|
|
||||||
/// Sync updates only
|
/// Sync updates only (apply pending updates)
|
||||||
#[clap(short = 'U', long)]
|
#[clap(short = 'U', long)]
|
||||||
pub updates: bool,
|
pub updates: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -371,7 +359,7 @@ pub struct ExportArgs {
|
||||||
|
|
||||||
/// Export modpack without server content
|
/// Export modpack without server content
|
||||||
/// Modrinth: exclude server-overrides and SERVER mods
|
/// Modrinth: exclude server-overrides and SERVER mods
|
||||||
/// ServerPack: skip export
|
/// `ServerPack`: skip export
|
||||||
#[clap(long = "no-server")]
|
#[clap(long = "no-server")]
|
||||||
pub no_server: bool,
|
pub no_server: bool,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,15 +13,18 @@ fn get_loaders(lockfile: &LockFile) -> Vec<String> {
|
||||||
|
|
||||||
pub fn create_all_platforms()
|
pub fn create_all_platforms()
|
||||||
-> Result<HashMap<String, Box<dyn crate::platform::PlatformClient>>> {
|
-> Result<HashMap<String, Box<dyn crate::platform::PlatformClient>>> {
|
||||||
|
const MODRINTH: &str = "modrinth";
|
||||||
|
const CURSEFORGE: &str = "curseforge";
|
||||||
|
|
||||||
let mut platforms = HashMap::new();
|
let mut platforms = HashMap::new();
|
||||||
|
|
||||||
if let Ok(platform) = create_platform("modrinth", None) {
|
if let Ok(platform) = create_platform(MODRINTH, None) {
|
||||||
platforms.insert("modrinth".to_string(), platform);
|
platforms.insert(MODRINTH.to_owned(), platform);
|
||||||
}
|
}
|
||||||
if let Ok(platform) =
|
if let Ok(platform) =
|
||||||
create_platform("curseforge", std::env::var("CURSEFORGE_API_KEY").ok())
|
create_platform(CURSEFORGE, std::env::var("CURSEFORGE_API_KEY").ok())
|
||||||
{
|
{
|
||||||
platforms.insert("curseforge".to_string(), platform);
|
platforms.insert(CURSEFORGE.to_owned(), platform);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(platforms)
|
Ok(platforms)
|
||||||
|
|
@ -54,9 +57,11 @@ use crate::{cli::AddArgs, model::fork::LocalConfig};
|
||||||
|
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
args: AddArgs,
|
args: AddArgs,
|
||||||
|
global_yes: bool,
|
||||||
lockfile_path: &Path,
|
lockfile_path: &Path,
|
||||||
config_path: &Path,
|
config_path: &Path,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let skip_prompts = global_yes;
|
||||||
log::info!("Adding projects: {:?}", args.inputs);
|
log::info!("Adding projects: {:?}", args.inputs);
|
||||||
|
|
||||||
// Load lockfile
|
// Load lockfile
|
||||||
|
|
@ -184,9 +189,9 @@ pub async fn execute(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prompt for confirmation unless --yes flag is set
|
// Prompt for confirmation unless --yes flag is set
|
||||||
if !args.yes {
|
if !skip_prompts {
|
||||||
let prompt_msg = format!("Add project '{}'?", project.get_name());
|
let prompt_msg = format!("Add project '{}'?", project.get_name());
|
||||||
if !crate::ui_utils::prompt_yes_no(&prompt_msg, true)? {
|
if !crate::ui_utils::prompt_yes_no(&prompt_msg, true, skip_prompts)? {
|
||||||
log::info!("Skipping project: {}", project.get_name());
|
log::info!("Skipping project: {}", project.get_name());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -210,13 +215,14 @@ pub async fn execute(
|
||||||
&& !all_new_projects.iter().any(|p| p.pakku_id == dep.pakku_id)
|
&& !all_new_projects.iter().any(|p| p.pakku_id == dep.pakku_id)
|
||||||
{
|
{
|
||||||
// Prompt user for confirmation unless --yes flag is set
|
// Prompt user for confirmation unless --yes flag is set
|
||||||
if !args.yes {
|
if !skip_prompts {
|
||||||
let prompt_msg = format!(
|
let prompt_msg = format!(
|
||||||
"Add dependency '{}' required by '{}'?",
|
"Add dependency '{}' required by '{}'?",
|
||||||
dep.get_name(),
|
dep.get_name(),
|
||||||
project.get_name()
|
project.get_name()
|
||||||
);
|
);
|
||||||
if !crate::ui_utils::prompt_yes_no(&prompt_msg, true)? {
|
if !crate::ui_utils::prompt_yes_no(&prompt_msg, true, skip_prompts)?
|
||||||
|
{
|
||||||
log::info!("Skipping dependency: {}", dep.get_name());
|
log::info!("Skipping dependency: {}", dep.get_name());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -232,7 +232,7 @@ pub async fn execute(
|
||||||
"Project '{existing_name}' already exists. Replace with \
|
"Project '{existing_name}' already exists. Replace with \
|
||||||
'{project_name}'?"
|
'{project_name}'?"
|
||||||
);
|
);
|
||||||
if !crate::ui_utils::prompt_yes_no(&prompt_msg, false)? {
|
if !crate::ui_utils::prompt_yes_no(&prompt_msg, false, yes)? {
|
||||||
log::info!("Operation cancelled by user");
|
log::info!("Operation cancelled by user");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
@ -244,7 +244,7 @@ pub async fn execute(
|
||||||
} else {
|
} else {
|
||||||
if !yes {
|
if !yes {
|
||||||
let prompt_msg = format!("Add project '{project_name}'?");
|
let prompt_msg = format!("Add project '{project_name}'?");
|
||||||
if !crate::ui_utils::prompt_yes_no(&prompt_msg, true)? {
|
if !crate::ui_utils::prompt_yes_no(&prompt_msg, true, yes)? {
|
||||||
log::info!("Operation cancelled by user");
|
log::info!("Operation cancelled by user");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
@ -283,7 +283,7 @@ pub async fn execute(
|
||||||
if !yes {
|
if !yes {
|
||||||
let prompt_msg =
|
let prompt_msg =
|
||||||
format!("Add dependency '{dep_name}' required by '{project_name}'?");
|
format!("Add dependency '{dep_name}' required by '{project_name}'?");
|
||||||
if !crate::ui_utils::prompt_yes_no(&prompt_msg, true)? {
|
if !crate::ui_utils::prompt_yes_no(&prompt_msg, true, yes)? {
|
||||||
log::info!("Skipping dependency: {dep_name}");
|
log::info!("Skipping dependency: {dep_name}");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ pub fn execute(
|
||||||
// Find the project in lockfile to get its pakku_id
|
// Find the project in lockfile to get its pakku_id
|
||||||
// Try multiple lookup strategies: pakku_id first, then slug, then name
|
// Try multiple lookup strategies: pakku_id first, then slug, then name
|
||||||
let found_project = lockfile
|
let found_project = lockfile
|
||||||
.find_project(&project)
|
.get_project(&project)
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
// Try to find by slug on any platform
|
// Try to find by slug on any platform
|
||||||
lockfile
|
lockfile
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ pub async fn execute(
|
||||||
let config_dir = config_path.parent().unwrap_or(Path::new("."));
|
let config_dir = config_path.parent().unwrap_or(Path::new("."));
|
||||||
|
|
||||||
// IPC coordination - prevent concurrent operations on the same modpack
|
// IPC coordination - prevent concurrent operations on the same modpack
|
||||||
let ipc = IpcCoordinator::new(&config_dir.to_path_buf())?;
|
let ipc = IpcCoordinator::new(config_dir)?;
|
||||||
let ipc_timeout = std::time::Duration::from_secs(60);
|
let ipc_timeout = std::time::Duration::from_secs(60);
|
||||||
|
|
||||||
// Check for conflicting export operations
|
// Check for conflicting export operations
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,8 @@ pub async fn execute(
|
||||||
let operation_id = coordinator.register_operation(OperationType::Fetch)?;
|
let operation_id = coordinator.register_operation(OperationType::Fetch)?;
|
||||||
let _guard = OperationGuard::new(coordinator, operation_id);
|
let _guard = OperationGuard::new(coordinator, operation_id);
|
||||||
|
|
||||||
// Create fetcher
|
// Create fetcher with shelve option
|
||||||
let fetcher = Fetcher::new(".");
|
let fetcher = Fetcher::new(".").with_shelve(args.shelve);
|
||||||
|
|
||||||
// Fetch all projects (progress indicators handled in fetch.rs)
|
// Fetch all projects (progress indicators handled in fetch.rs)
|
||||||
fetcher.fetch_all(&lockfile, &config).await?;
|
fetcher.fetch_all(&lockfile, &config).await?;
|
||||||
|
|
|
||||||
|
|
@ -211,13 +211,12 @@ fn execute_init(
|
||||||
.args(["log", "--limit", "1", "--template", ""])
|
.args(["log", "--limit", "1", "--template", ""])
|
||||||
.current_dir(path)
|
.current_dir(path)
|
||||||
.output()
|
.output()
|
||||||
|
&& !output.stdout.is_empty()
|
||||||
{
|
{
|
||||||
if !output.stdout.is_empty() {
|
println!(
|
||||||
println!(
|
"Note: Jujutsu repository detected. Make sure to run 'jj git \
|
||||||
"Note: Jujutsu repository detected. Make sure to run 'jj git \
|
push' to sync changes with remote if needed."
|
||||||
push' to sync changes with remote if needed."
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
VcsType::None => {
|
VcsType::None => {
|
||||||
|
|
@ -256,7 +255,7 @@ fn execute_init(
|
||||||
local_config.parent_config_hash = Some(config_hash);
|
local_config.parent_config_hash = Some(config_hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now clone from the local path into .pakku/parent — this avoids
|
// Now clone from the local path into .pakku/parent, this avoids
|
||||||
// re-downloading objects
|
// re-downloading objects
|
||||||
let parent_path = Path::new(&parent_path_str);
|
let parent_path = Path::new(&parent_path_str);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,11 @@ use crate::{
|
||||||
|
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
args: ImportArgs,
|
args: ImportArgs,
|
||||||
|
global_yes: bool,
|
||||||
lockfile_path: &Path,
|
lockfile_path: &Path,
|
||||||
config_path: &Path,
|
config_path: &Path,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let skip_prompts = global_yes;
|
||||||
log::info!("Importing modpack from {}", args.file);
|
log::info!("Importing modpack from {}", args.file);
|
||||||
log::info!(
|
log::info!(
|
||||||
"Dependency resolution: {}",
|
"Dependency resolution: {}",
|
||||||
|
|
@ -27,7 +29,7 @@ pub async fn execute(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if lockfile or config already exist
|
// Check if lockfile or config already exist
|
||||||
if (lockfile_path.exists() || config_path.exists()) && !args.yes {
|
if (lockfile_path.exists() || config_path.exists()) && !skip_prompts {
|
||||||
let msg = if lockfile_path.exists() && config_path.exists() {
|
let msg = if lockfile_path.exists() && config_path.exists() {
|
||||||
"Both pakku-lock.json and pakku.json exist. Importing will overwrite \
|
"Both pakku-lock.json and pakku.json exist. Importing will overwrite \
|
||||||
them. Continue?"
|
them. Continue?"
|
||||||
|
|
@ -37,7 +39,7 @@ pub async fn execute(
|
||||||
"pakku.json exists. Importing will overwrite it. Continue?"
|
"pakku.json exists. Importing will overwrite it. Continue?"
|
||||||
};
|
};
|
||||||
|
|
||||||
if !prompt_yes_no(msg, false)? {
|
if !prompt_yes_no(msg, false, skip_prompts)? {
|
||||||
log::info!("Import cancelled by user");
|
log::info!("Import cancelled by user");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
@ -134,16 +136,20 @@ async fn import_modrinth(
|
||||||
{
|
{
|
||||||
log::info!("Fetching project: {project_id}");
|
log::info!("Fetching project: {project_id}");
|
||||||
match platform
|
match platform
|
||||||
.request_project_with_files(project_id, &lockfile.mc_versions, &[
|
.request_project_with_files(
|
||||||
loader.0.clone(),
|
project_id,
|
||||||
])
|
&lockfile.mc_versions,
|
||||||
|
std::slice::from_ref(&loader.0),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(mut project) => {
|
Ok(mut project) => {
|
||||||
// Select best file
|
// Select best file
|
||||||
if let Err(e) =
|
if let Err(e) = project.select_file(
|
||||||
project.select_file(&lockfile.mc_versions, &[loader.0.clone()])
|
&lockfile.mc_versions,
|
||||||
{
|
std::slice::from_ref(&loader.0),
|
||||||
|
None, // Use default (1 file) during import
|
||||||
|
) {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"Failed to select file for {}: {}",
|
"Failed to select file for {}: {}",
|
||||||
project.get_name(),
|
project.get_name(),
|
||||||
|
|
@ -182,6 +188,7 @@ async fn import_modrinth(
|
||||||
projects: None,
|
projects: None,
|
||||||
export_profiles: None,
|
export_profiles: None,
|
||||||
export_server_side_projects_to_client: None,
|
export_server_side_projects_to_client: None,
|
||||||
|
file_count_preference: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Save files using provided paths
|
// Save files using provided paths
|
||||||
|
|
@ -311,6 +318,7 @@ async fn import_curseforge(
|
||||||
if let Err(e) = project.select_file(
|
if let Err(e) = project.select_file(
|
||||||
&lockfile.mc_versions,
|
&lockfile.mc_versions,
|
||||||
&loaders.keys().cloned().collect::<Vec<_>>(),
|
&loaders.keys().cloned().collect::<Vec<_>>(),
|
||||||
|
None, // Use default (1 file) during import
|
||||||
) {
|
) {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"Failed to select file for {}: {}",
|
"Failed to select file for {}: {}",
|
||||||
|
|
@ -325,6 +333,7 @@ async fn import_curseforge(
|
||||||
if let Err(e) = project.select_file(
|
if let Err(e) = project.select_file(
|
||||||
&lockfile.mc_versions,
|
&lockfile.mc_versions,
|
||||||
&loaders.keys().cloned().collect::<Vec<_>>(),
|
&loaders.keys().cloned().collect::<Vec<_>>(),
|
||||||
|
None, // Use default (1 file) during import
|
||||||
) {
|
) {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"Failed to select file for {}: {}",
|
"Failed to select file for {}: {}",
|
||||||
|
|
@ -357,7 +366,7 @@ async fn import_curseforge(
|
||||||
description: None,
|
description: None,
|
||||||
author: manifest["author"]
|
author: manifest["author"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.map(|s| s.to_string()),
|
.map(std::string::ToString::to_string),
|
||||||
overrides: vec!["overrides".to_string()],
|
overrides: vec!["overrides".to_string()],
|
||||||
server_overrides: None,
|
server_overrides: None,
|
||||||
client_overrides: None,
|
client_overrides: None,
|
||||||
|
|
@ -365,6 +374,7 @@ async fn import_curseforge(
|
||||||
projects: None,
|
projects: None,
|
||||||
export_profiles: None,
|
export_profiles: None,
|
||||||
export_server_side_projects_to_client: None,
|
export_server_side_projects_to_client: None,
|
||||||
|
file_count_preference: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Save files using provided paths
|
// Save files using provided paths
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,12 @@ use crate::{
|
||||||
|
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
args: InitArgs,
|
args: InitArgs,
|
||||||
|
global_yes: bool,
|
||||||
lockfile_path: &Path,
|
lockfile_path: &Path,
|
||||||
config_path: &Path,
|
config_path: &Path,
|
||||||
) -> Result<(), PakkerError> {
|
) -> Result<(), PakkerError> {
|
||||||
|
let skip_prompts = global_yes;
|
||||||
|
|
||||||
if lockfile_path.exists() {
|
if lockfile_path.exists() {
|
||||||
return Err(PakkerError::AlreadyExists(
|
return Err(PakkerError::AlreadyExists(
|
||||||
"Lock file already exists".into(),
|
"Lock file already exists".into(),
|
||||||
|
|
@ -24,7 +27,7 @@ pub async fn execute(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interactive mode: prompt for values not provided via CLI and --yes not set
|
// Interactive mode: prompt for values not provided via CLI and --yes not set
|
||||||
let is_interactive = !args.yes && args.name.is_none();
|
let is_interactive = !skip_prompts && args.name.is_none();
|
||||||
|
|
||||||
// Get modpack name
|
// Get modpack name
|
||||||
let name = if let Some(name) = args.name.clone() {
|
let name = if let Some(name) = args.name.clone() {
|
||||||
|
|
@ -137,6 +140,7 @@ pub async fn execute(
|
||||||
projects: None,
|
projects: None,
|
||||||
export_profiles: None,
|
export_profiles: None,
|
||||||
export_server_side_projects_to_client: None,
|
export_server_side_projects_to_client: None,
|
||||||
|
file_count_preference: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let config_dir = config_path.parent().unwrap_or(Path::new("."));
|
let config_dir = config_path.parent().unwrap_or(Path::new("."));
|
||||||
|
|
@ -164,9 +168,13 @@ pub async fn execute(
|
||||||
|
|
||||||
if !has_cf_key {
|
if !has_cf_key {
|
||||||
println!();
|
println!();
|
||||||
if prompt_yes_no("Would you like to set up CurseForge API key now?", true)
|
if prompt_yes_no(
|
||||||
.map_err(|e| PakkerError::InvalidInput(e.to_string()))?
|
"Would you like to set up CurseForge API key now?",
|
||||||
&& let Ok(Some(api_key)) = prompt_curseforge_api_key()
|
true,
|
||||||
|
skip_prompts,
|
||||||
|
)
|
||||||
|
.map_err(|e| PakkerError::InvalidInput(e.to_string()))?
|
||||||
|
&& let Ok(Some(api_key)) = prompt_curseforge_api_key(skip_prompts)
|
||||||
{
|
{
|
||||||
// Save to credentials file
|
// Save to credentials file
|
||||||
let creds_path = std::env::var("HOME").map_or_else(
|
let creds_path = std::env::var("HOME").map_or_else(
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,11 @@ use crate::{
|
||||||
|
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
args: RmArgs,
|
args: RmArgs,
|
||||||
|
global_yes: bool,
|
||||||
lockfile_path: &Path,
|
lockfile_path: &Path,
|
||||||
_config_path: &Path,
|
_config_path: &Path,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let skip_prompts = global_yes;
|
||||||
// Load expects directory path, so get parent directory
|
// Load expects directory path, so get parent directory
|
||||||
let lockfile_dir = lockfile_path.parent().unwrap_or(Path::new("."));
|
let lockfile_dir = lockfile_path.parent().unwrap_or(Path::new("."));
|
||||||
let mut lockfile = LockFile::load(lockfile_dir)?;
|
let mut lockfile = LockFile::load(lockfile_dir)?;
|
||||||
|
|
@ -79,7 +81,9 @@ pub async fn execute(
|
||||||
resolved_inputs.push(input.clone());
|
resolved_inputs.push(input.clone());
|
||||||
} else if !args.all {
|
} else if !args.all {
|
||||||
// Try typo suggestion
|
// Try typo suggestion
|
||||||
if let Ok(Some(suggestion)) = prompt_typo_suggestion(input, &all_slugs) {
|
if let Ok(Some(suggestion)) =
|
||||||
|
prompt_typo_suggestion(input, &all_slugs, skip_prompts)
|
||||||
|
{
|
||||||
log::info!("Using suggested project: {suggestion}");
|
log::info!("Using suggested project: {suggestion}");
|
||||||
resolved_inputs.push(suggestion);
|
resolved_inputs.push(suggestion);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -111,13 +115,13 @@ pub async fn execute(
|
||||||
|
|
||||||
// Ask for confirmation unless --yes flag is provided or --all with no
|
// Ask for confirmation unless --yes flag is provided or --all with no
|
||||||
// projects
|
// projects
|
||||||
if !args.yes {
|
if !skip_prompts {
|
||||||
println!("The following projects will be removed:");
|
println!("The following projects will be removed:");
|
||||||
for name in &projects_to_remove {
|
for name in &projects_to_remove {
|
||||||
println!(" - {name}");
|
println!(" - {name}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !prompt_yes_no("Do you want to continue?", false)? {
|
if !prompt_yes_no("Do you want to continue?", false, skip_prompts)? {
|
||||||
println!("Removal cancelled.");
|
println!("Removal cancelled.");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ use crate::{
|
||||||
|
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
parallel: bool,
|
parallel: bool,
|
||||||
|
skip_prompts: bool,
|
||||||
lockfile_path: &Path,
|
lockfile_path: &Path,
|
||||||
config_path: &Path,
|
config_path: &Path,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
|
@ -77,15 +78,15 @@ pub async fn execute(
|
||||||
// Prompt to update if there are updates available
|
// Prompt to update if there are updates available
|
||||||
if !updates.is_empty() {
|
if !updates.is_empty() {
|
||||||
println!();
|
println!();
|
||||||
if crate::ui_utils::prompt_yes_no("Update now?", false)? {
|
if crate::ui_utils::prompt_yes_no("Update now?", false, skip_prompts)? {
|
||||||
// Call update command programmatically (update all projects)
|
// Call update command programmatically (update all projects)
|
||||||
let update_args = crate::cli::UpdateArgs {
|
let update_args = crate::cli::UpdateArgs {
|
||||||
inputs: vec![],
|
inputs: vec![],
|
||||||
all: true,
|
all: true,
|
||||||
yes: true, // Auto-yes for status command
|
|
||||||
};
|
};
|
||||||
crate::cli::commands::update::execute(
|
crate::cli::commands::update::execute(
|
||||||
update_args,
|
update_args,
|
||||||
|
true, // Auto-yes for status command
|
||||||
lockfile_path,
|
lockfile_path,
|
||||||
config_path,
|
config_path,
|
||||||
)
|
)
|
||||||
|
|
@ -380,17 +381,6 @@ fn display_update_results(updates: &[ProjectUpdate]) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn get_project_display_name(project: &Project) -> String {
|
|
||||||
project
|
|
||||||
.name
|
|
||||||
.values()
|
|
||||||
.next()
|
|
||||||
.or_else(|| project.slug.values().next())
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_else(|| "Unknown".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_api_key(platform: &str) -> Option<String> {
|
fn get_api_key(platform: &str) -> Option<String> {
|
||||||
match platform {
|
match platform {
|
||||||
"modrinth" => std::env::var("MODRINTH_TOKEN").ok(),
|
"modrinth" => std::env::var("MODRINTH_TOKEN").ok(),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
fs,
|
fs,
|
||||||
io::{self, Write},
|
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -22,6 +21,7 @@ enum SyncChange {
|
||||||
|
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
args: SyncArgs,
|
args: SyncArgs,
|
||||||
|
global_yes: bool,
|
||||||
lockfile_path: &Path,
|
lockfile_path: &Path,
|
||||||
config_path: &Path,
|
config_path: &Path,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
|
@ -66,7 +66,11 @@ pub async fn execute(
|
||||||
for (file_path, _) in &additions {
|
for (file_path, _) in &additions {
|
||||||
spinner
|
spinner
|
||||||
.set_message(format!("Processing addition: {}", file_path.display()));
|
.set_message(format!("Processing addition: {}", file_path.display()));
|
||||||
if prompt_user(&format!("Add {} to lockfile?", file_path.display()))? {
|
if crate::ui_utils::prompt_yes_no(
|
||||||
|
&format!("Add {} to lockfile?", file_path.display()),
|
||||||
|
false,
|
||||||
|
global_yes,
|
||||||
|
)? {
|
||||||
add_file_to_lockfile(&mut lockfile, file_path, &config).await?;
|
add_file_to_lockfile(&mut lockfile, file_path, &config).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +91,11 @@ pub async fn execute(
|
||||||
.or(project.pakku_id.as_deref())
|
.or(project.pakku_id.as_deref())
|
||||||
.unwrap_or("unknown");
|
.unwrap_or("unknown");
|
||||||
spinner.set_message(format!("Processing removal: {name}"));
|
spinner.set_message(format!("Processing removal: {name}"));
|
||||||
if prompt_user(&format!("Remove {name} from lockfile?"))? {
|
if crate::ui_utils::prompt_yes_no(
|
||||||
|
&format!("Remove {name} from lockfile?"),
|
||||||
|
false,
|
||||||
|
global_yes,
|
||||||
|
)? {
|
||||||
lockfile
|
lockfile
|
||||||
.remove_project(pakku_id)
|
.remove_project(pakku_id)
|
||||||
.ok_or_else(|| PakkerError::ProjectNotFound(pakku_id.clone()))?;
|
.ok_or_else(|| PakkerError::ProjectNotFound(pakku_id.clone()))?;
|
||||||
|
|
@ -174,7 +182,7 @@ async fn add_file_to_lockfile(
|
||||||
_config: &Config,
|
_config: &Config,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// Try to identify the file by hash lookup
|
// Try to identify the file by hash lookup
|
||||||
let _modrinth = ModrinthPlatform::new();
|
let modrinth = ModrinthPlatform::new();
|
||||||
let curseforge = CurseForgePlatform::new(None);
|
let curseforge = CurseForgePlatform::new(None);
|
||||||
|
|
||||||
// Compute file hash
|
// Compute file hash
|
||||||
|
|
@ -186,7 +194,7 @@ async fn add_file_to_lockfile(
|
||||||
let hash = format!("{:x}", hasher.finalize());
|
let hash = format!("{:x}", hasher.finalize());
|
||||||
|
|
||||||
// Try Modrinth first (SHA-1 hash)
|
// Try Modrinth first (SHA-1 hash)
|
||||||
if let Ok(Some(project)) = _modrinth.lookup_by_hash(&hash).await {
|
if let Ok(Some(project)) = modrinth.lookup_by_hash(&hash).await {
|
||||||
lockfile.add_project(project);
|
lockfile.add_project(project);
|
||||||
println!("✓ Added {} (from Modrinth)", file_path.display());
|
println!("✓ Added {} (from Modrinth)", file_path.display());
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|
@ -202,15 +210,3 @@ async fn add_file_to_lockfile(
|
||||||
println!("⚠ Could not identify {}, skipping", file_path.display());
|
println!("⚠ Could not identify {}, skipping", file_path.display());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_user(message: &str) -> Result<bool> {
|
|
||||||
print!("{message} [y/N] ");
|
|
||||||
io::stdout().flush().map_err(PakkerError::IoError)?;
|
|
||||||
|
|
||||||
let mut input = String::new();
|
|
||||||
io::stdin()
|
|
||||||
.read_line(&mut input)
|
|
||||||
.map_err(PakkerError::IoError)?;
|
|
||||||
|
|
||||||
Ok(input.trim().eq_ignore_ascii_case("y"))
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{collections::HashMap, path::Path};
|
use std::path::Path;
|
||||||
|
|
||||||
use indicatif::{ProgressBar, ProgressStyle};
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
|
|
||||||
|
|
@ -6,15 +6,16 @@ use crate::{
|
||||||
cli::UpdateArgs,
|
cli::UpdateArgs,
|
||||||
error::{MultiError, PakkerError},
|
error::{MultiError, PakkerError},
|
||||||
model::{Config, LockFile, UpdateStrategy},
|
model::{Config, LockFile, UpdateStrategy},
|
||||||
platform::create_platform,
|
|
||||||
ui_utils::{prompt_select, prompt_typo_suggestion, prompt_yes_no},
|
ui_utils::{prompt_select, prompt_typo_suggestion, prompt_yes_no},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
args: UpdateArgs,
|
args: UpdateArgs,
|
||||||
|
global_yes: bool,
|
||||||
lockfile_path: &Path,
|
lockfile_path: &Path,
|
||||||
config_path: &Path,
|
config_path: &Path,
|
||||||
) -> Result<(), PakkerError> {
|
) -> Result<(), PakkerError> {
|
||||||
|
let skip_prompts = global_yes;
|
||||||
// Load expects directory path, so get parent directory
|
// Load expects directory path, so get parent directory
|
||||||
let lockfile_dir = lockfile_path.parent().unwrap_or(Path::new("."));
|
let lockfile_dir = lockfile_path.parent().unwrap_or(Path::new("."));
|
||||||
let config_dir = config_path.parent().unwrap_or(Path::new("."));
|
let config_dir = config_path.parent().unwrap_or(Path::new("."));
|
||||||
|
|
@ -23,15 +24,7 @@ pub async fn execute(
|
||||||
let _config = Config::load(config_dir)?;
|
let _config = Config::load(config_dir)?;
|
||||||
|
|
||||||
// Create platforms
|
// Create platforms
|
||||||
let mut platforms = HashMap::new();
|
let platforms = super::add::create_all_platforms()?;
|
||||||
if let Ok(platform) = create_platform("modrinth", None) {
|
|
||||||
platforms.insert("modrinth".to_string(), platform);
|
|
||||||
}
|
|
||||||
if let Ok(platform) =
|
|
||||||
create_platform("curseforge", std::env::var("CURSEFORGE_API_KEY").ok())
|
|
||||||
{
|
|
||||||
platforms.insert("curseforge".to_string(), platform);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect all known project identifiers for typo suggestions
|
// Collect all known project identifiers for typo suggestions
|
||||||
let all_slugs: Vec<String> = lockfile
|
let all_slugs: Vec<String> = lockfile
|
||||||
|
|
@ -63,7 +56,8 @@ pub async fn execute(
|
||||||
indices.push(idx);
|
indices.push(idx);
|
||||||
} else {
|
} else {
|
||||||
// Try typo suggestion
|
// Try typo suggestion
|
||||||
if let Ok(Some(suggestion)) = prompt_typo_suggestion(input, &all_slugs)
|
if let Ok(Some(suggestion)) =
|
||||||
|
prompt_typo_suggestion(input, &all_slugs, skip_prompts)
|
||||||
&& let Some((idx, _)) = lockfile
|
&& let Some((idx, _)) = lockfile
|
||||||
.projects
|
.projects
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -159,17 +153,18 @@ pub async fn execute(
|
||||||
} else {
|
} else {
|
||||||
// Interactive confirmation and version selection if not using --yes
|
// Interactive confirmation and version selection if not using --yes
|
||||||
// flag
|
// flag
|
||||||
let mut should_update = args.yes || args.all;
|
let mut should_update = skip_prompts || args.all;
|
||||||
let mut selected_idx: Option<usize> = None;
|
let mut selected_idx: Option<usize> = None;
|
||||||
|
|
||||||
if !args.yes && !args.all {
|
if !skip_prompts && !args.all {
|
||||||
pb.suspend(|| {
|
pb.suspend(|| {
|
||||||
// First, confirm the update
|
// First, confirm the update
|
||||||
let prompt_msg = format!(
|
let prompt_msg = format!(
|
||||||
"Update '{project_name}' from {old_file_name} to \
|
"Update '{project_name}' from {old_file_name} to \
|
||||||
{new_file_name}?"
|
{new_file_name}?"
|
||||||
);
|
);
|
||||||
should_update = prompt_yes_no(&prompt_msg, true).unwrap_or(false);
|
should_update =
|
||||||
|
prompt_yes_no(&prompt_msg, true, skip_prompts).unwrap_or(false);
|
||||||
|
|
||||||
// If confirmed and multiple versions available, offer selection
|
// If confirmed and multiple versions available, offer selection
|
||||||
if should_update && updated_project.files.len() > 1 {
|
if should_update && updated_project.files.len() > 1 {
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,7 @@ mod tests {
|
||||||
projects: None,
|
projects: None,
|
||||||
export_profiles: None,
|
export_profiles: None,
|
||||||
export_server_side_projects_to_client: None,
|
export_server_side_projects_to_client: None,
|
||||||
|
file_count_preference: None,
|
||||||
};
|
};
|
||||||
assert!(config.export_server_side_projects_to_client.is_none());
|
assert!(config.export_server_side_projects_to_client.is_none());
|
||||||
}
|
}
|
||||||
|
|
@ -220,6 +221,7 @@ mod tests {
|
||||||
projects: None,
|
projects: None,
|
||||||
export_profiles: None,
|
export_profiles: None,
|
||||||
export_server_side_projects_to_client: None,
|
export_server_side_projects_to_client: None,
|
||||||
|
file_count_preference: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let json = serde_json::to_string_pretty(&config).unwrap();
|
let json = serde_json::to_string_pretty(&config).unwrap();
|
||||||
|
|
|
||||||
|
|
@ -6,65 +6,75 @@ pub trait ExportProfile {
|
||||||
fn rules(&self) -> Vec<Box<dyn Rule>>;
|
fn rules(&self) -> Vec<Box<dyn Rule>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CurseForgeProfile;
|
/// Implements [`ExportProfile`] for a unit struct with a static name and rule
|
||||||
pub struct ModrinthProfile;
|
/// list.
|
||||||
pub struct ServerPackProfile;
|
///
|
||||||
|
/// Each rule entry is an expression evaluated as
|
||||||
|
/// `Box::new(super::rules::<expr>)`, supporting both bare unit struct names and
|
||||||
|
/// constructor calls with arguments.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// export_profile! {
|
||||||
|
/// MyProfile => "my-profile" {
|
||||||
|
/// SomeRule,
|
||||||
|
/// AnotherRule::new("arg"),
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
macro_rules! export_profile {
|
||||||
|
($struct:ident => $name:literal { $($rule:expr),* $(,)? }) => {
|
||||||
|
pub struct $struct;
|
||||||
|
|
||||||
impl ExportProfile for CurseForgeProfile {
|
impl ExportProfile for $struct {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
"curseforge"
|
$name
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rules(&self) -> Vec<Box<dyn Rule>> {
|
fn rules(&self) -> Vec<Box<dyn Rule>> {
|
||||||
vec![
|
use super::rules::*;
|
||||||
Box::new(super::rules::CopyProjectFilesRule),
|
vec![
|
||||||
Box::new(super::rules::FilterByPlatformRule),
|
$(Box::new($rule)),*
|
||||||
Box::new(super::rules::MissingProjectsAsOverridesRule::new(
|
]
|
||||||
"curseforge",
|
}
|
||||||
)),
|
}
|
||||||
Box::new(super::rules::CopyOverridesRule),
|
};
|
||||||
Box::new(super::rules::CopyClientOverridesRule),
|
}
|
||||||
Box::new(super::rules::FilterServerOnlyRule),
|
|
||||||
Box::new(super::rules::GenerateManifestRule::curseforge()),
|
export_profile! {
|
||||||
Box::new(super::rules::FilterNonRedistributableRule),
|
CurseForgeProfile => "curseforge" {
|
||||||
Box::new(super::rules::TextReplacementRule),
|
CopyProjectFilesRule,
|
||||||
]
|
FilterByPlatformRule,
|
||||||
|
MissingProjectsAsOverridesRule::new("curseforge"),
|
||||||
|
CopyOverridesRule,
|
||||||
|
CopyClientOverridesRule,
|
||||||
|
FilterServerOnlyRule,
|
||||||
|
GenerateManifestRule::curseforge(),
|
||||||
|
FilterNonRedistributableRule,
|
||||||
|
TextReplacementRule
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExportProfile for ModrinthProfile {
|
export_profile! {
|
||||||
fn name(&self) -> &'static str {
|
ModrinthProfile => "modrinth" {
|
||||||
"modrinth"
|
CopyProjectFilesRule,
|
||||||
}
|
FilterByPlatformRule,
|
||||||
|
MissingProjectsAsOverridesRule::new("modrinth"),
|
||||||
fn rules(&self) -> Vec<Box<dyn Rule>> {
|
CopyOverridesRule,
|
||||||
vec![
|
CopyClientOverridesRule,
|
||||||
Box::new(super::rules::CopyProjectFilesRule),
|
FilterServerOnlyRule,
|
||||||
Box::new(super::rules::FilterByPlatformRule),
|
GenerateManifestRule::modrinth(),
|
||||||
Box::new(super::rules::MissingProjectsAsOverridesRule::new(
|
TextReplacementRule
|
||||||
"modrinth",
|
|
||||||
)),
|
|
||||||
Box::new(super::rules::CopyOverridesRule),
|
|
||||||
Box::new(super::rules::CopyClientOverridesRule),
|
|
||||||
Box::new(super::rules::FilterServerOnlyRule),
|
|
||||||
Box::new(super::rules::GenerateManifestRule::modrinth()),
|
|
||||||
Box::new(super::rules::TextReplacementRule),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExportProfile for ServerPackProfile {
|
export_profile! {
|
||||||
fn name(&self) -> &'static str {
|
ServerPackProfile => "serverpack" {
|
||||||
"serverpack"
|
CopyProjectFilesRule,
|
||||||
}
|
CopyServerOverridesRule,
|
||||||
|
FilterClientOnlyRule,
|
||||||
fn rules(&self) -> Vec<Box<dyn Rule>> {
|
TextReplacementRule
|
||||||
vec![
|
|
||||||
Box::new(super::rules::CopyProjectFilesRule),
|
|
||||||
Box::new(super::rules::CopyServerOverridesRule),
|
|
||||||
Box::new(super::rules::FilterClientOnlyRule),
|
|
||||||
Box::new(super::rules::TextReplacementRule),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1232,6 +1232,7 @@ mod tests {
|
||||||
projects: None,
|
projects: None,
|
||||||
export_profiles: None,
|
export_profiles: None,
|
||||||
export_server_side_projects_to_client: None,
|
export_server_side_projects_to_client: None,
|
||||||
|
file_count_preference: None,
|
||||||
},
|
},
|
||||||
profile_config,
|
profile_config,
|
||||||
export_path: PathBuf::from("/tmp/export"),
|
export_path: PathBuf::from("/tmp/export"),
|
||||||
|
|
@ -1362,6 +1363,7 @@ mod tests {
|
||||||
projects: None,
|
projects: None,
|
||||||
export_profiles: None,
|
export_profiles: None,
|
||||||
export_server_side_projects_to_client: None,
|
export_server_side_projects_to_client: None,
|
||||||
|
file_count_preference: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(get_project_type_dir(&ProjectType::Mod, &config), "mods");
|
assert_eq!(get_project_type_dir(&ProjectType::Mod, &config), "mods");
|
||||||
|
|
@ -1398,6 +1400,7 @@ mod tests {
|
||||||
projects: None,
|
projects: None,
|
||||||
export_profiles: None,
|
export_profiles: None,
|
||||||
export_server_side_projects_to_client: None,
|
export_server_side_projects_to_client: None,
|
||||||
|
file_count_preference: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -1462,6 +1465,7 @@ mod tests {
|
||||||
projects: None,
|
projects: None,
|
||||||
export_profiles: None,
|
export_profiles: None,
|
||||||
export_server_side_projects_to_client: None,
|
export_server_side_projects_to_client: None,
|
||||||
|
file_count_preference: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut context = create_test_context(None);
|
let mut context = create_test_context(None);
|
||||||
|
|
@ -1492,6 +1496,7 @@ mod tests {
|
||||||
projects: None,
|
projects: None,
|
||||||
export_profiles: None,
|
export_profiles: None,
|
||||||
export_server_side_projects_to_client: None,
|
export_server_side_projects_to_client: None,
|
||||||
|
file_count_preference: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut context = create_test_context(None);
|
let mut context = create_test_context(None);
|
||||||
|
|
|
||||||
38
src/fetch.rs
38
src/fetch.rs
|
|
@ -24,12 +24,6 @@ pub struct Fetcher {
|
||||||
shelve: bool,
|
shelve: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FileFetcher {
|
|
||||||
client: Client,
|
|
||||||
base_path: PathBuf,
|
|
||||||
shelve: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Fetcher {
|
impl Fetcher {
|
||||||
pub fn new<P: AsRef<Path>>(base_path: P) -> Self {
|
pub fn new<P: AsRef<Path>>(base_path: P) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -44,25 +38,10 @@ impl Fetcher {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_all(
|
|
||||||
&self,
|
|
||||||
lockfile: &LockFile,
|
|
||||||
config: &Config,
|
|
||||||
) -> Result<()> {
|
|
||||||
let fetcher = FileFetcher {
|
|
||||||
client: self.client.clone(),
|
|
||||||
base_path: self.base_path.clone(),
|
|
||||||
shelve: self.shelve,
|
|
||||||
};
|
|
||||||
fetcher.fetch_all(lockfile, config).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn sync(&self, lockfile: &LockFile, config: &Config) -> Result<()> {
|
pub async fn sync(&self, lockfile: &LockFile, config: &Config) -> Result<()> {
|
||||||
self.fetch_all(lockfile, config).await
|
self.fetch_all(lockfile, config).await
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl FileFetcher {
|
|
||||||
/// Fetch all project files according to lockfile with parallel downloads
|
/// Fetch all project files according to lockfile with parallel downloads
|
||||||
pub async fn fetch_all(
|
pub async fn fetch_all(
|
||||||
&self,
|
&self,
|
||||||
|
|
@ -94,14 +73,14 @@ impl FileFetcher {
|
||||||
let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT_DOWNLOADS));
|
let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT_DOWNLOADS));
|
||||||
|
|
||||||
// Prepare download tasks
|
// Prepare download tasks
|
||||||
|
let client = &self.client;
|
||||||
|
let base_path = &self.base_path;
|
||||||
let download_tasks: Vec<_> = exportable_projects
|
let download_tasks: Vec<_> = exportable_projects
|
||||||
.iter()
|
.iter()
|
||||||
.map(|project| {
|
.map(|project| {
|
||||||
let semaphore = Arc::clone(&semaphore);
|
let semaphore = Arc::clone(&semaphore);
|
||||||
let client = self.client.clone();
|
let client = client.clone();
|
||||||
let base_path = self.base_path.clone();
|
let base_path = base_path.clone();
|
||||||
let lockfile = lockfile.clone();
|
|
||||||
let config = config.clone();
|
|
||||||
let project = (*project).clone();
|
let project = (*project).clone();
|
||||||
let overall_bar = overall_bar.clone();
|
let overall_bar = overall_bar.clone();
|
||||||
|
|
||||||
|
|
@ -111,11 +90,7 @@ impl FileFetcher {
|
||||||
PakkerError::InternalError("Semaphore acquisition failed".into())
|
PakkerError::InternalError("Semaphore acquisition failed".into())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let name = project
|
let name = project.get_name();
|
||||||
.name
|
|
||||||
.values()
|
|
||||||
.next()
|
|
||||||
.map_or("unknown".to_string(), std::clone::Clone::clone);
|
|
||||||
|
|
||||||
let fetcher = Self {
|
let fetcher = Self {
|
||||||
client,
|
client,
|
||||||
|
|
@ -123,8 +98,7 @@ impl FileFetcher {
|
||||||
shelve: false, // Shelving happens at sync level, not per-project
|
shelve: false, // Shelving happens at sync level, not per-project
|
||||||
};
|
};
|
||||||
|
|
||||||
let result =
|
let result = fetcher.fetch_project(&project, lockfile, config).await;
|
||||||
fetcher.fetch_project(&project, &lockfile, &config).await;
|
|
||||||
|
|
||||||
// Update progress bar
|
// Update progress bar
|
||||||
overall_bar.inc(1);
|
overall_bar.inc(1);
|
||||||
|
|
|
||||||
|
|
@ -288,10 +288,9 @@ pub fn detect_vcs_type<P: AsRef<Path>>(path: P) -> VcsType {
|
||||||
.args(["root"])
|
.args(["root"])
|
||||||
.current_dir(path)
|
.current_dir(path)
|
||||||
.output()
|
.output()
|
||||||
|
&& output.status.success()
|
||||||
{
|
{
|
||||||
if output.status.success() {
|
return VcsType::Jujutsu;
|
||||||
return VcsType::Jujutsu;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for git
|
// Check for git
|
||||||
|
|
@ -299,10 +298,9 @@ pub fn detect_vcs_type<P: AsRef<Path>>(path: P) -> VcsType {
|
||||||
.args(["rev-parse", "--show-toplevel"])
|
.args(["rev-parse", "--show-toplevel"])
|
||||||
.current_dir(path)
|
.current_dir(path)
|
||||||
.output()
|
.output()
|
||||||
|
&& output.status.success()
|
||||||
{
|
{
|
||||||
if output.status.success() {
|
return VcsType::Git;
|
||||||
return VcsType::Git;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VcsType::None
|
VcsType::None
|
||||||
|
|
@ -333,7 +331,7 @@ pub fn repo_has_uncommitted_changes<P: AsRef<Path>>(path: P) -> Result<bool> {
|
||||||
.current_dir(path)
|
.current_dir(path)
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
PakkerError::GitError(format!("Failed to run jj status: {}", e))
|
PakkerError::GitError(format!("Failed to run jj status: {e}"))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let output_str = String::from_utf8_lossy(&output.stdout);
|
let output_str = String::from_utf8_lossy(&output.stdout);
|
||||||
|
|
@ -443,7 +441,7 @@ pub fn ahead_behind<P: AsRef<Path>>(
|
||||||
})?;
|
})?;
|
||||||
Ok((ahead, behind))
|
Ok((ahead, behind))
|
||||||
} else {
|
} else {
|
||||||
// Remote ref missing — count commits reachable from local
|
// Remote ref missing, count commits reachable from local
|
||||||
let ahead_count = count_commits(&repo, local_oid)?;
|
let ahead_count = count_commits(&repo, local_oid)?;
|
||||||
Ok((ahead_count, 0))
|
Ok((ahead_count, 0))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
25
src/http.rs
Normal file
25
src/http.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use reqwest::Client;
|
||||||
|
|
||||||
|
/// Create HTTP client with optimized settings for API requests.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the HTTP client cannot be built, which should only happen in
|
||||||
|
/// extreme cases like OOM or broken TLS configuration.
|
||||||
|
pub fn create_http_client() -> Client {
|
||||||
|
Client::builder()
|
||||||
|
.pool_max_idle_per_host(10)
|
||||||
|
.pool_idle_timeout(Duration::from_secs(30))
|
||||||
|
.tcp_keepalive(Duration::from_secs(60))
|
||||||
|
.tcp_nodelay(true)
|
||||||
|
.connect_timeout(Duration::from_secs(15))
|
||||||
|
.timeout(Duration::from_secs(30))
|
||||||
|
.user_agent("Pakker/0.1.0")
|
||||||
|
.build()
|
||||||
|
.expect(
|
||||||
|
"Failed to build HTTP client - this should never happen unless system \
|
||||||
|
resources are exhausted",
|
||||||
|
)
|
||||||
|
}
|
||||||
21
src/ipc.rs
21
src/ipc.rs
|
|
@ -153,17 +153,18 @@ impl IpcCoordinator {
|
||||||
let ipc_dir = ipc_base.join(&modpack_hash);
|
let ipc_dir = ipc_base.join(&modpack_hash);
|
||||||
|
|
||||||
// Create IPC directory with restricted permissions
|
// Create IPC directory with restricted permissions
|
||||||
if let Err(e) = fs::create_dir_all(&ipc_dir)
|
fs::create_dir_all(&ipc_dir).or_else(|e| {
|
||||||
&& !ipc_dir.exists()
|
if ipc_dir.exists() {
|
||||||
{
|
Ok(())
|
||||||
return Err(IpcError::IpcDirCreationFailed(e.to_string()));
|
} else {
|
||||||
}
|
Err(IpcError::IpcDirCreationFailed(e.to_string()))
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
if ipc_dir.exists() {
|
// Set permissions to 700 (owner only)
|
||||||
// Set permissions to 700 (owner only)
|
if let Ok(metadata) = fs::metadata(&ipc_dir) {
|
||||||
if let Ok(metadata) = fs::metadata(&ipc_dir)
|
let current_mode = metadata.permissions().mode() & 0o777;
|
||||||
&& metadata.permissions().mode() != 0o700
|
if current_mode != 0o700 {
|
||||||
{
|
|
||||||
let mut perms = metadata.permissions();
|
let mut perms = metadata.permissions();
|
||||||
perms.set_mode(0o700);
|
perms.set_mode(0o700);
|
||||||
let _ = fs::set_permissions(&ipc_dir, perms);
|
let _ = fs::set_permissions(&ipc_dir, perms);
|
||||||
|
|
|
||||||
126
src/main.rs
126
src/main.rs
|
|
@ -1,8 +1,15 @@
|
||||||
|
// Allow pre-existing clippy warnings for functions with many arguments
|
||||||
|
// and complex types that would require significant refactoring
|
||||||
|
#![allow(clippy::too_many_arguments)]
|
||||||
|
#![allow(clippy::type_complexity)]
|
||||||
|
#![allow(clippy::large_enum_variant)]
|
||||||
|
|
||||||
mod cli;
|
mod cli;
|
||||||
mod error;
|
mod error;
|
||||||
mod export;
|
mod export;
|
||||||
mod fetch;
|
mod fetch;
|
||||||
mod git;
|
mod git;
|
||||||
|
mod http;
|
||||||
mod ipc;
|
mod ipc;
|
||||||
mod model;
|
mod model;
|
||||||
mod platform;
|
mod platform;
|
||||||
|
|
@ -11,13 +18,30 @@ mod resolver;
|
||||||
mod ui_utils;
|
mod ui_utils;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::{env, path::PathBuf};
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use cli::{Cli, Commands};
|
use cli::{Cli, Commands};
|
||||||
use error::PakkerError;
|
use error::PakkerError;
|
||||||
|
|
||||||
use crate::rate_limiter::RateLimiter;
|
/// Search for pakker-lock.json in current directory and parent directories
|
||||||
|
/// Returns the directory containing pakker-lock.json, or None if not found
|
||||||
|
fn find_working_directory() -> Option<PathBuf> {
|
||||||
|
let mut current_dir = env::current_dir().ok()?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let lockfile = current_dir.join("pakker-lock.json");
|
||||||
|
if lockfile.exists() {
|
||||||
|
return Some(current_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try parent directory
|
||||||
|
if !current_dir.pop() {
|
||||||
|
// Reached filesystem root
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), PakkerError> {
|
async fn main() -> Result<(), PakkerError> {
|
||||||
|
|
@ -38,46 +62,73 @@ async fn main() -> Result<(), PakkerError> {
|
||||||
.format_module_path(false)
|
.format_module_path(false)
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
let working_dir = PathBuf::from(".");
|
// Search for pakker-lock.json in current directory and parent directories
|
||||||
|
let working_dir =
|
||||||
|
find_working_directory().unwrap_or_else(|| PathBuf::from("."));
|
||||||
let lockfile_path = working_dir.join("pakker-lock.json");
|
let lockfile_path = working_dir.join("pakker-lock.json");
|
||||||
let config_path = working_dir.join("pakker.json");
|
let config_path = working_dir.join("pakker.json");
|
||||||
|
|
||||||
let _rate_limiter = std::sync::Arc::new(RateLimiter::new(None));
|
let global_yes = cli.yes;
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Commands::Init(args) => {
|
Commands::Init(args) => {
|
||||||
cli::commands::init::execute(args, &lockfile_path, &config_path).await
|
cli::commands::init::execute(
|
||||||
|
args,
|
||||||
|
global_yes,
|
||||||
|
&lockfile_path,
|
||||||
|
&config_path,
|
||||||
|
)
|
||||||
|
.await
|
||||||
},
|
},
|
||||||
Commands::Import(args) => {
|
Commands::Import(args) => {
|
||||||
cli::commands::import::execute(args, &lockfile_path, &config_path).await
|
cli::commands::import::execute(
|
||||||
|
args,
|
||||||
|
global_yes,
|
||||||
|
&lockfile_path,
|
||||||
|
&config_path,
|
||||||
|
)
|
||||||
|
.await
|
||||||
},
|
},
|
||||||
Commands::Add(args) => {
|
Commands::Add(args) => {
|
||||||
cli::commands::add::execute(args, &lockfile_path, &config_path).await
|
cli::commands::add::execute(
|
||||||
|
args,
|
||||||
|
global_yes,
|
||||||
|
&lockfile_path,
|
||||||
|
&config_path,
|
||||||
|
)
|
||||||
|
.await
|
||||||
},
|
},
|
||||||
Commands::AddPrj(args) => {
|
Commands::AddPrj(args) => {
|
||||||
cli::commands::add_prj::execute(
|
cli::commands::add_prj::execute(
|
||||||
args.curseforge.clone(),
|
args.curseforge,
|
||||||
args.modrinth.clone(),
|
args.modrinth,
|
||||||
args.github.clone(),
|
args.github,
|
||||||
args.project_type,
|
args.project_type,
|
||||||
args.side,
|
args.side,
|
||||||
args.strategy,
|
args.strategy,
|
||||||
args.redistributable,
|
args.redistributable,
|
||||||
args.subpath.clone(),
|
args.subpath,
|
||||||
args.aliases.clone(),
|
args.aliases,
|
||||||
args.export,
|
args.export,
|
||||||
args.no_deps,
|
args.no_deps,
|
||||||
args.yes,
|
global_yes,
|
||||||
&lockfile_path,
|
&lockfile_path,
|
||||||
&config_path,
|
&config_path,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
},
|
},
|
||||||
Commands::Rm(args) => {
|
Commands::Rm(args) => {
|
||||||
cli::commands::rm::execute(args, &lockfile_path, &config_path).await
|
cli::commands::rm::execute(args, global_yes, &lockfile_path, &config_path)
|
||||||
|
.await
|
||||||
},
|
},
|
||||||
Commands::Update(args) => {
|
Commands::Update(args) => {
|
||||||
cli::commands::update::execute(args, &lockfile_path, &config_path).await
|
cli::commands::update::execute(
|
||||||
|
args,
|
||||||
|
global_yes,
|
||||||
|
&lockfile_path,
|
||||||
|
&config_path,
|
||||||
|
)
|
||||||
|
.await
|
||||||
},
|
},
|
||||||
Commands::Ls(args) => cli::commands::ls::execute(args, &lockfile_path),
|
Commands::Ls(args) => cli::commands::ls::execute(args, &lockfile_path),
|
||||||
Commands::Set(args) => {
|
Commands::Set(args) => {
|
||||||
|
|
@ -92,7 +143,13 @@ async fn main() -> Result<(), PakkerError> {
|
||||||
cli::commands::fetch::execute(args, &lockfile_path, &config_path).await
|
cli::commands::fetch::execute(args, &lockfile_path, &config_path).await
|
||||||
},
|
},
|
||||||
Commands::Sync(args) => {
|
Commands::Sync(args) => {
|
||||||
cli::commands::sync::execute(args, &lockfile_path, &config_path).await
|
cli::commands::sync::execute(
|
||||||
|
args,
|
||||||
|
global_yes,
|
||||||
|
&lockfile_path,
|
||||||
|
&config_path,
|
||||||
|
)
|
||||||
|
.await
|
||||||
},
|
},
|
||||||
Commands::Export(args) => {
|
Commands::Export(args) => {
|
||||||
cli::commands::export::execute(args, &lockfile_path, &config_path).await
|
cli::commands::export::execute(args, &lockfile_path, &config_path).await
|
||||||
|
|
@ -104,6 +161,7 @@ async fn main() -> Result<(), PakkerError> {
|
||||||
Commands::Status(args) => {
|
Commands::Status(args) => {
|
||||||
cli::commands::status::execute(
|
cli::commands::status::execute(
|
||||||
args.parallel,
|
args.parallel,
|
||||||
|
global_yes,
|
||||||
&lockfile_path,
|
&lockfile_path,
|
||||||
&config_path,
|
&config_path,
|
||||||
)
|
)
|
||||||
|
|
@ -118,12 +176,12 @@ async fn main() -> Result<(), PakkerError> {
|
||||||
.await
|
.await
|
||||||
},
|
},
|
||||||
Commands::Credentials(args) => {
|
Commands::Credentials(args) => {
|
||||||
match &args.subcommand {
|
match args.subcommand {
|
||||||
Some(cli::CredentialsSubcommand::Set(set_args)) => {
|
Some(cli::CredentialsSubcommand::Set(set_args)) => {
|
||||||
cli::commands::credentials_set::execute(
|
cli::commands::credentials_set::execute(
|
||||||
set_args.cf_api_key.clone(),
|
set_args.cf_api_key,
|
||||||
set_args.modrinth_token.clone(),
|
set_args.modrinth_token,
|
||||||
set_args.gh_access_token.clone(),
|
set_args.gh_access_token,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
None => {
|
None => {
|
||||||
|
|
@ -136,34 +194,34 @@ async fn main() -> Result<(), PakkerError> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Commands::Cfg(args) => {
|
Commands::Cfg(args) => {
|
||||||
match &args.subcommand {
|
match args.subcommand {
|
||||||
Some(cli::CfgSubcommand::Prj(prj_args)) => {
|
Some(cli::CfgSubcommand::Prj(prj_args)) => {
|
||||||
cli::commands::cfg_prj::execute(
|
cli::commands::cfg_prj::execute(
|
||||||
&config_path,
|
&config_path,
|
||||||
&lockfile_path,
|
&lockfile_path,
|
||||||
prj_args.project.clone(),
|
prj_args.project,
|
||||||
prj_args.r#type.as_deref(),
|
prj_args.r#type.as_deref(),
|
||||||
prj_args.side.as_deref(),
|
prj_args.side.as_deref(),
|
||||||
prj_args.update_strategy.as_deref(),
|
prj_args.update_strategy.as_deref(),
|
||||||
prj_args.redistributable,
|
prj_args.redistributable,
|
||||||
prj_args.subpath.clone(),
|
prj_args.subpath,
|
||||||
prj_args.add_alias.clone(),
|
prj_args.add_alias,
|
||||||
prj_args.remove_alias.clone(),
|
prj_args.remove_alias,
|
||||||
prj_args.export,
|
prj_args.export,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
None => {
|
None => {
|
||||||
cli::commands::cfg::execute(
|
cli::commands::cfg::execute(
|
||||||
&config_path,
|
&config_path,
|
||||||
args.name.clone(),
|
args.name,
|
||||||
args.version.clone(),
|
args.version,
|
||||||
args.description.clone(),
|
args.description,
|
||||||
args.author.clone(),
|
args.author,
|
||||||
args.mods_path.clone(),
|
args.mods_path,
|
||||||
args.resource_packs_path.clone(),
|
args.resource_packs_path,
|
||||||
args.data_packs_path.clone(),
|
args.data_packs_path,
|
||||||
args.worlds_path.clone(),
|
args.worlds_path,
|
||||||
args.shaders_path.clone(),
|
args.shaders_path,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,9 @@ pub struct Config {
|
||||||
rename = "exportServerSideProjectsToClient"
|
rename = "exportServerSideProjectsToClient"
|
||||||
)]
|
)]
|
||||||
pub export_server_side_projects_to_client: Option<bool>,
|
pub export_server_side_projects_to_client: Option<bool>,
|
||||||
|
/// Number of files to select per project (defaults to 1)
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub file_count_preference: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
|
|
@ -80,6 +83,7 @@ impl Default for Config {
|
||||||
projects: Some(HashMap::new()),
|
projects: Some(HashMap::new()),
|
||||||
export_profiles: None,
|
export_profiles: None,
|
||||||
export_server_side_projects_to_client: None,
|
export_server_side_projects_to_client: None,
|
||||||
|
file_count_preference: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -145,6 +149,7 @@ impl Config {
|
||||||
projects: Some(pakku.projects),
|
projects: Some(pakku.projects),
|
||||||
export_profiles: None,
|
export_profiles: None,
|
||||||
export_server_side_projects_to_client: None,
|
export_server_side_projects_to_client: None,
|
||||||
|
file_count_preference: None,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
Err(e) => Err(PakkerError::InvalidConfigFile(e.to_string())),
|
Err(e) => Err(PakkerError::InvalidConfigFile(e.to_string())),
|
||||||
|
|
@ -203,6 +208,7 @@ mod tests {
|
||||||
projects: None,
|
projects: None,
|
||||||
export_profiles: None,
|
export_profiles: None,
|
||||||
export_server_side_projects_to_client: None,
|
export_server_side_projects_to_client: None,
|
||||||
|
file_count_preference: None,
|
||||||
};
|
};
|
||||||
assert_eq!(config.name, "test-pack");
|
assert_eq!(config.name, "test-pack");
|
||||||
assert_eq!(config.version, "1.0.0");
|
assert_eq!(config.version, "1.0.0");
|
||||||
|
|
@ -224,6 +230,7 @@ mod tests {
|
||||||
projects: None,
|
projects: None,
|
||||||
export_profiles: None,
|
export_profiles: None,
|
||||||
export_server_side_projects_to_client: None,
|
export_server_side_projects_to_client: None,
|
||||||
|
file_count_preference: None,
|
||||||
};
|
};
|
||||||
config.description = Some("A test modpack".to_string());
|
config.description = Some("A test modpack".to_string());
|
||||||
config.author = Some("Test Author".to_string());
|
config.author = Some("Test Author".to_string());
|
||||||
|
|
|
||||||
|
|
@ -103,9 +103,13 @@ impl std::fmt::Display for UpdateStrategy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(
|
||||||
|
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize,
|
||||||
|
)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum ReleaseType {
|
pub enum ReleaseType {
|
||||||
|
// Order matters: Release < Beta < Alpha for sorting (we want Release first)
|
||||||
|
// But we want reverse order, so we'll use reverse() or handle in comparison
|
||||||
Release,
|
Release,
|
||||||
Beta,
|
Beta,
|
||||||
Alpha,
|
Alpha,
|
||||||
|
|
|
||||||
|
|
@ -20,13 +20,15 @@ mod tests {
|
||||||
let mut id_map = HashMap::new();
|
let mut id_map = HashMap::new();
|
||||||
id_map.insert("modrinth".to_string(), pakku_id.to_string());
|
id_map.insert("modrinth".to_string(), pakku_id.to_string());
|
||||||
|
|
||||||
|
let slug_map = name_map.clone();
|
||||||
|
|
||||||
Project {
|
Project {
|
||||||
pakku_id: Some(pakku_id.to_string()),
|
pakku_id: Some(pakku_id.to_string()),
|
||||||
pakku_links: HashSet::new(),
|
pakku_links: HashSet::new(),
|
||||||
r#type: ProjectType::Mod,
|
r#type: ProjectType::Mod,
|
||||||
side: ProjectSide::Both,
|
side: ProjectSide::Both,
|
||||||
slug: name_map.clone(),
|
slug: slug_map,
|
||||||
name: name_map.clone(),
|
name: name_map,
|
||||||
id: id_map,
|
id: id_map,
|
||||||
update_strategy: UpdateStrategy::Latest,
|
update_strategy: UpdateStrategy::Latest,
|
||||||
redistributable: true,
|
redistributable: true,
|
||||||
|
|
@ -74,11 +76,11 @@ mod tests {
|
||||||
|
|
||||||
lockfile.add_project(create_test_project("test-id", "test-slug"));
|
lockfile.add_project(create_test_project("test-id", "test-slug"));
|
||||||
|
|
||||||
let found = lockfile.find_project("test-id");
|
let found = lockfile.get_project("test-id");
|
||||||
assert!(found.is_some());
|
assert!(found.is_some());
|
||||||
assert_eq!(found.unwrap().pakku_id, Some("test-id".to_string()));
|
assert_eq!(found.unwrap().pakku_id, Some("test-id".to_string()));
|
||||||
|
|
||||||
let not_found = lockfile.find_project("nonexistent");
|
let not_found = lockfile.get_project("nonexistent");
|
||||||
assert!(not_found.is_none());
|
assert!(not_found.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -578,6 +580,49 @@ pub struct LockFile {
|
||||||
pub lockfile_version: u32,
|
pub lockfile_version: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl LockFile {
|
||||||
|
pub fn get_project(&self, pakku_id: &str) -> Option<&Project> {
|
||||||
|
self
|
||||||
|
.projects
|
||||||
|
.iter()
|
||||||
|
.find(|p| p.pakku_id.as_deref() == Some(pakku_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_loader_names(&self) -> Vec<String> {
|
||||||
|
self.loaders.keys().cloned().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_project(&mut self, pakku_id: &str) -> Option<Project> {
|
||||||
|
if let Some(pos) = self
|
||||||
|
.projects
|
||||||
|
.iter()
|
||||||
|
.position(|p| p.pakku_id.as_deref() == Some(pakku_id))
|
||||||
|
{
|
||||||
|
Some(self.projects.remove(pos))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_project_mut(&mut self, pakku_id: &str) -> Option<&mut Project> {
|
||||||
|
self
|
||||||
|
.projects
|
||||||
|
.iter_mut()
|
||||||
|
.find(|p| p.pakku_id.as_deref() == Some(pakku_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_project_by_platform_id(
|
||||||
|
&self,
|
||||||
|
platform: &str,
|
||||||
|
id: &str,
|
||||||
|
) -> Option<&Project> {
|
||||||
|
self
|
||||||
|
.projects
|
||||||
|
.iter()
|
||||||
|
.find(|p| p.id.get(platform).is_some_and(|pid| pid == id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl LockFile {
|
impl LockFile {
|
||||||
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
|
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||||
Self::load_with_validation(path, true)
|
Self::load_with_validation(path, true)
|
||||||
|
|
@ -720,52 +765,4 @@ impl LockFile {
|
||||||
self.projects.push(project);
|
self.projects.push(project);
|
||||||
self.projects.sort_by_key(super::project::Project::get_name);
|
self.projects.sort_by_key(super::project::Project::get_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_project(&self, pakku_id: &str) -> Option<&Project> {
|
|
||||||
self
|
|
||||||
.projects
|
|
||||||
.iter()
|
|
||||||
.find(|p| p.pakku_id.as_deref() == Some(pakku_id))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_loader_names(&self) -> Vec<String> {
|
|
||||||
self.loaders.keys().cloned().collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove_project(&mut self, pakku_id: &str) -> Option<Project> {
|
|
||||||
if let Some(pos) = self
|
|
||||||
.projects
|
|
||||||
.iter()
|
|
||||||
.position(|p| p.pakku_id.as_deref() == Some(pakku_id))
|
|
||||||
{
|
|
||||||
Some(self.projects.remove(pos))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn find_project(&self, pakku_id: &str) -> Option<&Project> {
|
|
||||||
self
|
|
||||||
.projects
|
|
||||||
.iter()
|
|
||||||
.find(|p| p.pakku_id.as_deref() == Some(pakku_id))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn find_project_mut(&mut self, pakku_id: &str) -> Option<&mut Project> {
|
|
||||||
self
|
|
||||||
.projects
|
|
||||||
.iter_mut()
|
|
||||||
.find(|p| p.pakku_id.as_deref() == Some(pakku_id))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn find_project_by_platform_id(
|
|
||||||
&self,
|
|
||||||
platform: &str,
|
|
||||||
id: &str,
|
|
||||||
) -> Option<&Project> {
|
|
||||||
self
|
|
||||||
.projects
|
|
||||||
.iter()
|
|
||||||
.find(|p| p.id.get(platform).is_some_and(|pid| pid == id))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,12 +91,13 @@ impl Project {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_name(&self) -> String {
|
pub fn get_name(&self) -> String {
|
||||||
self.name.values().next().cloned().unwrap_or_else(|| {
|
self
|
||||||
self
|
.name
|
||||||
.pakku_id
|
.values()
|
||||||
.clone()
|
.next()
|
||||||
.unwrap_or_else(|| "unknown".to_string())
|
.map(|s| s.to_owned())
|
||||||
})
|
.or_else(|| self.pakku_id.as_ref().map(|s| s.to_owned()))
|
||||||
|
.unwrap_or_else(|| "unknown".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn matches_input(&self, input: &str) -> bool {
|
pub fn matches_input(&self, input: &str) -> bool {
|
||||||
|
|
@ -145,10 +146,10 @@ impl Project {
|
||||||
pub fn merge(&mut self, other: Self) {
|
pub fn merge(&mut self, other: Self) {
|
||||||
// Merge platform identifiers
|
// Merge platform identifiers
|
||||||
for (platform, id) in other.id {
|
for (platform, id) in other.id {
|
||||||
self.id.entry(platform.clone()).or_insert(id);
|
self.id.entry(platform).or_insert(id);
|
||||||
}
|
}
|
||||||
for (platform, slug) in other.slug {
|
for (platform, slug) in other.slug {
|
||||||
self.slug.entry(platform.clone()).or_insert(slug);
|
self.slug.entry(platform).or_insert(slug);
|
||||||
}
|
}
|
||||||
for (platform, name) in other.name {
|
for (platform, name) in other.name {
|
||||||
self.name.entry(platform).or_insert(name);
|
self.name.entry(platform).or_insert(name);
|
||||||
|
|
@ -192,15 +193,29 @@ impl Project {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if all providers have the same latest file name
|
// Compare semantic versions extracted from file names
|
||||||
// (simplified check - in reality would compare semantic versions)
|
let parse_version = |name: &str| {
|
||||||
let file_names: Vec<_> = versions_by_provider
|
// Try to extract version from patterns like "mod-1.0.0.jar" or
|
||||||
|
// "mod_v1.0.0"
|
||||||
|
let version_str = name
|
||||||
|
.rsplit_once('-')
|
||||||
|
.and_then(|(_, v)| v.strip_suffix(".jar"))
|
||||||
|
.or_else(|| {
|
||||||
|
name
|
||||||
|
.rsplit_once('_')
|
||||||
|
.and_then(|(_, v)| v.strip_suffix(".jar"))
|
||||||
|
})
|
||||||
|
.unwrap_or(name);
|
||||||
|
semver::Version::parse(version_str).ok()
|
||||||
|
};
|
||||||
|
|
||||||
|
let versions: Vec<_> = versions_by_provider
|
||||||
.values()
|
.values()
|
||||||
.filter_map(|files| files.first().copied())
|
.filter_map(|files| files.first().copied().and_then(parse_version))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// All file names should be the same for versions to match
|
// All versions should be the same
|
||||||
file_names.windows(2).all(|w| w[0] == w[1])
|
versions.windows(2).all(|w| w[0] == w[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if versions do NOT match across providers.
|
/// Check if versions do NOT match across providers.
|
||||||
|
|
@ -231,6 +246,7 @@ impl Project {
|
||||||
&mut self,
|
&mut self,
|
||||||
mc_versions: &[String],
|
mc_versions: &[String],
|
||||||
loaders: &[String],
|
loaders: &[String],
|
||||||
|
file_count: Option<usize>,
|
||||||
) -> crate::error::Result<()> {
|
) -> crate::error::Result<()> {
|
||||||
// Filter compatible files
|
// Filter compatible files
|
||||||
let compatible_files: Vec<_> = self
|
let compatible_files: Vec<_> = self
|
||||||
|
|
@ -246,27 +262,17 @@ impl Project {
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by release type (release > beta > alpha) and date
|
// Sort by release type (Release < Beta < Alpha) and date (newest first)
|
||||||
let mut sorted_files = compatible_files.clone();
|
let mut sorted_files = compatible_files.to_vec();
|
||||||
sorted_files.sort_by(|a, b| {
|
sorted_files.sort_by(|a, b| {
|
||||||
use super::enums::ReleaseType;
|
a.release_type
|
||||||
let type_order = |rt: &ReleaseType| {
|
.cmp(&b.release_type)
|
||||||
match rt {
|
|
||||||
ReleaseType::Release => 0,
|
|
||||||
ReleaseType::Beta => 1,
|
|
||||||
ReleaseType::Alpha => 2,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
type_order(&a.release_type)
|
|
||||||
.cmp(&type_order(&b.release_type))
|
|
||||||
.then_with(|| b.date_published.cmp(&a.date_published))
|
.then_with(|| b.date_published.cmp(&a.date_published))
|
||||||
});
|
});
|
||||||
|
|
||||||
// Keep only the best file
|
// Keep the specified number of files (default to 1 if not specified)
|
||||||
if let Some(best_file) = sorted_files.first() {
|
let count = file_count.unwrap_or(1);
|
||||||
self.files = vec![(*best_file).clone()];
|
self.files = sorted_files.into_iter().take(count).cloned().collect();
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -525,7 +531,7 @@ mod tests {
|
||||||
let lockfile_mc = vec!["1.20.1".to_string()];
|
let lockfile_mc = vec!["1.20.1".to_string()];
|
||||||
let lockfile_loaders = vec!["fabric".to_string()];
|
let lockfile_loaders = vec!["fabric".to_string()];
|
||||||
|
|
||||||
let result = project.select_file(&lockfile_mc, &lockfile_loaders);
|
let result = project.select_file(&lockfile_mc, &lockfile_loaders, None);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,19 @@ use std::sync::Arc;
|
||||||
pub use curseforge::CurseForgePlatform;
|
pub use curseforge::CurseForgePlatform;
|
||||||
pub use github::GitHubPlatform;
|
pub use github::GitHubPlatform;
|
||||||
pub use modrinth::ModrinthPlatform;
|
pub use modrinth::ModrinthPlatform;
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
pub use traits::PlatformClient;
|
pub use traits::PlatformClient;
|
||||||
|
|
||||||
use crate::{error::Result, rate_limiter::RateLimiter};
|
use crate::{error::Result, http, rate_limiter::RateLimiter};
|
||||||
|
|
||||||
static RATE_LIMITER: Lazy<Arc<RateLimiter>> =
|
static HTTP_CLIENT: std::sync::LazyLock<Arc<reqwest::Client>> =
|
||||||
Lazy::new(|| Arc::new(RateLimiter::new(None)));
|
std::sync::LazyLock::new(|| Arc::new(http::create_http_client()));
|
||||||
|
|
||||||
|
static RATE_LIMITER: std::sync::LazyLock<Arc<RateLimiter>> =
|
||||||
|
std::sync::LazyLock::new(|| Arc::new(RateLimiter::new(None)));
|
||||||
|
|
||||||
|
pub fn get_http_client() -> Arc<reqwest::Client> {
|
||||||
|
HTTP_CLIENT.clone()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn create_platform(
|
pub fn create_platform(
|
||||||
platform: &str,
|
platform: &str,
|
||||||
|
|
@ -34,9 +40,21 @@ fn create_client(
|
||||||
api_key: Option<String>,
|
api_key: Option<String>,
|
||||||
) -> Result<Box<dyn PlatformClient>> {
|
) -> Result<Box<dyn PlatformClient>> {
|
||||||
match platform {
|
match platform {
|
||||||
"modrinth" => Ok(Box::new(ModrinthPlatform::new())),
|
"modrinth" => {
|
||||||
"curseforge" => Ok(Box::new(CurseForgePlatform::new(api_key))),
|
Ok(Box::new(ModrinthPlatform::with_client(get_http_client())))
|
||||||
"github" => Ok(Box::new(GitHubPlatform::new(api_key))),
|
},
|
||||||
|
"curseforge" => {
|
||||||
|
Ok(Box::new(CurseForgePlatform::with_client(
|
||||||
|
get_http_client(),
|
||||||
|
api_key,
|
||||||
|
)))
|
||||||
|
},
|
||||||
|
"github" => {
|
||||||
|
Ok(Box::new(GitHubPlatform::with_client(
|
||||||
|
get_http_client(),
|
||||||
|
api_key,
|
||||||
|
)))
|
||||||
|
},
|
||||||
_ => {
|
_ => {
|
||||||
Err(crate::error::PakkerError::ConfigError(format!(
|
Err(crate::error::PakkerError::ConfigError(format!(
|
||||||
"Unknown platform: {platform}"
|
"Unknown platform: {platform}"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use std::collections::HashMap;
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
|
@ -12,21 +12,30 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
const CURSEFORGE_API_BASE: &str = "https://api.curseforge.com/v1";
|
const CURSEFORGE_API_BASE: &str = "https://api.curseforge.com/v1";
|
||||||
|
/// CurseForge game version type ID for loader versions (e.g., "fabric",
|
||||||
|
/// "forge")
|
||||||
const LOADER_VERSION_TYPE_ID: i32 = 68441;
|
const LOADER_VERSION_TYPE_ID: i32 = 68441;
|
||||||
|
/// CurseForge relation type ID for "required dependency" (mod embeds or
|
||||||
|
/// requires another mod)
|
||||||
|
const DEPENDENCY_RELATION_TYPE_REQUIRED: u32 = 3;
|
||||||
|
|
||||||
pub struct CurseForgePlatform {
|
pub struct CurseForgePlatform {
|
||||||
client: Client,
|
client: Arc<Client>,
|
||||||
api_key: Option<String>,
|
api_key: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CurseForgePlatform {
|
impl CurseForgePlatform {
|
||||||
pub fn new(api_key: Option<String>) -> Self {
|
pub fn new(api_key: Option<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
client: Client::new(),
|
client: Arc::new(Client::new()),
|
||||||
api_key,
|
api_key,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_client(client: Arc<Client>, api_key: Option<String>) -> Self {
|
||||||
|
Self { client, api_key }
|
||||||
|
}
|
||||||
|
|
||||||
fn get_headers(&self) -> Result<reqwest::header::HeaderMap> {
|
fn get_headers(&self) -> Result<reqwest::header::HeaderMap> {
|
||||||
let mut headers = reqwest::header::HeaderMap::new();
|
let mut headers = reqwest::header::HeaderMap::new();
|
||||||
|
|
||||||
|
|
@ -66,11 +75,81 @@ impl CurseForgePlatform {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Determine project side based on `CurseForge` categories.
|
||||||
|
/// `CurseForge` doesn't have explicit client/server fields like Modrinth,
|
||||||
|
/// so we infer from category names and IDs.
|
||||||
|
fn detect_side_from_categories(
|
||||||
|
categories: &[CurseForgeCategory],
|
||||||
|
) -> ProjectSide {
|
||||||
|
// Known client-only category indicators (slugs and partial name matches)
|
||||||
|
const CLIENT_INDICATORS: &[&str] = &[
|
||||||
|
"client",
|
||||||
|
"hud",
|
||||||
|
"gui",
|
||||||
|
"cosmetic",
|
||||||
|
"shader",
|
||||||
|
"optifine",
|
||||||
|
"resource-pack",
|
||||||
|
"texture",
|
||||||
|
"minimap",
|
||||||
|
"tooltip",
|
||||||
|
"inventory",
|
||||||
|
"quality-of-life", // Often client-side QoL
|
||||||
|
];
|
||||||
|
|
||||||
|
// Known server-only category indicators
|
||||||
|
const SERVER_INDICATORS: &[&str] = &[
|
||||||
|
"server-utility",
|
||||||
|
"bukkit",
|
||||||
|
"spigot",
|
||||||
|
"paper",
|
||||||
|
"admin-tools",
|
||||||
|
"anti-grief",
|
||||||
|
"economy",
|
||||||
|
"permissions",
|
||||||
|
"chat",
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut client_score = 0;
|
||||||
|
let mut server_score = 0;
|
||||||
|
|
||||||
|
for category in categories {
|
||||||
|
let slug_lower = category.slug.to_lowercase();
|
||||||
|
let name_lower = category.name.to_lowercase();
|
||||||
|
|
||||||
|
for indicator in CLIENT_INDICATORS {
|
||||||
|
if slug_lower.contains(indicator) || name_lower.contains(indicator) {
|
||||||
|
client_score += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for indicator in SERVER_INDICATORS {
|
||||||
|
if slug_lower.contains(indicator) || name_lower.contains(indicator) {
|
||||||
|
server_score += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only assign a specific side if there's clear indication
|
||||||
|
// and not conflicting signals
|
||||||
|
if client_score > 0 && server_score == 0 {
|
||||||
|
ProjectSide::Client
|
||||||
|
} else if server_score > 0 && client_score == 0 {
|
||||||
|
ProjectSide::Server
|
||||||
|
} else {
|
||||||
|
// Default to Both - works on both client and server
|
||||||
|
ProjectSide::Both
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn convert_project(&self, cf_project: CurseForgeProject) -> Project {
|
fn convert_project(&self, cf_project: CurseForgeProject) -> Project {
|
||||||
let pakku_id = generate_pakku_id();
|
let pakku_id = generate_pakku_id();
|
||||||
let project_type = Self::map_class_id(cf_project.class_id.unwrap_or(6));
|
let project_type = Self::map_class_id(cf_project.class_id.unwrap_or(6));
|
||||||
|
|
||||||
let mut project = Project::new(pakku_id, project_type, ProjectSide::Both);
|
// Detect side from categories
|
||||||
|
let side = Self::detect_side_from_categories(&cf_project.categories);
|
||||||
|
|
||||||
|
let mut project = Project::new(pakku_id, project_type, side);
|
||||||
|
|
||||||
project.add_platform(
|
project.add_platform(
|
||||||
"curseforge".to_string(),
|
"curseforge".to_string(),
|
||||||
|
|
@ -124,7 +203,7 @@ impl CurseForgePlatform {
|
||||||
required_dependencies: cf_file
|
required_dependencies: cf_file
|
||||||
.dependencies
|
.dependencies
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|d| d.relation_type == 3)
|
.filter(|d| d.relation_type == DEPENDENCY_RELATION_TYPE_REQUIRED)
|
||||||
.map(|d| d.mod_id.to_string())
|
.map(|d| d.mod_id.to_string())
|
||||||
.collect(),
|
.collect(),
|
||||||
size: cf_file.file_length,
|
size: cf_file.file_length,
|
||||||
|
|
@ -317,11 +396,20 @@ impl PlatformClient for CurseForgePlatform {
|
||||||
// CurseForge API models
|
// CurseForge API models
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
struct CurseForgeProject {
|
struct CurseForgeProject {
|
||||||
id: u32,
|
id: u32,
|
||||||
name: String,
|
name: String,
|
||||||
slug: String,
|
slug: String,
|
||||||
#[serde(rename = "classId")]
|
#[serde(rename = "classId")]
|
||||||
class_id: Option<u32>,
|
class_id: Option<u32>,
|
||||||
|
#[serde(default)]
|
||||||
|
categories: Vec<CurseForgeCategory>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
struct CurseForgeCategory {
|
||||||
|
id: u32,
|
||||||
|
name: String,
|
||||||
|
slug: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
|
@ -381,3 +469,112 @@ struct CurseForgeFilesResponse {
|
||||||
struct CurseForgeSearchResponse {
|
struct CurseForgeSearchResponse {
|
||||||
data: Vec<CurseForgeProject>,
|
data: Vec<CurseForgeProject>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn make_category(id: u32, name: &str, slug: &str) -> CurseForgeCategory {
|
||||||
|
CurseForgeCategory {
|
||||||
|
id,
|
||||||
|
name: name.to_string(),
|
||||||
|
slug: slug.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_detect_side_client_only() {
|
||||||
|
// HUD mod should be client-only
|
||||||
|
let categories = vec![
|
||||||
|
make_category(1, "HUD Mods", "hud"),
|
||||||
|
make_category(2, "Fabric", "fabric"),
|
||||||
|
];
|
||||||
|
let side = CurseForgePlatform::detect_side_from_categories(&categories);
|
||||||
|
assert_eq!(side, ProjectSide::Client);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_detect_side_server_only() {
|
||||||
|
// Server utility should be server-only
|
||||||
|
let categories = vec![
|
||||||
|
make_category(1, "Server Utility", "server-utility"),
|
||||||
|
make_category(2, "Bukkit Plugins", "bukkit"),
|
||||||
|
];
|
||||||
|
let side = CurseForgePlatform::detect_side_from_categories(&categories);
|
||||||
|
assert_eq!(side, ProjectSide::Server);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_detect_side_both() {
|
||||||
|
// Generic mod categories should be both
|
||||||
|
let categories = vec![
|
||||||
|
make_category(1, "Technology", "technology"),
|
||||||
|
make_category(2, "Fabric", "fabric"),
|
||||||
|
];
|
||||||
|
let side = CurseForgePlatform::detect_side_from_categories(&categories);
|
||||||
|
assert_eq!(side, ProjectSide::Both);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_detect_side_conflicting_signals() {
|
||||||
|
// Mixed categories should default to both
|
||||||
|
let categories = vec![
|
||||||
|
make_category(1, "Client HUD", "client-hud"),
|
||||||
|
make_category(2, "Server Utility", "server-utility"),
|
||||||
|
];
|
||||||
|
let side = CurseForgePlatform::detect_side_from_categories(&categories);
|
||||||
|
assert_eq!(side, ProjectSide::Both);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_detect_side_empty_categories() {
|
||||||
|
let categories = vec![];
|
||||||
|
let side = CurseForgePlatform::detect_side_from_categories(&categories);
|
||||||
|
assert_eq!(side, ProjectSide::Both);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_detect_side_gui_client() {
|
||||||
|
let categories =
|
||||||
|
vec![make_category(1, "GUI Enhancement", "gui-enhancement")];
|
||||||
|
let side = CurseForgePlatform::detect_side_from_categories(&categories);
|
||||||
|
assert_eq!(side, ProjectSide::Client);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_detect_side_permissions_server() {
|
||||||
|
let categories = vec![make_category(1, "Permissions", "permissions")];
|
||||||
|
let side = CurseForgePlatform::detect_side_from_categories(&categories);
|
||||||
|
assert_eq!(side, ProjectSide::Server);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_map_class_id() {
|
||||||
|
assert_eq!(CurseForgePlatform::map_class_id(6), ProjectType::Mod);
|
||||||
|
assert_eq!(
|
||||||
|
CurseForgePlatform::map_class_id(12),
|
||||||
|
ProjectType::ResourcePack
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
CurseForgePlatform::map_class_id(6945),
|
||||||
|
ProjectType::DataPack
|
||||||
|
);
|
||||||
|
assert_eq!(CurseForgePlatform::map_class_id(6552), ProjectType::Shader);
|
||||||
|
assert_eq!(CurseForgePlatform::map_class_id(17), ProjectType::World);
|
||||||
|
assert_eq!(CurseForgePlatform::map_class_id(9999), ProjectType::Mod); // Unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_map_release_type() {
|
||||||
|
assert_eq!(
|
||||||
|
CurseForgePlatform::map_release_type(1),
|
||||||
|
ReleaseType::Release
|
||||||
|
);
|
||||||
|
assert_eq!(CurseForgePlatform::map_release_type(2), ReleaseType::Beta);
|
||||||
|
assert_eq!(CurseForgePlatform::map_release_type(3), ReleaseType::Alpha);
|
||||||
|
assert_eq!(
|
||||||
|
CurseForgePlatform::map_release_type(99),
|
||||||
|
ReleaseType::Release
|
||||||
|
); // Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use std::collections::HashMap;
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
@ -20,9 +20,9 @@ pub struct GitHubPlatform {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GitHubPlatform {
|
impl GitHubPlatform {
|
||||||
pub fn new(token: Option<String>) -> Self {
|
pub fn with_client(client: Arc<Client>, token: Option<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
client: Client::new(),
|
client: (*client).clone(),
|
||||||
token,
|
token,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use std::collections::HashMap;
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
|
@ -14,16 +14,76 @@ use crate::{
|
||||||
const MODRINTH_API_BASE: &str = "https://api.modrinth.com/v2";
|
const MODRINTH_API_BASE: &str = "https://api.modrinth.com/v2";
|
||||||
|
|
||||||
pub struct ModrinthPlatform {
|
pub struct ModrinthPlatform {
|
||||||
client: Client,
|
client: Arc<Client>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModrinthPlatform {
|
impl ModrinthPlatform {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
client: Client::new(),
|
client: Arc::new(Client::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_client(client: Arc<Client>) -> Self {
|
||||||
|
Self { client }
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn request_project_url(&self, url: &str) -> Result<Project> {
|
||||||
|
let response = self.client.get(url).send().await?;
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(PakkerError::ProjectNotFound(url.to_string()));
|
||||||
|
}
|
||||||
|
let mr_project: ModrinthProject = response.json().await?;
|
||||||
|
Ok(self.convert_project(mr_project))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn request_project_files_url(
|
||||||
|
&self,
|
||||||
|
url: &str,
|
||||||
|
) -> Result<Vec<ProjectFile>> {
|
||||||
|
let response = self.client.get(url).send().await?;
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(PakkerError::ProjectNotFound(url.to_string()));
|
||||||
|
}
|
||||||
|
let mr_versions: Vec<ModrinthVersion> = response.json().await?;
|
||||||
|
let project_id = url
|
||||||
|
.split('/')
|
||||||
|
.nth(4)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
PakkerError::InvalidResponse(
|
||||||
|
"Cannot parse project ID from URL".to_string(),
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.to_string();
|
||||||
|
Ok(
|
||||||
|
mr_versions
|
||||||
|
.into_iter()
|
||||||
|
.map(|v| self.convert_version(v, &project_id))
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn lookup_by_hash_url(&self, url: &str) -> Result<Option<Project>> {
|
||||||
|
let response = self.client.get(url).send().await?;
|
||||||
|
if response.status().as_u16() == 404 {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(PakkerError::PlatformApiError(format!(
|
||||||
|
"Modrinth API error: {}",
|
||||||
|
response.status()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let version_data: serde_json::Value = response.json().await?;
|
||||||
|
let project_id = version_data["project_id"].as_str().ok_or_else(|| {
|
||||||
|
PakkerError::InvalidResponse("Missing project_id".to_string())
|
||||||
|
})?;
|
||||||
|
self
|
||||||
|
.request_project_with_files(project_id, &[], &[])
|
||||||
|
.await
|
||||||
|
.map(Some)
|
||||||
|
}
|
||||||
|
|
||||||
fn map_project_type(type_str: &str) -> ProjectType {
|
fn map_project_type(type_str: &str) -> ProjectType {
|
||||||
match type_str {
|
match type_str {
|
||||||
"mod" => ProjectType::Mod,
|
"mod" => ProjectType::Mod,
|
||||||
|
|
@ -123,15 +183,7 @@ impl PlatformClient for ModrinthPlatform {
|
||||||
_loaders: &[String],
|
_loaders: &[String],
|
||||||
) -> Result<Project> {
|
) -> Result<Project> {
|
||||||
let url = format!("{MODRINTH_API_BASE}/project/{identifier}");
|
let url = format!("{MODRINTH_API_BASE}/project/{identifier}");
|
||||||
|
self.request_project_url(&url).await
|
||||||
let response = self.client.get(&url).send().await?;
|
|
||||||
|
|
||||||
if !response.status().is_success() {
|
|
||||||
return Err(PakkerError::ProjectNotFound(identifier.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mr_project: ModrinthProject = response.json().await?;
|
|
||||||
Ok(self.convert_project(mr_project))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn request_project_files(
|
async fn request_project_files(
|
||||||
|
|
@ -170,20 +222,7 @@ impl PlatformClient for ModrinthPlatform {
|
||||||
url.push_str(¶ms.join("&"));
|
url.push_str(¶ms.join("&"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = self.client.get(&url).send().await?;
|
self.request_project_files_url(&url).await
|
||||||
|
|
||||||
if !response.status().is_success() {
|
|
||||||
return Err(PakkerError::ProjectNotFound(project_id.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mr_versions: Vec<ModrinthVersion> = response.json().await?;
|
|
||||||
|
|
||||||
Ok(
|
|
||||||
mr_versions
|
|
||||||
.into_iter()
|
|
||||||
.map(|v| self.convert_version(v, project_id))
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn request_project_with_files(
|
async fn request_project_with_files(
|
||||||
|
|
@ -213,30 +252,7 @@ impl PlatformClient for ModrinthPlatform {
|
||||||
async fn lookup_by_hash(&self, hash: &str) -> Result<Option<Project>> {
|
async fn lookup_by_hash(&self, hash: &str) -> Result<Option<Project>> {
|
||||||
// Modrinth uses SHA-1 hash for file lookups
|
// Modrinth uses SHA-1 hash for file lookups
|
||||||
let url = format!("{MODRINTH_API_BASE}/version_file/{hash}");
|
let url = format!("{MODRINTH_API_BASE}/version_file/{hash}");
|
||||||
|
self.lookup_by_hash_url(&url).await
|
||||||
let response = self.client.get(&url).send().await?;
|
|
||||||
|
|
||||||
if response.status().as_u16() == 404 {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
if !response.status().is_success() {
|
|
||||||
return Err(PakkerError::PlatformApiError(format!(
|
|
||||||
"Modrinth API error: {}",
|
|
||||||
response.status()
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let version_data: serde_json::Value = response.json().await?;
|
|
||||||
|
|
||||||
let project_id = version_data["project_id"].as_str().ok_or_else(|| {
|
|
||||||
PakkerError::InvalidResponse("Missing project_id".to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
self
|
|
||||||
.request_project_with_files(project_id, &[], &[])
|
|
||||||
.await
|
|
||||||
.map(Some)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -280,3 +296,128 @@ struct ModrinthDependency {
|
||||||
project_id: Option<String>,
|
project_id: Option<String>,
|
||||||
dependency_type: String,
|
dependency_type: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use reqwest::Client;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl ModrinthPlatform {
|
||||||
|
fn with_raw_client(client: Client) -> Self {
|
||||||
|
Self {
|
||||||
|
client: Arc::new(client),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_platform_with_mock()
|
||||||
|
-> (ModrinthPlatform, mockito::ServerGuard) {
|
||||||
|
let server = mockito::Server::new_async().await;
|
||||||
|
let client = Client::new();
|
||||||
|
let platform = ModrinthPlatform::with_raw_client(client);
|
||||||
|
(platform, server)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_request_project_success() {
|
||||||
|
let (platform, mut server) = create_platform_with_mock().await;
|
||||||
|
let url = format!("{}/project/test-mod", server.url());
|
||||||
|
|
||||||
|
let _mock = server
|
||||||
|
.mock("GET", "/project/test-mod")
|
||||||
|
.with_status(200)
|
||||||
|
.with_header("content-type", "application/json")
|
||||||
|
.with_body(
|
||||||
|
r#"{
|
||||||
|
"id": "abc123",
|
||||||
|
"slug": "test-mod",
|
||||||
|
"title": "Test Mod",
|
||||||
|
"project_type": "mod",
|
||||||
|
"client_side": "required",
|
||||||
|
"server_side": "required"
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let result = platform.request_project_url(&url).await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let project = result.unwrap();
|
||||||
|
assert!(project.get_platform_id("modrinth").is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_request_project_not_found() {
|
||||||
|
let (platform, mut server) = create_platform_with_mock().await;
|
||||||
|
let url = format!("{}/project/nonexistent", server.url());
|
||||||
|
|
||||||
|
let _mock = server
|
||||||
|
.mock("GET", "/project/nonexistent")
|
||||||
|
.with_status(404)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let result = platform.request_project_url(&url).await;
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_request_project_files() {
|
||||||
|
let (platform, mut server) = create_platform_with_mock().await;
|
||||||
|
let url = format!("{}/project/abc123/version", server.url());
|
||||||
|
|
||||||
|
let _mock = server
|
||||||
|
.mock("GET", "/project/abc123/version")
|
||||||
|
.with_status(200)
|
||||||
|
.with_header("content-type", "application/json")
|
||||||
|
.with_body(
|
||||||
|
r#"[
|
||||||
|
{
|
||||||
|
"id": "v1",
|
||||||
|
"project_id": "abc123",
|
||||||
|
"name": "Test Mod v1.0.0",
|
||||||
|
"version_number": "1.0.0",
|
||||||
|
"game_versions": ["1.20.1"],
|
||||||
|
"version_type": "release",
|
||||||
|
"loaders": ["fabric"],
|
||||||
|
"date_published": "2024-01-01T00:00:00Z",
|
||||||
|
"files": [{
|
||||||
|
"hashes": {"sha1": "abc123def456"},
|
||||||
|
"url": "https://example.com/mod.jar",
|
||||||
|
"filename": "test-mod-1.0.0.jar",
|
||||||
|
"primary": true,
|
||||||
|
"size": 1024
|
||||||
|
}],
|
||||||
|
"dependencies": []
|
||||||
|
}
|
||||||
|
]"#,
|
||||||
|
)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let result = platform.request_project_files_url(&url).await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let files = result.unwrap();
|
||||||
|
assert_eq!(files.len(), 1);
|
||||||
|
assert_eq!(files[0].file_name, "test-mod-1.0.0.jar");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_lookup_by_hash_not_found() {
|
||||||
|
let (platform, mut server) = create_platform_with_mock().await;
|
||||||
|
let url = format!("{}/version_file/unknownhash123", server.url());
|
||||||
|
|
||||||
|
let _mock = server
|
||||||
|
.mock("GET", "/version_file/unknownhash123")
|
||||||
|
.with_status(404)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let result = platform.lookup_by_hash_url(&url).await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert!(result.unwrap().is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,46 +56,61 @@ impl RateLimiter {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn acquire(&self, platform: &str) -> Result<()> {
|
pub async fn acquire(&self, platform: &str) -> Result<()> {
|
||||||
let config = {
|
let (rate, burst) = {
|
||||||
let inner = self.inner.lock().await;
|
let inner = self.inner.lock().await;
|
||||||
inner.config.clone()
|
match platform.to_lowercase().as_str() {
|
||||||
};
|
"modrinth" => {
|
||||||
|
(
|
||||||
let (rate, burst) = match platform.to_lowercase().as_str() {
|
inner.config.modrinth_requests_per_min,
|
||||||
"modrinth" => (config.modrinth_requests_per_min, config.modrinth_burst),
|
inner.config.modrinth_burst,
|
||||||
"curseforge" => {
|
)
|
||||||
(config.curseforge_requests_per_min, config.curseforge_burst)
|
},
|
||||||
},
|
"curseforge" => {
|
||||||
"github" => (config.github_requests_per_min, config.github_burst),
|
(
|
||||||
_ => (config.default_requests_per_min, config.default_burst),
|
inner.config.curseforge_requests_per_min,
|
||||||
|
inner.config.curseforge_burst,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
"github" => {
|
||||||
|
(
|
||||||
|
inner.config.github_requests_per_min,
|
||||||
|
inner.config.github_burst,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
(
|
||||||
|
inner.config.default_requests_per_min,
|
||||||
|
inner.config.default_burst,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let interval = Duration::from_secs(60) / rate.max(1);
|
let interval = Duration::from_secs(60) / rate.max(1);
|
||||||
|
|
||||||
let mut inner = self.inner.lock().await;
|
loop {
|
||||||
let now = Instant::now();
|
let mut inner = self.inner.lock().await;
|
||||||
let platform_requests =
|
let now = Instant::now();
|
||||||
inner.requests.entry(platform.to_string()).or_default();
|
let platform_requests =
|
||||||
|
inner.requests.entry(platform.to_owned()).or_default();
|
||||||
|
|
||||||
platform_requests
|
platform_requests
|
||||||
.retain(|t| now.duration_since(*t) < Duration::from_secs(60));
|
.retain(|t| now.duration_since(*t) < Duration::from_secs(60));
|
||||||
|
|
||||||
if platform_requests.len() >= burst as usize {
|
if platform_requests.len() >= burst as usize
|
||||||
if let Some(oldest) = platform_requests.first() {
|
&& let Some(oldest) = platform_requests.first()
|
||||||
|
{
|
||||||
let wait_time = interval.saturating_sub(now.duration_since(*oldest));
|
let wait_time = interval.saturating_sub(now.duration_since(*oldest));
|
||||||
if wait_time > Duration::ZERO {
|
if wait_time > Duration::ZERO {
|
||||||
drop(inner);
|
drop(inner);
|
||||||
tokio::time::sleep(wait_time).await;
|
tokio::time::sleep(wait_time).await;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
platform_requests.push(Instant::now());
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut inner = self.inner.lock().await;
|
|
||||||
let platform_requests =
|
|
||||||
inner.requests.entry(platform.to_string()).or_default();
|
|
||||||
platform_requests.push(Instant::now());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn wait_for(&self, platform: &str) {
|
pub async fn wait_for(&self, platform: &str) {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
use dialoguer::{Confirm, Input, MultiSelect, Select, theme::ColorfulTheme};
|
use dialoguer::{Confirm, Input, Select, theme::ColorfulTheme};
|
||||||
|
|
||||||
/// Creates a terminal hyperlink using OSC 8 escape sequence
|
/// Creates a terminal hyperlink using OSC 8 escape sequence
|
||||||
/// Format: \x1b]8;;<URL>\x1b\\<TEXT>\x1b]8;;\x1b\\
|
/// Format: \x1b]8;;<URL>\x1b\\<TEXT>\x1b]8;;\x1b\\
|
||||||
|
|
@ -12,7 +12,16 @@ pub fn hyperlink(url: &str, text: &str) -> String {
|
||||||
|
|
||||||
/// Prompts user with a yes/no question
|
/// Prompts user with a yes/no question
|
||||||
/// Returns true for yes, false for no
|
/// Returns true for yes, false for no
|
||||||
pub fn prompt_yes_no(question: &str, default: bool) -> io::Result<bool> {
|
/// If `skip_prompts` is true, returns the default value without prompting
|
||||||
|
pub fn prompt_yes_no(
|
||||||
|
question: &str,
|
||||||
|
default: bool,
|
||||||
|
skip_prompts: bool,
|
||||||
|
) -> io::Result<bool> {
|
||||||
|
if skip_prompts {
|
||||||
|
return Ok(default);
|
||||||
|
}
|
||||||
|
|
||||||
Confirm::with_theme(&ColorfulTheme::default())
|
Confirm::with_theme(&ColorfulTheme::default())
|
||||||
.with_prompt(question)
|
.with_prompt(question)
|
||||||
.default(default)
|
.default(default)
|
||||||
|
|
@ -22,7 +31,6 @@ pub fn prompt_yes_no(question: &str, default: bool) -> io::Result<bool> {
|
||||||
|
|
||||||
/// Prompts user to select from a list of options
|
/// Prompts user to select from a list of options
|
||||||
/// Returns the index of the selected option
|
/// Returns the index of the selected option
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn prompt_select(question: &str, options: &[&str]) -> io::Result<usize> {
|
pub fn prompt_select(question: &str, options: &[&str]) -> io::Result<usize> {
|
||||||
Select::with_theme(&ColorfulTheme::default())
|
Select::with_theme(&ColorfulTheme::default())
|
||||||
.with_prompt(question)
|
.with_prompt(question)
|
||||||
|
|
@ -32,28 +40,12 @@ pub fn prompt_select(question: &str, options: &[&str]) -> io::Result<usize> {
|
||||||
.map_err(io::Error::other)
|
.map_err(io::Error::other)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prompts user to select multiple items from a list
|
|
||||||
/// Returns the indices of the selected options
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn prompt_multi_select(
|
|
||||||
question: &str,
|
|
||||||
options: &[&str],
|
|
||||||
) -> io::Result<Vec<usize>> {
|
|
||||||
MultiSelect::with_theme(&ColorfulTheme::default())
|
|
||||||
.with_prompt(question)
|
|
||||||
.items(options)
|
|
||||||
.interact()
|
|
||||||
.map_err(io::Error::other)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a formatted project URL for Modrinth
|
/// Creates a formatted project URL for Modrinth
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn modrinth_project_url(slug: &str) -> String {
|
pub fn modrinth_project_url(slug: &str) -> String {
|
||||||
format!("https://modrinth.com/mod/{slug}")
|
format!("https://modrinth.com/mod/{slug}")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a formatted project URL for `CurseForge`
|
/// Creates a formatted project URL for `CurseForge`
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn curseforge_project_url(project_id: &str) -> String {
|
pub fn curseforge_project_url(project_id: &str) -> String {
|
||||||
format!("https://www.curseforge.com/minecraft/mc-mods/{project_id}")
|
format!("https://www.curseforge.com/minecraft/mc-mods/{project_id}")
|
||||||
}
|
}
|
||||||
|
|
@ -118,16 +110,22 @@ pub fn suggest_similar<'a>(
|
||||||
|
|
||||||
/// Prompt user if they meant a similar project name.
|
/// Prompt user if they meant a similar project name.
|
||||||
/// Returns `Some(suggested_name)` if user confirms, None otherwise.
|
/// Returns `Some(suggested_name)` if user confirms, None otherwise.
|
||||||
|
/// If `skip_prompts` is true, automatically accepts the first suggestion.
|
||||||
pub fn prompt_typo_suggestion(
|
pub fn prompt_typo_suggestion(
|
||||||
input: &str,
|
input: &str,
|
||||||
candidates: &[String],
|
candidates: &[String],
|
||||||
|
skip_prompts: bool,
|
||||||
) -> io::Result<Option<String>> {
|
) -> io::Result<Option<String>> {
|
||||||
// Use a max distance based on input length for reasonable suggestions
|
// Use a max distance based on input length for reasonable suggestions
|
||||||
let max_distance = (input.len() / 2).clamp(2, 4);
|
let max_distance = (input.len() / 2).clamp(2, 4);
|
||||||
let suggestions = suggest_similar(input, candidates, max_distance);
|
let suggestions = suggest_similar(input, candidates, max_distance);
|
||||||
|
|
||||||
if let Some(first_suggestion) = suggestions.first()
|
if let Some(first_suggestion) = suggestions.first()
|
||||||
&& prompt_yes_no(&format!("Did you mean '{first_suggestion}'?"), true)?
|
&& prompt_yes_no(
|
||||||
|
&format!("Did you mean '{first_suggestion}'?"),
|
||||||
|
true,
|
||||||
|
skip_prompts,
|
||||||
|
)?
|
||||||
{
|
{
|
||||||
return Ok(Some((*first_suggestion).to_string()));
|
return Ok(Some((*first_suggestion).to_string()));
|
||||||
}
|
}
|
||||||
|
|
@ -164,7 +162,14 @@ pub fn prompt_input_optional(prompt: &str) -> io::Result<Option<String>> {
|
||||||
|
|
||||||
/// Prompt for `CurseForge` API key when authentication fails.
|
/// Prompt for `CurseForge` API key when authentication fails.
|
||||||
/// Returns the API key if provided, None if cancelled.
|
/// Returns the API key if provided, None if cancelled.
|
||||||
pub fn prompt_curseforge_api_key() -> io::Result<Option<String>> {
|
/// If `skip_prompts` is true, returns None immediately.
|
||||||
|
pub fn prompt_curseforge_api_key(
|
||||||
|
skip_prompts: bool,
|
||||||
|
) -> io::Result<Option<String>> {
|
||||||
|
if skip_prompts {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
use dialoguer::Password;
|
use dialoguer::Password;
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
|
|
@ -172,7 +177,7 @@ pub fn prompt_curseforge_api_key() -> io::Result<Option<String>> {
|
||||||
println!("Get your API key from: https://console.curseforge.com/");
|
println!("Get your API key from: https://console.curseforge.com/");
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
if !prompt_yes_no("Would you like to enter your API key now?", true)? {
|
if !prompt_yes_no("Would you like to enter your API key now?", true, false)? {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,58 +10,6 @@ use sha2::{Sha256, Sha512};
|
||||||
|
|
||||||
use crate::error::{PakkerError, Result};
|
use crate::error::{PakkerError, Result};
|
||||||
|
|
||||||
/// Compute Murmur2 hash (32-bit) for `CurseForge` fingerprinting
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn compute_murmur2_hash(data: &[u8]) -> u32 {
|
|
||||||
murmur2_hash(data, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Murmur2 hash implementation
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn murmur2_hash(data: &[u8], seed: u32) -> u32 {
|
|
||||||
const M: u32 = 0x5BD1E995;
|
|
||||||
const R: i32 = 24;
|
|
||||||
|
|
||||||
let mut h: u32 = seed ^ (data.len() as u32);
|
|
||||||
let mut chunks = data.chunks_exact(4);
|
|
||||||
|
|
||||||
for chunk in chunks.by_ref() {
|
|
||||||
let mut k = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
|
|
||||||
k = k.wrapping_mul(M);
|
|
||||||
k ^= k >> R;
|
|
||||||
k = k.wrapping_mul(M);
|
|
||||||
|
|
||||||
h = h.wrapping_mul(M);
|
|
||||||
h ^= k;
|
|
||||||
}
|
|
||||||
|
|
||||||
let remainder = chunks.remainder();
|
|
||||||
match remainder.len() {
|
|
||||||
3 => {
|
|
||||||
h ^= u32::from(remainder[2]) << 16;
|
|
||||||
h ^= u32::from(remainder[1]) << 8;
|
|
||||||
h ^= u32::from(remainder[0]);
|
|
||||||
h = h.wrapping_mul(M);
|
|
||||||
},
|
|
||||||
2 => {
|
|
||||||
h ^= u32::from(remainder[1]) << 8;
|
|
||||||
h ^= u32::from(remainder[0]);
|
|
||||||
h = h.wrapping_mul(M);
|
|
||||||
},
|
|
||||||
1 => {
|
|
||||||
h ^= u32::from(remainder[0]);
|
|
||||||
h = h.wrapping_mul(M);
|
|
||||||
},
|
|
||||||
_ => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
h ^= h >> 13;
|
|
||||||
h = h.wrapping_mul(M);
|
|
||||||
h ^= h >> 15;
|
|
||||||
|
|
||||||
h
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute SHA1 hash of a file
|
/// Compute SHA1 hash of a file
|
||||||
pub fn compute_sha1<P: AsRef<Path>>(path: P) -> Result<String> {
|
pub fn compute_sha1<P: AsRef<Path>>(path: P) -> Result<String> {
|
||||||
let file = File::open(path)?;
|
let file = File::open(path)?;
|
||||||
|
|
@ -167,31 +115,6 @@ pub fn verify_hash<P: AsRef<Path>>(
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_murmur2_hash_deterministic() {
|
|
||||||
let data = b"hello world";
|
|
||||||
let hash1 = compute_murmur2_hash(data);
|
|
||||||
let hash2 = compute_murmur2_hash(data);
|
|
||||||
assert_eq!(hash1, hash2, "Murmur2 hash must be deterministic");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_murmur2_hash_empty() {
|
|
||||||
let data = b"";
|
|
||||||
let hash = compute_murmur2_hash(data);
|
|
||||||
assert_ne!(hash, 0, "Empty data should produce a non-zero hash");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_murmur2_hash_different_inputs() {
|
|
||||||
let hash1 = compute_murmur2_hash(b"hello");
|
|
||||||
let hash2 = compute_murmur2_hash(b"world");
|
|
||||||
assert_ne!(
|
|
||||||
hash1, hash2,
|
|
||||||
"Different inputs should produce different hashes"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sha256_bytes_deterministic() {
|
fn test_sha256_bytes_deterministic() {
|
||||||
let data = b"test data";
|
let data = b"test data";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use rand::Rng;
|
use rand::RngExt;
|
||||||
|
|
||||||
const CHARSET: &[u8] =
|
const CHARSET: &[u8] =
|
||||||
b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
pub mod hash;
|
pub mod hash;
|
||||||
pub mod id;
|
pub mod id;
|
||||||
pub mod prompt;
|
|
||||||
|
|
||||||
pub use hash::verify_hash;
|
pub use hash::verify_hash;
|
||||||
pub use id::generate_pakku_id;
|
pub use id::generate_pakku_id;
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
use std::io::{self, Write};
|
|
||||||
|
|
||||||
use crate::error::Result;
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn prompt_user(message: &str) -> Result<String> {
|
|
||||||
print!("{message}");
|
|
||||||
io::stdout().flush()?;
|
|
||||||
|
|
||||||
let mut input = String::new();
|
|
||||||
io::stdin().read_line(&mut input)?;
|
|
||||||
|
|
||||||
Ok(input.trim().to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn prompt_select(message: &str, options: &[String]) -> Result<usize> {
|
|
||||||
println!("{message}");
|
|
||||||
for (i, option) in options.iter().enumerate() {
|
|
||||||
println!(" {}. {}", i + 1, option);
|
|
||||||
}
|
|
||||||
|
|
||||||
loop {
|
|
||||||
print!("Select (1-{}): ", options.len());
|
|
||||||
io::stdout().flush()?;
|
|
||||||
|
|
||||||
let mut input = String::new();
|
|
||||||
io::stdin().read_line(&mut input)?;
|
|
||||||
|
|
||||||
if let Ok(choice) = input.trim().parse::<usize>()
|
|
||||||
&& choice > 0
|
|
||||||
&& choice <= options.len()
|
|
||||||
{
|
|
||||||
return Ok(choice - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Invalid selection. Please try again.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn prompt_confirm(message: &str) -> Result<bool> {
|
|
||||||
print!("{message} (y/n): ");
|
|
||||||
io::stdout().flush()?;
|
|
||||||
|
|
||||||
let mut input = String::new();
|
|
||||||
io::stdin().read_line(&mut input)?;
|
|
||||||
|
|
||||||
let answer = input.trim().to_lowercase();
|
|
||||||
Ok(answer == "y" || answer == "yes")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn confirm(message: &str) -> Result<bool> {
|
|
||||||
prompt_confirm(message)
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue