wokku-cli 0.4.0 → 0.5.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/commands/apps_lifecycle.rb +50 -0
- data/lib/wokku/commands/billing.rb +30 -0
- data/lib/wokku/commands/cdn.rb +26 -0
- data/lib/wokku/commands/cron.rb +43 -0
- data/lib/wokku/commands/db_monitor.rb +30 -0
- data/lib/wokku/commands/dev.rb +92 -0
- data/lib/wokku/commands/github.rb +25 -0
- data/lib/wokku/commands/https.rb +25 -0
- data/lib/wokku/commands/maintenance.rb +25 -0
- data/lib/wokku/commands/metrics.rb +21 -0
- data/lib/wokku/commands/monitor.rb +29 -0
- data/lib/wokku/commands/notifications.rb +49 -0
- data/lib/wokku/commands/previews.rb +26 -0
- data/lib/wokku/commands/teams.rb +59 -0
- data/lib/wokku/commands/tunnel.rb +141 -0
- data/lib/wokku/commands/vitals.rb +23 -0
- data/lib/wokku/version.rb +1 -1
- metadata +17 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 07765bcc128b05f4309dcba3b0b5ac7679a67ecb7db3da6bac86b6ebe947b44d
|
|
4
|
+
data.tar.gz: 466a009f4a186f7e621e16fbb74bae021b150a0a7bdef4f5f3e22eba417620f5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e3e6bbadba6e52c20b843542879100f0d4d40428c4b1a3f877c171c6f84452ff67ffc0bce4a15dfe182115bc32b1b7c5ea44000d8d88559c41b6e69c24bcdfde
|
|
7
|
+
data.tar.gz: '058f35efb1dd358b5183169fbdb35ef5ce7ee68ab708576cbed8ee9b03d01a896d9c465fed9cf53d655283bc4fa3621b944f22117825e7efe9fa87c61ed245e0'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0 — 2026-06-04
|
|
4
|
+
|
|
5
|
+
`wokku tunnel` — share a local port at `https://<sub>.wokku.dev` in one command. Pairs with wokku-cloud canonical backlog 3.4.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `wokku tunnel PORT [--subdomain S] [--app A]` — provisions a tunnel session via `POST /api/v1/tunnels`, downloads `frpc` v0.62.1 to `~/.wokku/bin/` on first run (~10 MB, one time), writes a per-process `frpc.toml`, execs frpc against the wokku-tunnel gateway, and closes the session on Ctrl-C.
|
|
10
|
+
|
|
11
|
+
Plan-tiered: Free gets 1 tunnel with a random subdomain and 2h timeout (watermarked); Solo+ unlocks custom subdomains and 3+ concurrent tunnels with no timeout. Server-side enforcement — the CLI just surfaces whichever the API returned.
|
|
12
|
+
|
|
3
13
|
## 0.4.0 — 2026-06-01
|
|
4
14
|
|
|
5
15
|
MCP commands — wire the wokku-plugin Claude Code MCP server with one command. Pairs with wokku-cloud canonical backlog 1.3.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- App lifecycle (rename / clone / lock / unlock / transfer) ---
|
|
4
|
+
|
|
5
|
+
register "apps:rename", "Rename an app (usage: wokku apps:rename APP NEW_NAME [--force])" do
|
|
6
|
+
id = ARGV.shift || abort("Usage: wokku apps:rename APP NEW_NAME [--force]")
|
|
7
|
+
new_name = ARGV.shift || abort("Missing NEW_NAME. Example: wokku apps:rename myapp myapp-renamed")
|
|
8
|
+
force = ARGV.delete("--force")
|
|
9
|
+
unless force || !$stdout.tty?
|
|
10
|
+
print "Rename `#{id}` to `#{new_name}`? This breaks deployed bookmarks. [y/N] "
|
|
11
|
+
answer = $stdin.gets.to_s.strip.downcase
|
|
12
|
+
abort "Aborted." unless answer == "y"
|
|
13
|
+
end
|
|
14
|
+
data = api(:patch, "/apps/#{id}/rename", { name: new_name })
|
|
15
|
+
Wokku::Output.status "Renamed to #{(data.is_a?(Hash) ? data['name'] : new_name)}."
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
register "apps:clone", "Clone an app config (no env vars/data carried). Usage: wokku apps:clone APP NEW_NAME [--no-skip-deploy]" do
|
|
19
|
+
id = ARGV.shift || abort("Usage: wokku apps:clone APP NEW_NAME [--no-skip-deploy]")
|
|
20
|
+
new_name = ARGV.shift || abort("Missing NEW_NAME. Example: wokku apps:clone myapp myapp-copy")
|
|
21
|
+
skip_deploy = !ARGV.delete("--no-skip-deploy")
|
|
22
|
+
data = api(:post, "/apps/#{id}/clone", { name: new_name, skip_deploy: skip_deploy })
|
|
23
|
+
Wokku::Output.status "Cloned to #{(data.is_a?(Hash) ? data['name'] : new_name)}. Push code to its remote to deploy."
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
register "apps:lock", "Set the dokku deploy lock (blocks future git push). Usage: wokku apps:lock APP" do
|
|
27
|
+
id = ARGV.shift || abort("Usage: wokku apps:lock APP")
|
|
28
|
+
data = api(:post, "/apps/#{id}/lock")
|
|
29
|
+
Wokku::Output.status((data.is_a?(Hash) && data["locked"]) ? "locked" : "ok")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
register "apps:unlock", "Release the dokku deploy lock. Usage: wokku apps:unlock APP" do
|
|
33
|
+
id = ARGV.shift || abort("Usage: wokku apps:unlock APP")
|
|
34
|
+
data = api(:post, "/apps/#{id}/unlock")
|
|
35
|
+
Wokku::Output.status((data.is_a?(Hash) && data["locked"] == false) ? "unlocked" : "ok")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
register "apps:transfer", "Initiate an app transfer to another user (they accept via email). Usage: wokku apps:transfer APP RECIPIENT_EMAIL [--force]" do
|
|
39
|
+
id = ARGV.shift || abort("Usage: wokku apps:transfer APP RECIPIENT_EMAIL [--force]")
|
|
40
|
+
recipient = ARGV.shift || abort("Missing RECIPIENT_EMAIL")
|
|
41
|
+
force = ARGV.delete("--force")
|
|
42
|
+
unless force || !$stdout.tty?
|
|
43
|
+
print "Transfer `#{id}` to `#{recipient}`? They must accept via the email link to take ownership. [y/N] "
|
|
44
|
+
answer = $stdin.gets.to_s.strip.downcase
|
|
45
|
+
abort "Aborted." unless answer == "y"
|
|
46
|
+
end
|
|
47
|
+
data = api(:post, "/apps/#{id}/transfer", { recipient_email: recipient })
|
|
48
|
+
email = data.is_a?(Hash) ? data["recipient_email"] : recipient
|
|
49
|
+
Wokku::Output.status "Transfer requested. #{email} will receive an email."
|
|
50
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- Billing (plan + usage; checkout/portal stay in the dashboard) ---
|
|
4
|
+
|
|
5
|
+
register "billing:plan", "Show your current Wokku plan + limits (usage: wokku billing:plan)" do
|
|
6
|
+
data = api(:get, "/billing/current_plan")
|
|
7
|
+
data = {} unless data.is_a?(Hash)
|
|
8
|
+
Wokku::Output.status "plan: #{data['plan'] || 'unknown'}"
|
|
9
|
+
Wokku::Output.status "max apps: #{data['max_apps']}" if data.key?("max_apps")
|
|
10
|
+
Wokku::Output.status "max databases: #{data['max_databases']}" if data.key?("max_databases")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
register "billing:usage", "Show month-to-date usage (usage: wokku billing:usage)" do
|
|
14
|
+
data = api(:get, "/billing/usage")
|
|
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
|
|
30
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- Cloudflare CDN proxy toggle ---
|
|
4
|
+
|
|
5
|
+
def print_cdn_state(enabled, note: nil)
|
|
6
|
+
Wokku::Output.status "cdn: #{enabled ? 'on' : 'off'}"
|
|
7
|
+
warn note if note
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
register "cdn", "Show Cloudflare CDN proxy state (usage: wokku cdn APP)" do
|
|
11
|
+
id = ARGV.shift || abort("Usage: wokku cdn APP")
|
|
12
|
+
data = api(:get, "/apps/#{id}")
|
|
13
|
+
print_cdn_state(data.is_a?(Hash) && data["cloudflare_proxied"])
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
register "cdn:on", "Enable Cloudflare CDN proxy (usage: wokku cdn:on APP)" do
|
|
17
|
+
id = ARGV.shift || abort("Usage: wokku cdn:on APP")
|
|
18
|
+
data = api(:patch, "/apps/#{id}/cdn", { enabled: true })
|
|
19
|
+
print_cdn_state(data.is_a?(Hash) ? data["enabled"] : true, note: "DNS propagation may take a minute.")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
register "cdn:off", "Disable Cloudflare CDN proxy (usage: wokku cdn:off APP)" do
|
|
23
|
+
id = ARGV.shift || abort("Usage: wokku cdn:off APP")
|
|
24
|
+
data = api(:patch, "/apps/#{id}/cdn", { enabled: false })
|
|
25
|
+
print_cdn_state(data.is_a?(Hash) ? data["enabled"] : false)
|
|
26
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- Scheduled tasks (cron) ---
|
|
4
|
+
|
|
5
|
+
register "cron", "List scheduled tasks for an app (usage: wokku cron APP)" do
|
|
6
|
+
id = ARGV.shift || abort("Usage: wokku cron APP")
|
|
7
|
+
data = api(:get, "/apps/#{id}/scheduled_tasks")
|
|
8
|
+
tasks = data.is_a?(Array) ? data : []
|
|
9
|
+
if tasks.empty?
|
|
10
|
+
Wokku::Output.status "no scheduled tasks"
|
|
11
|
+
next
|
|
12
|
+
end
|
|
13
|
+
tasks.each do |t|
|
|
14
|
+
Wokku::Output.status "#{t['id']} #{t['schedule']} #{t['command']}#{t['description'] ? " (#{t['description']})" : ''}"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
register "cron:add", "Add a scheduled task. Usage: wokku cron:add APP --schedule '* * * * *' --command 'CMD' [--description 'DESC']" do
|
|
19
|
+
id = ARGV.shift || abort("Usage: wokku cron:add APP --schedule '...' --command '...' [--description '...']")
|
|
20
|
+
schedule = nil
|
|
21
|
+
command = nil
|
|
22
|
+
description = nil
|
|
23
|
+
while arg = ARGV.shift
|
|
24
|
+
case arg
|
|
25
|
+
when "--schedule" then schedule = ARGV.shift
|
|
26
|
+
when "--command" then command = ARGV.shift
|
|
27
|
+
when "--description" then description = ARGV.shift
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
abort "Missing --schedule" if schedule.nil? || schedule.empty?
|
|
31
|
+
abort "Missing --command" if command.nil? || command.empty?
|
|
32
|
+
|
|
33
|
+
data = api(:post, "/apps/#{id}/scheduled_tasks", { command: command, schedule: schedule, description: description })
|
|
34
|
+
task_id = data.is_a?(Hash) ? data["id"] : nil
|
|
35
|
+
Wokku::Output.status "Scheduled task added#{task_id ? " (id=#{task_id})" : ''}."
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
register "cron:remove", "Remove a scheduled task (usage: wokku cron:remove APP TASK_ID)" do
|
|
39
|
+
id = ARGV.shift || abort("Usage: wokku cron:remove APP TASK_ID")
|
|
40
|
+
task_id = ARGV.shift || abort("Missing TASK_ID")
|
|
41
|
+
api(:delete, "/apps/#{id}/scheduled_tasks/#{task_id}")
|
|
42
|
+
Wokku::Output.status "Scheduled task removed."
|
|
43
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- Database monitor (connections, cache hit, slow queries) ---
|
|
4
|
+
|
|
5
|
+
register "db:monitor", "Show database connections, cache hit, size, and slow queries (usage: wokku db:monitor DB)" do
|
|
6
|
+
id = ARGV.shift || abort("Usage: wokku db:monitor DB")
|
|
7
|
+
data = api(:get, "/databases/#{id}/monitor")
|
|
8
|
+
|
|
9
|
+
if !data.is_a?(Hash) || data["recorded_at"].nil?
|
|
10
|
+
Wokku::Output.status "no stats collected yet"
|
|
11
|
+
next
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
size_mb = data["db_size_bytes"] ? (data["db_size_bytes"].to_f / (1024 * 1024)).round(2) : nil
|
|
15
|
+
Wokku::Output.status "conns: #{data['active_connections']} / #{data['max_connections']}"
|
|
16
|
+
Wokku::Output.status "size: #{size_mb} MB" if size_mb
|
|
17
|
+
Wokku::Output.status "cache hit: #{(data['cache_hit_ratio'].to_f * 100).round(2)}%"
|
|
18
|
+
|
|
19
|
+
queries = Array(data["top_queries"]).first(5)
|
|
20
|
+
if queries.any?
|
|
21
|
+
Wokku::Output.status ""
|
|
22
|
+
Wokku::Output.status "top queries:"
|
|
23
|
+
queries.each do |q|
|
|
24
|
+
qstr = q["query"].to_s.gsub(/\s+/, " ").strip
|
|
25
|
+
qstr = qstr[0, 80] + "…" if qstr.length > 80
|
|
26
|
+
Wokku::Output.status " #{qstr}"
|
|
27
|
+
Wokku::Output.status " calls=#{q['calls']} mean=#{q['mean_ms']}ms total=#{q['total_ms']}ms"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
# --- wokku dev: prod env + read-only DB tunnel for local dev ---
|
|
7
|
+
|
|
8
|
+
register "dev",
|
|
9
|
+
"Run a local command with prod env + read-only DB tunnel. Usage: wokku dev APP [--duration MIN] [--rw] [--local-port N] -- CMD [ARGS...]" do
|
|
10
|
+
args = ARGV.dup
|
|
11
|
+
ARGV.clear
|
|
12
|
+
|
|
13
|
+
id = args.shift || abort("Usage: wokku dev APP [flags] -- CMD [ARGS...]")
|
|
14
|
+
duration = 30
|
|
15
|
+
readwrite = false
|
|
16
|
+
local_port = 5433
|
|
17
|
+
user_cmd = nil
|
|
18
|
+
|
|
19
|
+
while (arg = args.shift)
|
|
20
|
+
case arg
|
|
21
|
+
when "--duration" then duration = args.shift.to_i
|
|
22
|
+
when "--rw" then readwrite = true
|
|
23
|
+
when "--local-port" then local_port = args.shift.to_i
|
|
24
|
+
when "--" then user_cmd = args.dup; break
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
abort "Missing user command. Add `-- bin/rails s` (or similar) after wokku flags." if user_cmd.nil? || user_cmd.empty?
|
|
29
|
+
|
|
30
|
+
session = api(:post, "/apps/#{id}/dev_sessions", { duration_min: duration.positive? ? duration : 30 })
|
|
31
|
+
session_id = session["session_id"]
|
|
32
|
+
|
|
33
|
+
tunnel_args = [
|
|
34
|
+
"ssh", "-N",
|
|
35
|
+
"-L", "#{local_port}:#{session['db_remote_host']}:#{session['db_remote_port']}",
|
|
36
|
+
"-p", session["ssh_port"].to_s,
|
|
37
|
+
"-o", "ExitOnForwardFailure=yes",
|
|
38
|
+
"-o", "ServerAliveInterval=30",
|
|
39
|
+
"-o", "StrictHostKeyChecking=accept-new",
|
|
40
|
+
"#{session['ssh_user']}@#{session['ssh_host']}"
|
|
41
|
+
]
|
|
42
|
+
tunnel_pid = Process.spawn(*tunnel_args)
|
|
43
|
+
|
|
44
|
+
ready = false
|
|
45
|
+
10.times do
|
|
46
|
+
begin
|
|
47
|
+
Socket.tcp("127.0.0.1", local_port, connect_timeout: 0.5) { |s| s.close rescue nil }
|
|
48
|
+
ready = true
|
|
49
|
+
rescue StandardError
|
|
50
|
+
sleep 0.5
|
|
51
|
+
end
|
|
52
|
+
break if ready
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
unless ready
|
|
56
|
+
Process.kill("TERM", tunnel_pid) rescue nil
|
|
57
|
+
api(:delete, "/apps/#{id}/dev_sessions/#{session_id}") rescue nil
|
|
58
|
+
abort "SSH tunnel failed to come up on port #{local_port}. Aborting."
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
env_data = api(:get, "/apps/#{id}/config")
|
|
62
|
+
env = env_data.is_a?(Hash) ? env_data.dup : {}
|
|
63
|
+
|
|
64
|
+
if env["DATABASE_URL"].to_s.start_with?("postgres")
|
|
65
|
+
db = URI.parse(env["DATABASE_URL"])
|
|
66
|
+
db.host = "localhost"
|
|
67
|
+
db.port = local_port
|
|
68
|
+
|
|
69
|
+
if readwrite
|
|
70
|
+
warn "⚠️ --rw MODE: writes will hit PROD database. 5s to Ctrl-C..."
|
|
71
|
+
sleep 5 unless ENV["WOKKU_DEV_SKIP_SLEEP"] == "1"
|
|
72
|
+
else
|
|
73
|
+
existing_query = db.query.to_s
|
|
74
|
+
ro = "options=-c%20default_transaction_read_only%3Don"
|
|
75
|
+
db.query = existing_query.empty? ? ro : "#{existing_query}&#{ro}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
env["DATABASE_URL"] = db.to_s
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
exit_status = nil
|
|
82
|
+
begin
|
|
83
|
+
user_pid = Process.spawn(env, *user_cmd)
|
|
84
|
+
_, status = Process.wait2(user_pid)
|
|
85
|
+
exit_status = status.exitstatus || 0
|
|
86
|
+
ensure
|
|
87
|
+
Process.kill("TERM", tunnel_pid) rescue nil
|
|
88
|
+
api(:delete, "/apps/#{id}/dev_sessions/#{session_id}") rescue nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
exit exit_status if exit_status
|
|
92
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- GitHub repository connect / disconnect ---
|
|
4
|
+
|
|
5
|
+
register "github:connect", "Connect a GitHub repo to an app (usage: wokku github:connect APP REPO [--branch BRANCH])" do
|
|
6
|
+
id = ARGV.shift || abort("Usage: wokku github:connect APP REPO [--branch BRANCH]")
|
|
7
|
+
repo = ARGV.shift || abort("Missing REPO. Example: wokku github:connect myapp owner/repo")
|
|
8
|
+
branch = "main"
|
|
9
|
+
while arg = ARGV.shift
|
|
10
|
+
case arg
|
|
11
|
+
when "--branch" then branch = ARGV.shift || abort("--branch requires a value")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
data = api(:post, "/apps/#{id}/github_connect", { repo: repo, branch: branch })
|
|
16
|
+
message = data.is_a?(Hash) ? data["message"] : nil
|
|
17
|
+
Wokku::Output.status(message || "Connected to #{repo} (#{branch}).")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
register "github:disconnect", "Disconnect the GitHub repo from an app (usage: wokku github:disconnect APP)" do
|
|
21
|
+
id = ARGV.shift || abort("Usage: wokku github:disconnect APP")
|
|
22
|
+
data = api(:delete, "/apps/#{id}/github_disconnect")
|
|
23
|
+
message = data.is_a?(Hash) ? data["message"] : nil
|
|
24
|
+
Wokku::Output.status(message || "Disconnected.")
|
|
25
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- HTTPS redirect toggle ---
|
|
4
|
+
|
|
5
|
+
def print_https_state(enabled)
|
|
6
|
+
Wokku::Output.status "https redirect: #{enabled ? 'on' : 'off'}"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
register "https", "Show HTTPS redirect state (usage: wokku https APP)" do
|
|
10
|
+
id = ARGV.shift || abort("Usage: wokku https APP")
|
|
11
|
+
data = api(:get, "/apps/#{id}")
|
|
12
|
+
print_https_state(data.is_a?(Hash) && data["https_redirect"])
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
register "https:on", "Enable HTTPS redirect (usage: wokku https:on APP)" do
|
|
16
|
+
id = ARGV.shift || abort("Usage: wokku https:on APP")
|
|
17
|
+
data = api(:patch, "/apps/#{id}/https", { enabled: true })
|
|
18
|
+
print_https_state(data.is_a?(Hash) ? data["enabled"] : true)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
register "https:off", "Disable HTTPS redirect (usage: wokku https:off APP)" do
|
|
22
|
+
id = ARGV.shift || abort("Usage: wokku https:off APP")
|
|
23
|
+
data = api(:patch, "/apps/#{id}/https", { enabled: false })
|
|
24
|
+
print_https_state(data.is_a?(Hash) ? data["enabled"] : false)
|
|
25
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- Maintenance mode toggle ---
|
|
4
|
+
|
|
5
|
+
def print_maintenance_state(enabled)
|
|
6
|
+
Wokku::Output.status "maintenance: #{enabled ? 'on' : 'off'}"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
register "maintenance", "Show maintenance mode state (usage: wokku maintenance APP)" do
|
|
10
|
+
id = ARGV.shift || abort("Usage: wokku maintenance APP")
|
|
11
|
+
data = api(:get, "/apps/#{id}")
|
|
12
|
+
print_maintenance_state(data.is_a?(Hash) && data["maintenance_enabled"])
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
register "maintenance:on", "Enable maintenance mode (usage: wokku maintenance:on APP)" do
|
|
16
|
+
id = ARGV.shift || abort("Usage: wokku maintenance:on APP")
|
|
17
|
+
data = api(:patch, "/apps/#{id}/maintenance", { enabled: true })
|
|
18
|
+
print_maintenance_state(data.is_a?(Hash) ? data["enabled"] : true)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
register "maintenance:off", "Disable maintenance mode (usage: wokku maintenance:off APP)" do
|
|
22
|
+
id = ARGV.shift || abort("Usage: wokku maintenance:off APP")
|
|
23
|
+
data = api(:patch, "/apps/#{id}/maintenance", { enabled: false })
|
|
24
|
+
print_maintenance_state(data.is_a?(Hash) ? data["enabled"] : false)
|
|
25
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- App runtime metrics (CPU / memory) ---
|
|
4
|
+
|
|
5
|
+
register "metrics", "Show latest CPU + memory for an app (usage: wokku metrics APP)" do
|
|
6
|
+
id = ARGV.shift || abort("Usage: wokku metrics APP")
|
|
7
|
+
data = api(:get, "/apps/#{id}/metrics")
|
|
8
|
+
minute = data.is_a?(Hash) ? Array(data["minute"]) : []
|
|
9
|
+
|
|
10
|
+
if minute.empty?
|
|
11
|
+
Wokku::Output.status "no metrics collected yet"
|
|
12
|
+
next
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
last = minute.last
|
|
16
|
+
cpu = last["cpu_pct"]
|
|
17
|
+
mem = last["memory_mb"]
|
|
18
|
+
limit = last["memory_limit_mb"]
|
|
19
|
+
Wokku::Output.status "cpu: #{cpu}%"
|
|
20
|
+
Wokku::Output.status "mem: #{mem} MB#{limit ? " / #{limit} MB" : ''}"
|
|
21
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- App HTTP request monitor (req-rate, latency, error %) ---
|
|
4
|
+
|
|
5
|
+
register "monitor", "Show request totals and slow URLs (usage: wokku monitor APP)" do
|
|
6
|
+
id = ARGV.shift || abort("Usage: wokku monitor APP")
|
|
7
|
+
data = api(:get, "/apps/#{id}/monitor")
|
|
8
|
+
totals = data.is_a?(Hash) ? (data["totals"] || {}) : {}
|
|
9
|
+
|
|
10
|
+
if totals["req_count"].to_i.zero?
|
|
11
|
+
Wokku::Output.status "no requests in the last 24h"
|
|
12
|
+
next
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
Wokku::Output.status "req: #{totals['req_count']}"
|
|
16
|
+
Wokku::Output.status "rpm: #{totals['rpm']}"
|
|
17
|
+
Wokku::Output.status "p50: #{totals['p50_ms']}ms"
|
|
18
|
+
Wokku::Output.status "p95: #{totals['p95_ms']}ms"
|
|
19
|
+
Wokku::Output.status "error rate: #{((totals['error_rate'].to_f * 100).round(2))}%"
|
|
20
|
+
|
|
21
|
+
slow = Array(data["recent_slow"]).first(3)
|
|
22
|
+
if slow.any?
|
|
23
|
+
Wokku::Output.status ""
|
|
24
|
+
Wokku::Output.status "top slow:"
|
|
25
|
+
slow.each do |s|
|
|
26
|
+
Wokku::Output.status " #{s['method']} #{s['path']} #{s['response_time_ms']}ms (#{s['status']})"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- Notifications (team-scoped channels) ---
|
|
4
|
+
|
|
5
|
+
register "notifications", "List your notifications (usage: wokku notifications)" do
|
|
6
|
+
data = api(:get, "/notifications")
|
|
7
|
+
rows = data.is_a?(Array) ? data : []
|
|
8
|
+
if rows.empty?
|
|
9
|
+
Wokku::Output.status "no notifications"
|
|
10
|
+
next
|
|
11
|
+
end
|
|
12
|
+
rows.each do |n|
|
|
13
|
+
events = Array(n["events"]).join(",")
|
|
14
|
+
Wokku::Output.status "#{n['id']} #{n['channel']} #{events}"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
register "notifications:add",
|
|
19
|
+
"Add a notification (email only in v1). Usage: wokku notifications:add --team TEAM_ID --channel email --events EVENT1,EVENT2" do
|
|
20
|
+
team_id = nil
|
|
21
|
+
channel = nil
|
|
22
|
+
events = nil
|
|
23
|
+
while arg = ARGV.shift
|
|
24
|
+
case arg
|
|
25
|
+
when "--team" then team_id = ARGV.shift
|
|
26
|
+
when "--channel" then channel = ARGV.shift
|
|
27
|
+
when "--events" then events = ARGV.shift
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
abort "Missing --team TEAM_ID" if team_id.nil? || team_id.empty?
|
|
31
|
+
abort "Missing --channel email" if channel.nil? || channel.empty?
|
|
32
|
+
abort "v1 only supports --channel email (use the dashboard for slack/webhook/etc.)" unless channel == "email"
|
|
33
|
+
abort "Missing --events. Example: --events deploy_succeeded,deploy_failed" if events.nil? || events.empty?
|
|
34
|
+
|
|
35
|
+
data = api(:post, "/notifications", {
|
|
36
|
+
team_id: team_id,
|
|
37
|
+
channel: channel,
|
|
38
|
+
events: events.split(",").map(&:strip)
|
|
39
|
+
})
|
|
40
|
+
id = data.is_a?(Hash) ? data["id"] : nil
|
|
41
|
+
Wokku::Output.status "Notification added#{id ? " (id=#{id})" : ''}."
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
register "notifications:remove", "Remove a notification (usage: wokku notifications:remove NOTIFICATION_ID)" do
|
|
45
|
+
id = ARGV.shift || abort("Usage: wokku notifications:remove NOTIFICATION_ID")
|
|
46
|
+
data = api(:delete, "/notifications/#{id}")
|
|
47
|
+
message = data.is_a?(Hash) ? data["message"] : nil
|
|
48
|
+
Wokku::Output.status(message || "Notification removed.")
|
|
49
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- PR previews ---
|
|
4
|
+
|
|
5
|
+
register "previews", "List PR preview apps (usage: wokku previews APP)" do
|
|
6
|
+
id = ARGV.shift || abort("Usage: wokku previews APP")
|
|
7
|
+
data = api(:get, "/apps/#{id}/previews")
|
|
8
|
+
rows = data.is_a?(Array) ? data : []
|
|
9
|
+
if rows.empty?
|
|
10
|
+
Wokku::Output.status "no preview apps"
|
|
11
|
+
next
|
|
12
|
+
end
|
|
13
|
+
rows.each do |p|
|
|
14
|
+
line = "##{p['pr_number']} #{p['branch']} #{p['status']} #{p['name']}"
|
|
15
|
+
line += " #{p['url']}" if p["url"]
|
|
16
|
+
Wokku::Output.status line
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
register "previews:destroy", "Queue cleanup of a PR preview (usage: wokku previews:destroy APP PR_NUMBER)" do
|
|
21
|
+
id = ARGV.shift || abort("Usage: wokku previews:destroy APP PR_NUMBER")
|
|
22
|
+
pr = ARGV.shift || abort("Missing PR_NUMBER")
|
|
23
|
+
data = api(:delete, "/apps/#{id}/previews/#{pr}")
|
|
24
|
+
message = data.is_a?(Hash) ? data["message"] : nil
|
|
25
|
+
Wokku::Output.status(message || "Preview destroy queued.")
|
|
26
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- Teams + team members ---
|
|
4
|
+
|
|
5
|
+
register "teams", "List the teams you have access to (usage: wokku teams)" do
|
|
6
|
+
data = api(:get, "/teams")
|
|
7
|
+
rows = data.is_a?(Array) ? data : []
|
|
8
|
+
if rows.empty?
|
|
9
|
+
Wokku::Output.status "no teams"
|
|
10
|
+
next
|
|
11
|
+
end
|
|
12
|
+
rows.each do |t|
|
|
13
|
+
Wokku::Output.status "#{t['id']} #{t['name']}"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
register "teams:create", "Create a team (usage: wokku teams:create NAME)" do
|
|
18
|
+
name = ARGV.shift || abort("Usage: wokku teams:create NAME")
|
|
19
|
+
data = api(:post, "/teams", { name: name })
|
|
20
|
+
team_id = data.is_a?(Hash) ? data["id"] : nil
|
|
21
|
+
Wokku::Output.status "Team created#{team_id ? " (id=#{team_id})" : ''}."
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
register "teams:members", "List members of a team (usage: wokku teams:members TEAM_ID)" do
|
|
25
|
+
team_id = ARGV.shift || abort("Usage: wokku teams:members TEAM_ID")
|
|
26
|
+
data = api(:get, "/teams/#{team_id}/members")
|
|
27
|
+
rows = data.is_a?(Array) ? data : []
|
|
28
|
+
if rows.empty?
|
|
29
|
+
Wokku::Output.status "no members"
|
|
30
|
+
next
|
|
31
|
+
end
|
|
32
|
+
rows.each do |m|
|
|
33
|
+
Wokku::Output.status "#{m['id']} #{m['email']} #{m['role']}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
register "teams:members:add", "Add a member to a team. Usage: wokku teams:members:add TEAM_ID --email EMAIL [--role member|admin]" do
|
|
38
|
+
team_id = ARGV.shift || abort("Usage: wokku teams:members:add TEAM_ID --email EMAIL [--role member|admin]")
|
|
39
|
+
email = nil
|
|
40
|
+
role = "member"
|
|
41
|
+
while arg = ARGV.shift
|
|
42
|
+
case arg
|
|
43
|
+
when "--email" then email = ARGV.shift
|
|
44
|
+
when "--role" then role = ARGV.shift
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
abort "Missing --email" if email.nil? || email.empty?
|
|
48
|
+
|
|
49
|
+
data = api(:post, "/teams/#{team_id}/members", { email: email, role: role })
|
|
50
|
+
membership_id = data.is_a?(Hash) ? data["id"] : nil
|
|
51
|
+
Wokku::Output.status "Added member#{membership_id ? " (id=#{membership_id})" : ''}."
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
register "teams:members:remove", "Remove a member from a team (usage: wokku teams:members:remove TEAM_ID MEMBERSHIP_ID)" do
|
|
55
|
+
team_id = ARGV.shift || abort("Usage: wokku teams:members:remove TEAM_ID MEMBERSHIP_ID")
|
|
56
|
+
membership_id = ARGV.shift || abort("Missing MEMBERSHIP_ID. Get it from `wokku teams:members TEAM_ID`.")
|
|
57
|
+
api(:delete, "/teams/#{team_id}/members/#{membership_id}")
|
|
58
|
+
Wokku::Output.status "Member removed."
|
|
59
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "open3"
|
|
5
|
+
require "rbconfig"
|
|
6
|
+
require "tempfile"
|
|
7
|
+
|
|
8
|
+
# --- wokku tunnel: share a local port at https://<sub>.wokku.dev ---
|
|
9
|
+
#
|
|
10
|
+
# Path B per project_wokku_tunnel_path_decision (memory): frp gateway.
|
|
11
|
+
# Wraps a downloaded `frpc` (one-time bootstrap, no platform-specific
|
|
12
|
+
# gem dependency). User runs `wokku tunnel 3000`; we POST to the API
|
|
13
|
+
# to provision a TunnelSession, write a per-process frpc.toml, exec
|
|
14
|
+
# frpc, and DELETE the session on exit.
|
|
15
|
+
|
|
16
|
+
FRPC_VERSION = "0.62.1"
|
|
17
|
+
|
|
18
|
+
def tunnel_frpc_path
|
|
19
|
+
File.join(Dir.home, ".wokku", "bin", "frpc")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def tunnel_frpc_platform
|
|
23
|
+
case RbConfig::CONFIG["host_os"]
|
|
24
|
+
when /darwin/ then "darwin_#{tunnel_frpc_arch}"
|
|
25
|
+
when /linux/ then "linux_#{tunnel_frpc_arch}"
|
|
26
|
+
when /mswin|mingw|cygwin/ then "windows_amd64"
|
|
27
|
+
else abort "Unsupported OS: #{RbConfig::CONFIG['host_os']}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def tunnel_frpc_arch
|
|
32
|
+
case RbConfig::CONFIG["host_cpu"]
|
|
33
|
+
when /arm64|aarch64/ then "arm64"
|
|
34
|
+
when /x86_64|amd64/ then "amd64"
|
|
35
|
+
else abort "Unsupported CPU: #{RbConfig::CONFIG['host_cpu']}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def tunnel_ensure_frpc!
|
|
40
|
+
return tunnel_frpc_path if File.executable?(tunnel_frpc_path)
|
|
41
|
+
|
|
42
|
+
warn "→ fetching tunnel helper (frpc #{FRPC_VERSION}, ~10 MB, one time)…"
|
|
43
|
+
platform = tunnel_frpc_platform
|
|
44
|
+
archive = "frp_#{FRPC_VERSION}_#{platform}.tar.gz"
|
|
45
|
+
url = "https://github.com/fatedier/frp/releases/download/v#{FRPC_VERSION}/#{archive}"
|
|
46
|
+
ext = platform.start_with?("windows") ? ".zip" : ".tar.gz"
|
|
47
|
+
archive = archive.sub(/\.tar\.gz\z/, ext)
|
|
48
|
+
url = url.sub(/\.tar\.gz\z/, ext)
|
|
49
|
+
|
|
50
|
+
Dir.mktmpdir do |tmp|
|
|
51
|
+
archive_path = File.join(tmp, archive)
|
|
52
|
+
unless system("curl", "-fL", url, "-o", archive_path)
|
|
53
|
+
abort "Failed to download frpc from #{url}"
|
|
54
|
+
end
|
|
55
|
+
if ext == ".zip"
|
|
56
|
+
system("unzip", "-q", archive_path, "-d", tmp) || abort("unzip failed")
|
|
57
|
+
else
|
|
58
|
+
system("tar", "-C", tmp, "-xzf", archive_path) || abort("tar failed")
|
|
59
|
+
end
|
|
60
|
+
frpc_dir = Dir[File.join(tmp, "frp_#{FRPC_VERSION}_*")].first || abort("frpc not found in archive")
|
|
61
|
+
src = File.join(frpc_dir, platform.start_with?("windows") ? "frpc.exe" : "frpc")
|
|
62
|
+
FileUtils.mkdir_p(File.dirname(tunnel_frpc_path))
|
|
63
|
+
FileUtils.cp(src, tunnel_frpc_path)
|
|
64
|
+
File.chmod(0o755, tunnel_frpc_path)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
tunnel_frpc_path
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
register "tunnel",
|
|
71
|
+
"Share a local port at https://<sub>.wokku.dev. Usage: wokku tunnel PORT [--subdomain S] [--app APP]" do
|
|
72
|
+
args = ARGV.dup
|
|
73
|
+
ARGV.clear
|
|
74
|
+
|
|
75
|
+
local_port = args.shift&.to_i
|
|
76
|
+
abort "Usage: wokku tunnel PORT [--subdomain S] [--app APP]" unless local_port&.positive?
|
|
77
|
+
|
|
78
|
+
subdomain = nil
|
|
79
|
+
app_id = nil
|
|
80
|
+
while (arg = args.shift)
|
|
81
|
+
case arg
|
|
82
|
+
when "--subdomain" then subdomain = args.shift
|
|
83
|
+
when "--app" then app_id = args.shift
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
payload = {}
|
|
88
|
+
payload[:subdomain] = subdomain if subdomain
|
|
89
|
+
payload[:app_record_id] = app_id if app_id
|
|
90
|
+
|
|
91
|
+
session = api(:post, "/tunnels", payload)
|
|
92
|
+
if session.is_a?(Hash) && session["error"]
|
|
93
|
+
abort "Failed to create tunnel: #{session['error']}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
tunnel_id = session["tunnel_id"]
|
|
97
|
+
token = session["token"]
|
|
98
|
+
frps_host = session["frps_host"]
|
|
99
|
+
frps_port = session["frps_port"]
|
|
100
|
+
public_url = session["public_url"]
|
|
101
|
+
sub = session["subdomain"]
|
|
102
|
+
|
|
103
|
+
warn ""
|
|
104
|
+
warn " → tunnel: #{public_url} → localhost:#{local_port}"
|
|
105
|
+
warn " → press Ctrl-C to close"
|
|
106
|
+
warn ""
|
|
107
|
+
|
|
108
|
+
frpc = tunnel_ensure_frpc!
|
|
109
|
+
|
|
110
|
+
cfg = Tempfile.new([ "frpc-", ".toml" ])
|
|
111
|
+
cfg.write(<<~TOML)
|
|
112
|
+
serverAddr = "#{frps_host}"
|
|
113
|
+
serverPort = #{frps_port}
|
|
114
|
+
|
|
115
|
+
[[proxies]]
|
|
116
|
+
name = "wokku-#{sub}"
|
|
117
|
+
type = "http"
|
|
118
|
+
localPort = #{local_port}
|
|
119
|
+
subdomain = "#{sub}"
|
|
120
|
+
|
|
121
|
+
[proxies.metadatas]
|
|
122
|
+
token = "#{token}"
|
|
123
|
+
TOML
|
|
124
|
+
cfg.close
|
|
125
|
+
|
|
126
|
+
cleanup = lambda do
|
|
127
|
+
File.unlink(cfg.path) rescue nil
|
|
128
|
+
api(:delete, "/tunnels/#{tunnel_id}") rescue nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
%w[INT TERM].each do |sig|
|
|
132
|
+
Signal.trap(sig) do
|
|
133
|
+
cleanup.call
|
|
134
|
+
exit 0
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
status = system(frpc, "-c", cfg.path)
|
|
139
|
+
cleanup.call
|
|
140
|
+
exit(status ? 0 : 1)
|
|
141
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# --- Core Web Vitals read (p75 per metric) ---
|
|
4
|
+
|
|
5
|
+
register "vitals", "Show p75 per Core Web Vital (usage: wokku vitals APP)" do
|
|
6
|
+
id = ARGV.shift || abort("Usage: wokku vitals APP")
|
|
7
|
+
data = api(:get, "/apps/#{id}/vitals")
|
|
8
|
+
latest = data.is_a?(Hash) ? (data["latest"] || {}) : {}
|
|
9
|
+
|
|
10
|
+
if latest.values.all?(&:nil?)
|
|
11
|
+
Wokku::Output.status "no vitals collected yet"
|
|
12
|
+
next
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
%w[CLS LCP INP FCP TTFB].each do |metric|
|
|
16
|
+
row = latest[metric]
|
|
17
|
+
if row.nil?
|
|
18
|
+
Wokku::Output.status "#{metric}: —"
|
|
19
|
+
else
|
|
20
|
+
Wokku::Output.status "#{metric}: #{row['p75']} (n=#{row['sample_count']})"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/wokku/version.rb
CHANGED
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.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Johannes Dwicahyo
|
|
@@ -43,16 +43,29 @@ files:
|
|
|
43
43
|
- lib/wokku/commands/activity.rb
|
|
44
44
|
- lib/wokku/commands/addons.rb
|
|
45
45
|
- lib/wokku/commands/apps.rb
|
|
46
|
+
- lib/wokku/commands/apps_lifecycle.rb
|
|
46
47
|
- lib/wokku/commands/auth.rb
|
|
48
|
+
- lib/wokku/commands/billing.rb
|
|
47
49
|
- lib/wokku/commands/buildpacks.rb
|
|
50
|
+
- lib/wokku/commands/cdn.rb
|
|
48
51
|
- lib/wokku/commands/certs.rb
|
|
49
52
|
- lib/wokku/commands/checks.rb
|
|
50
53
|
- lib/wokku/commands/config.rb
|
|
54
|
+
- lib/wokku/commands/cron.rb
|
|
51
55
|
- lib/wokku/commands/databases.rb
|
|
56
|
+
- lib/wokku/commands/db_monitor.rb
|
|
57
|
+
- lib/wokku/commands/dev.rb
|
|
52
58
|
- lib/wokku/commands/do.rb
|
|
53
59
|
- lib/wokku/commands/domains.rb
|
|
60
|
+
- lib/wokku/commands/github.rb
|
|
61
|
+
- lib/wokku/commands/https.rb
|
|
54
62
|
- lib/wokku/commands/logs.rb
|
|
63
|
+
- lib/wokku/commands/maintenance.rb
|
|
55
64
|
- lib/wokku/commands/mcp.rb
|
|
65
|
+
- lib/wokku/commands/metrics.rb
|
|
66
|
+
- lib/wokku/commands/monitor.rb
|
|
67
|
+
- lib/wokku/commands/notifications.rb
|
|
68
|
+
- lib/wokku/commands/previews.rb
|
|
56
69
|
- lib/wokku/commands/ps.rb
|
|
57
70
|
- lib/wokku/commands/releases.rb
|
|
58
71
|
- lib/wokku/commands/run.rb
|
|
@@ -60,8 +73,11 @@ files:
|
|
|
60
73
|
- lib/wokku/commands/shell.rb
|
|
61
74
|
- lib/wokku/commands/ssh_keys.rb
|
|
62
75
|
- lib/wokku/commands/storage.rb
|
|
76
|
+
- lib/wokku/commands/teams.rb
|
|
63
77
|
- lib/wokku/commands/templates.rb
|
|
78
|
+
- lib/wokku/commands/tunnel.rb
|
|
64
79
|
- lib/wokku/commands/version.rb
|
|
80
|
+
- lib/wokku/commands/vitals.rb
|
|
65
81
|
- lib/wokku/config.rb
|
|
66
82
|
- lib/wokku/helpers.rb
|
|
67
83
|
- lib/wokku/output.rb
|