@zenifra/cli 0.2.3 → 0.2.5
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 +52 -4
- package/bin/zenifra.mjs +1169 -67
- package/examples/http-autoscaling-project.json +30 -0
- package/examples/http-github-project.json +1 -0
- package/examples/http-project.json +1 -0
- package/examples/valkey-cache-project.json +14 -0
- package/examples/valkey-key-value-project.json +18 -0
- package/examples/valkey-queue-project.json +18 -0
- package/package.json +4 -2
package/bin/zenifra.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
3
4
|
import { mkdir, readFile, rm, writeFile, chmod } from 'node:fs/promises';
|
|
4
5
|
import { existsSync } from 'node:fs';
|
|
5
6
|
import { dirname, join } from 'node:path';
|
|
@@ -13,6 +14,7 @@ const DOCS_RUNTIME_URL = `${DOCS_BASE_URL}/docs/runtime`;
|
|
|
13
14
|
const DOCS_CREATE_HTTP_URL = `${DOCS_BASE_URL}/docs/how-use-zenifra/http/how-to-create-a-http-project-at-zenifra-via-console`;
|
|
14
15
|
const DOCS_CREATE_POSTGRESQL_URL = `${DOCS_BASE_URL}/docs/how-use-zenifra/database/how-to-create-a-postgresql-project-at-zenifra-via-console`;
|
|
15
16
|
const DOCS_CREATE_MARIADB_URL = `${DOCS_BASE_URL}/docs/how-use-zenifra/database/how-to-create-a-mariadb-project-at-zenifra-via-console`;
|
|
17
|
+
const DOCS_CREATE_VALKEY_URL = `${DOCS_BASE_URL}/docs/managed-services/valkey`;
|
|
16
18
|
const DOCS_CONFIGURATION_URL = `${DOCS_BASE_URL}/docs/configuration`;
|
|
17
19
|
const DOCS_DATABASE_CONFIGURATION_URL = `${DOCS_BASE_URL}/docs/database/configuration/configuration`;
|
|
18
20
|
const DOCS_PAYMENTS_URL = `${DOCS_BASE_URL}/docs/payments/payments`;
|
|
@@ -21,6 +23,56 @@ const SESSION_FILE = join(SESSION_DIR, 'session.json');
|
|
|
21
23
|
const PROFILES_FILE = join(SESSION_DIR, 'profiles.json');
|
|
22
24
|
const PROFILE_STORE_VERSION = 1;
|
|
23
25
|
const DEFAULT_PROFILE_NAME = 'default';
|
|
26
|
+
const DEFAULT_HTTP_TIMEOUT_MS = 30_000;
|
|
27
|
+
const KNOWN_FLAG_NAMES = new Set([
|
|
28
|
+
'apiBase',
|
|
29
|
+
'branch',
|
|
30
|
+
'build',
|
|
31
|
+
'challengeToken',
|
|
32
|
+
'code',
|
|
33
|
+
'commitSha',
|
|
34
|
+
'config',
|
|
35
|
+
'count',
|
|
36
|
+
'cpu',
|
|
37
|
+
'cursor',
|
|
38
|
+
'description',
|
|
39
|
+
'direction',
|
|
40
|
+
'email',
|
|
41
|
+
'exposure',
|
|
42
|
+
'follow',
|
|
43
|
+
'from',
|
|
44
|
+
'help',
|
|
45
|
+
'image',
|
|
46
|
+
'idempotencyKey',
|
|
47
|
+
'instance',
|
|
48
|
+
'interval',
|
|
49
|
+
'json',
|
|
50
|
+
'key',
|
|
51
|
+
'limit',
|
|
52
|
+
'max',
|
|
53
|
+
'memory',
|
|
54
|
+
'min',
|
|
55
|
+
'mode',
|
|
56
|
+
'name',
|
|
57
|
+
'org',
|
|
58
|
+
'operation',
|
|
59
|
+
'page',
|
|
60
|
+
'password',
|
|
61
|
+
'paymentMode',
|
|
62
|
+
'plan',
|
|
63
|
+
'profile',
|
|
64
|
+
'project',
|
|
65
|
+
'revoke',
|
|
66
|
+
'showValues',
|
|
67
|
+
'status',
|
|
68
|
+
'timeout',
|
|
69
|
+
'to',
|
|
70
|
+
'totp',
|
|
71
|
+
'type',
|
|
72
|
+
'value',
|
|
73
|
+
'view',
|
|
74
|
+
'wait',
|
|
75
|
+
]);
|
|
24
76
|
const ALLOWED_PLAN_VALUES = new Set([
|
|
25
77
|
'free',
|
|
26
78
|
'static',
|
|
@@ -35,10 +87,21 @@ const ALLOWED_PLAN_VALUES = new Set([
|
|
|
35
87
|
'db-basic',
|
|
36
88
|
'db-premium',
|
|
37
89
|
'db-enterprise',
|
|
90
|
+
'cache-free',
|
|
91
|
+
'cache-starter',
|
|
92
|
+
'cache-basic',
|
|
93
|
+
'cache-premium',
|
|
94
|
+
'cache-enterprise',
|
|
95
|
+
'queue-free',
|
|
96
|
+
'queue-starter',
|
|
97
|
+
'queue-basic',
|
|
98
|
+
'queue-premium',
|
|
99
|
+
'queue-enterprise',
|
|
38
100
|
]);
|
|
39
101
|
const ALLOWED_PAYMENT_MODE_VALUES = new Set(['hourly', 'monthly', 'yearly']);
|
|
40
|
-
const ALLOWED_TYPE_PROJECT_VALUES = new Set(['http', 'postgresql', 'mariadb']);
|
|
102
|
+
const ALLOWED_TYPE_PROJECT_VALUES = new Set(['http', 'postgresql', 'mariadb', 'valkey']);
|
|
41
103
|
const ALLOWED_RUNTIME_VALUES = new Set(['nodejs', 'python']);
|
|
104
|
+
const ALLOWED_EXPOSURE_VALUES = new Set(['public', 'private']);
|
|
42
105
|
const GITHUB_RUNTIME_VERSIONS = {
|
|
43
106
|
nodejs: ['24', '22', '20'],
|
|
44
107
|
python: ['3.13', '3.12', '3.11'],
|
|
@@ -67,7 +130,9 @@ const WIZARD_DATABASE_PLAN_OPTIONS = [
|
|
|
67
130
|
'db-free',
|
|
68
131
|
];
|
|
69
132
|
const WIZARD_PAYMENT_MODE_OPTIONS = ['hourly', 'monthly', 'yearly'];
|
|
70
|
-
const WIZARD_TYPE_PROJECT_OPTIONS = ['http', 'postgresql', 'mariadb'];
|
|
133
|
+
const WIZARD_TYPE_PROJECT_OPTIONS = ['http', 'postgresql', 'mariadb', 'valkey'];
|
|
134
|
+
const WIZARD_VALKEY_PROFILE_OPTIONS = ['key_value', 'cache', 'queue'];
|
|
135
|
+
const WIZARD_HTTP_EXPOSURE_OPTIONS = ['public', 'private'];
|
|
71
136
|
let promptInterface;
|
|
72
137
|
let pipedInputPromise;
|
|
73
138
|
let pipedInputIndex = 0;
|
|
@@ -95,7 +160,7 @@ Usage:
|
|
|
95
160
|
zenifra help <command>
|
|
96
161
|
zenifra auth login [--profile <name>] [--api-base <url>] [--code <code>]
|
|
97
162
|
zenifra auth api-key --key <znf_key> [--profile <name>] [--api-base <url>]
|
|
98
|
-
zenifra auth logout [--profile <name>]
|
|
163
|
+
zenifra auth logout [--profile <name>] [--revoke]
|
|
99
164
|
zenifra profile list [--json]
|
|
100
165
|
zenifra profile show [<name>] [--json]
|
|
101
166
|
zenifra profile add [--name <name>] [--description <text>] [--api-base <url>] [--mode <api-key|login>] [--key <znf_key>] [--json]
|
|
@@ -104,20 +169,30 @@ Usage:
|
|
|
104
169
|
zenifra profile remove <name> [--json]
|
|
105
170
|
zenifra orgs [--json]
|
|
106
171
|
zenifra org set [--org <id>]
|
|
107
|
-
zenifra plans [--type <all|http|database|storage>] [--json]
|
|
172
|
+
zenifra plans [--type <all|http|database|storage|valkey>] [--json]
|
|
108
173
|
zenifra create project
|
|
109
174
|
zenifra create project --name <name> --plan <plan> --payment-mode <mode> --config <json|@file> [--description <text>] [--org <id>] [--json]
|
|
110
|
-
zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb>] [--page <n>] [--limit <n>]
|
|
175
|
+
zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb|valkey>] [--page <n>] [--limit <n>]
|
|
176
|
+
zenifra valkey status --project <id> [--json]
|
|
177
|
+
zenifra valkey connection --project <id> [--json]
|
|
178
|
+
zenifra valkey credentials rotate --project <id> [--idempotency-key <key>] [--wait] [--interval <seconds>] [--timeout <seconds>] [--json]
|
|
179
|
+
zenifra valkey credentials status --project <id> --operation <id> [--json]
|
|
111
180
|
zenifra project info --project <id> [--json]
|
|
112
181
|
zenifra project url --project <id> [--json]
|
|
113
182
|
zenifra project logs --project <id> [--instance <id>] [--json]
|
|
114
183
|
zenifra project metrics --project <id> [--instance <id>] [--json]
|
|
115
184
|
zenifra project network --project <id> [--view <summary|status-codes|routes|user-agents|request-events|source-ips>] [--json]
|
|
116
185
|
zenifra project image set --project <id> --image <image> [--json]
|
|
186
|
+
zenifra project exposure set --project <id> --exposure <public|private> [--json]
|
|
117
187
|
zenifra project envs --project <id> [--json] [--show-values]
|
|
118
188
|
zenifra project env add --project <id> --name <name> --value <value> [--json]
|
|
119
189
|
zenifra project env update --project <id> --name <name> --value <value> [--json]
|
|
120
190
|
zenifra project env remove --project <id> --name <name> [--json]
|
|
191
|
+
zenifra project autoscaling --project <id> [--json]
|
|
192
|
+
zenifra project autoscaling set --project <id> --min <n> --max <n> [--cpu <percent>] [--memory <percent>] [--json]
|
|
193
|
+
zenifra project autoscaling disable --project <id> [--json]
|
|
194
|
+
zenifra project autoscaling events --project <id> [--direction <scale_up|scale_down>] [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]
|
|
195
|
+
zenifra project billing usage --project <id> [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]
|
|
121
196
|
zenifra project instances --project <id> [--json]
|
|
122
197
|
zenifra project instances set --project <id> --count <n> [--json]
|
|
123
198
|
zenifra builds --project <id> [--page <n>] [--limit <n>] [--branch <name>] [--status <status>] [--json]
|
|
@@ -138,7 +213,7 @@ Run "zenifra help <command>" or "zenifra <command> --help" for command-specific
|
|
|
138
213
|
const HELP_SPECS = [
|
|
139
214
|
{
|
|
140
215
|
command: 'auth',
|
|
141
|
-
usage: 'zenifra auth\n zenifra auth login [--profile <name>] [--api-base <url>] [--code <code>]\n zenifra auth api-key --key <znf_key> [--profile <name>] [--api-base <url>]\n zenifra auth logout [--profile <name>]',
|
|
216
|
+
usage: 'zenifra auth\n zenifra auth login [--profile <name>] [--api-base <url>] [--code <code>]\n zenifra auth api-key --key <znf_key> [--profile <name>] [--api-base <url>]\n zenifra auth logout [--profile <name>] [--revoke]',
|
|
142
217
|
description: 'Gerencia a autenticacao dos perfis locais da CLI.',
|
|
143
218
|
examples: ['zenifra auth', 'zenifra auth login --profile staging', 'zenifra auth api-key --profile prod --key znf_0123456789abcdef01234567_abcd...'],
|
|
144
219
|
output: 'Zenifra CLI - auth',
|
|
@@ -168,11 +243,12 @@ const HELP_SPECS = [
|
|
|
168
243
|
},
|
|
169
244
|
{
|
|
170
245
|
command: 'auth logout',
|
|
171
|
-
usage: 'zenifra auth logout [--profile <name>]',
|
|
246
|
+
usage: 'zenifra auth logout [--profile <name>] [--revoke]',
|
|
172
247
|
description: 'Remove a autenticacao do perfil ativo ou do perfil informado.',
|
|
173
|
-
flags: ['--profile <name> Limpa outro perfil sem trocar o perfil ativo.'],
|
|
174
|
-
examples: ['zenifra auth logout', 'zenifra auth logout --profile staging'],
|
|
248
|
+
flags: ['--profile <name> Limpa outro perfil sem trocar o perfil ativo.', '--revoke Revoga as sessoes de usuario no servidor antes de limpar o perfil.'],
|
|
249
|
+
examples: ['zenifra auth logout', 'zenifra auth logout --profile staging', 'zenifra auth logout --revoke'],
|
|
175
250
|
output: 'Autenticacao removida do perfil active.',
|
|
251
|
+
notes: ['Sem --revoke, remove somente a credencial local. A revogacao invalida as sessoes de login do usuario e nao se aplica a API keys.'],
|
|
176
252
|
},
|
|
177
253
|
{
|
|
178
254
|
command: 'profile',
|
|
@@ -263,16 +339,65 @@ const HELP_SPECS = [
|
|
|
263
339
|
},
|
|
264
340
|
{
|
|
265
341
|
command: 'plans',
|
|
266
|
-
usage: 'zenifra plans [--type <all|http|database|storage>] [--json]',
|
|
267
|
-
description: 'Lista os catalogos publicos de preco de planos HTTP, banco e
|
|
268
|
-
flags: ['--type <type> Filtra o catalogo: all, http, database ou
|
|
269
|
-
examples: ['zenifra plans', 'zenifra plans --type http', 'zenifra plans --type
|
|
342
|
+
usage: 'zenifra plans [--type <all|http|database|storage|valkey>] [--json]',
|
|
343
|
+
description: 'Lista os catalogos publicos de preco de planos HTTP, banco, armazenamento e Valkey.',
|
|
344
|
+
flags: ['--type <type> Filtra o catalogo: all, http, database, storage, valkey, key-value, cache ou queue.', '--json Imprime a resposta em JSON.'],
|
|
345
|
+
examples: ['zenifra plans', 'zenifra plans --type http', 'zenifra plans --type valkey --json'],
|
|
270
346
|
output: 'HTTP\nPlano Hora Mes Ano Recursos\nfree R$ 0,00 R$ 0,00 R$ 0,00 1 GB Armazenamento Efemero',
|
|
271
347
|
jsonOutput: '{"http":[{"plan":"free","prices":{"hourly":0,"monthly":0,"yearly":0},"features":["1 GB Armazenamento Efemero"]}],"database":[],"storage":[]}',
|
|
272
348
|
},
|
|
349
|
+
{
|
|
350
|
+
command: 'valkey',
|
|
351
|
+
usage: 'zenifra valkey\n zenifra valkey status --project <id> [--json]\n zenifra valkey connection --project <id> [--json]\n zenifra valkey credentials rotate --project <id> [--idempotency-key <key>] [--wait] [--interval <seconds>] [--timeout <seconds>] [--json]\n zenifra valkey credentials status --project <id> --operation <id> [--json]',
|
|
352
|
+
description: 'Consulta projetos Valkey e gerencia suas conexoes e credenciais.',
|
|
353
|
+
examples: ['zenifra valkey status --project proj_1', 'zenifra valkey connection --project proj_1', 'zenifra valkey credentials rotate --project proj_1 --wait'],
|
|
354
|
+
output: 'Zenifra CLI - valkey',
|
|
355
|
+
notes: ['A string de conexao completa aparece somente quando uma criacao ou rotacao a disponibiliza.'],
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
command: 'valkey status',
|
|
359
|
+
usage: 'zenifra valkey status --project <id> [--json]',
|
|
360
|
+
description: 'Mostra estado, perfil, versao e persistencia de um projeto Valkey.',
|
|
361
|
+
flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.'],
|
|
362
|
+
examples: ['zenifra valkey status --project proj_1'],
|
|
363
|
+
output: 'Status Perfil Versao Persistente Instancias\nrunning cache 9.1.1 nao 1',
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
command: 'valkey connection',
|
|
367
|
+
usage: 'zenifra valkey connection --project <id> [--json]',
|
|
368
|
+
description: 'Mostra os dados de conexao de um projeto Valkey sem revelar a credencial.',
|
|
369
|
+
flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.'],
|
|
370
|
+
examples: ['zenifra valkey connection --project proj_1'],
|
|
371
|
+
output: 'Host Porta TLS Conexao\nvalkey.example.com 6380 sim valkeys://default:********@valkey.example.com:6380/0',
|
|
372
|
+
notes: ['Use a credencial recebida na criacao ou em uma rotacao concluida.'],
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
command: 'valkey credentials',
|
|
376
|
+
usage: 'zenifra valkey credentials\n zenifra valkey credentials rotate --project <id> [--idempotency-key <key>] [--wait] [--interval <seconds>] [--timeout <seconds>] [--json]\n zenifra valkey credentials status --project <id> --operation <id> [--json]',
|
|
377
|
+
description: 'Agrupa comandos de renovacao e acompanhamento de credenciais Valkey.',
|
|
378
|
+
examples: ['zenifra valkey credentials rotate --project proj_1', 'zenifra valkey credentials status --project proj_1 --operation operation_1'],
|
|
379
|
+
output: 'Zenifra CLI - valkey credentials',
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
command: 'valkey credentials rotate',
|
|
383
|
+
usage: 'zenifra valkey credentials rotate --project <id> [--idempotency-key <key>] [--wait] [--interval <seconds>] [--timeout <seconds>] [--json]',
|
|
384
|
+
description: 'Solicita a renovacao da credencial e retorna uma operacao acompanhavel.',
|
|
385
|
+
flags: ['--project <id> ID do projeto.', '--idempotency-key <key> Chave para repetir a mesma solicitacao.', '--wait Aguarda a conclusao.', '--interval <seconds> Intervalo do polling. Padrao: 2.', '--timeout <seconds> Timeout total. Padrao: 900.', '--json Imprime a resposta em JSON.'],
|
|
386
|
+
examples: ['zenifra valkey credentials rotate --project proj_1', 'zenifra valkey credentials rotate --project proj_1 --wait --timeout 120'],
|
|
387
|
+
output: 'Operacao aceita: rotation_123\nEstado: accepted',
|
|
388
|
+
notes: ['Salve a nova string de conexao quando a operacao atingir completed.'],
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
command: 'valkey credentials status',
|
|
392
|
+
usage: 'zenifra valkey credentials status --project <id> --operation <id> [--json]',
|
|
393
|
+
description: 'Consulta o estado de uma renovacao de credencial Valkey.',
|
|
394
|
+
flags: ['--project <id> ID do projeto.', '--operation <id> ID da operacao.', '--json Imprime a resposta em JSON.'],
|
|
395
|
+
examples: ['zenifra valkey credentials status --project proj_1 --operation rotation_123'],
|
|
396
|
+
output: 'Operacao: rotation_123\nEstado: completed',
|
|
397
|
+
},
|
|
273
398
|
{
|
|
274
399
|
command: 'projects',
|
|
275
|
-
usage: 'zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb>] [--page <n>] [--limit <n>]',
|
|
400
|
+
usage: 'zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb|valkey>] [--page <n>] [--limit <n>]',
|
|
276
401
|
description: 'Lista projetos da organizacao ativa ou da API key.',
|
|
277
402
|
flags: ['--json Imprime a resposta em JSON.', '--org <id> Usa uma organizacao especifica com sessao de usuario.', '--type <type> Filtra por tipo de projeto.', '--page <n> Pagina. Padrao: 1.', '--limit <n> Itens por pagina. Padrao: 15.'],
|
|
278
403
|
examples: ['zenifra projects --type http --page 1 --limit 15', 'zenifra projects --json'],
|
|
@@ -281,9 +406,9 @@ const HELP_SPECS = [
|
|
|
281
406
|
},
|
|
282
407
|
{
|
|
283
408
|
command: 'create project',
|
|
284
|
-
usage: 'zenifra create project\n zenifra create project --name <name> --plan <plan> --payment-mode <mode> --config <json|@file> [--description <text>] [--org <id>] [--json]',
|
|
409
|
+
usage: 'zenifra create project\n zenifra create project --name <name> --plan <plan> --payment-mode <mode> --config <json|@file> [--description <text>] [--idempotency-key <key>] [--org <id>] [--json]',
|
|
285
410
|
description: 'Cria um projeto via wizard interativo ou via payload de configuracao JSON.',
|
|
286
|
-
flags: ['--name <name>
|
|
411
|
+
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.', '--idempotency-key <key> Chave para repetir a mesma criacao com seguranca.', '--json Imprime a resposta em JSON.'],
|
|
287
412
|
examples: ['zenifra create project', 'zenifra create project --name api-web --plan free --payment-mode hourly --config @examples/http-project.json'],
|
|
288
413
|
output: 'Campo Valor\n------- --------------------------------------\nProjeto 507f1f77bcf86cd799439012\nDominio https://api-web.client.zenifra.com',
|
|
289
414
|
jsonOutput: '{"status":"success","data":{"id":"507f1f77bcf86cd799439012","name":"api-web"}}',
|
|
@@ -291,11 +416,11 @@ const HELP_SPECS = [
|
|
|
291
416
|
},
|
|
292
417
|
{
|
|
293
418
|
command: 'project',
|
|
294
|
-
usage: 'zenifra project\n zenifra project info --project <id> [--json]\n zenifra project url --project <id> [--json]\n zenifra project logs --project <id> [--instance <id>] [--json]\n zenifra project metrics --project <id> [--instance <id>] [--json]\n zenifra project network --project <id> [--view <summary|status-codes|routes|user-agents|request-events|source-ips>] [--json]\n zenifra project image set --project <id> --image <image> [--json]\n zenifra project envs --project <id> [--json] [--show-values]\n zenifra project env add --project <id> --name <name> --value <value> [--json]\n zenifra project env update --project <id> --name <name> --value <value> [--json]\n zenifra project env remove --project <id> --name <name> [--json]\n zenifra project instances --project <id> [--json]\n zenifra project instances set --project <id> --count <n> [--json]',
|
|
419
|
+
usage: 'zenifra project\n zenifra project info --project <id> [--json]\n zenifra project url --project <id> [--json]\n zenifra project logs --project <id> [--instance <id>] [--json]\n zenifra project metrics --project <id> [--instance <id>] [--json]\n zenifra project network --project <id> [--view <summary|status-codes|routes|user-agents|request-events|source-ips>] [--json]\n zenifra project image set --project <id> --image <image> [--json]\n zenifra project exposure set --project <id> --exposure <public|private> [--json]\n zenifra project envs --project <id> [--json] [--show-values]\n zenifra project env add --project <id> --name <name> --value <value> [--json]\n zenifra project env update --project <id> --name <name> --value <value> [--json]\n zenifra project env remove --project <id> --name <name> [--json]\n zenifra project autoscaling --project <id> [--json]\n zenifra project autoscaling set --project <id> --min <n> --max <n> [--cpu <percent>] [--memory <percent>] [--json]\n zenifra project autoscaling disable --project <id> [--json]\n zenifra project autoscaling events --project <id> [--direction <scale_up|scale_down>] [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]\n zenifra project billing usage --project <id> [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]\n zenifra project instances --project <id> [--json]\n zenifra project instances set --project <id> --count <n> [--json]',
|
|
295
420
|
description: 'Agrupa comandos operacionais e de introspecao sobre um projeto especifico.',
|
|
296
421
|
examples: ['zenifra project', 'zenifra project info --project proj_1', 'zenifra project env add --project proj_1 --name NODE_ENV --value production'],
|
|
297
422
|
output: 'Zenifra CLI - project',
|
|
298
|
-
notes: ['Use "zenifra help project <subcomando>" para detalhes de info, url, logs, metrics, network, image, envs e instances.'],
|
|
423
|
+
notes: ['Use "zenifra help project <subcomando>" para detalhes de info, url, logs, metrics, network, image, exposure, autoscaling, billing, envs e instances.'],
|
|
299
424
|
},
|
|
300
425
|
{
|
|
301
426
|
command: 'project info',
|
|
@@ -352,6 +477,16 @@ const HELP_SPECS = [
|
|
|
352
477
|
output: 'Imagem atualizada com sucesso.',
|
|
353
478
|
jsonOutput: '{"status":"success","message":"updated with success"}',
|
|
354
479
|
},
|
|
480
|
+
{
|
|
481
|
+
command: 'project exposure set',
|
|
482
|
+
usage: 'zenifra project exposure set --project <id> --exposure <public|private> [--json]',
|
|
483
|
+
description: 'Altera se um projeto HTTP expoe rota publica e dominio.',
|
|
484
|
+
flags: ['--project <id> ID do projeto.', '--exposure <mode> Use public para criar rota publica ou private para remover exposicao.', '--json Imprime a resposta em JSON.'],
|
|
485
|
+
examples: ['zenifra project exposure set --project 507f1f77bcf86cd799439012 --exposure private', 'zenifra project exposure set --project 507f1f77bcf86cd799439012 --exposure public --json'],
|
|
486
|
+
output: 'Campo Valor\n--------- ----------------------------------\nExposicao private\nDominio -',
|
|
487
|
+
jsonOutput: '{"status":"success","message":"project exposure updated with success","exposure":"private","custom_domains":[]}',
|
|
488
|
+
notes: ['Ao mudar para private, a API remove a rota publica e dominios personalizados do projeto.'],
|
|
489
|
+
},
|
|
355
490
|
{
|
|
356
491
|
command: 'project envs',
|
|
357
492
|
usage: 'zenifra project envs --project <id> [--json] [--show-values]',
|
|
@@ -392,10 +527,55 @@ const HELP_SPECS = [
|
|
|
392
527
|
jsonOutput: '{"status":"success","message":"updated with success","envs":[]}',
|
|
393
528
|
notes: ['Falha se a variavel nao existir.'],
|
|
394
529
|
},
|
|
530
|
+
{
|
|
531
|
+
command: 'project autoscaling',
|
|
532
|
+
usage: 'zenifra project autoscaling --project <id> [--json]',
|
|
533
|
+
description: 'Mostra a configuracao de auto-scaling HTTP do projeto.',
|
|
534
|
+
flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.'],
|
|
535
|
+
examples: ['zenifra project autoscaling --project 507f1f77bcf86cd799439012'],
|
|
536
|
+
output: 'Campo Valor\n---------- -----\nStatus ativo\nMinimo 2\nMaximo 8\nCPU alvo 70%\nMemoria 80%',
|
|
537
|
+
jsonOutput: '{"enabled":true,"min_instances":2,"max_instances":8,"target_cpu_utilization_percent":70,"target_memory_utilization_percent":80}',
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
command: 'project autoscaling set',
|
|
541
|
+
usage: 'zenifra project autoscaling set --project <id> --min <n> --max <n> [--cpu <percent>] [--memory <percent>] [--json]',
|
|
542
|
+
description: 'Ativa ou atualiza o auto-scaling HTTP do projeto.',
|
|
543
|
+
flags: ['--project <id> ID do projeto.', '--min <n> Minimo de instancias reservadas.', '--max <n> Maximo de instancias permitidas.', '--cpu <percent> CPU alvo. Padrao da API: 70.', '--memory <percent> Memoria alvo. Padrao da API: 80.', '--json Imprime a resposta em JSON.'],
|
|
544
|
+
examples: ['zenifra project autoscaling set --project 507f1f77bcf86cd799439012 --min 2 --max 8 --cpu 70 --memory 80'],
|
|
545
|
+
output: 'Auto-scaling atualizado: 2-8 instancias.',
|
|
546
|
+
jsonOutput: '{"status":"success","message":"project autoscaling updated with success","data":{"autoscaling":{"enabled":true,"min_instances":2,"max_instances":8}}}',
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
command: 'project autoscaling disable',
|
|
550
|
+
usage: 'zenifra project autoscaling disable --project <id> [--json]',
|
|
551
|
+
description: 'Desativa o auto-scaling HTTP e volta para instancias fixas no minimo configurado.',
|
|
552
|
+
flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.'],
|
|
553
|
+
examples: ['zenifra project autoscaling disable --project 507f1f77bcf86cd799439012'],
|
|
554
|
+
output: 'Auto-scaling desativado.',
|
|
555
|
+
jsonOutput: '{"status":"success","message":"project autoscaling updated with success","data":{"autoscaling":{"enabled":false}}}',
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
command: 'project autoscaling events',
|
|
559
|
+
usage: 'zenifra project autoscaling events --project <id> [--direction <scale_up|scale_down>] [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]',
|
|
560
|
+
description: 'Lista o historico de scale up e scale down do auto-scaling HTTP.',
|
|
561
|
+
flags: ['--project <id> ID do projeto.', '--direction <valor> Filtra por scale_up ou scale_down.', '--from <iso> Inicio do periodo.', '--to <iso> Fim do periodo.', '--page <n> Pagina. Padrao da API: 1.', '--limit <n> Itens por pagina. Maximo da API: 100.', '--json Imprime a resposta em JSON.'],
|
|
562
|
+
examples: ['zenifra project autoscaling events --project 507f1f77bcf86cd799439012 --direction scale_up'],
|
|
563
|
+
output: 'Quando Direcao Instancias Limites Motivo\n05/06/2026, 18:40 aumento 2 -> 5 CPU 91%/70% capacidade aumentada',
|
|
564
|
+
jsonOutput: '{"events":[{"direction":"scale_up","previous_instances":2,"new_instances":5,"occurred_at":"2026-06-05T21:40:00.000Z"}],"pagination":{"page":1,"limit":10,"total":1,"total_pages":1}}',
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
command: 'project billing usage',
|
|
568
|
+
usage: 'zenifra project billing usage --project <id> [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]',
|
|
569
|
+
description: 'Lista o consumo horario consolidado de um projeto.',
|
|
570
|
+
flags: ['--project <id> ID do projeto.', '--from <iso> Inicio do periodo.', '--to <iso> Fim do periodo.', '--page <n> Pagina. Padrao da API: 1.', '--limit <n> Itens por pagina. Maximo: 50.', '--json Imprime a resposta em JSON.'],
|
|
571
|
+
examples: ['zenifra project billing usage --project 507f1f77bcf86cd799439012 --from 2026-06-01T00:00:00Z --limit 20'],
|
|
572
|
+
output: 'Inicio Fim Instancia-horas Armazenamento Total\n2026-06-01T10:00:00.000Z 2026-06-01T11:00:00.000Z 2 1.5 GB-h R$ 0,30',
|
|
573
|
+
jsonOutput: '{"hours":[],"summary":{"currency":"brl","compute_amount":0,"storage_amount":0,"total_amount":0},"pagination":{"page":1,"limit":10,"total":0,"total_pages":0}}',
|
|
574
|
+
},
|
|
395
575
|
{
|
|
396
576
|
command: 'project instances',
|
|
397
577
|
usage: 'zenifra project instances --project <id> [--json]',
|
|
398
|
-
description: 'Lista instancias
|
|
578
|
+
description: 'Lista instancias do projeto.',
|
|
399
579
|
flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.'],
|
|
400
580
|
examples: ['zenifra project instances --project 507f1f77bcf86cd799439012'],
|
|
401
581
|
output: 'Instancia\nweb-1',
|
|
@@ -426,11 +606,11 @@ const HELP_SPECS = [
|
|
|
426
606
|
flags: [
|
|
427
607
|
'--project <id> ID do projeto.',
|
|
428
608
|
'--build <id> ID do build.',
|
|
429
|
-
'--cursor <n> Cursor inicial. Padrao: 0.',
|
|
430
|
-
'--limit <n> Quantidade de logs por request. Padrao: 200.',
|
|
609
|
+
'--cursor <n> Cursor inicial inteiro, maior ou igual a 0. Padrao: 0.',
|
|
610
|
+
'--limit <n> Quantidade de logs por request, entre 1 e 500. Padrao: 200.',
|
|
431
611
|
'--follow Continua em polling ate o build finalizar.',
|
|
432
|
-
'--interval <seconds> Intervalo de polling com --follow. Padrao: 5.',
|
|
433
|
-
'--timeout <seconds> Timeout total com --follow. Padrao: 900.',
|
|
612
|
+
'--interval <seconds> Intervalo de polling com --follow, minimo 0.1. Padrao: 5.',
|
|
613
|
+
'--timeout <seconds> Timeout total com --follow, minimo 1. Padrao: 900.',
|
|
434
614
|
'--json Imprime a resposta em JSON.',
|
|
435
615
|
],
|
|
436
616
|
examples: ['zenifra builds logs --project 507f1f77bcf86cd799439012 --build build_123', 'zenifra builds logs --project 507f1f77bcf86cd799439012 --build build_123 --follow'],
|
|
@@ -466,7 +646,7 @@ const HELP_SPECS = [
|
|
|
466
646
|
command: 'deploy watch',
|
|
467
647
|
usage: 'zenifra deploy watch --project <id> --build <id> [--interval <seconds>] [--timeout <seconds>] [--json]',
|
|
468
648
|
description: 'Acompanha um build em tempo real, fazendo polling do status e imprimindo os logs incrementais do build ate o estado terminal.',
|
|
469
|
-
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.'],
|
|
649
|
+
flags: ['--project <id> ID do projeto.', '--build <id> ID do build.', '--interval <seconds> Intervalo de polling, minimo 0.1. Padrao: 5.', '--timeout <seconds> Timeout total, minimo 1. Padrao: 900.', '--json Imprime cada resposta em JSON.'],
|
|
470
650
|
examples: [
|
|
471
651
|
'zenifra deploy watch --project 507f1f77bcf86cd799439012 --build build_123',
|
|
472
652
|
'zenifra deploy watch --project 507f1f77bcf86cd799439012 --build build_123 --interval 2',
|
|
@@ -543,7 +723,7 @@ function removedProjectsCreateMessage() {
|
|
|
543
723
|
}
|
|
544
724
|
|
|
545
725
|
function isNamespaceCommand(command) {
|
|
546
|
-
return ['auth', 'profile', 'project', 'org'].includes(command);
|
|
726
|
+
return ['auth', 'profile', 'project', 'org', 'valkey'].includes(command);
|
|
547
727
|
}
|
|
548
728
|
|
|
549
729
|
function parseArgs(argv) {
|
|
@@ -578,6 +758,17 @@ function parseArgs(argv) {
|
|
|
578
758
|
return { positional, flags };
|
|
579
759
|
}
|
|
580
760
|
|
|
761
|
+
function flagNameForDisplay(value) {
|
|
762
|
+
return `--${String(value).replace(/[A-Z]/g, (character) => `-${character.toLowerCase()}`)}`;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function validateKnownFlags(flags) {
|
|
766
|
+
const unknown = Object.keys(flags).filter((name) => !KNOWN_FLAG_NAMES.has(name));
|
|
767
|
+
if (unknown.length > 0) {
|
|
768
|
+
throw new CliError(`Flag desconhecida: ${unknown.map(flagNameForDisplay).join(', ')}.`);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
581
772
|
function emptyProfileStore() {
|
|
582
773
|
return {
|
|
583
774
|
version: PROFILE_STORE_VERSION,
|
|
@@ -667,7 +858,9 @@ async function migrateLegacySession() {
|
|
|
667
858
|
});
|
|
668
859
|
store.profiles[DEFAULT_PROFILE_NAME] = migratedProfile;
|
|
669
860
|
store.activeProfile = DEFAULT_PROFILE_NAME;
|
|
670
|
-
|
|
861
|
+
const normalized = await writeProfileStore(store);
|
|
862
|
+
await rm(SESSION_FILE, { force: true });
|
|
863
|
+
return normalized;
|
|
671
864
|
}
|
|
672
865
|
|
|
673
866
|
async function readProfileStore() {
|
|
@@ -861,9 +1054,16 @@ function estimateWizardTotal(state = {}) {
|
|
|
861
1054
|
if (state.httpStoragePersistent) total += 1;
|
|
862
1055
|
}
|
|
863
1056
|
|
|
1057
|
+
if (state.planAllowsAutoscaling) {
|
|
1058
|
+
total += 1;
|
|
1059
|
+
if (state.httpAutoscalingEnabled) total += 3;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
864
1062
|
total += 1;
|
|
865
1063
|
if (state.envs?.enabled) total += state.envs.count * 3;
|
|
866
1064
|
|
|
1065
|
+
total += 1;
|
|
1066
|
+
|
|
867
1067
|
if (state.httpSource === 'github') {
|
|
868
1068
|
total += 9;
|
|
869
1069
|
} else if (state.httpSource === 'oci') {
|
|
@@ -877,13 +1077,13 @@ function estimateWizardTotal(state = {}) {
|
|
|
877
1077
|
total += 1;
|
|
878
1078
|
}
|
|
879
1079
|
|
|
880
|
-
if (state.planAllowsBlockIp) {
|
|
1080
|
+
if (state.httpExposure !== 'private' && state.planAllowsBlockIp) {
|
|
881
1081
|
total += 2;
|
|
882
1082
|
if (state.whitelist?.enabled) total += state.whitelist.count * 3;
|
|
883
1083
|
if (state.blacklist?.enabled) total += state.blacklist.count * 3;
|
|
884
1084
|
}
|
|
885
1085
|
|
|
886
|
-
if (state.planAllowsSubdomain) total += 1;
|
|
1086
|
+
if (state.httpExposure !== 'private' && state.planAllowsSubdomain) total += 1;
|
|
887
1087
|
return total;
|
|
888
1088
|
}
|
|
889
1089
|
|
|
@@ -898,6 +1098,11 @@ function estimateWizardTotal(state = {}) {
|
|
|
898
1098
|
return total;
|
|
899
1099
|
}
|
|
900
1100
|
|
|
1101
|
+
if (typeProject === 'valkey') {
|
|
1102
|
+
total += 3;
|
|
1103
|
+
return total;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
901
1106
|
return 18;
|
|
902
1107
|
}
|
|
903
1108
|
|
|
@@ -1199,8 +1404,25 @@ async function promptHidden(question) {
|
|
|
1199
1404
|
});
|
|
1200
1405
|
}
|
|
1201
1406
|
|
|
1202
|
-
|
|
1203
|
-
const
|
|
1407
|
+
function httpTimeoutMs() {
|
|
1408
|
+
const configured = process.env.ZENIFRA_HTTP_TIMEOUT_MS;
|
|
1409
|
+
if (configured === undefined) return DEFAULT_HTTP_TIMEOUT_MS;
|
|
1410
|
+
|
|
1411
|
+
const value = Number(configured);
|
|
1412
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
1413
|
+
throw new CliError('ZENIFRA_HTTP_TIMEOUT_MS deve ser um inteiro positivo em milissegundos.');
|
|
1414
|
+
}
|
|
1415
|
+
return value;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
async function request(session, flags, method, path, {
|
|
1419
|
+
body,
|
|
1420
|
+
orgId,
|
|
1421
|
+
tokenRequired = true,
|
|
1422
|
+
credential: credentialOverride,
|
|
1423
|
+
headers: extraHeaders = {},
|
|
1424
|
+
} = {}) {
|
|
1425
|
+
const credential = credentialOverride || resolveCredential(session);
|
|
1204
1426
|
if (tokenRequired && !credential) {
|
|
1205
1427
|
throw new CliError('Voce precisa autenticar primeiro: zenifra auth login, zenifra auth api-key --key <znf_key> ou ZENIFRA_API_KEY.');
|
|
1206
1428
|
}
|
|
@@ -1209,6 +1431,14 @@ async function request(session, flags, method, path, { body, orgId, tokenRequire
|
|
|
1209
1431
|
if (body !== undefined) {
|
|
1210
1432
|
headers['Content-Type'] = 'application/json';
|
|
1211
1433
|
}
|
|
1434
|
+
for (const [name, value] of Object.entries(extraHeaders)) {
|
|
1435
|
+
if (value === undefined) continue;
|
|
1436
|
+
const normalizedName = name.toLowerCase();
|
|
1437
|
+
if (['authorization', 'x-organization-id', 'content-type'].includes(normalizedName)) {
|
|
1438
|
+
throw new CliError(`Header nao permitido: ${name}.`);
|
|
1439
|
+
}
|
|
1440
|
+
headers[name] = String(value);
|
|
1441
|
+
}
|
|
1212
1442
|
if (credential?.token) {
|
|
1213
1443
|
headers.Authorization = `Bearer ${credential.token}`;
|
|
1214
1444
|
}
|
|
@@ -1216,11 +1446,21 @@ async function request(session, flags, method, path, { body, orgId, tokenRequire
|
|
|
1216
1446
|
headers['x-organization-id'] = orgId;
|
|
1217
1447
|
}
|
|
1218
1448
|
|
|
1219
|
-
const
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1449
|
+
const timeoutMs = httpTimeoutMs();
|
|
1450
|
+
let response;
|
|
1451
|
+
try {
|
|
1452
|
+
response = await fetch(`${apiBaseUrl(session, flags)}${path}`, {
|
|
1453
|
+
method,
|
|
1454
|
+
headers,
|
|
1455
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
1456
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
1457
|
+
});
|
|
1458
|
+
} catch (error) {
|
|
1459
|
+
if (error?.name === 'TimeoutError' || error?.name === 'AbortError') {
|
|
1460
|
+
throw new CliError(`A API da Zenifra nao respondeu em ate ${timeoutMs} ms.`);
|
|
1461
|
+
}
|
|
1462
|
+
throw new CliError(`Nao foi possivel conectar a API da Zenifra: ${error?.message || 'erro de rede'}.`);
|
|
1463
|
+
}
|
|
1224
1464
|
|
|
1225
1465
|
const text = await response.text();
|
|
1226
1466
|
let payload = {};
|
|
@@ -1234,12 +1474,18 @@ async function request(session, flags, method, path, { body, orgId, tokenRequire
|
|
|
1234
1474
|
|
|
1235
1475
|
if (!response.ok) {
|
|
1236
1476
|
const message = payload?.message || payload?.error || `Zenifra API retornou HTTP ${response.status}`;
|
|
1237
|
-
if (response.status === 401
|
|
1477
|
+
if (response.status === 401) {
|
|
1238
1478
|
if (credential?.type === 'api_key') {
|
|
1239
|
-
throw new CliError(`${message}. Verifique se a API key esta ativa
|
|
1479
|
+
throw new CliError(`${message}. Verifique se a API key esta ativa e se o IP atual esta permitido.`);
|
|
1240
1480
|
}
|
|
1241
1481
|
throw new CliError(`${message}. Rode "zenifra auth login" para renovar sua sessao.`);
|
|
1242
1482
|
}
|
|
1483
|
+
if (response.status === 403) {
|
|
1484
|
+
throw new CliError(message);
|
|
1485
|
+
}
|
|
1486
|
+
if (response.status === 429 && Number.isFinite(Number(payload?.retry_after_seconds))) {
|
|
1487
|
+
throw new CliError(`${message} Tente novamente em ${Number(payload.retry_after_seconds)} segundo(s).`);
|
|
1488
|
+
}
|
|
1243
1489
|
if (String(message).includes('missing x-organization-id')) {
|
|
1244
1490
|
throw new CliError('Organizacao nao selecionada. Rode "zenifra org set" ou use --org <id>.');
|
|
1245
1491
|
}
|
|
@@ -1345,19 +1591,33 @@ function printEnvs(envs, flags) {
|
|
|
1345
1591
|
}
|
|
1346
1592
|
|
|
1347
1593
|
function printProject(project) {
|
|
1348
|
-
const
|
|
1594
|
+
const managedService = project.managed_service;
|
|
1595
|
+
const isValkey = project.type_project === 'valkey' || managedService?.engine === 'valkey';
|
|
1349
1596
|
const rows = [
|
|
1350
1597
|
{ label: 'ID', value: projectIdOf(project) || '-' },
|
|
1351
1598
|
{ label: 'Nome', value: project.name || '-' },
|
|
1352
1599
|
{ label: 'Status', value: project.status || '-' },
|
|
1353
1600
|
{ label: 'Tipo', value: project.type_project || '-' },
|
|
1354
1601
|
{ label: 'Plano', value: project.plan || '-' },
|
|
1355
|
-
{ label: 'URL', value: url || '-' },
|
|
1356
|
-
{ label: 'Imagem', value: project.image || '-' },
|
|
1357
|
-
{ label: 'Instancias', value: project.instances ?? '-' },
|
|
1358
|
-
{ label: 'Instancias atuais', value: project.additional_info?.current_instances ?? '-' },
|
|
1359
1602
|
];
|
|
1360
1603
|
|
|
1604
|
+
if (isValkey) {
|
|
1605
|
+
rows.push(
|
|
1606
|
+
{ label: 'Perfil', value: managedService?.profile || '-' },
|
|
1607
|
+
{ label: 'Versao', value: managedService?.version || '-' },
|
|
1608
|
+
{ label: 'Persistente', value: managedService?.topology?.persistent === true ? 'sim' : 'nao' },
|
|
1609
|
+
{ label: 'Instancias', value: project.instances ?? '-' },
|
|
1610
|
+
);
|
|
1611
|
+
} else {
|
|
1612
|
+
rows.push(
|
|
1613
|
+
{ label: 'Exposicao', value: project.exposure || '-' },
|
|
1614
|
+
{ label: 'URL', value: projectUrlOf(project) || '-' },
|
|
1615
|
+
{ label: 'Imagem', value: project.image || '-' },
|
|
1616
|
+
{ label: 'Instancias', value: project.instances ?? '-' },
|
|
1617
|
+
{ label: 'Instancias atuais', value: project.additional_info?.current_instances ?? '-' },
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1361
1621
|
for (const row of rows) {
|
|
1362
1622
|
process.stdout.write(`${row.label}: ${row.value}\n`);
|
|
1363
1623
|
}
|
|
@@ -1507,6 +1767,18 @@ async function handleLogout(session, flags) {
|
|
|
1507
1767
|
throw new CliError('Nenhum perfil ativo configurado. Crie um perfil com "zenifra profile add" ou autentique com "zenifra auth login".');
|
|
1508
1768
|
}
|
|
1509
1769
|
const target = requireExistingProfile(session, targetName);
|
|
1770
|
+
if (flags.revoke) {
|
|
1771
|
+
if (!target.accessToken) {
|
|
1772
|
+
throw new CliError('A revogacao exige um perfil autenticado por login de usuario; API keys devem ser revogadas na organizacao.');
|
|
1773
|
+
}
|
|
1774
|
+
await request(target, flags, 'DELETE', '/authentication', {
|
|
1775
|
+
credential: {
|
|
1776
|
+
token: target.accessToken,
|
|
1777
|
+
type: 'user',
|
|
1778
|
+
source: 'profile',
|
|
1779
|
+
},
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1510
1782
|
target.accessToken = undefined;
|
|
1511
1783
|
target.apiKey = undefined;
|
|
1512
1784
|
target.selectedOrganizationId = undefined;
|
|
@@ -1516,7 +1788,7 @@ async function handleLogout(session, flags) {
|
|
|
1516
1788
|
if (target.__profileName === getProfileName(session)) {
|
|
1517
1789
|
Object.assign(session, buildSession(target.__store));
|
|
1518
1790
|
}
|
|
1519
|
-
process.stdout.write(`Autenticacao removida do perfil ${target.__profileName}.\n`);
|
|
1791
|
+
process.stdout.write(`Autenticacao removida do perfil ${target.__profileName}${flags.revoke ? ' e sessoes de usuario revogadas' : ''}.\n`);
|
|
1520
1792
|
}
|
|
1521
1793
|
|
|
1522
1794
|
function maskApiKey(value) {
|
|
@@ -1761,6 +2033,26 @@ function normalizePlan(value) {
|
|
|
1761
2033
|
['dbpremium', 'db-premium'],
|
|
1762
2034
|
['db_enterprise', 'db-enterprise'],
|
|
1763
2035
|
['dbenterprise', 'db-enterprise'],
|
|
2036
|
+
['cache_free', 'cache-free'],
|
|
2037
|
+
['cachefree', 'cache-free'],
|
|
2038
|
+
['cache_starter', 'cache-starter'],
|
|
2039
|
+
['cachestarter', 'cache-starter'],
|
|
2040
|
+
['cache_basic', 'cache-basic'],
|
|
2041
|
+
['cachebasic', 'cache-basic'],
|
|
2042
|
+
['cache_premium', 'cache-premium'],
|
|
2043
|
+
['cachepremium', 'cache-premium'],
|
|
2044
|
+
['cache_enterprise', 'cache-enterprise'],
|
|
2045
|
+
['cacheenterprise', 'cache-enterprise'],
|
|
2046
|
+
['queue_free', 'queue-free'],
|
|
2047
|
+
['queuefree', 'queue-free'],
|
|
2048
|
+
['queue_starter', 'queue-starter'],
|
|
2049
|
+
['queuestarter', 'queue-starter'],
|
|
2050
|
+
['queue_basic', 'queue-basic'],
|
|
2051
|
+
['queuebasic', 'queue-basic'],
|
|
2052
|
+
['queue_premium', 'queue-premium'],
|
|
2053
|
+
['queuepremium', 'queue-premium'],
|
|
2054
|
+
['queue_enterprise', 'queue-enterprise'],
|
|
2055
|
+
['queueenterprise', 'queue-enterprise'],
|
|
1764
2056
|
]);
|
|
1765
2057
|
|
|
1766
2058
|
if (aliases.has(normalized)) return aliases.get(normalized);
|
|
@@ -1797,6 +2089,37 @@ function normalizeTypeProject(value) {
|
|
|
1797
2089
|
['postgresql', 'postgresql'],
|
|
1798
2090
|
['mariadb', 'mariadb'],
|
|
1799
2091
|
['maria db', 'mariadb'],
|
|
2092
|
+
['valkey', 'valkey'],
|
|
2093
|
+
['key value', 'valkey'],
|
|
2094
|
+
['key-value', 'valkey'],
|
|
2095
|
+
]);
|
|
2096
|
+
return aliases.get(raw) || raw;
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
function normalizeValkeyProfile(value) {
|
|
2100
|
+
const raw = normalizeFreeText(value);
|
|
2101
|
+
const aliases = new Map([
|
|
2102
|
+
['key_value', 'key_value'],
|
|
2103
|
+
['key value', 'key_value'],
|
|
2104
|
+
['key-value', 'key_value'],
|
|
2105
|
+
['cache', 'cache'],
|
|
2106
|
+
['queue', 'queue'],
|
|
2107
|
+
]);
|
|
2108
|
+
return aliases.get(raw) || raw;
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
function normalizeExposure(value) {
|
|
2112
|
+
const raw = normalizeFreeText(value);
|
|
2113
|
+
const aliases = new Map([
|
|
2114
|
+
['public', 'public'],
|
|
2115
|
+
['publico', 'public'],
|
|
2116
|
+
['público', 'public'],
|
|
2117
|
+
['exposto', 'public'],
|
|
2118
|
+
['sim', 'public'],
|
|
2119
|
+
['private', 'private'],
|
|
2120
|
+
['privado', 'private'],
|
|
2121
|
+
['nao', 'private'],
|
|
2122
|
+
['não', 'private'],
|
|
1800
2123
|
]);
|
|
1801
2124
|
return aliases.get(raw) || raw;
|
|
1802
2125
|
}
|
|
@@ -1842,6 +2165,13 @@ function isIpAccessEnabled(planCatalog, planId) {
|
|
|
1842
2165
|
return planHasFeature(planCatalog, planId, 'Bloqueio de IPs');
|
|
1843
2166
|
}
|
|
1844
2167
|
|
|
2168
|
+
function isAutoscalingEnabledForPlan(planCatalog, planId) {
|
|
2169
|
+
const plan = Array.isArray(planCatalog)
|
|
2170
|
+
? planCatalog.find((item) => item?.plan === planId)
|
|
2171
|
+
: undefined;
|
|
2172
|
+
return plan?.permissions?.allow_autoscaling === 'true';
|
|
2173
|
+
}
|
|
2174
|
+
|
|
1845
2175
|
function getMaxReplicasFromFeatures(features) {
|
|
1846
2176
|
const replicaFeature = features.find((feature) => /réplicas?/i.test(String(feature)));
|
|
1847
2177
|
if (!replicaFeature) return 5;
|
|
@@ -1876,7 +2206,6 @@ async function fetchWizardCatalogs(session, flags) {
|
|
|
1876
2206
|
unwrapData(await request(session, flags, 'GET', '/project/database/plans')),
|
|
1877
2207
|
'os planos de banco',
|
|
1878
2208
|
);
|
|
1879
|
-
|
|
1880
2209
|
return { httpPlans, availableInstances, databasePlans };
|
|
1881
2210
|
}
|
|
1882
2211
|
|
|
@@ -1885,6 +2214,10 @@ function normalizePlansCatalogType(value) {
|
|
|
1885
2214
|
if (!normalized || normalized === 'all') return 'all';
|
|
1886
2215
|
if (normalized === 'http') return 'http';
|
|
1887
2216
|
if (['database', 'db', 'postgresql', 'mariadb'].includes(normalized)) return 'database';
|
|
2217
|
+
if (normalized === 'valkey') return 'valkey';
|
|
2218
|
+
if (normalized === 'key-value' || normalized === 'key value' || normalized === 'key_value') return 'valkey-key_value';
|
|
2219
|
+
if (normalized === 'cache') return 'valkey-cache';
|
|
2220
|
+
if (normalized === 'queue') return 'valkey-queue';
|
|
1888
2221
|
if (normalized === 'storage') return 'storage';
|
|
1889
2222
|
return null;
|
|
1890
2223
|
}
|
|
@@ -1917,6 +2250,49 @@ function humanizeStorageName(value) {
|
|
|
1917
2250
|
return { storage: raw, type: '-' };
|
|
1918
2251
|
}
|
|
1919
2252
|
|
|
2253
|
+
const VALKEY_PROFILE_LABELS = {
|
|
2254
|
+
key_value: 'Key Value',
|
|
2255
|
+
cache: 'Cache',
|
|
2256
|
+
queue: 'Queue',
|
|
2257
|
+
};
|
|
2258
|
+
|
|
2259
|
+
function requireValkeyCatalog(value) {
|
|
2260
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
2261
|
+
|| value.engine !== 'valkey'
|
|
2262
|
+
|| typeof value.version !== 'string'
|
|
2263
|
+
|| typeof value.currency !== 'string'
|
|
2264
|
+
|| !Array.isArray(value.profiles)
|
|
2265
|
+
|| !Array.isArray(value.plans)
|
|
2266
|
+
|| value.profiles.length === 0
|
|
2267
|
+
|| value.plans.length === 0) {
|
|
2268
|
+
throw new CliError('Falha ao carregar o catalogo Valkey. Tente novamente mais tarde.');
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
const validProfiles = new Set(Object.keys(VALKEY_PROFILE_LABELS));
|
|
2272
|
+
if (value.profiles.some((profile) => !validProfiles.has(profile?.id) || typeof profile.persistence !== 'boolean')) {
|
|
2273
|
+
throw new CliError('Falha ao carregar o catalogo Valkey. Tente novamente mais tarde.');
|
|
2274
|
+
}
|
|
2275
|
+
if (value.plans.some((plan) => !validProfiles.has(plan?.profile) || typeof plan.id !== 'string' || !plan.prices || typeof plan.prices !== 'object')) {
|
|
2276
|
+
throw new CliError('Falha ao carregar o catalogo Valkey. Tente novamente mais tarde.');
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
return value;
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
function valkeyProfileFilter(type) {
|
|
2283
|
+
if (!type.startsWith('valkey-')) return null;
|
|
2284
|
+
return type.slice('valkey-'.length);
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
function filterValkeyCatalog(catalog, profile) {
|
|
2288
|
+
if (!profile) return catalog;
|
|
2289
|
+
return {
|
|
2290
|
+
...catalog,
|
|
2291
|
+
profiles: catalog.profiles.filter((entry) => entry.id === profile),
|
|
2292
|
+
plans: catalog.plans.filter((plan) => plan.profile === profile),
|
|
2293
|
+
};
|
|
2294
|
+
}
|
|
2295
|
+
|
|
1920
2296
|
async function fetchPlansCatalogs(session, flags, type) {
|
|
1921
2297
|
if (type === 'http') {
|
|
1922
2298
|
return {
|
|
@@ -1951,19 +2327,51 @@ async function fetchPlansCatalogs(session, flags, type) {
|
|
|
1951
2327
|
};
|
|
1952
2328
|
}
|
|
1953
2329
|
|
|
1954
|
-
|
|
2330
|
+
if (type === 'valkey' || type.startsWith('valkey-')) {
|
|
2331
|
+
const catalog = requireValkeyCatalog(
|
|
2332
|
+
unwrapData(await request(session, flags, 'GET', '/managed-services/catalog', { tokenRequired: false })),
|
|
2333
|
+
);
|
|
2334
|
+
return {
|
|
2335
|
+
http: [],
|
|
2336
|
+
database: [],
|
|
2337
|
+
storage: [],
|
|
2338
|
+
valkey: filterValkeyCatalog(catalog, valkeyProfileFilter(type)),
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
const [httpPayload, databasePayload, storagePayload, valkeyPayload] = await Promise.all([
|
|
1955
2343
|
request(session, flags, 'GET', '/project/plans', { tokenRequired: false }),
|
|
1956
2344
|
request(session, flags, 'GET', '/project/database/plans', { tokenRequired: false }),
|
|
1957
2345
|
request(session, flags, 'GET', '/project/storage/plans', { tokenRequired: false }),
|
|
2346
|
+
request(session, flags, 'GET', '/managed-services/catalog', { tokenRequired: false }),
|
|
1958
2347
|
]);
|
|
1959
2348
|
|
|
1960
2349
|
return {
|
|
1961
2350
|
http: requireWizardCatalogArray(unwrapData(httpPayload), 'os planos HTTP'),
|
|
1962
2351
|
database: requireWizardCatalogArray(unwrapData(databasePayload), 'os planos de banco'),
|
|
1963
2352
|
storage: requireWizardCatalogArray(unwrapData(storagePayload), 'os planos de storage'),
|
|
2353
|
+
valkey: requireValkeyCatalog(unwrapData(valkeyPayload)),
|
|
1964
2354
|
};
|
|
1965
2355
|
}
|
|
1966
2356
|
|
|
2357
|
+
function printValkeyCatalog(catalog, profileFilter) {
|
|
2358
|
+
const profiles = profileFilter ? [profileFilter] : Object.keys(VALKEY_PROFILE_LABELS);
|
|
2359
|
+
profiles.forEach((profile, index) => {
|
|
2360
|
+
if (index > 0) process.stdout.write('\n');
|
|
2361
|
+
process.stdout.write(`${VALKEY_PROFILE_LABELS[profile]}\n`);
|
|
2362
|
+
const plans = catalog.plans.filter((plan) => plan.profile === profile);
|
|
2363
|
+
printTable(plans, [
|
|
2364
|
+
{ label: 'Plano', value: (plan) => plan.id || '-' },
|
|
2365
|
+
{ label: 'Hora', value: (plan) => formatBrl(plan.prices?.hourly, catalog.currency) },
|
|
2366
|
+
{ label: 'Mes', value: (plan) => formatBrl(plan.prices?.monthly, catalog.currency) },
|
|
2367
|
+
{ label: 'Ano', value: (plan) => formatBrl(plan.prices?.yearly, catalog.currency) },
|
|
2368
|
+
{ label: 'Capacidade', value: (plan) => `${plan.resources?.cpu || '-'} / ${plan.resources?.memory || '-'}` },
|
|
2369
|
+
{ label: 'Storage incluído', value: (plan) => `${plan.included_storage_gb ?? 0} GB` },
|
|
2370
|
+
{ label: 'Alta disponibilidade', value: (plan) => plan.high_availability?.[profile] ? 'sim' : 'nao' },
|
|
2371
|
+
]);
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
|
|
1967
2375
|
function printPlansCatalogs(payload, type) {
|
|
1968
2376
|
if (type === 'all' || type === 'http') {
|
|
1969
2377
|
process.stdout.write('HTTP\n');
|
|
@@ -1999,12 +2407,17 @@ function printPlansCatalogs(payload, type) {
|
|
|
1999
2407
|
{ label: 'Ano', value: (storage) => formatBrl(storage.prices?.yearly) },
|
|
2000
2408
|
]);
|
|
2001
2409
|
}
|
|
2410
|
+
|
|
2411
|
+
if (type === 'all' || type === 'valkey' || type.startsWith('valkey-')) {
|
|
2412
|
+
if (type === 'all') process.stdout.write('\n');
|
|
2413
|
+
printValkeyCatalog(payload.valkey, valkeyProfileFilter(type));
|
|
2414
|
+
}
|
|
2002
2415
|
}
|
|
2003
2416
|
|
|
2004
2417
|
async function handlePlans(session, flags) {
|
|
2005
2418
|
const type = normalizePlansCatalogType(flags.type);
|
|
2006
2419
|
if (!type) {
|
|
2007
|
-
throw new CliError('Tipo de catalogo invalido. Valores aceitos: all, http, database, storage.');
|
|
2420
|
+
throw new CliError('Tipo de catalogo invalido. Valores aceitos: all, http, database, storage, valkey.');
|
|
2008
2421
|
}
|
|
2009
2422
|
|
|
2010
2423
|
const payload = await fetchPlansCatalogs(session, flags, type);
|
|
@@ -2016,9 +2429,99 @@ function formatAllowedValues(values) {
|
|
|
2016
2429
|
return [...values].join(', ');
|
|
2017
2430
|
}
|
|
2018
2431
|
|
|
2019
|
-
function
|
|
2432
|
+
function validateCreateAutoscaling(config, plan, typeProject) {
|
|
2433
|
+
if (config?.autoscaling === undefined) return config;
|
|
2434
|
+
if (typeProject !== 'http') {
|
|
2435
|
+
throw new CliError('config.autoscaling deve ser informado apenas para projetos HTTP.');
|
|
2436
|
+
}
|
|
2437
|
+
if (plan === 'free') {
|
|
2438
|
+
throw new CliError('Auto-scaling nao esta disponivel para projetos HTTP no plano free.');
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
const autoscaling = config.autoscaling;
|
|
2442
|
+
if (!autoscaling || typeof autoscaling !== 'object' || Array.isArray(autoscaling)) {
|
|
2443
|
+
throw new CliError('config.autoscaling deve ser um objeto.');
|
|
2444
|
+
}
|
|
2445
|
+
if (autoscaling.enabled !== true) {
|
|
2446
|
+
throw new CliError('config.autoscaling.enabled deve ser true na criacao do projeto.');
|
|
2447
|
+
}
|
|
2448
|
+
if (autoscaling.min_instances !== undefined) {
|
|
2449
|
+
throw new CliError('Use config.instances como minimo inicial; config.autoscaling.min_instances nao e aceito na criacao.');
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
const instances = Number(config.instances);
|
|
2453
|
+
if (!Number.isInteger(instances) || instances <= 0) {
|
|
2454
|
+
throw new CliError('config.instances deve ser um inteiro positivo ao criar um projeto com auto-scaling.');
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
const maxInstances = Number(autoscaling.max_instances);
|
|
2458
|
+
if (!Number.isInteger(maxInstances) || maxInstances < instances) {
|
|
2459
|
+
throw new CliError('config.autoscaling.max_instances deve ser um inteiro maior ou igual a config.instances.');
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
const nextAutoscaling = {
|
|
2463
|
+
enabled: true,
|
|
2464
|
+
max_instances: maxInstances,
|
|
2465
|
+
};
|
|
2466
|
+
for (const key of ['target_cpu_utilization_percent', 'target_memory_utilization_percent']) {
|
|
2467
|
+
if (autoscaling[key] === undefined) continue;
|
|
2468
|
+
const value = Number(autoscaling[key]);
|
|
2469
|
+
if (!Number.isInteger(value) || value < 1 || value > 100) {
|
|
2470
|
+
throw new CliError(`config.autoscaling.${key} deve ser um inteiro entre 1 e 100.`);
|
|
2471
|
+
}
|
|
2472
|
+
nextAutoscaling[key] = value;
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
return { ...config, autoscaling: nextAutoscaling };
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
function validateValkeyConfig(config, plan, catalog) {
|
|
2479
|
+
const valkeyCatalog = requireValkeyCatalog(catalog);
|
|
2480
|
+
const profile = normalizeValkeyProfile(config?.profile);
|
|
2481
|
+
const profileDefinition = valkeyCatalog.profiles.find((entry) => entry.id === profile);
|
|
2482
|
+
if (!profileDefinition) {
|
|
2483
|
+
throw new CliError('config.profile invalido. Use key_value, cache ou queue.');
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
const planDefinition = valkeyCatalog.plans.find((entry) => entry.id === plan && entry.profile === profile);
|
|
2487
|
+
if (!planDefinition) {
|
|
2488
|
+
throw new CliError(`Plano ${plan} nao pode ser usado com o perfil ${profile}. Consulte "zenifra plans --type valkey".`);
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
if (config.version !== valkeyCatalog.version) {
|
|
2492
|
+
throw new CliError(`config.version invalida: use ${valkeyCatalog.version}, conforme o catalogo Valkey.`);
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
const forbiddenFields = ['instances', 'envs', 'image', 'github', 'port', 'exposure', 'autoscaling'];
|
|
2496
|
+
const forbiddenField = forbiddenFields.find((field) => config[field] !== undefined);
|
|
2497
|
+
if (forbiddenField) {
|
|
2498
|
+
throw new CliError(`Valkey nao aceita config.${forbiddenField}. A capacidade e definida pelo plano.`);
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
if (profileDefinition.persistence) {
|
|
2502
|
+
if (!isRecord(config.storage) || config.storage.persistent !== true) {
|
|
2503
|
+
throw new CliError('Perfis Valkey persistentes exigem storage.persistent=true.');
|
|
2504
|
+
}
|
|
2505
|
+
const capacity = Number(config.storage.capacity);
|
|
2506
|
+
if (!Number.isInteger(capacity) || capacity < 1 || capacity > 250) {
|
|
2507
|
+
throw new CliError('storage.capacity deve ser um inteiro entre 1 e 250 GiB.');
|
|
2508
|
+
}
|
|
2509
|
+
} else if (config.storage !== undefined) {
|
|
2510
|
+
throw new CliError('O perfil Cache nao aceita storage.');
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
return {
|
|
2514
|
+
...config,
|
|
2515
|
+
type_project: 'valkey',
|
|
2516
|
+
profile,
|
|
2517
|
+
version: valkeyCatalog.version,
|
|
2518
|
+
};
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
function validateCreateInput({ plan, paymentMode, config, valkeyCatalog }) {
|
|
2522
|
+
const nextTypeProject = normalizeTypeProject(config?.type_project);
|
|
2020
2523
|
const nextPlan = normalizePlan(plan);
|
|
2021
|
-
if (!nextPlan || !ALLOWED_PLAN_VALUES.has(nextPlan)) {
|
|
2524
|
+
if (!nextPlan || (nextTypeProject !== 'valkey' && !ALLOWED_PLAN_VALUES.has(nextPlan))) {
|
|
2022
2525
|
throw new CliError(`Plano invalido: "${plan}". Valores aceitos: ${formatAllowedValues(ALLOWED_PLAN_VALUES)}. Docs: ${DOCS_CREATE_HTTP_URL}`);
|
|
2023
2526
|
}
|
|
2024
2527
|
|
|
@@ -2027,12 +2530,19 @@ function validateCreateInput({ plan, paymentMode, config }) {
|
|
|
2027
2530
|
throw new CliError(`Modo de pagamento invalido: "${paymentMode}". Valores aceitos: ${formatAllowedValues(ALLOWED_PAYMENT_MODE_VALUES)}. Docs: ${DOCS_PAYMENTS_URL}`);
|
|
2028
2531
|
}
|
|
2029
2532
|
|
|
2030
|
-
const nextTypeProject = normalizeTypeProject(config?.type_project);
|
|
2031
2533
|
if (!nextTypeProject || !ALLOWED_TYPE_PROJECT_VALUES.has(nextTypeProject)) {
|
|
2032
2534
|
throw new CliError(`type_project invalido: "${config?.type_project}". Valores aceitos: ${formatAllowedValues(ALLOWED_TYPE_PROJECT_VALUES)}. Docs: ${DOCS_CREATE_HTTP_URL}`);
|
|
2033
2535
|
}
|
|
2034
2536
|
|
|
2035
2537
|
const nextConfig = { ...config, type_project: nextTypeProject };
|
|
2538
|
+
if (nextTypeProject === 'valkey') {
|
|
2539
|
+
return {
|
|
2540
|
+
plan: nextPlan,
|
|
2541
|
+
paymentMode: nextPaymentMode,
|
|
2542
|
+
config: validateValkeyConfig(nextConfig, nextPlan, valkeyCatalog),
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2036
2546
|
if (nextTypeProject === 'http' && nextConfig.github) {
|
|
2037
2547
|
const nextRuntime = normalizeRuntime(nextConfig.github.runtime);
|
|
2038
2548
|
if (!nextRuntime || !ALLOWED_RUNTIME_VALUES.has(nextRuntime)) {
|
|
@@ -2041,10 +2551,22 @@ function validateCreateInput({ plan, paymentMode, config }) {
|
|
|
2041
2551
|
nextConfig.github = { ...nextConfig.github, runtime: nextRuntime };
|
|
2042
2552
|
}
|
|
2043
2553
|
|
|
2554
|
+
if (nextTypeProject === 'http') {
|
|
2555
|
+
const nextExposure = normalizeExposure(nextConfig.exposure);
|
|
2556
|
+
if (!nextExposure || !ALLOWED_EXPOSURE_VALUES.has(nextExposure)) {
|
|
2557
|
+
throw new CliError(`exposure invalido: "${nextConfig.exposure || ''}". Valores aceitos: ${formatAllowedValues(ALLOWED_EXPOSURE_VALUES)}. Docs: ${DOCS_CREATE_HTTP_URL}`);
|
|
2558
|
+
}
|
|
2559
|
+
nextConfig.exposure = nextExposure;
|
|
2560
|
+
} else if (nextConfig.exposure !== undefined) {
|
|
2561
|
+
throw new CliError('exposure deve ser informado apenas para projetos HTTP.');
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
const validatedConfig = validateCreateAutoscaling(nextConfig, nextPlan, nextTypeProject);
|
|
2565
|
+
|
|
2044
2566
|
return {
|
|
2045
2567
|
plan: nextPlan,
|
|
2046
2568
|
paymentMode: nextPaymentMode,
|
|
2047
|
-
config:
|
|
2569
|
+
config: validatedConfig,
|
|
2048
2570
|
};
|
|
2049
2571
|
}
|
|
2050
2572
|
|
|
@@ -2072,7 +2594,10 @@ function printWizardSummary(payload) {
|
|
|
2072
2594
|
if (masked.description) lines.push(` descricao: ${masked.description}`);
|
|
2073
2595
|
|
|
2074
2596
|
if (masked.config.type_project === 'http') {
|
|
2075
|
-
lines.push(` http: porta ${masked.config.port} | instancias ${masked.config.instances} | storage ${masked.config.storage.capacity}Gi (${masked.config.storage.persistent ? 'persistente' : 'efemero'})`);
|
|
2597
|
+
lines.push(` http: exposicao ${masked.config.exposure} | porta ${masked.config.port} | instancias ${masked.config.instances} | storage ${masked.config.storage.capacity}Gi (${masked.config.storage.persistent ? 'persistente' : 'efemero'})`);
|
|
2598
|
+
if (masked.config.autoscaling?.enabled) {
|
|
2599
|
+
lines.push(` auto-scaling: ${masked.config.instances}-${masked.config.autoscaling.max_instances} instancias | CPU ${masked.config.autoscaling.target_cpu_utilization_percent}% | memoria ${masked.config.autoscaling.target_memory_utilization_percent}%`);
|
|
2600
|
+
}
|
|
2076
2601
|
if (masked.config.github) {
|
|
2077
2602
|
lines.push(` github: ${masked.config.github.repository_owner}/${masked.config.github.repository_name}@${masked.config.github.branch} | runtime ${masked.config.github.runtime}@${masked.config.github.version}`);
|
|
2078
2603
|
} else if (masked.config.image) {
|
|
@@ -2084,6 +2609,10 @@ function printWizardSummary(payload) {
|
|
|
2084
2609
|
lines.push(` banco: versao ${masked.config.version} | instancias ${masked.config.instances} | storage ${masked.config.storage.capacity}Gi`);
|
|
2085
2610
|
}
|
|
2086
2611
|
|
|
2612
|
+
if (masked.config.type_project === 'valkey') {
|
|
2613
|
+
lines.push(` Valkey: perfil ${masked.config.profile} | versao ${masked.config.version}${masked.config.storage ? ` | storage ${masked.config.storage.capacity}Gi persistente` : ' | sem storage persistente'}`);
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2087
2616
|
process.stdout.write(`${lines.join('\n')}\n`);
|
|
2088
2617
|
}
|
|
2089
2618
|
|
|
@@ -2277,6 +2806,42 @@ async function buildHttpConfig({ plan, httpPlans, availableInstances }) {
|
|
|
2277
2806
|
max: Math.max(1, Number(availableInstances?.[plan] || 1)),
|
|
2278
2807
|
});
|
|
2279
2808
|
|
|
2809
|
+
let autoscaling;
|
|
2810
|
+
if (isAutoscalingEnabledForPlan(httpPlans, plan)) {
|
|
2811
|
+
const enabled = await promptWizardBoolean({
|
|
2812
|
+
label: 'Ativar auto-scaling',
|
|
2813
|
+
docs: DOCS_CONFIGURATION_URL,
|
|
2814
|
+
examples: ['sim', 'nao'],
|
|
2815
|
+
});
|
|
2816
|
+
promptWizardBoolean.ui.state.httpAutoscalingEnabled = enabled;
|
|
2817
|
+
if (enabled) {
|
|
2818
|
+
autoscaling = {
|
|
2819
|
+
enabled: true,
|
|
2820
|
+
max_instances: await promptWizardNumber({
|
|
2821
|
+
label: 'Maximo de instancias do auto-scaling',
|
|
2822
|
+
docs: DOCS_CONFIGURATION_URL,
|
|
2823
|
+
examples: [String(Math.max(instances, 2))],
|
|
2824
|
+
min: instances,
|
|
2825
|
+
max: Math.max(instances, Number(availableInstances?.[plan] || instances)),
|
|
2826
|
+
}),
|
|
2827
|
+
target_cpu_utilization_percent: await promptWizardNumber({
|
|
2828
|
+
label: 'CPU alvo do auto-scaling (%)',
|
|
2829
|
+
docs: DOCS_CONFIGURATION_URL,
|
|
2830
|
+
examples: ['70'],
|
|
2831
|
+
min: 1,
|
|
2832
|
+
max: 100,
|
|
2833
|
+
}),
|
|
2834
|
+
target_memory_utilization_percent: await promptWizardNumber({
|
|
2835
|
+
label: 'Memoria alvo do auto-scaling (%)',
|
|
2836
|
+
docs: DOCS_CONFIGURATION_URL,
|
|
2837
|
+
examples: ['80'],
|
|
2838
|
+
min: 1,
|
|
2839
|
+
max: 100,
|
|
2840
|
+
}),
|
|
2841
|
+
};
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2280
2845
|
let storage;
|
|
2281
2846
|
if (plan === 'free') {
|
|
2282
2847
|
storage = { persistent: false, capacity: 1 };
|
|
@@ -2320,8 +2885,17 @@ async function buildHttpConfig({ plan, httpPlans, availableInstances }) {
|
|
|
2320
2885
|
],
|
|
2321
2886
|
});
|
|
2322
2887
|
|
|
2888
|
+
const exposure = await promptWizardSelect({
|
|
2889
|
+
label: 'Exposicao HTTP',
|
|
2890
|
+
docs: DOCS_CREATE_HTTP_URL,
|
|
2891
|
+
examples: WIZARD_HTTP_EXPOSURE_OPTIONS,
|
|
2892
|
+
options: WIZARD_HTTP_EXPOSURE_OPTIONS.map((value) => ({ value })),
|
|
2893
|
+
});
|
|
2894
|
+
promptWizardSelect.ui.state.httpExposure = exposure;
|
|
2895
|
+
|
|
2896
|
+
const isPublicExposure = exposure === 'public';
|
|
2323
2897
|
const allowBlockIp = isIpAccessEnabled(httpPlans, plan);
|
|
2324
|
-
const customWhitelist = allowBlockIp
|
|
2898
|
+
const customWhitelist = isPublicExposure && allowBlockIp
|
|
2325
2899
|
? await promptWizardPairs({
|
|
2326
2900
|
introLabel: 'Deseja configurar whitelist personalizada de entrada?',
|
|
2327
2901
|
docs: DOCS_CONFIGURATION_URL,
|
|
@@ -2334,7 +2908,7 @@ async function buildHttpConfig({ plan, httpPlans, availableInstances }) {
|
|
|
2334
2908
|
})
|
|
2335
2909
|
: [];
|
|
2336
2910
|
|
|
2337
|
-
const ingress_black_list = allowBlockIp
|
|
2911
|
+
const ingress_black_list = isPublicExposure && allowBlockIp
|
|
2338
2912
|
? await promptWizardPairs({
|
|
2339
2913
|
introLabel: 'Deseja adicionar blacklist de entrada?',
|
|
2340
2914
|
docs: DOCS_CONFIGURATION_URL,
|
|
@@ -2347,7 +2921,7 @@ async function buildHttpConfig({ plan, httpPlans, availableInstances }) {
|
|
|
2347
2921
|
})
|
|
2348
2922
|
: [];
|
|
2349
2923
|
|
|
2350
|
-
const subdomain = isSubdomainCustomizable(httpPlans, plan)
|
|
2924
|
+
const subdomain = isPublicExposure && isSubdomainCustomizable(httpPlans, plan)
|
|
2351
2925
|
? await promptWizardText({
|
|
2352
2926
|
label: 'Subdomain personalizado',
|
|
2353
2927
|
examples: ['minha-api'],
|
|
@@ -2357,9 +2931,11 @@ async function buildHttpConfig({ plan, httpPlans, availableInstances }) {
|
|
|
2357
2931
|
|
|
2358
2932
|
return {
|
|
2359
2933
|
type_project: 'http',
|
|
2934
|
+
exposure,
|
|
2360
2935
|
...(source === 'github' ? { github: await buildHttpGithubConfig() } : { image: await buildHttpImageConfig() }),
|
|
2361
2936
|
port,
|
|
2362
2937
|
instances,
|
|
2938
|
+
...(autoscaling ? { autoscaling } : {}),
|
|
2363
2939
|
storage,
|
|
2364
2940
|
envs,
|
|
2365
2941
|
...(subdomain ? { subdomain } : {}),
|
|
@@ -2428,6 +3004,65 @@ async function buildMariadbConfig() {
|
|
|
2428
3004
|
};
|
|
2429
3005
|
}
|
|
2430
3006
|
|
|
3007
|
+
async function buildValkeyConfig({ profile, plan, catalog }) {
|
|
3008
|
+
const planDefinition = catalog.plans.find((entry) => entry.id === plan && entry.profile === profile);
|
|
3009
|
+
const profileDefinition = catalog.profiles.find((entry) => entry.id === profile);
|
|
3010
|
+
if (!planDefinition || !profileDefinition) {
|
|
3011
|
+
throw new CliError('O catalogo Valkey nao possui o plano ou perfil selecionado. Tente novamente.');
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
let storage;
|
|
3015
|
+
if (profileDefinition.persistence) {
|
|
3016
|
+
storage = {
|
|
3017
|
+
persistent: true,
|
|
3018
|
+
capacity: await promptWizardNumber({
|
|
3019
|
+
label: `Capacidade de armazenamento persistente (Gi, inclui ${planDefinition.included_storage_gb ?? 0} Gi)`,
|
|
3020
|
+
docs: DOCS_CREATE_VALKEY_URL,
|
|
3021
|
+
examples: [String(Math.max(1, planDefinition.included_storage_gb ?? 1)), '10', '20'],
|
|
3022
|
+
min: 1,
|
|
3023
|
+
max: 250,
|
|
3024
|
+
}),
|
|
3025
|
+
};
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
const whitelist = await promptWizardPairs({
|
|
3029
|
+
introLabel: 'Deseja configurar acesso de rede para este projeto?',
|
|
3030
|
+
docs: DOCS_CREATE_VALKEY_URL,
|
|
3031
|
+
examples: ['sim', 'nao'],
|
|
3032
|
+
stateKey: 'valkeyNetworkAccess',
|
|
3033
|
+
itemLabels: [
|
|
3034
|
+
{ key: 'cidr', label: 'CIDR permitido', required: true, examples: ['203.0.113.0/24'], docs: DOCS_CREATE_VALKEY_URL },
|
|
3035
|
+
{ key: 'description', label: 'Descricao do acesso', required: true, examples: ['Aplicacao'], docs: DOCS_CREATE_VALKEY_URL },
|
|
3036
|
+
],
|
|
3037
|
+
});
|
|
3038
|
+
|
|
3039
|
+
let blacklist = [];
|
|
3040
|
+
if (whitelist.length > 0) {
|
|
3041
|
+
blacklist = await promptWizardPairs({
|
|
3042
|
+
introLabel: 'Deseja adicionar bloqueios de rede?',
|
|
3043
|
+
docs: DOCS_CREATE_VALKEY_URL,
|
|
3044
|
+
examples: ['sim', 'nao'],
|
|
3045
|
+
itemLabels: [
|
|
3046
|
+
{ key: 'cidr', label: 'CIDR bloqueado', required: true, examples: ['198.51.100.0/24'], docs: DOCS_CREATE_VALKEY_URL },
|
|
3047
|
+
{ key: 'description', label: 'Descricao do bloqueio', required: true, examples: ['Bloqueio'], docs: DOCS_CREATE_VALKEY_URL },
|
|
3048
|
+
],
|
|
3049
|
+
});
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
return {
|
|
3053
|
+
type_project: 'valkey',
|
|
3054
|
+
profile,
|
|
3055
|
+
version: catalog.version,
|
|
3056
|
+
...(storage ? { storage } : {}),
|
|
3057
|
+
...(whitelist.length > 0 ? {
|
|
3058
|
+
network_access: {
|
|
3059
|
+
ingress_white_list: whitelist,
|
|
3060
|
+
ingress_black_list: blacklist,
|
|
3061
|
+
},
|
|
3062
|
+
} : {}),
|
|
3063
|
+
};
|
|
3064
|
+
}
|
|
3065
|
+
|
|
2431
3066
|
async function runProjectCreateWizard(session, flags) {
|
|
2432
3067
|
const catalogs = await fetchWizardCatalogs(session, flags);
|
|
2433
3068
|
const ui = createWizardUi();
|
|
@@ -2458,16 +3093,34 @@ async function runProjectCreateWizard(session, flags) {
|
|
|
2458
3093
|
options: WIZARD_TYPE_PROJECT_OPTIONS.map((value) => ({ value })),
|
|
2459
3094
|
});
|
|
2460
3095
|
ui.state.typeProject = typeProject;
|
|
3096
|
+
const valkeyCatalog = typeProject === 'valkey'
|
|
3097
|
+
? requireValkeyCatalog(unwrapData(await request(session, flags, 'GET', '/managed-services/catalog')))
|
|
3098
|
+
: undefined;
|
|
3099
|
+
const profile = typeProject === 'valkey'
|
|
3100
|
+
? await promptWizardSelect({
|
|
3101
|
+
label: 'Perfil Valkey',
|
|
3102
|
+
docs: DOCS_CREATE_VALKEY_URL,
|
|
3103
|
+
examples: WIZARD_VALKEY_PROFILE_OPTIONS,
|
|
3104
|
+
options: WIZARD_VALKEY_PROFILE_OPTIONS.map((value) => ({ value })),
|
|
3105
|
+
})
|
|
3106
|
+
: undefined;
|
|
3107
|
+
ui.state.valkeyProfile = profile;
|
|
2461
3108
|
const planOptions = typeProject === 'http'
|
|
2462
3109
|
? sortWizardOptions(catalogs.httpPlans
|
|
2463
3110
|
.map((plan) => plan?.plan)
|
|
2464
3111
|
.filter((plan) => WIZARD_HTTP_PLAN_OPTIONS.includes(plan) && Number(catalogs.availableInstances?.[plan] || 0) > 0), WIZARD_HTTP_PLAN_OPTIONS)
|
|
3112
|
+
: typeProject === 'valkey'
|
|
3113
|
+
? valkeyCatalog.plans
|
|
3114
|
+
.filter((planDefinition) => planDefinition.profile === profile)
|
|
3115
|
+
.map((planDefinition) => planDefinition.id)
|
|
2465
3116
|
: sortWizardOptions(catalogs.databasePlans
|
|
2466
3117
|
.map((plan) => plan?.plan)
|
|
2467
3118
|
.filter((plan) => WIZARD_DATABASE_PLAN_OPTIONS.includes(plan) && (typeProject !== 'mariadb' || plan !== 'db-free')), WIZARD_DATABASE_PLAN_OPTIONS);
|
|
2468
3119
|
const plan = await promptWizardSelect({
|
|
2469
3120
|
label: 'Plano',
|
|
2470
|
-
docs: typeProject === 'http'
|
|
3121
|
+
docs: typeProject === 'http'
|
|
3122
|
+
? DOCS_CREATE_HTTP_URL
|
|
3123
|
+
: typeProject === 'valkey' ? DOCS_CREATE_VALKEY_URL : DOCS_DATABASE_CONFIGURATION_URL,
|
|
2471
3124
|
examples: [planOptions[0], planOptions.at(-1)],
|
|
2472
3125
|
options: planOptions.map((value) => ({ value })),
|
|
2473
3126
|
});
|
|
@@ -2475,6 +3128,7 @@ async function runProjectCreateWizard(session, flags) {
|
|
|
2475
3128
|
if (typeProject === 'http') {
|
|
2476
3129
|
ui.state.planAllowsBlockIp = isIpAccessEnabled(catalogs.httpPlans, plan);
|
|
2477
3130
|
ui.state.planAllowsSubdomain = isSubdomainCustomizable(catalogs.httpPlans, plan);
|
|
3131
|
+
ui.state.planAllowsAutoscaling = isAutoscalingEnabledForPlan(catalogs.httpPlans, plan);
|
|
2478
3132
|
}
|
|
2479
3133
|
const paymentMode = await promptWizardSelect({
|
|
2480
3134
|
label: 'Modo de pagamento',
|
|
@@ -2492,6 +3146,8 @@ async function runProjectCreateWizard(session, flags) {
|
|
|
2492
3146
|
});
|
|
2493
3147
|
} else if (typeProject === 'postgresql') {
|
|
2494
3148
|
config = await buildPostgresqlConfig({ plan, databasePlans: catalogs.databasePlans });
|
|
3149
|
+
} else if (typeProject === 'valkey') {
|
|
3150
|
+
config = await buildValkeyConfig({ profile, plan, catalog: valkeyCatalog });
|
|
2495
3151
|
} else {
|
|
2496
3152
|
config = await buildMariadbConfig();
|
|
2497
3153
|
}
|
|
@@ -2507,7 +3163,9 @@ async function runProjectCreateWizard(session, flags) {
|
|
|
2507
3163
|
printWizardSummary(payload);
|
|
2508
3164
|
const confirmed = await promptWizardBoolean({
|
|
2509
3165
|
label: 'Confirmar criacao do projeto',
|
|
2510
|
-
docs: typeProject === 'http'
|
|
3166
|
+
docs: typeProject === 'http'
|
|
3167
|
+
? DOCS_CREATE_HTTP_URL
|
|
3168
|
+
: typeProject === 'valkey' ? DOCS_CREATE_VALKEY_URL : DOCS_DATABASE_CONFIGURATION_URL,
|
|
2511
3169
|
examples: ['sim', 'nao'],
|
|
2512
3170
|
});
|
|
2513
3171
|
if (!confirmed) return null;
|
|
@@ -2528,10 +3186,18 @@ async function handleProjectCreate(session, flags) {
|
|
|
2528
3186
|
const plan = wizardPayload?.plan || flags.plan || await prompt('Plano');
|
|
2529
3187
|
const paymentMode = wizardPayload?.payment_mode || flags.paymentMode || await prompt('Modo de pagamento');
|
|
2530
3188
|
const config = wizardPayload?.config || await parseConfig(flags.config || await prompt('Config JSON ou @arquivo'));
|
|
2531
|
-
const
|
|
3189
|
+
const typeProject = normalizeTypeProject(config?.type_project);
|
|
3190
|
+
const valkeyCatalog = typeProject === 'valkey'
|
|
3191
|
+
? requireValkeyCatalog(unwrapData(await request(session, flags, 'GET', '/managed-services/catalog')))
|
|
3192
|
+
: undefined;
|
|
3193
|
+
if (flags.idempotencyKey !== undefined && !/^[A-Za-z0-9._-]{16,200}$/.test(String(flags.idempotencyKey))) {
|
|
3194
|
+
throw new CliError('--idempotency-key deve ter entre 16 e 200 caracteres: letras, numeros, ponto, underscore ou hifen.');
|
|
3195
|
+
}
|
|
3196
|
+
const validated = validateCreateInput({ plan, paymentMode, config, valkeyCatalog });
|
|
2532
3197
|
|
|
2533
3198
|
const payload = await request(session, flags, 'POST', '/project', {
|
|
2534
3199
|
orgId,
|
|
3200
|
+
headers: flags.idempotencyKey === undefined ? {} : { 'Idempotency-Key': String(flags.idempotencyKey) },
|
|
2535
3201
|
body: {
|
|
2536
3202
|
name,
|
|
2537
3203
|
...(description ? { description: String(description) } : {}),
|
|
@@ -2551,6 +3217,21 @@ async function handleProjectCreate(session, flags) {
|
|
|
2551
3217
|
...(project.domain ? [{ field: 'Dominio', value: formatPublicUrl(project.domain) }] : []),
|
|
2552
3218
|
...(project.api_key ? [{ field: 'API key', value: project.api_key }] : []),
|
|
2553
3219
|
];
|
|
3220
|
+
if (validated.config.type_project === 'valkey') {
|
|
3221
|
+
const service = project.managed_service || {};
|
|
3222
|
+
rows.push(
|
|
3223
|
+
{ field: 'Perfil', value: service.profile || validated.config.profile },
|
|
3224
|
+
{ field: 'Host', value: service.host || '-' },
|
|
3225
|
+
{ field: 'Porta', value: service.port ?? '-' },
|
|
3226
|
+
{ field: 'TLS', value: service.tls === true ? 'sim' : 'nao' },
|
|
3227
|
+
);
|
|
3228
|
+
if (service.connection_string) {
|
|
3229
|
+
rows.push({ field: 'String de conexao', value: service.connection_string });
|
|
3230
|
+
process.stdout.write('Salve a string de conexao em um local seguro; ela pode nao ser exibida novamente.\n');
|
|
3231
|
+
} else {
|
|
3232
|
+
rows.push({ field: 'Credencial', value: 'nao retornada nesta resposta; use valkey credentials rotate' });
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
2554
3235
|
process.stdout.write('\n');
|
|
2555
3236
|
printTable(rows, [
|
|
2556
3237
|
{ label: 'Campo', value: (row) => row.field },
|
|
@@ -2654,6 +3335,33 @@ async function handleProjectImageSet(session, flags) {
|
|
|
2654
3335
|
process.stdout.write('Imagem atualizada com sucesso.\n');
|
|
2655
3336
|
}
|
|
2656
3337
|
|
|
3338
|
+
async function handleProjectExposureSet(session, flags) {
|
|
3339
|
+
const projectId = requireProjectId(flags, 'project exposure set');
|
|
3340
|
+
if (!projectId) return
|
|
3341
|
+
const exposure = normalizeExposure(flags.exposure);
|
|
3342
|
+
|
|
3343
|
+
if (!ALLOWED_EXPOSURE_VALUES.has(exposure)) {
|
|
3344
|
+
throw new CliError('exposure invalido. Use public ou private.');
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3348
|
+
const payload = await request(session, flags, 'PATCH', `/project/${projectId}/exposure`, {
|
|
3349
|
+
orgId,
|
|
3350
|
+
body: { exposure },
|
|
3351
|
+
});
|
|
3352
|
+
|
|
3353
|
+
if (flags.json) return printJson(payload);
|
|
3354
|
+
|
|
3355
|
+
printTable([
|
|
3356
|
+
{ field: 'Exposicao', value: payload.exposure || exposure },
|
|
3357
|
+
{ field: 'Dominio', value: payload.domain ? formatPublicUrl(payload.domain) : '-' },
|
|
3358
|
+
{ field: 'Dominios customizados', value: asArray(payload.custom_domains).join(', ') || '-' },
|
|
3359
|
+
], [
|
|
3360
|
+
{ label: 'Campo', value: (row) => row.field },
|
|
3361
|
+
{ label: 'Valor', value: (row) => row.value },
|
|
3362
|
+
]);
|
|
3363
|
+
}
|
|
3364
|
+
|
|
2657
3365
|
async function getProjectEnvs(session, flags, projectId, orgId) {
|
|
2658
3366
|
return asArray(unwrapData(await request(session, flags, 'GET', `/project/${projectId}/envs`, { orgId })));
|
|
2659
3367
|
}
|
|
@@ -2760,6 +3468,366 @@ async function handleProjectInstancesSet(session, flags) {
|
|
|
2760
3468
|
process.stdout.write(`Quantidade de instancias atualizada para ${instances}.\n`);
|
|
2761
3469
|
}
|
|
2762
3470
|
|
|
3471
|
+
function parseAutoscalingPositiveInteger(value, flagName) {
|
|
3472
|
+
const parsed = Number(value);
|
|
3473
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
3474
|
+
throw new CliError(`Informe ${flagName} <n> com um inteiro positivo.`);
|
|
3475
|
+
}
|
|
3476
|
+
return parsed;
|
|
3477
|
+
}
|
|
3478
|
+
|
|
3479
|
+
function parseAutoscalingTargetPercent(value, flagName) {
|
|
3480
|
+
const parsed = parseAutoscalingPositiveInteger(value, flagName);
|
|
3481
|
+
if (parsed > 100) {
|
|
3482
|
+
throw new CliError(`Informe ${flagName} entre 1 e 100.`);
|
|
3483
|
+
}
|
|
3484
|
+
return parsed;
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
function autoscalingFromProject(project) {
|
|
3488
|
+
return project?.additional_info?.autoscaling || project?.autoscaling || { enabled: false };
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
function printAutoscaling(autoscaling) {
|
|
3492
|
+
printTable([
|
|
3493
|
+
{
|
|
3494
|
+
status: autoscaling.enabled ? 'ativo' : 'inativo',
|
|
3495
|
+
min: autoscaling.min_instances || '-',
|
|
3496
|
+
max: autoscaling.max_instances || '-',
|
|
3497
|
+
cpu: autoscaling.target_cpu_utilization_percent ? `${autoscaling.target_cpu_utilization_percent}%` : '-',
|
|
3498
|
+
memory: autoscaling.target_memory_utilization_percent ? `${autoscaling.target_memory_utilization_percent}%` : '-',
|
|
3499
|
+
},
|
|
3500
|
+
], [
|
|
3501
|
+
{ label: 'Status', value: (item) => item.status },
|
|
3502
|
+
{ label: 'Minimo', value: (item) => item.min },
|
|
3503
|
+
{ label: 'Maximo', value: (item) => item.max },
|
|
3504
|
+
{ label: 'CPU alvo', value: (item) => item.cpu },
|
|
3505
|
+
{ label: 'Memoria alvo', value: (item) => item.memory },
|
|
3506
|
+
]);
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
async function handleProjectAutoscaling(session, flags) {
|
|
3510
|
+
const projectId = requireProjectId(flags, 'project autoscaling');
|
|
3511
|
+
if (!projectId) return
|
|
3512
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3513
|
+
const project = await getProject(session, flags, projectId, orgId);
|
|
3514
|
+
const autoscaling = autoscalingFromProject(project);
|
|
3515
|
+
|
|
3516
|
+
if (flags.json) return printJson(autoscaling);
|
|
3517
|
+
printAutoscaling(autoscaling);
|
|
3518
|
+
}
|
|
3519
|
+
|
|
3520
|
+
async function handleProjectAutoscalingSet(session, flags) {
|
|
3521
|
+
const projectId = requireProjectId(flags, 'project autoscaling set');
|
|
3522
|
+
if (!projectId) return
|
|
3523
|
+
if (flags.min === undefined || flags.max === undefined) {
|
|
3524
|
+
printCommandHelpAndFail('project autoscaling set')
|
|
3525
|
+
return
|
|
3526
|
+
}
|
|
3527
|
+
|
|
3528
|
+
const minInstances = parseAutoscalingPositiveInteger(flags.min, '--min');
|
|
3529
|
+
const maxInstances = parseAutoscalingPositiveInteger(flags.max, '--max');
|
|
3530
|
+
if (maxInstances < minInstances) {
|
|
3531
|
+
throw new CliError('Informe --max maior ou igual a --min.');
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
const body = {
|
|
3535
|
+
enabled: true,
|
|
3536
|
+
min_instances: minInstances,
|
|
3537
|
+
max_instances: maxInstances,
|
|
3538
|
+
};
|
|
3539
|
+
|
|
3540
|
+
if (flags.cpu !== undefined) {
|
|
3541
|
+
body.target_cpu_utilization_percent = parseAutoscalingTargetPercent(flags.cpu, '--cpu');
|
|
3542
|
+
}
|
|
3543
|
+
if (flags.memory !== undefined) {
|
|
3544
|
+
body.target_memory_utilization_percent = parseAutoscalingTargetPercent(flags.memory, '--memory');
|
|
3545
|
+
}
|
|
3546
|
+
|
|
3547
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3548
|
+
const payload = await request(session, flags, 'PATCH', `/project/${projectId}/autoscaling`, {
|
|
3549
|
+
orgId,
|
|
3550
|
+
body,
|
|
3551
|
+
});
|
|
3552
|
+
|
|
3553
|
+
if (flags.json) return printJson(payload);
|
|
3554
|
+
process.stdout.write(`Auto-scaling atualizado: ${minInstances}-${maxInstances} instancias.\n`);
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3557
|
+
async function handleProjectAutoscalingDisable(session, flags) {
|
|
3558
|
+
const projectId = requireProjectId(flags, 'project autoscaling disable');
|
|
3559
|
+
if (!projectId) return
|
|
3560
|
+
|
|
3561
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3562
|
+
const payload = await request(session, flags, 'PATCH', `/project/${projectId}/autoscaling`, {
|
|
3563
|
+
orgId,
|
|
3564
|
+
body: { enabled: false },
|
|
3565
|
+
});
|
|
3566
|
+
|
|
3567
|
+
if (flags.json) return printJson(payload);
|
|
3568
|
+
process.stdout.write('Auto-scaling desativado.\n');
|
|
3569
|
+
}
|
|
3570
|
+
|
|
3571
|
+
function printAutoscalingEvents(data) {
|
|
3572
|
+
const directions = { scale_up: 'aumento', scale_down: 'reducao' };
|
|
3573
|
+
const reasons = { increased_capacity: 'capacidade aumentada', reduced_capacity: 'capacidade reduzida' };
|
|
3574
|
+
const events = asArray(data?.events);
|
|
3575
|
+
printTable(events, [
|
|
3576
|
+
{ label: 'Quando', value: (event) => event.occurred_at || '-' },
|
|
3577
|
+
{ label: 'Direcao', value: (event) => directions[event.direction] || event.direction || '-' },
|
|
3578
|
+
{
|
|
3579
|
+
label: 'Instancias',
|
|
3580
|
+
value: (event) => `${event.previous_instances ?? '-'} -> ${event.new_instances ?? '-'}`,
|
|
3581
|
+
},
|
|
3582
|
+
{
|
|
3583
|
+
label: 'Limites',
|
|
3584
|
+
value: (event) => {
|
|
3585
|
+
const cpu = typeof event.current_cpu_utilization_percent === 'number' || typeof event.target_cpu_utilization_percent === 'number'
|
|
3586
|
+
? `CPU ${event.current_cpu_utilization_percent ?? '-'}%/${event.target_cpu_utilization_percent ?? '-'}%`
|
|
3587
|
+
: null;
|
|
3588
|
+
const memory = typeof event.current_memory_utilization_percent === 'number' || typeof event.target_memory_utilization_percent === 'number'
|
|
3589
|
+
? `MEM ${event.current_memory_utilization_percent ?? '-'}%/${event.target_memory_utilization_percent ?? '-'}%`
|
|
3590
|
+
: null;
|
|
3591
|
+
return [cpu, memory].filter(Boolean).join(' ');
|
|
3592
|
+
},
|
|
3593
|
+
},
|
|
3594
|
+
{ label: 'Motivo', value: (event) => reasons[event.reason] || event.reason || event.trigger_metric || '-' },
|
|
3595
|
+
]);
|
|
3596
|
+
|
|
3597
|
+
printPagination(data?.pagination, 'evento');
|
|
3598
|
+
}
|
|
3599
|
+
|
|
3600
|
+
function isIsoDate(value) {
|
|
3601
|
+
const text = String(value);
|
|
3602
|
+
const match = text.match(/^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/);
|
|
3603
|
+
if (!match || Number.isNaN(Date.parse(text))) return false;
|
|
3604
|
+
|
|
3605
|
+
const [, year, month, day, hour = '00', minute = '00', second = '00'] = match;
|
|
3606
|
+
const calendarDate = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
|
|
3607
|
+
return calendarDate.getUTCFullYear() === Number(year)
|
|
3608
|
+
&& calendarDate.getUTCMonth() === Number(month) - 1
|
|
3609
|
+
&& calendarDate.getUTCDate() === Number(day)
|
|
3610
|
+
&& Number(hour) <= 23
|
|
3611
|
+
&& Number(minute) <= 59
|
|
3612
|
+
&& Number(second) <= 59;
|
|
3613
|
+
}
|
|
3614
|
+
|
|
3615
|
+
function validateProjectListFlags(flags, { maxLimit }) {
|
|
3616
|
+
for (const key of ['from', 'to']) {
|
|
3617
|
+
if (flags[key] !== undefined && !isIsoDate(flags[key])) {
|
|
3618
|
+
throw new CliError(`Informe --${key} <iso> com uma data ISO valida.`);
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
if (flags.from !== undefined && flags.to !== undefined && Date.parse(String(flags.from)) > Date.parse(String(flags.to))) {
|
|
3622
|
+
throw new CliError('Informe --from anterior ou igual a --to.');
|
|
3623
|
+
}
|
|
3624
|
+
for (const key of ['page', 'limit']) {
|
|
3625
|
+
if (flags[key] === undefined) continue;
|
|
3626
|
+
const value = Number(flags[key]);
|
|
3627
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
3628
|
+
throw new CliError(`Informe --${key} <n> com um inteiro positivo.`);
|
|
3629
|
+
}
|
|
3630
|
+
if (key === 'limit' && value > maxLimit) {
|
|
3631
|
+
throw new CliError(`Informe --limit entre 1 e ${maxLimit}.`);
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
|
|
3636
|
+
function printPagination(pagination, singular = 'item') {
|
|
3637
|
+
if (!pagination) return;
|
|
3638
|
+
const { page, total_pages: totalPages, total } = pagination;
|
|
3639
|
+
process.stdout.write(`Pagina ${page} de ${totalPages || 1} - ${total} ${singular}(s)\n`);
|
|
3640
|
+
}
|
|
3641
|
+
|
|
3642
|
+
function formatMoney(value, currency = 'brl') {
|
|
3643
|
+
const amount = Number(value);
|
|
3644
|
+
if (!Number.isFinite(amount)) return '-';
|
|
3645
|
+
try {
|
|
3646
|
+
return new Intl.NumberFormat('pt-BR', {
|
|
3647
|
+
style: 'currency',
|
|
3648
|
+
currency: String(currency || 'brl').toUpperCase(),
|
|
3649
|
+
}).format(amount);
|
|
3650
|
+
} catch {
|
|
3651
|
+
return `${String(currency || 'brl').toUpperCase()} ${amount.toFixed(2)}`;
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
|
|
3655
|
+
function printBillingUsage(data) {
|
|
3656
|
+
const hours = asArray(data?.hours);
|
|
3657
|
+
printTable(hours, [
|
|
3658
|
+
{ label: 'Inicio', value: (item) => item.hour_start || '-' },
|
|
3659
|
+
{ label: 'Fim', value: (item) => item.hour_end || '-' },
|
|
3660
|
+
{ label: 'Instancia-horas', value: (item) => item.compute_instance_hours ?? '-' },
|
|
3661
|
+
{ label: 'Armazenamento', value: (item) => item.storage_gb_hours === undefined ? '-' : `${item.storage_gb_hours} GB-h` },
|
|
3662
|
+
{ label: 'Computacao', value: (item) => formatMoney(item.compute_amount, item.currency) },
|
|
3663
|
+
{ label: 'Storage', value: (item) => formatMoney(item.storage_amount, item.currency) },
|
|
3664
|
+
{ label: 'Total', value: (item) => formatMoney(item.total_amount, item.currency) },
|
|
3665
|
+
{ label: 'Status', value: (item) => item.status || '-' },
|
|
3666
|
+
]);
|
|
3667
|
+
|
|
3668
|
+
if (data?.summary) {
|
|
3669
|
+
const summary = data.summary;
|
|
3670
|
+
process.stdout.write(`Resumo: computacao ${formatMoney(summary.compute_amount, summary.currency)}, armazenamento ${formatMoney(summary.storage_amount, summary.currency)}, total ${formatMoney(summary.total_amount, summary.currency)}\n`);
|
|
3671
|
+
}
|
|
3672
|
+
printPagination(data?.pagination, 'hora');
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3675
|
+
|
|
3676
|
+
async function handleProjectAutoscalingEvents(session, flags) {
|
|
3677
|
+
const projectId = requireProjectId(flags, 'project autoscaling events');
|
|
3678
|
+
if (!projectId) return
|
|
3679
|
+
|
|
3680
|
+
if (flags.direction !== undefined && !['scale_up', 'scale_down'].includes(String(flags.direction))) {
|
|
3681
|
+
throw new CliError('direction invalido. Use scale_up ou scale_down.');
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3684
|
+
validateProjectListFlags(flags, { maxLimit: 100 });
|
|
3685
|
+
|
|
3686
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3687
|
+
const query = buildQuery(flags, ['direction', 'from', 'to', 'page', 'limit']);
|
|
3688
|
+
const data = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/autoscaling/events${query}`, { orgId }));
|
|
3689
|
+
|
|
3690
|
+
if (flags.json) return printJson(data);
|
|
3691
|
+
printAutoscalingEvents(data);
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
async function handleProjectBillingUsage(session, flags) {
|
|
3695
|
+
const projectId = requireProjectId(flags, 'project billing usage');
|
|
3696
|
+
if (!projectId) return;
|
|
3697
|
+
validateProjectListFlags(flags, { maxLimit: 50 });
|
|
3698
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3699
|
+
const query = buildQuery(flags, ['from', 'to', 'page', 'limit']);
|
|
3700
|
+
const data = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/billing/hourly-usage${query}`, { orgId }));
|
|
3701
|
+
|
|
3702
|
+
if (flags.json) return printJson(data);
|
|
3703
|
+
printBillingUsage(data);
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3706
|
+
function printValkeyStatus(data) {
|
|
3707
|
+
printTable([data], [
|
|
3708
|
+
{ label: 'Status', value: (service) => service.status || '-' },
|
|
3709
|
+
{ label: 'Perfil', value: (service) => service.profile || '-' },
|
|
3710
|
+
{ label: 'Versao', value: (service) => service.version || '-' },
|
|
3711
|
+
{ label: 'Persistente', value: (service) => service.persistence === true ? 'sim' : 'nao' },
|
|
3712
|
+
{ label: 'Instancias', value: (service) => service.instances ?? '-' },
|
|
3713
|
+
]);
|
|
3714
|
+
}
|
|
3715
|
+
|
|
3716
|
+
function printValkeyConnection(data) {
|
|
3717
|
+
printTable([data], [
|
|
3718
|
+
{ label: 'Host', value: (service) => service.host || '-' },
|
|
3719
|
+
{ label: 'Porta', value: (service) => service.port ?? '-' },
|
|
3720
|
+
{ label: 'Usuario', value: (service) => service.username || '-' },
|
|
3721
|
+
{ label: 'TLS', value: (service) => service.tls === true ? 'sim' : 'nao' },
|
|
3722
|
+
{ label: 'Conexao', value: (service) => service.connection_string || '-' },
|
|
3723
|
+
]);
|
|
3724
|
+
}
|
|
3725
|
+
|
|
3726
|
+
function printValkeyRotation(data, { accepted = false } = {}) {
|
|
3727
|
+
const rows = [
|
|
3728
|
+
{ field: accepted ? 'Operacao aceita' : 'Operacao', value: data.operation_id || '-' },
|
|
3729
|
+
{ field: 'Estado', value: data.state || '-' },
|
|
3730
|
+
];
|
|
3731
|
+
if (data.connection_string) {
|
|
3732
|
+
rows.push({ field: 'String de conexao', value: data.connection_string });
|
|
3733
|
+
process.stdout.write('Salve a nova string de conexao em um local seguro; ela pode nao ser exibida novamente.\n');
|
|
3734
|
+
}
|
|
3735
|
+
printTable(rows, [
|
|
3736
|
+
{ label: 'Campo', value: (row) => row.field },
|
|
3737
|
+
{ label: 'Valor', value: (row) => row.value },
|
|
3738
|
+
]);
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3741
|
+
async function waitForValkeyRotation(session, flags, projectId, operationId, orgId) {
|
|
3742
|
+
const intervalMs = parseSecondsOption(flags.interval, '--interval', { defaultValue: 2, min: 0.1 }) * 1000;
|
|
3743
|
+
const timeoutMs = parseSecondsOption(flags.timeout, '--timeout', { defaultValue: 900, min: 1 }) * 1000;
|
|
3744
|
+
const startedAt = Date.now();
|
|
3745
|
+
let lastState = 'accepted';
|
|
3746
|
+
|
|
3747
|
+
while (true) {
|
|
3748
|
+
const data = unwrapData(await request(
|
|
3749
|
+
session,
|
|
3750
|
+
flags,
|
|
3751
|
+
'GET',
|
|
3752
|
+
`/managed-services/${projectId}/credential-rotations/${operationId}`,
|
|
3753
|
+
{ orgId },
|
|
3754
|
+
));
|
|
3755
|
+
lastState = data?.state || lastState;
|
|
3756
|
+
if (lastState === 'completed') return data;
|
|
3757
|
+
if (['failed', 'cancelled', 'canceled'].includes(lastState)) {
|
|
3758
|
+
throw new CliError(`A renovacao de credencial terminou com estado ${lastState}. Operacao: ${operationId}.`);
|
|
3759
|
+
}
|
|
3760
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
3761
|
+
throw new CliError(`Timeout aguardando a renovacao ${operationId}. Ultimo estado: ${lastState}.`);
|
|
3762
|
+
}
|
|
3763
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, intervalMs));
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3766
|
+
|
|
3767
|
+
async function handleValkeyStatus(session, flags) {
|
|
3768
|
+
const projectId = requireProjectId(flags, 'valkey status');
|
|
3769
|
+
if (!projectId) return;
|
|
3770
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3771
|
+
const data = unwrapData(await request(session, flags, 'GET', `/managed-services/${projectId}/status`, { orgId }));
|
|
3772
|
+
if (flags.json) return printJson(data);
|
|
3773
|
+
printValkeyStatus(data);
|
|
3774
|
+
}
|
|
3775
|
+
|
|
3776
|
+
async function handleValkeyConnection(session, flags) {
|
|
3777
|
+
const projectId = requireProjectId(flags, 'valkey connection');
|
|
3778
|
+
if (!projectId) return;
|
|
3779
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3780
|
+
const data = unwrapData(await request(session, flags, 'GET', `/managed-services/${projectId}/connection`, { orgId }));
|
|
3781
|
+
if (flags.json) return printJson(data);
|
|
3782
|
+
printValkeyConnection(data);
|
|
3783
|
+
}
|
|
3784
|
+
|
|
3785
|
+
async function handleValkeyCredentialStatus(session, flags) {
|
|
3786
|
+
const projectId = requireProjectId(flags, 'valkey credentials status');
|
|
3787
|
+
if (!projectId) return;
|
|
3788
|
+
if (!flags.operation) {
|
|
3789
|
+
printCommandHelpAndFail('valkey credentials status');
|
|
3790
|
+
return;
|
|
3791
|
+
}
|
|
3792
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3793
|
+
const data = unwrapData(await request(
|
|
3794
|
+
session,
|
|
3795
|
+
flags,
|
|
3796
|
+
'GET',
|
|
3797
|
+
`/managed-services/${projectId}/credential-rotations/${flags.operation}`,
|
|
3798
|
+
{ orgId },
|
|
3799
|
+
));
|
|
3800
|
+
if (flags.json) return printJson(data);
|
|
3801
|
+
printValkeyRotation(data);
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3804
|
+
async function handleValkeyCredentialRotate(session, flags) {
|
|
3805
|
+
const projectId = requireProjectId(flags, 'valkey credentials rotate');
|
|
3806
|
+
if (!projectId) return;
|
|
3807
|
+
const idempotencyKey = String(flags.idempotencyKey || randomUUID());
|
|
3808
|
+
if (!/^[A-Za-z0-9._-]{16,200}$/.test(idempotencyKey)) {
|
|
3809
|
+
throw new CliError('--idempotency-key deve ter entre 16 e 200 caracteres: letras, numeros, ponto, underscore ou hifen.');
|
|
3810
|
+
}
|
|
3811
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3812
|
+
const acceptedPayload = await request(session, flags, 'PATCH', `/managed-services/${projectId}/credentials`, {
|
|
3813
|
+
orgId,
|
|
3814
|
+
body: {},
|
|
3815
|
+
headers: { 'Idempotency-Key': idempotencyKey },
|
|
3816
|
+
});
|
|
3817
|
+
const accepted = unwrapData(acceptedPayload);
|
|
3818
|
+
if (!accepted?.operation_id) throw new CliError('A API nao retornou o identificador da renovacao de credencial.');
|
|
3819
|
+
|
|
3820
|
+
if (flags.wait) {
|
|
3821
|
+
const completed = await waitForValkeyRotation(session, flags, projectId, accepted.operation_id, orgId);
|
|
3822
|
+
if (flags.json) return printJson(completed);
|
|
3823
|
+
printValkeyRotation(completed);
|
|
3824
|
+
return;
|
|
3825
|
+
}
|
|
3826
|
+
|
|
3827
|
+
if (flags.json) return printJson(acceptedPayload);
|
|
3828
|
+
printValkeyRotation(accepted, { accepted: true });
|
|
3829
|
+
}
|
|
3830
|
+
|
|
2763
3831
|
function buildQuery(flags, allowedKeys) {
|
|
2764
3832
|
const params = new URLSearchParams();
|
|
2765
3833
|
for (const key of allowedKeys) {
|
|
@@ -2820,6 +3888,25 @@ async function getBuildLogs(session, flags, projectId, buildId, orgId, { cursor
|
|
|
2820
3888
|
return unwrapData(await request(session, flags, 'GET', `/project/${projectId}/github/builds/${buildId}/logs${query}`, { orgId }))
|
|
2821
3889
|
}
|
|
2822
3890
|
|
|
3891
|
+
function parseIntegerOption(value, flagName, { defaultValue, min, max }) {
|
|
3892
|
+
if (value === undefined) return defaultValue;
|
|
3893
|
+
const parsed = Number(value);
|
|
3894
|
+
if (!Number.isInteger(parsed) || parsed < min || (max !== undefined && parsed > max)) {
|
|
3895
|
+
const range = max === undefined ? `maior ou igual a ${min}` : `entre ${min} e ${max}`;
|
|
3896
|
+
throw new CliError(`Informe ${flagName} com um inteiro ${range}.`);
|
|
3897
|
+
}
|
|
3898
|
+
return parsed;
|
|
3899
|
+
}
|
|
3900
|
+
|
|
3901
|
+
function parseSecondsOption(value, flagName, { defaultValue, min }) {
|
|
3902
|
+
if (value === undefined) return defaultValue;
|
|
3903
|
+
const parsed = Number(value);
|
|
3904
|
+
if (!Number.isFinite(parsed) || parsed < min) {
|
|
3905
|
+
throw new CliError(`Informe ${flagName} com um numero maior ou igual a ${min}.`);
|
|
3906
|
+
}
|
|
3907
|
+
return parsed;
|
|
3908
|
+
}
|
|
3909
|
+
|
|
2823
3910
|
function printBuildLogs(logs) {
|
|
2824
3911
|
for (const log of asArray(logs)) {
|
|
2825
3912
|
process.stdout.write(`${buildLogLineOf(log)}\n`)
|
|
@@ -2838,10 +3925,10 @@ async function followBuildLogs(session, flags, projectId, buildId, orgId, {
|
|
|
2838
3925
|
intervalSeconds = 5,
|
|
2839
3926
|
timeoutSeconds = 900,
|
|
2840
3927
|
} = {}) {
|
|
2841
|
-
let cursor =
|
|
2842
|
-
const pollLimit =
|
|
2843
|
-
const intervalMs =
|
|
2844
|
-
const timeoutMs =
|
|
3928
|
+
let cursor = parseIntegerOption(initialCursor, '--cursor', { defaultValue: 0, min: 0 })
|
|
3929
|
+
const pollLimit = parseIntegerOption(limit, '--limit', { defaultValue: 200, min: 1, max: 500 })
|
|
3930
|
+
const intervalMs = parseSecondsOption(intervalSeconds, '--interval', { defaultValue: 5, min: 0.1 }) * 1000
|
|
3931
|
+
const timeoutMs = parseSecondsOption(timeoutSeconds, '--timeout', { defaultValue: 900, min: 1 }) * 1000
|
|
2845
3932
|
const startedAt = Date.now()
|
|
2846
3933
|
|
|
2847
3934
|
while (true) {
|
|
@@ -2887,15 +3974,15 @@ async function handleBuildLogs(session, flags) {
|
|
|
2887
3974
|
}
|
|
2888
3975
|
|
|
2889
3976
|
const orgId = await resolveOrgId(session, flags);
|
|
2890
|
-
const cursor =
|
|
2891
|
-
const limit =
|
|
3977
|
+
const cursor = parseIntegerOption(flags.cursor, '--cursor', { defaultValue: 0, min: 0 })
|
|
3978
|
+
const limit = parseIntegerOption(flags.limit, '--limit', { defaultValue: 200, min: 1, max: 500 })
|
|
2892
3979
|
|
|
2893
3980
|
if (flags.follow) {
|
|
2894
3981
|
return followBuildLogs(session, flags, projectId, buildId, orgId, {
|
|
2895
3982
|
initialCursor: cursor,
|
|
2896
3983
|
limit,
|
|
2897
|
-
intervalSeconds:
|
|
2898
|
-
timeoutSeconds:
|
|
3984
|
+
intervalSeconds: parseSecondsOption(flags.interval, '--interval', { defaultValue: 5, min: 0.1 }),
|
|
3985
|
+
timeoutSeconds: parseSecondsOption(flags.timeout, '--timeout', { defaultValue: 900, min: 1 }),
|
|
2899
3986
|
})
|
|
2900
3987
|
}
|
|
2901
3988
|
|
|
@@ -2916,13 +4003,14 @@ async function handleDeployWatch(session, flags) {
|
|
|
2916
4003
|
return followBuildLogs(session, flags, projectId, buildId, orgId, {
|
|
2917
4004
|
initialCursor: 0,
|
|
2918
4005
|
limit: 200,
|
|
2919
|
-
intervalSeconds:
|
|
2920
|
-
timeoutSeconds:
|
|
4006
|
+
intervalSeconds: parseSecondsOption(flags.interval, '--interval', { defaultValue: 5, min: 0.1 }),
|
|
4007
|
+
timeoutSeconds: parseSecondsOption(flags.timeout, '--timeout', { defaultValue: 900, min: 1 }),
|
|
2921
4008
|
})
|
|
2922
4009
|
}
|
|
2923
4010
|
|
|
2924
4011
|
async function main() {
|
|
2925
4012
|
const { positional, flags } = parseArgs(process.argv.slice(2));
|
|
4013
|
+
validateKnownFlags(flags);
|
|
2926
4014
|
const [command, subcommand] = positional;
|
|
2927
4015
|
const session = await readSession();
|
|
2928
4016
|
try {
|
|
@@ -2966,16 +4054,30 @@ async function main() {
|
|
|
2966
4054
|
if (command === 'orgs') return handleOrgs(session, flags);
|
|
2967
4055
|
if (command === 'org' && subcommand === 'set') return handleOrgSet(session, flags);
|
|
2968
4056
|
if (command === 'projects') return handleProjects(session, flags);
|
|
4057
|
+
if (command === 'valkey' && subcommand === 'credentials' && positional.length === 2) {
|
|
4058
|
+
process.stdout.write(commandHelp(['valkey', 'credentials']));
|
|
4059
|
+
return;
|
|
4060
|
+
}
|
|
4061
|
+
if (command === 'valkey' && subcommand === 'status') return handleValkeyStatus(session, flags);
|
|
4062
|
+
if (command === 'valkey' && subcommand === 'connection') return handleValkeyConnection(session, flags);
|
|
4063
|
+
if (command === 'valkey' && subcommand === 'credentials' && positional[2] === 'rotate') return handleValkeyCredentialRotate(session, flags);
|
|
4064
|
+
if (command === 'valkey' && subcommand === 'credentials' && positional[2] === 'status') return handleValkeyCredentialStatus(session, flags);
|
|
2969
4065
|
if (command === 'project' && subcommand === 'info') return handleProjectInfo(session, flags);
|
|
2970
4066
|
if (command === 'project' && subcommand === 'url') return handleProjectUrl(session, flags);
|
|
2971
4067
|
if (command === 'project' && subcommand === 'logs') return handleProjectLogs(session, flags);
|
|
2972
4068
|
if (command === 'project' && subcommand === 'metrics') return handleProjectMetrics(session, flags);
|
|
2973
4069
|
if (command === 'project' && subcommand === 'network') return handleProjectNetwork(session, flags);
|
|
2974
4070
|
if (command === 'project' && subcommand === 'image' && positional[2] === 'set') return handleProjectImageSet(session, flags);
|
|
4071
|
+
if (command === 'project' && subcommand === 'exposure' && positional[2] === 'set') return handleProjectExposureSet(session, flags);
|
|
2975
4072
|
if (command === 'project' && subcommand === 'envs') return handleProjectEnvs(session, flags);
|
|
2976
4073
|
if (command === 'project' && subcommand === 'env' && positional[2] === 'add') return handleProjectEnvMutation(session, flags, 'add');
|
|
2977
4074
|
if (command === 'project' && subcommand === 'env' && positional[2] === 'update') return handleProjectEnvMutation(session, flags, 'update');
|
|
2978
4075
|
if (command === 'project' && subcommand === 'env' && positional[2] === 'remove') return handleProjectEnvMutation(session, flags, 'remove');
|
|
4076
|
+
if (command === 'project' && subcommand === 'autoscaling' && positional[2] === 'set') return handleProjectAutoscalingSet(session, flags);
|
|
4077
|
+
if (command === 'project' && subcommand === 'autoscaling' && positional[2] === 'disable') return handleProjectAutoscalingDisable(session, flags);
|
|
4078
|
+
if (command === 'project' && subcommand === 'autoscaling' && positional[2] === 'events') return handleProjectAutoscalingEvents(session, flags);
|
|
4079
|
+
if (command === 'project' && subcommand === 'autoscaling') return handleProjectAutoscaling(session, flags);
|
|
4080
|
+
if (command === 'project' && subcommand === 'billing' && positional[2] === 'usage') return handleProjectBillingUsage(session, flags);
|
|
2979
4081
|
if (command === 'project' && subcommand === 'instances' && positional[2] === 'set') return handleProjectInstancesSet(session, flags);
|
|
2980
4082
|
if (command === 'project' && subcommand === 'instances') return handleProjectInstances(session, flags);
|
|
2981
4083
|
if (command === 'builds' && subcommand === 'logs') return handleBuildLogs(session, flags);
|