@reliverse/rempts

1.7.38 • Public • Published

📃 rempts • powerful js/ts cli builder

@reliverse/rempts is a modern, type-safe toolkit for building delightful cli experiences. it's fast, flexible, and made for developer happiness. file-based commands keep things simple—no clutter, just clean and easy workflows. this is how cli should feel.

sponsordiscordreponpm

Features

  • 😘 drop-in to libraries like unjs/citty and @clack/prompts
  • 📝 includes comprehensive set of built-in cli prompts
  • 📂 file-based commands (app-router style by default)
  • 🫂 rempts keeps you from fighting with your CLI tool
  • 🏎️ prompt engine that feels modern — and actually is
  • ✨ rempts is your end-to-end CLI UI + command framework
  • 🌿 multi-level file-based subcommands (sibling + nested)
  • 💪 built for DX precision and high-context terminal UX
  • 🎭 looks great in plain scripts or full CLI apps
  • 🎨 customizable themes and styled output
  • 📦 built-in output formatter and logger
  • 🚨 crash-safe (Ctrl+C, SIGINT, errors)
  • ⚡ blazing-fast, zero runtime baggage
  • 🧩 router + argument parser built-in
  • 🧠 type-safe from args to prompts
  • 📐 smart layout for small terminals
  • 🎛️ override styles via prompt options
  • 🪄 minimal API surface, maximum expressiveness
  • 🧪 scriptable for testing, stable for production
  • 🏞️ no more hacking together inquirer/citty/commander/chalk
  • 🆕 automatic command creation (bun dler rempts --init cmd1 cmd2)
  • 🐦‍🔥 automatic creation of src/app/cmds.ts file (bun dler rempts)
  • 🔌 tRPC/ORPC router integration - automatically generate CLI commands from your RPC procedures

Installation

bun add @reliverse/rempts

Coming soon:

bun add -D @reliverse/dler
bun dler rempts --init cmd1 cmd2 # creates `src/app/cmd1/cmd.ts` and `src/app/cmd2/cmd.ts` files
bun dler rempts # creates `src/app/cmds.ts` file

Usage Examples

Screenshot

Rempts Example CLI Screenshot

API Overview

All main prompts APIs are available from the package root:

import {
  // ...prompts
  inputPrompt, selectPrompt, multiselectPrompt, numberPrompt,
  confirmPrompt, togglePrompt, taskSpinPrompt, taskProgressPrompt,
  startPrompt, endPrompt, resultPrompt, nextStepsPrompt,
  // ...hooks
  useSpinner,
  // ...launcher
  createCli, defineCommand, defineArgs,
  // ...types
  // ...more
} from "@reliverse/rempts";

See src/mod.ts for the full list of exports.

Prompts

Built-in Prompts

Prompt Description
useSpinner Start/stop spinner
inputPrompt Single-line input (with mask support, e.g. for passwords)
selectPrompt Single-choice radio menu
multiselectPrompt Multi-choice checkbox menu
numberPrompt Type-safe number input
confirmPrompt Yes/No toggle
togglePrompt Custom on/off toggles
taskProgressPrompt Progress bar for async tasks
resultPrompt Show results in a styled box
nextStepsPrompt Show next steps in a styled list
startPrompt/endPrompt Makes CLI start/end flows look nice
taskSpinPrompt Async loader with spinner (possibly will be deprecated)
datePrompt Date input with format validation
anykeyPrompt Wait for any keypress

Aliases

To help you migrate from the different CLI frameworks, @reliverse/rempts has some aliases for the most popular prompts.

Prompt Aliases
createCli runMain
onCmdInit setup
onCmdExit cleanup
useSpinner spinner
selectPrompt select
multiselectPrompt multiselect
inputPrompt text, input
confirmPrompt confirm
introPrompt intro, start
outroPrompt outro, end
log relinka

Prompts Usage Example

import { relinka } from "@reliverse/relinka";

import {
  startPrompt,
  inputPrompt,
  selectPrompt,
  defineCommand,
  runMain
} from "@reliverse/rempts";

async function main() {
  await startPrompt({ title: "Project Setup" });

  const name = await inputPrompt({
    title: "What's your project name?",
    defaultValue: "my-cool-project",
  });

  const spinner = useSpinner({
    text: "Loading...",
    indicator: "timer", // or "dots"
    frames: ["◒", "◐", "◓", "◑"], // custom frames
    delay: 80, // custom delay
    onCancel: () => {
      console.log("Operation cancelled");
    },
    cancelMessage: "Operation cancelled by user",
    errorMessage: "Operation failed",
    signal: abortController.signal,
  }).start();

  // The spinner will show:
  // ◒  Loading... [5s]
  // With animated frames and timer

  const framework = await selectPrompt({
    title: "Pick your framework",
    options: [
      { value: "next", label: "Next.js" },
      { value: "svelte", label: "SvelteKit" },
      { value: "start", label: "TanStack Start" },
    ],
    defaultValue: "next",
  });

  console.log("Your result:", { name, framework });
};

await main();

Available spinner options:

Option Description
cancelMessage The message to display when the spinner is cancelled
color The color of the spinner
delay The delay between frames
errorMessage The message to display when the spinner fails
failText The text to display when the spinner fails
frames The frames to use for the spinner
hideCursor Whether to hide the cursor
indicator The indicator to use for the spinner
onCancel The function to call when the spinner is cancelled
prefixText The text to display before the spinner
signal The signal to use for the spinner
silent Whether to hide the spinner
spinner The spinner to use for the spinner
successText The text to display when the spinner succeeds
text The text to display next to the spinner

Available indicator options:

Option Description
timer The timer indicator
dots The dots indicator

Available signal options:

Option Description
abortController.signal The signal to use for the spinner

Available frames options:

Option Description
["◒", "◐", "◓", "◑"] The frames to use for the spinner

Available delay options:

Option Description
80 The delay between frames

Available onCancel options:

Option Description
() => { console.log("Operation cancelled"); } The function to call when the spinner is cancelled

Launcher

Note: runMain is now an alias for createCli and is still supported for backward compatibility. The new createCli API provides a more intuitive object-based configuration format.

Terminology

  • Launcher/Router: The main entry point for your CLI. Visit CLI Launcher (Router) section to learn more.
  • Command: A command is a function that defines the inner script launched by the main script where runMain() is used or by some other command.
  • Argument: An argument is a value that is passed to a command.
  • Flag: A flag is a boolean argument that is used to enable or disable a feature.
  • Option: An option is a named argument that is used to configure a command.

Launcher Usage Example

Important: Ensure your commands don't have await main();, await createCli();, or something like that — to prevent any unexpected behavior. Only main command should have it.

import { relinka } from "@reliverse/relinka";

import { defineCommand, createCli } from "@reliverse/rempts";

const main = defineCommand({
  meta: {
    name: "rempts",
    version: "1.0.0",
    description: "Rempts Launcher Playground CLI",
  },
  onCmdInit() {
    relinka("success", "Setup");
  },
  onCmdExit() {
    relinka("success", "Cleanup");
  },
  commands: {
    build: () => import("./app/build/cmd.js").then((r) => r.default),
    deploy: () => import("./app/deploy/cmd.js").then((r) => r.default),
    debug: () => import("./app/debug/cmd.js").then((r) => r.default),
  },
});

// New object format (recommended)
await createCli({
  mainCommand: main,
  fileBased: {
    enable: true,
    cmdsRootPath: "my-cmds", // default is `./app`
  },
  // Optionally disable auto-exit to handle errors manually:
  autoExit: false,
});

// Legacy format (still supported)
await createCli(main, {
  fileBased: {
    enable: true,
    cmdsRootPath: "my-cmds", // default is `./app`
  },
  // Optionally disable auto-exit to handle errors manually:
  autoExit: false,
});

This flexibility allows you to easily build a rich, multi-command CLI with minimal boilerplate. The launcher even supports nested commands, making it simple to construct complex CLI applications.

File-Based Commands

Drop a ./src/cli/app/add/index.ts and it's live.

import { defineArgs, defineCommand } from "@reliverse/rempts";
export default defineCommand({
  meta: {
    name: "add",
    version: "1.0.0",
    description: "Add stuff to your project",
  },
  args: {
    name: defineArgs({ // 💡 PRO TIP: use defineArgs() to get fully correct intellisense
      type: "string",
      required: true,
      description: "Name of what to add",
    }),
  },
  async run({ args }) {
    relinka("log", "Adding:", args.name);
  },
});

Supports:

  • arg-cmdName.{ts,js},
  • cmdName/index.{ts,js},
  • cmdName/cmdName-mod.{ts,js},
  • Multi-level subcommands: foo/bar/baz/cmd.tsmy-cli foo bar baz
  • And more — with automatic usage output.

Hint:

  • Install bun add -D @reliverse/dler
  • Use bun dler rempts --init cmd1 cmd2 to init commands for rempts launcher's automatically

Advanced Launcher Usage

defineCommand({
  meta: { name: "cli", version: "1.0.0" },
  args: {
    name: { type: "string", required: true },
    verbose: { type: "boolean", default: false },
    animals: { type: "array", default: ["cat","dog"] },
  },
  async run({ args, raw }) { // or `async run(ctx)`
    relinka("log", args.name, args.verbose, args.animals); // or `relinka("log", ctx.args.name, ...);`
  },
});

Supports:

  • positional args
  • array types (--tag foo --tag bar)
  • Default values, validations, descriptions
  • Full help rendering from metadata

By the way! Multi-level subcommands!

You can also nest subcommands arbitrarily deep:

app/
  foo/
    bar/
      baz/
        cmd.ts

Invoke with:

my-cli foo bar baz --some-flag

The launcher will recursively traverse subfolders for each non-flag argument, loading the deepest cmd.ts/cmd.js it finds, and passing the remaining arguments to it.

See example/launcher/app/nested and example/launcher/app/sibling folders to learn more.

When playing with the example, you can run e.g. bun dev:modern nested foo bar baz to see the result in action.

RPC Integration

Rempts now supports seamless integration with tRPC and ORPC routers, allowing you to automatically generate CLI commands from your RPC procedures. This provides a powerful way to expose your API endpoints as command-line tools.

import { z } from "zod";
import { initTRPC } from "@trpc/server";
import { createCli } from "@reliverse/rempts";

const t = initTRPC.create();

const appRouter = t.router({
  hello: t.procedure
    .input(z.object({ name: z.string().optional() }))
    .query(({ input }) => `Hello ${input.name ?? "World"}!`),
  
  add: t.procedure
    .input(z.object({ a: z.number(), b: z.number() }))
    .mutation(({ input }) => input.a + input.b)
});

// Automatically generates CLI commands from your tRPC procedures
await createCli({
  name: "my-cli",
  rpc: { router: appRouter }
});

Features:

  • 🚀 Automatic CLI generation from tRPC procedures
  • 🔄 Support for both tRPC v10 and v11
  • 🏗️ Nested command structures from sub-routers
  • ✅ Input validation from Zod schemas
  • 📖 Automatic help generation from procedure metadata
  • 🎯 Full TypeScript support with type inference
  • 🎨 Interactive prompts for missing arguments
  • ⌨️ Shell completion support
  • 🔧 Customizable logging and error handling

See RPC Integration Guide for detailed documentation and examples.

Playground

git clone https://github.com/reliverse/rempts
cd rempts
bun i
bun dev
  • bun dev:prompts: This example will show you a multiselectPrompt() where you can choose which CLI prompts you want to play with.
  • bun dev:modern: This example will show you a modern CLI launcher usage with file-based commands.
  • bun dev:classic: This example will show you a classic CLI launcher usage with programmatic commands.

tRPC/oRPC Integration Example Commands

bun example/trpc-orpc/rempts/effect-primary.ts create-profile --name 'Jane Smith' --age 28 --bio 'Software Engineer' --tags 'developer,typescript'

Launcher Usage Examples

Minimal Usage Example

1 Create a src/mod.ts file:

import { createCli, defineCommand } from "@reliverse/rempts";

// New object format (recommended)
await createCli({
  mainCommand: defineCommand({}),
});

// Legacy format (still supported)
await createCli(defineCommand({}));

2 Run the following:

bun add -D @reliverse/dler
bun dler rempts --init my-cmd-1 # or: dler rempts --init my-cmd-1 my-cmd-2 --main src/mod.ts
# * `--main` is optional, default is `./src/mod.ts`
# * you can specify multiple commands at once

3 Visit src/app/my-cmd-1/mod.ts and edit it:

export default defineCommand({
  run() { console.log("Hello, world!"); },
});

4. Test it:

bun src/mod.ts

Medium Usage Example

import { defineCommand, createCli } from "@reliverse/rempts";

const main = defineCommand({
  meta: {
    name: "mycli",
  },
  run() {
    console.log("Happy, Reliversing!");
  },
});

// New object format (recommended)
await createCli({
  mainCommand: main,
});

// Legacy format (still supported)
await createCli(main);

Classic Usage Example

import { relinka } from "@reliverse/relinka";

import {
  startPrompt,
  inputPrompt,
  selectPrompt,
  defineCommand,
  createCli
} from "@reliverse/rempts";

const main = defineCommand({
  meta: {
    name: "mycli",
    version: "1.0.0",
    description: "CLI powered by Rempts",
  },
  args: {
    name: {
      type: "string",
      required: true,
      description: "The name of the project",
    },
  },
  async run({ args }) {
    await startPrompt({
      title: "Project Setup",
    });

    const name = await inputPrompt({
      title: "What's your project name?",
      placeholder: args.name,
    });

    const framework = await selectPrompt({
      title: "Pick your framework",
      options: [
        { value: "next", label: "Next.js" },
        { value: "svelte", label: "SvelteKit" },
        { value: "start", label: "TanStack Start" },
      ],
    });

    relinka("log", "You have selected:", { name, framework });
  },
});

// New object format (recommended)
await createCli({
  mainCommand: main,
});

// Legacy format (still supported)
await createCli(main);

Advanced Usage Example

import { relinka } from "@reliverse/relinka";

import {
  startPrompt,
  inputPrompt,
  selectPrompt,
  defineCommand,
  runMain,
} from "@reliverse/rempts";

/**
 * Main command defined using `defineCommand()`.
 *
 * This command demonstrates the full range of launcher features along with all supported argument types:
 *
 * - Global Usage Handling: Automatically processes `--help` and `--version`.
 * - File-Based Commands: Scans "app" for commands (e.g., `init`).
 * - Comprehensive Argument Parsing: Supports positional, boolean, string, number, and array arguments.
 * - Interactive Prompts: Uses built-in prompt functions for an engaging CLI experience.
 */
const mainCommand = defineCommand({
  meta: {
    name: "rempts",
    version: "1.6.0",
    description:
      "An example CLI that supports file-based commands and all argument types.",
  },
  args: {
    // Positional arguments
    inputFile: {
      type: "positional",
      description: "Path to the input file (only for the main command).",
    },
    config: {
      type: "positional",
      description: "Path to the configuration file.",
    },
    // Boolean arguments
    verbose: {
      type: "boolean",
      default: false,
      description: "Whether to print verbose logs in the main command.",
    },
    debug: {
      type: "boolean",
      default: false,
      description: "Enable debug mode for additional logging.",
    },
    // String argument
    name: {
      type: "string",
      description: "The name of the project.",
    },
    // Number argument
    timeout: {
      type: "number",
      default: 30,
      description: "Timeout in seconds for the CLI operation.",
    },
    // Array argument
    tags: {
      type: "array",
      default: ["cli", "rempts"],
      description: "List of tags associated with the project.",
    },
  },
  async run({ args, raw }) {
    // Display invocation details and parsed arguments.
    relinka("log", "Main command was invoked!");
    relinka("log", "Parsed main-command args:", args);
    relinka("log", "Raw argv:", raw);
    relinka("log", "\nHelp: `rempts --help`, `rempts cmdName --help`");

    // Begin interactive session with a prompt.
    await startPrompt({
      title: "Project Setup",
    });

    // Ask for the project name, falling back to provided argument or a default.
    const projectName = await inputPrompt({
      title: "What's your project name?",
      placeholder: args.name ?? "my-cool-cli",
    });

    // Let the user pick a framework from a select prompt.
    const framework = await selectPrompt({
      title: "Pick your framework",
      options: [
        { value: "next", label: "Next.js" },
        { value: "svelte", label: "SvelteKit" },
        { value: "start", label: "TanStack Start" },
      ],
    });

    // Log all gathered input details.
    relinka("log", "You have selected:", {
      projectName,
      framework,
      inputFile: args.inputFile,
      config: args.config,
      verbose: args.verbose,
      debug: args.debug,
      timeout: args.timeout,
      tags: args.tags,
    });
  },
});

/**
 * The `createCli()` function sets up the launcher with several advanced features:
 *
 * - File-Based Commands: Enables scanning for commands within the "app" directory.
 * - Alias Mapping: Shorthand flags (e.g., `-v`) are mapped to their full names (e.g., `--verbose`).
 * - Strict Mode & Unknown Flag Warnings: Unknown flags are either warned about or handled via a callback.
 * - Negated Boolean Support: Allows flags to be negated (e.g., `--no-verbose`).
 * - Custom Unknown Flag Handler: Provides custom handling for unrecognized flags.
 */
// New object format (recommended)
await createCli({
  mainCommand: mainCommand,
  fileBased: {
    enable: true, // Enables file-based command detection.
    cmdsRootPath: "app", // Directory to scan for commands.
  },
  alias: {
    v: "verbose", // Maps shorthand flag -v to --verbose.
  },
  strict: false, // Do not throw errors for unknown flags.
  warnOnUnknown: false, // Warn when encountering unknown flags.
  negatedBoolean: true, // Support for negated booleans (e.g., --no-verbose).
  // unknown: (flagName) => {
  //   relinka("warn", "Unknown flag encountered:", flagName);
  //   return false;
  // },
});

// Legacy format (still supported)
await createCli(mainCommand, {
  fileBased: {
    enable: true, // Enables file-based command detection.
    cmdsRootPath: "app", // Directory to scan for commands.
  },
  alias: {
    v: "verbose", // Maps shorthand flag -v to --verbose.
  },
  strict: false, // Do not throw errors for unknown flags.
  warnOnUnknown: false, // Warn when encountering unknown flags.
  negatedBoolean: true, // Support for negated booleans (e.g., --no-verbose).
  // unknown: (flagName) => {
  //   relinka("warn", "Unknown flag encountered:", flagName);
  //   return false;
  // },
});

CLI Launcher (Router)

Finally, a full-featured CLI launcher without the ceremony. @reliverse/rempts's so called "launcher" is a uniquely powerful and ergonomic CLI toolkit—one that helps you build delightful developer experiences with less code and more confidence. The launcher supports both programmatically defined commands and file-based routing, so you can structure your CLI however you like. It automatically detects and loads commands from your filesystem and provides robust usage and error handling out-of-the-box. The launcher is more than just a command runner—it's a robust, developer-friendly engine with several advanced features and thoughtful design choices:

  • File-Based & Defined Commands:
    Use commands in your command definition or let the launcher automatically load commands from a specified directory.

  • Automatic Command Detection:
    The launcher scans your specified cmdsRootPath for command files matching common patterns such as:

    • arg-cmdName.{ts,js}
    • cmdName/index.{ts,js}
    • cmdName/cmdName-mod.{ts,js}
    • And more — with automatic usage output if a command file is not found.
  • Built-In Flag Handling:
    Automatically processes global flags such as:

    • --help and -h to show usage details.
    • --version and -v to display version information.
    • --debug for verbose logging during development.
  • Unified Argument Parsing:
    Seamlessly combines positional and named arguments with zero configuration, auto-parsing booleans, strings, numbers, arrays, and even supporting negated flags like --no-flag.

  • Customizable Behavior:
    Options such as fileBased.enable, cmdsRootPath, and autoExit allow you to tailor the launcher's behavior. For example, you can choose whether the process should exit automatically on error or allow manual error handling.

  • Error Management & Usage Output:
    The launcher provides clear error messages for missing required arguments, invalid types, or command import issues, and it automatically displays usage information for your CLI.

  • Lifecycle Hooks: You can define optional lifecycle hooks in your main command:

    • onLauncherInit and onLauncherExit (global, called once per CLI process)
    • onCmdInit and onCmdExit (per-command, called before/after each command, but NOT for the main run() handler)

    Global Hooks:

    • onLauncherInit: Called once, before any command/run() is executed.
    • onLauncherExit: Called once, after all command/run() logic is finished (even if an error occurs).

    Per-Command Hooks:

    • onCmdInit: Called before each command (not for main run()).
    • onCmdExit: Called after each command (not for main run()).

    This means:

    • If your CLI has multiple commands, onCmdInit and onCmdExit will be called for each command invocation, not just once for the whole CLI process.
    • If your main command has a run() handler (and no command is invoked), these hooks are not called; use the run() handler itself or the global hooks for such logic.
    • This allows you to perform setup/teardown logic specific to each command execution.
    • If you want logic to run only once for the entire CLI process, use onLauncherInit and onLauncherExit.

    Example:

    const main = defineCommand({
      onLauncherInit() { relinka('info', 'Global setup (once per process)'); },
      onLauncherExit() { relinka('info', 'Global cleanup (once per process)'); },
      onCmdInit() { relinka('info', 'Setup for each command'); },
      onCmdExit() { relinka('info', 'Cleanup for each command'); },
      commands: { ... },
      run() { relinka('info', 'Main run handler (no command)'); },
    });
    // onLauncherInit/onLauncherExit are called once per process
    // onCmdInit/onCmdExit are called for every command (not for main run())
    // If you want per-run() logic, use the run() handler or global hooks
  • Deprecation Notice

    • The legacy setup and cleanup names are still supported as aliases for per-command hooks, but will be removed in a future major version. Prefer onCmdInit and onCmdExit going forward.
    • The subCommands property is deprecated as well. Please use commands instead. subCommands will be removed in a future major version.
  • Dynamic Usage Examples:

    • The launcher inspects your available commands and their argument definitions, then prints a plausible example CLI invocation for a random command directly in the help output. This helps users understand real-world usage at a glance.
  • File-Based & Programmatic Commands:

    • Both file-based and object commands are fully supported. The launcher can introspect their argument definitions and metadata for help, usage, and validation.
    • File-based commands are auto-discovered from your filesystem, while programmatic commands can be defined inline in your main command.
  • Context-Aware Help Output:

    • The help/usage output adapts to your CLI's structure, showing available commands, their aliases, argument details, and even dynamic usage examples. It also displays global options and context-specific error messages.
  • Error Handling:

    • The launcher provides clear, actionable error messages for missing required arguments, invalid types, unknown commands, and import errors. It always shows relevant usage information to help users recover quickly.
  • Unified Argument Parsing:

    • All arguments (positional, named, boolean, string, number, array) are parsed and validated automatically. Negated flags (like --no-flag) are supported out of the box.
  • Extensible & Flexible:

    • The launcher is highly extensible. You can use it with both Bun and Node.js, and it works seamlessly with both file-based and programmatic command definitions. You can also customize its behavior with options like autoExit, cmdsRootPath, and more.
  • Bun & Node.js Support:

    • The launcher is designed to work in both Bun and Node.js environments, so you can use it in any modern JavaScript/TypeScript project.
  • Prompt-First, Modern UX:

    • The launcher integrates tightly with the prompt engine, so you can build interactive, delightful CLIs with minimal effort.

Launcher Programmatic Execution

For larger CLIs or when you want to programmatically run commands (e.g.: prompt demo, tests, etc), you can organize your commands in a cmds.ts file and use the runCmd utility. Example:

// example/launcher/app/runcmd/cmd.ts

import { relinka } from "@reliverse/relinka";
import { defineArgs, defineCommand, runCmd } from "@reliverse/rempts";
import { cmdMinimal } from "../cmds.js";

export default defineCommand({
  meta: {
    name: "runcmd",
    description:
      "Demonstrate how to use runCmd() to invoke another command programmatically.",
  },
  args: defineArgs({
    name: {
      type: "string",
      description: "your name",
    },
  }),
  async run({ args }) {
    // const username = args.name ?? "Alice";
    const username = args.name; // intentionally missing fallback
    relinka(
      "info",
      `Running the 'minimal' command using runCmd() with name='${username}'`,
    );
    await runCmd(await cmdMinimal(), ["--name", username]);
    relinka("log", "Done running 'minimal' via runCmd().");
  },
});

Loading Commands with loadCommand

The loadCommand utility helps you load command files from your filesystem. It automatically handles:

  • Relative paths (both ./build and build work the same)
  • Automatic detection of cmd.{ts,js} files
  • Clear error messages when files are not found
import { loadCommand } from "@reliverse/rempts";

// These are equivalent:
const cmd1 = await loadCommand("./build");     // Looks for build/cmd.ts or build/cmd.js
const cmd2 = await loadCommand("build");       // Same as above
const cmd3 = await loadCommand("./build/cmd"); // Explicit path to cmd file

// You can then use the loaded command with runCmd:
await runCmd(cmd1, ["--some-flag"]);
// src/app/cmds.ts
export const getBuildCmd = async (): Promise<Command> => loadCommand("./build");

// src/cli.ts
import { runCmd } from "@reliverse/rempts";
import { getBuildCmd } from "./app/cmds";
await runCmd(await getBuildCmd(), ["--prod"]);

Error Handling: If the command file is not found, you'll get a clear error message:

No command file found in /path/to/build. Expected to find either:
  - /path/to/build/cmd.ts
  - /path/to/build/cmd.js
Please ensure one of these files exists and exports a default command.

Best Practices:

  • Use loadCommand when you need to load commands from the filesystem
  • Use runCmd to execute the loaded command with arguments
  • Keep your command files in a consistent location (e.g., src/app/yourCmdName/cmd.ts)
  • Export commands from a central file like src/app/cmds.ts for better organization
// example/launcher/app/cmds.ts
import { loadCommand } from "@reliverse/rempts";

export async function getBuildCmd() {
  return loadCommand("./build");
}

export async function getDeployCmd() {
  return loadCommand("./deploy");
}

// Usage:
import { getBuildCmd } from "./cmds";
const buildCmd = await getBuildCmd();
await runCmd(buildCmd, ["--prod"]);
// example/launcher/app/minimal/cmd.ts

import { relinka } from "@reliverse/relinka";
import { defineArgs, defineCommand } from "@reliverse/rempts";

export default defineCommand({
  meta: {
    name: "minimal",
    description: "hello world",
  },
  args: defineArgs({
    name: {
      type: "string",
      description: "your name",
      required: true,
    },
  }),
  run({ args }) {
    relinka("success", `👋 Hello, ${args.name}!`);
  },
});

Argument Types: Usage Comparison

Below is a demonstration of how to define and use all supported argument types in rempts: positional, boolean, string, number, and array. This includes example CLI invocations and the resulting parsed output.

import { defineCommand, createCli } from "@reliverse/rempts";

const main = defineCommand({
  meta: {
    name: "mycli",
    version: "1.0.0",
    description: "Demo of all argument types",
  },
  args: {
    // Positional argument (required)
    input: {
      type: "positional",
      required: true,
      description: "Input file path",
    },
    // Boolean flag (default: false)
    verbose: {
      type: "boolean",
      default: false,
      description: "Enable verbose output",
    },
    // String option (optional)
    name: {
      type: "string",
      description: "Your name",
    },
    // Number option (optional, with default)
    count: {
      type: "number",
      default: 1,
      description: "How many times to run",
    },
    // Array option (can be repeated, accepts any value)
    tags: {
      type: "array",
      default: ["demo"],
      description: "Tags for this run (repeatable)",
    },
  },
  run({ args }) {
    console.log("Parsed args:", args);
  },
});

// New object format (recommended)
await createCli({
  mainCommand: main,
});

// Legacy format (still supported)
await createCli(main);

Example CLI Invocations

1. Positional argument

mycli input.txt
# → args.input = "input.txt"

2. Boolean flag

mycli input.txt --verbose
# → args.verbose = true
mycli input.txt --no-verbose
# → args.verbose = false

3. String option

mycli input.txt --name Alice
# → args.name = "Alice"
mycli input.txt
# → args.name = undefined

4. Number option

mycli input.txt --count 5
# → args.count = 5
mycli input.txt
# → args.count = 1 (default)

5. Array option (repeatable, accepts any value)

You can provide array values using any of the following syntaxes (mix and match as needed):

  • Repeated flags:

    mycli input.txt --tags foo --tags bar --tags baz
    # → args.tags = ["foo", "bar", "baz"]
  • Comma-separated values (with or without spaces):

    mycli input.txt --tags foo,bar,baz
    mycli input.txt --tags foo, bar, baz
    # → args.tags = ["foo", "bar", "baz"]
  • Bracketed values (must be passed as a single argument!):

    mycli input.txt --tags "[foo,bar,baz]"
    # → args.tags = ["foo", "bar", "baz"]
  • Mix and match:

    mycli input.txt --tags foo --tags "[bar,bar2,bar3]" --tags baz
    # → args.tags = ["foo", "bar", "bar2", "bar3", "baz"]

Important:

  • Quoted values (single or double quotes around elements) are NOT supported and will throw an error.
    • Example: --tags 'foo' or --tags "[\"bar\",'baz']" will throw an error.
  • Bracketed or comma-separated lists must be passed as a single argument.
    • Example: --tags "[foo,bar]" (quotes around the whole value, not around elements)
    • If you split a bracketed value across arguments, you will get a warning or incorrect parsing.
  • Shells remove quotes before passing arguments to the CLI. If you want to pass a value with commas or brackets, always quote the whole value.
  • Troubleshooting:
    • If you see a warning about possible shell splitting, try quoting the whole value: --tags "[a,b,c]"
    • If you see an error about quoted values, remove quotes around individual elements.

Example error:

$ bun example/launcher/modern.ts build --entry "[foo.ts," "bar.ts]"
✖   Don't use quotes around array elements.
✖   Also — don't use spaces — unless you wrap the whole array in quotes.
⚠   Array argument --entry: Detected possible shell splitting of bracketed value ('[foo.ts,').
⚠   If you intended to pass a bracketed list, quote the whole value like: --entry "[a, b, c]"

7. All together

mycli input.txt --verbose --name Alice --count 3 --tags foo --tags bar
# → args = {
#     input: "input.txt",
#     verbose: true,
#     name: "Alice",
#     count: 3,
#     tags: ["foo", "bar"]
#   }

8. Value Validation with allowed

All argument types support an optional allowed property that restricts which values can be passed:

const main = defineCommand({
  args: {
    // Only allow specific string values
    mode: {
      type: "string",
      allowed: ["development", "production", "test"],
      description: "The mode to run in"
    },
    
    // Only allow specific boolean values (e.g. if you only want true)
    force: {
      type: "boolean",
      allowed: [true],
      description: "Force the operation"
    },
    
    // Only allow specific numbers
    level: {
      type: "number",
      allowed: [1, 2, 3],
      description: "The level to use"
    },
    
    // Only allow specific values in an array
    tags: {
      type: "array",
      allowed: ["web", "api", "mobile"],
      description: "Tags to apply"
    },
    
    // Only allow specific positional values
    action: {
      type: "positional",
      allowed: ["build", "serve", "test"],
      description: "The action to perform"
    }
  }
});

If someone tries to pass a value that's not in the allowed list, they'll get a helpful error message:

mycli --mode staging
# Error: Invalid value for --mode: staging. Allowed values are: development, production, test

mycli --level 4
# Error: Invalid value for --level: 4. Allowed values are: 1, 2, 3

mycli --tags desktop
# Error: Invalid value in array --tags: desktop. Allowed values are: web, api, mobile

The validation happens after type casting, so for example with numbers, the input will first be converted to a number and then checked against the allowed list.

Contributing

Bug report? Prompt idea? Want to build the best DX possible?

You're in the right place! Please help us make the best CLI toolkit possible.

Notices For Contributors

TypeScript Support:

All APIs are fully typed. See src/types.ts for advanced customization and type inference.

Examples:

Components and Utilities:

  • components/: All prompt UIs, CLI output, launcher logic, etc.
  • utils/: Color, error, validation, streaming, and system helpers.
  • hooks/: Useful hooks for prompt state and effects.

Helpful Links

TODO

  • [ ] migrate to dler libs in the future (all main components will be published as separate packages; @reliverse/rempts will be a wrapper for all of them)

Related

Shoutouts

  • citty - launcher design inspiration

Support

Bug report? Prompt idea? Want to build the best DX possible?

You're in the right place:

No classes. No magic. Just clean, composable tools for CLI devs.

License

💖 MIT (see LICENSE and LICENCES) © blefnk (Nazar Kornienko)

Readme

Keywords

Package Sidebar

Install

npm i @reliverse/rempts

Weekly Downloads

694

Version

1.7.38

License

MIT

Unpacked Size

405 kB

Total Files

131

Last publish

Collaborators

  • blefnk