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

termcraft

v0.2.0

Published

A type-safe CLI framework for Bun with declarative argument parsing, subcommands, and lifecycle hooks.

Downloads

208

Readme

termcraft

CI License: MIT

Build type-safe CLIs with a single function call. Parsing, help, subcommands, and lifecycle hooks, all inferred from your definition.

Note: termcraft is in early development (v0.1.0). The API may change between minor versions until v1.0.0.

Quick Start

bun add termcraft
import { defineCommand, parseAsString, parseAsBoolean, runMain } from "termcraft";

const command = defineCommand({
  meta: { name: "greet", version: "1.0.0" },
  args: {
    name: parseAsString.positional(0).withDescription("Name to greet."),
    loud: parseAsBoolean.withDefault(false).withDescription("Print in uppercase."),
  },
  run: ({ args }) => {
    const line = `Hello ${args.name}!`;
    console.log(args.loud ? line.toUpperCase() : line);
  },
});

await runMain(command);
$ bun run greet.ts world --loud
HELLO WORLD!

$ bun run greet.ts --help
greet

USAGE
  greet <name> [options]

ARGUMENTS
  <name>  Name to greet.

OPTIONS
  --loud       Print in uppercase. (default: false)
  -h, --help   Show help

Why termcraft?

  • Bun-native Designed for Bun from the ground up, no Node baggage.
  • Fully type-safe Argument types are inferred from your definition. args.port is number, not any. .withDefault() and .withRequired() narrow the type from T | null to T.
  • Lifecycle hooks setup / run / cleanup / onError with guaranteed cleanup, even on SIGINT. No other lightweight CLI framework handles resource teardown this well.
  • Flag-or-prompt pattern Combine CLI flags with interactive prompts: args.name ?? await text({ message: "Name?" }). Ship CLIs that work both interactively and in scripts.
  • Batteries included Prompts, colors, and banners as sub-path imports. One bun add, no juggling packages.

Features

  • Declarative commands Define CLIs with defineCommand().
  • Type-safe parsers parseAsString, parseAsInteger, parseAsFloat, parseAsBoolean, parseAsEnum.
  • Fluent API .withAlias(), .withDefault(), .withRequired(), .withDescription(), .positional(), .variadic().
  • Subcommand routing Nested commands with lazy-loaded imports via () => import().
  • Auto-generated help --help / -h with aligned columns, usage lines, and examples.
  • Signal handling Graceful SIGINT (exit 130) / SIGTERM (exit 143) with cleanup.
  • Interactive prompts via termcraft/prompts (@clack/prompts).
  • Terminal colors via termcraft/colors (picocolors).
  • ASCII banners via termcraft/banner (figlet).
  • Pass-through args. Everything after -- is available as context.rest for forwarding to child processes.
  • Boolean negation. --no-verbose automatically negates --verbose for any boolean flag.
  • Repeated options. --file a.txt --file b.txt collects values into an array via .repeated().
  • Custom parsers from parse/serialize functions or any Zod-compatible schema.

Argument Parsers

Parsers start as optional (T | null). Chain methods to refine:

import { parseAsString, parseAsInteger, parseAsEnum } from "termcraft";

parseAsString.positional(0)                                    // string | null
parseAsString.positional(0).withRequired()                     // string (non-null)
parseAsInteger.withAlias("c").withDefault(1)                   // number (non-null)
parseAsString.withRequired().withDescription("Output file.")   // string (non-null)
parseAsEnum(["stable", "beta", "canary"] as const)             // "stable" | "beta" | "canary" | null
parseAsString.positional(0).variadic()                         // string[] (collects remaining)

Built-in Parsers

| Parser | Type | Description | |--------|------|-------------| | parseAsString | string | Pass-through string | | parseAsInteger | number | Whole numbers only | | parseAsFloat | number | Any valid number | | parseAsBoolean | boolean | true/yes/1 or false/no/0 | | parseAsEnum(values) | values[number] | One of the provided values |

Custom Parsers

Create parsers from functions or any object with safeParse (e.g., Zod):

import { createParser } from "termcraft";

// From parse/serialize functions
const parseAsDate = createParser({
  parse: (value: string) => {
    const date = new Date(value);
    return isNaN(date.getTime()) ? null : date;
  },
  serialize: (value: Date) => value.toISOString(),
});

// From a Zod schema (requires zod as a peer dependency)
import { z } from "zod";
const parseAsPort = createParser(z.coerce.number().int().min(1).max(65535));

Subcommands

Nest commands with the commands field. Supports deeply nested routing and lazy-loaded imports:

import { defineCommand, parseAsInteger, runMain } from "termcraft";

const command = defineCommand({
  meta: { name: "tasks", version: "1.0.0" },
  commands: {
    list: defineCommand({
      meta: { name: "list", description: "List tasks." },
      args: {
        limit: parseAsInteger.withDefault(10).withDescription("Max tasks."),
      },
      run: ({ args }) => console.log(`Listing ${args.limit} tasks`),
    }),
    // Lazy-loaded subcommand
    deploy: () => import("./commands/deploy"),
  },
});

await runMain(command);
bun run cli.ts list --limit 5
bun run cli.ts deploy

Lifecycle Hooks

Commands support setup, run, cleanup, and onError hooks. Cleanup is guaranteed to run even if setup or run throws:

const command = defineCommand({
  meta: { name: "release", version: "1.0.0" },
  args: {
    tag: parseAsString.withRequired().withDescription("Version tag."),
  },
  setup: async ({ args }) => {
    console.log(`Preparing release ${args.tag}`);
  },
  run: ({ args }) => {
    console.log(`Publishing ${args.tag}`);
  },
  cleanup: () => {
    console.log("Cleaning up temporary files");
  },
  onError: (error) => {
    console.error(`Release failed: ${error}`);
    process.exitCode = 1;
  },
});

Interactive Prompts

Use termcraft/prompts for interactive input. The flag-or-prompt pattern lets your CLI work both interactively and in CI:

import { defineCommand, parseAsString, runMain } from "termcraft";
import { text, select, intro, outro } from "termcraft/prompts";

const command = defineCommand({
  meta: { name: "init", version: "1.0.0" },
  args: {
    name: parseAsString.withDescription("Project name."),
  },
  run: async ({ args }) => {
    intro("Project Setup");

    // Use CLI flag if provided, prompt otherwise
    const name = args.name ?? await text({ message: "Project name?" });

    const template = await select({
      message: "Template?",
      options: [
        { value: "web", label: "Web App" },
        { value: "api", label: "API Server" },
      ],
    });

    outro(`Created ${name} with ${template} template.`);
  },
});

await runMain(command);

Colors and Banners

import { colors } from "termcraft/colors";
import { banner } from "termcraft/banner";

console.log(await banner("my-app"));
console.log(colors.green("Success!"));
console.log(colors.bold(colors.red("Error:")), "something went wrong");

Error Handling

termcraft exports error classes for programmatic error handling:

import { TermcraftError, MissingArgError, InvalidValueError } from "termcraft";
  • MissingArgError is thrown when a required argument or option is missing.
  • InvalidValueError is thrown when a value doesn't match the parser's expected type.
  • TermcraftError is the base class for all termcraft errors.

Signal handling is automatic: SIGINT exits with code 130, SIGTERM with 143. Cleanup hooks always run before exit.

Documentation

Full guides and API reference in docs/:

Examples

See examples/ for complete working CLIs:

  • greet.ts positional args, flags, aliases
  • init.ts interactive prompts with flag-or-prompt pattern
  • release.ts lifecycle hooks, banners, error handling
  • tasks.ts subcommands and nested commands
  • build.ts repeated options, --no-flag negation, -- pass-through
  • workspace.ts full app-like CLI with grouped subcommands
bun run examples/greet.ts world --loud

Development

bun install        # install dependencies
bun test           # run tests
bun run lint       # check for lint/format issues
bun run lint:fix   # auto-fix lint/format issues
bun run typecheck  # run TypeScript type checking
bun run check      # lint + typecheck
bun run build      # build dist/ for publishing

License

MIT