diff --git a/README.md b/README.md index a438e1a..c68790c 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,21 @@ Runs a command with usage support and graceful error handling. Create a wrapper around command that calls `runMain` when called. +### `runRawMain` + +Runs a command. Unlike `runMain`, this function requires you to define command execution handling like `handleCommand` and error handling yourself like `handleError` and specify them as arguments. + +> [!NOTE] +> This function is a low-level function compared to `runMain`. You should also implement your own `showUsage` etc. and specify it as an option. + +### `handleCommand` + +A handler that implements the logic to execute commands with usage support. It is used inside in `runMain`. You can specify it as a helper to `run`. + +### `handleError` + +An error handler that implementes the logic to graceful error handling. It is used inside `runMain`. You can specify it as a helper to `run`. + ### `runCommand` Parses input args and runs command and sub-commands (unsupervised). You can access `result` key from returnd/awaited value to access command's result. @@ -104,6 +119,10 @@ Renders command usage to a string value. Renders usage and prints to the console +### `formatLineColumns` + +Formats line columns. If you define custom `showUsage`, it is convenient to adjust the format with this function. + ## Development - Clone this repository diff --git a/src/_utils.ts b/src/_utils.ts index 467fe01..2275c31 100644 --- a/src/_utils.ts +++ b/src/_utils.ts @@ -7,25 +7,6 @@ export function toArray(val: any) { return val === undefined ? [] : [val]; } -export function formatLineColumns(lines: string[][], linePrefix = "") { - const maxLengh: number[] = []; - for (const line of lines) { - for (const [i, element] of line.entries()) { - maxLengh[i] = Math.max(maxLengh[i] || 0, element.length); - } - } - return lines - .map((l) => - l - .map( - (c, i) => - linePrefix + c[i === 0 ? "padStart" : "padEnd"](maxLengh[i]), - ) - .join(" "), - ) - .join("\n"); -} - export function resolveValue(input: Resolvable): T | Promise { return typeof input === "function" ? (input as any)() : input; } diff --git a/src/index.ts b/src/index.ts index 7c0cfd9..75f5975 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,14 @@ export * from "./types"; export type { RunCommandOptions } from "./command"; -export type { RunMainOptions } from "./main"; +export type { RunMainOptions, RunArgs, RunHandlers } from "./main"; export { defineCommand, runCommand } from "./command"; export { parseArgs } from "./args"; -export { renderUsage, showUsage } from "./usage"; -export { runMain, createMain } from "./main"; +export { renderUsage, showUsage, formatLineColumns } from "./usage"; +export { + runMain, + createMain, + runRawMain, + handleCommand, + handleError, +} from "./main"; diff --git a/src/main.ts b/src/main.ts index 49cb953..604fbfc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,39 +9,77 @@ export interface RunMainOptions { showUsage?: typeof _showUsage; } -export async function runMain( +export interface RunArgs { + cmd: CommandDef; + rawArgs: string[]; + showUsage?: typeof _showUsage; +} + +export interface RunHandlers< + T extends ArgsDef = ArgsDef, + E extends Error = Error, +> { + command: (args: RunArgs) => Promise; + error: (args: RunArgs, error: E) => Promise; +} + +export async function handleCommand({ + cmd, + rawArgs, + showUsage, +}: RunArgs): Promise { + if ((rawArgs.includes("--help") || rawArgs.includes("-h")) && showUsage) { + await showUsage(...(await resolveSubCommand(cmd, rawArgs))); + process.exit(0); + } else if (rawArgs.length === 1 && rawArgs[0] === "--version") { + const meta = + typeof cmd.meta === "function" ? await cmd.meta() : await cmd.meta; + if (!meta?.version) { + throw new CLIError("No version specified", "E_NO_VERSION"); + } + consola.log(meta.version); + } else { + await runCommand(cmd, { rawArgs }); + } +} + +export async function handleError< + T extends ArgsDef = ArgsDef, + E extends Error = Error, +>({ cmd, rawArgs, showUsage }: RunArgs, error: E) { + const isCLIError = error instanceof CLIError; + if (!isCLIError) { + consola.error(error, "\n"); + } + if (isCLIError && showUsage) { + await showUsage(...(await resolveSubCommand(cmd, rawArgs))); + } + consola.error(error.message); + process.exit(1); +} + +export async function runRawMain( cmd: CommandDef, + handlers: RunHandlers, opts: RunMainOptions = {}, ) { const rawArgs = opts.rawArgs || process.argv.slice(2); - const showUsage = opts.showUsage || _showUsage; + const args = { cmd, rawArgs, showUsage: opts.showUsage }; try { - if (rawArgs.includes("--help") || rawArgs.includes("-h")) { - await showUsage(...(await resolveSubCommand(cmd, rawArgs))); - process.exit(0); - } else if (rawArgs.length === 1 && rawArgs[0] === "--version") { - const meta = - typeof cmd.meta === "function" ? await cmd.meta() : await cmd.meta; - if (!meta?.version) { - throw new CLIError("No version specified", "E_NO_VERSION"); - } - consola.log(meta.version); - } else { - await runCommand(cmd, { rawArgs }); - } + await handlers.command(args); } catch (error: any) { - const isCLIError = error instanceof CLIError; - if (!isCLIError) { - consola.error(error, "\n"); - } - if (isCLIError) { - await showUsage(...(await resolveSubCommand(cmd, rawArgs))); - } - consola.error(error.message); - process.exit(1); + await handlers.error(args, error); } } +export async function runMain( + cmd: CommandDef, + opts: RunMainOptions = {}, +) { + opts.showUsage = opts.showUsage || _showUsage; + await runRawMain(cmd, { command: handleCommand, error: handleError }, opts); +} + export function createMain( cmd: CommandDef, ): (opts?: RunMainOptions) => Promise { diff --git a/src/usage.ts b/src/usage.ts index cb4a794..b7feab6 100644 --- a/src/usage.ts +++ b/src/usage.ts @@ -1,6 +1,6 @@ import consola from "consola"; import { colors } from "consola/utils"; -import { formatLineColumns, resolveValue } from "./_utils"; +import { resolveValue } from "./_utils"; import type { ArgsDef, CommandDef } from "./types"; import { resolveArgs } from "./args"; @@ -136,3 +136,22 @@ export async function renderUsage( return usageLines.filter((l) => typeof l === "string").join("\n"); } + +export function formatLineColumns(lines: string[][], linePrefix = "") { + const maxLengh: number[] = []; + for (const line of lines) { + for (const [i, element] of line.entries()) { + maxLengh[i] = Math.max(maxLengh[i] || 0, element.length); + } + } + return lines + .map((l) => + l + .map( + (c, i) => + linePrefix + c[i === 0 ? "padStart" : "padEnd"](maxLengh[i]), + ) + .join(" "), + ) + .join("\n"); +}