@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/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 FINAL_BUILD_STATUSES = new Set(['success', 'failed']);
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/pods do projeto.',
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: ['zenifra deploy --project 507f1f77bcf86cd799439012 --branch main'],
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 ate status terminal.',
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: ['zenifra deploy watch --project 507f1f77bcf86cd799439012 --build build_123'],
446
- output: '[2026-05-27T12:00:00.000Z] build build_123: building\n[2026-05-27T12:00:05.000Z] build build_123: success',
447
- jsonOutput: '{"id":"build_123","status":"success"}',
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
- return writeProfileStore(store);
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
- async function request(session, flags, method, path, { body, orgId, tokenRequired = true } = {}) {
1164
- const credential = resolveCredential(session);
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 response = await fetch(`${apiBaseUrl(session, flags)}${path}`, {
1181
- method,
1182
- headers,
1183
- body: body === undefined ? undefined : JSON.stringify(body),
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 || response.status === 403) {
1394
+ if (response.status === 401) {
1199
1395
  if (credential?.type === 'api_key') {
1200
- throw new CliError(`${message}. Verifique se a API key esta ativa, possui permissao RBAC e se o IP atual esta permitido.`);
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 requireProjectId(flags) {
1266
- if (!flags.project) throw new CliError('Informe --project <id>.');
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) throw new CliError('Informe o nome do perfil: zenifra profile use <name>.');
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) throw new CliError('Informe o nome do perfil: zenifra profile edit <name>.');
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) throw new CliError('Informe o nome do perfil: zenifra profile remove <name>.');
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: nextConfig,
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) throw new CliError('Informe --image <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 projectId = requireProjectId(flags);
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) throw new CliError('Informe --name <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) throw new CliError('Informe --value <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) throw new CliError('Informe --project <id>.');
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) throw new CliError('Informe --project <id>.');
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 getBuild(session, flags, projectId, buildId, orgId) {
2725
- return unwrapData(await request(session, flags, 'GET', `/project/${projectId}/github/builds/${buildId}`, { orgId }));
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
- async function handleDeployWatch(session, flags) {
2729
- const projectId = flags.project;
2730
- const buildId = flags.build;
2731
- if (!projectId) throw new CliError('Informe --project <id>.');
2732
- if (!buildId) throw new CliError('Informe --build <id>.');
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
- const orgId = await resolveOrgId(session, flags);
2735
- const intervalMs = Math.max(Number(flags.interval || 5), 1) * 1000;
2736
- const timeoutMs = Math.max(Number(flags.timeout || 900), 1) * 1000;
2737
- const startedAt = Date.now();
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 build = await getBuild(session, flags, projectId, buildId, orgId);
2741
- const status = build?.status || 'unknown';
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
- printJson(build);
3444
+ printBuildLogEventsJson(logs)
2745
3445
  } else {
2746
- process.stdout.write(`[${new Date().toISOString()}] build ${buildId}: ${status}\n`);
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 (FINAL_BUILD_STATUSES.has(status)) {
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 handleDeployments(session, flags);
2822
- if (command === 'deployments') return handleDeployments(session, flags);
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