bktide 1.0.1755547716 → 1.0.1755568192

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 (33) hide show
  1. package/dist/commands/ShowBuild.js +92 -0
  2. package/dist/commands/ShowBuild.js.map +1 -0
  3. package/dist/commands/index.js +1 -0
  4. package/dist/commands/index.js.map +1 -1
  5. package/dist/formatters/FormatterFactory.js +4 -0
  6. package/dist/formatters/FormatterFactory.js.map +1 -1
  7. package/dist/formatters/build-detail/AlfredFormatter.js +113 -0
  8. package/dist/formatters/build-detail/AlfredFormatter.js.map +1 -0
  9. package/dist/formatters/build-detail/Formatter.js +3 -0
  10. package/dist/formatters/build-detail/Formatter.js.map +1 -0
  11. package/dist/formatters/build-detail/JsonFormatter.js +132 -0
  12. package/dist/formatters/build-detail/JsonFormatter.js.map +1 -0
  13. package/dist/formatters/build-detail/PlainTextFormatter.js +851 -0
  14. package/dist/formatters/build-detail/PlainTextFormatter.js.map +1 -0
  15. package/dist/formatters/build-detail/index.js +21 -0
  16. package/dist/formatters/build-detail/index.js.map +1 -0
  17. package/dist/formatters/builds/PlainTextFormatter.js +4 -2
  18. package/dist/formatters/builds/PlainTextFormatter.js.map +1 -1
  19. package/dist/formatters/pipelines/PlainTextFormatter.js +3 -6
  20. package/dist/formatters/pipelines/PlainTextFormatter.js.map +1 -1
  21. package/dist/graphql/queries.js +185 -0
  22. package/dist/graphql/queries.js.map +1 -1
  23. package/dist/index.js +22 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/services/BuildkiteClient.js +87 -26
  26. package/dist/services/BuildkiteClient.js.map +1 -1
  27. package/dist/services/BuildkiteRestClient.js +8 -7
  28. package/dist/services/BuildkiteRestClient.js.map +1 -1
  29. package/dist/ui/theme.js +193 -8
  30. package/dist/ui/theme.js.map +1 -1
  31. package/dist/utils/terminal-links.js +165 -0
  32. package/dist/utils/terminal-links.js.map +1 -0
  33. package/package.json +2 -1
@@ -0,0 +1,851 @@
1
+ import { BaseBuildDetailFormatter } from './Formatter.js';
2
+ import { formatDistanceToNow } from 'date-fns';
3
+ import { htmlToText } from 'html-to-text';
4
+ import { formatEmptyState, formatError, SEMANTIC_COLORS, formatBuildStatus, formatTips, TipStyle, getStateIcon, getAnnotationIcon, getProgressIcon, BUILD_STATUS_THEME } from '../../ui/theme.js';
5
+ // Standard emoji mappings only
6
+ // Only map universally recognized emoji codes, not Buildkite-specific ones
7
+ const STANDARD_EMOJI = {
8
+ // Faces & emotions
9
+ ':smile:': '😊',
10
+ ':grin:': '😁',
11
+ ':joy:': '😂',
12
+ ':laughing:': '😆',
13
+ ':blush:': '😊',
14
+ ':heart_eyes:': '😍',
15
+ ':sob:': '😭',
16
+ ':cry:': '😢',
17
+ ':angry:': '😠',
18
+ ':rage:': '😡',
19
+ ':thinking:': '🤔',
20
+ ':confused:': '😕',
21
+ ':neutral_face:': '😐',
22
+ // Hands & gestures
23
+ ':thumbsup:': '👍',
24
+ ':thumbsdown:': '👎',
25
+ ':clap:': '👏',
26
+ ':wave:': '👋',
27
+ ':raised_hand:': '✋',
28
+ ':ok_hand:': '👌',
29
+ ':pray:': '🙏',
30
+ ':muscle:': '💪',
31
+ ':point_left:': '👈',
32
+ ':point_right:': '👉',
33
+ ':point_up:': '👆',
34
+ ':point_down:': '👇',
35
+ // Objects & symbols
36
+ ':heart:': '❤️',
37
+ ':broken_heart:': '💔',
38
+ ':star:': '⭐',
39
+ ':sparkles:': '✨',
40
+ ':boom:': '💥',
41
+ ':fire:': '🔥',
42
+ ':zap:': '⚡',
43
+ ':rocket:': '🚀',
44
+ ':sun:': '☀️',
45
+ ':moon:': '🌙',
46
+ ':cloud:': '☁️',
47
+ ':umbrella:': '☔',
48
+ ':snowflake:': '❄️',
49
+ // Status symbols
50
+ ':white_check_mark:': '✅',
51
+ ':x:': '❌',
52
+ ':warning:': '⚠️',
53
+ ':exclamation:': '❗',
54
+ ':question:': '❓',
55
+ ':heavy_plus_sign:': '➕',
56
+ ':heavy_minus_sign:': '➖',
57
+ ':heavy_check_mark:': '✔️',
58
+ // Common tools/tech (universally recognized)
59
+ ':computer:': '💻',
60
+ ':iphone:': '📱',
61
+ ':email:': '📧',
62
+ ':package:': '📦',
63
+ ':lock:': '🔒',
64
+ ':key:': '🔑',
65
+ ':mag:': '🔍',
66
+ ':bulb:': '💡',
67
+ ':books:': '📚',
68
+ ':memo:': '📝',
69
+ ':pencil:': '✏️',
70
+ ':art:': '🎨',
71
+ ':camera:': '📷',
72
+ ':movie_camera:': '🎥',
73
+ ':musical_note:': '🎵',
74
+ ':bell:': '🔔',
75
+ ':link:': '🔗',
76
+ ':paperclip:': '📎',
77
+ ':hourglass:': '⏳',
78
+ ':alarm_clock:': '⏰',
79
+ ':stopwatch:': '⏱️',
80
+ ':timer_clock:': '⏲️',
81
+ ':calendar:': '📅',
82
+ ':date:': '📅',
83
+ };
84
+ export class PlainTextFormatter extends BaseBuildDetailFormatter {
85
+ name = 'plain-text';
86
+ parseEmoji(text) {
87
+ if (!text)
88
+ return text;
89
+ // Only replace standard emoji codes, leave Buildkite-specific ones as-is
90
+ return text.replace(/:[\w_]+:/g, (match) => {
91
+ return STANDARD_EMOJI[match] || match;
92
+ });
93
+ }
94
+ formatBuildDetail(buildData, options) {
95
+ // Handle error cases first
96
+ if (options?.hasError || !buildData) {
97
+ return this.formatErrorState(options);
98
+ }
99
+ const build = buildData.build;
100
+ // Choose display mode based on options
101
+ if (options?.summary) {
102
+ return this.formatSummaryLine(build);
103
+ }
104
+ if (options?.full) {
105
+ return this.formatFullDetails(build, options);
106
+ }
107
+ // Default: contextual display based on state
108
+ switch (build.state) {
109
+ case 'FAILED':
110
+ return this.formatFailedBuild(build, options);
111
+ case 'RUNNING':
112
+ return this.formatRunningBuild(build, options);
113
+ case 'BLOCKED':
114
+ return this.formatBlockedBuild(build, options);
115
+ case 'PASSED':
116
+ return this.formatPassedBuild(build, options);
117
+ case 'CANCELED':
118
+ return this.formatCanceledBuild(build, options);
119
+ default:
120
+ return this.formatDefaultBuild(build, options);
121
+ }
122
+ }
123
+ formatErrorState(options) {
124
+ if (options?.errorType === 'not_found') {
125
+ return formatEmptyState('Build not found', ['Check the build reference format', 'Verify the build exists']);
126
+ }
127
+ return formatError(options?.errorMessage || 'Unknown error');
128
+ }
129
+ formatSummaryLine(build) {
130
+ const statusIcon = this.getStatusIcon(build.state);
131
+ const coloredIcon = this.colorizeStatusIcon(statusIcon, build.state);
132
+ const duration = this.formatDuration(build);
133
+ const age = this.formatAge(build.createdAt);
134
+ const stateFormatted = formatBuildStatus(build.state, { useSymbol: false });
135
+ const branch = SEMANTIC_COLORS.identifier(build.branch);
136
+ return `${coloredIcon} ${SEMANTIC_COLORS.label(`#${build.number}`)} ${stateFormatted} • ${duration} • ${branch} • ${age}`;
137
+ }
138
+ formatPassedBuild(build, options) {
139
+ const lines = [];
140
+ // Header line
141
+ lines.push(this.formatHeader(build));
142
+ lines.push(this.formatCommitInfo(build));
143
+ // Show annotations summary if present
144
+ if (build.annotations?.edges?.length > 0) {
145
+ lines.push('');
146
+ lines.push(this.formatAnnotationSummary(build.annotations.edges));
147
+ if (!options?.annotations) {
148
+ lines.push('');
149
+ const tips = formatTips(['Use --annotations to view annotation details'], TipStyle.GROUPED);
150
+ lines.push(tips);
151
+ }
152
+ }
153
+ // Show annotations detail if requested
154
+ if (options?.annotations) {
155
+ lines.push('');
156
+ lines.push(this.formatAnnotationDetails(build.annotations.edges));
157
+ }
158
+ return lines.join('\n');
159
+ }
160
+ formatFailedBuild(build, options) {
161
+ const lines = [];
162
+ // Header line
163
+ lines.push(this.formatHeader(build));
164
+ lines.push(this.formatCommitInfo(build));
165
+ lines.push('');
166
+ // Failed jobs summary
167
+ const failedJobs = this.getFailedJobs(build.jobs?.edges);
168
+ const allHints = [];
169
+ if (failedJobs.length > 0) {
170
+ const { summary, hints } = this.formatFailedJobsSummaryWithHints(failedJobs, options);
171
+ lines.push(summary);
172
+ allHints.push(...hints);
173
+ }
174
+ // Annotation summary
175
+ if (build.annotations?.edges?.length > 0) {
176
+ lines.push(this.formatAnnotationSummary(build.annotations.edges));
177
+ }
178
+ // Show detailed job info if requested
179
+ if (options?.jobs || options?.failed) {
180
+ lines.push('');
181
+ lines.push(this.formatJobDetails(build.jobs?.edges, options));
182
+ }
183
+ // Show annotations detail if requested
184
+ if (options?.annotations) {
185
+ lines.push('');
186
+ lines.push(this.formatAnnotationDetails(build.annotations.edges));
187
+ }
188
+ // Collect all hints for more info
189
+ if (!options?.failed && failedJobs.length > 0) {
190
+ allHints.push('Use --failed to show failure details');
191
+ }
192
+ if (!options?.annotations && build.annotations?.edges?.length > 0) {
193
+ allHints.push('Use --annotations to view annotation details');
194
+ }
195
+ // Display all hints together
196
+ if (allHints.length > 0) {
197
+ lines.push('');
198
+ lines.push(formatTips(allHints, TipStyle.GROUPED));
199
+ }
200
+ return lines.join('\n');
201
+ }
202
+ formatRunningBuild(build, options) {
203
+ const lines = [];
204
+ // Header line
205
+ lines.push(this.formatHeader(build));
206
+ lines.push(this.formatCommitInfo(build));
207
+ lines.push('');
208
+ // Progress information
209
+ const jobStats = this.getJobStats(build.jobs?.edges);
210
+ lines.push(`Progress: ${SEMANTIC_COLORS.count(String(jobStats.completed))}/${jobStats.total} complete, ${SEMANTIC_COLORS.info(String(jobStats.running))} running, ${jobStats.queued} queued`);
211
+ // Show running jobs
212
+ const runningJobs = this.getRunningJobs(build.jobs?.edges);
213
+ if (runningJobs.length > 0) {
214
+ const labels = runningJobs.map(j => this.parseEmoji(j.node.label)).join(', ');
215
+ lines.push(`${SEMANTIC_COLORS.info('Running')}: ${labels}`);
216
+ }
217
+ // Annotation summary
218
+ if (build.annotations?.edges?.length > 0) {
219
+ lines.push('');
220
+ lines.push(this.formatAnnotationSummary(build.annotations.edges));
221
+ }
222
+ // Show job details if requested
223
+ if (options?.jobs) {
224
+ lines.push('');
225
+ lines.push(this.formatJobDetails(build.jobs?.edges, options));
226
+ }
227
+ // Show annotations detail if requested
228
+ if (options?.annotations) {
229
+ lines.push('');
230
+ lines.push(this.formatAnnotationDetails(build.annotations.edges));
231
+ }
232
+ return lines.join('\n');
233
+ }
234
+ formatBlockedBuild(build, options) {
235
+ const lines = [];
236
+ // Header line
237
+ lines.push(this.formatHeader(build));
238
+ lines.push(this.formatCommitInfo(build));
239
+ lines.push('');
240
+ // Blocked information
241
+ const blockedJobs = this.getBlockedJobs(build.jobs?.edges);
242
+ if (blockedJobs.length > 0) {
243
+ lines.push(`${getProgressIcon('BLOCKED_MESSAGE')} Blocked: "${blockedJobs[0].node.label}" (manual unblock required)`);
244
+ }
245
+ // Show jobs summary
246
+ const jobStats = this.getJobStats(build.jobs?.edges);
247
+ if (jobStats.completed > 0) {
248
+ lines.push(`${getStateIcon('PASSED')} ${jobStats.completed} jobs passed before block`);
249
+ }
250
+ // Annotation summary
251
+ if (build.annotations?.edges?.length > 0) {
252
+ lines.push('');
253
+ lines.push(this.formatAnnotationSummary(build.annotations.edges));
254
+ }
255
+ // Show job details if requested
256
+ if (options?.jobs) {
257
+ lines.push('');
258
+ lines.push(this.formatJobDetails(build.jobs?.edges, options));
259
+ }
260
+ // Show annotations detail if requested
261
+ if (options?.annotations) {
262
+ lines.push('');
263
+ lines.push(this.formatAnnotationDetails(build.annotations.edges));
264
+ }
265
+ return lines.join('\n');
266
+ }
267
+ formatCanceledBuild(build, options) {
268
+ const lines = [];
269
+ // Header line
270
+ lines.push(this.formatHeader(build));
271
+ lines.push(this.formatCommitInfo(build));
272
+ lines.push('');
273
+ // Canceled information
274
+ if (build.createdBy) {
275
+ const creator = build.createdBy.name || build.createdBy.email;
276
+ lines.push(`Canceled by: ${creator}`);
277
+ }
278
+ // Show jobs summary
279
+ const jobStats = this.getJobStats(build.jobs?.edges);
280
+ lines.push(`Completed: ${jobStats.completed}/${jobStats.total} jobs before cancellation`);
281
+ // Show job details if requested
282
+ if (options?.jobs) {
283
+ lines.push('');
284
+ lines.push(this.formatJobDetails(build.jobs?.edges, options));
285
+ }
286
+ return lines.join('\n');
287
+ }
288
+ formatDefaultBuild(build, options) {
289
+ return this.formatPassedBuild(build, options);
290
+ }
291
+ formatFullDetails(build, options) {
292
+ const lines = [];
293
+ // Full header information
294
+ lines.push(this.formatHeader(build));
295
+ lines.push(this.formatCommitInfo(build));
296
+ lines.push('');
297
+ // Build metadata
298
+ lines.push('Build Details:');
299
+ lines.push(` URL: ${build.url}`);
300
+ lines.push(` Organization: ${build.organization?.name || 'Unknown'}`);
301
+ lines.push(` Pipeline: ${build.pipeline?.name || 'Unknown'}`);
302
+ if (build.pullRequest) {
303
+ // Try to construct PR URL from repository URL
304
+ const repoUrl = build.pipeline?.repository?.url;
305
+ if (repoUrl && repoUrl.includes('github.com')) {
306
+ // Extract owner/repo from various GitHub URL formats
307
+ const match = repoUrl.match(/github\.com[/:]([\w-]+)\/([\w-]+)/);
308
+ if (match && build.pullRequest.id) {
309
+ // Extract PR number from GraphQL ID if possible
310
+ // GitHub PR IDs often contain the number
311
+ const prUrl = `https://github.com/${match[1]}/${match[2]}/pull/${build.pullRequest.id}`;
312
+ lines.push(` Pull Request: ${SEMANTIC_COLORS.url(prUrl)}`);
313
+ }
314
+ else {
315
+ lines.push(` Pull Request: ${build.pullRequest.id}`);
316
+ }
317
+ }
318
+ else {
319
+ lines.push(` Pull Request: ${build.pullRequest.id}`);
320
+ }
321
+ }
322
+ if (build.triggeredFrom) {
323
+ lines.push(` Triggered from: ${build.triggeredFrom.pipeline?.name} #${build.triggeredFrom.number}`);
324
+ }
325
+ lines.push('');
326
+ // Jobs section
327
+ lines.push('Jobs:');
328
+ lines.push(this.formatJobDetails(build.jobs?.edges, { ...options, full: true }));
329
+ // Annotations section
330
+ if (build.annotations?.edges?.length > 0) {
331
+ lines.push('');
332
+ lines.push('Annotations:');
333
+ lines.push(this.formatAnnotationDetails(build.annotations.edges));
334
+ }
335
+ return lines.join('\n');
336
+ }
337
+ formatHeader(build) {
338
+ const statusIcon = this.getStatusIcon(build.state);
339
+ // Apply appropriate color to the icon based on the state
340
+ const coloredIcon = this.colorizeStatusIcon(statusIcon, build.state);
341
+ const stateFormatted = formatBuildStatus(build.state, { useSymbol: false });
342
+ const duration = this.formatDuration(build);
343
+ const age = this.formatAge(build.createdAt);
344
+ const branch = SEMANTIC_COLORS.identifier(build.branch);
345
+ return `${coloredIcon} ${SEMANTIC_COLORS.label(`#${build.number}`)} ${stateFormatted} • ${duration} • ${branch} • ${age}`;
346
+ }
347
+ formatCommitInfo(build) {
348
+ const shortSha = build.commit ? build.commit.substring(0, 7) : 'unknown';
349
+ const message = build.message || 'No commit message';
350
+ const truncatedMessage = message.length > 60 ? message.substring(0, 57) + '...' : message;
351
+ return ` "${truncatedMessage}" (${shortSha})`;
352
+ }
353
+ formatAnnotationSummary(annotations) {
354
+ if (!annotations || annotations.length === 0) {
355
+ return '';
356
+ }
357
+ const lines = [];
358
+ const total = annotations.length;
359
+ // Header with count
360
+ const counts = this.countAnnotationsByStyle(annotations);
361
+ const countParts = [];
362
+ if (counts.ERROR > 0)
363
+ countParts.push(SEMANTIC_COLORS.error(`${counts.ERROR} error${counts.ERROR > 1 ? 's' : ''}`));
364
+ if (counts.WARNING > 0)
365
+ countParts.push(SEMANTIC_COLORS.warning(`${counts.WARNING} warning${counts.WARNING > 1 ? 's' : ''}`));
366
+ if (counts.INFO > 0)
367
+ countParts.push(SEMANTIC_COLORS.info(`${counts.INFO} info`));
368
+ if (counts.SUCCESS > 0)
369
+ countParts.push(SEMANTIC_COLORS.success(`${counts.SUCCESS} success`));
370
+ lines.push(`${getAnnotationIcon('DEFAULT')} ${SEMANTIC_COLORS.count(String(total))} annotation${total > 1 ? 's' : ''}: ${countParts.join(', ')}`);
371
+ // List each annotation with style and context
372
+ const grouped = this.groupAnnotationsByStyle(annotations);
373
+ const styleOrder = ['ERROR', 'WARNING', 'INFO', 'SUCCESS'];
374
+ for (const style of styleOrder) {
375
+ if (grouped[style]) {
376
+ for (const annotation of grouped[style]) {
377
+ const icon = this.getAnnotationIcon(style);
378
+ const context = annotation.node.context || 'default';
379
+ const styleColored = this.colorizeAnnotationStyle(style);
380
+ lines.push(` ${icon} ${styleColored}: ${context}`);
381
+ }
382
+ }
383
+ }
384
+ return lines.join('\n');
385
+ }
386
+ formatAnnotationDetails(annotations) {
387
+ const lines = [];
388
+ // Group annotations by style
389
+ const grouped = this.groupAnnotationsByStyle(annotations);
390
+ const styleOrder = ['ERROR', 'WARNING', 'INFO', 'SUCCESS'];
391
+ for (const style of styleOrder) {
392
+ if (grouped[style]) {
393
+ for (const annotation of grouped[style]) {
394
+ const icon = this.getAnnotationIcon(style);
395
+ const context = annotation.node.context || 'default';
396
+ const styleColored = this.colorizeAnnotationStyle(style);
397
+ // When showing annotation details, always show the body text
398
+ lines.push(`${icon} ${styleColored}: ${context}`);
399
+ const body = htmlToText(annotation.node.body?.html || '', {
400
+ wordwrap: 80,
401
+ preserveNewlines: true
402
+ });
403
+ lines.push(body.split('\n').map(l => ` ${l}`).join('\n'));
404
+ lines.push('');
405
+ }
406
+ }
407
+ }
408
+ return lines.join('\n').trim();
409
+ }
410
+ formatJobDetails(jobs, options) {
411
+ if (!jobs || jobs.length === 0) {
412
+ return 'No jobs found';
413
+ }
414
+ const lines = [];
415
+ const jobStats = this.getJobStats(jobs);
416
+ // Summary line
417
+ const parts = [];
418
+ if (jobStats.passed > 0)
419
+ parts.push(`${getStateIcon('PASSED')} ${jobStats.passed} passed`);
420
+ if (jobStats.failed > 0)
421
+ parts.push(`${getStateIcon('FAILED')} ${jobStats.failed} failed`);
422
+ if (jobStats.running > 0)
423
+ parts.push(`${getStateIcon('RUNNING')} ${jobStats.running} running`);
424
+ if (jobStats.blocked > 0)
425
+ parts.push(`${getStateIcon('BLOCKED')} ${jobStats.blocked} blocked`);
426
+ if (jobStats.skipped > 0)
427
+ parts.push(`${getStateIcon('SKIPPED')} ${jobStats.skipped} skipped`);
428
+ lines.push(`Jobs: ${parts.join(' ')}`);
429
+ lines.push('');
430
+ // Filter jobs based on options
431
+ let filteredJobs = jobs;
432
+ if (options?.failed) {
433
+ filteredJobs = this.getFailedJobs(jobs);
434
+ }
435
+ // Group jobs by state
436
+ const grouped = this.groupJobsByState(filteredJobs);
437
+ for (const [state, stateJobs] of Object.entries(grouped)) {
438
+ if (stateJobs.length === 0)
439
+ continue;
440
+ const icon = this.getJobStateIcon(state);
441
+ const stateColored = this.colorizeJobState(state);
442
+ lines.push(`${icon} ${stateColored} (${SEMANTIC_COLORS.count(String(stateJobs.length))}):`);
443
+ for (const job of stateJobs) {
444
+ const label = this.parseEmoji(job.node.label);
445
+ const duration = this.formatJobDuration(job.node);
446
+ // Basic job line
447
+ lines.push(` ${label} (${duration})`);
448
+ // Show additional details if --jobs or --full
449
+ if (options?.jobs || options?.full) {
450
+ // Timing details
451
+ if (job.node.startedAt) {
452
+ const startTime = new Date(job.node.startedAt).toLocaleTimeString();
453
+ const endTime = job.node.finishedAt
454
+ ? new Date(job.node.finishedAt).toLocaleTimeString()
455
+ : 'still running';
456
+ lines.push(` ${SEMANTIC_COLORS.dim(`${getProgressIcon('TIMING')} ${startTime} → ${endTime}`)}`);
457
+ }
458
+ // Parallel group info
459
+ if (job.node.parallelGroupIndex !== undefined && job.node.parallelGroupTotal) {
460
+ lines.push(` ${SEMANTIC_COLORS.dim(`${getProgressIcon('PARALLEL')} Parallel: ${job.node.parallelGroupIndex + 1}/${job.node.parallelGroupTotal}`)}`);
461
+ }
462
+ // Retry info
463
+ if (job.node.retried) {
464
+ lines.push(` ${SEMANTIC_COLORS.warning(`${getProgressIcon('RETRY')} Retried`)}`);
465
+ }
466
+ }
467
+ }
468
+ lines.push('');
469
+ }
470
+ return lines.join('\n').trim();
471
+ }
472
+ formatFailedJobsSummaryWithHints(failedJobs, options) {
473
+ const hints = [];
474
+ const summary = this.formatFailedJobsSummary(failedJobs, options, hints);
475
+ return { summary, hints };
476
+ }
477
+ formatFailedJobsSummary(failedJobs, options, hints) {
478
+ const lines = [];
479
+ // Group identical jobs by label
480
+ const jobGroups = this.groupJobsByLabel(failedJobs);
481
+ // Show all groups if --all-jobs, otherwise limit to 10
482
+ const displayGroups = options?.allJobs
483
+ ? jobGroups
484
+ : jobGroups.slice(0, 10);
485
+ for (const group of displayGroups) {
486
+ const label = this.parseEmoji(group.label);
487
+ if (group.count === 1) {
488
+ const duration = this.formatJobDuration(group.jobs[0].node);
489
+ lines.push(` ${SEMANTIC_COLORS.error('Failed')}: ${label} - ran ${duration}`);
490
+ }
491
+ else {
492
+ // Multiple jobs with same label - show detailed breakdown
493
+ const statusParts = [];
494
+ if (group.stateCounts.failed > 0) {
495
+ statusParts.push(`${group.stateCounts.failed} failed`);
496
+ }
497
+ if (group.stateCounts.broken > 0) {
498
+ statusParts.push(`${group.stateCounts.broken} broken`);
499
+ }
500
+ if (group.stateCounts.notStarted > 0) {
501
+ statusParts.push(`${group.stateCounts.notStarted} not started`);
502
+ }
503
+ if (group.stateCounts.passed > 0) {
504
+ statusParts.push(`${group.stateCounts.passed} passed`);
505
+ }
506
+ if (group.stateCounts.other > 0) {
507
+ statusParts.push(`${group.stateCounts.other} other`);
508
+ }
509
+ const statusInfo = statusParts.join(', ') || 'various states';
510
+ // Show parallel info if it's a parallel job group
511
+ const parallelInfo = group.parallelTotal > 0 ? ` (${group.count}/${group.parallelTotal} parallel)` : ` (${SEMANTIC_COLORS.count(String(group.count))} jobs)`;
512
+ lines.push(` ${SEMANTIC_COLORS.error('Failed')}: ${label}${parallelInfo}: ${statusInfo}`);
513
+ }
514
+ }
515
+ // Add summary if there are more job types and not showing all
516
+ if (!options?.allJobs) {
517
+ const remaining = jobGroups.length - displayGroups.length;
518
+ if (remaining > 0) {
519
+ lines.push(` ${SEMANTIC_COLORS.muted(`...and ${remaining} more job types`)}`);
520
+ // If hints array is provided, add hint there; otherwise format inline
521
+ if (hints) {
522
+ hints.push('Use --all-jobs to show all jobs');
523
+ }
524
+ else {
525
+ lines.push('');
526
+ const tips = formatTips(['Use --all-jobs to show all jobs'], TipStyle.GROUPED);
527
+ lines.push(tips);
528
+ }
529
+ }
530
+ }
531
+ return lines.join('\n');
532
+ }
533
+ groupJobsByLabel(jobs) {
534
+ const groups = new Map();
535
+ for (const job of jobs) {
536
+ const fullLabel = job.node.label || 'Unnamed job';
537
+ // Strip parallel job index from label for grouping
538
+ // e.g., "deposit_and_filing_schedule_calculator rspec (1/22)" -> "deposit_and_filing_schedule_calculator rspec"
539
+ const baseLabel = fullLabel.replace(/\s*\(\d+\/\d+\)\s*$/, '').trim();
540
+ if (!groups.has(baseLabel)) {
541
+ groups.set(baseLabel, {
542
+ label: baseLabel,
543
+ count: 0,
544
+ jobs: [],
545
+ parallelTotal: 0,
546
+ stateCounts: {
547
+ failed: 0,
548
+ broken: 0,
549
+ notStarted: 0,
550
+ passed: 0,
551
+ other: 0
552
+ }
553
+ });
554
+ }
555
+ const group = groups.get(baseLabel);
556
+ group.count++;
557
+ group.jobs.push(job);
558
+ // Track the maximum parallel total for this job group
559
+ if (job.node.parallelGroupTotal && job.node.parallelGroupTotal > group.parallelTotal) {
560
+ group.parallelTotal = job.node.parallelGroupTotal;
561
+ }
562
+ // Count by state
563
+ const state = job.node.state?.toUpperCase();
564
+ // Use exit status as source of truth when available
565
+ // Note: exitStatus comes as a string from Buildkite API
566
+ if (job.node.exitStatus !== null && job.node.exitStatus !== undefined) {
567
+ const exitCode = parseInt(job.node.exitStatus, 10);
568
+ if (exitCode === 0) {
569
+ group.stateCounts.passed++;
570
+ }
571
+ else {
572
+ group.stateCounts.failed++;
573
+ }
574
+ }
575
+ else if (!job.node.startedAt) {
576
+ group.stateCounts.notStarted++;
577
+ }
578
+ else if (state === 'FINISHED' || state === 'COMPLETED') {
579
+ // For finished jobs without exit status, check passed field
580
+ if (job.node.passed === true) {
581
+ group.stateCounts.passed++;
582
+ }
583
+ else if (job.node.passed === false) {
584
+ group.stateCounts.failed++;
585
+ }
586
+ else {
587
+ group.stateCounts.other++;
588
+ }
589
+ }
590
+ else if (state === 'FAILED') {
591
+ group.stateCounts.failed++;
592
+ }
593
+ else if (state === 'BROKEN') {
594
+ group.stateCounts.broken++;
595
+ }
596
+ else if (state === 'PASSED' || job.node.passed === true) {
597
+ group.stateCounts.passed++;
598
+ }
599
+ else {
600
+ group.stateCounts.other++;
601
+ }
602
+ }
603
+ // Convert to array and sort by count (most failures first)
604
+ return Array.from(groups.values())
605
+ .sort((a, b) => b.count - a.count);
606
+ }
607
+ formatDuration(build) {
608
+ if (!build.startedAt) {
609
+ return 'not started';
610
+ }
611
+ const start = new Date(build.startedAt);
612
+ const end = build.finishedAt ? new Date(build.finishedAt) : new Date();
613
+ const durationMs = end.getTime() - start.getTime();
614
+ const minutes = Math.floor(durationMs / 60000);
615
+ const seconds = Math.floor((durationMs % 60000) / 1000);
616
+ if (build.state === 'RUNNING') {
617
+ return `${minutes}m ${seconds}s elapsed`;
618
+ }
619
+ return `${minutes}m ${seconds}s`;
620
+ }
621
+ formatJobDuration(job) {
622
+ if (!job.startedAt) {
623
+ return 'not started';
624
+ }
625
+ const start = new Date(job.startedAt);
626
+ const end = job.finishedAt ? new Date(job.finishedAt) : new Date();
627
+ const durationMs = end.getTime() - start.getTime();
628
+ const minutes = Math.floor(durationMs / 60000);
629
+ const seconds = Math.floor((durationMs % 60000) / 1000);
630
+ return `${minutes}m ${seconds}s`;
631
+ }
632
+ formatAge(createdAt) {
633
+ return formatDistanceToNow(new Date(createdAt), { addSuffix: true });
634
+ }
635
+ colorizeJobState(state) {
636
+ switch (state.toLowerCase()) {
637
+ case 'failed':
638
+ return SEMANTIC_COLORS.error(state);
639
+ case 'passed':
640
+ return SEMANTIC_COLORS.success(state);
641
+ case 'running':
642
+ return SEMANTIC_COLORS.info(state);
643
+ case 'blocked':
644
+ return SEMANTIC_COLORS.warning(state);
645
+ case 'skipped':
646
+ case 'canceled':
647
+ return SEMANTIC_COLORS.muted(state);
648
+ default:
649
+ return state;
650
+ }
651
+ }
652
+ getStatusIcon(state) {
653
+ return getStateIcon(state);
654
+ }
655
+ getJobStateIcon(state) {
656
+ return getStateIcon(state);
657
+ }
658
+ getAnnotationIcon(style) {
659
+ return getAnnotationIcon(style);
660
+ }
661
+ colorizeAnnotationStyle(style) {
662
+ switch (style.toUpperCase()) {
663
+ case 'ERROR':
664
+ return SEMANTIC_COLORS.error(style.toLowerCase());
665
+ case 'WARNING':
666
+ return SEMANTIC_COLORS.warning(style.toLowerCase());
667
+ case 'INFO':
668
+ return SEMANTIC_COLORS.info(style.toLowerCase());
669
+ case 'SUCCESS':
670
+ return SEMANTIC_COLORS.success(style.toLowerCase());
671
+ default:
672
+ return style.toLowerCase();
673
+ }
674
+ }
675
+ colorizeStatusIcon(icon, state) {
676
+ const upperState = state.toUpperCase();
677
+ const theme = BUILD_STATUS_THEME[upperState];
678
+ if (!theme) {
679
+ return SEMANTIC_COLORS.muted(icon);
680
+ }
681
+ return theme.color(icon);
682
+ }
683
+ getJobStats(jobs) {
684
+ const stats = {
685
+ total: jobs?.length || 0,
686
+ passed: 0,
687
+ failed: 0,
688
+ running: 0,
689
+ blocked: 0,
690
+ skipped: 0,
691
+ queued: 0,
692
+ completed: 0
693
+ };
694
+ if (!jobs)
695
+ return stats;
696
+ for (const job of jobs) {
697
+ const state = job.node.state?.toUpperCase() || '';
698
+ // If we have an exit status, use that as the source of truth
699
+ // Note: exitStatus comes as a string from Buildkite API
700
+ if (job.node.exitStatus !== null && job.node.exitStatus !== undefined) {
701
+ const exitCode = parseInt(job.node.exitStatus, 10);
702
+ if (exitCode === 0) {
703
+ stats.passed++;
704
+ stats.completed++;
705
+ }
706
+ else {
707
+ stats.failed++;
708
+ stats.completed++;
709
+ }
710
+ }
711
+ else if (state === 'RUNNING') {
712
+ stats.running++;
713
+ }
714
+ else if (state === 'BLOCKED') {
715
+ stats.blocked++;
716
+ }
717
+ else if (state === 'SKIPPED' || state === 'CANCELED') {
718
+ stats.skipped++;
719
+ stats.completed++;
720
+ }
721
+ else if (state === 'SCHEDULED' || state === 'ASSIGNED') {
722
+ stats.queued++;
723
+ }
724
+ else if (state === 'FINISHED' || state === 'COMPLETED') {
725
+ // For finished jobs without exit status, check passed field
726
+ if (job.node.passed === true) {
727
+ stats.passed++;
728
+ stats.completed++;
729
+ }
730
+ else if (job.node.passed === false) {
731
+ stats.failed++;
732
+ stats.completed++;
733
+ }
734
+ }
735
+ else if (state === 'PASSED' || job.node.passed === true) {
736
+ stats.passed++;
737
+ stats.completed++;
738
+ }
739
+ else if (state === 'FAILED' || state === 'BROKEN' || job.node.passed === false) {
740
+ stats.failed++;
741
+ stats.completed++;
742
+ }
743
+ }
744
+ return stats;
745
+ }
746
+ getFailedJobs(jobs) {
747
+ if (!jobs)
748
+ return [];
749
+ return jobs.filter(job => {
750
+ const state = job.node.state?.toUpperCase();
751
+ // If we have an exit status, use that as the source of truth
752
+ // Note: exitStatus comes as a string from Buildkite API
753
+ if (job.node.exitStatus !== null && job.node.exitStatus !== undefined) {
754
+ const exitCode = parseInt(job.node.exitStatus, 10);
755
+ return exitCode !== 0;
756
+ }
757
+ // Otherwise fall back to state
758
+ return state === 'FAILED' || state === 'BROKEN' || job.node.passed === false;
759
+ });
760
+ }
761
+ getRunningJobs(jobs) {
762
+ if (!jobs)
763
+ return [];
764
+ return jobs.filter(job => job.node.state?.toLowerCase() === 'running');
765
+ }
766
+ getBlockedJobs(jobs) {
767
+ if (!jobs)
768
+ return [];
769
+ return jobs.filter(job => job.node.state?.toLowerCase() === 'blocked');
770
+ }
771
+ groupJobsByState(jobs) {
772
+ const grouped = {
773
+ 'Failed': [],
774
+ 'Passed': [],
775
+ 'Running': [],
776
+ 'Blocked': [],
777
+ 'Skipped': []
778
+ };
779
+ if (!jobs)
780
+ return grouped;
781
+ for (const job of jobs) {
782
+ const state = job.node.state?.toUpperCase() || '';
783
+ // If we have an exit status, use that as the source of truth
784
+ // Note: exitStatus comes as a string from Buildkite API
785
+ if (job.node.exitStatus !== null && job.node.exitStatus !== undefined) {
786
+ const exitCode = parseInt(job.node.exitStatus, 10);
787
+ if (exitCode === 0) {
788
+ grouped['Passed'].push(job);
789
+ }
790
+ else {
791
+ grouped['Failed'].push(job);
792
+ }
793
+ }
794
+ else if (state === 'RUNNING') {
795
+ grouped['Running'].push(job);
796
+ }
797
+ else if (state === 'BLOCKED') {
798
+ grouped['Blocked'].push(job);
799
+ }
800
+ else if (state === 'SKIPPED' || state === 'CANCELED') {
801
+ grouped['Skipped'].push(job);
802
+ }
803
+ else if (state === 'FINISHED' || state === 'COMPLETED') {
804
+ // For finished jobs without exit status, check passed field
805
+ if (job.node.passed === true) {
806
+ grouped['Passed'].push(job);
807
+ }
808
+ else if (job.node.passed === false) {
809
+ grouped['Failed'].push(job);
810
+ }
811
+ }
812
+ else if (state === 'PASSED' || job.node.passed === true) {
813
+ grouped['Passed'].push(job);
814
+ }
815
+ else if (state === 'FAILED' || state === 'BROKEN' || job.node.passed === false) {
816
+ grouped['Failed'].push(job);
817
+ }
818
+ }
819
+ return grouped;
820
+ }
821
+ countAnnotationsByStyle(annotations) {
822
+ const counts = {
823
+ ERROR: 0,
824
+ WARNING: 0,
825
+ INFO: 0,
826
+ SUCCESS: 0
827
+ };
828
+ for (const annotation of annotations) {
829
+ const style = annotation.node.style?.toUpperCase() || 'INFO';
830
+ if (style in counts) {
831
+ counts[style]++;
832
+ }
833
+ }
834
+ return counts;
835
+ }
836
+ groupAnnotationsByStyle(annotations) {
837
+ const grouped = {};
838
+ for (const annotation of annotations) {
839
+ const style = annotation.node.style?.toUpperCase() || 'INFO';
840
+ if (!grouped[style]) {
841
+ grouped[style] = [];
842
+ }
843
+ grouped[style].push(annotation);
844
+ }
845
+ return grouped;
846
+ }
847
+ formatError(action, error) {
848
+ return formatError(action, error);
849
+ }
850
+ }
851
+ //# sourceMappingURL=PlainTextFormatter.js.map