upd-cli 0.1.1__tar.gz → 0.1.2__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 (73) hide show
  1. {upd_cli-0.1.1 → upd_cli-0.1.2}/CHANGELOG.md +29 -0
  2. {upd_cli-0.1.1 → upd_cli-0.1.2}/Cargo.lock +115 -1
  3. {upd_cli-0.1.1 → upd_cli-0.1.2}/Cargo.toml +2 -1
  4. {upd_cli-0.1.1 → upd_cli-0.1.2}/PKG-INFO +46 -1
  5. {upd_cli-0.1.1 → upd_cli-0.1.2}/README.md +45 -0
  6. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/cache.rs +83 -1
  7. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/cli.rs +37 -0
  8. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/config.rs +324 -2
  9. upd_cli-0.1.2/src/cooldown.rs +965 -0
  10. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/lib.rs +2 -1
  11. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/main.rs +411 -10
  12. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/output.rs +142 -0
  13. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/registry/crates_io.rs +92 -6
  14. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/registry/github_releases.rs +104 -1
  15. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/registry/go_proxy.rs +119 -1
  16. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/registry/mock.rs +61 -1
  17. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/registry/mod.rs +47 -0
  18. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/registry/npm.rs +150 -1
  19. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/registry/nuget.rs +14 -0
  20. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/registry/pypi.rs +109 -4
  21. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/registry/rubygems.rs +73 -1
  22. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/updater/cargo_toml.rs +65 -6
  23. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/updater/csproj.rs +55 -0
  24. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/updater/gemfile.rs +61 -3
  25. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/updater/github_actions.rs +53 -1
  26. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/updater/go_mod.rs +58 -1
  27. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/updater/mise.rs +55 -1
  28. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/updater/mod.rs +401 -1
  29. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/updater/package_json.rs +53 -3
  30. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/updater/pre_commit.rs +53 -1
  31. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/updater/pyproject.rs +120 -10
  32. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/updater/requirements.rs +56 -0
  33. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/updater/terraform.rs +1 -0
  34. upd_cli-0.1.2/tests/cooldown_e2e.rs +77 -0
  35. {upd_cli-0.1.1 → upd_cli-0.1.2}/.mise.toml +0 -0
  36. {upd_cli-0.1.1 → upd_cli-0.1.2}/.pre-commit-config.yaml +0 -0
  37. {upd_cli-0.1.1 → upd_cli-0.1.2}/.pre-commit-hooks.yaml +0 -0
  38. {upd_cli-0.1.1 → upd_cli-0.1.2}/.rumdl.toml +0 -0
  39. {upd_cli-0.1.1 → upd_cli-0.1.2}/LICENSE +0 -0
  40. {upd_cli-0.1.1 → upd_cli-0.1.2}/Makefile +0 -0
  41. {upd_cli-0.1.1 → upd_cli-0.1.2}/assets/logo-wide.svg +0 -0
  42. {upd_cli-0.1.1 → upd_cli-0.1.2}/assets/logo.svg +0 -0
  43. {upd_cli-0.1.1 → upd_cli-0.1.2}/pyproject.toml +0 -0
  44. {upd_cli-0.1.1 → upd_cli-0.1.2}/python/upd_cli/__init__.py +0 -0
  45. {upd_cli-0.1.1 → upd_cli-0.1.2}/python/upd_cli/__main__.py +0 -0
  46. {upd_cli-0.1.1 → upd_cli-0.1.2}/python/upd_cli/py.typed +0 -0
  47. {upd_cli-0.1.1 → upd_cli-0.1.2}/rust-toolchain.toml +0 -0
  48. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/align.rs +0 -0
  49. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/audit/cache.rs +0 -0
  50. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/audit/cvss.rs +0 -0
  51. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/audit/mod.rs +0 -0
  52. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/interactive.rs +0 -0
  53. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/lockfile.rs +0 -0
  54. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/registry/terraform.rs +0 -0
  55. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/registry/utils.rs +0 -0
  56. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/version/mod.rs +0 -0
  57. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/version/pep440.rs +0 -0
  58. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/version/semver_util.rs +0 -0
  59. {upd_cli-0.1.1 → upd_cli-0.1.2}/src/version/tag.rs +0 -0
  60. {upd_cli-0.1.1 → upd_cli-0.1.2}/tests/audit_offline.rs +0 -0
  61. {upd_cli-0.1.1 → upd_cli-0.1.2}/tests/audit_sarif.rs +0 -0
  62. {upd_cli-0.1.1 → upd_cli-0.1.2}/tests/audit_severity.rs +0 -0
  63. {upd_cli-0.1.1 → upd_cli-0.1.2}/tests/bump_filter.rs +0 -0
  64. {upd_cli-0.1.1 → upd_cli-0.1.2}/tests/exit_codes.rs +0 -0
  65. {upd_cli-0.1.1 → upd_cli-0.1.2}/tests/fix_audit.rs +0 -0
  66. {upd_cli-0.1.1 → upd_cli-0.1.2}/tests/format_json.rs +0 -0
  67. {upd_cli-0.1.1 → upd_cli-0.1.2}/tests/help_text.rs +0 -0
  68. {upd_cli-0.1.1 → upd_cli-0.1.2}/tests/interactive_tty.rs +0 -0
  69. {upd_cli-0.1.1 → upd_cli-0.1.2}/tests/invalid_positional.rs +0 -0
  70. {upd_cli-0.1.1 → upd_cli-0.1.2}/tests/no_args_scope.rs +0 -0
  71. {upd_cli-0.1.1 → upd_cli-0.1.2}/tests/output_streams.rs +0 -0
  72. {upd_cli-0.1.1 → upd_cli-0.1.2}/tests/package_filter.rs +0 -0
  73. {upd_cli-0.1.1 → upd_cli-0.1.2}/vership.toml +0 -0
@@ -11,6 +11,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
 
12
12
 
13
13
 
14
+
15
+ ## [0.1.2](https://github.com/rvben/upd/compare/v0.1.1...v0.1.2) - 2026-04-24
16
+
17
+ ### Added
18
+
19
+ - **cache**: add optional versions field to CacheEntry for future list_versions caching ([1beb34d](https://github.com/rvben/upd/commit/1beb34dc030f160e3748dff9a63e71bfa1772043))
20
+ - **output**: report held-back and skipped-by-cooldown packages ([3d1a2ce](https://github.com/rvben/upd/commit/3d1a2cef2ae31c59a87b417b074e0d672b7256d2))
21
+ - **updater**: propagate cooldown policy to remaining updaters ([8e80f25](https://github.com/rvben/upd/commit/8e80f252339f022d90f8120694e529c68c3bcf90))
22
+ - **updater**: apply cooldown policy in requirements updater ([5d6cfd3](https://github.com/rvben/upd/commit/5d6cfd32bfe87df7af86453476a93e2945f009ff))
23
+ - **registry**: implement list_versions for GitHub releases ([5f6472b](https://github.com/rvben/upd/commit/5f6472b5d7c4394d59fbabfd4bd9a1d9b736a67a))
24
+ - **registry**: implement list_versions for RubyGems ([1a1dda3](https://github.com/rvben/upd/commit/1a1dda31d73e0f25d8a73e6438aed7c4daadc007))
25
+ - **registry**: implement list_versions for Go module proxy ([196fef6](https://github.com/rvben/upd/commit/196fef634b0ae3c53096222e8bbe3161d8b67a33))
26
+ - **registry**: implement list_versions for crates.io ([8869dec](https://github.com/rvben/upd/commit/8869dec1a69148b5b3db44cc3393a05aaa2b01fb))
27
+ - **registry**: implement list_versions for npm ([b23cd78](https://github.com/rvben/upd/commit/b23cd787e748dfc5404a8fd51981c67f858e1d5a))
28
+ - **registry**: implement list_versions for PyPI ([9aa342c](https://github.com/rvben/upd/commit/9aa342c5c11ae504024aed5aa2d8930c66b4a6df))
29
+ - **cli**: add --min-age flag for cooldown override ([b5bfb30](https://github.com/rvben/upd/commit/b5bfb304c39f6b8099aef3a066e2ef4ed17f606f))
30
+ - **config**: show cooldown policy in --show-config ([9486257](https://github.com/rvben/upd/commit/9486257b22456e55e9af23709089a574cce262be))
31
+ - **config**: add [cooldown] table with default and per-ecosystem overrides ([a9ff8e3](https://github.com/rvben/upd/commit/a9ff8e31050485056a9bb6e03f4d313df1262998))
32
+ - **cooldown**: implement select() selection algorithm ([8b588bb](https://github.com/rvben/upd/commit/8b588bb1b42d84d696a7a17d8e97141a223351a1))
33
+ - **cooldown**: add CooldownPolicy with precedence resolution ([ddba284](https://github.com/rvben/upd/commit/ddba284329dad87bf84cf566ecb487ef665408a1))
34
+ - **cooldown**: add parse_duration for release-age config ([a7e67e0](https://github.com/rvben/upd/commit/a7e67e035764e4d905ace1dc4092f41c27510a5c))
35
+ - **registry**: re-export VersionMeta from crate root ([b2cdd60](https://github.com/rvben/upd/commit/b2cdd6037c09110881f53db4992542e77596f6c3))
36
+ - **registry**: add VersionMeta and list_versions trait method ([09ddbf9](https://github.com/rvben/upd/commit/09ddbf9c2b8f0546feafeb642f27547bed1882da))
37
+
38
+ ### Fixed
39
+
40
+ - **cooldown**: harden selection against real-world constraints and per-file policy ([a284ea4](https://github.com/rvben/upd/commit/a284ea497dcf3abf65fda2c7e7f6c0c03c3dd8e2))
41
+ - **updater**: pass Poetry constraint to cooldown selection ([a0383e9](https://github.com/rvben/upd/commit/a0383e9167b2ef05cc4583aa23c1035d32268750))
42
+
14
43
  ## [0.1.1](https://github.com/rvben/upd/compare/v0.1.0...v0.1.1) - 2026-04-22
15
44
 
16
45
  ### 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.2"
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.2"
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.2
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -469,6 +469,51 @@ upd --verbose
469
469
  # pyproject.toml:13: Skipped internal-utils 1.0.0 (ignored)
470
470
  ```
471
471
 
472
+ ## Cooldown (minimum release age)
473
+
474
+ Hold back updates to versions that have been public for less than N days.
475
+ Reduces exposure to supply-chain attacks that rely on freshly published
476
+ malicious versions being installed before detection. Modelled after
477
+ Renovate's `minimumReleaseAge` / Dependabot's `cooldown`.
478
+
479
+ Enable in `.updrc.toml`:
480
+
481
+ ```toml
482
+ [cooldown]
483
+ default = "7d" # applies to every ecosystem unless overridden
484
+
485
+ [cooldown.ecosystem]
486
+ npm = "14d" # stricter for npm
487
+ pypi = "14d"
488
+ "crates.io" = "3d"
489
+ ```
490
+
491
+ Duration syntax: `<integer><unit>` where unit is `s`, `m`, `h`, `d`, `w`.
492
+ A bare `0` disables cooldown.
493
+
494
+ Override from the CLI for one-off runs:
495
+
496
+ ```text
497
+ upd --min-age 14d # use 14 days regardless of config
498
+ upd --min-age 0 # disable cooldown entirely for this run
499
+ ```
500
+
501
+ **How it works:** when the latest version is still inside the cooldown
502
+ window, `upd` updates to the newest version that *is* old enough. If nothing
503
+ newer is old enough yet, the package is held back. Output marks these
504
+ packages explicitly:
505
+
506
+ ```text
507
+ requirements.txt: Updated requests 2.28.0 → 2.31.0
508
+ package.json: Held back lodash 4.17.20 → 4.17.21 (4.17.22 released 2d ago, cooldown 7d)
509
+ package.json: Skipped express (only newer version 4.19.0 released 1d ago, cooldown 7d)
510
+ ```
511
+
512
+ **Supported ecosystems:** PyPI, npm, crates.io, Go modules, RubyGems,
513
+ GitHub releases (covers GitHub Actions, pre-commit, Mise). NuGet and
514
+ Terraform Registry do not expose per-version publish dates we can
515
+ consume today; cooldown is reported as unavailable for those files.
516
+
472
517
  ## Caching
473
518
 
474
519
  Version lookups are cached for 24 hours in:
@@ -446,6 +446,51 @@ upd --verbose
446
446
  # pyproject.toml:13: Skipped internal-utils 1.0.0 (ignored)
447
447
  ```
448
448
 
449
+ ## Cooldown (minimum release age)
450
+
451
+ Hold back updates to versions that have been public for less than N days.
452
+ Reduces exposure to supply-chain attacks that rely on freshly published
453
+ malicious versions being installed before detection. Modelled after
454
+ Renovate's `minimumReleaseAge` / Dependabot's `cooldown`.
455
+
456
+ Enable in `.updrc.toml`:
457
+
458
+ ```toml
459
+ [cooldown]
460
+ default = "7d" # applies to every ecosystem unless overridden
461
+
462
+ [cooldown.ecosystem]
463
+ npm = "14d" # stricter for npm
464
+ pypi = "14d"
465
+ "crates.io" = "3d"
466
+ ```
467
+
468
+ Duration syntax: `<integer><unit>` where unit is `s`, `m`, `h`, `d`, `w`.
469
+ A bare `0` disables cooldown.
470
+
471
+ Override from the CLI for one-off runs:
472
+
473
+ ```text
474
+ upd --min-age 14d # use 14 days regardless of config
475
+ upd --min-age 0 # disable cooldown entirely for this run
476
+ ```
477
+
478
+ **How it works:** when the latest version is still inside the cooldown
479
+ window, `upd` updates to the newest version that *is* old enough. If nothing
480
+ newer is old enough yet, the package is held back. Output marks these
481
+ packages explicitly:
482
+
483
+ ```text
484
+ requirements.txt: Updated requests 2.28.0 → 2.31.0
485
+ package.json: Held back lodash 4.17.20 → 4.17.21 (4.17.22 released 2d ago, cooldown 7d)
486
+ package.json: Skipped express (only newer version 4.19.0 released 1d ago, cooldown 7d)
487
+ ```
488
+
489
+ **Supported ecosystems:** PyPI, npm, crates.io, Go modules, RubyGems,
490
+ GitHub releases (covers GitHub Actions, pre-commit, Mise). NuGet and
491
+ Terraform Registry do not expose per-version publish dates we can
492
+ consume today; cooldown is reported as unavailable for those files.
493
+
449
494
  ## Caching
450
495
 
451
496
  Version lookups are cached for 24 hours in:
@@ -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;
@@ -161,6 +161,17 @@ pub struct Cli {
161
161
  #[arg(long, global = true)]
162
162
  pub show_config: bool,
163
163
 
164
+ /// Minimum release age before a version is eligible for update.
165
+ ///
166
+ /// Overrides the `[cooldown]` config for this run. Setting `--min-age 0`
167
+ /// disables cooldown entirely. Accepts durations like `72h`, `7d`, `2w`.
168
+ ///
169
+ /// Example: `upd --min-age 7d` only updates to versions published at least
170
+ /// 7 days ago. Protects against supply-chain attacks that rely on freshly
171
+ /// published malicious packages being installed before detection.
172
+ #[arg(long, global = true, value_name = "DURATION")]
173
+ pub min_age: Option<String>,
174
+
164
175
  /// Update only the named package(s), skipping all others.
165
176
  ///
166
177
  /// Comma-separated or repeatable. Exact case-sensitive match.
@@ -275,6 +286,7 @@ mod tests {
275
286
  assert!(!cli.apply);
276
287
  assert!(cli.paths.is_empty());
277
288
  assert!(cli.command.is_none());
289
+ assert!(cli.min_age.is_none());
278
290
  }
279
291
 
280
292
  #[test]
@@ -804,4 +816,29 @@ mod tests {
804
816
  "align --help should describe monorepo use-case; got:\n{align_help}"
805
817
  );
806
818
  }
819
+
820
+ #[test]
821
+ fn test_cli_parses_min_age() {
822
+ let cli = Cli::try_parse_from(["upd", "--min-age", "7d"]).unwrap();
823
+ assert_eq!(cli.min_age.as_deref(), Some("7d"));
824
+ }
825
+
826
+ #[test]
827
+ fn test_cli_min_age_zero_for_disable() {
828
+ let cli = Cli::try_parse_from(["upd", "--min-age", "0"]).unwrap();
829
+ assert_eq!(cli.min_age.as_deref(), Some("0"));
830
+ }
831
+
832
+ #[test]
833
+ fn test_cli_min_age_default_none() {
834
+ let cli = Cli::try_parse_from(["upd"]).unwrap();
835
+ assert!(cli.min_age.is_none());
836
+ }
837
+
838
+ #[test]
839
+ fn test_cli_min_age_is_global_across_subcommands() {
840
+ let cli = Cli::try_parse_from(["upd", "update", "--min-age", "14d", "path1"]).unwrap();
841
+ assert_eq!(cli.min_age.as_deref(), Some("14d"));
842
+ assert!(matches!(cli.command, Some(Command::Update { .. })));
843
+ }
807
844
  }