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
data/lib/ukiryu/cli.rb CHANGED
@@ -1,348 +1,174 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "thor"
4
- require "ukiryu"
5
- require_relative "version"
3
+ require 'thor'
4
+ require_relative 'cli_commands/base_command'
5
+ require_relative 'cli_commands/run_file_command'
6
+ require_relative 'cli_commands/run_command'
7
+ require_relative 'cli_commands/list_command'
8
+ require_relative 'cli_commands/info_command'
9
+ require_relative 'cli_commands/commands_command'
10
+ require_relative 'cli_commands/opts_command'
11
+ require_relative 'cli_commands/describe_command'
12
+ require_relative 'cli_commands/which_command'
13
+ require_relative 'cli_commands/config_command'
14
+ require_relative 'cli_commands/version_command'
15
+ require_relative 'cli_commands/system_command'
16
+ require_relative 'cli_commands/validate_command'
17
+ require_relative 'cli_commands/extract_command'
18
+ require_relative 'thor_ext'
19
+ require_relative 'version'
6
20
 
7
21
  module Ukiryu
8
22
  # CLI for exploring and interacting with Ukiryu tool profiles
23
+ #
24
+ # Each command is implemented as a separate class in the CliCommands::Commands namespace.
25
+ # This file just delegates to those command classes, keeping each file under 200 lines.
9
26
  class Cli < Thor
10
- package_name "ukiryu"
27
+ package_name 'ukiryu'
28
+
29
+ # Extend FriendlyCLI for better Thor behavior
30
+ extend FriendlyCLI
11
31
 
12
32
  # Set default registry path if not configured
13
33
  def self.exit_on_failure?
14
34
  false
15
35
  end
16
36
 
17
- desc "list", "List all available tools in the registry"
18
- method_option :registry, aliases: :r, desc: "Path to tool registry", type: :string
19
- def list
20
- setup_registry(options[:registry])
21
-
22
- tools = Registry.tools
23
- if tools.empty?
24
- say "No tools found in registry", :red
25
- return
26
- end
27
-
28
- say "Available tools (#{tools.count}):", :cyan
29
- tools.sort.each do |name|
30
- begin
31
- tool = Tool.get(name)
32
- version_info = tool.version ? "v#{tool.version}" : "version unknown"
33
- available = tool.available? ? "[✓]" : "[✗]"
34
- say " #{available.ljust(4)} #{name.ljust(20)} #{version_info}", tool.available? ? :green : :red
35
- rescue Ukiryu::Error => e
36
- say " [?] #{name.ljust(20)} error: #{e.message}", :red
37
- end
38
- end
37
+ desc 'run-file REQUEST_FILE', 'Execute a Ukiryu Structured Execution Request from a YAML file'
38
+ method_option :registry, aliases: :r, desc: 'Path to tool registry', type: :string
39
+ method_option :output, aliases: :o, desc: 'Output file for response (default: stdout)', type: :string
40
+ method_option :format, aliases: :f, desc: 'Response format (yaml, json, table, raw)', type: :string, default: 'yaml'
41
+ method_option :dry_run, aliases: :d, desc: 'Show execution request without executing', type: :boolean,
42
+ default: false
43
+ method_option :shell, desc: 'Shell to use for command execution (bash, zsh, fish, sh, powershell, cmd)', type: :string
44
+ def run_file(request_file)
45
+ CliCommands::RunFileCommand.new(options).run(request_file)
39
46
  end
40
47
 
41
- desc "info TOOL", "Show detailed information about a tool"
42
- method_option :registry, aliases: :r, desc: "Path to tool registry", type: :string
43
- def info(tool_name)
44
- setup_registry(options[:registry])
45
-
46
- tool = Tool.get(tool_name)
47
- profile = tool.profile
48
-
49
- say "", :clear
50
- say "Tool: #{profile[:name] || tool_name}", :cyan
51
- say "Display Name: #{profile[:display_name] || 'N/A'}", :white
52
- say "Version: #{profile[:version] || 'N/A'}", :white
53
- say "Homepage: #{profile[:homepage] || 'N/A'}", :white
54
-
55
- if profile[:aliases] && !profile[:aliases].empty?
56
- say "Aliases: #{profile[:aliases].join(', ')}", :white
57
- end
58
-
59
- # Version detection
60
- if profile[:version_detection]
61
- vd = profile[:version_detection]
62
- say "", :clear
63
- say "Version Detection:", :yellow
64
- say " Command: #{vd[:command]}", :white
65
- say " Pattern: #{vd[:pattern]}", :white
66
- if vd[:modern_threshold]
67
- say " Modern Threshold: #{vd[:modern_threshold]}", :white
68
- end
69
- end
70
-
71
- # Search paths
72
- if profile[:search_paths]
73
- say "", :clear
74
- say "Search Paths:", :yellow
75
- profile[:search_paths].each do |platform, paths|
76
- next if paths.nil? || paths.empty?
77
- say " #{platform}:", :white
78
- Array(paths).each { |p| say " - #{p}", :white }
79
- end
80
- end
48
+ desc 'exec TOOL [COMMAND] [KEY=VALUE ...]', 'Execute a tool command inline'
49
+ method_option :registry, aliases: :r, desc: 'Path to tool registry', type: :string
50
+ method_option :format, aliases: :f, desc: 'Response format (yaml, json, table, raw)', type: :string, default: 'yaml'
51
+ method_option :output, aliases: :o, desc: 'Output file for response (default: stdout)', type: :string
52
+ method_option :dry_run, aliases: :d, desc: 'Show execution request without executing', type: :boolean,
53
+ default: false
54
+ method_option :shell, desc: 'Shell to use for command execution (bash, zsh, fish, sh, powershell, cmd)', type: :string
55
+ method_option :stdin, desc: 'Read input from stdin (pass to command)', type: :boolean, default: false
56
+ method_option :raw, desc: 'Output raw stdout/stderr (for pipe composition)', type: :boolean, default: false
57
+ def exec(tool_name, command_name = nil, *params)
58
+ CliCommands::RunCommand.new(options).run(tool_name, command_name, *params)
59
+ end
81
60
 
82
- # Profiles
83
- if profile[:profiles]
84
- say "", :clear
85
- say "Profiles (#{profile[:profiles].count}):", :yellow
86
- profile[:profiles].each do |prof|
87
- platforms = Array(prof[:platforms] || ['all']).join(', ')
88
- shells = Array(prof[:shells] || ['all']).join(', ')
89
- say " #{prof[:name] || 'unnamed'}:", :white
90
- say " Platforms: #{platforms}", :white
91
- say " Shells: #{shells}", :white
92
- say " Version: #{prof[:version] || 'any'}", :white
93
- end
94
- end
61
+ desc 'list', 'List all available tools in the registry'
62
+ method_option :registry, aliases: :r, desc: 'Path to tool registry', type: :string
63
+ def list
64
+ CliCommands::ListCommand.new(options).run
65
+ end
95
66
 
96
- # Availability
97
- say "", :clear
98
- if tool.available?
99
- say "Status: INSTALLED", :green
100
- say "Executable: #{tool.executable}", :white
101
- say "Detected Version: #{tool.version || 'unknown'}", :white
67
+ desc 'info [TOOL]', 'Show detailed information about a tool (or general info if no tool specified)'
68
+ method_option :registry, aliases: :r, desc: 'Path to tool registry', type: :string
69
+ method_option :all, desc: 'Show all implementations for interfaces', type: :boolean, default: false
70
+ def info(tool_name = nil)
71
+ if tool_name.nil?
72
+ # Show general info when no tool specified
73
+ show_general_info
102
74
  else
103
- say "Status: NOT FOUND", :red
75
+ CliCommands::InfoCommand.new(options).run(tool_name)
104
76
  end
105
77
  end
106
78
 
107
- desc "commands TOOL", "List all commands available for a tool"
108
- method_option :registry, aliases: :r, desc: "Path to tool registry", type: :string
79
+ desc 'commands TOOL', 'List all commands available for a tool'
80
+ method_option :registry, aliases: :r, desc: 'Path to tool registry', type: :string
109
81
  def commands(tool_name)
110
- setup_registry(options[:registry])
111
-
112
- tool = Tool.get(tool_name)
113
- commands = tool.commands
114
-
115
- unless commands
116
- say "No commands defined for #{tool_name}", :red
117
- return
118
- end
119
-
120
- say "Commands for #{tool_name}:", :cyan
121
- commands.each do |cmd_name, cmd|
122
- cmd_name = cmd_name || 'unnamed'
123
- description = cmd[:description] || 'No description'
124
- say " #{cmd_name.to_s.ljust(20)} #{description}", :white
125
-
126
- # Show usage if available
127
- if cmd[:usage]
128
- say " Usage: #{cmd[:usage]}", :dim
129
- end
130
-
131
- # Show subcommand if exists
132
- if cmd[:subcommand]
133
- subcommand_info = cmd[:subcommand].nil? ? '(none)' : cmd[:subcommand]
134
- say " Subcommand: #{subcommand_info}", :dim
135
- end
136
- end
82
+ CliCommands::CommandsCommand.new(options).run(tool_name)
137
83
  end
138
84
 
139
- desc "opts TOOL [COMMAND]", "Show options for a tool or specific command"
140
- method_option :registry, aliases: :r, desc: "Path to tool registry", type: :string
85
+ desc 'opts TOOL [COMMAND]', 'Show options for a tool or specific command'
86
+ method_option :registry, aliases: :r, desc: 'Path to tool registry', type: :string
141
87
  def opts(tool_name, command_name = nil)
142
- setup_registry(options[:registry])
143
-
144
- tool = Tool.get(tool_name)
145
- commands = tool.commands
146
-
147
- unless commands
148
- say "No commands defined for #{tool_name}", :red
149
- return
150
- end
151
-
152
- # Find the command
153
- cmds = if command_name
154
- cmd = commands[command_name.to_sym] || commands[command_name]
155
- cmd ? [cmd] : []
156
- else
157
- commands.values
158
- end
159
-
160
- cmds.each do |cmd|
161
- cmd_title = command_name ? "#{tool_name} #{command_name}" : tool_name
162
- say "", :clear
163
- say "Options for #{cmd_title}:", :cyan
164
- say "#{cmd[:description]}" if cmd[:description]
165
-
166
- # Arguments
167
- if cmd[:arguments] && !cmd[:arguments].empty?
168
- say "", :clear
169
- say "Arguments:", :yellow
170
- cmd[:arguments].each do |arg|
171
- name = arg[:name] || 'unnamed'
172
- type = arg[:type] || 'unknown'
173
- position = arg[:position] || 'default'
174
- variadic = arg[:variadic] ? '(variadic)' : ''
175
-
176
- say " #{name} (#{type}#{variadic})", :white
177
- say " Position: #{position}", :dim if position != 'default'
178
- say " Description: #{arg[:description]}", :dim if arg[:description]
179
- end
180
- end
181
-
182
- # Options
183
- if cmd[:options] && !cmd[:options].empty?
184
- say "", :clear
185
- say "Options:", :yellow
186
- cmd[:options].each do |opt|
187
- name = opt[:name] || 'unnamed'
188
- cli = opt[:cli] || 'N/A'
189
- type = opt[:type] || 'unknown'
190
- description = opt[:description] || ''
191
-
192
- say " --#{name.ljust(20)} #{cli}", :white
193
- say " Type: #{type}", :dim
194
- say " #{description}", :dim if description
195
- if opt[:values]
196
- say " Values: #{opt[:values].join(', ')}", :dim
197
- end
198
- if opt[:range]
199
- say " Range: #{opt[:range].join('..')}", :dim
200
- end
201
- end
202
- end
203
-
204
- # Post-options (options between input and output)
205
- if cmd[:post_options] && !cmd[:post_options].empty?
206
- say "", :clear
207
- say "Post-Options (between input and output):", :yellow
208
- cmd[:post_options].each do |opt|
209
- name = opt[:name] || 'unnamed'
210
- cli = opt[:cli] || 'N/A'
211
- type = opt[:type] || 'unknown'
212
- description = opt[:description] || ''
213
-
214
- say " --#{name.ljust(20)} #{cli}", :white
215
- say " Type: #{type}", :dim
216
- say " #{description}", :dim if description
217
- end
218
- end
219
-
220
- # Flags
221
- if cmd[:flags] && !cmd[:flags].empty?
222
- say "", :clear
223
- say "Flags:", :yellow
224
- cmd[:flags].each do |flag|
225
- name = flag[:name] || 'unnamed'
226
- cli = flag[:cli] || 'N/A'
227
- default = flag[:default]
228
- default_str = default.nil? ? '' : " (default: #{default})"
229
-
230
- say " #{cli.ljust(25)} #{name}#{default_str}", :white
231
- say " #{flag[:description]}", :dim if flag[:description]
232
- end
233
- end
234
- end
88
+ CliCommands::OptsCommand.new(options).run(tool_name, command_name)
235
89
  end
236
90
 
237
- desc "execute TOOL COMMAND [OPTIONS]", "Execute a tool command with options"
238
- method_option :registry, aliases: :r, desc: "Path to tool registry", type: :string
239
- method_option :inputs, aliases: :i, desc: "Input files", type: :array
240
- method_option :output, aliases: :o, desc: "Output file", type: :string
241
- method_option :dry_run, aliases: :d, desc: "Show command without executing", type: :boolean, default: false
242
- def execute(tool_name, command_name, **extra_opts)
243
- setup_registry(options[:registry])
244
-
245
- tool = Tool.get(tool_name)
246
-
247
- # Build params from options
248
- params = {}
249
- params[:inputs] = options[:inputs] if options[:inputs]
250
- params[:output] = options[:output] if options[:output]
251
-
252
- # Add extra options as params
253
- extra_opts.each do |key, value|
254
- params[key.to_sym] = value
255
- end
256
-
257
- if options[:dry_run]
258
- # Show what would be executed without actually running
259
- say "DRY RUN - Would execute:", :yellow
260
- say " Tool: #{tool_name}", :white
261
- say " Command: #{command_name}", :white
262
- say " Parameters:", :white
263
- params.each do |k, v|
264
- say " #{k}: #{v.inspect}", :dim
265
- end
266
- else
267
- result = tool.execute(command_name.to_sym, params)
268
-
269
- if result.success?
270
- say "Command completed successfully", :green
271
- say "Exit status: #{result.status}", :white
272
- say "Duration: #{result.metadata.formatted_duration}", :white
273
-
274
- if result.output.stdout && !result.output.stdout.empty?
275
- say "", :clear
276
- say "STDOUT:", :yellow
277
- say result.output.stdout
278
- end
279
-
280
- if result.output.stderr && !result.output.stderr.empty?
281
- say "", :clear
282
- say "STDERR:", :yellow
283
- say result.output.stderr
284
- end
285
- else
286
- say "Command failed", :red
287
- say "Exit status: #{result.status}", :white
288
- say "Duration: #{result.metadata.formatted_duration}", :white
289
-
290
- if result.output.stdout && !result.output.stdout.empty?
291
- say "", :clear
292
- say "STDOUT:", :yellow
293
- say result.output.stdout
294
- end
295
-
296
- if result.output.stderr && !result.output.stderr.empty?
297
- say "", :clear
298
- say "STDERR:", :yellow
299
- say result.output.stderr
300
- end
91
+ desc 'describe TOOL [COMMAND]', 'Show comprehensive documentation for a tool or specific command'
92
+ method_option :registry, aliases: :r, desc: 'Path to tool registry', type: :string
93
+ method_option :format, aliases: :f, desc: 'Output format (text, yaml, json)', type: :string, default: 'text'
94
+ def describe(tool_name, command_name = nil)
95
+ CliCommands::DescribeCommand.new(options).run(tool_name, command_name)
96
+ end
301
97
 
302
- exit 1
303
- end
304
- end
98
+ desc 'system [SUBCOMMAND]', 'Show system information (shells, etc.)'
99
+ method_option :registry, aliases: :r, desc: 'Path to tool registry', type: :string
100
+ def system(subcommand = nil)
101
+ CliCommands::SystemCommand.new(options).run(subcommand)
305
102
  end
306
103
 
307
- desc "version", "Show Ukiryu version"
104
+ desc 'version', 'Show Ukiryu version'
308
105
  def version
309
- say "Ukiryu version #{Ukiryu::VERSION}", :cyan
106
+ CliCommands::VersionCommand.new(options).run
310
107
  end
311
108
 
312
- private
313
-
314
- def setup_registry(custom_path)
315
- registry_path = custom_path ||
316
- ENV['UKIRYU_REGISTRY'] ||
317
- default_registry_path
318
- if registry_path && Dir.exist?(registry_path)
319
- Registry.default_registry_path = registry_path
320
- end
109
+ desc 'which IDENTIFIER', 'Show which tool implementation would be selected'
110
+ method_option :registry, aliases: :r, desc: 'Path to tool registry', type: :string
111
+ method_option :platform, desc: 'Platform to check (macos, linux, windows)', type: :string
112
+ method_option :shell, desc: 'Shell to check (bash, zsh, fish, sh, powershell, cmd)', type: :string
113
+ def which(identifier)
114
+ CliCommands::WhichCommand.new(options).run(identifier)
321
115
  end
322
116
 
323
- def default_registry_path
324
- # Try multiple approaches to find the registry
325
- # 1. Check environment variable
326
- env_path = ENV['UKIRYU_REGISTRY']
327
- return env_path if env_path && Dir.exist?(env_path)
117
+ desc 'config [ACTION] [KEY] [VALUE]', 'Manage configuration (list, get, set, unset)'
118
+ method_option :registry, aliases: :r, desc: 'Path to tool registry', type: :string
119
+ def config(action = 'list', key = nil, value = nil)
120
+ CliCommands::ConfigCommand.new(options).run(action, key, value)
121
+ end
328
122
 
329
- # 2. Try relative to gem location
330
- # From lib/ukiryu/cli.rb, go up to gem root, then to sibling register/
331
- gem_root = File.dirname(File.dirname(File.dirname(__FILE__)))
332
- registry_path = File.join(gem_root, '..', 'register')
333
- if Dir.exist?(registry_path)
334
- return File.expand_path(registry_path)
335
- end
123
+ desc 'validate [TOOL]', 'Validate tool profile(s) against schema'
124
+ method_option :registry, aliases: :r, desc: 'Path to tool registry', type: :string
125
+ def validate(tool_name = nil)
126
+ CliCommands::ValidateCommand.new(options).run(tool_name)
127
+ end
336
128
 
337
- # 3. Try from current directory (development setup)
338
- current = File.expand_path('../register', Dir.pwd)
339
- return current if Dir.exist?(current)
129
+ desc 'extract TOOL', 'Extract tool definition from an installed CLI tool'
130
+ method_option :output, aliases: :o, desc: 'Output file for extracted definition', type: :string
131
+ method_option :method, aliases: :m, desc: 'Extraction method (auto, native, help)', type: :string, default: 'auto'
132
+ method_option :verbose, aliases: :v, desc: 'Enable verbose output', type: :boolean, default: false
133
+ def extract(tool_name)
134
+ CliCommands::ExtractCommand.new(options).run(tool_name)
135
+ end
340
136
 
341
- # 4. Try from parent directory
342
- parent = File.expand_path('../../register', Dir.pwd)
343
- return parent if Dir.exist?(parent)
137
+ private
344
138
 
345
- nil
139
+ # Show general information when no specific tool is requested
140
+ def show_general_info
141
+ require_relative 'shell'
142
+ require_relative 'runtime'
143
+ require_relative 'platform'
144
+
145
+ puts "Ukiryu v#{VERSION}"
146
+ puts ''
147
+ puts 'System Information:'
148
+ puts " Platform: #{Platform.detect}"
149
+ puts " Shell: #{Runtime.instance.shell}"
150
+ puts " Ruby: #{RUBY_VERSION}"
151
+ puts ''
152
+ puts 'Available Shells:'
153
+ Shell.available_shells.each do |shell|
154
+ puts " - #{shell}"
155
+ end
156
+ puts ''
157
+ puts 'Available Commands:'
158
+ puts ' list - List all available tools'
159
+ puts ' info TOOL - Show detailed information about a tool'
160
+ puts ' which IDENTIFIER - Show which tool implementation would be selected'
161
+ puts ' commands TOOL - List all commands available for a tool'
162
+ puts ' opts TOOL [COMMAND] - Show options for a tool or specific command'
163
+ puts ' describe TOOL [COMMAND] - Show comprehensive documentation'
164
+ puts ' config [ACTION] - Manage configuration (list, get, set, unset)'
165
+ puts ' system [shells] - Show system information (shells, etc.)'
166
+ puts ' exec ... - Execute a tool command inline'
167
+ puts ' run-file ... - Execute from a YAML file'
168
+ puts ' version - Show Ukiryu version'
169
+ puts ''
170
+ puts 'For more information on a specific command:'
171
+ puts ' ukiryu help COMMAND'
346
172
  end
347
173
  end
348
174
  end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../config'
4
+
5
+ module Ukiryu
6
+ module CliCommands
7
+ # Base class for CLI commands
8
+ #
9
+ # Provides shared functionality for all CLI commands including
10
+ # registry setup, output formatting, and error handling.
11
+ #
12
+ # @abstract Subclasses must implement the `run` method
13
+ class BaseCommand
14
+ attr_reader :options, :config
15
+
16
+ # Initialize a new command
17
+ #
18
+ # @param options [Hash] command options from Thor
19
+ def initialize(options = {})
20
+ @options = options
21
+ @config = Ukiryu::Config.instance
22
+ apply_cli_options_to_config
23
+ end
24
+
25
+ # Execute the command
26
+ #
27
+ # Subclasses must implement this method
28
+ #
29
+ # @raise [NotImplementedError] if not implemented in subclass
30
+ def run
31
+ raise NotImplementedError, "#{self.class} must implement #run"
32
+ end
33
+
34
+ # Setup the registry path
35
+ #
36
+ # @param custom_path [String, nil] custom registry path
37
+ def setup_registry(custom_path = nil)
38
+ registry_path = custom_path || config.registry || default_registry_path
39
+ return unless registry_path && Dir.exist?(registry_path)
40
+
41
+ Registry.default_registry_path = registry_path
42
+ end
43
+
44
+ # Apply CLI options to the Config instance
45
+ # CLI options have the highest priority in the configuration chain
46
+ def apply_cli_options_to_config
47
+ # Thor option defaults that should not override ENV or programmatic config
48
+ # Only apply CLI option if it's not the default value
49
+ cli_mappings = {
50
+ format: 'yaml', # default format in Thor
51
+ output: nil,
52
+ registry: nil,
53
+ timeout: nil,
54
+ shell: nil
55
+ }
56
+
57
+ cli_mappings.each do |cli_key, default_value|
58
+ next unless options.key?(cli_key)
59
+
60
+ # Only set CLI option if it's not the default value
61
+ # This allows ENV and programmatic config to take precedence when user doesn't specify
62
+ option_value = options[cli_key]
63
+ should_set = if default_value.nil?
64
+ # No default, always set
65
+ !option_value.nil? && !option_value.empty?
66
+ else
67
+ # Only set if different from default (user explicitly specified)
68
+ option_value != default_value
69
+ end
70
+
71
+ config.set_cli_option(cli_key, option_value) if should_set
72
+ end
73
+
74
+ # Handle boolean options from Thor
75
+ config.set_cli_option(:debug, options[:verbose]) if options.key?(:verbose)
76
+
77
+ # Handle dry_run option
78
+ return unless options.key?(:dry_run)
79
+
80
+ config.set_cli_option(:dry_run, options[:dry_run])
81
+ end
82
+
83
+ # Get the default registry path
84
+ #
85
+ # @return [String, nil] the default registry path
86
+ def default_registry_path
87
+ # Try multiple approaches to find the registry
88
+ # Note: ENV and Config.registry are already checked by setup_registry
89
+
90
+ # 1. Try relative to gem location
91
+ gem_root = File.dirname(File.dirname(File.dirname(__FILE__)))
92
+ registry_path = File.join(gem_root, '..', 'register')
93
+ return File.expand_path(registry_path) if Dir.exist?(registry_path)
94
+
95
+ # 2. Try from current directory (development setup)
96
+ current = File.expand_path('../register', Dir.pwd)
97
+ return current if Dir.exist?(current)
98
+
99
+ # 3. Try from parent directory
100
+ parent = File.expand_path('../../register', Dir.pwd)
101
+ return parent if Dir.exist?(parent)
102
+
103
+ nil
104
+ end
105
+
106
+ # Convert string keys to symbols
107
+ #
108
+ # @param hash [Hash] hash with string keys
109
+ # @return [Hash] hash with symbol keys
110
+ def stringify_keys(hash)
111
+ hash.transform_keys(&:to_sym)
112
+ end
113
+
114
+ protected
115
+
116
+ # Say output (Thor compatibility)
117
+ #
118
+ # Respects the use_color configuration option and NO_COLOR environment variable.
119
+ #
120
+ # @param message [String] the message
121
+ # @param color [Symbol] the color
122
+ def say(message, color = nil)
123
+ # Check if colors should be disabled
124
+ # Config handles both use_color setting and NO_COLOR environment variable
125
+ colors_disabled = config.colors_disabled?
126
+
127
+ if color && !colors_disabled
128
+ # Use ANSI color codes
129
+ colors = {
130
+ black: 30,
131
+ red: 31,
132
+ green: 32,
133
+ yellow: 33,
134
+ blue: 34,
135
+ magenta: 35,
136
+ cyan: 36,
137
+ white: 37,
138
+ dim: 2 # bright/black for dim
139
+ }
140
+ code = colors[color] || 37
141
+ puts "\e[#{code}m#{message}\e[0m"
142
+ else
143
+ puts message
144
+ end
145
+ end
146
+
147
+ # Exit with error message
148
+ #
149
+ # @param message [String] the error message
150
+ def error!(message)
151
+ raise Thor::Error, message
152
+ end
153
+ end
154
+ end
155
+ end