wokku-cli 0.1.0 → 0.2.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/CHANGELOG.md +17 -0
- data/lib/wokku/auth.rb +117 -0
- data/lib/wokku/cable_client.rb +121 -0
- data/lib/wokku/commands/auth.rb +11 -28
- data/lib/wokku/commands/shell.rb +68 -0
- data/lib/wokku/pty_session.rb +82 -0
- data/lib/wokku/version.rb +1 -1
- data/lib/wokku.rb +1 -0
- metadata +20 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 90dbd93905c3fc3dc7422cf4342def249b9ff04d3c5bbefd64f5d4a5ccc23cb6
|
|
4
|
+
data.tar.gz: 01b1043ff82e7ae509ddb36eed9fa6d9657b7321ae82502996c448eb4d755440
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7650db80c9d0608c8f879c86f556008ae47af5cd3827e029378e7327e455b9762ef6f4260a73f0d1da61c7c8a93b2bdfe03fd01fae1ca3212e60acbbfa61bc56
|
|
7
|
+
data.tar.gz: 33fbc61d1092de38e30737cedcb83750bc55fa8193f1cf3604599e5ecf7e6103bc16faa7775c8bab0217d44dc3ba2b33cae6804c921007888f9097c277e94f78
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0 — 2026-05-06
|
|
4
|
+
|
|
5
|
+
Interactive shell over WebSocket: open a real PTY in your app's container, exec one-off commands, and connect to managed databases without leaving the terminal.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `wokku enter APP` — interactive shell in the app's running container.
|
|
9
|
+
- `wokku ps:exec APP [-t|-T] -- CMD [ARGS...]` — run a command in a one-off container (Heroku-`run`-style; auto-detects TTY, `-t`/`-T` overrides).
|
|
10
|
+
- `wokku databases:connect DB` — interactive client for the managed database (psql / redis-cli / mysql via dokku's `<plugin>:connect`).
|
|
11
|
+
|
|
12
|
+
### Internals
|
|
13
|
+
- New `Wokku::CableClient` — thin `websocket-driver` wrapper, Bearer-token handshake on `/cable`.
|
|
14
|
+
- New `Wokku::PtySession` — local raw-mode PTY orchestration with SIGWINCH resize and guaranteed `cooked!` restore.
|
|
15
|
+
- Runtime dependency: `websocket-driver ~> 0.7`.
|
|
16
|
+
|
|
17
|
+
### Requires
|
|
18
|
+
- wokku-cloud server-side support shipped 2026-05-06 (Bearer auth on `ApplicationCable::Connection`, new `TerminalChannel` wire protocol with modes).
|
|
19
|
+
|
|
3
20
|
## 0.1.0 — 2026-05-04
|
|
4
21
|
|
|
5
22
|
First public release on RubyGems.
|
data/lib/wokku/auth.rb
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module Wokku
|
|
8
|
+
# Authentication flows for `wokku auth:login`.
|
|
9
|
+
#
|
|
10
|
+
# CLI requests a device_code + user_code from /auth/device/code, opens
|
|
11
|
+
# the verification URL in the browser, and polls /auth/device/token
|
|
12
|
+
# until the user approves the session in the dashboard. Server returns
|
|
13
|
+
# an api_token which the CLI persists to ~/.wokku/config.
|
|
14
|
+
module Auth
|
|
15
|
+
POLL_FLOOR = 1.0 # don't poll faster than 1s regardless of server hint
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def login_with_device_flow!(url)
|
|
20
|
+
code = post_json(url, "/auth/device/code", {})
|
|
21
|
+
user_code = code.fetch("user_code")
|
|
22
|
+
device_code = code.fetch("device_code")
|
|
23
|
+
verify_uri = code.fetch("verification_uri_complete")
|
|
24
|
+
interval = (code["interval"] || 5).to_i
|
|
25
|
+
expires_at = Time.now + (code["expires_in"] || 600).to_i
|
|
26
|
+
|
|
27
|
+
puts
|
|
28
|
+
puts " To finish signing in, open this URL in your browser:"
|
|
29
|
+
puts
|
|
30
|
+
puts " #{verify_uri}"
|
|
31
|
+
puts
|
|
32
|
+
puts " And confirm the code: \e[1m#{user_code}\e[0m"
|
|
33
|
+
puts
|
|
34
|
+
open_browser(verify_uri)
|
|
35
|
+
print " Waiting for approval"
|
|
36
|
+
|
|
37
|
+
loop do
|
|
38
|
+
if Time.now > expires_at
|
|
39
|
+
puts
|
|
40
|
+
abort "Login timed out. Run `wokku auth:login` again."
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
sleep_for([interval, POLL_FLOOR].max)
|
|
44
|
+
print "."
|
|
45
|
+
$stdout.flush
|
|
46
|
+
|
|
47
|
+
resp = post_raw(url, "/auth/device/token", { device_code: device_code })
|
|
48
|
+
body = JSON.parse(resp.body) rescue {}
|
|
49
|
+
case resp.code.to_i
|
|
50
|
+
when 200
|
|
51
|
+
token = body.fetch("token")
|
|
52
|
+
email = body.dig("user", "email")
|
|
53
|
+
save_config({ "api_url" => url, "token" => token, "email" => email })
|
|
54
|
+
puts
|
|
55
|
+
Wokku::Output.status "Logged in as #{email}"
|
|
56
|
+
Wokku::Output.status "Connected to: #{instance_label(url)}"
|
|
57
|
+
return
|
|
58
|
+
when 202
|
|
59
|
+
# authorization_pending — keep polling
|
|
60
|
+
when 400
|
|
61
|
+
# slow_down — back off
|
|
62
|
+
interval += 5
|
|
63
|
+
when 403
|
|
64
|
+
puts
|
|
65
|
+
abort "Login denied."
|
|
66
|
+
when 410
|
|
67
|
+
puts
|
|
68
|
+
abort "Login code expired. Run `wokku auth:login` again."
|
|
69
|
+
else
|
|
70
|
+
puts
|
|
71
|
+
abort "Login failed: #{resp.code} #{body['error']}"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def post_json(base_url, path, payload)
|
|
77
|
+
resp = post_raw(base_url, path, payload)
|
|
78
|
+
data = JSON.parse(resp.body) rescue {}
|
|
79
|
+
unless resp.is_a?(Net::HTTPSuccess)
|
|
80
|
+
abort "Request failed: #{resp.code} #{data['error'] || resp.body}"
|
|
81
|
+
end
|
|
82
|
+
data
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def post_raw(base_url, path, payload)
|
|
86
|
+
uri = URI("#{base_url}#{path}")
|
|
87
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
88
|
+
http.use_ssl = uri.scheme == "https"
|
|
89
|
+
http.open_timeout = 10
|
|
90
|
+
http.read_timeout = 30
|
|
91
|
+
req = Net::HTTP::Post.new(uri)
|
|
92
|
+
req["Content-Type"] = "application/json"
|
|
93
|
+
req["Accept"] = "application/json"
|
|
94
|
+
req.body = payload.to_json
|
|
95
|
+
http.request(req)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def open_browser(url)
|
|
99
|
+
cmd =
|
|
100
|
+
case RUBY_PLATFORM
|
|
101
|
+
when /darwin/ then "open"
|
|
102
|
+
when /mswin|mingw|cygwin/ then "start"
|
|
103
|
+
else "xdg-open"
|
|
104
|
+
end
|
|
105
|
+
# Best-effort; ignore failures (CI, no GUI, etc.)
|
|
106
|
+
system(cmd, url, out: File::NULL, err: File::NULL) rescue nil
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def instance_label(url)
|
|
110
|
+
url.include?("wokku.cloud") ? "wokku.cloud (managed)" : "#{URI(url).host} (self-hosted)"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def sleep_for(seconds)
|
|
114
|
+
Kernel.sleep(seconds)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "socket"
|
|
5
|
+
require "openssl"
|
|
6
|
+
require "uri"
|
|
7
|
+
require "websocket/driver"
|
|
8
|
+
|
|
9
|
+
module Wokku
|
|
10
|
+
class CableClient
|
|
11
|
+
SUBSCRIBE_TIMEOUT = 10
|
|
12
|
+
|
|
13
|
+
def initialize(url:, token:)
|
|
14
|
+
@uri = URI(url)
|
|
15
|
+
@token = token
|
|
16
|
+
@on_message = ->(_) {}
|
|
17
|
+
@on_close = ->(_) {}
|
|
18
|
+
@subscribed = false
|
|
19
|
+
@identifier = nil
|
|
20
|
+
@socket = open_socket
|
|
21
|
+
@driver = WebSocket::Driver.client(self)
|
|
22
|
+
@driver.set_header("Authorization", "Bearer #{@token}") if @token && !@token.empty?
|
|
23
|
+
@driver.set_header("Origin", origin_header)
|
|
24
|
+
attach_driver_callbacks
|
|
25
|
+
@driver.start
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Origin matches the cable host so ActionCable's same-origin check passes.
|
|
29
|
+
# CSRF concerns don't apply: the CLI authenticates via Bearer token, not
|
|
30
|
+
# a cookie-borne session, so cross-site forgery is not a vector here.
|
|
31
|
+
def origin_header
|
|
32
|
+
scheme = @uri.scheme == "wss" ? "https" : "http"
|
|
33
|
+
port = (@uri.port && ![ 80, 443 ].include?(@uri.port)) ? ":#{@uri.port}" : ""
|
|
34
|
+
"#{scheme}://#{@uri.host}#{port}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# WebSocket::Driver duck-types these on its socket adapter:
|
|
38
|
+
def url
|
|
39
|
+
@uri.to_s
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def write(data)
|
|
43
|
+
@socket.write(data)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def on_message(&block)
|
|
47
|
+
@on_message = block
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def on_close(&block)
|
|
51
|
+
@on_close = block
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def subscribe(channel:, params: {})
|
|
55
|
+
@identifier = JSON.dump({ channel: channel, **params })
|
|
56
|
+
@driver.text(JSON.dump(command: "subscribe", identifier: @identifier))
|
|
57
|
+
deadline = Time.now + SUBSCRIBE_TIMEOUT
|
|
58
|
+
pump_until { @subscribed || Time.now > deadline }
|
|
59
|
+
raise "subscribe timed out" unless @subscribed
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def send_message(payload)
|
|
63
|
+
@driver.text(JSON.dump(command: "message", identifier: @identifier, data: JSON.dump(payload)))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def pump(timeout = 0.05)
|
|
67
|
+
ready = IO.select([@socket], nil, nil, timeout)
|
|
68
|
+
return unless ready
|
|
69
|
+
@driver.parse(@socket.read_nonblock(4096))
|
|
70
|
+
rescue IO::WaitReadable
|
|
71
|
+
# transient
|
|
72
|
+
rescue EOFError
|
|
73
|
+
@on_close.call(:eof)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def close
|
|
77
|
+
@driver.close rescue nil
|
|
78
|
+
@socket.close rescue nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def open_socket
|
|
84
|
+
tcp = TCPSocket.new(@uri.host, @uri.port || (@uri.scheme == "wss" ? 443 : 80))
|
|
85
|
+
return tcp if @uri.scheme == "ws"
|
|
86
|
+
|
|
87
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
|
88
|
+
ctx.set_params
|
|
89
|
+
ssl = OpenSSL::SSL::SSLSocket.new(tcp, ctx)
|
|
90
|
+
ssl.hostname = @uri.host
|
|
91
|
+
ssl.sync_close = true
|
|
92
|
+
ssl.connect
|
|
93
|
+
ssl
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def attach_driver_callbacks
|
|
97
|
+
@driver.on(:message) { |e| dispatch_frame(e.data) }
|
|
98
|
+
@driver.on(:close) { |e| @on_close.call(e) }
|
|
99
|
+
@driver.on(:error) { |e| @on_close.call(e) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def dispatch_frame(text)
|
|
103
|
+
frame = JSON.parse(text)
|
|
104
|
+
case frame["type"]
|
|
105
|
+
when "confirm_subscription"
|
|
106
|
+
@subscribed = true
|
|
107
|
+
when "ping", "welcome"
|
|
108
|
+
# ignore
|
|
109
|
+
else
|
|
110
|
+
msg = frame["message"]
|
|
111
|
+
@on_message.call(msg) if msg
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def pump_until
|
|
116
|
+
until yield
|
|
117
|
+
pump(0.05)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
data/lib/wokku/commands/auth.rb
CHANGED
|
@@ -1,35 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
register "auth:login", "Authenticate with Wokku" do
|
|
5
|
-
print "Wokku API URL [https://wokku.cloud/api/v1]: "
|
|
6
|
-
url = $stdin.gets.strip
|
|
7
|
-
url = "https://wokku.cloud/api/v1" if url.empty?
|
|
8
|
-
|
|
9
|
-
print "Email: "
|
|
10
|
-
email = $stdin.gets.strip
|
|
11
|
-
print "Password: "
|
|
12
|
-
password = ($stdin.respond_to?(:noecho) ? $stdin.noecho(&:gets) : $stdin.gets).strip
|
|
13
|
-
puts
|
|
14
|
-
|
|
15
|
-
uri = URI("#{url}/auth/login")
|
|
16
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
17
|
-
http.use_ssl = uri.scheme == "https"
|
|
18
|
-
req = Net::HTTP::Post.new(uri)
|
|
19
|
-
req["Content-Type"] = "application/json"
|
|
20
|
-
req.body = { email: email, password: password }.to_json
|
|
21
|
-
|
|
22
|
-
resp = http.request(req)
|
|
23
|
-
data = JSON.parse(resp.body) rescue {}
|
|
3
|
+
DEFAULT_API_URL = "https://wokku.cloud/api/v1"
|
|
24
4
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
abort "
|
|
5
|
+
# --- Auth ---
|
|
6
|
+
register "auth:login", "Authenticate with Wokku via browser device flow. Flag: --url URL (for self-hosted)" do
|
|
7
|
+
url = DEFAULT_API_URL
|
|
8
|
+
while (arg = ARGV.shift)
|
|
9
|
+
case arg
|
|
10
|
+
when "--url" then url = ARGV.shift or abort "--url requires a value"
|
|
11
|
+
else abort "Unknown argument: #{arg}"
|
|
12
|
+
end
|
|
32
13
|
end
|
|
14
|
+
|
|
15
|
+
Wokku::Auth.login_with_device_flow!(url)
|
|
33
16
|
end
|
|
34
17
|
|
|
35
18
|
register "auth:logout", "Log out" do
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
require "wokku/cable_client"
|
|
5
|
+
require "wokku/pty_session"
|
|
6
|
+
|
|
7
|
+
# --- wokku enter APP ---
|
|
8
|
+
register "enter", "Open an interactive shell in APP's running container (usage: wokku enter APP)" do
|
|
9
|
+
app_name = ARGV.shift or abort "Usage: wokku enter APP"
|
|
10
|
+
Wokku::Commands::Shell.run!(mode: "enter", target_kind: :app, target: app_name, argv: [], force_tty: true)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# --- wokku ps:exec APP -- CMD ARGS... ---
|
|
14
|
+
register "ps:exec", "Run CMD in a one-off container (like `heroku run`; usage: wokku ps:exec APP [-t|-T] -- CMD [ARGS...])" do
|
|
15
|
+
force = nil
|
|
16
|
+
app_name = nil
|
|
17
|
+
argv = []
|
|
18
|
+
while (arg = ARGV.shift)
|
|
19
|
+
case arg
|
|
20
|
+
when "-t", "--tty" then force = true
|
|
21
|
+
when "-T", "--no-tty" then force = false
|
|
22
|
+
when "--" then argv = ARGV.shift(ARGV.length); break
|
|
23
|
+
else app_name ||= arg
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
abort "Usage: wokku ps:exec APP [-t|-T] -- CMD [ARGS...]" unless app_name && !argv.empty?
|
|
27
|
+
Wokku::Commands::Shell.run!(mode: "exec", target_kind: :app, target: app_name, argv: argv, force_tty: force)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# --- wokku databases:connect DB ---
|
|
31
|
+
register "databases:connect", "Open an interactive client for DB (usage: wokku databases:connect DB)" do
|
|
32
|
+
db_name = ARGV.shift or abort "Usage: wokku databases:connect DB"
|
|
33
|
+
Wokku::Commands::Shell.run!(mode: "db_connect", target_kind: :database, target: db_name, argv: [], force_tty: true)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
module Wokku
|
|
37
|
+
module Commands
|
|
38
|
+
module Shell
|
|
39
|
+
module_function
|
|
40
|
+
|
|
41
|
+
def run!(mode:, target_kind:, target:, argv:, force_tty:)
|
|
42
|
+
resource_path = target_kind == :app ? "/apps/#{target}" : "/databases/#{target}"
|
|
43
|
+
resource = api(:get, resource_path)
|
|
44
|
+
server_id = resource["server_id"] or abort "could not resolve server for #{target}"
|
|
45
|
+
|
|
46
|
+
token = Wokku::Config.api_token or abort "not logged in — run `wokku auth:login`"
|
|
47
|
+
cable_url = derive_cable_url(Wokku::Config.api_url)
|
|
48
|
+
|
|
49
|
+
cable = Wokku::CableClient.new(url: cable_url, token: token)
|
|
50
|
+
cable.subscribe(channel: "TerminalChannel", params: { server_id: server_id })
|
|
51
|
+
|
|
52
|
+
tty = force_tty.nil? ? $stdout.tty? : force_tty
|
|
53
|
+
session = Wokku::PtySession.new(cable: cable, tty: tty)
|
|
54
|
+
session.start!(mode: mode, target: target, argv: argv)
|
|
55
|
+
session.run
|
|
56
|
+
cable.close
|
|
57
|
+
exit(session.exit_code || 0)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def derive_cable_url(api_url)
|
|
61
|
+
u = URI(api_url)
|
|
62
|
+
scheme = u.scheme == "https" ? "wss" : "ws"
|
|
63
|
+
port = (u.port && ![80, 443].include?(u.port)) ? ":#{u.port}" : ""
|
|
64
|
+
"#{scheme}://#{u.host}#{port}/cable"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "io/console"
|
|
4
|
+
require "base64"
|
|
5
|
+
|
|
6
|
+
module Wokku
|
|
7
|
+
class PtySession
|
|
8
|
+
attr_reader :exit_code
|
|
9
|
+
|
|
10
|
+
def initialize(cable:, tty:)
|
|
11
|
+
@cable = cable
|
|
12
|
+
@tty = tty
|
|
13
|
+
@exit_code = nil
|
|
14
|
+
@done = false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def start!(mode:, target:, argv: [])
|
|
18
|
+
rows, cols = current_winsize
|
|
19
|
+
@cable.send_message(type: "start", mode: mode, target: target, argv: argv, cols: cols, rows: rows)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Runs until @done is set by an "exit" or "error" message.
|
|
23
|
+
# Optional block (used in specs) is called once per pump tick and may return :stop.
|
|
24
|
+
def run
|
|
25
|
+
install_message_handler
|
|
26
|
+
install_signal_handlers if @tty
|
|
27
|
+
|
|
28
|
+
IO.console.raw! if @tty
|
|
29
|
+
begin
|
|
30
|
+
loop do
|
|
31
|
+
break if @done
|
|
32
|
+
forward_stdin if @tty
|
|
33
|
+
@cable.pump(0.05)
|
|
34
|
+
break if block_given? && yield == :stop
|
|
35
|
+
end
|
|
36
|
+
ensure
|
|
37
|
+
IO.console.cooked! if @tty
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def install_signal_handlers
|
|
44
|
+
Signal.trap("WINCH") do
|
|
45
|
+
rows, cols = current_winsize
|
|
46
|
+
@cable.send_message(type: "resize", cols: cols, rows: rows)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def install_message_handler
|
|
51
|
+
@cable.on_message do |msg|
|
|
52
|
+
case msg["type"]
|
|
53
|
+
when "stdout"
|
|
54
|
+
$stdout.write(Base64.strict_decode64(msg["data"].to_s))
|
|
55
|
+
$stdout.flush
|
|
56
|
+
when "exit"
|
|
57
|
+
@exit_code = msg["code"].to_i
|
|
58
|
+
@done = true
|
|
59
|
+
when "error"
|
|
60
|
+
warn(msg["message"].to_s)
|
|
61
|
+
@exit_code = 1
|
|
62
|
+
@done = true
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def forward_stdin
|
|
68
|
+
ready = IO.select([$stdin], nil, nil, 0)
|
|
69
|
+
return unless ready
|
|
70
|
+
data = $stdin.read_nonblock(4096)
|
|
71
|
+
@cable.send_message(type: "stdin", data: Base64.strict_encode64(data))
|
|
72
|
+
rescue IO::WaitReadable, EOFError, TypeError
|
|
73
|
+
# nothing to read (TypeError: StringIO in tests, or non-IO stdin)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def current_winsize
|
|
77
|
+
IO.console.winsize
|
|
78
|
+
rescue
|
|
79
|
+
[24, 80]
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
data/lib/wokku/version.rb
CHANGED
data/lib/wokku.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: wokku-cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Johannes Dwicahyo
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: websocket-driver
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.7'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.7'
|
|
12
26
|
description: Deploy and manage apps, databases, domains, and SSH keys on Wokku.cloud
|
|
13
27
|
or self-hosted Wokku servers.
|
|
14
28
|
email:
|
|
@@ -24,6 +38,8 @@ files:
|
|
|
24
38
|
- exe/wokku
|
|
25
39
|
- lib/wokku.rb
|
|
26
40
|
- lib/wokku/api_client.rb
|
|
41
|
+
- lib/wokku/auth.rb
|
|
42
|
+
- lib/wokku/cable_client.rb
|
|
27
43
|
- lib/wokku/commands/activity.rb
|
|
28
44
|
- lib/wokku/commands/addons.rb
|
|
29
45
|
- lib/wokku/commands/apps.rb
|
|
@@ -40,6 +56,7 @@ files:
|
|
|
40
56
|
- lib/wokku/commands/releases.rb
|
|
41
57
|
- lib/wokku/commands/run.rb
|
|
42
58
|
- lib/wokku/commands/servers.rb
|
|
59
|
+
- lib/wokku/commands/shell.rb
|
|
43
60
|
- lib/wokku/commands/ssh_keys.rb
|
|
44
61
|
- lib/wokku/commands/storage.rb
|
|
45
62
|
- lib/wokku/commands/templates.rb
|
|
@@ -47,6 +64,7 @@ files:
|
|
|
47
64
|
- lib/wokku/config.rb
|
|
48
65
|
- lib/wokku/helpers.rb
|
|
49
66
|
- lib/wokku/output.rb
|
|
67
|
+
- lib/wokku/pty_session.rb
|
|
50
68
|
- lib/wokku/registry.rb
|
|
51
69
|
- lib/wokku/version.rb
|
|
52
70
|
homepage: https://wokku.cloud
|