rufio 0.40.1 → 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.
@@ -3,7 +3,7 @@
3
3
  require 'open3'
4
4
 
5
5
  module Rufio
6
- # バックグラウンドでシェルコマンドを実行するクラス
6
+ # バックグラウンドでシェルコマンドまたはRubyコードを実行するクラス
7
7
  class BackgroundCommandExecutor
8
8
  attr_reader :command_logger
9
9
 
@@ -13,11 +13,12 @@ module Rufio
13
13
  @command_logger = command_logger
14
14
  @thread = nil
15
15
  @command = nil
16
+ @command_type = nil # :shell または :ruby
16
17
  @completed = false
17
18
  @completion_message = nil
18
19
  end
19
20
 
20
- # コマンドを非同期で実行
21
+ # シェルコマンドを非同期で実行
21
22
  # @param command [String] 実行するコマンド
22
23
  # @return [Boolean] 実行を開始した場合はtrue、既に実行中の場合はfalse
23
24
  def execute_async(command)
@@ -25,6 +26,7 @@ module Rufio
25
26
  return false if running?
26
27
 
27
28
  @command = command
29
+ @command_type = :shell
28
30
  @completed = false
29
31
  @completion_message = nil
30
32
 
@@ -73,6 +75,54 @@ module Rufio
73
75
  true
74
76
  end
75
77
 
78
+ # Rubyコード(プラグインコマンド)を非同期で実行
79
+ # @param command_name [String] コマンド名(表示用)
80
+ # @param block [Proc] 実行するコードブロック
81
+ # @return [Boolean] 実行を開始した場合はtrue、既に実行中の場合はfalse
82
+ def execute_ruby_async(command_name, &block)
83
+ # 既に実行中の場合は新しいコマンドを開始しない
84
+ return false if running?
85
+
86
+ @command = command_name
87
+ @command_type = :ruby
88
+ @completed = false
89
+ @completion_message = nil
90
+
91
+ @thread = Thread.new do
92
+ begin
93
+ # Rubyコードを実行
94
+ result = block.call
95
+
96
+ # 結果をログに保存
97
+ output = result.to_s
98
+
99
+ @command_logger.log(
100
+ command_name,
101
+ output,
102
+ success: true,
103
+ error: nil
104
+ )
105
+
106
+ # 完了メッセージを生成
107
+ @completion_message = "✓ #{command_name} 完了"
108
+ @completed = true
109
+ rescue StandardError => e
110
+ # エラーが発生した場合もログに記録
111
+ @command_logger.log(
112
+ command_name,
113
+ "",
114
+ success: false,
115
+ error: e.message
116
+ )
117
+
118
+ @completion_message = "✗ #{command_name} エラー: #{e.message}"
119
+ @completed = true
120
+ end
121
+ end
122
+
123
+ true
124
+ end
125
+
76
126
  # コマンドが実行中かどうか
77
127
  # @return [Boolean] 実行中の場合はtrue
78
128
  def running?
@@ -85,6 +135,18 @@ module Rufio
85
135
  @completion_message
86
136
  end
87
137
 
138
+ # 現在実行中のコマンド名を取得
139
+ # @return [String, nil] コマンド名(実行中でない場合はnil)
140
+ def current_command
141
+ running? ? @command : nil
142
+ end
143
+
144
+ # コマンドタイプを取得
145
+ # @return [Symbol, nil] :shell または :ruby(実行中でない場合はnil)
146
+ def command_type
147
+ running? ? @command_type : nil
148
+ end
149
+
88
150
  private
89
151
 
90
152
  # コマンド文字列からコマンド名を抽出
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rufio
4
+ # 組み込みコマンドを定義するモジュール
5
+ # DSL形式で定義されたコマンドをDslCommandインスタンスとして提供する
6
+ module BuiltinCommands
7
+ class << self
8
+ # 組み込みコマンドをロードする
9
+ # @return [Hash{Symbol => DslCommand}] コマンド名をキーとしたハッシュ
10
+ def load
11
+ commands = {}
12
+
13
+ # hello コマンド
14
+ commands[:hello] = DslCommand.new(
15
+ name: "hello",
16
+ ruby_block: -> { "Hello, World!\n\nこのコマンドはDSLで定義されています。" },
17
+ description: "挨拶メッセージを返す"
18
+ )
19
+
20
+ # stop コマンド
21
+ commands[:stop] = DslCommand.new(
22
+ name: "stop",
23
+ ruby_block: lambda {
24
+ sleep 5
25
+ "done"
26
+ },
27
+ description: "5秒待機してdoneを返す"
28
+ )
29
+
30
+ commands
31
+ end
32
+ end
33
+ end
34
+ end
@@ -3,14 +3,16 @@
3
3
  require 'open3'
4
4
 
5
5
  module Rufio
6
- # コマンドモード - プラグインコマンドを実行するためのインターフェース
6
+ # コマンドモード - DSLコマンドを実行するための統一インターフェース
7
+ # すべてのコマンドはDslCommandとして扱われる
7
8
  class CommandMode
8
9
  attr_accessor :background_executor
9
10
 
10
11
  def initialize(background_executor = nil)
11
12
  @commands = {}
12
13
  @background_executor = background_executor
13
- load_plugin_commands
14
+ load_builtin_commands
15
+ load_dsl_commands
14
16
  end
15
17
 
16
18
  # コマンドを実行する
@@ -38,18 +40,14 @@ module Rufio
38
40
  # コマンド名を取得 (前後の空白を削除)
39
41
  command_name = command_string.strip.to_sym
40
42
 
41
- # コマンドが存在するかチェック
42
- unless @commands.key?(command_name)
43
+ # 統一されたコマンドストアから検索
44
+ command = @commands[command_name]
45
+ unless command
43
46
  return "⚠️ コマンドが見つかりません: #{command_name}"
44
47
  end
45
48
 
46
- # コマンドを実行
47
- begin
48
- command_method = @commands[command_name][:method]
49
- command_method.call
50
- rescue StandardError => e
51
- "⚠️ コマンド実行エラー: #{e.message}"
52
- end
49
+ # 統一された実行パス
50
+ execute_unified_command(command_name, command)
53
51
  end
54
52
 
55
53
  # 利用可能なコマンドのリストを取得
@@ -59,17 +57,72 @@ module Rufio
59
57
 
60
58
  # コマンドの情報を取得
61
59
  def command_info(command_name)
62
- return nil unless @commands.key?(command_name)
60
+ command = @commands[command_name]
61
+ return nil unless command
63
62
 
64
63
  {
65
64
  name: command_name,
66
- plugin: @commands[command_name][:plugin],
67
- description: @commands[command_name][:description]
65
+ plugin: command[:source] || "dsl",
66
+ description: command[:command].description
68
67
  }
69
68
  end
70
69
 
70
+ # DSLコマンドをロードする
71
+ # @param paths [Array<String>, nil] 設定ファイルのパス配列(nilの場合はデフォルトパス)
72
+ def load_dsl_commands(paths = nil)
73
+ loader = DslCommandLoader.new
74
+
75
+ commands = if paths
76
+ loader.load_from_paths(paths)
77
+ else
78
+ loader.load
79
+ end
80
+
81
+ # ユーザーDSLコマンドは既存のコマンドを上書きする(優先度が高い)
82
+ commands.each do |cmd|
83
+ @commands[cmd.name.to_sym] = {
84
+ command: cmd,
85
+ source: "dsl"
86
+ }
87
+ end
88
+ end
89
+
71
90
  private
72
91
 
92
+ # 組み込みコマンドをロードする
93
+ def load_builtin_commands
94
+ builtin = BuiltinCommands.load
95
+ builtin.each do |name, cmd|
96
+ @commands[name] = {
97
+ command: cmd,
98
+ source: "builtin"
99
+ }
100
+ end
101
+ end
102
+
103
+ # 統一されたコマンド実行
104
+ # @param command_name [Symbol] コマンド名
105
+ # @param command [Hash] コマンド情報 { command: DslCommand, source: String }
106
+ # @return [Hash] 実行結果
107
+ def execute_unified_command(command_name, command)
108
+ dsl_cmd = command[:command]
109
+
110
+ # バックグラウンドエグゼキュータが利用可能な場合は非同期実行
111
+ if @background_executor
112
+ command_display_name = command_name.to_s
113
+ if @background_executor.execute_ruby_async(command_display_name) do
114
+ ScriptExecutor.execute_command(dsl_cmd)
115
+ end
116
+ return "🔄 バックグラウンドで実行中: #{command_display_name}"
117
+ else
118
+ return "⚠️ 既にコマンドが実行中です"
119
+ end
120
+ end
121
+
122
+ # 同期実行
123
+ ScriptExecutor.execute_command(dsl_cmd)
124
+ end
125
+
73
126
  # シェルコマンドを実行する
74
127
  def execute_shell_command(shell_command)
75
128
  # コマンドが空の場合
@@ -97,26 +150,5 @@ module Rufio
97
150
  { success: false, error: "コマンド実行エラー: #{e.message}" }
98
151
  end
99
152
  end
100
-
101
- # プラグインからコマンドを読み込む
102
- def load_plugin_commands
103
- # 有効なプラグインを取得
104
- enabled_plugins = PluginManager.enabled_plugins
105
-
106
- # 各プラグインからコマンドを取得
107
- enabled_plugins.each do |plugin|
108
- plugin_name = plugin.name
109
- plugin_commands = plugin.commands
110
-
111
- # 各コマンドを登録
112
- plugin_commands.each do |command_name, command_method|
113
- @commands[command_name] = {
114
- method: command_method,
115
- plugin: plugin_name,
116
- description: plugin.description
117
- }
118
- end
119
- end
120
- end
121
153
  end
122
154
  end
@@ -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
@@ -67,12 +67,20 @@ module Rufio
67
67
  file.each_line.with_index do |line, index|
68
68
  break if index >= max_lines
69
69
 
70
- # truncate too long lines
71
- if line.length > MAX_LINE_LENGTH
72
- line = line[0...MAX_LINE_LENGTH] + "..."
73
- end
70
+ begin
71
+ # Ensure line is properly encoded as UTF-8
72
+ line = line.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?')
73
+
74
+ # truncate too long lines
75
+ if line.length > MAX_LINE_LENGTH
76
+ line = line[0...MAX_LINE_LENGTH] + "..."
77
+ end
74
78
 
75
- lines << line.chomp
79
+ lines << line.chomp
80
+ rescue EncodingError, ArgumentError => e
81
+ # If encoding fails, add placeholder
82
+ lines << "[encoding error in line #{index + 1}]"
83
+ end
76
84
  end
77
85
 
78
86
  # check if there are more lines to read
@@ -91,7 +99,15 @@ module Rufio
91
99
  File.open(file_path, "r:Shift_JIS:UTF-8", invalid: :replace, undef: :replace, replace: '�') do |file|
92
100
  file.each_line.with_index do |line, index|
93
101
  break if index >= max_lines
94
- lines << line.chomp
102
+
103
+ begin
104
+ # Ensure line is properly encoded as UTF-8
105
+ line = line.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?')
106
+ lines << line.chomp
107
+ rescue EncodingError, ArgumentError => e
108
+ # If encoding fails, add placeholder
109
+ lines << "[encoding error in line #{index + 1}]"
110
+ end
95
111
  end
96
112
  truncated = !file.eof?
97
113
  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