terminalwire-rails 0.3.5.alpha2 → 2.0.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/README.md +31 -0
- data/lib/generators/terminalwire/install/templates/application_terminal.rb.tt +3 -3
- data/lib/terminalwire/rails.rb +17 -241
- metadata +50 -23
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ed6ed5844031c4aed946289c735aa1956a5f88deb571bb67dcd163b3fd209316
|
|
4
|
+
data.tar.gz: 4466f5755eb45b06fa58ebded9b4ef81a0104aa60808c07a277be58081924d1e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4930364b90f660e7c06b0b60bed426e21b152726c30b0fcf41256efbed337322b91d939f66e2794c2df9d13d62a1bdfb3559c323483ecfa3fb37c8fa8304d3a0
|
|
7
|
+
data.tar.gz: 2383456285b2b09a28f5baedecab2218707a9b23cce04068c5f565b5077a3d6b965073fb72619b238acc8b1267b2fcdb9b125f62679fe485f62689e7a855cc33
|
data/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# terminalwire-rails
|
|
2
|
+
|
|
3
|
+
Drop-in [Terminalwire](https://terminalwire.com) integration for Rails — serve a
|
|
4
|
+
Thor CLI over Terminalwire straight from your Rails app. You write your CLI *in
|
|
5
|
+
your app* (calling your models and business logic directly); your users install
|
|
6
|
+
one small, self-updating client with a single `curl … | bash`.
|
|
7
|
+
|
|
8
|
+
```ruby
|
|
9
|
+
# Gemfile
|
|
10
|
+
gem "terminalwire-rails"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then follow the Rails guide to mount your CLI and ship it.
|
|
14
|
+
|
|
15
|
+
## Documentation
|
|
16
|
+
|
|
17
|
+
- Rails guide: https://terminalwire.com/docs/rails
|
|
18
|
+
- Client manual: https://terminalwire.com/docs/client
|
|
19
|
+
- All docs: https://terminalwire.com/docs
|
|
20
|
+
|
|
21
|
+
## License
|
|
22
|
+
|
|
23
|
+
The Terminalwire **server** (this gem) is open source under the **Apache License
|
|
24
|
+
2.0** — free to use, modify, and self-host, including commercially.
|
|
25
|
+
|
|
26
|
+
The Terminalwire **client** your users install is proprietary, and only
|
|
27
|
+
Terminalwire distributes it. Shipping a **commercial** CLI to your users needs a
|
|
28
|
+
subscription, priced per active client install:
|
|
29
|
+
|
|
30
|
+
- Pricing & licensing: https://terminalwire.com/pricing
|
|
31
|
+
- Terms: https://terminalwire.com/terms — Privacy: https://terminalwire.com/privacy
|
|
@@ -9,9 +9,9 @@ class ApplicationTerminal < Thor
|
|
|
9
9
|
private
|
|
10
10
|
|
|
11
11
|
def current_user=(user)
|
|
12
|
-
# The Session object is a hash-like object that
|
|
13
|
-
# stored on the client's file
|
|
14
|
-
#
|
|
12
|
+
# The Session object is a hash-like object that's encrypted and signed
|
|
13
|
+
# (AES-256-GCM, keyed off secret_key_base) and stored on the client's file
|
|
14
|
+
# system. Conceptually, it's similar to Rails' encrypted client-side cookies.
|
|
15
15
|
session["user_id"] = user.id
|
|
16
16
|
end
|
|
17
17
|
|
data/lib/terminalwire/rails.rb
CHANGED
|
@@ -1,241 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
attr_reader :name, :server, :logger, :timeout
|
|
19
|
-
|
|
20
|
-
def initialize(name: self.class.random_name, server: self.class.server, logger: self.class.logger, timeout: TIMEOUT_SECONDS)
|
|
21
|
-
@server = server
|
|
22
|
-
@name = name
|
|
23
|
-
@queue = Queue.new
|
|
24
|
-
@logger = logger
|
|
25
|
-
@timeout = timeout
|
|
26
|
-
|
|
27
|
-
yield self if block_given?
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def broadcast(message)
|
|
31
|
-
@logger.info "ActiveExchange: Publishing #{message.inspect} to #{@name.inspect}"
|
|
32
|
-
@server.broadcast(@name, message)
|
|
33
|
-
end
|
|
34
|
-
alias :publish :broadcast
|
|
35
|
-
|
|
36
|
-
def subscribe
|
|
37
|
-
return if @queue.closed?
|
|
38
|
-
@logger.info "ActiveExchange: Subscribed to #{@name.inspect}"
|
|
39
|
-
@server.subscribe(@name, -> (message) { @queue << message })
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def read
|
|
43
|
-
Timeout::timeout @timeout do
|
|
44
|
-
subscribe
|
|
45
|
-
@logger.debug "Queue: waiting"
|
|
46
|
-
@queue.pop
|
|
47
|
-
end
|
|
48
|
-
ensure
|
|
49
|
-
@logger.debug "Queue: closed"
|
|
50
|
-
@queue.close
|
|
51
|
-
end
|
|
52
|
-
alias :pop :read
|
|
53
|
-
|
|
54
|
-
def to_param
|
|
55
|
-
self.class.verifier.generate(@name, expires_in: @timeout)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
class << self
|
|
59
|
-
def server
|
|
60
|
-
ActionCable.server.pubsub
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def logger
|
|
64
|
-
::Rails.logger
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def random_name
|
|
68
|
-
[NAMESPACE, SecureRandom.hex(RANDOM_NAME_LENGTH)].join(":")
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Use Rails' secret_key_base for signing
|
|
72
|
-
def verifier
|
|
73
|
-
@verifier ||= ActiveSupport::MessageVerifier.new(::Rails.application.secret_key_base)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def from_param(...)
|
|
77
|
-
new name: verifier.verify(...)
|
|
78
|
-
end
|
|
79
|
-
alias :find :from_param
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
class Session
|
|
84
|
-
# JWT file name for the session file.
|
|
85
|
-
FILENAME = "session.jwt"
|
|
86
|
-
|
|
87
|
-
# Empty dictionary the user can stash all their session data into.
|
|
88
|
-
EMPTY_SESSION = {}.freeze
|
|
89
|
-
|
|
90
|
-
extend Forwardable
|
|
91
|
-
|
|
92
|
-
# Delegate `dig` and `fetch` to the `read` method
|
|
93
|
-
def_delegators :read,
|
|
94
|
-
:dig, :fetch, :[]
|
|
95
|
-
|
|
96
|
-
def initialize(context:, path: nil, secret_key: self.class.secret_key)
|
|
97
|
-
@context = context
|
|
98
|
-
@path = Pathname.new(path || context.storage_path)
|
|
99
|
-
@config_file_path = @path.join(FILENAME)
|
|
100
|
-
@secret_key = secret_key
|
|
101
|
-
|
|
102
|
-
ensure_file
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def read
|
|
106
|
-
jwt_token = @context.file.read(@config_file_path)
|
|
107
|
-
decoded_data = JWT.decode(jwt_token, @secret_key, true, algorithm: 'HS256')
|
|
108
|
-
decoded_data[0] # JWT payload is the first element in the array
|
|
109
|
-
rescue JWT::DecodeError => e
|
|
110
|
-
raise "Invalid or tampered file: #{e.message}"
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def reset
|
|
114
|
-
@context.file.delete @config_file_path
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def edit
|
|
118
|
-
config = read
|
|
119
|
-
yield config
|
|
120
|
-
write(config)
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def []=(key, value)
|
|
124
|
-
edit { |config| config[key] = value }
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def write(config)
|
|
128
|
-
token = JWT.encode(config, @secret_key, 'HS256')
|
|
129
|
-
@context.file.write(@config_file_path, token)
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
private
|
|
133
|
-
|
|
134
|
-
def ensure_file
|
|
135
|
-
return true if @context.file.exist? @config_file_path
|
|
136
|
-
# Create the path if it doesn't exist on the client.
|
|
137
|
-
@context.directory.create @path
|
|
138
|
-
# Write an empty configuration on initialization
|
|
139
|
-
write(EMPTY_SESSION)
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def self.secret_key
|
|
143
|
-
::Rails.application.secret_key_base
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
class Thor < Server::WebSocket
|
|
148
|
-
include Logging
|
|
149
|
-
|
|
150
|
-
def initialize(cli_class)
|
|
151
|
-
@cli_class = cli_class
|
|
152
|
-
|
|
153
|
-
unless @cli_class.included_modules.include?(Terminalwire::Server::Thor)
|
|
154
|
-
raise 'Add `include Terminalwire::Server::Thor` to the #{@cli_class.inspect} class.'
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def error_message
|
|
159
|
-
"An error occurred. Please try again."
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def handle(adapter:, env:)
|
|
163
|
-
logger.info "ThorServer: Running #{@cli_class.inspect}"
|
|
164
|
-
while message = adapter.read
|
|
165
|
-
case message
|
|
166
|
-
in { event: "initialization", protocol:, program: { arguments: }, entitlement: }
|
|
167
|
-
context = Terminalwire::Server::Context.new(adapter:, entitlement:)
|
|
168
|
-
exit_code = 0
|
|
169
|
-
|
|
170
|
-
begin
|
|
171
|
-
@cli_class.terminalwire arguments:, context: do |cli|
|
|
172
|
-
cli.default_url_options[:host] = env["HTTP_HOST"]
|
|
173
|
-
end
|
|
174
|
-
rescue ::Thor::UndefinedCommandError, ::Thor::InvocationError => e
|
|
175
|
-
context.stdout.puts e.message
|
|
176
|
-
rescue ::StandardError => e
|
|
177
|
-
# Log the error
|
|
178
|
-
handler_error_message = <<~ERROR
|
|
179
|
-
#{e.class.name} (#{e.message})
|
|
180
|
-
|
|
181
|
-
#{e.backtrace.join("\n")}
|
|
182
|
-
ERROR
|
|
183
|
-
|
|
184
|
-
::Rails.logger.error(handler_error_message)
|
|
185
|
-
# Report the error to Rails' notification system
|
|
186
|
-
::Rails.error.report(e, handled: true)
|
|
187
|
-
|
|
188
|
-
if ::Rails.application.config.consider_all_requests_local
|
|
189
|
-
# Show the full error message with stack trace in development
|
|
190
|
-
context.stderr.puts handler_error_message
|
|
191
|
-
else
|
|
192
|
-
# Show a generic message in production
|
|
193
|
-
context.stderr.puts error_message
|
|
194
|
-
end
|
|
195
|
-
exit_code = 1
|
|
196
|
-
ensure
|
|
197
|
-
context.exit exit_code
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
end
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
# If I move this, then the current production integration breaks because
|
|
206
|
-
# it wants `include Terminalwire::Thor`.
|
|
207
|
-
module Thor
|
|
208
|
-
class Shell < Terminalwire::Server::Thor::Shell
|
|
209
|
-
attr_reader :session
|
|
210
|
-
|
|
211
|
-
def initialize(context, *, **, &)
|
|
212
|
-
@session = Terminalwire::Rails::Session.new(context:)
|
|
213
|
-
super(context, *,**,&)
|
|
214
|
-
end
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
def self.included(base)
|
|
218
|
-
base.include Terminalwire::Server::Thor
|
|
219
|
-
base.extend ClassMethods
|
|
220
|
-
|
|
221
|
-
# I have to do this in a block to deal with some of Thor's DSL
|
|
222
|
-
base.class_eval do
|
|
223
|
-
protected
|
|
224
|
-
|
|
225
|
-
no_commands do
|
|
226
|
-
def_delegators :shell,
|
|
227
|
-
:session
|
|
228
|
-
|
|
229
|
-
include ::Rails.application.routes.url_helpers
|
|
230
|
-
end
|
|
231
|
-
end
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
module ClassMethods
|
|
235
|
-
# Use the extend Rails shell with this shell
|
|
236
|
-
def terminalwire_shell(context)
|
|
237
|
-
Terminalwire::Thor::Shell.new(context)
|
|
238
|
-
end
|
|
239
|
-
end
|
|
240
|
-
end
|
|
241
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# terminalwire-rails 2.x: the v2 Rails integration.
|
|
4
|
+
#
|
|
5
|
+
# Bundler auto-requires this for `gem "terminalwire-rails"`. It loads the v2 server
|
|
6
|
+
# and defines the v1 API names backed by v2 (see terminalwire/v2/rails.rb):
|
|
7
|
+
#
|
|
8
|
+
# * Terminalwire::Thor -> the v2 Rails terminal mixin (include in your CLI)
|
|
9
|
+
# * Terminalwire::Rails::Thor -> the v2 Rack handler (mount in routes)
|
|
10
|
+
# * Terminalwire::Rails::Session -> the v2 encrypted client session
|
|
11
|
+
# (AES-256-GCM via ActiveSupport::MessageEncryptor; reads and migrates
|
|
12
|
+
# legacy signed-only JWT sessions on first access)
|
|
13
|
+
#
|
|
14
|
+
# So a 1.x/0.x app upgrades to v2 by bumping this gem to "~> 2.0" and redeploying —
|
|
15
|
+
# its unchanged `include Terminalwire::Thor` and
|
|
16
|
+
# `match "/terminal", to: Terminalwire::Rails::Thor.new(MainTerminal)` now serve v2.
|
|
17
|
+
require "terminalwire/v2/rails"
|
metadata
CHANGED
|
@@ -1,63 +1,93 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: terminalwire-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brad Gessler
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 2026-08-14 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
|
-
name: terminalwire
|
|
13
|
+
name: terminalwire
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.
|
|
18
|
+
version: 2.0.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.
|
|
25
|
+
version: 2.0.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
|
-
name:
|
|
27
|
+
name: activesupport
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
|
-
- - "
|
|
30
|
+
- - ">="
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '7.
|
|
33
|
-
type: :
|
|
32
|
+
version: '7.1'
|
|
33
|
+
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
|
-
- - "
|
|
37
|
+
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '7.
|
|
39
|
+
version: '7.1'
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: jwt
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
|
-
- - "
|
|
44
|
+
- - ">="
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
46
|
version: '2.0'
|
|
47
47
|
type: :runtime
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
|
-
- - "
|
|
51
|
+
- - ">="
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '2.0'
|
|
54
|
-
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: thor
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '1.3'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '1.3'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rails
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '7.2'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '7.2'
|
|
82
|
+
description: 'Serve a Thor CLI over Terminalwire v2 from a Rails app. Drop-in for
|
|
83
|
+
the v1 terminalwire-rails: bump to 2.x and redeploy.'
|
|
55
84
|
email:
|
|
56
85
|
- brad@terminalwire.com
|
|
57
86
|
executables: []
|
|
58
87
|
extensions: []
|
|
59
88
|
extra_rdoc_files: []
|
|
60
89
|
files:
|
|
90
|
+
- README.md
|
|
61
91
|
- lib/generators/terminalwire/install/USAGE
|
|
62
92
|
- lib/generators/terminalwire/install/install_generator.rb
|
|
63
93
|
- lib/generators/terminalwire/install/templates/application_terminal.rb.tt
|
|
@@ -65,15 +95,12 @@ files:
|
|
|
65
95
|
- lib/generators/terminalwire/install/templates/main_terminal.rb
|
|
66
96
|
- lib/terminalwire-rails.rb
|
|
67
97
|
- lib/terminalwire/rails.rb
|
|
68
|
-
homepage: https://terminalwire.com
|
|
98
|
+
homepage: https://terminalwire.com
|
|
69
99
|
licenses:
|
|
70
|
-
-
|
|
100
|
+
- Apache-2.0
|
|
71
101
|
metadata:
|
|
72
|
-
allowed_push_host: https://rubygems.org
|
|
73
|
-
|
|
74
|
-
source_code_uri: https://github.com/terminalwire/ruby/tree/main/terminalwire-rails
|
|
75
|
-
changelog_uri: https://github.com/terminalwire/ruby/tags
|
|
76
|
-
funding_uri: https://terminalwire.com/funding
|
|
102
|
+
allowed_push_host: https://rubygems.org
|
|
103
|
+
source_code_uri: https://github.com/terminalwire/ruby/tree/main/gem/terminalwire-rails
|
|
77
104
|
rdoc_options: []
|
|
78
105
|
require_paths:
|
|
79
106
|
- lib
|
|
@@ -81,7 +108,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
81
108
|
requirements:
|
|
82
109
|
- - ">="
|
|
83
110
|
- !ruby/object:Gem::Version
|
|
84
|
-
version: 3.
|
|
111
|
+
version: 3.2.0
|
|
85
112
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
86
113
|
requirements:
|
|
87
114
|
- - ">="
|
|
@@ -90,5 +117,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
90
117
|
requirements: []
|
|
91
118
|
rubygems_version: 3.6.2
|
|
92
119
|
specification_version: 4
|
|
93
|
-
summary:
|
|
120
|
+
summary: Drop-in Terminalwire v2 integration for Rails
|
|
94
121
|
test_files: []
|