Compare commits

..

No commits in common. "d40cbb74fca3d47706c15dd1ffd236d9aa9653e2" and "96c24680974eb4b058d3798c52ebda22f9ba713e" have entirely different histories.

22 changed files with 264 additions and 1116 deletions

371
Cargo.lock generated
View file

@ -16,7 +16,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cipher", "cipher",
"cpufeatures 0.2.17", "cpufeatures",
] ]
[[package]] [[package]]
@ -80,9 +80,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.101" version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]] [[package]]
name = "assert-json-diff" name = "assert-json-diff"
@ -211,17 +211,6 @@ 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"
@ -234,9 +223,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.58" version = "4.5.54"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806" checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -244,9 +233,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.58" version = "4.5.54"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2" checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -256,9 +245,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.55" version = "4.5.49"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
@ -268,9 +257,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_lex" name = "clap_lex"
version = "1.0.0" version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
[[package]] [[package]]
name = "cmake" name = "cmake"
@ -332,9 +321,9 @@ dependencies = [
[[package]] [[package]]
name = "constant_time_eq" name = "constant_time_eq"
version = "0.4.2" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
@ -371,15 +360,6 @@ 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"
@ -518,9 +498,9 @@ dependencies = [
[[package]] [[package]]
name = "env_filter" name = "env_filter"
version = "1.0.0" version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2"
dependencies = [ dependencies = [
"log", "log",
"regex", "regex",
@ -528,9 +508,9 @@ dependencies = [
[[package]] [[package]]
name = "env_logger" name = "env_logger"
version = "0.11.9" version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -584,12 +564,6 @@ 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"
@ -731,27 +705,11 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "getrandom"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi",
"rand_core 0.10.0",
"wasip2",
"wasip3",
"wasm-bindgen",
]
[[package]] [[package]]
name = "git2" name = "git2"
version = "0.20.4" version = "0.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" checksum = "3e2b37e2f62729cdada11f0e6b3b6fe383c69c29fc619e391223e12856af308c"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"libc", "libc",
@ -762,12 +720,6 @@ 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"
@ -787,15 +739,6 @@ 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"
@ -1008,12 +951,6 @@ 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"
@ -1042,9 +979,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.16.1", "hashbrown",
"serde",
"serde_core",
] ]
[[package]] [[package]]
@ -1173,12 +1108,6 @@ 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"
@ -1187,9 +1116,9 @@ checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.181" version = "0.2.180"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
[[package]] [[package]]
name = "libgit2-sys" name = "libgit2-sys"
@ -1334,9 +1263,9 @@ dependencies = [
[[package]] [[package]]
name = "mockito" name = "mockito"
version = "1.7.2" version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90820618712cab19cfc46b274c6c22546a82affcb3c3bdf0f29e3db8e1bb92c0" checksum = "7e0603425789b4a70fcc4ac4f5a46a566c116ee3e2a6b768dc623f7719c611de"
dependencies = [ dependencies = [
"assert-json-diff", "assert-json-diff",
"bytes", "bytes",
@ -1349,7 +1278,7 @@ dependencies = [
"hyper-util", "hyper-util",
"log", "log",
"pin-project-lite", "pin-project-lite",
"rand 0.9.2", "rand",
"regex", "regex",
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
@ -1359,9 +1288,9 @@ dependencies = [
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.2.0" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]] [[package]]
name = "once_cell" name = "once_cell"
@ -1411,17 +1340,16 @@ dependencies = [
"env_logger", "env_logger",
"futures", "futures",
"git2", "git2",
"glob",
"indicatif", "indicatif",
"keyring", "keyring",
"libc", "libc",
"log", "log",
"md-5", "md-5",
"mockito", "mockito",
"rand 0.10.0", "once_cell",
"rand",
"regex", "regex",
"reqwest", "reqwest",
"semver",
"serde", "serde",
"serde_json", "serde_json",
"sha1", "sha1",
@ -1429,7 +1357,7 @@ dependencies = [
"strsim", "strsim",
"tempfile", "tempfile",
"textwrap", "textwrap",
"thiserror 2.0.18", "thiserror 2.0.17",
"tokio", "tokio",
"walkdir", "walkdir",
"yansi", "yansi",
@ -1525,9 +1453,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "ppmd-rust" name = "ppmd-rust"
version = "1.4.0" version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" checksum = "d558c559f0450f16f2a27a1f017ef38468c1090c9ce63c8e51366232d53717b4"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
@ -1538,16 +1466,6 @@ 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"
@ -1571,7 +1489,7 @@ dependencies = [
"rustc-hash", "rustc-hash",
"rustls", "rustls",
"socket2", "socket2",
"thiserror 2.0.18", "thiserror 2.0.17",
"tokio", "tokio",
"tracing", "tracing",
"web-time", "web-time",
@ -1587,13 +1505,13 @@ dependencies = [
"bytes", "bytes",
"getrandom 0.3.4", "getrandom 0.3.4",
"lru-slab", "lru-slab",
"rand 0.9.2", "rand",
"ring", "ring",
"rustc-hash", "rustc-hash",
"rustls", "rustls",
"rustls-pki-types", "rustls-pki-types",
"slab", "slab",
"thiserror 2.0.18", "thiserror 2.0.17",
"tinyvec", "tinyvec",
"tracing", "tracing",
"web-time", "web-time",
@ -1635,18 +1553,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [ dependencies = [
"rand_chacha", "rand_chacha",
"rand_core 0.9.3", "rand_core",
]
[[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]]
@ -1656,7 +1563,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [ dependencies = [
"ppv-lite86", "ppv-lite86",
"rand_core 0.9.3", "rand_core",
] ]
[[package]] [[package]]
@ -1668,12 +1575,6 @@ 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"
@ -1685,9 +1586,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.12.3" version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -1714,9 +1615,9 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.13.2" version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62"
dependencies = [ dependencies = [
"base64", "base64",
"bytes", "bytes",
@ -1919,12 +1820,6 @@ 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"
@ -1987,7 +1882,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cpufeatures 0.2.17", "cpufeatures",
"digest", "digest",
] ]
@ -1998,7 +1893,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cpufeatures 0.2.17", "cpufeatures",
"digest", "digest",
] ]
@ -2136,12 +2031,12 @@ dependencies = [
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.25.0" version = "3.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
dependencies = [ dependencies = [
"fastrand", "fastrand",
"getrandom 0.4.1", "getrandom 0.3.4",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.61.2", "windows-sys 0.61.2",
@ -2169,11 +2064,11 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.18" version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
dependencies = [ dependencies = [
"thiserror-impl 2.0.18", "thiserror-impl 2.0.17",
] ]
[[package]] [[package]]
@ -2189,9 +2084,9 @@ dependencies = [
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "2.0.18" version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2200,23 +2095,22 @@ dependencies = [
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.47" version = "0.3.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
dependencies = [ dependencies = [
"deranged", "deranged",
"js-sys",
"num-conv", "num-conv",
"powerfmt", "powerfmt",
"serde_core", "serde",
"time-core", "time-core",
] ]
[[package]] [[package]]
name = "time-core" name = "time-core"
version = "0.1.8" version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
[[package]] [[package]]
name = "tinystr" name = "tinystr"
@ -2364,12 +2258,6 @@ 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"
@ -2400,12 +2288,6 @@ 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"
@ -2485,16 +2367,7 @@ 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 0.46.0", "wit-bindgen",
]
[[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]]
@ -2555,40 +2428,6 @@ 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"
@ -2921,94 +2760,6 @@ 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"
@ -3140,9 +2891,9 @@ dependencies = [
[[package]] [[package]]
name = "zip" name = "zip"
version = "7.4.0" version = "7.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc12baa6db2b15a140161ce53d72209dacea594230798c24774139b54ecaa980" checksum = "9013f1222db8a6d680f13a7ccdc60a781199cd09c2fa4eff58e728bb181757fc"
dependencies = [ dependencies = [
"aes", "aes",
"bzip2", "bzip2",
@ -3150,7 +2901,8 @@ dependencies = [
"crc32fast", "crc32fast",
"deflate64", "deflate64",
"flate2", "flate2",
"getrandom 0.4.1", "generic-array",
"getrandom 0.3.4",
"hmac", "hmac",
"indexmap", "indexmap",
"lzma-rust2", "lzma-rust2",
@ -3159,7 +2911,6 @@ dependencies = [
"ppmd-rust", "ppmd-rust",
"sha1", "sha1",
"time", "time",
"typed-path",
"zeroize", "zeroize",
"zopfli", "zopfli",
"zstd", "zstd",

View file

@ -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.101" anyhow = "1.0.100"
async-trait = "0.1.89" async-trait = "0.1.89"
clap = { version = "4.5.58", features = [ "derive" ] } clap = { version = "4.5.54", features = [ "derive" ] }
comfy-table = "7.2.2" comfy-table = "7.1"
dialoguer = "0.12.0" dialoguer = "0.12.0"
env_logger = "0.11.9" env_logger = "0.11.8"
futures = "0.3.31" futures = "0.3.31"
git2 = "0.20.4" git2 = "0.20.3"
glob = "0.3.3"
indicatif = "0.18.3" indicatif = "0.18.3"
keyring = "3.6.3" keyring = "3.6.3"
libc = "0.2.181" libc = "0.2.180"
log = "0.4.29" log = "0.4.29"
md-5 = "0.10.6" md-5 = "0.10.6"
rand = "0.10.0" once_cell = "1.20"
regex = "1.12.3" rand = "0.9.2"
reqwest = { version = "0.13.2", features = [ "json" ] } regex = "1.12"
semver = "1.0.27" reqwest = { version = "0.13.1", features = [ "json" ] }
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.9" sha2 = "0.10.0"
strsim = "0.11.1" strsim = "0.11.1"
tempfile = "3.25.0" tempfile = "3.24.0"
textwrap = "0.16.2" textwrap = "0.16"
thiserror = "2.0.18" thiserror = "2.0.17"
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.4.0" zip = "7.1.0"
[dev-dependencies] [dev-dependencies]
mockito = "1.7.2" mockito = "1.7.1"
tempfile = "3.25.0" tempfile = "3.24.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

View file

@ -97,20 +97,20 @@ pub struct InitArgs {
pub version: Option<String>, pub version: Option<String>,
/// Target platform /// Target platform
#[clap(short, long)] #[clap(short, long, default_value = "multiplatform")]
pub target: Option<String>, pub target: String,
/// Minecraft versions (space-separated) /// Minecraft version
#[clap(short, long = "mc-versions", value_delimiter = ' ', num_args = 1..)] #[clap(short, long, default_value = "1.20.1")]
pub mc_versions: Option<Vec<String>>, pub mc_version: String,
/// Mod loaders (format: name=version, can be specified multiple times) /// Mod loader
#[clap(short, long = "loaders", value_delimiter = ',')] #[clap(short, long, default_value = "fabric")]
pub loaders: Option<Vec<String>>, pub loader: String,
/// Skip interactive prompts (use defaults) /// Mod loader version
#[clap(short, long)] #[clap(short = 'v', long, default_value = "latest")]
pub yes: bool, pub loader_version: String,
} }
#[derive(Args)] #[derive(Args)]
@ -214,10 +214,6 @@ pub struct RmArgs {
/// Skip confirmation prompt /// Skip confirmation prompt
#[clap(short, long)] #[clap(short, long)]
pub yes: bool, pub yes: bool,
/// Skip removing dependent projects
#[clap(short = 'D', long = "no-deps")]
pub no_deps: bool,
} }
#[derive(Args)] #[derive(Args)]
@ -226,10 +222,6 @@ pub struct UpdateArgs {
#[arg(value_name = "PROJECT")] #[arg(value_name = "PROJECT")]
pub inputs: Vec<String>, pub inputs: Vec<String>,
/// Update all projects
#[arg(short, long)]
pub all: bool,
/// Skip confirmation prompts /// Skip confirmation prompts
#[arg(short, long)] #[arg(short, long)]
pub yes: bool, pub yes: bool,
@ -352,7 +344,7 @@ pub struct SyncArgs {
#[clap(short = 'R', long)] #[clap(short = 'R', long)]
pub removals: bool, pub removals: bool,
/// Sync updates only (apply pending updates) /// Sync updates only
#[clap(short = 'U', long)] #[clap(short = 'U', long)]
pub updates: bool, pub updates: bool,
} }
@ -379,7 +371,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,
} }

View file

@ -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)?; let ipc = IpcCoordinator::new(&config_dir.to_path_buf())?;
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

View file

@ -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 with shelve option // Create fetcher
let fetcher = Fetcher::new(".").with_shelve(args.shelve); let fetcher = Fetcher::new(".");
// 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?;

View file

@ -211,12 +211,13 @@ 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()
{ {
println!( if !output.stdout.is_empty() {
"Note: Jujutsu repository detected. Make sure to run 'jj git \ println!(
push' to sync changes with remote if needed." "Note: Jujutsu repository detected. Make sure to run 'jj git \
); push' to sync changes with remote if needed."
);
}
} }
}, },
VcsType::None => { VcsType::None => {

View file

@ -134,19 +134,16 @@ async fn import_modrinth(
{ {
log::info!("Fetching project: {project_id}"); log::info!("Fetching project: {project_id}");
match platform match platform
.request_project_with_files( .request_project_with_files(project_id, &lockfile.mc_versions, &[
project_id, loader.0.clone(),
&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) = project.select_file( if let Err(e) =
&lockfile.mc_versions, project.select_file(&lockfile.mc_versions, &[loader.0.clone()])
std::slice::from_ref(&loader.0), {
) {
log::warn!( log::warn!(
"Failed to select file for {}: {}", "Failed to select file for {}: {}",
project.get_name(), project.get_name(),
@ -360,7 +357,7 @@ async fn import_curseforge(
description: None, description: None,
author: manifest["author"] author: manifest["author"]
.as_str() .as_str()
.map(std::string::ToString::to_string), .map(|s| s.to_string()),
overrides: vec!["overrides".to_string()], overrides: vec!["overrides".to_string()],
server_overrides: None, server_overrides: None,
client_overrides: None, client_overrides: None,

View file

@ -176,7 +176,7 @@ fn display_project_inspection(
// Display project files // Display project files
println!(); println!();
display_project_files(&project.files, project)?; display_project_files(&project.files)?;
// Display properties // Display properties
println!(); println!();
@ -228,10 +228,7 @@ fn display_project_header(project: &Project) -> Result<()> {
Ok(()) Ok(())
} }
fn display_project_files( fn display_project_files(files: &[ProjectFile]) -> Result<()> {
files: &[ProjectFile],
project: &Project,
) -> Result<()> {
if files.is_empty() { if files.is_empty() {
println!("{}", "No files available".yellow()); println!("{}", "No files available".yellow());
return Ok(()); return Ok(());
@ -253,31 +250,19 @@ fn display_project_files(
format!(" {status}") format!(" {status}")
}; };
// File path line with optional site URL // File path line
let file_path = format!("{}={}", file.file_type, file.file_name); let file_path = format!("{}={}", file.file_type, file.file_name);
let file_display = if let Some(site_url) = file.get_site_url(project) { table.add_row(vec![
// Create hyperlink for the file Cell::new(format!("{file_path}:{status_text}")).fg(if idx == 0 {
let hyperlink = crate::ui_utils::hyperlink(&site_url, &file_path); Color::Green
format!("{hyperlink}:{status_text}") } else {
} else { Color::White
format!("{file_path}:{status_text}") }),
}; ]);
table.add_row(vec![Cell::new(file_display).fg(if idx == 0 {
Color::Green
} else {
Color::White
})]);
// Date published // Date published
table.add_row(vec![Cell::new(&file.date_published).fg(Color::DarkGrey)]); table.add_row(vec![Cell::new(&file.date_published).fg(Color::DarkGrey)]);
// Show site URL if available (for non-hyperlink terminals)
if let Some(site_url) = file.get_site_url(project) {
table
.add_row(vec![Cell::new(format!("URL: {site_url}")).fg(Color::Blue)]);
}
// Empty line // Empty line
table.add_row(vec![Cell::new("")]); table.add_row(vec![Cell::new("")]);

View file

@ -2,18 +2,6 @@ use std::path::Path;
use crate::{cli::LsArgs, error::Result, model::LockFile}; use crate::{cli::LsArgs, error::Result, model::LockFile};
/// Truncate a name to fit within `max_len` characters, adding "..." if
/// truncated
fn truncate_name(name: &str, max_len: usize) -> String {
if name.len() <= max_len {
name.to_string()
} else if max_len > 3 {
format!("{}...", &name[..max_len - 3])
} else {
name[..max_len].to_string()
}
}
pub fn execute(args: LsArgs, lockfile_path: &Path) -> Result<()> { pub fn execute(args: LsArgs, lockfile_path: &Path) -> Result<()> {
// 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("."));
@ -27,33 +15,10 @@ pub fn execute(args: LsArgs, lockfile_path: &Path) -> Result<()> {
println!("Installed projects ({}):", lockfile.projects.len()); println!("Installed projects ({}):", lockfile.projects.len());
println!(); println!();
// Calculate max name length for alignment
let max_name_len = args.name_max_length.unwrap_or_else(|| {
lockfile
.projects
.iter()
.map(|p| p.get_name().len())
.max()
.unwrap_or(20)
.min(50)
});
for project in &lockfile.projects { for project in &lockfile.projects {
// Check for version mismatch across providers
let version_warning = if project.versions_match_across_providers() {
""
} else {
// Use the detailed check_version_mismatch for logging
if let Some(mismatch_detail) = project.check_version_mismatch() {
log::warn!("{mismatch_detail}");
}
" [!] versions do not match across providers"
};
if args.detailed { if args.detailed {
let id = project.pakku_id.as_deref().unwrap_or("unknown"); let id = project.pakku_id.as_deref().unwrap_or("unknown");
let name = truncate_name(&project.get_name(), max_name_len); println!(" {} ({})", project.get_name(), id);
println!(" {name} ({id}){version_warning}");
println!(" Type: {:?}", project.r#type); println!(" Type: {:?}", project.r#type);
println!(" Side: {:?}", project.side); println!(" Side: {:?}", project.side);
@ -65,28 +30,19 @@ pub fn execute(args: LsArgs, lockfile_path: &Path) -> Result<()> {
); );
} }
// Show version details if there's a mismatch
if !version_warning.is_empty() {
println!(" Provider versions:");
for file in &project.files {
println!(" {}: {}", file.file_type, file.file_name);
}
}
if !project.pakku_links.is_empty() { if !project.pakku_links.is_empty() {
println!(" Dependencies: {}", project.pakku_links.len()); println!(" Dependencies: {}", project.pakku_links.len());
} }
println!(); println!();
} else { } else {
let name = truncate_name(&project.get_name(), max_name_len);
let file_info = project let file_info = project
.files .files
.first() .first()
.map(|f| format!(" ({})", f.file_name)) .map(|f| format!(" ({})", f.file_name))
.unwrap_or_default(); .unwrap_or_default();
println!(" {name}{file_info}{version_warning}"); println!(" {}{}", project.get_name(), file_info);
} }
} }

View file

@ -1,4 +1,4 @@
use std::path::{Path, PathBuf}; use std::path::PathBuf;
use crate::{cli::RemoteUpdateArgs, error::PakkerError, git, model::Config}; use crate::{cli::RemoteUpdateArgs, error::PakkerError, git, model::Config};
@ -71,7 +71,7 @@ pub async fn execute(args: RemoteUpdateArgs) -> Result<(), PakkerError> {
} }
/// Sync override files from remote directory to current directory /// Sync override files from remote directory to current directory
async fn sync_overrides(remote_dir: &Path) -> Result<(), PakkerError> { async fn sync_overrides(remote_dir: &PathBuf) -> Result<(), PakkerError> {
let remote_config_path = remote_dir.join("pakku.json"); let remote_config_path = remote_dir.join("pakku.json");
if !remote_config_path.exists() { if !remote_config_path.exists() {
return Ok(()); return Ok(());

View file

@ -6,7 +6,7 @@ use tokio::sync::Semaphore;
use yansi::Paint; use yansi::Paint;
use crate::{ use crate::{
error::{ErrorSeverity, Result}, error::Result,
model::{Config, LockFile, Project}, model::{Config, LockFile, Project},
platform::create_platform, platform::create_platform,
}; };
@ -36,42 +36,13 @@ pub async fn execute(
// Display results // Display results
display_update_results(&updates); display_update_results(&updates);
// Display errors if any, categorized by severity // Display errors if any
if !errors.is_empty() { if !errors.is_empty() {
println!(); println!();
println!("{}", "Errors encountered:".red());
// Categorize errors by severity for (project, error) in &errors {
let (warnings, errors_only): (Vec<_>, Vec<_>) = println!(" - {}: {}", project.yellow(), error.red());
errors.iter().partition(|(_, err)| {
// Network errors and "not found" are warnings (non-fatal)
err.contains("Failed to check") || err.contains("not found")
});
// Display warnings (ErrorSeverity::Warning)
if !warnings.is_empty() {
let severity = ErrorSeverity::Warning;
println!("{}", format_severity_header(severity, "Warnings"));
for (project, error) in &warnings {
println!(" - {}: {}", project.yellow(), error.dim());
}
} }
// Display errors (ErrorSeverity::Error)
if !errors_only.is_empty() {
let severity = ErrorSeverity::Error;
println!("{}", format_severity_header(severity, "Errors"));
for (project, error) in &errors_only {
println!(" - {}: {}", project.yellow(), error.red());
}
}
// Log info level summary
let _info_severity = ErrorSeverity::Info;
log::info!(
"Update check completed with {} warning(s) and {} error(s)",
warnings.len(),
errors_only.len()
);
} }
// Prompt to update if there are updates available // Prompt to update if there are updates available
@ -81,7 +52,6 @@ pub async fn execute(
// 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,
yes: true, // Auto-yes for status command yes: true, // Auto-yes for status command
}; };
crate::cli::commands::update::execute( crate::cli::commands::update::execute(
@ -398,12 +368,3 @@ fn get_api_key(platform: &str) -> Option<String> {
_ => None, _ => None,
} }
} }
/// Format severity header with appropriate color
fn format_severity_header(severity: ErrorSeverity, label: &str) -> String {
match severity {
ErrorSeverity::Error => format!("{label}:").red().to_string(),
ErrorSeverity::Warning => format!("{label}:").yellow().to_string(),
ErrorSeverity::Info => format!("{label}:").cyan().to_string(),
}
}

View file

@ -4,10 +4,10 @@ use indicatif::{ProgressBar, ProgressStyle};
use crate::{ use crate::{
cli::UpdateArgs, cli::UpdateArgs,
error::{MultiError, PakkerError}, error::PakkerError,
model::{Config, LockFile, UpdateStrategy}, model::{Config, LockFile},
platform::create_platform, platform::create_platform,
ui_utils::{prompt_select, prompt_typo_suggestion, prompt_yes_no}, ui_utils::prompt_select,
}; };
pub async fn execute( pub async fn execute(
@ -33,22 +33,6 @@ pub async fn execute(
platforms.insert("curseforge".to_string(), platform); platforms.insert("curseforge".to_string(), platform);
} }
// Collect all known project identifiers for typo suggestions
let all_slugs: Vec<String> = lockfile
.projects
.iter()
.flat_map(|p| {
let mut ids = Vec::new();
if let Some(ref pakku_id) = p.pakku_id {
ids.push(pakku_id.clone());
}
ids.extend(p.slug.values().cloned());
ids.extend(p.name.values().cloned());
ids.extend(p.aliases.iter().cloned());
ids
})
.collect();
let project_indices: Vec<_> = if args.inputs.is_empty() { let project_indices: Vec<_> = if args.inputs.is_empty() {
(0..lockfile.projects.len()).collect() (0..lockfile.projects.len()).collect()
} else { } else {
@ -62,29 +46,14 @@ pub async fn execute(
{ {
indices.push(idx); indices.push(idx);
} else { } else {
// Try typo suggestion
if let Ok(Some(suggestion)) = prompt_typo_suggestion(input, &all_slugs)
&& let Some((idx, _)) = lockfile
.projects
.iter()
.enumerate()
.find(|(_, p)| p.matches_input(&suggestion))
{
log::info!("Using suggested project: {suggestion}");
indices.push(idx);
continue;
}
return Err(PakkerError::ProjectNotFound(input.clone())); return Err(PakkerError::ProjectNotFound(input.clone()));
} }
} }
indices indices
}; };
// Capture count before consuming the iterator
let total_projects = project_indices.len();
// Create progress bar // Create progress bar
let pb = ProgressBar::new(total_projects as u64); let pb = ProgressBar::new(project_indices.len() as u64);
pb.set_style( pb.set_style(
ProgressStyle::default_bar() ProgressStyle::default_bar()
.template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} {msg}") .template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} {msg}")
@ -92,23 +61,8 @@ pub async fn execute(
.progress_chars("#>-"), .progress_chars("#>-"),
); );
let mut skipped_pinned = 0;
let mut update_errors = MultiError::new();
for idx in project_indices { for idx in project_indices {
let old_project = &lockfile.projects[idx]; let old_project = &lockfile.projects[idx];
// Skip projects with UpdateStrategy::None (pinned)
if old_project.update_strategy == UpdateStrategy::None {
pb.println(format!(
" {} - Skipped (update strategy: NONE)",
old_project.get_name()
));
skipped_pinned += 1;
pb.inc(1);
continue;
}
pb.set_message(format!("Updating {}...", old_project.get_name())); pb.set_message(format!("Updating {}...", old_project.get_name()));
let slug = old_project let slug = old_project
@ -133,116 +87,54 @@ pub async fn execute(
} }
} }
if updated_project.is_none() {
// Failed to fetch update info from any platform
update_errors.push(PakkerError::PlatformApiError(format!(
"Failed to check updates for '{}'",
old_project.get_name()
)));
pb.inc(1);
continue;
}
if let Some(mut updated_project) = updated_project if let Some(mut updated_project) = updated_project
&& !updated_project.files.is_empty() && !updated_project.files.is_empty()
&& let Some(old_file) = lockfile.projects[idx].files.first() && let Some(old_file) = lockfile.projects[idx].files.first()
{ {
// Clone data needed for comparisons to avoid borrow issues let new_file = updated_project.files.first().unwrap();
let new_file_id = updated_project.files.first().unwrap().id.clone();
let new_file_name =
updated_project.files.first().unwrap().file_name.clone();
let old_file_name = old_file.file_name.clone();
let project_name = old_project.get_name();
if new_file_id == old_file.id { if new_file.id == old_file.id {
pb.println(format!(" {project_name} - Already up to date")); pb.println(format!(
" {} - Already up to date",
old_project.get_name()
));
} else { } else {
// Interactive confirmation and version selection if not using --yes // Interactive version selection if not using --yes flag
// flag if !args.yes && updated_project.files.len() > 1 {
let mut should_update = args.yes || args.all;
let mut selected_idx: Option<usize> = None;
if !args.yes && !args.all {
pb.suspend(|| { pb.suspend(|| {
// First, confirm the update let choices: Vec<String> = updated_project
let prompt_msg = format!( .files
"Update '{project_name}' from {old_file_name} to \ .iter()
{new_file_name}?" .map(|f| format!("{} ({})", f.file_name, f.id))
); .collect();
should_update = prompt_yes_no(&prompt_msg, true).unwrap_or(false);
// If confirmed and multiple versions available, offer selection let choice_refs: Vec<&str> =
if should_update && updated_project.files.len() > 1 { choices.iter().map(std::string::String::as_str).collect();
let choices: Vec<String> = updated_project
.files
.iter()
.map(|f| format!("{} ({})", f.file_name, f.id))
.collect();
let choice_refs: Vec<&str> = if let Ok(selected_idx) = prompt_select(
choices.iter().map(std::string::String::as_str).collect(); &format!("Select version for {}:", old_project.get_name()),
&choice_refs,
if let Ok(idx) = prompt_select( ) {
&format!("Select version for {project_name}:"), // Move selected file to front
&choice_refs, if selected_idx > 0 {
) { updated_project.files.swap(0, selected_idx);
selected_idx = Some(idx);
} }
} }
}); });
} }
// Apply file selection outside the closure let selected_file = updated_project.files.first().unwrap();
if let Some(idx) = selected_idx pb.println(format!(
&& idx > 0 " {} -> {}",
{ old_file.file_name, selected_file.file_name
updated_project.files.swap(0, idx); ));
} lockfile.projects[idx] = updated_project;
if should_update {
let selected_file = updated_project.files.first().unwrap();
pb.println(format!(
" {} -> {}",
old_file_name, selected_file.file_name
));
lockfile.projects[idx] = updated_project;
} else {
pb.println(format!(" {project_name} - Skipped by user"));
}
} }
} }
pb.inc(1); pb.inc(1);
} }
if skipped_pinned > 0 { pb.finish_with_message("Update complete");
pb.finish_with_message(format!(
"Update complete ({skipped_pinned} pinned projects skipped)"
));
} else {
pb.finish_with_message("Update complete");
}
lockfile.save(lockfile_dir)?; lockfile.save(lockfile_dir)?;
// Report any errors that occurred during updates
if !update_errors.is_empty() {
let error_list = update_errors.errors();
log::warn!(
"{} project(s) encountered errors during update check",
error_list.len()
);
for err in error_list {
log::warn!(" - {err}");
}
// Extend with any additional collected errors and check if we should fail
let all_errors = update_errors.into_errors();
if all_errors.len() == total_projects {
// All projects failed - return error
let mut multi = MultiError::new();
multi.extend(all_errors);
return multi.into_result(());
}
}
Ok(()) Ok(())
} }

View file

@ -288,9 +288,10 @@ 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()
{ {
return VcsType::Jujutsu; if output.status.success() {
return VcsType::Jujutsu;
}
} }
// Check for git // Check for git
@ -298,9 +299,10 @@ 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()
{ {
return VcsType::Git; if output.status.success() {
return VcsType::Git;
}
} }
VcsType::None VcsType::None
@ -331,7 +333,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);

View file

@ -1,16 +0,0 @@
use std::time::Duration;
use reqwest::Client;
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")
}

View file

@ -1,15 +1,8 @@
// 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;
@ -24,6 +17,8 @@ use clap::Parser;
use cli::{Cli, Commands}; use cli::{Cli, Commands};
use error::PakkerError; use error::PakkerError;
use crate::rate_limiter::RateLimiter;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), PakkerError> { async fn main() -> Result<(), PakkerError> {
let cli = Cli::parse(); let cli = Cli::parse();
@ -47,6 +42,8 @@ async fn main() -> Result<(), PakkerError> {
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));
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, &lockfile_path, &config_path).await

View file

@ -192,29 +192,15 @@ impl Project {
return true; return true;
} }
// Compare semantic versions extracted from file names // Check if all providers have the same latest file name
let parse_version = |name: &str| { // (simplified check - in reality would compare semantic versions)
// Try to extract version from patterns like "mod-1.0.0.jar" or let file_names: Vec<_> = versions_by_provider
// "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().and_then(parse_version)) .filter_map(|files| files.first().copied())
.collect(); .collect();
// All versions should be the same // All file names should be the same for versions to match
versions.windows(2).all(|w| w[0] == w[1]) file_names.windows(2).all(|w| w[0] == w[1])
} }
/// Check if versions do NOT match across providers. /// Check if versions do NOT match across providers.

View file

@ -8,19 +8,13 @@ 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, http, rate_limiter::RateLimiter}; use crate::{error::Result, rate_limiter::RateLimiter};
static HTTP_CLIENT: std::sync::LazyLock<Arc<reqwest::Client>> = static RATE_LIMITER: Lazy<Arc<RateLimiter>> =
std::sync::LazyLock::new(|| Arc::new(http::create_http_client())); Lazy::new(|| Arc::new(RateLimiter::new(None)));
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,
@ -40,21 +34,9 @@ fn create_client(
api_key: Option<String>, api_key: Option<String>,
) -> Result<Box<dyn PlatformClient>> { ) -> Result<Box<dyn PlatformClient>> {
match platform { match platform {
"modrinth" => { "modrinth" => Ok(Box::new(ModrinthPlatform::new())),
Ok(Box::new(ModrinthPlatform::with_client(get_http_client()))) "curseforge" => Ok(Box::new(CurseForgePlatform::new(api_key))),
}, "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}"

View file

@ -1,4 +1,4 @@
use std::{collections::HashMap, sync::Arc}; use std::collections::HashMap;
use async_trait::async_trait; use async_trait::async_trait;
use reqwest::Client; use reqwest::Client;
@ -12,30 +12,21 @@ 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: Arc<Client>, client: 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: Arc::new(Client::new()), client: 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();
@ -75,81 +66,11 @@ 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));
// Detect side from categories let mut project = Project::new(pakku_id, project_type, ProjectSide::Both);
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(),
@ -203,7 +124,7 @@ impl CurseForgePlatform {
required_dependencies: cf_file required_dependencies: cf_file
.dependencies .dependencies
.iter() .iter()
.filter(|d| d.relation_type == DEPENDENCY_RELATION_TYPE_REQUIRED) .filter(|d| d.relation_type == 3)
.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,
@ -396,20 +317,11 @@ 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)]
@ -469,112 +381,3 @@ 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
}
}

View file

@ -1,4 +1,4 @@
use std::{collections::HashMap, sync::Arc}; use std::collections::HashMap;
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 with_client(client: Arc<Client>, token: Option<String>) -> Self { pub fn new(token: Option<String>) -> Self {
Self { Self {
client: (*client).clone(), client: Client::new(),
token, token,
} }
} }

View file

@ -1,4 +1,4 @@
use std::{collections::HashMap, sync::Arc}; use std::collections::HashMap;
use async_trait::async_trait; use async_trait::async_trait;
use reqwest::Client; use reqwest::Client;
@ -14,76 +14,16 @@ 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: Arc<Client>, client: Client,
} }
impl ModrinthPlatform { impl ModrinthPlatform {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
client: Arc::new(Client::new()), client: 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,
@ -183,7 +123,15 @@ 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(
@ -222,7 +170,20 @@ impl PlatformClient for ModrinthPlatform {
url.push_str(&params.join("&")); url.push_str(&params.join("&"));
} }
self.request_project_files_url(&url).await let response = self.client.get(&url).send().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(
@ -252,7 +213,30 @@ 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)
} }
} }
@ -296,128 +280,3 @@ 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());
}
}

View file

@ -80,13 +80,13 @@ impl RateLimiter {
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 {
&& let Some(oldest) = platform_requests.first() if 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; }
} }
} }

View file

@ -1,4 +1,4 @@
use rand::RngExt; use rand::Rng;
const CHARSET: &[u8] = const CHARSET: &[u8] =
b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";