ukiryu 0.1.1 → 0.1.3
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/release.yml +58 -14
- data/.gitignore +3 -0
- data/.rubocop_todo.yml +170 -79
- data/Gemfile +1 -1
- data/README.adoc +1603 -576
- data/docs/.gitignore +1 -0
- data/docs/Gemfile +10 -0
- data/docs/INDEX.adoc +261 -0
- data/docs/_config.yml +180 -0
- data/docs/advanced/custom-tool-classes.adoc +581 -0
- data/docs/advanced/index.adoc +20 -0
- data/docs/features/configuration.adoc +657 -0
- data/docs/features/index.adoc +31 -0
- data/docs/features/platform-support.adoc +488 -0
- data/docs/getting-started/core-concepts.adoc +666 -0
- data/docs/getting-started/index.adoc +36 -0
- data/docs/getting-started/installation.adoc +216 -0
- data/docs/getting-started/quick-start.adoc +258 -0
- data/docs/guides/env-var-sets.adoc +388 -0
- data/docs/guides/index.adoc +20 -0
- data/docs/interfaces/cli.adoc +609 -0
- data/docs/interfaces/index.adoc +153 -0
- data/docs/interfaces/ruby-api.adoc +538 -0
- data/docs/lychee.toml +49 -0
- data/docs/reference/configuration-options.adoc +720 -0
- data/docs/reference/error-codes.adoc +634 -0
- data/docs/reference/index.adoc +20 -0
- data/docs/reference/ruby-api.adoc +1217 -0
- data/docs/understanding/index.adoc +20 -0
- data/lib/ukiryu/cli.rb +43 -58
- data/lib/ukiryu/cli_commands/base_command.rb +16 -27
- data/lib/ukiryu/cli_commands/cache_command.rb +100 -0
- data/lib/ukiryu/cli_commands/commands_command.rb +8 -8
- data/lib/ukiryu/cli_commands/commands_command.rb.fixed +1 -1
- data/lib/ukiryu/cli_commands/config_command.rb +49 -7
- data/lib/ukiryu/cli_commands/definitions_command.rb +254 -0
- data/lib/ukiryu/cli_commands/describe_command.rb +13 -7
- data/lib/ukiryu/cli_commands/describe_command.rb.fixed +1 -1
- data/lib/ukiryu/cli_commands/docs_command.rb +148 -0
- data/lib/ukiryu/cli_commands/exec_inline_command.rb.fixed +1 -1
- data/lib/ukiryu/cli_commands/extract_command.rb +2 -2
- data/lib/ukiryu/cli_commands/info_command.rb +7 -7
- data/lib/ukiryu/cli_commands/lint_command.rb +167 -0
- data/lib/ukiryu/cli_commands/list_command.rb +6 -6
- data/lib/ukiryu/cli_commands/opts_command.rb +2 -2
- data/lib/ukiryu/cli_commands/opts_command.rb.fixed +1 -1
- data/lib/ukiryu/cli_commands/register_command.rb +144 -0
- data/lib/ukiryu/cli_commands/resolve_command.rb +124 -0
- data/lib/ukiryu/cli_commands/run_command.rb +38 -14
- data/lib/ukiryu/cli_commands/run_file_command.rb +2 -2
- data/lib/ukiryu/cli_commands/system_command.rb +50 -32
- data/lib/ukiryu/cli_commands/validate_command.rb +452 -51
- data/lib/ukiryu/cli_commands/which_command.rb +5 -5
- data/lib/ukiryu/command_builder.rb +81 -23
- data/lib/ukiryu/config/env_provider.rb +3 -3
- data/lib/ukiryu/config/env_schema.rb +6 -6
- data/lib/ukiryu/config.rb +11 -11
- data/lib/ukiryu/definition/definition_cache.rb +238 -0
- data/lib/ukiryu/definition/definition_composer.rb +257 -0
- data/lib/ukiryu/definition/definition_linter.rb +460 -0
- data/lib/ukiryu/definition/definition_validator.rb +320 -0
- data/lib/ukiryu/definition/discovery.rb +239 -0
- data/lib/ukiryu/definition/documentation_generator.rb +429 -0
- data/lib/ukiryu/definition/lint_issue.rb +168 -0
- data/lib/ukiryu/definition/loader.rb +139 -0
- data/lib/ukiryu/definition/metadata.rb +159 -0
- data/lib/ukiryu/definition/source.rb +87 -0
- data/lib/ukiryu/definition/sources/file.rb +138 -0
- data/lib/ukiryu/definition/sources/string.rb +88 -0
- data/lib/ukiryu/definition/validation_result.rb +158 -0
- data/lib/ukiryu/definition/version_resolver.rb +194 -0
- data/lib/ukiryu/definition.rb +40 -0
- data/lib/ukiryu/errors.rb +6 -0
- data/lib/ukiryu/execution_context.rb +11 -11
- data/lib/ukiryu/executor.rb +6 -0
- data/lib/ukiryu/extractors/extractor.rb +6 -5
- data/lib/ukiryu/extractors/help_parser.rb +13 -19
- data/lib/ukiryu/logger.rb +3 -1
- data/lib/ukiryu/models/command_definition.rb +3 -3
- data/lib/ukiryu/models/command_info.rb +1 -1
- data/lib/ukiryu/models/components.rb +1 -3
- data/lib/ukiryu/models/env_var_definition.rb +11 -3
- data/lib/ukiryu/models/flag_definition.rb +15 -0
- data/lib/ukiryu/models/option_definition.rb +7 -7
- data/lib/ukiryu/models/platform_profile.rb +6 -3
- data/lib/ukiryu/models/routing.rb +1 -1
- data/lib/ukiryu/models/tool_definition.rb +2 -4
- data/lib/ukiryu/models/tool_metadata.rb +6 -6
- data/lib/ukiryu/models/validation_result.rb +1 -1
- data/lib/ukiryu/models/version_compatibility.rb +6 -3
- data/lib/ukiryu/models/version_detection.rb +10 -1
- data/lib/ukiryu/{registry.rb → register.rb} +54 -38
- data/lib/ukiryu/register_auto_manager.rb +268 -0
- data/lib/ukiryu/schema_validator.rb +31 -10
- data/lib/ukiryu/shell/base.rb +18 -0
- data/lib/ukiryu/shell/bash.rb +19 -1
- data/lib/ukiryu/shell/cmd.rb +11 -1
- data/lib/ukiryu/shell/powershell.rb +11 -1
- data/lib/ukiryu/shell.rb +1 -1
- data/lib/ukiryu/tool.rb +107 -95
- data/lib/ukiryu/tool_index.rb +22 -22
- data/lib/ukiryu/tools/base.rb +12 -25
- data/lib/ukiryu/tools/generator.rb +7 -7
- data/lib/ukiryu/tools.rb +3 -3
- data/lib/ukiryu/type.rb +20 -5
- data/lib/ukiryu/version.rb +1 -1
- data/lib/ukiryu/version_detector.rb +21 -2
- data/lib/ukiryu.rb +6 -3
- data/ukiryu-proposal.md +41 -41
- data/ukiryu.gemspec +1 -0
- metadata +64 -8
- data/.gitmodules +0 -3
data/lib/ukiryu/tool.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '
|
|
3
|
+
require_relative 'register'
|
|
4
4
|
require_relative 'executor'
|
|
5
5
|
require_relative 'shell'
|
|
6
6
|
require_relative 'runtime'
|
|
@@ -48,7 +48,7 @@ module Ukiryu
|
|
|
48
48
|
#
|
|
49
49
|
# @param name [String] the tool name
|
|
50
50
|
# @param options [Hash] initialization options
|
|
51
|
-
# @option options [String] :
|
|
51
|
+
# @option options [String] :register_path path to tool profiles
|
|
52
52
|
# @option options [Symbol] :platform platform to use
|
|
53
53
|
# @option options [Symbol] :shell shell to use
|
|
54
54
|
# @option options [String] :version specific version to use
|
|
@@ -59,7 +59,7 @@ module Ukiryu
|
|
|
59
59
|
cached = tools_cache[cache_key]
|
|
60
60
|
return cached if cached
|
|
61
61
|
|
|
62
|
-
# Load profile from
|
|
62
|
+
# Load profile from register
|
|
63
63
|
profile = load_profile(name, options)
|
|
64
64
|
raise ToolNotFoundError, "Tool not found: #{name}" unless profile
|
|
65
65
|
|
|
@@ -95,8 +95,8 @@ module Ukiryu
|
|
|
95
95
|
begin
|
|
96
96
|
tool = get(identifier, options)
|
|
97
97
|
if logger.debug_enabled?
|
|
98
|
-
require_relative '
|
|
99
|
-
all_tools =
|
|
98
|
+
require_relative 'register'
|
|
99
|
+
all_tools = Register.tools
|
|
100
100
|
logger.debug_section_tool_resolution(
|
|
101
101
|
identifier: identifier,
|
|
102
102
|
platform: platform,
|
|
@@ -134,8 +134,8 @@ module Ukiryu
|
|
|
134
134
|
end
|
|
135
135
|
|
|
136
136
|
# 4. Fallback to exhaustive search (should rarely reach here)
|
|
137
|
-
require_relative '
|
|
138
|
-
all_tools =
|
|
137
|
+
require_relative 'register'
|
|
138
|
+
all_tools = Register.tools
|
|
139
139
|
|
|
140
140
|
all_tools.each do |tool_name|
|
|
141
141
|
tool_def = Tools::Generator.load_tool_definition(tool_name)
|
|
@@ -215,6 +215,32 @@ module Ukiryu
|
|
|
215
215
|
Tools::Generator.clear_cache
|
|
216
216
|
end
|
|
217
217
|
|
|
218
|
+
# Clear the definition cache only
|
|
219
|
+
#
|
|
220
|
+
# @api public
|
|
221
|
+
def clear_definition_cache
|
|
222
|
+
require_relative 'definition/loader'
|
|
223
|
+
Definition::Loader.clear_cache
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Alias for load - load from file path
|
|
227
|
+
#
|
|
228
|
+
# @param file_path [String] path to the YAML file
|
|
229
|
+
# @param options [Hash] initialization options
|
|
230
|
+
# @return [Tool] the tool instance
|
|
231
|
+
def from_file(file_path, options = {})
|
|
232
|
+
load(file_path, options)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Alias for load_from_string - load from YAML string
|
|
236
|
+
#
|
|
237
|
+
# @param yaml_string [String] YAML content
|
|
238
|
+
# @param options [Hash] initialization options
|
|
239
|
+
# @return [Tool] the tool instance
|
|
240
|
+
def from_definition(yaml_string, options = {})
|
|
241
|
+
load_from_string(yaml_string, options)
|
|
242
|
+
end
|
|
243
|
+
|
|
218
244
|
# Configure default options
|
|
219
245
|
#
|
|
220
246
|
# @param options [Hash] default options
|
|
@@ -230,15 +256,14 @@ module Ukiryu
|
|
|
230
256
|
# @option options [Symbol] :validation validation mode (:strict, :lenient, :none)
|
|
231
257
|
# @option options [Symbol] :version_check version check mode (:strict, :lenient, :probe)
|
|
232
258
|
# @return [Tool] the tool instance
|
|
233
|
-
# @raise [
|
|
259
|
+
# @raise [DefinitionLoadError] if file cannot be loaded or validation fails
|
|
234
260
|
def load(file_path, options = {})
|
|
235
|
-
|
|
236
|
-
require_relative '
|
|
261
|
+
require_relative 'definition/loader'
|
|
262
|
+
require_relative 'definition/sources/file'
|
|
237
263
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
load_from_string(content, options.merge(file_path: file_path))
|
|
264
|
+
source = Definition::Sources::FileSource.new(file_path)
|
|
265
|
+
profile = Definition::Loader.load_from_source(source, options)
|
|
266
|
+
new(profile, options.merge(definition_source: source))
|
|
242
267
|
end
|
|
243
268
|
|
|
244
269
|
# Load a tool definition from a YAML string
|
|
@@ -249,25 +274,14 @@ module Ukiryu
|
|
|
249
274
|
# @option options [Symbol] :validation validation mode (:strict, :lenient, :none)
|
|
250
275
|
# @option options [Symbol] :version_check version check mode (:strict, :lenient, :probe)
|
|
251
276
|
# @return [Tool] the tool instance
|
|
252
|
-
# @raise [
|
|
277
|
+
# @raise [DefinitionLoadError] if YAML cannot be parsed or validation fails
|
|
253
278
|
def load_from_string(yaml_string, options = {})
|
|
254
|
-
require_relative '
|
|
255
|
-
|
|
256
|
-
begin
|
|
257
|
-
# Use lutaml-model's from_yaml to parse
|
|
258
|
-
profile = Models::ToolDefinition.from_yaml(yaml_string)
|
|
259
|
-
rescue Psych::SyntaxError => e
|
|
260
|
-
raise LoadError, "Invalid YAML: #{e.message}"
|
|
261
|
-
rescue StandardError => e
|
|
262
|
-
raise LoadError, "Invalid YAML: #{e.message}"
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
# Validate profile if validation mode is set
|
|
266
|
-
validation_mode = options[:validation] || :strict
|
|
267
|
-
validate_profile(profile, validation_mode) if validation_mode != :none
|
|
279
|
+
require_relative 'definition/loader'
|
|
280
|
+
require_relative 'definition/sources/string'
|
|
268
281
|
|
|
269
|
-
|
|
270
|
-
|
|
282
|
+
source = Definition::Sources::StringSource.new(yaml_string)
|
|
283
|
+
profile = Definition::Loader.load_from_source(source, options)
|
|
284
|
+
new(profile, options.merge(definition_source: source))
|
|
271
285
|
end
|
|
272
286
|
|
|
273
287
|
# Load a tool from bundled system locations
|
|
@@ -286,12 +300,10 @@ module Ukiryu
|
|
|
286
300
|
|
|
287
301
|
search_paths.each do |base_path|
|
|
288
302
|
Dir.glob(File.join(base_path, tool_name.to_s, '*.yaml')).each do |file|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
next
|
|
294
|
-
end
|
|
303
|
+
return load(file, options)
|
|
304
|
+
rescue DefinitionLoadError, DefinitionNotFoundError
|
|
305
|
+
# Try next file
|
|
306
|
+
next
|
|
295
307
|
end
|
|
296
308
|
end
|
|
297
309
|
|
|
@@ -367,47 +379,6 @@ module Ukiryu
|
|
|
367
379
|
|
|
368
380
|
private
|
|
369
381
|
|
|
370
|
-
# Validate a tool profile
|
|
371
|
-
#
|
|
372
|
-
# @param profile [Models::ToolDefinition] the profile to validate
|
|
373
|
-
# @param mode [Symbol] validation mode (:strict, :lenient)
|
|
374
|
-
# @raise [LoadError] if validation fails in strict mode
|
|
375
|
-
def validate_profile(profile, mode)
|
|
376
|
-
errors = []
|
|
377
|
-
|
|
378
|
-
# Check required fields
|
|
379
|
-
errors << "Missing 'name' field" unless profile.name
|
|
380
|
-
errors << "Missing 'version' field" unless profile.version
|
|
381
|
-
errors << "Missing 'profiles' field or profiles is empty" unless profile.profiles&.any?
|
|
382
|
-
|
|
383
|
-
# Check ukiryu_schema format if present
|
|
384
|
-
if profile.ukiryu_schema && !profile.ukiryu_schema.match?(/^\d+\.\d+$/)
|
|
385
|
-
errors << "Invalid ukiryu_schema format: #{profile.ukiryu_schema}"
|
|
386
|
-
end
|
|
387
|
-
|
|
388
|
-
# Check $self URI format if present
|
|
389
|
-
if profile.self_uri && !valid_uri?(profile.self_uri)
|
|
390
|
-
errors << "Invalid $self URI format: #{profile.self_uri}" if mode == :strict
|
|
391
|
-
end
|
|
392
|
-
|
|
393
|
-
if errors.any?
|
|
394
|
-
message = "Profile validation failed:\n - #{errors.join("\n - ")}"
|
|
395
|
-
if mode == :strict
|
|
396
|
-
raise LoadError, message
|
|
397
|
-
else
|
|
398
|
-
warn "[Ukiryu] #{message}" if mode == :lenient
|
|
399
|
-
end
|
|
400
|
-
end
|
|
401
|
-
end
|
|
402
|
-
|
|
403
|
-
# Check if a string is a valid URI
|
|
404
|
-
#
|
|
405
|
-
# @param uri_string [String] the URI to check
|
|
406
|
-
# @return [Boolean] true if valid URI
|
|
407
|
-
def valid_uri?(uri_string)
|
|
408
|
-
(uri_string =~ %r{^https?://} || uri_string =~ %r{^file://}) ? true : false
|
|
409
|
-
end
|
|
410
|
-
|
|
411
382
|
# Generate a cache key for a tool
|
|
412
383
|
def cache_key_for(name, options)
|
|
413
384
|
runtime = Runtime.instance
|
|
@@ -434,9 +405,11 @@ module Ukiryu
|
|
|
434
405
|
#
|
|
435
406
|
# @param profile [Models::ToolDefinition] the tool definition model
|
|
436
407
|
# @param options [Hash] initialization options
|
|
408
|
+
# @option options [Definition::Source] :definition_source the source of this definition
|
|
437
409
|
def initialize(profile, options = {})
|
|
438
410
|
@profile = profile
|
|
439
411
|
@options = options
|
|
412
|
+
@definition_source = options[:definition_source]
|
|
440
413
|
runtime = Runtime.instance
|
|
441
414
|
|
|
442
415
|
# Allow override via options for testing
|
|
@@ -471,6 +444,25 @@ module Ukiryu
|
|
|
471
444
|
@version || detect_version
|
|
472
445
|
end
|
|
473
446
|
|
|
447
|
+
# Get the definition source if loaded from non-register source
|
|
448
|
+
#
|
|
449
|
+
# @return [Definition::Source, nil] the definition source
|
|
450
|
+
attr_reader :definition_source
|
|
451
|
+
|
|
452
|
+
# Get the definition path if loaded from file
|
|
453
|
+
#
|
|
454
|
+
# @return [String, nil] the file path
|
|
455
|
+
def definition_path
|
|
456
|
+
@definition_source&.path if @definition_source.respond_to?(:path)
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# Get the definition mtime if loaded from file
|
|
460
|
+
#
|
|
461
|
+
# @return [Time, nil] the file modification time
|
|
462
|
+
def definition_mtime
|
|
463
|
+
@definition_source&.mtime if @definition_source.respond_to?(:mtime)
|
|
464
|
+
end
|
|
465
|
+
|
|
474
466
|
# Get the executable path
|
|
475
467
|
#
|
|
476
468
|
# @return [String] the executable path
|
|
@@ -528,7 +520,7 @@ module Ukiryu
|
|
|
528
520
|
Executor.execute(
|
|
529
521
|
@executable,
|
|
530
522
|
args,
|
|
531
|
-
env: build_env_vars(command, params),
|
|
523
|
+
env: build_env_vars(command, @command_profile, params),
|
|
532
524
|
timeout: @profile.timeout || 90,
|
|
533
525
|
shell: @shell,
|
|
534
526
|
stdin: stdin,
|
|
@@ -658,7 +650,7 @@ module Ukiryu
|
|
|
658
650
|
Executor.execute(
|
|
659
651
|
resolution[:executable],
|
|
660
652
|
args,
|
|
661
|
-
env: build_env_vars(action, params),
|
|
653
|
+
env: build_env_vars(action, @command_profile, params),
|
|
662
654
|
timeout: @profile.timeout || 90,
|
|
663
655
|
shell: @shell,
|
|
664
656
|
stdin: stdin,
|
|
@@ -726,15 +718,37 @@ module Ukiryu
|
|
|
726
718
|
end
|
|
727
719
|
|
|
728
720
|
# Detect tool version using VersionDetector
|
|
721
|
+
#
|
|
722
|
+
# @return [String, nil] the detected version or nil if not detected
|
|
723
|
+
public
|
|
724
|
+
|
|
729
725
|
def detect_version
|
|
730
726
|
vd = @profile.version_detection
|
|
731
727
|
return nil unless vd
|
|
732
728
|
|
|
729
|
+
# Only attempt version detection if command is configured
|
|
730
|
+
return nil if vd.command.nil? || vd.command.empty?
|
|
731
|
+
|
|
732
|
+
# For man page detection, the executable is 'man' and command is the tool name
|
|
733
|
+
# For command detection, the executable is the tool itself
|
|
734
|
+
source = vd.respond_to?(:source) ? vd.source : 'command'
|
|
735
|
+
if source == 'man'
|
|
736
|
+
# command is ['man', 'tool_name'], so:
|
|
737
|
+
# - executable = 'man'
|
|
738
|
+
# - command = ['tool_name'] (just the tool name for man)
|
|
739
|
+
executable = 'man'
|
|
740
|
+
command_args = vd.command[1..] # Skip 'man', use rest of array
|
|
741
|
+
else
|
|
742
|
+
executable = @executable
|
|
743
|
+
command_args = vd.command
|
|
744
|
+
end
|
|
745
|
+
|
|
733
746
|
VersionDetector.detect(
|
|
734
|
-
executable:
|
|
735
|
-
command:
|
|
747
|
+
executable: executable,
|
|
748
|
+
command: command_args,
|
|
736
749
|
pattern: vd.pattern || /(\d+\.\d+)/,
|
|
737
|
-
shell: @shell
|
|
750
|
+
shell: @shell,
|
|
751
|
+
source: source
|
|
738
752
|
)
|
|
739
753
|
end
|
|
740
754
|
|
|
@@ -749,20 +763,20 @@ module Ukiryu
|
|
|
749
763
|
requirement = profile_version_requirement
|
|
750
764
|
|
|
751
765
|
# If no requirement, always compatible
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
766
|
+
if !requirement || requirement.empty?
|
|
767
|
+
return VersionCompatibility.new(
|
|
768
|
+
installed_version: installed || 'unknown',
|
|
769
|
+
required_version: 'none',
|
|
770
|
+
compatible: true,
|
|
771
|
+
reason: nil
|
|
772
|
+
)
|
|
773
|
+
end
|
|
758
774
|
|
|
759
775
|
# If installed version unknown, probe for it
|
|
760
|
-
if !installed && mode == :probe
|
|
761
|
-
installed = detect_version
|
|
762
|
-
end
|
|
776
|
+
installed = detect_version if !installed && mode == :probe
|
|
763
777
|
|
|
764
778
|
# If still unknown, handle based on mode
|
|
765
|
-
|
|
779
|
+
unless installed
|
|
766
780
|
if mode == :strict
|
|
767
781
|
return VersionCompatibility.new(
|
|
768
782
|
installed_version: 'unknown',
|
|
@@ -818,8 +832,6 @@ module Ukiryu
|
|
|
818
832
|
result.success? && !result.stderr.include?('unknown')
|
|
819
833
|
end
|
|
820
834
|
|
|
821
|
-
private
|
|
822
|
-
|
|
823
835
|
# Get version requirement from compatible profile
|
|
824
836
|
#
|
|
825
837
|
# @return [String, nil] the version requirement
|
data/lib/ukiryu/tool_index.rb
CHANGED
|
@@ -9,7 +9,7 @@ module Ukiryu
|
|
|
9
9
|
# This class maintains cached mappings for:
|
|
10
10
|
# - Interfaces to tools (multiple tools can implement one interface)
|
|
11
11
|
# - Aliases to tool names
|
|
12
|
-
# -
|
|
12
|
+
# - Register change detection via mtime
|
|
13
13
|
#
|
|
14
14
|
# Built once and cached for the lifetime of the process.
|
|
15
15
|
#
|
|
@@ -31,13 +31,13 @@ module Ukiryu
|
|
|
31
31
|
|
|
32
32
|
# Initialize the index
|
|
33
33
|
#
|
|
34
|
-
# @param
|
|
35
|
-
def initialize(
|
|
36
|
-
@
|
|
34
|
+
# @param register_path [String] the path to the tool register
|
|
35
|
+
def initialize(register_path: Ukiryu::Register.default_register_path)
|
|
36
|
+
@register_path = register_path
|
|
37
37
|
@interface_to_tools = {} # interface => [tool_names]
|
|
38
38
|
@alias_to_tool = {} # alias => tool_name
|
|
39
39
|
@built = false
|
|
40
|
-
@cache_key = nil
|
|
40
|
+
@cache_key = nil # Register state for change detection
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
# Find tool metadata by interface name
|
|
@@ -89,7 +89,7 @@ module Ukiryu
|
|
|
89
89
|
@interface_to_tools.dup
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
-
# Check if the index needs rebuilding due to
|
|
92
|
+
# Check if the index needs rebuilding due to register changes
|
|
93
93
|
#
|
|
94
94
|
# @return [Boolean] true if rebuild is needed
|
|
95
95
|
def stale?
|
|
@@ -99,13 +99,13 @@ module Ukiryu
|
|
|
99
99
|
@cache_key != current_cache_key
|
|
100
100
|
end
|
|
101
101
|
|
|
102
|
-
# Update the
|
|
102
|
+
# Update the register path
|
|
103
103
|
#
|
|
104
|
-
# @param new_path [String] the new
|
|
105
|
-
def
|
|
106
|
-
return if @
|
|
104
|
+
# @param new_path [String] the new register path
|
|
105
|
+
def register_path=(new_path)
|
|
106
|
+
return if @register_path == new_path
|
|
107
107
|
|
|
108
|
-
@
|
|
108
|
+
@register_path = new_path
|
|
109
109
|
@built = false # Rebuild index with new path
|
|
110
110
|
@cache_key = nil
|
|
111
111
|
@interface_to_tools = {}
|
|
@@ -119,12 +119,12 @@ module Ukiryu
|
|
|
119
119
|
build_index if stale?
|
|
120
120
|
end
|
|
121
121
|
|
|
122
|
-
# Build cache key for
|
|
123
|
-
# Uses mtime of
|
|
122
|
+
# Build cache key for register change detection
|
|
123
|
+
# Uses mtime of register directory + file count for fast comparison
|
|
124
124
|
#
|
|
125
125
|
# @return [String] the cache key
|
|
126
126
|
def build_cache_key
|
|
127
|
-
current_path =
|
|
127
|
+
current_path = register_path
|
|
128
128
|
return 'empty' unless current_path
|
|
129
129
|
|
|
130
130
|
tools_dir = File.join(current_path, 'tools')
|
|
@@ -137,17 +137,17 @@ module Ukiryu
|
|
|
137
137
|
"#{mtime}-#{file_count}"
|
|
138
138
|
end
|
|
139
139
|
|
|
140
|
-
# Get the current
|
|
140
|
+
# Get the current register path
|
|
141
141
|
#
|
|
142
|
-
# @return [String, nil] the
|
|
143
|
-
def
|
|
144
|
-
@
|
|
142
|
+
# @return [String, nil] the register path
|
|
143
|
+
def register_path
|
|
144
|
+
@register_path ||= Ukiryu::Register.default_register_path
|
|
145
145
|
end
|
|
146
146
|
|
|
147
147
|
# Build the index by scanning tool directories
|
|
148
148
|
# This is done once and cached
|
|
149
149
|
def build_index
|
|
150
|
-
current_path =
|
|
150
|
+
current_path = register_path
|
|
151
151
|
return unless current_path
|
|
152
152
|
|
|
153
153
|
tools_dir = File.join(current_path, 'tools')
|
|
@@ -175,7 +175,7 @@ module Ukiryu
|
|
|
175
175
|
|
|
176
176
|
# Index by alias
|
|
177
177
|
aliases = hash['aliases']
|
|
178
|
-
if aliases
|
|
178
|
+
if aliases.respond_to?(:each)
|
|
179
179
|
aliases.each do |alias_name|
|
|
180
180
|
@alias_to_tool[alias_name.to_sym] = tool_sym
|
|
181
181
|
end
|
|
@@ -200,7 +200,7 @@ module Ukiryu
|
|
|
200
200
|
hash = YAML.safe_load(yaml_content, permitted_classes: [Symbol])
|
|
201
201
|
return nil unless hash
|
|
202
202
|
|
|
203
|
-
ToolMetadata.from_hash(hash, tool_name: tool_name.to_s,
|
|
203
|
+
ToolMetadata.from_hash(hash, tool_name: tool_name.to_s, register_path: register_path)
|
|
204
204
|
end
|
|
205
205
|
|
|
206
206
|
# Load YAML content for a specific tool
|
|
@@ -208,7 +208,7 @@ module Ukiryu
|
|
|
208
208
|
# @param tool_name [Symbol, String] the tool name
|
|
209
209
|
# @return [String, nil] the YAML content
|
|
210
210
|
def load_yaml_for_tool(tool_name)
|
|
211
|
-
current_path =
|
|
211
|
+
current_path = register_path
|
|
212
212
|
return nil unless current_path
|
|
213
213
|
|
|
214
214
|
# Search for version files
|
data/lib/ukiryu/tools/base.rb
CHANGED
|
@@ -251,8 +251,8 @@ module Ukiryu
|
|
|
251
251
|
# Get shell
|
|
252
252
|
shell_sym = Ukiryu::Runtime.instance.shell
|
|
253
253
|
|
|
254
|
-
# Build environment variables (including
|
|
255
|
-
env = build_execution_env(command_def, params)
|
|
254
|
+
# Build environment variables (including env var sets)
|
|
255
|
+
env = build_execution_env(command_def, @platform_profile, params)
|
|
256
256
|
|
|
257
257
|
# Execute
|
|
258
258
|
result = Ukiryu::Executor.execute(
|
|
@@ -299,30 +299,14 @@ module Ukiryu
|
|
|
299
299
|
args
|
|
300
300
|
end
|
|
301
301
|
|
|
302
|
-
# Build execution environment
|
|
302
|
+
# Build execution environment
|
|
303
303
|
#
|
|
304
304
|
# @param command_def [Models::CommandDefinition] the command definition
|
|
305
|
+
# @param profile [Models::PlatformProfile] the profile (for env_var_sets)
|
|
305
306
|
# @param params [Hash] the parameters
|
|
306
307
|
# @return [Hash] environment variables
|
|
307
|
-
def build_execution_env(command_def, params)
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
# Handle execution modes
|
|
311
|
-
execution_mode = command_def.execution_mode
|
|
312
|
-
|
|
313
|
-
case execution_mode
|
|
314
|
-
when 'headless', :headless
|
|
315
|
-
# Set headless environment variables
|
|
316
|
-
platform = Ukiryu::Runtime.instance.platform
|
|
317
|
-
case platform
|
|
318
|
-
when :macos, :linux
|
|
319
|
-
env['DISPLAY'] ||= '' # Disable X11 display
|
|
320
|
-
when :windows
|
|
321
|
-
# Windows doesn't need special handling for headless
|
|
322
|
-
end
|
|
323
|
-
end
|
|
324
|
-
|
|
325
|
-
env
|
|
308
|
+
def build_execution_env(command_def, profile, params)
|
|
309
|
+
build_env_vars(command_def, profile, params)
|
|
326
310
|
end
|
|
327
311
|
|
|
328
312
|
# Check if command needs batch processing flag for headless mode
|
|
@@ -330,8 +314,8 @@ module Ukiryu
|
|
|
330
314
|
# @param command_def [Models::CommandDefinition] the command definition
|
|
331
315
|
# @return [Boolean] true if batch processing flag should be added
|
|
332
316
|
def needs_batch_process_flag?(command_def)
|
|
333
|
-
|
|
334
|
-
|
|
317
|
+
# Check if command uses the 'headless' env var set
|
|
318
|
+
command_def.use_env_vars&.include?('headless') || false
|
|
335
319
|
end
|
|
336
320
|
|
|
337
321
|
# Build a response object
|
|
@@ -356,7 +340,10 @@ module Ukiryu
|
|
|
356
340
|
vd = self.class.tool_definition.version_detection
|
|
357
341
|
return nil unless vd
|
|
358
342
|
|
|
359
|
-
|
|
343
|
+
# Only attempt version detection if command is configured
|
|
344
|
+
return nil if vd.command.nil? || vd.command.empty?
|
|
345
|
+
|
|
346
|
+
cmd = vd.command
|
|
360
347
|
|
|
361
348
|
executable = ExecutableFinder.find_executable(self.class.tool_definition.name.to_s, self.class.tool_definition)
|
|
362
349
|
return nil unless executable
|
|
@@ -45,15 +45,15 @@ module Ukiryu
|
|
|
45
45
|
tool_class
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
# Load a ToolDefinition model from the
|
|
48
|
+
# Load a ToolDefinition model from the register
|
|
49
49
|
#
|
|
50
50
|
# @param tool_name [Symbol] the tool name
|
|
51
51
|
# @return [Models::ToolDefinition, nil] the tool definition model
|
|
52
52
|
def load_tool_definition(tool_name)
|
|
53
|
-
require_relative '../
|
|
53
|
+
require_relative '../register'
|
|
54
54
|
|
|
55
55
|
# Load the YAML file content
|
|
56
|
-
yaml_content =
|
|
56
|
+
yaml_content = Register.load_tool_yaml(tool_name)
|
|
57
57
|
return nil unless yaml_content
|
|
58
58
|
|
|
59
59
|
# Use lutaml-model's from_yaml to parse
|
|
@@ -136,12 +136,12 @@ module Ukiryu
|
|
|
136
136
|
#
|
|
137
137
|
# @return [Array<Symbol>] list of tool names
|
|
138
138
|
def available_tools
|
|
139
|
-
require_relative '../
|
|
139
|
+
require_relative '../register'
|
|
140
140
|
|
|
141
|
-
|
|
142
|
-
return [] unless
|
|
141
|
+
register_path = Register.default_register_path
|
|
142
|
+
return [] unless register_path
|
|
143
143
|
|
|
144
|
-
tools_dir = File.join(
|
|
144
|
+
tools_dir = File.join(register_path, 'tools')
|
|
145
145
|
return [] unless Dir.exist?(tools_dir)
|
|
146
146
|
|
|
147
147
|
Dir.entries(tools_dir)
|
data/lib/ukiryu/tools.rb
CHANGED
|
@@ -65,10 +65,10 @@ module Ukiryu
|
|
|
65
65
|
# @param alias_name [Symbol] the alias to resolve
|
|
66
66
|
# @return [Symbol, nil] the platform-specific tool name
|
|
67
67
|
def find_platform_implementation(alias_name)
|
|
68
|
-
|
|
69
|
-
return nil unless
|
|
68
|
+
register_path = Register.default_register_path
|
|
69
|
+
return nil unless register_path && Dir.exist?(register_path)
|
|
70
70
|
|
|
71
|
-
tools_dir = File.join(
|
|
71
|
+
tools_dir = File.join(register_path, 'tools')
|
|
72
72
|
return nil unless Dir.exist?(tools_dir)
|
|
73
73
|
|
|
74
74
|
current_platform = Platform.detect
|
data/lib/ukiryu/type.rb
CHANGED
|
@@ -88,7 +88,10 @@ module Ukiryu
|
|
|
88
88
|
value = value.to_s
|
|
89
89
|
raise ValidationError, 'String cannot be empty' if value.empty? && !options[:allow_empty]
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
if options[:pattern] && value !~ options[:pattern]
|
|
92
|
+
raise ValidationError,
|
|
93
|
+
"String does not match required pattern: #{options[:pattern]}"
|
|
94
|
+
end
|
|
92
95
|
|
|
93
96
|
value
|
|
94
97
|
end
|
|
@@ -146,7 +149,10 @@ module Ukiryu
|
|
|
146
149
|
if options[:values]
|
|
147
150
|
# Convert values to symbols for comparison (handle both string and symbol values)
|
|
148
151
|
valid_values = options[:values].map { |v| v.is_a?(String) ? v.to_sym : v }
|
|
149
|
-
|
|
152
|
+
unless valid_values.include?(value)
|
|
153
|
+
raise ValidationError,
|
|
154
|
+
"Invalid symbol: #{value.inspect}. Valid values: #{options[:values].inspect}"
|
|
155
|
+
end
|
|
150
156
|
end
|
|
151
157
|
|
|
152
158
|
value
|
|
@@ -214,9 +220,15 @@ module Ukiryu
|
|
|
214
220
|
def validate_array(value, options)
|
|
215
221
|
array = value.is_a?(Array) ? value : [value]
|
|
216
222
|
|
|
217
|
-
|
|
223
|
+
if options[:min] && array.size < options[:min]
|
|
224
|
+
raise ValidationError,
|
|
225
|
+
"Array has #{array.size} elements, minimum is #{options[:min]}"
|
|
226
|
+
end
|
|
218
227
|
|
|
219
|
-
|
|
228
|
+
if options[:max] && array.size > options[:max]
|
|
229
|
+
raise ValidationError,
|
|
230
|
+
"Array has #{array.size} elements, maximum is #{options[:max]}"
|
|
231
|
+
end
|
|
220
232
|
|
|
221
233
|
if options[:size]
|
|
222
234
|
if options[:size].is_a?(Integer)
|
|
@@ -225,7 +237,10 @@ module Ukiryu
|
|
|
225
237
|
"Array has #{array.size} elements, expected #{options[:size]}"
|
|
226
238
|
end
|
|
227
239
|
elsif options[:size].is_a?(Array)
|
|
228
|
-
|
|
240
|
+
unless options[:size].include?(array.size)
|
|
241
|
+
raise ValidationError,
|
|
242
|
+
"Array has #{array.size} elements, expected one of: #{options[:size].inspect}"
|
|
243
|
+
end
|
|
229
244
|
end
|
|
230
245
|
end
|
|
231
246
|
|
data/lib/ukiryu/version.rb
CHANGED
|
@@ -9,14 +9,23 @@ module Ukiryu
|
|
|
9
9
|
# - Configurable version command patterns
|
|
10
10
|
# - Regex pattern matching for version strings
|
|
11
11
|
# - Proper shell handling for command execution
|
|
12
|
+
# - Support for man-page based version detection (BSD/system tools)
|
|
12
13
|
#
|
|
13
|
-
# @example Detecting version
|
|
14
|
+
# @example Detecting version from command output (GNU tools)
|
|
14
15
|
# version = VersionDetector.detect(
|
|
15
16
|
# executable: '/usr/bin/ffmpeg',
|
|
16
17
|
# command: '-version',
|
|
17
18
|
# pattern: /version (\d+\.\d+)/,
|
|
18
19
|
# shell: :bash
|
|
19
20
|
# )
|
|
21
|
+
#
|
|
22
|
+
# @example Detecting version from man page (BSD/system tools)
|
|
23
|
+
# version = VersionDetector.detect(
|
|
24
|
+
# executable: '/usr/bin/man',
|
|
25
|
+
# command: ['man', 'find'],
|
|
26
|
+
# pattern: /macOS ([\d.]+)/,
|
|
27
|
+
# source: 'man'
|
|
28
|
+
# )
|
|
20
29
|
module VersionDetector
|
|
21
30
|
class << self
|
|
22
31
|
# Detect the version of an external tool
|
|
@@ -25,8 +34,9 @@ module Ukiryu
|
|
|
25
34
|
# @param command [String, Array<String>] the version command (default: '--version')
|
|
26
35
|
# @param pattern [Regexp] the regex pattern to extract version
|
|
27
36
|
# @param shell [Symbol] the shell to use for execution
|
|
37
|
+
# @param source [String] the version source: 'command' (default) or 'man'
|
|
28
38
|
# @return [String, nil] the detected version or nil if not found
|
|
29
|
-
def detect(executable:, command: '--version', pattern: /(\d+\.\d+)/, shell: nil)
|
|
39
|
+
def detect(executable:, command: '--version', pattern: /(\d+\.\d+)/, shell: nil, source: 'command')
|
|
30
40
|
# Return nil if executable is not found
|
|
31
41
|
return nil if executable.nil? || executable.empty?
|
|
32
42
|
|
|
@@ -43,6 +53,15 @@ module Ukiryu
|
|
|
43
53
|
stdout = result.stdout.scrub
|
|
44
54
|
stderr = result.stderr.scrub
|
|
45
55
|
|
|
56
|
+
# For man pages, look at the tail (last few lines)
|
|
57
|
+
if source == 'man'
|
|
58
|
+
output = stdout + stderr
|
|
59
|
+
# Get last 500 characters to catch the OS version at bottom
|
|
60
|
+
tail = output[-500..] || output
|
|
61
|
+
match = tail.match(pattern)
|
|
62
|
+
return match[1] if match
|
|
63
|
+
end
|
|
64
|
+
|
|
46
65
|
match = stdout.match(pattern) || stderr.match(pattern)
|
|
47
66
|
match[1] if match
|
|
48
67
|
end
|