makit 0.0.98 → 0.0.111

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 (148) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -0
  3. data/exe/makit +5 -0
  4. data/lib/makit/apache.rb +7 -11
  5. data/lib/makit/cli/build_commands.rb +500 -0
  6. data/lib/makit/cli/generators/base_generator.rb +74 -0
  7. data/lib/makit/cli/generators/dotnet_generator.rb +50 -0
  8. data/lib/makit/cli/generators/generator_factory.rb +49 -0
  9. data/lib/makit/cli/generators/node_generator.rb +50 -0
  10. data/lib/makit/cli/generators/ruby_generator.rb +77 -0
  11. data/lib/makit/cli/generators/rust_generator.rb +50 -0
  12. data/lib/makit/cli/generators/templates/dotnet_templates.rb +167 -0
  13. data/lib/makit/cli/generators/templates/node_templates.rb +161 -0
  14. data/lib/makit/cli/generators/templates/ruby/gemfile.rb +26 -0
  15. data/lib/makit/cli/generators/templates/ruby/gemspec.rb +40 -0
  16. data/lib/makit/cli/generators/templates/ruby/main_lib.rb +33 -0
  17. data/lib/makit/cli/generators/templates/ruby/rakefile.rb +35 -0
  18. data/lib/makit/cli/generators/templates/ruby/readme.rb +63 -0
  19. data/lib/makit/cli/generators/templates/ruby/test.rb +39 -0
  20. data/lib/makit/cli/generators/templates/ruby/test_helper.rb +29 -0
  21. data/lib/makit/cli/generators/templates/ruby/version.rb +29 -0
  22. data/lib/makit/cli/generators/templates/rust_templates.rb +128 -0
  23. data/lib/makit/cli/main.rb +48 -19
  24. data/lib/makit/cli/project_commands.rb +868 -0
  25. data/lib/makit/cli/repository_commands.rb +661 -0
  26. data/lib/makit/cli/utility_commands.rb +521 -0
  27. data/lib/makit/command_runner.rb +187 -128
  28. data/lib/makit/commands/compatibility.rb +365 -0
  29. data/lib/makit/commands/factory.rb +359 -0
  30. data/lib/makit/commands/middleware/base.rb +73 -0
  31. data/lib/makit/commands/middleware/cache.rb +248 -0
  32. data/lib/makit/commands/middleware/command_logger.rb +323 -0
  33. data/lib/makit/commands/middleware/unified_logger.rb +243 -0
  34. data/lib/makit/commands/middleware/validator.rb +269 -0
  35. data/lib/makit/commands/request.rb +254 -0
  36. data/lib/makit/commands/result.rb +323 -0
  37. data/lib/makit/commands/runner.rb +317 -0
  38. data/lib/makit/commands/strategies/base.rb +160 -0
  39. data/lib/makit/commands/strategies/synchronous.rb +134 -0
  40. data/lib/makit/commands.rb +24 -3
  41. data/lib/makit/configuration/gitlab_helper.rb +60 -0
  42. data/lib/makit/configuration/project.rb +127 -0
  43. data/lib/makit/configuration/rakefile_helper.rb +43 -0
  44. data/lib/makit/configuration/step.rb +34 -0
  45. data/lib/makit/configuration.rb +14 -0
  46. data/lib/makit/content/default_gitignore.rb +4 -2
  47. data/lib/makit/content/default_rakefile.rb +4 -2
  48. data/lib/makit/content/gem_rakefile.rb +4 -2
  49. data/lib/makit/context.rb +1 -0
  50. data/lib/makit/data.rb +9 -10
  51. data/lib/makit/directories.rb +48 -52
  52. data/lib/makit/directory.rb +38 -52
  53. data/lib/makit/docs/files.rb +5 -10
  54. data/lib/makit/docs/rake.rb +16 -20
  55. data/lib/makit/dotnet/cli.rb +65 -0
  56. data/lib/makit/dotnet/project.rb +153 -0
  57. data/lib/makit/dotnet/solution.rb +38 -0
  58. data/lib/makit/dotnet/solution_classlib.rb +239 -0
  59. data/lib/makit/dotnet/solution_console.rb +264 -0
  60. data/lib/makit/dotnet/solution_maui.rb +354 -0
  61. data/lib/makit/dotnet/solution_wasm.rb +275 -0
  62. data/lib/makit/dotnet/solution_wpf.rb +304 -0
  63. data/lib/makit/dotnet.rb +54 -171
  64. data/lib/makit/email.rb +46 -17
  65. data/lib/makit/environment.rb +22 -19
  66. data/lib/makit/examples/runner.rb +370 -0
  67. data/lib/makit/exceptions.rb +45 -0
  68. data/lib/makit/fileinfo.rb +3 -5
  69. data/lib/makit/files.rb +12 -16
  70. data/lib/makit/gems.rb +40 -39
  71. data/lib/makit/git/cli.rb +54 -0
  72. data/lib/makit/git/repository.rb +90 -0
  73. data/lib/makit/git.rb +44 -91
  74. data/lib/makit/gitlab_runner.rb +0 -1
  75. data/lib/makit/humanize.rb +31 -23
  76. data/lib/makit/indexer.rb +15 -24
  77. data/lib/makit/logging/configuration.rb +305 -0
  78. data/lib/makit/logging/format_registry.rb +84 -0
  79. data/lib/makit/logging/formatters/base.rb +39 -0
  80. data/lib/makit/logging/formatters/console_formatter.rb +127 -0
  81. data/lib/makit/logging/formatters/json_formatter.rb +65 -0
  82. data/lib/makit/logging/formatters/plain_text_formatter.rb +71 -0
  83. data/lib/makit/logging/formatters/text_formatter.rb +64 -0
  84. data/lib/makit/logging/log_request.rb +115 -0
  85. data/lib/makit/logging/logger.rb +159 -0
  86. data/lib/makit/logging/sinks/base.rb +91 -0
  87. data/lib/makit/logging/sinks/console.rb +72 -0
  88. data/lib/makit/logging/sinks/file_sink.rb +92 -0
  89. data/lib/makit/logging/sinks/structured.rb +129 -0
  90. data/lib/makit/logging/sinks/unified_file_sink.rb +303 -0
  91. data/lib/makit/logging.rb +452 -37
  92. data/lib/makit/markdown.rb +18 -18
  93. data/lib/makit/mp/basic_object_mp.rb +5 -4
  94. data/lib/makit/mp/command_mp.rb +5 -5
  95. data/lib/makit/mp/command_request.mp.rb +3 -2
  96. data/lib/makit/mp/project_mp.rb +85 -96
  97. data/lib/makit/mp/string_mp.rb +245 -73
  98. data/lib/makit/nuget.rb +27 -25
  99. data/lib/makit/port.rb +25 -27
  100. data/lib/makit/process.rb +127 -29
  101. data/lib/makit/protoc.rb +27 -24
  102. data/lib/makit/rake/cli.rb +196 -0
  103. data/lib/makit/rake.rb +6 -6
  104. data/lib/makit/ruby/cli.rb +185 -0
  105. data/lib/makit/ruby.rb +25 -0
  106. data/lib/makit/secrets.rb +18 -18
  107. data/lib/makit/serializer.rb +29 -27
  108. data/lib/makit/services/builder.rb +186 -0
  109. data/lib/makit/services/error_handler.rb +226 -0
  110. data/lib/makit/services/repository_manager.rb +229 -0
  111. data/lib/makit/services/validator.rb +112 -0
  112. data/lib/makit/setup/classlib.rb +53 -0
  113. data/lib/makit/setup/gem.rb +250 -0
  114. data/lib/makit/setup/runner.rb +40 -0
  115. data/lib/makit/show.rb +16 -16
  116. data/lib/makit/storage.rb +32 -37
  117. data/lib/makit/symbols.rb +12 -0
  118. data/lib/makit/task_hooks.rb +125 -0
  119. data/lib/makit/task_info.rb +63 -21
  120. data/lib/makit/tasks/at_exit.rb +13 -0
  121. data/lib/makit/tasks/build.rb +18 -0
  122. data/lib/makit/tasks/clean.rb +11 -0
  123. data/lib/makit/tasks/hook_manager.rb +239 -0
  124. data/lib/makit/tasks/init.rb +47 -0
  125. data/lib/makit/tasks/integrate.rb +15 -0
  126. data/lib/makit/tasks/pull_incoming.rb +12 -0
  127. data/lib/makit/tasks/setup.rb +6 -0
  128. data/lib/makit/tasks/sync.rb +11 -0
  129. data/lib/makit/tasks/task_monkey_patch.rb +79 -0
  130. data/lib/makit/tasks.rb +5 -150
  131. data/lib/makit/test_cache.rb +239 -0
  132. data/lib/makit/v1/makit.v1_pb.rb +34 -35
  133. data/lib/makit/v1/makit.v1_services_pb.rb +2 -0
  134. data/lib/makit/version.rb +1 -60
  135. data/lib/makit/wix.rb +23 -23
  136. data/lib/makit/yaml.rb +18 -6
  137. data/lib/makit.rb +2 -261
  138. metadata +109 -145
  139. data/lib/makit/cli/clean.rb +0 -14
  140. data/lib/makit/cli/clone.rb +0 -59
  141. data/lib/makit/cli/init.rb +0 -38
  142. data/lib/makit/cli/make.rb +0 -54
  143. data/lib/makit/cli/new.rb +0 -37
  144. data/lib/makit/cli/nuget_cache.rb +0 -38
  145. data/lib/makit/cli/pull.rb +0 -31
  146. data/lib/makit/cli/setup.rb +0 -71
  147. data/lib/makit/cli/work.rb +0 -21
  148. data/lib/makit/content/default_gitignore.txt +0 -222
@@ -0,0 +1,323 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "time"
5
+
6
+ module Makit
7
+ module Commands
8
+ # Structured result objects with comprehensive metadata.
9
+ # Uses native Ruby objects instead of protobuf for better performance.
10
+ #
11
+ # @example Basic result creation
12
+ # result = Result.new(
13
+ # command: "git --version",
14
+ # exit_code: 0,
15
+ # stdout: "git version 2.39.5\n",
16
+ # stderr: ""
17
+ # )
18
+ #
19
+ # @example Result with full metadata
20
+ # result = Result.new(
21
+ # command: "bundle install",
22
+ # exit_code: 0,
23
+ # stdout: "Bundle complete!",
24
+ # stderr: "",
25
+ # started_at: Time.now - 10,
26
+ # finished_at: Time.now,
27
+ # metadata: { gems_installed: 42, cache_hit: true }
28
+ # )
29
+ class Result
30
+ # Command execution result with comprehensive metadata.
31
+ #
32
+ # @!attribute [r] command
33
+ # @return [String] the command that was executed
34
+ # @!attribute [r] exit_code
35
+ # @return [Integer] process exit code
36
+ # @!attribute [r] stdout
37
+ # @return [String] standard output
38
+ # @!attribute [r] stderr
39
+ # @return [String] standard error output
40
+ # @!attribute [r] started_at
41
+ # @return [Time] when execution started
42
+ # @!attribute [r] finished_at
43
+ # @return [Time, nil] when execution finished
44
+ # @!attribute [r] duration
45
+ # @return [Float, nil] execution duration in seconds
46
+ # @!attribute [r] metadata
47
+ # @return [Hash] additional metadata
48
+ attr_reader :command, :exit_code, :stdout, :stderr, :started_at, :finished_at, :duration, :metadata
49
+
50
+ # Initialize a new command result.
51
+ #
52
+ # @param command [String] the command that was executed
53
+ # @param exit_code [Integer] process exit code (default: nil)
54
+ # @param stdout [String] standard output (default: "")
55
+ # @param stderr [String] standard error output (default: "")
56
+ # @param started_at [Time] when execution started (default: Time.now)
57
+ # @param finished_at [Time, nil] when execution finished
58
+ # @param metadata [Hash] additional metadata (default: {})
59
+ def initialize(command:, **attributes)
60
+ @command = command.to_s
61
+ @exit_code = attributes[:exit_code]
62
+ @stdout = attributes[:stdout] || ""
63
+ @stderr = attributes[:stderr] || ""
64
+ @started_at = attributes[:started_at] || Time.now
65
+ @finished_at = attributes[:finished_at]
66
+ @duration = calculate_duration
67
+ @metadata = attributes[:metadata] || {}
68
+
69
+ # Ensure output is always strings
70
+ @stdout = @stdout.to_s
71
+ @stderr = @stderr.to_s
72
+ end
73
+
74
+ # Mark the result as finished.
75
+ #
76
+ # @param finished_at [Time] when execution finished (default: Time.now)
77
+ # @param exit_code [Integer] process exit code
78
+ # @param stdout [String] standard output
79
+ # @param stderr [String] standard error output
80
+ # @return [self] for method chaining
81
+ def finish!(finished_at: Time.now, exit_code: nil, stdout: nil, stderr: nil)
82
+ @finished_at = finished_at
83
+ @duration = calculate_duration
84
+ @exit_code = exit_code if exit_code
85
+ @stdout = stdout.to_s if stdout
86
+ @stderr = stderr.to_s if stderr
87
+ self
88
+ end
89
+
90
+ # Check if the command executed successfully.
91
+ #
92
+ # @return [Boolean] true if exit code is 0
93
+ def success?
94
+ exit_code.zero?
95
+ end
96
+
97
+ # Check if the command execution failed.
98
+ #
99
+ # @return [Boolean] true if exit code is not 0
100
+ def failure?
101
+ !success?
102
+ end
103
+
104
+ # Check if the command execution is complete.
105
+ #
106
+ # @return [Boolean] true if finished_at is set
107
+ def finished?
108
+ !finished_at.nil?
109
+ end
110
+
111
+ # Check if the command is still running.
112
+ #
113
+ # @return [Boolean] true if execution started but not finished
114
+ def running?
115
+ !started_at.nil? && finished_at.nil?
116
+ end
117
+
118
+ # Get combined output (stdout + stderr).
119
+ #
120
+ # @return [String] combined output
121
+ def output
122
+ [stdout, stderr].reject(&:empty?).join("\n")
123
+ end
124
+
125
+ # Get the first line of stdout (useful for simple commands).
126
+ #
127
+ # @return [String, nil] first line of stdout
128
+ def first_line
129
+ return nil if stdout.empty?
130
+
131
+ stdout.lines.first&.chomp
132
+ end
133
+
134
+ # Get the last line of stdout (useful for progress indicators).
135
+ #
136
+ # @return [String, nil] last line of stdout
137
+ def last_line
138
+ return nil if stdout.empty?
139
+
140
+ stdout.lines.last&.chomp
141
+ end
142
+
143
+ # Check if output contains specific text.
144
+ #
145
+ # @param text [String] text to search for
146
+ # @param case_sensitive [Boolean] whether to perform case-sensitive search
147
+ # @return [Boolean] true if text is found in stdout or stderr
148
+ def contains?(text, case_sensitive: true)
149
+ search_text = case_sensitive ? text : text.downcase
150
+ search_stdout = case_sensitive ? stdout : stdout.downcase
151
+ search_stderr = case_sensitive ? stderr : stderr.downcase
152
+
153
+ search_stdout.include?(search_text) || search_stderr.include?(search_text)
154
+ end
155
+
156
+ # Convert result to hash representation.
157
+ #
158
+ # @return [Hash] hash representation of the result
159
+ def to_h
160
+ {
161
+ command: command,
162
+ exit_code: exit_code,
163
+ stdout: stdout,
164
+ stderr: stderr,
165
+ started_at: started_at&.iso8601,
166
+ finished_at: finished_at&.iso8601,
167
+ duration: duration,
168
+ success: success?,
169
+ metadata: metadata,
170
+ }
171
+ end
172
+
173
+ # Convert result to JSON representation.
174
+ #
175
+ # @param args [Array] arguments passed to JSON.generate
176
+ # @return [String] JSON representation
177
+ def to_json(*args)
178
+ JSON.generate(to_h, *args)
179
+ end
180
+
181
+ # Create result from hash representation.
182
+ #
183
+ # @param hash [Hash] hash containing result information
184
+ # @return [Result] new result object
185
+ def self.from_hash(hash)
186
+ # Parse time strings back to Time objects
187
+ started_at = hash[:started_at] || hash["started_at"]
188
+ finished_at = hash[:finished_at] || hash["finished_at"]
189
+
190
+ started_at = Time.iso8601(started_at) if started_at.is_a?(String)
191
+ finished_at = Time.iso8601(finished_at) if finished_at.is_a?(String)
192
+
193
+ new(
194
+ command: hash[:command] || hash["command"],
195
+ exit_code: hash[:exit_code] || hash["exit_code"],
196
+ stdout: hash[:stdout] || hash["stdout"] || "",
197
+ stderr: hash[:stderr] || hash["stderr"] || "",
198
+ started_at: started_at,
199
+ finished_at: finished_at,
200
+ metadata: hash[:metadata] || hash["metadata"] || {},
201
+ )
202
+ end
203
+
204
+ # Create result from JSON string.
205
+ #
206
+ # @param json_string [String] JSON string representation
207
+ # @return [Result] new result object
208
+ # @raise [JSON::ParserError] if JSON is invalid
209
+ def self.from_json(json_string)
210
+ hash = JSON.parse(json_string)
211
+ from_hash(hash)
212
+ end
213
+
214
+ # Create a failed result with error information.
215
+ #
216
+ # @param command [String] the command that failed
217
+ # @param error [Exception, String] error that occurred
218
+ # @param started_at [Time] when execution started
219
+ # @return [Result] failed result object
220
+ def self.failure(command:, error:, started_at: Time.now)
221
+ error_message = error.is_a?(Exception) ? error.message : error.to_s
222
+ error_class = error.is_a?(Exception) ? error.class.name : "Error"
223
+
224
+ new(
225
+ command: command,
226
+ exit_code: 1,
227
+ stdout: "",
228
+ stderr: error_message,
229
+ started_at: started_at,
230
+ finished_at: Time.now,
231
+ metadata: { error_class: error_class, failed: true },
232
+ )
233
+ end
234
+
235
+ # Create a successful result.
236
+ #
237
+ # @param command [String] the command that succeeded
238
+ # @param stdout [String] standard output
239
+ # @param started_at [Time] when execution started
240
+ # @param duration [Float] execution duration
241
+ # @return [Result] successful result object
242
+ def self.success(command:, stdout: "", started_at: Time.now, duration: nil)
243
+ finished_at = duration ? started_at + duration : Time.now
244
+
245
+ new(
246
+ command: command,
247
+ exit_code: 0,
248
+ stdout: stdout,
249
+ stderr: "",
250
+ started_at: started_at,
251
+ finished_at: finished_at,
252
+ )
253
+ end
254
+
255
+ # Add metadata to the result.
256
+ #
257
+ # @param key [String, Symbol] metadata key
258
+ # @param value [Object] metadata value
259
+ # @return [self] for method chaining
260
+ def add_metadata(key, value)
261
+ @metadata[key] = value
262
+ self
263
+ end
264
+
265
+ # Merge additional metadata.
266
+ #
267
+ # @param additional_metadata [Hash] metadata to merge
268
+ # @return [self] for method chaining
269
+ def merge_metadata(additional_metadata)
270
+ @metadata.merge!(additional_metadata)
271
+ self
272
+ end
273
+
274
+ # Get execution summary for logging/display.
275
+ #
276
+ # @return [String] human-readable execution summary
277
+ def summary
278
+ status = success? ? "✓" : "✗"
279
+ duration_text = duration ? " (#{format("%.2f", duration)}s)" : ""
280
+
281
+ "#{status} #{command}#{duration_text}"
282
+ end
283
+
284
+ # Get detailed execution information.
285
+ #
286
+ # @return [String] detailed execution information
287
+ def details
288
+ lines = []
289
+ lines << "Command: #{command}"
290
+ lines << "Exit Code: #{exit_code}"
291
+ lines << "Started: #{started_at}"
292
+ lines << "Finished: #{finished_at}" if finished_at
293
+ lines << "Duration: #{format("%.2f", duration)}s" if duration
294
+ lines << "Status: #{success? ? "Success" : "Failed"}"
295
+
296
+ unless stdout.empty?
297
+ lines << "STDOUT:"
298
+ lines << stdout.split("\n").map { |line| " #{line}" }
299
+ end
300
+
301
+ unless stderr.empty?
302
+ lines << "STDERR:"
303
+ lines << stderr.split("\n").map { |line| " #{line}" }
304
+ end
305
+
306
+ lines << "Metadata: #{metadata}" unless metadata.empty?
307
+
308
+ lines.flatten.join("\n")
309
+ end
310
+
311
+ private
312
+
313
+ # Calculate execution duration.
314
+ #
315
+ # @return [Float, nil] duration in seconds, or nil if not finished
316
+ def calculate_duration
317
+ return nil unless started_at && finished_at
318
+
319
+ finished_at - started_at
320
+ end
321
+ end
322
+ end
323
+ end
@@ -0,0 +1,317 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "request"
4
+ require_relative "result"
5
+ require_relative "strategies/synchronous"
6
+ require_relative "middleware/base"
7
+ require_relative "middleware/unified_logger"
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 = Makit::Logging.current_log_level
45
+ if @default.nil? || @last_log_level != current_log_level
46
+ @last_log_level = current_log_level
47
+ @default = new(
48
+ middleware: [
49
+ Middleware::CommandLogger.new(
50
+ log_stdout: true,
51
+ log_stderr: true,
52
+ log_performance: true,
53
+ max_output_lines: 50,
54
+ ),
55
+ ],
56
+ )
57
+ end
58
+ @default
59
+ end
60
+
61
+ # Initialize a new command runner.
62
+ #
63
+ # @param middleware [Array<Middleware::Base>] middleware chain
64
+ # @param strategy [Strategies::Base] execution strategy
65
+ # @param options [Hash] additional configuration
66
+ def initialize(middleware: nil, strategy: nil, **options)
67
+ @middleware = Array(middleware || default_middleware)
68
+ @strategy = strategy || Strategies::Synchronous.new
69
+ @options = options
70
+
71
+ validate_middleware
72
+ validate_strategy
73
+
74
+ # Log runner initialization
75
+ log_runner_initialization
76
+ end
77
+
78
+ # Execute a single command request.
79
+ #
80
+ # @param request [Request, String, Hash] command request to execute
81
+ # @return [Result] execution result
82
+ # @raise [ArgumentError] if request is invalid
83
+ def execute(request)
84
+ # Normalize request to Request object
85
+ normalized_request = normalize_request(request)
86
+
87
+ # Apply middleware chain
88
+ execute_with_middleware(normalized_request) do |processed_request|
89
+ @strategy.execute(processed_request)
90
+ end
91
+ end
92
+
93
+ # Execute multiple command requests.
94
+ #
95
+ # @param requests [Array<Request, String, Hash>] requests to execute
96
+ # @return [Array<Result>] execution results
97
+ def execute_batch(requests)
98
+ # Normalize all requests
99
+ normalized_requests = requests.map { |req| normalize_request(req) }
100
+
101
+ # Use strategy's batch execution if available, otherwise execute individually
102
+ if @strategy.respond_to?(:execute_batch)
103
+ # Apply middleware to the entire batch
104
+ execute_with_middleware_batch(normalized_requests) do |processed_requests|
105
+ @strategy.execute_batch(processed_requests)
106
+ end
107
+ else
108
+ # Execute each request individually with middleware
109
+ normalized_requests.map { |request| execute(request) }
110
+ end
111
+ end
112
+
113
+ # Add middleware to the execution chain.
114
+ #
115
+ # @param middleware_instance [Middleware::Base] middleware to add
116
+ # @return [self] for method chaining
117
+ def add_middleware(middleware_instance)
118
+ validate_middleware_instance(middleware_instance)
119
+ @middleware << middleware_instance
120
+ self
121
+ end
122
+
123
+ # Remove middleware from the execution chain.
124
+ #
125
+ # @param middleware_class [Class] middleware class to remove
126
+ # @return [self] for method chaining
127
+ def remove_middleware(middleware_class)
128
+ @middleware.reject! { |m| m.is_a?(middleware_class) }
129
+ self
130
+ end
131
+
132
+ # Check if specific middleware is present.
133
+ #
134
+ # @param middleware_class [Class] middleware class to check
135
+ # @return [Boolean] true if middleware is present
136
+ def has_middleware?(middleware_class)
137
+ @middleware.any? { |m| m.is_a?(middleware_class) }
138
+ end
139
+
140
+ # Get runner configuration for debugging.
141
+ #
142
+ # @return [Hash] runner configuration
143
+ def config
144
+ {
145
+ middleware: @middleware.map(&:config),
146
+ strategy: @strategy.config,
147
+ options: @options,
148
+ }
149
+ end
150
+
151
+ # Get execution statistics.
152
+ #
153
+ # @return [Hash] execution statistics
154
+ def stats
155
+ @stats ||= {
156
+ total_executions: 0,
157
+ successful_executions: 0,
158
+ failed_executions: 0,
159
+ total_duration: 0.0,
160
+ }
161
+ end
162
+
163
+ private
164
+
165
+ # Execute request with middleware chain.
166
+ #
167
+ # @param request [Request] normalized request
168
+ # @yield [Request] yields processed request to execution
169
+ # @return [Result] execution result
170
+ def execute_with_middleware(request, &block)
171
+ # Build middleware chain
172
+ chain = build_middleware_chain(request)
173
+
174
+ # Execute chain
175
+ result = chain.call(request, &block)
176
+
177
+ # Update statistics
178
+ update_stats(result)
179
+
180
+ result
181
+ end
182
+
183
+ # Execute batch with middleware chain.
184
+ #
185
+ # @param requests [Array<Request>] normalized requests
186
+ # @yield [Array<Request>] yields processed requests to execution
187
+ # @return [Array<Result>] execution results
188
+ def execute_with_middleware_batch(requests, &block)
189
+ # For batch execution, apply middleware to each request individually
190
+ # This maintains the middleware contract while supporting batch execution
191
+ results = requests.map do |request|
192
+ execute_with_middleware(request) { |req| [req] }
193
+ end.flatten
194
+
195
+ # Then execute the batch
196
+ processed_requests = results.map(&:command).map { |cmd| Request.from_string(cmd) }
197
+ batch_results = block.call(processed_requests)
198
+
199
+ # Update statistics for batch
200
+ batch_results.each { |result| update_stats(result) }
201
+
202
+ batch_results
203
+ end
204
+
205
+ # Build middleware execution chain.
206
+ #
207
+ # @param request [Request] the request to process
208
+ # @return [Proc] middleware chain
209
+ def build_middleware_chain(request)
210
+ # Filter middleware that applies to this request
211
+ applicable_middleware = @middleware.select { |m| m.applicable?(request) }
212
+
213
+ # Build chain in reverse order so first middleware wraps everything
214
+ applicable_middleware.reverse.reduce(method(:execute_final)) do |chain, middleware|
215
+ ->(req) { middleware.call(req, &chain) }
216
+ end
217
+ end
218
+
219
+ # Final execution step (after all middleware).
220
+ #
221
+ # @param request [Request] processed request
222
+ # @return [Result] execution result
223
+ def execute_final(request)
224
+ @strategy.execute(request)
225
+ end
226
+
227
+ # Normalize various input types to Request objects.
228
+ #
229
+ # @param request [Request, String, Hash] request in various formats
230
+ # @return [Request] normalized request
231
+ # @raise [ArgumentError] if request cannot be normalized
232
+ def normalize_request(request)
233
+ case request
234
+ when Request
235
+ request
236
+ when String
237
+ Request.from_string(request)
238
+ when Hash
239
+ Request.from_hash(request)
240
+ else
241
+ raise ArgumentError, "Invalid request type: #{request.class}. Expected Request, String, or Hash."
242
+ end
243
+ end
244
+
245
+ # Update execution statistics.
246
+ #
247
+ # @param result [Result] execution result
248
+ def update_stats(result)
249
+ stats[:total_executions] += 1
250
+
251
+ if result.success?
252
+ stats[:successful_executions] += 1
253
+ else
254
+ stats[:failed_executions] += 1
255
+ end
256
+
257
+ stats[:total_duration] += result.duration if result.duration
258
+ end
259
+
260
+ # Get default middleware chain.
261
+ #
262
+ # @return [Array<Middleware::Base>] default middleware
263
+ def default_middleware
264
+ # No default middleware for now - keep it simple
265
+ # Subclasses or configuration can add middleware as needed
266
+ []
267
+ end
268
+
269
+ # Validate middleware array.
270
+ def validate_middleware
271
+ @middleware.each { |m| validate_middleware_instance(m) }
272
+ end
273
+
274
+ # Validate individual middleware instance.
275
+ #
276
+ # @param middleware [Object] middleware to validate
277
+ # @raise [ArgumentError] if middleware is invalid
278
+ def validate_middleware_instance(middleware)
279
+ unless middleware.respond_to?(:call)
280
+ raise ArgumentError, "Middleware must respond to #call: #{middleware.class}"
281
+ end
282
+
283
+ return if middleware.respond_to?(:applicable?)
284
+
285
+ raise ArgumentError, "Middleware must respond to #applicable?: #{middleware.class}"
286
+ end
287
+
288
+ # Validate execution strategy.
289
+ #
290
+ # @raise [ArgumentError] if strategy is invalid
291
+ def validate_strategy
292
+ unless @strategy.respond_to?(:execute)
293
+ raise ArgumentError, "Strategy must respond to #execute: #{@strategy.class}"
294
+ end
295
+
296
+ return if @strategy.respond_to?(:supports?)
297
+
298
+ raise ArgumentError, "Strategy must respond to #supports?: #{@strategy.class}"
299
+ end
300
+
301
+ # Log runner initialization for debugging.
302
+ #
303
+ # @return [void]
304
+ def log_runner_initialization
305
+ # Only log if we have a unified logger middleware
306
+ logger_middleware = @middleware.find { |m| m.is_a?(Middleware::UnifiedLogger) }
307
+ return unless logger_middleware
308
+
309
+ logger_middleware.logger.debug("Command runner initialized",
310
+ middleware_count: @middleware.length,
311
+ strategy: @strategy.class.name,
312
+ working_directory: Dir.pwd,
313
+ options: @options)
314
+ end
315
+ end
316
+ end
317
+ end