ruby-progress 1.3.4 → 1.3.5

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.
@@ -1,159 +1,198 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # CLI: prg job
4
-
5
- # Provides the `prg job send` helper to enqueue commands into a daemon's
6
- # job directory. This file contains a minimal implementation used by tests.
3
+ # CLI: prg job [subcommand]
4
+ # Send control messages to backgrounded progress indicators.
7
5
 
8
6
  require 'optparse'
9
7
  require 'json'
10
- require 'securerandom'
11
8
  require 'fileutils'
12
9
  require_relative '../daemon'
13
10
 
14
- # Job CLI helpers
11
+ # JobCLI - sends control messages to backgrounded progress indicators
15
12
  #
16
- # Exposed as `prg job send`.
13
+ # Usage:
14
+ # prg job stop --daemon-name mytask [--message "Done!"] [--checkmark]
15
+ # prg job advance --daemon-name mytask [--amount 10]
16
+ # prg job status --daemon-name mytask
17
17
  module JobCLI
18
- # JobCLI
19
- #
20
- # Small CLI module that exposes `prg job send` for enqueuing jobs into the
21
- # daemon job directory. This is intentionally minimal: it writes a single
22
- # JSON file atomically and optionally waits for a result file created by
23
- # the daemon's job processor.
24
- # Simple CLI for submitting jobs to a running daemon job directory.
25
- # Usage: prg job send --pid-file /tmp/... --command "echo hi" [--wait]
26
- class Options
27
- def self.parse(argv)
28
- options = { wait: false }
29
- opt = OptionParser.new do |o|
30
- o.banner = 'Usage: prg job send [options]'
31
- o.on('--pid-file PATH', 'Path to daemon pid file') do |v|
32
- options[:pid_file] = v
33
- end
34
- o.on('--daemon-name NAME', 'Daemon name (maps to /tmp/ruby-progress/NAME.pid)') do |v|
35
- options[:daemon_name] = v
36
- end
37
- o.on('--command CMD', 'Command to run') do |v|
38
- options[:command] = v
39
- end
40
- o.on('--stdin', 'Read command from stdin (overrides --command)') do
41
- options[:stdin] = true
42
- end
43
- o.on('--advance', 'Send an advance action (no value)') do
44
- options[:action] = 'advance'
45
- end
46
- o.on('--percent N', Integer, 'Send a percent action with value N') do |v|
47
- options[:action] = 'percent'
48
- options[:value] = v
49
- end
50
- o.on('--complete', 'Send a complete action (no value)') do
51
- options[:action] = 'complete'
52
- end
53
- o.on('--cancel', 'Send a cancel action (no value)') do
54
- options[:action] = 'cancel'
55
- end
56
- o.on('--action ACTION', 'Send a custom action name') do |v|
57
- options[:action] = v
58
- end
59
- o.on('--value VAL', 'Value for the action (string or number)') do |v|
60
- options[:value] = v
61
- end
62
- o.on('--wait', 'Wait for result file and print it') do
63
- options[:wait] = true
64
- end
65
- o.on('--timeout SECONDS', Integer, 'Timeout seconds for wait') do |v|
66
- options[:timeout] = v
67
- end
68
- end
18
+ def self.run(argv = ARGV)
19
+ if argv.empty?
20
+ print_help
21
+ exit 1
22
+ end
69
23
 
70
- rest = opt.parse(argv)
71
- options[:command] ||= rest.join(' ') unless rest.empty?
72
- options
24
+ subcommand = argv.shift
25
+
26
+ case subcommand
27
+ when 'stop'
28
+ stop(argv)
29
+ when 'advance'
30
+ advance(argv)
31
+ when 'status'
32
+ status(argv)
33
+ when 'send'
34
+ # Backward compatibility: 'send' is now 'stop'
35
+ warn "Warning: 'prg job send' is deprecated. Use 'prg job stop' instead."
36
+ stop(argv)
37
+ when '--help', '-h'
38
+ print_help
39
+ else
40
+ warn "Error: Unknown subcommand '#{subcommand}'"
41
+ print_help
42
+ exit 1
73
43
  end
74
44
  end
75
45
 
76
- def self.send(argv = ARGV)
77
- opts = Options.parse(argv)
78
-
79
- # Resolve pid file
80
- pid_file = if opts[:pid_file]
81
- opts[:pid_file]
82
- elsif opts[:daemon_name]
83
- "/tmp/ruby-progress/#{opts[:daemon_name]}.pid"
84
- else
85
- RubyProgress::Daemon.default_pid_file
86
- end
87
-
88
- job_dir = RubyProgress::Daemon.job_dir_for_pid(pid_file)
89
- FileUtils.mkdir_p(job_dir)
90
-
91
- cmd = if opts[:stdin]
92
- $stdin.read
93
- else
94
- opts[:command]
95
- end
96
-
97
- is_action = !opts[:action].nil? && opts[:action] != false
98
-
99
- if is_action
100
- if cmd && !cmd.strip.empty?
101
- warn 'Cannot specify both --command/--stdin and an action flag'
102
- exit 1
103
- end
46
+ def self.print_help
47
+ puts 'Usage: prg job [subcommand] [options]'
48
+ puts
49
+ puts 'Subcommands:'
50
+ puts ' stop Stop a running progress indicator'
51
+ puts ' advance Advance a fill progress bar'
52
+ puts ' status Check status of a running indicator'
53
+ puts
54
+ puts 'Common Options:'
55
+ puts ' --daemon-name NAME Name of the daemon to control'
56
+ puts ' --pid-file PATH Path to daemon PID file'
57
+ puts
58
+ puts 'Examples:'
59
+ puts ' prg job stop --daemon-name mytask --message "Complete!"'
60
+ puts ' prg job advance --daemon-name mybar --amount 10'
61
+ puts ' prg job status --daemon-name mytask'
62
+ end
63
+
64
+ def self.resolve_pid_file(opts)
65
+ if opts[:pid_file]
66
+ opts[:pid_file]
67
+ elsif opts[:daemon_name]
68
+ "/tmp/ruby-progress/#{opts[:daemon_name]}.pid"
104
69
  else
105
- unless cmd && !cmd.strip.empty?
106
- warn 'No command specified. Use --command, --stdin, or pass an action flag.'
107
- exit 1
108
- end
70
+ RubyProgress::Daemon.default_pid_file
71
+ end
72
+ end
73
+
74
+ def self.stop(argv)
75
+ opts = parse_stop_options(argv)
76
+ pid_file = resolve_pid_file(opts)
77
+
78
+ unless File.exist?(pid_file)
79
+ warn "PID file #{pid_file} not found. Is the daemon running?"
80
+ exit 1
109
81
  end
110
82
 
111
- job_id = SecureRandom.uuid
112
- tmp = File.join(job_dir, "#{job_id}.json.tmp")
113
- final = File.join(job_dir, "#{job_id}.json")
114
-
115
- payload = build_payload(opts, job_id, cmd)
116
-
117
- File.write(tmp, JSON.dump(payload))
118
- FileUtils.mv(tmp, final)
119
-
120
- if opts[:wait]
121
- timeout = opts[:timeout] || 10
122
- start = Time.now
123
- result_path = "#{final}.processing.result"
124
- loop do
125
- if File.exist?(result_path)
126
- puts File.read(result_path)
127
- break
128
- end
129
- if Time.now - start > timeout
130
- warn 'Timed out waiting for result'
131
- exit 2
132
- end
133
- sleep 0.1
83
+ RubyProgress::Daemon.stop_daemon_by_pid_file(
84
+ pid_file,
85
+ message: opts[:message],
86
+ checkmark: opts[:checkmark] || false,
87
+ error: opts[:error] || false
88
+ )
89
+
90
+ # Don't output confirmation - the daemon itself shows the completion message
91
+ end
92
+
93
+ def self.advance(argv)
94
+ opts = parse_advance_options(argv)
95
+ pid_file = resolve_pid_file(opts)
96
+
97
+ unless File.exist?(pid_file)
98
+ warn "PID file #{pid_file} not found. Is the daemon running?"
99
+ exit 1
100
+ end
101
+
102
+ # Write advance command to control message file
103
+ cmf = RubyProgress::Daemon.control_message_file(pid_file)
104
+ control_data = {
105
+ action: 'advance',
106
+ amount: opts[:amount] || 1,
107
+ total: opts[:total]
108
+ }.compact
109
+
110
+ File.write(cmf, JSON.generate(control_data))
111
+
112
+ # Send signal to daemon to check for messages
113
+ pid = File.read(pid_file).strip.to_i
114
+ begin
115
+ Process.kill('USR2', pid)
116
+ rescue Errno::ESRCH
117
+ # Process doesn't exist
118
+ end
119
+
120
+ # Silent operation for script-friendly usage
121
+ end
122
+
123
+ def self.status(argv)
124
+ opts = parse_status_options(argv)
125
+ pid_file = resolve_pid_file(opts)
126
+
127
+ RubyProgress::Daemon.show_status(pid_file)
128
+ end
129
+
130
+ def self.parse_stop_options(argv)
131
+ options = {}
132
+ opt = OptionParser.new do |o|
133
+ o.banner = 'Usage: prg job stop [options]'
134
+ o.on('--pid-file PATH', 'Path to daemon PID file') do |v|
135
+ options[:pid_file] = v
136
+ end
137
+ o.on('--daemon-name NAME', 'Daemon name (maps to /tmp/ruby-progress/NAME.pid)') do |v|
138
+ options[:daemon_name] = v
139
+ end
140
+ o.on('--message MSG', 'Optional completion message to display') do |v|
141
+ options[:message] = v
142
+ end
143
+ o.on('--checkmark', 'Display a checkmark on completion') do
144
+ options[:checkmark] = true
145
+ end
146
+ o.on('--error', 'Mark completion as error state') do
147
+ options[:error] = true
134
148
  end
135
- else
136
- puts job_id
137
149
  end
150
+
151
+ opt.parse(argv)
152
+ options
138
153
  end
139
154
 
140
- # Build the JSON payload for a job based on parsed options.
141
- def self.build_payload(opts, job_id, cmd)
142
- payload = { 'id' => job_id }
155
+ def self.parse_advance_options(argv)
156
+ options = {}
157
+ opt = OptionParser.new do |o|
158
+ o.banner = 'Usage: prg job advance [options]'
159
+ o.on('--pid-file PATH', 'Path to daemon PID file') do |v|
160
+ options[:pid_file] = v
161
+ end
162
+ o.on('--daemon-name NAME', 'Daemon name (maps to /tmp/ruby-progress/NAME.pid)') do |v|
163
+ options[:daemon_name] = v
164
+ end
165
+ o.on('--amount N', Integer, 'Amount to advance (default: 1)') do |v|
166
+ options[:amount] = v
167
+ end
168
+ o.on('--total N', Integer, 'Update total if needed') do |v|
169
+ options[:total] = v
170
+ end
171
+ end
143
172
 
144
- is_action = !opts[:action].nil? && opts[:action] != false
173
+ opt.parse(argv)
174
+ options
175
+ end
145
176
 
146
- if is_action
147
- payload['action'] = opts[:action]
148
- if opts.key?(:value)
149
- val = opts[:value]
150
- payload['value'] = val.to_i if val.is_a?(String) && val =~ /^\d+$/
151
- payload['value'] ||= val
177
+ def self.parse_status_options(argv)
178
+ options = {}
179
+ opt = OptionParser.new do |o|
180
+ o.banner = 'Usage: prg job status [options]'
181
+ o.on('--pid-file PATH', 'Path to daemon PID file') do |v|
182
+ options[:pid_file] = v
183
+ end
184
+ o.on('--daemon-name NAME', 'Daemon name (maps to /tmp/ruby-progress/NAME.pid)') do |v|
185
+ options[:daemon_name] = v
152
186
  end
153
- else
154
- payload['command'] = cmd
155
187
  end
156
188
 
157
- payload
189
+ opt.parse(argv)
190
+ options
191
+ end
192
+
193
+ # Backward compatibility
194
+ def self.send(argv = ARGV)
195
+ warn "Warning: 'JobCLI.send' is deprecated. Use 'JobCLI.stop' instead."
196
+ stop(argv)
158
197
  end
159
198
  end
@@ -8,6 +8,14 @@ require_relative '../output_capture'
8
8
 
9
9
  # Enhanced Ripple CLI with unified flags (extracted from bin/prg)
10
10
  module RippleCLI
11
+ def self.resolve_pid_file(options, name_key = :daemon_name)
12
+ return options[:pid_file] if options[:pid_file]
13
+
14
+ return "/tmp/ruby-progress/#{options[name_key]}.pid" if options[name_key]
15
+
16
+ RubyProgress::Daemon.default_pid_file
17
+ end
18
+
11
19
  def self.run
12
20
  trap('INT') do
13
21
  RubyProgress::Utils.show_cursor
@@ -18,11 +26,11 @@ module RippleCLI
18
26
 
19
27
  # Daemon/status/stop handling (process these without requiring text)
20
28
  if options[:status]
21
- pid_file = options[:pid_file] || RubyProgress::Daemon.default_pid_file
29
+ pid_file = resolve_pid_file(options, :status_name)
22
30
  RubyProgress::Daemon.show_status(pid_file)
23
31
  exit
24
32
  elsif options[:stop]
25
- pid_file = options[:pid_file] || RubyProgress::Daemon.default_pid_file
33
+ pid_file = resolve_pid_file(options, :stop_name)
26
34
  stop_msg = options[:stop_error] || options[:stop_success]
27
35
  is_error = !options[:stop_error].nil?
28
36
  RubyProgress::Daemon.stop_daemon_by_pid_file(
@@ -33,13 +41,8 @@ module RippleCLI
33
41
  )
34
42
  exit
35
43
  elsif options[:daemon]
36
- # For daemon mode, detach so shell has no tracked job unless the user
37
- # requested a non-detaching background child via --no-detach.
38
- if options[:no_detach]
39
- PrgCLI.backgroundize
40
- else
41
- PrgCLI.daemonize
42
- end
44
+ # Background without detaching so ripple remains visible in current terminal
45
+ PrgCLI.backgroundize
43
46
 
44
47
  # For daemon mode, default message if none provided
45
48
  text = options[:message] || ARGV.join(' ')
@@ -125,7 +128,7 @@ module RippleCLI
125
128
  end
126
129
 
127
130
  def self.run_daemon_mode(text, options)
128
- pid_file = options[:pid_file] || RubyProgress::Daemon.default_pid_file
131
+ pid_file = resolve_pid_file(options, :daemon_name)
129
132
  FileUtils.mkdir_p(File.dirname(pid_file))
130
133
  File.write(pid_file, Process.pid.to_s)
131
134
  begin
@@ -139,9 +142,6 @@ module RippleCLI
139
142
  Signal.trap('TERM') { stop_requested = true }
140
143
  Signal.trap('HUP') { stop_requested = true }
141
144
 
142
- job_dir = RubyProgress::Daemon.job_dir_for_pid(pid_file)
143
- job_thread = Thread.new { process_daemon_jobs_for_rippler(job_dir, rippler, options) }
144
-
145
145
  rippler.advance until stop_requested
146
146
  ensure
147
147
  RubyProgress::Utils.clear_line
@@ -184,51 +184,9 @@ module RippleCLI
184
184
  end
185
185
 
186
186
  # stop job thread and cleanup
187
- job_thread&.kill
188
187
  FileUtils.rm_f(pid_file)
189
188
  end
190
189
  end
191
190
 
192
- def self.process_daemon_jobs_for_rippler(job_dir, rippler, options)
193
- RubyProgress::Daemon.process_jobs(job_dir) do |job|
194
- jid = job['id'] || SecureRandom.uuid
195
- log_path = begin
196
- File.join(File.dirname(job_dir), "#{jid}.log")
197
- rescue StandardError
198
- nil
199
- end
200
-
201
- oc = RubyProgress::OutputCapture.new(
202
- command: job['command'],
203
- lines: options[:output_lines] || 3,
204
- position: options[:output_position] || :above,
205
- log_path: log_path
206
- )
207
- oc.start
208
-
209
- rippler.instance_variable_set(:@output_capture, oc)
210
- oc.wait
211
- captured = oc.lines.join("\n")
212
- exit_status = oc.exit_status
213
- rippler.instance_variable_set(:@output_capture, nil)
214
-
215
- success = exit_status.to_i.zero?
216
- if job['message']
217
- RubyProgress::Utils.display_completion(
218
- job['message'],
219
- success: success,
220
- show_checkmark: job['checkmark'] || false,
221
- output_stream: :stdout,
222
- icons: { success: options[:success_icon], error: options[:error_icon] }
223
- )
224
- end
225
-
226
- { 'exit_status' => exit_status, 'output' => captured, 'log_path' => log_path }
227
- rescue StandardError
228
- # ignore per-job errors; process_jobs will write result
229
- nil
230
- end
231
- end
232
-
233
191
  # Options parsing moved to ripple_options.rb
234
192
  end
@@ -106,8 +106,9 @@ module RippleCLI
106
106
  options[:daemon] = true
107
107
  end
108
108
 
109
- opts.on('--no-detach', 'When used with --daemon: run background child but do not fully detach from the terminal') do
110
- options[:no_detach] = true
109
+ opts.on('--daemon-as NAME', 'Run in daemon mode with custom name (creates /tmp/ruby-progress/NAME.pid)') do |name|
110
+ options[:daemon] = true
111
+ options[:daemon_name] = name
111
112
  end
112
113
 
113
114
  opts.on('--pid-file FILE', 'Write process ID to file (default: /tmp/ruby-progress/progress.pid)') do |file|
@@ -118,10 +119,20 @@ module RippleCLI
118
119
  options[:stop] = true
119
120
  end
120
121
 
122
+ opts.on('--stop-id NAME', 'Stop daemon by name (automatically implies --stop)') do |name|
123
+ options[:stop] = true
124
+ options[:stop_name] = name
125
+ end
126
+
121
127
  opts.on('--status', 'Show daemon status (running/not running)') do
122
128
  options[:status] = true
123
129
  end
124
130
 
131
+ opts.on('--status-id NAME', 'Show daemon status by name') do |name|
132
+ options[:status] = true
133
+ options[:status_name] = name
134
+ end
135
+
125
136
  opts.on('--stop-success MESSAGE', 'When stopping, show this success message') do |msg|
126
137
  options[:stop_success] = msg
127
138
  end
@@ -135,7 +146,7 @@ module RippleCLI
135
146
  opts.separator ''
136
147
  opts.separator 'Daemon notes:'
137
148
  opts.separator ' - Do not append &; prg detaches itself and returns immediately.'
138
- opts.separator ' - Use --status/--stop with optional --pid-file to control it.'
149
+ opts.separator ' - Use --daemon-as NAME for named daemons, or --stop-id/--status-id for named control.'
139
150
 
140
151
  opts.separator ''
141
152
  opts.separator 'General:'
@@ -26,7 +26,9 @@ module TwirlCLI
26
26
  )
27
27
  exit
28
28
  elsif options[:daemon]
29
- PrgCLI.daemonize
29
+ # Background without detaching so spinner remains visible in current terminal
30
+ PrgCLI.backgroundize
31
+
30
32
  TwirlRunner.run_daemon_mode(options)
31
33
  elsif options[:command]
32
34
  TwirlRunner.run_with_command(options)
@@ -92,10 +92,6 @@ module TwirlCLI
92
92
  options[:daemon] = true
93
93
  end
94
94
 
95
- opts.on('--no-detach', 'When used with --daemon/--daemon-as: run background child but do not fully detach from the terminal') do
96
- options[:no_detach] = true
97
- end
98
-
99
95
  opts.on('--daemon-as NAME', 'Run in daemon mode with custom name (creates /tmp/ruby-progress/NAME.pid)') do |name|
100
96
  options[:daemon] = true
101
97
  options[:daemon_name] = name
@@ -104,41 +104,6 @@ module TwirlRunner
104
104
  begin
105
105
  RubyProgress::Utils.hide_cursor
106
106
 
107
- # Start job processor thread for twirl
108
- job_dir = RubyProgress::Daemon.job_dir_for_pid(pid_file)
109
- job_thread = Thread.new do
110
- RubyProgress::Daemon.process_jobs(job_dir) do |job|
111
- oc = RubyProgress::OutputCapture.new(
112
- command: job['command'],
113
- lines: options[:output_lines] || 3,
114
- position: options[:output_position] || :above,
115
- stream: options[:stdout] || options[:stdout_live]
116
- )
117
- oc.start
118
-
119
- spinner.instance_variable_set(:@output_capture, oc)
120
- oc.wait
121
- captured = oc.lines.join("\n")
122
- exit_status = oc.exit_status
123
- spinner.instance_variable_set(:@output_capture, nil)
124
-
125
- success = exit_status.to_i.zero?
126
- if job['message']
127
- RubyProgress::Utils.display_completion(
128
- job['message'],
129
- success: success,
130
- show_checkmark: job['checkmark'] || false,
131
- output_stream: :stdout,
132
- icons: { success: options[:success_icon], error: options[:error_icon] }
133
- )
134
- end
135
-
136
- { 'exit_status' => exit_status, 'output' => captured }
137
- rescue StandardError
138
- # ignore
139
- end
140
- end
141
-
142
107
  spinner.animate until stop_requested
143
108
  ensure
144
109
  RubyProgress::Utils.clear_line
@@ -172,7 +137,6 @@ module TwirlRunner
172
137
  end
173
138
  end
174
139
 
175
- job_thread&.kill
176
140
  FileUtils.rm_f(pid_file)
177
141
  end
178
142
  end
@@ -33,13 +33,8 @@ module WormCLI
33
33
  )
34
34
  exit
35
35
  elsif options[:daemon]
36
- # Detach (or background without detaching) before starting daemon logic
37
- # so the invoking shell/script continues immediately.
38
- if options[:no_detach]
39
- PrgCLI.backgroundize
40
- else
41
- PrgCLI.daemonize
42
- end
36
+ # Background without detaching so worm remains visible in current terminal
37
+ PrgCLI.backgroundize
43
38
 
44
39
  run_daemon_mode(options)
45
40
  else
@@ -61,49 +56,6 @@ module WormCLI
61
56
  progress = RubyProgress::Worm.new(options)
62
57
 
63
58
  begin
64
- # Start job processor thread for worm
65
- job_dir = RubyProgress::Daemon.job_dir_for_pid(pid_file)
66
- job_thread = Thread.new do
67
- RubyProgress::Daemon.process_jobs(job_dir) do |job|
68
- jid = job['id'] || SecureRandom.uuid
69
- log_path = begin
70
- File.join(File.dirname(job_dir), "#{jid}.log")
71
- rescue StandardError
72
- nil
73
- end
74
-
75
- oc = RubyProgress::OutputCapture.new(
76
- command: job['command'],
77
- lines: options[:output_lines] || 3,
78
- position: options[:output_position] || :above,
79
- log_path: log_path,
80
- stream: options[:stdout] || options[:stdout_live]
81
- )
82
- oc.start
83
-
84
- progress.instance_variable_set(:@output_capture, oc)
85
- oc.wait
86
- captured = oc.lines.join("\n")
87
- exit_status = oc.exit_status
88
- progress.instance_variable_set(:@output_capture, nil)
89
-
90
- success = exit_status.to_i.zero?
91
- if job['message']
92
- RubyProgress::Utils.display_completion(
93
- job['message'],
94
- success: success,
95
- show_checkmark: job['checkmark'] || false,
96
- output_stream: :stdout,
97
- icons: { success: options[:success_icon], error: options[:error_icon] }
98
- )
99
- end
100
-
101
- { 'exit_status' => exit_status, 'output' => captured, 'log_path' => log_path }
102
- rescue StandardError
103
- # ignore per-job errors
104
- end
105
- end
106
-
107
59
  progress.run_daemon_mode(
108
60
  success_message: options[:success],
109
61
  show_checkmark: options[:checkmark],
@@ -111,7 +63,6 @@ module WormCLI
111
63
  icons: { success: options[:success_icon], error: options[:error_icon] }
112
64
  )
113
65
  ensure
114
- job_thread&.kill
115
66
  FileUtils.rm_f(pid_file)
116
67
  end
117
68
  end
@@ -13,7 +13,6 @@ module WormCLI
13
13
  output_position: :above,
14
14
  output_lines: 3
15
15
  }
16
- # rubocop:disable Metrics/BlockLength
17
16
  begin
18
17
  OptionParser.new do |opts|
19
18
  opts.banner = 'Usage: prg worm [options]'
@@ -105,10 +104,6 @@ module WormCLI
105
104
  options[:daemon_name] = name
106
105
  end
107
106
 
108
- opts.on('--no-detach', 'When used with --daemon/--daemon-as: run background child but do not fully detach from the terminal') do
109
- options[:no_detach] = true
110
- end
111
-
112
107
  opts.on('--pid-file FILE', 'Write process ID to file (default: /tmp/ruby-progress/progress.pid)') do |file|
113
108
  options[:pid_file] = file
114
109
  end
@@ -182,7 +177,6 @@ module WormCLI
182
177
  puts "Run 'prg worm --help' for more information."
183
178
  exit 1
184
179
  end
185
- # rubocop:enable Metrics/BlockLength
186
180
  options
187
181
  end
188
182
  end
@@ -70,7 +70,13 @@ module WormRunner
70
70
  oc.wait
71
71
  end
72
72
  @output_capture = nil
73
- oc.lines.join("\n")
73
+ # For non-live capture, flush_to handles the output directly
74
+ if @output_live
75
+ oc.lines.join("\n")
76
+ else
77
+ oc.flush_to($stdout) if @output_stdout
78
+ nil # Don't return content since it's already been flushed
79
+ end
74
80
  else
75
81
  animate do
76
82
  Open3.popen3(@command) do |_stdin, stdout, stderr, wait_thr|