@zenifra/cli 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -47,6 +47,7 @@ zenifra profile add --name staging --description Homologacao --api-base https://
47
47
  zenifra profile use staging
48
48
  zenifra profile edit staging --description "Homologacao interna"
49
49
  zenifra profile remove staging
50
+ zenifra whoami
50
51
  zenifra orgs
51
52
  zenifra org set
52
53
  zenifra plans
@@ -59,12 +60,16 @@ zenifra create project --name <name> --plan basic --payment-mode hourly --config
59
60
  zenifra create project --name <name> --plan premium --payment-mode hourly --config @examples/http-autoscaling-project.json
60
61
  zenifra create project --name <name> --plan db-basic --payment-mode monthly --config @examples/postgresql-project.json
61
62
  zenifra create project --name <name> --plan db-basic --payment-mode monthly --config @examples/mariadb-project.json
63
+ zenifra create project --name <name> --plan analytics-starter --payment-mode hourly --config @examples/clickhouse-project.json
62
64
  zenifra create project --name <name> --plan db-free --payment-mode hourly --config @examples/valkey-key-value-project.json
63
65
  zenifra create project --name <name> --plan cache-free --payment-mode hourly --config @examples/valkey-cache-project.json
64
66
  zenifra create project --name <name> --plan queue-free --payment-mode hourly --config @examples/valkey-queue-project.json
65
67
  zenifra projects --type http --page 1 --limit 15
66
68
  zenifra projects --type valkey --page 1 --limit 15
67
69
  zenifra project info --project <project-id>
70
+ zenifra project stop --project <project-id>
71
+ zenifra project resume --project <project-id>
72
+ zenifra project delete --project <project-id> --yes
68
73
  zenifra valkey status --project <project-id>
69
74
  zenifra valkey connection --project <project-id>
70
75
  zenifra valkey credentials rotate --project <project-id> --wait
@@ -148,6 +153,8 @@ Fluxos:
148
153
  - `zenifra deploy`: dispara o build/deploy GitHub e retorna o `build_id`
149
154
  - `zenifra deploy watch`: usa esse `build_id` para acompanhar o build em tempo real e imprimir os logs incrementais ate o fim
150
155
 
156
+ Alguns builds antigos podem disponibilizar somente um resumo terminal. Nesse caso, a saida legivel avisa que eventos detalhados nao estavam disponiveis; `--json` preserva a resposta recebida da API.
157
+
151
158
  Antes de uma mutacao, confirme o perfil, a API e a organizacao ativa. Para criacoes, confirme tambem o catalogo, plano, pagamento, exposicao, dominio, origem do deploy, porta, instancias e ambientes. Depois, leia o projeto de volta e acompanhe o build/deployment ate um estado terminal.
152
159
 
153
160
  O dominio principal e um dominio personalizado sao campos diferentes. Nao repita o dominio principal em `config.custom_domains`; depois de adicionar um dominio personalizado, valide DNS/TLS e leia a URL final antes de considerar a operacao concluida.
@@ -174,6 +181,7 @@ Para executar a copia local deste repositorio, substitua `zenifra` por
174
181
 
175
182
  - `--read-only` solicita somente leitura; as permissoes da sua organizacao continuam valendo.
176
183
  - `--no-browser` mostra o endereco para abrir manualmente no navegador da mesma maquina.
184
+ - Em um terminal sem navegador, use `--no-browser` e abra o endereco em uma sessao grafica da mesma maquina. O retorno de autenticacao precisa ser concluido nessa maquina.
177
185
  - O retorno usa uma porta local temporaria em `127.0.0.1`; nao precisa cadastrar uma porta fixa.
178
186
  - O login aguarda ate tres minutos. Use `Ctrl+C` para cancelar.
179
187
  - Os tokens ficam no perfil local privado e sao renovados automaticamente antes de expirar.
@@ -213,6 +221,8 @@ zenifra plans --type storage --json
213
221
 
214
222
  `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.
215
223
 
224
+ Para planos HTTP, a saida legivel tambem mostra as capacidades disponiveis, como logs, metricas, verificacao de saude, auto-scaling, subdominio personalizado e acesso de rede. Use `--json` quando precisar consumir o catalogo sem formatacao.
225
+
216
226
  Valores de variaveis de ambiente sao mascarados por padrao, inclusive em `--json`.
217
227
  Use `--show-values` apenas quando precisar inspecionar os valores completos.
218
228
 
@@ -280,6 +290,7 @@ Use os arquivos em `examples/` como base para `zenifra create project`:
280
290
  - `examples/http-autoscaling-project.json`: projeto HTTP pago criado com auto-scaling
281
291
  - `examples/postgresql-project.json`: projeto PostgreSQL
282
292
  - `examples/mariadb-project.json`: projeto MariaDB
293
+ - `examples/clickhouse-project.json`: projeto ClickHouse com Native TLS e armazenamento persistente
283
294
  - `examples/valkey-key-value-project.json`: projeto Valkey Key Value com armazenamento persistente
284
295
  - `examples/valkey-cache-project.json`: projeto Valkey Cache sem armazenamento persistente
285
296
  - `examples/valkey-queue-project.json`: projeto Valkey Queue com armazenamento persistente
@@ -297,6 +308,8 @@ O wizard atual cobre:
297
308
  - projetos `mariadb`
298
309
  - projetos `valkey` nos perfis `key_value`, `cache` e `queue`
299
310
 
311
+ Projetos `clickhouse` usam atualmente o fluxo nao interativo com `--config`, como em `examples/clickhouse-project.json`.
312
+
300
313
  `zenifra create project` nao assume valores default para `--plan` e `--payment-mode`.
301
314
  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.
302
315
  Antes de escolher um plano com o usuario, compare os catalogos com `zenifra plans` para evitar suposicoes sobre custo.
@@ -304,9 +317,9 @@ Antes de escolher um plano com o usuario, compare os catalogos com `zenifra plan
304
317
  Valores aceitos:
305
318
 
306
319
  - `payment_mode`: `hourly`, `monthly`, `yearly`
307
- - `type_project` no `config`: `http`, `postgresql`, `mariadb`, `valkey`
320
+ - `type_project` no `config`: `http`, `postgresql`, `mariadb`, `valkey`, `clickhouse`
308
321
  - `exposure` no `config` HTTP: `public`, `private`
309
- - `plan`: consulte `zenifra plans` para os planos atuais; Valkey usa `db-*` para Key Value, `cache-*` para Cache e `queue-*` para Queue
322
+ - `plan`: consulte `zenifra plans` para os planos atuais; ClickHouse usa `analytics-*`; Valkey usa `db-*` para Key Value, `cache-*` para Cache e `queue-*` para Queue
310
323
  - `config.profile` em projetos Valkey: `key_value`, `cache` ou `queue`
311
324
  - `config.version` em projetos Valkey: a versão retornada por `zenifra plans --type valkey`
312
325
  - `config.storage` em projetos Valkey: obrigatório e persistente para Key Value/Queue; omitido para Cache
package/bin/zenifra.mjs CHANGED
@@ -80,6 +80,7 @@ const KNOWN_FLAG_NAMES = new Set([
80
80
  'value',
81
81
  'view',
82
82
  'wait',
83
+ 'yes',
83
84
  ]);
84
85
  const ALLOWED_PLAN_VALUES = new Set([
85
86
  'free',
@@ -95,6 +96,10 @@ const ALLOWED_PLAN_VALUES = new Set([
95
96
  'db-basic',
96
97
  'db-premium',
97
98
  'db-enterprise',
99
+ 'analytics-sandbox',
100
+ 'analytics-starter',
101
+ 'analytics-production',
102
+ 'analytics-enterprise',
98
103
  'cache-free',
99
104
  'cache-starter',
100
105
  'cache-basic',
@@ -107,7 +112,7 @@ const ALLOWED_PLAN_VALUES = new Set([
107
112
  'queue-enterprise',
108
113
  ]);
109
114
  const ALLOWED_PAYMENT_MODE_VALUES = new Set(['hourly', 'monthly', 'yearly']);
110
- const ALLOWED_TYPE_PROJECT_VALUES = new Set(['http', 'postgresql', 'mariadb', 'valkey']);
115
+ const ALLOWED_TYPE_PROJECT_VALUES = new Set(['http', 'postgresql', 'mariadb', 'valkey', 'clickhouse']);
111
116
  const ALLOWED_RUNTIME_VALUES = new Set(['nodejs', 'python']);
112
117
  const ALLOWED_EXPOSURE_VALUES = new Set(['public', 'private']);
113
118
  const GITHUB_RUNTIME_VERSIONS = {
@@ -175,6 +180,7 @@ Usage:
175
180
  zenifra profile edit <name> [--description <text>] [--api-base <url>] [--json]
176
181
  zenifra profile use <name> [--json]
177
182
  zenifra profile remove <name> [--json]
183
+ zenifra whoami [--json]
178
184
  zenifra orgs [--json]
179
185
  zenifra org set [--org <id>]
180
186
  zenifra plans [--type <all|http|database|storage|valkey>] [--json]
@@ -186,6 +192,9 @@ Usage:
186
192
  zenifra valkey credentials rotate --project <id> [--idempotency-key <key>] [--wait] [--interval <seconds>] [--timeout <seconds>] [--json]
187
193
  zenifra valkey credentials status --project <id> --operation <id> [--json]
188
194
  zenifra project info --project <id> [--json]
195
+ zenifra project stop --project <id> [--json]
196
+ zenifra project resume --project <id> [--json]
197
+ zenifra project delete --project <id> --yes [--json]
189
198
  zenifra project url --project <id> [--json]
190
199
  zenifra project logs --project <id> [--instance <id>] [--json]
191
200
  zenifra project metrics --project <id> [--instance <id>] [--json]
@@ -246,7 +255,7 @@ const HELP_SPECS = [
246
255
  ],
247
256
  examples: ['zenifra auth login', 'zenifra auth login --profile staging --code 123456'],
248
257
  output: 'Login realizado com sucesso.',
249
- notes: ['Exige email, senha e, quando habilitado, codigo de desafio.'],
258
+ notes: ['Exige email, senha e, quando habilitado, codigo de desafio.', 'Em um terminal sem navegador, use --oauth --no-browser e abra o endereco em uma sessao grafica da mesma maquina.'],
250
259
  },
251
260
  {
252
261
  command: 'auth api-key',
@@ -345,6 +354,16 @@ const HELP_SPECS = [
345
354
  output: 'ID Nome Role Status\n507f1f77bcf86cd799439011 Minha org owner active',
346
355
  jsonOutput: '[{"_id":"507f1f77bcf86cd799439011","name":"Minha org","role":"owner","status":"active"}]',
347
356
  },
357
+ {
358
+ command: 'whoami',
359
+ usage: 'zenifra whoami [--json]',
360
+ description: 'Mostra o perfil ativo e a organizacao selecionada para os proximos comandos.',
361
+ flags: ['--json Imprime a identificacao em JSON.'],
362
+ examples: ['zenifra whoami', 'zenifra whoami --json'],
363
+ output: 'Perfil: staging\nAPI base: https://api.zenifra.com/v1\nAutenticacao: access_token\nOrganizacao: Minha organizacao (507f1f77bcf86cd799439011)',
364
+ jsonOutput: '{"profile":"staging","api_base_url":"https://api.zenifra.com/v1","authentication_mode":"access_token","selected_organization_id":"507f1f77bcf86cd799439011","organization_name":"Minha organizacao"}',
365
+ notes: ['Nenhuma credencial e exibida. Quando nao houver organizacao selecionada, use "zenifra org set" ou informe --org no comando.'],
366
+ },
348
367
  {
349
368
  command: 'org set',
350
369
  usage: 'zenifra org set [--org <id>]',
@@ -426,17 +445,17 @@ const HELP_SPECS = [
426
445
  description: 'Cria um projeto via wizard interativo ou via payload de configuracao JSON.',
427
446
  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.'],
428
447
  examples: ['zenifra create project', 'zenifra create project --name api-web --plan free --payment-mode hourly --config @examples/http-project.json'],
429
- output: 'Campo Valor\n------- --------------------------------------\nProjeto 507f1f77bcf86cd799439012\nDominio https://api-web.client.zenifra.com',
448
+ output: 'Campo Valor\n------- --------------------------------------\nProjeto 507f1f77bcf86cd799439012\nDominio https://api-web.clients.zenifra.com',
430
449
  jsonOutput: '{"status":"success","data":{"id":"507f1f77bcf86cd799439012","name":"api-web"}}',
431
450
  notes: ['Sem flags de criacao, a CLI abre um wizard guiado com docs, exemplos e indicacao de campos obrigatorios.'],
432
451
  },
433
452
  {
434
453
  command: 'project',
435
- 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 healthcheck get --project <id> [--json]\n zenifra project healthcheck set --project <id> --path /health [--json]\n zenifra project healthcheck disable --project <id> [--json]\n zenifra project healthcheck failures --project <id> [--page <n>] [--limit <n>] [--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]',
454
+ usage: 'zenifra project\n zenifra project info --project <id> [--json]\n zenifra project stop --project <id> [--json]\n zenifra project resume --project <id> [--json]\n zenifra project delete --project <id> --yes [--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 healthcheck get --project <id> [--json]\n zenifra project healthcheck set --project <id> --path /health [--json]\n zenifra project healthcheck disable --project <id> [--json]\n zenifra project healthcheck failures --project <id> [--page <n>] [--limit <n>] [--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]',
436
455
  description: 'Agrupa comandos operacionais e de introspecao sobre um projeto especifico.',
437
456
  examples: ['zenifra project', 'zenifra project info --project proj_1', 'zenifra project env add --project proj_1 --name NODE_ENV --value production'],
438
457
  output: 'Zenifra CLI - project',
439
- notes: ['Use "zenifra help project <subcomando>" para detalhes de info, url, logs, metrics, capabilities, network, image, exposure, autoscaling, billing, envs e instances.'],
458
+ notes: ['Use "zenifra help project <subcomando>" para detalhes de info, stop, resume, delete, url, logs, metrics, capabilities, network, image, exposure, autoscaling, billing, envs e instances.'],
440
459
  },
441
460
  {
442
461
  command: 'project info',
@@ -444,8 +463,36 @@ const HELP_SPECS = [
444
463
  description: 'Mostra dados principais de um projeto.',
445
464
  flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.'],
446
465
  examples: ['zenifra project info --project 507f1f77bcf86cd799439012'],
447
- output: 'Nome: api-web\nStatus: running\nURL: https://api-web.client.zenifra.com\nInstancias: 2',
448
- jsonOutput: '{"name":"api-web","status":"running","domain":"api-web.client.zenifra.com","instances":2}',
466
+ output: 'Nome: api-web\nStatus: running\nURL: https://api-web.clients.zenifra.com\nInstancias: 2',
467
+ jsonOutput: '{"name":"api-web","status":"running","domain":"api-web.clients.zenifra.com","instances":2}',
468
+ },
469
+ {
470
+ command: 'project stop',
471
+ usage: 'zenifra project stop --project <id> [--json]',
472
+ description: 'Para um projeto e mostra o estado confirmado em seguida.',
473
+ flags: ['--project <id> ID do projeto.', '--json Imprime o estado confirmado em JSON.'],
474
+ examples: ['zenifra project stop --project 507f1f77bcf86cd799439012'],
475
+ output: 'Nome: api-web\nStatus: stopped',
476
+ jsonOutput: '{"id":"507f1f77bcf86cd799439012","status":"stopped"}',
477
+ },
478
+ {
479
+ command: 'project resume',
480
+ usage: 'zenifra project resume --project <id> [--json]',
481
+ description: 'Retoma um projeto e mostra o estado confirmado em seguida.',
482
+ flags: ['--project <id> ID do projeto.', '--json Imprime o estado confirmado em JSON.'],
483
+ examples: ['zenifra project resume --project 507f1f77bcf86cd799439012'],
484
+ output: 'Nome: api-web\nStatus: running',
485
+ jsonOutput: '{"id":"507f1f77bcf86cd799439012","status":"running"}',
486
+ },
487
+ {
488
+ command: 'project delete',
489
+ usage: 'zenifra project delete --project <id> --yes [--json]',
490
+ description: 'Remove um projeto quando a confirmacao explicita e informada.',
491
+ flags: ['--project <id> ID do projeto.', '--yes Confirma a remocao.', '--json Imprime a resposta da API em JSON.'],
492
+ examples: ['zenifra project delete --project 507f1f77bcf86cd799439012 --yes'],
493
+ output: 'Projeto removido.',
494
+ jsonOutput: '{"status":"success","message":"project removed"}',
495
+ notes: ['Sem --yes, a CLI nao envia nenhuma solicitacao de remocao.'],
449
496
  },
450
497
  {
451
498
  command: 'project url',
@@ -453,8 +500,8 @@ const HELP_SPECS = [
453
500
  description: 'Imprime a URL publica principal do projeto.',
454
501
  flags: ['--project <id> ID do projeto.', '--json Inclui dominio e custom domains em JSON.'],
455
502
  examples: ['zenifra project url --project 507f1f77bcf86cd799439012'],
456
- output: 'https://api-web.client.zenifra.com',
457
- jsonOutput: '{"project_id":"507f1f77bcf86cd799439012","url":"https://api-web.client.zenifra.com","domain":"api-web.client.zenifra.com","custom_domains":[]}',
503
+ output: 'https://api-web.clients.zenifra.com',
504
+ jsonOutput: '{"project_id":"507f1f77bcf86cd799439012","url":"https://api-web.clients.zenifra.com","domain":"api-web.clients.zenifra.com","custom_domains":[]}',
458
505
  },
459
506
  {
460
507
  command: 'project logs',
@@ -2264,6 +2311,38 @@ async function handleOrgs(session, flags) {
2264
2311
  ]);
2265
2312
  }
2266
2313
 
2314
+ async function handleWhoami(session, flags) {
2315
+ if (!getProfileName(session)) {
2316
+ throw new CliError('Nenhum perfil ativo configurado. Crie um perfil com "zenifra profile add" ou autentique com "zenifra auth login".');
2317
+ }
2318
+ const profile = profileOutput(session, getStore(session).activeProfile);
2319
+ const effectiveCredential = resolveCredential(session);
2320
+ const usingApiKey = effectiveCredential?.type === 'api_key';
2321
+ const selectedOrganizationId = usingApiKey ? null : profile.selected_organization_id || null;
2322
+ let organizationName = null;
2323
+
2324
+ if (selectedOrganizationId && !hasApiKeyCredential(session) && session.accessToken) {
2325
+ const organizations = await getOrganizations(session, flags);
2326
+ organizationName = organizations.find((organization) => idOf(organization) === selectedOrganizationId)?.name
2327
+ || organizations.find((organization) => idOf(organization) === selectedOrganizationId)?.organization?.name
2328
+ || null;
2329
+ }
2330
+
2331
+ const output = {
2332
+ profile: profile.name,
2333
+ api_base_url: apiBaseUrl(session, flags),
2334
+ authentication_mode: usingApiKey ? 'api_key' : profile.auth_mode,
2335
+ selected_organization_id: selectedOrganizationId,
2336
+ organization_name: organizationName,
2337
+ };
2338
+
2339
+ if (flags.json) return printJson(output);
2340
+ process.stdout.write(`Perfil: ${output.profile}\n`);
2341
+ process.stdout.write(`API base: ${output.api_base_url}\n`);
2342
+ process.stdout.write(`Autenticacao: ${output.authentication_mode}\n`);
2343
+ process.stdout.write(`Organizacao: ${output.organization_name ? `${output.organization_name} (${selectedOrganizationId})` : selectedOrganizationId || 'nao selecionada'}\n`);
2344
+ }
2345
+
2267
2346
  async function handleOrgSet(session, flags) {
2268
2347
  requireUserSession(session, 'Selecionar organizacao');
2269
2348
 
@@ -2347,6 +2426,14 @@ function normalizePlan(value) {
2347
2426
  ['dbpremium', 'db-premium'],
2348
2427
  ['db_enterprise', 'db-enterprise'],
2349
2428
  ['dbenterprise', 'db-enterprise'],
2429
+ ['analytics_sandbox', 'analytics-sandbox'],
2430
+ ['analyticssandbox', 'analytics-sandbox'],
2431
+ ['analytics_starter', 'analytics-starter'],
2432
+ ['analyticsstarter', 'analytics-starter'],
2433
+ ['analytics_production', 'analytics-production'],
2434
+ ['analyticsproduction', 'analytics-production'],
2435
+ ['analytics_enterprise', 'analytics-enterprise'],
2436
+ ['analyticsenterprise', 'analytics-enterprise'],
2350
2437
  ['cache_free', 'cache-free'],
2351
2438
  ['cachefree', 'cache-free'],
2352
2439
  ['cache_starter', 'cache-starter'],
@@ -2406,6 +2493,8 @@ function normalizeTypeProject(value) {
2406
2493
  ['valkey', 'valkey'],
2407
2494
  ['key value', 'valkey'],
2408
2495
  ['key-value', 'valkey'],
2496
+ ['clickhouse', 'clickhouse'],
2497
+ ['click house', 'clickhouse'],
2409
2498
  ]);
2410
2499
  return aliases.get(raw) || raw;
2411
2500
  }
@@ -2691,6 +2780,22 @@ function printValkeyCatalog(catalog, profileFilter) {
2691
2780
  });
2692
2781
  }
2693
2782
 
2783
+ const HTTP_CAPABILITY_LABELS = {
2784
+ logs: 'logs',
2785
+ metrics: 'metricas',
2786
+ healthcheck: 'verificacao de saude',
2787
+ autoscaling: 'auto-scaling',
2788
+ custom_subdomain: 'subdominio personalizado',
2789
+ network_access: 'acesso de rede',
2790
+ };
2791
+
2792
+ function formatPlanCapabilities(capabilities) {
2793
+ const available = Object.entries(HTTP_CAPABILITY_LABELS)
2794
+ .filter(([key]) => capabilities?.[key] === true)
2795
+ .map(([, label]) => label);
2796
+ return available.join(', ') || '-';
2797
+ }
2798
+
2694
2799
  function printPlansCatalogs(payload, type) {
2695
2800
  if (type === 'all' || type === 'http') {
2696
2801
  process.stdout.write('HTTP\n');
@@ -2700,6 +2805,7 @@ function printPlansCatalogs(payload, type) {
2700
2805
  { label: 'Mes', value: (plan) => formatBrlFromCents(plan.prices?.monthly) },
2701
2806
  { label: 'Ano', value: (plan) => formatBrlFromCents(plan.prices?.yearly) },
2702
2807
  { label: 'Recursos', value: (plan) => asArray(plan.features).join(', ') || '-' },
2808
+ { label: 'Capacidades', value: (plan) => formatPlanCapabilities(plan.capabilities) },
2703
2809
  ]);
2704
2810
  }
2705
2811
 
@@ -3505,6 +3611,7 @@ async function handleProjectCreate(session, flags) {
3505
3611
  const plan = wizardPayload?.plan || flags.plan || await prompt('Plano');
3506
3612
  const paymentMode = wizardPayload?.payment_mode || flags.paymentMode || await prompt('Modo de pagamento');
3507
3613
  const config = wizardPayload?.config || await parseConfig(flags.config || await prompt('Config JSON ou @arquivo'));
3614
+ const idempotencyKey = flags.idempotencyKey === undefined ? undefined : validateIdempotencyKey(flags.idempotencyKey);
3508
3615
  if (Array.isArray(config?.custom_domains)) {
3509
3616
  config.custom_domains = normalizeCustomDomains(config.custom_domains, { primaryDomain: config.domain });
3510
3617
  }
@@ -3512,14 +3619,11 @@ async function handleProjectCreate(session, flags) {
3512
3619
  const valkeyCatalog = typeProject === 'valkey'
3513
3620
  ? requireValkeyCatalog(unwrapData(await request(session, flags, 'GET', '/managed-services/catalog')))
3514
3621
  : undefined;
3515
- if (flags.idempotencyKey !== undefined && !/^[A-Za-z0-9._-]{16,200}$/.test(String(flags.idempotencyKey))) {
3516
- throw new CliError('--idempotency-key deve ter entre 16 e 200 caracteres: letras, numeros, ponto, underscore ou hifen.');
3517
- }
3518
3622
  const validated = validateCreateInput({ plan, paymentMode, config, valkeyCatalog });
3519
3623
 
3520
3624
  const payload = await request(session, flags, 'POST', '/project', {
3521
3625
  orgId,
3522
- headers: flags.idempotencyKey === undefined ? {} : { 'Idempotency-Key': String(flags.idempotencyKey) },
3626
+ headers: idempotencyKey === undefined ? {} : { 'Idempotency-Key': idempotencyKey },
3523
3627
  body: {
3524
3628
  name,
3525
3629
  ...(description ? { description: String(description) } : {}),
@@ -3575,6 +3679,33 @@ async function handleProjectInfo(session, flags) {
3575
3679
  printProject({ ...project, id: projectId });
3576
3680
  }
3577
3681
 
3682
+ async function handleProjectLifecycleMutation(session, flags, action) {
3683
+ const commandKey = `project ${action}`;
3684
+ const projectId = requireProjectId(flags, commandKey);
3685
+ if (!projectId) return;
3686
+
3687
+ const orgId = await resolveOrgId(session, flags);
3688
+ await request(session, flags, 'PATCH', `/project/${projectId}/${action}`, { orgId });
3689
+ const project = await getProject(session, flags, projectId, orgId);
3690
+
3691
+ if (flags.json) return printJson(projectInfoForOutput(project));
3692
+ printProject({ ...project, id: projectId });
3693
+ }
3694
+
3695
+ async function handleProjectDelete(session, flags) {
3696
+ const projectId = requireProjectId(flags, 'project delete');
3697
+ if (!projectId) return;
3698
+ if (flags.yes !== true) {
3699
+ throw new CliError('A remocao exige --yes. Nenhuma solicitacao foi enviada.');
3700
+ }
3701
+
3702
+ const orgId = await resolveOrgId(session, flags);
3703
+ const payload = await request(session, flags, 'DELETE', `/project/${projectId}`, { orgId });
3704
+
3705
+ if (flags.json) return printJson(payload);
3706
+ process.stdout.write('Projeto removido.\n');
3707
+ }
3708
+
3578
3709
  async function handleProjectUrl(session, flags) {
3579
3710
  const projectId = requireProjectId(flags, 'project url');
3580
3711
  if (!projectId) return
@@ -4262,10 +4393,7 @@ async function handleValkeyCredentialStatus(session, flags) {
4262
4393
  async function handleValkeyCredentialRotate(session, flags) {
4263
4394
  const projectId = requireProjectId(flags, 'valkey credentials rotate');
4264
4395
  if (!projectId) return;
4265
- const idempotencyKey = String(flags.idempotencyKey || randomUUID());
4266
- if (!/^[A-Za-z0-9._-]{16,200}$/.test(idempotencyKey)) {
4267
- throw new CliError('--idempotency-key deve ter entre 16 e 200 caracteres: letras, numeros, ponto, underscore ou hifen.');
4268
- }
4396
+ const idempotencyKey = validateIdempotencyKey(flags.idempotencyKey === undefined ? randomUUID() : flags.idempotencyKey);
4269
4397
  const orgId = await resolveOrgId(session, flags);
4270
4398
  const acceptedPayload = await request(session, flags, 'PATCH', `/managed-services/${projectId}/credentials`, {
4271
4399
  orgId,
@@ -4376,8 +4504,25 @@ function parseSecondsOption(value, flagName, { defaultValue, min }) {
4376
4504
  return parsed;
4377
4505
  }
4378
4506
 
4507
+ function validateIdempotencyKey(value) {
4508
+ const key = String(value);
4509
+ if (key.length < 16 || key.length > 200) {
4510
+ throw new CliError('--idempotency-key deve ter entre 16 e 200 caracteres.');
4511
+ }
4512
+
4513
+ const invalidIndex = Array.from(key).findIndex((character) => !/[A-Za-z0-9._-]/.test(character));
4514
+ if (invalidIndex !== -1) {
4515
+ throw new CliError(`--idempotency-key contem caractere invalido na posicao ${invalidIndex + 1}. Use apenas letras, numeros, ponto, underscore ou hifen.`);
4516
+ }
4517
+
4518
+ return key;
4519
+ }
4520
+
4379
4521
  function printBuildLogs(logs) {
4380
4522
  for (const log of asArray(logs)) {
4523
+ if (log?.source === 'summary') {
4524
+ process.stdout.write('Este build possui apenas um resumo; eventos detalhados nao estavam disponiveis.\n');
4525
+ }
4381
4526
  process.stdout.write(`${buildLogLineOf(log)}\n`)
4382
4527
  }
4383
4528
  }
@@ -4521,6 +4666,7 @@ async function main() {
4521
4666
  if (command === 'plans') return handlePlans(session, flags);
4522
4667
  if (command === 'create' && subcommand === 'project') return handleProjectCreate(session, flags);
4523
4668
  if (command === 'orgs') return handleOrgs(session, flags);
4669
+ if (command === 'whoami') return handleWhoami(session, flags);
4524
4670
  if (command === 'org' && subcommand === 'set') return handleOrgSet(session, flags);
4525
4671
  if (command === 'projects') return handleProjects(session, flags);
4526
4672
  if (command === 'valkey' && subcommand === 'credentials' && positional.length === 2) {
@@ -4540,6 +4686,9 @@ async function main() {
4540
4686
  if (command === 'project' && subcommand === 'healthcheck' && positional[2] === 'disable') return handleProjectHealthcheckDisable(session, flags);
4541
4687
  if (command === 'project' && subcommand === 'healthcheck' && positional[2] === 'failures') return handleProjectHealthcheckFailures(session, flags);
4542
4688
  if (command === 'project' && subcommand === 'info') return handleProjectInfo(session, flags);
4689
+ if (command === 'project' && subcommand === 'stop') return handleProjectLifecycleMutation(session, flags, 'stop');
4690
+ if (command === 'project' && subcommand === 'resume') return handleProjectLifecycleMutation(session, flags, 'resume');
4691
+ if (command === 'project' && subcommand === 'delete') return handleProjectDelete(session, flags);
4543
4692
  if (command === 'project' && subcommand === 'url') return handleProjectUrl(session, flags);
4544
4693
  if (command === 'project' && subcommand === 'logs') return handleProjectLogs(session, flags);
4545
4694
  if (command === 'project' && subcommand === 'metrics' && positional[2] === 'capabilities') return handleProjectMetricsCapabilities(session, flags);
@@ -0,0 +1,19 @@
1
+ {
2
+ "type_project": "clickhouse",
3
+ "version": "26.8.6.5",
4
+ "instances": 1,
5
+ "storage": {
6
+ "capacity": 20,
7
+ "persistent": true
8
+ },
9
+ "envs": [],
10
+ "network_access": {
11
+ "ingress_white_list": [
12
+ {
13
+ "cidr": "203.0.113.10/32",
14
+ "description": "Application egress IP"
15
+ }
16
+ ],
17
+ "ingress_black_list": []
18
+ }
19
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenifra/cli",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Official Zenifra command line interface.",
5
5
  "type": "module",
6
6
  "license": "MIT",