Runner
The Runner is the execution surface passed into command run() functions. It exposes shell execution, task execution, grouping, parallel execution, the command reporter, and the prompter.
Availability
The runner is provided to command run() functions. You do not import it directly.
defineCommand({
run: async (r, ctx) => {
// r is the Runner
},
});Contract
interface Runner<TContext> {
cwd: string;
reporter: CommandReporter;
prompter: Prompter;
exec(cmd: ExecInput, opts?: ExecOptions): Command;
run<TReturn>(task: AnyTaskConfig, params?: Record<string, unknown>): DeferredTask<TReturn>;
parallel(items: RunnerItem[], options?: ParallelOptions): Promise<void>;
group<T>(
label: string,
options: GroupOptions,
fn: (reporter: Reporter) => Promise<T> | T
): Promise<T>;
}Properties
cwd
cwd: string;The project root directory. Use this for file operations:
run: async (r) => {
const configPath = path.join(r.cwd, 'config.json');
};reporter
reporter: CommandReporter;Event emission for logging and progress updates:
run: async (r) => {
r.reporter.info('Starting...');
r.reporter.warn('This might take a while');
r.reporter.error('Something went wrong');
r.reporter.success('Done!');
};Render a markdown document with markdown(). The raw markdown is emitted onto
the event bus and rendered by the active adapter for its medium — @pokit/terminal
renders it to ANSI (headings, emphasis, syntax-highlighted code fences, lists),
while a web adapter can render it to HTML. When the output is not a styled TTY
(piped, --no-color, NO_COLOR), the raw markdown is passed through unchanged so
it composes with pipes:
run: async (r) => {
const readme = await readFile('README.md', 'utf8');
r.reporter.markdown(readme);
};$ mycli docs # styled ANSI in a terminal
$ mycli docs | glow # raw markdown passed through to another rendererMethods
exec
Execute a shell command.
exec(cmd: ExecInput, opts?: ExecOptions): Command
type ExecInput = string | string[] | ShellPromise;
type ExecOptions = {
timeout?: number; // Override default timeout
retry?: RetryConfig; // Retry configuration
};
type RetryConfig = {
maxAttempts: number; // Retry attempts (not including initial)
delay?: number; // Base delay in ms (default: 1000)
backoff?: 'fixed' | 'linear' | 'exponential'; // Default: 'fixed'
maxDelay?: number; // Cap for backoff growth
};Returns a Command that is thenable (can be awaited):
run: async (r) => {
// Simple execution
await r.exec('npm install');
// With timeout
await r.exec('npm test', { timeout: 60000 });
// With retry on failure
await r.exec('curl https://flaky-api.com', {
retry: { maxAttempts: 3, delay: 1000, backoff: 'exponential' },
});
// Array form (no shell interpolation, safe for dynamic input)
await r.exec(['git', 'checkout', branchName]);
};run
Execute a task with optional parameters.
run<TReturn>(task: AnyTaskConfig, params?: Record<string, unknown>): DeferredTask<TReturn>Returns a DeferredTask that is thenable:
import { buildTask, deployTask } from '../tasks';
run: async (r) => {
// Execute task
await r.run(buildTask);
// With parameters
await r.run(deployTask, { env: 'staging' });
// Get return value
const version = await r.run(getVersionTask);
};parallel
Run multiple commands/tasks in parallel with configurable execution modes.
parallel(items: RunnerItem[], options?: ParallelOptions): Promise<void>
type ParallelMode = 'race' | 'fail-fast' | 'all-settled';
type ParallelOptions = {
mode?: ParallelMode; // Default: 'race'
};Execution Modes
| Mode | Behavior |
|---|---|
race | First to settle wins, cancel rest (default) |
fail-fast | First failure cancels rest, otherwise wait for all |
all-settled | Run all to completion, throw AggregateError if any fail |
run: async (r) => {
// Race mode (default) - exits when first completes
await r.parallel([r.exec('npm run dev'), r.exec('npm run watch')]);
// Fail-fast mode - all must succeed, abort on first failure
await r.parallel([r.run(buildTask), r.run(testTask), r.run(lintTask)], { mode: 'fail-fast' });
// All-settled mode - run all, collect failures
await r.parallel([r.run(deploy1), r.run(deploy2), r.run(deploy3)], { mode: 'all-settled' });
};Retry Interaction
Tasks with retry configuration will exhaust all retries before the parallel mode rules apply:
const flakyTask = defineTask({
label: 'Flaky API call',
retry: { maxAttempts: 3, delay: 1000, backoff: 'exponential' },
exec: 'curl https://api.example.com/flaky',
});
// In fail-fast mode: flakyTask retries up to 3 times before
// being considered a failure that triggers cancellation
await r.parallel([r.run(flakyTask), r.run(stableTask)], { mode: 'fail-fast' });prompter
Interactive input inside a command. See the Prompter API.
prompter: Prompter;run: async (r) => {
const env = await r.prompter.select({
message: 'Environment',
options: [
{ value: 'staging', label: 'Staging' },
{ value: 'prod', label: 'Production' },
],
});
};group
Create a visual group for organizing activities.
group<T>(
label: string,
options: GroupOptions,
fn: (reporter: Reporter) => Promise<T> | T
): Promise<T>
type GroupOptions = {
layout: 'sequence' | 'parallel';
};run: async (r) => {
await r.group('Database Setup', { layout: 'sequence' }, async (g) => {
await g.activity('Run migrations', async () => {
await r.exec('prisma migrate deploy');
});
await g.activity('Seed data', async () => {
await r.exec('prisma db seed');
});
});
};Reporter Methods
The reporter property provides logging:
type CommandReporter = {
info(message: string): void;
warn(message: string): void;
error(message: string | Error): void;
success(message: string): void;
step(message: string): void;
};r.reporter is the restricted command reporter (logging + step). Grouping and
activities are created through r.group(...), which yields the full Reporter.
See the Events API for the full reporter surface.
Error Handling
CommandError
Thrown when a command fails, includes captured output:
import { CommandError } from '@pokit/core';
run: async (r) => {
try {
await r.exec('npm test');
} catch (error) {
if (error instanceof CommandError) {
console.log('Output:', error.output);
}
}
};AbortError
Thrown when execution is cancelled via AbortSignal:
import { AbortError } from '@pokit/core';
// This is handled internally - commands can be cancelled
// when running in parallel mode and another command failsEnvironment Variables
Resolved environment variables are automatically injected into shell commands:
const dbTask = defineTask({
env: dbEnv, // Resolves DATABASE_URL
exec: 'prisma migrate deploy', // DATABASE_URL available
});
run: async (r) => {
await r.run(dbTask);
// After dbTask, DATABASE_URL is cached and available
// in subsequent exec calls
await r.exec('psql $DATABASE_URL -c "SELECT 1"');
};Process Management
pok handles process lifecycle:
- Signal handlers - SIGINT/SIGTERM cleanup
- Process tracking - All spawned processes tracked
- Automatic cleanup - Processes killed on exit
- Parallel race - First exit kills siblings
Examples
Complete Command
import { defineCommand } from '@pokit/core';
import { buildTask, testTask, deployTask } from '../tasks';
import { dockerRunning } from '../checks';
export const command = defineCommand({
label: 'Deploy to production',
pre: [dockerRunning],
run: async (r) => {
await r.group('Build', { layout: 'sequence' }, async (g) => {
await g.activity('Compile', () => r.run(buildTask));
await g.activity('Test', () => r.run(testTask));
});
r.reporter.info('Deploying...');
await r.run(deployTask, { env: 'prod' });
r.reporter.success('Deployed!');
},
});Development Mode
export const command = defineCommand({
label: 'Start development',
run: async (r) => {
// race mode: the first process to exit tears down the rest
await r.parallel([
r.exec('npm run dev'),
r.exec('npm run watch:css'),
r.exec('stripe listen --forward-to localhost:3000/webhooks'),
]);
},
});