ukiryu 0.1.0 → 0.1.1

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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/docs.yml +63 -0
  3. data/.github/workflows/links.yml +99 -0
  4. data/.github/workflows/rake.yml +19 -0
  5. data/.github/workflows/release.yml +27 -0
  6. data/.gitignore +18 -4
  7. data/.rubocop.yml +1 -0
  8. data/.rubocop_todo.yml +213 -0
  9. data/Gemfile +12 -8
  10. data/README.adoc +613 -0
  11. data/Rakefile +2 -2
  12. data/docs/assets/logo.svg +1 -0
  13. data/exe/ukiryu +11 -0
  14. data/lib/ukiryu/action/base.rb +77 -0
  15. data/lib/ukiryu/cache.rb +199 -0
  16. data/lib/ukiryu/cli.rb +133 -307
  17. data/lib/ukiryu/cli_commands/base_command.rb +155 -0
  18. data/lib/ukiryu/cli_commands/commands_command.rb +120 -0
  19. data/lib/ukiryu/cli_commands/commands_command.rb.fixed +40 -0
  20. data/lib/ukiryu/cli_commands/config_command.rb +249 -0
  21. data/lib/ukiryu/cli_commands/describe_command.rb +326 -0
  22. data/lib/ukiryu/cli_commands/describe_command.rb.fixed +254 -0
  23. data/lib/ukiryu/cli_commands/exec_inline_command.rb.fixed +180 -0
  24. data/lib/ukiryu/cli_commands/extract_command.rb +84 -0
  25. data/lib/ukiryu/cli_commands/info_command.rb +156 -0
  26. data/lib/ukiryu/cli_commands/list_command.rb +70 -0
  27. data/lib/ukiryu/cli_commands/opts_command.rb +106 -0
  28. data/lib/ukiryu/cli_commands/opts_command.rb.fixed +105 -0
  29. data/lib/ukiryu/cli_commands/response_formatter.rb +240 -0
  30. data/lib/ukiryu/cli_commands/run_command.rb +375 -0
  31. data/lib/ukiryu/cli_commands/run_file_command.rb +215 -0
  32. data/lib/ukiryu/cli_commands/system_command.rb +90 -0
  33. data/lib/ukiryu/cli_commands/validate_command.rb +87 -0
  34. data/lib/ukiryu/cli_commands/version_command.rb +16 -0
  35. data/lib/ukiryu/cli_commands/which_command.rb +166 -0
  36. data/lib/ukiryu/command_builder.rb +205 -0
  37. data/lib/ukiryu/config/env_provider.rb +64 -0
  38. data/lib/ukiryu/config/env_schema.rb +63 -0
  39. data/lib/ukiryu/config/override_resolver.rb +68 -0
  40. data/lib/ukiryu/config/type_converter.rb +59 -0
  41. data/lib/ukiryu/config.rb +249 -0
  42. data/lib/ukiryu/errors.rb +3 -0
  43. data/lib/ukiryu/executable_locator.rb +114 -0
  44. data/lib/ukiryu/execution/command_info.rb +64 -0
  45. data/lib/ukiryu/execution/metadata.rb +97 -0
  46. data/lib/ukiryu/execution/output.rb +144 -0
  47. data/lib/ukiryu/execution/result.rb +194 -0
  48. data/lib/ukiryu/execution.rb +15 -0
  49. data/lib/ukiryu/execution_context.rb +251 -0
  50. data/lib/ukiryu/executor.rb +76 -493
  51. data/lib/ukiryu/extractors/base_extractor.rb +63 -0
  52. data/lib/ukiryu/extractors/extractor.rb +150 -0
  53. data/lib/ukiryu/extractors/help_parser.rb +188 -0
  54. data/lib/ukiryu/extractors/native_extractor.rb +47 -0
  55. data/lib/ukiryu/io.rb +196 -0
  56. data/lib/ukiryu/logger.rb +544 -0
  57. data/lib/ukiryu/models/argument.rb +28 -0
  58. data/lib/ukiryu/models/argument_definition.rb +119 -0
  59. data/lib/ukiryu/models/arguments.rb +113 -0
  60. data/lib/ukiryu/models/command_definition.rb +176 -0
  61. data/lib/ukiryu/models/command_info.rb +37 -0
  62. data/lib/ukiryu/models/components.rb +107 -0
  63. data/lib/ukiryu/models/env_var_definition.rb +30 -0
  64. data/lib/ukiryu/models/error_response.rb +41 -0
  65. data/lib/ukiryu/models/execution_metadata.rb +31 -0
  66. data/lib/ukiryu/models/execution_report.rb +236 -0
  67. data/lib/ukiryu/models/exit_codes.rb +74 -0
  68. data/lib/ukiryu/models/flag_definition.rb +67 -0
  69. data/lib/ukiryu/models/option_definition.rb +102 -0
  70. data/lib/ukiryu/models/output_info.rb +25 -0
  71. data/lib/ukiryu/models/platform_profile.rb +153 -0
  72. data/lib/ukiryu/models/routing.rb +211 -0
  73. data/lib/ukiryu/models/search_paths.rb +39 -0
  74. data/lib/ukiryu/models/success_response.rb +85 -0
  75. data/lib/ukiryu/models/tool_definition.rb +145 -0
  76. data/lib/ukiryu/models/tool_metadata.rb +82 -0
  77. data/lib/ukiryu/models/validation_result.rb +80 -0
  78. data/lib/ukiryu/models/version_compatibility.rb +152 -0
  79. data/lib/ukiryu/models/version_detection.rb +39 -0
  80. data/lib/ukiryu/models.rb +23 -0
  81. data/lib/ukiryu/options/base.rb +95 -0
  82. data/lib/ukiryu/options_builder/formatter.rb +87 -0
  83. data/lib/ukiryu/options_builder/validator.rb +43 -0
  84. data/lib/ukiryu/options_builder.rb +311 -0
  85. data/lib/ukiryu/platform.rb +6 -6
  86. data/lib/ukiryu/registry.rb +143 -183
  87. data/lib/ukiryu/response/base.rb +217 -0
  88. data/lib/ukiryu/runtime.rb +179 -0
  89. data/lib/ukiryu/schema_validator.rb +8 -10
  90. data/lib/ukiryu/shell/bash.rb +3 -3
  91. data/lib/ukiryu/shell/cmd.rb +4 -4
  92. data/lib/ukiryu/shell/fish.rb +1 -1
  93. data/lib/ukiryu/shell/powershell.rb +3 -3
  94. data/lib/ukiryu/shell/sh.rb +1 -1
  95. data/lib/ukiryu/shell/zsh.rb +1 -1
  96. data/lib/ukiryu/shell.rb +146 -39
  97. data/lib/ukiryu/thor_ext.rb +208 -0
  98. data/lib/ukiryu/tool.rb +649 -258
  99. data/lib/ukiryu/tool_index.rb +224 -0
  100. data/lib/ukiryu/tools/base.rb +381 -0
  101. data/lib/ukiryu/tools/class_generator.rb +132 -0
  102. data/lib/ukiryu/tools/executable_finder.rb +29 -0
  103. data/lib/ukiryu/tools/generator.rb +154 -0
  104. data/lib/ukiryu/tools.rb +109 -0
  105. data/lib/ukiryu/type.rb +28 -43
  106. data/lib/ukiryu/validation/constraints.rb +281 -0
  107. data/lib/ukiryu/validation/validator.rb +188 -0
  108. data/lib/ukiryu/validation.rb +21 -0
  109. data/lib/ukiryu/version.rb +1 -1
  110. data/lib/ukiryu/version_detector.rb +51 -0
  111. data/lib/ukiryu.rb +31 -15
  112. data/ukiryu-proposal.md +2952 -0
  113. data/ukiryu.gemspec +18 -14
  114. metadata +137 -5
  115. data/.github/workflows/test.yml +0 -143
@@ -0,0 +1,311 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'shell'
4
+ require_relative 'type'
5
+ require_relative 'cache'
6
+ require_relative 'options_builder/formatter'
7
+ require_relative 'options_builder/validator'
8
+
9
+ module Ukiryu
10
+ # Builds structured option classes from tool profile metadata
11
+ #
12
+ # This module dynamically creates option classes based on YAML profile definitions,
13
+ # providing type-safe option building with shell serialization capabilities.
14
+ #
15
+ # @example
16
+ # # Get options class for a command
17
+ # options_class = Ukiryu::OptionsBuilder.for(:imagemagick, :convert)
18
+ # options = options_class.new
19
+ # options.inputs = ["input.png"]
20
+ # options.output = "output.jpg"
21
+ # options.resize = "50x50"
22
+ #
23
+ # # Serialize to shell command
24
+ # shell_args = options.to_shell(type: :bash)
25
+ module OptionsBuilder
26
+ class << self
27
+ # Get the options classes cache (bounded LRU cache)
28
+ #
29
+ # @return [Cache] the options classes cache
30
+ def option_classes_cache
31
+ @option_classes_cache ||= Cache.new(max_size: 100, ttl: 3600)
32
+ end
33
+
34
+ # Get or create an options class for a tool command
35
+ #
36
+ # @param tool_name [String, Symbol] the tool name
37
+ # @param command_name [String, Symbol] the command name
38
+ # @return [Class] the dynamically generated options class
39
+ def for(tool_name, command_name)
40
+ tool_name_sym = tool_name.to_sym
41
+ command_name_sym = command_name.to_sym
42
+
43
+ # Check if we've already created this class
44
+ cache_key = [tool_name_sym, command_name_sym]
45
+ cached = option_classes_cache[cache_key]
46
+ return cached if cached
47
+
48
+ # Get the tool and command profile
49
+ tool = Tool.get(tool_name_sym)
50
+ command_def = tool.command_definition(command_name_sym)
51
+
52
+ raise ArgumentError, "Unknown command: #{command_name} for tool: #{tool_name}" unless command_def
53
+
54
+ # Create the options class
55
+ options_class = create_options_class(tool_name_sym, command_name_sym, command_def)
56
+ option_classes_cache[cache_key] = options_class
57
+ options_class
58
+ end
59
+
60
+ # Clear the options class cache (mainly for testing)
61
+ #
62
+ # @api private
63
+ def clear_cache
64
+ option_classes_cache.clear
65
+ end
66
+
67
+ # Convert an options object to a hash for backward compatibility
68
+ #
69
+ # @param options [Object] an options object created by the options class
70
+ # @return [Hash] the options as a hash
71
+ def to_hash(options)
72
+ hash = {}
73
+
74
+ # Get the command definition from the options class
75
+ command_def = options.class.command_def
76
+ return {} unless command_def
77
+
78
+ # Extract all argument values
79
+ (command_def.arguments || []).each do |arg_def|
80
+ attr_name = arg_def.name
81
+ value = options.send(attr_name)
82
+ hash[attr_name.to_sym] = value unless value.nil?
83
+ end
84
+
85
+ # Extract all option values
86
+ (command_def.options || []).each do |opt_def|
87
+ attr_name = opt_def.name
88
+ value = options.send(attr_name)
89
+ hash[attr_name.to_sym] = value unless value.nil?
90
+ end
91
+
92
+ # Extract all flag values
93
+ (command_def.flags || []).each do |flag_def|
94
+ attr_name = flag_def.name
95
+ value = options.send(attr_name)
96
+ # Only include flags that are true
97
+ hash[attr_name.to_sym] = value if value
98
+ end
99
+
100
+ # Extract all post_option values
101
+ (command_def.post_options || []).each do |opt_def|
102
+ attr_name = opt_def.name
103
+ value = options.send(attr_name)
104
+ hash[attr_name.to_sym] = value unless value.nil?
105
+ end
106
+
107
+ # Include extra_args if present (for manual option injection)
108
+ hash[:extra_args] = options.extra_args if options.respond_to?(:extra_args) && !options.extra_args.nil?
109
+
110
+ hash
111
+ end
112
+
113
+ # Create a dynamic options class from command definition
114
+ #
115
+ # @param tool_name [Symbol] the tool name
116
+ # @param command_name [Symbol] the command name
117
+ # @param command_def [Hash] the command definition from profile
118
+ # @return [Class] the generated options class
119
+ def create_options_class(tool_name, command_name, command_def)
120
+ # Capture values in closure for singleton methods
121
+ cmd_def = command_def
122
+ t_name = tool_name
123
+ c_name = command_name
124
+
125
+ Class.new do
126
+ # Define class methods with closure access
127
+ singleton_class.send(:define_method, :command_def) do
128
+ cmd_def
129
+ end
130
+
131
+ singleton_class.send(:define_method, :tool_name) do
132
+ t_name
133
+ end
134
+
135
+ singleton_class.send(:define_method, :command_name) do
136
+ c_name
137
+ end
138
+
139
+ # Define attribute accessors for each argument and option
140
+ OptionsBuilder.define_accessors(self, command_def)
141
+
142
+ # Define to_shell method for serialization
143
+ OptionsBuilder.define_to_shell_method(self, command_def)
144
+
145
+ # Define validation method
146
+ OptionsBuilder::Validator.define_validation_method(self, command_def)
147
+ end
148
+ end
149
+
150
+ # Define attribute accessors for arguments and options
151
+ #
152
+ # @param klass [Class] the class to define accessors on
153
+ # @param command_def [CommandDefinition] the command definition
154
+ def define_accessors(klass, command_def)
155
+ # Define accessors for arguments
156
+ (command_def.arguments || []).each do |arg_def|
157
+ attr_name = arg_def.name
158
+ # Create getter and setter
159
+ klass.define_method(attr_name) do
160
+ instance_variable_get("@#{attr_name}")
161
+ end
162
+
163
+ klass.define_method("#{attr_name}=") do |value|
164
+ # For variadic arguments, validate each element
165
+ validated = if arg_def.variadic && value.is_a?(Array)
166
+ value.map { |v| Type.validate(v, arg_def.type || :string, arg_def) }
167
+ else
168
+ Type.validate(value, arg_def.type || :string, arg_def)
169
+ end
170
+ instance_variable_set("@#{attr_name}", validated)
171
+ end
172
+ end
173
+
174
+ # Define accessors for options
175
+ (command_def.options || []).each do |opt_def|
176
+ attr_name = opt_def.name
177
+ klass.define_method(attr_name) do
178
+ instance_variable_get("@#{attr_name}")
179
+ end
180
+
181
+ klass.define_method("#{attr_name}=") do |value|
182
+ # Skip if nil (optional option not set)
183
+ return if value.nil?
184
+
185
+ # Validate and coerce the value
186
+ validated = Type.validate(value, opt_def.type || :string, opt_def)
187
+ instance_variable_set("@#{attr_name}", validated)
188
+ end
189
+ end
190
+
191
+ # Define accessors for flags
192
+ (command_def.flags || []).each do |flag_def|
193
+ attr_name = flag_def.name
194
+ klass.define_method(attr_name) do
195
+ instance_variable_get("@#{attr_name}")
196
+ end
197
+
198
+ klass.define_method("#{attr_name}=") do |value|
199
+ instance_variable_set("@#{attr_name}", !!value)
200
+ end
201
+ end
202
+
203
+ # Define accessors for post_options
204
+ (command_def.post_options || []).each do |opt_def|
205
+ attr_name = opt_def.name
206
+ klass.define_method(attr_name) do
207
+ instance_variable_get("@#{attr_name}")
208
+ end
209
+
210
+ klass.define_method("#{attr_name}=") do |value|
211
+ return if value.nil?
212
+
213
+ validated = Type.validate(value, opt_def.type || :string, opt_def)
214
+ instance_variable_set("@#{attr_name}", validated)
215
+ end
216
+ end
217
+ end
218
+
219
+ # Define to_shell method for command serialization
220
+ #
221
+ # @param klass [Class] the class to define the method on
222
+ # @param command_def [CommandDefinition] the command definition
223
+ def define_to_shell_method(klass, command_def)
224
+ klass.define_method(:to_shell) do |shell_type: :bash|
225
+ shell_type = shell_type.to_sym
226
+ shell_class = Shell.class_for(shell_type)
227
+ shell_instance = shell_class.new
228
+
229
+ args = []
230
+
231
+ # Add subcommand if present
232
+ args << command_def.subcommand if command_def.subcommand
233
+
234
+ # Add options (before arguments)
235
+ (command_def.options || []).each do |opt_def|
236
+ attr_name = opt_def.name
237
+ value = instance_variable_get("@#{attr_name}")
238
+ next if value.nil? # Skip unset options
239
+
240
+ formatted = Formatter.format_option(opt_def, value, shell_instance)
241
+ Array(formatted).each { |a| args << a unless a.nil? || a.empty? }
242
+ end
243
+
244
+ # Add flags
245
+ (command_def.flags || []).each do |flag_def|
246
+ attr_name = flag_def.name
247
+ value = instance_variable_get("@#{attr_name}")
248
+
249
+ # Use default if not set
250
+ value = flag_def.default if value.nil?
251
+ next unless value
252
+
253
+ formatted = Formatter.format_flag(flag_def, shell_instance)
254
+ Array(formatted).each { |f| args << f unless f.nil? || f.empty? }
255
+ end
256
+
257
+ # Separate "last" positioned argument from other arguments
258
+ arguments = command_def.arguments || []
259
+ last_arg = arguments.find { |a| ['last', :last].include?(a.position) }
260
+ regular_args = arguments.reject { |a| ['last', :last].include?(a.position) }
261
+
262
+ # Add regular positional arguments (in order)
263
+ regular_args.sort_by do |a|
264
+ pos = a.position
265
+ pos.is_a?(Integer) ? pos : (pos || 99)
266
+ end.each do |arg_def|
267
+ attr_name = arg_def.name
268
+ value = instance_variable_get("@#{attr_name}")
269
+ next if value.nil?
270
+
271
+ if arg_def.variadic
272
+ Array(value).each do |v|
273
+ args << Formatter.format_arg(v, arg_def, shell_instance)
274
+ end
275
+ else
276
+ args << Formatter.format_arg(value, arg_def, shell_instance)
277
+ end
278
+ end
279
+
280
+ # Add post_options (between regular args and last arg)
281
+ (command_def.post_options || []).each do |opt_def|
282
+ attr_name = opt_def.name
283
+ value = instance_variable_get("@#{attr_name}")
284
+ next if value.nil?
285
+
286
+ formatted = Formatter.format_option(opt_def, value, shell_instance)
287
+ Array(formatted).each { |a| args << a unless a.nil? || a.empty? }
288
+ end
289
+
290
+ # Add the "last" positioned argument (typically output)
291
+ if last_arg
292
+ attr_name = last_arg.name
293
+ value = instance_variable_get("@#{attr_name}")
294
+ if value
295
+ if last_arg.variadic
296
+ Array(value).each do |v|
297
+ args << Formatter.format_arg(v, last_arg, shell_instance)
298
+ end
299
+ else
300
+ args << Formatter.format_arg(value, last_arg, shell_instance)
301
+ end
302
+ end
303
+ end
304
+
305
+ # Join into command string
306
+ shell_instance.join(*args)
307
+ end
308
+ end
309
+ end
310
+ end
311
+ end
@@ -20,7 +20,7 @@ module Ukiryu
20
20
  :linux
21
21
  else
22
22
  # Try to determine from RbConfig
23
- host_os = RbConfig::CONFIG["host_os"]
23
+ host_os = RbConfig::CONFIG['host_os']
24
24
  case host_os
25
25
  when /mswin|mingw|windows/i
26
26
  :windows
@@ -47,21 +47,21 @@ module Ukiryu
47
47
  #
48
48
  # @return [Boolean]
49
49
  def windows?
50
- Gem.win_platform? || RbConfig::CONFIG["host_os"] =~ /mswin|mingw|windows/i
50
+ Gem.win_platform? || RbConfig::CONFIG['host_os'] =~ /mswin|mingw|windows/i
51
51
  end
52
52
 
53
53
  # Check if running on macOS
54
54
  #
55
55
  # @return [Boolean]
56
56
  def macos?
57
- RbConfig::CONFIG["host_os"] =~ /darwin|mac os/i
57
+ RbConfig::CONFIG['host_os'] =~ /darwin|mac os/i
58
58
  end
59
59
 
60
60
  # Check if running on Linux
61
61
  #
62
62
  # @return [Boolean]
63
63
  def linux?
64
- RbConfig::CONFIG["host_os"] =~ /linux/i
64
+ RbConfig::CONFIG['host_os'] =~ /linux/i
65
65
  end
66
66
 
67
67
  # Check if running on a Unix-like system (macOS or Linux)
@@ -77,8 +77,8 @@ module Ukiryu
77
77
  # @return [Array<String>] array of directory paths
78
78
  def executable_search_paths
79
79
  @executable_search_paths ||= begin
80
- path_sep = windows? ? ";" : ":"
81
- (ENV["PATH"] || "").split(path_sep)
80
+ path_sep = windows? ? ';' : ':'
81
+ (ENV['PATH'] || '').split(path_sep)
82
82
  end
83
83
  end
84
84