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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1042e6507c0e480ec99a84c3ef81f874eb46d3c937f3c3878940dfb065df2e87
4
- data.tar.gz: 99e20e7324f22619d6ec67938fcc31ee96bd6b083bb1817674ccbffe1ceca715
3
+ metadata.gz: ed6ed5844031c4aed946289c735aa1956a5f88deb571bb67dcd163b3fd209316
4
+ data.tar.gz: 4466f5755eb45b06fa58ebded9b4ef81a0104aa60808c07a277be58081924d1e
5
5
  SHA512:
6
- metadata.gz: 83e28d56aaccb65d679bdfdbc260fa2d887197d90db8f323f67d9d2f8c2c467426b5f1d7b72fab93897c6ad205bbecc3a53cb85b8681951ea4de17fc1c4b8c49
7
- data.tar.gz: e5b5ac7371bfc02554c7d9123502fcd9165a744f20e2573860e3b9d50277ccf5461f184f27d9b83eb06b93074129e725b7a4f8325a8ac7d8d26fde3fae95675f
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 encrypts and signs a hash that's
13
- # stored on the client's file sytem. Conceptually, it's similar to Rails signed
14
- # and encrypted client-side cookies.
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
 
@@ -1,241 +1,17 @@
1
- require "terminalwire-server"
2
- require "thor"
3
- require "rails"
4
- require "jwt"
5
-
6
- module Terminalwire
7
- module Rails
8
- class Channel
9
- # Prefix used to for channel name to avoid conflicts.
10
- NAMESPACE = "terminalwire".freeze
11
-
12
- # Length of random channel name.
13
- RANDOM_NAME_LENGTH = 32
14
-
15
- # 5 minutes, which is really way too much time.
16
- TIMEOUT_SECONDS = 5 * 60
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.3.5.alpha2
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: 2025-05-01 00:00:00.000000000 Z
10
+ date: 2026-08-14 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: terminalwire-server
13
+ name: terminalwire
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.3.5.alpha2
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.3.5.alpha2
25
+ version: 2.0.0
26
26
  - !ruby/object:Gem::Dependency
27
- name: rails
27
+ name: activesupport
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
- - - "~>"
30
+ - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: '7.2'
33
- type: :development
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.2'
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
- description: Stream command-line apps from your server without a web API
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/ruby
98
+ homepage: https://terminalwire.com
69
99
  licenses:
70
- - AGPL
100
+ - Apache-2.0
71
101
  metadata:
72
- allowed_push_host: https://rubygems.org/
73
- homepage_uri: https://terminalwire.com/ruby
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.0.0
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: Ship a CLI for your web app. No API required.
120
+ summary: Drop-in Terminalwire v2 integration for Rails
94
121
  test_files: []