foruiman 0.2.0 → 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: 726795acd4c4991d3a924364ec484ca7ec1e7051b8c9a660a53e6b8ecd1759f8
4
- data.tar.gz: 0c24bdd2fded37d051b8bb0e771991b0284893888a89b3a461662d2cc483dee4
3
+ metadata.gz: 906d36f65025e56552898118d3a3e5fec8b81c6046f32a7df9c627f048a89aa6
4
+ data.tar.gz: af7c8f777be7302aca9bd3e5cb50a17cc9acd9e2c6036e5d5b8033779d0a492f
5
5
  SHA512:
6
- metadata.gz: 68b7fa94a095c09fc3ed636165ee88db37640d24aa2ede0b4d2c417680cf2655910b881ad2180116babc701949c91cc9f751f2e0b67f222d081f71ca9153a178
7
- data.tar.gz: 22ec4c35cd32fd23d897ca2de19d72d5c219b53d0b86019f3fc3f562b3436371bd353ca5119aac2d4e1c559c68ddca37984c53b68e5e036cc12538e04f71321e
6
+ metadata.gz: b24d2f3e374331fe3c054c7930f5f1e5cd1e9bcc12e85befa8aeaf7bf8c367b70ab67942c19ac8a3fed56930ce9236a3cf422121a84bc2043584356450035ae0
7
+ data.tar.gz: 2b78d3244f014e4cf2e4959a8bedb62b308fb04c38fc8326d151d015983feeb8373a346852f2e730360b4e31a10a591dc63e410990e861571351646201ac9026
data/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
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
+
5
16
  ## 0.2.0
6
17
 
7
18
  - Keep child stdin open so default watcher modes continue running.
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
  ```
@@ -59,13 +61,19 @@ process tab, `s` starts it again.
59
61
  | Option | Default | Purpose |
60
62
  | --- | --- | --- |
61
63
  | `-f`, `--procfile FILE` | `Procfile` | Procfile to run |
62
- | `-d`, `--root DIR` | Current directory | Working directory |
63
- | `-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` |
64
66
  | `--no-dotenv` | `.env` enabled | Skip `.env` |
65
- | `-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 |
66
70
  | `--log-lines N` | `10000` | Retained records per process and in `all` |
67
71
  | `--no-tui` | TUI when interactive | Force plain output |
68
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
+
69
77
  ## Keyboard
70
78
 
71
79
  | Key | Action |
data/docs/ARCHITECTURE.md CHANGED
@@ -4,6 +4,9 @@
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
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
@@ -47,8 +50,10 @@ Call engine methods and event listeners on the driving thread; this is not a
47
50
  cross-thread messaging API. `run` handles INT/TERM/HUP and restores existing
48
51
  handlers. An embedding loop that drives `tick` directly owns its signal handling.
49
52
  `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
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
52
57
  rendering; callers should not mutate it.
53
58
 
54
59
  Children inherit the configured input stream in plain and embedded use. Before
@@ -1,26 +1,74 @@
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.2.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, `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
22
  | 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 |
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 |
25
+
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.
24
72
 
25
73
  Full-screen child terminal applications and background daemonization are outside
26
74
  this release. Remote process control, persistence across Foruiman sessions,
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,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
@@ -18,12 +18,16 @@ class Foruiman::Engine
18
18
  keyword_init: true)
19
19
  Event = Data.define(:type, :name, :pid, :status, :record, :message)
20
20
 
21
- attr_reader :logs, :env, :processes, :root, :procfile_path
21
+ attr_reader :logs, :env, :processes, :root, :procfile_path, :term_timeout, :exit_on
22
22
 
23
23
  def initialize(procfile: nil, root: Dir.pwd, env: ENV.to_h, input: $stdin, port: 5000, log_lines: 10_000,
24
- term_timeout: TERM_TIMEOUT)
24
+ term_timeout: TERM_TIMEOUT, exit_on: :all)
25
25
  raise Foruiman::Error, "port must be an integer in 1..65535" unless port.is_a?(Integer) && (1..65_535).cover?(port)
26
26
  raise Foruiman::Error, "log-lines must be a positive integer" unless log_lines.is_a?(Integer) && log_lines.positive?
27
+ unless term_timeout.is_a?(Numeric) && term_timeout.real? && term_timeout.finite? && term_timeout >= 0
28
+ raise Foruiman::Error, "timeout must be a finite nonnegative number"
29
+ end
30
+ raise Foruiman::Error, "exit-on must be all, any, or failure" unless %i[all any failure].include?(exit_on)
27
31
 
28
32
  @root = File.expand_path(root)
29
33
  raise Foruiman::Error, "working directory does not exist: #{@root}" unless File.directory?(@root)
@@ -34,6 +38,7 @@ class Foruiman::Engine
34
38
  @base_port = port
35
39
  @log_lines = log_lines
36
40
  @term_timeout = term_timeout
41
+ @exit_on = exit_on
37
42
  @processes = []
38
43
  @names = {}
39
44
  @running = {}
@@ -208,7 +213,9 @@ class Foruiman::Engine
208
213
  end
209
214
 
210
215
  def exit_code
211
- @explicit_shutdown || !@failed ? 0 : 1
216
+ return 0 if @explicit_shutdown
217
+
218
+ @policy_exit_code || (@failed ? 1 : 0)
212
219
  end
213
220
 
214
221
  def run(keep_open: false)
@@ -282,6 +289,7 @@ class Foruiman::Engine
282
289
  entry.restart_pending = false
283
290
  @failed = true
284
291
  lifecycle(entry, :failed, "failed to start: #{e.message}")
292
+ apply_exit_policy(1)
285
293
  return
286
294
  end
287
295
  entry.pid = entry.pgid = pid
@@ -337,10 +345,18 @@ class Foruiman::Engine
337
345
  @failed ||= !success && !transitioning
338
346
  entry.status = success ? :exited : :failed unless transitioning
339
347
  lifecycle(entry, :exited, termination_message_for(entry.exit_status))
348
+ apply_exit_policy(entry.exit_status.exitstatus || (128 + entry.exit_status.termsig)) unless transitioning
340
349
  terminate(entry)
341
350
  end
342
351
  end
343
352
 
353
+ def apply_exit_policy(code)
354
+ return if @shutdown || exit_on == :all || (exit_on == :failure && code.zero?)
355
+
356
+ @policy_exit_code = code
357
+ shutdown(explicit: false)
358
+ end
359
+
344
360
  def advance_groups
345
361
  processes.each do |entry|
346
362
  next unless entry.pgid
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Foruiman
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.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.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Foruiman contributors
@@ -70,6 +70,7 @@ files:
70
70
  - lib/foruiman.rb
71
71
  - lib/foruiman/ansi.rb
72
72
  - lib/foruiman/cli.rb
73
+ - lib/foruiman/configuration.rb
73
74
  - lib/foruiman/diagnostics.rb
74
75
  - lib/foruiman/engine.rb
75
76
  - lib/foruiman/env.rb
@@ -112,7 +113,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
112
113
  - !ruby/object:Gem::Version
113
114
  version: '0'
114
115
  requirements: []
115
- rubygems_version: 4.0.16
116
+ rubygems_version: 4.0.20
116
117
  specification_version: 4
117
118
  summary: A Procfile supervisor with process tabs, bounded logs, and isolated restarts
118
119
  test_files: []