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.
- checksums.yaml +4 -4
- data/.github/workflows/docs.yml +63 -0
- data/.github/workflows/links.yml +99 -0
- data/.github/workflows/rake.yml +19 -0
- data/.github/workflows/release.yml +27 -0
- data/.gitignore +18 -4
- data/.rubocop.yml +1 -0
- data/.rubocop_todo.yml +213 -0
- data/Gemfile +12 -8
- data/README.adoc +613 -0
- data/Rakefile +2 -2
- data/docs/assets/logo.svg +1 -0
- data/exe/ukiryu +11 -0
- data/lib/ukiryu/action/base.rb +77 -0
- data/lib/ukiryu/cache.rb +199 -0
- data/lib/ukiryu/cli.rb +133 -307
- data/lib/ukiryu/cli_commands/base_command.rb +155 -0
- data/lib/ukiryu/cli_commands/commands_command.rb +120 -0
- data/lib/ukiryu/cli_commands/commands_command.rb.fixed +40 -0
- data/lib/ukiryu/cli_commands/config_command.rb +249 -0
- data/lib/ukiryu/cli_commands/describe_command.rb +326 -0
- data/lib/ukiryu/cli_commands/describe_command.rb.fixed +254 -0
- data/lib/ukiryu/cli_commands/exec_inline_command.rb.fixed +180 -0
- data/lib/ukiryu/cli_commands/extract_command.rb +84 -0
- data/lib/ukiryu/cli_commands/info_command.rb +156 -0
- data/lib/ukiryu/cli_commands/list_command.rb +70 -0
- data/lib/ukiryu/cli_commands/opts_command.rb +106 -0
- data/lib/ukiryu/cli_commands/opts_command.rb.fixed +105 -0
- data/lib/ukiryu/cli_commands/response_formatter.rb +240 -0
- data/lib/ukiryu/cli_commands/run_command.rb +375 -0
- data/lib/ukiryu/cli_commands/run_file_command.rb +215 -0
- data/lib/ukiryu/cli_commands/system_command.rb +90 -0
- data/lib/ukiryu/cli_commands/validate_command.rb +87 -0
- data/lib/ukiryu/cli_commands/version_command.rb +16 -0
- data/lib/ukiryu/cli_commands/which_command.rb +166 -0
- data/lib/ukiryu/command_builder.rb +205 -0
- data/lib/ukiryu/config/env_provider.rb +64 -0
- data/lib/ukiryu/config/env_schema.rb +63 -0
- data/lib/ukiryu/config/override_resolver.rb +68 -0
- data/lib/ukiryu/config/type_converter.rb +59 -0
- data/lib/ukiryu/config.rb +249 -0
- data/lib/ukiryu/errors.rb +3 -0
- data/lib/ukiryu/executable_locator.rb +114 -0
- data/lib/ukiryu/execution/command_info.rb +64 -0
- data/lib/ukiryu/execution/metadata.rb +97 -0
- data/lib/ukiryu/execution/output.rb +144 -0
- data/lib/ukiryu/execution/result.rb +194 -0
- data/lib/ukiryu/execution.rb +15 -0
- data/lib/ukiryu/execution_context.rb +251 -0
- data/lib/ukiryu/executor.rb +76 -493
- data/lib/ukiryu/extractors/base_extractor.rb +63 -0
- data/lib/ukiryu/extractors/extractor.rb +150 -0
- data/lib/ukiryu/extractors/help_parser.rb +188 -0
- data/lib/ukiryu/extractors/native_extractor.rb +47 -0
- data/lib/ukiryu/io.rb +196 -0
- data/lib/ukiryu/logger.rb +544 -0
- data/lib/ukiryu/models/argument.rb +28 -0
- data/lib/ukiryu/models/argument_definition.rb +119 -0
- data/lib/ukiryu/models/arguments.rb +113 -0
- data/lib/ukiryu/models/command_definition.rb +176 -0
- data/lib/ukiryu/models/command_info.rb +37 -0
- data/lib/ukiryu/models/components.rb +107 -0
- data/lib/ukiryu/models/env_var_definition.rb +30 -0
- data/lib/ukiryu/models/error_response.rb +41 -0
- data/lib/ukiryu/models/execution_metadata.rb +31 -0
- data/lib/ukiryu/models/execution_report.rb +236 -0
- data/lib/ukiryu/models/exit_codes.rb +74 -0
- data/lib/ukiryu/models/flag_definition.rb +67 -0
- data/lib/ukiryu/models/option_definition.rb +102 -0
- data/lib/ukiryu/models/output_info.rb +25 -0
- data/lib/ukiryu/models/platform_profile.rb +153 -0
- data/lib/ukiryu/models/routing.rb +211 -0
- data/lib/ukiryu/models/search_paths.rb +39 -0
- data/lib/ukiryu/models/success_response.rb +85 -0
- data/lib/ukiryu/models/tool_definition.rb +145 -0
- data/lib/ukiryu/models/tool_metadata.rb +82 -0
- data/lib/ukiryu/models/validation_result.rb +80 -0
- data/lib/ukiryu/models/version_compatibility.rb +152 -0
- data/lib/ukiryu/models/version_detection.rb +39 -0
- data/lib/ukiryu/models.rb +23 -0
- data/lib/ukiryu/options/base.rb +95 -0
- data/lib/ukiryu/options_builder/formatter.rb +87 -0
- data/lib/ukiryu/options_builder/validator.rb +43 -0
- data/lib/ukiryu/options_builder.rb +311 -0
- data/lib/ukiryu/platform.rb +6 -6
- data/lib/ukiryu/registry.rb +143 -183
- data/lib/ukiryu/response/base.rb +217 -0
- data/lib/ukiryu/runtime.rb +179 -0
- data/lib/ukiryu/schema_validator.rb +8 -10
- data/lib/ukiryu/shell/bash.rb +3 -3
- data/lib/ukiryu/shell/cmd.rb +4 -4
- data/lib/ukiryu/shell/fish.rb +1 -1
- data/lib/ukiryu/shell/powershell.rb +3 -3
- data/lib/ukiryu/shell/sh.rb +1 -1
- data/lib/ukiryu/shell/zsh.rb +1 -1
- data/lib/ukiryu/shell.rb +146 -39
- data/lib/ukiryu/thor_ext.rb +208 -0
- data/lib/ukiryu/tool.rb +649 -258
- data/lib/ukiryu/tool_index.rb +224 -0
- data/lib/ukiryu/tools/base.rb +381 -0
- data/lib/ukiryu/tools/class_generator.rb +132 -0
- data/lib/ukiryu/tools/executable_finder.rb +29 -0
- data/lib/ukiryu/tools/generator.rb +154 -0
- data/lib/ukiryu/tools.rb +109 -0
- data/lib/ukiryu/type.rb +28 -43
- data/lib/ukiryu/validation/constraints.rb +281 -0
- data/lib/ukiryu/validation/validator.rb +188 -0
- data/lib/ukiryu/validation.rb +21 -0
- data/lib/ukiryu/version.rb +1 -1
- data/lib/ukiryu/version_detector.rb +51 -0
- data/lib/ukiryu.rb +31 -15
- data/ukiryu-proposal.md +2952 -0
- data/ukiryu.gemspec +18 -14
- metadata +137 -5
- 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
|