wokku-cli 0.1.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 +7 -0
- data/CHANGELOG.md +33 -0
- data/LICENSE +21 -0
- data/README.md +48 -0
- data/exe/wokku +4 -0
- data/lib/wokku/api_client.rb +121 -0
- data/lib/wokku/commands/activity.rb +13 -0
- data/lib/wokku/commands/addons.rb +27 -0
- data/lib/wokku/commands/apps.rb +39 -0
- data/lib/wokku/commands/auth.rb +51 -0
- data/lib/wokku/commands/buildpacks.rb +50 -0
- data/lib/wokku/commands/certs.rb +10 -0
- data/lib/wokku/commands/checks.rb +25 -0
- data/lib/wokku/commands/config.rb +62 -0
- data/lib/wokku/commands/databases.rb +67 -0
- data/lib/wokku/commands/do.rb +32 -0
- data/lib/wokku/commands/domains.rb +39 -0
- data/lib/wokku/commands/logs.rb +32 -0
- data/lib/wokku/commands/ps.rb +46 -0
- data/lib/wokku/commands/releases.rb +21 -0
- data/lib/wokku/commands/run.rb +13 -0
- data/lib/wokku/commands/servers.rb +34 -0
- data/lib/wokku/commands/ssh_keys.rb +57 -0
- data/lib/wokku/commands/storage.rb +31 -0
- data/lib/wokku/commands/templates.rb +31 -0
- data/lib/wokku/commands/version.rb +5 -0
- data/lib/wokku/config.rb +41 -0
- data/lib/wokku/helpers.rb +36 -0
- data/lib/wokku/output.rb +43 -0
- data/lib/wokku/registry.rb +13 -0
- data/lib/wokku/version.rb +4 -0
- data/lib/wokku.rb +95 -0
- metadata +78 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3d6db99db4bf59f5b55f619cbd6abb12fc6b3f711fba103b327e6d0b01479114
|
|
4
|
+
data.tar.gz: 0b10a6ea71101885f3929f7f6055705b6a0d98ebcb2a21147070911c0522b24a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8b3e284c92712469309c2a3aab4fffa60e26b89727f8bf948e1bfaba87e70b572eca298ec69ee1449d6112656d478294c825f1d826b0a7f9d2f7a02b57820085
|
|
7
|
+
data.tar.gz: e3f998fd1ec992942da0319c4838cf664dbbda1ef49cb9a777a35da0acc3d1a29ec0486df464f5b45bd3d9cc3eb431098e23b7bb047462d1fc6a27d90f26772d
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-05-04
|
|
4
|
+
|
|
5
|
+
First public release on RubyGems.
|
|
6
|
+
|
|
7
|
+
The wokku CLI was previously distributed as a tarball from `wokku.cloud/cli/wokku.tar.gz`. This is a fresh start under the `0.x` line — pre-1.0 means the surface may evolve as we gather feedback. Reach 1.0.0 once the API stabilizes.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
- Apps: `apps`, `apps:create`, `apps:destroy`, `apps:info`
|
|
11
|
+
- Process: `ps:start/stop/restart/scale/rebuild`, `redeploy`
|
|
12
|
+
- Config: `config`, `config:set/get/unset`, `config:export` (.env-safe quoting)
|
|
13
|
+
- Domains: `domains`, `domains:add/remove/clear`, `certs:enable`
|
|
14
|
+
- Buildpacks: `buildpacks`, `buildpacks:add/remove/clear/set`
|
|
15
|
+
- Health checks: `checks`, `checks:set`
|
|
16
|
+
- Storage: `storage`, `storage:mount/unmount`
|
|
17
|
+
- Releases: `releases`, `rollback`
|
|
18
|
+
- Add-ons: `addons`, `addons:add`
|
|
19
|
+
- Templates: `templates`, `deploy`
|
|
20
|
+
- Activity: `activity`
|
|
21
|
+
- Logs: `logs` with `--follow` / `-f` streaming
|
|
22
|
+
- SSH keys: `ssh-keys`, `ssh-keys:add/remove`
|
|
23
|
+
- Servers: `servers`, `servers:info`, `servers:default`
|
|
24
|
+
- Databases: `databases`, `databases:create/destroy/info/link/unlink`
|
|
25
|
+
- One-off: `wokku run APP -- COMMAND` (in-container)
|
|
26
|
+
- Passthrough: `wokku do APP -- DOKKU_ARGS` (raw dokku command, audit-logged)
|
|
27
|
+
- Authentication: `auth:login`, `auth:logout`, `auth:whoami`
|
|
28
|
+
- Global flags: `--json` (machine-readable), `--quiet` / `-q`
|
|
29
|
+
|
|
30
|
+
### Distribution
|
|
31
|
+
- Install: `gem install wokku-cli` or `brew install johannesdwicahyo/tap/wokku`
|
|
32
|
+
- Source: <https://github.com/johannesdwicahyo/wokku-cli>
|
|
33
|
+
- Old `curl install.sh` flow continues to work (delegates to `gem install wokku-cli`).
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Johannes Dwicahyo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# wokku-cli
|
|
2
|
+
|
|
3
|
+
CLI for [Wokku](https://wokku.cloud) — deploy and manage apps, databases, domains, and SSH keys on Wokku.cloud or self-hosted Wokku servers.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
gem install wokku-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or via Homebrew:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
brew install johannesdwicahyo/tap/wokku
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
wokku auth:login
|
|
21
|
+
wokku apps
|
|
22
|
+
wokku apps:create myapp
|
|
23
|
+
wokku ps:restart myapp
|
|
24
|
+
wokku logs myapp -f
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Commands
|
|
28
|
+
|
|
29
|
+
Run `wokku --help` for the full list. Highlights:
|
|
30
|
+
|
|
31
|
+
- **Apps:** `apps`, `apps:create`, `apps:destroy`, `apps:info`
|
|
32
|
+
- **Process:** `ps:start/stop/restart/scale/rebuild`, `redeploy`
|
|
33
|
+
- **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
|
+
- **SSH keys:** `ssh-keys`, `ssh-keys:add/remove`
|
|
37
|
+
- **Servers:** `servers`, `servers:info`, `servers:default`
|
|
38
|
+
- **Logs:** `wokku logs APP --follow`
|
|
39
|
+
- **Run / passthrough:** `wokku run APP -- COMMAND`, `wokku do APP -- DOKKU_ARGS`
|
|
40
|
+
|
|
41
|
+
## Global flags
|
|
42
|
+
|
|
43
|
+
- `--json` — machine-readable JSON output (read commands)
|
|
44
|
+
- `--quiet` / `-q` — suppress success messages and hints
|
|
45
|
+
|
|
46
|
+
## License
|
|
47
|
+
|
|
48
|
+
MIT — see [LICENSE](LICENSE).
|
data/exe/wokku
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
require_relative "config"
|
|
7
|
+
|
|
8
|
+
module Wokku
|
|
9
|
+
class ApiClient
|
|
10
|
+
class Error < StandardError; end
|
|
11
|
+
class NotAuthenticated < Error; end
|
|
12
|
+
class Timeout < Error; end
|
|
13
|
+
class Unreachable < Error; end
|
|
14
|
+
|
|
15
|
+
OPEN_TIMEOUT = 10
|
|
16
|
+
READ_TIMEOUT = 30
|
|
17
|
+
|
|
18
|
+
def initialize(url: nil, token: nil, user_agent: "WokkuCLI")
|
|
19
|
+
@url = url
|
|
20
|
+
@token = token
|
|
21
|
+
@user_agent = user_agent
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def get(path) = request(:get, path)
|
|
25
|
+
def post(path, body = nil) = request(:post, path, body)
|
|
26
|
+
def put(path, body = nil) = request(:put, path, body)
|
|
27
|
+
def patch(path, body = nil) = request(:patch, path, body)
|
|
28
|
+
def delete(path, body = nil) = request(:delete, path, body)
|
|
29
|
+
|
|
30
|
+
def request(method, path, body = nil)
|
|
31
|
+
@last_path = path
|
|
32
|
+
raise NotAuthenticated, "Not logged in. Run: wokku auth:login" unless token
|
|
33
|
+
|
|
34
|
+
uri = URI("#{url}#{path}")
|
|
35
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
36
|
+
http.use_ssl = uri.scheme == "https"
|
|
37
|
+
http.open_timeout = OPEN_TIMEOUT
|
|
38
|
+
http.read_timeout = READ_TIMEOUT
|
|
39
|
+
|
|
40
|
+
req = build_request(method, uri)
|
|
41
|
+
req["Authorization"] = "Bearer #{token}"
|
|
42
|
+
req["Content-Type"] = "application/json"
|
|
43
|
+
req["User-Agent"] = @user_agent
|
|
44
|
+
req.body = body.to_json if body
|
|
45
|
+
|
|
46
|
+
resp = perform(http, req, uri)
|
|
47
|
+
parse_response(resp)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def stream(method, path, &block)
|
|
51
|
+
@last_path = path
|
|
52
|
+
raise NotAuthenticated, "Not logged in. Run: wokku auth:login" unless token
|
|
53
|
+
|
|
54
|
+
uri = URI("#{url}#{path}")
|
|
55
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
56
|
+
http.use_ssl = uri.scheme == "https"
|
|
57
|
+
# Local Net::HTTP instance — disabling read_timeout doesn't leak to other calls.
|
|
58
|
+
http.read_timeout = nil
|
|
59
|
+
|
|
60
|
+
req = build_request(method, uri)
|
|
61
|
+
req["Authorization"] = "Bearer #{token}"
|
|
62
|
+
req["Accept"] = "text/plain"
|
|
63
|
+
req["User-Agent"] = @user_agent
|
|
64
|
+
|
|
65
|
+
http.request(req) do |resp|
|
|
66
|
+
raise Error, "Stream failed: HTTP #{resp.code}" unless resp.is_a?(Net::HTTPSuccess)
|
|
67
|
+
resp.read_body { |chunk| block.call(chunk) }
|
|
68
|
+
end
|
|
69
|
+
rescue Interrupt
|
|
70
|
+
# Ctrl-C — exit gracefully
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def url
|
|
76
|
+
@url || Wokku::Config.api_url
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def token
|
|
80
|
+
@token || Wokku::Config.api_token
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def build_request(method, uri)
|
|
84
|
+
case method
|
|
85
|
+
when :get then Net::HTTP::Get.new(uri)
|
|
86
|
+
when :post then Net::HTTP::Post.new(uri)
|
|
87
|
+
when :put then Net::HTTP::Put.new(uri)
|
|
88
|
+
when :patch then Net::HTTP::Patch.new(uri)
|
|
89
|
+
when :delete then Net::HTTP::Delete.new(uri)
|
|
90
|
+
else raise ArgumentError, "Unsupported method: #{method}"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def perform(http, req, uri)
|
|
95
|
+
http.request(req)
|
|
96
|
+
rescue Net::ReadTimeout, Net::OpenTimeout
|
|
97
|
+
raise Timeout, "Request timed out — the server didn't respond in #{READ_TIMEOUT}s."
|
|
98
|
+
rescue SocketError => e
|
|
99
|
+
raise Unreachable, "Cannot reach #{uri.host}: #{e.message.lines.first&.strip}."
|
|
100
|
+
rescue Errno::ECONNREFUSED
|
|
101
|
+
raise Unreachable, "Connection refused by #{uri.host}:#{uri.port}."
|
|
102
|
+
rescue OpenSSL::SSL::SSLError => e
|
|
103
|
+
raise Unreachable, "TLS error talking to #{uri.host}: #{e.message.lines.first&.strip}."
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def parse_response(resp)
|
|
107
|
+
data = JSON.parse(resp.body) rescue resp.body
|
|
108
|
+
return data if resp.is_a?(Net::HTTPSuccess)
|
|
109
|
+
|
|
110
|
+
raise Error, friendly_error(resp, data)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def friendly_error(resp, data)
|
|
114
|
+
if resp.code == "404" && (req_path = @last_path) && (m = req_path.match(%r{\A/apps/([^/?]+)}))
|
|
115
|
+
return "No app named '#{m[1]}'"
|
|
116
|
+
end
|
|
117
|
+
msg = data.is_a?(Hash) ? (data["error"] || data["errors"]&.join(", ") || resp.code) : resp.code
|
|
118
|
+
"Error: #{msg}"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- Activity ---
|
|
4
|
+
register "activity", "Show recent activity" do
|
|
5
|
+
data = api(:get, "/activities?limit=20")
|
|
6
|
+
Wokku::Output.render(data) do |d|
|
|
7
|
+
if d.is_a?(Array)
|
|
8
|
+
d.each { |a| puts "#{a['created_at'].to_s[0..18]} #{a['action']} #{a['target_name']}" }
|
|
9
|
+
else
|
|
10
|
+
puts_json d
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- Add-ons ---
|
|
4
|
+
register "addons", "List add-ons (usage: wokku addons APP)" do
|
|
5
|
+
id = ARGV.shift || abort("Usage: wokku addons APP")
|
|
6
|
+
data = api(:get, "/apps/#{id}/addons")
|
|
7
|
+
Wokku::Output.render(data) do |d|
|
|
8
|
+
if d.is_a?(Array)
|
|
9
|
+
table(d.map { |a| { "name" => a["name"], "type" => a["service_type"], "status" => a["status"] } })
|
|
10
|
+
else
|
|
11
|
+
puts_json d
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
register "addons:add", "Add add-on (usage: wokku addons:add APP postgres)" do
|
|
17
|
+
id = ARGV.shift || abort("Usage: wokku addons:add APP SERVICE_TYPE")
|
|
18
|
+
service_type = ARGV.shift || abort("Missing service type (postgres, redis, mysql, etc)")
|
|
19
|
+
name = nil
|
|
20
|
+
while arg = ARGV.shift
|
|
21
|
+
name = ARGV.shift if arg == "--name"
|
|
22
|
+
end
|
|
23
|
+
body = { service_type: service_type }
|
|
24
|
+
body[:name] = name if name
|
|
25
|
+
data = api(:post, "/apps/#{id}/addons", body)
|
|
26
|
+
Wokku::Output.status "Added #{service_type}: #{data['name'] || data['id']}"
|
|
27
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- Apps ---
|
|
4
|
+
register "apps", "List all apps" do
|
|
5
|
+
apps = api(:get, "/apps")
|
|
6
|
+
Wokku::Output.render(apps) do |list|
|
|
7
|
+
table(list.map { |a| { "name" => a["name"], "status" => a["status"], "server" => a["server_id"] } })
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
register "apps:info", "Show app details (usage: wokku apps:info APP)" do
|
|
12
|
+
id = ARGV.shift || abort("Usage: wokku apps:info APP")
|
|
13
|
+
data = api(:get, "/apps/#{id}")
|
|
14
|
+
Wokku::Output.render(data) { |d| puts_json d }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
register "apps:create", "Create app (usage: wokku apps:create NAME [--server SERVER] [--branch BRANCH])" do
|
|
18
|
+
name = ARGV.shift || abort("Usage: wokku apps:create NAME [--server SERVER]")
|
|
19
|
+
server = nil
|
|
20
|
+
branch = "main"
|
|
21
|
+
while arg = ARGV.shift
|
|
22
|
+
case arg
|
|
23
|
+
when "--server" then server = ARGV.shift
|
|
24
|
+
when "--branch" then branch = ARGV.shift
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
server = resolve_server(explicit: server)
|
|
28
|
+
data = api(:post, "/apps", { name: name, server_id: server, deploy_branch: branch })
|
|
29
|
+
Wokku::Output.status "Created app: #{data['name']} (id: #{data['id']})"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
register "apps:destroy", "Delete app (usage: wokku apps:destroy APP)" do
|
|
33
|
+
id = ARGV.shift || abort("Usage: wokku apps:destroy APP")
|
|
34
|
+
print "Are you sure you want to delete this app? (y/N): "
|
|
35
|
+
confirm = $stdin.gets.strip
|
|
36
|
+
abort "Cancelled." unless confirm.downcase == "y"
|
|
37
|
+
api(:delete, "/apps/#{id}")
|
|
38
|
+
Wokku::Output.status "App deleted."
|
|
39
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- Auth ---
|
|
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 {}
|
|
24
|
+
|
|
25
|
+
if resp.is_a?(Net::HTTPSuccess) && data["token"]
|
|
26
|
+
save_config({ "api_url" => url, "token" => data["token"], "email" => email })
|
|
27
|
+
instance = url.include?("wokku.cloud") ? "wokku.cloud (managed)" : "#{URI(url).host} (self-hosted)"
|
|
28
|
+
Wokku::Output.status "Logged in as #{email}"
|
|
29
|
+
Wokku::Output.status "Connected to: #{instance}"
|
|
30
|
+
else
|
|
31
|
+
abort "Login failed: #{data['error'] || resp.code}"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
register "auth:logout", "Log out" do
|
|
36
|
+
path = Wokku::Config.file
|
|
37
|
+
if File.exist?(path)
|
|
38
|
+
File.delete(path)
|
|
39
|
+
Wokku::Output.status "Logged out."
|
|
40
|
+
else
|
|
41
|
+
Wokku::Output.status "Not logged in."
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
register "auth:whoami", "Show current user" do
|
|
46
|
+
data = api(:get, "/auth/whoami")
|
|
47
|
+
Wokku::Output.render(data) do |d|
|
|
48
|
+
puts "Email: #{d['email']}"
|
|
49
|
+
puts "Role: #{d['role']}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- Buildpacks ---
|
|
4
|
+
register "buildpacks", "List buildpacks (usage: wokku buildpacks APP)" do
|
|
5
|
+
id = ARGV.shift || abort("Usage: wokku buildpacks APP")
|
|
6
|
+
data = api(:get, "/apps/#{id}/buildpacks")
|
|
7
|
+
Wokku::Output.render(data) do |d|
|
|
8
|
+
list = d.is_a?(Hash) ? Array(d["buildpacks"]) : Array(d)
|
|
9
|
+
if list.empty?
|
|
10
|
+
puts "(no buildpacks configured — auto-detected from source)"
|
|
11
|
+
else
|
|
12
|
+
list.each_with_index { |url, i| puts "#{i + 1}. #{url}" }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
register "buildpacks:add", "Append a buildpack (usage: wokku buildpacks:add APP URL [--index N])" do
|
|
18
|
+
id = ARGV.shift || abort("Usage: wokku buildpacks:add APP URL [--index N]")
|
|
19
|
+
url = ARGV.shift || abort("Missing buildpack URL")
|
|
20
|
+
index = nil
|
|
21
|
+
while arg = ARGV.shift
|
|
22
|
+
index = ARGV.shift if arg == "--index"
|
|
23
|
+
end
|
|
24
|
+
body = { url: url }
|
|
25
|
+
body[:index] = index if index
|
|
26
|
+
api(:post, "/apps/#{id}/buildpacks", body)
|
|
27
|
+
Wokku::Output.status "Added buildpack: #{url}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
register "buildpacks:remove", "Remove a buildpack (usage: wokku buildpacks:remove APP URL)" do
|
|
31
|
+
id = ARGV.shift || abort("Usage: wokku buildpacks:remove APP URL")
|
|
32
|
+
url = ARGV.shift || abort("Missing buildpack URL")
|
|
33
|
+
api(:delete, "/apps/#{id}/buildpacks", { url: url })
|
|
34
|
+
Wokku::Output.status "Removed buildpack: #{url}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
register "buildpacks:clear", "Remove every buildpack (usage: wokku buildpacks:clear APP)" do
|
|
38
|
+
id = ARGV.shift || abort("Usage: wokku buildpacks:clear APP")
|
|
39
|
+
api(:delete, "/apps/#{id}/buildpacks")
|
|
40
|
+
Wokku::Output.status "All buildpacks removed."
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
register "buildpacks:set", "Replace the buildpack stack in order (usage: wokku buildpacks:set APP URL [URL...])" do
|
|
44
|
+
id = ARGV.shift || abort("Usage: wokku buildpacks:set APP URL [URL...]")
|
|
45
|
+
urls = ARGV.dup
|
|
46
|
+
ARGV.clear
|
|
47
|
+
abort "No buildpack URLs given." if urls.empty?
|
|
48
|
+
api(:put, "/apps/#{id}/buildpacks", { urls: urls })
|
|
49
|
+
Wokku::Output.status "Buildpacks set: #{urls.join(', ')}"
|
|
50
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- TLS / Certs ---
|
|
4
|
+
register "certs:enable", "Enable Let's Encrypt SSL on a domain (usage: wokku certs:enable APP DOMAIN)" do
|
|
5
|
+
id = ARGV.shift || abort("Usage: wokku certs:enable APP DOMAIN")
|
|
6
|
+
domain = ARGV.shift || abort("Missing domain name")
|
|
7
|
+
row = find_domain(id, domain) || abort("No domain '#{domain}' on app #{id} — add it first with wokku domains:add")
|
|
8
|
+
api(:post, "/apps/#{id}/domains/#{row['id']}/ssl")
|
|
9
|
+
Wokku::Output.status "SSL enabled for #{domain}"
|
|
10
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- Health checks ---
|
|
4
|
+
register "checks", "Show Dokku health check config (usage: wokku checks APP)" do
|
|
5
|
+
id = ARGV.shift || abort("Usage: wokku checks APP")
|
|
6
|
+
data = api(:get, "/apps/#{id}/checks")
|
|
7
|
+
Wokku::Output.render(data) { |d| puts_json d }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
register "checks:set", "Update health check settings (usage: wokku checks:set APP [--enabled BOOL] [--wait N] [--timeout N] [--attempts N] [--path PATH])" do
|
|
11
|
+
id = ARGV.shift || abort("Usage: wokku checks:set APP [flags]")
|
|
12
|
+
body = {}
|
|
13
|
+
while arg = ARGV.shift
|
|
14
|
+
case arg
|
|
15
|
+
when "--enabled" then body[:enabled] = ARGV.shift
|
|
16
|
+
when "--wait" then body[:wait] = ARGV.shift
|
|
17
|
+
when "--timeout" then body[:timeout] = ARGV.shift
|
|
18
|
+
when "--attempts" then body[:attempts] = ARGV.shift
|
|
19
|
+
when "--path" then body[:path] = ARGV.shift
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
abort "No flags given. Try --enabled true, --path /healthz, --wait 5, etc." if body.empty?
|
|
23
|
+
api(:put, "/apps/#{id}/checks", body)
|
|
24
|
+
Wokku::Output.status "Health check settings updated."
|
|
25
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- Config ---
|
|
4
|
+
register "config", "Show config vars (usage: wokku config APP)" do
|
|
5
|
+
id = ARGV.shift || abort("Usage: wokku config APP")
|
|
6
|
+
data = api(:get, "/apps/#{id}/config")
|
|
7
|
+
Wokku::Output.render(data) do |d|
|
|
8
|
+
if d.is_a?(Hash)
|
|
9
|
+
d.each { |k, v| puts "#{k}=#{v}" }
|
|
10
|
+
else
|
|
11
|
+
puts_json d
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
register "config:set", "Set config vars (usage: wokku config:set APP KEY=VAL ...)" do
|
|
17
|
+
id = ARGV.shift || abort("Usage: wokku config:set APP KEY=VALUE ...")
|
|
18
|
+
vars = {}
|
|
19
|
+
ARGV.each do |pair|
|
|
20
|
+
key, val = pair.split("=", 2)
|
|
21
|
+
vars[key] = val if key && val
|
|
22
|
+
end
|
|
23
|
+
ARGV.clear
|
|
24
|
+
abort "No KEY=VALUE pairs given." if vars.empty?
|
|
25
|
+
# Server expects { vars: { K: V } }, not the bare hash. Sending the bare
|
|
26
|
+
# hash worked previously because Rails was permissive; the controller
|
|
27
|
+
# actually reads params[:vars].
|
|
28
|
+
api(:put, "/apps/#{id}/config", { vars: vars })
|
|
29
|
+
Wokku::Output.status "Config updated: #{vars.keys.join(', ')}"
|
|
30
|
+
Wokku::Output.status "Restart the app to pick up changes: wokku ps:restart #{id}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
register "config:get", "Show one config var (usage: wokku config:get APP KEY)" do
|
|
34
|
+
id = ARGV.shift || abort("Usage: wokku config:get APP KEY")
|
|
35
|
+
key = ARGV.shift || abort("Missing KEY")
|
|
36
|
+
data = api(:get, "/apps/#{id}/config")
|
|
37
|
+
vars = data.is_a?(Hash) ? (data["config"] || data) : {}
|
|
38
|
+
abort "Key not set: #{key}" unless vars.key?(key)
|
|
39
|
+
Wokku::Output.render({ "key" => key, "value" => vars[key] }) { |_| puts vars[key] }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
register "config:export", "Export config as .env (usage: wokku config:export APP)" do
|
|
43
|
+
id = ARGV.shift || abort("Usage: wokku config:export APP")
|
|
44
|
+
data = api(:get, "/apps/#{id}/config")
|
|
45
|
+
vars = data.is_a?(Hash) ? (data["config"] || data) : {}
|
|
46
|
+
Wokku::Output.render(vars) do |d|
|
|
47
|
+
d.each do |k, v|
|
|
48
|
+
escaped = v.to_s.gsub('\\') { '\\\\' }.gsub('"') { '\\"' }
|
|
49
|
+
puts %Q(#{k}="#{escaped}")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
register "config:unset", "Remove config vars (usage: wokku config:unset APP KEY [KEY...])" do
|
|
55
|
+
id = ARGV.shift || abort("Usage: wokku config:unset APP KEY [KEY...]")
|
|
56
|
+
keys = ARGV.dup
|
|
57
|
+
ARGV.clear
|
|
58
|
+
abort "No KEYs given." if keys.empty?
|
|
59
|
+
api(:delete, "/apps/#{id}/config", { keys: keys })
|
|
60
|
+
Wokku::Output.status "Removed: #{keys.join(', ')}"
|
|
61
|
+
Wokku::Output.status "Restart the app to pick up changes: wokku ps:restart #{id}"
|
|
62
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- Databases ---
|
|
4
|
+
register "databases", "List your databases" do
|
|
5
|
+
data = api(:get, "/databases")
|
|
6
|
+
Wokku::Output.render(data) do |list|
|
|
7
|
+
table(Array(list).map { |db| {
|
|
8
|
+
"name" => db["name"],
|
|
9
|
+
"type" => db["service_type"],
|
|
10
|
+
"status" => db["status"],
|
|
11
|
+
"server" => db["server_id"]
|
|
12
|
+
} })
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
register "databases:info", "Show database details (usage: wokku databases:info NAME)" do
|
|
17
|
+
name = ARGV.shift || abort("Usage: wokku databases:info NAME")
|
|
18
|
+
data = api(:get, "/databases/#{name}")
|
|
19
|
+
Wokku::Output.render(data) { |d| puts_json d }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
register "databases:create", "Create a database (usage: wokku databases:create NAME --type TYPE [--server SERVER])" do
|
|
23
|
+
name = ARGV.shift || abort("Usage: wokku databases:create NAME --type TYPE [--server SERVER]")
|
|
24
|
+
type = nil
|
|
25
|
+
server = nil
|
|
26
|
+
while arg = ARGV.shift
|
|
27
|
+
case arg
|
|
28
|
+
when "--type" then type = ARGV.shift
|
|
29
|
+
when "--server" then server = ARGV.shift
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
abort "Missing --type (postgres, mysql, redis, mongo)" unless type
|
|
33
|
+
server = resolve_server(explicit: server)
|
|
34
|
+
data = api(:post, "/databases", { name: name, service_type: type, server_id: server })
|
|
35
|
+
Wokku::Output.status "Created #{data['service_type']} database: #{data['name']}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
register "databases:destroy", "Destroy a database (usage: wokku databases:destroy NAME)" do
|
|
39
|
+
name = ARGV.shift || abort("Usage: wokku databases:destroy NAME")
|
|
40
|
+
print "Are you sure you want to destroy database '#{name}'? (y/N): "
|
|
41
|
+
confirm = $stdin.gets.to_s.strip
|
|
42
|
+
abort "Cancelled." unless confirm.downcase == "y"
|
|
43
|
+
api(:delete, "/databases/#{name}")
|
|
44
|
+
Wokku::Output.status "Database '#{name}' destroyed."
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
register "databases:link", "Link a database to an app (usage: wokku databases:link DB APP [--alias ALIAS])" do
|
|
48
|
+
db_name = ARGV.shift || abort("Usage: wokku databases:link DB APP [--alias ALIAS]")
|
|
49
|
+
app = ARGV.shift || abort("Missing APP")
|
|
50
|
+
alias_name = nil
|
|
51
|
+
while arg = ARGV.shift
|
|
52
|
+
case arg
|
|
53
|
+
when "--alias" then alias_name = ARGV.shift
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
body = { app_id: app }
|
|
57
|
+
body[:alias_name] = alias_name if alias_name
|
|
58
|
+
api(:post, "/databases/#{db_name}/link", body)
|
|
59
|
+
Wokku::Output.status "Linked '#{db_name}' to '#{app}'#{alias_name ? " as #{alias_name}" : ""}."
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
register "databases:unlink", "Unlink a database from an app (usage: wokku databases:unlink DB APP)" do
|
|
63
|
+
db_name = ARGV.shift || abort("Usage: wokku databases:unlink DB APP")
|
|
64
|
+
app = ARGV.shift || abort("Missing APP")
|
|
65
|
+
api(:post, "/databases/#{db_name}/unlink", { app_id: app })
|
|
66
|
+
Wokku::Output.status "Unlinked '#{db_name}' from '#{app}'."
|
|
67
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- Raw dokku passthrough ---
|
|
4
|
+
register "do", "Run any dokku command (usage: wokku do APP -- DOKKU_ARGS | wokku do --server N -- DOKKU_ARGS)" do
|
|
5
|
+
app = nil
|
|
6
|
+
server = nil
|
|
7
|
+
force = false
|
|
8
|
+
while ARGV.any? && ARGV.first != "--"
|
|
9
|
+
arg = ARGV.shift
|
|
10
|
+
case arg
|
|
11
|
+
when "--server"
|
|
12
|
+
server = ARGV.shift
|
|
13
|
+
abort "--server requires a server ID" if server.nil? || server == "--"
|
|
14
|
+
when "--force" then force = true
|
|
15
|
+
else app ||= arg
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
abort "Missing `--` separator. Example: wokku do myapp -- ps:list" unless ARGV.first == "--"
|
|
20
|
+
ARGV.shift # drop the --
|
|
21
|
+
args = ARGV.dup
|
|
22
|
+
ARGV.clear
|
|
23
|
+
abort "Missing dokku command. Example: wokku do myapp -- ps:list" if args.empty?
|
|
24
|
+
abort "Pass either APP or --server N, not both" if app && server
|
|
25
|
+
abort "Pass APP or --server N before --" if app.nil? && server.nil?
|
|
26
|
+
|
|
27
|
+
path = app ? "/apps/#{app}/dokku" : "/servers/#{server}/dokku"
|
|
28
|
+
data = api(:post, path, { args: args, force: force })
|
|
29
|
+
$stdout.write(data["stdout"]) if data["stdout"] && !data["stdout"].empty?
|
|
30
|
+
$stderr.write(data["stderr"]) if data["stderr"] && !data["stderr"].empty?
|
|
31
|
+
exit (data["exit_code"] || 0).to_i
|
|
32
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- Domains ---
|
|
4
|
+
register "domains", "List domains (usage: wokku domains APP)" do
|
|
5
|
+
id = ARGV.shift || abort("Usage: wokku domains APP")
|
|
6
|
+
data = api(:get, "/apps/#{id}/domains")
|
|
7
|
+
Wokku::Output.render(data) do |d|
|
|
8
|
+
if d.is_a?(Array)
|
|
9
|
+
d.each { |row| puts row["hostname"] || row }
|
|
10
|
+
else
|
|
11
|
+
puts_json d
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
register "domains:add", "Add domain (usage: wokku domains:add APP DOMAIN)" do
|
|
17
|
+
id = ARGV.shift || abort("Usage: wokku domains:add APP DOMAIN")
|
|
18
|
+
domain = ARGV.shift || abort("Missing domain name")
|
|
19
|
+
api(:post, "/apps/#{id}/domains", { hostname: domain })
|
|
20
|
+
Wokku::Output.status "Added domain: #{domain}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
register "domains:remove", "Remove a domain (usage: wokku domains:remove APP DOMAIN)" do
|
|
24
|
+
id = ARGV.shift || abort("Usage: wokku domains:remove APP DOMAIN")
|
|
25
|
+
domain = ARGV.shift || abort("Missing domain name")
|
|
26
|
+
row = find_domain(id, domain) || abort("No domain '#{domain}' on app #{id}")
|
|
27
|
+
api(:delete, "/apps/#{id}/domains/#{row['id']}")
|
|
28
|
+
Wokku::Output.status "Removed domain: #{domain}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
register "domains:clear", "Remove every custom domain on an app (usage: wokku domains:clear APP)" do
|
|
32
|
+
id = ARGV.shift || abort("Usage: wokku domains:clear APP")
|
|
33
|
+
list = api(:get, "/apps/#{id}/domains")
|
|
34
|
+
abort "No domains on app #{id}" unless list.is_a?(Array) && list.any?
|
|
35
|
+
list.each do |d|
|
|
36
|
+
api(:delete, "/apps/#{id}/domains/#{d['id']}")
|
|
37
|
+
Wokku::Output.status "Removed: #{d['hostname']}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- Logs ---
|
|
4
|
+
register "logs", "View logs (usage: wokku logs APP [--lines N] [--follow|-f])" do
|
|
5
|
+
id = ARGV.shift || abort("Usage: wokku logs APP")
|
|
6
|
+
lines = 100
|
|
7
|
+
follow = false
|
|
8
|
+
while arg = ARGV.shift
|
|
9
|
+
case arg
|
|
10
|
+
when "--lines" then lines = ARGV.shift.to_i
|
|
11
|
+
when "--follow", "-f" then follow = true
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
if follow
|
|
16
|
+
Wokku.api_client.stream(:get, "/apps/#{id}/logs?follow=1") do |chunk|
|
|
17
|
+
$stdout.write(chunk)
|
|
18
|
+
$stdout.flush
|
|
19
|
+
end
|
|
20
|
+
else
|
|
21
|
+
data = api(:get, "/apps/#{id}/logs?lines=#{lines}")
|
|
22
|
+
Wokku::Output.render(data) do |d|
|
|
23
|
+
if d.is_a?(Array)
|
|
24
|
+
d.each { |l| puts l }
|
|
25
|
+
elsif d.is_a?(Hash) && d["logs"]
|
|
26
|
+
puts d["logs"]
|
|
27
|
+
else
|
|
28
|
+
puts_json d
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- Process management ---
|
|
4
|
+
register "ps:restart", "Restart app (usage: wokku ps:restart APP)" do
|
|
5
|
+
id = ARGV.shift || abort("Usage: wokku ps:restart APP")
|
|
6
|
+
api(:post, "/apps/#{id}/restart")
|
|
7
|
+
Wokku::Output.status "Restarting..."
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
register "ps:stop", "Stop app (usage: wokku ps:stop APP)" do
|
|
11
|
+
id = ARGV.shift || abort("Usage: wokku ps:stop APP")
|
|
12
|
+
api(:post, "/apps/#{id}/stop")
|
|
13
|
+
Wokku::Output.status "Stopped."
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
register "ps:start", "Start app (usage: wokku ps:start APP)" do
|
|
17
|
+
id = ARGV.shift || abort("Usage: wokku ps:start APP")
|
|
18
|
+
api(:post, "/apps/#{id}/start")
|
|
19
|
+
Wokku::Output.status "Started."
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
register "ps:rebuild", "Rebuild + redeploy app (usage: wokku ps:rebuild APP)" do
|
|
23
|
+
id = ARGV.shift || abort("Usage: wokku ps:rebuild APP")
|
|
24
|
+
data = api(:post, "/apps/#{id}/deploy")
|
|
25
|
+
Wokku::Output.status "Rebuild triggered. Deploy ##{data['deploy_id']} (release ##{data['release_id']})."
|
|
26
|
+
Wokku::Output.status "Tail logs with: wokku logs #{id} --lines 200"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
register "redeploy", "Redeploy app from latest source (usage: wokku redeploy APP)" do
|
|
30
|
+
id = ARGV.shift || abort("Usage: wokku redeploy APP")
|
|
31
|
+
data = api(:post, "/apps/#{id}/deploy")
|
|
32
|
+
Wokku::Output.status "Redeploy triggered. Deploy ##{data['deploy_id']} (release ##{data['release_id']})."
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
register "ps:scale", "Scale processes (usage: wokku ps:scale APP web=2 worker=1)" do
|
|
36
|
+
id = ARGV.shift || abort("Usage: wokku ps:scale APP web=N worker=N")
|
|
37
|
+
scaling = {}
|
|
38
|
+
ARGV.each do |pair|
|
|
39
|
+
type, count = pair.split("=")
|
|
40
|
+
scaling[type] = count.to_i if type && count
|
|
41
|
+
end
|
|
42
|
+
ARGV.clear
|
|
43
|
+
abort "No scaling pairs given. Example: web=2 worker=1" if scaling.empty?
|
|
44
|
+
api(:patch, "/apps/#{id}/ps", { scaling: scaling })
|
|
45
|
+
Wokku::Output.status "Scaled: #{scaling.map { |t, c| "#{t}=#{c}" }.join(' ')}"
|
|
46
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- Releases ---
|
|
4
|
+
register "releases", "List releases (usage: wokku releases APP)" do
|
|
5
|
+
id = ARGV.shift || abort("Usage: wokku releases APP")
|
|
6
|
+
data = api(:get, "/apps/#{id}/releases")
|
|
7
|
+
Wokku::Output.render(data) do |d|
|
|
8
|
+
if d.is_a?(Array)
|
|
9
|
+
table(d.map { |r| { "version" => "v#{r['version']}", "description" => r["description"].to_s[0..40], "created" => r["created_at"].to_s[0..18] } })
|
|
10
|
+
else
|
|
11
|
+
puts_json d
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
register "rollback", "Rollback to release (usage: wokku rollback APP RELEASE_ID)" do
|
|
17
|
+
id = ARGV.shift || abort("Usage: wokku rollback APP RELEASE_ID")
|
|
18
|
+
release_id = ARGV.shift || abort("Missing release ID")
|
|
19
|
+
api(:post, "/apps/#{id}/releases/#{release_id}/rollback")
|
|
20
|
+
Wokku::Output.status "Rolling back..."
|
|
21
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- One-off command in an ephemeral container ---
|
|
4
|
+
register "run", "Run a one-off command (usage: wokku run APP -- COMMAND [ARGS...])" do
|
|
5
|
+
id = ARGV.shift || abort("Usage: wokku run APP -- COMMAND")
|
|
6
|
+
ARGV.shift if ARGV.first == "--"
|
|
7
|
+
cmd = ARGV.join(" ")
|
|
8
|
+
ARGV.clear
|
|
9
|
+
abort "Missing command. Example: wokku run APP -- bin/rails console" if cmd.strip.empty?
|
|
10
|
+
data = api(:post, "/apps/#{id}/run", { command: cmd })
|
|
11
|
+
puts data["output"] if data.is_a?(Hash) && data["output"]
|
|
12
|
+
exit (data["exit_code"] || 0).to_i if data.is_a?(Hash) && data.key?("exit_code")
|
|
13
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- Servers ---
|
|
4
|
+
register "servers", "List available servers" do
|
|
5
|
+
data = api(:get, "/servers")
|
|
6
|
+
default_name = Wokku::Config.load["default_server"]
|
|
7
|
+
Wokku::Output.render(data) do |list|
|
|
8
|
+
table(list.map { |s| {
|
|
9
|
+
"name" => s["name"], "host" => s["host"], "status" => s["status"],
|
|
10
|
+
"default" => s["name"] == default_name ? "*" : ""
|
|
11
|
+
} })
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
register "servers:info", "Show server details (usage: wokku servers:info SERVER)" do
|
|
16
|
+
name = ARGV.shift || abort("Usage: wokku servers:info SERVER")
|
|
17
|
+
data = api(:get, "/servers/#{name}")
|
|
18
|
+
Wokku::Output.render(data) { |d| puts_json d }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
register "servers:default", "Set the default server (usage: wokku servers:default SERVER | --clear)" do
|
|
22
|
+
arg = ARGV.shift || abort("Usage: wokku servers:default SERVER | --clear")
|
|
23
|
+
cfg = Wokku::Config.load
|
|
24
|
+
if arg == "--clear"
|
|
25
|
+
cfg.delete("default_server")
|
|
26
|
+
Wokku::Config.save(cfg)
|
|
27
|
+
Wokku::Output.status "Default server cleared."
|
|
28
|
+
else
|
|
29
|
+
api(:get, "/servers/#{arg}") # validates server exists; aborts on 404
|
|
30
|
+
cfg["default_server"] = arg
|
|
31
|
+
Wokku::Config.save(cfg)
|
|
32
|
+
Wokku::Output.status "Default server set to: #{arg}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- SSH keys ---
|
|
4
|
+
register "ssh-keys", "List your SSH public keys" do
|
|
5
|
+
data = api(:get, "/ssh_keys")
|
|
6
|
+
Wokku::Output.render(data) do |list|
|
|
7
|
+
table(list.map { |k| {
|
|
8
|
+
"name" => k["name"],
|
|
9
|
+
"fingerprint" => "#{k['fingerprint'].to_s[0..30]}...",
|
|
10
|
+
"created" => k["created_at"].to_s[0..18]
|
|
11
|
+
} })
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
register "ssh-keys:add", "Upload an SSH public key (usage: wokku ssh-keys:add [PATH] --name LABEL)" do
|
|
16
|
+
path = nil
|
|
17
|
+
name = nil
|
|
18
|
+
while arg = ARGV.shift
|
|
19
|
+
case arg
|
|
20
|
+
when "--name" then name = ARGV.shift
|
|
21
|
+
else path ||= arg
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
path ||= File.expand_path("~/.ssh/id_ed25519.pub")
|
|
25
|
+
abort "Public key not found at #{path}. Pass an explicit path." unless File.exist?(path)
|
|
26
|
+
abort "Missing --name LABEL" unless name
|
|
27
|
+
|
|
28
|
+
public_key = File.read(path).strip
|
|
29
|
+
data = api(:post, "/ssh_keys", { name: name, public_key: public_key })
|
|
30
|
+
Wokku::Output.status "Added SSH key '#{data['name']}' (#{data['fingerprint'][0..20]}...)"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
register "ssh-keys:remove", "Remove an SSH public key (usage: wokku ssh-keys:remove NAME | --fingerprint SHA — fingerprint takes precedence)" do
|
|
34
|
+
name = nil
|
|
35
|
+
fingerprint = nil
|
|
36
|
+
while arg = ARGV.shift
|
|
37
|
+
case arg
|
|
38
|
+
when "--fingerprint" then fingerprint = ARGV.shift
|
|
39
|
+
else name ||= arg
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
abort "Usage: wokku ssh-keys:remove NAME [--fingerprint SHA]" unless name || fingerprint
|
|
43
|
+
|
|
44
|
+
list = api(:get, "/ssh_keys")
|
|
45
|
+
match = if fingerprint
|
|
46
|
+
list.find { |k| k["fingerprint"] == fingerprint }
|
|
47
|
+
else
|
|
48
|
+
by_name = list.select { |k| k["name"] == name }
|
|
49
|
+
abort "No SSH key named '#{name}'" if by_name.empty?
|
|
50
|
+
abort "Multiple keys named '#{name}' — pass --fingerprint to disambiguate." if by_name.size > 1
|
|
51
|
+
by_name.first
|
|
52
|
+
end
|
|
53
|
+
abort "SSH key not found" unless match
|
|
54
|
+
|
|
55
|
+
api(:delete, "/ssh_keys/#{match['id']}")
|
|
56
|
+
Wokku::Output.status "Removed SSH key '#{match['name']}'"
|
|
57
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- Storage (host-bind volumes) ---
|
|
4
|
+
register "storage", "List storage mounts (usage: wokku storage APP)" do
|
|
5
|
+
id = ARGV.shift || abort("Usage: wokku storage APP")
|
|
6
|
+
data = api(:get, "/apps/#{id}/storage")
|
|
7
|
+
Wokku::Output.render(data) do |d|
|
|
8
|
+
list = d.is_a?(Hash) ? Array(d["mounts"]) : Array(d)
|
|
9
|
+
if list.empty?
|
|
10
|
+
puts "(no mounts configured)"
|
|
11
|
+
else
|
|
12
|
+
list.each { |m| puts m }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
register "storage:mount", "Add a host-bind mount (usage: wokku storage:mount APP HOST_PATH:CONTAINER_PATH)" do
|
|
18
|
+
id = ARGV.shift || abort("Usage: wokku storage:mount APP HOST_PATH:CONTAINER_PATH")
|
|
19
|
+
spec = ARGV.shift || abort("Missing HOST_PATH:CONTAINER_PATH")
|
|
20
|
+
abort "Mount must be HOST_PATH:CONTAINER_PATH" unless spec.include?(":")
|
|
21
|
+
api(:post, "/apps/#{id}/storage", { mount: spec })
|
|
22
|
+
Wokku::Output.status "Mounted: #{spec}"
|
|
23
|
+
Wokku::Output.status "Restart the app to pick up the mount: wokku ps:restart #{id}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
register "storage:unmount", "Remove a host-bind mount (usage: wokku storage:unmount APP HOST_PATH:CONTAINER_PATH)" do
|
|
27
|
+
id = ARGV.shift || abort("Usage: wokku storage:unmount APP HOST_PATH:CONTAINER_PATH")
|
|
28
|
+
spec = ARGV.shift || abort("Missing HOST_PATH:CONTAINER_PATH")
|
|
29
|
+
api(:delete, "/apps/#{id}/storage", { mount: spec })
|
|
30
|
+
Wokku::Output.status "Unmounted: #{spec}"
|
|
31
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- Templates ---
|
|
4
|
+
register "templates", "List available templates" do
|
|
5
|
+
data = api(:get, "/templates")
|
|
6
|
+
Wokku::Output.render(data) do |d|
|
|
7
|
+
if d.is_a?(Array)
|
|
8
|
+
d.each { |t| puts "#{t['slug'] || t['name']} #{t['description'].to_s[0..60]}" }
|
|
9
|
+
else
|
|
10
|
+
puts_json d
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
register "deploy", "Deploy template (usage: wokku deploy TEMPLATE [--server SERVER] [--name NAME])" do
|
|
16
|
+
slug = ARGV.shift || abort("Usage: wokku deploy TEMPLATE_SLUG [--server SERVER]")
|
|
17
|
+
server = nil
|
|
18
|
+
name = nil
|
|
19
|
+
while arg = ARGV.shift
|
|
20
|
+
case arg
|
|
21
|
+
when "--server" then server = ARGV.shift
|
|
22
|
+
when "--name" then name = ARGV.shift
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
server = resolve_server(explicit: server)
|
|
26
|
+
body = { slug: slug, server_id: server }
|
|
27
|
+
body[:name] = name if name
|
|
28
|
+
data = api(:post, "/templates/deploy", body)
|
|
29
|
+
Wokku::Output.status "Deploying #{slug}..."
|
|
30
|
+
Wokku::Output.render(data) { |d| puts_json d }
|
|
31
|
+
end
|
data/lib/wokku/config.rb
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Wokku
|
|
7
|
+
module Config
|
|
8
|
+
DEFAULT_DIR = File.expand_path("~/.wokku")
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def dir
|
|
13
|
+
ENV["WOKKU_CONFIG_DIR"] || DEFAULT_DIR
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def file
|
|
17
|
+
File.join(dir, "config.json")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def load
|
|
21
|
+
return {} unless File.exist?(file)
|
|
22
|
+
JSON.parse(File.read(file))
|
|
23
|
+
rescue StandardError
|
|
24
|
+
{}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def save(data)
|
|
28
|
+
FileUtils.mkdir_p(dir)
|
|
29
|
+
File.write(file, JSON.pretty_generate(data))
|
|
30
|
+
File.chmod(0600, file)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def api_url
|
|
34
|
+
load["api_url"] || ENV["WOKKU_API_URL"] || "https://wokku.cloud/api/v1"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def api_token
|
|
38
|
+
load["token"] || ENV["WOKKU_API_TOKEN"]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Cross-command helpers. Defined as top-level methods (matching the
|
|
4
|
+
# api/table/puts_json pattern) so command blocks can call them without
|
|
5
|
+
# a namespace prefix. Loaded by cli/wokku at boot.
|
|
6
|
+
|
|
7
|
+
# Look up a domain row by hostname so users can pass the name they know
|
|
8
|
+
# rather than an internal id. Returns nil when not found or when the API
|
|
9
|
+
# returns a non-array (error envelope).
|
|
10
|
+
def find_domain(app_id, hostname)
|
|
11
|
+
list = api(:get, "/apps/#{app_id}/domains")
|
|
12
|
+
return nil unless list.is_a?(Array)
|
|
13
|
+
list.find { |d| d["hostname"] == hostname }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Resolve which server to target for a deploy/create. Walks the
|
|
17
|
+
# precedence chain: explicit flag → env → config → auto-pick if
|
|
18
|
+
# only one server → abort with helpful error. Returns the server
|
|
19
|
+
# name or UUID (whatever was explicitly passed/configured).
|
|
20
|
+
def resolve_server(explicit: nil)
|
|
21
|
+
return explicit if explicit
|
|
22
|
+
env = ENV["WOKKU_DEFAULT_SERVER"]
|
|
23
|
+
return env if env && !env.empty?
|
|
24
|
+
cfg_default = Wokku::Config.load["default_server"]
|
|
25
|
+
return cfg_default if cfg_default
|
|
26
|
+
|
|
27
|
+
servers = api(:get, "/servers")
|
|
28
|
+
unless servers.is_a?(Array)
|
|
29
|
+
abort "Unexpected response from /servers: #{servers.inspect[0..200]}"
|
|
30
|
+
end
|
|
31
|
+
abort "No servers available. Contact wokku.cloud support." if servers.empty?
|
|
32
|
+
return servers.first["name"] if servers.size == 1
|
|
33
|
+
|
|
34
|
+
names = servers.map { |s| s["name"] }.join(", ")
|
|
35
|
+
abort "Multiple servers available (#{names}). Pass --server NAME or set a default: wokku servers:default NAME"
|
|
36
|
+
end
|
data/lib/wokku/output.rb
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Wokku
|
|
6
|
+
module Output
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def table(rows, headers: nil)
|
|
10
|
+
return if rows.empty?
|
|
11
|
+
cols = (headers || rows.first.keys).map(&:to_s)
|
|
12
|
+
widths = cols.map(&:length)
|
|
13
|
+
rows.each { |r| cols.each_with_index { |c, i| widths[i] = [widths[i], r[c].to_s.length].max } }
|
|
14
|
+
|
|
15
|
+
fmt = widths.map { |w| "%-#{w}s" }.join(" ")
|
|
16
|
+
puts fmt % cols.map(&:upcase)
|
|
17
|
+
puts widths.map { |w| "-" * w }.join(" ")
|
|
18
|
+
rows.each { |r| puts fmt % cols.map { |c| r[c] } }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def puts_json(data)
|
|
22
|
+
puts JSON.pretty_generate(data)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Smart printer for read commands: prints raw API response as JSON when
|
|
26
|
+
# the global --json flag is set; otherwise yields to the human formatter.
|
|
27
|
+
def render(data)
|
|
28
|
+
if Wokku.json?
|
|
29
|
+
puts JSON.pretty_generate(data)
|
|
30
|
+
else
|
|
31
|
+
yield data
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Suppressible status / hint message. Silenced by --quiet or --json.
|
|
36
|
+
# Errors should NOT use this — they go through `abort` which writes to
|
|
37
|
+
# stderr and is never silenced.
|
|
38
|
+
def status(message)
|
|
39
|
+
return if Wokku.quiet? || Wokku.json?
|
|
40
|
+
puts message
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/wokku.rb
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "json"
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "io/console"
|
|
6
|
+
require "fileutils"
|
|
7
|
+
|
|
8
|
+
require "wokku/version"
|
|
9
|
+
require "wokku/config"
|
|
10
|
+
require "wokku/output"
|
|
11
|
+
require "wokku/api_client"
|
|
12
|
+
require "wokku/registry"
|
|
13
|
+
require "wokku/helpers"
|
|
14
|
+
|
|
15
|
+
module Wokku
|
|
16
|
+
class << self
|
|
17
|
+
attr_writer :api_client
|
|
18
|
+
attr_accessor :json, :quiet
|
|
19
|
+
|
|
20
|
+
def api_client
|
|
21
|
+
@api_client ||= ApiClient.new(user_agent: "WokkuCLI/#{VERSION}")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def json? = !!@json
|
|
25
|
+
def quiet? = !!@quiet
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module CLI
|
|
29
|
+
COMMANDS = Wokku::Registry::COMMANDS
|
|
30
|
+
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
def dispatch(args)
|
|
34
|
+
sep = args.index("--")
|
|
35
|
+
head = sep ? args[0...sep] : args.dup
|
|
36
|
+
tail = sep ? args[sep..] : []
|
|
37
|
+
Wokku.json = !!head.delete("--json")
|
|
38
|
+
Wokku.quiet = !!(head.delete("--quiet") || head.delete("-q"))
|
|
39
|
+
ARGV.replace(head + tail)
|
|
40
|
+
command = ARGV.shift
|
|
41
|
+
|
|
42
|
+
if command.nil? || command == "help" || command == "--help" || command == "-h"
|
|
43
|
+
show_help
|
|
44
|
+
return 0
|
|
45
|
+
elsif COMMANDS[command]
|
|
46
|
+
COMMANDS[command][:handler].call
|
|
47
|
+
return 0
|
|
48
|
+
else
|
|
49
|
+
puts "Unknown command: #{command}"
|
|
50
|
+
puts "Run 'wokku help' for available commands."
|
|
51
|
+
return 1
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def show_help
|
|
56
|
+
puts "Wokku CLI v#{Wokku::VERSION}"
|
|
57
|
+
puts "Usage: wokku COMMAND [args] [--json] [--quiet]"
|
|
58
|
+
puts
|
|
59
|
+
puts "Commands:"
|
|
60
|
+
COMMANDS.sort_by { |k, _| k }.each do |name, cmd|
|
|
61
|
+
puts " %-20s %s" % [name, cmd[:desc]]
|
|
62
|
+
end
|
|
63
|
+
puts
|
|
64
|
+
puts "Global flags:"
|
|
65
|
+
puts " --json Machine-readable JSON output (read commands only)"
|
|
66
|
+
puts " --quiet Suppress success messages and hints (alias: -q)"
|
|
67
|
+
puts
|
|
68
|
+
puts "Run 'wokku auth:login' to get started."
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Top-level helper shims used by command blocks (preserved for compatibility
|
|
74
|
+
# with the existing register-DSL pattern).
|
|
75
|
+
def api(method, path, body = nil)
|
|
76
|
+
Wokku.api_client.request(method, path, body)
|
|
77
|
+
rescue Wokku::ApiClient::NotAuthenticated, Wokku::ApiClient::Timeout,
|
|
78
|
+
Wokku::ApiClient::Unreachable, Wokku::ApiClient::Error => e
|
|
79
|
+
abort e.message
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def load_config = Wokku::Config.load
|
|
83
|
+
def save_config(data) = Wokku::Config.save(data)
|
|
84
|
+
def api_url = Wokku::Config.api_url
|
|
85
|
+
def api_token = Wokku::Config.api_token
|
|
86
|
+
def table(rows, headers: nil) = Wokku::Output.table(rows, headers: headers)
|
|
87
|
+
def puts_json(data) = Wokku::Output.puts_json(data)
|
|
88
|
+
def register(name, desc, &block) = Wokku::Registry.register(name, desc, &block)
|
|
89
|
+
|
|
90
|
+
CONFIG_DIR = Wokku::Config::DEFAULT_DIR
|
|
91
|
+
CONFIG_FILE = File.join(CONFIG_DIR, "config.json")
|
|
92
|
+
VERSION = Wokku::VERSION
|
|
93
|
+
|
|
94
|
+
# Load all command files (they register via the top-level `register` shim).
|
|
95
|
+
Dir[File.expand_path("wokku/commands/*.rb", __dir__)].sort.each { |f| require f }
|
metadata
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: wokku-cli
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Johannes Dwicahyo
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Deploy and manage apps, databases, domains, and SSH keys on Wokku.cloud
|
|
13
|
+
or self-hosted Wokku servers.
|
|
14
|
+
email:
|
|
15
|
+
- johannesdwicahyo@gmail.com
|
|
16
|
+
executables:
|
|
17
|
+
- wokku
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- CHANGELOG.md
|
|
22
|
+
- LICENSE
|
|
23
|
+
- README.md
|
|
24
|
+
- exe/wokku
|
|
25
|
+
- lib/wokku.rb
|
|
26
|
+
- lib/wokku/api_client.rb
|
|
27
|
+
- lib/wokku/commands/activity.rb
|
|
28
|
+
- lib/wokku/commands/addons.rb
|
|
29
|
+
- lib/wokku/commands/apps.rb
|
|
30
|
+
- lib/wokku/commands/auth.rb
|
|
31
|
+
- lib/wokku/commands/buildpacks.rb
|
|
32
|
+
- lib/wokku/commands/certs.rb
|
|
33
|
+
- lib/wokku/commands/checks.rb
|
|
34
|
+
- lib/wokku/commands/config.rb
|
|
35
|
+
- lib/wokku/commands/databases.rb
|
|
36
|
+
- lib/wokku/commands/do.rb
|
|
37
|
+
- lib/wokku/commands/domains.rb
|
|
38
|
+
- lib/wokku/commands/logs.rb
|
|
39
|
+
- lib/wokku/commands/ps.rb
|
|
40
|
+
- lib/wokku/commands/releases.rb
|
|
41
|
+
- lib/wokku/commands/run.rb
|
|
42
|
+
- lib/wokku/commands/servers.rb
|
|
43
|
+
- lib/wokku/commands/ssh_keys.rb
|
|
44
|
+
- lib/wokku/commands/storage.rb
|
|
45
|
+
- lib/wokku/commands/templates.rb
|
|
46
|
+
- lib/wokku/commands/version.rb
|
|
47
|
+
- lib/wokku/config.rb
|
|
48
|
+
- lib/wokku/helpers.rb
|
|
49
|
+
- lib/wokku/output.rb
|
|
50
|
+
- lib/wokku/registry.rb
|
|
51
|
+
- lib/wokku/version.rb
|
|
52
|
+
homepage: https://wokku.cloud
|
|
53
|
+
licenses:
|
|
54
|
+
- MIT
|
|
55
|
+
metadata:
|
|
56
|
+
homepage_uri: https://wokku.cloud
|
|
57
|
+
source_code_uri: https://github.com/johannesdwicahyo/wokku-cli
|
|
58
|
+
bug_tracker_uri: https://github.com/johannesdwicahyo/wokku-cli/issues
|
|
59
|
+
changelog_uri: https://github.com/johannesdwicahyo/wokku-cli/blob/main/CHANGELOG.md
|
|
60
|
+
rubygems_mfa_required: 'true'
|
|
61
|
+
rdoc_options: []
|
|
62
|
+
require_paths:
|
|
63
|
+
- lib
|
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: 3.2.0
|
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '0'
|
|
74
|
+
requirements: []
|
|
75
|
+
rubygems_version: 3.6.9
|
|
76
|
+
specification_version: 4
|
|
77
|
+
summary: Wokku CLI — manage your Wokku apps from the terminal
|
|
78
|
+
test_files: []
|