kdeploy 1.2.12 → 1.2.14

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7619f068ed4e5681529a7f1a06574d116e7e1552f0f36742f44e4994610bde51
4
- data.tar.gz: c388f8b7cd2e3833757f440ff535ec40d9baa7c5c3037bda7e2c418f288d0528
3
+ metadata.gz: 72ea750e6176a809ebf7e45a9b4568477bad90a81682ef49491f976f0484cad2
4
+ data.tar.gz: 54d0f9bca80bb59b6598152d84cbafb9c7b3f7aab21de1090e9de75bf1dce081
5
5
  SHA512:
6
- metadata.gz: b8dc29c50ce326bfd7fb2e41e983432a8219140e23256f2521ce29d20f8ae28681193dc31ce2f04ddb3c93881434a9c43ff0f20439dae200800989527a26cbcb
7
- data.tar.gz: 9d442d78aeacaa8c3577a98b66dd493790abbe88c6ca400b637ff3a938f25fdc47b50c736296d3d82026ac76045602ad76d70a8804d7d051d369f2703e6a8b01
6
+ metadata.gz: 150d44235775b15b9d23458a162c4f0f60668f07c6a6bdc4a40a64f973e5bfb5ceee0ce31045db6b6d3d1803540d3df5928792eade256f765aec8d7c38739bfb
7
+ data.tar.gz: 1f31cf01e39d1b43d7fbf0df12f764c870850be60b1da21642f7afba6eb1553f7fe911431485eb0ecf002d3b725725a125d8227a96739cd6dbb73cbdaa0941b4
@@ -28,22 +28,38 @@ module Kdeploy
28
28
  VERSION
29
29
  end
30
30
 
31
- def show_error(message)
31
+ def show_error(message, include_banner: false)
32
32
  pastel = Pastel.new
33
- <<~ERROR
34
- #{show}
35
- #{pastel.red("Error: #{message}")}
33
+ error_msg = pastel.red("Error: #{message}")
34
+ if include_banner
35
+ <<~ERROR
36
+ #{show}
37
+ #{error_msg}
38
+
39
+ ERROR
40
+ else
41
+ <<~ERROR
42
+ #{error_msg}
36
43
 
37
- ERROR
44
+ ERROR
45
+ end
38
46
  end
39
47
 
40
- def show_success(message)
48
+ def show_success(message, include_banner: false)
41
49
  pastel = Pastel.new
42
- <<~SUCCESS
43
- #{show}
44
- #{pastel.green("Success: #{message}")}
50
+ success_msg = pastel.green("Success: #{message}")
51
+ if include_banner
52
+ <<~SUCCESS
53
+ #{show}
54
+ #{success_msg}
55
+
56
+ SUCCESS
57
+ else
58
+ <<~SUCCESS
59
+ #{success_msg}
45
60
 
46
- SUCCESS
61
+ SUCCESS
62
+ end
47
63
  end
48
64
 
49
65
  ASCII_LOGO = <<~'LOGO'
data/lib/kdeploy/cli.rb CHANGED
@@ -45,9 +45,11 @@ module Kdeploy
45
45
  method_option :limit, type: :string, desc: 'Limit to specific hosts (comma-separated)'
46
46
  method_option :parallel, type: :numeric, default: 10, desc: 'Number of parallel executions'
47
47
  method_option :dry_run, type: :boolean, desc: 'Show what would be done'
48
+ method_option :debug, type: :boolean, desc: 'Show detailed command output (stdout/stderr)'
48
49
  def execute(task_file, task_name = nil)
49
50
  load_config_file
50
51
  show_banner_once
52
+ @task_file_dir = File.dirname(File.expand_path(task_file))
51
53
  load_task_file(task_file)
52
54
 
53
55
  tasks_to_run = determine_tasks(task_name)
@@ -96,7 +98,7 @@ module Kdeploy
96
98
 
97
99
  def print_dry_run(hosts, task_name)
98
100
  formatter = OutputFormatter.new
99
- puts Kdeploy::Banner.show
101
+ # Banner already shown by show_banner_once, don't show again
100
102
  puts formatter.format_dry_run_header
101
103
  puts
102
104
 
@@ -113,8 +115,8 @@ module Kdeploy
113
115
  puts
114
116
  end
115
117
 
116
- def print_results(results, task_name)
117
- formatter = OutputFormatter.new
118
+ def print_results(results, task_name, show_summary: false, debug: false)
119
+ formatter = OutputFormatter.new(debug: debug)
118
120
  puts formatter.format_task_header(task_name)
119
121
 
120
122
  if results.empty?
@@ -128,7 +130,8 @@ module Kdeploy
128
130
  print_host_result(host, result, formatter)
129
131
  end
130
132
 
131
- print_summary(results, formatter)
133
+ # Only show summary if explicitly requested (for single task execution)
134
+ print_summary(results, formatter) if show_summary
132
135
  end
133
136
 
134
137
  def print_host_result(_host, result, formatter)
@@ -194,9 +197,16 @@ module Kdeploy
194
197
  end
195
198
 
196
199
  def execute_tasks(tasks_to_run)
200
+ all_results = {}
201
+
197
202
  tasks_to_run.each do |task|
198
- execute_single_task(task)
203
+ task_results = execute_single_task(task)
204
+ # Collect results for final summary
205
+ all_results[task] = task_results if task_results
199
206
  end
207
+
208
+ # Show combined summary at the end for all tasks
209
+ print_all_tasks_summary(all_results) unless all_results.empty?
200
210
  end
201
211
 
202
212
  def execute_single_task(task)
@@ -205,12 +215,12 @@ module Kdeploy
205
215
 
206
216
  if hosts.empty?
207
217
  puts Kdeploy::Banner.show_error("No hosts found for task: #{task}")
208
- return
218
+ return nil
209
219
  end
210
220
 
211
221
  if options[:dry_run]
212
222
  print_dry_run(hosts, task)
213
- return
223
+ return nil
214
224
  end
215
225
 
216
226
  run_task(hosts, task)
@@ -219,9 +229,40 @@ module Kdeploy
219
229
  def run_task(hosts, task)
220
230
  output = ConsoleOutput.new
221
231
  parallel_count = options[:parallel] || Configuration.default_parallel
222
- runner = Runner.new(hosts, self.class.kdeploy_tasks, parallel: parallel_count, output: output)
232
+ debug_mode = options[:debug] || false
233
+ base_dir = @task_file_dir
234
+ runner = Runner.new(hosts, self.class.kdeploy_tasks, parallel: parallel_count, output: output, debug: debug_mode,
235
+ base_dir: base_dir)
223
236
  results = runner.run(task)
224
- print_results(results, task)
237
+ # Don't show summary here - it will be shown at the end for all tasks
238
+ print_results(results, task, show_summary: false, debug: debug_mode)
239
+ results
240
+ end
241
+
242
+ def print_all_tasks_summary(all_results)
243
+ debug_mode = options[:debug] || false
244
+ formatter = OutputFormatter.new(debug: debug_mode)
245
+ puts formatter.format_summary_header
246
+
247
+ # Collect all hosts from all tasks
248
+ all_hosts = {}
249
+ all_results.each do |task_name, task_results|
250
+ task_results.each do |host, result|
251
+ all_hosts[host] ||= { ok: 0, changed: 0, failed: 0, tasks: [] }
252
+ counts = formatter.calculate_summary_counts(result)
253
+ all_hosts[host][:ok] += counts[:ok]
254
+ all_hosts[host][:changed] += counts[:changed]
255
+ all_hosts[host][:failed] += counts[:failed]
256
+ all_hosts[host][:tasks] << task_name
257
+ end
258
+ end
259
+
260
+ max_host_len = all_hosts.keys.map(&:length).max || 16
261
+ all_hosts.keys.sort.each do |host|
262
+ counts = all_hosts[host]
263
+ line = formatter.build_summary_line(host, counts, max_host_len)
264
+ puts formatter.colorize_summary_line(line, counts)
265
+ end
225
266
  end
226
267
 
227
268
  def extract_error_message(result)
@@ -3,9 +3,10 @@
3
3
  module Kdeploy
4
4
  # Executes a single command and records execution time
5
5
  class CommandExecutor
6
- def initialize(executor, output)
6
+ def initialize(executor, output, debug: false)
7
7
  @executor = executor
8
8
  @output = output
9
+ @debug = debug
9
10
  end
10
11
 
11
12
  def execute_run(command, host_name)
@@ -15,7 +16,6 @@ module Kdeploy
15
16
 
16
17
  # Show progress indicator for long-running commands
17
18
  pastel = @output.respond_to?(:pastel) ? @output.pastel : Pastel.new
18
- Time.now
19
19
 
20
20
  result, duration = measure_time do
21
21
  @executor.execute(cmd, use_sudo: use_sudo)
@@ -24,8 +24,8 @@ module Kdeploy
24
24
  # Show execution time if command took more than 1 second
25
25
  @output.write_line(pastel.dim(" [completed in #{format('%.2f', duration)}s]")) if duration > 1.0
26
26
 
27
- # Show command output
28
- show_command_output(result)
27
+ # Show command output only in debug mode
28
+ show_command_output(result) if @debug
29
29
  { command: cmd, output: result, duration: duration, type: :run }
30
30
  end
31
31
 
@@ -87,9 +87,8 @@ module Kdeploy
87
87
  end
88
88
 
89
89
  def show_command_header(host_name, type, description)
90
- pastel = pastel_instance
91
- @output.write_line(pastel.bright_white("\n#{host_name.ljust(24)} : "))
92
- format_command_by_type(type, description, pastel)
90
+ # Don't show command header during execution - it will be shown in results
91
+ # This reduces noise during execution
93
92
  end
94
93
 
95
94
  def pastel_instance
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'net/ssh'
4
4
  require 'net/scp'
5
+ require 'pathname'
5
6
 
6
7
  module Kdeploy
7
8
  # SSH/SCP executor for remote command execution and file operations
@@ -15,6 +16,7 @@ module Kdeploy
15
16
  @port = host_config[:port] # 新增端口支持
16
17
  @use_sudo = host_config[:use_sudo] || false
17
18
  @sudo_password = host_config[:sudo_password]
19
+ @base_dir = host_config[:base_dir] # Base directory for resolving relative paths
18
20
  end
19
21
 
20
22
  def execute(command, use_sudo: nil)
@@ -63,22 +65,75 @@ module Kdeploy
63
65
  }
64
66
  end
65
67
 
66
- def upload(source, destination)
67
- Net::SCP.start(@ip, @user, ssh_options) do |scp|
68
- scp.upload!(source, destination)
68
+ def upload(source, destination, use_sudo: nil)
69
+ use_sudo = @use_sudo if use_sudo.nil?
70
+
71
+ # Resolve relative paths relative to base_dir
72
+ resolved_source = resolve_path(source)
73
+
74
+ # If destination requires sudo, upload to temp location first, then move with sudo
75
+ if use_sudo || requires_sudo?(destination)
76
+ upload_with_sudo(resolved_source, destination)
77
+ else
78
+ Net::SCP.start(@ip, @user, ssh_options) do |scp|
79
+ scp.upload!(resolved_source, destination)
80
+ end
69
81
  end
70
82
  rescue StandardError => e
71
83
  raise SCPError.new("SCP upload failed: #{e.message}", e)
72
84
  end
73
85
 
74
86
  def upload_template(source, destination, variables = {})
75
- Template.render_and_upload(self, source, destination, variables)
87
+ # Resolve relative paths relative to base_dir
88
+ resolved_source = resolve_path(source)
89
+ Template.render_and_upload(self, resolved_source, destination, variables)
76
90
  rescue StandardError => e
77
91
  raise TemplateError.new("Template upload failed: #{e.message}", e)
78
92
  end
79
93
 
80
94
  private
81
95
 
96
+ def upload_with_sudo(source, destination)
97
+ # Generate a unique temp file name
98
+ temp_dest = "/tmp/kdeploy_#{File.basename(destination)}_#{Time.now.to_i}_#{rand(10_000)}"
99
+
100
+ # Upload to temp location first
101
+ Net::SCP.start(@ip, @user, ssh_options) do |scp|
102
+ scp.upload!(source, temp_dest)
103
+ end
104
+
105
+ # Move to final destination with sudo
106
+ move_command = "mv #{temp_dest} #{destination}"
107
+ execute(move_command, use_sudo: true)
108
+ rescue StandardError => e
109
+ # Try to clean up temp file if it exists
110
+ begin
111
+ execute("rm -f #{temp_dest}", use_sudo: false) if defined?(temp_dest)
112
+ rescue StandardError
113
+ # Ignore cleanup errors
114
+ end
115
+ raise SCPError.new("SCP upload failed: #{e.message}", e)
116
+ end
117
+
118
+ def resolve_path(path)
119
+ # If path is absolute, return as is
120
+ return path if Pathname.new(path).absolute?
121
+
122
+ # If base_dir is set, resolve relative to base_dir
123
+ if @base_dir
124
+ File.expand_path(path, @base_dir)
125
+ else
126
+ # Otherwise, resolve relative to current working directory
127
+ File.expand_path(path)
128
+ end
129
+ end
130
+
131
+ def requires_sudo?(path)
132
+ # Check if path is in system directories that typically require sudo
133
+ system_dirs = %w[/etc /usr /var /opt /sbin /bin /lib /lib64 /root]
134
+ system_dirs.any? { |dir| path.start_with?(dir) }
135
+ end
136
+
82
137
  def ssh_options
83
138
  options = base_ssh_options
84
139
  add_authentication(options)
@@ -109,9 +164,20 @@ module Kdeploy
109
164
  # 如果命令已经以 sudo 开头,不重复添加
110
165
  return command if command.strip.start_with?('sudo')
111
166
 
112
- if @sudo_password
113
- # 使用 echo 和管道传递密码给 sudo -S
114
- # 注意:密码会出现在进程列表中,建议使用 NOPASSWD 配置
167
+ # 对于多行命令或包含 shell 控制结构的命令,使用 bash -c 包装
168
+ is_multiline = command.include?("\n") || command.match?(/\b(if|for|while|case|function)\b/)
169
+
170
+ if is_multiline
171
+ # 转义命令中的单引号,然后用 bash -c 执行
172
+ escaped_command = command.gsub("'", "'\"'\"'")
173
+ if @sudo_password
174
+ escaped_password = @sudo_password.gsub('\'', "'\"'\"'").gsub('$', '\\$').gsub('`', '\\`')
175
+ "echo '#{escaped_password}' | sudo -S bash -c '#{escaped_command}'"
176
+ else
177
+ "sudo bash -c '#{escaped_command}'"
178
+ end
179
+ elsif @sudo_password
180
+ # 单行命令直接包装
115
181
  escaped_password = @sudo_password.gsub('\'', "'\"'\"'").gsub('$', '\\$').gsub('`', '\\`')
116
182
  "echo '#{escaped_password}' | sudo -S #{command}"
117
183
  else
@@ -6,21 +6,22 @@ require 'tty-box'
6
6
  module Kdeploy
7
7
  # Formats and displays execution results
8
8
  class OutputFormatter
9
- def initialize
9
+ def initialize(debug: false)
10
10
  @pastel = Pastel.new
11
+ @debug = debug
11
12
  end
12
13
 
13
14
  def format_task_header(task_name)
14
- @pastel.cyan("\nPLAY [#{task_name}] " + ('*' * 64))
15
+ "#{@pastel.bright_cyan("\n🚀 Task: #{task_name}")}\n#{@pastel.dim('' * 60)}"
15
16
  end
16
17
 
17
18
  def format_host_status(host, status)
18
19
  status_str = case status
19
- when :success then @pastel.green('ok')
20
- when :changed then @pastel.yellow('changed')
21
- else @pastel.red('failed')
20
+ when :success then @pastel.green('ok')
21
+ when :changed then @pastel.yellow('~ changed')
22
+ else @pastel.red('failed')
22
23
  end
23
- @pastel.bright_white("\n#{host.ljust(24)} : #{status_str}")
24
+ @pastel.bright_white(" #{host.ljust(20)} #{status_str}")
24
25
  end
25
26
 
26
27
  def format_upload_steps(steps, shown)
@@ -31,8 +32,8 @@ module Kdeploy
31
32
  format_file_steps(steps, shown, :upload_template, @pastel.yellow(' === Template ==='), 'upload_template: ')
32
33
  end
33
34
 
34
- def format_file_steps(steps, shown, type, header, prefix)
35
- output = [header]
35
+ def format_file_steps(steps, shown, type, _header, prefix)
36
+ output = []
36
37
  steps.each do |step|
37
38
  next if step_already_shown?(step, type, shown)
38
39
 
@@ -44,13 +45,16 @@ module Kdeploy
44
45
 
45
46
  def format_file_step(step, type, prefix)
46
47
  duration_str = format_duration(step[:duration])
47
- label = type == :upload ? '[upload]' : '[template]'
48
- color(" #{label} #{step[:command].sub(prefix, '')}#{duration_str}")
48
+ icon = type == :upload ? '📤' : '📝'
49
+ file_path = step[:command].sub(prefix, '')
50
+ # Truncate long paths for cleaner output
51
+ display_path = file_path.length > 50 ? "...#{file_path[-47..]}" : file_path
52
+ color_method = type == :upload ? :green : :yellow
53
+ @pastel.dim(" #{icon} ") + @pastel.send(color_method, display_path) + duration_str
49
54
  end
50
55
 
51
56
  def format_run_steps(steps, shown)
52
57
  output = []
53
- output << @pastel.cyan(' === Run ===')
54
58
  steps.each do |step|
55
59
  next if step_already_shown?(step, :run, shown)
56
60
 
@@ -64,20 +68,24 @@ module Kdeploy
64
68
  output = []
65
69
  duration_str = format_duration(step[:duration])
66
70
  command_line = step[:command].to_s.lines.first.strip
67
- output << @pastel.cyan(" [run] #{command_line}#{duration_str}")
68
- output.concat(format_multiline_command(step[:command]))
69
- # Format and add command output (stdout/stderr)
70
- cmd_output = format_command_output(step[:output])
71
- output.concat(cmd_output) if cmd_output.any?
71
+ # Truncate long commands for cleaner output
72
+ display_cmd = command_line.length > 60 ? "#{command_line[0..57]}..." : command_line
73
+ output << (@pastel.dim(' • ') + @pastel.cyan(display_cmd) + duration_str)
74
+ # Only show multiline details in debug mode
75
+ if @debug
76
+ output.concat(format_multiline_command(step[:command]))
77
+ cmd_output = format_command_output(step[:output])
78
+ output.concat(cmd_output) if cmd_output.any?
79
+ end
72
80
  output
73
81
  end
74
82
 
75
83
  def format_error(error_message)
76
- @pastel.red(" ERROR: #{error_message}")
84
+ @pastel.red("ERROR: #{error_message}")
77
85
  end
78
86
 
79
87
  def format_summary_header
80
- @pastel.cyan("\nPLAY RECAP #{'*' * 64}")
88
+ "#{@pastel.bright_cyan("\n📊 Execution Summary")}\n#{@pastel.dim('' * 60)}"
81
89
  end
82
90
 
83
91
  def format_summary_line(host, result, max_host_len)
@@ -168,24 +176,33 @@ module Kdeploy
168
176
  result = []
169
177
  return result unless output
170
178
 
171
- # Handle Hash with stdout/stderr keys
172
179
  if output.is_a?(Hash)
173
- # Check for stdout key
174
- if output.key?(:stdout)
175
- stdout = output[:stdout]
176
- format_stdout_lines(stdout, result) if stdout && !stdout.to_s.strip.empty?
177
- end
178
- # Check for stderr key
179
- if output.key?(:stderr)
180
- stderr = output[:stderr]
181
- format_stderr_lines(stderr, result) if stderr && !stderr.to_s.strip.empty?
182
- end
180
+ format_hash_output(output, result)
183
181
  elsif output.is_a?(String) && !output.strip.empty?
184
182
  format_stdout_lines(output, result)
185
183
  end
186
184
  result
187
185
  end
188
186
 
187
+ def format_hash_output(output, result)
188
+ format_stdout_from_hash(output, result)
189
+ format_stderr_from_hash(output, result)
190
+ end
191
+
192
+ def format_stdout_from_hash(output, result)
193
+ return unless output.key?(:stdout)
194
+
195
+ stdout = output[:stdout]
196
+ format_stdout_lines(stdout, result) if stdout && !stdout.to_s.strip.empty?
197
+ end
198
+
199
+ def format_stderr_from_hash(output, result)
200
+ return unless output.key?(:stderr)
201
+
202
+ stderr = output[:stderr]
203
+ format_stderr_lines(stderr, result) if stderr && !stderr.to_s.strip.empty?
204
+ end
205
+
189
206
  def format_stdout_lines(stdout, result)
190
207
  return result if stdout.nil? || stdout.to_s.empty?
191
208
 
@@ -5,18 +5,22 @@ require 'concurrent'
5
5
  module Kdeploy
6
6
  # Concurrent task runner for executing tasks across multiple hosts
7
7
  class Runner
8
- def initialize(hosts, tasks, parallel: Configuration.default_parallel, output: ConsoleOutput.new)
8
+ def initialize(hosts, tasks, parallel: Configuration.default_parallel, output: ConsoleOutput.new, debug: false,
9
+ base_dir: nil)
9
10
  @hosts = hosts
10
11
  @tasks = tasks
11
12
  @parallel = parallel
12
13
  @output = output
14
+ @debug = debug
15
+ @base_dir = base_dir
13
16
  @pool = Concurrent::FixedThreadPool.new(@parallel)
14
17
  @results = Concurrent::Hash.new
15
18
  end
16
19
 
17
20
  def run(task_name)
18
21
  task = find_task(task_name)
19
- execute_concurrent_tasks(task)
22
+ results = execute_concurrent_tasks(task)
23
+ results
20
24
  ensure
21
25
  @pool.shutdown
22
26
  end
@@ -32,12 +36,52 @@ module Kdeploy
32
36
  def execute_concurrent_tasks(task)
33
37
  futures = create_task_futures(task)
34
38
 
39
+ # If no hosts, return empty results immediately
40
+ return @results if futures.empty?
41
+
35
42
  # Show progress while waiting for tasks to complete
36
43
  total = futures.length
37
44
  completed = 0
38
45
 
39
- futures.each do |future|
40
- future.wait
46
+ # Collect results from futures
47
+ futures.each_with_index do |future, index|
48
+ host_name = @host_names[index] # Get host name from the stored list
49
+ begin
50
+ # Wait for future to complete and get its value
51
+ # This ensures the future has finished executing
52
+ future_result = future.value
53
+
54
+ # Handle the result
55
+ if future_result.nil?
56
+ # Future returned nil - create a default result
57
+ @results[host_name] = { status: :unknown, error: 'Future returned nil', output: [] }
58
+ elsif future_result.is_a?(Array) && future_result.length == 2
59
+ name, result = future_result
60
+ # Store the result using the name from the future
61
+ @results[name] = result
62
+ else
63
+ # Handle unexpected result format - create a default result
64
+ @results[host_name] =
65
+ { status: :unknown, error: "Unexpected result format: #{future_result.class}", output: [] }
66
+ end
67
+
68
+ # Check if future raised an exception
69
+ if future.rejected?
70
+ error = begin
71
+ future.reason
72
+ rescue StandardError
73
+ 'Unknown error'
74
+ end
75
+ @results[host_name] = { status: :failed, error: error, output: [] } unless @results.key?(host_name)
76
+ end
77
+ rescue StandardError => e
78
+ # If future.value raises an exception, create an error result
79
+ @results[host_name] = { status: :failed, error: "#{e.class}: #{e.message}", output: [] }
80
+ ensure
81
+ # Ensure we always have a result for this host
82
+ @results[host_name] ||= { status: :unknown, error: 'No result collected', output: [] }
83
+ end
84
+
41
85
  completed += 1
42
86
  # Show progress for multiple hosts
43
87
  next unless total > 1
@@ -51,6 +95,8 @@ module Kdeploy
51
95
  end
52
96
 
53
97
  def create_task_futures(task)
98
+ # Store host names in order to match with futures
99
+ @host_names = @hosts.keys
54
100
  @hosts.map do |name, config|
55
101
  Concurrent::Future.execute(executor: @pool) do
56
102
  execute_task_for_host(name, config, task)
@@ -61,14 +107,22 @@ module Kdeploy
61
107
  private
62
108
 
63
109
  def execute_task_for_host(name, config, task)
64
- executor = Executor.new(config)
65
- command_executor = CommandExecutor.new(executor, @output)
110
+ # Add base_dir to config for path resolution
111
+ config_with_base_dir = config.merge(base_dir: @base_dir)
112
+ executor = Executor.new(config_with_base_dir)
113
+ command_executor = CommandExecutor.new(executor, @output, debug: @debug)
66
114
  result = { status: :success, output: [] }
67
115
 
68
- execute_grouped_commands(task, command_executor, name, result)
69
- @results[name] = result
70
- rescue StandardError => e
71
- @results[name] = { status: :failed, error: e.message }
116
+ begin
117
+ execute_grouped_commands(task, command_executor, name, result)
118
+ rescue StandardError => e
119
+ # Ensure result is always set, even on error
120
+ # Don't re-raise, as it would cause future.value to fail
121
+ result = { status: :failed, error: e.message, output: [] }
122
+ end
123
+
124
+ # Return the result so it can be collected from the future
125
+ [name, result]
72
126
  end
73
127
 
74
128
  def execute_grouped_commands(task, command_executor, name, result)
@@ -111,8 +165,8 @@ module Kdeploy
111
165
  end
112
166
 
113
167
  def show_task_header(task_desc)
114
- pastel = @output.respond_to?(:pastel) ? @output.pastel : Pastel.new
115
- @output.write_line(pastel.cyan("\nTASK [#{task_desc}] " + ('*' * 64)))
168
+ # Don't show command header during execution - it will be shown in results
169
+ # This reduces noise during execution
116
170
  end
117
171
  end
118
172
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Kdeploy module for version management
4
4
  module Kdeploy
5
- VERSION = '1.2.12' unless const_defined?(:VERSION)
5
+ VERSION = '1.2.14' unless const_defined?(:VERSION)
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kdeploy
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.12
4
+ version: 1.2.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kk
@@ -10,6 +10,20 @@ bindir: exe
10
10
  cert_chain: []
11
11
  date: 2025-11-19 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bcrypt_pbkdf
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.1'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: concurrent-ruby
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -24,6 +38,20 @@ dependencies:
24
38
  - - "~>"
25
39
  - !ruby/object:Gem::Version
26
40
  version: '1.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: ed25519
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.2'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.2'
27
55
  - !ruby/object:Gem::Dependency
28
56
  name: net-scp
29
57
  requirement: !ruby/object:Gem::Requirement