brasa 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +65 -0
- data/exe/brasa +4 -0
- data/lib/brasa/api/client.rb +83 -0
- data/lib/brasa/cli.rb +81 -0
- data/lib/brasa/commands/base.rb +47 -0
- data/lib/brasa/commands/database.rb +64 -0
- data/lib/brasa/commands/deploy.rb +48 -0
- data/lib/brasa/commands/destroy.rb +31 -0
- data/lib/brasa/commands/domains.rb +95 -0
- data/lib/brasa/commands/env.rb +66 -0
- data/lib/brasa/commands/init.rb +59 -0
- data/lib/brasa/commands/login.rb +25 -0
- data/lib/brasa/commands/logs.rb +32 -0
- data/lib/brasa/commands/scale.rb +36 -0
- data/lib/brasa/commands/status.rb +49 -0
- data/lib/brasa/commands/up.rb +77 -0
- data/lib/brasa/config.rb +45 -0
- data/lib/brasa/version.rb +3 -0
- data/lib/brasa.rb +7 -0
- metadata +174 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 80e53e0d5fb933d533d94ab5863759651936979cf39b41bee334e2558e3fc343
|
|
4
|
+
data.tar.gz: 3274b32a209a87202587d48a9acc8f25df7f05b6f59472805ebad6b0ed646971
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 0ee91bdc2edea4b69d84a9d788b525f5275c5b0483055e8235bc201fb9308751df3e7697dcd6c9d423612d243431ff651d8d243aaf72ef5ad1561e37570d190e
|
|
7
|
+
data.tar.gz: 55e7419b734c594a766e8e48ebc0ceba35a1ac83be0364b50f8d087b6a450b16fdfd4c461a0b8653d4d0174a25c2a69d17327ed959309905e8d29f8498a34b11
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Brasa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Brasa CLI
|
|
2
|
+
|
|
3
|
+
CLI para deploy de aplicações na nuvem brasileira (Magalu Cloud). Soberania de dados, faturamento em reais.
|
|
4
|
+
|
|
5
|
+
## Instalacao
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
gem install brasa
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Autenticar na plataforma
|
|
15
|
+
brasa login
|
|
16
|
+
|
|
17
|
+
# Inicializar projeto (cria .brasa.yml)
|
|
18
|
+
brasa init
|
|
19
|
+
|
|
20
|
+
# Criar app e fazer primeiro deploy
|
|
21
|
+
brasa up
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Comandos
|
|
25
|
+
|
|
26
|
+
| Comando | Descricao |
|
|
27
|
+
|---------|-----------|
|
|
28
|
+
| `brasa login` | Autenticar na plataforma |
|
|
29
|
+
| `brasa init` | Inicializar projeto no diretorio atual |
|
|
30
|
+
| `brasa up` | Criar app e fazer primeiro deploy |
|
|
31
|
+
| `brasa deploy` | Fazer deploy da aplicacao |
|
|
32
|
+
| `brasa status` | Ver status da aplicacao |
|
|
33
|
+
| `brasa logs` | Ver logs da aplicacao |
|
|
34
|
+
| `brasa env list` | Listar variaveis de ambiente |
|
|
35
|
+
| `brasa env set KEY=VALUE` | Definir variavel de ambiente |
|
|
36
|
+
| `brasa env unset KEY` | Remover variavel de ambiente |
|
|
37
|
+
| `brasa db info` | Informacoes do banco de dados |
|
|
38
|
+
| `brasa db backup` | Criar backup do banco |
|
|
39
|
+
| `brasa db restore BACKUP_ID` | Restaurar backup |
|
|
40
|
+
| `brasa domains list` | Listar dominios customizados |
|
|
41
|
+
| `brasa domains add HOSTNAME` | Adicionar dominio |
|
|
42
|
+
| `brasa domains remove HOSTNAME` | Remover dominio |
|
|
43
|
+
| `brasa domains verify HOSTNAME` | Verificar dominio |
|
|
44
|
+
| `brasa scale` | Escalar a aplicacao |
|
|
45
|
+
| `brasa destroy` | Destruir aplicacao e recursos |
|
|
46
|
+
| `brasa version` | Exibir versao da CLI |
|
|
47
|
+
|
|
48
|
+
## Configuracao
|
|
49
|
+
|
|
50
|
+
O comando `brasa init` cria um arquivo `.brasa.yml` na raiz do projeto:
|
|
51
|
+
|
|
52
|
+
```yaml
|
|
53
|
+
app: meu-app
|
|
54
|
+
stack: rails
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Variaveis de Ambiente
|
|
58
|
+
|
|
59
|
+
| Variavel | Descricao | Padrao |
|
|
60
|
+
|----------|-----------|--------|
|
|
61
|
+
| `BRASA_API_URL` | URL da API | `https://api.brasa.com.br` |
|
|
62
|
+
|
|
63
|
+
## Licenca
|
|
64
|
+
|
|
65
|
+
MIT License - veja [LICENSE.txt](LICENSE.txt).
|
data/exe/brasa
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
require "faraday"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module Brasa
|
|
5
|
+
module Api
|
|
6
|
+
class Client
|
|
7
|
+
class ApiError < StandardError
|
|
8
|
+
attr_reader :status, :body
|
|
9
|
+
|
|
10
|
+
def initialize(message, status: nil, body: nil)
|
|
11
|
+
@status = status
|
|
12
|
+
@body = body
|
|
13
|
+
super(message)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class AuthenticationError < ApiError; end
|
|
18
|
+
class NotFoundError < ApiError; end
|
|
19
|
+
class ValidationError < ApiError; end
|
|
20
|
+
|
|
21
|
+
def initialize(token: nil, api_url: nil)
|
|
22
|
+
@token = token || Config.token
|
|
23
|
+
@api_url = api_url || Config.api_url
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def get(path, params: {})
|
|
27
|
+
request(:get, path, params: params)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def post(path, body: {})
|
|
31
|
+
request(:post, path, body: body)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def put(path, body: {})
|
|
35
|
+
request(:put, path, body: body)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def patch(path, body: {})
|
|
39
|
+
request(:patch, path, body: body)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def delete(path)
|
|
43
|
+
request(:delete, path)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def request(method, path, body: nil, params: nil)
|
|
49
|
+
response = connection.public_send(method, path) do |req|
|
|
50
|
+
req.params = params if params && !params.empty?
|
|
51
|
+
req.body = body.to_json if body
|
|
52
|
+
end
|
|
53
|
+
handle_response(response)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def connection
|
|
57
|
+
@connection ||= Faraday.new(url: @api_url) do |conn|
|
|
58
|
+
conn.request :json
|
|
59
|
+
conn.response :json
|
|
60
|
+
conn.headers["Content-Type"] = "application/json"
|
|
61
|
+
conn.headers["Authorization"] = "Bearer #{@token}" if @token
|
|
62
|
+
conn.adapter Faraday.default_adapter
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def handle_response(response)
|
|
67
|
+
case response.status
|
|
68
|
+
when 200..299
|
|
69
|
+
response.body
|
|
70
|
+
when 401
|
|
71
|
+
raise AuthenticationError.new("Não autenticado. Execute `brasa login`.", status: 401, body: response.body)
|
|
72
|
+
when 404
|
|
73
|
+
raise NotFoundError.new("Recurso não encontrado.", status: 404, body: response.body)
|
|
74
|
+
when 422
|
|
75
|
+
msg = response.body&.dig("errors")&.join(", ") || "Dados inválidos"
|
|
76
|
+
raise ValidationError.new(msg, status: 422, body: response.body)
|
|
77
|
+
else
|
|
78
|
+
raise ApiError.new("Erro na API (#{response.status})", status: response.status, body: response.body)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
data/lib/brasa/cli.rb
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
require "thor"
|
|
2
|
+
require "brasa/commands/login"
|
|
3
|
+
require "brasa/commands/init"
|
|
4
|
+
require "brasa/commands/up"
|
|
5
|
+
require "brasa/commands/deploy"
|
|
6
|
+
require "brasa/commands/logs"
|
|
7
|
+
require "brasa/commands/env"
|
|
8
|
+
require "brasa/commands/database"
|
|
9
|
+
require "brasa/commands/scale"
|
|
10
|
+
require "brasa/commands/status"
|
|
11
|
+
require "brasa/commands/destroy"
|
|
12
|
+
require "brasa/commands/domains"
|
|
13
|
+
|
|
14
|
+
module Brasa
|
|
15
|
+
class CLI < Thor
|
|
16
|
+
def self.exit_on_failure?
|
|
17
|
+
true
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
desc "login", "Autenticar na plataforma Brasa"
|
|
21
|
+
def login
|
|
22
|
+
Commands::Login.new.execute
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
desc "init", "Inicializar projeto no diretório atual"
|
|
26
|
+
def init
|
|
27
|
+
Commands::Init.new.execute
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
desc "up", "Criar app e fazer primeiro deploy"
|
|
31
|
+
def up
|
|
32
|
+
Commands::Up.new.execute
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
desc "deploy", "Fazer deploy da aplicação"
|
|
36
|
+
option :branch, type: :string, desc: "Branch para deploy"
|
|
37
|
+
def deploy
|
|
38
|
+
Commands::Deploy.new.execute(options)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
desc "logs", "Ver logs da aplicação em tempo real"
|
|
42
|
+
option :tail, type: :numeric, default: 100, desc: "Número de linhas"
|
|
43
|
+
def logs
|
|
44
|
+
Commands::Logs.new.execute(options)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
desc "env SUBCOMMAND", "Gerenciar variáveis de ambiente"
|
|
48
|
+
subcommand "env", Commands::Env
|
|
49
|
+
|
|
50
|
+
desc "db SUBCOMMAND", "Gerenciar banco de dados"
|
|
51
|
+
subcommand "db", Commands::Database
|
|
52
|
+
|
|
53
|
+
desc "domains SUBCOMMAND", "Gerenciar domínios customizados"
|
|
54
|
+
subcommand "domains", Commands::Domains
|
|
55
|
+
|
|
56
|
+
desc "scale", "Escalar a aplicação"
|
|
57
|
+
option :web, type: :numeric, desc: "Número de instâncias web"
|
|
58
|
+
option :preset, type: :string, desc: "Preset (hobby, production, scale)"
|
|
59
|
+
def scale
|
|
60
|
+
Commands::Scale.new.execute(options)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
desc "status", "Ver status da aplicação"
|
|
64
|
+
def status
|
|
65
|
+
Commands::Status.new.execute
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
desc "destroy", "Destruir aplicação e recursos"
|
|
69
|
+
def destroy
|
|
70
|
+
Commands::Destroy.new.execute
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
desc "version", "Exibir versão da CLI"
|
|
74
|
+
def version
|
|
75
|
+
puts "brasa #{Brasa::VERSION}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
map "-v" => :version
|
|
79
|
+
map "--version" => :version
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
require "pastel"
|
|
3
|
+
|
|
4
|
+
module Brasa
|
|
5
|
+
module Commands
|
|
6
|
+
class Base
|
|
7
|
+
def pastel
|
|
8
|
+
@pastel ||= Pastel.new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def api
|
|
12
|
+
@api ||= Api::Client.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def project_config
|
|
16
|
+
@project_config ||= begin
|
|
17
|
+
config_path = File.join(Dir.pwd, Config::PROJECT_FILE)
|
|
18
|
+
unless File.exist?(config_path)
|
|
19
|
+
abort pastel.red("Arquivo .brasa.yml não encontrado. Execute `brasa init` primeiro.")
|
|
20
|
+
end
|
|
21
|
+
YAML.safe_load(File.read(config_path), symbolize_names: true)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def app_slug
|
|
26
|
+
project_config[:app]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def require_auth!
|
|
30
|
+
return if Config.token
|
|
31
|
+
abort pastel.red("Não autenticado. Execute `brasa login` primeiro.")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def success(msg)
|
|
35
|
+
puts pastel.green(msg)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def error(msg)
|
|
39
|
+
puts pastel.red(msg)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def info(msg)
|
|
43
|
+
puts pastel.cyan(msg)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
require "thor"
|
|
2
|
+
require "tty-prompt"
|
|
3
|
+
require "brasa/commands/base"
|
|
4
|
+
|
|
5
|
+
module Brasa
|
|
6
|
+
module Commands
|
|
7
|
+
class Database < Thor
|
|
8
|
+
namespace :db
|
|
9
|
+
|
|
10
|
+
desc "info", "Informações do banco de dados"
|
|
11
|
+
def info
|
|
12
|
+
helper = DatabaseHelper.new
|
|
13
|
+
helper.require_auth!
|
|
14
|
+
slug = helper.app_slug
|
|
15
|
+
|
|
16
|
+
db = helper.api.get("/api/v1/apps/#{slug}/database")
|
|
17
|
+
|
|
18
|
+
puts helper.pastel.bold("Banco de dados de #{slug}")
|
|
19
|
+
puts " Host: #{db["host"]}"
|
|
20
|
+
puts " Porta: #{db["port"]}"
|
|
21
|
+
puts " Nome: #{db["name"]}"
|
|
22
|
+
puts " Usuário: #{db["username"]}"
|
|
23
|
+
puts " Status: #{db["status"]}"
|
|
24
|
+
puts " Tamanho: #{db["size"]}" if db["size"]
|
|
25
|
+
rescue Api::Client::ApiError => e
|
|
26
|
+
DatabaseHelper.new.error("Erro: #{e.message}")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
desc "backup", "Criar backup do banco de dados"
|
|
30
|
+
def backup
|
|
31
|
+
helper = DatabaseHelper.new
|
|
32
|
+
helper.require_auth!
|
|
33
|
+
slug = helper.app_slug
|
|
34
|
+
|
|
35
|
+
helper.info("Criando backup do banco de dados...")
|
|
36
|
+
result = helper.api.post("/api/v1/apps/#{slug}/database/backups")
|
|
37
|
+
helper.success("Backup criado: #{result["filename"]}")
|
|
38
|
+
rescue Api::Client::ApiError => e
|
|
39
|
+
DatabaseHelper.new.error("Erro: #{e.message}")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
desc "restore BACKUP_ID", "Restaurar backup do banco de dados"
|
|
43
|
+
def restore(backup_id)
|
|
44
|
+
helper = DatabaseHelper.new
|
|
45
|
+
helper.require_auth!
|
|
46
|
+
slug = helper.app_slug
|
|
47
|
+
|
|
48
|
+
prompt = TTY::Prompt.new
|
|
49
|
+
unless prompt.yes?("Restaurar backup #{backup_id}? Isso substituirá os dados atuais.")
|
|
50
|
+
helper.info("Restauração cancelada.")
|
|
51
|
+
return
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
helper.info("Restaurando backup...")
|
|
55
|
+
helper.api.post("/api/v1/apps/#{slug}/database/backups/#{backup_id}/restore")
|
|
56
|
+
helper.success("Backup restaurado com sucesso!")
|
|
57
|
+
rescue Api::Client::ApiError => e
|
|
58
|
+
DatabaseHelper.new.error("Erro: #{e.message}")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
class DatabaseHelper < Base; end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require "brasa/commands/base"
|
|
2
|
+
|
|
3
|
+
module Brasa
|
|
4
|
+
module Commands
|
|
5
|
+
class Deploy < Base
|
|
6
|
+
POLL_INTERVAL = 3
|
|
7
|
+
MAX_POLLS = 120
|
|
8
|
+
|
|
9
|
+
def execute(options = {})
|
|
10
|
+
require_auth!
|
|
11
|
+
slug = app_slug
|
|
12
|
+
|
|
13
|
+
info("Iniciando deploy de #{slug}...")
|
|
14
|
+
|
|
15
|
+
body = {}
|
|
16
|
+
body[:branch] = options["branch"] if options["branch"]
|
|
17
|
+
|
|
18
|
+
deploy = api.post("/api/v1/apps/#{slug}/deploys", body: body.empty? ? {} : body)
|
|
19
|
+
info("Deploy ##{deploy["id"]} criado. Aguardando...")
|
|
20
|
+
|
|
21
|
+
wait_for_deploy(slug, deploy["id"])
|
|
22
|
+
rescue Api::Client::ApiError => e
|
|
23
|
+
error("Erro: #{e.message}")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def wait_for_deploy(slug, deploy_id)
|
|
29
|
+
MAX_POLLS.times do
|
|
30
|
+
deploy = api.get("/api/v1/apps/#{slug}/deploys/#{deploy_id}")
|
|
31
|
+
|
|
32
|
+
case deploy["status"]
|
|
33
|
+
when "live"
|
|
34
|
+
success("Deploy concluído com sucesso!")
|
|
35
|
+
return
|
|
36
|
+
when "failed"
|
|
37
|
+
error("Deploy falhou.")
|
|
38
|
+
return
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
print "."
|
|
42
|
+
sleep(POLL_INTERVAL)
|
|
43
|
+
end
|
|
44
|
+
error("Timeout aguardando deploy.")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require "brasa/commands/base"
|
|
2
|
+
require "tty-prompt"
|
|
3
|
+
|
|
4
|
+
module Brasa
|
|
5
|
+
module Commands
|
|
6
|
+
class Destroy < Base
|
|
7
|
+
def execute(options = {})
|
|
8
|
+
require_auth!
|
|
9
|
+
slug = app_slug
|
|
10
|
+
|
|
11
|
+
prompt = TTY::Prompt.new
|
|
12
|
+
unless prompt.yes?("Tem certeza que deseja destruir #{slug}? Esta ação é irreversível.")
|
|
13
|
+
info("Operação cancelada.")
|
|
14
|
+
return
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
confirmation = prompt.ask("Digite o nome do app para confirmar (#{slug}):")
|
|
18
|
+
unless confirmation == slug
|
|
19
|
+
error("Nome não confere. Operação cancelada.")
|
|
20
|
+
return
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
info("Destruindo #{slug}...")
|
|
24
|
+
api.delete("/api/v1/apps/#{slug}")
|
|
25
|
+
success("App #{slug} destruído com sucesso.")
|
|
26
|
+
rescue Api::Client::ApiError => e
|
|
27
|
+
error("Erro: #{e.message}")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
require "thor"
|
|
2
|
+
require "brasa/commands/base"
|
|
3
|
+
|
|
4
|
+
module Brasa
|
|
5
|
+
module Commands
|
|
6
|
+
class Domains < Thor
|
|
7
|
+
namespace :domains
|
|
8
|
+
|
|
9
|
+
desc "list", "Listar domínios customizados"
|
|
10
|
+
def list
|
|
11
|
+
helper = DomainsHelper.new
|
|
12
|
+
helper.require_auth!
|
|
13
|
+
slug = helper.app_slug
|
|
14
|
+
|
|
15
|
+
domains = helper.api.get("/api/v1/apps/#{slug}/domains")
|
|
16
|
+
|
|
17
|
+
if domains.nil? || domains.empty?
|
|
18
|
+
helper.info("Nenhum domínio configurado.")
|
|
19
|
+
return
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
domains.each do |domain|
|
|
23
|
+
status_icon = domain["status"] == "active" ? helper.pastel.green("●") : helper.pastel.yellow("○")
|
|
24
|
+
puts "#{status_icon} #{helper.pastel.bold(domain["hostname"])} (#{domain["status"]})"
|
|
25
|
+
puts " Token: #{domain["verification_token"]}" if domain["status"] == "pending"
|
|
26
|
+
end
|
|
27
|
+
rescue Api::Client::ApiError => e
|
|
28
|
+
DomainsHelper.new.error("Erro: #{e.message}")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
desc "add HOSTNAME", "Adicionar domínio customizado"
|
|
32
|
+
def add(hostname)
|
|
33
|
+
helper = DomainsHelper.new
|
|
34
|
+
helper.require_auth!
|
|
35
|
+
slug = helper.app_slug
|
|
36
|
+
|
|
37
|
+
result = helper.api.post("/api/v1/apps/#{slug}/domains", body: { domain: { hostname: hostname } })
|
|
38
|
+
helper.success("Domínio #{hostname} adicionado.")
|
|
39
|
+
helper.info("Token de verificação: #{result["verification_token"]}")
|
|
40
|
+
helper.info("Adicione um registro CNAME ou TXT apontando para #{slug}.brasa.com.br")
|
|
41
|
+
rescue Api::Client::ValidationError => e
|
|
42
|
+
DomainsHelper.new.error("Erro: #{e.message}")
|
|
43
|
+
rescue Api::Client::ApiError => e
|
|
44
|
+
DomainsHelper.new.error("Erro: #{e.message}")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
desc "remove HOSTNAME", "Remover domínio customizado"
|
|
48
|
+
def remove(hostname)
|
|
49
|
+
helper = DomainsHelper.new
|
|
50
|
+
helper.require_auth!
|
|
51
|
+
slug = helper.app_slug
|
|
52
|
+
|
|
53
|
+
domains = helper.api.get("/api/v1/apps/#{slug}/domains")
|
|
54
|
+
domain = domains&.find { |d| d["hostname"] == hostname }
|
|
55
|
+
|
|
56
|
+
unless domain
|
|
57
|
+
helper.error("Domínio #{hostname} não encontrado.")
|
|
58
|
+
return
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
helper.api.delete("/api/v1/apps/#{slug}/domains/#{domain["id"]}")
|
|
62
|
+
helper.success("Domínio #{hostname} removido.")
|
|
63
|
+
rescue Api::Client::ApiError => e
|
|
64
|
+
DomainsHelper.new.error("Erro: #{e.message}")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
desc "verify HOSTNAME", "Verificar domínio customizado"
|
|
68
|
+
def verify(hostname)
|
|
69
|
+
helper = DomainsHelper.new
|
|
70
|
+
helper.require_auth!
|
|
71
|
+
slug = helper.app_slug
|
|
72
|
+
|
|
73
|
+
domains = helper.api.get("/api/v1/apps/#{slug}/domains")
|
|
74
|
+
domain = domains&.find { |d| d["hostname"] == hostname }
|
|
75
|
+
|
|
76
|
+
unless domain
|
|
77
|
+
helper.error("Domínio #{hostname} não encontrado.")
|
|
78
|
+
return
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
result = helper.api.post("/api/v1/apps/#{slug}/domains/#{domain["id"]}/verify")
|
|
82
|
+
|
|
83
|
+
if result["status"] == "active"
|
|
84
|
+
helper.success("Domínio #{hostname} verificado com sucesso!")
|
|
85
|
+
else
|
|
86
|
+
helper.error("Verificação falhou. Certifique-se de que o registro DNS está configurado.")
|
|
87
|
+
end
|
|
88
|
+
rescue Api::Client::ApiError => e
|
|
89
|
+
DomainsHelper.new.error("Erro: #{e.message}")
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
class DomainsHelper < Base; end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
require "thor"
|
|
2
|
+
require "brasa/commands/base"
|
|
3
|
+
|
|
4
|
+
module Brasa
|
|
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,59 @@
|
|
|
1
|
+
require "brasa/commands/base"
|
|
2
|
+
require "tty-prompt"
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Brasa
|
|
6
|
+
module Commands
|
|
7
|
+
class Init < Base
|
|
8
|
+
PRESETS = %w[hobby production scale].freeze
|
|
9
|
+
REGIONS = { "br-se1" => "São Paulo", "br-ne1" => "Nordeste" }.freeze
|
|
10
|
+
|
|
11
|
+
def execute(options = {})
|
|
12
|
+
prompt = TTY::Prompt.new
|
|
13
|
+
config_path = File.join(Dir.pwd, Config::PROJECT_FILE)
|
|
14
|
+
|
|
15
|
+
if File.exist?(config_path)
|
|
16
|
+
return unless prompt.yes?("Arquivo .brasa.yml já existe. Sobrescrever?")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
stack = detect_stack
|
|
20
|
+
unless stack
|
|
21
|
+
error("Não foi possível detectar o stack do projeto. Certifique-se de ter um Gemfile (Rails) ou package.json (Node.js).")
|
|
22
|
+
return
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
info("Stack detectado: #{stack == "rails" ? "Rails" : "Node.js"}")
|
|
26
|
+
|
|
27
|
+
name = prompt.ask("Nome do app:")
|
|
28
|
+
preset = prompt.select("Preset:", PRESETS)
|
|
29
|
+
region = prompt.select("Região:", REGIONS.map { |k, v| { name: "#{v} (#{k})", value: k } })
|
|
30
|
+
|
|
31
|
+
config = {
|
|
32
|
+
"app" => name,
|
|
33
|
+
"stack" => stack,
|
|
34
|
+
"preset" => preset,
|
|
35
|
+
"region" => region,
|
|
36
|
+
"branch" => "main"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
File.write(config_path, YAML.dump(config))
|
|
40
|
+
success("Arquivo .brasa.yml criado com sucesso!")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def detect_stack
|
|
46
|
+
if File.exist?(File.join(Dir.pwd, "Gemfile"))
|
|
47
|
+
gemfile = File.read(File.join(Dir.pwd, "Gemfile"))
|
|
48
|
+
return "rails" if gemfile.include?("rails")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if File.exist?(File.join(Dir.pwd, "package.json"))
|
|
52
|
+
return "node"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require "brasa/commands/base"
|
|
2
|
+
require "tty-prompt"
|
|
3
|
+
|
|
4
|
+
module Brasa
|
|
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,32 @@
|
|
|
1
|
+
require "brasa/commands/base"
|
|
2
|
+
|
|
3
|
+
module Brasa
|
|
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
|
+
message = entry["message"]
|
|
24
|
+
|
|
25
|
+
puts "#{pastel.dim(timestamp)} #{pastel.cyan(source)} #{message}"
|
|
26
|
+
end
|
|
27
|
+
rescue Api::Client::ApiError => e
|
|
28
|
+
error("Erro: #{e.message}")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require "brasa/commands/base"
|
|
2
|
+
require "tty-prompt"
|
|
3
|
+
|
|
4
|
+
module Brasa
|
|
5
|
+
module Commands
|
|
6
|
+
class Scale < Base
|
|
7
|
+
def execute(options = {})
|
|
8
|
+
require_auth!
|
|
9
|
+
slug = app_slug
|
|
10
|
+
|
|
11
|
+
body = {}
|
|
12
|
+
body[:web] = options["web"].to_i if options["web"]
|
|
13
|
+
body[:preset] = options["preset"] if options["preset"]
|
|
14
|
+
|
|
15
|
+
if body.empty?
|
|
16
|
+
error("Informe ao menos --web ou --preset. Ex: brasa scale --web 3")
|
|
17
|
+
return
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
estimate = api.post("/api/v1/apps/#{slug}/scale/estimate", body: body)
|
|
21
|
+
info("Custo estimado: R$ #{estimate["monthly_cost"]}/mês")
|
|
22
|
+
|
|
23
|
+
prompt = TTY::Prompt.new
|
|
24
|
+
unless prompt.yes?("Confirmar escalonamento?")
|
|
25
|
+
info("Escalonamento cancelado.")
|
|
26
|
+
return
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
api.put("/api/v1/apps/#{slug}/scale", body: body)
|
|
30
|
+
success("Escalonamento aplicado com sucesso!")
|
|
31
|
+
rescue Api::Client::ApiError => e
|
|
32
|
+
error("Erro: #{e.message}")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require "brasa/commands/base"
|
|
2
|
+
|
|
3
|
+
module Brasa
|
|
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 " URL: https://#{app["slug"]}.brasa.com.br"
|
|
19
|
+
|
|
20
|
+
if app["last_deploy"]
|
|
21
|
+
deploy = app["last_deploy"]
|
|
22
|
+
puts ""
|
|
23
|
+
puts pastel.bold("Último deploy:")
|
|
24
|
+
puts " ID: #{deploy["id"]}"
|
|
25
|
+
puts " Status: #{colorize_status(deploy["status"])}"
|
|
26
|
+
puts " Commit: #{deploy["commit_sha"]}" if deploy["commit_sha"]
|
|
27
|
+
puts " Data: #{deploy["created_at"]}"
|
|
28
|
+
end
|
|
29
|
+
rescue Api::Client::ApiError => e
|
|
30
|
+
error("Erro: #{e.message}")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def colorize_status(status)
|
|
36
|
+
case status
|
|
37
|
+
when "active", "live"
|
|
38
|
+
pastel.green(status)
|
|
39
|
+
when "failed", "error"
|
|
40
|
+
pastel.red(status)
|
|
41
|
+
when "building", "deploying", "provisioning"
|
|
42
|
+
pastel.yellow(status)
|
|
43
|
+
else
|
|
44
|
+
status
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
require "brasa/commands/base"
|
|
2
|
+
|
|
3
|
+
module Brasa
|
|
4
|
+
module Commands
|
|
5
|
+
class Up < Base
|
|
6
|
+
POLL_INTERVAL = 3
|
|
7
|
+
MAX_POLLS = 60
|
|
8
|
+
|
|
9
|
+
def execute(options = {})
|
|
10
|
+
require_auth!
|
|
11
|
+
config = project_config
|
|
12
|
+
|
|
13
|
+
info("Criando app #{config[:app]}...")
|
|
14
|
+
|
|
15
|
+
app = create_app(config)
|
|
16
|
+
info("App #{app["slug"]} criado. Provisionando infraestrutura...")
|
|
17
|
+
|
|
18
|
+
wait_for_provisioning(app["slug"])
|
|
19
|
+
success("Infraestrutura provisionada!")
|
|
20
|
+
|
|
21
|
+
info("Iniciando primeiro deploy...")
|
|
22
|
+
deploy = trigger_deploy(app["slug"])
|
|
23
|
+
wait_for_deploy(app["slug"], deploy["id"])
|
|
24
|
+
|
|
25
|
+
success("Deploy concluído! App disponível em https://#{app["slug"]}.brasa.com.br")
|
|
26
|
+
rescue Api::Client::ValidationError => e
|
|
27
|
+
error("Erro ao criar app: #{e.message}")
|
|
28
|
+
rescue Api::Client::ApiError => e
|
|
29
|
+
error("Erro: #{e.message}")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def create_app(config)
|
|
35
|
+
api.post("/api/v1/apps", body: {
|
|
36
|
+
name: config[:app],
|
|
37
|
+
stack: config[:stack],
|
|
38
|
+
preset: config[:preset],
|
|
39
|
+
region: config[:region],
|
|
40
|
+
repo_branch: config[:branch]
|
|
41
|
+
})
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def trigger_deploy(slug)
|
|
45
|
+
api.post("/api/v1/apps/#{slug}/deploys")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def wait_for_provisioning(slug)
|
|
49
|
+
MAX_POLLS.times do
|
|
50
|
+
app = api.get("/api/v1/apps/#{slug}")
|
|
51
|
+
return if app["status"] == "active"
|
|
52
|
+
if app["status"] == "error"
|
|
53
|
+
error("Erro no provisionamento.")
|
|
54
|
+
return
|
|
55
|
+
end
|
|
56
|
+
print "."
|
|
57
|
+
sleep(POLL_INTERVAL)
|
|
58
|
+
end
|
|
59
|
+
error("Timeout aguardando provisionamento.")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def wait_for_deploy(slug, deploy_id)
|
|
63
|
+
MAX_POLLS.times do
|
|
64
|
+
deploy = api.get("/api/v1/apps/#{slug}/deploys/#{deploy_id}")
|
|
65
|
+
return if deploy["status"] == "live"
|
|
66
|
+
if deploy["status"] == "failed"
|
|
67
|
+
error("Deploy falhou.")
|
|
68
|
+
return
|
|
69
|
+
end
|
|
70
|
+
print "."
|
|
71
|
+
sleep(POLL_INTERVAL)
|
|
72
|
+
end
|
|
73
|
+
error("Timeout aguardando deploy.")
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
data/lib/brasa/config.rb
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
|
|
4
|
+
module Brasa
|
|
5
|
+
class Config
|
|
6
|
+
CONFIG_DIR = File.expand_path("~/.brasa").freeze
|
|
7
|
+
CREDENTIALS_FILE = File.join(CONFIG_DIR, "credentials.json").freeze
|
|
8
|
+
PROJECT_FILE = ".brasa.yml".freeze
|
|
9
|
+
|
|
10
|
+
DEFAULT_API_URL = "https://api.brasa.com.br".freeze
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def api_url
|
|
14
|
+
ENV.fetch("BRASA_API_URL", DEFAULT_API_URL)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def token
|
|
18
|
+
credentials["token"]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def save_token(token)
|
|
22
|
+
FileUtils.mkdir_p(CONFIG_DIR)
|
|
23
|
+
File.write(CREDENTIALS_FILE, JSON.generate({ token: token }))
|
|
24
|
+
File.chmod(0600, CREDENTIALS_FILE)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def clear_token
|
|
28
|
+
File.delete(CREDENTIALS_FILE) if File.exist?(CREDENTIALS_FILE)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def logged_in?
|
|
32
|
+
!token.nil? && !token.empty?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def credentials
|
|
38
|
+
return {} unless File.exist?(CREDENTIALS_FILE)
|
|
39
|
+
JSON.parse(File.read(CREDENTIALS_FILE))
|
|
40
|
+
rescue JSON::ParserError
|
|
41
|
+
{}
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/brasa.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: brasa
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Brasa
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: thor
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.3'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.3'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: faraday
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: tty-prompt
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0.23'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0.23'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: tty-spinner
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0.9'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0.9'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: tty-table
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0.12'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0.12'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: pastel
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0.8'
|
|
89
|
+
type: :runtime
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0.8'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: rspec
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '3.13'
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '3.13'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: webmock
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - "~>"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '3.24'
|
|
117
|
+
type: :development
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - "~>"
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '3.24'
|
|
124
|
+
description: Deploy apps Rails e Node.js na nuvem brasileira com um comando. Soberania
|
|
125
|
+
de dados, faturamento em reais.
|
|
126
|
+
email:
|
|
127
|
+
- contato@brasa.com.br
|
|
128
|
+
executables:
|
|
129
|
+
- brasa
|
|
130
|
+
extensions: []
|
|
131
|
+
extra_rdoc_files: []
|
|
132
|
+
files:
|
|
133
|
+
- LICENSE.txt
|
|
134
|
+
- README.md
|
|
135
|
+
- exe/brasa
|
|
136
|
+
- lib/brasa.rb
|
|
137
|
+
- lib/brasa/api/client.rb
|
|
138
|
+
- lib/brasa/cli.rb
|
|
139
|
+
- lib/brasa/commands/base.rb
|
|
140
|
+
- lib/brasa/commands/database.rb
|
|
141
|
+
- lib/brasa/commands/deploy.rb
|
|
142
|
+
- lib/brasa/commands/destroy.rb
|
|
143
|
+
- lib/brasa/commands/domains.rb
|
|
144
|
+
- lib/brasa/commands/env.rb
|
|
145
|
+
- lib/brasa/commands/init.rb
|
|
146
|
+
- lib/brasa/commands/login.rb
|
|
147
|
+
- lib/brasa/commands/logs.rb
|
|
148
|
+
- lib/brasa/commands/scale.rb
|
|
149
|
+
- lib/brasa/commands/status.rb
|
|
150
|
+
- lib/brasa/commands/up.rb
|
|
151
|
+
- lib/brasa/config.rb
|
|
152
|
+
- lib/brasa/version.rb
|
|
153
|
+
homepage: https://brasa.com.br
|
|
154
|
+
licenses:
|
|
155
|
+
- MIT
|
|
156
|
+
metadata: {}
|
|
157
|
+
rdoc_options: []
|
|
158
|
+
require_paths:
|
|
159
|
+
- lib
|
|
160
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
161
|
+
requirements:
|
|
162
|
+
- - ">="
|
|
163
|
+
- !ruby/object:Gem::Version
|
|
164
|
+
version: '3.1'
|
|
165
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
166
|
+
requirements:
|
|
167
|
+
- - ">="
|
|
168
|
+
- !ruby/object:Gem::Version
|
|
169
|
+
version: '0'
|
|
170
|
+
requirements: []
|
|
171
|
+
rubygems_version: 3.6.9
|
|
172
|
+
specification_version: 4
|
|
173
|
+
summary: CLI para deploy na Magalu Cloud
|
|
174
|
+
test_files: []
|