bktide 1.0.1755267617 → 1.0.1755559112

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 (93) hide show
  1. package/README.md +107 -1
  2. package/WORKFLOW_README.md +1 -1
  3. package/completions/bktide-dynamic.fish +171 -0
  4. package/completions/bktide.bash +124 -0
  5. package/completions/bktide.fish +107 -0
  6. package/completions/bktide.zsh +139 -0
  7. package/dist/commands/BaseCommand.js +7 -7
  8. package/dist/commands/BaseCommand.js.map +1 -1
  9. package/dist/commands/GenerateCompletions.js +238 -0
  10. package/dist/commands/GenerateCompletions.js.map +1 -0
  11. package/dist/commands/ListAnnotations.js +7 -0
  12. package/dist/commands/ListAnnotations.js.map +1 -1
  13. package/dist/commands/ListBuilds.js +67 -3
  14. package/dist/commands/ListBuilds.js.map +1 -1
  15. package/dist/commands/ListOrganizations.js +6 -0
  16. package/dist/commands/ListOrganizations.js.map +1 -1
  17. package/dist/commands/ListPipelines.js +87 -12
  18. package/dist/commands/ListPipelines.js.map +1 -1
  19. package/dist/commands/ManageToken.js +32 -9
  20. package/dist/commands/ManageToken.js.map +1 -1
  21. package/dist/commands/ShowBuild.js +88 -0
  22. package/dist/commands/ShowBuild.js.map +1 -0
  23. package/dist/commands/ShowViewer.js +7 -1
  24. package/dist/commands/ShowViewer.js.map +1 -1
  25. package/dist/commands/index.js +2 -0
  26. package/dist/commands/index.js.map +1 -1
  27. package/dist/formatters/FormatterFactory.js +4 -0
  28. package/dist/formatters/FormatterFactory.js.map +1 -1
  29. package/dist/formatters/annotations/PlainTextFormatter.js +37 -9
  30. package/dist/formatters/annotations/PlainTextFormatter.js.map +1 -1
  31. package/dist/formatters/build-detail/AlfredFormatter.js +113 -0
  32. package/dist/formatters/build-detail/AlfredFormatter.js.map +1 -0
  33. package/dist/formatters/build-detail/Formatter.js +3 -0
  34. package/dist/formatters/build-detail/Formatter.js.map +1 -0
  35. package/dist/formatters/build-detail/JsonFormatter.js +132 -0
  36. package/dist/formatters/build-detail/JsonFormatter.js.map +1 -0
  37. package/dist/formatters/build-detail/PlainTextFormatter.js +680 -0
  38. package/dist/formatters/build-detail/PlainTextFormatter.js.map +1 -0
  39. package/dist/formatters/build-detail/index.js +21 -0
  40. package/dist/formatters/build-detail/index.js.map +1 -0
  41. package/dist/formatters/builds/PlainTextFormatter.js +82 -60
  42. package/dist/formatters/builds/PlainTextFormatter.js.map +1 -1
  43. package/dist/formatters/errors/AlfredFormatter.js +20 -0
  44. package/dist/formatters/errors/AlfredFormatter.js.map +1 -1
  45. package/dist/formatters/errors/PlainTextFormatter.js +121 -23
  46. package/dist/formatters/errors/PlainTextFormatter.js.map +1 -1
  47. package/dist/formatters/organizations/PlainTextFormatter.js +37 -6
  48. package/dist/formatters/organizations/PlainTextFormatter.js.map +1 -1
  49. package/dist/formatters/pipelines/AlfredFormatter.js.map +1 -1
  50. package/dist/formatters/pipelines/Formatter.js.map +1 -1
  51. package/dist/formatters/pipelines/JsonFormatter.js.map +1 -1
  52. package/dist/formatters/pipelines/PlainTextFormatter.js +165 -19
  53. package/dist/formatters/pipelines/PlainTextFormatter.js.map +1 -1
  54. package/dist/formatters/token/AlfredFormatter.js +15 -2
  55. package/dist/formatters/token/AlfredFormatter.js.map +1 -1
  56. package/dist/formatters/token/PlainTextFormatter.js +56 -18
  57. package/dist/formatters/token/PlainTextFormatter.js.map +1 -1
  58. package/dist/formatters/viewer/PlainTextFormatter.js +8 -7
  59. package/dist/formatters/viewer/PlainTextFormatter.js.map +1 -1
  60. package/dist/graphql/queries.js +181 -0
  61. package/dist/graphql/queries.js.map +1 -1
  62. package/dist/index.js +67 -6
  63. package/dist/index.js.map +1 -1
  64. package/dist/services/BuildkiteClient.js +61 -1
  65. package/dist/services/BuildkiteClient.js.map +1 -1
  66. package/dist/services/CredentialManager.js +80 -10
  67. package/dist/services/CredentialManager.js.map +1 -1
  68. package/dist/ui/help.js +69 -0
  69. package/dist/ui/help.js.map +1 -0
  70. package/dist/ui/progress.js +356 -0
  71. package/dist/ui/progress.js.map +1 -0
  72. package/dist/ui/reporter.js +111 -0
  73. package/dist/ui/reporter.js.map +1 -0
  74. package/dist/ui/responsive-table.js +183 -0
  75. package/dist/ui/responsive-table.js.map +1 -0
  76. package/dist/ui/spinner.js +20 -0
  77. package/dist/ui/spinner.js.map +1 -0
  78. package/dist/ui/symbols.js +46 -0
  79. package/dist/ui/symbols.js.map +1 -0
  80. package/dist/ui/table.js +32 -0
  81. package/dist/ui/table.js.map +1 -0
  82. package/dist/ui/theme.js +280 -0
  83. package/dist/ui/theme.js.map +1 -0
  84. package/dist/ui/width.js +111 -0
  85. package/dist/ui/width.js.map +1 -0
  86. package/dist/utils/alfred.js +6 -0
  87. package/dist/utils/alfred.js.map +1 -0
  88. package/dist/utils/cli-error-handler.js +35 -20
  89. package/dist/utils/cli-error-handler.js.map +1 -1
  90. package/dist/utils/pagination.js +92 -0
  91. package/dist/utils/pagination.js.map +1 -0
  92. package/info.plist +51 -218
  93. package/package.json +24 -5
@@ -0,0 +1,680 @@
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 } 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 status = this.getStatusIcon(build.state);
131
+ const duration = this.formatDuration(build);
132
+ const age = this.formatAge(build.createdAt);
133
+ return `${status} #${build.number} ${build.state.toLowerCase()} • ${duration} • ${build.branch} • ${age}`;
134
+ }
135
+ formatPassedBuild(build, options) {
136
+ const lines = [];
137
+ // Header line
138
+ lines.push(this.formatHeader(build));
139
+ lines.push(this.formatCommitInfo(build));
140
+ // Show annotations summary if present
141
+ if (build.annotations?.edges?.length > 0) {
142
+ lines.push('');
143
+ lines.push(this.formatAnnotationSummary(build.annotations.edges));
144
+ if (!options?.annotations) {
145
+ lines.push(SEMANTIC_COLORS.dim(`→ bin/bktide build ${build.number} --annotations # view annotations`));
146
+ }
147
+ }
148
+ // Show annotations detail if requested
149
+ if (options?.annotations) {
150
+ lines.push('');
151
+ lines.push(this.formatAnnotationDetails(build.annotations.edges, options));
152
+ }
153
+ return lines.join('\n');
154
+ }
155
+ formatFailedBuild(build, options) {
156
+ const lines = [];
157
+ // Header line
158
+ lines.push(this.formatHeader(build));
159
+ lines.push(this.formatCommitInfo(build));
160
+ lines.push('');
161
+ // Failed jobs summary
162
+ const failedJobs = this.getFailedJobs(build.jobs?.edges);
163
+ if (failedJobs.length > 0) {
164
+ lines.push(this.formatFailedJobsSummary(failedJobs));
165
+ }
166
+ // Annotation summary
167
+ if (build.annotations?.edges?.length > 0) {
168
+ lines.push(this.formatAnnotationSummary(build.annotations.edges));
169
+ }
170
+ // Show detailed job info if requested
171
+ if (options?.jobs || options?.failed) {
172
+ lines.push('');
173
+ lines.push(this.formatJobDetails(build.jobs?.edges, options));
174
+ }
175
+ // Show annotations detail if requested
176
+ if (options?.annotations) {
177
+ lines.push('');
178
+ lines.push(this.formatAnnotationDetails(build.annotations.edges, options));
179
+ }
180
+ // Hints for more info (no Tips label)
181
+ if (!options?.failed && failedJobs.length > 0) {
182
+ lines.push('');
183
+ lines.push(SEMANTIC_COLORS.dim(`→ bin/bktide build ${build.number} --failed # show failure details`));
184
+ }
185
+ if (!options?.annotations && build.annotations?.edges?.length > 0) {
186
+ lines.push(SEMANTIC_COLORS.dim(`→ bin/bktide build ${build.number} --annotations # view annotations`));
187
+ }
188
+ return lines.join('\n');
189
+ }
190
+ formatRunningBuild(build, options) {
191
+ const lines = [];
192
+ // Header line
193
+ lines.push(this.formatHeader(build));
194
+ lines.push(this.formatCommitInfo(build));
195
+ lines.push('');
196
+ // Progress information
197
+ const jobStats = this.getJobStats(build.jobs?.edges);
198
+ lines.push(`Progress: ${SEMANTIC_COLORS.count(String(jobStats.completed))}/${jobStats.total} complete, ${SEMANTIC_COLORS.info(String(jobStats.running))} running, ${jobStats.queued} queued`);
199
+ // Show running jobs
200
+ const runningJobs = this.getRunningJobs(build.jobs?.edges);
201
+ if (runningJobs.length > 0) {
202
+ const labels = runningJobs.map(j => this.parseEmoji(j.node.label)).join(', ');
203
+ lines.push(`${SEMANTIC_COLORS.info('Running')}: ${labels}`);
204
+ }
205
+ // Show job details if requested
206
+ if (options?.jobs) {
207
+ lines.push('');
208
+ lines.push(this.formatJobDetails(build.jobs?.edges, options));
209
+ }
210
+ return lines.join('\n');
211
+ }
212
+ formatBlockedBuild(build, options) {
213
+ const lines = [];
214
+ // Header line
215
+ lines.push(this.formatHeader(build));
216
+ lines.push(this.formatCommitInfo(build));
217
+ lines.push('');
218
+ // Blocked information
219
+ const blockedJobs = this.getBlockedJobs(build.jobs?.edges);
220
+ if (blockedJobs.length > 0) {
221
+ lines.push(`🚫 Blocked: "${blockedJobs[0].node.label}" (manual unblock required)`);
222
+ }
223
+ // Show jobs summary
224
+ const jobStats = this.getJobStats(build.jobs?.edges);
225
+ if (jobStats.completed > 0) {
226
+ lines.push(`✅ ${jobStats.completed} jobs passed before block`);
227
+ }
228
+ // Show job details if requested
229
+ if (options?.jobs) {
230
+ lines.push('');
231
+ lines.push(this.formatJobDetails(build.jobs?.edges, options));
232
+ }
233
+ return lines.join('\n');
234
+ }
235
+ formatCanceledBuild(build, options) {
236
+ const lines = [];
237
+ // Header line
238
+ lines.push(this.formatHeader(build));
239
+ lines.push(this.formatCommitInfo(build));
240
+ lines.push('');
241
+ // Canceled information
242
+ if (build.createdBy) {
243
+ const creator = build.createdBy.name || build.createdBy.email;
244
+ lines.push(`Canceled by: ${creator}`);
245
+ }
246
+ // Show jobs summary
247
+ const jobStats = this.getJobStats(build.jobs?.edges);
248
+ lines.push(`Completed: ${jobStats.completed}/${jobStats.total} jobs before cancellation`);
249
+ // Show job details if requested
250
+ if (options?.jobs) {
251
+ lines.push('');
252
+ lines.push(this.formatJobDetails(build.jobs?.edges, options));
253
+ }
254
+ return lines.join('\n');
255
+ }
256
+ formatDefaultBuild(build, options) {
257
+ return this.formatPassedBuild(build, options);
258
+ }
259
+ formatFullDetails(build, options) {
260
+ const lines = [];
261
+ // Full header information
262
+ lines.push(this.formatHeader(build));
263
+ lines.push(this.formatCommitInfo(build));
264
+ lines.push('');
265
+ // Build metadata
266
+ lines.push('Build Details:');
267
+ lines.push(` URL: ${build.url}`);
268
+ lines.push(` Organization: ${build.organization?.name || 'Unknown'}`);
269
+ lines.push(` Pipeline: ${build.pipeline?.name || 'Unknown'}`);
270
+ if (build.pullRequest) {
271
+ lines.push(` Pull Request: #${build.pullRequest.number}`);
272
+ }
273
+ if (build.triggeredFrom) {
274
+ lines.push(` Triggered from: ${build.triggeredFrom.pipeline?.name} #${build.triggeredFrom.number}`);
275
+ }
276
+ lines.push('');
277
+ // Jobs section
278
+ lines.push('Jobs:');
279
+ lines.push(this.formatJobDetails(build.jobs?.edges, { ...options, full: true }));
280
+ // Annotations section
281
+ if (build.annotations?.edges?.length > 0) {
282
+ lines.push('');
283
+ lines.push('Annotations:');
284
+ lines.push(this.formatAnnotationDetails(build.annotations.edges, { ...options, annotationsFull: true }));
285
+ }
286
+ return lines.join('\n');
287
+ }
288
+ formatHeader(build) {
289
+ const status = this.getStatusIcon(build.state);
290
+ const stateFormatted = formatBuildStatus(build.state, { useSymbol: false });
291
+ const duration = this.formatDuration(build);
292
+ const age = this.formatAge(build.createdAt);
293
+ const branch = SEMANTIC_COLORS.identifier(build.branch);
294
+ return `${status} ${SEMANTIC_COLORS.label(`#${build.number}`)} ${stateFormatted} • ${duration} • ${branch} • ${age}`;
295
+ }
296
+ formatCommitInfo(build) {
297
+ const shortSha = build.commit ? build.commit.substring(0, 7) : 'unknown';
298
+ const message = build.message || 'No commit message';
299
+ const truncatedMessage = message.length > 60 ? message.substring(0, 57) + '...' : message;
300
+ return ` "${truncatedMessage}" (${shortSha})`;
301
+ }
302
+ formatAnnotationSummary(annotations) {
303
+ const counts = this.countAnnotationsByStyle(annotations);
304
+ const parts = [];
305
+ if (counts.ERROR > 0)
306
+ parts.push(SEMANTIC_COLORS.error(`${counts.ERROR} error${counts.ERROR > 1 ? 's' : ''}`));
307
+ if (counts.WARNING > 0)
308
+ parts.push(SEMANTIC_COLORS.warning(`${counts.WARNING} warning${counts.WARNING > 1 ? 's' : ''}`));
309
+ if (counts.INFO > 0)
310
+ parts.push(SEMANTIC_COLORS.info(`${counts.INFO} info`));
311
+ if (counts.SUCCESS > 0)
312
+ parts.push(SEMANTIC_COLORS.success(`${counts.SUCCESS} success`));
313
+ const total = annotations.length;
314
+ return `📝 ${SEMANTIC_COLORS.count(String(total))} annotation${total > 1 ? 's' : ''}: ${parts.join(', ')}`;
315
+ }
316
+ formatAnnotationDetails(annotations, options) {
317
+ const lines = [];
318
+ // Group annotations by style
319
+ const grouped = this.groupAnnotationsByStyle(annotations);
320
+ for (const [style, items] of Object.entries(grouped)) {
321
+ for (const annotation of items) {
322
+ const icon = this.getAnnotationIcon(style);
323
+ const context = annotation.node.context || 'default';
324
+ if (options?.annotationsFull) {
325
+ // Full content
326
+ lines.push(`${icon} ${style} [${context}]:`);
327
+ const body = htmlToText(annotation.node.body?.html || '', {
328
+ wordwrap: 80,
329
+ preserveNewlines: true
330
+ });
331
+ lines.push(body.split('\n').map(l => ` ${l}`).join('\n'));
332
+ lines.push('');
333
+ }
334
+ else {
335
+ // Summary only
336
+ lines.push(`${icon} ${style} [${context}]`);
337
+ }
338
+ }
339
+ }
340
+ return lines.join('\n');
341
+ }
342
+ formatJobDetails(jobs, options) {
343
+ if (!jobs || jobs.length === 0) {
344
+ return 'No jobs found';
345
+ }
346
+ const lines = [];
347
+ const jobStats = this.getJobStats(jobs);
348
+ // Summary line
349
+ const parts = [];
350
+ if (jobStats.passed > 0)
351
+ parts.push(`✅ ${jobStats.passed} passed`);
352
+ if (jobStats.failed > 0)
353
+ parts.push(`❌ ${jobStats.failed} failed`);
354
+ if (jobStats.running > 0)
355
+ parts.push(`🔄 ${jobStats.running} running`);
356
+ if (jobStats.blocked > 0)
357
+ parts.push(`⏸️ ${jobStats.blocked} blocked`);
358
+ if (jobStats.skipped > 0)
359
+ parts.push(`⏭️ ${jobStats.skipped} skipped`);
360
+ lines.push(`Jobs: ${parts.join(' ')}`);
361
+ lines.push('');
362
+ // Filter jobs based on options
363
+ let filteredJobs = jobs;
364
+ if (options?.failed) {
365
+ filteredJobs = this.getFailedJobs(jobs);
366
+ }
367
+ // Group jobs by state
368
+ const grouped = this.groupJobsByState(filteredJobs);
369
+ for (const [state, stateJobs] of Object.entries(grouped)) {
370
+ if (stateJobs.length === 0)
371
+ continue;
372
+ const icon = this.getJobStateIcon(state);
373
+ const stateColored = this.colorizeJobState(state);
374
+ lines.push(`${icon} ${stateColored} (${SEMANTIC_COLORS.count(String(stateJobs.length))}):`);
375
+ for (const job of stateJobs) {
376
+ const label = this.parseEmoji(job.node.label);
377
+ const duration = this.formatJobDuration(job.node);
378
+ const exitCode = job.node.exitStatus ? `, exit ${job.node.exitStatus}` : '';
379
+ lines.push(` ${label} (${duration}${exitCode})`);
380
+ if (options?.full && job.node.agent) {
381
+ lines.push(` ${SEMANTIC_COLORS.dim(`Agent: ${job.node.agent.name || job.node.agent.hostname}`)}`);
382
+ }
383
+ }
384
+ lines.push('');
385
+ }
386
+ return lines.join('\n').trim();
387
+ }
388
+ formatFailedJobsSummary(failedJobs) {
389
+ const lines = [];
390
+ // Group identical jobs by label
391
+ const jobGroups = this.groupJobsByLabel(failedJobs);
392
+ // Show first 10 unique job types
393
+ const displayGroups = jobGroups.slice(0, 10);
394
+ for (const group of displayGroups) {
395
+ const label = this.parseEmoji(group.label);
396
+ if (group.count === 1) {
397
+ const duration = this.formatJobDuration(group.jobs[0].node);
398
+ lines.push(` ${SEMANTIC_COLORS.error('Failed')}: ${label} - ran ${duration}`);
399
+ }
400
+ else {
401
+ // Multiple jobs with same label - show detailed breakdown
402
+ const statusParts = [];
403
+ if (group.stateCounts.failed > 0) {
404
+ statusParts.push(`${group.stateCounts.failed} failed`);
405
+ }
406
+ if (group.stateCounts.broken > 0) {
407
+ statusParts.push(`${group.stateCounts.broken} broken`);
408
+ }
409
+ if (group.stateCounts.notStarted > 0) {
410
+ statusParts.push(`${group.stateCounts.notStarted} not started`);
411
+ }
412
+ if (group.stateCounts.passed > 0) {
413
+ statusParts.push(`${group.stateCounts.passed} passed`);
414
+ }
415
+ if (group.stateCounts.other > 0) {
416
+ statusParts.push(`${group.stateCounts.other} other`);
417
+ }
418
+ // Add exit codes if available
419
+ if (group.exitCodes.length > 0) {
420
+ const exitCodeStr = group.exitCodes.length === 1
421
+ ? `exit ${group.exitCodes[0]}`
422
+ : `exits: ${group.exitCodes.join(', ')}`;
423
+ statusParts.push(exitCodeStr);
424
+ }
425
+ const statusInfo = statusParts.join(', ') || 'various states';
426
+ lines.push(` ${SEMANTIC_COLORS.error('Failed')}: ${label} (${SEMANTIC_COLORS.count(String(group.count))} jobs: ${statusInfo})`);
427
+ }
428
+ }
429
+ // Add summary if there are more job types
430
+ const remaining = jobGroups.length - displayGroups.length;
431
+ if (remaining > 0) {
432
+ lines.push(` ${SEMANTIC_COLORS.muted(`...and ${remaining} more job types`)}`);
433
+ }
434
+ return lines.join('\n');
435
+ }
436
+ groupJobsByLabel(jobs) {
437
+ const groups = new Map();
438
+ for (const job of jobs) {
439
+ const label = job.node.label || 'Unnamed job';
440
+ if (!groups.has(label)) {
441
+ groups.set(label, {
442
+ label,
443
+ count: 0,
444
+ jobs: [],
445
+ exitCodes: new Set(),
446
+ stateCounts: {
447
+ failed: 0,
448
+ broken: 0,
449
+ notStarted: 0,
450
+ passed: 0,
451
+ other: 0
452
+ }
453
+ });
454
+ }
455
+ const group = groups.get(label);
456
+ group.count++;
457
+ group.jobs.push(job);
458
+ // Track exit codes
459
+ if (job.node.exitStatus !== null && job.node.exitStatus !== undefined) {
460
+ group.exitCodes.add(job.node.exitStatus);
461
+ }
462
+ // Count by state
463
+ const state = job.node.state?.toUpperCase();
464
+ if (!job.node.startedAt) {
465
+ group.stateCounts.notStarted++;
466
+ }
467
+ else if (state === 'FAILED') {
468
+ group.stateCounts.failed++;
469
+ }
470
+ else if (state === 'BROKEN') {
471
+ group.stateCounts.broken++;
472
+ }
473
+ else if (state === 'PASSED' || job.node.passed === true) {
474
+ group.stateCounts.passed++;
475
+ }
476
+ else {
477
+ group.stateCounts.other++;
478
+ }
479
+ }
480
+ // Convert to array and sort by count (most failures first)
481
+ return Array.from(groups.values())
482
+ .map(g => ({ ...g, exitCodes: Array.from(g.exitCodes) }))
483
+ .sort((a, b) => b.count - a.count);
484
+ }
485
+ formatDuration(build) {
486
+ if (!build.startedAt) {
487
+ return 'not started';
488
+ }
489
+ const start = new Date(build.startedAt);
490
+ const end = build.finishedAt ? new Date(build.finishedAt) : new Date();
491
+ const durationMs = end.getTime() - start.getTime();
492
+ const minutes = Math.floor(durationMs / 60000);
493
+ const seconds = Math.floor((durationMs % 60000) / 1000);
494
+ if (build.state === 'RUNNING') {
495
+ return `${minutes}m ${seconds}s elapsed`;
496
+ }
497
+ return `${minutes}m ${seconds}s`;
498
+ }
499
+ formatJobDuration(job) {
500
+ if (!job.startedAt) {
501
+ return 'not started';
502
+ }
503
+ const start = new Date(job.startedAt);
504
+ const end = job.finishedAt ? new Date(job.finishedAt) : new Date();
505
+ const durationMs = end.getTime() - start.getTime();
506
+ const minutes = Math.floor(durationMs / 60000);
507
+ const seconds = Math.floor((durationMs % 60000) / 1000);
508
+ return `${minutes}m ${seconds}s`;
509
+ }
510
+ formatAge(createdAt) {
511
+ return formatDistanceToNow(new Date(createdAt), { addSuffix: true });
512
+ }
513
+ colorizeJobState(state) {
514
+ switch (state.toLowerCase()) {
515
+ case 'failed':
516
+ return SEMANTIC_COLORS.error(state);
517
+ case 'passed':
518
+ return SEMANTIC_COLORS.success(state);
519
+ case 'running':
520
+ return SEMANTIC_COLORS.info(state);
521
+ case 'blocked':
522
+ return SEMANTIC_COLORS.warning(state);
523
+ case 'skipped':
524
+ case 'canceled':
525
+ return SEMANTIC_COLORS.muted(state);
526
+ default:
527
+ return state;
528
+ }
529
+ }
530
+ getStatusIcon(state) {
531
+ const icons = {
532
+ 'PASSED': '✅',
533
+ 'FAILED': '❌',
534
+ 'RUNNING': '🔄',
535
+ 'BLOCKED': '⏸️',
536
+ 'CANCELED': '🚫',
537
+ 'SCHEDULED': '📅',
538
+ 'SKIPPED': '⏭️'
539
+ };
540
+ return icons[state] || '❓';
541
+ }
542
+ getJobStateIcon(state) {
543
+ const icons = {
544
+ 'passed': '✅',
545
+ 'failed': '❌',
546
+ 'running': '🔄',
547
+ 'blocked': '⏸️',
548
+ 'canceled': '🚫',
549
+ 'scheduled': '📅',
550
+ 'skipped': '⏭️'
551
+ };
552
+ return icons[state.toLowerCase()] || '❓';
553
+ }
554
+ getAnnotationIcon(style) {
555
+ const icons = {
556
+ 'ERROR': '❌',
557
+ 'WARNING': '⚠️',
558
+ 'INFO': 'ℹ️',
559
+ 'SUCCESS': '✅'
560
+ };
561
+ return icons[style.toUpperCase()] || '📝';
562
+ }
563
+ getJobStats(jobs) {
564
+ const stats = {
565
+ total: jobs?.length || 0,
566
+ passed: 0,
567
+ failed: 0,
568
+ running: 0,
569
+ blocked: 0,
570
+ skipped: 0,
571
+ queued: 0,
572
+ completed: 0
573
+ };
574
+ if (!jobs)
575
+ return stats;
576
+ for (const job of jobs) {
577
+ const state = job.node.state?.toUpperCase() || '';
578
+ if (state === 'PASSED' || (job.node.passed === true && state !== 'BROKEN')) {
579
+ stats.passed++;
580
+ stats.completed++;
581
+ }
582
+ else if (state === 'FAILED' || state === 'BROKEN' || (job.node.exitStatus && job.node.exitStatus !== 0) || job.node.passed === false) {
583
+ stats.failed++;
584
+ stats.completed++;
585
+ }
586
+ else if (state === 'RUNNING') {
587
+ stats.running++;
588
+ }
589
+ else if (state === 'BLOCKED') {
590
+ stats.blocked++;
591
+ }
592
+ else if (state === 'SKIPPED' || state === 'CANCELED') {
593
+ stats.skipped++;
594
+ stats.completed++;
595
+ }
596
+ else if (state === 'SCHEDULED' || state === 'ASSIGNED') {
597
+ stats.queued++;
598
+ }
599
+ }
600
+ return stats;
601
+ }
602
+ getFailedJobs(jobs) {
603
+ if (!jobs)
604
+ return [];
605
+ return jobs.filter(job => {
606
+ const state = job.node.state?.toUpperCase();
607
+ return state === 'FAILED' || state === 'BROKEN' || (job.node.exitStatus && job.node.exitStatus !== 0) || job.node.passed === false;
608
+ });
609
+ }
610
+ getRunningJobs(jobs) {
611
+ if (!jobs)
612
+ return [];
613
+ return jobs.filter(job => job.node.state?.toLowerCase() === 'running');
614
+ }
615
+ getBlockedJobs(jobs) {
616
+ if (!jobs)
617
+ return [];
618
+ return jobs.filter(job => job.node.state?.toLowerCase() === 'blocked');
619
+ }
620
+ groupJobsByState(jobs) {
621
+ const grouped = {
622
+ 'Failed': [],
623
+ 'Passed': [],
624
+ 'Running': [],
625
+ 'Blocked': [],
626
+ 'Skipped': []
627
+ };
628
+ if (!jobs)
629
+ return grouped;
630
+ for (const job of jobs) {
631
+ const state = job.node.state?.toUpperCase() || '';
632
+ if (state === 'FAILED' || state === 'BROKEN' || (job.node.exitStatus && job.node.exitStatus !== 0) || job.node.passed === false) {
633
+ grouped['Failed'].push(job);
634
+ }
635
+ else if (state === 'PASSED' || (job.node.passed === true && state !== 'BROKEN')) {
636
+ grouped['Passed'].push(job);
637
+ }
638
+ else if (state === 'RUNNING') {
639
+ grouped['Running'].push(job);
640
+ }
641
+ else if (state === 'BLOCKED') {
642
+ grouped['Blocked'].push(job);
643
+ }
644
+ else if (state === 'SKIPPED' || state === 'CANCELED') {
645
+ grouped['Skipped'].push(job);
646
+ }
647
+ }
648
+ return grouped;
649
+ }
650
+ countAnnotationsByStyle(annotations) {
651
+ const counts = {
652
+ ERROR: 0,
653
+ WARNING: 0,
654
+ INFO: 0,
655
+ SUCCESS: 0
656
+ };
657
+ for (const annotation of annotations) {
658
+ const style = annotation.node.style?.toUpperCase() || 'INFO';
659
+ if (style in counts) {
660
+ counts[style]++;
661
+ }
662
+ }
663
+ return counts;
664
+ }
665
+ groupAnnotationsByStyle(annotations) {
666
+ const grouped = {};
667
+ for (const annotation of annotations) {
668
+ const style = annotation.node.style?.toUpperCase() || 'INFO';
669
+ if (!grouped[style]) {
670
+ grouped[style] = [];
671
+ }
672
+ grouped[style].push(annotation);
673
+ }
674
+ return grouped;
675
+ }
676
+ formatError(action, error) {
677
+ return formatError(action, error);
678
+ }
679
+ }
680
+ //# sourceMappingURL=PlainTextFormatter.js.map