foruiman 0.1.2 → 0.3.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 +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +21 -4
- data/docs/ARCHITECTURE.md +24 -6
- data/docs/COMPATIBILITY.md +62 -14
- data/docs/RAILS.md +19 -2
- data/lib/foruiman/ansi.rb +8 -3
- data/lib/foruiman/cli.rb +41 -20
- data/lib/foruiman/configuration.rb +101 -0
- data/lib/foruiman/engine.rb +135 -21
- data/lib/foruiman/env.rb +5 -2
- data/lib/foruiman/output.rb +22 -4
- data/lib/foruiman/output_line.rb +100 -0
- data/lib/foruiman/process.rb +1 -1
- data/lib/foruiman/tui/application.rb +78 -3
- data/lib/foruiman/tui/input_line.rb +217 -0
- data/lib/foruiman/tui/keyboard.rb +1 -1
- data/lib/foruiman/tui/log_formatter.rb +2 -0
- data/lib/foruiman/tui/renderer.rb +48 -11
- data/lib/foruiman/tui/state.rb +12 -1
- data/lib/foruiman/tui/theme.rb +2 -2
- data/lib/foruiman/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 906d36f65025e56552898118d3a3e5fec8b81c6046f32a7df9c627f048a89aa6
|
|
4
|
+
data.tar.gz: af7c8f777be7302aca9bd3e5cb50a17cc9acd9e2c6036e5d5b8033779d0a492f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b24d2f3e374331fe3c054c7930f5f1e5cd1e9bcc12e85befa8aeaf7bf8c367b70ab67942c19ac8a3fed56930ce9236a3cf422121a84bc2043584356450035ae0
|
|
7
|
+
data.tar.gz: 2b78d3244f014e4cf2e4959a8bedb62b308fb04c38fc8326d151d015983feeb8373a346852f2e730360b4e31a10a591dc63e410990e861571351646201ac9026
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.3.0
|
|
6
|
+
|
|
7
|
+
- Add `.foreman` YAML defaults with explicit CLI overrides and validated options.
|
|
8
|
+
- Add `run` for one-off commands and named Procfile entries, preserving terminal
|
|
9
|
+
input, argument boundaries, signals, and exit status.
|
|
10
|
+
- Match Foreman's explicit environment-file replacement, comma-separated loading,
|
|
11
|
+
environment `PORT` fallback, and Procfile/root path handling. See the migration
|
|
12
|
+
notes for changes from 0.2.
|
|
13
|
+
- Add `-t` / `--timeout` and `--exit-on all|any|failure`. Automatic group shutdown
|
|
14
|
+
preserves the triggering exit code and cleans up descendants and pending restarts.
|
|
15
|
+
|
|
16
|
+
## 0.2.0
|
|
17
|
+
|
|
18
|
+
- Keep child stdin open so default watcher modes continue running.
|
|
19
|
+
- Add per-process TTY input from the TUI: select a process, press `i`, and use
|
|
20
|
+
Ctrl-X to return to Foruiman. Ctrl-C interrupts the attached process group.
|
|
21
|
+
- Add a command input bar with local backspace, cursor editing and per-process
|
|
22
|
+
history. Interpret debugger redraws instead of appending repeated prompts.
|
|
23
|
+
- Make `s` stop a running selected process and start it again once stopped,
|
|
24
|
+
without affecting peers. Use clear `▶` running and `■` stopped glyphs.
|
|
25
|
+
- Make `s` on the `all` tab stop every process, matching the scope of `r`.
|
|
26
|
+
|
|
5
27
|
- Test Ruby 3.2, 3.3, 3.4, and 4.0 in a reusable CI workflow and keep the
|
|
6
28
|
development dependency set compatible with Ruby 3.2.
|
|
7
29
|
- Add a tag-driven RubyGems trusted-publishing workflow gated by the full CI suite.
|
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
|
```
|
|
@@ -46,18 +48,32 @@ The TUI opens when stdin and stdout are terminals. Otherwise, Foruiman streams
|
|
|
46
48
|
plain logs and exits when the processes finish. Processes run independently, and
|
|
47
49
|
restarting one process does not interrupt the others.
|
|
48
50
|
|
|
51
|
+
Each TUI process gets its own open terminal input, so asset builders using their
|
|
52
|
+
default `--watch` mode stay alive. Select a process and press `i` to type into a
|
|
53
|
+
debugger such as Pry, Byebug or IRB. Edit in the input bar, press Enter to send,
|
|
54
|
+
and Ctrl-X to return to Foruiman. Arrow keys edit and recall commands; Ctrl-C
|
|
55
|
+
interrupts the selected process. Debugger tab completion is not forwarded.
|
|
56
|
+
Press `s` to stop the selected process, or all processes from `all`; on a stopped
|
|
57
|
+
process tab, `s` starts it again.
|
|
58
|
+
|
|
49
59
|
## Options
|
|
50
60
|
|
|
51
61
|
| Option | Default | Purpose |
|
|
52
62
|
| --- | --- | --- |
|
|
53
63
|
| `-f`, `--procfile FILE` | `Procfile` | Procfile to run |
|
|
54
|
-
| `-d`, `--root DIR` |
|
|
55
|
-
| `-e`, `--env
|
|
64
|
+
| `-d`, `--root DIR` | Procfile directory | Working directory |
|
|
65
|
+
| `-e`, `--env FILES` | `.env` | Comma-separated files, loaded in order instead of `.env` |
|
|
56
66
|
| `--no-dotenv` | `.env` enabled | Skip `.env` |
|
|
57
|
-
| `-p`, `--port PORT` | `5000` | Base port; entries increment by 100 |
|
|
67
|
+
| `-p`, `--port PORT` | Environment `PORT`, then `5000` | Base port; entries increment by 100 |
|
|
68
|
+
| `-t`, `--timeout SECONDS` | `5` | Grace period before forced shutdown |
|
|
69
|
+
| `--exit-on all\|any\|failure` | `all` | Keep peers running, stop on any exit, or stop on failure |
|
|
58
70
|
| `--log-lines N` | `10000` | Retained records per process and in `all` |
|
|
59
71
|
| `--no-tui` | TUI when interactive | Force plain output |
|
|
60
72
|
|
|
73
|
+
Set defaults in a `.foreman` YAML file in the invocation directory; CLI flags
|
|
74
|
+
override them. See [compatibility and migration notes](docs/COMPATIBILITY.md) for
|
|
75
|
+
path rules, supported settings, and changes from 0.2.
|
|
76
|
+
|
|
61
77
|
## Keyboard
|
|
62
78
|
|
|
63
79
|
| Key | Action |
|
|
@@ -69,9 +85,10 @@ restarting one process does not interrupt the others.
|
|
|
69
85
|
| Home / `g` | Jump to oldest retained log |
|
|
70
86
|
| End / `G` / `f` | Follow new output |
|
|
71
87
|
| Space | Pause or resume following |
|
|
88
|
+
| `i` / Ctrl-X | Open command input / return to Foruiman |
|
|
72
89
|
| `r` | Restart the selected process, or all from `all` |
|
|
73
90
|
| `R` | Restart all processes |
|
|
74
|
-
| `s` / `S` |
|
|
91
|
+
| `s` / `S` | Start/stop selected (`s` on `all` stops all) / stop all |
|
|
75
92
|
| `?` / Escape | Toggle help / close help |
|
|
76
93
|
| `q` / Ctrl-C | Stop processes and quit |
|
|
77
94
|
|
data/docs/ARCHITECTURE.md
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
|
|
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
|
-
wraps `/bin/sh -c` with a new process group, two output streams, and
|
|
5
|
+
wraps `/bin/sh -c` with a new process group, two output streams, and configurable stdin.
|
|
6
6
|
`CLI < Thor` validates the full configuration before starting the engine.
|
|
7
|
+
`Configuration` safely loads `.foreman` defaults, merges explicit CLI values,
|
|
8
|
+
resolves Foreman-compatible paths and environments, and validates numeric options.
|
|
9
|
+
`run` uses `exec` for one-off commands with direct terminal IO and exact exit status.
|
|
7
10
|
|
|
8
11
|
`Engine` owns registration, PID/group tracking, nonblocking pipes, child reaping,
|
|
9
12
|
and lifecycle transitions. A single caller thread drives all mutations. Signal
|
|
@@ -35,6 +38,7 @@ begin
|
|
|
35
38
|
engine.tick(timeout: 0.03)
|
|
36
39
|
engine.restart("web") # asynchronous; peers keep running
|
|
37
40
|
engine.stop("worker") # asynchronous
|
|
41
|
+
engine.start("worker") # start it again once stopped
|
|
38
42
|
engine.shutdown # cancels replacements and requests group cleanup
|
|
39
43
|
engine.tick until engine.finished?
|
|
40
44
|
ensure
|
|
@@ -46,10 +50,24 @@ Call engine methods and event listeners on the driving thread; this is not a
|
|
|
46
50
|
cross-thread messaging API. `run` handles INT/TERM/HUP and restores existing
|
|
47
51
|
handlers. An embedding loop that drives `tick` directly owns its signal handling.
|
|
48
52
|
`close` disconnects observers so a failed renderer/output consumer cannot prevent
|
|
49
|
-
process cleanup. `term_timeout:`
|
|
50
|
-
|
|
53
|
+
process cleanup. `term_timeout:` controls the shutdown grace period (CLI `-t`,
|
|
54
|
+
default five seconds). `exit_on: :all` retains independent processes; `:any` or
|
|
55
|
+
`:failure` requests group shutdown after a qualifying natural exit and preserves
|
|
56
|
+
its exit code. Intentional stops/restarts do not trigger the policy. `state(name)` exposes lifecycle state for
|
|
51
57
|
rendering; callers should not mutate it.
|
|
52
58
|
|
|
59
|
+
Children inherit the configured input stream in plain and embedded use. Before
|
|
60
|
+
startup, `manage_input!` gives each child a dedicated pseudo-terminal while the
|
|
61
|
+
caller retains the real terminal; `write_input(name, bytes)` forwards input,
|
|
62
|
+
`interrupt_process(name)` sends SIGINT to that process group, and `resize_inputs` keeps
|
|
63
|
+
the pseudo-terminals sized with the UI. The TUI uses this mode so watch commands
|
|
64
|
+
do not see EOF and only the selected process receives interactive input.
|
|
65
|
+
|
|
66
|
+
The TUI edits commands locally in a bounded `InputLine`, with independent history
|
|
67
|
+
for each process, then sends a complete line on Enter. Ctrl-X leaves input mode.
|
|
68
|
+
`OutputLine` interprets carriage returns, backspaces, horizontal cursor moves and
|
|
69
|
+
line erasure before recording child output; screen controls remain suppressed.
|
|
70
|
+
|
|
53
71
|
`LogStore` uses per-process `Ring` instances and an aggregate `Ring`, each with O(1)
|
|
54
72
|
append, eviction, record replacement, and identity lookup. Immutable `Data` records
|
|
55
73
|
are shared. A partial record has one sequence identity; completing it replaces
|
|
@@ -74,9 +92,9 @@ clips rows safely. `Theme` uses the terminal's ANSI palette, default foreground
|
|
|
74
92
|
background, and reverse-video selection, with a monochrome fallback. It does not
|
|
75
93
|
read OS theme files or override palette entries; child SGR resets restore terminal
|
|
76
94
|
defaults. The header uses the engine's `procfile_path` to identify the loaded file.
|
|
77
|
-
`LogFormatter` aligns timestamps,
|
|
78
|
-
|
|
79
|
-
|
|
95
|
+
`LogFormatter` aligns timestamps, process names, stream markers, and content.
|
|
96
|
+
Complete frames reset styles at each row. `Application` connects navigation,
|
|
97
|
+
start/stop controls, and selected-child input to the engine and limits drawing to 30 FPS. Resize handling reads current
|
|
80
98
|
terminal dimensions each loop; it does not replace an application's WINCH handler.
|
|
81
99
|
|
|
82
100
|
`Plain` prints completed records and runs to natural completion. `Diagnostics`
|
data/docs/COMPATIBILITY.md
CHANGED
|
@@ -1,27 +1,75 @@
|
|
|
1
1
|
# Intentional differences from Foreman
|
|
2
2
|
|
|
3
|
-
Foruiman
|
|
3
|
+
Foruiman supports common Foreman development workflows. It is not a drop-in
|
|
4
|
+
replacement for every Foreman feature.
|
|
4
5
|
|
|
5
6
|
| Area | Foruiman behavior |
|
|
6
7
|
| --- | --- |
|
|
7
|
-
| Identity | `foruiman` gem/executable, `Foruiman` namespace, version 0.
|
|
8
|
+
| Identity | `foruiman` gem/executable, `Foruiman` namespace, version 0.3.0 |
|
|
8
9
|
| 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,
|
|
10
|
+
| CLI | Thor-based `start [PROCESS]`, `run COMMAND [ARGS...]`, `check`, `version`, and `help` |
|
|
11
|
+
| Excluded features | No export, scaling/formation, forced color, or timestamp toggle |
|
|
11
12
|
| Procfile validation | Reject malformed lines, duplicates, empty commands, and reserved `all`; report line numbers |
|
|
12
|
-
| Working directory |
|
|
13
|
-
| Environment |
|
|
14
|
-
| Ports |
|
|
13
|
+
| Working directory | Procfile directory unless `-d`; explicit `-f` and `-e` paths resolve from invocation |
|
|
14
|
+
| Environment | Explicit `-e file1,file2` replaces default `.env` loading; later files win; parent `ENV` stays untouched |
|
|
15
|
+
| Ports | `-p` or `.foreman` port, then loaded `PORT`, then 5000; plus 100 per original entry |
|
|
15
16
|
| Expansion | `/bin/sh -c` performs shell expansion; no Ruby string substitution |
|
|
16
17
|
| Instances | Exactly one instance per entry; `PS=name.1` |
|
|
17
|
-
| Failures | Independent
|
|
18
|
+
| Failures | Independent by default; `--exit-on any` stops on any natural exit, `failure` only on an unsuccessful exit |
|
|
18
19
|
| Restart | Stop only the affected group, wait for descendants and output, then replace |
|
|
19
|
-
| Shutdown | TERM, five
|
|
20
|
+
| Shutdown | TERM, configurable `-t` grace (default five seconds), KILL; track groups after leader exit; restore prior signal handlers |
|
|
20
21
|
| Output | Separate stdout/stderr metadata; bounded logs and live partial records |
|
|
21
|
-
| Terminal | Tabs, independent scroll/follow, restart
|
|
22
|
-
| Plain exit |
|
|
23
|
-
| Interactive exit | Remain open after all processes exit;
|
|
22
|
+
| Terminal | Tabs, independent scroll/follow, restart, start/stop, and selected-process input controls |
|
|
23
|
+
| 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 |
|
|
24
|
+
| Interactive exit | Remain open after all processes exit by default; automatic shutdown policies close the TUI after cleanup |
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
## Configuration and paths
|
|
27
|
+
|
|
28
|
+
Like Foreman, `.foreman` is read from the invocation directory. Use long option
|
|
29
|
+
names; CLI flags override YAML values. Supported keys are `procfile`, `root`, `env`,
|
|
30
|
+
`port`, `timeout`, `dotenv`, `tui`, `log-lines`, and `exit-on` (underscores also work).
|
|
31
|
+
Unsupported keys, including `formation`, produce an error instead of being ignored.
|
|
32
|
+
YAML object tags and aliases are not supported.
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
procfile: Procfile.dev
|
|
36
|
+
port: 3000
|
|
37
|
+
timeout: 10
|
|
38
|
+
exit-on: any
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
An explicit `-f` path is relative to invocation, even when `-d` is supplied. Without
|
|
42
|
+
`-f`, the Procfile is `<root>/Procfile`. Without `-d`, child commands run in the
|
|
43
|
+
Procfile's directory. Default `.env` loading follows Foreman's CLI: use the explicit
|
|
44
|
+
`-d` directory when supplied, otherwise the invocation directory. An inferred root
|
|
45
|
+
from `-f` does not move the default `.env` lookup. Explicit `-e` paths always resolve
|
|
46
|
+
from invocation. `--no-dotenv` disables only default `.env` loading.
|
|
47
|
+
|
|
48
|
+
## One-off commands
|
|
49
|
+
|
|
50
|
+
`foruiman run bin/rails console` uses the same environment-file rules and passes
|
|
51
|
+
stdin, stdout, and stderr directly to the command. No Procfile is required. A
|
|
52
|
+
single argument matching a Procfile entry runs that entry's shell command.
|
|
53
|
+
Other commands retain their exact argument boundaries. For shell operators, use
|
|
54
|
+
`foruiman run sh -c 'command1 && command2'`. Put Foruiman options before the command;
|
|
55
|
+
options after the command belong to that command.
|
|
56
|
+
|
|
57
|
+
`run` replaces Foruiman with the command, preserving signals and exit status. It
|
|
58
|
+
does not generate per-process `PORT` or `PS` values or open the TUI.
|
|
59
|
+
|
|
60
|
+
## Migrating from 0.2
|
|
61
|
+
|
|
62
|
+
- `-e custom.env` now replaces `.env`. Use `-e .env,custom.env` to retain layering.
|
|
63
|
+
- Environment `PORT` now selects the base port. Use `-p 5000` to retain the old default.
|
|
64
|
+
- A nested `-f` changes the default child working directory. Add `-d .` to keep
|
|
65
|
+
invocation as the working directory. With `-d app`, an old `-f config/Procfile.dev`
|
|
66
|
+
becomes `-f app/config/Procfile.dev` when that file lives inside `app`.
|
|
67
|
+
- A `.foreman` file now supplies defaults. Remove unsupported keys before using it.
|
|
68
|
+
|
|
69
|
+
Use `--exit-on any` for Foreman's stop-on-first-exit behavior. Intentional TUI
|
|
70
|
+
stops and restarts do not trigger that policy. Exact Foreman log formatting,
|
|
71
|
+
USR1/USR2 forwarding, and its Ruby embedding API remain outside compatibility.
|
|
72
|
+
|
|
73
|
+
Full-screen child terminal applications and background daemonization are outside
|
|
74
|
+
this release. Remote process control, persistence across Foruiman sessions,
|
|
27
75
|
search, horizontal scrolling, and log export are not provided.
|
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
|
-
|
|
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
|
|
|
@@ -46,7 +57,13 @@ exec foruiman start -f Procfile.dev "$@"
|
|
|
46
57
|
|
|
47
58
|
Select the web tab and press `r` to restart Rails without interrupting the worker
|
|
48
59
|
or asset watcher. On the `all` tab, `r` restarts all entries; `R` does so from any
|
|
49
|
-
tab.
|
|
60
|
+
tab. Default watch modes stay running because their stdin remains open. When Rails
|
|
61
|
+
stops in Pry, Byebug or IRB, select `web` and press `i`. Edit commands in the input
|
|
62
|
+
bar and press Enter to send them. Up/Down recall history, Left/Right move the
|
|
63
|
+
cursor, and Ctrl-X returns to Foruiman. Ctrl-C interrupts the selected process;
|
|
64
|
+
debugger tab completion is not forwarded. Press `s` to stop the selected entry
|
|
65
|
+
and press it again to start it; `s` on `all` stops every entry. The header shows the
|
|
66
|
+
active Procfile. `q` or Ctrl-C stops all owned process
|
|
50
67
|
groups, including grandchildren, before returning to the shell.
|
|
51
68
|
|
|
52
69
|
Redirect output with `foruiman start -f Procfile.dev > development.log` to use plain
|
data/lib/foruiman/ansi.rb
CHANGED
|
@@ -63,10 +63,11 @@ module Foruiman::ANSI
|
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
-
#
|
|
67
|
-
#
|
|
66
|
+
# Screen controls are discarded. Output may opt into horizontal line editing
|
|
67
|
+
# controls, which it interprets before storing or displaying any records.
|
|
68
68
|
class Decoder
|
|
69
|
-
def initialize
|
|
69
|
+
def initialize(line_controls: false)
|
|
70
|
+
@line_controls = line_controls
|
|
70
71
|
@state = :text
|
|
71
72
|
@escape = +""
|
|
72
73
|
@pending = +"".b
|
|
@@ -92,6 +93,7 @@ module Foruiman::ANSI
|
|
|
92
93
|
case byte
|
|
93
94
|
when 27 then @state = :escape
|
|
94
95
|
when 9 then output << " "
|
|
96
|
+
when 8, 13 then output << byte if @line_controls
|
|
95
97
|
when 10, 32..126, 128..255 then output << byte
|
|
96
98
|
end
|
|
97
99
|
when :escape
|
|
@@ -108,6 +110,9 @@ module Foruiman::ANSI
|
|
|
108
110
|
when :csi
|
|
109
111
|
if byte.between?(64, 126)
|
|
110
112
|
output << "\e[#{@escape}m" if byte == 109 && @escape.match?(/\A[0-9;:]*\z/)
|
|
113
|
+
if @line_controls && [67, 68, 71, 75].include?(byte) && @escape.match?(/\A[0-9]*\z/)
|
|
114
|
+
output << "\e[#{@escape}#{byte.chr}"
|
|
115
|
+
end
|
|
111
116
|
@state = :text
|
|
112
117
|
elsif @escape.bytesize < 96
|
|
113
118
|
@escape << byte
|
data/lib/foruiman/cli.rb
CHANGED
|
@@ -4,31 +4,38 @@ 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
|
|
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",
|
|
16
|
-
class_option :root, type: :string, aliases: "-d", desc: "Working directory (default:
|
|
17
|
-
class_option :env, type: :string, aliases: "-e", desc: "
|
|
18
|
-
class_option :dotenv, type: :boolean,
|
|
19
|
-
class_option :port, type: :string, aliases: "-p",
|
|
20
|
-
class_option :log_lines, type: :string,
|
|
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,
|
|
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 =
|
|
38
|
+
interactive = configuration.tui? && $stdin.tty? && $stdout.tty?
|
|
32
39
|
diagnostics = Foruiman::Diagnostics.new(interactive: interactive)
|
|
33
40
|
code = if interactive
|
|
34
41
|
require_relative "tui/application"
|
|
@@ -48,6 +55,24 @@ class Foruiman::CLI < Thor
|
|
|
48
55
|
diagnostics&.close
|
|
49
56
|
end
|
|
50
57
|
|
|
58
|
+
desc "run COMMAND [ARGS...]", "Run a command or Procfile entry with the application's environment"
|
|
59
|
+
stop_on_unknown_option! :run
|
|
60
|
+
def run(*args)
|
|
61
|
+
raise Foruiman::Error, "run requires a command" if args.empty?
|
|
62
|
+
|
|
63
|
+
config = configuration
|
|
64
|
+
env = config.environment
|
|
65
|
+
command = Foruiman::Procfile.new(config.procfile)[args.first] if args.size == 1 && File.file?(config.procfile)
|
|
66
|
+
if command
|
|
67
|
+
exec(env, "/bin/sh", "-c", command, chdir: config.root, unsetenv_others: true)
|
|
68
|
+
else
|
|
69
|
+
# Preserve argv, terminal input, signals and the command's exact exit code.
|
|
70
|
+
exec(env, [args.first, args.first], *args.drop(1), chdir: config.root, unsetenv_others: true)
|
|
71
|
+
end
|
|
72
|
+
rescue Foruiman::Error, SystemCallError => e
|
|
73
|
+
raise Thor::Error, e.message
|
|
74
|
+
end
|
|
75
|
+
|
|
51
76
|
desc "check", "Validate the Procfile, environment files, ports, and log capacity without starting processes"
|
|
52
77
|
def check
|
|
53
78
|
engine = build_engine
|
|
@@ -65,19 +90,15 @@ class Foruiman::CLI < Thor
|
|
|
65
90
|
|
|
66
91
|
private
|
|
67
92
|
|
|
68
|
-
def
|
|
69
|
-
|
|
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
|
|
93
|
+
def configuration
|
|
94
|
+
@configuration ||= Foruiman::Configuration.new(options)
|
|
73
95
|
end
|
|
74
96
|
|
|
75
97
|
def build_engine
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
port: integer_option(:port), log_lines: integer_option(:log_lines))
|
|
98
|
+
config = configuration
|
|
99
|
+
env = config.environment
|
|
100
|
+
Foruiman::Engine.new(procfile: config.procfile, root: config.root, env: env,
|
|
101
|
+
port: config.port(env), log_lines: config.log_lines,
|
|
102
|
+
term_timeout: config.timeout, exit_on: config.exit_on)
|
|
82
103
|
end
|
|
83
104
|
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
|