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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -0
- data/README.md +96 -194
- data/bin/rufio +0 -3
- data/docs/CHANGELOG_v0.50.0.md +128 -0
- data/examples/config.yml +8 -0
- data/lib/rufio/builtin_commands.rb +34 -0
- data/lib/rufio/command_completion.rb +20 -5
- data/lib/rufio/command_mode.rb +157 -46
- data/lib/rufio/config_loader.rb +37 -0
- data/lib/rufio/dsl_command.rb +120 -0
- data/lib/rufio/dsl_command_loader.rb +177 -0
- data/lib/rufio/interpreter_resolver.rb +79 -0
- data/lib/rufio/job_manager.rb +128 -0
- data/lib/rufio/job_mode.rb +146 -0
- data/lib/rufio/keybind_handler.rb +243 -232
- data/lib/rufio/notification_manager.rb +77 -0
- data/lib/rufio/script_config_loader.rb +101 -0
- data/lib/rufio/script_executor.rb +253 -0
- data/lib/rufio/script_path_manager.rb +386 -0
- data/lib/rufio/script_runner.rb +216 -0
- data/lib/rufio/task_status.rb +118 -0
- data/lib/rufio/terminal_ui.rb +181 -495
- data/lib/rufio/version.rb +1 -1
- data/lib/rufio.rb +15 -11
- data/scripts/test_jobs/build_simulation.sh +29 -0
- data/scripts/test_jobs/deploy_simulation.sh +37 -0
- data/scripts/test_jobs/quick.sh +11 -0
- data/scripts/test_jobs/random_result.sh +23 -0
- data/scripts/test_jobs/slow_fail.sh +10 -0
- data/scripts/test_jobs/slow_success.sh +10 -0
- data/scripts/test_jobs/very_slow.sh +19 -0
- metadata +23 -13
- data/docs/file-preview-optimization-analysis.md +0 -759
- data/docs/file-preview-performance-issue-FIXED.md +0 -547
- data/lib/rufio/plugin.rb +0 -89
- data/lib/rufio/plugin_config.rb +0 -59
- data/lib/rufio/plugin_manager.rb +0 -84
- data/lib/rufio/plugins/file_operations.rb +0 -44
- data/lib/rufio/plugins/hello.rb +0 -30
- data/lib/rufio/plugins/stop.rb +0 -32
- data/lib/rufio/project_command.rb +0 -147
- data/lib/rufio/project_log.rb +0 -68
- 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
|