kdeploy 1.2.30 → 1.2.38

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.
@@ -24,64 +24,60 @@ module Kdeploy
24
24
  @pastel.bright_white(" #{host.ljust(20)} #{status_str}")
25
25
  end
26
26
 
27
- def format_upload_steps(steps, shown)
28
- format_file_steps(steps, shown, :upload, @pastel.green(' === Upload ==='), 'upload: ')
27
+ # Prefix for per-step lines to make multi-host logs easier to scan.
28
+ def host_prefix(host)
29
+ @pastel.dim(" #{host.ljust(20)} ")
29
30
  end
30
31
 
31
- def format_template_steps(steps, shown)
32
- format_file_steps(steps, shown, :upload_template, @pastel.yellow(' === Template ==='), 'upload_template: ')
32
+ def format_host_completed(duration)
33
+ @pastel.dim(" [completed in #{format('%.2f', duration)}s]")
33
34
  end
34
35
 
35
- def format_sync_steps(steps, shown)
36
- output = []
37
- steps.each do |step|
38
- next if step_already_shown?(step, :sync, shown)
36
+ def calculate_host_duration(result)
37
+ return 0.0 unless result.is_a?(Hash)
39
38
 
40
- mark_step_as_shown(step, :sync, shown)
41
- output << format_sync_step(step)
42
- end
43
- output
39
+ Array(result[:output]).sum { |step| step[:duration].to_f }
44
40
  end
45
41
 
46
- def format_file_steps(steps, shown, type, _header, prefix)
47
- output = []
48
- steps.each do |step|
49
- next if step_already_shown?(step, type, shown)
42
+ def format_upload_steps(steps, _shown = nil)
43
+ format_file_steps(steps, :upload, 'upload: ')
44
+ end
50
45
 
51
- mark_step_as_shown(step, type, shown)
52
- output << format_file_step(step, type, prefix)
53
- end
54
- output
46
+ def format_template_steps(steps, _shown = nil)
47
+ format_file_steps(steps, :upload_template, 'upload_template: ')
48
+ end
49
+
50
+ def format_sync_steps(steps, _shown = nil)
51
+ steps.map { |step| format_sync_step(step) }
52
+ end
53
+
54
+ def format_file_steps(steps, type, prefix)
55
+ steps.map { |step| format_file_step(step, type, prefix) }
55
56
  end
56
57
 
57
58
  def format_file_step(step, type, prefix)
58
59
  duration_str = format_duration(step[:duration])
60
+ status_str = format_step_status(step)
59
61
  icon = type == :upload ? '📤' : '📝'
60
62
  file_path = step[:command].sub(prefix, '')
61
63
  # Truncate long paths for cleaner output
62
64
  display_path = file_path.length > 50 ? "...#{file_path[-47..]}" : file_path
63
65
  color_method = type == :upload ? :green : :yellow
64
- @pastel.dim(" #{icon} ") + @pastel.send(color_method, display_path) + duration_str
66
+ @pastel.dim(" #{icon} ") + @pastel.send(color_method, display_path) + duration_str + " #{status_str}"
65
67
  end
66
68
 
67
- def format_run_steps(steps, shown)
68
- output = []
69
- steps.each do |step|
70
- next if step_already_shown?(step, :run, shown)
71
-
72
- mark_step_as_shown(step, :run, shown)
73
- output.concat(format_single_run_step(step))
74
- end
75
- output
69
+ def format_run_steps(steps, _shown = nil)
70
+ steps.flat_map { |step| format_single_run_step(step) }
76
71
  end
77
72
 
78
73
  def format_single_run_step(step)
79
74
  output = []
80
75
  duration_str = format_duration(step[:duration])
81
- command_line = step[:command].to_s.lines.first.strip
76
+ status_str = format_step_status(step)
77
+ command_line = first_meaningful_command_line(step[:command].to_s)
82
78
  # Truncate long commands for cleaner output
83
79
  display_cmd = command_line.length > 60 ? "#{command_line[0..57]}..." : command_line
84
- output << (@pastel.dim(' • ') + @pastel.cyan(display_cmd) + duration_str)
80
+ output << (@pastel.dim(' • ') + @pastel.cyan(display_cmd) + duration_str + " #{status_str}")
85
81
  # Only show multiline details in debug mode
86
82
  if @debug
87
83
  output.concat(format_multiline_command(step[:command]))
@@ -187,6 +183,17 @@ module Kdeploy
187
183
  output
188
184
  end
189
185
 
186
+ def first_meaningful_command_line(command)
187
+ lines = command.to_s.lines.map(&:strip)
188
+ lines.each do |line|
189
+ next if line.empty?
190
+ next if line.start_with?('#')
191
+
192
+ return line
193
+ end
194
+ lines.first.to_s
195
+ end
196
+
190
197
  def format_command_output(output)
191
198
  result = []
192
199
  return result unless output
@@ -240,18 +247,9 @@ module Kdeploy
240
247
  result
241
248
  end
242
249
 
243
- def step_already_shown?(step, type, shown)
244
- key = [step[:command], type].hash
245
- shown[key]
246
- end
247
-
248
- def mark_step_as_shown(step, type, shown)
249
- key = [step[:command], type].hash
250
- shown[key] = true
251
- end
252
-
253
250
  def format_sync_step(step)
254
251
  duration_str = format_duration(step[:duration])
252
+ status_str = format_step_status(step)
255
253
  sync_path = step[:command].sub('sync: ', '')
256
254
  # Truncate long paths for cleaner output
257
255
  display_path = sync_path.length > 50 ? "...#{sync_path[-47..]}" : sync_path
@@ -266,7 +264,15 @@ module Kdeploy
266
264
  stats << @pastel.yellow("#{deleted} deleted") if deleted.positive?
267
265
  stats_str = stats.any? ? " (#{stats.join(', ')})" : " (#{total} files)"
268
266
 
269
- @pastel.dim(' 📁 ') + @pastel.cyan(display_path) + @pastel.dim(stats_str) + duration_str
267
+ @pastel.dim(' 📁 ') + @pastel.cyan(display_path) + @pastel.dim(stats_str) + duration_str + " #{status_str}"
268
+ end
269
+
270
+ def format_step_status(step)
271
+ if step.is_a?(Hash) && step.key?(:error) && step[:error] && !step[:error].to_s.empty?
272
+ @pastel.red('✗ failed')
273
+ else
274
+ @pastel.green('✓ ok')
275
+ end
270
276
  end
271
277
  end
272
278
  end
@@ -6,21 +6,23 @@ module Kdeploy
6
6
  # Concurrent task runner for executing tasks across multiple hosts
7
7
  class Runner
8
8
  def initialize(hosts, tasks, parallel: Configuration.default_parallel, output: ConsoleOutput.new,
9
- debug: false, base_dir: nil)
9
+ debug: false, base_dir: nil, retries: Configuration.default_retries,
10
+ retry_delay: Configuration.default_retry_delay)
10
11
  @hosts = hosts
11
12
  @tasks = tasks
12
13
  @parallel = parallel
13
14
  @output = output
14
15
  @debug = debug
15
16
  @base_dir = base_dir
17
+ @retries = retries
18
+ @retry_delay = retry_delay
16
19
  @pool = Concurrent::FixedThreadPool.new(@parallel)
17
20
  @results = Concurrent::Hash.new
18
21
  end
19
22
 
20
23
  def run(task_name)
21
24
  task = find_task(task_name)
22
- results = execute_concurrent_tasks(task)
23
- results
25
+ execute_concurrent_tasks(task, task_name)
24
26
  ensure
25
27
  @pool.shutdown
26
28
  end
@@ -33,16 +35,12 @@ module Kdeploy
33
35
  task
34
36
  end
35
37
 
36
- def execute_concurrent_tasks(task)
37
- futures = create_task_futures(task)
38
+ def execute_concurrent_tasks(task, task_name)
39
+ futures = create_task_futures(task, task_name)
38
40
 
39
41
  # If no hosts, return empty results immediately
40
42
  return @results if futures.empty?
41
43
 
42
- # Show progress while waiting for tasks to complete
43
- total = futures.length
44
- completed = 0
45
-
46
44
  # Collect results from futures
47
45
  futures.each_with_index do |future, index|
48
46
  host_name = @host_names[index] # Get host name from the stored list
@@ -84,74 +82,78 @@ module Kdeploy
84
82
  # Ensure we always have a result for this host
85
83
  @results[host_name] ||= { status: :unknown, error: 'No result collected', output: [] }
86
84
  end
87
-
88
- completed += 1
89
- # Show progress for multiple hosts
90
- next unless total > 1
91
-
92
- pastel = @output.respond_to?(:pastel) ? @output.pastel : Pastel.new
93
- @output.write_line(pastel.dim(" [Progress: #{completed}/#{total} hosts completed]"))
94
- @output.flush if @output.respond_to?(:flush)
95
85
  end
96
86
 
97
87
  @results
98
88
  end
99
89
 
100
- def create_task_futures(task)
90
+ def create_task_futures(task, task_name)
101
91
  # Store host names in order to match with futures
102
92
  @host_names = @hosts.keys
103
93
  @hosts.map do |name, config|
104
94
  Concurrent::Future.execute(executor: @pool) do
105
- execute_task_for_host(name, config, task)
95
+ execute_task_for_host(name, config, task, task_name)
106
96
  end
107
97
  end
108
98
  end
109
99
 
110
100
  private
111
101
 
112
- def execute_task_for_host(name, config, task)
102
+ def execute_task_for_host(name, config, task, task_name)
113
103
  # Add base_dir to config for path resolution
114
104
  config_with_base_dir = config.merge(base_dir: @base_dir)
115
105
  executor = Executor.new(config_with_base_dir)
116
- command_executor = CommandExecutor.new(executor, @output, debug: @debug)
106
+ command_executor = CommandExecutor.new(
107
+ executor,
108
+ @output,
109
+ debug: @debug,
110
+ retries: @retries,
111
+ retry_delay: @retry_delay
112
+ )
117
113
  result = { status: :success, output: [] }
118
114
 
119
115
  begin
120
- execute_grouped_commands(task, command_executor, name, result)
116
+ execute_commands(task, command_executor, name, result, task_name)
121
117
  rescue StandardError => e
122
- # Ensure result is always set, even on error
123
- # Don't re-raise, as it would cause future.value to fail
124
- result = { status: :failed, error: e.message, output: [] }
118
+ # Keep any already collected step output for troubleshooting.
119
+ result[:status] = :failed
120
+ result[:error] = "#{e.class}: #{e.message}"
125
121
  end
126
122
 
127
123
  # Return the result so it can be collected from the future
128
124
  [name, result]
129
125
  end
130
126
 
131
- def execute_grouped_commands(task, command_executor, name, result)
127
+ def execute_commands(task, command_executor, name, result, task_name)
132
128
  commands = task[:block].call
133
- grouped_commands = CommandGrouper.group(commands)
134
129
 
135
- grouped_commands.each_value do |command_group|
136
- execute_command_group(command_group, command_executor, name, result)
130
+ commands.each do |command|
131
+ step_result = execute_command(command_executor, command, name)
132
+ result[:output] << step_result
133
+ rescue StandardError => e
134
+ step = step_description(command)
135
+ result[:status] = :failed
136
+ result[:error] = "task=#{task_name} host=#{name} step=#{step} error=#{e.class}: #{e.message}"
137
+ result[:output] << {
138
+ type: command[:type],
139
+ command: step_command_string(command),
140
+ duration: 0.0,
141
+ error: "#{e.class}: #{e.message}",
142
+ output: error_output_for_step(e)
143
+ }
144
+ break
137
145
  end
138
146
  end
139
147
 
140
- def execute_command_group(command_group, command_executor, name, result)
141
- first_cmd = command_group.first
142
- task_desc = CommandGrouper.task_description(first_cmd)
143
- show_task_header(task_desc)
144
-
145
- command_group.each_with_index do |command, index|
146
- # Show progress for multiple commands
147
- if command_group.length > 1
148
- pastel = @output.respond_to?(:pastel) ? @output.pastel : Pastel.new
149
- @output.write_line(pastel.dim(" [Step #{index + 1}/#{command_group.length}]"))
150
- end
148
+ def error_output_for_step(error)
149
+ return nil unless error.is_a?(Kdeploy::SSHError)
151
150
 
152
- step_result = execute_command(command_executor, command, name)
153
- result[:output] << step_result
154
- end
151
+ {
152
+ stdout: error.stdout,
153
+ stderr: error.stderr,
154
+ exit_status: error.exit_status,
155
+ command: error.command
156
+ }
155
157
  end
156
158
 
157
159
  def execute_command(command_executor, command, host_name)
@@ -169,9 +171,35 @@ module Kdeploy
169
171
  end
170
172
  end
171
173
 
172
- def show_task_header(task_desc)
173
- # Don't show command header during execution - it will be shown in results
174
- # This reduces noise during execution
174
+ def step_command_string(command)
175
+ case command[:type]
176
+ when :run
177
+ command[:command].to_s
178
+ when :upload
179
+ "upload: #{command[:source]} -> #{command[:destination]}"
180
+ when :upload_template
181
+ "upload_template: #{command[:source]} -> #{command[:destination]}"
182
+ when :sync
183
+ "sync: #{command[:source]} -> #{command[:destination]}"
184
+ else
185
+ command[:type].to_s
186
+ end
187
+ end
188
+
189
+ def step_description(command)
190
+ case command[:type]
191
+ when :run
192
+ first = command[:command].to_s.lines.first&.strip
193
+ "run: #{first}"
194
+ when :upload
195
+ "upload: #{command[:source]} -> #{command[:destination]}"
196
+ when :upload_template
197
+ "upload_template: #{command[:source]} -> #{command[:destination]}"
198
+ when :sync
199
+ "sync: #{command[:source]} -> #{command[:destination]}"
200
+ else
201
+ command[:type].to_s
202
+ end
175
203
  end
176
204
  end
177
205
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Kdeploy module for version management
4
4
  module Kdeploy
5
- VERSION = '1.2.30' unless const_defined?(:VERSION)
5
+ VERSION = '1.2.38' unless const_defined?(:VERSION)
6
6
  end
data/lib/kdeploy.rb CHANGED
@@ -8,7 +8,6 @@ require_relative 'kdeploy/banner'
8
8
  require_relative 'kdeploy/file_filter'
9
9
  require_relative 'kdeploy/dsl'
10
10
  require_relative 'kdeploy/executor'
11
- require_relative 'kdeploy/command_grouper'
12
11
  require_relative 'kdeploy/command_executor'
13
12
  require_relative 'kdeploy/output_formatter'
14
13
  require_relative 'kdeploy/help_formatter'
data/r.md ADDED
@@ -0,0 +1 @@
1
+ # Your product documentation
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.2.30
4
+ version: 1.2.38
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kk
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-11-21 00:00:00.000000000 Z
11
+ date: 2026-02-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bcrypt_pbkdf
@@ -160,6 +160,7 @@ extensions:
160
160
  - ext/mkrf_conf.rb
161
161
  extra_rdoc_files: []
162
162
  files:
163
+ - AGENTS.md
163
164
  - README.md
164
165
  - README_EN.md
165
166
  - exe/kdeploy
@@ -168,7 +169,6 @@ files:
168
169
  - lib/kdeploy/banner.rb
169
170
  - lib/kdeploy/cli.rb
170
171
  - lib/kdeploy/command_executor.rb
171
- - lib/kdeploy/command_grouper.rb
172
172
  - lib/kdeploy/completions/kdeploy.bash
173
173
  - lib/kdeploy/completions/kdeploy.zsh
174
174
  - lib/kdeploy/configuration.rb
@@ -184,6 +184,7 @@ files:
184
184
  - lib/kdeploy/runner.rb
185
185
  - lib/kdeploy/template.rb
186
186
  - lib/kdeploy/version.rb
187
+ - r.md
187
188
  homepage: https://github.com/kevin197011/kdeploy
188
189
  licenses:
189
190
  - MIT
@@ -1,38 +0,0 @@
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, :upload_template, :sync
15
- "#{cmd[:type]}_#{cmd[:source]}"
16
- when :run
17
- "#{cmd[:type]}_#{cmd[:command].to_s.lines.first.strip}"
18
- else
19
- cmd[:type].to_s
20
- end
21
- end
22
-
23
- def self.task_description(command)
24
- case command[:type]
25
- when :upload
26
- "upload #{command[:source]}"
27
- when :upload_template
28
- "template #{command[:source]}"
29
- when :sync
30
- "sync #{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