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 renderer

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

ModeBehavior
raceFirst to settle wins, cancel rest (default)
fail-fastFirst failure cancels rest, otherwise wait for all
all-settledRun 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 fails

Environment 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'),
    ]);
  },
});