Runner
The Runner is the execution surface passed into command run() functions. It exposes shell execution, task execution, grouping, parallel execution, tabs, apps, and the command reporter.
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>;
tabs(items: RunnerItem[], options?: TabsRunnerOptions): Promise<void>;
app<TProps>(
component: AnyComponent<TProps>,
props: TProps
): 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!');
};Methods
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' });
### tabs
Run multiple commands/tasks in a tabbed terminal interface.
```typescript
tabs(items: RunnerItem[], options?: TabsRunnerOptions): Promise<void>
type TabsRunnerOptions = {
name?: string; // Console name (e.g., "Development")
};Requires a tabs adapter (e.g., @pokit/opentui):
run: async (r) => {
await r.tabs([r.exec('npm run dev'), r.exec('stripe listen'), r.run(watchTask)], {
name: 'Development',
});
};Features:
- Each tab shows buffered output
- Keyboard navigation between tabs
- Scrollable output history
- Process lifecycle management
app
Run a fullscreen interactive app component.
app<TProps>(
component: AnyComponent<TProps>,
props: TProps
): Promise<void>Requires an app adapter (e.g., @pokit/opentui):
run: async (r) => {
await r.app(MyExplorer, {
data: await loadData(r.cwd),
onSave: async (id, fields) => { /* persist */ },
});
},The component is a standard React function component that:
- Owns its own state via hooks
- Handles keyboard input via
useKeyboard - Can use optional
onExit()to close and return control
See App Adapter for full details.
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' | 'tabs' | 'grid';
};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:
interface CommandReporter {
info(message: string): void;
warn(message: string): void;
error(message: string): void;
success(message: string): void;
step(message: string): void;
group<T>(label: string, options: GroupOptions, fn: (r: Reporter) => T): Promise<T>;
activity<T>(label: string, fn: () => T): Promise<T>;
suspend(): void; // Suspend output (for TUI takeover)
resume(): void; // Resume output
}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) => {
await r.tabs(
[
r.exec('npm run dev'),
r.exec('npm run watch:css'),
r.exec('stripe listen --forward-to localhost:3000/webhooks'),
],
{ name: 'Dev' }
);
},
});