@zenifra/cli 0.2.1 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +27 -4
  2. package/bin/zenifra.mjs +226 -56
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -56,7 +56,7 @@ zenifra create project --name <name> --plan free --payment-mode hourly --config
56
56
  zenifra create project --name <name> --plan basic --payment-mode hourly --config @examples/http-github-project.json
57
57
  zenifra create project --name <name> --plan db-basic --payment-mode monthly --config @examples/postgresql-project.json
58
58
  zenifra create project --name <name> --plan db-basic --payment-mode monthly --config @examples/mariadb-project.json
59
- zenifra projects --type http
59
+ zenifra projects --type http --page 1 --limit 15
60
60
  zenifra project info --project <project-id>
61
61
  zenifra project url --project <project-id>
62
62
  zenifra project logs --project <project-id> --instance <instance-id>
@@ -70,11 +70,34 @@ zenifra project env remove --project <project-id> --name NODE_ENV
70
70
  zenifra project instances --project <project-id>
71
71
  zenifra project instances set --project <project-id> --count 3
72
72
  zenifra builds --project <project-id>
73
+ zenifra builds logs --project <project-id> --build <build-id>
74
+ zenifra builds logs --project <project-id> --build <build-id> --follow
73
75
  zenifra deployments --project <project-id>
74
76
  zenifra deploy --project <project-id> --branch main
75
77
  zenifra deploy watch --project <project-id> --build <build-id>
76
78
  ```
77
79
 
80
+ ## Builds GitHub
81
+
82
+ Use `zenifra builds` para listar o historico de builds e `zenifra builds logs` para ler os logs do pipeline GitHub de um build especifico.
83
+
84
+ ```bash
85
+ zenifra builds --project <project-id>
86
+ zenifra builds logs --project <project-id> --build <build-id>
87
+ zenifra builds logs --project <project-id> --build <build-id> --follow
88
+ zenifra deploy --project <project-id> --branch main
89
+ zenifra deploy watch --project <project-id> --build <build-id>
90
+ ```
91
+
92
+ Fluxos:
93
+
94
+ - `zenifra project logs`: logs da aplicacao em execucao
95
+ - `zenifra builds logs`: logs do build GitHub
96
+ - `zenifra deploy`: dispara o build/deploy GitHub e retorna o `build_id`
97
+ - `zenifra deploy watch`: usa esse `build_id` para acompanhar o build em tempo real e imprimir os logs incrementais ate o fim
98
+
99
+ Se voce rodar apenas `zenifra deploy`, a CLI mostra a ajuda especifica do comando com uso, flags, exemplos e exemplo de retorno.
100
+
78
101
  ## Configuracao
79
102
 
80
103
  - API padrao: `https://api.zenifra.com/v1`
@@ -83,7 +106,7 @@ zenifra deploy watch --project <project-id> --build <build-id>
83
106
  - Store local de perfis: `~/.config/zenifra-cli/profiles.json`
84
107
  - Override do diretorio local: `ZENIFRA_CONFIG_DIR=/path/custom`
85
108
 
86
- Todos os comandos de listagem aceitam `--json`.
109
+ Todos os comandos de listagem aceitam `--json`. `zenifra projects` e paginado por padrao com 15 itens por pagina; use `--page <n>` e `--limit <n>` para navegar.
87
110
 
88
111
  Para comparar custo antes de criar um projeto, use:
89
112
 
@@ -137,7 +160,7 @@ Crie uma API key global no painel da organizacao e use-a em jobs, pipelines e sc
137
160
 
138
161
  ```bash
139
162
  export ZENIFRA_API_KEY=znf_sua_chave
140
- zenifra projects --type http
163
+ zenifra projects --type http --page 1 --limit 15
141
164
  zenifra deploy --project <project-id> --branch main
142
165
  ```
143
166
 
@@ -147,7 +170,7 @@ Tambem e possivel salvar a chave localmente no perfil ativo:
147
170
  zenifra auth api-key --key znf_sua_chave
148
171
  ```
149
172
 
150
- API keys globais ja sao vinculadas a uma organizacao, entao comandos como `projects`, `deploy`, `builds` e `deployments` nao precisam de `org set`. Comandos pessoais como `orgs` e `org set` continuam exigindo `zenifra auth login` em um perfil com token de usuario.
173
+ API keys globais ja sao vinculadas a uma organizacao, entao comandos como `projects`, `deploy`, `builds`, `builds logs` e `deployments` nao precisam de `org set`. Comandos pessoais como `orgs` e `org set` continuam exigindo `zenifra auth login` em um perfil com token de usuario.
151
174
 
152
175
  Para reduzir impacto de vazamento, configure IPs permitidos na criacao da API key sempre que a automacao tiver origem fixa.
153
176
 
package/bin/zenifra.mjs CHANGED
@@ -21,7 +21,6 @@ const SESSION_FILE = join(SESSION_DIR, 'session.json');
21
21
  const PROFILES_FILE = join(SESSION_DIR, 'profiles.json');
22
22
  const PROFILE_STORE_VERSION = 1;
23
23
  const DEFAULT_PROFILE_NAME = 'default';
24
- const FINAL_BUILD_STATUSES = new Set(['success', 'failed']);
25
24
  const ALLOWED_PLAN_VALUES = new Set([
26
25
  'free',
27
26
  'static',
@@ -108,7 +107,7 @@ Usage:
108
107
  zenifra plans [--type <all|http|database|storage>] [--json]
109
108
  zenifra create project
110
109
  zenifra create project --name <name> --plan <plan> --payment-mode <mode> --config <json|@file> [--description <text>] [--org <id>] [--json]
111
- zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb>]
110
+ zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb>] [--page <n>] [--limit <n>]
112
111
  zenifra project info --project <id> [--json]
113
112
  zenifra project url --project <id> [--json]
114
113
  zenifra project logs --project <id> [--instance <id>] [--json]
@@ -122,6 +121,7 @@ Usage:
122
121
  zenifra project instances --project <id> [--json]
123
122
  zenifra project instances set --project <id> --count <n> [--json]
124
123
  zenifra builds --project <id> [--page <n>] [--limit <n>] [--branch <name>] [--status <status>] [--json]
124
+ zenifra builds logs --project <id> --build <id> [--cursor <n>] [--limit <n>] [--follow] [--interval <seconds>] [--timeout <seconds>] [--json]
125
125
  zenifra deployments --project <id> [--page <n>] [--limit <n>] [--branch <name>] [--status <status>] [--json]
126
126
  zenifra deploy --project <id> [--branch <name>] [--commit-sha <sha>] [--json]
127
127
  zenifra deploy watch --project <id> --build <id> [--interval <seconds>] [--timeout <seconds>] [--json]
@@ -272,12 +272,12 @@ const HELP_SPECS = [
272
272
  },
273
273
  {
274
274
  command: 'projects',
275
- usage: 'zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb>]',
275
+ usage: 'zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb>] [--page <n>] [--limit <n>]',
276
276
  description: 'Lista projetos da organizacao ativa ou da API key.',
277
- flags: ['--json Imprime a resposta em JSON.', '--org <id> Usa uma organizacao especifica com sessao de usuario.', '--type <type> Filtra por tipo de projeto.'],
278
- examples: ['zenifra projects --type http', 'zenifra projects --json'],
277
+ flags: ['--json Imprime a resposta em JSON.', '--org <id> Usa uma organizacao especifica com sessao de usuario.', '--type <type> Filtra por tipo de projeto.', '--page <n> Pagina. Padrao: 1.', '--limit <n> Itens por pagina. Padrao: 15.'],
278
+ examples: ['zenifra projects --type http --page 1 --limit 15', 'zenifra projects --json'],
279
279
  output: 'ID Nome Status Plano Tipo\n507f1f77bcf86cd799439012 api-web running free http',
280
- jsonOutput: '[{"id":"507f1f77bcf86cd799439012","name":"api-web","status":"running","type_project":"http"}]',
280
+ jsonOutput: '{"projects":[{"id":"507f1f77bcf86cd799439012","name":"api-web","status":"running","type_project":"http"}],"pagination":{"page":1,"limit":15,"total":1,"pages":1}}',
281
281
  },
282
282
  {
283
283
  command: 'create project',
@@ -419,6 +419,24 @@ const HELP_SPECS = [
419
419
  output: 'Build Status Branch Commit\nbuild_123 success main abc123',
420
420
  jsonOutput: '[{"id":"build_123","status":"success","branch":"main","commit_sha":"abc123"}]',
421
421
  },
422
+ {
423
+ command: 'builds logs',
424
+ usage: 'zenifra builds logs --project <id> --build <id> [--cursor <n>] [--limit <n>] [--follow] [--interval <seconds>] [--timeout <seconds>] [--json]',
425
+ description: 'Lê os logs de um build GitHub existente e opcionalmente acompanha novos chunks em tempo real.',
426
+ flags: [
427
+ '--project <id> ID do projeto.',
428
+ '--build <id> ID do build.',
429
+ '--cursor <n> Cursor inicial. Padrao: 0.',
430
+ '--limit <n> Quantidade de logs por request. Padrao: 200.',
431
+ '--follow Continua em polling ate o build finalizar.',
432
+ '--interval <seconds> Intervalo de polling com --follow. Padrao: 5.',
433
+ '--timeout <seconds> Timeout total com --follow. Padrao: 900.',
434
+ '--json Imprime a resposta em JSON.',
435
+ ],
436
+ examples: ['zenifra builds logs --project 507f1f77bcf86cd799439012 --build build_123', 'zenifra builds logs --project 507f1f77bcf86cd799439012 --build build_123 --follow'],
437
+ output: '[2026-05-27T12:00:00.000Z] install: npm ci\n[2026-05-27T12:00:05.000Z] build: npm run build',
438
+ jsonOutput: '{"logs":[{"sequence":1,"timestamp":"2026-05-27T12:00:00.000Z","level":"info","step":"install","message":"npm ci","final":false}],"next_cursor":1,"status":"running","finished":false,"truncated":false}',
439
+ },
422
440
  {
423
441
  command: 'deployments',
424
442
  usage: 'zenifra deployments --project <id> [--page <n>] [--limit <n>] [--branch <name>] [--status <status>] [--json]',
@@ -431,20 +449,35 @@ const HELP_SPECS = [
431
449
  {
432
450
  command: 'deploy',
433
451
  usage: 'zenifra deploy --project <id> [--branch <name>] [--commit-sha <sha>] [--json]',
434
- description: 'Dispara um deploy GitHub para o projeto.',
452
+ description: 'Dispara um build/deploy GitHub para o projeto e retorna o build_id para acompanhamento posterior.',
435
453
  flags: ['--project <id> ID do projeto.', '--branch <name> Branch a publicar.', '--commit-sha <sha> Commit especifico.', '--json Imprime a resposta em JSON.'],
436
- examples: ['zenifra deploy --project 507f1f77bcf86cd799439012 --branch main'],
454
+ examples: [
455
+ 'zenifra deploy --project 507f1f77bcf86cd799439012 --branch main',
456
+ 'zenifra deploy watch --project 507f1f77bcf86cd799439012 --build build_123',
457
+ ],
437
458
  output: 'Deploy iniciado: build_123',
438
459
  jsonOutput: '{"status":"success","data":{"build_id":"build_123"}}',
460
+ notes: [
461
+ 'Use o build_id retornado para acompanhar a execucao com "zenifra deploy watch --project <id> --build <build_id>".',
462
+ 'O subcomando "deploy watch" acompanha status e logs incrementais do build ate o estado terminal.',
463
+ ],
439
464
  },
440
465
  {
441
466
  command: 'deploy watch',
442
467
  usage: 'zenifra deploy watch --project <id> --build <id> [--interval <seconds>] [--timeout <seconds>] [--json]',
443
- description: 'Acompanha um build ate status terminal.',
468
+ description: 'Acompanha um build em tempo real, fazendo polling do status e imprimindo os logs incrementais do build ate o estado terminal.',
444
469
  flags: ['--project <id> ID do projeto.', '--build <id> ID do build.', '--interval <seconds> Intervalo de polling. Padrao: 5.', '--timeout <seconds> Timeout total. Padrao: 900.', '--json Imprime cada resposta em JSON.'],
445
- examples: ['zenifra deploy watch --project 507f1f77bcf86cd799439012 --build build_123'],
446
- output: '[2026-05-27T12:00:00.000Z] build build_123: building\n[2026-05-27T12:00:05.000Z] build build_123: success',
447
- jsonOutput: '{"id":"build_123","status":"success"}',
470
+ examples: [
471
+ 'zenifra deploy watch --project 507f1f77bcf86cd799439012 --build build_123',
472
+ 'zenifra deploy watch --project 507f1f77bcf86cd799439012 --build build_123 --interval 2',
473
+ 'zenifra deploy watch --project 507f1f77bcf86cd799439012 --build build_123 --json',
474
+ ],
475
+ output: '[2026-05-27T12:00:00.000Z] install: npm ci\n[2026-05-27T12:00:05.000Z] build: npm run build\n[2026-05-27T12:00:09.000Z] publish: Build concluida e publicada',
476
+ jsonOutput: '{"sequence":1,"timestamp":"2026-05-27T12:00:00.000Z","level":"info","step":"install","message":"npm ci","final":false}',
477
+ notes: [
478
+ 'Use este subcomando logo apos "zenifra deploy" para acompanhar o build pelo build_id retornado.',
479
+ 'No modo --json, o comando imprime eventos de log em streaming, um JSON por linha.',
480
+ ],
448
481
  },
449
482
  ];
450
483
 
@@ -495,6 +528,12 @@ function commandHelp(positionals) {
495
528
  return formatHelp(spec);
496
529
  }
497
530
 
531
+ function printCommandHelpAndFail(commandKey) {
532
+ const positionals = Array.isArray(commandKey) ? commandKey : String(commandKey).split(' ')
533
+ process.stdout.write(commandHelp(positionals))
534
+ process.exitCode = 1
535
+ }
536
+
498
537
  function isRemovedProjectsCreate(positionals) {
499
538
  return positionals[0] === 'projects' && positionals[1] === 'create';
500
539
  }
@@ -1262,8 +1301,18 @@ function buildIdOf(build) {
1262
1301
  return build?.id || build?._id || build?.build_id || '';
1263
1302
  }
1264
1303
 
1265
- function requireProjectId(flags) {
1266
- if (!flags.project) throw new CliError('Informe --project <id>.');
1304
+ function buildLogLineOf(log) {
1305
+ const timestamp = log?.timestamp ? new Date(log.timestamp).toISOString() : new Date().toISOString()
1306
+ const step = log?.step || 'build'
1307
+ const message = log?.message || ''
1308
+ return `[${timestamp}] ${step}: ${message}`
1309
+ }
1310
+
1311
+ function requireProjectId(flags, commandKey) {
1312
+ if (!flags.project) {
1313
+ printCommandHelpAndFail(commandKey)
1314
+ return null
1315
+ }
1267
1316
  return String(flags.project);
1268
1317
  }
1269
1318
 
@@ -1528,7 +1577,10 @@ async function handleProfileShow(session, flags, positional = []) {
1528
1577
 
1529
1578
  async function handleProfileUse(session, flags, positional = []) {
1530
1579
  const targetName = positional[2] || flags.name;
1531
- if (!targetName) throw new CliError('Informe o nome do perfil: zenifra profile use <name>.');
1580
+ if (!targetName) {
1581
+ printCommandHelpAndFail('profile use')
1582
+ return
1583
+ }
1532
1584
  activateProfileSession(session, targetName);
1533
1585
  await writeProfileStore(getStore(session));
1534
1586
  if (flags.json) return printJson({ active_profile: getProfileName(session) });
@@ -1578,7 +1630,10 @@ async function handleProfileAdd(session, flags) {
1578
1630
 
1579
1631
  async function handleProfileEdit(session, flags, positional = []) {
1580
1632
  const targetName = positional[2] || flags.name;
1581
- if (!targetName) throw new CliError('Informe o nome do perfil: zenifra profile edit <name>.');
1633
+ if (!targetName) {
1634
+ printCommandHelpAndFail('profile edit')
1635
+ return
1636
+ }
1582
1637
  const target = requireExistingProfile(session, targetName);
1583
1638
  target.description = String(flags.description ?? await prompt('Descricao do perfil', target.description || ''));
1584
1639
  target.apiBaseUrl = normalizeApiBaseUrl(flags.apiBase || await prompt('API base', target.apiBaseUrl || DEFAULT_API_BASE_URL));
@@ -1594,7 +1649,10 @@ async function handleProfileEdit(session, flags, positional = []) {
1594
1649
 
1595
1650
  async function handleProfileRemove(session, flags, positional = []) {
1596
1651
  const targetName = positional[2] || flags.name;
1597
- if (!targetName) throw new CliError('Informe o nome do perfil: zenifra profile remove <name>.');
1652
+ if (!targetName) {
1653
+ printCommandHelpAndFail('profile remove')
1654
+ return
1655
+ }
1598
1656
  const normalizedName = normalizeProfileName(targetName);
1599
1657
  const store = getStore(session);
1600
1658
  if (!store.profiles[normalizedName]) {
@@ -1636,7 +1694,12 @@ async function handleOrgSet(session, flags) {
1636
1694
 
1637
1695
  async function handleProjects(session, flags) {
1638
1696
  const orgId = await resolveOrgId(session, flags);
1639
- const query = buildQuery(flags, ['type']);
1697
+ const projectFlags = {
1698
+ ...flags,
1699
+ page: flags.page ?? 1,
1700
+ limit: flags.limit ?? 15,
1701
+ };
1702
+ const query = buildQuery(projectFlags, ['type', 'page', 'limit']);
1640
1703
  const data = unwrapData(await request(session, flags, 'GET', `/project${query}`, { orgId }));
1641
1704
  const projects = asArray(data);
1642
1705
 
@@ -1648,6 +1711,10 @@ async function handleProjects(session, flags) {
1648
1711
  { label: 'Plano', value: (project) => project.plan || '-' },
1649
1712
  { label: 'Tipo', value: (project) => project.type_project || project.config?.type_project || '-' },
1650
1713
  ]);
1714
+ if (data?.pagination) {
1715
+ const { page = 1, pages = 1, total = projects.length } = data.pagination;
1716
+ process.stdout.write(`\nPagina ${page} de ${Math.max(pages, 1)} (${total} projeto(s))\n`);
1717
+ }
1651
1718
  }
1652
1719
 
1653
1720
  async function parseConfig(input) {
@@ -2496,7 +2563,8 @@ async function getProject(session, flags, projectId, orgId) {
2496
2563
  }
2497
2564
 
2498
2565
  async function handleProjectInfo(session, flags) {
2499
- const projectId = requireProjectId(flags);
2566
+ const projectId = requireProjectId(flags, 'project info');
2567
+ if (!projectId) return
2500
2568
  const orgId = await resolveOrgId(session, flags);
2501
2569
  const project = await getProject(session, flags, projectId, orgId);
2502
2570
 
@@ -2505,7 +2573,8 @@ async function handleProjectInfo(session, flags) {
2505
2573
  }
2506
2574
 
2507
2575
  async function handleProjectUrl(session, flags) {
2508
- const projectId = requireProjectId(flags);
2576
+ const projectId = requireProjectId(flags, 'project url');
2577
+ if (!projectId) return
2509
2578
  const orgId = await resolveOrgId(session, flags);
2510
2579
  const project = await getProject(session, flags, projectId, orgId);
2511
2580
  const url = projectUrlOf(project);
@@ -2522,7 +2591,8 @@ async function handleProjectUrl(session, flags) {
2522
2591
  }
2523
2592
 
2524
2593
  async function handleProjectLogs(session, flags) {
2525
- const projectId = requireProjectId(flags);
2594
+ const projectId = requireProjectId(flags, 'project logs');
2595
+ if (!projectId) return
2526
2596
  const orgId = await resolveOrgId(session, flags);
2527
2597
  const query = buildQuery(flags, ['instance']);
2528
2598
  const logs = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/logs${query}`, { orgId }));
@@ -2531,7 +2601,8 @@ async function handleProjectLogs(session, flags) {
2531
2601
  }
2532
2602
 
2533
2603
  async function handleProjectMetrics(session, flags) {
2534
- const projectId = requireProjectId(flags);
2604
+ const projectId = requireProjectId(flags, 'project metrics');
2605
+ if (!projectId) return
2535
2606
  const orgId = await resolveOrgId(session, flags);
2536
2607
  const query = buildQuery(flags, ['instance']);
2537
2608
  const metrics = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/metrics${query}`, { orgId }));
@@ -2549,7 +2620,8 @@ async function handleProjectMetrics(session, flags) {
2549
2620
  }
2550
2621
 
2551
2622
  async function handleProjectNetwork(session, flags) {
2552
- const projectId = requireProjectId(flags);
2623
+ const projectId = requireProjectId(flags, 'project network');
2624
+ if (!projectId) return
2553
2625
  const orgId = await resolveOrgId(session, flags);
2554
2626
  const view = String(flags.view || 'summary');
2555
2627
  const allowedViews = new Set(['summary', 'status-codes', 'routes', 'user-agents', 'request-events', 'source-ips']);
@@ -2564,9 +2636,13 @@ async function handleProjectNetwork(session, flags) {
2564
2636
  }
2565
2637
 
2566
2638
  async function handleProjectImageSet(session, flags) {
2567
- const projectId = requireProjectId(flags);
2639
+ const projectId = requireProjectId(flags, 'project image set');
2640
+ if (!projectId) return
2568
2641
  const image = flags.image || await prompt('Imagem');
2569
- if (!image) throw new CliError('Informe --image <image>.');
2642
+ if (!image) {
2643
+ printCommandHelpAndFail('project image set')
2644
+ return
2645
+ }
2570
2646
 
2571
2647
  const orgId = await resolveOrgId(session, flags);
2572
2648
  const payload = await request(session, flags, 'PATCH', `/project/${projectId}/image`, {
@@ -2590,7 +2666,8 @@ async function patchProjectEnvs(session, flags, projectId, orgId, envs) {
2590
2666
  }
2591
2667
 
2592
2668
  async function handleProjectEnvs(session, flags) {
2593
- const projectId = requireProjectId(flags);
2669
+ const projectId = requireProjectId(flags, 'project envs');
2670
+ if (!projectId) return
2594
2671
  const orgId = await resolveOrgId(session, flags);
2595
2672
  const envs = await getProjectEnvs(session, flags, projectId, orgId);
2596
2673
 
@@ -2598,13 +2675,26 @@ async function handleProjectEnvs(session, flags) {
2598
2675
  }
2599
2676
 
2600
2677
  async function handleProjectEnvMutation(session, flags, action) {
2601
- const projectId = requireProjectId(flags);
2678
+ const helpKeyByAction = {
2679
+ add: 'project env add',
2680
+ update: 'project env update',
2681
+ remove: 'project env remove',
2682
+ }
2683
+ const helpKey = helpKeyByAction[action]
2684
+ const projectId = requireProjectId(flags, helpKey);
2685
+ if (!projectId) return
2602
2686
  const name = String(flags.name || '').trim();
2603
- if (!name) throw new CliError('Informe --name <name>.');
2687
+ if (!name) {
2688
+ printCommandHelpAndFail(helpKey)
2689
+ return
2690
+ }
2604
2691
 
2605
2692
  const needsValue = action === 'add' || action === 'update';
2606
2693
  const value = needsValue ? String(flags.value ?? await promptHidden('Valor')) : undefined;
2607
- if (needsValue && !value) throw new CliError('Informe --value <value>.');
2694
+ if (needsValue && !value) {
2695
+ printCommandHelpAndFail(helpKey)
2696
+ return
2697
+ }
2608
2698
 
2609
2699
  const orgId = await resolveOrgId(session, flags);
2610
2700
  const envs = await getProjectEnvs(session, flags, projectId, orgId);
@@ -2637,7 +2727,8 @@ async function handleProjectEnvMutation(session, flags, action) {
2637
2727
  }
2638
2728
 
2639
2729
  async function handleProjectInstances(session, flags) {
2640
- const projectId = requireProjectId(flags);
2730
+ const projectId = requireProjectId(flags, 'project instances');
2731
+ if (!projectId) return
2641
2732
  const orgId = await resolveOrgId(session, flags);
2642
2733
  const instances = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/instances`, { orgId }));
2643
2734
 
@@ -2648,7 +2739,12 @@ async function handleProjectInstances(session, flags) {
2648
2739
  }
2649
2740
 
2650
2741
  async function handleProjectInstancesSet(session, flags) {
2651
- const projectId = requireProjectId(flags);
2742
+ const projectId = requireProjectId(flags, 'project instances set');
2743
+ if (!projectId) return
2744
+ if (flags.count === undefined) {
2745
+ printCommandHelpAndFail('project instances set')
2746
+ return
2747
+ }
2652
2748
  const instances = Number(flags.count);
2653
2749
  if (!Number.isInteger(instances) || instances <= 0) {
2654
2750
  throw new CliError('Informe --count <n> com um inteiro positivo.');
@@ -2678,7 +2774,10 @@ function buildQuery(flags, allowedKeys) {
2678
2774
 
2679
2775
  async function handleDeployments(session, flags) {
2680
2776
  const projectId = flags.project;
2681
- if (!projectId) throw new CliError('Informe --project <id>.');
2777
+ if (!projectId) {
2778
+ printCommandHelpAndFail(flags.__commandKey || 'builds')
2779
+ return
2780
+ }
2682
2781
  const orgId = await resolveOrgId(session, flags);
2683
2782
  const query = buildQuery(flags, ['page', 'limit', 'branch', 'status']);
2684
2783
  const data = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/github/builds${query}`, { orgId }));
@@ -2696,7 +2795,11 @@ async function handleDeployments(session, flags) {
2696
2795
 
2697
2796
  async function handleDeploy(session, flags) {
2698
2797
  const projectId = flags.project;
2699
- if (!projectId) throw new CliError('Informe --project <id>.');
2798
+ if (!projectId) {
2799
+ process.stdout.write(commandHelp(['deploy']));
2800
+ process.exitCode = 1;
2801
+ return;
2802
+ }
2700
2803
  const orgId = await resolveOrgId(session, flags);
2701
2804
  const body = {};
2702
2805
  if (flags.branch) body.branch = String(flags.branch);
@@ -2712,44 +2815,110 @@ async function handleDeploy(session, flags) {
2712
2815
  process.stdout.write(`Deploy iniciado${buildId ? `: ${buildId}` : '.'}\n`);
2713
2816
  }
2714
2817
 
2715
- async function getBuild(session, flags, projectId, buildId, orgId) {
2716
- return unwrapData(await request(session, flags, 'GET', `/project/${projectId}/github/builds/${buildId}`, { orgId }));
2818
+ async function getBuildLogs(session, flags, projectId, buildId, orgId, { cursor = 0, limit = 200 } = {}) {
2819
+ const query = buildQuery({ cursor, limit }, ['cursor', 'limit'])
2820
+ return unwrapData(await request(session, flags, 'GET', `/project/${projectId}/github/builds/${buildId}/logs${query}`, { orgId }))
2717
2821
  }
2718
2822
 
2719
- async function handleDeployWatch(session, flags) {
2720
- const projectId = flags.project;
2721
- const buildId = flags.build;
2722
- if (!projectId) throw new CliError('Informe --project <id>.');
2723
- if (!buildId) throw new CliError('Informe --build <id>.');
2823
+ function printBuildLogs(logs) {
2824
+ for (const log of asArray(logs)) {
2825
+ process.stdout.write(`${buildLogLineOf(log)}\n`)
2826
+ }
2827
+ }
2724
2828
 
2725
- const orgId = await resolveOrgId(session, flags);
2726
- const intervalMs = Math.max(Number(flags.interval || 5), 1) * 1000;
2727
- const timeoutMs = Math.max(Number(flags.timeout || 900), 1) * 1000;
2728
- const startedAt = Date.now();
2829
+ function printBuildLogEventsJson(logs) {
2830
+ for (const log of asArray(logs)) {
2831
+ process.stdout.write(`${JSON.stringify(log)}\n`)
2832
+ }
2833
+ }
2834
+
2835
+ async function followBuildLogs(session, flags, projectId, buildId, orgId, {
2836
+ initialCursor = 0,
2837
+ limit = 200,
2838
+ intervalSeconds = 5,
2839
+ timeoutSeconds = 900,
2840
+ } = {}) {
2841
+ let cursor = Math.max(Number(initialCursor || 0), 0)
2842
+ const pollLimit = Math.max(Number(limit || 200), 1)
2843
+ const intervalMs = Math.max(Number(intervalSeconds || 5), 0.1) * 1000
2844
+ const timeoutMs = Math.max(Number(timeoutSeconds || 900), 1) * 1000
2845
+ const startedAt = Date.now()
2729
2846
 
2730
2847
  while (true) {
2731
- const build = await getBuild(session, flags, projectId, buildId, orgId);
2732
- const status = build?.status || 'unknown';
2848
+ const result = await getBuildLogs(session, flags, projectId, buildId, orgId, {
2849
+ cursor,
2850
+ limit: pollLimit,
2851
+ })
2852
+ const logs = asArray(result?.logs)
2733
2853
 
2734
2854
  if (flags.json) {
2735
- printJson(build);
2855
+ printBuildLogEventsJson(logs)
2736
2856
  } else {
2737
- process.stdout.write(`[${new Date().toISOString()}] build ${buildId}: ${status}\n`);
2857
+ printBuildLogs(logs)
2738
2858
  }
2739
2859
 
2740
- if (FINAL_BUILD_STATUSES.has(status)) {
2741
- if (status === 'failed') {
2742
- process.exitCode = 1;
2860
+ if (typeof result?.next_cursor === 'number') {
2861
+ cursor = result.next_cursor
2862
+ } else if (logs.length > 0) {
2863
+ cursor = logs[logs.length - 1].sequence || cursor
2864
+ }
2865
+
2866
+ if (result?.finished) {
2867
+ if (result?.status === 'failed') {
2868
+ process.exitCode = 1
2743
2869
  }
2744
- return;
2870
+ return result
2745
2871
  }
2746
2872
 
2747
2873
  if (Date.now() - startedAt > timeoutMs) {
2748
- throw new CliError(`Timeout aguardando build ${buildId}. Ultimo status: ${status}`);
2874
+ throw new CliError(`Timeout aguardando build ${buildId}. Ultimo status: ${result?.status || 'unknown'}`)
2749
2875
  }
2750
2876
 
2751
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
2877
+ await new Promise((resolve) => setTimeout(resolve, intervalMs))
2878
+ }
2879
+ }
2880
+
2881
+ async function handleBuildLogs(session, flags) {
2882
+ const projectId = flags.project;
2883
+ const buildId = flags.build;
2884
+ if (!projectId || !buildId) {
2885
+ printCommandHelpAndFail('builds logs')
2886
+ return
2887
+ }
2888
+
2889
+ const orgId = await resolveOrgId(session, flags);
2890
+ const cursor = Math.max(Number(flags.cursor || 0), 0)
2891
+ const limit = Math.max(Number(flags.limit || 200), 1)
2892
+
2893
+ if (flags.follow) {
2894
+ return followBuildLogs(session, flags, projectId, buildId, orgId, {
2895
+ initialCursor: cursor,
2896
+ limit,
2897
+ intervalSeconds: Number(flags.interval || 5),
2898
+ timeoutSeconds: Number(flags.timeout || 900),
2899
+ })
2752
2900
  }
2901
+
2902
+ const result = await getBuildLogs(session, flags, projectId, buildId, orgId, { cursor, limit })
2903
+ if (flags.json) return printJson(result)
2904
+ printBuildLogs(result?.logs)
2905
+ }
2906
+
2907
+ async function handleDeployWatch(session, flags) {
2908
+ const projectId = flags.project;
2909
+ const buildId = flags.build;
2910
+ if (!projectId || !buildId) {
2911
+ printCommandHelpAndFail('deploy watch')
2912
+ return;
2913
+ }
2914
+
2915
+ const orgId = await resolveOrgId(session, flags);
2916
+ return followBuildLogs(session, flags, projectId, buildId, orgId, {
2917
+ initialCursor: 0,
2918
+ limit: 200,
2919
+ intervalSeconds: Number(flags.interval || 5),
2920
+ timeoutSeconds: Number(flags.timeout || 900),
2921
+ })
2753
2922
  }
2754
2923
 
2755
2924
  async function main() {
@@ -2809,8 +2978,9 @@ async function main() {
2809
2978
  if (command === 'project' && subcommand === 'env' && positional[2] === 'remove') return handleProjectEnvMutation(session, flags, 'remove');
2810
2979
  if (command === 'project' && subcommand === 'instances' && positional[2] === 'set') return handleProjectInstancesSet(session, flags);
2811
2980
  if (command === 'project' && subcommand === 'instances') return handleProjectInstances(session, flags);
2812
- if (command === 'builds') return handleDeployments(session, flags);
2813
- if (command === 'deployments') return handleDeployments(session, flags);
2981
+ if (command === 'builds' && subcommand === 'logs') return handleBuildLogs(session, flags);
2982
+ if (command === 'builds') return handleDeployments(session, { ...flags, __commandKey: 'builds' });
2983
+ if (command === 'deployments') return handleDeployments(session, { ...flags, __commandKey: 'deployments' });
2814
2984
  if (command === 'deploy' && subcommand === 'watch') return handleDeployWatch(session, flags);
2815
2985
  if (command === 'deploy') return handleDeploy(session, flags);
2816
2986
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenifra/cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Official Zenifra command line interface.",
5
5
  "type": "module",
6
6
  "license": "MIT",