rufio 0.41.0 → 0.50.0

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.
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rufio
4
+ # DSLで定義されたコマンドを表すクラス
5
+ class DslCommand
6
+ attr_reader :name, :script, :description, :interpreter, :errors
7
+ attr_reader :ruby_block, :shell_command
8
+
9
+ # コマンドを初期化する
10
+ # @param name [String] コマンド名
11
+ # @param script [String, nil] スクリプトパス
12
+ # @param description [String] コマンドの説明
13
+ # @param interpreter [String, nil] インタープリタ(nilの場合は自動検出)
14
+ # @param ruby_block [Proc, nil] inline Rubyブロック
15
+ # @param shell_command [String, nil] inline シェルコマンド
16
+ def initialize(name:, script: nil, description: "", interpreter: nil,
17
+ ruby_block: nil, shell_command: nil)
18
+ @name = name.to_s
19
+ @script = script ? normalize_path(script.to_s) : nil
20
+ @description = description.to_s
21
+ @interpreter = interpreter || auto_resolve_interpreter
22
+ @ruby_block = ruby_block
23
+ @shell_command = shell_command
24
+ @errors = []
25
+ end
26
+
27
+ # コマンドタイプを返す
28
+ # @return [Symbol] :ruby, :shell, :script のいずれか
29
+ def command_type
30
+ if @ruby_block
31
+ :ruby
32
+ elsif @shell_command
33
+ :shell
34
+ else
35
+ :script
36
+ end
37
+ end
38
+
39
+ # コマンドが有効かどうかを検証する
40
+ # @return [Boolean]
41
+ def valid?
42
+ @errors = []
43
+ validate_name
44
+ validate_execution_source
45
+ @errors.empty?
46
+ end
47
+
48
+ # 実行用の引数配列を返す
49
+ # @return [Array<String>] [インタープリタ, スクリプトパス]
50
+ def to_execution_args
51
+ [@interpreter, @script]
52
+ end
53
+
54
+ # ハッシュ表現を返す
55
+ # @return [Hash]
56
+ def to_h
57
+ hash = {
58
+ name: @name,
59
+ script: @script,
60
+ description: @description,
61
+ interpreter: @interpreter
62
+ }
63
+ hash[:has_ruby_block] = true if @ruby_block
64
+ hash[:shell_command] = @shell_command if @shell_command
65
+ hash
66
+ end
67
+
68
+ private
69
+
70
+ # パスを正規化する(チルダ展開、パストラバーサル解決)
71
+ # @param path [String] 入力パス
72
+ # @return [String] 正規化されたパス
73
+ def normalize_path(path)
74
+ return "" if path.empty?
75
+
76
+ # チルダを展開
77
+ expanded = File.expand_path(path)
78
+
79
+ # ファイルが存在する場合は実際のパスを取得(パストラバーサル解決)
80
+ if File.exist?(expanded)
81
+ File.realpath(expanded)
82
+ else
83
+ # ファイルが存在しない場合は展開されたパスをそのまま返す
84
+ expanded
85
+ end
86
+ end
87
+
88
+ # 拡張子からインタープリタを自動検出する
89
+ # @return [String, nil]
90
+ def auto_resolve_interpreter
91
+ return nil if @script.nil? || @script.empty?
92
+
93
+ InterpreterResolver.resolve_from_path(@script)
94
+ end
95
+
96
+ # コマンド名のバリデーション
97
+ def validate_name
98
+ if @name.empty?
99
+ @errors << "Command name is required"
100
+ end
101
+ end
102
+
103
+ # 実行ソースのバリデーション
104
+ # ruby_block, shell_command, script のいずれかが必要
105
+ def validate_execution_source
106
+ # ruby_block または shell_command がある場合はスクリプト不要
107
+ return if @ruby_block || @shell_command
108
+
109
+ # スクリプトパスのバリデーション
110
+ if @script.nil? || @script.empty?
111
+ @errors << "Script path is required"
112
+ return
113
+ end
114
+
115
+ unless File.exist?(@script)
116
+ @errors << "Script not found: #{@script}"
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rufio
4
+ # DSL設定ファイルを読み込んでDslCommandを生成するクラス
5
+ class DslCommandLoader
6
+ attr_reader :errors, :warnings
7
+
8
+ def initialize
9
+ @errors = []
10
+ @warnings = []
11
+ end
12
+
13
+ # 文字列からDSLを解析してコマンドをロードする
14
+ # @param dsl_string [String] DSL文字列
15
+ # @return [Array<DslCommand>] 有効なコマンドの配列
16
+ def load_from_string(dsl_string)
17
+ @errors = []
18
+ @warnings = []
19
+
20
+ context = DslContext.new
21
+ begin
22
+ context.instance_eval(dsl_string)
23
+ rescue SyntaxError, StandardError => e
24
+ @errors << "DSL parse error: #{e.message}"
25
+ return []
26
+ end
27
+
28
+ validate_commands(context.commands)
29
+ end
30
+
31
+ # ファイルからDSLをロードする
32
+ # @param file_path [String] 設定ファイルのパス
33
+ # @return [Array<DslCommand>] 有効なコマンドの配列
34
+ def load_from_file(file_path)
35
+ @errors = []
36
+ @warnings = []
37
+
38
+ expanded_path = File.expand_path(file_path)
39
+ unless File.exist?(expanded_path)
40
+ return []
41
+ end
42
+
43
+ content = File.read(expanded_path)
44
+ load_from_string(content)
45
+ end
46
+
47
+ # 複数のパスからDSLをロードする
48
+ # @param paths [Array<String>] 設定ファイルのパス配列
49
+ # @return [Array<DslCommand>] 有効なコマンドの配列
50
+ def load_from_paths(paths)
51
+ commands = []
52
+
53
+ paths.each do |path|
54
+ commands.concat(load_from_file(path))
55
+ end
56
+
57
+ commands
58
+ end
59
+
60
+ # デフォルトのパスからDSLをロードする
61
+ # @return [Array<DslCommand>] 有効なコマンドの配列
62
+ def load
63
+ load_from_paths(default_config_paths)
64
+ end
65
+
66
+ # デフォルトの設定ファイルパスを返す
67
+ # @return [Array<String>] 設定ファイルパスの配列
68
+ def default_config_paths
69
+ home = Dir.home
70
+ [
71
+ File.join(home, ".rufio", "commands.rb"),
72
+ File.join(home, ".config", "rufio", "commands.rb")
73
+ ]
74
+ end
75
+
76
+ private
77
+
78
+ # コマンドをバリデーションし、有効なもののみを返す
79
+ # @param commands [Array<DslCommand>] コマンドの配列
80
+ # @return [Array<DslCommand>] 有効なコマンドの配列
81
+ def validate_commands(commands)
82
+ valid_commands = []
83
+
84
+ commands.each do |cmd|
85
+ if cmd.valid?
86
+ valid_commands << cmd
87
+ else
88
+ @warnings << "Command '#{cmd.name}' is invalid: #{cmd.errors.join(', ')}"
89
+ end
90
+ end
91
+
92
+ valid_commands
93
+ end
94
+
95
+ # DSL評価用の独立したコンテキスト
96
+ class DslContext < BasicObject
97
+ attr_reader :commands
98
+
99
+ def initialize
100
+ @commands = []
101
+ end
102
+
103
+ # コマンドを定義する
104
+ # @param name [String] コマンド名
105
+ # @yield コマンドの設定ブロック
106
+ def command(name, &block)
107
+ builder = CommandBuilder.new(name)
108
+ builder.instance_eval(&block) if block
109
+ @commands << builder.build
110
+ end
111
+
112
+ # 安全でないメソッドをブロック
113
+ def method_missing(method_name, *_args)
114
+ ::Kernel.raise ::NoMethodError, "Method '#{method_name}' is not allowed in DSL"
115
+ end
116
+
117
+ def respond_to_missing?(_method_name, _include_private = false)
118
+ false
119
+ end
120
+ end
121
+
122
+ # コマンドビルダー
123
+ class CommandBuilder
124
+ def initialize(name)
125
+ @name = name
126
+ @script = nil
127
+ @description = ""
128
+ @interpreter = nil
129
+ @ruby_block = nil
130
+ @shell_command = nil
131
+ end
132
+
133
+ def script(path)
134
+ @script = path
135
+ end
136
+
137
+ def description(desc)
138
+ @description = desc
139
+ end
140
+
141
+ def interpreter(interp)
142
+ @interpreter = interp
143
+ end
144
+
145
+ # inline Rubyブロックを定義
146
+ def ruby(&block)
147
+ @ruby_block = block
148
+ end
149
+
150
+ # inline シェルコマンドを定義
151
+ def shell(command)
152
+ @shell_command = command
153
+ end
154
+
155
+ def build
156
+ DslCommand.new(
157
+ name: @name,
158
+ script: @script,
159
+ description: @description,
160
+ interpreter: @interpreter,
161
+ ruby_block: @ruby_block,
162
+ shell_command: @shell_command
163
+ )
164
+ end
165
+
166
+ # 未知のメソッドは無視
167
+ def method_missing(_method_name, *_args)
168
+ # DSL内で未知のメソッドが呼ばれた場合は無視
169
+ nil
170
+ end
171
+
172
+ def respond_to_missing?(_method_name, _include_private = false)
173
+ true
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rufio
4
+ # 拡張子からインタープリタを解決するクラス
5
+ class InterpreterResolver
6
+ # デフォルトの拡張子とインタープリタのマッピング
7
+ DEFAULT_EXTENSIONS = {
8
+ ".rb" => "ruby",
9
+ ".py" => "python3",
10
+ ".sh" => "bash",
11
+ ".js" => "node",
12
+ ".pl" => "perl",
13
+ ".lua" => "lua",
14
+ ".ts" => "ts-node",
15
+ ".php" => "php",
16
+ ".ps1" => nil # プラットフォーム依存
17
+ }.freeze
18
+
19
+ class << self
20
+ # 拡張子からインタープリタを解決する
21
+ # @param extension [String] 拡張子(ドット付きまたはなし)
22
+ # @return [String, nil] インタープリタ名、解決できない場合はnil
23
+ def resolve(extension)
24
+ # ドットで始まらない場合は追加
25
+ ext = extension.start_with?(".") ? extension : ".#{extension}"
26
+ ext = ext.downcase
27
+
28
+ # PowerShellはプラットフォーム依存
29
+ return resolve_powershell if ext == ".ps1"
30
+
31
+ DEFAULT_EXTENSIONS[ext]
32
+ end
33
+
34
+ # ファイルパスから拡張子を取得してインタープリタを解決する
35
+ # @param path [String] ファイルパス
36
+ # @return [String, nil] インタープリタ名、解決できない場合はnil
37
+ def resolve_from_path(path)
38
+ ext = File.extname(path)
39
+ return nil if ext.empty?
40
+
41
+ resolve(ext)
42
+ end
43
+
44
+ # 全ての拡張子マッピングを取得する
45
+ # @return [Hash] 拡張子とインタープリタのマッピング
46
+ def all_extensions
47
+ # PowerShellのプラットフォーム依存を解決した状態で返す
48
+ DEFAULT_EXTENSIONS.merge(".ps1" => resolve_powershell)
49
+ end
50
+
51
+ # Windowsプラットフォームかどうか
52
+ # @return [Boolean]
53
+ def windows?
54
+ RUBY_PLATFORM =~ /mingw|mswin|cygwin/ ? true : false
55
+ end
56
+
57
+ # macOSプラットフォームかどうか
58
+ # @return [Boolean]
59
+ def macos?
60
+ RUBY_PLATFORM =~ /darwin/ ? true : false
61
+ end
62
+
63
+ # Linuxプラットフォームかどうか
64
+ # @return [Boolean]
65
+ def linux?
66
+ RUBY_PLATFORM =~ /linux/ ? true : false
67
+ end
68
+
69
+ private
70
+
71
+ # PowerShellのインタープリタを解決する
72
+ # WindowsではpowershellとWindowsではpwsh
73
+ # @return [String]
74
+ def resolve_powershell
75
+ windows? ? "powershell" : "pwsh"
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,253 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "timeout"
5
+
6
+ module Rufio
7
+ # スクリプトを安全に実行するクラス
8
+ class ScriptExecutor
9
+ class << self
10
+ # スクリプトを実行する
11
+ # @param interpreter [String] インタープリタ(ruby, python3, bashなど)
12
+ # @param script_path [String] スクリプトのパス
13
+ # @param args [Array<String>] スクリプトへの引数
14
+ # @param timeout [Numeric, nil] タイムアウト秒数(nilの場合は無制限)
15
+ # @param chdir [String, nil] 作業ディレクトリ
16
+ # @param env [Hash, nil] 環境変数
17
+ # @return [Hash] 実行結果
18
+ def execute(interpreter, script_path, args = [], timeout: nil, chdir: nil, env: nil)
19
+ # 配列ベースのコマンドを構築(シェルインジェクション防止)
20
+ command = [interpreter, script_path, *args]
21
+
22
+ # オプションを構築
23
+ options = {}
24
+ options[:chdir] = chdir if chdir
25
+ # 環境変数をマージ(既存の環境変数を保持)
26
+ spawn_env = env || {}
27
+
28
+ execute_with_options(command, spawn_env, options, timeout)
29
+ rescue StandardError => e
30
+ build_error_result(e)
31
+ end
32
+
33
+ # DslCommandを実行する(タイプ別に分岐)
34
+ # @param dsl_command [DslCommand] 実行するDSLコマンド
35
+ # @param args [Array<String>] 追加の引数
36
+ # @param timeout [Numeric, nil] タイムアウト秒数
37
+ # @param chdir [String, nil] 作業ディレクトリ
38
+ # @param env [Hash, nil] 環境変数
39
+ # @return [Hash] 実行結果
40
+ def execute_command(dsl_command, args = [], timeout: nil, chdir: nil, env: nil)
41
+ case dsl_command.command_type
42
+ when :ruby
43
+ execute_ruby(dsl_command)
44
+ when :shell
45
+ execute_shell(dsl_command, timeout: timeout, chdir: chdir, env: env)
46
+ else
47
+ exec_args = dsl_command.to_execution_args
48
+ execute(exec_args[0], exec_args[1], args, timeout: timeout, chdir: chdir, env: env)
49
+ end
50
+ end
51
+
52
+ # inline Rubyコマンドを実行する
53
+ # @param dsl_command [DslCommand] 実行するDSLコマンド
54
+ # @return [Hash] 実行結果
55
+ def execute_ruby(dsl_command)
56
+ result = dsl_command.ruby_block.call
57
+ {
58
+ success: true,
59
+ exit_code: 0,
60
+ stdout: result.to_s,
61
+ stderr: "",
62
+ timeout: false
63
+ }
64
+ rescue StandardError => e
65
+ {
66
+ success: false,
67
+ exit_code: 1,
68
+ stdout: "",
69
+ stderr: "",
70
+ error: e.message,
71
+ timeout: false
72
+ }
73
+ end
74
+
75
+ # inline シェルコマンドを実行する
76
+ # @param dsl_command [DslCommand] 実行するDSLコマンド
77
+ # @param timeout [Numeric, nil] タイムアウト秒数
78
+ # @param chdir [String, nil] 作業ディレクトリ
79
+ # @param env [Hash, nil] 環境変数
80
+ # @return [Hash] 実行結果
81
+ def execute_shell(dsl_command, timeout: nil, chdir: nil, env: nil)
82
+ options = {}
83
+ options[:chdir] = chdir if chdir
84
+ spawn_env = env || {}
85
+
86
+ if timeout
87
+ execute_shell_with_timeout(dsl_command.shell_command, spawn_env, options, timeout)
88
+ else
89
+ execute_shell_without_timeout(dsl_command.shell_command, spawn_env, options)
90
+ end
91
+ rescue StandardError => e
92
+ build_error_result(e)
93
+ end
94
+
95
+ private
96
+
97
+ # コマンドを実行し、結果を返す
98
+ # @param command [Array<String>] 実行するコマンド
99
+ # @param env [Hash] 環境変数
100
+ # @param options [Hash] Open3オプション
101
+ # @param timeout_sec [Numeric, nil] タイムアウト秒数
102
+ # @return [Hash] 実行結果
103
+ def execute_with_options(command, env, options, timeout_sec)
104
+ if timeout_sec
105
+ execute_with_timeout(command, env, options, timeout_sec)
106
+ else
107
+ execute_without_timeout(command, env, options)
108
+ end
109
+ end
110
+
111
+ # タイムアウト付きで実行
112
+ def execute_with_timeout(command, env, options, timeout_sec)
113
+ stdout = ""
114
+ stderr = ""
115
+ status = nil
116
+ timed_out = false
117
+ pid = nil
118
+
119
+ begin
120
+ Timeout.timeout(timeout_sec) do
121
+ stdin, stdout_io, stderr_io, wait_thread = Open3.popen3(env, *command, **options)
122
+ pid = wait_thread.pid
123
+ stdin.close
124
+ stdout = stdout_io.read
125
+ stderr = stderr_io.read
126
+ stdout_io.close
127
+ stderr_io.close
128
+ status = wait_thread.value
129
+ end
130
+ rescue Timeout::Error
131
+ timed_out = true
132
+ # プロセスを終了
133
+ if pid
134
+ begin
135
+ Process.kill("TERM", pid)
136
+ sleep 0.1
137
+ Process.kill("KILL", pid)
138
+ rescue Errno::ESRCH, Errno::EPERM
139
+ # プロセスが既に終了している、または権限がない
140
+ end
141
+ end
142
+ end
143
+
144
+ if timed_out
145
+ {
146
+ success: false,
147
+ exit_code: nil,
148
+ stdout: stdout,
149
+ stderr: stderr,
150
+ timeout: true
151
+ }
152
+ else
153
+ {
154
+ success: status&.success? || false,
155
+ exit_code: status&.exitstatus || 1,
156
+ stdout: stdout,
157
+ stderr: stderr,
158
+ timeout: false
159
+ }
160
+ end
161
+ end
162
+
163
+ # タイムアウトなしで実行
164
+ def execute_without_timeout(command, env, options)
165
+ stdout, stderr, status = Open3.capture3(env, *command, **options)
166
+
167
+ {
168
+ success: status.success?,
169
+ exit_code: status.exitstatus,
170
+ stdout: stdout,
171
+ stderr: stderr,
172
+ timeout: false
173
+ }
174
+ end
175
+
176
+ # シェルコマンドをタイムアウト付きで実行
177
+ def execute_shell_with_timeout(shell_command, env, options, timeout_sec)
178
+ stdout = ""
179
+ stderr = ""
180
+ status = nil
181
+ timed_out = false
182
+ pid = nil
183
+
184
+ begin
185
+ Timeout.timeout(timeout_sec) do
186
+ stdin, stdout_io, stderr_io, wait_thread = Open3.popen3(env, shell_command, **options)
187
+ pid = wait_thread.pid
188
+ stdin.close
189
+ stdout = stdout_io.read
190
+ stderr = stderr_io.read
191
+ stdout_io.close
192
+ stderr_io.close
193
+ status = wait_thread.value
194
+ end
195
+ rescue Timeout::Error
196
+ timed_out = true
197
+ if pid
198
+ begin
199
+ Process.kill("TERM", pid)
200
+ sleep 0.1
201
+ Process.kill("KILL", pid)
202
+ rescue Errno::ESRCH, Errno::EPERM
203
+ # プロセスが既に終了している、または権限がない
204
+ end
205
+ end
206
+ end
207
+
208
+ if timed_out
209
+ {
210
+ success: false,
211
+ exit_code: nil,
212
+ stdout: stdout,
213
+ stderr: stderr,
214
+ timeout: true
215
+ }
216
+ else
217
+ {
218
+ success: status&.success? || false,
219
+ exit_code: status&.exitstatus || 1,
220
+ stdout: stdout,
221
+ stderr: stderr,
222
+ timeout: false
223
+ }
224
+ end
225
+ end
226
+
227
+ # シェルコマンドをタイムアウトなしで実行
228
+ def execute_shell_without_timeout(shell_command, env, options)
229
+ stdout, stderr, status = Open3.capture3(env, shell_command, **options)
230
+
231
+ {
232
+ success: status.success?,
233
+ exit_code: status.exitstatus,
234
+ stdout: stdout,
235
+ stderr: stderr,
236
+ timeout: false
237
+ }
238
+ end
239
+
240
+ # エラー結果を構築
241
+ def build_error_result(error)
242
+ {
243
+ success: false,
244
+ exit_code: 1,
245
+ stdout: "",
246
+ stderr: "",
247
+ error: error.message,
248
+ timeout: false
249
+ }
250
+ end
251
+ end
252
+ end
253
+ end
data/lib/rufio/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rufio
4
- VERSION = '0.41.0'
4
+ VERSION = '0.50.0'
5
5
  end
data/lib/rufio.rb CHANGED
@@ -21,10 +21,13 @@ require_relative "rufio/application"
21
21
  require_relative "rufio/file_opener"
22
22
  require_relative "rufio/health_checker"
23
23
 
24
- # プラグインシステム
25
- require_relative "rufio/plugin_config"
26
- require_relative "rufio/plugin"
27
- require_relative "rufio/plugin_manager"
24
+ # DSLコマンドシステム
25
+ require_relative "rufio/interpreter_resolver"
26
+ require_relative "rufio/dsl_command"
27
+ require_relative "rufio/script_executor"
28
+ require_relative "rufio/dsl_command_loader"
29
+ require_relative "rufio/builtin_commands"
30
+
28
31
  require_relative "rufio/command_mode"
29
32
  require_relative "rufio/command_mode_ui"
30
33
  require_relative "rufio/command_history"
@@ -45,9 +48,6 @@ require_relative "rufio/project_mode"
45
48
  require_relative "rufio/project_command"
46
49
  require_relative "rufio/project_log"
47
50
 
48
- # プラグインをロード
49
- Rufio::PluginManager.load_all
50
-
51
51
  module Rufio
52
52
  class Error < StandardError; end
53
53
  end