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,544 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+ require_relative 'config'
5
+
6
+ module Ukiryu
7
+ # Ukiryu Logger with level-based message classification
8
+ #
9
+ # Provides structured logging with support for:
10
+ # - Debug mode controlled by UKIRYU_DEBUG environment variable
11
+ # - Colored output via Paint gem (when available)
12
+ # - Message classification (debug, info, warn, error)
13
+ # - Structured output for tool resolution process
14
+ #
15
+ # @example Enable debug mode
16
+ # ENV['UKIRYU_DEBUG'] = '1'
17
+ # logger = Ukiryu::Logger.new
18
+ # logger.debug("Tool resolution started")
19
+ #
20
+ # @example Standard logging
21
+ # logger = Ukiryu::Logger.new
22
+ # logger.info("Command completed successfully")
23
+ # logger.warn("Tool not found: #{name}")
24
+ # logger.error("Execution failed: #{message}")
25
+ class Logger
26
+ # Log levels
27
+ LEVELS = %i[debug info warn error].freeze
28
+
29
+ attr_reader :level, :logger, :output, :paint_available
30
+
31
+ # Initialize a new Logger
32
+ #
33
+ # @param output [IO] the output stream (default: $stderr for debug, $stdout for info/warn/error)
34
+ # @param level [Symbol] the log level (default: :warn)
35
+ def initialize(output: nil, level: nil)
36
+ @output = output || $stderr
37
+ @logger = ::Logger.new(@output)
38
+ @logger.level = ::Logger::WARN
39
+ @paint_available = false
40
+
41
+ # Check if debug mode is enabled via Config system
42
+ @debug_mode = Config.debug
43
+
44
+ # Try to load Paint for colored output
45
+ if @debug_mode
46
+ begin
47
+ require 'paint'
48
+ @paint_available = true
49
+ rescue LoadError
50
+ # Paint not available, fall back to plain text
51
+ end
52
+ end
53
+
54
+ # Set log level based on debug mode or explicit level
55
+ set_level(level) if level
56
+ end
57
+
58
+ # Set the log level
59
+ #
60
+ # @param level [Symbol] the log level (:debug, :info, :warn, :error)
61
+ def set_level(level)
62
+ level_sym = level.to_sym
63
+ raise ArgumentError, "Invalid log level: #{level}" unless LEVELS.include?(level_sym)
64
+
65
+ @level = level_sym
66
+ @logger.level = case level_sym
67
+ when :debug then ::Logger::DEBUG
68
+ when :info then ::Logger::INFO
69
+ when :warn then ::Logger::WARN
70
+ when :error then ::Logger::ERROR
71
+ end
72
+ end
73
+
74
+ # Log a debug message (only when UKIRYU_DEBUG is enabled)
75
+ #
76
+ # @param message [String] the message
77
+ # @param context [Hash] optional context data
78
+ def debug(message, context = {})
79
+ return unless @debug_mode
80
+
81
+ @output.puts(format_message('DEBUG', message, :cyan, context))
82
+ end
83
+
84
+ # Log an info message
85
+ #
86
+ # @param message [String] the message
87
+ # @param context [Hash] optional context data
88
+ def info(message, context = {})
89
+ @output.puts(format_message('INFO', message, :green, context))
90
+ end
91
+
92
+ # Log a warning message
93
+ #
94
+ # @param message [String] the message
95
+ # @param context [Hash] optional context data
96
+ def warn(message, context = {})
97
+ @output.puts(format_message('WARN', message, :yellow, context))
98
+ end
99
+
100
+ # Log an error message
101
+ #
102
+ # @param message [String] the message
103
+ # @param context [Hash] optional context data
104
+ def error(message, context = {})
105
+ @output.puts(format_message('ERROR', message, :red, context))
106
+ end
107
+
108
+ # Check if debug mode is enabled
109
+ #
110
+ # @return [Boolean] true if debug mode is enabled
111
+ def debug_enabled?
112
+ @debug_mode
113
+ end
114
+
115
+ # Log structured tool resolution debug information
116
+ #
117
+ # @param identifier [String] the tool identifier being resolved
118
+ # @param step [Symbol] the resolution step (:header, :context, :step, :result, :not_found)
119
+ # @param data [Hash] the step-specific data
120
+ def debug_resolution(identifier, step, data = {})
121
+ return unless @debug_mode
122
+
123
+ case step
124
+ when :header
125
+ debug_header(identifier)
126
+ when :context
127
+ debug_context(data[:platform], data[:shell], data[:all_tools])
128
+ when :step
129
+ debug_step(data[:tool_name], data[:tool_def], data[:interface_match], data[:cached])
130
+ when :result
131
+ debug_result(identifier, data[:tool_name], data[:executable])
132
+ when :not_found
133
+ debug_not_found(identifier)
134
+ end
135
+ end
136
+
137
+ # Debug section: Ukiryu CLI Options
138
+ # Shows the options passed to the Ukiryu CLI itself (not the tool options)
139
+ #
140
+ # @param options [Hash] the Ukiryu CLI options
141
+ def debug_section_ukiryu_options(options)
142
+ return unless @debug_mode
143
+
144
+ debug_section_header('Ukiryu CLI Options')
145
+ options.each do |key, value|
146
+ debug_field(key.to_s, value.inspect, boxed: false)
147
+ end
148
+ debug_section_footer
149
+ end
150
+
151
+ # Debug section: Tool Resolution
152
+ # Shows the tool resolution process with bordered style
153
+ #
154
+ # @param identifier [String] the tool identifier being resolved
155
+ # @param platform [Symbol] the detected platform
156
+ # @param shell [Symbol] the detected shell
157
+ # @param all_tools [Array<String>] list of all available tools
158
+ # @param selected_tool [String] the selected tool name
159
+ # @param executable [String] the path to the executable
160
+ def debug_section_tool_resolution(identifier:, platform:, shell:, all_tools:, selected_tool:, executable:)
161
+ return unless @debug_mode
162
+
163
+ debug_section_header("Tool Resolution: #{identifier}")
164
+
165
+ debug_field('Platform', platform.to_s, boxed: false)
166
+ debug_field('Shell', shell.to_s, boxed: false)
167
+ debug_field('Available Tools', all_tools.count.to_s, boxed: false)
168
+
169
+ @output.puts ''
170
+ @output.puts " #{all_tools.sort.join(' • ')}"
171
+
172
+ if @paint_available
173
+ paint = Paint.method(:[])
174
+ @output.puts ''
175
+ @output.puts "#{paint[' ✓', :green]} #{paint[selected_tool, :cyan, :bright]}#{paint[' implements: ', :white]}#{paint[identifier, :yellow]}"
176
+ else
177
+ @output.puts ''
178
+ @output.puts " ✓ #{selected_tool} implements: #{identifier}"
179
+ end
180
+ @output.puts ''
181
+ debug_field('Selected', selected_tool, boxed: false)
182
+ debug_field('Executable', executable, boxed: false)
183
+
184
+ debug_section_footer
185
+ end
186
+
187
+ # Debug section: Tool Not Found
188
+ # Shows the tool not found error with bordered style
189
+ #
190
+ # @param identifier [String] the tool identifier being resolved
191
+ # @param platform [Symbol] the detected platform
192
+ # @param shell [Symbol] the detected shell
193
+ # @param all_tools [Array<String>] list of all available tools
194
+ def debug_section_tool_not_found(identifier:, platform:, shell:, all_tools:)
195
+ return unless @debug_mode
196
+
197
+ debug_section_header("Tool Resolution: #{identifier}")
198
+
199
+ debug_field('Platform', platform.to_s, boxed: false)
200
+ debug_field('Shell', shell.to_s, boxed: false)
201
+ debug_field('Available Tools', all_tools.count.to_s, boxed: false)
202
+
203
+ @output.puts ''
204
+ @output.puts " #{all_tools.sort.join(' • ')}"
205
+ @output.puts ''
206
+
207
+ if @paint_available
208
+ paint = Paint.method(:[])
209
+ @output.puts "#{paint[' ✗', :red]} #{paint['Tool not found', :red, :bright]}"
210
+ else
211
+ @output.puts ' ✗ Tool not found'
212
+ end
213
+
214
+ debug_section_footer
215
+ end
216
+
217
+ # Debug section: Structured Options (Tool Command Options)
218
+ # Shows the structured options object that will be passed to the executable
219
+ #
220
+ # @param tool_name [String] the tool name
221
+ # @param command_name [String] the command name
222
+ # @param options_object [Object] the structured options object
223
+ def debug_section_structured_options(tool_name, command_name, options_object)
224
+ return unless @debug_mode
225
+
226
+ require_relative 'models/arguments'
227
+ debug_section_header("Structured Options (#{tool_name} #{command_name})")
228
+
229
+ # Show the options object's attributes
230
+ if options_object.respond_to?(:to_h)
231
+ options_object.to_h.each do |key, value|
232
+ debug_field(key.to_s, format_value(value), boxed: false)
233
+ end
234
+ elsif options_object.is_a?(Hash)
235
+ options_object.each do |key, value|
236
+ debug_field(key.to_s, format_value(value), boxed: false)
237
+ end
238
+ else
239
+ # Try to get instance variables
240
+ options_object.instance_variables.each do |var|
241
+ value = options_object.instance_variable_get(var)
242
+ debug_field(var.to_s.sub('@', ''), format_value(value), boxed: false)
243
+ end
244
+ end
245
+
246
+ debug_section_footer
247
+ end
248
+
249
+ # Debug section: Shell Command
250
+ # Shows the actual shell command that will be executed
251
+ #
252
+ # @param executable [String] the executable path
253
+ # @param full_command [String] the full command string
254
+ # @param env_vars [Hash] optional environment variables
255
+ def debug_section_shell_command(executable:, full_command:, env_vars: {})
256
+ return unless @debug_mode
257
+
258
+ debug_section_header('Shell Command')
259
+
260
+ debug_field('Executable', executable, boxed: false)
261
+ debug_field('Full Command', full_command, boxed: false)
262
+
263
+ unless env_vars.empty?
264
+ @output.puts ''
265
+ @output.puts ' Environment Variables:'
266
+ env_vars.each do |key, value|
267
+ @output.puts " #{key}=#{value}"
268
+ end
269
+ end
270
+
271
+ debug_section_footer
272
+ end
273
+
274
+ # Debug section: Raw Response
275
+ # Shows the raw output from the command
276
+ #
277
+ # @param stdout [String] the stdout from the command
278
+ # @param stderr [String] the stderr from the command
279
+ # @param exit_code [Integer] the exit code
280
+ def debug_section_raw_response(stdout:, stderr:, exit_code:)
281
+ return unless @debug_mode
282
+
283
+ debug_section_header('Raw Command Response')
284
+
285
+ debug_field('Exit Code', exit_code.to_s, boxed: false)
286
+
287
+ unless stdout.empty?
288
+ @output.puts ''
289
+ @output.puts ' STDOUT:'
290
+ stdout.each_line do |line|
291
+ @output.puts " #{line}"
292
+ end
293
+ end
294
+
295
+ unless stderr.empty?
296
+ @output.puts ''
297
+ @output.puts ' STDERR:'
298
+ stderr.each_line do |line|
299
+ @output.puts " #{line}"
300
+ end
301
+ end
302
+
303
+ debug_section_footer
304
+ end
305
+
306
+ # Debug section: Structured Response
307
+ # Shows the final structured response object
308
+ #
309
+ # @param response [Object] the response object
310
+ def debug_section_structured_response(response)
311
+ return unless @debug_mode
312
+
313
+ debug_section_header('Structured Response')
314
+
315
+ # Show response as YAML for readability
316
+ response_yaml = if response.respond_to?(:to_yaml)
317
+ response.to_yaml
318
+ else
319
+ response.inspect
320
+ end
321
+
322
+ response_yaml.each_line do |line|
323
+ @output.puts " #{line}"
324
+ end
325
+
326
+ debug_section_footer
327
+ end
328
+
329
+ # Debug section: Execution Report (metrics)
330
+ # Shows detailed metrics for each execution stage
331
+ #
332
+ # @param execution_report [ExecutionReport] the execution report
333
+ def debug_section_execution_report(execution_report)
334
+ return unless @debug_mode
335
+
336
+ debug_section_header('Execution Report')
337
+
338
+ @output.puts ' Run Environment:'
339
+ format_env_field(@output, 'Hostname', execution_report.run_environment.hostname)
340
+ format_env_field(@output, 'Platform', execution_report.run_environment.platform)
341
+ format_env_field(@output, 'OS Version', execution_report.run_environment.os_version)
342
+ format_env_field(@output, 'Shell', execution_report.run_environment.shell)
343
+ format_env_field(@output, 'Ruby', execution_report.run_environment.ruby_version)
344
+ format_env_field(@output, 'Ukiryu', execution_report.run_environment.ukiryu_version)
345
+ format_env_field(@output, 'CPUs', execution_report.run_environment.cpu_count.to_s)
346
+ format_env_field(@output, 'Memory', "#{execution_report.run_environment.total_memory}GB")
347
+
348
+ @output.puts ''
349
+
350
+ @output.puts ' Stage Timings:'
351
+ execution_report.all_stages.each do |stage|
352
+ @output.puts " #{stage.name.ljust(20)}: #{stage.formatted_duration.ljust(10)} " \
353
+ "(#{stage.memory_delta}KB)" \
354
+ "#{stage.success ? '' : ' - FAILED'}"
355
+ @output.puts " #{stage.error}" unless stage.success
356
+ end
357
+
358
+ @output.puts ''
359
+ @output.puts " Total: #{execution_report.formatted_total_duration}"
360
+
361
+ debug_section_footer
362
+ end
363
+
364
+ private
365
+
366
+ # Format an environment field for debug output
367
+ def format_env_field(output, label, value)
368
+ output.puts " #{label.ljust(15)}: #{value}"
369
+ end
370
+
371
+ # Format a value for debug display
372
+ def format_value(value)
373
+ case value
374
+ when Array
375
+ "[#{value.map(&:inspect).join(', ')}]"
376
+ when Hash
377
+ "{#{value.map { |k, v| "#{k.inspect}: #{v.inspect}" }.join(', ')}}"
378
+ when String, Numeric, TrueClass, FalseClass, NilClass
379
+ value.inspect
380
+ else
381
+ value.to_s
382
+ end
383
+ end
384
+
385
+ # Print a debug section header (box enclosed)
386
+ def debug_section_header(title)
387
+ if @paint_available
388
+ paint = Paint.method(:[])
389
+ @output.puts ''
390
+ @output.puts paint["┌─ #{title} #{'─' * [75 - title.length - 3, 3].max}", :cyan]
391
+ else
392
+ @output.puts ''
393
+ @output.puts "┌─ #{title} #{'─' * [75 - title.length - 3, 3].max}"
394
+ end
395
+ end
396
+
397
+ # Print a debug section footer
398
+ def debug_section_footer
399
+ if @paint_available
400
+ paint = Paint.method(:[])
401
+ @output.puts paint["└#{'─' * 75}", :cyan]
402
+ else
403
+ @output.puts "└#{'─' * 75}"
404
+ end
405
+ end
406
+
407
+ # Print a debug field
408
+ # @param label [String] the field label
409
+ # @param value [String] the field value
410
+ # @param boxed [Boolean] whether to draw box borders around the field
411
+ def debug_field(label, value, boxed: true)
412
+ if boxed
413
+ if @paint_available
414
+ paint = Paint.method(:[])
415
+ @output.puts paint['║ ', :cyan] +
416
+ paint[label.to_s.ljust(20), :white] +
417
+ paint[': ', :cyan] +
418
+ paint[value.to_s.ljust(41), :yellow] +
419
+ paint['║', :cyan]
420
+ else
421
+ @output.puts "│ #{label.to_s.ljust(20)}: #{value.to_s.ljust(41)}│"
422
+ end
423
+ elsif @paint_available
424
+ paint = Paint.method(:[])
425
+ @output.puts paint[' ', :cyan] +
426
+ paint[label.to_s.ljust(20), :white] +
427
+ paint[': ', :cyan] +
428
+ paint[value.to_s, :yellow]
429
+ else
430
+ @output.puts " #{label.to_s.ljust(20)}: #{value}"
431
+ end
432
+ end
433
+
434
+ # Format a log message with color and context
435
+ def format_message(level, message, color, context)
436
+ timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S')
437
+ formatted = if @paint_available
438
+ paint = Paint.method(:[])
439
+ level_str = paint["[#{level}]", color]
440
+ time_str = timestamp # No color - let terminal decide
441
+ "#{time_str} #{level_str} #{message}"
442
+ else
443
+ "[#{timestamp}] [#{level}] #{message}"
444
+ end
445
+
446
+ # Add context if provided
447
+ unless context.empty?
448
+ context_str = context.map { |k, v| "#{k}=#{v}" }.join(' ')
449
+ formatted << " #{context_str}"
450
+ end
451
+
452
+ formatted
453
+ end
454
+
455
+ # Format debug header
456
+ def debug_header(identifier)
457
+ if @paint_available
458
+ paint = Paint.method(:[])
459
+ header = paint['🔍 Tool Resolution: ', :cyan] + paint[identifier, :yellow, :bright]
460
+ separator = paint['─' * 60, :cyan]
461
+ else
462
+ header = "🔍 Tool Resolution: #{identifier}"
463
+ separator = '─' * 60
464
+ end
465
+ @output.puts "\n#{header}\n#{separator}\n"
466
+ end
467
+
468
+ # Format debug context
469
+ def debug_context(platform, shell, all_tools)
470
+ if @paint_available
471
+ paint = Paint.method(:[])
472
+ platform_str = paint['Platform: ', :white] + paint[platform.to_s, :green]
473
+ shell_str = paint['Shell: ', :white] + paint[shell.to_s, :green]
474
+ tools_str = paint["Available Tools (#{all_tools.count}): ", :white] +
475
+ paint[all_tools.count.to_s, :yellow] + paint[' tools', :white]
476
+ tools_list = paint['• ', :cyan] + all_tools.sort.join(paint[' • ', :cyan])
477
+ else
478
+ platform_str = "Platform: #{platform}"
479
+ shell_str = "Shell: #{shell}"
480
+ tools_str = "Available Tools (#{all_tools.count}): #{all_tools.count} tools"
481
+ tools_list = "• #{all_tools.sort.join(' • ')}"
482
+ end
483
+ @output.puts "#{platform_str} | #{shell_str}\n#{tools_str}"
484
+ @output.puts " #{tools_list}\n"
485
+ end
486
+
487
+ # Format debug step
488
+ def debug_step(tool_name, tool_def, interface_match, cached = false)
489
+ if @paint_available
490
+ paint = Paint.method(:[])
491
+ status_icon = interface_match ? paint['✓', :green] : paint['◆', :yellow]
492
+ cached_str = cached ? ' (cached)' : '' # No color - let terminal decide
493
+
494
+ @output.puts paint[" #{status_icon} ", :white] +
495
+ paint[tool_name, :cyan, :bright] +
496
+ cached_str +
497
+ paint[' implements: ', :white] +
498
+ (tool_def.implements ? paint[tool_def.implements, :yellow] : 'none')
499
+ else
500
+ status_icon = interface_match ? '✓' : '◆'
501
+ cached_str = cached ? ' (cached)' : ''
502
+ @output.puts " #{status_icon} #{tool_name}#{cached_str} implements: #{tool_def.implements || 'none'}"
503
+ end
504
+ end
505
+
506
+ # Format debug result
507
+ def debug_result(_identifier, tool_name, executable)
508
+ if @paint_available
509
+ paint = Paint.method(:[])
510
+ separator = paint['─' * 60, :cyan]
511
+ result_icon = paint['✅', :green]
512
+
513
+ @output.puts "\n#{result_icon} " +
514
+ paint['Selected: ', :white] +
515
+ paint[tool_name, :cyan, :bright] +
516
+ paint[' | Executable: ', :white] +
517
+ paint[executable, :yellow]
518
+ else
519
+ separator = '─' * 60
520
+ result_icon = '✅'
521
+ @output.puts "\n#{result_icon} Selected: #{tool_name} | Executable: #{executable}"
522
+ end
523
+ @output.puts "#{separator}\n"
524
+ end
525
+
526
+ # Format debug not found
527
+ def debug_not_found(identifier)
528
+ if @paint_available
529
+ paint = Paint.method(:[])
530
+ separator = paint['─' * 60, :red]
531
+ error_icon = paint['❌', :red]
532
+
533
+ @output.puts "\n#{error_icon} " +
534
+ paint['Tool not found: ', :white] +
535
+ paint[identifier, :red, :bright]
536
+ else
537
+ separator = '─' * 60
538
+ error_icon = '❌'
539
+ @output.puts "\n#{error_icon} Tool not found: #{identifier}"
540
+ end
541
+ @output.puts "#{separator}\n"
542
+ end
543
+ end
544
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lutaml/model'
4
+
5
+ module Ukiryu
6
+ module Models
7
+ # A single command argument
8
+ #
9
+ # Represents one argument with its name, value, and type information.
10
+ class Argument < Lutaml::Model::Serializable
11
+ attribute :name, :string
12
+ attribute :value, :string
13
+ attribute :type, :string, default: 'argument'
14
+
15
+ yaml do
16
+ map_element 'name', to: :name
17
+ map_element 'value', to: :value
18
+ map_element 'type', to: :type
19
+ end
20
+
21
+ json do
22
+ map 'name', to: :name
23
+ map 'value', to: :value
24
+ map 'type', to: :type
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lutaml/model'
4
+
5
+ module Ukiryu
6
+ module Models
7
+ # Argument definition for a command
8
+ #
9
+ # @example
10
+ # arg = ArgumentDefinition.new(
11
+ # name: 'input',
12
+ # type: 'file',
13
+ # variadic: true,
14
+ # position: 'last'
15
+ # )
16
+ class ArgumentDefinition < Lutaml::Model::Serializable
17
+ attribute :name, :string
18
+ attribute :type, :string, default: 'string'
19
+ attribute :required, :boolean, default: false
20
+ attribute :position, :string, default: '99'
21
+ attribute :variadic, :boolean, default: false
22
+ attribute :min, :integer
23
+ attribute :max, :integer
24
+ # Can be Integer or Array[Integer]
25
+ attribute :size, :integer, collection: true
26
+ # Type of array elements
27
+ attribute :of, :string
28
+ # Array for numeric range [min, max]
29
+ attribute :range, :integer, collection: true
30
+ # Valid values for symbols
31
+ attribute :values, :string, collection: true
32
+ attribute :separator, :string, default: ' '
33
+ attribute :format, :string
34
+ attribute :description, :string
35
+
36
+ yaml do
37
+ map_element 'name', to: :name
38
+ map_element 'type', to: :type
39
+ map_element 'required', to: :required
40
+ map_element 'position', to: :position
41
+ map_element 'variadic', to: :variadic
42
+ map_element 'min', to: :min
43
+ map_element 'max', to: :max
44
+ map_element 'size', to: :size
45
+ map_element 'of', to: :of
46
+ map_element 'range', to: :range
47
+ map_element 'values', to: :values
48
+ map_element 'separator', to: :separator
49
+ map_element 'format', to: :format
50
+ map_element 'description', to: :description
51
+ end
52
+
53
+ # Check if this is the last argument
54
+ #
55
+ # @return [Boolean] true if position is :last
56
+ def last?
57
+ position == 'last'
58
+ end
59
+
60
+ # Get the position as symbol or integer
61
+ #
62
+ # @return [Symbol, Integer] the parsed position
63
+ def parsed_position
64
+ case position
65
+ when 'last'
66
+ :last
67
+ when 'first'
68
+ :first
69
+ when /^\d+$/
70
+ position.to_i
71
+ else
72
+ 99
73
+ end
74
+ end
75
+
76
+ # Get numeric position for sorting
77
+ #
78
+ # @return [Integer] the numeric position
79
+ def numeric_position
80
+ case position
81
+ when 'last'
82
+ 999
83
+ when 'first'
84
+ 1
85
+ when /^\d+$/
86
+ position.to_i
87
+ else
88
+ 99
89
+ end
90
+ end
91
+
92
+ # Hash-like access for Type validation compatibility
93
+ #
94
+ # @param key [Symbol, String] the attribute key
95
+ # @return [Object] the attribute value
96
+ def [](key)
97
+ key_sym = key.to_sym
98
+ # Return nil for unknown keys (like Type validation options)
99
+ return nil unless respond_to?(key_sym, true)
100
+
101
+ send(key_sym)
102
+ end
103
+
104
+ # Get name as symbol (cached for performance)
105
+ #
106
+ # @return [Symbol] the name as symbol
107
+ def name_sym
108
+ @name_sym ||= name.to_sym
109
+ end
110
+
111
+ # Get type as symbol (cached for performance)
112
+ #
113
+ # @return [Symbol] the type as symbol
114
+ def type_sym
115
+ @type_sym ||= type.to_sym
116
+ end
117
+ end
118
+ end
119
+ end