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; defaultsargvtoprocess.argv.slice(2)--version/-Vprints the version string (if set) and exits cleanly--help/-hprintshelpText()and exits cleanlyoptions.signal(AbortSignal) — throwsCliError(CLI_CODES.APP.ABORTED)immediately when already abortedonErrorhandler — catches thrown errors, wraps non-CliErrorinCliError(CLI_CODES.APP.RUN_FAILED), and calls the handler instead of re-throwing- OTel span on every
run()call: span namecli.app.run, attributecli.appset to the app name;SpanStatusCode.ERRORset 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.