makit 0.0.99 → 0.0.112

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 (152) 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 +28 -32
  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 +62 -33
  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/commands/factory.rb +359 -0
  28. data/lib/makit/commands/middleware/base.rb +73 -0
  29. data/lib/makit/commands/middleware/cache.rb +248 -0
  30. data/lib/makit/commands/middleware/command_logger.rb +320 -0
  31. data/lib/makit/commands/middleware/unified_logger.rb +243 -0
  32. data/lib/makit/commands/middleware/validator.rb +269 -0
  33. data/lib/makit/commands/request.rb +254 -0
  34. data/lib/makit/commands/result.rb +323 -0
  35. data/lib/makit/commands/runner.rb +337 -0
  36. data/lib/makit/commands/strategies/base.rb +160 -0
  37. data/lib/makit/commands/strategies/synchronous.rb +134 -0
  38. data/lib/makit/commands.rb +51 -21
  39. data/lib/makit/configuration/gitlab_helper.rb +60 -0
  40. data/lib/makit/configuration/project.rb +127 -0
  41. data/lib/makit/configuration/rakefile_helper.rb +43 -0
  42. data/lib/makit/configuration/step.rb +34 -0
  43. data/lib/makit/configuration.rb +14 -0
  44. data/lib/makit/content/default_gitignore.rb +7 -5
  45. data/lib/makit/content/default_rakefile.rb +13 -11
  46. data/lib/makit/content/gem_rakefile.rb +16 -14
  47. data/lib/makit/context.rb +1 -0
  48. data/lib/makit/data.rb +49 -50
  49. data/lib/makit/directories.rb +141 -145
  50. data/lib/makit/directory.rb +262 -276
  51. data/lib/makit/docs/files.rb +89 -94
  52. data/lib/makit/docs/rake.rb +102 -106
  53. data/lib/makit/dotnet/cli.rb +65 -0
  54. data/lib/makit/dotnet/project.rb +153 -0
  55. data/lib/makit/dotnet/solution.rb +38 -0
  56. data/lib/makit/dotnet/solution_classlib.rb +239 -0
  57. data/lib/makit/dotnet/solution_console.rb +264 -0
  58. data/lib/makit/dotnet/solution_maui.rb +354 -0
  59. data/lib/makit/dotnet/solution_wasm.rb +275 -0
  60. data/lib/makit/dotnet/solution_wpf.rb +304 -0
  61. data/lib/makit/dotnet.rb +102 -219
  62. data/lib/makit/email.rb +90 -61
  63. data/lib/makit/environment.rb +142 -139
  64. data/lib/makit/examples/runner.rb +370 -0
  65. data/lib/makit/exceptions.rb +45 -0
  66. data/lib/makit/fileinfo.rb +24 -26
  67. data/lib/makit/files.rb +43 -47
  68. data/lib/makit/gems.rb +29 -28
  69. data/lib/makit/git/cli.rb +54 -0
  70. data/lib/makit/git/repository.rb +90 -0
  71. data/lib/makit/git.rb +98 -145
  72. data/lib/makit/gitlab_runner.rb +59 -60
  73. data/lib/makit/humanize.rb +137 -129
  74. data/lib/makit/indexer.rb +47 -56
  75. data/lib/makit/logging/configuration.rb +305 -0
  76. data/lib/makit/logging/format_registry.rb +84 -0
  77. data/lib/makit/logging/formatters/base.rb +39 -0
  78. data/lib/makit/logging/formatters/console_formatter.rb +140 -0
  79. data/lib/makit/logging/formatters/json_formatter.rb +65 -0
  80. data/lib/makit/logging/formatters/plain_text_formatter.rb +71 -0
  81. data/lib/makit/logging/formatters/text_formatter.rb +64 -0
  82. data/lib/makit/logging/log_request.rb +115 -0
  83. data/lib/makit/logging/logger.rb +163 -0
  84. data/lib/makit/logging/sinks/base.rb +91 -0
  85. data/lib/makit/logging/sinks/console.rb +72 -0
  86. data/lib/makit/logging/sinks/file_sink.rb +92 -0
  87. data/lib/makit/logging/sinks/structured.rb +129 -0
  88. data/lib/makit/logging/sinks/unified_file_sink.rb +303 -0
  89. data/lib/makit/logging.rb +530 -106
  90. data/lib/makit/markdown.rb +75 -75
  91. data/lib/makit/mp/basic_object_mp.rb +17 -16
  92. data/lib/makit/mp/command_mp.rb +13 -13
  93. data/lib/makit/mp/command_request.mp.rb +17 -16
  94. data/lib/makit/mp/project_mp.rb +199 -210
  95. data/lib/makit/mp/string_mp.rb +193 -176
  96. data/lib/makit/nuget.rb +74 -72
  97. data/lib/makit/port.rb +32 -34
  98. data/lib/makit/process.rb +163 -65
  99. data/lib/makit/protoc.rb +107 -104
  100. data/lib/makit/rake/cli.rb +196 -0
  101. data/lib/makit/rake.rb +25 -25
  102. data/lib/makit/ruby/cli.rb +185 -0
  103. data/lib/makit/ruby.rb +25 -0
  104. data/lib/makit/secrets.rb +51 -51
  105. data/lib/makit/serializer.rb +130 -115
  106. data/lib/makit/services/builder.rb +186 -0
  107. data/lib/makit/services/error_handler.rb +226 -0
  108. data/lib/makit/services/repository_manager.rb +229 -0
  109. data/lib/makit/services/validator.rb +112 -0
  110. data/lib/makit/setup/classlib.rb +53 -0
  111. data/lib/makit/setup/gem.rb +263 -0
  112. data/lib/makit/setup/runner.rb +45 -0
  113. data/lib/makit/setup.rb +5 -0
  114. data/lib/makit/show.rb +110 -110
  115. data/lib/makit/storage.rb +126 -131
  116. data/lib/makit/symbols.rb +170 -149
  117. data/lib/makit/task_info.rb +128 -86
  118. data/lib/makit/tasks/at_exit.rb +13 -0
  119. data/lib/makit/tasks/build.rb +19 -0
  120. data/lib/makit/tasks/clean.rb +11 -0
  121. data/lib/makit/tasks/hook_manager.rb +393 -0
  122. data/lib/makit/tasks/init.rb +47 -0
  123. data/lib/makit/tasks/integrate.rb +17 -0
  124. data/lib/makit/tasks/pull_incoming.rb +11 -0
  125. data/lib/makit/tasks/setup.rb +6 -0
  126. data/lib/makit/tasks/sync.rb +12 -0
  127. data/lib/makit/tasks/tag.rb +15 -0
  128. data/lib/makit/tasks/task_monkey_patch.rb +79 -0
  129. data/lib/makit/tasks.rb +15 -150
  130. data/lib/makit/test_cache.rb +239 -0
  131. data/lib/makit/tree.rb +37 -37
  132. data/lib/makit/v1/makit.v1_pb.rb +3 -4
  133. data/lib/makit/v1/makit.v1_services_pb.rb +27 -25
  134. data/lib/makit/version.rb +5 -61
  135. data/lib/makit/version_util.rb +21 -0
  136. data/lib/makit/wix.rb +95 -95
  137. data/lib/makit/yaml.rb +29 -17
  138. data/lib/makit/zip.rb +17 -17
  139. data/lib/makit copy.rb +44 -0
  140. data/lib/makit.rb +40 -267
  141. metadata +117 -110
  142. data/lib/makit/cli/clean.rb +0 -14
  143. data/lib/makit/cli/clone.rb +0 -59
  144. data/lib/makit/cli/init.rb +0 -38
  145. data/lib/makit/cli/make.rb +0 -54
  146. data/lib/makit/cli/new.rb +0 -37
  147. data/lib/makit/cli/nuget_cache.rb +0 -38
  148. data/lib/makit/cli/pull.rb +0 -31
  149. data/lib/makit/cli/setup.rb +0 -71
  150. data/lib/makit/cli/work.rb +0 -21
  151. data/lib/makit/command_runner.rb +0 -404
  152. data/lib/makit/content/default_gitignore.txt +0 -222
@@ -0,0 +1,359 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "request"
4
+
5
+ module Makit
6
+ module Commands
7
+ # Factory for creating common command requests.
8
+ #
9
+ # This factory provides convenient methods for creating Request objects
10
+ # for common operations like git commands, bundle operations, rake tasks,
11
+ # and system utilities.
12
+ #
13
+ # @example Creating git commands
14
+ # request = Factory.git_clone("https://github.com/user/repo.git", "/path/to/clone")
15
+ # request = Factory.git_status("/path/to/repo")
16
+ #
17
+ # @example Creating Ruby commands
18
+ # request = Factory.bundle_install("/path/to/project")
19
+ # request = Factory.rake_task("test", "/path/to/project")
20
+ class Factory
21
+ # Create a git clone request.
22
+ #
23
+ # @param repository_url [String] git repository URL
24
+ # @param target_directory [String, nil] target directory (optional)
25
+ # @param options [Hash] additional options
26
+ # @option options [Integer] :timeout (300) timeout in seconds
27
+ # @option options [Hash] :metadata additional metadata
28
+ # @return [Request] git clone request
29
+ def self.git_clone(repository_url, target_directory = nil, **options)
30
+ arguments = ["clone", repository_url]
31
+ arguments << target_directory if target_directory
32
+
33
+ Request.new(
34
+ command: "git",
35
+ arguments: arguments,
36
+ timeout: options.fetch(:timeout, 300),
37
+ metadata: {
38
+ operation: "git_clone",
39
+ repository: repository_url,
40
+ target: target_directory,
41
+ }.merge(options.fetch(:metadata, {})),
42
+ )
43
+ end
44
+
45
+ # Create a git pull request.
46
+ #
47
+ # @param directory [String] repository directory
48
+ # @param options [Hash] additional options
49
+ # @option options [String] :remote ("origin") remote name
50
+ # @option options [String] :branch (nil) specific branch
51
+ # @option options [Integer] :timeout (120) timeout in seconds
52
+ # @return [Request] git pull request
53
+ def self.git_pull(directory = Dir.pwd, **options)
54
+ arguments = ["pull"]
55
+ arguments << options[:remote] if options[:remote]
56
+ arguments << options[:branch] if options[:branch]
57
+
58
+ Request.new(
59
+ command: "git",
60
+ arguments: arguments,
61
+ directory: directory,
62
+ timeout: options.fetch(:timeout, 120),
63
+ metadata: {
64
+ operation: "git_pull",
65
+ remote: options[:remote] || "origin",
66
+ branch: options[:branch],
67
+ }.merge(options.fetch(:metadata, {})),
68
+ )
69
+ end
70
+
71
+ # Create a git status request.
72
+ #
73
+ # @param directory [String] repository directory
74
+ # @param options [Hash] additional options
75
+ # @option options [Boolean] :porcelain (false) use porcelain format
76
+ # @option options [Boolean] :short (false) use short format
77
+ # @return [Request] git status request
78
+ def self.git_status(directory = Dir.pwd, **options)
79
+ arguments = ["status"]
80
+ arguments << "--porcelain" if options[:porcelain]
81
+ arguments << "--short" if options[:short]
82
+
83
+ Request.new(
84
+ command: "git",
85
+ arguments: arguments,
86
+ directory: directory,
87
+ metadata: {
88
+ operation: "git_status",
89
+ porcelain: options[:porcelain],
90
+ short: options[:short],
91
+ }.merge(options.fetch(:metadata, {})),
92
+ )
93
+ end
94
+
95
+ # Create a git log request.
96
+ #
97
+ # @param directory [String] repository directory
98
+ # @param options [Hash] additional options
99
+ # @option options [Integer] :limit (nil) limit number of commits
100
+ # @option options [String] :format (nil) log format
101
+ # @option options [String] :since (nil) since date/commit
102
+ # @return [Request] git log request
103
+ def self.git_log(directory = Dir.pwd, **options)
104
+ arguments = ["log"]
105
+ arguments.push("-n", options[:limit].to_s) if options[:limit]
106
+ arguments.push("--format", options[:format]) if options[:format]
107
+ arguments.push("--since", options[:since]) if options[:since]
108
+
109
+ Request.new(
110
+ command: "git",
111
+ arguments: arguments,
112
+ directory: directory,
113
+ metadata: {
114
+ operation: "git_log",
115
+ limit: options[:limit],
116
+ format: options[:format],
117
+ since: options[:since],
118
+ }.merge(options.fetch(:metadata, {})),
119
+ )
120
+ end
121
+
122
+ # Create a bundle install request.
123
+ #
124
+ # @param directory [String] project directory
125
+ # @param options [Hash] additional options
126
+ # @option options [Integer] :jobs (nil) number of parallel jobs
127
+ # @option options [String] :path (nil) install path
128
+ # @option options [Boolean] :deployment (false) deployment mode
129
+ # @option options [Integer] :timeout (300) timeout in seconds
130
+ # @return [Request] bundle install request
131
+ def self.bundle_install(directory = Dir.pwd, **options)
132
+ arguments = ["install"]
133
+ arguments.push("--jobs", options[:jobs].to_s) if options[:jobs]
134
+ arguments.push("--path", options[:path]) if options[:path]
135
+ arguments << "--deployment" if options[:deployment]
136
+
137
+ Request.new(
138
+ command: "bundle",
139
+ arguments: arguments,
140
+ directory: directory,
141
+ timeout: options.fetch(:timeout, 300),
142
+ metadata: {
143
+ operation: "bundle_install",
144
+ jobs: options[:jobs],
145
+ path: options[:path],
146
+ deployment: options[:deployment],
147
+ }.merge(options.fetch(:metadata, {})),
148
+ )
149
+ end
150
+
151
+ # Create a bundle exec request.
152
+ #
153
+ # @param command_string [String] command to execute with bundle exec
154
+ # @param directory [String] project directory
155
+ # @param options [Hash] additional options
156
+ # @option options [Integer] :timeout (120) timeout in seconds
157
+ # @return [Request] bundle exec request
158
+ def self.bundle_exec(command_string, directory = Dir.pwd, **options)
159
+ # Parse the command string to get command and arguments
160
+ parts = command_string.strip.split(/\s+/)
161
+ arguments = ["exec"] + parts
162
+
163
+ Request.new(
164
+ command: "bundle",
165
+ arguments: arguments,
166
+ directory: directory,
167
+ timeout: options.fetch(:timeout, 120),
168
+ metadata: {
169
+ operation: "bundle_exec",
170
+ exec_command: command_string,
171
+ }.merge(options.fetch(:metadata, {})),
172
+ )
173
+ end
174
+
175
+ # Create a rake task request.
176
+ #
177
+ # @param task_name [String] rake task name
178
+ # @param directory [String] project directory
179
+ # @param options [Hash] additional options
180
+ # @option options [Array<String>] :arguments additional task arguments
181
+ # @option options [Hash] :environment environment variables
182
+ # @option options [Integer] :timeout (300) timeout in seconds
183
+ # @return [Request] rake task request
184
+ def self.rake_task(task_name, directory = Dir.pwd, **options)
185
+ arguments = [task_name]
186
+ arguments.concat(options[:arguments]) if options[:arguments]
187
+
188
+ Request.new(
189
+ command: "rake",
190
+ arguments: arguments,
191
+ directory: directory,
192
+ environment: options[:environment] || {},
193
+ timeout: options.fetch(:timeout, 300),
194
+ metadata: {
195
+ operation: "rake_task",
196
+ task: task_name,
197
+ arguments: options[:arguments],
198
+ }.merge(options.fetch(:metadata, {})),
199
+ )
200
+ end
201
+
202
+ # Create a gem build request.
203
+ #
204
+ # @param gemspec_file [String, nil] gemspec file (optional, will auto-detect)
205
+ # @param directory [String] project directory
206
+ # @param options [Hash] additional options
207
+ # @option options [Integer] :timeout (120) timeout in seconds
208
+ # @return [Request] gem build request
209
+ def self.gem_build(gemspec_file = nil, directory = Dir.pwd, **options)
210
+ arguments = ["build"]
211
+ arguments << gemspec_file if gemspec_file
212
+
213
+ Request.new(
214
+ command: "gem",
215
+ arguments: arguments,
216
+ directory: directory,
217
+ timeout: options.fetch(:timeout, 120),
218
+ metadata: {
219
+ operation: "gem_build",
220
+ gemspec: gemspec_file,
221
+ }.merge(options.fetch(:metadata, {})),
222
+ )
223
+ end
224
+
225
+ # Create a gem install request.
226
+ #
227
+ # @param gem_name [String] gem name or gem file
228
+ # @param options [Hash] additional options
229
+ # @option options [String] :version specific version
230
+ # @option options [Boolean] :local (false) install from local file
231
+ # @option options [String] :source gem source
232
+ # @option options [Integer] :timeout (180) timeout in seconds
233
+ # @return [Request] gem install request
234
+ def self.gem_install(gem_name, **options)
235
+ arguments = ["install", gem_name]
236
+ arguments.push("--version", options[:version]) if options[:version]
237
+ arguments << "--local" if options[:local]
238
+ arguments.push("--source", options[:source]) if options[:source]
239
+
240
+ Request.new(
241
+ command: "gem",
242
+ arguments: arguments,
243
+ timeout: options.fetch(:timeout, 180),
244
+ metadata: {
245
+ operation: "gem_install",
246
+ gem: gem_name,
247
+ version: options[:version],
248
+ local: options[:local],
249
+ source: options[:source],
250
+ }.merge(options.fetch(:metadata, {})),
251
+ )
252
+ end
253
+
254
+ # Create a Ruby script execution request.
255
+ #
256
+ # @param script_file [String] Ruby script file to execute
257
+ # @param directory [String] working directory
258
+ # @param options [Hash] additional options
259
+ # @option options [Array<String>] :arguments script arguments
260
+ # @option options [Hash] :environment environment variables
261
+ # @option options [Integer] :timeout (120) timeout in seconds
262
+ # @return [Request] Ruby script execution request
263
+ def self.ruby_script(script_file, directory = Dir.pwd, **options)
264
+ arguments = [script_file]
265
+ arguments.concat(options[:arguments]) if options[:arguments]
266
+
267
+ Request.new(
268
+ command: "ruby",
269
+ arguments: arguments,
270
+ directory: directory,
271
+ environment: options[:environment] || {},
272
+ timeout: options.fetch(:timeout, 120),
273
+ metadata: {
274
+ operation: "ruby_script",
275
+ script: script_file,
276
+ arguments: options[:arguments],
277
+ }.merge(options.fetch(:metadata, {})),
278
+ )
279
+ end
280
+
281
+ # Create a system command request with validation.
282
+ #
283
+ # @param command [String] system command
284
+ # @param arguments [Array<String>] command arguments
285
+ # @param options [Hash] additional options
286
+ # @option options [String] :directory working directory
287
+ # @option options [Hash] :environment environment variables
288
+ # @option options [Integer] :timeout (60) timeout in seconds
289
+ # @return [Request] system command request
290
+ def self.system_command(command, arguments = [], **options)
291
+ Request.new(
292
+ command: command,
293
+ arguments: Array(arguments),
294
+ directory: options[:directory] || Dir.pwd,
295
+ environment: options[:environment] || {},
296
+ timeout: options.fetch(:timeout, 60),
297
+ metadata: {
298
+ operation: "system_command",
299
+ system_command: command,
300
+ }.merge(options.fetch(:metadata, {})),
301
+ )
302
+ end
303
+
304
+ # Create a dotnet CLI request.
305
+ #
306
+ # @param subcommand [String] dotnet subcommand (build, test, run, etc.)
307
+ # @param directory [String] project directory
308
+ # @param options [Hash] additional options
309
+ # @option options [Array<String>] :arguments additional arguments
310
+ # @option options [String] :configuration (nil) build configuration
311
+ # @option options [String] :framework (nil) target framework
312
+ # @option options [Integer] :timeout (300) timeout in seconds
313
+ # @return [Request] dotnet CLI request
314
+ def self.dotnet_command(subcommand, directory = Dir.pwd, **options)
315
+ arguments = [subcommand]
316
+ arguments.concat(options[:arguments]) if options[:arguments]
317
+ arguments.push("--configuration", options[:configuration]) if options[:configuration]
318
+ arguments.push("--framework", options[:framework]) if options[:framework]
319
+
320
+ Request.new(
321
+ command: "dotnet",
322
+ arguments: arguments,
323
+ directory: directory,
324
+ timeout: options.fetch(:timeout, 300),
325
+ metadata: {
326
+ operation: "dotnet_#{subcommand}",
327
+ subcommand: subcommand,
328
+ configuration: options[:configuration],
329
+ framework: options[:framework],
330
+ }.merge(options.fetch(:metadata, {})),
331
+ )
332
+ end
333
+
334
+ # Create a Node.js/npm command request.
335
+ #
336
+ # @param subcommand [String] npm subcommand (install, test, run, etc.)
337
+ # @param directory [String] project directory
338
+ # @param options [Hash] additional options
339
+ # @option options [Array<String>] :arguments additional arguments
340
+ # @option options [Integer] :timeout (300) timeout in seconds
341
+ # @return [Request] npm command request
342
+ def self.npm_command(subcommand, directory = Dir.pwd, **options)
343
+ arguments = [subcommand]
344
+ arguments.concat(options[:arguments]) if options[:arguments]
345
+
346
+ Request.new(
347
+ command: "npm",
348
+ arguments: arguments,
349
+ directory: directory,
350
+ timeout: options.fetch(:timeout, 300),
351
+ metadata: {
352
+ operation: "npm_#{subcommand}",
353
+ subcommand: subcommand,
354
+ }.merge(options.fetch(:metadata, {})),
355
+ )
356
+ end
357
+ end
358
+ end
359
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Makit
4
+ module Commands
5
+ module Middleware
6
+ # Base class for all command execution middleware.
7
+ #
8
+ # Middleware provides a way to add cross-cutting concerns like logging,
9
+ # caching, validation, and timing to command execution without modifying
10
+ # the core execution logic.
11
+ #
12
+ # @example Creating custom middleware
13
+ # class CustomMiddleware < Base
14
+ # def call(request, &block)
15
+ # # Pre-execution logic
16
+ # puts "About to execute: #{request.command}"
17
+ #
18
+ # # Execute the command
19
+ # result = block.call(request)
20
+ #
21
+ # # Post-execution logic
22
+ # puts "Execution completed: #{result.success? ? 'SUCCESS' : 'FAILED'}"
23
+ #
24
+ # result
25
+ # end
26
+ # end
27
+ class Base
28
+ # Execute middleware logic.
29
+ #
30
+ # This method must be implemented by subclasses to provide the actual
31
+ # middleware functionality. The pattern is to perform any pre-execution
32
+ # logic, call the block to continue the middleware chain, then perform
33
+ # any post-execution logic.
34
+ #
35
+ # @param request [Request] the command request to process
36
+ # @yield [Request] yields the processed request to the next middleware
37
+ # @yieldreturn [Result] the execution result
38
+ # @return [Result] the final execution result
39
+ # @raise [NotImplementedError] if not overridden by subclass
40
+ def call(request, &block)
41
+ raise NotImplementedError, "#{self.class.name} must implement #call"
42
+ end
43
+
44
+ # Check if this middleware should be applied to the given request.
45
+ #
46
+ # Override this method to provide conditional middleware application
47
+ # based on request properties.
48
+ #
49
+ # @param request [Request] the command request
50
+ # @return [Boolean] true if middleware should be applied
51
+ def applicable?(_request)
52
+ true
53
+ end
54
+
55
+ # Get middleware name for logging and debugging.
56
+ #
57
+ # @return [String] middleware name
58
+ def name
59
+ self.class.name.split("::").last
60
+ end
61
+
62
+ # Get middleware configuration.
63
+ #
64
+ # Override this method to provide middleware-specific configuration.
65
+ #
66
+ # @return [Hash] middleware configuration
67
+ def config
68
+ {}
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "digest"
5
+
6
+ module Makit
7
+ module Commands
8
+ module Middleware
9
+ # Caching middleware for command execution results
10
+ #
11
+ # This middleware provides caching functionality for command execution,
12
+ # allowing commands to be cached based on their content and timestamp.
13
+ #
14
+ # @example Basic usage
15
+ # runner = Commands::Runner.new(middleware: [Cache.new])
16
+ # result = runner.execute("git --version")
17
+ #
18
+ # @example With custom cache directory
19
+ # cache = Cache.new(cache_dir: "custom/cache")
20
+ # runner = Commands::Runner.new(middleware: [cache])
21
+ class Cache < Base
22
+ # @!attribute [r] cache_dir
23
+ # @return [String] cache directory path
24
+ attr_reader :cache_dir
25
+
26
+ # @!attribute [r] enabled
27
+ # @return [Boolean] whether caching is enabled
28
+ attr_reader :enabled
29
+
30
+ # Initialize caching middleware
31
+ #
32
+ # @param cache_dir [String] directory to store cache files
33
+ # @param enabled [Boolean] whether caching is enabled
34
+ # @param options [Hash] additional options
35
+ def initialize(cache_dir: nil, enabled: true, **options)
36
+ @cache_dir = cache_dir || default_cache_dir
37
+ @enabled = enabled
38
+ @options = options
39
+
40
+ # Ensure cache directory exists
41
+ FileUtils.mkdir_p(@cache_dir) if @enabled
42
+ end
43
+
44
+ # Check if this middleware applies to the given request
45
+ #
46
+ # @param request [Request] command request
47
+ # @return [Boolean] true if caching should be applied
48
+ def applicable?(request)
49
+ @enabled && request.metadata[:cache_key].present?
50
+ end
51
+
52
+ # Execute the middleware chain with caching
53
+ #
54
+ # @param request [Request] command request
55
+ # @yield [Request] yields to next middleware or execution
56
+ # @return [Result] execution result
57
+ def call(request, &block)
58
+ return block.call(request) unless applicable?(request)
59
+
60
+ cache_key = request.metadata[:cache_key]
61
+ cache_timestamp = request.metadata[:cache_timestamp]
62
+ cache_file = get_cache_file(cache_key)
63
+
64
+ # Check if we have a valid cached result
65
+ if cache_file && File.exist?(cache_file) && cache_timestamp
66
+ cached_result = load_cached_result(cache_file, cache_timestamp)
67
+ return cached_result if cached_result
68
+ end
69
+
70
+ # Execute the command
71
+ result = block.call(request)
72
+
73
+ # Cache the result if successful
74
+ cache_result(cache_file, result) if result.success?
75
+
76
+ result
77
+ end
78
+
79
+ # Get middleware configuration
80
+ #
81
+ # @return [Hash] configuration hash
82
+ def config
83
+ {
84
+ cache_dir: @cache_dir,
85
+ enabled: @enabled,
86
+ options: @options,
87
+ }
88
+ end
89
+
90
+ private
91
+
92
+ # Get the default cache directory
93
+ #
94
+ # @return [String] default cache directory path
95
+ def default_cache_dir
96
+ File.join(Makit::Directories::PROJECT_ARTIFACTS, "commands", "cache")
97
+ end
98
+
99
+ # Get cache file path for a given key
100
+ #
101
+ # @param cache_key [String] cache key
102
+ # @return [String] cache file path
103
+ def get_cache_file(cache_key)
104
+ return nil unless cache_key
105
+
106
+ File.join(@cache_dir, "#{cache_key}.pb")
107
+ end
108
+
109
+ # Load cached result if it's newer than the timestamp
110
+ #
111
+ # @param cache_file [String] path to cache file
112
+ # @param timestamp [Time] minimum timestamp for cache validity
113
+ # @return [Result, nil] cached result or nil if not valid
114
+ def load_cached_result(cache_file, timestamp)
115
+ return nil unless File.exist?(cache_file)
116
+
117
+ cache_mtime = File.mtime(cache_file)
118
+ return nil if cache_mtime <= timestamp
119
+
120
+ begin
121
+ # Load the cached command result
122
+ cached_command = Makit::Serializer.open(cache_file, Makit::V1::Command)
123
+
124
+ # Convert to new Result format
125
+ convert_cached_command_to_result(cached_command)
126
+ rescue => e
127
+ Makit::Logging.debug("Failed to load cached result", {
128
+ cache_file: cache_file,
129
+ error: e.message,
130
+ })
131
+ nil
132
+ end
133
+ end
134
+
135
+ # Cache a successful result
136
+ #
137
+ # @param cache_file [String] path to cache file
138
+ # @param result [Result] result to cache
139
+ def cache_result(cache_file, result)
140
+ return unless cache_file && result.success?
141
+
142
+ begin
143
+ # Convert Result to legacy Command format for caching
144
+ legacy_command = convert_result_to_legacy_command(result)
145
+
146
+ # Save to cache
147
+ Makit::Serializer.save_as(cache_file, legacy_command)
148
+
149
+ Makit::Logging.debug("Cached command result", {
150
+ cache_file: cache_file,
151
+ command: result.command,
152
+ success: result.success?,
153
+ })
154
+ rescue => e
155
+ Makit::Logging.warn("Failed to cache result", {
156
+ cache_file: cache_file,
157
+ error: e.message,
158
+ })
159
+ end
160
+ end
161
+
162
+ # Convert cached Command to Result format
163
+ #
164
+ # @param cached_command [Makit::V1::Command] cached command
165
+ # @return [Result] converted result
166
+ def convert_cached_command_to_result(cached_command)
167
+ # Create a Result object from the cached command
168
+ Result.new(
169
+ command: "#{cached_command.name} #{cached_command.arguments.join(" ")}",
170
+ exit_code: cached_command.exit_code,
171
+ stdout: cached_command.output,
172
+ stderr: cached_command.error,
173
+ started_at: convert_protobuf_timestamp_to_time(cached_command.started_at),
174
+ duration: convert_protobuf_duration_to_float(cached_command.duration),
175
+ metadata: {
176
+ cached: true,
177
+ directory: cached_command.directory,
178
+ },
179
+ )
180
+ end
181
+
182
+ # Convert Result to legacy Command format for caching
183
+ #
184
+ # @param result [Result] result to convert
185
+ # @return [Makit::V1::Command] legacy command object
186
+ def convert_result_to_legacy_command(result)
187
+ command_parts = result.command.split
188
+ name = command_parts.first
189
+ arguments = command_parts[1..] || []
190
+
191
+ Makit::V1::Command.new(
192
+ name: name,
193
+ arguments: arguments,
194
+ exit_code: result.exit_code,
195
+ output: result.stdout,
196
+ error: result.stderr,
197
+ started_at: convert_time_to_protobuf_timestamp(result.started_at),
198
+ duration: convert_duration_to_protobuf(result.duration),
199
+ directory: result.metadata[:directory] || Dir.pwd,
200
+ )
201
+ end
202
+
203
+ # Convert protobuf timestamp to Time
204
+ #
205
+ # @param timestamp [Google::Protobuf::Timestamp] protobuf timestamp
206
+ # @return [Time, nil] converted time
207
+ def convert_protobuf_timestamp_to_time(timestamp)
208
+ return nil unless timestamp
209
+
210
+ Time.at(timestamp.seconds, timestamp.nanos, :nsec)
211
+ end
212
+
213
+ # Convert protobuf duration to float seconds
214
+ #
215
+ # @param duration [Google::Protobuf::Duration] protobuf duration
216
+ # @return [Float, nil] duration in seconds
217
+ def convert_protobuf_duration_to_float(duration)
218
+ return nil unless duration
219
+
220
+ duration.seconds + (duration.nanos / 1_000_000_000.0)
221
+ end
222
+
223
+ # Convert Time to protobuf timestamp
224
+ #
225
+ # @param time [Time] time to convert
226
+ # @return [Google::Protobuf::Timestamp] protobuf timestamp
227
+ def convert_time_to_protobuf_timestamp(time)
228
+ return nil unless time
229
+
230
+ Google::Protobuf::Timestamp.new(seconds: time.to_i, nanos: time.nsec)
231
+ end
232
+
233
+ # Convert duration to protobuf duration
234
+ #
235
+ # @param duration [Float] duration in seconds
236
+ # @return [Google::Protobuf::Duration] protobuf duration
237
+ def convert_duration_to_protobuf(duration)
238
+ return nil unless duration
239
+
240
+ seconds = duration.to_i
241
+ nanos = ((duration - seconds) * 1_000_000_000).to_i
242
+
243
+ Google::Protobuf::Duration.new(seconds: seconds, nanos: nanos)
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end