@pokit/reporter-clack 0.0.7 → 0.0.9

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.
package/README.md CHANGED
@@ -32,7 +32,12 @@ await run(args, {
32
32
 
33
33
  ```typescript
34
34
  createReporterAdapter({
35
- plain: false, // Disable colors and spinners
35
+ output: {
36
+ color: false,
37
+ unicode: false,
38
+ interactive: false,
39
+ verbose: false,
40
+ },
36
41
  });
37
42
  ```
38
43
 
package/dist/adapter.d.ts CHANGED
@@ -12,18 +12,19 @@
12
12
  * - Process output is never interleaved with spinners
13
13
  *
14
14
  * Rendering strategy:
15
- * - group:start -> p.intro() with bold label (or plain text in plain mode)
15
+ * - group:start -> p.intro() with bold label (or line-based output)
16
16
  * - group:end -> p.outro() with success indicator
17
- * - activity:start (sequential) -> spinner.start() (or plain text indicator)
17
+ * - activity:start (sequential) -> spinner.start() (or line-based output)
18
18
  * - activity:start (parallel) -> track activity, update combined spinner
19
19
  * - activity:success -> spinner.stop() with checkmark (code 0) or update combined spinner
20
20
  * - activity:failure -> spinner.stop() with X (code 1)
21
21
  * - activity:update -> spinner.message()
22
22
  * - log -> pause spinner if active, p.log.*, resume spinner
23
23
  *
24
- * Plain mode (--plain or CI environment):
25
- * - When unicode is disabled, uses ASCII symbols and bypasses clack's decorative output
26
- * - When color is disabled (--no-color or NO_COLOR env), strips ANSI color codes
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
27
28
  */
28
29
  import type { ReporterAdapter, OutputConfig } from '@pokit/core';
29
30
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAIH,OAAO,KAAK,EACV,eAAe,EAQf,YAAY,EACb,MAAM,aAAa,CAAC;AA+MrB;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,0FAA0F;IAC1F,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,CAAC,EAAE,sBAAsB,GAAG,eAAe,CA0iBvF"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAIH,OAAO,KAAK,EACV,eAAe,EAQf,YAAY,EACb,MAAM,aAAa,CAAC;AAuNrB;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,0FAA0F;IAC1F,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,CAAC,EAAE,sBAAsB,GAAG,eAAe,CA2iBvF"}
package/dist/adapter.js CHANGED
@@ -12,22 +12,23 @@
12
12
  * - Process output is never interleaved with spinners
13
13
  *
14
14
  * Rendering strategy:
15
- * - group:start -> p.intro() with bold label (or plain text in plain mode)
15
+ * - group:start -> p.intro() with bold label (or line-based output)
16
16
  * - group:end -> p.outro() with success indicator
17
- * - activity:start (sequential) -> spinner.start() (or plain text indicator)
17
+ * - activity:start (sequential) -> spinner.start() (or line-based output)
18
18
  * - activity:start (parallel) -> track activity, update combined spinner
19
19
  * - activity:success -> spinner.stop() with checkmark (code 0) or update combined spinner
20
20
  * - activity:failure -> spinner.stop() with X (code 1)
21
21
  * - activity:update -> spinner.message()
22
22
  * - log -> pause spinner if active, p.log.*, resume spinner
23
23
  *
24
- * Plain mode (--plain or CI environment):
25
- * - When unicode is disabled, uses ASCII symbols and bypasses clack's decorative output
26
- * - When color is disabled (--no-color or NO_COLOR env), strips ANSI color codes
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
27
28
  */
28
29
  import * as p from '@clack/prompts';
29
30
  import pc from 'picocolors';
30
- import { CommandError } from '@pokit/core';
31
+ import { detectOutputConfig, CommandError } from '@pokit/core';
31
32
  /**
32
33
  * Extract error message and optional output from an error.
33
34
  * If the error is a CommandError with output, includes that in the message.
@@ -57,6 +58,12 @@ function colorize(text, colorFn, useColor) {
57
58
  function writeLine(line) {
58
59
  process.stdout.write(line + '\n');
59
60
  }
61
+ function isPlainOutput(outputConfig) {
62
+ return !outputConfig.unicode || !outputConfig.interactive;
63
+ }
64
+ function canUseInteractiveUI(outputConfig) {
65
+ return outputConfig.unicode && outputConfig.interactive;
66
+ }
60
67
  /** Maximum number of logs to buffer per activity to prevent memory issues */
61
68
  const MAX_BUFFERED_LOGS_PER_ACTIVITY = 100;
62
69
  /**
@@ -80,7 +87,7 @@ function updateParallelSpinnerMessage(state) {
80
87
  }
81
88
  /**
82
89
  * Display a log message.
83
- * Uses clack's log functions in unicode mode, plain console.log in plain mode.
90
+ * Uses clack's log functions in interactive unicode mode, line output otherwise.
84
91
  *
85
92
  * @param level - The log level
86
93
  * @param message - The message to display
@@ -89,8 +96,8 @@ function updateParallelSpinnerMessage(state) {
89
96
  */
90
97
  function displayLog(level, message, state, indented = false) {
91
98
  const { outputConfig, symbols } = state;
92
- // In plain mode (no unicode), use simple console output
93
- if (!outputConfig.unicode) {
99
+ // In plain or non-interactive mode, use simple console output
100
+ if (isPlainOutput(outputConfig)) {
94
101
  const prefix = indented ? `${symbols.groupLine} ` : '';
95
102
  const levelPrefix = {
96
103
  info: symbols.info,
@@ -102,7 +109,7 @@ function displayLog(level, message, state, indented = false) {
102
109
  writeLine(`${prefix}${levelPrefix} ${message}`);
103
110
  return;
104
111
  }
105
- // Unicode mode - use clack's decorative output
112
+ // Unicode + interactive mode - use clack's decorative output
106
113
  const prefix = indented ? '\u2502 ' : ''; // │ for indented logs
107
114
  const formattedMessage = prefix + message;
108
115
  switch (level) {
@@ -147,12 +154,12 @@ function flushLogsForActivity(state, activityId) {
147
154
  * @param options - Optional configuration for the adapter
148
155
  */
149
156
  export function createReporterAdapter(options) {
150
- // Get output config from options, or use defaults
151
- const outputConfig = options?.output ?? {
152
- color: true,
153
- unicode: true,
154
- verbose: options?.verbose ?? false,
155
- };
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
+ }
156
163
  // Verbose can be set via options.verbose for backwards compatibility
157
164
  if (options?.verbose !== undefined) {
158
165
  outputConfig.verbose = options.verbose;
@@ -189,8 +196,8 @@ export function createReporterAdapter(options) {
189
196
  layout: event.layout,
190
197
  hasFailure: false,
191
198
  });
192
- // In plain mode, use simple bracket notation
193
- if (!state.outputConfig.unicode) {
199
+ // In plain or non-interactive mode, use simple bracket notation
200
+ if (isPlainOutput(state.outputConfig)) {
194
201
  const label = colorize(event.label, pc.bold, state.outputConfig.color);
195
202
  writeLine(`${state.symbols.groupStart}${label}${state.symbols.groupEnd}`);
196
203
  }
@@ -258,7 +265,7 @@ export function createReporterAdapter(options) {
258
265
  if (activity.groupId !== event.id)
259
266
  continue;
260
267
  if (activity.status === 'success') {
261
- if (!state.outputConfig.unicode) {
268
+ if (isPlainOutput(state.outputConfig)) {
262
269
  const prefix = colorize(state.symbols.success, pc.green, state.outputConfig.color);
263
270
  writeLine(` ${prefix} ${activity.label}`);
264
271
  }
@@ -269,7 +276,7 @@ export function createReporterAdapter(options) {
269
276
  else if (activity.status === 'failure') {
270
277
  hasFailures = true;
271
278
  // Show label inside group, defer error message with remediation
272
- if (!state.outputConfig.unicode) {
279
+ if (isPlainOutput(state.outputConfig)) {
273
280
  const prefix = colorize(state.symbols.error, pc.red, state.outputConfig.color);
274
281
  writeLine(` ${prefix} ${activity.label}`);
275
282
  }
@@ -295,8 +302,8 @@ export function createReporterAdapter(options) {
295
302
  }
296
303
  }
297
304
  // Show appropriate outro based on whether there were failures
298
- if (!state.outputConfig.unicode) {
299
- // Plain mode - simple bracket notation
305
+ if (isPlainOutput(state.outputConfig)) {
306
+ // Plain output - simple bracket notation
300
307
  if (hasFailures) {
301
308
  const failedText = colorize(state.symbols.failed, pc.red, state.outputConfig.color);
302
309
  writeLine(`${state.symbols.groupStart}${failedText}${state.symbols.groupEnd}`);
@@ -307,7 +314,7 @@ export function createReporterAdapter(options) {
307
314
  }
308
315
  }
309
316
  else {
310
- // Unicode mode - use clack's outro
317
+ // Unicode + interactive mode - use clack's outro
311
318
  if (hasFailures) {
312
319
  p.outro(colorize(`${state.symbols.failed} Failed`, pc.red, state.outputConfig.color));
313
320
  }
@@ -317,7 +324,7 @@ export function createReporterAdapter(options) {
317
324
  }
318
325
  // Print deferred error messages with remediation after the group closes
319
326
  for (const deferred of deferredErrors) {
320
- if (!state.outputConfig.unicode) {
327
+ if (isPlainOutput(state.outputConfig)) {
321
328
  writeLine(`${state.symbols.error} ${deferred.error}`);
322
329
  }
323
330
  else {
@@ -352,8 +359,8 @@ export function createReporterAdapter(options) {
352
359
  groupId: event.parentId,
353
360
  status: 'pending',
354
361
  });
355
- // In plain mode, just track activities - results shown at group:end
356
- if (!state.outputConfig.unicode) {
362
+ // In plain or non-interactive mode, track activities - results shown at group:end
363
+ if (isPlainOutput(state.outputConfig)) {
357
364
  if (!state.parallelSpinnerGroupId) {
358
365
  state.parallelSpinnerGroupId = event.parentId;
359
366
  const activities = state.parallelActivities.size;
@@ -361,7 +368,7 @@ export function createReporterAdapter(options) {
361
368
  }
362
369
  }
363
370
  else {
364
- // Unicode mode: create or update the parallel spinner
371
+ // Unicode + interactive mode: create or update the parallel spinner
365
372
  if (!state.parallelSpinner) {
366
373
  const spinner = p.spinner();
367
374
  state.parallelSpinner = {
@@ -377,17 +384,17 @@ export function createReporterAdapter(options) {
377
384
  }
378
385
  else {
379
386
  // Sequential activity
380
- if (!state.outputConfig.unicode) {
381
- // Plain mode: track activity without spinner - result shown on completion
387
+ if (isPlainOutput(state.outputConfig)) {
388
+ // Plain output: track activity without spinner - result shown on completion
382
389
  state.spinners.set(event.id, {
383
- spinner: null, // Not used in plain mode
390
+ spinner: null, // Not used in non-interactive output
384
391
  label: event.label,
385
392
  currentMessage: event.label,
386
393
  parentGroupId: event.parentId,
387
394
  });
388
395
  }
389
396
  else {
390
- // Unicode mode: create individual spinner
397
+ // Unicode + interactive mode: create individual spinner
391
398
  const spinner = p.spinner();
392
399
  state.spinners.set(event.id, {
393
400
  spinner,
@@ -409,7 +416,7 @@ export function createReporterAdapter(options) {
409
416
  // Update the activity label if message provided
410
417
  if (event.payload.message) {
411
418
  parallelActivity.label = event.payload.message;
412
- if (state.outputConfig.unicode) {
419
+ if (canUseInteractiveUI(state.outputConfig)) {
413
420
  updateParallelSpinnerMessage(state);
414
421
  }
415
422
  }
@@ -423,7 +430,7 @@ export function createReporterAdapter(options) {
423
430
  if (text) {
424
431
  entry.currentMessage = text;
425
432
  // Only update spinner in unicode mode
426
- if (state.outputConfig.unicode && entry.spinner) {
433
+ if (canUseInteractiveUI(state.outputConfig) && entry.spinner) {
427
434
  entry.spinner.message(text);
428
435
  }
429
436
  }
@@ -435,7 +442,7 @@ export function createReporterAdapter(options) {
435
442
  const parallelActivity = state.parallelActivities.get(event.id);
436
443
  if (parallelActivity) {
437
444
  parallelActivity.status = 'success';
438
- if (state.outputConfig.unicode) {
445
+ if (canUseInteractiveUI(state.outputConfig)) {
439
446
  updateParallelSpinnerMessage(state);
440
447
  }
441
448
  // Note: For parallel activities, logs will be flushed at group:end
@@ -444,13 +451,13 @@ export function createReporterAdapter(options) {
444
451
  // Sequential activity
445
452
  const entry = state.spinners.get(event.id);
446
453
  if (entry) {
447
- if (!state.outputConfig.unicode) {
448
- // Plain mode - print success line
454
+ if (isPlainOutput(state.outputConfig)) {
455
+ // Plain output - print success line
449
456
  const prefix = colorize(state.symbols.success, pc.green, state.outputConfig.color);
450
457
  writeLine(` ${prefix} ${entry.label}`);
451
458
  }
452
459
  else if (entry.spinner) {
453
- // Unicode mode - stop spinner
460
+ // Unicode + interactive mode - stop spinner
454
461
  entry.spinner.stop(entry.label, 0);
455
462
  }
456
463
  state.spinners.delete(event.id);
@@ -461,7 +468,7 @@ export function createReporterAdapter(options) {
461
468
  // Check if this was a suspended activity
462
469
  const suspended = state.suspendedActivities.get(event.id);
463
470
  if (suspended && !state.suspended) {
464
- if (!state.outputConfig.unicode) {
471
+ if (isPlainOutput(state.outputConfig)) {
465
472
  const prefix = colorize(state.symbols.success, pc.green, state.outputConfig.color);
466
473
  writeLine(` ${prefix} ${suspended.label}`);
467
474
  }
@@ -487,7 +494,7 @@ export function createReporterAdapter(options) {
487
494
  if (parentGroup) {
488
495
  parentGroup.hasFailure = true;
489
496
  }
490
- if (state.outputConfig.unicode) {
497
+ if (canUseInteractiveUI(state.outputConfig)) {
491
498
  updateParallelSpinnerMessage(state);
492
499
  }
493
500
  break;
@@ -502,13 +509,13 @@ export function createReporterAdapter(options) {
502
509
  parentGroup.hasFailure = true;
503
510
  }
504
511
  }
505
- if (!state.outputConfig.unicode) {
506
- // Plain mode - print error line
512
+ if (isPlainOutput(state.outputConfig)) {
513
+ // Plain output - print error line
507
514
  const prefix = colorize(state.symbols.error, pc.red, state.outputConfig.color);
508
515
  writeLine(` ${prefix} ${errorMessage}`);
509
516
  }
510
517
  else if (entry.spinner) {
511
- // Unicode mode - stop spinner with error
518
+ // Unicode + interactive mode - stop spinner with error
512
519
  entry.spinner.stop(errorMessage, 1);
513
520
  }
514
521
  state.spinners.delete(event.id);
@@ -533,7 +540,7 @@ export function createReporterAdapter(options) {
533
540
  // Check if this was a suspended activity
534
541
  const suspended = state.suspendedActivities.get(event.id);
535
542
  if (suspended && !state.suspended) {
536
- if (!state.outputConfig.unicode) {
543
+ if (isPlainOutput(state.outputConfig)) {
537
544
  const prefix = colorize(state.symbols.error, pc.red, state.outputConfig.color);
538
545
  writeLine(` ${prefix} ${suspended.label}: ${errorMessage}`);
539
546
  }
@@ -559,7 +566,7 @@ export function createReporterAdapter(options) {
559
566
  if (event.level === 'error' &&
560
567
  hasActiveSpinners &&
561
568
  event.activityId &&
562
- state.outputConfig.unicode) {
569
+ canUseInteractiveUI(state.outputConfig)) {
563
570
  const spinner = state.spinners.get(event.activityId);
564
571
  if (spinner && spinner.spinner) {
565
572
  // Temporarily stop spinner, show error, resume
@@ -596,8 +603,8 @@ export function createReporterAdapter(options) {
596
603
  // Reporter control events
597
604
  case 'reporter:suspend': {
598
605
  state.suspended = true;
599
- // Stop all active spinners and track them for completion messages (only in unicode mode)
600
- if (state.outputConfig.unicode) {
606
+ // Stop all active spinners and track them for completion messages
607
+ if (canUseInteractiveUI(state.outputConfig)) {
601
608
  for (const [id, entry] of state.spinners) {
602
609
  try {
603
610
  if (entry.spinner) {
@@ -632,8 +639,8 @@ export function createReporterAdapter(options) {
632
639
  const unsubscribe = bus.on(handleEvent);
633
640
  return {
634
641
  stop() {
635
- // Stop all active spinners (only in unicode mode where spinners exist)
636
- if (state.outputConfig.unicode) {
642
+ // Stop all active spinners (only in interactive mode where spinners exist)
643
+ if (canUseInteractiveUI(state.outputConfig)) {
637
644
  for (const entry of state.spinners.values()) {
638
645
  try {
639
646
  if (entry.spinner) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pokit/reporter-clack",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Clack-based event reporter for pok CLI applications",
5
5
  "keywords": [
6
6
  "cli",
@@ -50,10 +50,11 @@
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/bun": "latest",
53
- "@pokit/core": "0.0.7"
53
+ "@pokit/core": "0.0.9",
54
+ "@pokit/test-utils": "0.0.0"
54
55
  },
55
56
  "peerDependencies": {
56
- "@pokit/core": "0.0.7"
57
+ "@pokit/core": "0.0.9"
57
58
  },
58
59
  "engines": {
59
60
  "bun": ">=1.0.0"
package/src/adapter.ts CHANGED
@@ -12,18 +12,19 @@
12
12
  * - Process output is never interleaved with spinners
13
13
  *
14
14
  * Rendering strategy:
15
- * - group:start -> p.intro() with bold label (or plain text in plain mode)
15
+ * - group:start -> p.intro() with bold label (or line-based output)
16
16
  * - group:end -> p.outro() with success indicator
17
- * - activity:start (sequential) -> spinner.start() (or plain text indicator)
17
+ * - activity:start (sequential) -> spinner.start() (or line-based output)
18
18
  * - activity:start (parallel) -> track activity, update combined spinner
19
19
  * - activity:success -> spinner.stop() with checkmark (code 0) or update combined spinner
20
20
  * - activity:failure -> spinner.stop() with X (code 1)
21
21
  * - activity:update -> spinner.message()
22
22
  * - log -> pause spinner if active, p.log.*, resume spinner
23
23
  *
24
- * Plain mode (--plain or CI environment):
25
- * - When unicode is disabled, uses ASCII symbols and bypasses clack's decorative output
26
- * - When color is disabled (--no-color or NO_COLOR env), strips ANSI color codes
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
27
28
  */
28
29
 
29
30
  import * as p from '@clack/prompts';
@@ -73,6 +74,14 @@ function writeLine(line: string): void {
73
74
  process.stdout.write(line + '\n');
74
75
  }
75
76
 
77
+ function isPlainOutput(outputConfig: OutputConfig): boolean {
78
+ return !outputConfig.unicode || !outputConfig.interactive;
79
+ }
80
+
81
+ function canUseInteractiveUI(outputConfig: OutputConfig): boolean {
82
+ return outputConfig.unicode && outputConfig.interactive;
83
+ }
84
+
76
85
  type SpinnerInstance = ReturnType<typeof p.spinner>;
77
86
 
78
87
  type SpinnerEntry = {
@@ -172,7 +181,7 @@ function updateParallelSpinnerMessage(state: AdapterState): void {
172
181
 
173
182
  /**
174
183
  * Display a log message.
175
- * Uses clack's log functions in unicode mode, plain console.log in plain mode.
184
+ * Uses clack's log functions in interactive unicode mode, line output otherwise.
176
185
  *
177
186
  * @param level - The log level
178
187
  * @param message - The message to display
@@ -187,8 +196,8 @@ function displayLog(
187
196
  ): void {
188
197
  const { outputConfig, symbols } = state;
189
198
 
190
- // In plain mode (no unicode), use simple console output
191
- if (!outputConfig.unicode) {
199
+ // In plain or non-interactive mode, use simple console output
200
+ if (isPlainOutput(outputConfig)) {
192
201
  const prefix = indented ? `${symbols.groupLine} ` : '';
193
202
  const levelPrefix = {
194
203
  info: symbols.info,
@@ -201,7 +210,7 @@ function displayLog(
201
210
  return;
202
211
  }
203
212
 
204
- // Unicode mode - use clack's decorative output
213
+ // Unicode + interactive mode - use clack's decorative output
205
214
  const prefix = indented ? '\u2502 ' : ''; // │ for indented logs
206
215
  const formattedMessage = prefix + message;
207
216
 
@@ -261,12 +270,13 @@ export type ReporterAdapterOptions = {
261
270
  * @param options - Optional configuration for the adapter
262
271
  */
263
272
  export function createReporterAdapter(options?: ReporterAdapterOptions): ReporterAdapter {
264
- // Get output config from options, or use defaults
265
- const outputConfig: OutputConfig = options?.output ?? {
266
- color: true,
267
- unicode: true,
268
- verbose: options?.verbose ?? false,
269
- };
273
+ // Get output config from options, or detect from args/env
274
+ const outputConfig: OutputConfig = options?.output ?? detectOutputConfig(process.argv.slice(2));
275
+ // Backwards compatibility: default interactive when missing
276
+ if (outputConfig.interactive === undefined) {
277
+ outputConfig.interactive = true;
278
+ }
279
+
270
280
  // Verbose can be set via options.verbose for backwards compatibility
271
281
  if (options?.verbose !== undefined) {
272
282
  outputConfig.verbose = options.verbose;
@@ -307,8 +317,8 @@ export function createReporterAdapter(options?: ReporterAdapterOptions): Reporte
307
317
  hasFailure: false,
308
318
  });
309
319
 
310
- // In plain mode, use simple bracket notation
311
- if (!state.outputConfig.unicode) {
320
+ // In plain or non-interactive mode, use simple bracket notation
321
+ if (isPlainOutput(state.outputConfig)) {
312
322
  const label = colorize(event.label, pc.bold, state.outputConfig.color);
313
323
  writeLine(`${state.symbols.groupStart}${label}${state.symbols.groupEnd}`);
314
324
  } else {
@@ -384,7 +394,7 @@ export function createReporterAdapter(options?: ReporterAdapterOptions): Reporte
384
394
  for (const [activityId, activity] of state.parallelActivities) {
385
395
  if (activity.groupId !== event.id) continue;
386
396
  if (activity.status === 'success') {
387
- if (!state.outputConfig.unicode) {
397
+ if (isPlainOutput(state.outputConfig)) {
388
398
  const prefix = colorize(
389
399
  state.symbols.success,
390
400
  pc.green,
@@ -397,7 +407,7 @@ export function createReporterAdapter(options?: ReporterAdapterOptions): Reporte
397
407
  } else if (activity.status === 'failure') {
398
408
  hasFailures = true;
399
409
  // Show label inside group, defer error message with remediation
400
- if (!state.outputConfig.unicode) {
410
+ if (isPlainOutput(state.outputConfig)) {
401
411
  const prefix = colorize(state.symbols.error, pc.red, state.outputConfig.color);
402
412
  writeLine(` ${prefix} ${activity.label}`);
403
413
  } else {
@@ -424,8 +434,8 @@ export function createReporterAdapter(options?: ReporterAdapterOptions): Reporte
424
434
  }
425
435
 
426
436
  // Show appropriate outro based on whether there were failures
427
- if (!state.outputConfig.unicode) {
428
- // Plain mode - simple bracket notation
437
+ if (isPlainOutput(state.outputConfig)) {
438
+ // Plain output - simple bracket notation
429
439
  if (hasFailures) {
430
440
  const failedText = colorize(state.symbols.failed, pc.red, state.outputConfig.color);
431
441
  writeLine(`${state.symbols.groupStart}${failedText}${state.symbols.groupEnd}`);
@@ -434,7 +444,7 @@ export function createReporterAdapter(options?: ReporterAdapterOptions): Reporte
434
444
  writeLine(`${state.symbols.groupStart}${doneText}${state.symbols.groupEnd}`);
435
445
  }
436
446
  } else {
437
- // Unicode mode - use clack's outro
447
+ // Unicode + interactive mode - use clack's outro
438
448
  if (hasFailures) {
439
449
  p.outro(
440
450
  colorize(`${state.symbols.failed} Failed`, pc.red, state.outputConfig.color)
@@ -446,7 +456,7 @@ export function createReporterAdapter(options?: ReporterAdapterOptions): Reporte
446
456
 
447
457
  // Print deferred error messages with remediation after the group closes
448
458
  for (const deferred of deferredErrors) {
449
- if (!state.outputConfig.unicode) {
459
+ if (isPlainOutput(state.outputConfig)) {
450
460
  writeLine(`${state.symbols.error} ${deferred.error}`);
451
461
  } else {
452
462
  p.log.error(deferred.error);
@@ -485,15 +495,15 @@ export function createReporterAdapter(options?: ReporterAdapterOptions): Reporte
485
495
  status: 'pending',
486
496
  });
487
497
 
488
- // In plain mode, just track activities - results shown at group:end
489
- if (!state.outputConfig.unicode) {
498
+ // In plain or non-interactive mode, track activities - results shown at group:end
499
+ if (isPlainOutput(state.outputConfig)) {
490
500
  if (!state.parallelSpinnerGroupId) {
491
501
  state.parallelSpinnerGroupId = event.parentId as GroupId;
492
502
  const activities = state.parallelActivities.size;
493
503
  writeLine(` Running ${activities} task${activities > 1 ? 's' : ''}...`);
494
504
  }
495
505
  } else {
496
- // Unicode mode: create or update the parallel spinner
506
+ // Unicode + interactive mode: create or update the parallel spinner
497
507
  if (!state.parallelSpinner) {
498
508
  const spinner = p.spinner();
499
509
  state.parallelSpinner = {
@@ -508,16 +518,16 @@ export function createReporterAdapter(options?: ReporterAdapterOptions): Reporte
508
518
  }
509
519
  } else {
510
520
  // Sequential activity
511
- if (!state.outputConfig.unicode) {
512
- // Plain mode: track activity without spinner - result shown on completion
521
+ if (isPlainOutput(state.outputConfig)) {
522
+ // Plain output: track activity without spinner - result shown on completion
513
523
  state.spinners.set(event.id, {
514
- spinner: null as unknown as SpinnerInstance, // Not used in plain mode
524
+ spinner: null as unknown as SpinnerInstance, // Not used in non-interactive output
515
525
  label: event.label,
516
526
  currentMessage: event.label,
517
527
  parentGroupId: event.parentId as GroupId | undefined,
518
528
  });
519
529
  } else {
520
- // Unicode mode: create individual spinner
530
+ // Unicode + interactive mode: create individual spinner
521
531
  const spinner = p.spinner();
522
532
  state.spinners.set(event.id, {
523
533
  spinner,
@@ -540,7 +550,7 @@ export function createReporterAdapter(options?: ReporterAdapterOptions): Reporte
540
550
  // Update the activity label if message provided
541
551
  if (event.payload.message) {
542
552
  parallelActivity.label = event.payload.message;
543
- if (state.outputConfig.unicode) {
553
+ if (canUseInteractiveUI(state.outputConfig)) {
544
554
  updateParallelSpinnerMessage(state);
545
555
  }
546
556
  }
@@ -556,7 +566,7 @@ export function createReporterAdapter(options?: ReporterAdapterOptions): Reporte
556
566
  if (text) {
557
567
  entry.currentMessage = text;
558
568
  // Only update spinner in unicode mode
559
- if (state.outputConfig.unicode && entry.spinner) {
569
+ if (canUseInteractiveUI(state.outputConfig) && entry.spinner) {
560
570
  entry.spinner.message(text);
561
571
  }
562
572
  }
@@ -569,7 +579,7 @@ export function createReporterAdapter(options?: ReporterAdapterOptions): Reporte
569
579
  const parallelActivity = state.parallelActivities.get(event.id);
570
580
  if (parallelActivity) {
571
581
  parallelActivity.status = 'success';
572
- if (state.outputConfig.unicode) {
582
+ if (canUseInteractiveUI(state.outputConfig)) {
573
583
  updateParallelSpinnerMessage(state);
574
584
  }
575
585
  // Note: For parallel activities, logs will be flushed at group:end
@@ -579,12 +589,12 @@ export function createReporterAdapter(options?: ReporterAdapterOptions): Reporte
579
589
  // Sequential activity
580
590
  const entry = state.spinners.get(event.id);
581
591
  if (entry) {
582
- if (!state.outputConfig.unicode) {
583
- // Plain mode - print success line
592
+ if (isPlainOutput(state.outputConfig)) {
593
+ // Plain output - print success line
584
594
  const prefix = colorize(state.symbols.success, pc.green, state.outputConfig.color);
585
595
  writeLine(` ${prefix} ${entry.label}`);
586
596
  } else if (entry.spinner) {
587
- // Unicode mode - stop spinner
597
+ // Unicode + interactive mode - stop spinner
588
598
  entry.spinner.stop(entry.label, 0);
589
599
  }
590
600
  state.spinners.delete(event.id);
@@ -595,7 +605,7 @@ export function createReporterAdapter(options?: ReporterAdapterOptions): Reporte
595
605
  // Check if this was a suspended activity
596
606
  const suspended = state.suspendedActivities.get(event.id);
597
607
  if (suspended && !state.suspended) {
598
- if (!state.outputConfig.unicode) {
608
+ if (isPlainOutput(state.outputConfig)) {
599
609
  const prefix = colorize(
600
610
  state.symbols.success,
601
611
  pc.green,
@@ -626,7 +636,7 @@ export function createReporterAdapter(options?: ReporterAdapterOptions): Reporte
626
636
  if (parentGroup) {
627
637
  parentGroup.hasFailure = true;
628
638
  }
629
- if (state.outputConfig.unicode) {
639
+ if (canUseInteractiveUI(state.outputConfig)) {
630
640
  updateParallelSpinnerMessage(state);
631
641
  }
632
642
  break;
@@ -643,12 +653,12 @@ export function createReporterAdapter(options?: ReporterAdapterOptions): Reporte
643
653
  }
644
654
  }
645
655
 
646
- if (!state.outputConfig.unicode) {
647
- // Plain mode - print error line
656
+ if (isPlainOutput(state.outputConfig)) {
657
+ // Plain output - print error line
648
658
  const prefix = colorize(state.symbols.error, pc.red, state.outputConfig.color);
649
659
  writeLine(` ${prefix} ${errorMessage}`);
650
660
  } else if (entry.spinner) {
651
- // Unicode mode - stop spinner with error
661
+ // Unicode + interactive mode - stop spinner with error
652
662
  entry.spinner.stop(errorMessage, 1);
653
663
  }
654
664
  state.spinners.delete(event.id);
@@ -675,7 +685,7 @@ export function createReporterAdapter(options?: ReporterAdapterOptions): Reporte
675
685
  // Check if this was a suspended activity
676
686
  const suspended = state.suspendedActivities.get(event.id);
677
687
  if (suspended && !state.suspended) {
678
- if (!state.outputConfig.unicode) {
688
+ if (isPlainOutput(state.outputConfig)) {
679
689
  const prefix = colorize(state.symbols.error, pc.red, state.outputConfig.color);
680
690
  writeLine(` ${prefix} ${suspended.label}: ${errorMessage}`);
681
691
  } else {
@@ -704,7 +714,7 @@ export function createReporterAdapter(options?: ReporterAdapterOptions): Reporte
704
714
  event.level === 'error' &&
705
715
  hasActiveSpinners &&
706
716
  event.activityId &&
707
- state.outputConfig.unicode
717
+ canUseInteractiveUI(state.outputConfig)
708
718
  ) {
709
719
  const spinner = state.spinners.get(event.activityId);
710
720
  if (spinner && spinner.spinner) {
@@ -747,8 +757,8 @@ export function createReporterAdapter(options?: ReporterAdapterOptions): Reporte
747
757
  // Reporter control events
748
758
  case 'reporter:suspend': {
749
759
  state.suspended = true;
750
- // Stop all active spinners and track them for completion messages (only in unicode mode)
751
- if (state.outputConfig.unicode) {
760
+ // Stop all active spinners and track them for completion messages
761
+ if (canUseInteractiveUI(state.outputConfig)) {
752
762
  for (const [id, entry] of state.spinners) {
753
763
  try {
754
764
  if (entry.spinner) {
@@ -785,8 +795,8 @@ export function createReporterAdapter(options?: ReporterAdapterOptions): Reporte
785
795
 
786
796
  return {
787
797
  stop(): void {
788
- // Stop all active spinners (only in unicode mode where spinners exist)
789
- if (state.outputConfig.unicode) {
798
+ // Stop all active spinners (only in interactive mode where spinners exist)
799
+ if (canUseInteractiveUI(state.outputConfig)) {
790
800
  for (const entry of state.spinners.values()) {
791
801
  try {
792
802
  if (entry.spinner) {