@pokit/terminal 0.2.0

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.
@@ -0,0 +1,599 @@
1
+ /**
2
+ * Clack Reporter Adapter
3
+ *
4
+ * Implements the ReporterAdapter interface using @clack/prompts.
5
+ * Consumes CLI events from the EventBus and renders them to the terminal.
6
+ *
7
+ * Design principles:
8
+ * - Groups are visual containers with a bold title (intro) and completion indicator (outro)
9
+ * - Activities within sequential groups show as spinner items that complete with checkmarks
10
+ * - Parallel groups use a single spinner that tracks progress, showing completions as they finish
11
+ * - Logs during an active spinner will pause the spinner, show the log, then resume
12
+ * - Process output is never interleaved with spinners
13
+ *
14
+ * Rendering strategy:
15
+ * - group:start -> p.intro() with bold label (or line-based output)
16
+ * - group:end -> p.outro() with success indicator
17
+ * - activity:start (sequential) -> spinner.start() (or line-based output)
18
+ * - activity:start (parallel) -> track activity, update combined spinner
19
+ * - activity:success -> spinner.stop() with checkmark (code 0) or update combined spinner
20
+ * - activity:failure -> spinner.stop() with X (code 1)
21
+ * - activity:update -> spinner.message()
22
+ * - log -> pause spinner if active, p.log.*, resume spinner
23
+ *
24
+ * Non-interactive output (--no-tty/NO_TTY/CI):
25
+ * - Uses line-based output without spinners or clack decorative UI
26
+ * - Unicode symbols are controlled separately with --no-unicode/NO_UNICODE
27
+ * - Color is controlled separately with --no-color/NO_COLOR
28
+ */
29
+ import * as p from '@clack/prompts';
30
+ import pc from 'picocolors';
31
+ import { detectOutputConfig, CommandError } from '@pokit/core';
32
+ import { getSymbols } from './symbols';
33
+ /**
34
+ * Extract error message and optional output from an error.
35
+ * If the error is a CommandError with output, includes that in the message.
36
+ */
37
+ function formatErrorMessage(error) {
38
+ if (typeof error === 'string') {
39
+ return error;
40
+ }
41
+ if (error instanceof CommandError && error.output) {
42
+ return `${error.message}\n\n${error.output}`;
43
+ }
44
+ return error.message;
45
+ }
46
+ /**
47
+ * Helper to conditionally apply color.
48
+ * In no-color mode, returns text unchanged.
49
+ */
50
+ function colorize(text, colorFn, useColor) {
51
+ return useColor ? colorFn(text) : text;
52
+ }
53
+ /**
54
+ * Write a line to stdout.
55
+ * Uses process.stdout.write to ensure proper capture in all environments.
56
+ * (Bun's console.log may not be captured by stdout interception)
57
+ */
58
+ function writeLine(line) {
59
+ process.stdout.write(line + '\n');
60
+ }
61
+ function isPlainOutput(outputConfig) {
62
+ return !outputConfig.unicode || !outputConfig.interactive;
63
+ }
64
+ function canUseInteractiveUI(outputConfig) {
65
+ return outputConfig.unicode && outputConfig.interactive;
66
+ }
67
+ /** Maximum number of logs to buffer per activity to prevent memory issues */
68
+ const MAX_BUFFERED_LOGS_PER_ACTIVITY = 100;
69
+ /**
70
+ * Update the parallel spinner message based on current activity states
71
+ */
72
+ function updateParallelSpinnerMessage(state) {
73
+ if (!state.parallelSpinner)
74
+ return;
75
+ const activities = Array.from(state.parallelActivities.values()).filter((a) => a.groupId === state.parallelSpinnerGroupId);
76
+ const pending = activities.filter((a) => a.status === 'pending');
77
+ const completed = activities.filter((a) => a.status !== 'pending');
78
+ if (pending.length === 0) {
79
+ // All done - this will be cleaned up by group:end
80
+ return;
81
+ }
82
+ const message = pending.length === 1
83
+ ? pending[0].label
84
+ : `Running ${pending.length} tasks (${completed.length}/${activities.length} done)`;
85
+ state.parallelSpinner.currentMessage = message;
86
+ state.parallelSpinner.spinner.message(message);
87
+ }
88
+ /**
89
+ * Display a log message.
90
+ * Uses clack's log functions in interactive unicode mode, line output otherwise.
91
+ *
92
+ * @param level - The log level
93
+ * @param message - The message to display
94
+ * @param state - The adapter state (for output config)
95
+ * @param indented - Whether to indent the log (for buffered logs inside activity context)
96
+ */
97
+ function displayLog(level, message, state, indented = false) {
98
+ const { outputConfig, symbols } = state;
99
+ // In plain or non-interactive mode, use simple console output
100
+ if (isPlainOutput(outputConfig)) {
101
+ const prefix = indented ? `${symbols.groupLine} ` : '';
102
+ const levelPrefix = {
103
+ info: symbols.info,
104
+ warn: symbols.warning,
105
+ error: symbols.error,
106
+ success: symbols.success,
107
+ step: symbols.step,
108
+ }[level];
109
+ writeLine(`${prefix}${levelPrefix} ${message}`);
110
+ return;
111
+ }
112
+ // Unicode + interactive mode - use clack's decorative output
113
+ const prefix = indented ? '\u2502 ' : ''; // │ for indented logs
114
+ const formattedMessage = prefix + message;
115
+ switch (level) {
116
+ case 'info':
117
+ p.log.info(formattedMessage);
118
+ break;
119
+ case 'warn':
120
+ p.log.warn(formattedMessage);
121
+ break;
122
+ case 'error':
123
+ p.log.error(formattedMessage);
124
+ break;
125
+ case 'success':
126
+ p.log.success(formattedMessage);
127
+ break;
128
+ case 'step':
129
+ p.log.step(formattedMessage);
130
+ break;
131
+ }
132
+ }
133
+ /**
134
+ * Flush buffered logs for a specific activity.
135
+ *
136
+ * @param state - The adapter state
137
+ * @param activityId - The activity ID whose logs should be flushed
138
+ */
139
+ function flushLogsForActivity(state, activityId) {
140
+ // Filter logs for this activity, sorted by timestamp
141
+ const activityLogs = state.bufferedLogs
142
+ .filter((log) => log.activityId === activityId)
143
+ .sort((a, b) => a.timestamp - b.timestamp);
144
+ // Display each buffered log with indentation
145
+ for (const log of activityLogs) {
146
+ displayLog(log.level, log.message, state, true);
147
+ }
148
+ // Remove flushed logs from buffer
149
+ state.bufferedLogs = state.bufferedLogs.filter((log) => log.activityId !== activityId);
150
+ }
151
+ /**
152
+ * Create a Clack-based ReporterAdapter
153
+ *
154
+ * @param options - Optional configuration for the adapter
155
+ */
156
+ export function createReporterAdapter(options) {
157
+ // Get output config from options, or detect from args/env
158
+ const outputConfig = options?.output ?? detectOutputConfig(process.argv.slice(2));
159
+ // Backwards compatibility: default interactive when missing
160
+ if (outputConfig.interactive === undefined) {
161
+ outputConfig.interactive = true;
162
+ }
163
+ // Verbose can be set via options.verbose for backwards compatibility
164
+ if (options?.verbose !== undefined) {
165
+ outputConfig.verbose = options.verbose;
166
+ }
167
+ const symbols = getSymbols(outputConfig);
168
+ return {
169
+ start(bus) {
170
+ const state = {
171
+ spinners: new Map(),
172
+ groups: new Map(),
173
+ parallelActivities: new Map(),
174
+ parallelSpinner: null,
175
+ parallelSpinnerGroupId: null,
176
+ bufferedLogs: [],
177
+ verbose: outputConfig.verbose,
178
+ outputConfig,
179
+ symbols,
180
+ };
181
+ const handleEvent = (event) => {
182
+ switch (event.type) {
183
+ // Root lifecycle (app-level intro/outro)
184
+ case 'root:start':
185
+ case 'root:end':
186
+ // Handled by the router
187
+ break;
188
+ // Group lifecycle (command-level intro/outro)
189
+ case 'group:start': {
190
+ state.groups.set(event.id, {
191
+ label: event.label,
192
+ layout: event.layout,
193
+ hasFailure: false,
194
+ });
195
+ // In plain or non-interactive mode, use simple bracket notation
196
+ if (isPlainOutput(state.outputConfig)) {
197
+ const label = colorize(event.label, pc.bold, state.outputConfig.color);
198
+ writeLine(`${state.symbols.groupStart}${label}${state.symbols.groupEnd}`);
199
+ }
200
+ else {
201
+ p.intro(colorize(event.label, pc.bold, state.outputConfig.color));
202
+ }
203
+ break;
204
+ }
205
+ case 'group:end': {
206
+ const group = state.groups.get(event.id);
207
+ state.groups.delete(event.id);
208
+ let hasFailures = group?.hasFailure ?? false;
209
+ const deferredErrors = [];
210
+ // If this was a parallel group, show completion results and clean up
211
+ if (group?.layout === 'parallel') {
212
+ // Collect activities for this group
213
+ const activities = Array.from(state.parallelActivities.entries()).filter(([, a]) => a.groupId === event.id);
214
+ // Sort: successes first, then failures (so failures are more visible at end)
215
+ const sorted = activities.sort(([, a], [, b]) => {
216
+ if (a.status === 'success' && b.status !== 'success')
217
+ return -1;
218
+ if (a.status !== 'success' && b.status === 'success')
219
+ return 1;
220
+ return 0;
221
+ });
222
+ // Stop the parallel spinner - use first success as the message
223
+ if (state.parallelSpinner && state.parallelSpinnerGroupId === event.id) {
224
+ const firstSuccess = sorted.find(([, a]) => a.status === 'success');
225
+ if (firstSuccess) {
226
+ // Show first success via spinner stop
227
+ state.parallelSpinner.spinner.stop(firstSuccess[1].label);
228
+ // Flush buffered logs for this activity
229
+ flushLogsForActivity(state, firstSuccess[0]);
230
+ state.parallelActivities.delete(firstSuccess[0]);
231
+ }
232
+ else {
233
+ // All failed - show first failure label (not error) via spinner stop
234
+ const firstFailure = sorted[0];
235
+ if (firstFailure) {
236
+ state.parallelSpinner.spinner.error(firstFailure[1].label);
237
+ hasFailures = true;
238
+ // Defer the error message with remediation to print after outro
239
+ if (firstFailure[1].error) {
240
+ deferredErrors.push({
241
+ error: firstFailure[1].error,
242
+ remediation: firstFailure[1].remediation,
243
+ documentationUrl: firstFailure[1].documentationUrl,
244
+ });
245
+ }
246
+ // Flush buffered logs for this activity
247
+ flushLogsForActivity(state, firstFailure[0]);
248
+ state.parallelActivities.delete(firstFailure[0]);
249
+ }
250
+ else {
251
+ state.parallelSpinner.spinner.stop('Complete');
252
+ }
253
+ }
254
+ state.parallelSpinner = null;
255
+ state.parallelSpinnerGroupId = null;
256
+ }
257
+ // Show remaining results (only labels inside group, defer errors)
258
+ for (const [activityId, activity] of state.parallelActivities) {
259
+ if (activity.groupId !== event.id)
260
+ continue;
261
+ if (activity.status === 'success') {
262
+ if (isPlainOutput(state.outputConfig)) {
263
+ const prefix = colorize(state.symbols.success, pc.green, state.outputConfig.color);
264
+ writeLine(` ${prefix} ${activity.label}`);
265
+ }
266
+ else {
267
+ p.log.success(activity.label);
268
+ }
269
+ }
270
+ else if (activity.status === 'failure') {
271
+ hasFailures = true;
272
+ // Show label inside group, defer error message with remediation
273
+ if (isPlainOutput(state.outputConfig)) {
274
+ const prefix = colorize(state.symbols.error, pc.red, state.outputConfig.color);
275
+ writeLine(` ${prefix} ${activity.label}`);
276
+ }
277
+ else {
278
+ p.log.error(activity.label);
279
+ }
280
+ if (activity.error) {
281
+ deferredErrors.push({
282
+ error: activity.error,
283
+ remediation: activity.remediation,
284
+ documentationUrl: activity.documentationUrl,
285
+ });
286
+ }
287
+ }
288
+ // Flush any buffered logs for this parallel activity
289
+ flushLogsForActivity(state, activityId);
290
+ }
291
+ // Clean up all activities for this group
292
+ for (const [id, activity] of [...state.parallelActivities]) {
293
+ if (activity.groupId === event.id) {
294
+ state.parallelActivities.delete(id);
295
+ }
296
+ }
297
+ }
298
+ // Show appropriate outro based on whether there were failures
299
+ if (isPlainOutput(state.outputConfig)) {
300
+ // Plain output - simple bracket notation
301
+ if (hasFailures) {
302
+ const failedText = colorize(state.symbols.failed, pc.red, state.outputConfig.color);
303
+ writeLine(`${state.symbols.groupStart}${failedText}${state.symbols.groupEnd}`);
304
+ }
305
+ else {
306
+ const doneText = colorize(state.symbols.done, pc.green, state.outputConfig.color);
307
+ writeLine(`${state.symbols.groupStart}${doneText}${state.symbols.groupEnd}`);
308
+ }
309
+ }
310
+ else {
311
+ // Unicode + interactive mode - use clack's outro
312
+ if (hasFailures) {
313
+ p.outro(colorize(`${state.symbols.failed} Failed`, pc.red, state.outputConfig.color));
314
+ }
315
+ else {
316
+ p.outro(colorize(`${state.symbols.done} Done`, pc.green, state.outputConfig.color));
317
+ }
318
+ }
319
+ // Print deferred error messages with remediation after the group closes
320
+ for (const deferred of deferredErrors) {
321
+ if (isPlainOutput(state.outputConfig)) {
322
+ writeLine(`${state.symbols.error} ${deferred.error}`);
323
+ }
324
+ else {
325
+ p.log.error(deferred.error);
326
+ }
327
+ // Display remediation steps if available
328
+ if (deferred.remediation && deferred.remediation.length > 0) {
329
+ writeLine('');
330
+ writeLine(' To fix:');
331
+ for (const step of deferred.remediation) {
332
+ writeLine(` - ${step}`);
333
+ }
334
+ }
335
+ // Display documentation link if available
336
+ if (deferred.documentationUrl) {
337
+ writeLine('');
338
+ writeLine(` More info: ${deferred.documentationUrl}`);
339
+ }
340
+ }
341
+ break;
342
+ }
343
+ // Activity lifecycle (task-level spinner)
344
+ case 'activity:start': {
345
+ // Find the parent group to determine layout
346
+ const parentGroup = event.parentId ? state.groups.get(event.parentId) : null;
347
+ if (parentGroup?.layout === 'parallel') {
348
+ // Track this activity for the parallel group
349
+ state.parallelActivities.set(event.id, {
350
+ label: event.label,
351
+ groupId: event.parentId,
352
+ status: 'pending',
353
+ });
354
+ // In plain or non-interactive mode, track activities - results shown at group:end
355
+ if (isPlainOutput(state.outputConfig)) {
356
+ if (!state.parallelSpinnerGroupId) {
357
+ state.parallelSpinnerGroupId = event.parentId;
358
+ const activities = state.parallelActivities.size;
359
+ writeLine(` Running ${activities} task${activities > 1 ? 's' : ''}...`);
360
+ }
361
+ }
362
+ else {
363
+ // Unicode + interactive mode: create or update the parallel spinner
364
+ if (!state.parallelSpinner) {
365
+ const spinner = p.spinner();
366
+ state.parallelSpinner = {
367
+ spinner,
368
+ label: event.label,
369
+ currentMessage: event.label,
370
+ };
371
+ state.parallelSpinnerGroupId = event.parentId;
372
+ spinner.start(event.label);
373
+ }
374
+ updateParallelSpinnerMessage(state);
375
+ }
376
+ }
377
+ else {
378
+ // Sequential activity
379
+ if (isPlainOutput(state.outputConfig)) {
380
+ // Plain output: track activity without spinner - result shown on completion
381
+ state.spinners.set(event.id, {
382
+ spinner: null, // Not used in non-interactive output
383
+ label: event.label,
384
+ currentMessage: event.label,
385
+ parentGroupId: event.parentId,
386
+ });
387
+ }
388
+ else {
389
+ // Unicode + interactive mode: create individual spinner
390
+ const spinner = p.spinner();
391
+ state.spinners.set(event.id, {
392
+ spinner,
393
+ label: event.label,
394
+ currentMessage: event.label,
395
+ parentGroupId: event.parentId,
396
+ });
397
+ spinner.start(event.label);
398
+ }
399
+ }
400
+ break;
401
+ }
402
+ case 'activity:update': {
403
+ // Check if this is a parallel activity
404
+ const parallelActivity = state.parallelActivities.get(event.id);
405
+ if (parallelActivity) {
406
+ // Update the activity label if message provided
407
+ if (event.payload.message) {
408
+ parallelActivity.label = event.payload.message;
409
+ if (canUseInteractiveUI(state.outputConfig)) {
410
+ updateParallelSpinnerMessage(state);
411
+ }
412
+ }
413
+ break;
414
+ }
415
+ // Sequential activity
416
+ const entry = state.spinners.get(event.id);
417
+ if (entry) {
418
+ const text = event.payload.message ||
419
+ (event.payload.progress !== undefined ? `${event.payload.progress}%` : null);
420
+ if (text) {
421
+ entry.currentMessage = text;
422
+ // Only update spinner in unicode mode
423
+ if (canUseInteractiveUI(state.outputConfig) && entry.spinner) {
424
+ entry.spinner.message(text);
425
+ }
426
+ }
427
+ }
428
+ break;
429
+ }
430
+ case 'activity:success': {
431
+ // Check if this is a parallel activity
432
+ const parallelActivity = state.parallelActivities.get(event.id);
433
+ if (parallelActivity) {
434
+ parallelActivity.status = 'success';
435
+ if (canUseInteractiveUI(state.outputConfig)) {
436
+ updateParallelSpinnerMessage(state);
437
+ }
438
+ // Note: For parallel activities, logs will be flushed at group:end
439
+ break;
440
+ }
441
+ // Sequential activity
442
+ const entry = state.spinners.get(event.id);
443
+ if (entry) {
444
+ if (isPlainOutput(state.outputConfig)) {
445
+ // Plain output - print success line
446
+ const prefix = colorize(state.symbols.success, pc.green, state.outputConfig.color);
447
+ writeLine(` ${prefix} ${entry.label}`);
448
+ }
449
+ else if (entry.spinner) {
450
+ // Unicode + interactive mode - stop spinner
451
+ entry.spinner.stop(entry.label);
452
+ }
453
+ state.spinners.delete(event.id);
454
+ // Flush any buffered logs for this activity
455
+ flushLogsForActivity(state, event.id);
456
+ }
457
+ break;
458
+ }
459
+ case 'activity:failure': {
460
+ const errorMessage = formatErrorMessage(event.error);
461
+ // Check if this is a parallel activity
462
+ const parallelActivity = state.parallelActivities.get(event.id);
463
+ if (parallelActivity) {
464
+ parallelActivity.status = 'failure';
465
+ parallelActivity.error = errorMessage;
466
+ parallelActivity.remediation = event.remediation;
467
+ parallelActivity.documentationUrl = event.documentationUrl;
468
+ // Mark the parent group as having failures
469
+ const parentGroup = state.groups.get(parallelActivity.groupId);
470
+ if (parentGroup) {
471
+ parentGroup.hasFailure = true;
472
+ }
473
+ if (canUseInteractiveUI(state.outputConfig)) {
474
+ updateParallelSpinnerMessage(state);
475
+ }
476
+ break;
477
+ }
478
+ // Sequential activity
479
+ const entry = state.spinners.get(event.id);
480
+ if (entry) {
481
+ // Mark the parent group as having failures
482
+ if (entry.parentGroupId) {
483
+ const parentGroup = state.groups.get(entry.parentGroupId);
484
+ if (parentGroup) {
485
+ parentGroup.hasFailure = true;
486
+ }
487
+ }
488
+ if (isPlainOutput(state.outputConfig)) {
489
+ // Plain output - print error line
490
+ const prefix = colorize(state.symbols.error, pc.red, state.outputConfig.color);
491
+ writeLine(` ${prefix} ${errorMessage}`);
492
+ }
493
+ else if (entry.spinner) {
494
+ // Unicode + interactive mode - stop spinner with error
495
+ entry.spinner.error(errorMessage);
496
+ }
497
+ state.spinners.delete(event.id);
498
+ // Display remediation steps if available
499
+ const linePrefix = state.outputConfig.unicode ? '\u2502' : state.symbols.groupLine;
500
+ if (event.remediation && event.remediation.length > 0) {
501
+ writeLine(linePrefix);
502
+ writeLine(`${linePrefix} To fix:`);
503
+ for (const step of event.remediation) {
504
+ writeLine(`${linePrefix} - ${step}`);
505
+ }
506
+ }
507
+ // Display documentation link if available
508
+ if (event.documentationUrl) {
509
+ writeLine(linePrefix);
510
+ writeLine(`${linePrefix} More info: ${event.documentationUrl}`);
511
+ }
512
+ // Flush any buffered logs for this activity
513
+ flushLogsForActivity(state, event.id);
514
+ }
515
+ break;
516
+ }
517
+ // Log events
518
+ case 'log': {
519
+ // Verbose mode: always display logs immediately
520
+ if (state.verbose) {
521
+ displayLog(event.level, event.message, state, false);
522
+ break;
523
+ }
524
+ const hasActiveSpinners = state.spinners.size > 0 || state.parallelSpinner !== null;
525
+ // Error logs interrupt spinners immediately (only in unicode mode)
526
+ if (event.level === 'error' &&
527
+ hasActiveSpinners &&
528
+ event.activityId &&
529
+ canUseInteractiveUI(state.outputConfig)) {
530
+ const spinner = state.spinners.get(event.activityId);
531
+ if (spinner && spinner.spinner) {
532
+ // Temporarily stop spinner, show error, resume
533
+ const currentMessage = spinner.currentMessage;
534
+ spinner.spinner.stop(currentMessage);
535
+ displayLog(event.level, event.message, state, false);
536
+ spinner.spinner.start(currentMessage);
537
+ }
538
+ else {
539
+ // No spinner for this activity, just display
540
+ displayLog(event.level, event.message, state, false);
541
+ }
542
+ break;
543
+ }
544
+ // Buffer logs during active spinners
545
+ if (hasActiveSpinners && event.activityId) {
546
+ // Check if we've hit the buffer limit for this activity
547
+ const activityLogCount = state.bufferedLogs.filter((log) => log.activityId === event.activityId).length;
548
+ if (activityLogCount < MAX_BUFFERED_LOGS_PER_ACTIVITY) {
549
+ state.bufferedLogs.push({
550
+ activityId: event.activityId,
551
+ level: event.level,
552
+ message: event.message,
553
+ timestamp: Date.now(),
554
+ });
555
+ }
556
+ // If over limit, silently drop (prevent memory issues)
557
+ break;
558
+ }
559
+ // No active spinners - display immediately
560
+ displayLog(event.level, event.message, state, false);
561
+ break;
562
+ }
563
+ }
564
+ };
565
+ const unsubscribe = bus.on(handleEvent);
566
+ return {
567
+ stop() {
568
+ // Stop all active spinners (only in interactive mode where spinners exist)
569
+ if (canUseInteractiveUI(state.outputConfig)) {
570
+ for (const entry of state.spinners.values()) {
571
+ try {
572
+ if (entry.spinner) {
573
+ entry.spinner.error('Stopped');
574
+ }
575
+ }
576
+ catch {
577
+ // Spinner may already be stopped
578
+ }
579
+ }
580
+ if (state.parallelSpinner) {
581
+ try {
582
+ state.parallelSpinner.spinner.error('Stopped');
583
+ }
584
+ catch {
585
+ // Spinner may already be stopped
586
+ }
587
+ }
588
+ }
589
+ state.spinners.clear();
590
+ state.groups.clear();
591
+ state.parallelActivities.clear();
592
+ state.parallelSpinner = null;
593
+ state.parallelSpinnerGroupId = null;
594
+ unsubscribe();
595
+ },
596
+ };
597
+ },
598
+ };
599
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Symbol Sets for CLI Output
3
+ *
4
+ * Provides Unicode and ASCII symbol sets for terminal output.
5
+ * The appropriate set is selected based on OutputConfig.
6
+ */
7
+ import type { OutputConfig } from '@pokit/core';
8
+ /**
9
+ * Complete set of symbols used in CLI output
10
+ */
11
+ export type SymbolSet = {
12
+ /** Success indicator (e.g., checkmark) */
13
+ success: string;
14
+ /** Error indicator (e.g., X mark) */
15
+ error: string;
16
+ /** Warning indicator */
17
+ warning: string;
18
+ /** Info indicator */
19
+ info: string;
20
+ /** Step/progress indicator */
21
+ step: string;
22
+ /** Group start (e.g., top-left corner) */
23
+ groupStart: string;
24
+ /** Group end (e.g., bottom-left corner) */
25
+ groupEnd: string;
26
+ /** Group line (e.g., vertical bar) */
27
+ groupLine: string;
28
+ /** Done message */
29
+ done: string;
30
+ /** Failed message */
31
+ failed: string;
32
+ };
33
+ /**
34
+ * Unicode symbols for rich terminal output
35
+ * Used when unicode support is detected
36
+ */
37
+ export declare const UNICODE_SYMBOLS: SymbolSet;
38
+ /**
39
+ * ASCII symbols for plain text output
40
+ * Used in CI environments or when unicode is not supported
41
+ */
42
+ export declare const ASCII_SYMBOLS: SymbolSet;
43
+ /**
44
+ * Get the appropriate symbol set based on output configuration
45
+ *
46
+ * @param config - Output configuration
47
+ * @returns Symbol set to use
48
+ */
49
+ export declare function getSymbols(config: OutputConfig): SymbolSet;
50
+ //# sourceMappingURL=symbols.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"symbols.d.ts","sourceRoot":"","sources":["../../src/reporter/symbols.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB,0CAA0C;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,qCAAqC;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,wBAAwB;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,qBAAqB;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,8BAA8B;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,sCAAsC;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,qBAAqB;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,SAW7B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,SAW3B,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,YAAY,GAAG,SAAS,CAE1D"}