foruiman 0.2.0 → 0.4.0

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: 726795acd4c4991d3a924364ec484ca7ec1e7051b8c9a660a53e6b8ecd1759f8
4
- data.tar.gz: 0c24bdd2fded37d051b8bb0e771991b0284893888a89b3a461662d2cc483dee4
3
+ metadata.gz: 5c7f857660af7989aab5c7175e501361ca523780f6eb14f65f7c1620da4040c9
4
+ data.tar.gz: ad726e87ac32a31a1eb07c3a6b9dc4e7eea5f34374e5395153b0c7a73f7831ad
5
5
  SHA512:
6
- metadata.gz: 68b7fa94a095c09fc3ed636165ee88db37640d24aa2ede0b4d2c417680cf2655910b881ad2180116babc701949c91cc9f751f2e0b67f222d081f71ca9153a178
7
- data.tar.gz: 22ec4c35cd32fd23d897ca2de19d72d5c219b53d0b86019f3fc3f562b3436371bd353ca5119aac2d4e1c559c68ddca37984c53b68e5e036cc12538e04f71321e
6
+ metadata.gz: 2256d813401208d3c672a391cc7ef8670f81cb4cd6ac9d84a22dc510fb612edc52863612d3d9310d84d9a4de05e239339c2a03b8a6ff87469257e81bc016e0c7
7
+ data.tar.gz: 4074ebafd99da6d35bdb99b2fa3aad0ce24eac452ff763e13180bb554db8135a23efdfb2cdf436da8fe001838f1db861445efa43a7e1be42a1f85ba95745c715
data/CHANGELOG.md CHANGED
@@ -1,6 +1,24 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.4.0
4
+
5
+ - Forward USR1/USR2 to child process groups in both TUI and plain mode, preserving
6
+ application handlers and the supervisor's independent-process policy.
7
+ - Fix inherited terminal stdin in plain mode while retaining descendant cleanup.
8
+ - Validate ports after process selection, preserving each entry's original offset.
9
+ - Allow a Procfile process named `all` with its own logs and controls, separate
10
+ from the aggregate tab.
11
+
12
+ ## 0.3.0
13
+
14
+ - Add `.foreman` YAML defaults with explicit CLI overrides and validated options.
15
+ - Add `run` for one-off commands and named Procfile entries, preserving terminal
16
+ input, argument boundaries, signals, and exit status.
17
+ - Match Foreman's explicit environment-file replacement, comma-separated loading,
18
+ environment `PORT` fallback, and Procfile/root path handling. See the migration
19
+ notes for changes from 0.2.
20
+ - Add `-t` / `--timeout` and `--exit-on all|any|failure`. Automatic group shutdown
21
+ preserves the triggering exit code and cleans up descendants and pending restarts.
4
22
 
5
23
  ## 0.2.0
6
24
 
data/README.md CHANGED
@@ -38,6 +38,8 @@ Useful commands:
38
38
  foruiman start -f Procfile.dev # use another Procfile
39
39
  foruiman start web # run one process
40
40
  foruiman start --no-tui # stream plain logs
41
+ foruiman run bin/rails console # one-off command with the app environment
42
+ foruiman start --exit-on any # stop the group when a process exits
41
43
  foruiman check # validate without starting
42
44
  foruiman --version
43
45
  ```
@@ -54,18 +56,31 @@ interrupts the selected process. Debugger tab completion is not forwarded.
54
56
  Press `s` to stop the selected process, or all processes from `all`; on a stopped
55
57
  process tab, `s` starts it again.
56
58
 
59
+ The aggregate `all` tab uses shortcut `0`. A process named `all` has its own
60
+ numbered tab and independent controls. USR1 and USR2 sent to Foruiman are forwarded
61
+ to every child process group for application-defined handling.
62
+
63
+ In plain mode, children inherit stdin, including terminal input. Multiple children
64
+ reading stdin share that stream; use TUI input controls to target one process.
65
+
57
66
  ## Options
58
67
 
59
68
  | Option | Default | Purpose |
60
69
  | --- | --- | --- |
61
70
  | `-f`, `--procfile FILE` | `Procfile` | Procfile to run |
62
- | `-d`, `--root DIR` | Current directory | Working directory |
63
- | `-e`, `--env FILE` | None | Additional environment file |
71
+ | `-d`, `--root DIR` | Procfile directory | Working directory |
72
+ | `-e`, `--env FILES` | `.env` | Comma-separated files, loaded in order instead of `.env` |
64
73
  | `--no-dotenv` | `.env` enabled | Skip `.env` |
65
- | `-p`, `--port PORT` | `5000` | Base port; entries increment by 100 |
74
+ | `-p`, `--port PORT` | Environment `PORT`, then `5000` | Base port; entries increment by 100 |
75
+ | `-t`, `--timeout SECONDS` | `5` | Grace period before forced shutdown |
76
+ | `--exit-on all\|any\|failure` | `all` | Keep peers running, stop on any exit, or stop on failure |
66
77
  | `--log-lines N` | `10000` | Retained records per process and in `all` |
67
78
  | `--no-tui` | TUI when interactive | Force plain output |
68
79
 
80
+ Set defaults in a `.foreman` YAML file in the invocation directory; CLI flags
81
+ override them. See [compatibility and migration notes](docs/COMPATIBILITY.md) for
82
+ path rules, supported settings, and changes from 0.2.
83
+
69
84
  ## Keyboard
70
85
 
71
86
  | Key | Action |
@@ -105,7 +120,9 @@ bundle exec ruby script/smoke_gem.rb
105
120
  Releases use RubyGems trusted publishing. Configure the publisher once with
106
121
  repository `Nuzair46/foruiman`, workflow `release.yml`, and environment `release`.
107
122
 
108
- To publish, update `lib/foruiman/version.rb` on `main`, open the
123
+ To publish, update `lib/foruiman/version.rb` and `CHANGELOG.md`, then run
124
+ `bundle install` to update the version in `Gemfile.lock`. Commit all three files
125
+ to `main`; CI requires the lockfile to match the gem version. Open the
109
126
  [Release workflow](https://github.com/Nuzair46/foruiman/actions/workflows/release.yml),
110
127
  choose **Run workflow**, and select `main`. GitHub Actions runs the full CI suite,
111
128
  creates the version tag, and publishes the gem to RubyGems.org.
data/docs/ARCHITECTURE.md CHANGED
@@ -3,11 +3,20 @@
3
3
  `Procfile` retains Foreman's ordered parser/writer API with strict file validation.
4
4
  `Env` retains its quoting rules and adds a non-mutating precedence merge. `Process`
5
5
  wraps `/bin/sh -c` with a new process group, two output streams, and configurable stdin.
6
- `CLI < Thor` validates the full configuration before starting the engine.
6
+ The shell catches USR1/USR2 while waiting so group forwarding does not kill the
7
+ wrapper ahead of a signal-aware application. Executed programs receive normal
8
+ signal dispositions and can install their own handlers.
9
+ `CLI < Thor` validates configuration and selected port allocations before opening
10
+ the TUI or starting children. `check` validates every allocation. Original Procfile
11
+ offsets survive selection; embedded `start` validates its entire batch before
12
+ launching any child, and `restart` validates before changing process state.
13
+ `Configuration` safely loads `.foreman` defaults, merges explicit CLI values,
14
+ resolves Foreman-compatible paths and environments, and validates numeric options.
15
+ `run` uses `exec` for one-off commands with direct terminal IO and exact exit status.
7
16
 
8
17
  `Engine` owns registration, PID/group tracking, nonblocking pipes, child reaping,
9
18
  and lifecycle transitions. A single caller thread drives all mutations. Signal
10
- handlers only set a flag and wake a self-pipe. The loop reaps only its own direct
19
+ handlers only queue the signal and wake a self-pipe. The loop reaps only its own direct
11
20
  children, services bounded reads in round-robin order, checks TERM deadlines, and
12
21
  starts pending replacements once old groups and pipes are finished. Group existence
13
22
  is tracked independently of leader status; Linux `/proc` distinguishes running
@@ -44,15 +53,25 @@ end
44
53
  ```
45
54
 
46
55
  Call engine methods and event listeners on the driving thread; this is not a
47
- cross-thread messaging API. `run` handles INT/TERM/HUP and restores existing
48
- handlers. An embedding loop that drives `tick` directly owns its signal handling.
56
+ cross-thread messaging API. `run` handles INT/TERM/HUP shutdown, forwards USR1/USR2
57
+ to owned groups, and restores existing handlers. An embedding loop that drives
58
+ `tick` directly owns its signal handling.
49
59
  `close` disconnects observers so a failed renderer/output consumer cannot prevent
50
- process cleanup. `term_timeout:` exists for deterministic embedded tests; CLI
51
- shutdown always uses five seconds. `state(name)` exposes lifecycle state for
60
+ process cleanup. `term_timeout:` controls the shutdown grace period (CLI `-t`,
61
+ default five seconds). `exit_on: :all` retains independent processes; `:any` or
62
+ `:failure` requests group shutdown after a qualifying natural exit and preserves
63
+ its exit code. Intentional stops/restarts do not trigger the policy. `state(name)` exposes lifecycle state for
52
64
  rendering; callers should not mutate it.
53
65
 
54
- Children inherit the configured input stream in plain and embedded use. Before
55
- startup, `manage_input!` gives each child a dedicated pseudo-terminal while the
66
+ Children inherit the configured input stream in plain and embedded use. For a
67
+ terminal input stream, `Process` forks, calls `setsid`, and execs the shell so reads
68
+ do not stop with SIGTTIN in a background group of the supervisor's session. The
69
+ child keeps its inherited terminal descriptor and its own process group; a pipe
70
+ reports exec failures synchronously. This does not give the child a controlling
71
+ terminal for `/dev/tty` or shell job control. Multiple inherited readers compete
72
+ for the same input. Nonterminal input keeps the ordinary `spawn` path.
73
+
74
+ Before startup, `manage_input!` gives each child a dedicated pseudo-terminal while the
56
75
  caller retains the real terminal; `write_input(name, bytes)` forwards input,
57
76
  `interrupt_process(name)` sends SIGINT to that process group, and `resize_inputs` keeps
58
77
  the pseudo-terminals sized with the UI. The TUI uses this mode so watch commands
@@ -65,7 +84,10 @@ line erasure before recording child output; screen controls remain suppressed.
65
84
 
66
85
  `LogStore` uses per-process `Ring` instances and an aggregate `Ring`, each with O(1)
67
86
  append, eviction, record replacement, and identity lookup. Immutable `Data` records
68
- are shared. A partial record has one sequence identity; completing it replaces
87
+ are shared. `logs.all` or `logs[:all]` accesses the aggregate; `logs["all"]` accesses
88
+ a process actually named `all`. The TUI uses symbol `:all` for aggregate state and
89
+ string names for processes, with `State#aggregate?` defining control scope.
90
+ A partial record has one sequence identity; completing it replaces
69
91
  retained references without re-inserting an already evicted aggregate record.
70
92
  Sequence order represents first observation, not line completion time. Actual
71
93
  memory use depends on line length and ring capacity; retained text is bounded by
data/docs/ASSUMPTIONS.md CHANGED
@@ -9,7 +9,8 @@ The missing RFC was requested during implementation; these choices fill the gaps
9
9
  - Foreman's `-f`, `-d`, `-e`, `-p` aliases; `--log-lines`, `--no-tui`, `--no-dotenv`.
10
10
  - Default capacity 10,000 records per ring.
11
11
  - Process tabs in entry order, then `all`; select `all` initially.
12
- - `all` reserved as a process name to avoid an ambiguous aggregate tab.
12
+ - The aggregate tab has a separate internal identity, so `all` is also a valid
13
+ process name. This replaces the original reserved-name assumption.
13
14
  - Vim-style and arrow navigation, digits, `f`, Space, `r`/`R`, `s`/`S`, `?`, `q`.
14
15
  - Generated `PS=name.1`, as in Foreman; no other generated variables.
15
16
  - Relative file paths resolved against invocation directory or explicit `-d`.
@@ -1,26 +1,85 @@
1
1
  # Intentional differences from Foreman
2
2
 
3
- Foruiman is a fork-derived MVP, not a drop-in replacement for every Foreman feature.
3
+ Foruiman supports common Foreman development workflows. It is not a drop-in
4
+ replacement for every Foreman feature.
5
+
6
+ See the [2026-09-27 compatibility audit](FOREMAN_AUDIT.md) for issue-backed gaps,
7
+ direct comparisons with Foreman 0.90.0, and the implementation scope for compatible
8
+ development workflows in this TUI. Exporters, the Foreman Ruby API, older
9
+ runtimes, and exact legacy presentation are excluded from that work. Its
10
+ modernization assessment recommends retaining consistent app-root execution,
11
+ strict configuration/env-file validation, and meaningful TUI options. The four
12
+ selected improvements are implemented in the unreleased changes below; the audit
13
+ preserves the original findings separately from their implementation status.
4
14
 
5
15
  | Area | Foruiman behavior |
6
16
  | --- | --- |
7
- | Identity | `foruiman` gem/executable, `Foruiman` namespace, version 0.2.0 |
17
+ | Identity | `foruiman` gem/executable, `Foruiman` namespace, version 0.3.0 |
8
18
  | Runtime | Ruby 3.2+, POSIX process groups; Linux CI |
9
- | CLI | Thor-based `start [PROCESS]`, `check`, `version`, and `help` |
10
- | Excluded features | No export, scaling/formation, `run`, `.foreman` YAML, custom shutdown timeout, forced color, or timestamp toggle |
11
- | Procfile validation | Reject malformed lines, duplicates, empty commands, and reserved `all`; report line numbers |
12
- | Working directory | Invocation directory unless `-d`; moving a Procfile does not change child cwd |
13
- | Environment | Optional `.env` then explicit env file; parent `ENV` stays untouched |
14
- | Ports | 5000 or `-p`, plus 100 per original entry; `PORT` in the environment is overridden |
19
+ | CLI | Thor-based `start [PROCESS]`, `run COMMAND [ARGS...]`, `check`, `version`, and `help` |
20
+ | Excluded features | No export, scaling/formation, forced color, or timestamp toggle |
21
+ | Procfile validation | Reject malformed lines, duplicates, and empty commands; report line numbers. A process named `all` is valid |
22
+ | Working directory | Procfile directory unless `-d`; explicit `-f` and `-e` paths resolve from invocation |
23
+ | Environment | Explicit `-e file1,file2` replaces default `.env` loading; later files win; parent `ENV` stays untouched |
24
+ | Ports | `-p` or `.foreman` port, then loaded `PORT`, then 5000; plus 100 per original entry. `start PROCESS` validates only that allocation; `check` validates all |
15
25
  | Expansion | `/bin/sh -c` performs shell expansion; no Ruby string substitution |
16
26
  | Instances | Exactly one instance per entry; `PS=name.1` |
17
- | Failures | Independent process failures; peers keep running |
27
+ | Failures | Independent by default; `--exit-on any` stops on any natural exit, `failure` only on an unsuccessful exit |
18
28
  | Restart | Stop only the affected group, wait for descendants and output, then replace |
19
- | Shutdown | TERM, five-second grace, KILL; track groups after leader exit; restore prior signal handlers |
29
+ | Shutdown | TERM, configurable `-t` grace (default five seconds), KILL; track groups after leader exit; restore prior signal handlers |
30
+ | Signals | USR1/USR2 reach every owned process group; applications choose how to handle them. INT/TERM/HUP request orderly shutdown |
20
31
  | Output | Separate stdout/stderr metadata; bounded logs and live partial records |
21
- | Terminal | Tabs, independent scroll/follow, restart, start/stop, and selected-process input controls |
22
- | Plain exit | Wait for all processes; status 0/1; explicit orderly shutdown returns 0 |
23
- | Interactive exit | Remain open after all processes exit; quit explicitly |
32
+ | Terminal | Tabs, independent scroll/follow, restart, start/stop, and selected-process input controls; aggregate `0: all` is separate from a process named `all` |
33
+ | Plain stdin | Inherited stdin supports pipes, terminal line input, raw reads, and EOF; concurrent readers share the stream |
34
+ | Plain exit | Default status 0/1; automatic group shutdown preserves its triggering exit code (128 + signal for a signal exit); explicit orderly shutdown returns 0 |
35
+ | Interactive exit | Remain open after all processes exit by default; automatic shutdown policies close the TUI after cleanup |
36
+
37
+ ## Configuration and paths
38
+
39
+ Like Foreman, `.foreman` is read from the invocation directory. Use long option
40
+ names; CLI flags override YAML values. Supported keys are `procfile`, `root`, `env`,
41
+ `port`, `timeout`, `dotenv`, `tui`, `log-lines`, and `exit-on` (underscores also work).
42
+ Unsupported keys, including `formation`, produce an error instead of being ignored.
43
+ YAML object tags and aliases are not supported.
44
+
45
+ ```yaml
46
+ procfile: Procfile.dev
47
+ port: 3000
48
+ timeout: 10
49
+ exit-on: any
50
+ ```
51
+
52
+ An explicit `-f` path is relative to invocation, even when `-d` is supplied. Without
53
+ `-f`, the Procfile is `<root>/Procfile`. Without `-d`, child commands run in the
54
+ Procfile's directory. Default `.env` loading follows Foreman's CLI: use the explicit
55
+ `-d` directory when supplied, otherwise the invocation directory. An inferred root
56
+ from `-f` does not move the default `.env` lookup. Explicit `-e` paths always resolve
57
+ from invocation. `--no-dotenv` disables only default `.env` loading.
58
+
59
+ ## One-off commands
60
+
61
+ `foruiman run bin/rails console` uses the same environment-file rules and passes
62
+ stdin, stdout, and stderr directly to the command. No Procfile is required. A
63
+ single argument matching a Procfile entry runs that entry's shell command.
64
+ Other commands retain their exact argument boundaries. For shell operators, use
65
+ `foruiman run sh -c 'command1 && command2'`. Put Foruiman options before the command;
66
+ options after the command belong to that command.
67
+
68
+ `run` replaces Foruiman with the command, preserving signals and exit status. It
69
+ does not generate per-process `PORT` or `PS` values or open the TUI.
70
+
71
+ ## Migrating from 0.2
72
+
73
+ - `-e custom.env` now replaces `.env`. Use `-e .env,custom.env` to retain layering.
74
+ - Environment `PORT` now selects the base port. Use `-p 5000` to retain the old default.
75
+ - A nested `-f` changes the default child working directory. Add `-d .` to keep
76
+ invocation as the working directory. With `-d app`, an old `-f config/Procfile.dev`
77
+ becomes `-f app/config/Procfile.dev` when that file lives inside `app`.
78
+ - A `.foreman` file now supplies defaults. Remove unsupported keys before using it.
79
+
80
+ Use `--exit-on any` for Foreman's stop-on-first-exit behavior. Intentional TUI
81
+ stops and restarts do not trigger that policy. Exact Foreman log formatting and
82
+ its Ruby embedding API remain outside compatibility.
24
83
 
25
84
  Full-screen child terminal applications and background daemonization are outside
26
85
  this release. Remote process control, persistence across Foruiman sessions,
@@ -0,0 +1,297 @@
1
+ # Foreman compatibility audit — 2026-09-27
2
+
3
+ **Scope: Foreman development workflows in a TUI Procfile runner.** The target
4
+ is to reuse Procfiles, environment files, applicable `.foreman` settings, and
5
+ `start` / `run` commands on Foruiman's supported Ruby/POSIX platforms. The TUI's
6
+ independent processes, restarts, input controls, bounded logs, and presentation
7
+ remain intentional behavior.
8
+
9
+ **Original modernization recommendation:** drop three of the nine proposed ports in
10
+ full, simplify two others, and treat formation as an optional product feature.
11
+ Prioritize signal forwarding, working terminal stdin, and selected-process port
12
+ validation; removing the process-name collision with `all` is a useful follow-up.
13
+ The table below assesses all nine items individually.
14
+
15
+ ## Implementation status
16
+
17
+ The selected work is implemented in the unreleased changes:
18
+
19
+ - **1 — USR1/USR2:** forward to owned process groups in TUI and plain mode, including
20
+ descendants. Signal-aware applications and their waiting shell wrappers stay
21
+ alive; applications without a handler retain the default signal behavior.
22
+ - **2 — Plain terminal stdin:** children can read lines, raw bytes, and EOF from
23
+ inherited terminal stdin. Isolated child sessions preserve process-group cleanup.
24
+ This supplies terminal IO, not a new controlling terminal or shell job control.
25
+ - **6 — Process named `all`:** process logs, input, and controls are separate from
26
+ the aggregate tab, which keeps shortcut `0`.
27
+ - **8 — Selected ports:** validate only the selected process at startup without
28
+ changing its original +100 offset. Whole-app startup and `check` still reject
29
+ invalid allocations before launching children. Positive base ports remain required.
30
+
31
+ Regression coverage is in [terminal integration](../spec/integration/terminal_spec.rb),
32
+ [supervisor integration](../spec/integration/supervisor_spec.rb),
33
+ [CLI](../spec/foruiman/cli_spec.rb), and [TUI](../spec/foruiman/tui_spec.rb) specs.
34
+ The remaining recommendations retain existing behavior or defer formation.
35
+
36
+ The baseline measurements and original assessment below describe the pre-fix
37
+ version. They are preserved as audit evidence, not claims of current failures.
38
+
39
+ ## Baseline and evidence
40
+
41
+ - Foruiman: commit `a2f8ebb3a81e4f026284f78c5a1bd615f3f3068e`, version 0.3.0.
42
+ - Foreman: installed gem 0.90.0, also the latest version returned by the
43
+ [RubyGems API](https://rubygems.org/api/v1/versions/foreman/latest.json) at audit time.
44
+ - Upstream `main`: [`f65ddba83932bd4670e014389d6e27ea1e20b469`](https://github.com/ddollar/foreman/tree/f65ddba83932bd4670e014389d6e27ea1e20b469),
45
+ matching our [recorded import](UPSTREAM.md).
46
+ - Execution environment: Linux, MRI Ruby 3.4.5. The existing RSpec suite passed:
47
+ **149 examples, 0 failures**, seed 59308.
48
+ - Ran **56 paired CLI probes**, plus **one paired controlling-terminal input
49
+ reproduction**. Each runner used a fresh temporary directory. Probes captured
50
+ stdout, stderr, exit status, termination signal, and timeouts. Some probes
51
+ deliberately used invalid input; this is not a compatibility percentage.
52
+ - Retrieved all **442 upstream issues: 53 open, 389 closed**, excluding PRs.
53
+ Screened titles, read all open issue bodies and relevant closed reports, and
54
+ followed selected resolution comments. This is not a claim to have reproduced
55
+ every historical report or read every comment.
56
+ - The [Foruiman tracker](https://github.com/Nuzair46/foruiman/issues) contained no
57
+ standalone issues and five closed PRs. [PR #5](https://github.com/Nuzair46/foruiman/pull/5)
58
+ explicitly excluded formation, export, exact log formatting, and USR forwarding.
59
+
60
+ The [Foreman manual](https://ddollar.github.io/foreman/), pinned implementation,
61
+ actual 0.90.0 behavior, and issue history were compared. An open issue is evidence
62
+ of a reported problem or requested feature, not proof that it still exists in
63
+ 0.90.0. Historical Foreman releases also differ from each other.
64
+
65
+ The runtime comparison uses the [upstream CI matrix](https://github.com/ddollar/foreman/blob/f65ddba83932bd4670e014389d6e27ea1e20b469/.github/workflows/ci.yml)
66
+ and [Foreman CLI/engine source](https://github.com/ddollar/foreman/tree/f65ddba83932bd4670e014389d6e27ea1e20b469/lib/foreman),
67
+ not an assumption that an unrestricted gemspec proves every Ruby version works.
68
+
69
+ ## Original modernization assessment of the nine items
70
+
71
+ The goal is a predictable development supervisor with a TUI. Existing useful
72
+ workflows matter; reproducing incidental parsing and CLI behavior is optional.
73
+ Dropping a port means retaining a documented difference, not claiming parity.
74
+
75
+ | Original item | Recommendation | Reason and intended contract |
76
+ | --- | --- | --- |
77
+ | 1. USR1/USR2 forwarding | **Keep the fix.** | These signals currently terminate the shared supervisor instead of reaching children. The TUI does not replace application-defined signal handling. Forward the signals without closing the supervisor; do not reinterpret them as TUI restart shortcuts. [Engine](../lib/foruiman/engine.rb), upstream [#673](https://github.com/ddollar/foreman/issues/673). |
78
+ | 2. Terminal stdin in `--no-tui` | **Keep the fix.** | A supported execution mode should allow children to read its controlling terminal. Correct the process-group/input interaction while preserving descendant cleanup. This is a correctness problem, not a legacy UI preference. [Process spawning](../lib/foruiman/process.rb). |
79
+ | 3. Formation, scaling and exclusions | **Defer full formation unless it is a product requirement.** | Multiple instances and startup exclusions remain useful capabilities; they are not obsolete just because the interface has tabs. They add instance identity, controls and port allocation work. Existing `start PROCESS` covers a single selected process. TUI stop buttons are not a substitute for excluding a process before it starts. If `-m` is implemented, preserve its counts/exclusions and default-zero semantics rather than silently changing the meaning of an existing flag. Upstream [#398](https://github.com/ddollar/foreman/issues/398). |
80
+ | 4. Reusing `.foreman` defaults | **Keep supported settings; drop the proposed permissiveness.** | Continue honoring supported env/root/port/timeout defaults and CLI overrides. There is no need to silently accept export-only `app`, `user`, `log`, `run`, or `template` keys, or treat mistyped/null values as absent. Retain clear validation and document removing unsupported keys during migration. This avoids suggesting a setting has an effect when it does not. [Configuration](../lib/foruiman/configuration.rb). |
81
+ | 5. Arbitrary `run` working directory | **Do not port Foreman's special case.** | Keep the current consistent application-root cwd for named and arbitrary commands. `-d` has one meaning, and relative commands work relative to the configured app. Foreman's arbitrary-command invocation-cwd behavior is a migration difference, not a correctness requirement for this product. Use `run -d . ...` when invocation cwd is wanted; this also selects invocation-root default `.env` lookup. An explicit `-e` can preserve a different env-file choice. [CLI](../lib/foruiman/cli.rb). |
82
+ | 6. Valid process name `all` | **Keep as a useful design improvement, after correctness fixes.** | An aggregate tab need not prevent a real process from being named `all`. Use a separate internal identity for the aggregate view. This removes an unnecessary UI/name collision without adopting permissive parsing for malformed or duplicate entries. The current explicit reserved-name error remains an acceptable documented migration restriction until changed. [Procfile](../lib/foruiman/procfile.rb). |
83
+ | 7. Legacy presentation-option acceptance | **Do not add acceptance-only compatibility flags.** | Do not add `-c`, `--color`, or `--timestamp` switches solely to accept and ignore them. The TUI owns presentation. Keep a clear unsupported-option error; if color/timestamp control is later useful in the plain fallback, implement it as a real feature with documented behavior. |
84
+ | 8. Port allocation for selected processes | **Fix selection validation; defer port-zero support.** | `start web -p 65500` should not fail because an unselected worker would receive 65600. Validate selected instances and preserve original offsets. Do not port arbitrary `to_i` coercion. Base port zero is a separate allocation policy: the +100 rule would give the second type 100, so accepting zero is not an automatic-port policy for the whole app. Keep positive-port validation until zero has an explicit use case and defined semantics. [Engine](../lib/foruiman/engine.rb). |
85
+ | 9. Environment-file option edge cases | **Do not port permissive list parsing.** | Keep rejecting `-e ''` and `-e custom.env,`. Use the existing `--no-dotenv` to disable default loading, and `-e custom.env` for a nonempty list. `--no-dotenv` still permits explicit `-e` files, including a configured `.foreman` env setting; remove that explicit setting too if the intent is to load no file. Preserve precedence and valid multiple-file support. [Configuration](../lib/foruiman/configuration.rb). |
86
+
87
+ This leaves **three correctness fixes**, **one process-name improvement**, and
88
+ **formation as a separate optional feature**. The other recommendations preserve
89
+ current behavior and require clear migration documentation, not porting code.
90
+
91
+ ## Differences excluded from this version's compatibility work
92
+
93
+ The wider exclusions below remain in addition to the recommendations above.
94
+ They do not imply that missing features exist or that an observed difference was
95
+ incorrect.
96
+
97
+ | Exclude | Treatment |
98
+ | --- | --- |
99
+ | Exporters and custom export templates | No systemd, upstart, launchd, runit, inittab, bluepill, daemon or supervisord export work. Keep clear errors for unsupported config keys and document removing them during migration. |
100
+ | Ruby embedding/extension API and old executable identity | Keep the `Foruiman` namespace and `foruiman` gem/executable. No `Foreman::*` adapter, replacement gem identity, or requirement to satisfy existing `gem list -i foreman` checks. Changing `bin/dev` to invoke Foruiman is part of migration. |
101
+ | Older Ruby and native Windows support | Keep the existing Ruby 3.2+ and POSIX scope. Platform expansion is separate work. Linux tests do not establish macOS correctness; verify any platform-specific claim separately. |
102
+ | Exact log format, raw-log transport and cross-stream ordering | Tabs, timestamps, stream labels, lifecycle text, colors and separate stdout/stderr metadata can differ. TUI control-sequence interpretation, bounded records and long-line splitting are intentional. Byte-identical plain output, raw JSON transport and exact merged ordering are not compatibility goals for this version. |
103
+ | Default stop-on-first-exit and aggregate exit status | Keep independent processes, isolated restarts, remaining open after children exit, aggregate 0/1 completion status, and explicit user quit returning 0. Retain the existing `--exit-on any` option and verify its documented triggering-code behavior. There is no unique Foreman-style triggering process under the independent default. |
104
+ | No-argument help, `tree`, and exact diagnostics | Keep default `start`, full-screen interaction, shortcuts, and current help/error wording and streams. Do not restore `tree` solely for Thor parity. |
105
+ | Permissive parsing and `check` behavior | Keep clear validation of malformed/duplicate/empty entries and the broader environment/port checks documented for `check`. The `all` name collision is resolved without relaxing those checks. |
106
+ | Upstream expansion and signal-status quirks | Keep shell quoting/variable boundaries and direct `run` exec. Do not copy substring replacement that expands single-quoted variables or corrupts `$FOOBAR`, return success for a signaled command just because upstream does, or reject arbitrary `run` solely because an unrelated Procfile is empty. |
107
+ | Production init, escaped daemons and unrelated feature requests | No PID 1 init guarantee, daemon management, remote sessions, automatic reload/sequencing, or features belonging to Puppet Foreman/node-foreman. Keep cleanup of owned development process groups. |
108
+
109
+ ## Important details from the reproductions
110
+
111
+ **Terminal input is a real regression, independent of the TUI.** The stdin
112
+ reproduction used a controlling terminal created by `PTY.spawn`, not just a PTY
113
+ file descriptor. The child wrote `READY`, then called `STDIN.gets`. Foreman
114
+ printed `GOT:hello` and exited normally. Foruiman's background process group
115
+ stopped on terminal input; after two seconds the harness requested shutdown and
116
+ the configured timeout escalated to KILL. Existing terminal specs mostly use
117
+ `PTY.open` plus `Process.spawn`, which does not establish that slave as the
118
+ child's controlling terminal. Add a controlling-terminal regression test before
119
+ changing process group or input handling.
120
+
121
+ **Excluded evidence: exact signal exit parity.** For a direct
122
+ `ruby child.rb` that sends itself TERM, Foreman returned 0. Foruiman's extra shell
123
+ reported 143 internally and the default policy returned 1. For `run ruby
124
+ child.rb`, Foreman returned 0 while Foruiman itself was terminated by TERM
125
+ (`Process::Status#termsig == 15`). A shell may display 143 for a signaled command,
126
+ but wait-status consumers can distinguish it from an ordinary exit 143. Foreman
127
+ 0.90.0's treatment of a directly signaled child is an upstream quirk, not a
128
+ recommendation to hide failures.
129
+
130
+ **Excluded evidence: upstream command-expansion quirks.** With
131
+ `.env` containing `FOO=hello` and `FOOBAR=world`:
132
+
133
+ | Procfile command | Foreman child output | Foruiman child output |
134
+ | --- | --- | --- |
135
+ | `printf '%s\n' '$FOO'` | `hello` | `$FOO` |
136
+ | `printf '%s\n' "$FOOBAR"` | `helloBAR` | `world` |
137
+
138
+ With `FOO=$(printf expanded)` and `printf '%s\n' "$FOO"`, Foreman printed
139
+ `expanded`; Foruiman printed the literal `$(printf expanded)`. Foruiman's normal
140
+ shell semantics are retained for this TUI scope; this difference is documented
141
+ and is not an implementation task.
142
+
143
+ **Port limits can reject an otherwise usable selected process.** This is not
144
+ only about invalid port strings: 65500 is valid for `web`, but validation of an
145
+ unselected second entry rejects the entire command. Formation implementation
146
+ would need to allocate selected instances without renumbering their original
147
+ Procfile positions. The selection-validation bug can be fixed independently of
148
+ formation support.
149
+
150
+ **No signal orphan was observed in the dedicated USR1 cleanup probe.** The
151
+ supervisor still terminated instead of forwarding. Report that confirmed
152
+ failure without assuming every such termination leaves a live child behind.
153
+
154
+ ## Minimal reproductions
155
+
156
+ Use a fresh directory for each fixture. Run the listed arguments with Foreman
157
+ 0.90.0 and then Foruiman 0.3.0. Use `--no-tui` on Foruiman when inspecting a short-lived command without
158
+ the persistent TUI. Compare process behavior, cwd, environment and argument
159
+ acceptance. These reproduce the original findings, including differences now
160
+ recommended for retention; they are not all failing acceptance criteria. Exact
161
+ log text and aggregate status under independent supervision are outside the
162
+ scoped comparison.
163
+
164
+ | Fixture | Command arguments | Observed difference |
165
+ | --- | --- | --- |
166
+ | `Procfile`: `web: echo hello` | `start -m web=2` | Two instances versus unknown switch. |
167
+ | Same Procfile; `.foreman`: `app: demo` | `start` | Runs versus unsupported config key. |
168
+ | Create empty `app/` directory; no Procfile | `run -d app ruby -e 'puts Dir.pwd'` | Invocation directory versus `app/`. |
169
+ | `Procfile`: `all: echo hello` | `start` | Runs versus reserved-name error. |
170
+ | `Procfile`: `web: echo hello` | `start -c` or `start --no-timestamp` | Accepted versus unknown switch. |
171
+ | `Procfile`: `web: echo hello`, then `worker: echo worker` on the next line | `start web -p 65500` | Runs web versus rejecting an unselected worker's port. |
172
+ | `.env`: `FOO=file`, with inherited `FOO=parent` | `run -e '' ruby -e 'puts ENV["FOO"]'` | Prints parent versus rejecting empty env option. |
173
+
174
+ For terminal input, create `reader.rb`:
175
+
176
+ ```ruby
177
+ STDOUT.sync = true
178
+ puts "READY"
179
+ puts "GOT:#{STDIN.gets}"
180
+ ```
181
+
182
+ Use `web: ruby reader.rb` as the Procfile. In a real foreground terminal, run
183
+ `foreman start -t 0.1`, type `hello` and Enter after READY. Repeat with
184
+ `foruiman start --no-tui -t 0.1`. The first prints `GOT:hello`; the second stops
185
+ the reader. Ctrl-C requests shutdown. A pipe-fed stdin test will not reproduce
186
+ this controlling-terminal problem.
187
+
188
+ For signal forwarding, create `signals.rb`:
189
+
190
+ ```ruby
191
+ STDOUT.sync = true
192
+ Signal.trap(:USR1) { puts "RECEIVED USR1" }
193
+ Signal.trap(:USR2) { puts "RECEIVED USR2" }
194
+ puts "READY"
195
+ sleep
196
+ ```
197
+
198
+ Use `web: ruby signals.rb` as the Procfile. After READY, send `kill -USR1 PID`
199
+ from another terminal to the **supervisor's** PID. Foreman prints the forwarded
200
+ message and remains running; Foruiman terminates. Repeat in a fresh invocation
201
+ for USR2. Terminate a surviving Foreman supervisor normally after observing it.
202
+
203
+ ## Behaviors already supported
204
+
205
+ The paired probes confirmed common environment precedence: inherited variables
206
+ are available, `.env` overrides them, explicit `-e first,last` replaces the
207
+ default `.env`, and later explicit files win. CLI port overrides `.foreman`,
208
+ loaded `PORT` and inherited `PORT`. Selecting the second process retains its
209
+ original +100 port offset and `PS=worker.1`.
210
+
211
+ Nested `-f` moves the supervised/named-command working directory while default
212
+ `.env` still comes from invocation unless `-d` is supplied. Named `run` uses the
213
+ Procfile root. Arbitrary `run` works without a Procfile, preserves argument
214
+ boundaries and child switches, and preserves ordinary nonzero exit codes.
215
+
216
+ The existing passing suite additionally covers quoted/escaped environment
217
+ values, CRLF, missing explicit environment files, stdin pipes, shutdown timeout
218
+ escalation, bounded log retention, cleanup of descendants in managed groups,
219
+ isolated restarts, and TUI terminal restoration. These are useful tests, but
220
+ they assert Foruiman's own contract; passing them is not a Foreman conformance
221
+ certificate.
222
+
223
+ ## Issue-history conclusions
224
+
225
+ The most useful closed reports are [#673](https://github.com/ddollar/foreman/issues/673)
226
+ (USR forwarding), [#676](https://github.com/ddollar/foreman/issues/676)
227
+ (context for our intentional lifetime difference), [#398](https://github.com/ddollar/foreman/issues/398)
228
+ (formation defaults), [#274](https://github.com/ddollar/foreman/issues/274) and
229
+ [#275](https://github.com/ddollar/foreman/issues/275) (one-off argument handling),
230
+ [#271](https://github.com/ddollar/foreman/issues/271) (run without a Procfile),
231
+ [#230](https://github.com/ddollar/foreman/issues/230) (named run), and
232
+ [#489](https://github.com/ddollar/foreman/issues/489) (interactive run signals).
233
+ They identify relevant command/process regressions and intentional differences;
234
+ the modernization assessment above determines which observations warrant work.
235
+
236
+ Do not restore historical failures as features. Unknown process selection
237
+ ([#565](https://github.com/ddollar/foreman/issues/565),
238
+ [#568](https://github.com/ddollar/foreman/issues/568)) hung in the 0.90.0 probe
239
+ until the harness sent TERM; Foruiman's immediate error is an improvement.
240
+ Foreman's environment interpolation changed between historical versions
241
+ ([#561](https://github.com/ddollar/foreman/issues/561)), so compatibility with
242
+ every Foreman release cannot be inferred from matching 0.90.0.
243
+
244
+ The following accounts for all 53 currently open issues. Grouping describes
245
+ their relevance; it does not claim each reported application failure is fixed.
246
+
247
+ | Open upstream issues | Assessment for Foruiman |
248
+ | --- | --- |
249
+ | [#779](https://github.com/ddollar/foreman/issues/779), [#775](https://github.com/ddollar/foreman/issues/775), [#768](https://github.com/ddollar/foreman/issues/768), [#708](https://github.com/ddollar/foreman/issues/708) | Child/descendant cleanup. Managed process-group cleanup is tested; escaped sessions, Spring daemons and exact reported applications remain unverified. Closed [#810](https://github.com/ddollar/foreman/issues/810) is a related npx case whose reporter used an exec wrapper. |
250
+ | [#681](https://github.com/ddollar/foreman/issues/681) | Excluded: successful child exit stopping peers is Foreman behavior. Preserve Foruiman's independent default. |
251
+ | [#703](https://github.com/ddollar/foreman/issues/703) | Debugger echo/input: Foruiman offers selected-process TUI input, but the plain-mode controlling-terminal failure remains. |
252
+ | [#808](https://github.com/ddollar/foreman/issues/808), [#750](https://github.com/ddollar/foreman/issues/750), [#754](https://github.com/ddollar/foreman/issues/754) | Logging requests. Exact presentation/raw logging is excluded. Acceptance-only color/timestamp aliases are also not recommended. Real display controls can be added independently if useful. |
253
+ | [#702](https://github.com/ddollar/foreman/issues/702), [#755](https://github.com/ddollar/foreman/issues/755) | Environment precedence and physical multiline `.env` values. Current parsers share Foreman's limitations. Dotenv compatibility is a different target; do not silently reverse precedence or assume full dotenv syntax. Escaped `\n` in double quotes is supported. |
254
+ | [#784](https://github.com/ddollar/foreman/issues/784), [#751](https://github.com/ddollar/foreman/issues/751), [#715](https://github.com/ddollar/foreman/issues/715), [#714](https://github.com/ddollar/foreman/issues/714) | PORT defaults, `.foreman` configuration and offsets. Common semantics match; fix selected-process validation and treat scaling as an optional feature. Do not change the default 5000 to resolve an OS-specific port conflict while claiming identical defaults. |
255
+ | [#705](https://github.com/ddollar/foreman/issues/705) | Optional product feature: multiple worker instances require formation or an equivalent explicit design. Historical `-c` concurrency syntax in the report is not the 0.90.0 contract; use `-m` if Foreman formation is implemented. |
256
+ | [#794](https://github.com/ddollar/foreman/issues/794), [#709](https://github.com/ddollar/foreman/issues/709), [#704](https://github.com/ddollar/foreman/issues/704), [#668](https://github.com/ddollar/foreman/issues/668), [#695](https://github.com/ddollar/foreman/issues/695) | Excluded: exporters and deployment templates. Their absence is not a blocker for this TUI scope. The old `File.exists?` failure in #794 was already fixed in the baseline source despite the issue remaining open. |
257
+ | [#797](https://github.com/ddollar/foreman/issues/797), [#793](https://github.com/ddollar/foreman/issues/793), [#789](https://github.com/ddollar/foreman/issues/789), [#733](https://github.com/ddollar/foreman/issues/733) | Excluded: native Windows. Foruiman remains POSIX-only; no Windows compatibility or fix is claimed. |
258
+ | [#759](https://github.com/ddollar/foreman/issues/759) | Windows DSC export is an upstream feature request, not a supported Foreman format that must be copied. |
259
+ | [#766](https://github.com/ddollar/foreman/issues/766), [#764](https://github.com/ddollar/foreman/issues/764), [#748](https://github.com/ddollar/foreman/issues/748) | Default-start UX is intentional and excluded from parity. Normal `start -f` path/option handling remains applicable and covered. |
260
+ | [#762](https://github.com/ddollar/foreman/issues/762), [#707](https://github.com/ddollar/foreman/issues/707) | Environment usage questions. Parent env is inherited; explicit files and their overriding precedence must stay consistent. |
261
+ | [#679](https://github.com/ddollar/foreman/issues/679), [#684](https://github.com/ddollar/foreman/issues/684), [#746](https://github.com/ddollar/foreman/issues/746), [#699](https://github.com/ddollar/foreman/issues/699), [#713](https://github.com/ddollar/foreman/issues/713) | Requests/questions about reload, code watching, sequencing, conditional Procfiles and remote log views. They are not existing Foreman requirements. HUP means shutdown in both implementations. Foruiman's manual restarts do not imply automatic reload support. |
262
+ | [#796](https://github.com/ddollar/foreman/issues/796), [#786](https://github.com/ddollar/foreman/issues/786), [#782](https://github.com/ddollar/foreman/issues/782), [#693](https://github.com/ddollar/foreman/issues/693), [#731](https://github.com/ddollar/foreman/issues/731), [#645](https://github.com/ddollar/foreman/issues/645), [#700](https://github.com/ddollar/foreman/issues/700) | App-specific failures, startup/output/shutdown reports, or insufficient reproductions. No verified Foreman conformance change can be concluded from their titles alone. Real Rails/Yarn/Redis/Sidekiq fixtures remain useful validation work. |
263
+ | [#752](https://github.com/ddollar/foreman/issues/752), [#688](https://github.com/ddollar/foreman/issues/688) | Historical Thor dependency failures. Foreman 0.90.0 now depends on Thor ~>1.4; Foruiman allows >=1.3,<2. Exact Thor behavior should be pinned in differential tests. |
264
+ | [#811](https://github.com/ddollar/foreman/issues/811), [#806](https://github.com/ddollar/foreman/issues/806), [#783](https://github.com/ddollar/foreman/issues/783), [#778](https://github.com/ddollar/foreman/issues/778), [#776](https://github.com/ddollar/foreman/issues/776), [#767](https://github.com/ddollar/foreman/issues/767) | Project links, another fork, release naming and style tooling. No runtime compatibility requirement. |
265
+ | [#736](https://github.com/ddollar/foreman/issues/736), [#718](https://github.com/ddollar/foreman/issues/718) | Reports concern Puppet's Foreman or node-foreman, not this Ruby CLI. |
266
+
267
+ ## Recommended implementation order
268
+
269
+ 1. **Fix process-control correctness.** Forward USR1/USR2 and fix real
270
+ controlling-terminal stdin in `--no-tui`. Preserve isolated restarts,
271
+ process-group cleanup, user quit, and optional shutdown policies.
272
+ 2. **Fix selected-process port validation.** Check allocations for selected
273
+ processes without renumbering original entry offsets. This does not require
274
+ formation or port-zero support.
275
+ 3. **Remove the process-name/UI collision if desired.** Give the aggregate tab
276
+ its own identity so a process called `all` can work. Keep strict validation
277
+ of malformed and duplicate entries.
278
+ 4. **Consider formation independently.** Add multi-instance processes and
279
+ exclusions only as an explicit capability. If exposing Foreman's `-m`, retain
280
+ its semantics and test per-instance ports, `PS`, logs and controls. Until
281
+ then, keep a clear unsupported-option error.
282
+ 5. **Document retained differences.** Keep consistent app-root `run`, supported
283
+ `.foreman` defaults with strict validation, explicit env-file syntax,
284
+ positive base ports and the TUI's presentation contract. Give migration
285
+ examples instead of adding silent no-op flags or copying parser quirks.
286
+
287
+ Test the correctness fixes with real controlling terminals, signal handlers,
288
+ cleanup checks and selected-process fixtures. Compare with pinned Foreman where
289
+ behavior is deliberately shared; test retained differences against Foruiman's
290
+ own documented contract. No default-lifetime reversal, exporter, API shim or
291
+ legacy rendering layer is needed.
292
+
293
+ With these modernization choices, describe the product as **a modern alternative
294
+ to Foreman for Procfile development workflows**. List the supported shared
295
+ workflows and migration differences. An unqualified drop-in claim would conflict
296
+ with the intentionally retained command/configuration differences. See
297
+ [COMPATIBILITY.md](COMPATIBILITY.md) for current implemented behavior.
data/docs/RAILS.md CHANGED
@@ -35,7 +35,18 @@ foruiman start -f Procfile.dev -p 3000
35
35
 
36
36
  The second example assigns 3000 to `web`, 3100 to `worker`, and 3200 to `css`.
37
37
  Only programs that consume their generated `PORT` use those values. The base port
38
- is set with `-p`; a `PORT` assignment in `.env` does not set it.
38
+ comes from `-p` or `.foreman`, then `PORT` in the loaded environment, then 5000.
39
+
40
+ Run a Rails command using the same environment without starting the Procfile:
41
+
42
+ ```sh
43
+ foruiman run bin/rails console
44
+ foruiman run -e .env,.env.local bin/rails db:migrate
45
+ ```
46
+
47
+ Explicit `-e` files replace default `.env` loading, so include `.env` in the list
48
+ when layering local overrides. For Foreman-style group shutdown and a longer grace
49
+ period, use `foruiman start -f Procfile.dev --exit-on any -t 10`.
39
50
 
40
51
  An optional `bin/dev`:
41
52
 
data/lib/foruiman/cli.rb CHANGED
@@ -4,31 +4,39 @@ require "thor"
4
4
  require "foruiman"
5
5
  require_relative "plain"
6
6
  require_relative "diagnostics"
7
+ require_relative "configuration"
7
8
 
8
- # Foreman's Thor command structure, reduced to the MVP surface.
9
+ # Foreman's Thor command structure with an interactive supervisor.
9
10
  class Foruiman::CLI < Thor
10
11
  map ["-v", "--version"] => :version
11
12
  default_task :start
12
13
  check_unknown_options!
13
14
  remove_command :tree
14
15
 
15
- class_option :procfile, type: :string, aliases: "-f", default: "Procfile", desc: "Procfile to read"
16
- class_option :root, type: :string, aliases: "-d", desc: "Working directory (default: invocation directory)"
17
- class_option :env, type: :string, aliases: "-e", desc: "Environment file layered after .env"
18
- class_option :dotenv, type: :boolean, default: true, desc: "Load optional .env from working directory"
19
- class_option :port, type: :string, aliases: "-p", default: "5000", desc: "Base port (increments by 100)"
20
- class_option :log_lines, type: :string, default: "10000", desc: "Maximum records per process and in all"
16
+ class_option :procfile, type: :string, aliases: "-f", desc: "Procfile to read (default: Procfile)"
17
+ class_option :root, type: :string, aliases: "-d", desc: "Working directory (default: Procfile directory)"
18
+ class_option :env, type: :string, aliases: "-e", desc: "Comma-separated environment files instead of .env"
19
+ class_option :dotenv, type: :boolean, desc: "Load default .env when -e is absent (default: true)"
20
+ class_option :port, type: :string, aliases: "-p", desc: "Base port (default: PORT or 5000)"
21
+ class_option :log_lines, type: :string, desc: "Maximum records per process and in all (default: 10000)"
22
+ class_option :timeout, type: :string, aliases: "-t", desc: "Seconds before escalating TERM to KILL (default: 5)"
23
+ class_option :exit_on, type: :string, desc: "Shutdown policy: all, any, failure (default: all)"
24
+
25
+ def self.is_thor_reserved_word?(word, type) # rubocop:disable Naming/PredicatePrefix -- Thor's extension API
26
+ word == "run" ? false : super
27
+ end
21
28
 
22
29
  def self.exit_on_failure?
23
30
  true
24
31
  end
25
32
 
26
33
  desc "start [PROCESS]", "Run the Procfile with process tabs, or stream logs without a TTY"
27
- method_option :tui, type: :boolean, default: true, desc: "Use the terminal interface when stdin and stdout are TTYs"
34
+ method_option :tui, type: :boolean, desc: "Use the terminal interface when stdin and stdout are TTYs (default: true)"
28
35
  def start(process = nil)
29
36
  engine = build_engine
30
37
  engine.select(process) if process
31
- interactive = options[:tui] && $stdin.tty? && $stdout.tty?
38
+ engine.validate_ports!
39
+ interactive = configuration.tui? && $stdin.tty? && $stdout.tty?
32
40
  diagnostics = Foruiman::Diagnostics.new(interactive: interactive)
33
41
  code = if interactive
34
42
  require_relative "tui/application"
@@ -48,9 +56,28 @@ class Foruiman::CLI < Thor
48
56
  diagnostics&.close
49
57
  end
50
58
 
59
+ desc "run COMMAND [ARGS...]", "Run a command or Procfile entry with the application's environment"
60
+ stop_on_unknown_option! :run
61
+ def run(*args)
62
+ raise Foruiman::Error, "run requires a command" if args.empty?
63
+
64
+ config = configuration
65
+ env = config.environment
66
+ command = Foruiman::Procfile.new(config.procfile)[args.first] if args.size == 1 && File.file?(config.procfile)
67
+ if command
68
+ exec(env, "/bin/sh", "-c", command, chdir: config.root, unsetenv_others: true)
69
+ else
70
+ # Preserve argv, terminal input, signals and the command's exact exit code.
71
+ exec(env, [args.first, args.first], *args.drop(1), chdir: config.root, unsetenv_others: true)
72
+ end
73
+ rescue Foruiman::Error, SystemCallError => e
74
+ raise Thor::Error, e.message
75
+ end
76
+
51
77
  desc "check", "Validate the Procfile, environment files, ports, and log capacity without starting processes"
52
78
  def check
53
79
  engine = build_engine
80
+ engine.validate_ports!
54
81
  puts "valid Procfile (#{engine.process_names.join(', ')})"
55
82
  rescue Foruiman::Error, SystemCallError => e
56
83
  raise Thor::Error, e.message
@@ -65,19 +92,15 @@ class Foruiman::CLI < Thor
65
92
 
66
93
  private
67
94
 
68
- def integer_option(name)
69
- text = options.fetch(name).to_s
70
- raise Foruiman::Error, "#{name.to_s.tr('_', '-')} must be a positive integer" unless text.match?(/\A[0-9]+\z/)
71
-
72
- text.to_i
95
+ def configuration
96
+ @configuration ||= Foruiman::Configuration.new(options)
73
97
  end
74
98
 
75
99
  def build_engine
76
- root = File.expand_path(options[:root] || Dir.pwd)
77
- procfile = File.expand_path(options[:procfile], root)
78
- file = File.expand_path(options[:env], root) if options[:env]
79
- env = Foruiman::Env.load(root: root, file: file, dotenv: options[:dotenv])
80
- Foruiman::Engine.new(procfile: procfile, root: root, env: env,
81
- port: integer_option(:port), log_lines: integer_option(:log_lines))
100
+ config = configuration
101
+ env = config.environment
102
+ Foruiman::Engine.new(procfile: config.procfile, root: config.root, env: env,
103
+ port: config.port(env), log_lines: config.log_lines,
104
+ term_timeout: config.timeout, exit_on: config.exit_on)
82
105
  end
83
106
  end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ class Foruiman::Configuration
6
+ OPTIONS = %w[procfile root env dotenv port log_lines timeout exit_on tui].freeze
7
+ BOOLEAN_OPTIONS = %w[dotenv tui].freeze
8
+ PATH_OPTIONS = %w[procfile root env].freeze
9
+
10
+ def initialize(options, directory: Dir.pwd)
11
+ @directory = directory
12
+ @options = defaults.merge(options.to_h.transform_keys { |key| key.to_s.tr("-", "_") })
13
+ BOOLEAN_OPTIONS.each do |key|
14
+ next unless @options.key?(key)
15
+ next if [true, false].include?(@options[key])
16
+
17
+ raise Foruiman::Error, "#{key}: expected true or false"
18
+ end
19
+ PATH_OPTIONS.each do |key|
20
+ next unless @options.key?(key)
21
+ next if @options[key].is_a?(String) && !@options[key].empty?
22
+
23
+ raise Foruiman::Error, "#{key}: expected a nonempty path"
24
+ end
25
+ end
26
+
27
+ def procfile
28
+ # Foreman's explicit -f is relative to invocation, independently of -d.
29
+ File.expand_path(@options.fetch("procfile", File.join(@options.fetch("root", "."), "Procfile")), @directory)
30
+ end
31
+
32
+ def root
33
+ File.expand_path(@options.fetch("root", File.dirname(procfile)), @directory)
34
+ end
35
+
36
+ def environment
37
+ # Foreman loads the default environment before inferring a root from -f.
38
+ env_root = File.expand_path(@options.fetch("root", "."), @directory)
39
+ files = @options["env"]&.split(",", -1)&.map do |file|
40
+ raise Foruiman::Error, "env: expected comma-separated file paths" if file.empty?
41
+
42
+ File.expand_path(file, @directory)
43
+ end
44
+ Foruiman::Env.load(root: env_root, file: files, dotenv: @options.fetch("dotenv", true))
45
+ end
46
+
47
+ def port(env)
48
+ integer("port", @options.fetch("port") { env.fetch("PORT", "5000") })
49
+ end
50
+
51
+ def log_lines
52
+ integer("log_lines", @options.fetch("log_lines", "10000"))
53
+ end
54
+
55
+ def timeout
56
+ value = Float(@options.fetch("timeout", 5), exception: false)
57
+ return value if value&.finite? && value >= 0
58
+
59
+ raise Foruiman::Error, "timeout must be a finite nonnegative number"
60
+ end
61
+
62
+ def exit_on
63
+ value = @options.fetch("exit_on", "all")
64
+ return value.to_sym if %w[all any failure].include?(value)
65
+
66
+ raise Foruiman::Error, "exit-on must be all, any, or failure"
67
+ end
68
+
69
+ def tui?
70
+ @options.fetch("tui", true)
71
+ end
72
+
73
+ private
74
+
75
+ def integer(name, value)
76
+ text = value.to_s
77
+ unless text.match?(/\A[0-9]+\z/) && text.to_i.positive?
78
+ raise Foruiman::Error, "#{name.tr('_', '-')} must be a positive integer"
79
+ end
80
+
81
+ text.to_i
82
+ end
83
+
84
+ def defaults
85
+ path = File.join(@directory, ".foreman")
86
+ return {} unless File.file?(path)
87
+
88
+ values = YAML.safe_load_file(path, permitted_classes: [], permitted_symbols: [], aliases: false)
89
+ return {} if values.nil?
90
+ raise Foruiman::Error, "#{path}: expected a YAML mapping of option names to values" unless values.is_a?(Hash)
91
+
92
+ values.each_with_object({}) do |(key, value), result|
93
+ name = key.to_s.tr("-", "_")
94
+ raise Foruiman::Error, "#{path}: unsupported option #{key.inspect}" unless OPTIONS.include?(name)
95
+
96
+ result[name] = value
97
+ end
98
+ rescue Psych::Exception => e
99
+ raise Foruiman::Error, "#{path}: #{e.message}"
100
+ end
101
+ end
@@ -7,7 +7,8 @@ require_relative "output"
7
7
  # Derived from Foreman's registration, process lookup, pipes and self-pipe signal
8
8
  # handling. All process state and output now belong to the caller's event loop.
9
9
  class Foruiman::Engine
10
- HANDLED_SIGNALS = %i[INT TERM HUP].freeze
10
+ FORWARDED_SIGNALS = %i[USR1 USR2].freeze
11
+ HANDLED_SIGNALS = %i[INT TERM HUP USR1 USR2].freeze
11
12
  TERM_TIMEOUT = 5.0
12
13
  READ_CHUNK = 4096
13
14
  READ_BUDGET = 64 * 1024
@@ -18,12 +19,16 @@ class Foruiman::Engine
18
19
  keyword_init: true)
19
20
  Event = Data.define(:type, :name, :pid, :status, :record, :message)
20
21
 
21
- attr_reader :logs, :env, :processes, :root, :procfile_path
22
+ attr_reader :logs, :env, :processes, :root, :procfile_path, :term_timeout, :exit_on
22
23
 
23
24
  def initialize(procfile: nil, root: Dir.pwd, env: ENV.to_h, input: $stdin, port: 5000, log_lines: 10_000,
24
- term_timeout: TERM_TIMEOUT)
25
+ term_timeout: TERM_TIMEOUT, exit_on: :all)
25
26
  raise Foruiman::Error, "port must be an integer in 1..65535" unless port.is_a?(Integer) && (1..65_535).cover?(port)
26
27
  raise Foruiman::Error, "log-lines must be a positive integer" unless log_lines.is_a?(Integer) && log_lines.positive?
28
+ unless term_timeout.is_a?(Numeric) && term_timeout.real? && term_timeout.finite? && term_timeout >= 0
29
+ raise Foruiman::Error, "timeout must be a finite nonnegative number"
30
+ end
31
+ raise Foruiman::Error, "exit-on must be all, any, or failure" unless %i[all any failure].include?(exit_on)
27
32
 
28
33
  @root = File.expand_path(root)
29
34
  raise Foruiman::Error, "working directory does not exist: #{@root}" unless File.directory?(@root)
@@ -34,6 +39,7 @@ class Foruiman::Engine
34
39
  @base_port = port
35
40
  @log_lines = log_lines
36
41
  @term_timeout = term_timeout
42
+ @exit_on = exit_on
37
43
  @processes = []
38
44
  @names = {}
39
45
  @running = {}
@@ -45,7 +51,7 @@ class Foruiman::Engine
45
51
  @failed = false
46
52
  @closed = false
47
53
  @started = false
48
- @signal_requested = false
54
+ @pending_signals = []
49
55
  @self_reader, @self_writer = create_pipe
50
56
  load_procfile(procfile) if procfile
51
57
  rescue StandardError
@@ -57,7 +63,6 @@ class Foruiman::Engine
57
63
  def register(name, command)
58
64
  raise Foruiman::Error, "cannot register after startup" if @started
59
65
  raise Foruiman::Error, "duplicate process: #{name}" if @names.key?(name)
60
- raise Foruiman::Error, "allocated port exceeds 65535 for #{name}" if @base_port + (processes.size * 100) > 65_535
61
66
 
62
67
  Foruiman::Procfile.new[name] = command
63
68
  process = Foruiman::Process.new(command, cwd: root, env: env)
@@ -72,9 +77,6 @@ class Foruiman::Engine
72
77
  def load_procfile(filename)
73
78
  parsed = Foruiman::Procfile.new(filename)
74
79
  entries = parsed.entries.to_a
75
- last_port = @base_port + ((processes.size + entries.size - 1) * 100)
76
- raise Foruiman::Error, "allocated port #{last_port} exceeds 65535" if last_port > 65_535
77
-
78
80
  entries.each { |name, command| register(name, command) }
79
81
  @procfile_path = File.expand_path(filename).freeze
80
82
  self
@@ -84,6 +86,15 @@ class Foruiman::Engine
84
86
  @names.keys
85
87
  end
86
88
 
89
+ def validate_ports!(entries = processes)
90
+ entries.each do |entry|
91
+ next if entry.port <= 65_535
92
+
93
+ raise Foruiman::Error, "allocated port #{entry.port} exceeds 65535 for #{entry.name}"
94
+ end
95
+ self
96
+ end
97
+
87
98
  def process(name)
88
99
  @names[name]&.process
89
100
  end
@@ -121,6 +132,7 @@ class Foruiman::Engine
121
132
  return if @shutdown
122
133
 
123
134
  targets = name ? [state(name)] : processes
135
+ validate_ports!(targets)
124
136
  unless @started
125
137
  @logs = Foruiman::LogStore.new(process_names, capacity: @log_lines) do |record|
126
138
  emit(:output, state(record.name), record: record)
@@ -141,6 +153,7 @@ class Foruiman::Engine
141
153
  entry = state(name)
142
154
  return if entry.restart_pending
143
155
 
156
+ validate_ports!([entry])
144
157
  entry.restart_pending = true
145
158
  entry.status = :restarting
146
159
  lifecycle(entry, :restarting, "restarting")
@@ -208,7 +221,9 @@ class Foruiman::Engine
208
221
  end
209
222
 
210
223
  def exit_code
211
- @explicit_shutdown || !@failed ? 0 : 1
224
+ return 0 if @explicit_shutdown
225
+
226
+ @policy_exit_code || (@failed ? 1 : 0)
212
227
  end
213
228
 
214
229
  def run(keep_open: false)
@@ -229,13 +244,13 @@ class Foruiman::Engine
229
244
  def tick(timeout: 0.03)
230
245
  return if @closed
231
246
 
232
- shutdown if @signal_requested
247
+ handle_signals
233
248
  reap_children
234
249
  advance_groups
235
250
  writable = processes.filter_map { |entry| entry.input unless entry.input_buffer.empty? }
236
251
  ready, writable = IO.select([@self_reader, *@readers.keys], writable, nil, timeout) || [[], []]
237
252
  drain_signal_pipe if ready.delete(@self_reader)
238
- shutdown if @signal_requested
253
+ handle_signals
239
254
  writable.each { |input| flush_input(@inputs.fetch(input)) if @inputs.key?(input) }
240
255
  read_output(ready)
241
256
  reap_children
@@ -273,6 +288,7 @@ class Foruiman::Engine
273
288
  begin
274
289
  pid = entry.process.run(output: stdout_writer, error: stderr_writer,
275
290
  input: input_slave || @input,
291
+ new_session: !@managed_input && @input.respond_to?(:tty?) && @input.tty?,
276
292
  env: { "PORT" => entry.port.to_s, "PS" => "#{entry.name}.1" })
277
293
  rescue SystemCallError => e
278
294
  stdout_reader.close
@@ -282,6 +298,7 @@ class Foruiman::Engine
282
298
  entry.restart_pending = false
283
299
  @failed = true
284
300
  lifecycle(entry, :failed, "failed to start: #{e.message}")
301
+ apply_exit_policy(1)
285
302
  return
286
303
  end
287
304
  entry.pid = entry.pgid = pid
@@ -337,10 +354,18 @@ class Foruiman::Engine
337
354
  @failed ||= !success && !transitioning
338
355
  entry.status = success ? :exited : :failed unless transitioning
339
356
  lifecycle(entry, :exited, termination_message_for(entry.exit_status))
357
+ apply_exit_policy(entry.exit_status.exitstatus || (128 + entry.exit_status.termsig)) unless transitioning
340
358
  terminate(entry)
341
359
  end
342
360
  end
343
361
 
362
+ def apply_exit_policy(code)
363
+ return if @shutdown || exit_on == :all || (exit_on == :failure && code.zero?)
364
+
365
+ @policy_exit_code = code
366
+ shutdown(explicit: false)
367
+ end
368
+
344
369
  def advance_groups
345
370
  processes.each do |entry|
346
371
  next unless entry.pgid
@@ -474,12 +499,24 @@ class Foruiman::Engine
474
499
  @old_handlers = {}
475
500
  HANDLED_SIGNALS.each do |signal|
476
501
  @old_handlers[signal] = Signal.trap(signal) do
477
- @signal_requested = true
502
+ @pending_signals << signal
478
503
  notice_signal
479
504
  end
480
505
  end
481
506
  end
482
507
 
508
+ def handle_signals
509
+ pending = @pending_signals
510
+ @pending_signals = []
511
+ pending.each do |signal|
512
+ if FORWARDED_SIGNALS.include?(signal)
513
+ processes.each { |entry| signal_group(entry, signal) }
514
+ else
515
+ shutdown
516
+ end
517
+ end
518
+ end
519
+
483
520
  def restore_signal_handlers
484
521
  @old_handlers&.each { |signal, handler| Signal.trap(signal, handler) }
485
522
  @old_handlers = nil
data/lib/foruiman/env.rb CHANGED
@@ -28,8 +28,11 @@ class Foruiman::Env
28
28
  def self.load(root: Dir.pwd, file: nil, dotenv: true, inherited: ENV.to_h)
29
29
  env = inherited.dup
30
30
  default = File.join(root, ".env")
31
- env.merge!(new(default).to_h) if dotenv && File.file?(default)
32
- env.merge!(new(file).to_h) if file
31
+ if file
32
+ Array(file).each { |filename| env.merge!(new(filename).to_h) }
33
+ elsif dotenv && File.file?(default)
34
+ env.merge!(new(default).to_h)
35
+ end
33
36
  env.transform_values { |value| value.dup.freeze }.freeze
34
37
  end
35
38
  end
@@ -14,7 +14,7 @@ class Foruiman::LogStore
14
14
  end
15
15
 
16
16
  def [](name)
17
- name == "all" ? all : @buffers.fetch(name)
17
+ name == :all ? all : @buffers.fetch(name)
18
18
  end
19
19
 
20
20
  def write(name:, stream:, pid:, text:, complete:, previous: nil)
@@ -11,9 +11,49 @@ class Foruiman::Process
11
11
  end
12
12
 
13
13
  def run(options = {})
14
- ::Process.spawn(env.merge(options.fetch(:env, {})), "/bin/sh", "-c", command,
15
- chdir: cwd, in: options.fetch(:input, $stdin), out: options.fetch(:output, $stdout),
16
- err: options.fetch(:error, $stderr), pgroup: true, unsetenv_others: true,
17
- close_others: true)
14
+ environment = env.merge(options.fetch(:env, {}))
15
+ redirects = { chdir: cwd, in: options.fetch(:input, $stdin), out: options.fetch(:output, $stdout),
16
+ err: options.fetch(:error, $stderr), unsetenv_others: true, close_others: true }
17
+ return spawn_session(environment, redirects) if options[:new_session]
18
+
19
+ ::Process.spawn(environment, "/bin/sh", "-c", shell_command, **redirects, pgroup: true)
20
+ end
21
+
22
+ private
23
+
24
+ def shell_command
25
+ # Group forwarding also reaches the shell waiting for the application.
26
+ # Catch (rather than ignore) these signals so exec resets the dispositions
27
+ # and the application can decide whether to handle them or terminate.
28
+ "trap ':' USR1 USR2\n#{command}"
29
+ end
30
+
31
+ def spawn_session(environment, redirects)
32
+ # A background group in the supervisor's session cannot read its controlling
33
+ # terminal. A new session keeps inherited stdin usable and still gives us a
34
+ # group whose ID is the child PID, without taking the terminal's foreground.
35
+ reader, writer = IO.pipe
36
+ writer.close_on_exec = true
37
+ pid = ::Process.fork do
38
+ reader.close
39
+ ::Process.setsid
40
+ exec(environment, "/bin/sh", "-c", shell_command, **redirects)
41
+ rescue Exception => e # rubocop:disable Lint/RescueException -- never unwind the fork into supervisor cleanup
42
+ writer.write("#{e.is_a?(SystemCallError) ? e.errno : 0}\n#{e.message}")
43
+ ensure
44
+ ::Process.exit!(127)
45
+ end
46
+ writer.close
47
+ failure = reader.read
48
+ unless failure.empty?
49
+ ::Process.waitpid(pid)
50
+ errno, message = failure.split("\n", 2)
51
+ raise ArgumentError, message if errno.to_i.zero?
52
+
53
+ raise SystemCallError.new(message, errno.to_i)
54
+ end
55
+ pid
56
+ ensure
57
+ [reader, writer].compact.each { |io| io.close unless io.closed? }
18
58
  end
19
59
  end
@@ -52,10 +52,9 @@ class Foruiman::Procfile
52
52
  private
53
53
 
54
54
  def validate_entry!(name, command, location)
55
- unless name.match?(/\A[A-Za-z0-9_-]+\z/) && !command.strip.empty? && !command.include?("\0")
56
- raise ParseError, "#{location}: expected NAME: command"
57
- end
58
- raise ParseError, "#{location}: 'all' is reserved for the aggregate tab" if name == "all"
55
+ return if name.match?(/\A[A-Za-z0-9_-]+\z/) && !command.strip.empty? && !command.include?("\0")
56
+
57
+ raise ParseError, "#{location}: expected NAME: command"
59
58
  end
60
59
 
61
60
  def parse(filename)
@@ -92,7 +92,7 @@ module Foruiman::TUI
92
92
  end
93
93
 
94
94
  def begin_input
95
- if state.name == "all"
95
+ if state.aggregate?
96
96
  feedback("Select a process before entering input mode")
97
97
  elsif @engine.state(state.name).status != :running
98
98
  feedback("#{state.name} is not running")
@@ -135,7 +135,7 @@ module Foruiman::TUI
135
135
  end
136
136
 
137
137
  def toggle_process
138
- if state.name == "all"
138
+ if state.aggregate?
139
139
  control_all(:stop)
140
140
  return
141
141
  end
@@ -151,7 +151,7 @@ module Foruiman::TUI
151
151
  end
152
152
 
153
153
  def control(action)
154
- if state.name == "all"
154
+ if state.aggregate?
155
155
  if action == :restart
156
156
  control_all(action)
157
157
  else
@@ -126,15 +126,15 @@ module Foruiman::TUI
126
126
 
127
127
  def tab(name, index, state, engine, width)
128
128
  selected = index == state.selected
129
- entry = name == "all" ? nil : engine.processes.find { |process| process.name == name }
130
- mark = name == "all" ? "≡" : Theme::STATUS_MARKS.fetch(entry&.status, "○")
131
- number = if name == "all"
129
+ entry = name == :all ? nil : engine.processes.find { |process| process.name == name }
130
+ mark = name == :all ? "≡" : Theme::STATUS_MARKS.fetch(entry&.status, "○")
131
+ number = if name == :all
132
132
  "0"
133
133
  else
134
134
  (index < 9 ? (index + 1).to_s : "·")
135
135
  end
136
- name = Text.clip(name, [width - 12, 6].max.clamp(6, 22), ellipsis: true)
137
- color = name == "all" ? :accent : @theme.process(index)
136
+ color = name == :all ? :accent : @theme.process(index)
137
+ name = Text.clip(name.to_s, [width - 12, 6].max.clamp(6, 22), ellipsis: true)
138
138
  mark_color = entry ? Theme::STATUS_COLORS.fetch(entry.status, :muted) : :muted
139
139
  @theme.paint(" #{number} ", selected ? :muted : :faint, selected: selected) +
140
140
  @theme.paint("#{mark} ", mark_color, selected: selected) +
@@ -147,14 +147,14 @@ module Foruiman::TUI
147
147
  @theme.paint(" ? / Escape close help ", :muted))
148
148
  end
149
149
 
150
- title = state.name == "all" ? " ≡ all logs " : " › #{state.name} "
151
- detail = if state.name == "all"
150
+ title = state.aggregate? ? " ≡ all logs " : " › #{state.name} "
151
+ detail = if state.aggregate?
152
152
  "#{engine.processes.size} processes"
153
153
  else
154
154
  entry = engine.state(state.name)
155
155
  process_detail(entry)
156
156
  end
157
- active = if state.name == "all"
157
+ active = if state.aggregate?
158
158
  engine.processes.any? { |entry| entry.status == :running }
159
159
  else
160
160
  engine.state(state.name).status == :running
@@ -171,7 +171,7 @@ module Foruiman::TUI
171
171
  title = @theme.paint(title, :text, bold: true)
172
172
  if @inside >= 45
173
173
  title += @theme.paint(" #{detail} ", :muted)
174
- elsif state.name != "all" && @inside >= 28
174
+ elsif !state.aggregate? && @inside >= 28
175
175
  entry = engine.state(state.name)
176
176
  title += @theme.paint(" #{entry.status} ", Theme::STATUS_COLORS.fetch(entry.status, :muted))
177
177
  end
@@ -200,7 +200,7 @@ module Foruiman::TUI
200
200
  thumb_start = buffer.size <= @height ? 0 : (first * travel / (buffer.size - @height))
201
201
  Array.new(@height) do |index|
202
202
  content = if records[index]
203
- @formatter.row(records[index], aggregate: state.name == "all", width: @inside - 2)
203
+ @formatter.row(records[index], aggregate: state.aggregate?, width: @inside - 2)
204
204
  elsif records.empty? && index == @height / 2
205
205
  @theme.paint("Waiting for output…", :faint)
206
206
  else
@@ -226,7 +226,7 @@ module Foruiman::TUI
226
226
  else
227
227
  " f resume "
228
228
  end
229
- location = " PID #{engine.state(state.name).pid || '-'} " if state.name != "all" && @inside < 45
229
+ location = " PID #{engine.state(state.name).pid || '-'} " if !state.aggregate? && @inside < 45
230
230
  border(@theme.paint(count, :faint), @theme.paint(location, state.viewport.following ? :faint : :amber),
231
231
  bottom: true)
232
232
  end
@@ -256,7 +256,8 @@ module Foruiman::TUI
256
256
 
257
257
  quit = @theme.paint(" q ", :accent, bold: true) + @theme.paint(" × quit ", :muted)
258
258
  left = if engine.shutting_down?
259
- @theme.paint(" ◌ Stopping process groups · TERM → KILL after 5s", :amber)
259
+ seconds = format("%g", engine.term_timeout)
260
+ @theme.paint(" ◌ Stopping process groups · TERM → KILL after #{seconds}s", :amber)
260
261
  elsif state.feedback
261
262
  @theme.paint(" #{state.feedback}", :amber)
262
263
  elsif state.help
@@ -268,8 +269,8 @@ module Foruiman::TUI
268
269
  end
269
270
 
270
271
  def shortcuts(state, engine)
271
- restart = ["r", state.name == "all" ? "↻ restart all" : "↻ restart"]
272
- toggle = if state.name == "all"
272
+ restart = ["r", state.aggregate? ? "↻ restart all" : "↻ restart"]
273
+ toggle = if state.aggregate?
273
274
  ["s", "■ stop all"]
274
275
  elsif process_active?(engine.state(state.name))
275
276
  ["s", "■ stop"]
@@ -9,7 +9,7 @@ module Foruiman::TUI
9
9
  attr_accessor :help, :feedback, :input_target
10
10
 
11
11
  def initialize(names)
12
- @tabs = [*names, "all"].freeze
12
+ @tabs = [*names, :all].freeze
13
13
  @selected = tabs.size - 1
14
14
  @viewports = tabs.to_h { |name| [name, Viewport.new] }
15
15
  @help = false
@@ -22,6 +22,10 @@ module Foruiman::TUI
22
22
  tabs[selected]
23
23
  end
24
24
 
25
+ def aggregate?
26
+ name == :all
27
+ end
28
+
25
29
  def viewport
26
30
  viewports.fetch(name)
27
31
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Foruiman
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foruiman
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Foruiman contributors
@@ -64,12 +64,14 @@ files:
64
64
  - docs/ARCHITECTURE.md
65
65
  - docs/ASSUMPTIONS.md
66
66
  - docs/COMPATIBILITY.md
67
+ - docs/FOREMAN_AUDIT.md
67
68
  - docs/RAILS.md
68
69
  - docs/UPSTREAM.md
69
70
  - docs/terminal-preview.png
70
71
  - lib/foruiman.rb
71
72
  - lib/foruiman/ansi.rb
72
73
  - lib/foruiman/cli.rb
74
+ - lib/foruiman/configuration.rb
73
75
  - lib/foruiman/diagnostics.rb
74
76
  - lib/foruiman/engine.rb
75
77
  - lib/foruiman/env.rb
@@ -112,7 +114,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
112
114
  - !ruby/object:Gem::Version
113
115
  version: '0'
114
116
  requirements: []
115
- rubygems_version: 4.0.16
117
+ rubygems_version: 4.0.20
116
118
  specification_version: 4
117
119
  summary: A Procfile supervisor with process tabs, bounded logs, and isolated restarts
118
120
  test_files: []