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,323 +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 == 0
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
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 == 0
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