@zenifra/cli 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.
- package/README.md +130 -0
- package/bin/zenifra.mjs +1139 -0
- package/examples/http-project.json +24 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Zenifra CLI
|
|
2
|
+
|
|
3
|
+
CLI oficial da Zenifra para autenticar, selecionar organizacao, listar projetos e disparar deployments.
|
|
4
|
+
|
|
5
|
+
## Uso local
|
|
6
|
+
|
|
7
|
+
Instale pelo npm:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @zenifra/cli
|
|
11
|
+
zenifra --help
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Para desenvolvimento local:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm link
|
|
18
|
+
zenifra --help
|
|
19
|
+
zenifra help project logs
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Ou sem link:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
node bin/zenifra.mjs --help
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Cada comando tem ajuda especifica com exemplos de uso e retorno:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
zenifra project logs --help
|
|
32
|
+
zenifra help project env add
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Comandos
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
zenifra auth login
|
|
39
|
+
zenifra auth login --code 123456
|
|
40
|
+
zenifra auth api-key --key znf_sua_chave
|
|
41
|
+
zenifra auth logout
|
|
42
|
+
zenifra orgs
|
|
43
|
+
zenifra org set
|
|
44
|
+
zenifra projects --type http
|
|
45
|
+
zenifra projects create --name <name> --plan free --payment-mode hourly --config @examples/http-project.json
|
|
46
|
+
zenifra project info --project <project-id>
|
|
47
|
+
zenifra project url --project <project-id>
|
|
48
|
+
zenifra project logs --project <project-id> --instance <instance-id>
|
|
49
|
+
zenifra project metrics --project <project-id> --instance <instance-id>
|
|
50
|
+
zenifra project network --project <project-id> --view summary
|
|
51
|
+
zenifra project image set --project <project-id> --image ghcr.io/zenifra/app:tag
|
|
52
|
+
zenifra project envs --project <project-id>
|
|
53
|
+
zenifra project env add --project <project-id> --name NODE_ENV --value production
|
|
54
|
+
zenifra project env update --project <project-id> --name NODE_ENV --value staging
|
|
55
|
+
zenifra project env remove --project <project-id> --name NODE_ENV
|
|
56
|
+
zenifra project instances --project <project-id>
|
|
57
|
+
zenifra project instances set --project <project-id> --count 3
|
|
58
|
+
zenifra builds --project <project-id>
|
|
59
|
+
zenifra deployments --project <project-id>
|
|
60
|
+
zenifra deploy --project <project-id> --branch main
|
|
61
|
+
zenifra deploy watch --project <project-id> --build <build-id>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Configuracao
|
|
65
|
+
|
|
66
|
+
- API padrao: `https://api.zenifra.com/v1`
|
|
67
|
+
- Override de API: `ZENIFRA_API_URL=https://api-stg.zenifra.com/v1`
|
|
68
|
+
- API key global: `ZENIFRA_API_KEY=znf_sua_chave`
|
|
69
|
+
- Sessao local: `~/.config/zenifra-cli/session.json`
|
|
70
|
+
- Override de sessao: `ZENIFRA_CONFIG_DIR=/path/custom`
|
|
71
|
+
|
|
72
|
+
Todos os comandos de listagem aceitam `--json`.
|
|
73
|
+
|
|
74
|
+
Valores de variaveis de ambiente sao mascarados por padrao, inclusive em `--json`.
|
|
75
|
+
Use `--show-values` apenas quando precisar inspecionar os valores completos.
|
|
76
|
+
|
|
77
|
+
## Automacao com API key
|
|
78
|
+
|
|
79
|
+
Crie uma API key global no painel da organizacao e use-a em jobs, pipelines e scripts:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
export ZENIFRA_API_KEY=znf_sua_chave
|
|
83
|
+
zenifra projects --type http
|
|
84
|
+
zenifra deploy --project <project-id> --branch main
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Tambem e possivel salvar a chave localmente:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
zenifra auth api-key --key znf_sua_chave
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
API keys globais ja sao vinculadas a uma organizacao, entao comandos como `projects`, `deploy`, `builds` e `deployments` nao precisam de `org set`. Comandos pessoais como `orgs` e `org set` continuam exigindo `zenifra auth login`.
|
|
94
|
+
|
|
95
|
+
Para reduzir impacto de vazamento, configure IPs permitidos na criacao da API key sempre que a automacao tiver origem fixa.
|
|
96
|
+
|
|
97
|
+
## Exemplo de projeto HTTP
|
|
98
|
+
|
|
99
|
+
Use `examples/http-project.json` como base para `zenifra projects create`.
|
|
100
|
+
|
|
101
|
+
## Publicacao no npm
|
|
102
|
+
|
|
103
|
+
Este pacote e publicado pelo GitHub Actions usando npm Trusted Publishing. O pacote precisa existir no npm antes de configurar Trusted Publishing.
|
|
104
|
+
|
|
105
|
+
### Primeiro publish
|
|
106
|
+
|
|
107
|
+
Como `@zenifra/cli` e um pacote scoped publico, faca o primeiro publish autenticado no npm:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npm login
|
|
111
|
+
npm publish --access public
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Releases seguintes
|
|
115
|
+
|
|
116
|
+
1. Em npmjs.com, configure o pacote `@zenifra/cli` com Trusted Publisher:
|
|
117
|
+
- Organization/user: `zenifra`
|
|
118
|
+
- Repository: `zenifra-cli`
|
|
119
|
+
- Workflow filename: `publish.yml`
|
|
120
|
+
- Environment: `npm`
|
|
121
|
+
- Allowed action: `npm publish`
|
|
122
|
+
2. Garanta que o repositorio GitHub esteja publico para gerar provenance automatica.
|
|
123
|
+
3. Crie uma release tag semver:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npm version patch
|
|
127
|
+
git push origin main --follow-tags
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Tags `v*` disparam `.github/workflows/publish.yml`, que roda `npm ci`, `npm run check`, `npm test`, `npm pack --dry-run` e `npm publish --access public`.
|
package/bin/zenifra.mjs
ADDED
|
@@ -0,0 +1,1139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { mkdir, readFile, rm, writeFile, chmod } from 'node:fs/promises';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import process from 'node:process';
|
|
8
|
+
import readline from 'node:readline/promises';
|
|
9
|
+
|
|
10
|
+
const DEFAULT_API_BASE_URL = 'https://api.zenifra.com/v1';
|
|
11
|
+
const SESSION_DIR = process.env.ZENIFRA_CONFIG_DIR || join(homedir(), '.config', 'zenifra-cli');
|
|
12
|
+
const SESSION_FILE = join(SESSION_DIR, 'session.json');
|
|
13
|
+
const FINAL_BUILD_STATUSES = new Set(['success', 'failed']);
|
|
14
|
+
|
|
15
|
+
class CliError extends Error {
|
|
16
|
+
constructor(message, exitCode = 1) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.exitCode = exitCode;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function usage() {
|
|
23
|
+
return `Zenifra CLI
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
zenifra help <command>
|
|
27
|
+
zenifra auth login [--api-base <url>] [--code <code>]
|
|
28
|
+
zenifra auth api-key --key <znf_key> [--api-base <url>]
|
|
29
|
+
zenifra auth logout
|
|
30
|
+
zenifra orgs [--json]
|
|
31
|
+
zenifra org set [--org <id>]
|
|
32
|
+
zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb>]
|
|
33
|
+
zenifra projects create --name <name> --plan <plan> --payment-mode <mode> --config <json|@file> [--description <text>] [--org <id>] [--json]
|
|
34
|
+
zenifra project info --project <id> [--json]
|
|
35
|
+
zenifra project url --project <id> [--json]
|
|
36
|
+
zenifra project logs --project <id> [--instance <id>] [--json]
|
|
37
|
+
zenifra project metrics --project <id> [--instance <id>] [--json]
|
|
38
|
+
zenifra project network --project <id> [--view <summary|status-codes|routes|user-agents|request-events|source-ips>] [--json]
|
|
39
|
+
zenifra project image set --project <id> --image <image> [--json]
|
|
40
|
+
zenifra project envs --project <id> [--json] [--show-values]
|
|
41
|
+
zenifra project env add --project <id> --name <name> --value <value> [--json]
|
|
42
|
+
zenifra project env update --project <id> --name <name> --value <value> [--json]
|
|
43
|
+
zenifra project env remove --project <id> --name <name> [--json]
|
|
44
|
+
zenifra project instances --project <id> [--json]
|
|
45
|
+
zenifra project instances set --project <id> --count <n> [--json]
|
|
46
|
+
zenifra builds --project <id> [--page <n>] [--limit <n>] [--branch <name>] [--status <status>] [--json]
|
|
47
|
+
zenifra deployments --project <id> [--page <n>] [--limit <n>] [--branch <name>] [--status <status>] [--json]
|
|
48
|
+
zenifra deploy --project <id> [--branch <name>] [--commit-sha <sha>] [--json]
|
|
49
|
+
zenifra deploy watch --project <id> --build <id> [--interval <seconds>] [--timeout <seconds>] [--json]
|
|
50
|
+
|
|
51
|
+
Environment:
|
|
52
|
+
ZENIFRA_API_URL Override API base URL.
|
|
53
|
+
ZENIFRA_CONFIG_DIR Override local session directory.
|
|
54
|
+
ZENIFRA_API_KEY Use a global organization API key for automation.
|
|
55
|
+
|
|
56
|
+
Run "zenifra help <command>" or "zenifra <command> --help" for command-specific usage, examples, and output.
|
|
57
|
+
`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const HELP_SPECS = [
|
|
61
|
+
{
|
|
62
|
+
command: 'auth login',
|
|
63
|
+
usage: 'zenifra auth login [--api-base <url>] [--code <code>]',
|
|
64
|
+
description: 'Autentica um usuario Zenifra e salva a sessao local.',
|
|
65
|
+
flags: [
|
|
66
|
+
'--api-base <url> Usa uma API diferente da producao.',
|
|
67
|
+
'--code <code> Informa o codigo de desafio sem prompt interativo.',
|
|
68
|
+
],
|
|
69
|
+
examples: ['zenifra auth login', 'zenifra auth login --code 123456'],
|
|
70
|
+
output: 'Login realizado com sucesso.',
|
|
71
|
+
notes: ['Exige email, senha e, quando habilitado, codigo de desafio.'],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
command: 'auth api-key',
|
|
75
|
+
usage: 'zenifra auth api-key --key <znf_key> [--api-base <url>]',
|
|
76
|
+
description: 'Salva uma API key organizacional para comandos de automacao.',
|
|
77
|
+
flags: ['--key <znf_key> API key organizacional.', '--api-base <url> Usa uma API diferente da producao.'],
|
|
78
|
+
examples: ['zenifra auth api-key --key znf_0123456789abcdef01234567_abcd...'],
|
|
79
|
+
output: 'API key salva com sucesso. Ela sera usada em comandos de automacao.',
|
|
80
|
+
notes: ['API keys ja carregam a organizacao e nao exigem "zenifra org set".'],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
command: 'auth logout',
|
|
84
|
+
usage: 'zenifra auth logout',
|
|
85
|
+
description: 'Remove a sessao local do CLI.',
|
|
86
|
+
examples: ['zenifra auth logout'],
|
|
87
|
+
output: 'Sessao removida.',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
command: 'orgs',
|
|
91
|
+
usage: 'zenifra orgs [--json]',
|
|
92
|
+
description: 'Lista organizacoes disponiveis para o usuario autenticado.',
|
|
93
|
+
flags: ['--json Imprime a resposta em JSON.'],
|
|
94
|
+
examples: ['zenifra orgs', 'zenifra orgs --json'],
|
|
95
|
+
output: 'ID Nome Role Status\n507f1f77bcf86cd799439011 Minha org owner active',
|
|
96
|
+
jsonOutput: '[{"_id":"507f1f77bcf86cd799439011","name":"Minha org","role":"owner","status":"active"}]',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
command: 'org set',
|
|
100
|
+
usage: 'zenifra org set [--org <id>]',
|
|
101
|
+
description: 'Seleciona a organizacao ativa para comandos com sessao de usuario.',
|
|
102
|
+
flags: ['--org <id> Define a organizacao sem prompt interativo.'],
|
|
103
|
+
examples: ['zenifra org set', 'zenifra org set --org 507f1f77bcf86cd799439011'],
|
|
104
|
+
output: 'Organizacao ativa: 507f1f77bcf86cd799439011',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
command: 'projects',
|
|
108
|
+
usage: 'zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb>]',
|
|
109
|
+
description: 'Lista projetos da organizacao ativa ou da API key.',
|
|
110
|
+
flags: ['--json Imprime a resposta em JSON.', '--org <id> Usa uma organizacao especifica com sessao de usuario.', '--type <type> Filtra por tipo de projeto.'],
|
|
111
|
+
examples: ['zenifra projects --type http', 'zenifra projects --json'],
|
|
112
|
+
output: 'ID Nome Status Plano Tipo\n507f1f77bcf86cd799439012 api-web running free http',
|
|
113
|
+
jsonOutput: '[{"id":"507f1f77bcf86cd799439012","name":"api-web","status":"running","type_project":"http"}]',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
command: 'projects create',
|
|
117
|
+
usage: 'zenifra projects create --name <name> --plan <plan> --payment-mode <mode> --config <json|@file> [--description <text>] [--org <id>] [--json]',
|
|
118
|
+
description: 'Cria um projeto usando um payload de configuracao JSON.',
|
|
119
|
+
flags: ['--name <name> Nome do projeto.', '--plan <plan> Plano do projeto.', '--payment-mode <mode> Modo de pagamento.', '--config <json|@file> JSON inline ou arquivo.', '--description <text> Descricao opcional.', '--json Imprime a resposta em JSON.'],
|
|
120
|
+
examples: ['zenifra projects create --name api-web --plan free --payment-mode hourly --config @examples/http-project.json'],
|
|
121
|
+
output: 'Projeto criado: 507f1f77bcf86cd799439012\nDominio: api-web.client.zenifra.com',
|
|
122
|
+
jsonOutput: '{"status":"success","data":{"id":"507f1f77bcf86cd799439012","name":"api-web"}}',
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
command: 'project info',
|
|
126
|
+
usage: 'zenifra project info --project <id> [--json]',
|
|
127
|
+
description: 'Mostra dados principais de um projeto.',
|
|
128
|
+
flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.'],
|
|
129
|
+
examples: ['zenifra project info --project 507f1f77bcf86cd799439012'],
|
|
130
|
+
output: 'Nome: api-web\nStatus: running\nURL: https://api-web.client.zenifra.com\nInstancias: 2',
|
|
131
|
+
jsonOutput: '{"name":"api-web","status":"running","domain":"api-web.client.zenifra.com","instances":2}',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
command: 'project url',
|
|
135
|
+
usage: 'zenifra project url --project <id> [--json]',
|
|
136
|
+
description: 'Imprime a URL publica principal do projeto.',
|
|
137
|
+
flags: ['--project <id> ID do projeto.', '--json Inclui dominio e custom domains em JSON.'],
|
|
138
|
+
examples: ['zenifra project url --project 507f1f77bcf86cd799439012'],
|
|
139
|
+
output: 'https://api-web.client.zenifra.com',
|
|
140
|
+
jsonOutput: '{"project_id":"507f1f77bcf86cd799439012","url":"https://api-web.client.zenifra.com","domain":"api-web.client.zenifra.com","custom_domains":[]}',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
command: 'project logs',
|
|
144
|
+
usage: 'zenifra project logs --project <id> [--instance <id>] [--json]',
|
|
145
|
+
description: 'Mostra snapshot dos logs de runtime do projeto.',
|
|
146
|
+
flags: ['--project <id> ID do projeto.', '--instance <id> Filtra uma instancia.', '--json Imprime a resposta em JSON.'],
|
|
147
|
+
examples: ['zenifra project logs --project 507f1f77bcf86cd799439012', 'zenifra project logs --project 507f1f77bcf86cd799439012 --instance web-1'],
|
|
148
|
+
output: 'server started\nGET /health 200',
|
|
149
|
+
jsonOutput: '"server started\\nGET /health 200"',
|
|
150
|
+
notes: ['Nao faz streaming continuo; retorna o snapshot disponibilizado pela API.'],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
command: 'project metrics',
|
|
154
|
+
usage: 'zenifra project metrics --project <id> [--instance <id>] [--json]',
|
|
155
|
+
description: 'Mostra CPU, memoria e dados basicos de rede do projeto.',
|
|
156
|
+
flags: ['--project <id> ID do projeto.', '--instance <id> Filtra uma instancia.', '--json Imprime a resposta em JSON.'],
|
|
157
|
+
examples: ['zenifra project metrics --project 507f1f77bcf86cd799439012 --instance web-1'],
|
|
158
|
+
output: 'Instancia Tipo CPU Memoria\nweb-1 application 0.2 64',
|
|
159
|
+
jsonOutput: '{"type":"application","instance":"web-1","cpu":0.2,"memory":64,"network":{"requests":120}}',
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
command: 'project network',
|
|
163
|
+
usage: 'zenifra project network --project <id> [--view <summary|status-codes|routes|user-agents|request-events|source-ips>] [--json]',
|
|
164
|
+
description: 'Mostra analytics de rede para projetos HTTP.',
|
|
165
|
+
flags: ['--project <id> ID do projeto.', '--view <view> Visao de rede. Padrao: summary.', '--json Imprime a resposta em JSON.'],
|
|
166
|
+
examples: ['zenifra project network --project 507f1f77bcf86cd799439012 --view summary'],
|
|
167
|
+
output: '{"requests":120,"bytes_received":4096,"bytes_sent":8192}',
|
|
168
|
+
jsonOutput: '{"requests":120,"bytes_received":4096,"bytes_sent":8192}',
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
command: 'project image set',
|
|
172
|
+
usage: 'zenifra project image set --project <id> --image <image> [--json]',
|
|
173
|
+
description: 'Atualiza a imagem de deploy do projeto.',
|
|
174
|
+
flags: ['--project <id> ID do projeto.', '--image <image> Imagem completa.', '--json Imprime a resposta em JSON.'],
|
|
175
|
+
examples: ['zenifra project image set --project 507f1f77bcf86cd799439012 --image ghcr.io/zenifra/app:1.2.3'],
|
|
176
|
+
output: 'Imagem atualizada com sucesso.',
|
|
177
|
+
jsonOutput: '{"status":"success","message":"updated with success"}',
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
command: 'project envs',
|
|
181
|
+
usage: 'zenifra project envs --project <id> [--json] [--show-values]',
|
|
182
|
+
description: 'Lista variaveis de ambiente do projeto.',
|
|
183
|
+
flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.', '--show-values Mostra valores completos. Use com cuidado.'],
|
|
184
|
+
examples: ['zenifra project envs --project 507f1f77bcf86cd799439012'],
|
|
185
|
+
output: 'Nome Valor\nNODE_ENV ********',
|
|
186
|
+
jsonOutput: '[{"name":"NODE_ENV","value":"********"}]',
|
|
187
|
+
notes: ['Valores sao mascarados por padrao, inclusive em JSON.'],
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
command: 'project env add',
|
|
191
|
+
usage: 'zenifra project env add --project <id> --name <name> --value <value> [--json]',
|
|
192
|
+
description: 'Adiciona uma variavel de ambiente ao projeto.',
|
|
193
|
+
flags: ['--project <id> ID do projeto.', '--name <name> Nome da variavel.', '--value <value> Valor da variavel.', '--json Imprime a resposta em JSON.'],
|
|
194
|
+
examples: ['zenifra project env add --project 507f1f77bcf86cd799439012 --name NODE_ENV --value production'],
|
|
195
|
+
output: 'Env NODE_ENV atualizada com sucesso.',
|
|
196
|
+
jsonOutput: '{"status":"success","message":"updated with success","envs":[{"name":"NODE_ENV","value":"********"}]}',
|
|
197
|
+
notes: ['Falha se a variavel ja existir. Use update para alterar.'],
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
command: 'project env update',
|
|
201
|
+
usage: 'zenifra project env update --project <id> --name <name> --value <value> [--json]',
|
|
202
|
+
description: 'Atualiza uma variavel de ambiente existente.',
|
|
203
|
+
flags: ['--project <id> ID do projeto.', '--name <name> Nome da variavel.', '--value <value> Novo valor.', '--json Imprime a resposta em JSON.'],
|
|
204
|
+
examples: ['zenifra project env update --project 507f1f77bcf86cd799439012 --name NODE_ENV --value staging'],
|
|
205
|
+
output: 'Env NODE_ENV atualizada com sucesso.',
|
|
206
|
+
jsonOutput: '{"status":"success","message":"updated with success","envs":[{"name":"NODE_ENV","value":"********"}]}',
|
|
207
|
+
notes: ['Falha se a variavel nao existir.'],
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
command: 'project env remove',
|
|
211
|
+
usage: 'zenifra project env remove --project <id> --name <name> [--json]',
|
|
212
|
+
description: 'Remove uma variavel de ambiente existente.',
|
|
213
|
+
flags: ['--project <id> ID do projeto.', '--name <name> Nome da variavel.', '--json Imprime a resposta em JSON.'],
|
|
214
|
+
examples: ['zenifra project env remove --project 507f1f77bcf86cd799439012 --name NODE_ENV'],
|
|
215
|
+
output: 'Env NODE_ENV removida com sucesso.',
|
|
216
|
+
jsonOutput: '{"status":"success","message":"updated with success","envs":[]}',
|
|
217
|
+
notes: ['Falha se a variavel nao existir.'],
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
command: 'project instances',
|
|
221
|
+
usage: 'zenifra project instances --project <id> [--json]',
|
|
222
|
+
description: 'Lista instancias/pods do projeto.',
|
|
223
|
+
flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.'],
|
|
224
|
+
examples: ['zenifra project instances --project 507f1f77bcf86cd799439012'],
|
|
225
|
+
output: 'Instancia\nweb-1',
|
|
226
|
+
jsonOutput: '[{"instance":"web-1"}]',
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
command: 'project instances set',
|
|
230
|
+
usage: 'zenifra project instances set --project <id> --count <n> [--json]',
|
|
231
|
+
description: 'Atualiza a quantidade de instancias do projeto.',
|
|
232
|
+
flags: ['--project <id> ID do projeto.', '--count <n> Quantidade desejada.', '--json Imprime a resposta em JSON.'],
|
|
233
|
+
examples: ['zenifra project instances set --project 507f1f77bcf86cd799439012 --count 3'],
|
|
234
|
+
output: 'Quantidade de instancias atualizada para 3.',
|
|
235
|
+
jsonOutput: '{"status":"success","message":"project instances changed with success"}',
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
command: 'builds',
|
|
239
|
+
usage: 'zenifra builds --project <id> [--page <n>] [--limit <n>] [--branch <name>] [--status <status>] [--json]',
|
|
240
|
+
description: 'Lista builds GitHub de um projeto.',
|
|
241
|
+
flags: ['--project <id> ID do projeto.', '--page <n> Pagina.', '--limit <n> Itens por pagina.', '--branch <name> Filtra branch.', '--status <status> Filtra status.', '--json Imprime a resposta em JSON.'],
|
|
242
|
+
examples: ['zenifra builds --project 507f1f77bcf86cd799439012 --status success'],
|
|
243
|
+
output: 'Build Status Branch Commit\nbuild_123 success main abc123',
|
|
244
|
+
jsonOutput: '[{"id":"build_123","status":"success","branch":"main","commit_sha":"abc123"}]',
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
command: 'deployments',
|
|
248
|
+
usage: 'zenifra deployments --project <id> [--page <n>] [--limit <n>] [--branch <name>] [--status <status>] [--json]',
|
|
249
|
+
description: 'Alias para listar builds/deployments GitHub de um projeto.',
|
|
250
|
+
flags: ['--project <id> ID do projeto.', '--page <n> Pagina.', '--limit <n> Itens por pagina.', '--branch <name> Filtra branch.', '--status <status> Filtra status.', '--json Imprime a resposta em JSON.'],
|
|
251
|
+
examples: ['zenifra deployments --project 507f1f77bcf86cd799439012'],
|
|
252
|
+
output: 'Build Status Branch Commit\nbuild_123 success main abc123',
|
|
253
|
+
jsonOutput: '[{"id":"build_123","status":"success","branch":"main","commit_sha":"abc123"}]',
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
command: 'deploy',
|
|
257
|
+
usage: 'zenifra deploy --project <id> [--branch <name>] [--commit-sha <sha>] [--json]',
|
|
258
|
+
description: 'Dispara um deploy GitHub para o projeto.',
|
|
259
|
+
flags: ['--project <id> ID do projeto.', '--branch <name> Branch a publicar.', '--commit-sha <sha> Commit especifico.', '--json Imprime a resposta em JSON.'],
|
|
260
|
+
examples: ['zenifra deploy --project 507f1f77bcf86cd799439012 --branch main'],
|
|
261
|
+
output: 'Deploy iniciado: build_123',
|
|
262
|
+
jsonOutput: '{"status":"success","data":{"build_id":"build_123"}}',
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
command: 'deploy watch',
|
|
266
|
+
usage: 'zenifra deploy watch --project <id> --build <id> [--interval <seconds>] [--timeout <seconds>] [--json]',
|
|
267
|
+
description: 'Acompanha um build ate status terminal.',
|
|
268
|
+
flags: ['--project <id> ID do projeto.', '--build <id> ID do build.', '--interval <seconds> Intervalo de polling. Padrao: 5.', '--timeout <seconds> Timeout total. Padrao: 900.', '--json Imprime cada resposta em JSON.'],
|
|
269
|
+
examples: ['zenifra deploy watch --project 507f1f77bcf86cd799439012 --build build_123'],
|
|
270
|
+
output: '[2026-05-27T12:00:00.000Z] build build_123: building\n[2026-05-27T12:00:05.000Z] build build_123: success',
|
|
271
|
+
jsonOutput: '{"id":"build_123","status":"success"}',
|
|
272
|
+
},
|
|
273
|
+
];
|
|
274
|
+
|
|
275
|
+
const HELP_BY_COMMAND = new Map(HELP_SPECS.map((spec) => [spec.command, spec]));
|
|
276
|
+
|
|
277
|
+
function formatHelp(spec) {
|
|
278
|
+
const sections = [
|
|
279
|
+
`Zenifra CLI - ${spec.command}`,
|
|
280
|
+
'',
|
|
281
|
+
'Usage:',
|
|
282
|
+
` ${spec.usage}`,
|
|
283
|
+
'',
|
|
284
|
+
'Description:',
|
|
285
|
+
` ${spec.description}`,
|
|
286
|
+
];
|
|
287
|
+
|
|
288
|
+
if (spec.flags?.length) {
|
|
289
|
+
sections.push('', 'Flags:', ...spec.flags.map((flag) => ` ${flag}`));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
sections.push('', 'Examples:', ...spec.examples.map((example) => ` ${example}`));
|
|
293
|
+
sections.push('', 'Example output:', spec.output.split('\n').map((line) => ` ${line}`).join('\n'));
|
|
294
|
+
|
|
295
|
+
if (spec.jsonOutput) {
|
|
296
|
+
sections.push('', 'Example JSON output:', ` ${spec.jsonOutput}`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (spec.notes?.length) {
|
|
300
|
+
sections.push('', 'Notes:', ...spec.notes.map((note) => ` ${note}`));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return `${sections.join('\n')}\n`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function helpKeyFromPositionals(positionals) {
|
|
307
|
+
return positionals.filter((value) => value !== 'help').join(' ').trim();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function commandHelp(positionals) {
|
|
311
|
+
const key = helpKeyFromPositionals(positionals);
|
|
312
|
+
if (!key) return usage();
|
|
313
|
+
|
|
314
|
+
const spec = HELP_BY_COMMAND.get(key);
|
|
315
|
+
if (!spec) {
|
|
316
|
+
throw new CliError(`Ajuda nao encontrada para: ${key}. Use "zenifra help <command>".`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return formatHelp(spec);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function parseArgs(argv) {
|
|
323
|
+
const positional = [];
|
|
324
|
+
const flags = {};
|
|
325
|
+
|
|
326
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
327
|
+
const arg = argv[index];
|
|
328
|
+
if (!arg.startsWith('--')) {
|
|
329
|
+
positional.push(arg);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const [rawKey, inlineValue] = arg.slice(2).split('=', 2);
|
|
334
|
+
const key = rawKey.replace(/-([a-z])/g, (_, value) => value.toUpperCase());
|
|
335
|
+
|
|
336
|
+
if (inlineValue !== undefined) {
|
|
337
|
+
flags[key] = inlineValue;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const next = argv[index + 1];
|
|
342
|
+
if (!next || next.startsWith('--')) {
|
|
343
|
+
flags[key] = true;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
flags[key] = next;
|
|
348
|
+
index += 1;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return { positional, flags };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function readSession() {
|
|
355
|
+
if (!existsSync(SESSION_FILE)) {
|
|
356
|
+
return {};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
return JSON.parse(await readFile(SESSION_FILE, 'utf8'));
|
|
361
|
+
} catch {
|
|
362
|
+
throw new CliError(`Sessao local invalida em ${SESSION_FILE}. Rode "zenifra auth logout" e faca login novamente.`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function writeSession(session) {
|
|
367
|
+
await mkdir(dirname(SESSION_FILE), { recursive: true });
|
|
368
|
+
await writeFile(SESSION_FILE, `${JSON.stringify(session, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
369
|
+
await chmod(SESSION_FILE, 0o600).catch(() => undefined);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function apiBaseUrl(session, flags) {
|
|
373
|
+
return String(flags.apiBase || process.env.ZENIFRA_API_URL || session.apiBaseUrl || DEFAULT_API_BASE_URL).replace(/\/$/, '');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function resolveCredential(session) {
|
|
377
|
+
if (process.env.ZENIFRA_API_KEY) {
|
|
378
|
+
return { token: process.env.ZENIFRA_API_KEY, type: 'api_key', source: 'env' };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (session.apiKey) {
|
|
382
|
+
return { token: session.apiKey, type: 'api_key', source: 'session' };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (session.accessToken) {
|
|
386
|
+
return { token: session.accessToken, type: 'user', source: 'session' };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function hasApiKeyCredential(session) {
|
|
393
|
+
return Boolean(process.env.ZENIFRA_API_KEY || session.apiKey);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function requireUserSession(session, action) {
|
|
397
|
+
if (session.accessToken) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (hasApiKeyCredential(session)) {
|
|
402
|
+
throw new CliError(`${action} exige login de usuario. A API key global ja define a organizacao para comandos de automacao.`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
throw new CliError('Voce precisa fazer login primeiro: zenifra auth login');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function prompt(question, defaultValue) {
|
|
409
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
410
|
+
const suffix = defaultValue ? ` (${defaultValue})` : '';
|
|
411
|
+
const answer = await rl.question(`${question}${suffix}: `);
|
|
412
|
+
rl.close();
|
|
413
|
+
return answer.trim() || defaultValue || '';
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function promptHidden(question) {
|
|
417
|
+
if (!process.stdin.isTTY) {
|
|
418
|
+
return prompt(question);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return new Promise((resolve, reject) => {
|
|
422
|
+
let value = '';
|
|
423
|
+
const stdin = process.stdin;
|
|
424
|
+
|
|
425
|
+
const cleanup = () => {
|
|
426
|
+
stdin.setRawMode(false);
|
|
427
|
+
stdin.pause();
|
|
428
|
+
stdin.off('data', onData);
|
|
429
|
+
process.stdout.write('\n');
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const onData = (buffer) => {
|
|
433
|
+
const char = buffer.toString('utf8');
|
|
434
|
+
if (char === '\u0003') {
|
|
435
|
+
cleanup();
|
|
436
|
+
reject(new CliError('Operacao cancelada.', 130));
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (char === '\r' || char === '\n') {
|
|
440
|
+
cleanup();
|
|
441
|
+
resolve(value);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (char === '\u007f') {
|
|
445
|
+
value = value.slice(0, -1);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
value += char;
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
process.stdout.write(`${question}: `);
|
|
452
|
+
stdin.setRawMode(true);
|
|
453
|
+
stdin.resume();
|
|
454
|
+
stdin.on('data', onData);
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function request(session, flags, method, path, { body, orgId, tokenRequired = true } = {}) {
|
|
459
|
+
const credential = resolveCredential(session);
|
|
460
|
+
if (tokenRequired && !credential) {
|
|
461
|
+
throw new CliError('Voce precisa autenticar primeiro: zenifra auth login, zenifra auth api-key --key <znf_key> ou ZENIFRA_API_KEY.');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const headers = { Accept: 'application/json' };
|
|
465
|
+
if (body !== undefined) {
|
|
466
|
+
headers['Content-Type'] = 'application/json';
|
|
467
|
+
}
|
|
468
|
+
if (credential?.token) {
|
|
469
|
+
headers.Authorization = `Bearer ${credential.token}`;
|
|
470
|
+
}
|
|
471
|
+
if (orgId && credential?.type !== 'api_key') {
|
|
472
|
+
headers['x-organization-id'] = orgId;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const response = await fetch(`${apiBaseUrl(session, flags)}${path}`, {
|
|
476
|
+
method,
|
|
477
|
+
headers,
|
|
478
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const text = await response.text();
|
|
482
|
+
let payload = {};
|
|
483
|
+
if (text) {
|
|
484
|
+
try {
|
|
485
|
+
payload = JSON.parse(text);
|
|
486
|
+
} catch {
|
|
487
|
+
payload = { message: text };
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (!response.ok) {
|
|
492
|
+
const message = payload?.message || payload?.error || `Zenifra API retornou HTTP ${response.status}`;
|
|
493
|
+
if (response.status === 401 || response.status === 403) {
|
|
494
|
+
if (credential?.type === 'api_key') {
|
|
495
|
+
throw new CliError(`${message}. Verifique se a API key esta ativa, possui permissao RBAC e se o IP atual esta permitido.`);
|
|
496
|
+
}
|
|
497
|
+
throw new CliError(`${message}. Rode "zenifra auth login" para renovar sua sessao.`);
|
|
498
|
+
}
|
|
499
|
+
if (String(message).includes('missing x-organization-id')) {
|
|
500
|
+
throw new CliError('Organizacao nao selecionada. Rode "zenifra org set" ou use --org <id>.');
|
|
501
|
+
}
|
|
502
|
+
throw new CliError(message);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return payload;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function unwrapData(payload) {
|
|
509
|
+
return payload?.data ?? payload;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function pickToken(payload) {
|
|
513
|
+
return payload?.data?.token || payload?.data?.access_token || payload?.token || payload?.access_token;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function printJson(value) {
|
|
517
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function printTable(rows, columns) {
|
|
521
|
+
if (!rows.length) {
|
|
522
|
+
process.stdout.write('Nenhum registro encontrado.\n');
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const widths = columns.map((column) => Math.max(
|
|
527
|
+
column.label.length,
|
|
528
|
+
...rows.map((row) => String(column.value(row) ?? '').length),
|
|
529
|
+
));
|
|
530
|
+
|
|
531
|
+
process.stdout.write(`${columns.map((column, index) => column.label.padEnd(widths[index])).join(' ')}\n`);
|
|
532
|
+
process.stdout.write(`${widths.map((width) => '-'.repeat(width)).join(' ')}\n`);
|
|
533
|
+
|
|
534
|
+
for (const row of rows) {
|
|
535
|
+
process.stdout.write(`${columns.map((column, index) => String(column.value(row) ?? '').padEnd(widths[index])).join(' ')}\n`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function asArray(value) {
|
|
540
|
+
if (Array.isArray(value)) return value;
|
|
541
|
+
if (Array.isArray(value?.projects)) return value.projects;
|
|
542
|
+
if (Array.isArray(value?.organizations)) return value.organizations;
|
|
543
|
+
if (Array.isArray(value?.builds)) return value.builds;
|
|
544
|
+
if (Array.isArray(value?.deployments)) return value.deployments;
|
|
545
|
+
return [];
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function idOf(item) {
|
|
549
|
+
return item?._id || item?.id || item?.organization_id || item?.project_id || '';
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function projectIdOf(project) {
|
|
553
|
+
return project?.project_id || idOf(project);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function buildIdOf(build) {
|
|
557
|
+
return build?.id || build?._id || build?.build_id || '';
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function requireProjectId(flags) {
|
|
561
|
+
if (!flags.project) throw new CliError('Informe --project <id>.');
|
|
562
|
+
return String(flags.project);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function maskEnvValue(value) {
|
|
566
|
+
if (value === undefined || value === null) return '';
|
|
567
|
+
return '********';
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function envsForOutput(envs, { showValues = false } = {}) {
|
|
571
|
+
return asArray(envs).map((env) => ({
|
|
572
|
+
...env,
|
|
573
|
+
value: showValues ? env.value : maskEnvValue(env.value),
|
|
574
|
+
}));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function printEnvs(envs, flags) {
|
|
578
|
+
const output = envsForOutput(envs, { showValues: Boolean(flags.showValues) });
|
|
579
|
+
if (flags.json) return printJson(output);
|
|
580
|
+
printTable(output, [
|
|
581
|
+
{ label: 'Nome', value: (env) => env.name || '-' },
|
|
582
|
+
{ label: 'Valor', value: (env) => env.value || '-' },
|
|
583
|
+
]);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function printProject(project) {
|
|
587
|
+
const url = projectUrlOf(project);
|
|
588
|
+
const rows = [
|
|
589
|
+
{ label: 'ID', value: projectIdOf(project) || '-' },
|
|
590
|
+
{ label: 'Nome', value: project.name || '-' },
|
|
591
|
+
{ label: 'Status', value: project.status || '-' },
|
|
592
|
+
{ label: 'Tipo', value: project.type_project || '-' },
|
|
593
|
+
{ label: 'Plano', value: project.plan || '-' },
|
|
594
|
+
{ label: 'URL', value: url || '-' },
|
|
595
|
+
{ label: 'Imagem', value: project.image || '-' },
|
|
596
|
+
{ label: 'Instancias', value: project.instances ?? '-' },
|
|
597
|
+
{ label: 'Instancias atuais', value: project.additional_info?.current_instances ?? '-' },
|
|
598
|
+
];
|
|
599
|
+
|
|
600
|
+
for (const row of rows) {
|
|
601
|
+
process.stdout.write(`${row.label}: ${row.value}\n`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function projectUrlOf(project) {
|
|
606
|
+
const domain = project?.domain || project?.url || '';
|
|
607
|
+
if (!domain) return '';
|
|
608
|
+
return /^https?:\/\//i.test(domain) ? domain : `https://${domain}`;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function printLogs(logs, flags) {
|
|
612
|
+
if (flags.json) return printJson(logs);
|
|
613
|
+
if (Array.isArray(logs)) {
|
|
614
|
+
process.stdout.write(`${logs.filter(Boolean).join('\n')}\n`);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
process.stdout.write(`${logs || ''}${String(logs || '').endsWith('\n') ? '' : '\n'}`);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function getOrganizations(session, flags) {
|
|
621
|
+
requireUserSession(session, 'Listar organizacoes');
|
|
622
|
+
return asArray(unwrapData(await request(session, flags, 'GET', '/organizations')));
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async function resolveOrgId(session, flags, { interactive = true } = {}) {
|
|
626
|
+
if (hasApiKeyCredential(session)) return undefined;
|
|
627
|
+
if (flags.org) return String(flags.org);
|
|
628
|
+
if (session.selectedOrganizationId) return session.selectedOrganizationId;
|
|
629
|
+
if (!interactive) throw new CliError('Organizacao nao selecionada. Rode "zenifra org set" ou use --org <id>.');
|
|
630
|
+
|
|
631
|
+
const organizations = await getOrganizations(session, flags);
|
|
632
|
+
if (organizations.length === 0) {
|
|
633
|
+
throw new CliError('Nenhuma organizacao disponivel para este usuario.');
|
|
634
|
+
}
|
|
635
|
+
if (organizations.length === 1) {
|
|
636
|
+
session.selectedOrganizationId = idOf(organizations[0]);
|
|
637
|
+
await writeSession(session);
|
|
638
|
+
return session.selectedOrganizationId;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
process.stdout.write('Selecione a organizacao ativa:\n');
|
|
642
|
+
organizations.forEach((org, index) => {
|
|
643
|
+
process.stdout.write(` ${index + 1}. ${org.name || org.organization?.name || idOf(org)} (${idOf(org)})\n`);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
const answer = await prompt('Numero da organizacao');
|
|
647
|
+
const selected = organizations[Number(answer) - 1];
|
|
648
|
+
if (!selected) {
|
|
649
|
+
throw new CliError('Organizacao invalida.');
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
session.selectedOrganizationId = idOf(selected);
|
|
653
|
+
await writeSession(session);
|
|
654
|
+
return session.selectedOrganizationId;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
async function handleLogin(session, flags) {
|
|
658
|
+
const email = String(flags.email || await prompt('Email'));
|
|
659
|
+
const password = String(flags.password || await promptHidden('Senha'));
|
|
660
|
+
|
|
661
|
+
const loginPayload = await request(session, flags, 'PATCH', '/authentication', {
|
|
662
|
+
tokenRequired: false,
|
|
663
|
+
body: { email, password },
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
let payload = loginPayload;
|
|
667
|
+
const directToken = pickToken(loginPayload);
|
|
668
|
+
|
|
669
|
+
if (!directToken) {
|
|
670
|
+
const loginData = unwrapData(loginPayload);
|
|
671
|
+
|
|
672
|
+
if (!loginData?.requires_challenge) {
|
|
673
|
+
throw new CliError('Login nao retornou token nem desafio de autenticacao.');
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const challengeToken = String(flags.challengeToken || loginData.challenge_token || '');
|
|
677
|
+
if (!challengeToken) {
|
|
678
|
+
throw new CliError('Login exigiu desafio, mas a API nao retornou challenge_token.');
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const challengeType = loginData.challenge_type || 'email_code';
|
|
682
|
+
const destination = loginData.masked_destination ? ` (${loginData.masked_destination})` : '';
|
|
683
|
+
const label = challengeType === 'totp_app'
|
|
684
|
+
? 'Codigo do app autenticador'
|
|
685
|
+
: `Codigo de verificacao${destination}`;
|
|
686
|
+
const code = String(flags.code || flags.totp || await prompt(label));
|
|
687
|
+
|
|
688
|
+
payload = await request(session, flags, 'POST', '/authentication/challenge/verify', {
|
|
689
|
+
tokenRequired: false,
|
|
690
|
+
body: {
|
|
691
|
+
challenge_token: challengeToken,
|
|
692
|
+
code,
|
|
693
|
+
},
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const accessToken = pickToken(payload);
|
|
698
|
+
if (!accessToken) {
|
|
699
|
+
throw new CliError('Login concluido sem token na resposta. Verifique o contrato de /authentication/challenge/verify.');
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const nextSession = {
|
|
703
|
+
...session,
|
|
704
|
+
accessToken,
|
|
705
|
+
apiKey: undefined,
|
|
706
|
+
apiBaseUrl: apiBaseUrl(session, flags),
|
|
707
|
+
selectedOrganizationId: session.selectedOrganizationId,
|
|
708
|
+
updatedAt: new Date().toISOString(),
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
await writeSession(nextSession);
|
|
712
|
+
process.stdout.write('Login realizado com sucesso.\n');
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async function handleApiKeyLogin(session, flags) {
|
|
716
|
+
const apiKey = String(flags.key || await promptHidden('API key Zenifra'));
|
|
717
|
+
|
|
718
|
+
if (!apiKey.startsWith('znf_')) {
|
|
719
|
+
throw new CliError('API key invalida. As API keys globais da Zenifra comecam com "znf_".');
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const nextSession = {
|
|
723
|
+
...session,
|
|
724
|
+
accessToken: undefined,
|
|
725
|
+
selectedOrganizationId: undefined,
|
|
726
|
+
apiKey,
|
|
727
|
+
apiBaseUrl: apiBaseUrl(session, flags),
|
|
728
|
+
updatedAt: new Date().toISOString(),
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
await writeSession(nextSession);
|
|
732
|
+
process.stdout.write('API key salva com sucesso. Ela sera usada em comandos de automacao.\n');
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
async function handleLogout() {
|
|
736
|
+
await rm(SESSION_FILE, { force: true });
|
|
737
|
+
process.stdout.write('Sessao removida.\n');
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
async function handleOrgs(session, flags) {
|
|
741
|
+
const organizations = await getOrganizations(session, flags);
|
|
742
|
+
if (flags.json) return printJson(organizations);
|
|
743
|
+
printTable(organizations, [
|
|
744
|
+
{ label: 'ID', value: idOf },
|
|
745
|
+
{ label: 'Nome', value: (org) => org.name || org.organization?.name || '-' },
|
|
746
|
+
{ label: 'Role', value: (org) => org.role || '-' },
|
|
747
|
+
{ label: 'Status', value: (org) => org.status || '-' },
|
|
748
|
+
]);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async function handleOrgSet(session, flags) {
|
|
752
|
+
requireUserSession(session, 'Selecionar organizacao');
|
|
753
|
+
|
|
754
|
+
if (flags.org) {
|
|
755
|
+
session.selectedOrganizationId = String(flags.org);
|
|
756
|
+
await writeSession(session);
|
|
757
|
+
process.stdout.write(`Organizacao ativa: ${session.selectedOrganizationId}\n`);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const orgId = await resolveOrgId({ ...session, selectedOrganizationId: undefined }, flags, { interactive: true });
|
|
762
|
+
process.stdout.write(`Organizacao ativa: ${orgId}\n`);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
async function handleProjects(session, flags) {
|
|
766
|
+
const orgId = await resolveOrgId(session, flags);
|
|
767
|
+
const query = buildQuery(flags, ['type']);
|
|
768
|
+
const data = unwrapData(await request(session, flags, 'GET', `/project${query}`, { orgId }));
|
|
769
|
+
const projects = asArray(data);
|
|
770
|
+
|
|
771
|
+
if (flags.json) return printJson(data);
|
|
772
|
+
printTable(projects, [
|
|
773
|
+
{ label: 'ID', value: projectIdOf },
|
|
774
|
+
{ label: 'Nome', value: (project) => project.name || '-' },
|
|
775
|
+
{ label: 'Status', value: (project) => project.status || '-' },
|
|
776
|
+
{ label: 'Plano', value: (project) => project.plan || '-' },
|
|
777
|
+
{ label: 'Tipo', value: (project) => project.type_project || project.config?.type_project || '-' },
|
|
778
|
+
]);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
async function parseConfig(input) {
|
|
782
|
+
if (!input) {
|
|
783
|
+
throw new CliError('Informe --config com JSON ou @arquivo.json.');
|
|
784
|
+
}
|
|
785
|
+
const raw = String(input).startsWith('@')
|
|
786
|
+
? await readFile(String(input).slice(1), 'utf8')
|
|
787
|
+
: String(input);
|
|
788
|
+
|
|
789
|
+
try {
|
|
790
|
+
return JSON.parse(raw);
|
|
791
|
+
} catch {
|
|
792
|
+
throw new CliError('Config invalido. Use JSON valido ou @arquivo.json.');
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
async function handleProjectCreate(session, flags) {
|
|
797
|
+
const orgId = await resolveOrgId(session, flags);
|
|
798
|
+
const name = flags.name || await prompt('Nome do projeto');
|
|
799
|
+
const plan = flags.plan || await prompt('Plano', 'free');
|
|
800
|
+
const paymentMode = flags.paymentMode || await prompt('Modo de pagamento', 'hourly');
|
|
801
|
+
const config = await parseConfig(flags.config || await prompt('Config JSON ou @arquivo'));
|
|
802
|
+
|
|
803
|
+
const payload = await request(session, flags, 'POST', '/project', {
|
|
804
|
+
orgId,
|
|
805
|
+
body: {
|
|
806
|
+
name,
|
|
807
|
+
...(flags.description ? { description: String(flags.description) } : {}),
|
|
808
|
+
plan,
|
|
809
|
+
payment_mode: paymentMode,
|
|
810
|
+
config,
|
|
811
|
+
},
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
if (flags.json) return printJson(payload);
|
|
815
|
+
const project = unwrapData(payload);
|
|
816
|
+
process.stdout.write(`Projeto criado: ${projectIdOf(project) || 'id indisponivel'}\n`);
|
|
817
|
+
if (project.domain) process.stdout.write(`Dominio: ${project.domain}\n`);
|
|
818
|
+
if (project.api_key) process.stdout.write(`API key: ${project.api_key}\n`);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
async function getProject(session, flags, projectId, orgId) {
|
|
822
|
+
return unwrapData(await request(session, flags, 'GET', `/project/${projectId}`, { orgId }));
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
async function handleProjectInfo(session, flags) {
|
|
826
|
+
const projectId = requireProjectId(flags);
|
|
827
|
+
const orgId = await resolveOrgId(session, flags);
|
|
828
|
+
const project = await getProject(session, flags, projectId, orgId);
|
|
829
|
+
|
|
830
|
+
if (flags.json) return printJson(project);
|
|
831
|
+
printProject({ ...project, id: projectId });
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
async function handleProjectUrl(session, flags) {
|
|
835
|
+
const projectId = requireProjectId(flags);
|
|
836
|
+
const orgId = await resolveOrgId(session, flags);
|
|
837
|
+
const project = await getProject(session, flags, projectId, orgId);
|
|
838
|
+
const url = projectUrlOf(project);
|
|
839
|
+
const payload = {
|
|
840
|
+
project_id: projectId,
|
|
841
|
+
url,
|
|
842
|
+
domain: project.domain || null,
|
|
843
|
+
custom_domains: project.custom_domains || [],
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
if (flags.json) return printJson(payload);
|
|
847
|
+
if (!url) throw new CliError('Projeto nao possui URL disponivel.');
|
|
848
|
+
process.stdout.write(`${url}\n`);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
async function handleProjectLogs(session, flags) {
|
|
852
|
+
const projectId = requireProjectId(flags);
|
|
853
|
+
const orgId = await resolveOrgId(session, flags);
|
|
854
|
+
const query = buildQuery(flags, ['instance']);
|
|
855
|
+
const logs = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/logs${query}`, { orgId }));
|
|
856
|
+
|
|
857
|
+
printLogs(logs, flags);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
async function handleProjectMetrics(session, flags) {
|
|
861
|
+
const projectId = requireProjectId(flags);
|
|
862
|
+
const orgId = await resolveOrgId(session, flags);
|
|
863
|
+
const query = buildQuery(flags, ['instance']);
|
|
864
|
+
const metrics = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/metrics${query}`, { orgId }));
|
|
865
|
+
|
|
866
|
+
if (flags.json) return printJson(metrics);
|
|
867
|
+
if (Array.isArray(metrics)) {
|
|
868
|
+
return printTable(metrics, [
|
|
869
|
+
{ label: 'Instancia', value: (metric) => metric.instance || '-' },
|
|
870
|
+
{ label: 'Tipo', value: (metric) => metric.type || '-' },
|
|
871
|
+
{ label: 'CPU', value: (metric) => metric.cpu ?? '-' },
|
|
872
|
+
{ label: 'Memoria', value: (metric) => metric.memory ?? '-' },
|
|
873
|
+
]);
|
|
874
|
+
}
|
|
875
|
+
printJson(metrics);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
async function handleProjectNetwork(session, flags) {
|
|
879
|
+
const projectId = requireProjectId(flags);
|
|
880
|
+
const orgId = await resolveOrgId(session, flags);
|
|
881
|
+
const view = String(flags.view || 'summary');
|
|
882
|
+
const allowedViews = new Set(['summary', 'status-codes', 'routes', 'user-agents', 'request-events', 'source-ips']);
|
|
883
|
+
|
|
884
|
+
if (!allowedViews.has(view)) {
|
|
885
|
+
throw new CliError('View de rede invalida. Use summary, status-codes, routes, user-agents, request-events ou source-ips.');
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const data = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/metrics/network/${view}`, { orgId }));
|
|
889
|
+
if (flags.json) return printJson(data);
|
|
890
|
+
printJson(data);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
async function handleProjectImageSet(session, flags) {
|
|
894
|
+
const projectId = requireProjectId(flags);
|
|
895
|
+
const image = flags.image || await prompt('Imagem');
|
|
896
|
+
if (!image) throw new CliError('Informe --image <image>.');
|
|
897
|
+
|
|
898
|
+
const orgId = await resolveOrgId(session, flags);
|
|
899
|
+
const payload = await request(session, flags, 'PATCH', `/project/${projectId}/image`, {
|
|
900
|
+
orgId,
|
|
901
|
+
body: { image: String(image) },
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
if (flags.json) return printJson(payload);
|
|
905
|
+
process.stdout.write('Imagem atualizada com sucesso.\n');
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
async function getProjectEnvs(session, flags, projectId, orgId) {
|
|
909
|
+
return asArray(unwrapData(await request(session, flags, 'GET', `/project/${projectId}/envs`, { orgId })));
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
async function patchProjectEnvs(session, flags, projectId, orgId, envs) {
|
|
913
|
+
return request(session, flags, 'PATCH', `/project/${projectId}/envs`, {
|
|
914
|
+
orgId,
|
|
915
|
+
body: { envs },
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
async function handleProjectEnvs(session, flags) {
|
|
920
|
+
const projectId = requireProjectId(flags);
|
|
921
|
+
const orgId = await resolveOrgId(session, flags);
|
|
922
|
+
const envs = await getProjectEnvs(session, flags, projectId, orgId);
|
|
923
|
+
|
|
924
|
+
printEnvs(envs, flags);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
async function handleProjectEnvMutation(session, flags, action) {
|
|
928
|
+
const projectId = requireProjectId(flags);
|
|
929
|
+
const name = String(flags.name || '').trim();
|
|
930
|
+
if (!name) throw new CliError('Informe --name <name>.');
|
|
931
|
+
|
|
932
|
+
const needsValue = action === 'add' || action === 'update';
|
|
933
|
+
const value = needsValue ? String(flags.value ?? await promptHidden('Valor')) : undefined;
|
|
934
|
+
if (needsValue && !value) throw new CliError('Informe --value <value>.');
|
|
935
|
+
|
|
936
|
+
const orgId = await resolveOrgId(session, flags);
|
|
937
|
+
const envs = await getProjectEnvs(session, flags, projectId, orgId);
|
|
938
|
+
const existingIndex = envs.findIndex((env) => env.name === name);
|
|
939
|
+
|
|
940
|
+
if (action === 'add' && existingIndex !== -1) {
|
|
941
|
+
throw new CliError(`Env ${name} ja existe. Use "zenifra project env update".`);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
if ((action === 'update' || action === 'remove') && existingIndex === -1) {
|
|
945
|
+
throw new CliError(`Env ${name} nao existe.`);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const nextEnvs = [...envs];
|
|
949
|
+
if (action === 'add') nextEnvs.push({ name, value });
|
|
950
|
+
if (action === 'update') nextEnvs[existingIndex] = { ...nextEnvs[existingIndex], name, value };
|
|
951
|
+
if (action === 'remove') nextEnvs.splice(existingIndex, 1);
|
|
952
|
+
|
|
953
|
+
const payload = await patchProjectEnvs(session, flags, projectId, orgId, nextEnvs);
|
|
954
|
+
|
|
955
|
+
if (flags.json) {
|
|
956
|
+
return printJson({
|
|
957
|
+
status: payload.status || 'success',
|
|
958
|
+
message: payload.message || 'updated with success',
|
|
959
|
+
envs: envsForOutput(nextEnvs),
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
process.stdout.write(`Env ${name} ${action === 'remove' ? 'removida' : 'atualizada'} com sucesso.\n`);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
async function handleProjectInstances(session, flags) {
|
|
967
|
+
const projectId = requireProjectId(flags);
|
|
968
|
+
const orgId = await resolveOrgId(session, flags);
|
|
969
|
+
const instances = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/instances`, { orgId }));
|
|
970
|
+
|
|
971
|
+
if (flags.json) return printJson(instances);
|
|
972
|
+
printTable(asArray(instances), [
|
|
973
|
+
{ label: 'Instancia', value: (instance) => instance.instance || instance.name || '-' },
|
|
974
|
+
]);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
async function handleProjectInstancesSet(session, flags) {
|
|
978
|
+
const projectId = requireProjectId(flags);
|
|
979
|
+
const instances = Number(flags.count);
|
|
980
|
+
if (!Number.isInteger(instances) || instances <= 0) {
|
|
981
|
+
throw new CliError('Informe --count <n> com um inteiro positivo.');
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const orgId = await resolveOrgId(session, flags);
|
|
985
|
+
const payload = await request(session, flags, 'PATCH', `/project/${projectId}/instances`, {
|
|
986
|
+
orgId,
|
|
987
|
+
body: { instances },
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
if (flags.json) return printJson(payload);
|
|
991
|
+
process.stdout.write(`Quantidade de instancias atualizada para ${instances}.\n`);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function buildQuery(flags, allowedKeys) {
|
|
995
|
+
const params = new URLSearchParams();
|
|
996
|
+
for (const key of allowedKeys) {
|
|
997
|
+
if (flags[key] !== undefined && flags[key] !== true) {
|
|
998
|
+
const apiKey = key.replace(/[A-Z]/g, (value) => `_${value.toLowerCase()}`);
|
|
999
|
+
params.set(apiKey, String(flags[key]));
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
const query = params.toString();
|
|
1003
|
+
return query ? `?${query}` : '';
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
async function handleDeployments(session, flags) {
|
|
1007
|
+
const projectId = flags.project;
|
|
1008
|
+
if (!projectId) throw new CliError('Informe --project <id>.');
|
|
1009
|
+
const orgId = await resolveOrgId(session, flags);
|
|
1010
|
+
const query = buildQuery(flags, ['page', 'limit', 'branch', 'status']);
|
|
1011
|
+
const data = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/github/builds${query}`, { orgId }));
|
|
1012
|
+
const builds = asArray(data);
|
|
1013
|
+
|
|
1014
|
+
if (flags.json) return printJson(data);
|
|
1015
|
+
printTable(builds, [
|
|
1016
|
+
{ label: 'Build', value: (build) => buildIdOf(build) || '-' },
|
|
1017
|
+
{ label: 'Status', value: (build) => build.status || '-' },
|
|
1018
|
+
{ label: 'Branch', value: (build) => build.branch || '-' },
|
|
1019
|
+
{ label: 'Commit', value: (build) => build.commit_sha || '-' },
|
|
1020
|
+
{ label: 'Inicio', value: (build) => build.started_at || build.created_at || '-' },
|
|
1021
|
+
]);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
async function handleDeploy(session, flags) {
|
|
1025
|
+
const projectId = flags.project;
|
|
1026
|
+
if (!projectId) throw new CliError('Informe --project <id>.');
|
|
1027
|
+
const orgId = await resolveOrgId(session, flags);
|
|
1028
|
+
const body = {};
|
|
1029
|
+
if (flags.branch) body.branch = String(flags.branch);
|
|
1030
|
+
if (flags.commitSha) body.commit_sha = String(flags.commitSha);
|
|
1031
|
+
|
|
1032
|
+
const payload = await request(session, flags, 'POST', `/project/${projectId}/github/deploy`, {
|
|
1033
|
+
orgId,
|
|
1034
|
+
body,
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
if (flags.json) return printJson(payload);
|
|
1038
|
+
const buildId = unwrapData(payload)?.build_id || payload?.build_id || unwrapData(payload)?.id;
|
|
1039
|
+
process.stdout.write(`Deploy iniciado${buildId ? `: ${buildId}` : '.'}\n`);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
async function getBuild(session, flags, projectId, buildId, orgId) {
|
|
1043
|
+
return unwrapData(await request(session, flags, 'GET', `/project/${projectId}/github/builds/${buildId}`, { orgId }));
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
async function handleDeployWatch(session, flags) {
|
|
1047
|
+
const projectId = flags.project;
|
|
1048
|
+
const buildId = flags.build;
|
|
1049
|
+
if (!projectId) throw new CliError('Informe --project <id>.');
|
|
1050
|
+
if (!buildId) throw new CliError('Informe --build <id>.');
|
|
1051
|
+
|
|
1052
|
+
const orgId = await resolveOrgId(session, flags);
|
|
1053
|
+
const intervalMs = Math.max(Number(flags.interval || 5), 1) * 1000;
|
|
1054
|
+
const timeoutMs = Math.max(Number(flags.timeout || 900), 1) * 1000;
|
|
1055
|
+
const startedAt = Date.now();
|
|
1056
|
+
|
|
1057
|
+
while (true) {
|
|
1058
|
+
const build = await getBuild(session, flags, projectId, buildId, orgId);
|
|
1059
|
+
const status = build?.status || 'unknown';
|
|
1060
|
+
|
|
1061
|
+
if (flags.json) {
|
|
1062
|
+
printJson(build);
|
|
1063
|
+
} else {
|
|
1064
|
+
process.stdout.write(`[${new Date().toISOString()}] build ${buildId}: ${status}\n`);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
if (FINAL_BUILD_STATUSES.has(status)) {
|
|
1068
|
+
if (status === 'failed') {
|
|
1069
|
+
process.exitCode = 1;
|
|
1070
|
+
}
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
1075
|
+
throw new CliError(`Timeout aguardando build ${buildId}. Ultimo status: ${status}`);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
async function main() {
|
|
1083
|
+
const { positional, flags } = parseArgs(process.argv.slice(2));
|
|
1084
|
+
const [command, subcommand] = positional;
|
|
1085
|
+
const session = await readSession();
|
|
1086
|
+
|
|
1087
|
+
if (command === 'help') {
|
|
1088
|
+
process.stdout.write(commandHelp(positional.slice(1)));
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
if (!command) {
|
|
1093
|
+
process.stdout.write(usage());
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
if (flags.help) {
|
|
1098
|
+
process.stdout.write(commandHelp(positional));
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
if (command === 'auth' && subcommand === 'login') return handleLogin(session, flags);
|
|
1103
|
+
if (command === 'auth' && subcommand === 'api-key') return handleApiKeyLogin(session, flags);
|
|
1104
|
+
if (command === 'auth' && subcommand === 'logout') return handleLogout();
|
|
1105
|
+
if (command === 'login') return handleLogin(session, flags);
|
|
1106
|
+
if (command === 'logout') return handleLogout();
|
|
1107
|
+
if (command === 'orgs') return handleOrgs(session, flags);
|
|
1108
|
+
if (command === 'org' && subcommand === 'set') return handleOrgSet(session, flags);
|
|
1109
|
+
if (command === 'projects' && subcommand === 'create') return handleProjectCreate(session, flags);
|
|
1110
|
+
if (command === 'projects') return handleProjects(session, flags);
|
|
1111
|
+
if (command === 'project' && subcommand === 'info') return handleProjectInfo(session, flags);
|
|
1112
|
+
if (command === 'project' && subcommand === 'url') return handleProjectUrl(session, flags);
|
|
1113
|
+
if (command === 'project' && subcommand === 'logs') return handleProjectLogs(session, flags);
|
|
1114
|
+
if (command === 'project' && subcommand === 'metrics') return handleProjectMetrics(session, flags);
|
|
1115
|
+
if (command === 'project' && subcommand === 'network') return handleProjectNetwork(session, flags);
|
|
1116
|
+
if (command === 'project' && subcommand === 'image' && positional[2] === 'set') return handleProjectImageSet(session, flags);
|
|
1117
|
+
if (command === 'project' && subcommand === 'envs') return handleProjectEnvs(session, flags);
|
|
1118
|
+
if (command === 'project' && subcommand === 'env' && positional[2] === 'add') return handleProjectEnvMutation(session, flags, 'add');
|
|
1119
|
+
if (command === 'project' && subcommand === 'env' && positional[2] === 'update') return handleProjectEnvMutation(session, flags, 'update');
|
|
1120
|
+
if (command === 'project' && subcommand === 'env' && positional[2] === 'remove') return handleProjectEnvMutation(session, flags, 'remove');
|
|
1121
|
+
if (command === 'project' && subcommand === 'instances' && positional[2] === 'set') return handleProjectInstancesSet(session, flags);
|
|
1122
|
+
if (command === 'project' && subcommand === 'instances') return handleProjectInstances(session, flags);
|
|
1123
|
+
if (command === 'builds') return handleDeployments(session, flags);
|
|
1124
|
+
if (command === 'deployments') return handleDeployments(session, flags);
|
|
1125
|
+
if (command === 'deploy' && subcommand === 'watch') return handleDeployWatch(session, flags);
|
|
1126
|
+
if (command === 'deploy') return handleDeploy(session, flags);
|
|
1127
|
+
|
|
1128
|
+
throw new CliError(`Comando desconhecido: ${positional.join(' ')}`);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
main().catch((error) => {
|
|
1132
|
+
if (error instanceof CliError) {
|
|
1133
|
+
process.stderr.write(`${error.message}\n`);
|
|
1134
|
+
process.exit(error.exitCode);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
process.stderr.write(`${error.stack || error.message}\n`);
|
|
1138
|
+
process.exit(1);
|
|
1139
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type_project": "http",
|
|
3
|
+
"image": {
|
|
4
|
+
"url": "nginxinc/nginx-unprivileged:latest",
|
|
5
|
+
"is_public": true
|
|
6
|
+
},
|
|
7
|
+
"instances": 1,
|
|
8
|
+
"storage": {
|
|
9
|
+
"persistent": false,
|
|
10
|
+
"capacity": 1
|
|
11
|
+
},
|
|
12
|
+
"envs": [],
|
|
13
|
+
"port": 8080,
|
|
14
|
+
"network_access": {
|
|
15
|
+
"ingress_white_list": [
|
|
16
|
+
{
|
|
17
|
+
"cidr": "0.0.0.0/0",
|
|
18
|
+
"description": "Public access"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"ingress_black_list": []
|
|
22
|
+
},
|
|
23
|
+
"custom_domains": []
|
|
24
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zenifra/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Official Zenifra command line interface.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://zenifra.com/",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/zenifra/zenifra-cli.git"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"zenifra",
|
|
17
|
+
"cli",
|
|
18
|
+
"deploy",
|
|
19
|
+
"projects",
|
|
20
|
+
"cloud"
|
|
21
|
+
],
|
|
22
|
+
"bin": {
|
|
23
|
+
"zenifra": "./bin/zenifra.mjs"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"bin",
|
|
30
|
+
"examples",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"check": "node --check bin/zenifra.mjs",
|
|
35
|
+
"test": "node --test"
|
|
36
|
+
}
|
|
37
|
+
}
|