rufio 0.50.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 +32 -36
- data/examples/config.yml +8 -0
- data/lib/rufio/command_completion.rb +20 -5
- data/lib/rufio/command_mode.rb +98 -5
- data/lib/rufio/config_loader.rb +37 -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_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 +8 -4
- 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 +17 -7
- data/docs/file-preview-optimization-analysis.md +0 -759
- data/docs/file-preview-performance-issue-FIXED.md +0 -547
- 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,386 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
module Rufio
|
|
6
|
+
# スクリプトパスを管理するクラス
|
|
7
|
+
# 設定ファイルからパスを読み込み、スクリプト名で解決する
|
|
8
|
+
class ScriptPathManager
|
|
9
|
+
attr_reader :paths
|
|
10
|
+
|
|
11
|
+
# サポートするスクリプト拡張子
|
|
12
|
+
SUPPORTED_EXTENSIONS = %w[.sh .rb .py .pl .js .ts .ps1].freeze
|
|
13
|
+
|
|
14
|
+
# 履歴の最大サイズ
|
|
15
|
+
MAX_HISTORY_SIZE = 100
|
|
16
|
+
|
|
17
|
+
# @param config_path [String] 設定ファイルのパス
|
|
18
|
+
def initialize(config_path)
|
|
19
|
+
@config_path = config_path
|
|
20
|
+
@config = load_config
|
|
21
|
+
@paths = expand_paths(@config['script_paths'] || [])
|
|
22
|
+
@cache = {}
|
|
23
|
+
@scripts_cache = nil
|
|
24
|
+
@execution_history = []
|
|
25
|
+
@execution_count = Hash.new(0)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# スクリプト名で解決
|
|
29
|
+
# @param command_name [String] スクリプト名(拡張子あり/なし)
|
|
30
|
+
# @return [String, nil] スクリプトのフルパス、見つからない場合はnil
|
|
31
|
+
def resolve(command_name)
|
|
32
|
+
# キャッシュをチェック
|
|
33
|
+
return @cache[command_name] if @cache.key?(command_name)
|
|
34
|
+
|
|
35
|
+
scripts = find_scripts(command_name)
|
|
36
|
+
|
|
37
|
+
case scripts.size
|
|
38
|
+
when 0
|
|
39
|
+
nil
|
|
40
|
+
when 1
|
|
41
|
+
@cache[command_name] = scripts.first
|
|
42
|
+
else
|
|
43
|
+
# 複数見つかった場合は最初のものを返す(on_multiple_match: 'first')
|
|
44
|
+
@cache[command_name] = scripts.first
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# 全スクリプト一覧を取得(タブ補完用)
|
|
49
|
+
# @return [Array<String>] スクリプト名(拡張子なし)の配列
|
|
50
|
+
def all_scripts
|
|
51
|
+
scripts = []
|
|
52
|
+
seen = Set.new
|
|
53
|
+
|
|
54
|
+
@paths.each do |path|
|
|
55
|
+
next unless Dir.exist?(path)
|
|
56
|
+
|
|
57
|
+
Dir.glob(File.join(path, '*')).each do |file|
|
|
58
|
+
next unless File.file?(file)
|
|
59
|
+
|
|
60
|
+
basename = File.basename(file)
|
|
61
|
+
next if basename.start_with?('.')
|
|
62
|
+
next unless executable_script?(file)
|
|
63
|
+
|
|
64
|
+
name = File.basename(file, '.*')
|
|
65
|
+
next if seen.include?(name)
|
|
66
|
+
|
|
67
|
+
seen.add(name)
|
|
68
|
+
scripts << name
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
scripts.sort
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# パスを追加
|
|
76
|
+
# @param path [String] 追加するディレクトリパス
|
|
77
|
+
# @return [Boolean] 追加成功した場合true、重複の場合false
|
|
78
|
+
def add_path(path)
|
|
79
|
+
expanded_path = File.expand_path(path)
|
|
80
|
+
return false if @paths.include?(expanded_path)
|
|
81
|
+
|
|
82
|
+
@paths << expanded_path
|
|
83
|
+
save_config
|
|
84
|
+
invalidate_cache
|
|
85
|
+
true
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# パスを削除
|
|
89
|
+
# @param path [String] 削除するディレクトリパス
|
|
90
|
+
# @return [Boolean] 削除成功した場合true
|
|
91
|
+
def remove_path(path)
|
|
92
|
+
expanded_path = File.expand_path(path)
|
|
93
|
+
result = @paths.delete(expanded_path)
|
|
94
|
+
if result
|
|
95
|
+
save_config
|
|
96
|
+
invalidate_cache
|
|
97
|
+
end
|
|
98
|
+
!!result
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# --- Phase 4: 複数マッチ ---
|
|
102
|
+
|
|
103
|
+
# すべてのマッチを取得(複数マッチ対応)
|
|
104
|
+
# @param command_name [String] コマンド名
|
|
105
|
+
# @return [Array<String>] マッチしたスクリプトのパス
|
|
106
|
+
def find_all_matches(command_name)
|
|
107
|
+
find_scripts_all_paths(command_name)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# --- Phase 4: タブ補完 ---
|
|
111
|
+
|
|
112
|
+
# スクリプト名を補完
|
|
113
|
+
# @param prefix [String] 入力中の文字列
|
|
114
|
+
# @return [Array<String>] 補完候補(拡張子なし)
|
|
115
|
+
def complete(prefix)
|
|
116
|
+
scripts = all_scripts
|
|
117
|
+
return scripts if prefix.empty?
|
|
118
|
+
|
|
119
|
+
scripts.select { |name| name.downcase.start_with?(prefix.downcase) }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# --- Phase 4: fuzzy matching ---
|
|
123
|
+
|
|
124
|
+
# fuzzy matchingで候補を取得
|
|
125
|
+
# @param query [String] 検索クエリ
|
|
126
|
+
# @return [Array<String>] マッチしたスクリプト名
|
|
127
|
+
def fuzzy_match(query)
|
|
128
|
+
return all_scripts if query.empty?
|
|
129
|
+
|
|
130
|
+
scripts = all_scripts
|
|
131
|
+
query_chars = query.downcase.chars
|
|
132
|
+
|
|
133
|
+
# スコア計算してソート
|
|
134
|
+
scored = scripts.map do |name|
|
|
135
|
+
score = fuzzy_score(name.downcase, query_chars)
|
|
136
|
+
[name, score]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# スコアが0より大きいものをスコア降順で返す
|
|
140
|
+
scored.select { |_, score| score > 0 }
|
|
141
|
+
.sort_by { |_, score| -score }
|
|
142
|
+
.map { |name, _| name }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# --- Phase 4: キャッシュ ---
|
|
146
|
+
|
|
147
|
+
# キャッシュを無効化(public)
|
|
148
|
+
def invalidate_cache
|
|
149
|
+
@cache.clear
|
|
150
|
+
@scripts_cache = nil
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# --- Phase 4: 実行履歴 ---
|
|
154
|
+
|
|
155
|
+
# 実行を記録
|
|
156
|
+
# @param script_name [String] スクリプト名
|
|
157
|
+
def record_execution(script_name)
|
|
158
|
+
# 履歴の先頭に追加
|
|
159
|
+
@execution_history.unshift(script_name)
|
|
160
|
+
@execution_history = @execution_history.take(MAX_HISTORY_SIZE)
|
|
161
|
+
|
|
162
|
+
# カウントを増やす
|
|
163
|
+
@execution_count[script_name] += 1
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# 実行履歴を取得
|
|
167
|
+
# @return [Array<String>] 最近実行したスクリプト名(新しい順)
|
|
168
|
+
def execution_history
|
|
169
|
+
@execution_history.dup
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# 実行頻度順にスクリプトを取得
|
|
173
|
+
# @return [Array<String>] スクリプト名(頻度順)
|
|
174
|
+
def scripts_by_frequency
|
|
175
|
+
scripts = all_scripts
|
|
176
|
+
scripts.sort_by { |name| -@execution_count[name] }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# --- セクション9: エラーハンドリング ---
|
|
180
|
+
|
|
181
|
+
# 類似スクリプトの候補を取得
|
|
182
|
+
# @param query [String] 検索クエリ(typoを含む可能性あり)
|
|
183
|
+
# @return [Array<String>] 類似スクリプト名
|
|
184
|
+
def suggest(query)
|
|
185
|
+
scripts = all_scripts
|
|
186
|
+
return [] if scripts.empty?
|
|
187
|
+
|
|
188
|
+
# レーベンシュタイン距離でソート
|
|
189
|
+
scored = scripts.map do |name|
|
|
190
|
+
distance = levenshtein_distance(query.downcase, name.downcase)
|
|
191
|
+
[name, distance]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# 距離が3以下のものを距離順で返す
|
|
195
|
+
scored.select { |_, dist| dist <= 3 }
|
|
196
|
+
.sort_by { |_, dist| dist }
|
|
197
|
+
.map { |name, _| name }
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# ファイルが実行可能かどうかをチェック
|
|
201
|
+
# @param path [String] ファイルパス
|
|
202
|
+
# @return [Boolean]
|
|
203
|
+
def executable?(path)
|
|
204
|
+
return false unless File.exist?(path)
|
|
205
|
+
|
|
206
|
+
File.executable?(path)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# ファイルに実行権限を付与
|
|
210
|
+
# @param path [String] ファイルパス
|
|
211
|
+
# @return [Boolean] 成功した場合true
|
|
212
|
+
def fix_permissions(path)
|
|
213
|
+
return false unless File.exist?(path)
|
|
214
|
+
|
|
215
|
+
current_mode = File.stat(path).mode
|
|
216
|
+
new_mode = current_mode | 0111 # 実行権限を追加
|
|
217
|
+
File.chmod(new_mode, path)
|
|
218
|
+
true
|
|
219
|
+
rescue StandardError
|
|
220
|
+
false
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
private
|
|
224
|
+
|
|
225
|
+
# 設定ファイルを読み込む
|
|
226
|
+
# @return [Hash] 設定内容
|
|
227
|
+
def load_config
|
|
228
|
+
return {} unless File.exist?(@config_path)
|
|
229
|
+
|
|
230
|
+
yaml = YAML.safe_load(File.read(@config_path), symbolize_names: false)
|
|
231
|
+
yaml || {}
|
|
232
|
+
rescue StandardError => e
|
|
233
|
+
warn "Failed to load config: #{e.message}"
|
|
234
|
+
{}
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# 設定ファイルを保存
|
|
238
|
+
def save_config
|
|
239
|
+
@config['script_paths'] = @paths
|
|
240
|
+
File.write(@config_path, YAML.dump(@config))
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# パスを展開(チルダ展開)
|
|
244
|
+
# @param paths [Array<String>] パスの配列
|
|
245
|
+
# @return [Array<String>] 展開済みのパス
|
|
246
|
+
def expand_paths(paths)
|
|
247
|
+
paths.map { |p| File.expand_path(p) }
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# スクリプトを検索
|
|
251
|
+
# @param command_name [String] コマンド名
|
|
252
|
+
# @return [Array<String>] 見つかったスクリプトのパス
|
|
253
|
+
def find_scripts(command_name)
|
|
254
|
+
scripts = []
|
|
255
|
+
basename_without_ext = command_name.sub(/\.[^.]+$/, '')
|
|
256
|
+
has_extension = command_name.include?('.')
|
|
257
|
+
|
|
258
|
+
@paths.each do |path|
|
|
259
|
+
next unless Dir.exist?(path)
|
|
260
|
+
|
|
261
|
+
Dir.glob(File.join(path, '*')).each do |file|
|
|
262
|
+
next unless File.file?(file)
|
|
263
|
+
|
|
264
|
+
file_basename = File.basename(file)
|
|
265
|
+
next if file_basename.start_with?('.')
|
|
266
|
+
next unless executable_script?(file)
|
|
267
|
+
|
|
268
|
+
if has_extension
|
|
269
|
+
# 拡張子付きで完全一致
|
|
270
|
+
scripts << file if file_basename.downcase == command_name.downcase
|
|
271
|
+
else
|
|
272
|
+
# 拡張子なしで比較
|
|
273
|
+
file_name_without_ext = File.basename(file, '.*')
|
|
274
|
+
scripts << file if file_name_without_ext.downcase == basename_without_ext.downcase
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# 最初のパスで見つかったらそれを優先(on_multiple_match: 'first'相当)
|
|
279
|
+
return scripts unless scripts.empty?
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
scripts
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# 実行可能なスクリプトかどうかを判定
|
|
286
|
+
# @param path [String] ファイルパス
|
|
287
|
+
# @return [Boolean]
|
|
288
|
+
def executable_script?(path)
|
|
289
|
+
ext = File.extname(path).downcase
|
|
290
|
+
return true if SUPPORTED_EXTENSIONS.include?(ext)
|
|
291
|
+
|
|
292
|
+
File.executable?(path)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# すべてのパスからスクリプトを検索(最初のパスで止まらない)
|
|
296
|
+
# @param command_name [String] コマンド名
|
|
297
|
+
# @return [Array<String>] 見つかったスクリプトのパス
|
|
298
|
+
def find_scripts_all_paths(command_name)
|
|
299
|
+
scripts = []
|
|
300
|
+
basename_without_ext = command_name.sub(/\.[^.]+$/, '')
|
|
301
|
+
has_extension = command_name.include?('.')
|
|
302
|
+
|
|
303
|
+
@paths.each do |path|
|
|
304
|
+
next unless Dir.exist?(path)
|
|
305
|
+
|
|
306
|
+
Dir.glob(File.join(path, '*')).each do |file|
|
|
307
|
+
next unless File.file?(file)
|
|
308
|
+
|
|
309
|
+
file_basename = File.basename(file)
|
|
310
|
+
next if file_basename.start_with?('.')
|
|
311
|
+
next unless executable_script?(file)
|
|
312
|
+
|
|
313
|
+
if has_extension
|
|
314
|
+
scripts << file if file_basename.downcase == command_name.downcase
|
|
315
|
+
else
|
|
316
|
+
file_name_without_ext = File.basename(file, '.*')
|
|
317
|
+
scripts << file if file_name_without_ext.downcase == basename_without_ext.downcase
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
scripts
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# レーベンシュタイン距離を計算
|
|
326
|
+
# @param s1 [String] 文字列1
|
|
327
|
+
# @param s2 [String] 文字列2
|
|
328
|
+
# @return [Integer] 編集距離
|
|
329
|
+
def levenshtein_distance(s1, s2)
|
|
330
|
+
m = s1.length
|
|
331
|
+
n = s2.length
|
|
332
|
+
return n if m == 0
|
|
333
|
+
return m if n == 0
|
|
334
|
+
|
|
335
|
+
# 動的計画法でテーブルを構築
|
|
336
|
+
d = Array.new(m + 1) { Array.new(n + 1, 0) }
|
|
337
|
+
|
|
338
|
+
(0..m).each { |i| d[i][0] = i }
|
|
339
|
+
(0..n).each { |j| d[0][j] = j }
|
|
340
|
+
|
|
341
|
+
(1..m).each do |i|
|
|
342
|
+
(1..n).each do |j|
|
|
343
|
+
cost = s1[i - 1] == s2[j - 1] ? 0 : 1
|
|
344
|
+
d[i][j] = [
|
|
345
|
+
d[i - 1][j] + 1, # 削除
|
|
346
|
+
d[i][j - 1] + 1, # 挿入
|
|
347
|
+
d[i - 1][j - 1] + cost # 置換
|
|
348
|
+
].min
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
d[m][n]
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# fuzzy matchingのスコアを計算
|
|
356
|
+
# @param text [String] 対象テキスト
|
|
357
|
+
# @param query_chars [Array<String>] クエリの文字配列
|
|
358
|
+
# @return [Integer] スコア(マッチしない場合は0)
|
|
359
|
+
def fuzzy_score(text, query_chars)
|
|
360
|
+
score = 0
|
|
361
|
+
text_index = 0
|
|
362
|
+
|
|
363
|
+
query_chars.each do |char|
|
|
364
|
+
# テキスト内で文字を探す
|
|
365
|
+
found_index = text.index(char, text_index)
|
|
366
|
+
return 0 unless found_index # マッチしない場合は0
|
|
367
|
+
|
|
368
|
+
# 連続していればボーナス
|
|
369
|
+
if found_index == text_index
|
|
370
|
+
score += 2
|
|
371
|
+
else
|
|
372
|
+
score += 1
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# 単語の先頭ならボーナス
|
|
376
|
+
if found_index == 0 || text[found_index - 1] == '_' || text[found_index - 1] == '-'
|
|
377
|
+
score += 1
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
text_index = found_index + 1
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
score
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module Rufio
|
|
6
|
+
# スクリプトパスからスクリプトを検索・実行するクラス
|
|
7
|
+
# ジョブマネージャーと連携してバックグラウンド実行を管理
|
|
8
|
+
class ScriptRunner
|
|
9
|
+
# サポートするスクリプト拡張子
|
|
10
|
+
SUPPORTED_EXTENSIONS = %w[.sh .rb .py .pl .js .ts .ps1].freeze
|
|
11
|
+
|
|
12
|
+
# @param script_paths [Array<String>] スクリプトを検索するディレクトリのリスト
|
|
13
|
+
# @param job_manager [JobManager, nil] ジョブマネージャー(nilの場合は同期実行)
|
|
14
|
+
def initialize(script_paths:, job_manager: nil)
|
|
15
|
+
@script_paths = script_paths.map { |p| File.expand_path(p) }
|
|
16
|
+
@job_manager = job_manager
|
|
17
|
+
@scripts_cache = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# 利用可能なスクリプト一覧を取得
|
|
21
|
+
# @return [Array<Hash>] スクリプト情報の配列 [{ name:, path:, dir: }, ...]
|
|
22
|
+
def available_scripts
|
|
23
|
+
@scripts_cache ||= scan_scripts
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# スクリプト名で検索
|
|
27
|
+
# @param name [String] スクリプト名(拡張子あり/なし)
|
|
28
|
+
# @return [Hash, nil] スクリプト情報 { name:, path:, dir: } または nil
|
|
29
|
+
def find_script(name)
|
|
30
|
+
# 完全一致を優先
|
|
31
|
+
script = available_scripts.find { |s| s[:name] == name }
|
|
32
|
+
return script if script
|
|
33
|
+
|
|
34
|
+
# 拡張子なしで検索
|
|
35
|
+
SUPPORTED_EXTENSIONS.each do |ext|
|
|
36
|
+
script = available_scripts.find { |s| s[:name] == "#{name}#{ext}" }
|
|
37
|
+
return script if script
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# スクリプト名の補完候補を取得
|
|
44
|
+
# @param prefix [String] 入力中の文字列
|
|
45
|
+
# @return [Array<String>] 補完候補のスクリプト名
|
|
46
|
+
def complete(prefix)
|
|
47
|
+
available_scripts
|
|
48
|
+
.map { |s| s[:name] }
|
|
49
|
+
.select { |name| name.start_with?(prefix) }
|
|
50
|
+
.uniq
|
|
51
|
+
.sort
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# スクリプトをジョブとして実行
|
|
55
|
+
# @param name [String] スクリプト名
|
|
56
|
+
# @param working_dir [String] 作業ディレクトリ
|
|
57
|
+
# @param selected_file [String, nil] 選択中のファイル
|
|
58
|
+
# @param selected_dir [String, nil] 選択中のディレクトリ
|
|
59
|
+
# @return [TaskStatus, nil] 作成されたジョブ、またはスクリプトが見つからない場合nil
|
|
60
|
+
def run(name, working_dir:, selected_file: nil, selected_dir: nil)
|
|
61
|
+
script = find_script(name)
|
|
62
|
+
return nil unless script
|
|
63
|
+
|
|
64
|
+
env = build_environment(working_dir, selected_file, selected_dir)
|
|
65
|
+
execute_script(script, working_dir, env)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# キャッシュをクリア
|
|
69
|
+
def clear_cache
|
|
70
|
+
@scripts_cache = nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# スクリプトディレクトリをスキャンしてスクリプトを収集
|
|
76
|
+
# @return [Array<Hash>] スクリプト情報の配列
|
|
77
|
+
def scan_scripts
|
|
78
|
+
scripts = []
|
|
79
|
+
seen_names = Set.new
|
|
80
|
+
|
|
81
|
+
@script_paths.each do |dir|
|
|
82
|
+
next unless Dir.exist?(dir)
|
|
83
|
+
|
|
84
|
+
Dir.glob(File.join(dir, '*')).each do |path|
|
|
85
|
+
next unless File.file?(path)
|
|
86
|
+
next unless executable_script?(path)
|
|
87
|
+
|
|
88
|
+
name = File.basename(path)
|
|
89
|
+
# 最初に見つかったものを優先(同名スクリプトは先のパスが優先)
|
|
90
|
+
next if seen_names.include?(name)
|
|
91
|
+
|
|
92
|
+
seen_names.add(name)
|
|
93
|
+
scripts << {
|
|
94
|
+
name: name,
|
|
95
|
+
path: path,
|
|
96
|
+
dir: dir
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
scripts.sort_by { |s| s[:name] }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# 実行可能なスクリプトかどうかを判定
|
|
105
|
+
# @param path [String] ファイルパス
|
|
106
|
+
# @return [Boolean]
|
|
107
|
+
def executable_script?(path)
|
|
108
|
+
ext = File.extname(path).downcase
|
|
109
|
+
return true if SUPPORTED_EXTENSIONS.include?(ext)
|
|
110
|
+
|
|
111
|
+
# 拡張子がなくても実行可能なら対象
|
|
112
|
+
File.executable?(path)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# 環境変数を構築
|
|
116
|
+
# @param working_dir [String] 作業ディレクトリ
|
|
117
|
+
# @param selected_file [String, nil] 選択中のファイル
|
|
118
|
+
# @param selected_dir [String, nil] 選択中のディレクトリ
|
|
119
|
+
# @return [Hash] 環境変数のハッシュ
|
|
120
|
+
def build_environment(working_dir, selected_file, selected_dir)
|
|
121
|
+
env = {
|
|
122
|
+
'RUFIO_CURRENT_DIR' => working_dir
|
|
123
|
+
}
|
|
124
|
+
env['RUFIO_SELECTED_FILE'] = selected_file if selected_file
|
|
125
|
+
env['RUFIO_SELECTED_DIR'] = selected_dir if selected_dir
|
|
126
|
+
env
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# スクリプトを実行してジョブを作成
|
|
130
|
+
# @param script [Hash] スクリプト情報
|
|
131
|
+
# @param working_dir [String] 作業ディレクトリ
|
|
132
|
+
# @param env [Hash] 環境変数
|
|
133
|
+
# @return [TaskStatus] 作成されたジョブ
|
|
134
|
+
def execute_script(script, working_dir, env = {})
|
|
135
|
+
command = build_command(script)
|
|
136
|
+
|
|
137
|
+
if @job_manager
|
|
138
|
+
# ジョブマネージャーにジョブを追加
|
|
139
|
+
job = @job_manager.add_job(
|
|
140
|
+
name: script[:name],
|
|
141
|
+
path: working_dir,
|
|
142
|
+
command: command
|
|
143
|
+
)
|
|
144
|
+
job.start
|
|
145
|
+
|
|
146
|
+
# バックグラウンドで実行
|
|
147
|
+
Thread.new do
|
|
148
|
+
execute_in_background(job, command, working_dir, env)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
job
|
|
152
|
+
else
|
|
153
|
+
# 同期実行(ジョブマネージャーがない場合)
|
|
154
|
+
stdout, stderr, status = Open3.capture3(env, command, chdir: working_dir)
|
|
155
|
+
{
|
|
156
|
+
success: status.success?,
|
|
157
|
+
output: stdout,
|
|
158
|
+
error: stderr,
|
|
159
|
+
exit_code: status.exitstatus
|
|
160
|
+
}
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# スクリプトの実行コマンドを構築
|
|
165
|
+
# @param script [Hash] スクリプト情報
|
|
166
|
+
# @return [String] 実行コマンド
|
|
167
|
+
def build_command(script)
|
|
168
|
+
path = script[:path]
|
|
169
|
+
ext = File.extname(path).downcase
|
|
170
|
+
|
|
171
|
+
case ext
|
|
172
|
+
when '.rb'
|
|
173
|
+
"ruby #{path.shellescape}"
|
|
174
|
+
when '.py'
|
|
175
|
+
"python3 #{path.shellescape}"
|
|
176
|
+
when '.js'
|
|
177
|
+
"node #{path.shellescape}"
|
|
178
|
+
when '.ts'
|
|
179
|
+
"ts-node #{path.shellescape}"
|
|
180
|
+
when '.pl'
|
|
181
|
+
"perl #{path.shellescape}"
|
|
182
|
+
when '.ps1'
|
|
183
|
+
"pwsh #{path.shellescape}"
|
|
184
|
+
else
|
|
185
|
+
# shスクリプトまたは実行可能ファイル
|
|
186
|
+
path.shellescape
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# バックグラウンドでスクリプトを実行
|
|
191
|
+
# @param job [TaskStatus] ジョブ
|
|
192
|
+
# @param command [String] 実行コマンド
|
|
193
|
+
# @param working_dir [String] 作業ディレクトリ
|
|
194
|
+
# @param env [Hash] 環境変数
|
|
195
|
+
def execute_in_background(job, command, working_dir, env = {})
|
|
196
|
+
stdout, stderr, status = Open3.capture3(env, command, chdir: working_dir)
|
|
197
|
+
|
|
198
|
+
# ログを追加
|
|
199
|
+
job.append_log(stdout) unless stdout.empty?
|
|
200
|
+
job.append_log(stderr) unless stderr.empty?
|
|
201
|
+
|
|
202
|
+
if status.success?
|
|
203
|
+
job.complete(exit_code: status.exitstatus)
|
|
204
|
+
else
|
|
205
|
+
job.fail(exit_code: status.exitstatus)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# 通知を送信
|
|
209
|
+
@job_manager&.notify_completion(job)
|
|
210
|
+
rescue StandardError => e
|
|
211
|
+
job.append_log("Error: #{e.message}")
|
|
212
|
+
job.fail(exit_code: -1)
|
|
213
|
+
@job_manager&.notify_completion(job)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|