ukiryu 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/docs.yml +63 -0
  3. data/.github/workflows/links.yml +99 -0
  4. data/.github/workflows/rake.yml +19 -0
  5. data/.github/workflows/release.yml +27 -0
  6. data/.gitignore +18 -4
  7. data/.rubocop.yml +1 -0
  8. data/.rubocop_todo.yml +213 -0
  9. data/Gemfile +12 -8
  10. data/README.adoc +613 -0
  11. data/Rakefile +2 -2
  12. data/docs/assets/logo.svg +1 -0
  13. data/exe/ukiryu +11 -0
  14. data/lib/ukiryu/action/base.rb +77 -0
  15. data/lib/ukiryu/cache.rb +199 -0
  16. data/lib/ukiryu/cli.rb +133 -307
  17. data/lib/ukiryu/cli_commands/base_command.rb +155 -0
  18. data/lib/ukiryu/cli_commands/commands_command.rb +120 -0
  19. data/lib/ukiryu/cli_commands/commands_command.rb.fixed +40 -0
  20. data/lib/ukiryu/cli_commands/config_command.rb +249 -0
  21. data/lib/ukiryu/cli_commands/describe_command.rb +326 -0
  22. data/lib/ukiryu/cli_commands/describe_command.rb.fixed +254 -0
  23. data/lib/ukiryu/cli_commands/exec_inline_command.rb.fixed +180 -0
  24. data/lib/ukiryu/cli_commands/extract_command.rb +84 -0
  25. data/lib/ukiryu/cli_commands/info_command.rb +156 -0
  26. data/lib/ukiryu/cli_commands/list_command.rb +70 -0
  27. data/lib/ukiryu/cli_commands/opts_command.rb +106 -0
  28. data/lib/ukiryu/cli_commands/opts_command.rb.fixed +105 -0
  29. data/lib/ukiryu/cli_commands/response_formatter.rb +240 -0
  30. data/lib/ukiryu/cli_commands/run_command.rb +375 -0
  31. data/lib/ukiryu/cli_commands/run_file_command.rb +215 -0
  32. data/lib/ukiryu/cli_commands/system_command.rb +90 -0
  33. data/lib/ukiryu/cli_commands/validate_command.rb +87 -0
  34. data/lib/ukiryu/cli_commands/version_command.rb +16 -0
  35. data/lib/ukiryu/cli_commands/which_command.rb +166 -0
  36. data/lib/ukiryu/command_builder.rb +205 -0
  37. data/lib/ukiryu/config/env_provider.rb +64 -0
  38. data/lib/ukiryu/config/env_schema.rb +63 -0
  39. data/lib/ukiryu/config/override_resolver.rb +68 -0
  40. data/lib/ukiryu/config/type_converter.rb +59 -0
  41. data/lib/ukiryu/config.rb +249 -0
  42. data/lib/ukiryu/errors.rb +3 -0
  43. data/lib/ukiryu/executable_locator.rb +114 -0
  44. data/lib/ukiryu/execution/command_info.rb +64 -0
  45. data/lib/ukiryu/execution/metadata.rb +97 -0
  46. data/lib/ukiryu/execution/output.rb +144 -0
  47. data/lib/ukiryu/execution/result.rb +194 -0
  48. data/lib/ukiryu/execution.rb +15 -0
  49. data/lib/ukiryu/execution_context.rb +251 -0
  50. data/lib/ukiryu/executor.rb +76 -493
  51. data/lib/ukiryu/extractors/base_extractor.rb +63 -0
  52. data/lib/ukiryu/extractors/extractor.rb +150 -0
  53. data/lib/ukiryu/extractors/help_parser.rb +188 -0
  54. data/lib/ukiryu/extractors/native_extractor.rb +47 -0
  55. data/lib/ukiryu/io.rb +196 -0
  56. data/lib/ukiryu/logger.rb +544 -0
  57. data/lib/ukiryu/models/argument.rb +28 -0
  58. data/lib/ukiryu/models/argument_definition.rb +119 -0
  59. data/lib/ukiryu/models/arguments.rb +113 -0
  60. data/lib/ukiryu/models/command_definition.rb +176 -0
  61. data/lib/ukiryu/models/command_info.rb +37 -0
  62. data/lib/ukiryu/models/components.rb +107 -0
  63. data/lib/ukiryu/models/env_var_definition.rb +30 -0
  64. data/lib/ukiryu/models/error_response.rb +41 -0
  65. data/lib/ukiryu/models/execution_metadata.rb +31 -0
  66. data/lib/ukiryu/models/execution_report.rb +236 -0
  67. data/lib/ukiryu/models/exit_codes.rb +74 -0
  68. data/lib/ukiryu/models/flag_definition.rb +67 -0
  69. data/lib/ukiryu/models/option_definition.rb +102 -0
  70. data/lib/ukiryu/models/output_info.rb +25 -0
  71. data/lib/ukiryu/models/platform_profile.rb +153 -0
  72. data/lib/ukiryu/models/routing.rb +211 -0
  73. data/lib/ukiryu/models/search_paths.rb +39 -0
  74. data/lib/ukiryu/models/success_response.rb +85 -0
  75. data/lib/ukiryu/models/tool_definition.rb +145 -0
  76. data/lib/ukiryu/models/tool_metadata.rb +82 -0
  77. data/lib/ukiryu/models/validation_result.rb +80 -0
  78. data/lib/ukiryu/models/version_compatibility.rb +152 -0
  79. data/lib/ukiryu/models/version_detection.rb +39 -0
  80. data/lib/ukiryu/models.rb +23 -0
  81. data/lib/ukiryu/options/base.rb +95 -0
  82. data/lib/ukiryu/options_builder/formatter.rb +87 -0
  83. data/lib/ukiryu/options_builder/validator.rb +43 -0
  84. data/lib/ukiryu/options_builder.rb +311 -0
  85. data/lib/ukiryu/platform.rb +6 -6
  86. data/lib/ukiryu/registry.rb +143 -183
  87. data/lib/ukiryu/response/base.rb +217 -0
  88. data/lib/ukiryu/runtime.rb +179 -0
  89. data/lib/ukiryu/schema_validator.rb +8 -10
  90. data/lib/ukiryu/shell/bash.rb +3 -3
  91. data/lib/ukiryu/shell/cmd.rb +4 -4
  92. data/lib/ukiryu/shell/fish.rb +1 -1
  93. data/lib/ukiryu/shell/powershell.rb +3 -3
  94. data/lib/ukiryu/shell/sh.rb +1 -1
  95. data/lib/ukiryu/shell/zsh.rb +1 -1
  96. data/lib/ukiryu/shell.rb +146 -39
  97. data/lib/ukiryu/thor_ext.rb +208 -0
  98. data/lib/ukiryu/tool.rb +649 -258
  99. data/lib/ukiryu/tool_index.rb +224 -0
  100. data/lib/ukiryu/tools/base.rb +381 -0
  101. data/lib/ukiryu/tools/class_generator.rb +132 -0
  102. data/lib/ukiryu/tools/executable_finder.rb +29 -0
  103. data/lib/ukiryu/tools/generator.rb +154 -0
  104. data/lib/ukiryu/tools.rb +109 -0
  105. data/lib/ukiryu/type.rb +28 -43
  106. data/lib/ukiryu/validation/constraints.rb +281 -0
  107. data/lib/ukiryu/validation/validator.rb +188 -0
  108. data/lib/ukiryu/validation.rb +21 -0
  109. data/lib/ukiryu/version.rb +1 -1
  110. data/lib/ukiryu/version_detector.rb +51 -0
  111. data/lib/ukiryu.rb +31 -15
  112. data/ukiryu-proposal.md +2952 -0
  113. data/ukiryu.gemspec +18 -14
  114. metadata +137 -5
  115. data/.github/workflows/test.yml +0 -143
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ require_relative 'platform'
6
+ require_relative 'shell'
7
+ require_relative 'config'
8
+
9
+ module Ukiryu
10
+ # Runtime singleton for centralized platform and shell detection.
11
+ #
12
+ # This class provides a single source of truth for platform and shell
13
+ # detection across the entire application, eliminating redundant detection
14
+ # calls and ensuring consistency.
15
+ #
16
+ # @example
17
+ # platform = Ukiryu::Runtime.instance.platform
18
+ # shell = Ukiryu::Runtime.instance.shell
19
+ class Runtime
20
+ include Singleton
21
+
22
+ # Initialize the runtime with cached values
23
+ def initialize
24
+ @platform = nil
25
+ @shell = nil
26
+ @platform_cached = false
27
+ @shell_cached = false
28
+ @locked = false
29
+ end
30
+
31
+ # Get the current platform (cached)
32
+ #
33
+ # @return [Symbol] the detected platform (:macos, :linux, :windows)
34
+ def platform
35
+ return @platform if @platform_cached
36
+
37
+ @platform = Platform.detect
38
+ @platform_cached = true
39
+ @platform
40
+ end
41
+
42
+ # Get the current shell (cached)
43
+ #
44
+ # Priority:
45
+ # 1. Explicitly set shell (via shell=)
46
+ # 2. Config.shell (from --shell CLI option, UKIRYU_SHELL env, or programmatic config)
47
+ # 3. Auto-detected shell
48
+ #
49
+ # @return [Symbol] the detected shell
50
+ def shell
51
+ return @shell if @shell_cached
52
+
53
+ # Check for explicit override
54
+ override = shell_override
55
+ if override
56
+ @shell = override
57
+ @shell_cached = true
58
+ return @shell
59
+ end
60
+
61
+ # Auto-detect
62
+ @shell = Shell.detect
63
+ @shell_cached = true
64
+ @shell
65
+ end
66
+
67
+ # Manually set the platform (for testing)
68
+ #
69
+ # @param value [Symbol] the platform to set
70
+ def platform=(value)
71
+ raise 'Runtime is locked' if @locked
72
+
73
+ @platform = value&.to_sym
74
+ @platform_cached = true
75
+ end
76
+
77
+ # Manually set the shell (for testing)
78
+ #
79
+ # @param value [Symbol] the shell to set
80
+ def shell=(value)
81
+ raise 'Runtime is locked' if @locked
82
+
83
+ @shell = value&.to_sym
84
+ @shell_cached = true
85
+ end
86
+
87
+ # Lock the runtime to prevent further changes
88
+ #
89
+ # This should be called after initial configuration is complete.
90
+ def lock!
91
+ @locked = true
92
+ end
93
+
94
+ # Reset the runtime cache (for testing)
95
+ #
96
+ # @api private
97
+ def reset!
98
+ @platform = nil
99
+ @shell = nil
100
+ @platform_cached = false
101
+ @shell_cached = false
102
+ @locked = false
103
+ end
104
+
105
+ # Get the platform class for the current platform
106
+ #
107
+ # @return [Class] the platform class
108
+ def platform_class
109
+ Platform.class_for(platform)
110
+ end
111
+
112
+ # Get the shell class for the current shell
113
+ #
114
+ # @return [Class] the shell class
115
+ def shell_class
116
+ Shell.class_for(shell)
117
+ end
118
+
119
+ # Check if running on a specific platform
120
+ #
121
+ # @param plat [Symbol] the platform to check
122
+ # @return [Boolean] true if running on the specified platform
123
+ def on_platform?(plat)
124
+ platform == plat.to_sym
125
+ end
126
+
127
+ # Check if using a specific shell
128
+ #
129
+ # @param sh [Symbol] the shell to check
130
+ # @return [Boolean] true if using the specified shell
131
+ def using_shell?(sh)
132
+ shell == sh.to_sym
133
+ end
134
+
135
+ # Check if running on Windows
136
+ #
137
+ # @return [Boolean] true if on Windows
138
+ def windows?
139
+ on_platform?(:windows)
140
+ end
141
+
142
+ # Check if running on macOS
143
+ #
144
+ # @return [Boolean] true if on macOS
145
+ def macos?
146
+ on_platform?(:macos)
147
+ end
148
+
149
+ # Check if running on Linux
150
+ #
151
+ # @return [Boolean] true if on Linux
152
+ def linux?
153
+ on_platform?(:linux)
154
+ end
155
+
156
+ # Check if using a Unix-like shell
157
+ #
158
+ # @return [Boolean] true if using bash, zsh, fish, or sh
159
+ def unix_shell?
160
+ %i[bash zsh fish sh].include?(shell)
161
+ end
162
+
163
+ # Check if using a Windows shell
164
+ #
165
+ # @return [Boolean] true if using powershell or cmd
166
+ def windows_shell?
167
+ %i[powershell cmd].include?(shell)
168
+ end
169
+
170
+ private
171
+
172
+ # Get shell override from Config
173
+ #
174
+ # @return [Symbol, nil] the shell override or nil
175
+ def shell_override
176
+ Config.shell
177
+ end
178
+ end
179
+ end
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
3
+ require 'json'
4
4
  begin
5
- require "json-schema"
5
+ require 'json-schema'
6
6
  rescue LoadError
7
7
  # json-schema is optional - only needed for schema validation
8
8
  end
9
- require "yaml"
9
+ require 'yaml'
10
10
 
11
11
  module Ukiryu
12
12
  # Schema validator for YAML tool profiles
@@ -23,15 +23,13 @@ module Ukiryu
23
23
  # @return [Array<String>] list of validation errors (empty if valid)
24
24
  def validate_profile(profile, options = {})
25
25
  # Check if json-schema gem is available
26
- unless defined?(JSON::Validator)
27
- return ["json-schema gem not installed. Add 'json-schema' to Gemfile for schema validation."]
28
- end
26
+ return ["json-schema gem not installed. Add 'json-schema' to Gemfile for schema validation."] unless defined?(JSON::Validator)
29
27
 
30
28
  errors = []
31
29
 
32
30
  # Load the schema
33
31
  schema = load_schema(options[:schema_path])
34
- return ["Failed to load schema"] unless schema
32
+ return ['Failed to load schema'] unless schema
35
33
 
36
34
  # Validate against JSON schema
37
35
  begin
@@ -71,9 +69,9 @@ module Ukiryu
71
69
  def default_schema_path
72
70
  # Schema is in the sibling 'schema' directory at the same level as the gem
73
71
  # From lib/ukiryu/, we go up to gem root, then to sibling schema/
74
- gem_root = File.expand_path("../..", __dir__) # ukiryu gem root
75
- schema_dir = File.expand_path("../schema", gem_root) # src/ukiryu/schema/
76
- schema_file = File.join(schema_dir, "tool-profile.schema.yaml")
72
+ gem_root = File.expand_path('../..', __dir__) # ukiryu gem root
73
+ schema_dir = File.expand_path('../schema', gem_root) # src/ukiryu/schema/
74
+ schema_file = File.join(schema_dir, 'tool-profile.schema.yaml')
77
75
  return schema_file if File.exist?(schema_file)
78
76
 
79
77
  nil
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "base"
3
+ require_relative 'base'
4
4
 
5
5
  module Ukiryu
6
6
  module Shell
@@ -46,14 +46,14 @@ module Ukiryu
46
46
  # @param args [Array<String>] the arguments
47
47
  # @return [String] the complete command line
48
48
  def join(executable, *args)
49
- [executable, *args.map { |a| quote(a) }].join(" ")
49
+ [executable, *args.map { |a| quote(a) }].join(' ')
50
50
  end
51
51
 
52
52
  # Get headless environment (disable DISPLAY on Unix)
53
53
  #
54
54
  # @return [Hash] environment variables for headless operation
55
55
  def headless_environment
56
- { "DISPLAY" => "" }
56
+ { 'DISPLAY' => '' }
57
57
  end
58
58
  end
59
59
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "base"
3
+ require_relative 'base'
4
4
 
5
5
  module Ukiryu
6
6
  module Shell
@@ -19,7 +19,7 @@ module Ukiryu
19
19
  # @param string [String] the string to escape
20
20
  # @return [String] the escaped string
21
21
  def escape(string)
22
- string.to_s.gsub(/[%^<>&|]/) { "^$&" }
22
+ string.to_s.gsub(/[%^<>&|]/) { '^$&' }
23
23
  end
24
24
 
25
25
  # Quote an argument for cmd.exe
@@ -44,7 +44,7 @@ module Ukiryu
44
44
  # @param path [String] the file path
45
45
  # @return [String] the formatted path
46
46
  def format_path(path)
47
- path.to_s.gsub("/", "\\")
47
+ path.to_s.gsub('/', '\\')
48
48
  end
49
49
 
50
50
  # Format an environment variable reference
@@ -61,7 +61,7 @@ module Ukiryu
61
61
  # @param args [Array<String>] the arguments
62
62
  # @return [String] the complete command line
63
63
  def join(executable, *args)
64
- [executable, *args.map { |a| quote(a) }].join(" ")
64
+ [executable, *args.map { |a| quote(a) }].join(' ')
65
65
  end
66
66
 
67
67
  # cmd.exe doesn't need DISPLAY variable
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "bash"
3
+ require_relative 'bash'
4
4
 
5
5
  module Ukiryu
6
6
  module Shell
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "base"
3
+ require_relative 'base'
4
4
 
5
5
  module Ukiryu
6
6
  module Shell
@@ -20,7 +20,7 @@ module Ukiryu
20
20
  # @param string [String] the string to escape
21
21
  # @return [String] the escaped string
22
22
  def escape(string)
23
- string.to_s.gsub(/[`"$]/) { "`$&" }
23
+ string.to_s.gsub(/[`"$]/) { '`$&' }
24
24
  end
25
25
 
26
26
  # Quote an argument for PowerShell
@@ -46,7 +46,7 @@ module Ukiryu
46
46
  # @param args [Array<String>] the arguments
47
47
  # @return [String] the complete command line
48
48
  def join(executable, *args)
49
- [executable, *args.map { |a| quote(a) }].join(" ")
49
+ [executable, *args.map { |a| quote(a) }].join(' ')
50
50
  end
51
51
 
52
52
  # PowerShell doesn't need DISPLAY variable
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "bash"
3
+ require_relative 'bash'
4
4
 
5
5
  module Ukiryu
6
6
  module Shell
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "bash"
3
+ require_relative 'bash'
4
4
 
5
5
  module Ukiryu
6
6
  module Shell
data/lib/ukiryu/shell.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "shell/base"
3
+ require_relative 'shell/base'
4
4
 
5
5
  module Ukiryu
6
6
  # Shell detection and management
@@ -8,10 +8,84 @@ module Ukiryu
8
8
  # Provides EXPLICIT shell detection with no fallbacks.
9
9
  # If shell cannot be determined, raises a clear error.
10
10
  module Shell
11
+ # All supported shell types
12
+ VALID_SHELLS = %i[bash zsh fish sh powershell cmd].freeze
13
+
14
+ # Platform-specific shell mappings
15
+ UNIX_SHELLS = %i[bash zsh fish sh].freeze
16
+ WINDOWS_SHELLS = %i[powershell cmd bash].freeze
17
+
11
18
  class << self
12
19
  # Get or set the current shell (for explicit configuration)
13
20
  attr_writer :current_shell
14
21
 
22
+ # Check if a shell symbol is valid
23
+ #
24
+ # @param shell_sym [Symbol] the shell symbol to check
25
+ # @return [Boolean] true if shell is valid
26
+ def valid?(shell_sym)
27
+ VALID_SHELLS.include?(shell_sym&.to_sym)
28
+ end
29
+
30
+ # Get list of all valid shells
31
+ #
32
+ # @return [Array<Symbol>] list of valid shell symbols
33
+ def all_valid
34
+ VALID_SHELLS.dup
35
+ end
36
+
37
+ # Get shells valid for current platform
38
+ #
39
+ # @return [Array<Symbol>] list of valid shells for current platform
40
+ def valid_for_platform
41
+ Platform.windows? ? WINDOWS_SHELLS.dup : UNIX_SHELLS.dup
42
+ end
43
+
44
+ # Convert string to shell symbol
45
+ #
46
+ # @param str [String] the shell name string
47
+ # @return [Symbol] the shell symbol
48
+ # @raise [ArgumentError] if shell name is invalid
49
+ def from_string(str)
50
+ shell_sym = str.to_s.downcase.to_sym
51
+ return shell_sym if valid?(shell_sym)
52
+
53
+ raise ArgumentError,
54
+ "Invalid shell: #{str}. Valid shells: #{VALID_SHELLS.join(', ')}"
55
+ end
56
+
57
+ # Check if a shell is available on the system
58
+ #
59
+ # @param shell_sym [Symbol] the shell to check
60
+ # @return [Boolean] true if shell is available
61
+ def available?(shell_sym)
62
+ return false unless valid?(shell_sym)
63
+
64
+ case shell_sym
65
+ when :bash
66
+ shell_available_on_unix?('bash') || bash_available_on_windows?
67
+ when :zsh
68
+ shell_available_on_unix?('zsh')
69
+ when :fish
70
+ shell_available_on_unix?('fish')
71
+ when :sh
72
+ shell_available_on_unix?('sh')
73
+ when :powershell
74
+ powershell_available?
75
+ when :cmd
76
+ true # cmd is always available on Windows
77
+ else
78
+ false
79
+ end
80
+ end
81
+
82
+ # Get all shells available on the current system
83
+ #
84
+ # @return [Array<Symbol>] list of available shells
85
+ def available_shells
86
+ VALID_SHELLS.select { |shell| available?(shell) }
87
+ end
88
+
15
89
  # Detect the current shell
16
90
  #
17
91
  # @return [Symbol] :bash, :zsh, :fish, :sh, :powershell, or :cmd
@@ -54,22 +128,22 @@ module Ukiryu
54
128
  def class_for(name)
55
129
  case name
56
130
  when :bash
57
- require_relative "shell/bash"
131
+ require_relative 'shell/bash'
58
132
  Bash
59
133
  when :zsh
60
- require_relative "shell/zsh"
134
+ require_relative 'shell/zsh'
61
135
  Zsh
62
136
  when :fish
63
- require_relative "shell/fish"
137
+ require_relative 'shell/fish'
64
138
  Fish
65
139
  when :sh
66
- require_relative "shell/sh"
140
+ require_relative 'shell/sh'
67
141
  Sh
68
142
  when :powershell
69
- require_relative "shell/powershell"
143
+ require_relative 'shell/powershell'
70
144
  PowerShell
71
145
  when :cmd
72
- require_relative "shell/cmd"
146
+ require_relative 'shell/cmd'
73
147
  Cmd
74
148
  else
75
149
  raise UnknownShellError, "Unknown shell: #{name}"
@@ -83,13 +157,13 @@ module Ukiryu
83
157
  # @return [Symbol] detected shell
84
158
  def detect_windows_shell
85
159
  # PowerShell check
86
- return :powershell if ENV["PSModulePath"]
160
+ return :powershell if ENV['PSModulePath']
87
161
 
88
162
  # Git Bash / MSYS check
89
- return :bash if ENV["MSYSTEM"] || ENV["MINGW_PREFIX"]
163
+ return :bash if ENV['MSYSTEM'] || ENV['MINGW_PREFIX']
90
164
 
91
165
  # WSL check
92
- return :bash if ENV["WSL_DISTRO"]
166
+ return :bash if ENV['WSL_DISTRO']
93
167
 
94
168
  # Default to cmd on Windows
95
169
  :cmd
@@ -99,37 +173,36 @@ module Ukiryu
99
173
  #
100
174
  # @return [Symbol] detected shell
101
175
  def detect_unix_shell
102
- shell_env = ENV["SHELL"]
176
+ shell_env = ENV['SHELL']
103
177
 
104
178
  # Try to determine from SHELL environment variable
105
- if shell_env
106
- return :bash if shell_env.end_with?("bash")
107
- return :zsh if shell_env.end_with?("zsh")
108
- return :fish if shell_env.end_with?("fish")
109
- return :sh if shell_env.end_with?("sh")
110
-
111
- # Try to determine from executable name
112
- shell_name = File.basename(shell_env)
113
- case shell_name
114
- when "bash"
115
- :bash
116
- when "zsh"
117
- :zsh
118
- when "fish"
119
- :fish
120
- when "sh"
121
- :sh
122
- else
123
- # Unknown shell in ENV - check if executable
124
- if File.executable?(shell_env)
125
- # Return as symbol for custom shell
126
- shell_name.to_sym
127
- else
128
- raise UnknownShellError, unknown_shell_error_msg("Unknown shell in SHELL: #{shell_env}")
129
- end
130
- end
179
+ raise UnknownShellError, unknown_shell_error_msg('SHELL environment variable not set') unless shell_env
180
+ return :bash if shell_env.end_with?('bash')
181
+ return :zsh if shell_env.end_with?('zsh')
182
+ return :fish if shell_env.end_with?('fish')
183
+ return :sh if shell_env.end_with?('sh')
184
+
185
+ # Try to determine from executable name
186
+ shell_name = File.basename(shell_env)
187
+ case shell_name
188
+ when 'bash'
189
+ :bash
190
+ when 'zsh'
191
+ :zsh
192
+ when 'fish'
193
+ :fish
194
+ when 'sh'
195
+ :sh
131
196
  else
132
- raise UnknownShellError, unknown_shell_error_msg("SHELL environment variable not set")
197
+ # Unknown shell in ENV - check if executable
198
+ unless File.executable?(shell_env)
199
+ raise UnknownShellError,
200
+ unknown_shell_error_msg("Unknown shell in SHELL: #{shell_env}")
201
+ end
202
+
203
+ # Return as symbol for custom shell
204
+ shell_name.to_sym
205
+
133
206
  end
134
207
  end
135
208
 
@@ -154,11 +227,45 @@ module Ukiryu
154
227
  end
155
228
 
156
229
  Current environment:
157
- Platform: #{RbConfig::CONFIG['host_os']}
230
+ Platform: #{RUBY_PLATFORM}
158
231
  SHELL: #{ENV['SHELL']}
159
232
  PSModulePath: #{ENV['PSModulePath']}
160
233
  ERROR
161
234
  end
235
+
236
+ # Check if a Unix shell is available on the system
237
+ #
238
+ # @param shell_name [String] the shell executable name
239
+ # @return [Boolean] true if shell is available
240
+ def shell_available_on_unix?(shell_name)
241
+ return false if Platform.windows?
242
+
243
+ # Check if shell is in PATH
244
+ system("which #{shell_name} > /dev/null 2>&1")
245
+ end
246
+
247
+ # Check if bash is available on Windows (Git Bash/MSYS)
248
+ #
249
+ # @return [Boolean] true if bash is available
250
+ def bash_available_on_windows?
251
+ return false unless Platform.windows?
252
+
253
+ # Check for Git Bash / MSYS
254
+ !!(ENV['MSYSTEM'] || ENV['MINGW_PREFIX'] || ENV['WSL_DISTRO'] ||
255
+ system('where bash >nul 2>&1'))
256
+ end
257
+
258
+ # Check if PowerShell is available
259
+ #
260
+ # @return [Boolean] true if PowerShell is available
261
+ def powershell_available?
262
+ return true if Platform.windows? && ENV['PSModulePath']
263
+
264
+ # On Unix, check for PowerShell Core (pwsh)
265
+ return true if !Platform.windows? && system('which pwsh > /dev/null 2>&1')
266
+
267
+ false
268
+ end
162
269
  end
163
270
  end
164
271
  end