makit 0.0.138 → 0.0.140

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 (151) 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/cli/build_commands.rb +500 -500
  7. data/lib/makit/cli/generators/base_generator.rb +74 -74
  8. data/lib/makit/cli/generators/dotnet_generator.rb +50 -50
  9. data/lib/makit/cli/generators/generator_factory.rb +49 -49
  10. data/lib/makit/cli/generators/node_generator.rb +50 -50
  11. data/lib/makit/cli/generators/ruby_generator.rb +77 -77
  12. data/lib/makit/cli/generators/rust_generator.rb +50 -50
  13. data/lib/makit/cli/generators/templates/dotnet_templates.rb +167 -167
  14. data/lib/makit/cli/generators/templates/node_templates.rb +161 -161
  15. data/lib/makit/cli/generators/templates/ruby/gemfile.rb +26 -26
  16. data/lib/makit/cli/generators/templates/ruby/gemspec.rb +40 -40
  17. data/lib/makit/cli/generators/templates/ruby/main_lib.rb +33 -33
  18. data/lib/makit/cli/generators/templates/ruby/rakefile.rb +35 -35
  19. data/lib/makit/cli/generators/templates/ruby/readme.rb +63 -63
  20. data/lib/makit/cli/generators/templates/ruby/test.rb +39 -39
  21. data/lib/makit/cli/generators/templates/ruby/test_helper.rb +29 -29
  22. data/lib/makit/cli/generators/templates/ruby/version.rb +29 -29
  23. data/lib/makit/cli/generators/templates/rust_templates.rb +128 -128
  24. data/lib/makit/cli/main.rb +69 -67
  25. data/lib/makit/cli/project_commands.rb +868 -868
  26. data/lib/makit/cli/repository_commands.rb +661 -661
  27. data/lib/makit/cli/strategy_commands.rb +51 -0
  28. data/lib/makit/cli/utility_commands.rb +521 -521
  29. data/lib/makit/commands/factory.rb +359 -359
  30. data/lib/makit/commands/middleware/base.rb +73 -73
  31. data/lib/makit/commands/middleware/cache.rb +248 -248
  32. data/lib/makit/commands/middleware/command_logger.rb +312 -312
  33. data/lib/makit/commands/middleware/validator.rb +269 -269
  34. data/lib/makit/commands/request.rb +316 -316
  35. data/lib/makit/commands/result.rb +323 -323
  36. data/lib/makit/commands/runner.rb +385 -385
  37. data/lib/makit/commands/strategies/base.rb +171 -171
  38. data/lib/makit/commands/strategies/child_process.rb +1 -1
  39. data/lib/makit/commands/strategies/synchronous.rb +139 -139
  40. data/lib/makit/commands.rb +50 -50
  41. data/lib/makit/configuration/dotnet_project.rb +12 -12
  42. data/lib/makit/configuration/gitlab_helper.rb +58 -58
  43. data/lib/makit/configuration/project.rb +168 -168
  44. data/lib/makit/configuration/rakefile_helper.rb +43 -43
  45. data/lib/makit/configuration/step.rb +34 -34
  46. data/lib/makit/configuration/timeout.rb +74 -0
  47. data/lib/makit/configuration.rb +15 -14
  48. data/lib/makit/content/default_gitignore.rb +7 -7
  49. data/lib/makit/content/default_gitignore.txt +225 -225
  50. data/lib/makit/content/default_rakefile.rb +13 -13
  51. data/lib/makit/content/gem_rakefile.rb +16 -16
  52. data/lib/makit/context.rb +1 -1
  53. data/lib/makit/data.rb +49 -49
  54. data/lib/makit/directories.rb +140 -140
  55. data/lib/makit/directory.rb +262 -262
  56. data/lib/makit/docs/files.rb +89 -89
  57. data/lib/makit/docs/rake.rb +102 -102
  58. data/lib/makit/dotnet/cli.rb +69 -69
  59. data/lib/makit/dotnet/project.rb +217 -217
  60. data/lib/makit/dotnet/solution.rb +38 -38
  61. data/lib/makit/dotnet/solution_classlib.rb +239 -239
  62. data/lib/makit/dotnet/solution_console.rb +264 -264
  63. data/lib/makit/dotnet/solution_maui.rb +354 -354
  64. data/lib/makit/dotnet/solution_wasm.rb +275 -275
  65. data/lib/makit/dotnet/solution_wpf.rb +304 -304
  66. data/lib/makit/dotnet.rb +102 -102
  67. data/lib/makit/email.rb +90 -90
  68. data/lib/makit/environment.rb +142 -142
  69. data/lib/makit/examples/runner.rb +370 -370
  70. data/lib/makit/exceptions.rb +45 -45
  71. data/lib/makit/fileinfo.rb +24 -24
  72. data/lib/makit/files.rb +43 -43
  73. data/lib/makit/gems.rb +40 -40
  74. data/lib/makit/git/cli.rb +54 -54
  75. data/lib/makit/git/repository.rb +90 -90
  76. data/lib/makit/git.rb +98 -98
  77. data/lib/makit/gitlab_runner.rb +59 -59
  78. data/lib/makit/humanize.rb +137 -137
  79. data/lib/makit/indexer.rb +47 -47
  80. data/lib/makit/logging/configuration.rb +308 -308
  81. data/lib/makit/logging/format_registry.rb +84 -84
  82. data/lib/makit/logging/formatters/base.rb +39 -39
  83. data/lib/makit/logging/formatters/console_formatter.rb +140 -140
  84. data/lib/makit/logging/formatters/json_formatter.rb +65 -65
  85. data/lib/makit/logging/formatters/plain_text_formatter.rb +71 -71
  86. data/lib/makit/logging/formatters/text_formatter.rb +64 -64
  87. data/lib/makit/logging/log_request.rb +119 -119
  88. data/lib/makit/logging/logger.rb +199 -199
  89. data/lib/makit/logging/sinks/base.rb +91 -91
  90. data/lib/makit/logging/sinks/console.rb +72 -72
  91. data/lib/makit/logging/sinks/file_sink.rb +92 -92
  92. data/lib/makit/logging/sinks/structured.rb +123 -123
  93. data/lib/makit/logging/sinks/unified_file_sink.rb +296 -296
  94. data/lib/makit/logging.rb +565 -565
  95. data/lib/makit/markdown.rb +75 -75
  96. data/lib/makit/mp/basic_object_mp.rb +17 -17
  97. data/lib/makit/mp/command_mp.rb +13 -13
  98. data/lib/makit/mp/command_request.mp.rb +17 -17
  99. data/lib/makit/mp/project_mp.rb +199 -199
  100. data/lib/makit/mp/string_mp.rb +191 -191
  101. data/lib/makit/nuget.rb +74 -74
  102. data/lib/makit/port.rb +32 -32
  103. data/lib/makit/process.rb +163 -163
  104. data/lib/makit/protoc.rb +107 -107
  105. data/lib/makit/rake/cli.rb +196 -196
  106. data/lib/makit/rake.rb +80 -80
  107. data/lib/makit/ruby/cli.rb +185 -185
  108. data/lib/makit/ruby.rb +25 -25
  109. data/lib/makit/secrets.rb +51 -51
  110. data/lib/makit/serializer.rb +130 -130
  111. data/lib/makit/services/builder.rb +186 -186
  112. data/lib/makit/services/error_handler.rb +226 -226
  113. data/lib/makit/services/repository_manager.rb +231 -231
  114. data/lib/makit/services/validator.rb +112 -112
  115. data/lib/makit/setup/classlib.rb +101 -101
  116. data/lib/makit/setup/gem.rb +268 -268
  117. data/lib/makit/setup/razorclasslib.rb +101 -101
  118. data/lib/makit/setup/runner.rb +54 -54
  119. data/lib/makit/setup.rb +5 -5
  120. data/lib/makit/show.rb +110 -110
  121. data/lib/makit/storage.rb +126 -126
  122. data/lib/makit/symbols.rb +170 -170
  123. data/lib/makit/task_info.rb +130 -130
  124. data/lib/makit/tasks/at_exit.rb +15 -15
  125. data/lib/makit/tasks/build.rb +22 -22
  126. data/lib/makit/tasks/clean.rb +13 -13
  127. data/lib/makit/tasks/configure.rb +10 -10
  128. data/lib/makit/tasks/format.rb +10 -10
  129. data/lib/makit/tasks/hook_manager.rb +443 -443
  130. data/lib/makit/tasks/init.rb +49 -49
  131. data/lib/makit/tasks/integrate.rb +29 -29
  132. data/lib/makit/tasks/pull_incoming.rb +13 -13
  133. data/lib/makit/tasks/setup.rb +13 -13
  134. data/lib/makit/tasks/sync.rb +17 -17
  135. data/lib/makit/tasks/tag.rb +16 -16
  136. data/lib/makit/tasks/task_monkey_patch.rb +81 -81
  137. data/lib/makit/tasks/test.rb +22 -22
  138. data/lib/makit/tasks/update.rb +18 -18
  139. data/lib/makit/tasks.rb +20 -20
  140. data/lib/makit/test_cache.rb +239 -239
  141. data/lib/makit/tree.rb +37 -37
  142. data/lib/makit/v1/makit.v1_pb.rb +35 -35
  143. data/lib/makit/v1/makit.v1_services_pb.rb +27 -27
  144. data/lib/makit/version.rb +99 -99
  145. data/lib/makit/version_util.rb +21 -21
  146. data/lib/makit/wix.rb +95 -95
  147. data/lib/makit/yaml.rb +29 -29
  148. data/lib/makit/zip.rb +17 -17
  149. data/lib/makit copy.rb +44 -44
  150. data/lib/makit.rb +42 -42
  151. metadata +3 -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.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
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