brasa 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/lib/brasa/commands/deploy.rb +62 -15
- data/lib/brasa/commands/up.rb +102 -30
- data/lib/brasa/version.rb +1 -1
- data/lib/brasa/websocket/cable_client.rb +123 -0
- metadata +16 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3618063ca02e720d10a6a270976219d508a078ff13917d3de2fc9f81d9ab8e73
|
|
4
|
+
data.tar.gz: 781b40094dfc2baef08fc091d266cb79acd984dfabed6d3f54b9f68e2616c6c7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: edbd4b233d795fd78006f054791743985f6a1513c90d37951313739d92a3e1c945cdf5254d7981941400575a23d26e891cb0c991554216419710001912214b22
|
|
7
|
+
data.tar.gz: c7db4110649d9378c0eb9d5351a15da213a716ca0037612c56632d00ea3cca2bb10e5188d43f1a709bb8dcec35cd2ff874d89f1b5bc0129e2371a08069a0cef0
|
|
@@ -5,7 +5,8 @@ module Brasa
|
|
|
5
5
|
module Commands
|
|
6
6
|
class Deploy < Base
|
|
7
7
|
POLL_INTERVAL = 5
|
|
8
|
-
|
|
8
|
+
DEPLOY_TIMEOUT = 1800
|
|
9
|
+
DEPLOY_MAX_POLLS = 360
|
|
9
10
|
|
|
10
11
|
def execute(options = {})
|
|
11
12
|
require_auth!
|
|
@@ -13,18 +14,16 @@ module Brasa
|
|
|
13
14
|
|
|
14
15
|
info("Empacotando código...")
|
|
15
16
|
archive_path = SourcePacker.pack
|
|
16
|
-
|
|
17
17
|
size_kb = File.size(archive_path) / 1024
|
|
18
18
|
info("Enviando #{size_kb}KB para o servidor...")
|
|
19
19
|
|
|
20
|
-
deploy = api.upload(
|
|
21
|
-
"/api/v1/apps/#{slug}/deploys",
|
|
20
|
+
deploy = api.upload("/api/v1/apps/#{slug}/deploys",
|
|
22
21
|
file_path: archive_path,
|
|
23
|
-
params: { branch: options["branch"] || project_config[:branch] || "main" }
|
|
24
|
-
)
|
|
25
|
-
info("
|
|
22
|
+
params: { branch: options["branch"] || project_config[:branch] || "main" })
|
|
23
|
+
info("Deploy ##{deploy["id"]} criado.")
|
|
24
|
+
info(" Na fila...")
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
wait_for_deploy_complete(slug, deploy["id"])
|
|
28
27
|
rescue Api::Client::ApiError => e
|
|
29
28
|
error("Erro: #{e.message}")
|
|
30
29
|
ensure
|
|
@@ -33,23 +32,71 @@ module Brasa
|
|
|
33
32
|
|
|
34
33
|
private
|
|
35
34
|
|
|
36
|
-
def
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
def wait_for_deploy_complete(slug, deploy_id)
|
|
36
|
+
ws = connect_websocket
|
|
37
|
+
started = Time.now
|
|
38
|
+
|
|
39
|
+
ws.on_log { |msg| print msg["line"] + "\n" if msg["line"] }
|
|
40
|
+
ws.subscribe("DeployLogChannel", deploy_id: deploy_id)
|
|
41
|
+
|
|
42
|
+
status = ws.wait_for("DeployStatusChannel",
|
|
43
|
+
params: { deploy_id: deploy_id },
|
|
44
|
+
until_status: %w[live failed],
|
|
45
|
+
timeout: DEPLOY_TIMEOUT)
|
|
46
|
+
|
|
47
|
+
elapsed = (Time.now - started).to_i
|
|
48
|
+
if status == "live"
|
|
49
|
+
success("\nDeploy concluído com sucesso! (#{elapsed}s)")
|
|
50
|
+
else
|
|
51
|
+
error("\nDeploy falhou.")
|
|
52
|
+
end
|
|
53
|
+
rescue Websocket::CableClient::ConnectionError => e
|
|
54
|
+
$stderr.puts "\n [WebSocket indisponível: #{e.message}. Usando polling...]"
|
|
55
|
+
wait_for_deploy_polling(slug, deploy_id)
|
|
56
|
+
ensure
|
|
57
|
+
ws&.disconnect
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def connect_websocket
|
|
61
|
+
require "brasa/websocket/cable_client"
|
|
62
|
+
Websocket::CableClient.new.tap(&:connect!)
|
|
63
|
+
end
|
|
39
64
|
|
|
65
|
+
def wait_for_deploy_polling(slug, deploy_id)
|
|
66
|
+
started = Time.now
|
|
67
|
+
last_status = nil
|
|
68
|
+
DEPLOY_MAX_POLLS.times do |i|
|
|
69
|
+
deploy = api.get("/api/v1/apps/#{slug}/deploys/#{deploy_id}")
|
|
40
70
|
case deploy["status"]
|
|
41
71
|
when "live"
|
|
42
|
-
success("
|
|
72
|
+
success("\nDeploy concluído com sucesso! (#{(Time.now - started).to_i}s)")
|
|
43
73
|
return
|
|
44
74
|
when "failed"
|
|
45
|
-
error("
|
|
75
|
+
error("\nDeploy falhou.")
|
|
46
76
|
return
|
|
47
77
|
end
|
|
48
|
-
|
|
78
|
+
if deploy["status"] != last_status
|
|
79
|
+
print "\n #{status_label(deploy["status"])}"
|
|
80
|
+
last_status = deploy["status"]
|
|
81
|
+
end
|
|
49
82
|
print "."
|
|
83
|
+
show_elapsed(started) if (i % 12).zero? && i > 0
|
|
84
|
+
sleep(POLL_INTERVAL)
|
|
85
|
+
rescue Faraday::Error, Errno::ECONNREFUSED, IO::TimeoutError
|
|
86
|
+
print "!"
|
|
50
87
|
sleep(POLL_INTERVAL)
|
|
51
88
|
end
|
|
52
|
-
error("
|
|
89
|
+
error("\nTimeout (#{(Time.now - started).to_i}s). Verifique com: brasa status")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def status_label(status)
|
|
93
|
+
{ "queued" => "Na fila...", "building" => "Construindo imagem...",
|
|
94
|
+
"deploying" => "Deployando..." }[status] || status
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def show_elapsed(started)
|
|
98
|
+
elapsed = (Time.now - started).to_i
|
|
99
|
+
print " (#{elapsed / 60}m#{elapsed % 60}s)"
|
|
53
100
|
end
|
|
54
101
|
end
|
|
55
102
|
end
|
data/lib/brasa/commands/up.rb
CHANGED
|
@@ -5,7 +5,10 @@ module Brasa
|
|
|
5
5
|
module Commands
|
|
6
6
|
class Up < Base
|
|
7
7
|
POLL_INTERVAL = 5
|
|
8
|
-
|
|
8
|
+
PROVISION_TIMEOUT = 1200
|
|
9
|
+
DEPLOY_TIMEOUT = 1800
|
|
10
|
+
PROVISION_MAX_POLLS = 240
|
|
11
|
+
DEPLOY_MAX_POLLS = 360
|
|
9
12
|
|
|
10
13
|
def execute(options = {})
|
|
11
14
|
require_auth!
|
|
@@ -18,7 +21,8 @@ module Brasa
|
|
|
18
21
|
info("Iniciando deploy...")
|
|
19
22
|
else
|
|
20
23
|
info("Provisionando infraestrutura...")
|
|
21
|
-
|
|
24
|
+
trigger_provision(slug) if %w[pending error].include?(@app["status"])
|
|
25
|
+
unless wait_for_provisioning(@app["id"], slug)
|
|
22
26
|
return
|
|
23
27
|
end
|
|
24
28
|
success("Infraestrutura provisionada!")
|
|
@@ -26,8 +30,9 @@ module Brasa
|
|
|
26
30
|
end
|
|
27
31
|
|
|
28
32
|
deploy = trigger_deploy(slug)
|
|
33
|
+
info(" Na fila...")
|
|
29
34
|
|
|
30
|
-
if
|
|
35
|
+
if wait_for_deploy_complete(slug, deploy["id"])
|
|
31
36
|
subdomain = @app["subdomain"] || slug
|
|
32
37
|
success("Deploy concluído! App disponível em https://#{subdomain}.usebrasa.com.br")
|
|
33
38
|
end
|
|
@@ -49,23 +54,22 @@ module Brasa
|
|
|
49
54
|
create_app(config)
|
|
50
55
|
end
|
|
51
56
|
|
|
57
|
+
def trigger_provision(slug)
|
|
58
|
+
api.post("/api/v1/apps/#{slug}/provision")
|
|
59
|
+
rescue Api::Client::ApiError
|
|
60
|
+
end
|
|
61
|
+
|
|
52
62
|
def ensure_repo_url(slug)
|
|
53
63
|
repo = detect_git_remote
|
|
54
64
|
return unless repo
|
|
55
|
-
|
|
56
65
|
api.patch("/api/v1/apps/#{slug}", body: { app: { repo_url: repo } })
|
|
57
66
|
rescue Api::Client::ApiError
|
|
58
|
-
# Não bloquear o fluxo se falhar ao atualizar repo
|
|
59
67
|
end
|
|
60
68
|
|
|
61
69
|
def create_app(config)
|
|
62
70
|
app_params = {
|
|
63
|
-
name: config[:app],
|
|
64
|
-
|
|
65
|
-
preset: config[:preset],
|
|
66
|
-
region: config[:region],
|
|
67
|
-
repo_url: detect_git_remote,
|
|
68
|
-
repo_branch: config[:branch]
|
|
71
|
+
name: config[:app], stack: config[:stack], preset: config[:preset],
|
|
72
|
+
region: config[:region], repo_url: detect_git_remote, repo_branch: config[:branch]
|
|
69
73
|
}
|
|
70
74
|
app_params[:database_engine] = config[:database_engine] if config[:database_engine]
|
|
71
75
|
api.post("/api/v1/apps", body: { app: app_params })
|
|
@@ -76,13 +80,8 @@ module Brasa
|
|
|
76
80
|
archive_path = SourcePacker.pack
|
|
77
81
|
size_kb = File.size(archive_path) / 1024
|
|
78
82
|
info("Enviando #{size_kb}KB para o servidor...")
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
"/api/v1/apps/#{slug}/deploys",
|
|
82
|
-
file_path: archive_path,
|
|
83
|
-
params: { branch: project_config[:branch] || "main" }
|
|
84
|
-
)
|
|
85
|
-
deploy
|
|
83
|
+
api.upload("/api/v1/apps/#{slug}/deploys", file_path: archive_path,
|
|
84
|
+
params: { branch: project_config[:branch] || "main" })
|
|
86
85
|
ensure
|
|
87
86
|
FileUtils.rm_f(archive_path) if archive_path
|
|
88
87
|
end
|
|
@@ -92,35 +91,108 @@ module Brasa
|
|
|
92
91
|
remote.empty? ? nil : remote
|
|
93
92
|
end
|
|
94
93
|
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
# ── WebSocket methods ──
|
|
95
|
+
|
|
96
|
+
def wait_for_provisioning(app_id, slug)
|
|
97
|
+
ws = connect_websocket
|
|
98
|
+
status = ws.wait_for("ProvisionStatusChannel",
|
|
99
|
+
params: { app_id: app_id },
|
|
100
|
+
until_status: %w[active error],
|
|
101
|
+
timeout: PROVISION_TIMEOUT)
|
|
102
|
+
|
|
103
|
+
status == "active" ? true : (error("\nErro no provisionamento."); false)
|
|
104
|
+
rescue Websocket::CableClient::ConnectionError => e
|
|
105
|
+
warn_fallback(e)
|
|
106
|
+
wait_for_provisioning_polling(slug)
|
|
107
|
+
ensure
|
|
108
|
+
ws&.disconnect
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def wait_for_deploy_complete(slug, deploy_id)
|
|
112
|
+
ws = connect_websocket
|
|
113
|
+
started = Time.now
|
|
114
|
+
|
|
115
|
+
ws.on_log { |msg| print msg["line"] + "\n" if msg["line"] }
|
|
116
|
+
ws.subscribe("DeployLogChannel", deploy_id: deploy_id)
|
|
117
|
+
|
|
118
|
+
status = ws.wait_for("DeployStatusChannel",
|
|
119
|
+
params: { deploy_id: deploy_id },
|
|
120
|
+
until_status: %w[live failed],
|
|
121
|
+
timeout: DEPLOY_TIMEOUT)
|
|
122
|
+
|
|
123
|
+
status == "live" ? true : (error("\nDeploy falhou."); false)
|
|
124
|
+
rescue Websocket::CableClient::ConnectionError => e
|
|
125
|
+
warn_fallback(e)
|
|
126
|
+
wait_for_deploy_polling(slug, deploy_id)
|
|
127
|
+
ensure
|
|
128
|
+
ws&.disconnect
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def connect_websocket
|
|
132
|
+
require "brasa/websocket/cable_client"
|
|
133
|
+
Websocket::CableClient.new.tap(&:connect!)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def warn_fallback(err)
|
|
137
|
+
$stderr.puts "\n [WebSocket indisponível: #{err.message}. Usando polling...]"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# ── Fallback polling ──
|
|
141
|
+
|
|
142
|
+
def wait_for_provisioning_polling(slug)
|
|
143
|
+
started = Time.now
|
|
144
|
+
last_status = nil
|
|
145
|
+
PROVISION_MAX_POLLS.times do |i|
|
|
97
146
|
app = api.get("/api/v1/apps/#{slug}")
|
|
98
147
|
return true if app["status"] == "active"
|
|
99
|
-
if app["status"] == "error"
|
|
100
|
-
|
|
101
|
-
|
|
148
|
+
return (error("\nErro no provisionamento."); false) if app["status"] == "error"
|
|
149
|
+
if app["status"] != last_status
|
|
150
|
+
print "\n #{status_label(app["status"])}"
|
|
151
|
+
last_status = app["status"]
|
|
102
152
|
end
|
|
103
153
|
print "."
|
|
154
|
+
show_elapsed(started) if (i % 12).zero? && i > 0
|
|
155
|
+
sleep(POLL_INTERVAL)
|
|
156
|
+
rescue Faraday::Error, Errno::ECONNREFUSED, IO::TimeoutError
|
|
157
|
+
print "!"
|
|
104
158
|
sleep(POLL_INTERVAL)
|
|
105
159
|
end
|
|
106
|
-
error("
|
|
160
|
+
error("\nTimeout (#{(Time.now - started).to_i}s). Verifique com: brasa status")
|
|
107
161
|
false
|
|
108
162
|
end
|
|
109
163
|
|
|
110
|
-
def
|
|
111
|
-
|
|
164
|
+
def wait_for_deploy_polling(slug, deploy_id)
|
|
165
|
+
started = Time.now
|
|
166
|
+
last_status = nil
|
|
167
|
+
DEPLOY_MAX_POLLS.times do |i|
|
|
112
168
|
deploy = api.get("/api/v1/apps/#{slug}/deploys/#{deploy_id}")
|
|
113
169
|
return true if deploy["status"] == "live"
|
|
114
|
-
if deploy["status"] == "failed"
|
|
115
|
-
|
|
116
|
-
|
|
170
|
+
return (error("\nDeploy falhou."); false) if deploy["status"] == "failed"
|
|
171
|
+
if deploy["status"] != last_status
|
|
172
|
+
print "\n #{status_label(deploy["status"])}"
|
|
173
|
+
last_status = deploy["status"]
|
|
117
174
|
end
|
|
118
175
|
print "."
|
|
176
|
+
show_elapsed(started) if (i % 12).zero? && i > 0
|
|
177
|
+
sleep(POLL_INTERVAL)
|
|
178
|
+
rescue Faraday::Error, Errno::ECONNREFUSED, IO::TimeoutError
|
|
179
|
+
print "!"
|
|
119
180
|
sleep(POLL_INTERVAL)
|
|
120
181
|
end
|
|
121
|
-
error("
|
|
182
|
+
error("\nTimeout (#{(Time.now - started).to_i}s). Verifique com: brasa status")
|
|
122
183
|
false
|
|
123
184
|
end
|
|
185
|
+
|
|
186
|
+
def status_label(status)
|
|
187
|
+
{ "pending" => "Aguardando...", "provisioning" => "Provisionando VM...",
|
|
188
|
+
"queued" => "Na fila...", "building" => "Construindo imagem...",
|
|
189
|
+
"deploying" => "Deployando..." }[status] || status
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def show_elapsed(started)
|
|
193
|
+
elapsed = (Time.now - started).to_i
|
|
194
|
+
print " (#{elapsed / 60}m#{elapsed % 60}s)"
|
|
195
|
+
end
|
|
124
196
|
end
|
|
125
197
|
end
|
|
126
198
|
end
|
data/lib/brasa/version.rb
CHANGED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
require "websocket-client-simple"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module Brasa
|
|
5
|
+
module Websocket
|
|
6
|
+
class CableClient
|
|
7
|
+
class ConnectionError < StandardError; end
|
|
8
|
+
|
|
9
|
+
RECONNECT_DELAYS = [1, 3, 10].freeze
|
|
10
|
+
|
|
11
|
+
def initialize(api_url: nil, token: nil)
|
|
12
|
+
@api_url = api_url || Brasa::Config.api_url
|
|
13
|
+
@token = token || Brasa::Config.token
|
|
14
|
+
@subscriptions = {}
|
|
15
|
+
@connected = false
|
|
16
|
+
@log_callback = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def connect!
|
|
20
|
+
raise ConnectionError, "No token. Run `brasa login`." unless @token
|
|
21
|
+
|
|
22
|
+
ws_url = @api_url.sub(%r{^https?://}, "wss://").chomp("/") + "/cable"
|
|
23
|
+
@ws = WebSocket::Client::Simple.connect(ws_url, headers: {
|
|
24
|
+
"Authorization" => "Bearer #{@token}",
|
|
25
|
+
"Origin" => @api_url
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
setup_handlers
|
|
29
|
+
wait_for_welcome
|
|
30
|
+
self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def subscribe(channel, params = {}, &callback)
|
|
34
|
+
identifier = { channel: channel }.merge(params).to_json
|
|
35
|
+
@subscriptions[identifier] = callback
|
|
36
|
+
send_command("subscribe", identifier)
|
|
37
|
+
identifier
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def wait_for(channel, params: {}, until_status: [], timeout: 1800)
|
|
41
|
+
result = nil
|
|
42
|
+
identifier = subscribe(channel, params) do |msg|
|
|
43
|
+
status = msg["status"]
|
|
44
|
+
result = status if until_status.include?(status)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
deadline = Time.now + timeout
|
|
48
|
+
loop do
|
|
49
|
+
return result if result
|
|
50
|
+
raise ConnectionError, "Timeout (#{timeout}s)" if Time.now > deadline
|
|
51
|
+
raise ConnectionError, "Connection lost" unless @connected
|
|
52
|
+
sleep 0.2
|
|
53
|
+
end
|
|
54
|
+
ensure
|
|
55
|
+
send_command("unsubscribe", identifier) if identifier && @connected
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def on_log(&block)
|
|
59
|
+
@log_callback = block
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def disconnect
|
|
63
|
+
@ws&.close
|
|
64
|
+
@connected = false
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def setup_handlers
|
|
70
|
+
client = self
|
|
71
|
+
|
|
72
|
+
@ws.on :message do |msg|
|
|
73
|
+
data = JSON.parse(msg.data) rescue next
|
|
74
|
+
client.send(:handle_message, data)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
@ws.on :close do |_|
|
|
78
|
+
client.instance_variable_set(:@connected, false)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
@ws.on :error do |_|
|
|
82
|
+
client.instance_variable_set(:@connected, false)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def handle_message(data)
|
|
87
|
+
case data["type"]
|
|
88
|
+
when "welcome"
|
|
89
|
+
@connected = true
|
|
90
|
+
when "ping"
|
|
91
|
+
# keepalive — noop
|
|
92
|
+
when "confirm_subscription"
|
|
93
|
+
# confirmed
|
|
94
|
+
when "reject_subscription"
|
|
95
|
+
raise ConnectionError, "Subscription rejected"
|
|
96
|
+
else
|
|
97
|
+
identifier = data["identifier"]
|
|
98
|
+
message = data["message"]
|
|
99
|
+
return unless message
|
|
100
|
+
|
|
101
|
+
callback = @subscriptions[identifier]
|
|
102
|
+
callback&.call(message)
|
|
103
|
+
|
|
104
|
+
@log_callback&.call(message) if message["line"]
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def wait_for_welcome
|
|
109
|
+
deadline = Time.now + 10
|
|
110
|
+
loop do
|
|
111
|
+
return if @connected
|
|
112
|
+
raise ConnectionError, "WebSocket connection timeout" if Time.now > deadline
|
|
113
|
+
sleep 0.1
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def send_command(command, identifier)
|
|
118
|
+
return unless @ws&.open?
|
|
119
|
+
@ws.send({ command: command, identifier: identifier }.to_json)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: brasa
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brasa
|
|
@@ -107,6 +107,20 @@ dependencies:
|
|
|
107
107
|
- - "~>"
|
|
108
108
|
- !ruby/object:Gem::Version
|
|
109
109
|
version: '0.8'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: websocket-client-simple
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - "~>"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0.8'
|
|
117
|
+
type: :runtime
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - "~>"
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '0.8'
|
|
110
124
|
- !ruby/object:Gem::Dependency
|
|
111
125
|
name: rspec
|
|
112
126
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -168,6 +182,7 @@ files:
|
|
|
168
182
|
- lib/brasa/config.rb
|
|
169
183
|
- lib/brasa/source_packer.rb
|
|
170
184
|
- lib/brasa/version.rb
|
|
185
|
+
- lib/brasa/websocket/cable_client.rb
|
|
171
186
|
homepage: https://usebrasa.com.br
|
|
172
187
|
licenses:
|
|
173
188
|
- MIT
|