upd-cli 0.0.23__tar.gz → 0.0.25__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 (55) hide show
  1. {upd_cli-0.0.23 → upd_cli-0.0.25}/CHANGELOG.md +14 -0
  2. {upd_cli-0.0.23 → upd_cli-0.0.25}/Cargo.lock +1 -1
  3. {upd_cli-0.0.23 → upd_cli-0.0.25}/Cargo.toml +2 -2
  4. upd_cli-0.0.25/Makefile +57 -0
  5. {upd_cli-0.0.23 → upd_cli-0.0.25}/PKG-INFO +18 -11
  6. {upd_cli-0.0.23 → upd_cli-0.0.25}/README.md +15 -8
  7. {upd_cli-0.0.23 → upd_cli-0.0.25}/pyproject.toml +2 -2
  8. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/align.rs +5 -4
  9. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/cache.rs +6 -0
  10. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/cli.rs +1 -1
  11. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/lib.rs +1 -1
  12. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/lockfile.rs +72 -0
  13. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/main.rs +58 -5
  14. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/registry/mod.rs +2 -0
  15. upd_cli-0.0.25/src/registry/nuget.rs +225 -0
  16. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/registry/pypi.rs +147 -23
  17. upd_cli-0.0.25/src/updater/csproj.rs +666 -0
  18. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/updater/mod.rs +22 -0
  19. upd_cli-0.0.25/vership.toml +2 -0
  20. upd_cli-0.0.23/Makefile +0 -127
  21. {upd_cli-0.0.23 → upd_cli-0.0.25}/.mise.toml +0 -0
  22. {upd_cli-0.0.23 → upd_cli-0.0.25}/.pre-commit-config.yaml +0 -0
  23. {upd_cli-0.0.23 → upd_cli-0.0.25}/.pre-commit-hooks.yaml +0 -0
  24. {upd_cli-0.0.23 → upd_cli-0.0.25}/.rumdl.toml +0 -0
  25. {upd_cli-0.0.23 → upd_cli-0.0.25}/LICENSE +0 -0
  26. {upd_cli-0.0.23 → upd_cli-0.0.25}/assets/logo-wide.svg +0 -0
  27. {upd_cli-0.0.23 → upd_cli-0.0.25}/assets/logo.svg +0 -0
  28. {upd_cli-0.0.23 → upd_cli-0.0.25}/python/upd_cli/__init__.py +0 -0
  29. {upd_cli-0.0.23 → upd_cli-0.0.25}/python/upd_cli/__main__.py +0 -0
  30. {upd_cli-0.0.23 → upd_cli-0.0.25}/python/upd_cli/py.typed +0 -0
  31. {upd_cli-0.0.23 → upd_cli-0.0.25}/rust-toolchain.toml +0 -0
  32. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/audit.rs +0 -0
  33. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/config.rs +0 -0
  34. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/interactive.rs +0 -0
  35. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/registry/crates_io.rs +0 -0
  36. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/registry/github_releases.rs +0 -0
  37. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/registry/go_proxy.rs +0 -0
  38. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/registry/mock.rs +0 -0
  39. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/registry/npm.rs +0 -0
  40. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/registry/rubygems.rs +0 -0
  41. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/registry/terraform.rs +0 -0
  42. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/registry/utils.rs +0 -0
  43. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/updater/cargo_toml.rs +0 -0
  44. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/updater/gemfile.rs +0 -0
  45. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/updater/github_actions.rs +0 -0
  46. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/updater/go_mod.rs +0 -0
  47. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/updater/mise.rs +0 -0
  48. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/updater/package_json.rs +0 -0
  49. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/updater/pre_commit.rs +0 -0
  50. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/updater/pyproject.rs +0 -0
  51. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/updater/requirements.rs +0 -0
  52. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/updater/terraform.rs +0 -0
  53. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/version/mod.rs +0 -0
  54. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/version/pep440.rs +0 -0
  55. {upd_cli-0.0.23 → upd_cli-0.0.25}/src/version/semver_util.rs +0 -0
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+
9
+ ## [0.0.25](https://github.com/rvben/upd/compare/v0.0.24...v0.0.25) - 2026-04-15
10
+
11
+ ### Fixed
12
+
13
+ - **pypi**: handle string-valued yanked field in PEP 691 JSON Simple API ([b17034b](https://github.com/rvben/upd/commit/b17034b540f6a5b62e446131c6e87f43695bbd9b))
14
+
15
+ ## [0.0.24] - 2026-03-23
16
+
17
+ ### Added
18
+
19
+ - **NuGet/.NET support**: Update `PackageReference` and `PackageVersion` elements in `.csproj` and `Directory.Packages.props` files via the NuGet v3 API
20
+ - **Gemfile.lock regeneration**: `--lock` flag now supports Ruby projects (runs `bundle install`)
21
+
8
22
  ## [0.0.23] - 2026-03-23
9
23
 
10
24
  ### Added
@@ -1780,7 +1780,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
1780
1780
 
1781
1781
  [[package]]
1782
1782
  name = "upd"
1783
- version = "0.0.23"
1783
+ version = "0.0.25"
1784
1784
  dependencies = [
1785
1785
  "anyhow",
1786
1786
  "async-trait",
@@ -1,9 +1,9 @@
1
1
  [package]
2
2
  name = "upd"
3
- version = "0.0.23"
3
+ version = "0.0.25"
4
4
  edition = "2024"
5
5
  rust-version = "1.91.1"
6
- description = "A fast dependency updater for Python, Node.js, Rust, Go, and GitHub Actions projects"
6
+ description = "A fast dependency updater for Python, Node.js, Rust, Go, Ruby, Terraform, GitHub Actions, pre-commit, and Mise projects"
7
7
  license = "MIT"
8
8
  repository = "https://github.com/rvben/upd"
9
9
  homepage = "https://github.com/rvben/upd"
@@ -0,0 +1,57 @@
1
+ .PHONY: build release test lint fmt check clean run install release-patch release-minor release-major
2
+
3
+ # Build debug binary
4
+ build:
5
+ cargo build
6
+
7
+ # Build release binary
8
+ release:
9
+ cargo build --release
10
+
11
+ # Run all tests
12
+ test:
13
+ cargo test
14
+
15
+ # Run tests with output
16
+ test-verbose:
17
+ cargo test -- --nocapture
18
+
19
+ # Run clippy lints
20
+ lint:
21
+ cargo clippy -- -D warnings
22
+
23
+ # Format code
24
+ fmt:
25
+ cargo fmt
26
+
27
+ # Check formatting without changing files
28
+ fmt-check:
29
+ cargo fmt -- --check
30
+
31
+ # Run all checks (format, lint, test)
32
+ check: fmt-check lint test
33
+
34
+ # Clean build artifacts
35
+ clean:
36
+ cargo clean
37
+
38
+ # Run debug build
39
+ run:
40
+ cargo run
41
+
42
+ # Run with arguments
43
+ run-release:
44
+ ./target/release/upd
45
+
46
+ # Install to ~/.cargo/bin
47
+ install:
48
+ cargo install --path .
49
+
50
+ release-patch:
51
+ vership bump patch
52
+
53
+ release-minor:
54
+ vership bump minor
55
+
56
+ release-major:
57
+ vership bump major
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: upd-cli
3
- Version: 0.0.23
3
+ Version: 0.0.25
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -10,8 +10,8 @@ Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Programming Language :: Rust
11
11
  Classifier: Topic :: Software Development :: Build Tools
12
12
  License-File: LICENSE
13
- Summary: A fast dependency updater for Python and Node.js projects
14
- Keywords: dependencies,update,python,nodejs,npm
13
+ Summary: A fast dependency updater for Python, Node.js, Rust, Go, Ruby, Terraform, GitHub Actions, pre-commit, and Mise projects
14
+ Keywords: dependencies,update,python,nodejs,rust,go,ruby,terraform,github-actions
15
15
  Home-Page: https://github.com/rvben/upd
16
16
  Author-email: Ruben Jongejan <ruben.jongejan@gmail.com>
17
17
  License: MIT
@@ -27,7 +27,7 @@ Project-URL: Repository, https://github.com/rvben/upd
27
27
 
28
28
  # upd
29
29
 
30
- A fast dependency updater for Python, Node.js, Rust, Go, Ruby, Terraform, GitHub Actions, pre-commit, and Mise projects, written in Rust.
30
+ A fast dependency updater for Python, Node.js, Rust, Go, Ruby, .NET, Terraform, GitHub Actions, pre-commit, and Mise projects, written in Rust.
31
31
 
32
32
  ## Quick Start
33
33
 
@@ -44,7 +44,7 @@ uvx --from upd-cli upd -n
44
44
 
45
45
  ## Features
46
46
 
47
- - **Multi-ecosystem**: Python, Node.js, Rust, Go, Ruby, Terraform, GitHub Actions, pre-commit, Mise/asdf
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
49
  - **Constraint-aware**: Respects version constraints like `>=2.0,<3` and `~> 7.1`
50
50
  - **Smart caching**: 24-hour version cache for faster subsequent runs
@@ -129,6 +129,7 @@ upd --lang python --lang go # Update Python and Go only
129
129
  upd --lang actions # Update only GitHub Actions
130
130
  upd --lang pre-commit # Update only pre-commit hooks
131
131
  upd --lang ruby # Update only Ruby gems
132
+ upd --lang dot-net # Update only .NET NuGet packages
132
133
  upd --lang terraform # Update only Terraform providers/modules
133
134
  upd --lang mise # Update only Mise/asdf tools
134
135
 
@@ -191,6 +192,14 @@ upd audit --check # Exit 1 if vulnerabilities found (for CI)
191
192
 
192
193
  - `Gemfile` (gem declarations with version constraints)
193
194
 
195
+ ### .NET / NuGet
196
+
197
+ - `.csproj` files (`PackageReference` elements)
198
+ - `Directory.Packages.props` and `Directory.Build.props` (`PackageVersion` elements)
199
+ - Supports both inline `Version` attributes and child `<Version>` elements
200
+ - Queries the NuGet v3 API (`api.nuget.org`)
201
+ - Skips range version constraints (`[1.0, 2.0)`)
202
+
194
203
  ### Terraform / OpenTofu
195
204
 
196
205
  - `.tf` files (HCL format)
@@ -324,7 +333,7 @@ Checking 42 unique package(s) for vulnerabilities...
324
333
  Summary: 2 vulnerable package(s), 3 total vulnerability/ies
325
334
  ```
326
335
 
327
- **Supported ecosystems for auditing:** PyPI, npm, crates.io, Go, RubyGems
336
+ **Supported ecosystems for auditing:** PyPI, npm, crates.io, Go, RubyGems, NuGet
328
337
 
329
338
  **CI/CD Integration:**
330
339
 
@@ -586,8 +595,8 @@ Add `upd` to your `.pre-commit-config.yaml`:
586
595
 
587
596
  ```yaml
588
597
  repos:
589
- - repo: https://github.com/rvben/upd
590
- rev: v0.0.22 # Use the latest version
598
+ - repo: https://github.com/rvben/upd-pre-commit
599
+ rev: v0.0.24
591
600
  hooks:
592
601
  - id: upd-check
593
602
  # Optional: only check specific ecosystems
@@ -601,9 +610,7 @@ Available hooks:
601
610
  | `upd-check` | Fail if any dependencies are outdated |
602
611
  | `upd-check-major` | Fail only on major (breaking) updates |
603
612
 
604
- Both hooks run on `pre-push` by default and trigger when dependency files change.
605
-
606
- **Note:** Requires `upd` to be installed and available in PATH.
613
+ Both hooks run on `pre-push` by default. Uses `language: python` which installs `upd-cli` from PyPI automatically — no manual installation needed.
607
614
 
608
615
  ## Development
609
616
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  # upd
6
6
 
7
- A fast dependency updater for Python, Node.js, Rust, Go, Ruby, Terraform, GitHub Actions, pre-commit, and Mise projects, written in Rust.
7
+ A fast dependency updater for Python, Node.js, Rust, Go, Ruby, .NET, Terraform, GitHub Actions, pre-commit, and Mise projects, written in Rust.
8
8
 
9
9
  ## Quick Start
10
10
 
@@ -21,7 +21,7 @@ uvx --from upd-cli upd -n
21
21
 
22
22
  ## Features
23
23
 
24
- - **Multi-ecosystem**: Python, Node.js, Rust, Go, Ruby, Terraform, GitHub Actions, pre-commit, Mise/asdf
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
26
  - **Constraint-aware**: Respects version constraints like `>=2.0,<3` and `~> 7.1`
27
27
  - **Smart caching**: 24-hour version cache for faster subsequent runs
@@ -106,6 +106,7 @@ upd --lang python --lang go # Update Python and Go only
106
106
  upd --lang actions # Update only GitHub Actions
107
107
  upd --lang pre-commit # Update only pre-commit hooks
108
108
  upd --lang ruby # Update only Ruby gems
109
+ upd --lang dot-net # Update only .NET NuGet packages
109
110
  upd --lang terraform # Update only Terraform providers/modules
110
111
  upd --lang mise # Update only Mise/asdf tools
111
112
 
@@ -168,6 +169,14 @@ upd audit --check # Exit 1 if vulnerabilities found (for CI)
168
169
 
169
170
  - `Gemfile` (gem declarations with version constraints)
170
171
 
172
+ ### .NET / NuGet
173
+
174
+ - `.csproj` files (`PackageReference` elements)
175
+ - `Directory.Packages.props` and `Directory.Build.props` (`PackageVersion` elements)
176
+ - Supports both inline `Version` attributes and child `<Version>` elements
177
+ - Queries the NuGet v3 API (`api.nuget.org`)
178
+ - Skips range version constraints (`[1.0, 2.0)`)
179
+
171
180
  ### Terraform / OpenTofu
172
181
 
173
182
  - `.tf` files (HCL format)
@@ -301,7 +310,7 @@ Checking 42 unique package(s) for vulnerabilities...
301
310
  Summary: 2 vulnerable package(s), 3 total vulnerability/ies
302
311
  ```
303
312
 
304
- **Supported ecosystems for auditing:** PyPI, npm, crates.io, Go, RubyGems
313
+ **Supported ecosystems for auditing:** PyPI, npm, crates.io, Go, RubyGems, NuGet
305
314
 
306
315
  **CI/CD Integration:**
307
316
 
@@ -563,8 +572,8 @@ Add `upd` to your `.pre-commit-config.yaml`:
563
572
 
564
573
  ```yaml
565
574
  repos:
566
- - repo: https://github.com/rvben/upd
567
- rev: v0.0.22 # Use the latest version
575
+ - repo: https://github.com/rvben/upd-pre-commit
576
+ rev: v0.0.24
568
577
  hooks:
569
578
  - id: upd-check
570
579
  # Optional: only check specific ecosystems
@@ -578,9 +587,7 @@ Available hooks:
578
587
  | `upd-check` | Fail if any dependencies are outdated |
579
588
  | `upd-check-major` | Fail only on major (breaking) updates |
580
589
 
581
- Both hooks run on `pre-push` by default and trigger when dependency files change.
582
-
583
- **Note:** Requires `upd` to be installed and available in PATH.
590
+ Both hooks run on `pre-push` by default. Uses `language: python` which installs `upd-cli` from PyPI automatically — no manual installation needed.
584
591
 
585
592
  ## Development
586
593
 
@@ -5,11 +5,11 @@ build-backend = "maturin"
5
5
  [project]
6
6
  name = "upd-cli"
7
7
  dynamic = ["version"]
8
- description = "A fast dependency updater for Python and Node.js projects"
8
+ description = "A fast dependency updater for Python, Node.js, Rust, Go, Ruby, Terraform, GitHub Actions, pre-commit, and Mise projects"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
11
11
  authors = [{ name = "Ruben Jongejan", email = "ruben.jongejan@gmail.com" }]
12
- keywords = ["dependencies", "update", "python", "nodejs", "npm"]
12
+ keywords = ["dependencies", "update", "python", "nodejs", "rust", "go", "ruby", "terraform", "github-actions"]
13
13
  classifiers = [
14
14
  "Development Status :: 4 - Beta",
15
15
  "Environment :: Console",
@@ -4,8 +4,8 @@
4
4
  //! used across multiple dependency files and update all occurrences to that version.
5
5
 
6
6
  use crate::updater::{
7
- CargoTomlUpdater, FileType, GemfileUpdater, GithubActionsUpdater, GoModUpdater, Lang,
8
- MiseUpdater, PackageJsonUpdater, ParsedDependency, PreCommitUpdater, PyProjectUpdater,
7
+ CargoTomlUpdater, CsprojUpdater, FileType, GemfileUpdater, GithubActionsUpdater, GoModUpdater,
8
+ Lang, MiseUpdater, PackageJsonUpdater, ParsedDependency, PreCommitUpdater, PyProjectUpdater,
9
9
  RequirementsUpdater, TerraformUpdater, Updater,
10
10
  };
11
11
  use anyhow::Result;
@@ -78,6 +78,7 @@ fn get_updater(file_type: FileType) -> Box<dyn Updater> {
78
78
  FileType::CargoToml => Box::new(CargoTomlUpdater::new()),
79
79
  FileType::GoMod => Box::new(GoModUpdater::new()),
80
80
  FileType::Gemfile => Box::new(GemfileUpdater::new()),
81
+ FileType::Csproj => Box::new(CsprojUpdater::new()),
81
82
  FileType::GithubActions => Box::new(GithubActionsUpdater::new()),
82
83
  FileType::PreCommitConfig => Box::new(PreCommitUpdater::new()),
83
84
  FileType::MiseToml | FileType::ToolVersions => Box::new(MiseUpdater::new()),
@@ -179,7 +180,7 @@ fn is_stable_version(version: &str, lang: Lang) -> bool {
179
180
  && !v.contains("beta")
180
181
  && !v.contains("dev")
181
182
  }
182
- Lang::Node | Lang::Rust | Lang::Go => {
183
+ Lang::Node | Lang::Rust | Lang::Go | Lang::DotNet => {
183
184
  // Semver pre-release indicator: hyphen followed by identifier
184
185
  !version.contains('-')
185
186
  }
@@ -201,7 +202,7 @@ fn is_stable_version(version: &str, lang: Lang) -> bool {
201
202
  fn compare_versions(a: &str, b: &str, lang: Lang) -> std::cmp::Ordering {
202
203
  match lang {
203
204
  Lang::Python => compare_pep440(a, b),
204
- Lang::Node | Lang::Rust | Lang::Ruby => compare_semver(a, b),
205
+ Lang::Node | Lang::Rust | Lang::Ruby | Lang::DotNet => compare_semver(a, b),
205
206
  Lang::Go => compare_go_version(a, b),
206
207
  Lang::Actions | Lang::PreCommit | Lang::Mise | Lang::Terraform => {
207
208
  let clean_a = a.trim_start_matches('v');
@@ -27,6 +27,8 @@ pub struct Cache {
27
27
  rubygems: HashMap<String, CacheEntry>,
28
28
  #[serde(default)]
29
29
  terraform: HashMap<String, CacheEntry>,
30
+ #[serde(default)]
31
+ nuget: HashMap<String, CacheEntry>,
30
32
  }
31
33
 
32
34
  #[derive(Debug, Serialize, Deserialize, Clone)]
@@ -83,6 +85,7 @@ impl Cache {
83
85
  "github-releases" => &self.github_releases,
84
86
  "rubygems" => &self.rubygems,
85
87
  "terraform" => &self.terraform,
88
+ "nuget" => &self.nuget,
86
89
  _ => return None,
87
90
  };
88
91
 
@@ -104,6 +107,7 @@ impl Cache {
104
107
  "github-releases" => &mut self.github_releases,
105
108
  "rubygems" => &mut self.rubygems,
106
109
  "terraform" => &mut self.terraform,
110
+ "nuget" => &mut self.nuget,
107
111
  _ => return,
108
112
  };
109
113
 
@@ -167,6 +171,8 @@ impl Cache {
167
171
  .retain(|_, entry| !Self::is_expired(entry.fetched_at));
168
172
  self.terraform
169
173
  .retain(|_, entry| !Self::is_expired(entry.fetched_at));
174
+ self.nuget
175
+ .retain(|_, entry| !Self::is_expired(entry.fetched_at));
170
176
  }
171
177
  }
172
178
 
@@ -7,7 +7,7 @@ use std::path::PathBuf;
7
7
  #[command(
8
8
  author,
9
9
  version,
10
- about = "A fast dependency updater for Python, Node.js, Rust, Go, Ruby, Terraform, GitHub Actions, pre-commit, and Mise/asdf projects"
10
+ about = "A fast dependency updater for Python, Node.js, Rust, Go, Ruby, .NET, Terraform, GitHub Actions, pre-commit, and Mise/asdf projects"
11
11
  )]
12
12
  pub struct Cli {
13
13
  #[command(subcommand)]
@@ -16,7 +16,7 @@ pub use cli::{Cli, Command};
16
16
  pub use config::UpdConfig;
17
17
  pub use lockfile::{LockfileType, detect_lockfiles, regenerate_lockfiles};
18
18
  pub use registry::{
19
- GitHubReleasesRegistry, NpmRegistry, PyPiRegistry, Registry, RubyGemsRegistry,
19
+ GitHubReleasesRegistry, NpmRegistry, NuGetRegistry, PyPiRegistry, Registry, RubyGemsRegistry,
20
20
  TerraformRegistry,
21
21
  };
22
22
  pub use updater::{FileType, Lang, UpdateResult, Updater, discover_files};
@@ -27,6 +27,8 @@ pub enum LockfileType {
27
27
  CargoLock,
28
28
  /// go.sum - regenerated with `go mod tidy`
29
29
  GoSum,
30
+ /// Gemfile.lock - regenerated with `bundle install`
31
+ GemfileLock,
30
32
  }
31
33
 
32
34
  impl LockfileType {
@@ -41,6 +43,7 @@ impl LockfileType {
41
43
  LockfileType::BunLock => "bun.lockb",
42
44
  LockfileType::CargoLock => "Cargo.lock",
43
45
  LockfileType::GoSum => "go.sum",
46
+ LockfileType::GemfileLock => "Gemfile.lock",
44
47
  }
45
48
  }
46
49
 
@@ -55,6 +58,7 @@ impl LockfileType {
55
58
  LockfileType::BunLock => ("bun", &["install"]),
56
59
  LockfileType::CargoLock => ("cargo", &["update"]),
57
60
  LockfileType::GoSum => ("go", &["mod", "tidy"]),
61
+ LockfileType::GemfileLock => ("bundle", &["install"]),
58
62
  }
59
63
  }
60
64
 
@@ -68,6 +72,7 @@ impl LockfileType {
68
72
  | LockfileType::BunLock => "package.json",
69
73
  LockfileType::CargoLock => "Cargo.toml",
70
74
  LockfileType::GoSum => "go.mod",
75
+ LockfileType::GemfileLock => "Gemfile",
71
76
  }
72
77
  }
73
78
  }
@@ -131,6 +136,16 @@ pub fn detect_lockfiles(manifest_path: &Path) -> Vec<LockfileType> {
131
136
  lockfiles.push(LockfileType::GoSum);
132
137
  }
133
138
 
139
+ // Check for Ruby lockfile (only if manifest is Gemfile)
140
+ if manifest_path
141
+ .file_name()
142
+ .map(|n| n == "Gemfile")
143
+ .unwrap_or(false)
144
+ && dir.join("Gemfile.lock").exists()
145
+ {
146
+ lockfiles.push(LockfileType::GemfileLock);
147
+ }
148
+
134
149
  lockfiles
135
150
  }
136
151
 
@@ -407,4 +422,61 @@ mod tests {
407
422
  let detected = detect_lockfiles(&manifest);
408
423
  assert!(detected.is_empty());
409
424
  }
425
+
426
+ #[test]
427
+ fn test_lockfile_type_gemfile_filename() {
428
+ assert_eq!(LockfileType::GemfileLock.filename(), "Gemfile.lock");
429
+ }
430
+
431
+ #[test]
432
+ fn test_lockfile_type_gemfile_command() {
433
+ let (cmd, args) = LockfileType::GemfileLock.command();
434
+ assert_eq!(cmd, "bundle");
435
+ assert_eq!(args, &["install"]);
436
+ }
437
+
438
+ #[test]
439
+ fn test_lockfile_type_gemfile_manifest() {
440
+ assert_eq!(LockfileType::GemfileLock.manifest(), "Gemfile");
441
+ }
442
+
443
+ #[test]
444
+ fn test_detect_lockfiles_gemfile() {
445
+ let dir = tempdir().unwrap();
446
+ let manifest = dir.path().join("Gemfile");
447
+ let lockfile = dir.path().join("Gemfile.lock");
448
+
449
+ fs::write(&manifest, "source 'https://rubygems.org'").unwrap();
450
+ fs::write(&lockfile, "").unwrap();
451
+
452
+ let detected = detect_lockfiles(&manifest);
453
+ assert_eq!(detected.len(), 1);
454
+ assert_eq!(detected[0], LockfileType::GemfileLock);
455
+ }
456
+
457
+ #[test]
458
+ fn test_detect_lockfiles_gemfile_no_lockfile() {
459
+ // Gemfile without Gemfile.lock should not detect any lockfile
460
+ let dir = tempdir().unwrap();
461
+ let manifest = dir.path().join("Gemfile");
462
+
463
+ fs::write(&manifest, "source 'https://rubygems.org'").unwrap();
464
+
465
+ let detected = detect_lockfiles(&manifest);
466
+ assert!(detected.is_empty());
467
+ }
468
+
469
+ #[test]
470
+ fn test_detect_lockfiles_gemfile_wrong_manifest() {
471
+ // Gemfile.lock should only be detected for Gemfile, not for other manifests
472
+ let dir = tempdir().unwrap();
473
+ let manifest = dir.path().join("pyproject.toml");
474
+ let lockfile = dir.path().join("Gemfile.lock");
475
+
476
+ fs::write(&manifest, "[project]").unwrap();
477
+ fs::write(&lockfile, "").unwrap();
478
+
479
+ let detected = detect_lockfiles(&manifest);
480
+ assert!(detected.is_empty());
481
+ }
410
482
  }
@@ -15,11 +15,11 @@ use upd::interactive::{PendingUpdate, prompt_all};
15
15
  use upd::lockfile::regenerate_lockfiles;
16
16
  use upd::registry::{
17
17
  CratesIoRegistry, GitHubReleasesRegistry, GoProxyRegistry, MultiPyPiRegistry, NpmRegistry,
18
- PyPiRegistry, RubyGemsRegistry, TerraformRegistry,
18
+ NuGetRegistry, PyPiRegistry, RubyGemsRegistry, TerraformRegistry,
19
19
  };
20
20
  use upd::updater::{
21
- CargoTomlUpdater, FileType, GemfileUpdater, GithubActionsUpdater, GoModUpdater, Lang,
22
- MiseUpdater, PackageJsonUpdater, PreCommitUpdater, PyProjectUpdater, RequirementsUpdater,
21
+ CargoTomlUpdater, CsprojUpdater, FileType, GemfileUpdater, GithubActionsUpdater, GoModUpdater,
22
+ Lang, MiseUpdater, PackageJsonUpdater, PreCommitUpdater, PyProjectUpdater, RequirementsUpdater,
23
23
  TerraformUpdater, UpdateOptions, UpdateResult, Updater, discover_files, read_file_safe,
24
24
  write_file_atomic,
25
25
  };
@@ -245,6 +245,10 @@ async fn run_update(cli: &Cli) -> Result<()> {
245
245
  let terraform_registry = TerraformRegistry::new();
246
246
  let terraform = CachedRegistry::new(terraform_registry, Arc::clone(&cache), cache_enabled);
247
247
 
248
+ // Create NuGet registry
249
+ let nuget_registry = NuGetRegistry::new();
250
+ let nuget = CachedRegistry::new(nuget_registry, Arc::clone(&cache), cache_enabled);
251
+
248
252
  // Create GitHub releases registry with optional token
249
253
  let github_releases_registry = GitHubReleasesRegistry::new();
250
254
  if cli.verbose && GitHubReleasesRegistry::detect_token().is_some() {
@@ -264,6 +268,7 @@ async fn run_update(cli: &Cli) -> Result<()> {
264
268
  let gemfile_updater = Arc::new(GemfileUpdater::new());
265
269
  let mise_updater = Arc::new(MiseUpdater::new());
266
270
  let terraform_updater = Arc::new(TerraformUpdater::new());
271
+ let csproj_updater = Arc::new(CsprojUpdater::new());
267
272
 
268
273
  // Wrap registries in Arc for parallel processing
269
274
  let pypi = Arc::new(pypi);
@@ -272,6 +277,7 @@ async fn run_update(cli: &Cli) -> Result<()> {
272
277
  let go_proxy = Arc::new(go_proxy);
273
278
  let rubygems = Arc::new(rubygems);
274
279
  let terraform = Arc::new(terraform);
280
+ let nuget = Arc::new(nuget);
275
281
  let github_releases = Arc::new(github_releases);
276
282
 
277
283
  // Interactive mode: first discover updates, then prompt, then apply approved ones
@@ -287,6 +293,7 @@ async fn run_update(cli: &Cli) -> Result<()> {
287
293
  &go_proxy,
288
294
  &rubygems,
289
295
  &terraform,
296
+ &nuget,
290
297
  &github_releases,
291
298
  &requirements_updater,
292
299
  &pyproject_updater,
@@ -298,6 +305,7 @@ async fn run_update(cli: &Cli) -> Result<()> {
298
305
  &pre_commit_updater,
299
306
  &mise_updater,
300
307
  &terraform_updater,
308
+ &csproj_updater,
301
309
  &cache,
302
310
  cache_enabled,
303
311
  )
@@ -328,6 +336,7 @@ async fn run_update(cli: &Cli) -> Result<()> {
328
336
  let go_proxy = Arc::clone(&go_proxy);
329
337
  let rubygems = Arc::clone(&rubygems);
330
338
  let terraform = Arc::clone(&terraform);
339
+ let nuget = Arc::clone(&nuget);
331
340
  let github_releases = Arc::clone(&github_releases);
332
341
  let requirements_updater = Arc::clone(&requirements_updater);
333
342
  let pyproject_updater = Arc::clone(&pyproject_updater);
@@ -338,6 +347,7 @@ async fn run_update(cli: &Cli) -> Result<()> {
338
347
  let github_actions_updater = Arc::clone(&github_actions_updater);
339
348
  let pre_commit_updater = Arc::clone(&pre_commit_updater);
340
349
  let mise_updater = Arc::clone(&mise_updater);
350
+ let csproj_updater = Arc::clone(&csproj_updater);
341
351
  let terraform_updater = Arc::clone(&terraform_updater);
342
352
  let update_options = update_options.clone();
343
353
 
@@ -388,6 +398,11 @@ async fn run_update(cli: &Cli) -> Result<()> {
388
398
  .update(&path, github_releases.as_ref(), update_options.clone())
389
399
  .await
390
400
  }
401
+ FileType::Csproj => {
402
+ csproj_updater
403
+ .update(&path, nuget.as_ref(), update_options.clone())
404
+ .await
405
+ }
391
406
  FileType::TerraformTf => {
392
407
  terraform_updater
393
408
  .update(&path, terraform.as_ref(), update_options.clone())
@@ -489,6 +504,7 @@ async fn run_interactive_update(
489
504
  go_proxy: &Arc<CachedRegistry<GoProxyRegistry>>,
490
505
  rubygems: &Arc<CachedRegistry<RubyGemsRegistry>>,
491
506
  terraform: &Arc<CachedRegistry<TerraformRegistry>>,
507
+ nuget: &Arc<CachedRegistry<NuGetRegistry>>,
492
508
  github_releases: &Arc<CachedRegistry<GitHubReleasesRegistry>>,
493
509
  requirements_updater: &Arc<RequirementsUpdater>,
494
510
  pyproject_updater: &Arc<PyProjectUpdater>,
@@ -500,6 +516,7 @@ async fn run_interactive_update(
500
516
  pre_commit_updater: &Arc<PreCommitUpdater>,
501
517
  mise_updater: &Arc<MiseUpdater>,
502
518
  terraform_updater: &Arc<TerraformUpdater>,
519
+ csproj_updater: &Arc<CsprojUpdater>,
503
520
  cache: &Arc<std::sync::Mutex<Cache>>,
504
521
  cache_enabled: bool,
505
522
  ) -> Result<()> {
@@ -565,6 +582,11 @@ async fn run_interactive_update(
565
582
  .update(path, github_releases.as_ref(), dry_run_options.clone())
566
583
  .await
567
584
  }
585
+ FileType::Csproj => {
586
+ csproj_updater
587
+ .update(path, nuget.as_ref(), dry_run_options.clone())
588
+ .await
589
+ }
568
590
  FileType::TerraformTf => {
569
591
  terraform_updater
570
592
  .update(path, terraform.as_ref(), dry_run_options.clone())
@@ -690,6 +712,11 @@ async fn run_interactive_update(
690
712
  .update(path, github_releases.as_ref(), apply_options.clone())
691
713
  .await
692
714
  }
715
+ FileType::Csproj => {
716
+ csproj_updater
717
+ .update(path, nuget.as_ref(), apply_options.clone())
718
+ .await
719
+ }
693
720
  FileType::TerraformTf => {
694
721
  terraform_updater
695
722
  .update(path, terraform.as_ref(), apply_options.clone())
@@ -884,11 +911,12 @@ async fn run_audit(cli: &Cli) -> Result<()> {
884
911
  std::collections::HashSet::new();
885
912
 
886
913
  for ((name, lang), occurrences) in &packages {
887
- // OSV doesn't cover GitHub Actions, pre-commit hooks, mise tools, or Terraform; skip
914
+ // OSV doesn't cover GitHub Actions, pre-commit hooks, mise tools, Terraform, or .NET; skip
888
915
  if *lang == Lang::Actions
889
916
  || *lang == Lang::PreCommit
890
917
  || *lang == Lang::Mise
891
918
  || *lang == Lang::Terraform
919
+ || *lang == Lang::DotNet
892
920
  {
893
921
  continue;
894
922
  }
@@ -899,7 +927,7 @@ async fn run_audit(cli: &Cli) -> Result<()> {
899
927
  Lang::Rust => Ecosystem::CratesIo,
900
928
  Lang::Go => Ecosystem::Go,
901
929
  Lang::Ruby => Ecosystem::RubyGems,
902
- Lang::Actions | Lang::PreCommit | Lang::Mise | Lang::Terraform => {
930
+ Lang::Actions | Lang::PreCommit | Lang::Mise | Lang::Terraform | Lang::DotNet => {
903
931
  unreachable!("filtered above")
904
932
  }
905
933
  };
@@ -1054,6 +1082,7 @@ fn print_alignment(alignment: &PackageAlignment, _dry_run: bool) {
1054
1082
  Lang::Rust => " (cargo)",
1055
1083
  Lang::Go => " (go)",
1056
1084
  Lang::Ruby => " (rubygems)",
1085
+ Lang::DotNet => " (nuget)",
1057
1086
  Lang::Actions => " (actions)",
1058
1087
  Lang::PreCommit => " (pre-commit)",
1059
1088
  Lang::Mise => " (mise)",
@@ -1187,6 +1216,9 @@ fn apply_version_updates(
1187
1216
  FileType::ToolVersions => {
1188
1217
  replace_tool_versions_version(&result, package, old_version, &target_version)
1189
1218
  }
1219
+ FileType::Csproj => {
1220
+ replace_csproj_version(&result, package, old_version, &target_version)
1221
+ }
1190
1222
  FileType::TerraformTf => {
1191
1223
  replace_terraform_version(&result, package, old_version, &target_version)
1192
1224
  }
@@ -1304,6 +1336,27 @@ fn replace_gemfile_version(content: &str, package: &str, old: &str, new: &str) -
1304
1336
  .to_string()
1305
1337
  }
1306
1338
 
1339
+ fn replace_csproj_version(content: &str, package: &str, old: &str, new: &str) -> String {
1340
+ // Pattern: <PackageReference Include="package" Version="old" />
1341
+ // and: <PackageVersion Include="package" Version="old" />
1342
+ let pattern = format!(
1343
+ r#"(<(?:PackageReference|PackageVersion)\s+Include="{}"[^>]*Version="){}"#,
1344
+ regex::escape(package),
1345
+ regex::escape(old)
1346
+ );
1347
+ let re = regex::Regex::new(&pattern).unwrap();
1348
+ let mut result = re
1349
+ .replace_all(content, format!(r#"${{1}}{}"#, new))
1350
+ .to_string();
1351
+
1352
+ // Also handle <Version>old</Version> child element for the given package
1353
+ let old_element = format!("<Version>{}</Version>", old);
1354
+ let new_element = format!("<Version>{}</Version>", new);
1355
+ result = result.replacen(&old_element, &new_element, 1);
1356
+
1357
+ result
1358
+ }
1359
+
1307
1360
  fn replace_github_actions_version(content: &str, package: &str, old: &str, new: &str) -> String {
1308
1361
  let pattern = format!(
1309
1362
  r#"({}@){}(\s|$|#|")"#,