@zenifra/cli 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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,8 +59,16 @@ 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>
@@ -125,7 +134,7 @@ zenifra plans --type database
125
134
  zenifra plans --type storage --json
126
135
  ```
127
136
 
128
- `zenifra plans` funciona sem autenticacao e mostra os catalogos publicos de HTTP, banco e armazenamento.
137
+ `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
138
 
130
139
  Valores de variaveis de ambiente sao mascarados por padrao, inclusive em `--json`.
131
140
  Use `--show-values` apenas quando precisar inspecionar os valores completos.
@@ -193,6 +202,9 @@ Use os arquivos em `examples/` como base para `zenifra create project`:
193
202
  - `examples/http-autoscaling-project.json`: projeto HTTP pago criado com auto-scaling
194
203
  - `examples/postgresql-project.json`: projeto PostgreSQL
195
204
  - `examples/mariadb-project.json`: projeto MariaDB
205
+ - `examples/valkey-key-value-project.json`: projeto Valkey Key Value com armazenamento persistente
206
+ - `examples/valkey-cache-project.json`: projeto Valkey Cache sem armazenamento persistente
207
+ - `examples/valkey-queue-project.json`: projeto Valkey Queue com armazenamento persistente
196
208
 
197
209
  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
210
 
@@ -205,6 +217,7 @@ O wizard atual cobre:
205
217
  - projetos `http` com origem `github` ou `oci`
206
218
  - projetos `postgresql`
207
219
  - projetos `mariadb`
220
+ - projetos `valkey` nos perfis `key_value`, `cache` e `queue`
208
221
 
209
222
  `zenifra create project` nao assume valores default para `--plan` e `--payment-mode`.
210
223
  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 +226,12 @@ Antes de escolher um plano com o usuario, compare os catalogos com `zenifra plan
213
226
  Valores aceitos:
214
227
 
215
228
  - `payment_mode`: `hourly`, `monthly`, `yearly`
216
- - `type_project` no `config`: `http`, `postgresql`, `mariadb`
229
+ - `type_project` no `config`: `http`, `postgresql`, `mariadb`, `valkey`
217
230
  - `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`
231
+ - `plan`: consulte `zenifra plans` para os planos atuais; Valkey usa `db-*` para Key Value, `cache-*` para Cache e `queue-*` para Queue
232
+ - `config.profile` em projetos Valkey: `key_value`, `cache` ou `queue`
233
+ - `config.version` em projetos Valkey: a versão retornada por `zenifra plans --type valkey`
234
+ - `config.storage` em projetos Valkey: obrigatório e persistente para Key Value/Queue; omitido para Cache
219
235
  - `config.github.runtime` (quando houver GitHub em projeto HTTP): `nodejs` ou `python`
220
236
  - `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
237
 
@@ -226,6 +242,9 @@ Observacoes do wizard:
226
242
  - na criacao com auto-scaling, `config.instances` e o minimo inicial e `config.autoscaling.max_instances` e o maximo
227
243
  - em projetos de banco, o wizard nao pergunta `username`, `password` nem `database name`
228
244
  - em projetos de banco, a CLI preenche apenas campos tecnicos minimos exigidos pela validacao atual da API
245
+ - 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
246
+ - 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
247
+ - `valkey credentials rotate` retorna uma operação assíncrona; use `--wait` ou `valkey credentials status` para acompanhar
229
248
 
230
249
  ## Regressao manual de auto-scaling em staging
231
250
 
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,10 +169,14 @@ 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]
@@ -319,16 +339,65 @@ const HELP_SPECS = [
319
339
  },
320
340
  {
321
341
  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'],
342
+ usage: 'zenifra plans [--type <all|http|database|storage|valkey>] [--json]',
343
+ description: 'Lista os catalogos publicos de preco de planos HTTP, banco, armazenamento e Valkey.',
344
+ flags: ['--type <type> Filtra o catalogo: all, http, database, storage, valkey, key-value, cache ou queue.', '--json Imprime a resposta em JSON.'],
345
+ examples: ['zenifra plans', 'zenifra plans --type http', 'zenifra plans --type valkey --json'],
326
346
  output: 'HTTP\nPlano Hora Mes Ano Recursos\nfree R$ 0,00 R$ 0,00 R$ 0,00 1 GB Armazenamento Efemero',
327
347
  jsonOutput: '{"http":[{"plan":"free","prices":{"hourly":0,"monthly":0,"yearly":0},"features":["1 GB Armazenamento Efemero"]}],"database":[],"storage":[]}',
328
348
  },
349
+ {
350
+ command: 'valkey',
351
+ usage: 'zenifra valkey\n zenifra valkey status --project <id> [--json]\n zenifra valkey connection --project <id> [--json]\n zenifra valkey credentials rotate --project <id> [--idempotency-key <key>] [--wait] [--interval <seconds>] [--timeout <seconds>] [--json]\n zenifra valkey credentials status --project <id> --operation <id> [--json]',
352
+ description: 'Consulta projetos Valkey e gerencia suas conexoes e credenciais.',
353
+ examples: ['zenifra valkey status --project proj_1', 'zenifra valkey connection --project proj_1', 'zenifra valkey credentials rotate --project proj_1 --wait'],
354
+ output: 'Zenifra CLI - valkey',
355
+ notes: ['A string de conexao completa aparece somente quando uma criacao ou rotacao a disponibiliza.'],
356
+ },
357
+ {
358
+ command: 'valkey status',
359
+ usage: 'zenifra valkey status --project <id> [--json]',
360
+ description: 'Mostra estado, perfil, versao e persistencia de um projeto Valkey.',
361
+ flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.'],
362
+ examples: ['zenifra valkey status --project proj_1'],
363
+ output: 'Status Perfil Versao Persistente Instancias\nrunning cache 9.1.1 nao 1',
364
+ },
365
+ {
366
+ command: 'valkey connection',
367
+ usage: 'zenifra valkey connection --project <id> [--json]',
368
+ description: 'Mostra os dados de conexao de um projeto Valkey sem revelar a credencial.',
369
+ flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.'],
370
+ examples: ['zenifra valkey connection --project proj_1'],
371
+ output: 'Host Porta TLS Conexao\nvalkey.example.com 6380 sim valkeys://default:********@valkey.example.com:6380/0',
372
+ notes: ['Use a credencial recebida na criacao ou em uma rotacao concluida.'],
373
+ },
374
+ {
375
+ command: 'valkey credentials',
376
+ usage: 'zenifra valkey credentials\n zenifra valkey credentials rotate --project <id> [--idempotency-key <key>] [--wait] [--interval <seconds>] [--timeout <seconds>] [--json]\n zenifra valkey credentials status --project <id> --operation <id> [--json]',
377
+ description: 'Agrupa comandos de renovacao e acompanhamento de credenciais Valkey.',
378
+ examples: ['zenifra valkey credentials rotate --project proj_1', 'zenifra valkey credentials status --project proj_1 --operation operation_1'],
379
+ output: 'Zenifra CLI - valkey credentials',
380
+ },
381
+ {
382
+ command: 'valkey credentials rotate',
383
+ usage: 'zenifra valkey credentials rotate --project <id> [--idempotency-key <key>] [--wait] [--interval <seconds>] [--timeout <seconds>] [--json]',
384
+ description: 'Solicita a renovacao da credencial e retorna uma operacao acompanhavel.',
385
+ flags: ['--project <id> ID do projeto.', '--idempotency-key <key> Chave para repetir a mesma solicitacao.', '--wait Aguarda a conclusao.', '--interval <seconds> Intervalo do polling. Padrao: 2.', '--timeout <seconds> Timeout total. Padrao: 900.', '--json Imprime a resposta em JSON.'],
386
+ examples: ['zenifra valkey credentials rotate --project proj_1', 'zenifra valkey credentials rotate --project proj_1 --wait --timeout 120'],
387
+ output: 'Operacao aceita: rotation_123\nEstado: accepted',
388
+ notes: ['Salve a nova string de conexao quando a operacao atingir completed.'],
389
+ },
390
+ {
391
+ command: 'valkey credentials status',
392
+ usage: 'zenifra valkey credentials status --project <id> --operation <id> [--json]',
393
+ description: 'Consulta o estado de uma renovacao de credencial Valkey.',
394
+ flags: ['--project <id> ID do projeto.', '--operation <id> ID da operacao.', '--json Imprime a resposta em JSON.'],
395
+ examples: ['zenifra valkey credentials status --project proj_1 --operation rotation_123'],
396
+ output: 'Operacao: rotation_123\nEstado: completed',
397
+ },
329
398
  {
330
399
  command: 'projects',
331
- usage: 'zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb>] [--page <n>] [--limit <n>]',
400
+ usage: 'zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb|valkey>] [--page <n>] [--limit <n>]',
332
401
  description: 'Lista projetos da organizacao ativa ou da API key.',
333
402
  flags: ['--json Imprime a resposta em JSON.', '--org <id> Usa uma organizacao especifica com sessao de usuario.', '--type <type> Filtra por tipo de projeto.', '--page <n> Pagina. Padrao: 1.', '--limit <n> Itens por pagina. Padrao: 15.'],
334
403
  examples: ['zenifra projects --type http --page 1 --limit 15', 'zenifra projects --json'],
@@ -337,9 +406,9 @@ const HELP_SPECS = [
337
406
  },
338
407
  {
339
408
  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]',
409
+ usage: 'zenifra create project\n zenifra create project --name <name> --plan <plan> --payment-mode <mode> --config <json|@file> [--description <text>] [--idempotency-key <key>] [--org <id>] [--json]',
341
410
  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.'],
411
+ flags: ['--name <name> Nome do projeto.', '--plan <plan> Plano do projeto.', '--payment-mode <mode> Modo de pagamento.', '--config <json|@file> JSON inline ou arquivo.', '--description <text> Descricao opcional.', '--idempotency-key <key> Chave para repetir a mesma criacao com seguranca.', '--json Imprime a resposta em JSON.'],
343
412
  examples: ['zenifra create project', 'zenifra create project --name api-web --plan free --payment-mode hourly --config @examples/http-project.json'],
344
413
  output: 'Campo Valor\n------- --------------------------------------\nProjeto 507f1f77bcf86cd799439012\nDominio https://api-web.client.zenifra.com',
345
414
  jsonOutput: '{"status":"success","data":{"id":"507f1f77bcf86cd799439012","name":"api-web"}}',
@@ -654,7 +723,7 @@ function removedProjectsCreateMessage() {
654
723
  }
655
724
 
656
725
  function isNamespaceCommand(command) {
657
- return ['auth', 'profile', 'project', 'org'].includes(command);
726
+ return ['auth', 'profile', 'project', 'org', 'valkey'].includes(command);
658
727
  }
659
728
 
660
729
  function parseArgs(argv) {
@@ -1029,6 +1098,11 @@ function estimateWizardTotal(state = {}) {
1029
1098
  return total;
1030
1099
  }
1031
1100
 
1101
+ if (typeProject === 'valkey') {
1102
+ total += 3;
1103
+ return total;
1104
+ }
1105
+
1032
1106
  return 18;
1033
1107
  }
1034
1108
 
@@ -1346,6 +1420,7 @@ async function request(session, flags, method, path, {
1346
1420
  orgId,
1347
1421
  tokenRequired = true,
1348
1422
  credential: credentialOverride,
1423
+ headers: extraHeaders = {},
1349
1424
  } = {}) {
1350
1425
  const credential = credentialOverride || resolveCredential(session);
1351
1426
  if (tokenRequired && !credential) {
@@ -1356,6 +1431,14 @@ async function request(session, flags, method, path, {
1356
1431
  if (body !== undefined) {
1357
1432
  headers['Content-Type'] = 'application/json';
1358
1433
  }
1434
+ for (const [name, value] of Object.entries(extraHeaders)) {
1435
+ if (value === undefined) continue;
1436
+ const normalizedName = name.toLowerCase();
1437
+ if (['authorization', 'x-organization-id', 'content-type'].includes(normalizedName)) {
1438
+ throw new CliError(`Header nao permitido: ${name}.`);
1439
+ }
1440
+ headers[name] = String(value);
1441
+ }
1359
1442
  if (credential?.token) {
1360
1443
  headers.Authorization = `Bearer ${credential.token}`;
1361
1444
  }
@@ -1508,20 +1591,33 @@ function printEnvs(envs, flags) {
1508
1591
  }
1509
1592
 
1510
1593
  function printProject(project) {
1511
- const url = projectUrlOf(project);
1594
+ const managedService = project.managed_service;
1595
+ const isValkey = project.type_project === 'valkey' || managedService?.engine === 'valkey';
1512
1596
  const rows = [
1513
1597
  { label: 'ID', value: projectIdOf(project) || '-' },
1514
1598
  { label: 'Nome', value: project.name || '-' },
1515
1599
  { label: 'Status', value: project.status || '-' },
1516
1600
  { label: 'Tipo', value: project.type_project || '-' },
1517
- { label: 'Exposicao', value: project.exposure || '-' },
1518
1601
  { 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
1602
  ];
1524
1603
 
1604
+ if (isValkey) {
1605
+ rows.push(
1606
+ { label: 'Perfil', value: managedService?.profile || '-' },
1607
+ { label: 'Versao', value: managedService?.version || '-' },
1608
+ { label: 'Persistente', value: managedService?.topology?.persistent === true ? 'sim' : 'nao' },
1609
+ { label: 'Instancias', value: project.instances ?? '-' },
1610
+ );
1611
+ } else {
1612
+ rows.push(
1613
+ { label: 'Exposicao', value: project.exposure || '-' },
1614
+ { label: 'URL', value: projectUrlOf(project) || '-' },
1615
+ { label: 'Imagem', value: project.image || '-' },
1616
+ { label: 'Instancias', value: project.instances ?? '-' },
1617
+ { label: 'Instancias atuais', value: project.additional_info?.current_instances ?? '-' },
1618
+ );
1619
+ }
1620
+
1525
1621
  for (const row of rows) {
1526
1622
  process.stdout.write(`${row.label}: ${row.value}\n`);
1527
1623
  }
@@ -1937,6 +2033,26 @@ function normalizePlan(value) {
1937
2033
  ['dbpremium', 'db-premium'],
1938
2034
  ['db_enterprise', 'db-enterprise'],
1939
2035
  ['dbenterprise', 'db-enterprise'],
2036
+ ['cache_free', 'cache-free'],
2037
+ ['cachefree', 'cache-free'],
2038
+ ['cache_starter', 'cache-starter'],
2039
+ ['cachestarter', 'cache-starter'],
2040
+ ['cache_basic', 'cache-basic'],
2041
+ ['cachebasic', 'cache-basic'],
2042
+ ['cache_premium', 'cache-premium'],
2043
+ ['cachepremium', 'cache-premium'],
2044
+ ['cache_enterprise', 'cache-enterprise'],
2045
+ ['cacheenterprise', 'cache-enterprise'],
2046
+ ['queue_free', 'queue-free'],
2047
+ ['queuefree', 'queue-free'],
2048
+ ['queue_starter', 'queue-starter'],
2049
+ ['queuestarter', 'queue-starter'],
2050
+ ['queue_basic', 'queue-basic'],
2051
+ ['queuebasic', 'queue-basic'],
2052
+ ['queue_premium', 'queue-premium'],
2053
+ ['queuepremium', 'queue-premium'],
2054
+ ['queue_enterprise', 'queue-enterprise'],
2055
+ ['queueenterprise', 'queue-enterprise'],
1940
2056
  ]);
1941
2057
 
1942
2058
  if (aliases.has(normalized)) return aliases.get(normalized);
@@ -1973,6 +2089,21 @@ function normalizeTypeProject(value) {
1973
2089
  ['postgresql', 'postgresql'],
1974
2090
  ['mariadb', 'mariadb'],
1975
2091
  ['maria db', 'mariadb'],
2092
+ ['valkey', 'valkey'],
2093
+ ['key value', 'valkey'],
2094
+ ['key-value', 'valkey'],
2095
+ ]);
2096
+ return aliases.get(raw) || raw;
2097
+ }
2098
+
2099
+ function normalizeValkeyProfile(value) {
2100
+ const raw = normalizeFreeText(value);
2101
+ const aliases = new Map([
2102
+ ['key_value', 'key_value'],
2103
+ ['key value', 'key_value'],
2104
+ ['key-value', 'key_value'],
2105
+ ['cache', 'cache'],
2106
+ ['queue', 'queue'],
1976
2107
  ]);
1977
2108
  return aliases.get(raw) || raw;
1978
2109
  }
@@ -2075,7 +2206,6 @@ async function fetchWizardCatalogs(session, flags) {
2075
2206
  unwrapData(await request(session, flags, 'GET', '/project/database/plans')),
2076
2207
  'os planos de banco',
2077
2208
  );
2078
-
2079
2209
  return { httpPlans, availableInstances, databasePlans };
2080
2210
  }
2081
2211
 
@@ -2084,6 +2214,10 @@ function normalizePlansCatalogType(value) {
2084
2214
  if (!normalized || normalized === 'all') return 'all';
2085
2215
  if (normalized === 'http') return 'http';
2086
2216
  if (['database', 'db', 'postgresql', 'mariadb'].includes(normalized)) return 'database';
2217
+ if (normalized === 'valkey') return 'valkey';
2218
+ if (normalized === 'key-value' || normalized === 'key value' || normalized === 'key_value') return 'valkey-key_value';
2219
+ if (normalized === 'cache') return 'valkey-cache';
2220
+ if (normalized === 'queue') return 'valkey-queue';
2087
2221
  if (normalized === 'storage') return 'storage';
2088
2222
  return null;
2089
2223
  }
@@ -2116,6 +2250,49 @@ function humanizeStorageName(value) {
2116
2250
  return { storage: raw, type: '-' };
2117
2251
  }
2118
2252
 
2253
+ const VALKEY_PROFILE_LABELS = {
2254
+ key_value: 'Key Value',
2255
+ cache: 'Cache',
2256
+ queue: 'Queue',
2257
+ };
2258
+
2259
+ function requireValkeyCatalog(value) {
2260
+ if (!value || typeof value !== 'object' || Array.isArray(value)
2261
+ || value.engine !== 'valkey'
2262
+ || typeof value.version !== 'string'
2263
+ || typeof value.currency !== 'string'
2264
+ || !Array.isArray(value.profiles)
2265
+ || !Array.isArray(value.plans)
2266
+ || value.profiles.length === 0
2267
+ || value.plans.length === 0) {
2268
+ throw new CliError('Falha ao carregar o catalogo Valkey. Tente novamente mais tarde.');
2269
+ }
2270
+
2271
+ const validProfiles = new Set(Object.keys(VALKEY_PROFILE_LABELS));
2272
+ if (value.profiles.some((profile) => !validProfiles.has(profile?.id) || typeof profile.persistence !== 'boolean')) {
2273
+ throw new CliError('Falha ao carregar o catalogo Valkey. Tente novamente mais tarde.');
2274
+ }
2275
+ if (value.plans.some((plan) => !validProfiles.has(plan?.profile) || typeof plan.id !== 'string' || !plan.prices || typeof plan.prices !== 'object')) {
2276
+ throw new CliError('Falha ao carregar o catalogo Valkey. Tente novamente mais tarde.');
2277
+ }
2278
+
2279
+ return value;
2280
+ }
2281
+
2282
+ function valkeyProfileFilter(type) {
2283
+ if (!type.startsWith('valkey-')) return null;
2284
+ return type.slice('valkey-'.length);
2285
+ }
2286
+
2287
+ function filterValkeyCatalog(catalog, profile) {
2288
+ if (!profile) return catalog;
2289
+ return {
2290
+ ...catalog,
2291
+ profiles: catalog.profiles.filter((entry) => entry.id === profile),
2292
+ plans: catalog.plans.filter((plan) => plan.profile === profile),
2293
+ };
2294
+ }
2295
+
2119
2296
  async function fetchPlansCatalogs(session, flags, type) {
2120
2297
  if (type === 'http') {
2121
2298
  return {
@@ -2150,19 +2327,51 @@ async function fetchPlansCatalogs(session, flags, type) {
2150
2327
  };
2151
2328
  }
2152
2329
 
2153
- const [httpPayload, databasePayload, storagePayload] = await Promise.all([
2330
+ if (type === 'valkey' || type.startsWith('valkey-')) {
2331
+ const catalog = requireValkeyCatalog(
2332
+ unwrapData(await request(session, flags, 'GET', '/managed-services/catalog', { tokenRequired: false })),
2333
+ );
2334
+ return {
2335
+ http: [],
2336
+ database: [],
2337
+ storage: [],
2338
+ valkey: filterValkeyCatalog(catalog, valkeyProfileFilter(type)),
2339
+ };
2340
+ }
2341
+
2342
+ const [httpPayload, databasePayload, storagePayload, valkeyPayload] = await Promise.all([
2154
2343
  request(session, flags, 'GET', '/project/plans', { tokenRequired: false }),
2155
2344
  request(session, flags, 'GET', '/project/database/plans', { tokenRequired: false }),
2156
2345
  request(session, flags, 'GET', '/project/storage/plans', { tokenRequired: false }),
2346
+ request(session, flags, 'GET', '/managed-services/catalog', { tokenRequired: false }),
2157
2347
  ]);
2158
2348
 
2159
2349
  return {
2160
2350
  http: requireWizardCatalogArray(unwrapData(httpPayload), 'os planos HTTP'),
2161
2351
  database: requireWizardCatalogArray(unwrapData(databasePayload), 'os planos de banco'),
2162
2352
  storage: requireWizardCatalogArray(unwrapData(storagePayload), 'os planos de storage'),
2353
+ valkey: requireValkeyCatalog(unwrapData(valkeyPayload)),
2163
2354
  };
2164
2355
  }
2165
2356
 
2357
+ function printValkeyCatalog(catalog, profileFilter) {
2358
+ const profiles = profileFilter ? [profileFilter] : Object.keys(VALKEY_PROFILE_LABELS);
2359
+ profiles.forEach((profile, index) => {
2360
+ if (index > 0) process.stdout.write('\n');
2361
+ process.stdout.write(`${VALKEY_PROFILE_LABELS[profile]}\n`);
2362
+ const plans = catalog.plans.filter((plan) => plan.profile === profile);
2363
+ printTable(plans, [
2364
+ { label: 'Plano', value: (plan) => plan.id || '-' },
2365
+ { label: 'Hora', value: (plan) => formatBrl(plan.prices?.hourly, catalog.currency) },
2366
+ { label: 'Mes', value: (plan) => formatBrl(plan.prices?.monthly, catalog.currency) },
2367
+ { label: 'Ano', value: (plan) => formatBrl(plan.prices?.yearly, catalog.currency) },
2368
+ { label: 'Capacidade', value: (plan) => `${plan.resources?.cpu || '-'} / ${plan.resources?.memory || '-'}` },
2369
+ { label: 'Storage incluído', value: (plan) => `${plan.included_storage_gb ?? 0} GB` },
2370
+ { label: 'Alta disponibilidade', value: (plan) => plan.high_availability?.[profile] ? 'sim' : 'nao' },
2371
+ ]);
2372
+ });
2373
+ }
2374
+
2166
2375
  function printPlansCatalogs(payload, type) {
2167
2376
  if (type === 'all' || type === 'http') {
2168
2377
  process.stdout.write('HTTP\n');
@@ -2198,12 +2407,17 @@ function printPlansCatalogs(payload, type) {
2198
2407
  { label: 'Ano', value: (storage) => formatBrl(storage.prices?.yearly) },
2199
2408
  ]);
2200
2409
  }
2410
+
2411
+ if (type === 'all' || type === 'valkey' || type.startsWith('valkey-')) {
2412
+ if (type === 'all') process.stdout.write('\n');
2413
+ printValkeyCatalog(payload.valkey, valkeyProfileFilter(type));
2414
+ }
2201
2415
  }
2202
2416
 
2203
2417
  async function handlePlans(session, flags) {
2204
2418
  const type = normalizePlansCatalogType(flags.type);
2205
2419
  if (!type) {
2206
- throw new CliError('Tipo de catalogo invalido. Valores aceitos: all, http, database, storage.');
2420
+ throw new CliError('Tipo de catalogo invalido. Valores aceitos: all, http, database, storage, valkey.');
2207
2421
  }
2208
2422
 
2209
2423
  const payload = await fetchPlansCatalogs(session, flags, type);
@@ -2261,9 +2475,53 @@ function validateCreateAutoscaling(config, plan, typeProject) {
2261
2475
  return { ...config, autoscaling: nextAutoscaling };
2262
2476
  }
2263
2477
 
2264
- function validateCreateInput({ plan, paymentMode, config }) {
2478
+ function validateValkeyConfig(config, plan, catalog) {
2479
+ const valkeyCatalog = requireValkeyCatalog(catalog);
2480
+ const profile = normalizeValkeyProfile(config?.profile);
2481
+ const profileDefinition = valkeyCatalog.profiles.find((entry) => entry.id === profile);
2482
+ if (!profileDefinition) {
2483
+ throw new CliError('config.profile invalido. Use key_value, cache ou queue.');
2484
+ }
2485
+
2486
+ const planDefinition = valkeyCatalog.plans.find((entry) => entry.id === plan && entry.profile === profile);
2487
+ if (!planDefinition) {
2488
+ throw new CliError(`Plano ${plan} nao pode ser usado com o perfil ${profile}. Consulte "zenifra plans --type valkey".`);
2489
+ }
2490
+
2491
+ if (config.version !== valkeyCatalog.version) {
2492
+ throw new CliError(`config.version invalida: use ${valkeyCatalog.version}, conforme o catalogo Valkey.`);
2493
+ }
2494
+
2495
+ const forbiddenFields = ['instances', 'envs', 'image', 'github', 'port', 'exposure', 'autoscaling'];
2496
+ const forbiddenField = forbiddenFields.find((field) => config[field] !== undefined);
2497
+ if (forbiddenField) {
2498
+ throw new CliError(`Valkey nao aceita config.${forbiddenField}. A capacidade e definida pelo plano.`);
2499
+ }
2500
+
2501
+ if (profileDefinition.persistence) {
2502
+ if (!isRecord(config.storage) || config.storage.persistent !== true) {
2503
+ throw new CliError('Perfis Valkey persistentes exigem storage.persistent=true.');
2504
+ }
2505
+ const capacity = Number(config.storage.capacity);
2506
+ if (!Number.isInteger(capacity) || capacity < 1 || capacity > 250) {
2507
+ throw new CliError('storage.capacity deve ser um inteiro entre 1 e 250 GiB.');
2508
+ }
2509
+ } else if (config.storage !== undefined) {
2510
+ throw new CliError('O perfil Cache nao aceita storage.');
2511
+ }
2512
+
2513
+ return {
2514
+ ...config,
2515
+ type_project: 'valkey',
2516
+ profile,
2517
+ version: valkeyCatalog.version,
2518
+ };
2519
+ }
2520
+
2521
+ function validateCreateInput({ plan, paymentMode, config, valkeyCatalog }) {
2522
+ const nextTypeProject = normalizeTypeProject(config?.type_project);
2265
2523
  const nextPlan = normalizePlan(plan);
2266
- if (!nextPlan || !ALLOWED_PLAN_VALUES.has(nextPlan)) {
2524
+ if (!nextPlan || (nextTypeProject !== 'valkey' && !ALLOWED_PLAN_VALUES.has(nextPlan))) {
2267
2525
  throw new CliError(`Plano invalido: "${plan}". Valores aceitos: ${formatAllowedValues(ALLOWED_PLAN_VALUES)}. Docs: ${DOCS_CREATE_HTTP_URL}`);
2268
2526
  }
2269
2527
 
@@ -2272,12 +2530,19 @@ function validateCreateInput({ plan, paymentMode, config }) {
2272
2530
  throw new CliError(`Modo de pagamento invalido: "${paymentMode}". Valores aceitos: ${formatAllowedValues(ALLOWED_PAYMENT_MODE_VALUES)}. Docs: ${DOCS_PAYMENTS_URL}`);
2273
2531
  }
2274
2532
 
2275
- const nextTypeProject = normalizeTypeProject(config?.type_project);
2276
2533
  if (!nextTypeProject || !ALLOWED_TYPE_PROJECT_VALUES.has(nextTypeProject)) {
2277
2534
  throw new CliError(`type_project invalido: "${config?.type_project}". Valores aceitos: ${formatAllowedValues(ALLOWED_TYPE_PROJECT_VALUES)}. Docs: ${DOCS_CREATE_HTTP_URL}`);
2278
2535
  }
2279
2536
 
2280
2537
  const nextConfig = { ...config, type_project: nextTypeProject };
2538
+ if (nextTypeProject === 'valkey') {
2539
+ return {
2540
+ plan: nextPlan,
2541
+ paymentMode: nextPaymentMode,
2542
+ config: validateValkeyConfig(nextConfig, nextPlan, valkeyCatalog),
2543
+ };
2544
+ }
2545
+
2281
2546
  if (nextTypeProject === 'http' && nextConfig.github) {
2282
2547
  const nextRuntime = normalizeRuntime(nextConfig.github.runtime);
2283
2548
  if (!nextRuntime || !ALLOWED_RUNTIME_VALUES.has(nextRuntime)) {
@@ -2344,6 +2609,10 @@ function printWizardSummary(payload) {
2344
2609
  lines.push(` banco: versao ${masked.config.version} | instancias ${masked.config.instances} | storage ${masked.config.storage.capacity}Gi`);
2345
2610
  }
2346
2611
 
2612
+ if (masked.config.type_project === 'valkey') {
2613
+ lines.push(` Valkey: perfil ${masked.config.profile} | versao ${masked.config.version}${masked.config.storage ? ` | storage ${masked.config.storage.capacity}Gi persistente` : ' | sem storage persistente'}`);
2614
+ }
2615
+
2347
2616
  process.stdout.write(`${lines.join('\n')}\n`);
2348
2617
  }
2349
2618
 
@@ -2735,6 +3004,65 @@ async function buildMariadbConfig() {
2735
3004
  };
2736
3005
  }
2737
3006
 
3007
+ async function buildValkeyConfig({ profile, plan, catalog }) {
3008
+ const planDefinition = catalog.plans.find((entry) => entry.id === plan && entry.profile === profile);
3009
+ const profileDefinition = catalog.profiles.find((entry) => entry.id === profile);
3010
+ if (!planDefinition || !profileDefinition) {
3011
+ throw new CliError('O catalogo Valkey nao possui o plano ou perfil selecionado. Tente novamente.');
3012
+ }
3013
+
3014
+ let storage;
3015
+ if (profileDefinition.persistence) {
3016
+ storage = {
3017
+ persistent: true,
3018
+ capacity: await promptWizardNumber({
3019
+ label: `Capacidade de armazenamento persistente (Gi, inclui ${planDefinition.included_storage_gb ?? 0} Gi)`,
3020
+ docs: DOCS_CREATE_VALKEY_URL,
3021
+ examples: [String(Math.max(1, planDefinition.included_storage_gb ?? 1)), '10', '20'],
3022
+ min: 1,
3023
+ max: 250,
3024
+ }),
3025
+ };
3026
+ }
3027
+
3028
+ const whitelist = await promptWizardPairs({
3029
+ introLabel: 'Deseja configurar acesso de rede para este projeto?',
3030
+ docs: DOCS_CREATE_VALKEY_URL,
3031
+ examples: ['sim', 'nao'],
3032
+ stateKey: 'valkeyNetworkAccess',
3033
+ itemLabels: [
3034
+ { key: 'cidr', label: 'CIDR permitido', required: true, examples: ['203.0.113.0/24'], docs: DOCS_CREATE_VALKEY_URL },
3035
+ { key: 'description', label: 'Descricao do acesso', required: true, examples: ['Aplicacao'], docs: DOCS_CREATE_VALKEY_URL },
3036
+ ],
3037
+ });
3038
+
3039
+ let blacklist = [];
3040
+ if (whitelist.length > 0) {
3041
+ blacklist = await promptWizardPairs({
3042
+ introLabel: 'Deseja adicionar bloqueios de rede?',
3043
+ docs: DOCS_CREATE_VALKEY_URL,
3044
+ examples: ['sim', 'nao'],
3045
+ itemLabels: [
3046
+ { key: 'cidr', label: 'CIDR bloqueado', required: true, examples: ['198.51.100.0/24'], docs: DOCS_CREATE_VALKEY_URL },
3047
+ { key: 'description', label: 'Descricao do bloqueio', required: true, examples: ['Bloqueio'], docs: DOCS_CREATE_VALKEY_URL },
3048
+ ],
3049
+ });
3050
+ }
3051
+
3052
+ return {
3053
+ type_project: 'valkey',
3054
+ profile,
3055
+ version: catalog.version,
3056
+ ...(storage ? { storage } : {}),
3057
+ ...(whitelist.length > 0 ? {
3058
+ network_access: {
3059
+ ingress_white_list: whitelist,
3060
+ ingress_black_list: blacklist,
3061
+ },
3062
+ } : {}),
3063
+ };
3064
+ }
3065
+
2738
3066
  async function runProjectCreateWizard(session, flags) {
2739
3067
  const catalogs = await fetchWizardCatalogs(session, flags);
2740
3068
  const ui = createWizardUi();
@@ -2765,16 +3093,34 @@ async function runProjectCreateWizard(session, flags) {
2765
3093
  options: WIZARD_TYPE_PROJECT_OPTIONS.map((value) => ({ value })),
2766
3094
  });
2767
3095
  ui.state.typeProject = typeProject;
3096
+ const valkeyCatalog = typeProject === 'valkey'
3097
+ ? requireValkeyCatalog(unwrapData(await request(session, flags, 'GET', '/managed-services/catalog')))
3098
+ : undefined;
3099
+ const profile = typeProject === 'valkey'
3100
+ ? await promptWizardSelect({
3101
+ label: 'Perfil Valkey',
3102
+ docs: DOCS_CREATE_VALKEY_URL,
3103
+ examples: WIZARD_VALKEY_PROFILE_OPTIONS,
3104
+ options: WIZARD_VALKEY_PROFILE_OPTIONS.map((value) => ({ value })),
3105
+ })
3106
+ : undefined;
3107
+ ui.state.valkeyProfile = profile;
2768
3108
  const planOptions = typeProject === 'http'
2769
3109
  ? sortWizardOptions(catalogs.httpPlans
2770
3110
  .map((plan) => plan?.plan)
2771
3111
  .filter((plan) => WIZARD_HTTP_PLAN_OPTIONS.includes(plan) && Number(catalogs.availableInstances?.[plan] || 0) > 0), WIZARD_HTTP_PLAN_OPTIONS)
3112
+ : typeProject === 'valkey'
3113
+ ? valkeyCatalog.plans
3114
+ .filter((planDefinition) => planDefinition.profile === profile)
3115
+ .map((planDefinition) => planDefinition.id)
2772
3116
  : sortWizardOptions(catalogs.databasePlans
2773
3117
  .map((plan) => plan?.plan)
2774
3118
  .filter((plan) => WIZARD_DATABASE_PLAN_OPTIONS.includes(plan) && (typeProject !== 'mariadb' || plan !== 'db-free')), WIZARD_DATABASE_PLAN_OPTIONS);
2775
3119
  const plan = await promptWizardSelect({
2776
3120
  label: 'Plano',
2777
- docs: typeProject === 'http' ? DOCS_CREATE_HTTP_URL : DOCS_DATABASE_CONFIGURATION_URL,
3121
+ docs: typeProject === 'http'
3122
+ ? DOCS_CREATE_HTTP_URL
3123
+ : typeProject === 'valkey' ? DOCS_CREATE_VALKEY_URL : DOCS_DATABASE_CONFIGURATION_URL,
2778
3124
  examples: [planOptions[0], planOptions.at(-1)],
2779
3125
  options: planOptions.map((value) => ({ value })),
2780
3126
  });
@@ -2800,6 +3146,8 @@ async function runProjectCreateWizard(session, flags) {
2800
3146
  });
2801
3147
  } else if (typeProject === 'postgresql') {
2802
3148
  config = await buildPostgresqlConfig({ plan, databasePlans: catalogs.databasePlans });
3149
+ } else if (typeProject === 'valkey') {
3150
+ config = await buildValkeyConfig({ profile, plan, catalog: valkeyCatalog });
2803
3151
  } else {
2804
3152
  config = await buildMariadbConfig();
2805
3153
  }
@@ -2815,7 +3163,9 @@ async function runProjectCreateWizard(session, flags) {
2815
3163
  printWizardSummary(payload);
2816
3164
  const confirmed = await promptWizardBoolean({
2817
3165
  label: 'Confirmar criacao do projeto',
2818
- docs: typeProject === 'http' ? DOCS_CREATE_HTTP_URL : DOCS_DATABASE_CONFIGURATION_URL,
3166
+ docs: typeProject === 'http'
3167
+ ? DOCS_CREATE_HTTP_URL
3168
+ : typeProject === 'valkey' ? DOCS_CREATE_VALKEY_URL : DOCS_DATABASE_CONFIGURATION_URL,
2819
3169
  examples: ['sim', 'nao'],
2820
3170
  });
2821
3171
  if (!confirmed) return null;
@@ -2836,10 +3186,18 @@ async function handleProjectCreate(session, flags) {
2836
3186
  const plan = wizardPayload?.plan || flags.plan || await prompt('Plano');
2837
3187
  const paymentMode = wizardPayload?.payment_mode || flags.paymentMode || await prompt('Modo de pagamento');
2838
3188
  const config = wizardPayload?.config || await parseConfig(flags.config || await prompt('Config JSON ou @arquivo'));
2839
- const validated = validateCreateInput({ plan, paymentMode, config });
3189
+ const typeProject = normalizeTypeProject(config?.type_project);
3190
+ const valkeyCatalog = typeProject === 'valkey'
3191
+ ? requireValkeyCatalog(unwrapData(await request(session, flags, 'GET', '/managed-services/catalog')))
3192
+ : undefined;
3193
+ if (flags.idempotencyKey !== undefined && !/^[A-Za-z0-9._-]{16,200}$/.test(String(flags.idempotencyKey))) {
3194
+ throw new CliError('--idempotency-key deve ter entre 16 e 200 caracteres: letras, numeros, ponto, underscore ou hifen.');
3195
+ }
3196
+ const validated = validateCreateInput({ plan, paymentMode, config, valkeyCatalog });
2840
3197
 
2841
3198
  const payload = await request(session, flags, 'POST', '/project', {
2842
3199
  orgId,
3200
+ headers: flags.idempotencyKey === undefined ? {} : { 'Idempotency-Key': String(flags.idempotencyKey) },
2843
3201
  body: {
2844
3202
  name,
2845
3203
  ...(description ? { description: String(description) } : {}),
@@ -2859,6 +3217,21 @@ async function handleProjectCreate(session, flags) {
2859
3217
  ...(project.domain ? [{ field: 'Dominio', value: formatPublicUrl(project.domain) }] : []),
2860
3218
  ...(project.api_key ? [{ field: 'API key', value: project.api_key }] : []),
2861
3219
  ];
3220
+ if (validated.config.type_project === 'valkey') {
3221
+ const service = project.managed_service || {};
3222
+ rows.push(
3223
+ { field: 'Perfil', value: service.profile || validated.config.profile },
3224
+ { field: 'Host', value: service.host || '-' },
3225
+ { field: 'Porta', value: service.port ?? '-' },
3226
+ { field: 'TLS', value: service.tls === true ? 'sim' : 'nao' },
3227
+ );
3228
+ if (service.connection_string) {
3229
+ rows.push({ field: 'String de conexao', value: service.connection_string });
3230
+ process.stdout.write('Salve a string de conexao em um local seguro; ela pode nao ser exibida novamente.\n');
3231
+ } else {
3232
+ rows.push({ field: 'Credencial', value: 'nao retornada nesta resposta; use valkey credentials rotate' });
3233
+ }
3234
+ }
2862
3235
  process.stdout.write('\n');
2863
3236
  printTable(rows, [
2864
3237
  { label: 'Campo', value: (row) => row.field },
@@ -3330,6 +3703,131 @@ async function handleProjectBillingUsage(session, flags) {
3330
3703
  printBillingUsage(data);
3331
3704
  }
3332
3705
 
3706
+ function printValkeyStatus(data) {
3707
+ printTable([data], [
3708
+ { label: 'Status', value: (service) => service.status || '-' },
3709
+ { label: 'Perfil', value: (service) => service.profile || '-' },
3710
+ { label: 'Versao', value: (service) => service.version || '-' },
3711
+ { label: 'Persistente', value: (service) => service.persistence === true ? 'sim' : 'nao' },
3712
+ { label: 'Instancias', value: (service) => service.instances ?? '-' },
3713
+ ]);
3714
+ }
3715
+
3716
+ function printValkeyConnection(data) {
3717
+ printTable([data], [
3718
+ { label: 'Host', value: (service) => service.host || '-' },
3719
+ { label: 'Porta', value: (service) => service.port ?? '-' },
3720
+ { label: 'Usuario', value: (service) => service.username || '-' },
3721
+ { label: 'TLS', value: (service) => service.tls === true ? 'sim' : 'nao' },
3722
+ { label: 'Conexao', value: (service) => service.connection_string || '-' },
3723
+ ]);
3724
+ }
3725
+
3726
+ function printValkeyRotation(data, { accepted = false } = {}) {
3727
+ const rows = [
3728
+ { field: accepted ? 'Operacao aceita' : 'Operacao', value: data.operation_id || '-' },
3729
+ { field: 'Estado', value: data.state || '-' },
3730
+ ];
3731
+ if (data.connection_string) {
3732
+ rows.push({ field: 'String de conexao', value: data.connection_string });
3733
+ process.stdout.write('Salve a nova string de conexao em um local seguro; ela pode nao ser exibida novamente.\n');
3734
+ }
3735
+ printTable(rows, [
3736
+ { label: 'Campo', value: (row) => row.field },
3737
+ { label: 'Valor', value: (row) => row.value },
3738
+ ]);
3739
+ }
3740
+
3741
+ async function waitForValkeyRotation(session, flags, projectId, operationId, orgId) {
3742
+ const intervalMs = parseSecondsOption(flags.interval, '--interval', { defaultValue: 2, min: 0.1 }) * 1000;
3743
+ const timeoutMs = parseSecondsOption(flags.timeout, '--timeout', { defaultValue: 900, min: 1 }) * 1000;
3744
+ const startedAt = Date.now();
3745
+ let lastState = 'accepted';
3746
+
3747
+ while (true) {
3748
+ const data = unwrapData(await request(
3749
+ session,
3750
+ flags,
3751
+ 'GET',
3752
+ `/managed-services/${projectId}/credential-rotations/${operationId}`,
3753
+ { orgId },
3754
+ ));
3755
+ lastState = data?.state || lastState;
3756
+ if (lastState === 'completed') return data;
3757
+ if (['failed', 'cancelled', 'canceled'].includes(lastState)) {
3758
+ throw new CliError(`A renovacao de credencial terminou com estado ${lastState}. Operacao: ${operationId}.`);
3759
+ }
3760
+ if (Date.now() - startedAt >= timeoutMs) {
3761
+ throw new CliError(`Timeout aguardando a renovacao ${operationId}. Ultimo estado: ${lastState}.`);
3762
+ }
3763
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, intervalMs));
3764
+ }
3765
+ }
3766
+
3767
+ async function handleValkeyStatus(session, flags) {
3768
+ const projectId = requireProjectId(flags, 'valkey status');
3769
+ if (!projectId) return;
3770
+ const orgId = await resolveOrgId(session, flags);
3771
+ const data = unwrapData(await request(session, flags, 'GET', `/managed-services/${projectId}/status`, { orgId }));
3772
+ if (flags.json) return printJson(data);
3773
+ printValkeyStatus(data);
3774
+ }
3775
+
3776
+ async function handleValkeyConnection(session, flags) {
3777
+ const projectId = requireProjectId(flags, 'valkey connection');
3778
+ if (!projectId) return;
3779
+ const orgId = await resolveOrgId(session, flags);
3780
+ const data = unwrapData(await request(session, flags, 'GET', `/managed-services/${projectId}/connection`, { orgId }));
3781
+ if (flags.json) return printJson(data);
3782
+ printValkeyConnection(data);
3783
+ }
3784
+
3785
+ async function handleValkeyCredentialStatus(session, flags) {
3786
+ const projectId = requireProjectId(flags, 'valkey credentials status');
3787
+ if (!projectId) return;
3788
+ if (!flags.operation) {
3789
+ printCommandHelpAndFail('valkey credentials status');
3790
+ return;
3791
+ }
3792
+ const orgId = await resolveOrgId(session, flags);
3793
+ const data = unwrapData(await request(
3794
+ session,
3795
+ flags,
3796
+ 'GET',
3797
+ `/managed-services/${projectId}/credential-rotations/${flags.operation}`,
3798
+ { orgId },
3799
+ ));
3800
+ if (flags.json) return printJson(data);
3801
+ printValkeyRotation(data);
3802
+ }
3803
+
3804
+ async function handleValkeyCredentialRotate(session, flags) {
3805
+ const projectId = requireProjectId(flags, 'valkey credentials rotate');
3806
+ if (!projectId) return;
3807
+ const idempotencyKey = String(flags.idempotencyKey || randomUUID());
3808
+ if (!/^[A-Za-z0-9._-]{16,200}$/.test(idempotencyKey)) {
3809
+ throw new CliError('--idempotency-key deve ter entre 16 e 200 caracteres: letras, numeros, ponto, underscore ou hifen.');
3810
+ }
3811
+ const orgId = await resolveOrgId(session, flags);
3812
+ const acceptedPayload = await request(session, flags, 'PATCH', `/managed-services/${projectId}/credentials`, {
3813
+ orgId,
3814
+ body: {},
3815
+ headers: { 'Idempotency-Key': idempotencyKey },
3816
+ });
3817
+ const accepted = unwrapData(acceptedPayload);
3818
+ if (!accepted?.operation_id) throw new CliError('A API nao retornou o identificador da renovacao de credencial.');
3819
+
3820
+ if (flags.wait) {
3821
+ const completed = await waitForValkeyRotation(session, flags, projectId, accepted.operation_id, orgId);
3822
+ if (flags.json) return printJson(completed);
3823
+ printValkeyRotation(completed);
3824
+ return;
3825
+ }
3826
+
3827
+ if (flags.json) return printJson(acceptedPayload);
3828
+ printValkeyRotation(accepted, { accepted: true });
3829
+ }
3830
+
3333
3831
  function buildQuery(flags, allowedKeys) {
3334
3832
  const params = new URLSearchParams();
3335
3833
  for (const key of allowedKeys) {
@@ -3556,6 +4054,14 @@ async function main() {
3556
4054
  if (command === 'orgs') return handleOrgs(session, flags);
3557
4055
  if (command === 'org' && subcommand === 'set') return handleOrgSet(session, flags);
3558
4056
  if (command === 'projects') return handleProjects(session, flags);
4057
+ if (command === 'valkey' && subcommand === 'credentials' && positional.length === 2) {
4058
+ process.stdout.write(commandHelp(['valkey', 'credentials']));
4059
+ return;
4060
+ }
4061
+ if (command === 'valkey' && subcommand === 'status') return handleValkeyStatus(session, flags);
4062
+ if (command === 'valkey' && subcommand === 'connection') return handleValkeyConnection(session, flags);
4063
+ if (command === 'valkey' && subcommand === 'credentials' && positional[2] === 'rotate') return handleValkeyCredentialRotate(session, flags);
4064
+ if (command === 'valkey' && subcommand === 'credentials' && positional[2] === 'status') return handleValkeyCredentialStatus(session, flags);
3559
4065
  if (command === 'project' && subcommand === 'info') return handleProjectInfo(session, flags);
3560
4066
  if (command === 'project' && subcommand === 'url') return handleProjectUrl(session, flags);
3561
4067
  if (command === 'project' && subcommand === 'logs') return handleProjectLogs(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.5",
4
4
  "description": "Official Zenifra command line interface.",
5
5
  "type": "module",
6
6
  "license": "MIT",