@zenifra/cli 0.2.4 → 0.2.6

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 CHANGED
@@ -51,6 +51,7 @@ zenifra orgs
51
51
  zenifra org set
52
52
  zenifra plans
53
53
  zenifra plans --type http
54
+ zenifra plans --type valkey
54
55
  zenifra plans --type storage --json
55
56
  zenifra create project
56
57
  zenifra create project --name <name> --plan free --payment-mode hourly --config @examples/http-project.json
@@ -58,11 +59,20 @@ zenifra create project --name <name> --plan basic --payment-mode hourly --config
58
59
  zenifra create project --name <name> --plan premium --payment-mode hourly --config @examples/http-autoscaling-project.json
59
60
  zenifra create project --name <name> --plan db-basic --payment-mode monthly --config @examples/postgresql-project.json
60
61
  zenifra create project --name <name> --plan db-basic --payment-mode monthly --config @examples/mariadb-project.json
62
+ zenifra create project --name <name> --plan db-free --payment-mode hourly --config @examples/valkey-key-value-project.json
63
+ zenifra create project --name <name> --plan cache-free --payment-mode hourly --config @examples/valkey-cache-project.json
64
+ zenifra create project --name <name> --plan queue-free --payment-mode hourly --config @examples/valkey-queue-project.json
61
65
  zenifra projects --type http --page 1 --limit 15
66
+ zenifra projects --type valkey --page 1 --limit 15
62
67
  zenifra project info --project <project-id>
68
+ zenifra valkey status --project <project-id>
69
+ zenifra valkey connection --project <project-id>
70
+ zenifra valkey credentials rotate --project <project-id> --wait
71
+ zenifra valkey credentials status --project <project-id> --operation <operation-id>
63
72
  zenifra project url --project <project-id>
64
73
  zenifra project logs --project <project-id> --instance <instance-id>
65
74
  zenifra project metrics --project <project-id> --instance <instance-id>
75
+ zenifra project metrics capabilities --project <project-id>
66
76
  zenifra project network --project <project-id> --view summary
67
77
  zenifra project image set --project <project-id> --image ghcr.io/zenifra/app:tag
68
78
  zenifra project envs --project <project-id>
@@ -84,6 +94,41 @@ zenifra deploy --project <project-id> --branch main
84
94
  zenifra deploy watch --project <project-id> --build <build-id>
85
95
  ```
86
96
 
97
+ ## Metricas de projetos Valkey
98
+
99
+ O comando `zenifra project metrics` mostra as metricas do projeto e, para projetos Valkey, o snapshot da instancia selecionada:
100
+
101
+ ```bash
102
+ zenifra project metrics --project <project-id> --instance instance-1
103
+ ```
104
+
105
+ A saida legivel inclui:
106
+
107
+ - recursos: CPU e memoria da instancia;
108
+ - capacidade: memoria usada, pico, limite e fragmentacao;
109
+ - clientes conectados e bloqueados;
110
+ - atividade: operacoes por segundo e trafego de entrada/saida;
111
+ - ciclo de vida: chaves expiradas e removidas;
112
+ - uptime e perfil (`cache`, `key-value` ou `queue`);
113
+ - confiabilidade: replicacao e persistencia, quando disponiveis.
114
+
115
+ Perfis especificos podem incluir:
116
+
117
+ - `cache`: hits, misses e hit ratio;
118
+ - `key-value`: total de chaves e chaves com expiracao;
119
+ - `queue`: metricas comuns, sem estimar profundidade quando ela nao esta disponivel.
120
+
121
+ Para consultar o nivel de acesso e os grupos disponiveis antes de buscar um snapshot:
122
+
123
+ ```bash
124
+ zenifra project metrics capabilities --project <project-id>
125
+ zenifra project metrics capabilities --project <project-id> --json
126
+ ```
127
+
128
+ `--json` preserva a resposta publica da API. Valores ausentes sao exibidos como `indisponivel` no modo legivel; `0` continua sendo um valor valido. O historico nao esta disponivel neste momento.
129
+
130
+ ---
131
+
87
132
  ## Builds GitHub
88
133
 
89
134
  Use `zenifra builds` para listar o historico de builds e `zenifra builds logs` para ler os logs do pipeline GitHub de um build especifico.
@@ -125,7 +170,7 @@ zenifra plans --type database
125
170
  zenifra plans --type storage --json
126
171
  ```
127
172
 
128
- `zenifra plans` funciona sem autenticacao e mostra os catalogos publicos de HTTP, banco e armazenamento.
173
+ `zenifra plans` funciona sem autenticacao e mostra os catalogos publicos de HTTP, banco, armazenamento e Valkey. Use `--type valkey` para consultar Key Value, Cache e Queue.
129
174
 
130
175
  Valores de variaveis de ambiente sao mascarados por padrao, inclusive em `--json`.
131
176
  Use `--show-values` apenas quando precisar inspecionar os valores completos.
@@ -193,6 +238,9 @@ Use os arquivos em `examples/` como base para `zenifra create project`:
193
238
  - `examples/http-autoscaling-project.json`: projeto HTTP pago criado com auto-scaling
194
239
  - `examples/postgresql-project.json`: projeto PostgreSQL
195
240
  - `examples/mariadb-project.json`: projeto MariaDB
241
+ - `examples/valkey-key-value-project.json`: projeto Valkey Key Value com armazenamento persistente
242
+ - `examples/valkey-cache-project.json`: projeto Valkey Cache sem armazenamento persistente
243
+ - `examples/valkey-queue-project.json`: projeto Valkey Queue com armazenamento persistente
196
244
 
197
245
  Se voce rodar apenas `zenifra create project`, a CLI abre um wizard interativo estilo `npm init` e pergunta todos os campos guiados. Cada pergunta mostra:
198
246
 
@@ -205,6 +253,7 @@ O wizard atual cobre:
205
253
  - projetos `http` com origem `github` ou `oci`
206
254
  - projetos `postgresql`
207
255
  - projetos `mariadb`
256
+ - projetos `valkey` nos perfis `key_value`, `cache` e `queue`
208
257
 
209
258
  `zenifra create project` nao assume valores default para `--plan` e `--payment-mode`.
210
259
  Configs HTTP nao interativas tambem devem informar `config.exposure`; use `public` para criar rota/dominio publico ou `private` para manter a aplicacao sem exposicao na internet.
@@ -213,9 +262,12 @@ Antes de escolher um plano com o usuario, compare os catalogos com `zenifra plan
213
262
  Valores aceitos:
214
263
 
215
264
  - `payment_mode`: `hourly`, `monthly`, `yearly`
216
- - `type_project` no `config`: `http`, `postgresql`, `mariadb`
265
+ - `type_project` no `config`: `http`, `postgresql`, `mariadb`, `valkey`
217
266
  - `exposure` no `config` HTTP: `public`, `private`
218
- - `plan`: `free`, `static`, `basic`, `premium`, `premium_plus`, `business`, `deep_learning_basic`, `deep_learning_premium`, `db-free`, `db-starter`, `db-basic`, `db-premium`, `db-enterprise`
267
+ - `plan`: consulte `zenifra plans` para os planos atuais; Valkey usa `db-*` para Key Value, `cache-*` para Cache e `queue-*` para Queue
268
+ - `config.profile` em projetos Valkey: `key_value`, `cache` ou `queue`
269
+ - `config.version` em projetos Valkey: a versão retornada por `zenifra plans --type valkey`
270
+ - `config.storage` em projetos Valkey: obrigatório e persistente para Key Value/Queue; omitido para Cache
219
271
  - `config.github.runtime` (quando houver GitHub em projeto HTTP): `nodejs` ou `python`
220
272
  - `config.autoscaling` (somente HTTP pago): `enabled: true`, `max_instances` maior ou igual a `config.instances` e alvos opcionais de CPU/memoria entre 1 e 100
221
273
 
@@ -226,6 +278,9 @@ Observacoes do wizard:
226
278
  - na criacao com auto-scaling, `config.instances` e o minimo inicial e `config.autoscaling.max_instances` e o maximo
227
279
  - em projetos de banco, o wizard nao pergunta `username`, `password` nem `database name`
228
280
  - em projetos de banco, a CLI preenche apenas campos tecnicos minimos exigidos pela validacao atual da API
281
+ - em projetos Valkey, a capacidade é definida pelo plano e a CLI não pergunta instâncias, imagem, variáveis de ambiente ou exposição HTTP
282
+ - a conexão mascarada pode ser consultada a qualquer momento; a credencial completa aparece apenas na criação ou em uma rotação concluída
283
+ - `valkey credentials rotate` retorna uma operação assíncrona; use `--wait` ou `valkey credentials status` para acompanhar
229
284
 
230
285
  ## Regressao manual de auto-scaling em staging
231
286
 
package/bin/zenifra.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { randomUUID } from 'node:crypto';
3
4
  import { mkdir, readFile, rm, writeFile, chmod } from 'node:fs/promises';
4
5
  import { existsSync } from 'node:fs';
5
6
  import { dirname, join } from 'node:path';
@@ -13,6 +14,7 @@ const DOCS_RUNTIME_URL = `${DOCS_BASE_URL}/docs/runtime`;
13
14
  const DOCS_CREATE_HTTP_URL = `${DOCS_BASE_URL}/docs/how-use-zenifra/http/how-to-create-a-http-project-at-zenifra-via-console`;
14
15
  const DOCS_CREATE_POSTGRESQL_URL = `${DOCS_BASE_URL}/docs/how-use-zenifra/database/how-to-create-a-postgresql-project-at-zenifra-via-console`;
15
16
  const DOCS_CREATE_MARIADB_URL = `${DOCS_BASE_URL}/docs/how-use-zenifra/database/how-to-create-a-mariadb-project-at-zenifra-via-console`;
17
+ const DOCS_CREATE_VALKEY_URL = `${DOCS_BASE_URL}/docs/managed-services/valkey`;
16
18
  const DOCS_CONFIGURATION_URL = `${DOCS_BASE_URL}/docs/configuration`;
17
19
  const DOCS_DATABASE_CONFIGURATION_URL = `${DOCS_BASE_URL}/docs/database/configuration/configuration`;
18
20
  const DOCS_PAYMENTS_URL = `${DOCS_BASE_URL}/docs/payments/payments`;
@@ -41,6 +43,7 @@ const KNOWN_FLAG_NAMES = new Set([
41
43
  'from',
42
44
  'help',
43
45
  'image',
46
+ 'idempotencyKey',
44
47
  'instance',
45
48
  'interval',
46
49
  'json',
@@ -52,6 +55,7 @@ const KNOWN_FLAG_NAMES = new Set([
52
55
  'mode',
53
56
  'name',
54
57
  'org',
58
+ 'operation',
55
59
  'page',
56
60
  'password',
57
61
  'paymentMode',
@@ -67,6 +71,7 @@ const KNOWN_FLAG_NAMES = new Set([
67
71
  'type',
68
72
  'value',
69
73
  'view',
74
+ 'wait',
70
75
  ]);
71
76
  const ALLOWED_PLAN_VALUES = new Set([
72
77
  'free',
@@ -82,9 +87,19 @@ const ALLOWED_PLAN_VALUES = new Set([
82
87
  'db-basic',
83
88
  'db-premium',
84
89
  'db-enterprise',
90
+ 'cache-free',
91
+ 'cache-starter',
92
+ 'cache-basic',
93
+ 'cache-premium',
94
+ 'cache-enterprise',
95
+ 'queue-free',
96
+ 'queue-starter',
97
+ 'queue-basic',
98
+ 'queue-premium',
99
+ 'queue-enterprise',
85
100
  ]);
86
101
  const ALLOWED_PAYMENT_MODE_VALUES = new Set(['hourly', 'monthly', 'yearly']);
87
- const ALLOWED_TYPE_PROJECT_VALUES = new Set(['http', 'postgresql', 'mariadb']);
102
+ const ALLOWED_TYPE_PROJECT_VALUES = new Set(['http', 'postgresql', 'mariadb', 'valkey']);
88
103
  const ALLOWED_RUNTIME_VALUES = new Set(['nodejs', 'python']);
89
104
  const ALLOWED_EXPOSURE_VALUES = new Set(['public', 'private']);
90
105
  const GITHUB_RUNTIME_VERSIONS = {
@@ -115,7 +130,8 @@ const WIZARD_DATABASE_PLAN_OPTIONS = [
115
130
  'db-free',
116
131
  ];
117
132
  const WIZARD_PAYMENT_MODE_OPTIONS = ['hourly', 'monthly', 'yearly'];
118
- const WIZARD_TYPE_PROJECT_OPTIONS = ['http', 'postgresql', 'mariadb'];
133
+ const WIZARD_TYPE_PROJECT_OPTIONS = ['http', 'postgresql', 'mariadb', 'valkey'];
134
+ const WIZARD_VALKEY_PROFILE_OPTIONS = ['key_value', 'cache', 'queue'];
119
135
  const WIZARD_HTTP_EXPOSURE_OPTIONS = ['public', 'private'];
120
136
  let promptInterface;
121
137
  let pipedInputPromise;
@@ -153,14 +169,19 @@ Usage:
153
169
  zenifra profile remove <name> [--json]
154
170
  zenifra orgs [--json]
155
171
  zenifra org set [--org <id>]
156
- zenifra plans [--type <all|http|database|storage>] [--json]
172
+ zenifra plans [--type <all|http|database|storage|valkey>] [--json]
157
173
  zenifra create project
158
174
  zenifra create project --name <name> --plan <plan> --payment-mode <mode> --config <json|@file> [--description <text>] [--org <id>] [--json]
159
- zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb>] [--page <n>] [--limit <n>]
175
+ zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb|valkey>] [--page <n>] [--limit <n>]
176
+ zenifra valkey status --project <id> [--json]
177
+ zenifra valkey connection --project <id> [--json]
178
+ zenifra valkey credentials rotate --project <id> [--idempotency-key <key>] [--wait] [--interval <seconds>] [--timeout <seconds>] [--json]
179
+ zenifra valkey credentials status --project <id> --operation <id> [--json]
160
180
  zenifra project info --project <id> [--json]
161
181
  zenifra project url --project <id> [--json]
162
182
  zenifra project logs --project <id> [--instance <id>] [--json]
163
183
  zenifra project metrics --project <id> [--instance <id>] [--json]
184
+ zenifra project metrics capabilities --project <id> [--json]
164
185
  zenifra project network --project <id> [--view <summary|status-codes|routes|user-agents|request-events|source-ips>] [--json]
165
186
  zenifra project image set --project <id> --image <image> [--json]
166
187
  zenifra project exposure set --project <id> --exposure <public|private> [--json]
@@ -319,16 +340,65 @@ const HELP_SPECS = [
319
340
  },
320
341
  {
321
342
  command: 'plans',
322
- usage: 'zenifra plans [--type <all|http|database|storage>] [--json]',
323
- description: 'Lista os catalogos publicos de preco de planos HTTP, banco e armazenamento.',
324
- flags: ['--type <type> Filtra o catalogo: all, http, database ou storage.', '--json Imprime a resposta em JSON.'],
325
- examples: ['zenifra plans', 'zenifra plans --type http', 'zenifra plans --type storage --json'],
343
+ usage: 'zenifra plans [--type <all|http|database|storage|valkey>] [--json]',
344
+ description: 'Lista os catalogos publicos de preco de planos HTTP, banco, armazenamento e Valkey.',
345
+ flags: ['--type <type> Filtra o catalogo: all, http, database, storage, valkey, key-value, cache ou queue.', '--json Imprime a resposta em JSON.'],
346
+ examples: ['zenifra plans', 'zenifra plans --type http', 'zenifra plans --type valkey --json'],
326
347
  output: 'HTTP\nPlano Hora Mes Ano Recursos\nfree R$ 0,00 R$ 0,00 R$ 0,00 1 GB Armazenamento Efemero',
327
348
  jsonOutput: '{"http":[{"plan":"free","prices":{"hourly":0,"monthly":0,"yearly":0},"features":["1 GB Armazenamento Efemero"]}],"database":[],"storage":[]}',
328
349
  },
350
+ {
351
+ command: 'valkey',
352
+ usage: 'zenifra valkey\n zenifra valkey status --project <id> [--json]\n zenifra valkey connection --project <id> [--json]\n zenifra valkey credentials rotate --project <id> [--idempotency-key <key>] [--wait] [--interval <seconds>] [--timeout <seconds>] [--json]\n zenifra valkey credentials status --project <id> --operation <id> [--json]',
353
+ description: 'Consulta projetos Valkey e gerencia suas conexoes e credenciais.',
354
+ examples: ['zenifra valkey status --project proj_1', 'zenifra valkey connection --project proj_1', 'zenifra valkey credentials rotate --project proj_1 --wait'],
355
+ output: 'Zenifra CLI - valkey',
356
+ notes: ['A string de conexao completa aparece somente quando uma criacao ou rotacao a disponibiliza.'],
357
+ },
358
+ {
359
+ command: 'valkey status',
360
+ usage: 'zenifra valkey status --project <id> [--json]',
361
+ description: 'Mostra estado, perfil, versao e persistencia de um projeto Valkey.',
362
+ flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.'],
363
+ examples: ['zenifra valkey status --project proj_1'],
364
+ output: 'Status Perfil Versao Persistente Instancias\nrunning cache 9.1.1 nao 1',
365
+ },
366
+ {
367
+ command: 'valkey connection',
368
+ usage: 'zenifra valkey connection --project <id> [--json]',
369
+ description: 'Mostra os dados de conexao de um projeto Valkey sem revelar a credencial.',
370
+ flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.'],
371
+ examples: ['zenifra valkey connection --project proj_1'],
372
+ output: 'Host Porta TLS Conexao\nvalkey.example.com 6380 sim valkeys://default:********@valkey.example.com:6380/0',
373
+ notes: ['Use a credencial recebida na criacao ou em uma rotacao concluida.'],
374
+ },
375
+ {
376
+ command: 'valkey credentials',
377
+ usage: 'zenifra valkey credentials\n zenifra valkey credentials rotate --project <id> [--idempotency-key <key>] [--wait] [--interval <seconds>] [--timeout <seconds>] [--json]\n zenifra valkey credentials status --project <id> --operation <id> [--json]',
378
+ description: 'Agrupa comandos de renovacao e acompanhamento de credenciais Valkey.',
379
+ examples: ['zenifra valkey credentials rotate --project proj_1', 'zenifra valkey credentials status --project proj_1 --operation operation_1'],
380
+ output: 'Zenifra CLI - valkey credentials',
381
+ },
382
+ {
383
+ command: 'valkey credentials rotate',
384
+ usage: 'zenifra valkey credentials rotate --project <id> [--idempotency-key <key>] [--wait] [--interval <seconds>] [--timeout <seconds>] [--json]',
385
+ description: 'Solicita a renovacao da credencial e retorna uma operacao acompanhavel.',
386
+ flags: ['--project <id> ID do projeto.', '--idempotency-key <key> Chave para repetir a mesma solicitacao.', '--wait Aguarda a conclusao.', '--interval <seconds> Intervalo do polling. Padrao: 2.', '--timeout <seconds> Timeout total. Padrao: 900.', '--json Imprime a resposta em JSON.'],
387
+ examples: ['zenifra valkey credentials rotate --project proj_1', 'zenifra valkey credentials rotate --project proj_1 --wait --timeout 120'],
388
+ output: 'Operacao aceita: rotation_123\nEstado: accepted',
389
+ notes: ['Salve a nova string de conexao quando a operacao atingir completed.'],
390
+ },
391
+ {
392
+ command: 'valkey credentials status',
393
+ usage: 'zenifra valkey credentials status --project <id> --operation <id> [--json]',
394
+ description: 'Consulta o estado de uma renovacao de credencial Valkey.',
395
+ flags: ['--project <id> ID do projeto.', '--operation <id> ID da operacao.', '--json Imprime a resposta em JSON.'],
396
+ examples: ['zenifra valkey credentials status --project proj_1 --operation rotation_123'],
397
+ output: 'Operacao: rotation_123\nEstado: completed',
398
+ },
329
399
  {
330
400
  command: 'projects',
331
- usage: 'zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb>] [--page <n>] [--limit <n>]',
401
+ usage: 'zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb|valkey>] [--page <n>] [--limit <n>]',
332
402
  description: 'Lista projetos da organizacao ativa ou da API key.',
333
403
  flags: ['--json Imprime a resposta em JSON.', '--org <id> Usa uma organizacao especifica com sessao de usuario.', '--type <type> Filtra por tipo de projeto.', '--page <n> Pagina. Padrao: 1.', '--limit <n> Itens por pagina. Padrao: 15.'],
334
404
  examples: ['zenifra projects --type http --page 1 --limit 15', 'zenifra projects --json'],
@@ -337,9 +407,9 @@ const HELP_SPECS = [
337
407
  },
338
408
  {
339
409
  command: 'create project',
340
- usage: 'zenifra create project\n zenifra create project --name <name> --plan <plan> --payment-mode <mode> --config <json|@file> [--description <text>] [--org <id>] [--json]',
410
+ usage: 'zenifra create project\n zenifra create project --name <name> --plan <plan> --payment-mode <mode> --config <json|@file> [--description <text>] [--idempotency-key <key>] [--org <id>] [--json]',
341
411
  description: 'Cria um projeto via wizard interativo ou via payload de configuracao JSON.',
342
- flags: ['--name <name> Nome do projeto.', '--plan <plan> Plano do projeto.', '--payment-mode <mode> Modo de pagamento.', '--config <json|@file> JSON inline ou arquivo.', '--description <text> Descricao opcional.', '--json Imprime a resposta em JSON.'],
412
+ flags: ['--name <name> Nome do projeto.', '--plan <plan> Plano do projeto.', '--payment-mode <mode> Modo de pagamento.', '--config <json|@file> JSON inline ou arquivo.', '--description <text> Descricao opcional.', '--idempotency-key <key> Chave para repetir a mesma criacao com seguranca.', '--json Imprime a resposta em JSON.'],
343
413
  examples: ['zenifra create project', 'zenifra create project --name api-web --plan free --payment-mode hourly --config @examples/http-project.json'],
344
414
  output: 'Campo Valor\n------- --------------------------------------\nProjeto 507f1f77bcf86cd799439012\nDominio https://api-web.client.zenifra.com',
345
415
  jsonOutput: '{"status":"success","data":{"id":"507f1f77bcf86cd799439012","name":"api-web"}}',
@@ -347,11 +417,11 @@ const HELP_SPECS = [
347
417
  },
348
418
  {
349
419
  command: 'project',
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]',
420
+ 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 metrics capabilities --project <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]',
351
421
  description: 'Agrupa comandos operacionais e de introspecao sobre um projeto especifico.',
352
422
  examples: ['zenifra project', 'zenifra project info --project proj_1', 'zenifra project env add --project proj_1 --name NODE_ENV --value production'],
353
423
  output: 'Zenifra CLI - project',
354
- notes: ['Use "zenifra help project <subcomando>" para detalhes de info, url, logs, metrics, network, image, exposure, autoscaling, billing, envs e instances.'],
424
+ notes: ['Use "zenifra help project <subcomando>" para detalhes de info, url, logs, metrics, capabilities, network, image, exposure, autoscaling, billing, envs e instances.'],
355
425
  },
356
426
  {
357
427
  command: 'project info',
@@ -384,11 +454,26 @@ const HELP_SPECS = [
384
454
  {
385
455
  command: 'project metrics',
386
456
  usage: 'zenifra project metrics --project <id> [--instance <id>] [--json]',
387
- description: 'Mostra CPU, memoria e dados basicos de rede do projeto.',
388
- flags: ['--project <id> ID do projeto.', '--instance <id> Filtra uma instancia.', '--json Imprime a resposta em JSON.'],
389
- examples: ['zenifra project metrics --project 507f1f77bcf86cd799439012 --instance web-1'],
390
- output: 'Instancia Tipo CPU Memoria\nweb-1 application 0.2 64',
391
- jsonOutput: '{"type":"application","instance":"web-1","cpu":0.2,"memory":64,"network":{"requests":120}}',
457
+ description: 'Mostra metricas de recursos do projeto e, para Valkey, o snapshot nativo da instancia.',
458
+ flags: ['--project <id> ID do projeto.', '--instance <id> Filtra uma instancia.', '--json Imprime a resposta em JSON sem formatacao adicional.'],
459
+ examples: ['zenifra project metrics --project 507f1f77bcf86cd799439012 --instance web-1', 'zenifra project metrics --project 507f1f77bcf86cd799439012 --instance instance-1'],
460
+ output: 'Campo Valor\n---------- ----------------\nPerfil cache\nDisponibilidade available\nMemoria usada 100 MB',
461
+ jsonOutput: '{"instance":"instance-1","type":"valkey","availability":"available","valkey":{"schema_version":1,"profile":"cache","memory":{"used_bytes":104857600}}}',
462
+ notes: [
463
+ 'Para projetos Valkey, a saida legivel inclui recursos, capacidade, clientes, atividade, ciclo de vida de chaves, perfil e confiabilidade.',
464
+ 'Valores ausentes sao exibidos como indisponivel; zero continua sendo um valor valido.',
465
+ 'Use "zenifra project metrics capabilities --project <id>" para consultar o acesso e os grupos de metricas disponiveis.',
466
+ ],
467
+ },
468
+ {
469
+ command: 'project metrics capabilities',
470
+ usage: 'zenifra project metrics capabilities --project <id> [--json]',
471
+ description: 'Mostra os grupos de metricas e o nivel de acesso disponivel para o projeto.',
472
+ flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.'],
473
+ examples: ['zenifra project metrics capabilities --project 507f1f77bcf86cd799439012'],
474
+ output: 'Campo Valor\n---------- ------------------------------------------------------------\nAcesso snapshot\nAtualizacao 60 s\nHistorico indisponivel',
475
+ jsonOutput: '{"access":"snapshot","groups":["resources","capacity","clients","activity","key_lifecycle","profile","reliability"],"refresh_seconds":60,"history":null}',
476
+ notes: ['A API permanece como fonte de autorizacao. A CLI nao mantem uma lista local de planos.'],
392
477
  },
393
478
  {
394
479
  command: 'project network',
@@ -654,7 +739,7 @@ function removedProjectsCreateMessage() {
654
739
  }
655
740
 
656
741
  function isNamespaceCommand(command) {
657
- return ['auth', 'profile', 'project', 'org'].includes(command);
742
+ return ['auth', 'profile', 'project', 'org', 'valkey'].includes(command);
658
743
  }
659
744
 
660
745
  function parseArgs(argv) {
@@ -1029,6 +1114,11 @@ function estimateWizardTotal(state = {}) {
1029
1114
  return total;
1030
1115
  }
1031
1116
 
1117
+ if (typeProject === 'valkey') {
1118
+ total += 3;
1119
+ return total;
1120
+ }
1121
+
1032
1122
  return 18;
1033
1123
  }
1034
1124
 
@@ -1346,6 +1436,7 @@ async function request(session, flags, method, path, {
1346
1436
  orgId,
1347
1437
  tokenRequired = true,
1348
1438
  credential: credentialOverride,
1439
+ headers: extraHeaders = {},
1349
1440
  } = {}) {
1350
1441
  const credential = credentialOverride || resolveCredential(session);
1351
1442
  if (tokenRequired && !credential) {
@@ -1356,6 +1447,14 @@ async function request(session, flags, method, path, {
1356
1447
  if (body !== undefined) {
1357
1448
  headers['Content-Type'] = 'application/json';
1358
1449
  }
1450
+ for (const [name, value] of Object.entries(extraHeaders)) {
1451
+ if (value === undefined) continue;
1452
+ const normalizedName = name.toLowerCase();
1453
+ if (['authorization', 'x-organization-id', 'content-type'].includes(normalizedName)) {
1454
+ throw new CliError(`Header nao permitido: ${name}.`);
1455
+ }
1456
+ headers[name] = String(value);
1457
+ }
1359
1458
  if (credential?.token) {
1360
1459
  headers.Authorization = `Bearer ${credential.token}`;
1361
1460
  }
@@ -1403,6 +1502,9 @@ async function request(session, flags, method, path, {
1403
1502
  if (response.status === 429 && Number.isFinite(Number(payload?.retry_after_seconds))) {
1404
1503
  throw new CliError(`${message} Tente novamente em ${Number(payload.retry_after_seconds)} segundo(s).`);
1405
1504
  }
1505
+ if (response.status === 402 && path.includes('/metrics') && /support metrics/i.test(String(message))) {
1506
+ throw new CliError('Este projeto nao possui acesso a metricas.');
1507
+ }
1406
1508
  if (String(message).includes('missing x-organization-id')) {
1407
1509
  throw new CliError('Organizacao nao selecionada. Rode "zenifra org set" ou use --org <id>.');
1408
1510
  }
@@ -1443,6 +1545,128 @@ function printTable(rows, columns) {
1443
1545
  }
1444
1546
  }
1445
1547
 
1548
+ function formatValkeyNumber(value) {
1549
+ if (value === null || value === undefined || !Number.isFinite(Number(value))) return 'indisponivel';
1550
+ return String(value);
1551
+ }
1552
+
1553
+ function formatValkeyBytes(value, suffix = '') {
1554
+ if (value === null || value === undefined || !Number.isFinite(Number(value))) return 'indisponivel';
1555
+
1556
+ const numericValue = Number(value);
1557
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
1558
+ let unitIndex = 0;
1559
+ let normalizedValue = numericValue;
1560
+ while (normalizedValue >= 1024 && unitIndex < units.length - 1) {
1561
+ normalizedValue /= 1024;
1562
+ unitIndex += 1;
1563
+ }
1564
+
1565
+ const digits = normalizedValue >= 100 ? 0 : normalizedValue >= 10 ? 1 : 2;
1566
+ const formattedValue = Number(normalizedValue.toFixed(digits)).toString();
1567
+ return `${formattedValue} ${units[unitIndex]}${suffix}`;
1568
+ }
1569
+
1570
+ function formatValkeyPercent(value) {
1571
+ if (value === null || value === undefined || !Number.isFinite(Number(value))) return 'indisponivel';
1572
+ return `${Number((Number(value) * 100).toFixed(2))}%`;
1573
+ }
1574
+
1575
+ function valkeyMetricRows(metrics) {
1576
+ const native = metrics?.valkey;
1577
+ const rows = [
1578
+ { field: 'Instancia', value: metrics?.instance || '-' },
1579
+ { field: 'Disponibilidade', value: metrics?.availability || 'indisponivel' },
1580
+ { field: 'Observado em', value: metrics?.observed_at || 'indisponivel' },
1581
+ { field: 'CPU', value: formatValkeyNumber(metrics?.cpu) },
1582
+ { field: 'Memoria', value: formatValkeyBytes(metrics?.memory) },
1583
+ ];
1584
+
1585
+ if (!native) {
1586
+ rows.push({ field: 'Metricas Valkey', value: 'indisponivel' });
1587
+ return rows;
1588
+ }
1589
+
1590
+ rows.push(
1591
+ { field: 'Perfil', value: native.profile || '-' },
1592
+ { field: 'Memoria usada', value: formatValkeyBytes(native.memory?.used_bytes) },
1593
+ { field: 'Pico de memoria', value: formatValkeyBytes(native.memory?.peak_bytes) },
1594
+ { field: 'Limite de memoria', value: formatValkeyBytes(native.memory?.limit_bytes) },
1595
+ { field: 'Fragmentacao', value: formatValkeyNumber(native.memory?.fragmentation_ratio) },
1596
+ { field: 'Clientes conectados', value: formatValkeyNumber(native.clients?.connected) },
1597
+ { field: 'Clientes bloqueados', value: formatValkeyNumber(native.clients?.blocked) },
1598
+ { field: 'Operacoes por segundo', value: formatValkeyNumber(native.activity?.operations_per_second) },
1599
+ { field: 'Entrada', value: formatValkeyBytes(native.activity?.input_bytes_per_second, '/s') },
1600
+ { field: 'Saida', value: formatValkeyBytes(native.activity?.output_bytes_per_second, '/s') },
1601
+ { field: 'Chaves expiradas', value: formatValkeyNumber(native.keys?.expired_total) },
1602
+ { field: 'Chaves removidas', value: formatValkeyNumber(native.keys?.evicted_total) },
1603
+ { field: 'Uptime', value: `${formatValkeyNumber(native.uptime_seconds)} s` },
1604
+ );
1605
+
1606
+ if (native.cache) {
1607
+ rows.push(
1608
+ { field: 'Cache hits', value: formatValkeyNumber(native.cache.hits_total) },
1609
+ { field: 'Cache misses', value: formatValkeyNumber(native.cache.misses_total) },
1610
+ { field: 'Hit ratio', value: formatValkeyPercent(native.cache.hit_ratio) },
1611
+ );
1612
+ }
1613
+
1614
+ if (native.key_value) {
1615
+ rows.push(
1616
+ { field: 'Total de chaves', value: formatValkeyNumber(native.key_value.keys_total) },
1617
+ { field: 'Chaves com expiracao', value: formatValkeyNumber(native.key_value.keys_with_expiration) },
1618
+ );
1619
+ }
1620
+
1621
+ if (native.reliability) {
1622
+ const replication = native.reliability.replication;
1623
+ const persistence = native.reliability.persistence;
1624
+ if (replication) {
1625
+ rows.push(
1626
+ { field: 'Replicacao', value: replication.status || 'indisponivel' },
1627
+ { field: 'Repl. disponiveis', value: formatValkeyNumber(replication.replicas_available) },
1628
+ { field: 'Repl. esperadas', value: formatValkeyNumber(replication.replicas_expected) },
1629
+ { field: 'Lag', value: replication.lag_seconds === null || replication.lag_seconds === undefined ? 'indisponivel' : `${replication.lag_seconds} s` },
1630
+ );
1631
+ }
1632
+ if (persistence) {
1633
+ rows.push(
1634
+ { field: 'Persistencia', value: persistence.status || 'indisponivel' },
1635
+ { field: 'Persistencia ativa', value: persistence.enabled === null || persistence.enabled === undefined ? 'indisponivel' : persistence.enabled ? 'sim' : 'nao' },
1636
+ { field: 'Ultimo sucesso', value: persistence.last_success_at || 'indisponivel' },
1637
+ );
1638
+ }
1639
+ }
1640
+
1641
+ return rows;
1642
+ }
1643
+
1644
+ function printValkeyMetrics(metrics) {
1645
+ printTable(valkeyMetricRows(metrics), [
1646
+ { label: 'Campo', value: (row) => row.field },
1647
+ { label: 'Valor', value: (row) => row.value },
1648
+ ]);
1649
+ }
1650
+
1651
+ function printMetricsCapabilities(capabilities) {
1652
+ const groups = Array.isArray(capabilities?.groups) && capabilities.groups.length > 0
1653
+ ? capabilities.groups.join(', ')
1654
+ : 'nenhum';
1655
+ const history = capabilities?.history
1656
+ ? `${capabilities.history.retention_seconds} s de retencao`
1657
+ : 'indisponivel';
1658
+
1659
+ printTable([
1660
+ { field: 'Acesso', value: capabilities?.access || 'indisponivel' },
1661
+ { field: 'Grupos', value: groups },
1662
+ { field: 'Atualizacao', value: capabilities?.refresh_seconds === null || capabilities?.refresh_seconds === undefined ? 'indisponivel' : `${capabilities.refresh_seconds} s` },
1663
+ { field: 'Historico', value: history },
1664
+ ], [
1665
+ { label: 'Campo', value: (row) => row.field },
1666
+ { label: 'Valor', value: (row) => row.value },
1667
+ ]);
1668
+ }
1669
+
1446
1670
  function asArray(value) {
1447
1671
  if (Array.isArray(value)) return value;
1448
1672
  if (Array.isArray(value?.projects)) return value.projects;
@@ -1508,20 +1732,33 @@ function printEnvs(envs, flags) {
1508
1732
  }
1509
1733
 
1510
1734
  function printProject(project) {
1511
- const url = projectUrlOf(project);
1735
+ const managedService = project.managed_service;
1736
+ const isValkey = project.type_project === 'valkey' || managedService?.engine === 'valkey';
1512
1737
  const rows = [
1513
1738
  { label: 'ID', value: projectIdOf(project) || '-' },
1514
1739
  { label: 'Nome', value: project.name || '-' },
1515
1740
  { label: 'Status', value: project.status || '-' },
1516
1741
  { label: 'Tipo', value: project.type_project || '-' },
1517
- { label: 'Exposicao', value: project.exposure || '-' },
1518
1742
  { label: 'Plano', value: project.plan || '-' },
1519
- { label: 'URL', value: url || '-' },
1520
- { label: 'Imagem', value: project.image || '-' },
1521
- { label: 'Instancias', value: project.instances ?? '-' },
1522
- { label: 'Instancias atuais', value: project.additional_info?.current_instances ?? '-' },
1523
1743
  ];
1524
1744
 
1745
+ if (isValkey) {
1746
+ rows.push(
1747
+ { label: 'Perfil', value: managedService?.profile || '-' },
1748
+ { label: 'Versao', value: managedService?.version || '-' },
1749
+ { label: 'Persistente', value: managedService?.topology?.persistent === true ? 'sim' : 'nao' },
1750
+ { label: 'Instancias', value: project.instances ?? '-' },
1751
+ );
1752
+ } else {
1753
+ rows.push(
1754
+ { label: 'Exposicao', value: project.exposure || '-' },
1755
+ { label: 'URL', value: projectUrlOf(project) || '-' },
1756
+ { label: 'Imagem', value: project.image || '-' },
1757
+ { label: 'Instancias', value: project.instances ?? '-' },
1758
+ { label: 'Instancias atuais', value: project.additional_info?.current_instances ?? '-' },
1759
+ );
1760
+ }
1761
+
1525
1762
  for (const row of rows) {
1526
1763
  process.stdout.write(`${row.label}: ${row.value}\n`);
1527
1764
  }
@@ -1937,6 +2174,26 @@ function normalizePlan(value) {
1937
2174
  ['dbpremium', 'db-premium'],
1938
2175
  ['db_enterprise', 'db-enterprise'],
1939
2176
  ['dbenterprise', 'db-enterprise'],
2177
+ ['cache_free', 'cache-free'],
2178
+ ['cachefree', 'cache-free'],
2179
+ ['cache_starter', 'cache-starter'],
2180
+ ['cachestarter', 'cache-starter'],
2181
+ ['cache_basic', 'cache-basic'],
2182
+ ['cachebasic', 'cache-basic'],
2183
+ ['cache_premium', 'cache-premium'],
2184
+ ['cachepremium', 'cache-premium'],
2185
+ ['cache_enterprise', 'cache-enterprise'],
2186
+ ['cacheenterprise', 'cache-enterprise'],
2187
+ ['queue_free', 'queue-free'],
2188
+ ['queuefree', 'queue-free'],
2189
+ ['queue_starter', 'queue-starter'],
2190
+ ['queuestarter', 'queue-starter'],
2191
+ ['queue_basic', 'queue-basic'],
2192
+ ['queuebasic', 'queue-basic'],
2193
+ ['queue_premium', 'queue-premium'],
2194
+ ['queuepremium', 'queue-premium'],
2195
+ ['queue_enterprise', 'queue-enterprise'],
2196
+ ['queueenterprise', 'queue-enterprise'],
1940
2197
  ]);
1941
2198
 
1942
2199
  if (aliases.has(normalized)) return aliases.get(normalized);
@@ -1973,6 +2230,21 @@ function normalizeTypeProject(value) {
1973
2230
  ['postgresql', 'postgresql'],
1974
2231
  ['mariadb', 'mariadb'],
1975
2232
  ['maria db', 'mariadb'],
2233
+ ['valkey', 'valkey'],
2234
+ ['key value', 'valkey'],
2235
+ ['key-value', 'valkey'],
2236
+ ]);
2237
+ return aliases.get(raw) || raw;
2238
+ }
2239
+
2240
+ function normalizeValkeyProfile(value) {
2241
+ const raw = normalizeFreeText(value);
2242
+ const aliases = new Map([
2243
+ ['key_value', 'key_value'],
2244
+ ['key value', 'key_value'],
2245
+ ['key-value', 'key_value'],
2246
+ ['cache', 'cache'],
2247
+ ['queue', 'queue'],
1976
2248
  ]);
1977
2249
  return aliases.get(raw) || raw;
1978
2250
  }
@@ -2075,7 +2347,6 @@ async function fetchWizardCatalogs(session, flags) {
2075
2347
  unwrapData(await request(session, flags, 'GET', '/project/database/plans')),
2076
2348
  'os planos de banco',
2077
2349
  );
2078
-
2079
2350
  return { httpPlans, availableInstances, databasePlans };
2080
2351
  }
2081
2352
 
@@ -2084,6 +2355,10 @@ function normalizePlansCatalogType(value) {
2084
2355
  if (!normalized || normalized === 'all') return 'all';
2085
2356
  if (normalized === 'http') return 'http';
2086
2357
  if (['database', 'db', 'postgresql', 'mariadb'].includes(normalized)) return 'database';
2358
+ if (normalized === 'valkey') return 'valkey';
2359
+ if (normalized === 'key-value' || normalized === 'key value' || normalized === 'key_value') return 'valkey-key_value';
2360
+ if (normalized === 'cache') return 'valkey-cache';
2361
+ if (normalized === 'queue') return 'valkey-queue';
2087
2362
  if (normalized === 'storage') return 'storage';
2088
2363
  return null;
2089
2364
  }
@@ -2096,6 +2371,11 @@ function formatBrl(value) {
2096
2371
  }).format(Number.isFinite(number) ? number : 0);
2097
2372
  }
2098
2373
 
2374
+ function formatBrlFromCents(value) {
2375
+ const number = Number(value ?? 0);
2376
+ return formatBrl(Number.isFinite(number) ? number / 100 : 0);
2377
+ }
2378
+
2099
2379
  function humanizeStorageName(value) {
2100
2380
  const raw = String(value || '').trim();
2101
2381
  if (!raw) return { storage: '-', type: '-' };
@@ -2116,6 +2396,49 @@ function humanizeStorageName(value) {
2116
2396
  return { storage: raw, type: '-' };
2117
2397
  }
2118
2398
 
2399
+ const VALKEY_PROFILE_LABELS = {
2400
+ key_value: 'Key Value',
2401
+ cache: 'Cache',
2402
+ queue: 'Queue',
2403
+ };
2404
+
2405
+ function requireValkeyCatalog(value) {
2406
+ if (!value || typeof value !== 'object' || Array.isArray(value)
2407
+ || value.engine !== 'valkey'
2408
+ || typeof value.version !== 'string'
2409
+ || typeof value.currency !== 'string'
2410
+ || !Array.isArray(value.profiles)
2411
+ || !Array.isArray(value.plans)
2412
+ || value.profiles.length === 0
2413
+ || value.plans.length === 0) {
2414
+ throw new CliError('Falha ao carregar o catalogo Valkey. Tente novamente mais tarde.');
2415
+ }
2416
+
2417
+ const validProfiles = new Set(Object.keys(VALKEY_PROFILE_LABELS));
2418
+ if (value.profiles.some((profile) => !validProfiles.has(profile?.id) || typeof profile.persistence !== 'boolean')) {
2419
+ throw new CliError('Falha ao carregar o catalogo Valkey. Tente novamente mais tarde.');
2420
+ }
2421
+ if (value.plans.some((plan) => !validProfiles.has(plan?.profile) || typeof plan.id !== 'string' || !plan.prices || typeof plan.prices !== 'object')) {
2422
+ throw new CliError('Falha ao carregar o catalogo Valkey. Tente novamente mais tarde.');
2423
+ }
2424
+
2425
+ return value;
2426
+ }
2427
+
2428
+ function valkeyProfileFilter(type) {
2429
+ if (!type.startsWith('valkey-')) return null;
2430
+ return type.slice('valkey-'.length);
2431
+ }
2432
+
2433
+ function filterValkeyCatalog(catalog, profile) {
2434
+ if (!profile) return catalog;
2435
+ return {
2436
+ ...catalog,
2437
+ profiles: catalog.profiles.filter((entry) => entry.id === profile),
2438
+ plans: catalog.plans.filter((plan) => plan.profile === profile),
2439
+ };
2440
+ }
2441
+
2119
2442
  async function fetchPlansCatalogs(session, flags, type) {
2120
2443
  if (type === 'http') {
2121
2444
  return {
@@ -2150,27 +2473,59 @@ async function fetchPlansCatalogs(session, flags, type) {
2150
2473
  };
2151
2474
  }
2152
2475
 
2153
- const [httpPayload, databasePayload, storagePayload] = await Promise.all([
2476
+ if (type === 'valkey' || type.startsWith('valkey-')) {
2477
+ const catalog = requireValkeyCatalog(
2478
+ unwrapData(await request(session, flags, 'GET', '/managed-services/catalog', { tokenRequired: false })),
2479
+ );
2480
+ return {
2481
+ http: [],
2482
+ database: [],
2483
+ storage: [],
2484
+ valkey: filterValkeyCatalog(catalog, valkeyProfileFilter(type)),
2485
+ };
2486
+ }
2487
+
2488
+ const [httpPayload, databasePayload, storagePayload, valkeyPayload] = await Promise.all([
2154
2489
  request(session, flags, 'GET', '/project/plans', { tokenRequired: false }),
2155
2490
  request(session, flags, 'GET', '/project/database/plans', { tokenRequired: false }),
2156
2491
  request(session, flags, 'GET', '/project/storage/plans', { tokenRequired: false }),
2492
+ request(session, flags, 'GET', '/managed-services/catalog', { tokenRequired: false }),
2157
2493
  ]);
2158
2494
 
2159
2495
  return {
2160
2496
  http: requireWizardCatalogArray(unwrapData(httpPayload), 'os planos HTTP'),
2161
2497
  database: requireWizardCatalogArray(unwrapData(databasePayload), 'os planos de banco'),
2162
2498
  storage: requireWizardCatalogArray(unwrapData(storagePayload), 'os planos de storage'),
2499
+ valkey: requireValkeyCatalog(unwrapData(valkeyPayload)),
2163
2500
  };
2164
2501
  }
2165
2502
 
2503
+ function printValkeyCatalog(catalog, profileFilter) {
2504
+ const profiles = profileFilter ? [profileFilter] : Object.keys(VALKEY_PROFILE_LABELS);
2505
+ profiles.forEach((profile, index) => {
2506
+ if (index > 0) process.stdout.write('\n');
2507
+ process.stdout.write(`${VALKEY_PROFILE_LABELS[profile]}\n`);
2508
+ const plans = catalog.plans.filter((plan) => plan.profile === profile);
2509
+ printTable(plans, [
2510
+ { label: 'Plano', value: (plan) => plan.id || '-' },
2511
+ { label: 'Hora', value: (plan) => formatBrl(plan.prices?.hourly, catalog.currency) },
2512
+ { label: 'Mes', value: (plan) => formatBrl(plan.prices?.monthly, catalog.currency) },
2513
+ { label: 'Ano', value: (plan) => formatBrl(plan.prices?.yearly, catalog.currency) },
2514
+ { label: 'Capacidade', value: (plan) => `${plan.resources?.cpu || '-'} / ${plan.resources?.memory || '-'}` },
2515
+ { label: 'Storage incluído', value: (plan) => `${plan.included_storage_gb ?? 0} GB` },
2516
+ { label: 'Alta disponibilidade', value: (plan) => plan.high_availability?.[profile] ? 'sim' : 'nao' },
2517
+ ]);
2518
+ });
2519
+ }
2520
+
2166
2521
  function printPlansCatalogs(payload, type) {
2167
2522
  if (type === 'all' || type === 'http') {
2168
2523
  process.stdout.write('HTTP\n');
2169
2524
  printTable(asArray(payload.http), [
2170
2525
  { label: 'Plano', value: (plan) => plan.plan || '-' },
2171
- { label: 'Hora', value: (plan) => formatBrl(plan.prices?.hourly) },
2172
- { label: 'Mes', value: (plan) => formatBrl(plan.prices?.monthly) },
2173
- { label: 'Ano', value: (plan) => formatBrl(plan.prices?.yearly) },
2526
+ { label: 'Hora', value: (plan) => formatBrlFromCents(plan.prices?.hourly) },
2527
+ { label: 'Mes', value: (plan) => formatBrlFromCents(plan.prices?.monthly) },
2528
+ { label: 'Ano', value: (plan) => formatBrlFromCents(plan.prices?.yearly) },
2174
2529
  { label: 'Recursos', value: (plan) => asArray(plan.features).join(', ') || '-' },
2175
2530
  ]);
2176
2531
  }
@@ -2180,9 +2535,9 @@ function printPlansCatalogs(payload, type) {
2180
2535
  process.stdout.write('PostgreSQL / MariaDB\n');
2181
2536
  printTable(asArray(payload.database), [
2182
2537
  { label: 'Plano', value: (plan) => plan.plan || '-' },
2183
- { label: 'Hora', value: (plan) => formatBrl(plan.prices?.hourly) },
2184
- { label: 'Mes', value: (plan) => formatBrl(plan.prices?.monthly) },
2185
- { label: 'Ano', value: (plan) => formatBrl(plan.prices?.yearly) },
2538
+ { label: 'Hora', value: (plan) => formatBrlFromCents(plan.prices?.hourly) },
2539
+ { label: 'Mes', value: (plan) => formatBrlFromCents(plan.prices?.monthly) },
2540
+ { label: 'Ano', value: (plan) => formatBrlFromCents(plan.prices?.yearly) },
2186
2541
  { label: 'Recursos', value: (plan) => asArray(plan.features).join(', ') || '-' },
2187
2542
  ]);
2188
2543
  }
@@ -2198,12 +2553,17 @@ function printPlansCatalogs(payload, type) {
2198
2553
  { label: 'Ano', value: (storage) => formatBrl(storage.prices?.yearly) },
2199
2554
  ]);
2200
2555
  }
2556
+
2557
+ if (type === 'all' || type === 'valkey' || type.startsWith('valkey-')) {
2558
+ if (type === 'all') process.stdout.write('\n');
2559
+ printValkeyCatalog(payload.valkey, valkeyProfileFilter(type));
2560
+ }
2201
2561
  }
2202
2562
 
2203
2563
  async function handlePlans(session, flags) {
2204
2564
  const type = normalizePlansCatalogType(flags.type);
2205
2565
  if (!type) {
2206
- throw new CliError('Tipo de catalogo invalido. Valores aceitos: all, http, database, storage.');
2566
+ throw new CliError('Tipo de catalogo invalido. Valores aceitos: all, http, database, storage, valkey.');
2207
2567
  }
2208
2568
 
2209
2569
  const payload = await fetchPlansCatalogs(session, flags, type);
@@ -2261,9 +2621,53 @@ function validateCreateAutoscaling(config, plan, typeProject) {
2261
2621
  return { ...config, autoscaling: nextAutoscaling };
2262
2622
  }
2263
2623
 
2264
- function validateCreateInput({ plan, paymentMode, config }) {
2624
+ function validateValkeyConfig(config, plan, catalog) {
2625
+ const valkeyCatalog = requireValkeyCatalog(catalog);
2626
+ const profile = normalizeValkeyProfile(config?.profile);
2627
+ const profileDefinition = valkeyCatalog.profiles.find((entry) => entry.id === profile);
2628
+ if (!profileDefinition) {
2629
+ throw new CliError('config.profile invalido. Use key_value, cache ou queue.');
2630
+ }
2631
+
2632
+ const planDefinition = valkeyCatalog.plans.find((entry) => entry.id === plan && entry.profile === profile);
2633
+ if (!planDefinition) {
2634
+ throw new CliError(`Plano ${plan} nao pode ser usado com o perfil ${profile}. Consulte "zenifra plans --type valkey".`);
2635
+ }
2636
+
2637
+ if (config.version !== valkeyCatalog.version) {
2638
+ throw new CliError(`config.version invalida: use ${valkeyCatalog.version}, conforme o catalogo Valkey.`);
2639
+ }
2640
+
2641
+ const forbiddenFields = ['instances', 'envs', 'image', 'github', 'port', 'exposure', 'autoscaling'];
2642
+ const forbiddenField = forbiddenFields.find((field) => config[field] !== undefined);
2643
+ if (forbiddenField) {
2644
+ throw new CliError(`Valkey nao aceita config.${forbiddenField}. A capacidade e definida pelo plano.`);
2645
+ }
2646
+
2647
+ if (profileDefinition.persistence) {
2648
+ if (!isRecord(config.storage) || config.storage.persistent !== true) {
2649
+ throw new CliError('Perfis Valkey persistentes exigem storage.persistent=true.');
2650
+ }
2651
+ const capacity = Number(config.storage.capacity);
2652
+ if (!Number.isInteger(capacity) || capacity < 1 || capacity > 250) {
2653
+ throw new CliError('storage.capacity deve ser um inteiro entre 1 e 250 GiB.');
2654
+ }
2655
+ } else if (config.storage !== undefined) {
2656
+ throw new CliError('O perfil Cache nao aceita storage.');
2657
+ }
2658
+
2659
+ return {
2660
+ ...config,
2661
+ type_project: 'valkey',
2662
+ profile,
2663
+ version: valkeyCatalog.version,
2664
+ };
2665
+ }
2666
+
2667
+ function validateCreateInput({ plan, paymentMode, config, valkeyCatalog }) {
2668
+ const nextTypeProject = normalizeTypeProject(config?.type_project);
2265
2669
  const nextPlan = normalizePlan(plan);
2266
- if (!nextPlan || !ALLOWED_PLAN_VALUES.has(nextPlan)) {
2670
+ if (!nextPlan || (nextTypeProject !== 'valkey' && !ALLOWED_PLAN_VALUES.has(nextPlan))) {
2267
2671
  throw new CliError(`Plano invalido: "${plan}". Valores aceitos: ${formatAllowedValues(ALLOWED_PLAN_VALUES)}. Docs: ${DOCS_CREATE_HTTP_URL}`);
2268
2672
  }
2269
2673
 
@@ -2272,12 +2676,19 @@ function validateCreateInput({ plan, paymentMode, config }) {
2272
2676
  throw new CliError(`Modo de pagamento invalido: "${paymentMode}". Valores aceitos: ${formatAllowedValues(ALLOWED_PAYMENT_MODE_VALUES)}. Docs: ${DOCS_PAYMENTS_URL}`);
2273
2677
  }
2274
2678
 
2275
- const nextTypeProject = normalizeTypeProject(config?.type_project);
2276
2679
  if (!nextTypeProject || !ALLOWED_TYPE_PROJECT_VALUES.has(nextTypeProject)) {
2277
2680
  throw new CliError(`type_project invalido: "${config?.type_project}". Valores aceitos: ${formatAllowedValues(ALLOWED_TYPE_PROJECT_VALUES)}. Docs: ${DOCS_CREATE_HTTP_URL}`);
2278
2681
  }
2279
2682
 
2280
2683
  const nextConfig = { ...config, type_project: nextTypeProject };
2684
+ if (nextTypeProject === 'valkey') {
2685
+ return {
2686
+ plan: nextPlan,
2687
+ paymentMode: nextPaymentMode,
2688
+ config: validateValkeyConfig(nextConfig, nextPlan, valkeyCatalog),
2689
+ };
2690
+ }
2691
+
2281
2692
  if (nextTypeProject === 'http' && nextConfig.github) {
2282
2693
  const nextRuntime = normalizeRuntime(nextConfig.github.runtime);
2283
2694
  if (!nextRuntime || !ALLOWED_RUNTIME_VALUES.has(nextRuntime)) {
@@ -2344,6 +2755,10 @@ function printWizardSummary(payload) {
2344
2755
  lines.push(` banco: versao ${masked.config.version} | instancias ${masked.config.instances} | storage ${masked.config.storage.capacity}Gi`);
2345
2756
  }
2346
2757
 
2758
+ if (masked.config.type_project === 'valkey') {
2759
+ lines.push(` Valkey: perfil ${masked.config.profile} | versao ${masked.config.version}${masked.config.storage ? ` | storage ${masked.config.storage.capacity}Gi persistente` : ' | sem storage persistente'}`);
2760
+ }
2761
+
2347
2762
  process.stdout.write(`${lines.join('\n')}\n`);
2348
2763
  }
2349
2764
 
@@ -2735,6 +3150,65 @@ async function buildMariadbConfig() {
2735
3150
  };
2736
3151
  }
2737
3152
 
3153
+ async function buildValkeyConfig({ profile, plan, catalog }) {
3154
+ const planDefinition = catalog.plans.find((entry) => entry.id === plan && entry.profile === profile);
3155
+ const profileDefinition = catalog.profiles.find((entry) => entry.id === profile);
3156
+ if (!planDefinition || !profileDefinition) {
3157
+ throw new CliError('O catalogo Valkey nao possui o plano ou perfil selecionado. Tente novamente.');
3158
+ }
3159
+
3160
+ let storage;
3161
+ if (profileDefinition.persistence) {
3162
+ storage = {
3163
+ persistent: true,
3164
+ capacity: await promptWizardNumber({
3165
+ label: `Capacidade de armazenamento persistente (Gi, inclui ${planDefinition.included_storage_gb ?? 0} Gi)`,
3166
+ docs: DOCS_CREATE_VALKEY_URL,
3167
+ examples: [String(Math.max(1, planDefinition.included_storage_gb ?? 1)), '10', '20'],
3168
+ min: 1,
3169
+ max: 250,
3170
+ }),
3171
+ };
3172
+ }
3173
+
3174
+ const whitelist = await promptWizardPairs({
3175
+ introLabel: 'Deseja configurar acesso de rede para este projeto?',
3176
+ docs: DOCS_CREATE_VALKEY_URL,
3177
+ examples: ['sim', 'nao'],
3178
+ stateKey: 'valkeyNetworkAccess',
3179
+ itemLabels: [
3180
+ { key: 'cidr', label: 'CIDR permitido', required: true, examples: ['203.0.113.0/24'], docs: DOCS_CREATE_VALKEY_URL },
3181
+ { key: 'description', label: 'Descricao do acesso', required: true, examples: ['Aplicacao'], docs: DOCS_CREATE_VALKEY_URL },
3182
+ ],
3183
+ });
3184
+
3185
+ let blacklist = [];
3186
+ if (whitelist.length > 0) {
3187
+ blacklist = await promptWizardPairs({
3188
+ introLabel: 'Deseja adicionar bloqueios de rede?',
3189
+ docs: DOCS_CREATE_VALKEY_URL,
3190
+ examples: ['sim', 'nao'],
3191
+ itemLabels: [
3192
+ { key: 'cidr', label: 'CIDR bloqueado', required: true, examples: ['198.51.100.0/24'], docs: DOCS_CREATE_VALKEY_URL },
3193
+ { key: 'description', label: 'Descricao do bloqueio', required: true, examples: ['Bloqueio'], docs: DOCS_CREATE_VALKEY_URL },
3194
+ ],
3195
+ });
3196
+ }
3197
+
3198
+ return {
3199
+ type_project: 'valkey',
3200
+ profile,
3201
+ version: catalog.version,
3202
+ ...(storage ? { storage } : {}),
3203
+ ...(whitelist.length > 0 ? {
3204
+ network_access: {
3205
+ ingress_white_list: whitelist,
3206
+ ingress_black_list: blacklist,
3207
+ },
3208
+ } : {}),
3209
+ };
3210
+ }
3211
+
2738
3212
  async function runProjectCreateWizard(session, flags) {
2739
3213
  const catalogs = await fetchWizardCatalogs(session, flags);
2740
3214
  const ui = createWizardUi();
@@ -2765,16 +3239,34 @@ async function runProjectCreateWizard(session, flags) {
2765
3239
  options: WIZARD_TYPE_PROJECT_OPTIONS.map((value) => ({ value })),
2766
3240
  });
2767
3241
  ui.state.typeProject = typeProject;
3242
+ const valkeyCatalog = typeProject === 'valkey'
3243
+ ? requireValkeyCatalog(unwrapData(await request(session, flags, 'GET', '/managed-services/catalog')))
3244
+ : undefined;
3245
+ const profile = typeProject === 'valkey'
3246
+ ? await promptWizardSelect({
3247
+ label: 'Perfil Valkey',
3248
+ docs: DOCS_CREATE_VALKEY_URL,
3249
+ examples: WIZARD_VALKEY_PROFILE_OPTIONS,
3250
+ options: WIZARD_VALKEY_PROFILE_OPTIONS.map((value) => ({ value })),
3251
+ })
3252
+ : undefined;
3253
+ ui.state.valkeyProfile = profile;
2768
3254
  const planOptions = typeProject === 'http'
2769
3255
  ? sortWizardOptions(catalogs.httpPlans
2770
3256
  .map((plan) => plan?.plan)
2771
3257
  .filter((plan) => WIZARD_HTTP_PLAN_OPTIONS.includes(plan) && Number(catalogs.availableInstances?.[plan] || 0) > 0), WIZARD_HTTP_PLAN_OPTIONS)
3258
+ : typeProject === 'valkey'
3259
+ ? valkeyCatalog.plans
3260
+ .filter((planDefinition) => planDefinition.profile === profile)
3261
+ .map((planDefinition) => planDefinition.id)
2772
3262
  : sortWizardOptions(catalogs.databasePlans
2773
3263
  .map((plan) => plan?.plan)
2774
3264
  .filter((plan) => WIZARD_DATABASE_PLAN_OPTIONS.includes(plan) && (typeProject !== 'mariadb' || plan !== 'db-free')), WIZARD_DATABASE_PLAN_OPTIONS);
2775
3265
  const plan = await promptWizardSelect({
2776
3266
  label: 'Plano',
2777
- docs: typeProject === 'http' ? DOCS_CREATE_HTTP_URL : DOCS_DATABASE_CONFIGURATION_URL,
3267
+ docs: typeProject === 'http'
3268
+ ? DOCS_CREATE_HTTP_URL
3269
+ : typeProject === 'valkey' ? DOCS_CREATE_VALKEY_URL : DOCS_DATABASE_CONFIGURATION_URL,
2778
3270
  examples: [planOptions[0], planOptions.at(-1)],
2779
3271
  options: planOptions.map((value) => ({ value })),
2780
3272
  });
@@ -2800,6 +3292,8 @@ async function runProjectCreateWizard(session, flags) {
2800
3292
  });
2801
3293
  } else if (typeProject === 'postgresql') {
2802
3294
  config = await buildPostgresqlConfig({ plan, databasePlans: catalogs.databasePlans });
3295
+ } else if (typeProject === 'valkey') {
3296
+ config = await buildValkeyConfig({ profile, plan, catalog: valkeyCatalog });
2803
3297
  } else {
2804
3298
  config = await buildMariadbConfig();
2805
3299
  }
@@ -2815,7 +3309,9 @@ async function runProjectCreateWizard(session, flags) {
2815
3309
  printWizardSummary(payload);
2816
3310
  const confirmed = await promptWizardBoolean({
2817
3311
  label: 'Confirmar criacao do projeto',
2818
- docs: typeProject === 'http' ? DOCS_CREATE_HTTP_URL : DOCS_DATABASE_CONFIGURATION_URL,
3312
+ docs: typeProject === 'http'
3313
+ ? DOCS_CREATE_HTTP_URL
3314
+ : typeProject === 'valkey' ? DOCS_CREATE_VALKEY_URL : DOCS_DATABASE_CONFIGURATION_URL,
2819
3315
  examples: ['sim', 'nao'],
2820
3316
  });
2821
3317
  if (!confirmed) return null;
@@ -2836,10 +3332,18 @@ async function handleProjectCreate(session, flags) {
2836
3332
  const plan = wizardPayload?.plan || flags.plan || await prompt('Plano');
2837
3333
  const paymentMode = wizardPayload?.payment_mode || flags.paymentMode || await prompt('Modo de pagamento');
2838
3334
  const config = wizardPayload?.config || await parseConfig(flags.config || await prompt('Config JSON ou @arquivo'));
2839
- const validated = validateCreateInput({ plan, paymentMode, config });
3335
+ const typeProject = normalizeTypeProject(config?.type_project);
3336
+ const valkeyCatalog = typeProject === 'valkey'
3337
+ ? requireValkeyCatalog(unwrapData(await request(session, flags, 'GET', '/managed-services/catalog')))
3338
+ : undefined;
3339
+ if (flags.idempotencyKey !== undefined && !/^[A-Za-z0-9._-]{16,200}$/.test(String(flags.idempotencyKey))) {
3340
+ throw new CliError('--idempotency-key deve ter entre 16 e 200 caracteres: letras, numeros, ponto, underscore ou hifen.');
3341
+ }
3342
+ const validated = validateCreateInput({ plan, paymentMode, config, valkeyCatalog });
2840
3343
 
2841
3344
  const payload = await request(session, flags, 'POST', '/project', {
2842
3345
  orgId,
3346
+ headers: flags.idempotencyKey === undefined ? {} : { 'Idempotency-Key': String(flags.idempotencyKey) },
2843
3347
  body: {
2844
3348
  name,
2845
3349
  ...(description ? { description: String(description) } : {}),
@@ -2859,6 +3363,21 @@ async function handleProjectCreate(session, flags) {
2859
3363
  ...(project.domain ? [{ field: 'Dominio', value: formatPublicUrl(project.domain) }] : []),
2860
3364
  ...(project.api_key ? [{ field: 'API key', value: project.api_key }] : []),
2861
3365
  ];
3366
+ if (validated.config.type_project === 'valkey') {
3367
+ const service = project.managed_service || {};
3368
+ rows.push(
3369
+ { field: 'Perfil', value: service.profile || validated.config.profile },
3370
+ { field: 'Host', value: service.host || '-' },
3371
+ { field: 'Porta', value: service.port ?? '-' },
3372
+ { field: 'TLS', value: service.tls === true ? 'sim' : 'nao' },
3373
+ );
3374
+ if (service.connection_string) {
3375
+ rows.push({ field: 'String de conexao', value: service.connection_string });
3376
+ process.stdout.write('Salve a string de conexao em um local seguro; ela pode nao ser exibida novamente.\n');
3377
+ } else {
3378
+ rows.push({ field: 'Credencial', value: 'nao retornada nesta resposta; use valkey credentials rotate' });
3379
+ }
3380
+ }
2862
3381
  process.stdout.write('\n');
2863
3382
  printTable(rows, [
2864
3383
  { label: 'Campo', value: (row) => row.field },
@@ -2908,6 +3427,16 @@ async function handleProjectLogs(session, flags) {
2908
3427
  printLogs(logs, flags);
2909
3428
  }
2910
3429
 
3430
+ async function handleProjectMetricsCapabilities(session, flags) {
3431
+ const projectId = requireProjectId(flags, 'project metrics capabilities');
3432
+ if (!projectId) return;
3433
+ const orgId = await resolveOrgId(session, flags);
3434
+ const capabilities = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/metrics/capabilities`, { orgId }));
3435
+
3436
+ if (flags.json) return printJson(capabilities);
3437
+ printMetricsCapabilities(capabilities);
3438
+ }
3439
+
2911
3440
  async function handleProjectMetrics(session, flags) {
2912
3441
  const projectId = requireProjectId(flags, 'project metrics');
2913
3442
  if (!projectId) return
@@ -2916,6 +3445,9 @@ async function handleProjectMetrics(session, flags) {
2916
3445
  const metrics = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/metrics${query}`, { orgId }));
2917
3446
 
2918
3447
  if (flags.json) return printJson(metrics);
3448
+ if (metrics?.type === 'valkey') {
3449
+ return printValkeyMetrics(metrics);
3450
+ }
2919
3451
  if (Array.isArray(metrics)) {
2920
3452
  return printTable(metrics, [
2921
3453
  { label: 'Instancia', value: (metric) => metric.instance || '-' },
@@ -3330,6 +3862,131 @@ async function handleProjectBillingUsage(session, flags) {
3330
3862
  printBillingUsage(data);
3331
3863
  }
3332
3864
 
3865
+ function printValkeyStatus(data) {
3866
+ printTable([data], [
3867
+ { label: 'Status', value: (service) => service.status || '-' },
3868
+ { label: 'Perfil', value: (service) => service.profile || '-' },
3869
+ { label: 'Versao', value: (service) => service.version || '-' },
3870
+ { label: 'Persistente', value: (service) => service.persistence === true ? 'sim' : 'nao' },
3871
+ { label: 'Instancias', value: (service) => service.instances ?? '-' },
3872
+ ]);
3873
+ }
3874
+
3875
+ function printValkeyConnection(data) {
3876
+ printTable([data], [
3877
+ { label: 'Host', value: (service) => service.host || '-' },
3878
+ { label: 'Porta', value: (service) => service.port ?? '-' },
3879
+ { label: 'Usuario', value: (service) => service.username || '-' },
3880
+ { label: 'TLS', value: (service) => service.tls === true ? 'sim' : 'nao' },
3881
+ { label: 'Conexao', value: (service) => service.connection_string || '-' },
3882
+ ]);
3883
+ }
3884
+
3885
+ function printValkeyRotation(data, { accepted = false } = {}) {
3886
+ const rows = [
3887
+ { field: accepted ? 'Operacao aceita' : 'Operacao', value: data.operation_id || '-' },
3888
+ { field: 'Estado', value: data.state || '-' },
3889
+ ];
3890
+ if (data.connection_string) {
3891
+ rows.push({ field: 'String de conexao', value: data.connection_string });
3892
+ process.stdout.write('Salve a nova string de conexao em um local seguro; ela pode nao ser exibida novamente.\n');
3893
+ }
3894
+ printTable(rows, [
3895
+ { label: 'Campo', value: (row) => row.field },
3896
+ { label: 'Valor', value: (row) => row.value },
3897
+ ]);
3898
+ }
3899
+
3900
+ async function waitForValkeyRotation(session, flags, projectId, operationId, orgId) {
3901
+ const intervalMs = parseSecondsOption(flags.interval, '--interval', { defaultValue: 2, min: 0.1 }) * 1000;
3902
+ const timeoutMs = parseSecondsOption(flags.timeout, '--timeout', { defaultValue: 900, min: 1 }) * 1000;
3903
+ const startedAt = Date.now();
3904
+ let lastState = 'accepted';
3905
+
3906
+ while (true) {
3907
+ const data = unwrapData(await request(
3908
+ session,
3909
+ flags,
3910
+ 'GET',
3911
+ `/managed-services/${projectId}/credential-rotations/${operationId}`,
3912
+ { orgId },
3913
+ ));
3914
+ lastState = data?.state || lastState;
3915
+ if (lastState === 'completed') return data;
3916
+ if (['failed', 'cancelled', 'canceled'].includes(lastState)) {
3917
+ throw new CliError(`A renovacao de credencial terminou com estado ${lastState}. Operacao: ${operationId}.`);
3918
+ }
3919
+ if (Date.now() - startedAt >= timeoutMs) {
3920
+ throw new CliError(`Timeout aguardando a renovacao ${operationId}. Ultimo estado: ${lastState}.`);
3921
+ }
3922
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, intervalMs));
3923
+ }
3924
+ }
3925
+
3926
+ async function handleValkeyStatus(session, flags) {
3927
+ const projectId = requireProjectId(flags, 'valkey status');
3928
+ if (!projectId) return;
3929
+ const orgId = await resolveOrgId(session, flags);
3930
+ const data = unwrapData(await request(session, flags, 'GET', `/managed-services/${projectId}/status`, { orgId }));
3931
+ if (flags.json) return printJson(data);
3932
+ printValkeyStatus(data);
3933
+ }
3934
+
3935
+ async function handleValkeyConnection(session, flags) {
3936
+ const projectId = requireProjectId(flags, 'valkey connection');
3937
+ if (!projectId) return;
3938
+ const orgId = await resolveOrgId(session, flags);
3939
+ const data = unwrapData(await request(session, flags, 'GET', `/managed-services/${projectId}/connection`, { orgId }));
3940
+ if (flags.json) return printJson(data);
3941
+ printValkeyConnection(data);
3942
+ }
3943
+
3944
+ async function handleValkeyCredentialStatus(session, flags) {
3945
+ const projectId = requireProjectId(flags, 'valkey credentials status');
3946
+ if (!projectId) return;
3947
+ if (!flags.operation) {
3948
+ printCommandHelpAndFail('valkey credentials status');
3949
+ return;
3950
+ }
3951
+ const orgId = await resolveOrgId(session, flags);
3952
+ const data = unwrapData(await request(
3953
+ session,
3954
+ flags,
3955
+ 'GET',
3956
+ `/managed-services/${projectId}/credential-rotations/${flags.operation}`,
3957
+ { orgId },
3958
+ ));
3959
+ if (flags.json) return printJson(data);
3960
+ printValkeyRotation(data);
3961
+ }
3962
+
3963
+ async function handleValkeyCredentialRotate(session, flags) {
3964
+ const projectId = requireProjectId(flags, 'valkey credentials rotate');
3965
+ if (!projectId) return;
3966
+ const idempotencyKey = String(flags.idempotencyKey || randomUUID());
3967
+ if (!/^[A-Za-z0-9._-]{16,200}$/.test(idempotencyKey)) {
3968
+ throw new CliError('--idempotency-key deve ter entre 16 e 200 caracteres: letras, numeros, ponto, underscore ou hifen.');
3969
+ }
3970
+ const orgId = await resolveOrgId(session, flags);
3971
+ const acceptedPayload = await request(session, flags, 'PATCH', `/managed-services/${projectId}/credentials`, {
3972
+ orgId,
3973
+ body: {},
3974
+ headers: { 'Idempotency-Key': idempotencyKey },
3975
+ });
3976
+ const accepted = unwrapData(acceptedPayload);
3977
+ if (!accepted?.operation_id) throw new CliError('A API nao retornou o identificador da renovacao de credencial.');
3978
+
3979
+ if (flags.wait) {
3980
+ const completed = await waitForValkeyRotation(session, flags, projectId, accepted.operation_id, orgId);
3981
+ if (flags.json) return printJson(completed);
3982
+ printValkeyRotation(completed);
3983
+ return;
3984
+ }
3985
+
3986
+ if (flags.json) return printJson(acceptedPayload);
3987
+ printValkeyRotation(accepted, { accepted: true });
3988
+ }
3989
+
3333
3990
  function buildQuery(flags, allowedKeys) {
3334
3991
  const params = new URLSearchParams();
3335
3992
  for (const key of allowedKeys) {
@@ -3540,7 +4197,7 @@ async function main() {
3540
4197
  return;
3541
4198
  }
3542
4199
 
3543
- if (command === 'auth' && subcommand === 'login') return handleLogin(session, flags);
4200
+ if (command === 'auth' && subcommand === 'login') return await handleLogin(session, flags);
3544
4201
  if (command === 'auth' && subcommand === 'api-key') return handleApiKeyLogin(session, flags);
3545
4202
  if (command === 'auth' && subcommand === 'logout') return handleLogout(session, flags);
3546
4203
  if (command === 'profile' && subcommand === 'list') return handleProfileList(session, flags);
@@ -3549,16 +4206,25 @@ async function main() {
3549
4206
  if (command === 'profile' && subcommand === 'edit') return handleProfileEdit(session, flags, positional);
3550
4207
  if (command === 'profile' && subcommand === 'use') return handleProfileUse(session, flags, positional);
3551
4208
  if (command === 'profile' && subcommand === 'remove') return handleProfileRemove(session, flags, positional);
3552
- if (command === 'login') return handleLogin(session, flags);
4209
+ if (command === 'login') return await handleLogin(session, flags);
3553
4210
  if (command === 'logout') return handleLogout(session, flags);
3554
4211
  if (command === 'plans') return handlePlans(session, flags);
3555
4212
  if (command === 'create' && subcommand === 'project') return handleProjectCreate(session, flags);
3556
4213
  if (command === 'orgs') return handleOrgs(session, flags);
3557
4214
  if (command === 'org' && subcommand === 'set') return handleOrgSet(session, flags);
3558
4215
  if (command === 'projects') return handleProjects(session, flags);
4216
+ if (command === 'valkey' && subcommand === 'credentials' && positional.length === 2) {
4217
+ process.stdout.write(commandHelp(['valkey', 'credentials']));
4218
+ return;
4219
+ }
4220
+ if (command === 'valkey' && subcommand === 'status') return handleValkeyStatus(session, flags);
4221
+ if (command === 'valkey' && subcommand === 'connection') return handleValkeyConnection(session, flags);
4222
+ if (command === 'valkey' && subcommand === 'credentials' && positional[2] === 'rotate') return handleValkeyCredentialRotate(session, flags);
4223
+ if (command === 'valkey' && subcommand === 'credentials' && positional[2] === 'status') return handleValkeyCredentialStatus(session, flags);
3559
4224
  if (command === 'project' && subcommand === 'info') return handleProjectInfo(session, flags);
3560
4225
  if (command === 'project' && subcommand === 'url') return handleProjectUrl(session, flags);
3561
4226
  if (command === 'project' && subcommand === 'logs') return handleProjectLogs(session, flags);
4227
+ if (command === 'project' && subcommand === 'metrics' && positional[2] === 'capabilities') return handleProjectMetricsCapabilities(session, flags);
3562
4228
  if (command === 'project' && subcommand === 'metrics') return handleProjectMetrics(session, flags);
3563
4229
  if (command === 'project' && subcommand === 'network') return handleProjectNetwork(session, flags);
3564
4230
  if (command === 'project' && subcommand === 'image' && positional[2] === 'set') return handleProjectImageSet(session, flags);
@@ -0,0 +1,14 @@
1
+ {
2
+ "type_project": "valkey",
3
+ "profile": "cache",
4
+ "version": "9.1.1",
5
+ "network_access": {
6
+ "ingress_white_list": [
7
+ {
8
+ "cidr": "203.0.113.0/24",
9
+ "description": "Application network"
10
+ }
11
+ ],
12
+ "ingress_black_list": []
13
+ }
14
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "type_project": "valkey",
3
+ "profile": "key_value",
4
+ "version": "9.1.1",
5
+ "storage": {
6
+ "persistent": true,
7
+ "capacity": 1
8
+ },
9
+ "network_access": {
10
+ "ingress_white_list": [
11
+ {
12
+ "cidr": "203.0.113.0/24",
13
+ "description": "Application network"
14
+ }
15
+ ],
16
+ "ingress_black_list": []
17
+ }
18
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "type_project": "valkey",
3
+ "profile": "queue",
4
+ "version": "9.1.1",
5
+ "storage": {
6
+ "persistent": true,
7
+ "capacity": 1
8
+ },
9
+ "network_access": {
10
+ "ingress_white_list": [
11
+ {
12
+ "cidr": "203.0.113.0/24",
13
+ "description": "Application network"
14
+ }
15
+ ],
16
+ "ingress_black_list": []
17
+ }
18
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenifra/cli",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Official Zenifra command line interface.",
5
5
  "type": "module",
6
6
  "license": "MIT",