makit 0.0.111 → 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 (140) 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/cli/build_commands.rb +500 -500
  6. data/lib/makit/cli/generators/base_generator.rb +74 -74
  7. data/lib/makit/cli/generators/dotnet_generator.rb +50 -50
  8. data/lib/makit/cli/generators/generator_factory.rb +49 -49
  9. data/lib/makit/cli/generators/node_generator.rb +50 -50
  10. data/lib/makit/cli/generators/ruby_generator.rb +77 -77
  11. data/lib/makit/cli/generators/rust_generator.rb +50 -50
  12. data/lib/makit/cli/generators/templates/dotnet_templates.rb +167 -167
  13. data/lib/makit/cli/generators/templates/node_templates.rb +161 -161
  14. data/lib/makit/cli/generators/templates/ruby/gemfile.rb +26 -26
  15. data/lib/makit/cli/generators/templates/ruby/gemspec.rb +40 -40
  16. data/lib/makit/cli/generators/templates/ruby/main_lib.rb +33 -33
  17. data/lib/makit/cli/generators/templates/ruby/rakefile.rb +35 -35
  18. data/lib/makit/cli/generators/templates/ruby/readme.rb +63 -63
  19. data/lib/makit/cli/generators/templates/ruby/test.rb +39 -39
  20. data/lib/makit/cli/generators/templates/ruby/test_helper.rb +29 -29
  21. data/lib/makit/cli/generators/templates/ruby/version.rb +29 -29
  22. data/lib/makit/cli/generators/templates/rust_templates.rb +128 -128
  23. data/lib/makit/cli/main.rb +62 -62
  24. data/lib/makit/cli/project_commands.rb +868 -868
  25. data/lib/makit/cli/repository_commands.rb +661 -661
  26. data/lib/makit/cli/utility_commands.rb +521 -521
  27. data/lib/makit/commands/factory.rb +359 -359
  28. data/lib/makit/commands/middleware/base.rb +73 -73
  29. data/lib/makit/commands/middleware/cache.rb +248 -248
  30. data/lib/makit/commands/middleware/command_logger.rb +320 -323
  31. data/lib/makit/commands/middleware/unified_logger.rb +243 -243
  32. data/lib/makit/commands/middleware/validator.rb +269 -269
  33. data/lib/makit/commands/request.rb +254 -254
  34. data/lib/makit/commands/result.rb +323 -323
  35. data/lib/makit/commands/runner.rb +337 -317
  36. data/lib/makit/commands/strategies/base.rb +160 -160
  37. data/lib/makit/commands/strategies/synchronous.rb +134 -134
  38. data/lib/makit/commands.rb +51 -42
  39. data/lib/makit/configuration/gitlab_helper.rb +60 -60
  40. data/lib/makit/configuration/project.rb +127 -127
  41. data/lib/makit/configuration/rakefile_helper.rb +43 -43
  42. data/lib/makit/configuration/step.rb +34 -34
  43. data/lib/makit/configuration.rb +14 -14
  44. data/lib/makit/content/default_gitignore.rb +7 -7
  45. data/lib/makit/content/default_rakefile.rb +13 -13
  46. data/lib/makit/content/gem_rakefile.rb +16 -16
  47. data/lib/makit/context.rb +1 -1
  48. data/lib/makit/data.rb +49 -49
  49. data/lib/makit/directories.rb +141 -141
  50. data/lib/makit/directory.rb +262 -262
  51. data/lib/makit/docs/files.rb +89 -89
  52. data/lib/makit/docs/rake.rb +102 -102
  53. data/lib/makit/dotnet/project.rb +153 -153
  54. data/lib/makit/dotnet/solution.rb +38 -38
  55. data/lib/makit/dotnet/solution_classlib.rb +239 -239
  56. data/lib/makit/dotnet/solution_console.rb +264 -264
  57. data/lib/makit/dotnet/solution_maui.rb +354 -354
  58. data/lib/makit/dotnet/solution_wasm.rb +275 -275
  59. data/lib/makit/dotnet/solution_wpf.rb +304 -304
  60. data/lib/makit/dotnet.rb +102 -102
  61. data/lib/makit/email.rb +90 -90
  62. data/lib/makit/environment.rb +142 -142
  63. data/lib/makit/examples/runner.rb +370 -370
  64. data/lib/makit/exceptions.rb +45 -45
  65. data/lib/makit/fileinfo.rb +24 -24
  66. data/lib/makit/files.rb +43 -43
  67. data/lib/makit/gems.rb +40 -40
  68. data/lib/makit/git/cli.rb +54 -54
  69. data/lib/makit/git/repository.rb +90 -90
  70. data/lib/makit/git.rb +98 -98
  71. data/lib/makit/gitlab_runner.rb +59 -59
  72. data/lib/makit/humanize.rb +137 -137
  73. data/lib/makit/indexer.rb +47 -47
  74. data/lib/makit/logging/configuration.rb +305 -305
  75. data/lib/makit/logging/formatters/base.rb +39 -39
  76. data/lib/makit/logging/formatters/console_formatter.rb +140 -127
  77. data/lib/makit/logging/formatters/json_formatter.rb +65 -65
  78. data/lib/makit/logging/formatters/plain_text_formatter.rb +71 -71
  79. data/lib/makit/logging/formatters/text_formatter.rb +64 -64
  80. data/lib/makit/logging/log_request.rb +115 -115
  81. data/lib/makit/logging/logger.rb +163 -159
  82. data/lib/makit/logging/sinks/console.rb +72 -72
  83. data/lib/makit/logging/sinks/file_sink.rb +92 -92
  84. data/lib/makit/logging/sinks/unified_file_sink.rb +303 -303
  85. data/lib/makit/logging.rb +530 -521
  86. data/lib/makit/markdown.rb +75 -75
  87. data/lib/makit/mp/basic_object_mp.rb +17 -17
  88. data/lib/makit/mp/command_mp.rb +13 -13
  89. data/lib/makit/mp/command_request.mp.rb +17 -17
  90. data/lib/makit/mp/project_mp.rb +199 -199
  91. data/lib/makit/mp/string_mp.rb +193 -348
  92. data/lib/makit/nuget.rb +74 -74
  93. data/lib/makit/port.rb +32 -32
  94. data/lib/makit/process.rb +163 -163
  95. data/lib/makit/protoc.rb +107 -107
  96. data/lib/makit/rake/cli.rb +196 -196
  97. data/lib/makit/rake.rb +25 -25
  98. data/lib/makit/ruby/cli.rb +185 -185
  99. data/lib/makit/ruby.rb +25 -25
  100. data/lib/makit/secrets.rb +51 -51
  101. data/lib/makit/serializer.rb +130 -117
  102. data/lib/makit/services/builder.rb +186 -186
  103. data/lib/makit/services/error_handler.rb +226 -226
  104. data/lib/makit/services/repository_manager.rb +229 -229
  105. data/lib/makit/services/validator.rb +112 -112
  106. data/lib/makit/setup/classlib.rb +53 -53
  107. data/lib/makit/setup/gem.rb +30 -17
  108. data/lib/makit/setup/runner.rb +45 -40
  109. data/lib/makit/setup.rb +5 -0
  110. data/lib/makit/show.rb +110 -110
  111. data/lib/makit/storage.rb +126 -126
  112. data/lib/makit/symbols.rb +170 -161
  113. data/lib/makit/task_info.rb +128 -128
  114. data/lib/makit/tasks/at_exit.rb +13 -13
  115. data/lib/makit/tasks/build.rb +19 -18
  116. data/lib/makit/tasks/clean.rb +11 -11
  117. data/lib/makit/tasks/hook_manager.rb +393 -239
  118. data/lib/makit/tasks/init.rb +47 -47
  119. data/lib/makit/tasks/integrate.rb +17 -15
  120. data/lib/makit/tasks/pull_incoming.rb +11 -12
  121. data/lib/makit/tasks/setup.rb +6 -6
  122. data/lib/makit/tasks/sync.rb +12 -11
  123. data/lib/makit/tasks/tag.rb +15 -0
  124. data/lib/makit/tasks/task_monkey_patch.rb +79 -79
  125. data/lib/makit/tasks.rb +10 -0
  126. data/lib/makit/test_cache.rb +239 -239
  127. data/lib/makit/tree.rb +37 -37
  128. data/lib/makit/v1/makit.v1_pb.rb +34 -34
  129. data/lib/makit/v1/makit.v1_services_pb.rb +27 -27
  130. data/lib/makit/version.rb +5 -5
  131. data/lib/makit/version_util.rb +21 -0
  132. data/lib/makit/wix.rb +95 -95
  133. data/lib/makit/yaml.rb +29 -29
  134. data/lib/makit/zip.rb +17 -17
  135. data/lib/makit copy.rb +44 -0
  136. data/lib/makit.rb +40 -8
  137. metadata +50 -7
  138. data/lib/makit/command_runner.rb +0 -463
  139. data/lib/makit/commands/compatibility.rb +0 -365
  140. data/lib/makit/task_hooks.rb +0 -125
@@ -1,254 +1,254 @@
1
- # frozen_string_literal: true
2
-
3
- require "shellwords"
4
- require "json"
5
-
6
- module Makit
7
- module Commands
8
- # Enhanced request handling with validation and type safety.
9
- # Uses plain Ruby objects - no protobuf dependency for better performance.
10
- #
11
- # @example Create a simple request
12
- # request = Request.new(command: "git", arguments: ["--version"])
13
- #
14
- # @example Create request from string
15
- # request = Request.from_string("git clone https://github.com/user/repo.git")
16
- #
17
- # @example Create request with full options
18
- # request = Request.new(
19
- # command: "bundle",
20
- # arguments: ["install"],
21
- # directory: "/path/to/project",
22
- # environment: { "BUNDLE_JOBS" => "4" },
23
- # timeout: 300,
24
- # metadata: { operation: "dependency_install" }
25
- # )
26
- class Request
27
- # Command execution request with validation and metadata.
28
- #
29
- # @!attribute [r] command
30
- # @return [String] the command to execute
31
- # @!attribute [r] arguments
32
- # @return [Array<String>] command line arguments
33
- # @!attribute [r] environment
34
- # @return [Hash<String,String>] environment variables
35
- # @!attribute [r] directory
36
- # @return [String] working directory for execution
37
- # @!attribute [r] timeout
38
- # @return [Integer] timeout in seconds
39
- # @!attribute [r] metadata
40
- # @return [Hash] additional metadata for logging/tracking
41
- attr_reader :command, :arguments, :environment, :directory, :timeout, :metadata
42
-
43
- # Initialize a new command request.
44
- #
45
- # @param command [String] the command to execute
46
- # @param arguments [Array<String>] command arguments
47
- # @param environment [Hash<String,String>] environment variables
48
- # @param directory [String] working directory
49
- # @param timeout [Integer] timeout in seconds
50
- # @param metadata [Hash] additional metadata
51
- # @raise [ArgumentError] if command is invalid
52
- def initialize(command:, arguments: [], **options)
53
- @command = validate_command(command)
54
- @arguments = validate_arguments(arguments)
55
- @environment = options[:environment] || {}
56
- @directory = options[:directory] || Dir.pwd
57
- @timeout = options[:timeout] || 30
58
- @metadata = options[:metadata] || {}
59
-
60
- validate_directory(@directory)
61
- validate_timeout(@timeout)
62
- end
63
-
64
- # Convert request to shell-executable command string.
65
- #
66
- # @return [String] shell command string
67
- def to_shell_command
68
- cmd_parts = [command] + arguments
69
- Shellwords.join(cmd_parts)
70
- end
71
-
72
- # Convert request to hash representation.
73
- #
74
- # @return [Hash] hash representation of the request
75
- def to_h
76
- {
77
- command: command,
78
- arguments: arguments,
79
- environment: environment,
80
- directory: directory,
81
- timeout: timeout,
82
- metadata: metadata,
83
- }
84
- end
85
-
86
- # Convert request to JSON representation.
87
- #
88
- # @param args [Array] arguments passed to JSON.generate
89
- # @return [String] JSON representation
90
- def to_json(*args)
91
- JSON.generate(to_h, *args)
92
- end
93
-
94
- # Create a request from a shell command string.
95
- #
96
- # @param command_string [String] shell command string to parse
97
- # @param options [Hash] additional options
98
- # @return [Request] new request object
99
- # @raise [ArgumentError] if command string is invalid
100
- # @example
101
- # Request.from_string("git clone https://github.com/user/repo.git")
102
- # Request.from_string("bundle install --jobs 4")
103
- def self.from_string(command_string, **options)
104
- raise ArgumentError, "Command string cannot be empty" if command_string.nil? || command_string.strip.empty?
105
-
106
- # Parse shell command string into command and arguments
107
- parts = Shellwords.split(command_string.strip)
108
- command = parts.shift
109
- arguments = parts
110
-
111
- new(command: command, arguments: arguments, **options)
112
- end
113
-
114
- # Create a request from hash representation.
115
- #
116
- # @param command_hash [Hash] hash containing command information
117
- # @return [Request] new request object
118
- # @raise [ArgumentError] if hash is invalid
119
- # @example
120
- # Request.from_hash({
121
- # "command" => "git",
122
- # "arguments" => ["clone", "https://github.com/user/repo.git"],
123
- # "timeout" => 300
124
- # })
125
- def self.from_hash(command_hash)
126
- raise ArgumentError, "Command hash cannot be nil" if command_hash.nil?
127
-
128
- # Convert string keys to symbols for consistency
129
- hash = command_hash.is_a?(Hash) ? normalize_hash_keys(command_hash) : command_hash
130
-
131
- new(
132
- command: hash[:command] || hash["command"],
133
- arguments: hash[:arguments] || hash["arguments"] || [],
134
- environment: hash[:environment] || hash["environment"] || {},
135
- directory: hash[:directory] || hash["directory"],
136
- timeout: hash[:timeout] || hash["timeout"],
137
- metadata: hash[:metadata] || hash["metadata"] || {},
138
- )
139
- end
140
-
141
- # Create a request from JSON string.
142
- #
143
- # @param json_string [String] JSON string representation
144
- # @return [Request] new request object
145
- # @raise [JSON::ParserError, ArgumentError] if JSON is invalid
146
- def self.from_json(json_string)
147
- hash = JSON.parse(json_string)
148
- from_hash(hash)
149
- end
150
-
151
- # Check if this request represents the same command.
152
- #
153
- # @param other [Request] another request to compare
154
- # @return [Boolean] true if commands are equivalent
155
- def equivalent_to?(other)
156
- return false unless other.is_a?(Request)
157
-
158
- command == other.command &&
159
- arguments == other.arguments &&
160
- environment == other.environment &&
161
- directory == other.directory
162
- end
163
-
164
- # Generate a cache key for this request.
165
- #
166
- # @return [String] cache key based on command, arguments, and context
167
- def cache_key
168
- require "digest"
169
- content = "#{command}:#{arguments.join(":")}:#{directory}"
170
- Digest::SHA256.hexdigest(content)[0..16]
171
- end
172
-
173
- private
174
-
175
- # Validate command parameter.
176
- #
177
- # @param command [String] command to validate
178
- # @return [String] validated command
179
- # @raise [ArgumentError] if command is invalid
180
- def validate_command(command)
181
- raise ArgumentError, "Command cannot be nil" if command.nil?
182
- raise ArgumentError, "Command cannot be empty" if command.strip.empty?
183
-
184
- cmd = command.strip
185
-
186
- # Check for dangerous characters that could indicate command injection
187
- raise ArgumentError, "Command contains potentially dangerous characters" if cmd.match?(/[;&|`$(){}]/)
188
-
189
- cmd
190
- end
191
-
192
- # Validate arguments array.
193
- #
194
- # @param arguments [Array] arguments to validate
195
- # @return [Array<String>] validated arguments
196
- # @raise [ArgumentError] if arguments are invalid
197
- def validate_arguments(arguments)
198
- return [] if arguments.nil?
199
- raise ArgumentError, "Arguments must be an array" unless arguments.is_a?(Array)
200
-
201
- # Convert all arguments to strings and validate
202
- arguments.map do |arg|
203
- raise ArgumentError, "Argument cannot be nil" if arg.nil?
204
-
205
- arg.to_s
206
- end
207
- end
208
-
209
- # Validate directory parameter.
210
- #
211
- # @param directory [String] directory to validate
212
- # @raise [ArgumentError] if directory is invalid
213
- def validate_directory(directory)
214
- return unless directory
215
-
216
- raise ArgumentError, "Directory must be a string" unless directory.is_a?(String)
217
-
218
- # NOTE: We don't validate directory existence here as it might be created later
219
- # We just validate it's a reasonable path format
220
- return unless directory.include?("\0")
221
-
222
- raise ArgumentError, "Directory path contains null bytes"
223
- end
224
-
225
- # Validate timeout parameter.
226
- #
227
- # @param timeout [Integer] timeout to validate
228
- # @raise [ArgumentError] if timeout is invalid
229
- def validate_timeout(timeout)
230
- raise ArgumentError, "Timeout must be a positive integer" unless timeout.is_a?(Integer) && timeout.positive?
231
-
232
- return unless timeout > 3600 # 1 hour max
233
-
234
- raise ArgumentError, "Timeout cannot exceed 3600 seconds"
235
- end
236
-
237
- # Normalize hash keys to support both string and symbol keys.
238
- #
239
- # @param hash [Hash] hash to normalize
240
- # @return [Hash] hash with normalized keys
241
- def self.normalize_hash_keys(hash)
242
- normalized = {}
243
- hash.each do |key, value|
244
- normalized_key = key.is_a?(String) ? key.to_sym : key
245
- normalized[key] = value # Keep original key
246
- normalized[normalized_key] = value # Add symbol version
247
- end
248
- normalized
249
- end
250
-
251
- private_class_method :normalize_hash_keys
252
- end
253
- end
254
- end
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+ require "json"
5
+
6
+ module Makit
7
+ module Commands
8
+ # Enhanced request handling with validation and type safety.
9
+ # Uses plain Ruby objects - no protobuf dependency for better performance.
10
+ #
11
+ # @example Create a simple request
12
+ # request = Request.new(command: "git", arguments: ["--version"])
13
+ #
14
+ # @example Create request from string
15
+ # request = Request.from_string("git clone https://github.com/user/repo.git")
16
+ #
17
+ # @example Create request with full options
18
+ # request = Request.new(
19
+ # command: "bundle",
20
+ # arguments: ["install"],
21
+ # directory: "/path/to/project",
22
+ # environment: { "BUNDLE_JOBS" => "4" },
23
+ # timeout: 300,
24
+ # metadata: { operation: "dependency_install" }
25
+ # )
26
+ class Request
27
+ # Command execution request with validation and metadata.
28
+ #
29
+ # @!attribute [r] command
30
+ # @return [String] the command to execute
31
+ # @!attribute [r] arguments
32
+ # @return [Array<String>] command line arguments
33
+ # @!attribute [r] environment
34
+ # @return [Hash<String,String>] environment variables
35
+ # @!attribute [r] directory
36
+ # @return [String] working directory for execution
37
+ # @!attribute [r] timeout
38
+ # @return [Integer] timeout in seconds
39
+ # @!attribute [r] metadata
40
+ # @return [Hash] additional metadata for logging/tracking
41
+ attr_reader :command, :arguments, :environment, :directory, :timeout, :metadata
42
+
43
+ # Initialize a new command request.
44
+ #
45
+ # @param command [String] the command to execute
46
+ # @param arguments [Array<String>] command arguments
47
+ # @param environment [Hash<String,String>] environment variables
48
+ # @param directory [String] working directory
49
+ # @param timeout [Integer] timeout in seconds
50
+ # @param metadata [Hash] additional metadata
51
+ # @raise [ArgumentError] if command is invalid
52
+ def initialize(command:, arguments: [], **options)
53
+ @command = validate_command(command)
54
+ @arguments = validate_arguments(arguments)
55
+ @environment = options[:environment] || {}
56
+ @directory = options[:directory] || Dir.pwd
57
+ @timeout = options[:timeout] || 30
58
+ @metadata = options[:metadata] || {}
59
+
60
+ validate_directory(@directory)
61
+ validate_timeout(@timeout)
62
+ end
63
+
64
+ # Convert request to shell-executable command string.
65
+ #
66
+ # @return [String] shell command string
67
+ def to_shell_command
68
+ cmd_parts = [command] + arguments
69
+ Shellwords.join(cmd_parts)
70
+ end
71
+
72
+ # Convert request to hash representation.
73
+ #
74
+ # @return [Hash] hash representation of the request
75
+ def to_h
76
+ {
77
+ command: command,
78
+ arguments: arguments,
79
+ environment: environment,
80
+ directory: directory,
81
+ timeout: timeout,
82
+ metadata: metadata,
83
+ }
84
+ end
85
+
86
+ # Convert request to JSON representation.
87
+ #
88
+ # @param args [Array] arguments passed to JSON.generate
89
+ # @return [String] JSON representation
90
+ def to_json(*args)
91
+ JSON.generate(to_h, *args)
92
+ end
93
+
94
+ # Create a request from a shell command string.
95
+ #
96
+ # @param command_string [String] shell command string to parse
97
+ # @param options [Hash] additional options
98
+ # @return [Request] new request object
99
+ # @raise [ArgumentError] if command string is invalid
100
+ # @example
101
+ # Request.from_string("git clone https://github.com/user/repo.git")
102
+ # Request.from_string("bundle install --jobs 4")
103
+ def self.from_string(command_string, **options)
104
+ raise ArgumentError, "Command string cannot be empty" if command_string.nil? || command_string.strip.empty?
105
+
106
+ # Parse shell command string into command and arguments
107
+ parts = Shellwords.split(command_string.strip)
108
+ command = parts.shift
109
+ arguments = parts
110
+
111
+ new(command: command, arguments: arguments, **options)
112
+ end
113
+
114
+ # Create a request from hash representation.
115
+ #
116
+ # @param command_hash [Hash] hash containing command information
117
+ # @return [Request] new request object
118
+ # @raise [ArgumentError] if hash is invalid
119
+ # @example
120
+ # Request.from_hash({
121
+ # "command" => "git",
122
+ # "arguments" => ["clone", "https://github.com/user/repo.git"],
123
+ # "timeout" => 300
124
+ # })
125
+ def self.from_hash(command_hash)
126
+ raise ArgumentError, "Command hash cannot be nil" if command_hash.nil?
127
+
128
+ # Convert string keys to symbols for consistency
129
+ hash = command_hash.is_a?(Hash) ? normalize_hash_keys(command_hash) : command_hash
130
+
131
+ new(
132
+ command: hash[:command] || hash["command"],
133
+ arguments: hash[:arguments] || hash["arguments"] || [],
134
+ environment: hash[:environment] || hash["environment"] || {},
135
+ directory: hash[:directory] || hash["directory"],
136
+ timeout: hash[:timeout] || hash["timeout"],
137
+ metadata: hash[:metadata] || hash["metadata"] || {},
138
+ )
139
+ end
140
+
141
+ # Create a request from JSON string.
142
+ #
143
+ # @param json_string [String] JSON string representation
144
+ # @return [Request] new request object
145
+ # @raise [JSON::ParserError, ArgumentError] if JSON is invalid
146
+ def self.from_json(json_string)
147
+ hash = JSON.parse(json_string)
148
+ from_hash(hash)
149
+ end
150
+
151
+ # Check if this request represents the same command.
152
+ #
153
+ # @param other [Request] another request to compare
154
+ # @return [Boolean] true if commands are equivalent
155
+ def equivalent_to?(other)
156
+ return false unless other.is_a?(Request)
157
+
158
+ command == other.command &&
159
+ arguments == other.arguments &&
160
+ environment == other.environment &&
161
+ directory == other.directory
162
+ end
163
+
164
+ # Generate a cache key for this request.
165
+ #
166
+ # @return [String] cache key based on command, arguments, and context
167
+ def cache_key
168
+ require "digest"
169
+ content = "#{command}:#{arguments.join(":")}:#{directory}"
170
+ Digest::SHA256.hexdigest(content)[0..16]
171
+ end
172
+
173
+ private
174
+
175
+ # Validate command parameter.
176
+ #
177
+ # @param command [String] command to validate
178
+ # @return [String] validated command
179
+ # @raise [ArgumentError] if command is invalid
180
+ def validate_command(command)
181
+ raise ArgumentError, "Command cannot be nil" if command.nil?
182
+ raise ArgumentError, "Command cannot be empty" if command.strip.empty?
183
+
184
+ cmd = command.strip
185
+
186
+ # Check for dangerous characters that could indicate command injection
187
+ raise ArgumentError, "Command contains potentially dangerous characters" if cmd.match?(/[;&|`$(){}]/)
188
+
189
+ cmd
190
+ end
191
+
192
+ # Validate arguments array.
193
+ #
194
+ # @param arguments [Array] arguments to validate
195
+ # @return [Array<String>] validated arguments
196
+ # @raise [ArgumentError] if arguments are invalid
197
+ def validate_arguments(arguments)
198
+ return [] if arguments.nil?
199
+ raise ArgumentError, "Arguments must be an array" unless arguments.is_a?(Array)
200
+
201
+ # Convert all arguments to strings and validate
202
+ arguments.map do |arg|
203
+ raise ArgumentError, "Argument cannot be nil" if arg.nil?
204
+
205
+ arg.to_s
206
+ end
207
+ end
208
+
209
+ # Validate directory parameter.
210
+ #
211
+ # @param directory [String] directory to validate
212
+ # @raise [ArgumentError] if directory is invalid
213
+ def validate_directory(directory)
214
+ return unless directory
215
+
216
+ raise ArgumentError, "Directory must be a string" unless directory.is_a?(String)
217
+
218
+ # NOTE: We don't validate directory existence here as it might be created later
219
+ # We just validate it's a reasonable path format
220
+ return unless directory.include?("\0")
221
+
222
+ raise ArgumentError, "Directory path contains null bytes"
223
+ end
224
+
225
+ # Validate timeout parameter.
226
+ #
227
+ # @param timeout [Integer] timeout to validate
228
+ # @raise [ArgumentError] if timeout is invalid
229
+ def validate_timeout(timeout)
230
+ raise ArgumentError, "Timeout must be a positive integer" unless timeout.is_a?(Integer) && timeout.positive?
231
+
232
+ return unless timeout > 3600 # 1 hour max
233
+
234
+ raise ArgumentError, "Timeout cannot exceed 3600 seconds"
235
+ end
236
+
237
+ # Normalize hash keys to support both string and symbol keys.
238
+ #
239
+ # @param hash [Hash] hash to normalize
240
+ # @return [Hash] hash with normalized keys
241
+ def self.normalize_hash_keys(hash)
242
+ normalized = {}
243
+ hash.each do |key, value|
244
+ normalized_key = key.is_a?(String) ? key.to_sym : key
245
+ normalized[key] = value # Keep original key
246
+ normalized[normalized_key] = value # Add symbol version
247
+ end
248
+ normalized
249
+ end
250
+
251
+ private_class_method :normalize_hash_keys
252
+ end
253
+ end
254
+ end