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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 446ecad2fd85e194293130a638b9e986270c5849ad5cd8a65733b5c2f01198df
4
- data.tar.gz: 35686adf5664171676333a89d48f3f296dcc03751636eea49711a3885ea86d4d
3
+ metadata.gz: 7f08082d9cebd17f7d3ece436ff76e4f4beb8f934d803719a6bdc3477fa8c97b
4
+ data.tar.gz: da85e2d2e2587c13f4c778094fb1dcd8e2aa832dd0cff34b947b90db0f4e6585
5
5
  SHA512:
6
- metadata.gz: d5cd10f336fae3e523e00c70bc9939f6b4162b1b9473424e1c481b0aa9f077b5f0e08037bb53f96f23f5d07a38a0ac5c72947daf90d545127736f002db692c77
7
- data.tar.gz: 86f820e4b75c810e45801f1a214d219f7103e386e4c53bd733eb22a231a3162900b0b011c566f2d134b0a78e06947d33ddf1e2f9fea5c8a35552f56bc4b1f7e0
6
+ metadata.gz: f3a816024fd6cc2aba00edd334c8b82e3783d6be3916bc36238172add46c695febcd80689ba49179b0d48a2175f983dca6b381e482c143e72f2bb0d80804b247
7
+ data.tar.gz: 56fe19077105ebf00cdae2c939eb761d0b37ae4f4cb82b439207c0562ed46cd97abd9105e7b6c0789868fa6087f5d646736b3ad0a58d3df6d09e5028add6d4bf
data/CHANGELOG.md CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  # CHANGELOG
3
2
 
4
3
  All notable changes to this project will be documented in this file.
@@ -6,6 +5,39 @@ All notable changes to this project will be documented in this file.
6
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
7
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
7
 
8
+ ## 1.3.0 - 2025-10-12
9
+
10
+ ### Added
11
+
12
+ - PTY-based output capture: `RubyProgress::OutputCapture` now allows running commands under a PTY, keeps a rolling buffer of the last N lines for live redraw, and optionally writes the full streamed output to a `log_path` file.
13
+ - File-based daemon job queue: `RubyProgress::Daemon.process_jobs` implements atomic job enqueue/claim/processing with `.processing.result` metadata files and processed-archive behavior.
14
+ - `prg job send` CLI helper: atomically writes jobs to daemon job dirs and supports `--wait` to poll for results, `--daemon-name`, `--pid-file`, `--stdin`, and `--timeout`.
15
+ - Integration of job processing into Ripple/Twirl/Worm daemon modes so a running daemon can accept and display job output without interrupting animations.
16
+ - CLI options for output handling: `--output-position` and `--output-lines` to reserve terminal rows for captured output.
17
+
18
+ ### Changed
19
+
20
+ - Job result files now merge any Hash returned by the job handler into the `.processing.result` JSON (e.g., `exit_status`, `output`, `log_path`).
21
+
22
+ ### Tests
23
+
24
+ - Added unit and integration tests covering job enqueueing, processing, and result persistence.
25
+
26
+ ### Release notes
27
+
28
+ - Merge commit: 99d9c39 (squash-merge of feature/output-handling)
29
+
30
+ ## 1.3.1 - 2025-10-12
31
+
32
+ ### Added
33
+
34
+ - `fill` subcommand: added `-c, --command COMMAND` so the determinate progress bar can run and capture command output like the other subcommands. This includes `--output-lines` and `--output-position` support for reserving terminal rows during capture.
35
+
36
+ ### Changed
37
+
38
+ - Bumped `FILL_VERSION` (patch) to reflect the new CLI behavior.
39
+
40
+
9
41
  ## 1.2.3 - 2025-10-11
10
42
 
11
43
  ### Added
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ruby-progress (1.2.4)
4
+ ruby-progress (1.3.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -8,6 +8,27 @@
8
8
 
9
9
  This repository contains three different Ruby progress indicator projects: **Ripple**, **Worm**, and **Twirl**. All provide animated terminal progress indicators with different visual styles and features.
10
10
 
11
+ ## Table of Contents
12
+
13
+ - [Unified Interface](#unified-interface)
14
+ - [Submitting jobs to a running daemon](#submitting-jobs-to-a-running-daemon)
15
+ - [Job result schema](#job-result-schema)
16
+ - [Example: start a daemon and send a job (simple)](#example-start-a-daemon-and-send-a-job-simple)
17
+ - [Ripple](#ripple)
18
+ - [Ripple Features](#ripple-features)
19
+ - [Ripple Usage](#ripple-usage)
20
+ - [Twirl](#twirl)
21
+ - [Worm](#worm)
22
+ - [Daemon mode (background indicator)](#daemon-mode-background-indicator)
23
+ - [Requirements](#requirements)
24
+ - [Installation](#installation)
25
+ - [Universal Utilities](#universal-utilities)
26
+ - [Terminal Control](#terminal-control)
27
+ - [Completion Messages](#completion-messages)
28
+ - [Development](#development)
29
+ - [Contributing](#contributing)
30
+ - [License](#license)
31
+
11
32
  ## Unified Interface
12
33
 
13
34
  The gem provides a unified `prg` command that supports all progress indicators through subcommands:
@@ -30,12 +51,12 @@ prg twirl --message "Working..." --style dots --speed fast
30
51
 
31
52
  # Run fill directly (delegates to prg)
32
53
  fill --report --percent 50
33
- ```
34
54
 
35
55
  ### With command execution
36
56
  prg worm --command "sleep 5" --success "Completed!" --error "Failed!" --checkmark
37
57
  prg ripple "Building..." --command "make build" --success "Build complete!" --stdout
38
58
  prg twirl --command "npm install" --message "Installing packages" --style arc
59
+ prg fill --command "sleep 5" --success "Done!" --checkmark
39
60
 
40
61
  ### With start/end character decoration using --ends
41
62
  prg ripple "Loading data" --ends "[]" --style rainbow
@@ -49,158 +70,73 @@ prg worm --message "Magic" --ends "🎯🎪" --style "custom=🟦🟨🟥"
49
70
  ### Global Options
50
71
 
51
72
  - `prg --help` - Show main help
52
- - `prg --version` - Show version info
53
- - `prg --list-styles` - Show all available styles for all subcommands
54
- - `prg <subcommand> --help` - Show specific subcommand help
55
73
 
56
- ### Common Options (available for all subcommands)
74
+ Notes:
57
75
 
58
- - `--speed SPEED` - Animation speed (fast/medium/slow or f/m/s)
59
- - `--message MESSAGE` - Message to display
60
- - `--command COMMAND` - Command to execute during animation
61
- - `--success MESSAGE` - Success message after completion
62
- - `--error MESSAGE` - Error message on failure
63
- - `--checkmark` - Show checkmarks (✅ success, 🛑 failure)
64
- - `--stdout` - Output command results to STDOUT
65
- - `--ends CHARS` - Start/end characters (even number of chars, split in half)
76
+ - The CLI detaches itself (double-fork); do not append `&`. This prevents shell job notifications like “job … has ended.” The command returns immediately.
77
+ - `--stop-success` and `--stop-error` are mutually exclusive; whichever you provide determines the success state and icon if `--stop-checkmark` is set.
78
+ - The indicator clears its line on shutdown and prints the final message to STDOUT.
79
+ - `--stop-pid` is still supported for backward compatibility, but `--stop [--pid-file FILE]` is preferred.
66
80
 
67
- ### Daemon Mode (Background Progress)
81
+ ### Submitting jobs to a running daemon
68
82
 
69
- For shell scripts where you need a continuous progress indicator across multiple steps, use daemon mode. You can use named daemons or custom PID files.
83
+ When running a long-lived daemon (for example `prg worm --daemon`), you can submit additional commands to run and have their output displayed without disrupting the animation using the `prg job send` helper.
84
+
85
+ Basic usage:
70
86
 
71
87
  ```bash
72
- ### Start in background (uses default PID file)
73
- prg worm --daemon --message "Working..."
88
+ # Enqueue a command to the default daemon PID
89
+ prg job send --command "./deploy-step.sh"
74
90
 
75
- ### Start with a custom name (creates /tmp/ruby-progress/NAME.pid)
76
- prg worm --daemon-as mytask --message "Processing data..."
91
+ # Enqueue to a named daemon (creates /tmp/ruby-progress/<name>.pid)
92
+ prg job send --daemon-name mytask --command "rsync -av ./dist/ user@host:/srv/app"
77
93
 
78
- ### ... run your tasks ...
94
+ # Read command from stdin (useful in scripts)
95
+ echo "bundle exec rake db:migrate" | prg job send --stdin --daemon-name mytask
79
96
 
80
- ### Stop with a success message and checkmark (--stop-success implies --stop)
81
- prg worm --stop-success "All done" --stop-checkmark
97
+ # Wait for the job result and print the job result JSON (default timeout 10s)
98
+ prg job send --daemon-name mytask --command "./deploy-step.sh" --wait --timeout 30
99
+ ```
82
100
 
83
- ### Stop a named daemon (--stop-id implies --stop)
84
- prg worm --stop-id mytask --stop-success "Task complete!" --stop-checkmark
101
+ Behavior and file layout:
85
102
 
86
- ### Or stop with an error message and checkmark
87
- prg worm --stop-error "Failed during step" --stop-checkmark
103
+ - Jobs are written as JSON files into the daemon's job directory, which is derived from the daemon PID file. For example, a PID file `/tmp/ruby-progress/mytask.pid` maps to the job directory `/tmp/ruby-progress/mytask.jobs`.
104
+ - The CLI writes the job atomically by first writing a `*.json.tmp` temporary file and then renaming it to `*.json`.
105
+ - The daemon's job processor claims jobs atomically by renaming the job file to `*.processing`, writes a `*.processing.result` JSON file when finished, and moves processed jobs to `processed-*`.
88
106
 
89
- ### Check status at any time
90
- prg worm --status
91
- prg worm --status-id mytask
107
+ This mechanism allows you to submit many commands to a single running indicator and have their output shown in reserved terminal rows while the animation continues.
92
108
 
93
- ### Use a completely custom PID file path
94
- prg worm --daemon --pid-file /tmp/custom-progress.pid
95
- prg worm --status --pid-file /tmp/custom-progress.pid
96
- prg worm --stop-success "Complete" --pid-file /tmp/custom-progress.pid
97
- ```
109
+ ## Job result schema
98
110
 
99
- Notes:
111
+ When a job is processed the daemon writes a small JSON result file next to the claimed job with the suffix `.processing.result` containing at least these keys:
100
112
 
101
- - The CLI detaches itself (double-fork); do not append `&`. This prevents shell job notifications like “job … has ended.” The command returns immediately.
102
- - `--stop-success` and `--stop-error` are mutually exclusive; whichever you provide determines the success state and icon if `--stop-checkmark` is set.
103
- - The indicator clears its line on shutdown and prints the final message to STDOUT.
104
- - `--stop-pid` is still supported for backward compatibility, but `--stop [--pid-file FILE]` is preferred.
105
- o- [Ruby Progress Indicators](#ruby-progress-indicators)
106
- - [Unified Interface](#unified-interface)
107
- - [With command execution](#with-command-execution)
108
- - [With start/end character decoration using --ends](#with-startend-character-decoration-using-ends)
109
- - [Complex --ends patterns with emojis](#complex-ends-patterns-with-emojis)
110
- - [Start in background (uses default PID file)](#start-in-background-uses-default-pid-file)
111
- - [Start with a custom name (creates /tmp/ruby-progress/NAME.pid)](#start-with-a-custom-name-creates-tmpruby-progressnamepid)
112
- - [... run your tasks ...](#-run-your-tasks-)
113
- - [Stop with a success message and checkmark (--stop-success implies --stop)](#stop-with-a-success-message-and-checkmark-stop-success-implies-stop)
114
- - [Stop a named daemon (--stop-id implies --stop)](#stop-a-named-daemon-stop-id-implies-stop)
115
- - [Or stop with an error message and checkmark](#or-stop-with-an-error-message-and-checkmark)
116
- - [Check status at any time](#check-status-at-any-time)
117
- - [Use a completely custom PID file path](#use-a-completely-custom-pid-file-path)
118
- - [Basic text animation](#basic-text-animation)
119
- - [With style options](#with-style-options)
120
- - [Multiple styles combined](#multiple-styles-combined)
121
- - [Case transformation mode](#case-transformation-mode)
122
- - [Run a command with progress animation](#run-a-command-with-progress-animation)
123
- - [Simple progress block](#simple-progress-block)
124
- - [With options](#with-options)
125
- - [Basic spinner animation](#basic-spinner-animation)
126
- - [With command execution](#with-command-execution)
127
- - [Different spinner styles](#different-spinner-styles)
128
- - [With success/error handling](#with-successerror-handling)
129
- - [Daemon mode for background tasks](#daemon-mode-for-background-tasks)
130
- - [... do other work ...](#-do-other-work-)
131
- - [Run indefinitely without a command (like ripple)](#run-indefinitely-without-a-command-like-ripple)
132
- - [Run a command with progress animation](#run-a-command-with-progress-animation)
133
- - [Customize the animation](#customize-the-animation)
134
- - [With custom error handling](#with-custom-error-handling)
135
- - [With checkmarks for visual feedback](#with-checkmarks-for-visual-feedback)
136
- - [Control animation direction (forward-only or bidirectional)](#control-animation-direction-forward-only-or-bidirectional)
137
- - [Create custom animations with 3-character patterns](#create-custom-animations-with-3-character-patterns)
138
- - [Add start/end characters around the animation](#add-startend-characters-around-the-animation)
139
- - [Capture and display command output](#capture-and-display-command-output)
140
- - [Combine checkmarks and stdout output](#combine-checkmarks-and-stdout-output)
141
- - [Start in the background (default PID file: /tmp/ruby-progress/progress.pid)](#start-in-the-background-default-pid-file-tmpruby-progressprogresspid)
142
- - [... run your tasks ...](#-run-your-tasks-)
143
- - [Stop using the default PID file](#stop-using-the-default-pid-file)
144
- - [Use a custom PID file](#use-a-custom-pid-file)
145
- - [Stop using the matching custom PID file](#stop-using-the-matching-custom-pid-file)
146
- - [Create and run animation with a block](#create-and-run-animation-with-a-block)
147
- - [Your work here](#your-work-here)
148
- - [With custom style and forward direction](#with-custom-style-and-forward-direction)
149
- - [Or run with a command](#or-run-with-a-command)
150
- - [ASCII characters](#ascii-characters)
151
- - [Unicode characters](#unicode-characters)
152
- - [Emojis (supports multi-byte characters)](#emojis-supports-multi-byte-characters)
153
- - [Mixed ASCII and emoji](#mixed-ascii-and-emoji)
154
- - [Cursor control](#cursor-control)
155
- - [Basic completion message](#basic-completion-message)
156
- - [With success/failure indication and checkmarks](#with-successfailure-indication-and-checkmarks)
157
- - [Clear line and display completion (useful for replacing progress indicators)](#clear-line-and-display-completion-useful-for-replacing-progress-indicators)
113
+ - `id` - the job id (string)
114
+ - `status` - `"done"` or `"error"`
115
+ - `time` - epoch seconds when the job finished (integer)
158
116
 
159
- ## Table of Contents
117
+ Depending on the job handler, additional keys may be present:
118
+
119
+ - `exit_status` - the numeric process exit status (integer or nil if unknown)
120
+ - `output` - a string with the last captured lines of output (if available)
121
+ - `error` - an error message when `status` is `error`
160
122
 
161
- - [Ruby Progress Indicators](#ruby-progress-indicators)
162
- - [Unified Interface](#unified-interface)
163
- - [With command execution](#with-command-execution)
164
- - [With start/end character decoration using --ends](#with-startend-character-decoration-using---ends)
165
- - [Complex --ends patterns with emojis](#complex---ends-patterns-with-emojis)
166
- - [Table of Contents](#table-of-contents)
167
- - [Ripple](#ripple)
168
- - [Ripple Features](#ripple-features)
169
- - [Ripple Usage](#ripple-usage)
170
- - [Ripple CLI examples](#ripple-cli-examples)
171
- - [Ripple Command Line Options](#ripple-command-line-options)
172
- - [Ripple Library Usage](#ripple-library-usage)
173
- - [Twirl](#twirl)
174
- - [Twirl Features](#twirl-features)
175
- - [Twirl Usage](#twirl-usage)
176
- - [Command Line](#command-line)
177
- - [Twirl Command Line Options](#twirl-command-line-options)
178
- - [Available Spinner Styles](#available-spinner-styles)
179
- - [Worm](#worm)
180
- - [Worm Features](#worm-features)
181
- - [Worm Usage](#worm-usage)
182
- - [Command Line](#command-line-1)
183
- - [Daemon mode (background indicator)](#daemon-mode-background-indicator)
184
- - [Worm Command Line Options](#worm-command-line-options)
185
- - [Worm Library Usage](#worm-library-usage)
186
- - [Animation Styles](#animation-styles)
187
- - [Circles](#circles)
188
- - [Blocks](#blocks)
189
- - [Geometric](#geometric)
190
- - [Custom Styles](#custom-styles)
191
- - [Direction Control](#direction-control)
192
- - [Requirements](#requirements)
193
- - [Installation](#installation)
194
- - [As a Gem (Recommended)](#as-a-gem-recommended)
195
- - [From Source](#from-source)
196
- - [Development](#development)
197
- - [Universal Utilities](#universal-utilities)
198
- - [Terminal Control](#terminal-control)
199
- - [Completion Messages](#completion-messages)
200
- - [Contributing](#contributing)
201
- - [License](#license)
123
+ Example:
202
124
 
125
+ ```json
126
+ {
127
+ "id": "8a1f6c1e-4b7a-4f2c-b0a8-9e9f1c2f1a2b",
128
+ "status": "done",
129
+ "time": 1634044800,
130
+ "exit_status": 0,
131
+ "output": "Step 1 completed\nStep 2 completed"
132
+ }
133
+ ```
134
+
135
+ This file is intended for short messages and small captured output snippets (the CLI captures the last N lines). If you need larger logs, write them to a persistent file from the command itself and include a reference in the job metadata.
203
136
 
137
+ ## Example: start a daemon and send a job (simple)
138
+
139
+ Below is an example script that demonstrates starting a worm daemon, sending a job, waiting for the result, and stopping the daemon.
204
140
  ---
205
141
 
206
142
  ## Ripple
@@ -407,6 +343,15 @@ prg worm --message "Emoji ends" --ends "🎯🎪" --style "custom=🟦🟨🟥"
407
343
  ### Capture and display command output
408
344
  prg worm --command "git status" --message "Checking status" --stdout
409
345
 
346
+ You can reserve terminal rows for captured command output so the animation doesn't interleave with the script output. Use:
347
+
348
+ - `--output-position POSITION` — `above` (default) or `below` the animation
349
+ - `--output-lines N` — how many terminal rows to reserve for captured output (default: 3)
350
+
351
+ Examples:
352
+
353
+ prg worm --command "git status" --stdout --output-position above --output-lines 4
354
+
410
355
  ### Combine checkmarks and stdout output
411
356
  prg worm --command "echo 'Build output'" --success "Build complete!" --checkmark --stdout
412
357
  ```
data/bin/prg CHANGED
@@ -7,6 +7,7 @@ require 'optparse'
7
7
  require 'json'
8
8
  require 'English'
9
9
 
10
+ require_relative '../lib/ruby-progress/cli/job_cli'
10
11
  # Load extracted per-subcommand CLI modules
11
12
  require_relative '../lib/ruby-progress/cli/ripple_cli'
12
13
  require_relative '../lib/ruby-progress/cli/worm_cli'
@@ -20,6 +21,18 @@ module PrgCLI
20
21
  exit 1
21
22
  end
22
23
 
24
+ # Handle `job` subcommands early
25
+ if ARGV[0] && ARGV[0].downcase == 'job'
26
+ ARGV.shift
27
+ sub = ARGV.shift
28
+ case sub
29
+ when 'send'
30
+ JobCLI.send(ARGV)
31
+ else
32
+ puts 'job subcommands: send'
33
+ exit 1
34
+ end
35
+ end
23
36
  # Early scan: detect --ends flag and validate its argument before dispatching
24
37
  if (i = ARGV.index('--ends')) && ARGV[i + 1]
25
38
  ends_val = ARGV[i + 1]
@@ -113,6 +126,28 @@ module PrgCLI
113
126
  puts '== fill =='
114
127
  end
115
128
 
129
+ # Detach the current process into a background daemon. Uses Process.daemon
130
+ # when available, otherwise falls back to a basic fork/exit helper. This is
131
+ # intentionally simple for the test environment.
132
+ def self.daemonize
133
+ if Process.respond_to?(:daemon)
134
+ Process.daemon(true)
135
+ else
136
+ pid = fork
137
+ if pid
138
+ # parent exits so child can continue as daemon
139
+ exit(0)
140
+ else
141
+ # child: detach from controlling terminal
142
+ begin
143
+ Process.setsid
144
+ rescue StandardError
145
+ nil
146
+ end
147
+ end
148
+ end
149
+ end
150
+
116
151
  # Attempt to stop processes for the given subcommand. Return true if any
117
152
  # process was signaled/stopped; false otherwise. Keep quiet on missing
118
153
  # processes to satisfy integration tests.
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env sh
2
+ # Example: start a worm daemon, send a job, wait for result, then stop.
3
+ # This script assumes you're running from the project root and have a working
4
+ # `bin/prg` script in the repository.
5
+
6
+ set -eu
7
+
8
+ PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
9
+ PRG_BIN="$PROJECT_ROOT/bin/prg"
10
+
11
+ echo "Starting worm daemon (named 'example')..."
12
+ # prg detaches in daemon mode so no & needed
13
+ $PRG_BIN worm --daemon-as example --message "Example daemon"
14
+
15
+ sleep 0.2
16
+
17
+ echo "Sending job and waiting for result..."
18
+ $PRG_BIN job send --daemon-name example --command "echo hello; sleep 0.1" --wait --timeout 10
19
+
20
+ sleep 0.1
21
+
22
+ echo "Stopping daemon with success message..."
23
+ $PRG_BIN worm --stop-success "Example finished" --stop-checkmark --daemon-name example
24
+
25
+ echo "Done."
@@ -23,6 +23,8 @@ module RubyProgress
23
23
  stop: false,
24
24
  status: false,
25
25
  current: false,
26
+ output_position: :above,
27
+ output_lines: 3,
26
28
  report: false
27
29
  }
28
30
 
@@ -44,6 +46,20 @@ module RubyProgress
44
46
  options[:ends] = chars
45
47
  end
46
48
 
49
+ opts.separator 'Output capture:'
50
+
51
+ opts.on('-c', '--command COMMAND', 'Command to run and capture output (optional)') do |cmd|
52
+ options[:command] = cmd
53
+ end
54
+
55
+ opts.on('--output-position POSITION', 'Position to render captured output: above or below (default: above)') do |pos|
56
+ options[:output_position] = pos.to_sym
57
+ end
58
+
59
+ opts.on('--output-lines N', Integer, 'Number of output lines to reserve for captured output (default: 3)') do |n|
60
+ options[:output_lines] = n
61
+ end
62
+
47
63
  opts.separator ''
48
64
  opts.separator 'Progress Control:'
49
65
 
@@ -173,6 +189,12 @@ module RubyProgress
173
189
  opts.on('--error MESSAGE', 'Error message to display on cancellation')
174
190
  opts.on('--checkmark', 'Show checkmarks (✅ success, 🛑 failure)')
175
191
 
192
+ opts.separator ''
193
+ opts.separator ''
194
+ opts.separator 'Output capture:'
195
+ opts.on('--output-position POSITION', 'Position to render captured output: above or below (default: above)')
196
+ opts.on('--output-lines N', Integer, 'Number of output lines to reserve for captured output (default: 3)')
197
+
176
198
  opts.separator ''
177
199
  opts.separator 'Daemon Mode:'
178
200
  opts.on('--daemon', 'Run in background daemon mode')
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
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.
7
+
8
+ require 'optparse'
9
+ require 'json'
10
+ require 'securerandom'
11
+ require 'fileutils'
12
+ require_relative '../daemon'
13
+
14
+ # Job CLI helpers
15
+ #
16
+ # Exposed as `prg job send`.
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') { |v| options[:pid_file] = v }
32
+ o.on('--daemon-name NAME', 'Daemon name (maps to /tmp/ruby-progress/NAME.pid)') { |v| options[:daemon_name] = v }
33
+ o.on('--command CMD', 'Command to run') { |v| options[:command] = v }
34
+ o.on('--stdin', 'Read command from stdin (overrides --command)') { options[:stdin] = true }
35
+ o.on('--wait', 'Wait for result file and print it') { options[:wait] = true }
36
+ o.on('--timeout SECONDS', Integer, 'Timeout seconds for wait') { |v| options[:timeout] = v }
37
+ end
38
+
39
+ rest = opt.parse(argv)
40
+ options[:command] ||= rest.join(' ') unless rest.empty?
41
+ options
42
+ end
43
+ end
44
+
45
+ def self.send(argv = ARGV)
46
+ opts = Options.parse(argv)
47
+
48
+ # Resolve pid file
49
+ pid_file = if opts[:pid_file]
50
+ opts[:pid_file]
51
+ elsif opts[:daemon_name]
52
+ "/tmp/ruby-progress/#{opts[:daemon_name]}.pid"
53
+ else
54
+ RubyProgress::Daemon.default_pid_file
55
+ end
56
+
57
+ job_dir = RubyProgress::Daemon.job_dir_for_pid(pid_file)
58
+ FileUtils.mkdir_p(job_dir)
59
+
60
+ cmd = if opts[:stdin]
61
+ $stdin.read
62
+ else
63
+ opts[:command]
64
+ end
65
+
66
+ unless cmd && !cmd.strip.empty?
67
+ warn 'No command specified. Use --command or --stdin.'
68
+ exit 1
69
+ end
70
+
71
+ job_id = SecureRandom.uuid
72
+ tmp = File.join(job_dir, "#{job_id}.json.tmp")
73
+ final = File.join(job_dir, "#{job_id}.json")
74
+
75
+ payload = { 'id' => job_id, 'command' => cmd }
76
+
77
+ File.write(tmp, JSON.dump(payload))
78
+ FileUtils.mv(tmp, final)
79
+
80
+ if opts[:wait]
81
+ timeout = opts[:timeout] || 10
82
+ start = Time.now
83
+ result_path = "#{final}.processing.result"
84
+ loop do
85
+ if File.exist?(result_path)
86
+ puts File.read(result_path)
87
+ break
88
+ end
89
+ if Time.now - start > timeout
90
+ warn 'Timed out waiting for result'
91
+ exit 2
92
+ end
93
+ sleep 0.1
94
+ end
95
+ else
96
+ puts job_id
97
+ end
98
+ end
99
+ end
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'fileutils'
4
+ require 'json'
5
+ require 'securerandom'
4
6
  require_relative 'ripple_options'
7
+ require_relative '../output_capture'
5
8
 
6
9
  # Enhanced Ripple CLI with unified flags (extracted from bin/prg)
7
10
  module RippleCLI
@@ -60,13 +63,27 @@ module RippleCLI
60
63
  end
61
64
 
62
65
  def self.run_with_command(text, options)
63
- captured_output = nil
64
- RubyProgress::Ripple.progress(text, options) do
66
+ if $stdout.tty? && options[:output] == :stdout
67
+ oc = RubyProgress::OutputCapture.new(command: options[:command], lines: options[:output_lines] || 3, position: options[:output_position] || :above)
68
+ oc.start
69
+
70
+ # Create rippler and attach output capture so redraw occurs each frame
71
+ rippler = RubyProgress::Ripple.new(text, options)
72
+ rippler.instance_variable_set(:@output_capture, oc)
73
+
74
+ thread = Thread.new { loop { rippler.advance } }
75
+ oc.wait
76
+ thread.kill
77
+
78
+ captured_lines = oc.lines
79
+ captured_output = captured_lines.join("\n")
80
+ success = true
81
+ else
82
+ # Fallback to legacy capture (non-interactive / CI)
65
83
  captured_output = `#{options[:command]} 2>&1`
84
+ success = $CHILD_STATUS.success?
66
85
  end
67
86
 
68
- success = $CHILD_STATUS.success?
69
-
70
87
  puts captured_output if options[:output] == :stdout
71
88
  if options[:success_message] || options[:complete_checkmark]
72
89
  message = success ? options[:success_message] : options[:fail_message] || options[:success_message]
@@ -90,7 +107,6 @@ module RippleCLI
90
107
  pid_file = options[:pid_file] || RubyProgress::Daemon.default_pid_file
91
108
  FileUtils.mkdir_p(File.dirname(pid_file))
92
109
  File.write(pid_file, Process.pid.to_s)
93
-
94
110
  begin
95
111
  # For Ripple, re-use the existing animation loop via a simple loop
96
112
  RubyProgress::Utils.hide_cursor
@@ -102,6 +118,9 @@ module RippleCLI
102
118
  Signal.trap('TERM') { stop_requested = true }
103
119
  Signal.trap('HUP') { stop_requested = true }
104
120
 
121
+ job_dir = RubyProgress::Daemon.job_dir_for_pid(pid_file)
122
+ job_thread = Thread.new { process_daemon_jobs_for_rippler(job_dir, rippler, options) }
123
+
105
124
  rippler.advance until stop_requested
106
125
  ensure
107
126
  RubyProgress::Utils.clear_line
@@ -142,9 +161,51 @@ module RippleCLI
142
161
  end
143
162
  end
144
163
 
164
+ # stop job thread and cleanup
165
+ job_thread&.kill
145
166
  FileUtils.rm_f(pid_file)
146
167
  end
147
168
  end
148
169
 
170
+ def self.process_daemon_jobs_for_rippler(job_dir, rippler, options)
171
+ RubyProgress::Daemon.process_jobs(job_dir) do |job|
172
+ jid = job['id'] || SecureRandom.uuid
173
+ log_path = begin
174
+ File.join(File.dirname(job_dir), "#{jid}.log")
175
+ rescue StandardError
176
+ nil
177
+ end
178
+
179
+ oc = RubyProgress::OutputCapture.new(
180
+ command: job['command'],
181
+ lines: options[:output_lines] || 3,
182
+ position: options[:output_position] || :above,
183
+ log_path: log_path
184
+ )
185
+ oc.start
186
+
187
+ rippler.instance_variable_set(:@output_capture, oc)
188
+ oc.wait
189
+ captured = oc.lines.join("\n")
190
+ exit_status = oc.exit_status
191
+ rippler.instance_variable_set(:@output_capture, nil)
192
+
193
+ success = exit_status.to_i.zero?
194
+ if job['message']
195
+ RubyProgress::Utils.display_completion(
196
+ job['message'],
197
+ success: success,
198
+ show_checkmark: job['checkmark'] || false,
199
+ output_stream: :stdout
200
+ )
201
+ end
202
+
203
+ { 'exit_status' => exit_status, 'output' => captured, 'log_path' => log_path }
204
+ rescue StandardError
205
+ # ignore per-job errors; process_jobs will write result
206
+ nil
207
+ end
208
+ end
209
+
149
210
  # Options parsing moved to ripple_options.rb
150
211
  end