makit 0.0.140 → 0.0.142

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