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,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_command'
4
+ require_relative '../tool'
5
+ require_relative '../executor'
6
+ require 'yaml'
7
+
8
+ module Ukiryu
9
+ module CliCommands
10
+ # Execute a tool command inline (shorthand for run)
11
+ class ExecInlineCommand < BaseCommand
12
+ # Supported output formats
13
+ OUTPUT_FORMATS = %i[yaml json].freeze
14
+
15
+ # Execute the command
16
+ #
17
+ # @param tool_name [String] the tool name
18
+ # @param command_name [String] the command name
19
+ # @param params [Array<String>] key=value parameter pairs
20
+ def run(tool_name, command_name, *params)
21
+ setup_registry
22
+
23
+ # Parse key=value pairs into arguments hash
24
+ arguments = parse_inline_params(params)
25
+
26
+ # Build execution request
27
+ request = {
28
+ 'tool' => tool_name,
29
+ 'command' => command_name,
30
+ 'arguments' => arguments
31
+ }
32
+
33
+ # Validate format option
34
+ format = (options[:format] || 'yaml').to_sym
35
+ error! "Invalid format: #{options[:format]}. Must be one of: #{OUTPUT_FORMATS.join(', ')}" unless OUTPUT_FORMATS.include?(format)
36
+
37
+ if options[:dry_run]
38
+ # Show dry run output
39
+ say_dry_run(request)
40
+ return
41
+ end
42
+
43
+ # Execute the request
44
+ response = execute_request(request)
45
+
46
+ # Output response
47
+ output_response(response, format, nil)
48
+
49
+ # Exit with error code if command failed
50
+ exit_code = response['exit_code'].to_i
51
+ exit(exit_code) if exit_code != 0
52
+ end
53
+
54
+ private
55
+
56
+ # Parse inline key=value params into a hash
57
+ def parse_inline_params(params_array)
58
+ arguments = {}
59
+
60
+ params_array.each do |param|
61
+ if param.include?('=')
62
+ key, value = param.split('=', 2)
63
+
64
+ # Try to parse value as YAML to handle types properly
65
+ begin
66
+ parsed_value = YAML.safe_load(value, permitted_classes: [Symbol])
67
+ value = parsed_value
68
+ rescue StandardError
69
+ # Keep as string if YAML parsing fails
70
+ end
71
+
72
+ # Convert key to symbol for consistency with API
73
+ arguments[key.to_sym] = value
74
+ else
75
+ error! "Invalid parameter format: #{param}. Use key=value"
76
+ end
77
+ end
78
+
79
+ arguments
80
+ end
81
+
82
+ # Execute the request and build response
83
+ def execute_request(request)
84
+ tool_name = request['tool']
85
+ command_name = request['command']
86
+ arguments = stringify_keys(request['arguments'])
87
+
88
+ begin
89
+ # Get tool
90
+ tool = Tool.get(tool_name.to_sym)
91
+ return build_error_response("Tool not available: #{tool_name}") unless tool.available?
92
+
93
+ # Build options object (OOP approach)
94
+ options_class = tool.options_for(command_name.to_sym)
95
+ options = options_class.new
96
+ arguments.each { |key, value| options.send("#{key}=", value) }
97
+
98
+ # Execute command
99
+ result = tool.execute(command_name.to_sym, options)
100
+
101
+ # Build successful response
102
+ build_success_response(result)
103
+ rescue Ukiryu::ToolNotFoundError => e
104
+ build_error_response("Tool not found: #{e.message}")
105
+ rescue Ukiryu::ProfileNotFoundError => e
106
+ build_error_response("Profile not found: #{e.message}")
107
+ rescue Ukiryu::ExecutionError => e
108
+ build_error_response(e.message)
109
+ rescue Ukiryu::TimeoutError => e
110
+ build_error_response("Command timed out: #{e.message}")
111
+ rescue ArgumentError => e
112
+ build_error_response("Invalid arguments: #{e.message}")
113
+ rescue StandardError => e
114
+ build_error_response("Unexpected error: #{e.class}: #{e.message}")
115
+ end
116
+ end
117
+
118
+ # Build success response from Result object
119
+ def build_success_response(result)
120
+ {
121
+ 'status' => 'success',
122
+ 'exit_code' => result.status,
123
+ 'command' => {
124
+ 'executable' => result.executable,
125
+ 'arguments' => result.command_info.arguments,
126
+ 'full_command' => result.command_info.full_command,
127
+ 'shell' => result.command_info.shell.to_s
128
+ },
129
+ 'output' => {
130
+ 'stdout' => result.stdout,
131
+ 'stderr' => result.error_output,
132
+ 'stdout_lines' => result.stdout_lines,
133
+ 'stderr_lines' => result.stderr_lines
134
+ },
135
+ 'metadata' => {
136
+ 'started_at' => result.started_at.iso8601,
137
+ 'finished_at' => result.finished_at.iso8601,
138
+ 'duration_seconds' => result.duration,
139
+ 'formatted_duration' => result.metadata.formatted_duration
140
+ }
141
+ }
142
+ end
143
+
144
+ # Build error response
145
+ def build_error_response(message)
146
+ {
147
+ 'status' => 'error',
148
+ 'exit_code' => 1,
149
+ 'error' => message
150
+ }
151
+ end
152
+
153
+ # Output response in specified format
154
+ def output_response(response, format, _output_file)
155
+ output_string = case format
156
+ when :yaml
157
+ response.to_yaml
158
+ when :json
159
+ require 'json'
160
+ JSON.pretty_generate(response)
161
+ end
162
+
163
+ say output_string
164
+ end
165
+
166
+ # Show dry run output
167
+ def say_dry_run(request)
168
+ say 'DRY RUN - Ukiryu Structured Execution Request:', :yellow
169
+ say '', :clear
170
+ say "Tool: #{request['tool']}", :cyan
171
+ say "Command: #{request['command']}", :cyan
172
+ say 'Arguments:', :cyan
173
+ request['arguments'].each do |key, value|
174
+ say " #{key}: #{value.inspect}", :white
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_command'
4
+ require_relative '../extractors/extractor'
5
+
6
+ module Ukiryu
7
+ module CliCommands
8
+ # Extract tool definition from an installed CLI tool
9
+ #
10
+ # This command attempts to extract a tool definition by:
11
+ # 1. Trying the tool's native --ukiryu-definition flag
12
+ # 2. Parsing the tool's --help output as a fallback
13
+ class ExtractCommand < BaseCommand
14
+ # Execute the extract command
15
+ #
16
+ # @param tool_name [String] the tool name to extract definition from
17
+ def run(tool_name)
18
+ result = Extractors::Extractor.extract(tool_name, extract_options)
19
+
20
+ if result[:success]
21
+ output_result(result)
22
+ else
23
+ handle_failure(result)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ # Build extraction options from CLI options
30
+ #
31
+ # @return [Hash] extraction options
32
+ def extract_options
33
+ {
34
+ method: options[:method]&.to_sym || :auto,
35
+ verbose: options[:verbose]
36
+ }
37
+ end
38
+
39
+ # Output the extracted definition
40
+ #
41
+ # @param result [Hash] extraction result
42
+ def output_result(result)
43
+ yaml_content = result[:yaml]
44
+
45
+ # Write to file if output option specified
46
+ if options[:output]
47
+ File.write(options[:output], yaml_content)
48
+ say "Definition extracted to: #{options[:output]}", :green
49
+ say "Method: #{result[:method]}", :cyan if options[:verbose]
50
+ else
51
+ # Output to stdout
52
+ puts yaml_content
53
+ say "\n# Extracted using: #{result[:method]}", :cyan if options[:verbose]
54
+ end
55
+ end
56
+
57
+ # Handle extraction failure
58
+ #
59
+ # @param result [Hash] extraction result
60
+ def handle_failure(result)
61
+ say "Failed to extract definition from '#{@tool_name}'", :red
62
+ say "Error: #{result[:error]}", :red
63
+ say '', :clear
64
+ say 'The tool may not be installed or may not support extraction.', :yellow
65
+ say '', :clear
66
+ say 'Extraction methods tried:', :yellow
67
+
68
+ if options[:method] && options[:method] != 'auto'
69
+ say " - #{options[:method]} (explicitly selected)", :white
70
+ else
71
+ say " - native (try --ukiryu-definition flag)", :white
72
+ say " - help (parse --help output)", :white
73
+ end
74
+
75
+ say '', :clear
76
+ say 'You can specify a method with --method:', :yellow
77
+ say ' ukiryu extract TOOL --method native', :white
78
+ say ' ukiryu extract TOOL --method help', :white
79
+
80
+ exit 1
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_command'
4
+ require_relative '../tool'
5
+
6
+ module Ukiryu
7
+ module CliCommands
8
+ # Show detailed information about a tool
9
+ class InfoCommand < BaseCommand
10
+ # Execute the info command
11
+ #
12
+ # @param tool_name [String] the tool name
13
+ def run(tool_name)
14
+ setup_registry
15
+
16
+ # Use find_by for interface-based discovery (ping -> ping_bsd/ping_gnu)
17
+ tool = Tool.find_by(tool_name.to_sym)
18
+ error!("Tool not found: #{tool_name}\nAvailable tools: #{Registry.tools.sort.join(', ')}") unless tool
19
+
20
+ profile = tool.profile
21
+ show_all = options[:all]
22
+
23
+ say '', :clear
24
+
25
+ # Show interface information if queried name differs from actual tool name
26
+ if profile.name != tool_name.to_s && profile.implements
27
+ say "Interface: #{tool_name}", :cyan
28
+ say " This tool implements the '#{tool_name}' interface", :dim
29
+ say " Tool: #{profile.name}", :white
30
+
31
+ # Find other implementations of this interface
32
+ other_implementations = find_other_implementations(tool_name.to_s, profile.name)
33
+ say " Other implementations: #{other_implementations.join(', ')}", :dim if other_implementations.any?
34
+
35
+ if show_all
36
+ say '', :clear
37
+ say "All '#{tool_name}' implementations:", :yellow
38
+ all_implementations = [profile.name, *other_implementations].sort
39
+ all_implementations.each do |impl|
40
+ impl_tool = Tool.get(impl)
41
+ if impl_tool
42
+ status = impl_tool.available? ? '[✓]' : '[✗]'
43
+ color = impl_tool.available? ? :green : :red
44
+ say " #{status.ljust(4)} #{impl}", color
45
+ else
46
+ say " [?] #{impl}", :white
47
+ end
48
+ rescue Ukiryu::ToolNotFoundError, Ukiryu::ProfileNotFoundError
49
+ # Tool exists but no compatible profile for this platform
50
+ say " [ ] #{impl}", :dim
51
+ end
52
+ end
53
+ else
54
+ say "Tool: #{profile.name || tool_name}", :cyan
55
+ end
56
+
57
+ say "Display Name: #{profile.display_name || 'N/A'}", :white
58
+ say "Version: #{profile.version || 'N/A'}", :white
59
+ say "Homepage: #{profile.homepage || 'N/A'}", :white
60
+
61
+ say "Aliases: #{profile.aliases.join(', ')}", :white if profile.aliases && !profile.aliases.empty?
62
+
63
+ # Version detection
64
+ if profile.version_detection
65
+ vd = profile.version_detection
66
+ say '', :clear
67
+ say 'Version Detection:', :yellow
68
+ command_display = vd.command.is_a?(Array) ? vd.command.join(' ') : vd.command
69
+ say " Command: #{command_display}", :white
70
+ say " Pattern: #{vd.pattern}", :white
71
+ say " Modern Threshold: #{vd.modern_threshold}", :white if vd.modern_threshold
72
+ end
73
+
74
+ # Search paths
75
+ if profile.search_paths
76
+ say '', :clear
77
+ say 'Search Paths:', :yellow
78
+ search_paths = profile.search_paths
79
+
80
+ # Iterate over platform attributes
81
+ %i[macos linux windows freebsd openbsd netbsd].each do |platform|
82
+ paths = search_paths.send(platform)
83
+ next if paths.nil? || paths.empty?
84
+
85
+ say " #{platform}:", :white
86
+ Array(paths).each { |p| say " - #{p}", :white }
87
+ end
88
+ end
89
+
90
+ # Profiles
91
+ if profile.profiles
92
+ say '', :clear
93
+ say "Profiles (#{profile.profiles.count}):", :yellow
94
+ profile.profiles.each do |prof|
95
+ platforms = Array(prof.platforms || ['all']).join(', ')
96
+ shells = Array(prof.shells || ['all']).join(', ')
97
+ option_style = prof.option_style || 'default'
98
+ say " #{prof.name || 'unnamed'}:", :white
99
+ say " Platforms: #{platforms}", :white
100
+ say " Shells: #{shells}", :white
101
+ say " Option Style: #{option_style}", :white
102
+ say " Inherits: #{prof.inherits || 'none'}", :white if prof.inherits
103
+ end
104
+ end
105
+
106
+ # Availability
107
+ say '', :clear
108
+ if tool.available?
109
+ say 'Status: INSTALLED', :green
110
+ say "Executable: #{tool.executable}", :white
111
+ say "Detected Version: #{tool.version || 'unknown'}", :white
112
+ else
113
+ say 'Status: NOT FOUND', :red
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ # Find other tools that implement the same interface
120
+ #
121
+ # @param interface_name [String] the interface name
122
+ # @param current_tool_name [String] the current tool name to exclude
123
+ # @return [Array<String>] list of other tool names
124
+ def find_other_implementations(interface_name, current_tool_name)
125
+ require_relative '../registry'
126
+
127
+ implementations = []
128
+ interface_sym = interface_name.to_sym
129
+
130
+ if config.debug
131
+ say " [DEBUG] Looking for tools implementing '#{interface_name}' (excluding '#{current_tool_name}')", :dim
132
+ say " [DEBUG] Registry tools: #{Registry.tools.inspect}", :dim
133
+ end
134
+
135
+ Registry.tools.each do |tool_name|
136
+ next if tool_name == current_tool_name
137
+
138
+ begin
139
+ # Don't pass registry_path - let it use the default
140
+ tool_metadata = Registry.load_tool_metadata(tool_name.to_sym)
141
+ if config.debug
142
+ say " [DEBUG] #{tool_name} -> metadata: #{tool_metadata ? tool_metadata.implements : 'nil'}", :dim
143
+ end
144
+ implementations << tool_name if tool_metadata && tool_metadata.implements == interface_sym
145
+ rescue StandardError => e
146
+ # Skip tools that fail to load
147
+ say " [DEBUG] Failed to load #{tool_name}: #{e.message}", :dim if config.debug
148
+ next
149
+ end
150
+ end
151
+
152
+ implementations.sort
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_command'
4
+ require_relative '../tool'
5
+ require_relative '../registry'
6
+
7
+ module Ukiryu
8
+ module CliCommands
9
+ # List all available tools in the registry
10
+ class ListCommand < BaseCommand
11
+ # Execute the list command
12
+ def run
13
+ setup_registry
14
+
15
+ tools = Registry.tools
16
+ error! 'No tools found in registry' if tools.empty?
17
+
18
+ say "Available tools (#{tools.count}):", :cyan
19
+
20
+ # Separate tools into interfaces and standalone tools
21
+ interfaces = {}
22
+ standalone_tools = []
23
+
24
+ tools.sort.each do |name|
25
+ metadata = Registry.load_tool_metadata(name.to_sym)
26
+
27
+ if metadata&.implements
28
+ # This tool implements an interface
29
+ interface_name = metadata.implements.to_s
30
+ interfaces[interface_name] ||= []
31
+ interfaces[interface_name] << name
32
+ elsif metadata&.aliases&.any?
33
+ # Tool has aliases but doesn't implement interface - treat as standalone
34
+ standalone_tools << name
35
+ else
36
+ # Regular standalone tool
37
+ standalone_tools << name
38
+ end
39
+ rescue Ukiryu::Error
40
+ # If we can't load metadata, treat as standalone
41
+ standalone_tools << name
42
+ end
43
+
44
+ # Display interfaces first
45
+ interfaces.sort.each do |interface_name, impls|
46
+ say " #{interface_name}:", :cyan
47
+
48
+ impls.sort.each do |impl_name|
49
+ tool = Tool.get(impl_name)
50
+ version_info = tool.version ? "v#{tool.version}" : 'version unknown'
51
+ available = tool.available? ? '[✓]' : '[✗]'
52
+ say " #{available.ljust(4)} #{impl_name.ljust(20)} #{version_info}", tool.available? ? :green : :red
53
+ rescue Ukiryu::Error => e
54
+ say " [?] #{impl_name.ljust(20)} error: #{e.message}", :red
55
+ end
56
+ end
57
+
58
+ # Display standalone tools
59
+ standalone_tools.sort.each do |name|
60
+ tool = Tool.get(name)
61
+ version_info = tool.version ? "v#{tool.version}" : 'version unknown'
62
+ available = tool.available? ? '[✓]' : '[✗]'
63
+ say " #{available.ljust(4)} #{name.ljust(20)} #{version_info}", tool.available? ? :green : :red
64
+ rescue Ukiryu::Error => e
65
+ say " [?] #{name.ljust(20)} error: #{e.message}", :red
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_command'
4
+ require_relative '../tool'
5
+
6
+ module Ukiryu
7
+ module CliCommands
8
+ # Show options for a tool or specific command
9
+ class OptsCommand < BaseCommand
10
+ # Execute the opts command
11
+ #
12
+ # @param tool_name [String] the tool name
13
+ # @param command_name [String, nil] optional command name
14
+ def run(tool_name, command_name = nil)
15
+ setup_registry
16
+
17
+ # Use find_by for interface-based discovery (ping -> ping_bsd/ping_gnu)
18
+ tool = Tool.find_by(tool_name.to_sym)
19
+ error!("Tool not found: #{tool_name}\nAvailable tools: #{Registry.tools.sort.join(', ')}") unless tool
20
+
21
+ tool_commands = tool.commands
22
+ error! "No commands defined for #{tool_name}" unless tool_commands
23
+
24
+ # Find the command
25
+ cmds = if command_name
26
+ tool_commands.find { |c| c.name.to_s == command_name.to_s || c.name.to_sym == command_name.to_sym }
27
+ cmds ? [cmds] : []
28
+ else
29
+ tool_commands
30
+ end
31
+
32
+ cmds.each do |cmd|
33
+ cmd_title = command_name ? "#{tool_name} #{command_name}" : tool_name
34
+ say '', :clear
35
+ say "Options for #{cmd_title}:", :cyan
36
+ say cmd.description.to_s if cmd.description
37
+
38
+ # Arguments
39
+ if cmd.arguments && !cmd.arguments.empty?
40
+ say '', :clear
41
+ say 'Arguments:', :yellow
42
+ cmd.arguments.each do |arg|
43
+ name = arg.name || 'unnamed'
44
+ type = arg.type || 'unknown'
45
+ position = arg.position || 'default'
46
+ variadic = arg.variadic ? '(variadic)' : ''
47
+
48
+ say " #{name} (#{type}#{variadic})", :white
49
+ say " Position: #{position}", :dim if position != 'default'
50
+ say " Description: #{arg.description}", :dim if arg.description
51
+ end
52
+ end
53
+
54
+ # Options
55
+ if cmd.options && !cmd.options.empty?
56
+ say '', :clear
57
+ say 'Options:', :yellow
58
+ cmd.options.each do |opt|
59
+ name = opt.name || 'unnamed'
60
+ cli = opt.cli || 'N/A'
61
+ type = opt.type || 'unknown'
62
+ description = opt.description || ''
63
+
64
+ say " --#{name.ljust(20)} #{cli}", :white
65
+ say " Type: #{type}", :dim
66
+ say " #{description}", :dim if description
67
+ say " Values: #{opt.values.join(', ')}", :dim if opt.values
68
+ say " Range: #{opt.range.join('..')}", :dim if opt.range
69
+ end
70
+ end
71
+
72
+ # Post-options (options between input and output)
73
+ if cmd.post_options && !cmd.post_options.empty?
74
+ say '', :clear
75
+ say 'Post-Options (between input and output):', :yellow
76
+ cmd.post_options.each do |opt|
77
+ name = opt.name || 'unnamed'
78
+ cli = opt.cli || 'N/A'
79
+ type = opt.type || 'unknown'
80
+ description = opt.description || ''
81
+
82
+ say " --#{name.ljust(20)} #{cli}", :white
83
+ say " Type: #{type}", :dim
84
+ say " #{description}", :dim if description
85
+ end
86
+ end
87
+
88
+ # Flags
89
+ next unless cmd.flags && !cmd.flags.empty?
90
+
91
+ say '', :clear
92
+ say 'Flags:', :yellow
93
+ cmd.flags.each do |flag|
94
+ name = flag.name || 'unnamed'
95
+ cli = flag.cli || 'N/A'
96
+ default = flag.default
97
+ default_str = default.nil? ? '' : " (default: #{default})"
98
+
99
+ say " #{cli.ljust(25)} #{name}#{default_str}", :white
100
+ say " #{flag.description}", :dim if flag.description
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end