Files
2026-05-27 23:29:53 +01:00

4.2 KiB

app

(no submodules)

Purpose

App is the top-level entry point for a @suaveplan/cli application. It wraps a root Command and adds application-level concerns: version printing, help generation, onError handling, OTel span instrumentation on run(), and AbortSignal support. The cli() factory function creates an App with a name and optional version string in a single expression.

App delegates all routing and parsing to the underlying Command hierarchy. Its primary responsibility is to intercept --version, --help, and abort signals before dispatching, and to wrap any thrown error with CliError(CLI_CODES.APP.RUN_FAILED) when an onError handler is registered.

Features

  • Fluent builder: .description(), .option(), .argument(), .action(), .command(), .example()
  • run(argv?, options?) dispatches to the root command; defaults argv to process.argv.slice(2)
  • --version / -V prints the version string (if set) and exits cleanly
  • --help / -h prints helpText() and exits cleanly
  • options.signal (AbortSignal) — throws CliError(CLI_CODES.APP.ABORTED) immediately when already aborted
  • onError handler — catches thrown errors, wraps non-CliError in CliError(CLI_CODES.APP.RUN_FAILED), and calls the handler instead of re-throwing
  • OTel span on every run() call: span name cli.app.run, attribute cli.app set to the app name; SpanStatusCode.ERROR set on failure
  • cli(name, version?) factory for single-expression app creation

Usage: Basic Example

import { cli } from "@suaveplan/cli/app/app";

const app = cli("deploy", "1.0.0")
  .description("Deployment toolkit");

app.command("up")
  .description("Bring services up")
  .action(async () => {
    console.log("Services are up");
  });

await app.run();

Usage: Advanced Example

import { App } from "@suaveplan/cli/app/app";

const app = new App({
  name: "myapp",
  version: "2.1.0",
  description: "My enterprise CLI",
  writer: (text) => process.stdout.write(text + "\n"),
  onError: (err) => {
    console.error(`Error [${err.code}]: ${err.message}`);
    process.exit(1);
  },
});

app
  .option("--verbose", "Enable verbose output")
  .action(async (ctx) => {
    if (ctx.options.verbose) console.log("Verbose mode on");
  });

const controller = new AbortController();
process.on("SIGINT", () => controller.abort());

await app.run(process.argv.slice(2), { signal: controller.signal });

API Reference

new App(config: AppConfig)

Creates a new app. Config fields: name (required), version, description, enableHelp (default true), enableVersion (default true), onError, writer.

app.name: string

The app's name as provided in config.

app.version: string | undefined

The app's version string, or undefined if not set.

app.description(text: string): this

Sets the app description shown in help text.

app.option(flags, description?, defaultValue?): this

Registers a root-level option.

app.argument(spec, description?): this

Registers a root-level positional argument.

app.action(fn: ActionFn): this

Sets the root action handler.

app.command(name: string): Command

Registers and returns a named sub-command.

app.example(text: string): this

Adds an example to the help output.

app.helpText(): string

Returns formatted multi-line help text for the root command.

app.run(argv?: string[], options?: { signal?: AbortSignal }): Promise<void>

Executes the app. If signal is already aborted, throws CliError(ABORTED) immediately. Intercepts --version and --help before routing. Delegates to the root Command.execute().

cli(name: string, version?: string): App

Factory function. Creates an App with name and optional version.

Implementation Notes

The writer config field injects the output sink, defaulting to process.stdout.write. This enables full testability without TTY or process coupling. The onError hook receives a CliError — if the thrown value was a plain Error, it is wrapped in CliError(RUN_FAILED) before being handed to the hook, so callers always receive a structured error with a stable code.