ruby-progress 1.2.4 → 1.3.1

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.
@@ -17,6 +17,8 @@ module RippleCLI
17
17
  fail_message: nil,
18
18
  complete_checkmark: false,
19
19
  output: :error,
20
+ output_position: :above,
21
+ output_lines: 3,
20
22
  message: nil
21
23
  }
22
24
 
@@ -57,6 +59,14 @@ module RippleCLI
57
59
  options[:command] = command
58
60
  end
59
61
 
62
+ opts.on('--output-position POSITION', 'Position to render captured output: above or below (default: above)') do |pos|
63
+ options[:output_position] = pos.to_sym
64
+ end
65
+
66
+ opts.on('--output-lines N', Integer, 'Number of output lines to reserve for captured output (default: 3)') do |n|
67
+ options[:output_lines] = n
68
+ end
69
+
60
70
  opts.on('--success MESSAGE', 'Success message to display') do |msg|
61
71
  options[:success_message] = msg
62
72
  end
@@ -10,7 +10,10 @@ module TwirlCLI
10
10
  # so the `TwirlCLI` module stays small and focused on dispatching.
11
11
  module Options
12
12
  def self.parse_cli_options
13
- options = {}
13
+ options = {
14
+ output_position: :above,
15
+ output_lines: 3
16
+ }
14
17
 
15
18
  OptionParser.new do |opts|
16
19
  opts.banner = 'Usage: prg twirl [options]'
@@ -40,6 +43,14 @@ module TwirlCLI
40
43
  options[:command] = command
41
44
  end
42
45
 
46
+ opts.on('--output-position POSITION', 'Position to render captured output: above or below (default: above)') do |pos|
47
+ options[:output_position] = pos.to_sym
48
+ end
49
+
50
+ opts.on('--output-lines N', Integer, 'Number of output lines to reserve for captured output (default: 3)') do |n|
51
+ options[:output_lines] = n
52
+ end
53
+
43
54
  opts.on('--success MESSAGE', 'Success message to display') do |text|
44
55
  options[:success] = text
45
56
  end
@@ -3,6 +3,7 @@
3
3
  require 'fileutils'
4
4
  require 'json'
5
5
  require_relative 'twirl_spinner'
6
+ require_relative '../output_capture'
6
7
 
7
8
  # Top-level runtime helper module for the Twirl CLI.
8
9
  #
@@ -22,8 +23,25 @@ module TwirlRunner
22
23
  RubyProgress::Utils.hide_cursor
23
24
  spinner_thread = Thread.new { loop { spinner.animate } }
24
25
 
25
- captured_output = `#{options[:command]} 2>&1`
26
- success = $CHILD_STATUS.success?
26
+ if $stdout.tty? && options[:stdout]
27
+ oc = RubyProgress::OutputCapture.new(
28
+ command: options[:command],
29
+ lines: options[:output_lines] || 3,
30
+ position: options[:output_position] || :above
31
+ )
32
+ oc.start
33
+
34
+ spinner.instance_variable_set(:@output_capture, oc)
35
+
36
+ # wait for command while spinner thread runs
37
+ oc.wait
38
+ captured_lines = oc.lines
39
+ captured_output = captured_lines.join("\n")
40
+ success = true
41
+ else
42
+ captured_output = `#{options[:command]} 2>&1`
43
+ success = $CHILD_STATUS.success?
44
+ end
27
45
 
28
46
  spinner_thread.kill
29
47
  RubyProgress::Utils.clear_line
@@ -82,6 +100,40 @@ module TwirlRunner
82
100
 
83
101
  begin
84
102
  RubyProgress::Utils.hide_cursor
103
+
104
+ # Start job processor thread for twirl
105
+ job_dir = RubyProgress::Daemon.job_dir_for_pid(pid_file)
106
+ job_thread = Thread.new do
107
+ RubyProgress::Daemon.process_jobs(job_dir) do |job|
108
+ oc = RubyProgress::OutputCapture.new(
109
+ command: job['command'],
110
+ lines: options[:output_lines] || 3,
111
+ position: options[:output_position] || :above
112
+ )
113
+ oc.start
114
+
115
+ spinner.instance_variable_set(:@output_capture, oc)
116
+ oc.wait
117
+ captured = oc.lines.join("\n")
118
+ exit_status = oc.exit_status
119
+ spinner.instance_variable_set(:@output_capture, nil)
120
+
121
+ success = exit_status.to_i.zero?
122
+ if job['message']
123
+ RubyProgress::Utils.display_completion(
124
+ job['message'],
125
+ success: success,
126
+ show_checkmark: job['checkmark'] || false,
127
+ output_stream: :stdout
128
+ )
129
+ end
130
+
131
+ { 'exit_status' => exit_status, 'output' => captured }
132
+ rescue StandardError
133
+ # ignore
134
+ end
135
+ end
136
+
85
137
  spinner.animate until stop_requested
86
138
  ensure
87
139
  RubyProgress::Utils.clear_line
@@ -114,6 +166,7 @@ module TwirlRunner
114
166
  end
115
167
  end
116
168
 
169
+ job_thread&.kill
117
170
  FileUtils.rm_f(pid_file)
118
171
  end
119
172
  end
@@ -18,6 +18,7 @@ class TwirlSpinner
18
18
  end
19
19
 
20
20
  def animate
21
+ @output_capture&.redraw($stderr)
21
22
  if @message && !@message.empty?
22
23
  $stderr.print "\r\e[2K#{@start_chars}#{@message} #{@frames[@index]}#{@end_chars}"
23
24
  else
@@ -6,6 +6,14 @@ require_relative 'worm_options'
6
6
 
7
7
  # Enhanced Worm CLI (extracted from bin/prg)
8
8
  module WormCLI
9
+ def self.resolve_pid_file(options, name_key = :daemon_name)
10
+ return options[:pid_file] if options[:pid_file]
11
+
12
+ return "/tmp/ruby-progress/#{options[name_key]}.pid" if options[name_key]
13
+
14
+ RubyProgress::Daemon.default_pid_file
15
+ end
16
+
9
17
  def self.run
10
18
  options = WormCLI::Options.parse_cli_options
11
19
 
@@ -47,29 +55,55 @@ module WormCLI
47
55
  progress = RubyProgress::Worm.new(options)
48
56
 
49
57
  begin
58
+ # Start job processor thread for worm
59
+ job_dir = RubyProgress::Daemon.job_dir_for_pid(pid_file)
60
+ job_thread = Thread.new do
61
+ RubyProgress::Daemon.process_jobs(job_dir) do |job|
62
+ jid = job['id'] || SecureRandom.uuid
63
+ log_path = begin
64
+ File.join(File.dirname(job_dir), "#{jid}.log")
65
+ rescue StandardError
66
+ nil
67
+ end
68
+
69
+ oc = RubyProgress::OutputCapture.new(
70
+ command: job['command'],
71
+ lines: options[:output_lines] || 3,
72
+ position: options[:output_position] || :above,
73
+ log_path: log_path
74
+ )
75
+ oc.start
76
+
77
+ progress.instance_variable_set(:@output_capture, oc)
78
+ oc.wait
79
+ captured = oc.lines.join("\n")
80
+ exit_status = oc.exit_status
81
+ progress.instance_variable_set(:@output_capture, nil)
82
+
83
+ success = exit_status.to_i.zero?
84
+ if job['message']
85
+ RubyProgress::Utils.display_completion(
86
+ job['message'],
87
+ success: success,
88
+ show_checkmark: job['checkmark'] || false,
89
+ output_stream: :stdout
90
+ )
91
+ end
92
+
93
+ { 'exit_status' => exit_status, 'output' => captured, 'log_path' => log_path }
94
+ rescue StandardError
95
+ # ignore per-job errors
96
+ end
97
+ end
98
+
50
99
  progress.run_daemon_mode(
51
100
  success_message: options[:success],
52
101
  show_checkmark: options[:checkmark],
53
102
  control_message_file: RubyProgress::Daemon.control_message_file(pid_file)
54
103
  )
55
104
  ensure
105
+ job_thread&.kill
56
106
  FileUtils.rm_f(pid_file)
57
107
  end
58
108
  end
59
-
60
- def self.resolve_pid_file(options, name_key)
61
- return options[:pid_file] if options[:pid_file]
62
-
63
- if options[name_key]
64
- "/tmp/ruby-progress/#{options[name_key]}.pid"
65
- else
66
- RubyProgress::Daemon.default_pid_file
67
- end
68
- end
69
-
70
- # parse_cli_options is intentionally kept together; further extraction possible
71
- # parse_cli_options is intentionally delegated to WormCLI::Options
72
- def self.parse_cli_options
73
- WormCLI::Options.parse_cli_options
74
- end
75
109
  end
@@ -9,8 +9,11 @@ module WormCLI
9
9
  # the main dispatcher to keep the CLI module small and focused.
10
10
  module Options
11
11
  def self.parse_cli_options
12
- options = {}
13
-
12
+ options = {
13
+ output_position: :above,
14
+ output_lines: 3
15
+ }
16
+ # rubocop:disable Metrics/BlockLength
14
17
  begin
15
18
  OptionParser.new do |opts|
16
19
  opts.banner = 'Usage: prg worm [options]'
@@ -48,6 +51,14 @@ module WormCLI
48
51
  options[:command] = command
49
52
  end
50
53
 
54
+ opts.on('--output-position POSITION', 'Position to render captured output: above or below (default: above)') do |pos|
55
+ options[:output_position] = pos.to_sym
56
+ end
57
+
58
+ opts.on('--output-lines N', Integer, 'Number of output lines to reserve for captured output (default: 3)') do |n|
59
+ options[:output_lines] = n
60
+ end
61
+
51
62
  opts.on('--success MESSAGE', 'Success message to display') do |text|
52
63
  options[:success] = text
53
64
  end
@@ -76,6 +87,12 @@ module WormCLI
76
87
  options[:daemon_name] = name
77
88
  end
78
89
 
90
+ # Accept --daemon-name as an alias for --daemon-as for compatibility
91
+ opts.on('--daemon-name NAME', 'Alias for --daemon-as (compat)') do |name|
92
+ options[:daemon] = true
93
+ options[:daemon_name] = name
94
+ end
95
+
79
96
  opts.on('--pid-file FILE', 'Write process ID to file (default: /tmp/ruby-progress/progress.pid)') do |file|
80
97
  options[:pid_file] = file
81
98
  end
@@ -149,7 +166,7 @@ module WormCLI
149
166
  puts "Run 'prg worm --help' for more information."
150
167
  exit 1
151
168
  end
152
-
169
+ # rubocop:enable Metrics/BlockLength
153
170
  options
154
171
  end
155
172
  end
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
+ # rubocop:disable Metrics/ModuleLength
4
5
  require 'open3'
5
6
  require 'json'
6
7
  require_relative '../utils'
8
+ require_relative '../output_capture'
7
9
 
8
10
  # Runtime helper methods for RubyProgress::Worm
9
11
  #
@@ -56,21 +58,36 @@ module WormRunner
56
58
  stdout_content = nil
57
59
 
58
60
  begin
59
- stdout_content = animate do
60
- Open3.popen3(@command) do |_stdin, stdout, stderr, wait_thr|
61
- captured_stdout = stdout.read
62
- stderr_content = stderr.read
63
- exit_code = wait_thr.value.exitstatus
64
-
65
- unless wait_thr.value.success?
66
- error_msg = @error_text || "Command failed with exit code #{exit_code}"
67
- error_msg += ": #{stderr_content.strip}" if stderr_content && !stderr_content.empty?
68
- raise StandardError, error_msg
69
- end
70
-
71
- captured_stdout
72
- end
73
- end
61
+ stdout_content = if $stdout.tty? && @output_stdout
62
+ oc = RubyProgress::OutputCapture.new(
63
+ command: @command,
64
+ lines: @output_lines || 3,
65
+ position: @output_position || :above
66
+ )
67
+ oc.start
68
+ @output_capture = oc
69
+ animate do
70
+ oc.wait
71
+ end
72
+ @output_capture = nil
73
+ oc.lines.join("\n")
74
+ else
75
+ animate do
76
+ Open3.popen3(@command) do |_stdin, stdout, stderr, wait_thr|
77
+ captured_stdout = stdout.read
78
+ stderr_content = stderr.read
79
+ exit_code = wait_thr.value.exitstatus
80
+
81
+ unless wait_thr.value.success?
82
+ error_msg = @error_text || "Command failed with exit code #{exit_code}"
83
+ error_msg += ": #{stderr_content.strip}" if stderr_content && !stderr_content.empty?
84
+ raise StandardError, error_msg
85
+ end
86
+
87
+ captured_stdout
88
+ end
89
+ end
90
+ end
74
91
 
75
92
  puts stdout_content if @output_stdout && stdout_content
76
93
  rescue StandardError
@@ -161,6 +178,7 @@ module WormRunner
161
178
  @direction ||= 1
162
179
 
163
180
  message_part = @message && !@message.empty? ? "#{@message} " : ''
181
+ @output_capture&.redraw($stderr)
164
182
  $stderr.print "\r\e[2K#{@start_chars}#{message_part}#{generate_dots(@position, @direction)}#{@end_chars}"
165
183
  $stderr.flush
166
184
 
@@ -184,6 +202,7 @@ module WormRunner
184
202
 
185
203
  while @running
186
204
  message_part = @message && !@message.empty? ? "#{@message} " : ''
205
+ @output_capture&.redraw($stderr)
187
206
  $stderr.print "\r\e[2K#{@start_chars}#{message_part}#{generate_dots(position, direction)}#{@end_chars}"
188
207
  $stderr.flush
189
208
 
@@ -209,6 +228,7 @@ module WormRunner
209
228
 
210
229
  while @running && !stop_requested_proc.call
211
230
  message_part = @message && !@message.empty? ? "#{@message} " : ''
231
+ @output_capture&.redraw($stderr)
212
232
 
213
233
  $stderr.print "\r\e[2K"
214
234
 
@@ -258,3 +278,5 @@ module WormRunner
258
278
  dots.join
259
279
  end
260
280
  end
281
+
282
+ # rubocop:enable Metrics/ModuleLength
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
+ require 'fileutils'
4
5
 
5
6
  module RubyProgress
6
7
  module Daemon
@@ -14,6 +15,70 @@ module RubyProgress
14
15
  "#{pid_file}.msg"
15
16
  end
16
17
 
18
+ # Resolve a job directory for the daemon based on pid_file or name.
19
+ # If pid_file is '/tmp/ruby-progress/mytask.pid' -> jobs dir '/tmp/ruby-progress/mytask.jobs'
20
+ def job_dir_for_pid(pid_file)
21
+ base = File.basename(pid_file, '.*')
22
+ File.join(File.dirname(pid_file), "#{base}.jobs")
23
+ end
24
+
25
+ # Process available job files in job_dir. Each job is a JSON file with {"id","command","meta"}.
26
+ # This method polls the directory and yields each parsed job hash to the provided block.
27
+ def process_jobs(job_dir, poll_interval: 0.2)
28
+ FileUtils.mkdir_p(job_dir)
29
+
30
+ loop do
31
+ # Accept any job file ending in .json (UUID filenames are common)
32
+ # Ignore processed-* archives and temporary files (e.g., .tmp)
33
+ files = Dir.children(job_dir).select do |f|
34
+ f.end_with?('.json') && !f.start_with?('processed-')
35
+ end.sort
36
+
37
+ files.each do |f|
38
+ path = File.join(job_dir, f)
39
+ processing = "#{path}.processing"
40
+
41
+ # Claim the file atomically
42
+ begin
43
+ File.rename(path, processing)
44
+ rescue StandardError
45
+ next
46
+ end
47
+
48
+ job = begin
49
+ JSON.parse(File.read(processing))
50
+ rescue StandardError
51
+ FileUtils.rm_f(processing)
52
+ next
53
+ end
54
+
55
+ begin
56
+ yielded = yield(job)
57
+
58
+ # on success, write .result info and merge any returned info
59
+ result = { 'id' => job['id'], 'status' => 'done', 'time' => Time.now.to_i }
60
+ if yielded.is_a?(Hash)
61
+ # ensure string keys
62
+ extra = yielded.transform_keys(&:to_s)
63
+ result.merge!(extra)
64
+ end
65
+ File.write("#{processing}.result", result.to_json)
66
+ rescue StandardError => e
67
+ result = { 'id' => job['id'], 'status' => 'error', 'error' => e.message }
68
+ File.write("#{processing}.result", result.to_json)
69
+ ensure
70
+ begin
71
+ FileUtils.mv(processing, File.join(job_dir, "processed-#{f}"))
72
+ rescue StandardError
73
+ FileUtils.rm_f(processing)
74
+ end
75
+ end
76
+ end
77
+
78
+ sleep(poll_interval)
79
+ end
80
+ end
81
+
17
82
  def show_status(pid_file)
18
83
  if File.exist?(pid_file)
19
84
  pid = File.read(pid_file).strip
@@ -25,6 +25,7 @@ module RubyProgress
25
25
  @current_progress = 0
26
26
  @success_message = options[:success]
27
27
  @error_message = options[:error]
28
+ @output_capture = options[:output_capture]
28
29
 
29
30
  # Parse --ends characters
30
31
  if options[:ends]
@@ -82,6 +83,9 @@ module RubyProgress
82
83
 
83
84
  # Render the current progress bar to stderr
84
85
  def render
86
+ # First redraw captured output (if any) so it appears above/below the bar
87
+ @output_capture&.redraw($stderr)
88
+
85
89
  filled = @style[:full] * @current_progress
86
90
  empty = @style[:empty] * (@length - @current_progress)
87
91
  bar = "#{@start_chars}#{filled}#{empty}#{@end_chars}"
@@ -3,6 +3,7 @@
3
3
  require 'optparse'
4
4
  require 'fileutils'
5
5
  require_relative 'cli/fill_options'
6
+ require_relative 'output_capture'
6
7
 
7
8
  module RubyProgress
8
9
  # CLI module for Fill command
@@ -160,6 +161,17 @@ module RubyProgress
160
161
  error: options[:error_message]
161
162
  }
162
163
 
164
+ # If a command is provided, capture its output and pass an OutputCapture
165
+ if options[:command]
166
+ oc = RubyProgress::OutputCapture.new(
167
+ command: options[:command],
168
+ lines: options[:output_lines] || 3,
169
+ position: options[:output_position] || :above
170
+ )
171
+ oc.start
172
+ fill_options[:output_capture] = oc
173
+ end
174
+
163
175
  fill_bar = Fill.new(fill_options)
164
176
  Fill.hide_cursor
165
177
 
@@ -172,6 +184,22 @@ module RubyProgress
172
184
  # For non-complete percentages, show the result briefly
173
185
  sleep(0.1)
174
186
  end
187
+ elsif options[:command]
188
+ # While the command runs, keep redrawing the bar (live redraw handled by Fill#render)
189
+ sleep_time = case options[:speed]
190
+ when :fast then 0.1
191
+ when :medium, nil then 0.2
192
+ when :slow then 0.5
193
+ when Numeric then 1.0 / options[:speed]
194
+ else 0.3
195
+ end
196
+
197
+ fill_bar.render
198
+ # Loop until the OutputCapture reader has finished
199
+ while oc.alive?
200
+ sleep(sleep_time)
201
+ fill_bar.render
202
+ end
175
203
  else
176
204
  # Auto-advance mode
177
205
  sleep_time = case options[:speed]
@@ -192,6 +220,8 @@ module RubyProgress
192
220
  rescue Interrupt
193
221
  fill_bar.cancel
194
222
  ensure
223
+ # Ensure we wait for capture thread to finish and show cursor
224
+ oc&.wait
195
225
  Fill.show_cursor
196
226
  end
197
227
  end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pty'
4
+ require 'io/console'
5
+ require 'English'
6
+
7
+ module RubyProgress
8
+ # Simple PTY-based output capture that reserves a small area of the terminal
9
+ # for printing captured output while the animation is drawn separately.
10
+ class OutputCapture
11
+ attr_reader :exit_status
12
+
13
+ def initialize(command:, lines: 3, position: :above, log_path: nil)
14
+ @command = command
15
+ @lines = lines
16
+ @position = position
17
+ @buffer = []
18
+ @buf_mutex = Mutex.new
19
+ @stop = false
20
+ @log_path = log_path
21
+ @log_file = nil
22
+ end
23
+
24
+ # Start the child process and return a thread that manages capture.
25
+ def start
26
+ @reader_thread = Thread.new { spawn_and_read }
27
+ self
28
+ end
29
+
30
+ def stop
31
+ @stop = true
32
+ @reader_thread&.join
33
+ end
34
+
35
+ # Wait for the reader thread to complete
36
+ def wait
37
+ @reader_thread&.join
38
+ end
39
+
40
+ # Return snapshot of buffered lines (thread-safe)
41
+ def lines
42
+ @buf_mutex.synchronize { @buffer.dup }
43
+ end
44
+
45
+ # Returns whether the reader thread is still alive
46
+ def alive?
47
+ @reader_thread&.alive? || false
48
+ end
49
+
50
+ # Redraw buffered output into the terminal above/below the current cursor
51
+ # io - IO object to write to (default $stderr)
52
+ def redraw(io = $stderr)
53
+ buf = lines
54
+ _rows, cols = IO.console.winsize
55
+
56
+ # Ensure we have exactly @lines entries (pad with empty strings)
57
+ display_lines = Array.new(@lines) { '' }
58
+ start = [0, buf.size - @lines].max
59
+ buf[start, @lines]&.each_with_index do |l, i|
60
+ display_lines[i + (@lines - [buf.size, @lines].min)] = l.to_s
61
+ end
62
+
63
+ # Save cursor, move up N lines, clear and print buffer, restore cursor
64
+ io.print "\e[s" # save position
65
+ io.print "\e[#{@lines}A" # move up @lines
66
+ display_lines.each do |line|
67
+ io.print "\e[2K" # clear line
68
+ io.print "\r" # move cursor to start of line
69
+ io.print line[0, cols]
70
+ io.print "\n"
71
+ end
72
+ io.print "\e[u" # restore
73
+ io.flush
74
+ end
75
+
76
+ private
77
+
78
+ def spawn_and_read
79
+ PTY.spawn(@command) do |reader, _writer, pid|
80
+ @child_pid = pid
81
+ until reader.eof? || @stop
82
+ next unless reader.wait_readable(0.1)
83
+
84
+ chunk = reader.read_nonblock(4096, exception: false)
85
+ next if chunk.nil? || chunk.empty?
86
+
87
+ # lazily open log file if requested
88
+ if @log_path && !@log_file
89
+ begin
90
+ FileUtils.mkdir_p(File.dirname(@log_path))
91
+ @log_file = File.open(@log_path, 'a')
92
+ rescue StandardError
93
+ @log_file = nil
94
+ end
95
+ end
96
+
97
+ process_chunk(chunk)
98
+ next unless @log_file
99
+
100
+ begin
101
+ @log_file.write(chunk)
102
+ @log_file.flush
103
+ rescue StandardError
104
+ # ignore logging errors
105
+ end
106
+ end
107
+ end
108
+ begin
109
+ Process.wait(@child_pid) if @child_pid
110
+ @exit_status = $CHILD_STATUS.exitstatus if $CHILD_STATUS
111
+ rescue StandardError
112
+ @exit_status = nil
113
+ ensure
114
+ if @log_file
115
+ begin
116
+ @log_file.close
117
+ rescue StandardError
118
+ nil
119
+ end
120
+ end
121
+ end
122
+ rescue Errno::EIO
123
+ # PTY finished
124
+ end
125
+
126
+ def process_chunk(chunk)
127
+ @buf_mutex.synchronize do
128
+ # split into lines, keep last N lines
129
+ chunk.each_line do |line|
130
+ @buffer << line.chomp
131
+ @buffer.shift while @buffer.size > @lines
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -140,6 +140,7 @@ module RubyProgress
140
140
  char = @rainbow ? char.rainbow(i) : char.extend(StringExtensions).light_white
141
141
  post = letters.slice!(0, letters.length).join.extend(StringExtensions).dark_white
142
142
  end
143
+ @output_capture&.redraw($stderr)
143
144
  $stderr.print "\r\e[2K#{@start_chars}#{pre}#{char}#{post}#{@end_chars}"
144
145
  $stderr.flush
145
146
  end
@@ -2,11 +2,11 @@
2
2
 
3
3
  module RubyProgress
4
4
  # Main gem version
5
- VERSION = '1.2.4'
5
+ VERSION = '1.3.1'
6
6
 
7
- # Component-specific versions
8
- WORM_VERSION = '1.1.2'
9
- TWIRL_VERSION = '1.1.2'
10
- RIPPLE_VERSION = '1.1.2'
11
- FILL_VERSION = '1.0.1'
7
+ # Component-specific versions (patch bumps)
8
+ WORM_VERSION = '1.1.3'
9
+ TWIRL_VERSION = '1.1.3'
10
+ RIPPLE_VERSION = '1.1.3'
11
+ FILL_VERSION = '1.0.3'
12
12
  end