makit 0.0.173 → 0.0.174

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 (180) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -41
  3. data/exe/makit +5 -5
  4. data/lib/makit/apache.rb +28 -28
  5. data/lib/makit/auto.rb +48 -48
  6. data/lib/makit/azure/blob_storage.rb +257 -257
  7. data/lib/makit/azure/cli.rb +284 -284
  8. data/lib/makit/azure-pipelines.rb +187 -187
  9. data/lib/makit/cli/base.rb +17 -17
  10. data/lib/makit/cli/build_commands.rb +500 -500
  11. data/lib/makit/cli/generators/base_generator.rb +74 -74
  12. data/lib/makit/cli/generators/dotnet_generator.rb +50 -50
  13. data/lib/makit/cli/generators/generator_factory.rb +49 -49
  14. data/lib/makit/cli/generators/node_generator.rb +50 -50
  15. data/lib/makit/cli/generators/ruby_generator.rb +77 -77
  16. data/lib/makit/cli/generators/rust_generator.rb +50 -50
  17. data/lib/makit/cli/generators/templates/dotnet_templates.rb +167 -167
  18. data/lib/makit/cli/generators/templates/node_templates.rb +161 -161
  19. data/lib/makit/cli/generators/templates/ruby/gemfile.rb +26 -26
  20. data/lib/makit/cli/generators/templates/ruby/gemspec.rb +41 -41
  21. data/lib/makit/cli/generators/templates/ruby/main_lib.rb +33 -33
  22. data/lib/makit/cli/generators/templates/ruby/rakefile.rb +35 -35
  23. data/lib/makit/cli/generators/templates/ruby/readme.rb +63 -63
  24. data/lib/makit/cli/generators/templates/ruby/test.rb +39 -39
  25. data/lib/makit/cli/generators/templates/ruby/test_helper.rb +29 -29
  26. data/lib/makit/cli/generators/templates/ruby/version.rb +29 -29
  27. data/lib/makit/cli/generators/templates/rust_templates.rb +128 -128
  28. data/lib/makit/cli/main.rb +78 -78
  29. data/lib/makit/cli/pipeline_commands.rb +311 -311
  30. data/lib/makit/cli/project_commands.rb +868 -868
  31. data/lib/makit/cli/repository_commands.rb +661 -661
  32. data/lib/makit/cli/strategy_commands.rb +207 -207
  33. data/lib/makit/cli/utility_commands.rb +521 -521
  34. data/lib/makit/commands/factory.rb +359 -359
  35. data/lib/makit/commands/middleware/base.rb +73 -73
  36. data/lib/makit/commands/middleware/cache.rb +248 -248
  37. data/lib/makit/commands/middleware/command_logger.rb +312 -312
  38. data/lib/makit/commands/middleware/validator.rb +269 -269
  39. data/lib/makit/commands/request.rb +316 -316
  40. data/lib/makit/commands/result.rb +323 -323
  41. data/lib/makit/commands/runner.rb +386 -386
  42. data/lib/makit/commands/strategies/base.rb +171 -171
  43. data/lib/makit/commands/strategies/child_process.rb +162 -162
  44. data/lib/makit/commands/strategies/factory.rb +136 -136
  45. data/lib/makit/commands/strategies/synchronous.rb +139 -139
  46. data/lib/makit/commands.rb +50 -50
  47. data/lib/makit/configuration/dotnet_project.rb +48 -48
  48. data/lib/makit/configuration/gitlab_helper.rb +61 -61
  49. data/lib/makit/configuration/project.rb +292 -292
  50. data/lib/makit/configuration/rakefile_helper.rb +43 -43
  51. data/lib/makit/configuration/step.rb +34 -34
  52. data/lib/makit/configuration/timeout.rb +74 -74
  53. data/lib/makit/configuration.rb +21 -21
  54. data/lib/makit/content/default_gitignore.rb +7 -7
  55. data/lib/makit/content/default_gitignore.txt +225 -225
  56. data/lib/makit/content/default_rakefile.rb +13 -13
  57. data/lib/makit/content/gem_rakefile.rb +16 -16
  58. data/lib/makit/context.rb +1 -1
  59. data/lib/makit/data.rb +49 -49
  60. data/lib/makit/directories.rb +170 -170
  61. data/lib/makit/directory.rb +262 -262
  62. data/lib/makit/docs/files.rb +89 -89
  63. data/lib/makit/docs/rake.rb +102 -102
  64. data/lib/makit/dotnet/cli.rb +224 -224
  65. data/lib/makit/dotnet/project.rb +217 -217
  66. data/lib/makit/dotnet/solution.rb +38 -38
  67. data/lib/makit/dotnet/solution_classlib.rb +239 -239
  68. data/lib/makit/dotnet/solution_console.rb +264 -264
  69. data/lib/makit/dotnet/solution_maui.rb +354 -354
  70. data/lib/makit/dotnet/solution_wasm.rb +275 -275
  71. data/lib/makit/dotnet/solution_wpf.rb +304 -304
  72. data/lib/makit/dotnet.rb +110 -110
  73. data/lib/makit/email.rb +90 -90
  74. data/lib/makit/environment.rb +142 -142
  75. data/lib/makit/examples/runner.rb +370 -370
  76. data/lib/makit/exceptions.rb +45 -45
  77. data/lib/makit/fileinfo.rb +32 -32
  78. data/lib/makit/files.rb +43 -43
  79. data/lib/makit/gems.rb +49 -49
  80. data/lib/makit/git/cli.rb +103 -103
  81. data/lib/makit/git/repository.rb +100 -100
  82. data/lib/makit/git.rb +104 -104
  83. data/lib/makit/github_actions.rb +202 -202
  84. data/lib/makit/gitlab/pipeline.rb +857 -857
  85. data/lib/makit/gitlab/pipeline_service_impl.rb +1535 -1535
  86. data/lib/makit/gitlab_runner.rb +59 -59
  87. data/lib/makit/humanize.rb +218 -218
  88. data/lib/makit/indexer.rb +47 -47
  89. data/lib/makit/io/filesystem.rb +111 -111
  90. data/lib/makit/io/filesystem_service_impl.rb +337 -337
  91. data/lib/makit/lint.rb +212 -212
  92. data/lib/makit/logging/configuration.rb +309 -309
  93. data/lib/makit/logging/format_registry.rb +84 -84
  94. data/lib/makit/logging/formatters/base.rb +39 -39
  95. data/lib/makit/logging/formatters/console_formatter.rb +140 -140
  96. data/lib/makit/logging/formatters/json_formatter.rb +65 -65
  97. data/lib/makit/logging/formatters/plain_text_formatter.rb +71 -71
  98. data/lib/makit/logging/formatters/text_formatter.rb +64 -64
  99. data/lib/makit/logging/log_request.rb +119 -119
  100. data/lib/makit/logging/logger.rb +199 -199
  101. data/lib/makit/logging/sinks/base.rb +91 -91
  102. data/lib/makit/logging/sinks/console.rb +72 -72
  103. data/lib/makit/logging/sinks/file_sink.rb +92 -92
  104. data/lib/makit/logging/sinks/structured.rb +123 -123
  105. data/lib/makit/logging/sinks/unified_file_sink.rb +296 -296
  106. data/lib/makit/logging.rb +578 -578
  107. data/lib/makit/markdown.rb +75 -75
  108. data/lib/makit/mp/basic_object_mp.rb +17 -17
  109. data/lib/makit/mp/command_mp.rb +13 -13
  110. data/lib/makit/mp/command_request.mp.rb +17 -17
  111. data/lib/makit/mp/project_mp.rb +199 -199
  112. data/lib/makit/mp/string_mp.rb +205 -205
  113. data/lib/makit/nuget.rb +604 -604
  114. data/lib/makit/podman/podman.rb +458 -458
  115. data/lib/makit/podman/podman_service_impl.rb +1081 -1081
  116. data/lib/makit/port.rb +212 -212
  117. data/lib/makit/port_utility.rb +17 -17
  118. data/lib/makit/process.rb +377 -377
  119. data/lib/makit/protoc.rb +112 -112
  120. data/lib/makit/rake/cli.rb +196 -196
  121. data/lib/makit/rake/trace_controller.rb +174 -174
  122. data/lib/makit/rake.rb +81 -81
  123. data/lib/makit/ruby/cli.rb +185 -185
  124. data/lib/makit/ruby.rb +25 -25
  125. data/lib/makit/rubygems.rb +137 -137
  126. data/lib/makit/secrets/azure_key_vault.rb +322 -322
  127. data/lib/makit/secrets/azure_secrets.rb +221 -221
  128. data/lib/makit/secrets/local_secrets.rb +72 -72
  129. data/lib/makit/secrets/secrets_manager.rb +105 -105
  130. data/lib/makit/secrets.rb +96 -96
  131. data/lib/makit/serializer.rb +130 -130
  132. data/lib/makit/services/builder.rb +186 -186
  133. data/lib/makit/services/error_handler.rb +226 -226
  134. data/lib/makit/services/repository_manager.rb +367 -367
  135. data/lib/makit/services/validator.rb +112 -112
  136. data/lib/makit/setup/classlib.rb +101 -101
  137. data/lib/makit/setup/gem.rb +268 -268
  138. data/lib/makit/setup/pages.rb +11 -11
  139. data/lib/makit/setup/razorclasslib.rb +101 -101
  140. data/lib/makit/setup/runner.rb +54 -54
  141. data/lib/makit/setup.rb +5 -5
  142. data/lib/makit/show.rb +110 -110
  143. data/lib/makit/storage.rb +126 -126
  144. data/lib/makit/symbols.rb +175 -175
  145. data/lib/makit/task_info.rb +130 -130
  146. data/lib/makit/tasks/at_exit.rb +15 -15
  147. data/lib/makit/tasks/build.rb +22 -22
  148. data/lib/makit/tasks/bump.rb +7 -7
  149. data/lib/makit/tasks/clean.rb +13 -13
  150. data/lib/makit/tasks/configure.rb +10 -10
  151. data/lib/makit/tasks/format.rb +10 -10
  152. data/lib/makit/tasks/hook_manager.rb +443 -443
  153. data/lib/makit/tasks/info.rb +368 -368
  154. data/lib/makit/tasks/init.rb +49 -49
  155. data/lib/makit/tasks/integrate.rb +60 -60
  156. data/lib/makit/tasks/pull_incoming.rb +13 -13
  157. data/lib/makit/tasks/secrets.rb +7 -7
  158. data/lib/makit/tasks/setup.rb +16 -16
  159. data/lib/makit/tasks/sync.rb +14 -14
  160. data/lib/makit/tasks/tag.rb +27 -27
  161. data/lib/makit/tasks/task_monkey_patch.rb +81 -81
  162. data/lib/makit/tasks/test.rb +22 -22
  163. data/lib/makit/tasks/update.rb +16 -21
  164. data/lib/makit/tasks/version.rb +6 -6
  165. data/lib/makit/tasks.rb +24 -24
  166. data/lib/makit/test_cache.rb +239 -239
  167. data/lib/makit/tree.rb +37 -37
  168. data/lib/makit/v1/configuration/project_service_impl.rb +370 -370
  169. data/lib/makit/v1/git/git_repository_service_impl.rb +295 -295
  170. data/lib/makit/v1/makit.v1_pb.rb +35 -35
  171. data/lib/makit/v1/makit.v1_services_pb.rb +27 -27
  172. data/lib/makit/v1/services/repository_manager_service_impl.rb +572 -572
  173. data/lib/makit/version.rb +661 -661
  174. data/lib/makit/version_util.rb +21 -21
  175. data/lib/makit/wix.rb +95 -95
  176. data/lib/makit/yaml.rb +29 -29
  177. data/lib/makit/zip.rb +17 -17
  178. data/lib/makit copy.rb +44 -44
  179. data/lib/makit.rb +122 -122
  180. metadata +2 -2
data/lib/makit/nuget.rb CHANGED
@@ -1,604 +1,604 @@
1
- # frozen_string_literal: true
2
-
3
- require "tmpdir"
4
- require "open3"
5
- require "fileutils"
6
- # This module provides classes for the Makit gem.
7
- module Makit
8
- # This class provide methods for working with the Nuget package cache
9
- #
10
- # Example:
11
- #
12
- # Makit::Directory.cache("Google.Protobuf", "3.27.2")
13
- #
14
- # dotnet nuget locals all --list
15
- # dotnet nuget locals all --clear
16
- #
17
- class NuGet
18
- def self.get_cache_dir(package, version)
19
- File.join(Makit::Directories::NUGET_PACKAGE_CACHE, package, version)
20
- end
21
-
22
- def self.cache(package, version)
23
- # if the package is already cached, there is nothing to do
24
- return if Dir.exist?(get_cache_dir(package, version))
25
-
26
- Dir.mktmpdir do |dir|
27
- # Use the temp directory here
28
- Dir.chdir(dir) do
29
- system("dotnet new classlib -n ClassLib")
30
- Dir.chdir("ClassLib") do
31
- # display a list of directories in the current directory
32
- puts Dir.entries(Dir.pwd)
33
- # display a list of files in the current directory
34
- puts Dir.entries(Dir.pwd)
35
- puts "dotnet add ClassLib.csproj package #{package} --version #{version}"
36
- system("dotnet add ClassLib.csproj package #{package} --version #{version}")
37
- end
38
- end
39
- # The directory and its contents will be removed automatically after the block
40
- end
41
- end
42
-
43
- def self.clear_cache(package, version)
44
- return unless Dir.exist?(get_cache_dir(package, version))
45
-
46
- FileUtils.rm_rf(get_cache_dir(package, version))
47
- end
48
-
49
- # get the latest version of the package
50
- def self.get_latest_version(package)
51
- dir = File.join(Makit::Directories::NUGET_PACKAGE_CACHE, package)
52
- if Dir.exist?(dir)
53
- versions = Dir.entries(dir).select do |entry|
54
- File.directory?(File.join(dir, entry)) && ![".", ".."].include?(entry)
55
- end
56
- highest_version = Makit::Version.get_highest_version(versions)
57
- return highest_version
58
- end
59
- nil
60
- end
61
-
62
- # publish a package to a nuget directory feed
63
- def self.add_package(_package, path)
64
- system("dotnet nuget push #{path} --source #{path}")
65
- end
66
-
67
- # Publishes a NuGet package to a remote or local NuGet feed
68
- #
69
- # This method pushes a .nupkg file to a NuGet source using `dotnet nuget push`.
70
- # It automatically includes `--skip-duplicate` to avoid errors when the package
71
- # version already exists on the feed.
72
- #
73
- # When an API key is provided, it is used for authentication but is masked
74
- # in the console output for security. When no API key is provided (nil or empty),
75
- # the `--api-key` flag is omitted, which is useful for:
76
- # - Local directory feeds that don't require authentication
77
- # - Azure DevOps feeds using Azure CLI authentication
78
- # - Feeds with credential providers configured
79
- #
80
- # @param package [String] Path to the .nupkg file to publish
81
- # @param api_key [String, nil] API key for authentication (nil to skip)
82
- # @param source [String] NuGet feed URL or source name
83
- # @return [void]
84
- #
85
- # @example Publish to NuGet.org with an API key
86
- # api_key = ENV["NUGET_API_KEY"]
87
- # Makit::NuGet.publish(
88
- # "bin/Release/MyPackage.1.0.0.nupkg",
89
- # api_key,
90
- # "https://api.nuget.org/v3/index.json"
91
- # )
92
- #
93
- # @example Publish to Azure DevOps feed (using Azure CLI auth, no API key needed)
94
- # # First authenticate with: az login
95
- # Makit::NuGet.publish(
96
- # "artifacts/MyCompany.Utils.1.2.3.nupkg",
97
- # nil,
98
- # "https://pkgs.dev.azure.com/myorg/myproject/_packaging/myfeed/nuget/v3/index.json"
99
- # )
100
- #
101
- # @example Publish to Azure DevOps feed with PAT token
102
- # pat_token = ENV["AZURE_DEVOPS_PAT"]
103
- # Makit::NuGet.publish(
104
- # "artifacts/MyCompany.Utils.1.2.3.nupkg",
105
- # pat_token,
106
- # "https://pkgs.dev.azure.com/myorg/myproject/_packaging/myfeed/nuget/v3/index.json"
107
- # )
108
- #
109
- # @example Publish to a local directory feed (no API key)
110
- # Makit::NuGet.publish(
111
- # "bin/Release/MyPackage.1.0.0.nupkg",
112
- # nil,
113
- # "/Users/louie/code/nuget"
114
- # )
115
- #
116
- # @example Publish to a configured source by name
117
- # # Using a source name configured via `dotnet nuget add source`
118
- # Makit::NuGet.publish(
119
- # "MyPackage.1.0.0.nupkg",
120
- # nil,
121
- # "local"
122
- # )
123
- #
124
- # @example Publish with API key from secrets management
125
- # api_key = Makit::Secrets.get("NUGET_API_KEY")
126
- # Makit::NuGet.publish(
127
- # "bin/Release/MyPackage.1.0.0.nupkg",
128
- # api_key,
129
- # "https://api.nuget.org/v3/index.json"
130
- # )
131
- #
132
- # @see publish_to_directory For publishing to local directory feeds with deduplication
133
- # @see configure_source For setting up NuGet sources
134
- #
135
- def self.publish(package, api_key, source)
136
- if api_key.nil? || api_key.to_s.strip.empty?
137
- # No API key provided, skip --api-key flag
138
- puts "dotnet nuget push #{package} --skip-duplicate --source #{source}"
139
- puts `dotnet nuget push #{package} --skip-duplicate --source #{source}`
140
- else
141
- # we do not want the api_key echoed to the console, substitute *****
142
- puts "dotnet nuget push #{package} --skip-duplicate --api-key ***** --source #{source}"
143
- puts `dotnet nuget push #{package} --skip-duplicate --api-key #{api_key} --source #{source}`
144
- end
145
- end
146
-
147
- # Publishes a NuGet package to a local directory feed
148
- #
149
- # This method pushes a .nupkg file to a local directory-based NuGet feed.
150
- # If the package already exists at the target location, it skips the push
151
- # and displays a message indicating the package is already present.
152
- #
153
- # The target path follows the standard NuGet directory feed structure:
154
- # {directory}/{package_name}/{version}/{package_name}.{version}.nupkg
155
- #
156
- # @param nuget_package_path [String] Full path to the .nupkg file to publish
157
- # @param directory [String] Path to the local NuGet feed directory
158
- # @param package_name [String] Name of the NuGet package (e.g., "MyCompany.MyPackage")
159
- # @param version [String] Version of the package (e.g., "1.2.3")
160
- # @return [void]
161
- #
162
- # @example Publish a package to a local feed directory
163
- # # First, configure a local NuGet source named "local"
164
- # Makit::NuGet.configure_source("local", "/Users/louie/code/nuget")
165
- #
166
- # # Publish a package to the local feed
167
- # Makit::NuGet.publish_to_directory(
168
- # "/path/to/MyPackage.1.0.0.nupkg",
169
- # "/Users/louie/code/nuget",
170
- # "MyPackage",
171
- # "1.0.0"
172
- # )
173
- # # Creates: /Users/louie/code/nuget/mypackage/1.0.0/mypackage.1.0.0.nupkg
174
- #
175
- # @example Publish multiple versions of a package
176
- # local_feed = "/Users/louie/code/nuget"
177
- #
178
- # # Publish version 1.0.0
179
- # Makit::NuGet.publish_to_directory(
180
- # "artifacts/MyCompany.Utils.1.0.0.nupkg",
181
- # local_feed,
182
- # "MyCompany.Utils",
183
- # "1.0.0"
184
- # )
185
- #
186
- # # Publish version 1.0.1
187
- # Makit::NuGet.publish_to_directory(
188
- # "artifacts/MyCompany.Utils.1.0.1.nupkg",
189
- # local_feed,
190
- # "MyCompany.Utils",
191
- # "1.0.1"
192
- # )
193
- #
194
- # @example Use with a configured "local" source
195
- # # Configure the "local" source if not already configured
196
- # Makit::NuGet.configure_source("local", "/Users/louie/code/nuget")
197
- #
198
- # # Get the source directory from the configured sources
199
- # sources = Makit::NuGet.list_sources
200
- # local_source = sources.find { |s| s[:name] == "local" }
201
- # local_dir = local_source[:url] if local_source
202
- #
203
- # # Publish to the local source
204
- # if local_dir
205
- # Makit::NuGet.publish_to_directory(
206
- # "bin/Release/MyApp.1.2.3.nupkg",
207
- # local_dir,
208
- # "MyApp",
209
- # "1.2.3"
210
- # )
211
- # end
212
- #
213
- # @see configure_source For setting up the local NuGet source
214
- # @see list_sources For retrieving configured NuGet sources
215
- # @see migrate_packages For migrating packages between feeds
216
- #
217
- def self.publish_to_directory(nuget_package_path, directory, package_name, version)
218
- target_package_path = "#{directory}/#{package_name}/#{version}/#{package_name}.#{version}.nupkg".downcase
219
- if File.exist?(target_package_path)
220
- puts " #{target_package_path} already exists".colorize(:grey)
221
- else
222
- "dotnet nuget push #{nuget_package_path} --source #{directory}".run
223
- end
224
- end
225
-
226
- # -----------------------
227
- # NuGet Source Management
228
- # -----------------------
229
-
230
- # Lists all configured NuGet sources
231
- #
232
- # @return [Array<Hash>] Array of source hashes with keys: :name, :url, :enabled
233
- def self.list_sources
234
- stdout, stderr, status = Open3.capture3("dotnet", "nuget", "list", "source")
235
-
236
- unless status.success?
237
- Makit::Logging.default_logger.warn("Failed to list NuGet sources: #{stderr}")
238
- return []
239
- end
240
-
241
- sources = []
242
- current_source = nil
243
- lines = stdout.lines
244
-
245
- lines.each_with_index do |line, index|
246
- line_stripped = line.strip
247
- next if line_stripped.empty?
248
-
249
- # Skip header lines
250
- next if line_stripped.start_with?("Registered") || line_stripped.start_with?("---")
251
-
252
- # Parse numbered source lines like:
253
- # " 11. nuget.org [Enabled]"
254
- # " 1. nuget.org [Disabled]"
255
- match = line_stripped.match(/^\s*\d+\.\s+(.+?)(?:\s+\[(Enabled|Disabled)\])?$/)
256
- if match
257
- name_part = match[1]
258
- enabled_str = match[2]
259
-
260
- next if name_part.nil? || name_part.strip.empty?
261
-
262
- name = name_part.strip
263
-
264
- # Default to enabled if not specified
265
- enabled = enabled_str.nil? ? true : (enabled_str == "Enabled")
266
-
267
- # Create new source entry
268
- current_source = { name: name, url: nil, enabled: enabled }
269
- sources << current_source
270
-
271
- # Check next line for URL or path (indented line)
272
- if index + 1 < lines.length
273
- next_line = lines[index + 1]
274
- next_line_stripped = next_line.strip
275
-
276
- # Check if next line is indented (starts with whitespace) and contains URL or path
277
- if next_line.match?(/^\s+/) && !next_line_stripped.empty?
278
- # Check if it's a URL
279
- if next_line_stripped.match?(/^https?:\/\//)
280
- current_source[:url] = next_line_stripped
281
- # Check if it's a file path (Windows drive letter, UNC path, or Unix path)
282
- elsif next_line_stripped.match?(/^[A-Z]:[\\\/]/) || next_line_stripped.match?(/^\\\\/) || next_line_stripped.match?(/^\/[^\/]/)
283
- # For local sources, use the path as the "URL" for comparison purposes
284
- current_source[:url] = next_line_stripped
285
- end
286
- end
287
- end
288
- end
289
- end
290
-
291
- sources
292
- end
293
-
294
- # Checks if a NuGet source with the given name exists
295
- #
296
- # @param name [String] The source name to check
297
- # @return [Boolean] true if the source exists, false otherwise
298
- def self.has_source?(name)
299
- sources = list_sources
300
- sources.any? { |source| source[:name] == name }
301
- end
302
-
303
- # Adds a NuGet source
304
- #
305
- # @param name [String] The source name
306
- # @param url [String] The source URL
307
- # @param username [String, nil] Optional username for authenticated sources
308
- # @param password [String, nil] Optional password for authenticated sources
309
- # @param store_password_in_clear_text [Boolean] Whether to store password in clear text (default: false)
310
- # @return [Boolean] true if successful, false otherwise
311
- def self.add_source(name, url, username: nil, password: nil, store_password_in_clear_text: false)
312
- raise ArgumentError, "name is required" if name.nil? || name.strip.empty?
313
- raise ArgumentError, "url is required" if url.nil? || url.strip.empty?
314
-
315
- # Check if source already exists
316
- if has_source?(name)
317
- Makit::Logging.default_logger.info("NuGet source '#{name}' already exists")
318
- return false
319
- end
320
-
321
- args = ["dotnet", "nuget", "add", "source", url, "--name", name]
322
- args << "--username" << username if username
323
- args << "--password" << password if password
324
- args << "--store-password-in-clear-text" if store_password_in_clear_text
325
-
326
- stdout, stderr, status = Open3.capture3(*args)
327
-
328
- if status.success?
329
- Makit::Logging.default_logger.info("Added NuGet source '#{name}' (#{url})")
330
- true
331
- else
332
- Makit::Logging.default_logger.error("Failed to add NuGet source '#{name}': #{stderr}")
333
- false
334
- end
335
- end
336
-
337
- # Removes a NuGet source
338
- #
339
- # @param name [String] The source name to remove
340
- # @return [Boolean] true if successful, false otherwise
341
- def self.remove_source(name)
342
- raise ArgumentError, "name is required" if name.nil? || name.strip.empty?
343
-
344
- unless has_source?(name)
345
- Makit::Logging.default_logger.info("NuGet source '#{name}' does not exist")
346
- return false
347
- end
348
-
349
- stdout, stderr, status = Open3.capture3("dotnet", "nuget", "remove", "source", name)
350
-
351
- if status.success?
352
- Makit::Logging.default_logger.info("Removed NuGet source '#{name}'")
353
- true
354
- else
355
- Makit::Logging.default_logger.error("Failed to remove NuGet source '#{name}': #{stderr}")
356
- false
357
- end
358
- end
359
-
360
- # Configures a NuGet source (adds if it doesn't exist, updates if URL differs, no-op if already configured)
361
- #
362
- # @param name [String] The source name
363
- # @param url [String] The source URL
364
- # @param username [String, nil] Optional username for authenticated sources
365
- # @param password [String, nil] Optional password for authenticated sources
366
- # @param store_password_in_clear_text [Boolean] Whether to store password in clear text (default: false)
367
- # @return [Boolean] true if successful or already configured, false otherwise
368
- def self.configure_source(name, url, username: nil, password: nil, store_password_in_clear_text: false)
369
- raise ArgumentError, "name is required" if name.nil? || name.strip.empty?
370
- raise ArgumentError, "url is required" if url.nil? || url.strip.empty?
371
-
372
- # Check if source already exists
373
- if has_source?(name)
374
- # Get existing source details
375
- sources = list_sources
376
- existing_source = sources.find { |s| s[:name] == name }
377
-
378
- # If source exists with the same URL, it's already configured correctly (no-op)
379
- if existing_source && existing_source[:url] == url
380
- Makit::Logging.default_logger.info("NuGet source '#{name}' already exists with URL '#{url}', skipping configuration")
381
- return true
382
- end
383
-
384
- # Source exists but URL is different, update it
385
- Makit::Logging.default_logger.info("NuGet source '#{name}' already exists with different URL, updating...")
386
- remove_source(name)
387
- end
388
-
389
- # Add the source
390
- add_source(name, url, username: username, password: password, store_password_in_clear_text: store_password_in_clear_text)
391
- end
392
-
393
- # Migrates packages from one NuGet source to another, matching a pattern
394
- #
395
- # For local directory feeds, packages are found by scanning the directory structure:
396
- # {source_path}/{package_name}/{version}/{package_name}.{version}.nupkg
397
- #
398
- # @param source_name [String] The name of the source to migrate from
399
- # @param package_pattern [String] The package name pattern to match (e.g., "rep*", "MyPackage.*")
400
- # @param destination_name [String] The name of the destination source
401
- # @return [Integer] The number of packages migrated, or -1 if an error occurred
402
- def self.migrate_packages(source_name, package_pattern, destination_name)
403
- raise ArgumentError, "source_name is required" if source_name.nil? || source_name.strip.empty?
404
- raise ArgumentError, "package_pattern is required" if package_pattern.nil? || package_pattern.strip.empty?
405
- raise ArgumentError, "destination_name is required" if destination_name.nil? || destination_name.strip.empty?
406
-
407
- source_name = source_name.strip
408
- package_pattern = package_pattern.strip
409
- destination_name = destination_name.strip
410
-
411
- # Get source information
412
- sources = list_sources
413
- source_info = sources.find { |s| s[:name] == source_name }
414
- destination_info = sources.find { |s| s[:name] == destination_name }
415
-
416
- unless source_info
417
- Makit::Logging.default_logger.error("Source '#{source_name}' not found")
418
- return -1
419
- end
420
-
421
- unless destination_info
422
- Makit::Logging.default_logger.error("Destination source '#{destination_name}' not found")
423
- return -1
424
- end
425
-
426
- source_path = source_info[:url]
427
- destination_path = destination_info[:url]
428
-
429
- # Source must be a local directory feed (not a URL)
430
- unless source_path && !source_path.match?(/^https?:\/\//)
431
- Makit::Logging.default_logger.error("Source '#{source_name}' is not a local directory feed (URL: #{source_path})")
432
- return -1
433
- end
434
-
435
- # Expand and normalize the source path
436
- source_path = File.expand_path(source_path)
437
-
438
- # Destination can be either local directory feed or remote feed (Azure DevOps, NuGet.org, etc.)
439
- is_remote_destination = destination_path && destination_path.match?(/^https?:\/\//)
440
-
441
- # Ensure source directory exists
442
- unless Dir.exist?(source_path)
443
- Makit::Logging.default_logger.error("Source directory does not exist: #{source_path}")
444
- return -1
445
- end
446
-
447
- Makit::Logging.default_logger.info("Searching for packages matching '#{package_pattern}' in source '#{source_name}' (#{source_path})")
448
- Makit::Logging.default_logger.info("Destination: #{destination_name} (#{destination_path}) - #{is_remote_destination ? 'Remote feed' : 'Local directory feed'}")
449
-
450
- # Ensure destination directory exists (only for local feeds)
451
- unless is_remote_destination
452
- FileUtils.mkdir_p(destination_path) unless Dir.exist?(destination_path)
453
- end
454
-
455
- # Find all .nupkg files matching the pattern
456
- # Simply search recursively for all .nupkg files and match package names
457
- matching_packages = []
458
-
459
- # Glob for all .nupkg files recursively in the source directory
460
- Dir.glob(File.join(source_path, "**", "*.nupkg")) do |nupkg_file|
461
- # Extract package name from the file path
462
- # Path structure is typically: {source_path}/{package_name}/{version}/{package_name}.{version}.nupkg
463
- relative_path = nupkg_file.sub(/^#{Regexp.escape(source_path)}[\/\\]?/, "")
464
- path_parts = relative_path.split(/[\/\\]/)
465
-
466
- # Get package name from directory structure (first directory after source_path)
467
- package_name = path_parts.length >= 1 ? path_parts[0] : nil
468
-
469
- # If we can't get it from path, try to extract from filename
470
- if package_name.nil? || package_name.empty?
471
- filename = File.basename(nupkg_file, ".nupkg")
472
- # Try to extract package name by removing version suffix (e.g., "1.2.3" or "1.2.3-beta")
473
- if filename.match?(/^(.+?)\.(\d+\.\d+.*)$/)
474
- package_name = $1
475
- else
476
- package_name = filename
477
- end
478
- end
479
-
480
- # Check if package name matches pattern
481
- if package_name && File.fnmatch?(package_pattern, package_name, File::FNM_DOTMATCH)
482
- # Extract version from path (second directory) or from filename
483
- version = path_parts.length >= 2 ? path_parts[1] : nil
484
-
485
- # If we couldn't extract version from path, try from filename
486
- if version.nil? || version.empty?
487
- filename = File.basename(nupkg_file, ".nupkg")
488
- if filename.match?(/^.+?\.(\d+\.\d+.*)$/)
489
- version = $1
490
- else
491
- version = "unknown"
492
- end
493
- end
494
-
495
- matching_packages << {
496
- package_name: package_name,
497
- version: version,
498
- nupkg_path: nupkg_file
499
- }
500
- end
501
- end
502
-
503
- if matching_packages.empty?
504
- Makit::Logging.default_logger.info("No packages found matching pattern '#{package_pattern}' in source '#{source_name}'")
505
- return 0
506
- end
507
-
508
- Makit::Logging.default_logger.info("Found #{matching_packages.length} package(s) matching pattern '#{package_pattern}' in source '#{source_name}'")
509
-
510
- # Migrate each package
511
- migrated_count = 0
512
- failed_count = 0
513
-
514
- matching_packages.each do |package_info|
515
- package_name = package_info[:package_name]
516
- version = package_info[:version]
517
- nupkg_path = package_info[:nupkg_path]
518
-
519
- Makit::Logging.default_logger.info("Migrating #{package_name}.#{version} from '#{source_name}' to '#{destination_name}'...")
520
-
521
- if is_remote_destination
522
- # For remote feeds (Azure DevOps, NuGet.org, etc.), use dotnet nuget push
523
- # Azure DevOps requires --api-key az (placeholder) and --skip-duplicate
524
- Makit::Logging.default_logger.info(" Pushing #{package_name}.#{version} to remote feed...")
525
-
526
- args = ["dotnet", "nuget", "push", nupkg_path, "--source", destination_path, "--skip-duplicate"]
527
-
528
- # For Azure DevOps feeds, add --api-key az (required placeholder)
529
- if destination_path.include?("dev.azure.com") || destination_path.include?("pkgs.dev.azure.com")
530
- args << "--api-key" << "az"
531
- end
532
-
533
- stdout, stderr, status = Open3.capture3(*args)
534
-
535
- if status.success?
536
- Makit::Logging.default_logger.info(" Successfully migrated #{package_name}.#{version} to remote feed")
537
- migrated_count += 1
538
- else
539
- # Check if it's a duplicate (which is okay with --skip-duplicate)
540
- if stderr.include?("already exists") || stderr.include?("duplicate")
541
- Makit::Logging.default_logger.info(" Package already exists in destination, skipping: #{package_name}.#{version}")
542
- migrated_count += 1
543
- else
544
- Makit::Logging.default_logger.error(" Failed to migrate #{package_name}.#{version}: #{stderr}")
545
- failed_count += 1
546
- end
547
- end
548
- else
549
- # For local directory feeds, use 'nuget add' which automatically organizes packages
550
- # into the hierarchical structure: {packageID}/{version}/packageID.version.nupkg
551
- target_package_dir = File.join(destination_path, package_name.downcase, version)
552
- target_package_path = File.join(target_package_dir, File.basename(nupkg_path).downcase)
553
-
554
- if File.exist?(target_package_path)
555
- Makit::Logging.default_logger.info(" Package already exists in destination, skipping: #{package_name}.#{version}")
556
- migrated_count += 1
557
- next
558
- end
559
-
560
- stdout, stderr, status = Open3.capture3("nuget", "add", nupkg_path, "-Source", destination_path)
561
-
562
- if status.success?
563
- Makit::Logging.default_logger.info(" Successfully migrated and organized #{package_name}.#{version}")
564
- migrated_count += 1
565
- else
566
- # Fallback to dotnet nuget push if nuget.exe is not available
567
- nuget_exe = Makit::Environment.which("nuget")
568
- if nuget_exe.nil? || nuget_exe.empty?
569
- Makit::Logging.default_logger.info(" nuget.exe not found, falling back to dotnet nuget push")
570
-
571
- # Manually organize package into proper directory structure
572
- Makit::Logging.default_logger.info(" Organizing package into directory structure: #{target_package_dir}")
573
- FileUtils.mkdir_p(target_package_dir)
574
- FileUtils.cp(nupkg_path, target_package_path)
575
-
576
- # Push using dotnet nuget push from the organized location
577
- stdout, stderr, status = Open3.capture3("dotnet", "nuget", "push", target_package_path, "--source", destination_path)
578
-
579
- if status.success?
580
- Makit::Logging.default_logger.info(" Successfully migrated and organized #{package_name}.#{version}")
581
- migrated_count += 1
582
- else
583
- Makit::Logging.default_logger.warn(" Package copied to organized structure but dotnet nuget push failed: #{stderr}")
584
- Makit::Logging.default_logger.warn(" Package is available at #{target_package_path} but may need manual indexing")
585
- migrated_count += 1
586
- end
587
- else
588
- Makit::Logging.default_logger.error(" Failed to migrate #{package_name}.#{version}: #{stderr}")
589
- failed_count += 1
590
- end
591
- end
592
- end
593
- end
594
-
595
- if failed_count > 0
596
- Makit::Logging.default_logger.warn("Migration completed with #{failed_count} failure(s). #{migrated_count} package(s) migrated successfully.")
597
- else
598
- Makit::Logging.default_logger.info("Successfully migrated #{migrated_count} package(s) from '#{source_name}' to '#{destination_name}'")
599
- end
600
-
601
- migrated_count
602
- end
603
- end
604
- end
1
+ # frozen_string_literal: true
2
+
3
+ require "tmpdir"
4
+ require "open3"
5
+ require "fileutils"
6
+ # This module provides classes for the Makit gem.
7
+ module Makit
8
+ # This class provide methods for working with the Nuget package cache
9
+ #
10
+ # Example:
11
+ #
12
+ # Makit::Directory.cache("Google.Protobuf", "3.27.2")
13
+ #
14
+ # dotnet nuget locals all --list
15
+ # dotnet nuget locals all --clear
16
+ #
17
+ class NuGet
18
+ def self.get_cache_dir(package, version)
19
+ File.join(Makit::Directories::NUGET_PACKAGE_CACHE, package, version)
20
+ end
21
+
22
+ def self.cache(package, version)
23
+ # if the package is already cached, there is nothing to do
24
+ return if Dir.exist?(get_cache_dir(package, version))
25
+
26
+ Dir.mktmpdir do |dir|
27
+ # Use the temp directory here
28
+ Dir.chdir(dir) do
29
+ system("dotnet new classlib -n ClassLib")
30
+ Dir.chdir("ClassLib") do
31
+ # display a list of directories in the current directory
32
+ puts Dir.entries(Dir.pwd)
33
+ # display a list of files in the current directory
34
+ puts Dir.entries(Dir.pwd)
35
+ puts "dotnet add ClassLib.csproj package #{package} --version #{version}"
36
+ system("dotnet add ClassLib.csproj package #{package} --version #{version}")
37
+ end
38
+ end
39
+ # The directory and its contents will be removed automatically after the block
40
+ end
41
+ end
42
+
43
+ def self.clear_cache(package, version)
44
+ return unless Dir.exist?(get_cache_dir(package, version))
45
+
46
+ FileUtils.rm_rf(get_cache_dir(package, version))
47
+ end
48
+
49
+ # get the latest version of the package
50
+ def self.get_latest_version(package)
51
+ dir = File.join(Makit::Directories::NUGET_PACKAGE_CACHE, package)
52
+ if Dir.exist?(dir)
53
+ versions = Dir.entries(dir).select do |entry|
54
+ File.directory?(File.join(dir, entry)) && ![".", ".."].include?(entry)
55
+ end
56
+ highest_version = Makit::Version.get_highest_version(versions)
57
+ return highest_version
58
+ end
59
+ nil
60
+ end
61
+
62
+ # publish a package to a nuget directory feed
63
+ def self.add_package(_package, path)
64
+ system("dotnet nuget push #{path} --source #{path}")
65
+ end
66
+
67
+ # Publishes a NuGet package to a remote or local NuGet feed
68
+ #
69
+ # This method pushes a .nupkg file to a NuGet source using `dotnet nuget push`.
70
+ # It automatically includes `--skip-duplicate` to avoid errors when the package
71
+ # version already exists on the feed.
72
+ #
73
+ # When an API key is provided, it is used for authentication but is masked
74
+ # in the console output for security. When no API key is provided (nil or empty),
75
+ # the `--api-key` flag is omitted, which is useful for:
76
+ # - Local directory feeds that don't require authentication
77
+ # - Azure DevOps feeds using Azure CLI authentication
78
+ # - Feeds with credential providers configured
79
+ #
80
+ # @param package [String] Path to the .nupkg file to publish
81
+ # @param api_key [String, nil] API key for authentication (nil to skip)
82
+ # @param source [String] NuGet feed URL or source name
83
+ # @return [void]
84
+ #
85
+ # @example Publish to NuGet.org with an API key
86
+ # api_key = ENV["NUGET_API_KEY"]
87
+ # Makit::NuGet.publish(
88
+ # "bin/Release/MyPackage.1.0.0.nupkg",
89
+ # api_key,
90
+ # "https://api.nuget.org/v3/index.json"
91
+ # )
92
+ #
93
+ # @example Publish to Azure DevOps feed (using Azure CLI auth, no API key needed)
94
+ # # First authenticate with: az login
95
+ # Makit::NuGet.publish(
96
+ # "artifacts/MyCompany.Utils.1.2.3.nupkg",
97
+ # nil,
98
+ # "https://pkgs.dev.azure.com/myorg/myproject/_packaging/myfeed/nuget/v3/index.json"
99
+ # )
100
+ #
101
+ # @example Publish to Azure DevOps feed with PAT token
102
+ # pat_token = ENV["AZURE_DEVOPS_PAT"]
103
+ # Makit::NuGet.publish(
104
+ # "artifacts/MyCompany.Utils.1.2.3.nupkg",
105
+ # pat_token,
106
+ # "https://pkgs.dev.azure.com/myorg/myproject/_packaging/myfeed/nuget/v3/index.json"
107
+ # )
108
+ #
109
+ # @example Publish to a local directory feed (no API key)
110
+ # Makit::NuGet.publish(
111
+ # "bin/Release/MyPackage.1.0.0.nupkg",
112
+ # nil,
113
+ # "/Users/louie/code/nuget"
114
+ # )
115
+ #
116
+ # @example Publish to a configured source by name
117
+ # # Using a source name configured via `dotnet nuget add source`
118
+ # Makit::NuGet.publish(
119
+ # "MyPackage.1.0.0.nupkg",
120
+ # nil,
121
+ # "local"
122
+ # )
123
+ #
124
+ # @example Publish with API key from secrets management
125
+ # api_key = Makit::Secrets.get("NUGET_API_KEY")
126
+ # Makit::NuGet.publish(
127
+ # "bin/Release/MyPackage.1.0.0.nupkg",
128
+ # api_key,
129
+ # "https://api.nuget.org/v3/index.json"
130
+ # )
131
+ #
132
+ # @see publish_to_directory For publishing to local directory feeds with deduplication
133
+ # @see configure_source For setting up NuGet sources
134
+ #
135
+ def self.publish(package, api_key, source)
136
+ if api_key.nil? || api_key.to_s.strip.empty?
137
+ # No API key provided, skip --api-key flag
138
+ puts "dotnet nuget push #{package} --skip-duplicate --source #{source}"
139
+ puts `dotnet nuget push #{package} --skip-duplicate --source #{source}`
140
+ else
141
+ # we do not want the api_key echoed to the console, substitute *****
142
+ puts "dotnet nuget push #{package} --skip-duplicate --api-key ***** --source #{source}"
143
+ puts `dotnet nuget push #{package} --skip-duplicate --api-key #{api_key} --source #{source}`
144
+ end
145
+ end
146
+
147
+ # Publishes a NuGet package to a local directory feed
148
+ #
149
+ # This method pushes a .nupkg file to a local directory-based NuGet feed.
150
+ # If the package already exists at the target location, it skips the push
151
+ # and displays a message indicating the package is already present.
152
+ #
153
+ # The target path follows the standard NuGet directory feed structure:
154
+ # {directory}/{package_name}/{version}/{package_name}.{version}.nupkg
155
+ #
156
+ # @param nuget_package_path [String] Full path to the .nupkg file to publish
157
+ # @param directory [String] Path to the local NuGet feed directory
158
+ # @param package_name [String] Name of the NuGet package (e.g., "MyCompany.MyPackage")
159
+ # @param version [String] Version of the package (e.g., "1.2.3")
160
+ # @return [void]
161
+ #
162
+ # @example Publish a package to a local feed directory
163
+ # # First, configure a local NuGet source named "local"
164
+ # Makit::NuGet.configure_source("local", "/Users/louie/code/nuget")
165
+ #
166
+ # # Publish a package to the local feed
167
+ # Makit::NuGet.publish_to_directory(
168
+ # "/path/to/MyPackage.1.0.0.nupkg",
169
+ # "/Users/louie/code/nuget",
170
+ # "MyPackage",
171
+ # "1.0.0"
172
+ # )
173
+ # # Creates: /Users/louie/code/nuget/mypackage/1.0.0/mypackage.1.0.0.nupkg
174
+ #
175
+ # @example Publish multiple versions of a package
176
+ # local_feed = "/Users/louie/code/nuget"
177
+ #
178
+ # # Publish version 1.0.0
179
+ # Makit::NuGet.publish_to_directory(
180
+ # "artifacts/MyCompany.Utils.1.0.0.nupkg",
181
+ # local_feed,
182
+ # "MyCompany.Utils",
183
+ # "1.0.0"
184
+ # )
185
+ #
186
+ # # Publish version 1.0.1
187
+ # Makit::NuGet.publish_to_directory(
188
+ # "artifacts/MyCompany.Utils.1.0.1.nupkg",
189
+ # local_feed,
190
+ # "MyCompany.Utils",
191
+ # "1.0.1"
192
+ # )
193
+ #
194
+ # @example Use with a configured "local" source
195
+ # # Configure the "local" source if not already configured
196
+ # Makit::NuGet.configure_source("local", "/Users/louie/code/nuget")
197
+ #
198
+ # # Get the source directory from the configured sources
199
+ # sources = Makit::NuGet.list_sources
200
+ # local_source = sources.find { |s| s[:name] == "local" }
201
+ # local_dir = local_source[:url] if local_source
202
+ #
203
+ # # Publish to the local source
204
+ # if local_dir
205
+ # Makit::NuGet.publish_to_directory(
206
+ # "bin/Release/MyApp.1.2.3.nupkg",
207
+ # local_dir,
208
+ # "MyApp",
209
+ # "1.2.3"
210
+ # )
211
+ # end
212
+ #
213
+ # @see configure_source For setting up the local NuGet source
214
+ # @see list_sources For retrieving configured NuGet sources
215
+ # @see migrate_packages For migrating packages between feeds
216
+ #
217
+ def self.publish_to_directory(nuget_package_path, directory, package_name, version)
218
+ target_package_path = "#{directory}/#{package_name}/#{version}/#{package_name}.#{version}.nupkg".downcase
219
+ if File.exist?(target_package_path)
220
+ puts " #{target_package_path} already exists".colorize(:grey)
221
+ else
222
+ "dotnet nuget push #{nuget_package_path} --source #{directory}".run
223
+ end
224
+ end
225
+
226
+ # -----------------------
227
+ # NuGet Source Management
228
+ # -----------------------
229
+
230
+ # Lists all configured NuGet sources
231
+ #
232
+ # @return [Array<Hash>] Array of source hashes with keys: :name, :url, :enabled
233
+ def self.list_sources
234
+ stdout, stderr, status = Open3.capture3("dotnet", "nuget", "list", "source")
235
+
236
+ unless status.success?
237
+ Makit::Logging.default_logger.warn("Failed to list NuGet sources: #{stderr}")
238
+ return []
239
+ end
240
+
241
+ sources = []
242
+ current_source = nil
243
+ lines = stdout.lines
244
+
245
+ lines.each_with_index do |line, index|
246
+ line_stripped = line.strip
247
+ next if line_stripped.empty?
248
+
249
+ # Skip header lines
250
+ next if line_stripped.start_with?("Registered") || line_stripped.start_with?("---")
251
+
252
+ # Parse numbered source lines like:
253
+ # " 11. nuget.org [Enabled]"
254
+ # " 1. nuget.org [Disabled]"
255
+ match = line_stripped.match(/^\s*\d+\.\s+(.+?)(?:\s+\[(Enabled|Disabled)\])?$/)
256
+ if match
257
+ name_part = match[1]
258
+ enabled_str = match[2]
259
+
260
+ next if name_part.nil? || name_part.strip.empty?
261
+
262
+ name = name_part.strip
263
+
264
+ # Default to enabled if not specified
265
+ enabled = enabled_str.nil? ? true : (enabled_str == "Enabled")
266
+
267
+ # Create new source entry
268
+ current_source = { name: name, url: nil, enabled: enabled }
269
+ sources << current_source
270
+
271
+ # Check next line for URL or path (indented line)
272
+ if index + 1 < lines.length
273
+ next_line = lines[index + 1]
274
+ next_line_stripped = next_line.strip
275
+
276
+ # Check if next line is indented (starts with whitespace) and contains URL or path
277
+ if next_line.match?(/^\s+/) && !next_line_stripped.empty?
278
+ # Check if it's a URL
279
+ if next_line_stripped.match?(/^https?:\/\//)
280
+ current_source[:url] = next_line_stripped
281
+ # Check if it's a file path (Windows drive letter, UNC path, or Unix path)
282
+ elsif next_line_stripped.match?(/^[A-Z]:[\\\/]/) || next_line_stripped.match?(/^\\\\/) || next_line_stripped.match?(/^\/[^\/]/)
283
+ # For local sources, use the path as the "URL" for comparison purposes
284
+ current_source[:url] = next_line_stripped
285
+ end
286
+ end
287
+ end
288
+ end
289
+ end
290
+
291
+ sources
292
+ end
293
+
294
+ # Checks if a NuGet source with the given name exists
295
+ #
296
+ # @param name [String] The source name to check
297
+ # @return [Boolean] true if the source exists, false otherwise
298
+ def self.has_source?(name)
299
+ sources = list_sources
300
+ sources.any? { |source| source[:name] == name }
301
+ end
302
+
303
+ # Adds a NuGet source
304
+ #
305
+ # @param name [String] The source name
306
+ # @param url [String] The source URL
307
+ # @param username [String, nil] Optional username for authenticated sources
308
+ # @param password [String, nil] Optional password for authenticated sources
309
+ # @param store_password_in_clear_text [Boolean] Whether to store password in clear text (default: false)
310
+ # @return [Boolean] true if successful, false otherwise
311
+ def self.add_source(name, url, username: nil, password: nil, store_password_in_clear_text: false)
312
+ raise ArgumentError, "name is required" if name.nil? || name.strip.empty?
313
+ raise ArgumentError, "url is required" if url.nil? || url.strip.empty?
314
+
315
+ # Check if source already exists
316
+ if has_source?(name)
317
+ Makit::Logging.default_logger.info("NuGet source '#{name}' already exists")
318
+ return false
319
+ end
320
+
321
+ args = ["dotnet", "nuget", "add", "source", url, "--name", name]
322
+ args << "--username" << username if username
323
+ args << "--password" << password if password
324
+ args << "--store-password-in-clear-text" if store_password_in_clear_text
325
+
326
+ stdout, stderr, status = Open3.capture3(*args)
327
+
328
+ if status.success?
329
+ Makit::Logging.default_logger.info("Added NuGet source '#{name}' (#{url})")
330
+ true
331
+ else
332
+ Makit::Logging.default_logger.error("Failed to add NuGet source '#{name}': #{stderr}")
333
+ false
334
+ end
335
+ end
336
+
337
+ # Removes a NuGet source
338
+ #
339
+ # @param name [String] The source name to remove
340
+ # @return [Boolean] true if successful, false otherwise
341
+ def self.remove_source(name)
342
+ raise ArgumentError, "name is required" if name.nil? || name.strip.empty?
343
+
344
+ unless has_source?(name)
345
+ Makit::Logging.default_logger.info("NuGet source '#{name}' does not exist")
346
+ return false
347
+ end
348
+
349
+ stdout, stderr, status = Open3.capture3("dotnet", "nuget", "remove", "source", name)
350
+
351
+ if status.success?
352
+ Makit::Logging.default_logger.info("Removed NuGet source '#{name}'")
353
+ true
354
+ else
355
+ Makit::Logging.default_logger.error("Failed to remove NuGet source '#{name}': #{stderr}")
356
+ false
357
+ end
358
+ end
359
+
360
+ # Configures a NuGet source (adds if it doesn't exist, updates if URL differs, no-op if already configured)
361
+ #
362
+ # @param name [String] The source name
363
+ # @param url [String] The source URL
364
+ # @param username [String, nil] Optional username for authenticated sources
365
+ # @param password [String, nil] Optional password for authenticated sources
366
+ # @param store_password_in_clear_text [Boolean] Whether to store password in clear text (default: false)
367
+ # @return [Boolean] true if successful or already configured, false otherwise
368
+ def self.configure_source(name, url, username: nil, password: nil, store_password_in_clear_text: false)
369
+ raise ArgumentError, "name is required" if name.nil? || name.strip.empty?
370
+ raise ArgumentError, "url is required" if url.nil? || url.strip.empty?
371
+
372
+ # Check if source already exists
373
+ if has_source?(name)
374
+ # Get existing source details
375
+ sources = list_sources
376
+ existing_source = sources.find { |s| s[:name] == name }
377
+
378
+ # If source exists with the same URL, it's already configured correctly (no-op)
379
+ if existing_source && existing_source[:url] == url
380
+ Makit::Logging.default_logger.info("NuGet source '#{name}' already exists with URL '#{url}', skipping configuration")
381
+ return true
382
+ end
383
+
384
+ # Source exists but URL is different, update it
385
+ Makit::Logging.default_logger.info("NuGet source '#{name}' already exists with different URL, updating...")
386
+ remove_source(name)
387
+ end
388
+
389
+ # Add the source
390
+ add_source(name, url, username: username, password: password, store_password_in_clear_text: store_password_in_clear_text)
391
+ end
392
+
393
+ # Migrates packages from one NuGet source to another, matching a pattern
394
+ #
395
+ # For local directory feeds, packages are found by scanning the directory structure:
396
+ # {source_path}/{package_name}/{version}/{package_name}.{version}.nupkg
397
+ #
398
+ # @param source_name [String] The name of the source to migrate from
399
+ # @param package_pattern [String] The package name pattern to match (e.g., "rep*", "MyPackage.*")
400
+ # @param destination_name [String] The name of the destination source
401
+ # @return [Integer] The number of packages migrated, or -1 if an error occurred
402
+ def self.migrate_packages(source_name, package_pattern, destination_name)
403
+ raise ArgumentError, "source_name is required" if source_name.nil? || source_name.strip.empty?
404
+ raise ArgumentError, "package_pattern is required" if package_pattern.nil? || package_pattern.strip.empty?
405
+ raise ArgumentError, "destination_name is required" if destination_name.nil? || destination_name.strip.empty?
406
+
407
+ source_name = source_name.strip
408
+ package_pattern = package_pattern.strip
409
+ destination_name = destination_name.strip
410
+
411
+ # Get source information
412
+ sources = list_sources
413
+ source_info = sources.find { |s| s[:name] == source_name }
414
+ destination_info = sources.find { |s| s[:name] == destination_name }
415
+
416
+ unless source_info
417
+ Makit::Logging.default_logger.error("Source '#{source_name}' not found")
418
+ return -1
419
+ end
420
+
421
+ unless destination_info
422
+ Makit::Logging.default_logger.error("Destination source '#{destination_name}' not found")
423
+ return -1
424
+ end
425
+
426
+ source_path = source_info[:url]
427
+ destination_path = destination_info[:url]
428
+
429
+ # Source must be a local directory feed (not a URL)
430
+ unless source_path && !source_path.match?(/^https?:\/\//)
431
+ Makit::Logging.default_logger.error("Source '#{source_name}' is not a local directory feed (URL: #{source_path})")
432
+ return -1
433
+ end
434
+
435
+ # Expand and normalize the source path
436
+ source_path = File.expand_path(source_path)
437
+
438
+ # Destination can be either local directory feed or remote feed (Azure DevOps, NuGet.org, etc.)
439
+ is_remote_destination = destination_path && destination_path.match?(/^https?:\/\//)
440
+
441
+ # Ensure source directory exists
442
+ unless Dir.exist?(source_path)
443
+ Makit::Logging.default_logger.error("Source directory does not exist: #{source_path}")
444
+ return -1
445
+ end
446
+
447
+ Makit::Logging.default_logger.info("Searching for packages matching '#{package_pattern}' in source '#{source_name}' (#{source_path})")
448
+ Makit::Logging.default_logger.info("Destination: #{destination_name} (#{destination_path}) - #{is_remote_destination ? 'Remote feed' : 'Local directory feed'}")
449
+
450
+ # Ensure destination directory exists (only for local feeds)
451
+ unless is_remote_destination
452
+ FileUtils.mkdir_p(destination_path) unless Dir.exist?(destination_path)
453
+ end
454
+
455
+ # Find all .nupkg files matching the pattern
456
+ # Simply search recursively for all .nupkg files and match package names
457
+ matching_packages = []
458
+
459
+ # Glob for all .nupkg files recursively in the source directory
460
+ Dir.glob(File.join(source_path, "**", "*.nupkg")) do |nupkg_file|
461
+ # Extract package name from the file path
462
+ # Path structure is typically: {source_path}/{package_name}/{version}/{package_name}.{version}.nupkg
463
+ relative_path = nupkg_file.sub(/^#{Regexp.escape(source_path)}[\/\\]?/, "")
464
+ path_parts = relative_path.split(/[\/\\]/)
465
+
466
+ # Get package name from directory structure (first directory after source_path)
467
+ package_name = path_parts.length >= 1 ? path_parts[0] : nil
468
+
469
+ # If we can't get it from path, try to extract from filename
470
+ if package_name.nil? || package_name.empty?
471
+ filename = File.basename(nupkg_file, ".nupkg")
472
+ # Try to extract package name by removing version suffix (e.g., "1.2.3" or "1.2.3-beta")
473
+ if filename.match?(/^(.+?)\.(\d+\.\d+.*)$/)
474
+ package_name = $1
475
+ else
476
+ package_name = filename
477
+ end
478
+ end
479
+
480
+ # Check if package name matches pattern
481
+ if package_name && File.fnmatch?(package_pattern, package_name, File::FNM_DOTMATCH)
482
+ # Extract version from path (second directory) or from filename
483
+ version = path_parts.length >= 2 ? path_parts[1] : nil
484
+
485
+ # If we couldn't extract version from path, try from filename
486
+ if version.nil? || version.empty?
487
+ filename = File.basename(nupkg_file, ".nupkg")
488
+ if filename.match?(/^.+?\.(\d+\.\d+.*)$/)
489
+ version = $1
490
+ else
491
+ version = "unknown"
492
+ end
493
+ end
494
+
495
+ matching_packages << {
496
+ package_name: package_name,
497
+ version: version,
498
+ nupkg_path: nupkg_file
499
+ }
500
+ end
501
+ end
502
+
503
+ if matching_packages.empty?
504
+ Makit::Logging.default_logger.info("No packages found matching pattern '#{package_pattern}' in source '#{source_name}'")
505
+ return 0
506
+ end
507
+
508
+ Makit::Logging.default_logger.info("Found #{matching_packages.length} package(s) matching pattern '#{package_pattern}' in source '#{source_name}'")
509
+
510
+ # Migrate each package
511
+ migrated_count = 0
512
+ failed_count = 0
513
+
514
+ matching_packages.each do |package_info|
515
+ package_name = package_info[:package_name]
516
+ version = package_info[:version]
517
+ nupkg_path = package_info[:nupkg_path]
518
+
519
+ Makit::Logging.default_logger.info("Migrating #{package_name}.#{version} from '#{source_name}' to '#{destination_name}'...")
520
+
521
+ if is_remote_destination
522
+ # For remote feeds (Azure DevOps, NuGet.org, etc.), use dotnet nuget push
523
+ # Azure DevOps requires --api-key az (placeholder) and --skip-duplicate
524
+ Makit::Logging.default_logger.info(" Pushing #{package_name}.#{version} to remote feed...")
525
+
526
+ args = ["dotnet", "nuget", "push", nupkg_path, "--source", destination_path, "--skip-duplicate"]
527
+
528
+ # For Azure DevOps feeds, add --api-key az (required placeholder)
529
+ if destination_path.include?("dev.azure.com") || destination_path.include?("pkgs.dev.azure.com")
530
+ args << "--api-key" << "az"
531
+ end
532
+
533
+ stdout, stderr, status = Open3.capture3(*args)
534
+
535
+ if status.success?
536
+ Makit::Logging.default_logger.info(" Successfully migrated #{package_name}.#{version} to remote feed")
537
+ migrated_count += 1
538
+ else
539
+ # Check if it's a duplicate (which is okay with --skip-duplicate)
540
+ if stderr.include?("already exists") || stderr.include?("duplicate")
541
+ Makit::Logging.default_logger.info(" Package already exists in destination, skipping: #{package_name}.#{version}")
542
+ migrated_count += 1
543
+ else
544
+ Makit::Logging.default_logger.error(" Failed to migrate #{package_name}.#{version}: #{stderr}")
545
+ failed_count += 1
546
+ end
547
+ end
548
+ else
549
+ # For local directory feeds, use 'nuget add' which automatically organizes packages
550
+ # into the hierarchical structure: {packageID}/{version}/packageID.version.nupkg
551
+ target_package_dir = File.join(destination_path, package_name.downcase, version)
552
+ target_package_path = File.join(target_package_dir, File.basename(nupkg_path).downcase)
553
+
554
+ if File.exist?(target_package_path)
555
+ Makit::Logging.default_logger.info(" Package already exists in destination, skipping: #{package_name}.#{version}")
556
+ migrated_count += 1
557
+ next
558
+ end
559
+
560
+ stdout, stderr, status = Open3.capture3("nuget", "add", nupkg_path, "-Source", destination_path)
561
+
562
+ if status.success?
563
+ Makit::Logging.default_logger.info(" Successfully migrated and organized #{package_name}.#{version}")
564
+ migrated_count += 1
565
+ else
566
+ # Fallback to dotnet nuget push if nuget.exe is not available
567
+ nuget_exe = Makit::Environment.which("nuget")
568
+ if nuget_exe.nil? || nuget_exe.empty?
569
+ Makit::Logging.default_logger.info(" nuget.exe not found, falling back to dotnet nuget push")
570
+
571
+ # Manually organize package into proper directory structure
572
+ Makit::Logging.default_logger.info(" Organizing package into directory structure: #{target_package_dir}")
573
+ FileUtils.mkdir_p(target_package_dir)
574
+ FileUtils.cp(nupkg_path, target_package_path)
575
+
576
+ # Push using dotnet nuget push from the organized location
577
+ stdout, stderr, status = Open3.capture3("dotnet", "nuget", "push", target_package_path, "--source", destination_path)
578
+
579
+ if status.success?
580
+ Makit::Logging.default_logger.info(" Successfully migrated and organized #{package_name}.#{version}")
581
+ migrated_count += 1
582
+ else
583
+ Makit::Logging.default_logger.warn(" Package copied to organized structure but dotnet nuget push failed: #{stderr}")
584
+ Makit::Logging.default_logger.warn(" Package is available at #{target_package_path} but may need manual indexing")
585
+ migrated_count += 1
586
+ end
587
+ else
588
+ Makit::Logging.default_logger.error(" Failed to migrate #{package_name}.#{version}: #{stderr}")
589
+ failed_count += 1
590
+ end
591
+ end
592
+ end
593
+ end
594
+
595
+ if failed_count > 0
596
+ Makit::Logging.default_logger.warn("Migration completed with #{failed_count} failure(s). #{migrated_count} package(s) migrated successfully.")
597
+ else
598
+ Makit::Logging.default_logger.info("Successfully migrated #{migrated_count} package(s) from '#{source_name}' to '#{destination_name}'")
599
+ end
600
+
601
+ migrated_count
602
+ end
603
+ end
604
+ end