Events & Reporter
pok uses an event-driven architecture for all output. Commands emit semantic events that adapters render.
Event pipeline
Command → Events → EventBus → ReporterAdapter → TerminalImports
import {
// Event Bus
createEventBus,
// Reporter
ScopedReporter,
createRootReporter,
// Type Guards
isRootEvent,
isGroupEvent,
isActivityEvent,
isLogEvent,
// Types
type CLIEvent,
type EventBus,
type Reporter,
type ReporterAdapter,
} from '@pokit/core';CLIEvent Types
type CLIEvent =
// Lifecycle
| { type: 'root:start'; appName: string; version?: string }
| { type: 'root:end'; exitCode: number }
// Grouping
| { type: 'group:start'; id: GroupId; parentId?: GroupId; label: string; layout: GroupLayout }
| { type: 'group:end'; id: GroupId }
// Activities
| {
type: 'activity:start';
id: ActivityId;
parentId?: GroupId | ActivityId;
label: string;
meta?: Record<string, unknown>;
}
| { type: 'activity:success'; id: ActivityId; result?: unknown }
| { type: 'activity:failure'; id: ActivityId; error: Error | string }
| { type: 'activity:update'; id: ActivityId; payload: ActivityUpdatePayload }
// Logging
| { type: 'log'; activityId?: ActivityId; level: LogLevel; message: string }
// TUI Control
| { type: 'reporter:suspend' }
| { type: 'reporter:resume' };
type GroupLayout = 'sequence' | 'parallel' | 'tabs' | 'grid';
type LogLevel = 'info' | 'warn' | 'error' | 'success' | 'step';EventBus
The event bus is a pub/sub system for CLI events.
Note: subscribe(listener) is supported as a deprecated alias of on(listener) for compatibility.
interface EventBus {
emit(event: CLIEvent): void;
on(listener: EventListener): Unsubscribe;
}
type EventListener = (event: CLIEvent) => void;
type Unsubscribe = () => void;Usage
const eventBus = createEventBus();
// Subscribe to events
const unsubscribe = eventBus.on((event) => {
console.log('Event:', event);
});
// Emit events
eventBus.emit({ type: 'log', level: 'info', message: 'Hello' });
// Unsubscribe
unsubscribe();Reporter
The Reporter provides a high-level API for emitting events.
Reporter Interface
interface Reporter {
// Logging
info(message: string): void;
warn(message: string): void;
error(message: string): void;
success(message: string): void;
step(message: string): void;
// Grouping
group<T>(
label: string,
options: GroupOptions,
fn: (reporter: Reporter) => Promise<T> | T
): Promise<T>;
// Activities
activity<T>(label: string, fn: () => Promise<T> | T): Promise<T>;
}CommandReporter
Extended reporter available in commands:
interface CommandReporter extends Reporter {
suspend(): void; // Suspend output for TUI takeover
resume(): void; // Resume output
}Usage in Commands
run: async (r) => {
// Simple logging
r.reporter.info('Starting process...');
r.reporter.warn('This may take a while');
// Grouped activities
await r.reporter.group('Build', { layout: 'sequence' }, async (g) => {
await g.activity('Compile', async () => {
await r.exec('tsc');
});
await g.activity('Bundle', async () => {
await r.exec('esbuild');
});
});
r.reporter.success('Build complete!');
};ReporterAdapter
Adapters subscribe to events and render them to the terminal.
Interface
interface ReporterAdapter {
start(eventBus: EventBus): ReporterAdapterController;
}
interface ReporterAdapterController {
stop(): void;
}Using an Adapter
import { run } from '@pokit/core';
import { createReporterAdapter } from '@pokit/reporter-clack';
await run(args, {
// ...
reporterAdapter: createReporterAdapter(),
});Activity Updates
Activities can report progress and status:
type ActivityUpdatePayload = {
progress?: number; // 0-100
message?: string; // "Processing file 3/10"
[key: string]: unknown; // Custom data
};
// In a task
run: async (r, ctx) => {
// The task reporter supports updates
ctx.reporter.update({ progress: 50, message: 'Halfway done' });
};Type Guards
import { isRootEvent, isGroupEvent, isActivityEvent, isLogEvent } from '@pokit/core';
eventBus.on((event) => {
if (isLogEvent(event)) {
console.log(`[${event.level}] ${event.message}`);
}
if (isActivityEvent(event)) {
if (event.type === 'activity:success') {
console.log(`Activity ${event.id} completed`);
}
}
});Raw Reporter Adapter
For testing, use the raw adapter that collects events:
import { createRawReporterAdapter } from '@pokit/core';
const { adapter, getEvents } = createRawReporterAdapter();
// Use adapter in tests
await run(args, {
reporterAdapter: adapter,
// ...
});
// Assert on collected events
const events = getEvents();
expect(events).toContainEqual({
type: 'log',
level: 'success',
message: 'Build complete!',
});Event Flow Example
Command starts
→ group:start { id: 'g1', label: 'Build', layout: 'sequence' }
→ activity:start { id: 'a1', parentId: 'g1', label: 'Compile' }
→ activity:update { id: 'a1', payload: { progress: 50 } }
→ activity:success { id: 'a1' }
→ activity:start { id: 'a2', parentId: 'g1', label: 'Bundle' }
→ activity:success { id: 'a2' }
→ group:end { id: 'g1' }
→ log { level: 'success', message: 'Build complete!' }Suspend/Resume
For full-screen TUI takeover (like r.tabs()):
// Reporter output is suspended
r.reporter.suspend();
// Full-screen TUI runs
await tabsAdapter.run([...]);
// Reporter output resumes
r.reporter.resume();The adapter handles these events to pause/resume rendering.