@pokit/reporter-clack 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Daniel Grant
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # @pokit/reporter-clack
2
+
3
+ Terminal output adapter for pok using [Clack](https://github.com/natemoo-re/clack).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add @pokit/reporter-clack
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { run } from '@pokit/core';
15
+ import { createReporterAdapter } from '@pokit/reporter-clack';
16
+
17
+ await run(args, {
18
+ reporterAdapter: createReporterAdapter(),
19
+ // ...
20
+ });
21
+ ```
22
+
23
+ ## Features
24
+
25
+ - Styled log messages (info, success, warn, error)
26
+ - Progress spinners
27
+ - Grouped output with collapsible sections
28
+ - Activity indicators
29
+ - Unicode and ASCII symbol sets
30
+
31
+ ## Options
32
+
33
+ ```typescript
34
+ createReporterAdapter({
35
+ plain: false, // Disable colors and spinners
36
+ });
37
+ ```
38
+
39
+ ## Exports
40
+
41
+ ```typescript
42
+ // Main adapter
43
+ import { createReporterAdapter } from '@pokit/reporter-clack';
44
+
45
+ // Symbol customization
46
+ import { getSymbols, UNICODE_SYMBOLS, ASCII_SYMBOLS } from '@pokit/reporter-clack';
47
+ ```
48
+
49
+ ## Documentation
50
+
51
+ See the [full documentation](https://github.com/openpok/pok/blob/main/docs/packages/reporter-clack.md).
@@ -0,0 +1,44 @@
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 plain text in plain mode)
16
+ * - group:end -> p.outro() with success indicator
17
+ * - activity:start (sequential) -> spinner.start() (or plain text indicator)
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
+ * 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
27
+ */
28
+ import type { ReporterAdapter, OutputConfig } from '@pokit/core';
29
+ /**
30
+ * Options for the reporter adapter
31
+ */
32
+ export type ReporterAdapterOptions = {
33
+ /** When true, logs are displayed immediately instead of being buffered during spinners */
34
+ verbose?: boolean;
35
+ /** Output configuration (color, unicode, verbose settings) */
36
+ output?: OutputConfig;
37
+ };
38
+ /**
39
+ * Create a Clack-based ReporterAdapter
40
+ *
41
+ * @param options - Optional configuration for the adapter
42
+ */
43
+ export declare function createReporterAdapter(options?: ReporterAdapterOptions): ReporterAdapter;
44
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,666 @@
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 plain text in plain mode)
16
+ * - group:end -> p.outro() with success indicator
17
+ * - activity:start (sequential) -> spinner.start() (or plain text indicator)
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
+ * 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
27
+ */
28
+ import * as p from '@clack/prompts';
29
+ import pc from 'picocolors';
30
+ import { CommandError } from '@pokit/core';
31
+ /**
32
+ * Extract error message and optional output from an error.
33
+ * If the error is a CommandError with output, includes that in the message.
34
+ */
35
+ function formatErrorMessage(error) {
36
+ if (typeof error === 'string') {
37
+ return error;
38
+ }
39
+ if (error instanceof CommandError && error.output) {
40
+ return `${error.message}\n\n${error.output}`;
41
+ }
42
+ return error.message;
43
+ }
44
+ import { getSymbols } from './symbols';
45
+ /**
46
+ * Helper to conditionally apply color.
47
+ * In no-color mode, returns text unchanged.
48
+ */
49
+ function colorize(text, colorFn, useColor) {
50
+ return useColor ? colorFn(text) : text;
51
+ }
52
+ /**
53
+ * Write a line to stdout.
54
+ * Uses process.stdout.write to ensure proper capture in all environments.
55
+ * (Bun's console.log may not be captured by stdout interception)
56
+ */
57
+ function writeLine(line) {
58
+ process.stdout.write(line + '\n');
59
+ }
60
+ /** Maximum number of logs to buffer per activity to prevent memory issues */
61
+ const MAX_BUFFERED_LOGS_PER_ACTIVITY = 100;
62
+ /**
63
+ * Update the parallel spinner message based on current activity states
64
+ */
65
+ function updateParallelSpinnerMessage(state) {
66
+ if (!state.parallelSpinner)
67
+ return;
68
+ const activities = Array.from(state.parallelActivities.values()).filter((a) => a.groupId === state.parallelSpinnerGroupId);
69
+ const pending = activities.filter((a) => a.status === 'pending');
70
+ const completed = activities.filter((a) => a.status !== 'pending');
71
+ if (pending.length === 0) {
72
+ // All done - this will be cleaned up by group:end
73
+ return;
74
+ }
75
+ const message = pending.length === 1
76
+ ? pending[0].label
77
+ : `Running ${pending.length} tasks (${completed.length}/${activities.length} done)`;
78
+ state.parallelSpinner.currentMessage = message;
79
+ state.parallelSpinner.spinner.message(message);
80
+ }
81
+ /**
82
+ * Display a log message.
83
+ * Uses clack's log functions in unicode mode, plain console.log in plain mode.
84
+ *
85
+ * @param level - The log level
86
+ * @param message - The message to display
87
+ * @param state - The adapter state (for output config)
88
+ * @param indented - Whether to indent the log (for buffered logs inside activity context)
89
+ */
90
+ function displayLog(level, message, state, indented = false) {
91
+ const { outputConfig, symbols } = state;
92
+ // In plain mode (no unicode), use simple console output
93
+ if (!outputConfig.unicode) {
94
+ const prefix = indented ? `${symbols.groupLine} ` : '';
95
+ const levelPrefix = {
96
+ info: symbols.info,
97
+ warn: symbols.warning,
98
+ error: symbols.error,
99
+ success: symbols.success,
100
+ step: symbols.step,
101
+ }[level];
102
+ writeLine(`${prefix}${levelPrefix} ${message}`);
103
+ return;
104
+ }
105
+ // Unicode mode - use clack's decorative output
106
+ const prefix = indented ? '\u2502 ' : ''; // │ for indented logs
107
+ const formattedMessage = prefix + message;
108
+ switch (level) {
109
+ case 'info':
110
+ p.log.info(formattedMessage);
111
+ break;
112
+ case 'warn':
113
+ p.log.warn(formattedMessage);
114
+ break;
115
+ case 'error':
116
+ p.log.error(formattedMessage);
117
+ break;
118
+ case 'success':
119
+ p.log.success(formattedMessage);
120
+ break;
121
+ case 'step':
122
+ p.log.step(formattedMessage);
123
+ break;
124
+ }
125
+ }
126
+ /**
127
+ * Flush buffered logs for a specific activity.
128
+ *
129
+ * @param state - The adapter state
130
+ * @param activityId - The activity ID whose logs should be flushed
131
+ */
132
+ function flushLogsForActivity(state, activityId) {
133
+ // Filter logs for this activity, sorted by timestamp
134
+ const activityLogs = state.bufferedLogs
135
+ .filter((log) => log.activityId === activityId)
136
+ .sort((a, b) => a.timestamp - b.timestamp);
137
+ // Display each buffered log with indentation
138
+ for (const log of activityLogs) {
139
+ displayLog(log.level, log.message, state, true);
140
+ }
141
+ // Remove flushed logs from buffer
142
+ state.bufferedLogs = state.bufferedLogs.filter((log) => log.activityId !== activityId);
143
+ }
144
+ /**
145
+ * Create a Clack-based ReporterAdapter
146
+ *
147
+ * @param options - Optional configuration for the adapter
148
+ */
149
+ 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
+ };
156
+ // Verbose can be set via options.verbose for backwards compatibility
157
+ if (options?.verbose !== undefined) {
158
+ outputConfig.verbose = options.verbose;
159
+ }
160
+ const symbols = getSymbols(outputConfig);
161
+ return {
162
+ start(bus) {
163
+ const state = {
164
+ spinners: new Map(),
165
+ groups: new Map(),
166
+ parallelActivities: new Map(),
167
+ parallelSpinner: null,
168
+ parallelSpinnerGroupId: null,
169
+ suspended: false,
170
+ suspendedActivities: new Map(),
171
+ bufferedLogs: [],
172
+ verbose: outputConfig.verbose,
173
+ outputConfig,
174
+ symbols,
175
+ };
176
+ const handleEvent = (event) => {
177
+ switch (event.type) {
178
+ // Root lifecycle (app-level intro/outro)
179
+ case 'root:start':
180
+ case 'root:end':
181
+ // Handled by the router
182
+ break;
183
+ // Group lifecycle (command-level intro/outro)
184
+ case 'group:start': {
185
+ if (state.suspended)
186
+ break;
187
+ state.groups.set(event.id, {
188
+ label: event.label,
189
+ layout: event.layout,
190
+ hasFailure: false,
191
+ });
192
+ // In plain mode, use simple bracket notation
193
+ if (!state.outputConfig.unicode) {
194
+ const label = colorize(event.label, pc.bold, state.outputConfig.color);
195
+ writeLine(`${state.symbols.groupStart}${label}${state.symbols.groupEnd}`);
196
+ }
197
+ else {
198
+ p.intro(colorize(event.label, pc.bold, state.outputConfig.color));
199
+ }
200
+ break;
201
+ }
202
+ case 'group:end': {
203
+ if (state.suspended)
204
+ break;
205
+ const group = state.groups.get(event.id);
206
+ state.groups.delete(event.id);
207
+ let hasFailures = group?.hasFailure ?? false;
208
+ const deferredErrors = [];
209
+ // If this was a parallel group, show completion results and clean up
210
+ if (group?.layout === 'parallel') {
211
+ // Collect activities for this group
212
+ const activities = Array.from(state.parallelActivities.entries()).filter(([, a]) => a.groupId === event.id);
213
+ // Sort: successes first, then failures (so failures are more visible at end)
214
+ const sorted = activities.sort(([, a], [, b]) => {
215
+ if (a.status === 'success' && b.status !== 'success')
216
+ return -1;
217
+ if (a.status !== 'success' && b.status === 'success')
218
+ return 1;
219
+ return 0;
220
+ });
221
+ // Stop the parallel spinner - use first success as the message
222
+ if (state.parallelSpinner && state.parallelSpinnerGroupId === event.id) {
223
+ const firstSuccess = sorted.find(([, a]) => a.status === 'success');
224
+ if (firstSuccess) {
225
+ // Show first success via spinner stop
226
+ state.parallelSpinner.spinner.stop(firstSuccess[1].label, 0);
227
+ // Flush buffered logs for this activity
228
+ flushLogsForActivity(state, firstSuccess[0]);
229
+ state.parallelActivities.delete(firstSuccess[0]);
230
+ }
231
+ else {
232
+ // All failed - show first failure label (not error) via spinner stop
233
+ const firstFailure = sorted[0];
234
+ if (firstFailure) {
235
+ state.parallelSpinner.spinner.stop(firstFailure[1].label, 1);
236
+ hasFailures = true;
237
+ // Defer the error message with remediation to print after outro
238
+ if (firstFailure[1].error) {
239
+ deferredErrors.push({
240
+ error: firstFailure[1].error,
241
+ remediation: firstFailure[1].remediation,
242
+ documentationUrl: firstFailure[1].documentationUrl,
243
+ });
244
+ }
245
+ // Flush buffered logs for this activity
246
+ flushLogsForActivity(state, firstFailure[0]);
247
+ state.parallelActivities.delete(firstFailure[0]);
248
+ }
249
+ else {
250
+ state.parallelSpinner.spinner.stop('Complete', 0);
251
+ }
252
+ }
253
+ state.parallelSpinner = null;
254
+ state.parallelSpinnerGroupId = null;
255
+ }
256
+ // Show remaining results (only labels inside group, defer errors)
257
+ for (const [activityId, activity] of state.parallelActivities) {
258
+ if (activity.groupId !== event.id)
259
+ continue;
260
+ if (activity.status === 'success') {
261
+ if (!state.outputConfig.unicode) {
262
+ const prefix = colorize(state.symbols.success, pc.green, state.outputConfig.color);
263
+ writeLine(` ${prefix} ${activity.label}`);
264
+ }
265
+ else {
266
+ p.log.success(activity.label);
267
+ }
268
+ }
269
+ else if (activity.status === 'failure') {
270
+ hasFailures = true;
271
+ // Show label inside group, defer error message with remediation
272
+ if (!state.outputConfig.unicode) {
273
+ const prefix = colorize(state.symbols.error, pc.red, state.outputConfig.color);
274
+ writeLine(` ${prefix} ${activity.label}`);
275
+ }
276
+ else {
277
+ p.log.error(activity.label);
278
+ }
279
+ if (activity.error) {
280
+ deferredErrors.push({
281
+ error: activity.error,
282
+ remediation: activity.remediation,
283
+ documentationUrl: activity.documentationUrl,
284
+ });
285
+ }
286
+ }
287
+ // Flush any buffered logs for this parallel activity
288
+ flushLogsForActivity(state, activityId);
289
+ }
290
+ // Clean up all activities for this group
291
+ for (const [id, activity] of [...state.parallelActivities]) {
292
+ if (activity.groupId === event.id) {
293
+ state.parallelActivities.delete(id);
294
+ }
295
+ }
296
+ }
297
+ // Show appropriate outro based on whether there were failures
298
+ if (!state.outputConfig.unicode) {
299
+ // Plain mode - simple bracket notation
300
+ if (hasFailures) {
301
+ const failedText = colorize(state.symbols.failed, pc.red, state.outputConfig.color);
302
+ writeLine(`${state.symbols.groupStart}${failedText}${state.symbols.groupEnd}`);
303
+ }
304
+ else {
305
+ const doneText = colorize(state.symbols.done, pc.green, state.outputConfig.color);
306
+ writeLine(`${state.symbols.groupStart}${doneText}${state.symbols.groupEnd}`);
307
+ }
308
+ }
309
+ else {
310
+ // Unicode mode - use clack's outro
311
+ if (hasFailures) {
312
+ p.outro(colorize(`${state.symbols.failed} Failed`, pc.red, state.outputConfig.color));
313
+ }
314
+ else {
315
+ p.outro(colorize(`${state.symbols.done} Done`, pc.green, state.outputConfig.color));
316
+ }
317
+ }
318
+ // Print deferred error messages with remediation after the group closes
319
+ for (const deferred of deferredErrors) {
320
+ if (!state.outputConfig.unicode) {
321
+ writeLine(`${state.symbols.error} ${deferred.error}`);
322
+ }
323
+ else {
324
+ p.log.error(deferred.error);
325
+ }
326
+ // Display remediation steps if available
327
+ if (deferred.remediation && deferred.remediation.length > 0) {
328
+ writeLine('');
329
+ writeLine(' To fix:');
330
+ for (const step of deferred.remediation) {
331
+ writeLine(` - ${step}`);
332
+ }
333
+ }
334
+ // Display documentation link if available
335
+ if (deferred.documentationUrl) {
336
+ writeLine('');
337
+ writeLine(` More info: ${deferred.documentationUrl}`);
338
+ }
339
+ }
340
+ break;
341
+ }
342
+ // Activity lifecycle (task-level spinner)
343
+ case 'activity:start': {
344
+ if (state.suspended)
345
+ break;
346
+ // Find the parent group to determine layout
347
+ const parentGroup = event.parentId ? state.groups.get(event.parentId) : null;
348
+ if (parentGroup?.layout === 'parallel') {
349
+ // Track this activity for the parallel group
350
+ state.parallelActivities.set(event.id, {
351
+ label: event.label,
352
+ groupId: event.parentId,
353
+ status: 'pending',
354
+ });
355
+ // In plain mode, just track activities - results shown at group:end
356
+ if (!state.outputConfig.unicode) {
357
+ if (!state.parallelSpinnerGroupId) {
358
+ state.parallelSpinnerGroupId = event.parentId;
359
+ const activities = state.parallelActivities.size;
360
+ writeLine(` Running ${activities} task${activities > 1 ? 's' : ''}...`);
361
+ }
362
+ }
363
+ else {
364
+ // Unicode mode: create or update the parallel spinner
365
+ if (!state.parallelSpinner) {
366
+ const spinner = p.spinner();
367
+ state.parallelSpinner = {
368
+ spinner,
369
+ label: event.label,
370
+ currentMessage: event.label,
371
+ };
372
+ state.parallelSpinnerGroupId = event.parentId;
373
+ spinner.start(event.label);
374
+ }
375
+ updateParallelSpinnerMessage(state);
376
+ }
377
+ }
378
+ else {
379
+ // Sequential activity
380
+ if (!state.outputConfig.unicode) {
381
+ // Plain mode: track activity without spinner - result shown on completion
382
+ state.spinners.set(event.id, {
383
+ spinner: null, // Not used in plain mode
384
+ label: event.label,
385
+ currentMessage: event.label,
386
+ parentGroupId: event.parentId,
387
+ });
388
+ }
389
+ else {
390
+ // Unicode mode: create individual spinner
391
+ const spinner = p.spinner();
392
+ state.spinners.set(event.id, {
393
+ spinner,
394
+ label: event.label,
395
+ currentMessage: event.label,
396
+ parentGroupId: event.parentId,
397
+ });
398
+ spinner.start(event.label);
399
+ }
400
+ }
401
+ break;
402
+ }
403
+ case 'activity:update': {
404
+ if (state.suspended)
405
+ break;
406
+ // Check if this is a parallel activity
407
+ const parallelActivity = state.parallelActivities.get(event.id);
408
+ if (parallelActivity) {
409
+ // Update the activity label if message provided
410
+ if (event.payload.message) {
411
+ parallelActivity.label = event.payload.message;
412
+ if (state.outputConfig.unicode) {
413
+ updateParallelSpinnerMessage(state);
414
+ }
415
+ }
416
+ break;
417
+ }
418
+ // Sequential activity
419
+ const entry = state.spinners.get(event.id);
420
+ if (entry) {
421
+ const text = event.payload.message ||
422
+ (event.payload.progress !== undefined ? `${event.payload.progress}%` : null);
423
+ if (text) {
424
+ entry.currentMessage = text;
425
+ // Only update spinner in unicode mode
426
+ if (state.outputConfig.unicode && entry.spinner) {
427
+ entry.spinner.message(text);
428
+ }
429
+ }
430
+ }
431
+ break;
432
+ }
433
+ case 'activity:success': {
434
+ // Check if this is a parallel activity
435
+ const parallelActivity = state.parallelActivities.get(event.id);
436
+ if (parallelActivity) {
437
+ parallelActivity.status = 'success';
438
+ if (state.outputConfig.unicode) {
439
+ updateParallelSpinnerMessage(state);
440
+ }
441
+ // Note: For parallel activities, logs will be flushed at group:end
442
+ break;
443
+ }
444
+ // Sequential activity
445
+ const entry = state.spinners.get(event.id);
446
+ if (entry) {
447
+ if (!state.outputConfig.unicode) {
448
+ // Plain mode - print success line
449
+ const prefix = colorize(state.symbols.success, pc.green, state.outputConfig.color);
450
+ writeLine(` ${prefix} ${entry.label}`);
451
+ }
452
+ else if (entry.spinner) {
453
+ // Unicode mode - stop spinner
454
+ entry.spinner.stop(entry.label, 0);
455
+ }
456
+ state.spinners.delete(event.id);
457
+ // Flush any buffered logs for this activity
458
+ flushLogsForActivity(state, event.id);
459
+ }
460
+ else {
461
+ // Check if this was a suspended activity
462
+ const suspended = state.suspendedActivities.get(event.id);
463
+ if (suspended && !state.suspended) {
464
+ if (!state.outputConfig.unicode) {
465
+ const prefix = colorize(state.symbols.success, pc.green, state.outputConfig.color);
466
+ writeLine(` ${prefix} ${suspended.label}`);
467
+ }
468
+ else {
469
+ p.log.success(suspended.label);
470
+ }
471
+ state.suspendedActivities.delete(event.id);
472
+ }
473
+ }
474
+ break;
475
+ }
476
+ case 'activity:failure': {
477
+ const errorMessage = formatErrorMessage(event.error);
478
+ // Check if this is a parallel activity
479
+ const parallelActivity = state.parallelActivities.get(event.id);
480
+ if (parallelActivity) {
481
+ parallelActivity.status = 'failure';
482
+ parallelActivity.error = errorMessage;
483
+ parallelActivity.remediation = event.remediation;
484
+ parallelActivity.documentationUrl = event.documentationUrl;
485
+ // Mark the parent group as having failures
486
+ const parentGroup = state.groups.get(parallelActivity.groupId);
487
+ if (parentGroup) {
488
+ parentGroup.hasFailure = true;
489
+ }
490
+ if (state.outputConfig.unicode) {
491
+ updateParallelSpinnerMessage(state);
492
+ }
493
+ break;
494
+ }
495
+ // Sequential activity
496
+ const entry = state.spinners.get(event.id);
497
+ if (entry) {
498
+ // Mark the parent group as having failures
499
+ if (entry.parentGroupId) {
500
+ const parentGroup = state.groups.get(entry.parentGroupId);
501
+ if (parentGroup) {
502
+ parentGroup.hasFailure = true;
503
+ }
504
+ }
505
+ if (!state.outputConfig.unicode) {
506
+ // Plain mode - print error line
507
+ const prefix = colorize(state.symbols.error, pc.red, state.outputConfig.color);
508
+ writeLine(` ${prefix} ${errorMessage}`);
509
+ }
510
+ else if (entry.spinner) {
511
+ // Unicode mode - stop spinner with error
512
+ entry.spinner.stop(errorMessage, 1);
513
+ }
514
+ state.spinners.delete(event.id);
515
+ // Display remediation steps if available
516
+ const linePrefix = state.outputConfig.unicode ? '\u2502' : state.symbols.groupLine;
517
+ if (event.remediation && event.remediation.length > 0) {
518
+ writeLine(linePrefix);
519
+ writeLine(`${linePrefix} To fix:`);
520
+ for (const step of event.remediation) {
521
+ writeLine(`${linePrefix} - ${step}`);
522
+ }
523
+ }
524
+ // Display documentation link if available
525
+ if (event.documentationUrl) {
526
+ writeLine(linePrefix);
527
+ writeLine(`${linePrefix} More info: ${event.documentationUrl}`);
528
+ }
529
+ // Flush any buffered logs for this activity
530
+ flushLogsForActivity(state, event.id);
531
+ }
532
+ else {
533
+ // Check if this was a suspended activity
534
+ const suspended = state.suspendedActivities.get(event.id);
535
+ if (suspended && !state.suspended) {
536
+ if (!state.outputConfig.unicode) {
537
+ const prefix = colorize(state.symbols.error, pc.red, state.outputConfig.color);
538
+ writeLine(` ${prefix} ${suspended.label}: ${errorMessage}`);
539
+ }
540
+ else {
541
+ p.log.error(`${suspended.label}: ${errorMessage}`);
542
+ }
543
+ state.suspendedActivities.delete(event.id);
544
+ }
545
+ }
546
+ break;
547
+ }
548
+ // Log events
549
+ case 'log': {
550
+ if (state.suspended)
551
+ break;
552
+ // Verbose mode: always display logs immediately
553
+ if (state.verbose) {
554
+ displayLog(event.level, event.message, state, false);
555
+ break;
556
+ }
557
+ const hasActiveSpinners = state.spinners.size > 0 || state.parallelSpinner !== null;
558
+ // Error logs interrupt spinners immediately (only in unicode mode)
559
+ if (event.level === 'error' &&
560
+ hasActiveSpinners &&
561
+ event.activityId &&
562
+ state.outputConfig.unicode) {
563
+ const spinner = state.spinners.get(event.activityId);
564
+ if (spinner && spinner.spinner) {
565
+ // Temporarily stop spinner, show error, resume
566
+ const currentMessage = spinner.currentMessage;
567
+ spinner.spinner.stop(currentMessage, 0);
568
+ displayLog(event.level, event.message, state, false);
569
+ spinner.spinner.start(currentMessage);
570
+ }
571
+ else {
572
+ // No spinner for this activity, just display
573
+ displayLog(event.level, event.message, state, false);
574
+ }
575
+ break;
576
+ }
577
+ // Buffer logs during active spinners
578
+ if (hasActiveSpinners && event.activityId) {
579
+ // Check if we've hit the buffer limit for this activity
580
+ const activityLogCount = state.bufferedLogs.filter((log) => log.activityId === event.activityId).length;
581
+ if (activityLogCount < MAX_BUFFERED_LOGS_PER_ACTIVITY) {
582
+ state.bufferedLogs.push({
583
+ activityId: event.activityId,
584
+ level: event.level,
585
+ message: event.message,
586
+ timestamp: Date.now(),
587
+ });
588
+ }
589
+ // If over limit, silently drop (prevent memory issues)
590
+ break;
591
+ }
592
+ // No active spinners - display immediately
593
+ displayLog(event.level, event.message, state, false);
594
+ break;
595
+ }
596
+ // Reporter control events
597
+ case 'reporter:suspend': {
598
+ state.suspended = true;
599
+ // Stop all active spinners and track them for completion messages (only in unicode mode)
600
+ if (state.outputConfig.unicode) {
601
+ for (const [id, entry] of state.spinners) {
602
+ try {
603
+ if (entry.spinner) {
604
+ entry.spinner.stop(entry.label + '...', 0);
605
+ }
606
+ state.suspendedActivities.set(id, { label: entry.label });
607
+ }
608
+ catch {
609
+ // Spinner may already be stopped
610
+ }
611
+ }
612
+ state.spinners.clear();
613
+ // Also stop parallel spinner
614
+ if (state.parallelSpinner) {
615
+ try {
616
+ state.parallelSpinner.spinner.stop('Paused...', 0);
617
+ }
618
+ catch {
619
+ // Spinner may already be stopped
620
+ }
621
+ state.parallelSpinner = null;
622
+ }
623
+ }
624
+ break;
625
+ }
626
+ case 'reporter:resume': {
627
+ state.suspended = false;
628
+ break;
629
+ }
630
+ }
631
+ };
632
+ const unsubscribe = bus.on(handleEvent);
633
+ return {
634
+ stop() {
635
+ // Stop all active spinners (only in unicode mode where spinners exist)
636
+ if (state.outputConfig.unicode) {
637
+ for (const entry of state.spinners.values()) {
638
+ try {
639
+ if (entry.spinner) {
640
+ entry.spinner.stop('Stopped', 1);
641
+ }
642
+ }
643
+ catch {
644
+ // Spinner may already be stopped
645
+ }
646
+ }
647
+ if (state.parallelSpinner) {
648
+ try {
649
+ state.parallelSpinner.spinner.stop('Stopped', 1);
650
+ }
651
+ catch {
652
+ // Spinner may already be stopped
653
+ }
654
+ }
655
+ }
656
+ state.spinners.clear();
657
+ state.groups.clear();
658
+ state.parallelActivities.clear();
659
+ state.parallelSpinner = null;
660
+ state.parallelSpinnerGroupId = null;
661
+ unsubscribe();
662
+ },
663
+ };
664
+ },
665
+ };
666
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @pokit/reporter-clack
3
+ *
4
+ * Clack-based implementation of the ReporterAdapter interface.
5
+ * Consumes CLI events and renders them using @clack/prompts.
6
+ */
7
+ export { createReporterAdapter } from './adapter';
8
+ export type { ReporterAdapterOptions } from './adapter';
9
+ export { getSymbols, UNICODE_SYMBOLS, ASCII_SYMBOLS } from './symbols';
10
+ export type { SymbolSet } from './symbols';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAClD,YAAY,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AAGxD,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AACvE,YAAY,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @pokit/reporter-clack
3
+ *
4
+ * Clack-based implementation of the ReporterAdapter interface.
5
+ * Consumes CLI events and renders them using @clack/prompts.
6
+ */
7
+ export { createReporterAdapter } from './adapter';
8
+ // Symbol exports for custom formatting
9
+ export { getSymbols, UNICODE_SYMBOLS, ASCII_SYMBOLS } from './symbols';
@@ -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/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"}
@@ -0,0 +1,47 @@
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
+ /**
8
+ * Unicode symbols for rich terminal output
9
+ * Used when unicode support is detected
10
+ */
11
+ export const UNICODE_SYMBOLS = {
12
+ success: '\u25C7', // ◇
13
+ error: '\u25A0', // ■
14
+ warning: '\u25B2', // ▲
15
+ info: '\u25CF', // ●
16
+ step: '\u25C7', // ◇
17
+ groupStart: '\u250C', // ┌
18
+ groupEnd: '\u2514', // └
19
+ groupLine: '\u2502', // │
20
+ done: '\u2714', // ✔
21
+ failed: '\u2718', // ✘
22
+ };
23
+ /**
24
+ * ASCII symbols for plain text output
25
+ * Used in CI environments or when unicode is not supported
26
+ */
27
+ export const ASCII_SYMBOLS = {
28
+ success: '[OK]',
29
+ error: '[ERR]',
30
+ warning: '[WARN]',
31
+ info: '[INFO]',
32
+ step: '-',
33
+ groupStart: '[',
34
+ groupEnd: ']',
35
+ groupLine: '|',
36
+ done: 'Done',
37
+ failed: 'Failed',
38
+ };
39
+ /**
40
+ * Get the appropriate symbol set based on output configuration
41
+ *
42
+ * @param config - Output configuration
43
+ * @returns Symbol set to use
44
+ */
45
+ export function getSymbols(config) {
46
+ return config.unicode ? UNICODE_SYMBOLS : ASCII_SYMBOLS;
47
+ }
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@pokit/reporter-clack",
3
+ "version": "0.0.1",
4
+ "description": "Clack-based event reporter for pok CLI applications",
5
+ "keywords": [
6
+ "cli",
7
+ "command-line",
8
+ "typescript",
9
+ "bun",
10
+ "terminal",
11
+ "pok",
12
+ "pokjs",
13
+ "clack",
14
+ "reporter",
15
+ "spinner",
16
+ "progress"
17
+ ],
18
+ "type": "module",
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/notation-dev/openpok.git",
23
+ "directory": "packages/reporter-clack"
24
+ },
25
+ "homepage": "https://github.com/notation-dev/openpok#readme",
26
+ "bugs": {
27
+ "url": "https://github.com/notation-dev/openpok/issues"
28
+ },
29
+ "main": "./src/index.ts",
30
+ "module": "./src/index.ts",
31
+ "types": "./src/index.ts",
32
+ "exports": {
33
+ ".": {
34
+ "bun": "./src/index.ts",
35
+ "types": "./dist/index.d.ts",
36
+ "import": "./dist/index.js"
37
+ }
38
+ },
39
+ "files": [
40
+ "dist",
41
+ "README.md",
42
+ "LICENSE"
43
+ ],
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "dependencies": {
48
+ "@clack/prompts": "^0.11.0",
49
+ "picocolors": "^1.1.1"
50
+ },
51
+ "devDependencies": {
52
+ "@types/bun": "latest",
53
+ "@pokit/core": "0.0.1"
54
+ },
55
+ "peerDependencies": {
56
+ "@pokit/core": "0.0.1"
57
+ },
58
+ "engines": {
59
+ "bun": ">=1.0.0"
60
+ }
61
+ }
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @pokit/reporter-clack
3
+ *
4
+ * Clack-based implementation of the ReporterAdapter interface.
5
+ * Consumes CLI events and renders them using @clack/prompts.
6
+ */
7
+
8
+ export { createReporterAdapter } from './adapter';
9
+ export type { ReporterAdapterOptions } from './adapter';
10
+
11
+ // Symbol exports for custom formatting
12
+ export { getSymbols, UNICODE_SYMBOLS, ASCII_SYMBOLS } from './symbols';
13
+ export type { SymbolSet } from './symbols';