@zenifra/cli 0.2.2 → 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 +24 -1
  2. package/bin/zenifra.mjs +211 -50
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -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`
@@ -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',
@@ -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]
@@ -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]) {
@@ -2505,7 +2563,8 @@ async function getProject(session, flags, projectId, orgId) {
2505
2563
  }
2506
2564
 
2507
2565
  async function handleProjectInfo(session, flags) {
2508
- const projectId = requireProjectId(flags);
2566
+ const projectId = requireProjectId(flags, 'project info');
2567
+ if (!projectId) return
2509
2568
  const orgId = await resolveOrgId(session, flags);
2510
2569
  const project = await getProject(session, flags, projectId, orgId);
2511
2570
 
@@ -2514,7 +2573,8 @@ async function handleProjectInfo(session, flags) {
2514
2573
  }
2515
2574
 
2516
2575
  async function handleProjectUrl(session, flags) {
2517
- const projectId = requireProjectId(flags);
2576
+ const projectId = requireProjectId(flags, 'project url');
2577
+ if (!projectId) return
2518
2578
  const orgId = await resolveOrgId(session, flags);
2519
2579
  const project = await getProject(session, flags, projectId, orgId);
2520
2580
  const url = projectUrlOf(project);
@@ -2531,7 +2591,8 @@ async function handleProjectUrl(session, flags) {
2531
2591
  }
2532
2592
 
2533
2593
  async function handleProjectLogs(session, flags) {
2534
- const projectId = requireProjectId(flags);
2594
+ const projectId = requireProjectId(flags, 'project logs');
2595
+ if (!projectId) return
2535
2596
  const orgId = await resolveOrgId(session, flags);
2536
2597
  const query = buildQuery(flags, ['instance']);
2537
2598
  const logs = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/logs${query}`, { orgId }));
@@ -2540,7 +2601,8 @@ async function handleProjectLogs(session, flags) {
2540
2601
  }
2541
2602
 
2542
2603
  async function handleProjectMetrics(session, flags) {
2543
- const projectId = requireProjectId(flags);
2604
+ const projectId = requireProjectId(flags, 'project metrics');
2605
+ if (!projectId) return
2544
2606
  const orgId = await resolveOrgId(session, flags);
2545
2607
  const query = buildQuery(flags, ['instance']);
2546
2608
  const metrics = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/metrics${query}`, { orgId }));
@@ -2558,7 +2620,8 @@ async function handleProjectMetrics(session, flags) {
2558
2620
  }
2559
2621
 
2560
2622
  async function handleProjectNetwork(session, flags) {
2561
- const projectId = requireProjectId(flags);
2623
+ const projectId = requireProjectId(flags, 'project network');
2624
+ if (!projectId) return
2562
2625
  const orgId = await resolveOrgId(session, flags);
2563
2626
  const view = String(flags.view || 'summary');
2564
2627
  const allowedViews = new Set(['summary', 'status-codes', 'routes', 'user-agents', 'request-events', 'source-ips']);
@@ -2573,9 +2636,13 @@ async function handleProjectNetwork(session, flags) {
2573
2636
  }
2574
2637
 
2575
2638
  async function handleProjectImageSet(session, flags) {
2576
- const projectId = requireProjectId(flags);
2639
+ const projectId = requireProjectId(flags, 'project image set');
2640
+ if (!projectId) return
2577
2641
  const image = flags.image || await prompt('Imagem');
2578
- if (!image) throw new CliError('Informe --image <image>.');
2642
+ if (!image) {
2643
+ printCommandHelpAndFail('project image set')
2644
+ return
2645
+ }
2579
2646
 
2580
2647
  const orgId = await resolveOrgId(session, flags);
2581
2648
  const payload = await request(session, flags, 'PATCH', `/project/${projectId}/image`, {
@@ -2599,7 +2666,8 @@ async function patchProjectEnvs(session, flags, projectId, orgId, envs) {
2599
2666
  }
2600
2667
 
2601
2668
  async function handleProjectEnvs(session, flags) {
2602
- const projectId = requireProjectId(flags);
2669
+ const projectId = requireProjectId(flags, 'project envs');
2670
+ if (!projectId) return
2603
2671
  const orgId = await resolveOrgId(session, flags);
2604
2672
  const envs = await getProjectEnvs(session, flags, projectId, orgId);
2605
2673
 
@@ -2607,13 +2675,26 @@ async function handleProjectEnvs(session, flags) {
2607
2675
  }
2608
2676
 
2609
2677
  async function handleProjectEnvMutation(session, flags, action) {
2610
- 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
2611
2686
  const name = String(flags.name || '').trim();
2612
- if (!name) throw new CliError('Informe --name <name>.');
2687
+ if (!name) {
2688
+ printCommandHelpAndFail(helpKey)
2689
+ return
2690
+ }
2613
2691
 
2614
2692
  const needsValue = action === 'add' || action === 'update';
2615
2693
  const value = needsValue ? String(flags.value ?? await promptHidden('Valor')) : undefined;
2616
- if (needsValue && !value) throw new CliError('Informe --value <value>.');
2694
+ if (needsValue && !value) {
2695
+ printCommandHelpAndFail(helpKey)
2696
+ return
2697
+ }
2617
2698
 
2618
2699
  const orgId = await resolveOrgId(session, flags);
2619
2700
  const envs = await getProjectEnvs(session, flags, projectId, orgId);
@@ -2646,7 +2727,8 @@ async function handleProjectEnvMutation(session, flags, action) {
2646
2727
  }
2647
2728
 
2648
2729
  async function handleProjectInstances(session, flags) {
2649
- const projectId = requireProjectId(flags);
2730
+ const projectId = requireProjectId(flags, 'project instances');
2731
+ if (!projectId) return
2650
2732
  const orgId = await resolveOrgId(session, flags);
2651
2733
  const instances = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/instances`, { orgId }));
2652
2734
 
@@ -2657,7 +2739,12 @@ async function handleProjectInstances(session, flags) {
2657
2739
  }
2658
2740
 
2659
2741
  async function handleProjectInstancesSet(session, flags) {
2660
- 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
+ }
2661
2748
  const instances = Number(flags.count);
2662
2749
  if (!Number.isInteger(instances) || instances <= 0) {
2663
2750
  throw new CliError('Informe --count <n> com um inteiro positivo.');
@@ -2687,7 +2774,10 @@ function buildQuery(flags, allowedKeys) {
2687
2774
 
2688
2775
  async function handleDeployments(session, flags) {
2689
2776
  const projectId = flags.project;
2690
- if (!projectId) throw new CliError('Informe --project <id>.');
2777
+ if (!projectId) {
2778
+ printCommandHelpAndFail(flags.__commandKey || 'builds')
2779
+ return
2780
+ }
2691
2781
  const orgId = await resolveOrgId(session, flags);
2692
2782
  const query = buildQuery(flags, ['page', 'limit', 'branch', 'status']);
2693
2783
  const data = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/github/builds${query}`, { orgId }));
@@ -2705,7 +2795,11 @@ async function handleDeployments(session, flags) {
2705
2795
 
2706
2796
  async function handleDeploy(session, flags) {
2707
2797
  const projectId = flags.project;
2708
- if (!projectId) throw new CliError('Informe --project <id>.');
2798
+ if (!projectId) {
2799
+ process.stdout.write(commandHelp(['deploy']));
2800
+ process.exitCode = 1;
2801
+ return;
2802
+ }
2709
2803
  const orgId = await resolveOrgId(session, flags);
2710
2804
  const body = {};
2711
2805
  if (flags.branch) body.branch = String(flags.branch);
@@ -2721,44 +2815,110 @@ async function handleDeploy(session, flags) {
2721
2815
  process.stdout.write(`Deploy iniciado${buildId ? `: ${buildId}` : '.'}\n`);
2722
2816
  }
2723
2817
 
2724
- async function getBuild(session, flags, projectId, buildId, orgId) {
2725
- 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 }))
2726
2821
  }
2727
2822
 
2728
- async function handleDeployWatch(session, flags) {
2729
- const projectId = flags.project;
2730
- const buildId = flags.build;
2731
- if (!projectId) throw new CliError('Informe --project <id>.');
2732
- 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
+ }
2733
2828
 
2734
- const orgId = await resolveOrgId(session, flags);
2735
- const intervalMs = Math.max(Number(flags.interval || 5), 1) * 1000;
2736
- const timeoutMs = Math.max(Number(flags.timeout || 900), 1) * 1000;
2737
- 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()
2738
2846
 
2739
2847
  while (true) {
2740
- const build = await getBuild(session, flags, projectId, buildId, orgId);
2741
- 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)
2742
2853
 
2743
2854
  if (flags.json) {
2744
- printJson(build);
2855
+ printBuildLogEventsJson(logs)
2745
2856
  } else {
2746
- process.stdout.write(`[${new Date().toISOString()}] build ${buildId}: ${status}\n`);
2857
+ printBuildLogs(logs)
2747
2858
  }
2748
2859
 
2749
- if (FINAL_BUILD_STATUSES.has(status)) {
2750
- if (status === 'failed') {
2751
- 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
2752
2869
  }
2753
- return;
2870
+ return result
2754
2871
  }
2755
2872
 
2756
2873
  if (Date.now() - startedAt > timeoutMs) {
2757
- throw new CliError(`Timeout aguardando build ${buildId}. Ultimo status: ${status}`);
2874
+ throw new CliError(`Timeout aguardando build ${buildId}. Ultimo status: ${result?.status || 'unknown'}`)
2758
2875
  }
2759
2876
 
2760
- 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
2761
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
+ })
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
+ })
2762
2922
  }
2763
2923
 
2764
2924
  async function main() {
@@ -2818,8 +2978,9 @@ async function main() {
2818
2978
  if (command === 'project' && subcommand === 'env' && positional[2] === 'remove') return handleProjectEnvMutation(session, flags, 'remove');
2819
2979
  if (command === 'project' && subcommand === 'instances' && positional[2] === 'set') return handleProjectInstancesSet(session, flags);
2820
2980
  if (command === 'project' && subcommand === 'instances') return handleProjectInstances(session, flags);
2821
- if (command === 'builds') return handleDeployments(session, flags);
2822
- 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' });
2823
2984
  if (command === 'deploy' && subcommand === 'watch') return handleDeployWatch(session, flags);
2824
2985
  if (command === 'deploy') return handleDeploy(session, flags);
2825
2986
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenifra/cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Official Zenifra command line interface.",
5
5
  "type": "module",
6
6
  "license": "MIT",