@zenifra/cli 0.3.1 → 0.3.3

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
@@ -65,6 +66,9 @@ zenifra create project --name <name> --plan queue-free --payment-mode hourly --c
65
66
  zenifra projects --type http --page 1 --limit 15
66
67
  zenifra projects --type valkey --page 1 --limit 15
67
68
  zenifra project info --project <project-id>
69
+ zenifra project stop --project <project-id>
70
+ zenifra project resume --project <project-id>
71
+ zenifra project delete --project <project-id> --yes
68
72
  zenifra valkey status --project <project-id>
69
73
  zenifra valkey connection --project <project-id>
70
74
  zenifra valkey credentials rotate --project <project-id> --wait
@@ -148,6 +152,8 @@ Fluxos:
148
152
  - `zenifra deploy`: dispara o build/deploy GitHub e retorna o `build_id`
149
153
  - `zenifra deploy watch`: usa esse `build_id` para acompanhar o build em tempo real e imprimir os logs incrementais ate o fim
150
154
 
155
+ 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.
156
+
151
157
  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
158
 
153
159
  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 +180,7 @@ Para executar a copia local deste repositorio, substitua `zenifra` por
174
180
 
175
181
  - `--read-only` solicita somente leitura; as permissoes da sua organizacao continuam valendo.
176
182
  - `--no-browser` mostra o endereco para abrir manualmente no navegador da mesma maquina.
183
+ - 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
184
  - O retorno usa uma porta local temporaria em `127.0.0.1`; nao precisa cadastrar uma porta fixa.
178
185
  - O login aguarda ate tres minutos. Use `Ctrl+C` para cancelar.
179
186
  - Os tokens ficam no perfil local privado e sao renovados automaticamente antes de expirar.
@@ -213,6 +220,8 @@ zenifra plans --type storage --json
213
220
 
214
221
  `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
222
 
223
+ 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.
224
+
216
225
  Valores de variaveis de ambiente sao mascarados por padrao, inclusive em `--json`.
217
226
  Use `--show-values` apenas quando precisar inspecionar os valores completos.
218
227
 
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',
@@ -175,6 +176,7 @@ Usage:
175
176
  zenifra profile edit <name> [--description <text>] [--api-base <url>] [--json]
176
177
  zenifra profile use <name> [--json]
177
178
  zenifra profile remove <name> [--json]
179
+ zenifra whoami [--json]
178
180
  zenifra orgs [--json]
179
181
  zenifra org set [--org <id>]
180
182
  zenifra plans [--type <all|http|database|storage|valkey>] [--json]
@@ -186,6 +188,9 @@ Usage:
186
188
  zenifra valkey credentials rotate --project <id> [--idempotency-key <key>] [--wait] [--interval <seconds>] [--timeout <seconds>] [--json]
187
189
  zenifra valkey credentials status --project <id> --operation <id> [--json]
188
190
  zenifra project info --project <id> [--json]
191
+ zenifra project stop --project <id> [--json]
192
+ zenifra project resume --project <id> [--json]
193
+ zenifra project delete --project <id> --yes [--json]
189
194
  zenifra project url --project <id> [--json]
190
195
  zenifra project logs --project <id> [--instance <id>] [--json]
191
196
  zenifra project metrics --project <id> [--instance <id>] [--json]
@@ -246,7 +251,7 @@ const HELP_SPECS = [
246
251
  ],
247
252
  examples: ['zenifra auth login', 'zenifra auth login --profile staging --code 123456'],
248
253
  output: 'Login realizado com sucesso.',
249
- notes: ['Exige email, senha e, quando habilitado, codigo de desafio.'],
254
+ 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
255
  },
251
256
  {
252
257
  command: 'auth api-key',
@@ -345,6 +350,16 @@ const HELP_SPECS = [
345
350
  output: 'ID Nome Role Status\n507f1f77bcf86cd799439011 Minha org owner active',
346
351
  jsonOutput: '[{"_id":"507f1f77bcf86cd799439011","name":"Minha org","role":"owner","status":"active"}]',
347
352
  },
353
+ {
354
+ command: 'whoami',
355
+ usage: 'zenifra whoami [--json]',
356
+ description: 'Mostra o perfil ativo e a organizacao selecionada para os proximos comandos.',
357
+ flags: ['--json Imprime a identificacao em JSON.'],
358
+ examples: ['zenifra whoami', 'zenifra whoami --json'],
359
+ output: 'Perfil: staging\nAPI base: https://api.zenifra.com/v1\nAutenticacao: access_token\nOrganizacao: Minha organizacao (507f1f77bcf86cd799439011)',
360
+ jsonOutput: '{"profile":"staging","api_base_url":"https://api.zenifra.com/v1","authentication_mode":"access_token","selected_organization_id":"507f1f77bcf86cd799439011","organization_name":"Minha organizacao"}',
361
+ notes: ['Nenhuma credencial e exibida. Quando nao houver organizacao selecionada, use "zenifra org set" ou informe --org no comando.'],
362
+ },
348
363
  {
349
364
  command: 'org set',
350
365
  usage: 'zenifra org set [--org <id>]',
@@ -426,17 +441,17 @@ const HELP_SPECS = [
426
441
  description: 'Cria um projeto via wizard interativo ou via payload de configuracao JSON.',
427
442
  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
443
  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',
444
+ output: 'Campo Valor\n------- --------------------------------------\nProjeto 507f1f77bcf86cd799439012\nDominio https://api-web.clients.zenifra.com',
430
445
  jsonOutput: '{"status":"success","data":{"id":"507f1f77bcf86cd799439012","name":"api-web"}}',
431
446
  notes: ['Sem flags de criacao, a CLI abre um wizard guiado com docs, exemplos e indicacao de campos obrigatorios.'],
432
447
  },
433
448
  {
434
449
  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]',
450
+ 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
451
  description: 'Agrupa comandos operacionais e de introspecao sobre um projeto especifico.',
437
452
  examples: ['zenifra project', 'zenifra project info --project proj_1', 'zenifra project env add --project proj_1 --name NODE_ENV --value production'],
438
453
  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.'],
454
+ 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
455
  },
441
456
  {
442
457
  command: 'project info',
@@ -444,8 +459,36 @@ const HELP_SPECS = [
444
459
  description: 'Mostra dados principais de um projeto.',
445
460
  flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.'],
446
461
  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}',
462
+ output: 'Nome: api-web\nStatus: running\nURL: https://api-web.clients.zenifra.com\nInstancias: 2',
463
+ jsonOutput: '{"name":"api-web","status":"running","domain":"api-web.clients.zenifra.com","instances":2}',
464
+ },
465
+ {
466
+ command: 'project stop',
467
+ usage: 'zenifra project stop --project <id> [--json]',
468
+ description: 'Para um projeto e mostra o estado confirmado em seguida.',
469
+ flags: ['--project <id> ID do projeto.', '--json Imprime o estado confirmado em JSON.'],
470
+ examples: ['zenifra project stop --project 507f1f77bcf86cd799439012'],
471
+ output: 'Nome: api-web\nStatus: stopped',
472
+ jsonOutput: '{"id":"507f1f77bcf86cd799439012","status":"stopped"}',
473
+ },
474
+ {
475
+ command: 'project resume',
476
+ usage: 'zenifra project resume --project <id> [--json]',
477
+ description: 'Retoma um projeto e mostra o estado confirmado em seguida.',
478
+ flags: ['--project <id> ID do projeto.', '--json Imprime o estado confirmado em JSON.'],
479
+ examples: ['zenifra project resume --project 507f1f77bcf86cd799439012'],
480
+ output: 'Nome: api-web\nStatus: running',
481
+ jsonOutput: '{"id":"507f1f77bcf86cd799439012","status":"running"}',
482
+ },
483
+ {
484
+ command: 'project delete',
485
+ usage: 'zenifra project delete --project <id> --yes [--json]',
486
+ description: 'Remove um projeto quando a confirmacao explicita e informada.',
487
+ flags: ['--project <id> ID do projeto.', '--yes Confirma a remocao.', '--json Imprime a resposta da API em JSON.'],
488
+ examples: ['zenifra project delete --project 507f1f77bcf86cd799439012 --yes'],
489
+ output: 'Projeto removido.',
490
+ jsonOutput: '{"status":"success","message":"project removed"}',
491
+ notes: ['Sem --yes, a CLI nao envia nenhuma solicitacao de remocao.'],
449
492
  },
450
493
  {
451
494
  command: 'project url',
@@ -453,8 +496,8 @@ const HELP_SPECS = [
453
496
  description: 'Imprime a URL publica principal do projeto.',
454
497
  flags: ['--project <id> ID do projeto.', '--json Inclui dominio e custom domains em JSON.'],
455
498
  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":[]}',
499
+ output: 'https://api-web.clients.zenifra.com',
500
+ jsonOutput: '{"project_id":"507f1f77bcf86cd799439012","url":"https://api-web.clients.zenifra.com","domain":"api-web.clients.zenifra.com","custom_domains":[]}',
458
501
  },
459
502
  {
460
503
  command: 'project logs',
@@ -1813,6 +1856,18 @@ function envsForOutput(envs, { showValues = false } = {}) {
1813
1856
  }));
1814
1857
  }
1815
1858
 
1859
+ const PROJECT_INFO_SECRET_FIELD = /(?:^|_)(?:api_?key|access_?token|refresh_?token|secret|password|credential|connection_string|private_?jwk)(?:$|_)/i;
1860
+
1861
+ function projectInfoForOutput(value, key = '') {
1862
+ if (key === 'envs' && Array.isArray(value)) return envsForOutput(value);
1863
+ if (Array.isArray(value)) return value.map((item) => projectInfoForOutput(item, key));
1864
+ if (!value || typeof value !== 'object') return PROJECT_INFO_SECRET_FIELD.test(key) ? maskEnvValue(value) : value;
1865
+ return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [
1866
+ entryKey,
1867
+ PROJECT_INFO_SECRET_FIELD.test(entryKey) ? maskEnvValue(entryValue) : projectInfoForOutput(entryValue, entryKey),
1868
+ ]));
1869
+ }
1870
+
1816
1871
  function formatPublicUrl(value) {
1817
1872
  const raw = String(value || '').trim();
1818
1873
  if (!raw) return '';
@@ -2252,6 +2307,38 @@ async function handleOrgs(session, flags) {
2252
2307
  ]);
2253
2308
  }
2254
2309
 
2310
+ async function handleWhoami(session, flags) {
2311
+ if (!getProfileName(session)) {
2312
+ throw new CliError('Nenhum perfil ativo configurado. Crie um perfil com "zenifra profile add" ou autentique com "zenifra auth login".');
2313
+ }
2314
+ const profile = profileOutput(session, getStore(session).activeProfile);
2315
+ const effectiveCredential = resolveCredential(session);
2316
+ const usingApiKey = effectiveCredential?.type === 'api_key';
2317
+ const selectedOrganizationId = usingApiKey ? null : profile.selected_organization_id || null;
2318
+ let organizationName = null;
2319
+
2320
+ if (selectedOrganizationId && !hasApiKeyCredential(session) && session.accessToken) {
2321
+ const organizations = await getOrganizations(session, flags);
2322
+ organizationName = organizations.find((organization) => idOf(organization) === selectedOrganizationId)?.name
2323
+ || organizations.find((organization) => idOf(organization) === selectedOrganizationId)?.organization?.name
2324
+ || null;
2325
+ }
2326
+
2327
+ const output = {
2328
+ profile: profile.name,
2329
+ api_base_url: apiBaseUrl(session, flags),
2330
+ authentication_mode: usingApiKey ? 'api_key' : profile.auth_mode,
2331
+ selected_organization_id: selectedOrganizationId,
2332
+ organization_name: organizationName,
2333
+ };
2334
+
2335
+ if (flags.json) return printJson(output);
2336
+ process.stdout.write(`Perfil: ${output.profile}\n`);
2337
+ process.stdout.write(`API base: ${output.api_base_url}\n`);
2338
+ process.stdout.write(`Autenticacao: ${output.authentication_mode}\n`);
2339
+ process.stdout.write(`Organizacao: ${output.organization_name ? `${output.organization_name} (${selectedOrganizationId})` : selectedOrganizationId || 'nao selecionada'}\n`);
2340
+ }
2341
+
2255
2342
  async function handleOrgSet(session, flags) {
2256
2343
  requireUserSession(session, 'Selecionar organizacao');
2257
2344
 
@@ -2679,6 +2766,22 @@ function printValkeyCatalog(catalog, profileFilter) {
2679
2766
  });
2680
2767
  }
2681
2768
 
2769
+ const HTTP_CAPABILITY_LABELS = {
2770
+ logs: 'logs',
2771
+ metrics: 'metricas',
2772
+ healthcheck: 'verificacao de saude',
2773
+ autoscaling: 'auto-scaling',
2774
+ custom_subdomain: 'subdominio personalizado',
2775
+ network_access: 'acesso de rede',
2776
+ };
2777
+
2778
+ function formatPlanCapabilities(capabilities) {
2779
+ const available = Object.entries(HTTP_CAPABILITY_LABELS)
2780
+ .filter(([key]) => capabilities?.[key] === true)
2781
+ .map(([, label]) => label);
2782
+ return available.join(', ') || '-';
2783
+ }
2784
+
2682
2785
  function printPlansCatalogs(payload, type) {
2683
2786
  if (type === 'all' || type === 'http') {
2684
2787
  process.stdout.write('HTTP\n');
@@ -2688,6 +2791,7 @@ function printPlansCatalogs(payload, type) {
2688
2791
  { label: 'Mes', value: (plan) => formatBrlFromCents(plan.prices?.monthly) },
2689
2792
  { label: 'Ano', value: (plan) => formatBrlFromCents(plan.prices?.yearly) },
2690
2793
  { label: 'Recursos', value: (plan) => asArray(plan.features).join(', ') || '-' },
2794
+ { label: 'Capacidades', value: (plan) => formatPlanCapabilities(plan.capabilities) },
2691
2795
  ]);
2692
2796
  }
2693
2797
 
@@ -3493,6 +3597,7 @@ async function handleProjectCreate(session, flags) {
3493
3597
  const plan = wizardPayload?.plan || flags.plan || await prompt('Plano');
3494
3598
  const paymentMode = wizardPayload?.payment_mode || flags.paymentMode || await prompt('Modo de pagamento');
3495
3599
  const config = wizardPayload?.config || await parseConfig(flags.config || await prompt('Config JSON ou @arquivo'));
3600
+ const idempotencyKey = flags.idempotencyKey === undefined ? undefined : validateIdempotencyKey(flags.idempotencyKey);
3496
3601
  if (Array.isArray(config?.custom_domains)) {
3497
3602
  config.custom_domains = normalizeCustomDomains(config.custom_domains, { primaryDomain: config.domain });
3498
3603
  }
@@ -3500,14 +3605,11 @@ async function handleProjectCreate(session, flags) {
3500
3605
  const valkeyCatalog = typeProject === 'valkey'
3501
3606
  ? requireValkeyCatalog(unwrapData(await request(session, flags, 'GET', '/managed-services/catalog')))
3502
3607
  : undefined;
3503
- if (flags.idempotencyKey !== undefined && !/^[A-Za-z0-9._-]{16,200}$/.test(String(flags.idempotencyKey))) {
3504
- throw new CliError('--idempotency-key deve ter entre 16 e 200 caracteres: letras, numeros, ponto, underscore ou hifen.');
3505
- }
3506
3608
  const validated = validateCreateInput({ plan, paymentMode, config, valkeyCatalog });
3507
3609
 
3508
3610
  const payload = await request(session, flags, 'POST', '/project', {
3509
3611
  orgId,
3510
- headers: flags.idempotencyKey === undefined ? {} : { 'Idempotency-Key': String(flags.idempotencyKey) },
3612
+ headers: idempotencyKey === undefined ? {} : { 'Idempotency-Key': idempotencyKey },
3511
3613
  body: {
3512
3614
  name,
3513
3615
  ...(description ? { description: String(description) } : {}),
@@ -3559,10 +3661,37 @@ async function handleProjectInfo(session, flags) {
3559
3661
  const orgId = await resolveOrgId(session, flags);
3560
3662
  const project = await getProject(session, flags, projectId, orgId);
3561
3663
 
3562
- if (flags.json) return printJson(project);
3664
+ if (flags.json) return printJson(projectInfoForOutput(project));
3563
3665
  printProject({ ...project, id: projectId });
3564
3666
  }
3565
3667
 
3668
+ async function handleProjectLifecycleMutation(session, flags, action) {
3669
+ const commandKey = `project ${action}`;
3670
+ const projectId = requireProjectId(flags, commandKey);
3671
+ if (!projectId) return;
3672
+
3673
+ const orgId = await resolveOrgId(session, flags);
3674
+ await request(session, flags, 'PATCH', `/project/${projectId}/${action}`, { orgId });
3675
+ const project = await getProject(session, flags, projectId, orgId);
3676
+
3677
+ if (flags.json) return printJson(projectInfoForOutput(project));
3678
+ printProject({ ...project, id: projectId });
3679
+ }
3680
+
3681
+ async function handleProjectDelete(session, flags) {
3682
+ const projectId = requireProjectId(flags, 'project delete');
3683
+ if (!projectId) return;
3684
+ if (flags.yes !== true) {
3685
+ throw new CliError('A remocao exige --yes. Nenhuma solicitacao foi enviada.');
3686
+ }
3687
+
3688
+ const orgId = await resolveOrgId(session, flags);
3689
+ const payload = await request(session, flags, 'DELETE', `/project/${projectId}`, { orgId });
3690
+
3691
+ if (flags.json) return printJson(payload);
3692
+ process.stdout.write('Projeto removido.\n');
3693
+ }
3694
+
3566
3695
  async function handleProjectUrl(session, flags) {
3567
3696
  const projectId = requireProjectId(flags, 'project url');
3568
3697
  if (!projectId) return
@@ -4250,10 +4379,7 @@ async function handleValkeyCredentialStatus(session, flags) {
4250
4379
  async function handleValkeyCredentialRotate(session, flags) {
4251
4380
  const projectId = requireProjectId(flags, 'valkey credentials rotate');
4252
4381
  if (!projectId) return;
4253
- const idempotencyKey = String(flags.idempotencyKey || randomUUID());
4254
- if (!/^[A-Za-z0-9._-]{16,200}$/.test(idempotencyKey)) {
4255
- throw new CliError('--idempotency-key deve ter entre 16 e 200 caracteres: letras, numeros, ponto, underscore ou hifen.');
4256
- }
4382
+ const idempotencyKey = validateIdempotencyKey(flags.idempotencyKey === undefined ? randomUUID() : flags.idempotencyKey);
4257
4383
  const orgId = await resolveOrgId(session, flags);
4258
4384
  const acceptedPayload = await request(session, flags, 'PATCH', `/managed-services/${projectId}/credentials`, {
4259
4385
  orgId,
@@ -4364,8 +4490,25 @@ function parseSecondsOption(value, flagName, { defaultValue, min }) {
4364
4490
  return parsed;
4365
4491
  }
4366
4492
 
4493
+ function validateIdempotencyKey(value) {
4494
+ const key = String(value);
4495
+ if (key.length < 16 || key.length > 200) {
4496
+ throw new CliError('--idempotency-key deve ter entre 16 e 200 caracteres.');
4497
+ }
4498
+
4499
+ const invalidIndex = Array.from(key).findIndex((character) => !/[A-Za-z0-9._-]/.test(character));
4500
+ if (invalidIndex !== -1) {
4501
+ throw new CliError(`--idempotency-key contem caractere invalido na posicao ${invalidIndex + 1}. Use apenas letras, numeros, ponto, underscore ou hifen.`);
4502
+ }
4503
+
4504
+ return key;
4505
+ }
4506
+
4367
4507
  function printBuildLogs(logs) {
4368
4508
  for (const log of asArray(logs)) {
4509
+ if (log?.source === 'summary') {
4510
+ process.stdout.write('Este build possui apenas um resumo; eventos detalhados nao estavam disponiveis.\n');
4511
+ }
4369
4512
  process.stdout.write(`${buildLogLineOf(log)}\n`)
4370
4513
  }
4371
4514
  }
@@ -4509,6 +4652,7 @@ async function main() {
4509
4652
  if (command === 'plans') return handlePlans(session, flags);
4510
4653
  if (command === 'create' && subcommand === 'project') return handleProjectCreate(session, flags);
4511
4654
  if (command === 'orgs') return handleOrgs(session, flags);
4655
+ if (command === 'whoami') return handleWhoami(session, flags);
4512
4656
  if (command === 'org' && subcommand === 'set') return handleOrgSet(session, flags);
4513
4657
  if (command === 'projects') return handleProjects(session, flags);
4514
4658
  if (command === 'valkey' && subcommand === 'credentials' && positional.length === 2) {
@@ -4528,6 +4672,9 @@ async function main() {
4528
4672
  if (command === 'project' && subcommand === 'healthcheck' && positional[2] === 'disable') return handleProjectHealthcheckDisable(session, flags);
4529
4673
  if (command === 'project' && subcommand === 'healthcheck' && positional[2] === 'failures') return handleProjectHealthcheckFailures(session, flags);
4530
4674
  if (command === 'project' && subcommand === 'info') return handleProjectInfo(session, flags);
4675
+ if (command === 'project' && subcommand === 'stop') return handleProjectLifecycleMutation(session, flags, 'stop');
4676
+ if (command === 'project' && subcommand === 'resume') return handleProjectLifecycleMutation(session, flags, 'resume');
4677
+ if (command === 'project' && subcommand === 'delete') return handleProjectDelete(session, flags);
4531
4678
  if (command === 'project' && subcommand === 'url') return handleProjectUrl(session, flags);
4532
4679
  if (command === 'project' && subcommand === 'logs') return handleProjectLogs(session, flags);
4533
4680
  if (command === 'project' && subcommand === 'metrics' && positional[2] === 'capabilities') return handleProjectMetricsCapabilities(session, flags);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenifra/cli",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Official Zenifra command line interface.",
5
5
  "type": "module",
6
6
  "license": "MIT",