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,375 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_command'
|
|
4
|
+
require_relative 'response_formatter'
|
|
5
|
+
require_relative '../tool'
|
|
6
|
+
require_relative '../executor'
|
|
7
|
+
require_relative '../logger'
|
|
8
|
+
require_relative '../models/success_response'
|
|
9
|
+
require_relative '../models/error_response'
|
|
10
|
+
require_relative '../models/execution_report'
|
|
11
|
+
require 'yaml'
|
|
12
|
+
|
|
13
|
+
module Ukiryu
|
|
14
|
+
module CliCommands
|
|
15
|
+
# Execute a tool command inline (shorthand for run)
|
|
16
|
+
class RunCommand < BaseCommand
|
|
17
|
+
include ResponseFormatter
|
|
18
|
+
|
|
19
|
+
# Execute the command
|
|
20
|
+
#
|
|
21
|
+
# @param tool_name [String] the tool name
|
|
22
|
+
# @param command_name [String, nil] the command name (optional, uses default if nil)
|
|
23
|
+
# @param params [Array<String>] key=value parameter pairs
|
|
24
|
+
def run(tool_name, command_name = nil, *params)
|
|
25
|
+
setup_registry
|
|
26
|
+
|
|
27
|
+
# Handle the case where command_name is omitted and first param is a key=value pair
|
|
28
|
+
# When user types: ukiryu exec-inline ping host=127.0.0.1
|
|
29
|
+
# Thor interprets it as: tool_name="ping", command_name="host=127.0.0.1", params=["count=1"]
|
|
30
|
+
# We need to detect if command_name looks like a parameter
|
|
31
|
+
if command_name&.include?('=')
|
|
32
|
+
# command_name is actually a parameter, shift it back to params
|
|
33
|
+
params.unshift(command_name)
|
|
34
|
+
command_name = nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Special handling for "help" command
|
|
38
|
+
if command_name&.to_s == 'help'
|
|
39
|
+
show_tool_help(tool_name, params)
|
|
40
|
+
return
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Resolve command name if not provided
|
|
44
|
+
command_name ||= resolve_default_command(tool_name)
|
|
45
|
+
|
|
46
|
+
# Parse key=value pairs into arguments hash
|
|
47
|
+
arguments = parse_inline_params(params)
|
|
48
|
+
|
|
49
|
+
# Handle stdin from CLI flag (--stdin) or special parameter (stdin=-)
|
|
50
|
+
if options[:stdin] || arguments[:stdin] == '-'
|
|
51
|
+
# Read from actual stdin
|
|
52
|
+
stdin_data = $stdin.read
|
|
53
|
+
arguments[:stdin] = stdin_data
|
|
54
|
+
elsif arguments[:stdin]
|
|
55
|
+
# stdin parameter contains data (string or file path)
|
|
56
|
+
# If it starts with @, treat as file path
|
|
57
|
+
if arguments[:stdin].is_a?(String) && arguments[:stdin].start_with?('@')
|
|
58
|
+
file_path = arguments[:stdin][1..]
|
|
59
|
+
begin
|
|
60
|
+
arguments[:stdin] = File.read(file_path)
|
|
61
|
+
rescue Errno::ENOENT
|
|
62
|
+
error! "File not found: #{file_path}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
# Otherwise, use the value as-is (already a string or IO object)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Build execution request
|
|
69
|
+
request = {
|
|
70
|
+
'tool' => tool_name,
|
|
71
|
+
'command' => command_name,
|
|
72
|
+
'arguments' => arguments
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# Output debug: Ukiryu CLI Options
|
|
76
|
+
if config.debug
|
|
77
|
+
logger = Ukiryu::Logger.new
|
|
78
|
+
ukiryu_options = {
|
|
79
|
+
format: config.format,
|
|
80
|
+
debug: config.debug,
|
|
81
|
+
dry_run: config.dry_run,
|
|
82
|
+
output: config.output,
|
|
83
|
+
registry: config.registry,
|
|
84
|
+
stdin: !arguments[:stdin].nil?
|
|
85
|
+
}
|
|
86
|
+
logger.debug_section_ukiryu_options(ukiryu_options)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Get format from Config (priority: CLI > ENV > programmatic > default)
|
|
90
|
+
# --raw flag overrides the format setting
|
|
91
|
+
format = if options[:raw]
|
|
92
|
+
:raw
|
|
93
|
+
else
|
|
94
|
+
config.format.to_sym
|
|
95
|
+
end
|
|
96
|
+
error! "Invalid format: #{format}. Must be one of: #{OUTPUT_FORMATS.join(', ')}" unless OUTPUT_FORMATS.include?(format)
|
|
97
|
+
|
|
98
|
+
if config.dry_run
|
|
99
|
+
# Show dry run output
|
|
100
|
+
say_dry_run(request)
|
|
101
|
+
return
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Execute the request
|
|
105
|
+
response = execute_request(request, tool_name, command_name)
|
|
106
|
+
|
|
107
|
+
# Output response
|
|
108
|
+
output_file = config.output
|
|
109
|
+
output_response(response, format, output_file, config)
|
|
110
|
+
|
|
111
|
+
# Don't exit here - let Thor handle the result
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
# Resolve the default command for a tool
|
|
117
|
+
# Uses the tool's default_command from profile, or falls back to the interface it implements
|
|
118
|
+
#
|
|
119
|
+
# @param tool_name [String] the tool name
|
|
120
|
+
# @return [String] the resolved command name
|
|
121
|
+
def resolve_default_command(tool_name)
|
|
122
|
+
# Use Registry to load tool metadata without full resolution
|
|
123
|
+
# This avoids triggering debug output for "Tool Resolution" twice
|
|
124
|
+
require_relative '../registry'
|
|
125
|
+
require_relative '../models/tool_metadata'
|
|
126
|
+
|
|
127
|
+
metadata = Registry.load_tool_metadata(tool_name.to_sym, registry_path: config.registry)
|
|
128
|
+
error! "Tool not found: #{tool_name}" unless metadata
|
|
129
|
+
|
|
130
|
+
# Get the default command (checks YAML default_command, then implements, then tool name)
|
|
131
|
+
command = metadata.default_command
|
|
132
|
+
return command.to_s if command
|
|
133
|
+
|
|
134
|
+
# Fallback
|
|
135
|
+
'default'
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Parse inline key=value params into a hash
|
|
139
|
+
def parse_inline_params(params_array)
|
|
140
|
+
arguments = {}
|
|
141
|
+
|
|
142
|
+
params_array.each do |param|
|
|
143
|
+
if param.include?('=')
|
|
144
|
+
key, value = param.split('=', 2)
|
|
145
|
+
|
|
146
|
+
# Special case: stdin parameter with special values should be treated as string literals
|
|
147
|
+
# - stdin=- : marker for reading from actual stdin
|
|
148
|
+
# - stdin=@filename : marker for reading from file
|
|
149
|
+
# These syntaxes use characters that are invalid YAML, so skip YAML parsing
|
|
150
|
+
skip_yaml_parse = key == 'stdin' && (value == '-' || value.start_with?('@'))
|
|
151
|
+
|
|
152
|
+
unless skip_yaml_parse
|
|
153
|
+
# Try to parse value as YAML to handle types properly
|
|
154
|
+
begin
|
|
155
|
+
parsed_value = YAML.safe_load(value, permitted_classes: [Symbol])
|
|
156
|
+
value = parsed_value
|
|
157
|
+
rescue StandardError
|
|
158
|
+
# Keep as string if YAML parsing fails
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Convert key to symbol for consistency with API
|
|
162
|
+
end
|
|
163
|
+
arguments[key.to_sym] = value
|
|
164
|
+
else
|
|
165
|
+
error! "Invalid parameter format: #{param}. Use key=value"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
arguments
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Execute the request and build response
|
|
173
|
+
def execute_request(request, tool_name = nil, command_name = nil)
|
|
174
|
+
tool_name ||= request['tool']
|
|
175
|
+
command_name ||= request['command']
|
|
176
|
+
arguments = stringify_keys(request['arguments'])
|
|
177
|
+
|
|
178
|
+
logger = Ukiryu::Logger.new if config.debug
|
|
179
|
+
collect_metrics = config.metrics
|
|
180
|
+
|
|
181
|
+
# Initialize execution report if metrics are enabled
|
|
182
|
+
execution_report = if collect_metrics
|
|
183
|
+
Models::ExecutionReport.new(
|
|
184
|
+
tool_resolution: Models::StageMetrics.new(name: 'tool_resolution'),
|
|
185
|
+
command_building: Models::StageMetrics.new(name: 'command_building'),
|
|
186
|
+
execution: Models::StageMetrics.new(name: 'execution'),
|
|
187
|
+
response_building: Models::StageMetrics.new(name: 'response_building'),
|
|
188
|
+
run_environment: Models::RunEnvironment.collect,
|
|
189
|
+
timestamp: Time.now.iso8601
|
|
190
|
+
)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
begin
|
|
194
|
+
# Stage: Tool Resolution
|
|
195
|
+
execution_report.tool_resolution.start! if collect_metrics
|
|
196
|
+
|
|
197
|
+
# Get tool - try find_by first for interface-based discovery, fallback to get
|
|
198
|
+
tool = Tool.find_by(tool_name.to_sym) || Tool.get(tool_name.to_sym)
|
|
199
|
+
return Models::ErrorResponse.from_message("Tool not available: #{tool_name}") unless tool
|
|
200
|
+
|
|
201
|
+
return Models::ErrorResponse.from_message("Tool found but not executable: #{tool_name}") unless tool.available?
|
|
202
|
+
|
|
203
|
+
execution_report.tool_resolution.finish! if collect_metrics
|
|
204
|
+
|
|
205
|
+
# Stage: Command Building
|
|
206
|
+
execution_report.command_building.start! if collect_metrics
|
|
207
|
+
|
|
208
|
+
# Get command definition for context
|
|
209
|
+
command_definition = tool.command_definition(command_name.to_sym)
|
|
210
|
+
|
|
211
|
+
# Build options object (OOP approach)
|
|
212
|
+
# Note: stdin is a special parameter, not passed to options
|
|
213
|
+
options_arguments = arguments.reject { |k, _| k == :stdin }
|
|
214
|
+
options_class = tool.options_for(command_name.to_sym)
|
|
215
|
+
options = options_class.new
|
|
216
|
+
options_arguments.each { |key, value| options.send("#{key}=", value) }
|
|
217
|
+
|
|
218
|
+
execution_report.command_building.finish! if collect_metrics
|
|
219
|
+
|
|
220
|
+
# Output debug: Structured Options (the tool's options object)
|
|
221
|
+
logger.debug_section_structured_options(tool_name, command_name, options) if config.debug && logger
|
|
222
|
+
|
|
223
|
+
# Stage: Execution
|
|
224
|
+
execution_report.execution.start! if collect_metrics
|
|
225
|
+
|
|
226
|
+
# Execute command (pass arguments hash with stdin, not just options object)
|
|
227
|
+
result = tool.execute(command_name.to_sym, arguments)
|
|
228
|
+
|
|
229
|
+
execution_report.execution.finish! if collect_metrics
|
|
230
|
+
|
|
231
|
+
# Output debug: Shell Command
|
|
232
|
+
if config.debug && logger
|
|
233
|
+
# CommandInfo doesn't have env_vars, so we pass empty hash
|
|
234
|
+
logger.debug_section_shell_command(
|
|
235
|
+
executable: result.command_info.executable,
|
|
236
|
+
full_command: result.command_info.full_command
|
|
237
|
+
)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Output debug: Raw Response
|
|
241
|
+
if config.debug && logger
|
|
242
|
+
logger.debug_section_raw_response(
|
|
243
|
+
stdout: result.stdout,
|
|
244
|
+
stderr: result.stderr,
|
|
245
|
+
exit_code: result.status
|
|
246
|
+
)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Stage: Response Building
|
|
250
|
+
execution_report.response_building.start! if collect_metrics
|
|
251
|
+
|
|
252
|
+
# Build successful response with original arguments and command definition
|
|
253
|
+
response = Models::SuccessResponse.from_result(
|
|
254
|
+
result,
|
|
255
|
+
arguments,
|
|
256
|
+
command_definition,
|
|
257
|
+
execution_report: collect_metrics ? execution_report : nil
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
execution_report.response_building.finish! if collect_metrics
|
|
261
|
+
|
|
262
|
+
# Calculate total duration
|
|
263
|
+
execution_report.calculate_total if collect_metrics
|
|
264
|
+
|
|
265
|
+
# Output debug: Execution Report
|
|
266
|
+
logger.debug_section_execution_report(execution_report) if config.debug && logger && collect_metrics
|
|
267
|
+
|
|
268
|
+
# Output debug: Structured Response
|
|
269
|
+
logger.debug_section_structured_response(response) if config.debug && logger
|
|
270
|
+
|
|
271
|
+
response
|
|
272
|
+
rescue Ukiryu::ToolNotFoundError => e
|
|
273
|
+
Models::ErrorResponse.from_message("Tool not found: #{e.message}")
|
|
274
|
+
rescue Ukiryu::ProfileNotFoundError => e
|
|
275
|
+
Models::ErrorResponse.from_message("Profile not found: #{e.message}")
|
|
276
|
+
rescue Ukiryu::ExecutionError => e
|
|
277
|
+
Models::ErrorResponse.from_message(e.message)
|
|
278
|
+
rescue Ukiryu::TimeoutError => e
|
|
279
|
+
Models::ErrorResponse.from_message("Command timed out: #{e.message}")
|
|
280
|
+
rescue ArgumentError => e
|
|
281
|
+
# Output full backtrace for debugging
|
|
282
|
+
warn 'ArgumentError backtrace:' if config.debug
|
|
283
|
+
e.backtrace.each { |line| warn " #{line}" } if config.debug
|
|
284
|
+
Models::ErrorResponse.from_message("Invalid arguments: #{e.message}")
|
|
285
|
+
rescue StandardError => e
|
|
286
|
+
# Output full backtrace for debugging
|
|
287
|
+
warn 'StandardError backtrace:' if config.debug
|
|
288
|
+
e.backtrace.each { |line| warn " #{line}" } if config.debug
|
|
289
|
+
Models::ErrorResponse.from_message("Unexpected error: #{e.class}: #{e.message}")
|
|
290
|
+
ensure
|
|
291
|
+
# Ensure metrics are finished even on error
|
|
292
|
+
if collect_metrics && execution_report
|
|
293
|
+
execution_report.tool_resolution.finish!(success: false) unless execution_report.tool_resolution.duration
|
|
294
|
+
execution_report.command_building.finish!(success: false) unless execution_report.command_building.duration
|
|
295
|
+
execution_report.execution.finish!(success: false) unless execution_report.execution.duration
|
|
296
|
+
execution_report.response_building.finish!(success: false) unless execution_report.response_building.duration
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Show dry run output
|
|
302
|
+
#
|
|
303
|
+
# @param request [Hash] the execution request
|
|
304
|
+
def say_dry_run(request)
|
|
305
|
+
say 'DRY RUN - Ukiryu Structured Execution Request:', :yellow
|
|
306
|
+
say '', :clear
|
|
307
|
+
say "Tool: #{request['tool']}", :cyan
|
|
308
|
+
say "Command: #{request['command']}", :cyan
|
|
309
|
+
say 'Arguments:', :cyan
|
|
310
|
+
request['arguments'].each do |key, value|
|
|
311
|
+
if key == :stdin
|
|
312
|
+
# Show stdin preview (first 100 chars)
|
|
313
|
+
preview = value.is_a?(String) ? value[0..100] : '[IO Stream]'
|
|
314
|
+
preview += '...' if value.is_a?(String) && value.length > 100
|
|
315
|
+
say " #{key}: #{preview.inspect}", :white
|
|
316
|
+
else
|
|
317
|
+
say " #{key}: #{value.inspect}", :white
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Show help information for a tool
|
|
323
|
+
#
|
|
324
|
+
# @param tool_name [String] the tool name
|
|
325
|
+
# @param params [Array<String>] additional parameters
|
|
326
|
+
def show_tool_help(tool_name, _params = [])
|
|
327
|
+
setup_registry
|
|
328
|
+
|
|
329
|
+
# Use find_by for interface-based discovery
|
|
330
|
+
tool = Tool.find_by(tool_name.to_sym)
|
|
331
|
+
error! "Tool not found: #{tool_name}\nAvailable tools: #{Registry.tools.sort.join(', ')}" unless tool
|
|
332
|
+
|
|
333
|
+
say '', :clear
|
|
334
|
+
say "Tool: #{tool.name}", :cyan
|
|
335
|
+
say "Display Name: #{tool.profile.display_name || 'N/A'}", :white
|
|
336
|
+
say "Version: #{tool.profile.version || 'N/A'}", :white
|
|
337
|
+
say "Homepage: #{tool.profile.homepage || 'N/A'}", :white
|
|
338
|
+
say '', :clear
|
|
339
|
+
|
|
340
|
+
# Show available commands
|
|
341
|
+
tool_commands = tool.commands
|
|
342
|
+
if tool_commands && !tool_commands.empty?
|
|
343
|
+
say 'Available commands:', :cyan
|
|
344
|
+
|
|
345
|
+
tool_commands.each do |cmd|
|
|
346
|
+
cmd_name = cmd.name || 'unnamed'
|
|
347
|
+
description = cmd.description || ''
|
|
348
|
+
say " #{cmd_name.to_s.ljust(20)} #{description}", :white
|
|
349
|
+
|
|
350
|
+
# Show usage if available
|
|
351
|
+
say " Usage: #{cmd.usage}", :dim if cmd.usage
|
|
352
|
+
|
|
353
|
+
# Show subcommand if exists
|
|
354
|
+
if cmd.subcommand
|
|
355
|
+
subcommand_info = cmd.subcommand.nil? ? '(none)' : cmd.subcommand
|
|
356
|
+
say " Subcommand: #{subcommand_info}", :dim
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
say '', :clear
|
|
361
|
+
say "Usage: ukiryu exec #{tool.name} <command> [KEY=VALUE ...]", :dim
|
|
362
|
+
say " or: ukiryu exec #{tool.name} help", :dim
|
|
363
|
+
say " or: ukiryu describe #{tool.name} <command>", :dim
|
|
364
|
+
say '', :clear
|
|
365
|
+
say 'For more information on a specific command:', :dim
|
|
366
|
+
say " ukiryu opts #{tool.name} <command>", :dim
|
|
367
|
+
say " ukiryu describe #{tool.name} <command>", :dim
|
|
368
|
+
else
|
|
369
|
+
say 'This tool has no defined commands (it may be a simple wrapper)', :dim
|
|
370
|
+
say "Usage: ukiryu exec #{tool.name} [KEY=VALUE ...]", :dim
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
end
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_command'
|
|
4
|
+
require_relative 'response_formatter'
|
|
5
|
+
require_relative '../tool'
|
|
6
|
+
require_relative '../executor'
|
|
7
|
+
require_relative '../logger'
|
|
8
|
+
require_relative '../models/success_response'
|
|
9
|
+
require_relative '../models/error_response'
|
|
10
|
+
require_relative '../models/execution_report'
|
|
11
|
+
require 'yaml'
|
|
12
|
+
|
|
13
|
+
module Ukiryu
|
|
14
|
+
module CliCommands
|
|
15
|
+
# Execute a Ukiryu Structured Execution Request from a YAML file
|
|
16
|
+
class RunFileCommand < BaseCommand
|
|
17
|
+
include ResponseFormatter
|
|
18
|
+
|
|
19
|
+
# Execute the command
|
|
20
|
+
#
|
|
21
|
+
# @param request_file [String] path to the request YAML file
|
|
22
|
+
def run(request_file)
|
|
23
|
+
setup_registry
|
|
24
|
+
|
|
25
|
+
# Output debug: Ukiryu CLI Options
|
|
26
|
+
if config.debug
|
|
27
|
+
logger = Ukiryu::Logger.new
|
|
28
|
+
ukiryu_options = {
|
|
29
|
+
format: config.format,
|
|
30
|
+
debug: config.debug,
|
|
31
|
+
dry_run: config.dry_run,
|
|
32
|
+
output: config.output,
|
|
33
|
+
registry: config.registry,
|
|
34
|
+
request_file: request_file
|
|
35
|
+
}
|
|
36
|
+
logger.debug_section_ukiryu_options(ukiryu_options)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Get format from Config (priority: CLI > ENV > programmatic > default)
|
|
40
|
+
format = config.format.to_sym
|
|
41
|
+
error! "Invalid format: #{format}. Must be one of: #{OUTPUT_FORMATS.join(', ')}" unless OUTPUT_FORMATS.include?(format)
|
|
42
|
+
|
|
43
|
+
# Load execution request
|
|
44
|
+
request = load_execution_request(request_file)
|
|
45
|
+
|
|
46
|
+
if config.dry_run
|
|
47
|
+
# Show dry run output
|
|
48
|
+
say_dry_run(request)
|
|
49
|
+
return
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Execute the request
|
|
53
|
+
response = execute_request(request)
|
|
54
|
+
|
|
55
|
+
# Output response
|
|
56
|
+
output_file = config.output
|
|
57
|
+
output_response(response, format, output_file, config)
|
|
58
|
+
|
|
59
|
+
# Don't exit here - let Thor handle the result
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# Load execution request from YAML file
|
|
65
|
+
def load_execution_request(file_path)
|
|
66
|
+
error! "Request file not found: #{file_path}" unless File.exist?(file_path)
|
|
67
|
+
|
|
68
|
+
begin
|
|
69
|
+
request = YAML.safe_load(File.read(file_path), permitted_classes: [Symbol])
|
|
70
|
+
validate_request!(request)
|
|
71
|
+
request
|
|
72
|
+
rescue Psych::SyntaxError => e
|
|
73
|
+
error! "Invalid YAML in request file: #{e.message}"
|
|
74
|
+
rescue StandardError => e
|
|
75
|
+
error! "Error loading request file: #{e.message}"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Validate execution request structure
|
|
80
|
+
def validate_request!(request)
|
|
81
|
+
raise 'Request must be a YAML object (hash)' unless request.is_a?(Hash)
|
|
82
|
+
raise "Request must include 'tool' field" unless request['tool']
|
|
83
|
+
raise "Request must include 'command' field" unless request['command']
|
|
84
|
+
raise "Request must include 'arguments' field" unless request['arguments']
|
|
85
|
+
raise "'arguments' must be a YAML object (hash)" unless request['arguments'].is_a?(Hash)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Execute the request and build response
|
|
89
|
+
def execute_request(request)
|
|
90
|
+
tool_name = request['tool']
|
|
91
|
+
command_name = request['command']
|
|
92
|
+
arguments = stringify_keys(request['arguments'])
|
|
93
|
+
|
|
94
|
+
logger = Ukiryu::Logger.new if config.debug
|
|
95
|
+
collect_metrics = config.metrics
|
|
96
|
+
|
|
97
|
+
# Initialize execution report if metrics are enabled
|
|
98
|
+
execution_report = if collect_metrics
|
|
99
|
+
Models::ExecutionReport.new(
|
|
100
|
+
tool_resolution: Models::StageMetrics.new(name: 'tool_resolution'),
|
|
101
|
+
command_building: Models::StageMetrics.new(name: 'command_building'),
|
|
102
|
+
execution: Models::StageMetrics.new(name: 'execution'),
|
|
103
|
+
response_building: Models::StageMetrics.new(name: 'response_building'),
|
|
104
|
+
run_environment: Models::RunEnvironment.collect,
|
|
105
|
+
timestamp: Time.now.iso8601
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
begin
|
|
110
|
+
# Stage: Tool Resolution
|
|
111
|
+
execution_report.tool_resolution.start! if collect_metrics
|
|
112
|
+
|
|
113
|
+
# Get tool - try find_by first for interface-based discovery, fallback to get
|
|
114
|
+
tool = Tool.find_by(tool_name.to_sym) || Tool.get(tool_name.to_sym)
|
|
115
|
+
return Models::ErrorResponse.from_message("Tool not available: #{tool_name}") unless tool
|
|
116
|
+
|
|
117
|
+
return Models::ErrorResponse.from_message("Tool found but not executable: #{tool_name}") unless tool.available?
|
|
118
|
+
|
|
119
|
+
execution_report.tool_resolution.finish! if collect_metrics
|
|
120
|
+
|
|
121
|
+
# Stage: Command Building
|
|
122
|
+
execution_report.command_building.start! if collect_metrics
|
|
123
|
+
|
|
124
|
+
# Get command definition for context
|
|
125
|
+
command_definition = tool.command_definition(command_name.to_sym)
|
|
126
|
+
|
|
127
|
+
# Build options object (OOP approach)
|
|
128
|
+
options_class = tool.options_for(command_name.to_sym)
|
|
129
|
+
options = options_class.new
|
|
130
|
+
arguments.each { |key, value| options.send("#{key}=", value) }
|
|
131
|
+
|
|
132
|
+
execution_report.command_building.finish! if collect_metrics
|
|
133
|
+
|
|
134
|
+
# Output debug: Structured Options (the tool's options object)
|
|
135
|
+
logger.debug_section_structured_options(tool_name, command_name, options) if config.debug && logger
|
|
136
|
+
|
|
137
|
+
# Stage: Execution
|
|
138
|
+
execution_report.execution.start! if collect_metrics
|
|
139
|
+
|
|
140
|
+
# Execute command
|
|
141
|
+
result = tool.execute(command_name.to_sym, options)
|
|
142
|
+
|
|
143
|
+
execution_report.execution.finish! if collect_metrics
|
|
144
|
+
|
|
145
|
+
# Output debug: Shell Command
|
|
146
|
+
if config.debug && logger
|
|
147
|
+
logger.debug_section_shell_command(
|
|
148
|
+
executable: result.command_info.executable,
|
|
149
|
+
full_command: result.command_info.full_command
|
|
150
|
+
)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Output debug: Raw Response
|
|
154
|
+
if config.debug && logger
|
|
155
|
+
logger.debug_section_raw_response(
|
|
156
|
+
stdout: result.stdout,
|
|
157
|
+
stderr: result.stderr,
|
|
158
|
+
exit_code: result.status
|
|
159
|
+
)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Stage: Response Building
|
|
163
|
+
execution_report.response_building.start! if collect_metrics
|
|
164
|
+
|
|
165
|
+
# Build successful response with original arguments and command definition
|
|
166
|
+
response = Models::SuccessResponse.from_result(
|
|
167
|
+
result,
|
|
168
|
+
arguments,
|
|
169
|
+
command_definition,
|
|
170
|
+
execution_report: collect_metrics ? execution_report : nil
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
execution_report.response_building.finish! if collect_metrics
|
|
174
|
+
|
|
175
|
+
# Calculate total duration
|
|
176
|
+
execution_report.calculate_total if collect_metrics
|
|
177
|
+
|
|
178
|
+
# Output debug: Execution Report
|
|
179
|
+
logger.debug_section_execution_report(execution_report) if config.debug && logger && collect_metrics
|
|
180
|
+
|
|
181
|
+
# Output debug: Structured Response
|
|
182
|
+
logger.debug_section_structured_response(response) if config.debug && logger
|
|
183
|
+
|
|
184
|
+
response
|
|
185
|
+
rescue Ukiryu::ToolNotFoundError => e
|
|
186
|
+
Models::ErrorResponse.from_message("Tool not found: #{e.message}")
|
|
187
|
+
rescue Ukiryu::ProfileNotFoundError => e
|
|
188
|
+
Models::ErrorResponse.from_message("Profile not found: #{e.message}")
|
|
189
|
+
rescue Ukiryu::ExecutionError => e
|
|
190
|
+
Models::ErrorResponse.from_message(e.message)
|
|
191
|
+
rescue Ukiryu::TimeoutError => e
|
|
192
|
+
Models::ErrorResponse.from_message("Command timed out: #{e.message}")
|
|
193
|
+
rescue ArgumentError => e
|
|
194
|
+
# Output full backtrace for debugging
|
|
195
|
+
warn 'ArgumentError backtrace:' if config.debug
|
|
196
|
+
e.backtrace.each { |line| warn " #{line}" } if config.debug
|
|
197
|
+
Models::ErrorResponse.from_message("Invalid arguments: #{e.message}")
|
|
198
|
+
rescue StandardError => e
|
|
199
|
+
# Output full backtrace for debugging
|
|
200
|
+
warn 'StandardError backtrace:' if config.debug
|
|
201
|
+
e.backtrace.each { |line| warn " #{line}" } if config.debug
|
|
202
|
+
Models::ErrorResponse.from_message("Unexpected error: #{e.class}: #{e.message}")
|
|
203
|
+
ensure
|
|
204
|
+
# Ensure metrics are finished even on error
|
|
205
|
+
if collect_metrics && execution_report
|
|
206
|
+
execution_report.tool_resolution.finish!(success: false) unless execution_report.tool_resolution.duration
|
|
207
|
+
execution_report.command_building.finish!(success: false) unless execution_report.command_building.duration
|
|
208
|
+
execution_report.execution.finish!(success: false) unless execution_report.execution.duration
|
|
209
|
+
execution_report.response_building.finish!(success: false) unless execution_report.response_building.duration
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../config'
|
|
4
|
+
require_relative '../shell'
|
|
5
|
+
require_relative '../platform'
|
|
6
|
+
require_relative '../runtime'
|
|
7
|
+
|
|
8
|
+
module Ukiryu
|
|
9
|
+
module CliCommands
|
|
10
|
+
# Command to list system information
|
|
11
|
+
class SystemCommand < BaseCommand
|
|
12
|
+
# Run the system command
|
|
13
|
+
#
|
|
14
|
+
# @param subcommand [String, nil] the subcommand (shells, etc.)
|
|
15
|
+
def run(subcommand = nil)
|
|
16
|
+
case subcommand
|
|
17
|
+
when 'shells', nil
|
|
18
|
+
list_shells
|
|
19
|
+
else
|
|
20
|
+
error!("Unknown subcommand: #{subcommand}. Valid subcommands: shells")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# List all available shells on the system
|
|
27
|
+
def list_shells
|
|
28
|
+
require_relative '../shell'
|
|
29
|
+
|
|
30
|
+
all_shells = Shell.all_valid
|
|
31
|
+
available_shells = Shell.available_shells
|
|
32
|
+
platform_shells = Shell.valid_for_platform
|
|
33
|
+
|
|
34
|
+
say 'Available Shells on This System:', :cyan
|
|
35
|
+
say ''
|
|
36
|
+
|
|
37
|
+
if available_shells.empty?
|
|
38
|
+
say ' No shells detected', :dim
|
|
39
|
+
else
|
|
40
|
+
available_shells.each do |shell|
|
|
41
|
+
status = '✓'
|
|
42
|
+
say " #{status} #{shell}", :green
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
say ''
|
|
47
|
+
say 'All Supported Shells:', :cyan
|
|
48
|
+
say ''
|
|
49
|
+
|
|
50
|
+
all_shells.each do |shell|
|
|
51
|
+
is_available = available_shells.include?(shell)
|
|
52
|
+
is_platform = platform_shells.include?(shell)
|
|
53
|
+
|
|
54
|
+
status = if is_available
|
|
55
|
+
'✓'
|
|
56
|
+
elsif is_platform
|
|
57
|
+
'✗'
|
|
58
|
+
else
|
|
59
|
+
'-'
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
color = if is_available
|
|
63
|
+
:green
|
|
64
|
+
else
|
|
65
|
+
(is_platform ? :red : :dim)
|
|
66
|
+
end
|
|
67
|
+
note = if !is_platform
|
|
68
|
+
' (not supported on this platform)'
|
|
69
|
+
elsif !is_available
|
|
70
|
+
' (supported but not found)'
|
|
71
|
+
else
|
|
72
|
+
''
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
say " #{status} #{shell}#{note}", color
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
say ''
|
|
79
|
+
say "Platform: #{Platform.detect}", :dim
|
|
80
|
+
say "Current shell: #{Runtime.instance.shell}", :dim
|
|
81
|
+
|
|
82
|
+
# Show shell override status
|
|
83
|
+
config_shell = Config.shell
|
|
84
|
+
return unless config_shell
|
|
85
|
+
|
|
86
|
+
say "Shell override: #{config_shell} (set via --shell, UKIRYU_SHELL, or config)", :yellow
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|