npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@charlie-labs/oclif-plugin-helpers

v0.3.0

Published

Shared oclif helpers for errors, logging, JSON support, etc.

Readme

@charlie-labs/oclif-plugin-helpers

npm version

Shared helpers to standardize error handling and output behavior across multiple oclif CLIs.

  • Shared error classes with stable static exit codes and string codes
  • Robust unknown → exit-code mapper (errorToExitCode)
  • A BaseCommand that centralizes --json gating, minimal error JSON shaping, stderr-only logging helpers, and a TSV printer
  • Optional handle() for bin/run catch chains to conform exit codes
  • Optional finally hook that conservatively sets process.exitCode when an error occurred

Installation

This package is publicly published on the npm registry as @charlie-labs/oclif-plugin-helpers and expects oclif v4.

# npm
npm install @charlie-labs/oclif-plugin-helpers @oclif/core@^4.5.2

# bun
bun add @charlie-labs/oclif-plugin-helpers @oclif/core@^4.5.2

Overview / Mission

A reusable place to make multiple CLIs behave the same way when errors happen and when users request JSON:

  • Error classes include a static exitCode and a string code for stability across boundaries.
  • errorToExitCode(err) maps any thrown value to a stable exit code using a resilient precedence order.
  • BaseCommand is the primary integration point: it enables --json for all commands, shapes error JSON, suppresses logs under --json, and provides stderr-only logging helpers plus a TSV printer.
  • handle() can be used in bin/run to conform exit codes for errors that bypass Command.catch().
  • An optional finally hook exists to set process.exitCode only when no other layer has set it.

Quick Start

Extend BaseCommand and implement execute(ctx) (do not override run()). Destructure only what you need from ctx{ parsed }, { deps }, or { parsed, deps }. If you don’t need the context at all, accept it as an unused parameter: execute(_ctx). When your command defines flags, build a manifest with defineFlags(...) and install it via static override flags = super.registerManifest(manifest);. Commands without flags can skip that line entirely; they fall back to an empty manifest. Use logInfo/logWarn for stderr-only logs and printRows for TSV content. Under --json, logs are suppressed and printRows is a no-op. The BaseCommand generic is kwargs‑style and order‑free: supply a union of tags — CfgFlags<typeof manifest> | Result<T> | Deps<D> — and specify only what you need.

import { BaseCommand } from '@charlie-labs/oclif-plugin-helpers';
import type { Result } from '@charlie-labs/oclif-plugin-helpers';

export default class Demo extends BaseCommand<Result<{ id: string }>> {
  // `--json` is enabled for all subclasses via BaseCommand
  protected async execute(_ctx) {
    this.logInfo('Fetching record…'); // stderr-only; suppressed under --json

    // pretend we looked something up
    const record = { id: 'rec_123' } as const;

    // TSV output (stdout). No-op under --json
    this.printRows([
      ['id', 'name'],
      ['123', 'example'],
    ]);

    return record;
  }
}

JSON mode and error JSON shape

All subclasses of BaseCommand automatically support oclif’s built-in --json flag. When --json is set:

  • Non-content logs are suppressed (logInfo/logWarn don’t write)
  • printRows() is a no-op
  • Errors are rendered as minimal, stable JSON using toErrorJson(err)

Exact error JSON shape:

{
  error: {
    type: string;
    message: string;
    exitCode: number;
    meta?: {
      code?: string;
      status?: number;
      retryable?: boolean;
    };
  };
}
  • type is derived from err.name (or constructor name)
  • message is concise and always present
  • exitCode is computed by errorToExitCode
  • meta includes optional hints when available: string error code, HTTP status, and retryable (based on common transport failures via isRetryableNetworkError)

Exit code mapping

errorToExitCode(err) resolves a stable process exit code using this order:

  1. err.exitCode (instance)
  2. err.constructor.exitCode (static)
  3. err.code (string)
  4. err.name
  5. instanceof checks
  6. default 1

Provided error classes and their static exit codes:

  • ValidationError → 2 (code: EVALIDATION)
  • NotFoundError → 3 (code: ERESOURCE_NOT_FOUND)
  • ConflictError → 4 (code: ECONFLICT)
  • UnauthorizedError → 5 (code: EUNAUTHORIZED)
  • RateLimitedError → 6 (code: ERATELIMIT)
  • ServiceUnavailableError → 7 (code: ESVCUNAVAILABLE)
  • CanceledError → 8 (code: ECANCELED)
  • ApiRequestError → 1 (code: EAPI)

Public API reference

All symbols below are exported from @charlie-labs/oclif-plugin-helpers (via src/index.ts).

Classes (errors)

  • ValidationError
  • NotFoundError
  • ConflictError
  • UnauthorizedError
  • RateLimitedError
  • ServiceUnavailableError
  • CanceledError
  • ApiRequestError

Functions

  • errorToExitCode(err: unknown): number
  • isRetryableNetworkError(err: unknown): boolean

FlagManifest (v1)

A tiny framework to define oclif flags alongside Zod schemas, compose them into a manifest, and parse the oclif flag bag into fully typed domain values with optional cross-flag validation.

Public surface (concise):

import { Flags, type Command } from '@oclif/core';
import { z } from 'zod';
import {
  defineFlags,
  type FlagSchema,
  type FlagManifest,
  zDateYYYYMMDD,
  zStringList,
  zPositiveInt,
  zOrderDir,
  zMultiEnum,
  CommonFlags,
} from '@charlie-labs/oclif-plugin-helpers';

// Build a manifest from inline flag schemas (preferred pattern)
const statusValues = ['started', 'completed', 'error'] as const;
export const manifest = defineFlags({
  status: {
    oclif: Flags.option({
      options: statusValues,
      multiple: true,
      delimiter: ',',
      description: 'Multi-select; repeats and/or comma-separated',
    })(),
    schema: zMultiEnum(statusValues),
  },
  start: {
    oclif: Flags.string({ description: 'Inclusive (YYYY-MM-DD, UTC)' }),
    schema: zDateYYYYMMDD.optional(),
  },
  end: {
    oclif: Flags.string({ description: 'Exclusive (YYYY-MM-DD, UTC)' }),
    schema: zDateYYYYMMDD.optional(),
  },
  limit: {
    oclif: Flags.integer({
      description: 'Positive integer (10000 max)',
      default: 100,
    }),
    schema: zPositiveInt({ default: 100, max: 10_000 }),
  },
})
  .withValidation((schema) =>
    schema.superRefine(({ start, end }, ctx) => {
      if (start && end && !(start < end))
        ctx.addIssue({
          code: z.ZodIssueCode.custom,
          message: 'end must be after start',
          path: ['end'],
        });
    })
  )
  .withPredicate(
    'start < end',
    ({ start, end }) => !start || !end || start < end,
    {
      path: ['end'],
      message: 'end must be after start',
    }
  );

// oclif command usage (preferred)
import { BaseCommand } from '@charlie-labs/oclif-plugin-helpers';
import type { CfgFlags, ExecCtxOf } from '@charlie-labs/oclif-plugin-helpers';

export class MyCmd extends BaseCommand<CfgFlags<typeof manifest>> {
  static override flags = super.registerManifest(manifest);

  protected override async execute({ parsed }: ExecCtxOf<this>) {
    // use typed flags
  }
}

Included atoms:

  • zDateYYYYMMDD: parse YYYY-MM-DD into Date at UTC midnight
  • zStringList: parse string | number | (string|number)[] | undefined (repeat/comma) → de-duplicated string[] (undefined normalizes to [])
  • zMultiEnum(values): normalize repeat/comma inputs and validate against provided enum values
  • zPositiveInt({ max?, default? })
  • zOrderDir ('asc'|'desc')
  • zDateComparator: parse a single comparator string like ">=2025-01-01" or "> 2025-01-01" to { op: 'gte'|'gt'|'lte'|'lt'|'eq', date: Date } (UTC midnight)
  • zDateComparatorList: alias for zStringList.pipe(z.array(zDateComparator)) — accepts repeat/comma inputs, returns DateComparator[], and normalizes undefined to []

Notes

  • Inline schemas are the primary path. For optional dates, use zDateYYYYMMDD.optional().
  • When using a default with zPositiveInt({ default: N, ... }), define the same default on the oclif flag to keep behavior consistent between oclif and Zod parsing.

Convenience: CommonFlags exports ready-made start, end, limit, order, and a sample status multi-enum.

Date comparator examples

import { Flags } from '@oclif/core';
import {
  defineFlags,
  zDateComparator,
  zDateComparatorList,
} from '@charlie-labs/oclif-plugin-helpers';

export const manifest = defineFlags({
  created: {
    oclif: Flags.string({
      description: '>, >=, <, <=, = followed by YYYY-MM-DD (UTC)',
    }),
    schema: zDateComparator.optional(),
  },
  updated: {
    oclif: Flags.string({
      multiple: true,
      delimiter: ',',
      description:
        '>, >=, <, <=, = followed by YYYY-MM-DD (UTC). Repeat or comma-separate',
    }),
    schema: zDateComparatorList,
  },
});

BaseCommand

  • class BaseCommand<Cfg> where Cfg is a union of order‑free tags you pick from:
    • CfgFlags<typeof manifest> — enables typed parsed flags in execute(...)
    • Result<T> — declares the return type of run()/execute() (defaults to unknown)
    • Deps<D> — declares the dependency object type used by DI helpers
  • static enableJsonFlag = true
  • static get manifest(): FlagManifest<Defs, ZodTypeAny> — returns the manifest registered on the concrete subclass (defaults to an empty manifest). Call static override flags = super.registerManifest(manifest); to install your manifest and expose its oclif flags to help output and parsing.
  • protected execute(ctx) — pick only what you need by destructuring:
    • execute({ parsed })
    • execute({ deps })
    • execute({ parsed, deps }) If you don’t need the context, accept it as execute(_ctx). When no CfgFlags<> tag is present, ctx.parsed is typed as Record<string, never>. When a Deps<> tag is present, ctx.deps is typed as D | undefined; without a Deps<> tag, ctx.deps is undefined.
  • Dependency injection (resolution order in run()): test override → static buildDeps(parsed)protected get deps()
    • static buildDeps(parsed): D | Promise<D | undefined> | undefined — override on your subclass to construct deps from flags. The override is fully typed to your Cfg via a polymorphic this parameter, so parsed is ParsedOf<typeof manifest> and the return type is your D. If you mark it async, the return type becomes Promise<D | undefined>.
    • protected get deps(): D | undefined — instance fallback when buildDeps isn’t used
      • Note: this getter is typed to your Deps<D> tag. If you previously returned a broader type, narrow it to D.
    • static setTestDeps(deps: D) — test-only override used first when present (one‑shot; cleared after use)
    • static clearTestDeps() — clears any previously set test override (useful in custom harnesses)
  • async run(): Promise<T> — provided by BaseCommand and final in practice (where T is the type you supplied via Result<T> in Cfg)
  • Helpers (no-op under --json):
    • protected logInfo(msg: string): void — stderr-only
    • protected logWarn(msg: string): void — stderr-only
    • protected printRows(rows: (string | string[])[], options?: { header?: string[] }): void — stdout TSV
  • Error shaping:
    • protected toErrorJson(err: unknown) — returns the JSON shape shown above (used automatically by oclif in JSON mode)

Cfg combinations (type-only examples)

Below are short, type-only class declarations that show common ways to configure BaseCommand<Cfg> using the CfgFlags<>, Result<>, and Deps<> tags. These intentionally omit method bodies and focus on the type surface.

import { BaseCommand } from '@charlie-labs/oclif-plugin-helpers';
import type {
  CfgFlags,
  Result,
  Deps,
  FlagManifest,
  Defs,
} from '@charlie-labs/oclif-plugin-helpers';
import type { ZodTypeAny } from 'zod';

// For these standalone examples, use a type-only stub for `manifest` with the correct generic shape.
declare const manifest: FlagManifest<Defs, ZodTypeAny>;

// 1) Deps-only (no flags; implicit output type = unknown)
export class DepsOnly extends BaseCommand<Deps<{ source: string }>> {}

// 2) Result + Deps (no flags)
export class ResultAndDeps extends BaseCommand<
  | Deps<{ client: { request: (s: string) => Promise<unknown> } }>
  | Result<number>
> {}

// 3) Flags + Result (no deps)
export class FlagsAndResult extends BaseCommand<
  CfgFlags<typeof manifest> | Result<string>
> {
  static override flags = super.registerManifest(manifest);
}

// 3a) Flags-only (no deps; implicit output type = unknown)
export class FlagsOnly extends BaseCommand<CfgFlags<typeof manifest>> {
  static override flags = super.registerManifest(manifest);
}

// 4) Flags + Deps + Result (all three)
export class FlagsDepsResult extends BaseCommand<
  CfgFlags<typeof manifest> | Deps<{ db: unknown }> | Result<void>
> {
  static override flags = super.registerManifest(manifest);
}

Notes

  • When you omit Result<T>, the command’s output type defaults to unknown.
  • When you omit CfgFlags<>, ctx.parsed in execute(ctx) is Record<string, never>.
  • When you omit Deps<>, ctx.deps is undefined.

Other

  • handle(err: unknown): Promise<void> — drop-in for bin/run catch chains

Optional integration points

handle() in a bin script

Use handle() to ensure exit codes conform to the mapper even when errors bypass Command.catch():

// bin/run (snippet)
import { handle } from '@charlie-labs/oclif-plugin-helpers';
import { run } from '@oclif/core';

await run(void 0, import.meta.url).catch(handle);

Finally hook

This package includes an optional finally hook at src/hooks/finally/report.ts that only sets process.exitCode if an error occurred and the exit code wasn’t set yet. It does not print anything. This is a conservative last resort layer.

Examples

1) Command with JSON gating, stderr logs, and TSV output

import { BaseCommand } from '@charlie-labs/oclif-plugin-helpers';
import type { Result } from '@charlie-labs/oclif-plugin-helpers';

export default class ListProjects extends BaseCommand<Result<void>> {
  protected async execute(_ctx) {
    this.logInfo('Listing projects');
    this.printRows(
      [
        ['p_123', 'Acme'],
        ['p_456', 'Beta'],
      ],
      { header: ['id', 'name'] }
    );
  }
}

2) Throwing a provided error → mapped exit code

import {
  BaseCommand,
  ValidationError,
} from '@charlie-labs/oclif-plugin-helpers';
import type { CfgFlags, Result } from '@charlie-labs/oclif-plugin-helpers';
import { defineFlags } from '@charlie-labs/oclif-plugin-helpers/flags';

const noFlags = defineFlags({} as const);

export default class Create extends BaseCommand<
  CfgFlags<typeof noFlags> | Result<void>
> {
  protected async execute(_ctx) {
    const name = '';
    if (!name) throw new ValidationError('`name` is required'); // exit 2
  }
}

3) Mapping an unknown error yourself

import { errorToExitCode } from '@charlie-labs/oclif-plugin-helpers';

try {
  // … your code …
} catch (err) {
  process.exitCode = errorToExitCode(err);
}

4) bin/run catch chain

import { handle } from '@charlie-labs/oclif-plugin-helpers';
import { run } from '@oclif/core';

await run(void 0, import.meta.url).catch(handle);

Compatibility and requirements

  • Peer: @oclif/core@^4.5.2
  • Node: >=18

Repository meta

  • Package: @charlie-labs/oclif-plugin-helpers
  • Published: public on npm (publishConfig.access: "public")
  • License: UNLICENSED
  • Spec: Issue #1 — “Initial Spec”