diff --git a/README.md b/README.md index efb8e87..569f093 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,11 @@ Install assets are currently expected to already exist locally, usually under: /cache/downloads/// ``` +Lock entries may include `download_url`, the exact public zip or DLL URL for the +locked artifact. The URL is currently audit/recovery metadata; `plan`, `check`, +and `apply` still use local assets and do not fetch missing downloads +automatically. + Composite local assets should have an explicit reconstruction recipe checked in with the helper. For example, Cinema's external Windows downloader tools are a small deterministic bundle rebuilt from upstream executables with: diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 690f137..fec11b0 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -162,6 +162,7 @@ id = "songcore" repo = "Kylemc1413/SongCore" tag = "v4.3.0" asset = "SongCore-4.3.0.zip" +download_url = "https://github.com/Kylemc1413/SongCore/releases/download/v4.3.0/SongCore-4.3.0.zip" sha256 = "..." reason = "Pinned until dependent mods support newer SongCore" @@ -170,9 +171,15 @@ id = "some-ui-mod" repo = "example/some-ui-mod" tag = "v1.2.1" asset = "SomeUIMod.dll" +download_url = "https://github.com/example/some-ui-mod/releases/download/v1.2.1/SomeUIMod.dll" sha256 = "..." ``` +`download_url` is the optional concrete URL for the locked artifact. It should +point at the exact zip or DLL when the artifact is publicly downloadable, and +should be omitted for local builds, private/commercial packages, and packages +that must be reconstructed from source or a recipe. + The lockfile should be the source of truth for reproducible installs. `check` may propose newer versions, but `apply` should install what is present in a plan generated from the lockfile or from an explicit update command. ## Install State diff --git a/docs/roadmap-01.md b/docs/roadmap-01.md index cc0c9ea..2d79ebf 100644 --- a/docs/roadmap-01.md +++ b/docs/roadmap-01.md @@ -46,15 +46,15 @@ There is no urgent need to migrate the layout before the rest of the helper settles, but new code should avoid assuming that downloads and per-install state must always live together. -The version lock should eventually include structured source URLs for every -asset so the helper can fetch missing downloads itself. The lock already pins -the selected repo, tag, asset name, and checksum; adding source fields would -make the fetch path explicit for both GitHub release assets and BeatMods CDN -fallbacks. Hashes should remain useful audit metadata and a warning signal, but -the UX needs a recovery path for replaced upstream assets: report the mismatch, -show the expected and actual hashes, and let the user intentionally refresh or -re-lock after inspection instead of treating every mismatch as an unrecoverable -dead end. +The version lock includes an optional `download_url` for publicly downloadable +artifacts. That makes the fetch path explicit for both GitHub release assets and +BeatMods CDN fallbacks, while local builds and private/commercial packages can +leave it unset. A future helper command can use this field to fetch missing +downloads itself. Hashes should remain useful audit metadata and a warning +signal, but the UX needs a recovery path for replaced upstream assets: report +the mismatch, show the expected and actual hashes, and let the user +intentionally refresh or re-lock after inspection instead of treating every +mismatch as an unrecoverable dead end. ## Future: Nix-Orchestrated Plugin Sets diff --git a/locks/1.44.1.lock.toml b/locks/1.44.1.lock.toml index 36bf5a1..17b11d5 100644 --- a/locks/1.44.1.lock.toml +++ b/locks/1.44.1.lock.toml @@ -6,6 +6,7 @@ id = "bsipa" repo = "nike4613/BeatSaber-IPA-Reloaded" tag = "4.3.7" asset = "BSIPA-net472-x64.zip" +download_url = "https://github.com/nike4613/BeatSaber-IPA-Reloaded/releases/download/4.3.7/BSIPA-net472-x64.zip" sha256 = "899b8a1dda91935bd5c19a211fa48e44e32f7be9ab82b6fc796709c753b7b2bc" install_strategy = "root-zip" reason = "BeatMods verified BSIPA 4.3.7 for Beat Saber 1.44.1 as version id 2561, zipHash 947774ef1010ff809ae05e345e269a90. GitHub asset is byte-identical to the BeatMods CDN zip used for initial bootstrap." @@ -15,6 +16,7 @@ id = "beatsabermarkuplanguage" repo = "monkeymanboy/BeatSaberMarkupLanguage" tag = "v1.14.1" asset = "BeatSaberMarkupLanguage-v1.14.1+bs.1.41.1-RELEASE.zip" +download_url = "https://github.com/monkeymanboy/BeatSaberMarkupLanguage/releases/download/v1.14.1/BeatSaberMarkupLanguage-v1.14.1%2Bbs.1.41.1-RELEASE.zip" sha256 = "816613459853955e2b7c02123ed42f8e5bdbd946cc774fd5fcc4e80a6a09120a" install_strategy = "bsipa-zip" reason = "BeatMods verified BeatSaberMarkupLanguage 1.14.1 for Beat Saber 1.44.1 as version id 2567, zipHash 46149d03f8549e07f2c88fefde4337b2. GitHub asset is byte-identical to the BeatMods CDN zip used for initial bootstrap." @@ -24,6 +26,7 @@ id = "sirautil" repo = "Auros/SiraUtil" tag = "v3.3.1" asset = "SiraUtil-v3.3.1+bs.1.42.0.zip" +download_url = "https://github.com/Auros/SiraUtil/releases/download/v3.3.1/SiraUtil-v3.3.1%2Bbs.1.42.0.zip" sha256 = "2253e5c31324e1e01b23480acba5ceee6b67eb8f16a079f58bc2f6ce7b12051f" install_strategy = "bsipa-zip" reason = "BeatMods verified SiraUtil 3.3.1 for Beat Saber 1.44.1 as version id 2565, zipHash ae14f7d3192a919d5d996c802fbde037. GitHub asset is byte-identical to the BeatMods CDN zip used for initial bootstrap." @@ -33,6 +36,7 @@ id = "songcore" repo = "Kylemc1413/SongCore" tag = "beatmods-3.16.0" asset = "SongCore-3.16.0.zip" +download_url = "https://beatmods.com/cdn/mod/0af9c0a03074c17ca15c1b667a0e30c8.zip" sha256 = "f9be5d66426d795c6c8af2a4da0150a298d2d6a65fbabcab316d379088a40019" install_strategy = "bsipa-zip" reason = "BeatMods verified SongCore 3.16.0 for Beat Saber 1.44.1 as version id 2564, zipHash 0af9c0a03074c17ca15c1b667a0e30c8. BeatMods points to Kylemc1413/SongCore, but that GitHub repo currently exposes releases only through 3.10.4, so this remains a BeatMods CDN fallback until a matching upstream release artifact is found." @@ -42,6 +46,7 @@ id = "customjsondata" repo = "Aeroluna/CustomJSONData" tag = "v2.6.8" asset = "CustomJSONData-2.6.8+1.40.0-bs1.40.0-7c2c32c.zip" +download_url = "https://github.com/Aeroluna/CustomJSONData/releases/download/v2.6.8/CustomJSONData-2.6.8%2B1.40.0-bs1.40.0-7c2c32c.zip" sha256 = "30555c77485d2837bd294608fe4e23c34415b89413997be67a6150318090b216" install_strategy = "bsipa-zip" reason = "BeatMods verified CustomJSONData 2.6.8+1.40.0 for Beat Saber 1.44.1 as version id 2327, zipHash fed31638bbb678580ef760ec83cfd486. GitHub asset is byte-identical to the BeatMods CDN zip." @@ -51,6 +56,7 @@ id = "heck" repo = "Aeroluna/Heck" tag = "v2026-05-02" asset = "Heck-1.8.3+1.42.1-bs1.42.1-3ebc6a2.zip" +download_url = "https://github.com/Aeroluna/Heck/releases/download/v2026-05-02/Heck-1.8.3%2B1.42.1-bs1.42.1-3ebc6a2.zip" sha256 = "d6c6a2b7e54285c8e7ee0515546f8cec274033afaa11324164803d003d9afab4" install_strategy = "bsipa-zip" reason = "No BeatMods verified 1.44.1 entry was returned on 2026-06-28. Use latest upstream GitHub Heck asset targeting the highest available Beat Saber version, bs1.42.1; GitHub release digest matched the downloaded asset." @@ -60,6 +66,7 @@ id = "chroma" repo = "Aeroluna/Heck" tag = "v2026-02-23" asset = "Chroma-2.9.22+1.42.1-bs1.42.1-b38d924.zip" +download_url = "https://github.com/Aeroluna/Heck/releases/download/v2026-02-23/Chroma-2.9.22%2B1.42.1-bs1.42.1-b38d924.zip" sha256 = "5438d039336ec91573818b7dbd8d5fce30454d16a66f3e70d6eedf5bff6c32bf" install_strategy = "bsipa-zip" reason = "No BeatMods verified 1.44.1 entry was returned on 2026-06-28. Use latest upstream GitHub Chroma asset targeting the highest available Beat Saber version, bs1.42.1; GitHub release digest matched the downloaded asset." @@ -69,6 +76,7 @@ id = "lookupid" repo = "Aeroluna/Heck" tag = "chroma-v2.5.10" asset = "LookupID-1.0.1.zip" +download_url = "https://github.com/Aeroluna/Heck/releases/download/chroma-v2.5.10/LookupID-1.0.1.zip" sha256 = "5fc838c9bf3693a8e3728056d95c07c5b0e13f944c1f9f49607f701b58e3d05d" install_strategy = "bsipa-zip" reason = "Required by Chroma. BeatMods verified LookupID 1.0.1 for Beat Saber 1.44.1 as version id 1527, zipHash 5f5655b91193602c0311b8b6d1b4c71c. GitHub exposes the same version as an upstream release asset." @@ -78,6 +86,7 @@ id = "noodleextensions" repo = "Aeroluna/Heck" tag = "v2026-05-09" asset = "NoodleExtensions-1.7.21+1.42.1-bs1.42.1-3bbcaf6.zip" +download_url = "https://github.com/Aeroluna/Heck/releases/download/v2026-05-09/NoodleExtensions-1.7.21%2B1.42.1-bs1.42.1-3bbcaf6.zip" sha256 = "116d5a56c52faf652cecee2dd0b9a02c1798d893fb5adef4145a1fa555ef727f" install_strategy = "bsipa-zip" reason = "No BeatMods verified 1.44.1 entry was returned on 2026-06-28. Use latest upstream GitHub Noodle Extensions asset targeting the highest available Beat Saber version, bs1.42.1; GitHub release digest matched the downloaded asset." @@ -87,6 +96,7 @@ id = "camerautils" repo = "Reezonate/CameraUtils" tag = "beatmods-1.0.8" asset = "CameraUtils-1.0.8.zip" +download_url = "https://beatmods.com/cdn/mod/38507e3a9ff8486c75b002cc47226f43.zip" sha256 = "7066ec311dc599e1d654d70557abc6c0809b72b5f4d695c8cfb85bb0bf2e2366" install_strategy = "bsipa-zip" reason = "Required by Vivify. BeatMods verified CameraUtils 1.0.8 for Beat Saber 1.44.1 as version id 2576, zipHash 38507e3a9ff8486c75b002cc47226f43. Upstream GitHub latest release is 1.0.7, so use the newer BeatMods CDN artifact." @@ -96,6 +106,7 @@ id = "assetbundleloadingtools" repo = "nicoco007/AssetBundleLoadingTools" tag = "v1.1.13" asset = "AssetBundleLoadingTools-v1.1.13+bs.1.41.1.zip" +download_url = "https://github.com/nicoco007/AssetBundleLoadingTools/releases/download/v1.1.13/AssetBundleLoadingTools-v1.1.13%2Bbs.1.41.1.zip" sha256 = "356b1aa4722ecca6f13199ad378429ece9873c365d9cf013e095fd5c0757a127" install_strategy = "root-zip" reason = "Required by Vivify. BeatMods verified AssetBundleLoadingTools 1.1.13 for Beat Saber 1.44.1 as version id 2590, zipHash a381a964cb69d26be8348edf04fabbc7. GitHub asset is the same version and its release digest matched the downloaded asset." @@ -105,6 +116,7 @@ id = "vivify" repo = "Aeroluna/Vivify" tag = "v1.1.0" asset = "Vivify-1.1.0+1.42.1-bs1.42.1-f83aa3c.zip" +download_url = "https://github.com/Aeroluna/Vivify/releases/download/v1.1.0/Vivify-1.1.0%2B1.42.1-bs1.42.1-f83aa3c.zip" sha256 = "0e8799a7243496925aa527bc4ca7d3bcab770f4e828bbceeed066539f90a0902" install_strategy = "bsipa-zip" reason = "No BeatMods verified 1.44.1 entry was returned on 2026-06-28. Use latest upstream GitHub Vivify asset targeting the highest available Beat Saber version, bs1.42.1; GitHub release digest matched the downloaded asset." @@ -114,6 +126,7 @@ id = "iniparser" repo = "rickyah/ini-parser" tag = "beatmods-2.5.9" asset = "IniParser-2.5.9.zip" +download_url = "https://beatmods.com/cdn/mod/5df74ad1c6b120fecdc615dd55f15b88.zip" sha256 = "b761293bea73ff3cb9998f404594aade617638aba4318203320189baf3449ff1" install_strategy = "bsipa-zip" reason = "BeatMods verified Ini Parser 2.5.9 for Beat Saber 1.44.1 as version id 1352, zipHash 5df74ad1c6b120fecdc615dd55f15b88. Use BeatMods CDN as a framework/library dependency payload." @@ -123,6 +136,7 @@ id = "bs-utils" repo = "Kylemc1413/Beat-Saber-Utils" tag = "beatmods-1.14.3" asset = "BSUtils-1.14.3.zip" +download_url = "https://beatmods.com/cdn/mod/918d13ac2821a3a17b2819f8861453e9.zip" sha256 = "b450fa4561da5b5ad834a3dcf6f127c2d73e5f971aa6c986514a6b185d68edfe" install_strategy = "bsipa-zip" reason = "BeatMods verified BS Utils 1.14.3 for Beat Saber 1.44.1 as version id 2563, zipHash 918d13ac2821a3a17b2819f8861453e9. Upstream GitHub releases do not expose a matching 1.14.3 asset, so this remains a BeatMods CDN fallback." @@ -131,6 +145,7 @@ reason = "BeatMods verified BS Utils 1.14.3 for Beat Saber 1.44.1 as version id id = "system-io-compression" tag = "beatmods-4.6.57" asset = "System.IO.Compression-4.6.57.zip" +download_url = "https://beatmods.com/cdn/mod/a4e9e26f61967e56168e08eecb01ab88.zip" sha256 = "9067ddaf52c077f38a27ace6180a8134ceaaf1ecba752d08de81b4cc0c13aa43" install_strategy = "bsipa-zip" reason = "BeatMods verified System.IO.Compression 4.6.57 for Beat Saber 1.44.1 as version id 1763, zipHash a4e9e26f61967e56168e08eecb01ab88. Use BeatMods CDN as a framework/library dependency payload." @@ -139,6 +154,7 @@ reason = "BeatMods verified System.IO.Compression 4.6.57 for Beat Saber 1.44.1 a id = "system-io-compression-filesystem" tag = "beatmods-4.7.3056" asset = "System.IO.Compression.FileSystem-4.7.3056.zip" +download_url = "https://beatmods.com/cdn/mod/e19f6fd395d54de7bfcbbbe3084dea28.zip" sha256 = "1b6f7a33a69980344717a37240ed80b022903a52968d4968f307cf754e3a03f7" install_strategy = "bsipa-zip" reason = "BeatMods verified System.IO.Compression.FileSystem 4.7.3056 for Beat Saber 1.44.1 as version id 1762, zipHash e19f6fd395d54de7bfcbbbe3084dea28. Use BeatMods CDN as a framework/library dependency payload." @@ -148,6 +164,7 @@ id = "imagesharp" repo = "SixLabors/ImageSharp" tag = "beatmods-2.0.0" asset = "ImageSharp-2.0.0.zip" +download_url = "https://beatmods.com/cdn/mod/b642fec88b0f84a0643ebd401d08da35.zip" sha256 = "b2bf6d195f25e1298199a389e381ab42f163b284105ae6181259d74f2528301b" install_strategy = "bsipa-zip" reason = "BeatMods verified ImageSharp 2.0.0 for Beat Saber 1.44.1 as version id 1428, zipHash b642fec88b0f84a0643ebd401d08da35. Use BeatMods CDN as a framework/library dependency payload." @@ -157,6 +174,7 @@ id = "beatsaberplaylistslib" repo = "Meivyn/BeatSaberPlaylistsLib" tag = "beatmods-1.7.2" asset = "BeatSaberPlaylistsLib-1.7.2.zip" +download_url = "https://beatmods.com/cdn/mod/a3418b75ed7294a3856f3eca12bbd672.zip" sha256 = "dfb26cf90cb73834c405727418c0121d8546cad0403abd35a09437dab766bd32" install_strategy = "bsipa-zip" reason = "BeatMods verified BeatSaberPlaylistsLib 1.7.2 for Beat Saber 1.44.1 as version id 2175, zipHash a3418b75ed7294a3856f3eca12bbd672. Upstream GitHub exposes no release assets through the releases API, so this remains a BeatMods CDN fallback." @@ -166,6 +184,7 @@ id = "beatsaversharp" repo = "Auros/BeatSaverSharper" tag = "beatmods-3.4.5" asset = "BeatSaverSharp-3.4.5.zip" +download_url = "https://beatmods.com/cdn/mod/be37e13e93d9ac7da4efbdc3f514fa8f.zip" sha256 = "f5f37b27438e9b2fa1d9fbdf51a4f015f44ae04979cbdd9b90f6ae18583a6911" install_strategy = "bsipa-zip" reason = "BeatMods verified BeatSaverSharp 3.4.5 for Beat Saber 1.44.1 as version id 1831, zipHash be37e13e93d9ac7da4efbdc3f514fa8f. Source repository is now Auros/BeatSaverSharper; this remains a BeatMods CDN fallback for the locked 3.4.5 package." @@ -174,6 +193,7 @@ reason = "BeatMods verified BeatSaverSharp 3.4.5 for Beat Saber 1.44.1 as versio id = "scoresabersharp" tag = "beatmods-0.1.0" asset = "ScoreSaberSharp-0.1.0.zip" +download_url = "https://beatmods.com/cdn/mod/8713168c598577ee7c73fa3cf0e26f5c.zip" sha256 = "7f30be996f8f0e997f2d848e34de1d03ef6dc744ffc32d3fe505881ca22c6cd3" install_strategy = "bsipa-zip" reason = "BeatMods verified ScoreSaberSharp 0.1.0 for Beat Saber 1.44.1 as version id 445, zipHash 8713168c598577ee7c73fa3cf0e26f5c. BeatMods lists scoresaber.com rather than a GitHub release source, so this remains a BeatMods CDN fallback." @@ -183,6 +203,7 @@ id = "scoresaber" repo = "ScoreSaber/pc-mod" tag = "v3.3.27" asset = "ScoreSaber-v3.3.27-bs1.42.0-to-1.44.0-9b4cfcf.zip" +download_url = "https://github.com/ScoreSaber/pc-mod/releases/download/v3.3.27/ScoreSaber-v3.3.27-bs1.42.0-to-1.44.0-9b4cfcf.zip" sha256 = "8ad509ab38353dfb4d92d127ba4e40917552f700580a7384509cafa70e62c096" install_strategy = "bsipa-zip" reason = "User-provided GitHub releases URL https://github.com/ScoreSaber/pc-mod/releases. Latest non-draft, non-prerelease release v3.3.27 includes this highest compatible asset, labeled for Beat Saber 1.42.0 through 1.44.0; GitHub release digest matched the downloaded asset." @@ -192,6 +213,7 @@ id = "protobuf-net" repo = "protobuf-net/protobuf-net" tag = "beatmods-3.0.102" asset = "protobuf-net-3.0.102.zip" +download_url = "https://beatmods.com/cdn/mod/1f55ae4b80b747b5f03fa18337ead864.zip" sha256 = "8bf4a361038172eab2c0c2e6737773d6c9a47fc6504a97304a35f697d5166414" install_strategy = "bsipa-zip" reason = "Required by SongDetailsCache. BeatMods verified protobuf-net 3.0.102 for Beat Saber 1.44.1 as version id 958, zipHash 1f55ae4b80b747b5f03fa18337ead864. Use BeatMods CDN as a framework/library dependency payload." @@ -201,6 +223,7 @@ id = "songdetailscache" repo = "kinsi55/BeatSaber_SongDetails" tag = "v1.4.0" asset = "SongDetailsCache.BS.Lib.zip" +download_url = "https://github.com/kinsi55/BeatSaber_SongDetails/releases/download/v1.4.0/SongDetailsCache.BS.Lib.zip" sha256 = "6ccc816dd40752b46d5e7e1cfb41e5af9d915d1a032be51ce1e52fb154daa28c" install_strategy = "bsipa-zip" reason = "Required by SongRankedBadge. BeatMods verified SongDetailsCache 1.4.0 for Beat Saber 1.44.1 as version id 2226, zipHash e1167b64cd3eff7e3651ec2dbbe50d81. GitHub asset is the same version selected from kinsi55/BeatSaber_SongDetails v1.4.0." @@ -210,6 +233,7 @@ id = "songrankedbadge" repo = "qe201020335/SongRankedBadge" tag = "v1.0.6" asset = "SongRankedBadge-1.0.6-bs1.40.0-88ee233.zip" +download_url = "https://github.com/qe201020335/SongRankedBadge/releases/download/v1.0.6/SongRankedBadge-1.0.6-bs1.40.0-88ee233.zip" sha256 = "6028f2392dd5e228f477837a7e32d95f0ca8af4f1b697b17c842ea62c050edff" install_strategy = "bsipa-zip" reason = "BeatMods verified SongRankedBadge 1.0.6 for Beat Saber 1.44.1 as version id 2267, zipHash c6944b8a4b00b0c0bb1d44f273b3bb18. GitHub release asset v1.0.6 was used." @@ -219,6 +243,7 @@ id = "beatleader" repo = "BeatLeader/beatleader-mod" tag = "v0.10.0" asset = "BeatLeader-0.10.0-bs1.42+.zip" +download_url = "https://github.com/BeatLeader/beatleader-mod/releases/download/v0.10.0/BeatLeader-0.10.0-bs1.42%2B.zip" sha256 = "0b7c2adcf4db9806cdc765164788a9394bca2ee12179fad1b8ed255da1b7104e" install_strategy = "bsipa-zip" reason = "User-provided GitHub releases URL https://github.com/BeatLeader/beatleader-mod/releases. Latest non-draft, non-prerelease release v0.10.0 includes this highest compatible asset, labeled for Beat Saber 1.42+; GitHub release digest matched the downloaded asset. The archive bundles Plugins/LeaderboardCore.dll, so the standalone LeaderboardCore lock entry is intentionally omitted." @@ -228,6 +253,7 @@ id = "beatsaverdownloader" repo = "Top-Cat/BeatSaverDownloader" tag = "beatmods-6.0.7" asset = "BeatSaverDownloader-6.0.7.zip" +download_url = "https://beatmods.com/cdn/mod/a740c6e68a9b5d1dfda3cc8e81f7cf06.zip" sha256 = "4108b11eae11d09f8c4e838dde1dc36108f1f3fd4fff31b951e7c551fa59f5f1" install_strategy = "bsipa-zip" reason = "BeatMods verified BeatSaverDownloader 6.0.7 for Beat Saber 1.44.1 as version id 2217, zipHash a740c6e68a9b5d1dfda3cc8e81f7cf06. Upstream GitHub exposes no release assets through the releases API, so this remains a BeatMods CDN fallback." @@ -237,6 +263,7 @@ id = "beatsaverupdater" repo = "ibillingsley/BeatSaverUpdater" tag = "1.2.11" asset = "BeatSaverUpdater-1.2.11-bs1.39.1-3698f98.zip" +download_url = "https://github.com/ibillingsley/BeatSaverUpdater/releases/download/1.2.11/BeatSaverUpdater-1.2.11-bs1.39.1-3698f98.zip" sha256 = "6237da05cbc044e99211cb0e569c917fe61bb0da25e42c61a6d438371548ebba" install_strategy = "bsipa-zip" reason = "BeatMods verified BeatSaverUpdater 1.2.11 for Beat Saber 1.44.1 as version id 2352, zipHash d9ea8dd0cbaac66cbb02fa59a548e42b. GitHub asset is byte-identical to the BeatMods CDN zip." @@ -246,6 +273,7 @@ id = "failbutton" repo = "qe201020335/FailButton" tag = "v0.0.4" asset = "FailButton-0.0.4-bs1.39.0-b6415fb.zip" +download_url = "https://github.com/qe201020335/FailButton/releases/download/v0.0.4/FailButton-0.0.4-bs1.39.0-b6415fb.zip" sha256 = "bb163170f400191592af1689e7b7557ec82e45e36de36e6eaaae16b4273e595b" install_strategy = "bsipa-zip" reason = "User-provided GitHub repository URL https://github.com/qe201020335/FailButton. Latest non-draft, non-prerelease release v0.0.4 exposes this asset; no newer 1.44.x-specific asset was available." @@ -255,6 +283,7 @@ id = "easyoffset" repo = "Reezonate/EasyOffset" tag = "v2.1.16" asset = "EasyOffset.dll" +download_url = "https://github.com/Reezonate/EasyOffset/releases/download/v2.1.16/EasyOffset.dll" sha256 = "6b9c63e5a4ae0ebc103fc11f442917b8fae12d62369faed1aca7470899a2bbbf" install_strategy = "dll-to-plugins" reason = "User-provided GitHub repository URL https://github.com/Reezonate/EasyOffset. Latest non-draft, non-prerelease release v2.1.16 exposes this direct DLL asset; GitHub release digest matched the downloaded asset." @@ -281,6 +310,7 @@ id = "gottagofast" repo = "kinsi55/CS_BeatSaber_GottaGoFast" tag = "v0.2.5" asset = "GottaGoFast.dll" +download_url = "https://github.com/kinsi55/CS_BeatSaber_GottaGoFast/releases/download/v0.2.5/GottaGoFast.dll" sha256 = "c9cd0e29dd7540027cdd3b9138335d81f93d69daeb8460d2fdfef31199e0e44e" install_strategy = "dll-to-plugins" reason = "User-provided GitHub repository URL https://github.com/kinsi55/CS_BeatSaber_GottaGoFast. Latest non-draft, non-prerelease release v0.2.5 is labeled for Beat Saber 1.42.0+ and exposes this direct DLL asset; GitHub release digest matched the downloaded asset." @@ -290,6 +320,7 @@ id = "keepmyoverridespls" repo = "qqrz997/KeepMyOverridesPls" tag = "v1.1.3-b" asset = "KeepMyOverridesPls-1.1.3-bs1.40.6-487d417.zip" +download_url = "https://github.com/qqrz997/KeepMyOverridesPls/releases/download/v1.1.3-b/KeepMyOverridesPls-1.1.3-bs1.40.6-487d417.zip" sha256 = "4c2da7349778eba98eb02ba518a1a056a784b719b974055c877aa6ce3e735fbc" install_strategy = "bsipa-zip" reason = "User-provided GitHub repository URL https://github.com/qqrz997/KeepMyOverridesPls. Latest non-draft, non-prerelease release v1.1.3-b exposes this asset; GitHub release digest matched the downloaded asset." @@ -317,6 +348,7 @@ id = "introskip" repo = "Loloppe/Intro-Skip" tag = "beatmods-4.0.8" asset = "IntroSkip-4.0.8.zip" +download_url = "https://beatmods.com/cdn/mod/7d98ae6049251eb4e3226a5c8ac675b3.zip" sha256 = "319bce38f69661a3b0997071b85d57e5b4295b9699ac5432877b0fed72aa48b9" install_strategy = "bsipa-zip" reason = "BeatMods verified IntroSkip 4.0.8 for Beat Saber 1.44.1 as version id 2570, zipHash 7d98ae6049251eb4e3226a5c8ac675b3. Use BeatMods CDN after the older GitHub 4.0.5 asset failed compatibility on this instance." @@ -326,6 +358,7 @@ id = "soundreplacer" repo = "Meivyn/SoundReplacer" tag = "beatmods-2.0.1" asset = "SoundReplacer-2.0.1.zip" +download_url = "https://beatmods.com/cdn/mod/7d7a869996e10249d1f85f95e060319b.zip" sha256 = "c14cc99cb0bd4ea4c3ab47345692489cbdfed8b2b577925200b1a8f6f4f93512" install_strategy = "bsipa-zip" reason = "BeatMods verified SoundReplacer 2.0.1 for Beat Saber 1.44.1 as version id 2213, zipHash 7d7a869996e10249d1f85f95e060319b. GitHub releases API returned no releases on 2026-06-29, so use the BeatMods CDN artifact." @@ -335,6 +368,7 @@ id = "bettersonglist" repo = "kinsi55/BeatSaber_BetterSongList" tag = "v0.4.3" asset = "BetterSongList.dll" +download_url = "https://github.com/kinsi55/BeatSaber_BetterSongList/releases/download/v0.4.3/BetterSongList.dll" sha256 = "50993315f41e5ca8e83ce7ded6be7780cfbe62ce248023849bd898068a4e25c1" install_strategy = "dll-to-plugins" reason = "User-provided GitHub repository URL https://github.com/kinsi55/BeatSaber_BetterSongList. Latest non-draft, non-prerelease release v0.4.3 exposes this direct DLL asset; GitHub release digest matched the downloaded asset." @@ -344,6 +378,7 @@ id = "hitscorevisualizer" repo = "ErisApps/HitScoreVisualizer" tag = "3.7.3" asset = "HitScoreVisualizer-3.7.3-bs1.42.0-a565cbb.zip" +download_url = "https://github.com/ErisApps/HitScoreVisualizer/releases/download/3.7.3/HitScoreVisualizer-3.7.3-bs1.42.0-a565cbb.zip" sha256 = "b889355cd47cc3b09a352f081110b8130ac6e1d285fa58776466117d585814db" install_strategy = "bsipa-zip" reason = "User-provided GitHub repository URL https://github.com/ErisApps/HitScoreVisualizer. Latest non-draft, non-prerelease release 3.7.3 exposes this asset for Beat Saber 1.42.0; GitHub release digest matched the downloaded asset." @@ -362,6 +397,7 @@ id = "hidethelogo" repo = "TheBlackParrot/HideTheLogo" tag = "1.0.3" asset = "HideTheLogo-1.0.3-bs1.40.3-c968d91.zip" +download_url = "https://github.com/TheBlackParrot/HideTheLogo/releases/download/1.0.3/HideTheLogo-1.0.3-bs1.40.3-c968d91.zip" sha256 = "f7177cf28add03ddd73fdbd720628755c7787f515523f2b92e0041c63e83d6ca" install_strategy = "bsipa-zip" reason = "User-provided GitHub repository URL https://github.com/TheBlackParrot/HideTheLogo. Latest non-draft, non-prerelease release 1.0.3 exposes this asset for Beat Saber 1.40.3; GitHub release did not expose a digest." @@ -371,6 +407,7 @@ id = "songchartvisualizer" repo = "NuggoDEV/SongChartVisualizer" tag = "beatmods-1.1.11" asset = "SongChartVisualizer-1.1.11.zip" +download_url = "https://beatmods.com/cdn/mod/5d3fc025fe098277667fc0846e1b8fe3.zip" sha256 = "290a55ff87d8769f3ae7c5d0b44a67e9e6e89e8ef83a2b1bb988ebea28f89da4" install_strategy = "bsipa-zip" reason = "BeatMods verified SongChartVisualizer 1.1.11 for Beat Saber 1.44.1 as version id 2249, zipHash 5d3fc025fe098277667fc0846e1b8fe3. User-provided GitHub repository URL https://github.com/NuggoDEV/SongChartVisualizer returned no releases through the GitHub releases API, so use the BeatMods CDN artifact." @@ -380,6 +417,7 @@ id = "adblocker" repo = "JonnyVR1/AdBlocker" tag = "beatmods-1.0.5" asset = "AdBlocker-1.0.5.zip" +download_url = "https://beatmods.com/cdn/mod/cd397e93b1a03f163534483462edf768.zip" sha256 = "0ee298563760d8c7f6ba7c8134982f9d1c2e554fe142648a3465051ac6468487" install_strategy = "bsipa-zip" reason = "BeatMods verified AdBlocker 1.0.5 for Beat Saber 1.44.1 as version id 1872, zipHash cd397e93b1a03f163534483462edf768. User-provided GitHub repository URL https://github.com/JonnyVR1/AdBlocker returned no releases through the GitHub releases API, so use the BeatMods CDN artifact." @@ -389,6 +427,7 @@ id = "highlightbombs" repo = "Meivyn/HighlightBombs" tag = "beatmods-1.0.3" asset = "HighlightBombs-1.0.3.zip" +download_url = "https://beatmods.com/cdn/mod/4bedaa80ce5dda8414fea7d914fb94ad.zip" sha256 = "fba7750632079dbce846ba57c2e3284fc72cb95671bff08394246e6664fe8113" install_strategy = "bsipa-zip" reason = "BeatMods verified HighlightBombs 1.0.3 for Beat Saber 1.44.1 as version id 2066, zipHash 4bedaa80ce5dda8414fea7d914fb94ad. GitHub latest release v1.0.1 is older than the BeatMods-verified 1.0.3 package, so use the BeatMods CDN artifact." @@ -398,6 +437,7 @@ id = "pitchblack" repo = "Loloppe/BeatSaber_PitchBlack" tag = "0.03" asset = "PitchBlack-v0.0.3-bs1.39.1.zip" +download_url = "https://github.com/Loloppe/BeatSaber_PitchBlack/releases/download/0.03/PitchBlack-v0.0.3-bs1.39.1.zip" sha256 = "ae7269b4f40fea8bee3d2b213a67c3e779ff8bfbcb4f4deabcb9e9c21bfadafb" install_strategy = "bsipa-zip" reason = "BeatMods verified PitchBlack 0.0.3 for Beat Saber 1.44.1 as version id 2233, zipHash 65656bf33b2a0b356c381132387ea7ea. User-provided GitHub repository URL https://github.com/Loloppe/BeatSaber_PitchBlack tag 0.03 asset is byte-identical to the BeatMods CDN zip." @@ -407,6 +447,7 @@ id = "reecamera" repo = "Reezonate/ReeCamera" tag = "v0.0.5" asset = "ReeCamera.1.42.0.zip" +download_url = "https://github.com/Reezonate/ReeCamera/releases/download/v0.0.5/ReeCamera.1.42.0.zip" sha256 = "76fb8efa1103ed18d7913c63818cb2699785fb3718d394176d5e2a6938842c24" install_strategy = "root-zip" reason = "User-provided GitHub repository URL https://github.com/Reezonate/ReeCamera. Latest non-draft, non-prerelease release v0.0.5 offers ReeCamera.1.42.0.zip as the highest Beat Saber version asset for instance 1.44.1; GitHub release digest matched the downloaded asset. Archive bundles Plugins/ReeCamera.dll, bundled CameraUtils.dll, and UserData/ReeCamera presets." diff --git a/src/plugin_helper/models.py b/src/plugin_helper/models.py index 78d5ff1..9559898 100644 --- a/src/plugin_helper/models.py +++ b/src/plugin_helper/models.py @@ -42,6 +42,7 @@ class LockedPlugin: tag: str | None asset: str | None sha256: str | None + download_url: str | None = None install_strategy: str | None = None reason: str | None = None @@ -72,6 +73,8 @@ def lockfile_to_toml(lockfile: Lockfile) -> str: lines.append(f"tag = {_quote_toml_string(plugin.tag)}") if plugin.asset is not None: lines.append(f"asset = {_quote_toml_string(plugin.asset)}") + if plugin.download_url is not None: + lines.append(f"download_url = {_quote_toml_string(plugin.download_url)}") if plugin.sha256 is not None: lines.append(f"sha256 = {_quote_toml_string(plugin.sha256)}") if plugin.install_strategy is not None: @@ -156,6 +159,7 @@ def load_lockfile(path: Path) -> Lockfile: tag=item.get("tag"), asset=item.get("asset"), sha256=item.get("sha256"), + download_url=item.get("download_url"), install_strategy=item.get("install_strategy"), reason=item.get("reason"), ) diff --git a/src/plugin_helper/update_runner.py b/src/plugin_helper/update_runner.py index 98a4b2d..30ea0cf 100644 --- a/src/plugin_helper/update_runner.py +++ b/src/plugin_helper/update_runner.py @@ -66,6 +66,7 @@ def _updated_locked_plugin( tag=plugin.get("latestTag"), asset=plugin.get("latestAsset"), sha256=actual_sha256, + download_url=plugin.get("latestAssetUrl"), reason=_reason_for_update( plugin, actual_sha256=actual_sha256, diff --git a/tests/test_plugin_helper.py b/tests/test_plugin_helper.py index 18e20f4..c2e6bd6 100644 --- a/tests/test_plugin_helper.py +++ b/tests/test_plugin_helper.py @@ -22,7 +22,7 @@ from plugin_helper.config import is_windows, load_local_config, resolve_runtime_ from plugin_helper.fsutil import sha256_file from plugin_helper.installer import apply_plan, disable_plugin, uninstall_plugin from plugin_helper.instances import get_instance, list_instances -from plugin_helper.models import Dependency, Lockfile, LockedPlugin, Registry, RegistryPlugin, load_lockfile, load_registry +from plugin_helper.models import Dependency, Lockfile, LockedPlugin, Registry, RegistryPlugin, load_lockfile, load_registry, write_lockfile from plugin_helper.operations import enable_disabled_plugin from plugin_helper.planner import create_plan from plugin_helper.scanner import scan_bootstrap_files, scan_instance @@ -82,6 +82,57 @@ dependencies = [ self.assertEqual(plugin.asset_patterns, ("Example-*.zip",)) self.assertEqual(plugin.dependencies, (Dependency(id="bsipa", constraint=">=4.3.7"),)) + def test_lockfile_download_url_is_optional_and_round_trips(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + old_lock_path = root / "old.lock.toml" + old_lock_path.write_text( + """ +beat_saber_version = "1.40.8" +instance = "1.40.8" + +[[plugins]] +id = "old" +repo = "owner/old" +tag = "v1" +asset = "Old.zip" +sha256 = "abc" +install_strategy = "bsipa-zip" +""".lstrip(), + encoding="utf-8", + ) + + old_lock = load_lockfile(old_lock_path) + + self.assertIsNone(old_lock.plugins[0].download_url) + + new_lock_path = root / "new.lock.toml" + write_lockfile( + new_lock_path, + Lockfile( + beat_saber_version="1.40.8", + instance="1.40.8", + plugins=( + LockedPlugin( + id="example", + repo="owner/example", + tag="v2", + asset="Example.zip", + download_url="https://example.invalid/Example.zip", + sha256="def", + install_strategy="bsipa-zip", + ), + ), + ), + ) + text = new_lock_path.read_text(encoding="utf-8") + reloaded = load_lockfile(new_lock_path) + + self.assertIn('download_url = "https://example.invalid/Example.zip"', text) + self.assertLess(text.index("asset = "), text.index("download_url = ")) + self.assertLess(text.index("download_url = "), text.index("sha256 = ")) + self.assertEqual(reloaded.plugins[0].download_url, "https://example.invalid/Example.zip") + def test_normalize_beatmods_current_nested_response(self) -> None: payload = { "mods": [ @@ -2015,6 +2066,7 @@ instance = "1.40.8" self.assertEqual(updated_lock.plugins[0].tag, "v2.0.0") self.assertEqual(updated_lock.plugins[0].asset, "Example-v2.zip") self.assertEqual(updated_lock.plugins[0].sha256, source_sha) + self.assertEqual(updated_lock.plugins[0].download_url, "https://example.invalid/Example-v2.zip") self.assertIn("GitHub release digest matched", updated_lock.plugins[0].reason or "") def test_update_command_prepares_beatmods_update(self) -> None: @@ -2080,6 +2132,7 @@ instance = "1.40.8" updated_lock = load_lockfile(lock_path) self.assertEqual(updated_lock.plugins[0].tag, "beatmods-3.17.0") self.assertEqual(updated_lock.plugins[0].asset, "SongCore-3.17.0.zip") + self.assertEqual(updated_lock.plugins[0].download_url, "https://beatmods.com/cdn/mod/abc.zip") self.assertIn("version id 2600", updated_lock.plugins[0].reason or "") self.assertIn("zipHash abc", updated_lock.plugins[0].reason or "")