@eve-horizon/cli 0.2.6 → 0.2.8

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.
@@ -5,6 +5,19 @@ const args_1 = require("../lib/args");
5
5
  const client_1 = require("../lib/client");
6
6
  const output_1 = require("../lib/output");
7
7
  const node_child_process_1 = require("node:child_process");
8
+ const ERROR_CODES = {
9
+ auth_error: { code: 'auth_error', label: 'Authentication Error', hint: "Check GITHUB_TOKEN via 'eve secrets set'" },
10
+ clone_error: { code: 'clone_error', label: 'Git Clone Error', hint: "Verify repo URL and access. Check 'eve secrets list'" },
11
+ build_error: { code: 'build_error', label: 'Build Error', hint: "Run 'eve build diagnose <build_id>' for full output" },
12
+ timeout_error: { code: 'timeout_error', label: 'Timeout Error', hint: 'Consider increasing timeout or checking resources' },
13
+ resource_error: { code: 'resource_error', label: 'Resource Error', hint: 'Check disk space and memory on build worker' },
14
+ registry_error: { code: 'registry_error', label: 'Registry Error', hint: "Check registry credentials via 'eve secrets list'" },
15
+ deploy_error: { code: 'deploy_error', label: 'Deploy Error', hint: "Run 'eve env diagnose <project> <env>'" },
16
+ unknown_error: { code: 'unknown_error', label: 'Unknown Error', hint: "Run 'eve build diagnose <build_id>' or 'eve job diagnose <job_id>'" },
17
+ };
18
+ function getErrorCodeInfo(code) {
19
+ return ERROR_CODES[code] ?? ERROR_CODES.unknown_error;
20
+ }
8
21
  // ---------------------------------------------------------------------------
9
22
  // Entry point
10
23
  // ---------------------------------------------------------------------------
@@ -154,6 +167,17 @@ async function handleRuns(positionals, flags, context, json) {
154
167
  }
155
168
  formatRunList(response.data);
156
169
  }
170
+ function formatLogTimestamp(timestamp) {
171
+ if (!timestamp)
172
+ return '';
173
+ try {
174
+ const d = new Date(timestamp);
175
+ return `[${d.toLocaleTimeString('en-US', { hour12: false })}] `;
176
+ }
177
+ catch {
178
+ return '';
179
+ }
180
+ }
157
181
  async function handleLogs(positionals, flags, context, json) {
158
182
  const buildId = positionals[0] ?? (0, args_1.getStringFlag)(flags, ['id']);
159
183
  const runId = (0, args_1.getStringFlag)(flags, ['run']);
@@ -171,20 +195,26 @@ async function handleLogs(positionals, flags, context, json) {
171
195
  return;
172
196
  }
173
197
  for (const entry of logs.logs) {
198
+ const prefix = formatLogTimestamp(entry.timestamp);
174
199
  const line = entry.line;
175
200
  if (typeof line.message === 'string') {
176
- console.log(line.message);
177
- continue;
201
+ console.log(`${prefix}${line.message}`);
178
202
  }
179
- if (Array.isArray(line.lines)) {
203
+ else if (Array.isArray(line.lines)) {
180
204
  for (const item of line.lines) {
181
205
  if (typeof item === 'string') {
182
- console.log(item);
206
+ console.log(`${prefix}${item}`);
183
207
  }
184
208
  }
185
- continue;
186
209
  }
187
- console.log(JSON.stringify(line));
210
+ else {
211
+ console.log(`${prefix}${JSON.stringify(line)}`);
212
+ }
213
+ if (line.level === 'error' && typeof line.error_code === 'string') {
214
+ const info = getErrorCodeInfo(line.error_code);
215
+ console.log(`${prefix} Type: ${info.label}`);
216
+ console.log(`${prefix} Hint: ${info.hint}`);
217
+ }
188
218
  }
189
219
  }
190
220
  async function handleArtifacts(positionals, flags, context, json) {
@@ -255,17 +285,22 @@ async function handleDiagnose(positionals, flags, context, json) {
255
285
  const line = entry.line;
256
286
  if (typeof line.message === 'string') {
257
287
  console.log(line.message);
258
- continue;
259
288
  }
260
- if (Array.isArray(line.lines)) {
289
+ else if (Array.isArray(line.lines)) {
261
290
  for (const item of line.lines) {
262
291
  if (typeof item === 'string') {
263
292
  console.log(item);
264
293
  }
265
294
  }
266
- continue;
267
295
  }
268
- console.log(JSON.stringify(line));
296
+ else {
297
+ console.log(JSON.stringify(line));
298
+ }
299
+ if (line.level === 'error' && typeof line.error_code === 'string') {
300
+ const info = getErrorCodeInfo(line.error_code);
301
+ console.log(` Type: ${info.label}`);
302
+ console.log(` Hint: ${info.hint}`);
303
+ }
269
304
  }
270
305
  }
271
306
  else {
@@ -320,6 +355,11 @@ function formatRunList(runs) {
320
355
  if (run.error_message) {
321
356
  console.log(` Error: ${run.error_message}`);
322
357
  }
358
+ if (run.error_code) {
359
+ const info = getErrorCodeInfo(run.error_code);
360
+ console.log(` Error Type: ${info.label}`);
361
+ console.log(` Hint: ${info.hint}`);
362
+ }
323
363
  }
324
364
  console.log('');
325
365
  console.log(`Total: ${runs.length} runs`);
@@ -267,11 +267,22 @@ async function handleDeploy(positionals, flags, context, json) {
267
267
  else {
268
268
  if (response.pipeline_run) {
269
269
  console.log('');
270
- console.log('Pipeline deployment queued.');
270
+ console.log('Pipeline deployment started.');
271
271
  console.log(` Pipeline Run: ${response.pipeline_run.run.id}`);
272
272
  console.log(` Pipeline: ${response.pipeline_run.run.pipeline_name}`);
273
273
  console.log(` Status: ${response.pipeline_run.run.status}`);
274
274
  console.log(` Environment: ${response.environment.name}`);
275
+ const watchFlag = (0, args_1.toBoolean)(flags.watch);
276
+ const shouldWatch = watchFlag ?? true; // default: watch
277
+ if (shouldWatch) {
278
+ const timeoutRaw = (0, args_1.getStringFlag)(flags, ['timeout']);
279
+ const timeoutSeconds = timeoutRaw ? parseInt(timeoutRaw, 10) : 300; // 5 min default for pipeline
280
+ const pipelineResult = await watchPipelineRun(context, projectId, response.pipeline_run.run.pipeline_name, response.pipeline_run.run.id, Number.isFinite(timeoutSeconds) ? timeoutSeconds : 300);
281
+ // After pipeline completes, watch deployment health if it succeeded
282
+ if (pipelineResult === 'succeeded') {
283
+ await watchDeploymentStatus(context, projectId, envName, 120);
284
+ }
285
+ }
275
286
  return;
276
287
  }
277
288
  console.log('');
@@ -603,6 +614,47 @@ function getDeploymentWarnings(status) {
603
614
  }
604
615
  return Array.from(new Set(warnings));
605
616
  }
617
+ /**
618
+ * Poll a pipeline run until it reaches a terminal status.
619
+ * Returns the final status string.
620
+ */
621
+ async function watchPipelineRun(context, projectId, pipelineName, runId, timeoutSeconds) {
622
+ const start = Date.now();
623
+ const pollIntervalMs = 3000;
624
+ const terminalStatuses = ['succeeded', 'failed', 'cancelled'];
625
+ console.log('');
626
+ console.log('Watching pipeline run...');
627
+ while ((Date.now() - start) / 1000 < timeoutSeconds) {
628
+ const detail = await (0, client_1.requestJson)(context, `/projects/${projectId}/pipelines/${pipelineName}/runs/${runId}`);
629
+ const elapsed = Math.floor((Date.now() - start) / 1000);
630
+ const stepSummary = detail.steps
631
+ .map(s => `${s.step_name}:${s.status}`)
632
+ .join(', ');
633
+ console.log(` [${elapsed}s] ${detail.run.status} (${stepSummary || 'no steps'})`);
634
+ if (terminalStatuses.includes(detail.run.status)) {
635
+ if (detail.run.status === 'succeeded') {
636
+ console.log(' Pipeline run succeeded.');
637
+ }
638
+ else if (detail.run.status === 'failed') {
639
+ console.log(` Pipeline run failed.`);
640
+ // Show error from failed steps
641
+ for (const step of detail.steps) {
642
+ if (step.status === 'failed') {
643
+ console.log(` Step "${step.step_name}" failed.`);
644
+ }
645
+ }
646
+ }
647
+ else {
648
+ console.log(` Pipeline run ${detail.run.status}.`);
649
+ }
650
+ return detail.run.status;
651
+ }
652
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
653
+ }
654
+ console.log(` Pipeline run did not complete within ${timeoutSeconds}s.`);
655
+ console.log(` Run "eve pipeline show-run ${pipelineName} ${runId}" to check status.`);
656
+ return 'timeout';
657
+ }
606
658
  async function watchDeploymentStatus(context, projectId, envName, timeoutSeconds) {
607
659
  const start = Date.now();
608
660
  const pollIntervalMs = 3000;
@@ -1012,6 +1012,22 @@ function formatJobDetails(job) {
1012
1012
  console.log(` Key: ${job.workspace.key}`);
1013
1013
  }
1014
1014
  }
1015
+ // Resolved Git
1016
+ if (job.resolved_git) {
1017
+ const rg = job.resolved_git;
1018
+ console.log('');
1019
+ console.log(' Resolved Git:');
1020
+ if (rg.resolved_sha)
1021
+ console.log(` SHA: ${rg.resolved_sha}`);
1022
+ if (rg.resolved_branch)
1023
+ console.log(` Branch: ${rg.resolved_branch}`);
1024
+ if (rg.ref_source)
1025
+ console.log(` Source: ${rg.ref_source}`);
1026
+ if (rg.pushed !== undefined)
1027
+ console.log(` Pushed: ${rg.pushed}`);
1028
+ if (rg.commits?.length)
1029
+ console.log(` Commits: ${rg.commits.join(', ')}`);
1030
+ }
1015
1031
  console.log('');
1016
1032
  console.log(` Created: ${formatDate(job.created_at)}`);
1017
1033
  console.log(` Updated: ${formatDate(job.updated_at)}`);
@@ -1795,6 +1811,22 @@ function formatResultFull(response) {
1795
1811
  if (pipelineOutput?.deploy?.preview_url) {
1796
1812
  console.log(`Preview: ${pipelineOutput.deploy.preview_url}`);
1797
1813
  }
1814
+ // Display git metadata if present in result_json
1815
+ const resolvedGit = response.resultJson.resolved_git;
1816
+ if (resolvedGit) {
1817
+ console.log('');
1818
+ console.log('Git:');
1819
+ if (resolvedGit.resolved_sha)
1820
+ console.log(` SHA: ${resolvedGit.resolved_sha}`);
1821
+ if (resolvedGit.resolved_branch)
1822
+ console.log(` Branch: ${resolvedGit.resolved_branch}`);
1823
+ if (resolvedGit.ref_source)
1824
+ console.log(` Source: ${resolvedGit.ref_source}`);
1825
+ if (resolvedGit.pushed !== undefined)
1826
+ console.log(` Pushed: ${resolvedGit.pushed}`);
1827
+ if (resolvedGit.commits?.length)
1828
+ console.log(` Commits: ${resolvedGit.commits.join(', ')}`);
1829
+ }
1798
1830
  }
1799
1831
  console.log('');
1800
1832
  console.log('Result:');
@@ -35,7 +35,8 @@ async function handlePipeline(subcommand, positionals, flags, context) {
35
35
  ' show-run <pipeline> <run-id> - show pipeline run status and steps\n' +
36
36
  ' approve <run-id> - approve a blocked pipeline run\n' +
37
37
  ' cancel <run-id> [--reason <text>] - cancel pipeline run\n' +
38
- ' logs <pipeline> <run-id> [--step <name>] - show logs for pipeline run');
38
+ ' logs <pipeline> <run-id> [--step <name>] - show logs for pipeline run\n' +
39
+ ' Options: --follow (-f) stream live');
39
40
  }
40
41
  }
41
42
  async function handleList(positionals, flags, context, json) {
@@ -150,6 +151,24 @@ function formatPipelineRunDetail(detail) {
150
151
  if (step.error_message) {
151
152
  console.log(` Error: ${step.error_message}`);
152
153
  }
154
+ // Task 3.1: Show structured error code info when available
155
+ const errorCode = step.error_code;
156
+ if (errorCode) {
157
+ const info = getErrorCodeInfo(errorCode);
158
+ console.log(` Type: ${info.label}`);
159
+ console.log(` Hint: ${info.hint}`);
160
+ }
161
+ // Task 2.4: Surface build_id hints on failure
162
+ if (step.status === 'failed' && step.step_type === 'build') {
163
+ const buildId = step.output_json?.build_id
164
+ ?? step.result_json?.build_id;
165
+ if (buildId) {
166
+ console.log(` Hint: Run 'eve build diagnose ${buildId}' for full build details`);
167
+ }
168
+ else {
169
+ console.log(` Hint: Run 'eve build diagnose' with the build ID for details`);
170
+ }
171
+ }
153
172
  }
154
173
  console.log('');
155
174
  console.log(`Total: ${steps.length} steps`);
@@ -326,7 +345,11 @@ async function handleLogs(positionals, flags, context, json) {
326
345
  const stepName = (0, args_1.getStringFlag)(flags, ['step']);
327
346
  const projectId = (0, args_1.getStringFlag)(flags, ['project']) ?? context.projectId;
328
347
  if (!pipelineName || !runId || !projectId) {
329
- throw new Error('Usage: eve pipeline logs <pipeline_name> <run-id> [--step <step_name>] [--project <id>]');
348
+ throw new Error('Usage: eve pipeline logs <pipeline_name> <run-id> [--step <step_name>] [--follow] [--project <id>]');
349
+ }
350
+ const follow = Boolean(flags.follow) || Boolean(flags.f);
351
+ if (follow) {
352
+ return handlePipelineFollow(context, pipelineName, runId, stepName ?? null);
330
353
  }
331
354
  // First get the run details to know which steps exist
332
355
  const runDetail = await (0, client_1.requestJson)(context, `/projects/${projectId}/pipelines/${pipelineName}/runs/${runId}`);
@@ -367,6 +390,24 @@ async function handleLogs(positionals, flags, context, json) {
367
390
  if (step.error_message) {
368
391
  console.log(`Error: ${step.error_message}`);
369
392
  }
393
+ // Show structured error code info when available
394
+ const errorCode = step.error_code;
395
+ if (errorCode) {
396
+ const info = getErrorCodeInfo(errorCode);
397
+ console.log(`Type: ${info.label}`);
398
+ console.log(`Hint: ${info.hint}`);
399
+ }
400
+ // Surface build_id hints on failure
401
+ if (step.status === 'failed' && step.step_type === 'build') {
402
+ const buildId = step.output_json?.build_id
403
+ ?? step.result_json?.build_id;
404
+ if (buildId) {
405
+ console.log(`Hint: Run 'eve build diagnose ${buildId}' for full build details`);
406
+ }
407
+ else {
408
+ console.log(`Hint: Run 'eve build diagnose' with the build ID for details`);
409
+ }
410
+ }
370
411
  if (step.result_text) {
371
412
  console.log('');
372
413
  console.log('Result:');
@@ -384,6 +425,158 @@ async function handleLogs(positionals, flags, context, json) {
384
425
  if (!stepName) {
385
426
  console.log(`Total steps: ${stepsToShow.length}`);
386
427
  }
428
+ // Fetch actual logs from the REST endpoint
429
+ const logsUrl = stepName
430
+ ? `/pipeline-runs/${runId}/logs?step=${encodeURIComponent(stepName)}`
431
+ : `/pipeline-runs/${runId}/logs`;
432
+ try {
433
+ const logsResp = await (0, client_1.requestJson)(context, logsUrl);
434
+ if (logsResp?.logs && Array.isArray(logsResp.logs) && logsResp.logs.length > 0) {
435
+ console.log('\n--- Logs ---');
436
+ for (const entry of logsResp.logs) {
437
+ const ts = entry.timestamp ? formatPipelineTime(entry.timestamp) : '';
438
+ const step = entry.step_name ? `[${entry.step_name}] ` : '';
439
+ const content = entry.content ?? {};
440
+ if (content.message) {
441
+ console.log(`${ts}${step}${content.message}`);
442
+ }
443
+ else if (content.lines && Array.isArray(content.lines)) {
444
+ for (const line of content.lines) {
445
+ console.log(`${ts}${step}${line}`);
446
+ }
447
+ }
448
+ }
449
+ }
450
+ }
451
+ catch {
452
+ // Logs endpoint may not exist yet; silently skip
453
+ }
454
+ }
455
+ async function handlePipelineFollow(context, pipelineName, runId, stepFilter) {
456
+ const endpoint = stepFilter
457
+ ? `${context.apiUrl}/pipeline-runs/${runId}/steps/${encodeURIComponent(stepFilter)}/stream`
458
+ : `${context.apiUrl}/pipeline-runs/${runId}/stream`;
459
+ console.log(`Following pipeline run ${runId}...`);
460
+ const headers = {
461
+ Accept: 'text/event-stream',
462
+ };
463
+ if (context.token) {
464
+ headers.Authorization = `Bearer ${context.token}`;
465
+ }
466
+ try {
467
+ const response = await fetch(endpoint, { headers });
468
+ if (!response.ok) {
469
+ const text = await response.text();
470
+ throw new Error(`HTTP ${response.status}: ${text}`);
471
+ }
472
+ if (!response.body) {
473
+ throw new Error('No response body received');
474
+ }
475
+ const reader = response.body.getReader();
476
+ const decoder = new TextDecoder();
477
+ let buffer = '';
478
+ while (true) {
479
+ const { done, value } = await reader.read();
480
+ if (done)
481
+ break;
482
+ buffer += decoder.decode(value, { stream: true });
483
+ const lines = buffer.split('\n');
484
+ buffer = lines.pop() ?? '';
485
+ let currentEvent = '';
486
+ let currentData = '';
487
+ for (const line of lines) {
488
+ if (line.startsWith('event:')) {
489
+ currentEvent = line.slice(6).trim();
490
+ }
491
+ else if (line.startsWith('data:')) {
492
+ currentData = line.slice(5).trim();
493
+ }
494
+ else if (line === '' && currentData) {
495
+ processPipelineSSEEvent(currentEvent, currentData);
496
+ currentEvent = '';
497
+ currentData = '';
498
+ }
499
+ }
500
+ }
501
+ // Process any remaining data in buffer
502
+ if (buffer.trim()) {
503
+ const remainingLines = buffer.split('\n');
504
+ let currentEvent = '';
505
+ let currentData = '';
506
+ for (const line of remainingLines) {
507
+ if (line.startsWith('event:')) {
508
+ currentEvent = line.slice(6).trim();
509
+ }
510
+ else if (line.startsWith('data:')) {
511
+ currentData = line.slice(5).trim();
512
+ }
513
+ }
514
+ if (currentData) {
515
+ processPipelineSSEEvent(currentEvent, currentData);
516
+ }
517
+ }
518
+ console.log('');
519
+ console.log('Stream ended.');
520
+ }
521
+ catch (error) {
522
+ const err = error;
523
+ console.error(`Error following pipeline run: ${err.message}`);
524
+ process.exit(1);
525
+ }
526
+ }
527
+ function processPipelineSSEEvent(eventType, dataStr) {
528
+ try {
529
+ const data = JSON.parse(dataStr);
530
+ if (eventType === 'log') {
531
+ const stepName = data.step_name ?? '???';
532
+ const content = data.line ?? {};
533
+ const timestamp = data.timestamp;
534
+ const timeStr = timestamp ? formatPipelineTime(timestamp) : '';
535
+ if (content.message) {
536
+ console.log(`${timeStr}[${stepName}] ${content.message}`);
537
+ }
538
+ else if (content.lines && Array.isArray(content.lines)) {
539
+ for (const l of content.lines) {
540
+ console.log(`${timeStr}[${stepName}] ${l}`);
541
+ }
542
+ }
543
+ else {
544
+ console.log(`${timeStr}[${stepName}] ${JSON.stringify(content)}`);
545
+ }
546
+ }
547
+ else if (eventType === 'complete') {
548
+ console.log(`\nPipeline run completed: ${data.status}`);
549
+ }
550
+ else if (eventType === 'error') {
551
+ console.error(`\nPipeline run failed: ${data.errorMessage ?? data.status}`);
552
+ process.exit(1);
553
+ }
554
+ }
555
+ catch {
556
+ // Ignore malformed events
557
+ }
558
+ }
559
+ function formatPipelineTime(timestamp) {
560
+ try {
561
+ const d = new Date(timestamp);
562
+ return `[${d.toLocaleTimeString('en-US', { hour12: false })}] `;
563
+ }
564
+ catch {
565
+ return '';
566
+ }
567
+ }
568
+ const ERROR_CODES = {
569
+ auth_error: { code: 'auth_error', label: 'Authentication Error', hint: "Check GITHUB_TOKEN via 'eve secrets set'" },
570
+ clone_error: { code: 'clone_error', label: 'Git Clone Error', hint: "Verify repo URL and access. Check 'eve secrets list'" },
571
+ build_error: { code: 'build_error', label: 'Build Error', hint: "Run 'eve build diagnose <build_id>' for full output" },
572
+ timeout_error: { code: 'timeout_error', label: 'Timeout Error', hint: 'Consider increasing timeout or checking resources' },
573
+ resource_error: { code: 'resource_error', label: 'Resource Error', hint: 'Check disk space and memory on build worker' },
574
+ registry_error: { code: 'registry_error', label: 'Registry Error', hint: "Check registry credentials via 'eve secrets list'" },
575
+ deploy_error: { code: 'deploy_error', label: 'Deploy Error', hint: "Run 'eve env diagnose <project> <env>'" },
576
+ unknown_error: { code: 'unknown_error', label: 'Unknown Error', hint: "Run 'eve build diagnose <build_id>' or 'eve job diagnose <job_id>'" },
577
+ };
578
+ function getErrorCodeInfo(code) {
579
+ return ERROR_CODES[code] ?? ERROR_CODES.unknown_error;
387
580
  }
388
581
  function buildQuery(params) {
389
582
  const search = new URLSearchParams();
package/dist/lib/help.js CHANGED
@@ -991,8 +991,22 @@ for cloud deployments. Credentials are stored per-profile.`,
991
991
  usage: 'eve pipeline cancel <run-id> [--reason <text>]',
992
992
  examples: ['eve pipeline cancel prun_xxx --reason "superseded"'],
993
993
  },
994
+ logs: {
995
+ description: 'Show logs for a pipeline run',
996
+ usage: 'eve pipeline logs <pipeline> <run-id> [--step <name>] [--follow]',
997
+ options: [
998
+ '--step <name> Show logs for a specific step only',
999
+ '--follow (-f) Stream live logs via SSE',
1000
+ '--project <id> Project ID (uses profile default)',
1001
+ ],
1002
+ examples: [
1003
+ 'eve pipeline logs deploy-test prun_xxx',
1004
+ 'eve pipeline logs deploy-test prun_xxx --step build',
1005
+ 'eve pipeline logs deploy-test prun_xxx --follow',
1006
+ ],
1007
+ },
994
1008
  },
995
- examples: ['eve pipeline list', 'eve pipeline run deploy-test --ref abc123 --env test'],
1009
+ examples: ['eve pipeline list', 'eve pipeline run deploy-test --ref abc123 --env test', 'eve pipeline logs deploy-test prun_xxx --follow'],
996
1010
  },
997
1011
  workflow: {
998
1012
  description: 'Inspect workflows defined in the project manifest (read-only in Phase 1).',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eve-horizon/cli",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "Eve Horizon CLI",
5
5
  "license": "MIT",
6
6
  "repository": {