@zenifra/cli 0.3.2 → 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',
@@ -2264,6 +2307,38 @@ async function handleOrgs(session, flags) {
2264
2307
  ]);
2265
2308
  }
2266
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
+
2267
2342
  async function handleOrgSet(session, flags) {
2268
2343
  requireUserSession(session, 'Selecionar organizacao');
2269
2344
 
@@ -2691,6 +2766,22 @@ function printValkeyCatalog(catalog, profileFilter) {
2691
2766
  });
2692
2767
  }
2693
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
+
2694
2785
  function printPlansCatalogs(payload, type) {
2695
2786
  if (type === 'all' || type === 'http') {
2696
2787
  process.stdout.write('HTTP\n');
@@ -2700,6 +2791,7 @@ function printPlansCatalogs(payload, type) {
2700
2791
  { label: 'Mes', value: (plan) => formatBrlFromCents(plan.prices?.monthly) },
2701
2792
  { label: 'Ano', value: (plan) => formatBrlFromCents(plan.prices?.yearly) },
2702
2793
  { label: 'Recursos', value: (plan) => asArray(plan.features).join(', ') || '-' },
2794
+ { label: 'Capacidades', value: (plan) => formatPlanCapabilities(plan.capabilities) },
2703
2795
  ]);
2704
2796
  }
2705
2797
 
@@ -3505,6 +3597,7 @@ async function handleProjectCreate(session, flags) {
3505
3597
  const plan = wizardPayload?.plan || flags.plan || await prompt('Plano');
3506
3598
  const paymentMode = wizardPayload?.payment_mode || flags.paymentMode || await prompt('Modo de pagamento');
3507
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);
3508
3601
  if (Array.isArray(config?.custom_domains)) {
3509
3602
  config.custom_domains = normalizeCustomDomains(config.custom_domains, { primaryDomain: config.domain });
3510
3603
  }
@@ -3512,14 +3605,11 @@ async function handleProjectCreate(session, flags) {
3512
3605
  const valkeyCatalog = typeProject === 'valkey'
3513
3606
  ? requireValkeyCatalog(unwrapData(await request(session, flags, 'GET', '/managed-services/catalog')))
3514
3607
  : 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
3608
  const validated = validateCreateInput({ plan, paymentMode, config, valkeyCatalog });
3519
3609
 
3520
3610
  const payload = await request(session, flags, 'POST', '/project', {
3521
3611
  orgId,
3522
- headers: flags.idempotencyKey === undefined ? {} : { 'Idempotency-Key': String(flags.idempotencyKey) },
3612
+ headers: idempotencyKey === undefined ? {} : { 'Idempotency-Key': idempotencyKey },
3523
3613
  body: {
3524
3614
  name,
3525
3615
  ...(description ? { description: String(description) } : {}),
@@ -3575,6 +3665,33 @@ async function handleProjectInfo(session, flags) {
3575
3665
  printProject({ ...project, id: projectId });
3576
3666
  }
3577
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
+
3578
3695
  async function handleProjectUrl(session, flags) {
3579
3696
  const projectId = requireProjectId(flags, 'project url');
3580
3697
  if (!projectId) return
@@ -4262,10 +4379,7 @@ async function handleValkeyCredentialStatus(session, flags) {
4262
4379
  async function handleValkeyCredentialRotate(session, flags) {
4263
4380
  const projectId = requireProjectId(flags, 'valkey credentials rotate');
4264
4381
  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
- }
4382
+ const idempotencyKey = validateIdempotencyKey(flags.idempotencyKey === undefined ? randomUUID() : flags.idempotencyKey);
4269
4383
  const orgId = await resolveOrgId(session, flags);
4270
4384
  const acceptedPayload = await request(session, flags, 'PATCH', `/managed-services/${projectId}/credentials`, {
4271
4385
  orgId,
@@ -4376,8 +4490,25 @@ function parseSecondsOption(value, flagName, { defaultValue, min }) {
4376
4490
  return parsed;
4377
4491
  }
4378
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
+
4379
4507
  function printBuildLogs(logs) {
4380
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
+ }
4381
4512
  process.stdout.write(`${buildLogLineOf(log)}\n`)
4382
4513
  }
4383
4514
  }
@@ -4521,6 +4652,7 @@ async function main() {
4521
4652
  if (command === 'plans') return handlePlans(session, flags);
4522
4653
  if (command === 'create' && subcommand === 'project') return handleProjectCreate(session, flags);
4523
4654
  if (command === 'orgs') return handleOrgs(session, flags);
4655
+ if (command === 'whoami') return handleWhoami(session, flags);
4524
4656
  if (command === 'org' && subcommand === 'set') return handleOrgSet(session, flags);
4525
4657
  if (command === 'projects') return handleProjects(session, flags);
4526
4658
  if (command === 'valkey' && subcommand === 'credentials' && positional.length === 2) {
@@ -4540,6 +4672,9 @@ async function main() {
4540
4672
  if (command === 'project' && subcommand === 'healthcheck' && positional[2] === 'disable') return handleProjectHealthcheckDisable(session, flags);
4541
4673
  if (command === 'project' && subcommand === 'healthcheck' && positional[2] === 'failures') return handleProjectHealthcheckFailures(session, flags);
4542
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);
4543
4678
  if (command === 'project' && subcommand === 'url') return handleProjectUrl(session, flags);
4544
4679
  if (command === 'project' && subcommand === 'logs') return handleProjectLogs(session, flags);
4545
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.2",
3
+ "version": "0.3.3",
4
4
  "description": "Official Zenifra command line interface.",
5
5
  "type": "module",
6
6
  "license": "MIT",