johnieskills

Builds type-safe command-line tools with cleye.

Build type-safe TypeScript CLIs with the cleye argv parser. Use when the project already depends on cleye or the user names cleye — authoring a cli() call, adding typed flags/parameters, defining subcommands with command(), handling parsed output in a callback, using cleye/formats helpers (oneOf/commaList/integer/float/range/url), customizing --help via help.render, or wiring strictFlags/booleanFlagNegation. For Bloomberg's Stricli framework use the stricli skill instead; skip for commander/yargs/oclif/minimist and for generic "which CLI framework should I use?" questions.

$ npx skills add johnie/skills --skill cleye
skills/cleye/references/formats.mdGitHub

Flag Formats & Custom Types

A flag's type is any function that accepts a string and returns the parsed value; validation failures are thrown from inside it and cleye surfaces the message to the user.

Custom type functions

A custom type both validates and narrows the resulting type. Here a Size type narrows the flag to "small" | "medium" | "large":

const possibleSizes = ["small", "medium", "large"] as const;

type Sizes = (typeof possibleSizes)[number]; // "small" | "medium" | "large"

const Size = (size: Sizes) => {
  if (!possibleSizes.includes(size)) {
    throw new Error(`Invalid size: "${size}"`);
  }
  return size;
};

const argv = cli({
  flags: {
    size: {
      type: Size,
      description: "Size of the pizza (small, medium, large)",
    },
  },
});

// $ my-script --size large
argv.flags.size; // => "large"  ("small" | "medium" | "large")

Wrap a custom type in an array (type: [Size]) to accept multiple validated values.

cleye/formats helpers

cleye/formats is a tree-shakable subpath with ready-made type-function helpers for common flag shapes. Import only what you need.

import { oneOf, commaList, integer, float, range, url } from "cleye/formats";

cli({
  flags: {
    format: { type: oneOf("json", "yaml", "csv") }, // => 'json' | 'yaml' | 'csv'
    tags: { type: commaList(String) }, // => string[]
    port: { type: range(1024, 65_535) }, // => number, validated in range
    count: { type: integer() }, // => number (integer only)
    ratio: { type: float() }, // => number (finite float)
    apiUrl: { type: url() }, // => URL object
  },
});
Helper Return type Description
oneOf(...values) Union of the given string literals Throws if the value is not in the list.
commaList(itemType) T[] Splits on ,, trims whitespace, maps each item through itemType.
integer() number Parses a base-10 integer. Throws on floats or non-numeric input.
float() number Parses a finite float. Throws on non-finite or non-numeric input.
range(min, max) (input: string) => number Returns a parser validating the input is a number in [min, max].
url() URL Parses with new URL(); returns a URL so callers get .host, .pathname, etc.

When to use which

  • oneOf(...) — a fixed set of string literals. Simpler than a hand-written choice parser and narrows the type automatically.
  • commaList(itemType) — comma-delimited input in a single token (--tags a,b,c). Use a [Type] array flag instead when you want repeated flags (--tag a --tag b).
  • integer() / float() / range(...) — numeric input with stricter validation than the bare Number constructor.
  • url() — when you want a URL object rather than a raw string.
  • Custom function — anything not covered above, especially domain-specific validation or async-free transformation.

Defaults and parsing

A default is the already-typed value (not a raw string), so it should match the flag's parsed type:

flags: {
    format: { type: oneOf("json", "yaml"), default: "json" },
    port:   { type: integer(), default: 3000 }
}

For array flags, the default is an array of typed values:

flags: {
    tags: { type: [String], default: ["a", "b"] }
}