@zenifra/cli 0.2.2 → 0.2.4
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 +54 -2
- package/bin/zenifra.mjs +834 -77
- package/examples/http-autoscaling-project.json +30 -0
- package/examples/http-github-project.json +1 -0
- package/examples/http-project.json +1 -0
- package/package.json +4 -2
package/bin/zenifra.mjs
CHANGED
|
@@ -21,7 +21,53 @@ const SESSION_FILE = join(SESSION_DIR, 'session.json');
|
|
|
21
21
|
const PROFILES_FILE = join(SESSION_DIR, 'profiles.json');
|
|
22
22
|
const PROFILE_STORE_VERSION = 1;
|
|
23
23
|
const DEFAULT_PROFILE_NAME = 'default';
|
|
24
|
-
const
|
|
24
|
+
const DEFAULT_HTTP_TIMEOUT_MS = 30_000;
|
|
25
|
+
const KNOWN_FLAG_NAMES = new Set([
|
|
26
|
+
'apiBase',
|
|
27
|
+
'branch',
|
|
28
|
+
'build',
|
|
29
|
+
'challengeToken',
|
|
30
|
+
'code',
|
|
31
|
+
'commitSha',
|
|
32
|
+
'config',
|
|
33
|
+
'count',
|
|
34
|
+
'cpu',
|
|
35
|
+
'cursor',
|
|
36
|
+
'description',
|
|
37
|
+
'direction',
|
|
38
|
+
'email',
|
|
39
|
+
'exposure',
|
|
40
|
+
'follow',
|
|
41
|
+
'from',
|
|
42
|
+
'help',
|
|
43
|
+
'image',
|
|
44
|
+
'instance',
|
|
45
|
+
'interval',
|
|
46
|
+
'json',
|
|
47
|
+
'key',
|
|
48
|
+
'limit',
|
|
49
|
+
'max',
|
|
50
|
+
'memory',
|
|
51
|
+
'min',
|
|
52
|
+
'mode',
|
|
53
|
+
'name',
|
|
54
|
+
'org',
|
|
55
|
+
'page',
|
|
56
|
+
'password',
|
|
57
|
+
'paymentMode',
|
|
58
|
+
'plan',
|
|
59
|
+
'profile',
|
|
60
|
+
'project',
|
|
61
|
+
'revoke',
|
|
62
|
+
'showValues',
|
|
63
|
+
'status',
|
|
64
|
+
'timeout',
|
|
65
|
+
'to',
|
|
66
|
+
'totp',
|
|
67
|
+
'type',
|
|
68
|
+
'value',
|
|
69
|
+
'view',
|
|
70
|
+
]);
|
|
25
71
|
const ALLOWED_PLAN_VALUES = new Set([
|
|
26
72
|
'free',
|
|
27
73
|
'static',
|
|
@@ -40,6 +86,7 @@ const ALLOWED_PLAN_VALUES = new Set([
|
|
|
40
86
|
const ALLOWED_PAYMENT_MODE_VALUES = new Set(['hourly', 'monthly', 'yearly']);
|
|
41
87
|
const ALLOWED_TYPE_PROJECT_VALUES = new Set(['http', 'postgresql', 'mariadb']);
|
|
42
88
|
const ALLOWED_RUNTIME_VALUES = new Set(['nodejs', 'python']);
|
|
89
|
+
const ALLOWED_EXPOSURE_VALUES = new Set(['public', 'private']);
|
|
43
90
|
const GITHUB_RUNTIME_VERSIONS = {
|
|
44
91
|
nodejs: ['24', '22', '20'],
|
|
45
92
|
python: ['3.13', '3.12', '3.11'],
|
|
@@ -69,6 +116,7 @@ const WIZARD_DATABASE_PLAN_OPTIONS = [
|
|
|
69
116
|
];
|
|
70
117
|
const WIZARD_PAYMENT_MODE_OPTIONS = ['hourly', 'monthly', 'yearly'];
|
|
71
118
|
const WIZARD_TYPE_PROJECT_OPTIONS = ['http', 'postgresql', 'mariadb'];
|
|
119
|
+
const WIZARD_HTTP_EXPOSURE_OPTIONS = ['public', 'private'];
|
|
72
120
|
let promptInterface;
|
|
73
121
|
let pipedInputPromise;
|
|
74
122
|
let pipedInputIndex = 0;
|
|
@@ -96,7 +144,7 @@ Usage:
|
|
|
96
144
|
zenifra help <command>
|
|
97
145
|
zenifra auth login [--profile <name>] [--api-base <url>] [--code <code>]
|
|
98
146
|
zenifra auth api-key --key <znf_key> [--profile <name>] [--api-base <url>]
|
|
99
|
-
zenifra auth logout [--profile <name>]
|
|
147
|
+
zenifra auth logout [--profile <name>] [--revoke]
|
|
100
148
|
zenifra profile list [--json]
|
|
101
149
|
zenifra profile show [<name>] [--json]
|
|
102
150
|
zenifra profile add [--name <name>] [--description <text>] [--api-base <url>] [--mode <api-key|login>] [--key <znf_key>] [--json]
|
|
@@ -115,13 +163,20 @@ Usage:
|
|
|
115
163
|
zenifra project metrics --project <id> [--instance <id>] [--json]
|
|
116
164
|
zenifra project network --project <id> [--view <summary|status-codes|routes|user-agents|request-events|source-ips>] [--json]
|
|
117
165
|
zenifra project image set --project <id> --image <image> [--json]
|
|
166
|
+
zenifra project exposure set --project <id> --exposure <public|private> [--json]
|
|
118
167
|
zenifra project envs --project <id> [--json] [--show-values]
|
|
119
168
|
zenifra project env add --project <id> --name <name> --value <value> [--json]
|
|
120
169
|
zenifra project env update --project <id> --name <name> --value <value> [--json]
|
|
121
170
|
zenifra project env remove --project <id> --name <name> [--json]
|
|
171
|
+
zenifra project autoscaling --project <id> [--json]
|
|
172
|
+
zenifra project autoscaling set --project <id> --min <n> --max <n> [--cpu <percent>] [--memory <percent>] [--json]
|
|
173
|
+
zenifra project autoscaling disable --project <id> [--json]
|
|
174
|
+
zenifra project autoscaling events --project <id> [--direction <scale_up|scale_down>] [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]
|
|
175
|
+
zenifra project billing usage --project <id> [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]
|
|
122
176
|
zenifra project instances --project <id> [--json]
|
|
123
177
|
zenifra project instances set --project <id> --count <n> [--json]
|
|
124
178
|
zenifra builds --project <id> [--page <n>] [--limit <n>] [--branch <name>] [--status <status>] [--json]
|
|
179
|
+
zenifra builds logs --project <id> --build <id> [--cursor <n>] [--limit <n>] [--follow] [--interval <seconds>] [--timeout <seconds>] [--json]
|
|
125
180
|
zenifra deployments --project <id> [--page <n>] [--limit <n>] [--branch <name>] [--status <status>] [--json]
|
|
126
181
|
zenifra deploy --project <id> [--branch <name>] [--commit-sha <sha>] [--json]
|
|
127
182
|
zenifra deploy watch --project <id> --build <id> [--interval <seconds>] [--timeout <seconds>] [--json]
|
|
@@ -138,7 +193,7 @@ Run "zenifra help <command>" or "zenifra <command> --help" for command-specific
|
|
|
138
193
|
const HELP_SPECS = [
|
|
139
194
|
{
|
|
140
195
|
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>]',
|
|
196
|
+
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
197
|
description: 'Gerencia a autenticacao dos perfis locais da CLI.',
|
|
143
198
|
examples: ['zenifra auth', 'zenifra auth login --profile staging', 'zenifra auth api-key --profile prod --key znf_0123456789abcdef01234567_abcd...'],
|
|
144
199
|
output: 'Zenifra CLI - auth',
|
|
@@ -168,11 +223,12 @@ const HELP_SPECS = [
|
|
|
168
223
|
},
|
|
169
224
|
{
|
|
170
225
|
command: 'auth logout',
|
|
171
|
-
usage: 'zenifra auth logout [--profile <name>]',
|
|
226
|
+
usage: 'zenifra auth logout [--profile <name>] [--revoke]',
|
|
172
227
|
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'],
|
|
228
|
+
flags: ['--profile <name> Limpa outro perfil sem trocar o perfil ativo.', '--revoke Revoga as sessoes de usuario no servidor antes de limpar o perfil.'],
|
|
229
|
+
examples: ['zenifra auth logout', 'zenifra auth logout --profile staging', 'zenifra auth logout --revoke'],
|
|
175
230
|
output: 'Autenticacao removida do perfil active.',
|
|
231
|
+
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
232
|
},
|
|
177
233
|
{
|
|
178
234
|
command: 'profile',
|
|
@@ -291,11 +347,11 @@ const HELP_SPECS = [
|
|
|
291
347
|
},
|
|
292
348
|
{
|
|
293
349
|
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]',
|
|
350
|
+
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
351
|
description: 'Agrupa comandos operacionais e de introspecao sobre um projeto especifico.',
|
|
296
352
|
examples: ['zenifra project', 'zenifra project info --project proj_1', 'zenifra project env add --project proj_1 --name NODE_ENV --value production'],
|
|
297
353
|
output: 'Zenifra CLI - project',
|
|
298
|
-
notes: ['Use "zenifra help project <subcomando>" para detalhes de info, url, logs, metrics, network, image, envs e instances.'],
|
|
354
|
+
notes: ['Use "zenifra help project <subcomando>" para detalhes de info, url, logs, metrics, network, image, exposure, autoscaling, billing, envs e instances.'],
|
|
299
355
|
},
|
|
300
356
|
{
|
|
301
357
|
command: 'project info',
|
|
@@ -352,6 +408,16 @@ const HELP_SPECS = [
|
|
|
352
408
|
output: 'Imagem atualizada com sucesso.',
|
|
353
409
|
jsonOutput: '{"status":"success","message":"updated with success"}',
|
|
354
410
|
},
|
|
411
|
+
{
|
|
412
|
+
command: 'project exposure set',
|
|
413
|
+
usage: 'zenifra project exposure set --project <id> --exposure <public|private> [--json]',
|
|
414
|
+
description: 'Altera se um projeto HTTP expoe rota publica e dominio.',
|
|
415
|
+
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.'],
|
|
416
|
+
examples: ['zenifra project exposure set --project 507f1f77bcf86cd799439012 --exposure private', 'zenifra project exposure set --project 507f1f77bcf86cd799439012 --exposure public --json'],
|
|
417
|
+
output: 'Campo Valor\n--------- ----------------------------------\nExposicao private\nDominio -',
|
|
418
|
+
jsonOutput: '{"status":"success","message":"project exposure updated with success","exposure":"private","custom_domains":[]}',
|
|
419
|
+
notes: ['Ao mudar para private, a API remove a rota publica e dominios personalizados do projeto.'],
|
|
420
|
+
},
|
|
355
421
|
{
|
|
356
422
|
command: 'project envs',
|
|
357
423
|
usage: 'zenifra project envs --project <id> [--json] [--show-values]',
|
|
@@ -392,10 +458,55 @@ const HELP_SPECS = [
|
|
|
392
458
|
jsonOutput: '{"status":"success","message":"updated with success","envs":[]}',
|
|
393
459
|
notes: ['Falha se a variavel nao existir.'],
|
|
394
460
|
},
|
|
461
|
+
{
|
|
462
|
+
command: 'project autoscaling',
|
|
463
|
+
usage: 'zenifra project autoscaling --project <id> [--json]',
|
|
464
|
+
description: 'Mostra a configuracao de auto-scaling HTTP do projeto.',
|
|
465
|
+
flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.'],
|
|
466
|
+
examples: ['zenifra project autoscaling --project 507f1f77bcf86cd799439012'],
|
|
467
|
+
output: 'Campo Valor\n---------- -----\nStatus ativo\nMinimo 2\nMaximo 8\nCPU alvo 70%\nMemoria 80%',
|
|
468
|
+
jsonOutput: '{"enabled":true,"min_instances":2,"max_instances":8,"target_cpu_utilization_percent":70,"target_memory_utilization_percent":80}',
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
command: 'project autoscaling set',
|
|
472
|
+
usage: 'zenifra project autoscaling set --project <id> --min <n> --max <n> [--cpu <percent>] [--memory <percent>] [--json]',
|
|
473
|
+
description: 'Ativa ou atualiza o auto-scaling HTTP do projeto.',
|
|
474
|
+
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.'],
|
|
475
|
+
examples: ['zenifra project autoscaling set --project 507f1f77bcf86cd799439012 --min 2 --max 8 --cpu 70 --memory 80'],
|
|
476
|
+
output: 'Auto-scaling atualizado: 2-8 instancias.',
|
|
477
|
+
jsonOutput: '{"status":"success","message":"project autoscaling updated with success","data":{"autoscaling":{"enabled":true,"min_instances":2,"max_instances":8}}}',
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
command: 'project autoscaling disable',
|
|
481
|
+
usage: 'zenifra project autoscaling disable --project <id> [--json]',
|
|
482
|
+
description: 'Desativa o auto-scaling HTTP e volta para instancias fixas no minimo configurado.',
|
|
483
|
+
flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.'],
|
|
484
|
+
examples: ['zenifra project autoscaling disable --project 507f1f77bcf86cd799439012'],
|
|
485
|
+
output: 'Auto-scaling desativado.',
|
|
486
|
+
jsonOutput: '{"status":"success","message":"project autoscaling updated with success","data":{"autoscaling":{"enabled":false}}}',
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
command: 'project autoscaling events',
|
|
490
|
+
usage: 'zenifra project autoscaling events --project <id> [--direction <scale_up|scale_down>] [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]',
|
|
491
|
+
description: 'Lista o historico de scale up e scale down do auto-scaling HTTP.',
|
|
492
|
+
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.'],
|
|
493
|
+
examples: ['zenifra project autoscaling events --project 507f1f77bcf86cd799439012 --direction scale_up'],
|
|
494
|
+
output: 'Quando Direcao Instancias Limites Motivo\n05/06/2026, 18:40 aumento 2 -> 5 CPU 91%/70% capacidade aumentada',
|
|
495
|
+
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}}',
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
command: 'project billing usage',
|
|
499
|
+
usage: 'zenifra project billing usage --project <id> [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]',
|
|
500
|
+
description: 'Lista o consumo horario consolidado de um projeto.',
|
|
501
|
+
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.'],
|
|
502
|
+
examples: ['zenifra project billing usage --project 507f1f77bcf86cd799439012 --from 2026-06-01T00:00:00Z --limit 20'],
|
|
503
|
+
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',
|
|
504
|
+
jsonOutput: '{"hours":[],"summary":{"currency":"brl","compute_amount":0,"storage_amount":0,"total_amount":0},"pagination":{"page":1,"limit":10,"total":0,"total_pages":0}}',
|
|
505
|
+
},
|
|
395
506
|
{
|
|
396
507
|
command: 'project instances',
|
|
397
508
|
usage: 'zenifra project instances --project <id> [--json]',
|
|
398
|
-
description: 'Lista instancias
|
|
509
|
+
description: 'Lista instancias do projeto.',
|
|
399
510
|
flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.'],
|
|
400
511
|
examples: ['zenifra project instances --project 507f1f77bcf86cd799439012'],
|
|
401
512
|
output: 'Instancia\nweb-1',
|
|
@@ -419,6 +530,24 @@ const HELP_SPECS = [
|
|
|
419
530
|
output: 'Build Status Branch Commit\nbuild_123 success main abc123',
|
|
420
531
|
jsonOutput: '[{"id":"build_123","status":"success","branch":"main","commit_sha":"abc123"}]',
|
|
421
532
|
},
|
|
533
|
+
{
|
|
534
|
+
command: 'builds logs',
|
|
535
|
+
usage: 'zenifra builds logs --project <id> --build <id> [--cursor <n>] [--limit <n>] [--follow] [--interval <seconds>] [--timeout <seconds>] [--json]',
|
|
536
|
+
description: 'Lê os logs de um build GitHub existente e opcionalmente acompanha novos chunks em tempo real.',
|
|
537
|
+
flags: [
|
|
538
|
+
'--project <id> ID do projeto.',
|
|
539
|
+
'--build <id> ID do build.',
|
|
540
|
+
'--cursor <n> Cursor inicial inteiro, maior ou igual a 0. Padrao: 0.',
|
|
541
|
+
'--limit <n> Quantidade de logs por request, entre 1 e 500. Padrao: 200.',
|
|
542
|
+
'--follow Continua em polling ate o build finalizar.',
|
|
543
|
+
'--interval <seconds> Intervalo de polling com --follow, minimo 0.1. Padrao: 5.',
|
|
544
|
+
'--timeout <seconds> Timeout total com --follow, minimo 1. Padrao: 900.',
|
|
545
|
+
'--json Imprime a resposta em JSON.',
|
|
546
|
+
],
|
|
547
|
+
examples: ['zenifra builds logs --project 507f1f77bcf86cd799439012 --build build_123', 'zenifra builds logs --project 507f1f77bcf86cd799439012 --build build_123 --follow'],
|
|
548
|
+
output: '[2026-05-27T12:00:00.000Z] install: npm ci\n[2026-05-27T12:00:05.000Z] build: npm run build',
|
|
549
|
+
jsonOutput: '{"logs":[{"sequence":1,"timestamp":"2026-05-27T12:00:00.000Z","level":"info","step":"install","message":"npm ci","final":false}],"next_cursor":1,"status":"running","finished":false,"truncated":false}',
|
|
550
|
+
},
|
|
422
551
|
{
|
|
423
552
|
command: 'deployments',
|
|
424
553
|
usage: 'zenifra deployments --project <id> [--page <n>] [--limit <n>] [--branch <name>] [--status <status>] [--json]',
|
|
@@ -431,20 +560,35 @@ const HELP_SPECS = [
|
|
|
431
560
|
{
|
|
432
561
|
command: 'deploy',
|
|
433
562
|
usage: 'zenifra deploy --project <id> [--branch <name>] [--commit-sha <sha>] [--json]',
|
|
434
|
-
description: 'Dispara um deploy GitHub para o projeto.',
|
|
563
|
+
description: 'Dispara um build/deploy GitHub para o projeto e retorna o build_id para acompanhamento posterior.',
|
|
435
564
|
flags: ['--project <id> ID do projeto.', '--branch <name> Branch a publicar.', '--commit-sha <sha> Commit especifico.', '--json Imprime a resposta em JSON.'],
|
|
436
|
-
examples: [
|
|
565
|
+
examples: [
|
|
566
|
+
'zenifra deploy --project 507f1f77bcf86cd799439012 --branch main',
|
|
567
|
+
'zenifra deploy watch --project 507f1f77bcf86cd799439012 --build build_123',
|
|
568
|
+
],
|
|
437
569
|
output: 'Deploy iniciado: build_123',
|
|
438
570
|
jsonOutput: '{"status":"success","data":{"build_id":"build_123"}}',
|
|
571
|
+
notes: [
|
|
572
|
+
'Use o build_id retornado para acompanhar a execucao com "zenifra deploy watch --project <id> --build <build_id>".',
|
|
573
|
+
'O subcomando "deploy watch" acompanha status e logs incrementais do build ate o estado terminal.',
|
|
574
|
+
],
|
|
439
575
|
},
|
|
440
576
|
{
|
|
441
577
|
command: 'deploy watch',
|
|
442
578
|
usage: 'zenifra deploy watch --project <id> --build <id> [--interval <seconds>] [--timeout <seconds>] [--json]',
|
|
443
|
-
description: 'Acompanha um build
|
|
444
|
-
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.'],
|
|
445
|
-
examples: [
|
|
446
|
-
|
|
447
|
-
|
|
579
|
+
description: 'Acompanha um build em tempo real, fazendo polling do status e imprimindo os logs incrementais do build ate o estado terminal.',
|
|
580
|
+
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.'],
|
|
581
|
+
examples: [
|
|
582
|
+
'zenifra deploy watch --project 507f1f77bcf86cd799439012 --build build_123',
|
|
583
|
+
'zenifra deploy watch --project 507f1f77bcf86cd799439012 --build build_123 --interval 2',
|
|
584
|
+
'zenifra deploy watch --project 507f1f77bcf86cd799439012 --build build_123 --json',
|
|
585
|
+
],
|
|
586
|
+
output: '[2026-05-27T12:00:00.000Z] install: npm ci\n[2026-05-27T12:00:05.000Z] build: npm run build\n[2026-05-27T12:00:09.000Z] publish: Build concluida e publicada',
|
|
587
|
+
jsonOutput: '{"sequence":1,"timestamp":"2026-05-27T12:00:00.000Z","level":"info","step":"install","message":"npm ci","final":false}',
|
|
588
|
+
notes: [
|
|
589
|
+
'Use este subcomando logo apos "zenifra deploy" para acompanhar o build pelo build_id retornado.',
|
|
590
|
+
'No modo --json, o comando imprime eventos de log em streaming, um JSON por linha.',
|
|
591
|
+
],
|
|
448
592
|
},
|
|
449
593
|
];
|
|
450
594
|
|
|
@@ -495,6 +639,12 @@ function commandHelp(positionals) {
|
|
|
495
639
|
return formatHelp(spec);
|
|
496
640
|
}
|
|
497
641
|
|
|
642
|
+
function printCommandHelpAndFail(commandKey) {
|
|
643
|
+
const positionals = Array.isArray(commandKey) ? commandKey : String(commandKey).split(' ')
|
|
644
|
+
process.stdout.write(commandHelp(positionals))
|
|
645
|
+
process.exitCode = 1
|
|
646
|
+
}
|
|
647
|
+
|
|
498
648
|
function isRemovedProjectsCreate(positionals) {
|
|
499
649
|
return positionals[0] === 'projects' && positionals[1] === 'create';
|
|
500
650
|
}
|
|
@@ -539,6 +689,17 @@ function parseArgs(argv) {
|
|
|
539
689
|
return { positional, flags };
|
|
540
690
|
}
|
|
541
691
|
|
|
692
|
+
function flagNameForDisplay(value) {
|
|
693
|
+
return `--${String(value).replace(/[A-Z]/g, (character) => `-${character.toLowerCase()}`)}`;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function validateKnownFlags(flags) {
|
|
697
|
+
const unknown = Object.keys(flags).filter((name) => !KNOWN_FLAG_NAMES.has(name));
|
|
698
|
+
if (unknown.length > 0) {
|
|
699
|
+
throw new CliError(`Flag desconhecida: ${unknown.map(flagNameForDisplay).join(', ')}.`);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
542
703
|
function emptyProfileStore() {
|
|
543
704
|
return {
|
|
544
705
|
version: PROFILE_STORE_VERSION,
|
|
@@ -628,7 +789,9 @@ async function migrateLegacySession() {
|
|
|
628
789
|
});
|
|
629
790
|
store.profiles[DEFAULT_PROFILE_NAME] = migratedProfile;
|
|
630
791
|
store.activeProfile = DEFAULT_PROFILE_NAME;
|
|
631
|
-
|
|
792
|
+
const normalized = await writeProfileStore(store);
|
|
793
|
+
await rm(SESSION_FILE, { force: true });
|
|
794
|
+
return normalized;
|
|
632
795
|
}
|
|
633
796
|
|
|
634
797
|
async function readProfileStore() {
|
|
@@ -822,9 +985,16 @@ function estimateWizardTotal(state = {}) {
|
|
|
822
985
|
if (state.httpStoragePersistent) total += 1;
|
|
823
986
|
}
|
|
824
987
|
|
|
988
|
+
if (state.planAllowsAutoscaling) {
|
|
989
|
+
total += 1;
|
|
990
|
+
if (state.httpAutoscalingEnabled) total += 3;
|
|
991
|
+
}
|
|
992
|
+
|
|
825
993
|
total += 1;
|
|
826
994
|
if (state.envs?.enabled) total += state.envs.count * 3;
|
|
827
995
|
|
|
996
|
+
total += 1;
|
|
997
|
+
|
|
828
998
|
if (state.httpSource === 'github') {
|
|
829
999
|
total += 9;
|
|
830
1000
|
} else if (state.httpSource === 'oci') {
|
|
@@ -838,13 +1008,13 @@ function estimateWizardTotal(state = {}) {
|
|
|
838
1008
|
total += 1;
|
|
839
1009
|
}
|
|
840
1010
|
|
|
841
|
-
if (state.planAllowsBlockIp) {
|
|
1011
|
+
if (state.httpExposure !== 'private' && state.planAllowsBlockIp) {
|
|
842
1012
|
total += 2;
|
|
843
1013
|
if (state.whitelist?.enabled) total += state.whitelist.count * 3;
|
|
844
1014
|
if (state.blacklist?.enabled) total += state.blacklist.count * 3;
|
|
845
1015
|
}
|
|
846
1016
|
|
|
847
|
-
if (state.planAllowsSubdomain) total += 1;
|
|
1017
|
+
if (state.httpExposure !== 'private' && state.planAllowsSubdomain) total += 1;
|
|
848
1018
|
return total;
|
|
849
1019
|
}
|
|
850
1020
|
|
|
@@ -1160,8 +1330,24 @@ async function promptHidden(question) {
|
|
|
1160
1330
|
});
|
|
1161
1331
|
}
|
|
1162
1332
|
|
|
1163
|
-
|
|
1164
|
-
const
|
|
1333
|
+
function httpTimeoutMs() {
|
|
1334
|
+
const configured = process.env.ZENIFRA_HTTP_TIMEOUT_MS;
|
|
1335
|
+
if (configured === undefined) return DEFAULT_HTTP_TIMEOUT_MS;
|
|
1336
|
+
|
|
1337
|
+
const value = Number(configured);
|
|
1338
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
1339
|
+
throw new CliError('ZENIFRA_HTTP_TIMEOUT_MS deve ser um inteiro positivo em milissegundos.');
|
|
1340
|
+
}
|
|
1341
|
+
return value;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
async function request(session, flags, method, path, {
|
|
1345
|
+
body,
|
|
1346
|
+
orgId,
|
|
1347
|
+
tokenRequired = true,
|
|
1348
|
+
credential: credentialOverride,
|
|
1349
|
+
} = {}) {
|
|
1350
|
+
const credential = credentialOverride || resolveCredential(session);
|
|
1165
1351
|
if (tokenRequired && !credential) {
|
|
1166
1352
|
throw new CliError('Voce precisa autenticar primeiro: zenifra auth login, zenifra auth api-key --key <znf_key> ou ZENIFRA_API_KEY.');
|
|
1167
1353
|
}
|
|
@@ -1177,11 +1363,21 @@ async function request(session, flags, method, path, { body, orgId, tokenRequire
|
|
|
1177
1363
|
headers['x-organization-id'] = orgId;
|
|
1178
1364
|
}
|
|
1179
1365
|
|
|
1180
|
-
const
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1366
|
+
const timeoutMs = httpTimeoutMs();
|
|
1367
|
+
let response;
|
|
1368
|
+
try {
|
|
1369
|
+
response = await fetch(`${apiBaseUrl(session, flags)}${path}`, {
|
|
1370
|
+
method,
|
|
1371
|
+
headers,
|
|
1372
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
1373
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
1374
|
+
});
|
|
1375
|
+
} catch (error) {
|
|
1376
|
+
if (error?.name === 'TimeoutError' || error?.name === 'AbortError') {
|
|
1377
|
+
throw new CliError(`A API da Zenifra nao respondeu em ate ${timeoutMs} ms.`);
|
|
1378
|
+
}
|
|
1379
|
+
throw new CliError(`Nao foi possivel conectar a API da Zenifra: ${error?.message || 'erro de rede'}.`);
|
|
1380
|
+
}
|
|
1185
1381
|
|
|
1186
1382
|
const text = await response.text();
|
|
1187
1383
|
let payload = {};
|
|
@@ -1195,12 +1391,18 @@ async function request(session, flags, method, path, { body, orgId, tokenRequire
|
|
|
1195
1391
|
|
|
1196
1392
|
if (!response.ok) {
|
|
1197
1393
|
const message = payload?.message || payload?.error || `Zenifra API retornou HTTP ${response.status}`;
|
|
1198
|
-
if (response.status === 401
|
|
1394
|
+
if (response.status === 401) {
|
|
1199
1395
|
if (credential?.type === 'api_key') {
|
|
1200
|
-
throw new CliError(`${message}. Verifique se a API key esta ativa
|
|
1396
|
+
throw new CliError(`${message}. Verifique se a API key esta ativa e se o IP atual esta permitido.`);
|
|
1201
1397
|
}
|
|
1202
1398
|
throw new CliError(`${message}. Rode "zenifra auth login" para renovar sua sessao.`);
|
|
1203
1399
|
}
|
|
1400
|
+
if (response.status === 403) {
|
|
1401
|
+
throw new CliError(message);
|
|
1402
|
+
}
|
|
1403
|
+
if (response.status === 429 && Number.isFinite(Number(payload?.retry_after_seconds))) {
|
|
1404
|
+
throw new CliError(`${message} Tente novamente em ${Number(payload.retry_after_seconds)} segundo(s).`);
|
|
1405
|
+
}
|
|
1204
1406
|
if (String(message).includes('missing x-organization-id')) {
|
|
1205
1407
|
throw new CliError('Organizacao nao selecionada. Rode "zenifra org set" ou use --org <id>.');
|
|
1206
1408
|
}
|
|
@@ -1262,8 +1464,18 @@ function buildIdOf(build) {
|
|
|
1262
1464
|
return build?.id || build?._id || build?.build_id || '';
|
|
1263
1465
|
}
|
|
1264
1466
|
|
|
1265
|
-
function
|
|
1266
|
-
|
|
1467
|
+
function buildLogLineOf(log) {
|
|
1468
|
+
const timestamp = log?.timestamp ? new Date(log.timestamp).toISOString() : new Date().toISOString()
|
|
1469
|
+
const step = log?.step || 'build'
|
|
1470
|
+
const message = log?.message || ''
|
|
1471
|
+
return `[${timestamp}] ${step}: ${message}`
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
function requireProjectId(flags, commandKey) {
|
|
1475
|
+
if (!flags.project) {
|
|
1476
|
+
printCommandHelpAndFail(commandKey)
|
|
1477
|
+
return null
|
|
1478
|
+
}
|
|
1267
1479
|
return String(flags.project);
|
|
1268
1480
|
}
|
|
1269
1481
|
|
|
@@ -1302,6 +1514,7 @@ function printProject(project) {
|
|
|
1302
1514
|
{ label: 'Nome', value: project.name || '-' },
|
|
1303
1515
|
{ label: 'Status', value: project.status || '-' },
|
|
1304
1516
|
{ label: 'Tipo', value: project.type_project || '-' },
|
|
1517
|
+
{ label: 'Exposicao', value: project.exposure || '-' },
|
|
1305
1518
|
{ label: 'Plano', value: project.plan || '-' },
|
|
1306
1519
|
{ label: 'URL', value: url || '-' },
|
|
1307
1520
|
{ label: 'Imagem', value: project.image || '-' },
|
|
@@ -1458,6 +1671,18 @@ async function handleLogout(session, flags) {
|
|
|
1458
1671
|
throw new CliError('Nenhum perfil ativo configurado. Crie um perfil com "zenifra profile add" ou autentique com "zenifra auth login".');
|
|
1459
1672
|
}
|
|
1460
1673
|
const target = requireExistingProfile(session, targetName);
|
|
1674
|
+
if (flags.revoke) {
|
|
1675
|
+
if (!target.accessToken) {
|
|
1676
|
+
throw new CliError('A revogacao exige um perfil autenticado por login de usuario; API keys devem ser revogadas na organizacao.');
|
|
1677
|
+
}
|
|
1678
|
+
await request(target, flags, 'DELETE', '/authentication', {
|
|
1679
|
+
credential: {
|
|
1680
|
+
token: target.accessToken,
|
|
1681
|
+
type: 'user',
|
|
1682
|
+
source: 'profile',
|
|
1683
|
+
},
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1461
1686
|
target.accessToken = undefined;
|
|
1462
1687
|
target.apiKey = undefined;
|
|
1463
1688
|
target.selectedOrganizationId = undefined;
|
|
@@ -1467,7 +1692,7 @@ async function handleLogout(session, flags) {
|
|
|
1467
1692
|
if (target.__profileName === getProfileName(session)) {
|
|
1468
1693
|
Object.assign(session, buildSession(target.__store));
|
|
1469
1694
|
}
|
|
1470
|
-
process.stdout.write(`Autenticacao removida do perfil ${target.__profileName}.\n`);
|
|
1695
|
+
process.stdout.write(`Autenticacao removida do perfil ${target.__profileName}${flags.revoke ? ' e sessoes de usuario revogadas' : ''}.\n`);
|
|
1471
1696
|
}
|
|
1472
1697
|
|
|
1473
1698
|
function maskApiKey(value) {
|
|
@@ -1528,7 +1753,10 @@ async function handleProfileShow(session, flags, positional = []) {
|
|
|
1528
1753
|
|
|
1529
1754
|
async function handleProfileUse(session, flags, positional = []) {
|
|
1530
1755
|
const targetName = positional[2] || flags.name;
|
|
1531
|
-
if (!targetName)
|
|
1756
|
+
if (!targetName) {
|
|
1757
|
+
printCommandHelpAndFail('profile use')
|
|
1758
|
+
return
|
|
1759
|
+
}
|
|
1532
1760
|
activateProfileSession(session, targetName);
|
|
1533
1761
|
await writeProfileStore(getStore(session));
|
|
1534
1762
|
if (flags.json) return printJson({ active_profile: getProfileName(session) });
|
|
@@ -1578,7 +1806,10 @@ async function handleProfileAdd(session, flags) {
|
|
|
1578
1806
|
|
|
1579
1807
|
async function handleProfileEdit(session, flags, positional = []) {
|
|
1580
1808
|
const targetName = positional[2] || flags.name;
|
|
1581
|
-
if (!targetName)
|
|
1809
|
+
if (!targetName) {
|
|
1810
|
+
printCommandHelpAndFail('profile edit')
|
|
1811
|
+
return
|
|
1812
|
+
}
|
|
1582
1813
|
const target = requireExistingProfile(session, targetName);
|
|
1583
1814
|
target.description = String(flags.description ?? await prompt('Descricao do perfil', target.description || ''));
|
|
1584
1815
|
target.apiBaseUrl = normalizeApiBaseUrl(flags.apiBase || await prompt('API base', target.apiBaseUrl || DEFAULT_API_BASE_URL));
|
|
@@ -1594,7 +1825,10 @@ async function handleProfileEdit(session, flags, positional = []) {
|
|
|
1594
1825
|
|
|
1595
1826
|
async function handleProfileRemove(session, flags, positional = []) {
|
|
1596
1827
|
const targetName = positional[2] || flags.name;
|
|
1597
|
-
if (!targetName)
|
|
1828
|
+
if (!targetName) {
|
|
1829
|
+
printCommandHelpAndFail('profile remove')
|
|
1830
|
+
return
|
|
1831
|
+
}
|
|
1598
1832
|
const normalizedName = normalizeProfileName(targetName);
|
|
1599
1833
|
const store = getStore(session);
|
|
1600
1834
|
if (!store.profiles[normalizedName]) {
|
|
@@ -1743,6 +1977,22 @@ function normalizeTypeProject(value) {
|
|
|
1743
1977
|
return aliases.get(raw) || raw;
|
|
1744
1978
|
}
|
|
1745
1979
|
|
|
1980
|
+
function normalizeExposure(value) {
|
|
1981
|
+
const raw = normalizeFreeText(value);
|
|
1982
|
+
const aliases = new Map([
|
|
1983
|
+
['public', 'public'],
|
|
1984
|
+
['publico', 'public'],
|
|
1985
|
+
['público', 'public'],
|
|
1986
|
+
['exposto', 'public'],
|
|
1987
|
+
['sim', 'public'],
|
|
1988
|
+
['private', 'private'],
|
|
1989
|
+
['privado', 'private'],
|
|
1990
|
+
['nao', 'private'],
|
|
1991
|
+
['não', 'private'],
|
|
1992
|
+
]);
|
|
1993
|
+
return aliases.get(raw) || raw;
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1746
1996
|
function normalizeRuntime(value) {
|
|
1747
1997
|
const raw = normalizeFreeText(value);
|
|
1748
1998
|
const aliases = new Map([
|
|
@@ -1784,6 +2034,13 @@ function isIpAccessEnabled(planCatalog, planId) {
|
|
|
1784
2034
|
return planHasFeature(planCatalog, planId, 'Bloqueio de IPs');
|
|
1785
2035
|
}
|
|
1786
2036
|
|
|
2037
|
+
function isAutoscalingEnabledForPlan(planCatalog, planId) {
|
|
2038
|
+
const plan = Array.isArray(planCatalog)
|
|
2039
|
+
? planCatalog.find((item) => item?.plan === planId)
|
|
2040
|
+
: undefined;
|
|
2041
|
+
return plan?.permissions?.allow_autoscaling === 'true';
|
|
2042
|
+
}
|
|
2043
|
+
|
|
1787
2044
|
function getMaxReplicasFromFeatures(features) {
|
|
1788
2045
|
const replicaFeature = features.find((feature) => /réplicas?/i.test(String(feature)));
|
|
1789
2046
|
if (!replicaFeature) return 5;
|
|
@@ -1958,6 +2215,52 @@ function formatAllowedValues(values) {
|
|
|
1958
2215
|
return [...values].join(', ');
|
|
1959
2216
|
}
|
|
1960
2217
|
|
|
2218
|
+
function validateCreateAutoscaling(config, plan, typeProject) {
|
|
2219
|
+
if (config?.autoscaling === undefined) return config;
|
|
2220
|
+
if (typeProject !== 'http') {
|
|
2221
|
+
throw new CliError('config.autoscaling deve ser informado apenas para projetos HTTP.');
|
|
2222
|
+
}
|
|
2223
|
+
if (plan === 'free') {
|
|
2224
|
+
throw new CliError('Auto-scaling nao esta disponivel para projetos HTTP no plano free.');
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
const autoscaling = config.autoscaling;
|
|
2228
|
+
if (!autoscaling || typeof autoscaling !== 'object' || Array.isArray(autoscaling)) {
|
|
2229
|
+
throw new CliError('config.autoscaling deve ser um objeto.');
|
|
2230
|
+
}
|
|
2231
|
+
if (autoscaling.enabled !== true) {
|
|
2232
|
+
throw new CliError('config.autoscaling.enabled deve ser true na criacao do projeto.');
|
|
2233
|
+
}
|
|
2234
|
+
if (autoscaling.min_instances !== undefined) {
|
|
2235
|
+
throw new CliError('Use config.instances como minimo inicial; config.autoscaling.min_instances nao e aceito na criacao.');
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
const instances = Number(config.instances);
|
|
2239
|
+
if (!Number.isInteger(instances) || instances <= 0) {
|
|
2240
|
+
throw new CliError('config.instances deve ser um inteiro positivo ao criar um projeto com auto-scaling.');
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
const maxInstances = Number(autoscaling.max_instances);
|
|
2244
|
+
if (!Number.isInteger(maxInstances) || maxInstances < instances) {
|
|
2245
|
+
throw new CliError('config.autoscaling.max_instances deve ser um inteiro maior ou igual a config.instances.');
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
const nextAutoscaling = {
|
|
2249
|
+
enabled: true,
|
|
2250
|
+
max_instances: maxInstances,
|
|
2251
|
+
};
|
|
2252
|
+
for (const key of ['target_cpu_utilization_percent', 'target_memory_utilization_percent']) {
|
|
2253
|
+
if (autoscaling[key] === undefined) continue;
|
|
2254
|
+
const value = Number(autoscaling[key]);
|
|
2255
|
+
if (!Number.isInteger(value) || value < 1 || value > 100) {
|
|
2256
|
+
throw new CliError(`config.autoscaling.${key} deve ser um inteiro entre 1 e 100.`);
|
|
2257
|
+
}
|
|
2258
|
+
nextAutoscaling[key] = value;
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
return { ...config, autoscaling: nextAutoscaling };
|
|
2262
|
+
}
|
|
2263
|
+
|
|
1961
2264
|
function validateCreateInput({ plan, paymentMode, config }) {
|
|
1962
2265
|
const nextPlan = normalizePlan(plan);
|
|
1963
2266
|
if (!nextPlan || !ALLOWED_PLAN_VALUES.has(nextPlan)) {
|
|
@@ -1983,10 +2286,22 @@ function validateCreateInput({ plan, paymentMode, config }) {
|
|
|
1983
2286
|
nextConfig.github = { ...nextConfig.github, runtime: nextRuntime };
|
|
1984
2287
|
}
|
|
1985
2288
|
|
|
2289
|
+
if (nextTypeProject === 'http') {
|
|
2290
|
+
const nextExposure = normalizeExposure(nextConfig.exposure);
|
|
2291
|
+
if (!nextExposure || !ALLOWED_EXPOSURE_VALUES.has(nextExposure)) {
|
|
2292
|
+
throw new CliError(`exposure invalido: "${nextConfig.exposure || ''}". Valores aceitos: ${formatAllowedValues(ALLOWED_EXPOSURE_VALUES)}. Docs: ${DOCS_CREATE_HTTP_URL}`);
|
|
2293
|
+
}
|
|
2294
|
+
nextConfig.exposure = nextExposure;
|
|
2295
|
+
} else if (nextConfig.exposure !== undefined) {
|
|
2296
|
+
throw new CliError('exposure deve ser informado apenas para projetos HTTP.');
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
const validatedConfig = validateCreateAutoscaling(nextConfig, nextPlan, nextTypeProject);
|
|
2300
|
+
|
|
1986
2301
|
return {
|
|
1987
2302
|
plan: nextPlan,
|
|
1988
2303
|
paymentMode: nextPaymentMode,
|
|
1989
|
-
config:
|
|
2304
|
+
config: validatedConfig,
|
|
1990
2305
|
};
|
|
1991
2306
|
}
|
|
1992
2307
|
|
|
@@ -2014,7 +2329,10 @@ function printWizardSummary(payload) {
|
|
|
2014
2329
|
if (masked.description) lines.push(` descricao: ${masked.description}`);
|
|
2015
2330
|
|
|
2016
2331
|
if (masked.config.type_project === 'http') {
|
|
2017
|
-
lines.push(` http: porta ${masked.config.port} | instancias ${masked.config.instances} | storage ${masked.config.storage.capacity}Gi (${masked.config.storage.persistent ? 'persistente' : 'efemero'})`);
|
|
2332
|
+
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'})`);
|
|
2333
|
+
if (masked.config.autoscaling?.enabled) {
|
|
2334
|
+
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}%`);
|
|
2335
|
+
}
|
|
2018
2336
|
if (masked.config.github) {
|
|
2019
2337
|
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}`);
|
|
2020
2338
|
} else if (masked.config.image) {
|
|
@@ -2219,6 +2537,42 @@ async function buildHttpConfig({ plan, httpPlans, availableInstances }) {
|
|
|
2219
2537
|
max: Math.max(1, Number(availableInstances?.[plan] || 1)),
|
|
2220
2538
|
});
|
|
2221
2539
|
|
|
2540
|
+
let autoscaling;
|
|
2541
|
+
if (isAutoscalingEnabledForPlan(httpPlans, plan)) {
|
|
2542
|
+
const enabled = await promptWizardBoolean({
|
|
2543
|
+
label: 'Ativar auto-scaling',
|
|
2544
|
+
docs: DOCS_CONFIGURATION_URL,
|
|
2545
|
+
examples: ['sim', 'nao'],
|
|
2546
|
+
});
|
|
2547
|
+
promptWizardBoolean.ui.state.httpAutoscalingEnabled = enabled;
|
|
2548
|
+
if (enabled) {
|
|
2549
|
+
autoscaling = {
|
|
2550
|
+
enabled: true,
|
|
2551
|
+
max_instances: await promptWizardNumber({
|
|
2552
|
+
label: 'Maximo de instancias do auto-scaling',
|
|
2553
|
+
docs: DOCS_CONFIGURATION_URL,
|
|
2554
|
+
examples: [String(Math.max(instances, 2))],
|
|
2555
|
+
min: instances,
|
|
2556
|
+
max: Math.max(instances, Number(availableInstances?.[plan] || instances)),
|
|
2557
|
+
}),
|
|
2558
|
+
target_cpu_utilization_percent: await promptWizardNumber({
|
|
2559
|
+
label: 'CPU alvo do auto-scaling (%)',
|
|
2560
|
+
docs: DOCS_CONFIGURATION_URL,
|
|
2561
|
+
examples: ['70'],
|
|
2562
|
+
min: 1,
|
|
2563
|
+
max: 100,
|
|
2564
|
+
}),
|
|
2565
|
+
target_memory_utilization_percent: await promptWizardNumber({
|
|
2566
|
+
label: 'Memoria alvo do auto-scaling (%)',
|
|
2567
|
+
docs: DOCS_CONFIGURATION_URL,
|
|
2568
|
+
examples: ['80'],
|
|
2569
|
+
min: 1,
|
|
2570
|
+
max: 100,
|
|
2571
|
+
}),
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2222
2576
|
let storage;
|
|
2223
2577
|
if (plan === 'free') {
|
|
2224
2578
|
storage = { persistent: false, capacity: 1 };
|
|
@@ -2262,8 +2616,17 @@ async function buildHttpConfig({ plan, httpPlans, availableInstances }) {
|
|
|
2262
2616
|
],
|
|
2263
2617
|
});
|
|
2264
2618
|
|
|
2619
|
+
const exposure = await promptWizardSelect({
|
|
2620
|
+
label: 'Exposicao HTTP',
|
|
2621
|
+
docs: DOCS_CREATE_HTTP_URL,
|
|
2622
|
+
examples: WIZARD_HTTP_EXPOSURE_OPTIONS,
|
|
2623
|
+
options: WIZARD_HTTP_EXPOSURE_OPTIONS.map((value) => ({ value })),
|
|
2624
|
+
});
|
|
2625
|
+
promptWizardSelect.ui.state.httpExposure = exposure;
|
|
2626
|
+
|
|
2627
|
+
const isPublicExposure = exposure === 'public';
|
|
2265
2628
|
const allowBlockIp = isIpAccessEnabled(httpPlans, plan);
|
|
2266
|
-
const customWhitelist = allowBlockIp
|
|
2629
|
+
const customWhitelist = isPublicExposure && allowBlockIp
|
|
2267
2630
|
? await promptWizardPairs({
|
|
2268
2631
|
introLabel: 'Deseja configurar whitelist personalizada de entrada?',
|
|
2269
2632
|
docs: DOCS_CONFIGURATION_URL,
|
|
@@ -2276,7 +2639,7 @@ async function buildHttpConfig({ plan, httpPlans, availableInstances }) {
|
|
|
2276
2639
|
})
|
|
2277
2640
|
: [];
|
|
2278
2641
|
|
|
2279
|
-
const ingress_black_list = allowBlockIp
|
|
2642
|
+
const ingress_black_list = isPublicExposure && allowBlockIp
|
|
2280
2643
|
? await promptWizardPairs({
|
|
2281
2644
|
introLabel: 'Deseja adicionar blacklist de entrada?',
|
|
2282
2645
|
docs: DOCS_CONFIGURATION_URL,
|
|
@@ -2289,7 +2652,7 @@ async function buildHttpConfig({ plan, httpPlans, availableInstances }) {
|
|
|
2289
2652
|
})
|
|
2290
2653
|
: [];
|
|
2291
2654
|
|
|
2292
|
-
const subdomain = isSubdomainCustomizable(httpPlans, plan)
|
|
2655
|
+
const subdomain = isPublicExposure && isSubdomainCustomizable(httpPlans, plan)
|
|
2293
2656
|
? await promptWizardText({
|
|
2294
2657
|
label: 'Subdomain personalizado',
|
|
2295
2658
|
examples: ['minha-api'],
|
|
@@ -2299,9 +2662,11 @@ async function buildHttpConfig({ plan, httpPlans, availableInstances }) {
|
|
|
2299
2662
|
|
|
2300
2663
|
return {
|
|
2301
2664
|
type_project: 'http',
|
|
2665
|
+
exposure,
|
|
2302
2666
|
...(source === 'github' ? { github: await buildHttpGithubConfig() } : { image: await buildHttpImageConfig() }),
|
|
2303
2667
|
port,
|
|
2304
2668
|
instances,
|
|
2669
|
+
...(autoscaling ? { autoscaling } : {}),
|
|
2305
2670
|
storage,
|
|
2306
2671
|
envs,
|
|
2307
2672
|
...(subdomain ? { subdomain } : {}),
|
|
@@ -2417,6 +2782,7 @@ async function runProjectCreateWizard(session, flags) {
|
|
|
2417
2782
|
if (typeProject === 'http') {
|
|
2418
2783
|
ui.state.planAllowsBlockIp = isIpAccessEnabled(catalogs.httpPlans, plan);
|
|
2419
2784
|
ui.state.planAllowsSubdomain = isSubdomainCustomizable(catalogs.httpPlans, plan);
|
|
2785
|
+
ui.state.planAllowsAutoscaling = isAutoscalingEnabledForPlan(catalogs.httpPlans, plan);
|
|
2420
2786
|
}
|
|
2421
2787
|
const paymentMode = await promptWizardSelect({
|
|
2422
2788
|
label: 'Modo de pagamento',
|
|
@@ -2505,7 +2871,8 @@ async function getProject(session, flags, projectId, orgId) {
|
|
|
2505
2871
|
}
|
|
2506
2872
|
|
|
2507
2873
|
async function handleProjectInfo(session, flags) {
|
|
2508
|
-
const projectId = requireProjectId(flags);
|
|
2874
|
+
const projectId = requireProjectId(flags, 'project info');
|
|
2875
|
+
if (!projectId) return
|
|
2509
2876
|
const orgId = await resolveOrgId(session, flags);
|
|
2510
2877
|
const project = await getProject(session, flags, projectId, orgId);
|
|
2511
2878
|
|
|
@@ -2514,7 +2881,8 @@ async function handleProjectInfo(session, flags) {
|
|
|
2514
2881
|
}
|
|
2515
2882
|
|
|
2516
2883
|
async function handleProjectUrl(session, flags) {
|
|
2517
|
-
const projectId = requireProjectId(flags);
|
|
2884
|
+
const projectId = requireProjectId(flags, 'project url');
|
|
2885
|
+
if (!projectId) return
|
|
2518
2886
|
const orgId = await resolveOrgId(session, flags);
|
|
2519
2887
|
const project = await getProject(session, flags, projectId, orgId);
|
|
2520
2888
|
const url = projectUrlOf(project);
|
|
@@ -2531,7 +2899,8 @@ async function handleProjectUrl(session, flags) {
|
|
|
2531
2899
|
}
|
|
2532
2900
|
|
|
2533
2901
|
async function handleProjectLogs(session, flags) {
|
|
2534
|
-
const projectId = requireProjectId(flags);
|
|
2902
|
+
const projectId = requireProjectId(flags, 'project logs');
|
|
2903
|
+
if (!projectId) return
|
|
2535
2904
|
const orgId = await resolveOrgId(session, flags);
|
|
2536
2905
|
const query = buildQuery(flags, ['instance']);
|
|
2537
2906
|
const logs = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/logs${query}`, { orgId }));
|
|
@@ -2540,7 +2909,8 @@ async function handleProjectLogs(session, flags) {
|
|
|
2540
2909
|
}
|
|
2541
2910
|
|
|
2542
2911
|
async function handleProjectMetrics(session, flags) {
|
|
2543
|
-
const projectId = requireProjectId(flags);
|
|
2912
|
+
const projectId = requireProjectId(flags, 'project metrics');
|
|
2913
|
+
if (!projectId) return
|
|
2544
2914
|
const orgId = await resolveOrgId(session, flags);
|
|
2545
2915
|
const query = buildQuery(flags, ['instance']);
|
|
2546
2916
|
const metrics = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/metrics${query}`, { orgId }));
|
|
@@ -2558,7 +2928,8 @@ async function handleProjectMetrics(session, flags) {
|
|
|
2558
2928
|
}
|
|
2559
2929
|
|
|
2560
2930
|
async function handleProjectNetwork(session, flags) {
|
|
2561
|
-
const projectId = requireProjectId(flags);
|
|
2931
|
+
const projectId = requireProjectId(flags, 'project network');
|
|
2932
|
+
if (!projectId) return
|
|
2562
2933
|
const orgId = await resolveOrgId(session, flags);
|
|
2563
2934
|
const view = String(flags.view || 'summary');
|
|
2564
2935
|
const allowedViews = new Set(['summary', 'status-codes', 'routes', 'user-agents', 'request-events', 'source-ips']);
|
|
@@ -2573,9 +2944,13 @@ async function handleProjectNetwork(session, flags) {
|
|
|
2573
2944
|
}
|
|
2574
2945
|
|
|
2575
2946
|
async function handleProjectImageSet(session, flags) {
|
|
2576
|
-
const projectId = requireProjectId(flags);
|
|
2947
|
+
const projectId = requireProjectId(flags, 'project image set');
|
|
2948
|
+
if (!projectId) return
|
|
2577
2949
|
const image = flags.image || await prompt('Imagem');
|
|
2578
|
-
if (!image)
|
|
2950
|
+
if (!image) {
|
|
2951
|
+
printCommandHelpAndFail('project image set')
|
|
2952
|
+
return
|
|
2953
|
+
}
|
|
2579
2954
|
|
|
2580
2955
|
const orgId = await resolveOrgId(session, flags);
|
|
2581
2956
|
const payload = await request(session, flags, 'PATCH', `/project/${projectId}/image`, {
|
|
@@ -2587,6 +2962,33 @@ async function handleProjectImageSet(session, flags) {
|
|
|
2587
2962
|
process.stdout.write('Imagem atualizada com sucesso.\n');
|
|
2588
2963
|
}
|
|
2589
2964
|
|
|
2965
|
+
async function handleProjectExposureSet(session, flags) {
|
|
2966
|
+
const projectId = requireProjectId(flags, 'project exposure set');
|
|
2967
|
+
if (!projectId) return
|
|
2968
|
+
const exposure = normalizeExposure(flags.exposure);
|
|
2969
|
+
|
|
2970
|
+
if (!ALLOWED_EXPOSURE_VALUES.has(exposure)) {
|
|
2971
|
+
throw new CliError('exposure invalido. Use public ou private.');
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2974
|
+
const orgId = await resolveOrgId(session, flags);
|
|
2975
|
+
const payload = await request(session, flags, 'PATCH', `/project/${projectId}/exposure`, {
|
|
2976
|
+
orgId,
|
|
2977
|
+
body: { exposure },
|
|
2978
|
+
});
|
|
2979
|
+
|
|
2980
|
+
if (flags.json) return printJson(payload);
|
|
2981
|
+
|
|
2982
|
+
printTable([
|
|
2983
|
+
{ field: 'Exposicao', value: payload.exposure || exposure },
|
|
2984
|
+
{ field: 'Dominio', value: payload.domain ? formatPublicUrl(payload.domain) : '-' },
|
|
2985
|
+
{ field: 'Dominios customizados', value: asArray(payload.custom_domains).join(', ') || '-' },
|
|
2986
|
+
], [
|
|
2987
|
+
{ label: 'Campo', value: (row) => row.field },
|
|
2988
|
+
{ label: 'Valor', value: (row) => row.value },
|
|
2989
|
+
]);
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2590
2992
|
async function getProjectEnvs(session, flags, projectId, orgId) {
|
|
2591
2993
|
return asArray(unwrapData(await request(session, flags, 'GET', `/project/${projectId}/envs`, { orgId })));
|
|
2592
2994
|
}
|
|
@@ -2599,7 +3001,8 @@ async function patchProjectEnvs(session, flags, projectId, orgId, envs) {
|
|
|
2599
3001
|
}
|
|
2600
3002
|
|
|
2601
3003
|
async function handleProjectEnvs(session, flags) {
|
|
2602
|
-
const projectId = requireProjectId(flags);
|
|
3004
|
+
const projectId = requireProjectId(flags, 'project envs');
|
|
3005
|
+
if (!projectId) return
|
|
2603
3006
|
const orgId = await resolveOrgId(session, flags);
|
|
2604
3007
|
const envs = await getProjectEnvs(session, flags, projectId, orgId);
|
|
2605
3008
|
|
|
@@ -2607,13 +3010,26 @@ async function handleProjectEnvs(session, flags) {
|
|
|
2607
3010
|
}
|
|
2608
3011
|
|
|
2609
3012
|
async function handleProjectEnvMutation(session, flags, action) {
|
|
2610
|
-
const
|
|
3013
|
+
const helpKeyByAction = {
|
|
3014
|
+
add: 'project env add',
|
|
3015
|
+
update: 'project env update',
|
|
3016
|
+
remove: 'project env remove',
|
|
3017
|
+
}
|
|
3018
|
+
const helpKey = helpKeyByAction[action]
|
|
3019
|
+
const projectId = requireProjectId(flags, helpKey);
|
|
3020
|
+
if (!projectId) return
|
|
2611
3021
|
const name = String(flags.name || '').trim();
|
|
2612
|
-
if (!name)
|
|
3022
|
+
if (!name) {
|
|
3023
|
+
printCommandHelpAndFail(helpKey)
|
|
3024
|
+
return
|
|
3025
|
+
}
|
|
2613
3026
|
|
|
2614
3027
|
const needsValue = action === 'add' || action === 'update';
|
|
2615
3028
|
const value = needsValue ? String(flags.value ?? await promptHidden('Valor')) : undefined;
|
|
2616
|
-
if (needsValue && !value)
|
|
3029
|
+
if (needsValue && !value) {
|
|
3030
|
+
printCommandHelpAndFail(helpKey)
|
|
3031
|
+
return
|
|
3032
|
+
}
|
|
2617
3033
|
|
|
2618
3034
|
const orgId = await resolveOrgId(session, flags);
|
|
2619
3035
|
const envs = await getProjectEnvs(session, flags, projectId, orgId);
|
|
@@ -2646,7 +3062,8 @@ async function handleProjectEnvMutation(session, flags, action) {
|
|
|
2646
3062
|
}
|
|
2647
3063
|
|
|
2648
3064
|
async function handleProjectInstances(session, flags) {
|
|
2649
|
-
const projectId = requireProjectId(flags);
|
|
3065
|
+
const projectId = requireProjectId(flags, 'project instances');
|
|
3066
|
+
if (!projectId) return
|
|
2650
3067
|
const orgId = await resolveOrgId(session, flags);
|
|
2651
3068
|
const instances = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/instances`, { orgId }));
|
|
2652
3069
|
|
|
@@ -2657,7 +3074,12 @@ async function handleProjectInstances(session, flags) {
|
|
|
2657
3074
|
}
|
|
2658
3075
|
|
|
2659
3076
|
async function handleProjectInstancesSet(session, flags) {
|
|
2660
|
-
const projectId = requireProjectId(flags);
|
|
3077
|
+
const projectId = requireProjectId(flags, 'project instances set');
|
|
3078
|
+
if (!projectId) return
|
|
3079
|
+
if (flags.count === undefined) {
|
|
3080
|
+
printCommandHelpAndFail('project instances set')
|
|
3081
|
+
return
|
|
3082
|
+
}
|
|
2661
3083
|
const instances = Number(flags.count);
|
|
2662
3084
|
if (!Number.isInteger(instances) || instances <= 0) {
|
|
2663
3085
|
throw new CliError('Informe --count <n> com um inteiro positivo.');
|
|
@@ -2673,6 +3095,241 @@ async function handleProjectInstancesSet(session, flags) {
|
|
|
2673
3095
|
process.stdout.write(`Quantidade de instancias atualizada para ${instances}.\n`);
|
|
2674
3096
|
}
|
|
2675
3097
|
|
|
3098
|
+
function parseAutoscalingPositiveInteger(value, flagName) {
|
|
3099
|
+
const parsed = Number(value);
|
|
3100
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
3101
|
+
throw new CliError(`Informe ${flagName} <n> com um inteiro positivo.`);
|
|
3102
|
+
}
|
|
3103
|
+
return parsed;
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
function parseAutoscalingTargetPercent(value, flagName) {
|
|
3107
|
+
const parsed = parseAutoscalingPositiveInteger(value, flagName);
|
|
3108
|
+
if (parsed > 100) {
|
|
3109
|
+
throw new CliError(`Informe ${flagName} entre 1 e 100.`);
|
|
3110
|
+
}
|
|
3111
|
+
return parsed;
|
|
3112
|
+
}
|
|
3113
|
+
|
|
3114
|
+
function autoscalingFromProject(project) {
|
|
3115
|
+
return project?.additional_info?.autoscaling || project?.autoscaling || { enabled: false };
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
function printAutoscaling(autoscaling) {
|
|
3119
|
+
printTable([
|
|
3120
|
+
{
|
|
3121
|
+
status: autoscaling.enabled ? 'ativo' : 'inativo',
|
|
3122
|
+
min: autoscaling.min_instances || '-',
|
|
3123
|
+
max: autoscaling.max_instances || '-',
|
|
3124
|
+
cpu: autoscaling.target_cpu_utilization_percent ? `${autoscaling.target_cpu_utilization_percent}%` : '-',
|
|
3125
|
+
memory: autoscaling.target_memory_utilization_percent ? `${autoscaling.target_memory_utilization_percent}%` : '-',
|
|
3126
|
+
},
|
|
3127
|
+
], [
|
|
3128
|
+
{ label: 'Status', value: (item) => item.status },
|
|
3129
|
+
{ label: 'Minimo', value: (item) => item.min },
|
|
3130
|
+
{ label: 'Maximo', value: (item) => item.max },
|
|
3131
|
+
{ label: 'CPU alvo', value: (item) => item.cpu },
|
|
3132
|
+
{ label: 'Memoria alvo', value: (item) => item.memory },
|
|
3133
|
+
]);
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3136
|
+
async function handleProjectAutoscaling(session, flags) {
|
|
3137
|
+
const projectId = requireProjectId(flags, 'project autoscaling');
|
|
3138
|
+
if (!projectId) return
|
|
3139
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3140
|
+
const project = await getProject(session, flags, projectId, orgId);
|
|
3141
|
+
const autoscaling = autoscalingFromProject(project);
|
|
3142
|
+
|
|
3143
|
+
if (flags.json) return printJson(autoscaling);
|
|
3144
|
+
printAutoscaling(autoscaling);
|
|
3145
|
+
}
|
|
3146
|
+
|
|
3147
|
+
async function handleProjectAutoscalingSet(session, flags) {
|
|
3148
|
+
const projectId = requireProjectId(flags, 'project autoscaling set');
|
|
3149
|
+
if (!projectId) return
|
|
3150
|
+
if (flags.min === undefined || flags.max === undefined) {
|
|
3151
|
+
printCommandHelpAndFail('project autoscaling set')
|
|
3152
|
+
return
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
const minInstances = parseAutoscalingPositiveInteger(flags.min, '--min');
|
|
3156
|
+
const maxInstances = parseAutoscalingPositiveInteger(flags.max, '--max');
|
|
3157
|
+
if (maxInstances < minInstances) {
|
|
3158
|
+
throw new CliError('Informe --max maior ou igual a --min.');
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
const body = {
|
|
3162
|
+
enabled: true,
|
|
3163
|
+
min_instances: minInstances,
|
|
3164
|
+
max_instances: maxInstances,
|
|
3165
|
+
};
|
|
3166
|
+
|
|
3167
|
+
if (flags.cpu !== undefined) {
|
|
3168
|
+
body.target_cpu_utilization_percent = parseAutoscalingTargetPercent(flags.cpu, '--cpu');
|
|
3169
|
+
}
|
|
3170
|
+
if (flags.memory !== undefined) {
|
|
3171
|
+
body.target_memory_utilization_percent = parseAutoscalingTargetPercent(flags.memory, '--memory');
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3175
|
+
const payload = await request(session, flags, 'PATCH', `/project/${projectId}/autoscaling`, {
|
|
3176
|
+
orgId,
|
|
3177
|
+
body,
|
|
3178
|
+
});
|
|
3179
|
+
|
|
3180
|
+
if (flags.json) return printJson(payload);
|
|
3181
|
+
process.stdout.write(`Auto-scaling atualizado: ${minInstances}-${maxInstances} instancias.\n`);
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
async function handleProjectAutoscalingDisable(session, flags) {
|
|
3185
|
+
const projectId = requireProjectId(flags, 'project autoscaling disable');
|
|
3186
|
+
if (!projectId) return
|
|
3187
|
+
|
|
3188
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3189
|
+
const payload = await request(session, flags, 'PATCH', `/project/${projectId}/autoscaling`, {
|
|
3190
|
+
orgId,
|
|
3191
|
+
body: { enabled: false },
|
|
3192
|
+
});
|
|
3193
|
+
|
|
3194
|
+
if (flags.json) return printJson(payload);
|
|
3195
|
+
process.stdout.write('Auto-scaling desativado.\n');
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
function printAutoscalingEvents(data) {
|
|
3199
|
+
const directions = { scale_up: 'aumento', scale_down: 'reducao' };
|
|
3200
|
+
const reasons = { increased_capacity: 'capacidade aumentada', reduced_capacity: 'capacidade reduzida' };
|
|
3201
|
+
const events = asArray(data?.events);
|
|
3202
|
+
printTable(events, [
|
|
3203
|
+
{ label: 'Quando', value: (event) => event.occurred_at || '-' },
|
|
3204
|
+
{ label: 'Direcao', value: (event) => directions[event.direction] || event.direction || '-' },
|
|
3205
|
+
{
|
|
3206
|
+
label: 'Instancias',
|
|
3207
|
+
value: (event) => `${event.previous_instances ?? '-'} -> ${event.new_instances ?? '-'}`,
|
|
3208
|
+
},
|
|
3209
|
+
{
|
|
3210
|
+
label: 'Limites',
|
|
3211
|
+
value: (event) => {
|
|
3212
|
+
const cpu = typeof event.current_cpu_utilization_percent === 'number' || typeof event.target_cpu_utilization_percent === 'number'
|
|
3213
|
+
? `CPU ${event.current_cpu_utilization_percent ?? '-'}%/${event.target_cpu_utilization_percent ?? '-'}%`
|
|
3214
|
+
: null;
|
|
3215
|
+
const memory = typeof event.current_memory_utilization_percent === 'number' || typeof event.target_memory_utilization_percent === 'number'
|
|
3216
|
+
? `MEM ${event.current_memory_utilization_percent ?? '-'}%/${event.target_memory_utilization_percent ?? '-'}%`
|
|
3217
|
+
: null;
|
|
3218
|
+
return [cpu, memory].filter(Boolean).join(' ');
|
|
3219
|
+
},
|
|
3220
|
+
},
|
|
3221
|
+
{ label: 'Motivo', value: (event) => reasons[event.reason] || event.reason || event.trigger_metric || '-' },
|
|
3222
|
+
]);
|
|
3223
|
+
|
|
3224
|
+
printPagination(data?.pagination, 'evento');
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3227
|
+
function isIsoDate(value) {
|
|
3228
|
+
const text = String(value);
|
|
3229
|
+
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})?)?$/);
|
|
3230
|
+
if (!match || Number.isNaN(Date.parse(text))) return false;
|
|
3231
|
+
|
|
3232
|
+
const [, year, month, day, hour = '00', minute = '00', second = '00'] = match;
|
|
3233
|
+
const calendarDate = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
|
|
3234
|
+
return calendarDate.getUTCFullYear() === Number(year)
|
|
3235
|
+
&& calendarDate.getUTCMonth() === Number(month) - 1
|
|
3236
|
+
&& calendarDate.getUTCDate() === Number(day)
|
|
3237
|
+
&& Number(hour) <= 23
|
|
3238
|
+
&& Number(minute) <= 59
|
|
3239
|
+
&& Number(second) <= 59;
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3242
|
+
function validateProjectListFlags(flags, { maxLimit }) {
|
|
3243
|
+
for (const key of ['from', 'to']) {
|
|
3244
|
+
if (flags[key] !== undefined && !isIsoDate(flags[key])) {
|
|
3245
|
+
throw new CliError(`Informe --${key} <iso> com uma data ISO valida.`);
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
if (flags.from !== undefined && flags.to !== undefined && Date.parse(String(flags.from)) > Date.parse(String(flags.to))) {
|
|
3249
|
+
throw new CliError('Informe --from anterior ou igual a --to.');
|
|
3250
|
+
}
|
|
3251
|
+
for (const key of ['page', 'limit']) {
|
|
3252
|
+
if (flags[key] === undefined) continue;
|
|
3253
|
+
const value = Number(flags[key]);
|
|
3254
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
3255
|
+
throw new CliError(`Informe --${key} <n> com um inteiro positivo.`);
|
|
3256
|
+
}
|
|
3257
|
+
if (key === 'limit' && value > maxLimit) {
|
|
3258
|
+
throw new CliError(`Informe --limit entre 1 e ${maxLimit}.`);
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
function printPagination(pagination, singular = 'item') {
|
|
3264
|
+
if (!pagination) return;
|
|
3265
|
+
const { page, total_pages: totalPages, total } = pagination;
|
|
3266
|
+
process.stdout.write(`Pagina ${page} de ${totalPages || 1} - ${total} ${singular}(s)\n`);
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3269
|
+
function formatMoney(value, currency = 'brl') {
|
|
3270
|
+
const amount = Number(value);
|
|
3271
|
+
if (!Number.isFinite(amount)) return '-';
|
|
3272
|
+
try {
|
|
3273
|
+
return new Intl.NumberFormat('pt-BR', {
|
|
3274
|
+
style: 'currency',
|
|
3275
|
+
currency: String(currency || 'brl').toUpperCase(),
|
|
3276
|
+
}).format(amount);
|
|
3277
|
+
} catch {
|
|
3278
|
+
return `${String(currency || 'brl').toUpperCase()} ${amount.toFixed(2)}`;
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
function printBillingUsage(data) {
|
|
3283
|
+
const hours = asArray(data?.hours);
|
|
3284
|
+
printTable(hours, [
|
|
3285
|
+
{ label: 'Inicio', value: (item) => item.hour_start || '-' },
|
|
3286
|
+
{ label: 'Fim', value: (item) => item.hour_end || '-' },
|
|
3287
|
+
{ label: 'Instancia-horas', value: (item) => item.compute_instance_hours ?? '-' },
|
|
3288
|
+
{ label: 'Armazenamento', value: (item) => item.storage_gb_hours === undefined ? '-' : `${item.storage_gb_hours} GB-h` },
|
|
3289
|
+
{ label: 'Computacao', value: (item) => formatMoney(item.compute_amount, item.currency) },
|
|
3290
|
+
{ label: 'Storage', value: (item) => formatMoney(item.storage_amount, item.currency) },
|
|
3291
|
+
{ label: 'Total', value: (item) => formatMoney(item.total_amount, item.currency) },
|
|
3292
|
+
{ label: 'Status', value: (item) => item.status || '-' },
|
|
3293
|
+
]);
|
|
3294
|
+
|
|
3295
|
+
if (data?.summary) {
|
|
3296
|
+
const summary = data.summary;
|
|
3297
|
+
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`);
|
|
3298
|
+
}
|
|
3299
|
+
printPagination(data?.pagination, 'hora');
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
|
|
3303
|
+
async function handleProjectAutoscalingEvents(session, flags) {
|
|
3304
|
+
const projectId = requireProjectId(flags, 'project autoscaling events');
|
|
3305
|
+
if (!projectId) return
|
|
3306
|
+
|
|
3307
|
+
if (flags.direction !== undefined && !['scale_up', 'scale_down'].includes(String(flags.direction))) {
|
|
3308
|
+
throw new CliError('direction invalido. Use scale_up ou scale_down.');
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
validateProjectListFlags(flags, { maxLimit: 100 });
|
|
3312
|
+
|
|
3313
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3314
|
+
const query = buildQuery(flags, ['direction', 'from', 'to', 'page', 'limit']);
|
|
3315
|
+
const data = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/autoscaling/events${query}`, { orgId }));
|
|
3316
|
+
|
|
3317
|
+
if (flags.json) return printJson(data);
|
|
3318
|
+
printAutoscalingEvents(data);
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3321
|
+
async function handleProjectBillingUsage(session, flags) {
|
|
3322
|
+
const projectId = requireProjectId(flags, 'project billing usage');
|
|
3323
|
+
if (!projectId) return;
|
|
3324
|
+
validateProjectListFlags(flags, { maxLimit: 50 });
|
|
3325
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3326
|
+
const query = buildQuery(flags, ['from', 'to', 'page', 'limit']);
|
|
3327
|
+
const data = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/billing/hourly-usage${query}`, { orgId }));
|
|
3328
|
+
|
|
3329
|
+
if (flags.json) return printJson(data);
|
|
3330
|
+
printBillingUsage(data);
|
|
3331
|
+
}
|
|
3332
|
+
|
|
2676
3333
|
function buildQuery(flags, allowedKeys) {
|
|
2677
3334
|
const params = new URLSearchParams();
|
|
2678
3335
|
for (const key of allowedKeys) {
|
|
@@ -2687,7 +3344,10 @@ function buildQuery(flags, allowedKeys) {
|
|
|
2687
3344
|
|
|
2688
3345
|
async function handleDeployments(session, flags) {
|
|
2689
3346
|
const projectId = flags.project;
|
|
2690
|
-
if (!projectId)
|
|
3347
|
+
if (!projectId) {
|
|
3348
|
+
printCommandHelpAndFail(flags.__commandKey || 'builds')
|
|
3349
|
+
return
|
|
3350
|
+
}
|
|
2691
3351
|
const orgId = await resolveOrgId(session, flags);
|
|
2692
3352
|
const query = buildQuery(flags, ['page', 'limit', 'branch', 'status']);
|
|
2693
3353
|
const data = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/github/builds${query}`, { orgId }));
|
|
@@ -2705,7 +3365,11 @@ async function handleDeployments(session, flags) {
|
|
|
2705
3365
|
|
|
2706
3366
|
async function handleDeploy(session, flags) {
|
|
2707
3367
|
const projectId = flags.project;
|
|
2708
|
-
if (!projectId)
|
|
3368
|
+
if (!projectId) {
|
|
3369
|
+
process.stdout.write(commandHelp(['deploy']));
|
|
3370
|
+
process.exitCode = 1;
|
|
3371
|
+
return;
|
|
3372
|
+
}
|
|
2709
3373
|
const orgId = await resolveOrgId(session, flags);
|
|
2710
3374
|
const body = {};
|
|
2711
3375
|
if (flags.branch) body.branch = String(flags.branch);
|
|
@@ -2721,48 +3385,134 @@ async function handleDeploy(session, flags) {
|
|
|
2721
3385
|
process.stdout.write(`Deploy iniciado${buildId ? `: ${buildId}` : '.'}\n`);
|
|
2722
3386
|
}
|
|
2723
3387
|
|
|
2724
|
-
async function
|
|
2725
|
-
|
|
3388
|
+
async function getBuildLogs(session, flags, projectId, buildId, orgId, { cursor = 0, limit = 200 } = {}) {
|
|
3389
|
+
const query = buildQuery({ cursor, limit }, ['cursor', 'limit'])
|
|
3390
|
+
return unwrapData(await request(session, flags, 'GET', `/project/${projectId}/github/builds/${buildId}/logs${query}`, { orgId }))
|
|
2726
3391
|
}
|
|
2727
3392
|
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
const
|
|
2731
|
-
if (!
|
|
2732
|
-
|
|
3393
|
+
function parseIntegerOption(value, flagName, { defaultValue, min, max }) {
|
|
3394
|
+
if (value === undefined) return defaultValue;
|
|
3395
|
+
const parsed = Number(value);
|
|
3396
|
+
if (!Number.isInteger(parsed) || parsed < min || (max !== undefined && parsed > max)) {
|
|
3397
|
+
const range = max === undefined ? `maior ou igual a ${min}` : `entre ${min} e ${max}`;
|
|
3398
|
+
throw new CliError(`Informe ${flagName} com um inteiro ${range}.`);
|
|
3399
|
+
}
|
|
3400
|
+
return parsed;
|
|
3401
|
+
}
|
|
2733
3402
|
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
const
|
|
2737
|
-
|
|
3403
|
+
function parseSecondsOption(value, flagName, { defaultValue, min }) {
|
|
3404
|
+
if (value === undefined) return defaultValue;
|
|
3405
|
+
const parsed = Number(value);
|
|
3406
|
+
if (!Number.isFinite(parsed) || parsed < min) {
|
|
3407
|
+
throw new CliError(`Informe ${flagName} com um numero maior ou igual a ${min}.`);
|
|
3408
|
+
}
|
|
3409
|
+
return parsed;
|
|
3410
|
+
}
|
|
3411
|
+
|
|
3412
|
+
function printBuildLogs(logs) {
|
|
3413
|
+
for (const log of asArray(logs)) {
|
|
3414
|
+
process.stdout.write(`${buildLogLineOf(log)}\n`)
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
|
|
3418
|
+
function printBuildLogEventsJson(logs) {
|
|
3419
|
+
for (const log of asArray(logs)) {
|
|
3420
|
+
process.stdout.write(`${JSON.stringify(log)}\n`)
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
|
|
3424
|
+
async function followBuildLogs(session, flags, projectId, buildId, orgId, {
|
|
3425
|
+
initialCursor = 0,
|
|
3426
|
+
limit = 200,
|
|
3427
|
+
intervalSeconds = 5,
|
|
3428
|
+
timeoutSeconds = 900,
|
|
3429
|
+
} = {}) {
|
|
3430
|
+
let cursor = parseIntegerOption(initialCursor, '--cursor', { defaultValue: 0, min: 0 })
|
|
3431
|
+
const pollLimit = parseIntegerOption(limit, '--limit', { defaultValue: 200, min: 1, max: 500 })
|
|
3432
|
+
const intervalMs = parseSecondsOption(intervalSeconds, '--interval', { defaultValue: 5, min: 0.1 }) * 1000
|
|
3433
|
+
const timeoutMs = parseSecondsOption(timeoutSeconds, '--timeout', { defaultValue: 900, min: 1 }) * 1000
|
|
3434
|
+
const startedAt = Date.now()
|
|
2738
3435
|
|
|
2739
3436
|
while (true) {
|
|
2740
|
-
const
|
|
2741
|
-
|
|
3437
|
+
const result = await getBuildLogs(session, flags, projectId, buildId, orgId, {
|
|
3438
|
+
cursor,
|
|
3439
|
+
limit: pollLimit,
|
|
3440
|
+
})
|
|
3441
|
+
const logs = asArray(result?.logs)
|
|
2742
3442
|
|
|
2743
3443
|
if (flags.json) {
|
|
2744
|
-
|
|
3444
|
+
printBuildLogEventsJson(logs)
|
|
2745
3445
|
} else {
|
|
2746
|
-
|
|
3446
|
+
printBuildLogs(logs)
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3449
|
+
if (typeof result?.next_cursor === 'number') {
|
|
3450
|
+
cursor = result.next_cursor
|
|
3451
|
+
} else if (logs.length > 0) {
|
|
3452
|
+
cursor = logs[logs.length - 1].sequence || cursor
|
|
2747
3453
|
}
|
|
2748
3454
|
|
|
2749
|
-
if (
|
|
2750
|
-
if (status === 'failed') {
|
|
2751
|
-
process.exitCode = 1
|
|
3455
|
+
if (result?.finished) {
|
|
3456
|
+
if (result?.status === 'failed') {
|
|
3457
|
+
process.exitCode = 1
|
|
2752
3458
|
}
|
|
2753
|
-
return
|
|
3459
|
+
return result
|
|
2754
3460
|
}
|
|
2755
3461
|
|
|
2756
3462
|
if (Date.now() - startedAt > timeoutMs) {
|
|
2757
|
-
throw new CliError(`Timeout aguardando build ${buildId}. Ultimo status: ${status}`)
|
|
3463
|
+
throw new CliError(`Timeout aguardando build ${buildId}. Ultimo status: ${result?.status || 'unknown'}`)
|
|
2758
3464
|
}
|
|
2759
3465
|
|
|
2760
|
-
await new Promise((resolve) => setTimeout(resolve, intervalMs))
|
|
3466
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs))
|
|
2761
3467
|
}
|
|
2762
3468
|
}
|
|
2763
3469
|
|
|
3470
|
+
async function handleBuildLogs(session, flags) {
|
|
3471
|
+
const projectId = flags.project;
|
|
3472
|
+
const buildId = flags.build;
|
|
3473
|
+
if (!projectId || !buildId) {
|
|
3474
|
+
printCommandHelpAndFail('builds logs')
|
|
3475
|
+
return
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3478
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3479
|
+
const cursor = parseIntegerOption(flags.cursor, '--cursor', { defaultValue: 0, min: 0 })
|
|
3480
|
+
const limit = parseIntegerOption(flags.limit, '--limit', { defaultValue: 200, min: 1, max: 500 })
|
|
3481
|
+
|
|
3482
|
+
if (flags.follow) {
|
|
3483
|
+
return followBuildLogs(session, flags, projectId, buildId, orgId, {
|
|
3484
|
+
initialCursor: cursor,
|
|
3485
|
+
limit,
|
|
3486
|
+
intervalSeconds: parseSecondsOption(flags.interval, '--interval', { defaultValue: 5, min: 0.1 }),
|
|
3487
|
+
timeoutSeconds: parseSecondsOption(flags.timeout, '--timeout', { defaultValue: 900, min: 1 }),
|
|
3488
|
+
})
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
const result = await getBuildLogs(session, flags, projectId, buildId, orgId, { cursor, limit })
|
|
3492
|
+
if (flags.json) return printJson(result)
|
|
3493
|
+
printBuildLogs(result?.logs)
|
|
3494
|
+
}
|
|
3495
|
+
|
|
3496
|
+
async function handleDeployWatch(session, flags) {
|
|
3497
|
+
const projectId = flags.project;
|
|
3498
|
+
const buildId = flags.build;
|
|
3499
|
+
if (!projectId || !buildId) {
|
|
3500
|
+
printCommandHelpAndFail('deploy watch')
|
|
3501
|
+
return;
|
|
3502
|
+
}
|
|
3503
|
+
|
|
3504
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3505
|
+
return followBuildLogs(session, flags, projectId, buildId, orgId, {
|
|
3506
|
+
initialCursor: 0,
|
|
3507
|
+
limit: 200,
|
|
3508
|
+
intervalSeconds: parseSecondsOption(flags.interval, '--interval', { defaultValue: 5, min: 0.1 }),
|
|
3509
|
+
timeoutSeconds: parseSecondsOption(flags.timeout, '--timeout', { defaultValue: 900, min: 1 }),
|
|
3510
|
+
})
|
|
3511
|
+
}
|
|
3512
|
+
|
|
2764
3513
|
async function main() {
|
|
2765
3514
|
const { positional, flags } = parseArgs(process.argv.slice(2));
|
|
3515
|
+
validateKnownFlags(flags);
|
|
2766
3516
|
const [command, subcommand] = positional;
|
|
2767
3517
|
const session = await readSession();
|
|
2768
3518
|
try {
|
|
@@ -2812,14 +3562,21 @@ async function main() {
|
|
|
2812
3562
|
if (command === 'project' && subcommand === 'metrics') return handleProjectMetrics(session, flags);
|
|
2813
3563
|
if (command === 'project' && subcommand === 'network') return handleProjectNetwork(session, flags);
|
|
2814
3564
|
if (command === 'project' && subcommand === 'image' && positional[2] === 'set') return handleProjectImageSet(session, flags);
|
|
3565
|
+
if (command === 'project' && subcommand === 'exposure' && positional[2] === 'set') return handleProjectExposureSet(session, flags);
|
|
2815
3566
|
if (command === 'project' && subcommand === 'envs') return handleProjectEnvs(session, flags);
|
|
2816
3567
|
if (command === 'project' && subcommand === 'env' && positional[2] === 'add') return handleProjectEnvMutation(session, flags, 'add');
|
|
2817
3568
|
if (command === 'project' && subcommand === 'env' && positional[2] === 'update') return handleProjectEnvMutation(session, flags, 'update');
|
|
2818
3569
|
if (command === 'project' && subcommand === 'env' && positional[2] === 'remove') return handleProjectEnvMutation(session, flags, 'remove');
|
|
3570
|
+
if (command === 'project' && subcommand === 'autoscaling' && positional[2] === 'set') return handleProjectAutoscalingSet(session, flags);
|
|
3571
|
+
if (command === 'project' && subcommand === 'autoscaling' && positional[2] === 'disable') return handleProjectAutoscalingDisable(session, flags);
|
|
3572
|
+
if (command === 'project' && subcommand === 'autoscaling' && positional[2] === 'events') return handleProjectAutoscalingEvents(session, flags);
|
|
3573
|
+
if (command === 'project' && subcommand === 'autoscaling') return handleProjectAutoscaling(session, flags);
|
|
3574
|
+
if (command === 'project' && subcommand === 'billing' && positional[2] === 'usage') return handleProjectBillingUsage(session, flags);
|
|
2819
3575
|
if (command === 'project' && subcommand === 'instances' && positional[2] === 'set') return handleProjectInstancesSet(session, flags);
|
|
2820
3576
|
if (command === 'project' && subcommand === 'instances') return handleProjectInstances(session, flags);
|
|
2821
|
-
if (command === 'builds') return
|
|
2822
|
-
if (command === '
|
|
3577
|
+
if (command === 'builds' && subcommand === 'logs') return handleBuildLogs(session, flags);
|
|
3578
|
+
if (command === 'builds') return handleDeployments(session, { ...flags, __commandKey: 'builds' });
|
|
3579
|
+
if (command === 'deployments') return handleDeployments(session, { ...flags, __commandKey: 'deployments' });
|
|
2823
3580
|
if (command === 'deploy' && subcommand === 'watch') return handleDeployWatch(session, flags);
|
|
2824
3581
|
if (command === 'deploy') return handleDeploy(session, flags);
|
|
2825
3582
|
|