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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0de0b215283d2c3dc89422aa9e6f6da235acf10f5ede3907afcf8e7c88dd9290
4
- data.tar.gz: 81dfcdcfbc8d7ab445242fd5c07419192b6445e9caa3107fcdce5a2a027d914e
3
+ metadata.gz: 906d36f65025e56552898118d3a3e5fec8b81c6046f32a7df9c627f048a89aa6
4
+ data.tar.gz: af7c8f777be7302aca9bd3e5cb50a17cc9acd9e2c6036e5d5b8033779d0a492f
5
5
  SHA512:
6
- metadata.gz: bb3cbdd6d6c38335bd5d6d4b467bbedec757cb2e534896b24d471c8b4275203d22cb22ec642fafe63410df21c1616322ae2e4616b0b26dce277959308f0092c5
7
- data.tar.gz: 03db66ce62f3e21723aac3808791b2a027bb2ab497d415de11730661ff90b5150b82a0d7351ea2c156abc93be670e93a5b575933d06bf6a0fdcdcece35f7b517
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` | Current directory | Working directory |
55
- | `-e`, `--env FILE` | None | Additional environment file |
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` | Stop selected / stop all |
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 null stdin.
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:` exists for deterministic embedded tests; CLI
50
- shutdown always uses five seconds. `state(name)` exposes lifecycle state for
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
- process names, stream markers, and content. Complete frames reset styles at each row. `Application` connects input and lifecycle
79
- commands to the engine and limits drawing to 30 FPS. Resize handling reads current
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`
@@ -1,27 +1,75 @@
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.
4
5
 
5
6
  | Area | Foruiman behavior |
6
7
  | --- | --- |
7
- | Identity | `foruiman` gem/executable, `Foruiman` namespace, version 0.1.2 |
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, `run`, `.foreman` YAML, custom shutdown timeout, forced color, or timestamp toggle |
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 | 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 |
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 process failures; peers keep running |
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-second grace, KILL; track groups after leader exit; restore prior signal handlers |
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 and stop controls; no child stdin |
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 |
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
- Commands needing a PTY, interactive child input, or background daemonization are
26
- outside this release. Remote process control, persistence across Foruiman sessions,
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
- 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
 
@@ -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. The header shows the active Procfile. `q` or Ctrl-C stops all owned process
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
- # Incremental byte parser: only text, newlines, and SGR can leave this class.
67
- # OSC (including clipboard), DCS, cursor controls, and other escapes are dropped.
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, 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
+ 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 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
93
+ def configuration
94
+ @configuration ||= Foruiman::Configuration.new(options)
73
95
  end
74
96
 
75
97
  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))
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