makit 0.0.169 → 0.0.170

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 (179) 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 +460 -460
  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 +32 -32
  117. data/lib/makit/process.rb +377 -377
  118. data/lib/makit/protoc.rb +112 -112
  119. data/lib/makit/rake/cli.rb +196 -196
  120. data/lib/makit/rake/trace_controller.rb +174 -174
  121. data/lib/makit/rake.rb +81 -81
  122. data/lib/makit/ruby/cli.rb +185 -185
  123. data/lib/makit/ruby.rb +25 -25
  124. data/lib/makit/rubygems.rb +137 -137
  125. data/lib/makit/secrets/azure_key_vault.rb +322 -322
  126. data/lib/makit/secrets/azure_secrets.rb +221 -221
  127. data/lib/makit/secrets/local_secrets.rb +72 -72
  128. data/lib/makit/secrets/secrets_manager.rb +105 -105
  129. data/lib/makit/secrets.rb +96 -96
  130. data/lib/makit/serializer.rb +130 -130
  131. data/lib/makit/services/builder.rb +186 -186
  132. data/lib/makit/services/error_handler.rb +226 -226
  133. data/lib/makit/services/repository_manager.rb +367 -367
  134. data/lib/makit/services/validator.rb +112 -112
  135. data/lib/makit/setup/classlib.rb +101 -101
  136. data/lib/makit/setup/gem.rb +268 -268
  137. data/lib/makit/setup/pages.rb +11 -11
  138. data/lib/makit/setup/razorclasslib.rb +101 -101
  139. data/lib/makit/setup/runner.rb +54 -54
  140. data/lib/makit/setup.rb +5 -5
  141. data/lib/makit/show.rb +110 -110
  142. data/lib/makit/storage.rb +126 -126
  143. data/lib/makit/symbols.rb +175 -175
  144. data/lib/makit/task_info.rb +130 -130
  145. data/lib/makit/tasks/at_exit.rb +15 -15
  146. data/lib/makit/tasks/build.rb +22 -22
  147. data/lib/makit/tasks/bump.rb +7 -7
  148. data/lib/makit/tasks/clean.rb +13 -13
  149. data/lib/makit/tasks/configure.rb +10 -10
  150. data/lib/makit/tasks/format.rb +10 -10
  151. data/lib/makit/tasks/hook_manager.rb +443 -443
  152. data/lib/makit/tasks/info.rb +368 -368
  153. data/lib/makit/tasks/init.rb +49 -49
  154. data/lib/makit/tasks/integrate.rb +60 -60
  155. data/lib/makit/tasks/pull_incoming.rb +13 -13
  156. data/lib/makit/tasks/secrets.rb +7 -7
  157. data/lib/makit/tasks/setup.rb +16 -16
  158. data/lib/makit/tasks/sync.rb +14 -14
  159. data/lib/makit/tasks/tag.rb +27 -27
  160. data/lib/makit/tasks/task_monkey_patch.rb +81 -81
  161. data/lib/makit/tasks/test.rb +22 -22
  162. data/lib/makit/tasks/update.rb +21 -21
  163. data/lib/makit/tasks/version.rb +6 -6
  164. data/lib/makit/tasks.rb +24 -24
  165. data/lib/makit/test_cache.rb +239 -239
  166. data/lib/makit/tree.rb +37 -37
  167. data/lib/makit/v1/configuration/project_service_impl.rb +370 -370
  168. data/lib/makit/v1/git/git_repository_service_impl.rb +295 -295
  169. data/lib/makit/v1/makit.v1_pb.rb +35 -35
  170. data/lib/makit/v1/makit.v1_services_pb.rb +27 -27
  171. data/lib/makit/v1/services/repository_manager_service_impl.rb +572 -572
  172. data/lib/makit/version.rb +661 -661
  173. data/lib/makit/version_util.rb +21 -21
  174. data/lib/makit/wix.rb +95 -95
  175. data/lib/makit/yaml.rb +29 -29
  176. data/lib/makit/zip.rb +17 -17
  177. data/lib/makit copy.rb +44 -44
  178. data/lib/makit.rb +121 -121
  179. metadata +2 -2
@@ -1,386 +1,386 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "request"
4
- require_relative "result"
5
- require_relative "strategies/synchronous"
6
- require_relative "strategies/factory"
7
- require_relative "middleware/base"
8
- require_relative "middleware/command_logger"
9
-
10
- module Makit
11
- module Commands
12
- # Modern command execution engine with proper separation of concerns.
13
- #
14
- # The Runner coordinates command execution through a middleware chain and
15
- # execution strategies. It provides a clean, extensible architecture for
16
- # command processing with support for caching, logging, validation, and
17
- # custom execution patterns.
18
- #
19
- # @example Basic usage
20
- # runner = Runner.new
21
- # request = Request.from_string("git --version")
22
- # result = runner.execute(request)
23
- # puts result.stdout
24
- #
25
- # @example With custom middleware
26
- # runner = Runner.new(middleware: [Logger.new, Cache.new])
27
- # result = runner.execute(request)
28
- #
29
- # @example With custom strategy
30
- # runner = Runner.new(strategy: Strategies::Parallel.new)
31
- # results = runner.execute_batch(requests)
32
- class Runner
33
- # @!attribute [r] middleware
34
- # @return [Array<Middleware::Base>] middleware chain
35
- # @!attribute [r] strategy
36
- # @return [Strategies::Base] execution strategy
37
- attr_reader :middleware, :strategy
38
-
39
- # Get the default configured runner instance.
40
- #
41
- # @return [Runner] default runner with standard middleware
42
- def self.default
43
- # Recreate the runner if the log level has changed
44
- current_log_level = defined?(Makit::Logging) ? Makit::Logging.current_log_level : :info
45
- if @default.nil? || @last_log_level != current_log_level
46
- @last_log_level = current_log_level
47
- begin
48
- @default = new(
49
- middleware: [
50
- Middleware::CommandLogger.new(
51
- log_stdout: true,
52
- log_stderr: true,
53
- log_performance: true,
54
- max_output_lines: 50,
55
- ),
56
- ],
57
- )
58
- rescue => e
59
- # Fallback to basic runner if there are issues
60
- @default = new
61
- end
62
- end
63
- @default
64
- end
65
-
66
- # Initialize a new command runner.
67
- #
68
- # @param middleware [Array<Middleware::Base>] middleware chain
69
- # @param strategy [Strategies::Base] execution strategy
70
- # @param options [Hash] additional configuration
71
- def initialize(middleware: nil, strategy: nil, **options)
72
- @middleware = Array(middleware || default_middleware)
73
- @strategy = strategy || Strategies::Factory.create(options)
74
- @options = options
75
-
76
- validate_middleware
77
- validate_strategy
78
-
79
- # Log runner initialization
80
- log_runner_initialization
81
- end
82
-
83
- # Execute a single command request.
84
- #
85
- # @param request [Request, String, Hash] command request to execute
86
- # @return [Result] execution result
87
- # @raise [ArgumentError] if request is invalid
88
- def execute(request)
89
- Makit::Logging.debug("Makit::Commands::Runner.execute starting")
90
- # Makit::Logging.debug("Executing request: #{request.to_json_pretty}")
91
- # Normalize request to Request object
92
- normalized_request = normalize_request(request)
93
-
94
- # Apply middleware chain
95
- Makit::Logging.debug("Makit::Commands::Runner.execute applying middleware chain")
96
- result = execute_with_middleware(normalized_request) do |processed_request|
97
- Makit::Logging.debug("Makit::Commands::Runner.execute executing strategy")
98
- result = @strategy.execute(processed_request)
99
- Makit::Logging.debug("Makit::Commands::Runner.execute strategy returned result")
100
- result
101
- end
102
- Makit::Logging.debug("Makit::Commands::Runner.execute returning result")
103
- Makit::Logging.debug("Makit::Commands::Runner request.exit_on_failure?: #{request.exit_on_failure?}")
104
- # stderr often contains warnings that we don't want to exit on
105
- if request.exit_on_failure? && (result.failure?)
106
- Makit::Logging.error("exiting with exit code: #{result.exit_code}")
107
- exit result.exit_code
108
- end
109
- result
110
- end
111
-
112
- # Execute multiple command requests.
113
- #
114
- # @param requests [Array<Request, String, Hash>] requests to execute
115
- # @return [Array<Result>] execution results
116
- def execute_batch(requests)
117
- # Normalize all requests
118
- normalized_requests = requests.map { |req| normalize_request(req) }
119
-
120
- # Use strategy's batch execution if available, otherwise execute individually
121
- if @strategy.respond_to?(:execute_batch)
122
- # Apply middleware to the entire batch
123
- execute_with_middleware_batch(normalized_requests) do |processed_requests|
124
- @strategy.execute_batch(processed_requests)
125
- end
126
- else
127
- # Execute each request individually with middleware
128
- normalized_requests.map { |request| execute(request) }
129
- end
130
- end
131
-
132
- # Add middleware to the execution chain.
133
- #
134
- # @param middleware_instance [Middleware::Base] middleware to add
135
- # @return [self] for method chaining
136
- def add_middleware(middleware_instance)
137
- validate_middleware_instance(middleware_instance)
138
- @middleware << middleware_instance
139
- self
140
- end
141
-
142
- # Remove middleware from the execution chain.
143
- #
144
- # @param middleware_class [Class] middleware class to remove
145
- # @return [self] for method chaining
146
- def remove_middleware(middleware_class)
147
- @middleware.reject! { |m| m.is_a?(middleware_class) }
148
- self
149
- end
150
-
151
- # Check if specific middleware is present.
152
- #
153
- # @param middleware_class [Class] middleware class to check
154
- # @return [Boolean] true if middleware is present
155
- def has_middleware?(middleware_class)
156
- @middleware.any? { |m| m.is_a?(middleware_class) }
157
- end
158
-
159
- # Get runner configuration for debugging.
160
- #
161
- # @return [Hash] runner configuration
162
- def config
163
- {
164
- middleware: @middleware.map(&:config),
165
- strategy: @strategy.config,
166
- options: @options,
167
- }
168
- end
169
-
170
- # Get execution statistics.
171
- #
172
- # @return [Hash] execution statistics
173
- def stats
174
- @stats ||= {
175
- total_executions: 0,
176
- successful_executions: 0,
177
- failed_executions: 0,
178
- total_duration: 0.0,
179
- }
180
- end
181
-
182
- # Make this method public as it is used by rake hooks and CLI output
183
- # Get information about the current strategy
184
- #
185
- # @return [Hash] strategy information
186
- def strategy_info
187
- {
188
- class: @strategy.class.name,
189
- type: @strategy.class.name.split("::").last.downcase,
190
- factory_info: Strategies::Factory.strategy_info,
191
- }
192
- end
193
-
194
- private
195
-
196
- # Execute request with middleware chain.
197
- #
198
- # @param request [Request] normalized request
199
- # @yield [Request] yields processed request to execution
200
- # @return [Result] execution result
201
- def execute_with_middleware(request, &block)
202
- # Build middleware chain
203
- chain = build_middleware_chain(request)
204
-
205
- # Execute chain
206
- result = chain.call(request, &block)
207
-
208
- # Log high-level success/error using the default logger
209
- log_command_result(request, result)
210
-
211
- # Update statistics
212
- update_stats(result)
213
-
214
- result
215
- end
216
-
217
- # Execute batch with middleware chain.
218
- #
219
- # @param requests [Array<Request>] normalized requests
220
- # @yield [Array<Request>] yields processed requests to execution
221
- # @return [Array<Result>] execution results
222
- def execute_with_middleware_batch(requests, &block)
223
- # For batch execution, apply middleware to each request individually
224
- # This maintains the middleware contract while supporting batch execution
225
- results = requests.map do |request|
226
- execute_with_middleware(request) { |req| [req] }
227
- end.flatten
228
-
229
- # Then execute the batch
230
- processed_requests = results.map(&:command).map { |cmd| Request.from_string(cmd) }
231
- batch_results = block.call(processed_requests)
232
-
233
- # Update statistics for batch
234
- batch_results.each { |result| update_stats(result) }
235
-
236
- batch_results
237
- end
238
-
239
- # Build middleware execution chain.
240
- #
241
- # @param request [Request] the request to process
242
- # @return [Proc] middleware chain
243
- def build_middleware_chain(request)
244
- # Filter middleware that applies to this request
245
- applicable_middleware = @middleware.select { |m| m.applicable?(request) }
246
-
247
- # Build chain in reverse order so first middleware wraps everything
248
- applicable_middleware.reverse.reduce(method(:execute_final)) do |chain, middleware|
249
- ->(req) { middleware.call(req, &chain) }
250
- end
251
- end
252
-
253
- # Final execution step (after all middleware).
254
- #
255
- # @param request [Request] processed request
256
- # @return [Result] execution result
257
- def execute_final(request)
258
- @strategy.execute(request)
259
- end
260
-
261
- # Normalize various input types to Request objects.
262
- #
263
- # @param request [Request, String, Hash] request in various formats
264
- # @return [Request] normalized request
265
- # @raise [ArgumentError] if request cannot be normalized
266
- def normalize_request(request)
267
- case request
268
- when Request
269
- request
270
- when String
271
- Request.from_string(request)
272
- when Hash
273
- Request.from_hash(request)
274
- else
275
- raise ArgumentError, "Invalid request type: #{request.class}. Expected Request, String, or Hash."
276
- end
277
- end
278
-
279
- # Update execution statistics.
280
- #
281
- # @param result [Result] execution result
282
- def update_stats(result)
283
- stats[:total_executions] += 1
284
-
285
- if result.success?
286
- stats[:successful_executions] += 1
287
- else
288
- stats[:failed_executions] += 1
289
- end
290
-
291
- stats[:total_duration] += result.duration if result.duration
292
- end
293
-
294
- # Get default middleware chain.
295
- #
296
- # @return [Array<Middleware::Base>] default middleware
297
- def default_middleware
298
- # No default middleware for now - keep it simple
299
- # Subclasses or configuration can add middleware as needed
300
- []
301
- end
302
-
303
- # Validate middleware array.
304
- def validate_middleware
305
- @middleware.each { |m| validate_middleware_instance(m) }
306
- end
307
-
308
- # Validate individual middleware instance.
309
- #
310
- # @param middleware [Object] middleware to validate
311
- # @raise [ArgumentError] if middleware is invalid
312
- def validate_middleware_instance(middleware)
313
- unless middleware.respond_to?(:call)
314
- raise ArgumentError, "Middleware must respond to #call: #{middleware.class}"
315
- end
316
-
317
- return if middleware.respond_to?(:applicable?)
318
-
319
- raise ArgumentError, "Middleware must respond to #applicable?: #{middleware.class}"
320
- end
321
-
322
- # Validate execution strategy.
323
- #
324
- # @raise [ArgumentError] if strategy is invalid
325
- def validate_strategy
326
- unless @strategy.respond_to?(:execute)
327
- raise ArgumentError, "Strategy must respond to #execute: #{@strategy.class}"
328
- end
329
-
330
- return if @strategy.respond_to?(:supports?)
331
-
332
- raise ArgumentError, "Strategy must respond to #supports?: #{@strategy.class}"
333
- end
334
-
335
- # Log command execution result using the default logger.
336
- #
337
- # @param request [Request] the executed request
338
- # @param result [Result] the execution result
339
- # @return [void]
340
- def log_command_result(request, result)
341
- # Build the full command string
342
- command_string = "#{request.command} #{request.arguments&.join(" ")}".strip
343
-
344
- # Log success or error using the default logger
345
- if result.success?
346
- Makit::Logging.success(command_string)
347
- else
348
- Makit::Logging.error(command_string)
349
- end
350
-
351
- # Log the stdout, if there is an error or stdout is enabled
352
- if result.failure? || request.show_stdout?
353
- if (!result.stdout.empty?)
354
- # split the stdout into lines and print each line
355
- result.stdout.split("\n").each do |line|
356
- Makit::Logging.info(line)
357
- end
358
- end
359
- end
360
-
361
- # Log the stderr
362
- return unless request.show_stderr? && !result.stderr.empty?
363
-
364
- # split the stderr into lines and print each line
365
- result.stderr.split("\n").each do |line|
366
- Makit::Logging.info(line)
367
- end
368
- end
369
-
370
- # Log runner initialization for debugging.
371
- #
372
- # @return [void]
373
- def log_runner_initialization
374
- # Only log if we have a command logger middleware
375
- logger_middleware = @middleware.find { |m| m.is_a?(Middleware::CommandLogger) }
376
- return unless logger_middleware
377
-
378
- logger_middleware.logger.debug("Command runner initialized",
379
- middleware_count: @middleware.length,
380
- strategy: @strategy.class.name,
381
- working_directory: Dir.pwd,
382
- options: @options)
383
- end
384
- end
385
- end
386
- end
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "request"
4
+ require_relative "result"
5
+ require_relative "strategies/synchronous"
6
+ require_relative "strategies/factory"
7
+ require_relative "middleware/base"
8
+ require_relative "middleware/command_logger"
9
+
10
+ module Makit
11
+ module Commands
12
+ # Modern command execution engine with proper separation of concerns.
13
+ #
14
+ # The Runner coordinates command execution through a middleware chain and
15
+ # execution strategies. It provides a clean, extensible architecture for
16
+ # command processing with support for caching, logging, validation, and
17
+ # custom execution patterns.
18
+ #
19
+ # @example Basic usage
20
+ # runner = Runner.new
21
+ # request = Request.from_string("git --version")
22
+ # result = runner.execute(request)
23
+ # puts result.stdout
24
+ #
25
+ # @example With custom middleware
26
+ # runner = Runner.new(middleware: [Logger.new, Cache.new])
27
+ # result = runner.execute(request)
28
+ #
29
+ # @example With custom strategy
30
+ # runner = Runner.new(strategy: Strategies::Parallel.new)
31
+ # results = runner.execute_batch(requests)
32
+ class Runner
33
+ # @!attribute [r] middleware
34
+ # @return [Array<Middleware::Base>] middleware chain
35
+ # @!attribute [r] strategy
36
+ # @return [Strategies::Base] execution strategy
37
+ attr_reader :middleware, :strategy
38
+
39
+ # Get the default configured runner instance.
40
+ #
41
+ # @return [Runner] default runner with standard middleware
42
+ def self.default
43
+ # Recreate the runner if the log level has changed
44
+ current_log_level = defined?(Makit::Logging) ? Makit::Logging.current_log_level : :info
45
+ if @default.nil? || @last_log_level != current_log_level
46
+ @last_log_level = current_log_level
47
+ begin
48
+ @default = new(
49
+ middleware: [
50
+ Middleware::CommandLogger.new(
51
+ log_stdout: true,
52
+ log_stderr: true,
53
+ log_performance: true,
54
+ max_output_lines: 50,
55
+ ),
56
+ ],
57
+ )
58
+ rescue => e
59
+ # Fallback to basic runner if there are issues
60
+ @default = new
61
+ end
62
+ end
63
+ @default
64
+ end
65
+
66
+ # Initialize a new command runner.
67
+ #
68
+ # @param middleware [Array<Middleware::Base>] middleware chain
69
+ # @param strategy [Strategies::Base] execution strategy
70
+ # @param options [Hash] additional configuration
71
+ def initialize(middleware: nil, strategy: nil, **options)
72
+ @middleware = Array(middleware || default_middleware)
73
+ @strategy = strategy || Strategies::Factory.create(options)
74
+ @options = options
75
+
76
+ validate_middleware
77
+ validate_strategy
78
+
79
+ # Log runner initialization
80
+ log_runner_initialization
81
+ end
82
+
83
+ # Execute a single command request.
84
+ #
85
+ # @param request [Request, String, Hash] command request to execute
86
+ # @return [Result] execution result
87
+ # @raise [ArgumentError] if request is invalid
88
+ def execute(request)
89
+ Makit::Logging.debug("Makit::Commands::Runner.execute starting")
90
+ # Makit::Logging.debug("Executing request: #{request.to_json_pretty}")
91
+ # Normalize request to Request object
92
+ normalized_request = normalize_request(request)
93
+
94
+ # Apply middleware chain
95
+ Makit::Logging.debug("Makit::Commands::Runner.execute applying middleware chain")
96
+ result = execute_with_middleware(normalized_request) do |processed_request|
97
+ Makit::Logging.debug("Makit::Commands::Runner.execute executing strategy")
98
+ result = @strategy.execute(processed_request)
99
+ Makit::Logging.debug("Makit::Commands::Runner.execute strategy returned result")
100
+ result
101
+ end
102
+ Makit::Logging.debug("Makit::Commands::Runner.execute returning result")
103
+ Makit::Logging.debug("Makit::Commands::Runner request.exit_on_failure?: #{request.exit_on_failure?}")
104
+ # stderr often contains warnings that we don't want to exit on
105
+ if request.exit_on_failure? && (result.failure?)
106
+ Makit::Logging.error("exiting with exit code: #{result.exit_code}")
107
+ exit result.exit_code
108
+ end
109
+ result
110
+ end
111
+
112
+ # Execute multiple command requests.
113
+ #
114
+ # @param requests [Array<Request, String, Hash>] requests to execute
115
+ # @return [Array<Result>] execution results
116
+ def execute_batch(requests)
117
+ # Normalize all requests
118
+ normalized_requests = requests.map { |req| normalize_request(req) }
119
+
120
+ # Use strategy's batch execution if available, otherwise execute individually
121
+ if @strategy.respond_to?(:execute_batch)
122
+ # Apply middleware to the entire batch
123
+ execute_with_middleware_batch(normalized_requests) do |processed_requests|
124
+ @strategy.execute_batch(processed_requests)
125
+ end
126
+ else
127
+ # Execute each request individually with middleware
128
+ normalized_requests.map { |request| execute(request) }
129
+ end
130
+ end
131
+
132
+ # Add middleware to the execution chain.
133
+ #
134
+ # @param middleware_instance [Middleware::Base] middleware to add
135
+ # @return [self] for method chaining
136
+ def add_middleware(middleware_instance)
137
+ validate_middleware_instance(middleware_instance)
138
+ @middleware << middleware_instance
139
+ self
140
+ end
141
+
142
+ # Remove middleware from the execution chain.
143
+ #
144
+ # @param middleware_class [Class] middleware class to remove
145
+ # @return [self] for method chaining
146
+ def remove_middleware(middleware_class)
147
+ @middleware.reject! { |m| m.is_a?(middleware_class) }
148
+ self
149
+ end
150
+
151
+ # Check if specific middleware is present.
152
+ #
153
+ # @param middleware_class [Class] middleware class to check
154
+ # @return [Boolean] true if middleware is present
155
+ def has_middleware?(middleware_class)
156
+ @middleware.any? { |m| m.is_a?(middleware_class) }
157
+ end
158
+
159
+ # Get runner configuration for debugging.
160
+ #
161
+ # @return [Hash] runner configuration
162
+ def config
163
+ {
164
+ middleware: @middleware.map(&:config),
165
+ strategy: @strategy.config,
166
+ options: @options,
167
+ }
168
+ end
169
+
170
+ # Get execution statistics.
171
+ #
172
+ # @return [Hash] execution statistics
173
+ def stats
174
+ @stats ||= {
175
+ total_executions: 0,
176
+ successful_executions: 0,
177
+ failed_executions: 0,
178
+ total_duration: 0.0,
179
+ }
180
+ end
181
+
182
+ # Make this method public as it is used by rake hooks and CLI output
183
+ # Get information about the current strategy
184
+ #
185
+ # @return [Hash] strategy information
186
+ def strategy_info
187
+ {
188
+ class: @strategy.class.name,
189
+ type: @strategy.class.name.split("::").last.downcase,
190
+ factory_info: Strategies::Factory.strategy_info,
191
+ }
192
+ end
193
+
194
+ private
195
+
196
+ # Execute request with middleware chain.
197
+ #
198
+ # @param request [Request] normalized request
199
+ # @yield [Request] yields processed request to execution
200
+ # @return [Result] execution result
201
+ def execute_with_middleware(request, &block)
202
+ # Build middleware chain
203
+ chain = build_middleware_chain(request)
204
+
205
+ # Execute chain
206
+ result = chain.call(request, &block)
207
+
208
+ # Log high-level success/error using the default logger
209
+ log_command_result(request, result)
210
+
211
+ # Update statistics
212
+ update_stats(result)
213
+
214
+ result
215
+ end
216
+
217
+ # Execute batch with middleware chain.
218
+ #
219
+ # @param requests [Array<Request>] normalized requests
220
+ # @yield [Array<Request>] yields processed requests to execution
221
+ # @return [Array<Result>] execution results
222
+ def execute_with_middleware_batch(requests, &block)
223
+ # For batch execution, apply middleware to each request individually
224
+ # This maintains the middleware contract while supporting batch execution
225
+ results = requests.map do |request|
226
+ execute_with_middleware(request) { |req| [req] }
227
+ end.flatten
228
+
229
+ # Then execute the batch
230
+ processed_requests = results.map(&:command).map { |cmd| Request.from_string(cmd) }
231
+ batch_results = block.call(processed_requests)
232
+
233
+ # Update statistics for batch
234
+ batch_results.each { |result| update_stats(result) }
235
+
236
+ batch_results
237
+ end
238
+
239
+ # Build middleware execution chain.
240
+ #
241
+ # @param request [Request] the request to process
242
+ # @return [Proc] middleware chain
243
+ def build_middleware_chain(request)
244
+ # Filter middleware that applies to this request
245
+ applicable_middleware = @middleware.select { |m| m.applicable?(request) }
246
+
247
+ # Build chain in reverse order so first middleware wraps everything
248
+ applicable_middleware.reverse.reduce(method(:execute_final)) do |chain, middleware|
249
+ ->(req) { middleware.call(req, &chain) }
250
+ end
251
+ end
252
+
253
+ # Final execution step (after all middleware).
254
+ #
255
+ # @param request [Request] processed request
256
+ # @return [Result] execution result
257
+ def execute_final(request)
258
+ @strategy.execute(request)
259
+ end
260
+
261
+ # Normalize various input types to Request objects.
262
+ #
263
+ # @param request [Request, String, Hash] request in various formats
264
+ # @return [Request] normalized request
265
+ # @raise [ArgumentError] if request cannot be normalized
266
+ def normalize_request(request)
267
+ case request
268
+ when Request
269
+ request
270
+ when String
271
+ Request.from_string(request)
272
+ when Hash
273
+ Request.from_hash(request)
274
+ else
275
+ raise ArgumentError, "Invalid request type: #{request.class}. Expected Request, String, or Hash."
276
+ end
277
+ end
278
+
279
+ # Update execution statistics.
280
+ #
281
+ # @param result [Result] execution result
282
+ def update_stats(result)
283
+ stats[:total_executions] += 1
284
+
285
+ if result.success?
286
+ stats[:successful_executions] += 1
287
+ else
288
+ stats[:failed_executions] += 1
289
+ end
290
+
291
+ stats[:total_duration] += result.duration if result.duration
292
+ end
293
+
294
+ # Get default middleware chain.
295
+ #
296
+ # @return [Array<Middleware::Base>] default middleware
297
+ def default_middleware
298
+ # No default middleware for now - keep it simple
299
+ # Subclasses or configuration can add middleware as needed
300
+ []
301
+ end
302
+
303
+ # Validate middleware array.
304
+ def validate_middleware
305
+ @middleware.each { |m| validate_middleware_instance(m) }
306
+ end
307
+
308
+ # Validate individual middleware instance.
309
+ #
310
+ # @param middleware [Object] middleware to validate
311
+ # @raise [ArgumentError] if middleware is invalid
312
+ def validate_middleware_instance(middleware)
313
+ unless middleware.respond_to?(:call)
314
+ raise ArgumentError, "Middleware must respond to #call: #{middleware.class}"
315
+ end
316
+
317
+ return if middleware.respond_to?(:applicable?)
318
+
319
+ raise ArgumentError, "Middleware must respond to #applicable?: #{middleware.class}"
320
+ end
321
+
322
+ # Validate execution strategy.
323
+ #
324
+ # @raise [ArgumentError] if strategy is invalid
325
+ def validate_strategy
326
+ unless @strategy.respond_to?(:execute)
327
+ raise ArgumentError, "Strategy must respond to #execute: #{@strategy.class}"
328
+ end
329
+
330
+ return if @strategy.respond_to?(:supports?)
331
+
332
+ raise ArgumentError, "Strategy must respond to #supports?: #{@strategy.class}"
333
+ end
334
+
335
+ # Log command execution result using the default logger.
336
+ #
337
+ # @param request [Request] the executed request
338
+ # @param result [Result] the execution result
339
+ # @return [void]
340
+ def log_command_result(request, result)
341
+ # Build the full command string
342
+ command_string = "#{request.command} #{request.arguments&.join(" ")}".strip
343
+
344
+ # Log success or error using the default logger
345
+ if result.success?
346
+ Makit::Logging.success(command_string)
347
+ else
348
+ Makit::Logging.error(command_string)
349
+ end
350
+
351
+ # Log the stdout, if there is an error or stdout is enabled
352
+ if result.failure? || request.show_stdout?
353
+ if (!result.stdout.empty?)
354
+ # split the stdout into lines and print each line
355
+ result.stdout.split("\n").each do |line|
356
+ Makit::Logging.info(line)
357
+ end
358
+ end
359
+ end
360
+
361
+ # Log the stderr
362
+ return unless request.show_stderr? && !result.stderr.empty?
363
+
364
+ # split the stderr into lines and print each line
365
+ result.stderr.split("\n").each do |line|
366
+ Makit::Logging.info(line)
367
+ end
368
+ end
369
+
370
+ # Log runner initialization for debugging.
371
+ #
372
+ # @return [void]
373
+ def log_runner_initialization
374
+ # Only log if we have a command logger middleware
375
+ logger_middleware = @middleware.find { |m| m.is_a?(Middleware::CommandLogger) }
376
+ return unless logger_middleware
377
+
378
+ logger_middleware.logger.debug("Command runner initialized",
379
+ middleware_count: @middleware.length,
380
+ strategy: @strategy.class.name,
381
+ working_directory: Dir.pwd,
382
+ options: @options)
383
+ end
384
+ end
385
+ end
386
+ end