wokku-cli 0.6.0 → 0.7.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 +10 -0
- data/lib/wokku/api_client.rb +22 -0
- data/lib/wokku/commands/addons.rb +12 -11
- data/lib/wokku/commands/apps.rb +13 -0
- data/lib/wokku/commands/auth.rb +9 -1
- data/lib/wokku/commands/backups.rb +45 -0
- data/lib/wokku/commands/billing.rb +3 -18
- data/lib/wokku/commands/config.rb +5 -5
- data/lib/wokku/commands/databases.rb +10 -0
- data/lib/wokku/commands/deploys.rb +47 -0
- data/lib/wokku/commands/doctor.rb +18 -0
- data/lib/wokku/commands/ps.rb +38 -0
- data/lib/wokku/commands/releases.rb +7 -0
- data/lib/wokku/commands/run.rb +19 -5
- data/lib/wokku/commands/servers.rb +6 -0
- data/lib/wokku/commands/templates.rb +6 -0
- data/lib/wokku/commands/tokens.rb +25 -0
- data/lib/wokku/helpers.rb +28 -0
- data/lib/wokku/version.rb +1 -1
- data/lib/wokku.rb +9 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4dc872124bc4d78ef3fa77530026b1b3b8dc9ca48465b575660bbfa6b872cd00
|
|
4
|
+
data.tar.gz: 100d67fadebf85a3f0dd758072bca38fa55e6a3892da888adabd3e73200e6b3d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bd282ef31062f4581c95d513d4debe4692d7f8e26f0465809a43b1b0c6f2ebc870c3a9c4ebe35550d5061f8d026ae4e53a75a28c7524dc9d7b51508ec8b47008
|
|
7
|
+
data.tar.gz: 667e796d0392e32f04923a62c20e8257e2c964e09cbe65605bebf2c88a76a1775606a64ea294b11e56ba7252388b727d49cef40fb8f3b15915d159ff14c3bb01
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0 — 2026-10-23
|
|
4
|
+
|
|
5
|
+
Full API parity for everything customers can do (CI now fails if the CLI falls behind the API contract).
|
|
6
|
+
|
|
7
|
+
- New: `deploys`, `deploys:info`, `deploy:tarball`, `releases:info`, `ps`, `ps:sizes`, `ps:dynos`, `ps:resize` (usage: `ps:resize APP TYPE --memory MB [--cpu N]`), `run:status`, `backups`, `backups:capture`, `backups:download`, `backups:restore`, `databases:import`, `addons:remove`, `apps:update`, `tokens`, `tokens:create`, `tokens:revoke`, `doctor`, `ai:diagnose`, `servers:status`, `templates:info`.
|
|
8
|
+
- Fixed: `wokku run` now waits for the command and exits with its exit code (it used to return immediately).
|
|
9
|
+
- Fixed: `wokku config` prints `KEY=value` (it printed the response wrapper); viewers see `(hidden — viewer)`.
|
|
10
|
+
- `auth:logout` also revokes the token on the server. It also signs out the MCP plugin (they share ~/.wokku/config.json).
|
|
11
|
+
- Retired: `billing:usage` (usage is per resource now; see `billing:plan`) and `addons:add` (use `addons:shared:enable` / `addons:dedicated:upgrade`).
|
|
12
|
+
|
|
3
13
|
## 0.6.0 — 2026-07-30
|
|
4
14
|
|
|
5
15
|
### Removed
|
data/lib/wokku/api_client.rb
CHANGED
|
@@ -47,6 +47,28 @@ module Wokku
|
|
|
47
47
|
parse_response(resp)
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
# Raw-body POST (databases.import). Streams the file — dumps can be GBs.
|
|
51
|
+
def upload(path, file_path)
|
|
52
|
+
@last_path = path
|
|
53
|
+
raise NotAuthenticated, "Not logged in. Run: wokku auth:login" unless token
|
|
54
|
+
|
|
55
|
+
uri = URI("#{url}#{path}")
|
|
56
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
57
|
+
http.use_ssl = uri.scheme == "https"
|
|
58
|
+
http.open_timeout = OPEN_TIMEOUT
|
|
59
|
+
http.read_timeout = 900
|
|
60
|
+
|
|
61
|
+
req = Net::HTTP::Post.new(uri)
|
|
62
|
+
req["Authorization"] = "Bearer #{token}"
|
|
63
|
+
req["Content-Type"] = "application/octet-stream"
|
|
64
|
+
req["User-Agent"] = @user_agent
|
|
65
|
+
File.open(file_path, "rb") do |f|
|
|
66
|
+
req["Content-Length"] = f.size.to_s
|
|
67
|
+
req.body_stream = f
|
|
68
|
+
parse_response(perform(http, req, uri))
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
50
72
|
def stream(method, path, &block)
|
|
51
73
|
@last_path = path
|
|
52
74
|
raise NotAuthenticated, "Not logged in. Run: wokku auth:login" unless token
|
|
@@ -20,17 +20,9 @@ register "addons", "List add-ons (usage: wokku addons APP)" do
|
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
register "addons:add", "
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
name = nil
|
|
27
|
-
while (arg = ARGV.shift)
|
|
28
|
-
name = ARGV.shift if arg == "--name"
|
|
29
|
-
end
|
|
30
|
-
body = { service_type: service_type }
|
|
31
|
-
body[:name] = name if name
|
|
32
|
-
data = api(:post, "/apps/#{id}/addons", body)
|
|
33
|
-
Wokku::Output.status "Added #{service_type}: #{data['name'] || data['id']}"
|
|
23
|
+
register "addons:add", "Retired — use addons:shared:enable or addons:dedicated:upgrade" do
|
|
24
|
+
ARGV.clear
|
|
25
|
+
abort "addons:add is retired — use addons:shared:enable APP ENGINE (free) or addons:dedicated:upgrade APP ENGINE"
|
|
34
26
|
end
|
|
35
27
|
|
|
36
28
|
# --- Bundle v2 — shared engines ---
|
|
@@ -78,3 +70,12 @@ register "addons:dedicated:upgrade", "Upgrade a box to a dedicated DB or Redis (
|
|
|
78
70
|
data = api(:post, "/apps/#{id}/addons/dedicated", { engine: engine })
|
|
79
71
|
Wokku::Output.status(data["message"] || "Dedicated #{engine} upgrade queued for #{id}")
|
|
80
72
|
end
|
|
73
|
+
|
|
74
|
+
register "addons:remove", "Remove an add-on from an app (usage: wokku addons:remove APP ADDON_ID)" do
|
|
75
|
+
id = ARGV.shift || abort("Usage: wokku addons:remove APP ADDON_ID")
|
|
76
|
+
addon = ARGV.shift || abort("Missing ADDON_ID (see: wokku addons #{id})")
|
|
77
|
+
print "Are you sure you want to remove add-on #{addon} from #{id}? This destroys the database (a farewell snapshot is taken). (y/N): "
|
|
78
|
+
abort "Cancelled." unless $stdin.gets.to_s.strip.downcase == "y"
|
|
79
|
+
api(:delete, "/apps/#{id}/addons/#{addon}")
|
|
80
|
+
Wokku::Output.status "Add-on removed."
|
|
81
|
+
end
|
data/lib/wokku/commands/apps.rb
CHANGED
|
@@ -51,3 +51,16 @@ register "apps:destroy", "Delete app (usage: wokku apps:destroy APP)" do
|
|
|
51
51
|
api(:delete, "/apps/#{id}")
|
|
52
52
|
Wokku::Output.status "App deleted."
|
|
53
53
|
end
|
|
54
|
+
|
|
55
|
+
register "apps:update", "Update app settings (usage: wokku apps:update APP [--branch BRANCH] [--name NAME])" do
|
|
56
|
+
id = ARGV.shift || abort("Usage: wokku apps:update APP [--branch BRANCH] [--name NAME]")
|
|
57
|
+
body = {}
|
|
58
|
+
{ "--branch" => :deploy_branch, "--name" => :name }.each do |flag, key|
|
|
59
|
+
next unless (i = ARGV.index(flag))
|
|
60
|
+
body[key] = ARGV[i + 1] || abort("Missing value for #{flag}")
|
|
61
|
+
ARGV.slice!(i, 2)
|
|
62
|
+
end
|
|
63
|
+
abort "Nothing to update. Pass --branch and/or --name." if body.empty?
|
|
64
|
+
api(:patch, "/apps/#{id}", body)
|
|
65
|
+
Wokku::Output.status "App updated."
|
|
66
|
+
end
|
data/lib/wokku/commands/auth.rb
CHANGED
|
@@ -9,9 +9,17 @@ register "auth:login", "Authenticate with Wokku via browser device flow" do
|
|
|
9
9
|
Wokku::Auth.login_with_device_flow!(Wokku::Config.api_url)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
register "auth:logout", "Log out" do
|
|
12
|
+
register "auth:logout", "Log out (revokes this token on the server)" do
|
|
13
13
|
path = Wokku::Config.file
|
|
14
14
|
if File.exist?(path)
|
|
15
|
+
begin
|
|
16
|
+
# covers: api(:delete, "/auth/logout")
|
|
17
|
+
# only revoke a token this file holds, never an env-provided (CI) one
|
|
18
|
+
Wokku.api_client.request(:delete, "/auth/logout") if Wokku::Config.load["token"]
|
|
19
|
+
rescue Wokku::ApiClient::NotAuthenticated, Wokku::ApiClient::Timeout,
|
|
20
|
+
Wokku::ApiClient::Unreachable, Wokku::ApiClient::Error
|
|
21
|
+
nil # best effort: always clear the local login
|
|
22
|
+
end
|
|
15
23
|
File.delete(path)
|
|
16
24
|
Wokku::Output.status "Logged out."
|
|
17
25
|
else
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- Database backups ---
|
|
4
|
+
register "backups", "List backups (usage: wokku backups DB)" do
|
|
5
|
+
db = ARGV.shift || abort("Usage: wokku backups DB")
|
|
6
|
+
data = api(:get, "/databases/#{db}/backups")
|
|
7
|
+
Wokku::Output.render(data) do |d|
|
|
8
|
+
next puts_json(d) unless d.is_a?(Array)
|
|
9
|
+
table(d.map { |b| { "id" => b["id"], "status" => b["status"], "size" => b["size_bytes"], "created" => b["created_at"].to_s[0..18] } })
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
register "backups:capture", "Start a backup now (usage: wokku backups:capture DB)" do
|
|
14
|
+
db = ARGV.shift || abort("Usage: wokku backups:capture DB")
|
|
15
|
+
data = api(:post, "/databases/#{db}/backups")
|
|
16
|
+
Wokku::Output.status "Backup started#{data.is_a?(Hash) && data['id'] ? " (#{data['id']})" : ''}."
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
register "backups:download", "Download a backup, or print its URL (usage: wokku backups:download DB BACKUP_ID [FILE])" do
|
|
20
|
+
db = ARGV.shift || abort("Usage: wokku backups:download DB BACKUP_ID [FILE]")
|
|
21
|
+
backup_id = ARGV.shift || abort("Missing BACKUP_ID")
|
|
22
|
+
out = ARGV.shift
|
|
23
|
+
data = api(:get, "/databases/#{db}/backups/#{backup_id}/download")
|
|
24
|
+
url = data.is_a?(Hash) && data["url"]
|
|
25
|
+
abort "Unexpected response: #{data.inspect[0..200]}" unless url
|
|
26
|
+
next puts(url) unless out
|
|
27
|
+
|
|
28
|
+
uri = URI(url)
|
|
29
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
|
|
30
|
+
http.request(Net::HTTP::Get.new(uri)) do |resp|
|
|
31
|
+
abort "Download failed: HTTP #{resp.code}" unless resp.is_a?(Net::HTTPSuccess)
|
|
32
|
+
File.open(out, "wb") { |f| resp.read_body { |chunk| f.write(chunk) } }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
Wokku::Output.status "Saved #{out}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
register "backups:restore", "Restore a backup — overwrites the database (usage: wokku backups:restore DB BACKUP_ID)" do
|
|
39
|
+
db = ARGV.shift || abort("Usage: wokku backups:restore DB BACKUP_ID")
|
|
40
|
+
backup_id = ARGV.shift || abort("Missing BACKUP_ID")
|
|
41
|
+
print "Are you sure you want to overwrite #{db} with backup #{backup_id}? (y/N): "
|
|
42
|
+
abort "Cancelled." unless $stdin.gets.to_s.strip.downcase == "y"
|
|
43
|
+
data = api(:post, "/databases/#{db}/backups/#{backup_id}/restore")
|
|
44
|
+
Wokku::Output.status((data.is_a?(Hash) && data["message"]) || "Restore requested.")
|
|
45
|
+
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# --- Billing (plan
|
|
3
|
+
# --- Billing (plan; checkout/portal stay in the dashboard) ---
|
|
4
4
|
|
|
5
5
|
register "billing:plan", "Show your current Wokku plan + limits (usage: wokku billing:plan)" do
|
|
6
6
|
data = api(:get, "/billing/current_plan")
|
|
@@ -10,21 +10,6 @@ register "billing:plan", "Show your current Wokku plan + limits (usage: wokku bi
|
|
|
10
10
|
Wokku::Output.status "max databases: #{data['max_databases']}" if data.key?("max_databases")
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
register "billing:usage", "
|
|
14
|
-
|
|
15
|
-
data = {} unless data.is_a?(Hash)
|
|
16
|
-
|
|
17
|
-
Wokku::Output.status "current: $#{format('%.2f', data['current_cost_dollars'] || 0.0)}"
|
|
18
|
-
Wokku::Output.status "projected: $#{format('%.2f', data['projected_cost_dollars'] || 0.0)}"
|
|
19
|
-
Wokku::Output.status "payment method: #{data['has_payment_method'] ? 'yes' : 'no'}"
|
|
20
|
-
|
|
21
|
-
resources = Array(data["resources"])
|
|
22
|
-
if resources.any?
|
|
23
|
-
Wokku::Output.status ""
|
|
24
|
-
Wokku::Output.status "resources:"
|
|
25
|
-
resources.each do |r|
|
|
26
|
-
cents = r["current_cost_cents"].to_i
|
|
27
|
-
Wokku::Output.status " #{r['resource_type']} #{r['tier_name']} $#{format('%.2f', cents / 100.0)}"
|
|
28
|
-
end
|
|
29
|
-
end
|
|
13
|
+
register "billing:usage", "Retired — usage is now per resource (see billing:plan)" do
|
|
14
|
+
abort "billing:usage is retired — usage is now per resource; see wokku billing:plan or the dashboard"
|
|
30
15
|
end
|
|
@@ -5,11 +5,9 @@ register "config", "Show config vars (usage: wokku config APP)" do
|
|
|
5
5
|
id = ARGV.shift || abort("Usage: wokku config APP")
|
|
6
6
|
data = api(:get, "/apps/#{id}/config")
|
|
7
7
|
Wokku::Output.render(data) do |d|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
puts_json d
|
|
12
|
-
end
|
|
8
|
+
next puts_json(d) unless d.is_a?(Hash)
|
|
9
|
+
vars = d["config"] || d
|
|
10
|
+
vars.each { |k, v| puts(d["masked"] && v.nil? ? "#{k}=(hidden — viewer)" : "#{k}=#{v}") }
|
|
13
11
|
end
|
|
14
12
|
end
|
|
15
13
|
|
|
@@ -35,6 +33,7 @@ register "config:get", "Show one config var (usage: wokku config:get APP KEY)" d
|
|
|
35
33
|
key = ARGV.shift || abort("Missing KEY")
|
|
36
34
|
data = api(:get, "/apps/#{id}/config")
|
|
37
35
|
vars = data.is_a?(Hash) ? (data["config"] || data) : {}
|
|
36
|
+
abort "Values are hidden for viewers — ask a team member or admin." if data.is_a?(Hash) && data["masked"]
|
|
38
37
|
abort "Key not set: #{key}" unless vars.key?(key)
|
|
39
38
|
Wokku::Output.render({ "key" => key, "value" => vars[key] }) { |_| puts vars[key] }
|
|
40
39
|
end
|
|
@@ -42,6 +41,7 @@ end
|
|
|
42
41
|
register "config:export", "Export config as .env (usage: wokku config:export APP)" do
|
|
43
42
|
id = ARGV.shift || abort("Usage: wokku config:export APP")
|
|
44
43
|
data = api(:get, "/apps/#{id}/config")
|
|
44
|
+
abort "Values are hidden for viewers — ask a team member or admin." if data.is_a?(Hash) && data["masked"]
|
|
45
45
|
vars = data.is_a?(Hash) ? (data["config"] || data) : {}
|
|
46
46
|
Wokku::Output.render(vars) do |d|
|
|
47
47
|
d.each do |k, v|
|
|
@@ -81,3 +81,13 @@ register "databases:unlink", "Unlink a database from an app (usage: wokku databa
|
|
|
81
81
|
api(:post, "/databases/#{db_name}/unlink", { app_id: app })
|
|
82
82
|
Wokku::Output.status "Unlinked '#{db_name}' from '#{app}'."
|
|
83
83
|
end
|
|
84
|
+
|
|
85
|
+
register "databases:import", "Import a dump — overwrites the database (usage: wokku databases:import DB FILE)" do
|
|
86
|
+
db = ARGV.shift || abort("Usage: wokku databases:import DB FILE")
|
|
87
|
+
file = ARGV.shift || abort("Missing FILE (e.g. a pg_dump -Fc dump)")
|
|
88
|
+
abort "File not found: #{file}" unless File.file?(file)
|
|
89
|
+
print "Are you sure you want to overwrite #{db} with #{File.basename(file)}? (y/N): "
|
|
90
|
+
abort "Cancelled." unless $stdin.gets.to_s.strip.downcase == "y"
|
|
91
|
+
data = api_upload(:post, "/databases/#{db}/import", file)
|
|
92
|
+
Wokku::Output.status(data.is_a?(Hash) && data["message"] ? data["message"] : "Imported.")
|
|
93
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
|
|
5
|
+
# --- Deploys ---
|
|
6
|
+
register "deploys", "List deploys (usage: wokku deploys APP)" do
|
|
7
|
+
id = ARGV.shift || abort("Usage: wokku deploys APP")
|
|
8
|
+
data = api(:get, "/apps/#{id}/deploys")
|
|
9
|
+
Wokku::Output.render(data) do |d|
|
|
10
|
+
next puts_json(d) unless d.is_a?(Array)
|
|
11
|
+
table(d.map { |x| { "id" => x["id"], "status" => x["status"], "commit" => x["commit_sha"].to_s[0, 7], "created" => x["created_at"].to_s[0..18] } })
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
register "deploys:info", "Show a deploy and its build log (usage: wokku deploys:info APP DEPLOY_ID)" do
|
|
16
|
+
id = ARGV.shift || abort("Usage: wokku deploys:info APP DEPLOY_ID")
|
|
17
|
+
deploy_id = ARGV.shift || abort("Missing DEPLOY_ID")
|
|
18
|
+
data = api(:get, "/apps/#{id}/deploys/#{deploy_id}")
|
|
19
|
+
Wokku::Output.render(data) do |d|
|
|
20
|
+
next puts_json(d) unless d.is_a?(Hash)
|
|
21
|
+
puts "status: #{d['status']}"
|
|
22
|
+
puts d["log"] if d["log"]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
TERMINAL_DEPLOY_STATUSES = %w[succeeded failed timed_out].freeze
|
|
27
|
+
|
|
28
|
+
register "deploy:tarball", "Deploy a local directory without git push (usage: wokku deploy:tarball APP [DIR])" do
|
|
29
|
+
id = ARGV.shift || abort("Usage: wokku deploy:tarball APP [DIR]")
|
|
30
|
+
dir = File.expand_path(ARGV.shift || ".")
|
|
31
|
+
abort "Not a directory: #{dir}" unless Dir.exist?(dir)
|
|
32
|
+
if Dir.exist?(File.join(dir, ".git")) && !IO.popen([ "git", "-C", dir, "status", "--porcelain" ], &:read).strip.empty?
|
|
33
|
+
$stderr.puts "warning: uncommitted changes are not deployed (git archive uses HEAD)"
|
|
34
|
+
end
|
|
35
|
+
Dir.mktmpdir do |tmp|
|
|
36
|
+
archive = File.join(tmp, "source.tar.gz")
|
|
37
|
+
build_tarball(dir, archive)
|
|
38
|
+
slot = api(:post, "/apps/#{id}/deploys/uploads")
|
|
39
|
+
upload_presigned(slot["upload_url"], archive)
|
|
40
|
+
dep = api(:post, "/apps/#{id}/deploys", { archive_key: slot["archive_key"] })
|
|
41
|
+
Wokku::Output.status "Build queued (#{dep['deploy_id']}). Following..."
|
|
42
|
+
final = poll_until("/apps/#{id}/deploys/#{dep['deploy_id']}", attempts: 320, interval: 3) { |d| d.is_a?(Hash) && TERMINAL_DEPLOY_STATUSES.include?(d["status"].to_s) }
|
|
43
|
+
abort "Still building after 16 minutes. Check: wokku deploys:info #{id} #{dep['deploy_id']}" unless final
|
|
44
|
+
Wokku::Output.status "Deploy #{final['status']}."
|
|
45
|
+
exit 1 unless final["status"] == "succeeded"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- Diagnostics ---
|
|
4
|
+
register "doctor", "Check auth, client version, host health and app state (usage: wokku doctor)" do
|
|
5
|
+
data = api(:get, "/doctor")
|
|
6
|
+
Wokku::Output.render(data) { |d| puts_json d }
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
register "ai:diagnose", "AI diagnosis of a failed deploy (usage: wokku ai:diagnose APP [DEPLOY_ID])" do
|
|
10
|
+
id = ARGV.shift || abort("Usage: wokku ai:diagnose APP [DEPLOY_ID]")
|
|
11
|
+
deploy_id = ARGV.shift
|
|
12
|
+
unless deploy_id
|
|
13
|
+
deploys = api(:get, "/apps/#{id}/deploys")
|
|
14
|
+
deploy_id = Array(deploys).first&.dig("id") || abort("No deploys for #{id}")
|
|
15
|
+
end
|
|
16
|
+
data = api(:post, "/ai/diagnose", { deploy_id: deploy_id })
|
|
17
|
+
Wokku::Output.render(data) { |d| d.is_a?(Hash) && d["diagnosis"] ? puts(d["diagnosis"]) : puts_json(d) }
|
|
18
|
+
end
|
data/lib/wokku/commands/ps.rb
CHANGED
|
@@ -44,3 +44,41 @@ register "ps:scale", "Scale processes (usage: wokku ps:scale APP web=2 worker=1)
|
|
|
44
44
|
api(:patch, "/apps/#{id}/ps", { scaling: scaling })
|
|
45
45
|
Wokku::Output.status "Scaled: #{scaling.map { |t, c| "#{t}=#{c}" }.join(' ')}"
|
|
46
46
|
end
|
|
47
|
+
|
|
48
|
+
register "ps", "Show process formation (usage: wokku ps APP)" do
|
|
49
|
+
id = ARGV.shift || abort("Usage: wokku ps APP")
|
|
50
|
+
data = api(:get, "/apps/#{id}/ps")
|
|
51
|
+
Wokku::Output.render(data) { |d| d.is_a?(Hash) && d["processes"].is_a?(Hash) ? d["processes"].each { |k, v| puts "#{k}: #{v}" } : puts_json(d) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
register "ps:sizes", "List box sizes (usage: wokku ps:sizes)" do
|
|
55
|
+
data = api(:get, "/dyno_tiers")
|
|
56
|
+
Wokku::Output.render(data) { |d| d.is_a?(Array) ? table(d) : puts_json(d) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
register "ps:dynos", "Show process allocations and box sizes (usage: wokku ps:dynos APP)" do
|
|
60
|
+
id = ARGV.shift || abort("Usage: wokku ps:dynos APP")
|
|
61
|
+
data = api(:get, "/apps/#{id}/dynos")
|
|
62
|
+
Wokku::Output.render(data) do |d|
|
|
63
|
+
next puts_json(d) unless d.is_a?(Array)
|
|
64
|
+
table(d.map { |a| { "type" => a["process_type"], "count" => a["count"], "size" => a["size_label"], "monthly" => format("%.2f", a["monthly_cost_cents"].to_i / 100.0) } })
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
register "ps:resize", "Change a process type's size (usage: wokku ps:resize APP TYPE --memory MB [--cpu N])" do
|
|
69
|
+
usage = "Usage: wokku ps:resize APP TYPE --memory MB [--cpu N]"
|
|
70
|
+
shape = {}
|
|
71
|
+
if (i = ARGV.index("--memory"))
|
|
72
|
+
shape[:memory_mb] = Integer(ARGV[i + 1] || abort("Missing value for --memory"))
|
|
73
|
+
ARGV.slice!(i, 2)
|
|
74
|
+
end
|
|
75
|
+
if (i = ARGV.index("--cpu"))
|
|
76
|
+
shape[:cpu] = Float(ARGV[i + 1] || abort("Missing value for --cpu"))
|
|
77
|
+
ARGV.slice!(i, 2)
|
|
78
|
+
end
|
|
79
|
+
id = ARGV.shift || abort(usage)
|
|
80
|
+
type = ARGV.shift || abort(usage)
|
|
81
|
+
abort "#{usage} (give --memory and/or --cpu; see: wokku ps:sizes)" if shape.empty?
|
|
82
|
+
api(:patch, "/apps/#{id}/ps", { resources: { type => shape } })
|
|
83
|
+
Wokku::Output.status "Resizing #{type}: #{shape.map { |k, v| "#{k}=#{v}" }.join(' ')}; applied in the background."
|
|
84
|
+
end
|
|
@@ -19,3 +19,10 @@ register "rollback", "Rollback to release (usage: wokku rollback APP RELEASE_ID)
|
|
|
19
19
|
api(:post, "/apps/#{id}/releases/#{release_id}/rollback")
|
|
20
20
|
Wokku::Output.status "Rolling back..."
|
|
21
21
|
end
|
|
22
|
+
|
|
23
|
+
register "releases:info", "Show one release (usage: wokku releases:info APP RELEASE_ID)" do
|
|
24
|
+
id = ARGV.shift || abort("Usage: wokku releases:info APP RELEASE_ID")
|
|
25
|
+
release_id = ARGV.shift || abort("Missing RELEASE_ID")
|
|
26
|
+
data = api(:get, "/apps/#{id}/releases/#{release_id}")
|
|
27
|
+
Wokku::Output.render(data) { |d| puts_json d }
|
|
28
|
+
end
|
data/lib/wokku/commands/run.rb
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# --- One-off command in an ephemeral container ---
|
|
4
|
-
|
|
4
|
+
# apps.run is async (202 {run_id}); poll runs.show to a terminal status.
|
|
5
|
+
register "run", "Run a one-off command (usage: wokku run APP -- COMMAND [ARGS...], e.g. wokku run APP -- bin/rails db:migrate)" do
|
|
5
6
|
id = ARGV.shift || abort("Usage: wokku run APP -- COMMAND")
|
|
6
7
|
ARGV.shift if ARGV.first == "--"
|
|
7
8
|
cmd = ARGV.join(" ")
|
|
8
9
|
ARGV.clear
|
|
9
|
-
abort "Missing command. Example: wokku run APP -- bin/rails
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
abort "Missing command. Example: wokku run APP -- bin/rails db:migrate" if cmd.strip.empty?
|
|
11
|
+
started = api(:post, "/apps/#{id}/run", { command: cmd })
|
|
12
|
+
run_id = started.is_a?(Hash) && started["run_id"]
|
|
13
|
+
abort "Unexpected response: #{started.inspect[0..200]}" unless run_id
|
|
14
|
+
$stderr.puts "Run #{run_id} started — check later with: wokku run:status #{id} #{run_id}"
|
|
15
|
+
final = poll_until("/apps/#{id}/runs/#{run_id}", attempts: 300, interval: 2) { |d| d.is_a?(Hash) && %w[succeeded failed].include?(d["status"].to_s) }
|
|
16
|
+
abort "Still running after 10 minutes. Check: wokku run:status #{id} #{run_id}" unless final
|
|
17
|
+
puts final["output"] if final["output"]
|
|
18
|
+
$stderr.puts final["error"] if final["error"]
|
|
19
|
+
exit (final["exit_code"] || (final["status"] == "succeeded" ? 0 : 1)).to_i
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
register "run:status", "Show a one-off run (usage: wokku run:status APP RUN_ID)" do
|
|
23
|
+
id = ARGV.shift || abort("Usage: wokku run:status APP RUN_ID")
|
|
24
|
+
run_id = ARGV.shift || abort("Missing RUN_ID")
|
|
25
|
+
data = api(:get, "/apps/#{id}/runs/#{run_id}")
|
|
26
|
+
Wokku::Output.render(data) { |d| puts_json d }
|
|
13
27
|
end
|
|
@@ -32,3 +32,9 @@ register "servers:default", "Set the default server (usage: wokku servers:defaul
|
|
|
32
32
|
Wokku::Output.status "Default server set to: #{arg}"
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
|
+
|
|
36
|
+
register "servers:status", "Show server health (usage: wokku servers:status SERVER)" do
|
|
37
|
+
server = ARGV.shift || abort("Usage: wokku servers:status SERVER")
|
|
38
|
+
data = api(:get, "/servers/#{server}/status")
|
|
39
|
+
Wokku::Output.render(data) { |d| puts_json d }
|
|
40
|
+
end
|
|
@@ -29,3 +29,9 @@ register "deploy", "Deploy template (usage: wokku deploy TEMPLATE [--server SERV
|
|
|
29
29
|
Wokku::Output.status "Deploying #{slug}..."
|
|
30
30
|
Wokku::Output.render(data) { |d| puts_json d }
|
|
31
31
|
end
|
|
32
|
+
|
|
33
|
+
register "templates:info", "Show a template (usage: wokku templates:info SLUG)" do
|
|
34
|
+
slug = ARGV.shift || abort("Usage: wokku templates:info SLUG")
|
|
35
|
+
data = api(:get, "/templates/#{slug}")
|
|
36
|
+
Wokku::Output.render(data) { |d| puts_json d }
|
|
37
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- API tokens ---
|
|
4
|
+
register "tokens", "List API tokens (usage: wokku tokens)" do
|
|
5
|
+
data = api(:get, "/auth/tokens")
|
|
6
|
+
Wokku::Output.render(data) do |d|
|
|
7
|
+
next puts_json(d) unless d.is_a?(Array)
|
|
8
|
+
table(d.map { |t| { "id" => t["id"], "name" => t["name"], "last_used" => t["last_used_at"].to_s[0..18], "expires" => t["expires_at"].to_s[0..18] } })
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
register "tokens:create", "Create an API token (usage: wokku tokens:create NAME)" do
|
|
13
|
+
name = ARGV.shift || abort("Usage: wokku tokens:create NAME")
|
|
14
|
+
data = api(:post, "/auth/tokens", { name: name })
|
|
15
|
+
Wokku::Output.render(data) do |d|
|
|
16
|
+
puts d["token"]
|
|
17
|
+
Wokku::Output.status "Copy it now — it won't be shown again."
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
register "tokens:revoke", "Revoke an API token (usage: wokku tokens:revoke TOKEN_ID)" do
|
|
22
|
+
token_id = ARGV.shift || abort("Usage: wokku tokens:revoke TOKEN_ID")
|
|
23
|
+
api(:delete, "/auth/tokens/#{token_id}")
|
|
24
|
+
Wokku::Output.status "Token revoked."
|
|
25
|
+
end
|
data/lib/wokku/helpers.rb
CHANGED
|
@@ -34,3 +34,31 @@ def resolve_server(explicit: nil)
|
|
|
34
34
|
names = servers.map { |s| s["name"] }.join(", ")
|
|
35
35
|
abort "Multiple servers available (#{names}). Pass --server NAME or set a default: wokku servers:default NAME"
|
|
36
36
|
end
|
|
37
|
+
|
|
38
|
+
# PUT a file to a presigned bucket URL. No Authorization header: the
|
|
39
|
+
# signature in the URL is the credential and an extra header breaks it.
|
|
40
|
+
def upload_presigned(url, file_path)
|
|
41
|
+
uri = URI(url)
|
|
42
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
43
|
+
http.use_ssl = uri.scheme == "https"
|
|
44
|
+
http.read_timeout = 300
|
|
45
|
+
req = Net::HTTP::Put.new(uri)
|
|
46
|
+
req["Content-Type"] = "application/gzip"
|
|
47
|
+
req.body = File.binread(file_path)
|
|
48
|
+
resp = http.request(req)
|
|
49
|
+
abort "Upload failed: HTTP #{resp.code}" unless resp.is_a?(Net::HTTPSuccess)
|
|
50
|
+
true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# .tar.gz of dir: committed files only for git repos, otherwise everything
|
|
54
|
+
# except .git/node_modules/tmp/log. COPYFILE_DISABLE stops macOS tar from
|
|
55
|
+
# adding ._ AppleDouble files that break builds.
|
|
56
|
+
def build_tarball(dir, archive_path)
|
|
57
|
+
ok = if Dir.exist?(File.join(dir, ".git"))
|
|
58
|
+
system("git", "-C", dir, "archive", "--format=tar.gz", "--output=#{archive_path}", "HEAD")
|
|
59
|
+
else
|
|
60
|
+
system({ "COPYFILE_DISABLE" => "1" }, "tar", "-czf", archive_path,
|
|
61
|
+
"--exclude=.git", "--exclude=node_modules", "--exclude=tmp", "--exclude=log", "-C", dir, ".")
|
|
62
|
+
end
|
|
63
|
+
abort "Could not archive #{dir}" unless ok
|
|
64
|
+
end
|
data/lib/wokku/version.rb
CHANGED
data/lib/wokku.rb
CHANGED
|
@@ -80,6 +80,15 @@ rescue Wokku::ApiClient::NotAuthenticated, Wokku::ApiClient::Timeout,
|
|
|
80
80
|
abort e.message
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
def api_upload(method, path, file_path)
|
|
84
|
+
abort "uploads are POST only" unless method == :post
|
|
85
|
+
abort "File not found: #{file_path}" unless File.file?(file_path.to_s)
|
|
86
|
+
Wokku.api_client.upload(path, file_path)
|
|
87
|
+
rescue Wokku::ApiClient::NotAuthenticated, Wokku::ApiClient::Timeout,
|
|
88
|
+
Wokku::ApiClient::Unreachable, Wokku::ApiClient::Error => e
|
|
89
|
+
abort e.message
|
|
90
|
+
end
|
|
91
|
+
|
|
83
92
|
# Poll GET <path> until the block returns truthy (returns that data) or the
|
|
84
93
|
# attempt budget is exhausted (returns nil). interval is injectable so specs
|
|
85
94
|
# can run with no real sleep.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: wokku-cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Johannes Dwicahyo
|
|
@@ -45,6 +45,7 @@ files:
|
|
|
45
45
|
- lib/wokku/commands/apps.rb
|
|
46
46
|
- lib/wokku/commands/apps_lifecycle.rb
|
|
47
47
|
- lib/wokku/commands/auth.rb
|
|
48
|
+
- lib/wokku/commands/backups.rb
|
|
48
49
|
- lib/wokku/commands/billing.rb
|
|
49
50
|
- lib/wokku/commands/cdn.rb
|
|
50
51
|
- lib/wokku/commands/certs.rb
|
|
@@ -53,7 +54,9 @@ files:
|
|
|
53
54
|
- lib/wokku/commands/cron.rb
|
|
54
55
|
- lib/wokku/commands/databases.rb
|
|
55
56
|
- lib/wokku/commands/db_monitor.rb
|
|
57
|
+
- lib/wokku/commands/deploys.rb
|
|
56
58
|
- lib/wokku/commands/dev.rb
|
|
59
|
+
- lib/wokku/commands/doctor.rb
|
|
57
60
|
- lib/wokku/commands/domains.rb
|
|
58
61
|
- lib/wokku/commands/github.rb
|
|
59
62
|
- lib/wokku/commands/https.rb
|
|
@@ -72,6 +75,7 @@ files:
|
|
|
72
75
|
- lib/wokku/commands/ssh_keys.rb
|
|
73
76
|
- lib/wokku/commands/teams.rb
|
|
74
77
|
- lib/wokku/commands/templates.rb
|
|
78
|
+
- lib/wokku/commands/tokens.rb
|
|
75
79
|
- lib/wokku/commands/tunnel.rb
|
|
76
80
|
- lib/wokku/commands/version.rb
|
|
77
81
|
- lib/wokku/commands/vitals.rb
|