Module: Terminalwire::V2::Rails

Defined in:
lib/terminalwire/v2/rails.rb

Overview

Drop-in Rails integration for serving a Terminalwire CLI over BOTH the v1 and v2 wire on a SINGLE endpoint during the transition. The whole wiring is one line in config/routes.rb:

require "terminalwire/v2/rails"
match "/terminal", to: Terminalwire::V2::Rails.dual_terminal(MainTerminal),
                 via: [:get, :connect]

A v2 client advertises the terminalwire.v2 WebSocket subprotocol on the upgrade; the dispatcher detects that (before the socket is accepted) and routes to the v2 server, otherwise to the unchanged v1 handler. Same URL for both — the exec launcher's url: never changes. dualize extends the Thor tree so it's the SAME CLI answering on either wire, not a v2 fork.

For a v2-only app (no v1 sub-gems), skip this and just mount Terminalwire::V2::Server::Rack.new(cli) directly.

Defined Under Namespace

Modules: Thor Classes: Dispatcher, Session, VersionEndpoint

Constant Summary collapse

SUBPROTOCOL =
"terminalwire.v2"

Class Method Summary collapse

Class Method Details

.default_v1(cli) ⇒ Object

Lazily resolve the v1 handler so this gem doesn't hard-depend on the v1 terminalwire-rails gem at load time. Apps doing the transition have it; if not, fail with a clear message instead of a NameError.



262
263
264
265
266
267
268
269
# File 'lib/terminalwire/v2/rails.rb', line 262

def self.default_v1(cli)
  unless defined?(Terminalwire::Rails::Thor)
    raise "terminalwire/v2/rails: the v1 handler (Terminalwire::Rails::Thor) " \
          "isn't loaded. Add the v1 `terminalwire-rails` gem, or pass v2:-only " \
          "and mount Terminalwire::V2::Server::Rack directly for a v2-only app."
  end
  Terminalwire::Rails::Thor.new(cli)
end

.dual_terminal(cli, v1: nil, v2: nil) ⇒ Object

Returns a Rack endpoint that serves cli over both protocols. Pass v1:/v2: to override the handlers (tests, custom adapters); by default it builds the stock v1 Terminalwire::Rails::Thor and v2 Terminalwire::V2::Server::Rack.



210
211
212
213
214
215
216
# File 'lib/terminalwire/v2/rails.rb', line 210

def self.dual_terminal(cli, v1: nil, v2: nil)
  Terminalwire::V2::Server.dualize(cli)
  Dispatcher.new(
    v1: v1 || default_v1(cli),
    v2: v2 || Terminalwire::V2::Server::Rack.new(cli, verbose: verbose?, report: report)
  )
end

.reportObject

Log + report unexpected command errors to Rails (mirrors the v1 handler). Without this the v2 Handler drops the exception on the floor behind the generic message, which is exactly what made the missing-host bug hard to find.



252
253
254
255
256
257
# File 'lib/terminalwire/v2/rails.rb', line 252

def self.report
  lambda do |error|
    ::Rails.error.report(error, handled: true) if ::Rails.respond_to?(:error)
    ::Rails.logger&.error("terminalwire: #{error.class}: #{error.message}\n#{Array(error.backtrace).join("\n")}")
  end
end

.terminal(cli, verbose: nil, report: nil) ⇒ Object

The v2-DEFAULT endpoint: serve cli over v2, with no v1. Mount it the same way as dual_terminal:

match "/terminal", to: Terminalwire::V2::Rails.terminal(MainTerminal),
                 via: [:get, :connect]

It returns a version endpoint (not the bare Rack): a connection advertising the terminalwire.v2 subprotocol — and any connection that doesn't ask for another version — is served by the v2 server. The endpoint is the forward-compatible seam: a future v3 registers another handler here without changing the app's route. (A bare Rack handed to match to: drops streaming output in production; the endpoint, like dual_terminal's, is what Rails routing needs.)



230
231
232
233
234
235
236
237
238
# File 'lib/terminalwire/v2/rails.rb', line 230

def self.terminal(cli, verbose: nil, report: nil)
  Terminalwire::V2::Server.terminalize(cli)
  v2 = Terminalwire::V2::Server::Rack.new(
    cli,
    verbose: verbose.nil? ? verbose?() : verbose,
    report: report || self.report
  )
  VersionEndpoint.new(default: v2, by_subprotocol: { SUBPROTOCOL => v2 })
end

.verbose?Boolean

In dev/test, show the full backtrace to the client (consider_all_requests_local, like v1). In production the client sees the generic message — but the real exception is still LOGGED + reported (below), never silently swallowed.

Returns:

  • (Boolean)


243
244
245
246
247
# File 'lib/terminalwire/v2/rails.rb', line 243

def self.verbose?
  ::Rails.application.config.consider_all_requests_local
rescue StandardError
  false
end