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.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +58 -14
  3. data/.gitignore +3 -0
  4. data/.rubocop_todo.yml +170 -79
  5. data/Gemfile +1 -1
  6. data/README.adoc +1603 -576
  7. data/docs/.gitignore +1 -0
  8. data/docs/Gemfile +10 -0
  9. data/docs/INDEX.adoc +261 -0
  10. data/docs/_config.yml +180 -0
  11. data/docs/advanced/custom-tool-classes.adoc +581 -0
  12. data/docs/advanced/index.adoc +20 -0
  13. data/docs/features/configuration.adoc +657 -0
  14. data/docs/features/index.adoc +31 -0
  15. data/docs/features/platform-support.adoc +488 -0
  16. data/docs/getting-started/core-concepts.adoc +666 -0
  17. data/docs/getting-started/index.adoc +36 -0
  18. data/docs/getting-started/installation.adoc +216 -0
  19. data/docs/getting-started/quick-start.adoc +258 -0
  20. data/docs/guides/env-var-sets.adoc +388 -0
  21. data/docs/guides/index.adoc +20 -0
  22. data/docs/interfaces/cli.adoc +609 -0
  23. data/docs/interfaces/index.adoc +153 -0
  24. data/docs/interfaces/ruby-api.adoc +538 -0
  25. data/docs/lychee.toml +49 -0
  26. data/docs/reference/configuration-options.adoc +720 -0
  27. data/docs/reference/error-codes.adoc +634 -0
  28. data/docs/reference/index.adoc +20 -0
  29. data/docs/reference/ruby-api.adoc +1217 -0
  30. data/docs/understanding/index.adoc +20 -0
  31. data/lib/ukiryu/cli.rb +43 -58
  32. data/lib/ukiryu/cli_commands/base_command.rb +16 -27
  33. data/lib/ukiryu/cli_commands/cache_command.rb +100 -0
  34. data/lib/ukiryu/cli_commands/commands_command.rb +8 -8
  35. data/lib/ukiryu/cli_commands/commands_command.rb.fixed +1 -1
  36. data/lib/ukiryu/cli_commands/config_command.rb +49 -7
  37. data/lib/ukiryu/cli_commands/definitions_command.rb +254 -0
  38. data/lib/ukiryu/cli_commands/describe_command.rb +13 -7
  39. data/lib/ukiryu/cli_commands/describe_command.rb.fixed +1 -1
  40. data/lib/ukiryu/cli_commands/docs_command.rb +148 -0
  41. data/lib/ukiryu/cli_commands/exec_inline_command.rb.fixed +1 -1
  42. data/lib/ukiryu/cli_commands/extract_command.rb +2 -2
  43. data/lib/ukiryu/cli_commands/info_command.rb +7 -7
  44. data/lib/ukiryu/cli_commands/lint_command.rb +167 -0
  45. data/lib/ukiryu/cli_commands/list_command.rb +6 -6
  46. data/lib/ukiryu/cli_commands/opts_command.rb +2 -2
  47. data/lib/ukiryu/cli_commands/opts_command.rb.fixed +1 -1
  48. data/lib/ukiryu/cli_commands/register_command.rb +144 -0
  49. data/lib/ukiryu/cli_commands/resolve_command.rb +124 -0
  50. data/lib/ukiryu/cli_commands/run_command.rb +38 -14
  51. data/lib/ukiryu/cli_commands/run_file_command.rb +2 -2
  52. data/lib/ukiryu/cli_commands/system_command.rb +50 -32
  53. data/lib/ukiryu/cli_commands/validate_command.rb +452 -51
  54. data/lib/ukiryu/cli_commands/which_command.rb +5 -5
  55. data/lib/ukiryu/command_builder.rb +81 -23
  56. data/lib/ukiryu/config/env_provider.rb +3 -3
  57. data/lib/ukiryu/config/env_schema.rb +6 -6
  58. data/lib/ukiryu/config.rb +11 -11
  59. data/lib/ukiryu/definition/definition_cache.rb +238 -0
  60. data/lib/ukiryu/definition/definition_composer.rb +257 -0
  61. data/lib/ukiryu/definition/definition_linter.rb +460 -0
  62. data/lib/ukiryu/definition/definition_validator.rb +320 -0
  63. data/lib/ukiryu/definition/discovery.rb +239 -0
  64. data/lib/ukiryu/definition/documentation_generator.rb +429 -0
  65. data/lib/ukiryu/definition/lint_issue.rb +168 -0
  66. data/lib/ukiryu/definition/loader.rb +139 -0
  67. data/lib/ukiryu/definition/metadata.rb +159 -0
  68. data/lib/ukiryu/definition/source.rb +87 -0
  69. data/lib/ukiryu/definition/sources/file.rb +138 -0
  70. data/lib/ukiryu/definition/sources/string.rb +88 -0
  71. data/lib/ukiryu/definition/validation_result.rb +158 -0
  72. data/lib/ukiryu/definition/version_resolver.rb +194 -0
  73. data/lib/ukiryu/definition.rb +40 -0
  74. data/lib/ukiryu/errors.rb +6 -0
  75. data/lib/ukiryu/execution_context.rb +11 -11
  76. data/lib/ukiryu/executor.rb +6 -0
  77. data/lib/ukiryu/extractors/extractor.rb +6 -5
  78. data/lib/ukiryu/extractors/help_parser.rb +13 -19
  79. data/lib/ukiryu/logger.rb +3 -1
  80. data/lib/ukiryu/models/command_definition.rb +3 -3
  81. data/lib/ukiryu/models/command_info.rb +1 -1
  82. data/lib/ukiryu/models/components.rb +1 -3
  83. data/lib/ukiryu/models/env_var_definition.rb +11 -3
  84. data/lib/ukiryu/models/flag_definition.rb +15 -0
  85. data/lib/ukiryu/models/option_definition.rb +7 -7
  86. data/lib/ukiryu/models/platform_profile.rb +6 -3
  87. data/lib/ukiryu/models/routing.rb +1 -1
  88. data/lib/ukiryu/models/tool_definition.rb +2 -4
  89. data/lib/ukiryu/models/tool_metadata.rb +6 -6
  90. data/lib/ukiryu/models/validation_result.rb +1 -1
  91. data/lib/ukiryu/models/version_compatibility.rb +6 -3
  92. data/lib/ukiryu/models/version_detection.rb +10 -1
  93. data/lib/ukiryu/{registry.rb → register.rb} +54 -38
  94. data/lib/ukiryu/register_auto_manager.rb +268 -0
  95. data/lib/ukiryu/schema_validator.rb +31 -10
  96. data/lib/ukiryu/shell/base.rb +18 -0
  97. data/lib/ukiryu/shell/bash.rb +19 -1
  98. data/lib/ukiryu/shell/cmd.rb +11 -1
  99. data/lib/ukiryu/shell/powershell.rb +11 -1
  100. data/lib/ukiryu/shell.rb +1 -1
  101. data/lib/ukiryu/tool.rb +107 -95
  102. data/lib/ukiryu/tool_index.rb +22 -22
  103. data/lib/ukiryu/tools/base.rb +12 -25
  104. data/lib/ukiryu/tools/generator.rb +7 -7
  105. data/lib/ukiryu/tools.rb +3 -3
  106. data/lib/ukiryu/type.rb +20 -5
  107. data/lib/ukiryu/version.rb +1 -1
  108. data/lib/ukiryu/version_detector.rb +21 -2
  109. data/lib/ukiryu.rb +6 -3
  110. data/ukiryu-proposal.md +41 -41
  111. data/ukiryu.gemspec +1 -0
  112. metadata +64 -8
  113. 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 'registry'
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] :registry_path path to tool profiles
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 registry
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 'registry'
99
- all_tools = Registry.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 'registry'
138
- all_tools = Registry.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 [LoadError] if file cannot be loaded or validation fails
259
+ # @raise [DefinitionLoadError] if file cannot be loaded or validation fails
234
260
  def load(file_path, options = {})
235
- require 'yaml'
236
- require_relative 'models/tool_definition'
261
+ require_relative 'definition/loader'
262
+ require_relative 'definition/sources/file'
237
263
 
238
- raise LoadError, "File not found: #{file_path}" unless File.exist?(file_path)
239
-
240
- content = File.read(file_path)
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 [LoadError] if YAML cannot be parsed or validation fails
277
+ # @raise [DefinitionLoadError] if YAML cannot be parsed or validation fails
253
278
  def load_from_string(yaml_string, options = {})
254
- require_relative 'models/tool_definition'
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
- # Create tool instance
270
- new(profile, options)
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
- begin
290
- return load(file, options)
291
- rescue LoadError, NameError
292
- # Try next file
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: @executable,
735
- command: vd.command || '--version',
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
- return VersionCompatibility.new(
753
- installed_version: installed || 'unknown',
754
- required_version: 'none',
755
- compatible: true,
756
- reason: nil
757
- ) if !requirement || requirement.empty?
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
- if !installed
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
@@ -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
- # - Registry change detection via mtime
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 registry_path [String] the path to the tool registry
35
- def initialize(registry_path: Ukiryu::Registry.default_registry_path)
36
- @registry_path = registry_path
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 # Registry state for change detection
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 registry changes
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 registry path
102
+ # Update the register path
103
103
  #
104
- # @param new_path [String] the new registry path
105
- def registry_path=(new_path)
106
- return if @registry_path == new_path
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
- @registry_path = new_path
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 registry change detection
123
- # Uses mtime of registry directory + file count for fast comparison
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 = registry_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 registry path
140
+ # Get the current register path
141
141
  #
142
- # @return [String, nil] the registry path
143
- def registry_path
144
- @registry_path ||= Ukiryu::Registry.default_registry_path
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 = registry_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 && aliases.respond_to?(:each)
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, registry_path: registry_path)
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 = registry_path
211
+ current_path = register_path
212
212
  return nil unless current_path
213
213
 
214
214
  # Search for version files
@@ -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 execution mode env vars)
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 including execution mode
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
- env = build_env_vars(command_def, params)
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
- execution_mode = command_def.execution_mode
334
- ['headless', :headless].include?(execution_mode)
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
- cmd = vd.command || '--version'
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 registry
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 '../registry'
53
+ require_relative '../register'
54
54
 
55
55
  # Load the YAML file content
56
- yaml_content = Registry.load_tool_yaml(tool_name)
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 '../registry'
139
+ require_relative '../register'
140
140
 
141
- registry_path = Registry.default_registry_path
142
- return [] unless registry_path
141
+ register_path = Register.default_register_path
142
+ return [] unless register_path
143
143
 
144
- tools_dir = File.join(registry_path, 'tools')
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
- registry_path = Registry.default_registry_path
69
- return nil unless registry_path && Dir.exist?(registry_path)
68
+ register_path = Register.default_register_path
69
+ return nil unless register_path && Dir.exist?(register_path)
70
70
 
71
- tools_dir = File.join(registry_path, 'tools')
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
- raise ValidationError, "String does not match required pattern: #{options[:pattern]}" if options[:pattern] && value !~ options[:pattern]
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
- raise ValidationError, "Invalid symbol: #{value.inspect}. Valid values: #{options[:values].inspect}" unless valid_values.include?(value)
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
- raise ValidationError, "Array has #{array.size} elements, minimum is #{options[:min]}" if options[:min] && array.size < options[:min]
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
- raise ValidationError, "Array has #{array.size} elements, maximum is #{options[:max]}" if options[:max] && array.size > options[:max]
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
- raise ValidationError, "Array has #{array.size} elements, expected one of: #{options[:size].inspect}" unless options[:size].include?(array.size)
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ukiryu
4
- VERSION = '0.1.1'
4
+ VERSION = '0.1.3'
5
5
  end
@@ -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