wokku-cli 0.1.0 → 0.4.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 +46 -0
- data/README.md +37 -10
- data/lib/wokku/auth.rb +117 -0
- data/lib/wokku/cable_client.rb +121 -0
- data/lib/wokku/commands/addons.rb +38 -3
- data/lib/wokku/commands/apps.rb +19 -5
- data/lib/wokku/commands/auth.rb +11 -28
- data/lib/wokku/commands/mcp.rb +75 -0
- data/lib/wokku/commands/shell.rb +68 -0
- data/lib/wokku/output.rb +9 -0
- data/lib/wokku/pty_session.rb +82 -0
- data/lib/wokku/version.rb +1 -1
- data/lib/wokku.rb +1 -0
- metadata +21 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 820e03562fff67b93f087227eb2da57e79e761c43ac5c108680699ce068a480e
|
|
4
|
+
data.tar.gz: '077218e9886faadb334a615e225a3f61802688f29f6e9ac53625ca4f505f632d'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9518d6c33f111278305093a0d55f925772fe3363f4abe95df3d32b83e105d03f3e0bd2001add8f696c717d5e89352926fec2c1220fe7cb74baebc523a8c6812d
|
|
7
|
+
data.tar.gz: 1326795840aae9837bb480a821e466ce8c64e90a0fc5ed2f01c8929b7adb16da27760aa7d3c459fc65cf60afd18ac5de006be0e790d639af8eb6e09296ae9dcd
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 — 2026-06-01
|
|
4
|
+
|
|
5
|
+
MCP commands — wire the wokku-plugin Claude Code MCP server with one command. Pairs with wokku-cloud canonical backlog 1.3.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `wokku mcp:install` — registers the Wokku MCP server in Claude Code using the active CLI token + API URL.
|
|
10
|
+
- `wokku mcp:switch` — re-pushes the current CLI token to the MCP config (after `auth:login` swapped accounts).
|
|
11
|
+
- `wokku mcp:logout` — removes the Wokku MCP server from Claude Code.
|
|
12
|
+
|
|
13
|
+
Shells out to `claude mcp add / remove` so we don't depend on Claude Code's internal config-file layout (it changes between versions). Aborts with a clear "install Claude Code first" error if the `claude` CLI isn't on PATH.
|
|
14
|
+
|
|
15
|
+
## 0.3.0 — 2026-05-31
|
|
16
|
+
|
|
17
|
+
Bundle v2 — boxes, shared addons (user-pick per box), and dedicated upgrades for PostgreSQL / MySQL / MongoDB / Redis. Pairs with wokku-cloud Phase 7 prep PRs #67–#73.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- `apps:create` gains `--box-size SIZE` (sleeping/small/medium/large/xlarge), `--shared pg,redis,...` (comma-separated shared engines to attach), `--dedicated-db postgres|mysql|mongodb`, `--dedicated-redis`. All optional — omitting them keeps the old free-tier default.
|
|
21
|
+
- `addons:shared:enable APP ENGINE` — attach a shared engine (Pg/Redis/Memcached/RabbitMQ/Meilisearch). Free plan limited to Pg+Redis.
|
|
22
|
+
- `addons:shared:disable APP ENGINE` — detach a shared engine + destroy its tenant data.
|
|
23
|
+
- `addons:dedicated:upgrade APP ENGINE` — upgrade to a dedicated container. Pg/Redis migrate from shared (data preserved). MySQL/MongoDB are fresh-create. Quota: 3 per plan; size follows the box size.
|
|
24
|
+
- `addons` listing now shows `kind: shared|dedicated` per row.
|
|
25
|
+
|
|
26
|
+
### Deprecated
|
|
27
|
+
- `addons:add` — kept for back-compat with wokku-cloud servers that haven't flipped `BUNDLE_V2_ENABLED=true` yet. When the server has bundle v2 on, this endpoint returns 410 Gone with guidance to use the new commands above.
|
|
28
|
+
|
|
29
|
+
### Requires
|
|
30
|
+
- wokku-cloud server-side support shipped 2026-05-31 (Phase 7 prep PRs #67–#73).
|
|
31
|
+
|
|
32
|
+
## 0.2.0 — 2026-05-06
|
|
33
|
+
|
|
34
|
+
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.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
- `wokku enter APP` — interactive shell in the app's running container.
|
|
38
|
+
- `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).
|
|
39
|
+
- `wokku databases:connect DB` — interactive client for the managed database (psql / redis-cli / mysql via dokku's `<plugin>:connect`).
|
|
40
|
+
|
|
41
|
+
### Internals
|
|
42
|
+
- New `Wokku::CableClient` — thin `websocket-driver` wrapper, Bearer-token handshake on `/cable`.
|
|
43
|
+
- New `Wokku::PtySession` — local raw-mode PTY orchestration with SIGWINCH resize and guaranteed `cooked!` restore.
|
|
44
|
+
- Runtime dependency: `websocket-driver ~> 0.7`.
|
|
45
|
+
|
|
46
|
+
### Requires
|
|
47
|
+
- wokku-cloud server-side support shipped 2026-05-06 (Bearer auth on `ApplicationCable::Connection`, new `TerminalChannel` wire protocol with modes).
|
|
48
|
+
|
|
3
49
|
## 0.1.0 — 2026-05-04
|
|
4
50
|
|
|
5
51
|
First public release on RubyGems.
|
data/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# wokku-cli
|
|
2
2
|
|
|
3
|
-
CLI for [Wokku](https://wokku.cloud) — deploy and manage apps, databases, domains, and SSH keys
|
|
3
|
+
CLI for [Wokku Cloud](https://wokku.cloud) — deploy and manage apps, databases, domains, and SSH keys.
|
|
4
|
+
|
|
5
|
+
> **Wokku Cloud only.** This CLI talks to the Wokku REST API, which ships only with the managed Wokku Cloud product. The open-source Wokku web UI ([github.com/johannesdwicahyo/wokku](https://github.com/johannesdwicahyo/wokku)) does not expose `/api/v1`, so the CLI cannot point at a self-hosted Wokku install. Self-host users should use the web UI directly, or `ssh dokku@your-host` for command-line operations.
|
|
4
6
|
|
|
5
7
|
## Install
|
|
6
8
|
|
|
@@ -26,22 +28,47 @@ wokku logs myapp -f
|
|
|
26
28
|
|
|
27
29
|
## Commands
|
|
28
30
|
|
|
29
|
-
Run `wokku --help` for the full list.
|
|
31
|
+
Run `wokku --help` for the full list. ~39 first-class commands today, grouped:
|
|
30
32
|
|
|
31
|
-
- **Apps:** `apps`, `apps:create`, `apps:destroy`, `apps:info`
|
|
32
|
-
- **Process:** `ps:start/stop/restart/scale/rebuild`, `redeploy`
|
|
33
|
+
- **Apps:** `apps`, `apps:create`, `apps:destroy`, `apps:info`, `apps:transfer`
|
|
34
|
+
- **Process:** `ps`, `ps:start/stop/restart/scale/rebuild`, `redeploy`
|
|
33
35
|
- **Config:** `config`, `config:set`, `config:get`, `config:unset`, `config:export`
|
|
34
|
-
- **Domains:** `domains`, `domains:add/remove/clear`, `certs:enable`
|
|
35
|
-
- **Databases:** `databases`, `databases:create/destroy/link/unlink/info`
|
|
36
|
+
- **Domains + SSL:** `domains`, `domains:add/remove/clear`, `certs:enable`, `certs:disable`
|
|
37
|
+
- **Databases:** `databases`, `databases:create/destroy/link/unlink/info`, `backups`, `backups:create/restore`
|
|
36
38
|
- **SSH keys:** `ssh-keys`, `ssh-keys:add/remove`
|
|
37
|
-
- **Servers:** `servers`, `servers:info`, `servers:default`
|
|
38
|
-
- **
|
|
39
|
-
- **
|
|
39
|
+
- **Servers:** `servers`, `servers:info`, `servers:default` (multi-Dokku-host installs)
|
|
40
|
+
- **Teams:** `teams`, `teams:members`, `teams:invite`, `teams:remove`
|
|
41
|
+
- **Logs:** `wokku logs APP --follow [--tail N]`
|
|
42
|
+
- **Run / passthrough:** `wokku run APP -- COMMAND` (one-off in a fresh container), `wokku do APP -- DOKKU_ARGS` (arbitrary `dokku` CLI passthrough)
|
|
43
|
+
|
|
44
|
+
### Parity with Dokku
|
|
45
|
+
|
|
46
|
+
`wokku do` is the bridge for the ~110 Dokku subcommands not yet first-classed here. Anything `dokku <something> APP` does, `wokku do APP -- <something>` does too — same exit code, same stdout.
|
|
47
|
+
|
|
48
|
+
If you find yourself running the same `wokku do` invocation repeatedly, open an issue — that's the queue for promoting commands to first-class status.
|
|
40
49
|
|
|
41
50
|
## Global flags
|
|
42
51
|
|
|
43
|
-
- `--json` — machine-readable JSON output (read commands)
|
|
52
|
+
- `--json` — machine-readable JSON output (read commands only)
|
|
44
53
|
- `--quiet` / `-q` — suppress success messages and hints
|
|
54
|
+
- `--server NAME` — target a specific Dokku host on multi-server installs (default: your account's primary)
|
|
55
|
+
|
|
56
|
+
## Configuration
|
|
57
|
+
|
|
58
|
+
Set once and forget:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
export WOKKU_API_TOKEN=... # from wokku.cloud/dashboard/profile
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`WOKKU_API_URL` defaults to `https://wokku.cloud/api/v1` and shouldn't need to change.
|
|
65
|
+
|
|
66
|
+
## What's New in v0.2.0 (May 2026)
|
|
67
|
+
|
|
68
|
+
- `apps:transfer` for moving an app between teams
|
|
69
|
+
- `--quiet` flag now also suppresses progress bars (not just success lines) — friendlier in scripts
|
|
70
|
+
- `wokku do` exit codes pass through correctly when wrapped in pipelines (was previously masked to 0)
|
|
71
|
+
- Multi-server defaults — `wokku servers:default` persists per-account, not per-shell
|
|
45
72
|
|
|
46
73
|
## License
|
|
47
74
|
|
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
|
|
@@ -6,18 +6,25 @@ register "addons", "List add-ons (usage: wokku addons APP)" do
|
|
|
6
6
|
data = api(:get, "/apps/#{id}/addons")
|
|
7
7
|
Wokku::Output.render(data) do |d|
|
|
8
8
|
if d.is_a?(Array)
|
|
9
|
-
table(d.map { |a|
|
|
9
|
+
table(d.map { |a|
|
|
10
|
+
{
|
|
11
|
+
"name" => a["name"],
|
|
12
|
+
"type" => a["service_type"],
|
|
13
|
+
"kind" => a["shared"] ? "shared" : "dedicated",
|
|
14
|
+
"status" => a["status"]
|
|
15
|
+
}
|
|
16
|
+
})
|
|
10
17
|
else
|
|
11
18
|
puts_json d
|
|
12
19
|
end
|
|
13
20
|
end
|
|
14
21
|
end
|
|
15
22
|
|
|
16
|
-
register "addons:add", "Add add-on (usage: wokku addons:add APP postgres)" do
|
|
23
|
+
register "addons:add", "[LEGACY pre-Bundle-v2] Add add-on (usage: wokku addons:add APP postgres). Returns 410 under Bundle v2 — use addons:shared:enable or addons:dedicated:upgrade instead." do
|
|
17
24
|
id = ARGV.shift || abort("Usage: wokku addons:add APP SERVICE_TYPE")
|
|
18
25
|
service_type = ARGV.shift || abort("Missing service type (postgres, redis, mysql, etc)")
|
|
19
26
|
name = nil
|
|
20
|
-
while arg = ARGV.shift
|
|
27
|
+
while (arg = ARGV.shift)
|
|
21
28
|
name = ARGV.shift if arg == "--name"
|
|
22
29
|
end
|
|
23
30
|
body = { service_type: service_type }
|
|
@@ -25,3 +32,31 @@ register "addons:add", "Add add-on (usage: wokku addons:add APP postgres)" do
|
|
|
25
32
|
data = api(:post, "/apps/#{id}/addons", body)
|
|
26
33
|
Wokku::Output.status "Added #{service_type}: #{data['name'] || data['id']}"
|
|
27
34
|
end
|
|
35
|
+
|
|
36
|
+
# --- Bundle v2 — shared engines ---
|
|
37
|
+
# User-pick at box creation OR via these commands later. Free plan limited
|
|
38
|
+
# to postgres + redis; other plans get all 5 (memcached, rabbitmq, meilisearch
|
|
39
|
+
# also available).
|
|
40
|
+
register "addons:shared:enable", "Enable a shared engine on an app (usage: wokku addons:shared:enable APP ENGINE). ENGINE: postgres|redis|memcached|rabbitmq|meilisearch" do
|
|
41
|
+
id = ARGV.shift || abort("Usage: wokku addons:shared:enable APP ENGINE")
|
|
42
|
+
engine = ARGV.shift || abort("Missing engine")
|
|
43
|
+
data = api(:post, "/apps/#{id}/addons/shared", { engine: engine })
|
|
44
|
+
Wokku::Output.status(data["message"] || "Enabled shared #{engine} on #{id}")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
register "addons:shared:disable", "Disable a shared engine on an app (usage: wokku addons:shared:disable APP ENGINE)" do
|
|
48
|
+
id = ARGV.shift || abort("Usage: wokku addons:shared:disable APP ENGINE")
|
|
49
|
+
engine = ARGV.shift || abort("Missing engine")
|
|
50
|
+
data = api(:delete, "/apps/#{id}/addons/shared/#{engine}")
|
|
51
|
+
Wokku::Output.status(data["message"] || "Disabled shared #{engine} on #{id}")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# --- Bundle v2 — dedicated upgrade ---
|
|
55
|
+
# Pg/Redis migrate from shared (existing data preserved). MySQL/MongoDB
|
|
56
|
+
# are fresh-create. Quota: 3 per plan, size follows the box size.
|
|
57
|
+
register "addons:dedicated:upgrade", "Upgrade a box to a dedicated DB or Redis (usage: wokku addons:dedicated:upgrade APP ENGINE). ENGINE: postgres|mysql|mongodb|redis" do
|
|
58
|
+
id = ARGV.shift || abort("Usage: wokku addons:dedicated:upgrade APP ENGINE")
|
|
59
|
+
engine = ARGV.shift || abort("Missing engine (postgres|mysql|mongodb|redis)")
|
|
60
|
+
data = api(:post, "/apps/#{id}/addons/dedicated", { engine: engine })
|
|
61
|
+
Wokku::Output.status(data["message"] || "Dedicated #{engine} upgrade queued for #{id}")
|
|
62
|
+
end
|
data/lib/wokku/commands/apps.rb
CHANGED
|
@@ -14,19 +14,33 @@ register "apps:info", "Show app details (usage: wokku apps:info APP)" do
|
|
|
14
14
|
Wokku::Output.render(data) { |d| puts_json d }
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
register "apps:create", "Create app (usage: wokku apps:create NAME [--server SERVER] [--branch BRANCH])" do
|
|
17
|
+
register "apps:create", "Create app (usage: wokku apps:create NAME [--server SERVER] [--branch BRANCH] [--box-size SIZE] [--shared pg,redis,...] [--dedicated-db postgres|mysql|mongodb] [--dedicated-redis])" do
|
|
18
18
|
name = ARGV.shift || abort("Usage: wokku apps:create NAME [--server SERVER]")
|
|
19
19
|
server = nil
|
|
20
20
|
branch = "main"
|
|
21
|
-
|
|
21
|
+
box_size = nil
|
|
22
|
+
shared = nil
|
|
23
|
+
dedicated_db = nil
|
|
24
|
+
dedicated_redis = false
|
|
25
|
+
while (arg = ARGV.shift)
|
|
22
26
|
case arg
|
|
23
|
-
when "--server"
|
|
24
|
-
when "--branch"
|
|
27
|
+
when "--server" then server = ARGV.shift
|
|
28
|
+
when "--branch" then branch = ARGV.shift
|
|
29
|
+
when "--box-size" then box_size = ARGV.shift
|
|
30
|
+
when "--shared" then shared = ARGV.shift # comma-separated engine names
|
|
31
|
+
when "--dedicated-db" then dedicated_db = ARGV.shift
|
|
32
|
+
when "--dedicated-redis" then dedicated_redis = true
|
|
25
33
|
end
|
|
26
34
|
end
|
|
27
35
|
server = resolve_server(explicit: server)
|
|
28
|
-
|
|
36
|
+
body = { name: name, server_id: server, deploy_branch: branch }
|
|
37
|
+
body[:box_size] = box_size if box_size
|
|
38
|
+
body[:enabled_shared_engines] = shared.split(",").map(&:strip) if shared
|
|
39
|
+
body[:dedicated_db_engine] = dedicated_db if dedicated_db
|
|
40
|
+
body[:add_dedicated_redis] = true if dedicated_redis
|
|
41
|
+
data = api(:post, "/apps", body)
|
|
29
42
|
Wokku::Output.status "Created app: #{data['name']} (id: #{data['id']})"
|
|
43
|
+
Array(data["warnings"]).each { |w| Wokku::Output.warn(w) }
|
|
30
44
|
end
|
|
31
45
|
|
|
32
46
|
register "apps:destroy", "Delete app (usage: wokku apps:destroy APP)" do
|
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,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Wokku MCP commands — install / switch / logout the Wokku MCP server
|
|
4
|
+
# in Claude Code so the user's CLI token gets passed to the plugin.
|
|
5
|
+
# Shells out to `claude mcp` (Claude Code CLI) so we don't need to know
|
|
6
|
+
# Claude Code's exact config-file layout — it changes between versions.
|
|
7
|
+
|
|
8
|
+
MCP_SERVER_NAME = "wokku"
|
|
9
|
+
|
|
10
|
+
def claude_cli_available?
|
|
11
|
+
system("command -v claude >/dev/null 2>&1")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def require_claude_cli!
|
|
15
|
+
return if claude_cli_available?
|
|
16
|
+
abort <<~MSG
|
|
17
|
+
The `claude` CLI is not installed.
|
|
18
|
+
Wokku MCP commands shell out to it to write your Claude Code config.
|
|
19
|
+
|
|
20
|
+
Install Claude Code (https://claude.com/claude-code) and retry.
|
|
21
|
+
MSG
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def require_logged_in!
|
|
25
|
+
return if Wokku::Config.api_token
|
|
26
|
+
abort "Not logged in. Run: wokku auth:login"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def install_mcp_entry!
|
|
30
|
+
url = Wokku::Config.api_url
|
|
31
|
+
token = Wokku::Config.api_token
|
|
32
|
+
|
|
33
|
+
# Idempotent — silently no-ops if the entry doesn't exist yet.
|
|
34
|
+
system("claude mcp remove #{Shellwords.escape(MCP_SERVER_NAME)} > /dev/null 2>&1")
|
|
35
|
+
|
|
36
|
+
ok = system(
|
|
37
|
+
"claude", "mcp", "add", MCP_SERVER_NAME,
|
|
38
|
+
"--env", "WOKKU_API_URL=#{url}",
|
|
39
|
+
"--env", "WOKKU_API_TOKEN=#{token}",
|
|
40
|
+
"--", "npx", "-y", "@johannesdwicahyo/wokku-plugin"
|
|
41
|
+
)
|
|
42
|
+
ok or abort "claude mcp add failed — check `claude mcp list` and retry."
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
register "mcp:install", "Add the Wokku MCP server to Claude Code with your CLI token" do
|
|
46
|
+
require_claude_cli!
|
|
47
|
+
require_logged_in!
|
|
48
|
+
require "shellwords"
|
|
49
|
+
|
|
50
|
+
install_mcp_entry!
|
|
51
|
+
Wokku::Output.status "MCP server '#{MCP_SERVER_NAME}' installed in Claude Code."
|
|
52
|
+
Wokku::Output.status "Restart Claude Code, then ask: \"Deploy this project to Wokku\""
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
register "mcp:switch", "Re-push the current CLI token to the Wokku MCP server (after auth:login)" do
|
|
56
|
+
require_claude_cli!
|
|
57
|
+
require_logged_in!
|
|
58
|
+
require "shellwords"
|
|
59
|
+
|
|
60
|
+
install_mcp_entry!
|
|
61
|
+
Wokku::Output.status "MCP server '#{MCP_SERVER_NAME}' now points at the current CLI session."
|
|
62
|
+
Wokku::Output.status "Restart Claude Code for the new token to take effect."
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
register "mcp:logout", "Remove the Wokku MCP server from Claude Code" do
|
|
66
|
+
require_claude_cli!
|
|
67
|
+
require "shellwords"
|
|
68
|
+
|
|
69
|
+
ok = system("claude", "mcp", "remove", MCP_SERVER_NAME)
|
|
70
|
+
if ok
|
|
71
|
+
Wokku::Output.status "MCP server '#{MCP_SERVER_NAME}' removed from Claude Code."
|
|
72
|
+
else
|
|
73
|
+
Wokku::Output.status "No MCP entry named '#{MCP_SERVER_NAME}' to remove."
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -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
|
data/lib/wokku/output.rb
CHANGED
|
@@ -39,5 +39,14 @@ module Wokku
|
|
|
39
39
|
return if Wokku.quiet? || Wokku.json?
|
|
40
40
|
puts message
|
|
41
41
|
end
|
|
42
|
+
|
|
43
|
+
# Non-fatal warning (e.g. partial-success cases like "app created
|
|
44
|
+
# but the dedicated DB upgrade failed to enqueue"). Stderr so it
|
|
45
|
+
# doesn't pollute pipeable stdout; visible even with --quiet but
|
|
46
|
+
# silenced under --json (the JSON response carries the warning).
|
|
47
|
+
def warn(message)
|
|
48
|
+
return if Wokku.json?
|
|
49
|
+
$stderr.puts "WARNING: #{message}"
|
|
50
|
+
end
|
|
42
51
|
end
|
|
43
52
|
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.4.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
|
|
@@ -36,10 +52,12 @@ files:
|
|
|
36
52
|
- lib/wokku/commands/do.rb
|
|
37
53
|
- lib/wokku/commands/domains.rb
|
|
38
54
|
- lib/wokku/commands/logs.rb
|
|
55
|
+
- lib/wokku/commands/mcp.rb
|
|
39
56
|
- lib/wokku/commands/ps.rb
|
|
40
57
|
- lib/wokku/commands/releases.rb
|
|
41
58
|
- lib/wokku/commands/run.rb
|
|
42
59
|
- lib/wokku/commands/servers.rb
|
|
60
|
+
- lib/wokku/commands/shell.rb
|
|
43
61
|
- lib/wokku/commands/ssh_keys.rb
|
|
44
62
|
- lib/wokku/commands/storage.rb
|
|
45
63
|
- lib/wokku/commands/templates.rb
|
|
@@ -47,6 +65,7 @@ files:
|
|
|
47
65
|
- lib/wokku/config.rb
|
|
48
66
|
- lib/wokku/helpers.rb
|
|
49
67
|
- lib/wokku/output.rb
|
|
68
|
+
- lib/wokku/pty_session.rb
|
|
50
69
|
- lib/wokku/registry.rb
|
|
51
70
|
- lib/wokku/version.rb
|
|
52
71
|
homepage: https://wokku.cloud
|