kdeploy 1.1.9 → 1.2.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.
data/exe/kdeploy CHANGED
@@ -2,17 +2,17 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  # 自动修复常见 gem 扩展
5
- %w[debug rbs].zip(%w[1.7.1 2.8.2]).each do |gem_name, version|
6
- require gem_name
7
- rescue LoadError
8
- warn "[Kdeploy] 自动修复 #{gem_name}-#{version} ..."
9
- system("gem pristine #{gem_name} --version #{version}")
10
- begin
11
- require gem_name
12
- rescue LoadError
13
- warn "[Kdeploy] 依然无法加载 #{gem_name}-#{version},请手动修复。"
14
- end
15
- end
5
+ # %w[debug rbs].zip(%w[1.7.1 2.8.2]).each do |gem_name, version|
6
+ # require gem_name
7
+ # rescue LoadError
8
+ # warn "[Kdeploy] 自动修复 #{gem_name}-#{version} ..."
9
+ # system("gem pristine #{gem_name} --version #{version}")
10
+ # begin
11
+ # require gem_name
12
+ # rescue LoadError
13
+ # warn "[Kdeploy] 依然无法加载 #{gem_name}-#{version},请手动修复。"
14
+ # end
15
+ # end
16
16
 
17
17
  require 'kdeploy'
18
18
 
@@ -7,7 +7,7 @@ module Kdeploy
7
7
  class << self
8
8
  def show
9
9
  pastel = Pastel.new
10
- <<~BANNER.freeze
10
+ <<~BANNER
11
11
  #{pastel.bright_blue(ASCII_LOGO)}
12
12
 
13
13
  #{pastel.bright_yellow('⚡')} #{pastel.bright_white('Lightweight Agentless Deployment Tool')}
data/lib/kdeploy/cli.rb CHANGED
@@ -108,7 +108,8 @@ module Kdeploy
108
108
  next
109
109
  end
110
110
 
111
- runner = Runner.new(hosts, self.class.kdeploy_tasks, parallel: options[:parallel])
111
+ output = ConsoleOutput.new
112
+ runner = Runner.new(hosts, self.class.kdeploy_tasks, parallel: options[:parallel], output: output)
112
113
  results = runner.run(task)
113
114
  print_results(results, task)
114
115
  end
@@ -127,6 +128,10 @@ module Kdeploy
127
128
 
128
129
  # 用 instance_eval 并传递顶层 binding,兼容 heredoc
129
130
  self.class.module_eval(File.read(file), file)
131
+ rescue StandardError => e
132
+ raise FileNotFoundError, file if e.message.include?('not found')
133
+
134
+ raise
130
135
  end
131
136
 
132
137
  def filter_hosts(limit, task_hosts)
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kdeploy
4
+ # Executes a single command and records execution time
5
+ class CommandExecutor
6
+ def initialize(executor, output)
7
+ @executor = executor
8
+ @output = output
9
+ end
10
+
11
+ def execute_run(command, host_name)
12
+ show_command_header(host_name, :run, command[:command])
13
+ result, duration = measure_time do
14
+ @executor.execute(command[:command])
15
+ end
16
+ show_command_output(result)
17
+ { command: command[:command], output: result, duration: duration, type: :run }
18
+ end
19
+
20
+ def execute_upload(command, host_name)
21
+ show_command_header(host_name, :upload, "#{command[:source]} -> #{command[:destination]}")
22
+ _result, duration = measure_time do
23
+ @executor.upload(command[:source], command[:destination])
24
+ end
25
+ {
26
+ command: "upload: #{command[:source]} -> #{command[:destination]}",
27
+ duration: duration,
28
+ type: :upload
29
+ }
30
+ end
31
+
32
+ def execute_upload_template(command, host_name)
33
+ show_command_header(host_name, :upload_template, "#{command[:source]} -> #{command[:destination]}")
34
+ _result, duration = measure_time do
35
+ @executor.upload_template(command[:source], command[:destination], command[:variables])
36
+ end
37
+ {
38
+ command: "upload_template: #{command[:source]} -> #{command[:destination]}",
39
+ duration: duration,
40
+ type: :upload_template
41
+ }
42
+ end
43
+
44
+ private
45
+
46
+ def measure_time
47
+ start_time = Time.now
48
+ result = yield
49
+ duration = Time.now - start_time
50
+ [result, duration]
51
+ end
52
+
53
+ def show_command_output(output)
54
+ return unless output.is_a?(Hash)
55
+
56
+ pastel = @output.respond_to?(:pastel) ? @output.pastel : Pastel.new
57
+
58
+ if output[:stdout] && !output[:stdout].empty?
59
+ output[:stdout].each_line do |line|
60
+ @output.write_line(" #{line.rstrip}") unless line.strip.empty?
61
+ end
62
+ end
63
+
64
+ return unless output[:stderr] && !output[:stderr].empty?
65
+
66
+ output[:stderr].each_line do |line|
67
+ @output.write_line(pastel.green(" #{line.rstrip}")) unless line.strip.empty?
68
+ end
69
+ end
70
+
71
+ def show_command_header(host_name, type, description)
72
+ pastel = @output.respond_to?(:pastel) ? @output.pastel : Pastel.new
73
+ @output.write_line(pastel.bright_white("\n#{host_name.ljust(24)} : "))
74
+
75
+ case type
76
+ when :run
77
+ @output.write_line(pastel.cyan(" [run] #{description.lines.first.strip}"))
78
+ description.lines[1..].each do |line|
79
+ @output.write_line(" > #{line.strip}") unless line.strip.empty?
80
+ end
81
+ when :upload
82
+ @output.write_line(pastel.green(" [upload] #{description}"))
83
+ when :upload_template
84
+ @output.write_line(pastel.yellow(" [template] #{description}"))
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kdeploy
4
+ # Groups commands by type and generates task descriptions
5
+ class CommandGrouper
6
+ def self.group(commands)
7
+ commands.group_by do |cmd|
8
+ group_key_for(cmd)
9
+ end
10
+ end
11
+
12
+ def self.group_key_for(cmd)
13
+ case cmd[:type]
14
+ when :upload
15
+ "#{cmd[:type]}_#{cmd[:source]}"
16
+ when :run
17
+ "#{cmd[:type]}_#{cmd[:command].to_s.lines.first.strip}"
18
+ when :upload_template
19
+ "#{cmd[:type]}_#{cmd[:source]}"
20
+ else
21
+ cmd[:type].to_s
22
+ end
23
+ end
24
+
25
+ def self.task_description(command)
26
+ case command[:type]
27
+ when :upload
28
+ "upload #{command[:source]}"
29
+ when :upload_template
30
+ "template #{command[:source]}"
31
+ when :run
32
+ command[:command].to_s.lines.first.strip
33
+ else
34
+ command[:type].to_s
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kdeploy
4
+ # Configuration management for Kdeploy
5
+ class Configuration
6
+ DEFAULT_PARALLEL = 10
7
+ DEFAULT_SSH_TIMEOUT = 30
8
+ DEFAULT_VERIFY_HOST_KEY = :never
9
+
10
+ class << self
11
+ attr_accessor :default_parallel,
12
+ :default_ssh_timeout,
13
+ :default_verify_host_key
14
+
15
+ def reset
16
+ @default_parallel = DEFAULT_PARALLEL
17
+ @default_ssh_timeout = DEFAULT_SSH_TIMEOUT
18
+ @default_verify_host_key = DEFAULT_VERIFY_HOST_KEY
19
+ end
20
+ end
21
+
22
+ # Initialize with defaults
23
+ reset
24
+ end
25
+ end
data/lib/kdeploy/dsl.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'set'
4
+
3
5
  module Kdeploy
4
6
  module DSL
5
7
  def self.extended(base)
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kdeploy
4
+ # Base error class for all Kdeploy errors
5
+ class Error < StandardError; end
6
+
7
+ # Raised when a task is not found
8
+ class TaskNotFoundError < Error
9
+ def initialize(task_name)
10
+ super("Task not found: #{task_name}")
11
+ end
12
+ end
13
+
14
+ # Raised when a host is not found
15
+ class HostNotFoundError < Error
16
+ def initialize(host_name)
17
+ super("Host not found: #{host_name}")
18
+ end
19
+ end
20
+
21
+ # Raised when SSH operation fails
22
+ class SSHError < Error
23
+ def initialize(message, original_error = nil)
24
+ super("SSH operation failed: #{message}")
25
+ @original_error = original_error
26
+ end
27
+
28
+ attr_reader :original_error
29
+ end
30
+
31
+ # Raised when SCP operation fails
32
+ class SCPError < Error
33
+ def initialize(message, original_error = nil)
34
+ super("SCP upload failed: #{message}")
35
+ @original_error = original_error
36
+ end
37
+
38
+ attr_reader :original_error
39
+ end
40
+
41
+ # Raised when template operation fails
42
+ class TemplateError < Error
43
+ def initialize(message, original_error = nil)
44
+ super("Template operation failed: #{message}")
45
+ @original_error = original_error
46
+ end
47
+
48
+ attr_reader :original_error
49
+ end
50
+
51
+ # Raised when configuration is invalid
52
+ class ConfigurationError < Error
53
+ def initialize(message)
54
+ super("Configuration error: #{message}")
55
+ end
56
+ end
57
+
58
+ # Raised when file operation fails
59
+ class FileNotFoundError < Error
60
+ def initialize(file_path)
61
+ super("File not found: #{file_path}")
62
+ end
63
+ end
64
+ end
@@ -21,7 +21,7 @@ module Kdeploy
21
21
 
22
22
  ssh.open_channel do |channel|
23
23
  channel.exec(command) do |_ch, success|
24
- raise "Could not execute command: #{command}" unless success
24
+ raise SSHError, "Could not execute command: #{command}" unless success
25
25
 
26
26
  channel.on_data do |_ch, data|
27
27
  stdout << data
@@ -40,9 +40,9 @@ module Kdeploy
40
40
  }
41
41
  end
42
42
  rescue Net::SSH::AuthenticationFailed => e
43
- raise "SSH authentication failed: #{e.message}"
43
+ raise SSHError.new("SSH authentication failed: #{e.message}", e)
44
44
  rescue StandardError => e
45
- raise "SSH execution failed: #{e.message}"
45
+ raise SSHError.new("SSH execution failed: #{e.message}", e)
46
46
  end
47
47
 
48
48
  def upload(source, destination)
@@ -50,13 +50,13 @@ module Kdeploy
50
50
  scp.upload!(source, destination)
51
51
  end
52
52
  rescue StandardError => e
53
- raise "SCP upload failed: #{e.message}"
53
+ raise SCPError.new("SCP upload failed: #{e.message}", e)
54
54
  end
55
55
 
56
56
  def upload_template(source, destination, variables = {})
57
57
  Template.render_and_upload(self, source, destination, variables)
58
58
  rescue StandardError => e
59
- raise "Template upload failed: #{e.message}"
59
+ raise TemplateError.new("Template upload failed: #{e.message}", e)
60
60
  end
61
61
 
62
62
  private
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pastel'
4
+
5
+ module Kdeploy
6
+ # Abstract output interface
7
+ class Output
8
+ def write(message)
9
+ raise NotImplementedError, 'Subclasses must implement write'
10
+ end
11
+
12
+ def write_line(message)
13
+ write("#{message}\n")
14
+ end
15
+
16
+ def write_error(message)
17
+ write_line(message)
18
+ end
19
+ end
20
+
21
+ # Console output implementation
22
+ class ConsoleOutput < Output
23
+ def initialize
24
+ @pastel = Pastel.new
25
+ end
26
+
27
+ def write(message)
28
+ print(message)
29
+ end
30
+
31
+ def write_line(message)
32
+ puts(message)
33
+ end
34
+
35
+ def write_error(message)
36
+ puts(@pastel.red(message))
37
+ end
38
+
39
+ attr_reader :pastel
40
+ end
41
+
42
+ # Silent output for testing
43
+ class SilentOutput < Output
44
+ attr_reader :messages, :errors
45
+
46
+ def initialize
47
+ @messages = []
48
+ @errors = []
49
+ end
50
+
51
+ def write(message)
52
+ @messages << message
53
+ end
54
+
55
+ def write_line(message)
56
+ @messages << "#{message}\n"
57
+ end
58
+
59
+ def write_error(message)
60
+ @errors << message
61
+ @messages << "#{message}\n"
62
+ end
63
+
64
+ def clear
65
+ @messages.clear
66
+ @errors.clear
67
+ end
68
+ end
69
+ end
@@ -4,89 +4,22 @@ require 'concurrent'
4
4
 
5
5
  module Kdeploy
6
6
  class Runner
7
- def initialize(hosts, tasks, parallel: 10)
7
+ def initialize(hosts, tasks, parallel: Configuration.default_parallel, output: ConsoleOutput.new)
8
8
  @hosts = hosts
9
9
  @tasks = tasks
10
10
  @parallel = parallel
11
+ @output = output
11
12
  @pool = Concurrent::FixedThreadPool.new(@parallel)
12
13
  @results = Concurrent::Hash.new
13
14
  end
14
15
 
15
16
  def run(task_name)
16
17
  task = @tasks[task_name]
17
- raise "Task not found: #{task_name}" unless task
18
+ raise TaskNotFoundError, task_name unless task
18
19
 
19
20
  futures = @hosts.map do |name, config|
20
21
  Concurrent::Future.execute(executor: @pool) do
21
- executor = Executor.new(config)
22
- result = { status: :success, output: [] }
23
-
24
- task[:block].call.group_by do |cmd|
25
- cmd[:type].to_s + (if cmd[:type] == :upload
26
- cmd[:source].to_s
27
- else
28
- cmd[:type] == :run ? cmd[:command].to_s.lines.first.strip : ''
29
- end)
30
- end.each_value do |commands|
31
- # 生成 TASK 横幅
32
- pastel = Pastel.new
33
- first_cmd = commands.first
34
- task_desc = case first_cmd[:type]
35
- when :upload
36
- "upload #{first_cmd[:source]}"
37
- when :upload_template
38
- "template #{first_cmd[:source]}"
39
- when :run
40
- first_cmd[:command].to_s.lines.first.strip
41
- else
42
- first_cmd[:type].to_s
43
- end
44
- puts pastel.cyan("\nTASK [#{task_desc}] " + ('*' * 64))
45
- commands.each do |command|
46
- case command[:type]
47
- when :run
48
- pastel = Pastel.new
49
- puts pastel.bright_white("\n#{name.ljust(24)} : ")
50
- puts pastel.cyan(" [run] #{command[:command].lines.first.strip}")
51
- command[:command].lines[1..].each { |line| puts " > #{line.strip}" unless line.strip.empty? }
52
- t1 = Time.now
53
- output = executor.execute(command[:command])
54
- t2 = Time.now
55
- duration = t2 - t1
56
- # 统一输出命令结果
57
- if output[:stdout] && !output[:stdout].empty?
58
- output[:stdout].each_line { |line| puts " #{line.rstrip}" unless line.strip.empty? }
59
- end
60
- if output[:stderr] && !output[:stderr].empty?
61
- output[:stderr].each_line { |line| puts pastel.green(" #{line.rstrip}") unless line.strip.empty? }
62
- end
63
- result[:output] << { command: command[:command], output: output, duration: duration, type: :run }
64
- when :upload
65
- pastel = Pastel.new
66
- puts pastel.bright_white("\n#{name.ljust(24)} : ")
67
- puts pastel.green(" [upload] #{command[:source]} -> #{command[:destination]}")
68
- t1 = Time.now
69
- executor.upload(command[:source], command[:destination])
70
- t2 = Time.now
71
- duration = t2 - t1
72
- result[:output] << { command: "upload: #{command[:source]} -> #{command[:destination]}", duration: duration, type: :upload }
73
- when :upload_template
74
- pastel = Pastel.new
75
- puts pastel.bright_white("\n#{name.ljust(24)} : ")
76
- puts pastel.yellow(" [template] #{command[:source]} -> #{command[:destination]}")
77
- t1 = Time.now
78
- executor.upload_template(command[:source], command[:destination], command[:vars])
79
- t2 = Time.now
80
- duration = t2 - t1
81
- result[:output] << { command: "upload_template: #{command[:source]} -> #{command[:destination]}", duration: duration,
82
- type: :upload_template }
83
- end
84
- end
85
- end
86
-
87
- @results[name] = result
88
- rescue StandardError => e
89
- @results[name] = { status: :failed, error: e.message }
22
+ execute_task_for_host(name, config, task)
90
23
  end
91
24
  end
92
25
 
@@ -95,5 +28,49 @@ module Kdeploy
95
28
  ensure
96
29
  @pool.shutdown
97
30
  end
31
+
32
+ private
33
+
34
+ def execute_task_for_host(name, config, task)
35
+ executor = Executor.new(config)
36
+ command_executor = CommandExecutor.new(executor, @output)
37
+ result = { status: :success, output: [] }
38
+
39
+ commands = task[:block].call
40
+ grouped_commands = CommandGrouper.group(commands)
41
+
42
+ grouped_commands.each_value do |command_group|
43
+ first_cmd = command_group.first
44
+ task_desc = CommandGrouper.task_description(first_cmd)
45
+ show_task_header(task_desc)
46
+
47
+ command_group.each do |command|
48
+ step_result = execute_command(command_executor, command, name)
49
+ result[:output] << step_result
50
+ end
51
+ end
52
+
53
+ @results[name] = result
54
+ rescue StandardError => e
55
+ @results[name] = { status: :failed, error: e.message }
56
+ end
57
+
58
+ def execute_command(command_executor, command, host_name)
59
+ case command[:type]
60
+ when :run
61
+ command_executor.execute_run(command, host_name)
62
+ when :upload
63
+ command_executor.execute_upload(command, host_name)
64
+ when :upload_template
65
+ command_executor.execute_upload_template(command, host_name)
66
+ else
67
+ raise ConfigurationError, "Unknown command type: #{command[:type]}"
68
+ end
69
+ end
70
+
71
+ def show_task_header(task_desc)
72
+ pastel = @output.respond_to?(:pastel) ? @output.pastel : Pastel.new
73
+ @output.write_line(pastel.cyan("\nTASK [#{task_desc}] " + ('*' * 64)))
74
+ end
98
75
  end
99
76
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kdeploy
4
- VERSION = '1.1.9'
4
+ VERSION = '1.2.0'
5
5
  end
data/lib/kdeploy.rb CHANGED
@@ -1,9 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'kdeploy/version'
4
+ require_relative 'kdeploy/errors'
5
+ require_relative 'kdeploy/configuration'
6
+ require_relative 'kdeploy/output'
4
7
  require_relative 'kdeploy/banner'
5
8
  require_relative 'kdeploy/dsl'
6
9
  require_relative 'kdeploy/executor'
10
+ require_relative 'kdeploy/command_grouper'
11
+ require_relative 'kdeploy/command_executor'
7
12
  require_relative 'kdeploy/runner'
8
13
  require_relative 'kdeploy/initializer'
9
14
  require_relative 'kdeploy/template'
@@ -11,6 +16,5 @@ require_relative 'kdeploy/post_install'
11
16
  require_relative 'kdeploy/cli'
12
17
 
13
18
  module Kdeploy
14
- class Error < StandardError; end
15
19
  # Your code goes here...
16
20
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kdeploy
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.9
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kk
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-07-15 00:00:00.000000000 Z
11
+ date: 2025-11-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -180,11 +180,16 @@ files:
180
180
  - lib/kdeploy.rb
181
181
  - lib/kdeploy/banner.rb
182
182
  - lib/kdeploy/cli.rb
183
+ - lib/kdeploy/command_executor.rb
184
+ - lib/kdeploy/command_grouper.rb
183
185
  - lib/kdeploy/completions/kdeploy.bash
184
186
  - lib/kdeploy/completions/kdeploy.zsh
187
+ - lib/kdeploy/configuration.rb
185
188
  - lib/kdeploy/dsl.rb
189
+ - lib/kdeploy/errors.rb
186
190
  - lib/kdeploy/executor.rb
187
191
  - lib/kdeploy/initializer.rb
192
+ - lib/kdeploy/output.rb
188
193
  - lib/kdeploy/post_install.rb
189
194
  - lib/kdeploy/runner.rb
190
195
  - lib/kdeploy/template.rb