upd-cli 0.1.1__tar.gz → 0.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. {upd_cli-0.1.1 → upd_cli-0.1.3}/CHANGELOG.md +45 -0
  2. {upd_cli-0.1.1 → upd_cli-0.1.3}/Cargo.lock +115 -1
  3. {upd_cli-0.1.1 → upd_cli-0.1.3}/Cargo.toml +2 -1
  4. {upd_cli-0.1.1 → upd_cli-0.1.3}/PKG-INFO +77 -2
  5. {upd_cli-0.1.1 → upd_cli-0.1.3}/README.md +76 -1
  6. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/audit/mod.rs +38 -39
  7. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/cache.rs +83 -1
  8. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/cli.rs +40 -2
  9. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/config.rs +324 -2
  10. upd_cli-0.1.3/src/cooldown.rs +926 -0
  11. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/lib.rs +2 -1
  12. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/lockfile.rs +174 -71
  13. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/main.rs +459 -15
  14. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/output.rs +142 -0
  15. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/registry/crates_io.rs +92 -6
  16. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/registry/github_releases.rs +104 -1
  17. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/registry/go_proxy.rs +119 -1
  18. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/registry/mock.rs +61 -1
  19. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/registry/mod.rs +47 -0
  20. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/registry/npm.rs +150 -1
  21. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/registry/nuget.rs +14 -0
  22. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/registry/pypi.rs +109 -4
  23. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/registry/rubygems.rs +73 -1
  24. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/updater/cargo_toml.rs +65 -6
  25. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/updater/csproj.rs +55 -0
  26. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/updater/gemfile.rs +61 -3
  27. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/updater/github_actions.rs +53 -1
  28. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/updater/go_mod.rs +58 -1
  29. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/updater/mise.rs +55 -1
  30. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/updater/mod.rs +403 -1
  31. upd_cli-0.1.3/src/updater/npm_range.rs +257 -0
  32. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/updater/package_json.rs +485 -20
  33. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/updater/pre_commit.rs +53 -1
  34. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/updater/pyproject.rs +120 -10
  35. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/updater/requirements.rs +56 -0
  36. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/updater/terraform.rs +1 -0
  37. upd_cli-0.1.3/src/version/compare.rs +79 -0
  38. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/version/mod.rs +1 -0
  39. upd_cli-0.1.3/tests/cooldown_e2e.rs +77 -0
  40. {upd_cli-0.1.1 → upd_cli-0.1.3}/.mise.toml +0 -0
  41. {upd_cli-0.1.1 → upd_cli-0.1.3}/.pre-commit-config.yaml +0 -0
  42. {upd_cli-0.1.1 → upd_cli-0.1.3}/.pre-commit-hooks.yaml +0 -0
  43. {upd_cli-0.1.1 → upd_cli-0.1.3}/.rumdl.toml +0 -0
  44. {upd_cli-0.1.1 → upd_cli-0.1.3}/LICENSE +0 -0
  45. {upd_cli-0.1.1 → upd_cli-0.1.3}/Makefile +0 -0
  46. {upd_cli-0.1.1 → upd_cli-0.1.3}/assets/logo-wide.svg +0 -0
  47. {upd_cli-0.1.1 → upd_cli-0.1.3}/assets/logo.svg +0 -0
  48. {upd_cli-0.1.1 → upd_cli-0.1.3}/pyproject.toml +0 -0
  49. {upd_cli-0.1.1 → upd_cli-0.1.3}/python/upd_cli/__init__.py +0 -0
  50. {upd_cli-0.1.1 → upd_cli-0.1.3}/python/upd_cli/__main__.py +0 -0
  51. {upd_cli-0.1.1 → upd_cli-0.1.3}/python/upd_cli/py.typed +0 -0
  52. {upd_cli-0.1.1 → upd_cli-0.1.3}/rust-toolchain.toml +0 -0
  53. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/align.rs +0 -0
  54. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/audit/cache.rs +0 -0
  55. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/audit/cvss.rs +0 -0
  56. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/interactive.rs +0 -0
  57. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/registry/terraform.rs +0 -0
  58. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/registry/utils.rs +0 -0
  59. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/version/pep440.rs +0 -0
  60. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/version/semver_util.rs +0 -0
  61. {upd_cli-0.1.1 → upd_cli-0.1.3}/src/version/tag.rs +0 -0
  62. {upd_cli-0.1.1 → upd_cli-0.1.3}/tests/audit_offline.rs +0 -0
  63. {upd_cli-0.1.1 → upd_cli-0.1.3}/tests/audit_sarif.rs +0 -0
  64. {upd_cli-0.1.1 → upd_cli-0.1.3}/tests/audit_severity.rs +0 -0
  65. {upd_cli-0.1.1 → upd_cli-0.1.3}/tests/bump_filter.rs +0 -0
  66. {upd_cli-0.1.1 → upd_cli-0.1.3}/tests/exit_codes.rs +0 -0
  67. {upd_cli-0.1.1 → upd_cli-0.1.3}/tests/fix_audit.rs +0 -0
  68. {upd_cli-0.1.1 → upd_cli-0.1.3}/tests/format_json.rs +0 -0
  69. {upd_cli-0.1.1 → upd_cli-0.1.3}/tests/help_text.rs +0 -0
  70. {upd_cli-0.1.1 → upd_cli-0.1.3}/tests/interactive_tty.rs +0 -0
  71. {upd_cli-0.1.1 → upd_cli-0.1.3}/tests/invalid_positional.rs +0 -0
  72. {upd_cli-0.1.1 → upd_cli-0.1.3}/tests/no_args_scope.rs +0 -0
  73. {upd_cli-0.1.1 → upd_cli-0.1.3}/tests/output_streams.rs +0 -0
  74. {upd_cli-0.1.1 → upd_cli-0.1.3}/tests/package_filter.rs +0 -0
  75. {upd_cli-0.1.1 → upd_cli-0.1.3}/vership.toml +0 -0
@@ -11,6 +11,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
 
12
12
 
13
13
 
14
+
15
+
16
+ ## [0.1.3](https://github.com/rvben/upd/compare/v0.1.2...v0.1.3) - 2026-04-25
17
+
18
+ ### Added
19
+
20
+ - **lock**: scope lockfile regeneration to the packages upd actually changed ([6b6cfa6](https://github.com/rvben/upd/commit/6b6cfa6fe3e8e8d787e78c7afea24883ebe67833))
21
+ - **npm**: classify and rewrite comparator-range specs ([a60979a](https://github.com/rvben/upd/commit/a60979acd7edb7eb6adfac554a590412a6cf8271))
22
+
23
+ ### Fixed
24
+
25
+ - **lock**: include config pins in targeted regenerate and update CLI help ([207fdf9](https://github.com/rvben/upd/commit/207fdf94af95c74865f828f258ec9cfa61d22085))
26
+ - **npm**: preserve upper bound when pinning comparator-range specs ([fb7c863](https://github.com/rvben/upd/commit/fb7c863de9f07201af310ab13a59696d17fcb766))
27
+ - **npm**: apply config policy and cooldown to comparator-range updates ([be095dd](https://github.com/rvben/upd/commit/be095dd30443dd547fb6913dae551cbd530cb019))
28
+ - **npm**: update comparator-range specs via constraint-aware resolution ([4481b8b](https://github.com/rvben/upd/commit/4481b8bad6a02c6f761cf489a547b5546099e871))
29
+ - **audit**: order fix versions numerically, not lexicographically ([069eb1d](https://github.com/rvben/upd/commit/069eb1dc8771640376957339e40ada7b60a82b0a))
30
+
31
+ ## [0.1.2](https://github.com/rvben/upd/compare/v0.1.1...v0.1.2) - 2026-04-24
32
+
33
+ ### Added
34
+
35
+ - **cache**: add optional versions field to CacheEntry for future list_versions caching ([1beb34d](https://github.com/rvben/upd/commit/1beb34dc030f160e3748dff9a63e71bfa1772043))
36
+ - **output**: report held-back and skipped-by-cooldown packages ([3d1a2ce](https://github.com/rvben/upd/commit/3d1a2cef2ae31c59a87b417b074e0d672b7256d2))
37
+ - **updater**: propagate cooldown policy to remaining updaters ([8e80f25](https://github.com/rvben/upd/commit/8e80f252339f022d90f8120694e529c68c3bcf90))
38
+ - **updater**: apply cooldown policy in requirements updater ([5d6cfd3](https://github.com/rvben/upd/commit/5d6cfd32bfe87df7af86453476a93e2945f009ff))
39
+ - **registry**: implement list_versions for GitHub releases ([5f6472b](https://github.com/rvben/upd/commit/5f6472b5d7c4394d59fbabfd4bd9a1d9b736a67a))
40
+ - **registry**: implement list_versions for RubyGems ([1a1dda3](https://github.com/rvben/upd/commit/1a1dda31d73e0f25d8a73e6438aed7c4daadc007))
41
+ - **registry**: implement list_versions for Go module proxy ([196fef6](https://github.com/rvben/upd/commit/196fef634b0ae3c53096222e8bbe3161d8b67a33))
42
+ - **registry**: implement list_versions for crates.io ([8869dec](https://github.com/rvben/upd/commit/8869dec1a69148b5b3db44cc3393a05aaa2b01fb))
43
+ - **registry**: implement list_versions for npm ([b23cd78](https://github.com/rvben/upd/commit/b23cd787e748dfc5404a8fd51981c67f858e1d5a))
44
+ - **registry**: implement list_versions for PyPI ([9aa342c](https://github.com/rvben/upd/commit/9aa342c5c11ae504024aed5aa2d8930c66b4a6df))
45
+ - **cli**: add --min-age flag for cooldown override ([b5bfb30](https://github.com/rvben/upd/commit/b5bfb304c39f6b8099aef3a066e2ef4ed17f606f))
46
+ - **config**: show cooldown policy in --show-config ([9486257](https://github.com/rvben/upd/commit/9486257b22456e55e9af23709089a574cce262be))
47
+ - **config**: add [cooldown] table with default and per-ecosystem overrides ([a9ff8e3](https://github.com/rvben/upd/commit/a9ff8e31050485056a9bb6e03f4d313df1262998))
48
+ - **cooldown**: implement select() selection algorithm ([8b588bb](https://github.com/rvben/upd/commit/8b588bb1b42d84d696a7a17d8e97141a223351a1))
49
+ - **cooldown**: add CooldownPolicy with precedence resolution ([ddba284](https://github.com/rvben/upd/commit/ddba284329dad87bf84cf566ecb487ef665408a1))
50
+ - **cooldown**: add parse_duration for release-age config ([a7e67e0](https://github.com/rvben/upd/commit/a7e67e035764e4d905ace1dc4092f41c27510a5c))
51
+ - **registry**: re-export VersionMeta from crate root ([b2cdd60](https://github.com/rvben/upd/commit/b2cdd6037c09110881f53db4992542e77596f6c3))
52
+ - **registry**: add VersionMeta and list_versions trait method ([09ddbf9](https://github.com/rvben/upd/commit/09ddbf9c2b8f0546feafeb642f27547bed1882da))
53
+
54
+ ### Fixed
55
+
56
+ - **cooldown**: harden selection against real-world constraints and per-file policy ([a284ea4](https://github.com/rvben/upd/commit/a284ea497dcf3abf65fda2c7e7f6c0c03c3dd8e2))
57
+ - **updater**: pass Poetry constraint to cooldown selection ([a0383e9](https://github.com/rvben/upd/commit/a0383e9167b2ef05cc4583aa23c1035d32268750))
58
+
14
59
  ## [0.1.1](https://github.com/rvben/upd/compare/v0.1.0...v0.1.1) - 2026-04-22
15
60
 
16
61
  ### Added
@@ -17,6 +17,15 @@ dependencies = [
17
17
  "memchr",
18
18
  ]
19
19
 
20
+ [[package]]
21
+ name = "android_system_properties"
22
+ version = "0.1.5"
23
+ source = "registry+https://github.com/rust-lang/crates.io-index"
24
+ checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
25
+ dependencies = [
26
+ "libc",
27
+ ]
28
+
20
29
  [[package]]
21
30
  name = "anstream"
22
31
  version = "1.0.0"
@@ -113,6 +122,12 @@ version = "1.1.2"
113
122
  source = "registry+https://github.com/rust-lang/crates.io-index"
114
123
  checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
115
124
 
125
+ [[package]]
126
+ name = "autocfg"
127
+ version = "1.5.0"
128
+ source = "registry+https://github.com/rust-lang/crates.io-index"
129
+ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
130
+
116
131
  [[package]]
117
132
  name = "aws-lc-rs"
118
133
  version = "1.16.3"
@@ -199,6 +214,18 @@ version = "0.2.1"
199
214
  source = "registry+https://github.com/rust-lang/crates.io-index"
200
215
  checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
201
216
 
217
+ [[package]]
218
+ name = "chrono"
219
+ version = "0.4.44"
220
+ source = "registry+https://github.com/rust-lang/crates.io-index"
221
+ checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
222
+ dependencies = [
223
+ "iana-time-zone",
224
+ "num-traits",
225
+ "serde",
226
+ "windows-link",
227
+ ]
228
+
202
229
  [[package]]
203
230
  name = "clap"
204
231
  version = "4.6.1"
@@ -728,6 +755,30 @@ dependencies = [
728
755
  "tracing",
729
756
  ]
730
757
 
758
+ [[package]]
759
+ name = "iana-time-zone"
760
+ version = "0.1.65"
761
+ source = "registry+https://github.com/rust-lang/crates.io-index"
762
+ checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
763
+ dependencies = [
764
+ "android_system_properties",
765
+ "core-foundation-sys",
766
+ "iana-time-zone-haiku",
767
+ "js-sys",
768
+ "log",
769
+ "wasm-bindgen",
770
+ "windows-core",
771
+ ]
772
+
773
+ [[package]]
774
+ name = "iana-time-zone-haiku"
775
+ version = "0.1.2"
776
+ source = "registry+https://github.com/rust-lang/crates.io-index"
777
+ checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
778
+ dependencies = [
779
+ "cc",
780
+ ]
781
+
731
782
  [[package]]
732
783
  name = "icu_collections"
733
784
  version = "2.1.1"
@@ -1030,6 +1081,15 @@ dependencies = [
1030
1081
  "windows-sys 0.61.2",
1031
1082
  ]
1032
1083
 
1084
+ [[package]]
1085
+ name = "num-traits"
1086
+ version = "0.2.19"
1087
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1088
+ checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
1089
+ dependencies = [
1090
+ "autocfg",
1091
+ ]
1092
+
1033
1093
  [[package]]
1034
1094
  name = "num_cpus"
1035
1095
  version = "1.17.0"
@@ -1964,10 +2024,11 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
1964
2024
 
1965
2025
  [[package]]
1966
2026
  name = "upd"
1967
- version = "0.1.1"
2027
+ version = "0.1.3"
1968
2028
  dependencies = [
1969
2029
  "anyhow",
1970
2030
  "async-trait",
2031
+ "chrono",
1971
2032
  "clap",
1972
2033
  "colored",
1973
2034
  "directories",
@@ -2143,12 +2204,65 @@ dependencies = [
2143
2204
  "windows-sys 0.61.2",
2144
2205
  ]
2145
2206
 
2207
+ [[package]]
2208
+ name = "windows-core"
2209
+ version = "0.62.2"
2210
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2211
+ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
2212
+ dependencies = [
2213
+ "windows-implement",
2214
+ "windows-interface",
2215
+ "windows-link",
2216
+ "windows-result",
2217
+ "windows-strings",
2218
+ ]
2219
+
2220
+ [[package]]
2221
+ name = "windows-implement"
2222
+ version = "0.60.2"
2223
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2224
+ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
2225
+ dependencies = [
2226
+ "proc-macro2",
2227
+ "quote",
2228
+ "syn",
2229
+ ]
2230
+
2231
+ [[package]]
2232
+ name = "windows-interface"
2233
+ version = "0.59.3"
2234
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2235
+ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
2236
+ dependencies = [
2237
+ "proc-macro2",
2238
+ "quote",
2239
+ "syn",
2240
+ ]
2241
+
2146
2242
  [[package]]
2147
2243
  name = "windows-link"
2148
2244
  version = "0.2.1"
2149
2245
  source = "registry+https://github.com/rust-lang/crates.io-index"
2150
2246
  checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
2151
2247
 
2248
+ [[package]]
2249
+ name = "windows-result"
2250
+ version = "0.4.1"
2251
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2252
+ checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
2253
+ dependencies = [
2254
+ "windows-link",
2255
+ ]
2256
+
2257
+ [[package]]
2258
+ name = "windows-strings"
2259
+ version = "0.5.1"
2260
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2261
+ checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
2262
+ dependencies = [
2263
+ "windows-link",
2264
+ ]
2265
+
2152
2266
  [[package]]
2153
2267
  name = "windows-sys"
2154
2268
  version = "0.45.0"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "upd"
3
- version = "0.1.1"
3
+ version = "0.1.3"
4
4
  edition = "2024"
5
5
  rust-version = "1.95.0"
6
6
  description = "A fast dependency updater for Python, Node.js, Rust, Go, Ruby, Terraform, GitHub Actions, pre-commit, and Mise projects"
@@ -60,6 +60,7 @@ colored = "3.1.1"
60
60
  async-trait = "0.1.89"
61
61
  futures = "0.3.32"
62
62
  url = "2.5"
63
+ chrono = { version = "0.4", default-features = false, features = ["serde", "clock"] }
63
64
 
64
65
  [dev-dependencies]
65
66
  tempfile = "3.27.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: upd-cli
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -46,7 +46,10 @@ pipx run --spec upd-cli upd --apply
46
46
 
47
47
  - **Multi-ecosystem**: Python, Node.js, Rust, Go, Ruby, .NET, Terraform, GitHub Actions, pre-commit, Mise/asdf
48
48
  - **Fast**: Parallel registry requests for all dependencies
49
- - **Constraint-aware**: Respects version constraints like `>=2.0,<3` and `~> 7.1`
49
+ - **Constraint-aware**: Respects `>=2.0,<3` (Python), `~> 7.1` (Ruby), and `^2.0.0` / `~2.0.0` (npm, Cargo).
50
+ For npm, comparator ranges such as `">=1.0.0 <2.0.0"` are rewritten with a **bump strategy**: the lower
51
+ bound moves to the highest version satisfying the constraint, preserving the upper bound. Hyphen
52
+ (`"1 - 2"`) and OR (`"^1 || ^2"`) ranges are reported as warnings and left untouched.
50
53
  - **Smart caching**: 24-hour version cache for faster subsequent runs
51
54
  - **Update filters**: Filter by bump level with `--only-bump <major|minor|patch>` (repeatable) or cap with `--max-bump`
52
55
  - **Interactive mode**: Approve updates individually with `-i`
@@ -469,6 +472,51 @@ upd --verbose
469
472
  # pyproject.toml:13: Skipped internal-utils 1.0.0 (ignored)
470
473
  ```
471
474
 
475
+ ## Cooldown (minimum release age)
476
+
477
+ Hold back updates to versions that have been public for less than N days.
478
+ Reduces exposure to supply-chain attacks that rely on freshly published
479
+ malicious versions being installed before detection. Modelled after
480
+ Renovate's `minimumReleaseAge` / Dependabot's `cooldown`.
481
+
482
+ Enable in `.updrc.toml`:
483
+
484
+ ```toml
485
+ [cooldown]
486
+ default = "7d" # applies to every ecosystem unless overridden
487
+
488
+ [cooldown.ecosystem]
489
+ npm = "14d" # stricter for npm
490
+ pypi = "14d"
491
+ "crates.io" = "3d"
492
+ ```
493
+
494
+ Duration syntax: `<integer><unit>` where unit is `s`, `m`, `h`, `d`, `w`.
495
+ A bare `0` disables cooldown.
496
+
497
+ Override from the CLI for one-off runs:
498
+
499
+ ```text
500
+ upd --min-age 14d # use 14 days regardless of config
501
+ upd --min-age 0 # disable cooldown entirely for this run
502
+ ```
503
+
504
+ **How it works:** when the latest version is still inside the cooldown
505
+ window, `upd` updates to the newest version that *is* old enough. If nothing
506
+ newer is old enough yet, the package is held back. Output marks these
507
+ packages explicitly:
508
+
509
+ ```text
510
+ requirements.txt: Updated requests 2.28.0 → 2.31.0
511
+ package.json: Held back lodash 4.17.20 → 4.17.21 (4.17.22 released 2d ago, cooldown 7d)
512
+ package.json: Skipped express (only newer version 4.19.0 released 1d ago, cooldown 7d)
513
+ ```
514
+
515
+ **Supported ecosystems:** PyPI, npm, crates.io, Go modules, RubyGems,
516
+ GitHub releases (covers GitHub Actions, pre-commit, Mise). NuGet and
517
+ Terraform Registry do not expose per-version publish dates we can
518
+ consume today; cooldown is reported as unavailable for those files.
519
+
472
520
  ## Caching
473
521
 
474
522
  Version lookups are cached for 24 hours in:
@@ -722,6 +770,33 @@ Global flags (accepted on every subcommand):
722
770
 
723
771
  Subcommands: `update` (default), `align`, `audit`, `clean-cache`, `self-update`.
724
772
 
773
+ #### Commands run by `--lock`
774
+
775
+ `upd --lock` runs the narrowest per-ecosystem refresh command that
776
+ updates only the packages `upd` just rewrote. Targeted forms are used
777
+ wherever the package manager supports them; targeting falls back to
778
+ `--lockfile-only` flags where no per-package form exists; otherwise
779
+ the manifest-wide refresh command is used.
780
+
781
+ | Ecosystem | Lockfile | Command |
782
+ |-----------|--------------------------|------------------------------------------------|
783
+ | Python | `poetry.lock` | `poetry lock --no-update` |
784
+ | Python | `uv.lock` | `uv lock` |
785
+ | Node | `package-lock.json` | `npm install --package-lock-only` |
786
+ | Node | `yarn.lock` | `yarn install --mode update-lockfile` (Yarn 2+)|
787
+ | Node | `pnpm-lock.yaml` | `pnpm install --lockfile-only` |
788
+ | Node | `bun.lockb` | `bun install` |
789
+ | Rust | `Cargo.lock` | `cargo update -p <changed> -p <changed> …` |
790
+ | Go | `go.sum` | `go mod tidy` (no targeted form) |
791
+ | Ruby | `Gemfile.lock` | `bundle lock --update <changed> …` |
792
+ | .NET | `packages.lock.json` | `dotnet restore` (no targeted form) |
793
+ | Terraform | `.terraform.lock.hcl` | `terraform providers lock` (no targeted form) |
794
+
795
+ Manifests whose `upd` pass produced zero changes have their lockfile
796
+ refresh skipped entirely. A directory where only config pins were
797
+ applied is still refreshed, and the changed-package list includes
798
+ those pinned packages so `cargo update -p <pkg>` / `bundle lock --update <pkg>` stay scoped.
799
+
725
800
  Stable `audit`-specific flags:
726
801
 
727
802
  | Flag | Purpose |
@@ -23,7 +23,10 @@ pipx run --spec upd-cli upd --apply
23
23
 
24
24
  - **Multi-ecosystem**: Python, Node.js, Rust, Go, Ruby, .NET, Terraform, GitHub Actions, pre-commit, Mise/asdf
25
25
  - **Fast**: Parallel registry requests for all dependencies
26
- - **Constraint-aware**: Respects version constraints like `>=2.0,<3` and `~> 7.1`
26
+ - **Constraint-aware**: Respects `>=2.0,<3` (Python), `~> 7.1` (Ruby), and `^2.0.0` / `~2.0.0` (npm, Cargo).
27
+ For npm, comparator ranges such as `">=1.0.0 <2.0.0"` are rewritten with a **bump strategy**: the lower
28
+ bound moves to the highest version satisfying the constraint, preserving the upper bound. Hyphen
29
+ (`"1 - 2"`) and OR (`"^1 || ^2"`) ranges are reported as warnings and left untouched.
27
30
  - **Smart caching**: 24-hour version cache for faster subsequent runs
28
31
  - **Update filters**: Filter by bump level with `--only-bump <major|minor|patch>` (repeatable) or cap with `--max-bump`
29
32
  - **Interactive mode**: Approve updates individually with `-i`
@@ -446,6 +449,51 @@ upd --verbose
446
449
  # pyproject.toml:13: Skipped internal-utils 1.0.0 (ignored)
447
450
  ```
448
451
 
452
+ ## Cooldown (minimum release age)
453
+
454
+ Hold back updates to versions that have been public for less than N days.
455
+ Reduces exposure to supply-chain attacks that rely on freshly published
456
+ malicious versions being installed before detection. Modelled after
457
+ Renovate's `minimumReleaseAge` / Dependabot's `cooldown`.
458
+
459
+ Enable in `.updrc.toml`:
460
+
461
+ ```toml
462
+ [cooldown]
463
+ default = "7d" # applies to every ecosystem unless overridden
464
+
465
+ [cooldown.ecosystem]
466
+ npm = "14d" # stricter for npm
467
+ pypi = "14d"
468
+ "crates.io" = "3d"
469
+ ```
470
+
471
+ Duration syntax: `<integer><unit>` where unit is `s`, `m`, `h`, `d`, `w`.
472
+ A bare `0` disables cooldown.
473
+
474
+ Override from the CLI for one-off runs:
475
+
476
+ ```text
477
+ upd --min-age 14d # use 14 days regardless of config
478
+ upd --min-age 0 # disable cooldown entirely for this run
479
+ ```
480
+
481
+ **How it works:** when the latest version is still inside the cooldown
482
+ window, `upd` updates to the newest version that *is* old enough. If nothing
483
+ newer is old enough yet, the package is held back. Output marks these
484
+ packages explicitly:
485
+
486
+ ```text
487
+ requirements.txt: Updated requests 2.28.0 → 2.31.0
488
+ package.json: Held back lodash 4.17.20 → 4.17.21 (4.17.22 released 2d ago, cooldown 7d)
489
+ package.json: Skipped express (only newer version 4.19.0 released 1d ago, cooldown 7d)
490
+ ```
491
+
492
+ **Supported ecosystems:** PyPI, npm, crates.io, Go modules, RubyGems,
493
+ GitHub releases (covers GitHub Actions, pre-commit, Mise). NuGet and
494
+ Terraform Registry do not expose per-version publish dates we can
495
+ consume today; cooldown is reported as unavailable for those files.
496
+
449
497
  ## Caching
450
498
 
451
499
  Version lookups are cached for 24 hours in:
@@ -699,6 +747,33 @@ Global flags (accepted on every subcommand):
699
747
 
700
748
  Subcommands: `update` (default), `align`, `audit`, `clean-cache`, `self-update`.
701
749
 
750
+ #### Commands run by `--lock`
751
+
752
+ `upd --lock` runs the narrowest per-ecosystem refresh command that
753
+ updates only the packages `upd` just rewrote. Targeted forms are used
754
+ wherever the package manager supports them; targeting falls back to
755
+ `--lockfile-only` flags where no per-package form exists; otherwise
756
+ the manifest-wide refresh command is used.
757
+
758
+ | Ecosystem | Lockfile | Command |
759
+ |-----------|--------------------------|------------------------------------------------|
760
+ | Python | `poetry.lock` | `poetry lock --no-update` |
761
+ | Python | `uv.lock` | `uv lock` |
762
+ | Node | `package-lock.json` | `npm install --package-lock-only` |
763
+ | Node | `yarn.lock` | `yarn install --mode update-lockfile` (Yarn 2+)|
764
+ | Node | `pnpm-lock.yaml` | `pnpm install --lockfile-only` |
765
+ | Node | `bun.lockb` | `bun install` |
766
+ | Rust | `Cargo.lock` | `cargo update -p <changed> -p <changed> …` |
767
+ | Go | `go.sum` | `go mod tidy` (no targeted form) |
768
+ | Ruby | `Gemfile.lock` | `bundle lock --update <changed> …` |
769
+ | .NET | `packages.lock.json` | `dotnet restore` (no targeted form) |
770
+ | Terraform | `.terraform.lock.hcl` | `terraform providers lock` (no targeted form) |
771
+
772
+ Manifests whose `upd` pass produced zero changes have their lockfile
773
+ refresh skipped entirely. A directory where only config pins were
774
+ applied is still refreshed, and the changed-package list includes
775
+ those pinned packages so `cargo update -p <pkg>` / `bundle lock --update <pkg>` stay scoped.
776
+
702
777
  Stable `audit`-specific flags:
703
778
 
704
779
  | Flag | Purpose |
@@ -134,7 +134,7 @@ pub fn compute_fix_plan(audit: &AuditResult) -> (HashMap<String, String>, Vec<(S
134
134
  .vulnerabilities
135
135
  .iter()
136
136
  .filter_map(|v| v.fixed_version.as_deref())
137
- .max_by(|a, b| compare_fix_versions(a, b));
137
+ .max_by(|a, b| crate::version::compare::compare_versions(a, b));
138
138
 
139
139
  if let Some(version) = max_fixed {
140
140
  fixable.insert(name.clone(), version.to_string());
@@ -144,44 +144,6 @@ pub fn compute_fix_plan(audit: &AuditResult) -> (HashMap<String, String>, Vec<(S
144
144
  (fixable, unfixable)
145
145
  }
146
146
 
147
- /// Compare two version strings for ordering, preferring semver but falling back
148
- /// to lexicographic comparison for non-semver ecosystems.
149
- fn compare_fix_versions(a: &str, b: &str) -> std::cmp::Ordering {
150
- match (semver_parse(a), semver_parse(b)) {
151
- (Some(va), Some(vb)) => va.cmp(&vb),
152
- _ => a.cmp(b),
153
- }
154
- }
155
-
156
- /// Parse a version string as semver, accepting an optional leading `v`.
157
- ///
158
- /// Returns `(major, minor, patch, is_stable)` where `is_stable` is 1 for stable
159
- /// releases and 0 for pre-releases (e.g. `2.0.0-rc1`). This ensures stable
160
- /// versions beat pre-releases when all numeric components are equal.
161
- fn semver_parse(v: &str) -> Option<(u64, u64, u64, u8)> {
162
- let v = v.trim_start_matches('v');
163
- let parts: Vec<&str> = v.split('.').collect();
164
- if parts.len() < 2 {
165
- return None;
166
- }
167
- let major: u64 = parts[0].parse().ok()?;
168
- let minor: u64 = parts[1].parse().ok()?;
169
- // Patch may carry a pre-release or build suffix (e.g. "0-rc1", "3+build1").
170
- // Take only the leading digit run so "0-rc1" → patch=0, rest="-rc1".
171
- let patch_part = parts.get(2).copied().unwrap_or("0");
172
- let (patch_digits, rest) = patch_part
173
- .split_once(|c: char| !c.is_ascii_digit())
174
- .unwrap_or((patch_part, ""));
175
- let patch: u64 = if patch_digits.is_empty() {
176
- 0
177
- } else {
178
- patch_digits.parse().ok()?
179
- };
180
- // Stability flag: 1 = stable, 0 = pre-release. Stable wins ties.
181
- let is_stable: u8 = if rest.is_empty() { 1 } else { 0 };
182
- Some((major, minor, patch, is_stable))
183
- }
184
-
185
147
  /// Return a sort key for a severity string such that Critical sorts first.
186
148
  ///
187
149
  /// Lower numeric values sort earlier, so Critical = 0, Unknown = 5.
@@ -858,6 +820,43 @@ mod tests {
858
820
  assert_eq!(fixable.get("pkg").map(|s| s.as_str()), Some("2.10.0"));
859
821
  }
860
822
 
823
+ #[test]
824
+ fn test_compute_fix_plan_picks_numerically_highest_non_semver_fix() {
825
+ // Regression test for 4-segment versions (e.g. Ruby or multi-segment tags)
826
+ // where the legacy `semver_parse` dropped the 4th segment, making
827
+ // `1.0.0.10` and `1.0.0.9` compare equal. `max_by` then returned the
828
+ // *last* element, so the answer depended on fix_version ordering.
829
+ //
830
+ // The correct "minimum safe fix" must order by full numeric value.
831
+ let mut audit = AuditResult::default();
832
+ audit.vulnerable.push(PackageAuditResult {
833
+ package: Package {
834
+ name: "pkg".to_string(),
835
+ version: "1.0.0.1".to_string(),
836
+ ecosystem: Ecosystem::RubyGems,
837
+ },
838
+ vulnerabilities: vec![
839
+ Vulnerability {
840
+ id: "CVE-A".to_string(),
841
+ summary: None,
842
+ severity: None,
843
+ url: None,
844
+ fixed_version: Some("1.0.0.10".to_string()),
845
+ },
846
+ Vulnerability {
847
+ id: "CVE-B".to_string(),
848
+ summary: None,
849
+ severity: None,
850
+ url: None,
851
+ fixed_version: Some("1.0.0.9".to_string()),
852
+ },
853
+ ],
854
+ });
855
+
856
+ let (fixable, _) = compute_fix_plan(&audit);
857
+ assert_eq!(fixable.get("pkg").map(String::as_str), Some("1.0.0.10"));
858
+ }
859
+
861
860
  // ─── check_packages_cached unit tests ────────────────────────────────────
862
861
 
863
862
  fn sample_package(name: &str) -> Package {
@@ -1,4 +1,4 @@
1
- use crate::registry::Registry;
1
+ use crate::registry::{Registry, VersionMeta};
2
2
  use anyhow::Result;
3
3
  use async_trait::async_trait;
4
4
  use directories::ProjectDirs;
@@ -35,6 +35,21 @@ pub struct Cache {
35
35
  pub struct CacheEntry {
36
36
  pub version: String,
37
37
  pub fetched_at: u64, // Unix timestamp
38
+ /// Full per-version metadata fetched via `list_versions`, when available.
39
+ /// Older cache files predate this field and deserialize with `None`.
40
+ #[serde(default, skip_serializing_if = "Option::is_none")]
41
+ pub versions: Option<Vec<CachedVersionMeta>>,
42
+ }
43
+
44
+ /// Cache-friendly mirror of [`crate::registry::VersionMeta`]. `published_at`
45
+ /// is stored as a Unix timestamp (seconds) so the cache file stays stable
46
+ /// across `chrono` serde format changes.
47
+ #[derive(Debug, Serialize, Deserialize, Clone)]
48
+ pub struct CachedVersionMeta {
49
+ pub version: String,
50
+ pub published_at: Option<i64>,
51
+ pub yanked: bool,
52
+ pub prerelease: bool,
38
53
  }
39
54
 
40
55
  impl Cache {
@@ -121,6 +136,7 @@ impl Cache {
121
136
  CacheEntry {
122
137
  version,
123
138
  fetched_at,
139
+ versions: None,
124
140
  },
125
141
  );
126
142
  }
@@ -255,6 +271,10 @@ impl<R: Registry> Registry for CachedRegistry<R> {
255
271
  Ok(version)
256
272
  }
257
273
 
274
+ async fn list_versions(&self, package: &str) -> Result<Vec<VersionMeta>> {
275
+ self.inner.list_versions(package).await
276
+ }
277
+
258
278
  fn name(&self) -> &'static str {
259
279
  self.inner.name()
260
280
  }
@@ -320,6 +340,7 @@ mod tests {
320
340
  CacheEntry {
321
341
  version: "0.1.0".to_string(),
322
342
  fetched_at: expired_time,
343
+ versions: None,
323
344
  },
324
345
  );
325
346
 
@@ -346,6 +367,7 @@ mod tests {
346
367
  CacheEntry {
347
368
  version: "0.1.0".to_string(),
348
369
  fetched_at: expired_time,
370
+ versions: None,
349
371
  },
350
372
  );
351
373
 
@@ -405,6 +427,66 @@ mod tests {
405
427
  assert_eq!(restored.get("npm", "lodash"), Some("4.17.21".to_string()));
406
428
  }
407
429
 
430
+ #[test]
431
+ fn test_cache_entry_deserialises_without_versions_field() {
432
+ // Older cache files predate the `versions` field and must still
433
+ // deserialise so upgrades do not invalidate on-disk caches.
434
+ let json = r#"{"version":"1.2.3","fetched_at":1700000000}"#;
435
+ let entry: CacheEntry = serde_json::from_str(json).unwrap();
436
+ assert_eq!(entry.version, "1.2.3");
437
+ assert_eq!(entry.fetched_at, 1_700_000_000);
438
+ assert!(entry.versions.is_none(), "missing field defaults to None");
439
+ }
440
+
441
+ #[test]
442
+ fn test_cache_roundtrip_with_versions() {
443
+ let entry = CacheEntry {
444
+ version: "2.0.0".to_string(),
445
+ fetched_at: 1_700_000_000,
446
+ versions: Some(vec![
447
+ CachedVersionMeta {
448
+ version: "2.0.0".to_string(),
449
+ published_at: Some(1_700_000_000),
450
+ yanked: false,
451
+ prerelease: false,
452
+ },
453
+ CachedVersionMeta {
454
+ version: "1.9.0".to_string(),
455
+ published_at: None,
456
+ yanked: true,
457
+ prerelease: false,
458
+ },
459
+ ]),
460
+ };
461
+ let json = serde_json::to_string(&entry).unwrap();
462
+ let back: CacheEntry = serde_json::from_str(&json).unwrap();
463
+ assert_eq!(back.version, entry.version);
464
+ let metas = back.versions.expect("versions must round-trip");
465
+ assert_eq!(metas.len(), 2);
466
+ assert!(metas.iter().any(|m| m.yanked));
467
+ assert!(
468
+ metas
469
+ .iter()
470
+ .any(|m| m.version == "2.0.0" && m.published_at == Some(1_700_000_000))
471
+ );
472
+ }
473
+
474
+ #[test]
475
+ fn test_cache_entry_skips_serializing_none_versions() {
476
+ // Ensure the new field is omitted from JSON when absent, keeping
477
+ // file size small and preserving compatibility with older readers.
478
+ let entry = CacheEntry {
479
+ version: "1.0.0".to_string(),
480
+ fetched_at: 1_700_000_000,
481
+ versions: None,
482
+ };
483
+ let json = serde_json::to_string(&entry).unwrap();
484
+ assert!(
485
+ !json.contains("versions"),
486
+ "versions field must be omitted when None, got: {json}"
487
+ );
488
+ }
489
+
408
490
  #[test]
409
491
  fn test_cache_file_operations() {
410
492
  use tempfile::tempdir;