@zenifra/cli 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +36 -0
  2. package/bin/zenifra.mjs +175 -15
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -72,6 +72,7 @@ zenifra valkey credentials status --project <project-id> --operation <operation-
72
72
  zenifra project url --project <project-id>
73
73
  zenifra project logs --project <project-id> --instance <instance-id>
74
74
  zenifra project metrics --project <project-id> --instance <instance-id>
75
+ zenifra project metrics capabilities --project <project-id>
75
76
  zenifra project network --project <project-id> --view summary
76
77
  zenifra project image set --project <project-id> --image ghcr.io/zenifra/app:tag
77
78
  zenifra project envs --project <project-id>
@@ -93,6 +94,41 @@ zenifra deploy --project <project-id> --branch main
93
94
  zenifra deploy watch --project <project-id> --build <build-id>
94
95
  ```
95
96
 
97
+ ## Metricas de projetos Valkey
98
+
99
+ O comando `zenifra project metrics` mostra as metricas do projeto e, para projetos Valkey, o snapshot da instancia selecionada:
100
+
101
+ ```bash
102
+ zenifra project metrics --project <project-id> --instance instance-1
103
+ ```
104
+
105
+ A saida legivel inclui:
106
+
107
+ - recursos: CPU e memoria da instancia;
108
+ - capacidade: memoria usada, pico, limite e fragmentacao;
109
+ - clientes conectados e bloqueados;
110
+ - atividade: operacoes por segundo e trafego de entrada/saida;
111
+ - ciclo de vida: chaves expiradas e removidas;
112
+ - uptime e perfil (`cache`, `key-value` ou `queue`);
113
+ - confiabilidade: replicacao e persistencia, quando disponiveis.
114
+
115
+ Perfis especificos podem incluir:
116
+
117
+ - `cache`: hits, misses e hit ratio;
118
+ - `key-value`: total de chaves e chaves com expiracao;
119
+ - `queue`: metricas comuns, sem estimar profundidade quando ela nao esta disponivel.
120
+
121
+ Para consultar o nivel de acesso e os grupos disponiveis antes de buscar um snapshot:
122
+
123
+ ```bash
124
+ zenifra project metrics capabilities --project <project-id>
125
+ zenifra project metrics capabilities --project <project-id> --json
126
+ ```
127
+
128
+ `--json` preserva a resposta publica da API. Valores ausentes sao exibidos como `indisponivel` no modo legivel; `0` continua sendo um valor valido. O historico nao esta disponivel neste momento.
129
+
130
+ ---
131
+
96
132
  ## Builds GitHub
97
133
 
98
134
  Use `zenifra builds` para listar o historico de builds e `zenifra builds logs` para ler os logs do pipeline GitHub de um build especifico.
package/bin/zenifra.mjs CHANGED
@@ -181,6 +181,7 @@ Usage:
181
181
  zenifra project url --project <id> [--json]
182
182
  zenifra project logs --project <id> [--instance <id>] [--json]
183
183
  zenifra project metrics --project <id> [--instance <id>] [--json]
184
+ zenifra project metrics capabilities --project <id> [--json]
184
185
  zenifra project network --project <id> [--view <summary|status-codes|routes|user-agents|request-events|source-ips>] [--json]
185
186
  zenifra project image set --project <id> --image <image> [--json]
186
187
  zenifra project exposure set --project <id> --exposure <public|private> [--json]
@@ -416,11 +417,11 @@ const HELP_SPECS = [
416
417
  },
417
418
  {
418
419
  command: 'project',
419
- usage: 'zenifra project\n zenifra project info --project <id> [--json]\n zenifra project url --project <id> [--json]\n zenifra project logs --project <id> [--instance <id>] [--json]\n zenifra project metrics --project <id> [--instance <id>] [--json]\n zenifra project network --project <id> [--view <summary|status-codes|routes|user-agents|request-events|source-ips>] [--json]\n zenifra project image set --project <id> --image <image> [--json]\n zenifra project exposure set --project <id> --exposure <public|private> [--json]\n zenifra project envs --project <id> [--json] [--show-values]\n zenifra project env add --project <id> --name <name> --value <value> [--json]\n zenifra project env update --project <id> --name <name> --value <value> [--json]\n zenifra project env remove --project <id> --name <name> [--json]\n zenifra project autoscaling --project <id> [--json]\n zenifra project autoscaling set --project <id> --min <n> --max <n> [--cpu <percent>] [--memory <percent>] [--json]\n zenifra project autoscaling disable --project <id> [--json]\n zenifra project autoscaling events --project <id> [--direction <scale_up|scale_down>] [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]\n zenifra project billing usage --project <id> [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]\n zenifra project instances --project <id> [--json]\n zenifra project instances set --project <id> --count <n> [--json]',
420
+ usage: 'zenifra project\n zenifra project info --project <id> [--json]\n zenifra project url --project <id> [--json]\n zenifra project logs --project <id> [--instance <id>] [--json]\n zenifra project metrics --project <id> [--instance <id>] [--json]\n zenifra project metrics capabilities --project <id> [--json]\n zenifra project network --project <id> [--view <summary|status-codes|routes|user-agents|request-events|source-ips>] [--json]\n zenifra project image set --project <id> --image <image> [--json]\n zenifra project exposure set --project <id> --exposure <public|private> [--json]\n zenifra project envs --project <id> [--json] [--show-values]\n zenifra project env add --project <id> --name <name> --value <value> [--json]\n zenifra project env update --project <id> --name <name> --value <value> [--json]\n zenifra project env remove --project <id> --name <name> [--json]\n zenifra project autoscaling --project <id> [--json]\n zenifra project autoscaling set --project <id> --min <n> --max <n> [--cpu <percent>] [--memory <percent>] [--json]\n zenifra project autoscaling disable --project <id> [--json]\n zenifra project autoscaling events --project <id> [--direction <scale_up|scale_down>] [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]\n zenifra project billing usage --project <id> [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]\n zenifra project instances --project <id> [--json]\n zenifra project instances set --project <id> --count <n> [--json]',
420
421
  description: 'Agrupa comandos operacionais e de introspecao sobre um projeto especifico.',
421
422
  examples: ['zenifra project', 'zenifra project info --project proj_1', 'zenifra project env add --project proj_1 --name NODE_ENV --value production'],
422
423
  output: 'Zenifra CLI - project',
423
- notes: ['Use "zenifra help project <subcomando>" para detalhes de info, url, logs, metrics, network, image, exposure, autoscaling, billing, envs e instances.'],
424
+ notes: ['Use "zenifra help project <subcomando>" para detalhes de info, url, logs, metrics, capabilities, network, image, exposure, autoscaling, billing, envs e instances.'],
424
425
  },
425
426
  {
426
427
  command: 'project info',
@@ -453,11 +454,26 @@ const HELP_SPECS = [
453
454
  {
454
455
  command: 'project metrics',
455
456
  usage: 'zenifra project metrics --project <id> [--instance <id>] [--json]',
456
- description: 'Mostra CPU, memoria e dados basicos de rede do projeto.',
457
- flags: ['--project <id> ID do projeto.', '--instance <id> Filtra uma instancia.', '--json Imprime a resposta em JSON.'],
458
- examples: ['zenifra project metrics --project 507f1f77bcf86cd799439012 --instance web-1'],
459
- output: 'Instancia Tipo CPU Memoria\nweb-1 application 0.2 64',
460
- jsonOutput: '{"type":"application","instance":"web-1","cpu":0.2,"memory":64,"network":{"requests":120}}',
457
+ description: 'Mostra metricas de recursos do projeto e, para Valkey, o snapshot nativo da instancia.',
458
+ flags: ['--project <id> ID do projeto.', '--instance <id> Filtra uma instancia.', '--json Imprime a resposta em JSON sem formatacao adicional.'],
459
+ examples: ['zenifra project metrics --project 507f1f77bcf86cd799439012 --instance web-1', 'zenifra project metrics --project 507f1f77bcf86cd799439012 --instance instance-1'],
460
+ output: 'Campo Valor\n---------- ----------------\nPerfil cache\nDisponibilidade available\nMemoria usada 100 MB',
461
+ jsonOutput: '{"instance":"instance-1","type":"valkey","availability":"available","valkey":{"schema_version":1,"profile":"cache","memory":{"used_bytes":104857600}}}',
462
+ notes: [
463
+ 'Para projetos Valkey, a saida legivel inclui recursos, capacidade, clientes, atividade, ciclo de vida de chaves, perfil e confiabilidade.',
464
+ 'Valores ausentes sao exibidos como indisponivel; zero continua sendo um valor valido.',
465
+ 'Use "zenifra project metrics capabilities --project <id>" para consultar o acesso e os grupos de metricas disponiveis.',
466
+ ],
467
+ },
468
+ {
469
+ command: 'project metrics capabilities',
470
+ usage: 'zenifra project metrics capabilities --project <id> [--json]',
471
+ description: 'Mostra os grupos de metricas e o nivel de acesso disponivel para o projeto.',
472
+ flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.'],
473
+ examples: ['zenifra project metrics capabilities --project 507f1f77bcf86cd799439012'],
474
+ output: 'Campo Valor\n---------- ------------------------------------------------------------\nAcesso snapshot\nAtualizacao 60 s\nHistorico indisponivel',
475
+ jsonOutput: '{"access":"snapshot","groups":["resources","capacity","clients","activity","key_lifecycle","profile","reliability"],"refresh_seconds":60,"history":null}',
476
+ notes: ['A API permanece como fonte de autorizacao. A CLI nao mantem uma lista local de planos.'],
461
477
  },
462
478
  {
463
479
  command: 'project network',
@@ -1486,6 +1502,9 @@ async function request(session, flags, method, path, {
1486
1502
  if (response.status === 429 && Number.isFinite(Number(payload?.retry_after_seconds))) {
1487
1503
  throw new CliError(`${message} Tente novamente em ${Number(payload.retry_after_seconds)} segundo(s).`);
1488
1504
  }
1505
+ if (response.status === 402 && path.includes('/metrics') && /support metrics/i.test(String(message))) {
1506
+ throw new CliError('Este projeto nao possui acesso a metricas.');
1507
+ }
1489
1508
  if (String(message).includes('missing x-organization-id')) {
1490
1509
  throw new CliError('Organizacao nao selecionada. Rode "zenifra org set" ou use --org <id>.');
1491
1510
  }
@@ -1526,6 +1545,128 @@ function printTable(rows, columns) {
1526
1545
  }
1527
1546
  }
1528
1547
 
1548
+ function formatValkeyNumber(value) {
1549
+ if (value === null || value === undefined || !Number.isFinite(Number(value))) return 'indisponivel';
1550
+ return String(value);
1551
+ }
1552
+
1553
+ function formatValkeyBytes(value, suffix = '') {
1554
+ if (value === null || value === undefined || !Number.isFinite(Number(value))) return 'indisponivel';
1555
+
1556
+ const numericValue = Number(value);
1557
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
1558
+ let unitIndex = 0;
1559
+ let normalizedValue = numericValue;
1560
+ while (normalizedValue >= 1024 && unitIndex < units.length - 1) {
1561
+ normalizedValue /= 1024;
1562
+ unitIndex += 1;
1563
+ }
1564
+
1565
+ const digits = normalizedValue >= 100 ? 0 : normalizedValue >= 10 ? 1 : 2;
1566
+ const formattedValue = Number(normalizedValue.toFixed(digits)).toString();
1567
+ return `${formattedValue} ${units[unitIndex]}${suffix}`;
1568
+ }
1569
+
1570
+ function formatValkeyPercent(value) {
1571
+ if (value === null || value === undefined || !Number.isFinite(Number(value))) return 'indisponivel';
1572
+ return `${Number((Number(value) * 100).toFixed(2))}%`;
1573
+ }
1574
+
1575
+ function valkeyMetricRows(metrics) {
1576
+ const native = metrics?.valkey;
1577
+ const rows = [
1578
+ { field: 'Instancia', value: metrics?.instance || '-' },
1579
+ { field: 'Disponibilidade', value: metrics?.availability || 'indisponivel' },
1580
+ { field: 'Observado em', value: metrics?.observed_at || 'indisponivel' },
1581
+ { field: 'CPU', value: formatValkeyNumber(metrics?.cpu) },
1582
+ { field: 'Memoria', value: formatValkeyBytes(metrics?.memory) },
1583
+ ];
1584
+
1585
+ if (!native) {
1586
+ rows.push({ field: 'Metricas Valkey', value: 'indisponivel' });
1587
+ return rows;
1588
+ }
1589
+
1590
+ rows.push(
1591
+ { field: 'Perfil', value: native.profile || '-' },
1592
+ { field: 'Memoria usada', value: formatValkeyBytes(native.memory?.used_bytes) },
1593
+ { field: 'Pico de memoria', value: formatValkeyBytes(native.memory?.peak_bytes) },
1594
+ { field: 'Limite de memoria', value: formatValkeyBytes(native.memory?.limit_bytes) },
1595
+ { field: 'Fragmentacao', value: formatValkeyNumber(native.memory?.fragmentation_ratio) },
1596
+ { field: 'Clientes conectados', value: formatValkeyNumber(native.clients?.connected) },
1597
+ { field: 'Clientes bloqueados', value: formatValkeyNumber(native.clients?.blocked) },
1598
+ { field: 'Operacoes por segundo', value: formatValkeyNumber(native.activity?.operations_per_second) },
1599
+ { field: 'Entrada', value: formatValkeyBytes(native.activity?.input_bytes_per_second, '/s') },
1600
+ { field: 'Saida', value: formatValkeyBytes(native.activity?.output_bytes_per_second, '/s') },
1601
+ { field: 'Chaves expiradas', value: formatValkeyNumber(native.keys?.expired_total) },
1602
+ { field: 'Chaves removidas', value: formatValkeyNumber(native.keys?.evicted_total) },
1603
+ { field: 'Uptime', value: `${formatValkeyNumber(native.uptime_seconds)} s` },
1604
+ );
1605
+
1606
+ if (native.cache) {
1607
+ rows.push(
1608
+ { field: 'Cache hits', value: formatValkeyNumber(native.cache.hits_total) },
1609
+ { field: 'Cache misses', value: formatValkeyNumber(native.cache.misses_total) },
1610
+ { field: 'Hit ratio', value: formatValkeyPercent(native.cache.hit_ratio) },
1611
+ );
1612
+ }
1613
+
1614
+ if (native.key_value) {
1615
+ rows.push(
1616
+ { field: 'Total de chaves', value: formatValkeyNumber(native.key_value.keys_total) },
1617
+ { field: 'Chaves com expiracao', value: formatValkeyNumber(native.key_value.keys_with_expiration) },
1618
+ );
1619
+ }
1620
+
1621
+ if (native.reliability) {
1622
+ const replication = native.reliability.replication;
1623
+ const persistence = native.reliability.persistence;
1624
+ if (replication) {
1625
+ rows.push(
1626
+ { field: 'Replicacao', value: replication.status || 'indisponivel' },
1627
+ { field: 'Repl. disponiveis', value: formatValkeyNumber(replication.replicas_available) },
1628
+ { field: 'Repl. esperadas', value: formatValkeyNumber(replication.replicas_expected) },
1629
+ { field: 'Lag', value: replication.lag_seconds === null || replication.lag_seconds === undefined ? 'indisponivel' : `${replication.lag_seconds} s` },
1630
+ );
1631
+ }
1632
+ if (persistence) {
1633
+ rows.push(
1634
+ { field: 'Persistencia', value: persistence.status || 'indisponivel' },
1635
+ { field: 'Persistencia ativa', value: persistence.enabled === null || persistence.enabled === undefined ? 'indisponivel' : persistence.enabled ? 'sim' : 'nao' },
1636
+ { field: 'Ultimo sucesso', value: persistence.last_success_at || 'indisponivel' },
1637
+ );
1638
+ }
1639
+ }
1640
+
1641
+ return rows;
1642
+ }
1643
+
1644
+ function printValkeyMetrics(metrics) {
1645
+ printTable(valkeyMetricRows(metrics), [
1646
+ { label: 'Campo', value: (row) => row.field },
1647
+ { label: 'Valor', value: (row) => row.value },
1648
+ ]);
1649
+ }
1650
+
1651
+ function printMetricsCapabilities(capabilities) {
1652
+ const groups = Array.isArray(capabilities?.groups) && capabilities.groups.length > 0
1653
+ ? capabilities.groups.join(', ')
1654
+ : 'nenhum';
1655
+ const history = capabilities?.history
1656
+ ? `${capabilities.history.retention_seconds} s de retencao`
1657
+ : 'indisponivel';
1658
+
1659
+ printTable([
1660
+ { field: 'Acesso', value: capabilities?.access || 'indisponivel' },
1661
+ { field: 'Grupos', value: groups },
1662
+ { field: 'Atualizacao', value: capabilities?.refresh_seconds === null || capabilities?.refresh_seconds === undefined ? 'indisponivel' : `${capabilities.refresh_seconds} s` },
1663
+ { field: 'Historico', value: history },
1664
+ ], [
1665
+ { label: 'Campo', value: (row) => row.field },
1666
+ { label: 'Valor', value: (row) => row.value },
1667
+ ]);
1668
+ }
1669
+
1529
1670
  function asArray(value) {
1530
1671
  if (Array.isArray(value)) return value;
1531
1672
  if (Array.isArray(value?.projects)) return value.projects;
@@ -2230,6 +2371,11 @@ function formatBrl(value) {
2230
2371
  }).format(Number.isFinite(number) ? number : 0);
2231
2372
  }
2232
2373
 
2374
+ function formatBrlFromCents(value) {
2375
+ const number = Number(value ?? 0);
2376
+ return formatBrl(Number.isFinite(number) ? number / 100 : 0);
2377
+ }
2378
+
2233
2379
  function humanizeStorageName(value) {
2234
2380
  const raw = String(value || '').trim();
2235
2381
  if (!raw) return { storage: '-', type: '-' };
@@ -2377,9 +2523,9 @@ function printPlansCatalogs(payload, type) {
2377
2523
  process.stdout.write('HTTP\n');
2378
2524
  printTable(asArray(payload.http), [
2379
2525
  { label: 'Plano', value: (plan) => plan.plan || '-' },
2380
- { label: 'Hora', value: (plan) => formatBrl(plan.prices?.hourly) },
2381
- { label: 'Mes', value: (plan) => formatBrl(plan.prices?.monthly) },
2382
- { label: 'Ano', value: (plan) => formatBrl(plan.prices?.yearly) },
2526
+ { label: 'Hora', value: (plan) => formatBrlFromCents(plan.prices?.hourly) },
2527
+ { label: 'Mes', value: (plan) => formatBrlFromCents(plan.prices?.monthly) },
2528
+ { label: 'Ano', value: (plan) => formatBrlFromCents(plan.prices?.yearly) },
2383
2529
  { label: 'Recursos', value: (plan) => asArray(plan.features).join(', ') || '-' },
2384
2530
  ]);
2385
2531
  }
@@ -2389,9 +2535,9 @@ function printPlansCatalogs(payload, type) {
2389
2535
  process.stdout.write('PostgreSQL / MariaDB\n');
2390
2536
  printTable(asArray(payload.database), [
2391
2537
  { label: 'Plano', value: (plan) => plan.plan || '-' },
2392
- { label: 'Hora', value: (plan) => formatBrl(plan.prices?.hourly) },
2393
- { label: 'Mes', value: (plan) => formatBrl(plan.prices?.monthly) },
2394
- { label: 'Ano', value: (plan) => formatBrl(plan.prices?.yearly) },
2538
+ { label: 'Hora', value: (plan) => formatBrlFromCents(plan.prices?.hourly) },
2539
+ { label: 'Mes', value: (plan) => formatBrlFromCents(plan.prices?.monthly) },
2540
+ { label: 'Ano', value: (plan) => formatBrlFromCents(plan.prices?.yearly) },
2395
2541
  { label: 'Recursos', value: (plan) => asArray(plan.features).join(', ') || '-' },
2396
2542
  ]);
2397
2543
  }
@@ -3281,6 +3427,16 @@ async function handleProjectLogs(session, flags) {
3281
3427
  printLogs(logs, flags);
3282
3428
  }
3283
3429
 
3430
+ async function handleProjectMetricsCapabilities(session, flags) {
3431
+ const projectId = requireProjectId(flags, 'project metrics capabilities');
3432
+ if (!projectId) return;
3433
+ const orgId = await resolveOrgId(session, flags);
3434
+ const capabilities = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/metrics/capabilities`, { orgId }));
3435
+
3436
+ if (flags.json) return printJson(capabilities);
3437
+ printMetricsCapabilities(capabilities);
3438
+ }
3439
+
3284
3440
  async function handleProjectMetrics(session, flags) {
3285
3441
  const projectId = requireProjectId(flags, 'project metrics');
3286
3442
  if (!projectId) return
@@ -3289,6 +3445,9 @@ async function handleProjectMetrics(session, flags) {
3289
3445
  const metrics = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/metrics${query}`, { orgId }));
3290
3446
 
3291
3447
  if (flags.json) return printJson(metrics);
3448
+ if (metrics?.type === 'valkey') {
3449
+ return printValkeyMetrics(metrics);
3450
+ }
3292
3451
  if (Array.isArray(metrics)) {
3293
3452
  return printTable(metrics, [
3294
3453
  { label: 'Instancia', value: (metric) => metric.instance || '-' },
@@ -4038,7 +4197,7 @@ async function main() {
4038
4197
  return;
4039
4198
  }
4040
4199
 
4041
- if (command === 'auth' && subcommand === 'login') return handleLogin(session, flags);
4200
+ if (command === 'auth' && subcommand === 'login') return await handleLogin(session, flags);
4042
4201
  if (command === 'auth' && subcommand === 'api-key') return handleApiKeyLogin(session, flags);
4043
4202
  if (command === 'auth' && subcommand === 'logout') return handleLogout(session, flags);
4044
4203
  if (command === 'profile' && subcommand === 'list') return handleProfileList(session, flags);
@@ -4047,7 +4206,7 @@ async function main() {
4047
4206
  if (command === 'profile' && subcommand === 'edit') return handleProfileEdit(session, flags, positional);
4048
4207
  if (command === 'profile' && subcommand === 'use') return handleProfileUse(session, flags, positional);
4049
4208
  if (command === 'profile' && subcommand === 'remove') return handleProfileRemove(session, flags, positional);
4050
- if (command === 'login') return handleLogin(session, flags);
4209
+ if (command === 'login') return await handleLogin(session, flags);
4051
4210
  if (command === 'logout') return handleLogout(session, flags);
4052
4211
  if (command === 'plans') return handlePlans(session, flags);
4053
4212
  if (command === 'create' && subcommand === 'project') return handleProjectCreate(session, flags);
@@ -4065,6 +4224,7 @@ async function main() {
4065
4224
  if (command === 'project' && subcommand === 'info') return handleProjectInfo(session, flags);
4066
4225
  if (command === 'project' && subcommand === 'url') return handleProjectUrl(session, flags);
4067
4226
  if (command === 'project' && subcommand === 'logs') return handleProjectLogs(session, flags);
4227
+ if (command === 'project' && subcommand === 'metrics' && positional[2] === 'capabilities') return handleProjectMetricsCapabilities(session, flags);
4068
4228
  if (command === 'project' && subcommand === 'metrics') return handleProjectMetrics(session, flags);
4069
4229
  if (command === 'project' && subcommand === 'network') return handleProjectNetwork(session, flags);
4070
4230
  if (command === 'project' && subcommand === 'image' && positional[2] === 'set') return handleProjectImageSet(session, flags);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenifra/cli",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Official Zenifra command line interface.",
5
5
  "type": "module",
6
6
  "license": "MIT",