johnieskills

Builds type-safe command-line tools with Stricli.

Build type-safe TypeScript CLIs with Bloomberg's Stricli framework. Use when the project already depends on @stricli/core or the user names Stricli — authoring a new CLI, changing typed flags/positionals/parsers, wiring subcommand routing via buildRouteMap, testing via run(app, argv, context), or configuring bash auto-complete via @stricli/auto-complete. For the cleye argv parser use the cleye skill instead; skip for commander/yargs/oclif/minimist and for generic "which CLI framework should I use?" questions.

$ npx skills add johnie/skills --skill stricli
skills/stricli/references/parsers.mdGitHub

Parsers

Parsers transform raw CLI strings into typed values for flags and positional arguments.

Current Parser Model

Stricli's current public parser type is effectively:

type InputParser<T, CONTEXT> = (this: CONTEXT, input: string) => T | Promise<T>;

Important details:

  • parsers can be synchronous or asynchronous
  • parsers should throw or reject on invalid input
  • the parser receives command context through this

Do not model parser failures by returning Error values — Stricli expects parsers to throw on invalid input, not return errors.

Built-In Parsers

String

Use String when no transformation is needed.

flags: {
    host: {
        kind: "parsed",
        parse: String,
        brief: "Hostname",
        optional: true
    }
}

numberParser

Prefer numberParser over Number for numeric input.

import { numberParser } from "@stricli/core";

flags: {
    port: {
        kind: "parsed",
        parse: numberParser,
        brief: "Port number"
    }
}

booleanParser

Use strict boolean parsing for explicit true / false style inputs.

import { booleanParser } from "@stricli/core";

flags: {
    enabled: {
        kind: "parsed",
        parse: booleanParser,
        brief: "Explicit boolean input",
        optional: true
    }
}

looseBooleanParser

Use looseBooleanParser when inputs like yes, no, 1, or 0 should work.

import { looseBooleanParser } from "@stricli/core";

flags: {
    enabled: {
        kind: "parsed",
        parse: looseBooleanParser,
        brief: "Flexible boolean input",
        optional: true
    }
}

buildChoiceParser

Use buildChoiceParser() when you want parsed-flag behavior but only from a known set of values.

import { buildChoiceParser } from "@stricli/core";

const logLevelParser = buildChoiceParser([
    "debug",
    "info",
    "warn",
    "error"
] as const);

flags: {
    level: {
        kind: "parsed",
        parse: logLevelParser,
        brief: "Log level"
    }
}

If the type is naturally a string literal union, kind: "enum" is often simpler.

Custom Parsers

Synchronous Parser

const portParser = (input: string): number => {
  const port = Number.parseInt(input, 10);
  if (Number.isNaN(port) || port < 1 || port > 65535) {
    throw new Error(`Invalid port: ${input}`);
  }
  return port;
};

Parser with Context Access

Parsers can use this to access injected services.

import type { CommandContext, InputParser } from "@stricli/core";

interface AppContext extends CommandContext {
  readonly aliases: Map<string, string>;
}

const aliasParser: InputParser<string, AppContext> = function (input) {
  return this.aliases.get(input) ?? input;
};

Async Parser

Async parsers are supported.

import type { CommandContext, InputParser } from "@stricli/core";

interface AppContext extends CommandContext {
  readonly registry: {
    hasProject(name: string): Promise<boolean>;
  };
}

const projectParser: InputParser<string, AppContext> = async function (input) {
  if (!(await this.registry.hasProject(input))) {
    throw new Error(`Unknown project: ${input}`);
  }
  return input;
};

URL Parser

const urlParser = (input: string): URL => {
  const url = new URL(input);
  if (!["http:", "https:"].includes(url.protocol)) {
    throw new Error("URL must use http or https");
  }
  return url;
};

JSON Parser

The as T cast is a convenience — it does not validate the shape at runtime. Add runtime validation (e.g., with Zod or a type guard) if the parsed structure matters.

const jsonParser = <T = unknown>(input: string): T => {
  try {
    return JSON.parse(input) as T;
  } catch {
    throw new Error(`Invalid JSON: ${input}`);
  }
};

When to Parse vs Validate in the Command

Use parsers for:

  • converting one input string to one typed value
  • synchronous validation of a single input
  • async validation of a single input when context makes it practical

Use command logic for:

  • cross-argument validation
  • validations that depend on multiple flags or positional args together
  • workflows where parsing and execution should remain separate for clarity

Default Values and Parsers

For parsed flags, Stricli parses default strings through the same parser.

flags: {
    timeout: {
        kind: "parsed",
        parse: numberParser,
        brief: "Timeout in seconds",
        optional: true,
        default: "30"
    }
}

For variadic parsed flags, defaults are arrays of raw strings.

flags: {
    include: {
        kind: "parsed",
        parse: String,
        brief: "Paths to include",
        optional: true,
        variadic: true,
        default: ["src", "test"]
    }
}

Best Practices

  • prefer numberParser over Number
  • keep parser error messages short and actionable
  • use enum instead of a parsed choice parser when the type is just a string union
  • use context-aware parsers sparingly and only when the dependency really belongs at parse time
  • keep cross-argument rules in the command implementation, not inside a single parser