Module: Copse

Defined in:
lib/copse.rb,
lib/copse/railtie.rb,
lib/copse/session.rb,
lib/copse/version.rb,
lib/copse/database.rb,
lib/copse/procfile.rb,
lib/copse/worktree.rb,
lib/copse/url_options.rb,
lib/generators/copse/install_generator.rb

Overview

Copse gives every Rails app and every git worktree its own hostname and its own port, derived rather than assigned, so nothing collides and nothing has to be chosen.

Defined Under Namespace

Modules: Database, Generators, UrlOptions Classes: Procfile, Railtie, Session, Worktree

Constant Summary collapse

PORT_RANGE =

The port space derivation draws from.

(3000..9999).freeze
RESERVED_PORTS =

Well-known service ports inside PORT_RANGE. These are excluded from the available set rather than probed around, so derivation stays a pure function of the hostname -- a reserved port is unreachable by construction.

Changing this list shifts every index in AVAILABLE_PORTS above the change and therefore moves most derived ports. That is a breaking change to the promise that teammates derive the same port (R4); it requires a major version bump.

[
  3036, # vite_ruby dev server default
  3306, # MySQL
  3389, # RDP
  4200, # Angular dev server
  4369, # Erlang EPMD
  5000, # macOS AirPlay Receiver, Flask
  5060, # SIP
  5173, # Vite default
  5432, # PostgreSQL
  5601, # Kibana
  5672, # RabbitMQ
  5900, # VNC
  6379, # Redis
  6432, # PgBouncer
  7000, # macOS AirPlay Receiver, Cassandra
  8000, # common alternate HTTP
  8025, # Mailhog / Mailpit web UI
  8080, # common alternate HTTP
  8443, # common alternate HTTPS
  8500, # Consul
  8888, # Jupyter
  9000, # PHP-FPM, SonarQube
  9092, # Kafka
  9200, # Elasticsearch
  9418  # git protocol
].freeze
AVAILABLE_PORTS =

The ordered set derivation indexes into. Frozen at load: there is no configuration accessor, so there is no memoized state that can go stale.

(PORT_RANGE.to_a - RESERVED_PORTS).freeze
PROCESS_MANAGERS =

The supervisors bin/dev can be generated against. Only the supervisor differs: the derivation is a pure function of the worktree and knows nothing about either one.

%w[foreman overmind].freeze
VERSION =
"0.1.0"

Class Method Summary collapse

Class Method Details

.companion_port_for(hostname) ⇒ Object

A second port derived from the same hostname, for a JS bundler's own dev server. Salting keeps it stable (R4) and inside the available set (so it can never land on a reserved service port) while guaranteeing it differs from the primary port for that same hostname.



92
93
94
95
96
97
98
99
# File 'lib/copse.rb', line 92

def self.companion_port_for(hostname)
  primary = port_for(hostname)
  index = Zlib.crc32("vite:#{hostname}") % AVAILABLE_PORTS.size
  candidate = AVAILABLE_PORTS.fetch(index)
  return candidate unless candidate == primary

  AVAILABLE_PORTS.fetch((index + 1) % AVAILABLE_PORTS.size)
end

.port_for(hostname) ⇒ Object

The derived port for a hostname. A pure function: same hostname, same port, on every machine, in every terminal, across reboots and Ruby versions.



84
85
86
# File 'lib/copse.rb', line 84

def self.port_for(hostname)
  AVAILABLE_PORTS.fetch(Zlib.crc32(hostname) % AVAILABLE_PORTS.size)
end

.start(root: Dir.pwd, process_manager: :foreman, args: []) ⇒ Object

Boots the app. This is what bin/dev calls.

Returns the exit status of the foreground process so bin/dev can propagate it, which matters for shell && chains and scripted use.

process_manager: :overmind hands the whole Procfile to Overmind instead, replacing this process, and args is forwarded to it (bin/dev -l web).



70
71
72
73
74
75
76
77
78
79
80
# File 'lib/copse.rb', line 70

def self.start(root: Dir.pwd, process_manager: :foreman, args: [])
  session = Session.new(Worktree.new(root))

  # `exec_overmind` never returns, so falling through to the foreman session
  # means overmind is not installed *on this machine*. That is a fallback rather
  # than an error on purpose: `bin/dev` is committed, and a teammate without
  # overmind should still boot.
  session.exec_overmind(args) if process_manager.to_s == "overmind" && session.overmind_available?

  session.start
end