makit 0.0.98 → 0.0.111
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.
- checksums.yaml +4 -4
- data/README.md +41 -0
- data/exe/makit +5 -0
- data/lib/makit/apache.rb +7 -11
- data/lib/makit/cli/build_commands.rb +500 -0
- data/lib/makit/cli/generators/base_generator.rb +74 -0
- data/lib/makit/cli/generators/dotnet_generator.rb +50 -0
- data/lib/makit/cli/generators/generator_factory.rb +49 -0
- data/lib/makit/cli/generators/node_generator.rb +50 -0
- data/lib/makit/cli/generators/ruby_generator.rb +77 -0
- data/lib/makit/cli/generators/rust_generator.rb +50 -0
- data/lib/makit/cli/generators/templates/dotnet_templates.rb +167 -0
- data/lib/makit/cli/generators/templates/node_templates.rb +161 -0
- data/lib/makit/cli/generators/templates/ruby/gemfile.rb +26 -0
- data/lib/makit/cli/generators/templates/ruby/gemspec.rb +40 -0
- data/lib/makit/cli/generators/templates/ruby/main_lib.rb +33 -0
- data/lib/makit/cli/generators/templates/ruby/rakefile.rb +35 -0
- data/lib/makit/cli/generators/templates/ruby/readme.rb +63 -0
- data/lib/makit/cli/generators/templates/ruby/test.rb +39 -0
- data/lib/makit/cli/generators/templates/ruby/test_helper.rb +29 -0
- data/lib/makit/cli/generators/templates/ruby/version.rb +29 -0
- data/lib/makit/cli/generators/templates/rust_templates.rb +128 -0
- data/lib/makit/cli/main.rb +48 -19
- data/lib/makit/cli/project_commands.rb +868 -0
- data/lib/makit/cli/repository_commands.rb +661 -0
- data/lib/makit/cli/utility_commands.rb +521 -0
- data/lib/makit/command_runner.rb +187 -128
- data/lib/makit/commands/compatibility.rb +365 -0
- data/lib/makit/commands/factory.rb +359 -0
- data/lib/makit/commands/middleware/base.rb +73 -0
- data/lib/makit/commands/middleware/cache.rb +248 -0
- data/lib/makit/commands/middleware/command_logger.rb +323 -0
- data/lib/makit/commands/middleware/unified_logger.rb +243 -0
- data/lib/makit/commands/middleware/validator.rb +269 -0
- data/lib/makit/commands/request.rb +254 -0
- data/lib/makit/commands/result.rb +323 -0
- data/lib/makit/commands/runner.rb +317 -0
- data/lib/makit/commands/strategies/base.rb +160 -0
- data/lib/makit/commands/strategies/synchronous.rb +134 -0
- data/lib/makit/commands.rb +24 -3
- data/lib/makit/configuration/gitlab_helper.rb +60 -0
- data/lib/makit/configuration/project.rb +127 -0
- data/lib/makit/configuration/rakefile_helper.rb +43 -0
- data/lib/makit/configuration/step.rb +34 -0
- data/lib/makit/configuration.rb +14 -0
- data/lib/makit/content/default_gitignore.rb +4 -2
- data/lib/makit/content/default_rakefile.rb +4 -2
- data/lib/makit/content/gem_rakefile.rb +4 -2
- data/lib/makit/context.rb +1 -0
- data/lib/makit/data.rb +9 -10
- data/lib/makit/directories.rb +48 -52
- data/lib/makit/directory.rb +38 -52
- data/lib/makit/docs/files.rb +5 -10
- data/lib/makit/docs/rake.rb +16 -20
- data/lib/makit/dotnet/cli.rb +65 -0
- data/lib/makit/dotnet/project.rb +153 -0
- data/lib/makit/dotnet/solution.rb +38 -0
- data/lib/makit/dotnet/solution_classlib.rb +239 -0
- data/lib/makit/dotnet/solution_console.rb +264 -0
- data/lib/makit/dotnet/solution_maui.rb +354 -0
- data/lib/makit/dotnet/solution_wasm.rb +275 -0
- data/lib/makit/dotnet/solution_wpf.rb +304 -0
- data/lib/makit/dotnet.rb +54 -171
- data/lib/makit/email.rb +46 -17
- data/lib/makit/environment.rb +22 -19
- data/lib/makit/examples/runner.rb +370 -0
- data/lib/makit/exceptions.rb +45 -0
- data/lib/makit/fileinfo.rb +3 -5
- data/lib/makit/files.rb +12 -16
- data/lib/makit/gems.rb +40 -39
- data/lib/makit/git/cli.rb +54 -0
- data/lib/makit/git/repository.rb +90 -0
- data/lib/makit/git.rb +44 -91
- data/lib/makit/gitlab_runner.rb +0 -1
- data/lib/makit/humanize.rb +31 -23
- data/lib/makit/indexer.rb +15 -24
- data/lib/makit/logging/configuration.rb +305 -0
- data/lib/makit/logging/format_registry.rb +84 -0
- data/lib/makit/logging/formatters/base.rb +39 -0
- data/lib/makit/logging/formatters/console_formatter.rb +127 -0
- data/lib/makit/logging/formatters/json_formatter.rb +65 -0
- data/lib/makit/logging/formatters/plain_text_formatter.rb +71 -0
- data/lib/makit/logging/formatters/text_formatter.rb +64 -0
- data/lib/makit/logging/log_request.rb +115 -0
- data/lib/makit/logging/logger.rb +159 -0
- data/lib/makit/logging/sinks/base.rb +91 -0
- data/lib/makit/logging/sinks/console.rb +72 -0
- data/lib/makit/logging/sinks/file_sink.rb +92 -0
- data/lib/makit/logging/sinks/structured.rb +129 -0
- data/lib/makit/logging/sinks/unified_file_sink.rb +303 -0
- data/lib/makit/logging.rb +452 -37
- data/lib/makit/markdown.rb +18 -18
- data/lib/makit/mp/basic_object_mp.rb +5 -4
- data/lib/makit/mp/command_mp.rb +5 -5
- data/lib/makit/mp/command_request.mp.rb +3 -2
- data/lib/makit/mp/project_mp.rb +85 -96
- data/lib/makit/mp/string_mp.rb +245 -73
- data/lib/makit/nuget.rb +27 -25
- data/lib/makit/port.rb +25 -27
- data/lib/makit/process.rb +127 -29
- data/lib/makit/protoc.rb +27 -24
- data/lib/makit/rake/cli.rb +196 -0
- data/lib/makit/rake.rb +6 -6
- data/lib/makit/ruby/cli.rb +185 -0
- data/lib/makit/ruby.rb +25 -0
- data/lib/makit/secrets.rb +18 -18
- data/lib/makit/serializer.rb +29 -27
- data/lib/makit/services/builder.rb +186 -0
- data/lib/makit/services/error_handler.rb +226 -0
- data/lib/makit/services/repository_manager.rb +229 -0
- data/lib/makit/services/validator.rb +112 -0
- data/lib/makit/setup/classlib.rb +53 -0
- data/lib/makit/setup/gem.rb +250 -0
- data/lib/makit/setup/runner.rb +40 -0
- data/lib/makit/show.rb +16 -16
- data/lib/makit/storage.rb +32 -37
- data/lib/makit/symbols.rb +12 -0
- data/lib/makit/task_hooks.rb +125 -0
- data/lib/makit/task_info.rb +63 -21
- data/lib/makit/tasks/at_exit.rb +13 -0
- data/lib/makit/tasks/build.rb +18 -0
- data/lib/makit/tasks/clean.rb +11 -0
- data/lib/makit/tasks/hook_manager.rb +239 -0
- data/lib/makit/tasks/init.rb +47 -0
- data/lib/makit/tasks/integrate.rb +15 -0
- data/lib/makit/tasks/pull_incoming.rb +12 -0
- data/lib/makit/tasks/setup.rb +6 -0
- data/lib/makit/tasks/sync.rb +11 -0
- data/lib/makit/tasks/task_monkey_patch.rb +79 -0
- data/lib/makit/tasks.rb +5 -150
- data/lib/makit/test_cache.rb +239 -0
- data/lib/makit/v1/makit.v1_pb.rb +34 -35
- data/lib/makit/v1/makit.v1_services_pb.rb +2 -0
- data/lib/makit/version.rb +1 -60
- data/lib/makit/wix.rb +23 -23
- data/lib/makit/yaml.rb +18 -6
- data/lib/makit.rb +2 -261
- metadata +109 -145
- data/lib/makit/cli/clean.rb +0 -14
- data/lib/makit/cli/clone.rb +0 -59
- data/lib/makit/cli/init.rb +0 -38
- data/lib/makit/cli/make.rb +0 -54
- data/lib/makit/cli/new.rb +0 -37
- data/lib/makit/cli/nuget_cache.rb +0 -38
- data/lib/makit/cli/pull.rb +0 -31
- data/lib/makit/cli/setup.rb +0 -71
- data/lib/makit/cli/work.rb +0 -21
- data/lib/makit/content/default_gitignore.txt +0 -222
@@ -0,0 +1,269 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "shellwords"
|
4
|
+
require "uri"
|
5
|
+
|
6
|
+
module Makit
|
7
|
+
module Commands
|
8
|
+
# Custom exception for security-related command validation failures
|
9
|
+
class SecurityError < StandardError; end
|
10
|
+
|
11
|
+
module Middleware
|
12
|
+
# Validation middleware that provides input sanitization and security checks
|
13
|
+
# for command execution contexts.
|
14
|
+
#
|
15
|
+
# This middleware validates and sanitizes:
|
16
|
+
# - Command strings for shell injection attacks
|
17
|
+
# - File paths for directory traversal
|
18
|
+
# - URLs for malicious schemes
|
19
|
+
# - Environment variables for sensitive data
|
20
|
+
# - Arguments for dangerous patterns
|
21
|
+
#
|
22
|
+
# @example Basic usage
|
23
|
+
# validator = Commands::Middleware::Validator.new
|
24
|
+
# context = Commands::Context.new(command: 'ls -la')
|
25
|
+
# validated_context = validator.call(context)
|
26
|
+
#
|
27
|
+
# @example With custom validation rules
|
28
|
+
# validator = Commands::Middleware::Validator.new(
|
29
|
+
# allow_shell_operators: false,
|
30
|
+
# blocked_commands: %w[rm sudo],
|
31
|
+
# max_command_length: 500
|
32
|
+
# )
|
33
|
+
class Validator
|
34
|
+
# Default blocked commands that pose security risks
|
35
|
+
DEFAULT_BLOCKED_COMMANDS = %w[
|
36
|
+
rm rmdir
|
37
|
+
sudo su
|
38
|
+
chmod chown
|
39
|
+
wget curl
|
40
|
+
eval exec
|
41
|
+
python ruby node
|
42
|
+
ssh scp rsync
|
43
|
+
dd fdisk mount umount
|
44
|
+
iptables ufw
|
45
|
+
systemctl service
|
46
|
+
crontab
|
47
|
+
passwd
|
48
|
+
].freeze
|
49
|
+
|
50
|
+
# Dangerous shell operators and patterns
|
51
|
+
DANGEROUS_PATTERNS = [
|
52
|
+
/[;&|`$(){}]/, # Shell operators and command substitution
|
53
|
+
%r{\.\./}, # Directory traversal
|
54
|
+
%r{/etc/|/proc/|/sys/}, # Sensitive system directories
|
55
|
+
/\$\{[^}]*\}/, # Variable expansion
|
56
|
+
%r{~[^/\s]*}, # User home directory expansion
|
57
|
+
%r{\bfile://|ftp://|https?://[^\s]*\.(sh|py|rb|js|exe|bat)\b}i, # Executable URLs
|
58
|
+
].freeze
|
59
|
+
|
60
|
+
# Maximum allowed lengths for various inputs
|
61
|
+
DEFAULT_LIMITS = {
|
62
|
+
command_length: 1000,
|
63
|
+
argument_length: 500,
|
64
|
+
path_length: 1000,
|
65
|
+
env_var_value_length: 2000,
|
66
|
+
}.freeze
|
67
|
+
|
68
|
+
attr_reader :options
|
69
|
+
|
70
|
+
# Initialize the validator with security options
|
71
|
+
#
|
72
|
+
# @param options [Hash] Configuration options
|
73
|
+
# @option options [Boolean] :allow_shell_operators (false) Allow shell operators like ;, |, &
|
74
|
+
# @option options [Array<String>] :blocked_commands Commands to block
|
75
|
+
# @option options [Hash] :limits Length limits for various inputs
|
76
|
+
# @option options [Boolean] :strict_mode (true) Enable strict validation
|
77
|
+
# @option options [Array<String>] :allowed_schemes URL schemes to allow
|
78
|
+
def initialize(options = {})
|
79
|
+
@options = {
|
80
|
+
allow_shell_operators: false,
|
81
|
+
blocked_commands: DEFAULT_BLOCKED_COMMANDS,
|
82
|
+
limits: DEFAULT_LIMITS,
|
83
|
+
strict_mode: true,
|
84
|
+
allowed_schemes: %w[http https file],
|
85
|
+
allow_environment_access: false,
|
86
|
+
}.merge(options)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Validate and sanitize the command context
|
90
|
+
#
|
91
|
+
# @param context [Commands::Context] The command execution context
|
92
|
+
# @return [Commands::Context] Validated context
|
93
|
+
# @raise [Commands::SecurityError] If validation fails
|
94
|
+
def call(context)
|
95
|
+
validate_command!(context.command) if context.command
|
96
|
+
validate_arguments!(context.arguments) if context.arguments
|
97
|
+
validate_environment!(context.environment) if context.environment
|
98
|
+
validate_working_directory!(context.working_directory) if context.working_directory
|
99
|
+
|
100
|
+
context
|
101
|
+
rescue StandardError => e
|
102
|
+
raise Commands::SecurityError, "Validation failed: #{e.message}"
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
# Validate the main command string
|
108
|
+
def validate_command!(command)
|
109
|
+
raise Commands::SecurityError, "Empty command" if command.nil? || command.strip.empty?
|
110
|
+
|
111
|
+
if command.length > limits[:command_length]
|
112
|
+
raise Commands::SecurityError,
|
113
|
+
"Command too long (#{command.length} > #{limits[:command_length]})"
|
114
|
+
end
|
115
|
+
|
116
|
+
# Check for blocked commands
|
117
|
+
command_parts = Shellwords.split(command)
|
118
|
+
base_command = File.basename(command_parts.first || "")
|
119
|
+
|
120
|
+
raise Commands::SecurityError, "Blocked command: #{base_command}" if blocked_commands.include?(base_command)
|
121
|
+
|
122
|
+
# Check for dangerous patterns
|
123
|
+
unless options[:allow_shell_operators]
|
124
|
+
DANGEROUS_PATTERNS.each do |pattern|
|
125
|
+
if command.match?(pattern)
|
126
|
+
raise Commands::SecurityError, "Dangerous pattern detected in command: #{pattern.inspect}"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Validate shell escaping
|
132
|
+
begin
|
133
|
+
Shellwords.split(command)
|
134
|
+
rescue ArgumentError => e
|
135
|
+
raise Commands::SecurityError, "Invalid shell command syntax: #{e.message}"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Validate command arguments
|
140
|
+
def validate_arguments!(arguments)
|
141
|
+
return unless arguments
|
142
|
+
|
143
|
+
arguments.each_with_index do |arg, index|
|
144
|
+
next unless arg
|
145
|
+
|
146
|
+
if arg.length > limits[:argument_length]
|
147
|
+
raise Commands::SecurityError, "Argument #{index} too long (#{arg.length} > #{limits[:argument_length]})"
|
148
|
+
end
|
149
|
+
|
150
|
+
# Check for dangerous patterns in arguments
|
151
|
+
if options[:strict_mode]
|
152
|
+
DANGEROUS_PATTERNS.each do |pattern|
|
153
|
+
if arg.match?(pattern)
|
154
|
+
raise Commands::SecurityError, "Dangerous pattern in argument #{index}: #{pattern.inspect}"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Validate file paths in arguments
|
160
|
+
validate_path!(arg) if looks_like_path?(arg)
|
161
|
+
|
162
|
+
# Validate URLs in arguments
|
163
|
+
validate_url!(arg) if looks_like_url?(arg)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Validate environment variables
|
168
|
+
def validate_environment!(environment)
|
169
|
+
return unless environment
|
170
|
+
|
171
|
+
environment.each do |key, value|
|
172
|
+
next unless value
|
173
|
+
|
174
|
+
if value.to_s.length > limits[:env_var_value_length]
|
175
|
+
raise Commands::SecurityError, "Environment variable #{key} value too long"
|
176
|
+
end
|
177
|
+
|
178
|
+
# Check for sensitive environment variables
|
179
|
+
if sensitive_env_var?(key) && !options[:allow_environment_access]
|
180
|
+
raise Commands::SecurityError, "Access to sensitive environment variable #{key} not allowed"
|
181
|
+
end
|
182
|
+
|
183
|
+
# Validate environment variable values
|
184
|
+
if options[:strict_mode] && contains_dangerous_content?(value.to_s)
|
185
|
+
raise Commands::SecurityError, "Dangerous content in environment variable #{key}"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Validate working directory path
|
191
|
+
def validate_working_directory!(path)
|
192
|
+
validate_path!(path, "working directory")
|
193
|
+
end
|
194
|
+
|
195
|
+
# Validate file system paths
|
196
|
+
def validate_path!(path, context = "path")
|
197
|
+
return if path.nil? || path.empty?
|
198
|
+
|
199
|
+
if path.length > limits[:path_length]
|
200
|
+
raise Commands::SecurityError, "#{context.capitalize} too long (#{path.length} > #{limits[:path_length]})"
|
201
|
+
end
|
202
|
+
|
203
|
+
# Check for directory traversal
|
204
|
+
normalized_path = File.expand_path(path)
|
205
|
+
if path.include?("..") && options[:strict_mode]
|
206
|
+
raise Commands::SecurityError, "Directory traversal detected in #{context}: #{path}"
|
207
|
+
end
|
208
|
+
|
209
|
+
# Check for access to sensitive directories
|
210
|
+
sensitive_dirs = %w[/etc /proc /sys /dev /boot /root]
|
211
|
+
return unless sensitive_dirs.any? { |dir| normalized_path.start_with?(dir) } && options[:strict_mode]
|
212
|
+
|
213
|
+
raise Commands::SecurityError, "Access to sensitive directory in #{context}: #{path}"
|
214
|
+
end
|
215
|
+
|
216
|
+
# Validate URLs
|
217
|
+
def validate_url!(url)
|
218
|
+
uri = URI.parse(url)
|
219
|
+
|
220
|
+
unless options[:allowed_schemes].include?(uri.scheme)
|
221
|
+
raise Commands::SecurityError, "Disallowed URL scheme: #{uri.scheme}"
|
222
|
+
end
|
223
|
+
|
224
|
+
# Check for suspicious hosts
|
225
|
+
if uri.host&.match?(/localhost|127\.0\.0\.1|0\.0\.0\.0/)
|
226
|
+
raise Commands::SecurityError, "Local network access not allowed: #{uri.host}"
|
227
|
+
end
|
228
|
+
rescue URI::InvalidURIError => e
|
229
|
+
raise Commands::SecurityError, "Invalid URL format: #{e.message}"
|
230
|
+
end
|
231
|
+
|
232
|
+
# Check if string looks like a file path
|
233
|
+
def looks_like_path?(str)
|
234
|
+
str.start_with?("/") || str.start_with?("./") || str.start_with?("../") || str.include?(File::SEPARATOR)
|
235
|
+
end
|
236
|
+
|
237
|
+
# Check if string looks like a URL
|
238
|
+
def looks_like_url?(str)
|
239
|
+
str.match?(/\A[a-z][a-z0-9+.-]*:/i)
|
240
|
+
end
|
241
|
+
|
242
|
+
# Check if environment variable is sensitive
|
243
|
+
def sensitive_env_var?(key)
|
244
|
+
sensitive_patterns = %w[
|
245
|
+
PASSWORD SECRET TOKEN KEY API PRIVATE
|
246
|
+
AWS GCP AZURE GITHUB GITLAB
|
247
|
+
DATABASE_URL DB_PASSWORD
|
248
|
+
RAILS_MASTER_KEY
|
249
|
+
]
|
250
|
+
|
251
|
+
sensitive_patterns.any? { |pattern| key.to_s.upcase.include?(pattern) }
|
252
|
+
end
|
253
|
+
|
254
|
+
# Check if content contains dangerous patterns
|
255
|
+
def contains_dangerous_content?(content)
|
256
|
+
DANGEROUS_PATTERNS.any? { |pattern| content.match?(pattern) }
|
257
|
+
end
|
258
|
+
|
259
|
+
def limits
|
260
|
+
options[:limits]
|
261
|
+
end
|
262
|
+
|
263
|
+
def blocked_commands
|
264
|
+
options[:blocked_commands]
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
@@ -0,0 +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
|