jatai 1.0.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.
@@ -0,0 +1,66 @@
1
+ require "thor"
2
+ require "jatai/commands/base"
3
+
4
+ module Jatai
5
+ module Commands
6
+ class Env < Thor
7
+ namespace :env
8
+
9
+ desc "list", "Listar variáveis de ambiente"
10
+ def list
11
+ helper = EnvHelper.new
12
+ helper.require_auth!
13
+ slug = helper.app_slug
14
+
15
+ vars = helper.api.get("/api/v1/apps/#{slug}/env")
16
+
17
+ if vars.nil? || vars.empty?
18
+ helper.info("Nenhuma variável de ambiente configurada.")
19
+ return
20
+ end
21
+
22
+ vars.each do |key, value|
23
+ puts "#{helper.pastel.bold(key)}=#{value}"
24
+ end
25
+ rescue Api::Client::ApiError => e
26
+ EnvHelper.new.error("Erro: #{e.message}")
27
+ end
28
+
29
+ desc "set KEY=VALUE [KEY=VALUE...]", "Definir variáveis de ambiente"
30
+ def set(*pairs)
31
+ helper = EnvHelper.new
32
+ helper.require_auth!
33
+ slug = helper.app_slug
34
+
35
+ env_vars = {}
36
+ pairs.each do |pair|
37
+ key, value = pair.split("=", 2)
38
+ if key.nil? || key.empty? || value.nil?
39
+ helper.error("Formato inválido: #{pair}. Use KEY=VALUE.")
40
+ return
41
+ end
42
+ env_vars[key] = value
43
+ end
44
+
45
+ helper.api.put("/api/v1/apps/#{slug}/env", body: { env: env_vars })
46
+ helper.success("Variáveis de ambiente atualizadas.")
47
+ rescue Api::Client::ApiError => e
48
+ EnvHelper.new.error("Erro: #{e.message}")
49
+ end
50
+
51
+ desc "unset KEY [KEY...]", "Remover variáveis de ambiente"
52
+ def unset(*keys)
53
+ helper = EnvHelper.new
54
+ helper.require_auth!
55
+ slug = helper.app_slug
56
+
57
+ helper.api.delete("/api/v1/apps/#{slug}/env/#{keys.join(",")}")
58
+ helper.success("Variáveis removidas: #{keys.join(", ")}")
59
+ rescue Api::Client::ApiError => e
60
+ EnvHelper.new.error("Erro: #{e.message}")
61
+ end
62
+ end
63
+
64
+ class EnvHelper < Base; end
65
+ end
66
+ end
@@ -0,0 +1,83 @@
1
+ require "jatai/commands/base"
2
+ require "tty-prompt"
3
+ require "yaml"
4
+
5
+ module Jatai
6
+ module Commands
7
+ class Init < Base
8
+ PRESETS = {
9
+ "micro" => "Micro — 256MB RAM, 1 réplica (Grátis)",
10
+ "hobby" => "Hobby — 1GB RAM, réplicas ilimitadas (R$ 19/mês)",
11
+ "pro" => "Pro — 2GB RAM, réplicas ilimitadas (R$ 69/mês)"
12
+ }.freeze
13
+
14
+ REGIONS = { "br-se1" => "São Paulo", "br-ne1" => "Nordeste" }.freeze
15
+ STACKS = { "rails" => "Rails", "node" => "Node.js", "docker" => "Docker (custom Dockerfile)" }.freeze
16
+ DATABASE_TIERS = {
17
+ "mini" => "Mini — 512MB, 20 conexões (Grátis)",
18
+ "essencial" => "Essencial — 1GB, 50 conexões (R$ 19/mês)",
19
+ "padrao" => "Padrão — 5GB, 100 conexões (R$ 49/mês)"
20
+ }.freeze
21
+
22
+ def execute(options = {})
23
+ prompt = TTY::Prompt.new
24
+ config_path = File.join(Dir.pwd, Config::PROJECT_FILE)
25
+
26
+ if File.exist?(config_path)
27
+ return unless prompt.yes?("Arquivo .jatai.yml já existe. Sobrescrever?")
28
+ end
29
+
30
+ stack = detect_stack
31
+ if stack
32
+ info("Stack detectado: #{STACKS[stack]}")
33
+ unless prompt.yes?("Confirmar stack #{STACKS[stack]}?")
34
+ stack = prompt.select("Stack:", STACKS.map { |k, v| { name: v, value: k } })
35
+ end
36
+ else
37
+ info("Não foi possível detectar o stack automaticamente.")
38
+ stack = prompt.select("Stack:", STACKS.map { |k, v| { name: v, value: k } })
39
+ end
40
+
41
+ name = prompt.ask("Nome do app:") { |q| q.required true }
42
+ preset = prompt.select("Preset:", PRESETS.map { |k, v| { name: v, value: k } })
43
+ region = prompt.select("Região:", REGIONS.map { |k, v| { name: "#{v} (#{k})", value: k } })
44
+ db_tier = prompt.select("Banco de dados:", DATABASE_TIERS.map { |k, v| { name: v, value: k } })
45
+ branch = detect_git_branch || "main"
46
+
47
+ config = {
48
+ "app" => name,
49
+ "stack" => stack,
50
+ "preset" => preset,
51
+ "region" => region,
52
+ "branch" => branch,
53
+ "database_tier" => db_tier
54
+ }
55
+
56
+ File.write(config_path, YAML.dump(config))
57
+ success("Arquivo .jatai.yml criado com sucesso!")
58
+ info("Branch: #{branch}")
59
+ info("Execute `jatai up` para criar e fazer deploy do app.")
60
+ end
61
+
62
+ private
63
+
64
+ def detect_stack
65
+ if File.exist?(File.join(Dir.pwd, "Gemfile"))
66
+ gemfile = File.read(File.join(Dir.pwd, "Gemfile"))
67
+ return "rails" if gemfile.include?("rails")
68
+ end
69
+
70
+ return "node" if File.exist?(File.join(Dir.pwd, "package.json"))
71
+
72
+ return "docker" if File.exist?(File.join(Dir.pwd, "Dockerfile"))
73
+
74
+ nil
75
+ end
76
+
77
+ def detect_git_branch
78
+ branch = `git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
79
+ branch.empty? ? nil : branch
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,25 @@
1
+ require "jatai/commands/base"
2
+ require "tty-prompt"
3
+
4
+ module Jatai
5
+ module Commands
6
+ class Login < Base
7
+ def execute(options = {})
8
+ prompt = TTY::Prompt.new
9
+
10
+ email = prompt.ask("Email:")
11
+ password = prompt.mask("Senha:")
12
+
13
+ client = Api::Client.new(token: nil)
14
+ response = client.post("/users/tokens/sign_in", body: { email: email, password: password })
15
+
16
+ Config.save_token(response["token"])
17
+ success("Login realizado com sucesso!")
18
+ rescue Api::Client::AuthenticationError
19
+ error("Credenciais inválidas. Verifique email e senha.")
20
+ rescue Api::Client::ApiError => e
21
+ error("Erro ao fazer login: #{e.message}")
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,40 @@
1
+ require "jatai/commands/base"
2
+
3
+ module Jatai
4
+ module Commands
5
+ class Logs < Base
6
+ DEFAULT_TAIL = 100
7
+
8
+ def execute(options = {})
9
+ require_auth!
10
+ slug = app_slug
11
+ tail = options.fetch("tail", DEFAULT_TAIL).to_s
12
+
13
+ logs = api.get("/api/v1/apps/#{slug}/logs", params: { tail: tail })
14
+
15
+ if logs.nil? || logs.empty?
16
+ info("Nenhum log disponível.")
17
+ return
18
+ end
19
+
20
+ logs.each do |entry|
21
+ timestamp = entry["timestamp"]
22
+ source = entry["source"]
23
+ level = entry["level"]
24
+ message = entry["message"]
25
+
26
+ level_str = case level
27
+ when "error" then pastel.red(level)
28
+ when "warn" then pastel.yellow(level)
29
+ when "info" then pastel.green(level)
30
+ else pastel.dim(level || "log")
31
+ end
32
+
33
+ puts "#{pastel.dim(timestamp)} #{pastel.cyan(source)} [#{level_str}] #{message}"
34
+ end
35
+ rescue Api::Client::ApiError => e
36
+ error("Erro: #{e.message}")
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,33 @@
1
+ require "jatai/commands/base"
2
+
3
+ module Jatai
4
+ module Commands
5
+ class LogsExport < Base
6
+ def execute(options = {})
7
+ require_auth!
8
+ slug = app_slug
9
+
10
+ format = options.fetch("format", "json")
11
+ params = { export_format: format }
12
+ params[:since] = options["since"] if options["since"]
13
+ params[:until] = options["until"] if options["until"]
14
+ params[:level] = options["level"] if options["level"]
15
+
16
+ result = api.get("/api/v1/apps/#{slug}/logs/export", params: params)
17
+
18
+ case result
19
+ when Array
20
+ puts JSON.pretty_generate(result)
21
+ when Hash
22
+ puts JSON.pretty_generate(result)
23
+ when String
24
+ puts result
25
+ else
26
+ puts result
27
+ end
28
+ rescue Api::Client::ApiError => e
29
+ error("Erro: #{e.message}")
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,47 @@
1
+ require "jatai/commands/base"
2
+
3
+ module Jatai
4
+ module Commands
5
+ class LogsSearch < Base
6
+ def execute(options = {})
7
+ require_auth!
8
+ slug = app_slug
9
+ query = options["query"]
10
+
11
+ unless query
12
+ info("Uso: jatai logs:search <query> [--level error] [--since 1h]")
13
+ return
14
+ end
15
+
16
+ params = { q: query, limit: options.fetch("limit", "100") }
17
+ params[:level] = options["level"] if options["level"]
18
+ params[:source] = options["source"] if options["source"]
19
+ params[:since] = options["since"] if options["since"]
20
+
21
+ results = api.get("/api/v1/apps/#{slug}/logs/search", params: params)
22
+
23
+ if results.nil? || results.empty?
24
+ info("Nenhum resultado encontrado.")
25
+ return
26
+ end
27
+
28
+ results.each do |entry|
29
+ puts "#{pastel.dim(entry["timestamp"])} #{pastel.cyan(entry["source"])} [#{colorize_level(entry["level"])}] #{entry["message"]}"
30
+ end
31
+ rescue Api::Client::ApiError => e
32
+ error("Erro: #{e.message}")
33
+ end
34
+
35
+ private
36
+
37
+ def colorize_level(level)
38
+ case level
39
+ when "error" then pastel.red(level)
40
+ when "warn" then pastel.yellow(level)
41
+ when "info" then pastel.green(level)
42
+ else pastel.dim(level)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,79 @@
1
+ require "thor"
2
+ require "tty-prompt"
3
+ require "jatai/commands/base"
4
+
5
+ module Jatai
6
+ module Commands
7
+ class Redis < Thor
8
+ namespace :redis
9
+
10
+ MEMORY_TO_PLAN = {
11
+ 256 => "starter",
12
+ 512 => "pro",
13
+ 1024 => "business"
14
+ }.freeze
15
+
16
+ desc "status", "Ver status do Redis"
17
+ def status
18
+ slug = helper.app_slug
19
+
20
+ addon = helper.api.get("/api/v1/apps/#{slug}/addons/cache")
21
+
22
+ puts helper.pastel.bold("Redis de #{slug}")
23
+ puts " Status: #{addon["status"]}"
24
+ puts " Plano: #{addon["plan"]}"
25
+ puts " Memória: #{addon.dig("config", "memory_mb")}MB"
26
+ puts " Custo: R$ #{addon["monthly_cost_brl"]}/mês"
27
+ rescue Api::Client::ApiError => e
28
+ helper.error("Erro: #{e.message}")
29
+ end
30
+
31
+ desc "enable", "Habilitar Redis para o app"
32
+ option :memory, type: :numeric, default: 256, desc: "Memória em MB (256, 512, 1024)"
33
+ def enable
34
+ slug = helper.app_slug
35
+
36
+ plan = MEMORY_TO_PLAN[options[:memory]]
37
+ unless plan
38
+ helper.error("Memória inválida. Use: 256, 512 ou 1024")
39
+ return
40
+ end
41
+
42
+ helper.info("Habilitando Redis com #{options[:memory]}MB (plano #{plan})...")
43
+ result = helper.api.post("/api/v1/apps/#{slug}/addons", body: { addon: { slug: "cache", plan: plan } })
44
+ helper.success("Redis habilitado! Status: #{result["status"]}")
45
+ rescue Api::Client::ApiError => e
46
+ helper.error("Erro: #{e.message}")
47
+ end
48
+
49
+ desc "disable", "Remover Redis do app"
50
+ def disable
51
+ slug = helper.app_slug
52
+
53
+ prompt = TTY::Prompt.new
54
+ unless prompt.yes?("Remover Redis? Todos os dados em cache serão perdidos.")
55
+ helper.info("Operação cancelada.")
56
+ return
57
+ end
58
+
59
+ helper.info("Removendo Redis...")
60
+ helper.api.delete("/api/v1/apps/#{slug}/addons/cache")
61
+ helper.success("Redis removido com sucesso!")
62
+ rescue Api::Client::ApiError => e
63
+ helper.error("Erro: #{e.message}")
64
+ end
65
+
66
+ private
67
+
68
+ def helper
69
+ @helper ||= begin
70
+ h = RedisHelper.new
71
+ h.require_auth!
72
+ h
73
+ end
74
+ end
75
+ end
76
+
77
+ class RedisHelper < Base; end
78
+ end
79
+ end
@@ -0,0 +1,39 @@
1
+ require "jatai/commands/base"
2
+ require "tty-prompt"
3
+
4
+ module Jatai
5
+ module Commands
6
+ class Scale < Base
7
+ def execute(options = {})
8
+ require_auth!
9
+ slug = app_slug
10
+
11
+ body = {}
12
+ body[:replicas] = options["replicas"].to_i if options["replicas"]
13
+ body[:preset] = options["preset"] if options["preset"]
14
+
15
+ if body.empty?
16
+ error("Informe ao menos --replicas ou --preset. Ex: jatai scale --replicas 3")
17
+ return
18
+ end
19
+
20
+ estimate = api.post("/api/v1/apps/#{slug}/scale/estimate", body: body)
21
+ info("Preset: #{estimate["preset"]}")
22
+ info("Réplicas: #{estimate["replicas"]}") if estimate["replicas"]
23
+ info("CPU: #{estimate["cpu_limit"]} | RAM: #{estimate["memory_limit"]}") if estimate["cpu_limit"]
24
+ info("Custo estimado: R$ #{estimate["monthly_cost"]}/mês")
25
+
26
+ prompt = TTY::Prompt.new
27
+ unless prompt.yes?("Confirmar escalonamento?")
28
+ info("Escalonamento cancelado.")
29
+ return
30
+ end
31
+
32
+ api.put("/api/v1/apps/#{slug}/scale", body: body)
33
+ success("Escalonamento aplicado com sucesso!")
34
+ rescue Api::Client::ApiError => e
35
+ error("Erro: #{e.message}")
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,53 @@
1
+ require "jatai/commands/base"
2
+
3
+ module Jatai
4
+ module Commands
5
+ class Status < Base
6
+ def execute(options = {})
7
+ require_auth!
8
+ slug = app_slug
9
+
10
+ app = api.get("/api/v1/apps/#{slug}")
11
+
12
+ puts pastel.bold("App: #{app["name"]}")
13
+ puts " Slug: #{app["slug"]}"
14
+ puts " Stack: #{app["stack"]}"
15
+ puts " Status: #{colorize_status(app["status"])}"
16
+ puts " Região: #{app["region"]}"
17
+ puts " Preset: #{app["preset"]}"
18
+ puts " DB: #{app["database_tier"]}"
19
+ subdomain = app["subdomain"] || app["slug"]
20
+ puts " URL: https://#{subdomain}.usebrasa.com.br"
21
+
22
+ if app["last_deploy"]
23
+ deploy = app["last_deploy"]
24
+ puts ""
25
+ puts pastel.bold("Último deploy:")
26
+ puts " ID: #{deploy["id"]}"
27
+ puts " Status: #{colorize_status(deploy["status"])}"
28
+ puts " Commit: #{deploy["commit_sha"]}" if deploy["commit_sha"]
29
+ puts " Data: #{deploy["created_at"]}"
30
+ end
31
+ rescue Api::Client::ApiError => e
32
+ error("Erro: #{e.message}")
33
+ end
34
+
35
+ private
36
+
37
+ def colorize_status(status)
38
+ case status
39
+ when "active", "live"
40
+ pastel.green(status)
41
+ when "sleeping"
42
+ pastel.blue(status)
43
+ when "failed", "error"
44
+ pastel.red(status)
45
+ when "building", "deploying", "provisioning"
46
+ pastel.yellow(status)
47
+ else
48
+ status
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,220 @@
1
+ require "jatai/commands/base"
2
+ require "jatai/source_packer"
3
+ require "tty-spinner"
4
+
5
+ module Jatai
6
+ module Commands
7
+ class Up < Base
8
+ POLL_INTERVAL = 5
9
+ PROVISION_TIMEOUT = 1200
10
+ DEPLOY_TIMEOUT = 1800
11
+ PROVISION_MAX_POLLS = 240
12
+ DEPLOY_MAX_POLLS = 360
13
+
14
+ def execute(options = {})
15
+ require_auth!
16
+ config = project_config
17
+
18
+ @app = find_or_create_app(config)
19
+ slug = @app["slug"]
20
+
21
+ if %w[active sleeping].include?(@app["status"])
22
+ info("Iniciando deploy...")
23
+ else
24
+ info("Provisionando infraestrutura...")
25
+ trigger_provision(slug) if %w[pending error].include?(@app["status"])
26
+ unless wait_for_provisioning(@app["id"], slug)
27
+ return
28
+ end
29
+ success("Infraestrutura provisionada!")
30
+ info("Iniciando primeiro deploy...")
31
+ end
32
+
33
+ deploy = trigger_deploy(slug)
34
+ info(" Na fila...")
35
+
36
+ if wait_for_deploy_complete(slug, deploy["id"])
37
+ subdomain = @app["subdomain"] || slug
38
+ success("Deploy concluído! App disponível em https://#{subdomain}.usebrasa.com.br")
39
+ end
40
+ rescue Api::Client::ValidationError => e
41
+ error("Erro ao criar app: #{e.message}")
42
+ rescue Api::Client::ApiError => e
43
+ error("Erro: #{e.message}")
44
+ end
45
+
46
+ private
47
+
48
+ def find_or_create_app(config)
49
+ existing = api.get("/api/v1/apps/#{config[:app]}")
50
+ info("App #{existing["slug"]} encontrado (#{existing["status"]}).")
51
+ ensure_repo_url(existing["slug"])
52
+ existing
53
+ rescue Api::Client::NotFoundError
54
+ info("Criando app #{config[:app]}...")
55
+ create_app(config)
56
+ end
57
+
58
+ def trigger_provision(slug)
59
+ api.post("/api/v1/apps/#{slug}/provision")
60
+ rescue Api::Client::ApiError
61
+ end
62
+
63
+ def ensure_repo_url(slug)
64
+ repo = detect_git_remote
65
+ return unless repo
66
+ api.patch("/api/v1/apps/#{slug}", body: { app: { repo_url: repo } })
67
+ rescue Api::Client::ApiError
68
+ end
69
+
70
+ def create_app(config)
71
+ app_params = {
72
+ name: config[:app], stack: config[:stack], preset: config[:preset],
73
+ region: config[:region], repo_url: detect_git_remote, repo_branch: config[:branch]
74
+ }
75
+ app_params[:database_tier] = config[:database_tier] if config[:database_tier]
76
+ app_params[:release_command] = config.dig(:release, :command) if config.dig(:release, :command)
77
+ api.post("/api/v1/apps", body: { app: app_params })
78
+ end
79
+
80
+ def trigger_deploy(slug)
81
+ info("Empacotando código...")
82
+ archive_path = SourcePacker.pack
83
+ size_kb = File.size(archive_path) / 1024
84
+
85
+ spinner = TTY::Spinner.new("#{pastel.cyan(" Enviando #{size_kb}KB...")} :spinner", format: :dots)
86
+ spinner.auto_spin
87
+ result = api.upload("/api/v1/apps/#{slug}/deploys", file_path: archive_path,
88
+ params: { branch: project_config[:branch] || "main" })
89
+ spinner.success(pastel.green("enviado!"))
90
+ result
91
+ ensure
92
+ FileUtils.rm_f(archive_path) if archive_path
93
+ end
94
+
95
+ def detect_git_remote
96
+ remote = `git remote get-url origin 2>/dev/null`.strip
97
+ remote.empty? ? nil : remote
98
+ end
99
+
100
+ # ── WebSocket methods ──
101
+
102
+ def wait_for_provisioning(app_id, slug)
103
+ ws = connect_websocket
104
+ status = ws.wait_for("ProvisionStatusChannel",
105
+ params: { app_id: app_id },
106
+ until_status: %w[active error],
107
+ timeout: PROVISION_TIMEOUT)
108
+
109
+ if status == "active"
110
+ true
111
+ else
112
+ error("\nErro no provisionamento.")
113
+ false
114
+ end
115
+ rescue Jatai::Websocket::CableClient::ConnectionError => e
116
+ warn_fallback(e)
117
+ wait_for_provisioning_polling(slug)
118
+ ensure
119
+ ws&.disconnect
120
+ end
121
+
122
+ def wait_for_deploy_complete(slug, deploy_id)
123
+ ws = connect_websocket
124
+ started = Time.now
125
+
126
+ ws.on_log { |msg| print msg["line"] + "\n" if msg.is_a?(Hash) && msg["line"] }
127
+ ws.subscribe("DeployLogChannel", deploy_id: deploy_id)
128
+
129
+ status = ws.wait_for("DeployStatusChannel",
130
+ params: { deploy_id: deploy_id },
131
+ until_status: %w[live failed],
132
+ timeout: DEPLOY_TIMEOUT)
133
+
134
+ if status == "live"
135
+ true
136
+ else
137
+ error("\nDeploy falhou.")
138
+ false
139
+ end
140
+ rescue Jatai::Websocket::CableClient::ConnectionError => e
141
+ warn_fallback(e)
142
+ wait_for_deploy_polling(slug, deploy_id)
143
+ ensure
144
+ ws&.disconnect
145
+ end
146
+
147
+ def connect_websocket
148
+ require "jatai/websocket/cable_client"
149
+ Jatai::Websocket::CableClient.new.tap(&:connect!)
150
+ end
151
+
152
+ def warn_fallback(err)
153
+ $stderr.puts "\n [WebSocket indisponível: #{err.message}. Usando polling...]"
154
+ end
155
+
156
+ # ── Fallback polling ──
157
+
158
+ def wait_for_provisioning_polling(slug)
159
+ started = Time.now
160
+ last_status = nil
161
+ PROVISION_MAX_POLLS.times do |i|
162
+ app = api.get("/api/v1/apps/#{slug}")
163
+ return true if app["status"] == "active"
164
+ if app["status"] == "error"
165
+ error("\nErro no provisionamento.")
166
+ return false
167
+ end
168
+ if app["status"] != last_status
169
+ print "\n #{status_label(app["status"])}"
170
+ last_status = app["status"]
171
+ end
172
+ print "."
173
+ show_elapsed(started) if (i % 12).zero? && i > 0
174
+ sleep(POLL_INTERVAL)
175
+ rescue Faraday::Error, Errno::ECONNREFUSED, IO::TimeoutError
176
+ print "!"
177
+ sleep(POLL_INTERVAL)
178
+ end
179
+ error("\nTimeout (#{(Time.now - started).to_i}s). Verifique com: jatai status")
180
+ false
181
+ end
182
+
183
+ def wait_for_deploy_polling(slug, deploy_id)
184
+ started = Time.now
185
+ last_status = nil
186
+ DEPLOY_MAX_POLLS.times do |i|
187
+ deploy = api.get("/api/v1/apps/#{slug}/deploys/#{deploy_id}")
188
+ return true if deploy["status"] == "live"
189
+ if deploy["status"] == "failed"
190
+ error("\nDeploy falhou.")
191
+ return false
192
+ end
193
+ if deploy["status"] != last_status
194
+ print "\n #{status_label(deploy["status"])}"
195
+ last_status = deploy["status"]
196
+ end
197
+ print "."
198
+ show_elapsed(started) if (i % 12).zero? && i > 0
199
+ sleep(POLL_INTERVAL)
200
+ rescue Faraday::Error, Errno::ECONNREFUSED, IO::TimeoutError
201
+ print "!"
202
+ sleep(POLL_INTERVAL)
203
+ end
204
+ error("\nTimeout (#{(Time.now - started).to_i}s). Verifique com: jatai status")
205
+ false
206
+ end
207
+
208
+ def status_label(status)
209
+ { "pending" => "Aguardando...", "provisioning" => "Provisionando...",
210
+ "queued" => "Na fila...", "building" => "Construindo imagem...",
211
+ "deploying" => "Implantando..." }[status] || status
212
+ end
213
+
214
+ def show_elapsed(started)
215
+ elapsed = (Time.now - started).to_i
216
+ print " (#{elapsed / 60}m#{elapsed % 60}s)"
217
+ end
218
+ end
219
+ end
220
+ end