first commit
This commit is contained in:
202
docs/devtools/tooling/cli/README.md
Normal file
202
docs/devtools/tooling/cli/README.md
Normal file
File diff suppressed because one or more lines are too long
120
docs/devtools/tooling/cli/src/app/app.md
Normal file
120
docs/devtools/tooling/cli/src/app/app.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# app
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## 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
|
||||
|
||||
```ts
|
||||
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
|
||||
|
||||
```ts
|
||||
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.
|
||||
122
docs/devtools/tooling/cli/src/command/command.md
Normal file
122
docs/devtools/tooling/cli/src/command/command.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# command
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`Command` is the core routing primitive for `@suaveplan/cli`. It models a single CLI command that can carry options, positional arguments, and an action handler, or act as a router that delegates to named sub-commands. Every `App` is backed by a root `Command`.
|
||||
|
||||
The class exposes a fluent builder API: chain `.description()`, `.option()`, `.argument()`, `.command()`, and `.action()` calls before calling `.execute()` at runtime. Sub-commands are registered via `.command(name)` which returns the child `Command`, enabling identical fluent chaining for nested hierarchies.
|
||||
|
||||
## Features
|
||||
|
||||
- Fluent builder: `.description()`, `.option()`, `.argument()`, `.action()`, `.command()`, `.alias()`, `.hidden()`, `.example()`
|
||||
- Sub-command routing: first non-flag token in `argv` is matched against registered names and aliases
|
||||
- Option parsing delegated to `parseArgs` — inherits full flag semantics (boolean, string, number, `--no-*`, short flags, `=` syntax, `--` rest)
|
||||
- OTel span on every `execute()` call: span name `cli.command.execute`, attribute `cli.command` set to the full qualified name
|
||||
- `CliError(CLI_CODES.COMMAND.NO_ACTION)` when no action is registered and no sub-command is found
|
||||
- `CliError(CLI_CODES.COMMAND.UNKNOWN_SUBCOMMAND)` when an unknown first token is provided and sub-commands are registered
|
||||
- Automatic `--help` / `-h` handling: prints `helpText()` and returns without error
|
||||
- `helpText(parentName?)` generates a formatted usage block with options and sub-command listing
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { Command } from "@suaveplan/cli/command/command";
|
||||
|
||||
const cmd = new Command("greet")
|
||||
.description("Greet a user")
|
||||
.option("--name <name>", "Name to greet", "World")
|
||||
.action(async (ctx) => {
|
||||
console.log(`Hello, ${ctx.options.name}!`);
|
||||
});
|
||||
|
||||
await cmd.execute(["--name", "Alice"]);
|
||||
// → Hello, Alice!
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Command } from "@suaveplan/cli/command/command";
|
||||
|
||||
const root = new Command("deploy")
|
||||
.description("Deployment toolkit");
|
||||
|
||||
root
|
||||
.command("up")
|
||||
.description("Bring services up")
|
||||
.option("--env <env>", "Target environment", "staging")
|
||||
.action(async (ctx) => {
|
||||
console.log(`Deploying to ${ctx.options.env}`);
|
||||
});
|
||||
|
||||
root
|
||||
.command("down")
|
||||
.alias("d")
|
||||
.description("Tear services down")
|
||||
.action(async () => {
|
||||
console.log("Tearing down...");
|
||||
});
|
||||
|
||||
// Route: "deploy up --env production"
|
||||
await root.execute(["up", "--env", "production"]);
|
||||
|
||||
// Route by alias: "deploy d"
|
||||
await root.execute(["d"]);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `new Command(name: string)`
|
||||
|
||||
Creates a new command with the given name.
|
||||
|
||||
### `.description(text: string): this`
|
||||
|
||||
Sets the human-readable description shown in help text.
|
||||
|
||||
### `.option(flags: string, description?: string, defaultValue?: unknown): this`
|
||||
|
||||
Registers an option. `flags` follows `parseOptionFlags` syntax: `"--name <value>"`, `"-v, --verbose"`.
|
||||
|
||||
### `.argument(spec: string, description?: string): this`
|
||||
|
||||
Registers a positional argument. `spec` follows `parseArgumentSpec` syntax: `"<file>"`, `"[file]"`, `"<files...>"`.
|
||||
|
||||
### `.action(fn: ActionFn): this`
|
||||
|
||||
Sets the action handler. `fn` receives an `ActionContext` with `options`, `args` (positionals), `rest`, and `command` info.
|
||||
|
||||
### `.command(name: string): Command`
|
||||
|
||||
Registers a named sub-command and returns it for further chaining.
|
||||
|
||||
### `.alias(name: string): this`
|
||||
|
||||
Adds an alias for this command, recognised during routing.
|
||||
|
||||
### `.hidden(): this`
|
||||
|
||||
Marks the command as hidden — it is excluded from help text listings.
|
||||
|
||||
### `.example(text: string): this`
|
||||
|
||||
Adds an example line shown in help text.
|
||||
|
||||
### `.helpText(parentName?: string): string`
|
||||
|
||||
Returns formatted multi-line help text including usage, options, sub-commands, and examples.
|
||||
|
||||
### `.getParserConfig(): ParserConfig`
|
||||
|
||||
Returns the `ParserConfig` derived from registered options and arguments. Used internally by `execute()`.
|
||||
|
||||
### `.execute(argv: string[], parentName?: string): Promise<void>`
|
||||
|
||||
Parses `argv`, routes to sub-commands or calls the action. Wraps the execution in an OTel span. Throws `CliError` on routing or parse failures.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Sub-command alias resolution scans all registered sub-commands for an `_aliases` entry matching the first token. An OTel span covers the full `execute()` lifecycle including sub-command delegation — the span status is set to `ERROR` if any exception propagates. The `--help` / `-h` shortcut is handled before sub-command routing and before the action call, so it always works even if no action is registered.
|
||||
171
docs/devtools/tooling/cli/src/errors/errors.md
Normal file
171
docs/devtools/tooling/cli/src/errors/errors.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# errors
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `errors` module defines the complete error taxonomy for `@suaveplan/cli`. It
|
||||
provides a stable, machine-readable code registry (`CLI_CODES`) and a strongly-typed
|
||||
error class (`CliError`) so that every failure surface in the CLI framework is
|
||||
discriminable at runtime without string-matching on messages. Callers can switch
|
||||
exhaustively on `err.code`, log structured records with stable identifiers, and
|
||||
forward typed errors across process boundaries without losing fidelity.
|
||||
|
||||
## Features
|
||||
|
||||
- **Three sub-namespaces** — `CLI.PARSE.*` covers argument/option parsing failures;
|
||||
`CLI.COMMAND.*` covers command-dispatch failures; `CLI.APP.*` covers top-level
|
||||
application lifecycle failures.
|
||||
- **Nine stable error codes** — each code is a dot-delimited string constant produced
|
||||
by `defineCodes()` from `@suaveplan/error`, guaranteeing uniqueness and structural
|
||||
consistency across the codebase.
|
||||
- **`CliCode` union type** — narrows the full set of valid codes for use in generic
|
||||
`SuaveplanError<CliCode>` type parameters and exhaustive switch statements.
|
||||
- **`CliError` class** — extends `SuaveplanError` with a public constructor, cause
|
||||
chaining, and structured `context` attachment for diagnostic metadata.
|
||||
- **Zero runtime overhead** — the `CLI_CODES` object is `as const`, so all string
|
||||
values are inlined at the call site by the TypeScript compiler.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { CliError, CLI_CODES } from "@suaveplan/cli/errors/errors";
|
||||
|
||||
function assertKnownOption(flag: string, known: Set<string>): void {
|
||||
if (!known.has(flag)) {
|
||||
throw new CliError(
|
||||
CLI_CODES.PARSE.UNKNOWN_OPTION,
|
||||
"cli.parse.unknown_option"
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { CliError, CLI_CODES } from "@suaveplan/cli/errors/errors";
|
||||
import { SuaveplanError } from "@suaveplan/error";
|
||||
|
||||
async function runApp(signal: AbortSignal): Promise<void> {
|
||||
try {
|
||||
await doWork(signal);
|
||||
} catch (err) {
|
||||
if (signal.aborted) {
|
||||
throw new CliError(CLI_CODES.APP.ABORTED, "cli.app.aborted", {
|
||||
cause: err instanceof Error ? err : undefined,
|
||||
context: { reason: signal.reason },
|
||||
});
|
||||
}
|
||||
throw new CliError(CLI_CODES.APP.RUN_FAILED, "cli.app.run_failed", {
|
||||
cause: err instanceof Error ? err : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleError(err: unknown): void {
|
||||
if (err instanceof CliError) {
|
||||
// Narrow on the stable code — no message-string matching needed.
|
||||
switch (err.code) {
|
||||
case CLI_CODES.APP.ABORTED:
|
||||
process.exitCode = 130; // standard SIGINT exit code
|
||||
break;
|
||||
case CLI_CODES.PARSE.UNKNOWN_OPTION:
|
||||
console.error(`Unknown option: ${String(err.context?.["option"])}`);
|
||||
process.exitCode = 1;
|
||||
break;
|
||||
default:
|
||||
console.error(err.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `CLI_CODES`
|
||||
|
||||
```ts
|
||||
const CLI_CODES: {
|
||||
PARSE: {
|
||||
UNKNOWN_OPTION: "CLI.PARSE.UNKNOWN_OPTION";
|
||||
MISSING_VALUE: "CLI.PARSE.MISSING_VALUE";
|
||||
INVALID_VALUE: "CLI.PARSE.INVALID_VALUE";
|
||||
MISSING_ARGUMENT: "CLI.PARSE.MISSING_ARGUMENT";
|
||||
MISSING_REQUIRED_OPTION: "CLI.PARSE.MISSING_REQUIRED_OPTION";
|
||||
};
|
||||
COMMAND: {
|
||||
NO_ACTION: "CLI.COMMAND.NO_ACTION";
|
||||
UNKNOWN_SUBCOMMAND: "CLI.COMMAND.UNKNOWN_SUBCOMMAND";
|
||||
};
|
||||
APP: {
|
||||
RUN_FAILED: "CLI.APP.RUN_FAILED";
|
||||
ABORTED: "CLI.APP.ABORTED";
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Immutable registry of all error codes used by `@suaveplan/cli`. Each value is a
|
||||
dot-delimited string in the form `<NAMESPACE>.<SUBCATEGORY>.<NAME>`.
|
||||
|
||||
### `CliCode`
|
||||
|
||||
```ts
|
||||
type CliCode =
|
||||
| "CLI.PARSE.UNKNOWN_OPTION"
|
||||
| "CLI.PARSE.MISSING_VALUE"
|
||||
| "CLI.PARSE.INVALID_VALUE"
|
||||
| "CLI.PARSE.MISSING_ARGUMENT"
|
||||
| "CLI.PARSE.MISSING_REQUIRED_OPTION"
|
||||
| "CLI.COMMAND.NO_ACTION"
|
||||
| "CLI.COMMAND.UNKNOWN_SUBCOMMAND"
|
||||
| "CLI.APP.RUN_FAILED"
|
||||
| "CLI.APP.ABORTED";
|
||||
```
|
||||
|
||||
Union of all values in `CLI_CODES`. Use as the type parameter for
|
||||
`SuaveplanError<CliCode>` or in exhaustive switch statements.
|
||||
|
||||
### `CliError`
|
||||
|
||||
```ts
|
||||
class CliError extends SuaveplanError<CliCode> {
|
||||
public constructor(
|
||||
code: CliCode,
|
||||
messageKey: string,
|
||||
options?: SuaveplanErrorOptions
|
||||
): CliError;
|
||||
}
|
||||
```
|
||||
|
||||
Typed error for all `@suaveplan/cli` failures.
|
||||
|
||||
- **`code`** — one of the `CliCode` values; accessible as `err.code` via the base
|
||||
class.
|
||||
- **`messageKey`** — i18n message key passed to the base class for message resolver
|
||||
integration.
|
||||
- **`options.cause`** — optional upstream `Error` for cause chaining; accessible as
|
||||
`err.cause`.
|
||||
- **`options.context`** — optional `Record<string, unknown>` with structured
|
||||
diagnostic metadata; accessible as `err.context`.
|
||||
|
||||
Throws: nothing — this is an Error constructor.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`CLI_CODES` is built from three separate `defineCodes()` calls rather than one flat
|
||||
call so that each sub-namespace is isolated. This mirrors the pattern established in
|
||||
`@suaveplan/readline` and keeps future code-set additions within a namespace
|
||||
non-breaking for callers who already destructure `CLI_CODES.PARSE`.
|
||||
|
||||
The `CliError` constructor comment (`biome-ignore`) documents why the constructor is
|
||||
not "useless" from Biome's perspective: the base `SuaveplanError` constructor is
|
||||
`protected`, so widening it to `public` here is a meaningful override even though
|
||||
the body only calls `super()`. This is the standard pattern across all
|
||||
`@suaveplan/*` packages.
|
||||
|
||||
No runtime dependencies beyond `@suaveplan/error` are introduced. The `defineCodes`
|
||||
helper produces plain string literals, so the entire module tree-shakes to near
|
||||
zero bytes when errors are not thrown on a given code path.
|
||||
93
docs/devtools/tooling/cli/src/output/format/format.md
Normal file
93
docs/devtools/tooling/cli/src/output/format/format.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# output/format
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`format` provides a collection of pure string-formatting utilities for structured CLI output. All functions take plain data and return formatted strings — no side effects, no I/O. Callers are responsible for printing the result. This design makes every function trivially testable and composable.
|
||||
|
||||
The module covers the most common CLI output patterns: key-value pairs, bulleted lists, directory-style trees, inline JSON, multi-column alignment, section headings, horizontal dividers, and indented blocks.
|
||||
|
||||
## Features
|
||||
|
||||
- `keyValue(obj, opts?)` — formats a record as aligned `key: value` pairs
|
||||
- `list(items, opts?)` — formats an array as a bulleted list
|
||||
- `tree(node, opts?)` — renders a recursive tree with `├─` / `└─` connectors
|
||||
- `json(value, opts?)` — `JSON.stringify` wrapper with configurable indent
|
||||
- `columns(rows, separator?)` — right-pads cells so columns align across rows
|
||||
- `heading(text, level?)` — box-style heading with `═` (level 1) or `─` (level 2) borders
|
||||
- `divider(char?, width?)` — repeated character line
|
||||
- `indent(text, spaces?)` — prefixes every line with spaces
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { keyValue, list, heading } from "@suaveplan/cli/output/format/format";
|
||||
|
||||
console.log(heading("Build Report"));
|
||||
console.log(keyValue({ status: "success", files: 42, duration: "1.2s" }));
|
||||
console.log(list(["Compiled main.ts", "Emitted dist/", "Linked types"]));
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { tree, columns, json, indent } from "@suaveplan/cli/output/format/format";
|
||||
|
||||
const structure = {
|
||||
label: "src/",
|
||||
children: [
|
||||
{ label: "app/", children: [{ label: "app.ts" }, { label: "app.test.ts" }] },
|
||||
{ label: "utils/", children: [{ label: "format.ts" }] },
|
||||
],
|
||||
};
|
||||
|
||||
console.log(tree(structure));
|
||||
|
||||
const rows = [
|
||||
["alice", "admin", "2024-01-01"],
|
||||
["bob", "user", "2024-03-15"],
|
||||
];
|
||||
console.log(columns([["Name", "Role", "Joined"], ...rows]));
|
||||
|
||||
console.log(indent(json({ ok: true, count: 7 }), 4));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `keyValue(obj: Record<string, unknown>, opts?: KeyValueOptions): string`
|
||||
|
||||
Formats each entry as `key: value`. Options: `separator` (default `": "`), `indent` (spaces before each line, default 0).
|
||||
|
||||
### `list(items: string[], opts?: ListOptions): string`
|
||||
|
||||
Formats each item with a bullet marker. Options: `bullet` (default `"•"`), `indent` (default 0).
|
||||
|
||||
### `tree(node: TreeNode, opts?: TreeOptions, _prefix?: string): string`
|
||||
|
||||
Renders a nested tree with connector lines. Options: `indent` (spaces per nesting level, default 2).
|
||||
|
||||
### `json(value: unknown, opts?: JsonOptions): string`
|
||||
|
||||
Returns `JSON.stringify(value, null, indent)`. Options: `indent` (default 2).
|
||||
|
||||
### `columns(rows: string[][], separator?: string): string`
|
||||
|
||||
Pads each cell in each column to the column's maximum width, then joins with `separator` (default `" "`). Returns empty string for empty input.
|
||||
|
||||
### `heading(text: string, level?: number): string`
|
||||
|
||||
Returns a three-line block: border, centered text, border. Level 1 uses `═`, level 2 uses `─`, others use `-`.
|
||||
|
||||
### `divider(char?: string, width?: number): string`
|
||||
|
||||
Returns `char.repeat(width)`. Defaults: `"─"` and `40`.
|
||||
|
||||
### `indent(text: string, spaces?: number): string`
|
||||
|
||||
Prepends `spaces` (default 2) spaces to every line of `text`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All functions are stateless and have no dependencies. `tree` recurses depth-first, tracking a prefix string for connector characters at each level. `columns` computes widths in a single pass over all rows, then renders in a second pass. The `_prefix` parameter of `tree` is internal — external callers should omit it.
|
||||
81
docs/devtools/tooling/cli/src/output/table/table.md
Normal file
81
docs/devtools/tooling/cli/src/output/table/table.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# output/table
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`table` renders structured data as a formatted ASCII table with header row, optional borders, and configurable column alignment. It computes column widths from the full data set — header and all rows — ensuring that every column is at least as wide as its widest cell. The result is a plain multi-line string ready for console output or piping.
|
||||
|
||||
The function is pure: it takes a `TableOptions` object and returns a `string`. No state, no I/O, no mutations.
|
||||
|
||||
## Features
|
||||
|
||||
- Automatic column width calculation: each column is as wide as its widest cell across all rows
|
||||
- Three alignment modes per column: `"left"` (default), `"right"`, `"center"`
|
||||
- Border mode (default on): header and data rows separated by `─┼─` divider
|
||||
- Border-off mode: columns joined with two spaces
|
||||
- Handles empty rows gracefully — returns header-only output
|
||||
- Property-tested: column widths always `>=` max cell content width
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { table } from "@suaveplan/cli/output/table/table";
|
||||
|
||||
const out = table({
|
||||
headers: ["Name", "Role", "Since"],
|
||||
rows: [
|
||||
["Alice", "Admin", "2022"],
|
||||
["Bob", "User", "2023"],
|
||||
],
|
||||
});
|
||||
console.log(out);
|
||||
// Name │ Role │ Since
|
||||
// ──────┼───────┼──────
|
||||
// Alice │ Admin │ 2022
|
||||
// Bob │ User │ 2023
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { table } from "@suaveplan/cli/output/table/table";
|
||||
|
||||
const out = table({
|
||||
headers: ["Package", "Version", "Size"],
|
||||
rows: [
|
||||
["@suaveplan/error", "0.1.0", "12 kB"],
|
||||
["@suaveplan/cli", "0.1.0", "48 kB"],
|
||||
],
|
||||
alignment: ["left", "center", "right"],
|
||||
border: true,
|
||||
});
|
||||
console.log(out);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `table(opts: TableOptions): string`
|
||||
|
||||
Renders the table and returns the formatted string.
|
||||
|
||||
**`opts.headers: string[]`** — column header labels. Determines the number of columns.
|
||||
|
||||
**`opts.rows: string[][]`** — data rows. Each row is an array of string cells. Extra cells beyond `headers.length` are ignored; missing cells are treated as empty.
|
||||
|
||||
**`opts.alignment?: Alignment[]`** — per-column alignment (`"left"` | `"right"` | `"center"`). Defaults to `"left"` for any column not specified.
|
||||
|
||||
**`opts.border?: boolean`** — whether to render `│` column separators and `─┼─` divider line. Default `true`.
|
||||
|
||||
### `Alignment`
|
||||
|
||||
`"left" | "right" | "center"` — the alignment mode for a table column.
|
||||
|
||||
### `TableOptions`
|
||||
|
||||
The full options type. Corresponds to `TableOptionsSchema` for Zod validation.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Column widths are computed with a single `Math.max` pass over `[headers, ...rows]`. Cells are padded with `String.prototype.padEnd` (left/center) or `String.prototype.padStart` (right via the `align` helper). The divider line joins `"─".repeat(width)` segments with `"─┼─"`. When `border` is false, the divider is omitted and segments are joined with `" "` instead of `" │ "`. Trailing whitespace on each row is trimmed to keep output clean.
|
||||
92
docs/devtools/tooling/cli/src/parser/parser.md
Normal file
92
docs/devtools/tooling/cli/src/parser/parser.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# parser
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `parser` module turns a raw `argv` token array into a structured `ParsedArgs` object with typed options, named positional arguments, and a `rest` passthrough channel. It is the low-level argument-parsing engine that higher-level command definitions build on, designed to model the behaviour CLI users expect from mature tools like `git`, `npm`, and `cargo`.
|
||||
|
||||
The module is deliberately imperative and framework-free: no reflection, no decorators, no hidden state. Every behaviour — type coercion, `--no-*` negation, short-flag resolution, choice validation, required-option checking — is enforced in a single linear pass over `argv`, and every failure mode raises a `CliError` with a stable code drawn from `CLI_CODES.PARSE.*` so callers can branch on machine-readable codes instead of message strings.
|
||||
|
||||
## Features
|
||||
|
||||
- Boolean flags (`--verbose`) and negation (`--no-verbose`)
|
||||
- String/number/array options via `--key value`, `--key=value`, or short-flag (`-v`) forms
|
||||
- Short-flag mapping via `short: "v"` on the option config
|
||||
- Passthrough `rest` channel after `--`
|
||||
- Variadic positionals (`<files...>`) collected as arrays
|
||||
- Required positionals and required options with dedicated error codes
|
||||
- Choice validation with `INVALID_VALUE`
|
||||
- `allowUnknown: true` to silently ignore unrecognised flags
|
||||
- Defaults for options and positionals
|
||||
- Helper parsers for flag specs (`parseOptionFlags`) and positional specs (`parseArgumentSpec`)
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { parseArgs } from "@suaveplan/cli/parser";
|
||||
|
||||
const result = parseArgs(["--verbose", "src/file.ts"], {
|
||||
options: { verbose: { type: "boolean" } },
|
||||
arguments: [{ name: "file", required: true, variadic: false }],
|
||||
});
|
||||
|
||||
console.log(result.options.verbose); // true
|
||||
console.log(result.positionals.file); // "src/file.ts"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { parseArgs, parseOptionFlags, parseArgumentSpec } from "@suaveplan/cli/parser";
|
||||
|
||||
const flag = parseOptionFlags("-o, --output <file>", "Output file");
|
||||
const posArg = parseArgumentSpec("<sources...>", "Source files");
|
||||
|
||||
const result = parseArgs(
|
||||
["-o", "out.txt", "--format=json", "a.ts", "b.ts", "--", "--not-a-flag"],
|
||||
{
|
||||
options: {
|
||||
[flag.name]: flag.config,
|
||||
format: { type: "string", choices: ["json", "text"], default: "text" },
|
||||
retries: { type: "number", default: 3 },
|
||||
},
|
||||
arguments: [posArg],
|
||||
}
|
||||
);
|
||||
|
||||
// result.options.output === "out.txt"
|
||||
// result.options.format === "json"
|
||||
// result.positionals.sources === ["a.ts", "b.ts"]
|
||||
// result.rest === ["--not-a-flag"]
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `parseArgs(argv, config?)`
|
||||
|
||||
- `argv: string[]` — raw CLI tokens (without the node/bun prefix)
|
||||
- `config?: ParserConfig` — optional options/arguments definitions, `allowUnknown`, `stopEarly`
|
||||
- Returns `ParsedArgs` with `options`, `positionals`, and `rest`
|
||||
- Throws `CliError` with codes `UNKNOWN_OPTION`, `MISSING_VALUE`, `INVALID_VALUE`, `MISSING_ARGUMENT`, `MISSING_REQUIRED_OPTION`
|
||||
|
||||
### `parseOptionFlags(flags, description?, defaultValue?)`
|
||||
|
||||
- `flags: string` — commander-style spec, e.g. `"-v, --verbose"` or `"--output <file>"`
|
||||
- Returns `{ name, config }` where `name` is the camelCased long-flag name
|
||||
- Throws `CliError(UNKNOWN_OPTION)` if no long flag is provided
|
||||
|
||||
### `parseArgumentSpec(spec, description?)`
|
||||
|
||||
- `spec: string` — `"<name>"`, `"[name]"`, `"<name...>"`, or `"[name...]"`
|
||||
- Returns an `ArgumentConfig` with `required`, `variadic`, `name`, and optional `description`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Single-pass state machine: tokens are consumed left-to-right with a small `restMode` flag that flips when `--` is encountered. There are no lookaheads beyond peeking at `argv[i + 1]` to decide whether a flag's value is the next token.
|
||||
- `--no-*` handling strips the literal `--no-` prefix (not the camelCased form) and then camelCases the remainder, so `--no-dry-run` correctly maps to the `dryRun` option.
|
||||
- Values are only consumed as the next token when that token does not start with `-`, preventing `--foo --bar` from stealing `--bar` as the value of `--foo`.
|
||||
- `number` type uses `Number(raw)` and rejects `NaN` so strings like `"abc"` raise `INVALID_VALUE` rather than silently becoming `NaN`.
|
||||
- All errors are `CliError` instances carrying `context` metadata (option name, offending value, allowed choices) so UIs and logs can render actionable diagnostics without re-parsing message strings.
|
||||
- Schemas mirror the TypeScript types exactly and are exported separately so callers can validate untrusted `ParserConfig` inputs at the boundary without pulling Zod into hot paths.
|
||||
92
docs/devtools/tooling/cli/src/progress/progress.md
Normal file
92
docs/devtools/tooling/cli/src/progress/progress.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# progress
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`createProgress` renders a fixed-width ASCII progress bar for tracking long-running operations with a known total. It writes bar updates to any `WritableStreamLike` output — TTY or non-TTY — using in-place line clearing on TTY terminals and newline-separated lines on non-TTY streams (CI, pipes). The bar updates are driven by explicit `update(current)` calls from the caller; no internal timer is used.
|
||||
|
||||
The instance is `AbortSignal`-aware: aborting the signal silently makes all subsequent `update()` calls no-ops, allowing calling code to cancel a long operation without crashing.
|
||||
|
||||
## Features
|
||||
|
||||
- Fixed-width block bar with configurable `width` (default 40 characters) using `█` fill and `░` empty characters
|
||||
- Percentage and `current/total` count displayed after the bar: `[████████████░░░░░░░░] 60% (60/100)`
|
||||
- TTY mode: clears the current line and re-renders in-place via `clearLine` / `cursorTo`
|
||||
- Non-TTY mode: writes each update on its own line for readable CI output
|
||||
- `render()` returns the bar string without writing to the stream — useful for embedding in larger output
|
||||
- `complete()` forces 100% and appends a newline; idempotent (safe to call multiple times)
|
||||
- `AbortSignal` support: abort event listener marks the instance done; pre-fired signal respected via guard check in `update()`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { createProgress } from "@suaveplan/cli/progress/progress";
|
||||
|
||||
const p = createProgress({ total: 100 });
|
||||
|
||||
for (let i = 0; i <= 100; i++) {
|
||||
p.update(i);
|
||||
await sleep(20);
|
||||
}
|
||||
|
||||
p.complete();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { createProgress } from "@suaveplan/cli/progress/progress";
|
||||
import { Writable } from "node:stream";
|
||||
|
||||
const controller = new AbortController();
|
||||
const p = createProgress({
|
||||
total: files.length,
|
||||
width: 60,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
let processed = 0;
|
||||
for (const file of files) {
|
||||
if (controller.signal.aborted) break;
|
||||
await processFile(file);
|
||||
p.update(++processed);
|
||||
}
|
||||
|
||||
p.complete();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `createProgress(opts: ProgressOptions): ProgressInstance`
|
||||
|
||||
Creates a progress bar instance. No output is written until `update()` or `complete()` is called.
|
||||
|
||||
**`opts.total: number`** — the maximum value (100%). Must be positive.
|
||||
|
||||
**`opts.width?: number`** — width of the bar in characters (default `40`).
|
||||
|
||||
**`opts.stream?: WritableStreamLike`** — output target (default `process.stdout`). TTY in-place rendering requires `clearLine` and `cursorTo` methods plus `isTTY === true`.
|
||||
|
||||
**`opts.signal?: AbortSignal`** — when aborted, future `update()` calls become no-ops and the bar stops advancing.
|
||||
|
||||
### `ProgressInstance`
|
||||
|
||||
**`update(current: number): void`** — updates the displayed progress to `current`. Values are clamped to `[0, total]`. No-op if `complete()` was already called or the signal was aborted.
|
||||
|
||||
**`complete(): void`** — writes a 100% bar with a trailing newline and marks the instance done. Idempotent.
|
||||
|
||||
**`render(): string`** — returns the current bar string without writing to the stream.
|
||||
|
||||
### `ProgressOptionsSchema`
|
||||
|
||||
Zod schema for `ProgressOptions` — validates `total` (positive number) and `width` (optional positive number). Excludes `stream` and `signal`.
|
||||
|
||||
### `WritableStreamLike`
|
||||
|
||||
Re-exported from `../spinner/spinner.types.js`. Any object with `write(text: string): void` satisfies this interface; `clearLine`, `cursorTo`, and `isTTY` are optional and enable TTY mode when present.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Progress percentage is computed as `Math.min(1, current / total)` to handle over-total updates gracefully. The filled block count is `Math.round(pct * width)` — rounding avoids a full-bar display at 99% on small widths. The `complete()` guard (`done` flag) prevents double-writes when called multiple times. The `AbortSignal` listener uses `{ once: true }` so it self-removes after the first event.
|
||||
95
docs/devtools/tooling/cli/src/prompt/prompt.md
Normal file
95
docs/devtools/tooling/cli/src/prompt/prompt.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# prompt
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`prompt` provides a collection of pure, injectable interactive prompt functions for CLI applications: `text`, `password`, `confirm`, `select`, and `multiselect`. All five functions accept an `InputReader` as their last argument for testing and embedding — callers supply any object with `question(prompt): Promise<string>` and `close(): void`. In production, `createDefaultReader()` builds an `InputReader` backed by `@suaveplan/readline/interface`, which in turn reads from `process.stdin`.
|
||||
|
||||
Prompt functions are asynchronous, loop-safe, and validation-aware. They re-prompt on invalid input rather than throwing, keeping the user experience coherent across all terminal types.
|
||||
|
||||
## Features
|
||||
|
||||
- `text(opts, reader?)` — free-form text input with optional default and validation callback
|
||||
- `password(opts, reader?)` — masked input (structurally identical to text; masking is delegated to the terminal emulator or TTY mode)
|
||||
- `confirm(opts, reader?)` — yes/no boolean with optional default; accepts `y`, `yes`, `n`, `no` (case-insensitive)
|
||||
- `select(opts, reader?)` — numbered single-choice list; re-prompts on out-of-range input
|
||||
- `multiselect(opts, reader?)` — comma-separated multi-choice with configurable `min`/`max` and deduplication
|
||||
- `createDefaultReader()` — async factory returning an `InputReader` wired to `process.stdin` via `@suaveplan/readline`
|
||||
- All functions accept both a plain `string` message or a full options object for brevity
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { confirm, select, text } from "@suaveplan/cli/prompt/prompt";
|
||||
|
||||
const name = await text("Your name?");
|
||||
const confirmed = await confirm({ message: "Continue?", default: true });
|
||||
const role = await select({ message: "Pick a role:", choices: ["admin", "user", "guest"] });
|
||||
|
||||
console.log(name, confirmed, role);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { multiselect, text } from "@suaveplan/cli/prompt/prompt";
|
||||
import type { InputReader } from "@suaveplan/cli/prompt/prompt";
|
||||
|
||||
// Inject a reader for testing
|
||||
function mockReader(lines: string[]): InputReader {
|
||||
let i = 0;
|
||||
return { question: async () => lines[i++] ?? "", close: () => {} };
|
||||
}
|
||||
|
||||
const username = await text(
|
||||
{ message: "Username?", validate: (v) => v.length >= 3 || "at least 3 chars" },
|
||||
mockReader(["ab", "alice"]),
|
||||
);
|
||||
|
||||
const features = await multiselect(
|
||||
{ message: "Enable features:", choices: ["logging", "metrics", "tracing"], min: 1 },
|
||||
mockReader(["1,2"]),
|
||||
);
|
||||
|
||||
console.log(username, features);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `text(opts: TextPromptOptions | string, reader?: InputReader): Promise<string>`
|
||||
|
||||
Prompts for free-form text. When `opts` is a string it is used as the `message`. Returns the user's input or `opts.default` if the input is empty and a default is set. Re-prompts if `opts.validate` returns an error string.
|
||||
|
||||
### `password(opts: PasswordPromptOptions | string, reader?: InputReader): Promise<string>`
|
||||
|
||||
Identical interface to `text` but semantically for secret input. The caller is responsible for disabling echo on the actual terminal if needed.
|
||||
|
||||
### `confirm(opts: ConfirmPromptOptions | string, reader?: InputReader): Promise<boolean>`
|
||||
|
||||
Prompts for a yes/no answer. Accepts `y`, `yes`, `n`, `no` (case-insensitive). Returns `opts.default` when the user submits an empty line and a default is configured. Re-prompts on unrecognized input.
|
||||
|
||||
### `select<T>(opts: SelectPromptOptions<T>, reader?: InputReader): Promise<T>`
|
||||
|
||||
Displays a numbered list of choices and returns the `value` of the selected entry. Choices may be plain strings (label = value) or `SelectChoice<T>` objects. Re-prompts when the entered number is out of range.
|
||||
|
||||
### `multiselect<T>(opts: MultiSelectPromptOptions<T>, reader?: InputReader): Promise<T[]>`
|
||||
|
||||
Displays a numbered list and accepts comma-separated indices. Deduplicates repeated indices. Enforces `opts.min` / `opts.max` bounds and re-prompts when any index is invalid or the count is out of bounds.
|
||||
|
||||
### `createDefaultReader(): Promise<InputReader>`
|
||||
|
||||
Async factory that lazily imports `@suaveplan/readline/interface` and wires it to `process.stdin` / `process.stdout`. Returns an `InputReader`. The caller is responsible for calling `close()` when done.
|
||||
|
||||
### `InputReader`
|
||||
|
||||
Injectable interface: `{ question(prompt: string): Promise<string>; close(): void; }`. Pass a mock for tests.
|
||||
|
||||
### `TextPromptOptionsSchema`, `ConfirmPromptOptionsSchema`, `SelectChoiceSchema`
|
||||
|
||||
Zod schemas for the corresponding option types — useful for config-file driven prompts.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All prompt functions manage the reader lifecycle when no reader is injected: they call `createDefaultReader()` in a `try/finally` block that calls `close()` after the prompt resolves. When a reader is injected, the caller owns the lifecycle. Validation callbacks return either `true` (pass) or an error string (fail); the error is written to `process.stderr` before re-prompting. The `multiselect` function considers any out-of-range index a hard error and re-prompts rather than silently discarding it.
|
||||
99
docs/devtools/tooling/cli/src/spinner/spinner.md
Normal file
99
docs/devtools/tooling/cli/src/spinner/spinner.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# spinner
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`createSpinner` renders an animated terminal spinner for long-running operations. It writes a spinning frame animation to any `WritableStreamLike` output — TTY or non-TTY — and exposes a chainable API to start, stop, succeed, fail, or warn when the operation resolves. The spinner integrates with `AbortSignal` so it can be cancelled programmatically without leaking timers.
|
||||
|
||||
The spinner is entirely synchronous after creation. No promises are used internally — the animation is driven by a `setInterval` timer that is always cancelled on `stop()`.
|
||||
|
||||
## Features
|
||||
|
||||
- Animated frame loop with configurable frames array and interval (default 80ms, 10-frame braille spinner)
|
||||
- Chainable API: `start().succeed()`, `start().fail()`, `start().warn()`, `start().stop()`
|
||||
- TTY mode: clears current line and re-renders in-place via `clearLine`/`cursorTo`
|
||||
- Non-TTY mode (CI/pipes): writes each frame on its own line — no escape codes
|
||||
- `AbortSignal` support: live abort event listener (`"abort"`) stops the spinner; pre-fired abort prevents `start()` entirely
|
||||
- `succeed` / `fail` / `warn` write a terminal symbol (`✔` / `✖` / `⚠`) followed by the text
|
||||
- `update(text)` changes the displayed text without restarting the animation
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { createSpinner } from "@suaveplan/cli/spinner/spinner";
|
||||
|
||||
const sp = createSpinner({ text: "Fetching data…" });
|
||||
sp.start();
|
||||
await fetchData();
|
||||
sp.succeed("Done!");
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { createSpinner } from "@suaveplan/cli/spinner/spinner";
|
||||
|
||||
const controller = new AbortController();
|
||||
const sp = createSpinner({
|
||||
text: "Building…",
|
||||
frames: ["-", "\\", "|", "/"],
|
||||
interval: 120,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
sp.start();
|
||||
setTimeout(() => controller.abort(), 5000); // cancel after 5 s
|
||||
|
||||
try {
|
||||
await runBuild();
|
||||
sp.succeed("Build complete");
|
||||
} catch {
|
||||
sp.fail("Build failed");
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `createSpinner(opts?: SpinnerOptions): SpinnerInstance`
|
||||
|
||||
Creates and returns a spinner instance. No animation starts until `start()` is called.
|
||||
|
||||
**`opts.text?: string`** — initial text displayed next to the spinner frame (default `""`).
|
||||
|
||||
**`opts.frames?: string[]`** — animation frame sequence (default: 10-step braille pattern).
|
||||
|
||||
**`opts.interval?: number`** — milliseconds between frame advances (default `80`).
|
||||
|
||||
**`opts.stream?: WritableStreamLike`** — output target (default `process.stdout`). Any object with a `write()` method is accepted; TTY-specific methods (`clearLine`, `cursorTo`) are optional.
|
||||
|
||||
**`opts.signal?: AbortSignal`** — when aborted, calls `stop()` immediately. If already aborted at construction time, `start()` becomes a no-op.
|
||||
|
||||
### `SpinnerInstance`
|
||||
|
||||
**`isSpinning: boolean`** — `true` between `start()` and `stop()`.
|
||||
|
||||
**`start(text?: string): SpinnerInstance`** — begins animation. If a `text` argument is provided, updates the displayed text before starting. No-op if already spinning or if the signal is pre-aborted.
|
||||
|
||||
**`stop(): SpinnerInstance`** — stops animation and clears the line (TTY only). No-op if not spinning.
|
||||
|
||||
**`succeed(text?: string): SpinnerInstance`** — calls `stop()` then writes `✔ <text>` (falls back to current text).
|
||||
|
||||
**`fail(text?: string): SpinnerInstance`** — calls `stop()` then writes `✖ <text>`.
|
||||
|
||||
**`warn(text?: string): SpinnerInstance`** — calls `stop()` then writes `⚠ <text>`.
|
||||
|
||||
**`update(text: string): SpinnerInstance`** — replaces the displayed text without interrupting animation.
|
||||
|
||||
### `SpinnerOptionsSchema`
|
||||
|
||||
Zod schema for `SpinnerOptions` — validates `text`, `frames`, and `interval` (excludes `stream` and `signal` which are not Zod-serialisable).
|
||||
|
||||
### `WritableStreamLike`
|
||||
|
||||
Minimal interface for the `stream` option: any object with `write(text: string): void`. The optional `clearLine`, `cursorTo`, and `isTTY` members enable TTY in-place rendering when present.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The TTY branch detects `stream.isTTY === true` at construction time and uses `clearLine(0)` + `cursorTo(0)` + `write(frame + " " + text)` on each tick. The non-TTY branch appends a newline to each frame write so CI logs remain readable. The `setInterval` is stored and cleared in `stop()` to prevent timer leaks; calling `stop()` when not spinning is a safe no-op. The `AbortSignal` listener uses `{ once: true }` so it self-removes after the first abort event.
|
||||
82
docs/devtools/tooling/cli/src/style/style.md
Normal file
82
docs/devtools/tooling/cli/src/style/style.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# style
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`style` provides terminal ANSI colour and text-decoration utilities for `@suaveplan/cli`. It wraps text in ANSI escape sequences for modifiers (bold, dim, italic, underline, strikethrough) and colours (foreground and background). The module is colour-support-aware: when the terminal does not support colour, all styling is bypassed and plain text is returned unchanged.
|
||||
|
||||
The design uses a `Proxy`-based fluent builder that allows infinite chaining of style codes — `style.bold.red.underline("text")` — without allocating intermediate strings until the final call.
|
||||
|
||||
## Features
|
||||
|
||||
- 24 style codes: 6 modifiers, 9 foreground colours, 1 gray, 8 background colours
|
||||
- `createStyler(enabled?)` — factory for a fresh styler instance, injectable colour flag for testing
|
||||
- `style` — singleton styler wired to `isColorSupported()` at module load time
|
||||
- `stripAnsi(text)` — removes all ANSI escape sequences from a string
|
||||
- `isColorSupported()` — returns `true` when stdout is a TTY or `FORCE_COLOR` is set
|
||||
- `getStyleNames()` — returns the list of all valid style code names
|
||||
- Proxy-based chaining: `style.bold.red.underline("x")` composes all codes in a single call
|
||||
- Disabled mode: `createStyler(false)` returns identity function — no allocation overhead
|
||||
- Zero runtime dependencies outside the package
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { style } from "@suaveplan/cli/style/style";
|
||||
|
||||
console.log(style.bold("Build complete"));
|
||||
console.log(style.red("Error occurred"));
|
||||
console.log(style.green.bold("✓ Tests passed"));
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { createStyler, isColorSupported, stripAnsi } from "@suaveplan/cli/style/style";
|
||||
|
||||
// Inject colour support for testing
|
||||
const s = createStyler(process.env.CI !== "true");
|
||||
|
||||
function label(text: string, type: "info" | "warn" | "error"): string {
|
||||
if (type === "error") return s.bold.red(`[ERROR] ${text}`);
|
||||
if (type === "warn") return s.yellow(`[WARN] ${text}`);
|
||||
return s.cyan(`[INFO] ${text}`);
|
||||
}
|
||||
|
||||
console.log(label("Connection timed out", "error"));
|
||||
|
||||
// Strip for log files
|
||||
const plain = stripAnsi(label("hello", "info"));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `createStyler(enabled?: boolean): Styler`
|
||||
|
||||
Creates a new styler instance. `enabled` defaults to `isColorSupported()`. Returns a `Styler` (Proxy) that composes ANSI codes on chained property access and applies them on call.
|
||||
|
||||
### `style: Styler`
|
||||
|
||||
Pre-built singleton styler bound to the current terminal's colour support. Use this for production output.
|
||||
|
||||
### `stripAnsi(text: string): string`
|
||||
|
||||
Strips all ANSI escape sequences from `text`. Safe for log files and length calculations.
|
||||
|
||||
### `isColorSupported(): boolean`
|
||||
|
||||
Returns `true` when `process.stdout.isTTY` is true or `FORCE_COLOR` env var is set.
|
||||
|
||||
### `getStyleNames(): StyleCode[]`
|
||||
|
||||
Returns the array of all known style code names in order.
|
||||
|
||||
### `Styler`
|
||||
|
||||
A callable type with all style codes as chainable properties. Calling it with a string applies all accumulated codes and returns the styled string.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `Proxy` trap intercepts property access on the styler function itself, returning a new `Proxy`-wrapped function with the accessed code appended to the accumulated stack. This avoids the class-per-style-combination explosion of naive implementations. Opening and closing ANSI codes are concatenated from the stack at call time. The `reset` code (`\x1b[0m`) is handled like any other code — callers can chain `.reset` to reset all styles.
|
||||
Reference in New Issue
Block a user