rufio 0.41.0 → 0.60.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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/README.md +96 -194
  4. data/bin/rufio +0 -3
  5. data/docs/CHANGELOG_v0.50.0.md +128 -0
  6. data/examples/config.yml +8 -0
  7. data/lib/rufio/builtin_commands.rb +34 -0
  8. data/lib/rufio/command_completion.rb +20 -5
  9. data/lib/rufio/command_mode.rb +157 -46
  10. data/lib/rufio/config_loader.rb +37 -0
  11. data/lib/rufio/dsl_command.rb +120 -0
  12. data/lib/rufio/dsl_command_loader.rb +177 -0
  13. data/lib/rufio/interpreter_resolver.rb +79 -0
  14. data/lib/rufio/job_manager.rb +128 -0
  15. data/lib/rufio/job_mode.rb +146 -0
  16. data/lib/rufio/keybind_handler.rb +243 -232
  17. data/lib/rufio/notification_manager.rb +77 -0
  18. data/lib/rufio/script_config_loader.rb +101 -0
  19. data/lib/rufio/script_executor.rb +253 -0
  20. data/lib/rufio/script_path_manager.rb +386 -0
  21. data/lib/rufio/script_runner.rb +216 -0
  22. data/lib/rufio/task_status.rb +118 -0
  23. data/lib/rufio/terminal_ui.rb +181 -495
  24. data/lib/rufio/version.rb +1 -1
  25. data/lib/rufio.rb +15 -11
  26. data/scripts/test_jobs/build_simulation.sh +29 -0
  27. data/scripts/test_jobs/deploy_simulation.sh +37 -0
  28. data/scripts/test_jobs/quick.sh +11 -0
  29. data/scripts/test_jobs/random_result.sh +23 -0
  30. data/scripts/test_jobs/slow_fail.sh +10 -0
  31. data/scripts/test_jobs/slow_success.sh +10 -0
  32. data/scripts/test_jobs/very_slow.sh +19 -0
  33. metadata +23 -13
  34. data/docs/file-preview-optimization-analysis.md +0 -759
  35. data/docs/file-preview-performance-issue-FIXED.md +0 -547
  36. data/lib/rufio/plugin.rb +0 -89
  37. data/lib/rufio/plugin_config.rb +0 -59
  38. data/lib/rufio/plugin_manager.rb +0 -84
  39. data/lib/rufio/plugins/file_operations.rb +0 -44
  40. data/lib/rufio/plugins/hello.rb +0 -30
  41. data/lib/rufio/plugins/stop.rb +0 -32
  42. data/lib/rufio/project_command.rb +0 -147
  43. data/lib/rufio/project_log.rb +0 -68
  44. data/lib/rufio/project_mode.rb +0 -58
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module Rufio
6
+ # 複数の設定ファイルからscript_pathsをロード・マージするクラス
7
+ # 優先順位: ローカル > ユーザー > システム
8
+ class ScriptConfigLoader
9
+ # デフォルトの設定ファイルパス
10
+ DEFAULT_LOCAL_PATH = './rufio.yml'
11
+ DEFAULT_USER_PATH = File.expand_path('~/.config/rufio/config.yml')
12
+ DEFAULT_SYSTEM_PATH = '/etc/rufio/config.yml'
13
+
14
+ # @param local_path [String, nil] ローカル設定ファイルのパス
15
+ # @param user_path [String, nil] ユーザー設定ファイルのパス
16
+ # @param system_path [String, nil] システム設定ファイルのパス
17
+ def initialize(local_path: nil, user_path: nil, system_path: nil)
18
+ @local_path = local_path || DEFAULT_LOCAL_PATH
19
+ @user_path = user_path || DEFAULT_USER_PATH
20
+ @system_path = system_path || DEFAULT_SYSTEM_PATH
21
+ end
22
+
23
+ # マージされたscript_pathsを取得
24
+ # @return [Array<String>] スクリプトパスの配列(優先順位順、重複なし)
25
+ def script_paths
26
+ paths = []
27
+ seen = Set.new
28
+
29
+ # 優先順位順に読み込み(ローカル > ユーザー > システム)
30
+ [@local_path, @user_path, @system_path].each do |config_path|
31
+ next unless config_path && File.exist?(config_path)
32
+
33
+ config_paths = load_paths_from_file(config_path)
34
+ config_paths.each do |path|
35
+ expanded = File.expand_path(path)
36
+ next if seen.include?(expanded)
37
+
38
+ seen.add(expanded)
39
+ paths << expanded
40
+ end
41
+ end
42
+
43
+ paths
44
+ end
45
+
46
+ # 全設定をマージして取得
47
+ # @return [Hash] マージされた設定
48
+ def merged_config
49
+ config = {}
50
+
51
+ # 逆順で読み込み(システム < ユーザー < ローカル)
52
+ [@system_path, @user_path, @local_path].each do |config_path|
53
+ next unless config_path && File.exist?(config_path)
54
+
55
+ file_config = load_config(config_path)
56
+ config = deep_merge(config, file_config)
57
+ end
58
+
59
+ # script_pathsは特別処理(マージではなく優先順位付き結合)
60
+ config['script_paths'] = script_paths
61
+ config
62
+ end
63
+
64
+ private
65
+
66
+ # 設定ファイルからscript_pathsを読み込む
67
+ # @param path [String] 設定ファイルのパス
68
+ # @return [Array<String>] パスの配列
69
+ def load_paths_from_file(path)
70
+ config = load_config(path)
71
+ config['script_paths'] || []
72
+ end
73
+
74
+ # 設定ファイルを読み込む
75
+ # @param path [String] 設定ファイルのパス
76
+ # @return [Hash] 設定内容
77
+ def load_config(path)
78
+ return {} unless File.exist?(path)
79
+
80
+ yaml = YAML.safe_load(File.read(path), symbolize_names: false)
81
+ yaml || {}
82
+ rescue StandardError => e
83
+ warn "Warning: Failed to load config #{path}: #{e.message}"
84
+ {}
85
+ end
86
+
87
+ # ハッシュを深くマージ
88
+ # @param base [Hash] ベースとなるハッシュ
89
+ # @param override [Hash] 上書きするハッシュ
90
+ # @return [Hash] マージされたハッシュ
91
+ def deep_merge(base, override)
92
+ base.merge(override) do |_key, old_val, new_val|
93
+ if old_val.is_a?(Hash) && new_val.is_a?(Hash)
94
+ deep_merge(old_val, new_val)
95
+ else
96
+ new_val
97
+ end
98
+ end
99
+ end
100
+ end
101
+ 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